From 84fe10df470ea0b7b12a9a66c9df607c83f9c91b Mon Sep 17 00:00:00 2001 From: Anton Ignatov Date: Fri, 26 Jul 2019 12:40:36 +0200 Subject: [PATCH 1/4] Fix tests for models and tema import/export --- app/models/asset_text_datum.rb | 2 +- app/models/checklist.rb | 2 +- app/models/checklist_item.rb | 3 +- app/models/experiment.rb | 14 +- app/models/my_module.rb | 2 +- app/models/my_module_group.rb | 2 +- app/models/my_module_repository_row.rb | 2 - app/models/my_module_tag.rb | 4 +- app/models/project.rb | 2 +- app/models/protocol.rb | 2 +- app/models/report.rb | 4 +- app/models/repository.rb | 2 +- app/models/repository_cell.rb | 2 +- app/models/repository_list_value.rb | 4 +- app/models/tiny_mce_asset.rb | 1 + app/models/user.rb | 9 + .../model_exporters/experiment_exporter.rb | 11 +- .../model_exporters/model_exporter.rb | 41 ++--- app/services/model_exporters/team_exporter.rb | 19 ++- app/services/team_importer.rb | 53 +++--- app/utilities/delayed_uploader_demo.rb | 10 +- spec/factories/assets.rb | 9 +- spec/factories/notifications.rb | 6 +- spec/factories/repository_cells.rb | 2 +- spec/factories/tinymce_assets.rb | 6 +- spec/fixtures/files/test.jpg | Bin 0 -> 70252 bytes spec/models/activity_spec.rb | 8 +- spec/models/asset_spec.rb | 16 +- spec/models/checklist_item_spec.rb | 6 +- spec/models/checklist_spec.rb | 6 +- spec/models/comment_spec.rb | 2 +- spec/models/custom_field_spec.rb | 3 +- spec/models/experiment_spec.rb | 6 +- spec/models/my_module_group_spec.rb | 4 +- spec/models/my_module_repository_row_spec.rb | 6 +- spec/models/my_module_spec.rb | 12 +- spec/models/my_module_tag_spec.rb | 6 +- spec/models/notification_spec.rb | 2 +- spec/models/project_spec.rb | 10 +- spec/models/protocol_keyword_spec.rb | 4 +- spec/models/protocol_spec.rb | 11 +- spec/models/report_element_spec.rb | 19 +-- spec/models/report_spec.rb | 3 +- spec/models/repository_asset_value_spec.rb | 8 +- spec/models/repository_row_spec.rb | 2 +- spec/models/result_spec.rb | 6 +- spec/models/sample_custom_field_spec.rb | 47 ------ spec/models/sample_group_spec.rb | 53 ------ spec/models/sample_my_module_spec.rb | 39 ----- spec/models/sample_spec.rb | 54 ------ spec/models/sample_type_spec.rb | 46 ------ spec/models/samples_table_spec.rb | 33 ---- spec/models/step_spec.rb | 4 +- spec/models/table_spec.rb | 6 +- spec/models/tag_spec.rb | 4 +- spec/models/team_spec.rb | 10 +- spec/models/tiny_mce_asset_spec.rb | 8 +- spec/models/user_my_module_spec.rb | 2 +- spec/models/user_notification_spec.rb | 8 +- spec/models/user_project_spec.rb | 2 +- spec/models/user_system_notification_spec.rb | 4 +- spec/models/user_team_spec.rb | 2 +- spec/models/views/datatables/teams_spec.rb | 8 +- spec/models/zip_export_spec.rb | 2 +- spec/rails_helper.rb | 3 + .../client_api/invitations_service_spec.rb | 18 +- .../client_api/teams/create_service_spec.rb | 2 + .../services/client_api/teams_service_spec.rb | 18 +- .../client_api/user_team_service_spec.rb | 26 +-- .../client_api/users/update_service_spec.rb | 2 + .../generate_workflow_image_service_spec.rb | 1 - .../model_importers/team_importer_spec.rb | 35 ++-- .../test_experiment_data/experiment.json | 156 +++++++++++------- .../repository_actions/duplicate_rows_spec.rb | 2 + .../repository_datatable_service_spec.rb | 2 +- ..._table_state_column_update_service_spec.rb | 62 +++---- .../repository_table_state_service_spec.rb | 4 +- spec/services/repository_zip_export_spec.rb | 2 + .../smart_annotations/html_preview_spec.rb | 2 + .../smart_annotations/permission_eval_spec.rb | 2 + .../smart_annotations/tag_to_html_spec.rb | 6 +- .../smart_annotations/tag_to_text_spec.rb | 8 +- .../smart_annotations/text_preview_spec.rb | 2 + ...es_to_repository_migration_service_spec.rb | 2 + spec/services/templates_service_spec.rb | 1 + 85 files changed, 420 insertions(+), 622 deletions(-) create mode 100644 spec/fixtures/files/test.jpg delete mode 100644 spec/models/sample_custom_field_spec.rb delete mode 100644 spec/models/sample_group_spec.rb delete mode 100644 spec/models/sample_my_module_spec.rb delete mode 100644 spec/models/sample_spec.rb delete mode 100644 spec/models/sample_type_spec.rb delete mode 100644 spec/models/samples_table_spec.rb diff --git a/app/models/asset_text_datum.rb b/app/models/asset_text_datum.rb index 30fa7bc6e..3b00cfe78 100644 --- a/app/models/asset_text_datum.rb +++ b/app/models/asset_text_datum.rb @@ -5,7 +5,7 @@ class AssetTextDatum < ApplicationRecord validates :data, presence: true validates :asset, presence: true, uniqueness: true - belongs_to :asset, inverse_of: :asset_text_datum, optional: true + belongs_to :asset, inverse_of: :asset_text_datum after_save :update_ts_index diff --git a/app/models/checklist.rb b/app/models/checklist.rb index 321ed514b..4150e1739 100644 --- a/app/models/checklist.rb +++ b/app/models/checklist.rb @@ -7,7 +7,7 @@ class Checklist < ApplicationRecord length: { maximum: Constants::TEXT_MAX_LENGTH } validates :step, presence: true - belongs_to :step, inverse_of: :checklists, touch: true, optional: true + belongs_to :step, inverse_of: :checklists, touch: true belongs_to :created_by, foreign_key: 'created_by_id', class_name: 'User', diff --git a/app/models/checklist_item.rb b/app/models/checklist_item.rb index bc9f7b4c3..d66c1d14c 100644 --- a/app/models/checklist_item.rb +++ b/app/models/checklist_item.rb @@ -7,8 +7,7 @@ class ChecklistItem < ApplicationRecord validates :checked, inclusion: { in: [true, false] } belongs_to :checklist, - inverse_of: :checklist_items, - optional: true + inverse_of: :checklist_items belongs_to :created_by, foreign_key: 'created_by_id', class_name: 'User', diff --git a/app/models/experiment.rb b/app/models/experiment.rb index 5d5bbb40b..5410a8838 100644 --- a/app/models/experiment.rb +++ b/app/models/experiment.rb @@ -3,15 +3,13 @@ class Experiment < ApplicationRecord include SearchableModel include SearchableByNameModel - belongs_to :project, inverse_of: :experiments, touch: true, optional: true + belongs_to :project, inverse_of: :experiments, touch: true belongs_to :created_by, foreign_key: :created_by_id, - class_name: 'User', - optional: true + class_name: 'User' belongs_to :last_modified_by, foreign_key: :last_modified_by_id, - class_name: 'User', - optional: true + class_name: 'User' belongs_to :archived_by, foreign_key: :archived_by_id, class_name: 'User', optional: true belongs_to :restored_by, @@ -225,6 +223,12 @@ class Experiment < ApplicationRecord workflowimg.service.exist?(workflowimg.blob.key) end + def workflowimg_file_name + return '' unless workflowimg.attached? + + workflowimg.blob&.filename&.sanitized + end + # Get projects where user is either owner or user in the same team # as this experiment def projects_with_role_above_user(current_user) diff --git a/app/models/my_module.rb b/app/models/my_module.rb index d764eb14f..ed3da9b15 100644 --- a/app/models/my_module.rb +++ b/app/models/my_module.rb @@ -34,7 +34,7 @@ class MyModule < ApplicationRecord foreign_key: 'restored_by_id', class_name: 'User', optional: true - belongs_to :experiment, inverse_of: :my_modules, touch: true, optional: true + belongs_to :experiment, inverse_of: :my_modules, touch: true belongs_to :my_module_group, inverse_of: :my_modules, optional: true has_many :results, inverse_of: :my_module, dependent: :destroy has_many :my_module_tags, inverse_of: :my_module, dependent: :destroy diff --git a/app/models/my_module_group.rb b/app/models/my_module_group.rb index 718c53fc0..6682ba6a0 100644 --- a/app/models/my_module_group.rb +++ b/app/models/my_module_group.rb @@ -3,7 +3,7 @@ class MyModuleGroup < ApplicationRecord validates :experiment, presence: true - belongs_to :experiment, inverse_of: :my_module_groups, optional: true + belongs_to :experiment, inverse_of: :my_module_groups belongs_to :created_by, foreign_key: 'created_by_id', class_name: 'User', diff --git a/app/models/my_module_repository_row.rb b/app/models/my_module_repository_row.rb index c28ce10a0..f0d9eb674 100644 --- a/app/models/my_module_repository_row.rb +++ b/app/models/my_module_repository_row.rb @@ -4,10 +4,8 @@ class MyModuleRepositoryRow < ApplicationRecord class_name: 'User', optional: true belongs_to :repository_row, - optional: true, inverse_of: :my_module_repository_rows belongs_to :my_module, - optional: true, touch: true, inverse_of: :my_module_repository_rows diff --git a/app/models/my_module_tag.rb b/app/models/my_module_tag.rb index 9dac83c2e..10963cec6 100644 --- a/app/models/my_module_tag.rb +++ b/app/models/my_module_tag.rb @@ -2,10 +2,10 @@ class MyModuleTag < ApplicationRecord validates :my_module, :tag, presence: true validates :tag_id, uniqueness: { scope: :my_module_id } - belongs_to :my_module, inverse_of: :my_module_tags, optional: true + belongs_to :my_module, inverse_of: :my_module_tags belongs_to :created_by, foreign_key: 'created_by_id', class_name: 'User', optional: true - belongs_to :tag, inverse_of: :my_module_tags, optional: true + belongs_to :tag, inverse_of: :my_module_tags end diff --git a/app/models/project.rb b/app/models/project.rb index 1ffd8983e..7b42f9d71 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -29,7 +29,7 @@ class Project < ApplicationRecord foreign_key: 'restored_by_id', class_name: 'User', optional: true - belongs_to :team, inverse_of: :projects, touch: true, optional: true + belongs_to :team, inverse_of: :projects, touch: true has_many :user_projects, inverse_of: :project has_many :users, through: :user_projects has_many :experiments, inverse_of: :project diff --git a/app/models/protocol.rb b/app/models/protocol.rb index 942386d98..8bc538287 100644 --- a/app/models/protocol.rb +++ b/app/models/protocol.rb @@ -92,7 +92,7 @@ class Protocol < ApplicationRecord belongs_to :my_module, inverse_of: :protocols, optional: true - belongs_to :team, inverse_of: :protocols, optional: true + belongs_to :team, inverse_of: :protocols belongs_to :parent, foreign_key: 'parent_id', class_name: 'Protocol', diff --git a/app/models/report.rb b/app/models/report.rb index ff8f9ecdb..f9f63db38 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -11,8 +11,8 @@ class Report < ApplicationRecord validates :project, presence: true validates :user, presence: true - belongs_to :project, inverse_of: :reports, optional: true - belongs_to :user, inverse_of: :reports, optional: true + belongs_to :project, inverse_of: :reports + belongs_to :user, inverse_of: :reports belongs_to :team, inverse_of: :reports belongs_to :last_modified_by, foreign_key: 'last_modified_by_id', diff --git a/app/models/repository.rb b/app/models/repository.rb index 618cbdc83..231ee7493 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -6,7 +6,7 @@ class Repository < ApplicationRecord attribute :discarded_by_id, :integer - belongs_to :team, optional: true + belongs_to :team belongs_to :created_by, foreign_key: :created_by_id, class_name: 'User' has_many :repository_columns, dependent: :destroy has_many :repository_rows, dependent: :destroy diff --git a/app/models/repository_cell.rb b/app/models/repository_cell.rb index 2ee710f0a..826f61e49 100644 --- a/app/models/repository_cell.rb +++ b/app/models/repository_cell.rb @@ -35,7 +35,7 @@ class RepositoryCell < ActiveRecord::Base validates_inclusion_of :repository_column, in: (lambda do |cell| - cell.repository_row.repository.repository_columns + cell.repository_row&.repository&.repository_columns || [] end) validates :repository_column, presence: true validate :repository_column_data_type diff --git a/app/models/repository_list_value.rb b/app/models/repository_list_value.rb index 77574d3da..d5cff3a04 100644 --- a/app/models/repository_list_value.rb +++ b/app/models/repository_list_value.rb @@ -12,9 +12,7 @@ class RepositoryListValue < ApplicationRecord validates :repository_cell, presence: true validates_inclusion_of :repository_list_item, in: (lambda do |list_value| - list_value.repository_cell - .repository_column - .repository_list_items + list_value.repository_cell&.repository_column&.repository_list_items || [] end) def formatted diff --git a/app/models/tiny_mce_asset.rb b/app/models/tiny_mce_asset.rb index edf5d234a..0ff5d8e60 100644 --- a/app/models/tiny_mce_asset.rb +++ b/app/models/tiny_mce_asset.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class TinyMceAsset < ApplicationRecord + include ActiveStorage::Downloading extend ProtocolsExporter attr_accessor :reference before_create :set_reference, optional: true diff --git a/app/models/user.rb b/app/models/user.rb index 9698b474a..e9d5313e7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -8,6 +8,7 @@ class User < ApplicationRecord include User::ProjectRoles include TeamBySubjectModel include InputSanitizeHelper + include ActiveStorage::Downloading acts_as_token_authenticatable devise :invitable, :confirmable, :database_authenticatable, :registerable, @@ -248,6 +249,8 @@ class User < ApplicationRecord end def avatar_variant(style) + return Constants::DEFAULT_AVATAR_URL.gsub(':style', style) unless avatar.attached? + format = case style.to_sym when :medium Constants::MEDIUM_PIC_FORMAT @@ -563,6 +566,12 @@ class User < ApplicationRecord .map { |i| { name: escape_input(i[:full_name]), id: i[:id] } } end + def file_name + return '' unless avatar.attached? + + avatar.blob&.filename&.sanitized + end + protected def confirmation_required? diff --git a/app/services/model_exporters/experiment_exporter.rb b/app/services/model_exporters/experiment_exporter.rb index 607a122a6..e5b63b298 100644 --- a/app/services/model_exporters/experiment_exporter.rb +++ b/app/services/model_exporters/experiment_exporter.rb @@ -67,10 +67,19 @@ module ModelExporters { result: result, result_comments: result.result_comments, - asset: result.asset, + asset: result_assets_data(result.asset), table: table(result.table), result_text: result.result_text } end + + def result_assets_data(asset) + return unless asset&.file&.attached? + + { + asset: asset, + asset_blob: asset.file.blob + } + end end end diff --git a/app/services/model_exporters/model_exporter.rb b/app/services/model_exporters/model_exporter.rb index 54c1fc09f..141572ce5 100644 --- a/app/services/model_exporters/model_exporter.rb +++ b/app/services/model_exporters/model_exporter.rb @@ -14,27 +14,21 @@ module ModelExporters def copy_files(assets, attachment_name, dir_name) assets.flatten.each do |a| - next unless a.public_send(attachment_name).present? + next unless a.public_send(attachment_name).attached? - unless a.public_send(attachment_name).exists? - raise StandardError, - "File id:#{a.id} of type #{attachment_name} is missing" - end yield if block_given? dir = FileUtils.mkdir_p(File.join(dir_name, a.id.to_s)).first - if defined?(S3_BUCKET) - s3_asset = - S3_BUCKET.object(a.public_send(attachment_name).path.remove(%r{^/})) - file_name = a.public_send(attachment_name).original_filename - File.open(File.join(dir, file_name), 'wb') do |f| - s3_asset.get(response_target: f) - end - else - FileUtils.cp( - a.public_send(attachment_name).path, - File.join(dir, a.public_send(attachment_name).original_filename) - ) - end + + tempfile = Tempfile.new + tempfile.binmode + a.public_send(attachment_name).blob.download { |chunk| tempfile.write(chunk) } + tempfile.flush + tempfile.rewind + FileUtils.cp( + tempfile.path, + File.join(dir, a.file_name) + ) + tempfile.close! end end @@ -57,12 +51,21 @@ module ModelExporters checklists: step.checklists.map { |c| checklist(c) }, step_comments: step.step_comments, step_assets: step.step_assets, - assets: step.assets, + assets: step.assets.map { |a| assets_data(a) }, step_tables: step.step_tables, tables: step.tables.map { |t| table(t) } } end + def assets_data(asset) + return unless asset.file.attached? + + { + asset: asset, + asset_blob: asset.file.blob + } + end + def checklist(checklist) { checklist: checklist, diff --git a/app/services/model_exporters/team_exporter.rb b/app/services/model_exporters/team_exporter.rb index 8bab1de3b..dc09b1a72 100644 --- a/app/services/model_exporters/team_exporter.rb +++ b/app/services/model_exporters/team_exporter.rb @@ -39,9 +39,7 @@ module ModelExporters private def team(team) - if team.tiny_mce_assets.present? - @tiny_mce_assets_to_copy.push(team.tiny_mce_assets) - end + @tiny_mce_assets_to_copy.push(team.tiny_mce_assets) if team.tiny_mce_assets.present? { team: team, default_admin_id: team.user_teams.where(role: 2).first.user.id, @@ -53,7 +51,7 @@ module ModelExporters .map { |n| notification(n) }, custom_fields: team.custom_fields, repositories: team.repositories.map { |r| repository(r) }, - tiny_mce_assets: team.tiny_mce_assets, + tiny_mce_assets: team.tiny_mce_assets.map { |tma| tiny_mce_asset_data(tma) }, protocols: team.protocols.where(my_module: nil).map do |pr| protocol(pr) end, @@ -80,6 +78,7 @@ module ModelExporters user_json['sign_in_count'] = user.sign_in_count user_json['last_sign_in_at'] = user.last_sign_in_at user_json['last_sign_in_ip'] = user.last_sign_in_ip + user_json['avatar'] = user.avatar.blob if user.avatar.attached? copy_files([user], :avatar, File.join(@dir_to_export, 'avatars')) { user: user_json, @@ -152,11 +151,21 @@ module ModelExporters } end + def tiny_mce_asset_data(asset) + { + tiny_mce_asset: asset, + tiny_mce_asset_blob: asset.image.blob + } + end + def get_cell_value_asset(cell) return unless cell.value_type == 'RepositoryAssetValue' @assets_to_copy.push(cell.value.asset) - cell.value.asset + { + asset: cell.value.asset, + asset_blob: cell.value.asset.blob + } end end end diff --git a/app/services/team_importer.rb b/app/services/team_importer.rb index ecb3c7dc1..906243b9f 100644 --- a/app/services/team_importer.rb +++ b/app/services/team_importer.rb @@ -90,6 +90,7 @@ class TeamImporter user_notification.notification_id = @notification_mappings[user_notification.notification_id] next if user_notification.notification_id.blank? + user_notification.save! end @@ -241,6 +242,7 @@ class TeamImporter comment.save! if update_annotation(comment.message) end next unless res.result_text + res.save! if update_annotation(res.result_text.text) end end @@ -250,6 +252,7 @@ class TeamImporter # Returns true if text was updated def update_annotation(text) return false if text.nil? + updated = false %w(prj exp tsk rep_item).each do |name| text.scan(/~#{name}~\w+\]/).each do |text_match| @@ -267,6 +270,7 @@ class TeamImporter @repository_row_mappings[orig_id] end next unless new_id + new_id_encoded = new_id.base62_encode text.sub!("~#{name}~#{orig_id_encoded}]", "~#{name}~#{new_id_encoded}]") updated = true @@ -276,6 +280,7 @@ class TeamImporter orig_id_encoded = user_match.match(/\[@[\w+-@?! ]+~(\w+)\]/)[1] orig_id = orig_id_encoded.base62_decode next unless @user_mappings[orig_id] + new_id_encoded = @user_mappings[orig_id].base62_encode text.sub!("~#{orig_id_encoded}]", "~#{new_id_encoded}]") updated = true @@ -327,11 +332,12 @@ class TeamImporter def create_tiny_mce_assets(tmce_assets_json, team) tmce_assets_json.each do |tiny_mce_asset_json| - tiny_mce_asset = TinyMceAsset.new(tiny_mce_asset_json) + tiny_mce_asset = TinyMceAsset.new(tiny_mce_asset_json['tiny_mce_asset']) + tiny_mce_asset_blob = tiny_mce_asset_json['tiny_mce_asset_blob'] # Try to find and load file File.open( File.join(@import_dir, 'tiny_mce_assets', tiny_mce_asset.id.to_s, - tiny_mce_asset.image_file_name) + tiny_mce_asset_blob['filename']) ) do |tiny_mce_file| orig_tmce_id = tiny_mce_asset.id tiny_mce_asset.id = nil @@ -341,7 +347,7 @@ class TeamImporter end tiny_mce_asset.team = team tiny_mce_asset.save! - tiny_mce_asset.image.attach(io: tiny_mce_file, filename: tiny_mce_file.basename) + tiny_mce_asset.image.attach(io: tiny_mce_file, filename: File.basename(tiny_mce_file)) @mce_asset_counter += 1 if tiny_mce_asset.object_id.present? object = tiny_mce_asset.object @@ -368,12 +374,10 @@ class TeamImporter user.password = user_json['user']['encrypted_password'] user.current_team_id = team.id user.invited_by_id = @user_mappings[user.invited_by_id] - if user.avatar.present? + if user_json['user']['avatar'] avatar_path = File.join(@import_dir, 'avatars', orig_user_id.to_s, - user.avatar_file_name) - if File.exist?(avatar_path) - File.open(avatar_path) { |f| user.avatar = f } - end + user_json['user']['avatar']['filename']) + File.open(avatar_path) { |f| user.avatar = f } if File.exist?(avatar_path) end user.save! @user_counter += 1 @@ -394,6 +398,7 @@ class TeamImporter notifications_json.each do |notification_json| notification = Notification.new(notification_json) next if notification.type_of.blank? + orig_notification_id = notification.id notification.id = nil notification.generator_user_id = find_user(notification.generator_user_id) @@ -415,7 +420,6 @@ class TeamImporter @repository_mappings[orig_repository_id] = repository.id @repository_counter += 1 repository_json['repository_columns'].each do |repository_column_json| - repository_column = RepositoryColumn.new( repository_column_json['repository_column'] ) @@ -427,6 +431,7 @@ class TeamImporter repository_column.save! @repository_column_mappings[orig_rep_col_id] = repository_column.id next unless repository_column.data_type == 'RepositoryListValue' + repository_column_json['repository_list_items'].each do |list_item| created_by_id = find_user(repository_column.created_by_id) repository_list_item = RepositoryListItem.new(data: list_item['data']) @@ -662,9 +667,7 @@ class TeamImporter protocol.archived_by_id = find_user(protocol.archived_by_id) protocol.restored_by_id = find_user(protocol.restored_by_id) protocol.my_module = my_module unless protocol.my_module_id.nil? - unless protocol.parent_id.nil? - protocol.parent_id = @protocol_mappings[protocol.parent_id] - end + protocol.parent_id = @protocol_mappings[protocol.parent_id] unless protocol.parent_id.nil? protocol.save! @protocol_counter += 1 @protocol_mappings[orig_protocol_id] = protocol.id @@ -792,9 +795,10 @@ class TeamImporter # returns asset object def create_asset(asset_json, team, user_id = nil) - asset = Asset.new(asset_json) + asset = Asset.new(asset_json['asset']) + asset_blob = asset_json['asset_blob'] File.open( - "#{@import_dir}/assets/#{asset.id}/#{asset.file_name}" + "#{@import_dir}/assets/#{asset.id}/#{asset_blob['filename']}" ) do |file| orig_asset_id = asset.id asset.id = nil @@ -804,7 +808,7 @@ class TeamImporter asset.team = team asset.in_template = true if @is_template asset.save! - asset.file.attach(io: file, filename: file.basename) + asset.file.attach(io: file, filename: File.basename(file)) asset.post_process_file(team) @asset_mappings[orig_asset_id] = asset.id @asset_counter += 1 @@ -864,22 +868,14 @@ class TeamImporter report_element.my_module_id = @my_module_mappings[report_element.my_module_id] end - if report_element.step_id - report_element.step_id = @step_mappings[report_element.step_id] - end - if report_element.result_id - report_element.result_id = @result_mappings[report_element.result_id] - end + report_element.step_id = @step_mappings[report_element.step_id] if report_element.step_id + report_element.result_id = @result_mappings[report_element.result_id] if report_element.result_id if report_element.checklist_id report_element.checklist_id = @checklist_mappings[report_element.checklist_id] end - if report_element.asset_id - report_element.asset_id = @asset_mappings[report_element.asset_id] - end - if report_element.table_id - report_element.table_id = @table_mappings[report_element.table_id] - end + report_element.asset_id = @asset_mappings[report_element.asset_id] if report_element.asset_id + report_element.table_id = @table_mappings[report_element.table_id] if report_element.table_id if report_element.experiment_id report_element.experiment_id = @experiment_mappings[report_element.experiment_id] @@ -902,7 +898,8 @@ class TeamImporter def find_user(user_id) return nil if user_id.nil? - @user_mappings[user_id] ? @user_mappings[user_id] : @admin_id + + @user_mappings[user_id] || @admin_id end def find_list_item_id(list_item_id) diff --git a/app/utilities/delayed_uploader_demo.rb b/app/utilities/delayed_uploader_demo.rb index e8a5a9f80..998db58c0 100644 --- a/app/utilities/delayed_uploader_demo.rb +++ b/app/utilities/delayed_uploader_demo.rb @@ -1,14 +1,15 @@ +# frozen_string_literal: true + module DelayedUploaderDemo # Get asset from demo_files folder def self.get_asset(user, team, file_name) - Asset.new( - file: File.open( - "#{Rails.root}/app/assets/demo_files/#{file_name}", 'r' - ), + asset = Asset.create( created_by: user, team: team, last_modified_by: user ) + asset.file.attach(io: File.open("#{Rails.root}/app/assets/demo_files/#{file_name}", 'r'), filename: file_name) + asset end # Generates results asset for given module, file_name assumes file is located @@ -32,7 +33,6 @@ module DelayedUploaderDemo ) temp_result.save - temp_asset.save # Generate comment if it exists generate_result_comment(temp_result, current_user, comment) if comment diff --git a/spec/factories/assets.rb b/spec/factories/assets.rb index ae9088e9b..a701f048a 100644 --- a/spec/factories/assets.rb +++ b/spec/factories/assets.rb @@ -2,11 +2,8 @@ FactoryBot.define do factory :asset do - file_file_name { 'sample_file.txt' } - file_content_type { 'text/plain' } - file_file_size { 69 } - version { 1 } - estimated_size { 232 } - file_processing { false } + file do + fixture_file_upload(Rails.root.join('spec', 'fixtures', 'files', 'test.jpg'), 'image/jpg') + end end end diff --git a/spec/factories/notifications.rb b/spec/factories/notifications.rb index 5ca75eeec..9df711663 100644 --- a/spec/factories/notifications.rb +++ b/spec/factories/notifications.rb @@ -2,8 +2,10 @@ FactoryBot.define do factory :notification do - title { 'Admin was added as Owner to project ' \ - 'Demo project - qPCR by User.' } + title do + 'Admin was added as Owner to project ' \ + 'Demo project - qPCR by User.' + end message { 'Project: Demo project - qPCR' } type_of { 'assignment' } end diff --git a/spec/factories/repository_cells.rb b/spec/factories/repository_cells.rb index b6b0d16de..e06597088 100644 --- a/spec/factories/repository_cells.rb +++ b/spec/factories/repository_cells.rb @@ -7,7 +7,7 @@ FactoryBot.define do trait :text_value do repository_column { create :repository_column, :text_type, repository: repository_row.repository } after(:build) do |repository_cell| - repository_cell.value ||= build(:repository_text_value, repository_cell: repository_cell) + repository_cell.value ||= create(:repository_text_value, repository_cell: repository_cell) end end diff --git a/spec/factories/tinymce_assets.rb b/spec/factories/tinymce_assets.rb index dbd07ffc2..5de498ec7 100644 --- a/spec/factories/tinymce_assets.rb +++ b/spec/factories/tinymce_assets.rb @@ -3,8 +3,8 @@ FactoryBot.define do factory :tiny_mce_asset do association :team, factory: :team - image_file_name { 'sample_file.jpg' } - image_content_type { 'image/jpeg' } - image_file_size { 69 } + image do + fixture_file_upload(Rails.root.join('spec', 'fixtures', 'files', 'test.jpg'), 'image/jpg') + end end end diff --git a/spec/fixtures/files/test.jpg b/spec/fixtures/files/test.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ef4fcb2ade1769a4b815372e0a3140c7490b4acd GIT binary patch literal 70252 zcmeFaY0NCimJs&cdwcJGYrlDbWXyFB4B(_YGpj1IvYPiAa;?hDT5{in!MX2yR#j#h zVPl4oWe^}s97`2!S8I^=rn)_49Ce>V2s{h_Q zkmqoiu=l&P8Pd9XdWGwH^>O(22OnG;`t)-jSKxozPIK&vTPuMNhecS1)zAYRqVVU{ z1K9br;^!j%yHr~9W&FhzV>m72@7CsS_r;aV@^fqUgA%?#=;!vg*QO?fm9VdmogaeY z-}`wZ=m+)$F!@Ck_gO&f383+FL8V1;?1#=B>hALazEt2w%AePt<|R7$%&@=d~#oM z{0kI5rRk@$*+=NpUty+IP@nr>`7QX>N-quX(^nzm4&f(Yd9FT0W*^QF4Mjf2W*;L< z2&_{Cc?rn9v;S+R0yr}go?~AHEqgO#<*DvZl%lx8xHv<0zUS;rtBxN;s1SKXZGG@sl zrcIJ$;H6CV`Z_rU>(#|wug*NPJMKtwA?L-Ogb?!fp-52vnqtZC``(fK)ueo_3_dbR z!4SVQ*@BVLm8|EhwW^OCCVtoGo+24B)ee_08f}-zcJ1w8>Q)Jp^canIe!cY5n$KlJx zI51=B^9<&q17g2DDc;WH3iop;61=_>_qE)o06iQOWik}xG5KeJQfUb$YFOx?UgmxV zD6L*+PfEbo(64gG+Gn2v1bLtWSj+k@b~g&*GxcdZ59YyyHIBJE+Oylt-->-v*&D#m zt?;7i&nWeLalG8a<8AFS_IS|j*;eC^JC$M`(@6qzM0oBtahW$8Pl3h9{+vqenI3Od z>Z0cX{k+xxR{=h6+I^1$X;grVYm{{hhlW4M!Tib1nc7DrBrk4AxL~N z4br0uhF54<-z!l&+OSDLi6qJS5jH~$+j_Aq0jK}; z^YWxT%M_(sqS`mkbkCAeLFdyZcHJ0ilx&Pwvm; z^2*f}nG#8s08E&2NNz#eXR0RG28>CQ^)tSyoF(@vcGh2{qPwirML23(1 zk+~gd@YD^UDRVJt`ji)QC~2Lk_`Ked4o{WwIw9Sh>g6>?`Ybic8-)yVY9VhLGU6#H z<{2_R(;UeMWNOpfGrzd^FlTA?Ea+r`(9T&H$*QIkQaq4NOxLm4lbubU9IB4df+eMgxipMM`<;u%7#su zJ{O$LHRae`6PqaI>l`Xu1r^zADz;53_1F6|z)g{_-Pl=D^?7{*Sjp~*>cKq9UPhht z4R!V%YU($V97xoR-2`N~rm%d|ks(a2_zfmU3!3ct%{iWE*5=JPInq4O7jfF&0Mgq9 zkHlF;OTEC7c|mKnpq-1HwsIjmmpSd#!sT2QbkvKiTvv34hz8lTbj^w-*&gX$7w2Sm zq0jyH29S??j7Hh5D<4T3i?=;_;%SK8Lh@N<=z1rgN1xf~yDXk6W=HO3^5w$F$XLiK+X<~oA1x(d3XcJMDc+=FOE!}9~=^cnWjGs@ls`u3yGGAf@!Kj|yY4lCInUvLpD>3B^;u)VZ}XDVv&&eN{*VjxA+1p~Q^s zcj{u?G3>O{V3$Zi0GF@J97$e>?yO}L=z9^M#Peppx_p{)g_V2!M?p76LP)Q?OPAiBt5P7x(y|Hyw-Kc z9ZO~H`OaLa*>zgGy3)+o&B7g(u3wL`cNm9ky{NrDIk_8>_r+vXH=EwC$II>pa3kr# zCP6nFiwsI-&TPVS$R!xLDeJJHaAWU`?- zp11Vm2C!B)2R`akVG<0!=F@K42K>3s4)ng{F*!S@(pjL0TvHW75c_Vlxfg-e^3 z;QPgDvMT}b^JH@8H-N%1uGVxQ2=lsXr^pvcq?V>+6h*#9>3l6}apO*9AO>VhrZp;- zaa&HEB%b+hpALYSEqiFEd9qFHqekQOw)2n6l*qSe zeacmMvxOJuy+-vrQ9jQuth&?dG0xWbF5)LK6Yi43uIVR~+3k0TeTgoI#SUf>@uC!a zVS&sTUEAvjnp`Y;A0e~-#hLc)VuovezMqlviixZPB`MYvO7}CoIlHzKo9zME?*iZ(j`(M_pE}bqO!vP9UF$?G~7J5Q>6^n zE#jS(i?O)@43UUb0smH!R8jYVu+fW5bqj%D>$zH?;&@#q=zd$~r&Obxou5y*CJTEt zpJ`e!9t0gbYF!|a?CP$kXxLtw8)T(#IQfXOYu%uf65W(~#;Fmv-RR?9v(Q7U!;UT& zs$}d3L*xw9@CB1W9p7jRbA<-1feF^FhYKz~nH$ue(A+i6!Z~vF2K;fGqC-u@@CxHp zSd8H(#l^JLAPBj+U?olHarnwQuOi!kE;ebz#*Du6Lt7dXZMIJB$VgRF^z9Rs$+LrH zLqjeD<;*!0g=Ctl69%PZx~g+bD#;8r2P$f5mYVKiY~)$#x)FB=sIJ?{Jq4XMJ60~d z>W$fS^5kT&*E7sZgrhrKvfhZD!Zob;^l(lpCib;vEUeAh&xfh0c9f5zm*Ym(Z$l0? z6mr?jKu*@z~&P3T9)V`*DmZsQi*GH z;V%;m;we69(j6}B7Nv6om`1qh@ST`m=xtG-h-`h>O*JxRPP&H|Sef;>w6CEsCtcZV z7&F%(r4@K3uUd71?f3c8)pZC0Ogq#=8MJLv=&0LE4Qg52&G&`lj{nd zQ6i$(VQ0VMOMiu-c@>DEEa1m#oJA=H}EkLl?~_SBd#wR zw=CDKyKPRh%4?&z<-~dv^Lgu*?M7^+_AGXRdulpv>JbrTJ1;%ndboA*q#NB+x+KYd zW1jJji~GQx)a91$FX^T0EYFE+6?1&Fue+PKP zLxT4v@1&TkY`G9mwF<$@6@G#((%&)t*^H0-4^Q1iiEfA)Xj{{2}-cprTTM9um zZ5$K<-JJ|=uoSeg%-KHhZ$m~Q#&-5uJu@#35j3;e>Ey-xGdJhEfZfl`d2$KsfoSG9 z9Pf|xYN2b%TtDt#AQ1E2KYPC9^uq`}XU@wAU7+PF4%-ov^1-aJla(4uKBro4(Y$}$4ysNWR ztaDVHuPNCU=yME2ws^~swMd_9@hUJgIN#ejzJB_1cjUCV4dhGqpNJO_G zUlX_AGkX=~_6&*GpapxK=nALQ_jk|B{4{?*3-G1FFagXwa!eLiwcyMuB*`nJyi>>s zX34u6z9FvzlV8^SW$p%$Jo9UyyliMP2g3seuEO}Mg=LZkqXj7Od0MWwpcD-n07AP= zgFJu{00XeGJHU9S06PH3EBq)M?3$gisc*CTu30D4XpV9!QTnh+cB7LLv@*pfwvaE1 zmbc<$&n0@V%T zz(mKuDpsa5OTc0udSF&N*>eRAs$db`_mpNCZBQthAK1ipHz{9=>0$ze4$(QL8buS@ zrPF$qFbgY8%o@=w8$4~=_SknWeyXqd1;ghVO4ixikO7&nG@uIair|E0DEca5u$->! z6Fzpv*kPyI+nl-GMp>wPnlpsdKd~2qQJ6S!Reds38!!@TF5T@m`%qxxG0^jKd>gWm zIovu8@_^IlM0>cLr?^2N>5>R)KI}9dlawL=x@zND92#1v0#x&$cFm^q?rH^k^k&a- z@Lh+`6av^{vD7J>qhC3Al(&bxaowsF%eKX*p=+TN?A~XYJOeTzY4%EI3_-5+MZ4rI z`=zh`NFD6Wifjc)SZ%9SRm?EG8y1T;XOF{Ns0Qb(Z9Mh9No8Ant5enrTbW0@p$}Cf z?bkEd-*Be#GSpim1WsToWOSK{p(^bz&?7I$D(-AVbkWs!ei#~?gp-X8$>vK2b4om9 zA*Yt%#;>%Xw)=xz9=BsM?WFcJCo;lQkCz-Jtf6wlaM|K95)>OWa7t4YZ$q{g8>yN5 z$d=smYvUmHQ98%GmFHJ96^hvkKGTxFIOG>c*`8OnDq#(#6wAvTQsBWv)se_%LB<{W zGh*HV;#o2j2}&c%f_69{fbgvBJF_{-^L8~XFeSNQUWcFfBZ`I_Z3IF;pIEvV1s63$ zh*cOYVe-(pQDog^Y@8ju#AFZ6$txT$Cka<1VuxB1!8kuS`LHMRlJ7HUJ(m|pB+yD$ z8N*E`5za;;0%(2;^Uw@{P9ucyt%r@KZ^tt+fuZG=c2QKZ8`hoA5bMQ;*Dsc{E0#LB zVwwRM22?`z;b`kEO9fN0Wp4y&_tMu5;468beO&LAp;lI5T2Sncn*NXHjVAJ=^cle(CUc4pF6o?WrwM5+``B?m&y3k$SjZbK^#t4fpAV;E0LnA z+L2$3Y@}08CX>ZM-dLx!b?yXq>)~?(HV%t4E8;xMGt=4VNnMd*F9tL-oD!GLzi@I>VKm<`VkPG%exFQ^!X^rr+yS}`3+4r|lds){uZ(eQ z%!Td77TdIO(jX?qiqq?GL@hn)^l>EuHg0o6=G_2Z#0R*hUE?R+n~X z$cK7}YyQN?V|VMJx-0Xf zLF)wC!9m*gWs7^f&IXqaFVKd9*t?4Nqhw3B$tmMV4RE*FUhB0DBj7a~j67ejd7e3* z1@caZ^}MC}Fc}S|((Brr>+7S#Wy8`u2&LWd2QlBN>bRj!tIXnbYZVNqPMy_ZzZ7ub z5vdj~(&=>EX`FHAHj?4dx;du#E}d6f{D|{QV23gGCb{Sk*lp5wI~p_b_%r#s4H*w|fiN2+~%Oy08NhjpWa_A}01g7GJO{ zSI73~onxC~V;Uv4bK4y3=L^P1n=`)j46+sNcG^xTrX2@8H%&uhoL0HmF_{~4Bl7hn z1p1~ccN^q^o-E*K1k`A%@S2b*RpnWi(|DkzyjeA zSmn(mGv~2V*e9<$%j{0%O6|T$4M74FPc!k{1FpmZISE?`hbFnN_fbNuIY1&#jzC7k zI|;kwjL_Ueg2XX~dY0@>V{t}Ry9VA13;90e7ICrx?um@00P%{5;eJ4(g z%JCdhH~U0jC<@-2gKM|h?&T$=8y$%th0ELxLE?TIC7HC_{E*k)aEu~B(#S4)MU09$*8L%@V} zdx=l^F3yhpe5aM7F5*?ji)@WD{R{>)H%H5PW1)J@0O9L!O481*?v{4jVAEU|+Vw^b zz-mkOKxAW6gmBZ!EX9s6)b1iRjN3HY45#}kmsm0OR%=dMw4nT&k0$JB8j*Q4?z-N@ z;ECDP_?}?4I^*Osei;h5-IltQd+IhcfQiU;&iFAmxcU--eI%Aq-A;$YJWI$S6AfY5 zwgA=bGlQho(7jD})cWA)z&0Y!AFn1sfU z+2BxHue(HxwdJ_FBr~?%o(iO$^5Q70jiZ4iSyf-|TL7n$&5WAuIkHPT-OSCMHoJ_V ztB8gyq|A-gL>Con5tAdJVsojf-5N==G?5eD+V%E!hi)9T+xS&`mk%YRJc{+{D!?I2 zUw4iJuyA8`J5O7V>+tzn#W-OtQ;Wjx(*c#ZR-6x(dnFXt94gKq37%yus@3$n7T}Zm zW|k4=D%x;5SkLPaW9B&s#K^+DuY+({+pX*A9AFjgSsrC&%dsbVzmKUEHkb732v+yb z@DEpFf6l&uY#<^-V;1fNJVDAj&GVh>msyZrYE5m7(#|tb&s1@tCN2pU9EN7YDlsE* z>yZ`k5fx#3AN8vnKmff=-R%bNw#}-qx(3%VR=2S+_d{msb(#yf`NdUgoNy2XqA=T_ z&xKt>HtRukTC0?rS0fX~UUb)_OaU%6$b;gjST(w_>WaosP29=*?Lk7;c^Qy2Dymi5 z5`dmoA|#NbppwJ@;~R}=;gyO-5s}$`Hr~z7+KC|RK$-~WJe29Gxz|F`T#?ICv8_JUf>7*be|Njj?%PN z7OXJ`{=s&U>+&U;1)|Wa(0EvCpr`Zc!qEJjI9V4z>T`AUnDbonz|5UBulIp9i(}h4 zeD{FkQDMQYwOzuw$K(g0QdI`7#BB065~B`|N)Hh*^zNjFTsQmzsl#CpGn*99#0IXm z(p0RyphPNDu!dU?F$Nf?iwk{Whp}z;Qm?DkW`2;^VGEN|?L||{d#(_$=-wEb`3NI+1U28MfZB_JI; z;5{=CVw|4Z^MX*3b{4@0=akP>T-kOCkiwV245$jmGfj$)3Ar%3p1MIaa5+Zw@c z7*qsWtithB>lIKuNO8XrYX>+T2?j6>pyz8Is=I^S_Kkq&d3PFy*{z48;Um7c%XYC- zUnV&x3$r#_3D`z5&HxM2gz}?FLgL}Dr=@{OFS{8WyItCzM#%x{i+G$9zTs^&z^`rq zfzWPrrOd2mwGA~EE*y##*(6XH8?+8^vtom@IBuuA4c44vYEfaMKIMwmbr$bIP@zw;e=?*~B^dIh>qkhf}Q3dq_7dAYW?gzv9bYSmaUaB{)7 z>{}1N%e#7l3xLrkr!t~*RdyNAcFI1ok@bG-p5cOl%*X@E*~aT^C0s)w7rv{vqyQ95 zqiQm^G6Hd>mKfB?g^Lh}eRw*7-sDyp5n%Dt?byS}2D6byri7K>II}b_)3DM|yNQUd zDeg;8R%Z(fc$-mb7tvA_ZKtkeum~=*iYFcV^+KXqZJCJdx`2c^Nsg z!(pewsij0E6{UCP0Ag*i`9u;qFkg~_3@K3;q~3Nq)y|-ZQnzp?WxWzhm{|y7c>=7W zl7__!z7!V55S2?|7Sck9rh41E!@9u&dByIXb7ahR!Z34+d{Lzs*68cF-k;c9rR7xu z^ANm?FeNk)b=Moc(T+G1WV3TZIT%<#eU7y1-3@n8V^&Ga8AjpWN|~Q) zEcS_YmmG{WTB81}jm+BCk0%Wd!}8Kstslm1TV>M?t<7iRPREy=4l4x+@TrDeud(xXvaWmB{f3xWjoU&iFbg$ zR&i*l2{gADNA)y^+ejiv_)KED1>MPJKF3^lyKm89WgL}`GHh6L#g?9hiP#efp09wC zm1n^rogCqO;t%Y6Zb11-lO{PnUYBG;bgYSM{PvDRwK_NmcTn)QS1cO^7R;V0IGl4~ zv&N7loF{RzILLgpjad3*kIe!)aP&4bg|%Y&BGUQ%StU&y_>ArmK`dbiK+x}?CF%sK zOCzkO(Bd9frL!c=oe=SWn}O3eK0b8AY2BSTT1LskuIY|sNXTG=GSBX%%ZY5f2SIT( zvoV`p(-3?IpE}M|m10T@fEDnwuBjy=klkt~4p&Pm@TPa`T$!`MAVel^?V80X4`Lil zYf(4U8)3USaW-blrQPtx<(DTJ?R%E_652ICMG502WE~jpwQJU z$Q;}Nw&%O55XBj=+cut$=RM(jci-qz$zelL&a`@(Y3Q^AI^HS5ygjfW9wU)d7AS>y z8YB*810(9;$nNx!7%vwY_koRjzlMRIG8c4YQJv4XA-7|;5?wI;yTJ!S&v?QYZ9NY= zw;qm|Nt6^h<8u!>iam}9^8%4H5b(F1R1?`AmTcaS86^n7O51`cyBok`0~`dTr%Uam zwq_=q-4s)5TXx-`_lufAm%+~P`Yf8~R57enRXq#`XGo6=5~oUF%8`evBYEMpnAOiF zWzBR;?*MEIk%C^O0hgB`g#oxSJkAQU_DZ~n91-aTh;S`nl&g&)L1xN;vaAH;aKLZD z+Axg!TFy1OSD=YE+vrr1qoMJrAFMMFI{|mT-40!MfM;NN<*Ect8K-9iuwi5FJS;j- zn*gH_V4d~)+RO0Z)7pNr)7@LZ!s^G1qRAufe zV@HwBWbUOF3=+F5kE*=#V7^2%kEmg>PaH_;iy528)>4c51Q`Dkx~Sj=K5^k`W;kvT z4Qzy*YikK6V%DSKA}^dwRaot=FE@^Qplq~lHdd8(MbC2-=~@vuH6Y=$(J-<^p%gcd zFl{%7fp^KUvgVn+C^b(}SFwt+zN`*Sxw=OLos1Rm2~`NR*<>SN=QuiB0H$ho(Jb5Z z>1Esj_9Yl>EEp|y+%mnpW70O0+I)ZFoo(vQlAe<};0-_D?T55H$f6-8A;A4HvfNV5 z*lACi<_=5wzKnt5a6pfCk@p+mr)a(Nr1Z636j)B2dLrj%ZE!4ibt%;8eaO9#%KiEb zIY2$D_WlLPkCCnz@B%juTkZsz77&`de5Qmz+8MfB0+PQZ-~*}RxM9}`1)Rm6VPNZe zc2~d`$mD?7JWm7BSHOIj(LnDh4vf}8lTVu>ST@!PA})XyM*|lyp#RZ&gn(xu0raZ5 zi*`W6)e#6(}LhZ^NO9x4R&)*(M9O`~o$xseNsGKy3y%G9x5qRYGV zwP+zHg21^n(4l;unjqMzu=2|u;Icmv<3Y)%bajEZTXnzlu1ws|%xNz`NkiOobC0yCbcOIPE*yCPE#P101JKEft?iuE5V@y7iouR(-Hu$;Rf(Uh6a4G1V&B; zT`f?Sch$q;Bv$R|jC0y>x52Iw$clhU&DnB1J!~&A0#jk2I zaThV`SF?F&FLM-+aMks;X&1D4a30lzdY5fnYpJW-t&t6c#$^7W8}Q{IHVJTmwjhAq zF){_%Yx;TXtL9-*3l$6|xH3OAy(bYCgVnPFA$E6w3m7YaMfWBGV-AqTDHz*s6&RXL z5mXyQCh$Nn%Y!YJ02Qgibal^PKs=I-_ja-M4+<7>D_e&o2*2wo^}&qCMW_Hts)ton zWua{;uI9@Kk(9mR8M?W{eICy%H1zF!HfyxiiqZ(bEg~f=cV`Kh0wLZyORaSmiFZyD zSGc^8MpC9g+zGP)?uHpWcUJs4#fh1I*o||jBf<#Rw~an8L6}*lvzbYXDxIr!XegR* zvG57H+>L1mp2F)`nByt4Oe_czy@iRs&;c}H{H-^P44WjW;d@TvGoSP)t|KnMz^E=u zS=TfHrIzGuaUU{PE^4x&_FT)TTi!TLxUtx4S_;-;t)0?BEqym}twV{UO#lSaghp1Y zV4Ar~-yRsN@H7IgrL;R+PIOz#n~onzZhrOBHpyBM_&UR)E|NCz!Oe?yBx->XLC`&( z3kE|>4cLdd8Q;2&B@8LB=}l*tC^}39_3uWEeFMWJM~@2`=-qn>CaKFBytHG$WmqjS z(u3G?OZc>dr)L}%Q(#SkY+%tMg*~ulh)4D~6U%vd7w35Tv^yj|;NbYK5P{h=IqrD^ zlRDS(t&#)BJlI~7n)3i4tWO(M$NF^+9l)-;YN464^GhkN$CVX1?sOZnU|nX8poa(h zV&gRsNCS`-kFB)fEMt?UU3-=FPz8A8&nMyq+q{UQA>0`2bb%`Y2FvqqKP+l6>sSoXz(~+g~z!J!d1ef3GpG5;64C& zcNC(lEQ1(mwOisqTx!B3beFWH@5!TgV0TE~Xepm7DCDH8~CKPxn zAJK9LoQ*szhj;5-DYKrf>=}EOYX+l?bw(z}-cTz=oidyOjwpPX@dnFGsx=LbfjTbO zObJBK{b25g4SN|)#I}8mG4AqV3=SfUggh?{Z5e#6vx9cru)m1Tvi_L2HY^6NkuRFY6oy&6W%?QTYpZgt%Yn*oNo?qyN&YTFb z$Oe7MaC!y8Y17f39cz98?6={J-)qoCNhS0~HgCZ8cJ~v;RUEb%jj4;=&V^_mgrE}VN;>Ig<8wI(|yQL-N(HW%=Z-|30ujV z)Ozi5BL^&hk+Zi)s1xGZMi{4E9p^x5wxCXnh`~S2m1w0C+x;AMJ-Fx!2)`S~;TDL6 zrxuw5v2?paEZC4ZD>4KiCLOJ1eS+MbVONkXK{uwn0L#(=-pc(|Mhs0b9an8M?}rV% zywjlo!TQLG*tv6s}+5lIOknJ_>oB(srPz=*T!nX!kPcYaq zfpG+DY)0<#LSEc(*jDGBPZ@}?;0PH`n-vj5`O+aLehz{MS`vg#cTFly7k-&m@M6KQ)YZu8|x;vb%i9Dja7zunu*2w{iLDzR?3k zo}345<)^i-Gv4a5j1EAt3oOG+Lo;>}0Y~$!%*6~A9n?9_)f12)KxD{DK%sDJH2cF5 zt1%g;pYcO6tQo$Ip=K@V_*8QkV%Z(|tvD+I+kmGqTWsrS`er4tY(PsZa5&(qv09jO zNP*OiDMm>;t2IK1^lg@dNpY3xYWPt$Wd&#D9=@;)oC@c5+;9fHg6Cxfyl5+yI0F4o zlyD#oJQs0xK!h2WDasPPg5bst;7Q@hoF&D<2n}5dOR=5fN)40dFv=TX2ig%Xuv3Ek zZj2;coXT0rscth;jvMmKh5$aT?V>y_Q(Ir&0CFNwLPujfU8Y4kFC}YnBpc`iT-|GV zQW9t@n5Xkm-1jcn89E@eyL;cT9k6r=^5iv}=_oSJ3Kx*GRSA68 zO;~W=t!pgn3^x!A$-uTjIETQI7fBO|`Y6~qBykD9n$ICfn%7HBsExZwyfp zF-r)aQMJoKBLyK*l`n`A2Ht53+l53oGfpnP>;Ou2cX5*RoTpI%j#l9g!C-e;R}=9} zoU>?KY5e6$}JM8;G^A*)H=6l@r z9lqTiv?L)^jYoFgRxP59pSk6#t3VY0;0VY)5@VYuhC*sfUy&IcIN;`nR&YN(}0!}&o631Db z(fs~+bqYbi#0E&cU-KJ2VCGzS6i)^4`<8BADHF9%j#x(A+(i-a)I5C*|P zHN8ZM7K1t~?W?=Pn)GrIF(g7@L1~OKKRMfQ0Rh`TClIo1akS_gbE}(!rnbo8qM|Fu z2r7Y<1)D$Q_8JUii9*0k1D5ZdY_1B&%euy^YDNww1yUy@f``6>TMnFUQ8&#TqsO@m z$*1jZ88$2Jkk@H;pJiTOX}~4EgdGt9Tck(=$#uJAJsL_*LpH==?O$(plcOpo20xek096r=?_Vc8y~f;0(*MA?%D@s$$FlFB|C@IBD}EtK2nu z4T#XdO%a+Pen+y^Ve6Q_&USI@usU@F{t8~ww*(Kdp63W4iuOFny1zlhyk2l}eSo<( z;5PP(lt$w&iohzD0Qq>>SuoYXAb6vNP1B6|SWi-wJ}5eB76DE!Q5eKtcogur7CKkY zYlqVfp65VBW8}vC2qJo*I~^|PtS!ZszF7i8nd{mXT^{CcJM;(+J0v`r%Yisn5jgmO z6@Uu-nQpK|0VukyQJ!RaYNgkAarJcHm(2tSLgQ5@i9Nx)bFZui}k+?^*-1bFax5+oKFkDv)XXGnM4clTI_lAxLGn@gI&NEIE z$jX^6cqO@-vJ2VW2^?V2AzD?%IarEf^GvqEVgyE|4DHnnQf!$yN*geCC4z)K3wmX) zQbqG(7uX%>Gq54qLftI%GO?u%PfokT0&IoPLI-W@#%*2_B|od$=RjybT-O{yF1=5; z!leMoFq~fvy&W)BpGCl>*z%mHmkWJAUiU&hcsbSVyQlC(XdOn$UZ$DNY!Y$CsS9`u{HVgT$W{mMa&nGEXt8r{M4ugnSYbY3A?($X1We%A zbO3C9J;AsNs*zj4582LkJ))5yKilZ-yrM_2b|MqJ?u)SozS~~92FPHtd|!Iwc??5O z;p+l)XPYi+PS;5gFdQVAVxEC4xT8U76<%p`7fgVv06JXd<(~6^GW60bAnn~XDcBLN zHH%9Gtku9RvyIHn1`?NgT;)#UtH4!uJdOioL?s};&h~)y#)2<-tm%-q6l>l#j>54P z_JA60WD{B-(u6j85j{Z`X#J97&bU6z2+V8+eK@O&VvRw;kev&U9w>|lXsH9$B$g(z zXJm185mjZ8CEiZF$2lMXRs~T2MNI@X%OQgDG=hQZ3D6V3F$lK-s$?53Ex6$;cq!}v z#y8R8O7x%xL4Nw`{Ok<4woZqHo>!O+P_?x>0fVA%kvAEokY}`2|u~x&U3dP5yv_iQqMYfOlNSxGsv72m}pY zaM?1{)}Jeiv)+J69j>R$`0h2csB^JTR%}!>_9Zl6K1?BNC5zL%EN6?P%HtMMY_@E< zbH;iruzxhEZbVTj)j<_)9rbL1(-08p=-FMIL!nXzlJZ`hHrh%~PC%Vbjlw)Ai*!SR zIdC_%HpB+|9p@Qp5#q`a0iVu1qsrR)nRl3`Bb-X-yEiF z8SnNjAYEyu;O^MUFn$0AAa)##1QuopjGw}50K%1CXLsds)+w-XoPjsT>l+|~yS`_5 zpg`-!AQu7%&QKVu#{tA^89QK^BK0%4$?QyKz+v+|8z|4({n$HLZ#)JjrOehSn4cyE zGm4j6BZ3rQ6G!Y>X8~5#gc97^D+9(h0(?<27w9ZBIB*6Q(55{B2%sOk`zB?~VX**R zFQu1Y0Ew(nanR^Xr9r_j1kAN}yUlax1^|MAvu=RTY23|IcF^E!x4~M|F92_Qjd^u6 zB?G#>*Fm>INEbM~d!xfyyNL2Rz?oz~zzdg$V8}N(8|+|$M$l-lNw5UsPLRc|?8?cy z&TKYteKi+&0O28UvGody-iNGfFgRxbYim?7639H1rg0X47kBiJhxq6)A^>{clAx;& zxTysA-NCg%Zo*ls0-V2HB6Om}hWRX5``*J(4ahHV0YJw4IXvC~-r=3#nf&cfZsH3A^+Z23%K{w+s4y`YX7;b-Tjui8_&zc7eKcAUt8c;IQT;`quGhESUk5KH>NzrhHqc+H{&~>rpEEh!d-pl@ zKd+hYyHf2P%q#C-fcTUqUQEtQx2Zj&=}p>$JLsmDUR=?4y+#d)wV%y>{sLT!_p`cY z?RLlD$F6@}d7tzypp)zLfqMs^=e_9ii)+^I1^yq9#})od)xUci=OnCNy)X~?c!58^ zFz?=zTdD63q_3ml>~VJ99;%WLfNFbnu-?HFt{D+74HruqSAAj}Xp{#~ZSyN9f+j;utUwnA{@b<$S59q^# z>-Wu0<+KOz;aU1=4+>q=FFt?wuRQ=r4-bFk!;SRx>5KfIQR-`c(+v*~k0elf9)SDk zLHheax;TRa1|aWC^uKgEh9Lc93(`MuO2acq|F2H*w+dn+K_TDvr2>k@45s-eb z(-az{zxD9&)_d_wY44@<5RRaq4-a(ROhAcEhNr*6|CLX{W&Ffba35b04#S5M_#Xpq z7l8i&P&RHgJv`jUe698H^($_l0wm+;3dcVLx0K(q?_Xc|@}+WLl)TRE{r%sOZ*%tO zn}6X&-iw+q^6KCJ@Id?|z|HS^k>~xMhlfA4&cp@z1&9OWk^@AHNFxSAY56f$7(P2;hMbDHAuYw zL;vXE8^7VBhp+y~Hy)n+?|<0o-w7Tc_i6m}noTR(Q~%hVu4%9)u3xV|yz}sthi^RmqKAL-;d>u`#lwdWvxnuw zuYUNo5A1{RaCp!j?1x|f;6Ee}E zqDQ~v(Jy-hJ(@it9)0qNeYAa~JhC7Cvyb9O)uZF1-}vaaJo;^q{*_0+_tC%g=np*l zj~@N8M}O+kpL_I|9{u%4KlbQp%Yb-+kkaH@^ACFMnhC#^w$CjqHto?v3C6#=rK) zfAGeieB&>@@i*T1$v5A8^B27dz4`4o4{y3}4sZUzo4@DHKlJ9Gc=Ip6`QvZ?y|>zBMWdy9L^d8>NsUwrEa-}-}Z{TFZj<+uLkTmSIwufF}u-v0RQ!`s2z)7!uE?H_vk zkH7sRZ~yq)|L~o!z4Oc8`Sv^7JJ~zG`JLbW&L4i~&%X0Nz4Le8efQlD-d(*bzni}M zFTDG^-~A8Y{jcBsv3LLedtZO=pMH;i@7KL|eD8O>_wT; z<=21Z@|7R_%76ToANk7P{_5Lb{l2f#UvviE-9_nfoe^R4gR@2qv#U6&utGu_oyUDZ|9-PPUCV}uug zSAsW+cZCnc7r{5e55zCUpTNH*AR~|<024$I)DSEXJ|m7LSY(wWjF(hbu;eSFV}8GSPS2m?F=FM}0B2E!EN3r0~!2gX9i6((FJ zc_vS$8m2>LN@gwQ2<9Hse3P=-5DPiEQKSsO*yLpV@2K zPdMl~j5ty_ra7@V6*&Dk+c|%5adX*nm2mBFQ*wXgPU4>8!RC3#6U@`g3(xz8_cL!J z?+qUppFLj%-w{6}KbXIOe_Mb?z(^oVU`>!*P+u@za9M~<=%Y}Y(6aC=VSV8Y;Z+d| z5krw2ku6bLQFGBE(Ze?^Z*1Sd-du_CiMffjio=OZi3f@gNxYO$l}M0Slq8olkt~!v zmg1Chk!qEOmzI-`ke-qumI2A+%N)yc%eu*S%Ds?NmP?RZm8X-pm9JNLsvxTnr7-`N z>aEq=8byGjjAE4H!aJIGkarDA2ukmi5|uXJv%YtI-=mDBtfyR{e5vwAB}`>b^|dNg zwOtKeO-C(X?Mhu-JyLyHgIU8(qhFIy(@e8g3sFl=D@W@>TS7ZVdqam)$5&@kmqyn~ zx9H}SI5OPR#xOP-?taQS3 zvT_>uME5D+)0VTObGFMf7m!Q4>nm3e*A+KWw{-U>?gsAdpD8|jf8O{a^(D^(*#qn` z=*i+4>3Qy@?p5zi?ES@i%}2_o&=<|u)_2N}-!IkwnZKF;Pyl;Ce8A5@!@zGrEJ3kB z_rajxZy~H9Uqc>3jYEgRxWZDx5yGv)CnJO-@**)Kog>$x6r!r5$)W?J&tvpr`eHd^ zQ{#~1pmEDz<-gX(Q^tqK-zS(ROeDTZEKMRx3P`$2HcTE(5lJaYB~A@Wy-hPqn@*QX zug;*(h|NUEbjaMyQp@Ve=E=^_A!*vZRWtDhY-K3#@)p{i*u6#*k1 z(Y7)1vF>r1@&1Xo6C;zVlhac=Q_Ita)7vu^GbghSvv+e}=Hcf97cdrL7l{@#mS~qs zm${akR>W5ZR+U%h)vrq+8{V6!o3UG@Tlw2;+l@PtJKuM;cQ^KI_U`t54=@js z4qqQu9f=$b9BUkJoYdCQ z#>FKhrXnQ-QV`+dk};4`P}9)T(Grj{vM|18p?XdG`WKQXkK-CL5;7JtGS+KCT*BA? z+kX$O08FH(^l;vAPsjmJF`vL;K6&T_5I;_7o;-v5*Le**+%p8kCr^={KNjm_0G_}- zeF}$wj*N_m_!I%|ab@%r?inWhD}*WN~tBnTf-KBE2C7Cd<* z0gv!l#*g_(?$<9wM7SrQD#a5yA27YXpm@JQH1NFs4m zo{5Wyi~pNMLO(cIdQ=Y)3N#me08GHTydlsm%%$PRGQXXah?Tp-a$*wC9HcCJ#e~4D zY2F4~7dJ!tvA0u~I`dL^xgC;FJIu9!o?&7k%itjNR_9mC_(cafuBY$Rl6(Zgjx<=E z=mxIvM$36H%=@J#uL{#R0Q9Tc1w zRoyQSd~Rxz7}{wyIV@cj!P6@?n~9!<`Ppu~U27<2;GI25`-3=pi8e0HYTKX?i_+Ho z>I^)76*&R{(KgJlsuEpOuU`I7*$}okX04lHPJM=NrMhF-5`~?FcLY|%kypeq3Cy$D_LISywDe%Mau!*;`$a@w!VhMgKf4?pY zu>>b13QAo3F?jc|2`fZx`!CIGJPNP`H`z;Hh^iF4mew$RQOV{97{?~Z17J2saAx-g zja|~Lbk3QM#_>Yjh5h(&IQm{)!(#eKT(%CSkTR={oI63o7LooCa#BAhv+r`l7?9>< zL=DV1f03CRN<2y3N0Gp{LnLvLm(T!H(0TL*td82;6E`sHF`j9_jz|%BNg3tNwRA$fPX_> zVVmqTRX6FB85fhgyvqK$zypBO%zHJO#~5ulSh9f7bh9XL!oFnU*rxe3br@xG+&Cu;lh%A)wTL4fo~joE;4 z*#MR_YcXb*QF>9xNgpN<#t=FQ_JjNCq~(Nkv0lB;$uhpfp^DKv)oQbP**g{i&LUW9 zg{eyn(zKt{`@cT^I>#wNF?fPYlz5tH@(s~@zn?y4``Gz1^}G~VD5QurS~dE;v`C_Q z_K|_%pOpoN0n_l*1jM4TGUh34nf_o|bY=iiRI5s}@mR?vIK2 zrv1OMoc|{d1qc1h=SsdxG=5sc#?J+xbr!AaTH154CG_ruB9!F$3BRVkJ-lZ~K0?+QKvcz@W?FOiZ}Y$JIbJvvL7Gsipu-4-Ih-HeEiOhQ29PvI>8I4r zgVk)l-4Z$`EtbEwz>;LqLBFLz$;x)@4`nTJIpj@&vKvZh#;4eh!UDU=ad}Gcqyopj zf_G*(3^Au(^f(o%y+9`&i73pBff(R}?eL*?LAv;>p-N@8m{zAl7=@BqotB4oOFm;w zchVcy_#48)q$ygTvAVW$a$C(HMTjqRpp1|dk?lDKJGc@#4K3%jDT|t?nVQ*EZ(e-h z($P=({A=Et%KHdXHgGT zz%rl&@+=&_vZ4`1X4MjBPP!vf5Ix&Xb8?lDJ7xboEE11!B#DNgJ>G9hNqpC|rwIX7 z?D?8LLDf}kQ4b$6sFtH7^;xt*RmLSKi6YN*^@jNzr{R5HX6LTy@Ist5W}E3=&tlj} zcKUXRl9b=LKA)5VBg3%I2p5o6Go9W{7$F*vgmS#4N|WipvnDb^GT1lI=sg9S*ic)%V(R%hS zYpJ{OIfc0e&&Cw7TP^1oiLH!uHCDb#68o?YbUZE#ruV?ePTItGmjs0y!#sx$l8^y$ ziRYM@PcSk52NH2J=B5!b<=F{Lx#dYz$Mi0_yaVzuXlprw%~ujB*QZkX&-|YHkIZ$z zOFWsAV$ZZ57qkVSh#v-^qNenxzoA2dY@yo9D zt_7cZzS+q~hc9*Pxu=}BC6VALD%Zl)mB%MBh!1@!M(@ylQuY8K=MjA+HL|vH!pCby zr>BlJ@V+92f&$KWj^C(}8;JaTTuG3nlTzr!TlUFHUVQL5szGe`25$>`4q`xQbXWz3 z9MvREn5v9VEB7UDeE9 zdNBz7;z(co{gM3tCQ-rwCww!@Ea)LncxyfgAnorA2)rKi2tD;{#UiZLEiKm85%1esdYxqX9y1DmZ6yK z=FlC2l)(kZARFg*w^>r8%J~t6NEB0coIlyuzLgSU;=r4R3n~-f?J>&ly#pSG1nc)73FZ^|BjG~L+pdu_jDQXu$lejbbk76a;s&w5pbMVh5@iigI2LS3Q=HTgLo=pR#>Zzy^jYbUo(dfZCE z+ATRDsj16vXvtp$sIGY~fO}a$c%YvXpN2Hgu*}1QjCwfe#eaHIqQc}qc{GpqbnO%Q zRovBF*|kJq!oOdesM&ueEcpTc++p5mQ(HbLviucTwl7=%i6&NK@d0q@Ah^-arJ-Ze z;o8tjmhx{`IRB>#;7$BjG2Tf|$?JSLDn`y>6j~ghZBBRdf=F*kQ{}Ix^UPI5>=+$A5h51g=zjx;3%QH*9kr9rC zkd06GSI&q`6C6)FHpc=XC(g)mr%i(&FJkn$5~c+1x^Or^=j(PqM979m_cRr<@yBam zDoSu|*c3Ml8VUgh>`M-vU?OxtoXTg1UaoAYAvVWwUSUl6n>O36&znZzTy?y}jy;VPT599k%V69!hFyw*;n_pe)hdCrTNRTuPNtw_ghuUe()qeS2WE?Zr zz5_hA5DY^puKxKfVrXH-b&|DqPQ}8qTf&ZbtuVsdh-?Zj+VZCqfGE>HdnPmz^Jqz zVkPl4?O!KLD9%@k#FWF?U_E{TA&lX8cL(oS_kyCT+eAs#C*Xyef7}XjPRInu0OAH-(JZqn_c=i&*S9KJw`zROGOEN4CEkNrr9--*K#Nw~qM4e>CMD>T^ z=8fk3>Lr$?;oL+Z!2+g)>gxJ}tkEL}<*}iUTtM5|Z1NyGX1*hby2-s{ z7efRcF)HJGYB;r9|0MyYh_+KMS~;yNw3mozVlC~S_lfZE9w0x=l02VPu5QuTuT}HS zEUTl6jIDxO@SI9WDJjxAIzS3J8MAUgkf9S!G!N$sEM+-GIDI!BM*U#(c}y^Wn3wg- z=U6UAus16)s%HkUstNRL*7va-Usr9IC#`BwLW-dr#c7#=$OQ?$h*mN1*;m}ere!s_ z(tHLN#T~~pyQwLga~`M3W$4ez=y>2=_O^p^F$2G*Z0A@QAD|KGcxzm%cDcRa91MtI zWHz*Y&S@P%Vbp`PuA{Uver`9+$YH=Zr>*KldurjAOYW!IhYqw~zu?Lvzjw?CI{4f?_Oj!a2C5XcGBNI2*)0q@(Ocfjr@7h6K zhSB$5^S!O8B6xUbmp5jBTHj3RolI8)D>Cvu=89HA+=g^558}KFz5gb-qf$^np5t-$ z_PF;eXVO|LhN=XdkC_A-g$iwlk*7LInf!OoyH2dsSA&Yr{-*Z73m5&pGbLPm`&wHn zgw5$IkXXf$3wLiWV(XdRYn&<$FN7e1oKRXr*S8@n2QC0gLs5bg&oX(rr-6Q?p@C*w zP>zEqk-U?POz>%m+Mwd)*W@w)lQ=hJ`x9=9kS(<;`SFPT{9uBWDAkqKo5gv7bjK9x zF@sUbHq%_f{Te3Fm_3f(`|d`zVP=Q!-3WL4SQ*W?J6CXZRZnGXrWJfYJnL$U^^GlW zd5$^1bl-9oqdO8i-nyMUEj_vfS8d1R{O(;K?N-1LID=q!w(|gw=nf|a4qkzM&B2xj zzv~)paxRF~8o9A8W$zH3uXG5{mdAK(2&eH&Tc7}U5f-Y$DZh1^Cx&*nqbpAhXeN#e zbmOCX6&Vv_A!x7miOAs6@pMav$9L5hmZ=&QVjN0tcd_txMMG7UN`hEA2S)|U1}+b_ zYMP6!)M1Prs|kRw;;>VE3pV3h!_9gN5@i}#2nE>KIN^TsT4o5t48jUMFLr%%?kV_g zR13~yz1d++{rWAUoUiJ*iZY*qSwMr#o))tU0J{Q!`=9zrLN>=~C*8HOmW>*vuID3? zN$}fVx$fP|IN#%NM!Ov(x(r=qivjx~f0Cv&o**0G-1s{c@%rk*lH9C`rI)@sFS(L@ z$6{M~F3IX7TFHXEY*dq`vxzvh7iE2)mprjDQAv)g?mtay60fhxDbCGLa7v4>kb#3y znevuTHU*-1OBn2H1;AXU$Jrew$H{cQ97QS=p_8Mm%N06n%{u4Pgr=xTUz65-BXmK8 zVB;a>k1G%hFvorZ63OG)&07FeObd4kp;uK28?lpwS(t9|7@j*ggtK|3`B`}d5#?g| zlP(Ko@b9^+B{}zB4ca`zY{zhr(ANwaQ5t%c_q9T~ygros+l_`vWHZ<+0cqM!^xJ=4 zeJ@873APn8K>$x|W!vn%Fx(l1)-cIK*&%Hu?n*yp^@xh0pam&ew|!R%zb#wGXz}Rm zV$AZas8)n={2!k_jvIo?xu*z#r;J`1U-LVUR~mrh;!w&TOf*hzjv94sq}0%tTnKgz z@w(e<6dmUwW$$~pIt1P#YMW9B-yfw7VLtsc68|#2yvT|V4~fj`Nb6@e^%nRz)c|F8 z_xiQhi=!vQHefBT+877q9+v%=np-%v9Rsjuo)pars}F$l3YNc_hyUH&{6FTQ;Evrg z&brmEMh-8;#;w=c}i3O+)fMD>)6Dtvj|rJ#D{<441>yU|}6KdI@9 zwxVSUoTSaBYeZ$rjmHJ;uzPRs->*<+8#7C;@A9QG28m zFxqMBZb{d95)va;EV|i~4~3@|I9RGkTnmJXSauim`ce!z7cZUoneUu&j7QqxlZcLr z)~qZ8AsV|Sd&APGh9FO|dw6&Y6Hn25ct`B-sNp@U-W+AbWsm>va+j`93w*lUCA~Ur zg<>>-g(f57E5?4@jcZn*$TTx*KOV|{f1~Z0U3u~IDCk-drILn}md1gsZ)^&SAm}E@ z$9W2o12L--@`4_N6?TEByYHgU zX!fiRc>RoSvUYjccs!Fw-Vt2*s5mqKxI@^7U}b=&n=}jwi z>g_^WAIQ)7^0=Le_;TYJp7MV9Vn%_7;8<+i1LOAQ@dnvt&RoF4V`7Itp8VXzBp5+&p6#}-#q&X zN(#Ouz6i5V?ol0H4smF+G2Le7$b}ZUTSq?tn9UPK){756@K(CTd9Ac*=fg;M3o)23Sj+t+#(Ha5nV5=3YuVEM^r+&*U}sg=6@c9LJbZB1(mc2?15R;5hx0$jj)6I@JL#-<6?Vp@eSozjRs zm<1XX>yNx}ciP!C=_W;e7tFeEI1Cw9*0Azj*BzWBL(u`S)uV<$Cx*ci^CCosMUobX zL_C}llstJBM}_Qcj?Gen5p%C>B!;RTs*5Xz&0&e+<(~|5u|Q}E2z}TUQWWcCO{Jh9 z-%s;wZMnsGO(kADQ-p0~1J(`s7fU`4HY%@fTdC@s26GHzuijrY?zmJm5@ zfulBa4rS>YK2zt23abhNQnzYe4Wcl7?s8ZW!6{RD8LLOe<RP z{N|`7SiolCTrDzDMl5VqMcm=rYv!z%x?}?~n)S6lCjeVis2V>imdZ1*i*v-t)6s9FJJ3{ZuF(z9^Zp@2aP zV}4Q}8z!=8tVviS95~$2G(9nBT-ag$_~v;ab?+vg<`wkz`<$(vjSU?Hgytq~q41JV z38=B)!om~J9B*nRpVC4de?F>5fwJnX zsU(y}KnmZC@|b7oObQ8!3R8Pj{w8zMP6VN=1Yu>hbt%fM@9`6u%6ghQXIi%o$|{G9 zJ|UJxfWaxez0#QCnDDn5yH^M-b)5(vwu%6ap^&x6DFe11f%=hgj^VeXgSvz1lM_vq zToaDoY2PrBJ==`)I|*8hitRy;t}ZjhR)u0Z+67k3#W6?j*jRaalPZs2onsX59_ehq zA`xLqvscDVft|H!zm9ZM8=7NRS1p+x^t{cbKlLUce_iBWVs5%a;jv1g{BByl6Rytl zdGjZo+4g%XhjyCDl#1jD94r~Q4&8?C$=G=vjW0}Sb7@f$aM@15;}ObZBSU2Q$(;cx zoL*WZb}^x21xv_L?S(*p{i+MdI@L#Lv&;$!oQ>xJHqP6d-AH7Isd297`jG~$1Ru3F zzaVP67^`gmGZZLF8U!2i@d#rOboi)uMpV%A;x^;~VCK+v6QT=I^fEd5;YVNl6SZmC z(|1kSlE2g3iv4n|QL->G`@2@)Cl5-M2s=B|xr8l9YIX7|r%ncnWw-mRqyPB4$w97D zh&s1iS~bh?+}jhv;$w%p@>;=-t@i@GaAztWbp0s%isNDXY0ctvLK>`C^aIfEFz<_E zjwv#To;Jrzq%^Q8WQQgl%EYSp0y0GUZcM7Uz6a!h)&rLOFBGuNb?;Vw{KTNq7H`kbWx`nL`@4zHnn~i71>xH9$AFmJ- z9soxQ|9BFmtYv~Tt8x3(aO}0+5wxA{n!uiUQEuOF2QDa>K`2Z^!$URdBkaVGS$?K1 znDU{tyIfu}|AO;6#!~!<+dgt=>!r(jUVOU&sl21)ea>0V_EPjB^bF{(>M-zAX>oih zMz>Np+7TwXe&EKt06a|`O3ZXu{xmgUd80bVek0|g{1@cB<(~kx%+}wC_BMs?3l8G% zGn?{T=o*Yazdkaz;%bhV5}26kk)L`3$6-80LrPOE6uS(uVGV!ky}z zDv9diL`%s}b-Wp+>-(0trXr_6jT4;Z`FF8?o~kJ(#)v*x=A`AeYI-%4guAE7JecV=(x z>bW<#levHOVd>YoM}#J)p_e&-^+D|;K*_J3H8*=bpcrQBr*y>jxc%{I_V58fca#2~ zF(!tT<~)rJXWhr_B)s&kMKMNbTh?`qK}$DYL(2zX>bJ^}L@N5Yj{Kz^s74xd%1Rr= z2MXO&qd#_}@-$xgsZ`pMx6d}}Zp`K+MCMhEOJV!ep@NBO8|H&H9sX+9UxXiZBcXd5 z#LVi;(KI^*tK@>+d{LoJPfN*$VJWxX_|#um&K`HQ zHUlTT6(>%&XBop2S?xVSY$K{4D2~I`WR7q68-YOn5QwQM>W5p7W9y(L`w6@j($6vf zWb07@Er$Q#YE0IDIp1gGEkz`aCAdg*XnT7ZDn|3l*O;Tc^xHB}N3Rucpy_W+f-^Z> zMSMHg!d7y{g!7W@8i$TgOO~3qDnbGS(f|YC2{eK?-TzJ+RyNilivtTVDCcQ&@sZWV zXbKPQv^E?FBp1|c`K6AVrd|DS339Nunge-XuybaIxT^!EBes!C8vYjs`@gj4|4{&j z&c!hx!^+LQaN344#8(p45i4#8^L}dJ$D=rv|C5KIgTf0Q_L&5qR5+q<*30-diwMu{ z21)kErYgh@hWsgimB|pZl1S4*wIk0p&Z;4<$&b->VMK4ude5lWE!|M@)xQeefvZz-r9JplOq@;~aronOXf2n7q}m^wDoORnG;%DcdrBs%6&4J>Sz$$ZMMk^~eV zZL08X4t3%!gHQsC82+Z0WhDQs=Lu8spJ;RbC?Cgr4z+GvS^fOW|NTKQZBMs? zx%2@bqq3UI_P*n2tYY8VV9&-e?$c}9bzB!6$t7<`n1dxY8GvvZLF75{fvEUOPYqb= z0TAwjht?rB3%6THgm^#a>{+{Lb0YlcFcODIXdVUoPAiz2#hs+IgI1@{_bJ+z5)U!XjKe$;1uLMQ6XVCIc z0pa+)AP|w$XZG<7vbOU^vrlf!GX0DuR*AD6fm2h*`$`)0_Io+!x~!bQeg~@qI~*8u z%E07cg{J*jz??!Nx!d*T+W7L-u0!Gh$^I$Sd~+@2eL?OLCjOO-L0xmz+X;c){%ndG z2s#ga{B&gP+8Zgt&>iW6d9fknwLkJ-3p^PKHKW-s%(jl{8rq2-tXoARL;HwJQqdg5 zIKG{nt)d?tenJg;WqWKf|t-8|- zo!%ny0Hf~uJ)Y@tEgH@mon8Gc;&XJHpv}2~MDIVL&i>y8<%`s8+1OXrzqR#GK@0Zs zkvY-?To>K-FVsp>DF!sPMTdGryBcze=iy*h02(R>!(5;7lRm7VEtju@8bLl?Lxw2a zuc~Ia;+;tUoaF zvuO>8z$m2S@tbawlusFl66G85{t2#VOY(<;x}RQU#W=ZI*+493rO7xFwas&O?*)Wa zxV1X{FglE{#?0cpe*7)LbKe3_by&y*I)zqO7K-D^0=VhhLPdR6Ui6B8MQoWkdk}OHKW14@} z8)vesKa6P+il3-xYmd3Z`<#pRlp`&hbr@MPkaGzR$831iZ%#pd z_PH#}W{Y9fA4L9YT;Td##Rvlm z#5OQ}C$)(vfgK3=!3ja+IA@GGc~&94UM6Fti#mKcX4>_AvYHHs8z>C7thR-EBJ7yi z{2bh+o%Pu(h24KxzBlJ8QWLx@@U{&C=}L7f#VHujXJvll4u2|ORxOMHnP2cj#65mV zWf_)rR;tHL>W)Af_x0#-_(G$}{R$Q`>Eb0tz@;AQpHk9WWZ0=sidg*W2WA|vT7~-C zpFL`2MFrI%T}1kvU$<+z|+J3mld`*{*U#Gcr7uVb>)KQ}u?kectT05pb9P z{RdEet#Y=qx){t8ZwrRZ65`I}TgdV32m#OCXd~KN7)uc13*(AAEG9O?R$AcAT+2Iq%r2GDMb)(V(&sRHd%~M)ijr2Z zlB|qEq;&%V`+nDXqN-X{>qaz6K%JEvDFzz~$x1~HS6Q-u00`rSV!4go-+$myaaI?d zs z5>K*({||kQ;8cVMK!KYa<95|#WrlkU=NHaQtRw*Z7s3sL_pZR7`4sf(}+BTBZYIZfD6eJgq0FkO8<EQ1B-boF7n3A_5$#&!j_AvdoyRXMDSy~;=`(v4*3X;a!47ChHNpi3hT zodR7di^Xf_)ECyI>F|O-@GQgzls`+%VVvP8J9CG9PaJ7<_r|_D*DXh9Rrp=1s<68# z{Ki`Hae=FyVh?3T3iN7@8@tg!vqFod5u zHj$h@hj0Nd`4Fd=nDz_!D8QU$$MCKD1vlEZCKW&XWSdY>D;%lTNG;s9{^X3ssCUu zw{Yj0*S&9r=2l?qF0D!!HFIQu@!Yc1Qq(o){po^E@6%Q%?8E=qH7Hu0YOH7nf_yt_ zw9=hoA$M|T`&;h_)xRv)D`H!8(isI;l3i0xP(m=jQZ(a={-*Uy0b@qA_(gp_;yY!f zCl47N>T&oSK{kZ@4PutYmxPAwe>49}rxD8TDR=XizAH=55Eap=p**?0p7K$ze*iq! zvKjJBkY0A+LPNhfDMhB!G_?+EgFiA(So7t^jq>$?+)gs9L=+vEL~cQ9=SyjSBKgjm zg@j`H8Et4aXthe4o6b5c|SPdRSx|y3i8!G^#6aCPY-36j#A!K}#J+>9M>m0m zNvHibHfcb-G=TxIsvB6gGemIi!yIG2mdj%@ik+fbpl-)RFNa+ca<`_L)1#Img=RB{ zGnI?&3Kaq~FX)_ay|{f7_YKBp=j--LZbQo}H0eEL{HCY{YRekV=U6_l-=le?jSxKu&N8YnIW4n2V(k^trxO&HQZ$kW$XLu z#Efw}d_NuBx+nuf^!WG~s%kPEVmRuCC|)Lk`OWr*9^YdAhx2a9IJ(HpPHo&3?xe?v zT3)(P%r{wVvD@(t{3*X8T=D3eRTm7a%Ilw~ZHwA}5vQc!KGwRY6PDVdLk!RYZtlXI9%Xb5V?KD5^1W557h$DmjlCQE3_o=D`ix46%~lsyI2eJUEyKz}JOI zyS+P{I#%=7j4N~pVW!E4G{s~9OBkXo;Whn=eU&N)3=`HB>Z&;Tq;=X;3Q1k)=9VNT zWcij=%tvXwO_37h=wkTEg6!$?fkk(RT}`vy47huR0jxE-su}{eeNtLh^Al@(Rh0E8 zHGM}1i)6*8qs%1cRCkStmxvmA|b{_H}?N8NTZ- zDn7Ew^pof2Tc9yM;a&q)k;BKi4;@~C^LSe-n{Bj|kx~KcX5o#EAx(C$3IQTb8?tzW zH%#38PU_rIk6p#@^k(K2i>e$J1z0<8ECQ8sYbfu$fE72B6}kF}i+Q8>%b&kQFfkNS zOjFdW=OZDE0#0^V<+S9>u0f}vxPJibm+WNDQzY>@e*573eJ@$*Bj6$x8Rg`wP6@Eg zG4A!>-WmAgT#}bxvRYIpQ@)?Hh~UQ-H^(S09yBU_)n`y@JgaW*ZX{w;3KsVA72Mvd z>+c`#wzi#BOK7xqk&y#)%LL21cTlt}6cQAgkbCuGgEz+-Y)0k(F2y5 z2duex1sUbKm2g?01n#;@7I0O4j1)EbaPq#ABSzg*CyjxTB95gsp)(V-|Wmqt{2t%QtQ#_8qpq= z&5mhpz@#wp(7PrJs;ueTfHi^l(wC{SjrJc@eXynM*cJ{!Vpa;Jmfk-0fM>Ha2WqJE zsmIhMAJlr6dHawv^;-<-9z$neQ#Z3wI*d$wo2AO(E$1Muh*+O7pJv6Q?lCyO4KE8@p6*Q6$m%NhS zt4m9%f;dK}P_|x5KDQx!03=8Y2CN{XMTTieAw`NT&*|yTqc-=aH7%)<3LNV1E1W5v z9c_|y8GmzgN;$p(fW-V4h?9ri92O^or46H08Y|{9OT(Kk)9RNvNris?vx4cZrAWJB z_+A;0%UVTU4dLPIfP#5$P3Gdl;*vx9{d$y;li!#-mvDa9Q?tmO3Y9|aMP+}=N% zfjKNTU({Pgn(ysu>&~k;kK<{tW^OGF2nKE;BxvfRB}fU>a|!bs%0Mq2J%gY^nIK4{ zSjhrps)6Cs@;yEgIno7O%AR&yQ+|mtr4~v%CM86|kQ<74 zPT;8&+XedyjQ$@_?bjFSb@_LFUcAiQ+n0-ofO?p|~)OYlZ-c7(iq=O(6k zS0)zG*CbuE^Y^rxlukIn4?Tz+Uu?5U9VA{+}A>}Lj3BUF z^V;e%-kNAQn0o`we)882BEE6N2eK{I?@z@<3HNJx_kiHVg)rV#9ex?U-VSq9=le91e;{P%c_z9ZDEnBif}7BkovIz$&2Mj;A%9v^pFJ<0~F3qTiuo zW*Rnsj%s`o`q(!7Eu|lP?ah%Hr~8+=Z_7%v@){ci^C&qJyLrbn8$roZ=X=PvvO)co zv&nyfrG@{hrtnxFO8O6c_J4tSiv6ngtN!@szwqV%1MGjA?A#++NOxJohX41Q4#C3E zf8e;@%qI;M>(1B@&7$pp+@2lIz-+H6Ck-lL*2Ihy{2q_AkUV{jtKW+Nq9f8HSVOSK;XoxX z@l-BX2Vl;S3vI?PM~0=`zlESME1YB!jPYj5^sFE^J>1~Bh<_3yR0PF+IgOdOUuo71 z3a9;!_XiY~L(mq%+B0nM-LE4ORGLc4-)KVLe5YV+y6{7JQ&*b8s8P!B5qzJ1D!cc1 z)hGJ!eiEBE*~1y-A^AS;Tx5N-Pi`@ zalmyL8zs3$V$3lv%SCmiQR9LwUbB%Eq@l%I)t^{7&rU5M_EmSOyjuZ102CxvIk|oD zX2;zr-f531%Npdtz1w4dr@6cZU2;}zf6>-XlgJtx%}a?(unaoDBEo~W4SSPgbM z7I}7S(Jm07qFT-lszc{cCtR6-Z4y;oI8b_9xPq=d%i1>s53i7pfS3Td;5Pe_tsHuHtqT-6m5}0ai?hU;$EP*7xyB;11Z716nA$o zuE9fqLV@5e#a)VPp=jxQ(pSIz?Y+-g=ePG+>-_U@!#p$VNkV4sxi1+@R0@1zydOr2 z+?2SfhTbG^=9CRuUJ2|NQ15Kbk<@pO)iON7sb5UKfeO*~eV6&J^M*ejPJPK8B+OVm zGK)lt*vu!*!N4l5*}yaWWPceP9Z7wxwZ+7r^e!(=svW>x!5!*oW}Mp z!=jF3Gu0@9XGHtV?Sy!=89al$rN{Z{k(?E}Srm$O!ah3> zcTHFHNTvtzz|JdPLZ3po;TiH_I8NO{u5Y_{)zo9!T>Y0AATSJRACMB#+1j-TWg4YG zboY%y_2(^W0t1Xf$ci#wkg}xjQJyVe#{y&PP)w_FoS>5U{zS@z)7UmxA&|0eH%LE; zsK!Rsb$6dh50YZjahfHsR4u-lhTYqEWtGMBmqG_0UA;$^shB$~9V?U_foJEOrbU?w z2kz}K8I2fgzR#L^QEz#64-SGH^@I?d|Ih(3)k%-L$*J*GH=0eNaK*ECccowIb0c{a z1!er060HSTvriY#c|22Y=XE_^i7cQV{eq(p_PYe4jtd zLga+rZS{|R+}j|hs*0cdKTu}hxK7dR+1FHvo0|c}{tj(_PuGQ5XPeiDT7SP{2Ys~Z z1*yUG<0#~cg(>OE#jfmSUvOO%(YrQ!jIFtVrYUCL$P-JX6Om$0 znp^#Ney#}}Od{ev-F?9CA0FHWXF-~yBgtf z!^JLmN^Ddtb5j*fDAiG6fyNVxF3KOsjH*%C9+kKP9#?c{X|t9%^ci3WH1)H!^v#N& z=T5Ic}mqJ{m{FXHd! z&xiu{U;UW>YW{DB#fq5N&?eN>{)}DdWL3WmpirPts9h}FKjZ`11ygiqt5;m7zLRvI z@X7iB!iM#R8z=`j(FRDZv&G1*-W5D-lQtYSTGgjt=dk5+I1TBx@zz=jzZ>jM{Ibri zM{cvBeLY@7c%ER!<~h>G}6nFi;!vlbNdxY=<_OL zN9&weQ33E$>@uZ1GKG^T$avBmTV`Rb>D%VUrm!QL)s6tZ#cV>)Ymj66cO(Dn5OkjX zTBZ&;#Pfsl(-U`IW3SOKaE{rmwu#jSb^kca;8_I=S19tWLIW9DJkdMHwdN*d2-3+M%eqhhf?XL<^PHarA;=jMCdfUECcMJRR z=2OHCssCHR=QfEMi>pVCt}@>>|7bvpwc`XrLj+MD%_9M@JnQ!sg%mGHsBI_HTyh;QjdwFv5wL1o=%g13aYqf7dkqi@Dc& z0D?B_NrDXkm;uSjC)kmQk&QO9wl`*s`j$?e{kMFKOtlY=haFS-L%u72UgvVu-GGU@ z?+5JlV>34ev>@+O;SC2>cZhelT$=maV6s+LCt!FwBkf=Xe|5jZ zj0_fMJ$L=fx?@~mNVpS>EwR?qwWDR`yvKIkzsab4RLsI;q53Ju?C6I!bm)>%m=#VHx88xmgCYqhR5?YmT?Nvqxm zg2wV*h@DXKuFjJDTW7Xs?=4Qw4aRlu^ORn43s0)hYNkR}579yM=sQGqg7^0#hIW|q zR6x@lNn1G%ErBpIX9<^CH$s_(&-sLtJv)w&z24D~zUiK(^d+HGxAaDt?r#{gnU%@} zsHlz@-1}^qHNWq6Y^KS6Lz3<_`IvJtw5tdVK$m*0a~XC=UU?9wuTMVC_jmYv7J&Pt zsrrTo68Y;IM7w8jZmf0n;p)*C@zaTh%|Ev-h|d>G%vdLr44rxhT9z70f`@b%auw9& z|EPeaFaEJXn{X^ntQ3<9(1dx17<;T{R&YE-VV}U<|EcC;d9N4)(mu|&5wOi*V!!Q~ zEuDk*`91^0dbfp#=}T6cr6QHzV2AtXs%0iLHQ|;X!IW2f0ROhAyNR-5BhcIb_r3r`2HIR;{%ljSCe z-lV({8U>&~9AM$VG(|Dw$u(!KW&~1&MWBzdKcAG@E1J^4aiLb}QQNgVoX6W;wyBW4 zGKAB;#$Mg4-I?e@qfC}ppf@uo;9guO#%(hacT8KyU~{e@*wAPhOSn_I3eRQm%Uzd( z%`lBG_~83CGy@W`H}~B_WkWjmisLFs2I);G6O|U!YHfjw$WHE(yY+K3W7iDJ>Qai~ z+DbQIeFOF}PFwTCY%CN^y6bT*+d8NMPg_C%*%KydBZl}*)e4+Ajg25?p{&E!zQrZk zD!X9w3L@_9!WG5!@#FR~jK8MemAG9XY=`aBl)A-k$;4H=N2cNtA`cM#D^|;r zUi`jl$6Tkdf@fhUU*fbrR!YSM$#cV%KnFBce+%`|X2>Dfv~{eFEJ+iu!cEn!3ux(T zb_pNA+R^Q=-x$TU!zID6W%FKBV~HrS>E{I69jac-o>PQvnp?1`2J@ zl~g)PV}hOPXtAgpY*_Q^yPP5#Gn%xYpQ%<|=X|HUoRr0qt862wO>;NQw|oa5Og`LP zsge4+c1?_%oRUVYad*=Uobd8^=RHj z`OnabyTVyw^oG&D!{7fIg9598{@9fTzpnWMBE-IWgb(?C0SzHRm+UtXmV3#2WE)J( z@W62r4pP?>KjAWLQfrvbZeZW~r)d8|8z21SMq1|JD=OVKv(JTE;wbtYvq z>(EPi>cQk9wWJmyJzJDV`y$FPrJ>C9iBHa&I8^^Dlf03USOaui2{ZJIS;80YqpnKw z-It96n}+L4h6`a4i)tt`u3s!W zHjf$Xtr}O>1|Uai3XH&>FrYd%pBBzP-1TB2lu#(h@D^HhG(1pFTX{vhf&@ zk!%**v?};MEWn|qNmrC+dHWMz9U_x=v^#XH&biq9sVfD*o)ZB|~k4$>IVrc}oV;!bd+L*d}wX1bC!0U_#67Bku<{h)?w z)Py-VwD12XN&m=2rOV1aUF zwAfckVE-$nCO+8CL6;61(?>60wB`5eI*}#MPT~i)GXqrVgIaqR0V;G74W_*fH3Kq6 z$P5|Qp(XklcKG^pU3f;wlRHutCl9&nx_Y2#bvQA}`EEut%V&H&9`z!_FsGs-U`VWW zS6^q=_XX?$Sc|}VYq{uU@UuI2O^MZ|(|5$=vmmJV+`yr?=pNIHvh{-SepS+#;N_?! zmkm9v%EvS0v!8KbbC&;Bg+?pCZ1}eLxt(U-ehKUE6#5I%$D>CT<-c<0Kg_b8JeMMy z+`SjuGOY;u0VfI5gSGyLxw^Qu`l;m4f2cnn63m=J3x#%r5;`Bj_m~nHGN#lu>%a? z!=%8^nL4pgJKdLTyl>eX;)@uY=GDR<{ICu#D?*#6gD2-kzkM9cC&V@@n})bYUm(8Y(H%Hle5WQ?1vb}Clzn(=Kv&xUao(H$-ub95o-07-6r>WDjPBcpt z;}HYky2N&}>c899K@=+=7-hdM9c!fPzBBxm;kK&tXQvk<9%wy>`5)>wjml!p={FP4 zpmGN={Z=+nJB%pyStXp_Ik5v>sXZu}PN>!lPxX;#)l!jzWxdk;QrV>0{$o)oF@y3R zU7Z^UO_+F04Wy*lrHh?FX_f>TIwyQgxyJ>HqMD)<+s7e7P4|=KtK~6Sg7`#h@zsrz z)pg&(sM*t0P>`$P<)tz6Nd?bTEFH9ua4pRO*s(Lm)|o+5Z!Dphh0QUhQ%$dwxLUJ@ zYW5BD7)e&7=pscuXCBzJwhQjdKiU6GDgUP7_l4%w9p`UIh9U;{V}(Cn*F6@+@Y6al zcqbcgG%2c-xl01G&*1+k3f)pvPQ&aM z3mf{&UkK}aiI0nT#Re-|5`Q*r6yc8@8H(3QhQ&wR-2otjd4klgVehA5 z7y$F-^9vNakNA87MtJUNlvfEp*EdIT)o_)ME9wKY!&TSqBev~h9w@fVyuS4BvPpE_DJ;?cu`3t4|#!wE!+11$y<|jLT{b#tJxNz3pyoa69b< zYT~Zvuv^I&s?dCf4iPey8IB1Z-P0|+>r`t8kNqb?q?E56W z8cwE8r2LW$PQ5Fr<$QQgvAs$eLA+%4MyD(?qM#&ZepKOkT?D)F<}j7YI7RkG8tG^9 z#`2=h)wkX*lpNUR=HUT5_WadkXN#k?qK`#EHB=FkO&4Tmc3DKy`rmdMg~wsIf+*n$ zwYmI2irPWz>+DD7Es|Ik39Z%8Hig;+e^}O;ek>mmpg6y{I1n7IUe2!uOVgugf;5Cm z>f;UTpBsUct28FG$8POh0~JlW<30ORIT~RM-a4{-2~h+Y&SeF9JrbH zIKL#LK}r$ZxtXb@dISqUEg(LSs#TB=r-ZQSTfksXBLA2ZaLLKh%(K5#v?~2Sl^QtI zi}O-v9`=61b>|K9lqEPS?vFU#WR&RBt6ho$sK_ZJn zZ1ki~PL?CU0MSa9y0_)-_=VMAV66j#kfl^4MaXn<&%yqjBXd%kY4F4h^jK-fuoAay zA}M?d6MUrcEhD3BT1}F3mKcMRzdbX5qw`GDh@F(0$~~*;D3m>X1C_-_>8ddas|xL& zwouRTt|4nocXDESGU8~9Osf~w2DgGhcqF{%!9&oNyky&xIL(nW>>%!XhYjf4|B!m% z#nWea=e#4NA1E6x_Jq@z89f+h4imoM8`=mC87?ms?4$hj?y;CmPoJ>o?*7XeqdDoH z*}YxhU z!7cgrQTj^}jvAuF*^^ihf$#7T`PmI2EA8kYkqbzJ25>A zSy33_>${x*?Qy>okZ`lg!C{;%cMa5C_pEj^(YB{qop{d@qXvGIy|ycLnnY=&U?(g;^8n zsSWNcoVaz*_?PJehoLlu+Jvq< zyscxq3b&>ViFmuhC!F90hsg|KB%*P}m%kxpc%-~SX)G^SyLdphZy75mU`4*vbPv}1r(LqtbO*PU_Vy*qD)era4mhA%@Db@MoK$hmjfEbDWtH=7Xl0uqxa@8Oh*gjcH;B;C8bM+bwkPjL za;JD!ND;UhT@ik{sVCWYBr?|_Z4I@6+ljh!&`he!f$gh>`)uVj_88nZ9QAOGZ=^LWrZFLQAMbWF4S z{rgJtQI8X&d92m$>(7j-4r5*IUt{dDz(G`8iZ57zG*!nFYJD(1BR1m@ldI|bFWD-* zTl69V(KcW6n7AfxY&)lDuIbMS^kAP^16E9oYZ}5V7P`z0U#!rPHs7y3`B+@MymA`f z*Vksmh83s^t(s|j1p3V)L((ICsa(3HLJ}$C;!9p z1^a`nrPQGViD>23cNf}qCQHRLMzaEg8ZB)ufL?eGz|4y?BYhWl_?~BK6cqT?jyvls zs42>`-%-Oc)o`?Gn!P5#T>3X8IL)dNk7#JrG1Tr5@qInD5%S87u9|ZPP)U>kx3+lG zTxH}d{-XH)r##@F23D5_*NCGQ@=oZMEldX=og{cqp`a{3$#<&1ou2(^K_TI{VfTaY zr?`S!O%1|TQN3 zGT-r~smwT2b_9g_uA0V3283d?=5wjhSUSvZ_dYp&ESMDIjtfrEVSh7V75muU(pS~} zeYzS%Pk?w((X|x!_=!5*n#E}4ZnJFtQ&}7~+CW@pjr9Lool)Jhi-c^3-lCuUr*h-f z_xn5*KD}=?j~^0gYG+;)T>cX`zR~4>PyQ46koERYrf&Xzk;KG|skV1_=d9P?;l%IX zNhh7^cU>v2zplw-82_NQwf-S_M7bs9!A2jPff5+|)~c`J{n-oHIcxRX zj))U4^1c;YjP2dVSnj4bq%+1rhLk)ue9=ugKs?p1;V*%pK{>}h(Qu9Qgn+m$(aYS^AGE{OfsSfavDbQ(&i{X4MK#yl5Kl-ox&p@ z_q9ofGdx-z?fN=AMe=NRCQPg!%ee0_ws($!LSWg;rXk3lkr{D+lCkr}3%yOZthqqe%x}8`vxR^4r5lEUdzyln(|)cEFrDifzxx z?5uUuUo3X-7~^ONckgd^8CiowQg z^&4D-9v(W1hS>`iKD!EQd(+*0>|*ZZP>HYX{3TnOlj|M8@jU%zYjjD@Rbkz;<6v&1i;2TNLbHA1NI4|*|QU6<>Cxg(Bhx2;b0lz zU3Ygk-S3gHX?59Ywk-8XTiu>t#l0(z>;axFwyOI_`O4fh%QXueKH}tEv8wO!EA%LNi66R7+O&^xbLr>+kwiAR8) zI({05SSSWN|HQwS!lb%%n?9rKO#ZsQv82L!#TMy5@v76HyX)@nB7HETVreXf!54-k zH4xZhEM!a@WF2F>G#vC&g{rq?>`@{wpvPH9Bz5vMzc!CIZocUx)Rm!ZvM?(B915Ii zs*~m57ucfDWaI%QW#!DOe1B(#3ELRvD(I|RG+fj?@>bj@J+>o1&v>-pwEEz7;@n~M z=<%y!SD!sf=8>%{X3a(MOJaf@@I6tU0#FvaRuJFEv|~ksGGyGpjLW4-0W1QCG(jx| z^o`28>Y(VcC&hLPAqUvW;es-IKW_J*6e@1D6E8a^6R14e?1gu&14Lv zSzE2x^TOyUIf=x&Jmfy6nGOjyWy1+9eR*R6Sk(MAl+A>x6D(3qk(<*%DfjV0mNw=D z#};V(*tTVJV^P}>OcshrBQ+iMNBW($%8yC!XwWWf+oN(z8v%R1B%=ja9Pf!E^ay@V z7K(mkNR#lZD~(6OB;)i>u4Z~rl3&2E(qDCKBNngz{Pgr8*P{mb8xjI;Z~F~Nx7#|} z_ON&1!v-#;MI!HyG*GeY`#+wWO%)k#U;7EeXHIgSiFc4`hmw+<^6*2dGJ}z>3t3uW zu~$3YY_!Vt^VXEf-;nN^^E=_trx`w6y+u~f4qeeUm$T0Xu-GH{=Gcm z|80-J|1Ub!I2~~Y;Q;%nq?Hw@r91P$(YDeGFJ^FHIaO-Hg8h6C+81heL{mH}? zvX1tzlKLOmXb4_@^xnloF1Th!>Zf@WymHy(UlU}_72UA>_kUxXMsW(Pbx;5-$nFw7 z5sEJw#A<@iq~Sl1t5foe<%y19s;C}MjUlQ3(B(Le-t$k;-JD6$&NMYrR_BBGI`Jni z3M3`T=5*F5t9mP?6+0fw8GwR1N>nLJ+(5)5OwPN9n%u1)bHH`YoNG`1oO7jZi>Y)8 z0;Loq^oGNAUcy>G9{H_!JQG;n=Fv=2Ss8ptvv|*U?vZFCwjCZG{klOE{W_JcG;3Q{ zzTN2sIH>B_dSKLX{CWCmRYnr8MtkabqF)s0v@gxGe)A%0{1|O>wU9B_*qFHMPDvyR z{5sh}SMywG9#F9aoOjVq@v)?F%k)@;Lp`LwMf9OSC`$F);k)4GJA43u$PSSlDIChJv! zB7%65ZqQ{%UC@`~swph!15R1r_zg2->%7QhS88urXt*5`!sUK|m%=|e&(Bc-Se`3Q zpA+W>;7xQjB=P1qiC|tArnb3wNNr8Dzgr62q zz>haHK_B1KCLj{MB1k(=>eJYXaSFKwy2epzE|Z6ua7}YwP3eGfToxl-{4n%BJ9x}F zhJBWVo*m;AEdhe$plg60IklCb8a`Y^RFJPo?@{FlFr4k@~B*?Hcbh&B*J3?NZ_(kfkFa6QA&ffED(xLWng{R&+*1zwQGys z0;{~v(0ydj?3wo1w2eH9+ps#z{Muuzvb<;UCdsbnPj9D}FZheRbsAwwPK%yf)&$y} zYP*EoC(;JXk{CB$XoDI@Jz(ZnwiL>mz%?5sspx*CRiw%4thRgiEG$$4l{CR}3|3%$% z+W&v205USfq`e%fCY-zSI_<^n-;eztvCD_ZDTZvR`-x^np`UonBDbduZbOHid~{`k z^ZRVz+;e@S#s&q-2WjZ@$bdP+C3maAE;n%KMtU`0!pY6YOeWZ~Ijl5XJjLITMzfV% zj1rays|QXjsRB7$mt%KSLGmuNM^TVw`j#X_(|{=5;jh-&Z4VyYf!0H3(G30;s*#|2 z&gN~g0Yy|SLaGEO)vza9|F`%4_XArE2yUI_fBlCc1MU82H9@mQ5M?UfAeNCoAjH?< zZ{+?Xaq)!nU#jUM=RBRN@OPeAW~FRP&BSC@ifrX$7p=PjR)&y;|{?1wQI z!n@7gr6o5c4_XI`(A$;GHD%Di!Zc-eL+v{+3q$|T8{3+=%gk{P6Azr}=czIdKY<*G zeEv-Ty^JDDVQBEoI>I8deX$ylEhrZ1XS-o=;KS1&({JwZ;GaEtM?hf^Yiwtu>bJjk zbT=ku;%<5L6KK|KF(-nE33)*M z*DZt_)^KLlD{d~Y&!dvi#K(NX*RHPPi3vSlQXc^rOHP>`yGGS{x1L>R{rDm#TPQ7+ zNbsghK{1%X3%&f%d27BY&8n0r!(Mlt1NIxzEmf(_PMgFjM^{7_7MaY=W=stcx520_ z1}`Ab3y7>0@)fZ|Zle8JZ`_V$xA_@G{$54G&Z^W4>b$iM%L1OPa1X;J^n=sMINvzD z)`fh{WscA^Pu}!>T_EDA9K^pZKqC6+LS4Sf`r`wB76+Ad=8-@q#?&B18-~!~A<8g# zr1?|^n^grM$C_Vi=O*KxDG*D75OvCSUoa)D^EbIRk>*gb*H6PqBMYSJc`#?%udMv7 zM$5~&Jxhd@`z~|a54nr+hrD(-X7-Mer>i8C4fpK$d$nK6vM-J{C^{WBd3L&W4_1O% zmB?7ODsvJxuq#)ICJFfEHjGTM`s^lrn*NO27J&eo!z zl`fP-tR+y9A0^xlql0~G3_1d2 z1(8C#r6KK(IqA3!RVT(_tO=d2K%CC6i7Fxl6h#32P1O^MI@HB~t9B9U3XP0Me%QpY zA7^pU$YmBf2wHFdfyuG{PUSF0`l6`B%K>NIr%x^ZI;Qh1m(eYJ78(-`)VC^&Kj_6$ zjT--k5`4z0HBL;Th$e=g!B88CN$%noMuUW6CrW7Pknx?DI|)T!w&D;2ptjPM?Xj`3 zeUy_Eh(gADU;|`EuZ_}~#qG;!^AI|P!v_~cMbTz2PYMCKaO$UIk4PmMXWlW>$xS_; z7l7?JFEZs^I=Z9-y0&O#@ladqdbS;#I&*@6u{Ip_u6&7uLe)J;Ys7RyAKJ|9)+s?^ zoM+h3z|cekW+GSs#b%E@o}dGc7)nfk$SQK`0-p?;Rm5awS>~QGeMR^ooTH5dOf8TT z4E7{CplyOzgjlgz&9M6oTSjN4ictu6Jc2|<+uREXTtydF#=Tv;AT#q?G}^3;*8TVe z<~!BLU;Jn?Rxrsr))~)0$=h6yK6~y}WXwvC?6sYokC3!yhu(~?XNZ-UjcaNa*$RVu zC4I~(sI9s#Uj%OBX|2>&5tteDv^3(h++|)+XaN-h*zp?K#%y8QPHN!Megl7dS4d&c zY8nCa*)AxN=ZOgu*T@A4P`Dop5M#0j(J-6>EqZez5^M$-;-Lv);=11#vSIGoqalHZ zm2!8^Mz}9Wo;ULhSjA@|%%yJ^0zKtGOmL3oz`%&hZ}wkw<%8wa4q3|MGn5# z@v~y3IqC_8Xv=(9fhKySASbQUwEKZ3h=UIsHFjuMw}QUrR7438QF8jQ^wUpLt)Id} z0s1-q13{}O&%0G(J{~)~8&ZcbM^*epN3)4W1U3> zN~?ddSD6z^_Sj7^^Enn-%QgzH3*;zv*tx0&PqMz0!qT;_K2~@TAZV~1&nvD`<>r3e zp=)9XJTKMe{_g2QSB%=U5j*ED6Jv&MW7hmWLA}CRmC(_pC?Oi?SY<5PbY+72xTkJq zv|bQ1N@uLrE(?Nm#NhOP>4SS>gF^PW51geCEUI#MSD0*U`g6!FN&a~Q^&EvM-IF6@ zv>JH46b6o($bkpcp0qH{rGa|E@bEMHzyNZy#eRq&r5StAc3YD{#m!>3D6Q%dm(Md? zNqrRPf$J!#mz|Gvz%1S7I`}&6`4J}vDhWm?Skb7-Y zJ5MiCUa6lPy~8M8+7i8cvwtp)E)^IA#q;Db2I3%&*m^tgys*5lxjj#s_}H;PQug*Y zq>HJR$U-vVVOat57el!E1zfNRodp zCaUzc$KLYe)Dv4f)92iBCRs+EqC~rEo&DcVe6mfOr9cLvzIF-`CQtXa zNUV_|01a8Hg6awo4kiQ=*->yZTNbf6>S62%7~s)t@Wn!SMKR2>4KZ`Z&{)9EP7g{3 zH^02=;I$zSm>K{Z&T3(!5Cp~?gL%^0>WK#&Se40a{TSInW5Uc4HFonGzFph;SD9va z=bEndmDt_Ol*b+_vkk7q#3=MWtfmWE5&c%eMPHnj7mvQz z*B!~T4hNqbZIF%C;ymlJCyQFc?{&(S!x>#YCoatKtva+nWA6*ZEU)v1m2Wv{kXrZ` zqGeSXpQ*x*FnD5KMc;_I`xXGUg!5MQ*mF5nO?|EV&aS`EvoQuC^MU)dh=9+zp>jRO z6Zd$OnC-nsx{~9+gJ^HC;dTdn?us6))-!`xD;-R>j5UIX*$V4=X{zlR=H24lLpLKG zQfpuvBL2YIDOo~(B&fB*t|A#g(ym32%@q47L9Mvvy3Nf(-nDLTReo`a75kUV2{^A! zI7UzBw8c6mY@IeGHSN7=R#-s)YLB6ak5*n)(V7mnQ-{35io&AtCU5Q83u3#H$4m*+ zW{wpeDy$Z~uU7`|YHS7E-D~MNpo{gAJSKuKdivht2xcPkLnJb>Pa-{5#vkzS3?AH0{!NX;F7In-*?KKV%o311w_jyV1Z zXKaj!qx8veG|qyyr;aE@f*LTSM8PPEJ& z+7Jqz;&?b=?B$O%1R9Two4WMO99@2AR!ZUp+6d=H`H^?=c@rp2V< zRw~YZC@j>e3mQ|)jSSM#U>X$bSgB#ihe?G`4@ct-3Mz$f9)>hYt3kZt4=HaRZy7qx zhcCSaZ|-0E0TnW*v*rC=zAlHA_PZ+i?!rfKaoF4PIR!-SI#bsodanBeeXzRE*sTLu z`!>LstJm=h2I)>5( ze2V>p?SEH89b!$DZfYqZr7C4K~oc5&#@i$Vd6GqKF< z(3YZ`w=TktEj@7Wb3N*<(7J`!omdsQE=^&`Vid;iU!f;~v54HSY5iT4;fud=Lgr4M z#V6#7mf5;gsVAR5tu%IH)y>?vRZEg7}()okQsnXKi{vi+(XxZ6@WFpCXRs`WIs+s|ON^YAUve zU5%g5^@IO}cYg^f_TR=zQQmO`*IQ#Wd%t#Y_a)UU#VKOPag}UzKsQK{J=dTFl2{q(#kvS@i zPCuk{wSIsMyuIgTW}NlKjYuhBTGtdI7XDc8+R0}rZ*1hV_8HpcUK5+#fIXMrnT_NH z+BdM~g;2!}T~(xj-((wl3=*R*WWBWW8II0BZU|UB4ucOwFqK}H2o`A#EV^?idNu8;H3&*%xI!H}s}F?|lW3TxQ0+`ZFCf?5`eFoBjY zAo;)_N}U?gI6y#<2FCk#^0`_u{YJi=)S)elwt~<=`z~1_WYn?eMgGgXudQxm8nj9t_tG`W4D;oS|nl|F(HLwUS!O9$7B#`7Iv@t2v^env0#~vg28nty> zt^_6{q9{`w9xot zv{-V8-Mo4 z)p5W$_nFcFA&Btp-v02$GWxbOV3UMcQ{g|5{r({r{f`x7H2)+_V^h%pnU7N{YOKpN zB&p>oPaW#zX>nX?GRTe$`_Fi2?N&vdG430}zE>H|k&ENEzB8IC-SC0CxnHkf@i%d$ z+-f-(3d5-!al}D}DG@jIpfDXmA`uovP6Bleb-7)|a>|`k4CB?^j(Xt^v|l9j$acmqGs!I zBjUQr)Wa?h5-W;BPM3Q{zbmVToYbqBtQ|Fe6F9|JrHsWU@8P3FQ1^z(r+u|3eED(| z6_MhFmpX1jW@l=fAgiJkt8InYuo%uIHM^}XvTri3uae|*iUhh}-rafU^gj50!w}SC zi!ufE*3$mWu6W4gYNG3ElzukV2Oyb>upOgm5#2n?arCXOYlhBbz8M>e)~qNH2jhOF zG#|IKTE-e|I{p?Isby)dMj~BXGG_XsKk*^&chu#hr7!oMiA;)$Z}M?b!W4Jtn2V$! zO6rh#9Oxzu0{WGF;n(;H#Eq(}*HAN3ayeKi7#lK->+ba6`&dKv(nl-|yD$DX8A$j7)zLtWS0~b(&sEz~oHFHt)Ocl9 z+L<3kl2eouq-Obcc0+HGC7Ab7=Zmwe`Bo;Mt*~9OovjUqav#=SHiMDZDO1Hvj`}^r zgvEWXq2A##=}a4H`*7;(PidIn+c-2DxOO}1T__l+e8`W(xcwrF6AV6DV*as${qt3N zB1&mTXw*h-d}jl7LZ2e{-q--uo|O~Q?Qw*N5qy!=JBtHyP}DdfZ=hHWY;5YWvf;R& zmkBv!ynJ+L@m@Ha{j}M}d%5CIO%T431(8ExiEI6%tt7*~D|PSypL1g1taJzWKsv7# zgVo9S#ahz)R;n=~V{4M(FlAWOC{=MZxTQ$w>0{zEh*O2l^mLk5LEI?51zPO0#36~A&;$wMzV=s2 z`EyOuX0rzrl_Yw_q<6WPJ9{Fb1SiE)+H$atokmSb@;V@Wk%*sf3W9skZ0MXT0d&H~ zv2b#Ca-@4Uvebp1q#4aU+xzqA*(RKC!D)bwV@t zhCaG$|8WfZcKoLV1!0yo2ge^z zHek7!{f$s)fY3H2gVndsPbG@Y?(cFn7)A2}RQj8ZxnaIVX$RYCy4$Bjvyt|?Jl>hN zXXN3zo#a>M<3QBVb!)VNTCul96weHwJULY6;1}EI-Z(LH>-TddZXM>Df~v^143s5wh~c0=@{v6P5OI4{!70>=i3to#Nv;o#tyI^l(9 zJ<@fyW270H0c~%xux+|Jb58kUmTtxn`NUxb;&){22+Tv)ukx0P(pDMR=yCG%$dPOI z9?uY{`yBSAlo6D~lgW#nuAO951DwA)!wIGq&F`q_JR5?FHwpQ%f80(3$}D9akIk(= zY1HPP=8rD4bmA`V*QpwYjC@Ud5)I@_=6mZvn?>Pl4|$p%>kSH4T_!_Iz0l*{siTE`_uIK+^03J|Hg)ffSlm3+UeFIwt++v@eRl>WmJ%E+eT8eal|Bm$gRXE#7H zmb~E1rpI3P4TLbWRE+!Ac{MSKi1TW=H`j0Qv|C?vNogrR(CDvs)sqKU+MZH`;W`VG zI9`PJn=_(1JHv&(3!y%HQ5a)oH<)f2aNc2!yvOJS=sCd2;}EBp_=>`IyNpE5A_X=v z!66{GVrajWr$BGm7E`5^xOICcugv!yr>Y0(8T3AycKJ2#v_DBg zFNTgq3rbnGrY3Kiwu6ZuujxPXJVaW%rK!CEz&q= zr-o`>(UiNP*m0L}q42Ju8A#QA{QU~W8wHKB++ZQE2Qxqsa~neAeA}VX2R>F^@WIO- zw%VVb<1e{rTK$2k%SZiAA^VvjaqHXuu1Z@yV0I`ypB(O-6SQ-UqrseT&Tz>$2_j|u$k~ZF9{Z8^A8ocuC2&z5+$1ovW8u{ z*h9(uIs#?60)Ij;Q;di2vMEb;d=#h2A|ty`F+Tl=wwGV=&0PgtB)PtpwondWUy^Kq zeX2MuZx`$|PulIGUGu(WGb|PKss}uqBVAZbD#XrUJok!WgPjT_UFu;KE7~yG2-JDWzQvU;Vm%r`j(NI&L3P==!7AE?(cb!yGXM&>*9SFx;l0LrSPe@SVcXopA#T_y-G9S=GL2k5=^LyioVXpJUr4PnTQ!98u2=wj+M$*0+ z#3w%H(2nAHXd%={E7WMgFQ6UWAVBB2ygl@>@LfLm$uF%^tZUmH zK!dhF0yefgyk!E4WroLYe5jYQ5j(G;e~FDf`)@0UZa#-^E)LozY`W}`SbhgG(r{BO zo%~FsU;P>FP12})8D(<@hH)9clR4ew?^~NITB@&zmt_zQe#Ec*%qMx5+va`^Q6SWEMkw(7 z207_^Tz(2mCnM>`9a7H2_`P36ucqil@We~Et(!uB-EODTWEg}Q>p?C|WmLffg~!_D z6f9BJ^A!@s3ne5!Bkj*pi(^~dwbLxRA{wvcQ=*exJmf#~W)rJh%#k7J{w})}?@yIa zIbj_XY;emeO^C`Cds5gI6If4}94ihxMPbA78Ri3 zmuCG&QfW`8Fu%B5!3|MKK-5A}Z??FxgP>!Ig58mLv1RfW2N2~u*f_~iiiu$I))YMk}SumSyFoX&;QESDy;FqSvt87PY)=1_pZ{! zOE@77>+M!)>a(`kwB!>+@Yqjef2bnaLm#oMF!_{srb41<%Z@D$u;@S4tlp`jhw7Jl zq4d|lUR-=Ic8ye@Y***!Ftr%8)WE7CaYC33@iOoXnp~=wD~P>?*mIvD4;^7ClpBQO zB1>PFV=@i*c$wmh8Qr7wjO`J=X_aQWW=dGFUFO|re6B_kB8#);W?O%U+bx9hNfL>Y z#Yr0Je>HFJ+N-m~L$O|^A+p!o1OC9VHdF(}q2_d@?y5}XzB|og@F5On^JcJLxJ4CS zOdA~cNeuvEm!OO6aY=D^NAYN#RMNAk)~AgE0j*MgdyQKj59J6^ry z*YUnw>1kw(&ShnhT$NRtYXc#gCbMiwH&H&b*VDs+r(OB5?zG}G+xkC47oRf_`ASbF zvTK;lMMnBcmTAmAIiAS4zJk_A+X5bla@?=JGB>rDTTeUWPvY*hTYvA`;to1;tt>=`PfVN9W!ZU$;?eL@6ikhz|hQf5x2$x7-nz

qHPSG+$`7PV_~Tb4e( z^aAFf5TZc6TH%0sFpwWu1Wzn+yFoDSr_CCcn`;pCUV zOvYf(Bh6xiSI0GZH70{h{hyt&PEjwS&b(V@19LES05?k<*>}`Vg7IbFIagK9H8Qtw zd!bf74u}3QEI-z+S?7Q>7&%Dh+?C%?-Q+l!-SgAaWRIaeRF~&nsBLwRVRzTHHpHo2A!xLFzBx;0^+Ph} zs|zb2Xn)Rqrw6wFg&H9ac;KMf$U8W&E zW9?zmF*X_QCZv{Jn`0RaJ{1oR;yYI^PdY9;gtSr&!M1wp-F-9v5iGw!V1gr+X?-z9 z{uA?s7_E~6z``@vTHi%OZ|;#?gmMJ~>JUJx7*L!s3Yu|WhlqfVJS^eDbj5z`bl(KD z)w6!5I_5DM=T(|db7xOe<*pCpP--0)p$P;1ryoqTD#0_>37*x+C~}oX4LTy%TS+O* z!@JhnPF!mBT-l+_Donx5s8{CfiTOf>LhJ-r(cvJ&Z{YB;3|0@dk*IYNP3}U^DWjny zbd<&%JTBlx*%?+fL`Q4||8(O9nIRh=norTcq5K%}+-sHQA(qSt$jt7P88C(dGV*%) zpC=SD;h2P35uMQ>Z&scSBBhLUl5^>viJ}m!y0GMZt%0C*zihY%lv$0xgQo;Ej^vpI z>0#23eN<-YQUJe?e6f#8Fw>1QuZ>H5(LfIck0vf;!qa0PR6D8nCHN01tdjt8o{ORZ zCSAmsuqLMdW~a+)r-}Fs47B4=UVtLdfr!ARWDx-WiQ8scO+9bcI5%XGMu zg(-*xV*1~0hj^I$zR zT=G}BK_2V)TloVWGXf@Mm_vGzN<-%bMnktE<2_j3DuB|vZW*3x*`b74xt(w+&AE%X z8Uk>wi?s%FQ{mFziki#<2P)826w=@0iFA?zV2@UceGmxXbbDnpZ>)kS9Ujz3Af_(^ zr~tm%2*_V?S)Y%YI8N>1c_;x(1{0NHvw!YH{CgfFih8M=QlEo&4YtWim<8<|h6>UW zx|RSKqcY$;1a;^Lt81qE8|9tJ#9)N9%%NT)<8YDrlV7>XX1X9{vY&fdTo@i5fE%BC3NboIN_-|&_ z`R!iOeR~>CFrJqo`{vz-cRNbWxjUq(iF-J+eS9Onr@-N$*wI1mLc@RkuKh3Xss8kH zF#4PwvWbiIS<0KEan#(;k}h~w*Q@rb(zj^OTH7C~GorDwc--#F zTDato(>e9>$=;*Y`oMV-{#b&m=iS&AM%c@E{%d0rQteH{dtN|9L+$tuh$2e%q7n(d zwP1b=1nO4V;52Poi5Z=W*5gGh)&nBzL^NI~L7j&_zASs177-CkC3gWKDd?wDB&DZS*;hp!mo#cuXJO++3LO%k6^{N ztCWc@sa8>ZSSK!o3A6VnwetT=Z(20SYTnoVnYZ`$3O_Ven?I7`vwuRtQ}__JT@gRF zq>2$wRij6EuC;-9b%**fx*r=e08>Cve?OP5fD;+Iwsl)=rkibRa`)lG7U6#Yedo*J z$7<8`a-V$Y%Kc`m2iU{(&)X)~?}e+WF?5r7dl4UWL^f=|(2Q8T+h)<0m3+UfTyIMU zEa!u!0agMYOK++HM$hQoW`7_=&vqHB|FIFN7(0(viDUni)G9!a8Uoq?X9q@7J(v51 z>g>5cIeSh;ecjokLB@bKS3p}o|APr54B%?4aUf+O^pCh;RWtzaA%d$l5J*g!$ blob_id.as_json[0]['id'].to_i).first # Basic fields expect(db_asset.created_at).to eq( - json_asset['created_at'].to_time + json_asset['asset']['created_at'].to_time ) expect(db_asset.created_by_id).to eq USER_ID expect(db_asset.last_modified_by_id).to eq USER_ID @@ -295,24 +292,24 @@ describe TeamImporter do # Other fields expect(db_asset.estimated_size).to eq( - json_asset['estimated_size'] + json_asset['asset']['estimated_size'] ) - expect(db_asset.file_content_type).to eq( - json_asset['file_content_type'] + expect(db_asset.blob.content_type).to eq( + json_asset['asset_blob']['content_type'] ) - expect(db_asset.file_file_size).to eq( - json_asset['file_file_size'] + expect(db_asset.blob.byte_size).to eq( + json_asset['asset_blob']['byte_size'] ) - expect(db_asset.file_updated_at).to be_within(10.seconds) + expect(db_asset.blob.created_at).to be_within(10.seconds) .of(Time.now) expect(db_asset.lock).to eq( - json_asset['lock'] + json_asset['asset']['lock'] ) expect(db_asset.lock_ttl).to eq( - json_asset['lock_ttl'] + json_asset['asset']['lock_ttl'] ) expect(db_asset.version).to eq( - json_asset['version'] + json_asset['asset']['version'] ) end @@ -385,9 +382,7 @@ describe TeamImporter do end # # User assigns to the the module - unless my_module['user_my_modules'].empty? - expect(db_module.user_my_modules.first.user_id).to eq USER_ID - end + expect(db_module.user_my_modules.first.user_id).to eq USER_ID unless my_module['user_my_modules'].empty? end end end diff --git a/spec/services/model_importers/test_experiment_data/experiment.json b/spec/services/model_importers/test_experiment_data/experiment.json index 1f85e2c00..ac60279ba 100644 --- a/spec/services/model_importers/test_experiment_data/experiment.json +++ b/spec/services/model_importers/test_experiment_data/experiment.json @@ -303,22 +303,29 @@ { "assets": [ { - "created_at": "2019-01-21T13:09:35.615Z", - "created_by_id": 1, - "estimated_size": 17799, - "file_content_type": "image/png", - "file_file_name": "100gen_line_bending.png", - "file_file_size": 16181, - "file_present": true, - "file_processing": false, - "file_updated_at": "2019-01-21T13:10:05.392Z", - "id": 21, - "last_modified_by_id": null, - "lock": null, - "lock_ttl": null, - "team_id": 1, - "updated_at": "2019-01-21T13:10:06.290Z", - "version": 1 + "asset": { + "created_at": "2019-01-21T13:09:35.615Z", + "created_by_id": 1, + "estimated_size": 17799, + "file_content_type": null, + "file_file_name": null, + "file_file_size": null, + "file_present": true, + "file_processing": null, + "file_updated_at": "2019-01-21T13:10:05.392Z", + "id": 21, + "last_modified_by_id": null, + "lock": null, + "lock_ttl": null, + "team_id": 1, + "updated_at": "2019-01-21T13:10:06.290Z", + "version": 1 + }, + "asset_blob": { + "filename": "100gen_line_bending.png", + "content_type": "image/png", + "byte_size": 16181 + } } ], "checklists": [], @@ -597,22 +604,29 @@ }, { "asset": { - "created_at": "2019-01-21T12:45:29.138Z", - "created_by_id": 1, - "estimated_size": 232, - "file_content_type": "text/plain", - "file_file_name": "samples.txt", - "file_file_size": 69, - "file_present": true, - "file_processing": false, - "file_updated_at": "2019-01-21T12:45:28.889Z", - "id": 1, - "last_modified_by_id": 1, - "lock": null, - "lock_ttl": null, - "team_id": 1, - "updated_at": "2019-01-21T13:06:01.610Z", - "version": 1 + "asset": { + "created_at": "2019-01-21T12:45:29.138Z", + "created_by_id": 1, + "estimated_size": 232, + "file_content_type": null, + "file_file_name": null, + "file_file_size": null, + "file_present": true, + "file_processing": null, + "file_updated_at": "2019-01-21T12:45:28.889Z", + "id": 1, + "last_modified_by_id": 1, + "lock": null, + "lock_ttl": null, + "team_id": 1, + "updated_at": "2019-01-21T13:06:01.610Z", + "version": 1 + }, + "asset_blob": { + "filename": "samples.txt", + "content_type": "text/plain", + "byte_size": 69 + } }, "result": { "archived": true, @@ -1069,22 +1083,29 @@ { "assets": [ { - "created_at": "2019-01-21T12:45:32.296Z", - "created_by_id": 1, - "estimated_size": 1129370, - "file_content_type": "application/pdf", - "file_file_name": "G2938-90034_KitRNA6000Nano_ebook.pdf", - "file_file_size": 974068, - "file_present": true, - "file_processing": false, - "file_updated_at": "2019-01-21T12:45:32.237Z", - "id": 8, - "last_modified_by_id": 1, - "lock": null, - "lock_ttl": null, - "team_id": 1, - "updated_at": "2019-01-21T13:06:04.590Z", - "version": 1 + "asset": { + "created_at": "2019-01-21T12:45:32.296Z", + "created_by_id": 1, + "estimated_size": 1129370, + "file_content_type": null, + "file_file_name": null, + "file_file_size": null, + "file_present": true, + "file_processing": null, + "file_updated_at": "2019-01-21T12:45:32.237Z", + "id": 8, + "last_modified_by_id": 1, + "lock": null, + "lock_ttl": null, + "team_id": 1, + "updated_at": "2019-01-21T13:06:04.590Z", + "version": 1 + }, + "asset_blob": { + "filename": "G2938-90034_KitRNA6000Nano_ebook.pdf", + "content_type": "application/pdf", + "byte_size": 974068 + } } ], "checklists": [], @@ -1118,22 +1139,29 @@ "results": [ { "asset": { - "created_at": "2019-01-21T12:45:32.847Z", - "created_by_id": 1, - "estimated_size": 46131, - "file_content_type": "image/jpeg", - "file_file_name": "Bioanalyser_result.JPG", - "file_file_size": 41938, - "file_present": true, - "file_processing": false, - "file_updated_at": "2019-01-21T12:45:43.655Z", - "id": 9, - "last_modified_by_id": 1, - "lock": null, - "lock_ttl": null, - "team_id": 1, - "updated_at": "2019-01-21T12:45:44.269Z", - "version": 1 + "asset": { + "created_at": "2019-01-21T12:45:32.847Z", + "created_by_id": 1, + "estimated_size": 46131, + "file_content_type": null, + "file_file_name": null, + "file_file_size": null, + "file_present": true, + "file_processing": null, + "file_updated_at": "2019-01-21T12:45:43.655Z", + "id": 9, + "last_modified_by_id": 1, + "lock": null, + "lock_ttl": null, + "team_id": 1, + "updated_at": "2019-01-21T12:45:44.269Z", + "version": 1 + }, + "asset_blob": { + "filename": "Bioanalyser_result.JPG", + "content_type": "image/jpeg", + "byte_size": 41938 + } }, "result": { "archived": true, diff --git a/spec/services/repository_actions/duplicate_rows_spec.rb b/spec/services/repository_actions/duplicate_rows_spec.rb index 5b433ab98..24f969caf 100644 --- a/spec/services/repository_actions/duplicate_rows_spec.rb +++ b/spec/services/repository_actions/duplicate_rows_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_helper' describe RepositoryActions::DuplicateRows do diff --git a/spec/services/repository_datatable_service_spec.rb b/spec/services/repository_datatable_service_spec.rb index b717c5dae..7a500f138 100644 --- a/spec/services/repository_datatable_service_spec.rb +++ b/spec/services/repository_datatable_service_spec.rb @@ -65,7 +65,7 @@ describe RepositoryDatatableService do contitions = subject.send(:build_conditions, params) expect(contitions[:search_value]).to eq 'row' expect(contitions[:order_by_column]).to eq( - { column: 3, dir: 'asc' } + column: 3, dir: 'asc' ) end end diff --git a/spec/services/repository_table_state_column_update_service_spec.rb b/spec/services/repository_table_state_column_update_service_spec.rb index 982c4694a..c8320ceeb 100644 --- a/spec/services/repository_table_state_column_update_service_spec.rb +++ b/spec/services/repository_table_state_column_update_service_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_helper' describe RepositoryTableStateColumnUpdateService do @@ -13,7 +15,7 @@ describe RepositoryTableStateColumnUpdateService do create :repository_column, name: 'My column 1', repository: repository, data_type: :RepositoryTextValue - end + end let!(:repository_column_2) do create :repository_column, name: 'My column 2', repository: repository, @@ -32,12 +34,12 @@ describe RepositoryTableStateColumnUpdateService do last_modified_by: user_2 end let!(:default_order) do - { '0' => ['2', 'asc'] } + { '0' => %w(2 asc) } end let!(:default_column_def) do { 'visible' => 'true', 'searchable' => 'true', - 'search' => { 'search' => '', + 'search' => { 'search' => '', 'smart' => 'true', 'regex' => 'false', 'caseInsensitive' => 'true' } } @@ -51,7 +53,7 @@ describe RepositoryTableStateColumnUpdateService do RepositoryTableStateService.new(user_1, repository).create_default_state end let!(:initial_state_2) do - RepositoryTableStateService.new(user_2, repository).create_default_state + RepositoryTableStateService.new(user_2, repository).create_default_state end it 'should keep default repository states valid' do @@ -81,11 +83,11 @@ describe RepositoryTableStateColumnUpdateService do end it 'should keep order as it was' do - initial_state_1.state['order'] = { '0' => ['3', 'desc'] } + initial_state_1.state['order'] = { '0' => %w(3 desc) } RepositoryTableStateService.new(user_1, repository).update_state( initial_state_1.state ) - initial_state_2.state['order'] = { '0' => ['4', 'asc'] } + initial_state_2.state['order'] = { '0' => %w(4 asc) } RepositoryTableStateService.new(user_2, repository).update_state( initial_state_2.state ) @@ -94,9 +96,9 @@ describe RepositoryTableStateColumnUpdateService do service.update_states_with_new_column(repository) state_1 = RepositoryTableStateService.new(user_1, repository).load_state - expect(state_1.state['order']).to eq({ '0' => ['3', 'desc'] }) + expect(state_1.state['order']).to eq('0' => %w(3 desc)) state_2 = RepositoryTableStateService.new(user_2, repository).load_state - expect(state_2.state['order']).to eq({ '0' => ['4', 'asc'] }) + expect(state_2.state['order']).to eq('0' => %w(4 asc)) end it 'should keep search as it was' do @@ -131,7 +133,7 @@ describe RepositoryTableStateColumnUpdateService do state_1 = RepositoryTableStateService.new(user_1, repository).load_state expect(state_1.state['columns']).to eq( - cols_1.merge({'8' => default_column_def, '9' => default_column_def}) + cols_1.merge('8' => default_column_def, '9' => default_column_def) ) state_2 = RepositoryTableStateService.new(user_2, repository).load_state expect(state_2.state['columns']).to eq( @@ -141,12 +143,12 @@ describe RepositoryTableStateColumnUpdateService do it 'should keep column order as it was' do initial_state_1.state['ColReorder'] = - ['5', '3', '2', '0', '1', '4', '6', '7'] + %w(5 3 2 0 1 4 6 7) RepositoryTableStateService.new(user_1, repository).update_state( initial_state_1.state ) initial_state_2.state['ColReorder'] = - ['0', '6', '1', '4', '5', '7', '2', '3'] + %w(0 6 1 4 5 7 2 3) RepositoryTableStateService.new(user_2, repository).update_state( initial_state_2.state ) @@ -156,11 +158,11 @@ describe RepositoryTableStateColumnUpdateService do state_1 = RepositoryTableStateService.new(user_1, repository).load_state expect(state_1.state['ColReorder']).to eq( - ['5', '3', '2', '0', '1', '4', '6', '7', '8', '9'] + %w(5 3 2 0 1 4 6 7 8 9) ) state_2 = RepositoryTableStateService.new(user_2, repository).load_state expect(state_2.state['ColReorder']).to eq( - ['0', '6', '1', '4', '5', '7', '2', '3', '8', '9'] + %w(0 6 1 4 5 7 2 3 8 9) ) end end @@ -170,7 +172,7 @@ describe RepositoryTableStateColumnUpdateService do RepositoryTableStateService.new(user_1, repository).create_default_state end let!(:initial_state_2) do - RepositoryTableStateService.new(user_2, repository).create_default_state + RepositoryTableStateService.new(user_2, repository).create_default_state end # For column removal, we often use the index '6' twice - first, to @@ -204,11 +206,11 @@ describe RepositoryTableStateColumnUpdateService do end it 'should keep order as it was' do - initial_state_1.state['order'] = { '0' => ['3', 'desc'] } + initial_state_1.state['order'] = { '0' => %w(3 desc) } RepositoryTableStateService.new(user_1, repository).update_state( initial_state_1.state ) - initial_state_2.state['order'] = { '0' => ['7', 'asc'] } + initial_state_2.state['order'] = { '0' => %w(7 asc) } RepositoryTableStateService.new(user_2, repository).update_state( initial_state_2.state ) @@ -217,7 +219,7 @@ describe RepositoryTableStateColumnUpdateService do service.update_states_with_removed_column(repository, '6') state_1 = RepositoryTableStateService.new(user_1, repository).load_state - expect(state_1.state['order']).to eq({ '0' => ['3', 'desc'] }) + expect(state_1.state['order']).to eq('0' => %w(3 desc)) state_2 = RepositoryTableStateService.new(user_2, repository).load_state expect(state_2.state['order']).to eq(default_order) end @@ -270,12 +272,12 @@ describe RepositoryTableStateColumnUpdateService do it 'should keep column order as it was' do initial_state_1.state['ColReorder'] = - ['5', '3', '2', '0', '1', '4', '6', '7'] + %w(5 3 2 0 1 4 6 7) RepositoryTableStateService.new(user_1, repository).update_state( initial_state_1.state ) initial_state_2.state['ColReorder'] = - ['0', '6', '1', '4', '5', '7', '2', '3'] + %w(0 6 1 4 5 7 2 3) RepositoryTableStateService.new(user_2, repository).update_state( initial_state_2.state ) @@ -285,11 +287,11 @@ describe RepositoryTableStateColumnUpdateService do state_1 = RepositoryTableStateService.new(user_1, repository).load_state expect(state_1.state['ColReorder']).to eq( - ['5', '3', '2', '0', '1', '4'] + %w(5 3 2 0 1 4) ) state_2 = RepositoryTableStateService.new(user_2, repository).load_state expect(state_2.state['ColReorder']).to eq( - ['0', '1', '4', '5', '2', '3'] + %w(0 1 4 5 2 3) ) end end @@ -308,12 +310,12 @@ describe RepositoryTableStateColumnUpdateService do let!(:initial_state) do state = RepositoryTableStateService.new(user_1, repository) .create_default_state - state.state['order'] = {'0' => ['8', 'desc']} + state.state['order'] = { '0' => %w(8 desc) } (0..9).each do |idx| state.state['columns'][idx.to_s]['search']['search'] = "search_#{idx}" end state.state['ColReorder'] = - ['0', '1', '2', '9', '8', '4', '7', '3', '5', '6'] + %w(0 1 2 9 8 4 7 3 5 6) RepositoryTableStateService.new(user_1, repository).update_state( state.state ) @@ -328,7 +330,7 @@ describe RepositoryTableStateColumnUpdateService do state = RepositoryTableStateService.new(user_1, repository).load_state expect(state).to be_valid_repository_table_state(5) expect(state.state['ColReorder']).to eq( - ['0', '1', '2', '9', '8', '4', '7', '3', '5', '6', '10'] + %w(0 1 2 9 8 4 7 3 5 6 10) ) service.update_states_with_removed_column(repository, '7') @@ -336,25 +338,25 @@ describe RepositoryTableStateColumnUpdateService do state = RepositoryTableStateService.new(user_1, repository).load_state expect(state).to be_valid_repository_table_state(4) expect(state.state['ColReorder']).to eq( - ['0', '1', '2', '8', '7', '4', '3', '5', '6', '9'] + %w(0 1 2 8 7 4 3 5 6 9) ) - expect(state.state['order']).to eq ({'0' => ['7', 'desc']}) + expect(state.state['order']).to eq ({ '0' => %w(7 desc) }) service.update_states_with_removed_column(repository, '7') state = RepositoryTableStateService.new(user_1, repository).load_state expect(state).to be_valid_repository_table_state(3) expect(state.state['ColReorder']).to eq( - ['0', '1', '2', '7', '4', '3', '5', '6', '8'] + %w(0 1 2 7 4 3 5 6 8) ) - expect(state.state['order']).to eq ({'0' => ['2', 'asc']}) + expect(state.state['order']).to eq ({ '0' => %w(2 asc) }) service.update_states_with_removed_column(repository, '7') state = RepositoryTableStateService.new(user_1, repository).load_state expect(state).to be_valid_repository_table_state(2) expect(state.state['ColReorder']).to eq( - ['0', '1', '2', '4', '3', '5', '6', '7'] + %w(0 1 2 4 3 5 6 7) ) service.update_states_with_new_column(repository) @@ -363,7 +365,7 @@ describe RepositoryTableStateColumnUpdateService do state = RepositoryTableStateService.new(user_1, repository).load_state expect(state).to be_valid_repository_table_state(4) expect(state.state['ColReorder']).to eq( - ['0', '1', '2', '4', '3', '5', '6', '7', '8', '9'] + %w(0 1 2 4 3 5 6 7 8 9) ) end end diff --git a/spec/services/repository_table_state_service_spec.rb b/spec/services/repository_table_state_service_spec.rb index 56e2075e0..a1fc4a637 100644 --- a/spec/services/repository_table_state_service_spec.rb +++ b/spec/services/repository_table_state_service_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_helper' describe RepositoryTableStateService do @@ -12,7 +14,7 @@ describe RepositoryTableStateService do create :repository_column, name: 'My column 1', repository: repository, data_type: :RepositoryTextValue - end + end let!(:repository_column_2) do create :repository_column, name: 'My column 2', repository: repository, diff --git a/spec/services/repository_zip_export_spec.rb b/spec/services/repository_zip_export_spec.rb index 695433bf6..9cd4fb167 100644 --- a/spec/services/repository_zip_export_spec.rb +++ b/spec/services/repository_zip_export_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_helper' require 'zip' diff --git a/spec/services/smart_annotations/html_preview_spec.rb b/spec/services/smart_annotations/html_preview_spec.rb index e74ee5f36..d502db0a5 100644 --- a/spec/services/smart_annotations/html_preview_spec.rb +++ b/spec/services/smart_annotations/html_preview_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_helper' require 'smart_annotations/html_preview' diff --git a/spec/services/smart_annotations/permission_eval_spec.rb b/spec/services/smart_annotations/permission_eval_spec.rb index a4039f628..6d93191e0 100644 --- a/spec/services/smart_annotations/permission_eval_spec.rb +++ b/spec/services/smart_annotations/permission_eval_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_helper' describe SmartAnnotations::PermissionEval do diff --git a/spec/services/smart_annotations/tag_to_html_spec.rb b/spec/services/smart_annotations/tag_to_html_spec.rb index f1cf713eb..05104dec4 100644 --- a/spec/services/smart_annotations/tag_to_html_spec.rb +++ b/spec/services/smart_annotations/tag_to_html_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_helper' describe SmartAnnotations::TagToHtml do @@ -32,9 +34,9 @@ describe SmartAnnotations::TagToHtml do describe '#fetch_object/2' do it 'rises an error if type is not valid' do - expect { + expect do subject.send(:fetch_object, 'banana', project.id) - }.to raise_error(ActiveRecord::RecordNotFound) + end.to raise_error(ActiveRecord::RecordNotFound) end it 'returns the required object' do diff --git a/spec/services/smart_annotations/tag_to_text_spec.rb b/spec/services/smart_annotations/tag_to_text_spec.rb index 76d453dd1..58c40e08e 100644 --- a/spec/services/smart_annotations/tag_to_text_spec.rb +++ b/spec/services/smart_annotations/tag_to_text_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_helper' describe SmartAnnotations::TagToText do @@ -29,9 +31,9 @@ describe SmartAnnotations::TagToText do describe '#fetch_object/2' do it 'rises an error if type is not valid' do - expect { + expect do subject.send(:fetch_object, 'banana', project.id) - }.to raise_error(ActiveRecord::RecordNotFound) + end.to raise_error(ActiveRecord::RecordNotFound) end it 'returns the required object' do @@ -52,7 +54,7 @@ describe SmartAnnotations::TagToText do random_text = "Sec:[@#{user_two.full_name}~#{user_two.id.base62_encode}]" expect( subject.send(:parse_users_annotations, user, team, random_text) - ).to eq "Sec:" + ).to eq 'Sec:' end end end diff --git a/spec/services/smart_annotations/text_preview_spec.rb b/spec/services/smart_annotations/text_preview_spec.rb index 42e9ade26..f2665cf6e 100644 --- a/spec/services/smart_annotations/text_preview_spec.rb +++ b/spec/services/smart_annotations/text_preview_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_helper' require 'smart_annotations/text_preview' diff --git a/spec/services/tasks/samples_to_repository_migration_service_spec.rb b/spec/services/tasks/samples_to_repository_migration_service_spec.rb index 6730cbb78..0f3968c3c 100644 --- a/spec/services/tasks/samples_to_repository_migration_service_spec.rb +++ b/spec/services/tasks/samples_to_repository_migration_service_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_helper' describe Tasks::SamplesToRepositoryMigrationService do diff --git a/spec/services/templates_service_spec.rb b/spec/services/templates_service_spec.rb index d7799b889..c088300f4 100644 --- a/spec/services/templates_service_spec.rb +++ b/spec/services/templates_service_spec.rb @@ -65,6 +65,7 @@ describe TemplatesService do demo_task.protocol.steps.size.positive? next end + demo_task.protocol.steps.each do |demo_step| tmpl_step = tmpl_task.protocol.steps.find_by_name(demo_step.name) expect(demo_step.name).to eq(tmpl_step.name) From 618f0cc334fd2e8a2d70c36255cbbc304e55062f Mon Sep 17 00:00:00 2001 From: Anton Ignatov Date: Fri, 26 Jul 2019 14:16:44 +0200 Subject: [PATCH 2/4] Fix tests for services --- app/models/zip_export.rb | 2 +- app/services/projects_overview_service.rb | 4 +++- spec/services/repository_zip_export_spec.rb | 3 ++- spec/services/templates_service_spec.rb | 4 ++-- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/models/zip_export.rb b/app/models/zip_export.rb index 6706662a8..83e0f955f 100644 --- a/app/models/zip_export.rb +++ b/app/models/zip_export.rb @@ -49,7 +49,7 @@ class ZipExport < ApplicationRecord end def zip_file_name - return '' unless file.attached? + return '' unless zip_file.attached? zip_file.blob&.filename&.to_s end diff --git a/app/services/projects_overview_service.rb b/app/services/projects_overview_service.rb index 082c0b589..78edc42cd 100644 --- a/app/services/projects_overview_service.rb +++ b/app/services/projects_overview_service.rb @@ -84,7 +84,9 @@ class ProjectsOverviewService ).joins( "LEFT OUTER JOIN (#{due_modules.to_sql}) due_modules "\ "ON due_modules.experiment_id = experiments.id" - ).left_outer_joins(:user_projects, :project_comments) + ).joins( + 'LEFT OUTER JOIN user_projects ON user_projects.project_id = projects.id' + ).left_outer_joins(:project_comments) # Only admins see all projects of the team unless @user.is_admin_of_team?(@team) diff --git a/spec/services/repository_zip_export_spec.rb b/spec/services/repository_zip_export_spec.rb index 9cd4fb167..8e5699810 100644 --- a/spec/services/repository_zip_export_spec.rb +++ b/spec/services/repository_zip_export_spec.rb @@ -71,7 +71,8 @@ describe RepositoryZipExport, type: :background_job do ZipExport.skip_callback(:create, :after, :self_destruct) RepositoryZipExport.generate_zip(params, repository, user) csv_zip_file = ZipExport.first.zip_file - parsed_csv_content = Zip::File.open(csv_zip_file.path) do |zip_file| + file_path = ActiveStorage::Blob.service.public_send(:path_for, csv_zip_file.key) + parsed_csv_content = Zip::File.open(file_path) do |zip_file| csv_file = zip_file.glob('*.csv').first csv_content = csv_file.get_input_stream.read CSV.parse(csv_content, headers: true) diff --git a/spec/services/templates_service_spec.rb b/spec/services/templates_service_spec.rb index c088300f4..7d119500b 100644 --- a/spec/services/templates_service_spec.rb +++ b/spec/services/templates_service_spec.rb @@ -50,7 +50,7 @@ describe TemplatesService do tmpl_res = tmpl_task.results.find_by_name(demo_res.name) expect(tmpl_res.name).to eq(demo_res.name) if demo_res.asset - expect(tmpl_res.asset.file.exists?).to eq(true) + expect(tmpl_res.asset.file.attached?).to eq(true) expect(demo_res.asset.file_file_name) .to eq(tmpl_res.asset.file_file_name) elsif demo_res.table @@ -75,7 +75,7 @@ describe TemplatesService do .to match_array(tmpl_step.assets.pluck(:file_file_name)) end tmpl_step.assets.each do |asset| - expect(asset.file.exists?).to eq(true) + expect(asset.file.attached?).to eq(true) end if demo_step.tables.present? expect(demo_step.tables.pluck(:contents)) From a4b28252585ef2827a81fc56c726c6b1eb10e15e Mon Sep 17 00:00:00 2001 From: Anton Ignatov Date: Fri, 26 Jul 2019 15:58:51 +0200 Subject: [PATCH 3/4] Fix tests for active storage --- Gemfile | 1 + Gemfile.lock | 3 ++- app/models/repository_asset_value.rb | 13 +++++++------ .../api/v1/repository_asset_value_serializer.rb | 4 ++-- app/serializers/api/v1/result_asset_serializer.rb | 4 ++-- app/serializers/api/v1/user_serializer.rb | 12 ++++++------ .../experiment_archive/_experiment.html.erb | 2 +- spec/requests/api/v1/inventories_controller_spec.rb | 3 ++- 8 files changed, 23 insertions(+), 19 deletions(-) diff --git a/Gemfile b/Gemfile index c5ad68913..590ec0f31 100644 --- a/Gemfile +++ b/Gemfile @@ -32,6 +32,7 @@ gem 'json-jwt' gem 'jwt', '~> 1.5' gem 'kaminari' gem 'rack-attack' +gem 'jsonapi-renderer', '= 0.2.0' # JS datetime library, requirement of datetime picker gem 'momentjs-rails', '~> 2.17.1' diff --git a/Gemfile.lock b/Gemfile.lock index 81e522c6c..a006d3b98 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -283,7 +283,7 @@ GEM json_matchers (0.11.0) json_schema json_schema (0.20.6) - jsonapi-renderer (0.2.1) + jsonapi-renderer (0.2.0) jwt (1.5.6) kaminari (1.1.1) activesupport (>= 4.1.0) @@ -616,6 +616,7 @@ DEPENDENCIES js_cookie_rails json-jwt json_matchers + jsonapi-renderer (= 0.2.0) jwt (~> 1.5) kaminari listen (~> 3.0) diff --git a/app/models/repository_asset_value.rb b/app/models/repository_asset_value.rb index e1eb6e4c3..30143a7bb 100644 --- a/app/models/repository_asset_value.rb +++ b/app/models/repository_asset_value.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class RepositoryAssetValue < ApplicationRecord belongs_to :created_by, foreign_key: :created_by_id, @@ -28,8 +30,7 @@ class RepositoryAssetValue < ApplicationRecord end def update_data!(new_data, user) - file.original_filename = new_data[:file_name] - asset.file.attach(io: new_data[:file_data], filename: new_data[:file_name]) + asset.file.attach(io: StringIO.new(Base64.decode64(new_data[:file_data].split(',')[1])), filename: new_data[:file_name]) asset.last_modified_by = user self.last_modified_by = user asset.save! && save! @@ -38,15 +39,15 @@ class RepositoryAssetValue < ApplicationRecord def self.new_with_payload(payload, attributes) value = new(attributes) team = value.repository_cell.repository_column.repository.team - file = Paperclip.io_adapters.for(payload[:file_data]) - file.original_filename = payload[:file_name] value.asset = Asset.create!( - file: file, created_by: value.created_by, last_modified_by: value.created_by, team: team ) - value.asset.post_process_file(team) + value.asset.file.attach( + io: StringIO.new(Base64.decode64(payload[:file_data].split(',')[1])), + filename: payload[:file_name] + ) value end end diff --git a/app/serializers/api/v1/repository_asset_value_serializer.rb b/app/serializers/api/v1/repository_asset_value_serializer.rb index a64c49532..b55e08664 100644 --- a/app/serializers/api/v1/repository_asset_value_serializer.rb +++ b/app/serializers/api/v1/repository_asset_value_serializer.rb @@ -18,10 +18,10 @@ module Api end def url - if !object.asset&.file&.exists? + if !object.asset&.file&.attached? nil else - rails_blob_path(object.asset.file, disposition: 'attachment') + Rails.application.routes.url_helpers.rails_blob_path(object.asset.file, disposition: 'attachment') end end end diff --git a/app/serializers/api/v1/result_asset_serializer.rb b/app/serializers/api/v1/result_asset_serializer.rb index d86e999f2..d3da45635 100644 --- a/app/serializers/api/v1/result_asset_serializer.rb +++ b/app/serializers/api/v1/result_asset_serializer.rb @@ -19,10 +19,10 @@ module Api end def url - if !object.asset&.file&.exists? + if !object.asset&.file&.attached? nil else - rails_blob_path(object.asset.file, disposition: 'attachment') + Rails.application.routes.url_helpers.rails_blob_path(object.asset.file, disposition: 'attachment') end end end diff --git a/app/serializers/api/v1/user_serializer.rb b/app/serializers/api/v1/user_serializer.rb index 4b29121b2..22c3f262d 100644 --- a/app/serializers/api/v1/user_serializer.rb +++ b/app/serializers/api/v1/user_serializer.rb @@ -5,20 +5,20 @@ module Api class UserSerializer < ActiveModel::Serializer type :users attributes :full_name, :initials, :email - attribute :avatar_file_name, if: -> { object.avatar.present? } - attribute :avatar_file_size, if: -> { object.avatar.present? } - attribute :avatar_url, if: -> { object.avatar.present? } + attribute :avatar_file_name, if: -> { object.avatar.attached? } + attribute :avatar_file_size, if: -> { object.avatar.attached? } + attribute :avatar_url, if: -> { object.avatar.attached? } def avatar_file_name - object.avatar_file_name + object.avatar.blob.filename end def avatar_file_size - object.avatar.size + object.avatar.blob.byte_size end def avatar_url - object.avatar.url(:icon) + object.avatar_url(:icon) end end end diff --git a/app/views/projects/experiment_archive/_experiment.html.erb b/app/views/projects/experiment_archive/_experiment.html.erb index fe27a09d7..da055d451 100644 --- a/app/views/projects/experiment_archive/_experiment.html.erb +++ b/app/views/projects/experiment_archive/_experiment.html.erb @@ -23,7 +23,7 @@

- <% if experiment.workflowimg? %> + <% if experiment.workflowimg.attached? %>
<%= image_tag( experiment.workflowimg.expiring_url( diff --git a/spec/requests/api/v1/inventories_controller_spec.rb b/spec/requests/api/v1/inventories_controller_spec.rb index 612b1c1b8..22f4e736b 100644 --- a/spec/requests/api/v1/inventories_controller_spec.rb +++ b/spec/requests/api/v1/inventories_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe "Api::V1::InventoriesController", type: :request do +RSpec.describe 'Api::V1::InventoriesController', type: :request do before :all do @user = create(:user) @teams = create_list(:team, 2, created_by: @user) @@ -55,6 +55,7 @@ RSpec.describe "Api::V1::InventoriesController", type: :request do id: @teams.first.repositories.first.id), headers: @valid_headers expect { hash_body = json }.not_to raise_exception + expect(hash_body[:data]).to match( ActiveModelSerializers::SerializableResource .new(@teams.first.repositories.first, From f35f4886a3c064b9015411584a90bdd40aab8812 Mon Sep 17 00:00:00 2001 From: Anton Ignatov Date: Mon, 29 Jul 2019 10:18:34 +0200 Subject: [PATCH 4/4] Fix markup --- Gemfile | 9 ++++----- app/models/repository_asset_value.rb | 3 ++- .../repository_table_state_column_update_service.rb | 8 ++++---- spec/services/model_importers/team_importer_spec.rb | 9 +++++++-- .../repository_table_state_column_update_service_spec.rb | 4 ++-- 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/Gemfile b/Gemfile index 590ec0f31..0acba81b4 100644 --- a/Gemfile +++ b/Gemfile @@ -4,9 +4,7 @@ source 'http://rubygems.org' ruby '2.6.3' - gem 'bootsnap', require: false -gem 'webpacker', '~> 3.5' gem 'bootstrap-sass', '~> 3.3.7' gem 'bootstrap_form', '~> 2.7.0' gem 'devise', '~> 4.6.2' @@ -19,6 +17,7 @@ gem 'recaptcha', require: 'recaptcha/rails' gem 'sanitize', '~> 4.4' gem 'sassc-rails' gem 'simple_token_authentication', '~> 1.15.1' # Token authentication for Devise +gem 'webpacker', '~> 3.5' gem 'yomu' # Gems for OAuth2 subsystem @@ -29,10 +28,10 @@ gem 'omniauth-linkedin-oauth2' # Gems for API implementation gem 'active_model_serializers', '~> 0.10.7' gem 'json-jwt' +gem 'jsonapi-renderer', '= 0.2.0' gem 'jwt', '~> 1.5' gem 'kaminari' gem 'rack-attack' -gem 'jsonapi-renderer', '= 0.2.0' # JS datetime library, requirement of datetime picker gem 'momentjs-rails', '~> 2.17.1' @@ -83,12 +82,12 @@ gem 'wkhtmltopdf-heroku', '2.12.4' gem 'aws-sdk-rails' gem 'aws-sdk-s3' -gem 'mini_magick' -gem 'paperclip', '~> 6.1' # File attachment, image attachment library gem 'delayed_job_active_record' gem 'devise-async', git: 'https://github.com/mhfs/devise-async.git', branch: 'devise-4.x' +gem 'mini_magick' +gem 'paperclip', '~> 6.1' # File attachment, image attachment library gem 'rufus-scheduler', '~> 3.5' gem 'discard', '~> 1.0' diff --git a/app/models/repository_asset_value.rb b/app/models/repository_asset_value.rb index 30143a7bb..a124069d0 100644 --- a/app/models/repository_asset_value.rb +++ b/app/models/repository_asset_value.rb @@ -30,7 +30,8 @@ class RepositoryAssetValue < ApplicationRecord end def update_data!(new_data, user) - asset.file.attach(io: StringIO.new(Base64.decode64(new_data[:file_data].split(',')[1])), filename: new_data[:file_name]) + asset.file.attach(io: StringIO.new(Base64.decode64(new_data[:file_data].split(',')[1])), + filename: new_data[:file_name]) asset.last_modified_by = user self.last_modified_by = user asset.save! && save! diff --git a/app/services/repository_table_state_column_update_service.rb b/app/services/repository_table_state_column_update_service.rb index 2598a7327..78d433f9f 100644 --- a/app/services/repository_table_state_column_update_service.rb +++ b/app/services/repository_table_state_column_update_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class RepositoryTableStateColumnUpdateService # We're using Constants::REPOSITORY_TABLE_DEFAULT_STATE as a reference for # default table state; this Ruby Hash makes heavy use of Ruby symbols @@ -56,9 +58,7 @@ class RepositoryTableStateColumnUpdateService state['order'].reject! { |_, v| v[0] == old_column_index } state['order'].each do |k, v| - if v[0].to_i > old_column_index.to_i - state['order'][k] = [(v[0].to_i - 1).to_s, v[1]] - end + state['order'][k] = [(v[0].to_i - 1).to_s, v[1]] if v[0].to_i > old_column_index.to_i end if state['order'].empty? # Fallback to default order if user had table ordered by @@ -73,4 +73,4 @@ class RepositoryTableStateColumnUpdateService table_state.save end end -end \ No newline at end of file +end diff --git a/spec/services/model_importers/team_importer_spec.rb b/spec/services/model_importers/team_importer_spec.rb index 19f56c7ba..95edbc094 100644 --- a/spec/services/model_importers/team_importer_spec.rb +++ b/spec/services/model_importers/team_importer_spec.rb @@ -279,8 +279,13 @@ describe TeamImporter do ) json_step['assets'].each do |json_asset| - blob_id = ActiveRecord::Base.connection.execute("SELECT active_storage_blobs.id FROM active_storage_blobs WHERE active_storage_blobs.filename = '#{json_asset['asset_blob']['filename']}' LIMIT 1") - db_asset = db_step.assets.joins(:file_attachment).where('active_storage_attachments.blob_id' => blob_id.as_json[0]['id'].to_i).first + blob_id = ActiveRecord::Base.connection.execute(\ + "SELECT active_storage_blobs.id "\ + "FROM active_storage_blobs "\ + "WHERE active_storage_blobs.filename = '#{json_asset['asset_blob']['filename']}' LIMIT 1" + ) + db_asset = db_step.assets.joins(:file_attachment) + .where('active_storage_attachments.blob_id' => blob_id.as_json[0]['id'].to_i).first # Basic fields expect(db_asset.created_at).to eq( diff --git a/spec/services/repository_table_state_column_update_service_spec.rb b/spec/services/repository_table_state_column_update_service_spec.rb index c8320ceeb..3f97a10cf 100644 --- a/spec/services/repository_table_state_column_update_service_spec.rb +++ b/spec/services/repository_table_state_column_update_service_spec.rb @@ -340,7 +340,7 @@ describe RepositoryTableStateColumnUpdateService do expect(state.state['ColReorder']).to eq( %w(0 1 2 8 7 4 3 5 6 9) ) - expect(state.state['order']).to eq ({ '0' => %w(7 desc) }) + expect(state.state['order']).to eq('0' => %w(7 desc)) service.update_states_with_removed_column(repository, '7') @@ -349,7 +349,7 @@ describe RepositoryTableStateColumnUpdateService do expect(state.state['ColReorder']).to eq( %w(0 1 2 7 4 3 5 6 8) ) - expect(state.state['order']).to eq ({ '0' => %w(2 asc) }) + expect(state.state['order']).to eq('0' => %w(2 asc)) service.update_states_with_removed_column(repository, '7')