diff options
| author | 2019-04-20 14:41:40 +0200 | |
|---|---|---|
| committer | 2019-04-20 14:41:40 +0200 | |
| commit | 14f8ca26b85f7cc3e009fb9ba2ac549e751c8068 (patch) | |
| tree | 041b093b2c9296fe23eb6e1bb54d0748fbe58dfa /pydis_site/apps | |
| parent | Revert linter to non-verbose. (diff) | |
| parent | Make the flakes happy. (diff) | |
Merge pull request #213 from python-discord/django_front_page
Django front page
Diffstat (limited to 'pydis_site/apps')
22 files changed, 351 insertions, 44 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.py b/pydis_site/apps/home/tests.py deleted file mode 100644 index 733ddaa3..00000000 --- a/pydis_site/apps/home/tests.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/wiki/__init__.py b/pydis_site/apps/home/tests/__init__.py index e69de29b..e69de29b 100644 --- a/pydis_site/apps/wiki/__init__.py +++ 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..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 56525af8..d8dba2f6 100644 --- a/pydis_site/apps/home/urls.py +++ b/pydis_site/apps/home/urls.py @@ -1,10 +1,10 @@  from django.contrib import admin  from django.urls import path -from django.views.generic import TemplateView +from .views import HomeView  app_name = 'home'  urlpatterns = [ -    path('', TemplateView.as_view(template_name='home/index.html'), name='home.index'), +    path('', HomeView.as_view(), name='home'),      path('admin/', admin.site.urls)  ] 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}) diff --git a/pydis_site/apps/wiki/admin.py b/pydis_site/apps/wiki/admin.py deleted file mode 100644 index 4185d360..00000000 --- a/pydis_site/apps/wiki/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -# from django.contrib import admin - -# Register your models here. diff --git a/pydis_site/apps/wiki/apps.py b/pydis_site/apps/wiki/apps.py deleted file mode 100644 index fce4708e..00000000 --- a/pydis_site/apps/wiki/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class WikiConfig(AppConfig): -    name = 'wiki' diff --git a/pydis_site/apps/wiki/migrations/__init__.py b/pydis_site/apps/wiki/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pydis_site/apps/wiki/migrations/__init__.py +++ /dev/null diff --git a/pydis_site/apps/wiki/models.py b/pydis_site/apps/wiki/models.py deleted file mode 100644 index 0b4331b3..00000000 --- a/pydis_site/apps/wiki/models.py +++ /dev/null @@ -1,3 +0,0 @@ -# from django.db import models - -# Create your models here. diff --git a/pydis_site/apps/wiki/tests.py b/pydis_site/apps/wiki/tests.py deleted file mode 100644 index a79ca8be..00000000 --- a/pydis_site/apps/wiki/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -# from django.test import TestCase - -# Create your tests here. diff --git a/pydis_site/apps/wiki/views.py b/pydis_site/apps/wiki/views.py deleted file mode 100644 index fd0e0449..00000000 --- a/pydis_site/apps/wiki/views.py +++ /dev/null @@ -1,3 +0,0 @@ -# from django.shortcuts import render - -# Create your views here.  |