Celery integration

This commit is contained in:
Sonny 2019-07-14 18:44:15 +02:00
parent 1b774a7208
commit a74ffae9a7
64 changed files with 829 additions and 221 deletions

View file

@ -10,6 +10,7 @@ python tests:
stage: test
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
DJANGO_SETTINGS_MODULE: "newsreader.conf.gitlab"
cache:
key: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG"
paths:
@ -20,6 +21,6 @@ python tests:
- source env/bin/activate
- pip install -r requirements/gitlab.txt
script:
- python src/manage.py test newsreader --settings=newsreader.conf.gitlab
- python src/manage.py test newsreader
- isort -rc src/ --check-only
- black -l 100 --check src/
- black -l 90 --check src/

View file

@ -1,6 +1,6 @@
[settings]
include_trailing_comma = true
line_length = 100
line_length = 90
multi_line_output = 3
skip = env/, venv/
default_section = THIRDPARTY

View file

@ -1,8 +1,10 @@
bleach==3.1.0
beautifulsoup4==4.7.1
celery==4.3.0
certifi==2019.3.9
chardet==3.0.4
Django==2.2
django-celery-beat==1.5.0
djangorestframework==3.9.4
lxml==4.3.4
feedparser==5.2.1

View file

@ -0,0 +1,4 @@
from .celery import app as celery_app
__all__ = ["celery_app"]

View file

@ -0,0 +1,5 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
name = "accounts"

View file

@ -0,0 +1,151 @@
# Generated by Django 2.2 on 2019-07-14 10:36
import django.contrib.auth.models
import django.contrib.auth.validators
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("auth", "0011_update_proxy_permissions"),
("django_celery_beat", "0011_auto_20190508_0153"),
]
operations = [
migrations.CreateModel(
name="User",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("password", models.CharField(max_length=128, verbose_name="password")),
(
"last_login",
models.DateTimeField(
blank=True, null=True, verbose_name="last login"
),
),
(
"is_superuser",
models.BooleanField(
default=False,
help_text="Designates that this user has all permissions without explicitly assigning them.",
verbose_name="superuser status",
),
),
(
"username",
models.CharField(
error_messages={
"unique": "A user with that username already exists."
},
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
max_length=150,
unique=True,
validators=[
django.contrib.auth.validators.UnicodeUsernameValidator()
],
verbose_name="username",
),
),
(
"first_name",
models.CharField(
blank=True, max_length=30, verbose_name="first name"
),
),
(
"last_name",
models.CharField(
blank=True, max_length=150, verbose_name="last name"
),
),
(
"is_staff",
models.BooleanField(
default=False,
help_text="Designates whether the user can log into this admin site.",
verbose_name="staff status",
),
),
(
"is_active",
models.BooleanField(
default=True,
help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
verbose_name="active",
),
),
(
"date_joined",
models.DateTimeField(
default=django.utils.timezone.now, verbose_name="date joined"
),
),
(
"email",
models.EmailField(
max_length=254, unique=True, verbose_name="email address"
),
),
(
"groups",
models.ManyToManyField(
blank=True,
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
related_name="user_set",
related_query_name="user",
to="auth.Group",
verbose_name="groups",
),
),
(
"task",
models.ForeignKey(
blank=True,
editable=False,
null=True,
on_delete="collection task",
to="django_celery_beat.PeriodicTask",
),
),
(
"task_interval",
models.ForeignKey(
blank=True,
null=True,
on_delete="collection schedule",
to="django_celery_beat.IntervalSchedule",
),
),
(
"user_permissions",
models.ManyToManyField(
blank=True,
help_text="Specific permissions for this user.",
related_name="user_set",
related_query_name="user",
to="auth.Permission",
verbose_name="user permissions",
),
),
],
options={
"verbose_name": "user",
"verbose_name_plural": "users",
"abstract": False,
},
managers=[("objects", django.contrib.auth.models.UserManager())],
)
]

View file

@ -0,0 +1,10 @@
# Generated by Django 2.2 on 2019-07-14 10:37
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [("accounts", "0001_initial")]
operations = [migrations.RemoveField(model_name="user", name="username")]

View file

@ -0,0 +1,16 @@
# Generated by Django 2.2 on 2019-07-14 14:17
from django.db import migrations
import newsreader.accounts.models
class Migration(migrations.Migration):
dependencies = [("accounts", "0002_remove_user_username")]
operations = [
migrations.AlterModelManagers(
name="user", managers=[("objects", newsreader.accounts.models.UserManager())]
)
]

View file

@ -0,0 +1,22 @@
# Generated by Django 2.2 on 2019-07-14 15:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("accounts", "0003_auto_20190714_1417")]
operations = [
migrations.AlterField(
model_name="user",
name="task",
field=models.OneToOneField(
blank=True,
editable=False,
null=True,
on_delete="collection task",
to="django_celery_beat.PeriodicTask",
),
)
]

View file

@ -0,0 +1,84 @@
import json
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.models import UserManager as DjangoUserManager
from django.db import models
from django.utils.translation import gettext as _
from django_celery_beat.models import IntervalSchedule, PeriodicTask
class UserManager(DjangoUserManager):
def _create_user(self, email, password, **extra_fields):
"""
Create and save a user with the given username, email, and password.
"""
if not email:
raise ValueError("The given email must be set")
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_user(self, email, password=None, **extra_fields):
extra_fields.setdefault("is_staff", False)
extra_fields.setdefault("is_superuser", False)
return self._create_user(email, password, **extra_fields)
def create_superuser(self, email, password, **extra_fields):
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
if extra_fields.get("is_staff") is not True:
raise ValueError("Superuser must have is_staff=True.")
if extra_fields.get("is_superuser") is not True:
raise ValueError("Superuser must have is_superuser=True.")
return self._create_user(email, password, **extra_fields)
class User(AbstractUser):
email = models.EmailField(_("email address"), unique=True)
task = models.OneToOneField(
PeriodicTask, _("collection task"), null=True, blank=True, editable=False
)
task_interval = models.ForeignKey(
IntervalSchedule, _("collection schedule"), null=True, blank=True
)
username = None
objects = UserManager()
USERNAME_FIELD = "email"
REQUIRED_FIELDS = []
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._original_interval = self.task_interval
def save(self, *args, **kwargs):
if self._original_interval != self.task_interval:
if self.task_interval and self.task:
self.task.interval = self.task_interval
self.task.enabled = True
self.task.save()
elif self.task_interval and not self.task:
self.task = PeriodicTask.objects.create(
enabled=True,
interval=self.task_interval,
name=f"{self.email}-collection-task",
task="newsreader.news.collection.tasks",
args=json.dumps([self.pk]),
kwargs=None,
)
elif not self.task_interval and self.task:
self.task.enabled = False
self.task.save()
super().save(*args, **kwargs)

View file

@ -1,11 +1,10 @@
from django.contrib.auth.models import User
import factory
from newsreader.accounts.models import User
class UserFactory(factory.django.DjangoModelFactory):
username = factory.Sequence(lambda n: f"user-{n}")
email = factory.LazyAttribute(lambda o: f"{o.username}@example.org")
email = factory.Faker("email")
password = factory.Faker("password")
is_staff = False

View file

@ -1,5 +0,0 @@
from django.apps import AppConfig
class AuthConfig(AppConfig):
name = "auth"

View file

@ -1,15 +0,0 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
class EmailBackend(ModelBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
user_model_class = get_user_model()
try:
user = user_model_class.objects.get(email=username)
except user_model_class.DoesNotExist:
return
if user.check_password(password) and self.user_can_authenticate(user):
return user

12
src/newsreader/celery.py Normal file
View file

@ -0,0 +1,12 @@
import os
from celery import Celery
# note: this should be consistent with the setting from manage.py
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "newsreader.conf.dev")
# note: use the --workdir flag when running from different directories
app = Celery("newsreader", broker="amqp://")
app.autodiscover_tasks()

View file

@ -38,7 +38,10 @@ INSTALLED_APPS = [
"django.contrib.staticfiles",
# third party apps
"rest_framework",
"celery",
"django_celery_beat",
# app modules
"newsreader.accounts",
"newsreader.news.core",
"newsreader.news.collection",
]
@ -92,8 +95,8 @@ AUTH_PASSWORD_VALIDATORS = [
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
# Authentication
AUTHENTICATION_BACKENDS = ["newsreader.auth.backends.EmailBackend"]
# Authentication user model
AUTH_USER_MODEL = "accounts.User"
# Internationalization
# https://docs.djangoproject.com/en/2.2/topics/i18n/
@ -110,10 +113,12 @@ STATIC_URL = "/static/"
# Third party settings
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": ("rest_framework.authentication.SessionAuthentication",),
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework.authentication.SessionAuthentication",
),
"DEFAULT_PERMISSION_CLASSES": (
"rest_framework.permissions.IsAuthenticated",
"newsreader.auth.permissions.IsOwner",
"newsreader.accounts.permissions.IsOwner",
),
"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
}

View file

@ -8,5 +8,10 @@ class CollectionRuleAdmin(admin.ModelAdmin):
list_display = ("name", "category", "url", "last_suceeded", "succeeded")
def save_model(self, request, obj, form, change):
if not change:
obj.user = request.user
obj.save()
admin.site.register(CollectionRule, CollectionRuleAdmin)

View file

@ -1,4 +1,6 @@
from typing import ContextManager, Dict, List, Optional, Tuple
from typing import ContextManager, Dict, Optional, Tuple
from django.db.models.query import QuerySet
from bs4 import BeautifulSoup
@ -67,11 +69,13 @@ class Collector:
client = None
builder = None
def __init__(self, client: Optional[Client] = None, builder: Optional[Builder] = None) -> None:
def __init__(
self, client: Optional[Client] = None, builder: Optional[Builder] = None
) -> None:
self.client = client if client else self.client
self.builder = builder if builder else self.builder
def collect(self, rules: Optional[List] = None) -> None:
def collect(self, rules: Optional[QuerySet] = None) -> None:
with self.client(rules=rules) as client:
for data, stream in client:
with self.builder((data, stream)) as builder:

View file

@ -78,7 +78,9 @@ class FaviconClient(Client):
def __enter__(self) -> ContextManager:
with ThreadPoolExecutor(max_workers=10) as executor:
futures = {executor.submit(stream.read): rule for rule, stream in self.streams}
futures = {
executor.submit(stream.read): rule for rule, stream in self.streams
}
for future in as_completed(futures):
rule = futures[future]

View file

@ -156,7 +156,7 @@ class FeedCollector(Collector):
class FeedDuplicateHandler:
def __init__(self, rule: CollectionRule) -> None:
self.queryset = rule.post_set.all()
self.queryset = rule.posts.all()
def __enter__(self) -> ContextManager:
self.existing_identifiers = self.queryset.filter(
@ -202,7 +202,9 @@ class FeedDuplicateHandler:
def handle_duplicate(self, instance: Post) -> Optional[Post]:
try:
existing_instance = self.queryset.get(remote_identifier=instance.remote_identifier)
existing_instance = self.queryset.get(
remote_identifier=instance.remote_identifier
)
except ObjectDoesNotExist:
return

View file

@ -1,14 +1,11 @@
from django.core.management.base import BaseCommand
from newsreader.news.collection.feed import FeedCollector
from newsreader.news.collection.models import CollectionRule
class Command(BaseCommand):
help = "Collects Atom/RSS feeds"
def handle(self, *args, **options):
CollectionRule.objects.all()
collector = FeedCollector()
collector.collect()

View file

@ -1,4 +1,4 @@
# Generated by Django 2.2 on 2019-07-05 20:59
# Generated by Django 2.2 on 2019-07-14 10:36
import django.utils.timezone
@ -18,7 +18,10 @@ class Migration(migrations.Migration):
(
"id",
models.AutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created", models.DateTimeField(default=django.utils.timezone.now)),
@ -27,7 +30,9 @@ class Migration(migrations.Migration):
("url", models.URLField(max_length=1024)),
(
"website_url",
models.URLField(blank=True, editable=False, max_length=1024, null=True),
models.URLField(
blank=True, editable=False, max_length=1024, null=True
),
),
("favicon", models.URLField(blank=True, null=True)),
(
@ -93,8 +98,14 @@ class Migration(migrations.Migration):
("America/Anguilla", "America/Anguilla"),
("America/Antigua", "America/Antigua"),
("America/Araguaina", "America/Araguaina"),
("America/Argentina/Buenos_Aires", "America/Argentina/Buenos_Aires"),
("America/Argentina/Catamarca", "America/Argentina/Catamarca"),
(
"America/Argentina/Buenos_Aires",
"America/Argentina/Buenos_Aires",
),
(
"America/Argentina/Catamarca",
"America/Argentina/Catamarca",
),
(
"America/Argentina/ComodRivadavia",
"America/Argentina/ComodRivadavia",
@ -103,7 +114,10 @@ class Migration(migrations.Migration):
("America/Argentina/Jujuy", "America/Argentina/Jujuy"),
("America/Argentina/La_Rioja", "America/Argentina/La_Rioja"),
("America/Argentina/Mendoza", "America/Argentina/Mendoza"),
("America/Argentina/Rio_Gallegos", "America/Argentina/Rio_Gallegos"),
(
"America/Argentina/Rio_Gallegos",
"America/Argentina/Rio_Gallegos",
),
("America/Argentina/Salta", "America/Argentina/Salta"),
("America/Argentina/San_Juan", "America/Argentina/San_Juan"),
("America/Argentina/San_Luis", "America/Argentina/San_Luis"),
@ -163,7 +177,10 @@ class Migration(migrations.Migration):
("America/Halifax", "America/Halifax"),
("America/Havana", "America/Havana"),
("America/Hermosillo", "America/Hermosillo"),
("America/Indiana/Indianapolis", "America/Indiana/Indianapolis"),
(
"America/Indiana/Indianapolis",
"America/Indiana/Indianapolis",
),
("America/Indiana/Knox", "America/Indiana/Knox"),
("America/Indiana/Marengo", "America/Indiana/Marengo"),
("America/Indiana/Petersburg", "America/Indiana/Petersburg"),
@ -177,8 +194,14 @@ class Migration(migrations.Migration):
("America/Jamaica", "America/Jamaica"),
("America/Jujuy", "America/Jujuy"),
("America/Juneau", "America/Juneau"),
("America/Kentucky/Louisville", "America/Kentucky/Louisville"),
("America/Kentucky/Monticello", "America/Kentucky/Monticello"),
(
"America/Kentucky/Louisville",
"America/Kentucky/Louisville",
),
(
"America/Kentucky/Monticello",
"America/Kentucky/Monticello",
),
("America/Knox_IN", "America/Knox_IN"),
("America/Kralendijk", "America/Kralendijk"),
("America/La_Paz", "America/La_Paz"),
@ -209,9 +232,18 @@ class Migration(migrations.Migration):
("America/Nipigon", "America/Nipigon"),
("America/Nome", "America/Nome"),
("America/Noronha", "America/Noronha"),
("America/North_Dakota/Beulah", "America/North_Dakota/Beulah"),
("America/North_Dakota/Center", "America/North_Dakota/Center"),
("America/North_Dakota/New_Salem", "America/North_Dakota/New_Salem"),
(
"America/North_Dakota/Beulah",
"America/North_Dakota/Beulah",
),
(
"America/North_Dakota/Center",
"America/North_Dakota/Center",
),
(
"America/North_Dakota/New_Salem",
"America/North_Dakota/New_Salem",
),
("America/Ojinaga", "America/Ojinaga"),
("America/Panama", "America/Panama"),
("America/Pangnirtung", "America/Pangnirtung"),

View file

@ -0,0 +1,37 @@
# Generated by Django 2.2 on 2019-07-14 10:36
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("collection", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("core", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="collectionrule",
name="category",
field=models.ForeignKey(
blank=True,
help_text="Posts from this rule will be tagged with this category",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="core.Category",
verbose_name="Category",
),
),
migrations.AddField(
model_name="collectionrule",
name="user",
field=models.ForeignKey(on_delete="Owner", to=settings.AUTH_USER_MODEL),
),
]

View file

@ -0,0 +1,19 @@
# Generated by Django 2.2 on 2019-07-14 14:17
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("collection", "0002_auto_20190714_1036")]
operations = [
migrations.AlterField(
model_name="collectionrule",
name="user",
field=models.ForeignKey(
on_delete="Owner", related_name="rules", to=settings.AUTH_USER_MODEL
),
)
]

View file

@ -1,21 +0,0 @@
# Generated by Django 2.2 on 2019-07-07 17:08
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("collection", "0002_collectionrule_category"),
]
operations = [
migrations.AddField(
model_name="collectionrule",
name="user",
field=models.ForeignKey(default=None, on_delete="Owner", to=settings.AUTH_USER_MODEL),
preserve_default=False,
)
]

View file

@ -1,4 +1,4 @@
# Generated by Django 2.2 on 2019-07-05 20:59
# Generated by Django 2.2 on 2019-07-14 14:22
import django.db.models.deletion
@ -7,12 +7,10 @@ from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [("collection", "0001_initial"), ("core", "0001_initial")]
dependencies = [("collection", "0003_auto_20190714_1417")]
operations = [
migrations.AddField(
migrations.AlterField(
model_name="collectionrule",
name="category",
field=models.ForeignKey(
@ -20,6 +18,7 @@ class Migration(migrations.Migration):
help_text="Posts from this rule will be tagged with this category",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="rules",
to="core.Category",
verbose_name="Category",
),

View file

@ -24,6 +24,7 @@ class CollectionRule(TimeStampedModel):
blank=True,
null=True,
verbose_name=_("Category"),
related_name="rules",
help_text=_("Posts from this rule will be tagged with this category"),
on_delete=models.SET_NULL,
)
@ -32,7 +33,7 @@ class CollectionRule(TimeStampedModel):
succeeded = models.BooleanField(default=False)
error = models.CharField(max_length=255, blank=True, null=True)
user = models.ForeignKey("auth.User", _("Owner"))
user = models.ForeignKey("accounts.User", _("Owner"), related_name="rules")
def __str__(self):
return self.name

View file

@ -10,9 +10,11 @@ class CollectionRuleSerializer(serializers.HyperlinkedModelSerializer):
def get_posts(self, instance):
request = self.context.get("request")
posts = instance.post_set.order_by("-publication_date")
posts = instance.posts.order_by("-publication_date")
serializer = core.serializers.PostSerializer(posts, context={"request": request}, many=True)
serializer = core.serializers.PostSerializer(
posts, context={"request": request}, many=True
)
return serializer.data
class Meta:

View file

@ -0,0 +1,19 @@
from django.core.exceptions import ObjectDoesNotExist
from newsreader.accounts.models import User
from newsreader.celery import app
from newsreader.news.collection.feed import FeedCollector
@app.task
def collect(user_pk):
try:
user = User.objects.get(pk=user_pk)
except ObjectDoesNotExist:
# TODO remove this task
return
rules = user.rules.all()
collector = FeedCollector()
collector.collect(rules=rules)

View file

@ -5,7 +5,7 @@ from urllib.parse import urljoin
from django.test import Client, TestCase
from django.urls import reverse
from newsreader.auth.tests.factories import UserFactory
from newsreader.accounts.tests.factories import UserFactory
from newsreader.news.collection.tests.factories import CollectionRuleFactory
from newsreader.news.core.tests.factories import CategoryFactory
@ -130,14 +130,18 @@ class CollectionRuleDetailViewTestCase(TestCase):
self.client.force_login(self.user)
response = self.client.patch(
reverse("api:rules-detail", args=[rule.pk]),
data=json.dumps({"category": reverse("api:categories-detail", args=[category.pk])}),
data=json.dumps(
{"category": reverse("api:categories-detail", args=[category.pk])}
),
content_type="application/json",
)
data = response.json()
url = data["category"]
self.assertEquals(response.status_code, 200)
self.assertTrue(url.endswith(reverse("api:categories-detail", args=[category.pk])))
self.assertTrue(
url.endswith(reverse("api:categories-detail", args=[category.pk]))
)
def test_put(self):
rule = CollectionRuleFactory(name="BBC", user=self.user)

View file

@ -7,7 +7,7 @@ from django.urls import reverse
import pytz
from newsreader.auth.tests.factories import UserFactory
from newsreader.accounts.tests.factories import UserFactory
from newsreader.news.collection.tests.factories import CollectionRuleFactory
from newsreader.news.core.tests.factories import CategoryFactory, PostFactory
@ -100,7 +100,9 @@ class CollectionRuleListViewTestCase(TestCase):
self.client.force_login(self.user)
response = self.client.post(
reverse("api:rules-list"), data=json.dumps(data), content_type="application/json"
reverse("api:rules-list"),
data=json.dumps(data),
content_type="application/json",
)
data = response.json()
category_url = data["category"]
@ -110,7 +112,9 @@ class CollectionRuleListViewTestCase(TestCase):
self.assertEquals(data["name"], "BBC")
self.assertEquals(data["url"], "https://www.bbc.co.uk")
self.assertTrue(category_url.endswith(reverse("api:categories-detail", args=[category.pk])))
self.assertTrue(
category_url.endswith(reverse("api:categories-detail", args=[category.pk]))
)
def test_patch(self):
self.client.force_login(self.user)

View file

@ -1,6 +1,6 @@
import factory
from newsreader.auth.tests.factories import UserFactory
from newsreader.accounts.tests.factories import UserFactory
from newsreader.news.collection.models import CollectionRule

View file

@ -18,7 +18,9 @@ class FaviconBuilderTestCase(TestCase):
self.assertEquals(rule.favicon, "https://www.bbc.com/favicon.ico")
def test_without_url(self):
rule = CollectionRuleFactory(website_url="https://www.theguardian.com/", favicon=None)
rule = CollectionRuleFactory(
website_url="https://www.theguardian.com/", favicon=None
)
with FaviconBuilder((rule, mock_without_url)) as builder:
builder.build()
@ -39,7 +41,9 @@ class FaviconBuilderTestCase(TestCase):
with FaviconBuilder((rule, mock_with_weird_path)) as builder:
builder.build()
self.assertEquals(rule.favicon, "https://www.theguardian.com/jabadaba/doe/favicon.ico")
self.assertEquals(
rule.favicon, "https://www.theguardian.com/jabadaba/doe/favicon.ico"
)
def test_other_url(self):
rule = CollectionRuleFactory(favicon=None)

View file

@ -137,7 +137,13 @@ feed_mock = {
"link": "https://www.bbc.co.uk/news/",
},
"link": "https://www.bbc.co.uk/news/",
"links": [{"href": "https://www.bbc.co.uk/news/", "rel": "alternate", "type": "text/html"}],
"links": [
{
"href": "https://www.bbc.co.uk/news/",
"rel": "alternate",
"type": "text/html",
}
],
"title": "BBC News - Home",
},
"href": "http://feeds.bbci.co.uk/news/rss.xml",

View file

@ -22,10 +22,14 @@ class FaviconCollectorTestCase(TestCase):
def setUp(self):
self.maxDiff = None
self.patched_feed_client = patch("newsreader.news.collection.favicon.FeedClient.__enter__")
self.patched_feed_client = patch(
"newsreader.news.collection.favicon.FeedClient.__enter__"
)
self.mocked_feed_client = self.patched_feed_client.start()
self.patched_website_read = patch("newsreader.news.collection.favicon.WebsiteStream.read")
self.patched_website_read = patch(
"newsreader.news.collection.favicon.WebsiteStream.read"
)
self.mocked_website_read = self.patched_website_read.start()
def tearDown(self):

View file

@ -57,7 +57,13 @@ simple_mock = {
"language": "en-gb",
"link": "https://www.bbc.co.uk/news/",
},
"links": [{"href": "https://www.bbc.co.uk/news/", "rel": "alternate", "type": "text/html"}],
"links": [
{
"href": "https://www.bbc.co.uk/news/",
"rel": "alternate",
"type": "text/html",
}
],
"title": "BBC News - Home",
},
"href": "http://feeds.bbci.co.uk/news/rss.xml",
@ -201,7 +207,13 @@ multiple_mock = {
"language": "en-gb",
"link": "https://www.bbc.co.uk/news/",
},
"links": [{"href": "https://www.bbc.co.uk/news/", "rel": "alternate", "type": "text/html"}],
"links": [
{
"href": "https://www.bbc.co.uk/news/",
"rel": "alternate",
"type": "text/html",
}
],
"title": "BBC News - Home",
},
"href": "http://feeds.bbci.co.uk/news/rss.xml",
@ -302,7 +314,13 @@ mock_without_identifier = {
"language": "en-gb",
"link": "https://www.bbc.co.uk/news/",
},
"links": [{"href": "https://www.bbc.co.uk/news/", "rel": "alternate", "type": "text/html"}],
"links": [
{
"href": "https://www.bbc.co.uk/news/",
"rel": "alternate",
"type": "text/html",
}
],
"title": "BBC News - Home",
},
"href": "http://feeds.bbci.co.uk/news/rss.xml",
@ -402,7 +420,13 @@ mock_without_publish_date = {
"language": "en-gb",
"link": "https://www.bbc.co.uk/news/",
},
"links": [{"href": "https://www.bbc.co.uk/news/", "rel": "alternate", "type": "text/html"}],
"links": [
{
"href": "https://www.bbc.co.uk/news/",
"rel": "alternate",
"type": "text/html",
}
],
"title": "BBC News - Home",
},
"href": "http://feeds.bbci.co.uk/news/rss.xml",
@ -492,7 +516,13 @@ mock_without_url = {
"language": "en-gb",
"link": "https://www.bbc.co.uk/news/",
},
"links": [{"href": "https://www.bbc.co.uk/news/", "rel": "alternate", "type": "text/html"}],
"links": [
{
"href": "https://www.bbc.co.uk/news/",
"rel": "alternate",
"type": "text/html",
}
],
"title": "BBC News - Home",
},
"href": "http://feeds.bbci.co.uk/news/rss.xml",
@ -575,7 +605,13 @@ mock_without_body = {
"language": "en-gb",
"link": "https://www.bbc.co.uk/news/",
},
"links": [{"href": "https://www.bbc.co.uk/news/", "rel": "alternate", "type": "text/html"}],
"links": [
{
"href": "https://www.bbc.co.uk/news/",
"rel": "alternate",
"type": "text/html",
}
],
"title": "BBC News - Home",
},
"href": "http://feeds.bbci.co.uk/news/rss.xml",
@ -676,7 +712,13 @@ mock_without_author = {
"language": "en-gb",
"link": "https://www.bbc.co.uk/news/",
},
"links": [{"href": "https://www.bbc.co.uk/news/", "rel": "alternate", "type": "text/html"}],
"links": [
{
"href": "https://www.bbc.co.uk/news/",
"rel": "alternate",
"type": "text/html",
}
],
"title": "BBC News - Home",
},
"href": "http://feeds.bbci.co.uk/news/rss.xml",
@ -822,7 +864,13 @@ mock_with_update_entries = {
"language": "en-gb",
"link": "https://www.bbc.co.uk/news/",
},
"links": [{"href": "https://www.bbc.co.uk/news/", "rel": "alternate", "type": "text/html"}],
"links": [
{
"href": "https://www.bbc.co.uk/news/",
"rel": "alternate",
"type": "text/html",
}
],
"title": "BBC News - Home",
},
"href": "http://feeds.bbci.co.uk/news/rss.xml",
@ -883,7 +931,13 @@ mock_with_html = {
"language": "en-gb",
"link": "https://www.bbc.co.uk/news/",
},
"links": [{"href": "https://www.bbc.co.uk/news/", "rel": "alternate", "type": "text/html"}],
"links": [
{
"href": "https://www.bbc.co.uk/news/",
"rel": "alternate",
"type": "text/html",
}
],
"title": "BBC News - Home",
},
"href": "http://feeds.bbci.co.uk/news/rss.xml",
@ -945,7 +999,13 @@ mock_with_long_author = {
"language": "en-gb",
"link": "https://www.bbc.co.uk/news/",
},
"links": [{"href": "https://www.bbc.co.uk/news/", "rel": "alternate", "type": "text/html"}],
"links": [
{
"href": "https://www.bbc.co.uk/news/",
"rel": "alternate",
"type": "text/html",
}
],
"title": "BBC News - Home",
},
"href": "http://feeds.bbci.co.uk/news/rss.xml",
@ -1012,7 +1072,13 @@ mock_with_long_title = {
"language": "en-gb",
"link": "https://www.bbc.co.uk/news/",
},
"links": [{"href": "https://www.bbc.co.uk/news/", "rel": "alternate", "type": "text/html"}],
"links": [
{
"href": "https://www.bbc.co.uk/news/",
"rel": "alternate",
"type": "text/html",
}
],
"title": "BBC News - Home",
},
"href": "http://feeds.bbci.co.uk/news/rss.xml",

View file

@ -42,7 +42,9 @@ class FeedBuilderTestCase(TestCase):
self.assertEquals(post.url, "https://www.bbc.co.uk/news/world-us-canada-48338168")
self.assertEquals(post.title, "Trump's 'genocidal taunts' will not end Iran - Zarif")
self.assertEquals(
post.title, "Trump's 'genocidal taunts' will not end Iran - Zarif"
)
def test_multiple_entries(self):
builder = FeedBuilder
@ -64,12 +66,17 @@ class FeedBuilderTestCase(TestCase):
self.assertEquals(first_post.publication_date, aware_date)
self.assertEquals(
first_post.remote_identifier, "https://www.bbc.co.uk/news/world-us-canada-48338168"
first_post.remote_identifier,
"https://www.bbc.co.uk/news/world-us-canada-48338168",
)
self.assertEquals(first_post.url, "https://www.bbc.co.uk/news/world-us-canada-48338168")
self.assertEquals(
first_post.url, "https://www.bbc.co.uk/news/world-us-canada-48338168"
)
self.assertEquals(first_post.title, "Trump's 'genocidal taunts' will not end Iran - Zarif")
self.assertEquals(
first_post.title, "Trump's 'genocidal taunts' will not end Iran - Zarif"
)
d = datetime.combine(date(2019, 5, 20), time(hour=12, minute=19, second=19))
aware_date = pytz.utc.localize(d)
@ -77,10 +84,13 @@ class FeedBuilderTestCase(TestCase):
self.assertEquals(second_post.publication_date, aware_date)
self.assertEquals(
second_post.remote_identifier, "https://www.bbc.co.uk/news/technology-48334739"
second_post.remote_identifier,
"https://www.bbc.co.uk/news/technology-48334739",
)
self.assertEquals(second_post.url, "https://www.bbc.co.uk/news/technology-48334739")
self.assertEquals(
second_post.url, "https://www.bbc.co.uk/news/technology-48334739"
)
self.assertEquals(second_post.title, "Huawei's Android loss: How it affects you")
@ -104,9 +114,13 @@ class FeedBuilderTestCase(TestCase):
self.assertEquals(first_post.remote_identifier, None)
self.assertEquals(first_post.url, "https://www.bbc.co.uk/news/world-us-canada-48338168")
self.assertEquals(
first_post.url, "https://www.bbc.co.uk/news/world-us-canada-48338168"
)
self.assertEquals(first_post.title, "Trump's 'genocidal taunts' will not end Iran - Zarif")
self.assertEquals(
first_post.title, "Trump's 'genocidal taunts' will not end Iran - Zarif"
)
@freeze_time("2019-10-30 12:30:00")
def test_entry_without_publication_date(self):
@ -125,12 +139,14 @@ class FeedBuilderTestCase(TestCase):
self.assertEquals(first_post.created, timezone.now())
self.assertEquals(
first_post.remote_identifier, "https://www.bbc.co.uk/news/world-us-canada-48338168"
first_post.remote_identifier,
"https://www.bbc.co.uk/news/world-us-canada-48338168",
)
self.assertEquals(second_post.created, timezone.now())
self.assertEquals(
second_post.remote_identifier, "https://www.bbc.co.uk/news/technology-48334739"
second_post.remote_identifier,
"https://www.bbc.co.uk/news/technology-48334739",
)
@freeze_time("2019-10-30 12:30:00")
@ -150,12 +166,14 @@ class FeedBuilderTestCase(TestCase):
self.assertEquals(first_post.created, timezone.now())
self.assertEquals(
first_post.remote_identifier, "https://www.bbc.co.uk/news/world-us-canada-48338168"
first_post.remote_identifier,
"https://www.bbc.co.uk/news/world-us-canada-48338168",
)
self.assertEquals(second_post.created, timezone.now())
self.assertEquals(
second_post.remote_identifier, "https://www.bbc.co.uk/news/technology-48334739"
second_post.remote_identifier,
"https://www.bbc.co.uk/news/technology-48334739",
)
@freeze_time("2019-10-30 12:30:00")
@ -175,7 +193,8 @@ class FeedBuilderTestCase(TestCase):
self.assertEquals(first_post.created, timezone.now())
self.assertEquals(
first_post.remote_identifier, "https://www.bbc.co.uk/news/world-us-canada-48338168"
first_post.remote_identifier,
"https://www.bbc.co.uk/news/world-us-canada-48338168",
)
self.assertEquals(second_post.created, timezone.now())
@ -201,12 +220,14 @@ class FeedBuilderTestCase(TestCase):
self.assertEquals(first_post.created, timezone.now())
self.assertEquals(
first_post.remote_identifier, "https://www.bbc.co.uk/news/world-us-canada-48338168"
first_post.remote_identifier,
"https://www.bbc.co.uk/news/world-us-canada-48338168",
)
self.assertEquals(second_post.created, timezone.now())
self.assertEquals(
second_post.remote_identifier, "https://www.bbc.co.uk/news/technology-48334739"
second_post.remote_identifier,
"https://www.bbc.co.uk/news/technology-48334739",
)
def test_empty_entries(self):
@ -241,10 +262,13 @@ class FeedBuilderTestCase(TestCase):
existing_second_post.refresh_from_db()
self.assertEquals(
existing_first_post.title, "Trump's 'genocidal taunts' will not end Iran - Zarif"
existing_first_post.title,
"Trump's 'genocidal taunts' will not end Iran - Zarif",
)
self.assertEquals(existing_second_post.title, "Huawei's Android loss: How it affects you")
self.assertEquals(
existing_second_post.title, "Huawei's Android loss: How it affects you"
)
def test_html_sanitizing(self):
builder = FeedBuilder

View file

@ -54,7 +54,13 @@ simple_mock = {
"language": "en-gb",
"link": "https://www.bbc.co.uk/news/",
},
"links": [{"href": "https://www.bbc.co.uk/news/", "rel": "alternate", "type": "text/html"}],
"links": [
{
"href": "https://www.bbc.co.uk/news/",
"rel": "alternate",
"type": "text/html",
}
],
"title": "BBC News - Home",
},
"href": "http://feeds.bbci.co.uk/news/rss.xml",

View file

@ -134,7 +134,13 @@ multiple_mock = {
"language": "en-gb",
"link": "https://www.bbc.co.uk/news/",
},
"links": [{"href": "https://www.bbc.co.uk/news/", "rel": "alternate", "type": "text/html"}],
"links": [
{
"href": "https://www.bbc.co.uk/news/",
"rel": "alternate",
"type": "text/html",
}
],
"title": "BBC News - Home",
},
"href": "http://feeds.bbci.co.uk/news/rss.xml",
@ -154,7 +160,13 @@ empty_mock = {
"language": "en-gb",
"link": "https://www.bbc.co.uk/news/",
},
"links": [{"href": "https://www.bbc.co.uk/news/", "rel": "alternate", "type": "text/html"}],
"links": [
{
"href": "https://www.bbc.co.uk/news/",
"rel": "alternate",
"type": "text/html",
}
],
"title": "BBC News - Home",
},
"href": "http://feeds.bbci.co.uk/news/rss.xml",
@ -292,7 +304,13 @@ duplicate_mock = {
"language": "en-gb",
"link": "https://www.bbc.co.uk/news/",
},
"links": [{"href": "https://www.bbc.co.uk/news/", "rel": "alternate", "type": "text/html"}],
"links": [
{
"href": "https://www.bbc.co.uk/news/",
"rel": "alternate",
"type": "text/html",
}
],
"title": "BBC News - Home",
},
"href": "http://feeds.bbci.co.uk/news/rss.xml",
@ -433,7 +451,13 @@ multiple_update_mock = {
"language": "en-gb",
"link": "https://www.bbc.co.uk/news/",
},
"links": [{"href": "https://www.bbc.co.uk/news/", "rel": "alternate", "type": "text/html"}],
"links": [
{
"href": "https://www.bbc.co.uk/news/",
"rel": "alternate",
"type": "text/html",
}
],
"title": "BBC News - Home",
},
"href": "http://feeds.bbci.co.uk/news/rss.xml",

View file

@ -234,8 +234,12 @@ class FeedCollectorTestCase(TestCase):
self.assertEquals(rule.last_suceeded, timezone.now())
self.assertEquals(rule.error, None)
self.assertEquals(first_post.title, "Trump's 'genocidal taunts' will not end Iran - Zarif")
self.assertEquals(
first_post.title, "Trump's 'genocidal taunts' will not end Iran - Zarif"
)
self.assertEquals(second_post.title, "Huawei's Android loss: How it affects you")
self.assertEquals(third_post.title, "Birmingham head teacher threatened over LGBT lessons")
self.assertEquals(
third_post.title, "Birmingham head teacher threatened over LGBT lessons"
)

View file

@ -54,7 +54,13 @@ simple_mock = {
"language": "en-gb",
"link": "https://www.bbc.co.uk/news/",
},
"links": [{"href": "https://www.bbc.co.uk/news/", "rel": "alternate", "type": "text/html"}],
"links": [
{
"href": "https://www.bbc.co.uk/news/",
"rel": "alternate",
"type": "text/html",
}
],
"title": "BBC News - Home",
},
"href": "http://feeds.bbci.co.uk/news/rss.xml",

View file

@ -20,7 +20,13 @@ simple_feed_mock = {
"link": "https://www.bbc.co.uk/news/",
},
"link": "https://www.bbc.co.uk/news/",
"links": [{"href": "https://www.bbc.co.uk/news/", "rel": "alternate", "type": "text/html"}],
"links": [
{
"href": "https://www.bbc.co.uk/news/",
"rel": "alternate",
"type": "text/html",
}
],
"title": "BBC News - Home",
},
"href": "http://feeds.bbci.co.uk/news/rss.xml",

View file

@ -118,7 +118,9 @@ class URLBuilderTestCase(TestCase):
def test_no_link(self):
initial_rule = CollectionRuleFactory()
with URLBuilder((feed_mock_without_link, MagicMock(rule=initial_rule))) as builder:
with URLBuilder(
(feed_mock_without_link, MagicMock(rule=initial_rule))
) as builder:
rule, url = builder.build()
self.assertEquals(rule.pk, initial_rule.pk)

View file

@ -1,6 +1,9 @@
from django.urls import path
from newsreader.news.collection.views import CollectionRuleAPIListView, CollectionRuleDetailView
from newsreader.news.collection.views import (
CollectionRuleAPIListView,
CollectionRuleDetailView,
)
endpoints = [

View file

@ -1,8 +1,9 @@
# Generated by Django 2.2 on 2019-07-05 20:59
# Generated by Django 2.2 on 2019-07-14 10:36
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
@ -10,43 +11,36 @@ class Migration(migrations.Migration):
initial = True
dependencies = [("collection", "0001_initial")]
dependencies = [
("collection", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="Category",
fields=[
(
"id",
models.AutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("created", models.DateTimeField(default=django.utils.timezone.now)),
("modified", models.DateTimeField(auto_now=True)),
("name", models.CharField(max_length=50, unique=True)),
],
options={"verbose_name": "Category", "verbose_name_plural": "Categories"},
),
migrations.CreateModel(
name="Post",
fields=[
(
"id",
models.AutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created", models.DateTimeField(default=django.utils.timezone.now)),
("modified", models.DateTimeField(auto_now=True)),
("title", models.CharField(blank=True, max_length=200, null=True)),
("body", models.TextField(blank=True, null=True)),
("author", models.CharField(blank=True, max_length=200, null=True)),
("author", models.CharField(blank=True, max_length=40, null=True)),
("publication_date", models.DateTimeField(blank=True, null=True)),
("url", models.URLField(blank=True, max_length=1024, null=True)),
(
"remote_identifier",
models.CharField(blank=True, editable=False, max_length=500, null=True),
models.CharField(
blank=True, editable=False, max_length=500, null=True
),
),
(
"rule",
@ -59,4 +53,26 @@ class Migration(migrations.Migration):
],
options={"abstract": False},
),
migrations.CreateModel(
name="Category",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created", models.DateTimeField(default=django.utils.timezone.now)),
("modified", models.DateTimeField(auto_now=True)),
("name", models.CharField(max_length=50, unique=True)),
(
"user",
models.ForeignKey(on_delete="Owner", to=settings.AUTH_USER_MODEL),
),
],
options={"verbose_name": "Category", "verbose_name_plural": "Categories"},
),
]

View file

@ -0,0 +1,31 @@
# Generated by Django 2.2 on 2019-07-14 14:25
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("core", "0001_initial")]
operations = [
migrations.AlterField(
model_name="category",
name="user",
field=models.ForeignKey(
on_delete="Owner", related_name="categories", to=settings.AUTH_USER_MODEL
),
),
migrations.AlterField(
model_name="post",
name="rule",
field=models.ForeignKey(
editable=False,
on_delete=django.db.models.deletion.CASCADE,
related_name="posts",
to="collection.CollectionRule",
),
),
]

View file

@ -1,21 +0,0 @@
# Generated by Django 2.2 on 2019-07-07 17:08
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("core", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="category",
name="user",
field=models.ForeignKey(default=None, on_delete="Owner", to=settings.AUTH_USER_MODEL),
preserve_default=False,
)
]

View file

@ -1,16 +0,0 @@
# Generated by Django 2.2 on 2019-07-10 18:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("core", "0002_category_user")]
operations = [
migrations.AlterField(
model_name="post",
name="author",
field=models.CharField(blank=True, max_length=40, null=True),
)
]

View file

@ -12,8 +12,12 @@ class Post(TimeStampedModel):
publication_date = models.DateTimeField(blank=True, null=True)
url = models.URLField(max_length=1024, blank=True, null=True)
rule = models.ForeignKey(CollectionRule, on_delete=models.CASCADE, editable=False)
remote_identifier = models.CharField(max_length=500, blank=True, null=True, editable=False)
rule = models.ForeignKey(
CollectionRule, on_delete=models.CASCADE, editable=False, related_name="posts"
)
remote_identifier = models.CharField(
max_length=500, blank=True, null=True, editable=False
)
def __str__(self):
return "Post-{}".format(self.pk)
@ -21,7 +25,7 @@ class Post(TimeStampedModel):
class Category(TimeStampedModel):
name = models.CharField(max_length=50, unique=True)
user = models.ForeignKey("auth.User", _("Owner"))
user = models.ForeignKey("accounts.User", _("Owner"), related_name="categories")
class Meta:
verbose_name = _("Category")

View file

@ -7,7 +7,7 @@ class CategorySerializer(serializers.ModelSerializer):
rules = serializers.SerializerMethodField()
def get_rules(self, instance):
rules = instance.collectionrule_set.order_by("-modified", "-created")
rules = instance.rules.order_by("-modified", "-created")
serializer = CollectionRuleSerializer(rules, many=True)
return serializer.data

View file

@ -26,7 +26,7 @@ class CategorySerializer(serializers.HyperlinkedModelSerializer):
def get_rules(self, instance):
request = self.context.get("request")
rules = instance.collectionrule_set.order_by("-modified", "-created")
rules = instance.rules.order_by("-modified", "-created")
serializer = collection.serializers.CollectionRuleSerializer(
rules, context={"request": request}, many=True

View file

@ -3,7 +3,7 @@ import json
from django.test import Client, TestCase
from django.urls import reverse
from newsreader.auth.tests.factories import UserFactory
from newsreader.accounts.tests.factories import UserFactory
from newsreader.news.collection.tests.factories import CollectionRuleFactory
from newsreader.news.core.tests.factories import CategoryFactory, PostFactory
@ -91,13 +91,17 @@ class CategoryDetailViewTestCase(TestCase):
category = CategoryFactory(user=self.user)
self.client.force_login(self.user)
response = self.client.delete(reverse("api:categories-detail", args=[category.pk]))
response = self.client.delete(
reverse("api:categories-detail", args=[category.pk])
)
self.assertEquals(response.status_code, 204)
def test_rules(self):
category = CategoryFactory(user=self.user)
rules = CollectionRuleFactory.create_batch(size=5, category=category, user=self.user)
rules = CollectionRuleFactory.create_batch(
size=5, category=category, user=self.user
)
self.client.force_login(self.user)
response = self.client.get(reverse("api:categories-detail", args=[category.pk]))
@ -149,7 +153,9 @@ class CategoryDetailViewTestCase(TestCase):
if count < 1:
continue
self.assertTrue(post["publication_date"] < posts[count - 1]["publication_date"])
self.assertTrue(
post["publication_date"] < posts[count - 1]["publication_date"]
)
def test_category_with_unauthenticated_user(self):
category = CategoryFactory(user=self.user)

View file

@ -7,7 +7,7 @@ from django.urls import reverse
import pytz
from newsreader.auth.tests.factories import UserFactory
from newsreader.accounts.tests.factories import UserFactory
from newsreader.news.collection.tests.factories import CollectionRuleFactory
from newsreader.news.core.tests.factories import CategoryFactory, PostFactory
@ -83,7 +83,9 @@ class CategoryListViewTestCase(TestCase):
self.client.force_login(self.user)
response = self.client.post(
reverse("api:categories-list"), data=json.dumps(data), content_type="application/json"
reverse("api:categories-list"),
data=json.dumps(data),
content_type="application/json",
)
response_data = response.json()

View file

@ -3,7 +3,7 @@ import json
from django.test import Client, TestCase
from django.urls import reverse
from newsreader.auth.tests.factories import UserFactory
from newsreader.accounts.tests.factories import UserFactory
from newsreader.news.collection.tests.factories import CollectionRuleFactory
from newsreader.news.core.tests.factories import CategoryFactory, PostFactory
@ -17,7 +17,9 @@ class PostDetailViewTestCase(TestCase):
self.user = UserFactory(is_staff=True, password="test")
def test_simple(self):
rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=self.user))
rule = CollectionRuleFactory(
user=self.user, category=CategoryFactory(user=self.user)
)
post = PostFactory(rule=rule)
self.client.force_login(self.user)
@ -44,7 +46,9 @@ class PostDetailViewTestCase(TestCase):
self.assertEquals(data["detail"], "Not found.")
def test_post(self):
rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=self.user))
rule = CollectionRuleFactory(
user=self.user, category=CategoryFactory(user=self.user)
)
post = PostFactory(rule=rule)
self.client.force_login(self.user)
@ -55,7 +59,9 @@ class PostDetailViewTestCase(TestCase):
self.assertEquals(data["detail"], 'Method "POST" not allowed.')
def test_patch(self):
rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=self.user))
rule = CollectionRuleFactory(
user=self.user, category=CategoryFactory(user=self.user)
)
post = PostFactory(title="This is clickbait for sure", rule=rule)
self.client.force_login(self.user)
@ -70,7 +76,9 @@ class PostDetailViewTestCase(TestCase):
self.assertEquals(data["title"], "This title is very accurate")
def test_identifier_cannot_be_changed(self):
rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=self.user))
rule = CollectionRuleFactory(
user=self.user, category=CategoryFactory(user=self.user)
)
post = PostFactory(title="This is clickbait for sure", rule=rule)
self.client.force_login(self.user)
@ -85,8 +93,12 @@ class PostDetailViewTestCase(TestCase):
self.assertEquals(data["id"], post.pk)
def test_rule_cannot_be_changed(self):
rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=self.user))
new_rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=self.user))
rule = CollectionRuleFactory(
user=self.user, category=CategoryFactory(user=self.user)
)
new_rule = CollectionRuleFactory(
user=self.user, category=CategoryFactory(user=self.user)
)
post = PostFactory(title="This is clickbait for sure", rule=rule)
self.client.force_login(self.user)
@ -103,7 +115,9 @@ class PostDetailViewTestCase(TestCase):
self.assertTrue(rule_url.endswith(reverse("api:rules-detail", args=[rule.pk])))
def test_put(self):
rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=self.user))
rule = CollectionRuleFactory(
user=self.user, category=CategoryFactory(user=self.user)
)
post = PostFactory(title="This is clickbait for sure", rule=rule)
self.client.force_login(self.user)
@ -118,7 +132,9 @@ class PostDetailViewTestCase(TestCase):
self.assertEquals(data["title"], "This title is very accurate")
def test_delete(self):
rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=self.user))
rule = CollectionRuleFactory(
user=self.user, category=CategoryFactory(user=self.user)
)
post = PostFactory(rule=rule)
self.client.force_login(self.user)
@ -137,7 +153,9 @@ class PostDetailViewTestCase(TestCase):
self.assertEquals(response.status_code, 403)
def test_post_with_unauthenticated_user_with_category(self):
rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=self.user))
rule = CollectionRuleFactory(
user=self.user, category=CategoryFactory(user=self.user)
)
post = PostFactory(rule=rule)
response = self.client.get(reverse("api:posts-detail", args=[post.pk]))
@ -156,7 +174,9 @@ class PostDetailViewTestCase(TestCase):
def test_post_with_unauthorized_user_with_category(self):
other_user = UserFactory()
rule = CollectionRuleFactory(user=other_user, category=CategoryFactory(user=other_user))
rule = CollectionRuleFactory(
user=other_user, category=CategoryFactory(user=other_user)
)
post = PostFactory(rule=rule)
self.client.force_login(self.user)
@ -166,7 +186,9 @@ class PostDetailViewTestCase(TestCase):
def test_post_with_different_user_for_category_and_rule(self):
other_user = UserFactory()
rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=other_user))
rule = CollectionRuleFactory(
user=self.user, category=CategoryFactory(user=other_user)
)
post = PostFactory(rule=rule)
self.client.force_login(self.user)

View file

@ -5,7 +5,7 @@ from django.urls import reverse
import pytz
from newsreader.auth.tests.factories import UserFactory
from newsreader.accounts.tests.factories import UserFactory
from newsreader.news.collection.tests.factories import CollectionRuleFactory
from newsreader.news.core.tests.factories import CategoryFactory, PostFactory
@ -18,7 +18,9 @@ class PostListViewTestCase(TestCase):
self.user = UserFactory(is_staff=True, password="test")
def test_simple(self):
rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=self.user))
rule = CollectionRuleFactory(
user=self.user, category=CategoryFactory(user=self.user)
)
PostFactory.create_batch(size=3, rule=rule)
self.client.force_login(self.user)
@ -31,7 +33,9 @@ class PostListViewTestCase(TestCase):
self.assertEquals(data["count"], 3)
def test_ordering(self):
rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=self.user))
rule = CollectionRuleFactory(
user=self.user, category=CategoryFactory(user=self.user)
)
posts = [
PostFactory(
@ -71,7 +75,9 @@ class PostListViewTestCase(TestCase):
self.assertEquals(data["results"][2]["id"], posts[0].pk)
def test_pagination_count(self):
rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=self.user))
rule = CollectionRuleFactory(
user=self.user, category=CategoryFactory(user=self.user)
)
PostFactory.create_batch(size=80, rule=rule)
page_size = 50
@ -180,7 +186,9 @@ class PostListViewTestCase(TestCase):
def test_posts_with_authorized_rule_unauthorized_category(self):
other_user = UserFactory()
rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=other_user))
rule = CollectionRuleFactory(
user=self.user, category=CategoryFactory(user=other_user)
)
PostFactory.create_batch(size=3, rule=rule)
self.client.force_login(self.user)

View file

@ -1,7 +1,7 @@
import factory
import pytz
from newsreader.auth.tests.factories import UserFactory
from newsreader.accounts.tests.factories import UserFactory
from newsreader.news.core.models import Category, Post
@ -21,7 +21,9 @@ class PostFactory(factory.django.DjangoModelFactory):
url = factory.Faker("url")
remote_identifier = factory.Faker("url")
rule = factory.SubFactory("newsreader.news.collection.tests.factories.CollectionRuleFactory")
rule = factory.SubFactory(
"newsreader.news.collection.tests.factories.CollectionRuleFactory"
)
class Meta:
model = Post

View file

@ -12,5 +12,7 @@ endpoints = [
path("posts/", ListPostAPIView.as_view(), name="posts-list"),
path("posts/<int:pk>/", DetailPostAPIView.as_view(), name="posts-detail"),
path("categories/", ListCategoryAPIView.as_view(), name="categories-list"),
path("categories/<int:pk>/", DetailCategoryAPIView.as_view(), name="categories-detail"),
path(
"categories/<int:pk>/", DetailCategoryAPIView.as_view(), name="categories-detail"
),
]

View file

@ -8,7 +8,7 @@ from rest_framework.generics import (
)
from rest_framework.permissions import IsAuthenticated
from newsreader.auth.permissions import IsPostOwner
from newsreader.accounts.permissions import IsPostOwner
from newsreader.core.pagination import LargeResultSetPagination, ResultSetPagination
from newsreader.news.core.models import Category, Post
from newsreader.news.core.serializers import CategorySerializer, PostSerializer