diff --git a/requirements/base.txt b/requirements/base.txt index 00667bb..05be61f 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -3,6 +3,7 @@ beautifulsoup4==4.7.1 certifi==2019.3.9 chardet==3.0.4 Django==2.2 +djangorestframework==3.9.4 lxml==4.3.4 feedparser==5.2.1 idna==2.8 diff --git a/requirements/dev.txt b/requirements/dev.txt index 0a685e3..dbf9e15 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -2,3 +2,5 @@ factory-boy==2.12.0 freezegun==0.3.12 +django-debug-toolbar==2.0 +django-extensions==2.1.9 diff --git a/src/manage.py b/src/manage.py index a30fa10..45fc02f 100755 --- a/src/manage.py +++ b/src/manage.py @@ -5,7 +5,7 @@ import sys def main(): - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'newsreader.conf.base') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "newsreader.conf.dev") try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -17,5 +17,5 @@ def main(): execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/src/newsreader/news/posts/__init__.py b/src/newsreader/auth/__init__.py similarity index 100% rename from src/newsreader/news/posts/__init__.py rename to src/newsreader/auth/__init__.py diff --git a/src/newsreader/auth/admin.py b/src/newsreader/auth/admin.py new file mode 100644 index 0000000..846f6b4 --- /dev/null +++ b/src/newsreader/auth/admin.py @@ -0,0 +1 @@ +# Register your models here. diff --git a/src/newsreader/auth/apps.py b/src/newsreader/auth/apps.py new file mode 100644 index 0000000..c467d4e --- /dev/null +++ b/src/newsreader/auth/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class AuthConfig(AppConfig): + name = "auth" diff --git a/src/newsreader/auth/backends.py b/src/newsreader/auth/backends.py new file mode 100644 index 0000000..30a78b9 --- /dev/null +++ b/src/newsreader/auth/backends.py @@ -0,0 +1,15 @@ +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 diff --git a/src/newsreader/news/posts/migrations/__init__.py b/src/newsreader/auth/migrations/__init__.py similarity index 100% rename from src/newsreader/news/posts/migrations/__init__.py rename to src/newsreader/auth/migrations/__init__.py diff --git a/src/newsreader/news/posts/tests/__init__.py b/src/newsreader/auth/models.py similarity index 100% rename from src/newsreader/news/posts/tests/__init__.py rename to src/newsreader/auth/models.py diff --git a/src/newsreader/auth/permissions.py b/src/newsreader/auth/permissions.py new file mode 100644 index 0000000..2c6cf25 --- /dev/null +++ b/src/newsreader/auth/permissions.py @@ -0,0 +1,23 @@ +from rest_framework.permissions import BasePermission + + +class IsOwner(BasePermission): + def has_object_permission(self, request, view, obj): + if hasattr(obj, "user"): + return obj.user == request.user + + +class IsPostOwner(BasePermission): + def has_object_permission(self, request, view, obj): + is_category_user = False + is_rule_user = False + rule = obj.rule + + if rule and rule.user: + is_rule_user = bool(rule.user == request.user) + + if rule.category and rule.category.user: + is_category_user = bool(rule.category.user == request.user) + return bool(is_category_user and is_rule_user) + + return is_rule_user diff --git a/src/newsreader/auth/tests/__init__.py b/src/newsreader/auth/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/auth/tests/factories.py b/src/newsreader/auth/tests/factories.py new file mode 100644 index 0000000..3975f62 --- /dev/null +++ b/src/newsreader/auth/tests/factories.py @@ -0,0 +1,20 @@ +from django.contrib.auth.models import User + +import factory + + +class UserFactory(factory.django.DjangoModelFactory): + username = factory.Sequence(lambda n: f"user-{n}") + email = factory.LazyAttribute(lambda o: f"{o.username}@example.org") + password = factory.Faker("password") + + is_staff = False + is_active = True + + @classmethod + def _create(cls, model_class, *args, **kwargs): + manager = cls._get_manager(model_class) + return manager.create_user(*args, **kwargs) + + class Meta: + model = User diff --git a/src/newsreader/auth/views.py b/src/newsreader/auth/views.py new file mode 100644 index 0000000..60f00ef --- /dev/null +++ b/src/newsreader/auth/views.py @@ -0,0 +1 @@ +# Create your views here. diff --git a/src/newsreader/conf/base.py b/src/newsreader/conf/base.py index 16927e4..593c42c 100644 --- a/src/newsreader/conf/base.py +++ b/src/newsreader/conf/base.py @@ -26,6 +26,7 @@ SECRET_KEY = "^!7a2jq5j!exc-55vf$anx9^6ff6=u_ub5=5p1(1x47fix)syh" DEBUG = False ALLOWED_HOSTS = ["127.0.0.1"] +INTERNAL_IPS = ["127.0.0.1"] # Application definition INSTALLED_APPS = [ @@ -35,9 +36,11 @@ INSTALLED_APPS = [ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + # third party apps + "rest_framework", # app modules + "newsreader.news.core", "newsreader.news.collection", - "newsreader.news.posts", ] MIDDLEWARE = [ @@ -89,6 +92,9 @@ AUTH_PASSWORD_VALIDATORS = [ {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, ] +# Authentication +AUTHENTICATION_BACKENDS = ["newsreader.auth.backends.EmailBackend"] + # Internationalization # https://docs.djangoproject.com/en/2.2/topics/i18n/ LANGUAGE_CODE = "en-us" @@ -101,3 +107,12 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.2/howto/static-files/ STATIC_URL = "/static/" + +# Third party settings +REST_FRAMEWORK = { + "DEFAULT_AUTHENTICATION_CLASSES": ("rest_framework.authentication.SessionAuthentication",), + "DEFAULT_PERMISSION_CLASSES": ( + "rest_framework.permissions.IsAuthenticated", + "newsreader.auth.permissions.IsOwner", + ), +} diff --git a/src/newsreader/conf/dev.py b/src/newsreader/conf/dev.py index 245a78c..7453d52 100644 --- a/src/newsreader/conf/dev.py +++ b/src/newsreader/conf/dev.py @@ -1,13 +1,15 @@ from .base import * -# Development settings - DEBUG = True +MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"] + EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" +INSTALLED_APPS += ["debug_toolbar", "django_extensions"] + try: - from .local import * + pass except ImportError: pass diff --git a/src/newsreader/core/admin.py b/src/newsreader/core/admin.py index a011d19..846f6b4 100644 --- a/src/newsreader/core/admin.py +++ b/src/newsreader/core/admin.py @@ -1,4 +1 @@ -from django.contrib import admin - - # Register your models here. diff --git a/src/newsreader/core/models.py b/src/newsreader/core/models.py index 2e696fb..f8bd80f 100644 --- a/src/newsreader/core/models.py +++ b/src/newsreader/core/models.py @@ -1,4 +1,5 @@ from django.db import models +from django.utils import timezone class TimeStampedModel(models.Model): @@ -7,7 +8,7 @@ class TimeStampedModel(models.Model): updating ``created`` and ``modified`` fields. """ - created = models.DateTimeField(auto_now_add=True) + created = models.DateTimeField(default=timezone.now) modified = models.DateTimeField(auto_now=True) class Meta: diff --git a/src/newsreader/core/pagination.py b/src/newsreader/core/pagination.py new file mode 100644 index 0000000..5e19771 --- /dev/null +++ b/src/newsreader/core/pagination.py @@ -0,0 +1,12 @@ +from rest_framework.pagination import PageNumberPagination + + +class ResultSetPagination(PageNumberPagination): + page_size_query_param = "count" + max_page_size = 50 + page_size = 30 + + +class LargeResultSetPagination(ResultSetPagination): + max_page_size = 100 + page_size = 50 diff --git a/src/newsreader/core/permissions.py b/src/newsreader/core/permissions.py new file mode 100644 index 0000000..b584b63 --- /dev/null +++ b/src/newsreader/core/permissions.py @@ -0,0 +1,6 @@ +from rest_framework.permissions import BasePermission + + +class IsOwner(BasePermission): + def has_object_permission(self, request, view, obj): + return obj.user == request.user diff --git a/src/newsreader/core/tests.py b/src/newsreader/core/tests.py index 7c72b39..a39b155 100644 --- a/src/newsreader/core/tests.py +++ b/src/newsreader/core/tests.py @@ -1,4 +1 @@ -from django.test import TestCase - - # Create your tests here. diff --git a/src/newsreader/core/views.py b/src/newsreader/core/views.py index dc1ba72..60f00ef 100644 --- a/src/newsreader/core/views.py +++ b/src/newsreader/core/views.py @@ -1,4 +1 @@ -from django.shortcuts import render - - # Create your views here. diff --git a/src/newsreader/news/collection/base.py b/src/newsreader/news/collection/base.py index c202df8..1b80256 100644 --- a/src/newsreader/news/collection/base.py +++ b/src/newsreader/news/collection/base.py @@ -1,9 +1,5 @@ from typing import ContextManager, Dict, List, Optional, Tuple -from django.utils import timezone - -import requests - from bs4 import BeautifulSoup from newsreader.news.collection.exceptions import StreamParseException diff --git a/src/newsreader/news/collection/feed.py b/src/newsreader/news/collection/feed.py index 2cf248c..a7ab2fc 100644 --- a/src/newsreader/news/collection/feed.py +++ b/src/newsreader/news/collection/feed.py @@ -17,9 +17,8 @@ from newsreader.news.collection.exceptions import ( StreamTimeOutException, ) from newsreader.news.collection.models import CollectionRule -from newsreader.news.collection.response_handler import ResponseHandler from newsreader.news.collection.utils import build_publication_date, fetch -from newsreader.news.posts.models import Post +from newsreader.news.core.models import Post class FeedBuilder(Builder): @@ -62,7 +61,7 @@ class FeedBuilder(Builder): tz = pytz.timezone(rule.timezone) for entry in entries: - data = {"rule_id": rule.pk, "category": rule.category} + data = {"rule_id": rule.pk} for field, value in field_mapping.items(): if field in entry: diff --git a/src/newsreader/news/collection/management/commands/collect.py b/src/newsreader/news/collection/management/commands/collect.py index 855089f..3da9905 100644 --- a/src/newsreader/news/collection/management/commands/collect.py +++ b/src/newsreader/news/collection/management/commands/collect.py @@ -1,4 +1,4 @@ -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import BaseCommand from newsreader.news.collection.feed import FeedCollector from newsreader.news.collection.models import CollectionRule diff --git a/src/newsreader/news/collection/migrations/0001_initial.py b/src/newsreader/news/collection/migrations/0001_initial.py index 1091b7a..00d3d31 100644 --- a/src/newsreader/news/collection/migrations/0001_initial.py +++ b/src/newsreader/news/collection/migrations/0001_initial.py @@ -1,4 +1,6 @@ -# Generated by Django 2.2 on 2019-04-10 20:10 +# Generated by Django 2.2 on 2019-07-05 20:59 + +import django.utils.timezone from django.db import migrations, models @@ -19,10 +21,623 @@ class Migration(migrations.Migration): 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=100)), - ("url", models.URLField()), - ("last_suceeded", models.DateTimeField()), + ("url", models.URLField(max_length=1024)), + ( + "website_url", + models.URLField(blank=True, editable=False, max_length=1024, null=True), + ), + ("favicon", models.URLField(blank=True, null=True)), + ( + "timezone", + models.CharField( + choices=[ + ("Africa/Abidjan", "Africa/Abidjan"), + ("Africa/Accra", "Africa/Accra"), + ("Africa/Addis_Ababa", "Africa/Addis_Ababa"), + ("Africa/Algiers", "Africa/Algiers"), + ("Africa/Asmara", "Africa/Asmara"), + ("Africa/Asmera", "Africa/Asmera"), + ("Africa/Bamako", "Africa/Bamako"), + ("Africa/Bangui", "Africa/Bangui"), + ("Africa/Banjul", "Africa/Banjul"), + ("Africa/Bissau", "Africa/Bissau"), + ("Africa/Blantyre", "Africa/Blantyre"), + ("Africa/Brazzaville", "Africa/Brazzaville"), + ("Africa/Bujumbura", "Africa/Bujumbura"), + ("Africa/Cairo", "Africa/Cairo"), + ("Africa/Casablanca", "Africa/Casablanca"), + ("Africa/Ceuta", "Africa/Ceuta"), + ("Africa/Conakry", "Africa/Conakry"), + ("Africa/Dakar", "Africa/Dakar"), + ("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"), + ("Africa/Djibouti", "Africa/Djibouti"), + ("Africa/Douala", "Africa/Douala"), + ("Africa/El_Aaiun", "Africa/El_Aaiun"), + ("Africa/Freetown", "Africa/Freetown"), + ("Africa/Gaborone", "Africa/Gaborone"), + ("Africa/Harare", "Africa/Harare"), + ("Africa/Johannesburg", "Africa/Johannesburg"), + ("Africa/Juba", "Africa/Juba"), + ("Africa/Kampala", "Africa/Kampala"), + ("Africa/Khartoum", "Africa/Khartoum"), + ("Africa/Kigali", "Africa/Kigali"), + ("Africa/Kinshasa", "Africa/Kinshasa"), + ("Africa/Lagos", "Africa/Lagos"), + ("Africa/Libreville", "Africa/Libreville"), + ("Africa/Lome", "Africa/Lome"), + ("Africa/Luanda", "Africa/Luanda"), + ("Africa/Lubumbashi", "Africa/Lubumbashi"), + ("Africa/Lusaka", "Africa/Lusaka"), + ("Africa/Malabo", "Africa/Malabo"), + ("Africa/Maputo", "Africa/Maputo"), + ("Africa/Maseru", "Africa/Maseru"), + ("Africa/Mbabane", "Africa/Mbabane"), + ("Africa/Mogadishu", "Africa/Mogadishu"), + ("Africa/Monrovia", "Africa/Monrovia"), + ("Africa/Nairobi", "Africa/Nairobi"), + ("Africa/Ndjamena", "Africa/Ndjamena"), + ("Africa/Niamey", "Africa/Niamey"), + ("Africa/Nouakchott", "Africa/Nouakchott"), + ("Africa/Ouagadougou", "Africa/Ouagadougou"), + ("Africa/Porto-Novo", "Africa/Porto-Novo"), + ("Africa/Sao_Tome", "Africa/Sao_Tome"), + ("Africa/Timbuktu", "Africa/Timbuktu"), + ("Africa/Tripoli", "Africa/Tripoli"), + ("Africa/Tunis", "Africa/Tunis"), + ("Africa/Windhoek", "Africa/Windhoek"), + ("America/Adak", "America/Adak"), + ("America/Anchorage", "America/Anchorage"), + ("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/ComodRivadavia", + "America/Argentina/ComodRivadavia", + ), + ("America/Argentina/Cordoba", "America/Argentina/Cordoba"), + ("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/Salta", "America/Argentina/Salta"), + ("America/Argentina/San_Juan", "America/Argentina/San_Juan"), + ("America/Argentina/San_Luis", "America/Argentina/San_Luis"), + ("America/Argentina/Tucuman", "America/Argentina/Tucuman"), + ("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"), + ("America/Aruba", "America/Aruba"), + ("America/Asuncion", "America/Asuncion"), + ("America/Atikokan", "America/Atikokan"), + ("America/Atka", "America/Atka"), + ("America/Bahia", "America/Bahia"), + ("America/Bahia_Banderas", "America/Bahia_Banderas"), + ("America/Barbados", "America/Barbados"), + ("America/Belem", "America/Belem"), + ("America/Belize", "America/Belize"), + ("America/Blanc-Sablon", "America/Blanc-Sablon"), + ("America/Boa_Vista", "America/Boa_Vista"), + ("America/Bogota", "America/Bogota"), + ("America/Boise", "America/Boise"), + ("America/Buenos_Aires", "America/Buenos_Aires"), + ("America/Cambridge_Bay", "America/Cambridge_Bay"), + ("America/Campo_Grande", "America/Campo_Grande"), + ("America/Cancun", "America/Cancun"), + ("America/Caracas", "America/Caracas"), + ("America/Catamarca", "America/Catamarca"), + ("America/Cayenne", "America/Cayenne"), + ("America/Cayman", "America/Cayman"), + ("America/Chicago", "America/Chicago"), + ("America/Chihuahua", "America/Chihuahua"), + ("America/Coral_Harbour", "America/Coral_Harbour"), + ("America/Cordoba", "America/Cordoba"), + ("America/Costa_Rica", "America/Costa_Rica"), + ("America/Creston", "America/Creston"), + ("America/Cuiaba", "America/Cuiaba"), + ("America/Curacao", "America/Curacao"), + ("America/Danmarkshavn", "America/Danmarkshavn"), + ("America/Dawson", "America/Dawson"), + ("America/Dawson_Creek", "America/Dawson_Creek"), + ("America/Denver", "America/Denver"), + ("America/Detroit", "America/Detroit"), + ("America/Dominica", "America/Dominica"), + ("America/Edmonton", "America/Edmonton"), + ("America/Eirunepe", "America/Eirunepe"), + ("America/El_Salvador", "America/El_Salvador"), + ("America/Ensenada", "America/Ensenada"), + ("America/Fort_Nelson", "America/Fort_Nelson"), + ("America/Fort_Wayne", "America/Fort_Wayne"), + ("America/Fortaleza", "America/Fortaleza"), + ("America/Glace_Bay", "America/Glace_Bay"), + ("America/Godthab", "America/Godthab"), + ("America/Goose_Bay", "America/Goose_Bay"), + ("America/Grand_Turk", "America/Grand_Turk"), + ("America/Grenada", "America/Grenada"), + ("America/Guadeloupe", "America/Guadeloupe"), + ("America/Guatemala", "America/Guatemala"), + ("America/Guayaquil", "America/Guayaquil"), + ("America/Guyana", "America/Guyana"), + ("America/Halifax", "America/Halifax"), + ("America/Havana", "America/Havana"), + ("America/Hermosillo", "America/Hermosillo"), + ("America/Indiana/Indianapolis", "America/Indiana/Indianapolis"), + ("America/Indiana/Knox", "America/Indiana/Knox"), + ("America/Indiana/Marengo", "America/Indiana/Marengo"), + ("America/Indiana/Petersburg", "America/Indiana/Petersburg"), + ("America/Indiana/Tell_City", "America/Indiana/Tell_City"), + ("America/Indiana/Vevay", "America/Indiana/Vevay"), + ("America/Indiana/Vincennes", "America/Indiana/Vincennes"), + ("America/Indiana/Winamac", "America/Indiana/Winamac"), + ("America/Indianapolis", "America/Indianapolis"), + ("America/Inuvik", "America/Inuvik"), + ("America/Iqaluit", "America/Iqaluit"), + ("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/Knox_IN", "America/Knox_IN"), + ("America/Kralendijk", "America/Kralendijk"), + ("America/La_Paz", "America/La_Paz"), + ("America/Lima", "America/Lima"), + ("America/Los_Angeles", "America/Los_Angeles"), + ("America/Louisville", "America/Louisville"), + ("America/Lower_Princes", "America/Lower_Princes"), + ("America/Maceio", "America/Maceio"), + ("America/Managua", "America/Managua"), + ("America/Manaus", "America/Manaus"), + ("America/Marigot", "America/Marigot"), + ("America/Martinique", "America/Martinique"), + ("America/Matamoros", "America/Matamoros"), + ("America/Mazatlan", "America/Mazatlan"), + ("America/Mendoza", "America/Mendoza"), + ("America/Menominee", "America/Menominee"), + ("America/Merida", "America/Merida"), + ("America/Metlakatla", "America/Metlakatla"), + ("America/Mexico_City", "America/Mexico_City"), + ("America/Miquelon", "America/Miquelon"), + ("America/Moncton", "America/Moncton"), + ("America/Monterrey", "America/Monterrey"), + ("America/Montevideo", "America/Montevideo"), + ("America/Montreal", "America/Montreal"), + ("America/Montserrat", "America/Montserrat"), + ("America/Nassau", "America/Nassau"), + ("America/New_York", "America/New_York"), + ("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/Ojinaga", "America/Ojinaga"), + ("America/Panama", "America/Panama"), + ("America/Pangnirtung", "America/Pangnirtung"), + ("America/Paramaribo", "America/Paramaribo"), + ("America/Phoenix", "America/Phoenix"), + ("America/Port-au-Prince", "America/Port-au-Prince"), + ("America/Port_of_Spain", "America/Port_of_Spain"), + ("America/Porto_Acre", "America/Porto_Acre"), + ("America/Porto_Velho", "America/Porto_Velho"), + ("America/Puerto_Rico", "America/Puerto_Rico"), + ("America/Punta_Arenas", "America/Punta_Arenas"), + ("America/Rainy_River", "America/Rainy_River"), + ("America/Rankin_Inlet", "America/Rankin_Inlet"), + ("America/Recife", "America/Recife"), + ("America/Regina", "America/Regina"), + ("America/Resolute", "America/Resolute"), + ("America/Rio_Branco", "America/Rio_Branco"), + ("America/Rosario", "America/Rosario"), + ("America/Santa_Isabel", "America/Santa_Isabel"), + ("America/Santarem", "America/Santarem"), + ("America/Santiago", "America/Santiago"), + ("America/Santo_Domingo", "America/Santo_Domingo"), + ("America/Sao_Paulo", "America/Sao_Paulo"), + ("America/Scoresbysund", "America/Scoresbysund"), + ("America/Shiprock", "America/Shiprock"), + ("America/Sitka", "America/Sitka"), + ("America/St_Barthelemy", "America/St_Barthelemy"), + ("America/St_Johns", "America/St_Johns"), + ("America/St_Kitts", "America/St_Kitts"), + ("America/St_Lucia", "America/St_Lucia"), + ("America/St_Thomas", "America/St_Thomas"), + ("America/St_Vincent", "America/St_Vincent"), + ("America/Swift_Current", "America/Swift_Current"), + ("America/Tegucigalpa", "America/Tegucigalpa"), + ("America/Thule", "America/Thule"), + ("America/Thunder_Bay", "America/Thunder_Bay"), + ("America/Tijuana", "America/Tijuana"), + ("America/Toronto", "America/Toronto"), + ("America/Tortola", "America/Tortola"), + ("America/Vancouver", "America/Vancouver"), + ("America/Virgin", "America/Virgin"), + ("America/Whitehorse", "America/Whitehorse"), + ("America/Winnipeg", "America/Winnipeg"), + ("America/Yakutat", "America/Yakutat"), + ("America/Yellowknife", "America/Yellowknife"), + ("Antarctica/Casey", "Antarctica/Casey"), + ("Antarctica/Davis", "Antarctica/Davis"), + ("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"), + ("Antarctica/Macquarie", "Antarctica/Macquarie"), + ("Antarctica/Mawson", "Antarctica/Mawson"), + ("Antarctica/McMurdo", "Antarctica/McMurdo"), + ("Antarctica/Palmer", "Antarctica/Palmer"), + ("Antarctica/Rothera", "Antarctica/Rothera"), + ("Antarctica/South_Pole", "Antarctica/South_Pole"), + ("Antarctica/Syowa", "Antarctica/Syowa"), + ("Antarctica/Troll", "Antarctica/Troll"), + ("Antarctica/Vostok", "Antarctica/Vostok"), + ("Arctic/Longyearbyen", "Arctic/Longyearbyen"), + ("Asia/Aden", "Asia/Aden"), + ("Asia/Almaty", "Asia/Almaty"), + ("Asia/Amman", "Asia/Amman"), + ("Asia/Anadyr", "Asia/Anadyr"), + ("Asia/Aqtau", "Asia/Aqtau"), + ("Asia/Aqtobe", "Asia/Aqtobe"), + ("Asia/Ashgabat", "Asia/Ashgabat"), + ("Asia/Ashkhabad", "Asia/Ashkhabad"), + ("Asia/Atyrau", "Asia/Atyrau"), + ("Asia/Baghdad", "Asia/Baghdad"), + ("Asia/Bahrain", "Asia/Bahrain"), + ("Asia/Baku", "Asia/Baku"), + ("Asia/Bangkok", "Asia/Bangkok"), + ("Asia/Barnaul", "Asia/Barnaul"), + ("Asia/Beirut", "Asia/Beirut"), + ("Asia/Bishkek", "Asia/Bishkek"), + ("Asia/Brunei", "Asia/Brunei"), + ("Asia/Calcutta", "Asia/Calcutta"), + ("Asia/Chita", "Asia/Chita"), + ("Asia/Choibalsan", "Asia/Choibalsan"), + ("Asia/Chongqing", "Asia/Chongqing"), + ("Asia/Chungking", "Asia/Chungking"), + ("Asia/Colombo", "Asia/Colombo"), + ("Asia/Dacca", "Asia/Dacca"), + ("Asia/Damascus", "Asia/Damascus"), + ("Asia/Dhaka", "Asia/Dhaka"), + ("Asia/Dili", "Asia/Dili"), + ("Asia/Dubai", "Asia/Dubai"), + ("Asia/Dushanbe", "Asia/Dushanbe"), + ("Asia/Famagusta", "Asia/Famagusta"), + ("Asia/Gaza", "Asia/Gaza"), + ("Asia/Harbin", "Asia/Harbin"), + ("Asia/Hebron", "Asia/Hebron"), + ("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"), + ("Asia/Hong_Kong", "Asia/Hong_Kong"), + ("Asia/Hovd", "Asia/Hovd"), + ("Asia/Irkutsk", "Asia/Irkutsk"), + ("Asia/Istanbul", "Asia/Istanbul"), + ("Asia/Jakarta", "Asia/Jakarta"), + ("Asia/Jayapura", "Asia/Jayapura"), + ("Asia/Jerusalem", "Asia/Jerusalem"), + ("Asia/Kabul", "Asia/Kabul"), + ("Asia/Kamchatka", "Asia/Kamchatka"), + ("Asia/Karachi", "Asia/Karachi"), + ("Asia/Kashgar", "Asia/Kashgar"), + ("Asia/Kathmandu", "Asia/Kathmandu"), + ("Asia/Katmandu", "Asia/Katmandu"), + ("Asia/Khandyga", "Asia/Khandyga"), + ("Asia/Kolkata", "Asia/Kolkata"), + ("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"), + ("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"), + ("Asia/Kuching", "Asia/Kuching"), + ("Asia/Kuwait", "Asia/Kuwait"), + ("Asia/Macao", "Asia/Macao"), + ("Asia/Macau", "Asia/Macau"), + ("Asia/Magadan", "Asia/Magadan"), + ("Asia/Makassar", "Asia/Makassar"), + ("Asia/Manila", "Asia/Manila"), + ("Asia/Muscat", "Asia/Muscat"), + ("Asia/Nicosia", "Asia/Nicosia"), + ("Asia/Novokuznetsk", "Asia/Novokuznetsk"), + ("Asia/Novosibirsk", "Asia/Novosibirsk"), + ("Asia/Omsk", "Asia/Omsk"), + ("Asia/Oral", "Asia/Oral"), + ("Asia/Phnom_Penh", "Asia/Phnom_Penh"), + ("Asia/Pontianak", "Asia/Pontianak"), + ("Asia/Pyongyang", "Asia/Pyongyang"), + ("Asia/Qatar", "Asia/Qatar"), + ("Asia/Qostanay", "Asia/Qostanay"), + ("Asia/Qyzylorda", "Asia/Qyzylorda"), + ("Asia/Rangoon", "Asia/Rangoon"), + ("Asia/Riyadh", "Asia/Riyadh"), + ("Asia/Saigon", "Asia/Saigon"), + ("Asia/Sakhalin", "Asia/Sakhalin"), + ("Asia/Samarkand", "Asia/Samarkand"), + ("Asia/Seoul", "Asia/Seoul"), + ("Asia/Shanghai", "Asia/Shanghai"), + ("Asia/Singapore", "Asia/Singapore"), + ("Asia/Srednekolymsk", "Asia/Srednekolymsk"), + ("Asia/Taipei", "Asia/Taipei"), + ("Asia/Tashkent", "Asia/Tashkent"), + ("Asia/Tbilisi", "Asia/Tbilisi"), + ("Asia/Tehran", "Asia/Tehran"), + ("Asia/Tel_Aviv", "Asia/Tel_Aviv"), + ("Asia/Thimbu", "Asia/Thimbu"), + ("Asia/Thimphu", "Asia/Thimphu"), + ("Asia/Tokyo", "Asia/Tokyo"), + ("Asia/Tomsk", "Asia/Tomsk"), + ("Asia/Ujung_Pandang", "Asia/Ujung_Pandang"), + ("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"), + ("Asia/Ulan_Bator", "Asia/Ulan_Bator"), + ("Asia/Urumqi", "Asia/Urumqi"), + ("Asia/Ust-Nera", "Asia/Ust-Nera"), + ("Asia/Vientiane", "Asia/Vientiane"), + ("Asia/Vladivostok", "Asia/Vladivostok"), + ("Asia/Yakutsk", "Asia/Yakutsk"), + ("Asia/Yangon", "Asia/Yangon"), + ("Asia/Yekaterinburg", "Asia/Yekaterinburg"), + ("Asia/Yerevan", "Asia/Yerevan"), + ("Atlantic/Azores", "Atlantic/Azores"), + ("Atlantic/Bermuda", "Atlantic/Bermuda"), + ("Atlantic/Canary", "Atlantic/Canary"), + ("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"), + ("Atlantic/Faeroe", "Atlantic/Faeroe"), + ("Atlantic/Faroe", "Atlantic/Faroe"), + ("Atlantic/Jan_Mayen", "Atlantic/Jan_Mayen"), + ("Atlantic/Madeira", "Atlantic/Madeira"), + ("Atlantic/Reykjavik", "Atlantic/Reykjavik"), + ("Atlantic/South_Georgia", "Atlantic/South_Georgia"), + ("Atlantic/St_Helena", "Atlantic/St_Helena"), + ("Atlantic/Stanley", "Atlantic/Stanley"), + ("Australia/ACT", "Australia/ACT"), + ("Australia/Adelaide", "Australia/Adelaide"), + ("Australia/Brisbane", "Australia/Brisbane"), + ("Australia/Broken_Hill", "Australia/Broken_Hill"), + ("Australia/Canberra", "Australia/Canberra"), + ("Australia/Currie", "Australia/Currie"), + ("Australia/Darwin", "Australia/Darwin"), + ("Australia/Eucla", "Australia/Eucla"), + ("Australia/Hobart", "Australia/Hobart"), + ("Australia/LHI", "Australia/LHI"), + ("Australia/Lindeman", "Australia/Lindeman"), + ("Australia/Lord_Howe", "Australia/Lord_Howe"), + ("Australia/Melbourne", "Australia/Melbourne"), + ("Australia/NSW", "Australia/NSW"), + ("Australia/North", "Australia/North"), + ("Australia/Perth", "Australia/Perth"), + ("Australia/Queensland", "Australia/Queensland"), + ("Australia/South", "Australia/South"), + ("Australia/Sydney", "Australia/Sydney"), + ("Australia/Tasmania", "Australia/Tasmania"), + ("Australia/Victoria", "Australia/Victoria"), + ("Australia/West", "Australia/West"), + ("Australia/Yancowinna", "Australia/Yancowinna"), + ("Brazil/Acre", "Brazil/Acre"), + ("Brazil/DeNoronha", "Brazil/DeNoronha"), + ("Brazil/East", "Brazil/East"), + ("Brazil/West", "Brazil/West"), + ("CET", "CET"), + ("CST6CDT", "CST6CDT"), + ("Canada/Atlantic", "Canada/Atlantic"), + ("Canada/Central", "Canada/Central"), + ("Canada/Eastern", "Canada/Eastern"), + ("Canada/Mountain", "Canada/Mountain"), + ("Canada/Newfoundland", "Canada/Newfoundland"), + ("Canada/Pacific", "Canada/Pacific"), + ("Canada/Saskatchewan", "Canada/Saskatchewan"), + ("Canada/Yukon", "Canada/Yukon"), + ("Chile/Continental", "Chile/Continental"), + ("Chile/EasterIsland", "Chile/EasterIsland"), + ("Cuba", "Cuba"), + ("EET", "EET"), + ("EST", "EST"), + ("EST5EDT", "EST5EDT"), + ("Egypt", "Egypt"), + ("Eire", "Eire"), + ("Etc/GMT", "Etc/GMT"), + ("Etc/GMT+0", "Etc/GMT+0"), + ("Etc/GMT+1", "Etc/GMT+1"), + ("Etc/GMT+10", "Etc/GMT+10"), + ("Etc/GMT+11", "Etc/GMT+11"), + ("Etc/GMT+12", "Etc/GMT+12"), + ("Etc/GMT+2", "Etc/GMT+2"), + ("Etc/GMT+3", "Etc/GMT+3"), + ("Etc/GMT+4", "Etc/GMT+4"), + ("Etc/GMT+5", "Etc/GMT+5"), + ("Etc/GMT+6", "Etc/GMT+6"), + ("Etc/GMT+7", "Etc/GMT+7"), + ("Etc/GMT+8", "Etc/GMT+8"), + ("Etc/GMT+9", "Etc/GMT+9"), + ("Etc/GMT-0", "Etc/GMT-0"), + ("Etc/GMT-1", "Etc/GMT-1"), + ("Etc/GMT-10", "Etc/GMT-10"), + ("Etc/GMT-11", "Etc/GMT-11"), + ("Etc/GMT-12", "Etc/GMT-12"), + ("Etc/GMT-13", "Etc/GMT-13"), + ("Etc/GMT-14", "Etc/GMT-14"), + ("Etc/GMT-2", "Etc/GMT-2"), + ("Etc/GMT-3", "Etc/GMT-3"), + ("Etc/GMT-4", "Etc/GMT-4"), + ("Etc/GMT-5", "Etc/GMT-5"), + ("Etc/GMT-6", "Etc/GMT-6"), + ("Etc/GMT-7", "Etc/GMT-7"), + ("Etc/GMT-8", "Etc/GMT-8"), + ("Etc/GMT-9", "Etc/GMT-9"), + ("Etc/GMT0", "Etc/GMT0"), + ("Etc/Greenwich", "Etc/Greenwich"), + ("Etc/UCT", "Etc/UCT"), + ("Etc/UTC", "Etc/UTC"), + ("Etc/Universal", "Etc/Universal"), + ("Etc/Zulu", "Etc/Zulu"), + ("Europe/Amsterdam", "Europe/Amsterdam"), + ("Europe/Andorra", "Europe/Andorra"), + ("Europe/Astrakhan", "Europe/Astrakhan"), + ("Europe/Athens", "Europe/Athens"), + ("Europe/Belfast", "Europe/Belfast"), + ("Europe/Belgrade", "Europe/Belgrade"), + ("Europe/Berlin", "Europe/Berlin"), + ("Europe/Bratislava", "Europe/Bratislava"), + ("Europe/Brussels", "Europe/Brussels"), + ("Europe/Bucharest", "Europe/Bucharest"), + ("Europe/Budapest", "Europe/Budapest"), + ("Europe/Busingen", "Europe/Busingen"), + ("Europe/Chisinau", "Europe/Chisinau"), + ("Europe/Copenhagen", "Europe/Copenhagen"), + ("Europe/Dublin", "Europe/Dublin"), + ("Europe/Gibraltar", "Europe/Gibraltar"), + ("Europe/Guernsey", "Europe/Guernsey"), + ("Europe/Helsinki", "Europe/Helsinki"), + ("Europe/Isle_of_Man", "Europe/Isle_of_Man"), + ("Europe/Istanbul", "Europe/Istanbul"), + ("Europe/Jersey", "Europe/Jersey"), + ("Europe/Kaliningrad", "Europe/Kaliningrad"), + ("Europe/Kiev", "Europe/Kiev"), + ("Europe/Kirov", "Europe/Kirov"), + ("Europe/Lisbon", "Europe/Lisbon"), + ("Europe/Ljubljana", "Europe/Ljubljana"), + ("Europe/London", "Europe/London"), + ("Europe/Luxembourg", "Europe/Luxembourg"), + ("Europe/Madrid", "Europe/Madrid"), + ("Europe/Malta", "Europe/Malta"), + ("Europe/Mariehamn", "Europe/Mariehamn"), + ("Europe/Minsk", "Europe/Minsk"), + ("Europe/Monaco", "Europe/Monaco"), + ("Europe/Moscow", "Europe/Moscow"), + ("Europe/Nicosia", "Europe/Nicosia"), + ("Europe/Oslo", "Europe/Oslo"), + ("Europe/Paris", "Europe/Paris"), + ("Europe/Podgorica", "Europe/Podgorica"), + ("Europe/Prague", "Europe/Prague"), + ("Europe/Riga", "Europe/Riga"), + ("Europe/Rome", "Europe/Rome"), + ("Europe/Samara", "Europe/Samara"), + ("Europe/San_Marino", "Europe/San_Marino"), + ("Europe/Sarajevo", "Europe/Sarajevo"), + ("Europe/Saratov", "Europe/Saratov"), + ("Europe/Simferopol", "Europe/Simferopol"), + ("Europe/Skopje", "Europe/Skopje"), + ("Europe/Sofia", "Europe/Sofia"), + ("Europe/Stockholm", "Europe/Stockholm"), + ("Europe/Tallinn", "Europe/Tallinn"), + ("Europe/Tirane", "Europe/Tirane"), + ("Europe/Tiraspol", "Europe/Tiraspol"), + ("Europe/Ulyanovsk", "Europe/Ulyanovsk"), + ("Europe/Uzhgorod", "Europe/Uzhgorod"), + ("Europe/Vaduz", "Europe/Vaduz"), + ("Europe/Vatican", "Europe/Vatican"), + ("Europe/Vienna", "Europe/Vienna"), + ("Europe/Vilnius", "Europe/Vilnius"), + ("Europe/Volgograd", "Europe/Volgograd"), + ("Europe/Warsaw", "Europe/Warsaw"), + ("Europe/Zagreb", "Europe/Zagreb"), + ("Europe/Zaporozhye", "Europe/Zaporozhye"), + ("Europe/Zurich", "Europe/Zurich"), + ("GB", "GB"), + ("GB-Eire", "GB-Eire"), + ("GMT", "GMT"), + ("GMT+0", "GMT+0"), + ("GMT-0", "GMT-0"), + ("GMT0", "GMT0"), + ("Greenwich", "Greenwich"), + ("HST", "HST"), + ("Hongkong", "Hongkong"), + ("Iceland", "Iceland"), + ("Indian/Antananarivo", "Indian/Antananarivo"), + ("Indian/Chagos", "Indian/Chagos"), + ("Indian/Christmas", "Indian/Christmas"), + ("Indian/Cocos", "Indian/Cocos"), + ("Indian/Comoro", "Indian/Comoro"), + ("Indian/Kerguelen", "Indian/Kerguelen"), + ("Indian/Mahe", "Indian/Mahe"), + ("Indian/Maldives", "Indian/Maldives"), + ("Indian/Mauritius", "Indian/Mauritius"), + ("Indian/Mayotte", "Indian/Mayotte"), + ("Indian/Reunion", "Indian/Reunion"), + ("Iran", "Iran"), + ("Israel", "Israel"), + ("Jamaica", "Jamaica"), + ("Japan", "Japan"), + ("Kwajalein", "Kwajalein"), + ("Libya", "Libya"), + ("MET", "MET"), + ("MST", "MST"), + ("MST7MDT", "MST7MDT"), + ("Mexico/BajaNorte", "Mexico/BajaNorte"), + ("Mexico/BajaSur", "Mexico/BajaSur"), + ("Mexico/General", "Mexico/General"), + ("NZ", "NZ"), + ("NZ-CHAT", "NZ-CHAT"), + ("Navajo", "Navajo"), + ("PRC", "PRC"), + ("PST8PDT", "PST8PDT"), + ("Pacific/Apia", "Pacific/Apia"), + ("Pacific/Auckland", "Pacific/Auckland"), + ("Pacific/Bougainville", "Pacific/Bougainville"), + ("Pacific/Chatham", "Pacific/Chatham"), + ("Pacific/Chuuk", "Pacific/Chuuk"), + ("Pacific/Easter", "Pacific/Easter"), + ("Pacific/Efate", "Pacific/Efate"), + ("Pacific/Enderbury", "Pacific/Enderbury"), + ("Pacific/Fakaofo", "Pacific/Fakaofo"), + ("Pacific/Fiji", "Pacific/Fiji"), + ("Pacific/Funafuti", "Pacific/Funafuti"), + ("Pacific/Galapagos", "Pacific/Galapagos"), + ("Pacific/Gambier", "Pacific/Gambier"), + ("Pacific/Guadalcanal", "Pacific/Guadalcanal"), + ("Pacific/Guam", "Pacific/Guam"), + ("Pacific/Honolulu", "Pacific/Honolulu"), + ("Pacific/Johnston", "Pacific/Johnston"), + ("Pacific/Kiritimati", "Pacific/Kiritimati"), + ("Pacific/Kosrae", "Pacific/Kosrae"), + ("Pacific/Kwajalein", "Pacific/Kwajalein"), + ("Pacific/Majuro", "Pacific/Majuro"), + ("Pacific/Marquesas", "Pacific/Marquesas"), + ("Pacific/Midway", "Pacific/Midway"), + ("Pacific/Nauru", "Pacific/Nauru"), + ("Pacific/Niue", "Pacific/Niue"), + ("Pacific/Norfolk", "Pacific/Norfolk"), + ("Pacific/Noumea", "Pacific/Noumea"), + ("Pacific/Pago_Pago", "Pacific/Pago_Pago"), + ("Pacific/Palau", "Pacific/Palau"), + ("Pacific/Pitcairn", "Pacific/Pitcairn"), + ("Pacific/Pohnpei", "Pacific/Pohnpei"), + ("Pacific/Ponape", "Pacific/Ponape"), + ("Pacific/Port_Moresby", "Pacific/Port_Moresby"), + ("Pacific/Rarotonga", "Pacific/Rarotonga"), + ("Pacific/Saipan", "Pacific/Saipan"), + ("Pacific/Samoa", "Pacific/Samoa"), + ("Pacific/Tahiti", "Pacific/Tahiti"), + ("Pacific/Tarawa", "Pacific/Tarawa"), + ("Pacific/Tongatapu", "Pacific/Tongatapu"), + ("Pacific/Truk", "Pacific/Truk"), + ("Pacific/Wake", "Pacific/Wake"), + ("Pacific/Wallis", "Pacific/Wallis"), + ("Pacific/Yap", "Pacific/Yap"), + ("Poland", "Poland"), + ("Portugal", "Portugal"), + ("ROC", "ROC"), + ("ROK", "ROK"), + ("Singapore", "Singapore"), + ("Turkey", "Turkey"), + ("UCT", "UCT"), + ("US/Alaska", "US/Alaska"), + ("US/Aleutian", "US/Aleutian"), + ("US/Arizona", "US/Arizona"), + ("US/Central", "US/Central"), + ("US/East-Indiana", "US/East-Indiana"), + ("US/Eastern", "US/Eastern"), + ("US/Hawaii", "US/Hawaii"), + ("US/Indiana-Starke", "US/Indiana-Starke"), + ("US/Michigan", "US/Michigan"), + ("US/Mountain", "US/Mountain"), + ("US/Pacific", "US/Pacific"), + ("US/Samoa", "US/Samoa"), + ("UTC", "UTC"), + ("Universal", "Universal"), + ("W-SU", "W-SU"), + ("WET", "WET"), + ("Zulu", "Zulu"), + ], + default="UTC", + max_length=100, + ), + ), + ("last_suceeded", models.DateTimeField(blank=True, null=True)), ("succeeded", models.BooleanField(default=False)), + ("error", models.CharField(blank=True, max_length=255, null=True)), ], + options={"abstract": False}, ) ] diff --git a/src/newsreader/news/collection/migrations/0002_auto_20190410_2028.py b/src/newsreader/news/collection/migrations/0002_auto_20190410_2028.py deleted file mode 100644 index ce45e0e..0000000 --- a/src/newsreader/news/collection/migrations/0002_auto_20190410_2028.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 2.2 on 2019-04-10 20:28 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [("collection", "0001_initial")] - - operations = [ - migrations.AlterField( - model_name="collectionrule", - name="last_suceeded", - field=models.DateTimeField(blank=True, null=True), - ) - ] diff --git a/src/newsreader/news/collection/migrations/0003_collectionrule_category.py b/src/newsreader/news/collection/migrations/0002_collectionrule_category.py similarity index 74% rename from src/newsreader/news/collection/migrations/0003_collectionrule_category.py rename to src/newsreader/news/collection/migrations/0002_collectionrule_category.py index b15f7f7..2cb1ee4 100644 --- a/src/newsreader/news/collection/migrations/0003_collectionrule_category.py +++ b/src/newsreader/news/collection/migrations/0002_collectionrule_category.py @@ -1,13 +1,14 @@ -# Generated by Django 2.2 on 2019-05-20 20:06 - -import django.db.models.deletion +# Generated by Django 2.2 on 2019-07-05 20:59 from django.db import migrations, models +import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [("posts", "0002_auto_20190520_2206"), ("collection", "0002_auto_20190410_2028")] + initial = True + + dependencies = [("collection", "0001_initial"), ("core", "0001_initial")] operations = [ migrations.AddField( @@ -18,7 +19,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, - to="posts.Category", + to="core.Category", verbose_name="Category", ), ) diff --git a/src/newsreader/news/collection/migrations/0003_collectionrule_user.py b/src/newsreader/news/collection/migrations/0003_collectionrule_user.py new file mode 100644 index 0000000..a735ea3 --- /dev/null +++ b/src/newsreader/news/collection/migrations/0003_collectionrule_user.py @@ -0,0 +1,21 @@ +# 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, + ) + ] diff --git a/src/newsreader/news/collection/migrations/0004_collectionrule_timezone.py b/src/newsreader/news/collection/migrations/0004_collectionrule_timezone.py deleted file mode 100644 index 837e625..0000000 --- a/src/newsreader/news/collection/migrations/0004_collectionrule_timezone.py +++ /dev/null @@ -1,613 +0,0 @@ -# Generated by Django 2.2 on 2019-05-20 20:41 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [("collection", "0003_collectionrule_category")] - - operations = [ - migrations.AddField( - model_name="collectionrule", - name="timezone", - field=models.CharField( - choices=[ - ("Africa/Abidjan", "Africa/Abidjan"), - ("Africa/Accra", "Africa/Accra"), - ("Africa/Addis_Ababa", "Africa/Addis_Ababa"), - ("Africa/Algiers", "Africa/Algiers"), - ("Africa/Asmara", "Africa/Asmara"), - ("Africa/Asmera", "Africa/Asmera"), - ("Africa/Bamako", "Africa/Bamako"), - ("Africa/Bangui", "Africa/Bangui"), - ("Africa/Banjul", "Africa/Banjul"), - ("Africa/Bissau", "Africa/Bissau"), - ("Africa/Blantyre", "Africa/Blantyre"), - ("Africa/Brazzaville", "Africa/Brazzaville"), - ("Africa/Bujumbura", "Africa/Bujumbura"), - ("Africa/Cairo", "Africa/Cairo"), - ("Africa/Casablanca", "Africa/Casablanca"), - ("Africa/Ceuta", "Africa/Ceuta"), - ("Africa/Conakry", "Africa/Conakry"), - ("Africa/Dakar", "Africa/Dakar"), - ("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"), - ("Africa/Djibouti", "Africa/Djibouti"), - ("Africa/Douala", "Africa/Douala"), - ("Africa/El_Aaiun", "Africa/El_Aaiun"), - ("Africa/Freetown", "Africa/Freetown"), - ("Africa/Gaborone", "Africa/Gaborone"), - ("Africa/Harare", "Africa/Harare"), - ("Africa/Johannesburg", "Africa/Johannesburg"), - ("Africa/Juba", "Africa/Juba"), - ("Africa/Kampala", "Africa/Kampala"), - ("Africa/Khartoum", "Africa/Khartoum"), - ("Africa/Kigali", "Africa/Kigali"), - ("Africa/Kinshasa", "Africa/Kinshasa"), - ("Africa/Lagos", "Africa/Lagos"), - ("Africa/Libreville", "Africa/Libreville"), - ("Africa/Lome", "Africa/Lome"), - ("Africa/Luanda", "Africa/Luanda"), - ("Africa/Lubumbashi", "Africa/Lubumbashi"), - ("Africa/Lusaka", "Africa/Lusaka"), - ("Africa/Malabo", "Africa/Malabo"), - ("Africa/Maputo", "Africa/Maputo"), - ("Africa/Maseru", "Africa/Maseru"), - ("Africa/Mbabane", "Africa/Mbabane"), - ("Africa/Mogadishu", "Africa/Mogadishu"), - ("Africa/Monrovia", "Africa/Monrovia"), - ("Africa/Nairobi", "Africa/Nairobi"), - ("Africa/Ndjamena", "Africa/Ndjamena"), - ("Africa/Niamey", "Africa/Niamey"), - ("Africa/Nouakchott", "Africa/Nouakchott"), - ("Africa/Ouagadougou", "Africa/Ouagadougou"), - ("Africa/Porto-Novo", "Africa/Porto-Novo"), - ("Africa/Sao_Tome", "Africa/Sao_Tome"), - ("Africa/Timbuktu", "Africa/Timbuktu"), - ("Africa/Tripoli", "Africa/Tripoli"), - ("Africa/Tunis", "Africa/Tunis"), - ("Africa/Windhoek", "Africa/Windhoek"), - ("America/Adak", "America/Adak"), - ("America/Anchorage", "America/Anchorage"), - ("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/ComodRivadavia", "America/Argentina/ComodRivadavia"), - ("America/Argentina/Cordoba", "America/Argentina/Cordoba"), - ("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/Salta", "America/Argentina/Salta"), - ("America/Argentina/San_Juan", "America/Argentina/San_Juan"), - ("America/Argentina/San_Luis", "America/Argentina/San_Luis"), - ("America/Argentina/Tucuman", "America/Argentina/Tucuman"), - ("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"), - ("America/Aruba", "America/Aruba"), - ("America/Asuncion", "America/Asuncion"), - ("America/Atikokan", "America/Atikokan"), - ("America/Atka", "America/Atka"), - ("America/Bahia", "America/Bahia"), - ("America/Bahia_Banderas", "America/Bahia_Banderas"), - ("America/Barbados", "America/Barbados"), - ("America/Belem", "America/Belem"), - ("America/Belize", "America/Belize"), - ("America/Blanc-Sablon", "America/Blanc-Sablon"), - ("America/Boa_Vista", "America/Boa_Vista"), - ("America/Bogota", "America/Bogota"), - ("America/Boise", "America/Boise"), - ("America/Buenos_Aires", "America/Buenos_Aires"), - ("America/Cambridge_Bay", "America/Cambridge_Bay"), - ("America/Campo_Grande", "America/Campo_Grande"), - ("America/Cancun", "America/Cancun"), - ("America/Caracas", "America/Caracas"), - ("America/Catamarca", "America/Catamarca"), - ("America/Cayenne", "America/Cayenne"), - ("America/Cayman", "America/Cayman"), - ("America/Chicago", "America/Chicago"), - ("America/Chihuahua", "America/Chihuahua"), - ("America/Coral_Harbour", "America/Coral_Harbour"), - ("America/Cordoba", "America/Cordoba"), - ("America/Costa_Rica", "America/Costa_Rica"), - ("America/Creston", "America/Creston"), - ("America/Cuiaba", "America/Cuiaba"), - ("America/Curacao", "America/Curacao"), - ("America/Danmarkshavn", "America/Danmarkshavn"), - ("America/Dawson", "America/Dawson"), - ("America/Dawson_Creek", "America/Dawson_Creek"), - ("America/Denver", "America/Denver"), - ("America/Detroit", "America/Detroit"), - ("America/Dominica", "America/Dominica"), - ("America/Edmonton", "America/Edmonton"), - ("America/Eirunepe", "America/Eirunepe"), - ("America/El_Salvador", "America/El_Salvador"), - ("America/Ensenada", "America/Ensenada"), - ("America/Fort_Nelson", "America/Fort_Nelson"), - ("America/Fort_Wayne", "America/Fort_Wayne"), - ("America/Fortaleza", "America/Fortaleza"), - ("America/Glace_Bay", "America/Glace_Bay"), - ("America/Godthab", "America/Godthab"), - ("America/Goose_Bay", "America/Goose_Bay"), - ("America/Grand_Turk", "America/Grand_Turk"), - ("America/Grenada", "America/Grenada"), - ("America/Guadeloupe", "America/Guadeloupe"), - ("America/Guatemala", "America/Guatemala"), - ("America/Guayaquil", "America/Guayaquil"), - ("America/Guyana", "America/Guyana"), - ("America/Halifax", "America/Halifax"), - ("America/Havana", "America/Havana"), - ("America/Hermosillo", "America/Hermosillo"), - ("America/Indiana/Indianapolis", "America/Indiana/Indianapolis"), - ("America/Indiana/Knox", "America/Indiana/Knox"), - ("America/Indiana/Marengo", "America/Indiana/Marengo"), - ("America/Indiana/Petersburg", "America/Indiana/Petersburg"), - ("America/Indiana/Tell_City", "America/Indiana/Tell_City"), - ("America/Indiana/Vevay", "America/Indiana/Vevay"), - ("America/Indiana/Vincennes", "America/Indiana/Vincennes"), - ("America/Indiana/Winamac", "America/Indiana/Winamac"), - ("America/Indianapolis", "America/Indianapolis"), - ("America/Inuvik", "America/Inuvik"), - ("America/Iqaluit", "America/Iqaluit"), - ("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/Knox_IN", "America/Knox_IN"), - ("America/Kralendijk", "America/Kralendijk"), - ("America/La_Paz", "America/La_Paz"), - ("America/Lima", "America/Lima"), - ("America/Los_Angeles", "America/Los_Angeles"), - ("America/Louisville", "America/Louisville"), - ("America/Lower_Princes", "America/Lower_Princes"), - ("America/Maceio", "America/Maceio"), - ("America/Managua", "America/Managua"), - ("America/Manaus", "America/Manaus"), - ("America/Marigot", "America/Marigot"), - ("America/Martinique", "America/Martinique"), - ("America/Matamoros", "America/Matamoros"), - ("America/Mazatlan", "America/Mazatlan"), - ("America/Mendoza", "America/Mendoza"), - ("America/Menominee", "America/Menominee"), - ("America/Merida", "America/Merida"), - ("America/Metlakatla", "America/Metlakatla"), - ("America/Mexico_City", "America/Mexico_City"), - ("America/Miquelon", "America/Miquelon"), - ("America/Moncton", "America/Moncton"), - ("America/Monterrey", "America/Monterrey"), - ("America/Montevideo", "America/Montevideo"), - ("America/Montreal", "America/Montreal"), - ("America/Montserrat", "America/Montserrat"), - ("America/Nassau", "America/Nassau"), - ("America/New_York", "America/New_York"), - ("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/Ojinaga", "America/Ojinaga"), - ("America/Panama", "America/Panama"), - ("America/Pangnirtung", "America/Pangnirtung"), - ("America/Paramaribo", "America/Paramaribo"), - ("America/Phoenix", "America/Phoenix"), - ("America/Port-au-Prince", "America/Port-au-Prince"), - ("America/Port_of_Spain", "America/Port_of_Spain"), - ("America/Porto_Acre", "America/Porto_Acre"), - ("America/Porto_Velho", "America/Porto_Velho"), - ("America/Puerto_Rico", "America/Puerto_Rico"), - ("America/Punta_Arenas", "America/Punta_Arenas"), - ("America/Rainy_River", "America/Rainy_River"), - ("America/Rankin_Inlet", "America/Rankin_Inlet"), - ("America/Recife", "America/Recife"), - ("America/Regina", "America/Regina"), - ("America/Resolute", "America/Resolute"), - ("America/Rio_Branco", "America/Rio_Branco"), - ("America/Rosario", "America/Rosario"), - ("America/Santa_Isabel", "America/Santa_Isabel"), - ("America/Santarem", "America/Santarem"), - ("America/Santiago", "America/Santiago"), - ("America/Santo_Domingo", "America/Santo_Domingo"), - ("America/Sao_Paulo", "America/Sao_Paulo"), - ("America/Scoresbysund", "America/Scoresbysund"), - ("America/Shiprock", "America/Shiprock"), - ("America/Sitka", "America/Sitka"), - ("America/St_Barthelemy", "America/St_Barthelemy"), - ("America/St_Johns", "America/St_Johns"), - ("America/St_Kitts", "America/St_Kitts"), - ("America/St_Lucia", "America/St_Lucia"), - ("America/St_Thomas", "America/St_Thomas"), - ("America/St_Vincent", "America/St_Vincent"), - ("America/Swift_Current", "America/Swift_Current"), - ("America/Tegucigalpa", "America/Tegucigalpa"), - ("America/Thule", "America/Thule"), - ("America/Thunder_Bay", "America/Thunder_Bay"), - ("America/Tijuana", "America/Tijuana"), - ("America/Toronto", "America/Toronto"), - ("America/Tortola", "America/Tortola"), - ("America/Vancouver", "America/Vancouver"), - ("America/Virgin", "America/Virgin"), - ("America/Whitehorse", "America/Whitehorse"), - ("America/Winnipeg", "America/Winnipeg"), - ("America/Yakutat", "America/Yakutat"), - ("America/Yellowknife", "America/Yellowknife"), - ("Antarctica/Casey", "Antarctica/Casey"), - ("Antarctica/Davis", "Antarctica/Davis"), - ("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"), - ("Antarctica/Macquarie", "Antarctica/Macquarie"), - ("Antarctica/Mawson", "Antarctica/Mawson"), - ("Antarctica/McMurdo", "Antarctica/McMurdo"), - ("Antarctica/Palmer", "Antarctica/Palmer"), - ("Antarctica/Rothera", "Antarctica/Rothera"), - ("Antarctica/South_Pole", "Antarctica/South_Pole"), - ("Antarctica/Syowa", "Antarctica/Syowa"), - ("Antarctica/Troll", "Antarctica/Troll"), - ("Antarctica/Vostok", "Antarctica/Vostok"), - ("Arctic/Longyearbyen", "Arctic/Longyearbyen"), - ("Asia/Aden", "Asia/Aden"), - ("Asia/Almaty", "Asia/Almaty"), - ("Asia/Amman", "Asia/Amman"), - ("Asia/Anadyr", "Asia/Anadyr"), - ("Asia/Aqtau", "Asia/Aqtau"), - ("Asia/Aqtobe", "Asia/Aqtobe"), - ("Asia/Ashgabat", "Asia/Ashgabat"), - ("Asia/Ashkhabad", "Asia/Ashkhabad"), - ("Asia/Atyrau", "Asia/Atyrau"), - ("Asia/Baghdad", "Asia/Baghdad"), - ("Asia/Bahrain", "Asia/Bahrain"), - ("Asia/Baku", "Asia/Baku"), - ("Asia/Bangkok", "Asia/Bangkok"), - ("Asia/Barnaul", "Asia/Barnaul"), - ("Asia/Beirut", "Asia/Beirut"), - ("Asia/Bishkek", "Asia/Bishkek"), - ("Asia/Brunei", "Asia/Brunei"), - ("Asia/Calcutta", "Asia/Calcutta"), - ("Asia/Chita", "Asia/Chita"), - ("Asia/Choibalsan", "Asia/Choibalsan"), - ("Asia/Chongqing", "Asia/Chongqing"), - ("Asia/Chungking", "Asia/Chungking"), - ("Asia/Colombo", "Asia/Colombo"), - ("Asia/Dacca", "Asia/Dacca"), - ("Asia/Damascus", "Asia/Damascus"), - ("Asia/Dhaka", "Asia/Dhaka"), - ("Asia/Dili", "Asia/Dili"), - ("Asia/Dubai", "Asia/Dubai"), - ("Asia/Dushanbe", "Asia/Dushanbe"), - ("Asia/Famagusta", "Asia/Famagusta"), - ("Asia/Gaza", "Asia/Gaza"), - ("Asia/Harbin", "Asia/Harbin"), - ("Asia/Hebron", "Asia/Hebron"), - ("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"), - ("Asia/Hong_Kong", "Asia/Hong_Kong"), - ("Asia/Hovd", "Asia/Hovd"), - ("Asia/Irkutsk", "Asia/Irkutsk"), - ("Asia/Istanbul", "Asia/Istanbul"), - ("Asia/Jakarta", "Asia/Jakarta"), - ("Asia/Jayapura", "Asia/Jayapura"), - ("Asia/Jerusalem", "Asia/Jerusalem"), - ("Asia/Kabul", "Asia/Kabul"), - ("Asia/Kamchatka", "Asia/Kamchatka"), - ("Asia/Karachi", "Asia/Karachi"), - ("Asia/Kashgar", "Asia/Kashgar"), - ("Asia/Kathmandu", "Asia/Kathmandu"), - ("Asia/Katmandu", "Asia/Katmandu"), - ("Asia/Khandyga", "Asia/Khandyga"), - ("Asia/Kolkata", "Asia/Kolkata"), - ("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"), - ("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"), - ("Asia/Kuching", "Asia/Kuching"), - ("Asia/Kuwait", "Asia/Kuwait"), - ("Asia/Macao", "Asia/Macao"), - ("Asia/Macau", "Asia/Macau"), - ("Asia/Magadan", "Asia/Magadan"), - ("Asia/Makassar", "Asia/Makassar"), - ("Asia/Manila", "Asia/Manila"), - ("Asia/Muscat", "Asia/Muscat"), - ("Asia/Nicosia", "Asia/Nicosia"), - ("Asia/Novokuznetsk", "Asia/Novokuznetsk"), - ("Asia/Novosibirsk", "Asia/Novosibirsk"), - ("Asia/Omsk", "Asia/Omsk"), - ("Asia/Oral", "Asia/Oral"), - ("Asia/Phnom_Penh", "Asia/Phnom_Penh"), - ("Asia/Pontianak", "Asia/Pontianak"), - ("Asia/Pyongyang", "Asia/Pyongyang"), - ("Asia/Qatar", "Asia/Qatar"), - ("Asia/Qostanay", "Asia/Qostanay"), - ("Asia/Qyzylorda", "Asia/Qyzylorda"), - ("Asia/Rangoon", "Asia/Rangoon"), - ("Asia/Riyadh", "Asia/Riyadh"), - ("Asia/Saigon", "Asia/Saigon"), - ("Asia/Sakhalin", "Asia/Sakhalin"), - ("Asia/Samarkand", "Asia/Samarkand"), - ("Asia/Seoul", "Asia/Seoul"), - ("Asia/Shanghai", "Asia/Shanghai"), - ("Asia/Singapore", "Asia/Singapore"), - ("Asia/Srednekolymsk", "Asia/Srednekolymsk"), - ("Asia/Taipei", "Asia/Taipei"), - ("Asia/Tashkent", "Asia/Tashkent"), - ("Asia/Tbilisi", "Asia/Tbilisi"), - ("Asia/Tehran", "Asia/Tehran"), - ("Asia/Tel_Aviv", "Asia/Tel_Aviv"), - ("Asia/Thimbu", "Asia/Thimbu"), - ("Asia/Thimphu", "Asia/Thimphu"), - ("Asia/Tokyo", "Asia/Tokyo"), - ("Asia/Tomsk", "Asia/Tomsk"), - ("Asia/Ujung_Pandang", "Asia/Ujung_Pandang"), - ("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"), - ("Asia/Ulan_Bator", "Asia/Ulan_Bator"), - ("Asia/Urumqi", "Asia/Urumqi"), - ("Asia/Ust-Nera", "Asia/Ust-Nera"), - ("Asia/Vientiane", "Asia/Vientiane"), - ("Asia/Vladivostok", "Asia/Vladivostok"), - ("Asia/Yakutsk", "Asia/Yakutsk"), - ("Asia/Yangon", "Asia/Yangon"), - ("Asia/Yekaterinburg", "Asia/Yekaterinburg"), - ("Asia/Yerevan", "Asia/Yerevan"), - ("Atlantic/Azores", "Atlantic/Azores"), - ("Atlantic/Bermuda", "Atlantic/Bermuda"), - ("Atlantic/Canary", "Atlantic/Canary"), - ("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"), - ("Atlantic/Faeroe", "Atlantic/Faeroe"), - ("Atlantic/Faroe", "Atlantic/Faroe"), - ("Atlantic/Jan_Mayen", "Atlantic/Jan_Mayen"), - ("Atlantic/Madeira", "Atlantic/Madeira"), - ("Atlantic/Reykjavik", "Atlantic/Reykjavik"), - ("Atlantic/South_Georgia", "Atlantic/South_Georgia"), - ("Atlantic/St_Helena", "Atlantic/St_Helena"), - ("Atlantic/Stanley", "Atlantic/Stanley"), - ("Australia/ACT", "Australia/ACT"), - ("Australia/Adelaide", "Australia/Adelaide"), - ("Australia/Brisbane", "Australia/Brisbane"), - ("Australia/Broken_Hill", "Australia/Broken_Hill"), - ("Australia/Canberra", "Australia/Canberra"), - ("Australia/Currie", "Australia/Currie"), - ("Australia/Darwin", "Australia/Darwin"), - ("Australia/Eucla", "Australia/Eucla"), - ("Australia/Hobart", "Australia/Hobart"), - ("Australia/LHI", "Australia/LHI"), - ("Australia/Lindeman", "Australia/Lindeman"), - ("Australia/Lord_Howe", "Australia/Lord_Howe"), - ("Australia/Melbourne", "Australia/Melbourne"), - ("Australia/NSW", "Australia/NSW"), - ("Australia/North", "Australia/North"), - ("Australia/Perth", "Australia/Perth"), - ("Australia/Queensland", "Australia/Queensland"), - ("Australia/South", "Australia/South"), - ("Australia/Sydney", "Australia/Sydney"), - ("Australia/Tasmania", "Australia/Tasmania"), - ("Australia/Victoria", "Australia/Victoria"), - ("Australia/West", "Australia/West"), - ("Australia/Yancowinna", "Australia/Yancowinna"), - ("Brazil/Acre", "Brazil/Acre"), - ("Brazil/DeNoronha", "Brazil/DeNoronha"), - ("Brazil/East", "Brazil/East"), - ("Brazil/West", "Brazil/West"), - ("CET", "CET"), - ("CST6CDT", "CST6CDT"), - ("Canada/Atlantic", "Canada/Atlantic"), - ("Canada/Central", "Canada/Central"), - ("Canada/Eastern", "Canada/Eastern"), - ("Canada/Mountain", "Canada/Mountain"), - ("Canada/Newfoundland", "Canada/Newfoundland"), - ("Canada/Pacific", "Canada/Pacific"), - ("Canada/Saskatchewan", "Canada/Saskatchewan"), - ("Canada/Yukon", "Canada/Yukon"), - ("Chile/Continental", "Chile/Continental"), - ("Chile/EasterIsland", "Chile/EasterIsland"), - ("Cuba", "Cuba"), - ("EET", "EET"), - ("EST", "EST"), - ("EST5EDT", "EST5EDT"), - ("Egypt", "Egypt"), - ("Eire", "Eire"), - ("Etc/GMT", "Etc/GMT"), - ("Etc/GMT+0", "Etc/GMT+0"), - ("Etc/GMT+1", "Etc/GMT+1"), - ("Etc/GMT+10", "Etc/GMT+10"), - ("Etc/GMT+11", "Etc/GMT+11"), - ("Etc/GMT+12", "Etc/GMT+12"), - ("Etc/GMT+2", "Etc/GMT+2"), - ("Etc/GMT+3", "Etc/GMT+3"), - ("Etc/GMT+4", "Etc/GMT+4"), - ("Etc/GMT+5", "Etc/GMT+5"), - ("Etc/GMT+6", "Etc/GMT+6"), - ("Etc/GMT+7", "Etc/GMT+7"), - ("Etc/GMT+8", "Etc/GMT+8"), - ("Etc/GMT+9", "Etc/GMT+9"), - ("Etc/GMT-0", "Etc/GMT-0"), - ("Etc/GMT-1", "Etc/GMT-1"), - ("Etc/GMT-10", "Etc/GMT-10"), - ("Etc/GMT-11", "Etc/GMT-11"), - ("Etc/GMT-12", "Etc/GMT-12"), - ("Etc/GMT-13", "Etc/GMT-13"), - ("Etc/GMT-14", "Etc/GMT-14"), - ("Etc/GMT-2", "Etc/GMT-2"), - ("Etc/GMT-3", "Etc/GMT-3"), - ("Etc/GMT-4", "Etc/GMT-4"), - ("Etc/GMT-5", "Etc/GMT-5"), - ("Etc/GMT-6", "Etc/GMT-6"), - ("Etc/GMT-7", "Etc/GMT-7"), - ("Etc/GMT-8", "Etc/GMT-8"), - ("Etc/GMT-9", "Etc/GMT-9"), - ("Etc/GMT0", "Etc/GMT0"), - ("Etc/Greenwich", "Etc/Greenwich"), - ("Etc/UCT", "Etc/UCT"), - ("Etc/UTC", "Etc/UTC"), - ("Etc/Universal", "Etc/Universal"), - ("Etc/Zulu", "Etc/Zulu"), - ("Europe/Amsterdam", "Europe/Amsterdam"), - ("Europe/Andorra", "Europe/Andorra"), - ("Europe/Astrakhan", "Europe/Astrakhan"), - ("Europe/Athens", "Europe/Athens"), - ("Europe/Belfast", "Europe/Belfast"), - ("Europe/Belgrade", "Europe/Belgrade"), - ("Europe/Berlin", "Europe/Berlin"), - ("Europe/Bratislava", "Europe/Bratislava"), - ("Europe/Brussels", "Europe/Brussels"), - ("Europe/Bucharest", "Europe/Bucharest"), - ("Europe/Budapest", "Europe/Budapest"), - ("Europe/Busingen", "Europe/Busingen"), - ("Europe/Chisinau", "Europe/Chisinau"), - ("Europe/Copenhagen", "Europe/Copenhagen"), - ("Europe/Dublin", "Europe/Dublin"), - ("Europe/Gibraltar", "Europe/Gibraltar"), - ("Europe/Guernsey", "Europe/Guernsey"), - ("Europe/Helsinki", "Europe/Helsinki"), - ("Europe/Isle_of_Man", "Europe/Isle_of_Man"), - ("Europe/Istanbul", "Europe/Istanbul"), - ("Europe/Jersey", "Europe/Jersey"), - ("Europe/Kaliningrad", "Europe/Kaliningrad"), - ("Europe/Kiev", "Europe/Kiev"), - ("Europe/Kirov", "Europe/Kirov"), - ("Europe/Lisbon", "Europe/Lisbon"), - ("Europe/Ljubljana", "Europe/Ljubljana"), - ("Europe/London", "Europe/London"), - ("Europe/Luxembourg", "Europe/Luxembourg"), - ("Europe/Madrid", "Europe/Madrid"), - ("Europe/Malta", "Europe/Malta"), - ("Europe/Mariehamn", "Europe/Mariehamn"), - ("Europe/Minsk", "Europe/Minsk"), - ("Europe/Monaco", "Europe/Monaco"), - ("Europe/Moscow", "Europe/Moscow"), - ("Europe/Nicosia", "Europe/Nicosia"), - ("Europe/Oslo", "Europe/Oslo"), - ("Europe/Paris", "Europe/Paris"), - ("Europe/Podgorica", "Europe/Podgorica"), - ("Europe/Prague", "Europe/Prague"), - ("Europe/Riga", "Europe/Riga"), - ("Europe/Rome", "Europe/Rome"), - ("Europe/Samara", "Europe/Samara"), - ("Europe/San_Marino", "Europe/San_Marino"), - ("Europe/Sarajevo", "Europe/Sarajevo"), - ("Europe/Saratov", "Europe/Saratov"), - ("Europe/Simferopol", "Europe/Simferopol"), - ("Europe/Skopje", "Europe/Skopje"), - ("Europe/Sofia", "Europe/Sofia"), - ("Europe/Stockholm", "Europe/Stockholm"), - ("Europe/Tallinn", "Europe/Tallinn"), - ("Europe/Tirane", "Europe/Tirane"), - ("Europe/Tiraspol", "Europe/Tiraspol"), - ("Europe/Ulyanovsk", "Europe/Ulyanovsk"), - ("Europe/Uzhgorod", "Europe/Uzhgorod"), - ("Europe/Vaduz", "Europe/Vaduz"), - ("Europe/Vatican", "Europe/Vatican"), - ("Europe/Vienna", "Europe/Vienna"), - ("Europe/Vilnius", "Europe/Vilnius"), - ("Europe/Volgograd", "Europe/Volgograd"), - ("Europe/Warsaw", "Europe/Warsaw"), - ("Europe/Zagreb", "Europe/Zagreb"), - ("Europe/Zaporozhye", "Europe/Zaporozhye"), - ("Europe/Zurich", "Europe/Zurich"), - ("GB", "GB"), - ("GB-Eire", "GB-Eire"), - ("GMT", "GMT"), - ("GMT+0", "GMT+0"), - ("GMT-0", "GMT-0"), - ("GMT0", "GMT0"), - ("Greenwich", "Greenwich"), - ("HST", "HST"), - ("Hongkong", "Hongkong"), - ("Iceland", "Iceland"), - ("Indian/Antananarivo", "Indian/Antananarivo"), - ("Indian/Chagos", "Indian/Chagos"), - ("Indian/Christmas", "Indian/Christmas"), - ("Indian/Cocos", "Indian/Cocos"), - ("Indian/Comoro", "Indian/Comoro"), - ("Indian/Kerguelen", "Indian/Kerguelen"), - ("Indian/Mahe", "Indian/Mahe"), - ("Indian/Maldives", "Indian/Maldives"), - ("Indian/Mauritius", "Indian/Mauritius"), - ("Indian/Mayotte", "Indian/Mayotte"), - ("Indian/Reunion", "Indian/Reunion"), - ("Iran", "Iran"), - ("Israel", "Israel"), - ("Jamaica", "Jamaica"), - ("Japan", "Japan"), - ("Kwajalein", "Kwajalein"), - ("Libya", "Libya"), - ("MET", "MET"), - ("MST", "MST"), - ("MST7MDT", "MST7MDT"), - ("Mexico/BajaNorte", "Mexico/BajaNorte"), - ("Mexico/BajaSur", "Mexico/BajaSur"), - ("Mexico/General", "Mexico/General"), - ("NZ", "NZ"), - ("NZ-CHAT", "NZ-CHAT"), - ("Navajo", "Navajo"), - ("PRC", "PRC"), - ("PST8PDT", "PST8PDT"), - ("Pacific/Apia", "Pacific/Apia"), - ("Pacific/Auckland", "Pacific/Auckland"), - ("Pacific/Bougainville", "Pacific/Bougainville"), - ("Pacific/Chatham", "Pacific/Chatham"), - ("Pacific/Chuuk", "Pacific/Chuuk"), - ("Pacific/Easter", "Pacific/Easter"), - ("Pacific/Efate", "Pacific/Efate"), - ("Pacific/Enderbury", "Pacific/Enderbury"), - ("Pacific/Fakaofo", "Pacific/Fakaofo"), - ("Pacific/Fiji", "Pacific/Fiji"), - ("Pacific/Funafuti", "Pacific/Funafuti"), - ("Pacific/Galapagos", "Pacific/Galapagos"), - ("Pacific/Gambier", "Pacific/Gambier"), - ("Pacific/Guadalcanal", "Pacific/Guadalcanal"), - ("Pacific/Guam", "Pacific/Guam"), - ("Pacific/Honolulu", "Pacific/Honolulu"), - ("Pacific/Johnston", "Pacific/Johnston"), - ("Pacific/Kiritimati", "Pacific/Kiritimati"), - ("Pacific/Kosrae", "Pacific/Kosrae"), - ("Pacific/Kwajalein", "Pacific/Kwajalein"), - ("Pacific/Majuro", "Pacific/Majuro"), - ("Pacific/Marquesas", "Pacific/Marquesas"), - ("Pacific/Midway", "Pacific/Midway"), - ("Pacific/Nauru", "Pacific/Nauru"), - ("Pacific/Niue", "Pacific/Niue"), - ("Pacific/Norfolk", "Pacific/Norfolk"), - ("Pacific/Noumea", "Pacific/Noumea"), - ("Pacific/Pago_Pago", "Pacific/Pago_Pago"), - ("Pacific/Palau", "Pacific/Palau"), - ("Pacific/Pitcairn", "Pacific/Pitcairn"), - ("Pacific/Pohnpei", "Pacific/Pohnpei"), - ("Pacific/Ponape", "Pacific/Ponape"), - ("Pacific/Port_Moresby", "Pacific/Port_Moresby"), - ("Pacific/Rarotonga", "Pacific/Rarotonga"), - ("Pacific/Saipan", "Pacific/Saipan"), - ("Pacific/Samoa", "Pacific/Samoa"), - ("Pacific/Tahiti", "Pacific/Tahiti"), - ("Pacific/Tarawa", "Pacific/Tarawa"), - ("Pacific/Tongatapu", "Pacific/Tongatapu"), - ("Pacific/Truk", "Pacific/Truk"), - ("Pacific/Wake", "Pacific/Wake"), - ("Pacific/Wallis", "Pacific/Wallis"), - ("Pacific/Yap", "Pacific/Yap"), - ("Poland", "Poland"), - ("Portugal", "Portugal"), - ("ROC", "ROC"), - ("ROK", "ROK"), - ("Singapore", "Singapore"), - ("Turkey", "Turkey"), - ("UCT", "UCT"), - ("US/Alaska", "US/Alaska"), - ("US/Aleutian", "US/Aleutian"), - ("US/Arizona", "US/Arizona"), - ("US/Central", "US/Central"), - ("US/East-Indiana", "US/East-Indiana"), - ("US/Eastern", "US/Eastern"), - ("US/Hawaii", "US/Hawaii"), - ("US/Indiana-Starke", "US/Indiana-Starke"), - ("US/Michigan", "US/Michigan"), - ("US/Mountain", "US/Mountain"), - ("US/Pacific", "US/Pacific"), - ("US/Samoa", "US/Samoa"), - ("UTC", "UTC"), - ("Universal", "Universal"), - ("W-SU", "W-SU"), - ("WET", "WET"), - ("Zulu", "Zulu"), - ], - default="UTC", - max_length=100, - ), - ) - ] diff --git a/src/newsreader/news/collection/migrations/0005_auto_20190521_1941.py b/src/newsreader/news/collection/migrations/0005_auto_20190521_1941.py deleted file mode 100644 index 8e3a53f..0000000 --- a/src/newsreader/news/collection/migrations/0005_auto_20190521_1941.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 2.2 on 2019-05-21 19:41 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [("collection", "0004_collectionrule_timezone")] - - operations = [ - migrations.AddField( - model_name="collectionrule", - name="favicon", - field=models.ImageField(blank=True, null=True, upload_to=""), - ), - migrations.AddField( - model_name="collectionrule", - name="source", - field=models.CharField(default="source", max_length=100), - preserve_default=False, - ), - ] diff --git a/src/newsreader/news/collection/migrations/0006_collectionrule_error.py b/src/newsreader/news/collection/migrations/0006_collectionrule_error.py deleted file mode 100644 index b70b638..0000000 --- a/src/newsreader/news/collection/migrations/0006_collectionrule_error.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 2.2 on 2019-06-08 14:13 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [("collection", "0005_auto_20190521_1941")] - - operations = [ - migrations.AddField( - model_name="collectionrule", - name="error", - field=models.CharField(blank=True, max_length=255, null=True), - ) - ] diff --git a/src/newsreader/news/collection/migrations/0007_auto_20190623_1837.py b/src/newsreader/news/collection/migrations/0007_auto_20190623_1837.py deleted file mode 100644 index cc27d6e..0000000 --- a/src/newsreader/news/collection/migrations/0007_auto_20190623_1837.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 2.2 on 2019-06-23 18:37 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [("collection", "0006_collectionrule_error")] - - operations = [ - migrations.AlterField( - model_name="collectionrule", - name="favicon", - field=models.ImageField(default="favicons/default-favicon.ico", upload_to="favicons/"), - ) - ] diff --git a/src/newsreader/news/collection/migrations/0008_auto_20190623_1847.py b/src/newsreader/news/collection/migrations/0008_auto_20190623_1847.py deleted file mode 100644 index 3c8ae66..0000000 --- a/src/newsreader/news/collection/migrations/0008_auto_20190623_1847.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 2.2 on 2019-06-23 18:47 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [("collection", "0007_auto_20190623_1837")] - - operations = [ - migrations.AlterField( - model_name="collectionrule", - name="favicon", - field=models.URLField(blank=True, null=True), - ) - ] diff --git a/src/newsreader/news/collection/migrations/0009_collectionrule_website_url.py b/src/newsreader/news/collection/migrations/0009_collectionrule_website_url.py deleted file mode 100644 index e5273b3..0000000 --- a/src/newsreader/news/collection/migrations/0009_collectionrule_website_url.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 2.2 on 2019-06-27 21:27 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [("collection", "0008_auto_20190623_1847")] - - operations = [ - migrations.AddField( - model_name="collectionrule", - name="website_url", - field=models.URLField(blank=True, editable=False, null=True), - ) - ] diff --git a/src/newsreader/news/collection/migrations/0010_auto_20190628_2142.py b/src/newsreader/news/collection/migrations/0010_auto_20190628_2142.py deleted file mode 100644 index 3726158..0000000 --- a/src/newsreader/news/collection/migrations/0010_auto_20190628_2142.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.2 on 2019-06-28 21:42 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [("collection", "0009_collectionrule_website_url")] - - operations = [ - migrations.AlterField( - model_name="collectionrule", name="url", field=models.URLField(max_length=1024) - ), - migrations.AlterField( - model_name="collectionrule", - name="website_url", - field=models.URLField(blank=True, editable=False, max_length=1024, null=True), - ), - ] diff --git a/src/newsreader/news/collection/models.py b/src/newsreader/news/collection/models.py index 03768ac..d65176a 100644 --- a/src/newsreader/news/collection/models.py +++ b/src/newsreader/news/collection/models.py @@ -1,13 +1,13 @@ -from django.conf import settings from django.db import models from django.utils.translation import gettext as _ import pytz +from newsreader.core.models import TimeStampedModel -class CollectionRule(models.Model): + +class CollectionRule(TimeStampedModel): name = models.CharField(max_length=100) - source = models.CharField(max_length=100) url = models.URLField(max_length=1024) website_url = models.URLField(max_length=1024, editable=False, blank=True, null=True) @@ -20,7 +20,7 @@ class CollectionRule(models.Model): ) category = models.ForeignKey( - "posts.Category", + "core.Category", blank=True, null=True, verbose_name=_("Category"), @@ -30,8 +30,9 @@ class CollectionRule(models.Model): last_suceeded = models.DateTimeField(blank=True, null=True) succeeded = models.BooleanField(default=False) - error = models.CharField(max_length=255, blank=True, null=True) + user = models.ForeignKey("auth.User", _("Owner")) + def __str__(self): return self.name diff --git a/src/newsreader/news/collection/response_handler.py b/src/newsreader/news/collection/response_handler.py index e412475..d9598c4 100644 --- a/src/newsreader/news/collection/response_handler.py +++ b/src/newsreader/news/collection/response_handler.py @@ -1,6 +1,5 @@ from typing import ContextManager -from requests import Response from requests.exceptions import ConnectionError as RequestConnectionError from requests.exceptions import ( HTTPError, diff --git a/src/newsreader/news/collection/serializers.py b/src/newsreader/news/collection/serializers.py new file mode 100644 index 0000000..cf6f9ea --- /dev/null +++ b/src/newsreader/news/collection/serializers.py @@ -0,0 +1,21 @@ +from rest_framework import serializers + +from newsreader.news import core +from newsreader.news.collection.models import CollectionRule + + +class CollectionRuleSerializer(serializers.HyperlinkedModelSerializer): + posts = serializers.SerializerMethodField() + user = serializers.HiddenField(default=serializers.CurrentUserDefault()) + + def get_posts(self, instance): + request = self.context.get("request") + posts = instance.post_set.order_by("-publication_date") + + serializer = core.serializers.PostSerializer(posts, context={"request": request}, many=True) + return serializer.data + + class Meta: + model = CollectionRule + fields = ("id", "name", "url", "favicon", "category", "posts", "user") + extra_kwargs = {"category": {"view_name": "api:categories-detail"}} diff --git a/src/newsreader/news/collection/tests/__init__.py b/src/newsreader/news/collection/tests/__init__.py index ea6a7c0..e69de29 100644 --- a/src/newsreader/news/collection/tests/__init__.py +++ b/src/newsreader/news/collection/tests/__init__.py @@ -1,4 +0,0 @@ -from .favicon import * -from .feed import * -from .tests import * -from .utils import * diff --git a/src/newsreader/news/collection/tests/endpoints/__init__.py b/src/newsreader/news/collection/tests/endpoints/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/collection/tests/endpoints/rules/__init__.py b/src/newsreader/news/collection/tests/endpoints/rules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/collection/tests/endpoints/rules/detail/__init__.py b/src/newsreader/news/collection/tests/endpoints/rules/detail/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/collection/tests/endpoints/rules/detail/tests.py b/src/newsreader/news/collection/tests/endpoints/rules/detail/tests.py new file mode 100644 index 0000000..8782533 --- /dev/null +++ b/src/newsreader/news/collection/tests/endpoints/rules/detail/tests.py @@ -0,0 +1,186 @@ +import json + +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.news.collection.tests.factories import CollectionRuleFactory +from newsreader.news.core.tests.factories import CategoryFactory + + +class CollectionRuleDetailViewTestCase(TestCase): + def setUp(self): + self.maxDiff = None + + self.user = UserFactory(is_staff=True, password="test") + self.client = Client() + + def test_simple(self): + rule = CollectionRuleFactory(user=self.user) + + self.client.force_login(self.user) + response = self.client.get(reverse("api:rules-detail", args=[rule.pk])) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["id"], rule.pk) + + self.assertTrue("name" in data) + self.assertTrue("url" in data) + self.assertTrue("favicon" in data) + self.assertTrue("category" in data) + self.assertTrue("posts" in data) + + def test_not_known(self): + self.client.force_login(self.user) + response = self.client.get(reverse("api:rules-detail", args=[100])) + data = response.json() + + self.assertEquals(response.status_code, 404) + self.assertEquals(data["detail"], "Not found.") + + def test_post(self): + rule = CollectionRuleFactory(user=self.user) + + self.client.force_login(self.user) + response = self.client.post(reverse("api:rules-detail", args=[rule.pk])) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "POST" not allowed.') + + def test_patch(self): + rule = CollectionRuleFactory(name="BBC", user=self.user) + + self.client.force_login(self.user) + response = self.client.patch( + reverse("api:rules-detail", args=[rule.pk]), + data=json.dumps({"name": "The guardian"}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["name"], "The guardian") + + def test_category_change_with_absolute_url(self): + old_category = CategoryFactory(user=self.user) + new_category = CategoryFactory(user=self.user) + + base_url = "http://testserver" + relative_url = reverse("api:categories-detail", args=[new_category.pk]) + + absolute_url = urljoin(base_url, relative_url) + + rule = CollectionRuleFactory(name="BBC", category=old_category, user=self.user) + + self.client.force_login(self.user) + response = self.client.patch( + reverse("api:rules-detail", args=[rule.pk]), + data=json.dumps({"category": absolute_url}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["category"], absolute_url) + + def test_category_change_with_relative_url(self): + old_category = CategoryFactory(user=self.user) + new_category = CategoryFactory(user=self.user) + + base_url = "http://testserver" + relative_url = reverse("api:categories-detail", args=[new_category.pk]) + + absolute_url = urljoin(base_url, relative_url) + + rule = CollectionRuleFactory(name="BBC", category=old_category, user=self.user) + + self.client.force_login(self.user) + response = self.client.patch( + reverse("api:rules-detail", args=[rule.pk]), + data=json.dumps({"category": relative_url}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["category"], absolute_url) + + def test_identifier_cannot_be_changed(self): + rule = CollectionRuleFactory(user=self.user) + + self.client.force_login(self.user) + response = self.client.patch( + reverse("api:rules-detail", args=[rule.pk]), + data=json.dumps({"id": 44}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["id"], rule.pk) + + def test_category_change(self): + rule = CollectionRuleFactory(user=self.user) + category = CategoryFactory(user=self.user) + + 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])}), + 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]))) + + def test_put(self): + rule = CollectionRuleFactory(name="BBC", user=self.user) + + self.client.force_login(self.user) + response = self.client.put( + reverse("api:rules-detail", args=[rule.pk]), + data=json.dumps({"name": "BBC", "url": "https://www.bbc.co.uk"}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["name"], "BBC") + + def test_delete(self): + rule = CollectionRuleFactory(user=self.user) + + self.client.force_login(self.user) + response = self.client.delete(reverse("api:rules-detail", args=[rule.pk])) + + self.assertEquals(response.status_code, 204) + + def test_rule_with_unauthenticated_user(self): + rule = CollectionRuleFactory(name="BBC", user=self.user) + + response = self.client.patch( + reverse("api:rules-detail", args=[rule.pk]), + data=json.dumps({"name": "The guardian"}), + content_type="application/json", + ) + + self.assertEquals(response.status_code, 403) + + def test_rule_with_unauthorized_user(self): + other_user = UserFactory() + rule = CollectionRuleFactory(name="BBC", user=other_user) + + self.client.force_login(self.user) + response = self.client.patch( + reverse("api:rules-detail", args=[rule.pk]), + data=json.dumps({"name": "The guardian"}), + content_type="application/json", + ) + + self.assertEquals(response.status_code, 403) diff --git a/src/newsreader/news/collection/tests/endpoints/rules/list/__init__.py b/src/newsreader/news/collection/tests/endpoints/rules/list/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/collection/tests/endpoints/rules/list/tests.py b/src/newsreader/news/collection/tests/endpoints/rules/list/tests.py new file mode 100644 index 0000000..f5a0cea --- /dev/null +++ b/src/newsreader/news/collection/tests/endpoints/rules/list/tests.py @@ -0,0 +1,206 @@ +import json + +from datetime import date, datetime, time + +from django.test import Client, TestCase +from django.urls import reverse + +import pytz + +from newsreader.auth.tests.factories import UserFactory +from newsreader.news.collection.tests.factories import CollectionRuleFactory +from newsreader.news.core.tests.factories import CategoryFactory, PostFactory + + +class CollectionRuleListViewTestCase(TestCase): + def setUp(self): + self.maxDiff = None + + self.user = UserFactory(is_staff=True, password="test") + self.client = Client() + + def test_simple(self): + CollectionRuleFactory.create_batch(size=3, user=self.user) + + self.client.force_login(self.user) + response = self.client.get(reverse("api:rules-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertTrue("results" in data) + self.assertTrue("count" in data) + self.assertEquals(data["count"], 3) + + def test_ordering(self): + rules = [ + CollectionRuleFactory( + created=datetime.combine( + date(2019, 5, 20), time(hour=16, minute=7, second=37), pytz.utc + ), + user=self.user, + ), + CollectionRuleFactory( + created=datetime.combine( + date(2019, 7, 20), time(hour=18, minute=7, second=37), pytz.utc + ), + user=self.user, + ), + CollectionRuleFactory( + created=datetime.combine( + date(2019, 7, 20), time(hour=16, minute=7, second=37), pytz.utc + ), + user=self.user, + ), + ] + + self.client.force_login(self.user) + response = self.client.get(reverse("api:rules-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertTrue("results" in data) + self.assertTrue("count" in data) + self.assertEquals(data["count"], 3) + + self.assertEquals(data["results"][0]["id"], rules[1].pk) + self.assertEquals(data["results"][1]["id"], rules[2].pk) + self.assertEquals(data["results"][2]["id"], rules[0].pk) + + def test_pagination_count(self): + CollectionRuleFactory.create_batch(size=80, user=self.user) + + self.client.force_login(self.user) + response = self.client.get(reverse("api:rules-list"), {"count": 30}) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["count"], 80) + self.assertEquals(len(data["results"]), 30) + + def test_empty(self): + self.client.force_login(self.user) + response = self.client.get(reverse("api:rules-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertTrue("results" in data) + self.assertTrue("count" in data) + + self.assertEquals(data["count"], 0) + self.assertEquals(len(data["results"]), 0) + + def test_post(self): + category = CategoryFactory(user=self.user) + + data = { + "name": "BBC", + "url": "https://www.bbc.co.uk", + "category": reverse("api:categories-detail", args=[category.pk]), + } + + self.client.force_login(self.user) + response = self.client.post( + reverse("api:rules-list"), data=json.dumps(data), content_type="application/json" + ) + data = response.json() + category_url = data["category"] + + self.assertEquals(response.status_code, 201) + + 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]))) + + def test_patch(self): + self.client.force_login(self.user) + response = self.client.patch(reverse("api:rules-list")) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "PATCH" not allowed.') + + def test_put(self): + self.client.force_login(self.user) + response = self.client.put(reverse("api:rules-list")) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "PUT" not allowed.') + + def test_delete(self): + self.client.force_login(self.user) + response = self.client.delete(reverse("api:rules-list")) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "DELETE" not allowed.') + + def test_rules_with_posts(self): + rules = { + rule: PostFactory.create_batch(size=5, rule=rule) + for rule in CollectionRuleFactory.create_batch(size=5, user=self.user) + } + + self.client.force_login(self.user) + response = self.client.get(reverse("api:rules-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertTrue("results" in data) + self.assertTrue("count" in data) + self.assertEquals(data["count"], 5) + + self.assertEquals(len(data["results"]), 5) + + self.assertEquals(len(data["results"][0]["posts"]), 5) + + def test_rules_with_posts_ordered(self): + rules = { + rule: PostFactory.create_batch(size=5, rule=rule) + for rule in CollectionRuleFactory.create_batch(size=2, user=self.user) + } + + self.client.force_login(self.user) + response = self.client.get(reverse("api:rules-list")) + data = response.json() + + first_post_set = data["results"][0]["posts"] + second_post_set = data["results"][1]["posts"] + + self.assertEquals(response.status_code, 200) + self.assertTrue("results" in data) + self.assertTrue("count" in data) + self.assertEquals(data["count"], 2) + + self.assertEquals(len(data["results"]), 2) + + for result_set in [first_post_set, second_post_set]: + for count, post in enumerate(result_set): + if count < 1: + continue + + self.assertTrue( + post["publication_date"] < result_set[count - 1]["publication_date"] + ) + + def test_rule_with_unauthenticated_user(self): + CollectionRuleFactory.create_batch(size=3, user=self.user) + + response = self.client.get(reverse("api:rules-list")) + response.json() + + self.assertEquals(response.status_code, 403) + + def test_rule_with_unauthorized_user(self): + other_user = UserFactory() + CollectionRuleFactory.create_batch(size=3, user=other_user) + + self.client.force_login(self.user) + response = self.client.get(reverse("api:rules-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + + self.assertEquals(data["count"], 0) + self.assertEquals(len(data["results"]), 0) diff --git a/src/newsreader/news/collection/tests/factories.py b/src/newsreader/news/collection/tests/factories.py index be54806..9d0803f 100644 --- a/src/newsreader/news/collection/tests/factories.py +++ b/src/newsreader/news/collection/tests/factories.py @@ -1,13 +1,17 @@ import factory +from newsreader.auth.tests.factories import UserFactory from newsreader.news.collection.models import CollectionRule class CollectionRuleFactory(factory.django.DjangoModelFactory): - class Meta: - model = CollectionRule - name = factory.Sequence(lambda n: "CollectionRule-{}".format(n)) - source = factory.Faker("name") url = factory.Faker("url") website_url = factory.Faker("url") + + category = factory.SubFactory("newsreader.news.core.tests.factories.CategoryFactory") + + user = factory.SubFactory(UserFactory) + + class Meta: + model = CollectionRule diff --git a/src/newsreader/news/collection/tests/favicon/__init__.py b/src/newsreader/news/collection/tests/favicon/__init__.py index 5fb0299..e69de29 100644 --- a/src/newsreader/news/collection/tests/favicon/__init__.py +++ b/src/newsreader/news/collection/tests/favicon/__init__.py @@ -1,3 +0,0 @@ -from .builder import * -from .client import * -from .collector import * diff --git a/src/newsreader/news/collection/tests/favicon/builder/__init__.py b/src/newsreader/news/collection/tests/favicon/builder/__init__.py index 8baa6e5..e69de29 100644 --- a/src/newsreader/news/collection/tests/favicon/builder/__init__.py +++ b/src/newsreader/news/collection/tests/favicon/builder/__init__.py @@ -1 +0,0 @@ -from .tests import * diff --git a/src/newsreader/news/collection/tests/favicon/builder/tests.py b/src/newsreader/news/collection/tests/favicon/builder/tests.py index c8bd14c..2e7b57a 100644 --- a/src/newsreader/news/collection/tests/favicon/builder/tests.py +++ b/src/newsreader/news/collection/tests/favicon/builder/tests.py @@ -1,7 +1,5 @@ from django.test import TestCase -from freezegun import freeze_time - from newsreader.news.collection.favicon import FaviconBuilder from newsreader.news.collection.tests.factories import CollectionRuleFactory from newsreader.news.collection.tests.favicon.builder.mocks import * diff --git a/src/newsreader/news/collection/tests/favicon/client/__init__.py b/src/newsreader/news/collection/tests/favicon/client/__init__.py index 8baa6e5..e69de29 100644 --- a/src/newsreader/news/collection/tests/favicon/client/__init__.py +++ b/src/newsreader/news/collection/tests/favicon/client/__init__.py @@ -1 +0,0 @@ -from .tests import * diff --git a/src/newsreader/news/collection/tests/favicon/client/tests.py b/src/newsreader/news/collection/tests/favicon/client/tests.py index 4ac2a40..717ee0c 100644 --- a/src/newsreader/news/collection/tests/favicon/client/tests.py +++ b/src/newsreader/news/collection/tests/favicon/client/tests.py @@ -2,7 +2,6 @@ from unittest.mock import MagicMock from django.test import TestCase -from newsreader.news.collection.base import WebsiteStream from newsreader.news.collection.exceptions import ( StreamDeniedException, StreamException, diff --git a/src/newsreader/news/collection/tests/favicon/collector/__init__.py b/src/newsreader/news/collection/tests/favicon/collector/__init__.py index 8baa6e5..e69de29 100644 --- a/src/newsreader/news/collection/tests/favicon/collector/__init__.py +++ b/src/newsreader/news/collection/tests/favicon/collector/__init__.py @@ -1 +0,0 @@ -from .tests import * diff --git a/src/newsreader/news/collection/tests/favicon/collector/tests.py b/src/newsreader/news/collection/tests/favicon/collector/tests.py index a292c16..48c16e7 100644 --- a/src/newsreader/news/collection/tests/favicon/collector/tests.py +++ b/src/newsreader/news/collection/tests/favicon/collector/tests.py @@ -1,9 +1,6 @@ from unittest.mock import MagicMock, patch from django.test import TestCase -from django.utils import timezone - -import pytz from bs4 import BeautifulSoup diff --git a/src/newsreader/news/collection/tests/feed/__init__.py b/src/newsreader/news/collection/tests/feed/__init__.py index 50cea54..e69de29 100644 --- a/src/newsreader/news/collection/tests/feed/__init__.py +++ b/src/newsreader/news/collection/tests/feed/__init__.py @@ -1,5 +0,0 @@ -from .builder import * -from .client import * -from .collector import * -from .duplicate_handler import * -from .stream import * diff --git a/src/newsreader/news/collection/tests/feed/builder/__init__.py b/src/newsreader/news/collection/tests/feed/builder/__init__.py index 8baa6e5..e69de29 100644 --- a/src/newsreader/news/collection/tests/feed/builder/__init__.py +++ b/src/newsreader/news/collection/tests/feed/builder/__init__.py @@ -1 +0,0 @@ -from .tests import * diff --git a/src/newsreader/news/collection/tests/feed/builder/tests.py b/src/newsreader/news/collection/tests/feed/builder/tests.py index 6efd432..94e84ea 100644 --- a/src/newsreader/news/collection/tests/feed/builder/tests.py +++ b/src/newsreader/news/collection/tests/feed/builder/tests.py @@ -10,8 +10,8 @@ from freezegun import freeze_time from newsreader.news.collection.feed import FeedBuilder from newsreader.news.collection.tests.factories import CollectionRuleFactory -from newsreader.news.posts.models import Post -from newsreader.news.posts.tests.factories import PostFactory +from newsreader.news.core.models import Post +from newsreader.news.core.tests.factories import PostFactory from .mocks import * diff --git a/src/newsreader/news/collection/tests/feed/client/__init__.py b/src/newsreader/news/collection/tests/feed/client/__init__.py index 8baa6e5..e69de29 100644 --- a/src/newsreader/news/collection/tests/feed/client/__init__.py +++ b/src/newsreader/news/collection/tests/feed/client/__init__.py @@ -1 +0,0 @@ -from .tests import * diff --git a/src/newsreader/news/collection/tests/feed/client/tests.py b/src/newsreader/news/collection/tests/feed/client/tests.py index bd9e4eb..9c11cbd 100644 --- a/src/newsreader/news/collection/tests/feed/client/tests.py +++ b/src/newsreader/news/collection/tests/feed/client/tests.py @@ -1,7 +1,6 @@ from unittest.mock import MagicMock, patch from django.test import TestCase -from django.utils import timezone from newsreader.news.collection.exceptions import ( StreamDeniedException, diff --git a/src/newsreader/news/collection/tests/feed/collector/__init__.py b/src/newsreader/news/collection/tests/feed/collector/__init__.py index 8baa6e5..e69de29 100644 --- a/src/newsreader/news/collection/tests/feed/collector/__init__.py +++ b/src/newsreader/news/collection/tests/feed/collector/__init__.py @@ -1 +0,0 @@ -from .tests import * diff --git a/src/newsreader/news/collection/tests/feed/collector/tests.py b/src/newsreader/news/collection/tests/feed/collector/tests.py index 16008dd..6978ee9 100644 --- a/src/newsreader/news/collection/tests/feed/collector/tests.py +++ b/src/newsreader/news/collection/tests/feed/collector/tests.py @@ -20,8 +20,8 @@ from newsreader.news.collection.exceptions import ( from newsreader.news.collection.feed import FeedCollector from newsreader.news.collection.tests.factories import CollectionRuleFactory from newsreader.news.collection.utils import build_publication_date -from newsreader.news.posts.models import Post -from newsreader.news.posts.tests.factories import PostFactory +from newsreader.news.core.models import Post +from newsreader.news.core.tests.factories import PostFactory from .mocks import ( duplicate_mock, diff --git a/src/newsreader/news/collection/tests/feed/duplicate_handler/__init__.py b/src/newsreader/news/collection/tests/feed/duplicate_handler/__init__.py index 8baa6e5..e69de29 100644 --- a/src/newsreader/news/collection/tests/feed/duplicate_handler/__init__.py +++ b/src/newsreader/news/collection/tests/feed/duplicate_handler/__init__.py @@ -1 +0,0 @@ -from .tests import * diff --git a/src/newsreader/news/collection/tests/feed/duplicate_handler/tests.py b/src/newsreader/news/collection/tests/feed/duplicate_handler/tests.py index eff63cc..18b6a99 100644 --- a/src/newsreader/news/collection/tests/feed/duplicate_handler/tests.py +++ b/src/newsreader/news/collection/tests/feed/duplicate_handler/tests.py @@ -3,8 +3,7 @@ from django.utils import timezone from newsreader.news.collection.feed import FeedDuplicateHandler from newsreader.news.collection.tests.factories import CollectionRuleFactory -from newsreader.news.posts.models import Post -from newsreader.news.posts.tests.factories import PostFactory +from newsreader.news.core.tests.factories import PostFactory class FeedDuplicateHandlerTestCase(TestCase): diff --git a/src/newsreader/news/collection/tests/feed/stream/__init__.py b/src/newsreader/news/collection/tests/feed/stream/__init__.py index 8baa6e5..e69de29 100644 --- a/src/newsreader/news/collection/tests/feed/stream/__init__.py +++ b/src/newsreader/news/collection/tests/feed/stream/__init__.py @@ -1 +0,0 @@ -from .tests import * diff --git a/src/newsreader/news/collection/tests/feed/stream/tests.py b/src/newsreader/news/collection/tests/feed/stream/tests.py index 8509156..7c0f203 100644 --- a/src/newsreader/news/collection/tests/feed/stream/tests.py +++ b/src/newsreader/news/collection/tests/feed/stream/tests.py @@ -1,7 +1,6 @@ from unittest.mock import MagicMock, patch from django.test import TestCase -from django.utils import timezone from newsreader.news.collection.exceptions import ( StreamDeniedException, diff --git a/src/newsreader/news/collection/tests/utils/__init__.py b/src/newsreader/news/collection/tests/utils/__init__.py index 8baa6e5..e69de29 100644 --- a/src/newsreader/news/collection/tests/utils/__init__.py +++ b/src/newsreader/news/collection/tests/utils/__init__.py @@ -1 +0,0 @@ -from .tests import * diff --git a/src/newsreader/news/collection/urls.py b/src/newsreader/news/collection/urls.py new file mode 100644 index 0000000..4b59a09 --- /dev/null +++ b/src/newsreader/news/collection/urls.py @@ -0,0 +1,12 @@ +from django.urls import path + +from newsreader.news.collection.views import ( + CollectionRuleAPIListView, + CollectionRuleDetailView, +) + + +endpoints = [ + path("rules/", CollectionRuleDetailView.as_view(), name="rules-detail"), + path("rules/", CollectionRuleAPIListView.as_view(), name="rules-list"), +] diff --git a/src/newsreader/news/collection/views.py b/src/newsreader/news/collection/views.py index dc1ba72..3de472b 100644 --- a/src/newsreader/news/collection/views.py +++ b/src/newsreader/news/collection/views.py @@ -1,4 +1,24 @@ -from django.shortcuts import render +from rest_framework.generics import ( + ListCreateAPIView, + RetrieveUpdateDestroyAPIView, +) + +from newsreader.core.pagination import ResultSetPagination +from newsreader.news.collection.models import CollectionRule +from newsreader.news.collection.serializers import CollectionRuleSerializer -# Create your views here. +class CollectionRuleAPIListView(ListCreateAPIView): + queryset = CollectionRule.objects.all() + serializer_class = CollectionRuleSerializer + pagination_class = ResultSetPagination + + def get_queryset(self): + user = self.request.user + return self.queryset.filter(user=user).order_by("-created") + + +class CollectionRuleDetailView(RetrieveUpdateDestroyAPIView): + queryset = CollectionRule.objects.all() + serializer_class = CollectionRuleSerializer + pagination_class = ResultSetPagination diff --git a/src/newsreader/news/core/__init__.py b/src/newsreader/news/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/posts/admin.py b/src/newsreader/news/core/admin.py similarity index 85% rename from src/newsreader/news/posts/admin.py rename to src/newsreader/news/core/admin.py index 3e6940b..3bcfc19 100644 --- a/src/newsreader/news/posts/admin.py +++ b/src/newsreader/news/core/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from newsreader.news.posts.models import Category, Post +from newsreader.news.core.models import Category, Post class PostAdmin(admin.ModelAdmin): @@ -10,7 +10,7 @@ class PostAdmin(admin.ModelAdmin): ordering = ("-publication_date", "title") - fields = ("title", "body", "author", "publication_date", "url", "remote_identifier", "category") + fields = ("title", "body", "author", "publication_date", "url") search_fields = ["title"] diff --git a/src/newsreader/news/core/apps.py b/src/newsreader/news/core/apps.py new file mode 100644 index 0000000..5ef1d60 --- /dev/null +++ b/src/newsreader/news/core/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class CoreConfig(AppConfig): + name = "core" diff --git a/src/newsreader/news/posts/migrations/0001_initial.py b/src/newsreader/news/core/migrations/0001_initial.py similarity index 54% rename from src/newsreader/news/posts/migrations/0001_initial.py rename to src/newsreader/news/core/migrations/0001_initial.py index c7e7604..5e0ffca 100644 --- a/src/newsreader/news/posts/migrations/0001_initial.py +++ b/src/newsreader/news/core/migrations/0001_initial.py @@ -1,8 +1,8 @@ -# Generated by Django 2.2 on 2019-04-10 20:10 - -import django.db.models.deletion +# Generated by Django 2.2 on 2019-07-05 20:59 from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone class Migration(migrations.Migration): @@ -21,11 +21,11 @@ class Migration(migrations.Migration): auto_created=True, primary_key=True, serialize=False, verbose_name="ID" ), ), - ("created", models.DateTimeField(auto_now_add=True)), + ("created", models.DateTimeField(default=django.utils.timezone.now)), ("modified", models.DateTimeField(auto_now=True)), - ("name", models.CharField(max_length=50)), + ("name", models.CharField(max_length=50, unique=True)), ], - options={"abstract": False}, + options={"verbose_name": "Category", "verbose_name_plural": "Categories"}, ), migrations.CreateModel( name="Post", @@ -36,27 +36,23 @@ class Migration(migrations.Migration): auto_created=True, primary_key=True, serialize=False, verbose_name="ID" ), ), - ("created", models.DateTimeField(auto_now_add=True)), + ("created", models.DateTimeField(default=django.utils.timezone.now)), ("modified", models.DateTimeField(auto_now=True)), - ("title", models.CharField(max_length=200)), - ("body", models.TextField()), - ("source", models.CharField(max_length=200)), - ("publication_date", models.DateTimeField()), - ("url", models.URLField()), - ("remote_identifier", models.CharField(max_length=500)), + ("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)), + ("publication_date", models.DateTimeField(blank=True, null=True)), + ("url", models.URLField(blank=True, max_length=1024, null=True)), ( - "category", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.PROTECT, - to="posts.Category", - ), + "remote_identifier", + models.CharField(blank=True, editable=False, max_length=500, null=True), ), ( "rule", models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="collection.CollectionRule" + editable=False, + on_delete=django.db.models.deletion.CASCADE, + to="collection.CollectionRule", ), ), ], diff --git a/src/newsreader/news/core/migrations/0002_category_user.py b/src/newsreader/news/core/migrations/0002_category_user.py new file mode 100644 index 0000000..d2fa17f --- /dev/null +++ b/src/newsreader/news/core/migrations/0002_category_user.py @@ -0,0 +1,21 @@ +# 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, + ) + ] diff --git a/src/newsreader/news/core/migrations/__init__.py b/src/newsreader/news/core/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/posts/models.py b/src/newsreader/news/core/models.py similarity index 78% rename from src/newsreader/news/posts/models.py rename to src/newsreader/news/core/models.py index fd2c6c1..704e752 100644 --- a/src/newsreader/news/posts/models.py +++ b/src/newsreader/news/core/models.py @@ -8,14 +8,12 @@ from newsreader.news.collection.models import CollectionRule class Post(TimeStampedModel): title = models.CharField(max_length=200, blank=True, null=True) body = models.TextField(blank=True, null=True) - author = models.CharField(max_length=100, blank=True, null=True) + author = models.CharField(max_length=200, blank=True, null=True) publication_date = models.DateTimeField(blank=True, null=True) - url = models.URLField(blank=True, null=True) + url = models.URLField(max_length=1024, blank=True, null=True) - rule = models.ForeignKey(CollectionRule, on_delete=models.CASCADE) - remote_identifier = models.CharField(max_length=500, blank=True, null=True) - - category = models.ForeignKey("Category", blank=True, null=True, on_delete=models.PROTECT) + rule = models.ForeignKey(CollectionRule, on_delete=models.CASCADE, editable=False) + remote_identifier = models.CharField(max_length=500, blank=True, null=True, editable=False) def __str__(self): return "Post-{}".format(self.pk) @@ -23,6 +21,7 @@ class Post(TimeStampedModel): class Category(TimeStampedModel): name = models.CharField(max_length=50, unique=True) + user = models.ForeignKey("auth.User", _("Owner")) class Meta: verbose_name = _("Category") diff --git a/src/newsreader/news/core/pagination.py b/src/newsreader/news/core/pagination.py new file mode 100644 index 0000000..357ff71 --- /dev/null +++ b/src/newsreader/news/core/pagination.py @@ -0,0 +1,31 @@ +from rest_framework import serializers + +from newsreader.news.posts.models import Category, Post + + +class CategorySerializer(serializers.ModelSerializer): + rules = serializers.SerializerMethodField() + + def get_rules(self, instance): + rules = instance.collectionrule_set.order_by("-modified", "-created") + serializer = CollectionRuleSerializer(rules, many=True) + return serializer.data + + class Meta: + model = Category + fields = ("id", "name", "rules") + + +class PostSerializer(serializers.ModelSerializer): + class Meta: + model = Post + fields = ( + "id", + "title", + "body", + "author", + "publication_date", + "url", + "rule", + "remote_identifier", + ) diff --git a/src/newsreader/news/core/serializers.py b/src/newsreader/news/core/serializers.py new file mode 100644 index 0000000..ca3e93c --- /dev/null +++ b/src/newsreader/news/core/serializers.py @@ -0,0 +1,39 @@ +from rest_framework import serializers + +from newsreader.news import collection +from newsreader.news.core.models import Category, Post + + +class PostSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Post + fields = ( + "id", + "remote_identifier", + "title", + "body", + "author", + "publication_date", + "url", + "rule", + ) + extra_kwargs = {"rule": {"view_name": "api:rules-detail"}} + + +class CategorySerializer(serializers.HyperlinkedModelSerializer): + rules = serializers.SerializerMethodField() + user = serializers.HiddenField(default=serializers.CurrentUserDefault()) + + def get_rules(self, instance): + request = self.context.get("request") + rules = instance.collectionrule_set.order_by("-modified", "-created") + + serializer = collection.serializers.CollectionRuleSerializer( + rules, context={"request": request}, many=True + ) + return serializer.data + + class Meta: + model = Category + fields = ("id", "name", "rules", "user") + extra_kwargs = {"rules": {"view_name": "api:rules-detail"}} diff --git a/src/newsreader/news/core/tests/__init__.py b/src/newsreader/news/core/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/core/tests/endpoints/__init__.py b/src/newsreader/news/core/tests/endpoints/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/core/tests/endpoints/category/__init__.py b/src/newsreader/news/core/tests/endpoints/category/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/core/tests/endpoints/category/detail/__init__.py b/src/newsreader/news/core/tests/endpoints/category/detail/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/core/tests/endpoints/category/detail/tests.py b/src/newsreader/news/core/tests/endpoints/category/detail/tests.py new file mode 100644 index 0000000..d65fcb7 --- /dev/null +++ b/src/newsreader/news/core/tests/endpoints/category/detail/tests.py @@ -0,0 +1,168 @@ +import json + +from django.test import Client, TestCase +from django.urls import reverse + +from newsreader.auth.tests.factories import UserFactory +from newsreader.news.collection.tests.factories import CollectionRuleFactory +from newsreader.news.core.tests.factories import CategoryFactory, PostFactory + + +class CategoryDetailViewTestCase(TestCase): + def setUp(self): + self.maxDiff = None + + self.client = Client() + self.user = UserFactory(is_staff=True, password="test") + + def test_simple(self): + category = CategoryFactory(user=self.user) + + self.client.force_login(self.user) + response = self.client.get(reverse("api:categories-detail", args=[category.pk])) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertTrue("id" in data) + self.assertTrue("name" in data) + self.assertTrue("rules" in data) + + def test_not_known(self): + self.client.force_login(self.user) + response = self.client.get(reverse("api:categories-detail", args=[100])) + data = response.json() + + self.assertEquals(response.status_code, 404) + self.assertEquals(data["detail"], "Not found.") + + def test_post(self): + category = CategoryFactory(user=self.user) + + self.client.force_login(self.user) + response = self.client.post(reverse("api:categories-detail", args=[category.pk])) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "POST" not allowed.') + + def test_patch(self): + category = CategoryFactory(name="Clickbait", user=self.user) + + self.client.force_login(self.user) + response = self.client.patch( + reverse("api:categories-detail", args=[category.pk]), + data=json.dumps({"name": "Interesting posts"}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["name"], "Interesting posts") + + def test_identifier_cannot_be_changed(self): + category = CategoryFactory(user=self.user) + + self.client.force_login(self.user) + response = self.client.patch( + reverse("api:categories-detail", args=[category.pk]), + data=json.dumps({"id": 44}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["id"], category.pk) + + def test_put(self): + category = CategoryFactory(name="Clickbait", user=self.user) + + self.client.force_login(self.user) + response = self.client.put( + reverse("api:categories-detail", args=[category.pk]), + data=json.dumps({"name": "Interesting posts"}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["name"], "Interesting posts") + + def test_delete(self): + category = CategoryFactory(user=self.user) + + self.client.force_login(self.user) + 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) + + self.client.force_login(self.user) + response = self.client.get(reverse("api:categories-detail", args=[category.pk])) + data = response.json() + + self.assertEquals(response.status_code, 200) + + self.assertTrue("id" in data["rules"][0]) + self.assertTrue("name" in data["rules"][0]) + self.assertTrue("url" in data["rules"][0]) + + def test_rules_with_posts(self): + category = CategoryFactory(user=self.user) + + rules = { + rule.pk: PostFactory.create_batch(size=5, rule=rule) + for rule in 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])) + data = response.json() + + self.assertEquals(response.status_code, 200) + + self.assertEquals(len(data["rules"][0]["posts"]), 5) + + def test_rules_with_posts_ordered(self): + category = CategoryFactory(user=self.user) + + rules = { + rule.pk: PostFactory.create_batch(size=5, rule=rule) + for rule in 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])) + data = response.json() + + self.assertEquals(response.status_code, 200) + + posts = data["rules"][0]["posts"] + + for count, post in enumerate(posts): + if count < 1: + continue + + self.assertTrue(post["publication_date"] < posts[count - 1]["publication_date"]) + + def test_category_with_unauthenticated_user(self): + category = CategoryFactory(user=self.user) + + response = self.client.get(reverse("api:categories-detail", args=[category.pk])) + + self.assertEquals(response.status_code, 403) + + def test_category_with_unauthorized_user(self): + other_user = UserFactory() + category = CategoryFactory(user=other_user) + + self.client.force_login(self.user) + response = self.client.get(reverse("api:categories-detail", args=[category.pk])) + + self.assertEquals(response.status_code, 403) diff --git a/src/newsreader/news/core/tests/endpoints/category/list/__init__.py b/src/newsreader/news/core/tests/endpoints/category/list/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/core/tests/endpoints/category/list/tests.py b/src/newsreader/news/core/tests/endpoints/category/list/tests.py new file mode 100644 index 0000000..050a430 --- /dev/null +++ b/src/newsreader/news/core/tests/endpoints/category/list/tests.py @@ -0,0 +1,186 @@ +import json + +from datetime import date, datetime, time + +from django.test import Client, TestCase +from django.urls import reverse + +import pytz + +from newsreader.auth.tests.factories import UserFactory +from newsreader.news.collection.tests.factories import CollectionRuleFactory +from newsreader.news.core.tests.factories import CategoryFactory, PostFactory + + +class CategoryListViewTestCase(TestCase): + def setUp(self): + self.maxDiff = None + + self.client = Client() + self.user = UserFactory(is_staff=True, password="test") + + def test_simple(self): + CategoryFactory.create_batch(size=3, user=self.user) + + self.client.force_login(self.user) + response = self.client.get(reverse("api:categories-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertTrue("results" in data) + self.assertTrue("count" in data) + self.assertEquals(data["count"], 3) + + def test_ordering(self): + categories = [ + CategoryFactory( + created=datetime.combine( + date(2019, 5, 20), time(hour=16, minute=7, second=37), pytz.utc + ), + user=self.user, + ), + CategoryFactory( + created=datetime.combine( + date(2019, 7, 20), time(hour=18, minute=7, second=37), pytz.utc + ), + user=self.user, + ), + CategoryFactory( + created=datetime.combine( + date(2019, 7, 20), time(hour=16, minute=7, second=37), pytz.utc + ), + user=self.user, + ), + ] + + self.client.force_login(self.user) + response = self.client.get(reverse("api:categories-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertTrue("results" in data) + self.assertTrue("count" in data) + self.assertEquals(data["count"], 3) + + self.assertEquals(data["results"][0]["id"], categories[1].pk) + self.assertEquals(data["results"][1]["id"], categories[2].pk) + self.assertEquals(data["results"][2]["id"], categories[0].pk) + + def test_empty(self): + self.client.force_login(self.user) + response = self.client.get(reverse("api:categories-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertTrue("results" in data) + self.assertTrue("count" in data) + + self.assertEquals(data["count"], 0) + self.assertEquals(len(data["results"]), 0) + + def test_post(self): + data = {"name": "Tech"} + + self.client.force_login(self.user) + response = self.client.post( + reverse("api:categories-list"), data=json.dumps(data), content_type="application/json" + ) + response_data = response.json() + + self.assertEquals(response.status_code, 201) + self.assertEquals(response_data["name"], "Tech") + + def test_patch(self): + self.client.force_login(self.user) + response = self.client.patch(reverse("api:categories-list")) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "PATCH" not allowed.') + + def test_put(self): + self.client.force_login(self.user) + response = self.client.put(reverse("api:categories-list")) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "PUT" not allowed.') + + def test_delete(self): + self.client.force_login(self.user) + response = self.client.delete(reverse("api:categories-list")) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "DELETE" not allowed.') + + def test_rules(self): + categories = { + category.pk: CollectionRuleFactory.create_batch( + size=5, category=category, user=self.user + ) + for category in CategoryFactory.create_batch(size=5, user=self.user) + } + + self.client.force_login(self.user) + response = self.client.get(reverse("api:categories-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertTrue("results" in data) + self.assertTrue("count" in data) + self.assertEquals(data["count"], 5) + + self.assertEquals(len(data["results"]), 5) + + self.assertEquals(len(data["results"][0]["rules"]), 5) + + self.assertTrue("id" in data["results"][0]["rules"][0]) + self.assertTrue("name" in data["results"][0]["rules"][0]) + self.assertTrue("url" in data["results"][0]["rules"][0]) + self.assertTrue("posts" in data["results"][0]["rules"][0]) + + def test_rules_with_posts(self): + categories = { + category.pk: CollectionRuleFactory.create_batch( + size=5, category=category, user=self.user + ) + for category in CategoryFactory.create_batch(size=5, user=self.user) + } + + for category in categories: + for rule in categories[category]: + PostFactory.create_batch(size=5, rule=rule) + + self.client.force_login(self.user) + response = self.client.get(reverse("api:categories-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertTrue("results" in data) + self.assertTrue("count" in data) + self.assertEquals(data["count"], 5) + + self.assertEquals(len(data["results"]), 5) + + self.assertEquals(len(data["results"][0]["rules"]), 5) + self.assertEquals(len(data["results"][0]["rules"][0]["posts"]), 5) + + def test_categories_with_unauthenticated_user(self): + CategoryFactory.create_batch(size=3, user=self.user) + + response = self.client.get(reverse("api:categories-list")) + + self.assertEquals(response.status_code, 403) + + def test_categories_with_unauthorized_user(self): + other_user = UserFactory() + CategoryFactory.create_batch(size=3, user=other_user) + + self.client.force_login(self.user) + response = self.client.get(reverse("api:categories-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(len(data["results"]), 0) + self.assertEquals(data["count"], 0) diff --git a/src/newsreader/news/core/tests/endpoints/post/__init__.py b/src/newsreader/news/core/tests/endpoints/post/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/core/tests/endpoints/post/detail/__init__.py b/src/newsreader/news/core/tests/endpoints/post/detail/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/core/tests/endpoints/post/detail/tests.py b/src/newsreader/news/core/tests/endpoints/post/detail/tests.py new file mode 100644 index 0000000..e963035 --- /dev/null +++ b/src/newsreader/news/core/tests/endpoints/post/detail/tests.py @@ -0,0 +1,175 @@ +import json + +from django.test import Client, TestCase +from django.urls import reverse + +from newsreader.auth.tests.factories import UserFactory +from newsreader.news.collection.tests.factories import CollectionRuleFactory +from newsreader.news.core.tests.factories import CategoryFactory, PostFactory + + +class PostDetailViewTestCase(TestCase): + def setUp(self): + self.maxDiff = None + self.client = Client() + + self.client = Client() + self.user = UserFactory(is_staff=True, password="test") + + def test_simple(self): + rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=self.user)) + post = PostFactory(rule=rule) + + self.client.force_login(self.user) + response = self.client.get(reverse("api:posts-detail", args=[post.pk])) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["id"], post.pk) + + self.assertTrue("title" in data) + self.assertTrue("body" in data) + self.assertTrue("author" in data) + self.assertTrue("publication_date" in data) + self.assertTrue("url" in data) + self.assertTrue("rule" in data) + self.assertTrue("remote_identifier" in data) + + def test_not_known(self): + self.client.force_login(self.user) + response = self.client.get(reverse("api:posts-detail", args=[100])) + data = response.json() + + self.assertEquals(response.status_code, 404) + self.assertEquals(data["detail"], "Not found.") + + def test_post(self): + rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=self.user)) + post = PostFactory(rule=rule) + + self.client.force_login(self.user) + response = self.client.post(reverse("api:posts-detail", args=[post.pk])) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "POST" not allowed.') + + def test_patch(self): + 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) + response = self.client.patch( + reverse("api:posts-detail", args=[post.pk]), + data=json.dumps({"title": "This title is very accurate"}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 200) + 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)) + post = PostFactory(title="This is clickbait for sure", rule=rule) + + self.client.force_login(self.user) + response = self.client.patch( + reverse("api:posts-detail", args=[post.pk]), + data=json.dumps({"id": 44}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 200) + 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)) + post = PostFactory(title="This is clickbait for sure", rule=rule) + + self.client.force_login(self.user) + response = self.client.patch( + reverse("api:posts-detail", args=[post.pk]), + data=json.dumps({"rule": reverse("api:rules-detail", args=[new_rule.pk])}), + content_type="application/json", + ) + data = response.json() + rule_url = data["rule"] + + self.assertEquals(response.status_code, 200) + + 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)) + post = PostFactory(title="This is clickbait for sure", rule=rule) + + self.client.force_login(self.user) + response = self.client.put( + reverse("api:posts-detail", args=[post.pk]), + data=json.dumps({"title": "This title is very accurate"}), + content_type="application/json", + ) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["title"], "This title is very accurate") + + def test_delete(self): + rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=self.user)) + post = PostFactory(rule=rule) + + self.client.force_login(self.user) + response = self.client.delete(reverse("api:posts-detail", args=[post.pk])) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "DELETE" not allowed.') + + def test_post_with_unauthenticated_user_without_category(self): + rule = CollectionRuleFactory(user=self.user, category=None) + post = PostFactory(rule=rule) + + response = self.client.get(reverse("api:posts-detail", args=[post.pk])) + + 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)) + post = PostFactory(rule=rule) + + response = self.client.get(reverse("api:posts-detail", args=[post.pk])) + + self.assertEquals(response.status_code, 403) + + def test_post_with_unauthorized_user_without_category(self): + other_user = UserFactory() + rule = CollectionRuleFactory(user=other_user, category=None) + post = PostFactory(rule=rule) + + self.client.force_login(self.user) + response = self.client.get(reverse("api:posts-detail", args=[post.pk])) + + self.assertEquals(response.status_code, 403) + + def test_post_with_unauthorized_user_with_category(self): + other_user = UserFactory() + rule = CollectionRuleFactory(user=other_user, category=CategoryFactory(user=other_user)) + post = PostFactory(rule=rule) + + self.client.force_login(self.user) + response = self.client.get(reverse("api:posts-detail", args=[post.pk])) + + self.assertEquals(response.status_code, 403) + + def test_post_with_different_user_for_category_and_rule(self): + other_user = UserFactory() + rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=other_user)) + post = PostFactory(rule=rule) + + self.client.force_login(self.user) + response = self.client.get(reverse("api:posts-detail", args=[post.pk])) + + self.assertEquals(response.status_code, 403) diff --git a/src/newsreader/news/core/tests/endpoints/post/list/__init__.py b/src/newsreader/news/core/tests/endpoints/post/list/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/newsreader/news/core/tests/endpoints/post/list/tests.py b/src/newsreader/news/core/tests/endpoints/post/list/tests.py new file mode 100644 index 0000000..ed1c29e --- /dev/null +++ b/src/newsreader/news/core/tests/endpoints/post/list/tests.py @@ -0,0 +1,208 @@ +from datetime import date, datetime, time + +from django.test import Client, TestCase +from django.urls import reverse + +import pytz + +from newsreader.auth.tests.factories import UserFactory +from newsreader.news.collection.tests.factories import CollectionRuleFactory +from newsreader.news.core.tests.factories import CategoryFactory, PostFactory + + +class PostListViewTestCase(TestCase): + def setUp(self): + self.maxDiff = None + + self.client = Client() + self.user = UserFactory(is_staff=True, password="test") + + def test_simple(self): + rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=self.user)) + PostFactory.create_batch(size=3, rule=rule) + + self.client.force_login(self.user) + response = self.client.get(reverse("api:posts-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertTrue("results" in data) + self.assertTrue("count" in data) + self.assertEquals(data["count"], 3) + + def test_ordering(self): + rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=self.user)) + + posts = [ + PostFactory( + title="I'm the first post", + rule=rule, + publication_date=datetime.combine( + date(2019, 5, 20), time(hour=16, minute=7, second=37), pytz.utc + ), + ), + PostFactory( + title="I'm the second post", + rule=rule, + publication_date=datetime.combine( + date(2019, 7, 20), time(hour=18, minute=7, second=37), pytz.utc + ), + ), + PostFactory( + title="I'm the third post", + rule=rule, + publication_date=datetime.combine( + date(2019, 7, 20), time(hour=16, minute=7, second=37), pytz.utc + ), + ), + ] + + self.client.force_login(self.user) + response = self.client.get(reverse("api:posts-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertTrue("results" in data) + self.assertTrue("count" in data) + self.assertEquals(data["count"], 3) + + self.assertEquals(data["results"][0]["id"], posts[1].pk) + self.assertEquals(data["results"][1]["id"], posts[2].pk) + self.assertEquals(data["results"][2]["id"], posts[0].pk) + + def test_pagination_count(self): + rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=self.user)) + PostFactory.create_batch(size=80, rule=rule) + page_size = 50 + + self.client.force_login(self.user) + response = self.client.get(reverse("api:posts-list"), {"count": 50}) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(data["count"], 80) + self.assertEquals(len(data["results"]), page_size) + + def test_empty(self): + self.client.force_login(self.user) + response = self.client.get(reverse("api:posts-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertTrue("results" in data) + self.assertTrue("count" in data) + + self.assertEquals(data["count"], 0) + self.assertEquals(len(data["results"]), 0) + + def test_post(self): + self.client.force_login(self.user) + response = self.client.post(reverse("api:posts-list")) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "POST" not allowed.') + + def test_patch(self): + self.client.force_login(self.user) + response = self.client.patch(reverse("api:posts-list")) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "PATCH" not allowed.') + + def test_put(self): + self.client.force_login(self.user) + response = self.client.put(reverse("api:posts-list")) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "PUT" not allowed.') + + def test_delete(self): + self.client.force_login(self.user) + response = self.client.delete(reverse("api:posts-list")) + data = response.json() + + self.assertEquals(response.status_code, 405) + self.assertEquals(data["detail"], 'Method "DELETE" not allowed.') + + def test_posts_with_unauthenticated_user_without_category(self): + PostFactory.create_batch(size=3, rule=CollectionRuleFactory(user=self.user)) + + response = self.client.get(reverse("api:posts-list")) + + self.assertEquals(response.status_code, 403) + + def test_posts_with_unauthenticated_user_with_category(self): + category = CategoryFactory(user=self.user) + + PostFactory.create_batch( + size=3, rule=CollectionRuleFactory(user=self.user, category=category) + ) + + response = self.client.get(reverse("api:posts-list")) + + self.assertEquals(response.status_code, 403) + + def test_posts_with_unauthorized_user_without_category(self): + other_user = UserFactory() + + rule = CollectionRuleFactory(user=other_user, category=None) + PostFactory.create_batch(size=3, rule=rule) + + self.client.force_login(self.user) + response = self.client.get(reverse("api:posts-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(len(data["results"]), 0) + self.assertEquals(data["count"], 0) + + def test_posts_with_unauthorized_user_with_category(self): + other_user = UserFactory() + category = CategoryFactory(user=other_user) + + PostFactory.create_batch( + size=3, rule=CollectionRuleFactory(user=other_user, category=category) + ) + + self.client.force_login(self.user) + response = self.client.get(reverse("api:posts-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertEquals(len(data["results"]), 0) + self.assertEquals(data["count"], 0) + + # Note that this situation should not be possible, due to the user not being able + # to specify the user when creating categories/rules + def test_posts_with_authorized_rule_unauthorized_category(self): + other_user = UserFactory() + + rule = CollectionRuleFactory(user=self.user, category=CategoryFactory(user=other_user)) + PostFactory.create_batch(size=3, rule=rule) + + self.client.force_login(self.user) + response = self.client.get(reverse("api:posts-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertTrue("results" in data) + self.assertTrue("count" in data) + self.assertEquals(data["count"], 0) + + def test_posts_with_authorized_user_without_category(self): + UserFactory() + + rule = CollectionRuleFactory(user=self.user, category=None) + PostFactory.create_batch(size=3, rule=rule) + + self.client.force_login(self.user) + response = self.client.get(reverse("api:posts-list")) + data = response.json() + + self.assertEquals(response.status_code, 200) + self.assertTrue("results" in data) + self.assertTrue("count" in data) + self.assertEquals(data["count"], 3) diff --git a/src/newsreader/news/posts/tests/factories.py b/src/newsreader/news/core/tests/factories.py similarity index 68% rename from src/newsreader/news/posts/tests/factories.py rename to src/newsreader/news/core/tests/factories.py index a961b4f..c4c6d4c 100644 --- a/src/newsreader/news/posts/tests/factories.py +++ b/src/newsreader/news/core/tests/factories.py @@ -1,21 +1,19 @@ import factory import pytz -from newsreader.news.collection.tests.factories import CollectionRuleFactory -from newsreader.news.posts.models import Category, Post +from newsreader.auth.tests.factories import UserFactory +from newsreader.news.core.models import Category, Post class CategoryFactory(factory.django.DjangoModelFactory): + name = factory.Sequence(lambda n: "Category-{}".format(n)) + user = factory.SubFactory(UserFactory) + class Meta: model = Category - name = factory.Sequence(lambda n: "Category-{}".format(n)) - class PostFactory(factory.django.DjangoModelFactory): - class Meta: - model = Post - title = factory.Faker("sentence") body = factory.Faker("paragraph") author = factory.Faker("name") @@ -23,6 +21,7 @@ class PostFactory(factory.django.DjangoModelFactory): url = factory.Faker("url") remote_identifier = factory.Faker("url") - rule = factory.SubFactory(CollectionRuleFactory) + rule = factory.SubFactory("newsreader.news.collection.tests.factories.CollectionRuleFactory") - category = factory.SubFactory(CategoryFactory) + class Meta: + model = Post diff --git a/src/newsreader/news/core/urls.py b/src/newsreader/news/core/urls.py new file mode 100644 index 0000000..c207440 --- /dev/null +++ b/src/newsreader/news/core/urls.py @@ -0,0 +1,16 @@ +from django.urls import path + +from newsreader.news.core.views import ( + DetailCategoryAPIView, + DetailPostAPIView, + ListCategoryAPIView, + ListPostAPIView, +) + + +endpoints = [ + path("posts/", ListPostAPIView.as_view(), name="posts-list"), + path("posts//", DetailPostAPIView.as_view(), name="posts-detail"), + path("categories/", ListCategoryAPIView.as_view(), name="categories-list"), + path("categories//", DetailCategoryAPIView.as_view(), name="categories-detail"), +] diff --git a/src/newsreader/news/core/views.py b/src/newsreader/news/core/views.py new file mode 100644 index 0000000..5631796 --- /dev/null +++ b/src/newsreader/news/core/views.py @@ -0,0 +1,52 @@ +from django.db.models import Q + +from rest_framework.generics import ( + ListAPIView, + ListCreateAPIView, + RetrieveUpdateAPIView, + RetrieveUpdateDestroyAPIView, +) +from rest_framework.permissions import IsAuthenticated + +from newsreader.auth.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 + + +class ListPostAPIView(ListAPIView): + queryset = Post.objects.all() + serializer_class = PostSerializer + pagination_class = LargeResultSetPagination + permission_classes = (IsAuthenticated, IsPostOwner) + + def get_queryset(self): + user = self.request.user + initial_queryset = self.queryset.filter(rule__user=user) + return initial_queryset.filter( + Q(rule__category=None) | Q(rule__category__user=user) + ).order_by("rule", "-publication_date", "-created") + + +class DetailPostAPIView(RetrieveUpdateAPIView): + queryset = Post.objects.all() + serializer_class = PostSerializer + permission_classes = (IsAuthenticated, IsPostOwner) + + +class ListCategoryAPIView(ListCreateAPIView): + queryset = Category.objects.all() + serializer_class = CategorySerializer + pagination_class = ResultSetPagination + + def get_queryset(self): + user = self.request.user + return self.queryset.filter(user=user).order_by("-created", "-modified") + + +class DetailCategoryAPIView(RetrieveUpdateDestroyAPIView): + queryset = Category.objects.all() + serializer_class = CategorySerializer diff --git a/src/newsreader/news/posts/apps.py b/src/newsreader/news/posts/apps.py deleted file mode 100644 index d39b3e1..0000000 --- a/src/newsreader/news/posts/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class PostsConfig(AppConfig): - name = "posts" diff --git a/src/newsreader/news/posts/migrations/0002_auto_20190520_2206.py b/src/newsreader/news/posts/migrations/0002_auto_20190520_2206.py deleted file mode 100644 index 4365972..0000000 --- a/src/newsreader/news/posts/migrations/0002_auto_20190520_2206.py +++ /dev/null @@ -1,15 +0,0 @@ -# Generated by Django 2.2 on 2019-05-20 20:06 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [("posts", "0001_initial")] - - operations = [ - migrations.AlterModelOptions( - name="category", - options={"verbose_name": "Category", "verbose_name_plural": "Categories"}, - ) - ] diff --git a/src/newsreader/news/posts/migrations/0003_auto_20190520_2031.py b/src/newsreader/news/posts/migrations/0003_auto_20190520_2031.py deleted file mode 100644 index 0905440..0000000 --- a/src/newsreader/news/posts/migrations/0003_auto_20190520_2031.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 2.2 on 2019-05-20 20:31 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [("posts", "0002_auto_20190520_2206")] - - operations = [ - migrations.AlterField( - model_name="category", name="name", field=models.CharField(max_length=50, unique=True) - ) - ] diff --git a/src/newsreader/news/posts/migrations/0004_auto_20190521_1941.py b/src/newsreader/news/posts/migrations/0004_auto_20190521_1941.py deleted file mode 100644 index 4a0ceb2..0000000 --- a/src/newsreader/news/posts/migrations/0004_auto_20190521_1941.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.2 on 2019-05-21 19:41 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [("posts", "0003_auto_20190520_2031")] - - operations = [ - migrations.RemoveField(model_name="post", name="source"), - migrations.AddField( - model_name="post", - name="author", - field=models.CharField(blank=True, max_length=100, null=True), - ), - ] diff --git a/src/newsreader/news/posts/migrations/0005_auto_20190608_1054.py b/src/newsreader/news/posts/migrations/0005_auto_20190608_1054.py deleted file mode 100644 index 9a5e03a..0000000 --- a/src/newsreader/news/posts/migrations/0005_auto_20190608_1054.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.2 on 2019-06-08 10:54 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [("posts", "0004_auto_20190521_1941")] - - operations = [ - migrations.AlterField(model_name="post", name="body", field=models.TextField(blank=True)), - migrations.AlterField( - model_name="post", - name="remote_identifier", - field=models.CharField(blank=True, max_length=500, null=True), - ), - ] diff --git a/src/newsreader/news/posts/migrations/0006_auto_20190608_1520.py b/src/newsreader/news/posts/migrations/0006_auto_20190608_1520.py deleted file mode 100644 index 7a5f7ec..0000000 --- a/src/newsreader/news/posts/migrations/0006_auto_20190608_1520.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 2.2 on 2019-06-08 15:20 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [("posts", "0005_auto_20190608_1054")] - - operations = [ - migrations.AlterField( - model_name="post", name="body", field=models.TextField(blank=True, null=True) - ), - migrations.AlterField( - model_name="post", - name="publication_date", - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AlterField( - model_name="post", - name="title", - field=models.CharField(blank=True, max_length=200, null=True), - ), - migrations.AlterField( - model_name="post", name="url", field=models.URLField(blank=True, null=True) - ), - ] diff --git a/src/newsreader/news/posts/views.py b/src/newsreader/news/posts/views.py deleted file mode 100644 index dc1ba72..0000000 --- a/src/newsreader/news/posts/views.py +++ /dev/null @@ -1,4 +0,0 @@ -from django.shortcuts import render - - -# Create your views here. diff --git a/src/newsreader/urls.py b/src/newsreader/urls.py index ed82d43..e8a3b87 100644 --- a/src/newsreader/urls.py +++ b/src/newsreader/urls.py @@ -1,5 +1,20 @@ +from django.conf import settings from django.contrib import admin from django.urls import include, path +from newsreader.news.collection.urls import endpoints as collection_endpoints +from newsreader.news.core.urls import endpoints as core_endpoints -urlpatterns = [path("admin/", admin.site.urls)] + +endpoints = collection_endpoints + core_endpoints + +urlpatterns = [ + path("admin/", admin.site.urls, name="admin"), + path("api/", include((endpoints, "api")), name="api"), +] + + +if settings.DEBUG: + import debug_toolbar + + urlpatterns = [path("debug/", include(debug_toolbar.urls))] + urlpatterns