Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions core/pins/migrations/0005_pins_to_follows.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from django.db import migrations

# TBv2's "My Pins" listed a user's own pins plus the org pins they created (includeCreatorPins). TBv3 keeps pins as
# overview highlights and lists follows as personal shortcuts, so each person's list becomes follows.
RESOURCE_MODELS = [('orgs', 'organization'), ('sources', 'source'), ('collections', 'collection')]


def is_public(obj):
# 'Edit' is retired and reads as 'View'
return (obj.public_access or '').lower() in ('view', 'edit')


def can_view_org(user, org):
"""CanViewConceptDictionary on an org: it's public, or the user is staff or a member."""
return user.is_staff or is_public(org) or user.organizations.filter(id=org.id).exists()


def can_view_repo(user, repo):
"""CanViewConceptDictionary on a repo: it's public (a version only while its HEAD is), or the user is staff, its
owner or a member of its org."""
if user.is_staff:
return True
if is_public(repo):
head = repo if repo.version == 'HEAD' else type(repo).objects.filter(
mnemonic=repo.mnemonic, organization_id=repo.organization_id, user_id=repo.user_id, version='HEAD',
is_active=True).first()
if head is None or is_public(head):
return True
if repo.organization_id:
return user.organizations.filter(id=repo.organization_id).exists()
return repo.user_id == user.id


def get_pin_follows(apps):
"""
(follower id, content type id, resource id) for every pin, followed by the person whose TBv2 My Pins list showed
it: a user pin's user, or an org pin's creator. Skips resources that are gone or deleted, and pins that list no
longer showed: a resource, or the pinning org, the person can't view any more.
"""
follows = set()
pins = apps.get_model('pins', 'Pin').objects.select_related('resource_type', 'user', 'created_by', 'organization')
for pin in pins:
follower = pin.user or pin.created_by
content_type = pin.resource_type
if (content_type.app_label, content_type.model) not in RESOURCE_MODELS:
continue
resource = apps.get_model(content_type.app_label, content_type.model).objects.filter(
id=pin.resource_id, is_active=True).first()
can_view = can_view_org if content_type.model == 'organization' else can_view_repo
if resource and can_view(follower, resource) and (
pin.organization is None or can_view_org(follower, pin.organization)):
follows.add((follower.id, content_type.id, resource.id))
return follows


def create_follows_from_pins(apps, schema_editor): # pylint: disable=unused-argument
"""Inserts the follows directly, not through UserProfile.follow(), so no Followed events are recorded. A follow
the person already has stays as it is."""
follow_model = apps.get_model('users', 'Follow')
follow_model.objects.bulk_create(
[
follow_model(follower_id=follower_id, following_type_id=type_id, following_id=resource_id)
for follower_id, type_id, resource_id in sorted(get_pin_follows(apps))
],
ignore_conflicts=True
)


class Migration(migrations.Migration):

dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('pins', '0004_pin_created_by'),
('users', '0037_userprofile_sso_id'),
('orgs', '0022_retire_public_edit_access'),
('sources', '0046_retire_public_edit_access'),
('collections', '0071_retire_public_edit_access'),
]

operations = [
# Pins stay as they are. Undoing it does nothing, as by then people may have changed their follows themselves.
migrations.RunPython(create_follows_from_pins, migrations.RunPython.noop),
]
74 changes: 72 additions & 2 deletions core/pins/tests.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import importlib

from django.core.exceptions import ValidationError
from django.db import connection
from django.db.migrations.loader import MigrationLoader
from mock import Mock

from core.collections.tests.factories import OrganizationCollectionFactory
from core.collections.tests.factories import OrganizationCollectionFactory, UserCollectionFactory
from core.common.constants import ACCESS_TYPE_NONE
from core.common.tests import OCLTestCase, OCLAPITestCase
from core.events.models import Event
from core.orgs.models import Organization
from core.orgs.tests.factories import OrganizationFactory
from core.pins.models import Pin
from core.sources.models import Source
from core.sources.tests.factories import OrganizationSourceFactory
from core.users.models import UserProfile
from core.users.models import UserProfile, Follow
from core.users.tests.factories import UserProfileFactory


Expand Down Expand Up @@ -406,3 +411,68 @@ def test_put_200(self):
user_pin2.refresh_from_db()
self.assertEqual(self.user_pin.order, 1)
self.assertEqual(user_pin2.order, 0)


class PinsToFollowsMigrationTest(OCLAPITestCase):
def test_migration_turns_my_pins_into_follows(self): # pylint: disable=too-many-locals
migration = importlib.import_module('core.pins.migrations.0005_pins_to_follows')
private_org = OrganizationFactory(public_access=ACCESS_TYPE_NONE)
public_org = OrganizationFactory()
member = UserProfileFactory(organizations=[private_org, public_org])
former_member = UserProfileFactory() # left private_org
outsider = UserProfileFactory()
admin = UserProfile.objects.get(username='ocladmin')
public_source = OrganizationSourceFactory(organization=public_org)
public_collection = OrganizationCollectionFactory(organization=public_org)
public_version = OrganizationSourceFactory(
organization=public_org, mnemonic=public_source.mnemonic, version='v1')
private_source = OrganizationSourceFactory(organization=private_org, public_access=ACCESS_TYPE_NONE)
public_version_of_private_source = OrganizationSourceFactory(
organization=private_org, mnemonic=private_source.mnemonic, version='v1')
private_collection = UserCollectionFactory(user=member, public_access=ACCESS_TYPE_NONE)
deleted_source = OrganizationSourceFactory(organization=public_org)
inactive_collection = OrganizationCollectionFactory(organization=public_org, is_active=False)

for resource in [
public_source, private_source, private_collection, private_org, deleted_source, inactive_collection]:
member.pins.create(resource=resource)
for resource in [public_source, private_source, private_collection, private_org]:
former_member.pins.create(resource=resource)
for resource in [public_version, public_version_of_private_source]:
outsider.pins.create(resource=resource)
# org pins go to their creator, while the creator can view the org too
public_org.pins.create(resource=public_source, created_by=member)
public_org.pins.create(resource=public_collection, created_by=former_member)
public_org.pins.create(resource=private_source, created_by=former_member)
private_org.pins.create(resource=public_source, created_by=outsider)
private_org.pins.create(resource=private_collection) # created by ocladmin, a staff user
Source.objects.filter(id=deleted_source.id).delete()
existing_follow = Follow.objects.create(follower=member, following=public_source)
pins = list(Pin.objects.order_by('id').values())
event_count = Event.objects.count()

historical_apps = MigrationLoader(connection).project_state(('pins', '0005_pins_to_follows')).apps
migration.create_follows_from_pins(historical_apps, None)
migration.create_follows_from_pins(historical_apps, None) # running it again adds nothing

def get_followed_uris(user):
return sorted(follow.following.uri for follow in user.following.all())

self.assertEqual(
get_followed_uris(member),
sorted([public_source.uri, private_source.uri, private_collection.uri, private_org.uri]))
self.assertEqual(get_followed_uris(former_member), sorted([public_source.uri, public_collection.uri]))
self.assertEqual(get_followed_uris(outsider), [public_version.uri])
self.assertEqual(get_followed_uris(admin), [private_collection.uri])
self.assertEqual(Follow.objects.count(), 8)
self.assertEqual(Follow.objects.get(follower=member, following_id=public_source.id).id, existing_follow.id)
self.assertEqual(list(Pin.objects.order_by('id').values()), pins)
self.assertEqual(Event.objects.count(), event_count)

# the follows are what each person's TBv2 My Pins list showed, less the deleted resources
for user in [member, former_member, outsider, admin]:
response = self.client.get(
user.uri + 'pins/?includeCreatorPins=true', HTTP_AUTHORIZATION='Token ' + user.get_token())
self.assertEqual(
sorted({pin['resource_uri'] for pin in response.data} - {None, inactive_collection.uri}),
get_followed_uris(user))
Loading