From 6f80051c10c44bcd8efc96f29085bdf20ccdc430 Mon Sep 17 00:00:00 2001 From: Daniel Baark <5101747+baarkerlounger@users.noreply.github.com> Date: Mon, 15 Nov 2021 17:01:00 +0000 Subject: [PATCH] CLDC-638: Bulk upload (#84) * Add file broswer view * Read file * Happy path * Route constraint * Some placeholders * Add more fields * Inferred vals * Add some file type error handling * Spec empty file handling * Fail silently for now * Collection route * Rubocop * Conventional routes are easier to reason about than constraints * Remove uneeded fields * Add a guard clause * Allow log creation with bad rows. * Update major repairs field name * Make route bulk upload singular * More realistic spec file * Fix merge conflict resolution * Set hhemb --- Gemfile | 2 + Gemfile.lock | 4 + app/controllers/bulk_upload_controller.rb | 22 ++ app/models/bulk_upload.rb | 199 ++++++++++++++++++ app/models/case_log.rb | 3 +- app/views/case_logs/bulk_upload.html.erb | 10 + app/views/form/page.html.erb | 2 +- config/routes.rb | 15 +- db/schema.rb | 4 +- .../files/2021_22_lettings_bulk_upload.xlsx | Bin 0 -> 25857 bytes .../2021_22_lettings_bulk_upload_empty.xlsx | Bin 0 -> 24693 bytes spec/fixtures/files/random.txt | 0 spec/requests/bulk_upload_controller_spec.rb | 60 ++++++ 13 files changed, 314 insertions(+), 7 deletions(-) create mode 100644 app/controllers/bulk_upload_controller.rb create mode 100644 app/models/bulk_upload.rb create mode 100644 app/views/case_logs/bulk_upload.html.erb create mode 100644 spec/fixtures/files/2021_22_lettings_bulk_upload.xlsx create mode 100644 spec/fixtures/files/2021_22_lettings_bulk_upload_empty.xlsx create mode 100644 spec/fixtures/files/random.txt create mode 100644 spec/requests/bulk_upload_controller_spec.rb diff --git a/Gemfile b/Gemfile index 599c5d369..3c8d77954 100644 --- a/Gemfile +++ b/Gemfile @@ -29,6 +29,8 @@ gem "discard" gem "activeadmin" # Admin charts gem "chartkick" +# Spreadsheet parsing +gem "roo" # Json Schema gem "json-schema" gem "uk_postcode" diff --git a/Gemfile.lock b/Gemfile.lock index d2a3b4a15..04db4e6f3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -294,6 +294,9 @@ GEM actionpack (>= 5.0) railties (>= 5.0) rexml (3.2.5) + roo (2.8.3) + nokogiri (~> 1) + rubyzip (>= 1.3.0, < 3.0.0) rubocop (1.21.0) parallel (~> 1.10) parser (>= 3.0.0.0) @@ -409,6 +412,7 @@ DEPENDENCIES puma (~> 5.0) rack-mini-profiler (~> 2.0) rails (~> 6.1.4) + roo rspec-core! rspec-expectations! rspec-mocks! diff --git a/app/controllers/bulk_upload_controller.rb b/app/controllers/bulk_upload_controller.rb new file mode 100644 index 000000000..ba552cb49 --- /dev/null +++ b/app/controllers/bulk_upload_controller.rb @@ -0,0 +1,22 @@ +class BulkUploadController < ApplicationController + def show + @bulk_upload = BulkUpload.new(nil, nil) + render "case_logs/bulk_upload" + end + + def bulk_upload + file = upload_params.tempfile + content_type = upload_params.content_type + @bulk_upload = BulkUpload.new(file, content_type) + @bulk_upload.process + if @bulk_upload.errors.present? + render "case_logs/bulk_upload", status: :unprocessable_entity + else + redirect_to(case_logs_path) + end + end + + def upload_params + params.require("bulk_upload")["case_log_bulk_upload"] + end +end diff --git a/app/models/bulk_upload.rb b/app/models/bulk_upload.rb new file mode 100644 index 000000000..f546ea56d --- /dev/null +++ b/app/models/bulk_upload.rb @@ -0,0 +1,199 @@ +class BulkUpload + include ActiveModel::Model + include ActiveModel::Validations + include ActiveModel::Conversion + + SPREADSHEET_CONTENT_TYPES = %w[ + application/vnd.ms-excel + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet + ].freeze + + FIRST_DATA_ROW = 7 + + def initialize(file, content_type) + @file = file + @content_type = content_type + end + + def process + return unless valid_content_type? + + xlsx = Roo::Spreadsheet.open(@file, extension: :xlsx) + sheet = xlsx.sheet(0) + last_row = sheet.last_row + if last_row < FIRST_DATA_ROW + errors.add(:case_log_bulk_upload, "No data found") + else + data_range = FIRST_DATA_ROW..last_row + data_range.map do |row_num| + case_log = CaseLog.create + map_row(sheet.row(row_num)).each do |attr_key, attr_val| + begin + case_log.update_attribute(attr_key, attr_val) + rescue ArgumentError + end + end + end + end + end + + def valid_content_type? + if SPREADSHEET_CONTENT_TYPES.include?(@content_type) + true + else + errors.add(:case_log_bulk_upload, "Invalid file type") + false + end + end + + def map_row(row) + { + lettype: row[1], + landlord: row[2], + # reg_num_la_core_code: row[3], + # managementgroup: row[4], + # schemecode: row[5], + # firstletting: row[6], + tenant_code: row[7], + startertenancy: row[8], + tenancy: row[9], + tenancyother: row[10], + # tenancyduration: row[11], + other_hhmemb: other_hhmemb(row), + hhmemb: other_hhmemb(row) + 1, + age1: row[12], + age2: row[13], + age3: row[14], + age4: row[15], + age5: row[16], + age6: row[17], + age7: row[18], + age8: row[19], + sex1: row[20], + sex2: row[21], + sex3: row[22], + sex4: row[23], + sex5: row[24], + sex6: row[25], + sex7: row[26], + sex8: row[27], + relat2: row[28], + relat3: row[29], + relat4: row[30], + relat5: row[31], + relat6: row[32], + relat7: row[33], + relat8: row[34], + ecstat1: row[35], + ecstat2: row[36], + ecstat3: row[37], + ecstat4: row[38], + ecstat5: row[39], + ecstat6: row[40], + ecstat7: row[41], + ecstat8: row[42], + ethnic: row[43], + national: row[44], + armed_forces: row[45], + reservist: row[46], + preg_occ: row[47], + hb: row[48], + benefits: row[49], + net_income_known: row[50].present? ? 1 : nil, + earnings: row[50], + # increfused: row[51], + reason: row[52], + other_reason_for_leaving_last_settled_home: row[53], + underoccupation_benefitcap: row[54], + housingneeds_a: row[55], + housingneeds_b: row[56], + housingneeds_c: row[57], + housingneeds_f: row[58], + housingneeds_g: row[59], + housingneeds_h: row[60], + prevten: row[61], + prevloc: row[62], + # ppostc1: row[63], + # ppostc2: row[64], + # prevpco_unknown: row[65], + layear: row[66], + lawaitlist: row[67], + homeless: row[68], + reasonpref: row[69], + rp_homeless: row[70], + rp_insan_unsat: row[71], + rp_medwel: row[72], + rp_hardship: row[73], + rp_dontknow: row[74], + cbl: row[75], + chr: row[76], + cap: row[77], + # referral_source: row[78], + period: row[79], + brent: row[80], + scharge: row[81], + pscharge: row[82], + supcharg: row[83], + tcharge: row[84], + # tcharge_care_homes: row[85], + # no_rent_or_charge: row[86], + hbrentshortfall: row[87], + tshortfall: row[88], + property_void_date: row[89].to_s + row[90].to_s + row[91].to_s, + # property_void_date_day: row[89], + # property_void_date_month: row[90], + # property_void_date_year: row[91], + majorrepairs: row[92].present? ? "1" : nil, + mrcdate: row[92].to_s + row[93].to_s + row[94].to_s, + mrcday: row[92], + mrcmonth: row[93], + mrcyear: row[94], + # supported_scheme: row[95], + startdate: row[96].to_s + row[97].to_s + row[98].to_s, + # startdate_day: row[96], + # startdate_month: row[97], + # startdate_year: row[98], + offered: row[99], + # property_reference: row[100], + beds: row[101], + unittype_gn: row[102], + property_building_type: row[103], + wchair: row[104], + property_relet: row[105], + rsnvac: row[106], + la: row[107], + # postcode: row[108], + # postcod2: row[109], + # row[110] removed + property_owner_organisation: row[111], + # username: row[112], + property_manager_organisation: row[113], + leftreg: row[114], + # uprn: row[115], + incfreq: row[116], + # sheltered_accom: row[117], + illness: row[118], + illness_type_1: row[119], + illness_type_2: row[120], + illness_type_3: row[121], + illness_type_4: row[122], + illness_type_8: row[123], + illness_type_5: row[124], + illness_type_6: row[125], + illness_type_7: row[126], + illness_type_9: row[127], + illness_type_10: row[128], + # london_affordable: row[129], + rent_type: row[130], + intermediate_rent_product_name: row[131], + # data_protection: row[132], + sale_or_letting: "letting", + gdpr_acceptance: 1, + gdpr_declined: 0, + } + end + + def other_hhmemb(row) + [13, 14, 15, 16, 17, 18, 19].count { |idx| row[idx].present? } + end +end diff --git a/app/models/case_log.rb b/app/models/case_log.rb index f73df0264..5b5dc6f40 100644 --- a/app/models/case_log.rb +++ b/app/models/case_log.rb @@ -39,7 +39,6 @@ class CaseLog < ApplicationRecord include SoftValidations include DbEnums default_scope -> { kept } - scope :not_completed, -> { where.not(status: "completed") } validates_with CaseLogValidator before_save :update_status! @@ -129,6 +128,8 @@ class CaseLog < ApplicationRecord end def weekly_net_income + return unless earnings && incfreq + case incfreq when "Weekly" earnings diff --git a/app/views/case_logs/bulk_upload.html.erb b/app/views/case_logs/bulk_upload.html.erb new file mode 100644 index 000000000..02cb109df --- /dev/null +++ b/app/views/case_logs/bulk_upload.html.erb @@ -0,0 +1,10 @@ +
~s>2DZkY_CR^-3yHCd3CL4FEUie#0O;y(?OilfG>Vr*dULd>%bI4F^ z)F9JZZBc()8Pjj_F7NnbKa6}Hp&<)i42fKOWZrCwc=J0gk&l)b_5B6cYxq_YTlK+; z+W!%EemEpbChh-0)&CH6dmIW#rB$xb;VG~x_8TeiDjzrAaHuij#;lHJhsJj3&YrSM zC^PkAad9UmDX6#S;{O;E&dkt?o(7`#_kt9ZYWuGL7OFjpO#NIlqh$u?ChD?z$2at} zaJ(#D ft8mI$ zt8f_O0L5}!X$naw?^6p{-b~t&4apX1Xf=zetbUHBoewp7qbNffQWBEKNU?7NCaJ}A z2o~vHgX#0{xMIY8cV^x`6DKoSM(*K=$vL#50VWBtNlQx<^-}$>(6XNyDW-bk#E3ik zU-1O?PRx&`C@w(LI3P w{fZkm5Fs zw*yq@&wO%DsBrAxBa6|{rAy|{1RyqB9f6w*e)*cRoOef+i7YjJlyi8oKwWlH*RDI1 z%d_wh6<0`mUnxHcU>QDvn`t%arPM%PQn9I(p8-fi8pm}*<3J~<8;4vqf?5!$Zc zSj03N!n=Q@$U_Kt2i+Y)j)t%qLW#z(`G&{LkSl%^Av-y-<3kvwQHpxjXUxo`M4U8^ zFyD`eezE`qQ->?D6oL-;5x=~ue*}t;hVAFfz05w?RJvFwpA*%QSSLAvhVy4kA!^EU z{!tcUX)SchIp`} 9p^SVZ^XM;pGnG0eb!xj(PzWViAYf^prdF z%g{%v#%U0Q6nVFDh)B_aB+<{EY;UQy!)f_wtr<|Drr(4#VVXH&awsmYo5Lz$Ys7xe zgrGxXl;u VTvsfmrf&!$2mGMBYR*=NTk_zj?pyZVPN#brl!fE0#GmQ#$ zX8COvN`|Iuw$wU|BStP9i5gZmY6AgN0}z&iK}jnuhX__-y54L!!gSHq)vJW$AUaiH zS8RpE4)YPVD9$glv+Fzd%d;mBr&U4G&VEX%>i>tR1atDgh>{y-qTXlb8OkFM#;=T) z3Rl4u(zGj@)XR4drz?)|%a7I!w?Q&y;?ZTO*2ldHfm|gyAEymDfrblniwq%!jPOYM zVJ(S?umu+aDNcgX2Ps9O))!~j{jkKMFznq(Q> +N`LZexu zd$K}1^ESV4Lo{(7+WsG5IW|*~y30sD?Us_rZf}m$?G&tGV 0$g$c$5rJ~5*@4xoFvJkD+CVa@QpX{#Qb%D}t9hEH$${42b&lx> zivk|znylJDyFP)gE;E$NF?0e3Trems*^oX=Gj~i5 !^6Dm z#(>9pM@__mQ%zGW!z^b%RT27?p-_O8nunT_)xZ h|$5=EJ!ujDo6!y->Bw6WHlHjGin-XgBgN^?34u;^EkO6`x(NP zotIfz!BK}4dOHrnm%4k6%Jp+%X7i$8Z_(@>r?cI7wE9RUE|3xOQaU6~VY`Wfx00#A zeKE$9f?0Y939|tZR)z7-{VDrXsuDb{;}YEa>v~g%K&ojIMQ~}-0ZmqONgs%%T|*J> zDF9+oiRe_@zf~w>>PcQGW9&s;`tAuB&>Z62JBb0SFR?4Mj$3JIRdJiKX~Wx!I`6R@ z a;r#}{ip&{C*g+6wR0TAs| zOX!#fV;ES*$nzHbNMA80Kj4OaH7lM6UtvnH%*p^{9i $0y9Jm3BrSH3=8-fYY+H_%Ek?;L}JjeY4hf^I~-B61ffpz<377VLj_e^ z a{P-ahkr4}`X~z W5$C!)@W|B9#v8AotLb^S*~ z)xBhan^zF=Kv;+Ly9{B!eFo*&h#vXk-Y%`dZG4zh1~kk~ejkh|$2_# W-#qDOFc#R_^;9Ik4>^2)OSV}W32~S-XHfSU1CZ(1V{j}D z=?!U6_W=XHYL*DUDkZMmtXSTzM*+0)L3^H7;t73U#GtOif;^Hc?d&tfTM63_AGF~i z<*6m65$fs@vZ#;S%s$oaY!%JIa5&rbL< l>d _u&Qtv*P?WM{6o3zgNTCbhum-=kiw?(b2rT=$<*k8c?? zu!iL>NJm(%YYh_^4mbO0yy<^nX@ULYzg{C0j 9hv8Hvagu-IDq|!en&R5b_p&z?nVO9G_ z0v@9OnxIfb)#jnC{}GnmGas7AL>6e+D)c;Yx*h+Xo}zyfh4{w*8daDss*DU!0l(`X zmDhzbC5mONxQGQ93Sc#ld1l%ekWkda*u?s5u!Z#*izl`2D}5r?=i?sQDi)KF6pYF2 zrZDY(F{Tl6V%BD6@8FP1fEl8O1Q9{bM7$7YV7(C071M=eNGs|g7{wVg!##e6 zD5vh8pe^`jhgccCPj)_|GZ&_WNvSD4goEilx?XP1x$lT-2$b9iJWeyni|Sh-0)}Cf z;8vS^7J=U*NGJjEQHj)4+ffLDRNwi8{}od9e}&XCBCglpA?5T4&wJdstjcD#QVV_X zHcKzROs!zD3jmKq%H&ZrSq~t`J!Ixl``=ch0rtaX1XV4eu-XrF)fWD4tfz*nA{ zEs<8WhlYFCU7~$n>sc1o3xKaYGrK3P`UVXpU={+6B(M iNMto|#>cR&|B$c-Ot4A#8*|qA`FQP|Pr4c|dnvG)SK%XY;Sk0jBuq!7Sf5K_JeX zdKd3 z1HY?g`tOCA@@RS&z*9Oi#-0xg1akGzH(n2C<@7Bo(I=?J&(M=!408m!$*W>5D2;!h zwma#^vu^@{5XqZBKhw#+j{Z)2x2T{WuljJ1jQw|+SOPyhhi%Q9+(oHQ2YpA#EqV?^ z6#aNmMx9APR;(Itn$xiYbA^6(M*j9RC*oF>f3$s&-xN=Gf!{k=q=sKFSmdYC?F=a% z#g sLS$abpBWUHBsZ$i2l`?P4mWQzu)@`4|bEq < z<5Y^ETAig}&h9TqUw-7yc((a3fAjVE>Cqe!xv(&t`^EV_@^ PC {P~a*GV8VO)y%0LzKcAlbBGef#Slh6pO{gQss_ejC48fOYH)y;P&}Xyw zo+xZwG_0UfzA)StdZNxgdMQ7X1v!qZS#1d2^p;tz-TnJ(o#g^5nrWebF2@im; Pp)bEy2J-_#x2G^Swe7@rUezpSV@3R$l=5E$jnjUWUZ*Bh^u`qJhq;AE0$uZ5R z($@9rcNDR7I&Kd{932IGhk|eS{*rJR>vZI0vrTLdu{ZM)r$uXe7J0%h@9pRm`+kK? zDx?D0v!y%lZS#Y&Y70-${VtbsU+YLU-%@3B-jV?}Q;bqo<5XpH0y)U!dOpmPE5_P_ z>&C&k;&}PXLa?8GHMT_NlCGoW9^HCv?TTHMhW!_t0 v26FpUF=N>fAUrM}&YyB0^u2p=8n>{|BWGYpsz4glh0y>?zDb2(gV?{z(b(pQ+ zb>jEh4}9MT@{6*6KP%Y_e^|ZlNdNXF2mi@?=Bl9^eCJVjG(f0^9Rb*sw-2DKj+#@m z4MB#}q@+)lmJM;zQ%V>TwJun_?(rehqikgCc7<5Ev|}b8z}}ca*m7Lt$^aHY|mb zG6EAjb54`-eLa}b4P#~-K#z#TCR62(E7+$fvx(pompTzX1HlOSghVJ9Sd*=TN0<$5 zb?wngg!^$U3I7G~{TybEbA)Xeao0-?7lNLEkE-fmZgk6M<6ba9<2^cy`~e~>{I=q? zEo@~Z4VRi?uuMa~JeE5o@?%b^i#z2s6%n-{g|Zqgj&j&HO}zyUe#UujJ>6kx>Abfm zM6?58mk2SeF#uF8IF+rcvhq;|#=1kSDZNlGwWBhk0F8KusDza(Tc&)Zd`al~*k})` ztPUJtx{}gqqVUV%UJZLWN93!@2Kl4vJ2vdHM+3*Bb31gF4RWW@NOZS2)kv cAoVM_znK1d zgOlcm** !q+G~l ;`DeK! zh-$$!hA=Ef?DPu*+rZXYK|_p>3nr{-8Z{PC2+HyaEH2@N=Y|g5>^9$^21&?>>N;5A zbOvW&C1|MxAvps_G3O=Fuz(- I^IzH@`Qu;ytv}x zEKt3JW2-QyWN5d(h(6rZdNv5rVX-?fqU8=MMs7?;F;};eQL9abj-y41afJz!En%7q zGCocp^N+>O<#z9JEQ_y&nsLy*<9kR1vN+?57?(0`TeynWO-uAZ?8s^zT%2hziT(s6 zbP+g)Tp=gH2)>%z2tsBd<5KCw3 p&irT<8jMC!wx6EudwSvIL?KW71sRE*6M;iW z#8tRF75R(Ql2KFg4PWb6#tRg#wng`$rLUoRS>yM#+G68?O+_V`_cX{M+=4pWiC8oP zEE)1WbiSO8I0^}c*%oA5Pw^sJYH8)S00)a)JK-N%qZ<7_WJQ$uqOn%+$YO%;E5rty z8unA;+zRz#(`*`tw_dK|cH$+r2x%ykX0fJo1>Uow;(d})E7&h_@MAP9_C%V!?-J@% zCN?@L4XI^Kjde3aigZ-MXnYuVR=SxAkC3q`n8Bs`tZ8UlFLs@Ww(ntMT|(+(Cq*FP zqyCo7Vx5q2LsexZ{~BIL`|=o%06#sXqt2A^8k-0I00N^wTp{A?J9Ay8nEZ2i1Wmjb zH_HCY&ka1d0cm@0df&2Nr3;m96EHP13F;Opeg||Y+om3iy60p#mSS-!NR9_&D7#!6 zgeeVOAyzDvhsT<)M=gU^q}CqD?b@DaWV7EMh4HUuFkVJsarGObk@2j 3f2)h+RSu0=4!;-^xtc47_62)Y~ijA0wZ2!w QkmJfSZuXTj)^x3@rmTzlh`(dw^APtUZ-tfR!Q;~m zgg0fqdst^Yf*38pviuaHI`dO7D_=kPo{r7axq*uGt{u{fo_Q!`dMZ1+q%0}ACkqb9 z-@Rr>d+;<6>lH%OV{X1VB9<8`!1`J};c}XlV>Dim5E NoJ>>j;1-(*KI z7o=jw;=?i8wY;(Hv+If$4>oGhPQ9;gh=MtLUnkOY1 Y4b&M^J@rJ6~S?CLBl6n*MW znv65pB1d`GRazo;neV3&; vCpud>2TIl;@5tKHftM8@ehNiofkVn!)E}x+|G9x6&5Q9 zpFXqyER-&Ve)uAFK4|%^kgaDx_X%5+26bjCV(f~s-2WVTf0-`~encZcXXgEToC&AW zZokH9CoSY3=G+R-TuLiG{GVHYb?BPun-ute+t>U_JPffzLyEI)*8Yxh7h{_kcg$i& z+HAK-#2LaiQu@mbbgiX5YrMo>KM04ft4TRrCUO9atO{un{X-Yf6R63)GC}*b-U4|* z@+Q$R_cKw`DwNr<$XKKTHB_HaM70R}*|rkC;OX(%tglUBa!4vyBu*qo*DhUj^=z`< z=Ex7rs;%;Q3HEGN<);&Olf?}(=%r=GHaE(k>dTrQRW8PK(f1M^Aw>gZBg9KGX-hDr z1ongYh3jXH10|OlFeep1eQ#04C4igGt0op}lW2^s;Rb@%>c_l&j}do$^aA!y{7fh= z`4X&Mp&SbzrP5Bd#cZ1KL@JTIPTgV^(0B*i6N!1G4fFEb_k*1p&)*&a1904yKH$j8 ze0o~n;0}#coi4HyVP - U{{hC)=+|U*BPymEF!>>?Q`vivb{W>`5}5Z>VALuL~3x)PC{HlCI%fJ2@28 z+^IIzEMgn=X@8YAqcR$T IvK%)-!15yc!hdoG(FJN1F)gS|-O)-~>8SMr$NCIq&a)O~1h}fqUkIES zpe~{VNPkvQV!*4t8+D=?OGl}{4Weguh@OhVcyq3CqCBC;&g(j}=oHnZx0uhw;-O9U zss?;7^58KH1!5x;%2Zyx)r{1;p!`|JV_V)DV#Wb7STPoIF)z#_EX3GFf6VFMjb@SX zKlyAsWtar}6<)&EV#+iLmY|LlD<*C}nx$K6#rX%7UR*dr&&NCxN-}d^vJZqsjjb4E z>oX5gS7-LmdG0Pp@T>lY;)@+EH}z|+r8o6P22*znu|z-D%YJpn5dqJfXI;0}-k=KK z@;NMawy5`xM{+rQtq`h>C90{IfGzw-Fc$)Kwf9ohInB=OwNMsESf;;kR MxUBH{ZeA;gG zv2E7z|1`0S=Qm(@c<&+(&pU?ZP2S$~NHYJjnb3Y+difIJ9ef=$GT{G1lJMqXCV-(= z`PDzVW!kY>RIMBN?&UP0Ch9rdX=v5FJ)7xE3;jh|RXgWvNG9bbQUF}TAptwqUS{hI zG9*HQp%*@jbCZuKp-Y88n}cbW^epBbU}K2FGC|+QgKUt;RqKr?oyi$8-t>{0Zo}w& z_l1S*U^E$M+Mmn}G;#@_heZ5kb)?2!Rm`Fg{>WYv+A(>4u F4!eT3dwZUQUvm%AJ|Lv`X?c;g&Vzg%V6zQV^m#>;JXwXNO zWS1i?AuoYE57#GdaIT)-1bMT6f-96PL21f!Tl9g%2q`oB!y1a~U3_A+*wqtHjFL)j z#QCY+vlF@L1L6LdsW%;)wLpFPxEPOa)}}3^Q6L*&(E#1I&B)+GZh)sk&f=L?$@$@S zlE%f;K;BOKF<#LTAuhXO@bl%_J?jktyRhX4jkM*qTGN-AZR_;bhAYPrw0Wr^!6n#7 z(vOYzNBWOD?M@3k-B -{wO~@dyTq z<<3I2mx$LI-<$v+d|Ifs109avw7<4&t?E=Z_7ml_*|20g#aoFvmpEjWJ=NZc;dXsS zY&LYan)m{Xet1!nkXC7bRYNwegMDr?zEauOWa#YEJ+trRQ@ebAy5jJ2so|A0(nZJU z5Hxr0>9~6I`q!$*#v5svCJYD&JInuV)dLf}>S5(#spjV5>dtQI;%5E(gm9pSy6SiE z|2?_YWeFsAZn3L-pOn{wFR#>R7f`thIoIsC486U+g371 +xYVurr=6w?qNB z5n6MSehx{EsHdH_Tr;q3$mgUH~_Sh@T?{g^UQ{vD@=O4)L6Xf3JFEXF4Wt5CJK zO7l@FYDBX{u_&QKb^l*8R~Z)Nwzlb%4k;0k7Nk=^a_9~Pq@`2Ip_FdvW 5y&?!#>|3wtIiy*}uMLuK6+7^{#u?v+nh-r`Gj+s`^+LPN3Y7CYOg%4U@p) zAcppVfl6u|DXZ`~rTAy1y$Gux`D!0jaOs(KSri0jb`yhUWVu~Z3zC>a+UvYZM+(1| zKJ(fL08d&KcWas@r3}X&of(*Oj8*{Y;WG#xAYyfSiPA>WlA*8HblJMwg5$y|43aV} z;*2bs;&nxuw!dJV**9SEwU#qs@>}R_nwt~phjJxhdv1^Gy)+)ge+$J@l%ZugH*sHg z7;GxNoNAOGeUCfzM8}La`y}Tz!m2Nu_?nI*p*)Z;7S(}qJ9Q>|gV&?NmOV4k`o+BE z@FmCSiIt8DPQ@cMEPxO^l9?x`I~+kwkTg~B!!xNj&`?l6|29o^$>=p 777A7RJ+HkTS))r6|;i4J{(e$x=g@DP+~? zA4WqMIizVVDwC_DJaHh^oFA pYT9&O?lc9ZX9G+UP7AwBI)-Hcdy>4!+5X zB&r?4^Ykz~Pti|?NCT|(V1f1}3G$@d6d;3^>UQmquVFAwQDKmnxV}9FY&p3yvZot^ z2J_EOF(8YXtJU9xak-c+m*2n6`QhALOapscJ8MhGdX`7Yh9)v&Leij}U%HwIdc@(> zsRSu2WGgL86s;e_Qm4Wb>(P+s9Z#>CNT;i;WU9VT|5dxmmQLu0Au>TAKGv HkIy+Gebr`IVO%n6a!tGR`!(ok zczf-$u-&Ai(N92hd3jG>Py~u}SFy4P7pjwFU^A|{cY8dqTUI>Fe?=FF3i@u<2O3NC zX=v>SjONt@Hy(JmN(y6oaHHv)kefrpKHVbQ#%e|-Ve$eV!xBs`upl607i_q?E4U0A zXSxFWDvDrTnmT`#Hz~>{ZcwT|E3F-<1*Gz5T+n#)3%7bco?OZ-{aE`N06Rp4l >zZEU(w22DuojE4!(3O}XyS0iBQEILC(TcXn6ici>Fcxb4SnSJz4X z0Am6lh1UlL$66fr N(6%RQHI XVKIG PK*} zEedl_P2I4a%ZeoYOg3rvYGK=4%@~$!h6u6=d^62+4Qq`V2PnGLM$UT3_9Tr;@bF%1 zhDW|S(3_307-o-Xshf3yC+ZJARK<#jH-~=AioQEpbv_y=fR^f=T!|a2d0tPW`c8)V zRTMFz?W%C42=aL+mhUrP%eOiH9{68eNeHmYps;9I%ni~OIK*5x2Xo#gFDc+WntYkE zi=E}BJhWRSP375TkT5!uK#^4q(kS&e9aA=s(MeF3ls_*$#(^y6{YHW!+{?v8xNI}= z_wUXhugnWe3no)LL(8k~66+e8wi6ZD?wdt3TYS>B0TG$G zSC94V($5cD9H$Mc8^1@6M6RaGr)(wHNI>V%;J S{HRLJS^&}6RhP6`{YpVJ&Pkp z{Uoalhw_)~1ZgS?35tRYSy{Yl6Mu?lGXiLo=@j*jj}03+Q!|8B8<(bI^75)^FSMN3 z#pj1IYv#D^X64S7PezqSF>0VM?9L`9TfPtNeeU%Fy0EeQJg(zldA@JqY`t1~0(@{X zog|ljZXyPc9>?^f!x366f?nd+QjoBcNV|O}4g#X`&aj|Zd+O&_PDWdi&nnq5mi@*% zM@B)Pa;GhR#<-;mkDz-pi1Mh}JX$ yf$FmWk)jV 1ctyn}IK< o-FRUF}1QO zuq^c=7>a5}-^3bx2AE9pJsC0|hWk1E@)`~d`!Qk%X9re!luSyew-kWTj~Uj%jw{H$ zBcMlu5SHQG;-R(%i@i;BYDs%UwBCxjmJMc_)E%2M4TX_9@0dS-4!tn-uma{`JX>c^ zN{%=zu6;I#?J_3XLI2j0G3CBv(mkbO&zLHJPA(U_Xv4q{<32tf|97^!1)a(T@vRz? zjt=U1?+g8_2+uML606e0nJmW2sGC5wUME$FtUoQQL+1sCc!vej1%-c3HjSV jx0e1%R|C#himg &k5F$!mw8q!oDn~a)J&Iov(X*H1^M^7QE5tJm4(E zBceWP*vhTps *xK!6+)OqS(+(WtF2w!?AT z@UoHaVnb=U;_+o4rbRbKzA(e{J3Mnv2y`wUhHF56)3sXa8E5)TKTpazObNaoS$M^~ z&ofH>nbPGGn3qok#1G5K_?1gOaO&FpvVYXS)P7O8)?cs&22GuMsqOc|zDs-4(;Xez z7>zWo^whFzB0mcVh@7gH?xSX#z_20Di^k1$_s0BG3)BAqM{H=*BYQlCnBnQG=c$#m z
Kx8hR X6CQCqJ zv~cnKtTg{+!?s1%3T_wrH)AW&rBbS5E*mk1=kNI&2KlPw`JGC9_I)$_U3l5!iv|LT zre$Gx%}A3DlE~Lew)}y@6cQlU1l+S}3|Jap%#WE*P )H6v0Ze1e6mnw zZ)EL*)!vOn&bL%*u3c|FXV}h`w@FhoW=pV~M>$|wxo5vXpNqnsRzVCqjwaP{5?nz% zk0szY4ECzkLh3b=a3arl;N#Xdl}uweQ#z%ufjRC$pzWp~+^G^>{vK@gN_u@a`OTbh zoiRmxE6OXpps!eg-$fSuHhd9Krs-k$Tlh(7<9+n+G3Sv@JH8mI!c~AtpYSujhfjvm z15dT92ldY91LIrxapBjGacA8MExwy+uzoWa$e8x+O4feK&NE2PLi20M$ V53Lc#-z$7U( zd0>M9t3~SPgMgd4^~7ZwpLdpCKIf%*8f*VbkDsk)wqf#nVWk7N8bWTS#Bi7g%=gzY zw}cgittZcwg?K_%@0tE8d|b9+u{csqt%q%~87QY4pQy7GgTOvDz6XVqo>KKq{`(4z zz%q^11u`g?3Lm62^NRWSL%^W&)Eb@jUR26%ROQpIqI9a>V*P**APjt+M~}AeqjOfe zNOo+RB@PtLiiTSBt(oAAZBuF64eoQbK0Aemm}BE!40Sm~C@2qxzca^>j!(DE@l{NB zWs}i*CQ-b|px|dt_ =*pwncN)U#kA-Z(|nHTSWii&O*E|cE}=J`L!XTy(pLAyZ5Gm z*jKRZV+smc7pzpSa1?Ek8cVNUjL&Mj>uAXgPFq!_Y}3q!XY4pGy8h$p&|)#LBKa?~ z*U4talgC_g@>PLNl1e45iYJ{)?EvUU;m>SHNBsfqJ;1W%VX!V`CfmL5()O{i+p@`< zPKQQ<<+>$VIG72AP-FFc=PeFyMR=s31T>9f#imh`&+&=zeNCBwu`XsZ b-xs^dPi4ysn`8~q&eGPXr52@|6lV>(SgN(uKCr*|1;;46^K}-<3UJu z0V-QWLzcuA?h;Cam0V6GGfhAG`dnL7dX@Xvgal~KxpCFegM-WJSReVBJC1Lp=5met z7-2 DwW;lF PSJj0=BohEjbJXHs4<2WyV|y>h_PUlEoX3r9M2cRFz)sYOQd z?FdnAn^oo@l_ Keu%t-o0(!`r!MjdZ` zPWHrs^Q4}_xSgv0uQHMIo<#1nsSn;GSL?bT#yIEV!A!>p@C;zrS2Co1 *#GcE~UV`<_BQe+xO!UabhHq+Pz%|Lt*p^*&qneM0wS#%%vJnN2pCN_5{q zN *DhBkC{REHvP+6sfAFbo)kHX}~IfV;5A}xL7)>w>feJ3wx2FIjDSJfY4MIq+hHdisDA_ z9OYap3j4Rm*Iy8Nr7c>I5b7Tm+TF*Ai?P}471oOZH!rQWkKQ8+#9p5+1VT6Yr@rhz zw0&2(E%dZoHyH~lD%xt$!C>nsP66#?W9-Rny+5t6op!GxVVD)Cy+;#&`_B8 oPH&N)xaz=ddr9acoR&tm~eJYLftm_xy}fFxWhdenq; zJrde9)lqgJzv{5WTTI6*I?u+xlYjUJbY2-IpMHMSW@cQU*-0J6jbv(BJcCa)Jj?Tl zg}?Um!gm1)`g_xwLhv*P2OhRrJ1*;y#2kb0PspY5C|e2WJF>AW5`CDNQ_!*o#Zq?Z zI_5~VkQXcJ@Rc02VjX})36UdGhHn})3V+Fz &ad%Ny}t)9kDB^125fWl=P^Klc1Hhm&6@b};E<6>6N(`M99ePn0t64Lo?ibP<&~ zdy^T tg3&rAPn%O$1xwRlfj>ydHBCty**V-XcjXvDAM z8q@smH|y$jM%{N4YBP7b5{O8K?@ N|_31D=bTP1-+1RM C6#Yq1@5v(a oLoV&LF?oV$HcnMSI}dhYCcoAqcE?Y zJJ2Tlin4j!J3J&!Dal(#6)KitD!=a}zlW=nPdrmJC$R;^vv(g3sl0E^jb~0%kcSe3 zpeNt7l9GiRhhYyjiyFFsb+={VG-#( dlKQzjMc+mny#VLoq>-g(&AY#J;^wW`JTxtL5GIq@`$Ec@^ zg?TMAG!dwV`>M^$VivBNWi jA5^sRe9?t5^T5-9zPf;LVH#7_`)OCaWn{CM< z4f1cxop$+m2j6T}3+Y^bEg6?3j?2N lMmhT!X%gbF-8Nxh8x=*#A)E?xo?o2sdjxkdoWA?2F#0=lo9r?(XoL)eJ}h z=vuM?H-`Va9CUZ|&Ez v;Dr%FV#)_B3*p@1k7!S9bw!#wU>Y>soSDZUX!+8oP^i z)8oI5m8SZ8tn2vZF4j#S`ZiXQ+V8QhBZj+JH@(u^STWCkk9F;X-^IG=`Q4^1?8Qwi zNZfiw+izF>rdxHJHYfGFC|7HAcN#Z6mfO?N)3^(8HI3h*+_e9 x3Gj{w=I_?Mra%Tmrm9mubL0dpq_Pyhe` literal 0 HcmV?d00001 diff --git a/spec/fixtures/files/2021_22_lettings_bulk_upload_empty.xlsx b/spec/fixtures/files/2021_22_lettings_bulk_upload_empty.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..f1b7de0bbd3a9f1cce894328a3c5e2df7c2760af GIT binary patch literal 24693 zcmeEuWq2G-lBU37i jMw@+w{y3*JGV3U z^H$H(74b$!WMxN2WmJW-T3!nL0~!c4G&G2ylerSe|MG_j 4H$v6qQj7NQ+9gvSMR0(J M2w`DOtgv4qm4!W`0Li=`}0xoCuy{x*~w zF0{=!=c?pur-K3>$N9xZN hHUS^ktugh@r^O)Wij}d!|A%vQo K>2dkyG^QASSQ&7MeMM>0ayA`_M4xZWN}%Zc+{8&mr(-p&a6DlGv~ zd4Lp@ftqyHl7Os$`!b|{F5O-ey+K2G^~(Af`SY;YvxQfw1k7ucj2`T2QiQF2rYM*K zHFh{VD3!>Qb3Dd-KN3=Y0fe=(K=jlv+1PLnDR2Z&icimYu(>{s`0f_5c?w?-go`DA zakix>HsYMp5N9H6Z>jz;x3vnei)P8-lbpqFpo3aX5uPh1R4+d%_C`ANTfH-GH(gHG zy$y bDp 5l beu2ei_tJ4#W(kp; }y@n7N4=Ijk5>?;qa!r6A6Q-ve2~x&-(|ka|a)3 zW?1kk(qvmaf`y$9ZxCcs`?RL2=r_Guvwj`v4(`$SM4T-fSsySDY$VsD iEf(CpjU|OVHQN!x-2w4V$p7SRY(>W+aVZ2`Hr@5&SNk*q`~Kx zq!|Zu(WJ;Ucx91fj1Kv#c6cw_4AExBC 1;5 6~-8A52UK{o PwS=~lu;Q wt0fhpt?pL<>MEKR}K7H&lYG`O $X2vi_XF$%`QJ?TYs4!H-bGd}`^s5hJb*20qNMaVn&W}*&r!=TwGByoo zRYHIg(qKUw#*rem(2k8TBRvpKEa;JN^PdrqRV=6=Hk!elu%yz_5{d4gtH{e%ohY5{ z`4t%}3|7O)R*c^;7v30#6OF?dG$E`N2Nob=a+||*!X{UHI0K2PWbs*}&aqB-2-{Fv z$-B2-Oy4Opr7#4vS6ifvtlGCUre1_iesGZ(m&$Ms%3BH jHk18Rg<%{(Ej z#+od-x9n_dd}+b4$ro~IS|wESeWT00>K7=Mhh(Yl<3uXMX=_8co@2imX&Df!Vrea8 z|L#r_&-*j?BQfNshuIYBcyz+l=g?z(lGFm%WBkyewEo-Ijkxmu!qL|GzUcX{kh`!F z$MjNUrJ?ZX)k(dgVOR-qrITwo3;sgnSC&H$4aJm$LC-qbPbROM{mdY#Af=sXqx~Sv zg8Hc;x;;sRz%lMh%1Lvkrf@=@7wRL&hVC0p2Dm2Q#5LuP#u{tGZW;2U)vO9*2S`Vb zNO=(wl_tZWzU&CjHYN*pYN_;+zC0`T>S@RsqSub3q_UqqJGB0yTpzRyE$d*J?XynB zn}nQH6%`r7(QY14XGpr9g%EX3Pg*RO`>EB? 5mjm=9DSAU_^- zp o58!E zTYSO>@GQTi6+<1u$LYNYy4`;^IQNjY|2R9z P%)ylnFzxfdqjW|JYqLkawXi-BzNd+2i~hJ{2oR7KvwxH( zu>O84V{d9? e>q)yd$_E-vEuOZ#N+K;qwl1$vVqU$ zk2)pH=5Jr?=vaJFvv5Chw7c{0zGHPG=vaHTxNmtmLwvhAeKLLCJKEc@tx3#Sf4F(M z-5c6A+B;dAi_Q+#J^i{C@ik~-C^mTNeq@F8;piT)G?#pFe(I&VKLS{iES+(A+Bv;& zb@vL&yddAh;^pD-!h77`nq%nM8+s<)+5oJOHb2izL^D(#EWFxxPCc9sIIqQP$y8F< zhw|~)@aQ%}?mb=Jf9G+-=apXd!1L75##uFbvT{G+>-1RO?(SHQe0zSptlaKp7#e%I zxWDFjuyBf8B3vDl_2OG~bMZ*~I6#sA)SDdKT0Ey`cf4~7sN&Ah?i`Jhs}S7_jUn)Q z+E`f<*+4uyC5%UtwOYeLBwV$TaxFI4*=04xu)E@TeocG0nmyW^x?9 A9z+8Wr?%aiTb#oXN4Z;i~ljaMeMG(!9sBr&e2_k4d~Zx=bx6FhX85Z}t*(o+n~ z+vj{*r^iRzRHX$^bIJUoawDkAh=ivdd@@DJG0N;X#gC2KVQV)0a;T398Ftsq<=sJY zasb?w)kls =H?scQ-}a>AO2~v+5c%>O`N}R%d>6cJv?S^;4HtYg2ENTZB_v zlnoE3yIswxY3M3zn5ZPJ)g5+r4EN^^LzdQPh)0fzM9R3ucXo8l=5=6*6^Ce9GW@W- zq7FCxw4=BPmmpqPE~F{$x^Q(~QS5`EDBGIFeB=&$2s3PUrewS#_Y)~#*z8WJ=tZtP z1y^RtIQ3TGH~SdaqKT0Ny;W`^d!j06V$r@TR!g6{rUYHNyc688HCK^xNnxL1V@t-T zD!sxS?((98@@WYnLFcrh#z$N&>CgCeKWW?hy0o;$yY`!OMk8mF>=|ca8ep--ElTr0 z@0zxzY!?O1nz~aN*Jwy_KjGNdEz%aW&rC0 ^h~=;O&08+>LXr0Jh><0|>R;83 cbrcz9mVZ;n@&m9@m|{}owut@P)kL`$t+o0XOWkCKb~Cj!)(H-NmW~y+gz*Fg zr%b8@t+s4Z{3!r3^R*rhdmP6sdooAP>_#6+k~Zdmd^HIc5VOluBf`jRZ!l zjMWL7*C3Rp=+&j(ipeYzqSx}wN 2!%Vs^zooJOcUhxCC7&EZ)=%FPj1T5i6G5LatC1OiqiK^)CrrubOa} FrW2gi zEFBu7PXmBHcdR8qA2R9o5}#dXfN~S!?hu~+2$5Q(Er5=0%{F;{xnOx7Z!xezuYF{0 znQRulKD_Doj@0Qi;%YYfWq)&>$AnHEO9JtaGKQbqs0qa6%{z()8Z<{?)I;$$g(2q2 zmDf`I>Kb&r)> Tg<_te~Mt%l*{HT7s-EH*psC|44;%7&X+xQK|!T$(W z98;ODn!P@0V@n-4(|nwE&(6`u1D`xHavJI#2%v;6HhNPoik9yyvrSOT#!J+AQ%381 zZ*x!{#c^wHJI)&Zd0P7DtvUHB1_<4mliiUFozd!b$g6pjuk5y|M>3!p@yb)&`HdlT zaS5nc$evg1>gRc3F-R^C_F7d5sU9g(%9Yt8 !SrZQz_OEsxcDhL49r41d0pV~YD`Sb3IPzE!~+>E zpgH#^BBEAeQt|>dah9@xXIP66qF+L3`ecUc;s#|wXmDmxzhev*NSKI*gwhN@8)}H> zmj$uHnI-*BF_~uk$uv=5q-u~Q0VwO|iEq$|bS00h#ki8zG@;u~Wu4F*y&?+@bv=zs zN!b&DvobQ!&yqkb@8gMQ(+me{*1c<*y=wy8Mp?{?Z(>ZyPU;9Q%L+1Qg|iat0B(b| zaZZ|uuBy|rM?@3DL1X`3K{P`q#%VFcmHea*`AT}xjAS>p4j}s8S0r$FvbezEWwHTJ z%5Bxl6aP0w>?;ou{l8?uAv-Nbyc=V`8?(F{8^0R^&qD$fURnaJg|i~-0IGwvu}+I& zR|eCvT0|3~i)J+23*y{Xfjqco#pN+32Jy>(S)jjnL-XE^>U%dx!i!YB!0| QAb^~Vbwx>h7@Kl0N885^MiMiDM!w>J3^K_&M;$SNkFqi6qX4p? zgaWf$VpU221UDs7Kuz#8pCl9|{`658;WT~bx^Ib43vp-_H%6OH*MMS7WK3^sGiB{p?8%b(eAxEf`$1{$)h6hIy1mE| z0m;^{>d=R@`O|2J^pftBC;1LOim(rh+sLYErj;-jGE{dKQioNG4np~xR}WO>OnkZ7 zOn`4=FiSCCCjmWRFpk`-PRU=tv?(8F-r$f~GKRHR- ^ dSgB{lp)#9@%^fB~E%F-7&G^mo zqEJcqNTfY13rM839UQs3dyUiv`uFDVGh}y3Kx9p>z9Y{es@rWcR{+Ri_wr;|>F?YY zzswQ9Z~0XM$Z@zKie=F7B-2fwiubro6k>jd8_i5dNhP#U>f|~^PQy#`COlHE {>vbyNA<_$Tu3^W7aWIA6&3f}>B%*>7~8&-p4HUN(3;SSG3S`JF^r zDQ^i?n3JO_;kk~bFWP@ue`dRHZ&uXE=jo7~U)xqiSYHm#!J7*IIFAR&nY7BFt#~R< zb{O{CYz=3@L0jDAgolmNoy@V7NoJ+wSTz#72>gZqE~+)`FMP$tL9;xV%K0Xh4H+1* zHk}i50SX^>b`>bvCjzmEww2ad)u0|4{<_IZ&8Jg4s~$S?S|gRe3AiDM42zyFi&Whl z?!=ocxu^7OlGs9-O`*Af_pO@M4y}aV4iFUymeo-9koaW6O3mdUo1Z6)`OY*;yiw}x zBkaJE&3>b~DD D|bscdH0CaO4LKT4lroAXM zc!N`jn}DQSxCOU7Nr!>wW9S!`SC> `wk|GZC1!~_1~bMKrR>)rRU zyv_(sj5ZN7kT{0(@)>WZ8`;*yCg?3udh`hqqG ({2^I1! zq6d(L1d^T!)(SXPxM4O<+(P;5xS$-K6emMV{DX&!H9}SxT2DhYLO)+elF@e)E9hvH zH=j&1p;YGl^|PBkYLa6nAW*jE98V1kg|7wcL*?N4*?kyhqNK1lYM1CUc@`=CJyO9U zKQ7n|vV)R9PpOupZDSkAF8>2ck}yF_9x^8+r+^Y$GD;DuP$%J5eo^cIq_78`nT!f; zq45~5G|6=&YMM&YgfcVV!FQVM{}{AS0nq=3l$?_I2~G-&!e)ep;0Xu{TtYA_st#IH zRw9k){~06P2*1MSurz5U+(_rxn&pbNkTZqT;N5d;6HJMp>ku&wBRRi^>+~4Z-l5lM zGk>O)yyED=?~({vo&_uLD{IXy-OxdB@R63uYtNt&=urbOar$fR?_YnU&+Io$HGAaS zJDw{#D@eAc_fL?2>gW_yeDV3Jn&HO4IL2p+pPQiHaw`sE^P*_SqO`1JS9tN0QnxCD z$_Zf@f-*VBL9iKY2PXkn-Yw@^uo=QAhI&COQuhEB&b+tTgU6ZTWNVk-Yhrxqs1`ht zP;QClguU}oY1K8ezTQ0~JT*L}KGfm)coai0u|DU1vgM s3=WxGW_j8hsh{) FZm0qM#m0QvER0->jYghF$Bn3`|-!&ig0b1GTiUUQ0*YLR)bBC+ms&&c%Nc!ZeYD zsYhl>-JV}I5n<(uEDkz<5AQDv4iPF5cPsM`{OWwg3Ks?|;hzmK8?odZC2W}J h%tz~@;o#qGCA&Kf`h@6$E+VIWAN2?%eAMT(q;U$G=9I*w)XAGMN=7F{sK(l~ zDjSHdw55aaI#D3;x;HHY1gix#S`=%35NU;^rR2-4aza_d !nXrAsJW1kv*(sk@C ;7*|1f-ilngyi$KpG3Ar$Cwt zq}L-?2ZR?O&f4z~#g?WY2n9j!Oy41f?bSQ<1$B0Ohd8#a13>r-^v?Gk z64?3=0wE}vbJRN|wv`%sw*|XPdxzw L|hk=lu<5xd} zYaj~= IIqEG(_1wf0;<)w8;O;K&GZd%p$)l* znFP|3KAl}(-MH6B$K*=%<46Qn6~5JKh+Zl($*c9s AEV&G~JbW>YMV(980v|=nZ5Y2QY3Xf|jHGrAr+7l$zz-qY_ucldBxOLRs=G#N3 zh2s^KthV@$``U7QxEH1`>J3)4U?(E*n-*+V*EigOl0ePX{f(!E>2qw`uLu5?X7go~ zl@7P{5SEl}SOSNIgo$6>?U6M`a0PjuF2TI$LWBp|YoNY!sg`-te@I8l29yjALWKKS zg)o>YU$;r+=QRRvx!qi3^7GW#fy{Ki0@Ix*4!XU!RKETx6f3oLEh#Om6ND-by4{+_ z_4k`a%^B)D3!9Ymi %uHE{HR7*0IV}Ct47Y`KrOb =Eg z5H#cjG&12Kg~Ob>@M!|cCrbPrhY*xSn*GY0Uw#O1nzZ}XIYWN*aErA2wK-FMvT&Mo z`}H{se%3TC<@*hJOJV;4le-jTjoV_i-|$Z}+$|^j4f#t^*8eo)WHNSAU#k0LqSX0V znRQ zFGbj2fyFHFFF0&PV#Qjzupavx_AP+U0hXhjzAH2?e@4&@v+(^ng-hT7sgNWgN`Xu8 z0IiTEVO*g_-~gqNUQ&%xC*Q$e5wVa@vXK&A9)$?!LMvoL2oF=g8EGOgg}&F3r~&Rm zC*(pX15>{p=`1jXq1Th>7Vbh98bk;h$ ad00pKR6pxwTx2!HVucpMvD zFi^4_w*LiDrX#{EMefcTHw+zk)BpKUaPVHVkX7Ik1{SH|66h5U^Be-FKTEPx-pC*3 zJA_Yz6dIz0{eSGZs&@VA_(@ow@P8rVB*}A2kUImh=zG5tCBa>2ha3oLW9qjetp#E+ z^tuww!d-j~c@SF0)bB)k3k3NT073)}ttSBeL5LE`&ll-minPCnC@|8F-eCT@Nc(#{ z{uNzp$e;e1Nc*>VoYdI+=82;D(jGra{ojuJC$8E_4LgAjnDUoZ2~+qkk$;y6r;#NG zC^K^$!ltn$_b7GV>o1svGzk?7ErO-NiiCw)36PXz@$xevYeF9}H=B?~0$u2PfyFzx zYMqcXp$N>)Hl(9K7lvLBqEonP-H 7%9#TFW*e3armD%BeL>bA_pG}?GxzbTfUTrPoqozt= 8# zKMrv-5<6J9rscGgjdqA9;dLv>#>1{6Nz$%A0g(6)N5OG#N{nZak4?mrG+v9_H6ZY6 z6}QxCkv)1j?!_F~X{^N3xNVbjPMe6${FGoji0#=PMCP^2z~jsn8;>JNz7}WaoX!_} z{Ee^V)Ej5Pk_IYO(ItBjyA2!h)t5}|8~e3511Ix0F`j0yVzu?Xp;#@a0+QdE2 6X|k&EopKYlY*{K^{Ex35H?H>{Nd@GozoH1^N0(&)Z(dIarw@Fb zds1 S{EO^bjsD;_i1(dfBo}?1y<{LD$7rDcV8bxZpPgX3c1D)=|J&Af z`gcirtc*nu T`l__kB4%*}zsE%UfMHC5x^iMETipO1PXT-kox} zn?D+kxdv_{s+ks^slbTjMWEq^tZx;vVy&! dYgyK*%n43a~4*SsKcepYNtjb BWm7@xzEhOU_?2%7s(cwm_P+I|{l_9;c(|8gQyvD@nxQ7xWPSmNA z$;+!&PU{s*YvGYoiR~HKxpYzr2d~cp7jNXcy==CA!~N84&oRqd3XS;Yv~Hl{Rl)Z4 zMVazQU>B3_?ob2{>UC&68W4(NOo_6CM;jq>H8Kxv6^Kg{+jWT=KTxR=P;6dMK0+o* z!3^M9L~bRNUF+5~=+i&{YHU}an8?XAEW@OSoSA!z?j7fu=?@_lc#d(aV1#0*FZX@d z@KkyAapO$zX`p9xhn$|+
{J4$l^M@os5)z~Fw^HipEU~da1~mw7*qrAXOP*^ zmuMlA&0^b0r1&oqKPLw=)5eJKMu+BLkdDz;*J<-Ik?Hn7 D== z7r7pSdnS 5kZ)oynK=0~sUH*@l{OM`~wzhiwO2hkhsc-CYm z;EtCLT#>!Fc^PJGiud}SPp;e6J2SJAA*_q{3jG_cGeAG G^%S>{Xm-oJ_e<$GA0(Vw`gV*JH;s=Dwq6I zA$AbYb%TNQque@g{&8H!H)|*rP+LDelJ2yx?L1MzS=m>V==WW530afC+vG3a3RQ06 z#NhSUYNWVnq)eDqReL<1>2R?aQ&>ZwW`5MUro_*g-7P`Q64hD1S|rJd!5l5-D4EC; z#MCV{(9Cm~ZE;LQWOPI&d{vS|`f(C`HN}3f$J;3jdn{)1E5WEh!{dY)DHot_Y!_Db z8S2{wFJGwfJ9|n5ay~nQz0G@v*{2Gu%&nUK%J;1D@ved+;2ZqRhd-?Bzq$WC1Tn9A zw453W^1eW1^tI0N{`N+ I$*60BDP@o@`RMK#01k%+z@ Na{`ypVUi2Apn&T;Cf zdjzU3v;8lNlBh~BfVtW|+ha@vtraGZPBGQCzAbOnw7@Tr9WmvDi_Su2w>p0UPo`Or zJxpR4yVsXdJ1`DR>Mvy&k&g3x#o6|bQRvud(*>oW4=)D#SgRyG!Uz;R@KnsKhNAWP zh&@{UibZ}AmUgKuFA&r}dVPKmTo!f9n*=iGO10kiCZ|AyNnn7O*p&?uk%>oWFvt7J zT l_wn>Upt_di&?}t<&h+w4Ic1e56^32#hzkp~5)L6?df0)fct)8SP0`@) zh*o1Uj2Xudyd)>?L <1gS4jGN|dho*H#{?l}~+62_|?QQu`! z4J*1w@}6jqHN#a3SIIUObv-qwT7guzP@9ZN<}t5*R;ooY3bUl(m`AFNnj4)E`S!zy z-^VjT{Z*ABR(E5_HZOOqE58o>WgG3g(@#gyT))7iZqh*^rk(jGB3R3*EPTHZ#}d4S zUe@!Y6C|MbBq@&)_-HJA_0c^bmfB?V#BAokpKRRJ`Jv9lXyj+N17T#3d8XVt2K3-Z zWKpIfqOYu(E>*`4*M-YmzIY!~sboY2SHea|%alYb7!{PItMqp1Dre`Hol8RNl#g?` z>oDwYgjfuk7tv8ARL~`lEcDTP5d7N>I>SJ3Lm;+ITPTF0vN!1v;#?W;@D$)4Tlmsl z$b%)Xp_y)$X}%|XJyolyWj#B#r`32AZ^oi2acdf}>!?Lqg{9Q-XKH2__F~zmMx~^p zy-mygK+@E=E1%Z@*1%gw;kYFAyGvC6g$zZTlGaO7jNYXn1`GsO2^GI#+eY`z^irs( z4Oj1iU=W^|>6%@g}9 Y4tZnnz6IIioH=%KTzPqf3-gDQN!(c zbspyGttshEAzj~Mb&fJNoi*$TmC{Ks6Nh@W23Z(_WiE7ZFe#=oFVJT!_C0~GV%L=5 z>SW5t&k?m%Pid-&-X)S<^8R_1&RtBQW>4F_v2lEi7%{T68@&gIQ kSE8(Wjm-jH(8!+i4T&LS~Ys zT=t>tl=gev?T2)kBO#uymaFJFit}*ai-kI=!*L~_U}+IBVedInzj&+Lf90iwQ=kD* zH=-CQ_&*Z16(dA3`W+5Ex@B8)5S&(Xc>0}TOVixeOS;_0lMj$;FU3dYJ!ub+_66e> zAbG4QDG@%%^HK@$I<&xtYx68y3us^f 8%6 QjIP0okZyRka88G7az%6f?}>yxc8*H#n$GPqm4x9i+?CW>cR<$zYHw)!A3f_bWt z2LOE;Bf8(JUQ -#Ep!BU>!;I}m z;9eDWAscp)RU7Wevh6BO6)nS5yY4mBX{4c#T^s_eUDZkxnmzPoB&>0@88M^1ViucE zt#IDhf$v;(9fnxWmA*SHuI^${cb>2T45BK;F_fV066ey-WUD=tW%X+CtsJ+(aycpZ zMQhM1LxNr5LS#^V{2tY;WM{jgaJ)N&+e4n(DTyx4RN*iZXx-W{eVQB6q^8R&tRt5n zypvLUN~qpX8JKKM0|E_BVHMgTYd_m8&TFM%jpaTeO?)b?4!#t OAkXc5GiW2Jsz6Yc_G-Dy zv;<`C$kHL#CmUh;ei(sD!Qu_{mEgBV!HHh_ %o84@F z?+VXdP-!ep8Ny{WJYvDSAen2yT3v9DSE}aEvjc*Gyol{Vh_r5bN!S!E@Pg;e&@OyP z=egr#9-kQ)7A(2joL&6b$VgY`Wdp013ItczZ~0w#dT@m=D%(nJE_iWJgnLZtR!R1P zG!m|} ?2?}Y5Mr31+;+=A-Ko$(JLk%l@LA0ePX65(Ni D4feJ*?T-DNPob;*j;vMpKEfFPbY@PvQk#UTDb;b#}%67bsv+Bjx>S189z?1*H% zGylf_G_#!W`qc8}+tK*!fZJZw?0}t_%$6>Txvg`4kTaY4d~>Zr&lorE=i}9d61}r= zQTWEwk&=y;!!~B~rV1|1h?c({DkBy()H2mX`+VAg|F!-`Q!5S^-q_-i3~_JeguE-y za47c3m^aG0JK !Mnv0@vfnnW`aT&6p&youQHOg2PuF>YRd zOPM)_e;vU-bG7CPYo3CC%IDyJM^*N+pzC}Pz4HQp_PYO)5P6sF$9#A*@E{+g^5!1V zIBikyDc{9%|9V j7DFh{b@im(~n`1BK30 zbHb#xs&nDRw)w%)Vy@fqWd{9TXnpXLL9~Xk1Hk~Rt+FUD`PVZzjOjB)wVHw1uHe3? zD0I)`!aLrs xgpD34xa!dXBr `P{dOhL<@l$I%xR9HV;l zgo55p!K&+@3DgF%T iyaqRaJ)w}R~ zQ|5oE(_n$K{;+weGnPLrsqIQ-3Qu64c<+AJrlc^0%eD?5?$YDJhkt1Z=UNj8PR_lG z!&bN(xh2s!hH9 MiYVaGB%fvkzF>Fj4#(j4v%LvOKH^I&Xc7F|Y=3#n1f9 zkKySoR(`(yRgUm>nB|c{0s$%c_K(UD$iQ-hp^bsOosF$MgMp2m(fhl4o{GO@f%Td8 zD|D4pooR$gG^ x%ZYC*q-ZB3`eK`P#k)E&B_@pw|MLGy%b~oU|H+Ui*-5k^ zBuVLT2czyhzl$-qsWbxvOHoapW;>YDrzk>437*o<*RR2^8Xs*)fjQZ~V5PYRb&xC& zg%#_X6`SljJ$${6-#zzd24j79f47FYF5Z0hSNPtR3bC7}{GbQa$C?)J cj2|#sj7ntKrQ9NNE?biViSry;5Haf?+RI!p0i3XD4t@Ax!eXeDA zuOBT;dY;&Uf`IJ)!}<|$M*luxdbYOj^OYI%w|d$W`mgIW4mu09u&o+INdNBs!C~t@ z` ANww^pe)eF=(mG*{!)6w!8`JYKzqox`hEMMrgDqx`;^%{l zX2Jm;?)c|h%ShDuD>>*-ZI-p{5$hNCnx>)?T={BG4}ZO|8dS}Cga~wI5AgG!7voU> zS7#bH+B?`-z5BC1UdLwX6I#&O9SyBDg?U6IKQV=hK6hNJvCbUpqD~;Zbz9#z(1+=| zH@>{_NW1EEndUE0S$CYa;x>pY{R`g }z#b pfIZ0j-2A8`lCdv-TYdJI?))$sGFQ$=eQ`Q%>{ zTJ}^95vJa#HV{!#(fZlk4$BNNhxUDF!HHOFDLf7Pc3p6wzWTxPcX5Vx`{k5~S8QT_ z^!k0bL6_yyu2Rmq3J9~Im*$r;5&VtFt*7>#$RwG Q(_i^b036b6pF|u6hMNC}IQ~{= zYV%$;$HQMgSz$x_<01_>t>fVTXlmhrQ~Rg<{8!1jeTG-AhY<;Ky}gt8a+O0us#HFX z9=a4)NJRhhEm<{+_h5(6{hc~`9vV6zci8mlw0(d;+gkK9sm*#DYYVY!TI>f_cA>)t z;)&MahcwGWjfW55J@T8}W}YZ+b(P}5re~BlZQy+#=Cu2^(^$n=L4_8jMr|f^lf)yc zGcTi`ZX}Fr(9yqZ#l{=n>21VW0$AgE+Be)Ga3&(|RgmLSEkJ3Q5w2I7UVo?X!RH2K zHKHYJy>?Kj_((GvCg4KZ9|<)I!@Le62k{44d6$IvV$6BqVIkLnAX6||8077I7V$Wn zD)G+Rmq#U8(ayd`De_dBzHX8t^BXou`#qCJT-2PYSsP+Dt7MU+o2DTl_gedi`o8F3 zc41WlEBPRhz?ZxK@pba&@=&+3WH56uvij?t?h{Qd`=tgHud~Yaa~`R-(73|V#qYGO zW2~~w)o$aiM*<+ExkNBQAXW!!&aR!3#8i!2vPV1|q<31UUhd!m-fvsK91~1%p!?ek z%_AIDatU?=Tw4ntj~?|L@?Y vd^ sN5T=AdQGGy~#O+_FOQW&-doIyq6sKqb#!-W)u`yB^SA)%D600JWYxkJ5d zboRnS%Ed`m6DC&=#^KeaYnHo7p7}yE2!6C*I8|*)_U?CfjGpx)U8};eKG(hRdx>xU z*zI01dN~qGF1-k7R;J4BKkhFgvG6}P_1k@fma#rJHP j(jS(*hx|d4xwbL z9m`13a+flhsdCK30hAgoCRm4 JUa$P&Hl<=?t7?F#YLATSyoi>~!G|2#eX8arofSs!WnaK&7`I z3ORukGr)=_z;VT=hX)x`;MU`-dJK+oiSukl`G9lMi*#Jv%CPcncGD~xO!9SL`&A8e zXO*x4j4+jDa5TF_3>?j|__O^168zo7`MwG1t#igl#VWs~CQ;o|cGfSQlUpX^Je(mu z_TS0}mC93lH6@G=4-?gfwBl6qctq<%uy^&eoCt%53w0YD#C`wQ7#u=9}gM@Z}2# z?XLWqfpbY{i8NlZqAalFJMYq&qJzcXpI8I_ywU#HwnKFL>JtF|-i>*&^(wR?1Y?xs zx=PvRTLa^OAZ~DyF`8bt^W?>rE4sTV^o=BA2{ew&V6%_^&Bupr&H=R4LZ Tj_@`E2=?F(KrEG#G@<1tOU$0`-!da-!v9djX?Q$kXxw{{+Ze86>4NT~4YXuhc}~ zp6?#?-)fw#MNkho EL<^e3#*~x!Z?{{r )p8p*h~xzzv%ZwW`h5bAo^myh@TI %KWiX50d#i zIm6qz%5>jV2tHQqXHj0rfl=Qca|4GvwcYGtVy&c#i2G@h(Q6cM=M+zq9Is1F;B8Pr zh&vZ+YUN}Y&YBDumpMVkT?Wx{&3TBc5V3fsM;hA88X`DF5K>toJ=_=;t|k%OcN&>& z=1*R6Qg)_on3`ruA8^)UMh5PCQhE6G(Gom0en1C&l`!10eg5oyO}Nd0Ndds7MvdmP zjfk5Nq4EpB4R(#aVWPj$0?!V-g|G8TPAZaPUHOn{;PSA8Jd2Sv=i*-WQ(vgw=8HLB zGI7lHuhtuzdMM4>qhg%2n!Qd6q_(apHFkoGR5sBkjqitYc|L55UYHPYkVmfAMpB)3 zCJu=msab-JWASbHM`NeyY~Ez59>aD+1@;^DeNyCf#o|n_2~Nn?Ud8GNn+a%Z%1|+n zi3GYX0*6qq$ymHb5&msD&|}8pE<`__csRbAN#xPKC_YoSfIW^vQjSnhT{V3 aqn(-}n_-J=SWXxVJe=4Li)8wX@ Y{G-Sc`W}$D zD<#{8ctf8`U2Uu;%ERk6Xt{h n z-g}B(YKB**_@;fXNZ+UKP}hJW%gh)>y)VA?3>O8{;&GXUPOVUFC_)sSC%#`%kc?g? zxBXnsSGDG_F!Yl17LCJtgtl2qn(1N^q1YsR2SA&bRX3*d@e`Na5|`5vNk$0=eTLrj zZK3__@PR9nD?H9SiCNo;wbGpxc^o~@%)_%YDCtev**8y7h}U|ncdXl`^;gVXigoB+ zv7sH= 8Qqo2AKtyrpoWdz=i@h=m0q9n-UiCT1&bY;+#+V+9ny-+nqPcS+hpR0|rHK^^)C zZb8$*K(G32NW+l|-y1mBXt6OaAy6RO1DT}2lUgtiNF`&|O&a9qvc1bL{8i!RA&RMK zj0{yif=3 ?W}1{Bfb(=||}saN)A7`m&x zkFB6SVs7l2_x00+HRrqTs3_;TEYvi))7^ k83q3EC|WBkiCN2j~-n(b`1JMquN zU%I|9bOMNVC+hk} Mq?FmT-vTr}D{a6F m5}p$DX3;9^G(2;}yBKQV^;%u8 zvJG E-b@gDsQ^Is+Qiv%)kYr P?>NlWQ{U7#}3Da1 ~w;e)(?|ln2k2bkooZt+o!o+DG|(X2?kK*O{a4wI7*lEh}8o z{Oh!9{czet|FGrSeU%*Lfs5!F3eo;Lc~M{*o-rbA^J#b{AR*sDJ&z=OK1u^uB^S|) zP(od%8BDpjX}vLUxzl<`M$PRB gQeXQ+w41?ftQqyYWY)_@lr?+ZC9t<%yv^y0Ha9ld)FK^?d zQnEmk8OvgvQ?A`^vlTCCl`+;n9Yoqtcxa_@J$?Lclz?fFxwu$b|M&Fn^5m_$zERDD zxd28#?i}VAl9y(Fdr9|8=y0c! |7M%i) D)2<;c_DIw5ecbocpzi*&%`_rHy-7|sDkK|(4WhX!Eo^^d#yQv`EWtK@zocP#j zz};fwYqBozU~T05Oj8)Q;ysfd2t%BcxSRiL**9A;cj90oY=|Bx@o4ClN*m}fWTM`# zq%PqJM0_@YDq{XI|CR_5{=1RzbjiRef2zT#29 |7{3+_Y(rr |NGJ ng$|T8&6A76C zn!;uiXVQ1~h;+AzkVG>yv?%lbCkGoTIXm66bh>A%@8x3YYK)A(?N_^;5Tc`Diw;t5 zU~W=c%ImLmwu8Pz|5MrB1mK h}+(|fSVi+zNz=LrzriL zf1YF}&L tU zR38rMylTZfTg|r%Za(lka4(|fc^v7XeUL`jRF#;~hkded3+1J)(_D|pEO!lBmXsZQ zjH rZI0O+CAn4^G^L#dA&)`)8C#h3#kk<)_F zmX9cBAQq!%2V*bA>?lUwn`rAI{%bV$5A!6kQ82r2k=@5=8$ORh`AIJ_HV$Ul4B4xT zwspZjiC}l`Vk2PIaggOTwE4+n)NcM$Y{OoTS-C(Kd(bARh_U?Nr626om@mhX)eW@y zC}FHd*E+BPFkc8G-=U-Jn)0zX>evf0Q`h6)oUekNuA&qjHUMTuc>ElQYGVVS^IdEd z%*5*WHkfs=QBe8{8vrvuLFQj*W75S0I4K*$#==zm$Fb-kCu5=W8*D604SgJoO7CPW zbjE; tkXe^H!AGPJHT^vg)|oR(?Q1JtORGU@Dg5 z+ZZ-DaT_P1V8;LB+kjogMnQ#p>}_Di)#KZEaSa;)^)yaI!MvuAZzKIW0B}M?*3}>& TL>^Rn#9ud}l#_4+`QN_)vami( literal 0 HcmV?d00001 diff --git a/spec/fixtures/files/random.txt b/spec/fixtures/files/random.txt new file mode 100644 index 000000000..e69de29bb diff --git a/spec/requests/bulk_upload_controller_spec.rb b/spec/requests/bulk_upload_controller_spec.rb new file mode 100644 index 000000000..6b9ab16e0 --- /dev/null +++ b/spec/requests/bulk_upload_controller_spec.rb @@ -0,0 +1,60 @@ +require "rails_helper" + +RSpec.describe BulkUploadController, type: :request do + let(:url) { "/case_logs/bulk_upload" } + + describe "GET #show" do + before do + get url, params: {} + end + + it "returns a success response" do + expect(response).to be_successful + end + + it "returns a page with a file upload form" do + expect(response.body).to match(/