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 +4 -4
- data/lib/truenorth/client.rb +147 -13
- data/lib/truenorth/version.rb +1 -1
- data/truenorth.gemspec +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8d89d28373a48164f8f6cf1f93672dd70d4f8aaf8035ae40bdc144fe76a8d6be
|
|
4
|
+
data.tar.gz: 266756fa35b43d17ad657bb11ab46d0411c63da6620bf630594a08ac7f4d5226
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e1c0d586aa1b9146bedb498c81036d1446f23a9f6ee92cae1d14887007227196986e4ed245511cd39a58375729fa55831174f1536721f5d57a39ca8db03983bb
|
|
7
|
+
data.tar.gz: a764f05fe8325d6457f03467ba1c3a169b9422b8bbaa8d2030264a0f8bf5bcacda0e346fd913c0d415880f26d1f6dde03c6ef3fd58525c80e8e5ef2153b862c3
|
data/lib/truenorth/client.rb
CHANGED
|
@@ -110,17 +110,24 @@ module Truenorth
|
|
|
110
110
|
response = get(BOOKING_PATH)
|
|
111
111
|
html = Nokogiri::HTML(response.body)
|
|
112
112
|
|
|
113
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
422
|
-
|
|
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
|
data/lib/truenorth/version.rb
CHANGED
data/truenorth.gemspec
CHANGED