Celery integration
This commit is contained in:
parent
1b774a7208
commit
a74ffae9a7
64 changed files with 829 additions and 221 deletions
|
|
@ -10,6 +10,7 @@ python tests:
|
||||||
stage: test
|
stage: test
|
||||||
variables:
|
variables:
|
||||||
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
|
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
|
||||||
|
DJANGO_SETTINGS_MODULE: "newsreader.conf.gitlab"
|
||||||
cache:
|
cache:
|
||||||
key: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG"
|
key: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG"
|
||||||
paths:
|
paths:
|
||||||
|
|
@ -20,6 +21,6 @@ python tests:
|
||||||
- source env/bin/activate
|
- source env/bin/activate
|
||||||
- pip install -r requirements/gitlab.txt
|
- pip install -r requirements/gitlab.txt
|
||||||
script:
|
script:
|
||||||
- python src/manage.py test newsreader --settings=newsreader.conf.gitlab
|
- python src/manage.py test newsreader
|
||||||
- isort -rc src/ --check-only
|
- isort -rc src/ --check-only
|
||||||
- black -l 100 --check src/
|
- black -l 90 --check src/
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[settings]
|
[settings]
|
||||||
include_trailing_comma = true
|
include_trailing_comma = true
|
||||||
line_length = 100
|
line_length = 90
|
||||||
multi_line_output = 3
|
multi_line_output = 3
|
||||||
skip = env/, venv/
|
skip = env/, venv/
|
||||||
default_section = THIRDPARTY
|
default_section = THIRDPARTY
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
bleach==3.1.0
|
bleach==3.1.0
|
||||||
beautifulsoup4==4.7.1
|
beautifulsoup4==4.7.1
|
||||||
|
celery==4.3.0
|
||||||
certifi==2019.3.9
|
certifi==2019.3.9
|
||||||
chardet==3.0.4
|
chardet==3.0.4
|
||||||
Django==2.2
|
Django==2.2
|
||||||
|
django-celery-beat==1.5.0
|
||||||
djangorestframework==3.9.4
|
djangorestframework==3.9.4
|
||||||
lxml==4.3.4
|
lxml==4.3.4
|
||||||
feedparser==5.2.1
|
feedparser==5.2.1
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
from .celery import app as celery_app
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["celery_app"]
|
||||||
5
src/newsreader/accounts/apps.py
Normal file
5
src/newsreader/accounts/apps.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AccountsConfig(AppConfig):
|
||||||
|
name = "accounts"
|
||||||
151
src/newsreader/accounts/migrations/0001_initial.py
Normal file
151
src/newsreader/accounts/migrations/0001_initial.py
Normal 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())],
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
@ -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")]
|
||||||
|
|
@ -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())]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
@ -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",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]
|
||||||
84
src/newsreader/accounts/models.py
Normal file
84
src/newsreader/accounts/models.py
Normal 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)
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
from django.contrib.auth.models import User
|
|
||||||
|
|
||||||
import factory
|
import factory
|
||||||
|
|
||||||
|
from newsreader.accounts.models import User
|
||||||
|
|
||||||
|
|
||||||
class UserFactory(factory.django.DjangoModelFactory):
|
class UserFactory(factory.django.DjangoModelFactory):
|
||||||
username = factory.Sequence(lambda n: f"user-{n}")
|
email = factory.Faker("email")
|
||||||
email = factory.LazyAttribute(lambda o: f"{o.username}@example.org")
|
|
||||||
password = factory.Faker("password")
|
password = factory.Faker("password")
|
||||||
|
|
||||||
is_staff = False
|
is_staff = False
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class AuthConfig(AppConfig):
|
|
||||||
name = "auth"
|
|
||||||
|
|
@ -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
12
src/newsreader/celery.py
Normal 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()
|
||||||
|
|
@ -38,7 +38,10 @@ INSTALLED_APPS = [
|
||||||
"django.contrib.staticfiles",
|
"django.contrib.staticfiles",
|
||||||
# third party apps
|
# third party apps
|
||||||
"rest_framework",
|
"rest_framework",
|
||||||
|
"celery",
|
||||||
|
"django_celery_beat",
|
||||||
# app modules
|
# app modules
|
||||||
|
"newsreader.accounts",
|
||||||
"newsreader.news.core",
|
"newsreader.news.core",
|
||||||
"newsreader.news.collection",
|
"newsreader.news.collection",
|
||||||
]
|
]
|
||||||
|
|
@ -92,8 +95,8 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||||
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
|
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
|
||||||
]
|
]
|
||||||
|
|
||||||
# Authentication
|
# Authentication user model
|
||||||
AUTHENTICATION_BACKENDS = ["newsreader.auth.backends.EmailBackend"]
|
AUTH_USER_MODEL = "accounts.User"
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/2.2/topics/i18n/
|
# https://docs.djangoproject.com/en/2.2/topics/i18n/
|
||||||
|
|
@ -110,10 +113,12 @@ STATIC_URL = "/static/"
|
||||||
|
|
||||||
# Third party settings
|
# Third party settings
|
||||||
REST_FRAMEWORK = {
|
REST_FRAMEWORK = {
|
||||||
"DEFAULT_AUTHENTICATION_CLASSES": ("rest_framework.authentication.SessionAuthentication",),
|
"DEFAULT_AUTHENTICATION_CLASSES": (
|
||||||
|
"rest_framework.authentication.SessionAuthentication",
|
||||||
|
),
|
||||||
"DEFAULT_PERMISSION_CLASSES": (
|
"DEFAULT_PERMISSION_CLASSES": (
|
||||||
"rest_framework.permissions.IsAuthenticated",
|
"rest_framework.permissions.IsAuthenticated",
|
||||||
"newsreader.auth.permissions.IsOwner",
|
"newsreader.accounts.permissions.IsOwner",
|
||||||
),
|
),
|
||||||
"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
|
"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,5 +8,10 @@ class CollectionRuleAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
list_display = ("name", "category", "url", "last_suceeded", "succeeded")
|
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)
|
admin.site.register(CollectionRule, CollectionRuleAdmin)
|
||||||
|
|
|
||||||
|
|
@ -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
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
|
@ -67,11 +69,13 @@ class Collector:
|
||||||
client = None
|
client = None
|
||||||
builder = 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.client = client if client else self.client
|
||||||
self.builder = builder if builder else self.builder
|
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:
|
with self.client(rules=rules) as client:
|
||||||
for data, stream in client:
|
for data, stream in client:
|
||||||
with self.builder((data, stream)) as builder:
|
with self.builder((data, stream)) as builder:
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,9 @@ class FaviconClient(Client):
|
||||||
|
|
||||||
def __enter__(self) -> ContextManager:
|
def __enter__(self) -> ContextManager:
|
||||||
with ThreadPoolExecutor(max_workers=10) as executor:
|
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):
|
for future in as_completed(futures):
|
||||||
rule = futures[future]
|
rule = futures[future]
|
||||||
|
|
|
||||||
|
|
@ -156,7 +156,7 @@ class FeedCollector(Collector):
|
||||||
|
|
||||||
class FeedDuplicateHandler:
|
class FeedDuplicateHandler:
|
||||||
def __init__(self, rule: CollectionRule) -> None:
|
def __init__(self, rule: CollectionRule) -> None:
|
||||||
self.queryset = rule.post_set.all()
|
self.queryset = rule.posts.all()
|
||||||
|
|
||||||
def __enter__(self) -> ContextManager:
|
def __enter__(self) -> ContextManager:
|
||||||
self.existing_identifiers = self.queryset.filter(
|
self.existing_identifiers = self.queryset.filter(
|
||||||
|
|
@ -202,7 +202,9 @@ class FeedDuplicateHandler:
|
||||||
|
|
||||||
def handle_duplicate(self, instance: Post) -> Optional[Post]:
|
def handle_duplicate(self, instance: Post) -> Optional[Post]:
|
||||||
try:
|
try:
|
||||||
existing_instance = self.queryset.get(remote_identifier=instance.remote_identifier)
|
existing_instance = self.queryset.get(
|
||||||
|
remote_identifier=instance.remote_identifier
|
||||||
|
)
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,11 @@
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
from newsreader.news.collection.feed import FeedCollector
|
from newsreader.news.collection.feed import FeedCollector
|
||||||
from newsreader.news.collection.models import CollectionRule
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = "Collects Atom/RSS feeds"
|
help = "Collects Atom/RSS feeds"
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
CollectionRule.objects.all()
|
|
||||||
|
|
||||||
collector = FeedCollector()
|
collector = FeedCollector()
|
||||||
collector.collect()
|
collector.collect()
|
||||||
|
|
|
||||||
|
|
@ -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
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
@ -18,7 +18,10 @@ class Migration(migrations.Migration):
|
||||||
(
|
(
|
||||||
"id",
|
"id",
|
||||||
models.AutoField(
|
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)),
|
("created", models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
|
|
@ -27,7 +30,9 @@ class Migration(migrations.Migration):
|
||||||
("url", models.URLField(max_length=1024)),
|
("url", models.URLField(max_length=1024)),
|
||||||
(
|
(
|
||||||
"website_url",
|
"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)),
|
("favicon", models.URLField(blank=True, null=True)),
|
||||||
(
|
(
|
||||||
|
|
@ -93,8 +98,14 @@ class Migration(migrations.Migration):
|
||||||
("America/Anguilla", "America/Anguilla"),
|
("America/Anguilla", "America/Anguilla"),
|
||||||
("America/Antigua", "America/Antigua"),
|
("America/Antigua", "America/Antigua"),
|
||||||
("America/Araguaina", "America/Araguaina"),
|
("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",
|
||||||
"America/Argentina/ComodRivadavia",
|
"America/Argentina/ComodRivadavia",
|
||||||
|
|
@ -103,7 +114,10 @@ class Migration(migrations.Migration):
|
||||||
("America/Argentina/Jujuy", "America/Argentina/Jujuy"),
|
("America/Argentina/Jujuy", "America/Argentina/Jujuy"),
|
||||||
("America/Argentina/La_Rioja", "America/Argentina/La_Rioja"),
|
("America/Argentina/La_Rioja", "America/Argentina/La_Rioja"),
|
||||||
("America/Argentina/Mendoza", "America/Argentina/Mendoza"),
|
("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/Salta", "America/Argentina/Salta"),
|
||||||
("America/Argentina/San_Juan", "America/Argentina/San_Juan"),
|
("America/Argentina/San_Juan", "America/Argentina/San_Juan"),
|
||||||
("America/Argentina/San_Luis", "America/Argentina/San_Luis"),
|
("America/Argentina/San_Luis", "America/Argentina/San_Luis"),
|
||||||
|
|
@ -163,7 +177,10 @@ class Migration(migrations.Migration):
|
||||||
("America/Halifax", "America/Halifax"),
|
("America/Halifax", "America/Halifax"),
|
||||||
("America/Havana", "America/Havana"),
|
("America/Havana", "America/Havana"),
|
||||||
("America/Hermosillo", "America/Hermosillo"),
|
("America/Hermosillo", "America/Hermosillo"),
|
||||||
("America/Indiana/Indianapolis", "America/Indiana/Indianapolis"),
|
(
|
||||||
|
"America/Indiana/Indianapolis",
|
||||||
|
"America/Indiana/Indianapolis",
|
||||||
|
),
|
||||||
("America/Indiana/Knox", "America/Indiana/Knox"),
|
("America/Indiana/Knox", "America/Indiana/Knox"),
|
||||||
("America/Indiana/Marengo", "America/Indiana/Marengo"),
|
("America/Indiana/Marengo", "America/Indiana/Marengo"),
|
||||||
("America/Indiana/Petersburg", "America/Indiana/Petersburg"),
|
("America/Indiana/Petersburg", "America/Indiana/Petersburg"),
|
||||||
|
|
@ -177,8 +194,14 @@ class Migration(migrations.Migration):
|
||||||
("America/Jamaica", "America/Jamaica"),
|
("America/Jamaica", "America/Jamaica"),
|
||||||
("America/Jujuy", "America/Jujuy"),
|
("America/Jujuy", "America/Jujuy"),
|
||||||
("America/Juneau", "America/Juneau"),
|
("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/Knox_IN", "America/Knox_IN"),
|
||||||
("America/Kralendijk", "America/Kralendijk"),
|
("America/Kralendijk", "America/Kralendijk"),
|
||||||
("America/La_Paz", "America/La_Paz"),
|
("America/La_Paz", "America/La_Paz"),
|
||||||
|
|
@ -209,9 +232,18 @@ class Migration(migrations.Migration):
|
||||||
("America/Nipigon", "America/Nipigon"),
|
("America/Nipigon", "America/Nipigon"),
|
||||||
("America/Nome", "America/Nome"),
|
("America/Nome", "America/Nome"),
|
||||||
("America/Noronha", "America/Noronha"),
|
("America/Noronha", "America/Noronha"),
|
||||||
("America/North_Dakota/Beulah", "America/North_Dakota/Beulah"),
|
(
|
||||||
("America/North_Dakota/Center", "America/North_Dakota/Center"),
|
"America/North_Dakota/Beulah",
|
||||||
("America/North_Dakota/New_Salem", "America/North_Dakota/New_Salem"),
|
"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/Ojinaga", "America/Ojinaga"),
|
||||||
("America/Panama", "America/Panama"),
|
("America/Panama", "America/Panama"),
|
||||||
("America/Pangnirtung", "America/Pangnirtung"),
|
("America/Pangnirtung", "America/Pangnirtung"),
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -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
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
@ -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,
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
@ -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
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
@ -7,12 +7,10 @@ from django.db import migrations, models
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
initial = True
|
dependencies = [("collection", "0003_auto_20190714_1417")]
|
||||||
|
|
||||||
dependencies = [("collection", "0001_initial"), ("core", "0001_initial")]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AlterField(
|
||||||
model_name="collectionrule",
|
model_name="collectionrule",
|
||||||
name="category",
|
name="category",
|
||||||
field=models.ForeignKey(
|
field=models.ForeignKey(
|
||||||
|
|
@ -20,6 +18,7 @@ class Migration(migrations.Migration):
|
||||||
help_text="Posts from this rule will be tagged with this category",
|
help_text="Posts from this rule will be tagged with this category",
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="rules",
|
||||||
to="core.Category",
|
to="core.Category",
|
||||||
verbose_name="Category",
|
verbose_name="Category",
|
||||||
),
|
),
|
||||||
|
|
@ -24,6 +24,7 @@ class CollectionRule(TimeStampedModel):
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name=_("Category"),
|
verbose_name=_("Category"),
|
||||||
|
related_name="rules",
|
||||||
help_text=_("Posts from this rule will be tagged with this category"),
|
help_text=_("Posts from this rule will be tagged with this category"),
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
)
|
)
|
||||||
|
|
@ -32,7 +33,7 @@ class CollectionRule(TimeStampedModel):
|
||||||
succeeded = models.BooleanField(default=False)
|
succeeded = models.BooleanField(default=False)
|
||||||
error = models.CharField(max_length=255, blank=True, null=True)
|
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):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,11 @@ class CollectionRuleSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
|
||||||
def get_posts(self, instance):
|
def get_posts(self, instance):
|
||||||
request = self.context.get("request")
|
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
|
return serializer.data
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
||||||
19
src/newsreader/news/collection/tasks.py
Normal file
19
src/newsreader/news/collection/tasks.py
Normal 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)
|
||||||
|
|
@ -5,7 +5,7 @@ from urllib.parse import urljoin
|
||||||
from django.test import Client, TestCase
|
from django.test import Client, TestCase
|
||||||
from django.urls import reverse
|
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.collection.tests.factories import CollectionRuleFactory
|
||||||
from newsreader.news.core.tests.factories import CategoryFactory
|
from newsreader.news.core.tests.factories import CategoryFactory
|
||||||
|
|
||||||
|
|
@ -130,14 +130,18 @@ class CollectionRuleDetailViewTestCase(TestCase):
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
response = self.client.patch(
|
response = self.client.patch(
|
||||||
reverse("api:rules-detail", args=[rule.pk]),
|
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",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
url = data["category"]
|
url = data["category"]
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
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):
|
def test_put(self):
|
||||||
rule = CollectionRuleFactory(name="BBC", user=self.user)
|
rule = CollectionRuleFactory(name="BBC", user=self.user)
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ from django.urls import reverse
|
||||||
|
|
||||||
import pytz
|
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.collection.tests.factories import CollectionRuleFactory
|
||||||
from newsreader.news.core.tests.factories import CategoryFactory, PostFactory
|
from newsreader.news.core.tests.factories import CategoryFactory, PostFactory
|
||||||
|
|
||||||
|
|
@ -100,7 +100,9 @@ class CollectionRuleListViewTestCase(TestCase):
|
||||||
|
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
response = self.client.post(
|
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()
|
data = response.json()
|
||||||
category_url = data["category"]
|
category_url = data["category"]
|
||||||
|
|
@ -110,7 +112,9 @@ class CollectionRuleListViewTestCase(TestCase):
|
||||||
self.assertEquals(data["name"], "BBC")
|
self.assertEquals(data["name"], "BBC")
|
||||||
self.assertEquals(data["url"], "https://www.bbc.co.uk")
|
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):
|
def test_patch(self):
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import factory
|
import factory
|
||||||
|
|
||||||
from newsreader.auth.tests.factories import UserFactory
|
from newsreader.accounts.tests.factories import UserFactory
|
||||||
from newsreader.news.collection.models import CollectionRule
|
from newsreader.news.collection.models import CollectionRule
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,9 @@ class FaviconBuilderTestCase(TestCase):
|
||||||
self.assertEquals(rule.favicon, "https://www.bbc.com/favicon.ico")
|
self.assertEquals(rule.favicon, "https://www.bbc.com/favicon.ico")
|
||||||
|
|
||||||
def test_without_url(self):
|
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:
|
with FaviconBuilder((rule, mock_without_url)) as builder:
|
||||||
builder.build()
|
builder.build()
|
||||||
|
|
@ -39,7 +41,9 @@ class FaviconBuilderTestCase(TestCase):
|
||||||
with FaviconBuilder((rule, mock_with_weird_path)) as builder:
|
with FaviconBuilder((rule, mock_with_weird_path)) as builder:
|
||||||
builder.build()
|
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):
|
def test_other_url(self):
|
||||||
rule = CollectionRuleFactory(favicon=None)
|
rule = CollectionRuleFactory(favicon=None)
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,13 @@ feed_mock = {
|
||||||
"link": "https://www.bbc.co.uk/news/",
|
"link": "https://www.bbc.co.uk/news/",
|
||||||
},
|
},
|
||||||
"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",
|
"title": "BBC News - Home",
|
||||||
},
|
},
|
||||||
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,14 @@ class FaviconCollectorTestCase(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.maxDiff = None
|
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.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()
|
self.mocked_website_read = self.patched_website_read.start()
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,13 @@ simple_mock = {
|
||||||
"language": "en-gb",
|
"language": "en-gb",
|
||||||
"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",
|
"title": "BBC News - Home",
|
||||||
},
|
},
|
||||||
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
||||||
|
|
@ -201,7 +207,13 @@ multiple_mock = {
|
||||||
"language": "en-gb",
|
"language": "en-gb",
|
||||||
"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",
|
"title": "BBC News - Home",
|
||||||
},
|
},
|
||||||
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
||||||
|
|
@ -302,7 +314,13 @@ mock_without_identifier = {
|
||||||
"language": "en-gb",
|
"language": "en-gb",
|
||||||
"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",
|
"title": "BBC News - Home",
|
||||||
},
|
},
|
||||||
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
||||||
|
|
@ -402,7 +420,13 @@ mock_without_publish_date = {
|
||||||
"language": "en-gb",
|
"language": "en-gb",
|
||||||
"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",
|
"title": "BBC News - Home",
|
||||||
},
|
},
|
||||||
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
||||||
|
|
@ -492,7 +516,13 @@ mock_without_url = {
|
||||||
"language": "en-gb",
|
"language": "en-gb",
|
||||||
"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",
|
"title": "BBC News - Home",
|
||||||
},
|
},
|
||||||
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
||||||
|
|
@ -575,7 +605,13 @@ mock_without_body = {
|
||||||
"language": "en-gb",
|
"language": "en-gb",
|
||||||
"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",
|
"title": "BBC News - Home",
|
||||||
},
|
},
|
||||||
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
||||||
|
|
@ -676,7 +712,13 @@ mock_without_author = {
|
||||||
"language": "en-gb",
|
"language": "en-gb",
|
||||||
"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",
|
"title": "BBC News - Home",
|
||||||
},
|
},
|
||||||
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
||||||
|
|
@ -822,7 +864,13 @@ mock_with_update_entries = {
|
||||||
"language": "en-gb",
|
"language": "en-gb",
|
||||||
"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",
|
"title": "BBC News - Home",
|
||||||
},
|
},
|
||||||
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
||||||
|
|
@ -883,7 +931,13 @@ mock_with_html = {
|
||||||
"language": "en-gb",
|
"language": "en-gb",
|
||||||
"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",
|
"title": "BBC News - Home",
|
||||||
},
|
},
|
||||||
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
||||||
|
|
@ -945,7 +999,13 @@ mock_with_long_author = {
|
||||||
"language": "en-gb",
|
"language": "en-gb",
|
||||||
"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",
|
"title": "BBC News - Home",
|
||||||
},
|
},
|
||||||
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
||||||
|
|
@ -1012,7 +1072,13 @@ mock_with_long_title = {
|
||||||
"language": "en-gb",
|
"language": "en-gb",
|
||||||
"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",
|
"title": "BBC News - Home",
|
||||||
},
|
},
|
||||||
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,9 @@ class FeedBuilderTestCase(TestCase):
|
||||||
|
|
||||||
self.assertEquals(post.url, "https://www.bbc.co.uk/news/world-us-canada-48338168")
|
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):
|
def test_multiple_entries(self):
|
||||||
builder = FeedBuilder
|
builder = FeedBuilder
|
||||||
|
|
@ -64,12 +66,17 @@ class FeedBuilderTestCase(TestCase):
|
||||||
self.assertEquals(first_post.publication_date, aware_date)
|
self.assertEquals(first_post.publication_date, aware_date)
|
||||||
|
|
||||||
self.assertEquals(
|
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))
|
d = datetime.combine(date(2019, 5, 20), time(hour=12, minute=19, second=19))
|
||||||
aware_date = pytz.utc.localize(d)
|
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.publication_date, aware_date)
|
||||||
|
|
||||||
self.assertEquals(
|
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")
|
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.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")
|
@freeze_time("2019-10-30 12:30:00")
|
||||||
def test_entry_without_publication_date(self):
|
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.created, timezone.now())
|
||||||
self.assertEquals(
|
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.created, timezone.now())
|
||||||
self.assertEquals(
|
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")
|
@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.created, timezone.now())
|
||||||
self.assertEquals(
|
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.created, timezone.now())
|
||||||
self.assertEquals(
|
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")
|
@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.created, timezone.now())
|
||||||
self.assertEquals(
|
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.created, timezone.now())
|
||||||
|
|
@ -201,12 +220,14 @@ class FeedBuilderTestCase(TestCase):
|
||||||
|
|
||||||
self.assertEquals(first_post.created, timezone.now())
|
self.assertEquals(first_post.created, timezone.now())
|
||||||
self.assertEquals(
|
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.created, timezone.now())
|
||||||
self.assertEquals(
|
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):
|
def test_empty_entries(self):
|
||||||
|
|
@ -241,10 +262,13 @@ class FeedBuilderTestCase(TestCase):
|
||||||
existing_second_post.refresh_from_db()
|
existing_second_post.refresh_from_db()
|
||||||
|
|
||||||
self.assertEquals(
|
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):
|
def test_html_sanitizing(self):
|
||||||
builder = FeedBuilder
|
builder = FeedBuilder
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,13 @@ simple_mock = {
|
||||||
"language": "en-gb",
|
"language": "en-gb",
|
||||||
"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",
|
"title": "BBC News - Home",
|
||||||
},
|
},
|
||||||
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,13 @@ multiple_mock = {
|
||||||
"language": "en-gb",
|
"language": "en-gb",
|
||||||
"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",
|
"title": "BBC News - Home",
|
||||||
},
|
},
|
||||||
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
||||||
|
|
@ -154,7 +160,13 @@ empty_mock = {
|
||||||
"language": "en-gb",
|
"language": "en-gb",
|
||||||
"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",
|
"title": "BBC News - Home",
|
||||||
},
|
},
|
||||||
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
||||||
|
|
@ -292,7 +304,13 @@ duplicate_mock = {
|
||||||
"language": "en-gb",
|
"language": "en-gb",
|
||||||
"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",
|
"title": "BBC News - Home",
|
||||||
},
|
},
|
||||||
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
||||||
|
|
@ -433,7 +451,13 @@ multiple_update_mock = {
|
||||||
"language": "en-gb",
|
"language": "en-gb",
|
||||||
"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",
|
"title": "BBC News - Home",
|
||||||
},
|
},
|
||||||
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
||||||
|
|
|
||||||
|
|
@ -234,8 +234,12 @@ class FeedCollectorTestCase(TestCase):
|
||||||
self.assertEquals(rule.last_suceeded, timezone.now())
|
self.assertEquals(rule.last_suceeded, timezone.now())
|
||||||
self.assertEquals(rule.error, None)
|
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(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"
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,13 @@ simple_mock = {
|
||||||
"language": "en-gb",
|
"language": "en-gb",
|
||||||
"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",
|
"title": "BBC News - Home",
|
||||||
},
|
},
|
||||||
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,13 @@ simple_feed_mock = {
|
||||||
"link": "https://www.bbc.co.uk/news/",
|
"link": "https://www.bbc.co.uk/news/",
|
||||||
},
|
},
|
||||||
"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",
|
"title": "BBC News - Home",
|
||||||
},
|
},
|
||||||
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
"href": "http://feeds.bbci.co.uk/news/rss.xml",
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,9 @@ class URLBuilderTestCase(TestCase):
|
||||||
def test_no_link(self):
|
def test_no_link(self):
|
||||||
initial_rule = CollectionRuleFactory()
|
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()
|
rule, url = builder.build()
|
||||||
|
|
||||||
self.assertEquals(rule.pk, initial_rule.pk)
|
self.assertEquals(rule.pk, initial_rule.pk)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from newsreader.news.collection.views import CollectionRuleAPIListView, CollectionRuleDetailView
|
from newsreader.news.collection.views import (
|
||||||
|
CollectionRuleAPIListView,
|
||||||
|
CollectionRuleDetailView,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
endpoints = [
|
endpoints = [
|
||||||
|
|
|
||||||
|
|
@ -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.db.models.deletion
|
||||||
import django.utils.timezone
|
import django.utils.timezone
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -10,43 +11,36 @@ class Migration(migrations.Migration):
|
||||||
|
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [("collection", "0001_initial")]
|
dependencies = [
|
||||||
|
("collection", "0001_initial"),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
operations = [
|
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(
|
migrations.CreateModel(
|
||||||
name="Post",
|
name="Post",
|
||||||
fields=[
|
fields=[
|
||||||
(
|
(
|
||||||
"id",
|
"id",
|
||||||
models.AutoField(
|
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)),
|
("created", models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
("modified", models.DateTimeField(auto_now=True)),
|
("modified", models.DateTimeField(auto_now=True)),
|
||||||
("title", models.CharField(blank=True, max_length=200, null=True)),
|
("title", models.CharField(blank=True, max_length=200, null=True)),
|
||||||
("body", models.TextField(blank=True, 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)),
|
("publication_date", models.DateTimeField(blank=True, null=True)),
|
||||||
("url", models.URLField(blank=True, max_length=1024, null=True)),
|
("url", models.URLField(blank=True, max_length=1024, null=True)),
|
||||||
(
|
(
|
||||||
"remote_identifier",
|
"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",
|
"rule",
|
||||||
|
|
@ -59,4 +53,26 @@ class Migration(migrations.Migration):
|
||||||
],
|
],
|
||||||
options={"abstract": False},
|
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"},
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -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,
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
@ -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),
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
@ -12,8 +12,12 @@ class Post(TimeStampedModel):
|
||||||
publication_date = models.DateTimeField(blank=True, null=True)
|
publication_date = models.DateTimeField(blank=True, null=True)
|
||||||
url = models.URLField(max_length=1024, blank=True, null=True)
|
url = models.URLField(max_length=1024, blank=True, null=True)
|
||||||
|
|
||||||
rule = models.ForeignKey(CollectionRule, on_delete=models.CASCADE, editable=False)
|
rule = models.ForeignKey(
|
||||||
remote_identifier = models.CharField(max_length=500, blank=True, null=True, editable=False)
|
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):
|
def __str__(self):
|
||||||
return "Post-{}".format(self.pk)
|
return "Post-{}".format(self.pk)
|
||||||
|
|
@ -21,7 +25,7 @@ class Post(TimeStampedModel):
|
||||||
|
|
||||||
class Category(TimeStampedModel):
|
class Category(TimeStampedModel):
|
||||||
name = models.CharField(max_length=50, unique=True)
|
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:
|
class Meta:
|
||||||
verbose_name = _("Category")
|
verbose_name = _("Category")
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ class CategorySerializer(serializers.ModelSerializer):
|
||||||
rules = serializers.SerializerMethodField()
|
rules = serializers.SerializerMethodField()
|
||||||
|
|
||||||
def get_rules(self, instance):
|
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)
|
serializer = CollectionRuleSerializer(rules, many=True)
|
||||||
return serializer.data
|
return serializer.data
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ class CategorySerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
|
||||||
def get_rules(self, instance):
|
def get_rules(self, instance):
|
||||||
request = self.context.get("request")
|
request = self.context.get("request")
|
||||||
rules = instance.collectionrule_set.order_by("-modified", "-created")
|
rules = instance.rules.order_by("-modified", "-created")
|
||||||
|
|
||||||
serializer = collection.serializers.CollectionRuleSerializer(
|
serializer = collection.serializers.CollectionRuleSerializer(
|
||||||
rules, context={"request": request}, many=True
|
rules, context={"request": request}, many=True
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import json
|
||||||
from django.test import Client, TestCase
|
from django.test import Client, TestCase
|
||||||
from django.urls import reverse
|
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.collection.tests.factories import CollectionRuleFactory
|
||||||
from newsreader.news.core.tests.factories import CategoryFactory, PostFactory
|
from newsreader.news.core.tests.factories import CategoryFactory, PostFactory
|
||||||
|
|
||||||
|
|
@ -91,13 +91,17 @@ class CategoryDetailViewTestCase(TestCase):
|
||||||
category = CategoryFactory(user=self.user)
|
category = CategoryFactory(user=self.user)
|
||||||
|
|
||||||
self.client.force_login(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)
|
self.assertEquals(response.status_code, 204)
|
||||||
|
|
||||||
def test_rules(self):
|
def test_rules(self):
|
||||||
category = CategoryFactory(user=self.user)
|
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)
|
self.client.force_login(self.user)
|
||||||
response = self.client.get(reverse("api:categories-detail", args=[category.pk]))
|
response = self.client.get(reverse("api:categories-detail", args=[category.pk]))
|
||||||
|
|
@ -149,7 +153,9 @@ class CategoryDetailViewTestCase(TestCase):
|
||||||
if count < 1:
|
if count < 1:
|
||||||
continue
|
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):
|
def test_category_with_unauthenticated_user(self):
|
||||||
category = CategoryFactory(user=self.user)
|
category = CategoryFactory(user=self.user)
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ from django.urls import reverse
|
||||||
|
|
||||||
import pytz
|
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.collection.tests.factories import CollectionRuleFactory
|
||||||
from newsreader.news.core.tests.factories import CategoryFactory, PostFactory
|
from newsreader.news.core.tests.factories import CategoryFactory, PostFactory
|
||||||
|
|
||||||
|
|
@ -83,7 +83,9 @@ class CategoryListViewTestCase(TestCase):
|
||||||
|
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
response = self.client.post(
|
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()
|
response_data = response.json()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import json
|
||||||
from django.test import Client, TestCase
|
from django.test import Client, TestCase
|
||||||
from django.urls import reverse
|
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.collection.tests.factories import CollectionRuleFactory
|
||||||
from newsreader.news.core.tests.factories import CategoryFactory, PostFactory
|
from newsreader.news.core.tests.factories import CategoryFactory, PostFactory
|
||||||
|
|
||||||
|
|
@ -17,7 +17,9 @@ class PostDetailViewTestCase(TestCase):
|
||||||
self.user = UserFactory(is_staff=True, password="test")
|
self.user = UserFactory(is_staff=True, password="test")
|
||||||
|
|
||||||
def test_simple(self):
|
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)
|
post = PostFactory(rule=rule)
|
||||||
|
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
@ -44,7 +46,9 @@ class PostDetailViewTestCase(TestCase):
|
||||||
self.assertEquals(data["detail"], "Not found.")
|
self.assertEquals(data["detail"], "Not found.")
|
||||||
|
|
||||||
def test_post(self):
|
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)
|
post = PostFactory(rule=rule)
|
||||||
|
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
@ -55,7 +59,9 @@ class PostDetailViewTestCase(TestCase):
|
||||||
self.assertEquals(data["detail"], 'Method "POST" not allowed.')
|
self.assertEquals(data["detail"], 'Method "POST" not allowed.')
|
||||||
|
|
||||||
def test_patch(self):
|
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)
|
post = PostFactory(title="This is clickbait for sure", rule=rule)
|
||||||
|
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
@ -70,7 +76,9 @@ class PostDetailViewTestCase(TestCase):
|
||||||
self.assertEquals(data["title"], "This title is very accurate")
|
self.assertEquals(data["title"], "This title is very accurate")
|
||||||
|
|
||||||
def test_identifier_cannot_be_changed(self):
|
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)
|
post = PostFactory(title="This is clickbait for sure", rule=rule)
|
||||||
|
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
@ -85,8 +93,12 @@ class PostDetailViewTestCase(TestCase):
|
||||||
self.assertEquals(data["id"], post.pk)
|
self.assertEquals(data["id"], post.pk)
|
||||||
|
|
||||||
def test_rule_cannot_be_changed(self):
|
def test_rule_cannot_be_changed(self):
|
||||||
rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=self.user))
|
rule = CollectionRuleFactory(
|
||||||
new_rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=self.user))
|
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)
|
post = PostFactory(title="This is clickbait for sure", rule=rule)
|
||||||
|
|
||||||
self.client.force_login(self.user)
|
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])))
|
self.assertTrue(rule_url.endswith(reverse("api:rules-detail", args=[rule.pk])))
|
||||||
|
|
||||||
def test_put(self):
|
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)
|
post = PostFactory(title="This is clickbait for sure", rule=rule)
|
||||||
|
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
@ -118,7 +132,9 @@ class PostDetailViewTestCase(TestCase):
|
||||||
self.assertEquals(data["title"], "This title is very accurate")
|
self.assertEquals(data["title"], "This title is very accurate")
|
||||||
|
|
||||||
def test_delete(self):
|
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)
|
post = PostFactory(rule=rule)
|
||||||
|
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
@ -137,7 +153,9 @@ class PostDetailViewTestCase(TestCase):
|
||||||
self.assertEquals(response.status_code, 403)
|
self.assertEquals(response.status_code, 403)
|
||||||
|
|
||||||
def test_post_with_unauthenticated_user_with_category(self):
|
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)
|
post = PostFactory(rule=rule)
|
||||||
|
|
||||||
response = self.client.get(reverse("api:posts-detail", args=[post.pk]))
|
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):
|
def test_post_with_unauthorized_user_with_category(self):
|
||||||
other_user = UserFactory()
|
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)
|
post = PostFactory(rule=rule)
|
||||||
|
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
@ -166,7 +186,9 @@ class PostDetailViewTestCase(TestCase):
|
||||||
|
|
||||||
def test_post_with_different_user_for_category_and_rule(self):
|
def test_post_with_different_user_for_category_and_rule(self):
|
||||||
other_user = UserFactory()
|
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)
|
post = PostFactory(rule=rule)
|
||||||
|
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from django.urls import reverse
|
||||||
|
|
||||||
import pytz
|
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.collection.tests.factories import CollectionRuleFactory
|
||||||
from newsreader.news.core.tests.factories import CategoryFactory, PostFactory
|
from newsreader.news.core.tests.factories import CategoryFactory, PostFactory
|
||||||
|
|
||||||
|
|
@ -18,7 +18,9 @@ class PostListViewTestCase(TestCase):
|
||||||
self.user = UserFactory(is_staff=True, password="test")
|
self.user = UserFactory(is_staff=True, password="test")
|
||||||
|
|
||||||
def test_simple(self):
|
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)
|
PostFactory.create_batch(size=3, rule=rule)
|
||||||
|
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
@ -31,7 +33,9 @@ class PostListViewTestCase(TestCase):
|
||||||
self.assertEquals(data["count"], 3)
|
self.assertEquals(data["count"], 3)
|
||||||
|
|
||||||
def test_ordering(self):
|
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 = [
|
posts = [
|
||||||
PostFactory(
|
PostFactory(
|
||||||
|
|
@ -71,7 +75,9 @@ class PostListViewTestCase(TestCase):
|
||||||
self.assertEquals(data["results"][2]["id"], posts[0].pk)
|
self.assertEquals(data["results"][2]["id"], posts[0].pk)
|
||||||
|
|
||||||
def test_pagination_count(self):
|
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)
|
PostFactory.create_batch(size=80, rule=rule)
|
||||||
page_size = 50
|
page_size = 50
|
||||||
|
|
||||||
|
|
@ -180,7 +186,9 @@ class PostListViewTestCase(TestCase):
|
||||||
def test_posts_with_authorized_rule_unauthorized_category(self):
|
def test_posts_with_authorized_rule_unauthorized_category(self):
|
||||||
other_user = UserFactory()
|
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)
|
PostFactory.create_batch(size=3, rule=rule)
|
||||||
|
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import factory
|
import factory
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
from newsreader.auth.tests.factories import UserFactory
|
from newsreader.accounts.tests.factories import UserFactory
|
||||||
from newsreader.news.core.models import Category, Post
|
from newsreader.news.core.models import Category, Post
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -21,7 +21,9 @@ class PostFactory(factory.django.DjangoModelFactory):
|
||||||
url = factory.Faker("url")
|
url = factory.Faker("url")
|
||||||
remote_identifier = 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:
|
class Meta:
|
||||||
model = Post
|
model = Post
|
||||||
|
|
|
||||||
|
|
@ -12,5 +12,7 @@ endpoints = [
|
||||||
path("posts/", ListPostAPIView.as_view(), name="posts-list"),
|
path("posts/", ListPostAPIView.as_view(), name="posts-list"),
|
||||||
path("posts/<int:pk>/", DetailPostAPIView.as_view(), name="posts-detail"),
|
path("posts/<int:pk>/", DetailPostAPIView.as_view(), name="posts-detail"),
|
||||||
path("categories/", ListCategoryAPIView.as_view(), name="categories-list"),
|
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"
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ from rest_framework.generics import (
|
||||||
)
|
)
|
||||||
from rest_framework.permissions import IsAuthenticated
|
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.core.pagination import LargeResultSetPagination, ResultSetPagination
|
||||||
from newsreader.news.core.models import Category, Post
|
from newsreader.news.core.models import Category, Post
|
||||||
from newsreader.news.core.serializers import CategorySerializer, PostSerializer
|
from newsreader.news.core.serializers import CategorySerializer, PostSerializer
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue