From 621280892b96875beb5053cbd07a837f1833310a Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Wed, 29 Mar 2023 00:58:27 +0200 Subject: Add a readme for the home app This commit also moves the nested structures for models and views in the home app into a single module, as they were not split up as part of the subpackage, with the goal of making this a bit more overseeable. Part of #674. --- .coveragerc | 2 +- pydis_site/apps/home/README.md | 35 +++++ pydis_site/apps/home/models.py | 33 +++++ pydis_site/apps/home/models/__init__.py | 3 - pydis_site/apps/home/models/repository_metadata.py | 33 ----- pydis_site/apps/home/views.py | 163 +++++++++++++++++++++ pydis_site/apps/home/views/__init__.py | 3 - pydis_site/apps/home/views/home.py | 163 --------------------- 8 files changed, 232 insertions(+), 203 deletions(-) create mode 100644 pydis_site/apps/home/README.md create mode 100644 pydis_site/apps/home/models.py delete mode 100644 pydis_site/apps/home/models/__init__.py delete mode 100644 pydis_site/apps/home/models/repository_metadata.py create mode 100644 pydis_site/apps/home/views.py delete mode 100644 pydis_site/apps/home/views/__init__.py delete mode 100644 pydis_site/apps/home/views/home.py diff --git a/.coveragerc b/.coveragerc index 039654db..38926b22 100644 --- a/.coveragerc +++ b/.coveragerc @@ -14,7 +14,7 @@ omit = pydis_site/wsgi.py pydis_site/settings.py pydis_site/utils/resources.py - pydis_site/apps/home/views/home.py + pydis_site/apps/home/views.py [report] fail_under = 100 diff --git a/pydis_site/apps/home/README.md b/pydis_site/apps/home/README.md new file mode 100644 index 00000000..34c1e367 --- /dev/null +++ b/pydis_site/apps/home/README.md @@ -0,0 +1,35 @@ +# The "home" app + +This Django application takes care of serving the homepage of our website, that +is, the first page that you see when you open pythondiscord.com. It also +manages the timeline page showcasing the history of our community. + +## Directory structure + +- `migrations` is the standard Django migrations folder. As with [the API + app](../api/README.md), you usually won't need to edit this manually, use + `python manage.py makemigrations [-n short_description]` to create a new + migration here. + +- `templatetags` contains custom [template tags and + filters](https://docs.djangoproject.com/en/dev/howto/custom-template-tags/) + used in the home app. + +- `tests` contains unit tests that validate the home app works as expected. If + you're looking for guidance in writing tests, the [Django tutorial + introducing automated + testing](https://docs.djangoproject.com/en/dev/intro/tutorial05/) is a great + starting point. + +As for the Python modules residing directly in here: + +- `models.py` contains our Django model definitions for this app. As this app + is rather minimal, this is kept as a single module - more models would be + split up into a subfolder as in the other apps. + +- `urls.py` configures Django's [URL + dispatcher](https://docs.djangoproject.com/en/dev/topics/http/urls/) for our + home endpoints. + +- `views.py` contains our Django views. You can see where they are linked in the + URL dispatcher. diff --git a/pydis_site/apps/home/models.py b/pydis_site/apps/home/models.py new file mode 100644 index 00000000..00a83cd7 --- /dev/null +++ b/pydis_site/apps/home/models.py @@ -0,0 +1,33 @@ +from django.db import models + + +class RepositoryMetadata(models.Model): + """Information about one of our repos fetched from the GitHub API.""" + + last_updated = models.DateTimeField( + help_text="The date and time this data was last fetched.", + auto_now=True, + ) + 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): + """Returns the repo name, for display purposes.""" + return self.repo_name diff --git a/pydis_site/apps/home/models/__init__.py b/pydis_site/apps/home/models/__init__.py deleted file mode 100644 index 6c68df9c..00000000 --- a/pydis_site/apps/home/models/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index 00a83cd7..00000000 --- a/pydis_site/apps/home/models/repository_metadata.py +++ /dev/null @@ -1,33 +0,0 @@ -from django.db import models - - -class RepositoryMetadata(models.Model): - """Information about one of our repos fetched from the GitHub API.""" - - last_updated = models.DateTimeField( - help_text="The date and time this data was last fetched.", - auto_now=True, - ) - 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): - """Returns the repo name, for display purposes.""" - return self.repo_name diff --git a/pydis_site/apps/home/views.py b/pydis_site/apps/home/views.py new file mode 100644 index 00000000..8a165682 --- /dev/null +++ b/pydis_site/apps/home/views.py @@ -0,0 +1,163 @@ +import logging +from typing import Dict, List + +import httpx +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 import settings +from pydis_site.apps.home.models import RepositoryMetadata + +log = logging.getLogger(__name__) + + +class HomeView(View): + """The main landing page for the website.""" + + github_api = "https://api.github.com/users/python-discord/repos?per_page=100" + repository_cache_ttl = 3600 + + # 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/sir-lancebot", + "python-discord/metricity", + "python-discord/king-arthur", + ] + + def __init__(self): + """Clean up stale RepositoryMetadata.""" + if not settings.STATIC_BUILD: + RepositoryMetadata.objects.exclude(repo_name__in=self.repos).delete() + + # If no token is defined (for example in local development), then + # it does not make sense to pass the Authorization header. More + # specifically, GitHub will reject any requests from us due to the + # invalid header. We can make a limited number of anonymous requests + # though, which is useful for testing. + if settings.GITHUB_TOKEN: + self.headers = {"Authorization": f"token {settings.GITHUB_TOKEN}"} + else: + self.headers = {} + + def _get_api_data(self) -> Dict[str, Dict[str, str]]: + """ + Call the GitHub API and get information about our repos. + + If we're unable to get that info for any reason, return an empty dict. + """ + repo_dict = {} + try: + # Fetch the data from the GitHub API + api_data: List[dict] = httpx.get( + self.github_api, + headers=self.headers, + timeout=settings.TIMEOUT_PERIOD + ).json() + except httpx.TimeoutException: + log.error("Request to fetch GitHub repository metadata for timed out!") + return repo_dict + + # Process the API data into our dict + for repo in api_data: + try: + 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"], + } + # Something is not right about the API data we got back from GitHub. + except (TypeError, ConnectionError, KeyError) as e: + log.error( + "Unable to parse the GitHub repository metadata from response!", + extra={ + 'api_data': api_data, + 'error': e + } + ) + continue + + 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.""" + # First off, load the timestamp of the least recently updated entry. + if settings.STATIC_BUILD: + last_update = None + else: + last_update = ( + RepositoryMetadata.objects.values_list("last_updated", flat=True) + .order_by("last_updated").first() + ) + + # If we did not retrieve any results here, we should import them! + if last_update is None: + + # Try to get new data from the API. If it fails, we'll return an empty list. + # In this case, we simply don't display our projects on the site. + api_repositories = self._get_api_data() + + # Create all the repodata records in the database. + 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"], + ) + for api_data in api_repositories.values() + ] + + if settings.STATIC_BUILD: + return data + else: + return RepositoryMetadata.objects.bulk_create(data) + + # If the data is stale, we should refresh it. + if (timezone.now() - last_update).seconds > self.repository_cache_ttl: + # Try to get new data from the API. If it fails, return the cached data. + api_repositories = self._get_api_data() + + if not api_repositories: + return RepositoryMetadata.objects.all() + + # Update or create all RepoData objects in self.repos + database_repositories = [] + for api_data in api_repositories.values(): + repo_data, _created = RepositoryMetadata.objects.update_or_create( + repo_name=api_data["full_name"], + defaults={ + 'repo_name': api_data["full_name"], + 'description': api_data["description"], + 'forks': api_data["forks_count"], + 'stargazers': api_data["stargazers_count"], + 'language': api_data["language"], + } + ) + 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() + + 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}) + + +def timeline(request: WSGIRequest) -> HttpResponse: + """Render timeline view.""" + return render(request, 'home/timeline.html') diff --git a/pydis_site/apps/home/views/__init__.py b/pydis_site/apps/home/views/__init__.py deleted file mode 100644 index 28cc4d65..00000000 --- a/pydis_site/apps/home/views/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .home import HomeView, timeline - -__all__ = ["HomeView", "timeline"] diff --git a/pydis_site/apps/home/views/home.py b/pydis_site/apps/home/views/home.py deleted file mode 100644 index 8a165682..00000000 --- a/pydis_site/apps/home/views/home.py +++ /dev/null @@ -1,163 +0,0 @@ -import logging -from typing import Dict, List - -import httpx -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 import settings -from pydis_site.apps.home.models import RepositoryMetadata - -log = logging.getLogger(__name__) - - -class HomeView(View): - """The main landing page for the website.""" - - github_api = "https://api.github.com/users/python-discord/repos?per_page=100" - repository_cache_ttl = 3600 - - # 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/sir-lancebot", - "python-discord/metricity", - "python-discord/king-arthur", - ] - - def __init__(self): - """Clean up stale RepositoryMetadata.""" - if not settings.STATIC_BUILD: - RepositoryMetadata.objects.exclude(repo_name__in=self.repos).delete() - - # If no token is defined (for example in local development), then - # it does not make sense to pass the Authorization header. More - # specifically, GitHub will reject any requests from us due to the - # invalid header. We can make a limited number of anonymous requests - # though, which is useful for testing. - if settings.GITHUB_TOKEN: - self.headers = {"Authorization": f"token {settings.GITHUB_TOKEN}"} - else: - self.headers = {} - - def _get_api_data(self) -> Dict[str, Dict[str, str]]: - """ - Call the GitHub API and get information about our repos. - - If we're unable to get that info for any reason, return an empty dict. - """ - repo_dict = {} - try: - # Fetch the data from the GitHub API - api_data: List[dict] = httpx.get( - self.github_api, - headers=self.headers, - timeout=settings.TIMEOUT_PERIOD - ).json() - except httpx.TimeoutException: - log.error("Request to fetch GitHub repository metadata for timed out!") - return repo_dict - - # Process the API data into our dict - for repo in api_data: - try: - 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"], - } - # Something is not right about the API data we got back from GitHub. - except (TypeError, ConnectionError, KeyError) as e: - log.error( - "Unable to parse the GitHub repository metadata from response!", - extra={ - 'api_data': api_data, - 'error': e - } - ) - continue - - 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.""" - # First off, load the timestamp of the least recently updated entry. - if settings.STATIC_BUILD: - last_update = None - else: - last_update = ( - RepositoryMetadata.objects.values_list("last_updated", flat=True) - .order_by("last_updated").first() - ) - - # If we did not retrieve any results here, we should import them! - if last_update is None: - - # Try to get new data from the API. If it fails, we'll return an empty list. - # In this case, we simply don't display our projects on the site. - api_repositories = self._get_api_data() - - # Create all the repodata records in the database. - 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"], - ) - for api_data in api_repositories.values() - ] - - if settings.STATIC_BUILD: - return data - else: - return RepositoryMetadata.objects.bulk_create(data) - - # If the data is stale, we should refresh it. - if (timezone.now() - last_update).seconds > self.repository_cache_ttl: - # Try to get new data from the API. If it fails, return the cached data. - api_repositories = self._get_api_data() - - if not api_repositories: - return RepositoryMetadata.objects.all() - - # Update or create all RepoData objects in self.repos - database_repositories = [] - for api_data in api_repositories.values(): - repo_data, _created = RepositoryMetadata.objects.update_or_create( - repo_name=api_data["full_name"], - defaults={ - 'repo_name': api_data["full_name"], - 'description': api_data["description"], - 'forks': api_data["forks_count"], - 'stargazers': api_data["stargazers_count"], - 'language': api_data["language"], - } - ) - 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() - - 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}) - - -def timeline(request: WSGIRequest) -> HttpResponse: - """Render timeline view.""" - return render(request, 'home/timeline.html') -- cgit v1.2.3