diff --git a/app/jobs/generate_contracts_zip_job.rb b/app/jobs/generate_contracts_zip_job.rb index 865a5ea..65bc376 100644 --- a/app/jobs/generate_contracts_zip_job.rb +++ b/app/jobs/generate_contracts_zip_job.rb @@ -6,8 +6,8 @@ class GenerateContractsZipJob < ApplicationJob before_perform do |job| @project = job.arguments.first @download = job.arguments.second - release_type = job.arguments.third - @folder_name = "#{@project.name.parameterize}_#{get_release_name(release_type).gsub('_', '-')}" + @release_type = job.arguments.third + @folder_name = "#{@project.name.parameterize}_#{get_release_name(@release_type).gsub('_', '-')}" @download.update!(name: @folder_name, status: :pending) end @@ -20,6 +20,11 @@ class GenerateContractsZipJob < ApplicationJob files.each do |attachment| zipfile.add(attachment, File.join("#{dir}/", attachment)) end + + if @release_type.constantize.include?(CsvExportable) + csv_file = generate_csv releases + zipfile.get_output_stream("#{@folder_name}.csv") { |f| f.puts(csv_file) } + end end @download.file.attach(io: File.open(zipfile_name), filename: "#{@folder_name}.zip") @@ -43,6 +48,19 @@ class GenerateContractsZipJob < ApplicationJob private + def generate_csv(releases) + release_class = @release_type.constantize + headers = release_class.csv_headers + + CSV.generate(headers: true) do |csv| + csv << headers + releases.each do |release| + csv_row_data = release.to_csv_row + csv << csv_row_data + end + end + end + def get_release_name(release_type) release_type.constantize.model_name.plural end diff --git a/app/models/acquired_media_release.rb b/app/models/acquired_media_release.rb index 6a12d35..8b7a7b0 100644 --- a/app/models/acquired_media_release.rb +++ b/app/models/acquired_media_release.rb @@ -9,6 +9,13 @@ class AcquiredMediaRelease < ApplicationRecord include Signable include Syncable include PersonName + include CsvExportable + + class << self + def custom_csv_exportable_headers + %i[name file_infos_count] + end + end has_many :file_infos, as: :releasable, dependent: :destroy @@ -57,4 +64,8 @@ class AcquiredMediaRelease < ApplicationRecord def uses_edl? true end + + def file_infos_count + file_infos.any? ? file_infos.size : I18n.t('acquired_media_releases.acquired_media_release.no_media') + end end diff --git a/app/models/appearance_release.rb b/app/models/appearance_release.rb index 9a750a7..c9bb4c0 100644 --- a/app/models/appearance_release.rb +++ b/app/models/appearance_release.rb @@ -15,6 +15,13 @@ class AppearanceRelease < ApplicationRecord include SecondGuardianPhotoable include GuardianName include SecondGuardianName + include CsvExportable + + class << self + def custom_csv_exportable_headers + %i[name contact_info] + end + end has_one_attached :person_photo diff --git a/app/models/concerns/csv_exportable.rb b/app/models/concerns/csv_exportable.rb new file mode 100644 index 0000000..4619e86 --- /dev/null +++ b/app/models/concerns/csv_exportable.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module CsvExportable + extend ActiveSupport::Concern + + COMMON_HEADERS = %i[notes tags signed_at].freeze + COMMON_VALUES = %w[clean_notes clean_tags signed_on].freeze + + included do + class << self + def custom_csv_exportable_headers + [] + end + + def csv_headers + headers = custom_csv_exportable_headers + COMMON_HEADERS + + headers.map do |header| + I18n.t("#{model_name.plural}.index.table_headers.#{header}") + end + end + end + + def to_csv_row + (self.class.custom_csv_exportable_headers + COMMON_VALUES).map do |function| + send(function) + end + end + + private + + def contact_info + contact_info = '' + contact_info += "#{person_address}; " if person_address.present? + contact_info += "P: #{person_phone}; " if person_phone.present? + contact_info += "E: #{person_email}" if person_email.present? + contact_info.delete_suffix '; ' + end + + def clean_notes + notes = '' + self.notes.order_by_recent.each do |note| + notes += "#{note.content}(#{note.email}), " + end + notes.delete_suffix ', ' + end + + def clean_tags + tags = '' + self.tags.each do |tag| + tags += "#{tag.name}, " + end + tags.delete_suffix ', ' + end + end +end diff --git a/app/models/location_release.rb b/app/models/location_release.rb index a5b6a02..1e49d73 100644 --- a/app/models/location_release.rb +++ b/app/models/location_release.rb @@ -10,6 +10,13 @@ class LocationRelease < ApplicationRecord include Syncable include Taggable include PersonName + include CsvExportable + + class << self + def custom_csv_exportable_headers + %i[name address] + end + end composed_of :address, mapping: [ diff --git a/app/models/material_release.rb b/app/models/material_release.rb index 56d45ee..dc5380e 100644 --- a/app/models/material_release.rb +++ b/app/models/material_release.rb @@ -10,8 +10,15 @@ class MaterialRelease < ApplicationRecord include Syncable include Taggable include PersonName - - composed_of :person_address, + include CsvExportable + + class << self + def custom_csv_exportable_headers + %i[name] + end + end + + composed_of :person_address, class_name: "Address", mapping: [ %w(person_address_street1 street1), @@ -30,15 +37,15 @@ class MaterialRelease < ApplicationRecord validates :signature, attached: true end - searchable_on %i[ - name + searchable_on %i[ + name person_address_street1 person_address_street2 person_address_city person_address_state person_address_zip person_address_country ] def contact_person @contact_person ||= Contact.new(person_name, person_address, person_email, person_phone) end - + def minor? false end diff --git a/app/models/medical_release.rb b/app/models/medical_release.rb index 996bfc5..e037ef5 100644 --- a/app/models/medical_release.rb +++ b/app/models/medical_release.rb @@ -11,6 +11,13 @@ class MedicalRelease < ApplicationRecord include SecondGuardianPhotoable include GuardianName include SecondGuardianName + include CsvExportable + + class << self + def custom_csv_exportable_headers + %i[approved? name contact_info] + end + end NUMBER_OF_CUSTOM_FIELDS = 15 diff --git a/app/models/misc_release.rb b/app/models/misc_release.rb index f9437dd..2679542 100644 --- a/app/models/misc_release.rb +++ b/app/models/misc_release.rb @@ -9,6 +9,13 @@ class MiscRelease < ApplicationRecord include PersonName include GuardianName include GuardianPhotoable + include CsvExportable + + class << self + def custom_csv_exportable_headers + %i[name contact_info] + end + end NUMBER_OF_CUSTOM_FIELDS = 15 diff --git a/app/models/music_release.rb b/app/models/music_release.rb index 29fd426..b324b9f 100644 --- a/app/models/music_release.rb +++ b/app/models/music_release.rb @@ -7,7 +7,14 @@ class MusicRelease < ApplicationRecord include Searchable include Taggable include PersonName - + include CsvExportable + + class << self + def custom_csv_exportable_headers + %i[name file_infos_count composers_count publishers_count] + end + end + has_many :file_infos, as: :releasable, dependent: :destroy has_many :composers, dependent: :destroy has_many :publishers, dependent: :destroy @@ -72,6 +79,18 @@ class MusicRelease < ApplicationRecord false end + def file_infos_count + file_infos.size + end + + def composers_count + composers.size + end + + def publishers_count + publishers.size + end + private def publisher_percentages_add_up_to_100 diff --git a/app/models/talent_release.rb b/app/models/talent_release.rb index 8ca4dbe..f507ba7 100644 --- a/app/models/talent_release.rb +++ b/app/models/talent_release.rb @@ -14,6 +14,13 @@ class TalentRelease < ApplicationRecord include SecondGuardianPhotoable include GuardianName include SecondGuardianName + include CsvExportable + + class << self + def custom_csv_exportable_headers + %i[name phone email] + end + end composed_of :person_address, class_name: "Address", @@ -86,6 +93,14 @@ class TalentRelease < ApplicationRecord person_name end + def phone + person_phone + end + + def email + person_email + end + def filename_suffix "#{person_last_name} #{person_first_name}" end diff --git a/config/locales/en.yml b/config/locales/en.yml index 4fd633d..290ef7e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -50,6 +50,7 @@ en: empty: Acquired Media Releases will appear here table_headers: file_infos_count: No. Files + name: Name notes: Notes signed_at: Date Signed tags: Tags @@ -151,6 +152,8 @@ en: empty: Appearance Releases will appear here imported_appearance_release_missing_attachment: Person photo or contract missing for imported appearance release table_headers: + contact_info: Contact info + name: Name notes: Notes signed_at: Date Signed tags: Tags @@ -742,6 +745,7 @@ en: empty: Location Releases will appear here table_headers: address: Address + name: Name notes: Notes signed_at: Date Signed tags: Tags @@ -776,6 +780,7 @@ en: search: Search empty: Material Releases will appear here table_headers: + name: Name notes: Notes signed_at: Date Signed tags: Tags @@ -798,6 +803,9 @@ en: empty: Medical releases will appear here table_headers: approved: Approved + approved?: Approved + contact_info: Contact info + name: Person name notes: Notes signed_at: Date Signed tags: Tags @@ -815,6 +823,8 @@ en: search: Search empty: Misc Releases will appear here table_headers: + contact_info: Contact info + name: Person name notes: Notes signed_at: Date Signed tags: Tags @@ -849,6 +859,7 @@ en: table_headers: composers_count: No. Composers file_infos_count: No. Files + name: Name notes: Notes publishers_count: No. Publishers signed_at: Date Signed @@ -1245,7 +1256,10 @@ en: search: Search empty: Talent Releases will appear here table_headers: + email: Email + name: Name notes: Notes + phone: Phone signed_at: Date Signed tags: Tags new: diff --git a/config/locales/es.yml b/config/locales/es.yml index 10d3be8..8c6711c 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -1,4 +1,14 @@ es: + acquired_media_releases: + acquired_media_release: + no_media: No Media (ES) + index: + table_headers: + file_infos_count: No. Files (ES) + name: Name (ES) + notes: Notes (ES) + signed_at: Date Signed (ES) + tags: Tags (ES) activerecord: attributes: appearance_release: @@ -41,6 +51,12 @@ es: heading: Person Photo (ES) index: imported_appearance_release_missing_attachment: Person photo or contract missing for imported appearance release (ES) + table_headers: + contact_info: "" + name: "" + notes: "" + signed_at: "" + tags: "" shared: imported_appearance_release_contract_name: Contrato Importado imported_appearance_release_headshot_name: Retrato Importado @@ -296,21 +312,57 @@ es: form: photos: dropzone_label: Tap to take a photo of the Property (optional) (ES) + index: + table_headers: + address: Address (ES) + notes: Notes (ES) + signed_at: Date Signed (ES) + tags: Tags (ES) material_releases: form: photos: dropzone_label: Tap to take a photo of Licensed Material (optional) (ES) + index: + table_headers: + name: Name (ES) + notes: Notes (ES) + signed_at: Date Signed (ES) + tags: Tags (ES) medical_releases: custom_validation_errors: question_answer_is_required: answer is required (ES) index: table_headers: approved: Approved (ES) + approved?: Approved (ES) + contact_info: Contact info (ES) + name: Person name (ES) + notes: Notes (ES) + signed_at: Date Signed (ES) + tags: Tags (ES) medical_release: actions: review: Review (ES) messages: approved_tooltip: "" + misc_releases: + index: + table_headers: + contact_info: Contact info (ES) + name: Person name (ES) + notes: Notes (ES) + signed_at: Date Signed (ES) + tags: Tags (ES) + music_releases: + index: + table_headers: + composers_count: No. Composers (ES) + file_infos_count: No. Files (ES) + name: Name (ES) + notes: Notes (ES) + publishers_count: No. Publishers (ES) + signed_at: Date Signed (ES) + tags: Tags (ES) public: appearance_releases: create: @@ -415,6 +467,14 @@ es: heading: Second Guardian Photo (ES) guardian_photo: heading: Guardian Photo (ES) + index: + table_headers: + email: Email (ES) + name: Name (ES) + notes: Notes (ES) + phone: Phone (ES) + signed_at: Date Signed (ES) + tags: Tags (ES) task_requests: create: success_message: Your task request was successfully submitted. Thank you. A chat window will pop up on the lower right in a few seconds. (ES) diff --git a/spec/jobs/generate_contracts_zip_job_spec.rb b/spec/jobs/generate_contracts_zip_job_spec.rb index a438ae4..3d3ee5f 100644 --- a/spec/jobs/generate_contracts_zip_job_spec.rb +++ b/spec/jobs/generate_contracts_zip_job_spec.rb @@ -11,7 +11,7 @@ describe GenerateContractsZipJob do dir = Rails.root.join("spec", "fixtures", "files") files = ["contract.pdf", "AppearanceRelease.pdf"] # Attachments in the test environment do not persist to cloud storage - # Therefore we want to stub calls to `open` with a cloud storage URL + # Therefore we want to stub calls to `open` with a cloud storage URL allow_any_instance_of(ReleaseContractCollectionService).to receive(:open).and_return(StringIO.new("file data")) allow_any_instance_of(ReleaseContractCollectionService).to receive(:build).and_yield(dir, files) end @@ -35,6 +35,38 @@ describe GenerateContractsZipJob do expect(download.file).to be_attached end + it "generates ZIP containing CSV file with all releases data for all release types" do + release_types = %w[AcquiredMediaRelease AppearanceRelease LocationRelease MaterialRelease MedicalRelease MiscRelease MusicRelease TalentRelease] + create_releases_for_all_types + + release_types.each do |type| + lowercase_plural = type.constantize.model_name.plural + GenerateContractsZipJob.perform_now(project, download, type, project.public_send(lowercase_plural).ids) + + generated_zip = download.file.blob.download + csv_file_name = "#{project.name.parameterize}_#{lowercase_plural.gsub('_', '-')}.csv" + Zip::InputStream.open(StringIO.new(generated_zip)) do |io| + while entry = io.get_next_entry + next unless entry.name == csv_file_name + + csv_file = entry.get_input_stream.read + + release_class = Object.const_get type + release_headers = release_class.csv_headers + + release_headers.each do |header| + expect(csv_file).to match header + end + end + + dummy_zip_file_name = "#{project.name.parameterize}_#{lowercase_plural.gsub('_', '-')}.zip" + if File.exist?(file_fixture(dummy_zip_file_name)) + File.delete(file_fixture(dummy_zip_file_name)) + end + end + end + end + context "When there are errors" do let(:error) { StandardError.new("Contracts or contract templates not found.") } @@ -42,10 +74,10 @@ describe GenerateContractsZipJob do allow(ProjectsChannel).to receive(:broadcast_download_generation_update).with(download, I18n.t("contract_downloads.download.failure")) allow_any_instance_of(ReleaseContractCollectionService).to receive(:build).and_raise(StandardError, "Contracts or contract templates not found") end - + it "updates status to 'failure' and sends user a notification" do GenerateContractsZipJob.perform_now(project, download, "AppearanceRelease", project.appearance_releases.ids) - + expect(download.status).to eq "failure" expect(ProjectsChannel).to have_received(:broadcast_download_generation_update).with(download, I18n.t("contract_downloads.download.failure")) end @@ -56,6 +88,21 @@ describe GenerateContractsZipJob do # Delete the file created in fixture. # Or the tests will fail on next run due to already existing files in existing zip. path = Rails.root.join("spec", "fixtures", "files") - File.delete("#{path}/my-video-project_appearance-releases.zip") if File.exists? "#{path}/my-video-project_appearance-releases.zip" + if File.exists? "#{path}/my-video-project_appearance-releases.zip" + File.delete("#{path}/my-video-project_appearance-releases.zip") + end + end + + private + + def create_releases_for_all_types + create(:acquired_media_release_with_contract_template, :native, project: project) + create(:appearance_release_with_contract_template, :native, project: project, person_name: "John Doe") + create(:location_release_with_contract_template, :native, project: project) + create(:material_release_with_contract_template, :native, project: project) + create(:medical_release_with_contract_template, :native, project: project) + create(:misc_release_with_contract_template, :native, project: project) + create(:music_release_with_contract_template, project: project) + create(:talent_release_with_contract_template, :native, project: project) end end