From c2555bcf220fc5ac7d8cf3435e6de29a68fb4f79 Mon Sep 17 00:00:00 2001 From: Evangelos Giataganas Date: Thu, 10 Aug 2023 16:45:38 +0300 Subject: [PATCH 1/2] Active storage, direct file upload --- app/controllers/csv_uploads_controller.rb | 73 ++++++++++ app/helpers/csv_uploads_helper.rb | 4 + app/javascript/activestorage-directupload.js | 94 +++++++++++++ app/models/csv_processing_message.rb | 6 + app/models/csv_upload.rb | 25 ++++ app/models/local_authority.rb | 1 + app/views/csv_uploads/_csv_upload.html.erb | 20 +++ .../csv_uploads/_csv_upload.json.jbuilder | 4 + app/views/csv_uploads/_form.html.erb | 63 +++++++++ app/views/csv_uploads/edit.html.erb | 10 ++ app/views/csv_uploads/index.html.erb | 17 +++ app/views/csv_uploads/index.json.jbuilder | 3 + app/views/csv_uploads/new.html.erb | 9 ++ app/views/csv_uploads/show.html.erb | 13 ++ app/views/csv_uploads/show.json.jbuilder | 3 + app/views/home/index.html.erb | 4 +- config/locales/en.yml | 3 + config/routes.rb | 1 + config/storage.yml | 5 + ...te_active_storage_tables.active_storage.rb | 57 ++++++++ .../20230207121814_create_csv_uploads.rb | 9 ++ ...07221712_create_csv_processing_messages.rb | 10 ++ ...d_timestamps_to_csv_processing_messages.rb | 8 ++ db/schema.rb | 50 ++++++- package.json | 1 + spec/factories/csv_processing_messages.rb | 7 + spec/factories/csv_uploads.rb | 7 + spec/requests/csv_uploads_spec.rb | 132 ++++++++++++++++++ spec/routing/csv_uploads_routing_spec.rb | 39 ++++++ spec/system/log_in_spec.rb | 2 +- spec/views/csv_uploads/edit.html.erb_spec.rb | 23 +++ spec/views/csv_uploads/index.html.erb_spec.rb | 22 +++ spec/views/csv_uploads/new.html.erb_spec.rb | 19 +++ spec/views/csv_uploads/show.html.erb_spec.rb | 16 +++ yarn.lock | 12 ++ 35 files changed, 769 insertions(+), 3 deletions(-) create mode 100644 app/controllers/csv_uploads_controller.rb create mode 100644 app/helpers/csv_uploads_helper.rb create mode 100644 app/javascript/activestorage-directupload.js create mode 100644 app/models/csv_processing_message.rb create mode 100644 app/models/csv_upload.rb create mode 100644 app/views/csv_uploads/_csv_upload.html.erb create mode 100644 app/views/csv_uploads/_csv_upload.json.jbuilder create mode 100644 app/views/csv_uploads/_form.html.erb create mode 100644 app/views/csv_uploads/edit.html.erb create mode 100644 app/views/csv_uploads/index.html.erb create mode 100644 app/views/csv_uploads/index.json.jbuilder create mode 100644 app/views/csv_uploads/new.html.erb create mode 100644 app/views/csv_uploads/show.html.erb create mode 100644 app/views/csv_uploads/show.json.jbuilder create mode 100644 db/migrate/20230207120738_create_active_storage_tables.active_storage.rb create mode 100644 db/migrate/20230207121814_create_csv_uploads.rb create mode 100644 db/migrate/20230207221712_create_csv_processing_messages.rb create mode 100644 db/migrate/20230215181318_add_timestamps_to_csv_processing_messages.rb create mode 100644 spec/factories/csv_processing_messages.rb create mode 100644 spec/factories/csv_uploads.rb create mode 100644 spec/requests/csv_uploads_spec.rb create mode 100644 spec/routing/csv_uploads_routing_spec.rb create mode 100644 spec/views/csv_uploads/edit.html.erb_spec.rb create mode 100644 spec/views/csv_uploads/index.html.erb_spec.rb create mode 100644 spec/views/csv_uploads/new.html.erb_spec.rb create mode 100644 spec/views/csv_uploads/show.html.erb_spec.rb diff --git a/app/controllers/csv_uploads_controller.rb b/app/controllers/csv_uploads_controller.rb new file mode 100644 index 0000000..127207a --- /dev/null +++ b/app/controllers/csv_uploads_controller.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +class CsvUploadsController < ApplicationController + before_action :set_csv_upload, only: %i[show edit update destroy] + + # GET /csv_uploads or /csv_uploads.json + def index + @csv_uploads = CsvUpload.all + end + + # GET /csv_uploads/1 or /csv_uploads/1.json + def show; end + + # GET /csv_uploads/new + def new + @csv_upload = CsvUpload.new + end + + # GET /csv_uploads/1/edit + def edit; end + + # POST /csv_uploads or /csv_uploads.json + def create + @csv_upload = CsvUpload.new(csv_upload_params) + + respond_to do |format| + if @csv_upload.save + format.html do + redirect_to csv_upload_url(@csv_upload), notice: I18n.t("upload_successfully_created_notice") + end + format.json { render :show, status: :created, location: @csv_upload } + else + format.html { render :new, status: :unprocessable_entity } + format.json { render json: @csv_upload.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /csv_uploads/1 or /csv_uploads/1.json + def update + respond_to do |format| + if @csv_upload.update(csv_upload_params) + format.html { redirect_to csv_upload_url(@csv_upload), notice: I18n.t("upload_successfully_updated_notice") } + format.json { render :show, status: :ok, location: @csv_upload } + else + format.html { render :edit, status: :unprocessable_entity } + format.json { render json: @csv_upload.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /csv_uploads/1 or /csv_uploads/1.json + def destroy + @csv_upload.destroy + + respond_to do |format| + format.html { redirect_to csv_uploads_url, notice: I18n.t("upload_successfully_destroyed_notice") } + format.json { head :no_content } + end + end + + private + + # Use callbacks to share common setup or constraints between actions. + def set_csv_upload + @csv_upload = CsvUpload.find(params[:id]) + end + + # Only allow a list of trusted parameters through. + def csv_upload_params + params.require(:csv_upload).permit(:title, csv_files: []) + end +end diff --git a/app/helpers/csv_uploads_helper.rb b/app/helpers/csv_uploads_helper.rb new file mode 100644 index 0000000..20e6aa6 --- /dev/null +++ b/app/helpers/csv_uploads_helper.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +module CsvUploadsHelper +end diff --git a/app/javascript/activestorage-directupload.js b/app/javascript/activestorage-directupload.js new file mode 100644 index 0000000..0ebad59 --- /dev/null +++ b/app/javascript/activestorage-directupload.js @@ -0,0 +1,94 @@ +import * as activestorage from "@rails/activestorage" +import { DirectUpload } from "@rails/activestorage"; + +activestorage.start(); + +// only select 'data-direct-upload' file inputs +const input = document.querySelector('input[type=file][data-direct-upload-url]:not([disabled])'); + +// bound to file select +input.addEventListener('change', (event) => { + const fileInput = event.target; + + // if no title is given, put a title in from the provided file + let titleEl = document.getElementById('csv_upload_title'); + if(titleEl) { + if(titleEl.value === null || titleEl.value.match(/^ *$/) !== null) { + // get the filename, without the extension + titleEl.value = fileInput.value.split(/(\\|\/)/g).pop().replace('.csv', ''); + } + } + Array.from(input.files).forEach(file => uploadFile(file, fileInput)) + // clear the selected files from the file input + input.value = null +}) + +const uploadFile = (file, fileInput) => { + const url = input.dataset.directUploadUrl; + new Uploader(file, url, fileInput); +} + +class Uploader { + // constructs and starts upload + constructor(file, url, fileInput) { + // file object, with metadata + this.file = file; + // create a uid for general dom selection + this.uid = String(Date.now().toString(32) + Math.random().toString(16)).replace(/\./g, ''); + this.url = url; + this.upload = new DirectUpload(this.file, this.url, this); + this.fileInput = fileInput; + + // create the template in the capturing form + this.progressTemplate = this.fileInput.form.querySelector('template.directupload-progress').innerHTML + this.progressDiv = document.createElement('div') + this.fileInput.after(this.progressDiv); + this.fileInput.blur(); + + // add the uid as an ID to the new element + this.progressDiv.id = 'directupload-progress-' + this.uid; + if(this.progressTemplate != null) { + // create a new template containing the filename and immutable details + let templateWithFilename = document.createElement('div'); + templateWithFilename.innerHTML = this.progressTemplate; + templateWithFilename.querySelector('span.directupload-progress-filename').textContent = file.name; + this.progressTemplate = templateWithFilename.innerHTML; + + // initialise the new template at 0% progress, and insert in form + let parsedTemplate = this.progressTemplate.replaceAll('{{status}}', 'Uploading'); + parsedTemplate.replaceAll('{{progress}}', 0); + this.progressDiv.innerHTML = parsedTemplate; + } + + this.upload.create((error, blob) => { + if (error) { + // Notify that there was an error, set to 0% + this.progressDiv.innerHTML = this.progressTemplate + .replaceAll('{{status}}', 'Error') + .replaceAll('{{progress}}', 0) + } else { + // Add a hidden field to end of form containing details and signed id + const hiddenField = document.createElement('input') + hiddenField.setAttribute("type", "hidden"); + hiddenField.setAttribute("value", blob.signed_id); + hiddenField.name = input.name; + this.fileInput.form.appendChild(hiddenField); + this.progressDiv.innerHTML = this.progressTemplate + .replaceAll('{{status}}', 'Uploaded') + .replaceAll('{{progress}}', 100); + } + }) + } + + directUploadWillStoreFileWithXHR(request) { + request.upload.addEventListener("progress", + event => this.directUploadDidProgress(event)) + } + + // update the progress element + directUploadDidProgress(event) { + this.progressDiv.innerHTML = this.progressTemplate + .replaceAll('{{status}}', 'Uploading') + .replaceAll('{{progress}}', Math.max(1, Math.floor((event.loaded / event.total) * 100))); + } +} \ No newline at end of file diff --git a/app/models/csv_processing_message.rb b/app/models/csv_processing_message.rb new file mode 100644 index 0000000..c8e0fa3 --- /dev/null +++ b/app/models/csv_processing_message.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class CsvProcessingMessage < ApplicationRecord + belongs_to :csv_upload + enum message_type: { success: 0, error: 1 } +end diff --git a/app/models/csv_upload.rb b/app/models/csv_upload.rb new file mode 100644 index 0000000..82f5cb4 --- /dev/null +++ b/app/models/csv_upload.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class CsvUpload < ApplicationRecord + has_many_attached :csv_files + has_many :csv_processing_messages, dependent: :destroy + + def filtered_csv_processing_messages + success_message = csv_processing_messages + .where(message_type: 0) + .order(created_at: :desc) + .first + + error_messages = csv_processing_messages + .where(message_type: 1) + .order(created_at: :desc) + + if success_message.present? + error_messages.or( + CsvProcessingMessage.where(id: success_message.id) + ) + else + error_messages + end + end +end diff --git a/app/models/local_authority.rb b/app/models/local_authority.rb index 436fc47..bf982d8 100644 --- a/app/models/local_authority.rb +++ b/app/models/local_authority.rb @@ -2,6 +2,7 @@ class LocalAuthority < ApplicationRecord has_many :planning_applications, dependent: :destroy + has_many :csv_uploads, dependent: :nullify belongs_to :api_client, optional: true validates :name, presence: true diff --git a/app/views/csv_uploads/_csv_upload.html.erb b/app/views/csv_uploads/_csv_upload.html.erb new file mode 100644 index 0000000..3f4dfb5 --- /dev/null +++ b/app/views/csv_uploads/_csv_upload.html.erb @@ -0,0 +1,20 @@ +
+

+ + <%= csv_upload.title %> +

+

Progress

+ +

Attached files

+ +

+
\ No newline at end of file diff --git a/app/views/csv_uploads/_csv_upload.json.jbuilder b/app/views/csv_uploads/_csv_upload.json.jbuilder new file mode 100644 index 0000000..d91448e --- /dev/null +++ b/app/views/csv_uploads/_csv_upload.json.jbuilder @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +json.extract! csv_upload, :id, :title, :created_at, :updated_at +json.url csv_upload_url(csv_upload, format: :json) diff --git a/app/views/csv_uploads/_form.html.erb b/app/views/csv_uploads/_form.html.erb new file mode 100644 index 0000000..49f0dbe --- /dev/null +++ b/app/views/csv_uploads/_form.html.erb @@ -0,0 +1,63 @@ +<%= form_with(model: csv_upload) do |form| %> + <% if csv_upload.errors.any? %> +
+

<%= pluralize(csv_upload.errors.count, "error") %> prohibited this csv_upload from being saved:

+ + +
+ <% end %> + +
+ <%= form.label :title, class: 'govuk-label' %> + <%= form.text_field :title, class: 'govuk-file-upload' %> +
+ +
+ <%= form.label :csv_files, 'Upload CSV files', class: 'govuk-label' %> + <%= form.file_field :csv_files, multiple: true, accept: ".csv", direct_upload: true, class: 'govuk-file-upload' %> + +
+ +
+ + + CSV formats + + +
+ CSV files must be of the following format: + + + + + + <% ["reference","application_type_code","application_type","area","description","received_at","assessor","reviewer","decision","decision_issued_at","validated_at","uprn_","code","type","FULL","map_east","map_north","ward_code","ward_name","Neighbour_Consult_Expiry","Standard_Consult_Expiry","Overall_Expiry","Target_Determination_Date","Extension_of_Time_Date","PPA_DATE","Actual_Committee_Date","Permission_Expiry_Date","Appeal_Status","Date_Appeal_Lodged","Date_Appeal_Determined","DM_STATUS","DM_Deciesion","Development_Type","Application_Type","DM_Decision_Code"].each do |heading| %> + + <% end %> + + + + + <% ["reference","application_type_code","application_type","area","description","received_at","assessor","reviewer","decision","decision_issued_at","validated_at","uprn_","code","type","FULL","map_east","map_north","ward_code","ward_name","Neighbour_Consult_Expiry","Standard_Consult_Expiry","Overall_Expiry","Target_Determination_Date","Extension_of_Time_Date","PPA_DATE","Actual_Committee_Date","Permission_Expiry_Date","Appeal_Status","Date_Appeal_Lodged","Date_Appeal_Determined","DM_STATUS","DM_Deciesion","Development_Type","Application_Type","DM_Decision_Code"].each do |heading| %> + + <% end %> + + +
CSV formats
<%= heading %>
<%= heading %>
+
+
+ +
+ <%= form.submit 'Upload', {class: 'govuk-button'} %> +
+ + <%= javascript_include_tag "activestorage-directupload" %> +<% end %> \ No newline at end of file diff --git a/app/views/csv_uploads/edit.html.erb b/app/views/csv_uploads/edit.html.erb new file mode 100644 index 0000000..fbfdc20 --- /dev/null +++ b/app/views/csv_uploads/edit.html.erb @@ -0,0 +1,10 @@ +

Editing csv upload

+ +<%= render "form", csv_upload: @csv_upload %> + +
+ +
+ <%= link_to "Show this csv upload", @csv_upload %> | + <%= link_to "CSV Uploads", csv_uploads_path, { class: 'govuk-back-link' } %> +
\ No newline at end of file diff --git a/app/views/csv_uploads/index.html.erb b/app/views/csv_uploads/index.html.erb new file mode 100644 index 0000000..e6acf16 --- /dev/null +++ b/app/views/csv_uploads/index.html.erb @@ -0,0 +1,17 @@ +

CSV Uploads

+ +
+ <% if @csv_uploads.empty? %> +
+ No CSV files have been uploaded. +
+ <% end %> + <% @csv_uploads.each do |csv_upload| %> + <%= render csv_upload %> +

+ <%= link_to "Show this csv upload", csv_upload %> +

+ <% end %> +
+ +<%= link_to "New CSV Upload", new_csv_upload_path, {class: "govuk-body"} %> \ No newline at end of file diff --git a/app/views/csv_uploads/index.json.jbuilder b/app/views/csv_uploads/index.json.jbuilder new file mode 100644 index 0000000..2e05f7d --- /dev/null +++ b/app/views/csv_uploads/index.json.jbuilder @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +json.array! @csv_uploads, partial: "csv_uploads/csv_upload", as: :csv_upload diff --git a/app/views/csv_uploads/new.html.erb b/app/views/csv_uploads/new.html.erb new file mode 100644 index 0000000..c124e78 --- /dev/null +++ b/app/views/csv_uploads/new.html.erb @@ -0,0 +1,9 @@ +

New CSV Upload

+ +<%= render "form", csv_upload: @csv_upload %> + +
+ +
+ <%= link_to "CSV Uploads", csv_uploads_path, { class: 'govuk-back-link' } %> +
\ No newline at end of file diff --git a/app/views/csv_uploads/show.html.erb b/app/views/csv_uploads/show.html.erb new file mode 100644 index 0000000..078017b --- /dev/null +++ b/app/views/csv_uploads/show.html.erb @@ -0,0 +1,13 @@ +<%= render @csv_upload %> + +
+ + + +
+ <%= button_to "Destroy this csv upload", @csv_upload, { class: 'govuk-button govuk-button--warning', method: :delete } %> +
+ + <%= link_to "CSV Uploads", csv_uploads_path, { class: 'govuk-back-link' } %> + +
\ No newline at end of file diff --git a/app/views/csv_uploads/show.json.jbuilder b/app/views/csv_uploads/show.json.jbuilder new file mode 100644 index 0000000..73c2789 --- /dev/null +++ b/app/views/csv_uploads/show.json.jbuilder @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +json.partial! "csv_uploads/csv_upload", csv_upload: @csv_upload diff --git a/app/views/home/index.html.erb b/app/views/home/index.html.erb index 7018601..703324e 100644 --- a/app/views/home/index.html.erb +++ b/app/views/home/index.html.erb @@ -1,2 +1,4 @@

Planning Register

-

This service allows ...

+

Welcome <%= current_user.email %>

+ +

<%= link_to "Upload a CSV", csv_uploads_path %>

diff --git a/config/locales/en.yml b/config/locales/en.yml index b456d6a..4910816 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,2 +1,5 @@ en: page_title: PAAPI + upload_successfully_created_notice: "Upload was successfully created. You can now monitor it's import progress." + upload_successfully_destroyed_notice: "CSV Upload was successfully destroyed." + upload_successfully_updated_notice: "CSV Upload was successfully updated." diff --git a/config/routes.rb b/config/routes.rb index 0e49a05..4c4b0d7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -7,6 +7,7 @@ devise_for :users + resources :csv_uploads get :healthcheck, to: proc { [200, {}, %w[OK]] } namespace :api do diff --git a/config/storage.yml b/config/storage.yml index 4942ab6..ee93f44 100644 --- a/config/storage.yml +++ b/config/storage.yml @@ -6,6 +6,11 @@ local: service: Disk root: <%= Rails.root.join("storage") %> +amazon: + service: S3 + region: <%= (ENV["AWS_REGION"] || "eu-west-2").inspect %> + bucket: <%= ENV["S3_BUCKET"].inspect %> + # Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) # amazon: # service: S3 diff --git a/db/migrate/20230207120738_create_active_storage_tables.active_storage.rb b/db/migrate/20230207120738_create_active_storage_tables.active_storage.rb new file mode 100644 index 0000000..1797c4d --- /dev/null +++ b/db/migrate/20230207120738_create_active_storage_tables.active_storage.rb @@ -0,0 +1,57 @@ +# This migration comes from active_storage (originally 20170806125915) +class CreateActiveStorageTables < ActiveRecord::Migration[5.2] + def change + # Use Active Record's configured type for primary and foreign keys + primary_key_type, foreign_key_type = primary_and_foreign_key_types + + create_table :active_storage_blobs, id: primary_key_type do |t| + t.string :key, null: false + t.string :filename, null: false + t.string :content_type + t.text :metadata + t.string :service_name, null: false + t.bigint :byte_size, null: false + t.string :checksum + + if connection.supports_datetime_with_precision? + t.datetime :created_at, precision: 6, null: false + else + t.datetime :created_at, null: false + end + + t.index [ :key ], unique: true + end + + create_table :active_storage_attachments, id: primary_key_type do |t| + t.string :name, null: false + t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type + t.references :blob, null: false, type: foreign_key_type + + if connection.supports_datetime_with_precision? + t.datetime :created_at, precision: 6, null: false + else + t.datetime :created_at, null: false + end + + t.index [ :record_type, :record_id, :name, :blob_id ], name: :index_active_storage_attachments_uniqueness, unique: true + t.foreign_key :active_storage_blobs, column: :blob_id + end + + create_table :active_storage_variant_records, id: primary_key_type do |t| + t.belongs_to :blob, null: false, index: false, type: foreign_key_type + t.string :variation_digest, null: false + + t.index [ :blob_id, :variation_digest ], name: :index_active_storage_variant_records_uniqueness, unique: true + t.foreign_key :active_storage_blobs, column: :blob_id + end + end + + private + def primary_and_foreign_key_types + config = Rails.configuration.generators + setting = config.options[config.orm][:primary_key_type] + primary_key_type = setting || :primary_key + foreign_key_type = setting || :bigint + [primary_key_type, foreign_key_type] + end + end \ No newline at end of file diff --git a/db/migrate/20230207121814_create_csv_uploads.rb b/db/migrate/20230207121814_create_csv_uploads.rb new file mode 100644 index 0000000..a9397f1 --- /dev/null +++ b/db/migrate/20230207121814_create_csv_uploads.rb @@ -0,0 +1,9 @@ +class CreateCsvUploads < ActiveRecord::Migration[7.0] + def change + create_table :csv_uploads do |t| + t.string :title + + t.timestamps + end + end + end \ No newline at end of file diff --git a/db/migrate/20230207221712_create_csv_processing_messages.rb b/db/migrate/20230207221712_create_csv_processing_messages.rb new file mode 100644 index 0000000..6171619 --- /dev/null +++ b/db/migrate/20230207221712_create_csv_processing_messages.rb @@ -0,0 +1,10 @@ +class CreateCsvProcessingMessages < ActiveRecord::Migration[7.0] + def change + create_table :csv_processing_messages do |t| + t.string :body + t.jsonb :data + t.integer :message_type, default: 0 + t.references :csv_upload, null: false + end + end + end \ No newline at end of file diff --git a/db/migrate/20230215181318_add_timestamps_to_csv_processing_messages.rb b/db/migrate/20230215181318_add_timestamps_to_csv_processing_messages.rb new file mode 100644 index 0000000..d21fe5d --- /dev/null +++ b/db/migrate/20230215181318_add_timestamps_to_csv_processing_messages.rb @@ -0,0 +1,8 @@ +class AddTimestampsToCsvProcessingMessages < ActiveRecord::Migration[7.0] + def change + add_column :csv_processing_messages, :created_at, :datetime + add_index :csv_processing_messages, :created_at + add_column :csv_processing_messages, :updated_at, :datetime + add_index :csv_processing_messages, :updated_at + end + end \ No newline at end of file diff --git a/db/schema.rb b/db/schema.rb index 2588bfa..9bb5a34 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,10 +10,38 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_01_30_150645) do +ActiveRecord::Schema[7.0].define(version: 2023_02_15_181318) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" + create_table "active_storage_attachments", force: :cascade do |t| + t.string "name", null: false + t.string "record_type", null: false + t.bigint "record_id", null: false + t.bigint "blob_id", null: false + t.datetime "created_at", null: false + t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" + t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true + end + + create_table "active_storage_blobs", force: :cascade do |t| + t.string "key", null: false + t.string "filename", null: false + t.string "content_type" + t.text "metadata" + t.string "service_name", null: false + t.bigint "byte_size", null: false + t.string "checksum" + t.datetime "created_at", null: false + t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true + end + + create_table "active_storage_variant_records", force: :cascade do |t| + t.bigint "blob_id", null: false + t.string "variation_digest", null: false + t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true + end + create_table "addresses", force: :cascade do |t| t.string "full", null: false t.string "town" @@ -39,6 +67,24 @@ t.index ["client_secret"], name: "index_api_clients_on_client_secret", unique: true end + create_table "csv_processing_messages", force: :cascade do |t| + t.string "body" + t.jsonb "data" + t.integer "message_type", default: 0 + t.bigint "csv_upload_id", null: false + t.datetime "created_at" + t.datetime "updated_at" + t.index ["created_at"], name: "index_csv_processing_messages_on_created_at" + t.index ["csv_upload_id"], name: "index_csv_processing_messages_on_csv_upload_id" + t.index ["updated_at"], name: "index_csv_processing_messages_on_updated_at" + end + + create_table "csv_uploads", force: :cascade do |t| + t.string "title" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "local_authorities", force: :cascade do |t| t.string "name", null: false t.datetime "created_at", null: false @@ -96,6 +142,8 @@ t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true end + add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" + add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" add_foreign_key "addresses", "properties" add_foreign_key "planning_applications_properties", "planning_applications" add_foreign_key "planning_applications_properties", "properties" diff --git a/package.json b/package.json index d587fb1..be0e621 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "dependencies": { "@hotwired/stimulus": "^3.2.1", "@hotwired/turbo-rails": "^7.2.4", + "@rails/activestorage": "^7.0.4-2", "esbuild": "^0.16.6", "govuk-frontend": "^4.4.1", "sass": "^1.56.2" diff --git a/spec/factories/csv_processing_messages.rb b/spec/factories/csv_processing_messages.rb new file mode 100644 index 0000000..5e63137 --- /dev/null +++ b/spec/factories/csv_processing_messages.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :csv_processing_message do + csv_upload_id { nil } + end +end diff --git a/spec/factories/csv_uploads.rb b/spec/factories/csv_uploads.rb new file mode 100644 index 0000000..475cfc8 --- /dev/null +++ b/spec/factories/csv_uploads.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :csv_upload do + title { "MyString" } + end +end diff --git a/spec/requests/csv_uploads_spec.rb b/spec/requests/csv_uploads_spec.rb new file mode 100644 index 0000000..d40d554 --- /dev/null +++ b/spec/requests/csv_uploads_spec.rb @@ -0,0 +1,132 @@ +# frozen_string_literal: true + +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 "/csv_uploads" do + # This should return the minimal set of attributes required to create a valid + # CsvUpload. As you add validations to CsvUpload, be sure to + # adjust the attributes here as well. + let(:valid_attributes) do + skip("Add a hash of attributes valid for your model") + end + + let(:invalid_attributes) do + skip("Add a hash of attributes invalid for your model") + end + + describe "GET /index" do + it "renders a successful response" do + CsvUpload.create! valid_attributes + get csv_uploads_url + expect(response).to be_successful + end + end + + describe "GET /show" do + it "renders a successful response" do + csv_upload = CsvUpload.create! valid_attributes + get csv_upload_url(csv_upload) + expect(response).to be_successful + end + end + + describe "GET /new" do + it "renders a successful response" do + get new_csv_upload_url + expect(response).to be_successful + end + end + + describe "GET /edit" do + it "renders a successful response" do + csv_upload = CsvUpload.create! valid_attributes + get edit_csv_upload_url(csv_upload) + expect(response).to be_successful + end + end + + describe "POST /create" do + context "with valid parameters" do + it "creates a new CsvUpload" do + expect do + post csv_uploads_url, params: { csv_upload: valid_attributes } + end.to change(CsvUpload, :count).by(1) + end + + it "redirects to the created csv_upload" do + post csv_uploads_url, params: { csv_upload: valid_attributes } + expect(response).to redirect_to(csv_upload_url(CsvUpload.last)) + end + end + + context "with invalid parameters" do + it "does not create a new CsvUpload" do + expect do + post csv_uploads_url, params: { csv_upload: invalid_attributes } + end.not_to change(CsvUpload, :count) + end + + it "renders a response with 422 status (i.e. to display the 'new' template)" do + post csv_uploads_url, params: { csv_upload: invalid_attributes } + expect(response).to have_http_status(:unprocessable_entity) + end + end + end + + describe "PATCH /update" do + context "with valid parameters" do + let(:new_attributes) do + skip("Add a hash of attributes valid for your model") + end + + it "updates the requested csv_upload" do + csv_upload = CsvUpload.create! valid_attributes + patch csv_upload_url(csv_upload), params: { csv_upload: new_attributes } + csv_upload.reload + skip("Add assertions for updated state") + end + + it "redirects to the csv_upload" do + csv_upload = CsvUpload.create! valid_attributes + patch csv_upload_url(csv_upload), params: { csv_upload: new_attributes } + csv_upload.reload + expect(response).to redirect_to(csv_upload_url(csv_upload)) + end + end + + context "with invalid parameters" do + it "renders a response with 422 status (i.e. to display the 'edit' template)" do + csv_upload = CsvUpload.create! valid_attributes + patch csv_upload_url(csv_upload), params: { csv_upload: invalid_attributes } + expect(response).to have_http_status(:unprocessable_entity) + end + end + end + + describe "DELETE /destroy" do + it "destroys the requested csv_upload" do + csv_upload = CsvUpload.create! valid_attributes + expect do + delete csv_upload_url(csv_upload) + end.to change(CsvUpload, :count).by(-1) + end + + it "redirects to the csv_uploads list" do + csv_upload = CsvUpload.create! valid_attributes + delete csv_upload_url(csv_upload) + expect(response).to redirect_to(csv_uploads_url) + end + end +end diff --git a/spec/routing/csv_uploads_routing_spec.rb b/spec/routing/csv_uploads_routing_spec.rb new file mode 100644 index 0000000..ad9320e --- /dev/null +++ b/spec/routing/csv_uploads_routing_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe CsvUploadsController do + describe "routing" do + it "routes to #index" do + expect(get: "/csv_uploads").to route_to("csv_uploads#index") + end + + it "routes to #new" do + expect(get: "/csv_uploads/new").to route_to("csv_uploads#new") + end + + it "routes to #show" do + expect(get: "/csv_uploads/1").to route_to("csv_uploads#show", id: "1") + end + + it "routes to #edit" do + expect(get: "/csv_uploads/1/edit").to route_to("csv_uploads#edit", id: "1") + end + + it "routes to #create" do + expect(post: "/csv_uploads").to route_to("csv_uploads#create") + end + + it "routes to #update via PUT" do + expect(put: "/csv_uploads/1").to route_to("csv_uploads#update", id: "1") + end + + it "routes to #update via PATCH" do + expect(patch: "/csv_uploads/1").to route_to("csv_uploads#update", id: "1") + end + + it "routes to #destroy" do + expect(delete: "/csv_uploads/1").to route_to("csv_uploads#destroy", id: "1") + end + end +end diff --git a/spec/system/log_in_spec.rb b/spec/system/log_in_spec.rb index 6ce5b1e..0edfdc2 100644 --- a/spec/system/log_in_spec.rb +++ b/spec/system/log_in_spec.rb @@ -32,7 +32,7 @@ end it "will redirect to homepage" do - expect(page).to have_text("This service allows ...") + expect(page).to have_text("Welcome #{user.email}") end end end diff --git a/spec/views/csv_uploads/edit.html.erb_spec.rb b/spec/views/csv_uploads/edit.html.erb_spec.rb new file mode 100644 index 0000000..a85423e --- /dev/null +++ b/spec/views/csv_uploads/edit.html.erb_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "csv_uploads/edit" do + let(:csv_upload) do + CsvUpload.create!( + title: "MyString" + ) + end + + before do + assign(:csv_upload, csv_upload) + end + + it "renders the edit csv_upload form" do + render + + assert_select "form[action=?][method=?]", csv_upload_path(csv_upload), "post" do + assert_select "input[name=?]", "csv_upload[title]" + end + end +end diff --git a/spec/views/csv_uploads/index.html.erb_spec.rb b/spec/views/csv_uploads/index.html.erb_spec.rb new file mode 100644 index 0000000..272c632 --- /dev/null +++ b/spec/views/csv_uploads/index.html.erb_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "csv_uploads/index" do + before do + assign(:csv_uploads, [ + CsvUpload.create!( + title: "Title" + ), + CsvUpload.create!( + title: "Title" + ) + ]) + end + + it "renders a list of csv_uploads" do + render + div_selector = Rails::VERSION::STRING >= "7" ? "div>h2" : "tr>td" + assert_select div_selector, text: Regexp.new("Title".to_s), count: 2 + end +end diff --git a/spec/views/csv_uploads/new.html.erb_spec.rb b/spec/views/csv_uploads/new.html.erb_spec.rb new file mode 100644 index 0000000..8e547cd --- /dev/null +++ b/spec/views/csv_uploads/new.html.erb_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "csv_uploads/new" do + before do + assign(:csv_upload, CsvUpload.new( + title: "MyString" + )) + end + + it "renders new csv_upload form" do + render + + assert_select "form[action=?][method=?]", csv_uploads_path, "post" do + assert_select "input[name=?]", "csv_upload[title]" + end + end +end diff --git a/spec/views/csv_uploads/show.html.erb_spec.rb b/spec/views/csv_uploads/show.html.erb_spec.rb new file mode 100644 index 0000000..0252632 --- /dev/null +++ b/spec/views/csv_uploads/show.html.erb_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "csv_uploads/show" do + before do + assign(:csv_upload, CsvUpload.create!( + title: "Title" + )) + end + + it "renders attributes in

" do + render + expect(rendered).to match(/Title/) + end +end diff --git a/yarn.lock b/yarn.lock index 310c0df..35434a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -135,6 +135,13 @@ resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-7.0.4.tgz#70a3ca56809f7aaabb80af2f9c01ae51e1a8ed41" integrity sha512-tz4oM+Zn9CYsvtyicsa/AwzKZKL+ITHWkhiu7x+xF77clh2b4Rm+s6xnOgY/sGDWoFWZmtKsE95hxBPkgQQNnQ== +"@rails/activestorage@^7.0.4-2": + version "7.0.4-2" + resolved "https://registry.yarnpkg.com/@rails/activestorage/-/activestorage-7.0.4-2.tgz#b535b1ff3087a929de4cdf7f686500d16e1cfc06" + integrity sha512-TVNuL13CmXlUb41+fgRzTmOp9Xf3LQtw1A3ntrOtQvDPipy4NOkZ9Emyr2m9Gq/5gPfWon6x8GWXBsMQw8a5mg== + dependencies: + spark-md5 "^3.0.1" + anymatch@~3.1.2: version "3.1.3" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" @@ -282,6 +289,11 @@ sass@^1.56.2: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== +spark-md5@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/spark-md5/-/spark-md5-3.0.2.tgz#7952c4a30784347abcee73268e473b9c0167e3fc" + integrity sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw== + to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" From ece6dfbd510fcf8f611c5cadb21fb6e5507660ac Mon Sep 17 00:00:00 2001 From: Kevin Sedgley Date: Thu, 23 Feb 2023 18:34:08 +0000 Subject: [PATCH 2/2] Process jobs --- app/controllers/csv_uploads_controller.rb | 10 +++- app/jobs/application_job.rb | 14 +++-- app/jobs/check_format_job.rb | 52 +++++++++++++++++++ app/jobs/delete_local_copy.rb | 9 ++++ app/jobs/download_local_copy_start_job.rb | 18 +++++++ app/jobs/insert_rows_job.rb | 12 +++++ app/models/csv_processing_message.rb | 2 +- app/models/csv_upload.rb | 5 +- app/views/csv_uploads/_csv_upload.html.erb | 3 +- app/views/csv_uploads/_form.html.erb | 5 ++ .../20230222165041_add_la_to_csv_upload.rb | 5 ++ db/schema.rb | 4 +- spec/jobs/check_format_job_spec.rb | 7 +++ spec/jobs/delete_local_copy_job_spec.rb | 7 +++ .../download_local_copy_start_job_spec.rb | 7 +++ spec/jobs/insert_rows_job_spec.rb | 7 +++ 16 files changed, 157 insertions(+), 10 deletions(-) create mode 100644 app/jobs/check_format_job.rb create mode 100644 app/jobs/delete_local_copy.rb create mode 100644 app/jobs/download_local_copy_start_job.rb create mode 100644 app/jobs/insert_rows_job.rb create mode 100644 db/migrate/20230222165041_add_la_to_csv_upload.rb create mode 100644 spec/jobs/check_format_job_spec.rb create mode 100644 spec/jobs/delete_local_copy_job_spec.rb create mode 100644 spec/jobs/download_local_copy_start_job_spec.rb create mode 100644 spec/jobs/insert_rows_job_spec.rb diff --git a/app/controllers/csv_uploads_controller.rb b/app/controllers/csv_uploads_controller.rb index 127207a..ba2ab4d 100644 --- a/app/controllers/csv_uploads_controller.rb +++ b/app/controllers/csv_uploads_controller.rb @@ -29,6 +29,8 @@ def create redirect_to csv_upload_url(@csv_upload), notice: I18n.t("upload_successfully_created_notice") end format.json { render :show, status: :created, location: @csv_upload } + + start_job else format.html { render :new, status: :unprocessable_entity } format.json { render json: @csv_upload.errors, status: :unprocessable_entity } @@ -61,6 +63,12 @@ def destroy private + # start first csv upload job + def start_job + # set a wait so front end can load + DownloadLocalCopyStartJob.set(wait: 2.seconds).perform_later @csv_upload + end + # Use callbacks to share common setup or constraints between actions. def set_csv_upload @csv_upload = CsvUpload.find(params[:id]) @@ -68,6 +76,6 @@ def set_csv_upload # Only allow a list of trusted parameters through. def csv_upload_params - params.require(:csv_upload).permit(:title, csv_files: []) + params.require(:csv_upload).permit(:title, :local_authority_id, csv_files: []) end end diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb index bef3959..2c21120 100644 --- a/app/jobs/application_job.rb +++ b/app/jobs/application_job.rb @@ -1,9 +1,15 @@ # frozen_string_literal: true class ApplicationJob < ActiveJob::Base - # Automatically retry jobs that encountered a deadlock - # retry_on ActiveRecord::Deadlocked + queue_as :low_priority - # Most jobs are safe to ignore if the underlying records are no longer available - # discard_on ActiveJob::DeserializationError + def add_message(message_text, type = :success) + new_message = CsvProcessingMessage.new( + message_type: type, + body: message_text, + data: [], + csv_upload: @csv_upload + ) + new_message.save! + end end diff --git a/app/jobs/check_format_job.rb b/app/jobs/check_format_job.rb new file mode 100644 index 0000000..c864db1 --- /dev/null +++ b/app/jobs/check_format_job.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require "csv" + +class CheckFormatJob < ApplicationJob + def perform(csv_upload) + @csv_upload = csv_upload + add_message "Starting CSV Upload" + csv_upload.csv_files.each do |csv_file| + csv_file.open do |csv_file_info| + add_message "Checking rows for #{csv_file.filename}" + check_rows(csv_file_info) + end + end + + InsertRowsJob.perform_later @csv_upload + add_message "Inserting rows" + end + + def check_rows(file_info) + results = [] + CSV.foreach(file_info.path, headers: true, header_converters: :symbol) do |row| + errors = check_row_for_errors(row) + + if errors + results.push errors + add_message "Error in row #{results.count}", :error + else + add_message "No errors in row #{results.count}" + end + end + + file_info.unlink + results + end + + def check_row_for_errors(row) + planning_application = PlanningApplicationCreation.new( + **row.to_h.merge(local_authority:) + ) + begin + planning_application.validate_property + false + rescue StandardError + true + end + end + + def local_authority + @csv_upload.local_authority + end +end diff --git a/app/jobs/delete_local_copy.rb b/app/jobs/delete_local_copy.rb new file mode 100644 index 0000000..81cc3d1 --- /dev/null +++ b/app/jobs/delete_local_copy.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class DeleteLocalCopyJob < ApplicationJob + queue_as :low_priority + + def perform(*args) + # Do something later + end +end diff --git a/app/jobs/download_local_copy_start_job.rb b/app/jobs/download_local_copy_start_job.rb new file mode 100644 index 0000000..6c42e8c --- /dev/null +++ b/app/jobs/download_local_copy_start_job.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class DownloadLocalCopyStartJob < ApplicationJob + def perform(csv_upload) + @csv_upload = csv_upload + csv_file_count = csv_upload.csv_files.count + csv_files_downloaded = 0 + + csv_upload.csv_files.each do |csv_file| + csv_file.download + csv_files_downloaded += 1 + add_message "Downloaded #{csv_files_downloaded} / #{csv_file_count} files" + end + + CheckFormatJob.perform_later @csv_upload + add_message "Checking format" + end +end diff --git a/app/jobs/insert_rows_job.rb b/app/jobs/insert_rows_job.rb new file mode 100644 index 0000000..7d076d6 --- /dev/null +++ b/app/jobs/insert_rows_job.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class InsertRowsJob < ApplicationJob + queue_as :default + + def perform(*_args) + # Do something later + + DeleteLocalCopyJob.perform_later @csv_upload + add_message message_text: "Deleting CSV uploads" + end +end diff --git a/app/models/csv_processing_message.rb b/app/models/csv_processing_message.rb index c8e0fa3..a453e41 100644 --- a/app/models/csv_processing_message.rb +++ b/app/models/csv_processing_message.rb @@ -2,5 +2,5 @@ class CsvProcessingMessage < ApplicationRecord belongs_to :csv_upload - enum message_type: { success: 0, error: 1 } + enum message_type: { info: 0, success: 1, error: 2 } end diff --git a/app/models/csv_upload.rb b/app/models/csv_upload.rb index 82f5cb4..684ea79 100644 --- a/app/models/csv_upload.rb +++ b/app/models/csv_upload.rb @@ -3,6 +3,7 @@ class CsvUpload < ApplicationRecord has_many_attached :csv_files has_many :csv_processing_messages, dependent: :destroy + belongs_to :local_authority, optional: true def filtered_csv_processing_messages success_message = csv_processing_messages @@ -11,8 +12,8 @@ def filtered_csv_processing_messages .first error_messages = csv_processing_messages - .where(message_type: 1) - .order(created_at: :desc) + .where(message_type: [1, 2]) + .order(created_at: :asc) if success_message.present? error_messages.or( diff --git a/app/views/csv_uploads/_csv_upload.html.erb b/app/views/csv_uploads/_csv_upload.html.erb index 3f4dfb5..3bc1372 100644 --- a/app/views/csv_uploads/_csv_upload.html.erb +++ b/app/views/csv_uploads/_csv_upload.html.erb @@ -1,8 +1,8 @@

- <%= csv_upload.title %>

+

Uploaded <%= csv_upload.created_at.to_fs(:short) %>

Progress

+ Refresh progress

Attached files

+
+ <%= form.label :local_authority_id, class: 'govuk-label' %> + <%= form.select :local_authority_id, LocalAuthority.all.collect {|la| [la.name.capitalize, la.id] }, {}, class: 'govuk-select' %> +
+
<%= form.label :csv_files, 'Upload CSV files', class: 'govuk-label' %> <%= form.file_field :csv_files, multiple: true, accept: ".csv", direct_upload: true, class: 'govuk-file-upload' %> diff --git a/db/migrate/20230222165041_add_la_to_csv_upload.rb b/db/migrate/20230222165041_add_la_to_csv_upload.rb new file mode 100644 index 0000000..33c085a --- /dev/null +++ b/db/migrate/20230222165041_add_la_to_csv_upload.rb @@ -0,0 +1,5 @@ +class AddLaToCsvUpload < ActiveRecord::Migration[7.0] + def change + add_reference :csv_uploads, :local_authority + end +end diff --git a/db/schema.rb b/db/schema.rb index 9bb5a34..ebed671 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[7.0].define(version: 2023_02_15_181318) do +ActiveRecord::Schema[7.0].define(version: 2023_02_22_165041) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -83,6 +83,8 @@ t.string "title" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.bigint "local_authority_id" + t.index ["local_authority_id"], name: "index_csv_uploads_on_local_authority_id" end create_table "local_authorities", force: :cascade do |t| diff --git a/spec/jobs/check_format_job_spec.rb b/spec/jobs/check_format_job_spec.rb new file mode 100644 index 0000000..4dda5fd --- /dev/null +++ b/spec/jobs/check_format_job_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe CheckFormatJob do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/jobs/delete_local_copy_job_spec.rb b/spec/jobs/delete_local_copy_job_spec.rb new file mode 100644 index 0000000..277d17c --- /dev/null +++ b/spec/jobs/delete_local_copy_job_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe DeleteLocalCopyJob do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/jobs/download_local_copy_start_job_spec.rb b/spec/jobs/download_local_copy_start_job_spec.rb new file mode 100644 index 0000000..e9bbb7d --- /dev/null +++ b/spec/jobs/download_local_copy_start_job_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe DownloadLocalCopyStartJob do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/jobs/insert_rows_job_spec.rb b/spec/jobs/insert_rows_job_spec.rb new file mode 100644 index 0000000..244ae3a --- /dev/null +++ b/spec/jobs/insert_rows_job_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe InsertRowsJob do + pending "add some examples to (or delete) #{__FILE__}" +end