-
Notifications
You must be signed in to change notification settings - Fork 12
feat: user submission management v1 #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1b0b487
1984a5a
41b7152
c109f51
5e81f77
75dc1f5
d1e0c5b
9522bc5
186046d
f9880cb
1afa35b
efb357b
b1ddfef
76919a9
f138be4
20b6e7f
2339760
0fc285b
33a9bab
fa98d2c
f0793e2
46aa1c2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,133 @@ | ||
| from datetime import datetime | ||
| from typing import Any | ||
|
|
||
| import frappe | ||
| from frappe import _ | ||
| from frappe.share import add_docshare | ||
| from pydantic import BaseModel, Field, field_validator | ||
|
|
||
| from forms_pro.forms_pro.doctype.form.form import Form | ||
| from forms_pro.utils.form_generator import SubmissionStatus | ||
|
|
||
|
|
||
| class UserSubmissionResponse(BaseModel): | ||
| name: str = Field(description="Name of the submission") | ||
| creation: datetime = Field(description="Creation date of the submission") | ||
| modified: datetime = Field(description="Last modified date of the submission") | ||
| submission_status: str = Field( | ||
| description="Status of the submission", | ||
| alias="fp_submission_status", | ||
| default=SubmissionStatus.SUBMITTED.value, | ||
| ) | ||
|
|
||
| @field_validator("creation", "modified", mode="before") | ||
| @classmethod | ||
| def parse_datetime(cls, v: Any) -> datetime: | ||
| """Convert datetime string to datetime object.""" | ||
| if isinstance(v, str): | ||
| return frappe.utils.get_datetime(v) | ||
| if isinstance(v, datetime): | ||
| return v | ||
| raise ValueError(f"Invalid datetime value: {v}") | ||
|
|
||
|
|
||
| @frappe.whitelist(allow_guest=True) | ||
| def submit_form_response(form_id: str, form_data: list[dict]): | ||
| form = frappe.get_doc("Form", form_id) | ||
| def submit_form_response( | ||
| form_id: str, | ||
| form_data: list[dict], | ||
| submission_status: SubmissionStatus = SubmissionStatus.SUBMITTED, | ||
| ) -> str: | ||
| """ | ||
| Submit a form response | ||
|
|
||
| Args: | ||
| form_id: The ID of the form | ||
| form_data: The data of the form | ||
| submission_status: The status of the submission: Default is `Submitted` | ||
|
|
||
| Returns: | ||
| The name of the submission | ||
| """ | ||
| try: | ||
| form: Form = frappe.get_doc("Form", form_id) | ||
| linked_doctype = form.linked_doctype | ||
|
|
||
| if not form.is_published: | ||
| frappe.throw( | ||
| _("This form is un-published, so responses are no longer being collected."), | ||
| frappe.PermissionError, | ||
| ) | ||
|
|
||
| submission = frappe.new_doc(linked_doctype) | ||
| for data in form_data: | ||
| submission.set(data["fieldname"], data["value"]) | ||
|
|
||
| submission.fp_linked_form = form_id | ||
| submission.fp_submission_status = submission_status.value | ||
| submission.insert(ignore_permissions=True) | ||
|
|
||
| # Share the submission with the owner | ||
| add_docshare( | ||
| doctype=linked_doctype, | ||
| name=submission.name, | ||
| user=frappe.session.user, | ||
| read=1, | ||
| write=1, | ||
| flags={ | ||
| "ignore_share_permission": True, | ||
| }, | ||
| ) | ||
|
|
||
| return submission.name | ||
| except Exception: | ||
| raise | ||
|
|
||
|
|
||
| @frappe.whitelist() | ||
| def get_user_submissions(form_id: str) -> list[UserSubmissionResponse]: | ||
| """ | ||
| Get the submissions for a user | ||
|
|
||
| Args: | ||
| form_id: The ID of the form | ||
|
|
||
| Returns: | ||
| A list of submissions for the user | ||
| """ | ||
|
|
||
| if frappe.session.user == "Guest": | ||
| return [] | ||
|
|
||
| form: Form = frappe.get_doc("Form", form_id) | ||
| linked_doctype = form.linked_doctype | ||
|
|
||
| submission = frappe.new_doc(linked_doctype) | ||
| for data in form_data: | ||
| submission.set(data["fieldname"], data["value"]) | ||
| submission.insert(ignore_permissions=True) | ||
| submissions = frappe.get_all( | ||
| doctype=linked_doctype, | ||
| filters={"owner": frappe.session.user}, | ||
| fields=["name", "creation", "modified", "fp_submission_status"], | ||
| order_by="creation", | ||
| ) | ||
|
|
||
| return [UserSubmissionResponse.model_validate(submission).model_dump() for submission in submissions] | ||
|
|
||
|
|
||
| @frappe.whitelist() | ||
| def get_submission(submission_doctype: str, submission_name: str) -> dict[str, Any]: | ||
| """ | ||
| Get a submission by name | ||
|
|
||
| Args: | ||
| submission_name: The name of the submission | ||
|
|
||
| Returns: | ||
| The submission | ||
| """ | ||
| submission = frappe.get_doc(submission_doctype, submission_name) | ||
|
|
||
| if not frappe.has_permission(doctype=submission.doctype, ptype="read", doc=submission.name): | ||
| frappe.throw( | ||
| _("You do not have permission to read this submission."), | ||
| frappe.PermissionError, | ||
| ) | ||
|
|
||
| return submission.name | ||
| return submission.as_dict() | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,8 @@ | ||||||||||||||||||||||||||||||||||
| from enum import Enum | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| import frappe | ||||||||||||||||||||||||||||||||||
| from frappe import _ | ||||||||||||||||||||||||||||||||||
| from frappe.custom.doctype.custom_field.custom_field import CustomField | ||||||||||||||||||||||||||||||||||
| from frappe.share import add_docshare | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| FORMS_PRO_ROLE = "Forms Pro User" | ||||||||||||||||||||||||||||||||||
|
|
@@ -53,6 +56,33 @@ def create_form(team_id: str): | |||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| USER_FORM_MODULE = "User Forms" | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| class SubmissionStatus(str, Enum): | ||||||||||||||||||||||||||||||||||
| DRAFT = "Draft" | ||||||||||||||||||||||||||||||||||
| SUBMITTED = "Submitted" | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| LINKED_FORM_FIELDOPTIONS = { | ||||||||||||||||||||||||||||||||||
| "label": "Linked Form", | ||||||||||||||||||||||||||||||||||
| "fieldname": "fp_linked_form", | ||||||||||||||||||||||||||||||||||
| "fieldtype": "Link", | ||||||||||||||||||||||||||||||||||
| "options": "Form", | ||||||||||||||||||||||||||||||||||
| "read_only": 1, | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+67
to
+73
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing The test LINKED_FORM_FIELDOPTIONS = {
"label": "Linked Form",
"fieldname": "fp_linked_form",
"fieldtype": "Link",
"options": "Form",
"read_only": 1,
+ "mandatory": 0,
+ "in_list_view": 0,
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| SUBMISSION_STATUS_FIELDOPTIONS = { | ||||||||||||||||||||||||||||||||||
| "label": "Submission Status (Form)", | ||||||||||||||||||||||||||||||||||
| "fieldname": "fp_submission_status", | ||||||||||||||||||||||||||||||||||
| "fieldtype": "Select", | ||||||||||||||||||||||||||||||||||
| "options": "\n".join([status.value for status in SubmissionStatus]), | ||||||||||||||||||||||||||||||||||
| "default": SubmissionStatus.DRAFT.value, | ||||||||||||||||||||||||||||||||||
| "read_only": 1, | ||||||||||||||||||||||||||||||||||
| "in_list_view": 1, | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| class FormGenerator: | ||||||||||||||||||||||||||||||||||
| def __init__( | ||||||||||||||||||||||||||||||||||
| self, | ||||||||||||||||||||||||||||||||||
|
|
@@ -66,6 +96,7 @@ def __init__( | |||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| def generate(self) -> None: | ||||||||||||||||||||||||||||||||||
| self._initialize_doctype() | ||||||||||||||||||||||||||||||||||
| self._add_status_field() | ||||||||||||||||||||||||||||||||||
| self._initialize_form_document() | ||||||||||||||||||||||||||||||||||
| frappe.clear_cache() | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
@@ -124,6 +155,39 @@ def _initialize_doctype(self) -> None: | |||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| self.doctype = placeholder_doctype | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| def _add_status_field(self) -> None: | ||||||||||||||||||||||||||||||||||
| # If the doctype is not custom, add a `Custom Field` to the doctype | ||||||||||||||||||||||||||||||||||
| if not self.doctype.custom: | ||||||||||||||||||||||||||||||||||
| self._add_custom_field_for_status() | ||||||||||||||||||||||||||||||||||
| self._add_custom_field_for_linked_form() | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| # If the doctype is custom, we can go ahead and append the field directly to the doctype | ||||||||||||||||||||||||||||||||||
| self.doctype.append( | ||||||||||||||||||||||||||||||||||
| "fields", | ||||||||||||||||||||||||||||||||||
| SUBMISSION_STATUS_FIELDOPTIONS, | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| # Add the linked form field | ||||||||||||||||||||||||||||||||||
| self.doctype.append( | ||||||||||||||||||||||||||||||||||
| "fields", | ||||||||||||||||||||||||||||||||||
| LINKED_FORM_FIELDOPTIONS, | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| self.doctype.save(ignore_permissions=True) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| def _add_custom_field_for_status(self) -> None: | ||||||||||||||||||||||||||||||||||
| custom_field: CustomField = frappe.new_doc("Custom Field") | ||||||||||||||||||||||||||||||||||
| custom_field.update(SUBMISSION_STATUS_FIELDOPTIONS) | ||||||||||||||||||||||||||||||||||
| custom_field.dt = self.doctype.name | ||||||||||||||||||||||||||||||||||
| custom_field.is_system_generated = True | ||||||||||||||||||||||||||||||||||
| custom_field.insert(ignore_permissions=True) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| def _add_custom_field_for_linked_form(self) -> None: | ||||||||||||||||||||||||||||||||||
| custom_field: CustomField = frappe.new_doc("Custom Field") | ||||||||||||||||||||||||||||||||||
| custom_field.update(LINKED_FORM_FIELDOPTIONS) | ||||||||||||||||||||||||||||||||||
| custom_field.dt = self.doctype.name | ||||||||||||||||||||||||||||||||||
| custom_field.is_system_generated = True | ||||||||||||||||||||||||||||||||||
| custom_field.insert(ignore_permissions=True) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| def _initialize_form_document(self) -> None: | ||||||||||||||||||||||||||||||||||
| form_document = frappe.new_doc("Form") | ||||||||||||||||||||||||||||||||||
| form_document.linked_doctype = self.doctype.name | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,4 @@ | |
| // Generated by unplugin-auto-import | ||
| // biome-ignore lint: disable | ||
| export {} | ||
| declare global { | ||
|
|
||
| } | ||
| declare global {} | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing filter for
fp_linked_formmay return unrelated submissions.The query retrieves all submissions from
linked_doctypeowned by the user, but doesn't filter by the specific form. If multiple forms share the samelinked_doctype, this will return submissions from all forms rather than just the requestedform_id.🐛 Proposed fix
submissions = frappe.get_all( doctype=linked_doctype, - filters={"owner": frappe.session.user}, + filters={"owner": frappe.session.user, "fp_linked_form": form_id}, fields=["name", "creation", "modified", "fp_submission_status"], order_by="creation", )🤖 Prompt for AI Agents