Add post/category/rule endpoints
This commit is contained in:
parent
982c5bb132
commit
9c6be7357d
99 changed files with 2174 additions and 951 deletions
|
|
@ -3,6 +3,7 @@ beautifulsoup4==4.7.1
|
||||||
certifi==2019.3.9
|
certifi==2019.3.9
|
||||||
chardet==3.0.4
|
chardet==3.0.4
|
||||||
Django==2.2
|
Django==2.2
|
||||||
|
djangorestframework==3.9.4
|
||||||
lxml==4.3.4
|
lxml==4.3.4
|
||||||
feedparser==5.2.1
|
feedparser==5.2.1
|
||||||
idna==2.8
|
idna==2.8
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,5 @@
|
||||||
|
|
||||||
factory-boy==2.12.0
|
factory-boy==2.12.0
|
||||||
freezegun==0.3.12
|
freezegun==0.3.12
|
||||||
|
django-debug-toolbar==2.0
|
||||||
|
django-extensions==2.1.9
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import sys
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'newsreader.conf.base')
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "newsreader.conf.dev")
|
||||||
try:
|
try:
|
||||||
from django.core.management import execute_from_command_line
|
from django.core.management import execute_from_command_line
|
||||||
except ImportError as exc:
|
except ImportError as exc:
|
||||||
|
|
@ -17,5 +17,5 @@ def main():
|
||||||
execute_from_command_line(sys.argv)
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
|
||||||
1
src/newsreader/auth/admin.py
Normal file
1
src/newsreader/auth/admin.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# Register your models here.
|
||||||
5
src/newsreader/auth/apps.py
Normal file
5
src/newsreader/auth/apps.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AuthConfig(AppConfig):
|
||||||
|
name = "auth"
|
||||||
15
src/newsreader/auth/backends.py
Normal file
15
src/newsreader/auth/backends.py
Normal 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
|
||||||
23
src/newsreader/auth/permissions.py
Normal file
23
src/newsreader/auth/permissions.py
Normal 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
|
||||||
0
src/newsreader/auth/tests/__init__.py
Normal file
0
src/newsreader/auth/tests/__init__.py
Normal file
20
src/newsreader/auth/tests/factories.py
Normal file
20
src/newsreader/auth/tests/factories.py
Normal 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
|
||||||
1
src/newsreader/auth/views.py
Normal file
1
src/newsreader/auth/views.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# Create your views here.
|
||||||
|
|
@ -26,6 +26,7 @@ SECRET_KEY = "^!7a2jq5j!exc-55vf$anx9^6ff6=u_ub5=5p1(1x47fix)syh"
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
|
|
||||||
ALLOWED_HOSTS = ["127.0.0.1"]
|
ALLOWED_HOSTS = ["127.0.0.1"]
|
||||||
|
INTERNAL_IPS = ["127.0.0.1"]
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
|
|
@ -35,9 +36,11 @@ INSTALLED_APPS = [
|
||||||
"django.contrib.sessions",
|
"django.contrib.sessions",
|
||||||
"django.contrib.messages",
|
"django.contrib.messages",
|
||||||
"django.contrib.staticfiles",
|
"django.contrib.staticfiles",
|
||||||
|
# third party apps
|
||||||
|
"rest_framework",
|
||||||
# app modules
|
# app modules
|
||||||
|
"newsreader.news.core",
|
||||||
"newsreader.news.collection",
|
"newsreader.news.collection",
|
||||||
"newsreader.news.posts",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
|
@ -89,6 +92,9 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||||
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
|
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
AUTHENTICATION_BACKENDS = ["newsreader.auth.backends.EmailBackend"]
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/2.2/topics/i18n/
|
# https://docs.djangoproject.com/en/2.2/topics/i18n/
|
||||||
LANGUAGE_CODE = "en-us"
|
LANGUAGE_CODE = "en-us"
|
||||||
|
|
@ -101,3 +107,12 @@ USE_TZ = True
|
||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/2.2/howto/static-files/
|
# https://docs.djangoproject.com/en/2.2/howto/static-files/
|
||||||
STATIC_URL = "/static/"
|
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",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
from .base import *
|
from .base import *
|
||||||
|
|
||||||
|
|
||||||
# Development settings
|
|
||||||
|
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
|
|
||||||
|
MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"]
|
||||||
|
|
||||||
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||||
|
|
||||||
|
INSTALLED_APPS += ["debug_toolbar", "django_extensions"]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from .local import *
|
pass
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1 @@
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
|
|
||||||
# Register your models here.
|
# Register your models here.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
|
||||||
class TimeStampedModel(models.Model):
|
class TimeStampedModel(models.Model):
|
||||||
|
|
@ -7,7 +8,7 @@ class TimeStampedModel(models.Model):
|
||||||
updating ``created`` and ``modified`` fields.
|
updating ``created`` and ``modified`` fields.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(default=timezone.now)
|
||||||
modified = models.DateTimeField(auto_now=True)
|
modified = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
||||||
12
src/newsreader/core/pagination.py
Normal file
12
src/newsreader/core/pagination.py
Normal 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
|
||||||
6
src/newsreader/core/permissions.py
Normal file
6
src/newsreader/core/permissions.py
Normal 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
|
||||||
|
|
@ -1,4 +1 @@
|
||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
|
|
||||||
# Create your tests here.
|
# Create your tests here.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1 @@
|
||||||
from django.shortcuts import render
|
|
||||||
|
|
||||||
|
|
||||||
# Create your views here.
|
# Create your views here.
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,5 @@
|
||||||
from typing import ContextManager, Dict, List, Optional, Tuple
|
from typing import ContextManager, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
from newsreader.news.collection.exceptions import StreamParseException
|
from newsreader.news.collection.exceptions import StreamParseException
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,8 @@ from newsreader.news.collection.exceptions import (
|
||||||
StreamTimeOutException,
|
StreamTimeOutException,
|
||||||
)
|
)
|
||||||
from newsreader.news.collection.models import CollectionRule
|
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.collection.utils import build_publication_date, fetch
|
||||||
from newsreader.news.posts.models import Post
|
from newsreader.news.core.models import Post
|
||||||
|
|
||||||
|
|
||||||
class FeedBuilder(Builder):
|
class FeedBuilder(Builder):
|
||||||
|
|
@ -62,7 +61,7 @@ class FeedBuilder(Builder):
|
||||||
tz = pytz.timezone(rule.timezone)
|
tz = pytz.timezone(rule.timezone)
|
||||||
|
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
data = {"rule_id": rule.pk, "category": rule.category}
|
data = {"rule_id": rule.pk}
|
||||||
|
|
||||||
for field, value in field_mapping.items():
|
for field, value in field_mapping.items():
|
||||||
if field in entry:
|
if field in entry:
|
||||||
|
|
|
||||||
|
|
@ -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.feed import FeedCollector
|
||||||
from newsreader.news.collection.models import CollectionRule
|
from newsreader.news.collection.models import CollectionRule
|
||||||
|
|
|
||||||
|
|
@ -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
|
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"
|
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)),
|
("name", models.CharField(max_length=100)),
|
||||||
("url", models.URLField()),
|
("url", models.URLField(max_length=1024)),
|
||||||
("last_suceeded", models.DateTimeField()),
|
(
|
||||||
|
"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)),
|
("succeeded", models.BooleanField(default=False)),
|
||||||
|
("error", models.CharField(blank=True, max_length=255, null=True)),
|
||||||
],
|
],
|
||||||
|
options={"abstract": False},
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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),
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
# Generated by Django 2.2 on 2019-05-20 20:06
|
# Generated by Django 2.2 on 2019-07-05 20:59
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
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 = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
|
|
@ -18,7 +19,7 @@ class Migration(migrations.Migration):
|
||||||
help_text="Posts from this rule will be tagged with this category",
|
help_text="Posts from this rule will be tagged with this category",
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
to="posts.Category",
|
to="core.Category",
|
||||||
verbose_name="Category",
|
verbose_name="Category",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
@ -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,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
@ -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,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
@ -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),
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
@ -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/"),
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
@ -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),
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
@ -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),
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
from django.conf import settings
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
|
from newsreader.core.models import TimeStampedModel
|
||||||
|
|
||||||
class CollectionRule(models.Model):
|
|
||||||
|
class CollectionRule(TimeStampedModel):
|
||||||
name = models.CharField(max_length=100)
|
name = models.CharField(max_length=100)
|
||||||
source = models.CharField(max_length=100)
|
|
||||||
|
|
||||||
url = models.URLField(max_length=1024)
|
url = models.URLField(max_length=1024)
|
||||||
website_url = models.URLField(max_length=1024, editable=False, blank=True, null=True)
|
website_url = models.URLField(max_length=1024, editable=False, blank=True, null=True)
|
||||||
|
|
@ -20,7 +20,7 @@ class CollectionRule(models.Model):
|
||||||
)
|
)
|
||||||
|
|
||||||
category = models.ForeignKey(
|
category = models.ForeignKey(
|
||||||
"posts.Category",
|
"core.Category",
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name=_("Category"),
|
verbose_name=_("Category"),
|
||||||
|
|
@ -30,8 +30,9 @@ class CollectionRule(models.Model):
|
||||||
|
|
||||||
last_suceeded = models.DateTimeField(blank=True, null=True)
|
last_suceeded = models.DateTimeField(blank=True, null=True)
|
||||||
succeeded = models.BooleanField(default=False)
|
succeeded = models.BooleanField(default=False)
|
||||||
|
|
||||||
error = models.CharField(max_length=255, blank=True, null=True)
|
error = models.CharField(max_length=255, blank=True, null=True)
|
||||||
|
|
||||||
|
user = models.ForeignKey("auth.User", _("Owner"))
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
from typing import ContextManager
|
from typing import ContextManager
|
||||||
|
|
||||||
from requests import Response
|
|
||||||
from requests.exceptions import ConnectionError as RequestConnectionError
|
from requests.exceptions import ConnectionError as RequestConnectionError
|
||||||
from requests.exceptions import (
|
from requests.exceptions import (
|
||||||
HTTPError,
|
HTTPError,
|
||||||
|
|
|
||||||
21
src/newsreader/news/collection/serializers.py
Normal file
21
src/newsreader/news/collection/serializers.py
Normal 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"}}
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
from .favicon import *
|
|
||||||
from .feed import *
|
|
||||||
from .tests import *
|
|
||||||
from .utils import *
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -1,13 +1,17 @@
|
||||||
import factory
|
import factory
|
||||||
|
|
||||||
|
from newsreader.auth.tests.factories import UserFactory
|
||||||
from newsreader.news.collection.models import CollectionRule
|
from newsreader.news.collection.models import CollectionRule
|
||||||
|
|
||||||
|
|
||||||
class CollectionRuleFactory(factory.django.DjangoModelFactory):
|
class CollectionRuleFactory(factory.django.DjangoModelFactory):
|
||||||
class Meta:
|
|
||||||
model = CollectionRule
|
|
||||||
|
|
||||||
name = factory.Sequence(lambda n: "CollectionRule-{}".format(n))
|
name = factory.Sequence(lambda n: "CollectionRule-{}".format(n))
|
||||||
source = factory.Faker("name")
|
|
||||||
url = factory.Faker("url")
|
url = factory.Faker("url")
|
||||||
website_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
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
from .builder import *
|
|
||||||
from .client import *
|
|
||||||
from .collector import *
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
from .tests import *
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from freezegun import freeze_time
|
|
||||||
|
|
||||||
from newsreader.news.collection.favicon import FaviconBuilder
|
from newsreader.news.collection.favicon import FaviconBuilder
|
||||||
from newsreader.news.collection.tests.factories import CollectionRuleFactory
|
from newsreader.news.collection.tests.factories import CollectionRuleFactory
|
||||||
from newsreader.news.collection.tests.favicon.builder.mocks import *
|
from newsreader.news.collection.tests.favicon.builder.mocks import *
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
from .tests import *
|
|
||||||
|
|
@ -2,7 +2,6 @@ from unittest.mock import MagicMock
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from newsreader.news.collection.base import WebsiteStream
|
|
||||||
from newsreader.news.collection.exceptions import (
|
from newsreader.news.collection.exceptions import (
|
||||||
StreamDeniedException,
|
StreamDeniedException,
|
||||||
StreamException,
|
StreamException,
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
from .tests import *
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
import pytz
|
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
from .builder import *
|
|
||||||
from .client import *
|
|
||||||
from .collector import *
|
|
||||||
from .duplicate_handler import *
|
|
||||||
from .stream import *
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
from .tests import *
|
|
||||||
|
|
@ -10,8 +10,8 @@ from freezegun import freeze_time
|
||||||
|
|
||||||
from newsreader.news.collection.feed import FeedBuilder
|
from newsreader.news.collection.feed import FeedBuilder
|
||||||
from newsreader.news.collection.tests.factories import CollectionRuleFactory
|
from newsreader.news.collection.tests.factories import CollectionRuleFactory
|
||||||
from newsreader.news.posts.models import Post
|
from newsreader.news.core.models import Post
|
||||||
from newsreader.news.posts.tests.factories import PostFactory
|
from newsreader.news.core.tests.factories import PostFactory
|
||||||
|
|
||||||
from .mocks import *
|
from .mocks import *
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
from .tests import *
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
from newsreader.news.collection.exceptions import (
|
from newsreader.news.collection.exceptions import (
|
||||||
StreamDeniedException,
|
StreamDeniedException,
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
from .tests import *
|
|
||||||
|
|
@ -20,8 +20,8 @@ from newsreader.news.collection.exceptions import (
|
||||||
from newsreader.news.collection.feed import FeedCollector
|
from newsreader.news.collection.feed import FeedCollector
|
||||||
from newsreader.news.collection.tests.factories import CollectionRuleFactory
|
from newsreader.news.collection.tests.factories import CollectionRuleFactory
|
||||||
from newsreader.news.collection.utils import build_publication_date
|
from newsreader.news.collection.utils import build_publication_date
|
||||||
from newsreader.news.posts.models import Post
|
from newsreader.news.core.models import Post
|
||||||
from newsreader.news.posts.tests.factories import PostFactory
|
from newsreader.news.core.tests.factories import PostFactory
|
||||||
|
|
||||||
from .mocks import (
|
from .mocks import (
|
||||||
duplicate_mock,
|
duplicate_mock,
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
from .tests import *
|
|
||||||
|
|
@ -3,8 +3,7 @@ from django.utils import timezone
|
||||||
|
|
||||||
from newsreader.news.collection.feed import FeedDuplicateHandler
|
from newsreader.news.collection.feed import FeedDuplicateHandler
|
||||||
from newsreader.news.collection.tests.factories import CollectionRuleFactory
|
from newsreader.news.collection.tests.factories import CollectionRuleFactory
|
||||||
from newsreader.news.posts.models import Post
|
from newsreader.news.core.tests.factories import PostFactory
|
||||||
from newsreader.news.posts.tests.factories import PostFactory
|
|
||||||
|
|
||||||
|
|
||||||
class FeedDuplicateHandlerTestCase(TestCase):
|
class FeedDuplicateHandlerTestCase(TestCase):
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
from .tests import *
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
from newsreader.news.collection.exceptions import (
|
from newsreader.news.collection.exceptions import (
|
||||||
StreamDeniedException,
|
StreamDeniedException,
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
from .tests import *
|
|
||||||
12
src/newsreader/news/collection/urls.py
Normal file
12
src/newsreader/news/collection/urls.py
Normal 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"),
|
||||||
|
]
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
0
src/newsreader/news/core/__init__.py
Normal file
0
src/newsreader/news/core/__init__.py
Normal file
|
|
@ -1,6 +1,6 @@
|
||||||
from django.contrib import admin
|
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):
|
class PostAdmin(admin.ModelAdmin):
|
||||||
|
|
@ -10,7 +10,7 @@ class PostAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
ordering = ("-publication_date", "title")
|
ordering = ("-publication_date", "title")
|
||||||
|
|
||||||
fields = ("title", "body", "author", "publication_date", "url", "remote_identifier", "category")
|
fields = ("title", "body", "author", "publication_date", "url")
|
||||||
|
|
||||||
search_fields = ["title"]
|
search_fields = ["title"]
|
||||||
|
|
||||||
5
src/newsreader/news/core/apps.py
Normal file
5
src/newsreader/news/core/apps.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class CoreConfig(AppConfig):
|
||||||
|
name = "core"
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
# Generated by Django 2.2 on 2019-04-10 20:10
|
# Generated by Django 2.2 on 2019-07-05 20:59
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
@ -21,11 +21,11 @@ class Migration(migrations.Migration):
|
||||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
("created", models.DateTimeField(auto_now_add=True)),
|
("created", models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
("modified", models.DateTimeField(auto_now=True)),
|
("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(
|
migrations.CreateModel(
|
||||||
name="Post",
|
name="Post",
|
||||||
|
|
@ -36,27 +36,23 @@ class Migration(migrations.Migration):
|
||||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
("created", models.DateTimeField(auto_now_add=True)),
|
("created", models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
("modified", models.DateTimeField(auto_now=True)),
|
("modified", models.DateTimeField(auto_now=True)),
|
||||||
("title", models.CharField(max_length=200)),
|
("title", models.CharField(blank=True, max_length=200, null=True)),
|
||||||
("body", models.TextField()),
|
("body", models.TextField(blank=True, null=True)),
|
||||||
("source", models.CharField(max_length=200)),
|
("author", models.CharField(blank=True, max_length=200, null=True)),
|
||||||
("publication_date", models.DateTimeField()),
|
("publication_date", models.DateTimeField(blank=True, null=True)),
|
||||||
("url", models.URLField()),
|
("url", models.URLField(blank=True, max_length=1024, null=True)),
|
||||||
("remote_identifier", models.CharField(max_length=500)),
|
|
||||||
(
|
(
|
||||||
"category",
|
"remote_identifier",
|
||||||
models.ForeignKey(
|
models.CharField(blank=True, editable=False, max_length=500, null=True),
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.PROTECT,
|
|
||||||
to="posts.Category",
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"rule",
|
"rule",
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
on_delete=django.db.models.deletion.CASCADE, to="collection.CollectionRule"
|
editable=False,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="collection.CollectionRule",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
21
src/newsreader/news/core/migrations/0002_category_user.py
Normal file
21
src/newsreader/news/core/migrations/0002_category_user.py
Normal 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,
|
||||||
|
)
|
||||||
|
]
|
||||||
0
src/newsreader/news/core/migrations/__init__.py
Normal file
0
src/newsreader/news/core/migrations/__init__.py
Normal file
|
|
@ -8,14 +8,12 @@ from newsreader.news.collection.models import CollectionRule
|
||||||
class Post(TimeStampedModel):
|
class Post(TimeStampedModel):
|
||||||
title = models.CharField(max_length=200, blank=True, null=True)
|
title = models.CharField(max_length=200, blank=True, null=True)
|
||||||
body = models.TextField(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)
|
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)
|
rule = models.ForeignKey(CollectionRule, on_delete=models.CASCADE, editable=False)
|
||||||
remote_identifier = models.CharField(max_length=500, blank=True, null=True)
|
remote_identifier = models.CharField(max_length=500, blank=True, null=True, editable=False)
|
||||||
|
|
||||||
category = models.ForeignKey("Category", blank=True, null=True, on_delete=models.PROTECT)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "Post-{}".format(self.pk)
|
return "Post-{}".format(self.pk)
|
||||||
|
|
@ -23,6 +21,7 @@ class Post(TimeStampedModel):
|
||||||
|
|
||||||
class Category(TimeStampedModel):
|
class Category(TimeStampedModel):
|
||||||
name = models.CharField(max_length=50, unique=True)
|
name = models.CharField(max_length=50, unique=True)
|
||||||
|
user = models.ForeignKey("auth.User", _("Owner"))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Category")
|
verbose_name = _("Category")
|
||||||
31
src/newsreader/news/core/pagination.py
Normal file
31
src/newsreader/news/core/pagination.py
Normal 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",
|
||||||
|
)
|
||||||
39
src/newsreader/news/core/serializers.py
Normal file
39
src/newsreader/news/core/serializers.py
Normal 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"}}
|
||||||
0
src/newsreader/news/core/tests/__init__.py
Normal file
0
src/newsreader/news/core/tests/__init__.py
Normal file
0
src/newsreader/news/core/tests/endpoints/__init__.py
Normal file
0
src/newsreader/news/core/tests/endpoints/__init__.py
Normal 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)
|
||||||
186
src/newsreader/news/core/tests/endpoints/category/list/tests.py
Normal file
186
src/newsreader/news/core/tests/endpoints/category/list/tests.py
Normal 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)
|
||||||
175
src/newsreader/news/core/tests/endpoints/post/detail/tests.py
Normal file
175
src/newsreader/news/core/tests/endpoints/post/detail/tests.py
Normal 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)
|
||||||
208
src/newsreader/news/core/tests/endpoints/post/list/tests.py
Normal file
208
src/newsreader/news/core/tests/endpoints/post/list/tests.py
Normal 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)
|
||||||
|
|
@ -1,21 +1,19 @@
|
||||||
import factory
|
import factory
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
from newsreader.news.collection.tests.factories import CollectionRuleFactory
|
from newsreader.auth.tests.factories import UserFactory
|
||||||
from newsreader.news.posts.models import Category, Post
|
from newsreader.news.core.models import Category, Post
|
||||||
|
|
||||||
|
|
||||||
class CategoryFactory(factory.django.DjangoModelFactory):
|
class CategoryFactory(factory.django.DjangoModelFactory):
|
||||||
|
name = factory.Sequence(lambda n: "Category-{}".format(n))
|
||||||
|
user = factory.SubFactory(UserFactory)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Category
|
model = Category
|
||||||
|
|
||||||
name = factory.Sequence(lambda n: "Category-{}".format(n))
|
|
||||||
|
|
||||||
|
|
||||||
class PostFactory(factory.django.DjangoModelFactory):
|
class PostFactory(factory.django.DjangoModelFactory):
|
||||||
class Meta:
|
|
||||||
model = Post
|
|
||||||
|
|
||||||
title = factory.Faker("sentence")
|
title = factory.Faker("sentence")
|
||||||
body = factory.Faker("paragraph")
|
body = factory.Faker("paragraph")
|
||||||
author = factory.Faker("name")
|
author = factory.Faker("name")
|
||||||
|
|
@ -23,6 +21,7 @@ class PostFactory(factory.django.DjangoModelFactory):
|
||||||
url = factory.Faker("url")
|
url = factory.Faker("url")
|
||||||
remote_identifier = factory.Faker("url")
|
remote_identifier = factory.Faker("url")
|
||||||
|
|
||||||
rule = factory.SubFactory(CollectionRuleFactory)
|
rule = factory.SubFactory("newsreader.news.collection.tests.factories.CollectionRuleFactory")
|
||||||
|
|
||||||
category = factory.SubFactory(CategoryFactory)
|
class Meta:
|
||||||
|
model = Post
|
||||||
16
src/newsreader/news/core/urls.py
Normal file
16
src/newsreader/news/core/urls.py
Normal 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"),
|
||||||
|
]
|
||||||
52
src/newsreader/news/core/views.py
Normal file
52
src/newsreader/news/core/views.py
Normal 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
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class PostsConfig(AppConfig):
|
|
||||||
name = "posts"
|
|
||||||
|
|
@ -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"},
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
@ -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)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
@ -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)
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
from django.shortcuts import render
|
|
||||||
|
|
||||||
|
|
||||||
# Create your views here.
|
|
||||||
|
|
@ -1,5 +1,20 @@
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import include, path
|
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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue