ba_upload 0.3.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 15804867b016b8c972bbb18385a2d1caec4a9729776ea2d1877dcddaeb73b15d
4
- data.tar.gz: 930ef0bf4b3817c43616873144a43e46e482d5baa1efc5f5db63038dd44fd6e0
3
+ metadata.gz: a97b2695bf77d8a8693710f458561034c58ae2a43e737863f283e65301e8aa65
4
+ data.tar.gz: 8e3da29f1068f76f6f5320110ab68a99a0d3240e1b8d4ea589bb82771b02a676
5
5
  SHA512:
6
- metadata.gz: 4b906aa0b7c5860142c9c2d01f1f6955b8ccf8217448fd9164e293081f697ff9d43de2411989ff266a4729eb79a087a73fd56124e79159ec146fb29bd72fe6a2
7
- data.tar.gz: fa9aaa0351fdf5758cb2a770e2b0105c6f599bae516f416ffa1ef25de58727f20b0180574086079474050042b253789dded4d58ce78e0edb55f81c146107e2cf
6
+ metadata.gz: 53b3dcee1f488d9b4758a03d9263bc6787a7b9b74680ef999d1ba1c01eb9625c3d2ea9210a2681a3804dd8727b43c9f745374e0f94567694a4470d9214c3d88a
7
+ data.tar.gz: 262b6794309cf833bc0d2a6301c0a757d036a3ac917a54c2eb9cea1301c02456359fa4a886e33b028ca8a264ac64dfeaadee8e3b9c67a2f30b057645e2b1c587
data/README.md CHANGED
@@ -33,7 +33,8 @@ require 'ba_upload'
33
33
  connection = BaUpload.open_connection(file_path: 'config/Zertifikat-1XXXX.p12', passphrase: 'YOURPASSPHRASE')
34
34
 
35
35
  # Upload a xml-file
36
- connection.upload(file: File.open('/opt/vam-transfer/data/DSP000132700_2016-08-08_05-00-09.xml'))
36
+ file_path = "/opt/vam-transfer/data/DSP000132700_2016-08-08_05-00-09.xml"
37
+ connection.upload(file: file_path))
37
38
 
38
39
  # later cronjob to download all error files
39
40
 
@@ -50,11 +51,23 @@ end
50
51
  ```ruby
51
52
  #!/usr/bin/env ruby
52
53
  require 'ba_upload'
53
- connection = BaUpload.open_connection(file_path: 'config/Zertifikat-1XXXX.p12', passphrase: 'YOURPASSPHRASE')
54
+ BaUpload.open_connection(file_path: 'config/Zertifikat-1XXXX.p12', passphrase: 'YOURPASSPHRASE')
54
55
  connection.upload(file: File.open(ARGV[0]))
55
56
  ```
56
57
 
57
- Save to a file and just run it with the xml file as argument
58
+ Save to a file and just run it with the xml file as argument. It's important, because the file upload of BA validates the original file name.
59
+
60
+
61
+ ### Memory safety
62
+
63
+ because we are using Mechanize under the hood, it's a good idea to always close sockets, (with connection.shutdown), or use the open helper:
64
+
65
+ ```ruby
66
+ BaUpload.open(file_path: 'config/Zertifikat-1XXXX.p12', passphrase:) do |c|
67
+ c.upload(file: file_path)
68
+ c.error_files....
69
+ end
70
+ ```
58
71
 
59
72
 
60
73
  ### Downloading "misc" files
@@ -70,6 +83,19 @@ connection.misc.each do |link|
70
83
  end
71
84
  ```
72
85
 
86
+ ### Downloading 'Statistiken' xlsx reports
87
+
88
+ Download XLSX reports (validation errors and "Stellenübersicht") from BA; You might use another Gem, like roo, axlsx etc. to parse those files if necessary.
89
+
90
+ ```ruby
91
+ connection.statistics.each do |link|
92
+ link.tempfile
93
+
94
+ # or:
95
+ link.read
96
+ end
97
+ ```
98
+
73
99
  ### Usage with multiple client certificats
74
100
 
75
101
  Since September 2022, users with multiple client certificats issued to the same email address need to provide their respective partner ID when using the API.
@@ -87,6 +113,190 @@ connection.misc(partner_id: 'P000XXXXXX')
87
113
 
88
114
  ```
89
115
 
116
+ ## Appendix: Berufe
117
+
118
+ Sooner or later, you have to provide a TitleCode = Vocation "Beruf" for each job. To fetch and process the Berufe, we create a ActiveRecord Model in our database:
119
+
120
+ Here an example of a implementation at Empfehlungsbund. You can also use our [search mask](https://login.empfehlungsbund.de/arbeitsagentur) to search for occupations.
121
+
122
+ We put the "help" / "validation" messages, that we found in the appropriate scopes, too, as "Ausbildungen" and "Duale Studiengänge" need different types of professions.
123
+
124
+ <details>
125
+ <summary>ActiveRecord Model for Ba::Profession</summary>
126
+
127
+ ```ruby
128
+ # migration:
129
+ create_table :ba_professions do |t|
130
+ t.string "bkz"
131
+ t.string "typ"
132
+ t.string "lbkgruppe"
133
+ t.string "hochschulberuf"
134
+ t.string "kuenstler"
135
+ t.string "bezeichnung_nl"
136
+ t.string "bezeichnung_nk"
137
+ t.string "suchname_nl"
138
+ t.datetime "created_at"
139
+ t.datetime "updated_at"
140
+ t.integer "ebene"
141
+ t.integer "qualifikationsniveau"
142
+ t.datetime "deleted_on"
143
+ end
144
+
145
+ class Ba::Profession < ApplicationRecord
146
+ has_many :jobs
147
+
148
+ scope :undeleted, -> { where 'deleted_on is null' }
149
+ scope :berufe, -> { where typ: 'B' }
150
+ scope :ausbildungen, -> { where typ: 'A' }
151
+ scope :sorted, -> { order(Arel.sql('deleted_on is not null, bezeichnung_nl')) }
152
+ # Bei Auswahl von „Ausbildung“ (EducationType=0) sind die Berufe mit dem
153
+ # Qualifikationsniveau 2 zulässig. Zusätzlich sind hier alle Berufe folgender
154
+ # berufskundlicher Gruppen erlaubt: [...]
155
+ scope :reine_ausbildungen, -> {
156
+ where(qualifikationsniveau: 2).or(
157
+ where(lbkgruppe: [1150, 3110, 5130])
158
+ ).ausbildungen
159
+ }
160
+ # Wird ein Stellenangebot vom Typ „Duales Studium“ (EducationType=1) übermittelt, sind der
161
+ # Studiengang und der ggf. vorhandene Ausbildungsberuf getrennt anzugeben. Als
162
+ # Studiengang (Course) sind Berufe mit ausschließlich dem Qualifikationsniveau 4 zulässig.
163
+ # Diese Berufe entstammen alle der berufskundlichen Gruppe 3120 („A Grundständige
164
+ # Studienfächer/-gänge“). Der als Ausbildung (TitleCode) angegebene Beruf darf
165
+ # dementsprechend nicht ausschließlich das Qualifikationsniveau 4 haben.
166
+ scope :duale_studiengaenge, -> { ausbildungen.where ebene: 3, qualifikationsniveau: 4 }
167
+
168
+ def duales_studium?
169
+ ebene == 3 && qualifikationsniveau == 4 && typ == 'A'
170
+ end
171
+
172
+ def self.download_from_ba
173
+ require 'tty/prompt'
174
+ prompt = TTY::Prompt.new
175
+ link = Ba::Distributor.ba_connection.misc.last do |link|
176
+ link.click
177
+ target = "public/ba/#{link.href}"
178
+ response = link.click
179
+ File.open(target, "wb+") { |f| f.write(response.body) }
180
+
181
+ puts "Unzipping vam_beruf_kurz.xml..."
182
+ `unzip -o -d public/ba/ #{target} vam_beruf_kurz.xml`
183
+ end
184
+
185
+ def self.import(path: 'public/ba/vam_beruf_kurz.xml')
186
+ doc = Nokogiri::XML.parse(File.open(path))
187
+ berufe_vorher = Ba::Beruf.undeleted.pluck(:id)
188
+ doc.search('beruf').each do |beruf_doc|
189
+ beruf = where(id: beruf_doc['id']).first_or_initialize
190
+
191
+ beruf.bkz = beruf_doc['bkz']
192
+
193
+ beruf.typ = beruf_doc.at('typ').text == 't' ? 'B' : 'A'
194
+ beruf.qualifikationsniveau = beruf_doc.at('qualifikationsNiveau[niveau]')['niveau']
195
+ beruf_doc.search(*%w[lbkgruppe hochschulberuf ebene kuenstler bezeichnung_nl bezeichnung_nk suchname_nl]).each do |i|
196
+ beruf.send("#{i.name}=", i.text)
197
+ end
198
+ beruf.save
199
+ berufe_vorher.delete(beruf.id)
200
+ end
201
+ Ba::Beruf.where(id: berufe_vorher).update_all deleted_on: Time.zone.now if berufe_vorher.any?
202
+ end
203
+ scope :duale_studiengaenge, -> { where ebene: 3, qualifikationsniveau: 4 }
204
+
205
+ def display_name
206
+ prefix = if deleted_on?
207
+ "[!VERALTET!] "
208
+ end
209
+ if typ == 'A'
210
+ if ebene == 3 && qualifikationsniveau == 4
211
+ "#{prefix}#{bezeichnung_nk} (DUALES STUDIUM/praxisorientiert)"
212
+ else
213
+ "#{prefix}#{bezeichnung_nk} (AUSBILDUNG)"
214
+ end
215
+ else
216
+ "#{prefix}#{bezeichnung_nk}"
217
+ end
218
+ end
219
+
220
+ def as_json(opts = {})
221
+ {
222
+ id: id,
223
+ display_name: display_name
224
+ }
225
+ end
226
+ ```
227
+
228
+ </details>
229
+
230
+ ## Appendix: How to construct a Job-Posting XML file to upload
231
+
232
+ - Download the most recent JobPosting xsd from https://baxml.arbeitsagentur.de/geschuetzt/download/
233
+ - You can visualize the xsd here: http://www.xml-tools.net/schemaviewer.html
234
+ - Now, you can construct the file with xml-builder:
235
+
236
+ <details>
237
+ <summary>Example for constructing a feed using XmlBuilder</summary>
238
+
239
+ ```ruby
240
+ xml = Builder::XmlMarkup.new(indent: 1)
241
+ xml.instruct!
242
+ xml.tag!("HRBAXMLJobPositionPosting") do
243
+ xml.tag!("Header") do
244
+ xml.tag!("SupplierId", SUPPLIER_ID)
245
+ xml.tag!("Timestamp", Time.zone.now.to_s(:db).tr(" ", "T"))
246
+ xml.tag!("Amount", obs.count)
247
+ # F: Full
248
+ # D: Diff
249
+ if @only_jobs
250
+ xml.tag!("TypeOfLoad", "D")
251
+ else
252
+ xml.tag!("TypeOfLoad", "F")
253
+ end
254
+ end
255
+ xml.tag!("Data") do
256
+ jobs.each do |job|
257
+ generate_xml_for_job(xml, job)
258
+ end
259
+
260
+ jobs_to_delete.each do |job|
261
+ xml.tag! "DeleteEntry" do
262
+ xml.tag! "EntryId", id
263
+ end
264
+ end
265
+ end
266
+ end
267
+ xml
268
+ ```
269
+ </details>
270
+
271
+ - Then, you should validate your feed:
272
+
273
+ ```ruby
274
+ xsd = Nokogiri::XML::Schema(File.open("vendor/ba/HRBAXML_JobPosition_Current.xsd"))
275
+ doc = Nokogiri::XML(xml.to_s)
276
+ xsd.validate(doc)
277
+ ```
278
+
279
+ - Then, you can put that into a file - so you will need to generate a filename **according to the spec**:
280
+
281
+ <details>
282
+ <summary>Generate a filename</summary>
283
+
284
+ ```ruby
285
+ # for historic reasons, you could transmit a bunch of files with the same timestamp using an index/offset, but usually, just putting 0 here should be enought
286
+ index = 0
287
+ number_of_feeds_to_push_now = 1
288
+ ended = index == (number_of_feeds_to_push_now - 1)
289
+ flag = ended ? "E" : "C"
290
+ date = Time.zone.now.strftime "%Y-%m-%d_%H-%M-%S_F#{'%03d' % (index + 1)}#{flag}"
291
+ "DS#{SUPPLIER_ID}_#{date}.xml"
292
+ ```
293
+
294
+ </details>
295
+
296
+ - Upload the file using this Gem. You should wait a "couple of minutes" (tip: enqueue a activeJob for 10 minutes later), to fetch the resulting **error file**, and analyse that.
297
+
298
+
299
+
90
300
  ## License
91
301
 
92
302
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/ba_upload.gemspec CHANGED
@@ -20,6 +20,6 @@ Gem::Specification.new do |spec|
20
20
  spec.require_paths = ["lib"]
21
21
 
22
22
  spec.add_dependency "mechanize"
23
- spec.add_development_dependency "bundler", "~> 1.11"
23
+ spec.add_development_dependency "bundler", "~> 2.0"
24
24
  spec.add_development_dependency "rake", "~> 10.0"
25
25
  end
@@ -1,4 +1,5 @@
1
1
  require 'ba_upload/error_file'
2
+ require 'ba_upload/statistic_file'
2
3
  module BaUpload
3
4
  class Connection
4
5
  attr_reader :m
@@ -16,7 +17,7 @@ module BaUpload
16
17
 
17
18
  def upload(file: nil, partner_id: nil)
18
19
  url = base_url(partner_id) + "in/"
19
- m.get url
20
+ m.get(url)
20
21
  form = m.page.forms.first
21
22
  form.file_uploads.first.file_name = file
22
23
  form.submit
@@ -24,13 +25,21 @@ module BaUpload
24
25
 
25
26
  def error_files(partner_id: nil)
26
27
  url = base_url(partner_id)
27
- m.get url
28
+ m.get(url)
28
29
  links = m.page.links_with(text: /ESP|ESV/)
29
30
  links.map do |link|
30
31
  ErrorFile.new(link)
31
32
  end
32
33
  end
33
34
 
35
+ def statistics(partner_id: nil)
36
+ url = base_url(partner_id) + "Statistiken"
37
+ m.get(url)
38
+ m.page.links_with(text: /xlsx/).map do |link|
39
+ StatisticFile.new(link)
40
+ end
41
+ end
42
+
34
43
  def misc(partner_id: nil)
35
44
  url = base_url(partner_id)
36
45
  m.get url
@@ -0,0 +1,18 @@
1
+ require 'ba_upload/error_file'
2
+ module BaUpload
3
+ class StatisticFile < ErrorFile
4
+ def tempfile
5
+ tf = Tempfile.new(['statistic_file', '.xlsx'])
6
+ tf.binmode
7
+ tf.write(read)
8
+ tf.flush
9
+ tf.rewind
10
+ tf
11
+ end
12
+
13
+ def read
14
+ @mechanize_link.click.body
15
+ end
16
+ end
17
+ end
18
+
@@ -1,3 +1,3 @@
1
1
  module BaUpload
2
- VERSION = "0.3.0"
2
+ VERSION = "0.5.0"
3
3
  end
data/lib/ba_upload.rb CHANGED
@@ -1,6 +1,11 @@
1
1
  require "ba_upload/version"
2
2
  require "openssl"
3
3
 
4
+ if OpenSSL::VERSION >= '3.0.0' && defined?(OpenSSL::Provider)
5
+ # import legacy
6
+ OpenSSL::Provider.load("legacy")
7
+ end
8
+
4
9
  module BaUpload
5
10
  def self.export_certificate(file_path:, passphrase:)
6
11
  cert = OpenSSL::PKCS12.new(File.read(file_path), passphrase)
@@ -15,6 +20,13 @@ module BaUpload
15
20
  cert = BaUpload.export_certificate(file_path: file_path, passphrase: passphrase)
16
21
  BaUpload::Connection.new(cert[:key], cert[:cert], cert[:ca_cert])
17
22
  end
23
+
24
+ def self.open(file_path:, passphrase:, &block)
25
+ conn = BaUpload.open_connection(file_path: file_path, passphrase: passphrase)
26
+ block.call(conn)
27
+ ensure
28
+ conn.shutdown
29
+ end
18
30
  end
19
31
 
20
32
  require 'ba_upload/connection'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ba_upload
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Wienert
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-09-08 00:00:00.000000000 Z
11
+ date: 2024-10-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mechanize
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '1.11'
33
+ version: '2.0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '1.11'
40
+ version: '2.0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rake
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -70,6 +70,7 @@ files:
70
70
  - lib/ba_upload.rb
71
71
  - lib/ba_upload/connection.rb
72
72
  - lib/ba_upload/error_file.rb
73
+ - lib/ba_upload/statistic_file.rb
73
74
  - lib/ba_upload/version.rb
74
75
  homepage: https://github.com/pludoni/ba_upload
75
76
  licenses:
@@ -90,7 +91,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
90
91
  - !ruby/object:Gem::Version
91
92
  version: '0'
92
93
  requirements: []
93
- rubygems_version: 3.0.3
94
+ rubygems_version: 3.5.21
94
95
  signing_key:
95
96
  specification_version: 4
96
97
  summary: Upload API for Bundesagentur fuer Arbeit (hrbaxml.arbeitsagentur)