Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Populate paper feed entries #2089

Merged
merged 7 commits into from
Jan 29, 2025
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,6 @@ src/Dockerfile
# Coverage
.coverage
coverage.xml

# Uploads from some paper tests
uploads/
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Generated by Django 5.1.5 on 2025-01-28 23:20

from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("contenttypes", "0002_remove_content_type_name"),
("feed", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.AddIndex(
model_name="feedentry",
index=models.Index(
fields=["parent_content_type", "parent_object_id"],
name="feed_parent_lookup_idx",
),
),
migrations.AddConstraint(
model_name="feedentry",
constraint=models.UniqueConstraint(
fields=(
"content_type",
"object_id",
"parent_content_type",
"parent_object_id",
),
name="unique_parent_child_combination",
),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Generated by Django 5.1.5 on 2025-01-29 13:42

from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("contenttypes", "0002_remove_content_type_name"),
("feed", "0002_feedentry_feed_parent_lookup_idx_and_more"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.RemoveConstraint(
model_name="feedentry",
name="unique_parent_child_combination",
),
migrations.AddConstraint(
model_name="feedentry",
constraint=models.UniqueConstraint(
fields=(
"content_type",
"object_id",
"parent_content_type",
"parent_object_id",
"action",
"user",
),
name="unique_feed_entry",
),
),
]
73 changes: 73 additions & 0 deletions src/feed/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from django.contrib.contenttypes.models import ContentType
from django.db import models

from researchhub.celery import app
from user.models import User
from utils.models import DefaultModel

Expand Down Expand Up @@ -30,3 +31,75 @@

action = models.TextField(choices=action_choices)
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True)

class Meta:
indexes = [
models.Index(
fields=["parent_content_type", "parent_object_id"],
name="feed_parent_lookup_idx",
)
]
constraints = [
models.UniqueConstraint(
fields=[
"content_type",
"object_id",
"parent_content_type",
"parent_object_id",
"action",
"user",
],
name="unique_feed_entry",
)
]


@app.task
def create_feed_entry(
item_id,
item_content_type_id,
action,
parent_item_id,
parent_content_type_id,
user_id=None,
):
# Get the ContentType objects
item_content_type = ContentType.objects.get(id=item_content_type_id)
parent_content_type = ContentType.objects.get(id=parent_content_type_id)

# Get the actual model instances
item = item_content_type.get_object_for_this_type(id=item_id)
parent_item = parent_content_type.get_object_for_this_type(id=parent_item_id)
if user_id:
user = User.objects.get(id=user_id)

Check warning on line 74 in src/feed/models.py

View check run for this annotation

Codecov / codecov/patch

src/feed/models.py#L74

Added line #L74 was not covered by tests
else:
user = None
# Create the feed entry
FeedEntry.objects.create(
user=user,
item=item,
content_type=item_content_type,
object_id=item_id,
action=action,
parent_item=parent_item,
parent_content_type=parent_content_type,
parent_object_id=parent_item_id,
)


@app.task
def delete_feed_entry(
item_id,
item_content_type_id,
parent_item_id,
parent_item_content_type_id,
):
item_content_type = ContentType.objects.get(id=item_content_type_id)
parent_item_content_type = ContentType.objects.get(id=parent_item_content_type_id)
feed_entry = FeedEntry.objects.get(
object_id=item_id,
content_type=item_content_type,
parent_object_id=parent_item_id,
parent_content_type=parent_item_content_type,
)
feed_entry.delete()
7 changes: 5 additions & 2 deletions src/feed/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,15 @@ def test_feed_returns_followed_items(self):

def test_feed_pagination(self):
"""Test feed pagination"""
for _ in range(25):
for i in range(25):
paper = Paper.objects.create(
title=f"Test Paper {i}",
)
FeedEntry.objects.create(
user=self.user,
action="PUBLISH",
content_type=self.paper_content_type,
object_id=self.paper.id,
object_id=paper.id,
parent_content_type=self.hub_content_type,
parent_object_id=self.hub.id,
)
Expand Down
48 changes: 45 additions & 3 deletions src/paper/signals.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from django.db.models.signals import post_delete, post_save
from django.contrib.contenttypes.models import ContentType
from django.db.models.signals import m2m_changed, post_save
from django.dispatch import receiver
from django.utils.crypto import get_random_string
from django.utils.text import slugify

from paper.related_models.authorship_model import Authorship
from paper.related_models.citation_model import Citation
from feed.models import create_feed_entry, delete_feed_entry
from hub.models import Hub
from researchhub_document.models import ResearchhubUnifiedDocument
from researchhub_document.related_models.constants.document_type import (
PAPER as PAPER_DOC_TYPE,
Expand Down Expand Up @@ -49,6 +50,47 @@
log_error("EXCPETION (add_unified_doc): ", e)


@receiver(m2m_changed, sender=Paper.hubs.through, dispatch_uid="paper_hubs_changed")
def handle_paper_hubs_changed(sender, instance, action, pk_set, **kwargs):
if action == "post_add":
for hub_id in pk_set:
if isinstance(instance, Paper):
hub = instance.hubs.get(id=hub_id)
paper = instance
else: # instance is Hub
hub = instance
paper = hub.papers.get(id=hub_id)

create_feed_entry.apply_async(
args=(
paper.id,
ContentType.objects.get_for_model(paper).id,
"PUBLISH",
hub.id,
ContentType.objects.get_for_model(hub).id,
),
priority=1,
)
elif action == "post_remove":
for hub_id in pk_set:
if isinstance(instance, Paper):
hub = Hub.objects.get(id=hub_id)
paper = instance
else: # instance is Hub
hub = instance
paper = Paper.objects.get(id=hub_id)

Check warning on line 81 in src/paper/signals.py

View check run for this annotation

Codecov / codecov/patch

src/paper/signals.py#L80-L81

Added lines #L80 - L81 were not covered by tests

delete_feed_entry.apply_async(
args=(
paper.id,
ContentType.objects.get_for_model(paper).id,
hub.id,
ContentType.objects.get_for_model(hub).id,
),
priority=1,
)


def check_file_updated(update_fields, file):
if update_fields is not None and file:
return "file" in update_fields
Expand Down
34 changes: 34 additions & 0 deletions src/paper/tests/test_process_openalex_works.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import json
from unittest.mock import patch

from django.contrib.contenttypes.models import ContentType
from django.test import override_settings
from rest_framework.test import APITestCase

from feed.models import FeedEntry
from hub.models import Hub
from paper.models import Paper
from paper.openalex_util import (
Expand Down Expand Up @@ -394,3 +397,34 @@ def test_get_or_create_journal_hub_witn_managed_journal_hub(self):

# Assert
self.assertEqual(journal_hub.name, managed_journal_hub.name)

@override_settings(CELERY_TASK_ALWAYS_EAGER=True, CELERY_TASK_EAGER_PROPAGATES=True)
@patch.object(OpenAlex, "get_authors")
def test_add_paper_to_feed(self, mock_get_authors):
with open("./paper/tests/openalex_authors.json", "r") as file:
mock_data = json.load(file)
mock_get_authors.return_value = (mock_data["results"], None)

process_openalex_works(self.works)

dois = [work.get("doi") for work in self.works]
dois = [doi.replace("https://doi.org/", "") for doi in dois]

created_papers = Paper.objects.filter(doi__in=dois).order_by("doi")
self.assertEqual(len(created_papers), 2)

for paper in created_papers:
content_type = ContentType.objects.get_for_model(Paper)
feed_entries = FeedEntry.objects.filter(
content_type=content_type, object_id=paper.id
)
self.assertEqual(len(feed_entries), paper.hubs.count())
self.assertEqual(feed_entries.first().action, "PUBLISH")
self.assertEqual(feed_entries.first().item, paper)

first_hub = paper.hubs.first()
paper.hubs.remove(first_hub)
feed_entries = FeedEntry.objects.filter(
content_type=content_type, object_id=paper.id
)
self.assertEqual(len(feed_entries), paper.hubs.count())
Loading