diff options
author | 2019-04-20 14:39:16 +0100 | |
---|---|---|
committer | 2019-04-20 14:39:16 +0100 | |
commit | 43039cd13afd19ea59addc3bb92c3c600826d8a3 (patch) | |
tree | 1aaaf8b862a7b55836c6a64a16d2fc4cb4100e60 /pydis_site/apps | |
parent | Address reviews (diff) | |
parent | Merge pull request #213 from python-discord/django_front_page (diff) |
Merge branch 'django' into django+200/wiki
# Conflicts:
# Pipfile
# Pipfile.lock
# pydis_site/apps/home/tests/test_app_basics.py
# pydis_site/apps/home/urls.py
# pydis_site/hosts.py
# pydis_site/settings.py
Diffstat (limited to 'pydis_site/apps')
-rw-r--r-- | pydis_site/apps/home/admin.py | 3 | ||||
-rw-r--r-- | pydis_site/apps/home/migrations/0001_initial.py | 26 | ||||
-rw-r--r-- | pydis_site/apps/home/models.py | 3 | ||||
-rw-r--r-- | pydis_site/apps/home/models/__init__.py | 3 | ||||
-rw-r--r-- | pydis_site/apps/home/models/repository_metadata.py | 33 | ||||
-rw-r--r-- | pydis_site/apps/home/templatetags/__init__.py | 3 | ||||
-rw-r--r-- | pydis_site/apps/home/tests/mock_github_api_response.json | 44 | ||||
-rw-r--r-- | pydis_site/apps/home/tests/test_app_basics.py | 16 | ||||
-rw-r--r-- | pydis_site/apps/home/tests/test_repodata_helpers.py | 105 | ||||
-rw-r--r-- | pydis_site/apps/home/tests/test_templatetags.py | 8 | ||||
-rw-r--r-- | pydis_site/apps/home/tests/test_views.py | 9 | ||||
-rw-r--r-- | pydis_site/apps/home/urls.py | 4 | ||||
-rw-r--r-- | pydis_site/apps/home/views.py | 3 | ||||
-rw-r--r-- | pydis_site/apps/home/views/__init__.py | 3 | ||||
-rw-r--r-- | pydis_site/apps/home/views/home.py | 115 |
15 files changed, 351 insertions, 27 deletions
diff --git a/pydis_site/apps/home/admin.py b/pydis_site/apps/home/admin.py deleted file mode 100644 index 4185d360..00000000 --- a/pydis_site/apps/home/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -# from django.contrib import admin - -# Register your models here. diff --git a/pydis_site/apps/home/migrations/0001_initial.py b/pydis_site/apps/home/migrations/0001_initial.py new file mode 100644 index 00000000..a2bf9f3e --- /dev/null +++ b/pydis_site/apps/home/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# Generated by Django 2.2 on 2019-04-16 15:27 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='RepositoryMetadata', + fields=[ + ('last_updated', models.DateTimeField(default=django.utils.timezone.now, help_text='The date and time this data was last fetched.')), + ('repo_name', models.CharField(help_text='The full name of the repo, e.g. python-discord/site', max_length=40, primary_key=True, serialize=False)), + ('description', models.CharField(help_text='The description of the repo.', max_length=400)), + ('forks', models.IntegerField(help_text='The number of forks of this repo')), + ('stargazers', models.IntegerField(help_text='The number of stargazers for this repo')), + ('language', models.CharField(help_text='The primary programming language used for this repo.', max_length=20)), + ], + ), + ] diff --git a/pydis_site/apps/home/models.py b/pydis_site/apps/home/models.py deleted file mode 100644 index 0b4331b3..00000000 --- a/pydis_site/apps/home/models.py +++ /dev/null @@ -1,3 +0,0 @@ -# from django.db import models - -# Create your models here. diff --git a/pydis_site/apps/home/models/__init__.py b/pydis_site/apps/home/models/__init__.py new file mode 100644 index 00000000..6c68df9c --- /dev/null +++ b/pydis_site/apps/home/models/__init__.py @@ -0,0 +1,3 @@ +from .repository_metadata import RepositoryMetadata + +__all__ = ["RepositoryMetadata"] diff --git a/pydis_site/apps/home/models/repository_metadata.py b/pydis_site/apps/home/models/repository_metadata.py new file mode 100644 index 00000000..c975c904 --- /dev/null +++ b/pydis_site/apps/home/models/repository_metadata.py @@ -0,0 +1,33 @@ +from django.db import models +from django.utils import timezone + + +class RepositoryMetadata(models.Model): + """Information about one of our repos fetched from the GitHub API.""" + + last_updated = models.DateTimeField( + default=timezone.now, + help_text="The date and time this data was last fetched." + ) + repo_name = models.CharField( + primary_key=True, + max_length=40, + help_text="The full name of the repo, e.g. python-discord/site" + ) + description = models.CharField( + max_length=400, + help_text="The description of the repo." + ) + forks = models.IntegerField( + help_text="The number of forks of this repo" + ) + stargazers = models.IntegerField( + help_text="The number of stargazers for this repo" + ) + language = models.CharField( + max_length=20, + help_text="The primary programming language used for this repo." + ) + + def __str__(self): + return self.repo_name diff --git a/pydis_site/apps/home/templatetags/__init__.py b/pydis_site/apps/home/templatetags/__init__.py index e69de29b..70aca169 100644 --- a/pydis_site/apps/home/templatetags/__init__.py +++ b/pydis_site/apps/home/templatetags/__init__.py @@ -0,0 +1,3 @@ +from .extra_filters import starts_with + +__all__ = ["starts_with"] diff --git a/pydis_site/apps/home/tests/mock_github_api_response.json b/pydis_site/apps/home/tests/mock_github_api_response.json new file mode 100644 index 00000000..37dc672e --- /dev/null +++ b/pydis_site/apps/home/tests/mock_github_api_response.json @@ -0,0 +1,44 @@ +[ + { + "full_name": "python-discord/bot", + "description": "test", + "stargazers_count": 97, + "language": "Python", + "forks_count": 31 + }, + { + "full_name": "python-discord/site", + "description": "test", + "stargazers_count": 97, + "language": "Python", + "forks_count": 31 + }, + { + "full_name": "python-discord/snekbox", + "description": "test", + "stargazers_count": 97, + "language": "Python", + "forks_count": 31 + }, + { + "full_name": "python-discord/django-simple-bulma", + "description": "test", + "stargazers_count": 97, + "language": "Python", + "forks_count": 31 + }, + { + "full_name": "python-discord/django-crispy-bulma", + "description": "test", + "stargazers_count": 97, + "language": "Python", + "forks_count": 31 + }, + { + "full_name": "python-discord/seasonalbot", + "description": "test", + "stargazers_count": 97, + "language": "Python", + "forks_count": 31 + } +] diff --git a/pydis_site/apps/home/tests/test_app_basics.py b/pydis_site/apps/home/tests/test_app_basics.py deleted file mode 100644 index 733ddaa3..00000000 --- a/pydis_site/apps/home/tests/test_app_basics.py +++ /dev/null @@ -1,16 +0,0 @@ -from django.test import TestCase -from django_hosts.resolvers import reverse - -from pydis_site.apps.home.templatetags import extra_filters - - -class TestIndexReturns200(TestCase): - def test_index_returns_200(self): - url = reverse('home.index') - resp = self.client.get(url) - self.assertEqual(resp.status_code, 200) - - -class TestExtraFilterTemplateTags(TestCase): - def test_starts_with(self): - self.assertTrue(extra_filters.starts_with('foo', 'f')) diff --git a/pydis_site/apps/home/tests/test_repodata_helpers.py b/pydis_site/apps/home/tests/test_repodata_helpers.py new file mode 100644 index 00000000..59cb2331 --- /dev/null +++ b/pydis_site/apps/home/tests/test_repodata_helpers.py @@ -0,0 +1,105 @@ +import json +from datetime import timedelta +from pathlib import Path +from unittest import mock + +from django.test import TestCase +from django.utils import timezone + +from pydis_site.apps.home.models import RepositoryMetadata +from pydis_site.apps.home.views import HomeView + + +def mocked_requests_get(*args, **kwargs) -> "MockResponse": # noqa + """A mock version of requests.get, so we don't need to call the API every time we run a test""" + class MockResponse: + def __init__(self, json_data, status_code): + self.json_data = json_data + self.status_code = status_code + + def json(self): + return self.json_data + + if args[0] == HomeView.github_api: + json_path = Path(__file__).resolve().parent / "mock_github_api_response.json" + with open(json_path, 'r') as json_file: + mock_data = json.load(json_file) + + return MockResponse(mock_data, 200) + + return MockResponse(None, 404) + + +class TestRepositoryMetadataHelpers(TestCase): + + def setUp(self): + """Executed before each test method.""" + + self.home_view = HomeView() + + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_returns_metadata(self, _: mock.MagicMock): + """Test if the _get_repo_data helper actually returns what it should.""" + + metadata = self.home_view._get_repo_data() + + self.assertIsInstance(metadata[0], RepositoryMetadata) + self.assertEquals(len(metadata), len(self.home_view.repos)) + + def test_returns_cached_metadata(self): + """Test if the _get_repo_data helper returns cached data when available.""" + + repo_data = RepositoryMetadata( + repo_name="python-discord/site", + description="testrepo", + forks=42, + stargazers=42, + language="English", + ) + repo_data.save() + metadata = self.home_view._get_repo_data() + + self.assertIsInstance(metadata[0], RepositoryMetadata) + self.assertIsInstance(str(metadata[0]), str) + + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_refresh_stale_metadata(self, _: mock.MagicMock): + """Test if the _get_repo_data helper will refresh when the data is stale""" + + repo_data = RepositoryMetadata( + repo_name="python-discord/site", + description="testrepo", + forks=42, + stargazers=42, + language="English", + last_updated=timezone.now() - timedelta(seconds=HomeView.repository_cache_ttl + 1), + ) + repo_data.save() + metadata = self.home_view._get_repo_data() + + self.assertIsInstance(metadata[0], RepositoryMetadata) + + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_returns_api_data(self, _: mock.MagicMock): + """Tests if the _get_api_data helper returns what it should.""" + + api_data = self.home_view._get_api_data() + repo = self.home_view.repos[0] + + self.assertIsInstance(api_data, dict) + self.assertEquals(len(api_data), len(self.home_view.repos)) + self.assertIn(repo, api_data.keys()) + self.assertIn("stargazers_count", api_data[repo]) + + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_mocked_requests_get(self, mock_get: mock.MagicMock): + """Tests if our mocked_requests_get is returning what it should.""" + + success_data = mock_get(HomeView.github_api) + fail_data = mock_get("failtest") + + self.assertEqual(success_data.status_code, 200) + self.assertEqual(fail_data.status_code, 404) + + self.assertIsNotNone(success_data.json_data) + self.assertIsNone(fail_data.json_data) diff --git a/pydis_site/apps/home/tests/test_templatetags.py b/pydis_site/apps/home/tests/test_templatetags.py new file mode 100644 index 00000000..813588c8 --- /dev/null +++ b/pydis_site/apps/home/tests/test_templatetags.py @@ -0,0 +1,8 @@ +from django.test import TestCase + +from pydis_site.apps.home.templatetags import starts_with + + +class TestTemplateTags(TestCase): + def test_starts_with(self): + self.assertTrue(starts_with('foo', 'f')) diff --git a/pydis_site/apps/home/tests/test_views.py b/pydis_site/apps/home/tests/test_views.py new file mode 100644 index 00000000..73678b0a --- /dev/null +++ b/pydis_site/apps/home/tests/test_views.py @@ -0,0 +1,9 @@ +from django.test import TestCase +from django_hosts.resolvers import reverse + + +class TestIndexReturns200(TestCase): + def test_index_returns_200(self): + url = reverse('home') + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) diff --git a/pydis_site/apps/home/urls.py b/pydis_site/apps/home/urls.py index 6d144aaa..b22508d9 100644 --- a/pydis_site/apps/home/urls.py +++ b/pydis_site/apps/home/urls.py @@ -2,13 +2,13 @@ from django.conf import settings from django.conf.urls.static import static from django.contrib import admin from django.urls import include, path -from django.views.generic import TemplateView +from .views import HomeView app_name = 'home' urlpatterns = [ + path('', HomeView.as_view(), name='home'), path('admin/', admin.site.urls), path('notifications/', include('django_nyt.urls')), path('wiki/', include('wiki.urls')), - path('', TemplateView.as_view(template_name='home/index.html'), name='home.index'), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/pydis_site/apps/home/views.py b/pydis_site/apps/home/views.py deleted file mode 100644 index fd0e0449..00000000 --- a/pydis_site/apps/home/views.py +++ /dev/null @@ -1,3 +0,0 @@ -# from django.shortcuts import render - -# Create your views here. diff --git a/pydis_site/apps/home/views/__init__.py b/pydis_site/apps/home/views/__init__.py new file mode 100644 index 00000000..971d73a3 --- /dev/null +++ b/pydis_site/apps/home/views/__init__.py @@ -0,0 +1,3 @@ +from .home import HomeView + +__all__ = ["HomeView"] diff --git a/pydis_site/apps/home/views/home.py b/pydis_site/apps/home/views/home.py new file mode 100644 index 00000000..e4daf380 --- /dev/null +++ b/pydis_site/apps/home/views/home.py @@ -0,0 +1,115 @@ +from typing import Dict, List + +import requests +from django.core.handlers.wsgi import WSGIRequest +from django.http import HttpResponse +from django.shortcuts import render +from django.utils import timezone +from django.views import View + +from pydis_site.apps.home.models import RepositoryMetadata + + +class HomeView(View): + """The main landing page for the website.""" + + github_api = "https://api.github.com/users/python-discord/repos" + repository_cache_ttl = 600 + + # Which of our GitHub repos should be displayed on the front page, and in which order? + repos = [ + "python-discord/site", + "python-discord/bot", + "python-discord/snekbox", + "python-discord/seasonalbot", + "python-discord/django-simple-bulma", + "python-discord/django-crispy-bulma", + ] + + def _get_api_data(self) -> Dict[str, Dict[str, str]]: + """Call the GitHub API and get information about our repos.""" + + repo_dict = {repo_name: {} for repo_name in self.repos} + + # Fetch the data from the GitHub API + api_data = requests.get(self.github_api) + api_data = api_data.json() + + # Process the API data into our dict + for repo in api_data: + full_name = repo["full_name"] + + if full_name in self.repos: + repo_dict[full_name] = { + "full_name": repo["full_name"], + "description": repo["description"], + "language": repo["language"], + "forks_count": repo["forks_count"], + "stargazers_count": repo["stargazers_count"], + } + return repo_dict + + def _get_repo_data(self) -> List[RepositoryMetadata]: + """Build a list of RepositoryMetadata objects that we can use to populate the front page.""" + + # Try to get site data from the cache + try: + repo_data = RepositoryMetadata.objects.get(repo_name="python-discord/site") + + # If the data is stale, we should refresh it. + if (timezone.now() - repo_data.last_updated).seconds > self.repository_cache_ttl: + + # Get new data from API + api_repositories = self._get_api_data() + database_repositories = [] + + # Update or create all RepoData objects in self.repos + for repo_name, api_data in api_repositories.items(): + try: + repo_data = RepositoryMetadata.objects.get(repo_name=repo_name) + repo_data.description = api_data["description"] + repo_data.language = api_data["language"] + repo_data.forks = api_data["forks_count"] + repo_data.stargazers = api_data["stargazers_count"] + except RepositoryMetadata.DoesNotExist: + repo_data = RepositoryMetadata( + repo_name=api_data["full_name"], + description=api_data["description"], + forks=api_data["forks_count"], + stargazers=api_data["stargazers_count"], + language=api_data["language"], + ) + repo_data.save() + database_repositories.append(repo_data) + return database_repositories + + # Otherwise, if the data is fresher than 2 minutes old, we should just return it. + else: + return RepositoryMetadata.objects.all() + + # If this is raised, the database has no repodata at all, we will create them all. + except RepositoryMetadata.DoesNotExist: + + # Get new data from API + api_repositories = self._get_api_data() + database_repositories = [] + + # Create all the repodata records in the database. + for api_data in api_repositories.values(): + repo_data = RepositoryMetadata( + repo_name=api_data["full_name"], + description=api_data["description"], + forks=api_data["forks_count"], + stargazers=api_data["stargazers_count"], + language=api_data["language"], + ) + repo_data.save() + database_repositories.append(repo_data) + + return database_repositories + + def get(self, request: WSGIRequest) -> HttpResponse: + """Collect repo data and render the homepage view""" + + repo_data = self._get_repo_data() + return render(request, "home/index.html", {"repo_data": repo_data}) |