snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-15 15:05:30 +05:00
commit 64898b2c79
8999 changed files with 1757358 additions and 0 deletions

View File

View File

@@ -0,0 +1,29 @@
# Zulip Jotform integration
Get Zulip notifications for your Jotform responses!
{start_tabs}
1. {!create-an-incoming-webhook.md!}
1. {!generate-webhook-url-basic.md!}
1. In the **Form Builder** on Jotform, go to **Settings**. Click on
**INTEGRATIONS**. Search for and select **WebHooks**.
1. Paste the URL generated above into the field, and click
**COMPLETE INTEGRATION**.
!!! tip ""
By default, the form's title will be used as the Zulip topic.
{end_tabs}
{!congrats.md!}
![](/static/images/integrations/jotform/001.png)
### Related documentation
{!webhooks-url-specification.md!}

View File

@@ -0,0 +1,93 @@
--------------------------J1JNT4vbVO4FCC1qLG4HSv
Content-Disposition: form-data; name="action"
--------------------------J1JNT4vbVO4FCC1qLG4HSv
Content-Disposition: form-data; name="webhookURL"
https://3a95-27-5-198-172.ngrok-free.app/api/v1/external/jotform?api_key=DhXGrkyy2bK2SXMQMXwhs95H4kVVwv2w
--------------------------J1JNT4vbVO4FCC1qLG4HSv
Content-Disposition: form-data; name="username"
UserNiloth
--------------------------J1JNT4vbVO4FCC1qLG4HSv
Content-Disposition: form-data; name="formID"
243231271343446
--------------------------J1JNT4vbVO4FCC1qLG4HSv
Content-Disposition: form-data; name="type"
WEB
--------------------------J1JNT4vbVO4FCC1qLG4HSv
Content-Disposition: form-data; name="customParams"
--------------------------J1JNT4vbVO4FCC1qLG4HSv
Content-Disposition: form-data; name="product"
--------------------------J1JNT4vbVO4FCC1qLG4HSv
Content-Disposition: form-data; name="formTitle"
Tutor Appointment Form
--------------------------J1JNT4vbVO4FCC1qLG4HSv
Content-Disposition: form-data; name="customTitle"
--------------------------J1JNT4vbVO4FCC1qLG4HSv
Content-Disposition: form-data; name="submissionID"
6084250982513018472
--------------------------J1JNT4vbVO4FCC1qLG4HSv
Content-Disposition: form-data; name="event"
--------------------------J1JNT4vbVO4FCC1qLG4HSv
Content-Disposition: form-data; name="documentID"
--------------------------J1JNT4vbVO4FCC1qLG4HSv
Content-Disposition: form-data; name="teamID"
--------------------------J1JNT4vbVO4FCC1qLG4HSv
Content-Disposition: form-data; name="subject"
--------------------------J1JNT4vbVO4FCC1qLG4HSv
Content-Disposition: form-data; name="isSilent"
--------------------------J1JNT4vbVO4FCC1qLG4HSv
Content-Disposition: form-data; name="customBody"
--------------------------J1JNT4vbVO4FCC1qLG4HSv
Content-Disposition: form-data; name="rawRequest"
{"slug":"submit\/243231271343446","jsExecutionTracker":"build-date-1732271172685=>init-started:1732615894703=>validator-called:1732615894706=>validator-mounted-false:1732615894706=>init-complete:1732615894707=>onsubmit-fired:1732615897940=>observerSubmitHandler_received-submit-event:1732615897941=>submit-validation-passed:1732615897942=>observerSubmitHandler_validation-passed-submitting-form:1732615897944","submitSource":"form","buildDate":"1732271172685","uploadServerUrl":"https:\/\/upload.jotform.com\/upload","eventObserver":"1","q3_studentsName":{"first":"Niloth","last":"P"},"q33_typeOf":"Online Tutoring","q34_subjectFor":"Math","q35_grade":"12","q27_priorTutoring":"No","q28_appointment":{"implementation":"new","date":"","duration":"60","timezone":"Asia\/Calcutta (GMT+06:30)"},"q15_date":{"month":"","day":"","year":""},"q19_table":{"colIds":"[\"0\",\"1\",\"2\",\"3\"]","rowIds":"[\"0\",\"1\",\"2\",\"3\"]"},"timeToSubmit":"3","temp_upload":{"q5_identityProof":["Student_ID_!#$&'()*+,-.;=@[]^_`{}~ .png#jotformfs-e4f4ece4d0a90#01940839-37f4-7a46-9b4e-71107f08774a", "Driving.license.png#jotformfs-e4f4ece4d0a90#01940839-37f4-7a46-9b4e-71107f08774a"], "q6_reports":["Report Card.pdf#jotformfs-e4f4ece4d0a90#01940839-37f4-7a46-9b4e-71107f08774a"]},"file_server":"jotforms-e4f4ece4d0a90#01940839-37f4-7a46-9b4e-71107f08774a","preview":"true","validatedNewRequiredFieldIDs":"{\"new\":1}","path":"\/submit\/243231271343446","q7_description":"","identityProof":["https:\/\/www.jotform.com\/uploads\/UserNiloth\/243231271343446\/6111139644845227683\/Student_ID_%21_%24%26%27%28%29_%2C-.%3B%3D%40%5B%5D%5E_%60%7B%7D~%20.png","https:\/\/www.jotform.com\/uploads\/UserNiloth\/243231271343446\/6111139644845227684\/Driving.license.png"],"reports":["https:\/\/www.jotform.com\/uploads\/UserNiloth\/243231271343446\/6111139644845227685\/Report%20Card.pdf"]}
--------------------------J1JNT4vbVO4FCC1qLG4HSv
Content-Disposition: form-data; name="fromTable"
--------------------------J1JNT4vbVO4FCC1qLG4HSv
Content-Disposition: form-data; name="appID"
--------------------------J1JNT4vbVO4FCC1qLG4HSv
Content-Disposition: form-data; name="pretty"
Student's Name:Niloth P, Type of Tutoring:Online Tutoring, Subject for Tutoring:Math, Grade:12, Prior Tutoring?:No, Identity Proof:Student_ID_!_$&'()_,-.;=@[]^_`{}~ .png Driving.license.png, Reports:Report Card.pdf
--------------------------J1JNT4vbVO4FCC1qLG4HSv
Content-Disposition: form-data; name="unread"
--------------------------J1JNT4vbVO4FCC1qLG4HSv
Content-Disposition: form-data; name="parent"
--------------------------J1JNT4vbVO4FCC1qLG4HSv
Content-Disposition: form-data; name="ip"
27.51.18.17
--------------------------J1JNT4vbVO4FCC1qLG4HSv--

View File

@@ -0,0 +1,93 @@
--------------------------BIoxRptelgempTlOi8T0XA
Content-Disposition: form-data; name="action"
--------------------------BIoxRptelgempTlOi8T0XA
Content-Disposition: form-data; name="webhookURL"
https://a395-103-151-209-147.ngrok-free.app/api/v1/external/jotform?api_key=6SuIPsdyiL6Bgljov4wp67wD4rqHRWOg&stream=15&topic=jotform
--------------------------BIoxRptelgempTlOi8T0XA
Content-Disposition: form-data; name="username"
kolanuvarun739
--------------------------BIoxRptelgempTlOi8T0XA
Content-Disposition: form-data; name="formID"
243490908500051
--------------------------BIoxRptelgempTlOi8T0XA
Content-Disposition: form-data; name="type"
WEB
--------------------------BIoxRptelgempTlOi8T0XA
Content-Disposition: form-data; name="customParams"
--------------------------BIoxRptelgempTlOi8T0XA
Content-Disposition: form-data; name="product"
--------------------------BIoxRptelgempTlOi8T0XA
Content-Disposition: form-data; name="formTitle"
Sample testing
--------------------------BIoxRptelgempTlOi8T0XA
Content-Disposition: form-data; name="customTitle"
--------------------------BIoxRptelgempTlOi8T0XA
Content-Disposition: form-data; name="submissionID"
6197916587414311452
--------------------------BIoxRptelgempTlOi8T0XA
Content-Disposition: form-data; name="event"
--------------------------BIoxRptelgempTlOi8T0XA
Content-Disposition: form-data; name="documentID"
--------------------------BIoxRptelgempTlOi8T0XA
Content-Disposition: form-data; name="teamID"
--------------------------BIoxRptelgempTlOi8T0XA
Content-Disposition: form-data; name="subject"
--------------------------BIoxRptelgempTlOi8T0XA
Content-Disposition: form-data; name="isSilent"
--------------------------BIoxRptelgempTlOi8T0XA
Content-Disposition: form-data; name="customBody"
--------------------------BIoxRptelgempTlOi8T0XA
Content-Disposition: form-data; name="rawRequest"
{"slug":"submit\/243490908500051","jsExecutionTracker":"build-date-1743981991368=>init-started:1743982445902=>validator-called:1743982445907=>validator-mounted-false:1743982445907=>init-complete:1743982445910=>onsubmit-fired:1743982457902=>observerSubmitHandler_received-submit-event:1743982457903=>submit-validation-passed:1743982457907=>observerSubmitHandler_validation-passed-submitting-form:1743982457914","submitSource":"form","buildDate":"1743981991368","uploadServerUrl":"https:\/\/upload.jotform.com\/upload","eventObserver":"1","q10_key1With":"Value1 with colon: and comma, end","q11_sameKey":"Value 1","q12_sameKey2":"Value 2","q13_sameKeyValue":"Same Key-Value","q14_value2":"Value 3","q23_multipleChoice":["Option; 1","Option2"],"event_id":"1743982445903_243490908500051_4IYtoJz","timeToSubmit":"11","temp_upload":{"q22_fileUpload":["error; frontend, UI.png#jotformfs-e4f4ece4d0a90#01960d74-6a25-7398-87c7-df56ce6711f1","Screenshot_20250331_201054.png#jotformfs-e4f4ece4d0a90#01960d74-6bbc-7e1b-8187-9028f55b6ef6"]},"file_server":"jotformfs-e4f4ece4d0a90#01960d74-6bbc-7e1b-8187-9028f55b6ef6","validatedNewRequiredFieldIDs":"{\"new\":1}","path":"\/submit\/243490908500051","fileUpload":["https:\/\/www.jotform.com\/uploads\/kolanuvarun739\/243490908500051\/6197916587414311452\/error%3B%20frontend%2C%20UI.png","https:\/\/www.jotform.com\/uploads\/kolanuvarun739\/243490908500051\/6197916587414311452\/Screenshot_20250331_201054.png"]}
--------------------------BIoxRptelgempTlOi8T0XA
Content-Disposition: form-data; name="fromTable"
--------------------------BIoxRptelgempTlOi8T0XA
Content-Disposition: form-data; name="appID"
--------------------------BIoxRptelgempTlOi8T0XA
Content-Disposition: form-data; name="pretty"
Key1 with colon: and comma, end:Value1 with colon: and comma, end, Same Key:Value 1, Same Key:Value 2, Same Key-Value:Same Key-Value, Value 2:Value 3, Multiple Choice Question, options::Option; 1 Option2, File Upload with colon : and comma , end:error; frontend, UI.png Screenshot_20250331_201054.png
--------------------------BIoxRptelgempTlOi8T0XA
Content-Disposition: form-data; name="unread"
--------------------------BIoxRptelgempTlOi8T0XA
Content-Disposition: form-data; name="parent"
--------------------------BIoxRptelgempTlOi8T0XA
Content-Disposition: form-data; name="ip"
103.151.209.147
--------------------------BIoxRptelgempTlOi8T0XA--

View File

@@ -0,0 +1,93 @@
--------------------------1WSA91c0mNqsH82UlEp0qj
Content-Disposition: form-data; name="action"
--------------------------1WSA91c0mNqsH82UlEp0qj
Content-Disposition: form-data; name="webhookURL"
https://959c-103-151-209-117.ngrok-free.app/api/v1/external/jotform?api_key=3BKWHdX6ZZpvIkzts68PKE30Dw8871kS&stream=22&topic=Feedback+Form
--------------------------1WSA91c0mNqsH82UlEp0qj
Content-Disposition: form-data; name="username"
kolanuvarun739
--------------------------1WSA91c0mNqsH82UlEp0qj
Content-Disposition: form-data; name="formID"
243615086540051
--------------------------1WSA91c0mNqsH82UlEp0qj
Content-Disposition: form-data; name="type"
WEB
--------------------------1WSA91c0mNqsH82UlEp0qj
Content-Disposition: form-data; name="customParams"
--------------------------1WSA91c0mNqsH82UlEp0qj
Content-Disposition: form-data; name="product"
--------------------------1WSA91c0mNqsH82UlEp0qj
Content-Disposition: form-data; name="formTitle"
Feedback Form
--------------------------1WSA91c0mNqsH82UlEp0qj
Content-Disposition: form-data; name="customTitle"
--------------------------1WSA91c0mNqsH82UlEp0qj
Content-Disposition: form-data; name="submissionID"
6114090137116205381
--------------------------1WSA91c0mNqsH82UlEp0qj
Content-Disposition: form-data; name="event"
--------------------------1WSA91c0mNqsH82UlEp0qj
Content-Disposition: form-data; name="documentID"
--------------------------1WSA91c0mNqsH82UlEp0qj
Content-Disposition: form-data; name="teamID"
--------------------------1WSA91c0mNqsH82UlEp0qj
Content-Disposition: form-data; name="subject"
--------------------------1WSA91c0mNqsH82UlEp0qj
Content-Disposition: form-data; name="isSilent"
--------------------------1WSA91c0mNqsH82UlEp0qj
Content-Disposition: form-data; name="customBody"
--------------------------1WSA91c0mNqsH82UlEp0qj
Content-Disposition: form-data; name="rawRequest"
{"slug":"submit\/243615086540051","jsExecutionTracker":"build-date-1735597456482=>init-started:1735597457312=>validator-called:1735597457313=>validator-mounted-false:1735597457313=>init-complete:1735597457316=>interval-complete:1735597478103=>onsubmit-fired:1735598288656=>observerSubmitHandler_received-submit-event:1735598288656=>submit-validation-passed:1735598288659=>observerSubmitHandler_validation-passed-submitting-form:1735598288668=>init-started:1735598444979=>validator-called:1735598444980=>validator-mounted-false:1735598444980=>init-complete:1735598444983=>init-started:1735598447982=>validator-called:1735598447983=>validator-mounted-false:1735598447983=>init-complete:1735598447985=>interval-complete:1735598469193=>onsubmit-fired:1735598480950=>observerSubmitHandler_received-submit-event:1735598480950=>submit-validation-passed:1735598480954=>observerSubmitHandler_validation-passed-submitting-form:1735598480962=>init-started:1735598975826=>validator-called:1735598975828=>validator-mounted-false:1735598975828=>init-complete:1735598975836=>interval-complete:1735598996957=>onsubmit-fired:1735599067954=>observerSubmitHandler_received-submit-event:1735599067954=>submit-validation-passed:1735599067956=>observerSubmitHandler_validation-passed-submitting-form:1735599067962=>init-started:1735599159857=>validator-called:1735599159859=>validator-mounted-false:1735599159859=>init-complete:1735599159861=>interval-complete:1735599181032=>onsubmit-fired:1735599182145=>observerSubmitHandler_received-submit-event:1735599182145=>submit-validation-passed:1735599182149=>observerSubmitHandler_validation-passed-submitting-form:1735599182155=>init-started:1735599251872=>validator-called:1735599251873=>validator-mounted-false:1735599251873=>init-complete:1735599251877=>interval-complete:1735599272970=>onsubmit-fired:1735599584836=>observerSubmitHandler_received-submit-event:1735599584836=>submit-validation-passed:1735599584840=>observerSubmitHandler_validation-passed-submitting-form:1735599584847=>init-started:1735599791882=>validator-called:1735599791884=>validator-mounted-false:1735599791884=>init-complete:1735599791885=>interval-complete:1735599813033=>onsubmit-fired:1735599813327=>observerSubmitHandler_received-submit-event:1735599813327=>submit-validation-passed:1735599813330=>observerSubmitHandler_validation-passed-submitting-form:1735599813338","submitSource":"form","buildDate":"1735598447069","uploadServerUrl":"https:\/\/upload.jotform.com\/upload","eventObserver":"1","q3_howOften":"Daily","q4_howLikely":"9","q7_feedback":"The new personalized recommendations feature is great!","event_id":"1735599791882_243615086540051_ZWbBbs3","timeToSubmit":"20","temp_upload":{"q9_filesRelated":["frontend setup.jpg#jotformfs-e4f4ece4d0a90#019419cf-4a02-7e71-a373-8a2d40622c3d","workflow.png#jotformfs-e4f4ece4d0a90#019419cf-4b57-7881-82a8-ab02b2cde277"]},"file_server":"jotformfs-e4f4ece4d0a90#019419cf-6b35-79cf-86c7-880b6c4343ca","validatedNewRequiredFieldIDs":"{\"new\":1}","path":"\/submit\/243615086540051","filesRelated":["https:\/\/www.jotform.com\/uploads\/kolanuvarun739\/243615086540051\/6114090137116205381\/frontend%20setup.jpg","https:\/\/www.jotform.com\/uploads\/kolanuvarun739\/243615086540051\/6114090137116205381\/workflow.png"]}
--------------------------1WSA91c0mNqsH82UlEp0qj
Content-Disposition: form-data; name="fromTable"
--------------------------1WSA91c0mNqsH82UlEp0qj
Content-Disposition: form-data; name="appID"
--------------------------1WSA91c0mNqsH82UlEp0qj
Content-Disposition: form-data; name="pretty"
How often do you use the application?:Daily, How likely are you to recommend it to a friend on a scale of 0-10?:9, Feedback:The new personalized recommendations feature is great!, Upload images of your customized setup to get featured!:frontend setup.jpg workflow.png
--------------------------1WSA91c0mNqsH82UlEp0qj
Content-Disposition: form-data; name="unread"
--------------------------1WSA91c0mNqsH82UlEp0qj
Content-Disposition: form-data; name="parent"
--------------------------1WSA91c0mNqsH82UlEp0qj
Content-Disposition: form-data; name="ip"
103.151.209.117
--------------------------1WSA91c0mNqsH82UlEp0qj--

View File

@@ -0,0 +1,70 @@
from typing_extensions import override
from zerver.lib.test_classes import WebhookTestCase
from zerver.lib.webhooks.common import parse_multipart_string
class JotformHookTests(WebhookTestCase):
def test_response(self) -> None:
expected_title = "Tutor Appointment Form"
expected_message = """
* **Student's Name**: Niloth P
* **Type of Tutoring**: Online Tutoring
* **Subject for Tutoring**: Math
* **Grade**: 12
* **Prior Tutoring?** No
* **Identity Proof**: [Student_ID_!_$&'()_,-.;=@[]^_`{}~ .png](https://www.jotform.com/uploads/UserNiloth/243231271343446/6111139644845227683/Student_ID_%21_%24%26%27%28%29_%2C-.%3B%3D%40%5B%5D%5E_%60%7B%7D~%20.png), [Driving.license.png](https://www.jotform.com/uploads/UserNiloth/243231271343446/6111139644845227684/Driving.license.png)
* **Reports**: [Report Card.pdf](https://www.jotform.com/uploads/UserNiloth/243231271343446/6111139644845227685/Report%20Card.pdf)""".strip()
self.check_webhook(
"response",
expected_title,
expected_message,
content_type="multipart/form-data",
)
def test_screenshot_response(self) -> None:
expected_title = "Feedback Form"
expected_message = """
* **How often do you use the application?** Daily
* **How likely are you to recommend it to a friend on a scale of 0-10?** 9
* **Feedback**: The new personalized recommendations feature is great!
* **Upload images of your customized setup to get featured!**: [frontend setup.jpg](https://www.jotform.com/uploads/kolanuvarun739/243615086540051/6114090137116205381/frontend%20setup.jpg), [workflow.png](https://www.jotform.com/uploads/kolanuvarun739/243615086540051/6114090137116205381/workflow.png)""".strip()
self.check_webhook(
"screenshot_response",
expected_title,
expected_message,
content_type="multipart/form-data",
)
def test_response_with_colon_comma_characters(self) -> None:
expected_title = "Sample testing"
expected_message = """
* **Key1 with colon: and comma, end**: Value1 with colon: and comma, end
* **Same Key**: Value 1
* **Same Key**: Value 2
* **Same Key-Value**: Same Key-Value
* **Value 2**: Value 3
* **Multiple Choice Question, options:**: Option; 1 Option2
* **File Upload with colon : and comma , end**: [error; frontend, UI.png](https://www.jotform.com/uploads/kolanuvarun739/243490908500051/6197916587414311452/error%3B%20frontend%2C%20UI.png), [Screenshot_20250331_201054.png](https://www.jotform.com/uploads/kolanuvarun739/243490908500051/6197916587414311452/Screenshot_20250331_201054.png)""".strip()
self.check_webhook(
"response_with_colon_comma_characters",
expected_title,
expected_message,
content_type="multipart/form-data",
)
def test_bad_payload(self) -> None:
with self.assertRaisesRegex(AssertionError, "Unable to handle Jotform payload"):
self.check_webhook("response")
@override
def get_payload(self, fixture_name: str) -> dict[str, str]:
body = self.webhook_fixture_data("jotform", fixture_name, file_type="multipart")
return parse_multipart_string(body)

View File

@@ -0,0 +1,169 @@
# Webhooks for external integrations.
import json
from typing import Any
from urllib.parse import unquote
from django.http import HttpRequest, HttpResponse
from django.utils.translation import gettext as _
from zerver.decorator import webhook_view
from zerver.lib.exceptions import JsonableError
from zerver.lib.response import json_success
from zerver.lib.typed_endpoint import typed_endpoint_without_parameters
from zerver.lib.webhooks.common import check_send_webhook_message
from zerver.models import UserProfile
def format_uploaded_file_links(file_question_value: str, file_url_mappings: dict[str, str]) -> str:
words = file_question_value.split()
url_markdown_list = []
current_file_name_words = []
for word in words:
current_file_name_words.append(word)
# Greedy approach to find the filenames in file_url_mappings.
potential_file_name = " ".join(current_file_name_words)
if potential_file_name in file_url_mappings:
file_url = file_url_mappings.get(potential_file_name)
url_markdown_list.append(f"[{potential_file_name}]({file_url})")
current_file_name_words = []
return ", ".join(url_markdown_list)
def is_non_file_upload_question_with_response(field: str, raw_request: dict[str, Any]) -> bool:
"""
File upload questions are in raw_request["temp_upload"].
So, the other questions are the keys in raw_request starting
with "q". We exclude any questions with a zero length value,
because they are not in the "pretty" data of the payload.
"""
field_value = raw_request.get(field)
return bool(
field
and field[0] == "q"
and field_value
and (
isinstance(field_value, str)
or (
isinstance(field_value, dict)
# Non-empty Appointment, Date fields
and "" not in field_value.values()
# Non-empty Input Table field
and not (
len(field_value) == 2 and "colIds" in field_value and "rowIds" in field_value
)
)
# Multiple choice questions
or isinstance(field_value, list)
)
)
def format_value(value: str | list[str] | dict[str, str]) -> str:
if isinstance(value, str):
return value
elif isinstance(value, list):
return " ".join(value)
return " ".join(value.values())
def get_pretty_fields(pretty: str, values: list[str]) -> list[tuple[str, str]]:
"""
The format of "pretty" data in the payload is "key1:value1, key2:value2, ..."
The parameter "values" contains all substrings that appear as values
in the pretty data. Extracts each key-value pair by locating the position
of each value in the string and separating the corresponding key and value,
by tracking the starting position of the next key-value pair.
This approach is necessary because keys and values may contain colons
or commas, making it unreliable to simply split the string using ', '.
Returns a list of (key, value) pairs to preserve order and allow duplicate keys.
"""
pretty_fields = []
pair_starting_index = 0
for value in values:
value_length = len(value)
possible_match_index = pretty.find(value + ":", pair_starting_index)
# If the current pair starts with a key that is equal to the value
if possible_match_index == pair_starting_index:
key = value
val = value
# 3 characters for colon, comma and space.
pair_starting_index += 2 * value_length + 3
else:
value_occurrence_index = pretty.find(value, pair_starting_index)
value_ending_index = value_occurrence_index + value_length
# -1 for colon, separating key and value
key = pretty[pair_starting_index : value_occurrence_index - 1]
val = pretty[value_occurrence_index:value_ending_index]
# 2 characters for comma and space, which separate the fields
pair_starting_index = value_ending_index + 2
pretty_fields.append((key, val))
return pretty_fields
@webhook_view("Jotform")
@typed_endpoint_without_parameters
def api_jotform_webhook(
request: HttpRequest,
user_profile: UserProfile,
) -> HttpResponse:
payload = request.POST
topic_name = payload.get("formTitle")
raw_request = json.loads(payload.get("rawRequest", "{}"))
non_file_upload_values_in_pretty_data = [
format_value(value)
for field, value in raw_request.items()
if is_non_file_upload_question_with_response(field, raw_request)
]
url_mappings = {}
file_values_in_pretty_data = []
if raw_request.get("temp_upload"):
upload_keys = raw_request.get("temp_upload").keys()
file_questions_keys = [key.split("_", 1)[-1] for key in upload_keys]
file_values_in_pretty_data = [
" ".join(unquote(url.split("/")[-1]) for url in raw_request.get(key, []))
for key in file_questions_keys
]
url_mappings = {
unquote(url.split("/")[-1]): url
for key in file_questions_keys
for url in raw_request.get(key, [])
}
# Non file values come earlier than file values in the "pretty" data of the payload.
pretty_values = non_file_upload_values_in_pretty_data + list(file_values_in_pretty_data)
pretty_fields = get_pretty_fields(payload.get("pretty", ""), pretty_values)
if not topic_name or not pretty_fields:
raise JsonableError(_("Unable to handle Jotform payload"))
form_response = ""
for index, (label, value) in enumerate(pretty_fields):
separator = " " if label.endswith("?") else ": "
# File upload fields are last in the "pretty" payload data.
if index >= len(non_file_upload_values_in_pretty_data):
value = format_uploaded_file_links(value, url_mappings)
# We add a new line so that image files are rendered
# correctly in the message.
form_response += "\n"
form_response += f"* **{label}**{separator}{value}\n"
message = form_response.strip()
check_send_webhook_message(request, user_profile, topic_name, message)
return json_success(request)