Skip to content

Commit

Permalink
add experiments model and endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
timcowlishaw committed Jul 20, 2024
1 parent 8657162 commit 68c5cb2
Show file tree
Hide file tree
Showing 14 changed files with 328 additions and 2 deletions.
59 changes: 59 additions & 0 deletions app/controllers/v0/experiments_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
module V0
class ExperimentsController < ApplicationController

before_action :check_if_authorized!, only: [:create, :update, :destroy]

def index
raise_ransack_errors_as_bad_request do
@q = Experiment.ransack(params[:q])
@q.sorts = 'id asc' if @q.sorts.empty?
@experiments = @q.result(distinct: true)
@experiments = paginate @experiments
end
end

def show
@experiment = Experiment.find(params[:id])
authorize @experiment
end

def create
@experiment = Experiment.new(experiment_params)
@experiment.owner ||= current_user
authorize @experiment
if @experiment.save
render :show, status: :created
else
raise Smartcitizen::UnprocessableEntity.new @experiment.errors
end
end

def update
@experiment = Experiment.find(params[:id])
authorize @experiment
if @experiment.update(experiment_params)
render :show, status: :ok
else
raise Smartcitizen::UnprocessableEntity.new @experiment.errors
end
end

def destroy
@experiment = Experiment.find(params[:id])
authorize @experiment
if @experiment.destroy!
render json: {message: 'OK'}, status: :ok
else
raise Smartcitizen::UnprocessableEntity.new @experiment.errors
end
end

private

def experiment_params
params.permit(
:name, :description, :active, :is_test, :starts_at, :ends_at, device_ids: []
)
end
end
end
2 changes: 2 additions & 0 deletions app/models/device.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ class Device < ActiveRecord::Base
has_many :sensors, through: :components
has_one :postprocessing, dependent: :destroy

has_and_belongs_to_many :experiments

accepts_nested_attributes_for :postprocessing, update_only: true

validates_presence_of :name
Expand Down
11 changes: 11 additions & 0 deletions app/models/experiment.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class Experiment < ApplicationRecord
belongs_to :owner, class_name: "User"
has_and_belongs_to_many :devices

validates_presence_of :name, :owner
validates_inclusion_of :is_test, in: [true, false]

def self.ransackable_attributes(auth_object = nil)
["created_at", "description", "ends_at", "id", "is_test", "name", "owner_id", "starts_at", "status", "updated_at"]
end
end
13 changes: 13 additions & 0 deletions app/policies/experiment_policy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class ExperimentPolicy < ApplicationPolicy
def update?
user.try(:is_admin?) || user == record.owner
end

def create?
user
end

def destroy?
update?
end
end
2 changes: 2 additions & 0 deletions app/views/v0/devices/_device.jbuilder
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,6 @@ end

json.data device.formatted_data if local_assigns[:with_data]

json.experiment_ids device.experiment_ids


3 changes: 3 additions & 0 deletions app/views/v0/experiments/_experiment.jbuilder
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
json.(experiment,
:id, :name, :description, :owner_id, :active, :is_test, :starts_at, :ends_at, :device_ids, :created_at, :updated_at
)
1 change: 1 addition & 0 deletions app/views/v0/experiments/index.jbuilder
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
json.array! @experiments, partial: 'experiment', as: :experiment
1 change: 1 addition & 0 deletions app/views/v0/experiments/show.jbuilder
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
json.partial! "experiment", experiment: @experiment
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
resources :sensors, except: [:destroy]
resources :components, only: [:show, :index]
resources :sessions, only: :create
resources :experiments

resources :uploads, path: 'avatars' do
post 'uploaded' => 'uploads#uploaded', on: :collection
Expand Down
22 changes: 22 additions & 0 deletions db/migrate/20240718054447_create_experiments.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
class CreateExperiments < ActiveRecord::Migration[6.1]
def change
create_table :experiments do |t|
t.string :name, null: false
t.string :description
t.belongs_to :owner, index: true
t.boolean :active, null: false, default: true
t.boolean :is_test, null: false, default: false
t.datetime :starts_at
t.datetime :ends_at
t.timestamps
end
add_foreign_key :experiments, :users, column: :owner_id

create_table :devices_experiments, id: false do |t|
t.belongs_to :device, index: true
t.belongs_to :experiment, index: true
end
add_foreign_key :devices_experiments, :devices, column: :device_id
add_foreign_key :devices_experiments, :experiments, column: :experiment_id
end
end
25 changes: 24 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 2024_06_24_175242) do
ActiveRecord::Schema.define(version: 2024_07_18_054447) do

# These are extensions that must be enabled in order to support this database
enable_extension "adminpack"
Expand Down Expand Up @@ -116,6 +116,13 @@
t.index ["workflow_state"], name: "index_devices_on_workflow_state"
end

create_table "devices_experiments", id: false, force: :cascade do |t|
t.bigint "device_id"
t.bigint "experiment_id"
t.index ["device_id"], name: "index_devices_experiments_on_device_id"
t.index ["experiment_id"], name: "index_devices_experiments_on_experiment_id"
end

create_table "devices_inventory", id: :serial, force: :cascade do |t|
t.jsonb "report", default: {}
t.datetime "created_at"
Expand All @@ -129,6 +136,19 @@
t.index ["device_id", "tag_id"], name: "index_devices_tags_on_device_id_and_tag_id", unique: true
end

create_table "experiments", force: :cascade do |t|
t.string "name", null: false
t.string "description"
t.bigint "owner_id"
t.boolean "active", default: true, null: false
t.boolean "is_test", default: false, null: false
t.datetime "starts_at"
t.datetime "ends_at"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["owner_id"], name: "index_experiments_on_owner_id"
end

create_table "friendly_id_slugs", id: :serial, force: :cascade do |t|
t.string "slug", null: false
t.integer "sluggable_id", null: false
Expand Down Expand Up @@ -313,8 +333,11 @@
add_foreign_key "api_tokens", "users", column: "owner_id"
add_foreign_key "components", "devices"
add_foreign_key "components", "sensors"
add_foreign_key "devices_experiments", "devices"
add_foreign_key "devices_experiments", "experiments"
add_foreign_key "devices_tags", "devices"
add_foreign_key "devices_tags", "tags"
add_foreign_key "experiments", "users", column: "owner_id"
add_foreign_key "postprocessings", "devices"
add_foreign_key "sensors", "measurements"
add_foreign_key "uploads", "users"
Expand Down
9 changes: 9 additions & 0 deletions spec/factories/experiment.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FactoryBot.define do
factory :experiment do
sequence("name") { |n| "experiment#{n}"}
description { "my experiment" }
association :owner, factory: :user
active { true }
is_test { false }
end
end
2 changes: 1 addition & 1 deletion spec/requests/v0/devices_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
expect(json.length).to eq(2)
# expect(json[0]['name']).to eq(first.name)
# expect(json[1]['name']).to eq(second.name)
expect(json[0].keys).to eq(%w(id uuid name description state system_tags user_tags last_reading_at created_at updated_at notify device_token postprocessing location data_policy hardware owner data))
expect(json[0].keys).to eq(%w(id uuid name description state system_tags user_tags last_reading_at created_at updated_at notify device_token postprocessing location data_policy hardware owner data experiment_ids))
end

describe "when not logged in" do
Expand Down
179 changes: 179 additions & 0 deletions spec/requests/v0/experiments_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
require 'rails_helper'

describe V0::ExperimentsController do

let(:application) { create :application }

let(:citizen_user) { create :user }
let(:citizen_token) { create :access_token, application: application, resource_owner_id: citizen_user.id }

let(:admin_user) { create :admin }
let(:admin_token) { create :access_token, application: application, resource_owner_id: admin_user.id }

let(:owner_user) { create :user }
let(:owner_token) { create :access_token, application: application, resource_owner_id: owner_user.id }

let(:experiment) {
create(:experiment, name: "Existing experiment", owner: owner_user)
}

let(:device) { create(:device) }
let(:valid_params) {
{
name: "test experiment",
description: "a test experiment",
is_test: false,
active: true,
starts_at: "2024-01-01T00:00:00Z",
ends_at: "2024-06-30T23:59:59Z",
device_ids: [ device.id ]
}
}

describe "GET /experiments" do
it "lists all experiments" do
first = create(:experiment, name: "first experiment")
second = create(:experiment, name: "second experiment")
j = api_get 'experiments'

expect(j.length).to eq(2)
expect(j[0]['name']).to eq('first experiment')
expect(j[1]['name']).to eq('second experiment')
expect(response.status).to eq(200)
end
end

describe "POST /experiments" do
context "When no user is logged in" do
it "does not create an experiment" do
before_count = Experiment.count
api_post "experiments", valid_params

expect(response.status).to eq(401)
expect(Experiment.count).to eq(before_count)
end
end

context "When a user is logged in" do
it "creates an experiment" do
before_count = Experiment.count
api_post "experiments", valid_params.merge(access_token: citizen_token.token)

expect(response.status).to eq(201)
expect(Experiment.count).to eq(before_count + 1)
created = Experiment.last
expect(created.name).to eq(valid_params[:name])
expect(created.description).to eq(valid_params[:description])
expect(created.is_test).to eq(valid_params[:is_test])
expect(created.active).to eq(valid_params[:active])
expect(created.starts_at).to eq(Time.parse(valid_params[:starts_at]))
expect(created.ends_at).to eq(Time.parse(valid_params[:ends_at]))
expect(created.device_ids).to eq(valid_params[:device_ids])
end
end
end

describe "GET /experiments/:id" do

it "returns the experiment" do
json = api_get "experiments/#{experiment.id}"

expect(response.status).to eq(200)
expect(json["name"]).to eq experiment.name
end
end

describe "PUT /experiments/:id" do
context "When no user is logged in" do
it "does not update the experiment" do
json = api_put "experiments/#{experiment.id}", valid_params

updated = Experiment.find(experiment.id)
expect(response.status).to eq(401)
expect(updated).to eq(experiment)
end
end

context "When the experiment owner is logged in" do
it "updates the experiment" do
json = api_put "experiments/#{experiment.id}", valid_params.merge(access_token: owner_token.token)

updated = Experiment.find(experiment.id)
expect(updated.name).to eq(valid_params[:name])
expect(updated.description).to eq(valid_params[:description])
expect(updated.is_test).to eq(valid_params[:is_test])
expect(updated.active).to eq(valid_params[:active])
expect(updated.starts_at).to eq(Time.parse(valid_params[:starts_at]))
expect(updated.ends_at).to eq(Time.parse(valid_params[:ends_at]))
expect(updated.device_ids).to eq(valid_params[:device_ids])
end
end

context "When a different user is logged in" do
it "does not update the experiment" do
json = api_put "experiments/#{experiment.id}", valid_params.merge(access_token: citizen_token.token)

expect(response.status).to eq(403)
updated = Experiment.find(experiment.id)
expect(updated).to eq(experiment)
end
end

context "When an admin is logged in" do
it "updates the experiment" do
json = api_put "experiments/#{experiment.id}", valid_params.merge(access_token: admin_token.token)

updated = Experiment.find(experiment.id)
expect(updated.name).to eq(valid_params[:name])
expect(updated.description).to eq(valid_params[:description])
expect(updated.is_test).to eq(valid_params[:is_test])
expect(updated.active).to eq(valid_params[:active])
expect(updated.starts_at).to eq(Time.parse(valid_params[:starts_at]))
expect(updated.ends_at).to eq(Time.parse(valid_params[:ends_at]))
expect(updated.device_ids).to eq(valid_params[:device_ids])
end
end
end

describe "DELETE /experiments/:id" do
context "When no user is logged in" do
it "does not delete the experiment" do
json = api_delete "experiments/#{experiment.id}"

updated = Experiment.where(id: experiment.id).first
expect(response.status).to eq(401)
expect(updated).not_to be(nil)
end
end

context "When the experiment owner is logged in" do
it "deletes the experiment" do
json = api_delete "experiments/#{experiment.id}", access_token: owner_token.token

updated = Experiment.where(id: experiment.id).first
expect(response.status).to eq(200)
expect(updated).to be(nil)
end
end

context "When a different user is logged in" do
it "does not delete the experiment" do
json = api_delete "experiments/#{experiment.id}", access_token: citizen_token.token

updated = Experiment.where(id: experiment.id).first
expect(response.status).to eq(403)
expect(updated).not_to be(nil)
end
end

context "When an admin is logged in" do
it "deletes the experiment" do
json = api_delete "experiments/#{experiment.id}", access_token: admin_token.token

updated = Experiment.where(id: experiment.id).first
expect(response.status).to eq(200)
expect(updated).to be(nil)
end
end
end
end

0 comments on commit 68c5cb2

Please sign in to comment.