0.2.3 #99

Merged
sonny merged 112 commits from development into master 2020-05-23 16:58:42 +02:00
99 changed files with 2174 additions and 951 deletions
Showing only changes of commit 9c6be7357d - Show all commits

View file

@ -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

View file

@ -2,3 +2,5 @@
factory-boy==2.12.0
freezegun==0.3.12
django-debug-toolbar==2.0
django-extensions==2.1.9

View file

@ -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()

View file

@ -0,0 +1 @@
# Register your models here.

View file

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

View file

@ -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

View file

@ -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

View file

View file

@ -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

View file

@ -0,0 +1 @@
# Create your views here.

View file

@ -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",
),
}

View file

@ -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

View file

@ -1,4 +1 @@
from django.contrib import admin
# Register your models here.

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -1,4 +1 @@
from django.test import TestCase
# Create your tests here.

View file

@ -1,4 +1 @@
from django.shortcuts import render
# Create your views here.

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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},
)
]

View file

@ -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),
)
]

View file

@ -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",
),
)

View file

@ -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,
)
]

View file

@ -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,
),
)
]

View file

@ -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,
),
]

View file

@ -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),
)
]

View file

@ -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/"),
)
]

View file

@ -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),
)
]

View file

@ -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),
)
]

View file

@ -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),
),
]

View file

@ -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

View file

@ -1,6 +1,5 @@
from typing import ContextManager
from requests import Response
from requests.exceptions import ConnectionError as RequestConnectionError
from requests.exceptions import (
HTTPError,

View file

@ -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"}}

View file

@ -1,4 +0,0 @@
from .favicon import *
from .feed import *
from .tests import *
from .utils import *

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -1,3 +0,0 @@
from .builder import *
from .client import *
from .collector import *

View file

@ -1 +0,0 @@
from .tests import *

View file

@ -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 *

View file

@ -1 +0,0 @@
from .tests import *

View file

@ -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,

View file

@ -1 +0,0 @@
from .tests import *

View file

@ -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

View file

@ -1,5 +0,0 @@
from .builder import *
from .client import *
from .collector import *
from .duplicate_handler import *
from .stream import *

View file

@ -1 +0,0 @@
from .tests import *

View file

@ -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 *

View file

@ -1 +0,0 @@
from .tests import *

View file

@ -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,

View file

@ -1 +0,0 @@
from .tests import *

View file

@ -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,

View file

@ -1 +0,0 @@
from .tests import *

View file

@ -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):

View file

@ -1 +0,0 @@
from .tests import *

View file

@ -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,

View file

@ -1 +0,0 @@
from .tests import *

View file

@ -0,0 +1,12 @@
from django.urls import path
from newsreader.news.collection.views import (
CollectionRuleAPIListView,
CollectionRuleDetailView,
)
endpoints = [
path("rules/<int:pk>", CollectionRuleDetailView.as_view(), name="rules-detail"),
path("rules/", CollectionRuleAPIListView.as_view(), name="rules-list"),
]

View file

@ -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

View file

View file

@ -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"]

View file

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

View file

@ -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",
),
),
],

View file

@ -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,
)
]

View file

@ -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")

View file

@ -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",
)

View file

@ -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"}}

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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/<int:pk>/", DetailPostAPIView.as_view(), name="posts-detail"),
path("categories/", ListCategoryAPIView.as_view(), name="categories-list"),
path("categories/<int:pk>/", DetailCategoryAPIView.as_view(), name="categories-detail"),
]

View file

@ -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

View file

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

View file

@ -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"},
)
]

View file

@ -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)
)
]

View file

@ -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),
),
]

View file

@ -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),
),
]

View file

@ -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)
),
]

View file

@ -1,4 +0,0 @@
from django.shortcuts import render
# Create your views here.

View file

@ -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