Skip to content

Commit f3d9c8f

Browse files
author
Todd Dembrey
authored
Merge pull request #1225 from OpenTechFund/feature/1215-comment-editing-react
Feature/1215 comment editing react
2 parents 4a115ae + 828064b commit f3d9c8f

File tree

22 files changed

+455
-182
lines changed

22 files changed

+455
-182
lines changed

opentech/apply/funds/api_views.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,12 @@ class CommentFilter(filters.FilterSet):
146146

147147
class Meta:
148148
model = Activity
149-
fields = ['submission', 'visibility', 'since', 'before', 'newer']
149+
fields = ['visibility', 'since', 'before', 'newer']
150+
151+
152+
class AllCommentFilter(CommentFilter):
153+
class Meta(CommentFilter.Meta):
154+
fields = CommentFilter.Meta.fields + ['submission']
150155

151156

152157
class CommentList(generics.ListAPIView):
@@ -156,15 +161,15 @@ class CommentList(generics.ListAPIView):
156161
permissions.IsAuthenticated, IsApplyStaffUser,
157162
)
158163
filter_backends = (filters.DjangoFilterBackend,)
159-
filter_class = CommentFilter
164+
filter_class = AllCommentFilter
160165
pagination_class = StandardResultsSetPagination
161166

162167
def get_queryset(self):
163168
return super().get_queryset().visible_to(self.request.user)
164169

165170

166171
class CommentListCreate(generics.ListCreateAPIView):
167-
queryset = Activity.comments.all()
172+
queryset = Activity.comments.all().select_related('user')
168173
serializer_class = CommentCreateSerializer
169174
permission_classes = (
170175
permissions.IsAuthenticated, IsApplyStaffUser,
@@ -180,6 +185,7 @@ def get_queryset(self):
180185

181186
def perform_create(self, serializer):
182187
obj = serializer.save(
188+
timestamp=timezone.now(),
183189
type=COMMENT,
184190
user=self.request.user,
185191
submission_id=self.kwargs['pk']
@@ -198,7 +204,7 @@ class CommentEdit(
198204
mixins.CreateModelMixin,
199205
generics.GenericAPIView,
200206
):
201-
queryset = Activity.comments.all()
207+
queryset = Activity.comments.all().select_related('user')
202208
serializer_class = CommentEditSerializer
203209
permission_classes = (
204210
permissions.IsAuthenticated, IsAuthor

opentech/apply/funds/serializers.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ def get_assigned(self, obj):
124124

125125
class TimestampField(serializers.Field):
126126
def to_representation(self, value):
127-
return value.timestamp()
127+
return value.timestamp() * 1000
128128

129129

130130
class SubmissionListSerializer(serializers.ModelSerializer):
@@ -218,23 +218,34 @@ class CommentSerializer(serializers.ModelSerializer):
218218
user = serializers.StringRelatedField()
219219
message = serializers.SerializerMethodField()
220220
edit_url = serializers.HyperlinkedIdentityField(view_name='funds:api:comments:edit')
221+
editable = serializers.SerializerMethodField()
222+
timestamp = TimestampField(read_only=True)
223+
edited = TimestampField(read_only=True)
221224

222225
class Meta:
223226
model = Activity
224-
fields = ('id', 'timestamp', 'user', 'submission', 'message', 'visibility', 'edited', 'edit_url')
227+
fields = ('id', 'timestamp', 'user', 'submission', 'message', 'visibility', 'edited', 'edit_url', 'editable')
225228

226229
def get_message(self, obj):
227230
return bleach_value(markdown(obj.message))
228231

232+
def get_editable(self, obj):
233+
return self.context['request'].user == obj.user
234+
229235

230236
class CommentCreateSerializer(serializers.ModelSerializer):
231237
user = serializers.StringRelatedField()
232238
edit_url = serializers.HyperlinkedIdentityField(view_name='funds:api:comments:edit')
239+
editable = serializers.SerializerMethodField()
240+
timestamp = TimestampField(read_only=True)
241+
edited = TimestampField(read_only=True)
233242

234243
class Meta:
235244
model = Activity
236-
fields = ('id', 'timestamp', 'user', 'message', 'visibility', 'edited', 'edit_url')
237-
read_only_fields = ('timestamp', 'edited',)
245+
fields = ('id', 'timestamp', 'user', 'message', 'visibility', 'edited', 'edit_url', 'editable')
246+
247+
def get_editable(self, obj):
248+
return self.context['request'].user == obj.user
238249

239250

240251
class CommentEditSerializer(CommentCreateSerializer):

opentech/apply/funds/tests/test_api_views.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from django.test import TestCase, override_settings
22
from django.urls import reverse_lazy
3-
from django.utils import timezone
43

54
from opentech.apply.activity.models import Activity, PUBLIC, PRIVATE
65
from opentech.apply.activity.tests.factories import CommentFactory
@@ -36,10 +35,7 @@ def test_edit_updates_correctly(self):
3635

3736
comment.refresh_from_db()
3837

39-
# Match the behaviour of DRF
40-
time = comment.timestamp.astimezone(timezone.get_current_timezone()).isoformat()
41-
if time.endswith('+00:00'):
42-
time = time[:-6] + 'Z'
38+
time = comment.timestamp.timestamp() * 1000
4339

4440
self.assertEqual(time, response.json()['timestamp'])
4541
self.assertFalse(comment.current)

opentech/static_src/src/app/src/api/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
fetchSubmissionsByStatuses
66
} from '@api/submissions';
77
import { fetchRound, fetchRounds } from '@api/rounds';
8-
import { createNoteForSubmission, fetchNotesForSubmission, fetchNewNotesForSubmission } from '@api/notes';
8+
import { createNoteForSubmission, fetchNotesForSubmission, fetchNewNotesForSubmission, editNoteForSubmission } from '@api/notes';
99

1010
export default {
1111
executeSubmissionAction,
@@ -20,4 +20,5 @@ export default {
2020
fetchNotesForSubmission,
2121
fetchNewNotesForSubmission,
2222
createNoteForSubmission,
23+
editNoteForSubmission,
2324
};

opentech/static_src/src/app/src/api/notes.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,13 @@ export function createNoteForSubmission(submissionID, note) {
3030
}
3131
};
3232
}
33+
34+
export function editNoteForSubmission(note) {
35+
return {
36+
path: `/apply/api/comments/${note.id}/edit/`,
37+
method: 'POST',
38+
options: {
39+
body: JSON.stringify({ message: note.message }),
40+
}
41+
}
42+
}

opentech/static_src/src/app/src/components/Listing/index.js

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import React from 'react';
22
import PropTypes from 'prop-types';
3-
//import { TransitionGroup } from 'react-transition-group';
43

54
import LoadingPanel from '@components/LoadingPanel';
65
import InlineLoading from '@components/InlineLoading'
@@ -35,12 +34,7 @@ export default class Listing extends React.Component {
3534
return (
3635
<>
3736
{ isErrored && this.renderErrorItem() }
38-
{/* This seems to cause a bug when after updating a status
39-
of the only one item in the group, it does not
40-
dissapear from the old status*/}
41-
{/*<TransitionGroup component={null} >*/}
42-
{items.map(v => renderItem(v))}
43-
{/*</TransitionGroup>*/}
37+
{items.map(v => renderItem(v))}
4438
</>
4539
);
4640
}
@@ -96,7 +90,6 @@ export default class Listing extends React.Component {
9690
listRef,
9791
} = this.props;
9892

99-
10093
if ( items.length === 0 ) {
10194
if (isLoading) {
10295
return (

opentech/static_src/src/app/src/components/Listing/style.scss

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
.listing {
22
overflow-y: overlay;
33
flex-grow: 3;
4+
transition: opacity $transition;
45

56
@include target-ie11 {
67
max-width: 390px;
78
width: 100%;
89
}
910

10-
&__header {
11-
height: $listing-header-height;
11+
&.is-blank {
1212
padding: 20px;
13+
text-align: center;
1314
}
1415

1516
// ensures the last item will be at the top of the column after navigating to it via the dropdown
@@ -29,9 +30,9 @@
2930
box-shadow: inset 0 -20px 20px -10px $color--light-mid-grey;
3031
}
3132

32-
&.is-blank {
33+
&__header {
34+
height: $listing-header-height;
3335
padding: 20px;
34-
text-align: center;
3536
}
3637

3738
// inner <li>'s

opentech/static_src/src/app/src/components/LoadingPanel/styles.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.loading-panel {
2+
height: 100%;
23
text-align: center;
34
padding: 20px;
45

opentech/static_src/src/app/src/components/NoteListingItem/index.js

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,45 @@ import './styles.scss';
55

66
export default class NoteListingItem extends React.Component {
77
static propTypes = {
8-
user: PropTypes.string.isRequired,
8+
author: PropTypes.string.isRequired,
99
message: PropTypes.string.isRequired,
1010
timestamp: PropTypes.string.isRequired,
11+
handleEditNote: PropTypes.func.isRequired,
12+
disabled: PropTypes.bool,
13+
editable: PropTypes.bool,
14+
edited: PropTypes.string,
1115
};
1216

1317
parseUser() {
14-
const { user } = this.props;
18+
const { author } = this.props;
1519

16-
if (user.length > 16) {
17-
return `${user.substring(0, 16)}...`
20+
if (author.length > 16) {
21+
return `${author.substring(0, 16)}...`
1822
} else {
19-
return user;
23+
return author;
2024
}
2125
}
2226

2327
render() {
24-
const { timestamp, message } = this.props;
28+
const { timestamp, message, handleEditNote, disabled, editable, edited} = this.props;
2529

2630
return (
27-
<li className="note">
31+
<li className={`note ${disabled ? 'disabled' : ''}`}>
2832
<p className="note__meta">
29-
<span>{this.parseUser()}</span>
30-
<span className="note__date">{timestamp}</span>
33+
<span className="note__meta note__meta--inner">
34+
<span>{this.parseUser()}</span>
35+
{ editable &&
36+
<a onClick={(e) => handleEditNote(e.preventDefault())} className="note__edit" href="#">
37+
Edit
38+
<svg className="icon icon--pen"><use xlinkHref="#pen"></use></svg>
39+
</a>
40+
}
41+
</span>
42+
43+
<span className="note__date">
44+
<span className="note__date_created">{timestamp}</span><br/>
45+
{edited && <span className="note__date_edited">(edited: {edited})</span>}
46+
</span>
3147
</p>
3248
<div className="note__content" dangerouslySetInnerHTML={{__html: message}} />
3349
</li>

opentech/static_src/src/app/src/components/NoteListingItem/styles.scss

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
.note {
22
margin: 20px;
3+
margin-top: 0px;
34
font-size: 14px;
4-
transition: opacity 200ms ease-out 200ms, transform 200ms ease-out 200ms;
5+
border-top: 3px solid $color--light-grey;
56

6-
&.add-note-enter {
7-
opacity: 0;
8-
transform: translate(0, 10px);
9-
}
10-
11-
&.add-note-enter-done {
12-
opacity: 1;
13-
transform: translate(0, 0);
7+
&.disabled {
8+
opacity: .5;
9+
pointer-events: none;
1410
}
1511

1612
&__meta {
@@ -25,13 +21,21 @@
2521
}
2622

2723
&__date {
28-
color: $color--dark-blue;
2924
margin-left: 10px;
25+
26+
&_created {
27+
color: $color--dark-blue;
28+
float: right;
29+
}
30+
31+
&_edited {
32+
font-size: 12px;
33+
}
3034
}
3135

3236
&__content {
3337
margin: 0;
34-
word-break: break-all;
38+
word-break: break-word;
3539
hyphens: auto;
3640

3741
ul {
@@ -52,4 +56,16 @@
5256
border-left: 2px solid $color--dark-blue;
5357
}
5458
}
59+
60+
&__edit {
61+
color: $color--dark-blue;
62+
padding-left: 10px;
63+
margin-left: 10px;
64+
max-height: 1.5rem;
65+
border-left: 2px solid $color--mid-grey;
66+
67+
svg {
68+
fill: $color--dark-blue;
69+
}
70+
}
5571
}

0 commit comments

Comments
 (0)