diff --git a/.circleci/config.yml b/.circleci/config.yml index 6a1d4db48..e7a36f5b0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -5,7 +5,7 @@ orbs: browser-tools: circleci/browser-tools@1.4.1 aliases: - &ruby_node_browsers_docker_image - - image: cimg/ruby:3.2.1-browsers + - image: cimg/ruby:3.2.2-browsers environment: PGHOST: localhost PGUSER: untitled_application @@ -29,7 +29,7 @@ jobs: steps: - checkout - browser-tools/install-browser-tools: - firefox-version: 108.0.1 + firefox-version: "112.0" - ruby/install-deps: clean-bundle: true @@ -62,7 +62,7 @@ jobs: - checkout: path: ~/project - browser-tools/install-browser-tools: - firefox-version: 108.0.1 + firefox-version: "112.0" # TODO: This is a workaround to get `git clone` working, but it shouldn't be here. # https://github.com/CircleCI-Public/browser-tools-orb/issues/62 @@ -114,7 +114,7 @@ jobs: - checkout: path: ~/project - browser-tools/install-browser-tools: - firefox-version: 108.0.1 + firefox-version: "112.0" # TODO: This is a workaround to get `git clone` working, but it shouldn't be here. # https://github.com/CircleCI-Public/browser-tools-orb/issues/62 diff --git a/.standard.yml b/.standard.yml index 1f3be5330..0dae34fd5 100644 --- a/.standard.yml +++ b/.standard.yml @@ -22,13 +22,5 @@ ignore: - Lint/RescueException # TODO would it be okay to rescue `StandardError`? - '*/lib/scaffolding/transformer.rb': - Layout/EndAlignment - # TODO Fix these files up for Standard Ruby. - - '*/lib/bullet_train/super_scaffolding/scaffolders/oauth_provider_scaffolder.rb' - - '*/lib/tasks/bullet_train/themes/light_tasks.rake': - - Style/CommandLiteral - '*/config/routes.rb': - Lint/UselessAssignment - - '*/app/helpers/theme_helper.rb': - - Style/GlobalVars - - '*/app/models/concerns/webhooks/outgoing/uri_filtering.rb': - - Lint/ShadowedException diff --git a/Gemfile b/Gemfile index d5d0c3bc7..4a0dacc4f 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source "https://rubygems.org" git_source(:github) { |repo| "https://github.com/#{repo}.git" } -ruby "3.2.1" +ruby "3.2.2" gem "standard" diff --git a/Gemfile.lock b/Gemfile.lock index a65e56af1..07c29b3b0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -35,13 +35,15 @@ GEM PLATFORMS arm64-darwin-20 arm64-darwin-21 + arm64-darwin-22 + ruby x86_64-linux DEPENDENCIES standard RUBY VERSION - ruby 3.2.1p31 + ruby 3.2.2p53 BUNDLED WITH 2.3.8 diff --git a/bin/checkout-and-link-starter-repo b/bin/checkout-and-link-starter-repo index 7900654a9..3438c528e 100755 --- a/bin/checkout-and-link-starter-repo +++ b/bin/checkout-and-link-starter-repo @@ -5,10 +5,15 @@ STARTER_REPO_BRANCH="main" # Look for a matching branch on the starter repository when running tests on CircleCI. CI_BRANCH=$CIRCLE_BRANCH -if [[ -v CI_BRANCH ]]; then - BRANCH_RESPONSE=$(curl --head -H "Accept: application.vnd.github+json" https://api.github.com/repos/bullet-train-co/bullet_train/branches/$CI_BRANCH) +if [[ -v CI_BRANCH ]] +then + BRANCH_RESPONSE=$(curl --verbose -H "Accept: application.vnd.github+json" https://api.github.com/repos/bullet-train-co/bullet_train/branches/$CI_BRANCH) - if echo $BRANCH_RESPONSE | grep "200"; then + echo "Branch response ====================" + echo $BRANCH_RESPONSE + + # If the branch is missing in the repo the response will not contain the branch name + if echo $BRANCH_RESPONSE | grep "$CIRCLE_BRANCH"; then STARTER_REPO_BRANCH=$CI_BRANCH fi fi @@ -16,6 +21,7 @@ fi echo "Cloning from ${STARTER_REPO_BRANCH}..." git clone -b $STARTER_REPO_BRANCH --depth 1 https://github.com/bullet-train-co/bullet_train.git . +# TODO: Maybe generate this list automatically based on the subdirectories in core that contain a .gemspec? packages=( "bullet_train" "bullet_train-api" diff --git a/bullet_train-api/.circleci/config.yml b/bullet_train-api/.circleci/config.yml index 6414b4b77..05f6b4988 100644 --- a/bullet_train-api/.circleci/config.yml +++ b/bullet_train-api/.circleci/config.yml @@ -26,7 +26,7 @@ aliases: paths: - node_modules - &ruby_node_browsers_docker_image - - image: cimg/ruby:3.1.2-browsers + - image: cimg/ruby:3.2.2-browsers environment: PGHOST: localhost PGUSER: untitled_application diff --git a/bullet_train-api/app/controllers/account/platform/access_tokens_controller.rb b/bullet_train-api/app/controllers/account/platform/access_tokens_controller.rb index 597fe8471..9fa5d5b46 100644 --- a/bullet_train-api/app/controllers/account/platform/access_tokens_controller.rb +++ b/bullet_train-api/app/controllers/account/platform/access_tokens_controller.rb @@ -70,7 +70,6 @@ def destroy include strong_parameters_from_api def process_params(strong_params) - assign_date_and_time(strong_params, :last_used_at) # π super scaffolding will insert processing for new fields above this line. end end diff --git a/bullet_train-api/app/controllers/api/open_api_controller.rb b/bullet_train-api/app/controllers/api/open_api_controller.rb index 29455208a..78b239d0b 100644 --- a/bullet_train-api/app/controllers/api/open_api_controller.rb +++ b/bullet_train-api/app/controllers/api/open_api_controller.rb @@ -1,128 +1,5 @@ -module OpenApiHelper - def indent(string, count) - lines = string.lines - first_line = lines.shift - lines = lines.map { |line| (" " * count).to_s + line } - lines.unshift(first_line).join.html_safe - end - - # TODO: Remove this method? It's not being used anywhere - def components_for(model) - for_model model do - indent(render("api/#{@version}/open_api/#{model.name.underscore.pluralize}/components"), 2) - end - end - - def current_model - @model_stack.last - end - - def for_model(model) - @model_stack ||= [] - @model_stack << model - result = yield - @model_stack.pop - result - end - - def gem_paths - @gem_paths ||= `bundle show --paths`.lines.map { |gem_path| gem_path.chomp } - end - - def automatic_paths_for(model, parent, except: []) - output = render("api/#{@version}/open_api/shared/paths", except: except) - output = Scaffolding::Transformer.new(model.name, [parent&.name]).transform_string(output).html_safe - indent(output, 1) - end - - def automatic_components_for(model, locals: {}) - path = "app/views/api/#{@version}" - paths = ([path] + gem_paths.map { |gem_path| "#{gem_path}/#{path}" }) - jbuilder = Jbuilder::Schema.renderer(paths, locals: { - # If we ever get to the point where we need a real model here, we should implement an example team in seeds that we can source it from. - model.name.underscore.split("/").last.to_sym => model.new, - # Same here, if we ever need this to be a real object, this should be `test@example.com` with an `SecureRandom.hex` password. - :current_user => User.new - }.merge(locals)) - - schema_json = jbuilder.json( - model.new, - title: I18n.t("#{model.name.underscore.pluralize}.label"), - # TODO Improve this. We don't have a generic description for models we can use here. - description: I18n.t("#{model.name.underscore.pluralize}.label"), - ) - - attributes_output = JSON.parse(schema_json) - - # Rails attachments aren't technically attributes in a model, - # so we add the attributes manually to make them available in the API. - if model.attachment_reflections.any? - model.attachment_reflections.each do |reflection| - attribute_name = reflection.first - - attributes_output["properties"][attribute_name] = { - "type" => "object", - "description" => attribute_name.titleize.to_s - } - - attributes_output["example"].merge!({attribute_name.to_s => nil}) - end - end - - if has_strong_parameters?("Api::#{@version.upcase}::#{model.name.pluralize}Controller".constantize) - strong_params_module = "Api::#{@version.upcase}::#{model.name.pluralize}Controller::StrongParameters".constantize - strong_parameter_keys = BulletTrain::Api::StrongParametersReporter.new(model, strong_params_module).report - if strong_parameter_keys.last.is_a?(Hash) - strong_parameter_keys += strong_parameter_keys.pop.keys - end - - parameters_output = JSON.parse(schema_json) - parameters_output["required"].select! { |key| strong_parameter_keys.include?(key.to_sym) } - parameters_output["properties"].select! { |key, value| strong_parameter_keys.include?(key.to_sym) } - - ( - indent(attributes_output.to_yaml.gsub("---", "#{model.name.gsub("::", "")}Attributes:"), 3) + - indent(" " + parameters_output.to_yaml.gsub("---", "#{model.name.gsub("::", "")}Parameters:"), 3) - ).html_safe - else - - indent(attributes_output.to_yaml.gsub("---", "#{model.name.gsub("::", "")}Attributes:"), 3) - .html_safe - end - end - - def paths_for(model) - for_model model do - indent(render("api/#{@version}/open_api/#{model.name.underscore.pluralize}/paths"), 1) - end - end - - def attribute(attribute) - heading = t("#{current_model.name.underscore.pluralize}.fields.#{attribute}.heading") - attribute_data = current_model.columns_hash[attribute.to_s] - - # Default to `string` when the type returns nil. - type = attribute_data.nil? ? "string" : attribute_data.type - - attribute_block = <<~YAML - #{attribute}: - description: "#{heading}" - type: #{type} - YAML - indent(attribute_block.chomp, 2) - end - alias_method :parameter, :attribute - - private - - def has_strong_parameters?(controller) - methods = controller.action_methods - methods.include?("create") || methods.include?("update") - end -end - class Api::OpenApiController < ApplicationController - helper :open_api + helper "api/open_api" def set_default_response_format request.format = :yaml diff --git a/bullet_train-api/app/controllers/concerns/api/controllers/base.rb b/bullet_train-api/app/controllers/concerns/api/controllers/base.rb index 33d00618b..e18f498bd 100644 --- a/bullet_train-api/app/controllers/concerns/api/controllers/base.rb +++ b/bullet_train-api/app/controllers/concerns/api/controllers/base.rb @@ -72,7 +72,6 @@ def current_user end # TODO Remove this rescue once workspace clusters can write to this column on the identity server. - # TODO Make this logic configurable so that downstream developers can write different methods for this column getting updated. if doorkeeper_token begin doorkeeper_token.update(last_used_at: Time.zone.now) diff --git a/bullet_train-api/app/controllers/concerns/api/v1/users/controller_base.rb b/bullet_train-api/app/controllers/concerns/api/v1/users/controller_base.rb index 695126e32..014a12ce5 100644 --- a/bullet_train-api/app/controllers/concerns/api/v1/users/controller_base.rb +++ b/bullet_train-api/app/controllers/concerns/api/v1/users/controller_base.rb @@ -14,7 +14,8 @@ def user_params :first_name, :last_name, :time_zone, - :locale + :locale, + :profile_photo_id ] selected_fields = if params.is_a?(BulletTrain::Api::StrongParametersReporter) diff --git a/bullet_train-api/app/helpers/api/open_api_helper.rb b/bullet_train-api/app/helpers/api/open_api_helper.rb new file mode 100644 index 000000000..80f50a1ef --- /dev/null +++ b/bullet_train-api/app/helpers/api/open_api_helper.rb @@ -0,0 +1,151 @@ +module Api + module OpenApiHelper + def indent(string, count) + lines = string.lines + first_line = lines.shift + lines = lines.map { |line| (" " * count).to_s + line } + lines.unshift(first_line).join.html_safe + end + + # TODO: Remove this method? It's not being used anywhere + def components_for(model) + for_model model do + indent(render("api/#{@version}/open_api/#{model.name.underscore.pluralize}/components"), 2) + end + end + + def current_model + @model_stack.last + end + + def for_model(model) + @model_stack ||= [] + @model_stack << model + result = yield + @model_stack.pop + result + end + + def gem_paths + @gem_paths ||= `bundle show --paths`.lines.map { |gem_path| gem_path.chomp } + end + + def automatic_paths_for(model, parent, except: []) + output = render("api/#{@version}/open_api/shared/paths", except: except) + output = Scaffolding::Transformer.new(model.name, [parent&.name]).transform_string(output).html_safe + + custom_actions_file_path = "api/#{@version}/open_api/#{model.name.underscore.pluralize}/paths" + output += render(custom_actions_file_path) if lookup_context.exists?(custom_actions_file_path, [], true) + + # There are some placeholders specific to this method that we still need to transform. + model_symbol = model.name.underscore.tr("/", "_").to_sym + + if (get_example = FactoryBot.get_example(model_symbol, version: @version)) + output.gsub!("π get_example", get_example) + end + + if (post_parameters = FactoryBot.post_parameters(model_symbol, version: @version)) + output.gsub!("π post_parameters", post_parameters) + end + + if (post_examples = FactoryBot.post_examples(model_symbol, version: @version)) + output.gsub!("π post_examples", post_examples) + end + + if (put_parameters = FactoryBot.put_parameters(model_symbol, version: @version)) + output.gsub!("π put_parameters", put_parameters) + end + + if (put_example = FactoryBot.put_example(model_symbol, version: @version)) + output.gsub!("π put_example", put_example) + end + + indent(output, 1) + end + + def automatic_components_for(model, locals: {}) + path = "app/views/api/#{@version}" + paths = ([path] + gem_paths.map { |gem_path| "#{gem_path}/#{path}" }) + jbuilder = Jbuilder::Schema.renderer(paths, locals: { + # If we ever get to the point where we need a real model here, we should implement an example team in seeds that we can source it from. + model.name.underscore.split("/").last.to_sym => model.new, + # Same here, if we ever need this to be a real object, this should be `test@example.com` with an `SecureRandom.hex` password. + :current_user => User.new + }.merge(locals)) + + schema_json = jbuilder.json( + model.new, + title: I18n.t("#{model.name.underscore.pluralize}.label"), + # TODO Improve this. We don't have a generic description for models we can use here. + description: I18n.t("#{model.name.underscore.pluralize}.label"), + ) + + attributes_output = JSON.parse(schema_json) + + # Rails attachments aren't technically attributes in a model, + # so we add the attributes manually to make them available in the API. + if model.attachment_reflections.any? + model.attachment_reflections.each do |reflection| + attribute_name = reflection.first + + attributes_output["properties"][attribute_name] = { + "type" => "object", + "description" => attribute_name.titleize.to_s + } + + attributes_output["example"].merge!({attribute_name.to_s => nil}) + end + end + + if has_strong_parameters?("Api::#{@version.upcase}::#{model.name.pluralize}Controller".constantize) + strong_params_module = "Api::#{@version.upcase}::#{model.name.pluralize}Controller::StrongParameters".constantize + strong_parameter_keys = BulletTrain::Api::StrongParametersReporter.new(model, strong_params_module).report + if strong_parameter_keys.last.is_a?(Hash) + strong_parameter_keys += strong_parameter_keys.pop.keys + end + + parameters_output = JSON.parse(schema_json) + parameters_output["required"].select! { |key| strong_parameter_keys.include?(key.to_sym) } + parameters_output["properties"].select! { |key, value| strong_parameter_keys.include?(key.to_sym) } + + ( + indent(attributes_output.to_yaml.gsub("---", "#{model.name.gsub("::", "")}Attributes:"), 3) + + indent(" " + parameters_output.to_yaml.gsub("---", "#{model.name.gsub("::", "")}Parameters:"), 3) + ).html_safe + else + + indent(attributes_output.to_yaml.gsub("---", "#{model.name.gsub("::", "")}Attributes:"), 3) + .html_safe + end + end + + def paths_for(model) + for_model model do + indent(render("api/#{@version}/open_api/#{model.name.underscore.pluralize}/paths"), 1) + end + end + + def attribute(attribute) + heading = t("#{current_model.name.underscore.pluralize}.fields.#{attribute}.heading") + attribute_data = current_model.columns_hash[attribute.to_s] + + # Default to `string` when the type returns nil. + type = attribute_data.nil? ? "string" : attribute_data.type + + attribute_block = <<~YAML + #{attribute}: + description: "#{heading}" + type: #{type} + YAML + indent(attribute_block.chomp, 2) + end + alias_method :parameter, :attribute + + private + + def has_strong_parameters?(controller) + methods = controller.action_methods + methods.include?("create") || methods.include?("update") + end + end +end diff --git a/bullet_train-api/app/models/platform/access_token.rb b/bullet_train-api/app/models/platform/access_token.rb index 6bd8807dc..e80198fa5 100644 --- a/bullet_train-api/app/models/platform/access_token.rb +++ b/bullet_train-api/app/models/platform/access_token.rb @@ -1,4 +1,4 @@ -class Platform::AccessToken < ApplicationRecord +class Platform::AccessToken < BulletTrain::Api.base_class.constantize self.table_name = "oauth_access_tokens" include Doorkeeper::Orm::ActiveRecord::Mixins::AccessToken diff --git a/bullet_train-api/app/models/platform/application.rb b/bullet_train-api/app/models/platform/application.rb index cc6a8f6dd..d0eedefa1 100644 --- a/bullet_train-api/app/models/platform/application.rb +++ b/bullet_train-api/app/models/platform/application.rb @@ -1,4 +1,4 @@ -class Platform::Application < ApplicationRecord +class Platform::Application < BulletTrain::Api.base_class.constantize self.table_name = "oauth_applications" include Doorkeeper::Orm::ActiveRecord::Mixins::Application diff --git a/bullet_train-api/app/views/account/platform/access_tokens/_index.html.erb b/bullet_train-api/app/views/account/platform/access_tokens/_index.html.erb index 63e611825..897bdd98d 100644 --- a/bullet_train-api/app/views/account/platform/access_tokens/_index.html.erb +++ b/bullet_train-api/app/views/account/platform/access_tokens/_index.html.erb @@ -8,7 +8,7 @@ <% pagy, access_tokens = pagy(access_tokens, page_param: :access_tokens_page) %> <%= action_model_select_controller do %> - <%= updates_for context, collection do %> + <%= cable_ready_updates_for context, collection do %> <%= render 'account/shared/box', pagy: pagy do |box| %> <% box.title t(".contexts.#{context.class.name.underscore}.header") %> <% box.description t(".contexts.#{context.class.name.underscore}.description") %> diff --git a/bullet_train-api/app/views/account/platform/access_tokens/new.html.erb b/bullet_train-api/app/views/account/platform/access_tokens/new.html.erb index 909322550..8c2af2407 100644 --- a/bullet_train-api/app/views/account/platform/access_tokens/new.html.erb +++ b/bullet_train-api/app/views/account/platform/access_tokens/new.html.erb @@ -1,7 +1,7 @@ <%= render 'account/shared/page' do |page| %> <% page.title t('.section') %> <% page.body.render 'account/shared/box', divider: true do |box| %> - <% box.t :description, title: t('.header') %> + <% box.t :description, title: '.header' %> <% box.body.render 'form', access_token: @access_token %> <% end %> <% end %> diff --git a/bullet_train-api/app/views/account/platform/access_tokens/show.html.erb b/bullet_train-api/app/views/account/platform/access_tokens/show.html.erb index 7b3ed546b..ee4d713ce 100644 --- a/bullet_train-api/app/views/account/platform/access_tokens/show.html.erb +++ b/bullet_train-api/app/views/account/platform/access_tokens/show.html.erb @@ -1,7 +1,7 @@ <%= render 'account/shared/page' do |page| %> <% page.title t('.section') %> <% page.body do %> - <%= updates_for @access_token do %> + <%= cable_ready_updates_for @access_token do %> <%= render 'account/shared/box', divider: true do |box| %> <% box.title t('.header') %> <% box.description do %> diff --git a/bullet_train-api/app/views/api/v1/open_api/shared/_paths.yaml.erb b/bullet_train-api/app/views/api/v1/open_api/shared/_paths.yaml.erb index 9fe0d0216..d08ee6a4e 100644 --- a/bullet_train-api/app/views/api/v1/open_api/shared/_paths.yaml.erb +++ b/bullet_train-api/app/views/api/v1/open_api/shared/_paths.yaml.erb @@ -4,7 +4,7 @@ <% unless except.include?(:index) %> get: tags: - - "Scaffolding/Completely Concrete/Tangible Things" + - Scaffolding::CompletelyConcrete::TangibleThing summary: "List Tangible Things" operationId: listScaffoldingCompletelyConcreteTangibleThings parameters: @@ -30,11 +30,13 @@ type: array items: $ref: "#/components/schemas/ScaffoldingCompletelyConcreteTangibleThingAttributes" + example: + π get_example <% end %> <% unless except.include?(:create) %> post: tags: - - "Scaffolding/Completely Concrete/Tangible Things" + - Scaffolding::CompletelyConcrete::TangibleThing summary: "Create Tangible Thing" operationId: createScaffoldingCompletelyConcreteTangibleThings parameters: @@ -54,6 +56,8 @@ scaffolding_completely_concrete_tangible_thing: type: object $ref: "#/components/schemas/ScaffoldingCompletelyConcreteTangibleThingParameters" + example: + π post_parameters responses: "404": description: "Not Found" @@ -63,6 +67,8 @@ application/json: schema: $ref: "#/components/schemas/ScaffoldingCompletelyConcreteTangibleThingAttributes" + example: + π post_examples <% end %> <% end %> <% unless except.include?(:show) && except.include?(:update) && except.include?(:destroy) %> @@ -70,7 +76,7 @@ <% unless except.include?(:show) %> get: tags: - - "Scaffolding/Completely Concrete/Tangible Things" + - Scaffolding::CompletelyConcrete::TangibleThing summary: "Fetch Tangible Thing" operationId: getScaffoldingCompletelyConcreteTangibleThings parameters: @@ -84,11 +90,13 @@ application/json: schema: $ref: "#/components/schemas/ScaffoldingCompletelyConcreteTangibleThingAttributes" + example: + π get_example <% end %> <% unless except.include?(:update) %> put: tags: - - "Scaffolding/Completely Concrete/Tangible Things" + - Scaffolding::CompletelyConcrete::TangibleThing summary: "Update Tangible Thing" operationId: updateScaffoldingCompletelyConcreteTangibleThings parameters: @@ -104,6 +112,8 @@ scaffolding_completely_concrete_tangible_thing: type: object $ref: "#/components/schemas/ScaffoldingCompletelyConcreteTangibleThingParameters" + example: + π put_parameters responses: "404": description: "Not Found" @@ -113,11 +123,13 @@ application/json: schema: $ref: "#/components/schemas/ScaffoldingCompletelyConcreteTangibleThingAttributes" + example: + π put_example <% end %> <% unless except.include?(:destroy) %> delete: tags: - - "Scaffolding/Completely Concrete/Tangible Things" + - Scaffolding::CompletelyConcrete::TangibleThing summary: "Remove Tangible Thing" operationId: removeScaffoldingCompletelyConcreteTangibleThings parameters: diff --git a/bullet_train-api/app/views/api/v1/open_api/teams/_paths.yaml.erb b/bullet_train-api/app/views/api/v1/open_api/teams/_paths.yaml.erb index 638f0d9d8..a2f63c204 100644 --- a/bullet_train-api/app/views/api/v1/open_api/teams/_paths.yaml.erb +++ b/bullet_train-api/app/views/api/v1/open_api/teams/_paths.yaml.erb @@ -1,7 +1,7 @@ /teams: get: tags: - - Teams + - Team summary: "List Teams" operationId: listTeams parameters: @@ -22,10 +22,13 @@ type: array items: $ref: "#/components/schemas/TeamAttributes" + example: + <%= FactoryBot.get_examples(:team, version: @version) %> + /teams/{id}: get: tags: - - Teams + - Team summary: "Fetch Team" operationId: fetchTeam parameters: @@ -39,9 +42,11 @@ application/json: schema: $ref: "#/components/schemas/TeamAttributes" + example: + <%= FactoryBot.get_example(:team, version: @version) %> put: tags: - - Teams + - Team summary: "Update Team" operationId: updateTeam parameters: @@ -57,6 +62,8 @@ team: type: object $ref: "#/components/schemas/TeamParameters" + example: + <%= FactoryBot.put_parameters(:team, version: @version) %> responses: "404": description: "Not Found" @@ -66,3 +73,5 @@ application/json: schema: $ref: "#/components/schemas/TeamAttributes" + example: + <%= FactoryBot.put_example(:team, version: @version) %> diff --git a/bullet_train-api/app/views/api/v1/open_api/users/_paths.yaml.erb b/bullet_train-api/app/views/api/v1/open_api/users/_paths.yaml.erb index 409946080..82c7e31e0 100644 --- a/bullet_train-api/app/views/api/v1/open_api/users/_paths.yaml.erb +++ b/bullet_train-api/app/views/api/v1/open_api/users/_paths.yaml.erb @@ -1,7 +1,7 @@ /users: get: tags: - - Users + - User summary: "List Users" operationId: listUsers parameters: @@ -22,10 +22,12 @@ type: array items: $ref: "#/components/schemas/UserAttributes" + example: + <%= FactoryBot.get_examples(:user, version: @version) %> /users/{id}: get: tags: - - Users + - User summary: "Fetch User" operationId: fetchUser parameters: @@ -39,9 +41,11 @@ application/json: schema: $ref: "#/components/schemas/UserAttributes" + example: + <%= FactoryBot.get_example(:user, version: @version) %> put: tags: - - Users + - User summary: "Update User" operationId: updateUser parameters: @@ -57,6 +61,8 @@ user: type: object $ref: "#/components/schemas/UserParameters" + example: + <%= FactoryBot.put_parameters(:user, version: @version) %> responses: "404": description: "Not Found" @@ -66,3 +72,5 @@ application/json: schema: $ref: "#/components/schemas/UserAttributes" + example: + <%= FactoryBot.put_example(:user, version: @version) %> diff --git a/bullet_train-api/bullet_train-api.gemspec b/bullet_train-api/bullet_train-api.gemspec index 4dc40daae..8280dfa62 100644 --- a/bullet_train-api/bullet_train-api.gemspec +++ b/bullet_train-api/bullet_train-api.gemspec @@ -5,7 +5,7 @@ Gem::Specification.new do |spec| spec.version = BulletTrain::Api::VERSION spec.authors = ["Andrew Culver"] spec.email = ["andrew.culver@gmail.com"] - spec.homepage = "https://github.com/bullet-train-co/bullet_train-api" + spec.homepage = "https://github.com/bullet-train-co/bullet_train-core/tree/main/bullet_train-api" spec.summary = "Bullet Train API" spec.description = spec.summary spec.license = "MIT" @@ -30,6 +30,7 @@ Gem::Specification.new do |spec| spec.add_dependency "rack-cors" spec.add_dependency "doorkeeper" spec.add_dependency "jbuilder-schema", ">= 2.0.0" + spec.add_dependency "factory_bot" spec.add_dependency "bullet_train" end diff --git a/bullet_train-api/config/routes.rb b/bullet_train-api/config/routes.rb index f35620dce..09ac685f2 100644 --- a/bullet_train-api/config/routes.rb +++ b/bullet_train-api/config/routes.rb @@ -17,9 +17,7 @@ end end - if ENV["TESTING_PROVISION_KEY"].present? - get "/testing/provision", to: "account/platform/applications#provision" - end + get "/testing/provision", to: "account/platform/applications#provision" namespace :api do match "*version/openapi.yaml" => "open_api#index", :via => :get diff --git a/bullet_train-api/lib/bullet_train/api.rb b/bullet_train-api/lib/bullet_train/api.rb index 77f0b8ab9..1f64ccb97 100644 --- a/bullet_train-api/lib/bullet_train/api.rb +++ b/bullet_train-api/lib/bullet_train/api.rb @@ -1,6 +1,7 @@ require "bullet_train/api/version" require "bullet_train/api/engine" require "bullet_train/api/strong_parameters_reporter" +require "bullet_train/api/example_bot" require "bullet_train/platform/connection_workflow" # require "wine_bouncer" @@ -15,6 +16,7 @@ module BulletTrain module Api + mattr_accessor :base_class, default: "ApplicationRecord" mattr_accessor :endpoints, default: [] mattr_accessor :current_version, default: "v1" mattr_accessor :initial_version, default: "v1" diff --git a/bullet_train-api/lib/bullet_train/api/example_bot.rb b/bullet_train-api/lib/bullet_train/api/example_bot.rb new file mode 100644 index 000000000..229e21b81 --- /dev/null +++ b/bullet_train-api/lib/bullet_train/api/example_bot.rb @@ -0,0 +1,146 @@ +require_relative "../../../app/helpers/api/open_api_helper" + +module FactoryBot + module ExampleBot + attr_accessor :tables_to_reset + + def example(model, **options) + @tables_to_reset = [model.to_s.pluralize] + + object = nil + + ActiveRecord::Base.transaction do + instance = FactoryBot.create(factory(model), **options) + object = deep_clone(instance) + + raise ActiveRecord::Rollback + end + + reset_tables! + object + end + + def example_list(model, quantity, **options) + @tables_to_reset = [model.to_s.pluralize] + + objects = [] + + ActiveRecord::Base.transaction do + instances = FactoryBot.create_list(factory(model), quantity, **options) + + instances.each do |instance| + objects << deep_clone(instance) + end + + raise ActiveRecord::Rollback + end + + reset_tables! + objects + end + + %i[get_examples get_example post_examples post_parameters put_example put_parameters patch_example patch_parameters].each do |method| + define_method(method) do |model, **options| + _path_examples(method.to_s, model, **options) + end + end + + private + + def factory(model) + factories = FactoryBot.factories.instance_variable_get(:@items).keys + factories.include?("#{model}_example") ? "#{model}_example".to_sym : model + end + + def reset_tables! + @tables_to_reset.each do |name| + ActiveRecord::Base.connection.reset_pk_sequence!(name) if ActiveRecord::Base.connection.table_exists?(name) + end + end + + def deep_clone(instance) + clone = instance.clone + + instance.class.reflections.each do |name, reflection| + if reflection.macro == :has_many + associations = instance.send(name).map { |association| association.clone } + clone.send("#{name}=", associations) + @tables_to_reset << name + elsif %i[belongs_to has_one].include?(reflection.macro) + clone.send("#{name}=", instance.send(name).clone) + @tables_to_reset << name.pluralize + end + end + + clone + end + + include ::Api::OpenApiHelper + def _path_examples(method, model, **options) + version = options.delete(:version) || "v1" + + case method.split("_").first + when "get" + count = (options.delete(:count) || method == "get_examples") ? 2 : 1 + template, class_name, var_name, values = _set_values(method, model, count) + else + template, class_name, var_name, values = _set_values("get_example", model) + + unless %w[example examples].include?(method.split("_").last) + if has_strong_parameters?("::Api::#{version.upcase}::#{class_name.pluralize}Controller".constantize) + strong_params_module = "::Api::#{version.upcase}::#{class_name.pluralize}Controller::StrongParameters".constantize + strong_parameter_keys = BulletTrain::Api::StrongParametersReporter.new(class_name.constantize, strong_params_module).report + if strong_parameter_keys.last.is_a?(Hash) + strong_parameter_keys += strong_parameter_keys.pop.keys + end + + output = _json_output(template, version, class_name, var_name, values) + + parameters_output = JSON.parse(output) + parameters_output&.select! { |key| strong_parameter_keys.include?(key.to_sym) } + + return indent(parameters_output.to_yaml.delete_prefix("---\n"), 6).html_safe + end + return nil + end + end + + _yaml_output(template, version, class_name, var_name, values) + end + + def _set_values(method, model, count = 1) + if count > 1 + values = FactoryBot.example_list(model, count) + class_name = values.first.class.name + var_name = class_name.demodulize.underscore.pluralize + else + values = FactoryBot.example(model) + class_name = values.class.name + var_name = class_name.demodulize.underscore + end + + template = (method == "get_examples") ? "index" : "show" + + [template, class_name, var_name, values] + end + + def _json_output(template, version, class_name, var_name, values) + ActionController::Base.render( + template: "api/#{version}/#{class_name.underscore.pluralize}/#{template}", + assigns: {"#{var_name}": values}, + formats: :json + ) + end + + def _yaml_output(template, version, class_name, var_name, values) + indent( + JSON.parse( + _json_output(template, version, class_name, var_name, values) + ).to_yaml + .delete_prefix("---\n"), 7 + ).html_safe + end + end + + extend ExampleBot +end diff --git a/bullet_train-api/lib/bullet_train/api/version.rb b/bullet_train-api/lib/bullet_train/api/version.rb index 2a8ac751a..204156b52 100644 --- a/bullet_train-api/lib/bullet_train/api/version.rb +++ b/bullet_train-api/lib/bullet_train/api/version.rb @@ -1,5 +1,5 @@ module BulletTrain module Api - VERSION = "1.2.21" + VERSION = "1.2.27" end end diff --git a/bullet_train-api/lib/tasks/bullet_train/api_tasks.rake b/bullet_train-api/lib/tasks/bullet_train/api_tasks.rake index f88f2a971..2e7becd9c 100644 --- a/bullet_train-api/lib/tasks/bullet_train/api_tasks.rake +++ b/bullet_train-api/lib/tasks/bullet_train/api_tasks.rake @@ -1,6 +1,9 @@ require "scaffolding" require "scaffolding/file_manipulator" +require "faraday" +require "tempfile" + namespace :bullet_train do namespace :api do desc "Bump the current version of application's API" @@ -88,5 +91,42 @@ namespace :bullet_train do puts "Finished bumping to #{new_version}" end + + desc "Bump the current version of application's API" + task push_to_redocly: :environment do + include Rails.application.routes.url_helpers + + raise "You need to set REDOCLY_ORGANIZATION_ID in your environment. You can fetch it from the URL when you're on your Redocly dashboard." unless ENV["REDOCLY_ORGANIZATION_ID"].present? + raise "You need to set REDOCLY_API_KEY in your environment. You can create one at https://app.redocly.com/org/#{ENV["REDOCLY_ORGANIZATION_ID"]}/settings/api-keys ." unless ENV["REDOCLY_API_KEY"].present? + + # Create a new Faraday connection + conn = Faraday.new(api_url(version: BulletTrain::Api.current_version)) + + # Fetch the file + response = conn.get + + # Check if the request was successful + if response.status == 200 + # Create a temp file + temp_file = Tempfile.new(["openapi-", ".yaml"]) + + # Write the file content to the temp file + temp_file.binmode + temp_file.write(response.body) + temp_file.rewind + + # Close and delete the temp file when the script exits + temp_file.close + puts "File downloaded and saved to: #{temp_file.path}" + + puts `echo "#{ENV["REDOCLY_API_KEY"]}" | redocly login` + + puts `redocly push #{temp_file.path} "@#{ENV["REDOCLY_ORGANIZATION_ID"]}/#{I18n.t("application.name")}@#{BulletTrain::Api.current_version}" --public --upsert` + + temp_file.unlink + else + puts "Failed to download the OpenAPI Document. Status code: #{response.status}" + end + end end end diff --git a/bullet_train-fields/.circleci/config.yml b/bullet_train-fields/.circleci/config.yml index c4ec6c5b8..4fdb8b598 100644 --- a/bullet_train-fields/.circleci/config.yml +++ b/bullet_train-fields/.circleci/config.yml @@ -26,7 +26,7 @@ aliases: paths: - node_modules - &ruby_node_browsers_docker_image - - image: cimg/ruby:3.1.2-browsers + - image: cimg/ruby:3.2.2-browsers environment: PGHOST: localhost PGUSER: untitled_application diff --git a/bullet_train-fields/Gemfile.lock b/bullet_train-fields/Gemfile.lock index 34ebf746d..497c58644 100644 --- a/bullet_train-fields/Gemfile.lock +++ b/bullet_train-fields/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - bullet_train-fields (1.2.21) + bullet_train-fields (1.2.27) chronic cloudinary phonelib @@ -79,7 +79,7 @@ GEM aws_cf_signer (0.1.3) builder (3.2.4) chronic (0.10.2) - cloudinary (1.25.0) + cloudinary (1.26.0) aws_cf_signer rest-client (>= 2.0.0) concurrent-ruby (1.1.10) @@ -112,7 +112,7 @@ GEM mini_mime (1.1.2) mini_portile2 (2.8.1) minitest (5.16.2) - net-imap (0.3.4) + net-imap (0.3.7) date net-protocol net-pop (0.1.2) @@ -122,14 +122,14 @@ GEM net-smtp (0.3.3) net-protocol netrc (0.11.0) - nio4r (2.5.8) + nio4r (2.5.9) nokogiri (1.13.7) mini_portile2 (~> 2.8.0) racc (~> 1.4) parallel (1.22.1) parser (3.1.2.0) ast (~> 2.4.1) - phonelib (0.7.7) + phonelib (0.8.2) racc (1.6.0) rack (2.2.4) rack-test (2.0.2) @@ -196,23 +196,24 @@ GEM standard (1.14.0) rubocop (= 1.32.0) rubocop-performance (= 1.14.3) - thor (1.2.1) - timeout (0.3.2) + thor (1.2.2) + timeout (0.4.0) tzinfo (2.0.4) concurrent-ruby (~> 1.0) unf (0.1.4) unf_ext unf_ext (0.0.8.2) unicode-display_width (2.2.0) - websocket-driver (0.7.5) + websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) - zeitwerk (2.6.7) + zeitwerk (2.6.9) PLATFORMS arm64-darwin-20 arm64-darwin-21 arm64-darwin-22 + ruby x86_64-darwin-21 x86_64-linux diff --git a/bullet_train-fields/app/controllers/concerns/fields/date_and_time_support.rb b/bullet_train-fields/app/controllers/concerns/fields/date_and_time_support.rb index 81a6be701..3bd25e017 100644 --- a/bullet_train-fields/app/controllers/concerns/fields/date_and_time_support.rb +++ b/bullet_train-fields/app/controllers/concerns/fields/date_and_time_support.rb @@ -2,12 +2,22 @@ module Fields::DateAndTimeSupport extend ActiveSupport::Concern def assign_date_and_time(strong_params, attribute) + deprecator = ActiveSupport::Deprecation.new("2.0", "BulletTrain::Fields") + deprecator.deprecation_warning( + "assign_date_and_time", + "Please assign an ISO8601 datetime string as form field value instead and remove all assign_date_and_time assignments, + see https://ruby-doc.org/3.2.2/exts/date/DateTime.html" + ) attribute = attribute.to_s time_zone_attribute = "#{attribute}_time_zone" if strong_params.dig(attribute).present? - time_zone = ActiveSupport::TimeZone.new(strong_params[time_zone_attribute] || current_team.time_zone) - strong_params.delete(time_zone_attribute) - strong_params[attribute] = time_zone.strptime(strong_params[attribute], t("global.formats.date_and_time")) + begin + strong_params[attribute] = DateTime.iso8601(strong_params[attribute]) + rescue ArgumentError + time_zone = ActiveSupport::TimeZone.new(strong_params[time_zone_attribute] || current_team.time_zone) + strong_params.delete(time_zone_attribute) + strong_params[attribute] = time_zone.strptime(strong_params[attribute], t("global.formats.date_and_time")) + end end end end diff --git a/bullet_train-fields/app/controllers/concerns/fields/date_support.rb b/bullet_train-fields/app/controllers/concerns/fields/date_support.rb index 4536bf50a..0906dc5ca 100644 --- a/bullet_train-fields/app/controllers/concerns/fields/date_support.rb +++ b/bullet_train-fields/app/controllers/concerns/fields/date_support.rb @@ -2,11 +2,21 @@ module Fields::DateSupport extend ActiveSupport::Concern def assign_date(strong_params, attribute) + deprecator = ActiveSupport::Deprecation.new("2.0", "BulletTrain::Fields") + deprecator.deprecation_warning( + "assign_date", + "Please assign an ISO8601 date string as field value instead and remove all assign_date assignments, + see https://ruby-doc.org/3.2.2/exts/date/Date.html" + ) attribute = attribute.to_s if strong_params.dig(attribute).present? - parsed_value = Chronic.parse(strong_params[attribute]) - return nil unless parsed_value - strong_params[attribute] = parsed_value.to_date + begin + strong_params[attribute] = Date.iso8601(strong_params[attribute]) + rescue ArgumentError + parsed_value = Chronic.parse(strong_params[attribute]) + return nil unless parsed_value + strong_params[attribute] = parsed_value.to_date + end end end end diff --git a/bullet_train-fields/app/javascript/controllers/fields/date_controller.js b/bullet_train-fields/app/javascript/controllers/fields/date_controller.js index 6a27a9a72..e8def6860 100644 --- a/bullet_train-fields/app/javascript/controllers/fields/date_controller.js +++ b/bullet_train-fields/app/javascript/controllers/fields/date_controller.js @@ -3,14 +3,18 @@ require("daterangepicker/daterangepicker.css"); // requires jQuery, moment, might want to consider a vanilla JS alternative import 'daterangepicker'; +import moment from 'moment-timezone' export default class extends Controller { - static targets = [ "field", "clearButton", "currentTimeZoneWrapper", "timeZoneButtons", "timeZoneSelectWrapper", "timeZoneField" ] + static targets = [ "field", "displayField", "clearButton", "currentTimeZoneWrapper", "timeZoneButtons", "timeZoneSelectWrapper", "timeZoneField", "timeZoneSelect" ] static values = { includeTime: Boolean, defaultTimeZones: Array, - cancelButtonLabel: { type: String, default: "Cancel" }, - applyButtonLabel: { type: String, default: "Apply" } + dateFormat: String, + timeFormat: String, + currentTimeZone: String, + isAmPm: Boolean, + pickerLocale: { type: Object, default: {} } } connect() { @@ -26,13 +30,33 @@ export default class extends Controller { event.preventDefault() $(this.fieldTarget).val('') + $(this.displayFieldTarget).val('') + } + + currentTimeZone(){ + return ( + ( this.hasTimeZoneSelectWrapperTarget && $(this.timeZoneSelectWrapperTarget).is(":visible") && this.timeZoneSelectTarget.value ) || + ( this.hasTimeZoneFieldTarget && this.timeZoneFieldTarget.value ) || + this.currentTimeZoneValue + ) } applyDateToField(event, picker) { - const format = this.includeTimeValue ? 'MM/DD/YYYY h:mm A' : 'MM/DD/YYYY' - $(this.fieldTarget).val(picker.startDate.format(format)) + const format = this.includeTimeValue ? this.timeFormatValue : this.dateFormatValue + const newTimeZone = this.currentTimeZone() + const momentVal = ( + picker ? + moment(picker.startDate.toISOString()).tz(newTimeZone, true) : + moment.tz(moment(this.fieldTarget.value, "YYYY-MM-DDTHH:mm").format("YYYY-MM-DDTHH:mm"), newTimeZone) + ) + const displayVal = momentVal.format(format) + const dataVal = this.includeTimeValue ? momentVal.toISOString(true) : momentVal.format('YYYY-MM-DD') + $(this.displayFieldTarget).val(displayVal) + $(this.fieldTarget).val(dataVal) // bubble up a change event when the input is updated for other listeners - this.fieldTarget.dispatchEvent(new CustomEvent('change', { detail: { picker: picker }})) + if(picker){ + this.displayFieldTarget.dispatchEvent(new CustomEvent('change', { detail: { picker: picker }})) + } } showTimeZoneButtons(event) { @@ -43,15 +67,18 @@ export default class extends Controller { $(this.timeZoneButtonsTarget).toggleClass('hidden') } + // triggered on other click from the timezone buttons showTimeZoneSelectWrapper(event) { // don't follow the anchor event.preventDefault() $(this.timeZoneButtonsTarget).toggleClass('hidden') - if (this.hasTimeZoneSelectWrapperTarget) { $(this.timeZoneSelectWrapperTarget).toggleClass('hidden') } + if(!["", null].includes(this.fieldTarget.value)){ + $(this.displayFieldTarget).trigger("apply.daterangepicker"); + } } resetTimeZoneUI(e) { @@ -65,39 +92,70 @@ export default class extends Controller { } } + // triggered on selecting a new timezone using the buttons setTimeZone(event) { // don't follow the anchor event.preventDefault() - const currentTimeZoneEl = this.currentTimeZoneWrapperTarget.querySelector('a') - const {value} = event.target.dataset - - $(this.timeZoneFieldTarget).val(value) - $(currentTimeZoneEl).text(value) - + $(this.timeZoneFieldTarget).val(event.target.dataset.value) + $(currentTimeZoneEl).text(event.target.dataset.label) $('.time-zone-button').removeClass('button').addClass('button-alternative') $(event.target).removeClass('button-alternative').addClass('button') + this.resetTimeZoneUI() + if(!["", null].includes(this.fieldTarget.value)){ + $(this.displayFieldTarget).trigger("apply.daterangepicker"); + } + } + + // triggered on selecting a new timezone from the timezone picker + selectTimeZoneChange(event) { + if(!["", null].includes(this.fieldTarget.value)){ + $(this.displayFieldTarget).trigger("apply.daterangepicker"); + } + } + // triggered on cancel click from the timezone picker + cancelSelect(event) { + event.preventDefault() this.resetTimeZoneUI() + if(!["", null].includes(this.fieldTarget.value)){ + $(this.displayFieldTarget).trigger("apply.daterangepicker") + } + } + + displayFieldChange(event) { + const newTimeZone = this.currentTimeZone() + const format = this.includeTimeValue ? this.timeFormatValue : this.dateFormatValue + const momentParsed = moment(this.displayFieldTarget.value, format, false) + if(momentParsed.isValid()){ + const momentVal = moment.tz(momentParsed.format("YYYY-MM-DDTHH:mm"), newTimeZone) + const dataVal = this.includeTimeValue ? momentVal.toISOString(true) : momentVal.format('YYYY-MM-DD') + $(this.fieldTarget).val(dataVal) + } else { + // nullify field value when the display format is wrong + $(this.fieldTarget).val("") + } } initPluginInstance() { - $(this.fieldTarget).daterangepicker({ + const localeValues = this.pickerLocaleValue + const isAmPm = this.isAmPmValue + localeValues['format'] = this.includeTimeValue ? this.timeFormatValue : this.dateFormatValue + + $(this.displayFieldTarget).daterangepicker({ singleDatePicker: true, timePicker: this.includeTimeValue, timePickerIncrement: 5, autoUpdateInput: false, - locale: { - cancelLabel: this.cancelButtonLabelValue, - applyLabel: this.applyButtonLabelValue, - format: this.includeTimeValue ? 'MM/DD/YYYY h:mm A' : 'MM/DD/YYYY' - } + locale: localeValues, + timePicker24Hour: !isAmPm, }) - $(this.fieldTarget).on('apply.daterangepicker', this.applyDateToField.bind(this)) - $(this.fieldTarget).on('cancel.daterangepicker', this.clearDate.bind(this)) + $(this.displayFieldTarget).on('apply.daterangepicker', this.applyDateToField.bind(this)) + $(this.displayFieldTarget).on('cancel.daterangepicker', this.clearDate.bind(this)) + $(this.displayFieldTarget).on('input', this,this.displayFieldChange.bind(this)); - this.pluginMainEl = this.fieldTarget + this.pluginMainEl = this.displayFieldTarget this.plugin = $(this.pluginMainEl).data('daterangepicker') // weird // Init time zone select @@ -113,7 +171,6 @@ export default class extends Controller { $(this.timeZoneSelect).on('change.select2', function(event) { const currentTimeZoneEl = self.currentTimeZoneWrapperTarget.querySelector('a') const {value} = event.target - $(self.timeZoneFieldTarget).val(value) $(currentTimeZoneEl).text(value) @@ -126,7 +183,6 @@ export default class extends Controller { } else { // deselect any selected button $('.time-zone-button').removeClass('button').addClass('button-alternative') - selectedOptionTimeZoneButton.text(value) selectedOptionTimeZoneButton.attr('data-value', value).removeAttr('hidden') selectedOptionTimeZoneButton.removeClass(['hidden', 'button-alternative']).addClass('button') @@ -139,10 +195,8 @@ export default class extends Controller { teardownPluginInstance() { if (this.plugin === undefined) { return } - $(this.pluginMainEl).off('apply.daterangepicker') $(this.pluginMainEl).off('cancel.daterangepicker') - // revert to original markup, remove any event listeners this.plugin.remove() diff --git a/bullet_train-fields/bullet_train-fields.gemspec b/bullet_train-fields/bullet_train-fields.gemspec index 9ddc038a2..4c8c68bb0 100644 --- a/bullet_train-fields/bullet_train-fields.gemspec +++ b/bullet_train-fields/bullet_train-fields.gemspec @@ -5,7 +5,7 @@ Gem::Specification.new do |spec| spec.version = BulletTrain::Fields::VERSION spec.authors = ["Andrew Culver"] spec.email = ["andrew.culver@gmail.com"] - spec.homepage = "https://github.com/bullet-train-co/bullet_train-fields" + spec.homepage = "https://github.com/bullet-train-co/bullet_train-core/tree/main/bullet_train-fields" spec.summary = "Bullet Train Fields" spec.description = spec.summary spec.license = "MIT" diff --git a/bullet_train-fields/lib/bullet_train/fields/version.rb b/bullet_train-fields/lib/bullet_train/fields/version.rb index bd2b42e7c..55aceb7ec 100644 --- a/bullet_train-fields/lib/bullet_train/fields/version.rb +++ b/bullet_train-fields/lib/bullet_train/fields/version.rb @@ -1,5 +1,5 @@ module BulletTrain module Fields - VERSION = "1.2.21" + VERSION = "1.2.27" end end diff --git a/bullet_train-fields/package.json b/bullet_train-fields/package.json index 1fbf7d079..89a0f1337 100644 --- a/bullet_train-fields/package.json +++ b/bullet_train-fields/package.json @@ -47,6 +47,7 @@ "cp-cli": "^2.0.0", "emoji-mart": "^5.1.0", "microbundle": "^0.13.0", + "moment-timezone": "^0.5.43", "np": "^7.6.0", "npm-watch": "^0.11.0", "rimraf": "^3.0.2", diff --git a/bullet_train-fields/yarn.lock b/bullet_train-fields/yarn.lock index 26cdbdd6e..ce20a8109 100644 --- a/bullet_train-fields/yarn.lock +++ b/bullet_train-fields/yarn.lock @@ -3445,6 +3445,18 @@ minimist@^1.2.0, minimist@^1.2.5: resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +moment-timezone@^0.5.43: + version "0.5.43" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.43.tgz#3dd7f3d0c67f78c23cd1906b9b2137a09b3c4790" + integrity sha512-72j3aNyuIsDxdF1i7CEgV2FfxM1r6aaqJyLB2vwb33mXYyoyLly+F1zbWqhA3/bVIoJ4szlUoMbUnVdid32NUQ== + dependencies: + moment "^2.29.4" + +moment@^2.29.4: + version "2.29.4" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" + integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== + moment@^2.9.0: version "2.29.1" resolved "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz" diff --git a/bullet_train-has_uuid/bullet_train-has_uuid.gemspec b/bullet_train-has_uuid/bullet_train-has_uuid.gemspec index 914885914..db6727939 100644 --- a/bullet_train-has_uuid/bullet_train-has_uuid.gemspec +++ b/bullet_train-has_uuid/bullet_train-has_uuid.gemspec @@ -5,7 +5,7 @@ Gem::Specification.new do |spec| spec.version = BulletTrain::HasUuid::VERSION spec.authors = ["Andrew Culver"] spec.email = ["andrew.culver@gmail.com"] - spec.homepage = "https://github.com/bullet-train-co/bullet_train-has_uuid" + spec.homepage = "https://github.com/bullet-train-co/bullet_train-core/tree/main/bullet_train-has_uuid" spec.summary = "Bullet Train Has UUID" spec.description = spec.summary spec.license = "MIT" diff --git a/bullet_train-has_uuid/lib/bullet_train/has_uuid/version.rb b/bullet_train-has_uuid/lib/bullet_train/has_uuid/version.rb index 270d757e3..4a02b87ee 100644 --- a/bullet_train-has_uuid/lib/bullet_train/has_uuid/version.rb +++ b/bullet_train-has_uuid/lib/bullet_train/has_uuid/version.rb @@ -1,5 +1,5 @@ module BulletTrain module HasUuid - VERSION = "1.2.21" + VERSION = "1.2.27" end end diff --git a/bullet_train-incoming_webhooks/bullet_train-incoming_webhooks.gemspec b/bullet_train-incoming_webhooks/bullet_train-incoming_webhooks.gemspec index a3c78df42..67ce8d513 100644 --- a/bullet_train-incoming_webhooks/bullet_train-incoming_webhooks.gemspec +++ b/bullet_train-incoming_webhooks/bullet_train-incoming_webhooks.gemspec @@ -5,7 +5,7 @@ Gem::Specification.new do |spec| spec.version = BulletTrain::IncomingWebhooks::VERSION spec.authors = ["Andrew Culver"] spec.email = ["andrew.culver@gmail.com"] - spec.homepage = "https://github.com/bullet-train-co/bullet_train-incoming_webhooks" + spec.homepage = "https://github.com/bullet-train-co/bullet_train-core/tree/main/bullet_train-incoming_webhooks" spec.summary = "Bullet Train Incoming Webhooks" spec.description = spec.summary spec.license = "MIT" diff --git a/bullet_train-incoming_webhooks/lib/bullet_train/incoming_webhooks/version.rb b/bullet_train-incoming_webhooks/lib/bullet_train/incoming_webhooks/version.rb index 139400754..52776221e 100644 --- a/bullet_train-incoming_webhooks/lib/bullet_train/incoming_webhooks/version.rb +++ b/bullet_train-incoming_webhooks/lib/bullet_train/incoming_webhooks/version.rb @@ -1,5 +1,5 @@ module BulletTrain module IncomingWebhooks - VERSION = "1.2.21" + VERSION = "1.2.27" end end diff --git a/bullet_train-integrations-stripe/bullet_train-integrations-stripe.gemspec b/bullet_train-integrations-stripe/bullet_train-integrations-stripe.gemspec index 0bb8b02e7..9e0efacdb 100644 --- a/bullet_train-integrations-stripe/bullet_train-integrations-stripe.gemspec +++ b/bullet_train-integrations-stripe/bullet_train-integrations-stripe.gemspec @@ -5,7 +5,7 @@ Gem::Specification.new do |spec| spec.version = BulletTrain::Integrations::Stripe::VERSION spec.authors = ["Andrew Culver"] spec.email = ["andrew.culver@gmail.com"] - spec.homepage = "https://github.com/bullet-train-co/bullet_train-integrations-stripe" + spec.homepage = "https://github.com/bullet-train-co/bullet_train-core/tree/main/bullet_train-integrations-stripe" spec.summary = "Example Stripe platform integration for Bullet Train applications." spec.description = spec.summary spec.license = "MIT" diff --git a/bullet_train-integrations-stripe/lib/bullet_train/integrations/stripe/version.rb b/bullet_train-integrations-stripe/lib/bullet_train/integrations/stripe/version.rb index ab33e43a5..9108c98c8 100644 --- a/bullet_train-integrations-stripe/lib/bullet_train/integrations/stripe/version.rb +++ b/bullet_train-integrations-stripe/lib/bullet_train/integrations/stripe/version.rb @@ -1,7 +1,7 @@ module BulletTrain module Integrations module Stripe - VERSION = "1.2.21" + VERSION = "1.2.27" end end end diff --git a/bullet_train-integrations/bullet_train-integrations.gemspec b/bullet_train-integrations/bullet_train-integrations.gemspec index e19ce6ef7..beffae28a 100644 --- a/bullet_train-integrations/bullet_train-integrations.gemspec +++ b/bullet_train-integrations/bullet_train-integrations.gemspec @@ -5,7 +5,7 @@ Gem::Specification.new do |spec| spec.version = BulletTrain::Integrations::VERSION spec.authors = ["Andrew Culver"] spec.email = ["andrew.culver@gmail.com"] - spec.homepage = "https://github.com/bullet-train-co/bullet_train-integrations" + spec.homepage = "https://github.com/bullet-train-co/bullet_train-core/tree/main/bullet_train-integrations" spec.summary = "Bullet Train Integrations" spec.description = spec.summary spec.license = "MIT" diff --git a/bullet_train-integrations/lib/bullet_train/integrations/version.rb b/bullet_train-integrations/lib/bullet_train/integrations/version.rb index d062d93de..5b6771b16 100644 --- a/bullet_train-integrations/lib/bullet_train/integrations/version.rb +++ b/bullet_train-integrations/lib/bullet_train/integrations/version.rb @@ -1,5 +1,5 @@ module BulletTrain module Integrations - VERSION = "1.2.21" + VERSION = "1.2.27" end end diff --git a/bullet_train-obfuscates_id/bullet_train-obfuscates_id.gemspec b/bullet_train-obfuscates_id/bullet_train-obfuscates_id.gemspec index 603dced26..ac0a9e07f 100644 --- a/bullet_train-obfuscates_id/bullet_train-obfuscates_id.gemspec +++ b/bullet_train-obfuscates_id/bullet_train-obfuscates_id.gemspec @@ -5,7 +5,7 @@ Gem::Specification.new do |spec| spec.version = BulletTrain::ObfuscatesId::VERSION spec.authors = ["Andrew Culver"] spec.email = ["andrew.culver@gmail.com"] - spec.homepage = "https://github.com/bullet-train-co/bullet_train-obfuscates_id" + spec.homepage = "https://github.com/bullet-train-co/bullet_train-core/tree/main/bullet_train-obfuscates_id" spec.summary = "Bullet Train Obfuscates ID" spec.description = spec.summary spec.license = "MIT" diff --git a/bullet_train-obfuscates_id/lib/bullet_train/obfuscates_id/version.rb b/bullet_train-obfuscates_id/lib/bullet_train/obfuscates_id/version.rb index e8a879d4a..f14e8fbf0 100644 --- a/bullet_train-obfuscates_id/lib/bullet_train/obfuscates_id/version.rb +++ b/bullet_train-obfuscates_id/lib/bullet_train/obfuscates_id/version.rb @@ -1,5 +1,5 @@ module BulletTrain module ObfuscatesId - VERSION = "1.2.21" + VERSION = "1.2.27" end end diff --git a/bullet_train-outgoing_webhooks/.circleci/config.yml b/bullet_train-outgoing_webhooks/.circleci/config.yml index 0671bce72..8c8ad63a8 100644 --- a/bullet_train-outgoing_webhooks/.circleci/config.yml +++ b/bullet_train-outgoing_webhooks/.circleci/config.yml @@ -26,7 +26,7 @@ aliases: paths: - node_modules - &ruby_node_browsers_docker_image - - image: cimg/ruby:3.1.2-browsers + - image: cimg/ruby:3.2.2-browsers environment: PGHOST: localhost PGUSER: untitled_application diff --git a/bullet_train-outgoing_webhooks/app/controllers/account/webhooks/outgoing/deliveries_controller.rb b/bullet_train-outgoing_webhooks/app/controllers/account/webhooks/outgoing/deliveries_controller.rb index 657d5f5be..e5722c3ad 100644 --- a/bullet_train-outgoing_webhooks/app/controllers/account/webhooks/outgoing/deliveries_controller.rb +++ b/bullet_train-outgoing_webhooks/app/controllers/account/webhooks/outgoing/deliveries_controller.rb @@ -63,17 +63,13 @@ def destroy # Never trust parameters from the scary internet, only allow the white list through. def delivery_params - strong_params = params.require(:webhooks_outgoing_delivery).permit( + params.require(:webhooks_outgoing_delivery).permit( :event_id, :endpoint_url, :delivered_at, # π super scaffolding will insert new fields above this line. # π super scaffolding will insert new arrays above this line. ) - - assign_date_and_time(strong_params, :delivered_at) # π super scaffolding will insert processing for new fields above this line. - - strong_params end end diff --git a/bullet_train-outgoing_webhooks/app/controllers/api/v1/webhooks/outgoing/endpoints_controller.rb b/bullet_train-outgoing_webhooks/app/controllers/api/v1/webhooks/outgoing/endpoints_controller.rb index 2b54b0b03..a92b5f312 100644 --- a/bullet_train-outgoing_webhooks/app/controllers/api/v1/webhooks/outgoing/endpoints_controller.rb +++ b/bullet_train-outgoing_webhooks/app/controllers/api/v1/webhooks/outgoing/endpoints_controller.rb @@ -43,7 +43,7 @@ def endpoint_params *permitted_fields, :url, :name, - :version, + :api_version, :scaffolding_absolutely_abstract_creative_concept_id, # π super scaffolding will insert new fields above this line. *permitted_arrays, diff --git a/bullet_train-outgoing_webhooks/app/models/concerns/webhooks/outgoing/delivery_attempt_support.rb b/bullet_train-outgoing_webhooks/app/models/concerns/webhooks/outgoing/delivery_attempt_support.rb index 8dfcd2afd..6c7a2b736 100644 --- a/bullet_train-outgoing_webhooks/app/models/concerns/webhooks/outgoing/delivery_attempt_support.rb +++ b/bullet_train-outgoing_webhooks/app/models/concerns/webhooks/outgoing/delivery_attempt_support.rb @@ -46,13 +46,19 @@ def attempt end # Net::HTTP will consider the url invalid (and not deliver the webhook) unless it ends with a '/' - unless uri.path.end_with?("/") - uri.path = uri.path + "/" + if uri.path == "" + uri.path = "/" end http = Net::HTTP.new(hostname, uri.port) - http.use_ssl = true if uri.scheme == "https" - request = Net::HTTP::Post.new(uri.path) + if uri.scheme == "https" + http.use_ssl = true + if BulletTrain::OutgoingWebhooks.http_verify_mode + # Developers might need to set this to `OpenSSL::SSL::VERIFY_NONE` in some cases. + http.verify_mode = BulletTrain::OutgoingWebhooks.http_verify_mode + end + end + request = Net::HTTP::Post.new(uri.request_uri) request.add_field("Host", uri.host) request.add_field("Content-Type", "application/json") request.body = delivery.event.payload.to_json diff --git a/bullet_train-outgoing_webhooks/app/models/concerns/webhooks/outgoing/issuing_model.rb b/bullet_train-outgoing_webhooks/app/models/concerns/webhooks/outgoing/issuing_model.rb index 377d7dc92..59b7bea29 100644 --- a/bullet_train-outgoing_webhooks/app/models/concerns/webhooks/outgoing/issuing_model.rb +++ b/bullet_train-outgoing_webhooks/app/models/concerns/webhooks/outgoing/issuing_model.rb @@ -13,6 +13,7 @@ def skip_generate_webhook?(action) false end + # TODO This should probably be called `outgoing_webhooks_parent` to avoid colliding with downstream `parent` methods. def parent return unless respond_to? BulletTrain::OutgoingWebhooks.parent_association send(BulletTrain::OutgoingWebhooks.parent_association) diff --git a/bullet_train-outgoing_webhooks/app/models/concerns/webhooks/outgoing/uri_filtering.rb b/bullet_train-outgoing_webhooks/app/models/concerns/webhooks/outgoing/uri_filtering.rb index e3bbac9f3..441436358 100644 --- a/bullet_train-outgoing_webhooks/app/models/concerns/webhooks/outgoing/uri_filtering.rb +++ b/bullet_train-outgoing_webhooks/app/models/concerns/webhooks/outgoing/uri_filtering.rb @@ -81,7 +81,7 @@ def resolve_ip_from_authoritative(hostname) resource = authoritative_resolver.getresource(hostname, Resolv::DNS::Resource::IN::A) Rails.cache.write(cache_key, resource.address.to_s, expires_in: resource.ttl, race_condition_ttl: 5) resource.address.to_s - rescue IPAddr::InvalidAddressError, ArgumentError # standard:disable Lint/ShadowedException + rescue ArgumentError Rails.cache.write(cache_key, "invalid", expires_in: 10.minutes, race_condition_ttl: 5) nil end diff --git a/bullet_train-outgoing_webhooks/app/views/api/v1/open_api/index.yaml.erb b/bullet_train-outgoing_webhooks/app/views/api/v1/open_api/index.yaml.erb deleted file mode 100644 index bead12524..000000000 --- a/bullet_train-outgoing_webhooks/app/views/api/v1/open_api/index.yaml.erb +++ /dev/null @@ -1,33 +0,0 @@ -openapi: 3.1.0 -info: - title: Bullet Train API - description: | - The baseline API of a new Bullet Train application. - license: - name: MIT - url: https://opensource.org/licenses/MIT - version: "<%= @version.upcase %>" -servers: - - url: <%= ENV["BASE_URL"] %>/api/<%= @version %> -components: - securitySchemes: - BearerAuth: - type: http - scheme: bearer - schemas: - <%= automatic_components_for Webhooks::Outgoing::Endpoint %> - <%= automatic_components_for Webhooks::Outgoing::Event %> - <%# π super scaffolding will insert new components above this line. %> - parameters: - id: - name: id - in: path - required: true - schema: - type: string -security: - - BearerAuth: [] -paths: - <%= automatic_paths_for Webhooks::Outgoing::Endpoint, Team %> - <%= automatic_paths_for Webhooks::Outgoing::Event, Team, except: %i[create update delete] %> - <%# π super scaffolding will insert new paths above this line. %> diff --git a/bullet_train-outgoing_webhooks/app/views/api/v1/webhooks/outgoing/events/_event.json.jbuilder b/bullet_train-outgoing_webhooks/app/views/api/v1/webhooks/outgoing/events/_event.json.jbuilder index d3e22ab9f..cea24b753 100644 --- a/bullet_train-outgoing_webhooks/app/views/api/v1/webhooks/outgoing/events/_event.json.jbuilder +++ b/bullet_train-outgoing_webhooks/app/views/api/v1/webhooks/outgoing/events/_event.json.jbuilder @@ -1,12 +1,9 @@ -json.data schema: {object: OpenStruct.new, object_title: I18n.t("webhooks/outgoing/events.fields.data.heading"), object_description: I18n.t("webhooks/outgoing/events.fields.data.heading")} do - json.id schema: {type: :integer, description: I18n.t("webhooks/outgoing/events.fields.data.id.heading")} - json.name schema: {type: :string, description: I18n.t("webhooks/outgoing/events.fields.data.name.heading")} - json.description schema: {type: :string, description: I18n.t("webhooks/outgoing/events.fields.data.description.heading")} - json.created_at schema: {type: :string, format: "date-time", description: I18n.t("webhooks/outgoing/events.fields.data.created_at.heading")} - json.updated_at schema: {type: :string, format: "date-time", description: I18n.t("webhooks/outgoing/events.fields.data.updated_at.heading")} -end - -json.event_id schema: {type: :string, description: I18n.t("webhooks/outgoing/events.fields.event_id.heading")} -json.event_type schema: {type: :integer, description: I18n.t("webhooks/outgoing/events.fields.event_type.heading")} -json.subject_id schema: {type: :integer, description: I18n.t("webhooks/outgoing/events.fields.subject_id.heading")} -json.subject_type schema: {type: :string, description: I18n.t("webhooks/outgoing/events.fields.subject_type.heading")} +json.extract! event, + :id, + :team_id, + :uuid, + :event_type_id, + :subject_id, + :subject_type, + :data, + :created_at diff --git a/bullet_train-outgoing_webhooks/bullet_train-outgoing_webhooks.gemspec b/bullet_train-outgoing_webhooks/bullet_train-outgoing_webhooks.gemspec index d1eb8dfc9..46e8750bb 100644 --- a/bullet_train-outgoing_webhooks/bullet_train-outgoing_webhooks.gemspec +++ b/bullet_train-outgoing_webhooks/bullet_train-outgoing_webhooks.gemspec @@ -5,7 +5,7 @@ Gem::Specification.new do |spec| spec.version = BulletTrain::OutgoingWebhooks::VERSION spec.authors = ["Andrew Culver"] spec.email = ["andrew.culver@gmail.com"] - spec.homepage = "https://github.com/bullet-train-co/bullet_train-outgoing_webhooks" + spec.homepage = "https://github.com/bullet-train-co/bullet_train-core/tree/main/bullet_train-outgoing_webhooks" spec.summary = "Allow users of your Rails application to subscribe and receive webhooks when activity takes place in your application." spec.description = spec.summary spec.license = "MIT" diff --git a/bullet_train-outgoing_webhooks/config/locales/en/webhooks/outgoing/events.en.yml b/bullet_train-outgoing_webhooks/config/locales/en/webhooks/outgoing/events.en.yml index 412558939..ec3f21312 100644 --- a/bullet_train-outgoing_webhooks/config/locales/en/webhooks/outgoing/events.en.yml +++ b/bullet_train-outgoing_webhooks/config/locales/en/webhooks/outgoing/events.en.yml @@ -2,45 +2,23 @@ en: webhooks/outgoing/events: &events label: &label Webhooks Events fields: &fields - data: - _: &data Event Data - label: *data - heading: *data - - id: - _: &id Event ID - label: *id - heading: *id - - name: - _: &name Event Name - label: *name - heading: *name - - description: - _: &description Event Description - label: *description - heading: *description - - created_at: - _: &created_at DateTime Event was added - label: *created_at - heading: *created_at - updated_at: - _: &updated_at DateTime Event was updated - label: *updated_at - heading: *updated_at + id: + _: &id Event ID + label: *id + heading: *id - event_id: - _: &event_id Event UUID - label: *event_id - heading: *event_id + team_id: + _: &team_id Team ID + label: *team_id + heading: *team_id - event_type: + event_type_id: &event_type_id _: &event_type Event Type label: *event_type heading: *event_type + event_type: *event_type_id + subject_id: _: &subject_id Subject ID label: *subject_id @@ -70,3 +48,13 @@ en: _: &event_type_name Event Type Name label: *event_type_name heading: *event_type_name + + data: + _: &data Object Data + label: *data + heading: *data + + created_at: + _: &created_at Happened At + label: *created_at + heading: *created_at diff --git a/bullet_train-outgoing_webhooks/db/migrate/20221230223200_add_api_version_to_webhooks_outgoing_endpoints.rb b/bullet_train-outgoing_webhooks/db/migrate/20221230223200_add_api_version_to_webhooks_outgoing_endpoints.rb new file mode 100644 index 000000000..ecbd43c1a --- /dev/null +++ b/bullet_train-outgoing_webhooks/db/migrate/20221230223200_add_api_version_to_webhooks_outgoing_endpoints.rb @@ -0,0 +1,5 @@ +class AddApiVersionToWebhooksOutgoingEndpoints < ActiveRecord::Migration[7.0] + def change + add_column :webhooks_outgoing_endpoints, :api_version, :integer, null: false, default: 1 + end +end diff --git a/bullet_train-outgoing_webhooks/db/migrate/20221230235326_add_api_version_to_webhooks_outgoing_events.rb b/bullet_train-outgoing_webhooks/db/migrate/20221230235326_add_api_version_to_webhooks_outgoing_events.rb new file mode 100644 index 000000000..04f131b46 --- /dev/null +++ b/bullet_train-outgoing_webhooks/db/migrate/20221230235326_add_api_version_to_webhooks_outgoing_events.rb @@ -0,0 +1,5 @@ +class AddApiVersionToWebhooksOutgoingEvents < ActiveRecord::Migration[7.0] + def change + add_column :webhooks_outgoing_events, :api_version, :integer, null: false, default: 1 + end +end diff --git a/bullet_train-outgoing_webhooks/db/migrate/20221231003437_remove_default_from_webhooks_outgoing_endpoints.rb b/bullet_train-outgoing_webhooks/db/migrate/20221231003437_remove_default_from_webhooks_outgoing_endpoints.rb new file mode 100644 index 000000000..5b95aff63 --- /dev/null +++ b/bullet_train-outgoing_webhooks/db/migrate/20221231003437_remove_default_from_webhooks_outgoing_endpoints.rb @@ -0,0 +1,5 @@ +class RemoveDefaultFromWebhooksOutgoingEndpoints < ActiveRecord::Migration[7.0] + def change + change_column_default :webhooks_outgoing_endpoints, :api_version, from: 1, to: nil + end +end diff --git a/bullet_train-outgoing_webhooks/db/migrate/20221231003438_remove_default_from_webhooks_outgoing_events.rb b/bullet_train-outgoing_webhooks/db/migrate/20221231003438_remove_default_from_webhooks_outgoing_events.rb new file mode 100644 index 000000000..9145f68d6 --- /dev/null +++ b/bullet_train-outgoing_webhooks/db/migrate/20221231003438_remove_default_from_webhooks_outgoing_events.rb @@ -0,0 +1,5 @@ +class RemoveDefaultFromWebhooksOutgoingEvents < ActiveRecord::Migration[7.0] + def change + change_column_default :webhooks_outgoing_events, :api_version, from: 1, to: nil + end +end diff --git a/bullet_train-outgoing_webhooks/lib/bullet_train/outgoing_webhooks.rb b/bullet_train-outgoing_webhooks/lib/bullet_train/outgoing_webhooks.rb index 86c7b2c87..7f63e6687 100644 --- a/bullet_train-outgoing_webhooks/lib/bullet_train/outgoing_webhooks.rb +++ b/bullet_train-outgoing_webhooks/lib/bullet_train/outgoing_webhooks.rb @@ -10,6 +10,7 @@ def self.default_for(klass, method, default_value) mattr_accessor :parent_class, default: default_for(BulletTrain, :parent_class, "Team") mattr_accessor :base_class, default: default_for(BulletTrain, :base_class, "ApplicationRecord") mattr_accessor :advanced_hostname_security, default: false + mattr_accessor :http_verify_mode def self.parent_association parent_class.underscore.to_sym diff --git a/bullet_train-outgoing_webhooks/lib/bullet_train/outgoing_webhooks/version.rb b/bullet_train-outgoing_webhooks/lib/bullet_train/outgoing_webhooks/version.rb index b71fd9262..5beebb69a 100644 --- a/bullet_train-outgoing_webhooks/lib/bullet_train/outgoing_webhooks/version.rb +++ b/bullet_train-outgoing_webhooks/lib/bullet_train/outgoing_webhooks/version.rb @@ -1,5 +1,5 @@ module BulletTrain module OutgoingWebhooks - VERSION = "1.2.21" + VERSION = "1.2.27" end end diff --git a/bullet_train-roles/.circleci/config.yml b/bullet_train-roles/.circleci/config.yml index d44d95878..6edcc984d 100644 --- a/bullet_train-roles/.circleci/config.yml +++ b/bullet_train-roles/.circleci/config.yml @@ -26,7 +26,7 @@ aliases: paths: - node_modules - &ruby_node_browsers_docker_image - - image: cimg/ruby:3.1.2-browsers + - image: cimg/ruby:3.2.2-browsers environment: PGHOST: localhost PGUSER: untitled_application diff --git a/bullet_train-roles/Gemfile.lock b/bullet_train-roles/Gemfile.lock index d1500a72c..4c349e752 100644 --- a/bullet_train-roles/Gemfile.lock +++ b/bullet_train-roles/Gemfile.lock @@ -9,7 +9,7 @@ GIT PATH remote: . specs: - bullet_train-roles (1.2.21) + bullet_train-roles (1.2.27) active_hash activesupport cancancan @@ -112,6 +112,7 @@ GEM marcel (1.0.2) method_source (1.0.0) mini_mime (1.1.2) + mini_portile2 (2.8.1) minitest (5.18.0) net-imap (0.3.4) date @@ -123,6 +124,9 @@ GEM net-smtp (0.3.3) net-protocol nio4r (2.5.8) + nokogiri (1.14.2) + mini_portile2 (~> 2.8.0) + racc (~> 1.4) nokogiri (1.14.2-arm64-darwin) racc (~> 1.4) nokogiri (1.14.2-x86_64-darwin) @@ -199,6 +203,7 @@ PLATFORMS arm64-darwin-20 arm64-darwin-21 arm64-darwin-22 + ruby x86_64-darwin-21 x86_64-linux diff --git a/bullet_train-roles/README.md b/bullet_train-roles/README.md index c1de40f81..24ac81571 100644 --- a/bullet_train-roles/README.md +++ b/bullet_train-roles/README.md @@ -58,7 +58,7 @@ The provided `Role` model is backed by a Yaml configuration in `config/models/ro To help explain this configuration and its options, we'll provide the following hypothetical example: -``` +```yaml default: models: Project: read @@ -92,6 +92,8 @@ Here's a breakdown of the structure of the configuration file: - `manageable_roles` provides a list of roles that can be assigned to other users by members that have the role being defined. - `includes` provides a list of other roles whose permissions should also be made available to members with the role being defined. - `manage`, `read`, etc. are all CanCanCan-defined actions that can be granted. + - `crud` is a special value that we substitute for the 4 CRUD actions - create, read, update and destroy. + This is instead of `manage` which covers all actions - 4 CRUD actions _and_ any extra actions the controller may respond to The following things are true given the example configuration above: @@ -111,7 +113,7 @@ The following things are true given the example configuration above: You can also grant more granular permissions by supplying a list of the specific actions per resource, like so: -``` +```yaml editor: models: project: @@ -123,7 +125,7 @@ editor: All of these definitions are interpreted and translated into CanCanCan directives when we invoke the following Bullet Train helper in `app/models/ability.rb`: -``` +```ruby permit user, through: :memberships, parent: :team ``` @@ -136,7 +138,7 @@ In the example above: To illustrate the flexibility of this approach, consider that you may want to grant non-administrative team members different permissions for different `Project` objects on a `Team`. In that case, `permit` actually allows us to re-use the same role definitions to assign permissions that are scoped by a specific resource, like this: -``` +```ruby permit user, through: :projects_collaborators, parent: :project ``` @@ -149,7 +151,7 @@ In some situations, you don't want all roles to be available to all Grant Models By default all Grant Models will show all roles as options. If you want to limit the roles available to a model, use the `roles_only` class method: -``` +```ruby class Membership < ApplicationRecord include Roles::Support roles_only :admin, :editor, :reader # Add this line to restrict the Membership model to only these roles @@ -158,7 +160,7 @@ end To access the array of all roles available for a particular model, use the `assignable_roles` class method. For example, in your Membership form, you probably _only_ want to show the assignable_roles as options. Your view could look like this: -``` +```erb <% Membership.assignable_roles.each do |role| %> <% if role.manageable_by?(current_membership.roles) %> @@ -166,13 +168,72 @@ To access the array of all roles available for a particular model, use the `assi <% end %> ``` +## Checking user permissions + +Generally the CanCanCan helper method (`account_load_and_authorize_resource`) at the top of each controller will handle checking user permissions and will only load resources appropriate for the current user. + +However, you may also want to check if a user can perform a specific action. For example, in a view you may want to only show the edit button if the current user has permissions to edit the object. For this, you can use regular CanCanCan helpers. For example: + +``` +<%= link_to "Edit", [:account, @document] if can? :edit, @document %> +``` + +Sometimes, you might want to check for the presence of a specific role. We provide a helper to check for the admin role: +``` +@membership.admin? +``` + +For all other roles, you can check for their presence like this: + +``` +@membership.roles.include?(Role.find("developer")) +``` + +However, when you do that, you're only checking the roles that have been directly assigned to that membership. + +Imagine a scenario like this: +``` +# roles.yml +admin: + includes: + - editor + - billing + +# somewhere else in your app: +@membership.roles << Role.admin +@membership.roles.include?(Role.find("editor")) +=> false +``` + +While that's technically correct that the user doesn't have the editor role, we probably want that to return true if we're checking what the user can and can't do. For this situation, we really want to check if the user can perform a role rather than if they've had that role assigned to them. + +``` +# roles.yml + +admin: + includes: + - editor + - billing + +# somewhere else in your app: + +@membership.roles << Role.admin +@membership.roles.can_perform_role?(Role.find("editor")) +=> true + +# You can also pass the role key as a symbol for a more concise syntax +@membership.roles.can_perform_role?(:editor) +=> true +``` + ## Debugging + If you want to see what CanCanCan directives are being created by your permit calls, you can add the `debug: true` option to your `permit` statement in `app/models/ability.rb`. Likewise, to see what abilities are being added for a certain user, you can run the following on the Rails console: -``` +```ruby user = User.first Ability.new(user).permit user, through: :projects_collaborators, parent: :project, debug: true ``` diff --git a/bullet_train-roles/bullet_train-roles.gemspec b/bullet_train-roles/bullet_train-roles.gemspec index e53b3e35c..5e0d63df0 100644 --- a/bullet_train-roles/bullet_train-roles.gemspec +++ b/bullet_train-roles/bullet_train-roles.gemspec @@ -10,7 +10,7 @@ Gem::Specification.new do |spec| spec.summary = "Yaml-backed ApplicationHash for CanCan Roles" spec.description = "Yaml-backed ApplicationHash for CanCan Roles" - spec.homepage = "https://github.com/bullet-train-co/bullet_train-roles" + spec.homepage = "https://github.com/bullet-train-co/bullet_train-core/tree/main/bullet_train-roles" spec.license = "MIT" spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0") diff --git a/bullet_train-roles/lib/bullet_train/roles/version.rb b/bullet_train-roles/lib/bullet_train/roles/version.rb index 584f01595..b9502916f 100644 --- a/bullet_train-roles/lib/bullet_train/roles/version.rb +++ b/bullet_train-roles/lib/bullet_train/roles/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Roles - VERSION = "1.2.21" + VERSION = "1.2.27" end diff --git a/bullet_train-roles/lib/models/role.rb b/bullet_train-roles/lib/models/role.rb index 7ae51bd8f..f59528efe 100644 --- a/bullet_train-roles/lib/models/role.rb +++ b/bullet_train-roles/lib/models/role.rb @@ -19,9 +19,15 @@ def self.default find_by_key("default") end + # Allow us to use either symbols or strings when searching + def self.find_by_key(key) + super(key.to_s) + end + def self.includes(role_or_key) - role_key = role_or_key.is_a?(Role) ? role_or_key.key : role_or_key + role_key = role_or_key.is_a?(Role) ? role_or_key.key : role_or_key.to_s role = Role.find_by_key(role_key) + return [] if role.nil? return Role.all.select(&:assignable?) if role.default? result = [] all.each do |role| @@ -35,7 +41,7 @@ def self.assignable end def self.find(key) - all.find { |role| role.key == key } + all.find { |role| role.key == key.to_s } end # We don't want to ever use the automatically generated ids from ActiveYaml. These are created based on the order of objects in the yml file diff --git a/bullet_train-roles/lib/roles/support.rb b/bullet_train-roles/lib/roles/support.rb index 411b7d7b7..de3c735b0 100644 --- a/bullet_train-roles/lib/roles/support.rb +++ b/bullet_train-roles/lib/roles/support.rb @@ -70,6 +70,20 @@ def roles Role::Collection.new(self, (self.class.default_roles + roles_without_defaults).compact.uniq) end + # Tests if the user can perform a given role + # They can have the role assigned directly, or the role can be included in another role they have + def can_perform_role?(role_or_key) + role_key = role_or_key.is_a?(Role) ? role_or_key.key : role_or_key + role = Role.find_by_key(role_key) + return true if roles.include?(role) + # Check all the roles that this role is included into + Role.includes(role_key).each do |included_in_role| + return true if roles.include?(included_in_role) + return true if can_perform_role?(included_in_role) + end + false + end + def roles=(roles) update(role_ids: roles.map(&:key)) end diff --git a/bullet_train-roles/test/dummy/config/models/roles.yml b/bullet_train-roles/test/dummy/config/models/roles.yml index 1cf53da44..edfd5d809 100644 --- a/bullet_train-roles/test/dummy/config/models/roles.yml +++ b/bullet_train-roles/test/dummy/config/models/roles.yml @@ -16,6 +16,14 @@ editor: - read - update +manager: + includes: + - editor + +supervisor: + includes: + - manager + admin: includes: - editor diff --git a/bullet_train-roles/test/lib/models/role_test.rb b/bullet_train-roles/test/lib/models/role_test.rb index 6d4d8da5e..cb4799e31 100644 --- a/bullet_train-roles/test/lib/models/role_test.rb +++ b/bullet_train-roles/test/lib/models/role_test.rb @@ -25,11 +25,11 @@ def setup end test "Role.includes works when given a string" do - assert_equal Role.includes("editor"), [Role.admin] + assert Role.includes("editor").include?(Role.admin) end test "Role.include works when given a role" do - assert_equal Role.includes(Role.find_by_key("editor")), [Role.admin] + assert Role.includes(Role.find_by_key("editor")).include? Role.admin end test "Role.assignable should not return the default role" do @@ -45,6 +45,8 @@ class InstanceMethodsTest < ActiveSupport::TestCase def setup @admin_user = FactoryBot.create :onboarded_user @membership = FactoryBot.create :membership, user: @admin_user, team: @admin_user.current_team, role_ids: [Role.admin.id] + @non_admin_user = FactoryBot.create :onboarded_user + @non_admin_membership = FactoryBot.create :membership, user: @non_admin_user, team: @non_admin_user.current_team, role_ids: [Role.find(:editor).id] end test "default_role#included_by returns the admin role" do @@ -131,6 +133,32 @@ def setup @membership.update_column(:role_ids, nil) assert_equal @membership.roles, [Role.find_by_key("default")] end + + test "#can_perform_role? returns true if the user has been assigned the role" do + assert @membership.can_perform_role?(Role.admin) + end + + test "#can_perform_role? returns true if the user has not been assigned the role, but it is included in another role they have" do + assert @membership.roles.include? Role.admin + refute @membership.roles.include?(Role.find(:editor)) + assert Role.admin.includes.include?("editor") + assert @membership.can_perform_role?(:editor) + end + + test "#can_perform_role? returns false if the user has not been assigned the role, and it is not included in another role they have" do + assert @non_admin_membership.roles.include?(Role.find(:editor)) + refute Role.find(:editor).includes.include?("crud_role") + assert Role.find :crud_role + refute @non_admin_membership.can_perform_role?(:crud_role) + end + + test "#can_perform_role? returns true if the role being tested is 2 layers deep" do + # supervisor includes manager, which includes editor + @supervisor_membership = FactoryBot.create :membership, user: @admin_user, team: @admin_user.current_team, role_ids: [] + refute @supervisor_membership.can_perform_role?(:editor) + @supervisor_membership.roles << Role.find(:supervisor) + assert @supervisor_membership.can_perform_role?(:editor) + end end class Role::AbilityGeneratorTest < ActiveSupport::TestCase diff --git a/bullet_train-scope_questions/bullet_train-scope_questions.gemspec b/bullet_train-scope_questions/bullet_train-scope_questions.gemspec index 6c7fc3f6d..300bf6b85 100644 --- a/bullet_train-scope_questions/bullet_train-scope_questions.gemspec +++ b/bullet_train-scope_questions/bullet_train-scope_questions.gemspec @@ -5,7 +5,7 @@ Gem::Specification.new do |spec| spec.version = BulletTrain::ScopeQuestions::VERSION spec.authors = ["Andrew Culver"] spec.email = ["andrew.culver@gmail.com"] - spec.homepage = "https://github.com/bullet-train-co/bullet_train-scope_questions" + spec.homepage = "https://github.com/bullet-train-co/bullet_train-core/tree/main/bullet_train-scope_questions" spec.summary = "Bullet Train Scope Questions" spec.description = spec.summary spec.license = "MIT" diff --git a/bullet_train-scope_questions/lib/bullet_train/scope_questions/version.rb b/bullet_train-scope_questions/lib/bullet_train/scope_questions/version.rb index 07fec41e7..2fb45a799 100644 --- a/bullet_train-scope_questions/lib/bullet_train/scope_questions/version.rb +++ b/bullet_train-scope_questions/lib/bullet_train/scope_questions/version.rb @@ -1,5 +1,5 @@ module BulletTrain module ScopeQuestions - VERSION = "1.2.21" + VERSION = "1.2.27" end end diff --git a/bullet_train-scope_validator/Gemfile.lock b/bullet_train-scope_validator/Gemfile.lock index a087af729..8acbbbc58 100644 --- a/bullet_train-scope_validator/Gemfile.lock +++ b/bullet_train-scope_validator/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - bullet_train-scope_validator (1.2.21) + bullet_train-scope_validator (1.2.27) GEM remote: https://rubygems.org/ diff --git a/bullet_train-scope_validator/README.md b/bullet_train-scope_validator/README.md index 8061f9670..d2b8d3877 100644 --- a/bullet_train-scope_validator/README.md +++ b/bullet_train-scope_validator/README.md @@ -50,7 +50,7 @@ end ### Example Form -``` +```erb <%= form.collection_select(:customer_id, @team.customers, :id, :name) %> ``` @@ -101,7 +101,7 @@ end If you're wondering what the connection between `validates :customer, scope: true` and `def valid_customers` is, it's just a convention that the former will call the latter based on the name of the attibute being validated. We've favored a full-blown method definition for this instead of simply passing in a proc into the validator because having a method allows us to also DRY up our form view to use the same definition of valid options, like so: -``` +```erb <%= form.collection_select(:customer_id, form.object.valid_customers, :id, :name) %> ``` diff --git a/bullet_train-scope_validator/bullet_train-scope_validator.gemspec b/bullet_train-scope_validator/bullet_train-scope_validator.gemspec index a012a8391..182ffad5f 100644 --- a/bullet_train-scope_validator/bullet_train-scope_validator.gemspec +++ b/bullet_train-scope_validator/bullet_train-scope_validator.gemspec @@ -10,7 +10,7 @@ Gem::Specification.new do |spec| spec.summary = "Protect `belongs_to` attributes from ID stuffing." spec.description = spec.summary - spec.homepage = "https://github.com/bullet-train-co/bullet_train-scope_validator" + spec.homepage = "https://github.com/bullet-train-co/bullet_train-core/tree/main/bullet_train-scope_validator" spec.license = "MIT" spec.required_ruby_version = ">= 2.6.0" diff --git a/bullet_train-scope_validator/lib/bullet_train/scope_validator/version.rb b/bullet_train-scope_validator/lib/bullet_train/scope_validator/version.rb index c8e544cc4..3c3e3b325 100644 --- a/bullet_train-scope_validator/lib/bullet_train/scope_validator/version.rb +++ b/bullet_train-scope_validator/lib/bullet_train/scope_validator/version.rb @@ -2,6 +2,6 @@ module BulletTrain module ScopeValidator - VERSION = "1.2.21" + VERSION = "1.2.27" end end diff --git a/bullet_train-sortable/.circleci/config.yml b/bullet_train-sortable/.circleci/config.yml index 2dd19bf05..eac14a502 100644 --- a/bullet_train-sortable/.circleci/config.yml +++ b/bullet_train-sortable/.circleci/config.yml @@ -26,7 +26,7 @@ aliases: paths: - node_modules - &ruby_node_browsers_docker_image - - image: cimg/ruby:3.1.2-browsers + - image: cimg/ruby:3.2.2-browsers environment: PGHOST: localhost PGUSER: untitled_application diff --git a/bullet_train-sortable/bullet_train-sortable.gemspec b/bullet_train-sortable/bullet_train-sortable.gemspec index 32b622093..743a692da 100644 --- a/bullet_train-sortable/bullet_train-sortable.gemspec +++ b/bullet_train-sortable/bullet_train-sortable.gemspec @@ -5,7 +5,7 @@ Gem::Specification.new do |spec| spec.version = BulletTrain::Sortable::VERSION spec.authors = ["Andrew Culver"] spec.email = ["andrew.culver@gmail.com"] - spec.homepage = "https://github.com/bullet-train-co/bullet_train-sortable" + spec.homepage = "https://github.com/bullet-train-co/bullet_train-core/tree/main/bullet_train-sortable" spec.summary = "Bullet Train Sortable" spec.description = spec.summary spec.license = "MIT" diff --git a/bullet_train-sortable/lib/bullet_train/sortable/version.rb b/bullet_train-sortable/lib/bullet_train/sortable/version.rb index eb3edbf2a..09dfe267c 100644 --- a/bullet_train-sortable/lib/bullet_train/sortable/version.rb +++ b/bullet_train-sortable/lib/bullet_train/sortable/version.rb @@ -1,5 +1,5 @@ module BulletTrain module Sortable - VERSION = "1.2.21" + VERSION = "1.2.27" end end diff --git a/bullet_train-super_load_and_authorize_resource/bullet_train-super_load_and_authorize_resource.gemspec b/bullet_train-super_load_and_authorize_resource/bullet_train-super_load_and_authorize_resource.gemspec index f7bd260ea..9397a7ca2 100644 --- a/bullet_train-super_load_and_authorize_resource/bullet_train-super_load_and_authorize_resource.gemspec +++ b/bullet_train-super_load_and_authorize_resource/bullet_train-super_load_and_authorize_resource.gemspec @@ -5,7 +5,7 @@ Gem::Specification.new do |spec| spec.version = BulletTrain::SuperLoadAndAuthorizeResource::VERSION spec.authors = ["Andrew Culver"] spec.email = ["andrew.culver@gmail.com"] - spec.homepage = "https://github.com/bullet-train-co/bullet_train-super_load_and_authorize_resource" + spec.homepage = "https://github.com/bullet-train-co/bullet_train-core/tree/main/bullet_train-super_load_and_authorize_resource" spec.summary = "Bullet Train Super Load And Authorize Resource" spec.description = spec.summary spec.license = "MIT" diff --git a/bullet_train-super_load_and_authorize_resource/lib/bullet_train/super_load_and_authorize_resource/version.rb b/bullet_train-super_load_and_authorize_resource/lib/bullet_train/super_load_and_authorize_resource/version.rb index 63503f8c5..bf9e8875e 100644 --- a/bullet_train-super_load_and_authorize_resource/lib/bullet_train/super_load_and_authorize_resource/version.rb +++ b/bullet_train-super_load_and_authorize_resource/lib/bullet_train/super_load_and_authorize_resource/version.rb @@ -1,5 +1,5 @@ module BulletTrain module SuperLoadAndAuthorizeResource - VERSION = "1.2.21" + VERSION = "1.2.27" end end diff --git a/bullet_train-super_scaffolding/.circleci/config.yml b/bullet_train-super_scaffolding/.circleci/config.yml index ee2749a97..09c6ef7f5 100644 --- a/bullet_train-super_scaffolding/.circleci/config.yml +++ b/bullet_train-super_scaffolding/.circleci/config.yml @@ -26,7 +26,7 @@ aliases: paths: - node_modules - &ruby_node_browsers_docker_image - - image: cimg/ruby:3.1.2-browsers + - image: cimg/ruby:3.2.2-browsers environment: PGHOST: localhost PGUSER: untitled_application diff --git a/bullet_train-super_scaffolding/.standard.yml b/bullet_train-super_scaffolding/.standard.yml index 3d51bfbed..747454221 100644 --- a/bullet_train-super_scaffolding/.standard.yml +++ b/bullet_train-super_scaffolding/.standard.yml @@ -1,5 +1,3 @@ ignore: - 'lib/scaffolding/transformer.rb': - Layout/EndAlignment - # TODO Fix these files up for Standard Ruby. - - 'lib/bullet_train/super_scaffolding/scaffolders/oauth_provider_scaffolder.rb' diff --git a/bullet_train-super_scaffolding/app/controllers/account/scaffolding/completely_concrete/tangible_things_controller.rb b/bullet_train-super_scaffolding/app/controllers/account/scaffolding/completely_concrete/tangible_things_controller.rb index 2b142ab7d..484c1b9f8 100644 --- a/bullet_train-super_scaffolding/app/controllers/account/scaffolding/completely_concrete/tangible_things_controller.rb +++ b/bullet_train-super_scaffolding/app/controllers/account/scaffolding/completely_concrete/tangible_things_controller.rb @@ -68,7 +68,6 @@ def destroy def process_params(strong_params) # π skip this section when scaffolding. assign_boolean(strong_params, :boolean_button_value) - assign_date_and_time(strong_params, :date_and_time_field_value) assign_checkboxes(strong_params, :multiple_button_values) assign_checkboxes(strong_params, :multiple_option_values) assign_select_options(strong_params, :multiple_super_select_values) diff --git a/bullet_train-super_scaffolding/app/controllers/api/v1/scaffolding/completely_concrete/tangible_things_controller.rb b/bullet_train-super_scaffolding/app/controllers/api/v1/scaffolding/completely_concrete/tangible_things_controller.rb index a61cac235..f8d1970c6 100644 --- a/bullet_train-super_scaffolding/app/controllers/api/v1/scaffolding/completely_concrete/tangible_things_controller.rb +++ b/bullet_train-super_scaffolding/app/controllers/api/v1/scaffolding/completely_concrete/tangible_things_controller.rb @@ -53,7 +53,6 @@ def tangible_thing_params :cloudinary_image_value, :date_field_value, :date_and_time_field_value, - :date_and_time_field_value_time_zone, :email_field_value, :file_field_value, :file_field_value_removal, diff --git a/bullet_train-super_scaffolding/app/views/account/scaffolding/absolutely_abstract/creative_concepts/_index.html.erb b/bullet_train-super_scaffolding/app/views/account/scaffolding/absolutely_abstract/creative_concepts/_index.html.erb index e187190b9..ffff23cbd 100644 --- a/bullet_train-super_scaffolding/app/views/account/scaffolding/absolutely_abstract/creative_concepts/_index.html.erb +++ b/bullet_train-super_scaffolding/app/views/account/scaffolding/absolutely_abstract/creative_concepts/_index.html.erb @@ -6,7 +6,7 @@ <% pagy ||= nil %> <% pagy, creative_concepts = pagy(creative_concepts, page_param: :creative_concepts_page) unless pagy %> -<%= updates_for context, :scaffolding_absolutely_abstract_creative_concepts do %> +<%= cable_ready_updates_for context, :scaffolding_absolutely_abstract_creative_concepts do %> <%= render 'account/shared/box' do |box| %> <% box.title t(".contexts.#{context.class.name.underscore}.header") %> <% box.description do %> @@ -27,9 +27,7 @@
- <% creative_concepts.each do |creative_concept| %> - <%= render 'account/scaffolding/absolutely_abstract/creative_concepts/creative_concept', creative_concept: creative_concept %> - <% end %> + <%= render partial: 'account/scaffolding/absolutely_abstract/creative_concepts/creative_concept', collection: creative_concepts %> <% end %> @@ -43,7 +41,7 @@ <%# π super scaffolding will insert new targets one parent action model buttons above this line. %> <%# π super scaffolding will insert new bulk action model buttons above this line. %> - <%= render "shared/bulk_action_select" %> + <%= render "shared/bulk_action_select" if creative_concepts.many? %> <%= link_to t('global.buttons.back'), [:account, context], class: "#{first_button_primary(:absolutely_abstract_creative_concept)} back" unless hide_back %> <% end %> diff --git a/bullet_train-super_scaffolding/app/views/account/scaffolding/absolutely_abstract/creative_concepts/show.html.erb b/bullet_train-super_scaffolding/app/views/account/scaffolding/absolutely_abstract/creative_concepts/show.html.erb index 42a737277..107a33685 100644 --- a/bullet_train-super_scaffolding/app/views/account/scaffolding/absolutely_abstract/creative_concepts/show.html.erb +++ b/bullet_train-super_scaffolding/app/views/account/scaffolding/absolutely_abstract/creative_concepts/show.html.erb @@ -1,7 +1,7 @@ <%= render 'account/shared/page' do |page| %> <% page.title t('.section') %> <% page.body do %> - <%= updates_for @creative_concept do %> + <%= cable_ready_updates_for @creative_concept do %> <%= render 'account/shared/box', divider: true do |box| %> <% box.t :description, title: '.header' %> <% box.body do %> diff --git a/bullet_train-super_scaffolding/app/views/account/scaffolding/completely_concrete/tangible_things/_index.html.erb b/bullet_train-super_scaffolding/app/views/account/scaffolding/completely_concrete/tangible_things/_index.html.erb index af35588ba..40799f6f7 100644 --- a/bullet_train-super_scaffolding/app/views/account/scaffolding/completely_concrete/tangible_things/_index.html.erb +++ b/bullet_train-super_scaffolding/app/views/account/scaffolding/completely_concrete/tangible_things/_index.html.erb @@ -9,7 +9,7 @@ <% pagy, tangible_things = pagy(tangible_things, page_param: :tangible_things_page) unless pagy %> <%= action_model_select_controller do %> - <% updates_for context, collection do %> + <% cable_ready_updates_for context, collection do %> <%= render 'account/shared/box', pagy: pagy do |box| %> <% box.title t(".contexts.#{context.class.name.underscore}.header") %> <% box.description do %> @@ -35,9 +35,7 @@ - <% tangible_things.each do |tangible_thing| %> - <%= render 'account/scaffolding/completely_concrete/tangible_things/tangible_thing', tangible_thing: tangible_thing %> - <% end %> + <%= render partial: 'account/scaffolding/completely_concrete/tangible_things/tangible_thing', collection: tangible_things %> <% end %> @@ -53,7 +51,7 @@ <%# π super scaffolding will insert new targets one parent action model buttons above this line. %> <%# π super scaffolding will insert new bulk action model buttons above this line. %> - <%= render "shared/bulk_action_select" %> + <%= render "shared/bulk_action_select" if tangible_things.many? %> <% unless hide_back %> <%= link_to t('global.buttons.back'), [:account, context], class: "#{first_button_primary(:completely_concrete_tangible_thing)} back" %> diff --git a/bullet_train-super_scaffolding/app/views/account/scaffolding/completely_concrete/tangible_things/show.html.erb b/bullet_train-super_scaffolding/app/views/account/scaffolding/completely_concrete/tangible_things/show.html.erb index c93903e1e..612b076c1 100644 --- a/bullet_train-super_scaffolding/app/views/account/scaffolding/completely_concrete/tangible_things/show.html.erb +++ b/bullet_train-super_scaffolding/app/views/account/scaffolding/completely_concrete/tangible_things/show.html.erb @@ -1,7 +1,7 @@ <%= render 'account/shared/page' do |page| %> <% page.title t('.section') %> <% page.body do %> - <%= updates_for @tangible_thing do %> + <%= cable_ready_updates_for @tangible_thing do %> <%= render 'account/shared/box', divider: true do |box| %> <% box.title t('.header') %> <% box.description do %> diff --git a/bullet_train-super_scaffolding/app/views/api/v1/open_api/scaffolding/absolutely_abstract/creative_concepts/_paths.yaml.erb b/bullet_train-super_scaffolding/app/views/api/v1/open_api/scaffolding/absolutely_abstract/creative_concepts/_paths.yaml.erb new file mode 100644 index 000000000..b58b39a30 --- /dev/null +++ b/bullet_train-super_scaffolding/app/views/api/v1/open_api/scaffolding/absolutely_abstract/creative_concepts/_paths.yaml.erb @@ -0,0 +1,93 @@ +/teams/{team_id}/scaffolding/absolutely_abstract/creative_concepts: + get: + tags: + - "Scaffolding/Absolutely Abstract/Creative Concept" + summary: "List Creative Concept" + operationId: listScaffoldingAbsolutelyAbstractCreativeConcepts + parameters: + - name: team_id + in: path + required: true + schema: + type: string + responses: + "404": + description: "Not Found" + "200": + description: "OK" + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: "#/components/schemas/ScaffoldingAbsolutelyAbstractCreativeConceptAttributes" + has_more: + type: boolean + post: + tags: + - "Scaffolding/Absolutely Abstract/Creative Concept" + summary: "Create Creative Concept" + operationId: createScaffoldingAbsolutelyAbstractCreativeConcepts + parameters: + - name: team_id + in: path + required: true + schema: + type: string + responses: + "404": + description: "Not Found" + "201": + description: "Created" + content: + application/json: + schema: + $ref: "#/components/schemas/ScaffoldingAbsolutelyAbstractCreativeConceptParameters" +/scaffolding/absolutely_abstract/creative_concepts/{id}: + get: + tags: + - "Scaffolding/Absolutely Abstract/Creative Concept" + summary: "Fetch Creative Concept" + operationId: getScaffoldingAbsolutelyAbstractCreativeConcepts + parameters: + - $ref: "#/components/parameters/id" + responses: + "404": + description: "Not Found" + "200": + description: "OK" + content: + application/json: + schema: + $ref: "#/components/schemas/ScaffoldingAbsolutelyAbstractCreativeConceptAttributes" + put: + tags: + - "Scaffolding/Absolutely Abstract/Creative Concept" + summary: "Update Creative Concept" + operationId: updateScaffoldingAbsolutelyAbstractCreativeConcepts + parameters: + - $ref: "#/components/parameters/id" + responses: + "404": + description: "Not Found" + "200": + description: "OK" + content: + application/json: + schema: + $ref: "#/components/schemas/ScaffoldingAbsolutelyAbstractCreativeConceptParameters" + delete: + tags: + - "Scaffolding/Absolutely Abstract/Creative Concept" + summary: "Remove Creative Concept" + operationId: removeScaffoldingAbsolutelyAbstractCreativeConcepts + parameters: + - $ref: "#/components/parameters/id" + responses: + "404": + description: "Not Found" + "200": + description: "OK" diff --git a/bullet_train-super_scaffolding/app/views/api/v1/open_api/scaffolding/completely_concrete/tangible_things/_components.yaml.erb b/bullet_train-super_scaffolding/app/views/api/v1/open_api/scaffolding/completely_concrete/tangible_things/_components.yaml.erb deleted file mode 100644 index 62ffbcfd2..000000000 --- a/bullet_train-super_scaffolding/app/views/api/v1/open_api/scaffolding/completely_concrete/tangible_things/_components.yaml.erb +++ /dev/null @@ -1,32 +0,0 @@ -Scaffolding::CompletelyConcrete::TangibleThing::Attributes: - type: object - properties: - <%= attribute :id %> - <%= attribute :absolutely_abstract_creative_concept_id %> - <%# π skip this section when scaffolding. %> - <%= attribute :text_field_value %> - <%= attribute :button_value %> - <%= attribute :boolean_button_value %> - <%= attribute :cloudinary_image_value %> - <%= attribute :date_field_value %> - <%= attribute :email_field_value %> - <%= attribute :file_field_value %> - <%= attribute :password_field_value %> - <%= attribute :phone_field_value %> - <%= attribute :option_value %> - <%= attribute :multiple_option_values %> - <%= attribute :super_select_value %> - <%= attribute :text_area_value %> - <%= attribute :action_text_value %> - <%# π stop any skipping we're doing now. %> - <%# π super scaffolding will insert new attributes above this line. %> - <%= attribute :created_at %> - <%= attribute :updated_at %> - -Scaffolding::CompletelyConcrete::TangibleThing::Parameters: - type: object - properties: - <%# π skip this section when scaffolding. %> - <%= parameter :text_field_value %> - <%# π stop any skipping we're doing now. %> - <%# π super scaffolding will insert new parameter above this line. %> diff --git a/bullet_train-super_scaffolding/app/views/api/v1/open_api/scaffolding/completely_concrete/tangible_things/_paths.yaml.erb b/bullet_train-super_scaffolding/app/views/api/v1/open_api/scaffolding/completely_concrete/tangible_things/_paths.yaml.erb deleted file mode 100644 index d9e154c9f..000000000 --- a/bullet_train-super_scaffolding/app/views/api/v1/open_api/scaffolding/completely_concrete/tangible_things/_paths.yaml.erb +++ /dev/null @@ -1,93 +0,0 @@ -/scaffolding/absolutely_abstract/creative_concepts/{absolutely_abstract_creative_concept_id}/completely_concrete/tangible_things: - get: - tags: - - "Scaffolding/Completely Concrete/Tangible Things" - summary: "List Tangible Things" - operationId: listScaffoldingCompletelyConcreteTangibleThings - parameters: - - name: absolutely_abstract_creative_concept_id - in: path - required: true - schema: - type: string - responses: - "404": - description: "Not Found" - "200": - description: "OK" - content: - application/json: - schema: - type: object - properties: - data: - type: array - items: - $ref: "#/components/schemas/ScaffoldingCompletelyConcreteTangibleThingAttributes" - has_more: - type: boolean - post: - tags: - - "Scaffolding/Completely Concrete/Tangible Things" - summary: "Create Tangible Thing" - operationId: createScaffoldingCompletelyConcreteTangibleThings - parameters: - - name: absolutely_abstract_creative_concept_id - in: path - required: true - schema: - type: string - responses: - "404": - description: "Not Found" - "201": - description: "Created" - content: - application/json: - schema: - $ref: "#/components/schemas/ScaffoldingCompletelyConcreteTangibleThingParameters" -/scaffolding/completely_concrete/tangible_things/{id}: - get: - tags: - - "Scaffolding/Completely Concrete/Tangible Things" - summary: "Fetch Tangible Thing" - operationId: getScaffoldingCompletelyConcreteTangibleThings - parameters: - - $ref: "#/components/parameters/id" - responses: - "404": - description: "Not Found" - "200": - description: "OK" - content: - application/json: - schema: - $ref: "#/components/schemas/ScaffoldingCompletelyConcreteTangibleThingAttributes" - put: - tags: - - "Scaffolding/Completely Concrete/Tangible Things" - summary: "Update Tangible Thing" - operationId: updateScaffoldingCompletelyConcreteTangibleThings - parameters: - - $ref: "#/components/parameters/id" - responses: - "404": - description: "Not Found" - "200": - description: "OK" - content: - application/json: - schema: - $ref: "#/components/schemas/ScaffoldingCompletelyConcreteTangibleThingParameters" - delete: - tags: - - "Scaffolding/Completely Concrete/Tangible Things" - summary: "Remove Tangible Thing" - operationId: removeScaffoldingCompletelyConcreteTangibleThings - parameters: - - $ref: "#/components/parameters/id" - responses: - "404": - description: "Not Found" - "200": - description: "OK" diff --git a/bullet_train-super_scaffolding/app/views/api/v1/scaffolding/absolutely_abstract/creative_concepts/_creative_concept.json.jbuilder b/bullet_train-super_scaffolding/app/views/api/v1/scaffolding/absolutely_abstract/creative_concepts/_creative_concept.json.jbuilder index fbaf36249..922d243f7 100644 --- a/bullet_train-super_scaffolding/app/views/api/v1/scaffolding/absolutely_abstract/creative_concepts/_creative_concept.json.jbuilder +++ b/bullet_train-super_scaffolding/app/views/api/v1/scaffolding/absolutely_abstract/creative_concepts/_creative_concept.json.jbuilder @@ -1,5 +1,6 @@ json.extract! creative_concept, :id, + :team_id, :name, :description, :created_at, diff --git a/bullet_train-super_scaffolding/bullet_train-super_scaffolding.gemspec b/bullet_train-super_scaffolding/bullet_train-super_scaffolding.gemspec index e8e8e3b60..1009ab9a8 100644 --- a/bullet_train-super_scaffolding/bullet_train-super_scaffolding.gemspec +++ b/bullet_train-super_scaffolding/bullet_train-super_scaffolding.gemspec @@ -5,7 +5,7 @@ Gem::Specification.new do |spec| spec.version = BulletTrain::SuperScaffolding::VERSION spec.authors = ["Andrew Culver"] spec.email = ["andrew.culver@gmail.com"] - spec.homepage = "https://github.com/bullet-train-co/bullet_train-super_scaffolding" + spec.homepage = "https://github.com/bullet-train-co/bullet_train-core/tree/main/bullet_train-super_scaffolding" spec.summary = "Bullet Train Super Scaffolding" spec.description = spec.summary spec.license = "MIT" diff --git a/bullet_train-super_scaffolding/config/locales/en/scaffolding/completely_concrete/tangible_things.en.yml b/bullet_train-super_scaffolding/config/locales/en/scaffolding/completely_concrete/tangible_things.en.yml index ba88da778..ef7cbdaa4 100644 --- a/bullet_train-super_scaffolding/config/locales/en/scaffolding/completely_concrete/tangible_things.en.yml +++ b/bullet_train-super_scaffolding/config/locales/en/scaffolding/completely_concrete/tangible_things.en.yml @@ -18,6 +18,8 @@ en: confirmations: # TODO customize for your use-case. destroy: Are you sure you want to remove %{tangible_thing_name}? This will also remove any child resources and can't be undone. + tangible_thing: + buttons: *buttons fields: &fields id: heading: Tangible Thing ID diff --git a/bullet_train-super_scaffolding/lib/bullet_train/super_scaffolding/scaffolders/crud_scaffolder.rb b/bullet_train-super_scaffolding/lib/bullet_train/super_scaffolding/scaffolders/crud_scaffolder.rb index 807d6a33d..d066e177b 100644 --- a/bullet_train-super_scaffolding/lib/bullet_train/super_scaffolding/scaffolders/crud_scaffolder.rb +++ b/bullet_train-super_scaffolding/lib/bullet_train/super_scaffolding/scaffolders/crud_scaffolder.rb @@ -12,8 +12,8 @@ def run puts " bin/super-scaffold crud Site Team name:text_field url:text_area" puts "" puts "E.g. a Section belongs to a Page, which belongs to a Site, which belongs to a Team:" - puts " rails g model Section page:references title:text body:text" - puts " bin/super-scaffold crud Section Page,Site,Team title:text_area body:text_area" + puts " rails g model Section page:references title:string body:text" + puts " bin/super-scaffold crud Section Page,Site,Team title:text_field body:text_area" puts "" puts "E.g. an Image belongs to either a Page or a Site:" puts " Doable! See https://bit.ly/2NvO8El for a step by step guide." diff --git a/bullet_train-super_scaffolding/lib/bullet_train/super_scaffolding/scaffolders/oauth_provider_scaffolder.rb b/bullet_train-super_scaffolding/lib/bullet_train/super_scaffolding/scaffolders/oauth_provider_scaffolder.rb index 3cb2a34c3..37f6ddd18 100644 --- a/bullet_train-super_scaffolding/lib/bullet_train/super_scaffolding/scaffolders/oauth_provider_scaffolder.rb +++ b/bullet_train-super_scaffolding/lib/bullet_train/super_scaffolding/scaffolders/oauth_provider_scaffolder.rb @@ -5,13 +5,15 @@ class OauthProviderScaffolder < Scaffolder def run unless argv.count >= 5 puts "" - puts "π usage: bin/super-scaffold oauth-provider<%= object.send(attribute) %>+
<%= object.public_send(attribute) %><% end %> <% end %> diff --git a/bullet_train-themes-light/app/views/themes/light/attributes/_progress_bar.html.erb b/bullet_train-themes-light/app/views/themes/light/attributes/_progress_bar.html.erb index a25db3ccd..a38c0835c 100644 --- a/bullet_train-themes-light/app/views/themes/light/attributes/_progress_bar.html.erb +++ b/bullet_train-themes-light/app/views/themes/light/attributes/_progress_bar.html.erb @@ -4,7 +4,7 @@ <% hide_completed ||= false %> <% if object.send(total).present? %> - <% completion_percent = (object.send(attribute).to_f / object.send(total).to_f) * 100.0 %> + <% completion_percent = (object.public_send(attribute).to_f / object.send(total).to_f) * 100.0 %> <% unless completion_percent == 100 && hide_completed %> <%= render 'shared/attributes/attribute', object: object, attribute: "#{attribute}_over_#{total}".to_sym, strategy: strategy, url: url do %> diff --git a/bullet_train-themes-light/app/views/themes/light/conversations/_card.html.erb b/bullet_train-themes-light/app/views/themes/light/conversations/_card.html.erb index 1c5e9c433..f07aa664f 100644 --- a/bullet_train-themes-light/app/views/themes/light/conversations/_card.html.erb +++ b/bullet_train-themes-light/app/views/themes/light/conversations/_card.html.erb @@ -7,7 +7,7 @@
<%= "#{time_ago_in_words conversation.last_message.created_at} ago" unless conversation.last_message.nil? %>
+<%= "#{t('global.time_ago', time: time_ago_in_words(conversation.last_message.created_at))}" if conversation.last_message.present? %>