diff --git a/resources/sql/autopatches/20140701.legalexemption.1.sql b/resources/sql/autopatches/20140701.legalexemption.1.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20140701.legalexemption.1.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_legalpad.legalpad_documentsignature + ADD isExemption BOOL NOT NULL DEFAULT 0 AFTER verified; diff --git a/resources/sql/autopatches/20140701.legalexemption.2.sql b/resources/sql/autopatches/20140701.legalexemption.2.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20140701.legalexemption.2.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_legalpad.legalpad_documentsignature + ADD exemptionPHID VARCHAR(64) COLLATE utf8_bin AFTER isExemption; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -873,10 +873,12 @@ 'LegalpadDocumentSearchEngine' => 'applications/legalpad/query/LegalpadDocumentSearchEngine.php', 'LegalpadDocumentSignController' => 'applications/legalpad/controller/LegalpadDocumentSignController.php', 'LegalpadDocumentSignature' => 'applications/legalpad/storage/LegalpadDocumentSignature.php', + 'LegalpadDocumentSignatureAddController' => 'applications/legalpad/controller/LegalpadDocumentSignatureAddController.php', 'LegalpadDocumentSignatureListController' => 'applications/legalpad/controller/LegalpadDocumentSignatureListController.php', 'LegalpadDocumentSignatureQuery' => 'applications/legalpad/query/LegalpadDocumentSignatureQuery.php', 'LegalpadDocumentSignatureSearchEngine' => 'applications/legalpad/query/LegalpadDocumentSignatureSearchEngine.php', 'LegalpadDocumentSignatureVerificationController' => 'applications/legalpad/controller/LegalpadDocumentSignatureVerificationController.php', + 'LegalpadDocumentSignatureViewController' => 'applications/legalpad/controller/LegalpadDocumentSignatureViewController.php', 'LegalpadMockMailReceiver' => 'applications/legalpad/mail/LegalpadMockMailReceiver.php', 'LegalpadReplyHandler' => 'applications/legalpad/mail/LegalpadReplyHandler.php', 'LegalpadTransaction' => 'applications/legalpad/storage/LegalpadTransaction.php', @@ -3638,10 +3640,12 @@ 0 => 'LegalpadDAO', 1 => 'PhabricatorPolicyInterface', ), + 'LegalpadDocumentSignatureAddController' => 'LegalpadController', 'LegalpadDocumentSignatureListController' => 'LegalpadController', 'LegalpadDocumentSignatureQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'LegalpadDocumentSignatureSearchEngine' => 'PhabricatorApplicationSearchEngine', 'LegalpadDocumentSignatureVerificationController' => 'LegalpadController', + 'LegalpadDocumentSignatureViewController' => 'LegalpadController', 'LegalpadMockMailReceiver' => 'PhabricatorObjectMailReceiver', 'LegalpadReplyHandler' => 'PhabricatorMailReplyHandler', 'LegalpadTransaction' => 'PhabricatorApplicationTransaction', diff --git a/src/applications/legalpad/application/PhabricatorApplicationLegalpad.php b/src/applications/legalpad/application/PhabricatorApplicationLegalpad.php --- a/src/applications/legalpad/application/PhabricatorApplicationLegalpad.php +++ b/src/applications/legalpad/application/PhabricatorApplicationLegalpad.php @@ -54,8 +54,11 @@ 'LegalpadDocumentSignatureVerificationController', 'signatures/(?:(?P<id>\d+)/)?(?:query/(?P<queryKey>[^/]+)/)?' => 'LegalpadDocumentSignatureListController', + 'addsignature/(?P<id>\d+)/' => 'LegalpadDocumentSignatureAddController', + 'signature/(?P<id>\d+)/' => 'LegalpadDocumentSignatureViewController', 'document/' => array( - 'preview/' => 'PhabricatorMarkupPreviewController'), + 'preview/' => 'PhabricatorMarkupPreviewController', + ), )); } diff --git a/src/applications/legalpad/controller/LegalpadDocumentSignController.php b/src/applications/legalpad/controller/LegalpadDocumentSignController.php --- a/src/applications/legalpad/controller/LegalpadDocumentSignController.php +++ b/src/applications/legalpad/controller/LegalpadDocumentSignController.php @@ -101,14 +101,26 @@ // In this case, we know they've signed. $signed_at = $signature->getDateCreated(); + + if ($signature->getIsExemption()) { + $exemption_phid = $signature->getExemptionPHID(); + $handles = $this->loadViewerHandles(array($exemption_phid)); + $exemption_handle = $handles[$exemption_phid]; + + $signed_text = pht( + 'You do not need to sign this document. '. + '%s added a signature exemption for you on %s.', + $exemption_handle->renderLink(), + phabricator_datetime($signed_at, $viewer)); + } else { + $signed_text = pht( + 'You signed this document on %s.', + phabricator_datetime($signed_at, $viewer)); + } + $signed_status = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) - ->setErrors( - array( - pht( - 'You signed this document on %s.', - phabricator_datetime($signed_at, $viewer)), - )); + ->setErrors(array($signed_text)); } $e_name = true; diff --git a/src/applications/legalpad/controller/LegalpadDocumentSignatureAddController.php b/src/applications/legalpad/controller/LegalpadDocumentSignatureAddController.php new file mode 100644 --- /dev/null +++ b/src/applications/legalpad/controller/LegalpadDocumentSignatureAddController.php @@ -0,0 +1,127 @@ +<?php + +final class LegalpadDocumentSignatureAddController extends LegalpadController { + + private $id; + + public function willProcessRequest(array $data) { + $this->id = $data['id']; + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $document = id(new LegalpadDocumentQuery()) + ->setViewer($viewer) + ->needDocumentBodies(true) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->withIDs(array($this->id)) + ->executeOne(); + if (!$document) { + return new Aphront404Response(); + } + + $next_uri = $this->getApplicationURI('signatures/'.$document->getID().'/'); + + $e_user = true; + $v_users = array(); + $v_notes = ''; + $errors = array(); + + if ($request->isFormPost()) { + $v_notes = $request->getStr('notes'); + $v_users = array_slice($request->getArr('users'), 0, 1); + + $user_phid = head($v_users); + if (!$user_phid) { + $e_user = pht('Required'); + $errors[] = pht('You must choose a user to exempt.'); + } else { + $user = id(new PhabricatorPeopleQuery()) + ->setViewer($viewer) + ->withPHIDs(array($user_phid)) + ->executeOne(); + + if (!$user) { + $e_user = pht('Invalid'); + $errors[] = pht('That user does not exist.'); + } else { + $signature = id(new LegalpadDocumentSignatureQuery()) + ->setViewer($viewer) + ->withDocumentPHIDs(array($document->getPHID())) + ->withSignerPHIDs(array($user->getPHID())) + ->executeOne(); + if ($signature) { + $e_user = pht('Signed'); + $errors[] = pht('That user has already signed this document.'); + } else { + $e_user = null; + } + } + } + + if (!$errors) { + $name = $user->getRealName(); + $email = $user->loadPrimaryEmailAddress(); + + $signature = id(new LegalpadDocumentSignature()) + ->setDocumentPHID($document->getPHID()) + ->setDocumentVersion($document->getVersions()) + ->setSignerPHID($user->getPHID()) + ->setSignerName($name) + ->setSignerEmail($email) + ->setIsExemption(1) + ->setExemptionPHID($viewer->getPHID()) + ->setVerified(LegalpadDocumentSignature::VERIFIED) + ->setSignatureData( + array( + 'name' => $name, + 'email' => $email, + 'notes' => $v_notes, + )); + + $signature->save(); + + return id(new AphrontRedirectResponse())->setURI($next_uri); + } + } + + $user_handles = $this->loadViewerHandles($v_users); + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->appendChild( + id(new AphrontFormTokenizerControl()) + ->setLabel(pht('Exempt User')) + ->setName('users') + ->setLimit(1) + ->setDatasource('/typeahead/common/users/') + ->setValue($user_handles) + ->setError($e_user)) + ->appendChild( + id(new AphrontFormTextAreaControl()) + ->setLabel(pht('Notes')) + ->setName('notes') + ->setValue($v_notes)); + + return $this->newDialog() + ->setTitle(pht('Add Signature Exemption')) + ->setWidth(AphrontDialogView::WIDTH_FORM) + ->setErrors($errors) + ->appendParagraph( + pht( + 'You can record a signature exemption if a user has signed an '. + 'equivalent document. Other applications will behave as through the '. + 'user has signed this document.')) + ->appendParagraph(null) + ->appendChild($form->buildLayoutView()) + ->addSubmitButton(pht('Add Exemption')) + ->addCancelButton($next_uri); + } + +} diff --git a/src/applications/legalpad/controller/LegalpadDocumentSignatureViewController.php b/src/applications/legalpad/controller/LegalpadDocumentSignatureViewController.php new file mode 100644 --- /dev/null +++ b/src/applications/legalpad/controller/LegalpadDocumentSignatureViewController.php @@ -0,0 +1,71 @@ +<?php + +final class LegalpadDocumentSignatureViewController extends LegalpadController { + + private $id; + + public function willProcessRequest(array $data) { + $this->id = $data['id']; + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $signature = id(new LegalpadDocumentSignatureQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->executeOne(); + if (!$signature) { + return new Aphront404Response(); + } + + + // NOTE: In order to see signature details (which include the relatively + // internal-feeling "notes" field) you must be able to edit the document. + // Essentially, this power is for document managers. Notably, this prevents + // users from seeing notes about their own exemptions by guessing their + // signature ID. This is purely a policy check. + + $document = id(new LegalpadDocumentQuery()) + ->setViewer($viewer) + ->withIDs(array($signature->getDocument()->getID())) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$document) { + return new Aphront404Response(); + } + + + $document_id = $signature->getDocument()->getID(); + $next_uri = $this->getApplicationURI('signatures/'.$document_id.'/'); + + $exemption_phid = $signature->getExemptionPHID(); + $handles = $this->loadViewerHandles(array($exemption_phid)); + $exemptor_handle = $handles[$exemption_phid]; + + $data = $signature->getSignatureData(); + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->appendChild( + id(new AphrontFormMarkupControl()) + ->setLabel(pht('Exemption By')) + ->setValue($exemptor_handle->renderLink())) + ->appendChild( + id(new AphrontFormMarkupControl()) + ->setLabel(pht('Notes')) + ->setValue(idx($data, 'notes'))); + + return $this->newDialog() + ->setTitle(pht('Signature Details')) + ->setWidth(AphrontDialogView::WIDTH_FORM) + ->appendChild($form->buildLayoutView()) + ->addCancelButton($next_uri, pht('Close')); + } + +} diff --git a/src/applications/legalpad/query/LegalpadDocumentSignatureSearchEngine.php b/src/applications/legalpad/query/LegalpadDocumentSignatureSearchEngine.php --- a/src/applications/legalpad/query/LegalpadDocumentSignatureSearchEngine.php +++ b/src/applications/legalpad/query/LegalpadDocumentSignatureSearchEngine.php @@ -178,6 +178,11 @@ 'red', pht('Unverified Email')); + $sig_exemption = $this->renderIcon( + 'fa-asterisk', + 'indigo', + pht('Exemption')); + id(new PHUIIconView()) ->setIconFont('fa-envelope', 'red') ->addSigil('has-tooltip') @@ -190,7 +195,18 @@ $document = $signature->getDocument(); - if (!$signature->isVerified()) { + if ($signature->getIsExemption()) { + $signature_href = $this->getApplicationURI( + 'signature/'.$signature->getID().'/'); + + $sig_icon = javelin_tag( + 'a', + array( + 'href' => $signature_href, + 'sigil' => 'workflow', + ), + $sig_exemption); + } else if (!$signature->isVerified()) { $sig_icon = $sig_unverified; } else if ($signature->getDocumentVersion() != $document->getVersions()) { $sig_icon = $sig_old; @@ -242,8 +258,23 @@ 'right', )); + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Signatures')); + + if ($this->document) { + $document_id = $this->document->getID(); + + $header->addActionLink( + id(new PHUIButtonView()) + ->setText(pht('Add Signature Exemption')) + ->setTag('a') + ->setHref($this->getApplicationURI('addsignature/'.$document_id.'/')) + ->setWorkflow(true) + ->setIcon(id(new PHUIIconView())->setIconFont('fa-pencil'))); + } + $box = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Signatures')) + ->setHeader($header) ->appendChild($table); if (!$this->document) { diff --git a/src/applications/legalpad/storage/LegalpadDocumentSignature.php b/src/applications/legalpad/storage/LegalpadDocumentSignature.php --- a/src/applications/legalpad/storage/LegalpadDocumentSignature.php +++ b/src/applications/legalpad/storage/LegalpadDocumentSignature.php @@ -14,6 +14,8 @@ protected $signerEmail; protected $signatureData = array(); protected $verified; + protected $isExemption = 0; + protected $exemptionPHID; protected $secretKey; private $document = self::ATTACHABLE; diff --git a/src/docs/user/userguide/legalpad.diviner b/src/docs/user/userguide/legalpad.diviner --- a/src/docs/user/userguide/legalpad.diviner +++ b/src/docs/user/userguide/legalpad.diviner @@ -61,6 +61,27 @@ Users will now only be able to take the action (for example, view or edit the object) if they have signed the specified documents. + +Adding Exemptions +================= + +If you have users who have signed an alternate form of a document (for example, +you have a hard copy on file), or an equivalent document, or who are otherwise +exempt from needing to sign a document in Legalpad, you can add a signature +exemption for them. + +Other applications will treat users with a signature exemption as though they +had signed the document, although the UI will show the signature as an exemption +rather than a normal signature. + +To add an exemption, go to **Manage Document**, then **View Signatures**, then +**Add Signature Exemption**. + +You can optionally add notes about why a user is exempt from signing a document. +To review the notes later (and see who added the exemption), click the colored +asterisk in the list view. + + Roadmap ========