Skip to content

Commit bb70948

Browse files
committed
do not count contrib views
1 parent eab3950 commit bb70948

3 files changed

Lines changed: 119 additions & 0 deletions

File tree

api/metrics/utils.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import re
2+
from urllib.parse import urlsplit
23

34
import pytz
45

56
from datetime import timedelta, datetime
67
from django.utils import timezone
78
from rest_framework.exceptions import ValidationError
89

10+
from osf.models import AbstractNode, Guid
11+
from osf.metrics.counted_usage import _get_immediate_wrapper
12+
913

1014
DATETIME_FORMAT = '%Y-%m-%dT%H:%M'
1115
DATE_FORMAT = '%Y-%m-%d'
@@ -114,3 +118,47 @@ def parse_date_range(query_params, is_monthly=False):
114118
start_date, end_date = parse_dates(query_params, is_monthly=is_monthly)
115119
report_date_range = {'gte': str(start_date), 'lte': str(end_date)}
116120
return report_date_range
121+
122+
123+
def _user_has_read_on_resolved_node(user, guid_referent):
124+
"""True if ``user`` has READ on the node this referent belongs to."""
125+
current = guid_referent
126+
while current is not None and not isinstance(current, AbstractNode):
127+
current = _get_immediate_wrapper(current)
128+
if current is None or not isinstance(current, AbstractNode):
129+
return False
130+
return current.contributors_and_group_members.filter(guids___id=user._id).exists()
131+
132+
133+
def should_skip_counted_usage(user, *, item_guid=None, pageview_info=None):
134+
"""Return True when this usage should not be recorded."""
135+
if not getattr(user, 'is_authenticated', False):
136+
return False
137+
138+
referents = []
139+
seen_ids = set()
140+
141+
def _add_referent(ref):
142+
if ref is None:
143+
return
144+
key = (ref.__class__.__name__, ref.pk)
145+
if key in seen_ids:
146+
return
147+
seen_ids.add(key)
148+
referents.append(ref)
149+
150+
if item_guid:
151+
guid_obj = Guid.load(item_guid)
152+
if guid_obj and guid_obj.referent:
153+
_add_referent(guid_obj.referent)
154+
155+
page_url = (pageview_info or {}).get('page_url')
156+
if page_url:
157+
for segment in urlsplit(page_url).path.split('/'):
158+
if not segment or len(segment) < 5:
159+
continue
160+
guid_obj = Guid.load(segment)
161+
if guid_obj and guid_obj.referent:
162+
_add_referent(guid_obj.referent)
163+
164+
return any(_user_has_read_on_resolved_node(user, ref) for ref in referents)

api/metrics/views.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
from api.metrics.utils import (
4444
parse_datetimes,
4545
parse_date_range,
46+
should_skip_counted_usage,
4647
)
4748
from api.nodes.permissions import MustBePublic
4849

@@ -388,6 +389,12 @@ class CountedAuthUsageView(JSONAPIBaseView):
388389
def post(self, request, *args, **kwargs):
389390
serializer = self.serializer_class(data=request.data)
390391
serializer.is_valid(raise_exception=True)
392+
if should_skip_counted_usage(
393+
request.user,
394+
item_guid=serializer.validated_data.get('item_guid'),
395+
pageview_info=serializer.validated_data.get('pageview_info'),
396+
):
397+
return HttpResponse(status=204)
391398
session_id, user_is_authenticated = self._get_session_id(
392399
request,
393400
client_session_id=serializer.validated_data.get('client_session_id'),

api_tests/metrics/test_counted_usage.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@
33
import pytest
44
from unittest import mock
55

6+
from framework.auth.core import Auth
7+
68
from osf_tests.factories import (
79
AuthUserFactory,
810
PreprintFactory,
911
NodeFactory,
12+
ProjectFactory,
1013
RegistrationFactory,
1114
# UserFactory,
1215
)
16+
from osf.utils.permissions import READ
1317
from api_tests.utils import create_test_file
1418

1519

@@ -351,3 +355,63 @@ def test_child_registration_file(self, app, mock_save, child_reg_file_guid, chil
351355
'surrounding_guids': None,
352356
},
353357
)
358+
359+
360+
@pytest.mark.django_db
361+
class TestContributorExclusion:
362+
363+
def test_creator_pageview_not_recorded(self, app, mock_save):
364+
user = AuthUserFactory()
365+
project = ProjectFactory(creator=user)
366+
payload = counted_usage_payload(
367+
item_guid=project._id,
368+
action_labels=['view', 'web'],
369+
pageview_info={'page_url': f'https://osf.io/{project._id}/'},
370+
)
371+
resp = app.post_json_api(COUNTED_USAGE_URL, payload, auth=user.auth)
372+
assert resp.status_code == 204
373+
assert mock_save.call_count == 0
374+
375+
def test_read_contributor_pageview_not_recorded(self, app, mock_save):
376+
creator = AuthUserFactory()
377+
reader = AuthUserFactory()
378+
project = ProjectFactory(creator=creator)
379+
project.add_contributor(reader, permissions=READ, auth=Auth(creator))
380+
payload = counted_usage_payload(
381+
item_guid=project._id,
382+
action_labels=['view', 'web'],
383+
pageview_info={'page_url': f'https://osf.io/{project._id}/analytics/'},
384+
)
385+
resp = app.post_json_api(COUNTED_USAGE_URL, payload, auth=reader.auth)
386+
assert resp.status_code == 204
387+
assert mock_save.call_count == 0
388+
389+
def test_non_contributor_pageview_recorded(self, app, mock_save):
390+
creator = AuthUserFactory()
391+
visitor = AuthUserFactory()
392+
project = ProjectFactory(creator=creator, is_public=True)
393+
payload = counted_usage_payload(
394+
item_guid=project._id,
395+
action_labels=['view', 'web'],
396+
pageview_info={'page_url': f'https://osf.io/{project._id}/'},
397+
)
398+
resp = app.post_json_api(COUNTED_USAGE_URL, payload, auth=visitor.auth)
399+
assert resp.status_code == 201
400+
assert mock_save.call_count == 1
401+
402+
def test_parent_contributor_not_on_child_component_pageview_recorded(self, app, mock_save):
403+
creator = AuthUserFactory()
404+
child_owner = AuthUserFactory()
405+
parent_reader = AuthUserFactory()
406+
parent = ProjectFactory(creator=creator, is_public=True)
407+
child = NodeFactory(parent=parent, creator=child_owner, is_public=True)
408+
parent.add_contributor(parent_reader, permissions=READ, auth=Auth(creator))
409+
assert not child.contributors_and_group_members.filter(guids___id=parent_reader._id).exists()
410+
payload = counted_usage_payload(
411+
item_guid=child._id,
412+
action_labels=['view', 'web'],
413+
pageview_info={'page_url': f'https://osf.io/{child._id}/'},
414+
)
415+
resp = app.post_json_api(COUNTED_USAGE_URL, payload, auth=parent_reader.auth)
416+
assert resp.status_code == 201
417+
assert mock_save.call_count == 1

0 commit comments

Comments
 (0)