diff --git a/app/assets/stylesheets/contract_pdf.scss b/app/assets/stylesheets/contract_pdf.scss
index 59c6abc..8c2571f 100644
--- a/app/assets/stylesheets/contract_pdf.scss
+++ b/app/assets/stylesheets/contract_pdf.scss
@@ -31,7 +31,7 @@ u {
}
.page {
- page-break-before: always;
+ page-break-before: always;
}
.logo {
@@ -44,6 +44,14 @@ u {
text-align: right;
}
+.qr-code {
+ margin-right: -30px;
+}
+
+.do-not-copy-warning {
+ padding-right: 15px;
+}
+
.heading-table td {
width: 50%;
}
diff --git a/app/controllers/appearance_release_imports_controller.rb b/app/controllers/appearance_release_imports_controller.rb
index 40a0857..b80a7d2 100644
--- a/app/controllers/appearance_release_imports_controller.rb
+++ b/app/controllers/appearance_release_imports_controller.rb
@@ -3,7 +3,6 @@
class AppearanceReleaseImportsController < ApplicationController
include AppearanceReleaseContext
include ProjectContext
- include CreateReleasableJobs
before_action :set_project, only: [:create]
@@ -11,24 +10,16 @@ class AppearanceReleaseImportsController < ApplicationController
def create
authorize AppearanceRelease
- @failed_files = []
attachments = appearance_release_params
+
if attachments.nil?
alert_message = t 'appearance_releases.create.no_attachments'
+ redirect_to [@project, :appearance_releases], alert: alert_message
else
- attachments.each do |attachment|
- create_imported_appearance_release attachment
- end
+ MatchAppearanceReleasesJob.perform_later(@project, attachments)
+ notice_message = t 'appearance_releases.create.matching_started'
+ redirect_to [@project, :appearance_releases], notice: notice_message
end
-
- unless @failed_files.empty?
- alert_message = t 'appearance_releases.create.failed_import'
- alert_message += '
| + | <%= t '.do_not_copy_warning' %> | +
| <% if has_logo %> @@ -11,17 +15,9 @@ <% end %> |
- |
| - | <%= serial_numbers[copy_index] %> | -
| - | <%= t '.do_not_copy_warning' %> | -
<%= fa_icon("info-circle", text: t(".custom_fields.instructions")) %>
<%= render "shared/custom_fields", form: form %> diff --git a/config/locales/en.yml b/config/locales/en.yml index a45e867..7073de2 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -112,6 +112,7 @@ en: no_photos: Needs Photo create: failed_import: Failed to create appearance release for files listed below + matching_started: Matching started no_attachments: Failed to import - no attachments edit: heading: Edit Appearance Release @@ -141,6 +142,8 @@ en: shared: imported_appearance_release_contract_name: Imported Contract imported_appearance_release_headshot_name: Imported Headshot + incomplete_match: Incomplete Match + matched_import: Complete Match type_filter_actions: all_releases: All Releases complete_releases: Complete Releases diff --git a/config/locales/es.yml b/config/locales/es.yml index 674a7f0..be353f4 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -25,6 +25,7 @@ es: appearance_releases: create: failed_import: Failed to create appearance release for files listed below (ES) + matching_started: Matching started (ES) no_attachments: Failed to import - no attachments (ES) form: photos: @@ -37,6 +38,8 @@ es: shared: imported_appearance_release_contract_name: Contrato Importado imported_appearance_release_headshot_name: Retrato Importado + incomplete_match: Incomplete Match (ES) + matched_import: Complete Match (ES) type_filter_actions: all_releases: All Releases (ES) complete_releases: Complete Releases (ES) diff --git a/db/migrate/20200430151828_create_matching_requests.rb b/db/migrate/20200430151828_create_matching_requests.rb new file mode 100644 index 0000000..cc4e4cb --- /dev/null +++ b/db/migrate/20200430151828_create_matching_requests.rb @@ -0,0 +1,9 @@ +class CreateMatchingRequests < ActiveRecord::Migration[6.0] + def change + create_table :matching_requests do |t| + t.belongs_to :project, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/migrate/20200430190412_add_identifier_to_appearance_releases.rb b/db/migrate/20200430190412_add_identifier_to_appearance_releases.rb new file mode 100644 index 0000000..a1ff2fa --- /dev/null +++ b/db/migrate/20200430190412_add_identifier_to_appearance_releases.rb @@ -0,0 +1,5 @@ +class AddIdentifierToAppearanceReleases < ActiveRecord::Migration[6.0] + def change + add_column :appearance_releases, :identifier, :string, default: nil + end +end diff --git a/db/structure.sql b/db/structure.sql index 204f848..f6a1918 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -9,20 +9,6 @@ SET xmloption = content; SET client_min_messages = warning; SET row_security = off; --- --- Name: plpgsql; Type: EXTENSION; Schema: -; Owner: - --- - -CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog; - - --- --- Name: EXTENSION plpgsql; Type: COMMENT; Schema: -; Owner: - --- - -COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language'; - - -- -- Name: fuzzystrmatch; Type: EXTENSION; Schema: -; Owner: - -- @@ -331,6 +317,7 @@ CREATE TABLE public.appearance_releases ( person_last_name character varying, guardian_first_name character varying, guardian_last_name character varying, + identifier character varying, guardian_email character varying ); @@ -886,6 +873,37 @@ CREATE SEQUENCE public.location_releases_id_seq ALTER SEQUENCE public.location_releases_id_seq OWNED BY public.location_releases.id; +-- +-- Name: matching_requests; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.matching_requests ( + id bigint NOT NULL, + project_id bigint, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: matching_requests_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.matching_requests_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: matching_requests_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.matching_requests_id_seq OWNED BY public.matching_requests.id; + + -- -- Name: material_releases; Type: TABLE; Schema: public; Owner: - -- @@ -1253,6 +1271,7 @@ CREATE TABLE public.settings ( -- CREATE SEQUENCE public.settings_id_seq + AS integer START WITH 1 INCREMENT BY 1 NO MINVALUE @@ -1288,6 +1307,7 @@ CREATE TABLE public.taggings ( -- CREATE SEQUENCE public.taggings_id_seq + AS integer START WITH 1 INCREMENT BY 1 NO MINVALUE @@ -1318,6 +1338,7 @@ CREATE TABLE public.tags ( -- CREATE SEQUENCE public.tags_id_seq + AS integer START WITH 1 INCREMENT BY 1 NO MINVALUE @@ -1630,7 +1651,6 @@ CREATE TABLE public.zoom_meetings ( api_meeting_id character varying, created_at timestamp(6) without time zone NOT NULL, updated_at timestamp(6) without time zone NOT NULL, - broadcast_id bigint, zoom_user_id bigint, project_id bigint, status integer DEFAULT 0 @@ -1829,6 +1849,13 @@ ALTER TABLE ONLY public.imports ALTER COLUMN id SET DEFAULT nextval('public.impo ALTER TABLE ONLY public.location_releases ALTER COLUMN id SET DEFAULT nextval('public.location_releases_id_seq'::regclass); +-- +-- Name: matching_requests id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.matching_requests ALTER COLUMN id SET DEFAULT nextval('public.matching_requests_id_seq'::regclass); + + -- -- Name: material_releases id; Type: DEFAULT; Schema: public; Owner: - -- @@ -2137,6 +2164,14 @@ ALTER TABLE ONLY public.location_releases ADD CONSTRAINT location_releases_pkey PRIMARY KEY (id); +-- +-- Name: matching_requests matching_requests_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.matching_requests + ADD CONSTRAINT matching_requests_pkey PRIMARY KEY (id); + + -- -- Name: material_releases material_releases_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -2606,6 +2641,13 @@ CREATE INDEX index_location_releases_on_term_id ON public.location_releases USIN CREATE INDEX index_location_releases_on_territory_id ON public.location_releases USING btree (territory_id); +-- +-- Name: index_matching_requests_on_project_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_matching_requests_on_project_id ON public.matching_requests USING btree (project_id); + + -- -- Name: index_material_releases_on_applicable_medium_id; Type: INDEX; Schema: public; Owner: - -- @@ -2935,13 +2977,6 @@ CREATE INDEX index_videos_on_audio_analysis_uid ON public.videos USING btree (au CREATE INDEX index_videos_on_project_id ON public.videos USING btree (project_id); --- --- Name: index_zoom_meetings_on_broadcast_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX index_zoom_meetings_on_broadcast_id ON public.zoom_meetings USING btree (broadcast_id); - - -- -- Name: index_zoom_meetings_on_project_id; Type: INDEX; Schema: public; Owner: - -- @@ -3250,6 +3285,14 @@ ALTER TABLE ONLY public.material_releases ADD CONSTRAINT fk_rails_6b945b36b9 FOREIGN KEY (contract_template_id) REFERENCES public.contract_templates(id); +-- +-- Name: matching_requests fk_rails_71d16e64c8; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.matching_requests + ADD CONSTRAINT fk_rails_71d16e64c8 FOREIGN KEY (project_id) REFERENCES public.projects(id); + + -- -- Name: appearance_releases fk_rails_7a58302526; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -3282,14 +3325,6 @@ ALTER TABLE ONLY public.music_releases ADD CONSTRAINT fk_rails_890d967673 FOREIGN KEY (territory_id) REFERENCES public.territories(id); --- --- Name: zoom_meetings fk_rails_8d814ea729; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.zoom_meetings - ADD CONSTRAINT fk_rails_8d814ea729 FOREIGN KEY (broadcast_id) REFERENCES public.broadcasts(id); - - -- -- Name: medical_releases fk_rails_98aa92daa9; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -3609,6 +3644,8 @@ INSERT INTO "schema_migrations" (version) VALUES ('20200424161117'), ('20200427073429'), ('20200428091105'), +('20200430151828'), +('20200430190412'), ('20200507110804'), ('20200512161738'), ('20200526113516'), @@ -3616,6 +3653,7 @@ INSERT INTO "schema_migrations" (version) VALUES ('20200606044747'), ('20200610085411'), ('20200610140459'), -('20200612121539'); +('20200612121539'), +('20200615131722'); diff --git a/lib/brayniac_ai.rb b/lib/brayniac_ai.rb index ce9bc72..a6a7143 100644 --- a/lib/brayniac_ai.rb +++ b/lib/brayniac_ai.rb @@ -10,5 +10,6 @@ require_relative "./brayniac_ai/edl_parse" require_relative "./brayniac_ai/edl_parse_result" require_relative "./brayniac_ai/facial_recognition" require_relative "./brayniac_ai/facial_recognition_result" +require_relative "./brayniac_ai/qr_matching" require_relative "./brayniac_ai/tag" require_relative "./brayniac_ai/validation" diff --git a/lib/brayniac_ai/qr_matching.rb b/lib/brayniac_ai/qr_matching.rb new file mode 100644 index 0000000..4328870 --- /dev/null +++ b/lib/brayniac_ai/qr_matching.rb @@ -0,0 +1,4 @@ +module BrayniacAI + class QrMatching < Base + end +end diff --git a/spec/factories/appearance_releases.rb b/spec/factories/appearance_releases.rb index a64c4a9..afcec4e 100644 --- a/spec/factories/appearance_releases.rb +++ b/spec/factories/appearance_releases.rb @@ -67,5 +67,24 @@ FactoryBot.define do appearance_release.contract_template = build(:appearance_release_contract_template) end end + + factory :appearance_release_import do + person_photo nil + + trait :with_headshot do + person_photo do + path = Rails.root.join("spec", "fixtures", "files", "person_photo.png") + Rack::Test::UploadedFile.new(path, "image/png") + end + end + + trait :with_contract do + contract do + path = Rails.root.join("spec", "fixtures", "files", "AppearanceRelease.pdf") + Rack::Test::UploadedFile.new(path, "application/pdf") + end + end + end + end end diff --git a/spec/features/user_managing_appearance_releases_spec.rb b/spec/features/user_managing_appearance_releases_spec.rb index 17e2ae1..d4f8c70 100644 --- a/spec/features/user_managing_appearance_releases_spec.rb +++ b/spec/features/user_managing_appearance_releases_spec.rb @@ -138,48 +138,25 @@ feature 'User managing appearance releases' do end scenario 'progress bar shows when user imports a release', js: true do - skip "TODO" + visit project_appearance_releases_path(project) + + expect(page).not_to have_css('#upload-progress-container') + large_pdf_file = build_large_pdf_file + attach_file import_appearance_release_field, Rails.root.join(large_pdf_file.path), visible: false + expect(page).to have_css('#upload-progress-container') + expect(page).to have_content submit_create_button, wait: 30 + click_button submit_create_button + expect(page).to have_content matching_started + end + + scenario 'MatchAppearanceReleasesJob is started when import is finished', js: true do visit project_appearance_releases_path(project) attach_file import_appearance_release_field, Rails.root.join(file_fixture('person_photo.png')), visible: false - expect(page).to have_content importing_label + allow(MatchAppearanceReleasesJob).to receive(:perform_later).with(project, anything) click_button submit_create_button - expect(page).to have_css('.progress') - end - - scenario 'importing a releases works when image is selected', js: true do + expect(page).to have_content matching_started visit project_appearance_releases_path(project) - - expect(page).to have_content submit_create_button - expect(page).to have_content no_appearance_releases - attach_file import_appearance_release_field, Rails.root.join(file_fixture('person_photo.png')), visible: false - expect(page).to have_content importing_label - click_button submit_create_button - expect(page).not_to have_content no_appearance_releases - expect(page).to have_content /Imported Headshot\s+\d{7}/ - end - - scenario 'importing a releases works when pdf is selected', js: true do - visit project_appearance_releases_path(project) - - expect(page).to have_content submit_create_button - expect(page).to have_content no_appearance_releases - attach_file import_appearance_release_field, Rails.root.join(file_fixture('contract.pdf')), visible: false - expect(page).to have_content importing_label - click_button submit_create_button - expect(page).not_to have_content no_appearance_releases - expect(page).to have_content /Imported Contract\s+\d{7}/ - end - - scenario 'importing a releases fails when file other than image or pdf is selected', js: true do - visit project_appearance_releases_path(project) - - expect(page).to have_content submit_create_button - expect(page).to have_content no_appearance_releases - attach_file import_appearance_release_field, Rails.root.join(file_fixture('audio.mp3')), visible: false - expect(page).to have_content importing_label - click_button submit_create_button - expect(page).to have_content failed_to_import_notice expect(page).to have_content no_appearance_releases end @@ -435,6 +412,10 @@ feature 'User managing appearance releases' do tempfile end + def matching_started + t 'appearance_releases.create.matching_started' + end + def filter_type_all t 'appearance_releases.type_filter_actions.all_releases' end diff --git a/spec/jobs/match_appearance_releases_job_spec.rb b/spec/jobs/match_appearance_releases_job_spec.rb new file mode 100644 index 0000000..bd35985 --- /dev/null +++ b/spec/jobs/match_appearance_releases_job_spec.rb @@ -0,0 +1,216 @@ +require "rails_helper" + +describe MatchAppearanceReleasesJob do + let(:project) { create(:project) } + let(:dummy_appearance_release) { create(:appearance_release_import, :with_headshot, :with_contract) } + let(:dummy_matching_request) { instance_double(MatchingRequest, id: 999) } + + before :all do + ENV["AWS_BUCKET"] = "" + end + + describe ".perform_now" do + it "returns if no attachment is sent" do + expect(MatchingRequest).not_to receive(:create) + attachments = [] + MatchAppearanceReleasesJob.perform_now project, attachments + end + + it "returns if no valid attachment is sent" do + expect(MatchingRequest).not_to receive(:create) + dummy_video = create(:video) + attachments = [dummy_video.file.blob.signed_id] + MatchAppearanceReleasesJob.perform_now project, attachments + end + + it "does not create new appearance release if BrayniacAI returns empty matches array" do + signed_ids = [dummy_appearance_release.person_photo.blob.signed_id] + keys = [dummy_appearance_release.person_photo.key] + payload = { + project: project, + attachments: signed_ids + } + qr_matching_payload = { + bucket: '', + files: keys, + request_id: dummy_matching_request.id + } + qr_matching_mock_response = double( + request_id: dummy_matching_request.id, + matches: [] + ) + + expect(MatchingRequest).to receive(:create).with(payload).and_return(dummy_matching_request) + expect(BrayniacAI::QrMatching).to receive(:create!).with(qr_matching_payload).and_return(qr_matching_mock_response) + expect(dummy_matching_request).to receive(:destroy) + + MatchAppearanceReleasesJob.perform_now project, signed_ids + + expect(AppearanceRelease.last).to eq dummy_appearance_release + end + + it "creates new incomplete appearance release if BrayniacAI returns single headshot match" do + signed_ids = [dummy_appearance_release.person_photo.blob.signed_id] + keys = [dummy_appearance_release.person_photo.key] + payload = { + project: project, + attachments: signed_ids + } + qr_matching_payload = { + bucket: '', + files: keys, + request_id: dummy_matching_request.id + } + mock_match = double( + headshots: keys, + contracts: [], + unknowns: [], + identifier: 'some/identifier/123' + ) + matches = [mock_match] + qr_matching_mock_response = double( + request_id: dummy_matching_request.id, + matches: matches + ) + + expect(MatchingRequest).to receive(:create).with(payload).and_return(dummy_matching_request) + expect(BrayniacAI::QrMatching).to receive(:create!).with(qr_matching_payload).and_return(qr_matching_mock_response) + expect(dummy_matching_request).to receive(:destroy) + + MatchAppearanceReleasesJob.perform_now project, signed_ids + + expect(AppearanceRelease.last.identifier).to eq mock_match.identifier + expect(AppearanceRelease.last.person_photo).to be_attached + expect(AppearanceRelease.last.contract).not_to be_attached + end + + it "creates new incomplete appearance release if BrayniacAI returns single contract match" do + signed_ids = [dummy_appearance_release.contract.blob.signed_id] + keys = [dummy_appearance_release.contract.key] + payload = { + project: project, + attachments: signed_ids + } + qr_matching_payload = { + bucket: '', + files: keys, + request_id: dummy_matching_request.id + } + mock_match = double( + headshots: [], + contracts: keys, + unknowns: [], + identifier: 'some/identifier/123' + ) + matches = [mock_match] + qr_matching_mock_response = double( + request_id: dummy_matching_request.id, + matches: matches + ) + + expect(MatchingRequest).to receive(:create).with(payload).and_return(dummy_matching_request) + expect(BrayniacAI::QrMatching).to receive(:create!).with(qr_matching_payload).and_return(qr_matching_mock_response) + expect(dummy_matching_request).to receive(:destroy) + + MatchAppearanceReleasesJob.perform_now project, signed_ids + + expect(AppearanceRelease.last.identifier).to eq mock_match.identifier + expect(AppearanceRelease.last.person_photo.attached?).to eq false + expect(AppearanceRelease.last.contract.attached?).to eq true + end + + it "creates new complete appearance release if BrayniacAI returns match for headshot and contract" do + signed_ids = [ + dummy_appearance_release.person_photo.blob.signed_id, + dummy_appearance_release.contract.blob.signed_id + ] + keys = [ + dummy_appearance_release.person_photo.key, + dummy_appearance_release.contract.key + ] + payload = { + project: project, + attachments: signed_ids + } + qr_matching_payload = { + bucket: '', + files: keys, + request_id: dummy_matching_request.id + } + mock_match = double( + headshots: [keys[0]], + contracts: [keys[1]], + unknowns: [], + identifier: 'some/identifier/123' + ) + matches = [mock_match] + qr_matching_mock_response = double( + request_id: dummy_matching_request.id, + matches: matches + ) + + expect(MatchingRequest).to receive(:create).with(payload).and_return(dummy_matching_request) + expect(BrayniacAI::QrMatching).to receive(:create!).with(qr_matching_payload).and_return(qr_matching_mock_response) + expect(dummy_matching_request).to receive(:destroy) + + MatchAppearanceReleasesJob.perform_now project, signed_ids + + expect(AppearanceRelease.last.identifier).to eq mock_match.identifier + expect(AppearanceRelease.last.person_photo.attached?).to eq true + expect(AppearanceRelease.last.contract.attached?).to eq true + end + + it "creates two new incomplete appearance releases if BrayniacAI returns two matches for headshot and contract" do + signed_ids = [ + dummy_appearance_release.person_photo.blob.signed_id, + dummy_appearance_release.contract.blob.signed_id + ] + keys = [ + dummy_appearance_release.person_photo.key, + dummy_appearance_release.contract.key + ] + payload = { + project: project, + attachments: signed_ids + } + qr_matching_payload = { + bucket: '', + files: keys, + request_id: dummy_matching_request.id + } + mock_match1 = double( + headshots: [keys[0]], + contracts: [], + unknowns: [], + identifier: 'some/identifier/123' + ) + mock_match2 = double( + headshots: [], + contracts: [keys[1]], + unknowns: [], + identifier: 'some/identifier/789' + ) + matches = [mock_match1, mock_match2] + qr_matching_mock_response = double( + request_id: dummy_matching_request.id, + matches: matches + ) + + expect(MatchingRequest).to receive(:create).with(payload).and_return(dummy_matching_request) + expect(BrayniacAI::QrMatching).to receive(:create!).with(qr_matching_payload).and_return(qr_matching_mock_response) + expect(dummy_matching_request).to receive(:destroy) + + MatchAppearanceReleasesJob.perform_now project, signed_ids + + releases = AppearanceRelease.last(2) + + expect(releases[0].identifier).to eq mock_match1.identifier + expect(releases[0].person_photo.attached?).to eq true + expect(releases[0].contract.attached?).to eq false + + expect(releases[1].identifier).to eq mock_match2.identifier + expect(releases[1].person_photo.attached?).to eq false + expect(releases[1].contract.attached?).to eq true + end + end +end \ No newline at end of file diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index 3fbd316..8147964 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -131,7 +131,8 @@ RSpec.describe Account do Broadcast, Account, ZoomMeeting, - MedicalRelease + MedicalRelease, + MatchingRequest ] Rails.application.eager_load! ActiveRecord::Base.descendants.each do |model| diff --git a/spec/models/blank_contract_spec.rb b/spec/models/blank_contract_spec.rb index d3c2d20..9b7e421 100644 --- a/spec/models/blank_contract_spec.rb +++ b/spec/models/blank_contract_spec.rb @@ -42,7 +42,6 @@ describe BlankContract do material_release = create(:material_release_with_contract_template, project: project, person_name: 'Jane Doe') result = render_contract_html_for(material_release) - expect(result).to include 'serial-number' expect(result).to include 'DO NOT COPY' end end