Skip to content

Commit dd85bb7

Browse files
committed
support PDF upload
1 parent 68b5165 commit dd85bb7

11 files changed

Lines changed: 1300 additions & 14 deletions

File tree

app/controllers/admin/meetings_controller.rb

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@ def new
1919

2020
def create
2121
unless params[:agenda_file].present?
22-
flash.now[:alert] = "Please select a JSON file to upload."
22+
flash.now[:alert] = "Please select a file to upload."
2323
return render :new, status: :unprocessable_entity
2424
end
2525

26-
data = JSON.parse(params[:agenda_file].read)
26+
data = parse_agenda_upload(params[:agenda_file])
27+
return render :new, status: :unprocessable_entity unless data
28+
2729
service = AgendaImportService.new(data).call
2830

2931
if service.success?
@@ -37,20 +39,22 @@ def create
3739
flash.now[:alert] = service.errors.join(", ")
3840
render :new, status: :unprocessable_entity
3941
end
40-
rescue JSON::ParserError
41-
flash.now[:alert] = "Invalid JSON file."
42-
render :new, status: :unprocessable_entity
4342
end
4443

4544
def import_minutes
4645
@meeting = Meeting.find(params[:id])
4746

4847
unless params[:minutes_file].present?
49-
redirect_to admin_meeting_path(@meeting), alert: "Please select a JSON file to upload."
48+
redirect_to admin_meeting_path(@meeting), alert: "Please select a file to upload."
49+
return
50+
end
51+
52+
data = parse_minutes_upload(params[:minutes_file])
53+
unless data
54+
redirect_to admin_meeting_path(@meeting), alert: @parse_error
5055
return
5156
end
5257

53-
data = JSON.parse(params[:minutes_file].read)
5458
service = MinutesImportService.new(data).call
5559

5660
if service.success?
@@ -60,8 +64,6 @@ def import_minutes
6064
else
6165
redirect_to admin_meeting_path(@meeting), alert: service.errors.join(", ")
6266
end
63-
rescue JSON::ParserError
64-
redirect_to admin_meeting_path(@meeting), alert: "Invalid JSON file."
6567
end
6668

6769
def delete_minutes
@@ -94,5 +96,46 @@ def destroy
9496
@meeting.destroy!
9597
redirect_to admin_meetings_path, notice: "Meeting deleted."
9698
end
99+
100+
private
101+
102+
def parse_agenda_upload(file)
103+
if pdf_file?(file)
104+
parser = AgendaPdfParserService.new(file.tempfile)
105+
data = parser.call
106+
unless parser.success?
107+
flash.now[:alert] = parser.errors.join(", ")
108+
return nil
109+
end
110+
data
111+
else
112+
JSON.parse(file.read)
113+
end
114+
rescue JSON::ParserError
115+
flash.now[:alert] = "Invalid JSON file."
116+
nil
117+
end
118+
119+
def parse_minutes_upload(file)
120+
if pdf_file?(file)
121+
parser = MinutesPdfParserService.new(file.tempfile)
122+
data = parser.call
123+
unless parser.success?
124+
@parse_error = parser.errors.join(", ")
125+
return nil
126+
end
127+
data
128+
else
129+
JSON.parse(file.read)
130+
end
131+
rescue JSON::ParserError
132+
@parse_error = "Invalid JSON file."
133+
nil
134+
end
135+
136+
def pdf_file?(file)
137+
file.content_type == "application/pdf" ||
138+
file.original_filename&.end_with?(".pdf")
139+
end
97140
end
98141
end
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
class AgendaPdfParserService
2+
attr_reader :errors
3+
4+
PARSER_SCRIPT = Rails.root.join("lib", "parsers", "parse_agenda.py").to_s
5+
6+
def initialize(pdf_io)
7+
@pdf_io = pdf_io
8+
@errors = []
9+
end
10+
11+
def call
12+
tempfile = write_to_tempfile
13+
output_file = Tempfile.new([ "agenda_parsed", ".json" ])
14+
15+
stdout, stderr, status = Open3.capture3(
16+
"python3", PARSER_SCRIPT, tempfile.path, output_file.path
17+
)
18+
19+
unless status.success?
20+
@errors << "PDF parsing failed: #{stderr.presence || stdout}"
21+
return nil
22+
end
23+
24+
JSON.parse(File.read(output_file.path))
25+
rescue JSON::ParserError => e
26+
@errors << "Parser produced invalid JSON: #{e.message}"
27+
nil
28+
ensure
29+
tempfile&.close!
30+
output_file&.close!
31+
end
32+
33+
def success?
34+
@errors.empty?
35+
end
36+
37+
private
38+
39+
def write_to_tempfile
40+
temp = Tempfile.new([ "agenda_upload", ".pdf" ])
41+
temp.binmode
42+
if @pdf_io.respond_to?(:read)
43+
temp.write(@pdf_io.read)
44+
@pdf_io.rewind if @pdf_io.respond_to?(:rewind)
45+
else
46+
temp.write(File.binread(@pdf_io))
47+
end
48+
temp.flush
49+
temp
50+
end
51+
end
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
class MinutesPdfParserService
2+
attr_reader :errors
3+
4+
PARSER_SCRIPT = Rails.root.join("lib", "parsers", "parse_minutes.py").to_s
5+
6+
def initialize(pdf_io)
7+
@pdf_io = pdf_io
8+
@errors = []
9+
end
10+
11+
def call
12+
tempfile = write_to_tempfile
13+
output_file = Tempfile.new([ "minutes_parsed", ".json" ])
14+
15+
stdout, stderr, status = Open3.capture3(
16+
"python3", PARSER_SCRIPT, tempfile.path, output_file.path
17+
)
18+
19+
unless status.success?
20+
@errors << "PDF parsing failed: #{stderr.presence || stdout}"
21+
return nil
22+
end
23+
24+
JSON.parse(File.read(output_file.path))
25+
rescue JSON::ParserError => e
26+
@errors << "Parser produced invalid JSON: #{e.message}"
27+
nil
28+
ensure
29+
tempfile&.close!
30+
output_file&.close!
31+
end
32+
33+
def success?
34+
@errors.empty?
35+
end
36+
37+
private
38+
39+
def write_to_tempfile
40+
temp = Tempfile.new([ "minutes_upload", ".pdf" ])
41+
temp.binmode
42+
if @pdf_io.respond_to?(:read)
43+
temp.write(@pdf_io.read)
44+
@pdf_io.rewind if @pdf_io.respond_to?(:rewind)
45+
else
46+
temp.write(File.binread(@pdf_io))
47+
end
48+
temp.flush
49+
temp
50+
end
51+
end

app/views/admin/meetings/new.html.erb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
<div class="max-w-lg bg-white rounded-lg border border-gray-200 p-6">
44
<%= form_with url: admin_meetings_path, multipart: true do |f| %>
55
<div class="mb-4">
6-
<label class="block text-sm font-medium text-gray-700 mb-2">Agenda JSON file</label>
7-
<input type="file" name="agenda_file" accept=".json" class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-medium file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100 file:cursor-pointer" />
8-
<p class="mt-1 text-xs text-gray-500">Select a structured JSON file containing the meeting agenda.</p>
6+
<label class="block text-sm font-medium text-gray-700 mb-2">Agenda file</label>
7+
<input type="file" name="agenda_file" accept=".json,.pdf" class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-medium file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100 file:cursor-pointer" />
8+
<p class="mt-1 text-xs text-gray-500">Upload a PDF agenda or a structured JSON file.</p>
99
</div>
1010

1111
<div class="flex items-center gap-3">

app/views/admin/meetings/show.html.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
<% end %>
5656

5757
<%= form_with url: import_minutes_admin_meeting_path(@meeting), method: :post, multipart: true, class: "flex items-center gap-3" do %>
58-
<input type="file" name="minutes_file" accept=".json" class="text-sm text-gray-500 file:mr-2 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-medium file:bg-gray-100 file:text-gray-700 hover:file:bg-gray-200 file:cursor-pointer file:transition-colors" />
58+
<input type="file" name="minutes_file" accept=".json,.pdf" class="text-sm text-gray-500 file:mr-2 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-medium file:bg-gray-100 file:text-gray-700 hover:file:bg-gray-200 file:cursor-pointer file:transition-colors" />
5959
<button type="submit" class="px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors cursor-pointer">Import Minutes</button>
6060
<% end %>
6161
</div>

0 commit comments

Comments
 (0)