diff --git a/app/inventory/forms.py b/app/inventory/forms.py index 114da899544e6e8b96e6287c85c1ec0d9dad16f9..f146e451906988750f18b171f80d9db2ad41f2f0 100644 --- a/app/inventory/forms.py +++ b/app/inventory/forms.py @@ -77,3 +77,7 @@ class ItemForm(CSEntryForm): class CommentForm(CSEntryForm): body = TextAreaField("Enter your comment:", validators=[validators.DataRequired()]) + + +class EditCommentForm(CSEntryForm): + body = TextAreaField("Edit comment:", validators=[validators.DataRequired()]) diff --git a/app/inventory/views.py b/app/inventory/views.py index 821ca472716ca08d4ea503bcbaa5e8f67be03051..6a24ab39f5b2ee438a9fdc8eaf1c467860e48630 100644 --- a/app/inventory/views.py +++ b/app/inventory/views.py @@ -22,7 +22,7 @@ from flask import ( current_app, ) from flask_login import login_required, current_user -from .forms import AttributeForm, ItemForm, CommentForm +from .forms import AttributeForm, ItemForm, CommentForm, EditCommentForm from ..extensions import db from ..decorators import login_groups_accepted from .. import utils, models, helpers @@ -115,6 +115,26 @@ def comment_item(ics_id): return render_template("inventory/comment_item.html", item=item, form=form) +@bp.route("/items/comment/edit/<comment_id>", methods=("GET", "POST")) +@login_groups_accepted("admin", "create") +def edit_comment(comment_id): + comment = models.ItemComment.query.get_or_404(comment_id) + form = EditCommentForm(request.form, obj=comment) + if form.validate_on_submit(): + comment.body = form.body.data + # Mark the item as "dirty" to add it to the session so that it will + # be re-indexed + sa.orm.attributes.flag_modified(comment.item, "comments") + db.session.commit() + return redirect(url_for("inventory.view_item", ics_id=comment.item.ics_id)) + return render_template( + "inventory/edit_comment.html", + item=comment.item, + comment_id=comment.id, + form=form, + ) + + @bp.route("/items/edit/<ics_id>", methods=("GET", "POST")) @login_groups_accepted("admin", "create") def edit_item(ics_id): diff --git a/app/models.py b/app/models.py index 5ba6e8f081a8d5519e59e3762312c7674e2a83a6..cc9e2dae5ddc4a9abc99998c561979fcce43fedf 100644 --- a/app/models.py +++ b/app/models.py @@ -563,7 +563,9 @@ class Item(CreatedMixin, SearchableMixin, db.Model): status = db.relationship("Status", back_populates="items", lazy="joined") children = db.relationship("Item", backref=db.backref("parent", remote_side=[id])) macs = db.relationship("Mac", backref="item", lazy="joined") - comments = db.relationship("ItemComment", backref="item", lazy="joined") + comments = db.relationship( + "ItemComment", backref="item", cascade="all, delete-orphan", lazy="joined" + ) __table_args__ = ( sa.CheckConstraint( diff --git a/app/static/js/items.js b/app/static/js/items.js index 8fb76149a01a7260b04d15b25fd99ed40d608223..2aebc493e0c0154f463d46470ded48a43f7a3439 100644 --- a/app/static/js/items.js +++ b/app/static/js/items.js @@ -29,11 +29,16 @@ $(document).ready(function() { simplifiedAutoLink: true }); - // Live rendering of markdown comment to HTML - $("#body").keyup(function(event) { - var comment = $(this).val(); - $("#commentLivePreview").html(converter.makeHtml(comment)); - }); + if( $("#commentLivePreview").length ) { + // Render comment when editing one + $("#commentLivePreview").html(converter.makeHtml($("#body").val())); + + // Live rendering of markdown comment to HTML + $("#body").keyup(function(event) { + var comment = $(this).val(); + $("#commentLivePreview").html(converter.makeHtml(comment)); + }); + } // render existing comments to HTML $(".item-comment").each(function() { diff --git a/app/templates/_helpers.html b/app/templates/_helpers.html index 39d68af5d23f4e339be970161e1f77148d8545b8..daa1c7f9b72e575e51ddb9b522c10d8bbbd118d4 100644 --- a/app/templates/_helpers.html +++ b/app/templates/_helpers.html @@ -196,3 +196,41 @@ </div> </div> {%- endmacro %} + +{% macro item_comment_form(form, item) -%} + <form id="CommentForm" method="POST"> + {{ form.hidden_tag() }} + <div class="form-group"> + {{ form.body.label() }} + {{ form.body(class_="form-control", rows=5, required=True) }} + <small class="form-text text-muted">Styling with Markdown is supported using + <a href="https://github.com/showdownjs/showdown/wiki/Showdown's-Markdown-syntax" target="_blank">Showdown</a>. + A preview is visible below. + </small> + </div> + <div class="card"> + <div class="card-header"> + Comment preview + </div> + <div class="card-body" id="commentLivePreview"></div> + </div> + <br> + <button type="submit" class="btn btn-primary">Submit</button> + <a class="btn btn-danger" href="{{ url_for('inventory.view_item', ics_id=item.ics_id) }}">Cancel</a> + </form> +{%- endmacro %} + +{% macro item_comment(comment) -%} + <div class="card border-light mb-3"> + <div class="card-header"> + {{ comment.user }} commented on {{ format_datetime(comment.created_at) }} + {% if comment.updated_at != comment.created_at %} + <small class="">Edited {{ format_datetime(comment.updated_at) }}</small> + {% endif %} + <a class="btn btn-light float-sm-right" href="{{ url_for('inventory.edit_comment', comment_id=comment.id) }}#body"> + <span class="oi oi-pencil" title="Edit comment" aria-hidden="true"></span> + </a> + </div> + <div class="card-body item-comment">{{ comment.body }}</div> + </div> +{%- endmacro %} diff --git a/app/templates/inventory/comment_item.html b/app/templates/inventory/comment_item.html index 04e602088b058cc5c42fbbc5b5e2c1cfc61addc1..6a91c40fcd97c6d9c5bf5b97f9ff28368a6cc796 100644 --- a/app/templates/inventory/comment_item.html +++ b/app/templates/inventory/comment_item.html @@ -1,24 +1,6 @@ {% extends "inventory/view_item.html" %} +{% from "_helpers.html" import item_comment_form %} {% block comment_item %} - <form id="CommentForm" method="POST"> - {{ form.hidden_tag() }} - <div class="form-group"> - {{ form.body.label() }} - {{ form.body(class_="form-control", rows=5, required=True) }} - <small class="form-text text-muted">Styling with Markdown is supported using - <a href="https://github.com/showdownjs/showdown/wiki/Showdown's-Markdown-syntax" target="_blank">Showdown</a>. - A preview is visible below. - </small> - </div> - <div class="card"> - <div class="card-header"> - Comment preview - </div> - <div class="card-body" id="commentLivePreview"></div> - </div> - <br> - <button type="submit" class="btn btn-primary">Submit</button> - <a class="btn btn-danger" href="{{ url_for('inventory.view_item', ics_id=item.ics_id) }}">Cancel</a> - </form> + {{ item_comment_form(form, item) }} {% endblock %} diff --git a/app/templates/inventory/edit_comment.html b/app/templates/inventory/edit_comment.html new file mode 100644 index 0000000000000000000000000000000000000000..df18650d809cfa901f97c08820bc99b66fefcc14 --- /dev/null +++ b/app/templates/inventory/edit_comment.html @@ -0,0 +1,15 @@ +{% extends "inventory/view_item.html" %} +{% from "_helpers.html" import item_comment_form, item_comment %} + + {% block comments %} + {% for comment in item.comments | sort(attribute='created_at') %} + {% if comment.id == comment_id %} + {{ item_comment_form(form, item) }} + {% else %} + {{ item_comment(comment) }} + {% endif %} + {% endfor %} + {% endblock %} + + {% block comment_item %} + {% endblock %} diff --git a/app/templates/inventory/view_item.html b/app/templates/inventory/view_item.html index 93b5069341886452267828e1c22329bba4aa0ded..336a04c2af622d6e0cf0e75b1bec39f864bda913 100644 --- a/app/templates/inventory/view_item.html +++ b/app/templates/inventory/view_item.html @@ -1,5 +1,5 @@ {% extends "inventory/items.html" %} -{% from "_helpers.html" import link_to_item, link_to_items, format_datetime, link_to_host %} +{% from "_helpers.html" import link_to_item, link_to_items, format_datetime, link_to_host, item_comment %} {% block title %}View Item - CSEntry{% endblock %} @@ -62,14 +62,12 @@ </dl> <h4>Comments</h4> + {% block comments %} {% for comment in item.comments | sort(attribute='created_at') %} - <div class="card border-light mb-3"> - <div class="card-header"> - {{ comment.user }} commented on {{ format_datetime(comment.created_at) }} - </div> - <div class="card-body item-comment">{{ comment.body }}</div> - </div> + {{ item_comment(comment) }} {% endfor %} + {% endblock %} + {% block comment_item %} <a class="btn btn-primary" href="{{ url_for('inventory.comment_item', ics_id=item.ics_id) }}#body">Comment</a> {% endblock %} diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py index 8b398826b3c67b58beceb0dc55205a1ee51ea768..65f5cdfed5e88525406cb939ff928885609136e4 100644 --- a/tests/functional/conftest.py +++ b/tests/functional/conftest.py @@ -25,6 +25,7 @@ register(factories.ModelFactory) register(factories.LocationFactory) register(factories.StatusFactory) register(factories.ItemFactory) +register(factories.ItemCommentFactory) register(factories.NetworkScopeFactory) register(factories.NetworkFactory) register(factories.InterfaceFactory) diff --git a/tests/functional/factories.py b/tests/functional/factories.py index 04805e5c67568f18a64a53213229a2eaaa4cb020..2631e8712b9a8675fdd05894b4573d21c6e7b2b6 100644 --- a/tests/functional/factories.py +++ b/tests/functional/factories.py @@ -85,6 +85,16 @@ class ItemFactory(factory.alchemy.SQLAlchemyModelFactory): user = factory.SubFactory(UserFactory) +class ItemCommentFactory(factory.alchemy.SQLAlchemyModelFactory): + class Meta: + model = models.ItemComment + sqlalchemy_session = common.Session + sqlalchemy_session_persistence = "commit" + + body = factory.Sequence(lambda n: f"comment{n}") + user = factory.SubFactory(UserFactory) + + class DomainFactory(factory.alchemy.SQLAlchemyModelFactory): class Meta: model = models.Domain diff --git a/tests/functional/test_web.py b/tests/functional/test_web.py index 048cf9a8785609c066acd0f07f9e3f71e10e39f3..cd655baa6c2f1cb8607bffc926aa684aab687ab3 100644 --- a/tests/functional/test_web.py +++ b/tests/functional/test_web.py @@ -226,3 +226,23 @@ def test_delete_interface_from_index(logged_rw_client, interface_factory, host_f instances, nb = models.Host.search("host1") assert list(instances) == [host1] assert nb == 1 + + +def test_edit_item_comment_in_index( + logged_rw_client, item_factory, item_comment_factory +): + item1 = item_factory(ics_id="AAA001") + comment = item_comment_factory(body="Hello", item_id=item1.id) + assert item1.comments == [comment] + # Edit the comment + body = "Hello world!" + response = logged_rw_client.post( + f"/inventory/items/comment/edit/{comment.id}", data={"body": body} + ) + assert response.status_code == 302 + # The comment was updated in the database + updated_comment = models.ItemComment.query.get(comment.id) + assert updated_comment.body == body + # And in the index + instances, nb = models.Item.search("world") + assert list(instances) == [item1]