Skip to content

Commit 1604d93

Browse files
committed
Add initial code for module.
1 parent b37777b commit 1604d93

File tree

6 files changed

+340
-0
lines changed

6 files changed

+340
-0
lines changed

_config.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<?php
2+

_config/extensions.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Comment:
2+
extensions:
3+
- CommentUserNotificationsExtension
4+
CommentingController:
5+
extensions:
6+
- CommentingControllerUserNotificationsExtension
7+
Controller:
8+
extensions:
9+
- ControllerCommentUserNotificationsExtension
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<?php
2+
/**
3+
* Class CommentingControllerUserNotificationsExtension
4+
* Allows site members who are logged in to tick a box when posting their comment that will notify them when subsequent
5+
* comments are made on that object.
6+
*
7+
* @author Matt Peel <[email protected]>
8+
*/
9+
class CommentUserNotificationsExtension extends DataExtension {
10+
/**
11+
* Add a boolean to track which {@link Comment} objects (and therefore the {@link Member} that posted them) wantk
12+
*
13+
*
14+
*
15+
* to be notified when new comments are posted.
16+
*
17+
* @var array Additional database fields to add to the {@link Comment} class.
18+
*/
19+
private static $db = array(
20+
"NotifyOfUpdates" => "Boolean"
21+
);
22+
23+
/**
24+
* We hook into onAfterWrite() because we want to check this every time the comment is written - primarily because
25+
* of the test that we perform to ensure that the comment isn't currently moderated. Most sites will moderate
26+
* comments initially, and there's no point sending an email to a user if the comment is still awaiting moderation
27+
* (and therefore the user can't see it yet).
28+
*
29+
* @todo This will lead to multiple emails being sent if a comment is edited after being posted
30+
*/
31+
public function onAfterWrite() {
32+
parent::onAfterWrite();
33+
34+
$parentClass = $this->owner->BaseClass;
35+
$parentID = $this->owner->ParentID;
36+
37+
// We only want to notify people if certain conditions are met:
38+
// - The comment has passed moderation (aka. if required, it has been approved by an admin)
39+
// - We are either seeing the Comment for the first time, or it has just passed moderation by an admin
40+
if($this->shouldSendUserNotificationEmails()) {
41+
if(ClassInfo::exists($parentClass)) {
42+
$commentParent = $parentClass::get()->byID($parentID);
43+
44+
// Get all comments attached to this page, which we have to do manually as the has_one relationship is
45+
// 'faked' by the Comment class (because it can be attached to multiple parent classes).
46+
if($commentParent) {
47+
$comments = Comment::get()->filter(array(
48+
'BaseClass' => $parentClass,
49+
'ParentID' => $parentID,
50+
'NotifyOfUpdates' => true
51+
));
52+
53+
// If we have comments, iterate over them to build a unique list of all email addresses to notify
54+
if($comments) {
55+
$emailList = array();
56+
57+
foreach($comments as $c) {
58+
$author = $c->Author();
59+
60+
if($author) {
61+
if(!in_array($author->Email, $emailList)) {
62+
$emailList[] = $author->Email;
63+
}
64+
}
65+
}
66+
67+
// Send an email to everyone in the list
68+
if(sizeof($emailList) > 0) {
69+
foreach($emailList as $emailAddress) {
70+
$email = new Email();
71+
$email->setSubject('New Comment on "' . $commentParent->dbObject('Title')->XML() . '"');
72+
$email->setFrom(Email::getAdminEmail());
73+
$email->setTo($emailAddress);
74+
$email->populateTemplate($this->owner);
75+
76+
$email->send();
77+
}
78+
}
79+
}
80+
}
81+
}
82+
}
83+
}
84+
85+
private function shouldSendUserNotificationEmails() {
86+
$changedFields = $this->owner->getChangedFields();
87+
88+
return
89+
$changedFields &&
90+
(
91+
// New record, automatically moderated as moderation is not enabled for this site
92+
(
93+
isset($changedFields['ID']) &&
94+
isset($changedFields['Moderated']) &&
95+
$changedFields['ID']['before'] == 0 &&
96+
$changedFields['Moderated']['after'] === true
97+
)
98+
||
99+
// Existing record, moderation has just been set - meaning it has been approved by an admin
100+
(
101+
isset($changedFields['Moderated']) &&
102+
$changedFields['Moderated']['before'] == false &&
103+
$changedFields['Moderated']['after'] === true
104+
)
105+
);
106+
}
107+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
/**
3+
* Class CommentingControllerUserNotificationsExtension
4+
* Extends the comment form to include a field that allows users to select whether or not they want to be notified when
5+
* further comments are made on that combination of the {@link Comment} db fields BaseClass and ParentID.
6+
*
7+
* @see Comment
8+
*/
9+
class CommentingControllerUserNotificationsExtension extends Extension {
10+
public static $allowed_actions = array(
11+
'unsubscribenotification'
12+
);
13+
14+
/**
15+
* Alter the comment form to add a checkbox to give users the ability to receive notifications when further
16+
* comments are posted. The {@link CommentingController::doPostComment()} method will take care of saving this
17+
* field for us, as it's part of the {@link Comment} DataObject
18+
*
19+
* @see CommentUserNotificationsExtension
20+
* @param Form $form The Form object used to render the comments form
21+
*/
22+
public function alterCommentForm(Form $form) {
23+
$form->Fields()->insertAfter(
24+
CheckboxField::create(
25+
'NotifyOfUpdates',
26+
_t('CommentInterface.NOTIFYOFUPDATES', 'Please notify me about new comments posted here.')
27+
),
28+
'Comment'
29+
);
30+
}
31+
32+
/**
33+
* Uses $this->owner->request (a {@link SS_HTTPRequest} object) to determine which comment we want to unsubscribe
34+
* the member from. If the current user isn't logged in, or is logged in as a different user, then we send them to
35+
* the login screen.
36+
*/
37+
public function unsubscribenotification() {
38+
$request = $this->owner->getRequest();
39+
40+
$commentID = $request->param('ID');
41+
$member = Member::currentUser();
42+
43+
if(!$commentID) {
44+
$this->owner->httpError(403);
45+
return;
46+
}
47+
48+
$comment = Comment::get()->byID($commentID);
49+
50+
if(!$comment) {
51+
$this->owner->httpError(403);
52+
return;
53+
}
54+
55+
if(!$member || $member->ID != $comment->AuthorID) {
56+
return Security::permissionFailure(
57+
$this->owner,
58+
array(
59+
'default' => _t(
60+
'CommentingControllerUserNotificationsExtension.DEFAULTFAIL',
61+
'You must login to unsubscribe.'
62+
),
63+
'alreadyLoggedIn' => _t(
64+
'CommentingControllerUserNotificationsExtension.ALREADYLOGGEDINFAIL',
65+
'You must login as the correct user (the user who submitted the comment) to continue.'
66+
),
67+
'logInAgain' => _t(
68+
'CommentingControllerUserNotificationsExtension.LOGINAGAINFAIL',
69+
'You have been logged out. If you would like to login again, enter your credentials below.'
70+
)
71+
)
72+
);
73+
}
74+
75+
// Currently logged in Member's ID matches the author of the comment, so we can unsubscribe them
76+
// We want to find all comments posted to this object by this author, and unsubscribe all of them.
77+
$allComments = Comment::get()->filter(array(
78+
'BaseClass' => $comment->BaseClass,
79+
'ParentID' => $comment->ParentID,
80+
'NotifyOfUpdates' => true
81+
));
82+
83+
foreach($allComments as $c) {
84+
$c->NotifyOfUpdates = false;
85+
$c->write();
86+
}
87+
88+
// This sets a session var that can be queried on the page that we redirect the user back to, so that we can
89+
// display a nice message to let the user know their unsubscription was successful.
90+
Session::set('CommentUserNotificationsUnsubscribed', '1');
91+
92+
$this->owner->redirectBack();
93+
}
94+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
/**
3+
* Class ControllerCommentUserNotificationsExtension
4+
* This class is injected into all {@link Controller} class instances, and provides some helper methods to determine
5+
* whether there are comment subscriptions for a given Member in the system
6+
*/
7+
class ControllerCommentUserNotificationsExtension extends Extension {
8+
/**
9+
* Returns a {@link DataList} of {@link DataObject} objects that the currently logged in {@link Member} has
10+
* requested. Note that this doesn't return {@link Comment} objects, as there may be many comments from one member,
11+
* all of which may have `NotifyOfUpdates` selected, but in reality only one notification will be sent per comment
12+
* thread.
13+
*
14+
* This can be used in any template as follows (assuming $Title exists all DataObjects that comments are bound to):
15+
* <% loop CommentUserNotificationSubscriptions %>
16+
* <li>$Title.XML &ndash; <a href="$CommentUserNotificationUnsubscribeLink">unsubscribe</a></li>
17+
* <% end_loop %>
18+
*/
19+
public function CommentUserNotificationSubscriptions() {
20+
return $this->CommentUserNotificationSubscriptionsFor(Member::currentUser());
21+
}
22+
23+
/**
24+
* This method is overly complex, because {@link Comment} doesn't have a standard 'Parent' has_one, as it can be
25+
* attached to multiple different object types.
26+
*
27+
* @todo Can we fix this method without losing the flexibility that {@link Comment} provides?
28+
*
29+
* @see the above CommentUserNotificationSubscriptions() method for documentation
30+
* @param Member $member The {@link Member} object to find comments posted by, where `NotifyOfUpdates` = 1
31+
* @return ArrayList The list of {@link ArrayData} objects that can be shown in the template
32+
*/
33+
public function CommentUserNotificationSubscriptionsFor(Member $member) {
34+
if(!$member || !$member->isInDB()) return null; // No member (or no ID yet), so nothing to find
35+
36+
$allComments = Comment::get()->filter(array(
37+
'AuthorID' => $member->ID,
38+
'NotifyOfUpdates' => true
39+
));
40+
41+
if(!$allComments) return null;
42+
43+
$allObjects = new ArrayList();
44+
$allAddedComments = new ArrayList();
45+
46+
// @todo O(n^2) :(
47+
foreach($allComments as $comment) {
48+
$alreadyAdded = false;
49+
50+
foreach($allAddedComments as $obj) {
51+
if($comment->BaseClass == $obj->BaseClass && $comment->ParentID == $obj->ParentID) {
52+
$alreadyAdded = true;
53+
break;
54+
}
55+
}
56+
57+
if(!$alreadyAdded) {
58+
$baseClass = $comment->BaseClass;
59+
$baseObject = $baseClass::get()->byID($comment->ParentID);
60+
61+
if($baseObject) {
62+
// @todo This could return the actual DataObject that we're expecting (e.g. the SiteTree object),
63+
// but we can't add the 'CommentUserNotificationUnsubscribeLink' easily to it
64+
$allObjects->push(new ArrayData(array(
65+
'CommentUserNotificationUnsubscribeLink' => Controller::join_links(
66+
'CommentingController',
67+
'unsubscribenotification',
68+
$comment->ID
69+
),
70+
'Title' => $baseObject->Title
71+
)));
72+
73+
$allAddedComments->push($comment); // Keep track of what we've already added
74+
}
75+
}
76+
}
77+
78+
return $allObjects;
79+
}
80+
81+
public function HasJustUnsubscribedFromUserCommentNotification() {
82+
$hasUnsubscribed = Session::get('CommentUserNotificationsUnsubscribed');
83+
Session::clear('CommentUserNotificationsUnsubscribed');
84+
85+
return $hasUnsubscribed;
86+
}
87+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title></title>
5+
</head>
6+
7+
<body>
8+
<p>A new comment has been posted.</p>
9+
10+
<% if Link %>
11+
<p><a href="$Link">Click here to view this entry</a></p>
12+
<% end_if %>
13+
14+
<p>Comment details</p>
15+
16+
<dl>
17+
<dt>Date Posted:</dt>
18+
<dd>$Created.Nice</dd>
19+
20+
<% if Name %>
21+
<dt>Name:</dt>
22+
<dd>$Name.XML</dd>
23+
<% end_if %>
24+
25+
<% if Email %>
26+
<dt>Email:</dt>
27+
<dd>$Email.XML</dd>
28+
<% end_if %>
29+
30+
<% if URL %>
31+
<dt>URL:</dt>
32+
<dd>$URL.XML</dd>
33+
<% end_if %>
34+
35+
<% if Comment %>
36+
<dt>Comment:</dt>
37+
<dd>$Comment.XML</dd>
38+
<% end_if %>
39+
</dl>
40+
</body>
41+
</html>

0 commit comments

Comments
 (0)