diff --git a/opentech/apply/activity/templates/activity/include/listing_base.html b/opentech/apply/activity/templates/activity/include/listing_base.html index f999f30d48506338aea55ecea8cafb49b56a566a..8f13bc06b68f315de7e3a96c9ffbdeb9f0d0fa6c 100644 --- a/opentech/apply/activity/templates/activity/include/listing_base.html +++ b/opentech/apply/activity/templates/activity/include/listing_base.html @@ -3,10 +3,25 @@ <div class="feed__pre-content"> <p class="feed__label feed__label--{{ activity.type }}">{{ activity.type|capfirst }}</p> </div> - <div class="feed__content"> - <div class="feed__meta"> + <div class="feed__content js-feed-content"> + <div class="feed__meta js-feed-meta"> <p class="feed__label feed__label--{{ activity.type }} feed__label--mobile">{{ activity.type|capfirst }}</p> <p class="feed__meta-item"><span>{{ activity|display_author:request.user }}</span> – {{ activity.timestamp|date:"Y-m-d H:i" }}</p> + + {% if editable %} + {% if activity.user == request.user %} + <p class="feed__meta-item feed__meta-item--edit-button"> + <a class="link link--edit-submission is-active js-edit-comment" href="#"> + Edit + <svg class="icon icon--pen"><use xlink:href="#pen"></use></svg> + </a> + </p> + {% endif %} + {% if activity.edited %} + <p class="feed__meta-item feed__meta-item--last-edited">(Last edited: <span class="js-last-edited">{{ activity.edited|date:"Y-m-d H:i" }}</span>)</p> + {% endif %} + {% endif %} + {% if activity.private %} <p class="feed__meta-item feed__meta-item--right"> <svg class="icon icon--eye"><use xlink:href="#eye"></use></svg> @@ -14,12 +29,21 @@ </p> {% endif %} </div> + <p class="feed__heading"> {% if submission_title %} updated <a href="{{ activity.submission.get_absolute_url }}">{{ activity.submission.title }}</a> {% endif %} - {{ activity|display_for:request.user|submission_links|markdown|bleach }} + {% if editable %} + <div class="feed__comment js-comment" data-id="{{activity.id}}" data-comment="{{activity|display_for:request.user|to_markdown}}" data-edit-url="{% url 'funds:api:comments:edit' pk=activity.pk %}"> + {{ activity|display_for:request.user|submission_links|markdown|bleach }} + </div> + + <div class="js-edit-block" aria-live="polite"></div> + {% else %} + {{ activity|display_for:request.user|submission_links|markdown|bleach }} + {% endif %} {% if not submission_title and activity|user_can_see_related:request.user %} {% with url=activity.related_object.get_absolute_url %} diff --git a/opentech/apply/funds/serializers.py b/opentech/apply/funds/serializers.py index d76d8ad1c849108aac8f101830c3616a73186dc3..bf933e22d34771eb49056adeef5fcd7d3889ffd2 100644 --- a/opentech/apply/funds/serializers.py +++ b/opentech/apply/funds/serializers.py @@ -217,10 +217,11 @@ class RoundLabSerializer(serializers.ModelSerializer): class CommentSerializer(serializers.ModelSerializer): user = serializers.StringRelatedField() message = serializers.SerializerMethodField() + edit_url = serializers.HyperlinkedIdentityField(view_name='funds:api:comments:edit') class Meta: model = Activity - fields = ('id', 'timestamp', 'user', 'submission', 'message', 'visibility', 'edited') + fields = ('id', 'timestamp', 'user', 'submission', 'message', 'visibility', 'edited', 'edit_url') def get_message(self, obj): return bleach_value(markdown(obj.message)) @@ -228,15 +229,14 @@ class CommentSerializer(serializers.ModelSerializer): class CommentCreateSerializer(serializers.ModelSerializer): user = serializers.StringRelatedField() + edit_url = serializers.HyperlinkedIdentityField(view_name='funds:api:comments:edit') class Meta: model = Activity - fields = ('id', 'timestamp', 'user', 'message', 'visibility', 'edited') + fields = ('id', 'timestamp', 'user', 'message', 'visibility', 'edited', 'edit_url') read_only_fields = ('timestamp', 'edited',) class CommentEditSerializer(CommentCreateSerializer): - user = serializers.StringRelatedField() - class Meta(CommentCreateSerializer.Meta): read_only_fields = ('timestamp', 'visibility', 'edited',) diff --git a/opentech/apply/funds/templates/funds/applicationsubmission_admin_detail.html b/opentech/apply/funds/templates/funds/applicationsubmission_admin_detail.html index 3087dda20779544542b8e1bf01b0763a006c0668..537f0b371d5aab914f588cb50f814f08f04cb5db 100644 --- a/opentech/apply/funds/templates/funds/applicationsubmission_admin_detail.html +++ b/opentech/apply/funds/templates/funds/applicationsubmission_admin_detail.html @@ -47,10 +47,12 @@ {{ comment_form.media.js }} {{ partner_form.media.js }} <script src="//cdnjs.cloudflare.com/ajax/libs/fancybox/3.4.1/jquery.fancybox.min.js"></script> + <script src="//cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.min.js"></script> <script src="{% static 'js/apply/fancybox-global.js' %}"></script> <script src="{% static 'js/apply/tabs.js' %}"></script> <script src="{% static 'js/apply/toggle-actions-panel.js' %}"></script> <script src="{% static 'js/apply/toggle-reviewers.js' %}"></script> <script src="{% static 'js/apply/toggle-sidebar.js' %}"></script> <script src="{% static 'js/apply/submission-text-cleanup.js' %}"></script> + <script src="{% static 'js/apply/edit-comment.js' %}"></script> {% endblock %} diff --git a/opentech/apply/funds/templates/funds/applicationsubmission_detail.html b/opentech/apply/funds/templates/funds/applicationsubmission_detail.html index 1fe5bdc008db857104a3c0c8cdc03c555533db57..f213ec869e0293accebecb7268055d6ecb59492f 100644 --- a/opentech/apply/funds/templates/funds/applicationsubmission_detail.html +++ b/opentech/apply/funds/templates/funds/applicationsubmission_detail.html @@ -141,7 +141,7 @@ <div class="tabs__content" id="tab-2"> <div class="feed"> {% include "activity/include/comment_form.html" %} - {% include "activity/include/comment_list.html" %} + {% include "activity/include/comment_list.html" with editable=True %} </div> </div> diff --git a/opentech/apply/funds/templates/funds/includes/activity-feed.html b/opentech/apply/funds/templates/funds/includes/activity-feed.html index 0a3595a6d1765fd7ab028c1bc83200a646f45054..37117315ccd58c1fb4b4da12cd87404d99020698 100644 --- a/opentech/apply/funds/templates/funds/includes/activity-feed.html +++ b/opentech/apply/funds/templates/funds/includes/activity-feed.html @@ -15,7 +15,7 @@ <div class="wrapper wrapper--medium wrapper--activity-feed js-tabs-content"> <div class="tabs__content tabs__content--current" id="tab-1"> - {% include "activity/include/comment_list.html" with submission_title=True %} + {% include "activity/include/comment_list.html" with submission_title=True editable=False %} </div> <div class="tabs__content" id="tab-2"> diff --git a/opentech/apply/funds/templatetags/markdown_tags.py b/opentech/apply/funds/templatetags/markdown_tags.py index 9ba5ff19dc0828092dcfab40fdcd9ab6876f7b58..bddae4d8a14e7e8c522967c64fd4eafa261e78a7 100644 --- a/opentech/apply/funds/templatetags/markdown_tags.py +++ b/opentech/apply/funds/templatetags/markdown_tags.py @@ -1,11 +1,21 @@ import mistune +import tomd from django import template register = template.Library() +mistune_markdown = mistune.Markdown() + @register.filter def markdown(value): - markdown = mistune.Markdown() - return markdown(value) + return mistune_markdown(value) + + +@register.filter +def to_markdown(value): + # pass through markdown to ensure comment is a + # fully formed HTML block + value = markdown(value) + return tomd.convert(value) diff --git a/opentech/static_src/src/javascript/apply/edit-comment.js b/opentech/static_src/src/javascript/apply/edit-comment.js new file mode 100644 index 0000000000000000000000000000000000000000..eef32d7815b679927a0ec9006e4beac4a3b249c1 --- /dev/null +++ b/opentech/static_src/src/javascript/apply/edit-comment.js @@ -0,0 +1,145 @@ +(function ($) { + + 'use strict'; + + const comment = '.js-comment'; + const pageDown = '.js-pagedown'; + const feedMeta = '.js-feed-meta'; + const editBlock = '.js-edit-block'; + const lastEdited = '.js-last-edited'; + const editButton = '.js-edit-comment'; + const feedContent = '.js-feed-content'; + const commentError = '.js-comment-error'; + const cancelEditButton = '.js-cancel-edit'; + const submitEditButton = '.js-submit-edit'; + + // handle edit + $(editButton).click(function (e) { + e.preventDefault(); + + closeAllEditors(); + + const editBlockWrapper = $(this).closest(feedContent).find(editBlock); + const commentWrapper = $(this).closest(feedContent).find(comment); + const commentContents = $(commentWrapper).attr('data-comment'); + + // hide the edit link and original comment + $(this).parent().hide(); + $(commentWrapper).hide(); + + const markup = ` + <div class="js-pagedown form"> + <div id="wmd-button-bar-edit-comment" class="wmd-button-bar"></div> + <textarea id="wmd-input-edit-comment" class="wmd-input" rows="10">${commentContents}</textarea> + <div id="wmd-preview-edit-comment" class="wmd-preview"></div> + <div class="wrapper--outer-space-medium"> + <button class="button button--primary js-submit-edit" type="submit">Update</button> + <button class="button button--white js-cancel-edit">Cancel</button> + </div> + </div> + `; + + // add the comment to the editor + $(editBlockWrapper).append(markup); + + // run the editor + initEditor(); + }); + + // handle cancel + $(document).on('click', cancelEditButton, function () { + showComment(this); + showEditButton(this); + hidePageDownEditor(this); + if ($(commentError).length) { + hideError(); + } + }); + + // handle submit + $(document).on('click', submitEditButton, function () { + const commentContainer = $(this).closest(editBlock).siblings(comment); + const editedComment = $(this).closest(pageDown).find('.wmd-preview').html(); + const commentMD = $(this).closest(editBlock).find('textarea').val(); + const editUrl = $(commentContainer).attr('data-edit-url'); + + fetch(editUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': $.cookie('csrftoken') + }, + body: JSON.stringify({ + message: editedComment + }) + }).then(response => { + if (!response.ok) { + const error = Object.assign({}, response, { + status: response.status, + statusText: response.statusText + }); + return Promise.reject(error); + } + return response.json(); + }).then(data => { + updateComment(commentContainer, data.id, data.message, data.edit_url, commentMD); + updateLastEdited(this, data.edited); + showComment(this); + showEditButton(this); + hidePageDownEditor(this); + }).catch((error) => { + if (error.status === 404) { + handleError(this, 'Update unsuccessful. This comment has been edited elsewhere. To get the latest updates please refresh the page, but note any unsaved changes will be lost by doing so.'); + } + else { + handleError(this, 'An error has occured. Please try again later.'); + } + }); + }); + + const handleError = (el, message) => { + $(el).closest(editBlock).append(`<p class="wrapper--error js-comment-error">${message}</p>`); + $(el).attr('disabled', true); + }; + + const initEditor = () => { + const converterOne = window.Markdown.getSanitizingConverter(); + const commentEditor = new window.Markdown.Editor(converterOne, '-edit-comment'); + commentEditor.run(); + }; + + const showEditButton = (el) => { + $(el).closest(editBlock).siblings(feedMeta).find(editButton).parent().show(); + }; + + const hidePageDownEditor = (el) => { + $(el).closest(pageDown).remove(); + }; + + const showComment = (el) => { + $(el).closest(editBlock).siblings(comment).show(); + }; + + const updateLastEdited = (el, date) => { + const parsedDate = new Date(date).toISOString().split('T')[0]; + const time = new Date(date).toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'}); + + $(el).closest(feedContent).find(lastEdited).html(`${parsedDate} ${time}`); + }; + + const updateComment = (el, id, comment, editUrl, commentMarkdown) => { + $(el).html(comment); + $(el).attr('data-id', id); + $(el).attr('data-edit-url', editUrl); + $(el).attr('data-comment', commentMarkdown); + }; + + const closeAllEditors = () => { + $(comment).show(); + $(pageDown).remove(); + $(editButton).parent().show(); + }; + + const hideError = () => $(commentError).remove(); + +})(jQuery); diff --git a/opentech/static_src/src/sass/apply/components/_feed.scss b/opentech/static_src/src/sass/apply/components/_feed.scss index 2ab489a4bf4b1029ce4e6df43d4de6063046599f..d3afc66ee7327436224057045a9fcf735e18661c 100644 --- a/opentech/static_src/src/sass/apply/components/_feed.scss +++ b/opentech/static_src/src/sass/apply/components/_feed.scss @@ -101,6 +101,20 @@ margin: 0 15px 0 0; } + &--edit-button { + border-left: 2px solid $color--mid-grey; + padding-left: 15px; + } + + &--last-edited { + margin-right: 5px; + color: $color--mid-dark-grey; + + span { + font-weight: $weight--normal; + } + } + &--right { margin-left: auto; } diff --git a/opentech/static_src/src/sass/apply/components/_wrapper.scss b/opentech/static_src/src/sass/apply/components/_wrapper.scss index b80aa2b154d104af6634779ee87cbfa784cadd25..e7165e01b4cd95706618d388763e6ce4601e6f5f 100644 --- a/opentech/static_src/src/sass/apply/components/_wrapper.scss +++ b/opentech/static_src/src/sass/apply/components/_wrapper.scss @@ -89,6 +89,10 @@ margin: 0 auto 2rem; background: $color--light-pink; border: 1px solid $color--tomato; + + .feed & { + margin: 0 0 1rem; + } } &--bottom-space { diff --git a/requirements.txt b/requirements.txt index 192f6e09ab86362a00dc1f4f7af179e3984463c3..ec3bc53376c6f238efb19d9ffea89edd0134b4e9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -43,6 +43,7 @@ mistune==0.8.4 Pillow==4.3.0 psycopg2==2.7.3.1 social_auth_app_django==3.1.0 +tomd==0.1.3 wagtail~=2.2.0 wagtail-cache==0.5.1 whitenoise==4.0