diff options
author | 2019-04-19 15:29:27 +0200 | |
---|---|---|
committer | 2019-04-19 15:29:27 +0200 | |
commit | 5d9d6eec3159a9c87ca45dc5b294534daf8495fa (patch) | |
tree | 281274633fe8a9cc5c79d5f66409cf1fd87702dc /pydis_site/apps/home | |
parent | Merge branch 'django_front_page' of github.com:python-discord/site into djang... (diff) |
Addressing all comments in volcyy's second review. The tests now mock the API calls so we don't have to actually call the API every time we run tests.
Diffstat (limited to 'pydis_site/apps/home')
-rw-r--r-- | pydis_site/apps/home/__init__.py | 0 | ||||
-rw-r--r-- | pydis_site/apps/home/apps.py | 5 | ||||
-rw-r--r-- | pydis_site/apps/home/migrations/0001_initial.py | 26 | ||||
-rw-r--r-- | pydis_site/apps/home/migrations/__init__.py | 0 | ||||
-rw-r--r-- | pydis_site/apps/home/models/__init__.py | 3 | ||||
-rw-r--r-- | pydis_site/apps/home/models/repo_data.py | 33 | ||||
-rw-r--r-- | pydis_site/apps/home/templatetags/__init__.py | 3 | ||||
-rw-r--r-- | pydis_site/apps/home/templatetags/extra_filters.py | 8 | ||||
-rw-r--r-- | pydis_site/apps/home/tests/__init__.py | 0 | ||||
-rw-r--r-- | pydis_site/apps/home/tests/mock_github_api_response.json | 44 | ||||
-rw-r--r-- | pydis_site/apps/home/tests/test_repodata_helpers.py | 92 | ||||
-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 | 10 | ||||
-rw-r--r-- | pydis_site/apps/home/views/__init__.py | 3 | ||||
-rw-r--r-- | pydis_site/apps/home/views/home.py | 110 |
16 files changed, 354 insertions, 0 deletions
diff --git a/pydis_site/apps/home/__init__.py b/pydis_site/apps/home/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/pydis_site/apps/home/__init__.py diff --git a/pydis_site/apps/home/apps.py b/pydis_site/apps/home/apps.py new file mode 100644 index 00000000..90dc7137 --- /dev/null +++ b/pydis_site/apps/home/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class HomeConfig(AppConfig): + name = 'home' 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/migrations/__init__.py b/pydis_site/apps/home/migrations/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/pydis_site/apps/home/migrations/__init__.py diff --git a/pydis_site/apps/home/models/__init__.py b/pydis_site/apps/home/models/__init__.py new file mode 100644 index 00000000..f327795a --- /dev/null +++ b/pydis_site/apps/home/models/__init__.py @@ -0,0 +1,3 @@ +from .repo_data import RepositoryMetadata + +__all__ = ["RepositoryMetadata"] diff --git a/pydis_site/apps/home/models/repo_data.py b/pydis_site/apps/home/models/repo_data.py new file mode 100644 index 00000000..c975c904 --- /dev/null +++ b/pydis_site/apps/home/models/repo_data.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 new file mode 100644 index 00000000..70aca169 --- /dev/null +++ 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/templatetags/extra_filters.py b/pydis_site/apps/home/templatetags/extra_filters.py new file mode 100644 index 00000000..edffe9ac --- /dev/null +++ b/pydis_site/apps/home/templatetags/extra_filters.py @@ -0,0 +1,8 @@ +from django import template + +register = template.Library() + + +def starts_with(value: str, arg: str): + return value.startswith(arg) diff --git a/pydis_site/apps/home/tests/__init__.py b/pydis_site/apps/home/tests/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/pydis_site/apps/home/tests/__init__.py 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_repodata_helpers.py b/pydis_site/apps/home/tests/test_repodata_helpers.py new file mode 100644 index 00000000..8ab2a467 --- /dev/null +++ b/pydis_site/apps/home/tests/test_repodata_helpers.py @@ -0,0 +1,92 @@ +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): + """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, _): + """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, _): + """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=121), # Make the data 2 minutes old. + ) + 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, _): + """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]) 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 new file mode 100644 index 00000000..d8dba2f6 --- /dev/null +++ b/pydis_site/apps/home/urls.py @@ -0,0 +1,10 @@ +from django.contrib import admin +from django.urls import path + +from .views import HomeView + +app_name = 'home' +urlpatterns = [ + path('', HomeView.as_view(), name='home'), + path('admin/', admin.site.urls) +] 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..c91d13e2 --- /dev/null +++ b/pydis_site/apps/home/views/home.py @@ -0,0 +1,110 @@ +import requests +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 view""" + + github_api = "https://api.github.com/users/python-discord/repos" + + # 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): + """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): + """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 older than 2 minutes, we should refresh it. + if (timezone.now() - repo_data.last_updated).seconds > 120: + + # 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): + """Collect repo data and render the homepage view""" + + repo_data = self._get_repo_data() + return render(request, "home/index.html", {"repo_data": repo_data}) |