Repository: learningequality/kolibri

Issue:
'recent' coach list contains duplicates

they should be de-duped:

![image](https://cloud.githubusercontent.com/assets/2367265/25322050/3c242312-2868-11e7-92ba-96a64488502a.png)




Discussion:
@rtibbles Correct me if I'm wrong, but I feel like this and https://github.com/learningequality/kolibri/issues/1271 might be caused by the same underlying issue?

I don't get dupes staying within the same channel, even if I have multiple users looking at the same content. I'm thinking that maybe, in this case, the exercise goes under various topic trees?
My intuition is that this shouldn't be the cause, because the initial summarisation happens on contentsummarylogs that aggregate by contentid, not the node id. So, there is only one summarylog per content. However, it is possible that if when the lookup is done, the other node happens to be looked up one time, and another node the other, that could cause it, but it would need some chasing down.

I don't think #1271 is linked - will comment separately there.
I can't reproduce this, using same channel, same exercises. Has this been fixed?
I don't think anyone has submitted an explicit fix for it - @indirectlylit is this still live for you?
yes, I'm still reproducing:

![image](https://cloud.githubusercontent.com/assets/2367265/25500699/df802784-2b44-11e7-823c-7125c24d648c.png)

I'll dig in a little...
@indirectlylit might be helpful to take a look at the `pageState` to see if there are differences in the objects for each report
I believe that these are content items with multiple locations in the tree. For example, all these have the same `content_id` but different `pk`s and `parent`s:

```json
[
  {
    "pk": "26ae896b07165a21a6004ff2f6cc2a18",
    "content_id": "fe64db5a5335545aa658b443ad5d634d",
    "title": "Add within 5",
    "progress": [
      {
        "log_count_complete": 0,
        "total_progress": 0.2,
        "log_count_total": 1
      }
    ],
    "kind": "exercise",
    "last_active": "2017-04-25T21:32:16.722Z",
    "parent": {
      "pk": "9b181802c3e25ea38d7c3826fe987a22",
      "title": "Represent and Solve Problems"
    }
  },
  {
    "pk": "0fe99d22381457cbbff1f3d3dac4fbf3",
    "content_id": "fe64db5a5335545aa658b443ad5d634d",
    "title": "Add within 5",
    "progress": [
      {
        "log_count_complete": 0,
        "total_progress": 0.2,
        "log_count_total": 1
      }
    ],
    "kind": "exercise",
    "last_active": "2017-04-25T21:32:16.722Z",
    "parent": {
      "pk": "f1d9396981995058adc2610fb2c953dc",
      "title": "What is addition? What is subtraction?"
    }
  },
  {
    "pk": "47f5d3200709579982328da53a0b7347",
    "content_id": "fe64db5a5335545aa658b443ad5d634d",
    "title": "Add within 5",
    "progress": [
      {
        "log_count_complete": 0,
        "total_progress": 0.2,
        "log_count_total": 1
      }
    ],
    "kind": "exercise",
    "last_active": "2017-04-25T21:32:16.722Z",
    "parent": {
      "pk": "fcdf34d304675181a8affd0ff9d9a1cd",
      "title": "What is addition? What is subtraction?"
    }
  },
  {
    "pk": "8c73096078f4561698ab541177be1fe8",
    "content_id": "fe64db5a5335545aa658b443ad5d634d",
    "title": "Add within 5",
    "progress": [
      {
        "log_count_complete": 0,
        "total_progress": 0.2,
        "log_count_total": 1
      }
    ],
    "kind": "exercise",
    "last_active": "2017-04-25T21:32:16.722Z",
    "parent": {
      "pk": "3ced2b624cd95c37bc379b1ff7b665b1",
      "title": "Basic addition and subtraction"
    }
  },
  {
    "pk": "5fb0846b993d5dc7923a24b6f990a6d5",
    "content_id": "fe64db5a5335545aa658b443ad5d634d",
    "title": "Add within 5",
    "progress": [
      {
        "log_count_complete": 0,
        "total_progress": 0.2,
        "log_count_total": 1
      }
    ],
    "kind": "exercise",
    "last_active": "2017-04-25T21:32:16.722Z",
    "parent": {
      "pk": "2c3eec9b3fbc5791be9154a111729ae8",
      "title": "Basic addition and subtraction"
    }
  },
]
```
Yes, looking at the serializer it does not unique-ify by `content_id`.

Resolving patch:
diff --git a/kolibri/plugins/coach/api.py b/kolibri/plugins/coach/api.py
--- a/kolibri/plugins/coach/api.py
+++ b/kolibri/plugins/coach/api.py
@@ -1,7 +1,7 @@
 import datetime
 from dateutil.parser import parse
 
-from django.db.models import Q
+from django.db.models import Min, Q
 from django.utils import timezone
 
 from kolibri.auth.constants import role_kinds
@@ -98,4 +98,11 @@ def get_queryset(self):
             Q(progress__gt=0) | Q(masterylogs__in=attempted_mastery_logs),
             user__in=get_members_or_user(self.kwargs['collection_kind'], self.kwargs['collection_id']),
             end_timestamp__gte=datetime_cutoff).values_list('content_id')
-        return ContentNode.objects.filter(content_id__in=recent_content_items)
+        # note from rtibbles:
+        # As good as either I or jamalex could come up with to ensure that we only return
+        # unique content_id'ed ContentNodes from the coach recent report endpoint.
+        # Would have loved to use distinct('content_id'), but unfortunately DISTINCT ON is Postgresql only
+        pks_with_unique_content_ids = ContentNode.objects.filter(
+            content_id__in=recent_content_items).values('content_id').annotate(
+            pk=Min('pk')).values_list('pk', flat=True)
+        return ContentNode.objects.filter(pk__in=pks_with_unique_content_ids)


Test patch: