From 2c684ca07934cfbdd83b2f0310bb4966351cbb09 Mon Sep 17 00:00:00 2001 From: Arthur Pemberton Date: Fri, 17 Apr 2020 01:55:43 -0400 Subject: [PATCH 1/7] initial commit of the web api --- dbdb/api/api_v202004/__init__.py | 0 dbdb/api/api_v202004/apps.py | 5 + dbdb/api/api_v202004/serializers.py | 140 ++++++++++++++++++++++++++++ dbdb/api/api_v202004/urls.py | 15 +++ dbdb/api/api_v202004/views.py | 34 +++++++ dbdb/api/pagination.py | 12 +++ dbdb/api/urls.py | 9 ++ dbdb/core/models.py | 11 +++ dbdb/settings.py | 15 ++- dbdb/urls.py | 1 + 10 files changed, 239 insertions(+), 3 deletions(-) create mode 100644 dbdb/api/api_v202004/__init__.py create mode 100644 dbdb/api/api_v202004/apps.py create mode 100644 dbdb/api/api_v202004/serializers.py create mode 100644 dbdb/api/api_v202004/urls.py create mode 100644 dbdb/api/api_v202004/views.py create mode 100644 dbdb/api/pagination.py create mode 100644 dbdb/api/urls.py diff --git a/dbdb/api/api_v202004/__init__.py b/dbdb/api/api_v202004/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dbdb/api/api_v202004/apps.py b/dbdb/api/api_v202004/apps.py new file mode 100644 index 00000000..86aae4f9 --- /dev/null +++ b/dbdb/api/api_v202004/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ApiV202004Config(AppConfig): + name = 'api_v202004' diff --git a/dbdb/api/api_v202004/serializers.py b/dbdb/api/api_v202004/serializers.py new file mode 100644 index 00000000..7a8e0241 --- /dev/null +++ b/dbdb/api/api_v202004/serializers.py @@ -0,0 +1,140 @@ +# stdlib imports +import collections +# third-party imports +from rest_framework import serializers +from rest_framework.reverse import reverse +# project imports +from dbdb.core.models import FeatureOption +from dbdb.core.models import System +from dbdb.core.models import SystemFeature + + +# serializers + +class SystemSerializer(serializers.HyperlinkedModelSerializer): + + class Meta: + model = System + fields = [ + 'href', + 'name', + 'former_names', + 'start_year', + 'end_year', + 'description', + 'history', + 'acquired_by', + 'developer', + 'countries', + 'features', + 'project_types', + 'urls', + 'version', + ] + + href = serializers.SerializerMethodField() + former_names = serializers.SerializerMethodField() + start_year = serializers.SerializerMethodField() + end_year = serializers.SerializerMethodField() + description = serializers.SerializerMethodField() + history = serializers.SerializerMethodField() + acquired_by = serializers.SerializerMethodField() + developer = serializers.SerializerMethodField() + countries = serializers.SerializerMethodField() + features = serializers.SerializerMethodField() + project_types = serializers.SerializerMethodField() + urls = serializers.SerializerMethodField() + version = serializers.IntegerField(source='ver', read_only=True) + + def get_acquired_by(self, obj): + current = obj.get_current() + return current.acquired_by + + def get_countries(self, obj): + current = obj.get_current() + return list( map(str, current.countries) ) + + def get_description(self, obj): + current = obj.get_current() + return current.description + + def get_developer(self, obj): + current = obj.get_current() + return current.developer + + def get_features(self, obj): + current = obj.get_current() + + sysfeatures = SystemFeature.objects \ + .filter(system=current) \ + .select_related('feature') \ + .order_by('feature__slug') + + items = collections.OrderedDict() + for sysfeature in sysfeatures: + empty = True + + f = { + 'name': sysfeature.feature.label, + } + + if sysfeature.feature.multivalued: + f['values'] = [] + pass + else: + f['value'] = None + pass + + if sysfeature.feature.multivalued: + for option in sysfeature.options.all().order_by('slug'): + f['values'].append(option.value) + empty = False + else: + option = sysfeature.options.first() + if option: + f['value'] = option.value + empty = False + pass + + if not empty: + items[sysfeature.feature_id] = f + pass + + return list( items.values() ) + + def get_former_names(self, obj): + current = obj.get_current() + return current.former_names if current.former_names else None + + def get_history(self, obj): + current = obj.get_current() + return current.history + + def get_href(self, obj): + request = self.context['request'] + return reverse('system', args=[obj.slug], request=request) + + def get_end_year(self, obj): + current = obj.get_current() + return current.end_year + + def get_project_types(self, obj): + current = obj.get_current() + return list( map(str, current.project_types.all()) ) + + def get_urls(self, obj): + current = obj.get_current() + data = { + 'docs': None if not current.tech_docs else current.tech_docs, + 'homepage': None if not current.url else current.url, + 'source': None if not current.source_url else current.source_url, + 'wikipedia': None if not current.wikipedia_url else current.wikipedia_url, + } + return data + + def get_start_year(self, obj): + current = obj.get_current() + return current.start_year + + pass + diff --git a/dbdb/api/api_v202004/urls.py b/dbdb/api/api_v202004/urls.py new file mode 100644 index 00000000..a274ef7a --- /dev/null +++ b/dbdb/api/api_v202004/urls.py @@ -0,0 +1,15 @@ +# django imports +from django.urls import path +# third-party imports +from rest_framework.urlpatterns import format_suffix_patterns +# local imports +from . import views + + +app_name = 'api_v202004' + +urlpatterns = [ + path('systems', views.SystemsView.as_view(), name='systems'), +] + +urlpatterns = format_suffix_patterns(urlpatterns) diff --git a/dbdb/api/api_v202004/views.py b/dbdb/api/api_v202004/views.py new file mode 100644 index 00000000..ab2c5672 --- /dev/null +++ b/dbdb/api/api_v202004/views.py @@ -0,0 +1,34 @@ +# django imports +from django.shortcuts import render +# third-party imports +from rest_framework import generics +# project imports +from dbdb.core.models import System +from dbdb.core.models import SystemVersion +# local imports +from .serializers import SystemSerializer + + +# class based views + +class SystemsView(generics.ListAPIView): + + queryset = System.objects.all() + serializer_class = SystemSerializer + + def paginate_queryset(self, queryset): + if self.paginator is None: + return None + + items = self.paginator.paginate_queryset(queryset, self.request, view=self) + + # db optimizations - versions + ids = { item.id for item in items } + versions = SystemVersion.objects.filter(system_id__in=ids, is_current=True) + versions_map = { sv.system_id : sv for sv in versions } + for item in items: + item._current = versions_map[item.id] + + return items + + pass diff --git a/dbdb/api/pagination.py b/dbdb/api/pagination.py new file mode 100644 index 00000000..8376c3b3 --- /dev/null +++ b/dbdb/api/pagination.py @@ -0,0 +1,12 @@ +# third-party imports +from rest_framework.pagination import LimitOffsetPagination + + +# classes + +class StandardPagination(LimitOffsetPagination): + + default_limit = 10 + max_limit = 100 + + pass diff --git a/dbdb/api/urls.py b/dbdb/api/urls.py new file mode 100644 index 00000000..a798cb8a --- /dev/null +++ b/dbdb/api/urls.py @@ -0,0 +1,9 @@ +# django imports +from django.urls import path +from django.urls import include +from django.conf.urls import url + + +urlpatterns = [ + path('v202004/', include('dbdb.api.api_v202004.urls')), +] diff --git a/dbdb/core/models.py b/dbdb/core/models.py index eaeb6088..a84c02bc 100644 --- a/dbdb/core/models.py +++ b/dbdb/core/models.py @@ -208,6 +208,17 @@ def current(self): def get_absolute_url(self): return reverse('system', args=[self.slug]) + + def get_current(self): + if not hasattr(self, '_current'): + if self.id is None: + self._current = SystemVersion(system=self) + else: + print('get version', self.slug) + self._current = self.versions.get(is_current=True) + pass + + return self._current pass diff --git a/dbdb/settings.py b/dbdb/settings.py index 33e5b6e7..4c173207 100644 --- a/dbdb/settings.py +++ b/dbdb/settings.py @@ -21,13 +21,14 @@ 'django.contrib.messages', 'django.contrib.staticfiles', - #'autoslug', 'bootstrap4', - 'easy_thumbnails', 'django_countries', + 'easy_thumbnails', 'haystack', # django-haystack + 'rest_framework', # djangorestframework - 'dbdb.core' + 'dbdb.core', + 'dbdb.api.api_v202004', ] MIDDLEWARE = [ @@ -157,6 +158,14 @@ MEDIA_ROOT = root.path('media')() MEDIA_URL = '/media/' + +# Rest Framework + +REST_FRAMEWORK = { + 'DEFAULT_PAGINATION_CLASS': 'dbdb.api.pagination.StandardPagination', +} + + # Security ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=['*']) diff --git a/dbdb/urls.py b/dbdb/urls.py index 1962aff1..9261b007 100644 --- a/dbdb/urls.py +++ b/dbdb/urls.py @@ -8,6 +8,7 @@ urlpatterns = [ url(r'^', include('django.contrib.auth.urls')), url(r'^', include('dbdb.core.urls')), + url(r'^api/', include('dbdb.api.urls')), url(r'^admin/', admin.site.urls), ] From 86b962ec136d1350eafde91655980eef298cdf4a Mon Sep 17 00:00:00 2001 From: Arthur Pemberton Date: Fri, 17 Apr 2020 02:01:07 -0400 Subject: [PATCH 2/7] renaming api module --- dbdb/api/urls.py | 2 +- dbdb/api/{api_v202004 => v202004}/__init__.py | 0 dbdb/api/{api_v202004 => v202004}/apps.py | 0 .../{api_v202004 => v202004}/serializers.py | 0 dbdb/api/{api_v202004 => v202004}/urls.py | 0 dbdb/api/{api_v202004 => v202004}/views.py | 0 dbdb/settings.py | 26 +++++++------------ 7 files changed, 10 insertions(+), 18 deletions(-) rename dbdb/api/{api_v202004 => v202004}/__init__.py (100%) rename dbdb/api/{api_v202004 => v202004}/apps.py (100%) rename dbdb/api/{api_v202004 => v202004}/serializers.py (100%) rename dbdb/api/{api_v202004 => v202004}/urls.py (100%) rename dbdb/api/{api_v202004 => v202004}/views.py (100%) diff --git a/dbdb/api/urls.py b/dbdb/api/urls.py index a798cb8a..328c1ad2 100644 --- a/dbdb/api/urls.py +++ b/dbdb/api/urls.py @@ -5,5 +5,5 @@ urlpatterns = [ - path('v202004/', include('dbdb.api.api_v202004.urls')), + path('v202004/', include('dbdb.api.v202004.urls')), ] diff --git a/dbdb/api/api_v202004/__init__.py b/dbdb/api/v202004/__init__.py similarity index 100% rename from dbdb/api/api_v202004/__init__.py rename to dbdb/api/v202004/__init__.py diff --git a/dbdb/api/api_v202004/apps.py b/dbdb/api/v202004/apps.py similarity index 100% rename from dbdb/api/api_v202004/apps.py rename to dbdb/api/v202004/apps.py diff --git a/dbdb/api/api_v202004/serializers.py b/dbdb/api/v202004/serializers.py similarity index 100% rename from dbdb/api/api_v202004/serializers.py rename to dbdb/api/v202004/serializers.py diff --git a/dbdb/api/api_v202004/urls.py b/dbdb/api/v202004/urls.py similarity index 100% rename from dbdb/api/api_v202004/urls.py rename to dbdb/api/v202004/urls.py diff --git a/dbdb/api/api_v202004/views.py b/dbdb/api/v202004/views.py similarity index 100% rename from dbdb/api/api_v202004/views.py rename to dbdb/api/v202004/views.py diff --git a/dbdb/settings.py b/dbdb/settings.py index 4c173207..58e18edf 100644 --- a/dbdb/settings.py +++ b/dbdb/settings.py @@ -28,7 +28,7 @@ 'rest_framework', # djangorestframework 'dbdb.core', - 'dbdb.api.api_v202004', + 'dbdb.api.v202004', ] MIDDLEWARE = [ @@ -82,24 +82,18 @@ 'default': env.db( default='sqlite:///{}'.format( root.path('data/db.sqlite3') ) ) } + # Password validation # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, + { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator' }, + { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator' }, + { 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator' }, + { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator' }, ] + # CACHE CACHES = { 'default': { @@ -132,10 +126,6 @@ 'PATH': root.path('data/xapian')(), 'FLAGS': HAYSTACK_XAPIAN_FLAGS, }, - # 'default': { - # 'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine', - # 'PATH': root.path('data/whoosh')(), - # }, } @@ -209,9 +199,11 @@ }, } + # Django Countries COUNTRIES_FIRST = ['US'] + # Django Invisible reCaptcha NORECAPTCHA_SITE_KEY = '6Lfo8VwUAAAAAEHNqeL01PSkiRul7ImQ8Bsw8Nqc' NORECAPTCHA_SECRET_KEY = '6Lfo8VwUAAAAALFGUrGKqrzCR94pfgFahtd56WY9' From 0a4c75221dafc5cf6a37844f312af970176b2cd9 Mon Sep 17 00:00:00 2001 From: Arthur Pemberton Date: Fri, 17 Apr 2020 02:21:02 -0400 Subject: [PATCH 3/7] added throttling to API --- dbdb/settings.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/dbdb/settings.py b/dbdb/settings.py index 58e18edf..1542e6b2 100644 --- a/dbdb/settings.py +++ b/dbdb/settings.py @@ -95,16 +95,14 @@ # CACHE + CACHES = { 'default': { - #'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', - 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', - #'LOCATION': 'dbdb_io_cache', + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', } } - # Haystack # https://django-haystack.readthedocs.io/ @@ -152,7 +150,18 @@ # Rest Framework REST_FRAMEWORK = { + # https://www.django-rest-framework.org/api-guide/pagination/ 'DEFAULT_PAGINATION_CLASS': 'dbdb.api.pagination.StandardPagination', + + # https://www.django-rest-framework.org/api-guide/throttling/ + 'DEFAULT_THROTTLE_CLASSES': [ + 'rest_framework.throttling.AnonRateThrottle', + 'rest_framework.throttling.UserRateThrottle' + ], + 'DEFAULT_THROTTLE_RATES': { + 'anon': '4/second', + 'user': '4/second' + } } From 23637f864441d997d321fb6814556f4104c66c18 Mon Sep 17 00:00:00 2001 From: Arthur Pemberton Date: Thu, 25 Jun 2020 09:37:38 -0400 Subject: [PATCH 4/7] fixing tests and travis --- .travis.yml | 11 +- dbdb/core/tests.py | 69 +- dbdb/core/views.py | 4 - requirements-dev.txt | 3 +- requirements.txt | 58 +- templates/base.html | 2 +- templates/core/create-database.html | 2 +- templates/core/database-browse.html | 2 +- templates/core/database_form_base.html | 2 +- templates/core/databases-edit.html | 2 +- templates/core/edit-database.html | 2 +- templates/core/empty-fields.html | 2 +- templates/core/home-listitem.html | 2 +- templates/core/home.html | 2 +- templates/core/recent.html | 2 +- templates/core/revision_view.html | 2 +- templates/core/stats.html | 2 +- templates/core/system-sys-sidebar.html | 2 +- templates/core/system.html | 2 +- templates/core/system_base.html | 2 +- templates/core/version-card.html | 2 +- xapian_backend.py | 1695 ++++++++++++++++++++++++ 22 files changed, 1781 insertions(+), 91 deletions(-) create mode 100644 xapian_backend.py diff --git a/.travis.yml b/.travis.yml index 62682475..f4c5daa9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,17 +2,14 @@ language: python python: - "3.6" install: - - mkdir -p media/logos media/twitter - - sudo apt-get install python3-sphinx - - pip3 install Sphinx - - ./install_xapian.sh 1.4.9 - - pip install git+https://github.com/notanumber/xapian-haystack.git - mkdir -p data/xapian + - mkdir -p media/logos + - mkdir -p media/twitter + - pip3 install -U pip setuptools wheel + - ./install_xapian.sh 1.4.16 - pip3 install -r requirements.txt - pip3 install -r requirements-test.txt before_script: - python manage.py migrate - script: - python manage.py test - diff --git a/dbdb/core/tests.py b/dbdb/core/tests.py index e3f84bb5..370d64d6 100644 --- a/dbdb/core/tests.py +++ b/dbdb/core/tests.py @@ -1,5 +1,6 @@ +import tempfile # django imports -from django.test import TestCase, Client, override_settings +from django.test import TestCase, override_settings from django.contrib.auth import get_user from django.urls import reverse from django.core import management @@ -15,8 +16,6 @@ from .models import Feature from .views import CounterView -import tempfile -from pprint import pprint # ============================================== # HAYSTACK CONFIG @@ -75,19 +74,19 @@ class SearchTestCase(BaseTestCase): #response = self.client.get(reverse('search')) #self.assertRedirects(response, reverse('home')) #return - + def test_haystack_contents(self): """Make sure we are setting up haystack correctly.""" sqs = SearchQuerySet() num_results = len(sqs) self.assertEquals(num_results, 2) - + expected = ["SQLite", "XXX"] for i in range(num_results): res = sqs[i] self.assertTrue(res.name in expected) return - + def test_search_no_parameters(self): query = {'q': 'sql'} response = self.client.get(reverse('browse')) @@ -97,8 +96,8 @@ def test_search_no_parameters(self): def test_search_valid_parameters(self): query = {'q': 'sql'} response = self.client.get(reverse('browse'), data=query) - #print(response.content) - self.assertContains(response, 'Found 1 database for \"sql\"', html=True) + + self.assertContains(response, 'Found 1 database', html=False) self.assertContains(response, 'SQLite', html=True) # self.assertContains(response, '

Nice description

', html=True) return @@ -106,7 +105,7 @@ def test_search_valid_parameters(self): def test_search_invalid_parameters(self): query = {'q': 'dock'} response = self.client.get(reverse('browse'), data=query) - self.assertContains(response, 'No databases found for \"dock\"', html=True) + self.assertContains(response, 'No databases found', html=False) return pass @@ -127,19 +126,19 @@ def test_autocom_valid_parameters(self): target = "SQLite" for i in range(1, len(target)): query = {'q': target[:i+1]} - #pprint(query) + response = self.client.get(reverse('search_autocomplete'), data=query) - #pprint(response.json()) + self.assertContains(response, 'SQLite', html=False) return - + def test_autocom_invalid_parameters(self): query = {'q': "YYY"} response = self.client.get(reverse('search_autocomplete'), data=query) - #pprint(response.json()) + self.assertEquals(len(response.json()), 0) return - + def test_autocom_no_parameters(self): response = self.client.get(reverse('search_autocomplete')) self.assertEquals(len(response.json()), 0) @@ -161,46 +160,21 @@ class SystemViewTestCase(BaseTestCase): def test_counter(self): target = "SQLite" system = System.objects.get(name=target) - orig_count = system.view_count + orig_visits = SystemVisit.objects.filter(system=system).count() - + data = {"token": CounterView.build_token('system', pk=system.id)} response = self.client.post(reverse('counter'), data) result = response.json(); self.assertTrue("status" in result) self.assertEquals(result["status"], "ok") - - # Make sure count increased by one - system = System.objects.get(name=target) - new_count = system.view_count - self.assertEquals(new_count, orig_count+1) - + # Check that we got added a SystemVisit new_visits = SystemVisit.objects.filter(system=system).count() self.assertEquals(new_visits, orig_visits+1) - - return - - def test_bot_block(self): - target = "SQLite" - system = System.objects.get(name=target) - orig_count = system.view_count - - c = Client(HTTP_USER_AGENT='(KHTML, like Gecko; compatible; Googlebot/2.1)') - - data = {"token": CounterView.build_token('system', pk=system.id)} - response = c.post(reverse('counter'), data) - result = response.json(); - self.assertTrue("status" in result) - self.assertEquals(result["status"], "bot") - - # Make sure count is the same - system = System.objects.get(name=target) - new_count = system.view_count - self.assertEquals(new_count, orig_count) + return - - + pass # ============================================== @@ -227,7 +201,7 @@ def test_inputs_quantity(self): filtergroups = d('div.filter-group') # Add two for the year filtergroups # Add nine for country, OS, project type, PL, inspired, derived, embedded compatiable, licenses - #pprint(filtergroups) + self.assertEquals(quantity + 2 + 9, len(filtergroups)) return @@ -236,7 +210,7 @@ def test_search_with_insuficient_data(self): 'feature1': ['option1'], } response = self.client.get(reverse('browse'), data=data) - #pprint(response.content) + self.assertContains(response, 'No databases found') return @@ -301,7 +275,7 @@ class CreateDatabaseTestCase(BaseTestCase): def test_cant_access_not_authenticated(self): response = self.client.get(reverse('create_database')) - self.assertEquals(response.status_code, 404) + self.assertEquals(response.status_code, 302) return def test_cant_access_not_superuser(self): @@ -344,6 +318,7 @@ def test_can_create_database(self): response = self.client.post(reverse('create_database'), data=data) self.assertRedirects(response, reverse('system', kwargs={'slug': 'testdb'})) return + pass # ============================================== diff --git a/dbdb/core/views.py b/dbdb/core/views.py index 3e6308c5..bbd72412 100644 --- a/dbdb/core/views.py +++ b/dbdb/core/views.py @@ -128,7 +128,6 @@ def get_removal_url(self): query = [] for key,values in self.query.lists(): - print(key, values) for value in values: if key == self.group_slug and value == self.tag_slug: continue @@ -660,8 +659,6 @@ def do_search(self, request): for row in FeatureOption.objects.filter(id__in=filter_option_ids).values_list('feature__slug','feature__label','slug','value') ) - #for st in search_tags: - #print('-', st) return (sqs, search_mapping, search_tags) def handle_old_urls(self, request): @@ -959,7 +956,6 @@ def build_features(self, feature_form): @never_cache def get(self, request, slug=None): - # If there is no slug, then they are trying to create a new database. # Only superusers are allowed to do that. if slug is None: diff --git a/requirements-dev.txt b/requirements-dev.txt index c6234e35..bcb7d9eb 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,2 @@ -django-debug-toolbar==1.8django-debug-toolbar==1.8 +django-debug-toolbar==1.8 django-extensions==1.9.6 -django-extensions==1.9.6 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index bfca0851..e7225421 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,17 +1,45 @@ -Django==2.0.13 -django-autoslug==1.9.3 -django-bootstrap4==0.0.7 -django-countries==5.3.2 +alabaster==0.7.12 +asgiref==3.2.7 +Babel==2.8.0 +beautifulsoup4==4.9.0 +certifi==2020.4.5.1 +chardet==3.0.4 +cssselect==1.1.0 +Django==3.0.5 +django-autoslug==1.9.7 +django-bootstrap4==1.1.1 +django-countries==6.1.2 django-environ==0.4.5 -django-haystack==2.8.1 +django-haystack==3.0b1 django-nocaptcha-recaptcha==0.0.20 -easy-thumbnails==2.5 -lxml==4.2.5 -mysqlclient==1.3.14 -Pillow==5.3.0 -PyJWT==1.7.0 -pytz==2018.7 -six==1.11.0 -Whoosh==2.7.4 -xapian-haystack==2.1.1 -python-dateutil==2.8.0 +djangorestframework==3.11.0 +docutils==0.16 +easy-thumbnails==2.7 +idna==2.9 +imagesize==1.2.0 +Jinja2==2.11.2 +lxml==4.5.0 +MarkupSafe==1.1.1 +mysqlclient==1.4.6 +packaging==20.3 +Pillow==7.1.1 +Pygments==2.6.1 +PyJWT==1.7.1 +pyparsing==2.4.7 +pyquery==1.4.1 +python-dateutil==2.8.1 +pytz==2019.3 +requests==2.23.0 +six==1.14.0 +snowballstemmer==2.0.0 +soupsieve==2.0 +Sphinx==3.0.1 +sphinxcontrib-applehelp==1.0.2 +sphinxcontrib-devhelp==1.0.2 +sphinxcontrib-htmlhelp==1.0.3 +sphinxcontrib-jsmath==1.0.1 +sphinxcontrib-qthelp==1.0.3 +sphinxcontrib-serializinghtml==1.1.4 +sphinxcontrib-websupport==1.2.1 +sqlparse==0.3.1 +urllib3==1.25.9 diff --git a/templates/base.html b/templates/base.html index 789f06b6..82a1f475 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,4 +1,4 @@ -{% load staticfiles %} +{% load static %} diff --git a/templates/core/create-database.html b/templates/core/create-database.html index 88eadc17..a6342818 100644 --- a/templates/core/create-database.html +++ b/templates/core/create-database.html @@ -1,5 +1,5 @@ {% extends 'core/database_form_base.html' %} -{% load staticfiles %} +{% load static %} {% load bootstrap4 %} {% block title %} diff --git a/templates/core/database-browse.html b/templates/core/database-browse.html index 16484328..673e76d3 100644 --- a/templates/core/database-browse.html +++ b/templates/core/database-browse.html @@ -1,6 +1,6 @@ {% extends 'base.html' %} {% load humanize %} -{% load staticfiles %} +{% load static %} {% load countries %} {% load bootstrap4 %} diff --git a/templates/core/database_form_base.html b/templates/core/database_form_base.html index 58b246bc..ff6090c1 100644 --- a/templates/core/database_form_base.html +++ b/templates/core/database_form_base.html @@ -1,5 +1,5 @@ {% extends 'base.html' %} -{% load staticfiles %} +{% load static %} {% load bootstrap4 %} {% block css %} diff --git a/templates/core/databases-edit.html b/templates/core/databases-edit.html index 4c255450..b6b3cac5 100644 --- a/templates/core/databases-edit.html +++ b/templates/core/databases-edit.html @@ -1,6 +1,6 @@ {% extends 'base.html' %} {% load bootstrap4 %} -{% load staticfiles %} +{% load static %} {% block title %} diff --git a/templates/core/edit-database.html b/templates/core/edit-database.html index 9b986e52..3db0235a 100644 --- a/templates/core/edit-database.html +++ b/templates/core/edit-database.html @@ -1,5 +1,5 @@ {% extends 'core/database_form_base.html' %} -{% load staticfiles %} +{% load static %} {% load bootstrap4 %} {% block title %} diff --git a/templates/core/empty-fields.html b/templates/core/empty-fields.html index 7cc6d14d..d7a84e18 100644 --- a/templates/core/empty-fields.html +++ b/templates/core/empty-fields.html @@ -1,5 +1,5 @@ {% extends 'base.html' %} -{% load staticfiles %} +{% load static %} {% load bootstrap4 %} {% block title %} diff --git a/templates/core/home-listitem.html b/templates/core/home-listitem.html index f6cea3f9..e27bee78 100644 --- a/templates/core/home-listitem.html +++ b/templates/core/home-listitem.html @@ -1,4 +1,4 @@ -{% load staticfiles %} +{% load static %} {% load thumbnail %} diff --git a/templates/core/home.html b/templates/core/home.html index 2e320b1a..9725660c 100644 --- a/templates/core/home.html +++ b/templates/core/home.html @@ -1,6 +1,6 @@ {% extends 'base.html' %} {% load humanize %} -{% load staticfiles %} +{% load static %} {% load thumbnail %} {% block title %}Home{% endblock %} diff --git a/templates/core/recent.html b/templates/core/recent.html index 884948b1..c0551a59 100644 --- a/templates/core/recent.html +++ b/templates/core/recent.html @@ -1,5 +1,5 @@ {% extends 'base.html' %} -{% load staticfiles %} +{% load static %} {% load thumbnail %} {% load bootstrap4 %} diff --git a/templates/core/revision_view.html b/templates/core/revision_view.html index ae61742d..0acd2298 100644 --- a/templates/core/revision_view.html +++ b/templates/core/revision_view.html @@ -1,5 +1,5 @@ {% extends 'core/system_base.html' %} -{% load staticfiles %} +{% load static %} {% block navbuttons %}