From 916ba4f8a64d27ab54c8480c5041559309b732d8 Mon Sep 17 00:00:00 2001 From: TheNeedful-DO Date: Sun, 3 Dec 2023 13:16:27 +0000 Subject: [PATCH] Add Command scaffold and bot join table --- app/assets/javascripts/commands.coffee | 3 + app/assets/stylesheets/commands.scss | 3 + app/controllers/commands_controller.rb | 81 +++++++++++ app/helpers/commands_helper.rb | 2 + app/models/bot.rb | 1 + app/models/command.rb | 7 + app/policies/command_policy.rb | 59 ++++++++ app/views/commands/_command.json.jbuilder | 2 + app/views/commands/_form.html.haml | 10 ++ app/views/commands/edit.html.haml | 7 + app/views/commands/index.html.haml | 26 ++++ app/views/commands/index.json.jbuilder | 1 + app/views/commands/new.html.haml | 5 + app/views/commands/show.html.haml | 15 ++ app/views/commands/show.json.jbuilder | 1 + config/routes.rb | 5 + db/migrate/20231202163732_create_commands.rb | 11 ++ ...2164132_create_join_table_bots_commands.rb | 8 ++ db/schema.rb | 15 +- spec/factories/commands.rb | 7 + spec/helpers/commands_helper_spec.rb | 15 ++ spec/models/command_spec.rb | 5 + spec/requests/commands_spec.rb | 135 ++++++++++++++++++ spec/routing/commands_routing_spec.rb | 38 +++++ spec/views/commands/edit.html.haml_spec.rb | 28 ++++ spec/views/commands/index.html.haml_spec.rb | 26 ++++ spec/views/commands/new.html.haml_spec.rb | 24 ++++ spec/views/commands/show.html.haml_spec.rb | 18 +++ 28 files changed, 557 insertions(+), 1 deletion(-) create mode 100644 app/assets/javascripts/commands.coffee create mode 100644 app/assets/stylesheets/commands.scss create mode 100644 app/controllers/commands_controller.rb create mode 100644 app/helpers/commands_helper.rb create mode 100644 app/models/command.rb create mode 100644 app/policies/command_policy.rb create mode 100644 app/views/commands/_command.json.jbuilder create mode 100644 app/views/commands/_form.html.haml create mode 100644 app/views/commands/edit.html.haml create mode 100644 app/views/commands/index.html.haml create mode 100644 app/views/commands/index.json.jbuilder create mode 100644 app/views/commands/new.html.haml create mode 100644 app/views/commands/show.html.haml create mode 100644 app/views/commands/show.json.jbuilder create mode 100644 db/migrate/20231202163732_create_commands.rb create mode 100644 db/migrate/20231202164132_create_join_table_bots_commands.rb create mode 100644 spec/factories/commands.rb create mode 100644 spec/helpers/commands_helper_spec.rb create mode 100644 spec/models/command_spec.rb create mode 100644 spec/requests/commands_spec.rb create mode 100644 spec/routing/commands_routing_spec.rb create mode 100644 spec/views/commands/edit.html.haml_spec.rb create mode 100644 spec/views/commands/index.html.haml_spec.rb create mode 100644 spec/views/commands/new.html.haml_spec.rb create mode 100644 spec/views/commands/show.html.haml_spec.rb diff --git a/app/assets/javascripts/commands.coffee b/app/assets/javascripts/commands.coffee new file mode 100644 index 00000000..24f83d18 --- /dev/null +++ b/app/assets/javascripts/commands.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/stylesheets/commands.scss b/app/assets/stylesheets/commands.scss new file mode 100644 index 00000000..f06df112 --- /dev/null +++ b/app/assets/stylesheets/commands.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the Commands controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: https://sass-lang.com/ diff --git a/app/controllers/commands_controller.rb b/app/controllers/commands_controller.rb new file mode 100644 index 00000000..d05f6873 --- /dev/null +++ b/app/controllers/commands_controller.rb @@ -0,0 +1,81 @@ +class CommandsController < ApplicationController + before_action :set_command, only: %i[ show edit update destroy run ] + + # GET /commands or /commands.json + def index + authorize @commands = Command.all + end + + # GET /commands/1 or /commands/1.json + def show + authorize @command + end + + # RUN /commands/1/run + def run + authorize @command + response_text = eval(@command.body) + redirect_to (request.referrer || @command), notice: response_text + end + + + # GET /commands/new + def new + @command = Command.new + end + + # GET /commands/1/edit + def edit + authorize @command + end + + # POST /commands or /commands.json + def create + @command = Command.new(command_params) + + respond_to do |format| + if @command.save + format.html { redirect_to command_url(@command), notice: "Command was successfully created." } + format.json { render :show, status: :created, location: @command } + else + format.html { render :new, status: :unprocessable_entity } + format.json { render json: @command.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /commands/1 or /commands/1.json + def update + authorize @command + respond_to do |format| + if @command.update(command_params) + format.html { redirect_to command_url(@command), notice: "Command was successfully updated." } + format.json { render :show, status: :ok, location: @command } + else + format.html { render :edit, status: :unprocessable_entity } + format.json { render json: @command.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /commands/1 or /commands/1.json + def destroy + @command.destroy + + respond_to do |format| + format.html { redirect_to commands_url, notice: "Command was successfully destroyed." } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_command + @command = Command.find(params[:id]) + end + + # Only allow a list of trusted parameters through. + def command_params + params.require(:command).permit(:name, :trigger, :body) + end +end diff --git a/app/helpers/commands_helper.rb b/app/helpers/commands_helper.rb new file mode 100644 index 00000000..9b0978ae --- /dev/null +++ b/app/helpers/commands_helper.rb @@ -0,0 +1,2 @@ +module CommandsHelper +end diff --git a/app/models/bot.rb b/app/models/bot.rb index a6b81a70..a74fcfa1 100644 --- a/app/models/bot.rb +++ b/app/models/bot.rb @@ -2,6 +2,7 @@ class Bot < ActiveRecord::Base belongs_to :user belongs_to :profile, optional: true serialize :telegram_subscriber_ids, Array + has_and_belongs_to_many :commands # def send_text # if self.telegram_api_token diff --git a/app/models/command.rb b/app/models/command.rb new file mode 100644 index 00000000..b747832b --- /dev/null +++ b/app/models/command.rb @@ -0,0 +1,7 @@ +class Command < ActiveRecord::Base + has_and_belongs_to_many :bots + + def display_name + self.name.blank? ? self.trigger.parameterize.titleize : self.name + end +end diff --git a/app/policies/command_policy.rb b/app/policies/command_policy.rb new file mode 100644 index 00000000..651d2c37 --- /dev/null +++ b/app/policies/command_policy.rb @@ -0,0 +1,59 @@ +class CommandPolicy + attr_reader :user, :model + + def initialize(user, model) + @user = user || User.new + @command = model + end + + class Scope + def initialize(user, scope) + @user = user + @scope = scope + end + + def resolve + if user.admin? + scope.all + else + scope.all + end + end + + private + + attr_reader :user, :scope + end + + def run? + @user.admin? || true + end + + def index? + true + end + + def show? + @user.admin? + end + + def new? + @user.admin? + end + + def create? + @user.admin? + end + + def edit? + @user.admin? + end + + def update? + @user.admin? + end + + def destroy? + @user.admin? + end +end diff --git a/app/views/commands/_command.json.jbuilder b/app/views/commands/_command.json.jbuilder new file mode 100644 index 00000000..d697c066 --- /dev/null +++ b/app/views/commands/_command.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! command, :id, :name, :trigger, :body, :created_at, :updated_at +json.url command_url(command, format: :json) diff --git a/app/views/commands/_form.html.haml b/app/views/commands/_form.html.haml new file mode 100644 index 00000000..0311d0be --- /dev/null +++ b/app/views/commands/_form.html.haml @@ -0,0 +1,10 @@ += simple_form_for(@command) do |f| + = f.error_notification + + .form-inputs + = f.input :name + = f.input :trigger + = f.input :body + + .form-actions + = f.button :submit diff --git a/app/views/commands/edit.html.haml b/app/views/commands/edit.html.haml new file mode 100644 index 00000000..0d50fbb5 --- /dev/null +++ b/app/views/commands/edit.html.haml @@ -0,0 +1,7 @@ +%h1 Editing command + += render 'form' + += link_to 'Show', @command +\| += link_to 'Back', commands_path diff --git a/app/views/commands/index.html.haml b/app/views/commands/index.html.haml new file mode 100644 index 00000000..dada3beb --- /dev/null +++ b/app/views/commands/index.html.haml @@ -0,0 +1,26 @@ +%h1 Listing commands + +%table + %thead + %tr + %th + %th Name + %th Trigger + - if policy(Command).edit? + %th Body + %th + %th + + %tbody + - @commands.each do |command| + %tr + %td= link_to 'Run', run_command_path(command) + %td= command.display_name + %td= command.trigger + - if policy(command).edit? + %td= command.body + %td= link_to 'Edit', edit_command_path(command) + %td= link_to 'Destroy', command, method: :delete, data: { confirm: 'Are you sure?' } + +- if policy(Command).new? + = link_to 'New Command', new_command_path diff --git a/app/views/commands/index.json.jbuilder b/app/views/commands/index.json.jbuilder new file mode 100644 index 00000000..36e67636 --- /dev/null +++ b/app/views/commands/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @commands, partial: "commands/command", as: :command diff --git a/app/views/commands/new.html.haml b/app/views/commands/new.html.haml new file mode 100644 index 00000000..67ec4fa3 --- /dev/null +++ b/app/views/commands/new.html.haml @@ -0,0 +1,5 @@ +%h1 New command + += render 'form' + += link_to 'Back', commands_path diff --git a/app/views/commands/show.html.haml b/app/views/commands/show.html.haml new file mode 100644 index 00000000..de33a91d --- /dev/null +++ b/app/views/commands/show.html.haml @@ -0,0 +1,15 @@ +%p#notice= notice + +%p + %b Name: + = @command.display_name +%p + %b Trigger: + = @command.trigger +%p + %b Body: + = @command.body + += link_to 'Edit', edit_command_path(@command) +\| += link_to 'Back', commands_path diff --git a/app/views/commands/show.json.jbuilder b/app/views/commands/show.json.jbuilder new file mode 100644 index 00000000..c31944e4 --- /dev/null +++ b/app/views/commands/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "commands/command", command: @command diff --git a/config/routes.rb b/config/routes.rb index 560e9fee..56397c08 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,9 @@ Rails.application.routes.draw do + resources :commands do + member do + get 'run' + end + end resources :bots do member do get 'telegram_listen' diff --git a/db/migrate/20231202163732_create_commands.rb b/db/migrate/20231202163732_create_commands.rb new file mode 100644 index 00000000..c3a997ba --- /dev/null +++ b/db/migrate/20231202163732_create_commands.rb @@ -0,0 +1,11 @@ +class CreateCommands < ActiveRecord::Migration[6.1] + def change + create_table :commands do |t| + t.string :name + t.string :trigger + t.text :body + + t.timestamps + end + end +end diff --git a/db/migrate/20231202164132_create_join_table_bots_commands.rb b/db/migrate/20231202164132_create_join_table_bots_commands.rb new file mode 100644 index 00000000..eb8edbe4 --- /dev/null +++ b/db/migrate/20231202164132_create_join_table_bots_commands.rb @@ -0,0 +1,8 @@ +class CreateJoinTableBotsCommands < ActiveRecord::Migration[6.1] + def change + create_join_table :bots, :commands do |t| + # t.index [:bot_id, :command_id] + # t.index [:command_id, :bot_id] + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 1c456786..3a09a8ee 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2023_12_01_182512) do +ActiveRecord::Schema.define(version: 2023_12_02_164132) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -28,6 +28,11 @@ ActiveRecord::Schema.define(version: 2023_12_01_182512) do t.index ["user_id"], name: "index_bots_on_user_id" end + create_table "bots_commands", id: false, force: :cascade do |t| + t.bigint "bot_id", null: false + t.bigint "command_id", null: false + end + create_table "canisters", force: :cascade do |t| t.string "icid" t.string "name" @@ -37,6 +42,14 @@ ActiveRecord::Schema.define(version: 2023_12_01_182512) do t.index ["user_id"], name: "index_canisters_on_user_id" end + create_table "commands", force: :cascade do |t| + t.string "name" + t.string "trigger" + t.text "body" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + end + create_table "delayed_jobs", force: :cascade do |t| t.integer "priority", default: 0, null: false t.integer "attempts", default: 0, null: false diff --git a/spec/factories/commands.rb b/spec/factories/commands.rb new file mode 100644 index 00000000..b1255456 --- /dev/null +++ b/spec/factories/commands.rb @@ -0,0 +1,7 @@ +FactoryGirl.define do + factory :command do + name "MyString" + trigger "MyString" + body "MyText" + end +end diff --git a/spec/helpers/commands_helper_spec.rb b/spec/helpers/commands_helper_spec.rb new file mode 100644 index 00000000..e4665162 --- /dev/null +++ b/spec/helpers/commands_helper_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +# Specs in this file have access to a helper object that includes +# the CommandsHelper. For example: +# +# describe CommandsHelper do +# describe "string concat" do +# it "concats two strings with spaces" do +# expect(helper.concat_strings("this","that")).to eq("this that") +# end +# end +# end +RSpec.describe CommandsHelper, type: :helper do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/command_spec.rb b/spec/models/command_spec.rb new file mode 100644 index 00000000..08251e62 --- /dev/null +++ b/spec/models/command_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Command, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/requests/commands_spec.rb b/spec/requests/commands_spec.rb new file mode 100644 index 00000000..a7ae4b4f --- /dev/null +++ b/spec/requests/commands_spec.rb @@ -0,0 +1,135 @@ +require 'rails_helper' + +# This spec was generated by rspec-rails when you ran the scaffold generator. +# It demonstrates how one might use RSpec to test the controller code that +# was generated by Rails when you ran the scaffold generator. +# +# It assumes that the implementation code is generated by the rails scaffold +# generator. If you are using any extension libraries to generate different +# controller code, this generated spec may or may not pass. +# +# It only uses APIs available in rails and/or rspec-rails. There are a number +# of tools you can use to make these specs even more expressive, but we're +# sticking to rails and rspec-rails APIs to keep things simple and stable. + +RSpec.describe "/commands", type: :request do + + # This should return the minimal set of attributes required to create a valid + # Command. As you add validations to Command, be sure to + # adjust the attributes here as well. + let(:valid_attributes) { + skip("Add a hash of attributes valid for your model") + } + + let(:invalid_attributes) { + skip("Add a hash of attributes invalid for your model") + } + + describe "GET /index" do + it "renders a successful response" do + Command.create! valid_attributes + get commands_url + expect(response).to be_successful + end + end + + describe "GET /show" do + it "renders a successful response" do + command = Command.create! valid_attributes + get command_url(command) + expect(response).to be_successful + end + end + + describe "GET /new" do + it "renders a successful response" do + get new_command_url + expect(response).to be_successful + end + end + + describe "GET /edit" do + it "renders a successful response" do + command = Command.create! valid_attributes + get edit_command_url(command) + expect(response).to be_successful + end + end + + describe "POST /create" do + context "with valid parameters" do + it "creates a new Command" do + expect { + post commands_url, params: { command: valid_attributes } + }.to change(Command, :count).by(1) + end + + it "redirects to the created command" do + post commands_url, params: { command: valid_attributes } + expect(response).to redirect_to(command_url(Command.last)) + end + end + + context "with invalid parameters" do + it "does not create a new Command" do + expect { + post commands_url, params: { command: invalid_attributes } + }.to change(Command, :count).by(0) + end + + + it "renders a successful response (i.e. to display the 'new' template)" do + post commands_url, params: { command: invalid_attributes } + expect(response).to be_successful + end + + end + end + + describe "PATCH /update" do + context "with valid parameters" do + let(:new_attributes) { + skip("Add a hash of attributes valid for your model") + } + + it "updates the requested command" do + command = Command.create! valid_attributes + patch command_url(command), params: { command: new_attributes } + command.reload + skip("Add assertions for updated state") + end + + it "redirects to the command" do + command = Command.create! valid_attributes + patch command_url(command), params: { command: new_attributes } + command.reload + expect(response).to redirect_to(command_url(command)) + end + end + + context "with invalid parameters" do + + it "renders a successful response (i.e. to display the 'edit' template)" do + command = Command.create! valid_attributes + patch command_url(command), params: { command: invalid_attributes } + expect(response).to be_successful + end + + end + end + + describe "DELETE /destroy" do + it "destroys the requested command" do + command = Command.create! valid_attributes + expect { + delete command_url(command) + }.to change(Command, :count).by(-1) + end + + it "redirects to the commands list" do + command = Command.create! valid_attributes + delete command_url(command) + expect(response).to redirect_to(commands_url) + end + end +end diff --git a/spec/routing/commands_routing_spec.rb b/spec/routing/commands_routing_spec.rb new file mode 100644 index 00000000..b690a36c --- /dev/null +++ b/spec/routing/commands_routing_spec.rb @@ -0,0 +1,38 @@ +require "rails_helper" + +RSpec.describe CommandsController, type: :routing do + describe "routing" do + it "routes to #index" do + expect(get: "/commands").to route_to("commands#index") + end + + it "routes to #new" do + expect(get: "/commands/new").to route_to("commands#new") + end + + it "routes to #show" do + expect(get: "/commands/1").to route_to("commands#show", id: "1") + end + + it "routes to #edit" do + expect(get: "/commands/1/edit").to route_to("commands#edit", id: "1") + end + + + it "routes to #create" do + expect(post: "/commands").to route_to("commands#create") + end + + it "routes to #update via PUT" do + expect(put: "/commands/1").to route_to("commands#update", id: "1") + end + + it "routes to #update via PATCH" do + expect(patch: "/commands/1").to route_to("commands#update", id: "1") + end + + it "routes to #destroy" do + expect(delete: "/commands/1").to route_to("commands#destroy", id: "1") + end + end +end diff --git a/spec/views/commands/edit.html.haml_spec.rb b/spec/views/commands/edit.html.haml_spec.rb new file mode 100644 index 00000000..4d0dd5de --- /dev/null +++ b/spec/views/commands/edit.html.haml_spec.rb @@ -0,0 +1,28 @@ +require 'rails_helper' + +RSpec.describe "commands/edit", type: :view do + let(:command) { + Command.create!( + name: "MyString", + trigger: "MyString", + body: "MyText" + ) + } + + before(:each) do + assign(:command, command) + end + + it "renders the edit command form" do + render + + assert_select "form[action=?][method=?]", command_path(command), "post" do + + assert_select "input[name=?]", "command[name]" + + assert_select "input[name=?]", "command[trigger]" + + assert_select "textarea[name=?]", "command[body]" + end + end +end diff --git a/spec/views/commands/index.html.haml_spec.rb b/spec/views/commands/index.html.haml_spec.rb new file mode 100644 index 00000000..2d605d18 --- /dev/null +++ b/spec/views/commands/index.html.haml_spec.rb @@ -0,0 +1,26 @@ +require 'rails_helper' + +RSpec.describe "commands/index", type: :view do + before(:each) do + assign(:commands, [ + Command.create!( + name: "Name", + trigger: "Trigger", + body: "MyText" + ), + Command.create!( + name: "Name", + trigger: "Trigger", + body: "MyText" + ) + ]) + end + + it "renders a list of commands" do + render + cell_selector = Rails::VERSION::STRING >= '7' ? 'div>p' : 'tr>td' + assert_select cell_selector, text: Regexp.new("Name".to_s), count: 2 + assert_select cell_selector, text: Regexp.new("Trigger".to_s), count: 2 + assert_select cell_selector, text: Regexp.new("MyText".to_s), count: 2 + end +end diff --git a/spec/views/commands/new.html.haml_spec.rb b/spec/views/commands/new.html.haml_spec.rb new file mode 100644 index 00000000..831a10ef --- /dev/null +++ b/spec/views/commands/new.html.haml_spec.rb @@ -0,0 +1,24 @@ +require 'rails_helper' + +RSpec.describe "commands/new", type: :view do + before(:each) do + assign(:command, Command.new( + name: "MyString", + trigger: "MyString", + body: "MyText" + )) + end + + it "renders new command form" do + render + + assert_select "form[action=?][method=?]", commands_path, "post" do + + assert_select "input[name=?]", "command[name]" + + assert_select "input[name=?]", "command[trigger]" + + assert_select "textarea[name=?]", "command[body]" + end + end +end diff --git a/spec/views/commands/show.html.haml_spec.rb b/spec/views/commands/show.html.haml_spec.rb new file mode 100644 index 00000000..c3e550fe --- /dev/null +++ b/spec/views/commands/show.html.haml_spec.rb @@ -0,0 +1,18 @@ +require 'rails_helper' + +RSpec.describe "commands/show", type: :view do + before(:each) do + assign(:command, Command.create!( + name: "Name", + trigger: "Trigger", + body: "MyText" + )) + end + + it "renders attributes in

" do + render + expect(rendered).to match(/Name/) + expect(rendered).to match(/Trigger/) + expect(rendered).to match(/MyText/) + end +end