truenorth 0.2.8 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ebf93538d8d794249ddcb8f4ddb2832eba001e6758d275c4ea59af595777d50e
4
- data.tar.gz: e857b71120bf6c327fb83284ad901472656e12f0df542abd75223a953c6ea202
3
+ metadata.gz: 8d89d28373a48164f8f6cf1f93672dd70d4f8aaf8035ae40bdc144fe76a8d6be
4
+ data.tar.gz: 266756fa35b43d17ad657bb11ab46d0411c63da6620bf630594a08ac7f4d5226
5
5
  SHA512:
6
- metadata.gz: c4b4e536e26ee506d3ee57b1cc9f186f9696c2f10ae61ab834e4dd40999d36381f1aa01b03096c1eb118ab92c720bef71d1b74f07c18c37fd6050545d13b67ad
7
- data.tar.gz: '0619e6eb3fcb0220aebdcc6bb13f60c452d157c6e772dbd63596981c0dc4b6fed5cdbd8acf16a3c00916b853791875020651a0a3c5fe326fa8115a93fae5ab76'
6
+ metadata.gz: e1c0d586aa1b9146bedb498c81036d1446f23a9f6ee92cae1d14887007227196986e4ed245511cd39a58375729fa55831174f1536721f5d57a39ca8db03983bb
7
+ data.tar.gz: a764f05fe8325d6457f03467ba1c3a169b9422b8bbaa8d2030264a0f8bf5bcacda0e346fd913c0d415880f26d1f6dde03c6ef3fd58525c80e8e5ef2153b862c3
@@ -110,17 +110,24 @@ module Truenorth
110
110
  response = get(BOOKING_PATH)
111
111
  html = Nokogiri::HTML(response.body)
112
112
 
113
- # Check if we need to change activity type first
113
+ # Navigate to the requested date first if needed
114
+ current_date = html.at_css('input[name*="sheetDate"]')&.[]('value')
115
+ requested_date = date.strftime('%m/%d/%Y')
114
116
  activity_id = ACTIVITIES[activity.to_s.downcase] || '5'
117
+
118
+ if current_date != requested_date
119
+ log "Navigating from #{current_date} to #{requested_date}"
120
+ html = change_date(html, requested_date, activity_id)
121
+ end
122
+
123
+ # Then change activity type - this should now return slots for the correct date
115
124
  current_activity = html.at_css('input[name*="activityId"]')&.[]('value')
116
125
 
117
126
  if current_activity && current_activity != activity_id
118
- # Need to change activity via AJAX
127
+ log "Changing activity from #{current_activity} to #{activity_id}"
119
128
  html = change_activity(html, activity_id)
120
129
  end
121
130
 
122
- # TODO: Handle date navigation if needed
123
-
124
131
  slots = parse_slots(html)
125
132
  log "Found #{slots.count} available time slots"
126
133
 
@@ -132,6 +139,7 @@ module Truenorth
132
139
  }
133
140
  end
134
141
 
142
+
135
143
  # Book a slot
136
144
  def book(time, date: Date.today, court: nil, activity: 'squash', dry_run: false)
137
145
  ensure_logged_in!
@@ -409,17 +417,58 @@ module Truenorth
409
417
  def parse_slots(html)
410
418
  slots = {}
411
419
 
412
- html.css('td.slot.open').each do |td|
413
- div = td.at_css('div[data-start-time]')
414
- next unless div
415
- next if td['class']&.include?('reserved')
420
+ # Find ALL slots with data-start-time
421
+ html.css('td.slot div[data-start-time]').each do |div|
422
+ td = div.parent
423
+ while td && td.name != 'td'
424
+ td = td.parent
425
+ end
426
+ next unless td
416
427
 
417
- start_time = div['data-start-time']
418
- area_id = div['data-area-id']
419
- court_name = COURTS[area_id] || "Court #{area_id}"
428
+ classes = td['class'].to_s
429
+
430
+ # Skip definitively unavailable slots
431
+ next if classes.include?('reserved')
432
+ next if classes.include?('restrict')
433
+ next if classes.include?('blocked')
434
+
435
+ # Include ANY slot that's marked as "open", even if it has past-time
436
+ # The website shows past-time slots for future dates as bookable
437
+ if classes.include?('open')
438
+ start_time = div['data-start-time']
439
+ area_id = div['data-area-id']
440
+ court_name = COURTS[area_id] || "Court #{area_id}"
441
+
442
+ slots[start_time] ||= []
443
+ slots[start_time] << court_name unless slots[start_time].include?(court_name)
444
+ end
445
+ end
446
+
447
+ # If we found very few slots, be more aggressive
448
+ if slots.length < 10
449
+ log "Found only #{slots.length} slots with strict parsing, trying relaxed parsing..."
450
+
451
+ # Try including ALL non-reserved slots
452
+ html.css('td.slot div[data-start-time]').each do |div|
453
+ td = div.parent
454
+ while td && td.name != 'td'
455
+ td = td.parent
456
+ end
457
+ next unless td
458
+
459
+ classes = td['class'].to_s
460
+ # Only skip if explicitly reserved/restricted/blocked
461
+ next if classes.include?('reserved') && !classes.include?('open')
462
+ next if classes.include?('restrict')
463
+ next if classes.include?('blocked')
420
464
 
421
- slots[start_time] ||= []
422
- slots[start_time] << court_name
465
+ start_time = div['data-start-time']
466
+ area_id = div['data-area-id']
467
+ court_name = COURTS[area_id] || "Court #{area_id}"
468
+
469
+ slots[start_time] ||= []
470
+ slots[start_time] << court_name unless slots[start_time].include?(court_name)
471
+ end
423
472
  end
424
473
 
425
474
  slots
@@ -544,6 +593,58 @@ module Truenorth
544
593
  Nokogiri::HTML(result[:body])
545
594
  end
546
595
 
596
+ def change_date(html, date_str, activity_id = nil)
597
+ # Extract what we can from the HTML
598
+ form_id = extract_form_id(html)
599
+ view_state = extract_view_state(html)
600
+
601
+ # Build minimal form data manually to avoid extraction issues
602
+ form_data = build_minimal_form_data(html, form_id, activity_id)
603
+
604
+ result = change_date_ajax(form_id, view_state, date_str, form_data)
605
+ return html unless result[:success]
606
+
607
+ # Parse the AJAX response
608
+ ajax_body = result[:body]
609
+ cdata_content = ajax_body.scan(/<!\[CDATA\[(.*?)\]\]>/m).flatten.join("\n")
610
+
611
+ if !cdata_content.empty? && cdata_content.include?('slot')
612
+ Nokogiri::HTML(cdata_content)
613
+ else
614
+ html
615
+ end
616
+ end
617
+
618
+ def build_minimal_form_data(html, form_id, activity_id)
619
+ # Build the minimum required form data from scratch
620
+ # Only extract the fields we absolutely need
621
+ data = {}
622
+
623
+ # Try to find the form
624
+ form = html.at_css("form[id='#{form_id}']")
625
+ return data unless form
626
+
627
+ # Extract only the essential hidden fields
628
+ form.css('input[type="hidden"]').each do |input|
629
+ name = input['name']
630
+ value = input['value']
631
+ next unless name
632
+
633
+ # Only keep fields that belong to our form
634
+ if name.to_s.start_with?(form_id)
635
+ data[name] = value || ''
636
+ end
637
+ end
638
+
639
+ # Override activity ID if provided
640
+ if activity_id
641
+ data["#{form_id}:activityId"] = activity_id.to_s
642
+ data["#{form_id}:j_idt51_input"] = activity_id.to_s
643
+ end
644
+
645
+ data
646
+ end
647
+
547
648
  def extract_view_state(html)
548
649
  html.at_css('input[name="javax.faces.ViewState"]')&.[]('value')
549
650
  end
@@ -629,6 +730,39 @@ module Truenorth
629
730
  end
630
731
  end
631
732
 
733
+ def change_date_ajax(form_id, view_state, date_str, form_data)
734
+ return { success: false, error: 'No form_id' } unless form_id
735
+
736
+ date_picker = "#{form_id}:j_idt57"
737
+ ajax_url = build_ajax_url
738
+ encoded_url = URI.encode_www_form_component(ajax_url)
739
+
740
+ # Use form_data as-is (already built correctly by build_minimal_form_data)
741
+ # Just add our date values and AJAX control parameters
742
+ form_data = form_data.dup
743
+ form_data["#{form_id}:sheetDate"] = date_str
744
+ form_data["#{form_id}:j_idt57_input"] = date_str
745
+
746
+ form_data.merge!(
747
+ 'javax.faces.partial.ajax' => 'true',
748
+ 'javax.faces.source' => date_picker,
749
+ 'javax.faces.partial.execute' => date_picker,
750
+ 'javax.faces.partial.render' => form_id,
751
+ 'javax.faces.behavior.event' => 'dateSelect',
752
+ 'javax.faces.partial.event' => 'dateSelect',
753
+ form_id => form_id,
754
+ 'javax.faces.encodedURL' => encoded_url,
755
+ 'javax.faces.ViewState' => view_state
756
+ )
757
+
758
+ response = post_ajax(ajax_url, form_data)
759
+ if response.is_a?(Net::HTTPSuccess)
760
+ { success: true, view_state: extract_view_state_from_ajax(response.body), body: response.body }
761
+ else
762
+ { success: false, error: "HTTP #{response.code}" }
763
+ end
764
+ end
765
+
632
766
  def select_slot_ajax(form_id, view_state, slot, components, form_fields)
633
767
  source_id = components['showReservationScreen'] || "#{form_id}:j_idt146"
634
768
  ajax_url = build_ajax_url
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Truenorth
4
- VERSION = '0.2.8'
4
+ VERSION = '0.3.0'
5
5
  end
data/truenorth.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = 'truenorth'
5
- spec.version = '0.2.8'
5
+ spec.version = '0.3.0'
6
6
  spec.authors = ['usiegj00']
7
7
  spec.email = ['112138+usiegj00@users.noreply.github.com']
8
8
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: truenorth
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.8
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - usiegj00