Repository: uclapi/uclapi

Issue:
[Feature Request] Separate most popular methods by service
In the `/dashboard/api/analytics/methods` endpoint we list the most popular methods. This however is not useful without also knowing the service for example both `https://uclapi.com/timetable/data/modules` and `https://uclapi.com/timetable/data/courses/modules` are classed as the `modules` method` but they are different.


Discussion:
It seems to me there are two ways to implement this. Either break compatibility with the results that are currently being returned by the api, or add a request parameter that toggles whether to group methods by services that defaults to False.

Resolving patch:
diff --git a/backend/uclapi/common/decorators.py b/backend/uclapi/common/decorators.py
--- a/backend/uclapi/common/decorators.py
+++ b/backend/uclapi/common/decorators.py
@@ -49,7 +49,7 @@ def how_many_seconds_until_midnight():
 def log_api_call(request, token, token_type):
     """This functions handles logging of api calls."""
     service = request.path.split("/")[1]
-    method = request.path.split("/")[2]
+    method = "/".join(request.path.split("/")[2:])
 
     headers = request.META
     version_headers = {}
diff --git a/backend/uclapi/dashboard/api_applications.py b/backend/uclapi/dashboard/api_applications.py
--- a/backend/uclapi/dashboard/api_applications.py
+++ b/backend/uclapi/dashboard/api_applications.py
@@ -247,7 +247,7 @@ def set_callback_url(request):
     url_not_safe_saved = is_url_unsafe(new_callback_url)
     if url_not_safe_saved:
         if url_not_safe_saved == NOT_HTTPS:
-            message = "The requested callback URL does not "\
+            message = "The requested callback URL does not " \
                       "start with 'https://'."
         elif url_not_safe_saved == NOT_VALID:
             message = "The requested callback URL is not valid."
@@ -535,15 +535,43 @@ def most_popular_service(request):
 
 
 def most_popular_method(request):
-    try:
-        service = request.GET["service"]
-        most_common = APICall.objects.filter(service__exact=service).values(
-            "method").annotate(count=Count('method')).order_by("-count")
-    except MultiValueDictKeyError:
-        most_common = APICall.objects.values(
-            "method").annotate(count=Count('method')).order_by("-count")
-
-    most_common = list(most_common)
+    service = request.GET.get("service", False)
+    split_by_service = request.GET.get("split_services", "false")
+    split_by_service = False if split_by_service.lower() in [
+        "false", "0"] else True
+
+    if service:
+        most_common = APICall.objects.filter(service__exact=service)\
+            .values("service", "method").annotate(count=Count('method')).order_by("-count")
+    else:
+        most_common = APICall.objects\
+            .values("service", "method").annotate(count=Count('method')).order_by("-count")
+
+    if not split_by_service:
+        t_most_common_counter = {}
+        for m in most_common:
+            if m["method"].split("/")[0] in t_most_common_counter:
+                t_most_common_counter[m["method"].split("/")[0]] += m["count"]
+            else:
+                t_most_common_counter[m["method"].split("/")[0]] = m["count"]
+        print(t_most_common_counter)
+
+        most_common = [{"method": method, "count": count}
+                       for method, count in t_most_common_counter.items()]
+    else:
+        temp_most_common_aggregate = {}
+        for method in most_common:
+            if method["service"] in temp_most_common_aggregate:
+                temp_most_common_aggregate[method["service"]].append({
+                    "method": method["method"],
+                    "count": method["count"]
+                })
+            else:
+                temp_most_common_aggregate[method["service"]] = [{
+                    "method": method["method"],
+                    "count": method["count"]
+                }]
+        most_common = temp_most_common_aggregate
 
     return PrettyJsonResponse({
         "ok": True,


Test patch:
diff --git a/backend/uclapi/dashboard/tests.py b/backend/uclapi/dashboard/tests.py
--- a/backend/uclapi/dashboard/tests.py
+++ b/backend/uclapi/dashboard/tests.py
@@ -905,6 +905,29 @@ def test_change_callback_url_not_valid(self):
             "The requested callback URL does not start with 'https://'."
         )
 
+    def test_change_callback_url_https_not_valid(self):
+        user_ = User.objects.create(
+            email="test@ucl.ac.uk",
+            cn="test",
+            given_name="Test Test"
+        )
+        app_ = App.objects.create(user=user_, name="An App")
+        request = self.factory.post(
+            '/api/setcallbackurl/',
+            {
+                "app_id": app_.id,
+                "callback_url": "https://a"
+            }
+        )
+        request.session = {'user_id': user_.id}
+        response = set_callback_url(request)
+        content = json.loads(response.content.decode())
+        self.assertEqual(response.status_code, 400)
+        self.assertEqual(
+            content["message"],
+            "The requested callback URL is not valid."
+        )
+
     def test_change_callback_url_success(self):
         user_ = User.objects.create(
             email="test@ucl.ac.uk",
@@ -1563,6 +1586,110 @@ def test_analytics_most_popular_method_filter_by_service_empty_good_flow(
         self.assertEqual(response.status_code, 200)
         self.assertEqual(content["data"], [])
 
+    def test_analytics_most_popular_method_alternate_split_services(self):
+        # Set up token
+        user_ = User.objects.create(
+            email="test@ucl.ac.uk",
+            cn="test",
+            given_name="Test Test"
+        )
+        app_ = App.objects.create(user=user_, name="An App")
+
+        # Create some request objects
+        _ = APICall.objects.create(app=app_, user=user_,
+                                   token_type="general",
+                                   service="service1",
+                                   method="method1",
+                                   queryparams="")
+
+        _ = APICall.objects.create(app=app_, user=user_,
+                                   token_type="general",
+                                   service="service1",
+                                   method="method1",
+                                   queryparams="")
+
+        _ = APICall.objects.create(app=app_, user=user_,
+                                   token_type="general",
+                                   service="service1",
+                                   method="method2",
+                                   queryparams="")
+
+        _ = APICall.objects.create(app=app_, user=user_,
+                                   token_type="general",
+                                   service="service2",
+                                   method="method2",
+                                   queryparams="")
+
+        # Hit endpoint and check number is correct
+        request = self.factory.get(
+            '/api/analytics/methods',
+            {
+                "split_services": "true"
+            }
+        )
+        response = most_popular_method(request)
+        content = json.loads(response.content.decode())
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(content["data"], {
+            "service1": [
+                {
+                    "method": "method1",
+                    "count": 2,
+                },
+                {
+                    "method": "method2",
+                    "count": 1,
+                }
+            ],
+            "service2": [
+                {
+                    "method": "method2",
+                    "count": 1,
+                }
+            ]
+        })
+
+    def test_analytics_most_popular_method_alternate_no_split_services(self):
+        # Set up token
+        user_ = User.objects.create(
+            email="test@ucl.ac.uk",
+            cn="test",
+            given_name="Test Test"
+        )
+        app_ = App.objects.create(user=user_, name="An App")
+
+        # Create some request objects
+        _ = APICall.objects.create(app=app_, user=user_,
+                                   token_type="general",
+                                   service="service1",
+                                   method="method1/name1",
+                                   queryparams="")
+
+        _ = APICall.objects.create(app=app_, user=user_,
+                                   token_type="general",
+                                   service="service1",
+                                   method="method1/name2",
+                                   queryparams="")
+
+        _ = APICall.objects.create(app=app_, user=user_,
+                                   token_type="general",
+                                   service="service2",
+                                   method="method2",
+                                   queryparams="")
+
+        # Hit endpoint and check number is correct
+        request = self.factory.get(
+            '/api/analytics/methods',
+            {
+                "split_services": "false"
+            }
+        )
+        response = most_popular_method(request)
+        content = json.loads(response.content.decode())
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(content["data"], [{'method': 'method1', 'count': 2},
+                                           {'method': 'method2', 'count': 1}])
+
     def test_analytics_users_per_app_good_flow(self):
         dev_ = User.objects.create(
             email="dev@ucl.ac.uk",