rairo commited on
Commit
600d444
·
verified ·
1 Parent(s): be1c27e

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +34 -21
main.py CHANGED
@@ -21,7 +21,7 @@ import base64
21
 
22
  # --- NEW IMPORTS FOR FILE GENERATION ---
23
  import weasyprint
24
- import imgkit
25
 
26
  # === Logging Configuration ===
27
  if not os.path.exists('logs'):
@@ -72,8 +72,7 @@ logger.info("Flask and APScheduler initialized.")
72
  # === UPDATED EMAIL SENDING FUNCTION ===
73
  def send_email(to, subject, html, attachments=None):
74
  """
75
- Sends an email using a direct requests call, now with attachment support.
76
- Attachments should be a list of dicts: [{'filename': 'roster.pdf', 'content': 'base64_string'}]
77
  """
78
  if not RESEND_API_KEY:
79
  logger.error("RESEND_API_KEY is not configured. Cannot send email.")
@@ -85,7 +84,7 @@ def send_email(to, subject, html, attachments=None):
85
  "Content-Type": "application/json"
86
  }
87
  payload = {
88
- "from": "Guard Admin <admin@sozofix.tech>", # Using custom domain
89
  "to": to.strip(),
90
  "subject": subject,
91
  "html": html,
@@ -95,7 +94,7 @@ def send_email(to, subject, html, attachments=None):
95
  try:
96
  response = requests.post(
97
  url, headers=headers, json=payload,
98
- verify=certifi.where(), timeout=30 # Increased timeout for attachments
99
  )
100
  response.raise_for_status()
101
  response_data = response.json()
@@ -139,17 +138,35 @@ def create_pdf_attachment(html_content, filename):
139
  logger.error(f"Failed to create PDF attachment: {e}", exc_info=True)
140
  return None
141
 
 
142
  def create_image_attachment(html_content, filename):
143
- """Creates a PNG image file from HTML content and returns its path."""
 
 
 
 
144
  try:
145
  filepath = f"/tmp/{filename}"
146
- # Options to improve image quality and ensure full capture
147
- options = {'width': 800, 'disable-smart-width': ''}
148
- imgkit.from_string(html_content, filepath, options=options)
149
- logger.info(f"Successfully created image attachment: {filepath}")
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  return filepath
151
  except Exception as e:
152
- logger.error(f"Failed to create image attachment: {e}", exc_info=True)
153
  return None
154
 
155
 
@@ -172,13 +189,13 @@ def send_rotation_notification(job_id, shift_record):
172
  except Exception:
173
  human_readable_time = shift_record.get('assigned_at', 'Unknown')
174
 
175
- # Generate HTML content once
176
  html_content = f"""
177
  <html><head><style>
178
- body {{ font-family: sans-serif; }}
179
- table {{ border-collapse: collapse; width: 100%; }}
180
- th, td {{ border: 1px solid #dddddd; text-align: left; padding: 8px; }}
181
  th {{ background-color: #f2f2f2; }}
 
182
  </style></head><body>
183
  <h2>Guard Rotation Notification</h2>
184
  <p><strong>Job:</strong> {job_name}</p>
@@ -193,18 +210,15 @@ def send_rotation_notification(job_id, shift_record):
193
  html_content += f"<tr><td>{point_name}</td><td>{members_html}</td></tr>"
194
  html_content += "</tbody></table><p><em>This is an automated notification from the Guard Rotation System.</em></p></body></html>"
195
 
196
- # Generate Attachments
197
  base_filename = f"roster_{job_name.replace(' ', '_')}_shift_{shift_number}_{int(time.time())}"
198
  paths_to_clean = []
199
  attachments = []
200
 
201
  try:
202
- # Create files
203
  csv_path = create_csv_attachment(shift_record, f"{base_filename}.csv")
204
  pdf_path = create_pdf_attachment(html_content, f"{base_filename}.pdf")
205
  img_path = create_image_attachment(html_content, f"{base_filename}.png")
206
 
207
- # Prepare for email sending
208
  file_paths = {'roster.csv': csv_path, 'roster.pdf': pdf_path, 'roster.png': img_path}
209
  for filename, filepath in file_paths.items():
210
  if filepath:
@@ -213,7 +227,6 @@ def send_rotation_notification(job_id, shift_record):
213
  content_b64 = base64.b64encode(f.read()).decode('utf-8')
214
  attachments.append({'filename': filename, 'content': content_b64})
215
 
216
- # Send emails
217
  subject = f"Guard Rotation - {job_name} (Shift {shift_number})"
218
  for send_to_email in SEND_TO_EMAILS:
219
  logger.info(f"Sending rotation notification with {len(attachments)} attachments to {send_to_email}...")
@@ -221,7 +234,6 @@ def send_rotation_notification(job_id, shift_record):
221
  time.sleep(1)
222
 
223
  finally:
224
- # CRITICAL: Clean up temporary files
225
  logger.info(f"Cleaning up {len(paths_to_clean)} temporary files...")
226
  for path in paths_to_clean:
227
  try:
@@ -370,7 +382,8 @@ def update_job(job_id):
370
  @app.route("/schedule_job/<job_id>", methods=["POST"])
371
  def schedule_job(job_id):
372
  if not verify_token(request): return jsonify({"error": "Unauthorized"}), 401
373
- data, start_time_iso = request.json, data.get("start_time")
 
374
  if not start_time_iso: return jsonify({"error": "start_time is required"}), 400
375
  job_ref = db.reference(f"jobs/{job_id}")
376
  if not job_ref.get(): return jsonify({"error": "Job not found"}), 404
 
21
 
22
  # --- NEW IMPORTS FOR FILE GENERATION ---
23
  import weasyprint
24
+ import fitz # PyMuPDF
25
 
26
  # === Logging Configuration ===
27
  if not os.path.exists('logs'):
 
72
  # === UPDATED EMAIL SENDING FUNCTION ===
73
  def send_email(to, subject, html, attachments=None):
74
  """
75
+ Sends an email using a direct requests call, with attachment support.
 
76
  """
77
  if not RESEND_API_KEY:
78
  logger.error("RESEND_API_KEY is not configured. Cannot send email.")
 
84
  "Content-Type": "application/json"
85
  }
86
  payload = {
87
+ "from": "Guard Admin <admin@sozofix.tech>",
88
  "to": to.strip(),
89
  "subject": subject,
90
  "html": html,
 
94
  try:
95
  response = requests.post(
96
  url, headers=headers, json=payload,
97
+ verify=certifi.where(), timeout=30
98
  )
99
  response.raise_for_status()
100
  response_data = response.json()
 
138
  logger.error(f"Failed to create PDF attachment: {e}", exc_info=True)
139
  return None
140
 
141
+ # --- DEFINITIVELY FIXED IMAGE GENERATION ---
142
  def create_image_attachment(html_content, filename):
143
+ """
144
+ Creates a PNG image by first rendering HTML to an in-memory PDF,
145
+ then converting that PDF to a high-quality image using PyMuPDF.
146
+ This method has no external command-line dependencies.
147
+ """
148
  try:
149
  filepath = f"/tmp/{filename}"
150
+
151
+ # Step 1: Render HTML to a PDF in memory
152
+ pdf_bytes = weasyprint.HTML(string=html_content).write_pdf()
153
+
154
+ # Step 2: Use PyMuPDF (fitz) to open the in-memory PDF
155
+ pdf_doc = fitz.open(stream=pdf_bytes, filetype="pdf")
156
+
157
+ # Step 3: Get the first page
158
+ page = pdf_doc[0]
159
+
160
+ # Step 4: Render the page to a pixmap (image). Increase DPI for better quality.
161
+ pix = page.get_pixmap(dpi=150)
162
+
163
+ # Step 5: Save the pixmap as a PNG file
164
+ pix.save(filepath)
165
+
166
+ logger.info(f"Successfully created image attachment via PDF conversion: {filepath}")
167
  return filepath
168
  except Exception as e:
169
+ logger.error(f"Failed to create image attachment using PyMuPDF: {e}", exc_info=True)
170
  return None
171
 
172
 
 
189
  except Exception:
190
  human_readable_time = shift_record.get('assigned_at', 'Unknown')
191
 
 
192
  html_content = f"""
193
  <html><head><style>
194
+ body {{ font-family: sans-serif; color: #333; }}
195
+ table {{ border-collapse: collapse; width: 100%; margin-top: 20px; }}
196
+ th, td {{ border: 1px solid #dddddd; text-align: left; padding: 12px; }}
197
  th {{ background-color: #f2f2f2; }}
198
+ h2, h3 {{ color: #1a1a1a; }}
199
  </style></head><body>
200
  <h2>Guard Rotation Notification</h2>
201
  <p><strong>Job:</strong> {job_name}</p>
 
210
  html_content += f"<tr><td>{point_name}</td><td>{members_html}</td></tr>"
211
  html_content += "</tbody></table><p><em>This is an automated notification from the Guard Rotation System.</em></p></body></html>"
212
 
 
213
  base_filename = f"roster_{job_name.replace(' ', '_')}_shift_{shift_number}_{int(time.time())}"
214
  paths_to_clean = []
215
  attachments = []
216
 
217
  try:
 
218
  csv_path = create_csv_attachment(shift_record, f"{base_filename}.csv")
219
  pdf_path = create_pdf_attachment(html_content, f"{base_filename}.pdf")
220
  img_path = create_image_attachment(html_content, f"{base_filename}.png")
221
 
 
222
  file_paths = {'roster.csv': csv_path, 'roster.pdf': pdf_path, 'roster.png': img_path}
223
  for filename, filepath in file_paths.items():
224
  if filepath:
 
227
  content_b64 = base64.b64encode(f.read()).decode('utf-8')
228
  attachments.append({'filename': filename, 'content': content_b64})
229
 
 
230
  subject = f"Guard Rotation - {job_name} (Shift {shift_number})"
231
  for send_to_email in SEND_TO_EMAILS:
232
  logger.info(f"Sending rotation notification with {len(attachments)} attachments to {send_to_email}...")
 
234
  time.sleep(1)
235
 
236
  finally:
 
237
  logger.info(f"Cleaning up {len(paths_to_clean)} temporary files...")
238
  for path in paths_to_clean:
239
  try:
 
382
  @app.route("/schedule_job/<job_id>", methods=["POST"])
383
  def schedule_job(job_id):
384
  if not verify_token(request): return jsonify({"error": "Unauthorized"}), 401
385
+ data = request.json
386
+ start_time_iso = data.get("start_time")
387
  if not start_time_iso: return jsonify({"error": "start_time is required"}), 400
388
  job_ref = db.reference(f"jobs/{job_id}")
389
  if not job_ref.get(): return jsonify({"error": "Job not found"}), 404