Welcome | Get started | Dive | Contribute | Topics | Reference | Changes | More

comments in Noi

The lino.modlib.comments plugin in Lino Noi is configured and used to satisfy the application requirements.

This page contains code snippets (lines starting with >>>), which are being tested during our development workflow. The following snippet initializes the demo project used throughout this page.

>>> from lino_book.projects.noi1r.startup import *

Overview

Public comments in Lino Noi are visible even to anonymous users.

There are seven Commentable models in Lino Noi, but only tickets have a CommentsByRFC panel in their detail.

>>> pprint(list(rt.models_by_base(comments.Commentable)))
[<class 'lino_xl.lib.contacts.models.Company'>,
 <class 'lino_xl.lib.contacts.models.Partner'>,
 <class 'lino_noi.lib.contacts.models.Person'>,
 <class 'lino_noi.lib.groups.models.Group'>,
 <class 'lino_noi.lib.tickets.models.Ticket'>,
 <class 'lino.modlib.uploads.models.Upload'>]

Whether a comment is private or not depends on its discussion topic: Comments on a ticket are public when neither the ticket nor its site are marked private.

Comments aren’t private by default in Noi:

>>> dd.plugins.users.private_default
False

Comments on a team are public when the team is not private.

Visibility of comments

The demo database contains 504 comments, of which 168 have no group and 462 are marked as confidential (private).

>>> comments.Comment.objects.all().count()
504
>>> comments.Comment.objects.filter(group__isnull=False).count()
168
>>> comments.Comment.objects.filter(private=True).count()
42
>>> comments.Comment.objects.filter(private=False).count()
462
>>> comments.Comment.objects.filter(ticket__isnull=False).count()
84
>>> comments.Comment.objects.filter(ticket__isnull=False).first()
Comment #337 ('Comment #337')
>>> comments.Comment.objects.filter(ticket=None).count()
420
>>> from django.db.models import Q
>>> rt.login("robin").show(comments.Comments,
...     column_names="id ticket__group user owner",
...     filter=Q(ticket__isnull=False),
...     limit=20, display_mode=DISPLAY_MODE_GRID)
...
===== ============ ================= =============================================
 ID    Team         Author            Topic
----- ------------ ----------------- ---------------------------------------------
 337   Sales team   Jean              `#116 (Why <p> tags are so bar) <…>`__
 338   Sales team   Luc               `#116 (Why <p> tags are so bar) <…>`__
 339   Sales team   Marc              `#116 (Why <p> tags are so bar) <…>`__
 340   Sales team   Mathieu           `#116 (Why <p> tags are so bar) <…>`__
 341   Sales team   Romain Raffault   `#116 (Why <p> tags are so bar) <…>`__
 342   Sales team   Rolf Rompen       `#116 (Why <p> tags are so bar) <…>`__
 343   Sales team   Robin Rood        `#116 (Why <p> tags are so bar) <…>`__
 344   Managers     Jean              `#115 (Cannot delete foo) <…>`__
 345   Managers     Luc               `#115 (Cannot delete foo) <…>`__
 346   Managers     Marc              `#115 (Cannot delete foo) <…>`__
 347   Managers     Mathieu           `#115 (Cannot delete foo) <…>`__
 348   Managers     Romain Raffault   `#115 (Cannot delete foo) <…>`__
 349   Managers     Rolf Rompen       `#115 (Cannot delete foo) <…>`__
 350   Managers     Robin Rood        `#115 (Cannot delete foo) <…>`__
 351   Developers   Jean              `#114 (No more foo when bar is gone) <…>`__
 352   Developers   Luc               `#114 (No more foo when bar is gone) <…>`__
 353   Developers   Marc              `#114 (No more foo when bar is gone) <…>`__
 354   Developers   Mathieu           `#114 (No more foo when bar is gone) <…>`__
 355   Developers   Romain Raffault   `#114 (No more foo when bar is gone) <…>`__
 356   Developers   Rolf Rompen       `#114 (No more foo when bar is gone) <…>`__
===== ============ ================= =============================================

Marc is a customer, so he can see only comments that are (1) public OR (2) his own OR (3) about a group (“team”) that he can see OR (4) about something that he can see.

Comments in Noi can be about tickets or about groups.

  • marc can see tickets that are (public OR his own) AND in a group that he can see

  • marc can see groups that are (public OR of which he is a member)

>>> rt.login('marc').user.user_type
<users.UserTypes.customer:100>
>>> qs = rt.login('marc').spawn(comments.RecentComments).data_iterator
>>> printsql(qs)
...
SELECT DISTINCT comments_comment.id,
                comments_comment.modified,
                comments_comment.created,
                comments_comment.body,
                comments_comment.body_short_preview,
                comments_comment.body_full_preview,
                comments_comment.user_id,
                comments_comment.private,
                comments_comment.group_id,
                comments_comment.owner_type_id,
                comments_comment.owner_id,
                comments_comment.reply_to_id,
                comments_comment.comment_type_id,
                COUNT(T6.id) AS num_replies,
                COUNT(comments_reaction.id) AS num_reactions
FROM comments_comment
LEFT OUTER JOIN groups_group ON (comments_comment.group_id = groups_group.id)
LEFT OUTER JOIN groups_membership ON (groups_group.id = groups_membership.group_id)
LEFT OUTER JOIN comments_comment T6 ON (comments_comment.id = T6.reply_to_id)
LEFT OUTER JOIN comments_reaction ON (comments_comment.id = comments_reaction.comment_id)
WHERE (NOT comments_comment.private
       OR comments_comment.user_id = 4
       OR NOT groups_group.private
       OR groups_membership.user_id = 4)
GROUP BY comments_comment.id,
         comments_comment.modified,
         comments_comment.created,
         comments_comment.body,
         comments_comment.body_short_preview,
         comments_comment.body_full_preview,
         comments_comment.user_id,
         comments_comment.private,
         comments_comment.group_id,
         comments_comment.owner_type_id,
         comments_comment.owner_id,
         comments_comment.reply_to_id,
         comments_comment.comment_type_id
ORDER BY comments_comment.created DESC
>>> rt.login("robin").show(comments.RecentComments,
...     column_names="id ticket__group user owner",
...     filter=Q(ticket__isnull=False),
...     limit=10, display_mode=DISPLAY_MODE_GRID)
...
===== ============ ================= ============================================
 ID    Team         Author            Topic
----- ------------ ----------------- --------------------------------------------
 420   Developers   Robin Rood        `#105 (Irritating message when bar) <…>`__
 419   Developers   Rolf Rompen       `#105 (Irritating message when bar) <…>`__
 418   Developers   Romain Raffault   `#105 (Irritating message when bar) <…>`__
 417   Developers   Mathieu           `#105 (Irritating message when bar) <…>`__
 416   Developers   Marc              `#105 (Irritating message when bar) <…>`__
 415   Developers   Luc               `#105 (Irritating message when bar) <…>`__
 414   Developers   Jean              `#105 (Irritating message when bar) <…>`__
 413   Managers     Robin Rood        `#106 (How can I see where bar?) <…>`__
 412   Managers     Rolf Rompen       `#106 (How can I see where bar?) <…>`__
 411   Managers     Romain Raffault   `#106 (How can I see where bar?) <…>`__
===== ============ ================= ============================================
>>> rt.login("marc").show(comments.RecentComments,
...     column_names="id user group ticket__group owner",
...     filter=Q(ticket__isnull=False),
...     limit=10, display_mode=DISPLAY_MODE_GRID)
...
===== ================= ============ ============ ============================================
 ID    Author            Team         Team         Topic
----- ----------------- ------------ ------------ --------------------------------------------
 420   Robin Rood        Developers   Developers   `#105 (Irritating message when bar) <…>`__
 419   Rolf Rompen       Developers   Developers   `#105 (Irritating message when bar) <…>`__
 418   Romain Raffault   Developers   Developers   `#105 (Irritating message when bar) <…>`__
 417   Mathieu           Developers   Developers   `#105 (Irritating message when bar) <…>`__
 416   Marc              Developers   Developers   `#105 (Irritating message when bar) <…>`__
 415   Luc               Developers   Developers   `#105 (Irritating message when bar) <…>`__
 414   Jean              Developers   Developers   `#105 (Irritating message when bar) <…>`__
 409   Marc              Managers     Managers     `#106 (How can I see where bar?) <…>`__
 406   Robin Rood        Sales team   Sales team   `#107 (Misc optimizations in Baz) <…>`__
 405   Rolf Rompen       Sales team   Sales team   `#107 (Misc optimizations in Baz) <…>`__
===== ================= ============ ============ ============================================

Anonymous users can see only public comments.

>>> rt.show(comments.RecentComments,
...     column_names="id group user owner",
...     filter=Q(ticket__isnull=False),
...     limit=10, display_mode=DISPLAY_MODE_GRID)
...
===== ============ ================= ============================================
 ID    Team         Author            Topic
----- ------------ ----------------- --------------------------------------------
 420   Developers   Robin Rood        `#105 (Irritating message when bar) <…>`__
 419   Developers   Rolf Rompen       `#105 (Irritating message when bar) <…>`__
 418   Developers   Romain Raffault   `#105 (Irritating message when bar) <…>`__
 417   Developers   Mathieu           `#105 (Irritating message when bar) <…>`__
 416   Developers   Marc              `#105 (Irritating message when bar) <…>`__
 415   Developers   Luc               `#105 (Irritating message when bar) <…>`__
 414   Developers   Jean              `#105 (Irritating message when bar) <…>`__
 406   Sales team   Robin Rood        `#107 (Misc optimizations in Baz) <…>`__
 405   Sales team   Rolf Rompen       `#107 (Misc optimizations in Baz) <…>`__
 404   Sales team   Romain Raffault   `#107 (Misc optimizations in Baz) <…>`__
===== ============ ================= ============================================

Just to make sure: is comment 406 really public? Yes, because neither the group, nor the owner, nor the comment itself have been marked as private (“confidential”).

>>> comment = comments.Comment.objects.get(pk=406)
>>> comment.private
False
>>> comment.group.private
False
>>> comment.owner.private
False

Overview how many database rows each user sees:

>>> rows = []
>>> views = (comments.Comments, tickets.Tickets, groups.Groups)
>>> headers = ["User", "type"] + [i.__name__ for i in views]
>>> user_list = [users.User.get_anonymous_user()] + list(users.User.objects.all())
>>> for u in user_list:
...    cells = [str(u.username), u.user_type.name]
...    for dv in views:
...       qs = dv.create_request(user=u).data_iterator
...       cells.append(str(qs.count()))
...    rows.append(cells)
>>> print(rstgen.table(headers, rows))
...
=========== ============= ========== ========= ========
 User        type          Comments   Tickets   Groups
----------- ------------- ---------- --------- --------
 anonymous   anonymous     98         58        0
 jean        developer     480        106       2
 luc         developer     504        116       3
 marc        customer      480        106       2
 mathieu     contributor   480        106       2
 romain      admin         504        116       3
 rolf        admin         504        116       3
 robin       admin         504        116       3
=========== ============= ========== ========= ========

#5759 (Anonymous can GET private comments)

Ticket #5759 (Anonymous can GET private comments) was a security issue between 20240920 and 20241001.

Let’s pick a confidential comment for the following tests:

>>> obj = comments.Comment.objects.filter(private=True).last()
>>> obj.private
True
>>> obj.pk
413

For example the following request failed to cause an exception.

An anonymous request may of course not see it:

>>> test_client.cookies  # nobody is signed in
<SimpleCookie: >

When processing the incoming request, Lino logs a warning in the logger and then returns a 404 error:

>>> res = test_client.get("/api/comments/RecentComments/413")
Error during ApiElement.get(): Invalid request for '413' on comments.RecentComments (Row 413 does not exist on comments.RecentComments)
Row 413 does not exist on comments.RecentComments
Traceback (most recent call last):
...
django.http.response.Http404: Row 413 does not exist on comments.RecentComments
Not Found: /api/comments/RecentComments/413
>>> res.status_code
404

It would be more correct to return 403 (Forbidden) instead of 404 (Not found), at least in above case, because the requested comment 413 does exist, only the user lacks permission to see it. The case below is quite similar, except that a comment with id 123456789 does not exist:

>>> res = test_client.get("/api/comments/RecentComments/123456789")
...
Error during ApiElement.get(): Invalid request for '123456789' on comments.RecentComments (Row 123456789 does not exist on comments.RecentComments)
Row 123456789 does not exist on comments.RecentComments
Traceback (most recent call last):
...
django.http.response.Http404: Row 123456789 does not exist on comments.RecentComments
Not Found: /api/comments/RecentComments/123456789
>>> res.status_code
404

In both cases we return the same error code until further notice because differentiating them would need an additional database query.

Ticket #5759 had been introduced by #5751 (ObjectDoesNotExist: Invalid primary key 4968 for avanti.Clients), which came because avanti.Clients has default table parameters that show only registered clients. When the user manually removed that filter and then double-clicked on a client that was usually filtered out, Lino gave this (false) error.

#5763 (Big unreadable warning with HTML tags)

#5763 (A permalink with Bad request shows a big unreadable warning with HTML tags)

TODO: The React front end does above call as an AJAX call and expects a JSON-encoded response. But Lino returns Django’s default HttpResponseNotFound response, which is in HTML. That’s why the user sees a big red warning saying

Bad request

<!DOCTYPE html> <html lang=”en”> <head> <meta http-equiv=”content-type” content=”text/html; charset=utf-8”> <title>Page not found at /media/cache/json/Lino_comments.Comments.413_000_en.json</title> <meta name=”robots” content=”NONE,NOARCHIVE”> <style> html * { padding:0; margin:0; …

>>> print(res.content.decode())
<!DOCTYPE html>
<html lang="en">
<head>
...