diff options
author | 2023-04-01 13:00:13 +0200 | |
---|---|---|
committer | 2023-04-01 13:11:17 +0200 | |
commit | 501dc165e1444a0c92060ef132bc06fbe56f2107 (patch) | |
tree | ddabee60d8072ab2765f6100d7ffc8e51cb0b83a | |
parent | Update footer (diff) |
Delete deprecated filesimprove-performance
549 files changed, 0 insertions, 33843 deletions
diff --git a/pydis_site/README.md b/pydis_site/README.md deleted file mode 100644 index db402743..00000000 --- a/pydis_site/README.md +++ /dev/null @@ -1,68 +0,0 @@ -# `pydis_site` project directory - -This directory hosts the root of our **Django project**[^1], and is responsible -for all logic powering our website. Let's go over the directories in detail: - -- [`apps`](./apps) contains our **Django apps**, which are the building blocks - that make up our Django project. A Django project must always consist of one - or more apps, and these apps can be made completely modular and reusable - across any Django project. In our project, each app controls a distinct part - of our website, such as the API or our resources system. - - For more information on reusable apps, see the official Django tutorial, - [which has a section on reusable - apps](https://docs.djangoproject.com/en/dev/intro/reusable-apps/). To learn - more about our specific apps, see the README inside the app folder itself. - -- [`static`](./static) contains our **static files**, such as CSS, JavaScript, - images, and anything else that isn't either content or Python code. Static - files relevant for a specific application are put into subdirectories named - after the application. For example, static files used by the `resources` app go in `static/resources`. - -- [`templates`](./templates) contains our **[Django - templates](https://docs.djangoproject.com/en/dev/topics/templates/)**. Like - with static files, templates specific to a single application are stored in a - subdirectory named after that application. We also have two special templates - here: - - - `404.html`, which is our error page shown when a site was not found. - - - `500.html`, which is our error page shown in the astronomically rare case - that we encounter an internal server error. - - -Note that for both `static` and `templates`, we are not using the default Django -directory structure which puts these directories in a directory per app (in our -case, this would for example be ``pydis_site/apps/content/static/``). - -We also have a few files in here that are relevant or useful in large parts of -the website: - -- [`context_processors.py`](./context_processors.py), which contains custom - *context processors* that add variables to the Django template context. To - read more, see the [`RequestContext` documentation from - Django](https://docs.djangoproject.com/en/dev/ref/templates/api/#django.template.RequestContext) - -- [`settings.py`](./settings.py), our Django settings file. This controls all - manner of crucial things, for instance, we use it to configure logging, our - connection to the database, which applications are run by the project, which - middleware we are using, and variables for `django-simple-bulma` (which - determines frontend colours & extensions for our pages). - -- [`urls.py`](./urls.py), the URL configuration for the project itself. Here we - can forward certain URL paths to our different apps, which have their own - `urls.py` files to configure where their subpaths will lead. These files - determine _which URLs will lead to which Django views_. - -- [`wsgi.py`](./wsgi.py), which serves as an adapter for - [`gunicorn`](https://github.com/benoitc/gunicorn), - [`uwsgi`](https://github.com/unbit/uwsgi), or other application servers to run - our application in production. Unless you want to test an interaction between - our application and those servers, you probably won't need to touch this. - - -For more information about contributing to our projects, please see our -[Contributing -page](https://www.pythondiscord.com/pages/guides/pydis-guides/contributing/). - -[^1]: See [Django Glossary: project](https://docs.djangoproject.com/en/dev/glossary/#term-project) diff --git a/pydis_site/__init__.py b/pydis_site/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pydis_site/__init__.py +++ /dev/null diff --git a/pydis_site/apps/__init__.py b/pydis_site/apps/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pydis_site/apps/__init__.py +++ /dev/null diff --git a/pydis_site/apps/api/README.md b/pydis_site/apps/api/README.md deleted file mode 100644 index 1c6358b3..00000000 --- a/pydis_site/apps/api/README.md +++ /dev/null @@ -1,71 +0,0 @@ -# The "api" app - -This application takes care of most of the heavy lifting in the site, that is, -allowing our bot to manipulate and query information stored in the site's -database. - -We make heavy use of [Django REST -Framework](https://www.django-rest-framework.org) here, which builds on top of -Django to allow us to easily build out the -[REST](https://en.wikipedia.org/wiki/Representational_state_transfer) API -consumed by our bot. Working with the API app requires basic knowledge of DRF - -the [quickstart -guide](https://www.django-rest-framework.org/tutorial/quickstart/) is a great -resource to get started. - -## Directory structure - -Let's look over each of the subdirectories here: - -- `migrations` is the standard Django migrations folder. You usually won't need - to edit this manually, as `python manage.py makemigrations` handles this for - you in case you change our models. (Note that when generating migrations and - Django doesn't generate a human-readable name for you, please supply one - manually using `-n add_this_field`.) - -- `models` contains our Django model definitions. We put models into subfolders - relevant as to where they are used - in our case, the `bot` folder contains - models used by our bot when working with the API. Each model is contained - within its own module, such as `api/models/bot/message_deletion_context.py`, - which contains the `MessageDeletionContext` model. - -- `tests` contains tests for our API. If you're unfamilar with Django testing, - the [Django tutorial introducing automated - testing](https://docs.djangoproject.com/en/dev/intro/tutorial05/) is a great - resource, and you can also check out code in there to see how we test it. - -- `viewsets` contains our [DRF - viewsets](https://www.django-rest-framework.org/api-guide/viewsets/), and is - structured similarly to the `models` folder: The `bot` subfolder contains - viewsets relevant to the Python Bot, and each viewset is contained within its - own module. - -The remaining modules mostly do what their name suggests: - -- `admin.py`, which hooks up our models to the [Django admin - site](https://docs.djangoproject.com/en/dev/ref/contrib/admin/). - -- `apps.py` contains the Django [application - config](https://docs.djangoproject.com/en/dev/ref/applications/) for the `api` - app, and is used to run any code that should run when the app is loaded. - -- `pagination.py` contains custom - [paginators](https://www.django-rest-framework.org/api-guide/pagination/) used - within our DRF viewsets. - -- `serializers.py` contains [DRF - serializers](https://www.django-rest-framework.org/api-guide/serializers/) for - our models, and also includes validation logic for the models. - -- `signals.py` contains [Django - Signals](https://docs.djangoproject.com/en/dev/topics/signals/) for running - custom functionality in response to events such as deletion of a model - instance. - -- `urls.py` configures Django's [URL - dispatcher](https://docs.djangoproject.com/en/dev/topics/http/urls/) for our - API endpoints. - -- `views.py` is for any standard Django views that don't make sense to be put - into DRF viewsets as they provide static data or other functionality that - doesn't interact with our models. diff --git a/pydis_site/apps/api/__init__.py b/pydis_site/apps/api/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pydis_site/apps/api/__init__.py +++ /dev/null diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py deleted file mode 100644 index 2aca38a1..00000000 --- a/pydis_site/apps/api/admin.py +++ /dev/null @@ -1,467 +0,0 @@ -from __future__ import annotations - -import json -from typing import Iterable, Optional, Tuple - -from django import urls -from django.contrib import admin -from django.db.models import QuerySet -from django.http import HttpRequest -from django.utils.html import SafeString, format_html - -from .models import ( - BotSetting, - DeletedMessage, - DocumentationLink, - Infraction, - MessageDeletionContext, - Nomination, - OffTopicChannelName, - OffensiveMessage, - Role, - User -) -from .models.bot.nomination import NominationEntry - -admin.site.site_header = "Python Discord | Administration" -admin.site.site_title = "Python Discord" - - [email protected](BotSetting) -class BotSettingAdmin(admin.ModelAdmin): - """Admin formatting for the BotSetting model.""" - - fields = ("name", "data") - list_display = ("name",) - readonly_fields = ("name",) - - def has_add_permission(self, *args) -> bool: - """Prevent adding from django admin.""" - return False - - def has_delete_permission(self, *args) -> bool: - """Prevent deleting from django admin.""" - return False - - [email protected](DocumentationLink) -class DocumentationLinkAdmin(admin.ModelAdmin): - """Admin formatting for the DocumentationLink model.""" - - fields = ("package", "inventory_url", "base_url") - list_display = ("package", "inventory_url", "base_url") - list_editable = ("base_url", "inventory_url") - search_fields = ("package",) - - -class InfractionActorFilter(admin.SimpleListFilter): - """Actor Filter for Infraction Admin list page.""" - - title = "Actor" - parameter_name = "actor" - - def lookups(self, request: HttpRequest, model: NominationAdmin) -> Iterable[Tuple[int, str]]: - """Selectable values for viewer to filter by.""" - actor_ids = Infraction.objects.order_by().values_list("actor").distinct() - actors = User.objects.filter(id__in=actor_ids) - return ((a.id, a.username) for a in actors) - - def queryset(self, request: HttpRequest, queryset: QuerySet) -> Optional[QuerySet]: - """Query to filter the list of Users against.""" - if not self.value(): - return - return queryset.filter(actor__id=self.value()) - - [email protected](Infraction) -class InfractionAdmin(admin.ModelAdmin): - """Admin formatting for the Infraction model.""" - - fieldsets = ( - ("Members", {"fields": ("user", "actor")}), - ("Action", {"fields": ("type", "hidden", "active")}), - ("Dates", {"fields": ("inserted_at", "expires_at")}), - ("Reason", {"fields": ("reason",)}), - ) - readonly_fields = ( - "user", - "actor", - "type", - "inserted_at", - "expires_at", - "active", - "hidden" - ) - list_display = ( - "type", - "active", - "user", - "inserted_at", - "reason", - ) - search_fields = ( - "id", - "user__name", - "user__id", - "actor__name", - "actor__id", - "reason", - "type" - ) - list_filter = ( - "type", - "hidden", - "active", - InfractionActorFilter - ) - - def has_add_permission(self, *args) -> bool: - """Prevent adding from django admin.""" - return False - - [email protected](DeletedMessage) -class DeletedMessageAdmin(admin.ModelAdmin): - """Admin formatting for the DeletedMessage model.""" - - fields = ( - "id", - "author", - "channel_id", - "content", - "embed_data", - "context", - "view_full_log" - ) - - exclude = ("embeds", "deletion_context") - - search_fields = ( - "id", - "content", - "author__name", - "author__id", - "deletion_context__actor__name", - "deletion_context__actor__id" - ) - - list_display = ("id", "author", "channel_id") - - def embed_data(self, message: DeletedMessage) -> Optional[str]: - """Format embed data in a code block for better readability.""" - if message.embeds: - return format_html( - "<pre style='word-wrap: break-word; white-space: pre-wrap; overflow-x: auto;'>" - "<code>{0}</code></pre>", - json.dumps(message.embeds, indent=4) - ) - - embed_data.short_description = "Embeds" - - @staticmethod - def context(message: DeletedMessage) -> str: - """Provide full context info with a link through to context admin view.""" - link = urls.reverse( - "admin:api_messagedeletioncontext_change", - args=[message.deletion_context.id] - ) - details = ( - f"Deleted by {message.deletion_context.actor} at " - f"{message.deletion_context.creation}" - ) - return format_html("<a href='{0}'>{1}</a>", link, details) - - @staticmethod - def view_full_log(message: DeletedMessage) -> str: - """Provide a link to the message logs for the relevant context.""" - return format_html( - "<a href='{0}'>Click to view full context log</a>", - message.deletion_context.log_url - ) - - def has_add_permission(self, *args) -> bool: - """Prevent adding from django admin.""" - return False - - def has_change_permission(self, *args) -> bool: - """Prevent editing from django admin.""" - return False - - -class DeletedMessageInline(admin.TabularInline): - """Tabular Inline Admin model for Deleted Message to be viewed within Context.""" - - model = DeletedMessage - - [email protected](MessageDeletionContext) -class MessageDeletionContextAdmin(admin.ModelAdmin): - """Admin formatting for the MessageDeletionContext model.""" - - fields = ("actor", "creation") - list_display = ("id", "creation", "actor") - inlines = (DeletedMessageInline,) - - def has_add_permission(self, *args) -> bool: - """Prevent adding from django admin.""" - return False - - def has_change_permission(self, *args) -> bool: - """Prevent editing from django admin.""" - return False - - -class NominationActorFilter(admin.SimpleListFilter): - """Actor Filter for Nomination Admin list page.""" - - title = "Actor" - parameter_name = "actor" - - def lookups(self, request: HttpRequest, model: NominationAdmin) -> Iterable[Tuple[int, str]]: - """Selectable values for viewer to filter by.""" - actor_ids = NominationEntry.objects.order_by().values_list("actor").distinct() - actors = User.objects.filter(id__in=actor_ids) - return ((a.id, a.username) for a in actors) - - def queryset(self, request: HttpRequest, queryset: QuerySet) -> Optional[QuerySet]: - """Query to filter the list of Users against.""" - if not self.value(): - return - nomination_ids = NominationEntry.objects.filter( - actor__id=self.value() - ).values_list("nomination_id").distinct() - return queryset.filter(id__in=nomination_ids) - - [email protected](Nomination) -class NominationAdmin(admin.ModelAdmin): - """Admin formatting for the Nomination model.""" - - search_fields = ( - "user__name", - "user__id", - "end_reason" - ) - - list_filter = ("active", NominationActorFilter) - - list_display = ( - "user", - "active", - "reviewed" - ) - - fields = ( - "user", - "active", - "inserted_at", - "ended_at", - "end_reason", - "reviewed" - ) - - # only allow end reason field to be edited. - readonly_fields = ( - "user", - "active", - "inserted_at", - "ended_at", - "reviewed" - ) - - def has_add_permission(self, *args) -> bool: - """Prevent adding from django admin.""" - return False - - -class NominationEntryActorFilter(admin.SimpleListFilter): - """Actor Filter for NominationEntry Admin list page.""" - - title = "Actor" - parameter_name = "actor" - - def lookups(self, request: HttpRequest, model: NominationAdmin) -> Iterable[Tuple[int, str]]: - """Selectable values for viewer to filter by.""" - actor_ids = NominationEntry.objects.order_by().values_list("actor").distinct() - actors = User.objects.filter(id__in=actor_ids) - return ((a.id, a.username) for a in actors) - - def queryset(self, request: HttpRequest, queryset: QuerySet) -> Optional[QuerySet]: - """Query to filter the list of Users against.""" - if not self.value(): - return - return queryset.filter(actor__id=self.value()) - - [email protected](NominationEntry) -class NominationEntryAdmin(admin.ModelAdmin): - """Admin formatting for the NominationEntry model.""" - - search_fields = ( - "actor__name", - "actor__id", - "reason", - ) - - list_filter = (NominationEntryActorFilter,) - - list_display = ( - "nomination", - "actor", - ) - - fields = ( - "nomination", - "actor", - "reason", - "inserted_at", - ) - - # only allow reason field to be edited - readonly_fields = ( - "nomination", - "actor", - "inserted_at", - ) - - def has_add_permission(self, request: HttpRequest) -> bool: - """Disable adding new nomination entry from admin.""" - return False - - [email protected](OffTopicChannelName) -class OffTopicChannelNameAdmin(admin.ModelAdmin): - """Admin formatting for the OffTopicChannelName model.""" - - search_fields = ("name",) - list_filter = ("used",) - - [email protected](OffensiveMessage) -class OffensiveMessageAdmin(admin.ModelAdmin): - """Admin formatting for the OffensiveMessage model.""" - - def message_jumplink(self, message: OffensiveMessage) -> SafeString: - """Message ID hyperlinked to the direct discord jumplink.""" - return format_html( - '<a href="https://canary.discordapp.com/channels/267624335836053506/{0}/{1}">{1}</a>', - message.channel_id, - message.id - ) - - message_jumplink.short_description = "Message ID" - - search_fields = ("id", "channel_id") - list_display = ("id", "channel_id", "delete_date") - fields = ("message_jumplink", "channel_id", "delete_date") - readonly_fields = ("message_jumplink", "channel_id") - - def has_add_permission(self, *args) -> bool: - """Prevent adding from django admin.""" - return False - - [email protected](Role) -class RoleAdmin(admin.ModelAdmin): - """Admin formatting for the Role model.""" - - def coloured_name(self, role: Role) -> SafeString: - """Role name with html style colouring.""" - return format_html( - '<span style="color: {0}!important; font-weight: bold;">{1}</span>', - f"#{role.colour:06X}", - role.name - ) - - coloured_name.short_description = "Name" - - def colour_with_preview(self, role: Role) -> SafeString: - """Show colour value in both int and hex, in bolded and coloured style.""" - return format_html( - "<span style='color: {0}; font-weight: bold;'>{0} ({1})</span>", - f"#{role.colour:06x}", - role.colour - ) - - colour_with_preview.short_description = "Colour" - - def permissions_with_calc_link(self, role: Role) -> SafeString: - """Show permissions with link to API permissions calculator page.""" - return format_html( - "<a href='https://discordapi.com/permissions.html#{0}' target='_blank'>{0}</a>", - role.permissions - ) - - permissions_with_calc_link.short_description = "Permissions" - - search_fields = ("name", "id") - list_display = ("coloured_name",) - fields = ("id", "name", "colour_with_preview", "permissions_with_calc_link", "position") - - def has_add_permission(self, *args) -> bool: - """Prevent adding from django admin.""" - return False - - def has_change_permission(self, *args) -> bool: - """Prevent editing from django admin.""" - return False - - -class UserRoleFilter(admin.SimpleListFilter): - """List Filter for User list Admin page.""" - - title = "Role" - parameter_name = "role" - - def lookups(self, request: HttpRequest, model: UserAdmin) -> Iterable[Tuple[str, str]]: - """Selectable values for viewer to filter by.""" - roles = Role.objects.all() - return ((r.name, r.name) for r in roles) - - def queryset(self, request: HttpRequest, queryset: QuerySet) -> Optional[QuerySet]: - """Query to filter the list of Users against.""" - if not self.value(): - return - role = Role.objects.get(name=self.value()) - return queryset.filter(roles__contains=[role.id]) - - [email protected](User) -class UserAdmin(admin.ModelAdmin): - """Admin formatting for the User model.""" - - def top_role_coloured(self, user: User) -> SafeString: - """Returns the top role of the user with html style matching role colour.""" - return format_html( - '<span style="color: {0}; font-weight: bold;">{1}</span>', - f"#{user.top_role.colour:06X}", - user.top_role.name - ) - - top_role_coloured.short_description = "Top Role" - - def all_roles_coloured(self, user: User) -> SafeString: - """Returns all user roles with html style matching role colours.""" - roles = Role.objects.filter(id__in=user.roles) - return format_html( - "</br>".join( - f'<span style="color: #{r.colour:06X}; font-weight: bold;">{r.name}</span>' - for r in roles - ) - ) - - all_roles_coloured.short_description = "All Roles" - - search_fields = ("name", "id", "roles") - list_filter = (UserRoleFilter, "in_guild") - list_display = ("username", "top_role_coloured", "in_guild") - fields = ("username", "id", "in_guild", "all_roles_coloured") - sortable_by = ("username",) - - def has_add_permission(self, *args) -> bool: - """Prevent adding from django admin.""" - return False - - def has_change_permission(self, *args) -> bool: - """Prevent editing from django admin.""" - return False diff --git a/pydis_site/apps/api/apps.py b/pydis_site/apps/api/apps.py deleted file mode 100644 index 18eda9e3..00000000 --- a/pydis_site/apps/api/apps.py +++ /dev/null @@ -1,15 +0,0 @@ -from django.apps import AppConfig - - -class ApiConfig(AppConfig): - """Django AppConfig for the API app.""" - - name = 'pydis_site.apps.api' - - def ready(self) -> None: - """ - Gets called as soon as the registry is fully populated. - - https://docs.djangoproject.com/en/3.2/ref/applications/#django.apps.AppConfig.ready - """ - import pydis_site.apps.api.signals # noqa: F401 diff --git a/pydis_site/apps/api/github_utils.py b/pydis_site/apps/api/github_utils.py deleted file mode 100644 index 44c571c3..00000000 --- a/pydis_site/apps/api/github_utils.py +++ /dev/null @@ -1,207 +0,0 @@ -"""Utilities for working with the GitHub API.""" -import dataclasses -import datetime -import math -import typing - -import httpx -import jwt - -from pydis_site import settings - -MAX_RUN_TIME = datetime.timedelta(minutes=10) -"""The maximum time allowed before an action is declared timed out.""" - - -class ArtifactProcessingError(Exception): - """Base exception for other errors related to processing a GitHub artifact.""" - - status: int - - -class UnauthorizedError(ArtifactProcessingError): - """The application does not have permission to access the requested repo.""" - - status = 401 - - -class NotFoundError(ArtifactProcessingError): - """The requested resource could not be found.""" - - status = 404 - - -class ActionFailedError(ArtifactProcessingError): - """The requested workflow did not conclude successfully.""" - - status = 400 - - -class RunTimeoutError(ArtifactProcessingError): - """The requested workflow run was not ready in time.""" - - status = 408 - - -class RunPendingError(ArtifactProcessingError): - """The requested workflow run is still pending, try again later.""" - - status = 202 - - [email protected](frozen=True) -class WorkflowRun: - """ - A workflow run from the GitHub API. - - https://docs.github.com/en/rest/actions/workflow-runs#get-a-workflow-run - """ - - name: str - head_sha: str - created_at: str - status: str - conclusion: str - artifacts_url: str - - @classmethod - def from_raw(cls, data: dict[str, typing.Any]): - """Create an instance using the raw data from the API, discarding unused fields.""" - return cls(**{ - key.name: data[key.name] for key in dataclasses.fields(cls) - }) - - -def generate_token() -> str: - """ - Generate a JWT token to access the GitHub API. - - The token is valid for roughly 10 minutes after generation, before the API starts - returning 401s. - - Refer to: - https://docs.github.com/en/developers/apps/building-github-apps/authenticating-with-github-apps#authenticating-as-a-github-app - """ - now = datetime.datetime.now() - return jwt.encode( - { - "iat": math.floor((now - datetime.timedelta(seconds=60)).timestamp()), # Issued at - "exp": math.floor((now + datetime.timedelta(minutes=9)).timestamp()), # Expires at - "iss": settings.GITHUB_APP_ID, - }, - settings.GITHUB_APP_KEY, - algorithm="RS256" - ) - - -def authorize(owner: str, repo: str) -> httpx.Client: - """ - Get an access token for the requested repository. - - The process is roughly: - - GET app/installations to get a list of all app installations - - POST <app_access_token> to get a token to access the given app - - GET installation/repositories and check if the requested one is part of those - """ - client = httpx.Client( - base_url=settings.GITHUB_API, - headers={"Authorization": f"bearer {generate_token()}"}, - timeout=10, - ) - - try: - # Get a list of app installations we have access to - apps = client.get("app/installations") - apps.raise_for_status() - - for app in apps.json(): - # Look for an installation with the right owner - if app["account"]["login"] != owner: - continue - - # Get the repositories of the specified owner - app_token = client.post(app["access_tokens_url"]) - app_token.raise_for_status() - client.headers["Authorization"] = f"bearer {app_token.json()['token']}" - - repos = client.get("installation/repositories") - repos.raise_for_status() - - # Search for the request repository - for accessible_repo in repos.json()["repositories"]: - if accessible_repo["name"] == repo: - # We've found the correct repository, and it's accessible with the current auth - return client - - raise NotFoundError( - "Could not find the requested repository. Make sure the application can access it." - ) - - except BaseException as e: - # Close the client if we encountered an unexpected exception - client.close() - raise e - - -def check_run_status(run: WorkflowRun) -> str: - """Check if the provided run has been completed, otherwise raise an exception.""" - created_at = datetime.datetime.strptime(run.created_at, settings.GITHUB_TIMESTAMP_FORMAT) - run_time = datetime.datetime.utcnow() - created_at - - if run.status != "completed": - if run_time <= MAX_RUN_TIME: - raise RunPendingError( - f"The requested run is still pending. It was created " - f"{run_time.seconds // 60}:{run_time.seconds % 60 :>02} minutes ago." - ) - else: - raise RunTimeoutError("The requested workflow was not ready in time.") - - if run.conclusion != "success": - # The action failed, or did not run - raise ActionFailedError(f"The requested workflow ended with: {run.conclusion}") - - # The requested action is ready - return run.artifacts_url - - -def get_artifact(owner: str, repo: str, sha: str, action_name: str, artifact_name: str) -> str: - """Get a download URL for a build artifact.""" - client = authorize(owner, repo) - - try: - # Get the workflow runs for this repository - runs = client.get(f"/repos/{owner}/{repo}/actions/runs", params={"per_page": 100}) - runs.raise_for_status() - runs = runs.json() - - # Filter the runs for the one associated with the given SHA - for run in runs["workflow_runs"]: - run = WorkflowRun.from_raw(run) - if run.name == action_name and sha == run.head_sha: - break - else: - raise NotFoundError( - "Could not find a run matching the provided settings in the previous hundred runs." - ) - - # Check the workflow status - url = check_run_status(run) - - # Filter the artifacts, and return the download URL - artifacts = client.get(url) - artifacts.raise_for_status() - - for artifact in artifacts.json()["artifacts"]: - if artifact["name"] == artifact_name: - data = client.get(artifact["archive_download_url"]) - if data.status_code == 302: - return str(data.next_request.url) - - # The following line is left untested since it should in theory be impossible - data.raise_for_status() # pragma: no cover - - raise NotFoundError("Could not find an artifact matching the provided name.") - - finally: - client.close() diff --git a/pydis_site/apps/api/migrations/0001_initial.py b/pydis_site/apps/api/migrations/0001_initial.py deleted file mode 100644 index dca6d17f..00000000 --- a/pydis_site/apps/api/migrations/0001_initial.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 2.1 on 2018-08-15 17:28 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='SnakeName', - fields=[ - ('name', models.CharField(max_length=100, primary_key=True, serialize=False)), - ('scientific', models.CharField(max_length=150)), - ], - ), - ] diff --git a/pydis_site/apps/api/migrations/0002_documentationlink.py b/pydis_site/apps/api/migrations/0002_documentationlink.py deleted file mode 100644 index 5dee679a..00000000 --- a/pydis_site/apps/api/migrations/0002_documentationlink.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 2.1 on 2018-08-16 19:42 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='DocumentationLink', - fields=[ - ('package', models.CharField(max_length=50, primary_key=True, serialize=False)), - ('base_url', models.URLField()), - ('inventory_url', models.URLField()), - ], - ), - ] diff --git a/pydis_site/apps/api/migrations/0003_offtopicchannelname.py b/pydis_site/apps/api/migrations/0003_offtopicchannelname.py deleted file mode 100644 index 2f19bfd8..00000000 --- a/pydis_site/apps/api/migrations/0003_offtopicchannelname.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 2.1 on 2018-08-31 22:21 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0002_documentationlink'), - ] - - operations = [ - migrations.CreateModel( - name='OffTopicChannelName', - fields=[ - ('name', models.CharField(max_length=96, primary_key=True, serialize=False, validators=[django.core.validators.RegexValidator(regex='^[a-z0-9-]+$')])), - ], - ), - ] diff --git a/pydis_site/apps/api/migrations/0004_role.py b/pydis_site/apps/api/migrations/0004_role.py deleted file mode 100644 index 0a6b6c43..00000000 --- a/pydis_site/apps/api/migrations/0004_role.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 2.1 on 2018-09-01 19:54 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0003_offtopicchannelname'), - ] - - operations = [ - migrations.CreateModel( - name='Role', - fields=[ - ('id', models.BigIntegerField(help_text="The role's ID, taken from Discord.", primary_key=True, serialize=False, validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')])), - ('name', models.CharField(help_text="The role's name, taken from Discord.", max_length=100)), - ('colour', models.IntegerField(help_text='The integer value of the colour of this role from Discord.', validators=[django.core.validators.MinValueValidator(limit_value=0, message='Colour hex cannot be negative.')])), - ('permissions', models.IntegerField(help_text='The integer value of the permission bitset of this role from Discord.', validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role permissions cannot be negative.'), django.core.validators.MaxValueValidator(limit_value=8589934592, message='Role permission bitset exceeds value of having all permissions')])), - ], - ), - ] diff --git a/pydis_site/apps/api/migrations/0005_user.py b/pydis_site/apps/api/migrations/0005_user.py deleted file mode 100644 index a771119c..00000000 --- a/pydis_site/apps/api/migrations/0005_user.py +++ /dev/null @@ -1,38 +0,0 @@ -# Generated by Django 2.1 on 2018-09-01 20:02 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0004_role'), - ] - - operations = [ - migrations.CreateModel( - name='Member', - fields=[ - ('id', models.BigIntegerField(help_text='The ID of this user, taken from Discord.', primary_key=True, serialize=False, validators=[django.core.validators.MinValueValidator(limit_value=0, message='User IDs cannot be negative.')])), - ('name', models.CharField(help_text='The username, taken from Discord.', max_length=32)), - ('discriminator', models.PositiveSmallIntegerField(help_text='The discriminator of this user, taken from Discord.', validators=[django.core.validators.MaxValueValidator(limit_value=9999, message='Discriminators may not exceed `9999`.')])), - ('avatar_hash', models.CharField(help_text="The user's avatar hash, taken from Discord. Null if the user does not have any custom avatar.", max_length=100, null=True)), - ], - ), - migrations.AlterField( - model_name='role', - name='id', - field=models.BigIntegerField(help_text='The role ID, taken from Discord.', primary_key=True, serialize=False, validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')]), - ), - migrations.AlterField( - model_name='role', - name='name', - field=models.CharField(help_text='The role name, taken from Discord.', max_length=100), - ), - migrations.AddField( - model_name='member', - name='roles', - field=models.ManyToManyField(help_text='Any roles this user has on our server.', to='api.Role'), - ), - ] diff --git a/pydis_site/apps/api/migrations/0006_add_help_texts.py b/pydis_site/apps/api/migrations/0006_add_help_texts.py deleted file mode 100644 index a57d2289..00000000 --- a/pydis_site/apps/api/migrations/0006_add_help_texts.py +++ /dev/null @@ -1,44 +0,0 @@ -# Generated by Django 2.1.1 on 2018-09-21 20:26 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0005_user'), - ] - - operations = [ - migrations.AlterField( - model_name='documentationlink', - name='base_url', - field=models.URLField(help_text='The base URL from which documentation will be available for this project. Used to generate links to various symbols within this package.'), - ), - migrations.AlterField( - model_name='documentationlink', - name='inventory_url', - field=models.URLField(help_text='The URL at which the Sphinx inventory is available for this package.'), - ), - migrations.AlterField( - model_name='documentationlink', - name='package', - field=models.CharField(help_text='The Python package name that this documentation link belongs to.', max_length=50, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='offtopicchannelname', - name='name', - field=models.CharField(help_text='The actual channel name that will be used on our Discord server.', max_length=96, primary_key=True, serialize=False, validators=[django.core.validators.RegexValidator(regex='^[a-z0-9-]+$')]), - ), - migrations.AlterField( - model_name='snakename', - name='name', - field=models.CharField(help_text="The regular name for this snake, e.g. 'Python'.", max_length=100, primary_key=True, serialize=False), - ), - migrations.AlterField( - model_name='snakename', - name='scientific', - field=models.CharField(help_text="The scientific name for this snake, e.g. 'Python bivittatus'.", max_length=150), - ), - ] diff --git a/pydis_site/apps/api/migrations/0007_tag.py b/pydis_site/apps/api/migrations/0007_tag.py deleted file mode 100644 index b6d146fe..00000000 --- a/pydis_site/apps/api/migrations/0007_tag.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 2.1.1 on 2018-09-21 22:05 - -import pydis_site.apps.api.models -import django.contrib.postgres.fields.jsonb -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0006_add_help_texts'), - ] - - operations = [ - migrations.CreateModel( - name='Tag', - fields=[ - ('title', models.CharField(help_text='The title of this tag, shown in searches and providing a quick overview over what this embed contains.', max_length=100, primary_key=True, serialize=False)), - ('embed', django.contrib.postgres.fields.jsonb.JSONField(help_text='The actual embed shown by this tag.')), - ], - bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), - ), - ] diff --git a/pydis_site/apps/api/migrations/0008_tag_embed_validator.py b/pydis_site/apps/api/migrations/0008_tag_embed_validator.py deleted file mode 100644 index d92042d2..00000000 --- a/pydis_site/apps/api/migrations/0008_tag_embed_validator.py +++ /dev/null @@ -1,13 +0,0 @@ -# Generated by Django 2.1.1 on 2018-09-23 10:07 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0007_tag'), - ] - - operations = [ - ] diff --git a/pydis_site/apps/api/migrations/0009_snakefact.py b/pydis_site/apps/api/migrations/0009_snakefact.py deleted file mode 100644 index fd583846..00000000 --- a/pydis_site/apps/api/migrations/0009_snakefact.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 2.1.2 on 2018-10-11 14:25 - -import pydis_site.apps.api.models -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0008_tag_embed_validator'), - ] - - operations = [ - migrations.CreateModel( - name='SnakeFact', - fields=[ - ('fact', models.CharField(help_text='A fact about snakes.', max_length=200, primary_key=True, serialize=False)), - ], - bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), - ), - ] diff --git a/pydis_site/apps/api/migrations/0010_snakeidiom.py b/pydis_site/apps/api/migrations/0010_snakeidiom.py deleted file mode 100644 index 7d06ce5f..00000000 --- a/pydis_site/apps/api/migrations/0010_snakeidiom.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 2.1.2 on 2018-10-19 16:27 - -import pydis_site.apps.api.models -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0009_snakefact'), - ] - - operations = [ - migrations.CreateModel( - name='SnakeIdiom', - fields=[ - ('idiom', models.CharField(help_text='A snake idiom', max_length=140, primary_key=True, serialize=False)), - ], - bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), - ), - ] diff --git a/pydis_site/apps/api/migrations/0011_auto_20181020_1904.py b/pydis_site/apps/api/migrations/0011_auto_20181020_1904.py deleted file mode 100644 index bb5a6325..00000000 --- a/pydis_site/apps/api/migrations/0011_auto_20181020_1904.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.1.2 on 2018-10-20 19:04 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0010_snakeidiom'), - ] - - operations = [ - migrations.AlterField( - model_name='snakeidiom', - name='idiom', - field=models.CharField(help_text='A saying about a snake.', max_length=140, primary_key=True, serialize=False), - ), - ] diff --git a/pydis_site/apps/api/migrations/0012_specialsnake.py b/pydis_site/apps/api/migrations/0012_specialsnake.py deleted file mode 100644 index ed0c1563..00000000 --- a/pydis_site/apps/api/migrations/0012_specialsnake.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 2.1.2 on 2018-10-22 09:53 - -import pydis_site.apps.api.models -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0011_auto_20181020_1904'), - ] - - operations = [ - migrations.CreateModel( - name='SpecialSnake', - fields=[ - ('name', models.CharField(max_length=140, primary_key=True, serialize=False)), - ('info', models.TextField()), - ], - bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), - ), - ] diff --git a/pydis_site/apps/api/migrations/0013_specialsnake_image.py b/pydis_site/apps/api/migrations/0013_specialsnake_image.py deleted file mode 100644 index 8ba3432f..00000000 --- a/pydis_site/apps/api/migrations/0013_specialsnake_image.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 2.1.2 on 2018-10-23 11:51 - -import datetime -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0012_specialsnake'), - ] - - operations = [ - migrations.AddField( - model_name='specialsnake', - name='image', - field=models.URLField(default=datetime.datetime(2018, 10, 23, 11, 51, 23, 703868, tzinfo=datetime.timezone.utc)), - preserve_default=False, - ), - ] diff --git a/pydis_site/apps/api/migrations/0014_auto_20181025_1959.py b/pydis_site/apps/api/migrations/0014_auto_20181025_1959.py deleted file mode 100644 index 3599d2cd..00000000 --- a/pydis_site/apps/api/migrations/0014_auto_20181025_1959.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 2.1.2 on 2018-10-25 19:59 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0013_specialsnake_image'), - ] - - operations = [ - migrations.AlterField( - model_name='specialsnake', - name='info', - field=models.TextField(help_text='Info about a special snake.'), - ), - migrations.AlterField( - model_name='specialsnake', - name='name', - field=models.CharField(help_text='A special snake name.', max_length=140, primary_key=True, serialize=False), - ), - ] diff --git a/pydis_site/apps/api/migrations/0015_auto_20181027_1617.py b/pydis_site/apps/api/migrations/0015_auto_20181027_1617.py deleted file mode 100644 index 8973ff6d..00000000 --- a/pydis_site/apps/api/migrations/0015_auto_20181027_1617.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.1.2 on 2018-10-27 16:17 - -import django.contrib.postgres.fields -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0014_auto_20181025_1959'), - ] - - operations = [ - migrations.AlterField( - model_name='specialsnake', - name='image', - field=django.contrib.postgres.fields.ArrayField(base_field=models.URLField(), size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0016_auto_20181027_1619.py b/pydis_site/apps/api/migrations/0016_auto_20181027_1619.py deleted file mode 100644 index b8bdfb16..00000000 --- a/pydis_site/apps/api/migrations/0016_auto_20181027_1619.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.1.2 on 2018-10-27 16:19 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0015_auto_20181027_1617'), - ] - - operations = [ - migrations.RenameField( - model_name='specialsnake', - old_name='image', - new_name='images', - ), - ] diff --git a/pydis_site/apps/api/migrations/0017_auto_20181029_1921.py b/pydis_site/apps/api/migrations/0017_auto_20181029_1921.py deleted file mode 100644 index 012bda61..00000000 --- a/pydis_site/apps/api/migrations/0017_auto_20181029_1921.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.1.2 on 2018-10-29 19:21 - -import django.contrib.postgres.fields -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0016_auto_20181027_1619'), - ] - - operations = [ - migrations.AlterField( - model_name='specialsnake', - name='images', - field=django.contrib.postgres.fields.ArrayField(base_field=models.URLField(), help_text='Images displaying this special snake.', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0018_messagedeletioncontext.py b/pydis_site/apps/api/migrations/0018_messagedeletioncontext.py deleted file mode 100644 index 7e372d04..00000000 --- a/pydis_site/apps/api/migrations/0018_messagedeletioncontext.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 2.1.1 on 2018-11-18 20:12 - -import pydis_site.apps.api.models -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0017_auto_20181029_1921'), - ] - - operations = [ - migrations.CreateModel( - name='MessageDeletionContext', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('creation', models.DateTimeField(help_text='When this deletion took place.')), - ('actor', models.ForeignKey(help_text='The original actor causing this deletion. Could be the author of a manual clean command invocation, the bot when executing automatic actions, or nothing to indicate that the bulk deletion was not issued by us.', null=True, on_delete=django.db.models.deletion.CASCADE, to='api.User')), - ], - bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), - ), - ] diff --git a/pydis_site/apps/api/migrations/0018_user_rename.py b/pydis_site/apps/api/migrations/0018_user_rename.py deleted file mode 100644 index f88eb5bc..00000000 --- a/pydis_site/apps/api/migrations/0018_user_rename.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.1.3 on 2018-11-19 20:09 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0017_auto_20181029_1921'), - ] - - operations = [ - migrations.RenameModel( - old_name='Member', - new_name='User', - ), - ] diff --git a/pydis_site/apps/api/migrations/0019_deletedmessage.py b/pydis_site/apps/api/migrations/0019_deletedmessage.py deleted file mode 100644 index 25d04434..00000000 --- a/pydis_site/apps/api/migrations/0019_deletedmessage.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 2.1.1 on 2018-11-18 20:26 - -import pydis_site.apps.api.models -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0018_messagedeletioncontext'), - ] - - operations = [ - migrations.CreateModel( - name='DeletedMessage', - fields=[ - ('id', models.BigIntegerField(help_text='The message ID as taken from Discord.', primary_key=True, serialize=False, validators=[django.core.validators.MinValueValidator(limit_value=0, message='Message IDs cannot be negative.')])), - ('channel_id', models.BigIntegerField(help_text='The channel ID that this message was sent in, taken from Discord.', validators=[django.core.validators.MinValueValidator(limit_value=0, message='Channel IDs cannot be negative.')])), - ('content', models.CharField(help_text='The content of this message, taken from Discord.', max_length=2000)), - ('embeds', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(validators=[]), help_text='Embeds attached to this message.', size=None)), - ('author', models.ForeignKey(help_text='The author of this message.', on_delete=django.db.models.deletion.CASCADE, to='api.User')), - ('deletion_context', models.ForeignKey(help_text='The deletion context this message is part of.', on_delete=django.db.models.deletion.CASCADE, to='api.MessageDeletionContext')), - ], - options={ - 'abstract': False, - }, - bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), - ), - ] diff --git a/pydis_site/apps/api/migrations/0019_user_in_guild.py b/pydis_site/apps/api/migrations/0019_user_in_guild.py deleted file mode 100644 index fda008c4..00000000 --- a/pydis_site/apps/api/migrations/0019_user_in_guild.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.1.3 on 2018-11-19 20:30 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0018_user_rename'), - ] - - operations = [ - migrations.AddField( - model_name='user', - name='in_guild', - field=models.BooleanField(default=True, help_text='Whether this user is in our server.'), - ), - ] diff --git a/pydis_site/apps/api/migrations/0020_add_snake_field_validators.py b/pydis_site/apps/api/migrations/0020_add_snake_field_validators.py deleted file mode 100644 index 3b625f9b..00000000 --- a/pydis_site/apps/api/migrations/0020_add_snake_field_validators.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 2.1.2 on 2018-11-24 17:11 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0019_user_in_guild'), - ] - - operations = [ - migrations.AlterField( - model_name='snakename', - name='name', - field=models.CharField(help_text="The regular name for this snake, e.g. 'Python'.", max_length=100, primary_key=True, serialize=False, validators=[django.core.validators.RegexValidator(regex='^([^0-9])+$')]), - ), - migrations.AlterField( - model_name='snakename', - name='scientific', - field=models.CharField(help_text="The scientific name for this snake, e.g. 'Python bivittatus'.", max_length=150, validators=[django.core.validators.RegexValidator(regex='^([^0-9])+$')]), - ), - ] diff --git a/pydis_site/apps/api/migrations/0020_infraction.py b/pydis_site/apps/api/migrations/0020_infraction.py deleted file mode 100644 index 96c71687..00000000 --- a/pydis_site/apps/api/migrations/0020_infraction.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 2.1.3 on 2018-11-19 22:02 - -import pydis_site.apps.api.models -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0019_user_in_guild'), - ] - - operations = [ - migrations.CreateModel( - name='Infraction', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('inserted_at', models.DateTimeField(auto_now_add=True, help_text='The date and time of the creation of this infraction.')), - ('expires_at', models.DateTimeField(help_text="The date and time of the expiration of this infraction. Null if the infraction is permanent or it can't expire.", null=True)), - ('active', models.BooleanField(default=True, help_text='Whether the infraction is still active.')), - ('type', models.CharField(choices=[('note', 'Note'), ('warning', 'Warning'), ('mute', 'Mute'), ('ban', 'Ban'), ('kick', 'Kick'), ('superstar', 'Superstar')], help_text='The type of the infraction.', max_length=9)), - ('reason', models.TextField(help_text='The reason for the infraction.')), - ('hidden', models.BooleanField(default=False, help_text='Whether the infraction is a shadow infraction.')), - ('actor', models.ForeignKey(help_text='The user which applied the infraction.', on_delete=django.db.models.deletion.CASCADE, related_name='infractions_given', to='api.User')), - ('user', models.ForeignKey(help_text='The user to which the infraction was applied.', on_delete=django.db.models.deletion.CASCADE, related_name='infractions_received', to='api.User')), - ], - bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), - ), - ] diff --git a/pydis_site/apps/api/migrations/0021_add_special_snake_validator.py b/pydis_site/apps/api/migrations/0021_add_special_snake_validator.py deleted file mode 100644 index d41b96e5..00000000 --- a/pydis_site/apps/api/migrations/0021_add_special_snake_validator.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.1.2 on 2018-11-25 14:59 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0020_add_snake_field_validators'), - ] - - operations = [ - migrations.AlterField( - model_name='specialsnake', - name='name', - field=models.CharField(help_text='A special snake name.', max_length=140, primary_key=True, serialize=False, validators=[django.core.validators.RegexValidator(regex='^([^0-9])+$')]), - ), - ] diff --git a/pydis_site/apps/api/migrations/0021_infraction_reason_null.py b/pydis_site/apps/api/migrations/0021_infraction_reason_null.py deleted file mode 100644 index 6600f230..00000000 --- a/pydis_site/apps/api/migrations/0021_infraction_reason_null.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.1.3 on 2018-11-21 00:50 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0020_infraction'), - ] - - operations = [ - migrations.AlterField( - model_name='infraction', - name='reason', - field=models.TextField(help_text='The reason for the infraction.', null=True), - ), - ] diff --git a/pydis_site/apps/api/migrations/0021_merge_20181125_1015.py b/pydis_site/apps/api/migrations/0021_merge_20181125_1015.py deleted file mode 100644 index d8eaa510..00000000 --- a/pydis_site/apps/api/migrations/0021_merge_20181125_1015.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 2.1.1 on 2018-11-25 10:15 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0020_add_snake_field_validators'), - ('api', '0019_deletedmessage'), - ] - - operations = [ - ] diff --git a/pydis_site/apps/api/migrations/0022_infraction_remove_note.py b/pydis_site/apps/api/migrations/0022_infraction_remove_note.py deleted file mode 100644 index eba84610..00000000 --- a/pydis_site/apps/api/migrations/0022_infraction_remove_note.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.1.3 on 2018-11-21 21:07 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0021_infraction_reason_null'), - ] - - operations = [ - migrations.AlterField( - model_name='infraction', - name='type', - field=models.CharField(choices=[('warning', 'Warning'), ('mute', 'Mute'), ('ban', 'Ban'), ('kick', 'Kick'), ('superstar', 'Superstar')], help_text='The type of the infraction.', max_length=9), - ), - ] diff --git a/pydis_site/apps/api/migrations/0023_merge_infractions_snake_validators.py b/pydis_site/apps/api/migrations/0023_merge_infractions_snake_validators.py deleted file mode 100644 index 916f78f2..00000000 --- a/pydis_site/apps/api/migrations/0023_merge_infractions_snake_validators.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 2.1.3 on 2018-11-29 19:37 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0022_infraction_remove_note'), - ('api', '0021_add_special_snake_validator'), - ] - - operations = [ - ] diff --git a/pydis_site/apps/api/migrations/0024_add_note_infraction_type.py b/pydis_site/apps/api/migrations/0024_add_note_infraction_type.py deleted file mode 100644 index 4adb53b8..00000000 --- a/pydis_site/apps/api/migrations/0024_add_note_infraction_type.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.1.4 on 2019-01-05 14:52 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0023_merge_infractions_snake_validators'), - ] - - operations = [ - migrations.AlterField( - model_name='infraction', - name='type', - field=models.CharField(choices=[('note', 'Note'), ('warning', 'Warning'), ('mute', 'Mute'), ('kick', 'Kick'), ('ban', 'Ban'), ('superstar', 'Superstar')], help_text='The type of the infraction.', max_length=9), - ), - ] diff --git a/pydis_site/apps/api/migrations/0025_allow_custom_inserted_at_infraction_field.py b/pydis_site/apps/api/migrations/0025_allow_custom_inserted_at_infraction_field.py deleted file mode 100644 index c7fac012..00000000 --- a/pydis_site/apps/api/migrations/0025_allow_custom_inserted_at_infraction_field.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.1.4 on 2019-01-06 16:01 - -from django.db import migrations, models -from django.utils import timezone - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0024_add_note_infraction_type'), - ] - - operations = [ - migrations.AlterField( - model_name='infraction', - name='inserted_at', - field=models.DateTimeField(default=timezone.now, help_text='The date and time of the creation of this infraction.'), - ), - ] diff --git a/pydis_site/apps/api/migrations/0026_use_proper_default_for_infraction_insertion_date.py b/pydis_site/apps/api/migrations/0026_use_proper_default_for_infraction_insertion_date.py deleted file mode 100644 index 56f3b2b8..00000000 --- a/pydis_site/apps/api/migrations/0026_use_proper_default_for_infraction_insertion_date.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.1.5 on 2019-01-09 19:50 - -from django.db import migrations, models -import django.utils.timezone - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0025_allow_custom_inserted_at_infraction_field'), - ] - - operations = [ - migrations.AlterField( - model_name='infraction', - name='inserted_at', - field=models.DateTimeField(default=django.utils.timezone.now, help_text='The date and time of the creation of this infraction.'), - ), - ] diff --git a/pydis_site/apps/api/migrations/0027_merge_20190120_0852.py b/pydis_site/apps/api/migrations/0027_merge_20190120_0852.py deleted file mode 100644 index 6fab4fd0..00000000 --- a/pydis_site/apps/api/migrations/0027_merge_20190120_0852.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 2.1.5 on 2019-01-20 08:52 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0026_use_proper_default_for_infraction_insertion_date'), - ('api', '0021_merge_20181125_1015'), - ] - - operations = [ - ] diff --git a/pydis_site/apps/api/migrations/0028_allow_message_content_blank.py b/pydis_site/apps/api/migrations/0028_allow_message_content_blank.py deleted file mode 100644 index 6d57db27..00000000 --- a/pydis_site/apps/api/migrations/0028_allow_message_content_blank.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.1.5 on 2019-01-20 09:41 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0027_merge_20190120_0852'), - ] - - operations = [ - migrations.AlterField( - model_name='deletedmessage', - name='content', - field=models.CharField(blank=True, help_text='The content of this message, taken from Discord.', max_length=2000), - ), - ] diff --git a/pydis_site/apps/api/migrations/0029_add_infraction_type_watch.py b/pydis_site/apps/api/migrations/0029_add_infraction_type_watch.py deleted file mode 100644 index c6f88a11..00000000 --- a/pydis_site/apps/api/migrations/0029_add_infraction_type_watch.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.1.5 on 2019-01-20 11:52 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0028_allow_message_content_blank'), - ] - - operations = [ - migrations.AlterField( - model_name='infraction', - name='type', - field=models.CharField(choices=[('note', 'Note'), ('warning', 'Warning'), ('watch', 'Watch'), ('mute', 'Mute'), ('kick', 'Kick'), ('ban', 'Ban'), ('superstar', 'Superstar')], help_text='The type of the infraction.', max_length=9), - ), - ] diff --git a/pydis_site/apps/api/migrations/0030_reminder.py b/pydis_site/apps/api/migrations/0030_reminder.py deleted file mode 100644 index e1f1afc3..00000000 --- a/pydis_site/apps/api/migrations/0030_reminder.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 2.1.5 on 2019-01-22 22:17 - -import pydis_site.apps.api.models -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0029_add_infraction_type_watch'), - ] - - operations = [ - migrations.CreateModel( - name='Reminder', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('active', models.BooleanField(default=True, help_text='Whether this reminder is still active. If not, it has been sent out to the user.')), - ('channel_id', models.BigIntegerField(help_text='The channel ID that this message was sent in, taken from Discord.', validators=[django.core.validators.MinValueValidator(limit_value=0, message='Channel IDs cannot be negative.')])), - ('content', models.CharField(help_text='The content that the user wants to be reminded of.', max_length=1500)), - ('expiration', models.DateTimeField(help_text='When this reminder should be sent.')), - ('author', models.ForeignKey(help_text='The creator of this reminder.', on_delete=django.db.models.deletion.CASCADE, to='api.User')), - ], - bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), - ), - ] diff --git a/pydis_site/apps/api/migrations/0031_nomination.py b/pydis_site/apps/api/migrations/0031_nomination.py deleted file mode 100644 index f39436c1..00000000 --- a/pydis_site/apps/api/migrations/0031_nomination.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 2.1.5 on 2019-01-27 11:01 - -import pydis_site.apps.api.models -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0030_reminder'), - ] - - operations = [ - migrations.CreateModel( - name='Nomination', - fields=[ - ('active', models.BooleanField(default=True, help_text='Whether this nomination is still relevant.')), - ('reason', models.TextField(help_text='Why this user was nominated.')), - ('user', models.OneToOneField(help_text='The nominated user.', on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='nomination', serialize=False, to='api.User')), - ('inserted_at', models.DateTimeField(auto_now_add=True, help_text='The creation date of this nomination.')), - ('author', models.ForeignKey(help_text='The staff member that nominated this user.', on_delete=django.db.models.deletion.CASCADE, related_name='nomination_set', to='api.User')), - ], - bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), - ), - ] diff --git a/pydis_site/apps/api/migrations/0032_botsetting.py b/pydis_site/apps/api/migrations/0032_botsetting.py deleted file mode 100644 index 3304edef..00000000 --- a/pydis_site/apps/api/migrations/0032_botsetting.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 2.1.5 on 2019-02-07 19:03 - -import pydis_site.apps.api.models -import django.contrib.postgres.fields.jsonb -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0031_nomination'), - ] - - operations = [ - migrations.CreateModel( - name='BotSetting', - fields=[ - ('name', models.CharField(max_length=50, primary_key=True, serialize=False)), - ('data', django.contrib.postgres.fields.jsonb.JSONField(help_text='The actual settings of this setting.')), - ], - bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), - ), - ] diff --git a/pydis_site/apps/api/migrations/0033_create_defcon_settings.py b/pydis_site/apps/api/migrations/0033_create_defcon_settings.py deleted file mode 100644 index 830f3fb0..00000000 --- a/pydis_site/apps/api/migrations/0033_create_defcon_settings.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 2.1.5 on 2019-02-18 19:30 - -from django.db import migrations - - -def up(apps, schema_editor): - BotSetting = apps.get_model('api', 'BotSetting') - setting = BotSetting( - name='defcon', - data={ - 'enabled': False, - 'days': 0 - } - ).save() - - -def down(apps, schema_editor): # pragma: no cover - not necessary to test - BotSetting = apps.get_model('api', 'BotSetting') - BotSetting.get(name='defcon').delete() - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0032_botsetting'), - ] - - operations = [ - migrations.RunPython(up, down) - ] diff --git a/pydis_site/apps/api/migrations/0034_add_botsetting_name_validator.py b/pydis_site/apps/api/migrations/0034_add_botsetting_name_validator.py deleted file mode 100644 index d2a98e5d..00000000 --- a/pydis_site/apps/api/migrations/0034_add_botsetting_name_validator.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 2.1.5 on 2019-02-18 19:41 - -import pydis_site.apps.api.models.bot.bot_setting -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0033_create_defcon_settings'), - ] - - operations = [ - migrations.AlterField( - model_name='botsetting', - name='name', - field=models.CharField(max_length=50, primary_key=True, serialize=False, validators=[ - pydis_site.apps.api.models.bot.bot_setting.validate_bot_setting_name]), - ), - ] diff --git a/pydis_site/apps/api/migrations/0035_create_table_log_entry.py b/pydis_site/apps/api/migrations/0035_create_table_log_entry.py deleted file mode 100644 index c9a1ad19..00000000 --- a/pydis_site/apps/api/migrations/0035_create_table_log_entry.py +++ /dev/null @@ -1,29 +0,0 @@ -# Generated by Django 2.1.5 on 2019-04-08 18:27 - -from django.db import migrations, models -import django.utils.timezone -import pydis_site.apps.api.models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0034_add_botsetting_name_validator'), - ] - - operations = [ - migrations.CreateModel( - name='LogEntry', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('application', models.CharField(choices=[('bot', 'Bot'), ('seasonalbot', 'Seasonalbot'), ('site', 'Website')], help_text='The application that generated this log entry.', max_length=20)), - ('logger_name', models.CharField(help_text='The name of the logger that generated this log entry.', max_length=100)), - ('timestamp', models.DateTimeField(default=django.utils.timezone.now, help_text='The date and time when this entry was created.')), - ('level', models.CharField(choices=[('debug', 'Debug'), ('info', 'Info'), ('warning', 'Warning'), ('error', 'Error'), ('critical', 'Critical')], help_text='The logger level at which this entry was emitted. The levels correspond to the Python `logging` levels.', max_length=8)), - ('module', models.CharField(help_text='The fully qualified path of the module generating this log line.', max_length=100)), - ('line', models.PositiveSmallIntegerField(help_text='The line at which the log line was emitted.')), - ('message', models.TextField(help_text='The textual content of the log line.')), - ], - bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), - ), - ] diff --git a/pydis_site/apps/api/migrations/0036_alter_nominations_api.py b/pydis_site/apps/api/migrations/0036_alter_nominations_api.py deleted file mode 100644 index f31be14c..00000000 --- a/pydis_site/apps/api/migrations/0036_alter_nominations_api.py +++ /dev/null @@ -1,42 +0,0 @@ -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0035_create_table_log_entry'), - ] - - operations = [ - migrations.AlterField( - model_name='nomination', - name='user', - field=models.ForeignKey(help_text='The nominated user.', on_delete=django.db.models.deletion.CASCADE, related_name='nomination', to='api.User'), - ), - migrations.AlterField( - model_name='nomination', - name='author', - field=models.ForeignKey(help_text='The staff member that nominated this user.', on_delete=django.db.models.deletion.CASCADE, related_name='nomination_set', to='api.User'), - ), - migrations.RenameField( - model_name='nomination', - old_name='author', - new_name='actor', - ), - migrations.AddField( - model_name='nomination', - name='id', - field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), - ), - migrations.AddField( - model_name='nomination', - name='unnominate_reason', - field=models.TextField(default='', help_text='Why the nomination was ended.'), - ), - migrations.AddField( - model_name='nomination', - name='unwatched_at', - field=models.DateTimeField(help_text='When the nomination was ended.', null=True), - ), - ] diff --git a/pydis_site/apps/api/migrations/0036_removing_snake_apis.py b/pydis_site/apps/api/migrations/0036_removing_snake_apis.py deleted file mode 100644 index 624afc95..00000000 --- a/pydis_site/apps/api/migrations/0036_removing_snake_apis.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 2.2.3 on 2019-07-10 09:07 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0035_create_table_log_entry'), - ] - - operations = [ - migrations.DeleteModel( - name='SnakeFact', - ), - migrations.DeleteModel( - name='SnakeIdiom', - ), - migrations.DeleteModel( - name='SnakeName', - ), - migrations.DeleteModel( - name='SpecialSnake', - ), - ] diff --git a/pydis_site/apps/api/migrations/0037_nomination_field_name_change.py b/pydis_site/apps/api/migrations/0037_nomination_field_name_change.py deleted file mode 100644 index c5f2d0c5..00000000 --- a/pydis_site/apps/api/migrations/0037_nomination_field_name_change.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 2.2 on 2019-06-28 18:09 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0036_alter_nominations_api'), - ] - - operations = [ - migrations.RenameField( - model_name='nomination', - old_name='unnominate_reason', - new_name='end_reason', - ), - migrations.RenameField( - model_name='nomination', - old_name='unwatched_at', - new_name='ended_at', - ), - ] diff --git a/pydis_site/apps/api/migrations/0038_merge_20190719_1817.py b/pydis_site/apps/api/migrations/0038_merge_20190719_1817.py deleted file mode 100644 index 532bcb70..00000000 --- a/pydis_site/apps/api/migrations/0038_merge_20190719_1817.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 2.2.3 on 2019-07-19 22:17 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0037_nomination_field_name_change'), - ('api', '0036_removing_snake_apis'), - ] - - operations = [ - ] diff --git a/pydis_site/apps/api/migrations/0039_add_position_field_to_role.py b/pydis_site/apps/api/migrations/0039_add_position_field_to_role.py deleted file mode 100644 index b6b27ff2..00000000 --- a/pydis_site/apps/api/migrations/0039_add_position_field_to_role.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 2.2.3 on 2019-08-15 11:42 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0038_merge_20190719_1817'), - ] - - operations = [ - migrations.AddField( - model_name='role', - name='position', - field=models.IntegerField(default=-1, help_text='The position of the role in the role hierarchy of the Discord Guild.'), - ), - migrations.AlterField( - model_name='role', - name='position', - field=models.IntegerField(help_text='The position of the role in the role hierarchy of the Discord Guild.'), - ) - ] diff --git a/pydis_site/apps/api/migrations/0040_allow_special_off_topic_name_chars.py b/pydis_site/apps/api/migrations/0040_allow_special_off_topic_name_chars.py deleted file mode 100644 index df88b76e..00000000 --- a/pydis_site/apps/api/migrations/0040_allow_special_off_topic_name_chars.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.2.3 on 2019-09-03 14:24 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0039_add_position_field_to_role'), - ] - - operations = [ - migrations.AlterField( - model_name='offtopicchannelname', - name='name', - field=models.CharField(help_text='The actual channel name that will be used on our Discord server.', max_length=96, primary_key=True, serialize=False, validators=[django.core.validators.RegexValidator(regex="^[a-z0-9\\U0001d5a0-\\U0001d5b9-ǃ?’']+$")]), - ), - ] diff --git a/pydis_site/apps/api/migrations/0041_add_default_ordering_deleted_messages.py b/pydis_site/apps/api/migrations/0041_add_default_ordering_deleted_messages.py deleted file mode 100644 index a603bf4f..00000000 --- a/pydis_site/apps/api/migrations/0041_add_default_ordering_deleted_messages.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.2.3 on 2019-09-12 16:13 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0040_allow_special_off_topic_name_chars'), - ] - - operations = [ - migrations.AlterModelOptions( - name='deletedmessage', - options={'ordering': ['id']}, - ), - ] diff --git a/pydis_site/apps/api/migrations/0042_infraction_add_default_ordering.py b/pydis_site/apps/api/migrations/0042_infraction_add_default_ordering.py deleted file mode 100644 index 1a0dbb34..00000000 --- a/pydis_site/apps/api/migrations/0042_infraction_add_default_ordering.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.2.3 on 2019-09-17 14:07 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0041_add_default_ordering_deleted_messages'), - ] - - operations = [ - migrations.AlterModelOptions( - name='infraction', - options={'ordering': ['-inserted_at']}, - ), - ] diff --git a/pydis_site/apps/api/migrations/0043_infraction_hidden_warnings_to_notes.py b/pydis_site/apps/api/migrations/0043_infraction_hidden_warnings_to_notes.py deleted file mode 100644 index 7c751f5d..00000000 --- a/pydis_site/apps/api/migrations/0043_infraction_hidden_warnings_to_notes.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 2.2.5 on 2019-09-25 08:41 - -from django.db import migrations - - -def migrate_hidden_warnings_to_notes(apps, schema_editor): - """Migrates hidden warnings to note.""" - Infraction = apps.get_model('api', 'Infraction') - - for infraction in Infraction.objects.filter(type="warning", hidden=True): - infraction.type = "note" - infraction.save() - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0042_infraction_add_default_ordering'), - ] - - operations = [ - migrations.RunPython(migrate_hidden_warnings_to_notes), - ] diff --git a/pydis_site/apps/api/migrations/0044_migrate_nominations_from_infraction_to_nomination_model.py b/pydis_site/apps/api/migrations/0044_migrate_nominations_from_infraction_to_nomination_model.py deleted file mode 100644 index a56450c0..00000000 --- a/pydis_site/apps/api/migrations/0044_migrate_nominations_from_infraction_to_nomination_model.py +++ /dev/null @@ -1,64 +0,0 @@ -# Generated by Django 2.2.5 on 2019-09-30 12:15 -import logging - -from django.db import migrations -from django.db.models import Q - -log = logging.getLogger('nomination_migration') - - -def migrate_nominations_to_new_model(apps, schema_editor): - """ - Migrations nominations from the infraction model to the nomination model. - - This migration works by replaying the nomination history in chronological order, adding and - ending nominations as we've recorded them. - """ - Infraction = apps.get_model('api', 'Infraction') - Nomination = apps.get_model('api', 'Nomination') - - all_nominations = ( - Q(reason__startswith="Helper nomination:") | Q(reason__startswith="Unwatched (talent-pool):") - ) - - for infraction in Infraction.objects.filter(all_nominations).order_by('inserted_at'): - if infraction.reason.startswith("Helper nomination:"): - if Nomination.objects.filter(user=infraction.user, active=True).exists(): - log.error( - f"User `{infraction.user.id}` already has an active nomination, aborting." - ) - continue - nomination = Nomination( - user=infraction.user, - inserted_at=infraction.inserted_at, - reason=infraction.reason[19:], # Strip "Helper nomination: " prefix - actor=infraction.actor, - active=True, - ) - nomination.save() - infraction.delete() - elif infraction.reason.startswith("Unwatched (talent-pool):"): - if not Nomination.objects.filter(user=infraction.user, active=True).exists(): - log.error( - f"User `{infraction.user.id}` has no active nomination, can't end it!" - ) - continue - nomination = Nomination.objects.get(user=infraction.user, active=True) - nomination.end_reason = infraction.reason[25:] # Strip "Unwatched (talent-pool):" - nomination.ended_at = infraction.inserted_at - nomination.active = False - nomination.save() - infraction.delete() - else: - log.error(f"I don't understand this infraction: {infraction}") - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0043_infraction_hidden_warnings_to_notes'), - ] - - operations = [ - migrations.RunPython(migrate_nominations_to_new_model), - ] diff --git a/pydis_site/apps/api/migrations/0045_add_plural_name_for_log_entry.py b/pydis_site/apps/api/migrations/0045_add_plural_name_for_log_entry.py deleted file mode 100644 index 6b9933d8..00000000 --- a/pydis_site/apps/api/migrations/0045_add_plural_name_for_log_entry.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.2.3 on 2019-10-11 17:48 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0044_migrate_nominations_from_infraction_to_nomination_model'), - ] - - operations = [ - migrations.AlterModelOptions( - name='logentry', - options={'verbose_name_plural': 'Log entries'}, - ), - ] diff --git a/pydis_site/apps/api/migrations/0046_reminder_jump_url.py b/pydis_site/apps/api/migrations/0046_reminder_jump_url.py deleted file mode 100644 index b145f0dd..00000000 --- a/pydis_site/apps/api/migrations/0046_reminder_jump_url.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.2.6 on 2019-10-21 14:46 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0045_add_plural_name_for_log_entry'), - ] - - operations = [ - migrations.AddField( - model_name='reminder', - name='jump_url', - field=models.URLField(default='', help_text='The jump url to the message that created the reminder', max_length=88), - preserve_default=False, - ), - ] diff --git a/pydis_site/apps/api/migrations/0047_active_infractions_migration.py b/pydis_site/apps/api/migrations/0047_active_infractions_migration.py deleted file mode 100644 index 9ac791dc..00000000 --- a/pydis_site/apps/api/migrations/0047_active_infractions_migration.py +++ /dev/null @@ -1,105 +0,0 @@ -# Generated by Django 2.2.6 on 2019-10-07 15:59 - -from django.db import migrations -from django.db.models import Count, Prefetch, QuerySet - - -class ExpirationWrapper: - """Wraps an expiration date to properly compare permanent and temporary infractions.""" - - def __init__(self, infraction): - self.expiration_date = infraction.expires_at - - def __lt__(self, other): - """An `expiration_date` is considered smaller when it comes earlier than the `other`.""" - if self.expiration_date is None: - # A permanent infraction can never end sooner than another infraction - return False - elif other.expiration_date is None: - # If `self` is temporary, but `other` is permanent, `self` is smaller - return True - else: - return self.expiration_date < other.expiration_date - - def __eq__(self, other): - """If both expiration dates are permanent they're equal, otherwise compare dates.""" - if self.expiration_date is None and other.expiration_date is None: - return True - elif self.expiration_date is None or other.expiration_date is None: - return False - else: - return self.expiration_date == other.expiration_date - - -def migrate_inactive_types_to_inactive(apps, schema_editor): - """Migrates infractions of non-active types to inactive.""" - infraction_model = apps.get_model('api', 'Infraction') - infraction_model.objects.filter(type__in=('note', 'warning', 'kick')).update(active=False) - - -def get_query(user_model, infraction_model, infr_type: str) -> QuerySet: - """ - Creates QuerySet to fetch users with multiple active infractions of the given `type`. - - The QuerySet will prefetch the infractions and attach them as an `.infractions` attribute to the - `User` instances. - """ - active_infractions = infraction_model.objects.filter(type=infr_type, active=True) - - # Build an SQL query by chaining methods together - - # Get users with active infraction(s) of the provided `infr_type` - query = user_model.objects.filter( - infractions_received__type=infr_type, infractions_received__active=True - ) - - # Prefetch their active received infractions of `infr_type` and attach `.infractions` attribute - query = query.prefetch_related( - Prefetch('infractions_received', queryset=active_infractions, to_attr='infractions') - ) - - # Count and only include them if they have at least 2 active infractions of the `type` - query = query.annotate(num_infractions=Count('infractions_received')) - query = query.filter(num_infractions__gte=2) - - # Make sure we return each individual only once - query = query.distinct() - - return query - - -def migrate_multiple_active_infractions_per_user_to_one(apps, schema_editor): - """ - Make sure a user only has one active infraction of a given "active" infraction type. - - If a user has multiple active infraction, we keep the one with longest expiration date active - and migrate the others to inactive. - """ - infraction_model = apps.get_model('api', 'Infraction') - user_model = apps.get_model('api', 'User') - - for infraction_type in ('ban', 'mute', 'superstar', 'watch'): - query = get_query(user_model, infraction_model, infraction_type) - for user in query: - infractions = sorted(user.infractions, key=ExpirationWrapper, reverse=True) - for infraction in infractions[1:]: - infraction.active = False - infraction.save() - - -def reverse_migration(apps, schema_editor): - """There's no need to do anything special to reverse these migrations.""" - return - - -class Migration(migrations.Migration): - """Data migration to get the database consistent with the new infraction validation rules.""" - - dependencies = [ - ('api', '0046_reminder_jump_url'), - ] - - operations = [ - migrations.RunPython(migrate_inactive_types_to_inactive, reverse_migration), - migrations.RunPython(migrate_multiple_active_infractions_per_user_to_one, reverse_migration) - ] diff --git a/pydis_site/apps/api/migrations/0048_add_infractions_unique_constraints_active.py b/pydis_site/apps/api/migrations/0048_add_infractions_unique_constraints_active.py deleted file mode 100644 index 4ea1fb90..00000000 --- a/pydis_site/apps/api/migrations/0048_add_infractions_unique_constraints_active.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.2.6 on 2019-10-07 18:27 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0047_active_infractions_migration'), - ] - - operations = [ - migrations.AddConstraint( - model_name='infraction', - constraint=models.UniqueConstraint(condition=models.Q(active=True), fields=('user', 'type'), name='unique_active_infraction_per_type_per_user'), - ), - ] diff --git a/pydis_site/apps/api/migrations/0049_deletedmessage_attachments.py b/pydis_site/apps/api/migrations/0049_deletedmessage_attachments.py deleted file mode 100644 index 31ac239a..00000000 --- a/pydis_site/apps/api/migrations/0049_deletedmessage_attachments.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 2.2.6 on 2019-10-28 17:12 - -import django.contrib.postgres.fields -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0049_offensivemessage'), - ] - - operations = [ - migrations.AddField( - model_name='deletedmessage', - name='attachments', - field=django.contrib.postgres.fields.ArrayField(base_field=models.URLField(max_length=512), default=[], blank=True, help_text='Attachments attached to this message.', size=None), - preserve_default=False, - ), - ] diff --git a/pydis_site/apps/api/migrations/0049_offensivemessage.py b/pydis_site/apps/api/migrations/0049_offensivemessage.py deleted file mode 100644 index f342cec3..00000000 --- a/pydis_site/apps/api/migrations/0049_offensivemessage.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 2.2.6 on 2019-11-07 18:08 - -import django.core.validators -from django.db import migrations, models -import pydis_site.apps.api.models.bot.offensive_message -import pydis_site.apps.api.models.mixins - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0048_add_infractions_unique_constraints_active'), - ] - - operations = [ - migrations.CreateModel( - name='OffensiveMessage', - fields=[ - ('id', models.BigIntegerField(help_text='The message ID as taken from Discord.', primary_key=True, serialize=False, validators=[django.core.validators.MinValueValidator(limit_value=0, message='Message IDs cannot be negative.')])), - ('channel_id', models.BigIntegerField(help_text='The channel ID that the message was sent in, taken from Discord.', validators=[django.core.validators.MinValueValidator(limit_value=0, message='Channel IDs cannot be negative.')])), - ('delete_date', models.DateTimeField(help_text='The date on which the message will be auto-deleted.', validators=[pydis_site.apps.api.models.bot.offensive_message.future_date_validator])), - ], - bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), - ), - ] diff --git a/pydis_site/apps/api/migrations/0050_remove_infractions_active_default_value.py b/pydis_site/apps/api/migrations/0050_remove_infractions_active_default_value.py deleted file mode 100644 index 90c91d63..00000000 --- a/pydis_site/apps/api/migrations/0050_remove_infractions_active_default_value.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.6 on 2020-02-08 19:00 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0049_deletedmessage_attachments'), - ] - - operations = [ - migrations.AlterField( - model_name='infraction', - name='active', - field=models.BooleanField(help_text='Whether the infraction is still active.'), - ), - ] diff --git a/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py b/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py deleted file mode 100644 index 622f21d1..00000000 --- a/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 3.0.4 on 2020-03-21 17:05 - -import django.contrib.postgres.fields -import django.contrib.postgres.fields.jsonb -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0050_remove_infractions_active_default_value'), - ] - - operations = [ - migrations.AlterField( - model_name='deletedmessage', - name='embeds', - field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(validators=[]), blank=True, help_text='Embeds attached to this message.', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0051_create_news_setting.py b/pydis_site/apps/api/migrations/0051_create_news_setting.py deleted file mode 100644 index f18fdfb1..00000000 --- a/pydis_site/apps/api/migrations/0051_create_news_setting.py +++ /dev/null @@ -1,25 +0,0 @@ -from django.db import migrations - - -def up(apps, schema_editor): - BotSetting = apps.get_model('api', 'BotSetting') - setting = BotSetting( - name='news', - data={} - ).save() - - -def down(apps, schema_editor): - BotSetting = apps.get_model('api', 'BotSetting') - BotSetting.objects.get(name='news').delete() - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0050_remove_infractions_active_default_value'), - ] - - operations = [ - migrations.RunPython(up, down) - ] diff --git a/pydis_site/apps/api/migrations/0051_delete_tag.py b/pydis_site/apps/api/migrations/0051_delete_tag.py deleted file mode 100644 index bada5788..00000000 --- a/pydis_site/apps/api/migrations/0051_delete_tag.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 2.2.11 on 2020-04-01 06:15 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0050_remove_infractions_active_default_value'), - ] - - operations = [ - migrations.DeleteModel( - name='Tag', - ), - ] diff --git a/pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py b/pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py deleted file mode 100644 index dfdf3835..00000000 --- a/pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.11 on 2020-03-30 10:24 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0051_create_news_setting'), - ] - - operations = [ - migrations.AddField( - model_name='offtopicchannelname', - name='used', - field=models.BooleanField(default=False, help_text='Whether or not this name has already been used during this rotation'), - ), - ] diff --git a/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py b/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py deleted file mode 100644 index 26b3b954..00000000 --- a/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.2.11 on 2020-05-27 07:17 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0051_create_news_setting'), - ] - - operations = [ - migrations.RemoveField( - model_name='user', - name='avatar_hash', - ), - ] diff --git a/pydis_site/apps/api/migrations/0053_user_roles_to_array.py b/pydis_site/apps/api/migrations/0053_user_roles_to_array.py deleted file mode 100644 index 7ff3a548..00000000 --- a/pydis_site/apps/api/migrations/0053_user_roles_to_array.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 2.2.11 on 2020-06-02 13:42 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0052_remove_user_avatar_hash'), - ] - - operations = [ - migrations.RemoveField( - model_name='user', - name='roles', - ), - migrations.AddField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')]), default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py b/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py deleted file mode 100644 index 96230015..00000000 --- a/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 2.2.11 on 2020-06-02 20:08 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models -import pydis_site.apps.api.models.bot.user - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0053_user_roles_to_array'), - ] - - operations = [ - migrations.AlterField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py b/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py deleted file mode 100644 index f2a0e638..00000000 --- a/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-14 20:27 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0051_allow_blank_message_embeds'), - ('api', '0054_user_invalidate_unknown_role'), - ] - - operations = [ - ] diff --git a/pydis_site/apps/api/migrations/0055_reminder_mentions.py b/pydis_site/apps/api/migrations/0055_reminder_mentions.py deleted file mode 100644 index d73b450d..00000000 --- a/pydis_site/apps/api/migrations/0055_reminder_mentions.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 2.2.14 on 2020-07-15 07:37 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0054_user_invalidate_unknown_role'), - ] - - operations = [ - migrations.AddField( - model_name='reminder', - name='mentions', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Mention IDs cannot be negative.')]), blank=True, default=list, help_text='IDs of roles or users to ping with the reminder.', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py b/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py deleted file mode 100644 index 489941c7..00000000 --- a/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-14 20:35 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models -import pydis_site.apps.api.models.bot.user - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0055_merge_20200714_2027'), - ] - - operations = [ - migrations.AlterField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), blank=True, default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py b/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py deleted file mode 100644 index 47a6d2d4..00000000 --- a/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 2.2.14 on 2020-07-16 07:51 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0055_reminder_mentions'), - ('api', '0056_allow_blank_user_roles'), - ] - - operations = [ - ] diff --git a/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py b/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py deleted file mode 100644 index aecfdad7..00000000 --- a/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-15 11:23 - -from django.db import migrations, models -import pydis_site.apps.api.models.mixins - - -class Migration(migrations.Migration): - dependencies = [ - ('api', '0057_merge_20200716_0751'), - ] - - operations = [ - migrations.CreateModel( - name='FilterList', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('type', models.CharField( - choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'), - ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token')], - help_text='The type of allowlist this is on.', max_length=50)), - ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), - ('content', models.TextField(help_text='The data to add to the allow or denylist.')), - ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)), - ], - bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), - ), - migrations.AddConstraint( - model_name='filterlist', - constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_filter_list') - ) - ] diff --git a/pydis_site/apps/api/migrations/0059_populate_filterlists.py b/pydis_site/apps/api/migrations/0059_populate_filterlists.py deleted file mode 100644 index 273db3d1..00000000 --- a/pydis_site/apps/api/migrations/0059_populate_filterlists.py +++ /dev/null @@ -1,153 +0,0 @@ -from django.db import migrations - -guild_invite_whitelist = [ - ("discord.gg/python", "Python Discord", True), - ("discord.gg/4JJdJKb", "RLBot", True), - ("discord.gg/djPtTRJ", "Kivy", True), - ("discord.gg/QXyegWe", "Pyglet", True), - ("discord.gg/9XsucTT", "Panda3D", True), - ("discord.gg/AP3rq2k", "PyWeek", True), - ("discord.gg/vSPsP9t", "Microsoft Python", True), - ("discord.gg/bRCvFy9", "Discord.js Official", True), - ("discord.gg/9zT7NHP", "Programming Discussions", True), - ("discord.gg/ysd6M4r", "JetBrains Community", True), - ("discord.gg/4xJeCgy", "Raspberry Pie", True), - ("discord.gg/AStb3kZ", "Ren'Py", True), - ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), - ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), - ("discord.gg/jTtgWuy", "Django", True), - ("discord.gg/W9BypZF", "STEM", True), - ("discord.gg/dpy", "discord.py", True), - ("discord.gg/programming", "Programmers Hangout", True), - ("discord.gg/qhGUjGD", "SpeakJS", True), - ("discord.gg/eTbWSZj", "Functional Programming", True), - ("discord.gg/r8yreB6", "PyGame", True), - ("discord.gg/5UBnR3P", "Python Atlanta", True), - ("discord.gg/ccyrDKv", "C#", True), -] - -domain_name_blacklist = [ - ("pornhub.com", None, False), - ("liveleak.com", None, False), - ("grabify.link", None, False), - ("bmwforum.co", None, False), - ("leancoding.co", None, False), - ("spottyfly.com", None, False), - ("stopify.co", None, False), - ("yoütu.be", None, False), - ("discörd.com", None, False), - ("minecräft.com", None, False), - ("freegiftcards.co", None, False), - ("disçordapp.com", None, False), - ("fortnight.space", None, False), - ("fortnitechat.site", None, False), - ("joinmy.site", None, False), - ("curiouscat.club", None, False), - ("catsnthings.fun", None, False), - ("yourtube.site", None, False), - ("youtubeshort.watch", None, False), - ("catsnthing.com", None, False), - ("youtubeshort.pro", None, False), - ("canadianlumberjacks.online", None, False), - ("poweredbydialup.club", None, False), - ("poweredbydialup.online", None, False), - ("poweredbysecurity.org", None, False), - ("poweredbysecurity.online", None, False), - ("ssteam.site", None, False), - ("steamwalletgift.com", None, False), - ("discord.gift", None, False), - ("lmgtfy.com", None, False), -] - -filter_token_blacklist = [ - (r"\bgoo+ks*\b", None, False), - (r"\bky+s+\b", None, False), - (r"\bki+ke+s*\b", None, False), - (r"\bbeaner+s?\b", None, False), - (r"\bcoo+ns*\b", None, False), - (r"\bnig+lets*\b", None, False), - (r"\bslant-eyes*\b", None, False), - (r"\btowe?l-?head+s*\b", None, False), - (r"\bchi*n+k+s*\b", None, False), - (r"\bspick*s*\b", None, False), - (r"\bkill* +(?:yo)?urself+\b", None, False), - (r"\bjew+s*\b", None, False), - (r"\bsuicide\b", None, False), - (r"\brape\b", None, False), - (r"\b(re+)tar+(d+|t+)(ed)?\b", None, False), - (r"\bta+r+d+\b", None, False), - (r"\bcunts*\b", None, False), - (r"\btrann*y\b", None, False), - (r"\bshemale\b", None, False), - (r"fa+g+s*", None, False), - (r"卐", None, False), - (r"卍", None, False), - (r"࿖", None, False), - (r"࿕", None, False), - (r"࿘", None, False), - (r"࿗", None, False), - (r"cuck(?!oo+)", None, False), - (r"nigg+(?:e*r+|a+h*?|u+h+)s?", None, False), - (r"fag+o+t+s*", None, False), -] - -file_format_whitelist = [ - (".3gp", None, True), - (".3g2", None, True), - (".avi", None, True), - (".bmp", None, True), - (".gif", None, True), - (".h264", None, True), - (".jpg", None, True), - (".jpeg", None, True), - (".m4v", None, True), - (".mkv", None, True), - (".mov", None, True), - (".mp4", None, True), - (".mpeg", None, True), - (".mpg", None, True), - (".png", None, True), - (".tiff", None, True), - (".wmv", None, True), - (".svg", None, True), - (".psd", "Photoshop", True), - (".ai", "Illustrator", True), - (".aep", "After Effects", True), - (".xcf", "GIMP", True), - (".mp3", None, True), - (".wav", None, True), - (".ogg", None, True), - (".webm", None, True), - (".webp", None, True), -] - -populate_data = { - "FILTER_TOKEN": filter_token_blacklist, - "DOMAIN_NAME": domain_name_blacklist, - "FILE_FORMAT": file_format_whitelist, - "GUILD_INVITE": guild_invite_whitelist, -} - - -class Migration(migrations.Migration): - dependencies = [("api", "0058_create_new_filterlist_model")] - - def populate_filterlists(app, _): - FilterList = app.get_model("api", "FilterList") - - for filterlist_type, metadata in populate_data.items(): - for content, comment, allowed in metadata: - FilterList.objects.create( - type=filterlist_type, - allowed=allowed, - content=content, - comment=comment, - ) - - def clear_filterlists(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.all().delete() - - operations = [ - migrations.RunPython(populate_filterlists, clear_filterlists) - ] diff --git a/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py b/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py deleted file mode 100644 index 53846f02..00000000 --- a/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py +++ /dev/null @@ -1,85 +0,0 @@ -from django.db import migrations - -bad_guild_invite_whitelist = [ - ("discord.gg/python", "Python Discord", True), - ("discord.gg/4JJdJKb", "RLBot", True), - ("discord.gg/djPtTRJ", "Kivy", True), - ("discord.gg/QXyegWe", "Pyglet", True), - ("discord.gg/9XsucTT", "Panda3D", True), - ("discord.gg/AP3rq2k", "PyWeek", True), - ("discord.gg/vSPsP9t", "Microsoft Python", True), - ("discord.gg/bRCvFy9", "Discord.js Official", True), - ("discord.gg/9zT7NHP", "Programming Discussions", True), - ("discord.gg/ysd6M4r", "JetBrains Community", True), - ("discord.gg/4xJeCgy", "Raspberry Pie", True), - ("discord.gg/AStb3kZ", "Ren'Py", True), - ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), - ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), - ("discord.gg/jTtgWuy", "Django", True), - ("discord.gg/W9BypZF", "STEM", True), - ("discord.gg/dpy", "discord.py", True), - ("discord.gg/programming", "Programmers Hangout", True), - ("discord.gg/qhGUjGD", "SpeakJS", True), - ("discord.gg/eTbWSZj", "Functional Programming", True), - ("discord.gg/r8yreB6", "PyGame", True), - ("discord.gg/5UBnR3P", "Python Atlanta", True), - ("discord.gg/ccyrDKv", "C#", True), -] - -guild_invite_whitelist = [ - ("267624335836053506", "Python Discord", True), - ("348658686962696195", "RLBot", True), - ("423249981340778496", "Kivy", True), - ("438622377094414346", "Pyglet", True), - ("524691714909274162", "Panda3D", True), - ("666560367173828639", "PyWeek", True), - ("702724176489873509", "Microsoft Python", True), - ("222078108977594368", "Discord.js Official", True), - ("238666723824238602", "Programming Discussions", True), - ("433980600391696384", "JetBrains Community", True), - ("204621105720328193", "Raspberry Pie", True), - ("286633898581164032", "Ren'Py", True), - ("440186186024222721", "Python Discord: Emojis 1", True), - ("578587418123304970", "Python Discord: Emojis 2", True), - ("159039020565790721", "Django", True), - ("273944235143593984", "STEM", True), - ("336642139381301249", "discord.py", True), - ("244230771232079873", "Programmers Hangout", True), - ("239433591950540801", "SpeakJS", True), - ("280033776820813825", "Functional Programming", True), - ("349505959032389632", "PyGame", True), - ("488751051629920277", "Python Atlanta", True), - ("143867839282020352", "C#", True), -] - - -class Migration(migrations.Migration): - dependencies = [("api", "0059_populate_filterlists")] - - def fix_filterlist(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.filter(type="GUILD_INVITE").delete() # Clear out the stuff added in 0059. - - for content, comment, allowed in guild_invite_whitelist: - FilterList.objects.create( - type="GUILD_INVITE", - allowed=allowed, - content=content, - comment=comment, - ) - - def restore_bad_filterlist(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.filter(type="GUILD_INVITE").delete() - - for content, comment, allowed in bad_guild_invite_whitelist: - FilterList.objects.create( - type="GUILD_INVITE", - allowed=allowed, - content=content, - comment=comment, - ) - - operations = [ - migrations.RunPython(fix_filterlist, restore_bad_filterlist) - ] diff --git a/pydis_site/apps/api/migrations/0061_merge_20200830_0526.py b/pydis_site/apps/api/migrations/0061_merge_20200830_0526.py deleted file mode 100644 index f0668696..00000000 --- a/pydis_site/apps/api/migrations/0061_merge_20200830_0526.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 3.0.8 on 2020-08-30 05:26 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0060_populate_filterlists_fix'), - ('api', '0052_offtopicchannelname_used'), - ] - - operations = [ - ] diff --git a/pydis_site/apps/api/migrations/0062_merge_20200901_1459.py b/pydis_site/apps/api/migrations/0062_merge_20200901_1459.py deleted file mode 100644 index d162acf1..00000000 --- a/pydis_site/apps/api/migrations/0062_merge_20200901_1459.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 3.0.8 on 2020-09-01 14:59 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0051_delete_tag'), - ('api', '0061_merge_20200830_0526'), - ] - - operations = [ - ] diff --git a/pydis_site/apps/api/migrations/0063_Allow_blank_or_null_for_nomination_reason.py b/pydis_site/apps/api/migrations/0063_Allow_blank_or_null_for_nomination_reason.py deleted file mode 100644 index 9eb05eaa..00000000 --- a/pydis_site/apps/api/migrations/0063_Allow_blank_or_null_for_nomination_reason.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.0.9 on 2020-09-11 21:06 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0062_merge_20200901_1459'), - ] - - operations = [ - migrations.AlterField( - model_name='nomination', - name='reason', - field=models.TextField(blank=True, help_text='Why this user was nominated.', null=True), - ), - ] diff --git a/pydis_site/apps/api/migrations/0064_auto_20200919_1900.py b/pydis_site/apps/api/migrations/0064_auto_20200919_1900.py deleted file mode 100644 index 0080eb42..00000000 --- a/pydis_site/apps/api/migrations/0064_auto_20200919_1900.py +++ /dev/null @@ -1,76 +0,0 @@ -# Generated by Django 3.0.9 on 2020-09-19 19:00 - -import django.core.validators -from django.db import migrations, models -import pydis_site.apps.api.models.bot.offensive_message - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0063_Allow_blank_or_null_for_nomination_reason'), - ] - - operations = [ - migrations.AlterModelOptions( - name='deletedmessage', - options={'ordering': ('-id',)}, - ), - migrations.AlterModelOptions( - name='messagedeletioncontext', - options={'ordering': ('-creation',)}, - ), - migrations.AlterModelOptions( - name='nomination', - options={'ordering': ('-inserted_at',)}, - ), - migrations.AlterModelOptions( - name='role', - options={'ordering': ('-position',)}, - ), - migrations.AlterField( - model_name='deletedmessage', - name='channel_id', - field=models.BigIntegerField(help_text='The channel ID that this message was sent in, taken from Discord.', validators=[django.core.validators.MinValueValidator(limit_value=0, message='Channel IDs cannot be negative.')], verbose_name='Channel ID'), - ), - migrations.AlterField( - model_name='deletedmessage', - name='id', - field=models.BigIntegerField(help_text='The message ID as taken from Discord.', primary_key=True, serialize=False, validators=[django.core.validators.MinValueValidator(limit_value=0, message='Message IDs cannot be negative.')], verbose_name='ID'), - ), - migrations.AlterField( - model_name='nomination', - name='end_reason', - field=models.TextField(blank=True, default='', help_text='Why the nomination was ended.'), - ), - migrations.AlterField( - model_name='offensivemessage', - name='channel_id', - field=models.BigIntegerField(help_text='The channel ID that the message was sent in, taken from Discord.', validators=[django.core.validators.MinValueValidator(limit_value=0, message='Channel IDs cannot be negative.')], verbose_name='Channel ID'), - ), - migrations.AlterField( - model_name='offensivemessage', - name='delete_date', - field=models.DateTimeField(help_text='The date on which the message will be auto-deleted.', validators=[pydis_site.apps.api.models.bot.offensive_message.future_date_validator], verbose_name='To Be Deleted'), - ), - migrations.AlterField( - model_name='offensivemessage', - name='id', - field=models.BigIntegerField(help_text='The message ID as taken from Discord.', primary_key=True, serialize=False, validators=[django.core.validators.MinValueValidator(limit_value=0, message='Message IDs cannot be negative.')], verbose_name='Message ID'), - ), - migrations.AlterField( - model_name='role', - name='id', - field=models.BigIntegerField(help_text='The role ID, taken from Discord.', primary_key=True, serialize=False, validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')], verbose_name='ID'), - ), - migrations.AlterField( - model_name='user', - name='id', - field=models.BigIntegerField(help_text='The ID of this user, taken from Discord.', primary_key=True, serialize=False, validators=[django.core.validators.MinValueValidator(limit_value=0, message='User IDs cannot be negative.')], verbose_name='ID'), - ), - migrations.AlterField( - model_name='user', - name='in_guild', - field=models.BooleanField(default=True, help_text='Whether this user is in our server.', verbose_name='In Guild'), - ), - ] diff --git a/pydis_site/apps/api/migrations/0064_delete_logentry.py b/pydis_site/apps/api/migrations/0064_delete_logentry.py deleted file mode 100644 index a5f344d1..00000000 --- a/pydis_site/apps/api/migrations/0064_delete_logentry.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 3.0.9 on 2020-10-03 06:57 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0063_Allow_blank_or_null_for_nomination_reason'), - ] - - operations = [ - migrations.DeleteModel( - name='LogEntry', - ), - ] diff --git a/pydis_site/apps/api/migrations/0065_auto_20200919_2033.py b/pydis_site/apps/api/migrations/0065_auto_20200919_2033.py deleted file mode 100644 index 89bc4e02..00000000 --- a/pydis_site/apps/api/migrations/0065_auto_20200919_2033.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 3.0.9 on 2020-09-19 20:33 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0064_auto_20200919_1900'), - ] - - operations = [ - migrations.AlterModelOptions( - name='documentationlink', - options={'ordering': ['package']}, - ), - ] diff --git a/pydis_site/apps/api/migrations/0066_merge_20201003_0730.py b/pydis_site/apps/api/migrations/0066_merge_20201003_0730.py deleted file mode 100644 index 298416db..00000000 --- a/pydis_site/apps/api/migrations/0066_merge_20201003_0730.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 3.0.9 on 2020-10-03 07:30 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0064_delete_logentry'), - ('api', '0065_auto_20200919_2033'), - ] - - operations = [ - ] diff --git a/pydis_site/apps/api/migrations/0067_add_voice_ban_infraction_type.py b/pydis_site/apps/api/migrations/0067_add_voice_ban_infraction_type.py deleted file mode 100644 index 9a940ff4..00000000 --- a/pydis_site/apps/api/migrations/0067_add_voice_ban_infraction_type.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.0.10 on 2020-10-10 16:08 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0066_merge_20201003_0730'), - ] - - operations = [ - migrations.AlterField( - model_name='infraction', - name='type', - field=models.CharField(choices=[('note', 'Note'), ('warning', 'Warning'), ('watch', 'Watch'), ('mute', 'Mute'), ('kick', 'Kick'), ('ban', 'Ban'), ('superstar', 'Superstar'), ('voice_ban', 'Voice Ban')], help_text='The type of the infraction.', max_length=9), - ), - ] diff --git a/pydis_site/apps/api/migrations/0068_split_nomination_tables.py b/pydis_site/apps/api/migrations/0068_split_nomination_tables.py deleted file mode 100644 index 79825ed7..00000000 --- a/pydis_site/apps/api/migrations/0068_split_nomination_tables.py +++ /dev/null @@ -1,75 +0,0 @@ -# Generated by Django 3.0.11 on 2021-02-21 15:32 - -from django.apps.registry import Apps -from django.db import backends, migrations, models -from django.db.backends.base.schema import BaseDatabaseSchemaEditor -import django.db.models.deletion -import pydis_site.apps.api.models.mixins - - -def migrate_nominations(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: - Nomination = apps.get_model("api", "Nomination") - NominationEntry = apps.get_model("api", "NominationEntry") - - for nomination in Nomination.objects.all(): - nomination_entry = NominationEntry( - nomination=nomination, - actor=nomination.actor, - reason=nomination.reason, - inserted_at=nomination.inserted_at - ) - nomination_entry.save() - - -def unmigrate_nominations(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: - Nomination = apps.get_model("api", "Nomination") - NominationEntry = apps.get_model("api", "NominationEntry") - - for entry in NominationEntry.objects.all(): - nomination = Nomination.objects.get(pk=entry.nomination.id) - nomination.actor = entry.actor - nomination.reason = entry.reason - nomination.inserted_at = entry.inserted_at - - nomination.save() - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0067_add_voice_ban_infraction_type'), - ] - - operations = [ - migrations.CreateModel( - name='NominationEntry', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('reason', models.TextField(blank=True, help_text='Why the actor nominated this user.', default="")), - ('inserted_at', - models.DateTimeField(auto_now_add=True, help_text='The creation date of this nomination entry.')), - ('actor', models.ForeignKey(help_text='The staff member that nominated this user.', - on_delete=django.db.models.deletion.CASCADE, related_name='nomination_set', - to='api.User')), - ('nomination', models.ForeignKey(help_text='The nomination this entry belongs to.', - on_delete=django.db.models.deletion.CASCADE, to='api.Nomination', - related_name='entries')), - ], - bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), - options={'ordering': ('-inserted_at',), 'verbose_name_plural': 'nomination entries'} - ), - migrations.RunPython(migrate_nominations, unmigrate_nominations), - migrations.RemoveField( - model_name='nomination', - name='actor', - ), - migrations.RemoveField( - model_name='nomination', - name='reason', - ), - migrations.AddField( - model_name='nomination', - name='reviewed', - field=models.BooleanField(default=False, help_text='Whether a review was made.'), - ), - ] diff --git a/pydis_site/apps/api/migrations/0069_documentationlink_validators.py b/pydis_site/apps/api/migrations/0069_documentationlink_validators.py deleted file mode 100644 index 347c0e1a..00000000 --- a/pydis_site/apps/api/migrations/0069_documentationlink_validators.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 3.0.11 on 2021-03-26 18:21 - -import django.core.validators -from django.db import migrations, models -import pydis_site.apps.api.models.bot.documentation_link - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0068_split_nomination_tables'), - ] - - operations = [ - migrations.AlterField( - model_name='documentationlink', - name='base_url', - field=models.URLField(help_text='The base URL from which documentation will be available for this project. Used to generate links to various symbols within this package.', validators=[pydis_site.apps.api.models.bot.documentation_link.ends_with_slash_validator]), - ), - migrations.AlterField( - model_name='documentationlink', - name='package', - field=models.CharField(help_text='The Python package name that this documentation link belongs to.', max_length=50, primary_key=True, serialize=False, validators=[django.core.validators.RegexValidator(message='Package names can only consist of lowercase a-z letters, digits, and underscores.', regex='^[a-z0-9_]+$')]), - ), - ] diff --git a/pydis_site/apps/api/migrations/0070_auto_20210519_0545.py b/pydis_site/apps/api/migrations/0070_auto_20210519_0545.py deleted file mode 100644 index dbd7ac91..00000000 --- a/pydis_site/apps/api/migrations/0070_auto_20210519_0545.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.0.14 on 2021-05-19 05:45 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0069_documentationlink_validators'), - ] - - operations = [ - migrations.AddField( - model_name='offtopicchannelname', - name='active', - field=models.BooleanField(default=True, help_text='Whether or not this name should be considered for naming channels.'), - ), - migrations.AlterField( - model_name='offtopicchannelname', - name='used', - field=models.BooleanField(default=False, help_text='Whether or not this name has already been used during this rotation.'), - ), - ] diff --git a/pydis_site/apps/api/migrations/0070_auto_20210618_2114.py b/pydis_site/apps/api/migrations/0070_auto_20210618_2114.py deleted file mode 100644 index 1d25e421..00000000 --- a/pydis_site/apps/api/migrations/0070_auto_20210618_2114.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 3.0.14 on 2021-06-18 21:14 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0069_documentationlink_validators'), - ] - - operations = [ - migrations.AlterField( - model_name='role', - name='permissions', - field=models.BigIntegerField(help_text='The integer value of the permission bitset of this role from Discord.', validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role permissions cannot be negative.')]), - ), - ] diff --git a/pydis_site/apps/api/migrations/0071_increase_message_content_4000.py b/pydis_site/apps/api/migrations/0071_increase_message_content_4000.py deleted file mode 100644 index 6ca5d21a..00000000 --- a/pydis_site/apps/api/migrations/0071_increase_message_content_4000.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.0.14 on 2021-06-24 14:45 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0070_auto_20210618_2114'), - ] - - operations = [ - migrations.AlterField( - model_name='deletedmessage', - name='content', - field=models.CharField(blank=True, help_text='The content of this message, taken from Discord.', max_length=4000), - ), - ] diff --git a/pydis_site/apps/api/migrations/0072_doc_allow_blank_base_url.py b/pydis_site/apps/api/migrations/0072_doc_allow_blank_base_url.py deleted file mode 100644 index d4899354..00000000 --- a/pydis_site/apps/api/migrations/0072_doc_allow_blank_base_url.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 3.0.14 on 2021-08-30 21:09 - -from django.db import migrations, models -import pydis_site.apps.api.models.bot.documentation_link - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0071_increase_message_content_4000'), - ] - - operations = [ - migrations.AlterField( - model_name='documentationlink', - name='base_url', - field=models.URLField(blank=True, help_text='The base URL from which documentation will be available for this project. Used to generate links to various symbols within this package.', validators=[pydis_site.apps.api.models.bot.documentation_link.ends_with_slash_validator]), - ), - ] diff --git a/pydis_site/apps/api/migrations/0072_merge_20210724_1354.py b/pydis_site/apps/api/migrations/0072_merge_20210724_1354.py deleted file mode 100644 index f12efab5..00000000 --- a/pydis_site/apps/api/migrations/0072_merge_20210724_1354.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 3.0.14 on 2021-07-24 13:54 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0071_increase_message_content_4000'), - ('api', '0070_auto_20210519_0545'), - ] - - operations = [ - ] diff --git a/pydis_site/apps/api/migrations/0073_otn_allow_GT_and_LT.py b/pydis_site/apps/api/migrations/0073_otn_allow_GT_and_LT.py deleted file mode 100644 index 09ad13da..00000000 --- a/pydis_site/apps/api/migrations/0073_otn_allow_GT_and_LT.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 3.0.14 on 2021-09-27 20:38 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0072_doc_allow_blank_base_url'), - ] - - operations = [ - migrations.AlterField( - model_name='offtopicchannelname', - name='name', - field=models.CharField(help_text='The actual channel name that will be used on our Discord server.', max_length=96, primary_key=True, serialize=False, validators=[django.core.validators.RegexValidator(regex="^[a-z0-9\\U0001d5a0-\\U0001d5b9-ǃ?’'<>]+$")]), - ), - ] diff --git a/pydis_site/apps/api/migrations/0074_merge_20211105_0518.py b/pydis_site/apps/api/migrations/0074_merge_20211105_0518.py deleted file mode 100644 index ebf5ae15..00000000 --- a/pydis_site/apps/api/migrations/0074_merge_20211105_0518.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 3.0.14 on 2021-11-05 05:18 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0072_merge_20210724_1354'), - ('api', '0073_otn_allow_GT_and_LT'), - ] - - operations = [ - ] diff --git a/pydis_site/apps/api/migrations/0074_reminder_failures.py b/pydis_site/apps/api/migrations/0074_reminder_failures.py deleted file mode 100644 index 2860046e..00000000 --- a/pydis_site/apps/api/migrations/0074_reminder_failures.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.0.14 on 2021-10-27 17:44 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0073_otn_allow_GT_and_LT'), - ] - - operations = [ - migrations.AddField( - model_name='reminder', - name='failures', - field=models.IntegerField(default=0, help_text='Number of times we attempted to send the reminder and failed.'), - ), - ] diff --git a/pydis_site/apps/api/migrations/0074_voice_mute.py b/pydis_site/apps/api/migrations/0074_voice_mute.py deleted file mode 100644 index 937557bc..00000000 --- a/pydis_site/apps/api/migrations/0074_voice_mute.py +++ /dev/null @@ -1,36 +0,0 @@ -# Generated by Django 3.0.14 on 2021-10-09 18:52 -from django.apps.registry import Apps -from django.db import migrations, models -from django.db.backends.base.schema import BaseDatabaseSchemaEditor - - -def migrate_infractions(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: - Infraction = apps.get_model("api", "Infraction") - - for infraction in Infraction.objects.filter(type="voice_ban"): - infraction.type = "voice_mute" - infraction.save() - - -def unmigrate_infractions(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: - Infraction = apps.get_model("api", "Infraction") - - for infraction in Infraction.objects.filter(type="voice_mute"): - infraction.type = "voice_ban" - infraction.save() - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0073_otn_allow_GT_and_LT'), - ] - - operations = [ - migrations.AlterField( - model_name='infraction', - name='type', - field=models.CharField(choices=[('note', 'Note'), ('warning', 'Warning'), ('watch', 'Watch'), ('mute', 'Mute'), ('kick', 'Kick'), ('ban', 'Ban'), ('superstar', 'Superstar'), ('voice_ban', 'Voice Ban'), ('voice_mute', 'Voice Mute')], help_text='The type of the infraction.', max_length=10), - ), - migrations.RunPython(migrate_infractions, unmigrate_infractions) - ] diff --git a/pydis_site/apps/api/migrations/0075_add_redirects_filter.py b/pydis_site/apps/api/migrations/0075_add_redirects_filter.py deleted file mode 100644 index 23dc176f..00000000 --- a/pydis_site/apps/api/migrations/0075_add_redirects_filter.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.0.14 on 2021-11-17 10:24 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0074_reminder_failures'), - ] - - operations = [ - migrations.AlterField( - model_name='filterlist', - name='type', - field=models.CharField(choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'), ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token'), ('REDIRECT', 'Redirect')], help_text='The type of allowlist this is on.', max_length=50), - ), - ] diff --git a/pydis_site/apps/api/migrations/0075_infraction_dm_sent.py b/pydis_site/apps/api/migrations/0075_infraction_dm_sent.py deleted file mode 100644 index c0ac709d..00000000 --- a/pydis_site/apps/api/migrations/0075_infraction_dm_sent.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.0.14 on 2021-11-10 22:06 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0074_reminder_failures'), - ] - - operations = [ - migrations.AddField( - model_name='infraction', - name='dm_sent', - field=models.BooleanField(help_text='Whether a DM was sent to the user when infraction was applied.', null=True), - ), - ] diff --git a/pydis_site/apps/api/migrations/0076_merge_20211125_1941.py b/pydis_site/apps/api/migrations/0076_merge_20211125_1941.py deleted file mode 100644 index 097d0a0c..00000000 --- a/pydis_site/apps/api/migrations/0076_merge_20211125_1941.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 3.0.14 on 2021-11-25 19:41 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0075_infraction_dm_sent'), - ('api', '0075_add_redirects_filter'), - ] - - operations = [ - ] diff --git a/pydis_site/apps/api/migrations/0077_use_generic_jsonfield.py b/pydis_site/apps/api/migrations/0077_use_generic_jsonfield.py deleted file mode 100644 index 95ef5850..00000000 --- a/pydis_site/apps/api/migrations/0077_use_generic_jsonfield.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 3.1.13 on 2021-11-27 12:27 - -import django.contrib.postgres.fields -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0076_merge_20211125_1941'), - ] - - operations = [ - migrations.AlterField( - model_name='botsetting', - name='data', - field=models.JSONField(help_text='The actual settings of this setting.'), - ), - migrations.AlterField( - model_name='deletedmessage', - name='embeds', - field=django.contrib.postgres.fields.ArrayField(base_field=models.JSONField(validators=[]), blank=True, help_text='Embeds attached to this message.', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0078_merge_20211213_0552.py b/pydis_site/apps/api/migrations/0078_merge_20211213_0552.py deleted file mode 100644 index 5ce0e871..00000000 --- a/pydis_site/apps/api/migrations/0078_merge_20211213_0552.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 3.1.14 on 2021-12-13 05:52 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0077_use_generic_jsonfield'), - ('api', '0074_merge_20211105_0518'), - ] - - operations = [ - ] diff --git a/pydis_site/apps/api/migrations/0079_merge_20220125_2022.py b/pydis_site/apps/api/migrations/0079_merge_20220125_2022.py deleted file mode 100644 index 9b9d9326..00000000 --- a/pydis_site/apps/api/migrations/0079_merge_20220125_2022.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 3.1.14 on 2022-01-25 20:22 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0078_merge_20211213_0552'), - ('api', '0074_voice_mute'), - ] - - operations = [ - ] diff --git a/pydis_site/apps/api/migrations/0080_add_aoc_tables.py b/pydis_site/apps/api/migrations/0080_add_aoc_tables.py deleted file mode 100644 index 2c0c689a..00000000 --- a/pydis_site/apps/api/migrations/0080_add_aoc_tables.py +++ /dev/null @@ -1,32 +0,0 @@ -# Generated by Django 3.1.14 on 2022-03-06 16:07 - -from django.db import migrations, models -import django.db.models.deletion -import pydis_site.apps.api.models.mixins - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0079_merge_20220125_2022'), - ] - - operations = [ - migrations.CreateModel( - name='AocAccountLink', - fields=[ - ('user', models.OneToOneField(help_text='The user that is blocked from getting the AoC Completionist Role', on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='api.user')), - ('aoc_username', models.CharField(help_text='The AoC username associated with the Discord User.', max_length=120)), - ], - bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), - ), - migrations.CreateModel( - name='AocCompletionistBlock', - fields=[ - ('user', models.OneToOneField(help_text='The user that is blocked from getting the AoC Completionist Role', on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='api.user')), - ('is_blocked', models.BooleanField(default=True, help_text='Whether this user is actively being blocked from getting the AoC Completionist Role', verbose_name='Blocked')), - ('reason', models.TextField(help_text='The reason for the AoC Completionist Role Block.', null=True)), - ], - bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), - ), - ] diff --git a/pydis_site/apps/api/migrations/0081_bumpedthread.py b/pydis_site/apps/api/migrations/0081_bumpedthread.py deleted file mode 100644 index 03e66cc1..00000000 --- a/pydis_site/apps/api/migrations/0081_bumpedthread.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 3.1.14 on 2022-02-19 16:26 - -import django.core.validators -from django.db import migrations, models -import pydis_site.apps.api.models.mixins - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0080_add_aoc_tables'), - ] - - operations = [ - migrations.CreateModel( - name='BumpedThread', - fields=[ - ('thread_id', models.BigIntegerField(help_text='The thread ID that should be bumped.', primary_key=True, serialize=False, validators=[django.core.validators.MinValueValidator(limit_value=0, message='Thread IDs cannot be negative.')], verbose_name='Thread ID')), - ], - bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), - ), - ] diff --git a/pydis_site/apps/api/migrations/0082_otn_allow_big_solidus.py b/pydis_site/apps/api/migrations/0082_otn_allow_big_solidus.py deleted file mode 100644 index abbb98ec..00000000 --- a/pydis_site/apps/api/migrations/0082_otn_allow_big_solidus.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 3.1.14 on 2022-04-21 23:29 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0081_bumpedthread'), - ] - - operations = [ - migrations.AlterField( - model_name='offtopicchannelname', - name='name', - field=models.CharField(help_text='The actual channel name that will be used on our Discord server.', max_length=96, primary_key=True, serialize=False, validators=[django.core.validators.RegexValidator(regex="^[a-z0-9\\U0001d5a0-\\U0001d5b9-ǃ?’'<>⧹⧸]+$")]), - ), - ] diff --git a/pydis_site/apps/api/migrations/0083_remove_embed_validation.py b/pydis_site/apps/api/migrations/0083_remove_embed_validation.py deleted file mode 100644 index e835bb66..00000000 --- a/pydis_site/apps/api/migrations/0083_remove_embed_validation.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 3.1.14 on 2022-06-30 09:41 - -import django.contrib.postgres.fields -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0082_otn_allow_big_solidus'), - ] - - operations = [ - migrations.AlterField( - model_name='deletedmessage', - name='embeds', - field=django.contrib.postgres.fields.ArrayField(base_field=models.JSONField(), blank=True, help_text='Embeds attached to this message.', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0084_infraction_last_applied.py b/pydis_site/apps/api/migrations/0084_infraction_last_applied.py deleted file mode 100644 index 7704ddb8..00000000 --- a/pydis_site/apps/api/migrations/0084_infraction_last_applied.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 4.0.6 on 2022-07-27 20:32 - -import django.utils.timezone -from django.db import migrations, models -from django.apps.registry import Apps - - -def set_last_applied_to_inserted_at(apps: Apps, schema_editor): - Infractions = apps.get_model("api", "infraction") - Infractions.objects.all().update(last_applied=models.F("inserted_at")) - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0083_remove_embed_validation'), - ] - - operations = [ - migrations.AddField( - model_name='infraction', - name='last_applied', - field=models.DateTimeField(default=django.utils.timezone.now, help_text='The date and time of when this infraction was last applied.'), - ), - migrations.RunPython(set_last_applied_to_inserted_at) - ] diff --git a/pydis_site/apps/api/migrations/0085_add_thread_id_to_nominations.py b/pydis_site/apps/api/migrations/0085_add_thread_id_to_nominations.py deleted file mode 100644 index 56a24cc3..00000000 --- a/pydis_site/apps/api/migrations/0085_add_thread_id_to_nominations.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.1.2 on 2022-11-12 14:52 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0084_infraction_last_applied'), - ] - - operations = [ - migrations.AddField( - model_name='nomination', - name='thread_id', - field=models.BigIntegerField(help_text="The nomination vote's thread id.", null=True), - ), - ] diff --git a/pydis_site/apps/api/migrations/0086_infraction_jump_url.py b/pydis_site/apps/api/migrations/0086_infraction_jump_url.py deleted file mode 100644 index 7ae65751..00000000 --- a/pydis_site/apps/api/migrations/0086_infraction_jump_url.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.1.7 on 2023-03-10 17:25 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0085_add_thread_id_to_nominations'), - ] - - operations = [ - migrations.AddField( - model_name='infraction', - name='jump_url', - field=models.URLField(default=None, help_text='The jump url to message invoking the infraction.', max_length=88, null=True), - ), - ] diff --git a/pydis_site/apps/api/migrations/0087_alter_mute_to_timeout.py b/pydis_site/apps/api/migrations/0087_alter_mute_to_timeout.py deleted file mode 100644 index 8a826ba5..00000000 --- a/pydis_site/apps/api/migrations/0087_alter_mute_to_timeout.py +++ /dev/null @@ -1,25 +0,0 @@ -from django.apps.registry import Apps -from django.db import migrations, models - -import pydis_site.apps.api.models - - -def rename_type(apps: Apps, _) -> None: - infractions: pydis_site.apps.api.models.Infraction = apps.get_model("api", "Infraction") - infractions.objects.filter(type="mute").update(type="timeout") - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0086_infraction_jump_url'), - ] - - operations = [ - migrations.AlterField( - model_name='infraction', - name='type', - field=models.CharField(choices=[('note', 'Note'), ('warning', 'Warning'), ('watch', 'Watch'), ('timeout', 'Timeout'), ('kick', 'Kick'), ('ban', 'Ban'), ('superstar', 'Superstar'), ('voice_ban', 'Voice Ban'), ('voice_mute', 'Voice Mute')], help_text='The type of the infraction.', max_length=10), - ), - migrations.RunPython(rename_type, migrations.RunPython.noop) - ] diff --git a/pydis_site/apps/api/migrations/__init__.py b/pydis_site/apps/api/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pydis_site/apps/api/migrations/__init__.py +++ /dev/null diff --git a/pydis_site/apps/api/models/__init__.py b/pydis_site/apps/api/models/__init__.py deleted file mode 100644 index a197e988..00000000 --- a/pydis_site/apps/api/models/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# flake8: noqa -from .bot import ( - BotSetting, - BumpedThread, - DocumentationLink, - DeletedMessage, - FilterList, - Infraction, - Message, - MessageDeletionContext, - Nomination, - NominationEntry, - OffensiveMessage, - AocAccountLink, - AocCompletionistBlock, - OffTopicChannelName, - Reminder, - Role, - User -) diff --git a/pydis_site/apps/api/models/bot/__init__.py b/pydis_site/apps/api/models/bot/__init__.py deleted file mode 100644 index 013bb85e..00000000 --- a/pydis_site/apps/api/models/bot/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# flake8: noqa -from .bot_setting import BotSetting -from .bumped_thread import BumpedThread -from .deleted_message import DeletedMessage -from .documentation_link import DocumentationLink -from .filter_list import FilterList -from .infraction import Infraction -from .message import Message -from .aoc_completionist_block import AocCompletionistBlock -from .aoc_link import AocAccountLink -from .message_deletion_context import MessageDeletionContext -from .nomination import Nomination, NominationEntry -from .off_topic_channel_name import OffTopicChannelName -from .offensive_message import OffensiveMessage -from .reminder import Reminder -from .role import Role -from .user import User diff --git a/pydis_site/apps/api/models/bot/aoc_completionist_block.py b/pydis_site/apps/api/models/bot/aoc_completionist_block.py deleted file mode 100644 index acbc0eba..00000000 --- a/pydis_site/apps/api/models/bot/aoc_completionist_block.py +++ /dev/null @@ -1,26 +0,0 @@ -from django.db import models - -from pydis_site.apps.api.models.bot.user import User -from pydis_site.apps.api.models.mixins import ModelReprMixin - - -class AocCompletionistBlock(ModelReprMixin, models.Model): - """A Discord user blocked from getting the AoC completionist Role.""" - - user = models.OneToOneField( - User, - on_delete=models.CASCADE, - help_text="The user that is blocked from getting the AoC Completionist Role", - primary_key=True - ) - - is_blocked = models.BooleanField( - default=True, - help_text="Whether this user is actively being blocked " - "from getting the AoC Completionist Role", - verbose_name="Blocked" - ) - reason = models.TextField( - null=True, - help_text="The reason for the AoC Completionist Role Block." - ) diff --git a/pydis_site/apps/api/models/bot/aoc_link.py b/pydis_site/apps/api/models/bot/aoc_link.py deleted file mode 100644 index 4e9d4882..00000000 --- a/pydis_site/apps/api/models/bot/aoc_link.py +++ /dev/null @@ -1,21 +0,0 @@ -from django.db import models - -from pydis_site.apps.api.models.bot.user import User -from pydis_site.apps.api.models.mixins import ModelReprMixin - - -class AocAccountLink(ModelReprMixin, models.Model): - """An AoC account link for a Discord User.""" - - user = models.OneToOneField( - User, - on_delete=models.CASCADE, - help_text="The user that is blocked from getting the AoC Completionist Role", - primary_key=True - ) - - aoc_username = models.CharField( - max_length=120, - help_text="The AoC username associated with the Discord User.", - blank=False - ) diff --git a/pydis_site/apps/api/models/bot/bot_setting.py b/pydis_site/apps/api/models/bot/bot_setting.py deleted file mode 100644 index 1bcb1ae6..00000000 --- a/pydis_site/apps/api/models/bot/bot_setting.py +++ /dev/null @@ -1,28 +0,0 @@ -from django.core.exceptions import ValidationError -from django.db import models - -from pydis_site.apps.api.models.mixins import ModelReprMixin - - -def validate_bot_setting_name(name: str) -> None: - """Raises a ValidationError if the given name is not a known setting.""" - known_settings = ( - 'defcon', - 'news', - ) - - if name not in known_settings: - raise ValidationError(f"`{name}` is not a known setting name.") - - -class BotSetting(ModelReprMixin, models.Model): - """A configuration entry for the bot.""" - - name = models.CharField( - primary_key=True, - max_length=50, - validators=(validate_bot_setting_name,) - ) - data = models.JSONField( - help_text="The actual settings of this setting." - ) diff --git a/pydis_site/apps/api/models/bot/bumped_thread.py b/pydis_site/apps/api/models/bot/bumped_thread.py deleted file mode 100644 index cdf9a950..00000000 --- a/pydis_site/apps/api/models/bot/bumped_thread.py +++ /dev/null @@ -1,22 +0,0 @@ -from django.core.validators import MinValueValidator -from django.db import models - -from pydis_site.apps.api.models.mixins import ModelReprMixin - - -class BumpedThread(ModelReprMixin, models.Model): - """A list of thread IDs to be bumped.""" - - thread_id = models.BigIntegerField( - primary_key=True, - help_text=( - "The thread ID that should be bumped." - ), - validators=( - MinValueValidator( - limit_value=0, - message="Thread IDs cannot be negative." - ), - ), - verbose_name="Thread ID", - ) diff --git a/pydis_site/apps/api/models/bot/deleted_message.py b/pydis_site/apps/api/models/bot/deleted_message.py deleted file mode 100644 index 50b70d8c..00000000 --- a/pydis_site/apps/api/models/bot/deleted_message.py +++ /dev/null @@ -1,19 +0,0 @@ -from django.db import models - -from pydis_site.apps.api.models.bot.message import Message -from pydis_site.apps.api.models.bot.message_deletion_context import MessageDeletionContext - - -class DeletedMessage(Message): - """A deleted message, previously sent somewhere on the Discord server.""" - - deletion_context = models.ForeignKey( - MessageDeletionContext, - help_text="The deletion context this message is part of.", - on_delete=models.CASCADE - ) - - class Meta: - """Sets the default ordering for list views to newest first.""" - - ordering = ("-id",) diff --git a/pydis_site/apps/api/models/bot/documentation_link.py b/pydis_site/apps/api/models/bot/documentation_link.py deleted file mode 100644 index 9941907c..00000000 --- a/pydis_site/apps/api/models/bot/documentation_link.py +++ /dev/null @@ -1,47 +0,0 @@ -from django.core.exceptions import ValidationError -from django.core.validators import RegexValidator -from django.db import models - -from pydis_site.apps.api.models.mixins import ModelReprMixin - -package_name_validator = RegexValidator( - regex=r"^[a-z0-9_]+$", - message="Package names can only consist of lowercase a-z letters, digits, and underscores." -) - - -def ends_with_slash_validator(string: str) -> None: - """Raise a ValidationError if `string` does not end with a slash.""" - if not string.endswith("/"): - raise ValidationError("The entered URL must end with a slash.") - - -class DocumentationLink(ModelReprMixin, models.Model): - """A documentation link used by the `!docs` command of the bot.""" - - package = models.CharField( - primary_key=True, - max_length=50, - validators=(package_name_validator,), - help_text="The Python package name that this documentation link belongs to." - ) - base_url = models.URLField( - help_text=( - "The base URL from which documentation will be available for this project. " - "Used to generate links to various symbols within this package." - ), - blank=True, - validators=(ends_with_slash_validator,) - ) - inventory_url = models.URLField( - help_text="The URL at which the Sphinx inventory is available for this package." - ) - - def __str__(self): - """Returns the package and URL for the current documentation link, for display purposes.""" - return f"{self.package} - {self.base_url}" - - class Meta: - """Defines the meta options for the documentation link model.""" - - ordering = ['package'] diff --git a/pydis_site/apps/api/models/bot/filter_list.py b/pydis_site/apps/api/models/bot/filter_list.py deleted file mode 100644 index d30f7213..00000000 --- a/pydis_site/apps/api/models/bot/filter_list.py +++ /dev/null @@ -1,42 +0,0 @@ -from django.db import models - -from pydis_site.apps.api.models.mixins import ModelReprMixin, ModelTimestampMixin - - -class FilterList(ModelTimestampMixin, ModelReprMixin, models.Model): - """An item that is either allowed or denied.""" - - FilterListType = models.TextChoices( - 'FilterListType', - 'GUILD_INVITE ' - 'FILE_FORMAT ' - 'DOMAIN_NAME ' - 'FILTER_TOKEN ' - 'REDIRECT ' - ) - type = models.CharField( - max_length=50, - help_text="The type of allowlist this is on.", - choices=FilterListType.choices, - ) - allowed = models.BooleanField( - help_text="Whether this item is on the allowlist or the denylist." - ) - content = models.TextField( - help_text="The data to add to the allow or denylist." - ) - comment = models.TextField( - help_text="Optional comment on this entry.", - null=True - ) - - class Meta: - """Metaconfig for this model.""" - - # This constraint ensures only one filterlist with the - # same content can exist. This means that we cannot have both an allow - # and a deny for the same item, and we cannot have duplicates of the - # same item. - constraints = [ - models.UniqueConstraint(fields=['content', 'type'], name='unique_filter_list'), - ] diff --git a/pydis_site/apps/api/models/bot/infraction.py b/pydis_site/apps/api/models/bot/infraction.py deleted file mode 100644 index 381b5b9d..00000000 --- a/pydis_site/apps/api/models/bot/infraction.py +++ /dev/null @@ -1,100 +0,0 @@ -from django.db import models -from django.utils import timezone - -from pydis_site.apps.api.models.bot.user import User -from pydis_site.apps.api.models.mixins import ModelReprMixin - - -class Infraction(ModelReprMixin, models.Model): - """An infraction for a Discord user.""" - - TYPE_CHOICES = ( - ("note", "Note"), - ("warning", "Warning"), - ("watch", "Watch"), - ("timeout", "Timeout"), - ("kick", "Kick"), - ("ban", "Ban"), - ("superstar", "Superstar"), - ("voice_ban", "Voice Ban"), - ("voice_mute", "Voice Mute"), - ) - inserted_at = models.DateTimeField( - default=timezone.now, - help_text="The date and time of the creation of this infraction." - ) - last_applied = models.DateTimeField( - # This default is for backwards compatibility with bot versions - # that don't explicitly give a value. - default=timezone.now, - help_text="The date and time of when this infraction was last applied." - ) - expires_at = models.DateTimeField( - null=True, - help_text=( - "The date and time of the expiration of this infraction. " - "Null if the infraction is permanent or it can't expire." - ) - ) - active = models.BooleanField( - help_text="Whether the infraction is still active." - ) - user = models.ForeignKey( - User, - on_delete=models.CASCADE, - related_name='infractions_received', - help_text="The user to which the infraction was applied." - ) - actor = models.ForeignKey( - User, - on_delete=models.CASCADE, - related_name='infractions_given', - help_text="The user which applied the infraction." - ) - type = models.CharField( - max_length=10, - choices=TYPE_CHOICES, - help_text="The type of the infraction." - ) - reason = models.TextField( - null=True, - help_text="The reason for the infraction." - ) - hidden = models.BooleanField( - default=False, - help_text="Whether the infraction is a shadow infraction." - ) - dm_sent = models.BooleanField( - null=True, - help_text="Whether a DM was sent to the user when infraction was applied." - ) - - jump_url = models.URLField( - default=None, - null=True, - max_length=88, - help_text=( - "The jump url to message invoking the infraction." - ) - ) - - def __str__(self): - """Returns some info on the current infraction, for display purposes.""" - s = f"#{self.id}: {self.type} on {self.user_id}" - if self.expires_at: - s += f" until {self.expires_at}" - if self.hidden: - s += " (hidden)" - return s - - class Meta: - """Defines the meta options for the infraction model.""" - - ordering = ['-inserted_at'] - constraints = ( - models.UniqueConstraint( - fields=["user", "type"], - condition=models.Q(active=True), - name="unique_active_infraction_per_type_per_user" - ), - ) diff --git a/pydis_site/apps/api/models/bot/message.py b/pydis_site/apps/api/models/bot/message.py deleted file mode 100644 index 89ae27e4..00000000 --- a/pydis_site/apps/api/models/bot/message.py +++ /dev/null @@ -1,71 +0,0 @@ -import datetime - -from django.contrib.postgres import fields as pgfields -from django.core.validators import MinValueValidator -from django.db import models - -from pydis_site.apps.api.models.bot.user import User -from pydis_site.apps.api.models.mixins import ModelReprMixin - - -class Message(ModelReprMixin, models.Model): - """A message, sent somewhere on the Discord server.""" - - id = models.BigIntegerField( - primary_key=True, - help_text="The message ID as taken from Discord.", - validators=( - MinValueValidator( - limit_value=0, - message="Message IDs cannot be negative." - ), - ), - verbose_name="ID" - ) - author = models.ForeignKey( - User, - on_delete=models.CASCADE, - help_text="The author of this message." - ) - channel_id = models.BigIntegerField( - help_text=( - "The channel ID that this message was " - "sent in, taken from Discord." - ), - validators=( - MinValueValidator( - limit_value=0, - message="Channel IDs cannot be negative." - ), - ), - verbose_name="Channel ID" - ) - content = models.CharField( - max_length=4_000, - help_text="The content of this message, taken from Discord.", - blank=True - ) - embeds = pgfields.ArrayField( - models.JSONField(), - blank=True, - help_text="Embeds attached to this message." - ) - attachments = pgfields.ArrayField( - models.URLField( - max_length=512 - ), - blank=True, - help_text="Attachments attached to this message." - ) - - @property - def timestamp(self) -> datetime.datetime: - """Attribute that represents the message timestamp as derived from the snowflake id.""" - return datetime.datetime.utcfromtimestamp( - ((self.id >> 22) + 1420070400000) / 1000 - ).replace(tzinfo=datetime.timezone.utc) - - class Meta: - """Metadata provided for Django's ORM.""" - - abstract = True diff --git a/pydis_site/apps/api/models/bot/message_deletion_context.py b/pydis_site/apps/api/models/bot/message_deletion_context.py deleted file mode 100644 index 25741266..00000000 --- a/pydis_site/apps/api/models/bot/message_deletion_context.py +++ /dev/null @@ -1,41 +0,0 @@ -from django.db import models -from django.urls import reverse - -from pydis_site.apps.api.models.bot.user import User -from pydis_site.apps.api.models.mixins import ModelReprMixin - - -class MessageDeletionContext(ModelReprMixin, models.Model): - """ - Represents the context for a deleted message. - - The context includes its creation date, as well as the actor associated with the deletion. - This helps to keep track of message deletions on the server. - """ - - actor = models.ForeignKey( - User, - on_delete=models.CASCADE, - help_text=( - "The original actor causing this deletion. Could be the author " - "of a manual clean command invocation, the bot when executing " - "automatic actions, or nothing to indicate that the bulk " - "deletion was not issued by us." - ), - null=True - ) - creation = models.DateTimeField( - # Consider whether we want to add a validator here that ensures - # the deletion context does not take place in the future. - help_text="When this deletion took place." - ) - - @property - def log_url(self) -> str: - """Create the url for the deleted message logs.""" - return reverse('staff:logs', args=(self.id,)) - - class Meta: - """Set the ordering for list views to newest first.""" - - ordering = ("-creation",) diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py deleted file mode 100644 index f53dd33c..00000000 --- a/pydis_site/apps/api/models/bot/metricity.py +++ /dev/null @@ -1,160 +0,0 @@ -from typing import List, Tuple - -from django.db import connections - -BLOCK_INTERVAL = 10 * 60 # 10 minute blocks - -EXCLUDE_CHANNELS = ( - "267659945086812160", # Bot commands - "607247579608121354" # SeasonalBot commands -) - - -class NotFoundError(Exception): # noqa: N818 - """Raised when an entity cannot be found.""" - - pass - - -class Metricity: - """Abstraction for a connection to the metricity database.""" - - def __init__(self): - self.cursor = connections['metricity'].cursor() - - def __enter__(self): - return self - - def __exit__(self, *_): - self.cursor.close() - - def user(self, user_id: str) -> dict: - """Query a user's data.""" - # TODO: Swap this back to some sort of verified at date - columns = ["joined_at"] - query = f"SELECT {','.join(columns)} FROM users WHERE id = '%s'" - self.cursor.execute(query, [user_id]) - values = self.cursor.fetchone() - - if not values: - raise NotFoundError() - - return dict(zip(columns, values)) - - def total_messages(self, user_id: str) -> int: - """Query total number of messages for a user.""" - self.cursor.execute( - """ - SELECT - COUNT(*) - FROM messages - WHERE - author_id = '%s' - AND NOT is_deleted - AND channel_id NOT IN %s - """, - [user_id, EXCLUDE_CHANNELS] - ) - values = self.cursor.fetchone() - - if not values: - raise NotFoundError() - - return values[0] - - def total_message_blocks(self, user_id: str) -> int: - """ - Query number of 10 minute blocks during which the user has been active. - - This metric prevents users from spamming to achieve the message total threshold. - """ - self.cursor.execute( - """ - SELECT - COUNT(*) - FROM ( - SELECT - (floor((extract('epoch' from created_at) / %s )) * %s) AS interval - FROM messages - WHERE - author_id='%s' - AND NOT is_deleted - AND channel_id NOT IN %s - GROUP BY interval - ) block_query; - """, - [BLOCK_INTERVAL, BLOCK_INTERVAL, user_id, EXCLUDE_CHANNELS] - ) - values = self.cursor.fetchone() - - if not values: - raise NotFoundError() - - return values[0] - - def top_channel_activity(self, user_id: str) -> List[Tuple[str, int]]: - """ - Query the top three channels in which the user is most active. - - Help channels are grouped under "the help channels", - and off-topic channels are grouped under "off-topic". - """ - self.cursor.execute( - """ - SELECT - CASE - WHEN channels.name ILIKE 'help-%%' THEN 'the help channels' - WHEN channels.name ILIKE 'ot%%' THEN 'off-topic' - WHEN channels.name ILIKE '%%voice%%' THEN 'voice chats' - ELSE channels.name - END, - COUNT(1) - FROM - messages - LEFT JOIN channels ON channels.id = messages.channel_id - WHERE - author_id = '%s' AND NOT messages.is_deleted - GROUP BY - 1 - ORDER BY - 2 DESC - LIMIT - 3; - """, - [user_id] - ) - - values = self.cursor.fetchall() - - if not values: - raise NotFoundError() - - return values - - def total_messages_in_past_n_days( - self, - user_ids: list[str], - days: int - ) -> list[tuple[str, int]]: - """ - Query activity by a list of users in the past `days` days. - - Returns a list of (user_id, message_count) tuples. - """ - self.cursor.execute( - """ - SELECT - author_id, COUNT(*) - FROM messages - WHERE - author_id IN %s - AND NOT is_deleted - AND channel_id NOT IN %s - AND created_at > now() - interval '%s days' - GROUP BY author_id - """, - [tuple(user_ids), EXCLUDE_CHANNELS, days] - ) - values = self.cursor.fetchall() - - return values diff --git a/pydis_site/apps/api/models/bot/nomination.py b/pydis_site/apps/api/models/bot/nomination.py deleted file mode 100644 index 58e70a83..00000000 --- a/pydis_site/apps/api/models/bot/nomination.py +++ /dev/null @@ -1,86 +0,0 @@ -from django.db import models - -from pydis_site.apps.api.models.bot.user import User -from pydis_site.apps.api.models.mixins import ModelReprMixin - - -class Nomination(ModelReprMixin, models.Model): - """A general helper nomination information created by staff.""" - - active = models.BooleanField( - default=True, - help_text="Whether this nomination is still relevant." - ) - user = models.ForeignKey( - User, - on_delete=models.CASCADE, - help_text="The nominated user.", - related_name='nomination' - ) - inserted_at = models.DateTimeField( - auto_now_add=True, - help_text="The creation date of this nomination." - ) - end_reason = models.TextField( - help_text="Why the nomination was ended.", - default="", - blank=True - ) - ended_at = models.DateTimeField( - auto_now_add=False, - help_text="When the nomination was ended.", - null=True - ) - reviewed = models.BooleanField( - default=False, - help_text="Whether a review was made." - ) - thread_id = models.BigIntegerField( - help_text="The nomination vote's thread id.", - null=True, - ) - - def __str__(self): - """Representation that makes the target and state of the nomination immediately evident.""" - status = "active" if self.active else "ended" - return f"Nomination of {self.user} ({status})" - - class Meta: - """Set the ordering of nominations to most recent first.""" - - ordering = ("-inserted_at",) - - -class NominationEntry(ModelReprMixin, models.Model): - """A nomination entry created by a single staff member.""" - - nomination = models.ForeignKey( - Nomination, - on_delete=models.CASCADE, - help_text="The nomination this entry belongs to.", - related_name="entries" - ) - actor = models.ForeignKey( - User, - on_delete=models.CASCADE, - help_text="The staff member that nominated this user.", - related_name='nomination_set' - ) - reason = models.TextField( - help_text="Why the actor nominated this user.", - default="", - blank=True - ) - inserted_at = models.DateTimeField( - auto_now_add=True, - help_text="The creation date of this nomination entry." - ) - - class Meta: - """Meta options for NominationEntry model.""" - - verbose_name_plural = "nomination entries" - - # Set default ordering here to latest first - # so we don't need to define it everywhere - ordering = ("-inserted_at",) diff --git a/pydis_site/apps/api/models/bot/off_topic_channel_name.py b/pydis_site/apps/api/models/bot/off_topic_channel_name.py deleted file mode 100644 index b380efad..00000000 --- a/pydis_site/apps/api/models/bot/off_topic_channel_name.py +++ /dev/null @@ -1,31 +0,0 @@ -from django.core.validators import RegexValidator -from django.db import models - -from pydis_site.apps.api.models.mixins import ModelReprMixin - - -class OffTopicChannelName(ModelReprMixin, models.Model): - """An off-topic channel name, used during the daily channel name shuffle.""" - - name = models.CharField( - primary_key=True, - max_length=96, - validators=( - RegexValidator(regex=r"^[a-z0-9\U0001d5a0-\U0001d5b9-ǃ?’'<>⧹⧸]+$"), - ), - help_text="The actual channel name that will be used on our Discord server." - ) - - used = models.BooleanField( - default=False, - help_text="Whether or not this name has already been used during this rotation.", - ) - - active = models.BooleanField( - default=True, - help_text="Whether or not this name should be considered for naming channels." - ) - - def __str__(self): - """Returns the current off-topic name, for display purposes.""" - return self.name diff --git a/pydis_site/apps/api/models/bot/offensive_message.py b/pydis_site/apps/api/models/bot/offensive_message.py deleted file mode 100644 index 74dab59b..00000000 --- a/pydis_site/apps/api/models/bot/offensive_message.py +++ /dev/null @@ -1,51 +0,0 @@ -import datetime - -from django.core.exceptions import ValidationError -from django.core.validators import MinValueValidator -from django.db import models - -from pydis_site.apps.api.models.mixins import ModelReprMixin - - -def future_date_validator(date: datetime.date) -> None: - """Raise ValidationError if the date isn't a future date.""" - if date < datetime.datetime.now(datetime.timezone.utc): - raise ValidationError("Date must be a future date") - - -class OffensiveMessage(ModelReprMixin, models.Model): - """A message that triggered a filter and that will be deleted one week after it was sent.""" - - id = models.BigIntegerField( - primary_key=True, - help_text="The message ID as taken from Discord.", - validators=( - MinValueValidator( - limit_value=0, - message="Message IDs cannot be negative." - ), - ), - verbose_name="Message ID" - ) - channel_id = models.BigIntegerField( - help_text=( - "The channel ID that the message was " - "sent in, taken from Discord." - ), - validators=( - MinValueValidator( - limit_value=0, - message="Channel IDs cannot be negative." - ), - ), - verbose_name="Channel ID" - ) - delete_date = models.DateTimeField( - help_text="The date on which the message will be auto-deleted.", - validators=(future_date_validator,), - verbose_name="To Be Deleted" - ) - - def __str__(self): - """Return some info on this message, for display purposes only.""" - return f"Message {self.id}, will be deleted at {self.delete_date}" diff --git a/pydis_site/apps/api/models/bot/reminder.py b/pydis_site/apps/api/models/bot/reminder.py deleted file mode 100644 index 173900ee..00000000 --- a/pydis_site/apps/api/models/bot/reminder.py +++ /dev/null @@ -1,69 +0,0 @@ -from django.contrib.postgres.fields import ArrayField -from django.core.validators import MinValueValidator -from django.db import models - -from pydis_site.apps.api.models.bot.user import User -from pydis_site.apps.api.models.mixins import ModelReprMixin - - -class Reminder(ModelReprMixin, models.Model): - """A reminder created by a user.""" - - active = models.BooleanField( - default=True, - help_text=( - "Whether this reminder is still active. " - "If not, it has been sent out to the user." - ) - ) - jump_url = models.URLField( - max_length=88, - help_text=( - "The jump url to the message that created the reminder" - ) - ) - author = models.ForeignKey( - User, - on_delete=models.CASCADE, - help_text="The creator of this reminder." - ) - channel_id = models.BigIntegerField( - help_text=( - "The channel ID that this message was " - "sent in, taken from Discord." - ), - validators=( - MinValueValidator( - limit_value=0, - message="Channel IDs cannot be negative." - ), - ) - ) - content = models.CharField( - max_length=1500, - help_text="The content that the user wants to be reminded of." - ) - expiration = models.DateTimeField( - help_text="When this reminder should be sent." - ) - mentions = ArrayField( - models.BigIntegerField( - validators=( - MinValueValidator( - limit_value=0, - message="Mention IDs cannot be negative." - ), - ) - ), - default=list, - blank=True, - help_text="IDs of roles or users to ping with the reminder." - ) - failures = models.IntegerField( - default=0, - help_text="Number of times we attempted to send the reminder and failed." - ) - - def __str__(self): - """Returns some info on the current reminder, for display purposes.""" - return f"{self.content} on {self.expiration} by {self.author}" diff --git a/pydis_site/apps/api/models/bot/role.py b/pydis_site/apps/api/models/bot/role.py deleted file mode 100644 index 733a8e08..00000000 --- a/pydis_site/apps/api/models/bot/role.py +++ /dev/null @@ -1,69 +0,0 @@ -from __future__ import annotations - -from django.core.validators import MinValueValidator -from django.db import models - -from pydis_site.apps.api.models.mixins import ModelReprMixin - - -class Role(ModelReprMixin, models.Model): - """ - A role on our Discord server. - - The comparison operators <, <=, >, >=, ==, != act the same as they do with Role-objects of the - discord.py library, see https://discordpy.readthedocs.io/en/latest/api.html#discord.Role - """ - - id = models.BigIntegerField( - primary_key=True, - validators=( - MinValueValidator( - limit_value=0, - message="Role IDs cannot be negative." - ), - ), - help_text="The role ID, taken from Discord.", - verbose_name="ID" - ) - name = models.CharField( - max_length=100, - help_text="The role name, taken from Discord." - ) - colour = models.IntegerField( - validators=( - MinValueValidator( - limit_value=0, - message="Colour hex cannot be negative." - ), - ), - help_text="The integer value of the colour of this role from Discord." - ) - permissions = models.BigIntegerField( - validators=( - MinValueValidator( - limit_value=0, - message="Role permissions cannot be negative." - ), - ), - help_text="The integer value of the permission bitset of this role from Discord." - ) - position = models.IntegerField( - help_text="The position of the role in the role hierarchy of the Discord Guild." - ) - - def __str__(self) -> str: - """Returns the name of the current role, for display purposes.""" - return self.name - - def __lt__(self, other: Role) -> bool: - """Compares the roles based on their position in the role hierarchy of the guild.""" - return self.position < other.position - - def __le__(self, other: Role) -> bool: - """Compares the roles based on their position in the role hierarchy of the guild.""" - return self.position <= other.position - - class Meta: - """Set role ordering from highest to lowest position.""" - - ordering = ("-position",) diff --git a/pydis_site/apps/api/models/bot/user.py b/pydis_site/apps/api/models/bot/user.py deleted file mode 100644 index afc5ba1e..00000000 --- a/pydis_site/apps/api/models/bot/user.py +++ /dev/null @@ -1,88 +0,0 @@ -from django.contrib.postgres.fields import ArrayField -from django.core.exceptions import ValidationError -from django.core.validators import MaxValueValidator, MinValueValidator -from django.db import models - -from pydis_site.apps.api.models.bot.role import Role -from pydis_site.apps.api.models.mixins import ModelReprMixin - - -def _validate_existing_role(value: int) -> None: - """Validate that a role exists when given in to the user model.""" - role = Role.objects.filter(id=value) - - if not role: - raise ValidationError(f"Role with ID {value} does not exist") - - -class User(ModelReprMixin, models.Model): - """A Discord user.""" - - id = models.BigIntegerField( - primary_key=True, - validators=( - MinValueValidator( - limit_value=0, - message="User IDs cannot be negative." - ), - ), - verbose_name="ID", - help_text="The ID of this user, taken from Discord." - ) - name = models.CharField( - max_length=32, - help_text="The username, taken from Discord.", - ) - discriminator = models.PositiveSmallIntegerField( - validators=( - MaxValueValidator( - limit_value=9999, - message="Discriminators may not exceed `9999`." - ), - ), - help_text="The discriminator of this user, taken from Discord." - ) - roles = ArrayField( - models.BigIntegerField( - validators=( - MinValueValidator( - limit_value=0, - message="Role IDs cannot be negative." - ), - _validate_existing_role - ) - ), - default=list, - blank=True, - help_text="IDs of roles the user has on the server" - ) - in_guild = models.BooleanField( - default=True, - help_text="Whether this user is in our server.", - verbose_name="In Guild" - ) - - def __str__(self): - """Returns the name and discriminator for the current user, for display purposes.""" - return f"{self.name}#{self.discriminator:04d}" - - @property - def top_role(self) -> Role: - """ - Attribute that returns the user's top role. - - This will fall back to the Developers role if the user does not have any roles. - """ - roles = Role.objects.filter(id__in=self.roles) - if not roles: - return Role.objects.get(name="Developers") - return max(roles) - - @property - def username(self) -> str: - """ - Returns the display version with name and discriminator as a standard attribute. - - For usability in read-only fields such as Django Admin. - """ - return str(self) diff --git a/pydis_site/apps/api/models/mixins.py b/pydis_site/apps/api/models/mixins.py deleted file mode 100644 index 5d75b78b..00000000 --- a/pydis_site/apps/api/models/mixins.py +++ /dev/null @@ -1,31 +0,0 @@ -from operator import itemgetter - -from django.db import models - - -class ModelReprMixin: - """Mixin providing a `__repr__()` to display model class name and initialisation parameters.""" - - def __repr__(self): - """Returns the current model class name and initialisation parameters.""" - attributes = ' '.join( - f'{attribute}={value!r}' - for attribute, value in sorted( - self.__dict__.items(), - key=itemgetter(0) - ) - if not attribute.startswith('_') - ) - return f'<{self.__class__.__name__}({attributes})>' - - -class ModelTimestampMixin(models.Model): - """Mixin providing created_at and updated_at fields.""" - - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - """Metaconfig for the mixin.""" - - abstract = True diff --git a/pydis_site/apps/api/pagination.py b/pydis_site/apps/api/pagination.py deleted file mode 100644 index 61707d33..00000000 --- a/pydis_site/apps/api/pagination.py +++ /dev/null @@ -1,48 +0,0 @@ -from rest_framework.pagination import LimitOffsetPagination -from rest_framework.response import Response -from rest_framework.utils.serializer_helpers import ReturnList - - -class LimitOffsetPaginationExtended(LimitOffsetPagination): - """ - Extend LimitOffsetPagination to customise the default response. - - For example: - - ## Default response - >>> { - ... "count": 1, - ... "next": None, - ... "previous": None, - ... "results": [{ - ... "id": 6, - ... "inserted_at": "2021-01-26T21:13:35.477879Z", - ... "expires_at": None, - ... "active": False, - ... "user": 1, - ... "actor": 2, - ... "type": "warning", - ... "reason": null, - ... "hidden": false - ... }] - ... } - - ## Required response - >>> [{ - ... "id": 6, - ... "inserted_at": "2021-01-26T21:13:35.477879Z", - ... "expires_at": None, - ... "active": False, - ... "user": 1, - ... "actor": 2, - ... "type": "warning", - ... "reason": None, - ... "hidden": False - ... }] - """ - - default_limit = 100 - - def get_paginated_response(self, data: ReturnList) -> Response: - """Override to skip metadata i.e. `count`, `next`, and `previous`.""" - return Response(data) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py deleted file mode 100644 index e74ca102..00000000 --- a/pydis_site/apps/api/serializers.py +++ /dev/null @@ -1,455 +0,0 @@ -"""Converters from Django models to data interchange formats and back.""" -from django.db.models.query import QuerySet -from django.db.utils import IntegrityError -from rest_framework.exceptions import NotFound -from rest_framework.serializers import ( - IntegerField, - ListSerializer, - ModelSerializer, - PrimaryKeyRelatedField, - ValidationError -) -from rest_framework.settings import api_settings -from rest_framework.validators import UniqueTogetherValidator - -from .models import ( - AocAccountLink, - AocCompletionistBlock, - BotSetting, - BumpedThread, - DeletedMessage, - DocumentationLink, - FilterList, - Infraction, - MessageDeletionContext, - Nomination, - NominationEntry, - OffTopicChannelName, - OffensiveMessage, - Reminder, - Role, - User -) - - -class BotSettingSerializer(ModelSerializer): - """A class providing (de-)serialization of `BotSetting` instances.""" - - class Meta: - """Metadata defined for the Django REST Framework.""" - - model = BotSetting - fields = ('name', 'data') - - -class ListBumpedThreadSerializer(ListSerializer): - """Custom ListSerializer to override to_representation() when list views are triggered.""" - - def to_representation(self, objects: list[BumpedThread]) -> int: - """ - Used by the `ListModelMixin` to return just the list of bumped thread ids. - - Only the thread_id field is useful, hence it is unnecessary to create a nested dictionary. - - Additionally, this allows bumped thread routes to simply return an - array of thread_id ints instead of objects, saving on bandwidth. - """ - return [obj.thread_id for obj in objects] - - -class BumpedThreadSerializer(ModelSerializer): - """A class providing (de-)serialization of `BumpedThread` instances.""" - - class Meta: - """Metadata defined for the Django REST Framework.""" - - list_serializer_class = ListBumpedThreadSerializer - model = BumpedThread - fields = ('thread_id',) - - -class DeletedMessageSerializer(ModelSerializer): - """ - A class providing (de-)serialization of `DeletedMessage` instances. - - The serializer generally requires a valid `deletion_context` to be - given, which should be created beforehand. See the `DeletedMessage` - model for more information. - """ - - author = PrimaryKeyRelatedField( - queryset=User.objects.all() - ) - deletion_context = PrimaryKeyRelatedField( - queryset=MessageDeletionContext.objects.all(), - # This will be overridden in the `create` function - # of the deletion context serializer. - required=False - ) - - class Meta: - """Metadata defined for the Django REST Framework.""" - - model = DeletedMessage - fields = ( - 'id', 'author', - 'channel_id', 'content', - 'embeds', 'deletion_context', - 'attachments' - ) - - -class MessageDeletionContextSerializer(ModelSerializer): - """A class providing (de-)serialization of `MessageDeletionContext` instances.""" - - actor = PrimaryKeyRelatedField(queryset=User.objects.all(), allow_null=True) - deletedmessage_set = DeletedMessageSerializer(many=True) - - class Meta: - """Metadata defined for the Django REST Framework.""" - - model = MessageDeletionContext - fields = ('actor', 'creation', 'id', 'deletedmessage_set') - depth = 1 - - def create(self, validated_data: dict) -> MessageDeletionContext: - """ - Return a `MessageDeletionContext` based on the given data. - - In addition to the normal attributes expected by the `MessageDeletionContext` model - itself, this serializer also allows for passing the `deletedmessage_set` element - which contains messages that were deleted as part of this context. - """ - messages = validated_data.pop('deletedmessage_set') - deletion_context = MessageDeletionContext.objects.create(**validated_data) - for message in messages: - DeletedMessage.objects.create( - deletion_context=deletion_context, - **message - ) - - return deletion_context - - -class DocumentationLinkSerializer(ModelSerializer): - """A class providing (de-)serialization of `DocumentationLink` instances.""" - - class Meta: - """Metadata defined for the Django REST Framework.""" - - model = DocumentationLink - fields = ('package', 'base_url', 'inventory_url') - - -class FilterListSerializer(ModelSerializer): - """A class providing (de-)serialization of `FilterList` instances.""" - - class Meta: - """Metadata defined for the Django REST Framework.""" - - model = FilterList - fields = ('id', 'created_at', 'updated_at', 'type', 'allowed', 'content', 'comment') - - # This validator ensures only one filterlist with the - # same content can exist. This means that we cannot have both an allow - # and a deny for the same item, and we cannot have duplicates of the - # same item. - validators = [ - UniqueTogetherValidator( - queryset=FilterList.objects.all(), - fields=['content', 'type'], - message=( - "A filterlist for this item already exists. " - "Please note that you cannot add the same item to both allow and deny." - ) - ), - ] - - -class InfractionSerializer(ModelSerializer): - """A class providing (de-)serialization of `Infraction` instances.""" - - class Meta: - """Metadata defined for the Django REST Framework.""" - - model = Infraction - fields = ( - 'id', - 'inserted_at', - 'last_applied', - 'expires_at', - 'active', - 'user', - 'actor', - 'type', - 'reason', - 'hidden', - 'dm_sent', - 'jump_url' - ) - - def validate(self, attrs: dict) -> dict: - """Validate data constraints for the given data and abort if it is invalid.""" - infr_type = attrs.get('type') - - active = attrs.get('active') - if active and infr_type in ('note', 'warning', 'kick'): - raise ValidationError({'active': [f'{infr_type} infractions cannot be active.']}) - - expires_at = attrs.get('expires_at') - if expires_at and infr_type in ('kick', 'warning'): - raise ValidationError({'expires_at': [f'{infr_type} infractions cannot expire.']}) - - hidden = attrs.get('hidden') - if hidden and infr_type in ('superstar', 'warning', 'voice_ban', 'voice_mute'): - raise ValidationError({'hidden': [f'{infr_type} infractions cannot be hidden.']}) - - if not hidden and infr_type in ('note', ): - raise ValidationError({'hidden': [f'{infr_type} infractions must be hidden.']}) - - return attrs - - -class ExpandedInfractionSerializer(InfractionSerializer): - """ - A class providing expanded (de-)serialization of `Infraction` instances. - - In addition to the fields of `Infraction` objects themselves, this - serializer also attaches the `user` and `actor` fields when serializing. - """ - - def to_representation(self, instance: Infraction) -> dict: - """Return the dictionary representation of this infraction.""" - ret = super().to_representation(instance) - - user = User.objects.get(id=ret['user']) - user_data = UserSerializer(user).data - ret['user'] = user_data - - actor = User.objects.get(id=ret['actor']) - actor_data = UserSerializer(actor).data - ret['actor'] = actor_data - - return ret - - -class OffTopicChannelNameListSerializer(ListSerializer): - """Custom ListSerializer to override to_representation() when list views are triggered.""" - - def to_representation(self, objects: list[OffTopicChannelName]) -> list[str]: - """ - Return a list with all `OffTopicChannelName`s in the database. - - This returns the list of off topic channel names. We want to only return - the name attribute, hence it is unnecessary to create a nested dictionary. - Additionally, this allows off topic channel name routes to simply return an - array of names instead of objects, saving on bandwidth. - """ - return [obj.name for obj in objects] - - -class OffTopicChannelNameSerializer(ModelSerializer): - """A class providing (de-)serialization of `OffTopicChannelName` instances.""" - - class Meta: - """Metadata defined for the Django REST Framework.""" - - list_serializer_class = OffTopicChannelNameListSerializer - model = OffTopicChannelName - fields = ('name', 'used', 'active') - - -class ReminderSerializer(ModelSerializer): - """A class providing (de-)serialization of `Reminder` instances.""" - - author = PrimaryKeyRelatedField(queryset=User.objects.all()) - - class Meta: - """Metadata defined for the Django REST Framework.""" - - model = Reminder - fields = ( - 'active', - 'author', - 'jump_url', - 'channel_id', - 'content', - 'expiration', - 'id', - 'mentions', - 'failures' - ) - - -class AocCompletionistBlockSerializer(ModelSerializer): - """A class providing (de-)serialization of `AocCompletionistBlock` instances.""" - - class Meta: - """Metadata defined for the Django REST Framework.""" - - model = AocCompletionistBlock - fields = ("user", "is_blocked", "reason") - - -class AocAccountLinkSerializer(ModelSerializer): - """A class providing (de-)serialization of `AocAccountLink` instances.""" - - class Meta: - """Metadata defined for the Django REST Framework.""" - - model = AocAccountLink - fields = ("user", "aoc_username") - - -class RoleSerializer(ModelSerializer): - """A class providing (de-)serialization of `Role` instances.""" - - class Meta: - """Metadata defined for the Django REST Framework.""" - - model = Role - fields = ('id', 'name', 'colour', 'permissions', 'position') - - -class UserListSerializer(ListSerializer): - """List serializer for User model to handle bulk updates.""" - - def create(self, validated_data: list) -> list: - """Override create method to optimize django queries.""" - new_users = [] - seen = set() - - for user_dict in validated_data: - if user_dict["id"] in seen: - raise ValidationError( - {"id": [f"User with ID {user_dict['id']} given multiple times."]} - ) - seen.add(user_dict["id"]) - new_users.append(User(**user_dict)) - - User.objects.bulk_create(new_users, ignore_conflicts=True) - return [] - - def update(self, queryset: QuerySet, validated_data: list) -> list: - """ - Override update method to support bulk updates. - - ref:https://www.django-rest-framework.org/api-guide/serializers/#customizing-multiple-update - """ - object_ids = set() - - for data in validated_data: - try: - if data["id"] in object_ids: - # If request data contains users with same ID. - raise ValidationError( - {"id": [f"User with ID {data['id']} given multiple times."]} - ) - except KeyError: - # If user ID not provided in request body. - raise ValidationError( - {"id": ["This field is required."]} - ) - object_ids.add(data["id"]) - - # filter queryset - filtered_instances = queryset.filter(id__in=object_ids) - - instance_mapping = {user.id: user for user in filtered_instances} - - updated = [] - fields_to_update = set() - for user_data in validated_data: - for key in user_data: - fields_to_update.add(key) - - try: - user = instance_mapping[user_data["id"]] - except KeyError: - raise NotFound({"detail": f"User with id {user_data['id']} not found."}) - - user.__dict__.update(user_data) - updated.append(user) - - fields_to_update.remove("id") - - if not fields_to_update: - # Raise ValidationError when only id field is given. - raise ValidationError( - {api_settings.NON_FIELD_ERRORS_KEY: ["Insufficient data provided."]} - ) - - User.objects.bulk_update(updated, fields_to_update) - return updated - - -class UserSerializer(ModelSerializer): - """A class providing (de-)serialization of `User` instances.""" - - # ID field must be explicitly set as the default id field is read-only. - id = IntegerField(min_value=0) - - class Meta: - """Metadata defined for the Django REST Framework.""" - - model = User - fields = ('id', 'name', 'discriminator', 'roles', 'in_guild') - depth = 1 - list_serializer_class = UserListSerializer - - def create(self, validated_data: dict) -> User: - """Override create method to catch IntegrityError.""" - try: - return super().create(validated_data) - except IntegrityError: - raise ValidationError({"id": ["User with ID already present."]}) - - -class NominationEntrySerializer(ModelSerializer): - """A class providing (de-)serialization of `NominationEntry` instances.""" - - # We need to define it here, because we don't want that nomination ID - # return inside nomination response entry, because ID is already available - # as top-level field. Queryset is required if field is not read only. - nomination = PrimaryKeyRelatedField( - queryset=Nomination.objects.all(), - write_only=True - ) - - class Meta: - """Metadata defined for the Django REST framework.""" - - model = NominationEntry - fields = ('nomination', 'actor', 'reason', 'inserted_at') - - -class NominationSerializer(ModelSerializer): - """A class providing (de-)serialization of `Nomination` instances.""" - - entries = NominationEntrySerializer(many=True, read_only=True) - - class Meta: - """Metadata defined for the Django REST Framework.""" - - model = Nomination - fields = ( - 'id', - 'active', - 'user', - 'inserted_at', - 'end_reason', - 'ended_at', - 'reviewed', - 'entries', - 'thread_id' - ) - - -class OffensiveMessageSerializer(ModelSerializer): - """A class providing (de-)serialization of `OffensiveMessage` instances.""" - - class Meta: - """Metadata defined for the Django REST Framework.""" - - model = OffensiveMessage - fields = ('id', 'channel_id', 'delete_date') diff --git a/pydis_site/apps/api/signals.py b/pydis_site/apps/api/signals.py deleted file mode 100644 index 5c26bfb6..00000000 --- a/pydis_site/apps/api/signals.py +++ /dev/null @@ -1,12 +0,0 @@ -from django.db.models.signals import post_delete -from django.dispatch import receiver - -from pydis_site.apps.api.models.bot import Role, User - - -@receiver(signal=post_delete, sender=Role) -def delete_role_from_user(sender: Role, instance: Role, **kwargs) -> None: - """Unassigns the Role (instance) that is being deleted from every user that has it.""" - for user in User.objects.filter(roles__contains=[instance.id]): - del user.roles[user.roles.index(instance.id)] - user.save() diff --git a/pydis_site/apps/api/tests/__init__.py b/pydis_site/apps/api/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pydis_site/apps/api/tests/__init__.py +++ /dev/null diff --git a/pydis_site/apps/api/tests/base.py b/pydis_site/apps/api/tests/base.py deleted file mode 100644 index c9f3cb7e..00000000 --- a/pydis_site/apps/api/tests/base.py +++ /dev/null @@ -1,66 +0,0 @@ -from django.contrib.auth.models import User -from rest_framework.test import APITestCase - - -test_user, _created = User.objects.get_or_create( - username='test', - email='[email protected]', - password='testpass', - is_superuser=True, - is_staff=True -) - - -class AuthenticatedAPITestCase(APITestCase): - """ - Configures the test client. - - This ensures that it uses the proper subdomain for requests, and forces authentication - for the test user. - - The test user is considered staff and superuser. - If you want to test for a custom user (for example, to test model permissions), - create the user, assign the relevant permissions, and use - `self.client.force_authenticate(user=created_user)` to force authentication - through the created user. - - Using this performs the following nicety for you which eases writing tests: - - forcing authentication for the test user. - If you don't want to force authentication (for example, to test a route's response - for an unauthenticated user), un-force authentication by using the following: - - >>> from pydis_site.apps.api.tests.base import AuthenticatedAPITestCase - >>> class UnauthedUserTestCase(AuthenticatedAPITestCase): - ... def setUp(self): - ... super().setUp() - ... self.client.force_authentication(user=None) - ... def test_can_read_objects_at_my_endpoint(self): - ... resp = self.client.get('/my-publicly-readable-endpoint') - ... self.assertEqual(resp.status_code, 200) - ... def test_cannot_delete_objects_at_my_endpoint(self): - ... resp = self.client.delete('/my-publicly-readable-endpoint/42') - ... self.assertEqual(resp.status_code, 401) - - ## Example - Using this in a test case is rather straightforward: - - >>> from pydis_site.apps.api.tests.base import AuthenticatedAPITestCase - >>> class MyAPITestCase(AuthenticatedAPITestCase): - ... def test_that_it_works(self): - ... response = self.client.get('/my-endpoint') - ... self.assertEqual(response.status_code, 200) - - To reverse URLs of the API host, you need to use `django.urls`: - - >>> from django.urls import reverse - >>> from pydis_site.apps.api.tests.base import AuthenticatedAPITestCase - >>> class MyReversedTestCase(AuthenticatedAPITestCase): - ... def test_my_endpoint(self): - ... url = reverse('api:user-detail') - ... response = self.client.get(url) - ... self.assertEqual(response.status_code, 200) - """ - - def setUp(self): - super().setUp() - self.client.force_authenticate(test_user) diff --git a/pydis_site/apps/api/tests/test_bumped_threads.py b/pydis_site/apps/api/tests/test_bumped_threads.py deleted file mode 100644 index 316e3f0b..00000000 --- a/pydis_site/apps/api/tests/test_bumped_threads.py +++ /dev/null @@ -1,63 +0,0 @@ -from django.urls import reverse - -from .base import AuthenticatedAPITestCase -from ..models import BumpedThread - - -class UnauthedBumpedThreadAPITests(AuthenticatedAPITestCase): - def setUp(self): - super().setUp() - self.client.force_authenticate(user=None) - - def test_detail_lookup_returns_401(self): - url = reverse('api:bot:bumpedthread-detail', args=(1,)) - response = self.client.get(url) - - self.assertEqual(response.status_code, 401) - - def test_list_returns_401(self): - url = reverse('api:bot:bumpedthread-list') - response = self.client.get(url) - - self.assertEqual(response.status_code, 401) - - def test_create_returns_401(self): - url = reverse('api:bot:bumpedthread-list') - response = self.client.post(url, {"thread_id": 3}) - - self.assertEqual(response.status_code, 401) - - def test_delete_returns_401(self): - url = reverse('api:bot:bumpedthread-detail', args=(1,)) - response = self.client.delete(url) - - self.assertEqual(response.status_code, 401) - - -class BumpedThreadAPITests(AuthenticatedAPITestCase): - @classmethod - def setUpTestData(cls): - cls.thread1 = BumpedThread.objects.create( - thread_id=1234, - ) - - def test_returns_bumped_threads_as_flat_list(self): - url = reverse('api:bot:bumpedthread-list') - - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), [1234]) - - def test_returns_204_for_existing_data(self): - url = reverse('api:bot:bumpedthread-detail', args=(1234,)) - - response = self.client.get(url) - self.assertEqual(response.status_code, 204) - self.assertEqual(response.content, b"") - - def test_returns_404_for_non_existing_data(self): - url = reverse('api:bot:bumpedthread-detail', args=(42,)) - - response = self.client.get(url) - self.assertEqual(response.status_code, 404) - self.assertEqual(response.json(), {"detail": "Not found."}) diff --git a/pydis_site/apps/api/tests/test_deleted_messages.py b/pydis_site/apps/api/tests/test_deleted_messages.py deleted file mode 100644 index 1eb535d8..00000000 --- a/pydis_site/apps/api/tests/test_deleted_messages.py +++ /dev/null @@ -1,99 +0,0 @@ -from datetime import datetime - -from django.urls import reverse -from django.utils import timezone - -from .base import AuthenticatedAPITestCase -from ..models import MessageDeletionContext, User - - -class DeletedMessagesWithoutActorTests(AuthenticatedAPITestCase): - @classmethod - def setUpTestData(cls): - cls.author = User.objects.create( - id=55, - name='Robbie Rotten', - discriminator=55, - ) - - cls.data = { - 'actor': None, - 'creation': datetime.utcnow().isoformat(), - 'deletedmessage_set': [ - { - 'author': cls.author.id, - 'id': 55, - 'channel_id': 5555, - 'content': "Terror Billy is a meanie", - 'embeds': [], - 'attachments': [] - }, - { - 'author': cls.author.id, - 'id': 56, - 'channel_id': 5555, - 'content': "If you purge this, you're evil", - 'embeds': [], - 'attachments': [] - } - ] - } - - def test_accepts_valid_data(self): - url = reverse('api:bot:messagedeletioncontext-list') - response = self.client.post(url, data=self.data) - self.assertEqual(response.status_code, 201) - [context] = MessageDeletionContext.objects.all() - self.assertIsNone(context.actor) - - -class DeletedMessagesWithActorTests(AuthenticatedAPITestCase): - @classmethod - def setUpTestData(cls): - cls.author = cls.actor = User.objects.create( - id=12904, - name='Joe Armstrong', - discriminator=1245, - ) - - cls.data = { - 'actor': cls.actor.id, - 'creation': datetime.utcnow().isoformat(), - 'deletedmessage_set': [ - { - 'author': cls.author.id, - 'id': 12903, - 'channel_id': 1824, - 'content': "I hate trailing commas", - 'embeds': [], - 'attachments': [] - }, - ] - } - - def test_accepts_valid_data_and_sets_actor(self): - url = reverse('api:bot:messagedeletioncontext-list') - response = self.client.post(url, data=self.data) - self.assertEqual(response.status_code, 201) - [context] = MessageDeletionContext.objects.all() - self.assertEqual(context.actor.id, self.actor.id) - - -class DeletedMessagesLogURLTests(AuthenticatedAPITestCase): - @classmethod - def setUpTestData(cls): - cls.author = cls.actor = User.objects.create( - id=324888, - name='Black Knight', - discriminator=1975, - ) - - cls.deletion_context = MessageDeletionContext.objects.create( - actor=cls.actor, - creation=timezone.now() - ) - - def test_valid_log_url(self): - expected_url = reverse('staff:logs', args=(1,)) - [context] = MessageDeletionContext.objects.all() - self.assertEqual(context.log_url, expected_url) diff --git a/pydis_site/apps/api/tests/test_documentation_links.py b/pydis_site/apps/api/tests/test_documentation_links.py deleted file mode 100644 index 4e238cbb..00000000 --- a/pydis_site/apps/api/tests/test_documentation_links.py +++ /dev/null @@ -1,172 +0,0 @@ -from django.urls import reverse - -from .base import AuthenticatedAPITestCase -from ..models import DocumentationLink - - -class UnauthedDocumentationLinkAPITests(AuthenticatedAPITestCase): - def setUp(self): - super().setUp() - self.client.force_authenticate(user=None) - - def test_detail_lookup_returns_401(self): - url = reverse('api:bot:documentationlink-detail', args=('whatever',)) - response = self.client.get(url) - - self.assertEqual(response.status_code, 401) - - def test_list_returns_401(self): - url = reverse('api:bot:documentationlink-list') - response = self.client.get(url) - - self.assertEqual(response.status_code, 401) - - def test_create_returns_401(self): - url = reverse('api:bot:documentationlink-list') - response = self.client.post(url, data={'hi': 'there'}) - - self.assertEqual(response.status_code, 401) - - def test_delete_returns_401(self): - url = reverse('api:bot:documentationlink-detail', args=('whatever',)) - response = self.client.delete(url) - - self.assertEqual(response.status_code, 401) - - -class EmptyDatabaseDocumentationLinkAPITests(AuthenticatedAPITestCase): - def test_detail_lookup_returns_404(self): - url = reverse('api:bot:documentationlink-detail', args=('whatever',)) - response = self.client.get(url) - - self.assertEqual(response.status_code, 404) - - def test_list_all_returns_empty_list(self): - url = reverse('api:bot:documentationlink-list') - response = self.client.get(url) - - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), []) - - def test_delete_returns_404(self): - url = reverse('api:bot:documentationlink-detail', args=('whatever',)) - response = self.client.delete(url) - - self.assertEqual(response.status_code, 404) - - -class DetailLookupDocumentationLinkAPITests(AuthenticatedAPITestCase): - @classmethod - def setUpTestData(cls): - cls.doc_link = DocumentationLink.objects.create( - package='testpackage', - base_url='https://example.com/', - inventory_url='https://example.com' - ) - - cls.doc_json = { - 'package': cls.doc_link.package, - 'base_url': cls.doc_link.base_url, - 'inventory_url': cls.doc_link.inventory_url - } - - def test_detail_lookup_unknown_package_returns_404(self): - url = reverse('api:bot:documentationlink-detail', args=('whatever',)) - response = self.client.get(url) - - self.assertEqual(response.status_code, 404) - - def test_detail_lookup_created_package_returns_package(self): - url = reverse('api:bot:documentationlink-detail', args=(self.doc_link.package,)) - response = self.client.get(url) - - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), self.doc_json) - - def test_list_all_packages_shows_created_package(self): - url = reverse('api:bot:documentationlink-list') - response = self.client.get(url) - - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), [self.doc_json]) - - def test_create_invalid_body_returns_400(self): - url = reverse('api:bot:documentationlink-list') - response = self.client.post(url, data={'i': 'am', 'totally': 'valid'}) - - self.assertEqual(response.status_code, 400) - - def test_create_invalid_url_returns_400(self): - body = { - 'package': 'example', - 'base_url': 'https://example.com', - 'inventory_url': 'totally an url' - } - - url = reverse('api:bot:documentationlink-list') - response = self.client.post(url, data=body) - - self.assertEqual(response.status_code, 400) - - def test_create_invalid_package_name_returns_400(self): - test_cases = ("InvalidPackage", "invalid package", "i\u0150valid") - for case in test_cases: - with self.subTest(package_name=case): - body = self.doc_json.copy() - body['package'] = case - url = reverse('api:bot:documentationlink-list') - response = self.client.post(url, data=body) - - self.assertEqual(response.status_code, 400) - - -class DocumentationLinkCreationTests(AuthenticatedAPITestCase): - def setUp(self): - super().setUp() - - self.body = { - 'package': 'example', - 'base_url': 'https://example.com/', - 'inventory_url': 'https://docs.example.com' - } - - url = reverse('api:bot:documentationlink-list') - response = self.client.post(url, data=self.body) - - self.assertEqual(response.status_code, 201) - - def test_package_in_full_list(self): - url = reverse('api:bot:documentationlink-list') - response = self.client.get(url) - - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), [self.body]) - - def test_detail_lookup_works_with_package(self): - url = reverse('api:bot:documentationlink-detail', args=(self.body['package'],)) - response = self.client.get(url) - - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), self.body) - - -class DocumentationLinkDeletionTests(AuthenticatedAPITestCase): - @classmethod - def setUpTestData(cls): - cls.doc_link = DocumentationLink.objects.create( - package='example', - base_url='https://example.com', - inventory_url='https://docs.example.com' - ) - - def test_unknown_package_returns_404(self): - url = reverse('api:bot:documentationlink-detail', args=('whatever',)) - response = self.client.delete(url) - - self.assertEqual(response.status_code, 404) - - def test_delete_known_package_returns_204(self): - url = reverse('api:bot:documentationlink-detail', args=(self.doc_link.package,)) - response = self.client.delete(url) - - self.assertEqual(response.status_code, 204) diff --git a/pydis_site/apps/api/tests/test_filterlists.py b/pydis_site/apps/api/tests/test_filterlists.py deleted file mode 100644 index 9959617e..00000000 --- a/pydis_site/apps/api/tests/test_filterlists.py +++ /dev/null @@ -1,122 +0,0 @@ -from django.urls import reverse - -from pydis_site.apps.api.models import FilterList -from pydis_site.apps.api.tests.base import AuthenticatedAPITestCase - -URL = reverse('api:bot:filterlist-list') -JPEG_ALLOWLIST = { - "type": 'FILE_FORMAT', - "allowed": True, - "content": ".jpeg", -} -PNG_ALLOWLIST = { - "type": 'FILE_FORMAT', - "allowed": True, - "content": ".png", -} - - -class UnauthenticatedTests(AuthenticatedAPITestCase): - def setUp(self): - super().setUp() - self.client.force_authenticate(user=None) - - def test_cannot_read_allowedlist_list(self): - response = self.client.get(URL) - - self.assertEqual(response.status_code, 401) - - -class EmptyDatabaseTests(AuthenticatedAPITestCase): - @classmethod - def setUpTestData(cls): - FilterList.objects.all().delete() - - def test_returns_empty_object(self): - response = self.client.get(URL) - - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), []) - - -class FetchTests(AuthenticatedAPITestCase): - @classmethod - def setUpTestData(cls): - FilterList.objects.all().delete() - cls.jpeg_format = FilterList.objects.create(**JPEG_ALLOWLIST) - cls.png_format = FilterList.objects.create(**PNG_ALLOWLIST) - - def test_returns_name_in_list(self): - response = self.client.get(URL) - - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()[0]["content"], self.jpeg_format.content) - self.assertEqual(response.json()[1]["content"], self.png_format.content) - - def test_returns_single_item_by_id(self): - response = self.client.get(f'{URL}/{self.jpeg_format.id}') - - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json().get("content"), self.jpeg_format.content) - - def test_returns_filter_list_types(self): - response = self.client.get(f'{URL}/get-types') - - self.assertEqual(response.status_code, 200) - for api_type, model_type in zip(response.json(), FilterList.FilterListType.choices): - self.assertEqual(api_type[0], model_type[0]) - self.assertEqual(api_type[1], model_type[1]) - - -class CreationTests(AuthenticatedAPITestCase): - @classmethod - def setUpTestData(cls): - FilterList.objects.all().delete() - - def test_returns_400_for_missing_params(self): - no_type_json = { - "allowed": True, - "content": ".jpeg" - } - no_allowed_json = { - "type": "FILE_FORMAT", - "content": ".jpeg" - } - no_content_json = { - "allowed": True, - "type": "FILE_FORMAT" - } - cases = [{}, no_type_json, no_allowed_json, no_content_json] - - for case in cases: - with self.subTest(case=case): - response = self.client.post(URL, data=case) - self.assertEqual(response.status_code, 400) - - def test_returns_201_for_successful_creation(self): - response = self.client.post(URL, data=JPEG_ALLOWLIST) - self.assertEqual(response.status_code, 201) - - def test_returns_400_for_duplicate_creation(self): - self.client.post(URL, data=JPEG_ALLOWLIST) - response = self.client.post(URL, data=JPEG_ALLOWLIST) - self.assertEqual(response.status_code, 400) - - -class DeletionTests(AuthenticatedAPITestCase): - @classmethod - def setUpTestData(cls): - FilterList.objects.all().delete() - cls.jpeg_format = FilterList.objects.create(**JPEG_ALLOWLIST) - cls.png_format = FilterList.objects.create(**PNG_ALLOWLIST) - - def test_deleting_unknown_id_returns_404(self): - response = self.client.delete(f"{URL}/200") - self.assertEqual(response.status_code, 404) - - def test_deleting_known_id_returns_204(self): - response = self.client.delete(f"{URL}/{self.jpeg_format.id}") - self.assertEqual(response.status_code, 204) - - response = self.client.get(f"{URL}/{self.jpeg_format.id}") - self.assertNotIn(self.png_format.content, response.json()) diff --git a/pydis_site/apps/api/tests/test_github_utils.py b/pydis_site/apps/api/tests/test_github_utils.py deleted file mode 100644 index 95bafec0..00000000 --- a/pydis_site/apps/api/tests/test_github_utils.py +++ /dev/null @@ -1,286 +0,0 @@ -import dataclasses -import datetime -import typing -import unittest -from unittest import mock - -import django.test -import httpx -import jwt -import rest_framework.response -import rest_framework.test -from django.urls import reverse - -from pydis_site import settings -from .. import github_utils - - -class GeneralUtilityTests(unittest.TestCase): - """Test the utility methods which do not fit in another class.""" - - def test_token_generation(self): - """Test that the a valid JWT token is generated.""" - def encode(payload: dict, _: str, algorithm: str, *args, **kwargs) -> str: - """ - Intercept the encode method. - - The result is encoded with an algorithm which does not require a PEM key, as it may - not be available in testing environments. - """ - self.assertEqual("RS256", algorithm, "The GitHub App JWT must be signed using RS256.") - return original_encode( - payload, "secret-encoding-key", *args, algorithm="HS256", **kwargs - ) - - original_encode = jwt.encode - with mock.patch("jwt.encode", new=encode): - token = github_utils.generate_token() - decoded = jwt.decode(token, "secret-encoding-key", algorithms=["HS256"]) - - delta = datetime.timedelta(minutes=10) - self.assertAlmostEqual(decoded["exp"] - decoded["iat"], delta.total_seconds()) - self.assertLess(decoded["exp"], (datetime.datetime.now() + delta).timestamp()) - - -class CheckRunTests(unittest.TestCase): - """Tests the check_run_status utility.""" - - run_kwargs: typing.Mapping = { - "name": "run_name", - "head_sha": "sha", - "status": "completed", - "conclusion": "success", - "created_at": datetime.datetime.utcnow().strftime(settings.GITHUB_TIMESTAMP_FORMAT), - "artifacts_url": "url", - } - - def test_completed_run(self): - """Test that an already completed run returns the correct URL.""" - final_url = "some_url_string_1234" - - kwargs = dict(self.run_kwargs, artifacts_url=final_url) - result = github_utils.check_run_status(github_utils.WorkflowRun(**kwargs)) - self.assertEqual(final_url, result) - - def test_pending_run(self): - """Test that a pending run raises the proper exception.""" - kwargs = dict(self.run_kwargs, status="pending") - with self.assertRaises(github_utils.RunPendingError): - github_utils.check_run_status(github_utils.WorkflowRun(**kwargs)) - - def test_timeout_error(self): - """Test that a timeout is declared after a certain duration.""" - kwargs = dict(self.run_kwargs, status="pending") - # Set the creation time to well before the MAX_RUN_TIME - # to guarantee the right conclusion - kwargs["created_at"] = ( - datetime.datetime.utcnow() - github_utils.MAX_RUN_TIME - datetime.timedelta(minutes=10) - ).strftime(settings.GITHUB_TIMESTAMP_FORMAT) - - with self.assertRaises(github_utils.RunTimeoutError): - github_utils.check_run_status(github_utils.WorkflowRun(**kwargs)) - - def test_failed_run(self): - """Test that a failed run raises the proper exception.""" - kwargs = dict(self.run_kwargs, conclusion="failed") - with self.assertRaises(github_utils.ActionFailedError): - github_utils.check_run_status(github_utils.WorkflowRun(**kwargs)) - - -def get_response_authorize(_: httpx.Client, request: httpx.Request, **__) -> httpx.Response: - """ - Helper method for the authorize tests. - - Requests are intercepted before being sent out, and the appropriate responses are returned. - """ - path = request.url.path - auth = request.headers.get("Authorization") - - if request.method == "GET": - if path == "/app/installations": - if auth == "bearer JWT initial token": - return httpx.Response(200, request=request, json=[{ - "account": {"login": "VALID_OWNER"}, - "access_tokens_url": "https://example.com/ACCESS_TOKEN_URL" - }]) - else: - return httpx.Response( - 401, json={"error": "auth app/installations"}, request=request - ) - - elif path == "/installation/repositories": - if auth == "bearer app access token": - return httpx.Response(200, request=request, json={ - "repositories": [{ - "name": "VALID_REPO" - }] - }) - else: # pragma: no cover - return httpx.Response( - 401, json={"error": "auth installation/repositories"}, request=request - ) - - elif request.method == "POST": - if path == "/ACCESS_TOKEN_URL": - if auth == "bearer JWT initial token": - return httpx.Response(200, request=request, json={"token": "app access token"}) - else: # pragma: no cover - return httpx.Response(401, json={"error": "auth access_token"}, request=request) - - # Reaching this point means something has gone wrong - return httpx.Response(500, request=request) # pragma: no cover - - [email protected]("httpx.Client.send", new=get_response_authorize) [email protected](github_utils, "generate_token", new=mock.Mock(return_value="JWT initial token")) -class AuthorizeTests(unittest.TestCase): - """Test the authorize utility.""" - - def test_invalid_apps_auth(self): - """Test that an exception is raised if authorization was attempted with an invalid token.""" - with mock.patch.object(github_utils, "generate_token", return_value="Invalid token"): - with self.assertRaises(httpx.HTTPStatusError) as error: - github_utils.authorize("VALID_OWNER", "VALID_REPO") - - exception: httpx.HTTPStatusError = error.exception - self.assertEqual(401, exception.response.status_code) - self.assertEqual("auth app/installations", exception.response.json()["error"]) - - def test_missing_repo(self): - """Test that an exception is raised when the selected owner or repo are not available.""" - with self.assertRaises(github_utils.NotFoundError): - github_utils.authorize("INVALID_OWNER", "VALID_REPO") - with self.assertRaises(github_utils.NotFoundError): - github_utils.authorize("VALID_OWNER", "INVALID_REPO") - - def test_valid_authorization(self): - """Test that an accessible repository can be accessed.""" - client = github_utils.authorize("VALID_OWNER", "VALID_REPO") - self.assertEqual("bearer app access token", client.headers.get("Authorization")) - - -class ArtifactFetcherTests(unittest.TestCase): - """Test the get_artifact utility.""" - - @staticmethod - def get_response_get_artifact(request: httpx.Request, **_) -> httpx.Response: - """ - Helper method for the get_artifact tests. - - Requests are intercepted before being sent out, and the appropriate responses are returned. - """ - path = request.url.path - - if "force_error" in path: - return httpx.Response(404, request=request) - - if request.method == "GET": - if path == "/repos/owner/repo/actions/runs": - run = github_utils.WorkflowRun( - name="action_name", - head_sha="action_sha", - created_at=datetime.datetime.now().strftime(settings.GITHUB_TIMESTAMP_FORMAT), - status="completed", - conclusion="success", - artifacts_url="artifacts_url" - ) - return httpx.Response( - 200, request=request, json={"workflow_runs": [dataclasses.asdict(run)]} - ) - elif path == "/artifact_url": - return httpx.Response( - 200, request=request, json={"artifacts": [{ - "name": "artifact_name", - "archive_download_url": "artifact_download_url" - }]} - ) - elif path == "/artifact_download_url": - response = httpx.Response(302, request=request) - response.next_request = httpx.Request( - "GET", - httpx.URL("https://final_download.url") - ) - return response - - # Reaching this point means something has gone wrong - return httpx.Response(500, request=request) # pragma: no cover - - def setUp(self) -> None: - self.call_args = ["owner", "repo", "action_sha", "action_name", "artifact_name"] - self.client = httpx.Client(base_url="https://example.com") - - self.patchers = [ - mock.patch.object(self.client, "send", new=self.get_response_get_artifact), - mock.patch.object(github_utils, "authorize", return_value=self.client), - mock.patch.object(github_utils, "check_run_status", return_value="artifact_url"), - ] - - for patcher in self.patchers: - patcher.start() - - def tearDown(self) -> None: - for patcher in self.patchers: - patcher.stop() - - def test_client_closed_on_errors(self): - """Test that the client is terminated even if an error occurs at some point.""" - self.call_args[0] = "force_error" - with self.assertRaises(httpx.HTTPStatusError): - github_utils.get_artifact(*self.call_args) - self.assertTrue(self.client.is_closed) - - def test_missing(self): - """Test that an exception is raised if the requested artifact was not found.""" - cases = ( - "invalid sha", - "invalid action name", - "invalid artifact name", - ) - for i, name in enumerate(cases, 2): - with self.subTest(f"Test {name} raises an error"): - new_args = self.call_args.copy() - new_args[i] = name - - with self.assertRaises(github_utils.NotFoundError): - github_utils.get_artifact(*new_args) - - def test_valid(self): - """Test that the correct download URL is returned for valid requests.""" - url = github_utils.get_artifact(*self.call_args) - self.assertEqual("https://final_download.url", url) - self.assertTrue(self.client.is_closed) - - [email protected](github_utils, "get_artifact") -class GitHubArtifactViewTests(django.test.TestCase): - """Test the GitHub artifact fetch API view.""" - - def setUp(self): - self.kwargs = { - "owner": "test_owner", - "repo": "test_repo", - "sha": "test_sha", - "action_name": "test_action", - "artifact_name": "test_artifact", - } - self.url = reverse("api:github-artifacts", kwargs=self.kwargs) - - def test_correct_artifact(self, artifact_mock: mock.Mock): - """Test a proper response is returned with proper input.""" - artifact_mock.return_value = "final download url" - result = self.client.get(self.url) - - self.assertIsInstance(result, rest_framework.response.Response) - self.assertEqual({"url": artifact_mock.return_value}, result.data) - - def test_failed_fetch(self, artifact_mock: mock.Mock): - """Test that a proper error is returned when the request fails.""" - artifact_mock.side_effect = github_utils.NotFoundError("Test error message") - result = self.client.get(self.url) - - self.assertIsInstance(result, rest_framework.response.Response) - self.assertEqual({ - "error_type": github_utils.NotFoundError.__name__, - "error": "Test error message", - "requested_resource": "/".join(self.kwargs.values()) - }, result.data) diff --git a/pydis_site/apps/api/tests/test_healthcheck.py b/pydis_site/apps/api/tests/test_healthcheck.py deleted file mode 100644 index 650403ad..00000000 --- a/pydis_site/apps/api/tests/test_healthcheck.py +++ /dev/null @@ -1,16 +0,0 @@ -from django.urls import reverse - -from .base import AuthenticatedAPITestCase - - -class UnauthedHealthcheckAPITests(AuthenticatedAPITestCase): - def setUp(self): - super().setUp() - self.client.force_authenticate(user=None) - - def test_can_access_healthcheck_view(self): - url = reverse('api:healthcheck') - response = self.client.get(url) - - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), {'status': 'ok'}) diff --git a/pydis_site/apps/api/tests/test_infractions.py b/pydis_site/apps/api/tests/test_infractions.py deleted file mode 100644 index ceb5591b..00000000 --- a/pydis_site/apps/api/tests/test_infractions.py +++ /dev/null @@ -1,852 +0,0 @@ -import datetime -from datetime import datetime as dt, timedelta, timezone -from unittest.mock import patch -from urllib.parse import quote - -from django.db import transaction -from django.db.utils import IntegrityError -from django.urls import reverse - -from .base import AuthenticatedAPITestCase -from ..models import Infraction, User -from ..serializers import InfractionSerializer - - -class UnauthenticatedTests(AuthenticatedAPITestCase): - def setUp(self): - super().setUp() - self.client.force_authenticate(user=None) - - def test_detail_lookup_returns_401(self): - url = reverse('api:bot:infraction-detail', args=(6,)) - response = self.client.get(url) - - self.assertEqual(response.status_code, 401) - - def test_list_returns_401(self): - url = reverse('api:bot:infraction-list') - response = self.client.get(url) - - self.assertEqual(response.status_code, 401) - - def test_create_returns_401(self): - url = reverse('api:bot:infraction-list') - response = self.client.post(url, data={'reason': 'Have a nice day.'}) - - self.assertEqual(response.status_code, 401) - - def test_partial_update_returns_401(self): - url = reverse('api:bot:infraction-detail', args=(6,)) - response = self.client.patch(url, data={'reason': 'Have a nice day.'}) - - self.assertEqual(response.status_code, 401) - - -class InfractionTests(AuthenticatedAPITestCase): - @classmethod - def setUpTestData(cls): - cls.user = User.objects.create( - id=6, - name='james', - discriminator=1, - ) - cls.ban_hidden = Infraction.objects.create( - user_id=cls.user.id, - actor_id=cls.user.id, - type='ban', - reason='He terk my jerb!', - hidden=True, - inserted_at=dt(2020, 10, 10, 0, 0, 0, tzinfo=timezone.utc), - expires_at=dt(5018, 11, 20, 15, 52, tzinfo=timezone.utc), - active=True, - ) - cls.ban_inactive = Infraction.objects.create( - user_id=cls.user.id, - actor_id=cls.user.id, - type='ban', - reason='James is an ass, and we won\'t be working with him again.', - active=False, - inserted_at=dt(2020, 10, 10, 0, 1, 0, tzinfo=timezone.utc), - ) - cls.timeout_permanent = Infraction.objects.create( - user_id=cls.user.id, - actor_id=cls.user.id, - type='timeout', - reason='He has a filthy mouth and I am his soap.', - active=True, - inserted_at=dt(2020, 10, 10, 0, 2, 0, tzinfo=timezone.utc), - expires_at=None, - ) - cls.superstar_expires_soon = Infraction.objects.create( - user_id=cls.user.id, - actor_id=cls.user.id, - type='superstar', - reason='This one doesn\'t matter anymore.', - active=True, - inserted_at=dt(2020, 10, 10, 0, 3, 0, tzinfo=timezone.utc), - expires_at=dt.now(timezone.utc) + datetime.timedelta(hours=5), - ) - cls.voiceban_expires_later = Infraction.objects.create( - user_id=cls.user.id, - actor_id=cls.user.id, - type='voice_ban', - reason='Jet engine mic', - active=True, - inserted_at=dt(2020, 10, 10, 0, 4, 0, tzinfo=timezone.utc), - expires_at=dt.now(timezone.utc) + datetime.timedelta(days=5), - ) - - def test_list_all(self): - """Tests the list-view, which should be ordered by inserted_at (newest first).""" - url = reverse('api:bot:infraction-list') - response = self.client.get(url) - - self.assertEqual(response.status_code, 200) - infractions = response.json() - - self.assertEqual(len(infractions), 5) - self.assertEqual(infractions[0]['id'], self.voiceban_expires_later.id) - self.assertEqual(infractions[1]['id'], self.superstar_expires_soon.id) - self.assertEqual(infractions[2]['id'], self.timeout_permanent.id) - self.assertEqual(infractions[3]['id'], self.ban_inactive.id) - self.assertEqual(infractions[4]['id'], self.ban_hidden.id) - - def test_filter_search(self): - url = reverse('api:bot:infraction-list') - pattern = quote(r'^James(\s\w+){3},') - response = self.client.get(f'{url}?search={pattern}') - - self.assertEqual(response.status_code, 200) - infractions = response.json() - - self.assertEqual(len(infractions), 1) - self.assertEqual(infractions[0]['id'], self.ban_inactive.id) - - def test_filter_field(self): - url = reverse('api:bot:infraction-list') - response = self.client.get(f'{url}?type=ban&hidden=true') - - self.assertEqual(response.status_code, 200) - infractions = response.json() - - self.assertEqual(len(infractions), 1) - self.assertEqual(infractions[0]['id'], self.ban_hidden.id) - - def test_filter_permanent_false(self): - url = reverse('api:bot:infraction-list') - response = self.client.get(f'{url}?type=timeout&permanent=false') - - self.assertEqual(response.status_code, 200) - infractions = response.json() - - self.assertEqual(len(infractions), 0) - - def test_filter_permanent_true(self): - url = reverse('api:bot:infraction-list') - response = self.client.get(f'{url}?type=timeout&permanent=true') - - self.assertEqual(response.status_code, 200) - infractions = response.json() - - self.assertEqual(infractions[0]['id'], self.timeout_permanent.id) - - def test_filter_after(self): - url = reverse('api:bot:infraction-list') - target_time = datetime.datetime.utcnow() + datetime.timedelta(hours=5) - response = self.client.get(f'{url}?type=superstar&expires_after={target_time.isoformat()}') - - self.assertEqual(response.status_code, 200) - infractions = response.json() - self.assertEqual(len(infractions), 0) - - def test_filter_before(self): - url = reverse('api:bot:infraction-list') - target_time = datetime.datetime.utcnow() + datetime.timedelta(hours=5) - response = self.client.get(f'{url}?type=superstar&expires_before={target_time.isoformat()}') - - self.assertEqual(response.status_code, 200) - infractions = response.json() - self.assertEqual(len(infractions), 1) - self.assertEqual(infractions[0]['id'], self.superstar_expires_soon.id) - - def test_filter_after_invalid(self): - url = reverse('api:bot:infraction-list') - response = self.client.get(f'{url}?expires_after=gibberish') - - self.assertEqual(response.status_code, 400) - self.assertEqual(list(response.json())[0], "expires_after") - - def test_filter_before_invalid(self): - url = reverse('api:bot:infraction-list') - response = self.client.get(f'{url}?expires_before=000000000') - - self.assertEqual(response.status_code, 400) - self.assertEqual(list(response.json())[0], "expires_before") - - def test_after_before_before(self): - url = reverse('api:bot:infraction-list') - target_time = datetime.datetime.utcnow() + datetime.timedelta(hours=4) - target_time_late = datetime.datetime.utcnow() + datetime.timedelta(hours=6) - response = self.client.get( - f'{url}?expires_before={target_time_late.isoformat()}' - f'&expires_after={target_time.isoformat()}' - ) - - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()), 1) - self.assertEqual(response.json()[0]["id"], self.superstar_expires_soon.id) - - def test_after_after_before_invalid(self): - url = reverse('api:bot:infraction-list') - target_time = datetime.datetime.utcnow() + datetime.timedelta(hours=5) - target_time_late = datetime.datetime.utcnow() + datetime.timedelta(hours=9) - response = self.client.get( - f'{url}?expires_before={target_time.isoformat()}' - f'&expires_after={target_time_late.isoformat()}' - ) - - self.assertEqual(response.status_code, 400) - errors = list(response.json()) - self.assertIn("expires_before", errors) - self.assertIn("expires_after", errors) - - def test_permanent_after_invalid(self): - url = reverse('api:bot:infraction-list') - target_time = datetime.datetime.utcnow() + datetime.timedelta(hours=5) - response = self.client.get(f'{url}?permanent=true&expires_after={target_time.isoformat()}') - - self.assertEqual(response.status_code, 400) - errors = list(response.json()) - self.assertEqual("permanent", errors[0]) - - def test_permanent_before_invalid(self): - url = reverse('api:bot:infraction-list') - target_time = datetime.datetime.utcnow() + datetime.timedelta(hours=5) - response = self.client.get(f'{url}?permanent=true&expires_before={target_time.isoformat()}') - - self.assertEqual(response.status_code, 400) - errors = list(response.json()) - self.assertEqual("permanent", errors[0]) - - def test_nonpermanent_before(self): - url = reverse('api:bot:infraction-list') - target_time = datetime.datetime.utcnow() + datetime.timedelta(hours=6) - response = self.client.get( - f'{url}?permanent=false&expires_before={target_time.isoformat()}' - ) - - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()), 1) - self.assertEqual(response.json()[0]["id"], self.superstar_expires_soon.id) - - def test_filter_manytypes(self): - url = reverse('api:bot:infraction-list') - response = self.client.get(f'{url}?types=timeout,ban') - - self.assertEqual(response.status_code, 200) - infractions = response.json() - self.assertEqual(len(infractions), 3) - - def test_types_type_invalid(self): - url = reverse('api:bot:infraction-list') - response = self.client.get(f'{url}?types=timeout,ban&type=superstar') - - self.assertEqual(response.status_code, 400) - errors = list(response.json()) - self.assertEqual("types", errors[0]) - - def test_sort_expiresby(self): - url = reverse('api:bot:infraction-list') - response = self.client.get(f'{url}?ordering=expires_at&permanent=false') - self.assertEqual(response.status_code, 200) - infractions = response.json() - - self.assertEqual(len(infractions), 3) - self.assertEqual(infractions[0]['id'], self.superstar_expires_soon.id) - self.assertEqual(infractions[1]['id'], self.voiceban_expires_later.id) - self.assertEqual(infractions[2]['id'], self.ban_hidden.id) - - def test_returns_empty_for_no_match(self): - url = reverse('api:bot:infraction-list') - response = self.client.get(f'{url}?type=ban&search=poop') - - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()), 0) - - def test_ignores_bad_filters(self): - url = reverse('api:bot:infraction-list') - response = self.client.get(f'{url}?type=ban&hidden=maybe&foo=bar') - - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()), 2) - - def test_retrieve_single_from_id(self): - url = reverse('api:bot:infraction-detail', args=(self.ban_inactive.id,)) - response = self.client.get(url) - - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()['id'], self.ban_inactive.id) - - def test_retrieve_returns_404_for_absent_id(self): - url = reverse('api:bot:infraction-detail', args=(1337,)) - response = self.client.get(url) - - self.assertEqual(response.status_code, 404) - - def test_partial_update(self): - url = reverse('api:bot:infraction-detail', args=(self.ban_hidden.id,)) - data = { - 'expires_at': '4143-02-15T21:04:31+00:00', - 'active': False, - 'reason': 'durka derr' - } - - response = self.client.patch(url, data=data) - self.assertEqual(response.status_code, 200) - infraction = Infraction.objects.get(id=self.ban_hidden.id) - - # These fields were updated. - self.assertEqual(infraction.expires_at.isoformat(), data['expires_at']) - self.assertEqual(infraction.active, data['active']) - self.assertEqual(infraction.reason, data['reason']) - - # These fields are still the same. - self.assertEqual(infraction.id, self.ban_hidden.id) - self.assertEqual(infraction.inserted_at, self.ban_hidden.inserted_at) - self.assertEqual(infraction.user.id, self.ban_hidden.user.id) - self.assertEqual(infraction.actor.id, self.ban_hidden.actor.id) - self.assertEqual(infraction.type, self.ban_hidden.type) - self.assertEqual(infraction.hidden, self.ban_hidden.hidden) - - def test_partial_update_returns_400_for_frozen_field(self): - url = reverse('api:bot:infraction-detail', args=(self.ban_hidden.id,)) - data = {'user': 6} - - response = self.client.patch(url, data=data) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - 'user': ['This field cannot be updated.'] - }) - - -class CreationTests(AuthenticatedAPITestCase): - @classmethod - def setUpTestData(cls): - cls.user = User.objects.create( - id=5, - name='james', - discriminator=1, - ) - cls.second_user = User.objects.create( - id=6, - name='carl', - discriminator=2, - ) - - def test_accepts_valid_data(self): - url = reverse('api:bot:infraction-list') - data = { - 'user': self.user.id, - 'actor': self.user.id, - 'type': 'ban', - 'reason': 'He terk my jerb!', - 'hidden': True, - 'expires_at': '5018-11-20T15:52:00+00:00', - 'active': True, - } - - response = self.client.post(url, data=data) - self.assertEqual(response.status_code, 201) - - infraction = Infraction.objects.get(id=response.json()['id']) - self.assertAlmostEqual( - infraction.inserted_at, - dt.now(timezone.utc), - delta=timedelta(seconds=2) - ) - self.assertEqual(infraction.expires_at.isoformat(), data['expires_at']) - self.assertEqual(infraction.user.id, data['user']) - self.assertEqual(infraction.actor.id, data['actor']) - self.assertEqual(infraction.type, data['type']) - self.assertEqual(infraction.reason, data['reason']) - self.assertEqual(infraction.hidden, data['hidden']) - self.assertEqual(infraction.active, True) - - def test_returns_400_for_missing_user(self): - url = reverse('api:bot:infraction-list') - data = { - 'actor': self.user.id, - 'type': 'kick', - 'active': False, - } - - response = self.client.post(url, data=data) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - 'user': ['This field is required.'] - }) - - def test_returns_400_for_bad_user(self): - url = reverse('api:bot:infraction-list') - data = { - 'user': 1337, - 'actor': self.user.id, - 'type': 'kick', - 'active': True, - } - - response = self.client.post(url, data=data) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - 'user': ['Invalid pk "1337" - object does not exist.'] - }) - - def test_returns_400_for_bad_type(self): - url = reverse('api:bot:infraction-list') - data = { - 'user': self.user.id, - 'actor': self.user.id, - 'type': 'hug', - 'active': True, - } - - response = self.client.post(url, data=data) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - 'type': ['"hug" is not a valid choice.'] - }) - - def test_returns_400_for_bad_expired_at_format(self): - url = reverse('api:bot:infraction-list') - data = { - 'user': self.user.id, - 'actor': self.user.id, - 'type': 'ban', - 'expires_at': '20/11/5018 15:52:00', - 'active': True, - } - - response = self.client.post(url, data=data) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - 'expires_at': [ - 'Datetime has wrong format. Use one of these formats instead: ' - 'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].' - ] - }) - - def test_returns_400_for_expiring_non_expirable_type(self): - url = reverse('api:bot:infraction-list') - - for infraction_type in ('kick', 'warning'): - data = { - 'user': self.user.id, - 'actor': self.user.id, - 'type': infraction_type, - 'expires_at': '5018-11-20T15:52:00+00:00', - 'active': False, - } - - response = self.client.post(url, data=data) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - 'expires_at': [f'{data["type"]} infractions cannot expire.'] - }) - - def test_returns_400_for_hidden_non_hideable_type(self): - url = reverse('api:bot:infraction-list') - - for infraction_type in ('superstar', 'warning'): - data = { - 'user': self.user.id, - 'actor': self.user.id, - 'type': infraction_type, - 'hidden': True, - 'active': False, - } - - response = self.client.post(url, data=data) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - 'hidden': [f'{data["type"]} infractions cannot be hidden.'] - }) - - def test_returns_400_for_non_hidden_required_hidden_type(self): - url = reverse('api:bot:infraction-list') - - data = { - 'user': self.user.id, - 'actor': self.user.id, - 'type': 'note', - 'hidden': False, - 'active': False, - } - - response = self.client.post(url, data=data) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - 'hidden': [f'{data["type"]} infractions must be hidden.'] - }) - - def test_returns_400_for_active_infraction_of_type_that_cannot_be_active(self): - """Test if the API rejects active infractions for types that cannot be active.""" - url = reverse('api:bot:infraction-list') - restricted_types = ( - ('note', True), - ('warning', False), - ('kick', False), - ) - - for infraction_type, hidden in restricted_types: - # https://stackoverflow.com/a/23326971 - with self.subTest(infraction_type=infraction_type): - invalid_infraction = { - 'user': self.user.id, - 'actor': self.user.id, - 'type': infraction_type, - 'reason': 'Take me on!', - 'hidden': hidden, - 'active': True, - 'expires_at': None, - } - response = self.client.post(url, data=invalid_infraction) - self.assertEqual(response.status_code, 400) - self.assertEqual( - response.json(), - {'active': [f'{infraction_type} infractions cannot be active.']} - ) - - def test_returns_400_for_second_active_infraction_of_the_same_type(self): - """Test if the API rejects a second active infraction of the same type for a given user.""" - url = reverse('api:bot:infraction-list') - active_infraction_types = ('timeout', 'ban', 'superstar') - - for infraction_type in active_infraction_types: - with self.subTest(infraction_type=infraction_type): - with transaction.atomic(): - first_active_infraction = { - 'user': self.user.id, - 'actor': self.user.id, - 'type': infraction_type, - 'reason': 'Take me on!', - 'active': True, - 'expires_at': '2019-10-04T12:52:00+00:00' - } - - # Post the first active infraction of a type and confirm it's accepted. - first_response = self.client.post(url, data=first_active_infraction) - self.assertEqual(first_response.status_code, 201) - - second_active_infraction = { - 'user': self.user.id, - 'actor': self.user.id, - 'type': infraction_type, - 'reason': 'Take on me!', - 'active': True, - 'expires_at': '2019-10-04T12:52:00+00:00' - } - second_response = self.client.post(url, data=second_active_infraction) - self.assertEqual(second_response.status_code, 400) - self.assertEqual( - second_response.json(), - { - 'non_field_errors': [ - 'This user already has an active infraction of this type.' - ] - } - ) - - def test_returns_201_for_second_active_infraction_of_different_type(self): - """Test if the API accepts a second active infraction of a different type than the first.""" - url = reverse('api:bot:infraction-list') - first_active_infraction = { - 'user': self.user.id, - 'actor': self.user.id, - 'type': 'timeout', - 'reason': 'Be silent!', - 'hidden': True, - 'active': True, - 'expires_at': '2019-10-04T12:52:00+00:00' - } - second_active_infraction = { - 'user': self.user.id, - 'actor': self.user.id, - 'type': 'ban', - 'reason': 'Be gone!', - 'hidden': True, - 'active': True, - 'expires_at': '2019-10-05T12:52:00+00:00' - } - # Post the first active infraction of a type and confirm it's accepted. - first_response = self.client.post(url, data=first_active_infraction) - self.assertEqual(first_response.status_code, 201) - - # Post the first active infraction of a type and confirm it's accepted. - second_response = self.client.post(url, data=second_active_infraction) - self.assertEqual(second_response.status_code, 201) - - def test_unique_constraint_raises_integrity_error_on_second_active_of_same_type(self): - """Do we raise `IntegrityError` for the second active infraction of a type for a user?""" - Infraction.objects.create( - user=self.user, - actor=self.user, - type="ban", - active=True, - reason="The first active ban" - ) - with self.assertRaises(IntegrityError): - Infraction.objects.create( - user=self.user, - actor=self.user, - type="ban", - active=True, - reason="The second active ban" - ) - - def test_unique_constraint_accepts_active_infraction_after_inactive_infraction(self): - """Do we accept an active infraction if the others of the same type are inactive?""" - try: - Infraction.objects.create( - user=self.user, - actor=self.user, - type="ban", - active=False, - reason="The first inactive ban" - ) - Infraction.objects.create( - user=self.user, - actor=self.user, - type="ban", - active=False, - reason="The second inactive ban" - ) - Infraction.objects.create( - user=self.user, - actor=self.user, - type="ban", - active=True, - reason="The first active ban" - ) - except IntegrityError: - self.fail("An unexpected IntegrityError was raised.") - - @patch(f"{__name__}.Infraction") - def test_if_accepts_active_infraction_test_catches_integrity_error(self, infraction_patch): - """Does the test properly catch the IntegrityError and raise an AssertionError?""" - infraction_patch.objects.create.side_effect = IntegrityError - with self.assertRaises(AssertionError, msg="An unexpected IntegrityError was raised."): - self.test_unique_constraint_accepts_active_infraction_after_inactive_infraction() - - def test_unique_constraint_accepts_second_active_of_different_type(self): - """Do we accept a second active infraction of a different type for a given user?""" - Infraction.objects.create( - user=self.user, - actor=self.user, - type="ban", - active=True, - reason="The first active ban" - ) - Infraction.objects.create( - user=self.user, - actor=self.user, - type="timeout", - active=True, - reason="The first active timeout" - ) - - def test_unique_constraint_accepts_active_infractions_for_different_users(self): - """Do we accept two active infractions of the same type for two different users?""" - Infraction.objects.create( - user=self.user, - actor=self.user, - type="ban", - active=True, - reason="An active ban for the first user" - ) - Infraction.objects.create( - user=self.second_user, - actor=self.second_user, - type="ban", - active=False, - reason="An active ban for the second user" - ) - - def test_integrity_error_if_missing_active_field(self): - pattern = ( - 'null value in column "active" (of relation "api_infraction" )?' - 'violates not-null constraint' - ) - with self.assertRaisesRegex(IntegrityError, pattern): - Infraction.objects.create( - user=self.user, - actor=self.user, - type='ban', - reason='A reason.', - ) - - -class InfractionDeletionTests(AuthenticatedAPITestCase): - @classmethod - def setUpTestData(cls): - cls.user = User.objects.create( - id=9876, - name='Unknown user', - discriminator=9876, - ) - - cls.warning = Infraction.objects.create( - user_id=cls.user.id, - actor_id=cls.user.id, - type='warning', - active=False - ) - - def test_delete_unknown_infraction_returns_404(self): - url = reverse('api:bot:infraction-detail', args=('something',)) - response = self.client.delete(url) - - self.assertEqual(response.status_code, 404) - - def test_delete_known_infraction_returns_204(self): - url = reverse('api:bot:infraction-detail', args=(self.warning.id,)) - response = self.client.delete(url) - - self.assertEqual(response.status_code, 204) - self.assertRaises(Infraction.DoesNotExist, Infraction.objects.get, id=self.warning.id) - - -class ExpandedTests(AuthenticatedAPITestCase): - @classmethod - def setUpTestData(cls): - cls.user = User.objects.create( - id=5, - name='james', - discriminator=1, - ) - cls.kick = Infraction.objects.create( - user_id=cls.user.id, - actor_id=cls.user.id, - type='kick', - active=False - ) - cls.warning = Infraction.objects.create( - user_id=cls.user.id, - actor_id=cls.user.id, - type='warning', - active=False, - ) - - def check_expanded_fields(self, infraction): - for key in ('user', 'actor'): - obj = infraction[key] - for field in ('id', 'name', 'discriminator', 'roles', 'in_guild'): - self.assertTrue(field in obj, msg=f'field "{field}" missing from {key}') - - def test_list_expanded(self): - url = reverse('api:bot:infraction-list-expanded') - - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - response_data = response.json() - self.assertEqual(len(response_data), 2) - - for infraction in response_data: - self.check_expanded_fields(infraction) - - def test_create_expanded(self): - url = reverse('api:bot:infraction-list-expanded') - data = { - 'user': self.user.id, - 'actor': self.user.id, - 'type': 'warning', - 'active': False - } - - response = self.client.post(url, data=data) - self.assertEqual(response.status_code, 201) - - self.assertEqual(len(Infraction.objects.all()), 3) - self.check_expanded_fields(response.json()) - - def test_retrieve_expanded(self): - url = reverse('api:bot:infraction-detail-expanded', args=(self.warning.id,)) - - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - infraction = response.json() - self.assertEqual(infraction['id'], self.warning.id) - self.check_expanded_fields(infraction) - - def test_partial_update_expanded(self): - url = reverse('api:bot:infraction-detail-expanded', args=(self.kick.id,)) - data = {'active': False} - - response = self.client.patch(url, data=data) - self.assertEqual(response.status_code, 200) - - infraction = Infraction.objects.get(id=self.kick.id) - self.assertEqual(infraction.active, data['active']) - self.check_expanded_fields(response.json()) - - -class SerializerTests(AuthenticatedAPITestCase): - @classmethod - def setUpTestData(cls): - cls.user = User.objects.create( - id=5, - name='james', - discriminator=1, - ) - - def create_infraction(self, _type: str, active: bool): - return Infraction.objects.create( - user_id=self.user.id, - actor_id=self.user.id, - type=_type, - reason='A reason.', - expires_at=dt(5018, 11, 20, 15, 52, tzinfo=timezone.utc), - active=active - ) - - def test_is_valid_if_active_infraction_with_same_fields_exists(self): - self.create_infraction('ban', active=True) - instance = self.create_infraction('ban', active=False) - - data = {'reason': 'hello'} - serializer = InfractionSerializer(instance, data=data, partial=True) - - self.assertTrue(serializer.is_valid(), msg=serializer.errors) - - def test_is_valid_for_new_active_infraction(self): - self.create_infraction('ban', active=False) - - data = { - 'user': self.user.id, - 'actor': self.user.id, - 'type': 'ban', - 'reason': 'A reason.', - 'active': True - } - serializer = InfractionSerializer(data=data) - - self.assertTrue(serializer.is_valid(), msg=serializer.errors) - - def test_validation_error_if_missing_active_field(self): - data = { - 'user': self.user.id, - 'actor': self.user.id, - 'type': 'ban', - 'reason': 'A reason.', - } - serializer = InfractionSerializer(data=data) - - if not serializer.is_valid(): - self.assertIn('active', serializer.errors) - - code = serializer.errors['active'][0].code - msg = f'Expected failure on required active field but got {serializer.errors}' - self.assertEqual(code, 'required', msg=msg) - else: # pragma: no cover - self.fail('Validation unexpectedly succeeded.') diff --git a/pydis_site/apps/api/tests/test_models.py b/pydis_site/apps/api/tests/test_models.py deleted file mode 100644 index c07d59cd..00000000 --- a/pydis_site/apps/api/tests/test_models.py +++ /dev/null @@ -1,178 +0,0 @@ -from datetime import datetime as dt, timezone - -from django.core.exceptions import ValidationError -from django.test import SimpleTestCase, TestCase - -from pydis_site.apps.api.models import ( - DeletedMessage, - DocumentationLink, - Infraction, - MessageDeletionContext, - Nomination, - NominationEntry, - OffTopicChannelName, - OffensiveMessage, - Reminder, - Role, - User -) -from pydis_site.apps.api.models.mixins import ModelReprMixin - - -class SimpleClass(ModelReprMixin): - def __init__(self, is_what): - self.the_cake = is_what - - -class ReprMixinTests(SimpleTestCase): - def setUp(self): - self.klass = SimpleClass('is a lie') - - def test_shows_attributes(self): - expected = "<SimpleClass(the_cake='is a lie')>" - self.assertEqual(repr(self.klass), expected) - - -class NitroMessageLengthTest(TestCase): - def setUp(self): - self.user = User.objects.create(id=50, name='bill', discriminator=5) - self.context = MessageDeletionContext.objects.create( - id=50, - actor=self.user, - creation=dt.now(timezone.utc) - ) - - def test_create(self): - message = DeletedMessage( - id=46, - author=self.user, - channel_id=666, - content="w"*4000, - deletion_context=self.context, - embeds=[] - ) - - try: - message.clean_fields() - except Exception as e: # pragma: no cover - self.fail(f"Creation of message of length 3950 failed with: {e}") - - def test_create_failure(self): - message = DeletedMessage( - id=47, - author=self.user, - channel_id=666, - content="w"*4001, - deletion_context=self.context, - embeds=[] - ) - - self.assertRaisesRegex(ValidationError, "content':", message.clean_fields) - - -class StringDunderMethodTests(SimpleTestCase): - def setUp(self): - self.nomination = Nomination( - id=123, - user=User( - id=9876, - name="Hemlock's Cat", - discriminator=7777, - ), - ) - - self.objects = ( - DeletedMessage( - id=45, - author=User( - id=444, - name='bill', - discriminator=5, - ), - channel_id=666, - content="wooey", - deletion_context=MessageDeletionContext( - actor=User( - id=5555, - name='shawn', - discriminator=555, - ), - creation=dt.now(timezone.utc) - ), - embeds=[] - ), - DocumentationLink( - 'test', 'http://example.com', 'http://example.com' - ), - OffensiveMessage( - id=602951077675139072, - channel_id=291284109232308226, - delete_date=dt(3000, 1, 1) - ), - OffTopicChannelName(name='bob-the-builders-playground'), - Role( - id=5, name='test role', - colour=0x5, permissions=0, - position=10, - ), - MessageDeletionContext( - actor=User( - id=5555, - name='shawn', - discriminator=555, - ), - creation=dt.utcnow() - ), - User( - id=5, - name='bob', - discriminator=1, - ), - Infraction( - user_id=5, - actor_id=5, - type='kick', - reason='He terk my jerb!' - ), - Infraction( - user_id=5, - actor_id=5, - hidden=True, - type='kick', - reason='He terk my jerb!', - expires_at=dt(5018, 11, 20, 15, 52, tzinfo=timezone.utc) - ), - Reminder( - author=User( - id=452, - name='billy', - discriminator=5, - ), - channel_id=555, - jump_url=( - 'https://discordapp.com/channels/' - '267624335836053506/291284109232308226/463087129459949587' - ), - content="oh no", - expiration=dt(5018, 11, 20, 15, 52, tzinfo=timezone.utc) - ), - NominationEntry( - nomination_id=self.nomination.id, - actor=User( - id=9876, - name='Mr. Hemlock', - discriminator=6666, - ), - reason="He purrrrs like the best!", - ) - ) - - def test_returns_string(self): - for instance in self.objects: - self.assertIsInstance(str(instance), str) - - def test_nomination_str_representation(self): - self.assertEqual( - "Nomination of Hemlock's Cat#7777 (active)", - str(self.nomination) - ) diff --git a/pydis_site/apps/api/tests/test_nominations.py b/pydis_site/apps/api/tests/test_nominations.py deleted file mode 100644 index b3742cdd..00000000 --- a/pydis_site/apps/api/tests/test_nominations.py +++ /dev/null @@ -1,558 +0,0 @@ -from datetime import datetime as dt, timedelta, timezone - -from django.urls import reverse - -from .base import AuthenticatedAPITestCase -from ..models import Nomination, NominationEntry, User - - -class CreationTests(AuthenticatedAPITestCase): - @classmethod - def setUpTestData(cls): - cls.user = User.objects.create( - id=1234, - name='joe dart', - discriminator=1111, - ) - cls.user2 = User.objects.create( - id=9876, - name='Who?', - discriminator=1234 - ) - - def test_accepts_valid_data(self): - url = reverse('api:bot:nomination-list') - data = { - 'actor': self.user.id, - 'reason': 'Joe Dart on Fender Bass', - 'user': self.user.id, - } - - response = self.client.post(url, data=data) - self.assertEqual(response.status_code, 201) - - nomination = Nomination.objects.get(id=response.json()['id']) - nomination_entry = NominationEntry.objects.get( - nomination_id=nomination.id, - actor_id=self.user.id - ) - self.assertAlmostEqual( - nomination.inserted_at, - dt.now(timezone.utc), - delta=timedelta(seconds=2) - ) - self.assertEqual(nomination.user.id, data['user']) - self.assertEqual(nomination_entry.reason, data['reason']) - self.assertEqual(nomination.active, True) - - def test_returns_200_on_second_active_nomination_by_different_user(self): - url = reverse('api:bot:nomination-list') - first_data = { - 'actor': self.user.id, - 'reason': 'Joe Dart on Fender Bass', - 'user': self.user.id, - } - second_data = { - 'actor': self.user2.id, - 'reason': 'Great user', - 'user': self.user.id - } - - response1 = self.client.post(url, data=first_data) - self.assertEqual(response1.status_code, 201) - - response2 = self.client.post(url, data=second_data) - self.assertEqual(response2.status_code, 201) - - def test_returns_400_on_second_active_nomination_by_existing_nominator(self): - url = reverse('api:bot:nomination-list') - data = { - 'actor': self.user.id, - 'reason': 'Joe Dart on Fender Bass', - 'user': self.user.id, - } - - response1 = self.client.post(url, data=data) - self.assertEqual(response1.status_code, 201) - - response2 = self.client.post(url, data=data) - self.assertEqual(response2.status_code, 400) - self.assertEqual(response2.json(), { - 'actor': ['This actor has already endorsed this nomination.'] - }) - - def test_returns_400_for_missing_user(self): - url = reverse('api:bot:nomination-list') - data = { - 'actor': self.user.id, - 'reason': 'Joe Dart on Fender Bass', - } - - response = self.client.post(url, data=data) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - 'user': ['This field is required.'] - }) - - def test_returns_400_for_missing_actor(self): - url = reverse('api:bot:nomination-list') - data = { - 'user': self.user.id, - 'reason': 'Joe Dart on Fender Bass', - } - - response = self.client.post(url, data=data) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - 'actor': ['This field is required.'] - }) - - def test_returns_201_for_missing_reason(self): - url = reverse('api:bot:nomination-list') - data = { - 'user': self.user.id, - 'actor': self.user.id, - } - - response = self.client.post(url, data=data) - self.assertEqual(response.status_code, 201) - - def test_returns_400_for_bad_user(self): - url = reverse('api:bot:nomination-list') - data = { - 'user': 1024, - 'reason': 'Joe Dart on Fender Bass', - 'actor': self.user.id, - } - - response = self.client.post(url, data=data) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - 'user': ['Invalid pk "1024" - object does not exist.'] - }) - - def test_returns_400_for_bad_actor(self): - url = reverse('api:bot:nomination-list') - data = { - 'user': self.user.id, - 'reason': 'Joe Dart on Fender Bass', - 'actor': 1024, - } - - response = self.client.post(url, data=data) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - 'actor': ['Invalid pk "1024" - object does not exist.'] - }) - - def test_returns_400_for_end_reason_at_creation(self): - url = reverse('api:bot:nomination-list') - data = { - 'user': self.user.id, - 'reason': 'Joe Dart on Fender Bass', - 'actor': self.user.id, - 'end_reason': "Joe Dart on the Joe Dart Bass" - } - - response = self.client.post(url, data=data) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - 'end_reason': ['This field cannot be set at creation.'] - }) - - def test_returns_400_for_ended_at_at_creation(self): - url = reverse('api:bot:nomination-list') - data = { - 'user': self.user.id, - 'reason': 'Joe Dart on Fender Bass', - 'actor': self.user.id, - 'ended_at': "Joe Dart on the Joe Dart Bass" - } - - response = self.client.post(url, data=data) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - 'ended_at': ['This field cannot be set at creation.'] - }) - - def test_returns_400_for_inserted_at_at_creation(self): - url = reverse('api:bot:nomination-list') - data = { - 'user': self.user.id, - 'reason': 'Joe Dart on Fender Bass', - 'actor': self.user.id, - 'inserted_at': "Joe Dart on the Joe Dart Bass" - } - - response = self.client.post(url, data=data) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - 'inserted_at': ['This field cannot be set at creation.'] - }) - - def test_returns_400_for_active_at_creation(self): - url = reverse('api:bot:nomination-list') - data = { - 'user': self.user.id, - 'reason': 'Joe Dart on Fender Bass', - 'actor': self.user.id, - 'active': False - } - - response = self.client.post(url, data=data) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - 'active': ['This field cannot be set at creation.'] - }) - - -class NominationTests(AuthenticatedAPITestCase): - @classmethod - def setUpTestData(cls): - cls.user = User.objects.create( - id=1234, - name='joe dart', - discriminator=1111, - ) - - cls.active_nomination = Nomination.objects.create( - user=cls.user - ) - cls.active_nomination_entry = NominationEntry.objects.create( - nomination=cls.active_nomination, - actor=cls.user, - reason="He's pretty funky" - ) - cls.inactive_nomination = Nomination.objects.create( - user=cls.user, - active=False, - end_reason="His neck couldn't hold the funk", - ended_at="5018-11-20T15:52:00+00:00" - ) - cls.inactive_nomination_entry = NominationEntry.objects.create( - nomination=cls.inactive_nomination, - actor=cls.user, - reason="He's pretty funky" - ) - - def test_returns_200_update_reason_on_active_with_actor(self): - url = reverse('api:bot:nomination-detail', args=(self.active_nomination.id,)) - data = { - 'reason': "He's one funky duck", - 'actor': self.user.id - } - - response = self.client.patch(url, data=data) - self.assertEqual(response.status_code, 200) - - nomination_entry = NominationEntry.objects.get( - nomination_id=response.json()['id'], - actor_id=self.user.id - ) - self.assertEqual(nomination_entry.reason, data['reason']) - - def test_returns_400_on_frozen_field_update(self): - url = reverse('api:bot:nomination-detail', args=(self.active_nomination.id,)) - data = { - 'user': "Theo Katzman" - } - - response = self.client.patch(url, data=data) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - 'user': ['This field cannot be updated.'] - }) - - def test_returns_400_update_end_reason_on_active(self): - url = reverse('api:bot:nomination-detail', args=(self.active_nomination.id,)) - data = { - 'end_reason': 'He started playing jazz' - } - - response = self.client.patch(url, data=data) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - 'end_reason': ["An active nomination can't have an end reason."] - }) - - def test_returns_200_update_reason_on_inactive(self): - url = reverse('api:bot:nomination-detail', args=(self.inactive_nomination.id,)) - data = { - 'reason': "He's one funky duck", - 'actor': self.user.id - } - - response = self.client.patch(url, data=data) - self.assertEqual(response.status_code, 200) - - nomination_entry = NominationEntry.objects.get( - nomination_id=response.json()['id'], - actor_id=self.user.id - ) - self.assertEqual(nomination_entry.reason, data['reason']) - - def test_returns_200_update_end_reason_on_inactive(self): - url = reverse('api:bot:nomination-detail', args=(self.inactive_nomination.id,)) - data = { - 'end_reason': 'He started playing jazz' - } - - response = self.client.patch(url, data=data) - self.assertEqual(response.status_code, 200) - - nomination = Nomination.objects.get(id=response.json()['id']) - self.assertEqual(nomination.end_reason, data['end_reason']) - - def test_returns_200_on_valid_end_nomination(self): - url = reverse( - 'api:bot:nomination-detail', - args=(self.active_nomination.id,), - ) - data = { - 'active': False, - 'end_reason': 'He started playing jazz' - } - response = self.client.patch(url, data=data) - self.assertEqual(response.status_code, 200) - - nomination = Nomination.objects.get(id=response.json()['id']) - - self.assertAlmostEqual( - nomination.ended_at, - dt.now(timezone.utc), - delta=timedelta(seconds=2) - ) - self.assertFalse(nomination.active) - self.assertEqual(nomination.end_reason, data['end_reason']) - - def test_returns_400_on_invalid_field_end_nomination(self): - url = reverse( - 'api:bot:nomination-detail', - args=(self.active_nomination.id,), - ) - data = { - 'active': False, - 'reason': 'Why does a whale have feet?', - } - response = self.client.patch(url, data=data) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - 'reason': ['This field cannot be set when ending a nomination.'] - }) - - def test_returns_400_on_missing_end_reason_end_nomination(self): - url = reverse( - 'api:bot:nomination-detail', - args=(self.active_nomination.id,), - ) - data = { - 'active': False, - } - - response = self.client.patch(url, data=data) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - 'end_reason': ['This field is required when ending a nomination.'] - }) - - def test_returns_400_on_invalid_use_of_active(self): - url = reverse( - 'api:bot:nomination-detail', - args=(self.inactive_nomination.id,), - ) - data = { - 'active': False, - } - - response = self.client.patch(url, data=data) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - 'active': ['This field can only be used to end a nomination'] - }) - - def test_returns_404_on_get_unknown_nomination(self): - url = reverse( - 'api:bot:nomination-detail', - args=(9999,), - ) - - response = self.client.get(url, data={}) - self.assertEqual(response.status_code, 404) - self.assertEqual(response.json(), { - "detail": "Not found." - }) - - def test_returns_404_on_patch_unknown_nomination(self): - url = reverse( - 'api:bot:nomination-detail', - args=(9999,), - ) - - response = self.client.patch(url, data={}) - self.assertEqual(response.status_code, 404) - self.assertEqual(response.json(), { - "detail": "Not found." - }) - - def test_returns_405_on_list_put(self): - url = reverse('api:bot:nomination-list') - - response = self.client.put(url, data={}) - self.assertEqual(response.status_code, 405) - self.assertEqual(response.json(), { - "detail": "Method \"PUT\" not allowed." - }) - - def test_returns_405_on_list_patch(self): - url = reverse('api:bot:nomination-list') - - response = self.client.patch(url, data={}) - self.assertEqual(response.status_code, 405) - self.assertEqual(response.json(), { - "detail": "Method \"PATCH\" not allowed." - }) - - def test_returns_405_on_list_delete(self): - url = reverse('api:bot:nomination-list') - - response = self.client.delete(url, data={}) - self.assertEqual(response.status_code, 405) - self.assertEqual(response.json(), { - "detail": "Method \"DELETE\" not allowed." - }) - - def test_returns_405_on_detail_post(self): - url = reverse('api:bot:nomination-detail', args=(self.active_nomination.id,)) - - response = self.client.post(url, data={}) - self.assertEqual(response.status_code, 405) - self.assertEqual(response.json(), { - "detail": "Method \"POST\" not allowed." - }) - - def test_returns_405_on_detail_delete(self): - url = reverse('api:bot:nomination-detail', args=(self.active_nomination.id,)) - - response = self.client.delete(url, data={}) - self.assertEqual(response.status_code, 405) - self.assertEqual(response.json(), { - "detail": "Method \"DELETE\" not allowed." - }) - - def test_returns_405_on_detail_put(self): - url = reverse('api:bot:nomination-detail', args=(self.active_nomination.id,)) - - response = self.client.put(url, data={}) - self.assertEqual(response.status_code, 405) - self.assertEqual(response.json(), { - "detail": "Method \"PUT\" not allowed." - }) - - def test_filter_returns_0_objects_unknown_user__id(self): - url = reverse('api:bot:nomination-list') - - response = self.client.get( - url, - data={ - "user__id": 99998888 - } - ) - - self.assertEqual(response.status_code, 200) - infractions = response.json() - - self.assertEqual(len(infractions), 0) - - def test_filter_returns_2_objects_for_testdata(self): - url = reverse('api:bot:nomination-list') - - response = self.client.get( - url, - data={ - "user__id": self.user.id - } - ) - - self.assertEqual(response.status_code, 200) - infractions = response.json() - - self.assertEqual(len(infractions), 2) - - def test_patch_nomination_set_reviewed_of_active_nomination(self): - url = reverse('api:bot:nomination-detail', args=(self.active_nomination.id,)) - data = {'reviewed': True} - - response = self.client.patch(url, data=data) - self.assertEqual(response.status_code, 200) - - def test_patch_nomination_set_reviewed_of_inactive_nomination(self): - url = reverse('api:bot:nomination-detail', args=(self.inactive_nomination.id,)) - data = {'reviewed': True} - - response = self.client.patch(url, data=data) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - 'reviewed': ['This field cannot be set if the nomination is inactive.'] - }) - - def test_patch_nomination_set_reviewed_and_end(self): - url = reverse('api:bot:nomination-detail', args=(self.active_nomination.id,)) - data = {'reviewed': True, 'active': False, 'end_reason': "What?"} - - response = self.client.patch(url, data=data) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - 'reviewed': ['This field cannot be set while you are ending a nomination.'] - }) - - def test_modifying_reason_without_actor(self): - url = reverse('api:bot:nomination-detail', args=(self.active_nomination.id,)) - data = {'reason': 'That is my reason!'} - - response = self.client.patch(url, data=data) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - 'actor': ['This field is required when editing the reason.'] - }) - - def test_modifying_reason_with_unknown_actor(self): - url = reverse('api:bot:nomination-detail', args=(self.active_nomination.id,)) - data = {'reason': 'That is my reason!', 'actor': 90909090909090} - - response = self.client.patch(url, data=data) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - 'actor': ["The actor doesn't exist or has not nominated the user."] - }) - - def test_patch_nomination_set_thread_id_of_active_nomination(self): - url = reverse('api:bot:nomination-detail', args=(self.active_nomination.id,)) - data = {'thread_id': 9876543210} - response = self.client.patch(url, data=data) - self.assertEqual(response.status_code, 200) - - def test_patch_nomination_set_thread_id_and_reviewed_of_active_nomination(self): - url = reverse('api:bot:nomination-detail', args=(self.active_nomination.id,)) - data = {'thread_id': 9876543210, "reviewed": True} - response = self.client.patch(url, data=data) - self.assertEqual(response.status_code, 200) - - def test_modifying_thread_id_when_ending_nomination(self): - url = reverse('api:bot:nomination-detail', args=(self.active_nomination.id,)) - data = {'thread_id': 9876543210, 'active': False, 'end_reason': "What?"} - - response = self.client.patch(url, data=data) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - 'thread_id': ['This field cannot be set when ending a nomination.'] - }) - - def test_patch_thread_id_for_inactive_nomination(self): - url = reverse('api:bot:nomination-detail', args=(self.inactive_nomination.id,)) - data = {'thread_id': 9876543210} - - response = self.client.patch(url, data=data) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - 'thread_id': ['This field cannot be set if the nomination is inactive.'] - }) diff --git a/pydis_site/apps/api/tests/test_off_topic_channel_names.py b/pydis_site/apps/api/tests/test_off_topic_channel_names.py deleted file mode 100644 index 34098c92..00000000 --- a/pydis_site/apps/api/tests/test_off_topic_channel_names.py +++ /dev/null @@ -1,233 +0,0 @@ -from django.urls import reverse - -from .base import AuthenticatedAPITestCase -from ..models import OffTopicChannelName - - -class UnauthenticatedTests(AuthenticatedAPITestCase): - def setUp(self): - super().setUp() - self.client.force_authenticate(user=None) - - def test_cannot_read_off_topic_channel_name_list(self): - """Return a 401 response when not authenticated.""" - url = reverse('api:bot:offtopicchannelname-list') - response = self.client.get(url) - - self.assertEqual(response.status_code, 401) - - def test_cannot_read_off_topic_channel_name_list_with_random_item_param(self): - """Return a 401 response when `random_items` provided and not authenticated.""" - url = reverse('api:bot:offtopicchannelname-list') - response = self.client.get(f'{url}?random_items=no') - - self.assertEqual(response.status_code, 401) - - -class EmptyDatabaseTests(AuthenticatedAPITestCase): - def test_returns_empty_object(self): - """Return empty list when no names in database.""" - url = reverse('api:bot:offtopicchannelname-list') - response = self.client.get(url) - - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), []) - - def test_returns_empty_list_with_get_all_param(self): - """Return empty list when no names and `random_items` param provided.""" - url = reverse('api:bot:offtopicchannelname-list') - response = self.client.get(f'{url}?random_items=5') - - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), []) - - def test_returns_400_for_bad_random_items_param(self): - """Return error message when passing not integer as `random_items`.""" - url = reverse('api:bot:offtopicchannelname-list') - response = self.client.get(f'{url}?random_items=totally-a-valid-integer') - - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - 'random_items': ["Must be a valid integer."] - }) - - def test_returns_400_for_negative_random_items_param(self): - """Return error message when passing negative int as `random_items`.""" - url = reverse('api:bot:offtopicchannelname-list') - response = self.client.get(f'{url}?random_items=-5') - - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - 'random_items': ["Must be a positive integer."] - }) - - -class ListTests(AuthenticatedAPITestCase): - @classmethod - def setUpTestData(cls): - cls.test_name = OffTopicChannelName.objects.create( - name='lemons-lemonade-stand', used=False, active=True - ) - cls.test_name_2 = OffTopicChannelName.objects.create( - name='bbq-with-bisk', used=False, active=True - ) - cls.test_name_3 = OffTopicChannelName.objects.create( - name="frozen-with-iceman", used=True, active=False - ) - cls.test_name_4 = OffTopicChannelName.objects.create( - name="xith-is-cool", used=True, active=True - ) - - def test_returns_name_in_list(self): - """Return all off-topic channel names.""" - url = reverse('api:bot:offtopicchannelname-list') - response = self.client.get(url) - - self.assertEqual(response.status_code, 200) - self.assertEqual( - set(response.json()), - { - self.test_name.name, - self.test_name_2.name, - self.test_name_3.name, - self.test_name_4.name - } - ) - - def test_returns_two_active_items_with_random_items_param_set_to_2(self): - """Return not-used active names instead used.""" - url = reverse('api:bot:offtopicchannelname-list') - response = self.client.get(f'{url}?random_items=2') - - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()), 2) - self.assertTrue( - all( - item in (self.test_name.name, self.test_name_2.name, self.test_name_4.name) - for item in response.json() - ) - ) - - def test_returns_three_active_items_with_random_items_param_set_to_3(self): - """Return not-used active names instead used.""" - url = reverse('api:bot:offtopicchannelname-list') - response = self.client.get(f'{url}?random_items=3') - - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()), 3) - self.assertEqual( - set(response.json()), - {self.test_name.name, self.test_name_2.name, self.test_name_4.name} - ) - - def test_running_out_of_names_with_random_parameter(self): - """Reset names `used` parameter to `False` when running out of active names.""" - url = reverse('api:bot:offtopicchannelname-list') - response = self.client.get(f'{url}?random_items=3') - - self.assertEqual(response.status_code, 200) - self.assertEqual( - set(response.json()), - {self.test_name.name, self.test_name_2.name, self.test_name_4.name} - ) - - def test_returns_inactive_ot_names(self): - """Return inactive off topic names.""" - url = reverse('api:bot:offtopicchannelname-list') - response = self.client.get(f"{url}?active=false") - - self.assertEqual(response.status_code, 200) - self.assertEqual( - response.json(), - [self.test_name_3.name] - ) - - def test_returns_active_ot_names(self): - """Return active off topic names.""" - url = reverse('api:bot:offtopicchannelname-list') - response = self.client.get(f"{url}?active=true") - - self.assertEqual(response.status_code, 200) - self.assertEqual( - set(response.json()), - {self.test_name.name, self.test_name_2.name, self.test_name_4.name} - ) - - -class CreationTests(AuthenticatedAPITestCase): - def setUp(self): - super().setUp() - - url = reverse('api:bot:offtopicchannelname-list') - self.name = "abcdefghijklmnopqrstuvwxyz-0123456789" - response = self.client.post(f'{url}?name={self.name}') - self.assertEqual(response.status_code, 201) - - def test_returns_201_for_unicode_chars(self): - """Accept all valid characters.""" - url = reverse('api:bot:offtopicchannelname-list') - names = ( - '𝖠𝖡𝖢𝖣𝖤𝖥𝖦𝖧𝖨𝖩𝖪𝖫𝖬𝖭𝖮𝖯𝖰𝖱𝖲𝖳𝖴𝖵𝖶𝖷𝖸𝖹', - 'ǃ?’', - ) - - for name in names: - response = self.client.post(f'{url}?name={name}') - self.assertEqual(response.status_code, 201) - - def test_returns_400_for_missing_name_param(self): - """Return error message when name not provided.""" - url = reverse('api:bot:offtopicchannelname-list') - response = self.client.post(url) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - 'name': ["This query parameter is required."] - }) - - def test_returns_400_for_bad_name_param(self): - """Return error message when invalid characters provided.""" - url = reverse('api:bot:offtopicchannelname-list') - invalid_names = ( - 'space between words', - 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', - '!?\'@#$%^&*()', - ) - - for name in invalid_names: - response = self.client.post(f'{url}?name={name}') - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - 'name': ["Enter a valid value."] - }) - - -class DeletionTests(AuthenticatedAPITestCase): - @classmethod - def setUpTestData(cls): - cls.test_name = OffTopicChannelName.objects.create(name='lemons-lemonade-stand') - cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk') - - def test_deleting_unknown_name_returns_404(self): - """Return 404 response when trying to delete unknown name.""" - url = reverse('api:bot:offtopicchannelname-detail', args=('unknown-name',)) - response = self.client.delete(url) - - self.assertEqual(response.status_code, 404) - - def test_deleting_known_name_returns_204(self): - """Return 204 response when deleting was successful.""" - url = reverse('api:bot:offtopicchannelname-detail', args=(self.test_name.name,)) - response = self.client.delete(url) - - self.assertEqual(response.status_code, 204) - - def test_name_gets_deleted(self): - """Name gets actually deleted.""" - url = reverse('api:bot:offtopicchannelname-detail', args=(self.test_name_2.name,)) - response = self.client.delete(url) - - self.assertEqual(response.status_code, 204) - - url = reverse('api:bot:offtopicchannelname-list') - response = self.client.get(url) - self.assertNotIn(self.test_name_2.name, response.json()) diff --git a/pydis_site/apps/api/tests/test_offensive_message.py b/pydis_site/apps/api/tests/test_offensive_message.py deleted file mode 100644 index 3cf95b75..00000000 --- a/pydis_site/apps/api/tests/test_offensive_message.py +++ /dev/null @@ -1,155 +0,0 @@ -import datetime - -from django.urls import reverse - -from .base import AuthenticatedAPITestCase -from ..models import OffensiveMessage - - -class CreationTests(AuthenticatedAPITestCase): - def test_accept_valid_data(self): - url = reverse('api:bot:offensivemessage-list') - delete_at = datetime.datetime.now() + datetime.timedelta(days=1) - data = { - 'id': '602951077675139072', - 'channel_id': '291284109232308226', - 'delete_date': delete_at.isoformat()[:-1] - } - - aware_delete_at = delete_at.replace(tzinfo=datetime.timezone.utc) - - response = self.client.post(url, data=data) - self.assertEqual(response.status_code, 201) - - offensive_message = OffensiveMessage.objects.get(id=response.json()['id']) - self.assertAlmostEqual( - aware_delete_at, - offensive_message.delete_date, - delta=datetime.timedelta(seconds=1) - ) - self.assertEqual(data['id'], str(offensive_message.id)) - self.assertEqual(data['channel_id'], str(offensive_message.channel_id)) - - def test_returns_400_on_non_future_date(self): - url = reverse('api:bot:offensivemessage-list') - delete_at = datetime.datetime.now() - datetime.timedelta(days=1) - data = { - 'id': '602951077675139072', - 'channel_id': '291284109232308226', - 'delete_date': delete_at.isoformat()[:-1] - } - response = self.client.post(url, data=data) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - 'delete_date': ['Date must be a future date'] - }) - - def test_returns_400_on_negative_id_or_channel_id(self): - url = reverse('api:bot:offensivemessage-list') - delete_at = datetime.datetime.now() + datetime.timedelta(days=1) - data = { - 'id': '602951077675139072', - 'channel_id': '291284109232308226', - 'delete_date': delete_at.isoformat()[:-1] - } - cases = ( - ('id', '-602951077675139072'), - ('channel_id', '-291284109232308226') - ) - - for field, invalid_value in cases: - with self.subTest(field=field, invalid_value=invalid_value): - test_data = data.copy() - test_data.update({field: invalid_value}) - - response = self.client.post(url, test_data) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - field: ['Ensure this value is greater than or equal to 0.'] - }) - - -class ListTests(AuthenticatedAPITestCase): - @classmethod - def setUpTestData(cls): - delete_at = datetime.datetime.now() + datetime.timedelta(days=1) - aware_delete_at = delete_at.replace(tzinfo=datetime.timezone.utc) - - cls.messages = [ - { - 'id': 602951077675139072, - 'channel_id': 91284109232308226, - }, - { - 'id': 645298201494159401, - 'channel_id': 592000283102674944 - } - ] - - cls.of1 = OffensiveMessage.objects.create( - **cls.messages[0], - delete_date=aware_delete_at.isoformat() - ) - cls.of2 = OffensiveMessage.objects.create( - **cls.messages[1], - delete_date=aware_delete_at.isoformat() - ) - - # Expected API answer : - cls.messages[0]['delete_date'] = delete_at.isoformat() + 'Z' - cls.messages[1]['delete_date'] = delete_at.isoformat() + 'Z' - - def test_get_data(self): - url = reverse('api:bot:offensivemessage-list') - - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - self.assertEqual(response.json(), self.messages) - - -class DeletionTests(AuthenticatedAPITestCase): - @classmethod - def setUpTestData(cls): - delete_at = datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta(days=1) - - cls.valid_offensive_message = OffensiveMessage.objects.create( - id=602951077675139072, - channel_id=291284109232308226, - delete_date=delete_at.isoformat() - ) - - def test_delete_data(self): - url = reverse( - 'api:bot:offensivemessage-detail', args=(self.valid_offensive_message.id,) - ) - - response = self.client.delete(url) - self.assertEqual(response.status_code, 204) - - self.assertFalse( - OffensiveMessage.objects.filter(id=self.valid_offensive_message.id).exists() - ) - - -class NotAllowedMethodsTests(AuthenticatedAPITestCase): - @classmethod - def setUpTestData(cls): - delete_at = datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta(days=1) - - cls.valid_offensive_message = OffensiveMessage.objects.create( - id=602951077675139072, - channel_id=291284109232308226, - delete_date=delete_at.isoformat() - ) - - def test_returns_405_for_patch_and_put_requests(self): - url = reverse( - 'api:bot:offensivemessage-detail', args=(self.valid_offensive_message.id,) - ) - not_allowed_methods = (self.client.patch, self.client.put) - - for method in not_allowed_methods: - with self.subTest(method=method): - response = method(url, {}) - self.assertEqual(response.status_code, 405) diff --git a/pydis_site/apps/api/tests/test_reminders.py b/pydis_site/apps/api/tests/test_reminders.py deleted file mode 100644 index e17569f0..00000000 --- a/pydis_site/apps/api/tests/test_reminders.py +++ /dev/null @@ -1,222 +0,0 @@ -from datetime import datetime, timezone - -from django.forms.models import model_to_dict -from django.urls import reverse - -from .base import AuthenticatedAPITestCase -from ..models import Reminder, User - - -class UnauthedReminderAPITests(AuthenticatedAPITestCase): - def setUp(self): - super().setUp() - self.client.force_authenticate(user=None) - - def test_list_returns_401(self): - url = reverse('api:bot:reminder-list') - response = self.client.get(url) - - self.assertEqual(response.status_code, 401) - - def test_create_returns_401(self): - url = reverse('api:bot:reminder-list') - response = self.client.post(url, data={'not': 'important'}) - - self.assertEqual(response.status_code, 401) - - def test_delete_returns_401(self): - url = reverse('api:bot:reminder-detail', args=('1234',)) - response = self.client.delete(url) - - self.assertEqual(response.status_code, 401) - - -class EmptyDatabaseReminderAPITests(AuthenticatedAPITestCase): - def test_list_all_returns_empty_list(self): - url = reverse('api:bot:reminder-list') - response = self.client.get(url) - - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), []) - - def test_delete_returns_404(self): - url = reverse('api:bot:reminder-detail', args=('1234',)) - response = self.client.delete(url) - - self.assertEqual(response.status_code, 404) - - -class ReminderCreationTests(AuthenticatedAPITestCase): - @classmethod - def setUpTestData(cls): - cls.author = User.objects.create( - id=1234, - name='Mermaid Man', - discriminator=1234, - ) - - def test_accepts_valid_data(self): - data = { - 'author': self.author.id, - 'content': 'Remember to...wait what was it again?', - 'expiration': datetime.utcnow().isoformat(), - 'jump_url': "https://www.google.com", - 'channel_id': 123, - 'mentions': [8888, 9999], - } - url = reverse('api:bot:reminder-list') - response = self.client.post(url, data=data) - self.assertEqual(response.status_code, 201) - self.assertIsNotNone(Reminder.objects.filter(id=1).first()) - - def test_rejects_invalid_data(self): - data = { - 'author': self.author.id, # Missing multiple required fields - } - url = reverse('api:bot:reminder-list') - response = self.client.post(url, data=data) - self.assertEqual(response.status_code, 400) - self.assertRaises(Reminder.DoesNotExist, Reminder.objects.get, id=1) - - -class ReminderDeletionTests(AuthenticatedAPITestCase): - @classmethod - def setUpTestData(cls): - cls.author = User.objects.create( - id=6789, - name='Barnacle Boy', - discriminator=6789, - ) - - cls.reminder = Reminder.objects.create( - author=cls.author, - content="Don't forget to set yourself a reminder", - expiration=datetime.now(timezone.utc), - jump_url="https://www.decliningmentalfaculties.com", - channel_id=123 - ) - - def test_delete_unknown_reminder_returns_404(self): - url = reverse('api:bot:reminder-detail', args=('something',)) - response = self.client.delete(url) - - self.assertEqual(response.status_code, 404) - - def test_delete_known_reminder_returns_204(self): - url = reverse('api:bot:reminder-detail', args=(self.reminder.id,)) - response = self.client.delete(url) - - self.assertEqual(response.status_code, 204) - self.assertRaises(Reminder.DoesNotExist, Reminder.objects.get, id=self.reminder.id) - - -class ReminderListTests(AuthenticatedAPITestCase): - @classmethod - def setUpTestData(cls): - cls.author = User.objects.create( - id=6789, - name='Patrick Star', - discriminator=6789, - ) - - cls.reminder_one = Reminder.objects.create( - author=cls.author, - content="We should take Bikini Bottom, and push it somewhere else!", - expiration=datetime.now(timezone.utc), - jump_url="https://www.icantseemyforehead.com", - channel_id=123 - ) - - cls.reminder_two = Reminder.objects.create( - author=cls.author, - content="Gahhh-I love being purple!", - expiration=datetime.now(timezone.utc), - jump_url="https://www.goofygoobersicecreampartyboat.com", - channel_id=123, - active=False - ) - - drf_format = '%Y-%m-%dT%H:%M:%S.%fZ' - cls.rem_dict_one = model_to_dict(cls.reminder_one) - cls.rem_dict_one['expiration'] = cls.rem_dict_one['expiration'].strftime(drf_format) - cls.rem_dict_two = model_to_dict(cls.reminder_two) - cls.rem_dict_two['expiration'] = cls.rem_dict_two['expiration'].strftime(drf_format) - - def test_reminders_in_full_list(self): - url = reverse('api:bot:reminder-list') - response = self.client.get(url) - - self.assertEqual(response.status_code, 200) - self.assertCountEqual(response.json(), [self.rem_dict_one, self.rem_dict_two]) - - def test_filter_search(self): - url = reverse('api:bot:reminder-list') - response = self.client.get(f'{url}?search={self.author.name}') - - self.assertEqual(response.status_code, 200) - self.assertCountEqual(response.json(), [self.rem_dict_one, self.rem_dict_two]) - - def test_filter_field(self): - url = reverse('api:bot:reminder-list') - response = self.client.get(f'{url}?active=true') - - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), [self.rem_dict_one]) - - -class ReminderRetrieveTests(AuthenticatedAPITestCase): - @classmethod - def setUpTestData(cls): - cls.author = User.objects.create( - id=6789, - name='Reminder author', - discriminator=6789, - ) - - cls.reminder = Reminder.objects.create( - author=cls.author, - content="Reminder content", - expiration=datetime.now(timezone.utc), - jump_url="http://example.com/", - channel_id=123 - ) - - def test_retrieve_unknown_returns_404(self): - url = reverse('api:bot:reminder-detail', args=("not_an_id",)) - response = self.client.get(url) - self.assertEqual(response.status_code, 404) - - def test_retrieve_known_returns_200(self): - url = reverse('api:bot:reminder-detail', args=(self.reminder.id,)) - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - -class ReminderUpdateTests(AuthenticatedAPITestCase): - @classmethod - def setUpTestData(cls): - cls.author = User.objects.create( - id=666, - name='Man Ray', - discriminator=666, - ) - - cls.reminder = Reminder.objects.create( - author=cls.author, - content="Squash those do-gooders", - expiration=datetime.now(timezone.utc), - jump_url="https://www.decliningmentalfaculties.com", - channel_id=123 - ) - - cls.data = {'content': 'Oops I forgot'} - - def test_patch_updates_record(self): - url = reverse('api:bot:reminder-detail', args=(self.reminder.id,)) - response = self.client.patch(url, data=self.data) - - self.assertEqual(response.status_code, 200) - self.assertEqual( - Reminder.objects.filter(id=self.reminder.id).first().content, - self.data['content'] - ) diff --git a/pydis_site/apps/api/tests/test_roles.py b/pydis_site/apps/api/tests/test_roles.py deleted file mode 100644 index 73c80c77..00000000 --- a/pydis_site/apps/api/tests/test_roles.py +++ /dev/null @@ -1,211 +0,0 @@ -from django.urls import reverse - -from .base import AuthenticatedAPITestCase -from ..models import Role, User - - -class CreationTests(AuthenticatedAPITestCase): - @classmethod - def setUpTestData(cls): - cls.admins_role = Role.objects.create( - id=1, - name="Admins", - colour=1, - permissions=1, - position=4, - ) - cls.developers_role = Role.objects.create( - id=4, - name="Helpers", - colour=4, - permissions=4, - position=1, - ) - cls.everyone_role = Role.objects.create( - id=5, - name="@everyone", - colour=5, - permissions=5, - position=0, - ) - cls.lowest_position_duplicate = Role.objects.create( - id=6, - name="lowest position duplicate", - colour=6, - permissions=6, - position=0, - ) - cls.role_to_delete = Role.objects.create( - id=7, - name="role to delete", - colour=7, - permissions=7, - position=0, - ) - cls.role_unassigned_test_user = User.objects.create( - id=8, - name="role_unassigned_test_user", - discriminator="0000", - roles=[cls.role_to_delete.id], - in_guild=True - ) - - def _validate_roledict(self, role_dict: dict) -> None: - """Helper method to validate a dict representing a role.""" - self.assertIsInstance(role_dict, dict) - self.assertEqual(len(role_dict), 5) - attributes = ('id', 'name', 'colour', 'permissions', 'position') - self.assertTrue(all(attribute in role_dict for attribute in attributes)) - - def test_role_ordering_lt(self): - """Tests the __lt__ comparisons based on role position in the hierarchy.""" - self.assertTrue(self.everyone_role < self.developers_role) - self.assertFalse(self.developers_role > self.admins_role) - - def test_role_ordering_le(self): - """Tests the __le__ comparisons based on role position in the hierarchy.""" - self.assertTrue(self.everyone_role <= self.developers_role) - self.assertTrue(self.everyone_role <= self.lowest_position_duplicate) - self.assertTrue(self.everyone_role >= self.lowest_position_duplicate) - self.assertTrue(self.developers_role >= self.everyone_role) - - self.assertFalse(self.developers_role >= self.admins_role) - self.assertFalse(self.developers_role <= self.everyone_role) - - def test_role_min_max_ordering(self): - """Tests the `min` and `max` operations based on the role hierarchy.""" - top_role_no_duplicates = max([self.developers_role, self.admins_role, self.everyone_role]) - self.assertIs(top_role_no_duplicates, self.admins_role) - - top_role_duplicates = max([self.developers_role, self.admins_role, self.admins_role]) - self.assertIs(top_role_duplicates, self.admins_role) - - bottom_role_no_duplicates = min( - [self.developers_role, self.admins_role, self.everyone_role] - ) - self.assertIs(bottom_role_no_duplicates, self.everyone_role) - - bottom_role_duplicates = min( - [self.lowest_position_duplicate, self.admins_role, self.everyone_role] - ) - self.assertIs(bottom_role_duplicates, self.lowest_position_duplicate) - - def test_role_list(self): - """Tests the GET list-view and validates the contents.""" - url = reverse('api:bot:role-list') - - response = self.client.get(url) - self.assertContains(response, text="id", count=5, status_code=200) - - roles = response.json() - self.assertIsInstance(roles, list) - self.assertEqual(len(roles), 5) - - for role in roles: - self._validate_roledict(role) - - def test_role_get_detail_success(self): - """Tests GET detail view of an existing role.""" - url = reverse('api:bot:role-detail', args=(self.admins_role.id, )) - response = self.client.get(url) - self.assertContains(response, text="id", count=1, status_code=200) - - role = response.json() - self._validate_roledict(role) - - admins_role = Role.objects.get(id=role["id"]) - self.assertEqual(admins_role.name, role["name"]) - self.assertEqual(admins_role.colour, role["colour"]) - self.assertEqual(admins_role.permissions, role["permissions"]) - self.assertEqual(admins_role.position, role["position"]) - - def test_role_post_201(self): - """Tests creation of a role with a valid request.""" - url = reverse('api:bot:role-list') - data = { - "id": 1234567890, - "name": "Role Creation Test", - "permissions": 0b01010010101, - "colour": 1, - "position": 10, - } - response = self.client.post(url, data=data) - self.assertEqual(response.status_code, 201) - - def test_role_post_invalid_request_body(self): - """Tests creation of a role with an invalid request body.""" - url = reverse('api:bot:role-list') - data = { - "name": "Role Creation Test", - "permissions": 0b01010010101, - "colour": 1, - "position": 10, - } - response = self.client.post(url, data=data) - self.assertEqual(response.status_code, 400) - self.assertJSONEqual(response.content, '{"id": ["This field is required."]}') - - def test_role_put_200(self): - """Tests PUT role request with valid request body.""" - url = reverse('api:bot:role-detail', args=(self.admins_role.id,)) - data = { - "id": 123454321, - "name": "Role Put Alteration Test", - "permissions": 255, - "colour": 999, - "position": 20, - } - - response = self.client.put(url, data=data) - self.assertEqual(response.status_code, 200) - - admins_role = Role.objects.get(id=data["id"]) - self.assertEqual(admins_role.name, data["name"]) - self.assertEqual(admins_role.permissions, data["permissions"]) - self.assertEqual(admins_role.colour, data["colour"]) - self.assertEqual(admins_role.position, data["position"]) - - def test_role_put_invalid_request_body(self): - """Tests PUT role request with invalid request body.""" - url = reverse('api:bot:role-detail', args=(self.admins_role.id,)) - data = { - "name": "Role Put Alteration Test", - "permissions": 255, - "colour": 999, - "position": 20, - } - response = self.client.put(url, data=data) - self.assertEqual(response.status_code, 400) - - def test_role_patch_200(self): - """Tests PATCH role request with valid request body.""" - url = reverse('api:bot:role-detail', args=(self.admins_role.id,)) - data = { - "name": "Owners" - } - response = self.client.patch(url, data=data) - self.assertEqual(response.status_code, 200) - - admins_role = Role.objects.get(id=self.admins_role.id) - self.assertEqual(admins_role.name, data["name"]) - - def test_role_delete_200(self): - """Tests DELETE requests for existing role.""" - url = reverse('api:bot:role-detail', args=(self.admins_role.id,)) - response = self.client.delete(url) - self.assertEqual(response.status_code, 204) - - def test_role_delete_unassigned(self): - """Tests if the deleted Role gets unassigned from the user.""" - self.role_to_delete.delete() - self.role_unassigned_test_user.refresh_from_db() - self.assertEqual(self.role_unassigned_test_user.roles, []) - - def test_role_detail_404_all_methods(self): - """Tests detail view with non-existing ID.""" - url = reverse('api:bot:role-detail', args=(20190815,)) - - for method in ('get', 'put', 'patch', 'delete'): - response = getattr(self.client, method)(url) - self.assertEqual(response.status_code, 404) - self.assertJSONEqual(response.content, '{"detail": "Not found."}') diff --git a/pydis_site/apps/api/tests/test_rules.py b/pydis_site/apps/api/tests/test_rules.py deleted file mode 100644 index d08c5fae..00000000 --- a/pydis_site/apps/api/tests/test_rules.py +++ /dev/null @@ -1,35 +0,0 @@ -from django.urls import reverse - -from .base import AuthenticatedAPITestCase -from ..views import RulesView - - -class RuleAPITests(AuthenticatedAPITestCase): - def setUp(self): - super().setUp() - self.client.force_authenticate(user=None) - - def test_can_access_rules_view(self): - url = reverse('api:rules') - response = self.client.get(url) - - self.assertEqual(response.status_code, 200) - self.assertIsInstance(response.json(), list) - - def test_link_format_query_param_produces_different_results(self): - url = reverse('api:rules') - markdown_links_response = self.client.get(url + '?link_format=md') - html_links_response = self.client.get(url + '?link_format=html') - self.assertNotEqual( - markdown_links_response.json(), - html_links_response.json() - ) - - def test_format_link_raises_value_error_for_invalid_target(self): - with self.assertRaises(ValueError): - RulesView._format_link("a", "b", "c") - - def test_get_returns_400_for_wrong_link_format(self): - url = reverse('api:rules') - response = self.client.get(url + '?link_format=unknown') - self.assertEqual(response.status_code, 400) diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py deleted file mode 100644 index d86e80bb..00000000 --- a/pydis_site/apps/api/tests/test_users.py +++ /dev/null @@ -1,649 +0,0 @@ -import random -from unittest.mock import Mock, patch - -from django.urls import reverse - -from .base import AuthenticatedAPITestCase -from ..models import Infraction, Role, User -from ..models.bot.metricity import NotFoundError -from ..viewsets.bot.user import UserListPagination - - -class UnauthedUserAPITests(AuthenticatedAPITestCase): - def setUp(self): - super().setUp() - self.client.force_authenticate(user=None) - - def test_detail_lookup_returns_401(self): - url = reverse('api:bot:user-detail', args=('whatever',)) - response = self.client.get(url) - - self.assertEqual(response.status_code, 401) - - def test_list_returns_401(self): - url = reverse('api:bot:user-list') - response = self.client.get(url) - - self.assertEqual(response.status_code, 401) - - def test_create_returns_401(self): - url = reverse('api:bot:user-list') - response = self.client.post(url, data={'hi': 'there'}) - - self.assertEqual(response.status_code, 401) - - def test_delete_returns_401(self): - url = reverse('api:bot:user-detail', args=('whatever',)) - response = self.client.delete(url) - - self.assertEqual(response.status_code, 401) - - -class CreationTests(AuthenticatedAPITestCase): - @classmethod - def setUpTestData(cls): - cls.role = Role.objects.create( - id=5, - name="Test role pls ignore", - colour=2, - permissions=0b01010010101, - position=1 - ) - - cls.user = User.objects.create( - id=11, - name="Name doesn't matter.", - discriminator=1122, - in_guild=True - ) - - def test_accepts_valid_data(self): - url = reverse('api:bot:user-list') - data = { - 'id': 42, - 'name': "Test", - 'discriminator': 42, - 'roles': [ - self.role.id - ], - 'in_guild': True - } - - response = self.client.post(url, data=data) - self.assertEqual(response.status_code, 201) - self.assertEqual(response.json(), data) - - user = User.objects.get(id=42) - self.assertEqual(user.name, data['name']) - self.assertEqual(user.discriminator, data['discriminator']) - self.assertEqual(user.in_guild, data['in_guild']) - - def test_supports_multi_creation(self): - url = reverse('api:bot:user-list') - data = [ - { - 'id': 5, - 'name': "test man", - 'discriminator': 42, - 'roles': [ - self.role.id - ], - 'in_guild': True - }, - { - 'id': 8, - 'name': "another test man", - 'discriminator': 555, - 'roles': [], - 'in_guild': False - } - ] - - response = self.client.post(url, data=data) - self.assertEqual(response.status_code, 201) - self.assertEqual(response.json(), []) - - def test_returns_400_for_unknown_role_id(self): - url = reverse('api:bot:user-list') - data = { - 'id': 5, - 'name': "test man", - 'discriminator': 42, - 'roles': [ - 190810291 - ] - } - - response = self.client.post(url, data=data) - self.assertEqual(response.status_code, 400) - - def test_returns_400_for_bad_data(self): - url = reverse('api:bot:user-list') - data = { - 'id': True, - 'discriminator': "totally!" - } - - response = self.client.post(url, data=data) - self.assertEqual(response.status_code, 400) - - def test_returns_400_for_user_recreation(self): - """Return 201 if User is already present in database as it skips User creation.""" - url = reverse('api:bot:user-list') - data = [{ - 'id': 11, - 'name': 'You saw nothing.', - 'discriminator': 112, - 'in_guild': True - }] - response = self.client.post(url, data=data) - self.assertEqual(response.status_code, 201) - - def test_returns_400_for_duplicate_request_users(self): - """Return 400 if 2 Users with same ID is passed in the request data.""" - url = reverse('api:bot:user-list') - data = [ - { - 'id': 11, - 'name': 'You saw nothing.', - 'discriminator': 112, - 'in_guild': True - }, - { - 'id': 11, - 'name': 'You saw nothing part 2.', - 'discriminator': 1122, - 'in_guild': False - } - ] - response = self.client.post(url, data=data) - self.assertEqual(response.status_code, 400) - - def test_returns_400_for_existing_user(self): - """Returns 400 if user is already present in DB.""" - url = reverse('api:bot:user-list') - data = { - 'id': 11, - 'name': 'You saw nothing part 3.', - 'discriminator': 1122, - 'in_guild': True - } - response = self.client.post(url, data=data) - self.assertEqual(response.status_code, 400) - - -class MultiPatchTests(AuthenticatedAPITestCase): - @classmethod - def setUpTestData(cls): - cls.role_developer = Role.objects.create( - id=159, - name="Developer", - colour=2, - permissions=0b01010010101, - position=10, - ) - cls.user_1 = User.objects.create( - id=1, - name="Patch test user 1.", - discriminator=1111, - in_guild=True - ) - cls.user_2 = User.objects.create( - id=2, - name="Patch test user 2.", - discriminator=2222, - in_guild=True - ) - - def test_multiple_users_patch(self): - url = reverse("api:bot:user-bulk-patch") - data = [ - { - "id": 1, - "name": "User 1 patched!", - "discriminator": 1010, - "roles": [self.role_developer.id], - "in_guild": False - }, - { - "id": 2, - "name": "User 2 patched!" - } - ] - - response = self.client.patch(url, data=data) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()[0], data[0]) - - user_2 = User.objects.get(id=2) - self.assertEqual(user_2.name, data[1]["name"]) - - def test_returns_400_for_missing_user_id(self): - url = reverse("api:bot:user-bulk-patch") - data = [ - { - "name": "I am ghost user!", - "discriminator": 1010, - "roles": [self.role_developer.id], - "in_guild": False - }, - { - "name": "patch me? whats my id?" - } - ] - response = self.client.patch(url, data=data) - self.assertEqual(response.status_code, 400) - - def test_returns_404_for_not_found_user(self): - url = reverse("api:bot:user-bulk-patch") - data = [ - { - "id": 1, - "name": "User 1 patched again!!!", - "discriminator": 1010, - "roles": [self.role_developer.id], - "in_guild": False - }, - { - "id": 22503405, - "name": "User unknown not patched!" - } - ] - response = self.client.patch(url, data=data) - self.assertEqual(response.status_code, 404) - - def test_returns_400_for_bad_data(self): - url = reverse("api:bot:user-bulk-patch") - data = [ - { - "id": 1, - "in_guild": "Catch me!" - }, - { - "id": 2, - "discriminator": "find me!" - } - ] - - response = self.client.patch(url, data=data) - self.assertEqual(response.status_code, 400) - - def test_returns_400_for_insufficient_data(self): - url = reverse("api:bot:user-bulk-patch") - data = [ - { - "id": 1, - }, - { - "id": 2, - } - ] - response = self.client.patch(url, data=data) - self.assertEqual(response.status_code, 400) - - def test_returns_400_for_duplicate_request_users(self): - """Return 400 if 2 Users with same ID is passed in the request data.""" - url = reverse("api:bot:user-bulk-patch") - data = [ - { - 'id': 1, - 'name': 'You saw nothing.', - }, - { - 'id': 1, - 'name': 'You saw nothing part 2.', - } - ] - response = self.client.patch(url, data=data) - self.assertEqual(response.status_code, 400) - - -class UserModelTests(AuthenticatedAPITestCase): - @classmethod - def setUpTestData(cls): - cls.role_top = Role.objects.create( - id=777, - name="High test role", - colour=2, - permissions=0b01010010101, - position=10, - ) - cls.role_bottom = Role.objects.create( - id=888, - name="Low test role", - colour=2, - permissions=0b01010010101, - position=1, - ) - cls.developers_role = Role.objects.create( - id=1234567, - name="Developers", - colour=1234, - permissions=0b01010010101, - position=2, - ) - cls.user_with_roles = User.objects.create( - id=1, - name="Test User with two roles", - discriminator=1, - in_guild=True, - ) - cls.user_with_roles.roles.extend([cls.role_bottom.id, cls.role_top.id]) - - cls.user_without_roles = User.objects.create( - id=2, - name="Test User without roles", - discriminator=2222, - in_guild=True, - ) - - def test_correct_top_role_property_user_with_roles(self): - """Tests if the top_role property returns the correct role.""" - top_role = self.user_with_roles.top_role - self.assertIsInstance(top_role, Role) - self.assertEqual(top_role.id, self.role_top.id) - - def test_correct_top_role_property_user_without_roles(self): - """Tests if the top_role property returns the correct role.""" - top_role = self.user_without_roles.top_role - self.assertIsInstance(top_role, Role) - self.assertEqual(top_role.id, self.developers_role.id) - - def test_correct_username_formatting(self): - """Tests the username property with both name and discriminator formatted together.""" - self.assertEqual(self.user_with_roles.username, "Test User with two roles#0001") - - -class UserPaginatorTests(AuthenticatedAPITestCase): - @classmethod - def setUpTestData(cls): - users = [] - for i in range(1, UserListPagination.page_size + 1): - users.append(User( - id=i, - name=f"user{i}", - discriminator=1111, - in_guild=True - )) - cls.users = User.objects.bulk_create(users) - - def test_returns_single_page_response(self): - url = reverse("api:bot:user-list") - response = self.client.get(url).json() - self.assertIsNone(response["next_page_no"]) - self.assertIsNone(response["previous_page_no"]) - - def test_returns_next_page_number(self): - user_id = UserListPagination.page_size + 1 - User.objects.create( - id=user_id, - name=f"user{user_id}", - discriminator=1111, - in_guild=True - ) - url = reverse("api:bot:user-list") - response = self.client.get(url).json() - self.assertEqual(2, response["next_page_no"]) - - def test_returns_previous_page_number(self): - user_id = UserListPagination.page_size + 1 - User.objects.create( - id=user_id, - name=f"user{user_id}", - discriminator=1111, - in_guild=True - ) - url = reverse("api:bot:user-list") - response = self.client.get(url, {"page": 2}).json() - self.assertEqual(1, response["previous_page_no"]) - - -class UserMetricityTests(AuthenticatedAPITestCase): - @classmethod - def setUpTestData(cls): - User.objects.create( - id=0, - name="Test user", - discriminator=1, - in_guild=True, - ) - - def test_get_metricity_data(self): - # Given - joined_at = "foo" - total_messages = 1 - total_blocks = 1 - self.mock_metricity_user(joined_at, total_messages, total_blocks, []) - - # When - url = reverse('api:bot:user-metricity-data', args=[0]) - response = self.client.get(url) - - # Then - self.assertEqual(response.status_code, 200) - self.assertCountEqual(response.json(), { - "joined_at": joined_at, - "total_messages": total_messages, - "voice_gate_blocked": False, - "activity_blocks": total_blocks - }) - - def test_no_metricity_user(self): - # Given - self.mock_no_metricity_user() - - # When - url = reverse('api:bot:user-metricity-data', args=[0]) - response = self.client.get(url) - - # Then - self.assertEqual(response.status_code, 404) - - def test_no_metricity_user_for_review(self): - # Given - self.mock_no_metricity_user() - - # When - url = reverse('api:bot:user-metricity-review-data', args=[0]) - response = self.client.get(url) - - # Then - self.assertEqual(response.status_code, 404) - - def test_metricity_voice_banned(self): - queryset_with_values = Mock(spec=Infraction.objects) - queryset_with_values.filter.return_value = queryset_with_values - queryset_with_values.exists.return_value = True - - queryset_without_values = Mock(spec=Infraction.objects) - queryset_without_values.filter.return_value = queryset_without_values - queryset_without_values.exists.return_value = False - cases = [ - {'voice_infractions': queryset_with_values, 'voice_gate_blocked': True}, - {'voice_infractions': queryset_without_values, 'voice_gate_blocked': False}, - ] - - self.mock_metricity_user("foo", 1, 1, [["bar", 1]]) - - for case in cases: - with self.subTest( - voice_infractions=case['voice_infractions'], - voice_gate_blocked=case['voice_gate_blocked'] - ): - with patch("pydis_site.apps.api.viewsets.bot.user.Infraction.objects.filter") as p: - p.return_value = case['voice_infractions'] - - url = reverse('api:bot:user-metricity-data', args=[0]) - response = self.client.get(url) - - self.assertEqual(response.status_code, 200) - self.assertEqual( - response.json()["voice_gate_blocked"], - case["voice_gate_blocked"] - ) - - def test_metricity_review_data(self): - # Given - joined_at = "foo" - total_messages = 10 - total_blocks = 1 - channel_activity = [["bar", 4], ["buzz", 6]] - self.mock_metricity_user(joined_at, total_messages, total_blocks, channel_activity) - - # When - url = reverse('api:bot:user-metricity-review-data', args=[0]) - response = self.client.get(url) - - # Then - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), { - "joined_at": joined_at, - "top_channel_activity": channel_activity, - "total_messages": total_messages - }) - - def test_metricity_activity_data(self): - # Given - self.mock_no_metricity_user() # Other functions shouldn't be used. - self.metricity.total_messages_in_past_n_days.return_value = [(0, 10)] - - # When - url = reverse("api:bot:user-metricity-activity-data") - response = self.client.post( - url, - data=[0, 1], - QUERY_STRING="days=10", - ) - - # Then - self.assertEqual(response.status_code, 200) - self.metricity.total_messages_in_past_n_days.assert_called_once_with(["0", "1"], 10) - self.assertEqual(response.json(), {"0": 10, "1": 0}) - - def test_metricity_activity_data_invalid_days(self): - # Given - self.mock_no_metricity_user() # Other functions shouldn't be used. - - # When - url = reverse("api:bot:user-metricity-activity-data") - response = self.client.post( - url, - data=[0, 1], - QUERY_STRING="days=fifty", - ) - - # Then - self.assertEqual(response.status_code, 400) - self.metricity.total_messages_in_past_n_days.assert_not_called() - self.assertEqual(response.json(), {"days": ["This query parameter must be an integer."]}) - - def test_metricity_activity_data_no_days(self): - # Given - self.mock_no_metricity_user() # Other functions shouldn't be used. - - # When - url = reverse('api:bot:user-metricity-activity-data') - response = self.client.post( - url, - data=[0, 1], - ) - - # Then - self.assertEqual(response.status_code, 400) - self.metricity.total_messages_in_past_n_days.assert_not_called() - self.assertEqual(response.json(), {'days': ["This query parameter is required."]}) - - def test_metricity_activity_data_no_users(self): - # Given - self.mock_no_metricity_user() # Other functions shouldn't be used. - - # When - url = reverse('api:bot:user-metricity-activity-data') - response = self.client.post( - url, - QUERY_STRING="days=10", - ) - - # Then - self.assertEqual(response.status_code, 400) - self.metricity.total_messages_in_past_n_days.assert_not_called() - self.assertEqual(response.json(), ['Expected a list of items but got type "dict".']) - - def test_metricity_activity_data_invalid_users(self): - # Given - self.mock_no_metricity_user() # Other functions shouldn't be used. - - # When - url = reverse('api:bot:user-metricity-activity-data') - response = self.client.post( - url, - data=[123, 'username'], - QUERY_STRING="days=10", - ) - - # Then - self.assertEqual(response.status_code, 400) - self.metricity.total_messages_in_past_n_days.assert_not_called() - self.assertEqual(response.json(), {'1': ['A valid integer is required.']}) - - def mock_metricity_user(self, joined_at, total_messages, total_blocks, top_channel_activity): - patcher = patch("pydis_site.apps.api.viewsets.bot.user.Metricity") - self.metricity = patcher.start() - self.addCleanup(patcher.stop) - self.metricity = self.metricity.return_value.__enter__.return_value - self.metricity.user.return_value = dict(joined_at=joined_at) - self.metricity.total_messages.return_value = total_messages - self.metricity.total_message_blocks.return_value = total_blocks - self.metricity.top_channel_activity.return_value = top_channel_activity - - def mock_no_metricity_user(self): - patcher = patch("pydis_site.apps.api.viewsets.bot.user.Metricity") - self.metricity = patcher.start() - self.addCleanup(patcher.stop) - self.metricity = self.metricity.return_value.__enter__.return_value - self.metricity.user.side_effect = NotFoundError() - self.metricity.total_messages.side_effect = NotFoundError() - self.metricity.total_message_blocks.side_effect = NotFoundError() - self.metricity.top_channel_activity.side_effect = NotFoundError() - - -class UserViewSetTests(AuthenticatedAPITestCase): - @classmethod - def setUpTestData(cls): - cls.searched_user = User.objects.create( - id=12095219, - name=f"Test user {random.randint(100, 1000)}", - discriminator=random.randint(1, 9999), - in_guild=True, - ) - cls.other_user = User.objects.create( - id=18259125, - name=f"Test user {random.randint(100, 1000)}", - discriminator=random.randint(1, 9999), - in_guild=True, - ) - - def test_search_lookup_of_wanted_user(self) -> None: - """Searching a user by name and discriminator should return that user.""" - url = reverse('api:bot:user-list') - params = { - 'username': self.searched_user.name, - 'discriminator': self.searched_user.discriminator, - } - response = self.client.get(url, params) - result = response.json() - self.assertEqual(result['count'], 1) - [user] = result['results'] - self.assertEqual(user['id'], self.searched_user.id) - - def test_search_lookup_of_unknown_user(self) -> None: - """Searching an unknown user should return no results.""" - url = reverse('api:bot:user-list') - params = { - 'username': "f-string enjoyer", - 'discriminator': 1245, - } - response = self.client.get(url, params) - result = response.json() - self.assertEqual(result['count'], 0) - self.assertEqual(result['results'], []) diff --git a/pydis_site/apps/api/tests/test_validators.py b/pydis_site/apps/api/tests/test_validators.py deleted file mode 100644 index 8c46fcbc..00000000 --- a/pydis_site/apps/api/tests/test_validators.py +++ /dev/null @@ -1,30 +0,0 @@ -from datetime import datetime, timezone - -from django.core.exceptions import ValidationError -from django.test import TestCase - -from ..models.bot.bot_setting import validate_bot_setting_name -from ..models.bot.offensive_message import future_date_validator - - -REQUIRED_KEYS = ( - 'content', 'fields', 'image', 'title', 'video' -) - - -class BotSettingValidatorTests(TestCase): - def test_accepts_valid_names(self): - validate_bot_setting_name('defcon') - - def test_rejects_bad_names(self): - with self.assertRaises(ValidationError): - validate_bot_setting_name('bad name') - - -class OffensiveMessageValidatorsTests(TestCase): - def test_accepts_future_date(self): - future_date_validator(datetime(3000, 1, 1, tzinfo=timezone.utc)) - - def test_rejects_non_future_date(self): - with self.assertRaises(ValidationError): - future_date_validator(datetime(1000, 1, 1, tzinfo=timezone.utc)) diff --git a/pydis_site/apps/api/urls.py b/pydis_site/apps/api/urls.py deleted file mode 100644 index 2757f176..00000000 --- a/pydis_site/apps/api/urls.py +++ /dev/null @@ -1,95 +0,0 @@ -from django.urls import include, path -from rest_framework.routers import DefaultRouter - -from .views import GitHubArtifactsView, HealthcheckView, RulesView -from .viewsets import ( - AocAccountLinkViewSet, - AocCompletionistBlockViewSet, - BotSettingViewSet, - BumpedThreadViewSet, - DeletedMessageViewSet, - DocumentationLinkViewSet, - FilterListViewSet, - InfractionViewSet, - NominationViewSet, - OffTopicChannelNameViewSet, - OffensiveMessageViewSet, - ReminderViewSet, - RoleViewSet, - UserViewSet -) - -# https://www.django-rest-framework.org/api-guide/routers/#defaultrouter -bot_router = DefaultRouter(trailing_slash=False) -bot_router.register( - "aoc-account-links", - AocAccountLinkViewSet -) -bot_router.register( - "aoc-completionist-blocks", - AocCompletionistBlockViewSet -) -bot_router.register( - 'bot-settings', - BotSettingViewSet -) -bot_router.register( - 'bumped-threads', - BumpedThreadViewSet -) -bot_router.register( - 'deleted-messages', - DeletedMessageViewSet -) -bot_router.register( - 'documentation-links', - DocumentationLinkViewSet -) -bot_router.register( - 'filter-lists', - FilterListViewSet -) -bot_router.register( - 'infractions', - InfractionViewSet -) -bot_router.register( - 'nominations', - NominationViewSet -) -bot_router.register( - 'offensive-messages', - OffensiveMessageViewSet -) -bot_router.register( - 'off-topic-channel-names', - OffTopicChannelNameViewSet, - basename='offtopicchannelname' -) -bot_router.register( - 'reminders', - ReminderViewSet -) -bot_router.register( - 'roles', - RoleViewSet -) -bot_router.register( - 'users', - UserViewSet -) - -app_name = 'api' -urlpatterns = ( - # Build URLs using something like... - # - # from django_hosts.resolvers import reverse - path('bot/', include((bot_router.urls, 'api'), namespace='bot')), - path('healthcheck', HealthcheckView.as_view(), name='healthcheck'), - path('rules', RulesView.as_view(), name='rules'), - path( - 'github/artifact/<str:owner>/<str:repo>/<str:sha>/<str:action_name>/<str:artifact_name>', - GitHubArtifactsView.as_view(), - name="github-artifacts" - ), -) diff --git a/pydis_site/apps/api/views.py b/pydis_site/apps/api/views.py deleted file mode 100644 index 20431a61..00000000 --- a/pydis_site/apps/api/views.py +++ /dev/null @@ -1,228 +0,0 @@ -from rest_framework.exceptions import ParseError -from rest_framework.request import Request -from rest_framework.response import Response -from rest_framework.views import APIView - -from . import github_utils - - -class HealthcheckView(APIView): - """ - Provides a simple view to check that the website is alive and well. - - ## Routes - ### GET /healthcheck - Returns a simple JSON document showcasing whether the system is working: - - >>> { - ... 'status': 'ok' - ... } - - Seems to be. - - ## Authentication - Does not require any authentication nor permissions. - """ - - authentication_classes = () - permission_classes = () - - def get(self, request, format=None): # noqa: D102,ANN001,ANN201 - return Response({'status': 'ok'}) - - -class RulesView(APIView): - """ - Return a list of the server's rules. - - ## Routes - ### GET /rules - Returns a JSON array containing the server's rules - and keywords relating to each rule. - Example response: - - >>> [ - ... ["Eat candy.", ["candy", "sweets"]], - ... ["Wake up at 4 AM.", ["wake_up", "early", "early_bird"]], - ... ["Take your medicine.", ["medicine", "health"]] - ... ] - - Since some of the the rules require links, this view - gives you the option to return rules in either Markdown - or HTML format by specifying the `link_format` query parameter - as either `md` or `html`. Specifying a different value than - `md` or `html` will return 400. - - ## Authentication - Does not require any authentication nor permissions. - """ - - authentication_classes = () - permission_classes = () - - @staticmethod - def _format_link(description: str, link: str, target: str) -> str: - """ - Build the markup for rendering the link. - - This will render `link` with `description` as its description in the given - `target` language. - - Arguments: - description (str): - A textual description of the string. Represents the content - between the `<a>` tags in HTML, or the content between the - array brackets in Markdown. - - link (str): - The resulting link that a user should be redirected to - upon clicking the generated element. - - target (str): - One of `{'md', 'html'}`, denoting the target format that the - link should be rendered in. - - Returns: - str: - The link, rendered appropriately for the given `target` format - using `description` as its textual description. - - Raises: - ValueError: - If `target` is not `'md'` or `'html'`. - """ - if target == 'html': - return f'<a href="{link}">{description}</a>' - elif target == 'md': - return f'[{description}]({link})' - else: - raise ValueError( - f"Can only template links to `html` or `md`, got `{target}`" - ) - - # `format` here is the result format, we have a link format here instead. - def get(self, request, format=None): # noqa: D102,ANN001,ANN201 - """ - Returns a list of our community rules coupled with their keywords. - - Each item in the returned list is a tuple with the rule as first item - and a list of keywords that match that rules as second item. - """ - link_format = request.query_params.get('link_format', 'md') - if link_format not in ('html', 'md'): - raise ParseError( - f"`format` must be `html` or `md`, got `{format}`." - ) - - discord_community_guidelines = self._format_link( - 'Discord Community Guidelines', - 'https://discordapp.com/guidelines', - link_format - ) - discord_tos = self._format_link( - 'Terms Of Service', - 'https://discordapp.com/terms', - link_format - ) - pydis_coc = self._format_link( - 'Python Discord Code of Conduct', - 'https://pythondiscord.com/pages/code-of-conduct/', - link_format - ) - - return Response([ - ( - f"Follow the {pydis_coc}.", - ["coc", "conduct", "code"] - ), - ( - f"Follow the {discord_community_guidelines} and {discord_tos}.", - ["discord", "guidelines", "discord_tos"] - ), - ( - "Respect staff members and listen to their instructions.", - ["respect", "staff", "instructions"] - ), - ( - "Use English to the best of your ability. " - "Be polite if someone speaks English imperfectly.", - ["english", "language"] - ), - ( - "Do not provide or request help on projects that may break laws, " - "breach terms of services, or are malicious or inappropriate.", - ["infraction", "tos", "breach", "malicious", "inappropriate"] - ), - ( - "Do not post unapproved advertising.", - ["ad", "ads", "advert", "advertising"] - ), - ( - "Keep discussions relevant to the channel topic. " - "Each channel's description tells you the topic.", - ["off-topic", "topic", "relevance"] - ), - ( - "Do not help with ongoing exams. When helping with homework, " - "help people learn how to do the assignment without doing it for them.", - ["exam", "exams", "assignment", "assignments", "homework"] - ), - ( - "Do not offer or ask for paid work of any kind.", - ["paid", "work", "money"] - ), - ( - "Do not copy and paste answers from ChatGPT or similar AI tools.", - ["gpt", "chatgpt", "gpt3", "ai"] - ), - ]) - - -class GitHubArtifactsView(APIView): - """ - Provides utilities for interacting with the GitHub API and obtaining action artifacts. - - ## Routes - ### GET /github/artifacts - Returns a download URL for the artifact requested. - - { - 'url': 'https://pipelines.actions.githubusercontent.com/...' - } - - ### Exceptions - In case of an error, the following body will be returned: - - { - "error_type": "<error class name>", - "error": "<error description>", - "requested_resource": "<owner>/<repo>/<sha>/<artifact_name>" - } - - ## Authentication - Does not require any authentication nor permissions. - """ - - authentication_classes = () - permission_classes = () - - def get( - self, - request: Request, - *, - owner: str, - repo: str, - sha: str, - action_name: str, - artifact_name: str - ) -> Response: - """Return a download URL for the requested artifact.""" - try: - url = github_utils.get_artifact(owner, repo, sha, action_name, artifact_name) - return Response({"url": url}) - except github_utils.ArtifactProcessingError as e: - return Response({ - "error_type": e.__class__.__name__, - "error": str(e), - "requested_resource": f"{owner}/{repo}/{sha}/{action_name}/{artifact_name}" - }, status=e.status) diff --git a/pydis_site/apps/api/viewsets/__init__.py b/pydis_site/apps/api/viewsets/__init__.py deleted file mode 100644 index ec52416a..00000000 --- a/pydis_site/apps/api/viewsets/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# flake8: noqa -from .bot import ( - BotSettingViewSet, - BumpedThreadViewSet, - DeletedMessageViewSet, - DocumentationLinkViewSet, - FilterListViewSet, - InfractionViewSet, - NominationViewSet, - OffensiveMessageViewSet, - AocAccountLinkViewSet, - AocCompletionistBlockViewSet, - OffTopicChannelNameViewSet, - ReminderViewSet, - RoleViewSet, - UserViewSet -) diff --git a/pydis_site/apps/api/viewsets/bot/__init__.py b/pydis_site/apps/api/viewsets/bot/__init__.py deleted file mode 100644 index 262aa59f..00000000 --- a/pydis_site/apps/api/viewsets/bot/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# flake8: noqa -from .filter_list import FilterListViewSet -from .bot_setting import BotSettingViewSet -from .bumped_thread import BumpedThreadViewSet -from .deleted_message import DeletedMessageViewSet -from .documentation_link import DocumentationLinkViewSet -from .infraction import InfractionViewSet -from .nomination import NominationViewSet -from .off_topic_channel_name import OffTopicChannelNameViewSet -from .offensive_message import OffensiveMessageViewSet -from .aoc_link import AocAccountLinkViewSet -from .aoc_completionist_block import AocCompletionistBlockViewSet -from .reminder import ReminderViewSet -from .role import RoleViewSet -from .user import UserViewSet diff --git a/pydis_site/apps/api/viewsets/bot/aoc_completionist_block.py b/pydis_site/apps/api/viewsets/bot/aoc_completionist_block.py deleted file mode 100644 index 97efb63c..00000000 --- a/pydis_site/apps/api/viewsets/bot/aoc_completionist_block.py +++ /dev/null @@ -1,73 +0,0 @@ -from django_filters.rest_framework import DjangoFilterBackend -from rest_framework.mixins import ( - CreateModelMixin, DestroyModelMixin, ListModelMixin, RetrieveModelMixin -) -from rest_framework.viewsets import GenericViewSet - -from pydis_site.apps.api.models.bot import AocCompletionistBlock -from pydis_site.apps.api.serializers import AocCompletionistBlockSerializer - - -class AocCompletionistBlockViewSet( - GenericViewSet, CreateModelMixin, DestroyModelMixin, RetrieveModelMixin, ListModelMixin -): - """ - View providing management for Users blocked from gettign the AoC completionist Role. - - ## Routes - - ### GET /bot/aoc-completionist-blocks/ - Returns all the AoC completionist blocks - - #### Response format - >>> [ - ... { - ... "user": 2, - ... "is_blocked": False, - ... "reason": "Too good to be true" - ... } - ... ] - - - ### GET /bot/aoc-completionist-blocks/<user__id:int> - Retrieve a single Block by User ID - - #### Response format - >>> - ... { - ... "user": 2, - ... "is_blocked": False, - ... "reason": "Too good to be true" - ... } - - #### Status codes - - 200: returned on success - - 404: returned if an AoC completionist block with the given `user__id` was not found. - - ### POST /bot/aoc-completionist-blocks - Adds a single AoC completionist block - - #### Request body - >>> { - ... "user": int, - ... "is_blocked": bool, - ... "reason": string - ... } - - #### Status codes - - 204: returned on success - - 400: if one of the given fields is invalid - - ### DELETE /bot/aoc-completionist-blocks/<user__id:int> - Deletes the AoC Completionist block item with the given `user__id`. - - #### Status codes - - 204: returned on success - - 404: returned if the AoC Completionist block with the given `user__id` was not found - - """ - - serializer_class = AocCompletionistBlockSerializer - queryset = AocCompletionistBlock.objects.all() - filter_backends = (DjangoFilterBackend,) - filterset_fields = ("user__id", "is_blocked") diff --git a/pydis_site/apps/api/viewsets/bot/aoc_link.py b/pydis_site/apps/api/viewsets/bot/aoc_link.py deleted file mode 100644 index 3cdc342d..00000000 --- a/pydis_site/apps/api/viewsets/bot/aoc_link.py +++ /dev/null @@ -1,71 +0,0 @@ -from django_filters.rest_framework import DjangoFilterBackend -from rest_framework.mixins import ( - CreateModelMixin, DestroyModelMixin, ListModelMixin, RetrieveModelMixin -) -from rest_framework.viewsets import GenericViewSet - -from pydis_site.apps.api.models.bot import AocAccountLink -from pydis_site.apps.api.serializers import AocAccountLinkSerializer - - -class AocAccountLinkViewSet( - GenericViewSet, CreateModelMixin, DestroyModelMixin, RetrieveModelMixin, ListModelMixin -): - """ - View providing management for Users who linked their AoC accounts to their Discord Account. - - ## Routes - - ### GET /bot/aoc-account-links - Returns all the AoC account links - - #### Response format - >>> [ - ... { - ... "user": 2, - ... "aoc_username": "AoCUser1" - ... }, - ... ... - ... ] - - - ### GET /bot/aoc-account-links/<user__id:int> - Retrieve a AoC account link by User ID - - #### Response format - >>> - ... { - ... "user": 2, - ... "aoc_username": "AoCUser1" - ... } - - #### Status codes - - 200: returned on success - - 404: returned if an AoC account link with the given `user__id` was not found. - - ### POST /bot/aoc-account-links - Adds a single AoC account link block - - #### Request body - >>> { - ... 'user': int, - ... 'aoc_username': str - ... } - - #### Status codes - - 204: returned on success - - 400: if one of the given fields was invalid - - ### DELETE /bot/aoc-account-links/<user__id:int> - Deletes the AoC account link item with the given `user__id`. - - #### Status codes - - 204: returned on success - - 404: returned if the AoC account link with the given `user__id` was not found - - """ - - serializer_class = AocAccountLinkSerializer - queryset = AocAccountLink.objects.all() - filter_backends = (DjangoFilterBackend,) - filterset_fields = ("user__id", "aoc_username") diff --git a/pydis_site/apps/api/viewsets/bot/bot_setting.py b/pydis_site/apps/api/viewsets/bot/bot_setting.py deleted file mode 100644 index 07f5b170..00000000 --- a/pydis_site/apps/api/viewsets/bot/bot_setting.py +++ /dev/null @@ -1,12 +0,0 @@ -from rest_framework.mixins import RetrieveModelMixin, UpdateModelMixin -from rest_framework.viewsets import GenericViewSet - -from pydis_site.apps.api.models.bot.bot_setting import BotSetting -from pydis_site.apps.api.serializers import BotSettingSerializer - - -class BotSettingViewSet(RetrieveModelMixin, UpdateModelMixin, GenericViewSet): - """View providing update operations on bot setting routes.""" - - serializer_class = BotSettingSerializer - queryset = BotSetting.objects.all() diff --git a/pydis_site/apps/api/viewsets/bot/bumped_thread.py b/pydis_site/apps/api/viewsets/bot/bumped_thread.py deleted file mode 100644 index 9d77bb6b..00000000 --- a/pydis_site/apps/api/viewsets/bot/bumped_thread.py +++ /dev/null @@ -1,66 +0,0 @@ -from rest_framework.mixins import ( - CreateModelMixin, DestroyModelMixin, ListModelMixin -) -from rest_framework.request import Request -from rest_framework.response import Response -from rest_framework.viewsets import GenericViewSet - -from pydis_site.apps.api.models.bot import BumpedThread -from pydis_site.apps.api.serializers import BumpedThreadSerializer - - -class BumpedThreadViewSet( - GenericViewSet, CreateModelMixin, DestroyModelMixin, ListModelMixin -): - """ - View providing CRUD (Minus the U) operations on threads to be bumped. - - ## Routes - ### GET /bot/bumped-threads - Returns all BumpedThread items in the database. - - #### Response format - >>> list[int] - - #### Status codes - - 200: returned on success - - 401: returned if unauthenticated - - ### GET /bot/bumped-threads/<thread_id:int> - Returns whether a specific BumpedThread exists in the database. - - #### Status codes - - 204: returned on success - - 404: returned if a BumpedThread with the given thread_id was not found. - - ### POST /bot/bumped-threads - Adds a single BumpedThread item to the database. - - #### Request body - >>> { - ... 'thread_id': int, - ... } - - #### Status codes - - 201: returned on success - - 400: if one of the given fields is invalid - - ### DELETE /bot/bumped-threads/<thread_id:int> - Deletes the BumpedThread item with the given `thread_id`. - - #### Status codes - - 204: returned on success - - 404: if a BumpedThread with the given `thread_id` does not exist - """ - - serializer_class = BumpedThreadSerializer - queryset = BumpedThread.objects.all() - - def retrieve(self, request: Request, *args, **kwargs) -> Response: - """ - DRF method for checking if the given BumpedThread exists. - - Called by the Django Rest Framework in response to the corresponding HTTP request. - """ - self.get_object() - return Response(status=204) diff --git a/pydis_site/apps/api/viewsets/bot/deleted_message.py b/pydis_site/apps/api/viewsets/bot/deleted_message.py deleted file mode 100644 index c14171bd..00000000 --- a/pydis_site/apps/api/viewsets/bot/deleted_message.py +++ /dev/null @@ -1,40 +0,0 @@ -from rest_framework.mixins import CreateModelMixin -from rest_framework.viewsets import GenericViewSet - -from pydis_site.apps.api.models.bot.message_deletion_context import MessageDeletionContext -from pydis_site.apps.api.serializers import MessageDeletionContextSerializer - - -class DeletedMessageViewSet(CreateModelMixin, GenericViewSet): - """ - View providing support for posting bulk deletion logs generated by the bot. - - ## Routes - ### POST /bot/deleted-messages - Post messages from bulk deletion logs. - - #### Body schema - >>> { - ... # The member ID of the original actor, if applicable. - ... # If a member ID is given, it must be present on the site. - ... 'actor': Optional[int] - ... 'creation': datetime, - ... 'messages': [ - ... { - ... 'id': int, - ... 'author': int, - ... 'channel_id': int, - ... 'content': str, - ... 'embeds': [ - ... # Discord embed objects - ... ] - ... } - ... ] - ... } - - #### Status codes - - 204: returned on success - """ - - queryset = MessageDeletionContext.objects.all() - serializer_class = MessageDeletionContextSerializer diff --git a/pydis_site/apps/api/viewsets/bot/documentation_link.py b/pydis_site/apps/api/viewsets/bot/documentation_link.py deleted file mode 100644 index 6432d344..00000000 --- a/pydis_site/apps/api/viewsets/bot/documentation_link.py +++ /dev/null @@ -1,72 +0,0 @@ -from rest_framework.mixins import ( - CreateModelMixin, DestroyModelMixin, - ListModelMixin, RetrieveModelMixin -) -from rest_framework.viewsets import GenericViewSet - -from pydis_site.apps.api.models.bot.documentation_link import DocumentationLink -from pydis_site.apps.api.serializers import DocumentationLinkSerializer - - -class DocumentationLinkViewSet( - CreateModelMixin, DestroyModelMixin, ListModelMixin, RetrieveModelMixin, GenericViewSet -): - """ - View providing management of documentation links used in the bot's `Doc` cog. - - ## Routes - ### GET /bot/documentation-links - Retrieve all currently stored entries from the database. - - #### Response format - >>> [ - ... { - ... 'package': 'flask', - ... 'base_url': 'https://flask.pocoo.org/docs/dev', - ... 'inventory_url': 'https://flask.pocoo.org/docs/objects.inv' - ... }, - ... # ... - ... ] - - #### Status codes - - 200: returned on success - - ### GET /bot/documentation-links/<package:str> - Look up the documentation object for the given `package`. - - #### Response format - >>> { - ... 'package': 'flask', - ... 'base_url': 'https://flask.pocoo.org/docs/dev', - ... 'inventory_url': 'https://flask.pocoo.org/docs/objects.inv' - ... } - - #### Status codes - - 200: returned on success - - 404: if no entry for the given `package` exists - - ### POST /bot/documentation-links - Create a new documentation link object. - - #### Body schema - >>> { - ... 'package': str, - ... 'base_url': URL, - ... 'inventory_url': URL - ... } - - #### Status codes - - 201: returned on success - - 400: if the request body has invalid fields, see the response for details - - ### DELETE /bot/documentation-links/<package:str> - Delete the entry for the given `package`. - - #### Status codes - - 204: returned on success - - 404: if the given `package` could not be found - """ - - queryset = DocumentationLink.objects.all() - serializer_class = DocumentationLinkSerializer - lookup_field = 'package' diff --git a/pydis_site/apps/api/viewsets/bot/filter_list.py b/pydis_site/apps/api/viewsets/bot/filter_list.py deleted file mode 100644 index 4b05acee..00000000 --- a/pydis_site/apps/api/viewsets/bot/filter_list.py +++ /dev/null @@ -1,98 +0,0 @@ -from rest_framework.decorators import action -from rest_framework.request import Request -from rest_framework.response import Response -from rest_framework.viewsets import ModelViewSet - -from pydis_site.apps.api.models.bot.filter_list import FilterList -from pydis_site.apps.api.serializers import FilterListSerializer - - -class FilterListViewSet(ModelViewSet): - """ - View providing CRUD operations on items allowed or denied by our bot. - - ## Routes - ### GET /bot/filter-lists - Returns all filterlist items in the database. - - #### Response format - >>> [ - ... { - ... 'id': "2309268224", - ... 'created_at': "01-01-2020 ...", - ... 'updated_at': "01-01-2020 ...", - ... 'type': "file_format", - ... 'allowed': 'true', - ... 'content': ".jpeg", - ... 'comment': "Popular image format.", - ... }, - ... ... - ... ] - - #### Status codes - - 200: returned on success - - 401: returned if unauthenticated - - ### GET /bot/filter-lists/<id:int> - Returns a specific FilterList item from the database. - - #### Response format - >>> { - ... 'id': "2309268224", - ... 'created_at': "01-01-2020 ...", - ... 'updated_at': "01-01-2020 ...", - ... 'type': "file_format", - ... 'allowed': 'true', - ... 'content': ".jpeg", - ... 'comment': "Popular image format.", - ... } - - #### Status codes - - 200: returned on success - - 404: returned if the id was not found. - - ### GET /bot/filter-lists/get-types - Returns a list of valid list types that can be used in POST requests. - - #### Response format - >>> [ - ... ["GUILD_INVITE","Guild Invite"], - ... ["FILE_FORMAT","File Format"], - ... ["DOMAIN_NAME","Domain Name"], - ... ["FILTER_TOKEN","Filter Token"], - ... ["REDIRECT", "Redirect"] - ... ] - - #### Status codes - - 200: returned on success - - ### POST /bot/filter-lists - Adds a single FilterList item to the database. - - #### Request body - >>> { - ... 'type': str, - ... 'allowed': bool, - ... 'content': str, - ... 'comment': Optional[str], - ... } - - #### Status codes - - 201: returned on success - - 400: if one of the given fields is invalid - - ### DELETE /bot/filter-lists/<id:int> - Deletes the FilterList item with the given `id`. - - #### Status codes - - 204: returned on success - - 404: if a tag with the given `id` does not exist - """ - - serializer_class = FilterListSerializer - queryset = FilterList.objects.all() - - @action(detail=False, url_path='get-types', methods=["get"]) - def get_types(self, _: Request) -> Response: - """Get a list of all the types of FilterLists we support.""" - return Response(FilterList.FilterListType.choices) diff --git a/pydis_site/apps/api/viewsets/bot/infraction.py b/pydis_site/apps/api/viewsets/bot/infraction.py deleted file mode 100644 index ec8b83a1..00000000 --- a/pydis_site/apps/api/viewsets/bot/infraction.py +++ /dev/null @@ -1,303 +0,0 @@ -import datetime - -from django.db import IntegrityError -from django.db.models import QuerySet -from django.http.request import HttpRequest -from django_filters.rest_framework import DjangoFilterBackend -from rest_framework.decorators import action -from rest_framework.exceptions import ValidationError -from rest_framework.filters import OrderingFilter, SearchFilter -from rest_framework.mixins import ( - CreateModelMixin, - DestroyModelMixin, - ListModelMixin, - RetrieveModelMixin -) -from rest_framework.response import Response -from rest_framework.viewsets import GenericViewSet - -from pydis_site.apps.api.models.bot.infraction import Infraction -from pydis_site.apps.api.pagination import LimitOffsetPaginationExtended -from pydis_site.apps.api.serializers import ( - ExpandedInfractionSerializer, - InfractionSerializer -) - - -class InfractionViewSet( - CreateModelMixin, - RetrieveModelMixin, - ListModelMixin, - GenericViewSet, - DestroyModelMixin -): - """ - View providing CRUD operations on infractions for Discord users. - - ## Routes - ### GET /bot/infractions - Retrieve all infractions. - May be filtered by the query parameters. - - #### Query parameters - - **active** `bool`: whether the infraction is still active - - **actor__id** `int`: snowflake of the user which applied the infraction - - **hidden** `bool`: whether the infraction is a shadow infraction - - **limit** `int`: number of results return per page (default 100) - - **offset** `int`: the initial index from which to return the results (default 0) - - **search** `str`: regular expression applied to the infraction's reason - - **type** `str`: the type of the infraction - - **types** `str`: comma separated sequence of types to filter for - - **user__id** `int`: snowflake of the user to which the infraction was applied - - **ordering** `str`: comma-separated sequence of fields to order the returned results - - **permanent** `bool`: whether or not to retrieve permanent infractions (default True) - - **expires_after** `isodatetime`: the earliest expires_at time to return infractions for - - **expires_before** `isodatetime`: the latest expires_at time to return infractions for - - Invalid query parameters are ignored. - Only one of `type` and `types` may be provided. If both `expires_before` and `expires_after` - are provided, `expires_after` must come after `expires_before`. - If `permanent` is provided and true, `expires_before` and `expires_after` must not be provided. - - #### Response format - Response is paginated but the result is returned without any pagination metadata. - >>> [ - ... { - ... 'id': 5, - ... 'inserted_at': '2018-11-22T07:24:06.132307Z', - ... 'expires_at': '5018-11-20T15:52:00Z', - ... 'active': False, - ... 'user': 172395097705414656, - ... 'actor': 125435062127820800, - ... 'type': 'ban', - ... 'reason': 'He terk my jerb!', - ... 'hidden': True, - ... 'dm_sent': True, - ... 'jump_url': '<discord message link>' - ... } - ... ] - - #### Status codes - - 200: returned on success - - ### GET /bot/infractions/<id:int> - Retrieve a single infraction by ID. - - #### Response format - See `GET /bot/infractions`. - - #### Status codes - - 200: returned on success - - 404: if an infraction with the given `id` could not be found - - ### POST /bot/infractions - Create a new infraction and return the created infraction. - Only `actor`, `type`, and `user` are required. - The `actor` and `user` must be users known by the site. - - #### Request body - >>> { - ... 'active': False, - ... 'actor': 125435062127820800, - ... 'expires_at': '5018-11-20T15:52:00+00:00', - ... 'hidden': True, - ... 'type': 'ban', - ... 'reason': 'He terk my jerb!', - ... 'user': 172395097705414656, - ... 'dm_sent': False, - ... 'jump_url': '<discord message link>' - ... } - - #### Response format - See `GET /bot/infractions`. - - #### Status codes - - 201: returned on success - - 400: if a given user is unknown or a field in the request body is invalid - - ### PATCH /bot/infractions/<id:int> - Update the infraction with the given `id` and return the updated infraction. - Only `active`, `reason`, and `expires_at` may be updated. - - #### Request body - >>> { - ... 'active': True, - ... 'expires_at': '4143-02-15T21:04:31+00:00', - ... 'reason': 'durka derr', - ... 'dm_sent': True - ... } - - #### Response format - See `GET /bot/infractions`. - - #### Status codes - - 200: returned on success - - 400: if a field in the request body is invalid or disallowed - - 404: if an infraction with the given `id` could not be found - - ### DELETE /bot/infractions/<id:int> - Delete the infraction with the given `id`. - - #### Status codes - - 204: returned on success - - 404: if an infraction with the given `id` does not exist - - ### Expanded routes - All routes support expansion of `user` and `actor` in responses. To use an expanded route, - append `/expanded` to the end of the route e.g. `GET /bot/infractions/expanded`. - - #### Response format - See `GET /bot/users/<snowflake:int>` for the expanded formats of `user` and `actor`. Responses - are otherwise identical to their non-expanded counterparts. - """ - - serializer_class = InfractionSerializer - queryset = Infraction.objects.all() - pagination_class = LimitOffsetPaginationExtended - filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter) - filterset_fields = ('user__id', 'actor__id', 'active', 'hidden', 'type') - search_fields = ('$reason',) - frozen_fields = ('id', 'inserted_at', 'type', 'user', 'actor', 'hidden') - - def partial_update(self, request: HttpRequest, *_args, **_kwargs) -> Response: - """Method that handles the nuts and bolts of updating an Infraction.""" - for field in request.data: - if field in self.frozen_fields: - raise ValidationError({field: ['This field cannot be updated.']}) - - instance = self.get_object() - serializer = self.get_serializer(instance, data=request.data, partial=True) - serializer.is_valid(raise_exception=True) - serializer.save() - - return Response(serializer.data) - - def get_queryset(self) -> QuerySet: - """ - Called to fetch the initial queryset, used to implement some of the more complex filters. - - This provides the `permanent` and the `expires_gte` and `expires_lte` options. - """ - filter_permanent = self.request.query_params.get('permanent') - additional_filters = {} - if filter_permanent is not None: - additional_filters['expires_at__isnull'] = filter_permanent.lower() == 'true' - - filter_expires_after = self.request.query_params.get('expires_after') - if filter_expires_after: - try: - expires_after_parsed = datetime.datetime.fromisoformat(filter_expires_after) - except ValueError: - raise ValidationError({'expires_after': ['failed to convert to datetime']}) - additional_filters['expires_at__gte'] = expires_after_parsed.replace( - tzinfo=datetime.timezone.utc - ) - - filter_expires_before = self.request.query_params.get('expires_before') - if filter_expires_before: - try: - expires_before_parsed = datetime.datetime.fromisoformat(filter_expires_before) - except ValueError: - raise ValidationError({'expires_before': ['failed to convert to datetime']}) - additional_filters['expires_at__lte'] = expires_before_parsed.replace( - tzinfo=datetime.timezone.utc - ) - - if 'expires_at__lte' in additional_filters and 'expires_at__gte' in additional_filters: - if additional_filters['expires_at__gte'] > additional_filters['expires_at__lte']: - raise ValidationError({ - 'expires_before': ['cannot be after expires_after'], - 'expires_after': ['cannot be before expires_before'], - }) - - if ( - ('expires_at__lte' in additional_filters or 'expires_at__gte' in additional_filters) - and 'expires_at__isnull' in additional_filters - and additional_filters['expires_at__isnull'] - ): - raise ValidationError({ - 'permanent': [ - 'cannot filter for permanent infractions at the' - ' same time as expires_at or expires_before', - ] - }) - - if filter_expires_before: - # Filter out permanent infractions specifically if we want ones that will expire - # before a given date - additional_filters['expires_at__isnull'] = False - - filter_types = self.request.query_params.get('types') - if filter_types: - if self.request.query_params.get('type'): - raise ValidationError({ - 'types': ['you must provide only one of "type" or "types"'], - }) - additional_filters['type__in'] = [i.strip() for i in filter_types.split(",")] - - return self.queryset.filter(**additional_filters) - - @action(url_path='expanded', detail=False) - def list_expanded(self, *args, **kwargs) -> Response: - """ - DRF method for listing Infraction entries. - - Called by the Django Rest Framework in response to the corresponding HTTP request. - """ - self.serializer_class = ExpandedInfractionSerializer - return self.list(*args, **kwargs) - - @list_expanded.mapping.post - def create_expanded(self, *args, **kwargs) -> Response: - """ - DRF method for creating an Infraction. - - Called by the Django Rest Framework in response to the corresponding HTTP request. - """ - self.serializer_class = ExpandedInfractionSerializer - return self.create(*args, **kwargs) - - @action(url_path='expanded', url_name='detail-expanded', detail=True) - def retrieve_expanded(self, *args, **kwargs) -> Response: - """ - DRF method for retrieving a specific Infraction. - - Called by the Django Rest Framework in response to the corresponding HTTP request. - """ - self.serializer_class = ExpandedInfractionSerializer - return self.retrieve(*args, **kwargs) - - @retrieve_expanded.mapping.patch - def partial_update_expanded(self, *args, **kwargs) -> Response: - """ - DRF method for updating an Infraction. - - Called by the Django Rest Framework in response to the corresponding HTTP request. - """ - self.serializer_class = ExpandedInfractionSerializer - return self.partial_update(*args, **kwargs) - - def create(self, request: HttpRequest, *args, **kwargs) -> Response: - """ - Create an infraction for a target user. - - Called by the Django Rest Framework in response to the corresponding HTTP request. - """ - try: - return super().create(request, *args, **kwargs) - except IntegrityError as err: - # We need to use `__cause__` here, as Django reraises the internal - # UniqueViolation emitted by psycopg2 (which contains the attribute - # that we actually need) - # - # _meta is documented and mainly named that way to prevent - # name clashes: https://docs.djangoproject.com/en/dev/ref/models/meta/ - if err.__cause__.diag.constraint_name == Infraction._meta.constraints[0].name: - raise ValidationError( - { - 'non_field_errors': [ - 'This user already has an active infraction of this type.', - ] - } - ) - raise # pragma: no cover - no other constraint to test with diff --git a/pydis_site/apps/api/viewsets/bot/nomination.py b/pydis_site/apps/api/viewsets/bot/nomination.py deleted file mode 100644 index 78687e0e..00000000 --- a/pydis_site/apps/api/viewsets/bot/nomination.py +++ /dev/null @@ -1,321 +0,0 @@ -from collections import ChainMap - -from django.http.request import HttpRequest -from django.utils import timezone -from django_filters.rest_framework import DjangoFilterBackend -from rest_framework import status -from rest_framework.exceptions import ValidationError -from rest_framework.filters import OrderingFilter, SearchFilter -from rest_framework.mixins import ( - CreateModelMixin, - ListModelMixin, - RetrieveModelMixin, -) -from rest_framework.response import Response -from rest_framework.viewsets import GenericViewSet - -from pydis_site.apps.api.models.bot import Nomination, NominationEntry -from pydis_site.apps.api.serializers import NominationEntrySerializer, NominationSerializer - - -class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, GenericViewSet): - """ - View providing CRUD operations on helper nominations done through the bot. - - ## Routes - ### GET /bot/nominations - Retrieve all nominations. - May be filtered and ordered by the query parameters. - - #### Query parameters - - **active** `bool`: whether the nomination is still active - - **user__id** `int`: snowflake of the user who received the nomination - - **ordering** `str`: comma-separated sequence of fields to order the returned results - - Invalid query parameters are ignored. - - #### Response format - >>> [ - ... { - ... 'id': 1, - ... 'active': false, - ... 'user': 336843820513755157, - ... 'inserted_at': '2019-04-25T14:02:37.775587Z', - ... 'end_reason': 'They were helpered after a staff-vote', - ... 'ended_at': '2019-04-26T15:12:22.123587Z', - ... 'entries': [ - ... { - ... 'actor': 336843820513755157, - ... 'reason': 'They know how to explain difficult concepts', - ... 'inserted_at': '2019-04-25T14:02:37.775587Z' - ... } - ... ], - ... 'reviewed': true - ... } - ... ] - - #### Status codes - - 200: returned on success - - ### GET /bot/nominations/<id:int> - Retrieve a single nomination by ID. - - ### Response format - >>> { - ... 'id': 1, - ... 'active': true, - ... 'user': 336843820513755157, - ... 'inserted_at': '2019-04-25T14:02:37.775587Z', - ... 'end_reason': 'They were helpered after a staff-vote', - ... 'ended_at': '2019-04-26T15:12:22.123587Z', - ... 'entries': [ - ... { - ... 'actor': 336843820513755157, - ... 'reason': 'They know how to explain difficult concepts', - ... 'inserted_at': '2019-04-25T14:02:37.775587Z' - ... } - ... ], - ... 'reviewed': false - ... } - - ### Status codes - - 200: returned on success - - 404: returned if a nomination with the given `id` could not be found - - ### POST /bot/nominations - Create a new, active nomination returns the created nominations. - The `user`, `reason` and `actor` fields are required and the `user` - and `actor` need to know by the site. Providing other valid fields - is not allowed and invalid fields are ignored. If `user` already has an - active nomination, a new nomination entry will be created and assigned to the - active nomination. - - #### Request body - >>> { - ... 'actor': 409107086526644234 - ... 'reason': 'He would make a great helper', - ... 'user': 409107086526644234 - ... } - - #### Response format - See `GET /bot/nominations/<id:int>` - - #### Status codes - - 201: returned on success - - 400: returned on failure for one of the following reasons: - - The `user` or `actor` are unknown to the site; - - The request contained a field that cannot be set at creation. - - ### PATCH /bot/nominations/<id:int> - Update or end the nomination with the given `id` and return the updated nomination. - - The PATCH route can be used for three distinct operations: - 1. Updating the `reason` of `active` nomination; - 2. Ending an `active` nomination; - 3. Updating the `end_reason` or `reason` field of an `inactive` nomination. - 4. Updating `reviewed` field of `active` nomination. - - While the response format and status codes are the same for all three operations (see - below), the request bodies vary depending on the operation. For all operations it holds - that providing other valid fields is not allowed and invalid fields are ignored. - - ### 1. Updating the `reason` of `active` nomination. The `actor` field is required. - - #### Request body - >>> { - ... 'reason': 'He would make a great helper', - ... 'actor': 409107086526644234 - ... } - - #### Response format - See `GET /bot/nominations/<id:int>` - - #### Status codes - - 200: returned on success - - 400: if a field in the request body is invalid or disallowed - - 404: if an infraction with the given `id` could not be found - - ### 2. Ending an `active` nomination - - #### Request body - >>> { - ... 'active': False - ... 'end_reason': 'They've been added to the Helpers team', - ... } - - See operation 1 for the response format and status codes. - - ### 3. Updating the `end_reason` or `reason` field of an `inactive` nomination. - Actor field is required when updating reason. - - #### Request body - >>> { - ... 'reason': 'Updated reason for this nomination', - ... 'actor': 409107086526644234, - ... 'end_reason': 'Updated end_reason for this nomination', - ... } - - Note: The request body may contain either or both fields. - - See operation 1 for the response format and status codes. - - ### 4. Setting nomination `reviewed` - - #### Request body - >>> { - ... 'reviewed': True - ... } - - See operation 1 for the response format and status codes. - """ - - serializer_class = NominationSerializer - queryset = Nomination.objects.all() - filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter) - filterset_fields = ('user__id', 'active') - frozen_fields = ('id', 'inserted_at', 'user', 'ended_at') - frozen_on_create = ('ended_at', 'end_reason', 'active', 'inserted_at', 'reviewed') - - def create(self, request: HttpRequest, *args, **kwargs) -> Response: - """ - DRF method for creating a Nomination. - - Called by the Django Rest Framework in response to the corresponding HTTP request. - """ - for field in request.data: - if field in self.frozen_on_create: - raise ValidationError({field: ['This field cannot be set at creation.']}) - - user_id = request.data.get("user") - nomination_filter = Nomination.objects.filter(active=True, user__id=user_id) - - if not nomination_filter.exists(): - serializer = NominationSerializer( - data=ChainMap( - request.data, - {"active": True} - ) - ) - serializer.is_valid(raise_exception=True) - nomination = Nomination.objects.create(**serializer.validated_data) - - # The serializer will truncate and get rid of excessive data - entry_serializer = NominationEntrySerializer( - data=ChainMap(request.data, {"nomination": nomination.id}) - ) - entry_serializer.is_valid(raise_exception=True) - NominationEntry.objects.create(**entry_serializer.validated_data) - - data = NominationSerializer(nomination).data - - headers = self.get_success_headers(data) - return Response(data, status=status.HTTP_201_CREATED, headers=headers) - - entry_serializer = NominationEntrySerializer( - data=ChainMap(request.data, {"nomination": nomination_filter[0].id}) - ) - entry_serializer.is_valid(raise_exception=True) - - # Don't allow a user to create many nomination entries in a single nomination - if NominationEntry.objects.filter( - nomination_id=nomination_filter[0].id, - actor__id=entry_serializer.validated_data["actor"].id - ).exists(): - raise ValidationError( - {'actor': ['This actor has already endorsed this nomination.']} - ) - - NominationEntry.objects.create(**entry_serializer.validated_data) - - data = NominationSerializer(nomination_filter[0]).data - - headers = self.get_success_headers(data) - return Response(data, status=status.HTTP_201_CREATED, headers=headers) - - def partial_update(self, request: HttpRequest, *args, **kwargs) -> Response: - """ - DRF method for updating a Nomination. - - Called by the Django Rest Framework in response to the corresponding HTTP request. - """ - for field in request.data: - if field in self.frozen_fields: - raise ValidationError({field: ['This field cannot be updated.']}) - - instance = self.get_object() - serializer = self.get_serializer(instance, data=request.data, partial=True) - serializer.is_valid(raise_exception=True) - - data = serializer.validated_data - - # There are three distinct PATCH scenarios we need to validate. - if instance.active and 'active' not in data: - # 1. We're updating an active nomination without ending it. - if 'end_reason' in data: - raise ValidationError( - {'end_reason': ["An active nomination can't have an end reason."]} - ) - - elif instance.active and not data['active']: - # 2. We're ending an active nomination. - if 'reason' in request.data: - raise ValidationError( - {'reason': ['This field cannot be set when ending a nomination.']} - ) - - if 'end_reason' not in request.data: - raise ValidationError( - {'end_reason': ['This field is required when ending a nomination.']} - ) - - if 'reviewed' in request.data: - raise ValidationError( - {'reviewed': ['This field cannot be set while you are ending a nomination.']} - ) - - if 'thread_id' in request.data: - raise ValidationError( - {'thread_id': ['This field cannot be set when ending a nomination.']} - ) - - instance.ended_at = timezone.now() - - elif 'active' in data: - # 3. The `active` field is only allowed when ending a nomination. - raise ValidationError( - {'active': ['This field can only be used to end a nomination']} - ) - - elif not instance.active and 'reviewed' in request.data: - raise ValidationError( - {'reviewed': ['This field cannot be set if the nomination is inactive.']} - ) - - elif not instance.active and 'thread_id' in request.data: - raise ValidationError( - {'thread_id': ['This field cannot be set if the nomination is inactive.']} - ) - - if 'reason' in request.data: - if 'actor' not in request.data: - raise ValidationError( - {'actor': ['This field is required when editing the reason.']} - ) - - entry_filter = NominationEntry.objects.filter( - nomination_id=instance.id, - actor__id=request.data['actor'] - ) - - if not entry_filter.exists(): - raise ValidationError( - {'actor': ["The actor doesn't exist or has not nominated the user."]} - ) - - entry = entry_filter[0] - entry.reason = request.data['reason'] - entry.save() - - serializer.save() - - return Response(serializer.data) diff --git a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py deleted file mode 100644 index d0519e86..00000000 --- a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py +++ /dev/null @@ -1,141 +0,0 @@ -from django.db.models import Case, Value, When -from django.db.models.query import QuerySet -from django.shortcuts import get_object_or_404 -from rest_framework.exceptions import ParseError -from rest_framework.request import Request -from rest_framework.response import Response -from rest_framework.status import HTTP_201_CREATED -from rest_framework.viewsets import ModelViewSet - -from pydis_site.apps.api.models.bot.off_topic_channel_name import OffTopicChannelName -from pydis_site.apps.api.serializers import OffTopicChannelNameSerializer - - -class OffTopicChannelNameViewSet(ModelViewSet): - """ - View of off-topic channel names used by the bot to rotate our off-topic names on a daily basis. - - ## Routes - ### GET /bot/off-topic-channel-names - Return all known off-topic channel names from the database. - If the `random_items` query parameter is given, for example using... - $ curl 127.0.0.1:8000/api/bot/off-topic-channel-names?random_items=5 - ... then the API will return `5` random items from the database - that is not used in current rotation. - When running out of names, API will mark all names to not used and start new rotation. - - #### Response format - Return a list of off-topic-channel names: - >>> [ - ... "lemons-lemonade-stand", - ... "bbq-with-bisk" - ... ] - - #### Status codes - - 200: returned on success - - 400: returned when `random_items` is not a positive integer - - ### POST /bot/off-topic-channel-names - Create a new off-topic-channel name in the database. - The name must be given as a query parameter, for example: - $ curl 127.0.0.1:8000/api/bot/off-topic-channel-names?name=lemons-lemonade-shop - - #### Status codes - - 201: returned on success - - 400: if the request body has invalid fields, see the response for details - - ### DELETE /bot/off-topic-channel-names/<name:str> - Delete the off-topic-channel name with the given `name`. - - #### Status codes - - 204: returned on success - - 404: returned when the given `name` was not found - - ## Authentication - Requires a API token. - """ - - lookup_field = 'name' - serializer_class = OffTopicChannelNameSerializer - queryset = OffTopicChannelName.objects.all() - - def get_object(self) -> OffTopicChannelName: - """ - Returns the OffTopicChannelName entry for this request, if it exists. - - If it doesn't, a HTTP 404 is returned by way of throwing an exception. - """ - name = self.kwargs[self.lookup_field] - return get_object_or_404(self.queryset, name=name) - - def get_queryset(self) -> QuerySet: - """Returns a queryset that covers the entire OffTopicChannelName table.""" - return OffTopicChannelName.objects.all() - - def create(self, request: Request, *args, **kwargs) -> Response: - """ - DRF method for creating a new OffTopicChannelName. - - Called by the Django Rest Framework in response to the corresponding HTTP request. - """ - if 'name' in request.query_params: - create_data = {'name': request.query_params['name']} - serializer = OffTopicChannelNameSerializer(data=create_data) - serializer.is_valid(raise_exception=True) - serializer.save() - return Response(create_data, status=HTTP_201_CREATED) - - else: - raise ParseError(detail={ - 'name': ["This query parameter is required."] - }) - - def list(self, request: Request, *args, **kwargs) -> Response: - """ - DRF method for listing OffTopicChannelName entries. - - Called by the Django Rest Framework in response to the corresponding HTTP request. - """ - if 'random_items' in request.query_params: - param = request.query_params['random_items'] - try: - random_count = int(param) - except ValueError: - raise ParseError(detail={'random_items': ["Must be a valid integer."]}) - - if random_count <= 0: - raise ParseError(detail={ - 'random_items': ["Must be a positive integer."] - }) - - queryset = self.queryset.filter(active=True).order_by('used', '?')[:random_count] - - # When any name is used in our listing then this means we reached end of round - # and we need to reset all other names `used` to False - if any(offtopic_name.used for offtopic_name in queryset): - # These names that we just got have to be excluded from updating used to False - self.queryset.update( - used=Case( - When( - name__in=(offtopic_name.name for offtopic_name in queryset), - then=Value(True) - ), - default=Value(False) - ) - ) - else: - # Otherwise mark selected names `used` to True - self.queryset.filter( - name__in=(offtopic_name.name for offtopic_name in queryset) - ).update(used=True) - - serialized = self.serializer_class(queryset, many=True) - return Response(serialized.data) - - params = {} - if active_param := request.query_params.get("active"): - params["active"] = active_param.lower() == "true" - - queryset = self.queryset.filter(**params) - serialized = self.serializer_class(queryset, many=True) - return Response(serialized.data) diff --git a/pydis_site/apps/api/viewsets/bot/offensive_message.py b/pydis_site/apps/api/viewsets/bot/offensive_message.py deleted file mode 100644 index 54cb3a38..00000000 --- a/pydis_site/apps/api/viewsets/bot/offensive_message.py +++ /dev/null @@ -1,61 +0,0 @@ -from rest_framework.mixins import ( - CreateModelMixin, - DestroyModelMixin, - ListModelMixin -) -from rest_framework.viewsets import GenericViewSet - -from pydis_site.apps.api.models.bot.offensive_message import OffensiveMessage -from pydis_site.apps.api.serializers import OffensiveMessageSerializer - - -class OffensiveMessageViewSet( - CreateModelMixin, ListModelMixin, DestroyModelMixin, GenericViewSet -): - """ - View providing CRUD access to offensive messages. - - ## Routes - ### GET /bot/offensive-messages - Returns all offensive messages in the database. - - #### Response format - >>> [ - ... { - ... 'id': '631953598091100200', - ... 'channel_id': '291284109232308226', - ... 'delete_date': '2019-11-01T21:51:15.545000Z' - ... }, - ... ... - ... ] - - #### Status codes - - 200: returned on success - - ### POST /bot/offensive-messages - Create a new offensive message object. - - #### Request body - >>> { - ... 'id': int, - ... 'channel_id': int, - ... 'delete_date': datetime.datetime # ISO-8601-formatted date - ... } - - #### Status codes - - 201: returned on success - - 400: if the body format is invalid - - ### DELETE /bot/offensive-messages/<id:int> - Delete the offensive message object with the given `id`. - - #### Status codes - - 204: returned on success - - 404: if a offensive message object with the given `id` does not exist - - ## Authentication - Requires an API token. - """ - - serializer_class = OffensiveMessageSerializer - queryset = OffensiveMessage.objects.all() diff --git a/pydis_site/apps/api/viewsets/bot/reminder.py b/pydis_site/apps/api/viewsets/bot/reminder.py deleted file mode 100644 index 5f997052..00000000 --- a/pydis_site/apps/api/viewsets/bot/reminder.py +++ /dev/null @@ -1,128 +0,0 @@ -from django_filters.rest_framework import DjangoFilterBackend -from rest_framework.filters import SearchFilter -from rest_framework.mixins import ( - CreateModelMixin, - DestroyModelMixin, - ListModelMixin, - RetrieveModelMixin, - UpdateModelMixin -) -from rest_framework.viewsets import GenericViewSet - -from pydis_site.apps.api.models.bot.reminder import Reminder -from pydis_site.apps.api.serializers import ReminderSerializer - - -class ReminderViewSet( - CreateModelMixin, - RetrieveModelMixin, - ListModelMixin, - DestroyModelMixin, - UpdateModelMixin, - GenericViewSet, -): - """ - View providing CRUD access to reminders. - - ## Routes - ### GET /bot/reminders - Returns all reminders in the database. - - #### Response format - >>> [ - ... { - ... 'active': True, - ... 'author': 1020103901030, - ... 'mentions': [ - ... 336843820513755157, - ... 165023948638126080, - ... 267628507062992896 - ... ], - ... 'content': "Make dinner", - ... 'expiration': '5018-11-20T15:52:00Z', - ... 'id': 11, - ... 'channel_id': 634547009956872193, - ... 'jump_url': "https://discord.com/channels/<guild_id>/<channel_id>/<message_id>", - ... 'failures': 3 - ... }, - ... ... - ... ] - - #### Status codes - - 200: returned on success - - ### GET /bot/reminders/<id:int> - Fetches the reminder with the given id. - - #### Response format - >>> - ... { - ... 'active': True, - ... 'author': 1020103901030, - ... 'mentions': [ - ... 336843820513755157, - ... 165023948638126080, - ... 267628507062992896 - ... ], - ... 'content': "Make dinner", - ... 'expiration': '5018-11-20T15:52:00Z', - ... 'id': 11, - ... 'channel_id': 634547009956872193, - ... 'jump_url': "https://discord.com/channels/<guild_id>/<channel_id>/<message_id>", - ... 'failures': 3 - ... } - - #### Status codes - - 200: returned on success - - 404: returned when the reminder doesn't exist - - ### POST /bot/reminders - Create a new reminder. - - #### Request body - >>> { - ... 'author': int, - ... 'mentions': list[int], - ... 'content': str, - ... 'expiration': str, # ISO-formatted datetime - ... 'channel_id': int, - ... 'jump_url': str - ... } - - #### Status codes - - 201: returned on success - - 400: if the body format is invalid - - 404: if no user with the given ID could be found - - ### PATCH /bot/reminders/<id:int> - Update the user with the given `id`. - All fields in the request body are optional. - - #### Request body - >>> { - ... 'mentions': list[int], - ... 'content': str, - ... 'expiration': str, # ISO-formatted datetime - ... 'failures': int - ... } - - #### Status codes - - 200: returned on success - - 400: if the body format is invalid - - 404: if no user with the given ID could be found - - ### DELETE /bot/reminders/<id:int> - Delete the reminder with the given `id`. - - #### Status codes - - 204: returned on success - - 404: if a reminder with the given `id` does not exist - - ## Authentication - Requires an API token. - """ - - serializer_class = ReminderSerializer - queryset = Reminder.objects.prefetch_related('author') - filter_backends = (DjangoFilterBackend, SearchFilter) - filterset_fields = ('active', 'author__id') diff --git a/pydis_site/apps/api/viewsets/bot/role.py b/pydis_site/apps/api/viewsets/bot/role.py deleted file mode 100644 index 1f82208d..00000000 --- a/pydis_site/apps/api/viewsets/bot/role.py +++ /dev/null @@ -1,107 +0,0 @@ -from rest_framework.viewsets import ModelViewSet - -from pydis_site.apps.api.models.bot.role import Role -from pydis_site.apps.api.serializers import RoleSerializer - - -class RoleViewSet(ModelViewSet): - """ - View providing CRUD access to the roles on our server. - - This is used by the bot to keep a mirror of our server's roles on the site. - - ## Routes - ### GET /bot/roles - Returns all roles in the database. - - #### Response format - >>> [ - ... { - ... 'id': 267628507062992896, - ... 'name': "Admins", - ... 'colour': 1337, - ... 'permissions': 8, - ... 'position': 1 - ... } - ... ] - - #### Status codes - - 200: returned on success - - ### GET /bot/roles/<snowflake:int> - Gets a single role by ID. - - #### Response format - >>> { - ... 'id': 267628507062992896, - ... 'name': "Admins", - ... 'colour': 1337, - ... 'permissions': 8, - ... 'position': 1 - ... } - - #### Status codes - - 200: returned on success - - 404: if a role with the given `snowflake` could not be found - - ### POST /bot/roles - Adds a single new role. - - #### Request body - >>> { - ... 'id': int, - ... 'name': str, - ... 'colour': int, - ... 'permissions': int, - ... 'position': 1, - ... } - - #### Status codes - - 201: returned on success - - 400: if the body format is invalid - - ### PUT /bot/roles/<snowflake:int> - Update the role with the given `snowflake`. - All fields in the request body are required. - - #### Request body - >>> { - ... 'id': int, - ... 'name': str, - ... 'colour': int, - ... 'permissions': int, - ... 'position': 1, - ... } - - #### Status codes - - 200: returned on success - - 400: if the request body was invalid - - 404: if a role with the given `snowflake` does not exist - - ### PATCH /bot/roles/<snowflake:int> - Update the role with the given `snowflake`. - - #### Request body - >>> { - ... 'id': int, - ... 'name': str, - ... 'colour': int, - ... 'permissions': int, - ... 'position': 1, - ... } - - #### Status codes - - 200: returned on success - - 400: if the request body was invalid - - 404: if a role with the given `snowflake` does not exist - - ### DELETE /bot/roles/<snowflake:int> - Deletes the role with the given `snowflake`. - - #### Status codes - - 204: returned on success - - 404: if a role with the given `snowflake` does not exist - """ - - queryset = Role.objects.all() - serializer_class = RoleSerializer diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py deleted file mode 100644 index db73a83c..00000000 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ /dev/null @@ -1,355 +0,0 @@ -import typing -from collections import OrderedDict - -from django.db.models import Q -from django_filters.rest_framework import DjangoFilterBackend -from rest_framework import fields, status -from rest_framework.decorators import action -from rest_framework.exceptions import ParseError -from rest_framework.pagination import PageNumberPagination -from rest_framework.request import Request -from rest_framework.response import Response -from rest_framework.serializers import ModelSerializer -from rest_framework.viewsets import ModelViewSet - -from pydis_site.apps.api.models.bot.infraction import Infraction -from pydis_site.apps.api.models.bot.metricity import Metricity, NotFoundError -from pydis_site.apps.api.models.bot.user import User -from pydis_site.apps.api.serializers import UserSerializer - - -class UserListPagination(PageNumberPagination): - """Custom pagination class for the User Model.""" - - page_size = 2500 - page_size_query_param = "page_size" - - def get_next_page_number(self) -> typing.Optional[int]: - """Get the next page number.""" - if not self.page.has_next(): - return None - page_number = self.page.next_page_number() - return page_number - - def get_previous_page_number(self) -> typing.Optional[int]: - """Get the previous page number.""" - if not self.page.has_previous(): - return None - - page_number = self.page.previous_page_number() - return page_number - - def get_paginated_response(self, data: list) -> Response: - """Override method to send modified response.""" - return Response(OrderedDict([ - ('count', self.page.paginator.count), - ('next_page_no', self.get_next_page_number()), - ('previous_page_no', self.get_previous_page_number()), - ('results', data) - ])) - - -class UserViewSet(ModelViewSet): - """ - View providing CRUD operations on Discord users through the bot. - - ## Routes - ### GET /bot/users - Returns all users currently known with pagination. - - #### Response format - >>> { - ... 'count': 95000, - ... 'next_page_no': "2", - ... 'previous_page_no': None, - ... 'results': [ - ... { - ... 'id': 409107086526644234, - ... 'name': "Python", - ... 'discriminator': 4329, - ... 'roles': [ - ... 352427296948486144, - ... 270988689419665409, - ... 277546923144249364, - ... 458226699344019457 - ... ], - ... 'in_guild': True - ... }, - ... ] - ... } - - #### Optional Query Parameters - - username: username to search for - - discriminator: discriminator to search for - - page_size: number of Users in one page, defaults to 10,000 - - page: page number - - #### Status codes - - 200: returned on success - - ### GET /bot/users/<snowflake:int> - Gets a single user by ID. - - #### Response format - >>> { - ... 'id': 409107086526644234, - ... 'name': "Python", - ... 'discriminator': 4329, - ... 'roles': [ - ... 352427296948486144, - ... 270988689419665409, - ... 277546923144249364, - ... 458226699344019457 - ... ], - ... 'in_guild': True - ... } - - #### Status codes - - 200: returned on success - - 404: if a user with the given `snowflake` could not be found - - ### GET /bot/users/<snowflake:int>/metricity_data - Gets metricity data for a single user by ID. - - #### Response format - >>> { - ... "joined_at": "2020-10-06T21:54:23.540766", - ... "total_messages": 2, - ... "voice_banned": False, - ... "activity_blocks": 1 - ...} - - #### Status codes - - 200: returned on success - - 404: if a user with the given `snowflake` could not be found - - ### GET /bot/users/<snowflake:int>/metricity_review_data - Gets metricity data for a single user's review by ID. - - #### Response format - >>> { - ... 'joined_at': '2020-08-26T08:09:43.507000', - ... 'top_channel_activity': [['off-topic', 15], - ... ['talent-pool', 4], - ... ['defcon', 2]], - ... 'total_messages': 22 - ... } - - #### Status codes - - 200: returned on success - - 404: if a user with the given `snowflake` could not be found - - ### POST /bot/users/metricity_activity_data - Returns a mapping of user ID to message count in a given period for - the given user IDs. - - #### Required Query Parameters - - days: how many days into the past to count message from. - - #### Request Format - >>> [ - ... 409107086526644234, - ... 493839819168808962 - ... ] - - #### Response format - >>> { - ... "409107086526644234": 54, - ... "493839819168808962": 0 - ... } - - #### Status codes - - 200: returned on success - - 400: if request body or query parameters were missing or invalid - - ### POST /bot/users - Adds a single or multiple new users. - The roles attached to the user(s) must be roles known by the site. - Users that already exist in the database will be skipped. - - #### Request body - >>> { - ... 'id': int, - ... 'name': str, - ... 'discriminator': int, - ... 'roles': List[int], - ... 'in_guild': bool - ... } - - Alternatively, request users can be POSTed as a list of above objects, - in which case multiple users will be created at once. In this case, - the response is an empty list. - - #### Status codes - - 201: returned on success - - 400: if one of the given roles does not exist, or one of the given fields is invalid - - 400: if multiple user objects with the same id are given - - ### PUT /bot/users/<snowflake:int> - Update the user with the given `snowflake`. - All fields in the request body are required. - - #### Request body - >>> { - ... 'id': int, - ... 'name': str, - ... 'discriminator': int, - ... 'roles': List[int], - ... 'in_guild': bool - ... } - - #### Status codes - - 200: returned on success - - 400: if the request body was invalid, see response body for details - - 404: if the user with the given `snowflake` could not be found - - ### PATCH /bot/users/<snowflake:int> - Update the user with the given `snowflake`. - All fields in the request body are optional. - - #### Request body - >>> { - ... 'id': int, - ... 'name': str, - ... 'discriminator': int, - ... 'roles': List[int], - ... 'in_guild': bool - ... } - - #### Status codes - - 200: returned on success - - 400: if the request body was invalid, see response body for details - - 404: if the user with the given `snowflake` could not be found - - ### BULK PATCH /bot/users/bulk_patch - Update users with the given `ids` and `details`. - `id` field and at least one other field is mandatory. - - #### Request body - >>> [ - ... { - ... 'id': int, - ... 'name': str, - ... 'discriminator': int, - ... 'roles': List[int], - ... 'in_guild': bool - ... }, - ... { - ... 'id': int, - ... 'name': str, - ... 'discriminator': int, - ... 'roles': List[int], - ... 'in_guild': bool - ... }, - ... ] - - #### Status codes - - 200: returned on success - - 400: if the request body was invalid, see response body for details - - 400: if multiple user objects with the same id are given - - 404: if the user with the given id does not exist - - ### DELETE /bot/users/<snowflake:int> - Deletes the user with the given `snowflake`. - - #### Status codes - - 204: returned on success - - 404: if a user with the given `snowflake` does not exist - """ - - serializer_class = UserSerializer - queryset = User.objects.all().order_by("id") - pagination_class = UserListPagination - filter_backends = (DjangoFilterBackend,) - filterset_fields = ('name', 'discriminator') - - def get_serializer(self, *args, **kwargs) -> ModelSerializer: - """Set Serializer many attribute to True if request body contains a list.""" - if isinstance(kwargs.get('data', {}), list): - kwargs['many'] = True - - return super().get_serializer(*args, **kwargs) - - @action(detail=False, methods=["PATCH"], name='user-bulk-patch') - def bulk_patch(self, request: Request) -> Response: - """Update multiple User objects in a single request.""" - serializer = self.get_serializer( - instance=self.get_queryset(), - data=request.data, - many=True, - partial=True - ) - - serializer.is_valid(raise_exception=True) - serializer.save() - - return Response(serializer.data, status=status.HTTP_200_OK) - - @action(detail=True) - def metricity_data(self, request: Request, pk: str = None) -> Response: - """Request handler for metricity_data endpoint.""" - user = self.get_object() - - has_voice_infraction = Infraction.objects.filter( - Q(user__id=user.id, active=True), - Q(type="voice_ban") | Q(type="voice_mute") - ).exists() - - with Metricity() as metricity: - try: - data = metricity.user(user.id) - - data["total_messages"] = metricity.total_messages(user.id) - data["activity_blocks"] = metricity.total_message_blocks(user.id) - - data["voice_gate_blocked"] = has_voice_infraction - return Response(data, status=status.HTTP_200_OK) - except NotFoundError: - return Response(dict(detail="User not found in metricity"), - status=status.HTTP_404_NOT_FOUND) - - @action(detail=True) - def metricity_review_data(self, request: Request, pk: str = None) -> Response: - """Request handler for metricity_review_data endpoint.""" - user = self.get_object() - - with Metricity() as metricity: - try: - data = metricity.user(user.id) - data["total_messages"] = metricity.total_messages(user.id) - data["top_channel_activity"] = metricity.top_channel_activity(user.id) - return Response(data, status=status.HTTP_200_OK) - except NotFoundError: - return Response(dict(detail="User not found in metricity"), - status=status.HTTP_404_NOT_FOUND) - - @action(detail=False, methods=["POST"]) - def metricity_activity_data(self, request: Request) -> Response: - """Request handler for metricity_activity_data endpoint.""" - if "days" in request.query_params: - try: - days = int(request.query_params["days"]) - except ValueError: - raise ParseError(detail={ - "days": ["This query parameter must be an integer."] - }) - else: - raise ParseError(detail={ - "days": ["This query parameter is required."] - }) - - user_id_list_validator = fields.ListField( - child=fields.IntegerField(min_value=0), - allow_empty=False - ) - user_ids = [ - str(user_id) for user_id in - user_id_list_validator.run_validation(request.data) - ] - - with Metricity() as metricity: - data = metricity.total_messages_in_past_n_days(user_ids, days) - - default_data = {user_id: 0 for user_id in user_ids} - response_data = default_data | dict(data) - return Response(response_data, status=status.HTTP_200_OK) diff --git a/pydis_site/apps/content/README.md b/pydis_site/apps/content/README.md deleted file mode 100644 index e7061207..00000000 --- a/pydis_site/apps/content/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# The "content" app - -This application serves static, Markdown-based content. Django-wise there is -relatively little code in there; most of it is concerned with serving our -content. - - -## Contributing pages - -The Markdown files hosting our content can be found in the -[`resources/`](./resources) directory. The process of contributing to pages is -covered extensively in our online guide which you can find -[here](https://www.pythondiscord.com/pages/guides/pydis-guides/how-to-contribute-a-page/). -Alternatively, read it directly at -[`resources/guides/pydis-guides/how-to-contribute-a-page.md`](./resources/guides/pydis-guides/how-to-contribute-a-page.md). - - -## Directory structure - -Let's look at the structure in here: - -- `resources` contains the static Markdown files that make up our site's - [pages](https://www.pythondiscord.com/pages/) - -- `tests` contains unit tests for verifying that the app works properly. - -- `views` contains Django views which generate and serve the pages from the - input Markdown. - -As for the modules, apart from the standard Django modules in here, the -`utils.py` module contains utility functions for discovering Markdown files to -serve. diff --git a/pydis_site/apps/content/__init__.py b/pydis_site/apps/content/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pydis_site/apps/content/__init__.py +++ /dev/null diff --git a/pydis_site/apps/content/apps.py b/pydis_site/apps/content/apps.py deleted file mode 100644 index 96019e1c..00000000 --- a/pydis_site/apps/content/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig - - -class ContentConfig(AppConfig): - """Django AppConfig for content app.""" - - name = 'pydis_site.apps.content' diff --git a/pydis_site/apps/content/migrations/0001_add_tags.py b/pydis_site/apps/content/migrations/0001_add_tags.py deleted file mode 100644 index 2c31e4c1..00000000 --- a/pydis_site/apps/content/migrations/0001_add_tags.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 4.0.6 on 2022-08-23 09:06 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='Commit', - fields=[ - ('sha', models.CharField(help_text='The SHA hash of this commit.', max_length=40, primary_key=True, serialize=False)), - ('message', models.TextField(help_text='The commit message.')), - ('date', models.DateTimeField(help_text='The date and time the commit was created.')), - ('authors', models.TextField(help_text='The person(s) who created the commit. This is a serialized JSON object. Refer to the GitHub documentation on the commit endpoint (schema/commit.author & schema/commit.committer) for more info. https://docs.github.com/en/rest/commits/commits#get-a-commit')), - ], - ), - migrations.CreateModel( - name='Tag', - fields=[ - ('last_updated', models.DateTimeField(auto_now=True, help_text='The date and time this data was last fetched.')), - ('sha', models.CharField(help_text="The tag's hash, as calculated by GitHub.", max_length=40)), - ('name', models.CharField(help_text="The tag's name.", max_length=50, primary_key=True, serialize=False)), - ('group', models.CharField(help_text='The group the tag belongs to.', max_length=50, null=True)), - ('body', models.TextField(help_text='The content of the tag.')), - ('last_commit', models.ForeignKey(help_text='The commit this file was last touched in.', null=True, on_delete=django.db.models.deletion.CASCADE, to='content.commit')), - ], - ), - ] diff --git a/pydis_site/apps/content/migrations/__init__.py b/pydis_site/apps/content/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pydis_site/apps/content/migrations/__init__.py +++ /dev/null diff --git a/pydis_site/apps/content/models/__init__.py b/pydis_site/apps/content/models/__init__.py deleted file mode 100644 index 60007e27..00000000 --- a/pydis_site/apps/content/models/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .tag import Commit, Tag - -__all__ = ["Commit", "Tag"] diff --git a/pydis_site/apps/content/models/tag.py b/pydis_site/apps/content/models/tag.py deleted file mode 100644 index 1a20d775..00000000 --- a/pydis_site/apps/content/models/tag.py +++ /dev/null @@ -1,80 +0,0 @@ -import collections.abc -import json - -from django.db import models - - -class Commit(models.Model): - """A git commit from the Python Discord Bot project.""" - - URL_BASE = "https://github.com/python-discord/bot/commit/" - - sha = models.CharField( - help_text="The SHA hash of this commit.", - primary_key=True, - max_length=40, - ) - message = models.TextField(help_text="The commit message.") - date = models.DateTimeField(help_text="The date and time the commit was created.") - authors = models.TextField(help_text=( - "The person(s) who created the commit. This is a serialized JSON object. " - "Refer to the GitHub documentation on the commit endpoint " - "(schema/commit.author & schema/commit.committer) for more info. " - "https://docs.github.com/en/rest/commits/commits#get-a-commit" - )) - - @property - def url(self) -> str: - """The URL to the commit on GitHub.""" - return self.URL_BASE + self.sha - - def lines(self) -> collections.abc.Iterable[str]: - """Return each line in the commit message.""" - for line in self.message.split("\n"): - yield line - - def format_authors(self) -> collections.abc.Iterable[str]: - """Return a nice representation of the author(s)' name and email.""" - for author in json.loads(self.authors): - yield f"{author['name']} <{author['email']}>" - - -class Tag(models.Model): - """A tag from the python-discord bot repository.""" - - URL_BASE = "https://github.com/python-discord/bot/tree/main/bot/resources/tags" - - last_updated = models.DateTimeField( - help_text="The date and time this data was last fetched.", - auto_now=True, - ) - sha = models.CharField( - help_text="The tag's hash, as calculated by GitHub.", - max_length=40, - ) - last_commit = models.ForeignKey( - Commit, - help_text="The commit this file was last touched in.", - null=True, - on_delete=models.CASCADE, - ) - name = models.CharField( - help_text="The tag's name.", - primary_key=True, - max_length=50, - ) - group = models.CharField( - help_text="The group the tag belongs to.", - null=True, - max_length=50, - ) - body = models.TextField(help_text="The content of the tag.") - - @property - def url(self) -> str: - """Get the URL of the tag on GitHub.""" - url = Tag.URL_BASE - if self.group: - url += f"/{self.group}" - url += f"/{self.name}.md" - return url diff --git a/pydis_site/apps/content/resources/_info.yml b/pydis_site/apps/content/resources/_info.yml deleted file mode 100644 index 6553dcc6..00000000 --- a/pydis_site/apps/content/resources/_info.yml +++ /dev/null @@ -1,2 +0,0 @@ -title: Pages -description: Guides, articles, and pages hosted on the site. diff --git a/pydis_site/apps/content/resources/code-of-conduct.md b/pydis_site/apps/content/resources/code-of-conduct.md deleted file mode 100644 index 56050230..00000000 --- a/pydis_site/apps/content/resources/code-of-conduct.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -title: Python Discord Code of Conduct -description: The Code of Conduct for our community. -icon: fab fa-discord ---- - -# Code of Conduct - -We are committed to providing a friendly, safe and welcoming environment for all, regardless of level of experience, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, nationality, or other similar characteristic. - -##### Examples of behavior that contributes to creating a positive environment include: - -* Being kind and courteous to others -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Collaborating with other community members -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -##### Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and sexual attention or advances -* The use of inappropriate images, including in a community member's avatar -* The use of inappropriate language, including in a community member's nickname -* Any spamming, flaming, baiting or other attention-stealing behavior -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic address, without explicit permission -* Discussing topics that are overly polarizing, sensitive, or incite arguments. This includes the discussion of polarizing political views, violence, suicide, and rape. -* Responding with “RTFM”, "just google it” or similar phrases in response to help requests -* Other conduct which could reasonably be considered inappropriate - -### Our Goal - -The goal of this document is to set the overall tone for our community. -This isn’t an exhaustive list of things you can and can't do. -Rather, take this document in the spirit in which it’s intended, and try to be your best self. - -We value many things beyond technical expertise, including collaboration and supporting others within our community. -Providing a positive experience for other community members can have a much more significant impact than simply providing the correct answer. - -### Scope - -This Code of Conduct applies to all spaces managed by Python Discord. -This includes, but is not limited to, the Discord server, our repositories on GitHub, the YouTube-channel, and meet-ups. -In addition, violations of this code outside these spaces may affect a person's ability to participate within them. - -The Python Code of Conduct applies equally to all members of the community, including staff. - ---- - -# Code of Conduct Policies - -### Moderation Policy - -These are the policies for upholding our community’s rules and the code of conduct. -If you want to report a situation that needs to be reviewed by our moderation team, please see our [reporting guide](#reporting-guide). - -1. The [Python Discord Code of Conduct](#code-of-conduct) and the [Community Rules](/pages/rules) are enforced by the moderation team, which consists of users with the Moderators, Admins or Owners role on the Python Discord server. -2. Behavior that moderators find inappropriate, whether listed in the code of conduct, the community rules, or not, is also not allowed. -3. Complaints about moderation in-channel are not allowed. If a moderator takes an action or makes a decision you do not agree with, please send a Direct Message (DM) to ModMail from our Discord server. -4. If you disagree with a moderation action or decision taken against you, you may appeal the action or decision by following the [Appeal Procedure](#appeal-procedure). - -### Reporting Guide - -Instances of behaviors that violate the Python Discord Code of Conduct or rules may be reported by any member of the community. -Community members are encouraged to report these situations, including situations they witness involving other community members. - -You may report in the following ways: - -* By tagging the `@Moderators` role on the Discord server in situations that require immediate moderator attention. -* By sending a direct message (DM) to ModMail from our Discord server. -* By sending an email to [[email protected]](mailto:[email protected]) - -### Appeal Procedure - -If you wish to appeal a decision or action taken by the moderation team, you can do so in one of the following ways: - -* By sending a direct message (DM) to ModMail from our Discord server. -* Joining our [ban appeals server](https://discord.gg/WXrCJxWBnm) and sending a direct message (DM) to the Ban Appeals bot. - -Please provide all relevant information in your appeal, including: - -* Your Discord username and id. -* The decision or action you are appealing. -* The reason for your appeal. - -Appeals will be discussed internally with the moderation team, but will be kept confidential otherwise. - ---- - -#### Attribution - -This Code of Conduct and parts of the policies are adapted from the [Adafruit Community Code of Conduct](https://github.com/adafruit/Adafruit_Community_Code_of_Conduct/blob/master/code-of-conduct.md), [Django Code of Conduct](https://www.djangoproject.com/conduct/), and the [Rust Code of Conduct](https://www.rust-lang.org/en-US/conduct.html). - -#### License - - -All content on this page is licensed under a [Creative Commons Attribution](https://creativecommons.org/licenses/by/3.0/) license. diff --git a/pydis_site/apps/content/resources/frequently-asked-questions.md b/pydis_site/apps/content/resources/frequently-asked-questions.md deleted file mode 100644 index 1c9c3f6d..00000000 --- a/pydis_site/apps/content/resources/frequently-asked-questions.md +++ /dev/null @@ -1,132 +0,0 @@ ---- -title: Frequently Asked Questions -description: The Python Discord FAQ. -icon: fab fa-discord -toc: 4 ---- - -Welcome to Python Discord! Sometimes in our channels we get similar questions asked every so often. -We've compiled the most frequently asked questions and provided our response to them. -This FAQ is aimed at answering questions about the Python Discord community. If you have question about Python, feel free to ask in `#python-general`. - -## Staff & Roles - -#### **Q: How do I get the helper role / become moderator / join staff?** - -There are no applications to become a Helper, Moderator, Admin, or other staff role. -To become a Helper, which is our base staff role, people are nominated by a staff member and are later put up to a vote by Moderators, Admins, and Owners. -If the candidate received enough votes unanimously, then we offer them the Helper role. -This whole process takes place in channels only viewable to staff members. - -Being a Helper is not only about helping people with Python questions or helping with our projects, but is also about demonstrating an understanding of our community's culture. -To read more about what we look for in a Helper and to read about our internal staff roles (Moderators, Leads, Core Developer, Admin), check out [this page](/pages/server-info/roles/). - - -#### **Q: What is this role on the server?** - -We document the purposes of our most important roles on our website. Check it out [here](/pages/server-info/roles/). - -The roles that are not documented on that page are for seasonal events. These are specific to those events and don't impact permissions on the server. - - -#### **Q: What perks are there for nitro boosters? Can I keep the nitro role once the boost expires?** - -People who boost our server automatically get a bright cyan role color, but this is purely cosmetic and there aren't any other incentives to give us Nitro boosts. -Discord itself manages that specific role, so the role is automatically removed when the boost expires. - -Patrons who donate via [our Patreon](https://www.patreon.com/python_discord) also get a blue role and this is also a purely cosmetic role. -We do appreciate our Patreon supporters and our Nitro Boosters! - -We have received suggestions to give Nitro boosters non-cosmetic perks like a Nitro boosters lounge or the ability to use emoji reactions in certain channels, though that isn't something we will consider as we don't want to gate any part of the server behind a paywall. - - -#### **Q: I'd like to report inappropriate behavior on the server. How do I do that?** - -To report inappropriate or rule-breaking behaviour on the server, please send a direct message to our `@ModMail`. -You should find the ModMail bot at the top of the server member list. -If it's an urgent situation that needs immediate moderator attention, such as spam or NSFW content, then you can ping the `@Moderators` role in the server. - -## Questions about our bots - -#### **Q: Can I get Python bot on my server?** - -There isn't a way to invite `@Python` to other servers. -`@Python` is closely tied to our server architecture and wouldn't be able to properly function without a specific set-up. -If you are interested in `@Python` though, you can host your own instance of it. -The entire project is open source and can be found on [our github](https://github.com/python-discord/bot). - -#### **Q: Who is the Dave bot?** - -Dave, in addition to [learning to code](https://www.youtube.com/watch?v=ZH26PuX3re0), is running an instance of [Metricity](https://github.com/python-discord/metricity), which collects advanced metrics about the usage of the server. -Message content is not stored or collected. -You can view what data we collect in our [data privacy policy](/pages/privacy/). - - -#### **Q: Do any of the bots do X? Can I contribute to the bot?** - -We have two bots that provide functionality in our server: `@Python` and `@Sir Lancebot`. - -* `@Python` is the bot that helps manage certain server functionality (i.e. our help channel and moderation systems). -* `@Sir Lancebot` is our community bot that is designed as an entry level project for people to learn about open source contribution. -* `@Sir Lancebot` contains our fun and silly commands, like `.battleship`, `.BunnyNameGenerator`, `.http_status` which provides dog and cat HTTP status codes, and more! - -You can check out [`@Python` here on github](https://github.com/python-discord/bot), and check out [`@Sir Lancebot` here](https://github.com/python-discord/sir-lancebot). -If you have any questions about how to contribute, drop by the `#dev-contrib` channel in server. - -## Server Specific Questions - -#### **Q: Why are the help channels named after elements/food?** - -We want to keep the help channels uniquely named with it being somewhat easy to remember, so we decided on elements/food. `#help-1` doesn't work as well as `help-carbon` (`help-strawberry`). -If we had them numbered, they would quickly move out of order and possibly cause confusion for newer members. - - -#### **Q: Why can't I upload a specific file type?** - -The only file types that we allow on this server are those that Discord supports a native preview for. -This is because it's easier and safer for people on the server since they do not need to download a file to view it. -It's also to ease the burden on our moderators, otherwise they would have to download and check the files posted to the server. - -Even though Discord does support previewing of files like `.txt` and `.py`, that support is only available on Desktop, not mobile. Additionally, we prefer people to use hastebin as it encourages them to only copy over the relevant code snippets instead of their whole code; this makes helping much easier for all involved. - -If you want to share code please use our hosted hastebin, [paste.pythondiscord.com](https://paste.pythondiscord.com). - - -#### **Q: Why is this permission not allowed in that channel?** - -Our general policy is to allow permissions unless proven that they are negatively affecting the channel. -If a certain channel doesn't have a permission, chances are it was allowed there at some point, but the cost of moderating or managing it outweighed the benefit. -Feel free to ask in `#community-meta` if you'd like the reasoning behind any specific decision. - - -#### **Q: Can we have a channel to show off projects or a channel to find people to work on projects with?** - -We previously had these channels, though they unfortunately did not work out the way we had hoped. -Engagement was low and they were a large burden on our moderators due to the number of low quality or rule violating posts. - -In general, a real-time chat client isn't the best avenue for showing off your projects or finding collaborators because messages are typically only seen by those actively engaged at the time they are posted. -You're welcome to showcase your projects in our off-topic channels or on a different platform like Reddit. - - -#### **Q: Can I make a recommendation about a specific feature in the server?** - -If you want to make a recommendation or suggestion about the server feel free to post in `#community-meta`. -You can also open an issue on our meta repo on GitHub, which can be found [here](https://github.com/python-discord/meta). - - -#### **Q: Why did the icon change?** - -While we love our blurple Python logo, we also enjoy celebrating other events throughout the year, like Advent of Code, Pride Month, Black History Month, Valentine's Day, Diwali, and more! In the spirit of those celebrations, we like to have some fun and change our icon instead. -If you're wondering why it's changed this time, check out `#changelog` on the server, as the reasoning for the recent change will be there. - -If you'd like to contribute and create a Python Discord server icon for us to use, check out [our branding repo](https://github.com/python-discord/branding) for what we currently have and talk to us in the `#dev-branding` channel in the server. - -## Misc - -#### **Q: Can I interact with the data collected on the server?** - -Unfortunately we don't allow direct interaction with the metrics we collect on the server. -The data we do collect is used for moderation purposes, please see [our Privacy Policy](/pages/privacy/) on what data is collected and how we use it. -Legend has it that if you say "SQL" or "graphs" enough times, a `@joe` might appear and provide some graphs and run queries you might have in mind. - -We do have some public stats available to view here: [https://stats.pythondiscord.com/](https://stats.pythondiscord.com/) diff --git a/pydis_site/apps/content/resources/guides/_info.yml b/pydis_site/apps/content/resources/guides/_info.yml deleted file mode 100644 index 2f65eaf9..00000000 --- a/pydis_site/apps/content/resources/guides/_info.yml +++ /dev/null @@ -1,2 +0,0 @@ -title: Guides -description: Made by us, for you. diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/_info.yml b/pydis_site/apps/content/resources/guides/pydis-guides/_info.yml deleted file mode 100644 index c126a68a..00000000 --- a/pydis_site/apps/content/resources/guides/pydis-guides/_info.yml +++ /dev/null @@ -1,2 +0,0 @@ -title: Python Discord Guides -description: Guides related to the Python Discord server and community. diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/asking-good-questions.md b/pydis_site/apps/content/resources/guides/pydis-guides/asking-good-questions.md deleted file mode 100644 index b08ba7c6..00000000 --- a/pydis_site/apps/content/resources/guides/pydis-guides/asking-good-questions.md +++ /dev/null @@ -1,180 +0,0 @@ ---- -title: Asking Good Questions -description: A guide for how to ask good questions in our community. -icon: fab fa-discord -toc: 3 ---- - -This document is intended to provide you with the information you need to get help as quickly and effectively as possible. -If you're stuck on a problem or you just don't understand something, you should always feel welcome to ask. - -# Before You Ask - -Before you ask your question, there are a few things you can do to find an answer on your own. -Experienced developers will do the following: - -* Read the official documentation for whatever you're working with -* Use a debugger to inspect your code -* Examine the traceback when your code raises an exception -* Do some research online - for example, on Stack Overflow -* Read the source code for whatever you're working with - -Essentially, doing your research is the first step towards a solution to any problem. -If your problem isn't extremely general, we're going to be doing exactly these steps ourselves when helping you, so doing the legwork beforehand saves everyone a lot of time. - -If none of the above steps help you or you're not sure how to do some of the above steps, feel free to ask us for help. - -# A Good Question - -When you're ready to ask a question, there are a few things you should have to hand before forming a query. - -* A code example that illustrates your problem -* If possible, make this a minimal example rather than an entire application -* Details on how you attempted to solve the problem on your own -* Full version information - for example, "Python 3.6.4 with `discord.py 1.0.0a`" -* The full traceback if your code raises an exception -* Do not curate the traceback as you may inadvertently exclude information crucial to solving your issue - -Your question should be informative, but to the point. -More importantly, how you phrase your question and how you address those that may help you is crucial. -Courtesy never hurts, and please type using correctly-spelled and grammatical language as far as you possibly can. - -When you're inspecting a problem, don't be quick to assume that you've found a bug, or that your approach is correct. -While it helps to detail what exactly you're trying to do, you should also be able to give us the bigger picture - describe the goal, not just the step. -Describe the problem's symptoms in chronological order - not your guesses as to their cause. - -| Bad Questions | Good Questions | -| ------------- | -------------- | -| Where can I find information on discord.py? | I used Google to try to find more information about "discord.py 1.0.0a", but I couldn't really find anything useful. Does anyone know where I might find a guide to writing commands using this library? | -| Pillow puts my text at the bottom of the image instead of where I wanted it. Why is it broken? | Pillow appears to insert text at the bottom of the image if the given X coordinate is negative. I had a look at the documentation and searched Stack Overflow, but I couldn't find any information on using negative coordinates to position text. Has anyone attempted this? | -| I'm having some trouble writing a YouTube random URL generator - can anyone help? | My YouTube random URL generator appears to be returning false positives for tested URLs, stating that a URL points to a real video when that video doesn't actually exist. Obviously there's some issue with how this is checked, but I can't put my finger on it. Is there anything I can check? | -| I was given this assignment by my teacher, but I'm not sure how to approach it. Does anyone have any ideas? | I have a list of numbers - how do I calculate how many of them are even? Is there a way to remove all the odd numbers from my list? Are there quick ways to find the average of a list of numbers, or add them all together? | - - -# What Not To Ask ---- - -#### Q: Can I ask a question? - -Yes. Always yes. Just ask. - -#### Q: Is anyone here good at Flask / Pygame / PyCharm? - -There are two problems with this question: - -1. This kind of question does not manage to pique anyone's interest, so you're less likely to get an answer overall. - On the other hand, a question like `"Is it possible to get PyCharm to automatically compile SCSS into CSS files"` is much more likely to be interesting to someone. - Sometimes, the best answers come from someone who does not already know the answer, but who finds the question interesting enough to go search for the answer on your behalf. -2. When you qualify your question by first asking if someone is good at something, you are filtering out potential answerers. - [Not only are people bad at judging their own skill at something](https://en.wikipedia.org/wiki/Dunning%E2%80%93Kruger_effect), but the truth is that even someone who has zero experience with the framework you're having trouble with might still be of excellent help to you. - -So instead of asking if someone is good at something, simply ask your question right away. - -#### Q: Can I use `str()` on a `discord.py` Channel object? - -Try it yourself and see. Experimentation is a great way to learn, and you'll save a lot of time by just trying things out. Don't be afraid of your computer! - -#### Q: My code doesn't work - -This isn't a question, and it provides absolutely no context or information. -Depending on the mood of the people that are around, you may even find yourself ignored. -Don't be offended by this - just try again with a better question. - -#### Q: Can anyone help me break into someone's Facebook account / write a virus / download videos from YouTube? - -We will absolutely not help you with hacking, pirating, or any other illegal activity. -A question like this is likely to be followed up with a ban if the person asking it doesn't back down quickly. - -#### Q: Can I send you a private message? - -Sure, but keep in mind that our staff members will not provide help via DMs. -We prefer that questions are answered in a public channel so lurkers can learn from them. - -#### Q: Can you help me over Teamviewer? - -No, sorry. - - -# Examining Tracebacks - -Usually, the first sign of trouble is that when you run your code, it raises an exception. -For beginning programmers, the traceback that's generated for the exception may feel overwhelming and discouraging at first. -However, in time, most developers start to appreciate the extensive information contained in the traceback as it helps them track down the error in their code. -So, don't panic and take a moment to carefully review the information provided to you. - -### Reading the Traceback - -```py -Traceback (most recent call last): - File "my_python_file.py", line 6, in <module> - spam = division(a=10, b=0) - File "my_python_file.py", line 2, in division - answer = a / b -ZeroDivisionError: division by zero -``` - -In general, the best strategy is to read the traceback from bottom to top. -As you can see in the example above, the last line of the traceback contains the actual exception that was raised by your code. -In this case, `ZeroDivisionError: division by zero`, clearly indicates the problem: We're trying to divide by zero somewhere in our code and that obviously can't be right. -However, while we now know which exception was raised, we still need to trace the exception back to the error in our code. - -To do so, we turn to the lines above the exception. -Reading from bottom to top again, we first encounter the line where the exception was raised: `answer = a / b`. -Directly above it, we can see that this line of code was `line 2` of the file `my_python_file.py` and that it's in the scope of the function `division`. -At this point, it's a good idea to inspect the code referenced here to see if we can spot an obvious mistake: - -```py -# Python Code -1| def division(a, b): -2| answer = a / b -3| return answer -``` - -Unfortunately, there's no obvious mistake in the code at this point, although one thing we do see here is that this function divides `a` by `b` and that the exception will only occur if `b` is somehow assigned the numeric value `0`. - -Keeping that observation in the back of our minds, we continue reading the traceback from bottom to top. The next thing we encounter is `spam = division(a=10, b=0)` from `line 6` of the file `my_python_file.py`. -In this case, `<module>` tells us that the code is in the global scope of that file. -While it's already clear from the traceback what's going wrong here, we're passing `b=0` to the function `division`, inspecting the code shows us the same: - -```python -5| spam = division(a=10, b=0) -6| print(spam) -``` - -We have now traced back the exception to a line of code calling the division function with a divisor of `0`. -Obviously, this is a simplified example, but the exact same steps apply to more complex situations as well. - -### The Error is Sometimes in the Line Before the Line in the Traceback - -Sometimes, the actual error is in the line just before the one referenced in the traceback. -This usually happens when we've inadvertently omitted a character meant to close an expression, like a brace, bracket, or parenthesis. -For instance, the following snippet of code will generate a traceback pointing at the line after the one in which we've missed the closing parenthesis: - -```python -# Python Code -1| print("Hello, world!" -2| print("This is my first Python program!") - -# Terminal output -Traceback (most recent call last): - File "my_python_file.py", line 2 - print("This is my first Python program!") - ^ -SyntaxError: invalid syntax -``` - -The reason this may happen is that Python allows for [implicit line continuation](https://docs.python.org/3/reference/lexical_analysis.html#implicit-line-joining) and will only notice the error when the expression does not continue as expected on the next line. -So, it's always a good idea to also check the line before the one mentioned in the traceback! - -### More Information on Exceptions - -Further information on exceptions can be found in the official Python documentation: - -* [The built-in exceptions page](https://docs.python.org/3/library/exceptions.html) lists all the built-in exceptions along with a short description of the exception. - If you're unsure of the meaning of an exception in your traceback, this is a good place to start. -* [The errors and exceptions chapter in the official tutorial ](https://docs.python.org/3/tutorial/errors.html) gives an overview of errors and exceptions in Python. - Besides explaining what exceptions are, it also explains how to handle expected exceptions graciously to keep your application from crashing when an expected exception is raised and how to define custom exceptions specific to your application. - -If you encounter an exception specific to an external module or package, it's usually a good idea to check the documentation of that package to see if the exception is documented. -Another option is to paste a part of the traceback, usually the last line, into your favorite search engine to see if anyone else has encountered a similar problem. -More often than not, you will be able to find a solution to your problem this way. diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/code-reviews-primer.md b/pydis_site/apps/content/resources/guides/pydis-guides/code-reviews-primer.md deleted file mode 100644 index cde7d63e..00000000 --- a/pydis_site/apps/content/resources/guides/pydis-guides/code-reviews-primer.md +++ /dev/null @@ -1,165 +0,0 @@ ---- -title: "Code Reviews: A Primer" -description: An introduction and guide to the world of code reviews! -icon: fab fa-github -toc: 3 ---- - -## The What, the Why, and the How - -##### The What & The Why - -This is a guide that will cover the unsung hero of the coding process: code reviews. -Whether you're working professionally or on an open-source project on GitHub, you'll find that code reviews are an essential part of the process. - -So what exactly is a code review? The closest thing to compare it to is proofreading. -In proofreading, you look at an essay someone wrote and give suggestions on how things could be written better or point out mistakes that the writer may have missed. -Code reviewing is the same way. -You're given some code that someone wrote and see if you can find ways to improve it or point mistakes that the coder may have missed. - -"Hemlock", you might say, "Why should I care? Why would this be important?" The thing to remember is that coding is a team effort. -Code reviews help the programs we write to be the very best they can be. -We're all human, and mistakes are made, but through working together, we can correct those mistakes (as best we can) before they get merged into production. -Even if you're not the best coder in the world, you may spot things that even a professional programmer might miss! - -##### The How - -And now for the most important part of the guide. -This will detail the general process of how to write a code review, what to look for, how to do basic testing, and how to submit your review. -Big thanks to Akarys, who wrote this section of the guide! - ---- - -## Our Code Review Process - -> Note: Everything described in this guide is the writer's way of doing code reviews. -> This is purely informative; there's nothing wrong with branching off of this guide and creating your own method. -> We would even encourage you to do so! - -We usually do reviews in 3 steps: - -1. Analyze the issue(s) and the pull request (PR) -2. Analyze and review the code -3. Test the functionality - -So let's jump right into it! - -## Step 1: Analyzing the Issue(s) and the Pull Request - -Every issue and pull request has a reason for being. -While it's possible to do a code review without knowing that reason, it will make your life easier to get the full context of why it exists and what it's trying to do. -Let's take a look at how we can get that context. - -### Analyzing the Issue(s) - -In almost every case, the PR will have some linked issues. -On the right-hand side of the PR's page in GitHub, you'll see a section labeled "Linked issues". -Reading the issue(s) and their comments will allow you to know why certain decisions were made. -This will be invaluable when you review the implementation. -The author of the issue may suggest the code be written one way but the PR ends up writing it another. -This happens during the development process: New ideas are thought of, optimizations are found, other ideas are removed, etc. -Even if there are changes, functionality or fix that the issue covers should be covered by the PR. - -### Analyzing the PR - -The author of the PR will (or should) have made comments detailing how they implemented the code and whether it differs from the proposed implementation in the issue. -If they do things differently than suggested, they should also have explained why those changes were made. - -Remember that just like in the issue, PR comments have value. -Some of your future concerns about the implementation or even review comments may have already been discussed. -Although, if you disagree with the decision, feel free to comment about it! The more input we get from our reviewers, the better! -The last thing you need to remember is that commit messages are important. They're notes from the PR's author that give details as to what they changed. This can help you keep track of what change happened at what point in development, which can help you get some context about a given change. - -Now that you know more about the changes, let's start the fun stuff: the code review! - -## Step 2: Reviewing the Code Itself - -After all, that's why we're here! Let's dive right in! - -### The 1st Read: Getting a Sense of the Code - -It's impossible to understand the code in the PR your first time through. -Don't be surprised if you have to read over it a few times in order to get the whole picture. -When reading the code for the first time, we would recommend you to only try to get a sense of how the code flow. -What does this mean, you may ask? -Well, it is about knowing how every individual piece fits together to achieve the desired goal. - -Pay close attention to how the functions and classes are called. -Make sure to read the comments and docstrings; they'll help explain sections of the code that may be confusing. -Finally, remember that your goal at the moment is to get a general idea of how the code achieves its goal. -You don't have to understand the finer points at this stage. - -### The 2nd Read: Looking at Every Little Detail - -Now that you know how the code flows, you can take a closer look at the code. -Comment on anything you think could be done better. -This includes, but is not limited to: - -* The general structure of the code -* The variable names -* The algorithm(s) used -* The use or the lack of use of already existing code or a library -* Blocks of code that could benefit from comments -* Spelling -* Anything you see that doesn't seem quite right is worth commenting on. Discussing the things you find benefits everyone involved! - -Another good technique is to imagine how you would have implemented a specific functionality and compare it with the proposed implementation. -GitHub has a feature allowing you to mark files as read, and it's recommended to take advantage of it so that you don't lose your place if you take a break. -Now that you know what to comment on, let's take a closer look at how to comment. - -### Leaving Good Review Comments - -When leaving a comment, don't forget that we can't know what you're thinking; you have to write it down. -Your comment should describe why you think this should be changed and how you propose to change it. -Note that you can omit the latter in some cases if it is outside of your area of expertise for instance. -There's nothing wrong with using your comments to ask questions! If there's something you're not sure about, don't hesitate to ask in your comment. -It might indicate that the PR's author needs to add comments, change variable or function names, or even change a block of code entirely. - -GitHub has a handy feature that allows you to leave suggestions. -This means that the author can drop your suggestion into their PR with a click of a button. -If you provide one, great! It will speed up the review process even more! -On the opposite side, note that you aren't required to do all of that when leaving a comment. If you are fixing a typo, leaving a suggestion is enough. - -If you have concerns about a particular piece of code for example a race condition, it is totally okay to point it out in a comment, even if you don't have a suggested way to fix it. - -## Testing the Functionalities - -A code review isn't only about looking at the code; it's also about testing it. -Always try to make sure that the code is working properly before approving the changes. - -Something else to note: you are free to review code without testing functionality. -That's totally okay! Just make sure to mention when you submit your review. - -### Reviewing the Functionality - -When reviewing functionality, keep asking yourself how you would have designed it and then compare your idea with the implementation. -Note that you are allowed to give suggestions about how the functionality should be designed. - -Start with the basic usages and work your way up to more complicated ones. -Verify that everything is working as intended. -If it isn't, leave a comment giving what you've done, what should have happened, and what actually happened, along with any potential logs or errors you could have. -If you like, you can even try to pinpoint the issue and find a fix for it. We would be very grateful if you did! - -### Objective: Break it - -Good functionality should be able to handle edge cases. -Try to throw every edge case you might think of and see how it responds. This might not be needed in some cases, but it's essential for security-related work. -If the implementation has a security breach, you should absolutely find it! Just like in the previous section, if you find something and comment on it, great! -If you manage to find a way to fix it and suggest the fix, even better! - -### But what if the Project doesn't even start? - -This is a tricky one. Sometimes the project won't even start, keeping you from testing it all together. - -In this case, you should try to investigate if it is failing because of an error in the code. If it ends up being because of the functionality, you should comment on that. - -If you aren't sure why it isn't starting, feel free to ask us! - -## Final Words - -You did it! -You are now ready to take on the wild world of code reviewing! -We know that process can seem long, tedious, and not feel like a necessary task, but it is! -We are very grateful to you for reading through this and for your potential future code reviews. -We couldn't move forward without you. -Thank you! diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing.md deleted file mode 100644 index 07e9a7bd..00000000 --- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing.md +++ /dev/null @@ -1,142 +0,0 @@ ---- -title: Contributing -description: A guide to contributing to our open source projects. -icon: fab fa-github ---- - -Our projects on Python Discord are open source and [available on GitHub](https://github.com/python-discord). If you would like to contribute, consider one of the following projects: - -<!-- Project cards --> -<div class="columns is-multiline is-centered is-3 is-variable"> - <div class="column is-one-third-desktop is-half-tablet"> - <div class="card github-card"> - <div class="card-header"> - <div class="card-header-title is-centered"> - <a class="is-size-5" href="https://github.com/python-discord/sir-lancebot"> - <i class="fab fa-github"></i> <strong >Sir Lancebot</strong> - </a> - </div> - </div> - <div class="card-content"> - <div class="content"> - Sir Lancebot has a collection of self-contained, for-fun features. If you're new to Discord bots or contributing, this is a great place to start! - </div> - </div> - <div class="card-footer"> - <a href="https://github.com/python-discord/sir-lancebot/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc" class="card-footer-item"><i class="fas fa-exclamation-circle"></i> Issues</a> - <a href="https://github.com/python-discord/sir-lancebot/pulls?q=is%3Apr+is%3Aopen+sort%3Aupdated-desc" class="card-footer-item"><i class="fas fa-code-merge"></i> PRs</a> - </div> - <div class="card-footer"> - <a href="/pages/guides/pydis-guides/contributing/sir-lancebot" class="card-footer-item"><i class="fas fa-cogs"></i> Setup and Configuration Guide</a> - </div> - </div> - </div> - <div class="column is-one-third-desktop is-half-tablet"> - <div class="card github-card"> - <div class="card-header"> - <div class="card-header-title is-centered"> - <a href="https://github.com/python-discord/bot"> - <strong class="is-size-5"><i class="fab fa-github"></i> Bot</strong> - </a> - </div> - </div> - <div class="card-content"> - <div class="content"> - Called @Python on the server, this bot handles moderation tools, help channels, and other critical features for our community. - </div> - </div> - <div class="card-footer"> - <a href="https://github.com/python-discord/bot/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc" class="card-footer-item"><i class="fas fa-exclamation-circle"></i> Issues</a> - <a href="https://github.com/python-discord/bot/pulls?q=is%3Apr+is%3Aopen+sort%3Aupdated-desc" class="card-footer-item"><i class="fas fa-code-merge"></i> PRs</a> - </div> - <div class="card-footer"> - <a href="/pages/guides/pydis-guides/contributing/bot" class="card-footer-item"><i class="fas fa-cogs"></i> Setup and Configuration Guide</a> - </div> - </div> - </div> - <div class="column is-one-third-desktop is-half-tablet"> - <div class="card github-card"> - <div class="card-header"> - <div class="card-header-title is-centered"> - <a href="https://github.com/python-discord/site"> - <strong class="is-size-5"><i class="fab fa-github"></i> Site</strong> - </a> - </div> - </div> - <div class="card-content"> - <div class="content"> - This website itself! This project is built with Django and includes our API, which is used by various services such as @Python. - </div> - </div> - <div class="card-footer"> - <a href="https://github.com/python-discord/site/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc" class="card-footer-item"><i class="fas fa-exclamation-circle"></i> Issues</a> - <a href="https://github.com/python-discord/site/pulls?q=is%3Apr+is%3Aopen+sort%3Aupdated-desc" class="card-footer-item"><i class="fas fa-code-merge"></i> PRs</a> - </div> - <div class="card-footer"> - <a href="/pages/guides/pydis-guides/contributing/site" class="card-footer-item"><i class="fas fa-cogs"></i> Setup and Configuration Guide</a> - </div> - </div> - </div> -</div> - -# How do I start contributing? -Unsure of what contributing to open source projects involves? Have questions about how to use GitHub? Just need to know about our contribution etiquette? Completing these steps will have you ready to make your first contribution no matter your starting point. - -Feel free to skip any steps you're already familiar with, but please make sure not to miss the [Contributing Guidelines](#5-read-our-contributing-guidelines). - -If you are here looking for the answer to a specific question, check out the sub-articles in the top right of the page to see a list of our guides. - -**Note:** We use Git to keep track of changes to the files in our projects. Git allows you to make changes to your local code and then distribute those changes to the other people working on the project. You'll use Git in a couple steps of the contributing process. You can refer to this [**guide on using Git**](./working-with-git/). -{: .notification } - -### 1. Fork and clone the repo -GitHub is a website based on Git that stores project files in the cloud. We use GitHub as a central place for sending changes, reviewing others' changes, and communicating with each other. You'll need to create a copy under your own GitHub account, a.k.a. "fork" it. You'll make your changes to this copy, which can then later be merged into the Python Discord repository. - -*Note: Members of the Python Discord staff can create feature branches directly on the repo without forking it.* - -Check out our [**guide on forking a GitHub repo**](./forking-repository/). - -Now that you have your own fork you need to be able to make changes to the code. You can clone the repo to your local machine, commit changes to it there, then push those changes to GitHub. - -Check out our [**guide on cloning a GitHub repo**](./cloning-repository/). - -### 2. Set up the project -You have the source code on your local computer, now how do you actually run it? We have detailed guides on setting up the environment for each of our main projects: - -* [**Sir Lancebot**](./sir-lancebot/) - -* [**Python Bot**](./bot/) - -* [**Site**](./site/) - -### 3. Read our Contributing Guidelines -We have a few short rules that all contributors must follow. Make sure you read and follow them while working on our projects. - -[**Read our Contributing Guidelines here.**](./contributing-guidelines/) - -As mentioned in the Contributing Guidelines, we have a simple style guide for our projects based on PEP 8. Give it a read to keep your code consistent with the rest of the codebase. - -[**Read our Style Guide here.**](./style-guide/) - -### 4. Create an issue -The first step to any new contribution is an issue describing a problem with the current codebase or proposing a new feature. All the open issues are viewable on the GitHub repositories, for instance here is the [issues page for Sir Lancebot](https://github.com/python-discord/sir-lancebot/issues). If you have something that you want to implement open a new issue to present your idea. Otherwise, you can browse the unassigned issues and ask to be assigned to one that you're interested in, either in the comments on the issue or in the [`#dev-contrib`](https://discord.gg/2h3qBv8Xaa) channel on Discord. - -[**Find out how to write a good issue here.**](./issues/) - -Don't move forward until your issue is approved by a Core Developer. Issues are not guaranteed to be approved so your work may be wasted. -{: .notification .is-warning } - -### 5. Make changes -Now it is time to make the changes to fulfill your approved issue. You should create a new Git branch for your feature; that way you can keep your main branch up to date with ours and even work on multiple features at once in separate branches. - -This is a good time to review [how to write good commit messages](./contributing-guidelines/commit-messages) if you haven't already. - -### 6. Open a pull request -After your issue has been approved and you've written your code and tested it, it's time to open a pull request. Pull requests are a feature in GitHub; you can think of them as asking the project maintainers to accept your changes. This gives other contributors a chance to review your code and make any needed changes before it's merged into the main branch of the project. - -Check out our [**Pull Request Guide**](./pull-requests/) for help with opening a pull request and going through the review process. - -Check out our [**Code Review Guide**](../code-reviews-primer/) to learn how to be a star reviewer. Reviewing PRs is a vital part of open source development, and we always need more reviewers! - -### That's it! -Thank you for contributing to our community projects. If there's anything you don't understand or you just want to discuss with other contributors, come visit the [`#dev-contrib`](https://discord.gg/2h3qBv8Xaa) channel to ask questions. Keep an eye out for staff members with the **@PyDis Core Developers** role in the server; we're always happy to help! diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/_info.yml b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/_info.yml deleted file mode 100644 index 4a338463..00000000 --- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/_info.yml +++ /dev/null @@ -1,3 +0,0 @@ -title: Contributing -description: How to contribute to our open source projects on Github. -icon: fab fa-github diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/bot.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/bot.md deleted file mode 100644 index 02316bca..00000000 --- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/bot.md +++ /dev/null @@ -1,652 +0,0 @@ ---- -title: Contributing to Bot -description: A guide to setting up and configuring Bot. -icon: fab fa-github -toc: 3 ---- -The purpose of this guide is to get you a running local version of [the Python bot](https://github.com/python-discord/bot). -You should have already forked the repository and cloned it to your local machine. If not, check out our [detailed walkthrough](../#1-fork-and-clone-the-repo). - -This page will focus on the quickest steps one can take, with mentions of alternatives afterwards. - ---- - -### Set Up a Test Server -The Python bot is tightly coupled with the Python Discord server, so to have a functional version of the bot you need a server with channels it can use. -It's possible to set the bot to use a single channel for all cogs, but that will cause extreme spam and will be difficult to work with. - -You can start your own server and set up channels as you see fit, but for your convenience we have a template for a development server you can use: [https://discord.new/zmHtscpYN9E3](https://discord.new/zmHtscpYN9E3). -Keep in mind that this is not a mirror of the Python server, but a reduced version for testing purposes. A lot of the channels in the Python server were merged. - ---- - -### Set Up a Bot Account -You will need your own bot account on Discord to test your changes to the bot. -See [here](../creating-bot-account) for help with setting up a bot account. Once you have a bot account, invite it to the test server you created in the previous section. - -#### Privileged Intents - -It is necessary to explicitly request that your Discord bot receives certain gateway events. -The Python bot requires the `Server Member Intent` to function. -In order to enable it, visit the [Developer Portal](https://discord.com/developers/applications/) (from where you copied your bot's login token) and scroll down to the `Privileged Gateway Intents` section. -The `Presence Intent` is not necessary and can be left disabled. - -If your bot fails to start with a `PrivilegedIntentsRequired` exception, this indicates that the required intent was not enabled. - ---- - -### Configure the Bot -You now have both the bot's code and a server to run it on. It's time you to connect the two by changing the bot's configurations. - -#### config.yml -Entering the directory of the cloned code, you will find a file named `config-default.yml`. -This file contains the various configurations we use to make the bot run on the Python Discord server, such as channel and role IDs, and the emojis it works with. -It also contains configurations such as how long it takes for a help channel to time out, and how many messages a user needs to voice-verify. - -To run the bot in your test server, you will need to override some of those configurations. -Create and open a new file in the directory called `config.yml`. Alternatively, copy the `config-default.yml` file and rename the copy to `config.yml`. -The bot will first look at the items in `config.yml`, and will fall back to `config-default.yml` only if necessary. Note that you don't have to specify all items in `config.yml`, just the ones you want to override such as channel IDs. - -See [here](../obtaining-discord-ids) for help with obtaining Discord IDs. - -<div class="card"> - <button type="button" class="card-header collapsible"> - <span class="card-header-title subtitle is-6 my-2 ml-2">Optional config.yml</span> - <span class="card-header-icon"> - <i class="fas fa-fw fa-angle-down title is-5" aria-hidden="true"></i> - </span> - </button> - <div class="collapsible-content collapsed"> - <div class="card-content"> - <p>If you used the provided server template, and you're not sure which channels belong where in the config file, you can use the config below. Pay attention to the comments with several <code>#</code> symbols, and replace the <code>�</code> characters with the right IDs.</p> - <pre> - <code class="language-yaml"> -bot: - prefix: "!" - - redis: - host: "redis" - password: null - port: 6379 - use_fakeredis: true - - stats: - presence_update_timeout: 300 - statsd_host: "graphite.default.svc.cluster.local" - -urls: - # PyDis site vars - site: &DOMAIN "web:8000" - site_api: &API !JOIN [*DOMAIN, "/api"] - site_api_schema: "http://" - site_paste: &PASTE !JOIN ["paste.", "pythondiscord.com"] - site_schema: &SCHEMA "http://" - site_staff: &STAFF !JOIN [*DOMAIN, "/staff"] - - paste_service: !JOIN ["https://", *PASTE, "/{key}"] - site_logs_view: !JOIN [*SCHEMA, *STAFF, "/bot/logs"] - - # Snekbox - snekbox_eval_api: "http://localhost:8060/eval" - snekbox_311_eval_api: "http://localhost:8065/eval" - -##### << Replace the following � characters with the channel IDs in your test server >> ##### -# This assumes the template was used: https://discord.new/zmHtscpYN9E3 -dev_guild: - id: &DEV_GUILD_ID � - - categories: - logs: &DEV_LOGS � - help_available: &DEV_HELP_AVAILABLE � - help_occupied: &DEV_HELP_OCCUPIED � - help_dormant: &DEV_HELP_DORMANT � - voice: &DEV_VOICE � - - channels: - # Staff - admins_mods: &DEV_ADMINS_MODS � - lounge_helpers_org: &DEV_LOUNGE_HELPERS_ORG � - defcon: &DEV_DEFCON � - incidents: &DEV_INCIDENTS � - incidents_archive: &DEV_INCIDENTS_ARCHIVE � - staff_announcements: &DEV_STAFF_ANNOUNCEMENTS � - dev_logs: &DEV_DEV_LOGS � - - # Logs - all_logs: &DEV_ALL_LOGS � - bb_logs: &DEV_BB_LOGS � - duck_pond: &DEV_DUCK_POND � - - # Available Help Channels - how_to_get_help: &DEV_HTGH � - - # Miscellaneous - bot_commands: &DEV_BOT_CMD � - general_meta_voice: &DEV_GMV � - dev_core_contrib: &DEV_DEV � - - # Voice - voice-verification: &DEV_VOICE_VER � - vc: &DEV_VC � - staff_voice: &DEV_STAFF_VOICE � - - # News - announcements: &DEV_ANNOUNCEMENTS � - py_news: &DEV_PY_NEWS � - - # Off-topic - off_topic_0: &DEV_OT_0 � - off_topic_1: &DEV_OT_1 � - off_topic_2: &DEV_OT_2 � - -guild: - ##### << Replace the following � characters with the role and webhook IDs in your test server >> ##### - roles: - announcements: � - contributors: � - help_cooldown: � - muted: &MUTED_ROLE � - partners: &PY_PARTNER_ROLE � - python_community: &PY_COMMUNITY_ROLE � - voice_verified: � - - # Staff - admins: &ADMINS_ROLE � - core_developers: � - devops: � - domain_leads: � - helpers: &HELPERS_ROLE � - moderators: &MODS_ROLE � - mod_team: &MOD_TEAM_ROLE � - owners: &OWNERS_ROLE � - code_jam_event_team: � - project_leads: � - - # Code Jam - team_leaders: � - - # Streaming - video: � - - webhooks: - big_brother: � - dev_log: � - duck_pond: � - incidents: � - incidents_archive: � - python_news: &PYNEWS_WEBHOOK � - talent_pool: � - - ##### << At this point your test bot should be able to mostly work with your test server >> ##### - # The following is the actual configs the bot uses, don't delete these. - id: *DEV_GUILD_ID - invite: "https://discord.gg/python" - - categories: - help_available: *DEV_HELP_AVAILABLE - help_dormant: *DEV_HELP_DORMANT - help_in_use: *DEV_HELP_OCCUPIED - logs: *DEV_LOGS - voice: *DEV_VOICE - - channels: - # Public announcement and news channels - announcements: *DEV_ANNOUNCEMENTS - change_log: *DEV_ANNOUNCEMENTS - mailing_lists: *DEV_ANNOUNCEMENTS - python_events: *DEV_ANNOUNCEMENTS - python_news: *DEV_PY_NEWS - - # Development - dev_contrib: *DEV_DEV - dev_core: *DEV_DEV - dev_log: *DEV_DEV_LOGS - - # Discussion - meta: *DEV_GMV - python_general: *DEV_GMV - - # Python Help: Available - cooldown: *DEV_HTGH - how_to_get_help: *DEV_HTGH - - # Topical - discord_py: *DEV_GMV - - # Logs - attachment_log: *DEV_ALL_LOGS - message_log: *DEV_ALL_LOGS - mod_log: *DEV_ALL_LOGS - user_log: *DEV_ALL_LOGS - voice_log: *DEV_ALL_LOGS - - # Off-topic - off_topic_0: *DEV_OT_0 - off_topic_1: *DEV_OT_1 - off_topic_2: *DEV_OT_2 - - # Special - bot_commands: *DEV_BOT_CMD - voice_gate: *DEV_VOICE_VER - code_jam_planning: *DEV_ADMINS_MODS - - # Staff - admins: *DEV_ADMINS_MODS - admin_spam: *DEV_ADMINS_MODS - defcon: *DEV_DEFCON - duck_pond: *DEV_DUCK_POND - helpers: *DEV_LOUNGE_HELPERS_ORG - incidents: *DEV_INCIDENTS - incidents_archive: *DEV_INCIDENTS_ARCHIVE - mods: *DEV_ADMINS_MODS - mod_alerts: *DEV_ADMINS_MODS - mod_meta: *DEV_ADMINS_MODS - mod_spam: *DEV_ADMINS_MODS - mod_tools: *DEV_ADMINS_MODS - organisation: *DEV_LOUNGE_HELPERS_ORG - staff_lounge: *DEV_LOUNGE_HELPERS_ORG - - # Staff announcement channels - admin_announcements: *DEV_STAFF_ANNOUNCEMENTS - mod_announcements: *DEV_STAFF_ANNOUNCEMENTS - staff_announcements: *DEV_STAFF_ANNOUNCEMENTS - - # Voice Channels - admins_voice: *DEV_STAFF_VOICE - code_help_voice_1: *DEV_VC - code_help_voice_2: *DEV_VC - general_voice: *DEV_VC - staff_voice: *DEV_STAFF_VOICE - - # Voice Chat - code_help_chat_1: *DEV_GMV - code_help_chat_2: *DEV_GMV - staff_voice_chat: *DEV_ADMINS_MODS - voice_chat: *DEV_GMV - - # Watch - big_brother_logs: *DEV_BB_LOGS - - moderation_categories: - - *DEV_LOGS - - moderation_channels: - - *DEV_ADMINS_MODS - - # Modlog cog ignores events which occur in these channels - modlog_blacklist: - - *DEV_ADMINS_MODS - - *DEV_ALL_LOGS - - *DEV_STAFF_VOICE - - reminder_whitelist: - - *DEV_BOT_CMD - - *DEV_DEV - - moderation_roles: - - *ADMINS_ROLE - - *MODS_ROLE - - *MOD_TEAM_ROLE - - *OWNERS_ROLE - - staff_roles: - - *ADMINS_ROLE - - *HELPERS_ROLE - - *MODS_ROLE - - *OWNERS_ROLE - -##### << The bot shouldn't fail without these, but commands adding specific emojis won't work. >> ##### -# You should at least set the trashcan. Set the incidents emojis if relevant. -style: - emojis: - badge_bug_hunter: "<:bug_hunter_lvl1:�>" - badge_bug_hunter_level_2: "<:bug_hunter_lvl2:�>" - badge_early_supporter: "<:early_supporter:�>" - badge_hypesquad: "<:hypesquad_events:�>" - badge_hypesquad_balance: "<:hypesquad_balance:�>" - badge_hypesquad_bravery: "<:hypesquad_bravery:�>" - badge_hypesquad_brilliance: "<:hypesquad_brilliance:�>" - badge_partner: "<:partner:�>" - badge_staff: "<:discord_staff:�>" - badge_verified_bot_developer: "<:verified_bot_dev:�>" - - defcon_shutdown: "<:defcondisabled:�>" - defcon_unshutdown: "<:defconenabled:�>" - defcon_update: "<:defconsettingsupdated:�>" - - failmail: "<:failmail:�>" - - #incident_actioned: "<:incident_actioned:�>" - incident_investigating: "<:incident_investigating:�>" - incident_unactioned: "<:incident_unactioned:�>" - - status_dnd: "<:status_dnd:�>" - status_idle: "<:status_idle:�>" - status_offline: "<:status_offline:�>" - status_online: "<:status_online:�>" - - trashcan: "<:trashcan:�>" - -##### << Optional - If you don't care about the filtering, help channel and py-news cogs, ignore the rest of this file >> ##### -filter: - # What do we filter? - filter_domains: true - filter_everyone_ping: true - filter_invites: true - filter_zalgo: false - watch_regex: true - watch_rich_embeds: true - - # Notify user on filter? - # Notifications are not expected for "watchlist" type filters - notify_user_domains: false - notify_user_everyone_ping: true - notify_user_invites: true - notify_user_zalgo: false - - # Filter configuration - offensive_msg_delete_days: 7 # How many days before deleting an offensive message? - ping_everyone: true - - # Censor doesn't apply to these - channel_whitelist: - - *DEV_ADMINS_MODS - - *DEV_BB_LOGS - - *DEV_ALL_LOGS - - *DEV_LOUNGE_HELPERS_ORG - - role_whitelist: - - *ADMINS_ROLE - - *HELPERS_ROLE - - *MODS_ROLE - - *OWNERS_ROLE - - *PY_COMMUNITY_ROLE - - *PY_PARTNER_ROLE - -help_channels: - enable: true - - # Minimum interval before allowing a certain user to claim a new help channel - claim_minutes: 1 - - # Roles which are allowed to use the command which makes channels dormant - cmd_whitelist: - - *HELPERS_ROLE - - # Allowed duration of inactivity before making a channel dormant - idle_minutes: 1 - - # Allowed duration of inactivity when channel is empty (due to deleted messages) - # before message making a channel dormant - deleted_idle_minutes: 1 - - # Maximum number of channels to put in the available category - max_available: 2 - - # Maximum number of channels across all 3 categories - # Note Discord has a hard limit of 50 channels per category, so this shouldn't be > 50 - max_total_channels: 20 - - # Prefix for help channel names - name_prefix: 'help-' - - # Notify if more available channels are needed but there are no more dormant ones - notify: true - - # Channel in which to send notifications - notify_channel: *DEV_LOUNGE_HELPERS_ORG - - # Minimum interval between helper notifications - notify_minutes: 5 - - # Mention these roles in notifications - notify_roles: - - *HELPERS_ROLE - -python_news: - channel: *DEV_PY_NEWS - webhook: *PYNEWS_WEBHOOK - -##### << Add any additional sections you need to override from config-default.yml >> ##### - </code> - </pre> -</div></div></div> -<br> - -If you don't wish to use the provided `config.yml` above, these are the main sections in `config-default.yml` that need overriding: - -* `guild.id` -* `guild.categories` -* `guild.channels` -* `guild.roles` -* `guild.webhooks` -* `style.emojis` - -Additionally: - -* At this stage, set `bot.redis.use_fakeredis` to `true`. If you're looking for instructions for working with Redis, see [Working with Redis](#optional-working-with-redis). -* Set `urls.site_api` to `!JOIN [*DOMAIN, "/api"]`. -* Set `urls.site_schema` and `urls.site_api_schema` to `"http://"`. - -We understand this is tedious and are working on a better solution for setting up test servers. - -<div class="card"> - <button type="button" class="card-header collapsible"> - <span class="card-header-title subtitle is-6 my-2 ml-2">Why do you need a separate config file?</span> - <span class="card-header-icon"> - <i class="fas fa-fw fa-angle-down title is-5" aria-hidden="true"></i> - </span> - </button> - <div class="collapsible-content collapsed"> - <div class="card-content"> - While it's technically possible to edit <code>config-default.yml</code> to match your server, it is heavily discouraged. - This file's purpose is to provide the configurations the Python bot needs to run in the Python server in production, and should remain as such. - In contrast, the <code>config.yml</code> file can remain in your local copy of the code, and will be ignored by commits via the project's <code>.gitignore</code>. - </div> - </div> -</div> -<br> - -#### .env -The second file you need to create is the one containing the environment variables, and needs to be named `.env`. -Inside, add the line `BOT_TOKEN=YourDiscordBotTokenHere`. See [here](../creating-bot-account) for help with obtaining the bot token. - -The `.env` file will be ignored by commits. - ---- - -### Run it! -#### With Docker -You are now almost ready to run the Python bot. The simplest way to do so is with Docker. - -<div class="card"> - <button type="button" class="card-header collapsible"> - <span class="card-header-title subtitle is-6 my-2 ml-2">Getting started with Docker</span> - <span class="card-header-icon"> - <i class="fas fa-fw fa-angle-down title is-5" aria-hidden="true"></i> - </span> - </button> - <div class="collapsible-content collapsed"> - <div class="card-content"> - The requirements for Docker are: - <ul> - <li><a href="https://docs.docker.com/install">Docker CE</a></li> - <li>Docker Compose. If you're using macOS and Windows, this already comes bundled with the previous installation. Otherwise, you can download it either from the <a href="https://docs.docker.com/compose/install">website</a>, or by running <code>pip install docker-compose</code>.</li> - </ul> - <p class="notification is-warning">If you get any Docker related errors, reference the <a href="../docker#possible-issues">Possible Issue</a> section of the Docker page.</p> - </div> - </div> -</div> -<br> - -In your `config.yml` file: - -* Set `urls.site` to `"web:8000"`. -* If you wish to work with snekbox set the following: - * `urls.snekbox_eval_api` to `"http://snekbox:8060/eval"` - * `urls.snekbox_311_eval_api` to `"http://snekbox-311:8060/eval"`. - -Assuming you have Docker installed **and running**, enter the cloned repo in the command line and type `docker-compose up`. - -If working with snekbox you can run `docker-compose --profile 3.10 up` to also start up a 3.10 snekbox container, in addition to the default 3.11 container! - -After pulling the images and building the containers, your bot will start. Enter your server and type `!help` (or whatever prefix you chose instead of `!`). - -Your bot is now running, but this method makes debugging with an IDE a fairly involved process. For additional running methods, continue reading the following sections. - -#### With the Bot Running Locally -The advantage of this method is that you can run the bot's code in your preferred editor, with debugger and all, while keeping all the setup of the bot's various dependencies inside Docker. - -* Append the following line to your `.env` file: `BOT_API_KEY=badbot13m0n8f570f942013fc818f234916ca531`. -* In your `config.yml` file, set `urls.site` to `"localhost:8000"`. If you wish to keep using `web:8000`, then [COMPOSE_PROJECT_NAME](../docker/#compose-project-names) has to be set. -* To work with snekbox, set `urls.snekbox_eval_api` to `"http://localhost:8060/eval"` and `urls.snekbox_311_eval_api` to `"http://localhost:8065/eval"` - -You will need to start the services separately, but if you got the previous section with Docker working, that's pretty simple: - -* `docker-compose up web` to start the site container. This is required. -* `docker-compose up snekbox` to start the snekbox container. You only need this if you're planning on working on the snekbox cog. -* `docker-compose up snekbox-311` to start the snekbox 3.11 container. You only need this if you're planning on working on the snekbox cog. -* `docker-compose up redis` to start the Redis container. You only need this if you're not using fakeredis. For more info refer to [Working with Redis](#optional-working-with-redis). - -You can start several services together: `docker-compose up web snekbox redis`. - -##### Setting Up a Development Environment -The bot's code is Python code like any other. To run it locally, you will need the right version of Python with the necessary packages installed: - -1. Make sure you have [Python 3.10](https://www.python.org/downloads/) installed. It helps if it is your system's default Python version. -2. [Install Poetry](https://github.com/python-poetry/poetry#installation). -3. [Install the dependencies](../installing-project-dependencies). - -With at least the site running in Docker already (see the previous section on how to start services separately), you can now start the bot locally through the command line, or through your preferred IDE. -<div class="card"> - <button type="button" class="card-header collapsible"> - <span class="card-header-title subtitle is-6 my-2 ml-2">Ways to run code</span> - <span class="card-header-icon"> - <i class="fas fa-fw fa-angle-down title is-5" aria-hidden="true"></i> - </span> - </button> - <div class="collapsible-content collapsed"> - <div class="card-content"> - Notice that the bot is started as a module. There are several ways to do so: - <ul> - <li>Through the command line, inside the bot directory, with either <code>poetry run task start</code>, or directly <code>python -m bot</code>.</li> - <li>If using PyCharm, enter <code>Edit Configurations</code> and set everything according to this image: <img src="/static/images/content/contributing/pycharm_run_module.png"></li> - <li>If using Visual Studio Code, set the interpreter to the poetry environment you created. In <code>launch.json</code> create a new Python configuration, and set the name of the program to be run to <code>bot</code>. VSC will correctly run it as a module.</li> - </ul> - </div> - </div> -</div> -<br> - -#### With More Things Running Locally -You can run additional services on the host, but this guide won't go over how to install and start them in this way. -If possible, prefer to start the services through Docker to replicate the production environment as much as possible. - -The site, however, is a mandatory service for the bot. -Refer to the [previous section](#with-the-bot-running-locally) and the [site contributing guide](../site) to learn how to start it on the host, in which case you will need to change `urls.site` in `config.yml` to wherever the site is being hosted. - ---- -### Development Tips -Now that you have everything setup, it is finally time to make changes to the bot! - -#### Working with Git - -Version control of our projects is done using Git and Github. -It can be intimidating at first, so feel free to ask for any help in the server. - -[**Click here to see the basic Git workflow when contributing to one of our projects.**](../working-with-git/) - -#### Running tests - -[This section](https://github.com/python-discord/bot/blob/main/tests/README.md#tools) of the README in the `tests` repository will explain how to run tests. -The whole document explains how unittesting works, and how it fits in the context of our project. - -Make sure to run tests *before* pushing code. - -Even if you run the bot through Docker, you might want to [setup a development environment](#setting-up-a-development-environment) in order to run the tests locally. - -#### Lint before you push -As mentioned in the [contributing guidelines](../contributing-guidelines), you should make sure your code passes linting for each commit you make. - -For ease of development, you can install the pre-commit hook with `poetry run task precommit`, which will check your code every time you try to commit it. -For that purpose, even if you run the bot through Docker, you might want to [setup a development environment](#setting-up-a-development-environment), as otherwise the hook installation will fail. - -#### Reloading parts of the bot -If you make changes to an extension, you might not need to restart the entire bot for the changes to take effect. The command `!ext reload <extension_name>` re-imports the files associated with the extension. -Invoke `!ext list` for a full list of the available extensions. In this bot in particular, cogs are defined inside extensions. - -Note that if you changed code that is not associated with a particular extension, such as utilities, converters, and constants, you will need to restart the bot. - -#### Adding new statistics - -Details on how to add new statistics can be found on the [statistic infrastructure page](https://blog.pythondiscord.com/statistics-infrastructure). -We are always open to more statistics so add as many as you can! - ---- - -### Optional: Working with Redis -In [Configure the Bot](#configyml) you were asked to set `bot.redis.use_fakeredis` to `true`. If you do not need to work on features that rely on Redis, this is enough. Fakeredis will give the illusion that features relying on Redis are saving information properly, but restarting the bot or the specific cog will wipe that information. - -If you are working on a feature that relies on Redis, you will need to enable Redis to make sure persistency is achieved for the feature across restarts. The first step towards that is going to `config.yml` and setting `bot.redis.use_fakeredis` to `false`. - -#### Starting Redis in Docker (Recommended) -If you're using the Docker image provided in the project's Docker Compose, open your `config.yml` file. If you're running the bot in Docker, set `bot.redis.host` to `redis`, and if you're running it on the host set it to `localhost`. Set `bot.redis.password` to `null`. - -#### Starting Redis Using Other Methods -You can run your own instance of Redis, but in that case you will need to correctly set `bot.redis.host` and `bot.redis.port`, and the `bot.redis.password` value in `config-default.yml` should not be overridden. Then, enter the `.env` file, and set `REDIS_PASSWORD` to whatever password you set. - ---- - -### Optional: Working with Metricity -[Metricity](https://github.com/python-discord/metricity) is our home-grown bot for collecting metrics on activity within the server, such as what users are present, and IDs of the messages they've sent. -Certain features in the Python bot rely on querying the Metricity database for information such as the number of messages a user has sent, most notably the voice verification system. - -If you wish to work on a feature that relies on Metricity, for your convenience we've made the process of using it relatively painless with Docker: Enter the `.env` file you've written for the Python bot, and append the line `USE_METRICITY=true`. -Note that if you don't need Metricity, there's no reason to have it enabled as it is just unnecessary overhead. - -To make the Metricity bot work with your test server, you will need to override its configurations similarly to the Python bot. -You can see the various configurations in [the Metricity repo](https://github.com/python-discord/metricity), but the bare minimum is the guild ID setting. -In your local version of the Python bot repo, create a file called `metricity-config.toml` and insert the following lines: -```yaml -[bot] -guild_id = replace_with_your_guild_id -``` -To properly replicate production behavior, set the `staff_role_id`, `staff_categories`, and `ignore_categories` fields as well. - -Now, `docker-compose up` will also start Metricity. - -If you want to run the bot locally, you can run `docker-compose up metricity` instead. - ---- - -### Issues? -If you have any issues with setting up the bot, come discuss it with us on the [#dev-contrib](https://discord.gg/2h3qBv8Xaa) channel on our server. - -If you find any bugs in the bot or would like to request a feature, feel free to open an issue on the repository. - ---- - -### Appendix: Full ENV File Options -The following is a list of all available environment variables used by the bot: - -| Variable | Required | Description | -| -------- | -------- | -------- | -| `BOT_TOKEN` | Always | Your Discord bot account's token (see [Set Up a Bot Account](#set-up-a-bot-account)). | -| `BOT_API_KEY` | When running bot without Docker | Used to authenticate with the site's API. When using Docker to run the bot, this is automatically set. By default, the site will always have the API key shown in the example below. | -| `BOT_SENTRY_DSN` | When connecting the bot to sentry | The DSN of the sentry monitor. | -| `BOT_TRACE_LOGGERS ` | When you wish to see specific or all trace logs | Comma separated list that specifies which loggers emit trace logs through the listed names. If the ! prefix is used, all of the loggers except the listed ones are set to the trace level. If * is used, the root logger is set to the trace level. | -| `BOT_DEBUG` | In production | `true` or `false`, depending on whether to enable debug mode, affecting the behavior of certain features. `true` by default. -| `REDIS_PASSWORD` | When not using FakeRedis | The password to connect to the Redis database (see [Optional: Working with Redis](#optional-working-with-redis)). | -| `USE_METRICITY` | When using Metricity | `true` or `false`, depending on whether to enable metrics collection using Metricity (see [Optional: Working with Metricity](#optional-working-with-metricity)). `false` by default. | -| `GITHUB_API_KEY` | When you wish to interact with GitHub | The API key to interact with GitHub, for example to download files for the branding manager. -| `METABASE_USERNAME` | When you wish to interact with Metabase | The username for a Metabase admin account. -| `METABASE_PASSWORD` | When you wish to interact with Metabase | The password for a Metabase admin account. - ---- - -# Next steps -Now that you have everything setup, it is finally time to make changes to the bot! If you have not yet read the [contributing guidelines](../contributing-guidelines.md), now is a good time. Contributions that do not adhere to the guidelines may be rejected. - -If you're not sure where to go from here, our [detailed walkthrough](../#2-set-up-the-project) is for you. - -Have fun! diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/cloning-repository.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/cloning-repository.md deleted file mode 100644 index 23d525b8..00000000 --- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/cloning-repository.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: Cloning a Repository -description: A guide to cloning git repositories. -icon: fab fa-github ---- - -> **Note:** The process varies depending on your choice of code editor / IDE, so refer to one of the following guides: - -- [Cloning with the command line](#cloning-with-the-command-line) -- [Cloning with PyCharm](#cloning-with-pycharm) - -The following will use the [Sir-Lancebot](https://github.com/python-discord/sir-lancebot/) repository as an example, but the steps are the same for all other repositories. You should have already retrieved your fork's Git URL as described in [**Creating a Fork**](../forking-repository). - ---- - -## Cloning with the command line - -1. Clone your forked repository using `git clone` followed by your fork's Git URL. Then, change your working directory to the repository. - -```shell -$ git clone https://github.com/<your username>/sir-lancebot -... -$ cd sir-lancebot -``` - ---- - -## Cloning with PyCharm - -1. Load up PyCharm and click `Get from VCS`.<br> -  -2. Enter the URL of your forked repository. -3. Change the directory if you desire and click `Clone`.<br> -  diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/commit-messages.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/commit-messages.md deleted file mode 100644 index ba476b65..00000000 --- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/commit-messages.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Writing Good Commit Messages -description: Information about logging in our projects. ---- - -A well-structured git log is key to a project's maintainability; it provides insight into when and *why* things were done for future maintainers of the project. - -Commits should be as narrow in scope as possible. -Commits that span hundreds of lines across multiple unrelated functions and/or files are very hard for maintainers to follow. -After about a week they'll probably be hard for you to follow, too. - -Please also avoid making minor commits for fixing typos or linting errors. -[Don’t forget to lint before you push!](https://soundcloud.com/lemonsaurusrex/lint-before-you-push) - -A more in-depth guide to writing great commit messages can be found in Chris Beam's [How to Write a Git Commit Message](https://chris.beams.io/posts/git-commit/). diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/configure-environment-variables.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/configure-environment-variables.md deleted file mode 100644 index 8b8e3f95..00000000 --- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/configure-environment-variables.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: Configure Environment Variables -description: A guide to configuring environment variables. -icon: fas fa-cog ---- - -1. Create a text file named **.env** in your project root (that's the base folder of your repository): - * Unix/Git Bash: `touch /path/to/project/.env` - * Windows CMD: `type nul > \path\to\project\.env` (The error *The system cannot find the file specified* can be safely ignored.) -> **Note:** The entire file name is literally `.env` -2. Open the file with any text editor. -3. Each environment variable is on its own line, with the variable and the value separated by a `=` sign. - -Example: - -* Set the environment variable `SEASONALBOT_DEBUG` to `True`: -``` -SEASONALBOT_DEBUG=True -``` -* Set the environment variable `CHANNEL_ANNOUNCEMENTS` to `12345`: -``` -CHANNEL_ANNOUNCEMENTS=12345 -``` diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines.md deleted file mode 100644 index d1e4250d..00000000 --- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: Contributing Guidelines -description: Guidelines to adhere to when contributing to our projects. ---- - -Thank you for your interest in our projects! -This page contains the golden rules to follow when contributing. If you have questions about how to get started contributing, check out our [in-depth walkthrough](../../contributing/). - -1. **Lint before you push.** -We have simple but strict style rules that are enforced through linting. -[Set up a pre-commit hook](../linting/) to lint your code when you commit it. -Not all of the style rules are enforced by linting, so make sure to read the [style guide](../style-guide/) as well. -2. **Make great commits.** -Great commits should be atomic, with a commit message explaining what and why. -Check out [Writing Good Commit Messages](../commit-messages/) for details. -3. **Do not open a pull request if you aren't assigned to the issue.** -If someone is already working on it, consider offering to collaborate with that person. -4. **Use assets licensed for public use.** -Whenever the assets are images, audio or even code, they must have a license compatible with our projects. -5. **Follow the [Python Discord Code of Conduct](https://pydis.com/coc).** -We aim to foster a welcoming and friendly environment on our open source projects. -We take violations of our Code of Conduct very seriously, and may respond with moderator action. - -<br/> - -Failing to comply with our guidelines may lead to a rejection of the contribution. -If you have questions about any of the rules, feel free to ask us in the [`#dev-contrib`](https://discord.gg/2h3qBv8Xaa) channel in our [Discord server](https://discord.gg/python). -{: .notification .is-warning } diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/creating-bot-account.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/creating-bot-account.md deleted file mode 100644 index 51da3f34..00000000 --- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/creating-bot-account.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: Setting up a Bot Account -description: How to set up a bot account. -icon: fab fa-discord ---- -1. Go to the [Discord Developers Portal](https://discordapp.com/developers/applications/). -2. Click on the `New Application` button, enter your desired bot name, and click `Create`. -3. In your new application, go to the `Bot` tab, click `Add Bot`, and confirm `Yes, do it!` -4. Change your bot's `Public Bot` setting off so only you can invite it, save, and then get your **Bot Token** with the `Copy` button. -> **Note:** **DO NOT** post your bot token anywhere public. If you do it can and will be compromised. -5. Save your **Bot Token** somewhere safe to use in the project settings later. -6. In the `General Information` tab, grab the **Application ID**. -7. Replace `<APPLICATION_ID_HERE>` in the following URL and visit it in the browser to invite your bot to your new test server. -```plaintext -https://discordapp.com/api/oauth2/authorize?client_id=<APPLICATION_ID_HERE>&permissions=8&scope=bot -``` -Optionally, you can generate your own invite url in the `OAuth` tab, after selecting `bot` as the scope. diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/docker.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/docker.md deleted file mode 100644 index 63be9f3e..00000000 --- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/docker.md +++ /dev/null @@ -1,120 +0,0 @@ ---- -title: Working with Docker & Docker Compose -description: Guide to running our projects with Docker and Docker CE. -icon: fab fa-docker -toc: 2 ---- - -Both our [Site](../site/) and [Bot](../bot/) projects use Docker and Docker-Compose during development in order to provide an easy to setup and consistent development environment. - -Consider reading some of the following topics if you're interested in learning more about Docker itself: - - * [**What is Docker?**](https://docs.docker.com/engine/docker-overview/) - * [**How can I learn to use it for my own stuff?**](https://docs.docker.com/get-started/) - * [**What about Docker Compose, what's it for?**](https://docs.docker.com/compose/) - -# Docker Installation -You can find installation guides available for your respective OS from the official Docker documentation: -[https://docs.docker.com/install/](https://docs.docker.com/install/) - -## After Installing on Linux -If you're on Linux, there's a few extra things you should do: - -1. [**Add your user to the `docker` user group so you don't have to use `sudo` when running docker or docker-compose.**](#add-user-group) -2. [**Start up the Docker service.**](#run-the-service) -3. [**Set the Docker service to start on boot.**](#start-on-boot) **(optional)** - -### Run the Service -Most linux distributions **systemd**, you can start the service with: -```shell -$ sudo systemctl start docker -``` - -### Add User Group -```shell -$ sudo groupadd docker -$ sudo usermod -aG docker $USER -``` -Log out and log back in to ensure your group changes work. - -### Start on Boot -```shell -$ sudo systemctl enable docker -``` - -# Possible Issues -### Couldn't connect to Docker daemon -```shell -ERROR: Couldn't connect to Docker daemon at http+docker://localhost - is it running? -``` -**Problem**<br> -Your Docker service is either not started, or you haven't yet installed Docker. - -**Solution**<br> -[Start the service](#run-the-service) or ensure it's installed. -If it's not, [install it](#docker-installation). - -### Error loading config file -```plaintext -WARNING: Error loading config file: /home/user/.docker/config.json - -stat /home/user/.docker/config.json: permission denied -``` -**Problem**<br> -You initially ran Docker using `sudo` before adding your user to the `docker` group, resulting in your `~/.docker/` directory being created with incorrect permissions. - -**Solution**<br> -Remove the existing `~/.docker/` directory. It will be automatically re-created with the correct permissions. - -### Drive has not been shared (Windows users) - -When attempting to run the `docker-compose up` command on a Windows machine, you receive the following or similar error message: -```text -ERROR: for bot_bot_1 Cannot create container for service bot: b'Drive has not been shared' -``` -**Problem**<br> -Windows has not been configured to share drives with Docker. - -**Solution**<br> -> NOTE: Solution requires Windows user credentials for an account that has administrative privileges. - -1. Right-click the Docker icon in the Windows system tray, and choose "Settings" from the context menu.<br> - - -2. Click the "Shared Drives" label at the left, and check the box next to the drive letter where your project is stored.<br> - - -3. Click "Apply" and enter appropriate Windows credentials (likely just your own account, if you have administrative privileges). - -4. Re-run the `docker-compose up` command. - -# Compose Project Names -When you launch services from a docker-compose, you'll notice the name of the containers aren't just the service name. -You'll see this when launching your compose, as well as being able to be seen in the command `docker-compose ps` which will list the containers. -It should match something like this: -``` -site_site_1 -``` -This matched the following container name format: -``` -projectname_servicename_1 -``` -By default, your project name will match the name of the folder your project is inside in all lowercase. - -You can specify a custom project name by adding a `COMPOSE_PROJECT_NAME` variable to your `.env` file before launching the compose: -``` -COMPOSE_PROJECT_NAME=site -``` -Containers with the same project name end up connected to the same network by default. -For example, the `site` container connects with `postgres` via the matching hostname inside the container. -Even if you didn't expose a port to the host, the two containers would be able to talk to each other. - -You can have two different projects able to communicate in the same way by having them use the same project name. -We use this feature to allow the `bot` container to communicate with a separate local copy of `site` that may need to be tested during development. - -By default, the `bot` container could launch with a name of `bot_bot_1` and the `site` container with a name of `site_site_1`. Since the prefixes are different, they're in distinct projects, so can't talk with each other. - -If we got to the bot's `.env` file, and add the line below, we can set `bot` to run in the same project as `site`: -``` -COMPOSE_PROJECT_NAME=site -``` -Now they can talk to each other! diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/forking-repository.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/forking-repository.md deleted file mode 100644 index 07535dbe..00000000 --- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/forking-repository.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: Forking a Repository -description: A guide to forking git repositories. -icon: fab fa-github ---- - -Before contributing to any project, you will have to fork the project, ie. create your own online copy of the project. -The following will use the [Sir-Lancebot](https://github.com/python-discord/sir-lancebot/) repository as an example, but the steps are the same for all other repositories. - -1. Navigate to the repository page and press the `Fork` button at the top of the page. - -2. Fork it to your account.<br> - -3. Later, you will need the Git URL of your forked repository in order to clone it. -In your newly forked repository, copy the Git URL by clicking the green `Code` button, then click the Copy Link button. - - -> If you have SSH set up with GitHub, you may instead click the `SSH` button above the Copy Link button to get the SSH URL. diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/hosts-file.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/hosts-file.md deleted file mode 100644 index bba5722d..00000000 --- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/hosts-file.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: Preparing Your Hosts file -description: How to setup your hosts file for project usage. -icon: fas fa-cog -toc: 3 ---- - -# What's a hosts file? -The hosts file maps a hostname/domain to an IP address, allowing you to visit a given domain on your browser and have it resolve by your system to the given IP address, even if it's pointed back to your own system or network. - -When staging a local [Site](https://pythondiscord.com/pages/contributing/site/) project, you may want to add an entries to your hosts file so you can visit the site with the domain `http://pythondiscord.local`. This is purely for convenience, and you can use `localhost` or `127.0.0.1` instead if you prefer. - -# What to add -You would add the following entry to your hosts file. - -```plaintext -127.0.0.1 pythondiscord.local -``` - -# How to add it - -### Linux -1. Run `sudo nano /etc/hosts` -2. Enter your user password. -3. Add the new content at the bottom of the file. -4. Use `CTRL+X` -5. Enter `y` to save. - -_This covers most linux distributions that come with `nano`, however you're welcome to use whatever CLI text editor you're comfortable with instead._ - -### Windows -1. Open Notepad as Administrator. -2. Open the file `C:\Windows\System32\Drivers\etc\hosts` -3. Add the new content at the bottom of the file. -4. Save. - -### MacOS -1. Run `sudo nano /private/etc/hosts` in Terminal. -2. Enter your user password. -3. Add the new content at the bottom of the file. -4. Use `CTRL+X` -5. Enter `y` to save. -6. Flush your DNS by running `dscacheutil -flushcache` diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/installing-project-dependencies.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/installing-project-dependencies.md deleted file mode 100644 index 26d6de30..00000000 --- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/installing-project-dependencies.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -title: Installing Project Dependencies -description: A guide to installing the dependencies of our projects. -icon: fab fa-python ---- - -> **Note:** The process varies depending on your choice of code editor / IDE, so refer to one of the following guides: - -- [Installing dependencies with the command line](#installing-dependencies-with-the-command-line) -- [Installing dependencies with PyCharm](#installing-dependencies-with-pycharm) - -The following will use the [Sir-Lancebot](https://github.com/python-discord/sir-lancebot/) repository as an example, but the steps are the same for all other repositories. -You should have already cloned your fork as described in [**Cloning a Repository**](../cloning-repository). - ---- - -## Installing dependencies with the command line - -1. Make sure you are in the root project directory. This directory will always have a file titled `README.md`. -2. Install project and development dependencies. Remember to also set up pre-commit hooks to ensure your pushed commits will never fail linting. - ---- - -```shell -$ poetry install -$ poetry run task precommit -``` - ---- - -## Installing dependencies with PyCharm - -1. Load up your project in PyCharm. -2. Go to the Project Settings by clicking `File`, then `Settings...`. Alternatively, use the shortcut key: `Ctrl+Alt+S` (`command+comma` on Mac OS). -3. Install the [poetry plugin](https://plugins.jetbrains.com/plugin/14307-poetry). -4. Navigate to `Project Interpreter`, then click the gear icon and click `Add`.<br/> - <br/> -5. Click `Poetry Environment`, then click `OK`.<br/> - <br/> -6. PyCharm will automatically install the packages required into a virtual environment.<br/> -  diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/issues.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/issues.md deleted file mode 100644 index 0c6d3513..00000000 --- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/issues.md +++ /dev/null @@ -1,121 +0,0 @@ ---- -title: Issues -description: Guide to Github issues. -icon: fab fa-github ---- - -## What are Issues? - -Issues are tickets that allow us to manage all the suggested features, bugs noticed and discussions about a project. - -An Issue ticket should have a simple, easy to understand title and a clearly written description outlining any of the available details. -Once an Issue is created, people can comment on it and if work is to be actioned due to it, it can be assigned to a contributor so others know that it's being worked on already. - -## How do I make an Issue? - -**Before making an Issue, search the existing ones!** -Often, an Issue ticket already exists within the scope of what you might be considering, so be sure to do a search beforehand and if there it, add any new information or suggestions to the comments on the existing Issue instead, or just add a thumbs up if you agree with it. - -If you don't see one existing, then: - -1. Click the `Issues` tab in a repository:<br> - - -2. Click `New Issue`:<br> - - -3. Enter the title and description for your issue, then click `Submit new issue`:<br> -{: width="600" } - -## What should I put as a title? - -A good title is short and to the point as to what the Issue is about. - -Avoid some of the following: - -- Writing a long title -- Being too vague -- Using informal language -- Using languages other than English - -## What makes a good description? - -A good description is well structured and contains all the information available to you at the time of creating it. If additional information comes to light, it can either be added in edits to the description afterwards, or as part of comments. - -Try to avoid: - -- Masses of unstructured blocks of never ending text -- Including unnecessary details that aren't constructive to the discussion - -Definitely try to: - -- Include relevant images in the description if it involves visual aspects -- Make use of [Github's markdown syntax](https://help.github.com/en/github/writing-on-github/basic-writing-and-formatting-syntax) for text formatting - -## What are labels? - -Labels allow us to better organise Issues by letting us view what type of Issue it is, how it might impact the codebase and at what stage it's at. - -In our repositories, we try to prefix labels belonging to the same group, for example the label groups `status` or `type`. We will be trying to keep to the same general structure across our project repositories, but just have a look at the full labels list in the respective repository to get a clear idea what's available. - -If you're a contributor, you can add relevant labels yourself to any new Issue ticket you create. - -### General label groups - -These label groups should be present on most of our main project repositories and can serve as a guide as to how they're used. - -#### area -Signifies what section of the project/codebase the Issue is focusing on addressing or discussing. Only one area should be selected usually, as the most focused area should be the selected one. Exceptions exist for Issues that impact multiple areas equally, but are generally not the norm. - -#### priority -How urgent the Issue should be addressed: - -- `critical` - Super important, likely a bug that's impacting the project severely at this moment. -- `high` - Important, impacts the project heavily and/or is time sensitive. -- `normal` - Would be convenient if it's addressed. -- `low` - Doesn't require us to look at any time soon. - -#### status -Where this issue is at currently: - -- `deferred` - Is being put off until a later date -- `planning` - The Issue is being discussed, implementation is not decided or ready to begin. -- `stale` - Hasn't been addressed or contributed to in a long time. Worth reconsidering as worth keeping open or bumped in priority if it needs to be done to get it out. -- `stalled` - Something else has prevented this Issue from moving forward for now. -- `WIP` - The issue is actively being worked on by someone already. - -#### type -What's the purpose of the Issue: - -- `bug` - Addresses a bug in the code -- `enhancement` - Changes or improves on an existing feature. -- `feature` - Addresses a possible new feature. -- `question` - Isn't addressing any code changes, only a discussion or clarification. - -#### Non-group labels -There are 4 labels that aren't in groups as they are globally recognised and shouldn't be renamed: - -- `duplicate` - Marks the Issue as being the same or within scope of an existing one -- `good first issue` - Marks the Issue as being suitable to work on by beginners -- `help wanted` - More people are needed to work on this Issue -- `invalid` - Marks the Issue as not being a proper Issue. - -## Assignments - -Once an Issue is not in the planning/discussing stage and is approved to be worked on, it can be assigned to someone interested in it. - -### Can I assign myself? - -Only staff can assign themselves to a ticket. -If a general contributor assigns themself, they'll be unassigned. - -### How do I get assigned? - -**First check that someone else isn't already assigned.** - -Once you're sure it's available and ready to be worked on, you can leave a comment in the Issue ticket. -Generally, it's first-come first served, so a staff member will usually assign you within the day if they confirm it's clear to do so. - -#### Do I get first preference to work on it if I made the Issue ticket? -As long as you say you'd like to work on it within the description of your ticket or be the first to request so in a comment. -If you forget to say so and someone else asks to be assigned, we aren't likely to unassign them afterwards, so it's entirely up to the discretion of the other person in that case. diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/linting.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/linting.md deleted file mode 100644 index f6f8a5f2..00000000 --- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/linting.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: Linting -description: A guide for linting and setting up pre-commit. ---- - -Your commit will be rejected by the build server if it fails to lint. -On most of our projects, we use `flake8` and `pre-commit` to ensure that the code style is consistent across the code base. - -`pre-commit` is a powerful tool that helps you automatically lint before you commit. -If the linter complains, the commit is aborted so that you can fix the linting errors before committing again. -That way, you never commit the problematic code in the first place! - -Please refer to the project-specific documentation to see how to setup and run those tools. -In most cases, you can install pre-commit using `poetry run task precommit`, and lint using `poetry run task lint` in the console. diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/logging.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/logging.md deleted file mode 100644 index 1291a7a4..00000000 --- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/logging.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: Logging -description: Information about logging in our projects. ---- - -Instead of using `print` statements for logging, we use the built-in [`logging`](https://docs.python.org/3/library/logging.html) module. -Here is an example usage: - -```python -import logging - -log = logging.getLogger(__name__) # Get a logger bound to the module name. -# This line is usually placed under the import statements at the top of the file. - -log.trace("This is a trace log.") -log.warning("BEEP! This is a warning.") -log.critical("It is about to go down!") -``` - -Print statements should be avoided when possible. -Our projects currently defines logging levels as follows, from lowest to highest severity: - -- **TRACE:** These events should be used to provide a *verbose* trace of every step of a complex process. This is essentially the `logging` equivalent of sprinkling `print` statements throughout the code. -- **Note:** This is a PyDis-implemented logging level. It may not be available on every project. -- **DEBUG:** These events should add context to what's happening in a development setup to make it easier to follow what's going while workig on a project. This is in the same vein as **TRACE** logging but at a much lower level of verbosity. -- **INFO:** These events are normal and don't need direct attention but are worth keeping track of in production, like checking which cogs were loaded during a start-up. -- **WARNING:** These events are out of the ordinary and should be fixed, but can cause a failure. -- **ERROR:** These events can cause a failure in a specific part of the application and require urgent attention. -- **CRITICAL:** These events can cause the whole application to fail and require immediate intervention. - -Any logging above the **INFO** level will trigger a [Sentry](https://sentry.io) issue and alert the Core Developer team. diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/obtaining-discord-ids.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/obtaining-discord-ids.md deleted file mode 100644 index afa07b5a..00000000 --- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/obtaining-discord-ids.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -title: Obtaining Discord IDs -description: How to obtain Discord IDs to set up the bots. -icon: fab fa-discord ---- -First, enable developer mode in your client so you can easily copy IDs. - -1. Go to your `User Settings` and click on the `Appearance` tab. -2. Under `Advanced`, enable `Developer Mode`. - -#### Guild ID - -Right click the server icon and click `Copy ID`. - -#### Channel ID - -Right click a channel name and click `Copy ID`. - -#### Role ID - -Right click a role and click `Copy ID`. -The easiest way to do this is by going to the role list in the guild's settings. - -#### Emoji ID - -Insert the emoji into the Discord text box, then add a backslash (`\`) right before the emoji and send the message. -The result should be similar to the following - -```plaintext -<:bbmessage:511950877733552138> -``` - -The long number you see, in this case `511950877733552138`, is the emoji's ID. - -#### Webhook ID - -Once a [webhook](https://support.discordapp.com/hc/en-us/articles/228383668-Intro-to-Webhooks) is created, the ID is found in the penultimate part of the URL. -For example, in the following URL, `661995360146817053` is the ID of the webhook. - -```plaintext -https://discordapp.com/api/webhooks/661995360146817053/t-9mI2VehOGcPuPS_F8R-6mB258Ob6K7ifhtoxerCvWyM9VEQug-anUr4hCHzdbhzfbz -``` diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/pull-requests.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/pull-requests.md deleted file mode 100644 index d193a455..00000000 --- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/pull-requests.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -title: Pull Requests -description: A guide for opening pull requests. ---- - -As stated in our [Contributing Guidelines](../contributing-guidelines/), do not open a pull request if you aren't assigned to an approved issue. You can check out our [Issues Guide](../issues/) for help with opening an issue or getting assigned to an existing one. -{: .notification .is-warning } - -Before opening a pull request you should have: - -1. Committed your changes to your local repository -2. [Linted](../linting/) your code -3. Tested your changes -4. Pushed the branch to your fork of the project on GitHub - -## Opening a Pull Request - -Navigate to your fork on GitHub and make sure you're on the branch with your changes. Click on `Contribute` and then `Open pull request`: - - - -In the page that it opened, write an overview of the changes you made and why. This should explain how you resolved the issue that spawned this PR and highlight any differences from the proposed implementation. You should also [link the issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue). - -At this stage you can also request reviews from individual contributors. If someone showed interest in the issue or has specific knowledge about it, they may be a good reviewer. It isn't necessary to request your reviewers; someone will review your PR either way. - -## The Review Process - -Before your changes are merged, your PR needs to be reviewed by other contributors. They will read the issue and your description of your PR, look at your code, test it, and then leave comments on the PR if they find any problems, possibly with suggested changes. Sometimes this can feel intrusive or insulting, but remember that the reviewers are there to help you make your code better. - -#### If the PR is already open, how do I make changes to it? - -A pull request is between a source branch and a target branch. Updating the source branch with new commits will automatically update the PR to include those commits; they'll even show up in the comment thread of the PR. Sometimes for small changes the reviewer will even write the suggested code themself, in which case you can simply accept them with the click of a button. - -If you truly disagree with a reviewer's suggestion, leave a reply in the thread explaining why or proposing an alternative change. Also feel free to ask questions if you want clarification about suggested changes or just want to discuss them further. - -## Draft Pull Requests - -GitHub [provides a PR feature](https://github.blog/2019-02-14-introducing-draft-pull-requests/) that allows the PR author to mark it as a draft when opening it. This provides both a visual and functional indicator that the contents of the PR are in a draft state and not yet ready for formal review. This is helpful when you want people to see the changes you're making before you're ready for the final pull request. - -This feature should be utilized in place of the traditional method of prepending `[WIP]` to the PR title. diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/setting-test-server-and-bot-account.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/setting-test-server-and-bot-account.md deleted file mode 100644 index 43d1c8f5..00000000 --- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/setting-test-server-and-bot-account.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: Setting Up a Test Server and Bot Account -description: How to get started with testing our bots. -icon: fab fa-discord ---- - -## Setting up a Test Server - -1. Create a Discord Server if you haven't got one already to use for testing. - ---- - -## Setting up a Bot Account - -1. Go to the [Discord Developers Portal](https://discordapp.com/developers/applications/). -2. Click on the `New Application` button, enter your desired bot name, and click `Create`. -3. In your new application, go to the `Bot` tab, click `Add Bot`, and confirm `Yes, do it!` -4. Change your bot's `Public Bot` setting off so only you can invite it, save, and then get your **Bot Token** with the `Copy` button. -> **Note:** **DO NOT** post your bot token anywhere public, or it can and will be compromised. -5. Save your **Bot Token** somewhere safe to use in the project settings later. -6. In the `OAuth2` tab, grab the **Client ID**. -7. Replace `<CLIENT_ID_HERE>` in the following URL and visit it in the browser to invite your bot to your new test server. -```plaintext -https://discordapp.com/api/oauth2/authorize?client_id=<CLIENT_ID_HERE>&permissions=8&scope=bot -``` ---- - -## Obtain the IDs - -First, enable developer mode in your client so you can easily copy IDs. - -1. Go to your `User Settings` and click on the `Appearance` tab. -2. Under `Advanced`, enable `Developer Mode`. - -#### Guild ID - -Right click the server icon and click `Copy ID`. - -#### Channel ID - -Right click a channel name and click `Copy ID`. - -#### Role ID - -Right click a role and click `Copy ID`. -The easiest way to do this is by going to the role list in the guild's settings. - -#### Emoji ID - -Insert the emoji into the Discord text box, then add a backslash `\` right before the emoji and send the message. -The result should be similar to the following - -```plaintext -<:bbmessage:511950877733552138> -``` - -The long number you see, in this case `511950877733552138`, is the emoji's ID. - -#### Webhook ID - -Once a [webhook](https://support.discordapp.com/hc/en-us/articles/228383668-Intro-to-Webhooks) is created, the ID is found in the penultimate part of the URL. -For example, in the following URL, `661995360146817053` is the ID of the webhook. - -```plaintext -https://discordapp.com/api/webhooks/661995360146817053/t-9mI2VehOGcPuPS_F8R-6mB258Ob6K7ifhtoxerCvWyM9VEQug-anUr4hCHzdbhzfbz -``` diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot.md deleted file mode 100644 index edfd7ac1..00000000 --- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot.md +++ /dev/null @@ -1,122 +0,0 @@ ---- -title: Contributing to Sir Lancebot -description: A guide to setting up and configuring Sir Lancebot. -icon: fab fa-github -toc: 1 ---- - -You should have already forked the [`sir-lancebot`](https://github.com/python-discord/sir-lancebot) repository and cloned it to your local machine. If not, check out our [detailed walkthrough](../#1-fork-and-clone-the-repo). - -Remember to ensure that you have read the [contributing guidelines](../contributing-guidelines) in full before you start contributing. - -### Requirements -- [Python 3.10.*](https://www.python.org/downloads/) -- [Poetry](https://github.com/python-poetry/poetry#installation) -- [Git](https://git-scm.com/downloads) - - [Windows Installer](https://git-scm.com/download/win) - - [MacOS Installer](https://git-scm.com/download/mac) or `brew install git` - - [Linux](https://git-scm.com/download/linux) - ---- - -## Using Gitpod -Sir Lancebot can be edited and tested on Gitpod. Gitpod will automatically install the correct dependencies and Python version, so you can get straight to coding. - -To do this, you will need a Gitpod account, which you can get [here](https://www.gitpod.io/#get-started), and a fork of Sir Lancebot. This guide covers forking the repository [here](../forking-repository). - -Afterwards, click on [this link](https://gitpod.io/#/github.com/python-discord/sir-lancebot) to spin up a new workspace for Sir Lancebot. Then run the following commands in the terminal after the existing tasks have finished running: -```sh -git remote rename origin upstream -git add remote origin https://github.com/{your_username}/sir-lancebot -``` -Make sure you replace `{your_username}` with your Github username. These commands will set the Sir Lancebot repository as the secondary remote, and your fork as the primary remote. This means you can easily grab new changes from the main Sir Lancebot repository. - -Once you've set up [a test server and bot account](#test-server-and-bot-account) and your [environment variables](#environment-variables), you are ready to begin contributing to Sir Lancebot! - -## Using Docker -Sir Lancebot can be started using Docker. Using Docker is generally recommended (but not strictly required) because it abstracts away some additional set up work. - -The requirements for Docker are: - -* [Docker CE](https://docs.docker.com/install/) -* [Docker Compose](https://docs.docker.com/compose/install/) - * `pip install docker-compose` - * This is only a required step for linux. Docker comes bundled with docker-compose on Mac OS and Windows. - ---- -# Development Environment -If you aren't using Docker, you will need to [install the project's dependencies](../installing-project-dependencies) yourself. - ---- -# Test Server and Bot Account - -You will need your own test server and bot account on Discord to test your changes to the bot. - -1. [**Create a test server**](../setting-test-server-and-bot-account#setting-up-a-test-server). -2. [**Create a bot account**](../setting-test-server-and-bot-account#setting-up-a-bot-account) and invite it to the server you just created. -3. Create the following text channels: - * `#announcements` - * `#dev-log` - * `#sir-lancebot-commands` -4. Create the following roles: - * `@Admins` -5. Note down the IDs for your server, as well as any channels and roles created. - * [**Learn how to obtain the ID of a server, channel or role here.**](../setting-test-server-and-bot-account#obtain-the-ids) - ---- - -## Environment variables -You will have to setup environment variables: - -* [**Learn how to set environment variables here.**](../configure-environment-variables) - -The following variables are needed for running Sir Lancebot: - -| Environment Variable | Description | -| -------- | -------- | -| `BOT_TOKEN` | Bot Token from the [Discord developer portal](https://discord.com/developers/applications) | -| `BOT_GUILD` | ID of the Discord Server | -| `BOT_ADMIN_ROLE_ID` | ID of the role `@Admins` | -| `ROLE_HELPERS` | ID of the role `@Helpers` | -| `CHANNEL_ANNOUNCEMENTS` | ID of the `#announcements` channel | -| `CHANNEL_DEVLOG` | ID of the `#dev-log` channel | -| `CHANNEL_COMMUNITY_BOT_COMMANDS` | ID of the `#sir-lancebot-commands` channel | - -[**Full environment variable reference for this project.**](./env-var-reference) - ---- - -While not required, we advise you set `USE_FAKEREDIS` to `true` in development to avoid the need of setting up a Redis server. -It does mean you may lose persistent data on restart but this is non-critical. -Otherwise, please see the below linked guide for Redis related variables. -{: .notification .is-warning } - ---- -# Run the project -The sections below describe the two ways you can run this project. We recommend Docker as it requires less setup. - -## Run with Docker -Make sure to have Docker running, then use the Docker command `docker-compose up` in the project root. -The first time you run this command, it may take a few minutes while Docker downloads and installs Sir Lancebot's dependencies. - -```shell -$ docker-compose up -``` - -If you get any Docker related errors, reference the [Possible Issues](../docker#possible-issues) section of the Docker page. -{: .notification .is-warning } - -## Run on the host -After installing project dependencies use the poetry command `poetry run task start` in the project root. - -```shell -$ poetry run task start -``` ---- - -# Next steps -Now that you have everything setup, it is finally time to make changes to the bot! If you have not yet read the [contributing guidelines](../contributing-guidelines.md), now is a good time. Contributions that do not adhere to the guidelines may be rejected. - -If you're not sure where to go from here, our [detailed walkthrough](../#2-set-up-the-project) is for you. - -Have fun! diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot/_info.yml b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot/_info.yml deleted file mode 100644 index 349e6149..00000000 --- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot/_info.yml +++ /dev/null @@ -1,2 +0,0 @@ -title: Contributing to Sir Lancebot -description: A guide to setting up and configuring Sir Lancebot. diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot/env-var-reference.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot/env-var-reference.md deleted file mode 100644 index 3862fb2e..00000000 --- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/sir-lancebot/env-var-reference.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -title: Sir-Lancebot Environment Variable Reference -description: The full environment variable reference for Sir-Lancebot. -toc: 2 ---- -## General Variables -The following variables are needed for running Sir Lancebot: - -| Environment Variable | Description | -| -------- | -------- | -| `BOT_TOKEN` | Bot Token from the [Discord developer portal](https://discord.com/developers/applications) | -| `BOT_GUILD` | ID of the Discord Server | -| `BOT_ADMIN_ROLE_ID` | ID of the role `@Admins` | -| `ROLE_HELPERS` | ID of the role `@Helpers` | -| `CHANNEL_ANNOUNCEMENTS` | ID of the `#announcements` channel | -| `CHANNEL_DEVLOG` | ID of the `#dev-log` channel | -| `CHANNEL_COMMUNITY_BOT_COMMANDS` | ID of the `#sir-lancebot-commands` channel | -| `CHANNEL_REDDIT` | ID of the `#reddit` channel | - ---- -## Debug Variables -Additionally, you may find the following environment variables useful during development: - -| Environment Variable | Description | -| -------- | -------- | -| `BOT_DEBUG` | Debug mode of the bot | False | -| `PREFIX` | The bot's invocation prefix | `.` | -| `CYCLE_FREQUENCY` | Amount of days between cycling server icon | 3 | -| `MONTH_OVERRIDE` | Integer in range `[0, 12]`, overrides current month w.r.t. seasonal decorators | -| `REDIS_HOST` | The address to connect to for the Redis database. | -| `REDIS_PORT` | | -| `REDIS_PASSWORD` | | -| `USE_FAKEREDIS` | If the FakeRedis module should be used. Set this to true if you don't have a Redis database setup. | -| `BOT_SENTRY_DSN` | The DSN of the sentry monitor. | -| `TRASHCAN_EMOJI` | The full emoji to use for the trashcan. Format should be like the output of sending `\:emoji:` on discord. | - ---- -## Tokens/APIs -If you will be working with an external service, you might have to set one of these tokens: - -| Token | Description | -| -------- | -------- | -| `GITHUB_TOKEN` | Personal access token for GitHub, raises rate limits from 60 to 5000 requests per hour. | -| `GIPHY_TOKEN` | Required for API access. [Docs](https://developers.giphy.com/docs/api) | -| `OMDB_API_KEY` | Required for API access. [Docs](https://www.omdbapi.com/) | -| `REDDIT_CLIENT_ID` | OAuth2 client ID for authenticating with the [reddit API](https://github.com/reddit-archive/reddit/wiki/OAuth2). | -| `REDDIT_SECRET` | OAuth2 secret for authenticating with the reddit API. *Leave empty if you're not using the reddit API.* | -| `REDDIT_WEBHOOK` | Webhook ID for Reddit channel | -| `YOUTUBE_API_KEY` | An OAuth Key or Token are required for API access. [Docs](https://developers.google.com/youtube/v3/docs#calling-the-api) | -| `TMDB_API_KEY` | Required for API access. [Docs](https://developers.themoviedb.org/3/getting-started/introduction) | -| `NASA_API_KEY` | Required for API access. [Docs](https://api.nasa.gov/) | -| `WOLFRAM_API_KEY` | | -| `UNSPLASH_KEY` | Required for API access. Use the `access_token` given by Unsplash. [Docs](https://unsplash.com/documentation) | -| `IGDB_CLIENT_ID` | OAuth2 client ID for authenticating with the [IGDB API](https://api-docs.igdb.com/) | -| `IGDB_CLIENT_SECRET` | OAuth2 secret for authenticating with the IGDB API. *Leave empty if you're not using the IGDB API.* | - ---- -## Seasonal Cogs -These variables might come in handy while working on certain cogs: - -| Cog | Environment Variable | Description | -| -------- | -------- | -------- | -| Advent of Code | `AOC_LEADERBOARDS` | List of leaderboards separated by `::`. Each entry should have an `id,session cookie,join code` separated by commas in that order. | -| Advent of Code | `AOC_STAFF_LEADERBOARD_ID` | Integer ID of the staff leaderboard. | -| Advent of Code | `AOC_ROLE_ID` | ID of the advent of code role. -| Advent of Code | `AOC_IGNORED_DAYS` | Comma separated list of days to ignore while calculating score. | -| Advent of Code | `AOC_YEAR` | Debug variable to change the year used for AoC. | -| Advent of Code | `AOC_CHANNEL_ID` | The ID of the `#advent-of-code` channel | -| Advent of Code | `AOC_COMMANDS_CHANNEL_ID` | The ID of the `#advent-of-code-commands` channel | -| Advent of Code | `AOC_FALLBACK_SESSION` | | -| Advent of Code | `AOC_SESSION_COOKIE` | | -| Valentines | `LOVEFEST_ROLE_ID` | | -| Wolfram | `WOLFRAM_USER_LIMIT_DAY` | | -| Wolfram | `WOLFRAM_GUILD_LIMIT_DAY` | | diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/site.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/site.md deleted file mode 100644 index 9786698b..00000000 --- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/site.md +++ /dev/null @@ -1,178 +0,0 @@ ---- -title: Contributing to Site -description: A guide to setting up and configuring Site. -icon: fab fa-github -toc: 1 ---- - -You should have already forked the [`site`](https://github.com/python-discord/site) repository and cloned it to your local machine. If not, check out our [detailed walkthrough](../#1-fork-and-clone-the-repo). - -### Requirements - -- [Python 3.10](https://www.python.org/downloads/) -- [Poetry](https://python-poetry.org/docs/#installation) - - `pip install poetry` -- [Git](https://git-scm.com/downloads) - - [Windows](https://git-scm.com/download/win) - - [MacOS](https://git-scm.com/download/mac) or `brew install git` - - [Linux](https://git-scm.com/download/linux) - -Using Docker (recommended): - -- [Docker CE](https://docs.docker.com/install/) -- [Docker Compose](https://docs.docker.com/compose/install/) - - `pip install docker-compose` - -Without Docker: - -- [PostgreSQL](https://www.postgresql.org/download/) - - Note that if you wish, the webserver can run on the host and still use Docker for PostgreSQL. - ---- -# Development environment - -[Install the project's dependencies](../installing-project-dependencies/) - -## Without Docker - -Some additional steps are needed when not using Docker. Docker abstracts away these steps which is why using it is generally recommended. - -### 1. PostgreSQL setup - -Enter psql, a terminal-based front-end to PostgreSQL: - -```shell -psql -qd postgres -``` - -Run the following queries to create the user and database: - -```sql -CREATE USER pysite WITH SUPERUSER PASSWORD 'pysite'; -CREATE DATABASE pysite WITH OWNER pysite; -CREATE DATABASE metricity WITH OWNER pysite; -``` - -Finally, enter `/q` to exit psql. - -### 2. Environment variables - -These contain various settings used by the website. To learn how to set environment variables, read [this page](../configure-environment-variables/) first. - -```shell -DATABASE_URL=postgres://pysite:pysite@localhost:7777/pysite -METRICITY_DB_URL=postgres://pysite:pysite@localhost:7777/metricity -DEBUG=1 -SECRET_KEY=suitable-for-development-only -STATIC_ROOT=staticfiles -``` - -The [Configuration in Detail](#configuration-in-detail) section contains -detailed information about these settings. - -#### Notes regarding `DATABASE_URL` - -- If the database is hosted locally i.e. on the same machine as the webserver, then use `localhost` for the host. Windows and macOS users may need to use the [Docker host IP](https://stackoverflow.com/questions/22944631/how-to-get-the-ip-address-of-the-docker-host-from-inside-a-docker-container) instead. -- If the database is running in Docker, use port `7777`. Otherwise, use `5432` as that is the default port used by PostegreSQL. -- If you configured PostgreSQL in a different manner or you are not hosting it locally, then you will need to determine the correct host and port yourself. -The user, password, and database name should all still be `pysite` unless you deviated from the setup instructions in the previous section. - ---- -# Run the project - -The project can be started with Docker or by running it directly on your system. - -## Run with Docker - -Start the containers using Docker Compose: - -```shell -docker-compose up -``` - -The `-d` option can be appended to the command to run in detached mode. This runs the containers in the background so the current terminal session is available for use with other things. - -If you get any Docker related errors, reference the [Possible Issues](https://pythondiscord.com/pages/contributing/docker/#possible-issues") section of the Docker page. -{: .notification .is-warning } - -## Run on the host - -Running on the host is particularly useful if you wish to debug the site. The [environment variables](#2-environment-variables) shown in a previous section need to have been configured. - -### Database - -First, start the PostgreSQL database. -Note that this can still be done with Docker even if the webserver will be running on the host - simply adjust the `DATABASE_URL` environment variable accordingly. - -If you chose to use Docker for just the database, use Docker Compose to start the container: - -```shell -docker-compose up postgres -``` - -If you're not using Docker, then use [pg_ctl](https://www.postgresql.org/docs/current/app-pg-ctl.html) or your system's service manager if PostgreSQL isn't already running. - -### Webserver - -Starting the webserver is done simply through poetry: - -```shell -poetry run task start -``` - ---- -# Working on the project - -The development environment will watch for code changes in your project directory and will restart the server when a module has been edited automatically. -Unless you are editing the Dockerfile or docker-compose.yml, you shouldn't need to manually restart the container during a developing session. - -[**Click here to see the basic Git workflow when contributing to one of our projects.**](../working-with-git/) - ---- -# Django admin site - -Django provides an interface for administration with which you can view and edit the models among other things. - -It can be found at [http://127.0.0.1:8000/admin/](http://127.0.0.1:8000/admin/). The default credentials are `admin` for the username and `admin` for the password. - ---- - -# Configuration in detail - -The website is configured through the following environment variables: - -## Essential -- **`DATABASE_URL`**: A string specifying the PostgreSQL database to connect to, - in the form `postgresql://user:password@host/database`, such as - `postgresql://joethedestroyer:ihavemnesia33@localhost/pysite_dev` - -- **`METRICITY_DB_URL`**: A string specifying the PostgreSQL metric database to - connect to, in the same form as `$DATABASE_URL`. - -- **`DEBUG`**: Controls Django's internal debugging setup. Enable this when - you're developing locally. Optional, defaults to `False`. - -- **`LOG_LEVEL`**: Any valid Python `logging` module log level - one of `DEBUG`, - `INFO`, `WARN`, `ERROR` or `CRITICAL`. When using debug mode, this defaults to - `INFO`. When testing, defaults to `ERROR`. Otherwise, defaults to `WARN`. - -## Deployment -- **`ALLOWED_HOSTS`**: A comma-separated lists of alternative hosts to allow to - host the website on, when `DEBUG` is not set. Optional, defaults to the - `pythondiscord.com` family of domains. - -- **`SECRET_KEY`**: The secret key used in various parts of Django. Keep this - secret as the name suggests! This is managed for you in debug setups. - -- **`STATIC_ROOT`**: The root in which `python manage.py collectstatic` - collects static files. Optional, defaults to `/app/staticfiles` for the - standard Docker deployment. - ---- - -# Next steps -Now that you have everything setup, it is finally time to make changes to the site! If you have not yet read the [contributing guidelines](../contributing-guidelines.md), now is a good time. Contributions that do not adhere to the guidelines may be rejected. - -If you're not sure where to go from here, our [detailed walkthrough](../#2-set-up-the-project) is for you. - -Have fun! diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/style-guide.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/style-guide.md deleted file mode 100644 index b26c467c..00000000 --- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/style-guide.md +++ /dev/null @@ -1,204 +0,0 @@ ---- -title: Style Guide -description: Coding conventions for our Open Source projects. -icon: fab fa-python ---- - -> A style guide is about consistency. -> Consistency with this style guide is important. -> Consistency within a project is more important. -> Consistency within one module or function is the most important. - -> However, know when to be inconsistent -- sometimes style guide recommendations just aren't applicable. -> When in doubt, use your best judgment. Look at other examples and decide what looks best. And don't hesitate to ask! - -> — [PEP 8, the general Style Guide for Python Code](https://www.python.org/dev/peps/pep-0008/) - -All of our projects have a certain project-wide style that contributions should attempt to maintain consistency with. -During PR review, it's not unusual for style adjustment requests to be commented. - -We've added below a guideline to aid new contributors, allowing them to refer to it during development, to help get more familiar and to hopefully lessen some of the frustrations that come from first-time contributions. - -Anything that isn't defined below falls back onto the [PEP 8 guidelines](https://www.python.org/dev/peps/pep-0008/), so be sure to reference it also. - -# Code Structure -## Maximum Line Length -Each project has specified their respective maximum line lengths. -Generally, we try to keep this at 100 or 120 characters, making our length longer than the typical 79 characters. - -Most IDEs and smarter editors will use the lint settings we store in the project's `tox.ini` or `.flake8` file after you install the appropriate development packages, so should conflict with our suggested project rules. -If your editor does not have this ability but instead requires setting it manually, make sure to change it to the appropriate length specified in these files. - -## Line Breaks -Avoid breaking a line far earlier than necessary, such as: - -```py -array = [ # there was plenty of room on this line - 1, 2, 3, - 4, 5, 6 -] -``` - -Try instead to make use of the space you're allowed to use appropriately: -```py -array = [1, 2, 3, 4, 5, 6] -``` - -Any line continuations must be indented a full level, i.e. 4 spaces. So don't do: -```py -def an_example_function_definition_that_is_kinda_long( - variable_name_of_the_first_positional_argument, # only 2 spaces on the indent - variable_name_of_the_second_positional_argument # same here -) -``` - -Do instead: -```py -def an_example_function_definition_that_is_kinda_long( - variable_name_of_the_first_positional_argument, - variable_name_of_the_second_positional_argument -) -``` - -### Bracket and Item Arrangement -In the case where items contained in brackets need to be broken across multiple lines, items should be dropped to a new line after the opening bracket with an additional level of indentation. -The closing bracket ends on it's own new line, on the same indentation level as the opening bracket. - -Avoid doing: -```py -def long_function_name_that_is_taking_up_too_much_space(var_one, var_two, var_three, # didn't drop a line after the brackets - var_four, var_five, var_six, - var_seven, var_eight): - print(var_one) -``` -```py -def long_function_name_that_is_taking_up_too_much_space( - var_one, - var_two, - var_three, - var_four, - var_five, - var_six, - var_seven, - var_eight): # didn't drop the closing bracket to a new line - print(var_one) -``` - -Instead the correct style is: -```py -def long_function_name_that_is_taking_up_too_much_space( - var_one, - var_two, - var_three, - var_four, - var_five, - var_six, - var_seven, - var_eight -): - print(var_one) -``` - -## Imports -Our projects require correctly ordering imports based on the pycharm import order rules. -If you use Pycharm as your main IDE, you can also use the `CTRL+ALT+O` shortcut to automatically reorder your imports to the correct style. - -There's three groups of imports which are defined in the following order: - -- Standard library -- 3rd party -- Local - -Each group must be ordered alphabetically, with uppercase modules coming before lowercase. -```py -from packagename import A, Z, c, e -``` - -Direct imports must be distinct, so you cannot do: -```py -import os, sys -``` -Instead do: -```py -import os -import sys -``` - -Absolute referencing for local project modules are preferenced over relative imports. - -Wildcard imports should be avoided. - -# Strings -## Quote Marks -Preference is to use double-quotes (`"`) wherever possible. -Single quotes should only be used for cases where it is logical. -Exceptions might include: - -- using a key string within an f-string: `f"Today is {data['day']}"`. -- using double quotes within a string: `'She said "oh dear" in response'` - -Docstrings must use triple double quotes (`"""`). - -## Docstrings -All public methods and functions should have docstrings defined. - -### Line Structure -Single-line docstrings can have the quotes on the same line: -```py -def add(a, b): - """Add two arguments together.""" - return a + b -``` - -Docstrings that require multiple lines instead keep both sets of triple quotes on their own lines: -```py -def exponent(base, exponent=2): - """ - Calculate the base raised to the exponents power. - - Default is 2 due to a squared base being the most common usage at this time. - """ - return a ** b -``` - -### Spacing -Functions and methods should not have an extra empty newline after the docstring. -```py -def greeting(name): - """Build a greeting string using the given name.""" - return f"Welcome, {name}" -``` - -Class docstrings do require an extra newline. -```py -class SecretStuffCog(commands.Cog): - """Handle the secret commands that must never been known.""" - - def __init__(self, bot): - ... -``` - -### Mood -Imperative mood and present tense usage is preferenced when writing docstrings. - -Imperative mood is a certain grammatical form of writing that expresses a clear command to do something. - -**Use:** "Build an information embed."<br> -**Don't use:** "Returns an embed containing information." - -Present tense defines that the work being done is now, in the present, rather than in the past or future. - -**Use:** "Build an information embed."<br> -**Don't use:** "Built an information embed." or "Will build an information embed." - -# Type Hinting -Functions are required to have type annotations as per the style defined in [PEP 484](https://www.python.org/dev/peps/pep-0484/). Type hints are recognized by most modern code editing tools and provide useful insight into both the input and output types of a function, preventing the user from having to go through the codebase to determine these types. - -A function with type hints looks like: -```python -def foo(input_1: int, input_2: dict[str, int]) -> bool: - ... -``` -This tells us that `foo` accepts an `int` and a `dict`, with `str` keys and `int` values, and returns a `bool`. - -In previous examples, we have purposely omitted annotations to keep focus on the specific points they represent. diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git.md deleted file mode 100644 index 59c57859..00000000 --- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: Working with Git -description: Basic workflows when using git. -icon: fab fa-git-alt ---- - -Working with git can be daunting, but it is a powerful tool for collaboration and version control. -Below are links to regular workflows for working with Git using PyCharm or the CLI. - -> **What's the difference?**<br> -> The integrated Git tool built into PyCharm offers a more visual and abstract way to use Git to manage your files.<br> -> However, the CLI offers more minute control and functionality compared to the GUI, which may not always do exactly what you want. - -* [**Working with Git in PyCharm**](./pycharm) -* [**Working with the Git CLI**](./cli) - ---- - -**Resources to learn Git** - -* [The Git Book](https://git-scm.com/book) -* [Corey Schafer's YouTube tutorials](https://www.youtube.com/watch?v=HVsySz-h9r4&list=PL-osiE80TeTuRUfjRe54Eea17-YfnOOAx) -* [GitHub Git resources portal](https://try.github.io/) -* [Git cheatsheet](https://education.github.com/git-cheat-sheet-education.pdf) -* [Learn Git branching](https://learngitbranching.js.org) diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/_info.yml b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/_info.yml deleted file mode 100644 index 68ef3fd6..00000000 --- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/_info.yml +++ /dev/null @@ -1,3 +0,0 @@ -title: Working with Git -description: Basic workflows when using git. -icon: fab fa-git-alt diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/cli.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/cli.md deleted file mode 100644 index 94f94d57..00000000 --- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/cli.md +++ /dev/null @@ -1,121 +0,0 @@ ---- -title: Working with the Git CLI -description: Basic workflow when using the git CLI. -toc: 2 ---- - -This is the basic workflow when working with Git with CLI. For the PyCharm version of the guide, [**click here**](../pycharm). -The following will use the [Sir-Lancebot](https://github.com/python-discord/sir-lancebot/) repository as an example, but the steps are the same for all other repositories. - -> **Note:** This is a guide only meant to get you started with git. For in-depth resources, check the [**Working with Git**](..) page. - ---- - -## Adding the Upstream Remote -Adding a *remote* to the main GitHub repository you forked off will allow you to later update your fork with changes from the main repository. - -Generally, a *remote* designates a repository that is on GitHub or another external location rather than on your computer. -The `origin` remote will refer to your fork on GitHub. The `upstream` remote will refer to the main repository on GitHub. -```sh -$ git remote add upstream https://github.com/python-discord/sir-lancebot.git -``` -If you use SSH, use `[email protected]:python-discord/sir-lancebot.git` for the upstream URL instead. - ---- - -## Creating a New Branch -You will be committing your changes to a new branch rather than to `main`. -Using branches allows you to work on muiltiple pull requests without conflicts. - -You can name your branch whatever you want, but it's recommended to name it something succinct and relevant to the changes you will be making. - -Run the following commands to create a new branch. Replace `branch_name` with the name you wish to give your branch. -```sh -$ git fetch --all -... -$ git checkout --no-track -b branch_name upstream/main -``` - ---- - -## Staging Changes -Files in git can be in one of four different states: - -- *Staged*: These files have been modified and will be committed. -- *Unstaged*: These files were already present but have been modified. -- *Untracked*: These files are new to the repository. -- *Ignored*: Specified in a `.gitignore` file in the project root, these files will never be committed, remaining only on your computer. - -As you can see, only staged files will end up being committed. -You can get an overview of this using `git status`. -If you wish to commit unstaged or untracked files, you will need to add them with `git add` first. -```sh -# Add files individually -$ git add path/to/file.py path/to/other/file.py - -# Add all unstaged and untracked files in a directory -$ git add path/to/directory - -# Add all unstaged and untracked files in the project -$ git add . - -# Add all tracked and modified files in the project -$ git add -u - -# Unstage a file -$ git reset -- path/to/file.py -``` - ---- - -## Discarding Changes -Be careful, these operations are **irreversible**! -```sh -# Discard changes to an unstaged file -$ git checkout -- path/to/file.py - -# Discard ALL uncommitted changes -$ git reset --hard HEAD -``` - ---- - -## Committing Changes -The basic command for committing staged changes is `git commit`. All commits must have a message attached to them. -```sh -# Commit staged changes and open your default editor to write the commit message -$ git commit - -# Specify the message directly -$ git commit -m "Turn pride avatar into an embed" - -# Commit all staged and unstaged changes. This will NOT commit untracked files -$ git commit -a -m "Update d.py documentation link" -``` - ---- - -## Pushing Commits -Commits remain local (ie. only on your computer) until they are pushed to the remote repository (ie. GitHub). - -The first time you push on your new branch, you'll need to set the upstream when you push: -```sh -$ git push -u origin branch_name -``` -Any subsequent pushes can be done with just `git push`. - ---- - -## Pulling Changes -Sometimes you want to update your repository with changes from GitHub. -This could be the case if you were working on the pull request on two different computers and one of them has an outdated local repository. - -You can pull the changes from GitHub with: -```sh -$ git pull -``` -You can also pull changes from other branches such as from branch `main` in `upstream`: -```sh -$ git pull upstream main -``` -This should generally only be needed if there are [merge conflicts](https://help.github.com/en/articles/about-merge-conflicts) that you need to resolve manually. Conflicts arise when you change the same code that someone else has changed and pushed since you last updated your local repository. diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/pycharm.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/pycharm.md deleted file mode 100644 index e0b2e33c..00000000 --- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/working-with-git/pycharm.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: Working with Git in PyCharm -description: Basic workflow when using git in PyCharm. -toc: 2 ---- - -This is the basic workflow when working with Git with PyCharm. For the CLI version of the guide, [**click here**](../cli). -The following will use the [Sir-Lancebot](https://github.com/python-discord/sir-lancebot/) repository as an example, but the steps are the same for all other repositories. - -> **Note:** This is a guide only meant to get you started with git. For in-depth resources, check the [**Working with Git**](wiki:/contributing/working-with-git/) page. - ---- - -## Adding the Upstream Remote -> Adding a *remote* to the main GitHub repository you forked off will allow you to later update your fork with changes from the main repository. - -> Generally, a *remote* designates a repository that is on GitHub or another external location rather than on your computer. The `origin` remote will refer to your fork on GitHub. The `upstream` remote will refer to the main repository on GitHub. - -1. In the menu bar, navigate to `Git` -> `Remotes...`.<br> - -2. In the popup menu, click the `+` icon, set `upstream` as the name, set the URL as the URL for the main repository on GitHub.<br> - -3. Click `OK`. - ---- - -## Creating a New Branch -> You will be committing your changes to a new branch rather than to `main`. Using branches allows you to work on multiple pull requests at the same time without conflicts. - -> You can name your branch whatever you want, but it's recommended to name it something succinct and relevant to the changes you will be making. - -> Before making new branches, be sure to checkout the `main` branch and ensure it's up to date. - -1. In the bottom right corner, click on `main` and then click `New Branch`.<br> - - ---- - -## Committing Changes -After making changes to the project files, you can commit by clicking the commit button that's part of the Git actions available in the top right corner of your workspace: - - - -The flow of making a commit is as follows: - -1. Select the files you wish to commit. -2. Write a brief description of what your commit is. This is your *commit message*. -3. See the actual changes your commit will be making, and optionally tick/untick specific changes to only commit the changes you want. -4. Click `Commit`.<br> - - ---- - -## Pushing Changes -When you are ready to have your commits be available in your remote fork, navigate to `Git` -> `Push...`. -Select the commits you want to push, make sure the remote branch is your intended branch to push to, and click `Push`. - - - ---- - -## Pulling Changes -> Sometimes you want to update your repository with changes from GitHub. This could be the case if you were working on the pull request on two different computers and one of them has an outdated local repository. - -To do that, navigate to `Git` -> `Pull...`. From there, select the *remote* and the branches to pull from, then click `Pull`. - - diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/help-channel-guide.md b/pydis_site/apps/content/resources/guides/pydis-guides/help-channel-guide.md deleted file mode 100644 index bef2df9b..00000000 --- a/pydis_site/apps/content/resources/guides/pydis-guides/help-channel-guide.md +++ /dev/null @@ -1,105 +0,0 @@ ---- -title: Help Channels -description: How do help channels work in the Python Discord community? -icon: fab fa-discord -relevant_links: - Asking Good Questions: ../asking-good-questions - Role Guide: /pages/server-info/roles - Helping Others: ../helping-others -toc: 3 ---- - -At Python Discord we have two different kinds of help channels: **topical help channels** and **help forum posts**. - -# Topical Help Channels - -In topical channels, users can ask for help regarding specific domains or areas of Python. -These channels also sometimes attract domain experts. For example, `#async-and-concurrency` has CPython contributors who helped write asyncio, and in `#game-development` you can find the creators and maintainers of several game frameworks. -If your question fits into the domain of one of our topical help channels, and if you're not in a big hurry, then this is probably the best place to ask for help. - - - -Some of the topical help channels have a broad scope, so they can cover many related topics. -For example, `#data-science-and-ai` covers scientific Python, statistics, and machine learning, while `#algos-and-data-structs` covers everything from data structures and algorithms to maths. - -Each channel on the server has a channel description which briefly describes the topics covered by that channel. If you're not sure where to post, feel free to ask us which channel is appropriate in `#community-meta`. - -# Help Forum Posts - -Help forum posts can be used for all Python-related help, and have the advantage of attracting a more diverse spectrum of helpers. There is also the added benefit of receiving individual focus and attention on your question. These posts are a great choice for generic Python help, but can be used for domain-specific Python help as well. - -## How to Create A New Post - -There are 4 easy needed steps to make this happen - -1. Navigate to the **Python Help System** category.<br> - -2. Open the **python-help** forum channel. -3. Click on the **New Post** button in the top-right corner.<br> - -4. Choose a brief title that best describes your issue, along with a message explaining it more in details, and **post** it. -Note that you can also choose one or more tags which can help attract experts of that tag easily.<br> - - -Be sure to [ask questions with enough information](../asking-good-questions) in order to give yourself the best chances of getting help! - -At this point you will have the **Help Cooldown** role which will remain on your profile until you close your newly created post. This ensures that users can only have one post at any given time, giving everyone a chance to have their question seen. - -# Frequently Asked Questions - -### I created a new help post, what happens now? -Once you click on `Post`, these events take place:<br> -1. A new channel will be created for you, and you'll have an `OP` next to you username, which tells people you're the `Original Poster`, or in other words, the owner of the help topic in that channel.<br> -2. Your original question/message will always be the first one in that channel.<br> -3. Our Python bot will send a message reminding you of what you should include in your question/message in case you could have missed anything.<br> -4. People will be able to jump on that channel, and you can have a discussion with anyone who's volunteering to help you by asking as many followup questions as you want.<br> - -#### Example -Suppose we're trying to find the minimum value in a list of integers. -Once we've chosen our title and message content, we are ready to make a new post.<br><br> -<br><br> -Note how we've checked the **Algos & data structs** tag here, whose circumference is highlighted in blue, since this is a question about an algorithm to find the minimum.<br> -This will greatly help others pinpoint where they can help you best based on a combination of your title and tag from a first glance.<br><br> -Once you click on post, a new channel is created, and you can see the original message on top along with the `OP` tag next to the poster's avatar.<br> -You will also see the message that our Python bot sends instantly right after yours.<br><br> - - -### How long does my help post stay active? - -The post remains open for **30 minutes** after your last message, or 10 minutes after the last message sent by another user (whichever time comes later). - - -*You'll see this message in your post once it goes dormant.* - -### No one answered my question. How come? - -The server has users active all over the world and all hours of the day, but some time periods are less active than others. It's also possible that the users that read your question didn't have the knowledge required to help you. If no one responded, feel free to open another post a little later, or try an appropriate topical channel. - -If you feel like your question is continuously being overlooked, read our guide on [asking good questions](../asking-good-questions) to increase your chances of getting a response. - -### My question was answered. What do I do? - -Go ahead and use one of the `!close` or `!solved` commands if you've satisfactorily solved your problem. You will only be able to run this command in your own post, and no one (outside of staff) will be able to close your post for you. - -Closing your post once you are finished leads to less occupied ones, which means more attention can be given to other users that still need help. - -### Can only Helpers answer help questions? - -Definitely not! We encourage all members of the community to participate in giving help. If you'd like to help answer some questions, you can either browse all posts in the **python-help** forum channel or head over to the **Topical Chat/Help** category. - -Before jumping in, please read our guide on [helping others](../helping-others) which explains our expectations for the culture and quailty of help that we aim for on the server. - -Tip: run the `!helpdm on` command in the `#bot-commands` channel to get notified via DM with jumplinks to help posts you're participating in. - -### Can I save my help session for future reference? - -Yes! Because the help posts are only closed without being deleted, this means you can always refer to a previous help session if you found one particularly helpful. - -Tip: reply to a message and run the `.bm` command to get bookmarks sent to you via DM for future reference. - -### I lost my help post! - -No need to panic. Your post was probably just closed due to inactivity. -All the dormant help posts are still available at the bottom of the **python-help** forum channel and also through search in the **Python Help System** category. -If you're not sure what the title of your help post was, you can easily find it by using the Discord Search feature. -Try searching for `from:<your nickname>` to find the last messages sent by yourself, and from there you will be able to jump directly into the channel by pressing the Jump button on your message. diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/helping-others.md b/pydis_site/apps/content/resources/guides/pydis-guides/helping-others.md deleted file mode 100644 index 9f0d947f..00000000 --- a/pydis_site/apps/content/resources/guides/pydis-guides/helping-others.md +++ /dev/null @@ -1,140 +0,0 @@ ---- -title: Helping Others -description: The staff's take on how to help others in our community. -icon: fab fa-discord -relevant_links: - Asking Good Questions: ../asking-good-questions - Help Channel Guide: ../help-channel-guide - Code of Conduct: /pages/code-of-conduct/ -toc: 2 ---- - -Python Discord has a lot of people asking questions, be it in the help forum, topical channels, or any other part of the server. -Therefore, you might sometimes want to give people the answers you have in mind. -But you might not be sure how best to approach the issue, or maybe you'd like to see how others handle it. -This article aims to present a few of the general principles which guide the staff on a day-to-day basis on the server. - -## Understanding the Problem - -Some people are good at asking for help. -They might be able to present their problem accurately and concisely, in which case discussing the problem and giving tips is simple and straight-forward. -But not everyone might be able to do that for their current issue. -Maybe they just don't know what they don't know. - -If you feel like there's a gap in your understanding of the problem, it's often a good first step to query the asker for more information. Some of this information might be: - -* More code -* The way in which the code doesn't work (if that is the issue), be it an exception message with traceback, undesired output, etc. -* You can sometimes infer what the problem is yourself by requesting short examples of the desired output for specific input. - -At this point, it's probably better being safe than sorry. -You don't want to accidentally pursue a direction that isn't even related to the real issue, as it could lead to frustration on both sides. -Beginners especially can be prone to asking a question which presents their attempt at solving the problem, instead of presenting the problem itself. -This is often called an [XY problem](https://xyproblem.info/). - -Even if eventually you can't help, simply clarifying what the problem is can help others join in, and give their input. - -> #### Example 1: -> A person might ask: *"How do I look at the values inside my function from outside?"* -> -> What they might be asking is: *"How do I return a value from a function?"* - - -## Understanding the Helpee - -Assuming you know what the problem is, it's vital to gauge the level of knowledge of the person asking the question. -There's a stark difference between explaining how to do something to someone experienced, who only lacks knowledge on the specific topic; and someone who is still learning the basics of the language. - -Try adjusting the solutions you present and their complexity accordingly. -Teaching new concepts allows the helpee to learn, but presenting too complex of a solution might overwhelm them, and the help session might not achieve its purpose. - -> #### Example 2: -> A user might ask how to count how often each word appears in a given text. -> You might lean towards solving it using `collections.Counter`, but is it really the right approach? -> If the user doesn't know yet how to update and access a dictionary, it might be better to start there. - -Generally, you should consider what approach will bring the most value to the person seeking help, instead of what is the most optimal, or "right" solution. - -Usually, understanding a person's level can be achieved after a short conversation (such as trying to understand the problem), or simply by seeing the person's code and what they need help with. -At other times, it might not be as obvious, and it might be a good idea to kindly inquire about the person's experience with the language and programming in general. - - -## Teach a Man to Fish... - -The path is often more important than the answer. -Your goal should primarily be to allow the helpee to apply, at least to a degree, the concepts you introduce in your answer. -Otherwise, they might keep struggling with the same problem over and over again. -That means that simply showing your answer might close the help post for the moment, but won't be very helpful in the long-term. - -A common approach is to walk the helpee through to an answer: - -* Break the task into smaller parts that will be easier to handle, and present them step by step. - Try to think of the order of the steps you yourself would take to reach the solution, and the concepts they need to understand for each of those steps. - If one step requires the helpee to understand several new concepts, break it down further. -* Ask instructive questions that might help the person think in the right direction. - -> #### Example 3: -> -> **user**: "Hey, how can I create a sudoku solver?"<br> -> *helper1 proceeds to paste 40 lines of sudoku solving code*<br> -> **helper2**: "Are you familiar with lists / recursion / backtracking?"<br> -> *helper2 proceeds to give the information the user lacks* -> -> With the first replier, there's a much smaller chance of the helpee understanding how the problem was solved, and gaining new tools for future projects. -> It's much more helpful in the long run to explain the new concepts or guide them to resources where they can learn. -> -> This is also an example of gauging the level of the person you're talking to. -> You can't properly help if you don't know what they already learned. -> If they don't know recursion, it's going to take a slower, more detailed approach if you try to explain backtracking to them. -> Likewise if they're not familiar with lists. -> -> The same applies to presenting external resources. -> If they only just started programming, pasting a link to the Python documentation is going to be of little help. -> They are unlikely to be able to get around that website and understand what you expect them to read. In contrast, for a more seasoned programmer, the docs might be just what they need. - - -## Add a Grain of Salt - -Giving people more information is generally a good thing, but it's important to remember that the person helped is trying to soak in as much as they can. -Too much irrelevant information can confuse them, and make them lose sight of the actual solution. -Presenting a solution that is considered a bad practice might be useful in certain situations, but it's important to make sure they are aware it's not a good solution, so they don't start using it themselves. - -> #### Example 4: -> -> **user**: "How can I print all elements in a list?"<br> -> **helper1**: "You can do it like so:"<br> -> -> for element in your_list: -> print(element) -> -> **helper2**: "You can also do it like this:"<br> -> -> for i in range(len(your_list)): -> print(your_list[i]) -> -> The second replier gave a valid solution, but it's important that he clarifies that it is considered a bad practice in Python, and that the first solution should usually be used in this case. - - -## It's OK to Step Away - -Sometimes you might discover you don't have the best chemistry with the person you're talking to. -Maybe there's a language barrier you don't manage to overcome. -In other cases, you might find yourself getting impatient or sarcastic, maybe because you already answered the question being asked three times in the past hour. - -That's OK- remember you can step away at any time and let others take over. -You're here helping others on your own free time (and we really appreciate it!), and have no quotas to fill. - -At other times, you might start talking with someone and realize you're not experienced in the topic they need help with. -There's nothing wrong with admitting you lack the specific knowledge required in this case, and wishing them good luck. -We can't know everything. - -Remember that helping means thinking of what's best for them, and we also wouldn't want to see you become agitated. -We're all here because we enjoy doing this. - - -## Respect Others Giving Help - -You might sometimes see others giving help and guiding others to an answer. -Throwing in additional ideas is great, but please remember both teaching and learning takes concentration, and you stepping in might break it. -You might have another idea to suggest, but you see that there's already a person helping, and that they're handling the situation. -In that case, it might be a good idea to patiently observe, and wait for a good opportunity to join in so as to not be disruptive. diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/how-to-contribute-a-page.md b/pydis_site/apps/content/resources/guides/pydis-guides/how-to-contribute-a-page.md deleted file mode 100644 index 0d14ef41..00000000 --- a/pydis_site/apps/content/resources/guides/pydis-guides/how-to-contribute-a-page.md +++ /dev/null @@ -1,258 +0,0 @@ ---- -title: How to Contribute a Page -description: Learn how to write and publish a page to this website. -icon: fas fa-info -relevant_links: - Contributing to Site: https://pythondiscord.com/pages/contributing/site/ - Using Git: https://pythondiscord.com/pages/contributing/working-with-git/ -toc: 4 ---- - -Pages, which include guides, articles, and other static content, are stored in markdown files in the `site` repository on Github. -If you are interested in writing or modifying pages seen here on the site, follow the steps below. - -For further assistance and help with contributing pages, send a message to the `#dev-contrib` channel in the Discord server! - -## Prerequisites -Before working on a new page, you have to [setup the site project locally](https://pythondiscord.com/pages/contributing/site/). -It is also a good idea to familiarize yourself with the [git workflow](https://pythondiscord.com/pages/contributing/working-with-git/), as it is part of the contribution workflow. - -Additionally, please submit your proposed page or modification to a page as an [issue in the site repository](https://github.com/python-discord/site/issues), or discuss it in the `#dev-contrib` channel in the server. -As website changes require staff approval, discussing the page content beforehand helps with accelerating the contribution process, and avoids wasted work in the event the proposed page is not accepted. - -## Creating the Page -All pages are located in the `site` repo, at the path `pydis_site/apps/content/resources/`. This is the root folder, which corresponds to the URL `www.pythondiscord.com/pages/`. - -For example, the file `pydis_site/apps/content/resources/hello-world.md` will result in a page available at `www.pythondiscord.com/pages/hello-world`. - -#### Page Categories -Nested folders represent page categories on the website. Each folder under the root folder must include a `_info.yml` file with the following: - -```yml -title: Category name -description: Category description -icon: fas fa-folder # Optional -``` - -All the markdown files in this folder will then be under this category. - -#### Having the Category Also Be a Page -In order to make categories a page, just create a page **with the same name as the category folder** in the category's parent directory. - -```plaintext -guides -├── contributing.md -├── contributing -│ ├── _info.yml -│ └── bot.md -└── _info.yml -``` - -In the above example, `www.pythondiscord.com/guides/` will list `Contributing` as a category entry with information from `contributing/_info.yml`. - -However, `www.pythondiscord.com/guides/contributing` will render `contributing.md` rather than show the category contents. -A dropdown menu will be automatically generated in the top right corner of the page listing the children of the category page. - -Therefore, `www.pythondiscord.com/guides/contributing/bot` will then render `bot.md`, with backlinks to `contributing.md`. - -## Writing the Page -Files representing pages are in `.md` (Markdown) format, with all-lowercase filenames and spaces replaced with `-` characters. - -Each page must include required metadata, and optionally additional metadata to modify the appearance of the page. -The metadata is written in YAML, and should be enclosed in triple dashes `---` *at the top of the markdown file*. - -**Example:** -```yaml ---- -title: How to Contribute a Page -description: Learn how to write and publish a page to this website. -icon: fas fa-info -relevant_links: - Contributing to Site: https://pythondiscord.com/pages/contributing/site/ - Using Git: https://pythondiscord.com/pages/contributing/working-with-git/ ---- - -Pages, which include guides, articles, and other static content,... -``` - -### Required Fields -- **title:** Easily-readable title for your article. -- **description:** Short, 1-2 line description of the page's content. - -### Optional Fields -- **icon:** Icon for the category entry for the page. Default: `fab fa-python` <i class="fab fa-python is-black" aria-hidden="true"></i> -- **relevant_links:** A YAML dictionary containing `text:link` pairs. See the example above. -- **toc:** A number representing the smallest heading tag to show in the table of contents. - See: [Table of Contents](#table-of-contents) - -## Extended Markdown - -Apart from standard Markdown, certain additions are available: - -### Abbreviations -HTML `<abbr>` tags can be used in markdown using this format: - -**Markdown:** -```nohighlight -This website is HTML generated from YAML and Markdown. - -*[HTML]: Hyper Text Markup Language -*[YAML]: YAML Ain't Markup Language -``` - -**Output:** - -This website is <abbr title="Hyper Text Markup Language">HTML</abbr> -generated from <abbr title="YAML Ain't Markup Language">YAML</abbr> and Markdown. - ---- - -### Footnotes -**Markdown:** -```nohighlight -This footnote[^1] links to the bottom[^custom_label] of the page[^3]. - -[^1]: Footnote labels start with a caret `^`. -[^3]: The footnote link is numbered based on the order of the labels. -[^custom label]: Footnote labels can contain any text within square brackets. -``` - -**Output:** - -This footnote[^1] links to the bottom[^custom label] of the page[^3]. - -[^1]: Footnote labels start with a caret `^`. -[^3]: The footnote link is numbered based on the order of the labels. -[^custom label]: Footnote labels can contain any text within square brackets. - ---- - -### Tables - -**Markdown:** -```nohighlight -| This is header | This is another header | -| -------------- | ---------------------- | -| An item | Another item | -``` - -**Output:** - -| This is header | This is another header | -| -------------- | ---------------------- | -| An item | Another item | - ---- - -### Codeblock Syntax Highlighting -Syntax highlighting is provided by `highlight.js`. -To activate syntax highlighting, put the language directly after the starting backticks. - -**Markdown:** -````nohighlight -```python -import os - -path = os.path.join("foo", "bar") -``` -```` - -**Output:** -```python -import os - -path = os.path.join("foo", "bar") -``` - ---- - -### HTML Attributes -To add HTML attributes to certain lines/paragraphs, [see this page](https://python-markdown.github.io/extensions/attr_list/#the-list) for the format and where to put it. - -This can be useful for setting the image size when adding an image using markdown (see the [Image Captions](#image-captions) section for an example), or for adding bulma styles to certain elements (like the warning notification [here](/pages/guides/pydis-guides/contributing/sir-lancebot#run-with-docker)).<br> -**This should be used sparingly, as it reduces readability and simplicity of the article.** - ---- - -### Image Captions -To add an image caption, place a sentence with italics *right below* the image link - -**Markdown:** -```nohighlight -{: width="400" } -*Summmer Code Jam 2020 banner with event information.* -``` - -**Output:** - -{: width="400"} -*Summer Code Jam 2020 banner with event information.* - -> Note: To display a regular italicized line below an image, leave an empty line between the two. - ---- - -### Table of Contents -In order to show the table of contents on a page, simply define the `toc` key in the page metadata. - -The value of the `toc` key corresponds to the smallest heading to list in the table of contents. -For example, with markdown content like this: - -```markdown -# Header 1 -words -### Header 3 -more words -# Another Header 1 -## Header 2 -even more words -``` - -and `toc: 2` in the page metadata, only `Header 1`, `Another Header 1` and `Header 2` will be listed in the table of contents. - -To use a custom label in the table of contents for a heading, set the `data-toc-label` attribute in the heading line. See [HTML Attributes](#html-attributes) for more information. - -```markdown -# Header 1 {: data-toc-label="Header One" } -``` - -## Tips - -### Nested/Unhighlighted Code Blocks -To nest code blocks, increase the number of ticks in the outer block by 1. To remove highlighting from code blocks (ie. no dark background), you can use the `nohighlight` language. -`````nohighlight -````nohighlight -```python -print("Some inner code block text.") -``` -```` -````` - -### Images in Lists -To properly indent images in lists, keep the image on the line directly after the previous line and add `<br>` to the end of the text, like this: - -```markdown -1. List item text one.<br> - - -2. List item text two.<br> - -``` - -### Keeping Text In The Same Paragraph -You can also use `<br>` to break lines while keeping them in the same paragraph (avoiding the vertical spacing added between paragraphs). - -```nohighlight -##### Same line, same paragraph -Line A -Line B - -##### Different line, different paragraph -Line A - -Line B - -##### Different line, same paragraph -Line A<br> -Line B -``` diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/off-topic-etiquette.md b/pydis_site/apps/content/resources/guides/pydis-guides/off-topic-etiquette.md deleted file mode 100644 index 5e785cd9..00000000 --- a/pydis_site/apps/content/resources/guides/pydis-guides/off-topic-etiquette.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: Conversation Etiquette in Our Off-Topic Channels -description: Guidelines on conversation etiquette. -icon: fab fa-discord ---- - -## Why do we need off-topic etiquette? -Everyone wants to have good conversations in our off-topic channels, but with hundreds of thousands of members, this might mean different things to different people. -To facilitate the best experience for everyone, here are some guidelines on conversation etiquette. - -## Three things you shouldn't do -1. Don't interrupt active conversations - * There's three off-topic channels which can support three simultaneous conversations. - If one is active and you have something you'd like to discuss, try a different channel. -2. Don't post memes unless they're relevant to a conversation - * There are better places to share memes; if you have a meme you think is worth sharing, try to find a relevant subreddit, like [/r/ProgrammerHumor](https://www.reddit.com/r/ProgrammerHumor/). -3. Don't snap at people - * We are a large, diverse community. Different native languages, experiences, and ages mean miscommunications happen. Always try to assume the best in other community members. - -## Three things you should do -1. Ask away - * If you have a question that isn't about Python, just ask it in an inactive off-topic channel. - If someone sees your question who knows the answer, they will answer you. - "Why is my wifi not working?", "how do I tune a guitar?", "is there a server for C#?", are all fair game for questions to ask. - If your question relates to Python, try to find the most suitable channel to ask your question, or open a help session. -2. When in doubt, ask someone to clarify what they mean - * If you're not sure you properly understand someone, ask them to clarify. - Text isn't necessarily the easiest way for everyone to communicate, so it makes life easier if we're all on the same page. -3. Join in! - * The off-topic channels have lots of fun or interesting conversations; if someone is talking about something you're interested in, don't be scared to hop into the conversation. - -While you can discuss other topics than Python in the off-topic channels, the [ordinary rules](/pages/rules/) still apply. diff --git a/pydis_site/apps/content/resources/guides/python-guides/_info.yml b/pydis_site/apps/content/resources/guides/python-guides/_info.yml deleted file mode 100644 index 67730962..00000000 --- a/pydis_site/apps/content/resources/guides/python-guides/_info.yml +++ /dev/null @@ -1,3 +0,0 @@ -title: Python Guides -description: Guides related to the Python Programming Language. -icon: fab fa-python diff --git a/pydis_site/apps/content/resources/guides/python-guides/app-commands.md b/pydis_site/apps/content/resources/guides/python-guides/app-commands.md deleted file mode 100644 index 713cd650..00000000 --- a/pydis_site/apps/content/resources/guides/python-guides/app-commands.md +++ /dev/null @@ -1,418 +0,0 @@ ---- -title: Discord.py 2.0 changes -description: Changes and new features in version 2.0 of discord.py ---- - -Upon the return of the most popular discord API wrapper library for Python, `discord.py`, while catching on to the latest features of the discord API, there have been numerous changes with additions of features to the library. Additions to the library include support for Buttons, Select Menus, Forms (AKA Modals), Slash Commands (AKA Application Commands) and a bunch more handy features! All the changes can be found [here](https://discordpy.readthedocs.io/en/latest/migrating.html). Original discord.py Gist regarding resumption can be found [here](https://gist.github.com/Rapptz/c4324f17a80c94776832430007ad40e6). - - -# Install the latest version of discord.py - -Before you can make use of any of the new 2.0 features, you need to install the latest version of discord.py. Make sure that the version is 2.0 or above! -Also, make sure to uninstall any third party libraries intended to add slash-command support to pre-2.0 discord.py, as they are no longer necessary and will likely cause issues. - -The latest and most up-to-date stable discord.py version can be installed using `pip install -U discord.py`. - -**Before migrating to discord.py 2.0, make sure you read the migration guide [here](https://discordpy.readthedocs.io/en/latest/migrating.html) as there are lots of breaking changes.**. -{: .notification .is-warning } - -# What are Slash Commands? - -Slash Commands are an exciting new way to build and interact with bots on Discord. As soon as you type "/", you can easily see all the commands a bot has. It also comes with autocomplete, validation and error handling, which will all help users of your bot get the command right the first time. - -# Basic structure for discord.py Slash Commands! - -### Note that Slash Commands in discord.py are also referred to as **Application Commmands** and **App Commands** and every *interaction* is a *webhook*. -Slash commands in discord.py are held by a container, [CommandTree](https://discordpy.readthedocs.io/en/latest/interactions/api.html?highlight=commandtree#discord.app_commands.CommandTree). A command tree is required to create Slash Commands in discord.py. This command tree provides a `command` method which decorates an asynchronous function indicating to discord.py that the decorated function is intended to be a slash command. This asynchronous function expects a default argument which acts as the interaction which took place that invoked the slash command. This default argument is an instance of the **Interaction** class from discord.py. Further up, the command logic takes over the behaviour of the slash command. - -# Fundamentals for this Gist! - -One new feature added in discord.py v2 is `setup_hook`. `setup_hook` is a special asynchronous method of the Client and Bot classes which can be overwritten to perform numerous tasks. This method is safe to use as it is always triggered before any events are dispatched, i.e. this method is triggered before the *IDENTIFY* payload is sent to the discord gateway. -Note that methods of the Bot class such as `change_presence` will not work in setup_hook as the current application does not have an active connection to the gateway at this point. -A full list of commands you can't use in setup_hook can be found [here](https://discord.com/developers/docs/topics/gateway-events#send-events). - -__**THE FOLLOWING ARE EXAMPLES OF HOW A `SETUP_HOOK` FUNCTION CAN BE DEFINED**__ - -Note that the default intents are defined [here](https://discordpy.readthedocs.io/en/stable/api.html?highlight=discord%20intents%20default#discord.Intents.default) to have all intents enabled except presences, members, and message_content. - -```python -import discord - -# You can create the setup_hook directly in the class definition - -class SlashClient(discord.Client): - def __init__(self) -> None: - super().__init__(intents=discord.Intents.default()) - - async def setup_hook(self) -> None: - ... - -# Or add it to the client after creating it - -client = discord.Client(intents=discord.Intents.default()) -async def my_setup_hook() -> None: - ... - -client.setup_hook = my_setup_hook -``` - -# Basic Slash Command application using discord.py. - -#### The `CommandTree` class resides within the `app_commands` of the discord.py package. - -## Slash Command Application with a Client - -```python -import discord - -class SlashClient(discord.Client): - def __init__(self) -> None: - super().__init__(intents=discord.Intents.default()) - self.tree = discord.app_commands.CommandTree(self) - - async def setup_hook(self) -> None: - self.tree.copy_global_to(guild=discord.Object(id=12345678900987654)) - await self.tree.sync() - -client = SlashClient() - [email protected](name="ping", description="...") -async def _ping(interaction: discord.Interaction) -> None: - await interaction.response.send_message("pong") - -client.run("token") -``` - - -__**EXPLANATION**__ - -- `import discord` imports the **discord.py** package. -- `class SlashClient(discord.Client)` is a class subclassing **Client**. Though there is no particular reason except readability to subclass the **Client** class, using the `Client.setup_hook = my_func` is equally valid. -- Next up `super().__init__(...)` runs the `__init__` function of the **Client** class, this is equivalent to `discord.Client(...)`. Then, `self.tree = discord.app_commands.CommandTree(self)` creates a CommandTree which acts as the container for slash commands. -- Then in the `setup_hook`, `self.tree.copy_global_to(...)` adds the slash command to the guild of which the ID is provided as a `discord.Object` object. **Essential to creation of commands** Further up, `self.tree.sync()` updates the API with any changes to the Slash Commands. -- Finishing up with the **Client** subclass, we create an instance of the subclassed Client class which here has been named as `SlashClient` with `client = SlashClient()`. -- Then using the `command` method of the `CommandTree` we decorate a function with it as `client.tree` is an instance of `CommandTree` for the current application. The command function takes a default argument as said, which acts as the interaction that took place. Catching up is `await interaction.response.send_message("pong")` which sends back a message to the slash command invoker. -- And the classic old `client.run("token")` is used to connect the client to the discord gateway. -- Note that the `send_message` is a method of the `InteractionResponse` class and `interaction.response` in this case is an instance of the `InteractionResponse` object. The `send_message` method will not function if the response is not sent within 3 seconds of command invocation. We will discuss how to handle this issue later following the Gist. - -## Slash Command application with the Bot class - -```python -import discord - -class SlashBot(commands.Bot): - def __init__(self) -> None: - super().__init__(command_prefix=".", intents=discord.Intents.default()) - - async def setup_hook(self) -> None: - self.tree.copy_global_to(guild=discord.Object(id=12345678900987654)) - await self.tree.sync() - -bot = SlashBot() - [email protected](name="ping", description="...") -async def _ping(interaction: discord.Interaction) -> None: - await interaction.response.send_message("pong") - -bot.run("token") -``` - -The above example shows a basic Slash Commands within discord.py using the Bot class. - -__**EXPLANATION**__ - -Most of the explanation is the same as the prior example that featured `SlashClient` which was a subclass of **discord.Client**. Though some minor changes are discussed below. - -- The `SlashBot` class now subclasses `discord.ext.commands.Bot` following the passing in of the required arguments to its `__init__` method. -- `discord.ext.commands.Bot` already consists of an instance of the `CommandTree` class which can be accessed using the `tree` property. - -# Slash Commands within a Cog! - -A cog is a collection of commands, listeners, and optional state to help group commands together. More information on them can be found on the [Cogs](https://discordpy.readthedocs.io/en/latest/ext/commands/cogs.html#ext-commands-cogs) page. - -## An Example to using cogs with discord.py for Slash Commands! - -```python -import discord -from discord.ext import commands -from discord import app_commands - -class MySlashCog(commands.Cog): - def __init__(self, bot: commands.Bot) -> None: - self.bot = bot - - @app_commands.command(name="ping", description="...") - async def _ping(self, interaction: discord.Interaction): - await interaction.response.send_message("pong!") - -class MySlashBot(commands.Bot): - def __init__(self) -> None: - super().__init__(command_prefix="!", intents=discord.Intents.default()) - - async def setup_hook(self) -> None: - await self.add_cog(MySlashCog(self)) - await self.tree.copy_global_to(discord.Object(id=123456789098765432)) - await self.tree.sync() - -bot = MySlashBot() - -bot.run("token") -``` - -__**EXPLANATION**__ - -- Firstly, `import discord` imports the **discord.py** package. `from discord import app_commands` imports the `app_commands` module from the **discord.py** root module. `from discord.ext import commands` imports the commands extension. -- Further up, `class MySlashCog(commands.Cog)` is a class subclassing the `Cog` class. You can read more about this [here](https://discordpy.readthedocs.io/en/latest/ext/commands/cogs.html#ext-commands-cogs). -- `def __init__(self, bot: commands.Bot): self.bot = bot` is the constructor method of the class that is always run when the class is instantiated and that is why we pass in a **Bot** object whenever we create an instance of the cog class. -- Following up is the `@app_commands.command(name="ping", description="...")` decorator. This decorator basically functions the same as a `bot.tree.command` but since the cog currently does not have a **bot**, the `app_commands.command` decorator is used instead. The next two lines follow the same structure for Slash Commands with **self** added as the first parameter to the function as it is a method of a class. -- The next up lines are mostly the same. -- Talking about the first line inside the `setup_hook` is the `add_cog` method of the **Bot** class. And since **self** acts as the **instance** of the current class, we use **self** to use the `add_cog` method of the **Bot** class as we are inside a subclassed class of the **Bot** class. Then we pass in **self** to the `add_cog` method as the `__init__` function of the **MySlashCog** cog accepts a `Bot` object. -- After that we instantiate the `MySlashBot` class and run the bot using the **run** method which executes our setup_hook function and our commands get loaded and synced. The bot is now ready to use! - -# An Example to using groups with discord.py for Slash Commands! - -## An example with optional group! - -```python -import discord -from discord.ext import commands -from discord import app_commands - -class MySlashGroupCog(commands.Cog): - def __init__(self, bot: commands.Bot) -> None: - self.bot = bot - - #-------------------------------------------------------- - group = app_commands.Group(name="uwu", description="...") - #-------------------------------------------------------- - - @app_commands.command(name="ping", description="...") - async def _ping(self, interaction: discord.) -> None: - await interaction.response.send_message("pong!") - - @group.command(name="command", description="...") - async def _cmd(self, interaction: discord.Interaction) -> None: - await interaction.response.send_message("uwu") - -class MySlashBot(commands.Bot): - def __init__(self) -> None: - super().__init__(command_prefix="!", intents=discord.Intents.default()) - - async def setup_hook(self) -> None: - await self.add_cog(MySlashGroupCog(self)) - await self.tree.copy_global_to(discord.Object(id=123456789098765432)) - await self.tree.sync() - -bot = MySlashBot() - -bot.run("token") -``` - -__**EXPLANATION**__ -- The only difference used here is `group = app_commands.Group(name="uwu", description="...")` and `group.command`. `app_commands.Group` is used to initiate a group while `group.command` registers a command under a group. For example, the ping command can be run using **/ping** but this is not the case for group commands. They are registered with the format of `group_name command_name`. So here, the **command** command of the **uwu** group would be run using **/uwu command**. Note that only group commands can have a single space between them. - -## An example with a **Group** subclass! - -```python -import discord -from discord.ext import commands -from discord import app_commands - -class MySlashGroup(app_commands.Group, name="uwu"): - def __init__(self, bot: commands.Bot) -> None: - self.bot = bot - super().__init__() - - @app_commands.command(name="ping", description="...") - async def _ping(self, interaction: discord.) -> None: - await interaction.response.send_message("pong!") - - @app_commands.command(name="command", description="...") - async def _cmd(self, interaction: discord.Interaction) -> None: - await interaction.response.send_message("uwu") - -class MySlashBot(commands.Bot): - def __init__(self) -> None: - super().__init__(command_prefix="!", intents=discord.Intents.default()) - - async def setup_hook(self) -> None: - await self.add_cog(MySlashGroup(self)) - await self.tree.copy_global_to(discord.Object(id=123456789098765432)) - await self.tree.sync() - -bot = MySlashBot() - -bot.run("token") -``` - -__**EXPLANATION**__ -- The only difference here too is that the `MySlashGroup` class directly subclasses the **Group** class from discord.app_commands which automatically registers all the methods within the group class to be commands of that specific group. So now, the commands such as `ping` can be run using **/uwu ping** and `command` using **/uwu command**. - -# Some common methods and features used for Slash Commands. - -### A common function used for Slash Commands is the `describe` function. This is used to add descriptions to the arguments of a slash command. The command function can decorated with this function. It goes by the following syntax as shown below. - -```python -from discord.ext import commands -from discord import app_commands -import discord - -bot = commands.Bot(command_prefix=".", intents=discord.Intents.default()) -#sync the commands - [email protected](name="echo", description="...") -@app_commands.describe(text="The text to send!", channel="The channel to send the message in!") -async def _echo(interaction: discord.Interaction, text: str, channel: discord.TextChannel=None): - channel = channel or interaction.channel - await channel.send(text) -``` - -### Another common issue that most people come across is the time duration of sending a message with `send_message`. This issue can be tackled by deferring the interaction response using the `defer` method of the `InteractionResponse` class. An example for fixing this issue is shown below. - -```python -import discord -from discord.ext import commands -import asyncio - -bot = commands.Bot(command_prefix="!", intents=discord.Intents.default()) -#sync the commands - [email protected](name="time", description="...") -async def _time(interaction: discord.Interaction, time_to_wait: int): - # ------------------------------------------------------------- - await interaction.response.defer(ephemeral=True) - # ------------------------------------------------------------- - await interaction.edit_original_response(content=f"I will notify you after {time_to_wait} seconds have passed!") - await asyncio.sleep(time_to_wait) - await interaction.edit_original_response(content=f"{interaction.user.mention}, {time_to_wait} seconds have already passed!") -``` - -# Checking for Permissions and Roles! - -To add a permissions check to a command, the methods are imported through `discord.app_commands.checks`. To check for a member's permissions, the function can be decorated with the [discord.app_commands.checks.has_permissions](https://discordpy.readthedocs.io/en/latest/interactions/api.html?highlight=has_permissions#discord.app_commands.checks.has_permissions) method. An example to this as follows. - -```py -from discord import app_commands -from discord.ext import commands -import discord - -bot = commands.Bot(command_prefix="!", intents=discord.Intents.default()) -#sync commands - [email protected](name="ping") -@app_commands.checks.has_permissions(manage_messages=True, manage_channels=True) #example permissions -async def _ping(interaction: discord.Interaction): - await interaction.response.send_message("pong!") - -``` - -If the check fails, it will raise a `MissingPermissions` error which can be handled within an app commands error handler! We will discuss making an error handler later in the Gist. All the permissions can be found [here](https://discordpy.readthedocs.io/en/latest/api.html?highlight=discord%20permissions#discord.Permissions). - -Other methods that you can decorate the commands with are - -- `bot_has_permissions` | This checks if the bot has the required permissions for executing the slash command. This raises a [BotMissingPermissions](https://discordpy.readthedocs.io/en/latest/interactions/api.html?highlight=app_commands%20checks%20has_role#discord.app_commands.BotMissingPermissions) exception. -- `has_role` | This checks if the slash command user has the required role or not. Only **ONE** role name or role ID can be passed to this. If the name is being passed, make sure to have the exact same name as the role name. This raises a [MissingRole](https://discordpy.readthedocs.io/en/latest/interactions/api.html?highlight=app_commands%20checks%20has_role#discord.app_commands.MissingRole) exception. -- To pass in several role names or role IDs, `has_any_role` can be used to decorate a command. This raises two exceptions -> [MissingAnyRole](https://discordpy.readthedocs.io/en/latest/interactions/api.html?highlight=app_commands%20checks%20has_role#discord.app_commands.MissingAnyRole) and [NoPrivateMessage](https://discordpy.readthedocs.io/en/latest/interactions/api.html?highlight=app_commands%20checks%20has_role#discord.app_commands.NoPrivateMessage) - - -# Adding cooldowns to Slash Commands! - -Slash Commands within discord.py can be applied cooldowns to in order to prevent spamming of the commands. This can be done through the `discord.app_commands.checks.cooldown` method which can be used to decorate a slash command function and register a cooldown to the function. This raises a [CommandOnCooldown](https://discordpy.readthedocs.io/en/latest/interactions/api.html?highlight=checks%20cooldown#discord.app_commands.CommandOnCooldown) exception if the command is currently on cooldown. -An example is as follows. - -```python -from discord.ext import commands -import discord - -class Bot(commands.Bot): - def __init__(self): - super().__init__(command_prefix="uwu", intents=discord.Intents.all()) - - async def setup_hook(self): - self.tree.copy_global_to(guild=discord.Object(id=12345678909876543)) - await self.tree.sync() - - -bot = Bot() - [email protected](name="ping") -# ----------------------------------------- [email protected]_commands.checks.cooldown(1, 30) -# ----------------------------------------- -async def ping(interaction: discord.Interaction): - await interaction.response.send_message("pong!") - -bot.run("token") -``` - -__**EXPLANATION**__ -- The first argument is the number of times this command can be invoked before the cooldown is triggered. -- The second argument it takes is the period of time in which the command can be run the specified number of times. -- The `CommandOnCooldown` exception can be handled using an error handler. We will discuss making an error handler for Slash Commands later in the Gist. - - -# Handling errors for Slash Commands! - -The Slash Commands exceptions can be handled by overwriting the `on_error` method of the `CommandTree`. The error handler takes two arguments. The first argument is the `Interaction` that took place when the error occurred and the second argument is the error that occurred when the Slash Commands was invoked. The error is an instance of [discord.app_commands.AppCommandError](https://discordpy.readthedocs.io/en/latest/interactions/api.html?highlight=appcommanderror#discord.app_commands.AppCommandError) which is a subclass of [DiscordException](https://discordpy.readthedocs.io/en/latest/api.html?highlight=discordexception#discord.DiscordException). -An example to creating an error handler for Slash Commands is as follows. - -```python -from discord.ext import commands -from discord import app_commands -import discord - -bot = commands.Bot(command_prefix="!", intents=discord.Intents.default()) -#sync commands - [email protected](name="ping") -@app_commands.checks.cooldown(1, 30) -async def ping(interaction: discord.Interaction): - await interaction.response.send_message("pong!") - -async def on_tree_error(interaction: discord.Interaction, error: app_commands.AppCommandError): - if isinstance(error, app_commands.CommandOnCooldown): - return await interaction.response.send_message(f"Command is currently on cooldown! Try again in **{error.retry_after:.2f}** seconds!") - elif isinstance(error, ...): - ... - else: - raise error - -bot.tree.on_error = on_tree_error - -bot.run("token") -``` - -__**EXPLANATION**__ - -First we create a simple asynchronous function named `on_tree_error` here. To which the first two required arguments are passed, `Interaction` which is named as `interaction` here and `AppCommandError` which is named as `error` here. Then using simple functions and keywords, we make an error handler like above. Here we have used the `isinstance` function which takes in an object and a base class as the second argument, this function returns a bool value. The `raise error` is just for displaying unhandled errors, i.e. the ones which have not been handled manually. If this is **removed**, you will not be able to see any exceptions raised by Slash Commands and makes debugging the code harder. -After creating the error handler function, we set the function as the error handler for the Slash Commands. Here, `bot.tree.on_error = on_tree_error` overwrites the default `on_error` method of the **CommandTree** class with our custom error handler which has been named as `on_tree_error` here. - -### Creating an error handler for a specific error! - -```python -from discord.ext import commands -from discord import app_commands -import discord - -bot = commands.Bot(command_prefix="!", intents=discord.Intents.default()) -#sync commands - [email protected](name="ping") -@app_commands.checks.cooldown(1, 30) -async def ping(interaction: discord.Interaction): - await interaction.response.send_message("pong!") - -async def ping_error(interaction: discord.Interaction, error: app_commands.AppCommandError): - if isinstance(error, app_commands.CommandOnCooldown): - return await interaction.response.send_message(f"Command is currently on cooldown! Try again in **{error.retry_after:.2f}** seconds!") - elif isinstance(error, ...): - ... - else: - raise error - -bot.run("token") -``` - -__**EXPLANATION**__ - -Here the command name is simply used to access the `error` method to decorate a function which acts as the `on_error` but for a specific command. You should not need to call the `error` method manually. diff --git a/pydis_site/apps/content/resources/guides/python-guides/creating-python-environment-windows.md b/pydis_site/apps/content/resources/guides/python-guides/creating-python-environment-windows.md deleted file mode 100644 index 635c384f..00000000 --- a/pydis_site/apps/content/resources/guides/python-guides/creating-python-environment-windows.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -title: Creating a Unix-style Python Environment on Windows -description: How to setup Python for Windows. ---- - -Many programmers use Linux or macOS operating systems for their work, though newcomers to programming will likely want to get started on the computer they already own, which will often be running Windows. -This guide will help you install Python on Windows. - -Programmers also need to become comfortable using a command prompt (also known as a terminal), and many guides for both beginning and advanced programming will often tell you certain commands to run. -The Windows command prompt has different names for similar commands that are available on Linux and macOS. -This guide will also help you set up a command prompt called Git Bash, which will support many of the commands available on Linux and macOS. - -## Installing Python -Python can be downloaded from the Python website on the [downloads page](https://www.python.org/downloads/). -The website will automatically present you with a download button for the latest release of the Windows version when you access the site from a Windows machine. - -Once the download is complete, you can begin the installation. -Select "Customize Installation". -The default settings for "Optional Features" are sufficient and you can click "Next". - -The next step is to decide on a location where the Python executable can be stored on your computer. -This should be a location that's easy for you to remember. -One possibility is to create a folder called "Python" at the root of your hard drive. -Once you have selected a location, you can click "Install", as no other settings on this screen need to be adjusted. -This will complete the installation. - -## Installing a text editor -You will also need a text editor for writing Python programs, and for subsequent steps of this guide. -Powerful programs called integrated development environments (IDEs) like PyCharm and Visual Studio Code contain text editors, but they also contain many other features with uses that aren't immediately obvious to new programmers. - -[Notepad++](https://notepad-plus-plus.org/) is a popular text editor for both beginners and advanced users who prefer a simpler interface. -Other editors we recommend can be found [here](https://pythondiscord.com/resources/tools/#editors). - -## Installing Git Bash -Git is a command line program that helps you keep track of changes to your code, among other things. -Many developers use it, and while you may not need it right away, it is useful to install it because it comes with Git Bash. -On the "Select Components" screen, no settings need to be changed. -The next screen will ask what text editor you want to use with Git. Vim is the default choice, though Vim is widely considered difficult to learn, so you may choose to select Notepad++ or whichever text editor you may have installed previously. - -For all remaining screens in the installation, the default selections are fine. - -## Configuring .bashrc -`.bashrc` is a file where we tell Git Bash where the Python executable is. -First, open Git Bash, and as your first command, type `echo ~` and hit enter. -This will most likely print `c/Users/YourUsername` to the terminal. -Navigate to this location in your file explorer, though keep in mind that Windows will display `c/Users/YourUsername` as `C:\Users\YourUsername`. -In this folder, there will be a file called `.bashrc`; open it with your text editor of choice. - -For this step, you will need to remember where you installed Python earlier. -In whichever folder that was, there is a file called `python.exe`; this is the executable that will run your Python programs. -Copy the full path of this file, starting from `C:`. -If you used the example location given earlier, it will be located at `C:\Python\python.exe`. - -In the `.bashrc` file, add a line to the end of the file saying `alias python='C:\\Python\\python.exe`, where `C:\\Python\\python.exe` is the location of your `python.exe` file, but each folder is separated by two backslashes instead of one. -The two backslashes are because a single backslash is used as an [escape character](https://en.wikipedia.org/wiki/Escape_character). -Save the file, and then type `source ~/.bashrc` to activate the change you have made. - -Finally, enter `python -c 'import sys; print(sys.executable)'` into Git Bash. -(If you attempt to copy and paste this into the terminal using Ctrl+V, it might not work, though Shift+Insert will.) -If all the steps have been followed correctly, this will print the location of your `python.exe` file and demonstrate that your environment is set up correctly. -You can hereafter use the `python` command in Git Bash to run any Python program that you write. - -## Running a test program -At any location on your computer, create a file named `hello.py` and open it with your text editor. -The program need only be one line: `print('Hello world!')`. -Save this file. - -To run this program in Git Bash, navigate to where it is saved on your hard drive. -If you know the path to this location, you can use the `cd` command ("cd" stands for "change directory") to navigate to it. -If it's saved to your desktop, `cd /c/Users/YourUsername/Desktop` will take you there. -Otherwise if you have the directory open in your file explorer, you can right click anywhere in the white space of the file explorer window (not on top of a file) and select "Git Bash Here". -Once you're there, type `python hello.py`, and the program will run. diff --git a/pydis_site/apps/content/resources/guides/python-guides/discord-embed-limits.md b/pydis_site/apps/content/resources/guides/python-guides/discord-embed-limits.md deleted file mode 100644 index ca97462b..00000000 --- a/pydis_site/apps/content/resources/guides/python-guides/discord-embed-limits.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -title: Discord Embed Limits -description: A guide that shows the limits of embeds in Discord and how to avoid them. ---- - -If you plan on using embed responses for your bot you should know the limits of the embeds on Discord or you will get `Invalid Form Body` errors: - -- Embed **title** is limited to **256 characters** -- Embed **description** is limited to **4096 characters** -- An embed can contain a maximum of **25 fields** -- A **field name/title** is limited to **256 character** and the **value of the field** is limited to **1024 characters** -- Embed **footer** is limited to **2048 characters** -- Embed **author name** is limited to **256 characters** -- The **total of characters** allowed in an embed is **6000** - -Now if you need to get over this limit (for example for a help command), you would need to use pagination. -There are several ways to do that: - -- A library called **[disputils](https://pypi.org/project/disputils)** -- An experimental library made by the discord.py developer called **[discord-ext-menus](https://github.com/Rapptz/discord-ext-menus)** -- Make your own setup using **[wait_for()](https://discordpy.readthedocs.io/en/stable/ext/commands/api.html#discord.ext.commands.Bot.wait_for)** and wait for a reaction to be added diff --git a/pydis_site/apps/content/resources/guides/python-guides/discord-messages-with-colors.md b/pydis_site/apps/content/resources/guides/python-guides/discord-messages-with-colors.md deleted file mode 100644 index 0e88490e..00000000 --- a/pydis_site/apps/content/resources/guides/python-guides/discord-messages-with-colors.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -title: Discord Messages with Colors -description: A guide on how to add colors to your codeblocks on Discord ---- - -Discord is now *slowly rolling out* the ability to send colored messages within code blocks. It uses the ANSI color codes, so if you've tried to print colored text in your terminal or console with Python or other languages then it will be easy for you. - -## Quick Explanation -To be able to send a colored text, you need to use the `ansi` language for your code block and provide a prefix of this format before writing your text: -```ansi -\u001b[{format};{color}m -``` -*`\u001b` is the unicode escape for ESCAPE/ESC, meant to be used in the source of your bot (see <http://www.unicode-symbol.com/u/001B.html>).* ***If you wish to send colored text without using your bot you need to copy the character from the website.*** - -After you've written this, you can now type the text you wish to color. If you want to reset the color back to normal, then you need to use the `\u001b[0m` prefix again. - -## Formats -Here is the list of values you can use to replace `{format}`: - -* 0: Normal -* 1: **Bold** -* 4: <ins>Underline</ins> - -## Colors -Here is the list of values you can use to replace `{color}`: - -### Text Colors - -* 30: Gray -* 31: Red -* 32: Green -* 33: Yellow -* 34: Blue -* 35: Pink -* 36: Cyan -* 37: White - -### Background Colors - -* 40: Firefly dark blue -* 41: Orange -* 42: Marble blue -* 43: Greyish turquoise -* 44: Gray -* 45: Indigo -* 46: Light gray -* 47: White - -## Example - -Let's take an example, I want a bold green colored text with the firefly dark blue background. -I simply use `\u001b[0;40m` (background color) and `\u001b[1;32m` (text color) as prefix. Note that the order is **important**, first you give the background color and then the text color. - -Alternatively you can also directly combine them into a single prefix like the following: `\u001b[1;40;32m` and you can also use multiple values. Something like `\u001b[1;40;4;32m` would underline the text, make it bold, make it green and have a dark blue background. - -Raw message: -````nohighlight -```ansi -\u001b[0;40m\u001b[1;32mThat's some cool formatted text right? -or -\u001b[1;40;32mThat's some cool formatted text right? -``` -```` - -Result: - - - -### ANSI Colors Showcase - -The way the colors look like on Discord is shown in the image below: - - - -*Message sent to get the output of above can be found [here](https://gist.github.com/kkrypt0nn/a02506f3712ff2d1c8ca7c9e0aed7c06#file-ansi-colors-showcase-md).* - -#### Disclaimer - -***Note**: The change has been brought to all stable desktop clients. Since syntax highlighting on mobile is far behind, ANSI is not supported on mobile as well. Refer to [this gist](https://gist.github.com/matthewzring/9f7bbfd102003963f9be7dbcf7d40e51) for other syntax highlighting methods.* diff --git a/pydis_site/apps/content/resources/guides/python-guides/discordpy-subclassing-context.md b/pydis_site/apps/content/resources/guides/python-guides/discordpy-subclassing-context.md deleted file mode 100644 index b77cb0f9..00000000 --- a/pydis_site/apps/content/resources/guides/python-guides/discordpy-subclassing-context.md +++ /dev/null @@ -1,129 +0,0 @@ ---- -title: Subclassing Context in discord.py -description: "Subclassing the default `commands.Context` class to add more functionability and customizability." ---- - -Start by reading the guide on [subclassing the `Bot` class](./subclassing_bot.md). A subclass of Bot has to be used to -inject your custom context subclass into discord.py. - -## Overview - -The way this works is by creating a subclass of discord.py's [`Context` class](https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.Context) -adding whatever functionality you wish. Usually this is done by adding custom methods or properties, so that you don't need to -copy it around or awkwardly import it elsewhere. - -This guide will show you how to add a `prompt()` method to the context and how to use it in a command. - -## Example subclass and code - -The first part - of course - is creating the actual context subclass. This is done similarly to creating a bot -subclass, it will look like this: - -```python -import asyncio -from typing import Optional - -from discord import RawReactionActionEvent -from discord.ext import commands - - -class CustomContext(commands.Context): - async def prompt( - self, - message: str, - *, - timeout=30.0, - delete_after=True - ) -> Optional[bool]: - """Prompt the author with an interactive confirmation message. - - This method will send the `message` content, and wait for max `timeout` seconds - (default is `30`) for the author to react to the message. - - If `delete_after` is `True`, the message will be deleted before returning a - True, False, or None indicating whether the author confirmed, denied, - or didn't interact with the message. - """ - msg = await self.send(message) - - for reaction in ('✅', '❌'): - await msg.add_reaction(reaction) - - confirmation = None - - # This function is a closure because it is defined inside of another - # function. This allows the function to access the self and msg - # variables defined above. - - def check(payload: RawReactionActionEvent): - # 'nonlocal' works almost like 'global' except for functions inside of - # functions. This means that when 'confirmation' is changed, that will - # apply to the variable above - nonlocal confirmation - - if payload.message_id != msg.id or payload.user_id != self.author.id: - return False - - emoji = str(payload.emoji) - - if emoji == '✅': - confirmation = True - return True - - elif emoji == '❌': - confirmation = False - return True - - # This means that it was neither of the two emojis added, so the author - # added some other unrelated reaction. - return False - - try: - await self.bot.wait_for('raw_reaction_add', check=check, timeout=timeout) - except asyncio.TimeoutError: - # The 'confirmation' variable is still None in this case - pass - - if delete_after: - await msg.delete() - - return confirmation -``` - -After creating your context subclass, you need to override the `get_context()` method on your -Bot class and change the default of the `cls` parameter to this subclass: - -```python -from discord.ext import commands - - -class CustomBot(commands.Bot): - async def get_context(self, message, *, cls=CustomContext): # From the above codeblock - return await super().get_context(message, cls=cls) -``` - -Now that discord.py is using your custom context, you can use it in a command. For example: - -```python -import discord -from discord.ext import commands - -# Enable the message intent so that we get message content. This is needed for -# the commands we define below -intents = discord.Intents.default() -intents.message_content = True - - -# Replace '...' with any additional arguments for the bot -bot = CustomBot(intents=intents, ...) - - -async def massban(ctx: CustomContext, members: commands.Greedy[discord.Member]): - prompt = await ctx.prompt(f"Are you sure you want to ban {len(members)} members?") - if not prompt: - # Return if the author cancelled, or didn't react in time - return - - ... # Perform the mass-ban, knowing the author has confirmed this action -``` diff --git a/pydis_site/apps/content/resources/guides/python-guides/discordpy.md b/pydis_site/apps/content/resources/guides/python-guides/discordpy.md deleted file mode 100644 index b0b2fad1..00000000 --- a/pydis_site/apps/content/resources/guides/python-guides/discordpy.md +++ /dev/null @@ -1,230 +0,0 @@ ---- -title: Discord.py Learning Guide -description: A learning guide for the discord.py bot framework written by members of our community. -icon: fab fa-python -toc: 2 ---- - -<!-- discord.py Badge --> -<a href="https://github.com/Rapptz/discord.py/"> - <div class="tags has-addons"> - <span class="tag is-dark">discord.py</span><span class="tag is-info">≥1.0</span> - </div> -</a> - -Interest in creating a Discord bot is a common introduction to the world of programming in our community. - -Using it as your first project in programming while trying to learn is a double-edged sword. -A large number of concepts need to be understood before becoming proficient at creating a bot, making the journey of learning and completing the project more arduous than more simple projects designed specifically for beginners. -However in return, you get the opportunity to expose yourself to many more aspects of Python than you normally would and so it can be an amazingly rewarding experience when you finally reach your goal. - -Another excellent aspect of building bots is that it has a huge scope as to what you can do with it, almost only limited by your own imagination. -This means you can continue to learn and apply more advanced concepts as you grow as a programmer while still building bots, so learning it can be a useful and enjoyable skillset. - -This page provides resources to make the path to learning as clear and easy as possible, and collates useful examples provided by the community that may address common ideas and concerns that are seen when working on Discord bots. - -## Essential References - -Official Documentation: [https://discord.py.readthedocs.io](https://discordpy.readthedocs.io/) - -Source Repository: [https://github.com/Rapptz/discord.py](https://github.com/Rapptz/discord.py) - -## Creating a Discord Bot Account - -1. Navigate to [https://discord.com/developers/applications](https://discord.com/developers/applications) and log in. -2. Click on `New Application`. -3. Enter the application's name. -4. Click on `Bot` on the left side settings menu. -5. Click `Add Bot` and confirm with `Yes, do it!`. - -### Client ID -Your Client ID is the same as the User ID of your Bot. -You will need this when creating an invite URL. - -You can find your Client ID located on the `General Information` settings page of your Application, under the `Name` field. - -Your Client ID is not a secret, and does not need to be kept private. - -### Bot Token - -Your Bot Token is the token that authorises your Bot account with the API. -Think of it like your Bot's API access key. -With your token, you can interact with any part of the API that's available to bots. - -You can find your Bot Token located on the Bot settings page of your Application, under the Username field. -You can click the Copy button to copy it without revealing it manually. - -**Your Bot Token is a secret, and must be kept private.** -If you leak your token anywhere other people has access to see it, no matter the duration, you should reset your Bot Token. - -To reset your token, go to the Bot settings page of your Application, and click the Regenerate button. -Be sure to update the token you're using for your bot script to this new one, as the old one will not work anymore. - -### Permissions Integer - -Discord Permissions are typically represented by a Permissions Integer which represents all the Permissions that have been allowed. - -You can find a reference to all the available Discord Permissions, their bitwise values and their descriptions here:<br> -[https://discordapp.com/developers/docs/topics/permissions#permissions-bitwise-permission-flags](https://discordapp.com/developers/docs/topics/permissions#permissions-bitwise-permission-flags) - -If you want to create your own Permissions Integer, you can generate it in the `Bot` settings page of your Application, located at the bottom of the page. - -Tick the permissions you want to be allowing, and it'll update the `Permissions Integer` field, which you can use in your Bot Invite URL to set your bot's default permissions when users go to invite it. - -### Bot Invite URL - -Bot's cannot use a server invite link. Instead, they have to be invited by a member with the Manage Server permission. - -The Bot Invite URL is formatted like: -`https://discordapp.com/oauth2/authorize?client_id={CLIENT_ID}&scope=bot&permissions={PERMISSIONS_INTEGER}` - -You can create the Invite URL for your bot by replacing: - -* `{CLIENT_ID}` with your [Client ID](#client-id) -* `{PERMISSIONS_INTEGER}` with the [Permissions Integer](#permissions-integer) - -You can also generate it with the [Permissions Calculator](https://discordapi.com/permissions.html tool) tool. - -## Using the Basic Client (`discord.Client`) { data-toc-label="Using the Basic Client" } - -Below are the essential resources to read over to get familiar with the basic functionality of `discord.py`. - -* [Basic event usage](https://discordpy.readthedocs.io/en/latest/intro.html#basic*concepts) -* [Simple bot walkthrough](https://discordpy.readthedocs.io/en/latest/quickstart.html#a*minimal*bot) -* [Available events reference](https://discordpy.readthedocs.io/en/latest/api.html#event*reference) -* [General API reference](https://discordpy.readthedocs.io/en/latest/api.html) - -## Using the Commands Extension (`commands.Bot`) { data-toc-label="Using the Commands Extension" } - -The Commands Extension has a explanatory documentation walking you through not only what it is and it's basic usage, but also more advanced concepts. -Be sure to read the prose documentation in full at:<br> -[https://discordpy.readthedocs.io/en/latest/ext/commands/commands.html](https://discordpy.readthedocs.io/en/latest/ext/commands/commands.html) - -It fully covers: -* How to create bot using the Commands Extension -* How to define commands and their arguments -* What the Context object is -* Argument Converters -* Error Handling basics -* Command checks - -You will also need to reference the following resources: -* [Commands Extension exclusive events](https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#event-reference) -* [Commands Extension API reference](https://discordpy.readthedocs.io/en/latest/ext/commands/api.html) - -## FAQ - -The documentation covers some basic FAQ's, and they are recommended to be read beforehand, and referenced before asking for help in case it covers your issue: -[https://discordpy.readthedocs.io/en/latest/faq.html](https://discordpy.readthedocs.io/en/latest/faq.html) - -## Usage Examples - -### Official Examples and Resources - -The official examples can be found on the [source repository](https://github.com/Rapptz/discord.py/tree/master/examples). - -The most commonly referenced examples are: - -* [Basic Commands Extension Bot](https://github.com/Rapptz/discord.py/blob/master/examples/basic_bot.py) -* [Background Task Example](https://github.com/Rapptz/discord.py/blob/master/examples/background_task.py) - -### Permissions Documentation - -* [Role Management 101](https://support.discordapp.com/hc/en-us/articles/214836687-Role-Management-101) -* [Full Permissions Documentation](https://discordapp.com/developers/docs/topics/permissions) - -### Community Examples and Resources - -The `discord.py` developer community over time have shared examples and references with each other.<br> -The following are a collated list of the most referenced community examples. - -#### Extensions / Cogs -* [Extension/Cog Example](https://gist.github.com/EvieePy/d78c061a4798ae81be9825468fe146be) - *Credit to EvieePy* -* [Available Cog Methods](https://gist.github.com/Ikusaba-san/69115b79d33e05ed07ec4a4f14db83b1) - *Credit to MIkusaba* - -#### Error Handling -* [Decent Error Handling Example](https://gist.github.com/EvieePy/7822af90858ef65012ea500bcecf1612) - *Credit to EvieePy* - -#### Embeds -* [Embed Live Designer and Visualiser](https://leovoel.github.io/embed-visualizer/) - *Credit to leovoel* -* [Embed Element Reference](https://cdn.discordapp.com/attachments/84319995256905728/252292324967710721/embed.png)<br> -{: width="200" } - -##### Using Local Images in Embeds -```py -filename = "image.png" - -f = discord.File("some_file_path", filename=filename) -embed = discord.Embed() - -embed.set_image(url=f"attachment://{filename}") -await messagable.send(file=f, embed=embed) -``` - -##### Embed Limits - -| **Element** | **Characters** | -| -------------- | ---------------------- | -| Title | 256 | -| Field Name | 256 | -| Field Value | 1024 | -| Description | 2048 | -| Footer | 2048 | -| **Entire Embed** | **6000** - -| **Element** | **Count** | -| -------------- | ---------------------- | -| Fields | 25 | - -#### Emoji - -- [Bot's Using Emoji](https://gist.github.com/scragly/b8d20aece2d058c8c601b44a689a47a0) - -#### Activity Presence - -- [Setting Bot's Discord Activity](https://gist.github.com/scragly/2579b4d335f87e83fbacb7dfd3d32828) - -#### Image Processing - -- [PIL Image Processing Example Cog](https://gist.github.com/Gorialis/e89482310d74a90a946b44cf34009e88) - *Credit to Gorialis* - -### Systemd Service -**botname.service**<br> -```ini -[Unit] -Description=My Bot Name -After=network-online.target - -[Service] -Type=simple -WorkingDirectory=/your/bots/directory -ExecStart=/usr/bin/python3 /your/bots/directory/file.py -User=username -Restart=on-failure - -[Install] -WantedBy=network-online.target -``` - -**Directory**<br> -`/usr/local/lib/systemd/system` - -**Service Commands**<br> -Refresh systemd after unit file changes:<br> -`systemctl daemon-reload` - -Set service to start on boot:<br> -`systemctl enable botname` - -Start service now:<br> -`systemctl start botname` - -Stop service:<br> -`systemctl stop botname` - -**Viewing Logs**<br> -All logs:<br> -`journalctl -u botname` - -Recent logs and continue printing new logs live:<br> -`journalctl -fu mybot` diff --git a/pydis_site/apps/content/resources/guides/python-guides/discordpy_help_command.md b/pydis_site/apps/content/resources/guides/python-guides/discordpy_help_command.md deleted file mode 100644 index 4b475146..00000000 --- a/pydis_site/apps/content/resources/guides/python-guides/discordpy_help_command.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: Custom Help Command -description: "Overwrite discord.py's help command to implement custom functionality" ---- - -First, a basic walkthrough can be found [here](https://gist.github.com/InterStella0/b78488fb28cadf279dfd3164b9f0cf96) by Stella#2000 on subclassing the HelpCommand. It will provide some foundational knowledge that is required before attempting a more customizable help command. - -## Custom Subclass of Help Command -If the types of classes of the HelpCommand do not fit your needs, you can subclass HelpCommand and use the class mehods to customize the output. Below is a simple demonstration using the following methods that can also be found on the documenation: - -- [filter_commands](https://discordpy.readthedocs.io/en/stable/ext/commands/api.html#discord.ext.commands.HelpCommand.filter_commands) - -- [send_group_help](https://discordpy.readthedocs.io/en/stable/ext/commands/api.html#discord.ext.commands.HelpCommand.send_bot_help) - -- [send_command_help](https://discordpy.readthedocs.io/en/stable/ext/commands/api.html#discord.ext.commands.HelpCommand.send_command_help) - -- [send_group_help](https://discordpy.readthedocs.io/en/stable/ext/commands/api.html#discord.ext.commands.HelpCommand.send_group_help) - -- [send_error_message](https://discordpy.readthedocs.io/en/stable/ext/commands/api.html#discord.ext.commands.HelpCommand.send_error_message) - -```python -class MyHelp(commands.HelpCommand): - - async def send_bot_help(self, mapping): - """ - This is triggered when !help is invoked. - - This example demonstrates how to list the commands that the member invoking the help command can run. - """ - filtered = await self.filter_commands(self.context.bot.commands, sort=True) # returns a list of command objects - names = [command.name for command in filtered] # iterating through the commands objects getting names - available_commands = "\n".join(names) # joining the list of names by a new line - embed = disnake.Embed(description=available_commands) - await self.context.send(embed=embed) - - async def send_command_help(self, command): - """This is triggered when !help <command> is invoked.""" - await self.context.send("This is the help page for a command") - - async def send_group_help(self, group): - """This is triggered when !help <group> is invoked.""" - await self.context.send("This is the help page for a group command") - - async def send_cog_help(self, cog): - """This is triggered when !help <cog> is invoked.""" - await self.context.send("This is the help page for a cog") - - async def send_error_message(self, error): - """If there is an error, send a embed containing the error.""" - channel = self.get_destination() # this defaults to the command context channel - await channel.send(error) - -bot.help_command = MyHelp() -``` - -You can handle when a user does not pass a command name when invoking the help command and make a fancy and customized embed; here a page that describes the bot and shows a list of commands is generally used. However if a command is passed in, you can display detailed information of the command. Below are references from the documentation below that can be utilised: - -- [Get the command object](https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.Bot.get_command) - -- [Get the command name](https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.Command.name) - -- [Get the command aliases](https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.Command.aliases) - -- [Get the command brief](https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.Command.brief) - -- [Get the command usage](https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.Command.usage) diff --git a/pydis_site/apps/content/resources/guides/python-guides/docker-hosting-guide.md b/pydis_site/apps/content/resources/guides/python-guides/docker-hosting-guide.md deleted file mode 100644 index 57d86e99..00000000 --- a/pydis_site/apps/content/resources/guides/python-guides/docker-hosting-guide.md +++ /dev/null @@ -1,323 +0,0 @@ ---- -title: How to host a bot with Docker and GitHub Actions on Ubuntu VPS -description: This guide shows how to host a bot with Docker and GitHub Actions on Ubuntu VPS ---- - -## Contents - -1. [You will learn](#you-will-learn) -2. [Introduction](#introduction) -3. [Installing Docker](#installing-docker) -4. [Creating Dockerfile](#creating-dockerfile) -5. [Building Image and Running Container](#building-image-and-running-container) -6. [Creating Volumes](#creating-volumes) -7. [Using GitHub Actions for full automation](#using-github-actions-for-full-automation) - -## You will learn how to - -- write Dockerfile -- build Docker image and run the container -- use Docker Compose -- make docker keep the files throughout the container's runs -- parse environment variables into container -- use GitHub Actions for automation -- set up self-hosted runner -- use runner secrets - -## Introduction - -Let's say you have got a nice discord bot written in python and you have a VPS to host it on. Now the only question is -how to run it 24/7. You might have been suggested to use *screen multiplexer*, but it has some disadvantages: - -1. Every time you update the bot you have to SSH to your server, attach to screen, shutdown the bot, run `git pull` and - run the bot again. You might have good extensions management that allows you to update the bot without restarting it, - but there are some other cons as well -2. If you update some dependencies, you have to update them manually -3. The bot doesn't run in an isolated environment, which is not good for security. - -But there's a nice and easy solution to these problems - **Docker**! Docker is a containerization utility that automates -some stuff like dependencies update and running the application in the background. So let's get started. - -## Installing Docker - -The best way to install Docker is to use -the [convenience script](https://docs.docker.com/engine/install/ubuntu/#install-using-the-convenience-script) provided -by Docker developers themselves. You just need 2 lines: - -```shell -$ curl -fsSL https://get.docker.com -o get-docker.sh -$ sudo sh get-docker.sh -``` - -## Creating Dockerfile - -To tell Docker what it has to do to run the application, we need to create a file named `Dockerfile` in our project's -root. - -1. First we need to specify the *base image*, which is the OS that the docker container will be running. Doing that will - make Docker install some apps we need to run our bot, for - example the Python interpreter - -```dockerfile -FROM python:3.10-bullseye -``` - -2. Next, we need to copy our Python project's external dependencies to some directory *inside the container*. Let's call - it `/app` - -```dockerfile -COPY requirements.txt /app/ -``` - -3. Now we need to set the directory as working and install the requirements - -```dockerfile -WORKDIR /app -RUN pip install -r requirements.txt -``` - -4. The only thing that is left to do is to copy the rest of project's files and run the main executable - -```dockerfile -COPY . . -CMD ["python3", "main.py"] -``` - -The final version of Dockerfile looks like this: - -```dockerfile -FROM python:3.10-bullseye -COPY requirements.txt /app/ -WORKDIR /app -RUN pip install -r requirements.txt -COPY . . -CMD ["python3", "main.py"] -``` - -## Building Image and Running Container - -Now update the project on your VPS, so we can run the bot with Docker. - -1. Build the image (dot at the end is very important) - -```shell -$ docker build -t mybot . -``` - -- the `-t` flag specifies a **tag** that will be assigned to the image. With it, we can easily run the image that the - tag was assigned to. -- the dot at the end is basically the path to search for Dockerfile. The dot means current directory (`./`) - -2. Run the container - -```shell -$ docker run -d --name mybot mybot:latest -``` - -- `-d` flag tells Docker to run the container in detached mode, meaning it will run the container in the background of - your terminal and not give us - any output from it. If we don't - provide it, the `run` will be giving us the output until the application exits. Discord bots aren't supposed to exit - after certain time, so we do need this flag -- `--name` assigns a name to the container. By default, container is identified by id that is not human-readable. To - conveniently refer to container when needed, - we can assign it a name -- `mybot:latest` means "latest version of `mybot` image" - -3. Read bot logs (keep in mind that this utility only allows to read STDERR) - -```shell -$ docker logs -f mybot -``` - -- `-f` flag tells the docker to keep reading logs as they appear in container and is called "follow mode". To exit - press `CTRL + C`. - -If everything went successfully, your bot will go online and will keep running! - -## Using Docker Compose - -Just 2 commands to run a container is cool, but we can shorten it down to just 1 simple command. For that, create -a `docker-compose.yml` file in project's root and fill it with the following contents: - -```yml -version: "3.8" -services: - main: - build: . - container_name: mybot -``` - -- `version` tells Docker what version of Compose to use. You may check all the - versions [here](https://docs.docker.com/compose/compose-file/compose-versioning/) -- `services` contains services to build and run. Read more about - services [here](https://docs.docker.com/compose/compose-file/#services-top-level-element) -- `main` is a service. We can call it whatever we would like to, not necessarily `main` -- `build: .` is a path to search for Dockerfile, just like `docker build` command's dot -- `container_name: mybot` is a container name to use for a bot, just like `docker run --name mybot` - -Update the project on VPS, remove the previous container with `docker rm -f mybot` and run this command - -```shell -docker compose up -d --build -``` - -Now the docker will automatically build the image for you and run the container. - -### Why docker-compose - -The main purpose of Compose is to run several services at once. Mostly we -don't need this in discord bots, however. -For us, it has the following benefits: - -- we can build and run the container with just one command -- if we need to parse some environment variables or volumes (more about them further in tutorial) our run command would - look like this - -```shell -$ docker run -d --name mybot -e TOKEN=... -e WEATHER_API_APIKEY=... -e SOME_USEFUL_ENVIRONMENT_VARIABLE=... --net=host -v /home/exenifix/bot-data/data:/app/data -v /home/exenifix/bot-data/images:/app/data/images -``` - -This is pretty long and unreadable. Compose allows us to transfer those flags into single config file and still -use just one short command to run the container. - -## Creating Volumes - -The files creating during container run are destroyed after its recreation. To prevent some files from getting -destroyed, we need to use *volumes* that basically save the files from directory inside of container somewhere on drive. - -1. Create a new directory somewhere and copy path to it - -```shell -$ mkdir mybot-data -$ echo $(pwd)/mybot-data -``` - -My path is `/home/exenifix/mybot-data`, yours is most likely **different**! - -2. In your project, store the files that need to be persistent in a separate directory (eg. `data`) -3. Add `volumes` to `docker-compose.yaml` so it looks like this: - -```yml -version: "3.8" -services: - main: - build: . - container_name: mybot - volumes: - - /home/exenifix/mybot-data:/app/data -``` - -The path before the colon `:` is the directory *on server's drive, outside of container*, and the second path is the -directory *inside of container*. -All the files saved in container in that directory will be saved on drive's directory as well and Docker will be -accessing them *from drive*. - -## Using GitHub Actions for full automation - -Now it's time to fully automate the process and make Docker update the bot automatically on every commit or release. For -that, we will use a **GitHub Actions workflow**, which basically runs some commands when we need to. You may read more -about them [here](https://docs.github.com/en/actions/using-workflows). - -### Create repository secret - -We will not have the ability to use `.env` files with the workflow, so it's better to store the environment variables -as **actions secrets**. Let's add your discord bot's token as a secret - -1. Head to your repository page -> Settings -> Secrets -> Actions -2. Press `New repository secret` -3. Give it a name like `TOKEN` and paste the token. - Now we will be able to access its value in workflow like `${{ secrets.TOKEN }}`. However, we also need to parse the - variable into container now. Edit `docker-compose` so it looks like this: - -```yml -version: "3.8" -services: - main: - build: . - container_name: mybot - volumes: - - /home/exenifix/mybot-data:/app/data - environment: - - TOKEN -``` - -### Setup self-hosted runner - -To run the workflow on our VPS, we will need to register it as *self-hosted runner*. - -1. Head to Settings -> Actions -> Runners -2. Press `New self-hosted runner` -3. Select runner image and architecture -4. Follow the instructions but don't run the runner -5. Instead, create a service - -```shell -$ sudo ./svc.sh install -$ sudo ./svc.sh start -``` - -Now we have registered our VPS as a self-hosted runner and we can run the workflow on it now. - -### Write a workflow - -Create a new file `.github/workflows/runner.yml` and paste the following content into it. Please pay attention to -the `branches` instruction. -The GitHub's standard main branch name is `main`, however it may be named `master` or something else if you edited its -name. Make sure to put -the correct branch name, otherwise it won't work. More about GitHub workflows -syntax [here](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions) - -```yml -name: Docker Runner - -on: - push: - branches: [ main ] - -jobs: - run: - runs-on: self-hosted - environment: production - - steps: - - uses: actions/checkout@v3 - - - name: Run Container - run: docker compose up -d --build - env: - TOKEN: ${{ secrets.TOKEN }} - - - name: Cleanup Unused Images - run: docker image prune -f -``` - -Run `docker rm -f mybot` (it only needs to be done once) and push to GitHub. Now if you open `Actions` tab on your -repository, you should see a workflow running your bot. Congratulations! - -### Displaying logs in actions terminal - -There's a nice utility for reading docker container's logs and stopping upon meeting a certain phrase and it might be -useful for you as well. - -1. Install the utility on your VPS with - -```shell -$ pip install exendlr -``` - -2. Add a step to your workflow that would show the logs until it meets `"ready"` phrase. I recommend putting it before - the cleanup. - -```yml -- name: Display Logs - run: python3 -m exendlr mybot "ready" -``` - -Now you should see the logs of your bot until the stop phrase is met. - -**WARNING** -> The utility only reads from STDERR and redirects to STDERR, if you are using STDOUT for logs, it will not work and -> will be waiting for stop phrase forever. The utility automatically exits if bot's container is stopped (e.g. error -> occurred during starting) or if a log line contains a stop phrase. Make sure that your bot 100% displays a stop phrase -> when it's ready otherwise your workflow will get stuck. diff --git a/pydis_site/apps/content/resources/guides/python-guides/fix-ssl-certificate.md b/pydis_site/apps/content/resources/guides/python-guides/fix-ssl-certificate.md deleted file mode 100644 index 096e3a90..00000000 --- a/pydis_site/apps/content/resources/guides/python-guides/fix-ssl-certificate.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: Fixing an SSL Certificate Verification Error -description: A guide on fixing verification of an SSL certificate. ---- - -We're fixing the error Python specifies as [ssl.SSLCertVerificationError](https://docs.python.org/3/library/ssl.html#ssl.SSLCertVerificationError). - -# How to fix SSL Certificate issue on Windows - -Firstly, try updating your OS, wouldn't hurt to try. - -Now, if you're still having an issue, you would need to download the certificate for the SSL. - -The SSL Certificate, Sectigo (cert vendor) provides a download link of an [SSL certificate](https://crt.sh/?id=2835394). You should find it in the bottom left corner, shown below: - -A picture where to find the certificate in the website is: - - -You have to setup the certificate yourself. To do that you can just click on it, or if that doesn't work, refer to [this link](https://portal.threatpulse.com/docs/sol/Solutions/ManagePolicy/SSL/ssl_chrome_cert_ta.htm) - -# How to fix SSL Certificate issue on Mac - -Navigate to your `Applications/Python 3.x/` folder and double-click the `Install Certificates.command` to fix this. diff --git a/pydis_site/apps/content/resources/guides/python-guides/keeping-tokens-safe.md b/pydis_site/apps/content/resources/guides/python-guides/keeping-tokens-safe.md deleted file mode 100644 index 92eb52a3..00000000 --- a/pydis_site/apps/content/resources/guides/python-guides/keeping-tokens-safe.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: Keeping Discord Bot Tokens Safe -description: How to keep your bot tokens safe and safety measures you can take. ---- -It's **very** important to keep a bot token safe, -primarily because anyone who has the bot token can do whatever they want with the bot -- -such as destroying servers your bot has been added to and getting your bot banned from the API. - -# How to Avoid Leaking your Token -To help prevent leaking your token, -you should ensure that you don't upload it to an open source program/website, -such as replit and github, as they show your code publicly. -The best practice for storing tokens is generally utilising .env files -([click here](https://tutorial.vco.sh/tips/tokens/) for more information on storing tokens safely). - -# What should I do if my token does get leaked? - -If for whatever reason your token gets leaked, you should immediately follow these steps: -- Go to the list of [Discord Bot Applications](https://discord.com/developers/applications) you have and select the bot application that had the token leaked. -- Select the Bot (1) tab on the left-hand side, next to a small image of a puzzle piece. After doing so you should see a small section named TOKEN (under your bot USERNAME and next to his avatar image) -- Press the Regenerate button to regenerate your bot token and invalidate the old one. - - - -Following these steps will create a new token for your bot, making it secure again and terminating any connections from the leaked token. -The old token will stop working though, so make sure to replace the old token with the new one in your code if you haven't already. - -# Summary -Make sure you keep your token secure by storing it safely, not sending it to anyone you don't trust, and regenerating your token if it does get leaked. diff --git a/pydis_site/apps/content/resources/guides/python-guides/mutability.md b/pydis_site/apps/content/resources/guides/python-guides/mutability.md deleted file mode 100644 index 185dc87c..00000000 --- a/pydis_site/apps/content/resources/guides/python-guides/mutability.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: Mutability and Immutability in Python -description: "Mutable and immutable data types: what they are and how they work." ---- - -Consider this example: -```python ->>> s = "hello" ->>> s.upper() -'HELLO' ->>> s -'hello' -``` -This might break your expectations. -After all, you've called the `upper()` method on `s`, so why didn't it change? That's because strings are _immutable_: you can't change them in-place, only create new ones. -In this example, `.upper()` just cannot change the string stored in `s`. - -How do you make `s` store `'HELLO'` instead of `'hello'` then? That's possible. -Even though you can't change the original string, you can create a new one, which is like the old one, but with all letters in upper case. - -In other words, `s.upper()` doesn't change an existing string. -It just returns a new one. -```python ->>> s = 'hello' ->>> s = s.upper() ->>> s -'HELLO' -``` - -Let's examine what's going on here. -At first, the variable `s` refers to some object, the string `'hello'`. - - - -When you call `s.upper()`, a new string, which contains the characters `'HELLO'`, gets created. - - - -This happens even if you just call `s.upper()` without any assignment, on its own line: -```python -"hello".upper() -``` -In this case, a new object will be created and discarded right away. - -Then the assignment part comes in: the name `s` gets disconnected from `'hello'`, and gets connected to `'HELLO'`. - - - -Now we can say that `'HELLO'` is stored in the `s` variable. - -Then, because no variables refer to the _object_ `'hello'`, it gets eaten by the garbage collector. - - - -It means that the memory reserved for that object will be freed. If that didn't happen, the 'garbage' would accumulate over time and fill up all the RAM. diff --git a/pydis_site/apps/content/resources/guides/python-guides/parameters-and-arguments.md b/pydis_site/apps/content/resources/guides/python-guides/parameters-and-arguments.md deleted file mode 100644 index 45ad60b1..00000000 --- a/pydis_site/apps/content/resources/guides/python-guides/parameters-and-arguments.md +++ /dev/null @@ -1,290 +0,0 @@ ---- -title: Function Parameters and Arguments in Python -description: An in-depth look at function parameters and arguments, and how to use them. -toc: 1 ---- - -A Python function is utilised in two steps: - -1. The function definition/signature (used just once). -2. The function invocation/call (used many times). - -The function definition uses parameters, whereas the function call uses arguments: - -```python -def foo(this_is_a_parameter): - print(this_is_a_parameter) - -foo(this_is_an_argument) -``` - -An important detail to be aware of is that by default any argument used to call a function in Python can be used as both a positional and a keyword argument—just not at the same time. -A function call may contain a mixture of positional and keyword arguments, and—unless otherwise specified—an argument can reference the parameters in the function definition positionally, or by name (keyword). - -# Positional Arguments - -```python -def foo(a, b, c): - print(a, b, c) - ->>> foo(1, 2, 3) -1 2 3 -``` - -In the above function definition we have three parameters `a`, `b`, and `c`. - -When we invoke the function with the arguments `1`, `2`, and `3`, the function will map these values in the exact order given to the parameters in the function definition. -With no keyword reference given they become positional arguments. - -# Keyword Arguments - -```python -def foo(a, b, c): - print(a, b, c) - ->>> foo(1, 2, 3) -1 2 3 - ->>> foo(c=3, b=2, a=1) -1 2 3 -``` - -As you can see, `foo(1, 2, 3)` and `foo(c=3, b=2, a=1)` are identical. -Referencing a function parameter by its name means that we are using a keyword argument. -The order in which keyword arguments are given does not matter. - -# Mixing Positional and Keyword Arguments - -So what happens if we want to mix the positional argument mapping with keyword arguments? - -Python prioritises the mapping of positional arguments to their parameter names before the mapping of keywords. - -```python -def foo(a, b, c): - print(a, b, c) - ->>> foo(1, c=3, b=2) -1 2 3 -``` - -Passing a keyword argument using the name of a parameter that has already been given will not work: - -```python ->>> foo(1, 2, a=3) -TypeError: foo() got multiple values for argument 'a' - ->>> foo(1, b=2, b=3) -SyntaxError: keyword argument repeated -``` - -Attempting to pass positional arguments after a keyword argument will also not work: - -```python ->>> foo(a=1, 2, 3) -SyntaxError: positional argument follows keyword argument -``` - -# Default Parameter Values - -Although the syntax is similar, these are not to be confused with keyword arguments.<br> -Default parameter values appear within the function definition and allow us to conveniently set a default value. This means that if any argument is omitted, its default value will be used as the argument. - -```python -def foo(a=0, b=0, c=0): - print(a, b, c) - ->>> foo() -0 0 0 - ->>> foo(1, 2, 3) -1 2 3 - ->>> foo(c=3, b=2) -0 2 3 - ->>> foo(1, c=3) -1 0 3 -``` - -Using default parameter values does not change how a function can be invoked with arguments: - -```python ->>> foo(1, 2, a=3) -TypeError: foo() got multiple values for argument 'a' - ->>> foo(1, b=2, b=3) -SyntaxError: keyword argument repeated - ->>> foo(a=1, 2, 3) -SyntaxError: positional argument follows keyword argument -``` - -You must specify any parameters without a default value before those with default values: - -```python -def foo(a=0, b): - ^ -SyntaxError: non-default argument follows default argument -``` - -# Positional-only Parameters -[Python 3.8](https://docs.python.org/3/whatsnew/3.8.html#positional-only-parameters) / [PEP 570](https://www.python.org/dev/peps/pep-0570/) introduces the possibility to specify which parameters are required to be positional-only via a bare `/` parameter within a function definition. - -```python -def foo(a=0, b=0, /, c=0, d=0): - print(a, b, c, d) -``` - -The parameters defined before the bare `/` are now considered to be positional-only and keyword mapping will no longer work on them.<br> -In the above function definition `a` and `b` are now positional-only parameters. - -These function calls will still work: - -```python ->>> foo() -0 0 0 0 - ->>> foo(1) -1 0 0 0 - ->>> foo(1, 2, 3, 4) -1 2 3 4 - ->>> foo(1, 2, d=4, c=3) -1 2 3 4 - ->>> foo(1, d=4, c=3) -1 0 3 4 - ->>> foo(c=3, d=4) -0 0 3 4 -``` - -However, attempting to pass keyword arguments for `a` or `b` will fail: - -```python ->>> foo(1, b=2, c=3, d=4) -TypeError: foo() got some positional-only arguments passed as keyword arguments: 'b' -``` - -### Q: Why is this useful? - -#### Keyword Argument Freedom - -Passing a keyword argument using the name of a parameter that has already been given will not work. -This becomes an issue if we require keyword arguments that use the same parameter names as defined in the function signature, such as via callback functions. - -```python -def foo(a, **kwargs): - print(a, kwargs) - ->>> foo(a=1, a=2) -SyntaxError: keyword argument repeated - ->>> foo(1, a=2) -TypeError: foo() got multiple values for argument 'a' -``` - -#### Backwards Compatibility - -Because Python allows that an argument by default can be either positional or keyword, a user is free to choose either option. -Unfortunately, this forces the author to keep the given parameter names as they are if they wish to support backwards compatibility, as changing the parameter names can cause dependent code to break. -Enforcing positional-only parameters gives the author the freedom to separate the variable name used within the function from its usage outside of it. - -```python -def calculate(a, b): - # do something with a and b - ->>> calculate(1, 2) -``` - -A user could call this function using `a` or `b` as keywords, which the author may have not intended: - -```python ->>> calculate(a=1, b=2) -``` - -However, by using `/`, the user will no longer be able to invoke using `a` or `b` as keywords, and the author is also free to rename these parameters: - -```python -def calculate(x, y, /): - # do something with x and y - ->>> calculate(1, 2) -``` - -# Keyword-only Parameters - -Similarly to enforcing positional-only parameters, we can also enforce keyword-only parameters using a bare `*` parameter. -The parameters defined after the bare `*` are now considered to be keyword-only. - -```python -def foo(a=0, b=0, /, c=0, *, d=0): - print(a, b, c, d) - ->>> foo() -0 0 0 0 - ->>> foo(1, 2, 3) -1 2 3 0 - ->>> foo(1, 2, d=4, c=3) -1 2 3 4 - ->>> foo(1, d=4, c=3) -1 0 3 4 -``` - -Although `c` can be either a positional or keyword argument, if we attempt to pass `d` as a non-keyword argument, it will fail: - -```python ->>> foo(1, 2, 3, 4) -TypeError: foo() takes from 0 to 3 positional arguments but 4 were given -``` - -At least one named parameter must be provided after a bare `*` parameter. -Writing a function definition similar to what is shown below would not make sense, as without the context of a named parameter the bare `*` can simply be omitted. - -```python -def foo(a=0, *, **kwargs): - ^ -SyntaxError: named arguments must follow bare * -``` - -### Q: Why is this useful? - -The main benefit of using keyword-only parameters is when they are used together with positional-only parameters to remove ambiguity. - -However, it may sometimes also be desirable to use keyword-only arguments on their own.<br> -If we were to expose a function as part of an API, we may want the parameter names to carry explicit meaning. - -Without using keyword names when invoking the function it can be unclear as to what the provided arguments are for. -Additionally, a user could also choose to interchange positional arguments with keyword arguments, which can potentially add to the confusion. - -```python -def update(identity=None, name=None, description=None): - # handle the parameters - ->>> update("value 1", "value 2", "value 3") - ->>> update(1234, "value 1", description="value 2") -``` - -Enforcing the keyword names is clearer, as it carries context without needing to look at the function definition: - -```python -def update(*, identity=None, name=None, description=None): - # handle the parameters - ->>> update(identity=1234, name="value 1", description="value 2") -``` - -# Summary - -* Unless otherwise specified, an argument can be both positional and keyword. -* Positional arguments, when provided, must be in sequence. -* Positional arguments must be used before keyword arguments. -* Keyword arguments may be in any order. -* A default parameter value is used when the argument is omitted. -* A bare `/` used as a parameter in a function definition enforces positional-only parameters to its left. -* A bare `*` used as a parameter in a function definition enforces keyword-only parameters to its right. diff --git a/pydis_site/apps/content/resources/guides/python-guides/proper-error-handling.md b/pydis_site/apps/content/resources/guides/python-guides/proper-error-handling.md deleted file mode 100644 index 74b0f59b..00000000 --- a/pydis_site/apps/content/resources/guides/python-guides/proper-error-handling.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -title: Proper error handling in discord.py -description: Are you not getting any errors? This might be why! ---- -If you're not recieving any errors in your console, even though you know you should be, try this: - -# With bot subclass: -```py -import discord -from discord.ext import commands - -import traceback -import sys - -class MyBot(commands.Bot): - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - async def on_command_error(self, ctx: commands.Context, error): - # Handle your errors here - if isinstance(error, commands.MemberNotFound): - await ctx.send("I could not find member '{error.argument}'. Please try again") - - elif isinstance(error, commands.MissingRequiredArgument): - await ctx.send(f"'{error.param.name}' is a required argument.") - else: - # All unhandled errors will print their original traceback - print(f'Ignoring exception in command {ctx.command}:', file=sys.stderr) - traceback.print_exception(type(error), error, error.__traceback__, file=sys.stderr) - -bot = MyBot(command_prefix="!", intents=discord.Intents.default()) - -bot.run("token") -``` - -# Without bot subclass -```py -import discord -from discord.ext import commands - -import traceback -import sys - -async def on_command_error(self, ctx: commands.Context, error): - # Handle your errors here - if isinstance(error, commands.MemberNotFound): - await ctx.send("I could not find member '{error.argument}'. Please try again") - - elif isinstance(error, commands.MissingRequiredArgument): - await ctx.send(f"'{error.param.name}' is a required argument.") - else: - # All unhandled errors will print their original traceback - print(f'Ignoring exception in command {ctx.command}:', file=sys.stderr) - traceback.print_exception(type(error), error, error.__traceback__, file=sys.stderr) - -bot = commands.Bot(command_prefix="!", intents=discord.Intents.default()) -bot.on_command_error = on_command_error - -bot.run("token") -``` - - -Make sure to import `traceback` and `sys`! - -------------------------------------------------------------------------------------------------------------- - -Useful Links: -- [FAQ](https://discordpy.readthedocs.io/en/latest/faq.html) -- [Simple Error Handling](https://gist.github.com/EvieePy/7822af90858ef65012ea500bcecf1612) diff --git a/pydis_site/apps/content/resources/guides/python-guides/setting-different-statuses-on-your-bot.md b/pydis_site/apps/content/resources/guides/python-guides/setting-different-statuses-on-your-bot.md deleted file mode 100644 index 45c7b37c..00000000 --- a/pydis_site/apps/content/resources/guides/python-guides/setting-different-statuses-on-your-bot.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: Setting Different Statuses on Your Bot -description: How to personalize your Discord bot status ---- - -You've probably seen a bot or two have a status message under their username in the member bar set to something such as `Playing Commands: .help`. - -This guide shows how to set such a status, so your bot can have one as well. - -**Please note:** - -If you want to change the bot status, it is suggested to not do so during the `on_ready` event, since it would be called many times and making an API call on that event has a chance to disconnect the bot. - -The status should not have a problem being set during runtime with `change_presence`, in the examples shown below. - -Instead, set the desired status using the activity / status kwarg of commands.Bot, for example: -```python -bot = commands.Bot(command_prefix="!", activity=..., status=...) -``` - -The following are examples of what you can put into the `activity` keyword argument. - -#### Setting 'Playing' Status -```python -await client.change_presence(activity=discord.Game(name="a game")) -``` - -#### Setting 'Streaming' Status -```python -await client.change_presence(activity=discord.Streaming(name="My Stream", url=my_twitch_url)) -``` - -#### Setting 'Listening' Status -```python -await client.change_presence(activity=discord.Activity(type=discord.ActivityType.listening, name="a song")) -``` - -#### Setting 'Watching' Status -```python -await client.change_presence(activity=discord.Activity(type=discord.ActivityType.watching, name="a movie")) -``` - -### Add Optional Status as Well: - -* `discord.Status.online` (default, green icon) -* `discord.Status.idle` (yellow icon) -* `discord.Status.do_not_disturb` (red icon) -* `discord.Status.offline` (gray icon) diff --git a/pydis_site/apps/content/resources/guides/python-guides/vps-services.md b/pydis_site/apps/content/resources/guides/python-guides/vps-services.md deleted file mode 100644 index 710fd914..00000000 --- a/pydis_site/apps/content/resources/guides/python-guides/vps-services.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -title: VPS and Free Hosting Service for Discord bots -description: This article lists recommended VPS services and covers the disasdvantages of utilising a free hosting service to run a discord bot. -toc: 2 ---- - -## Recommended VPS services - -If you need to run your bot 24/7 (with no downtime), you should consider using a virtual private server (VPS). Here is a list of VPS services that are sufficient for running Discord bots. - -* Europe - * [netcup](https://www.netcup.eu/) - * Germany & Austria data centres. - * Great affiliate program. - * [Yandex Cloud](https://cloud.yandex.ru/) - * Vladimir, Ryazan, and Moscow region data centres. - * [Scaleway](https://www.scaleway.com/) - * France data centre. - * [Time 4 VPS](https://www.time4vps.eu/) - * Lithuania data centre. -* US - * [GalaxyGate](https://galaxygate.net/) - * New York data centre. - * Great affiliate program. -* Global - * [Linode](https://www.linode.com/) - * [Digital Ocean](https://www.digitalocean.com/) - * [OVHcloud](https://www.ovhcloud.com/) - * [Vultr](https://www.vultr.com/) - - -## Why not to use free hosting services for bots? -While these may seem like nice and free services, it has a lot more caveats than you may think. For example, the drawbacks of using common free hosting services to host a discord bot are discussed below. - -### Replit - -- The machines are super underpowered, resulting in your bot lagging a lot as it gets bigger. - -- You need to run a webserver alongside your bot to prevent it from being shut off. This uses extra machine power. - -- Repl.it uses an ephemeral file system. This means any file you saved through your bot will be overwritten when you next launch. - -- They use a shared IP for everything running on the service. -This one is important - if someone is running a user bot on their service and gets banned, everyone on that IP will be banned. Including you. - -### Heroku -- Bots are not what the platform is designed for. Heroku is designed to provide web servers (like Django, Flask, etc). This is why they give you a domain name and open a port on their local emulator. - -- Heroku's environment is heavily containerized, making it significantly underpowered for a standard use case. - -- Heroku's environment is volatile. In order to handle the insane amount of users trying to use it for their own applications, Heroku will dispose your environment every time your application dies unless you pay. - -- Heroku has minimal system dependency control. If any of your Python requirements need C bindings (such as PyNaCl - binding to libsodium, or lxml binding to libxml), they are unlikely to function properly, if at all, in a native - environment. As such, you often need to resort to adding third-party buildpacks to facilitate otherwise normal - CPython extension functionality. (This is the reason why voice doesn't work natively on heroku) - -- Heroku only offers a limited amount of time on their free programme for your applications. If you exceed this limit, which you probably will, they'll shut down your application until your free credit resets. diff --git a/pydis_site/apps/content/resources/guides/python-guides/why-not-json-as-database.md b/pydis_site/apps/content/resources/guides/python-guides/why-not-json-as-database.md deleted file mode 100644 index 6d9f433e..00000000 --- a/pydis_site/apps/content/resources/guides/python-guides/why-not-json-as-database.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: Why JSON is unsuitable as a database -description: The many reasons why you shouldn't use JSON as a database, and instead opt for SQL. -relevant_links: - Tips on Storing Data: https://tutorial.vco.sh/tips/storage/ ---- - -JSON, quite simply, is not a database. It's not designed to be a data storage format, -rather a wayof transmitting data over a network. It's also often used as a way of doing configuration files for programs. - -There is no redundancy built in to JSON. JSON is just a format, and Python has libraries for it -like json and ujson that let you load and dump it, sometimes to files, but that's all it does, write data to a file. -There is no sort of DBMS (Database Management System), which means no sort of sophistication in how the data is stored, -or built in ways to keep it safe and backed up, there's no built in encryption either - bear in mind -in larger applications encryption may be necessary for GDPR/relevant data protection regulations compliance. - -JSON, unlike relational databases, has no way to store relational data, -which is a very commonly needed way of storing data. -Relational data, as the name may suggest, is data that relates to other data. -For example if you have a table of users and a table of servers, the server table will probably have an owner field, -where you'd reference a user from the users table. (**This is only relevant for relational data**). - -JSON is primarily a KV (key-value) format, for example `{"a": "b"}` where `a` is the key and `b` is the value, -but what if you want to search not by that key but by a sub-key? Well, instead of being able to quickly use `var[key]`, -which in a Python dictionary has a constant return time (for more info look up hash tables), -you now have to iterate through every object in the dictionary and compare to find what you're looking for. -Most relational database systems, like MySQL, MariaDB, and PostgreSQL have ways of indexing secondary fields -apart from the primary key so that you can easily search by multiple attributes. diff --git a/pydis_site/apps/content/resources/privacy.md b/pydis_site/apps/content/resources/privacy.md deleted file mode 100644 index a2ab6f87..00000000 --- a/pydis_site/apps/content/resources/privacy.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -title: Privacy Policy -description: Our server's privacy policy. -icon: fab fa-discord ---- - -You should be redirected. If you are not, [please click here](https://www.notion.so/pythondiscord/Python-Discord-Privacy-ee2581fea4854ddcb1ebc06c1dbb9fbd). - -<script> - // Redirect visitor to the privacy page - window.location.href = "https://www.notion.so/pythondiscord/Python-Discord-Privacy-ee2581fea4854ddcb1ebc06c1dbb9fbd"; -</script> diff --git a/pydis_site/apps/content/resources/rules.md b/pydis_site/apps/content/resources/rules.md deleted file mode 100644 index 803c8041..00000000 --- a/pydis_site/apps/content/resources/rules.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: Python Discord Rules -description: The rules of our community. -icon: fab fa-discord ---- -We have a small but strict set of rules on our server. Please read over them and take them on board. If you don't understand a rule or need to report an incident, please send a direct message to <code>@ModMail</code>! - -> 1. Follow the [Python Discord Code of Conduct](/pages/code-of-conduct/). -> 2. Follow the [Discord Community Guidelines](https://discordapp.com/guidelines) and [Terms of Service](https://discordapp.com/terms). -> 3. Respect staff members and listen to their instructions. -> 4. Use English to the best of your ability. Be polite if someone speaks English imperfectly. -> 5. Do not provide or request help on projects that may break laws, breach terms of services, or are malicious or inappropriate. -> 6. Do not post unapproved advertising. -> 7. Keep discussions relevant to the channel topic. Each channel's description tells you the topic. -> 8. Do not help with ongoing exams. When helping with homework, help people learn how to do the assignment without doing it for them. -> 9. Do not offer or ask for paid work of any kind. -> 10. Do not copy and paste answers from ChatGPT or similar AI tools. - -# Name & Profile Policy - -In order to keep things pleasant and workable for both users and staff members, we enforce the following requirements regarding your name, avatar, and profile. Staff reserve the right to change any nickname we judge to be violating these requirements. - -We also reserve the right to enforce compliance of hateful or otherwise inappropriate usernames and profiles regardless of the server-specific nickname or profile. - - -1. No blank or "invisible" names. -2. No slurs or other offensive sentiments or imagery. -3. No noisy unicode characters (for example z̯̯͡a̧͎̺̻̝͕̠l̡͓̫̣g̹̲o̡̼̘) or rapidly flashing avatars. - - -# Infractions - -We have a generally no-nonsense policy when it comes to our rules. If you notice someone breaking them, feel free to mention or DM a staff member and we'll try to deal with it as soon as possible. - -The possible actions we take based on infractions can include the following: - -* A public verbal or textual warning -* Forced nick changes, where appropriate -* A short temporary mute -* A long temporary mute -* A kick from the server -* A temporary ban from the server -* A permanent ban from the server - -While we do discuss more serious matters internally before handing out a punishment, simpler infractions are dealt with directly by individual staffers and the punishment they hand out is left to their own discretion. - -If you wish to appeal a decision or action taken by the moderation team, you can do so in one of the following ways: - -* By sending a direct message (DM) to ModMail from our Discord server. -* Joining our [ban appeals server](https://discord.gg/WXrCJxWBnm) and sending a direct message (DM) to the Ban Appeals bot. diff --git a/pydis_site/apps/content/resources/security-notice.md b/pydis_site/apps/content/resources/security-notice.md deleted file mode 100644 index e3630ae1..00000000 --- a/pydis_site/apps/content/resources/security-notice.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: Security Notice -description: How vulnerabilities in our projects should be reported. -icon: fas fa-shield-alt ---- - -This is the security notice for all Python Discord repositories. -The notice explains how vulnerabilities should be reported. - -# Reporting a Vulnerability - -If you've found a vulnerability, we would like to know so we can fix it before it is released publicly. -**Do not open a GitHub issue for a found vulnerability**. - -Send details to [[email protected]](mailto:[email protected]) or through a Discord direct message to an Admin of Python Discord, including: - -* the website, page or repository where the vulnerability can be observed -* a brief description of the vulnerability -* optionally the type of vulnerability and any related [OWASP category](https://www.owasp.org/index.php/Category:OWASP_Top_Ten_2017_Project) -* non-destructive exploitation details - -We will do our best to reply as fast as possible. - -# Scope - -The following vulnerabilities **are not** in scope: - -* volumetric vulnerabilities, for example overwhelming a service with a high volume of requests -* reports indicating that our services do not fully align with “best practice”, for example missing security headers - -If you aren't sure, you can still reach out via email or direct message. - ---- - -This notice is inspired by the [GDS Security Notice](https://github.com/alphagov/.github/blob/master/SECURITY.md). - -*Version 2021-03* diff --git a/pydis_site/apps/content/resources/server-info/_info.yml b/pydis_site/apps/content/resources/server-info/_info.yml deleted file mode 100644 index 52df0f8d..00000000 --- a/pydis_site/apps/content/resources/server-info/_info.yml +++ /dev/null @@ -1,3 +0,0 @@ -title: Server Information -description: Information on roles, tooling, and infrastructure at Python Discord. -icon: fab fa-discord diff --git a/pydis_site/apps/content/resources/server-info/roles.md b/pydis_site/apps/content/resources/server-info/roles.md deleted file mode 100644 index dc4240d6..00000000 --- a/pydis_site/apps/content/resources/server-info/roles.md +++ /dev/null @@ -1,133 +0,0 @@ ---- -title: Python Discord Server Roles -description: Information on the various roles at Python Discord. -icon: fab fa-discord ---- - -# Basic Roles - -### <span class="fas fa-circle" style="color:#6e6e6e"></span> Announcements -**Description:** A role pinged by Admins when an announcement is made in the `#announcements` channel. - -**How to get it:** Run the command `!subscribe` in the `#bot-commands` channel. -To unsubscribe, run `!unsubscribe` in the `#bot-commands` channel. - - -### <span class="fas fa-circle" style="color:#6e6e6e"></span> Voice Verified -**Description:** A role that lets people speak in voice channels. - -**How to get it:** Send `!voiceverify` in the `#voice-verification` channel. -There are multiple requirements listed there for getting the role. - ---- - -# Server Support Roles - -### <span class="fas fa-circle" style="color:#55cc6c"></span> Contributors -**Description:** A role given by staff to people who make substantial contributions to any of the server's [open source repositories](https://github.com/python-discord/) (Sir Lancebot, Python, the site, the branding repo, etc..).<br> -This includes writing pull requests for open issues, and also for reviewing open pull requests (**we really need reviewers!**) - -**How to get it:** Contribute to the projects! -It’s difficult to precisely quantify contributions, but we’ve come up with the following guidelines for the role: - -- The member has made several significant contributions to our projects. -- The member has a positive influence in our contributors subcommunity. - -The role will be assigned at the discretion of the Admin Team in consultation with the Core Developers Team. Check out our [walkthrough](/pages/contributing/) to get started contributing. - ---- - -# Financial Support Roles - -### <span class="fas fa-circle" style="color:#46e6e8"></span> Nitro Boosters -**Description:** A vanity role for people who boost the server with their nitro subscription. - -**How to get it:** Boost the server with a nitro subscription. - - -### <span class="fas fa-circle" style="color:#46e6e8"></span> <span class="fas fa-circle" style="color:#3e7be9"></span> <span class="fas fa-circle" style="color:#2a82bd"></span> Patrons -**Description:** A vanity role for Patreon patrons of the server. - -**How to get it:** [Become a patron here!](https://www.patreon.com/python_discord) - ---- - -# Staff Roles -#### Note regarding staff roles: -##### Q: How do I apply for Helper/Moderator/Admin? -There is no application, and there are no public nominations. Staff keep an eye out for potential candidates, and people nominated (by staff) are put in a pool for evaluation. After a period of time the candidate for a certain role is voted on by staff higher up the hierarchy. - -##### Q: How do I become Helper? -See the description of a Helper. Being active in helping others, providing good help, contributing to our projects, and abiding by our rules go a long way towards catching staff attention, and make the server a better place for both beginners and advanced Python devs. - -##### Role Expectations -In addition to the informal descriptions below, we've also written down a more formal list of expectations that come with each staff role. While this list is mostly for internal use, you can read it [here](/pages/server-info/staff-role-expectations/). - -### <span class="fas fa-circle" style="color:#f85950"></span> Owners -**Description:** Owners of the server. - -### <span class="fas fa-circle" style="color:#ff784d"></span> Admins -**Description:** Staff who administrate the server, its function, its staff, and are involved in deciding the direction of the server. - -### <span class="fas fa-circle" style="color:#1abc9c"></span> Domain Leads -**Description:** Staff in charge of a certain domain such as moderation, events, and outreach. A lead will have a second role specifying their domain. - -### <span class="fas fa-circle" style="color:#00aeb4"></span> Project Leads -**Description:** Staff in charge of a certain project that require special attention, such as a YouTube video series or our new forms page. - -### <span class="fas fa-circle" style="color:#ff9f1b"></span> Moderators -**Description:** Staff who moderate the server, enforce the rules, and coordinate with staff to support the server. - -### <span class="fas fa-circle" style="color:#a1d1ff"></span> PyDis Core Developers -**Description:** A role for staff who are critical contributors to the server's core projects, like the [bot](https://github.com/python-discord/bot) and the [site](https://github.com/python-discord/site), and are in charge of managing the repositories. - -### <span class="fas fa-circle" style="color:#a1d1ff"></span> DevOps -**Description:** A role for staff involved with the DevOps toolchain of our core projects. - -### <span class="fas fa-circle" style="color:#7de29c"></span> Events Team -**Description:** The events team are staff members who help plan and execute Python Discord events. This can range from the Code Jam, to Pixels, to our survey, specific workshops we want to run, and more. - -### <span class="fas fa-circle" style="color:#eecd36"></span> Helpers -**Description:** This is the core staff role in our organization: All staff members have the Helpers role. - -In general, being a helper means that you provide substantial help for the server's function, and have a good understanding of the culture and rules of the server. - -Helpers assist in the help channels, demonstrate proficiency in the language, and have strong teaching and explanation skills. -Otherwise they might assist in other areas of the organization, such as being a core developer, events team member, or moderator. - -Being a helper is also more than just quantity of messages, it's about quality. We watch and we pick these people out of the crowd, because we believe that they're a valuable asset to the community, and want our users to know that they're someone that can be relied on for answers and help. - ---- - -# Code Jam Roles -### <span class="fas fa-circle" style="color:#f87dc8"></span> Code Jam Champions -**Description:** A vanity role for winners of past code jams. - -**How to get it:** Win a code jam! - - -### <span class="fas fa-circle" style="color:#28866c"></span> Code Jam Leaders -**Description:** A temporary role for the duration of a code jam given to team leaders. - -**How to get it:** Team leaders are picked from the participants by the Events Team, and assigned for the duration of a jam. - - -### <span class="fas fa-circle" style="color:#229939"></span> Code Jam Participants -**Description:** A temporary role for the duration of a code jam given to participants. - -**How to get it:** Qualify for and participate in a code jam. - -*Note: Similar roles may exist for a game jam.* - - ---- - -# Miscellaneous Roles - -### <span class="fas fa-circle" style="color:#9f3fee"></span> Partners -**Description:** Representatives of communities we are partnered with. - -### <span class="fas fa-circle" style="color:#c77cfa"></span> Python Community -**Description:** Prominent people in the Python ecosystem. -Typically this will be people who have written books, people who speak at PyCon, YouTube content creators, podcasters, or notable contributors to a Python runtime or a major Python module. -These members will have a meta role attached to further explain why they have this role, for example `CPython: Core Developer`. diff --git a/pydis_site/apps/content/resources/server-info/staff-role-expectations.md b/pydis_site/apps/content/resources/server-info/staff-role-expectations.md deleted file mode 100644 index 286386d7..00000000 --- a/pydis_site/apps/content/resources/server-info/staff-role-expectations.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: Staff Role Expectations -description: List of expectations that come with being on the staff team at Python Discord. -icon: fab fa-discord ---- - -This page has a list of expectations that come with having a certain staff role in our community. -While the term "expectations" may sound a bit formal, it's important to keep in mind that everyone with a staff role is just a volunteer and that this list is a way of having a clear overview of what each role entails. - -This document is mostly meant for internal reference. -If you want a more informal description of each staff role, take a look at our [roles page](/pages/server-info/roles/#staff-roles). - -## Expectations - -### <span class="fas fa-circle" style="color:#eecd36"></span> Helpers - -* In general, helpers participate in Python-related channels (e.g. Help Channels, Topical Channels) and help other members of our community. -* Helpers may also help the community by taking up organizational tasks. -* There are no real requirements for the level of activity a helper has to have, although we do expect their activity level to be more than "nothing". - -### <span class="fas fa-circle" style="color:#ff9f1b"></span> Moderators - -* Moderators moderate our community and participate in moderation discussions in our moderators channel. -* While moderators don't need to have high levels of activity, we do expect some form of consistent activity. -This means that consistently being active a few times a month is better than having one day with a lot of activity per year. -Having some kind of consistent activity helps moderators bond with the rest of the moderation team and helps them to stay up to date with the moderation policy. -* **Moderators are not required to fulfill the helper criteria in addition to this,** although it's is obviously appreciated if they do. - -### <span class="fas fa-circle" style="color:#ff784d"></span> Admins - -* Admins are expected to work on tasks that directly improve the community on a regular basis. -* Examples of these tasks include: - * Doing pull request reviews; - * Being involved in events; - * Overseeing road map items; - * Solving critical issues; - * Handling raids; - * Joining our meetings (if in a compatible timezone); - * Actioning issues on the organisation repo; - * Improving our infrastructure; - * Writing documentation or guides; - * Recruiting and on-boarding new staff members; - * Calling staff votes for nominees; - * Having one-on-ones with moderators. -* Admins are also expected to keep each other updated on the status of the tasks they are working on. - -### <span class="fas fa-circle" style="color:#f85950"></span> Owners - -**In addition to** the regular Admin criteria, Owners also have to: - -* Join staff/admin meetings as often as possible and lead those meetings. -* Help identify the most critical tasks and try to distribute them among the Admins during the weekly Admin meeting. -* Make sure that no one is "blocked" in performing their tasks. -* Ensure that the community isn’t neglecting important responsibilities. -* Manage partnerships, sponsorships and speak on behalf of the community in public settings. - ---- - -## Staff Management -First of all, it's important to appreciate that everything staff members do in this community is voluntary and the expectations listed above are not meant to change that. -**This means it's absolutely fine for all staff members to take breaks or vacations from their activities in the community when they need to.** -We will never hold it against someone if they are temporarily away from their responsibilities. - -At the same time, it's only natural for a community like ours that there's some amount of staff turnover as personal interests and circumstances change. -Going forward, we will periodically review the activity of individual staff members and open a dialogue with staff members who are currently not meeting the expectations to see what can be done. -It might happen that we come to conclusion that it's better for a staff member to step down from their current position. -Do note that there are no hard feelings involved if that happens; we just want to make sure that the current staffing reflects the people who are still interested in volunteering in this community. diff --git a/pydis_site/apps/content/resources/tags/_info.yml b/pydis_site/apps/content/resources/tags/_info.yml deleted file mode 100644 index 054125ec..00000000 --- a/pydis_site/apps/content/resources/tags/_info.yml +++ /dev/null @@ -1,3 +0,0 @@ -title: Tags -description: Useful snippets that are often used in the server. -icon: fas fa-tags diff --git a/pydis_site/apps/content/tests/__init__.py b/pydis_site/apps/content/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pydis_site/apps/content/tests/__init__.py +++ /dev/null diff --git a/pydis_site/apps/content/tests/helpers.py b/pydis_site/apps/content/tests/helpers.py deleted file mode 100644 index fad91050..00000000 --- a/pydis_site/apps/content/tests/helpers.py +++ /dev/null @@ -1,102 +0,0 @@ -import atexit -import shutil -import tempfile -from pathlib import Path - -from django.test import TestCase - - -BASE_PATH = Path(tempfile.mkdtemp(prefix='pydis-site-content-app-tests-')) -atexit.register(shutil.rmtree, BASE_PATH, ignore_errors=True) - - -# Valid markdown content with YAML metadata -MARKDOWN_WITH_METADATA = """ ---- -title: TestTitle -description: TestDescription -relevant_links: - Python Discord: https://pythondiscord.com - Discord: https://discord.com -toc: 0 ---- -# This is a header. -""" - -MARKDOWN_WITHOUT_METADATA = """#This is a header.""" - -# Valid YAML in a _info.yml file -CATEGORY_INFO = """ -title: Category Name -description: Description -""" - -# The HTML generated from the above markdown data -PARSED_HTML = ( - '<h1 id="this-is-a-header">This is a header.' - '<a class="headerlink" href="#this-is-a-header" title="Permanent link">¶</a></h1>' -) - -# The YAML metadata parsed from the above markdown data -PARSED_METADATA = { - "title": "TestTitle", "description": "TestDescription", - "relevant_links": { - "Python Discord": "https://pythondiscord.com", - "Discord": "https://discord.com" - }, - "toc": 0 -} - -# The YAML data parsed from the above _info.yml file -PARSED_CATEGORY_INFO = {"title": "Category Name", "description": "Description"} - - -class MockPagesTestCase(TestCase): - """ - TestCase with a fake filesystem for testing. - - Structure (relative to BASE_PATH): - ├── _info.yml - ├── root.md - ├── root_without_metadata.md - ├── not_a_page.md - ├── tmp.md - ├── tmp - | ├── _info.yml - | └── category - | ├── _info.yml - | └── subcategory_without_info - └── category - ├── _info.yml - ├── with_metadata.md - └── subcategory - ├── with_metadata.md - └── without_metadata.md - """ - - def setUp(self): - """Create the fake filesystem.""" - Path(f"{BASE_PATH}/_info.yml").write_text(CATEGORY_INFO) - Path(f"{BASE_PATH}/root.md").write_text(MARKDOWN_WITH_METADATA) - Path(f"{BASE_PATH}/root_without_metadata.md").write_text(MARKDOWN_WITHOUT_METADATA) - Path(f"{BASE_PATH}/not_a_page.md").mkdir(exist_ok=True) - Path(f"{BASE_PATH}/not_a_page.md/_info.yml").write_text(CATEGORY_INFO) - Path(f"{BASE_PATH}/category").mkdir(exist_ok=True) - Path(f"{BASE_PATH}/category/_info.yml").write_text(CATEGORY_INFO) - Path(f"{BASE_PATH}/category/with_metadata.md").write_text(MARKDOWN_WITH_METADATA) - Path(f"{BASE_PATH}/category/subcategory").mkdir(exist_ok=True) - Path(f"{BASE_PATH}/category/subcategory/_info.yml").write_text(CATEGORY_INFO) - Path( - f"{BASE_PATH}/category/subcategory/with_metadata.md" - ).write_text(MARKDOWN_WITH_METADATA) - Path( - f"{BASE_PATH}/category/subcategory/without_metadata.md" - ).write_text(MARKDOWN_WITHOUT_METADATA) - - temp = f"{BASE_PATH}/tmp" # noqa: S108 - Path(f"{temp}").mkdir(exist_ok=True) - Path(f"{temp}/_info.yml").write_text(CATEGORY_INFO) - Path(f"{temp}.md").write_text(MARKDOWN_WITH_METADATA) - Path(f"{temp}/category").mkdir(exist_ok=True) - Path(f"{temp}/category/_info.yml").write_text(CATEGORY_INFO) - Path(f"{temp}/category/subcategory_without_info").mkdir(exist_ok=True) diff --git a/pydis_site/apps/content/tests/test_utils.py b/pydis_site/apps/content/tests/test_utils.py deleted file mode 100644 index 462818b5..00000000 --- a/pydis_site/apps/content/tests/test_utils.py +++ /dev/null @@ -1,385 +0,0 @@ -import datetime -import json -import tarfile -import tempfile -import textwrap -from pathlib import Path -from unittest import mock - -import httpx -import markdown -from django.http import Http404 -from django.test import TestCase - -from pydis_site import settings -from pydis_site.apps.content import models, utils -from pydis_site.apps.content.tests.helpers import ( - BASE_PATH, MockPagesTestCase, PARSED_CATEGORY_INFO, PARSED_HTML, PARSED_METADATA -) - -_time = datetime.datetime(2022, 10, 10, 10, 10, 10, tzinfo=datetime.timezone.utc) -_time_str = _time.strftime(settings.GITHUB_TIMESTAMP_FORMAT) -TEST_COMMIT_KWARGS = { - "sha": "123", - "message": "Hello world\n\nThis is a commit message", - "date": _time, - "authors": json.dumps([ - {"name": "Author 1", "email": "[email protected]", "date": _time_str}, - {"name": "Author 2", "email": "[email protected]", "date": _time_str}, - ]), -} - - -class GetCategoryTests(MockPagesTestCase): - """Tests for the get_category function.""" - - def test_get_valid_category(self): - result = utils.get_category(Path(BASE_PATH, "category")) - - self.assertEqual(result, {"title": "Category Name", "description": "Description"}) - - def test_get_nonexistent_category(self): - with self.assertRaises(Http404): - utils.get_category(Path(BASE_PATH, "invalid")) - - def test_get_category_with_path_to_file(self): - # Valid categories are directories, not files - with self.assertRaises(Http404): - utils.get_category(Path(BASE_PATH, "root.md")) - - def test_get_category_without_info_yml(self): - # Categories should provide an _info.yml file - with self.assertRaises(FileNotFoundError): - utils.get_category(Path(BASE_PATH, "tmp/category/subcategory_without_info")) - - -class GetCategoriesTests(MockPagesTestCase): - """Tests for the get_categories function.""" - - def test_get_root_categories(self): - result = utils.get_categories(BASE_PATH) - - info = PARSED_CATEGORY_INFO - categories = { - "category": info, - "tmp": info, - "not_a_page.md": info, - } - self.assertEqual(result, categories) - - def test_get_categories_with_subcategories(self): - result = utils.get_categories(Path(BASE_PATH, "category")) - - self.assertEqual(result, {"subcategory": PARSED_CATEGORY_INFO}) - - def test_get_categories_without_subcategories(self): - result = utils.get_categories(Path(BASE_PATH, "category/subcategory")) - - self.assertEqual(result, {}) - - -class GetCategoryPagesTests(MockPagesTestCase): - """Tests for the get_category_pages function.""" - - def test_get_pages_in_root_category_successfully(self): - """The method should successfully retrieve page metadata.""" - root_category_pages = utils.get_category_pages(BASE_PATH) - self.assertEqual( - root_category_pages, {"root": PARSED_METADATA, "root_without_metadata": {}} - ) - - def test_get_pages_in_subcategories_successfully(self): - """The method should successfully retrieve page metadata.""" - category_pages = utils.get_category_pages(Path(BASE_PATH, "category")) - - # Page metadata is properly retrieved - self.assertEqual(category_pages, {"with_metadata": PARSED_METADATA}) - - -class GetPageTests(MockPagesTestCase): - """Tests for the get_page function.""" - - def test_get_page(self): - # TOC is a special case because the markdown converter outputs the TOC as HTML - updated_metadata = {**PARSED_METADATA, "toc": '<div class="toc">\n<ul></ul>\n</div>\n'} - cases = [ - ("Root page with metadata", "root.md", PARSED_HTML, updated_metadata), - ("Root page without metadata", "root_without_metadata.md", PARSED_HTML, {}), - ("Page with metadata", "category/with_metadata.md", PARSED_HTML, updated_metadata), - ("Page without metadata", "category/subcategory/without_metadata.md", PARSED_HTML, {}), - ] - - for msg, page_path, expected_html, expected_metadata in cases: - with self.subTest(msg=msg): - html, metadata = utils.get_page(Path(BASE_PATH, page_path)) - self.assertEqual(html, expected_html) - self.assertEqual(metadata, expected_metadata) - - def test_get_nonexistent_page_returns_404(self): - with self.assertRaises(Http404): - utils.get_page(Path(BASE_PATH, "invalid")) - - -class TagUtilsTests(TestCase): - """Tests for the tag-related utilities.""" - - def setUp(self) -> None: - super().setUp() - self.commit = models.Commit.objects.create(**TEST_COMMIT_KWARGS) - - @mock.patch.object(utils, "fetch_tags") - def test_static_fetch(self, fetch_mock: mock.Mock): - """Test that the static fetch function is only called at most once during static builds.""" - tags = [models.Tag(name="Name", body="body")] - fetch_mock.return_value = tags - result = utils.get_tags_static() - second_result = utils.get_tags_static() - - fetch_mock.assert_called_once() - self.assertEqual(tags, result) - self.assertEqual(tags, second_result) - - @mock.patch("httpx.Client.get") - def test_mocked_fetch(self, get_mock: mock.Mock): - """Test that proper data is returned from fetch, but with a mocked API response.""" - fake_request = httpx.Request("GET", "https://google.com") - - # Metadata requests - returns = [httpx.Response( - request=fake_request, - status_code=200, - json=[ - {"type": "file", "name": "first_tag.md", "sha": "123"}, - {"type": "file", "name": "second_tag.md", "sha": "456"}, - {"type": "dir", "name": "some_group", "sha": "789", "url": "/some_group"}, - ] - ), httpx.Response( - request=fake_request, - status_code=200, - json=[{"type": "file", "name": "grouped_tag.md", "sha": "789123"}] - )] - - # Main content request - bodies = ( - "This is the first tag!", - textwrap.dedent(""" - --- - frontmatter: empty - --- - This tag has frontmatter! - """), - "This is a grouped tag!", - ) - - # Generate a tar archive with a few tags - with tempfile.TemporaryDirectory() as tar_folder: - tar_folder = Path(tar_folder) - with tempfile.TemporaryDirectory() as folder: - folder = Path(folder) - (folder / "ignored_file.md").write_text("This is an ignored file.") - tags_folder = folder / "bot/resources/tags" - tags_folder.mkdir(parents=True) - - (tags_folder / "first_tag.md").write_text(bodies[0]) - (tags_folder / "second_tag.md").write_text(bodies[1]) - - group_folder = tags_folder / "some_group" - group_folder.mkdir() - (group_folder / "grouped_tag.md").write_text(bodies[2]) - - with tarfile.open(tar_folder / "temp.tar", "w") as file: - file.add(folder, recursive=True) - - body = (tar_folder / "temp.tar").read_bytes() - - returns.append(httpx.Response( - status_code=200, - content=body, - request=fake_request, - )) - - get_mock.side_effect = returns - result = utils.fetch_tags() - - def sort(_tag: models.Tag) -> str: - return _tag.name - - self.assertEqual(sorted([ - models.Tag(name="first_tag", body=bodies[0], sha="123"), - models.Tag(name="second_tag", body=bodies[1], sha="245"), - models.Tag(name="grouped_tag", body=bodies[2], group=group_folder.name, sha="789123"), - ], key=sort), sorted(result, key=sort)) - - def test_get_real_tag(self): - """Test that a single tag is returned if it exists.""" - tag = models.Tag.objects.create(name="real-tag", last_commit=self.commit) - result = utils.get_tag("real-tag") - - self.assertEqual(tag, result) - - def test_get_grouped_tag(self): - """Test fetching a tag from a group.""" - tag = models.Tag.objects.create( - name="real-tag", group="real-group", last_commit=self.commit - ) - result = utils.get_tag("real-group/real-tag") - - self.assertEqual(tag, result) - - def test_get_group(self): - """Test fetching a group of tags.""" - included = [ - models.Tag.objects.create(name="tag-1", group="real-group"), - models.Tag.objects.create(name="tag-2", group="real-group"), - models.Tag.objects.create(name="tag-3", group="real-group"), - ] - - models.Tag.objects.create(name="not-included-1") - models.Tag.objects.create(name="not-included-2", group="other-group") - - result = utils.get_tag("real-group") - self.assertListEqual(included, result) - - def test_get_tag_404(self): - """Test that an error is raised when we fetch a non-existing tag.""" - models.Tag.objects.create(name="real-tag") - with self.assertRaises(models.Tag.DoesNotExist): - utils.get_tag("fake") - - @mock.patch.object(utils, "get_tag_category") - def test_category_pages(self, get_mock: mock.Mock): - """Test that the category pages function calls the correct method for tags.""" - tag = models.Tag.objects.create(name="tag") - get_mock.return_value = tag - result = utils.get_category_pages(settings.CONTENT_PAGES_PATH / "tags") - self.assertEqual(tag, result) - get_mock.assert_called_once_with(collapse_groups=True) - - def test_get_category_root(self): - """Test that all tags are returned and formatted properly for the tag root page.""" - body = "normal body" - base = {"description": markdown.markdown(body), "icon": "fas fa-tag"} - - models.Tag.objects.create(name="tag-1", body=body), - models.Tag.objects.create(name="tag-2", body=body), - models.Tag.objects.create(name="tag-3", body=body), - - models.Tag.objects.create(name="tag-4", body=body, group="tag-group") - models.Tag.objects.create(name="tag-5", body=body, group="tag-group") - - result = utils.get_tag_category(collapse_groups=True) - - self.assertDictEqual({ - "tag-1": {**base, "title": "tag-1"}, - "tag-2": {**base, "title": "tag-2"}, - "tag-3": {**base, "title": "tag-3"}, - "tag-group": { - "title": "tag-group", - "description": "Contains the following tags: tag-4, tag-5", - "icon": "fas fa-tags" - } - }, result) - - def test_get_category_group(self): - """Test the function for a group root page.""" - body = "normal body" - base = {"description": markdown.markdown(body), "icon": "fas fa-tag"} - - included = [ - models.Tag.objects.create(name="tag-1", body=body, group="group"), - models.Tag.objects.create(name="tag-2", body=body, group="group"), - ] - models.Tag.objects.create(name="not-included", body=body) - - result = utils.get_tag_category(included, collapse_groups=False) - self.assertDictEqual({ - "tag-1": {**base, "title": "tag-1"}, - "tag-2": {**base, "title": "tag-2"}, - }, result) - - def test_tag_url(self): - """Test that tag URLs are generated correctly.""" - cases = [ - ({"name": "tag"}, f"{models.Tag.URL_BASE}/tag.md"), - ({"name": "grouped", "group": "abc"}, f"{models.Tag.URL_BASE}/abc/grouped.md"), - ] - - for options, url in cases: - tag = models.Tag(**options) - with self.subTest(tag=tag): - self.assertEqual(url, tag.url) - - @mock.patch("httpx.Client.get") - def test_get_tag_commit(self, get_mock: mock.Mock): - """Test the get commit function with a normal tag.""" - tag = models.Tag.objects.create(name="example") - - authors = json.loads(self.commit.authors) - - get_mock.return_value = httpx.Response( - request=httpx.Request("GET", "https://google.com"), - status_code=200, - json=[{ - "sha": self.commit.sha, - "commit": { - "message": self.commit.message, - "author": authors[0], - "committer": authors[1], - } - }] - ) - - result = utils.get_tag(tag.name) - self.assertEqual(tag, result) - - get_mock.assert_called_once() - call_params = get_mock.call_args[1]["params"] - - self.assertEqual({"path": "/bot/resources/tags/example.md"}, call_params) - self.assertEqual(self.commit, models.Tag.objects.get(name=tag.name).last_commit) - - @mock.patch("httpx.Client.get") - def test_get_group_tag_commit(self, get_mock: mock.Mock): - """Test the get commit function with a group tag.""" - tag = models.Tag.objects.create(name="example", group="group-name") - - authors = json.loads(self.commit.authors) - authors.pop() - self.commit.authors = json.dumps(authors) - self.commit.save() - - get_mock.return_value = httpx.Response( - request=httpx.Request("GET", "https://google.com"), - status_code=200, - json=[{ - "sha": self.commit.sha, - "commit": { - "message": self.commit.message, - "author": authors[0], - "committer": authors[0], - } - }] - ) - - utils.set_tag_commit(tag) - - get_mock.assert_called_once() - call_params = get_mock.call_args[1]["params"] - - self.assertEqual({"path": "/bot/resources/tags/group-name/example.md"}, call_params) - self.assertEqual(self.commit, models.Tag.objects.get(name=tag.name).last_commit) - - @mock.patch.object(utils, "set_tag_commit") - def test_exiting_commit(self, set_commit_mock: mock.Mock): - """Test that a commit is saved when the data has not changed.""" - tag = models.Tag.objects.create(name="tag-name", body="old body", last_commit=self.commit) - - # This is only applied to the object, not to the database - tag.last_commit = None - - utils.record_tags([tag]) - self.assertEqual(self.commit, tag.last_commit) - - result = utils.get_tag("tag-name") - self.assertEqual(tag, result) - set_commit_mock.assert_not_called() diff --git a/pydis_site/apps/content/tests/test_views.py b/pydis_site/apps/content/tests/test_views.py deleted file mode 100644 index 3ef9bcc4..00000000 --- a/pydis_site/apps/content/tests/test_views.py +++ /dev/null @@ -1,402 +0,0 @@ -import textwrap -from pathlib import Path -from unittest import TestCase - -import django.test -import markdown -from django.http import Http404 -from django.test import RequestFactory, SimpleTestCase, override_settings -from django.urls import reverse - -from pydis_site.apps.content.models import Commit, Tag -from pydis_site.apps.content.tests.helpers import ( - BASE_PATH, MockPagesTestCase, PARSED_CATEGORY_INFO, PARSED_HTML, PARSED_METADATA -) -from pydis_site.apps.content.tests.test_utils import TEST_COMMIT_KWARGS -from pydis_site.apps.content.views import PageOrCategoryView - - -def patch_dispatch_attributes(view: PageOrCategoryView, location: str) -> None: - """ - Set the attributes set in the `dispatch` method manually. - - This is necessary because it is never automatically called during tests. - """ - view.location = Path(BASE_PATH, location) - - # URL location on the filesystem - view.full_location = view.location - - # Possible places to find page content information - view.category_path = view.full_location - view.page_path = view.full_location.with_suffix(".md") - - -@override_settings(CONTENT_PAGES_PATH=BASE_PATH) -class PageOrCategoryViewTests(MockPagesTestCase, SimpleTestCase, TestCase): - """Tests for the PageOrCategoryView class.""" - - def setUp(self): - """Set test helpers, then set up fake filesystem.""" - self.factory = RequestFactory() - self.view = PageOrCategoryView.as_view() - self.ViewClass = PageOrCategoryView() - super().setUp() - - # Integration tests - def test_valid_page_or_category_returns_200(self): - cases = [ - ("Page at root", "root"), - ("Category page", "category"), - ("Page in category", "category/with_metadata"), - ("Subcategory page", "category/subcategory"), - ("Page in subcategory", "category/subcategory/with_metadata"), - ] - for msg, path in cases: - with self.subTest(msg=msg, path=path): - request = self.factory.get(f"/{path}") - response = self.view(request, location=path) - self.assertEqual(response.status_code, 200) - - def test_nonexistent_page_returns_404(self): - with self.assertRaises(Http404): - request = self.factory.get("/invalid") - self.view(request, location="invalid") - - # Unit tests - def test_get_template_names_returns_correct_templates(self): - category_template = "content/listing.html" - page_template = "content/page.html" - cases = [ - ("root", page_template), - ("root_without_metadata", page_template), - ("category/with_metadata", page_template), - ("category/subcategory/with_metadata", page_template), - ("category", category_template), - ("category/subcategory", category_template), - ] - - for path, expected_template in cases: - with self.subTest(path=path, expected_template=expected_template): - patch_dispatch_attributes(self.ViewClass, path) - self.assertEqual(self.ViewClass.get_template_names(), [expected_template]) - - def test_get_template_names_with_nonexistent_paths_returns_404(self): - for path in ("invalid", "another_invalid", "nonexistent"): - with self.subTest(path=path): - patch_dispatch_attributes(self.ViewClass, path) - with self.assertRaises(Http404): - self.ViewClass.get_template_names() - - def test_get_template_names_returns_page_template_for_category_with_page(self): - """Make sure the proper page is returned for category locations with pages.""" - patch_dispatch_attributes(self.ViewClass, "tmp") - self.assertEqual(self.ViewClass.get_template_names(), ["content/page.html"]) - - def test_get_context_data_with_valid_page(self): - """The method should return required fields in the template context.""" - request = self.factory.get("/root") - self.ViewClass.setup(request) - self.ViewClass.dispatch(request, location="root") - - cases = [ - ("Context includes HTML page content", "page", PARSED_HTML), - ("Context includes page title", "page_title", PARSED_METADATA["title"]), - ( - "Context includes page description", - "page_description", - PARSED_METADATA["description"] - ), - ( - "Context includes relevant link names and URLs", - "relevant_links", - PARSED_METADATA["relevant_links"] - ), - ] - context = self.ViewClass.get_context_data() - for msg, key, expected_value in cases: - with self.subTest(msg=msg): - self.assertEqual(context[key], expected_value) - - def test_get_context_data_with_valid_category(self): - """The method should return required fields in the template context.""" - request = self.factory.get("/category") - self.ViewClass.setup(request) - self.ViewClass.dispatch(request, location="category") - - cases = [ - ( - "Context includes subcategory names and their information", - "categories", - {"subcategory": PARSED_CATEGORY_INFO} - ), - ( - "Context includes page names and their metadata", - "pages", - {"with_metadata": PARSED_METADATA} - ), - ( - "Context includes page description", - "page_description", - PARSED_CATEGORY_INFO["description"] - ), - ("Context includes page title", "page_title", PARSED_CATEGORY_INFO["title"]), - ] - - context = self.ViewClass.get_context_data() - for msg, key, expected_value in cases: - with self.subTest(msg=msg): - self.assertEqual(context[key], expected_value) - - def test_get_context_data_for_category_with_page(self): - """Make sure the proper page is returned for category locations with pages.""" - request = self.factory.get("/category") - self.ViewClass.setup(request) - self.ViewClass.dispatch(request, location="tmp") - - context = self.ViewClass.get_context_data() - expected_page_context = { - "page": PARSED_HTML, - "page_title": PARSED_METADATA["title"], - "page_description": PARSED_METADATA["description"], - "relevant_links": PARSED_METADATA["relevant_links"], - "subarticles": [{"path": "category", "name": "Category Name"}] - } - for key, expected_value in expected_page_context.items(): - with self.subTest(): - self.assertEqual(context[key], expected_value) - - def test_get_context_data_breadcrumbs(self): - """The method should return correct breadcrumbs.""" - request = self.factory.get("/category/subcategory/with_metadata") - self.ViewClass.setup(request) - self.ViewClass.dispatch(request, location="category/subcategory/with_metadata") - - context = self.ViewClass.get_context_data() - - # Convert to paths to avoid dealing with non-standard path separators - for item in context["breadcrumb_items"]: - item["path"] = Path(item["path"]) - - self.assertEqual( - context["breadcrumb_items"], - [ - {"name": PARSED_CATEGORY_INFO["title"], "path": Path(".")}, - {"name": PARSED_CATEGORY_INFO["title"], "path": Path("category")}, - {"name": PARSED_CATEGORY_INFO["title"], "path": Path("category/subcategory")}, - ] - ) - - -class TagViewTests(django.test.TestCase): - """Tests for the TagView class.""" - - def setUp(self): - """Set test helpers, then set up fake filesystem.""" - super().setUp() - self.commit = Commit.objects.create(**TEST_COMMIT_KWARGS) - - def test_routing(self): - """Test that the correct template is returned for each route.""" - Tag.objects.create(name="example", last_commit=self.commit) - Tag.objects.create(name="grouped-tag", group="group-name", last_commit=self.commit) - - cases = [ - ("/pages/tags/example/", "content/tag.html"), - ("/pages/tags/group-name/", "content/listing.html"), - ("/pages/tags/group-name/grouped-tag/", "content/tag.html"), - ] - - for url, template in cases: - with self.subTest(url=url): - response = self.client.get(url) - self.assertEqual(200, response.status_code) - self.assertTemplateUsed(response, template) - - def test_valid_tag_returns_200(self): - """Test that a page is returned for a valid tag.""" - Tag.objects.create(name="example", body="This is the tag body.", last_commit=self.commit) - response = self.client.get("/pages/tags/example/") - self.assertEqual(200, response.status_code) - self.assertIn("This is the tag body", response.content.decode("utf-8")) - self.assertTemplateUsed(response, "content/tag.html") - - def test_invalid_tag_404(self): - """Test that a tag which doesn't exist raises a 404.""" - response = self.client.get("/pages/tags/non-existent/") - self.assertEqual(404, response.status_code) - - def test_context_tag(self): - """Test that the context contains the required data for a tag.""" - body = textwrap.dedent(""" - --- - unused: frontmatter - ---- - Tag content here. - """) - - tag = Tag.objects.create(name="example", body=body, last_commit=self.commit) - response = self.client.get("/pages/tags/example/") - expected = { - "page_title": "example", - "page": markdown.markdown("Tag content here."), - "tag": tag, - "breadcrumb_items": [ - {"name": "Pages", "path": "."}, - {"name": "Tags", "path": "tags"}, - ] - } - for key in expected: - self.assertEqual( - expected[key], response.context.get(key), f"context.{key} did not match" - ) - - def test_context_grouped_tag(self): - """ - Test the context for a tag in a group. - - The only difference between this and a regular tag are the breadcrumbs, - so only those are checked. - """ - Tag.objects.create( - name="example", body="Body text", group="group-name", last_commit=self.commit - ) - response = self.client.get("/pages/tags/group-name/example/") - self.assertListEqual([ - {"name": "Pages", "path": "."}, - {"name": "Tags", "path": "tags"}, - {"name": "group-name", "path": "tags/group-name"}, - ], response.context.get("breadcrumb_items")) - - def test_group_page(self): - """Test rendering of a group's root page.""" - Tag.objects.create(name="tag-1", body="Body 1", group="group-name", last_commit=self.commit) - Tag.objects.create(name="tag-2", body="Body 2", group="group-name", last_commit=self.commit) - Tag.objects.create(name="not-included", last_commit=self.commit) - - response = self.client.get("/pages/tags/group-name/") - content = response.content.decode("utf-8") - - self.assertInHTML("<div class='level-left'>group-name</div>", content) - self.assertInHTML( - f"<a class='level-item fab fa-github' href='{Tag.URL_BASE}/group-name'>", - content - ) - self.assertIn(">tag-1</span>", content) - self.assertIn(">tag-2</span>", content) - self.assertNotIn( - ">not-included</span>", - content, - "Tags not in this group shouldn't be rendered." - ) - - self.assertInHTML("<p>Body 1</p>", content) - - def test_markdown(self): - """Test that markdown content is rendered properly.""" - body = textwrap.dedent(""" - ```py - Hello world! - ``` - - **This text is in bold** - """) - - Tag.objects.create(name="example", body=body, last_commit=self.commit) - response = self.client.get("/pages/tags/example/") - content = response.content.decode("utf-8") - - self.assertInHTML('<code class="language-py">Hello world!</code>', content) - self.assertInHTML("<strong>This text is in bold</strong>", content) - - def test_embed(self): - """Test that an embed from the frontmatter is treated correctly.""" - body = textwrap.dedent(""" - --- - embed: - title: Embed title - image: - url: https://google.com - --- - Tag body. - """) - - Tag.objects.create(name="example", body=body, last_commit=self.commit) - response = self.client.get("/pages/tags/example/") - content = response.content.decode("utf-8") - - self.assertInHTML('<img alt="Embed title" src="https://google.com"/>', content) - self.assertInHTML("<p>Tag body.</p>", content) - - def test_embed_title(self): - """Test that the page title gets set to the embed title.""" - body = textwrap.dedent(""" - --- - embed: - title: Embed title - --- - """) - - Tag.objects.create(name="example", body=body, last_commit=self.commit) - response = self.client.get("/pages/tags/example/") - self.assertEqual( - "Embed title", - response.context.get("page_title"), - "The page title must match the embed title." - ) - - def test_hyperlinked_item(self): - """Test hyperlinking of tags works as intended.""" - filler_before, filler_after = "empty filler text\n\n", "more\nfiller" - body = filler_before + "`!tags return`" + filler_after - Tag.objects.create(name="example", body=body, last_commit=self.commit) - - other_url = reverse("content:tag", kwargs={"location": "return"}) - response = self.client.get("/pages/tags/example/") - self.assertEqual( - markdown.markdown(filler_before + f"[`!tags return`]({other_url})" + filler_after), - response.context.get("page") - ) - - def test_hyperlinked_group(self): - """Test hyperlinking with a group works as intended.""" - Tag.objects.create( - name="example", body="!tags group-name grouped-tag", last_commit=self.commit - ) - Tag.objects.create(name="grouped-tag", group="group-name") - - other_url = reverse("content:tag", kwargs={"location": "group-name/grouped-tag"}) - response = self.client.get("/pages/tags/example/") - self.assertEqual( - markdown.markdown(f"[!tags group-name grouped-tag]({other_url})"), - response.context.get("page") - ) - - def test_hyperlinked_extra_text(self): - """Test hyperlinking when a tag is followed by extra, unrelated text.""" - Tag.objects.create( - name="example", body="!tags other unrelated text", last_commit=self.commit - ) - Tag.objects.create(name="other") - - other_url = reverse("content:tag", kwargs={"location": "other"}) - response = self.client.get("/pages/tags/example/") - self.assertEqual( - markdown.markdown(f"[!tags other]({other_url}) unrelated text"), - response.context.get("page") - ) - - def test_tag_root_page(self): - """Test the root tag page which lists all tags.""" - Tag.objects.create(name="tag-1", last_commit=self.commit) - Tag.objects.create(name="tag-2", last_commit=self.commit) - Tag.objects.create(name="tag-3", last_commit=self.commit) - - response = self.client.get("/pages/tags/") - content = response.content.decode("utf-8") - - self.assertTemplateUsed(response, "content/listing.html") - self.assertInHTML('<div class="level-left">Tags</div>', content) - - for tag_number in range(1, 4): - self.assertIn(f"tag-{tag_number}</span>", content) diff --git a/pydis_site/apps/content/urls.py b/pydis_site/apps/content/urls.py deleted file mode 100644 index a7695a27..00000000 --- a/pydis_site/apps/content/urls.py +++ /dev/null @@ -1,69 +0,0 @@ -import typing -from pathlib import Path - -from django_distill import distill_path - -from . import utils, views - -app_name = "content" - - -def __get_all_files(root: Path, folder: typing.Optional[Path] = None) -> list[str]: - """Find all folders and markdown files recursively starting from `root`.""" - if not folder: - folder = root - - results = [] - - for item in folder.iterdir(): - name = item.relative_to(root).__str__().replace("\\", "/") - - if item.is_dir(): - results.append(name) - results.extend(__get_all_files(root, item)) - else: - path, extension = name.rsplit(".", maxsplit=1) - if extension == "md": - results.append(path) - - return results - - -DISTILL_RETURN = typing.Iterator[dict[str, str]] - - -def get_all_pages() -> DISTILL_RETURN: - """Yield a dict of all page categories.""" - for location in __get_all_files(Path("pydis_site", "apps", "content", "resources")): - yield {"location": location} - - -def get_all_tags() -> DISTILL_RETURN: - """Return all tag names and groups in static builds.""" - # We instantiate the set with None here to make filtering it out later easier - # whether it was added in the loop or not - groups = {None} - for tag in utils.get_tags_static(): - groups.add(tag.group) - yield {"location": (f"{tag.group}/" if tag.group else "") + tag.name} - - groups.remove(None) - for group in groups: - yield {"location": group} - - -urlpatterns = [ - distill_path("", views.PageOrCategoryView.as_view(), name='pages'), - distill_path( - "tags/<path:location>/", - views.TagView.as_view(), - name="tag", - distill_func=get_all_tags - ), - distill_path( - "<path:location>/", - views.PageOrCategoryView.as_view(), - name='page_category', - distill_func=get_all_pages - ), -] diff --git a/pydis_site/apps/content/utils.py b/pydis_site/apps/content/utils.py deleted file mode 100644 index c12893ef..00000000 --- a/pydis_site/apps/content/utils.py +++ /dev/null @@ -1,336 +0,0 @@ -import datetime -import functools -import json -import tarfile -import tempfile -from io import BytesIO -from pathlib import Path - -import frontmatter -import httpx -import markdown -import yaml -from django.http import Http404 -from django.utils import timezone -from markdown.extensions.toc import TocExtension - -from pydis_site import settings -from .models import Commit, Tag - -TAG_CACHE_TTL = datetime.timedelta(hours=1) - - -def github_client(**kwargs) -> httpx.Client: - """Get a client to access the GitHub API with important settings pre-configured.""" - client = httpx.Client( - base_url=settings.GITHUB_API, - follow_redirects=True, - timeout=settings.TIMEOUT_PERIOD, - **kwargs - ) - if settings.GITHUB_TOKEN: # pragma: no cover - if not client.headers.get("Authorization"): - client.headers = {"Authorization": f"token {settings.GITHUB_TOKEN}"} - - return client - - -def get_category(path: Path) -> dict[str, str]: - """Load category information by name from _info.yml.""" - if not path.is_dir(): - raise Http404("Category not found.") - - return yaml.safe_load(path.joinpath("_info.yml").read_text(encoding="utf-8")) - - -def get_categories(path: Path) -> dict[str, dict]: - """Get information for all categories.""" - categories = {} - - for item in path.iterdir(): - if item.is_dir(): - categories[item.name] = get_category(item) - - return categories - - -def get_tags_static() -> list[Tag]: - """ - Fetch tag information in static builds. - - This also includes some fake tags to preview the tag groups feature. - This will return a cached value, so it should only be used for static builds. - """ - tags = fetch_tags() - for tag in tags[3:5]: # pragma: no cover - tag.group = "very-cool-group" - return tags - - -def fetch_tags() -> list[Tag]: - """ - Fetch tag data from the GitHub API. - - The entire repository is downloaded and extracted locally because - getting file content would require one request per file, and can get rate-limited. - """ - with github_client() as client: - # Grab metadata - metadata = client.get("/repos/python-discord/bot/contents/bot/resources") - metadata.raise_for_status() - - hashes = {} - for entry in metadata.json(): - if entry["type"] == "dir": - # Tag group - files = client.get(entry["url"]) - files.raise_for_status() - files = files.json() - else: - files = [entry] - - for file in files: - hashes[file["name"]] = file["sha"] - - # Download the files - tar_file = client.get("/repos/python-discord/bot/tarball") - tar_file.raise_for_status() - - tags = [] - with tempfile.TemporaryDirectory() as folder: - with tarfile.open(fileobj=BytesIO(tar_file.content)) as repo: - included = [] - for file in repo.getmembers(): - if "/bot/resources/tags" in file.path: - included.append(file) - repo.extractall(folder, included) - - for tag_file in Path(folder).rglob("*.md"): - name = tag_file.name - group = None - if tag_file.parent.name != "tags": - # Tags in sub-folders are considered part of a group - group = tag_file.parent.name - - tags.append(Tag( - name=name.removesuffix(".md"), - sha=hashes[name], - group=group, - body=tag_file.read_text(encoding="utf-8"), - last_commit=None, - )) - - return tags - - -def set_tag_commit(tag: Tag) -> None: - """Fetch commit information from the API, and save it for the tag.""" - if settings.STATIC_BUILD: # pragma: no cover - # Static builds request every page during build, which can ratelimit it. - # Instead, we return some fake data. - tag.last_commit = Commit( - sha="68da80efc00d9932a209d5cccd8d344cec0f09ea", - message="Initial Commit\n\nTHIS IS FAKE DEMO DATA", - date=datetime.datetime(2018, 2, 3, 12, 20, 26, tzinfo=datetime.timezone.utc), - authors=json.dumps([{"name": "Joseph", "email": "[email protected]"}]), - ) - return - - path = "/bot/resources/tags" - if tag.group: - path += f"/{tag.group}" - path += f"/{tag.name}.md" - - # Fetch and set the commit - with github_client() as client: - data = client.get("/repos/python-discord/bot/commits", params={"path": path}) - data.raise_for_status() - data = data.json()[0] - - commit = data["commit"] - author, committer = commit["author"], commit["committer"] - - date = datetime.datetime.strptime(committer["date"], settings.GITHUB_TIMESTAMP_FORMAT) - date = date.replace(tzinfo=datetime.timezone.utc) - - if author["email"] == committer["email"]: - authors = [author] - else: - authors = [author, committer] - - commit_obj, _ = Commit.objects.get_or_create( - sha=data["sha"], - message=commit["message"], - date=date, - authors=json.dumps(authors), - ) - tag.last_commit = commit_obj - tag.save() - - -def record_tags(tags: list[Tag]) -> None: - """Sync the database with an updated set of tags.""" - # Remove entries which no longer exist - Tag.objects.exclude(name__in=[tag.name for tag in tags]).delete() - - # Insert/update the tags - for new_tag in tags: - try: - old_tag = Tag.objects.get(name=new_tag.name) - except Tag.DoesNotExist: - # The tag is not in the database yet, - # pretend it's previous state is the current state - old_tag = new_tag - - if old_tag.sha == new_tag.sha and old_tag.last_commit is not None: - # We still have an up-to-date commit entry - new_tag.last_commit = old_tag.last_commit - - new_tag.save() - - # Drop old, unused commits - Commit.objects.filter(tag__isnull=True).delete() - - -def get_tags() -> list[Tag]: - """Return a list of all tags visible to the application, from the cache or API.""" - if settings.STATIC_BUILD: # pragma: no cover - last_update = None - else: - last_update = ( - Tag.objects.values_list("last_updated", flat=True) - .order_by("last_updated").first() - ) - - if last_update is None or timezone.now() >= (last_update + TAG_CACHE_TTL): - # Stale or empty cache - if settings.STATIC_BUILD: # pragma: no cover - tags = get_tags_static() - else: - tags = fetch_tags() - record_tags(tags) - - return tags - else: - # Get tags from database - return list(Tag.objects.all()) - - -def get_tag(path: str, *, skip_sync: bool = False) -> Tag | list[Tag]: - """ - Return a tag based on the search location. - - If certain tag data is out of sync (for instance a commit date is missing), - an extra request will be made to sync the information. - - The tag name and group must match. If only one argument is provided in the path, - it's assumed to either be a group name, or a no-group tag name. - - If it's a group name, a list of tags which belong to it is returned. - """ - path = path.split("/") - if len(path) == 2: - group, name = path - else: - name = path[0] - group = None - - matches = [] - for tag in get_tags(): - if tag.name == name and tag.group == group: - if tag.last_commit is None and not skip_sync: - set_tag_commit(tag) - return tag - elif tag.group == name and group is None: - matches.append(tag) - - if matches: - return matches - - raise Tag.DoesNotExist() - - -def get_tag_category(tags: list[Tag] | None = None, *, collapse_groups: bool) -> dict[str, dict]: - """ - Generate context data for `tags`, or all tags if None. - - If `tags` is None, `get_tag` is used to populate the data. - If `collapse_groups` is True, tags with parent groups are not included in the list, - and instead the parent itself is included as a single entry with it's sub-tags - in the description. - """ - if not tags: - tags = get_tags() - - data = [] - groups = {} - - # Create all the metadata for the tags - for tag in tags: - if tag.group is None or not collapse_groups: - content = frontmatter.parse(tag.body)[1] - data.append({ - "title": tag.name, - "description": markdown.markdown(content, extensions=["pymdownx.superfences"]), - "icon": "fas fa-tag", - }) - else: - if tag.group not in groups: - groups[tag.group] = { - "title": tag.group, - "description": [tag.name], - "icon": "fas fa-tags", - } - else: - groups[tag.group]["description"].append(tag.name) - - # Flatten group description into a single string - for group in groups.values(): - # If the following string is updated, make sure to update it in the frontend JS as well - group["description"] = "Contains the following tags: " + ", ".join(group["description"]) - data.append(group) - - # Sort the tags, and return them in the proper format - return {tag["title"]: tag for tag in sorted(data, key=lambda tag: tag["title"].casefold())} - - -def get_category_pages(path: Path) -> dict[str, dict]: - """Get all page names and their metadata at a category path.""" - # Special handling for tags - if path == Path(__file__).parent / "resources/tags": - return get_tag_category(collapse_groups=True) - - pages = {} - - for item in path.glob("*.md"): - # Only list page if there is no category with the same name - if item.is_file() and not item.with_suffix("").is_dir(): - pages[item.stem] = frontmatter.load(item).metadata - - return pages - - -def get_page(path: Path) -> tuple[str, dict]: - """Get one specific page.""" - if not path.is_file(): - raise Http404("Page not found.") - - metadata, content = frontmatter.parse(path.read_text(encoding="utf-8")) - toc_depth = metadata.get("toc", 1) - - md = markdown.Markdown( - extensions=[ - "extra", - # Empty string for marker to disable text searching for [TOC] - # By using a metadata key instead, we save time on long markdown documents - TocExtension(permalink=True, marker="", toc_depth=toc_depth) - ] - ) - html = md.convert(content) - - # Don't set the TOC if the metadata does not specify one - if "toc" in metadata: - metadata["toc"] = md.toc - - return str(html), metadata diff --git a/pydis_site/apps/content/views/__init__.py b/pydis_site/apps/content/views/__init__.py deleted file mode 100644 index a969b1dc..00000000 --- a/pydis_site/apps/content/views/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .page_category import PageOrCategoryView -from .tags import TagView - -__all__ = ["PageOrCategoryView", "TagView"] diff --git a/pydis_site/apps/content/views/page_category.py b/pydis_site/apps/content/views/page_category.py deleted file mode 100644 index 062c2bc1..00000000 --- a/pydis_site/apps/content/views/page_category.py +++ /dev/null @@ -1,97 +0,0 @@ -from pathlib import Path - -import frontmatter -from django.conf import settings -from django.http import Http404, HttpRequest, HttpResponse -from django.views.generic import TemplateView - -from pydis_site.apps.content import models, utils - - -class PageOrCategoryView(TemplateView): - """Handles pages and page categories.""" - - def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: - """Conform URL path location to the filesystem path.""" - self.location = Path(kwargs.get("location", "")) - - # URL location on the filesystem - self.full_location = settings.CONTENT_PAGES_PATH / self.location - - # Possible places to find page content information - self.category_path = self.full_location - self.page_path = self.full_location.with_suffix(".md") - - return super().dispatch(request, *args, **kwargs) - - def get_template_names(self) -> list[str]: - """Checks if the view uses the page template or listing template.""" - if self.page_path.is_file(): - template_name = "content/page.html" - elif self.category_path.is_dir(): - template_name = "content/listing.html" - else: - raise Http404 - - return [template_name] - - def get_context_data(self, **kwargs) -> dict[str, any]: - """Assign proper context variables based on what resource user requests.""" - context = super().get_context_data(**kwargs) - - if self.page_path.is_file(): - context.update(self._get_page_context(self.page_path)) - elif self.category_path.is_dir(): - context.update(self._get_category_context(self.category_path)) - context["path"] = f"{self.location}/" # Add trailing slash to simplify template - else: - raise Http404 - - # Add subarticle information for dropdown menu if the page is also a category - if self.page_path.is_file() and self.category_path.is_dir(): - context["subarticles"] = [] - for entry in self.category_path.iterdir(): - entry_info = {"path": entry.stem} - if entry.suffix == ".md" and not entry.with_suffix("").is_dir(): - entry_info["name"] = frontmatter.load(entry).metadata["title"] - elif entry.is_dir(): - entry_info["name"] = utils.get_category(entry)["title"] - else: # pragma: no cover - # TODO: Remove coverage.py pragma in Python 3.10 - # See: https://github.com/nedbat/coveragepy/issues/198 - continue - context["subarticles"].append(entry_info) - - context["breadcrumb_items"] = [ - { - "name": utils.get_category(settings.CONTENT_PAGES_PATH / location)["title"], - "path": str(location) - } for location in reversed(self.location.parents) - ] - - return context - - @staticmethod - def _get_page_context(path: Path) -> dict[str, any]: - page, metadata = utils.get_page(path) - return { - "page": page, - "page_title": metadata["title"], - "page_description": metadata["description"], - "relevant_links": metadata.get("relevant_links", {}), - "toc": metadata.get("toc") - } - - @staticmethod - def _get_category_context(path: Path) -> dict[str, any]: - category = utils.get_category(path) - return { - "categories": utils.get_categories(path), - "pages": utils.get_category_pages(path), - "page_title": category["title"], - "page_description": category["description"], - "icon": category.get("icon"), - "app_name": "content:page_category", - "is_tag_listing": "/resources/tags" in path.as_posix(), - "tag_url": models.Tag.URL_BASE, - } diff --git a/pydis_site/apps/content/views/tags.py b/pydis_site/apps/content/views/tags.py deleted file mode 100644 index 4f4bb5a2..00000000 --- a/pydis_site/apps/content/views/tags.py +++ /dev/null @@ -1,124 +0,0 @@ -import re -import typing - -import frontmatter -import markdown -from django.conf import settings -from django.http import Http404 -from django.urls import reverse -from django.views.generic import TemplateView - -from pydis_site.apps.content import utils -from pydis_site.apps.content.models import Tag - -# The following regex tries to parse a tag command -# It'll read up to two words seperated by spaces -# If the command does not include a group, the tag name will be in the `first` group -# If there's a second word after the command, or if there's a tag group, extra logic -# is necessary to determine whether it's a tag with a group, or a tag with text after it -COMMAND_REGEX = re.compile(r"`*!tags? (?P<first>[\w-]+)(?P<second> [\w-]+)?`*") - - -class TagView(TemplateView): - """Handles tag pages.""" - - tag: typing.Union[Tag, list[Tag]] - is_group: bool - - def setup(self, *args, **kwargs) -> None: - """Look for a tag, and configure the view.""" - super().setup(*args, **kwargs) - - try: - self.tag = utils.get_tag(kwargs.get("location")) - self.is_group = isinstance(self.tag, list) - except Tag.DoesNotExist: - raise Http404 - - def get_template_names(self) -> list[str]: - """Either return the tag page template, or the listing.""" - if self.is_group: - template_name = "content/listing.html" - else: - template_name = "content/tag.html" - - return [template_name] - - def get_context_data(self, **kwargs) -> dict: - """Get the relevant context for this tag page or group.""" - context = super().get_context_data(**kwargs) - context["breadcrumb_items"] = [{ - "name": utils.get_category(settings.CONTENT_PAGES_PATH / location)["title"], - "path": location, - } for location in (".", "tags")] - - if self.is_group: - self._set_group_context(context, self.tag) - else: - self._set_tag_context(context, self.tag) - - return context - - @staticmethod - def _set_tag_context(context: dict[str, any], tag: Tag) -> None: - """Update the context with the information for a tag page.""" - context.update({ - "page_title": tag.name, - "tag": tag, - }) - - if tag.group: - # Add group names to the breadcrumbs - context["breadcrumb_items"].append({ - "name": tag.group, - "path": f"tags/{tag.group}", - }) - - # Clean up tag body - body = frontmatter.parse(tag.body) - content = body[1] - - # Check for tags which can be hyperlinked - def sub(match: re.Match) -> str: - first, second = match.groups() - location = first - text, extra = match.group(), "" - - if second is not None: - # Possibly a tag group - try: - new_location = f"{first}/{second.strip()}" - utils.get_tag(new_location, skip_sync=True) - location = new_location - except Tag.DoesNotExist: - # Not a group, remove the second argument from the link - extra = text[text.find(second):] - text = text[:text.find(second)] - - link = reverse("content:tag", kwargs={"location": location}) - return f"[{text}]({link}){extra}" - content = COMMAND_REGEX.sub(sub, content) - - # Add support for some embed elements - if embed := body[0].get("embed"): - context["page_title"] = embed["title"] - if image := embed.get("image"): - content = f"![{embed['title']}]({image['url']})\n\n" + content - - # Insert the content - context["page"] = markdown.markdown(content, extensions=["pymdownx.superfences"]) - - @staticmethod - def _set_group_context(context: dict[str, any], tags: list[Tag]) -> None: - """Update the context with the information for a group of tags.""" - group = tags[0].group - context.update({ - "categories": {}, - "pages": utils.get_tag_category(tags, collapse_groups=False), - "page_title": group, - "icon": "fab fa-tags", - "is_tag_listing": True, - "app_name": "content:tag", - "path": f"{group}/", - "tag_url": f"{tags[0].URL_BASE}/{group}" - }) diff --git a/pydis_site/apps/events/README.md b/pydis_site/apps/events/README.md deleted file mode 100644 index f0d20510..00000000 --- a/pydis_site/apps/events/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# The "events" app - -This application serves mostly static pages that showcase events we run on our -community. You most likely want to look at the [templates -directory](../../templates/events) for this app if you want to change anything. - -## Directory structure - -This app has a relatively minimal structure: - -- `migrations` is empty as we don't work with any models here. - -- `tests` contains a few tests to make sure that serving our events pages works. - -- `views` contains Django views that concern themselves with looking up the - matching Django template. - -The actual content lives in the [templates directory two layers -up](../../templates/events). diff --git a/pydis_site/apps/events/__init__.py b/pydis_site/apps/events/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pydis_site/apps/events/__init__.py +++ /dev/null diff --git a/pydis_site/apps/events/apps.py b/pydis_site/apps/events/apps.py deleted file mode 100644 index 70762bc2..00000000 --- a/pydis_site/apps/events/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig - - -class EventsConfig(AppConfig): - """Django AppConfig for events app.""" - - name = 'pydis_site.apps.events' diff --git a/pydis_site/apps/events/migrations/__init__.py b/pydis_site/apps/events/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pydis_site/apps/events/migrations/__init__.py +++ /dev/null diff --git a/pydis_site/apps/events/tests/__init__.py b/pydis_site/apps/events/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pydis_site/apps/events/tests/__init__.py +++ /dev/null diff --git a/pydis_site/apps/events/tests/test_views.py b/pydis_site/apps/events/tests/test_views.py deleted file mode 100644 index 669fbf82..00000000 --- a/pydis_site/apps/events/tests/test_views.py +++ /dev/null @@ -1,42 +0,0 @@ -from pathlib import Path - -from django.conf import settings -from django.test import TestCase, override_settings -from django.urls import reverse - - -PAGES_PATH = Path(settings.BASE_DIR, "pydis_site", "templates", "events", "test-pages") - - -class IndexTests(TestCase): - def test_events_index_response_200(self): - """Should return response code 200 when visiting index of events.""" - url = reverse("events:index") - resp = self.client.get(url) - self.assertEqual(resp.status_code, 200) - - -class PageTests(TestCase): - @override_settings(EVENTS_PAGES_PATH=PAGES_PATH) - def test_valid_event_page_reponse_200(self): - """Should return response code 200 when visiting valid event page.""" - pages = ( - reverse("events:page", args=("my-event",)), - reverse("events:page", args=("my-event/subpage",)), - ) - for page in pages: - with self.subTest(page=page): - resp = self.client.get(page) - self.assertEqual(resp.status_code, 200) - - @override_settings(EVENTS_PAGES_PATH=PAGES_PATH) - def test_invalid_event_page_404(self): - """Should return response code 404 when visiting invalid event page.""" - pages = ( - reverse("events:page", args=("invalid",)), - reverse("events:page", args=("invalid/invalid",)) - ) - for page in pages: - with self.subTest(page=page): - resp = self.client.get(page) - self.assertEqual(resp.status_code, 404) diff --git a/pydis_site/apps/events/urls.py b/pydis_site/apps/events/urls.py deleted file mode 100644 index 7ea65a31..00000000 --- a/pydis_site/apps/events/urls.py +++ /dev/null @@ -1,38 +0,0 @@ -import typing -from pathlib import Path - -from django_distill import distill_path - -from pydis_site.apps.events.views import IndexView, PageView - -app_name = "events" - - -def __get_all_files(root: Path, folder: typing.Optional[Path] = None) -> list[str]: - """Find all folders and HTML files recursively starting from `root`.""" - if not folder: - folder = root - - results = [] - - for sub_folder in folder.iterdir(): - results.append( - sub_folder.relative_to(root).__str__().replace("\\", "/").replace(".html", "") - ) - - if sub_folder.is_dir(): - results.extend(__get_all_files(root, sub_folder)) - - return results - - -def get_all_events() -> typing.Iterator[dict[str, str]]: - """Yield a dict of all event pages.""" - for file in __get_all_files(Path("pydis_site", "templates", "events", "pages")): - yield {"path": file} - - -urlpatterns = [ - distill_path("", IndexView.as_view(), name="index"), - distill_path("<path:path>/", PageView.as_view(), name="page", distill_func=get_all_events), -] diff --git a/pydis_site/apps/events/views/__init__.py b/pydis_site/apps/events/views/__init__.py deleted file mode 100644 index 8a107e2f..00000000 --- a/pydis_site/apps/events/views/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .index import IndexView -from .page import PageView - -__all__ = ["IndexView", "PageView"] diff --git a/pydis_site/apps/events/views/index.py b/pydis_site/apps/events/views/index.py deleted file mode 100644 index 7ffba74a..00000000 --- a/pydis_site/apps/events/views/index.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.views.generic import TemplateView - - -class IndexView(TemplateView): - """Events index page view.""" - - template_name = "events/index.html" diff --git a/pydis_site/apps/events/views/page.py b/pydis_site/apps/events/views/page.py deleted file mode 100644 index 1622ad70..00000000 --- a/pydis_site/apps/events/views/page.py +++ /dev/null @@ -1,25 +0,0 @@ -from typing import List - -from django.conf import settings -from django.http import Http404 -from django.views.generic import TemplateView - - -class PageView(TemplateView): - """Handles event pages showing.""" - - def get_template_names(self) -> List[str]: - """Get specific template names.""" - path: str = self.kwargs['path'] - page_path = settings.EVENTS_PAGES_PATH / path - if page_path.is_dir(): - page_path = page_path / "_index.html" - path = f"{path}/_index.html" - else: - page_path = settings.EVENTS_PAGES_PATH / f"{path}.html" - path = f"{path}.html" - - if not page_path.exists(): - raise Http404 - - return [f"events/{settings.EVENTS_PAGES_PATH.name}/{path}"] diff --git a/pydis_site/apps/home/README.md b/pydis_site/apps/home/README.md deleted file mode 100644 index 34c1e367..00000000 --- a/pydis_site/apps/home/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# 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/__init__.py b/pydis_site/apps/home/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pydis_site/apps/home/__init__.py +++ /dev/null diff --git a/pydis_site/apps/home/migrations/0001_initial.py b/pydis_site/apps/home/migrations/0001_initial.py deleted file mode 100644 index a2bf9f3e..00000000 --- a/pydis_site/apps/home/migrations/0001_initial.py +++ /dev/null @@ -1,26 +0,0 @@ -# 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/0002_auto_now_on_repository_metadata.py b/pydis_site/apps/home/migrations/0002_auto_now_on_repository_metadata.py deleted file mode 100644 index 7e78045b..00000000 --- a/pydis_site/apps/home/migrations/0002_auto_now_on_repository_metadata.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.0.11 on 2020-12-21 22:57 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('home', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='repositorymetadata', - name='last_updated', - field=models.DateTimeField(auto_now=True, help_text='The date and time this data was last fetched.'), - ), - ] diff --git a/pydis_site/apps/home/migrations/__init__.py b/pydis_site/apps/home/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pydis_site/apps/home/migrations/__init__.py +++ /dev/null diff --git a/pydis_site/apps/home/models.py b/pydis_site/apps/home/models.py deleted file mode 100644 index 00a83cd7..00000000 --- a/pydis_site/apps/home/models.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/templatetags/__init__.py b/pydis_site/apps/home/templatetags/__init__.py deleted file mode 100644 index 70aca169..00000000 --- a/pydis_site/apps/home/templatetags/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index 89b45831..00000000 --- a/pydis_site/apps/home/templatetags/extra_filters.py +++ /dev/null @@ -1,19 +0,0 @@ -from django import template - -register = template.Library() - - -def starts_with(value: str, arg: str) -> bool: - """ - Simple filter for checking if a string value starts with another string. - - Usage: - - ```django - {% if request.url | starts_with:"/events" %} - ... - {% endif %} - ``` - """ - return value.startswith(arg) diff --git a/pydis_site/apps/home/tests/__init__.py b/pydis_site/apps/home/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pydis_site/apps/home/tests/__init__.py +++ /dev/null diff --git a/pydis_site/apps/home/tests/mock_github_api_response.json b/pydis_site/apps/home/tests/mock_github_api_response.json deleted file mode 100644 index 3b0a7078..00000000 --- a/pydis_site/apps/home/tests/mock_github_api_response.json +++ /dev/null @@ -1,44 +0,0 @@ -[ - { - "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/king-arthur", - "description": "test", - "stargazers_count": 97, - "language": "Python", - "forks_count": 31 - }, - { - "full_name": "python-discord/metricity", - "description": "test", - "stargazers_count": 97, - "language": "Python", - "forks_count": 31 - }, - { - "full_name": "python-discord/sir-lancebot", - "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 deleted file mode 100644 index a963f733..00000000 --- a/pydis_site/apps/home/tests/test_repodata_helpers.py +++ /dev/null @@ -1,163 +0,0 @@ -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: F821 - """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('httpx.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.assertEqual(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('httpx.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('httpx.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.assertEqual(len(api_data), len(self.home_view.repos)) - self.assertIn(repo, api_data.keys()) - self.assertIn("stargazers_count", api_data[repo]) - - @mock.patch('httpx.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) - - @mock.patch('httpx.get') - def test_falls_back_to_database_on_error(self, mock_get: mock.MagicMock): - """Tests that fallback to the database is performed when we get garbage back.""" - 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() - - mock_get.return_value.json.return_value = ['garbage'] - - metadata = self.home_view._get_repo_data() - [item] = metadata - self.assertEqual(item, repo_data) - - @mock.patch('httpx.get') - def test_falls_back_to_database_on_error_without_entries(self, mock_get: mock.MagicMock): - """Tests that fallback to the database is performed when we get garbage back.""" - mock_get.return_value.json.return_value = ['garbage'] - - # Capture logs and ensure the problematic response is logged - with self.assertLogs(): - metadata = self.home_view._get_repo_data() - - self.assertEqual(len(metadata), 0) - - def test_cleans_up_stale_metadata(self): - """Tests that we clean up stale metadata when we start the HomeView.""" - repo_data = RepositoryMetadata( - repo_name="python-discord/INVALID", - description="testrepo", - forks=42, - stargazers=42, - language="English", - last_updated=timezone.now() - timedelta(seconds=HomeView.repository_cache_ttl + 1), - ) - repo_data.save() - self.home_view.__init__() - cached_repos = RepositoryMetadata.objects.all() - cached_names = [repo.repo_name for repo in cached_repos] - - self.assertNotIn("python-discord/INVALID", cached_names) - - def test_dont_clean_up_unstale_metadata(self): - """Tests that we don't clean up good metadata when we start the HomeView.""" - 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() - self.home_view.__init__() - cached_repos = RepositoryMetadata.objects.all() - cached_names = [repo.repo_name for repo in cached_repos] - - self.assertIn("python-discord/site", cached_names) diff --git a/pydis_site/apps/home/tests/test_templatetags.py b/pydis_site/apps/home/tests/test_templatetags.py deleted file mode 100644 index 813588c8..00000000 --- a/pydis_site/apps/home/tests/test_templatetags.py +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index b1215df4..00000000 --- a/pydis_site/apps/home/tests/test_views.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.test import TestCase -from django.urls import reverse - - -class TestIndexReturns200(TestCase): - def test_index_returns_200(self): - """Check that the index page returns a HTTP 200 response.""" - url = reverse('home: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 deleted file mode 100644 index 30321ece..00000000 --- a/pydis_site/apps/home/urls.py +++ /dev/null @@ -1,9 +0,0 @@ -from django_distill import distill_path - -from .views import HomeView, timeline - -app_name = 'home' -urlpatterns = [ - distill_path('', HomeView.as_view(), name='home'), - distill_path('timeline/', timeline, name="timeline"), -] diff --git a/pydis_site/apps/home/views.py b/pydis_site/apps/home/views.py deleted file mode 100644 index 8a165682..00000000 --- a/pydis_site/apps/home/views.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') diff --git a/pydis_site/apps/redirect/__init__.py b/pydis_site/apps/redirect/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pydis_site/apps/redirect/__init__.py +++ /dev/null diff --git a/pydis_site/apps/redirect/apps.py b/pydis_site/apps/redirect/apps.py deleted file mode 100644 index 0234bc93..00000000 --- a/pydis_site/apps/redirect/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig - - -class RedirectConfig(AppConfig): - """AppConfig instance for Redirect app.""" - - name = 'pydis_site.apps.redirect' diff --git a/pydis_site/apps/redirect/migrations/__init__.py b/pydis_site/apps/redirect/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pydis_site/apps/redirect/migrations/__init__.py +++ /dev/null diff --git a/pydis_site/apps/redirect/redirects.yaml b/pydis_site/apps/redirect/redirects.yaml deleted file mode 100644 index 4a48ba0c..00000000 --- a/pydis_site/apps/redirect/redirects.yaml +++ /dev/null @@ -1,247 +0,0 @@ -# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -# The redirects here are for dewikification backwards compatibility -# and SHOULD NOT be used for adding new redirects for convenience. -# -# Convenience redirects should be added using our cloudflare worker -# at https://github.com/python-discord/workers/tree/main/short-urls -# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - -# Root pages -roles_redirect: - original_path: pages/roles/ - redirect_route: "content:page_category" - redirect_arguments: ["server-info/roles"] - -roles_expectations_redirect: - original_path: pages/roles/staff-role-expectations/ - redirect_route: "content:page_category" - redirect_arguments: ["server-info/staff-role-expectations"] - -contributing_redirect: - original_path: pages/contributing/ - redirect_route: "content:page_category" - redirect_arguments: ["guides/pydis-guides/contributing"] - -# Code of Conduct Policy -code_of_conduct_policy: - original_path: pages/code-of-conduct/policies/ - redirect_route: "content:page_category" - redirect_arguments: ["code-of-conduct"] - -# Guides -guides_redirect: - original_path: pages/resources/guides/ - redirect_route: "content:page_category" - redirect_arguments: ["guides"] - -# - Python guides -discord_py_redirect: - original_path: pages/resources/guides/discordpy/ - redirect_route: "content:page_category" - redirect_arguments: ["guides/python-guides/discordpy"] - -mutability_redirect: - original_path: pages/resources/guides/core-concepts/mutability/ - redirect_route: "content:page_category" - redirect_arguments: ["guides/python-guides/mutability"] - -function_params_redirect: - original_path: pages/resources/guides/core-concepts/parameters-and-arguments/ - redirect_route: "content:page_category" - redirect_arguments: ["guides/python-guides/parameters-and-arguments"] - -# - Pydis guides -help_channel_redirect: - original_path: pages/resources/guides/help-channels/ - redirect_route: "content:page_category" - redirect_arguments: ["guides/pydis-guides/help-channel-guide"] - -good_questions_redirect: - original_path: pages/resources/guides/asking-good-questions/ - redirect_route: "content:page_category" - redirect_arguments: ["guides/pydis-guides/asking-good-questions"] - -helping_others_redirect: - original_path: pages/resources/guides/helping-others/ - redirect_route: "content:page_category" - redirect_arguments: ["guides/pydis-guides/helping-others"] - -code_review_redirect: - original_path: pages/resources/guides/code-reviews-primer/ - redirect_route: "content:page_category" - redirect_arguments: ["guides/pydis-guides/code-reviews-primer"] - -off-topic_redirect: - original_path: pages/resources/guides/off-topic-etiquette/ - redirect_route: "content:page_category" - redirect_arguments: ["guides/pydis-guides/off-topic-etiquette"] - -good_questions_redirect_alt: - # In a few places we were linking to a version outside of the guides app. - original_path: pages/asking-good-questions/ - redirect_route: "content:page_category" - redirect_arguments: ["guides/pydis-guides/asking-good-questions"] - -# Resources -resources_old_communities_redirect: - original_path: pages/resources/communities/ - redirect_route: "resources:index" - redirect_arguments: ["community"] - -resources_index_redirect: - original_path: pages/resources/ - redirect_route: "resources:index" - -resources_reading_redirect: - original_path: resources/reading/ - redirect_route: "resources:index" - redirect_arguments: ["book"] - -resources_books_redirect: - original_path: resources/books/ - redirect_route: "resources:index" - redirect_arguments: ["book"] - -resources_videos_redirect: - original_path: resources/videos/ - redirect_route: "resources:index" - redirect_arguments: ["video"] - -resources_courses_redirect: - original_path: resources/courses/ - redirect_route: "resources:index" - redirect_arguments: ["course"] - -resources_communities_redirect: - original_path: resources/communities/ - redirect_route: "resources:index" - redirect_arguments: ["community"] - -resources_podcasts_redirect: - original_path: resources/podcasts/ - redirect_route: "resources:index" - redirect_arguments: ["podcast"] - -resources_tutorials_redirect: - original_path: resources/tutorials/ - redirect_route: "resources:index" - redirect_arguments: ["tutorial"] - -resources_tools_redirect: - original_path: resources/tools/ - redirect_route: "resources:index" - redirect_arguments: ["tool"] - -# Events -events_index_redirect: - original_path: pages/events/ - redirect_route: "events:index" - -events_code_jams_index_redirect: - original_path: pages/code-jams/ - redirect_route: "events:page" - redirect_arguments: ["code-jams"] - -events_code_jams_one_redirect: - original_path: pages/code-jams/code-jam-1-snakes-bot/ - redirect_route: "events:page" - redirect_arguments: ["code-jams/1"] - -events_code_jams_two_redirect: - original_path: pages/code-jams/code-jam-2/ - redirect_route: "events:page" - redirect_arguments: ["code-jams/2"] - -events_code_jams_three_redirect: - original_path: pages/code-jams/code-jam-3/ - redirect_route: "events:page" - redirect_arguments: ["code-jams/3"] - -events_code_jams_four_redirect: - original_path: pages/code-jams/code-jam-4/ - redirect_route: "events:page" - redirect_arguments: ["code-jams/4"] - -events_code_jams_five_redirect: - original_path: pages/code-jams/code-jam-5/ - redirect_route: "events:page" - redirect_arguments: ["code-jams/5"] - -events_code_jams_six_redirect: - original_path: pages/code-jams/code-jam-6/ - redirect_route: "events:page" - redirect_arguments: ["code-jams/6"] - -events_code_jams_six_rules_redirect: - original_path: pages/code-jams/code-jam-6/rules/ - redirect_route: "events:page" - redirect_arguments: ["code-jams/6/rules"] - -events_code_jams_seven_redirect: - original_path: pages/code-jams/code-jam-7/ - redirect_route: "events:page" - redirect_arguments: ["code-jams/7"] - -events_code_jams_seven_rules_redirect: - original_path: pages/code-jams/code-jam-7/rules/ - redirect_route: "events:page" - redirect_arguments: ["code-jams/7/rules"] - -events_code_jams_how_to_use_git_redirect: - original_path: pages/code-jams/using-git/ - redirect_route: "events:page" - redirect_arguments: ["code-jams/using-git"] - -events_code_jams_judging_redirect: - original_path: pages/code-jams/judging/ - redirect_route: "events:page" - redirect_arguments: ["code-jams/judging"] - -events_code_jams_pull_request_redirect: - original_path: pages/code-jams/pull-request/ - redirect_route: "events:page" - redirect_arguments: ["code-jams/pull-request"] - -events_game_jams_twenty_twenty_index_redirect: - original_path: pages/events/game-jam-2020/ - redirect_route: "events:page" - redirect_arguments: ["game-jams/2020"] - -events_game_jams_twenty_twenty_judging_redirect: - original_path: pages/events/game-jam-2020/judging/ - redirect_route: "events:page" - redirect_arguments: ["game-jams/2020/judging"] - -events_game_jams_twenty_twenty_project_setup_redirect: - original_path: pages/events/game-jam-2020/project-setup/ - redirect_route: "events:page" - redirect_arguments: ["game-jams/2020/project-setup"] - -events_game_jams_twenty_twenty_rules_redirect: - original_path: pages/events/game-jam-2020/rules/ - redirect_route: "events:page" - redirect_arguments: ["game-jams/2020/rules"] - -events_game_jams_twenty_twenty_technical_requirements_redirect: - original_path: pages/events/game-jam-2020/technical-requirements/ - redirect_route: "events:page" - redirect_arguments: ["game-jams/2020/technical-requirements"] - -# This are overrides for the contributing prefix redirect -security_notice_redirect: - original_path: pages/contributing/security-notice/ - redirect_route: "content:page_category" - redirect_arguments: ["security-notice"] - -sir-lancebot_env_var_redirect: - original_path: pages/contributing/sir-lancebot/sir-lancebot-env-var-reference/ - redirect_route: "content:page_category" - redirect_arguments: ["guides/pydis-guides/contributing/sir-lancebot/env-var-reference"] - -# Prefix redirects -# Prefix redirects must be last in each group. -guides_pydis_guides_contributing_prefix_redirect: - original_path: pages/contributing/<path:path>/ # path:path will be joined together with static arguments. - redirect_route: "content:page_category" - redirect_arguments: ["guides/pydis-guides/contributing/"] # It is important to put / at end in prefix redirect! - prefix_redirect: true diff --git a/pydis_site/apps/redirect/tests.py b/pydis_site/apps/redirect/tests.py deleted file mode 100644 index c181d6e5..00000000 --- a/pydis_site/apps/redirect/tests.py +++ /dev/null @@ -1,61 +0,0 @@ -import yaml -from django.conf import settings -from django.test import TestCase -from django.urls import reverse - -TESTING_ARGUMENTS = { - "resources_resources_redirect": ("reading",), - "guides_pydis_guides_contributing_prefix_redirect": ("sir-lancebot/env-var-reference",), -} - - -class RedirectTests(TestCase): - """Survival tests for redirects.""" - - def test_redirects(self) -> None: - """ - Should redirect to given route based on redirect rules. - - Makes sure that every redirect: - 1. Redirects only once. - 2. Redirects to right URL. - 3. Resulting page status code is 200. - """ - for name, data in yaml.safe_load(settings.REDIRECTIONS_PATH.read_text()).items(): - with self.subTest( - original_path=data["original_path"], - redirect_route=data["redirect_route"], - name=name, - redirect_arguments=tuple(data.get("redirect_arguments", ())), - args=TESTING_ARGUMENTS.get(name, ()) - ): - resp = self.client.get( - reverse( - f"redirect:{name}", - args=TESTING_ARGUMENTS.get(name, ()) - ), - follow=True - ) - - if data.get("prefix_redirect", False): - expected_args = ( - "".join( - tuple(data.get("redirect_arguments", ())) - + TESTING_ARGUMENTS.get(name, ()) - ), - ) - else: - expected_args = ( - TESTING_ARGUMENTS.get(name, ()) + tuple(data.get("redirect_arguments", ())) - ) - - self.assertEqual(1, len(resp.redirect_chain)) - self.assertRedirects( - resp, - reverse( - f"{data['redirect_route']}", - args=expected_args - ), - status_code=301 - ) - self.assertEqual(resp.status_code, 200) diff --git a/pydis_site/apps/redirect/urls.py b/pydis_site/apps/redirect/urls.py deleted file mode 100644 index 067cccc3..00000000 --- a/pydis_site/apps/redirect/urls.py +++ /dev/null @@ -1,106 +0,0 @@ -import dataclasses -import re - -import yaml -from django import conf -from django.http import HttpResponse -from django.urls import URLPattern, path -from django_distill import distill_path - -from pydis_site import settings -from pydis_site.apps.content import urls as pages_urls -from pydis_site.apps.redirect.views import CustomRedirectView -from pydis_site.apps.resources import urls as resources_urls - -app_name = "redirect" - - -__PARAMETER_REGEX = re.compile(r"<\w+:\w+>") -REDIRECT_TEMPLATE = "<meta http-equiv=\"refresh\" content=\"0; URL={url}\"/>" - - [email protected](frozen=True) -class Redirect: - """Metadata about a redirect route.""" - - original_path: str - redirect_route: str - redirect_arguments: tuple[str] = tuple() - - prefix_redirect: bool = False - - -def map_redirect(name: str, data: Redirect) -> list[URLPattern]: - """Return a pattern using the Redirects app, or a static HTML redirect for static builds.""" - if not settings.STATIC_BUILD: - # Normal dynamic redirect - return [path( - data.original_path, - CustomRedirectView.as_view( - pattern_name=data.redirect_route, - static_args=tuple(data.redirect_arguments), - prefix_redirect=data.prefix_redirect - ), - name=name - )] - - # Create static HTML redirects for static builds - new_app_name = data.redirect_route.split(":")[0] - - if __PARAMETER_REGEX.search(data.original_path): - # Redirects for paths which accept parameters - # We generate an HTML redirect file for all possible entries - paths = [] - - class RedirectFunc: - def __init__(self, new_url: str, _name: str): - self.result = HttpResponse(REDIRECT_TEMPLATE.format(url=new_url)) - self.__qualname__ = _name - - def __call__(self, *args, **kwargs): - return self.result - - if new_app_name == resources_urls.app_name: - items = resources_urls.get_all_resources() - elif new_app_name == pages_urls.app_name: - items = pages_urls.get_all_pages() - else: - raise ValueError(f"Unknown app in redirect: {new_app_name}") - - for item in items: - entry = list(item.values())[0] - - # Replace dynamic redirect with concrete path - concrete_path = __PARAMETER_REGEX.sub(entry, data.original_path) - new_redirect = f"/{new_app_name}/{entry}" - pattern_name = f"{name}_{entry}" - - paths.append(distill_path( - concrete_path, - RedirectFunc(new_redirect, pattern_name), - name=pattern_name - )) - - return paths - - else: - redirect_path_name = "pages" if new_app_name == "content" else new_app_name - if len(data.redirect_arguments) > 0: - redirect_arg = data.redirect_arguments[0] - else: - redirect_arg = "resources/" - new_redirect = f"/{redirect_path_name}/{redirect_arg}" - - if new_redirect == "/resources/resources/": - new_redirect = "/resources/" - - return [distill_path( - data.original_path, - lambda *args: HttpResponse(REDIRECT_TEMPLATE.format(url=new_redirect)), - name=name, - )] - - -urlpatterns = [] -for _name, _data in yaml.safe_load(conf.settings.REDIRECTIONS_PATH.read_text()).items(): - urlpatterns.extend(map_redirect(_name, Redirect(**_data))) diff --git a/pydis_site/apps/redirect/views.py b/pydis_site/apps/redirect/views.py deleted file mode 100644 index 21180cdf..00000000 --- a/pydis_site/apps/redirect/views.py +++ /dev/null @@ -1,24 +0,0 @@ -import typing as t - -from django.views.generic import RedirectView - - -class CustomRedirectView(RedirectView): - """Extended RedirectView for manual route args.""" - - permanent = True - static_args = () - prefix_redirect = False - - @classmethod - def as_view(cls, **initkwargs): - """Overwrites original as_view to add static args.""" - return super().as_view(**initkwargs) - - def get_redirect_url(self, *args, **kwargs) -> t.Optional[str]: - """Extends default behaviour to use static args.""" - args = self.static_args + args + tuple(kwargs.values()) - if self.prefix_redirect: - args = ("".join(args),) - - return super().get_redirect_url(*args) diff --git a/pydis_site/apps/resources/__init__.py b/pydis_site/apps/resources/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pydis_site/apps/resources/__init__.py +++ /dev/null diff --git a/pydis_site/apps/resources/apps.py b/pydis_site/apps/resources/apps.py deleted file mode 100644 index 93117654..00000000 --- a/pydis_site/apps/resources/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig - - -class ResourcesConfig(AppConfig): - """AppConfig instance for Resources app.""" - - name = 'pydis_site.apps.resources' diff --git a/pydis_site/apps/resources/migrations/__init__.py b/pydis_site/apps/resources/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pydis_site/apps/resources/migrations/__init__.py +++ /dev/null diff --git a/pydis_site/apps/resources/resources/adafruit.yaml b/pydis_site/apps/resources/resources/adafruit.yaml deleted file mode 100644 index c687f507..00000000 --- a/pydis_site/apps/resources/resources/adafruit.yaml +++ /dev/null @@ -1,22 +0,0 @@ -name: Adafruit -description: Adafruit is an open-source electronics manufacturer - that makes all the components you need to start your own Python-powered hardware projects. - Their official community host regular show-and-tells, - provide help with your projects, - and the Adafruit devs do all the CircuitPython Development right out in the open. -title_image: https://www.mouser.com/images/suppliers/logos/adafruit.png -title_url: https://adafruit.com/ -urls: - - icon: branding/discord - url: https://discord.gg/adafruit - color: blurple -tags: - topics: - - microcontrollers - payment_tiers: - - free - difficulty: - - beginner - - intermediate - type: - - community diff --git a/pydis_site/apps/resources/resources/automate_the_boring_stuff_book.yaml b/pydis_site/apps/resources/resources/automate_the_boring_stuff_book.yaml deleted file mode 100644 index 63f63193..00000000 --- a/pydis_site/apps/resources/resources/automate_the_boring_stuff_book.yaml +++ /dev/null @@ -1,21 +0,0 @@ -description: One of the best books out there for Python beginners. This book will - teach you the basics of Python, while also teaching invaluable automation tools - and techniques for solving common problems. You'll learn how to go about scraping - the web, manipulating files and automating keyboard and mouse input. Ideal for an - office worker who wants to make himself more useful. -name: Automate the Boring Stuff with Python -title_url: https://automatetheboringstuff.com/ -urls: -- icon: branding/goodreads - url: https://www.goodreads.com/book/show/22514127-automate-the-boring-stuff-with-python - color: black -tags: - topics: - - general - payment_tiers: - - free - - paid - difficulty: - - beginner - type: - - book diff --git a/pydis_site/apps/resources/resources/automate_the_boring_stuff_course.yaml b/pydis_site/apps/resources/resources/automate_the_boring_stuff_course.yaml deleted file mode 100644 index 4632f5bd..00000000 --- a/pydis_site/apps/resources/resources/automate_the_boring_stuff_course.yaml +++ /dev/null @@ -1,13 +0,0 @@ -description: The interactive course version of Al Sweigart's excellent book for beginners, taught by the author himself. -name: Automate the Boring Stuff with Python Udemy Course -title_url: https://www.udemy.com/automate/ -tags: - topics: - - general - payment_tiers: - - paid - difficulty: - - beginner - type: - - course - - interactive diff --git a/pydis_site/apps/resources/resources/awesome_programming_discord.yaml b/pydis_site/apps/resources/resources/awesome_programming_discord.yaml deleted file mode 100644 index 0ef7aefc..00000000 --- a/pydis_site/apps/resources/resources/awesome_programming_discord.yaml +++ /dev/null @@ -1,18 +0,0 @@ -description: We have listed our favourite communities, - but there are many more excellent communities out there! - An awesome list collating the best programming related Discord servers is available on GitHub - and has all sorts of topics from blockchain to virtual reality! -title_icon: branding/github -title_icon_color: black -title_url: https://github.com/mhxion/awesome-programming-discord -name: awesome-programming-discord -tags: - topics: - - general - payment_tiers: - - free - difficulty: - - beginner - - intermediate - type: - - community diff --git a/pydis_site/apps/resources/resources/byte_of_python.yaml b/pydis_site/apps/resources/resources/byte_of_python.yaml deleted file mode 100644 index c2f6ab84..00000000 --- a/pydis_site/apps/resources/resources/byte_of_python.yaml +++ /dev/null @@ -1,22 +0,0 @@ -description: A free book on programming using the Python language. - It serves as a tutorial or guide to the Python language for a beginner audience. - If all you know about computers is how to save text files, then this is the book for you. -name: A Byte of Python -title_url: https://python.swaroopch.com/ -urls: -- icon: regular/book - url: https://www.lulu.com/shop/swaroop-c-h/a-byte-of-python/paperback/product-21142968.html - color: black -- icon: branding/goodreads - url: https://www.goodreads.com/book/show/6762544-a-byte-of-python - color: black -tags: - topics: - - general - payment_tiers: - - free - - paid - difficulty: - - beginner - type: - - book diff --git a/pydis_site/apps/resources/resources/code_combat.yaml b/pydis_site/apps/resources/resources/code_combat.yaml deleted file mode 100644 index 84597c4d..00000000 --- a/pydis_site/apps/resources/resources/code_combat.yaml +++ /dev/null @@ -1,20 +0,0 @@ -description: Learn Python while gaming - an open-source project with thousands of - contributors, which teaches you Python through a deep, top-down RPG. -name: Code Combat -title_url: https://codecombat.com/ -urls: -- icon: branding/github - url: https://github.com/codecombat/codecombat - color: black -tags: - topics: - - general - - algorithms and data structures - payment_tiers: - - free - - subscription - difficulty: - - beginner - - intermediate - type: - - interactive diff --git a/pydis_site/apps/resources/resources/corey_schafer.yaml b/pydis_site/apps/resources/resources/corey_schafer.yaml deleted file mode 100644 index d66ea004..00000000 --- a/pydis_site/apps/resources/resources/corey_schafer.yaml +++ /dev/null @@ -1,30 +0,0 @@ -name: Corey Schafer -description: 'Corey has a number of exceptionally high quality tutorial series - on everything from Python basics to Django and Flask: - <ul> - <li><a href="https://www.youtube.com/playlist?list=PL-osiE80TeTskrapNbzXhwoFUiLCjGgY7">Python Programming Beginner Tutorials</a></li> - <li><a href="https://www.youtube.com/playlist?list=PL-osiE80TeTsqhIuOqKhwlXsIBIdSeYtc">Python OOP Tutorials - Working With Classes</a></li> - <li><a href="https://www.youtube.com/playlist?list=PL-osiE80TeTs4UjLw5MM6OjgkjFeUxCYH">Flask Tutorials</a></li> - <li><a href="https://www.youtube.com/playlist?list=PL-osiE80TeTtoQCKZ03TU5fNfx2UY6U4p">Django Tutorials</a></li> - </ul> - Check out his channel for more video series! - ' -title_image: https://i.imgur.com/KIfWw3b.png -title_url: https://www.youtube.com/channel/UCCezIgC97PvUuR4_gbFUs5g -urls: - - icon: solid/external-link-alt - url: https://coreyms.com/ - color: teal -tags: - topics: - - general - - software design - - web development - - tooling - payment_tiers: - - free - difficulty: - - beginner - - intermediate - type: - - video diff --git a/pydis_site/apps/resources/resources/data_science_from_scratch.yaml b/pydis_site/apps/resources/resources/data_science_from_scratch.yaml deleted file mode 100644 index 86955fdb..00000000 --- a/pydis_site/apps/resources/resources/data_science_from_scratch.yaml +++ /dev/null @@ -1,22 +0,0 @@ -description: Data Science from Scratch is a good introduction to data science for the complete beginner, and covers - some of the fundamentals of Python programming as well as the basic math, probability and statistics needed to get - started. While either edition of this book is useful for those with prior Python experience, complete beginners - should use the second edition, which contains more up-to-date code examples and better practices. -name: Data Science from Scratch -title_url: https://www.oreilly.com/library/view/data-science-from/9781492041122/ -urls: - - icon: branding/goodreads - url: https://www.goodreads.com/en/book/show/52059715-data-science-from-scratch - color: black - - icon: branding/github - url: https://github.com/joelgrus/data-science-from-scratch - color: black -tags: - topics: - - data science - payment_tiers: - - paid - difficulty: - - beginner - type: - - book diff --git a/pydis_site/apps/resources/resources/edublocks.yaml b/pydis_site/apps/resources/resources/edublocks.yaml deleted file mode 100644 index 3eaefc35..00000000 --- a/pydis_site/apps/resources/resources/edublocks.yaml +++ /dev/null @@ -1,18 +0,0 @@ -description: EduBlocks provides a simple drag and drop interface to help beginners get to grips - with the key concepts of Python. There is built-in support for modules such as random, - turtle, processing and pygal to play around with, - and it even allows you to export the Python code you have written in the graphical editor. - EduBlocks also has integration with BBC micro:bit, - Raspberry Pi and CircuitPython allowing you to write code for these devices graphically - and export the code to run on actual devices. -name: EduBlocks -title_url: https://edublocks.org/ -tags: - topics: - - general - payment_tiers: - - free - difficulty: - - beginner - type: - - interactive diff --git a/pydis_site/apps/resources/resources/effective_python.yaml b/pydis_site/apps/resources/resources/effective_python.yaml deleted file mode 100644 index b82fa0c3..00000000 --- a/pydis_site/apps/resources/resources/effective_python.yaml +++ /dev/null @@ -1,21 +0,0 @@ -description: A book that gives 90 best practices for writing excellent Python. Great - for intermediates. -name: Effective Python -title_url: https://effectivepython.com/ -urls: -- icon: branding/goodreads - url: https://www.goodreads.com/book/show/48566725-effective-python - color: black -- icon: branding/github - url: https://github.com/bslatkin/effectivepython - color: black -tags: - topics: - - general - - software design - payment_tiers: - - paid - difficulty: - - intermediate - type: - - book diff --git a/pydis_site/apps/resources/resources/exercism.yaml b/pydis_site/apps/resources/resources/exercism.yaml deleted file mode 100644 index c623db2d..00000000 --- a/pydis_site/apps/resources/resources/exercism.yaml +++ /dev/null @@ -1,20 +0,0 @@ -description: Level up your programming skills with more than 2600 exercises across - 47 programming languages, Python included. The website provides a mentored mode, - where you can get your code reviewed for each solution you submit. The mentors will - give you insightful advice to make you a better programmer. -name: Exercism -title_url: https://exercism.org/ -urls: -- icon: branding/github - url: https://github.com/exercism/python - color: black -tags: - topics: - - general - payment_tiers: - - free - difficulty: - - beginner - - intermediate - type: - - interactive diff --git a/pydis_site/apps/resources/resources/flask_web_development.yaml b/pydis_site/apps/resources/resources/flask_web_development.yaml deleted file mode 100644 index 6905b2b4..00000000 --- a/pydis_site/apps/resources/resources/flask_web_development.yaml +++ /dev/null @@ -1,21 +0,0 @@ -description: A comprehensive Flask walkthrough that has you building a complete social - blogging application from scratch. -name: Flask Web Development -title_url: http://shop.oreilly.com/product/0636920031116.do -urls: -- icon: branding/goodreads - url: https://www.goodreads.com/book/show/18774655-flask-web-development - color: black -- icon: branding/github - url: https://github.com/miguelgrinberg/flasky - color: black -tags: - topics: - - web development - payment_tiers: - - paid - difficulty: - - beginner - - intermediate - type: - - book diff --git a/pydis_site/apps/resources/resources/fluent_python.yaml b/pydis_site/apps/resources/resources/fluent_python.yaml deleted file mode 100644 index c22fd388..00000000 --- a/pydis_site/apps/resources/resources/fluent_python.yaml +++ /dev/null @@ -1,21 +0,0 @@ -description: A veritable tome of intermediate and advanced Python information. A must-read - for any Python professional. By far the most recommended book for intermediates. -name: Fluent Python -title_url: https://www.oreilly.com/library/view/fluent-python/9781491946237/ -urls: -- icon: branding/goodreads - url: https://www.goodreads.com/book/show/22800567-fluent-python - color: black -- icon: branding/github - url: https://github.com/fluentpython - color: black -tags: - topics: - - general - - software design - payment_tiers: - - paid - difficulty: - - intermediate - type: - - book diff --git a/pydis_site/apps/resources/resources/getting_started_with_kivy.yaml b/pydis_site/apps/resources/resources/getting_started_with_kivy.yaml deleted file mode 100644 index 06eb2c14..00000000 --- a/pydis_site/apps/resources/resources/getting_started_with_kivy.yaml +++ /dev/null @@ -1,14 +0,0 @@ -description: A big list of excellent resources for getting started making Kivy applications. -name: Getting Started with Kivy -title_url: https://blog.kivy.org/2019/12/getting-started-with-kivy/ -icon_image: https://raw.githubusercontent.com/kivy/kivy-website/master/logos/kivy-logo-black-256.png -tags: - topics: - - user interface - - game development - payment_tiers: - - free - difficulty: - - beginner - type: - - tutorial diff --git a/pydis_site/apps/resources/resources/getting_started_with_python_for_non_programmers.yaml b/pydis_site/apps/resources/resources/getting_started_with_python_for_non_programmers.yaml deleted file mode 100644 index 6fab0114..00000000 --- a/pydis_site/apps/resources/resources/getting_started_with_python_for_non_programmers.yaml +++ /dev/null @@ -1,13 +0,0 @@ -description: A list of beginner resources for programmers with no prior developer experience, - from Python's official guide. -name: Getting Started with Python for Non-Programmers -title_url: https://wiki.python.org/moin/BeginnersGuide/NonProgrammers -tags: - topics: - - general - payment_tiers: - - free - difficulty: - - beginner - type: - - tutorial diff --git a/pydis_site/apps/resources/resources/getting_started_with_python_for_programmers.yaml b/pydis_site/apps/resources/resources/getting_started_with_python_for_programmers.yaml deleted file mode 100644 index 74b6efb9..00000000 --- a/pydis_site/apps/resources/resources/getting_started_with_python_for_programmers.yaml +++ /dev/null @@ -1,14 +0,0 @@ -description: A list of beginner resources for programmers coming from other languages, - from Python's official guide. -name: Getting Started with Python for Programmers -title_url: https://wiki.python.org/moin/BeginnersGuide/Programmers -position: 0 -tags: - topics: - - general - payment_tiers: - - free - difficulty: - - intermediate - type: - - tutorial diff --git a/pydis_site/apps/resources/resources/google_colab.yaml b/pydis_site/apps/resources/resources/google_colab.yaml deleted file mode 100644 index 5e1ca677..00000000 --- a/pydis_site/apps/resources/resources/google_colab.yaml +++ /dev/null @@ -1,17 +0,0 @@ -description: Google Colab is a custom version of Jupyter Notebook that runs code in the cloud, allowing you to - share your Colab notebooks with other people and work collaboratively. - Colab offers a generous amount of memory and computation time for free, and allows you to run programs on GPUs, - making it a great deep learning sandbox for beginners. -name: Google Colab -title_url: https://colab.research.google.com/notebooks/intro.ipynb -tags: - topics: - - general - - data science - payment_tiers: - - free - difficulty: - - beginner - - intermediate - type: - - tool diff --git a/pydis_site/apps/resources/resources/hitchhikers_guide_to_python.yaml b/pydis_site/apps/resources/resources/hitchhikers_guide_to_python.yaml deleted file mode 100644 index e48e5717..00000000 --- a/pydis_site/apps/resources/resources/hitchhikers_guide_to_python.yaml +++ /dev/null @@ -1,18 +0,0 @@ -description: A best practice handbook for both novice and expert Python developers to the installation, - configuration, and usage of Python on a daily basis. -name: The Hitchhiker's Guide to Python -title_url: https://python-guide.org/ -urls: -- icon: branding/goodreads - url: https://www.goodreads.com/book/show/28321007-the-hitchhiker-s-guide-to-python - color: black -tags: - topics: - - general - payment_tiers: - - paid - difficulty: - - beginner - - intermediate - type: - - book diff --git a/pydis_site/apps/resources/resources/inferential_thinking.yaml b/pydis_site/apps/resources/resources/inferential_thinking.yaml deleted file mode 100644 index a8cf2bc8..00000000 --- a/pydis_site/apps/resources/resources/inferential_thinking.yaml +++ /dev/null @@ -1,15 +0,0 @@ -description: Inferential Thinking is the textbook for the <a href="http://data8.org/">Foundations of Data Science</a> course at UC Berkley. - It introduces you the fundamentals of both Data Science and Python at a level accessible to all. - It is available both through your browser and in PDF form. -name: Inferential Thinking -title_url: https://inferentialthinking.com/chapters/intro -tags: - topics: - - data science - payment_tiers: - - free - difficulty: - - beginner - - intermediate - type: - - book diff --git a/pydis_site/apps/resources/resources/jetbrains_academy.yaml b/pydis_site/apps/resources/resources/jetbrains_academy.yaml deleted file mode 100644 index c3cb7657..00000000 --- a/pydis_site/apps/resources/resources/jetbrains_academy.yaml +++ /dev/null @@ -1,18 +0,0 @@ -description: Learn Python with a wide range of high quality, project-based lessons. - Keep track of your progress as you cover all the basic concepts a Python programmer needs to know, - as well as touching on more advanced areas such as web development with Django, - natural language processing with NLTK, and data science with NumPy, pandas, and scikit-learn! - It requires a paid subscription, but a free trial is available. -name: JetBrains Academy -title_url: https://www.jetbrains.com/academy/ -tags: - topics: - - general - - web development - - data science - payment_tiers: - - subscription - difficulty: - - beginner - type: - - interactive diff --git a/pydis_site/apps/resources/resources/jetbrains_videos.yaml b/pydis_site/apps/resources/resources/jetbrains_videos.yaml deleted file mode 100644 index 00d34e69..00000000 --- a/pydis_site/apps/resources/resources/jetbrains_videos.yaml +++ /dev/null @@ -1,21 +0,0 @@ -description: A collection of videos made by the PyCharm team at JetBrains on subjects such as TDD, - Django, pytest and much more!<br><br> - Episodes of their "What does this package do?" series go over all sorts of libraries in Python - both in the standard library and from the community and give a video explanation of the key concepts. -name: JetBrains YouTube Channel -icon_image: https://upload.wikimedia.org/wikipedia/commons/thumb/1/1a/JetBrains_Logo_2016.svg/1200px-JetBrains_Logo_2016.svg.png -icon_size: 50 -title_image: https://resources.jetbrains.com/storage/products/pycharm/img/meta/pycharm_logo_300x300.png -title_url: https://www.youtube.com/channel/UCak6beUTLlVmf0E4AmnQkmw -tags: - topics: - - general - - testing - - web development - payment_tiers: - - free - difficulty: - - beginner - - intermediate - type: - - video diff --git a/pydis_site/apps/resources/resources/jim_shaped_coding.yaml b/pydis_site/apps/resources/resources/jim_shaped_coding.yaml deleted file mode 100644 index c9727888..00000000 --- a/pydis_site/apps/resources/resources/jim_shaped_coding.yaml +++ /dev/null @@ -1,22 +0,0 @@ -description: 'JimShapedCoding contains a set of YouTube tutorials covering things from Flask to GUI development in Python: - <ul> - <li><a href="https://www.youtube.com/playlist?list=PLOkVupluCIjuPtTkhO6jmA76uQR994Wvi">Flask tutorials</a></li> - <li><a href="https://www.youtube.com/watch?v=0tqZ6rMcqGE&list=PLOkVupluCIjuAzAmDNUXcD-0Fsb8sbv9F">GUI tutorials</a></li> - <li><a href="https://www.youtube.com/watch?v=qMrAFscMBBc&list=PLOkVupluCIjvORWaF4kG-sXLgbVemYpEi">Django tutorials</a></li> - </ul> - Check out his channel for more videos!' -name: JimShapedCoding -title_image: https://i.imgur.com/DlovZPf.png -title_url: https://www.youtube.com/channel/UCU8d7rcShA7MGuDyYH1aWGg -tags: - topics: - - general - - user interface - - web development - payment_tiers: - - free - difficulty: - - beginner - - intermediate - type: - - video diff --git a/pydis_site/apps/resources/resources/kaggle_pandas_tutorial.yaml b/pydis_site/apps/resources/resources/kaggle_pandas_tutorial.yaml deleted file mode 100644 index c8e72c6e..00000000 --- a/pydis_site/apps/resources/resources/kaggle_pandas_tutorial.yaml +++ /dev/null @@ -1,13 +0,0 @@ -description: An interactive tutorial for learning Pandas, the most popular library for manipulating tabular data - in Python's data science ecosystem. This tutorial assumes some familiarity with writing code in notebooks. -name: Kaggle Pandas Tutorial -title_url: https://www.kaggle.com/learn/pandas -tags: - topics: - - data science - payment_tiers: - - free - difficulty: - - intermediate - type: - - tutorial diff --git a/pydis_site/apps/resources/resources/kivy.yaml b/pydis_site/apps/resources/resources/kivy.yaml deleted file mode 100644 index b1f57483..00000000 --- a/pydis_site/apps/resources/resources/kivy.yaml +++ /dev/null @@ -1,29 +0,0 @@ -name: Kivy -description: The Kivy project, through the Kivy framework and its sister projects, - aims to provide all the tools to create desktop and mobile applications in Python. - Allowing rapid development of multitouch applications with custom and exciting user interfaces. -icon_image: https://raw.githubusercontent.com/kivy/kivy-website/master/logos/kivy-logo-black-256.png -icon_size: 50 -title_image: https://i.imgur.com/EVP3jZR.png -title_url: https://discord.gg/djPtTRJ -urls: - - icon: solid/external-link-alt - url: https://kivy.org/ - color: teal - - icon: branding/discord - url: https://discord.gg/djPtTRJ - color: blurple - - icon: branding/github - url: https://github.com/kivy - color: black -tags: - topics: - - user interface - - game development - payment_tiers: - - free - difficulty: - - beginner - - intermediate - type: - - community diff --git a/pydis_site/apps/resources/resources/microsoft.yaml b/pydis_site/apps/resources/resources/microsoft.yaml deleted file mode 100644 index 290283cc..00000000 --- a/pydis_site/apps/resources/resources/microsoft.yaml +++ /dev/null @@ -1,20 +0,0 @@ -name: Microsoft Python -description: Microsoft Python is a Discord server for discussing all things relating to using Python with Microsoft products, - they have channels for Azure, VS Code, IoT, Data Science and much more! -title_image: https://1000logos.net/wp-content/uploads/2017/04/Microsoft-Logo.png -title_url: https://www.microsoft.com/en-us/boards/pycon2020.aspx -urls: - - icon: branding/discord - url: https://discord.gg/b8YJQPx - color: blurple -tags: - topics: - - general - - tooling - payment_tiers: - - free - difficulty: - - beginner - - intermediate - type: - - community diff --git a/pydis_site/apps/resources/resources/microsoft_videos.yaml b/pydis_site/apps/resources/resources/microsoft_videos.yaml deleted file mode 100644 index f45aef63..00000000 --- a/pydis_site/apps/resources/resources/microsoft_videos.yaml +++ /dev/null @@ -1,26 +0,0 @@ -description: A trove of tutorials & guides for developers from Microsoft's Developer hub. - Follow the links below for a series of high-quality Python tutorials for beginners. - <ul> - <li><a href="https://www.youtube.com/playlist?list=PLlrxD0HtieHhS8VzuMCfQD4uJ9yne1mE6">Python for Beginners</a></li> - <li><a href="https://www.youtube.com/playlist?list=PLlrxD0HtieHiXd-nEby-TMCoUNwhbLUnj">More Python for Beginners</a></li> - <li><a href="https://www.youtube.com/playlist?list=PLlrxD0HtieHhHnCUVtR8UHS7eLl33zfJ-">Even More Python for Beginners - Data Tools</a></li> - </ul> - Microsoft's Python Development Team also runs a Discord Server for discussions of Python in the Microsoft ecosystem, - including Visual Studio Code and Azure. -name: Microsoft Developer -title_image: https://upload.wikimedia.org/wikipedia/commons/4/44/Microsoft_logo.svg -title_url: https://www.youtube.com/channel/UCsMica-v34Irf9KVTh6xx-g -urls: - - icon: branding/discord - url: https://aka.ms/python-discord - color: blurple -tags: - topics: - - general - payment_tiers: - - free - difficulty: - - beginner - type: - - video - - community diff --git a/pydis_site/apps/resources/resources/mission_python.yaml b/pydis_site/apps/resources/resources/mission_python.yaml deleted file mode 100644 index 391a2983..00000000 --- a/pydis_site/apps/resources/resources/mission_python.yaml +++ /dev/null @@ -1,20 +0,0 @@ -description: Learn programming and Python while building a complete and awesome space-themed - game using cutting-edge Python 3.6 and Pygame Zero. Extensive use of code examples, - images, and walk-throughs make this a pleasure to both read and follow along. Excellent - book for beginners. -name: Mission Python -title_url: https://www.sean.co.uk/books/mission-python/index.shtm -urls: -- icon: branding/goodreads - url: https://www.goodreads.com/book/show/35545850-mission-python - color: black -tags: - topics: - - general - - game development - payment_tiers: - - paid - difficulty: - - beginner - type: - - book diff --git a/pydis_site/apps/resources/resources/mit_introduction_to_computer_science_and_programming.yaml b/pydis_site/apps/resources/resources/mit_introduction_to_computer_science_and_programming.yaml deleted file mode 100644 index 4e74936d..00000000 --- a/pydis_site/apps/resources/resources/mit_introduction_to_computer_science_and_programming.yaml +++ /dev/null @@ -1,16 +0,0 @@ -description: This MITx offering teaches computer science with Python. - It covers computational thinking, algorithms, data structures - and the Python programming language itself. -name: 'MIT: Introduction to Computer Science and Programming' -title_url: https://www.edx.org/course/introduction-computer-science-mitx-6-00-1x-11 -tags: - topics: - - general - - algorithms and data structures - payment_tiers: - - free - - paid - difficulty: - - beginner - type: - - course diff --git a/pydis_site/apps/resources/resources/mu_editor.yaml b/pydis_site/apps/resources/resources/mu_editor.yaml deleted file mode 100644 index b6318d0e..00000000 --- a/pydis_site/apps/resources/resources/mu_editor.yaml +++ /dev/null @@ -1,15 +0,0 @@ -description: An editor aimed at beginners for the purpose of learning how to code - without the distractions more advanced editors sometimes cause. - Particularly useful for use with microcontrollers, - with built-in tools to interact with Adafruit and Arduino boards. -name: Mu-Editor -title_url: https://codewith.mu/ -tags: - topics: - - microcontrollers - payment_tiers: - - free - difficulty: - - beginner - type: - - tool diff --git a/pydis_site/apps/resources/resources/netbats_project_ideas.yaml b/pydis_site/apps/resources/resources/netbats_project_ideas.yaml deleted file mode 100644 index 80ba771c..00000000 --- a/pydis_site/apps/resources/resources/netbats_project_ideas.yaml +++ /dev/null @@ -1,14 +0,0 @@ -description: A repository of project ideas to help one apply what they're learning, maintained by Python - community member Ned Batchelder, known on Python Discord as nedbat. -name: Ned Batchelder's Kindling Projects -title_url: https://nedbatchelder.com/text/kindling.html -tags: - topics: - - general - payment_tiers: - - free - difficulty: - - beginner - - intermediate - type: - - project ideas diff --git a/pydis_site/apps/resources/resources/neural_networks_from_scratch_in_python.yaml b/pydis_site/apps/resources/resources/neural_networks_from_scratch_in_python.yaml deleted file mode 100644 index 26e88cb9..00000000 --- a/pydis_site/apps/resources/resources/neural_networks_from_scratch_in_python.yaml +++ /dev/null @@ -1,19 +0,0 @@ -description: '"Neural Networks From Scratch" is a book intended to teach you how to build neural networks on your own, - without any libraries, so you can better understand deep learning and how all of the elements work. - This is so you can go out and do new/novel things with deep learning as well as to become more successful with even more basic models. - This book is to accompany the usual free tutorial videos and sample code from youtube.com/sentdex.' -name: Neural Networks from Scratch -title_url: https://nnfs.io/ -urls: - - icon: branding/goodreads - url: https://www.goodreads.com/book/show/55927899-neural-networks-from-scratch-in-python - color: black -tags: - topics: - - data science - payment_tiers: - - paid - difficulty: - - intermediate - type: - - book diff --git a/pydis_site/apps/resources/resources/pallets.yaml b/pydis_site/apps/resources/resources/pallets.yaml deleted file mode 100644 index a330b756..00000000 --- a/pydis_site/apps/resources/resources/pallets.yaml +++ /dev/null @@ -1,20 +0,0 @@ -name: Pallets Projects -description: The Pallets Projects develop Python libraries such as the Flask web framework, - the Jinja templating library, and the Click command line toolkit. Join to discuss - and get help from the Pallets community. -title_image: https://i.imgur.com/sV9Ypdf.png -title_url: https://www.palletsprojects.com/ -urls: - - icon: branding/discord - url: https://discord.gg/t6rrQZH - color: blurple -tags: - topics: - - web development - payment_tiers: - - free - difficulty: - - beginner - - intermediate - type: - - community diff --git a/pydis_site/apps/resources/resources/panda3d.yaml b/pydis_site/apps/resources/resources/panda3d.yaml deleted file mode 100644 index eeb54465..00000000 --- a/pydis_site/apps/resources/resources/panda3d.yaml +++ /dev/null @@ -1,24 +0,0 @@ -name: Panda3D -description: Panda3D is a Python-focused 3-D framework for rapid development of games, - visualizations, and simulations, written in C++ with an emphasis on performance and flexibility. -title_image: https://www.panda3d.org/wp-content/uploads/2019/01/panda3d_logo.png -title_url: https://discord.gg/9XsucTT -position: 9 -urls: - - icon: solid/external-link-alt - url: https://www.panda3d.org/ - color: teal - - icon: branding/discord - url: https://discord.gg/9XsucTT - color: blurple -tags: - topics: - - user interface - - game development - payment_tiers: - - free - difficulty: - - beginner - - intermediate - type: - - community diff --git a/pydis_site/apps/resources/resources/people_postgres_data.yaml b/pydis_site/apps/resources/resources/people_postgres_data.yaml deleted file mode 100644 index 9fec6634..00000000 --- a/pydis_site/apps/resources/resources/people_postgres_data.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: People, Postgres, Data -description: People, Postgres, Data specializes in building users of Postgres - and related ecosystem including but not limited to technologies such as RDS Postgres, - Aurora for Postgres, Google Postgres, PostgreSQL.Org Postgres, Greenplum, Timescale and ZomboDB. - They take a holistic approach to their community inviting not only technical topics but Professional Development - and Life in general including movies, games, books and travel. -title_image: https://media.discordapp.net/attachments/748954447857844318/750519488268730377/people_postgres_data.png -title_url: https://discord.gg/Ujw8m8v -urls: - - icon: solid/external-link-alt - url: https://postgresconf.org/ - color: teal - - icon: branding/discord - url: https://discord.gg/Ujw8m8v - color: bluple - - icon: branding/reddit - url: https://reddit.com/r/postgresql - color: orangered -tags: - topics: - - databases - payment_tiers: - - free - difficulty: - - beginner - - intermediate - type: - - community diff --git a/pydis_site/apps/resources/resources/podcast_dunder_init.yaml b/pydis_site/apps/resources/resources/podcast_dunder_init.yaml deleted file mode 100644 index 2751481a..00000000 --- a/pydis_site/apps/resources/resources/podcast_dunder_init.yaml +++ /dev/null @@ -1,14 +0,0 @@ -description: The podcast about Python and the people who make it great. Weekly long-form - interviews with the creators of notable Python packages. -name: Podcast.__init__ -title_url: https://www.podcastinit.com/ -tags: - topics: - - general - payment_tiers: - - free - difficulty: - - beginner - - intermediate - type: - - podcast diff --git a/pydis_site/apps/resources/resources/practical_python_programming.yaml b/pydis_site/apps/resources/resources/practical_python_programming.yaml deleted file mode 100644 index 12873b7c..00000000 --- a/pydis_site/apps/resources/resources/practical_python_programming.yaml +++ /dev/null @@ -1,18 +0,0 @@ -description: Created and taught by <a href="https://dabeaz.com/">David Beazley</a>, - this course is a conversion of his instructor-led Python training course used for corporate training - and professional development. It has been in continual development since 2007 - and battle tested in real-world classrooms. Usually, it’s taught in-person over the span of three - or four days–requiring approximately 25-35 hours of intense work. - This includes the completion of approximately 130 hands-on coding exercises. -name: Practical Python Programming -title_url: https://dabeaz-course.github.io/practical-python/ -position: 4 -tags: - topics: - - general - payment_tiers: - - free - difficulty: - - beginner - type: - - course diff --git a/pydis_site/apps/resources/resources/pycharm.yaml b/pydis_site/apps/resources/resources/pycharm.yaml deleted file mode 100644 index e8c787e6..00000000 --- a/pydis_site/apps/resources/resources/pycharm.yaml +++ /dev/null @@ -1,15 +0,0 @@ -description: The very best Python IDE, with a wealth of advanced features and convenience - functions. -name: PyCharm -title_image: https://resources.jetbrains.com/storage/products/pycharm/img/meta/pycharm_logo_300x300.png -title_url: https://www.jetbrains.com/pycharm/ -tags: - topics: - - general - payment_tiers: - - free - - paid - difficulty: - - intermediate - type: - - tool diff --git a/pydis_site/apps/resources/resources/pyglet.yaml b/pydis_site/apps/resources/resources/pyglet.yaml deleted file mode 100644 index bdfb84cf..00000000 --- a/pydis_site/apps/resources/resources/pyglet.yaml +++ /dev/null @@ -1,23 +0,0 @@ -name: Pyglet -description: Pyglet is a powerful, - yet easy to use Python library for developing games and other visually-rich applications on Windows, - Mac OS X and Linux. It supports windowing, user interface event handling, Joysticks, OpenGL graphics, - loading images and videos, and playing sounds and music. All of this with a friendly Pythonic API, - that's simple to learn and doesn't get in your way. -title_image: https://i.imgur.com/LfQwXUe.png -title_url: http://pyglet.org/ -urls: - - icon: branding/discord - url: https://discord.gg/QXyegWe - color: blurple -tags: - topics: - - user interface - - game development - payment_tiers: - - free - difficulty: - - beginner - - intermediate - type: - - community diff --git a/pydis_site/apps/resources/resources/python_bytes.yaml b/pydis_site/apps/resources/resources/python_bytes.yaml deleted file mode 100644 index 9beba4f4..00000000 --- a/pydis_site/apps/resources/resources/python_bytes.yaml +++ /dev/null @@ -1,15 +0,0 @@ -description: A byte-sized podcast where Michael Kennedy and Brian Okken work through - this week's notable Python headlines. -name: Python Bytes -title_url: https://pythonbytes.fm/ -position: 1 -tags: - topics: - - general - payment_tiers: - - free - difficulty: - - beginner - - intermediate - type: - - podcast diff --git a/pydis_site/apps/resources/resources/python_cheat_sheet.yaml b/pydis_site/apps/resources/resources/python_cheat_sheet.yaml deleted file mode 100644 index 56f61165..00000000 --- a/pydis_site/apps/resources/resources/python_cheat_sheet.yaml +++ /dev/null @@ -1,13 +0,0 @@ -description: A Python 3 cheat sheet with useful information and tips, as well as common - pitfalls for beginners. This is a PDF. -name: Python Cheat Sheet -title_url: https://perso.limsi.fr/pointal/_media/python:cours:mementopython3-english.pdf -tags: - topics: - - general - payment_tiers: - - free - difficulty: - - beginner - type: - - tutorial diff --git a/pydis_site/apps/resources/resources/python_cookbook.yaml b/pydis_site/apps/resources/resources/python_cookbook.yaml deleted file mode 100644 index bc05d743..00000000 --- a/pydis_site/apps/resources/resources/python_cookbook.yaml +++ /dev/null @@ -1,21 +0,0 @@ -description: A book full of very smart problem-solving recipes for various Python topics, - including moving from Python 2 to Python 3. -name: Python Cookbook -title_url: http://shop.oreilly.com/product/0636920027072.do -urls: -- icon: branding/goodreads - url: https://www.goodreads.com/book/show/17152735-python-cookbook - color: black -- icon: branding/github - url: https://github.com/dabeaz/python-cookbook - color: black -tags: - topics: - - general - - software design - payment_tiers: - - paid - difficulty: - - intermediate - type: - - book diff --git a/pydis_site/apps/resources/resources/python_crash_course.yaml b/pydis_site/apps/resources/resources/python_crash_course.yaml deleted file mode 100644 index d916075e..00000000 --- a/pydis_site/apps/resources/resources/python_crash_course.yaml +++ /dev/null @@ -1,27 +0,0 @@ -description: "This fast-paced, thorough introduction to programming with Python will have you writing programs, - solving problems, and making things that work in no time. - In the first half of the book, you’ll learn basic programming concepts, such as variables, lists, classes, and loops, - and practice writing clean code with exercises for each topic. - You’ll also learn how to make your programs interactive and test your code safely before adding it to a project. - In the second half, you’ll put your new knowledge into practice with three substantial projects: - a Space Invaders–inspired arcade game, a set of data visualizations with Python’s handy libraries, - and a simple web app you can deploy online." -name: Python Crash Course -title_url: https://nostarch.com/pythoncrashcourse2e -urls: - - icon: branding/goodreads - url: https://www.goodreads.com/book/show/23241059-python-crash-course - color: black - - icon: branding/github - url: https://ehmatthes.github.io/pcc/ - color: black -tags: - topics: - - general - - game development - payment_tiers: - - paid - difficulty: - - beginner - type: - - book diff --git a/pydis_site/apps/resources/resources/python_developer_guide.yaml b/pydis_site/apps/resources/resources/python_developer_guide.yaml deleted file mode 100644 index 2806d75d..00000000 --- a/pydis_site/apps/resources/resources/python_developer_guide.yaml +++ /dev/null @@ -1,13 +0,0 @@ -description: This guide is a comprehensive resource for contributing to Python – for both new and experienced contributors. - It is maintained by the same community that maintains Python. -name: Python Developer's Guide -title_url: https://devguide.python.org/ -tags: - topics: - - general - payment_tiers: - - free - difficulty: - - intermediate - type: - - tutorial diff --git a/pydis_site/apps/resources/resources/python_discord_videos.yaml b/pydis_site/apps/resources/resources/python_discord_videos.yaml deleted file mode 100644 index 012ec8ea..00000000 --- a/pydis_site/apps/resources/resources/python_discord_videos.yaml +++ /dev/null @@ -1,16 +0,0 @@ -name: Python Discord YouTube Channel -description: It's our YouTube channel! We are slowly gathering content here directly related to Python, - our community and the events we host. Come check us out! -title_image: https://raw.githubusercontent.com/python-discord/branding/master/logos/logo_banner/logo_site_banner_dark_512.png -title_url: https://www.youtube.com/pythondiscord -tags: - topics: - - general - - software design - payment_tiers: - - free - difficulty: - - beginner - - intermediate - type: - - video diff --git a/pydis_site/apps/resources/resources/python_morsels.yaml b/pydis_site/apps/resources/resources/python_morsels.yaml deleted file mode 100644 index 4cdff36b..00000000 --- a/pydis_site/apps/resources/resources/python_morsels.yaml +++ /dev/null @@ -1,20 +0,0 @@ -description: 'Learn to write more idiomatic Python code with deliberate practice! - - - Sign up for this service and receive one short Python exercise every week. After - you attempt to work through the exercise, you''ll receive a number of solutions - to the exercise with explanations of each one. Each exercise will include automated - tests and some may include bonuses for a little more of a challenge!' -name: Python Morsels -title_url: https://www.pythonmorsels.com/ -tags: - topics: - - general - - software design - payment_tiers: - - subscription - difficulty: - - intermediate - type: - - interactive - - video diff --git a/pydis_site/apps/resources/resources/python_org.yaml b/pydis_site/apps/resources/resources/python_org.yaml deleted file mode 100644 index ece954dd..00000000 --- a/pydis_site/apps/resources/resources/python_org.yaml +++ /dev/null @@ -1,14 +0,0 @@ -name: The Python Tutorial -description: The official Python tutorial by Python.org -title_image: https://www.python.org/static/community_logos/python-logo-master-v3-TM.png -title_url: https://docs.python.org/3/tutorial/ -tags: - topics: - - general - payment_tiers: - - free - difficulty: - - beginner - - intermediate - type: - - tutorial diff --git a/pydis_site/apps/resources/resources/python_subreddit.yaml b/pydis_site/apps/resources/resources/python_subreddit.yaml deleted file mode 100644 index e94f84fc..00000000 --- a/pydis_site/apps/resources/resources/python_subreddit.yaml +++ /dev/null @@ -1,16 +0,0 @@ -description: News about the Python programming language, and language-related discussion. -name: r/Python -title_icon: branding/reddit -title_icon_color: orangered -title_url: https://www.reddit.com/r/Python/ -position: 0 -tags: - topics: - - general - payment_tiers: - - free - difficulty: - - beginner - - intermediate - type: - - community diff --git a/pydis_site/apps/resources/resources/python_tricks.yaml b/pydis_site/apps/resources/resources/python_tricks.yaml deleted file mode 100644 index aa1b2fcd..00000000 --- a/pydis_site/apps/resources/resources/python_tricks.yaml +++ /dev/null @@ -1,19 +0,0 @@ -description: Full of useful Python tips, tricks and features. Get this if you have - a good grasp of the basics and want to take your Python skills to the next level, - or are a experienced programmer looking to add to your toolbelt. -name: Python Tricks -title_url: https://realpython.com/products/python-tricks-book/ -urls: -- icon: branding/goodreads - url: https://www.goodreads.com/book/show/36990732-python-tricks - color: black -tags: - topics: - - general - - software design - payment_tiers: - - paid - difficulty: - - intermediate - type: - - book diff --git a/pydis_site/apps/resources/resources/python_tutor.yaml b/pydis_site/apps/resources/resources/python_tutor.yaml deleted file mode 100644 index 6bee0d69..00000000 --- a/pydis_site/apps/resources/resources/python_tutor.yaml +++ /dev/null @@ -1,14 +0,0 @@ -description: Write Python code in your web browser, and see it visualized step by step. -name: Python Tutor -title_url: https://www.pythontutor.com/ -tags: - topics: - - general - payment_tiers: - - free - difficulty: - - beginner - - intermediate - type: - - tool - - interactive diff --git a/pydis_site/apps/resources/resources/real_python.yaml b/pydis_site/apps/resources/resources/real_python.yaml deleted file mode 100644 index 93953004..00000000 --- a/pydis_site/apps/resources/resources/real_python.yaml +++ /dev/null @@ -1,22 +0,0 @@ -name: Real Python -description: Dan Bader's treasure trove of quizzes, tutorials and interactive content for learning Python. - An absolute goldmine. -title_image: https://i.imgur.com/WDqhZ36.png -title_url: https://realpython.com/ -position: 3 -urls: - - icon: branding/youtube - url: https://www.youtube.com/channel/UCI0vQvr9aFn27yR6Ej6n5UA - color: youtube-red -tags: - topics: - - general - payment_tiers: - - free - difficulty: - - beginner - - intermediate - type: - - tutorial - - video - - community diff --git a/pydis_site/apps/resources/resources/regex101.yaml b/pydis_site/apps/resources/resources/regex101.yaml deleted file mode 100644 index 45d00f1b..00000000 --- a/pydis_site/apps/resources/resources/regex101.yaml +++ /dev/null @@ -1,15 +0,0 @@ -description: An online tool for testing regular expressions that helps you understand what the regular expression can - match. Remember to set the "flavor" to Python. -name: regex101 -title_url: https://regex101.com/ -tags: - topics: - - general - - other - payment_tiers: - - free - difficulty: - - beginner - - intermediate - type: - - tool diff --git a/pydis_site/apps/resources/resources/repl_it.yaml b/pydis_site/apps/resources/resources/repl_it.yaml deleted file mode 100644 index e0f6cbb3..00000000 --- a/pydis_site/apps/resources/resources/repl_it.yaml +++ /dev/null @@ -1,14 +0,0 @@ -description: A free, collaborative, in-browser IDE to code in 50+ languages — - without spending a second on setup. -name: repl.it -title_url: https://repl.it/ -tags: - topics: - - general - payment_tiers: - - free - difficulty: - - beginner - - intermediate - type: - - tool diff --git a/pydis_site/apps/resources/resources/screen_readers.yaml b/pydis_site/apps/resources/resources/screen_readers.yaml deleted file mode 100644 index b086b301..00000000 --- a/pydis_site/apps/resources/resources/screen_readers.yaml +++ /dev/null @@ -1,17 +0,0 @@ -description: Screen readers are software programs that allow blind - or visually impaired users to read the text displayed on a computer screen with a speech synthesizer or braille display. - There are many different screen reader program options, - with this link describing many of them and their capabilities. -name: Screen Readers - American Foundation for the Blind -title_url: https://www.afb.org/blindness-and-low-vision/using-technology/assistive-technology-products/screen-readers -tags: - topics: - - other - payment_tiers: - - free - - paid - difficulty: - - beginner - - intermediate - type: - - tool diff --git a/pydis_site/apps/resources/resources/sentdex.yaml b/pydis_site/apps/resources/resources/sentdex.yaml deleted file mode 100644 index 7cb0a8a4..00000000 --- a/pydis_site/apps/resources/resources/sentdex.yaml +++ /dev/null @@ -1,33 +0,0 @@ -name: Sentdex -description: 'An enormous amount of Python content for all skill levels - from the most popular Python YouTuber on the web. - <ul> - <li><a href="https://www.youtube.com/playlist?list=PLQVvvaa0QuDeAams7fkdcwOGBpGdHpXln">Learning to program with Python 3 (py 3.7)</a></li> - <li><a href="https://www.youtube.com/playlist?list=PLQVvvaa0QuDfwnDTZWw8H3hN_VRQfq8rF">Kivy - Mobile and Desktop App Dev w/ Python</a></li> - <li><a href="https://www.youtube.com/playlist?list=PLQVvvaa0QuDfSfqQuee6K8opKtZsh7sA9">Data Analysis w/ Python 3 and Pandas</a></li> - <li><a href="https://www.youtube.com/playlist?list=PLQVvvaa0QuDfKTOs3Keq_kaG2P55YRn5v">Machine Learning with Python</a></li> - </ul> - Check out his channel for more video series! - ' -title_image: https://i.imgur.com/kJgWZIu.png -title_url: https://www.youtube.com/user/sentdex -urls: - - icon: solid/external-link-alt - url: https://pythonprogramming.net/ - color: teal - - icon: branding/discord - url: https://discord.gg/sentdex - color: blurple -tags: - topics: - - general - - user interface - - data science - payment_tiers: - - free - difficulty: - - beginner - - intermediate - type: - - video - - community diff --git a/pydis_site/apps/resources/resources/simple_guide_to_git.yaml b/pydis_site/apps/resources/resources/simple_guide_to_git.yaml deleted file mode 100644 index 3bb46e6d..00000000 --- a/pydis_site/apps/resources/resources/simple_guide_to_git.yaml +++ /dev/null @@ -1,14 +0,0 @@ -description: A simple, no-nonsense guide to the basics of using Git. -name: A Simple Guide to Git -title_url: https://rogerdudler.github.io/git-guide/ -title_icon: branding/github -title_icon_color: black -tags: - topics: - - tooling - payment_tiers: - - free - difficulty: - - beginner - type: - - tutorial diff --git a/pydis_site/apps/resources/resources/socratica.yaml b/pydis_site/apps/resources/resources/socratica.yaml deleted file mode 100644 index 45150b33..00000000 --- a/pydis_site/apps/resources/resources/socratica.yaml +++ /dev/null @@ -1,23 +0,0 @@ -name: Socratica -description: 'Socratica is a small studio focused on producing high quality STEM-related educational content, -including a series about Python. Their videos star actress Ulka Simone Mohanty, who plays an android-like -instructor explaining fundamental concepts in a concise and entertaining way.' -title_image: https://i.imgur.com/4SoHeLz.png -title_url: https://www.youtube.com/playlist?list=PLi01XoE8jYohWFPpC17Z-wWhPOSuh8Er- -urls: - - icon: solid/database - url: https://www.youtube.com/playlist?list=PLi01XoE8jYojRqM4qGBF1U90Ee1Ecb5tt - color: teal - - icon: branding/youtube - url: https://www.youtube.com/channel/UCW6TXMZ5Pq6yL6_k5NZ2e0Q - color: youtube-red -tags: - topics: - - general - - databases - payment_tiers: - - free - difficulty: - - beginner - type: - - video diff --git a/pydis_site/apps/resources/resources/sololearn.yaml b/pydis_site/apps/resources/resources/sololearn.yaml deleted file mode 100644 index 998f5368..00000000 --- a/pydis_site/apps/resources/resources/sololearn.yaml +++ /dev/null @@ -1,17 +0,0 @@ -description: SoloLearn's Python 3 course serves as a simple and convenient introduction to Python. - Containing exercises and quizzes in modules to cover introductory subjects of the language, - you can pick it up and put it down between your busier aspects of life thanks to both PC - and mobile apps being available to use. -name: SoloLearn -title_url: https://www.sololearn.com/Course/Python/ -tags: - topics: - - general - payment_tiers: - - free - - subscription - difficulty: - - beginner - type: - - interactive - - course diff --git a/pydis_site/apps/resources/resources/spyder.yaml b/pydis_site/apps/resources/resources/spyder.yaml deleted file mode 100644 index 668e9306..00000000 --- a/pydis_site/apps/resources/resources/spyder.yaml +++ /dev/null @@ -1,14 +0,0 @@ -description: The Scientific Python Development Environment. - Simpler and lighter than PyCharm, but still packs a punch. -name: Spyder -title_url: https://www.spyder-ide.org/ -tags: - topics: - - data science - payment_tiers: - - free - difficulty: - - beginner - - intermediate - type: - - tool diff --git a/pydis_site/apps/resources/resources/sublime_text.yaml b/pydis_site/apps/resources/resources/sublime_text.yaml deleted file mode 100644 index 05596477..00000000 --- a/pydis_site/apps/resources/resources/sublime_text.yaml +++ /dev/null @@ -1,14 +0,0 @@ -description: A powerful Python-backed editor with great community support and a wealth - of extensions. -name: Sublime Text -title_url: https://www.sublimetext.com/ -tags: - topics: - - general - payment_tiers: - - free - difficulty: - - beginner - - intermediate - type: - - tool diff --git a/pydis_site/apps/resources/resources/talk_python_to_me.yaml b/pydis_site/apps/resources/resources/talk_python_to_me.yaml deleted file mode 100644 index 509922c3..00000000 --- a/pydis_site/apps/resources/resources/talk_python_to_me.yaml +++ /dev/null @@ -1,14 +0,0 @@ -description: The essential weekly Python podcast. Michael Kennedy and a prominent - name within the Python community dive into a topic that relates to their experience. -name: Talk Python To Me -title_url: https://talkpython.fm/ -tags: - topics: - - general - payment_tiers: - - free - difficulty: - - beginner - - intermediate - type: - - podcast diff --git a/pydis_site/apps/resources/resources/talon_voice.yaml b/pydis_site/apps/resources/resources/talon_voice.yaml deleted file mode 100644 index 3be5fe20..00000000 --- a/pydis_site/apps/resources/resources/talon_voice.yaml +++ /dev/null @@ -1,15 +0,0 @@ -description: Talon is a tool being built that aims to bring programming, - realtime video gaming, command line, and full desktop computer proficiency to people - who have limited or no use of their hands. -name: Talon Voice -title_url: https://talonvoice.com/ -tags: - topics: - - other - payment_tiers: - - free - difficulty: - - beginner - - intermediate - type: - - tool diff --git a/pydis_site/apps/resources/resources/test_and_code.yaml b/pydis_site/apps/resources/resources/test_and_code.yaml deleted file mode 100644 index f0d1c3b3..00000000 --- a/pydis_site/apps/resources/resources/test_and_code.yaml +++ /dev/null @@ -1,15 +0,0 @@ -description: Brian Okken's weekly podcast on testing. Usually deals with Python, - but also covers many language-agnostic topics from the testing and DevOps world. -name: Test & Code -title_url: https://testandcode.com/ -tags: - topics: - - testing - - tooling - payment_tiers: - - free - difficulty: - - beginner - - intermediate - type: - - podcast diff --git a/pydis_site/apps/resources/resources/the_algorithms_github.yaml b/pydis_site/apps/resources/resources/the_algorithms_github.yaml deleted file mode 100644 index 30a0a5da..00000000 --- a/pydis_site/apps/resources/resources/the_algorithms_github.yaml +++ /dev/null @@ -1,17 +0,0 @@ -description: A git repository of Python implementations of many of the algorithms taught in algorithm - and data structure courses, as well as algorithms for neural networks, block chains, and compression. This is - a great resource for students wanting to see algorithms implemented in a familiar language. -name: The Algorithms -title_url: https://github.com/TheAlgorithms/Python -tags: - topics: - - algorithms and data structures - - data science - - security - payment_tiers: - - free - difficulty: - - beginner - - intermediate - type: - - tutorial diff --git a/pydis_site/apps/resources/resources/the_flask_mega_tutorial.yaml b/pydis_site/apps/resources/resources/the_flask_mega_tutorial.yaml deleted file mode 100644 index 151768a5..00000000 --- a/pydis_site/apps/resources/resources/the_flask_mega_tutorial.yaml +++ /dev/null @@ -1,13 +0,0 @@ -description: Miguel Grinberg's fully featured mega-tutorial for learning how to create web applications with the Flask framework. -name: The Flask Mega-Tutorial -title_url: https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-i-hello-world -tags: - topics: - - web development - payment_tiers: - - free - difficulty: - - beginner - - intermediate - type: - - tutorial diff --git a/pydis_site/apps/resources/resources/the_real_python_podcast.yaml b/pydis_site/apps/resources/resources/the_real_python_podcast.yaml deleted file mode 100644 index 647779d5..00000000 --- a/pydis_site/apps/resources/resources/the_real_python_podcast.yaml +++ /dev/null @@ -1,16 +0,0 @@ -description: A weekly Python podcast hosted by Christopher Bailey with interviews, - coding tips, and conversation with guests from the Python community. - The show covers a wide range of topics including Python programming best practices, - career tips, and related software development topics. -name: The Real Python Podcast -title_url: https://realpython.com/podcasts/rpp/ -tags: - topics: - - general - payment_tiers: - - free - difficulty: - - beginner - - intermediate - type: - - podcast diff --git a/pydis_site/apps/resources/resources/think_python.yaml b/pydis_site/apps/resources/resources/think_python.yaml deleted file mode 100644 index 7099afd8..00000000 --- a/pydis_site/apps/resources/resources/think_python.yaml +++ /dev/null @@ -1,24 +0,0 @@ -description: Think Python is an introduction to Python programming for beginners. - It starts with basic concepts of programming, - and is carefully designed to define all terms when they are first used and to develop each new concept in a logical progression. - Larger pieces, like recursion and object-oriented programming are divided into a sequence of smaller steps - and introduced over the course of several chapters. -name: Think Python -title_url: https://greenteapress.com/wp/think-python-2e/ -urls: - - icon: branding/goodreads - url: https://www.goodreads.com/book/show/14514306-think-python - color: black - - icon: branding/github - url: https://github.com/AllenDowney/ThinkPython2 - color: black -tags: - topics: - - general - - software design - payment_tiers: - - paid - difficulty: - - beginner - type: - - book diff --git a/pydis_site/apps/resources/resources/thonny.yaml b/pydis_site/apps/resources/resources/thonny.yaml deleted file mode 100644 index 29ba9e07..00000000 --- a/pydis_site/apps/resources/resources/thonny.yaml +++ /dev/null @@ -1,14 +0,0 @@ -description: A Python IDE specifically aimed at learning programming. Has a lot of - helpful features to help you understand your code. -name: Thonny -title_url: https://thonny.org/ -position: 2 -tags: - topics: - - general - payment_tiers: - - free - difficulty: - - beginner - type: - - tool diff --git a/pydis_site/apps/resources/resources/two_scoops_of_django.yaml b/pydis_site/apps/resources/resources/two_scoops_of_django.yaml deleted file mode 100644 index f372d35d..00000000 --- a/pydis_site/apps/resources/resources/two_scoops_of_django.yaml +++ /dev/null @@ -1,20 +0,0 @@ -description: Tips, tricks, and best practices for your Django project. - A highly recommended resource for Django web developers. -name: Two Scoops of Django -title_url: https://www.feldroy.com/books/two-scoops-of-django-3-x -urls: -- icon: branding/goodreads - url: https://www.goodreads.com/book/show/55822151-two-scoops-of-django-3-x - color: black -- icon: branding/github - url: https://github.com/twoscoops/two-scoops-of-django-2.0-code-examples - color: black -tags: - topics: - - web development - payment_tiers: - - paid - difficulty: - - intermediate - type: - - book diff --git a/pydis_site/apps/resources/resources/university_of_michigan.yaml b/pydis_site/apps/resources/resources/university_of_michigan.yaml deleted file mode 100644 index 7aaaf2ae..00000000 --- a/pydis_site/apps/resources/resources/university_of_michigan.yaml +++ /dev/null @@ -1,13 +0,0 @@ -description: A 5-part specialization course that teaches Python from scratch. - The course has no pre-requisites and avoids all but the simplest mathematics. -name: 'University of Michigan: Programming for Everybody' -title_url: https://www.coursera.org/learn/python -tags: - topics: - - general - payment_tiers: - - free - difficulty: - - beginner - type: - - course diff --git a/pydis_site/apps/resources/resources/university_of_toronto.yaml b/pydis_site/apps/resources/resources/university_of_toronto.yaml deleted file mode 100644 index 94df96f2..00000000 --- a/pydis_site/apps/resources/resources/university_of_toronto.yaml +++ /dev/null @@ -1,20 +0,0 @@ -description: A 2-part course that teaches Python. Primarily intended for high school students - and first-year university students who want to learn programming. -name: 'University of Toronto: Learn to Program' -urls: - - icon: regular/graduation-cap - url: https://www.coursera.org/learn/learn-to-program - color: orangered - - icon: regular/graduation-cap - url: https://www.coursera.org/learn/program-code - color: youtube-red -tags: - topics: - - general - payment_tiers: - - free - difficulty: - - beginner - - intermediate - type: - - course diff --git a/pydis_site/apps/resources/resources/vcokltfre_discord_bot_tutorial.yaml b/pydis_site/apps/resources/resources/vcokltfre_discord_bot_tutorial.yaml deleted file mode 100644 index 482cdf91..00000000 --- a/pydis_site/apps/resources/resources/vcokltfre_discord_bot_tutorial.yaml +++ /dev/null @@ -1,14 +0,0 @@ -description: This tutorial, written by vcokltfre, - will walk you through all the aspects of creating your own Discord bot, - starting from creating the bot user itself. -name: vcokltfre's Discord Bot Tutorial -title_url: https://tutorial.vco.sh/ -tags: - topics: - - discord bots - payment_tiers: - - free - difficulty: - - intermediate - type: - - tutorial diff --git a/pydis_site/apps/resources/resources/visual_studio_code.yaml b/pydis_site/apps/resources/resources/visual_studio_code.yaml deleted file mode 100644 index 3cf858f8..00000000 --- a/pydis_site/apps/resources/resources/visual_studio_code.yaml +++ /dev/null @@ -1,13 +0,0 @@ -description: A fully-featured editor based on Electron, extendable with plugins. -name: Visual Studio Code -title_url: https://code.visualstudio.com/ -tags: - topics: - - general - payment_tiers: - - free - difficulty: - - beginner - - intermediate - type: - - tool diff --git a/pydis_site/apps/resources/resources/wtf_python.yaml b/pydis_site/apps/resources/resources/wtf_python.yaml deleted file mode 100644 index 6d90ba39..00000000 --- a/pydis_site/apps/resources/resources/wtf_python.yaml +++ /dev/null @@ -1,18 +0,0 @@ -description: Python, being a beautifully designed high-level and interpreter-based programming language, - provides us with many features for the programmer's comfort. - But sometimes, the outcomes of a Python snippet may not seem obvious at first sight. - Here's a fun project attempting to explain what exactly is happening under the hood for some counter-intuitive snippets - and lesser-known features in Python. -name: WTF Python -title_url: https://github.com/satwikkansal/wtfpython -position: 7 -tags: - topics: - - software design - - other - payment_tiers: - - free - difficulty: - - intermediate - type: - - tutorial diff --git a/pydis_site/apps/resources/templatetags/__init__.py b/pydis_site/apps/resources/templatetags/__init__.py deleted file mode 100644 index 2b266b94..00000000 --- a/pydis_site/apps/resources/templatetags/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .as_icon import as_icon - -__all__ = ["as_icon"] diff --git a/pydis_site/apps/resources/templatetags/as_icon.py b/pydis_site/apps/resources/templatetags/as_icon.py deleted file mode 100644 index b211407c..00000000 --- a/pydis_site/apps/resources/templatetags/as_icon.py +++ /dev/null @@ -1,14 +0,0 @@ -from django import template - -register = template.Library() - - -def as_icon(icon: str) -> str: - """Convert icon string in format 'type/icon' to fa-icon HTML classes.""" - icon_type, icon_name = icon.split("/") - if icon_type.lower() == "branding": - icon_type = "fab" - else: - icon_type = "fas" - return f'{icon_type} fa-{icon_name}' diff --git a/pydis_site/apps/resources/templatetags/get_category_icon.py b/pydis_site/apps/resources/templatetags/get_category_icon.py deleted file mode 100644 index 30bc4eaa..00000000 --- a/pydis_site/apps/resources/templatetags/get_category_icon.py +++ /dev/null @@ -1,40 +0,0 @@ -from django import template - -register = template.Library() - -_ICONS = { - "Algorithms And Data Structures": "fa-cogs", - "Beginner": "fa-play-circle", - "Book": "fa-book", - "Community": "fa-users", - "Course": "fa-chalkboard-teacher", - "Data Science": "fa-flask", - "Databases": "fa-server", - "Discord Bots": "fa-robot", - "Free": "fa-first-aid", - "Game Development": "fa-gamepad", - "General": "fa-book", - "Interactive": "fa-mouse-pointer", - "Intermediate": "fa-align-center", - "Microcontrollers": "fa-microchip", - "Other": "fa-question-circle", - "Paid": "fa-dollar-sign", - "Podcast": "fa-microphone-alt", - "Project Ideas": "fa-lightbulb-o", - "Security": "fa-solid fa-lock", - "Software Design": "fa-paint-brush", - "Subscription": "fa-credit-card", - "Testing": "fa-vial", - "Tool": "fa-tools", - "Tooling": "fa-toolbox", - "Tutorial": "fa-clipboard-list", - "User Interface": "fa-desktop", - "Video": "fa-video", - "Web Development": "fa-wifi", -} - - -def get_category_icon(name: str) -> str: - """Get icon of a specific resource category.""" - return f'fa {_ICONS[name]}' diff --git a/pydis_site/apps/resources/templatetags/to_kebabcase.py b/pydis_site/apps/resources/templatetags/to_kebabcase.py deleted file mode 100644 index 41e2ac85..00000000 --- a/pydis_site/apps/resources/templatetags/to_kebabcase.py +++ /dev/null @@ -1,39 +0,0 @@ -import re - -from django import template - -REGEX_CONSECUTIVE_NON_LETTERS = r"[^A-Za-z0-9]+" -register = template.Library() - - -def _to_kebabcase(class_name: str) -> str: - """ - Convert any string to kebab-case. - - For example, convert - "__Favorite FROOT¤#/$?is----LeMON???" to - "favorite-froot-is-lemon" - """ - # First, make it lowercase, and just remove any apostrophes. - # We remove the apostrophes because "wasnt" is better than "wasn-t" - class_name = class_name.casefold() - class_name = class_name.replace("'", '') - - # Now, replace any non-letter that remains with a dash. - # If there are multiple consecutive non-letters, just replace them with a single dash. - # my-favorite-class is better than my-favorite------class - class_name = re.sub( - REGEX_CONSECUTIVE_NON_LETTERS, - "-", - class_name, - ) - - # Now we use strip to get rid of any leading or trailing dashes. - class_name = class_name.strip("-") - return class_name - - -def to_kebabcase(class_name: str) -> str: - """Convert a string to kebab-case.""" - return _to_kebabcase(class_name) diff --git a/pydis_site/apps/resources/tests/__init__.py b/pydis_site/apps/resources/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pydis_site/apps/resources/tests/__init__.py +++ /dev/null diff --git a/pydis_site/apps/resources/tests/test_as_icon.py b/pydis_site/apps/resources/tests/test_as_icon.py deleted file mode 100644 index 5b33910d..00000000 --- a/pydis_site/apps/resources/tests/test_as_icon.py +++ /dev/null @@ -1,28 +0,0 @@ -from django.test import TestCase - -from pydis_site.apps.resources.templatetags import as_icon - - -class TestAsIcon(TestCase): - """Tests for `as_icon` templatetag.""" - - def test_as_icon(self): - """Should return proper icon type class and icon class based on input.""" - test_cases = [ - { - "input": "regular/icon", - "output": "fas fa-icon", - }, - { - "input": "branding/brand", - "output": "fab fa-brand", - }, - { - "input": "fake/my-icon", - "output": "fas fa-my-icon", - } - ] - - for case in test_cases: - with self.subTest(input=case["input"], output=case["output"]): - self.assertEqual(case["output"], as_icon(case["input"])) diff --git a/pydis_site/apps/resources/tests/test_to_kebabcase.py b/pydis_site/apps/resources/tests/test_to_kebabcase.py deleted file mode 100644 index a141143d..00000000 --- a/pydis_site/apps/resources/tests/test_to_kebabcase.py +++ /dev/null @@ -1,19 +0,0 @@ -from django.test import TestCase - -from pydis_site.apps.resources.templatetags.to_kebabcase import _to_kebabcase - - -class TestToKebabcase(TestCase): - """Tests for the `as_css_class` template tag.""" - - def test_to_kebabcase(self): - """Test the to_kebabcase utility and template tag.""" - weird_input = ( - "_-_--_A_LEm0n?in&¤'the##trEE£$@€@€@@£is-NOT----QUITE//" - "as#good! as one __IN-YOUR|||HaND" - ) - - self.assertEqual( - _to_kebabcase(weird_input), - "a-lem0n-in-the-tree-is-not-quite-as-good-as-one-in-your-hand", - ) diff --git a/pydis_site/apps/resources/tests/test_views.py b/pydis_site/apps/resources/tests/test_views.py deleted file mode 100644 index a2a203ce..00000000 --- a/pydis_site/apps/resources/tests/test_views.py +++ /dev/null @@ -1,29 +0,0 @@ -from pathlib import Path - -from django.conf import settings -from django.test import TestCase -from django.urls import reverse - -TESTING_RESOURCES_PATH = Path( - settings.BASE_DIR, "pydis_site", "apps", "resources", "tests", "testing_resources" -) - - -class TestResourcesView(TestCase): - def test_resources_index_200(self): - """Check does index of resources app return 200 HTTP response.""" - url = reverse("resources:index") - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - def test_resources_with_valid_argument(self): - """Check that you can resolve the resources when passing a valid argument.""" - url = reverse("resources:index", kwargs={"resource_type": "book"}) - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - def test_resources_with_invalid_argument(self): - """Check that you can resolve the resources when passing an invalid argument.""" - url = reverse("resources:index", kwargs={"resource_type": "urinal-cake"}) - response = self.client.get(url) - self.assertEqual(response.status_code, 404) diff --git a/pydis_site/apps/resources/urls.py b/pydis_site/apps/resources/urls.py deleted file mode 100644 index ed24dc99..00000000 --- a/pydis_site/apps/resources/urls.py +++ /dev/null @@ -1,9 +0,0 @@ -from django_distill import distill_path - -from pydis_site.apps.resources import views - -app_name = "resources" -urlpatterns = [ - distill_path("", views.resources.ResourceView.as_view(), name="index"), - distill_path("<resource_type>/", views.resources.ResourceView.as_view(), name="index"), -] diff --git a/pydis_site/apps/resources/views/__init__.py b/pydis_site/apps/resources/views/__init__.py deleted file mode 100644 index 986f3e10..00000000 --- a/pydis_site/apps/resources/views/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .resources import ResourceView - -__all__ = ["ResourceView"] diff --git a/pydis_site/apps/resources/views/resources.py b/pydis_site/apps/resources/views/resources.py deleted file mode 100644 index 2375f722..00000000 --- a/pydis_site/apps/resources/views/resources.py +++ /dev/null @@ -1,126 +0,0 @@ -import json -import typing as t -from pathlib import Path - -import yaml -from django.core.handlers.wsgi import WSGIRequest -from django.http import HttpResponse, HttpResponseNotFound -from django.shortcuts import render -from django.views import View - -from pydis_site import settings -from pydis_site.apps.resources.templatetags.to_kebabcase import to_kebabcase - -RESOURCES_PATH = Path(settings.BASE_DIR, "pydis_site", "apps", "resources", "resources") - - -class ResourceView(View): - """Our curated list of good learning resources.""" - - @staticmethod - def _sort_key_disregard_the(tuple_: tuple) -> str: - """Sort a tuple by its key alphabetically, disregarding 'the' as a prefix.""" - name, resource = tuple_ - name = name.casefold() - if name.startswith("the ") or name.startswith("the_"): - return name[4:] - return name - - def __init__(self, *args, **kwargs): - """Set up all the resources.""" - super().__init__(*args, **kwargs) - - # Load the resources from the yaml files in /resources/ - self.resources = { - path.stem: yaml.safe_load(path.read_text()) - for path in RESOURCES_PATH.rglob("*.yaml") - } - - # Sort the resources alphabetically - self.resources = dict(sorted(self.resources.items(), key=self._sort_key_disregard_the)) - - # Parse out all current tags - resource_tags = { - "topics": set(), - "payment_tiers": set(), - "difficulty": set(), - "type": set(), - } - for resource_name, resource in self.resources.items(): - css_classes = [] - for tag_type in resource_tags.keys(): - # Store the tags into `resource_tags` - tags = resource.get("tags", {}).get(tag_type, []) - for tag in tags: - tag = tag.title() - tag = tag.replace("And", "and") - resource_tags[tag_type].add(tag) - - # Make a CSS class friendly representation too, while we're already iterating. - for tag in tags: - css_tag = to_kebabcase(f"{tag_type}-{tag}") - css_classes.append(css_tag) - - # Now add the css classes back to the resource, so we can use them in the template. - self.resources[resource_name]["css_classes"] = " ".join(css_classes) - - # Set up all the filter checkbox metadata - self.filters = { - "Difficulty": { - "filters": sorted(resource_tags.get("difficulty")), - "icon": "fas fa-brain", - "hidden": False, - }, - "Type": { - "filters": sorted(resource_tags.get("type")), - "icon": "fas fa-photo-video", - "hidden": False, - }, - "Payment tiers": { - "filters": sorted(resource_tags.get("payment_tiers")), - "icon": "fas fa-dollar-sign", - "hidden": True, - }, - "Topics": { - "filters": sorted(resource_tags.get("topics")), - "icon": "fas fa-lightbulb", - "hidden": True, - } - } - - # The bottom topic should always be "Other". - self.filters["Topics"]["filters"].remove("Other") - self.filters["Topics"]["filters"].append("Other") - - # A complete list of valid filter names - self.valid_filters = { - "topics": [to_kebabcase(topic) for topic in self.filters["Topics"]["filters"]], - "payment_tiers": [ - to_kebabcase(tier) for tier in self.filters["Payment tiers"]["filters"] - ], - "type": [to_kebabcase(type_) for type_ in self.filters["Type"]["filters"]], - "difficulty": [to_kebabcase(tier) for tier in self.filters["Difficulty"]["filters"]], - } - - def get(self, request: WSGIRequest, resource_type: t.Optional[str] = None) -> HttpResponse: - """List out all the resources, and any filtering options from the URL.""" - # Add type filtering if the request is made to somewhere like /resources/video. - # We also convert all spaces to dashes, so they'll correspond with the filters. - if resource_type: - dashless_resource_type = resource_type.replace("-", " ") - - if dashless_resource_type.title() not in self.filters["Type"]["filters"]: - return HttpResponseNotFound() - - resource_type = resource_type.replace(" ", "-") - - return render( - request, - template_name="resources/resources.html", - context={ - "resources": self.resources, - "filters": self.filters, - "valid_filters": json.dumps(self.valid_filters), - "resource_type": resource_type, - } - ) diff --git a/pydis_site/apps/staff/__init__.py b/pydis_site/apps/staff/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pydis_site/apps/staff/__init__.py +++ /dev/null diff --git a/pydis_site/apps/staff/apps.py b/pydis_site/apps/staff/apps.py deleted file mode 100644 index d68a80c3..00000000 --- a/pydis_site/apps/staff/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig - - -class StaffConfig(AppConfig): - """Django AppConfig for the staff app.""" - - name = 'pydis_site.apps.staff' diff --git a/pydis_site/apps/staff/migrations/0001_initial.py b/pydis_site/apps/staff/migrations/0001_initial.py deleted file mode 100644 index 7748e553..00000000 --- a/pydis_site/apps/staff/migrations/0001_initial.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 2.2.6 on 2019-10-03 18:24 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('auth', '0011_update_proxy_permissions'), - ('api', '0043_infraction_hidden_warnings_to_notes'), - ] - - operations = [ - migrations.CreateModel( - name='RoleMapping', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('group', models.OneToOneField(help_text='The Django permissions group to use for this mapping.', on_delete=django.db.models.deletion.CASCADE, to='auth.Group')), - ('role', models.OneToOneField(help_text='The Discord role to use for this mapping.', on_delete=django.db.models.deletion.CASCADE, to='api.Role')), - ], - ), - ] diff --git a/pydis_site/apps/staff/migrations/0002_add_is_staff_to_role_mappings.py b/pydis_site/apps/staff/migrations/0002_add_is_staff_to_role_mappings.py deleted file mode 100644 index 0404d270..00000000 --- a/pydis_site/apps/staff/migrations/0002_add_is_staff_to_role_mappings.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.6 on 2019-10-20 14:19 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('staff', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='rolemapping', - name='is_staff', - field=models.BooleanField(default=False, help_text='Whether this role mapping relates to a Django staff group'), - ), - ] diff --git a/pydis_site/apps/staff/migrations/0003_delete_rolemapping.py b/pydis_site/apps/staff/migrations/0003_delete_rolemapping.py deleted file mode 100644 index e9b6114e..00000000 --- a/pydis_site/apps/staff/migrations/0003_delete_rolemapping.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 3.0.9 on 2020-10-04 17:49 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('staff', '0002_add_is_staff_to_role_mappings'), - ] - - operations = [ - migrations.DeleteModel( - name='RoleMapping', - ), - ] diff --git a/pydis_site/apps/staff/migrations/__init__.py b/pydis_site/apps/staff/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pydis_site/apps/staff/migrations/__init__.py +++ /dev/null diff --git a/pydis_site/apps/staff/templatetags/__init__.py b/pydis_site/apps/staff/templatetags/__init__.py deleted file mode 100644 index e8b6983a..00000000 --- a/pydis_site/apps/staff/templatetags/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .deletedmessage_filters import footer_datetime, hex_colour - -__all__ = ["hex_colour", "footer_datetime"] diff --git a/pydis_site/apps/staff/templatetags/deletedmessage_filters.py b/pydis_site/apps/staff/templatetags/deletedmessage_filters.py deleted file mode 100644 index 5026068e..00000000 --- a/pydis_site/apps/staff/templatetags/deletedmessage_filters.py +++ /dev/null @@ -1,32 +0,0 @@ -from datetime import datetime -from typing import Union - -from django import template - -register = template.Library() - - -def hex_colour(colour: Union[str, int]) -> str: - """ - Converts the given representation of a colour to its RGB hex string. - - As we are using a Discord dark theme analogue, black colours are returned as white instead. - """ - if isinstance(colour, str): - colour = colour if colour.startswith("#") else f"#{colour}" - else: - colour = f"#{colour:0>6X}" - return colour if colour != "#000000" else "#FFFFFF" - - -def footer_datetime(timestamp: str) -> datetime: - """Takes an embed timestamp and returns a timezone-aware datetime object.""" - return datetime.fromisoformat(timestamp) - - -def visible_newlines(text: str) -> str: - """Takes an embed timestamp and returns a timezone-aware datetime object.""" - return text.replace("\n", " <span class='has-text-grey'>↵</span><br>") diff --git a/pydis_site/apps/staff/tests/__init__.py b/pydis_site/apps/staff/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/pydis_site/apps/staff/tests/__init__.py +++ /dev/null diff --git a/pydis_site/apps/staff/tests/test_deletedmessage_filters.py b/pydis_site/apps/staff/tests/test_deletedmessage_filters.py deleted file mode 100644 index 31215784..00000000 --- a/pydis_site/apps/staff/tests/test_deletedmessage_filters.py +++ /dev/null @@ -1,66 +0,0 @@ -import enum - -from django.test import TestCase -from django.utils import timezone - -from ..templatetags import deletedmessage_filters - - -class Colour(enum.IntEnum): - """Enumeration of integer colour values for readability.""" - - BLACK = 0 - BLUE = 255 - GREEN = 65280 - RED = 16711680 - WHITE = 16777215 - - -class DeletedMessageFilterTests(TestCase): - def test_hex_colour_filter(self): - """The filter should produce the correct hex values from the integer representations.""" - test_values = ( - (Colour.BLUE, "#0000FF"), - (Colour.GREEN, "#00FF00"), - (Colour.RED, "#FF0000"), - (Colour.WHITE, "#FFFFFF"), - - # Since we're using a "Discord dark theme"-like front-end, show black text as white. - (Colour.BLACK, "#FFFFFF"), - ) - - for colour, hex_value in test_values: - with self.subTest(colour=colour, hex_value=hex_value): - self.assertEqual(deletedmessage_filters.hex_colour(colour), hex_value) - - def test_footer_datetime_filter(self): - """The filter should parse the ISO-datetime string and return a timezone-aware datetime.""" - datetime_aware = timezone.now() - iso_string = datetime_aware.isoformat() - - datetime_returned = deletedmessage_filters.footer_datetime(iso_string) - self.assertTrue(timezone.is_aware(datetime_returned)) - self.assertEqual(datetime_aware, datetime_returned) - - def test_visual_newlines_filter(self): - """The filter should replace newline characters by newline character and html linebreak.""" - html_br = " <span class='has-text-grey'>↵</span><br>" - - test_values = ( - ( - "Hello, this line does not contain a linebreak", - "Hello, this line does not contain a linebreak" - ), - ( - "A single linebreak\nin a string", - f"A single linebreak{html_br}in a string" - ), - ( - "Consecutive linebreaks\n\n\nwork, too", - f"Consecutive linebreaks{html_br}{html_br}{html_br}work, too" - ) - ) - - for input_, expected_output in test_values: - with self.subTest(input=input_, expected_output=expected_output): - self.assertEqual(deletedmessage_filters.visible_newlines(input_), expected_output) diff --git a/pydis_site/apps/staff/tests/test_logs_view.py b/pydis_site/apps/staff/tests/test_logs_view.py deleted file mode 100644 index 3e5726cd..00000000 --- a/pydis_site/apps/staff/tests/test_logs_view.py +++ /dev/null @@ -1,182 +0,0 @@ -from django.test import TestCase -from django.urls import reverse -from django.utils import timezone - -from pydis_site.apps.api.models.bot import DeletedMessage, MessageDeletionContext, Role, User -from pydis_site.apps.staff.templatetags.deletedmessage_filters import hex_colour - - -class TestLogsView(TestCase): - @classmethod - def setUpTestData(cls): - cls.developers_role = Role.objects.create( - id=12345678, - name="Developers", - colour=16777215, - permissions=104324673, - position=1, - ) - - cls.author = cls.actor = User.objects.create( - id=12345678901, - name='Alan Turing', - discriminator=1912, - ) - - cls.author.roles.append(cls.developers_role.id) - - cls.deletion_context = MessageDeletionContext.objects.create( - actor=cls.actor, - creation=timezone.now() - ) - - cls.deleted_message_one = DeletedMessage.objects.create( - author=cls.author, - id=614125807161573397, - channel_id=1984, - content='<em>I think my tape has run out...</em>', - embeds=[], - attachments=[], - deletion_context=cls.deletion_context, - ) - - cls.embed_one = { - "footer": { - "text": "This will be displayed in the footer!", - "icon_url": "https://avatars0.githubusercontent.com/u/33516116?s=460&v=4" - }, - "image": { - "url": "https://avatars0.githubusercontent.com/u/33516116?s=460&v=4" - }, - "thumbnail": { - "url": "https://avatars0.githubusercontent.com/u/33516116?s=460&v=4" - }, - "author": { - "name": "Ves Zappa", - "url": "https://pydis.com", - "icon_url": "https://avatars0.githubusercontent.com/u/33516116?s=460&v=4" - }, - "fields": [ - { - "inline": False, - "name": "Field Name 1", - "value": "Field Value 1" - }, - { - "inline": False, - "name": "Field Name 2", - "value": "Field Value 2" - }, - { - "inline": True, - "name": "Field Name 3", - "value": "Field Value 3" - }, - { - "inline": True, - "name": "Field Name 4", - "value": "Field Value 4" - }, - { - "inline": True, - "name": "Field Name 5", - "value": "Field Value 5" - } - ], - "color": 16711680, - "timestamp": "2019-08-21T13:58:34.480053+00:00", - "type": "rich", - "description": "This embed is way too cool to be seen in public channels.", - "url": "https://pythondiscord.com/", - "title": "Hello, PyDis" - } - - cls.embed_two = { - "description": "This embed is way too cool to be seen in public channels.", - } - - cls.embed_three = { - "description": "This embed is way too cool to be seen in public channels.", - "color": "#e74c3c", - } - - cls.embed_four = { - "description": "This embed is way too cool to be seen in public channels.", - "color": "e74c3c", - } - - cls.deleted_message_two = DeletedMessage.objects.create( - author=cls.author, - id=614444836291870750, - channel_id=1984, - content='Does that mean this thing will halt?', - embeds=[cls.embed_one, cls.embed_two, cls.embed_three, cls.embed_four], - attachments=['https://http.cat/100', 'https://http.cat/402'], - deletion_context=cls.deletion_context, - ) - - def test_logs_returns_200_for_existing_logs_pk(self): - url = reverse('staff:logs', args=(self.deletion_context.id,)) - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - - def test_logs_returns_404_for_nonexisting_logs_pk(self): - url = reverse('staff:logs', args=(self.deletion_context.id + 100,)) - response = self.client.get(url) - self.assertEqual(response.status_code, 404) - - def test_author_color_is_set_in_response(self): - url = reverse('staff:logs', args=(self.deletion_context.id,)) - response = self.client.get(url) - role_colour = hex_colour(self.developers_role.colour) - html_needle = ( - f'<span class="discord-username" style="color: {role_colour}">{self.author}</span>' - ) - self.assertInHTML(html_needle, response.content.decode()) - - def test_correct_messages_have_been_passed_to_template(self): - url = reverse('staff:logs', args=(self.deletion_context.id,)) - response = self.client.get(url) - self.assertIn("messages", response.context) - self.assertListEqual( - [self.deleted_message_two, self.deleted_message_one], - list(response.context["deletion_context"].deletedmessage_set.all()) - ) - - def test_if_both_embeds_are_included_html_response(self): - url = reverse('staff:logs', args=(self.deletion_context.id,)) - response = self.client.get(url) - - html_response = response.content.decode() - embed_colour_needle = ( - '<div class="discord-embed-color" style="background-color: {colour}"></div>' - ) - embed_one_colour = hex_colour(self.embed_one["color"]) - embed_two_colour = "#cacbce" - self.assertInHTML(embed_colour_needle.format(colour=embed_one_colour), html_response) - self.assertInHTML(embed_colour_needle.format(colour=embed_two_colour), html_response) - - def test_if_both_attachments_are_included_html_response(self): - url = reverse('staff:logs', args=(self.deletion_context.id,)) - response = self.client.get(url) - - html_response = response.content.decode() - attachment_needle = '<img alt="Attachment" class="discord-attachment" src="{url}">' - self.assertInHTML( - attachment_needle.format(url=self.deleted_message_two.attachments[0]), - html_response - ) - self.assertInHTML( - attachment_needle.format(url=self.deleted_message_two.attachments[1]), - html_response - ) - - def test_if_html_in_content_is_properly_escaped(self): - url = reverse('staff:logs', args=(self.deletion_context.id,)) - response = self.client.get(url) - - html_response = response.content.decode() - unescaped_content = "<em>I think my tape has run out...</em>" - self.assertInHTML(unescaped_content, html_response, count=0) - escaped_content = "<em>I think my tape has run out...</em>" - self.assertInHTML(escaped_content, html_response, count=1) diff --git a/pydis_site/apps/staff/urls.py b/pydis_site/apps/staff/urls.py deleted file mode 100644 index ca8d1a0f..00000000 --- a/pydis_site/apps/staff/urls.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.urls import path - -from .viewsets import LogView - -app_name = 'staff' -urlpatterns = [ - path('bot/logs/<int:pk>/', LogView.as_view(), name="logs"), -] diff --git a/pydis_site/apps/staff/viewsets/__init__.py b/pydis_site/apps/staff/viewsets/__init__.py deleted file mode 100644 index 6b10eb83..00000000 --- a/pydis_site/apps/staff/viewsets/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .logs import LogView - -__all__ = ["LogView"] diff --git a/pydis_site/apps/staff/viewsets/logs.py b/pydis_site/apps/staff/viewsets/logs.py deleted file mode 100644 index 22dede95..00000000 --- a/pydis_site/apps/staff/viewsets/logs.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.views.generic.detail import DetailView - -from pydis_site.apps.api.models.bot.message_deletion_context import MessageDeletionContext - - -class LogView(DetailView): - """The default view for the Deleted Messages logs.""" - - model = MessageDeletionContext - context_object_name = "deletion_context" - template_name = "staff/logs.html" diff --git a/pydis_site/context_processors.py b/pydis_site/context_processors.py deleted file mode 100644 index 0e8b4a94..00000000 --- a/pydis_site/context_processors.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.conf import settings -from django.template import RequestContext - - -def git_sha_processor(_: RequestContext) -> dict: - """Expose the git SHA for this repo to all views.""" - return {'git_sha': settings.GIT_SHA} diff --git a/pydis_site/settings.py b/pydis_site/settings.py deleted file mode 100644 index e9e0ba67..00000000 --- a/pydis_site/settings.py +++ /dev/null @@ -1,334 +0,0 @@ -""" -Django settings for pydis_site project. - -Generated by 'django-admin startproject' using Django 2.1. - -For more information on this file, see -https://docs.djangoproject.com/en/2.1/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/2.1/ref/settings/ -""" - -import os -import secrets -import sys -import warnings -from pathlib import Path -from socket import gethostbyname, gethostname - -import environ -import sentry_sdk -from sentry_sdk.integrations.django import DjangoIntegration - -env = environ.Env( - DEBUG=(bool, False), - SITE_DSN=(str, ""), - BUILDING_DOCKER=(bool, False), - STATIC_BUILD=(bool, False), - GIT_SHA=(str, 'development'), - TIMEOUT_PERIOD=(int, 5), - GITHUB_TOKEN=(str, None), - GITHUB_APP_ID=(str, None), - GITHUB_APP_KEY=(str, None), -) - -GIT_SHA = env("GIT_SHA") -GITHUB_API = "https://api.github.com" -GITHUB_TOKEN = env("GITHUB_TOKEN") -GITHUB_APP_ID = env("GITHUB_APP_ID") -GITHUB_APP_KEY = env("GITHUB_APP_KEY") -GITHUB_TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%SZ" -"""The datetime string format GitHub uses.""" - -STATIC_BUILD: bool = env("STATIC_BUILD") - -if GITHUB_APP_KEY and (key_file := Path(GITHUB_APP_KEY)).is_file(): - # Allow the OAuth key to be loaded from a file - GITHUB_APP_KEY = key_file.read_text(encoding="utf-8") - -if not STATIC_BUILD: - sentry_sdk.init( - dsn=env('SITE_DSN'), - integrations=[DjangoIntegration()], - send_default_pii=True, - release=f"site@{GIT_SHA}" - ) - -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -DEBUG = env('DEBUG') - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -if DEBUG: - ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=['*']) - SECRET_KEY = "yellow polkadot bikini" # noqa: S105 - - # Prevent verbose warnings emitted when passing a non-timezone aware - # datetime object to the database, whilst we have time zone support - # active. See the Django documentation for more details: - # https://docs.djangoproject.com/en/dev/topics/i18n/timezones/ - warnings.filterwarnings( - 'error', r"DateTimeField .* received a naive datetime", - RuntimeWarning, r'django\.db\.models\.fields', - ) - -elif 'CI' in os.environ: - ALLOWED_HOSTS = ['*'] - SECRET_KEY = secrets.token_urlsafe(32) - - # See above. We run with `CI=true`, but debug unset in GitHub Actions, - # so we also want to filter it there. - warnings.filterwarnings( - 'error', r"DateTimeField .* received a naive datetime", - RuntimeWarning, r'django\.db\.models\.fields', - ) - -else: - ALLOWED_HOSTS = env.list( - 'ALLOWED_HOSTS', - default=[ - 'www.pythondiscord.com', - 'pythondiscord.com', - gethostname(), - gethostbyname(gethostname()), - 'site.default.svc.cluster.local', - ], - ) - SECRET_KEY = env('SECRET_KEY') - -# Application definition -NON_STATIC_APPS = [ - 'pydis_site.apps.api', - 'pydis_site.apps.staff', -] if not STATIC_BUILD else [] - -INSTALLED_APPS = [ - *NON_STATIC_APPS, - 'pydis_site.apps.home', - 'pydis_site.apps.resources', - 'pydis_site.apps.content', - 'pydis_site.apps.events', - 'pydis_site.apps.redirect', - - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.sites', - 'django.contrib.staticfiles', - - 'django_filters', - 'django_simple_bulma', - 'rest_framework', - 'rest_framework.authtoken', - - 'django_distill', -] - -if not env("BUILDING_DOCKER"): - INSTALLED_APPS.append("django_prometheus") - -if STATIC_BUILD: - # The only middleware required during static builds - MIDDLEWARE = [ - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - ] -else: - # Ensure that Prometheus middlewares are first and last here. - MIDDLEWARE = [ - 'django_prometheus.middleware.PrometheusBeforeMiddleware', - - 'django.middleware.security.SecurityMiddleware', - 'whitenoise.middleware.WhiteNoiseMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - - 'django_prometheus.middleware.PrometheusAfterMiddleware' - ] - -ROOT_URLCONF = 'pydis_site.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(BASE_DIR, 'pydis_site', 'templates')], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - "pydis_site.context_processors.git_sha_processor" - ], - }, - }, -] - -WSGI_APPLICATION = 'pydis_site.wsgi.application' - -# Database -# https://docs.djangoproject.com/en/2.1/ref/settings/#databases - -DATABASES = { - 'default': env.db(), - 'metricity': env.db('METRICITY_DB_URL'), -} if not STATIC_BUILD else {} - -# Password validation -# https://docs.djangoproject.com/en/2.1/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', - }, -] - -# Internationalization -# https://docs.djangoproject.com/en/2.1/topics/i18n/ -LANGUAGE_CODE = 'en-us' -TIME_ZONE = 'UTC' -USE_I18N = True -USE_TZ = True - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/2.1/howto/static-files/ - -STATIC_URL = '/static/' -STATICFILES_DIRS = [os.path.join(BASE_DIR, 'pydis_site', 'static')] -STATIC_ROOT = env('STATIC_ROOT', default='/app/staticfiles') - -STATICFILES_FINDERS = [ - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', - - 'django_simple_bulma.finders.SimpleBulmaFinder', -] - -if DEBUG: - PARENT_HOST = env('PARENT_HOST', default='pythondiscord.local:8000') - - if ":" in PARENT_HOST: - ALLOWED_HOSTS.append(PARENT_HOST.split(":", 1)[0]) - else: - ALLOWED_HOSTS.append(PARENT_HOST) -else: - PARENT_HOST = env('PARENT_HOST', default='pythondiscord.com') - -# Django Model Configuration -DEFAULT_AUTO_FIELD = "django.db.models.AutoField" - -# Django REST framework -# https://www.django-rest-framework.org -REST_FRAMEWORK = { - 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'rest_framework.authentication.TokenAuthentication', - ), - 'DEFAULT_PERMISSION_CLASSES': ( - 'rest_framework.permissions.DjangoModelPermissions', - ), - 'TEST_REQUEST_DEFAULT_FORMAT': 'json' -} - -# Logging -# https://docs.djangoproject.com/en/2.1/topics/logging/ -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'verbose': { - 'format': ( - '%(asctime)s | %(process)d:%(thread)d | %(module)s | %(levelname)-8s | %(message)s' - ) - } - }, - 'handlers': { - 'console': { - 'class': 'logging.StreamHandler' - } - }, - 'loggers': { - 'django': { - 'handlers': ['console'], - 'propagate': True, - 'level': env( - 'LOG_LEVEL', - default=( - # If there is no explicit `LOG_LEVEL` set, - # use `DEBUG` if we're running in debug mode but not - # testing. Use `ERROR` if we're running tests, else - # default to using `WARN`. - 'INFO' - if DEBUG and 'test' not in sys.argv - else ( - 'ERROR' - if 'test' in sys.argv - else 'WARN' - ) - ) - ) - } - } -} - -# Custom settings for django-simple-bulma -BULMA_SETTINGS = { - "variables": { # If you update these colours, please update the notification.css file - "primary": "#7289DA", # Discord blurple - "green": "#32ac66", # Colour picked after Discord discussion - "turquoise": "#7289DA", # Blurple, because Bulma uses this regardless of `primary` above - "blue": "#2482c1", # Colour picked after Discord discussion - "cyan": "#2482c1", # Colour picked after Discord discussion (matches the blue) - "purple": "#aa55e4", # Apparently unused, but changed for consistency - "red": "#d63852", # Colour picked after Discord discussion - - "link": "$primary", - - "dimensions": "16 24 32 48 64 96 128 256 512", # Possible image dimensions - "navbar-height": "4.75rem", - "footer-padding": "1rem 1.5rem 1rem", - "tooltip-max-width": "30rem", - }, - "extensions": [ - "bulma-dropdown", - "bulma-navbar-burger", - ], - "fontawesome_token": "ff22cb6f41", -} - -# Information about site repository -SITE_REPOSITORY_OWNER = "python-discord" -SITE_REPOSITORY_NAME = "site" -SITE_REPOSITORY_BRANCH = "master" - -# Path for events pages -EVENTS_PAGES_PATH = Path(BASE_DIR, "pydis_site", "templates", "events", "pages") - -# Path for content pages -CONTENT_PAGES_PATH = Path(BASE_DIR, "pydis_site", "apps", "content", "resources") - -# Path for redirection links -REDIRECTIONS_PATH = Path(BASE_DIR, "pydis_site", "apps", "redirect", "redirects.yaml") - -# How long to wait for synchronous requests before timing out -TIMEOUT_PERIOD = env("TIMEOUT_PERIOD") diff --git a/pydis_site/static/css/base/base.css b/pydis_site/static/css/base/base.css deleted file mode 100644 index 4b36b7ce..00000000 --- a/pydis_site/static/css/base/base.css +++ /dev/null @@ -1,170 +0,0 @@ -html { - overflow: auto; - scroll-behavior: smooth; -} - -body.site { - display: flex; - min-height: 100vh; - flex-direction: column; -} - -main.site-content { - flex: 1; -} - -.card.has-equal-height { - height: 100%; - display: flex; - flex-direction: column; -} - -.navbar { - padding-right: 0.8em; -} - -.navbar-item .navbar-link { - padding-left: 1.5em; - padding-right: 2.5em; -} - -.navbar-link:not(.is-arrowless)::after { - right: 1.125em; - margin-top: -0.455em; -} - -.navbar-item.has-no-highlight:hover { - background-color: transparent; -} - -#navbar-banner { - background-color: transparent; -} - -#navbar-banner img { - max-height: 3rem; -} - -#discord-btn a { - color: transparent; - background-image: url(../../images/navbar/discord.svg); - background-size: 200%; - background-position: 100% 50%; - background-repeat: no-repeat; - padding-left: 2.5rem; - padding-right: 2.5rem; - background-color: #697ec4ff; - margin-left: 0.5rem; - transition: all 0.2s cubic-bezier(.25,.8,.25,1); - overflow: hidden; -} - -#discord-btn:hover a { - box-shadow: 0 1px 4px rgba(0,0,0,0.16), 0 1px 6px rgba(0,0,0,0.23); - /*transform: scale(1.03) translate3d(0,0,0);*/ - background-size: 200%; - background-position: 1% 50%; -} - -#discord-btn:hover { - background-color: transparent; -} - -#linode-logo { - padding-left: 15px; - background: url(https://www.linode.com/wp-content/uploads/2021/01/Linode-Logo-Black.svg) no-repeat center; - filter: invert(1) grayscale(1); - background-size: 60px; - color: #00000000; -} - -#netcup-logo { - padding-left: 15px; - background: url(https://www.netcup-wiki.de/static/assets/images/netcup_logo_white.svg) no-repeat center; - background-size: 60px; - background-position: 0px 3px; - color: #00000000; -} - -#django-logo { - padding-bottom: 2px; - background: url(https://static.djangoproject.com/img/logos/django-logo-negative.svg) no-repeat center; - filter: grayscale(1) invert(0.09); - background-size: 52px 25.5px; - background-position: -2px -1px; - color: #00000000; -} - -#bulma-logo { - padding-left: 18px; - height: 20px; - background: url(https://bulma.io/images/bulma-logo-white.png) no-repeat center; - background-size: 60px; - background-position: 0px 3px; - color: #00000000; -} - -#pydis-text { - font-weight: bold; -} - - -/* Navbar "more" menu should be on top of messages. I could not figure - * out a better way to fix this one without just applying a high z-index - * to everything - anyone have any better ideas? Does it matter since the - * navbar should be above everything anyway? - */ - -.navbar, .navbar * { - z-index: 1001; -} - -/* Fix for logout form submit button in navbar */ - -button.is-size-navbar-menu, a.is-size-navbar-menu { - font-size: 14px; - padding-left: 1.5rem; - padding-right: 1.5rem; -} - -@media screen and (min-width: 1088px) { - button.is-size-navbar-menu, a.is-size-navbar-menu { - padding-left: 1rem; - padding-right: 1rem; - } -} - -/* 16:9 aspect ratio fixing */ -.force-aspect-container { - position: relative; - padding-bottom: 56.25%; -} - -.force-aspect-content { - top: 0; - left: 0; - width: 100%; - height: 100%; - position: absolute; -} - -/* 16:9 aspect ratio fixing */ -.force-aspect-container { - position: relative; - padding-bottom: 56.25%; -} - -.force-aspect-content { - top: 0; - left: 0; - width: 100%; - height: 100%; - position: absolute; -} - -/* Tone down animations to avoid motion triggers */ -@media (prefers-reduced-motion: reduce) { - html { - scroll-behavior: auto; - } -} diff --git a/pydis_site/static/css/collapsibles.css b/pydis_site/static/css/collapsibles.css deleted file mode 100644 index 1d73fa00..00000000 --- a/pydis_site/static/css/collapsibles.css +++ /dev/null @@ -1,11 +0,0 @@ -.collapsible { - cursor: pointer; - width: 100%; - border: none; - outline: none; -} - -.collapsible-content { - transition: max-height 0.3s ease-out; - overflow: hidden; -} diff --git a/pydis_site/static/css/content/color.css b/pydis_site/static/css/content/color.css deleted file mode 100644 index f4801c28..00000000 --- a/pydis_site/static/css/content/color.css +++ /dev/null @@ -1,7 +0,0 @@ -.content .fa-github { - color: black; -} - -.content .fa-github:hover { - color: #7289DA; -} diff --git a/pydis_site/static/css/content/page.css b/pydis_site/static/css/content/page.css deleted file mode 100644 index d831f86d..00000000 --- a/pydis_site/static/css/content/page.css +++ /dev/null @@ -1,79 +0,0 @@ -.breadcrumb-section { - padding: 1rem; -} - -i.has-icon-padding { - padding: 0 10px 25px 0; -} - -/* - * Move padding padding from <pre> tag to hljs <code> tags so the padding - * space is colored the same as the background of hljs <code> blocks. - */ -.content pre { - padding: 0; -} - -code.hljs { - padding: 1.75em 2em; -} - -/* - * Show header permalink on hover. - */ -.headerlink { - display: none; - padding-left: 0.5em; -} - -:is(h1, h2, h3, h4, h5, h6):hover > .headerlink { - display: inline; -} - -/* - * Display <em> tags immediately following <img> tags like figure subcaptions. - * Note: There must not be a newline between the image and the italicized line - * for this to work. Otherwise, it's regular markdown. - * - * Image caption: - * - *  - * *This is my caption.* - * - */ -img + em { - /* Place the caption on it's own line */ - display: block; - white-space: pre; - - /* Style */ - font-size: .875em; -} - -/* - * Remove extra padding on the left of TOC items - */ -ul.menu-list.toc { - margin-left: 0; -} - -/* - * Remove bullets set by the markdown extension, since bulma adds vertical - * lines to represent nesting - */ -.toc li { - list-style-type: none; -} -/* ..but we still want bullets on the top <ul> items */ -.toc > ul > li { - list-style-type: disc; -} - -/* - * Increase space between images and any text above the image in a list. - * This is used instead of a newline between the image and the text in markdown - * so the image remains left-aligned with the list item. - */ -li img { - margin-top: 0.5em; -} diff --git a/pydis_site/static/css/content/tag.css b/pydis_site/static/css/content/tag.css deleted file mode 100644 index 79795f9e..00000000 --- a/pydis_site/static/css/content/tag.css +++ /dev/null @@ -1,13 +0,0 @@ -.content a * { - /* This is the original color, but propagated down the chain */ - /* which allows for elements inside links, such as codeblocks */ - color: #7289DA; -} - -.content a *:hover { - color: dimgray; -} - -span.update-time { - text-decoration: black underline dotted; -} diff --git a/pydis_site/static/css/error_pages.css b/pydis_site/static/css/error_pages.css deleted file mode 100644 index 042a53a0..00000000 --- a/pydis_site/static/css/error_pages.css +++ /dev/null @@ -1,67 +0,0 @@ -html { - height: 100%; -} - -body { - background-color: #7289DA; - background-image: url("https://raw.githubusercontent.com/python-discord/branding/main/logos/banner_pattern/banner_pattern.svg"); - background-size: 128px; - font-family: "Hind", "Helvetica", "Arial", sans-serif; - display: flex; - align-items: center; - justify-content: center; - height: 100%; - margin: 0; -} - -h1, -p { - color: black; - padding: 0; - margin: 0; - margin-bottom: 10px; -} - -h1 { - margin-bottom: 15px; - font-size: 26px; -} - -p, -li { - line-height: 125%; -} - -a { - color: #7289DA; -} - -ul { - margin-bottom: 0; -} - -li { - margin-top: 10px; -} - -.error-box { - display: flex; - flex-direction: column; - max-width: 512px; - border-radius: 20px; - overflow: hidden; - box-shadow: 5px 7px 40px rgba(0, 0, 0, 0.432); -} - -.logo-box { - display: flex; - justify-content: center; - height: 80px; - padding: 15px; - background-color: #758ad4; -} - -.content-box { - padding: 25px; - background: #fff; -} diff --git a/pydis_site/static/css/events/base.css b/pydis_site/static/css/events/base.css deleted file mode 100644 index 9e244ed9..00000000 --- a/pydis_site/static/css/events/base.css +++ /dev/null @@ -1,20 +0,0 @@ -.breadcrumb-section { - padding: 1rem; -} - -pre { - /* - * Style it the same as the <code> tag, since highlight.js does not style - * backgrounds of <pre> tags but bulma does, resulting in a weird off-white - * border. - */ - background-color: #282c34; -} - -.panel .panel-heading { - /* - * Remove whitespace between the panel heading and the first item in a panel, - * since it makes the first panel item taller than the others. - */ - margin-bottom: 0 !important -} diff --git a/pydis_site/static/css/home/index.css b/pydis_site/static/css/home/index.css deleted file mode 100644 index e117a35b..00000000 --- a/pydis_site/static/css/home/index.css +++ /dev/null @@ -1,238 +0,0 @@ -h1 { - padding-bottom: 0.5em; -} - -/* Mobile-only notice banner */ - -#mobile-notice { - margin: 5px; - margin-bottom: -10px!important; -} - -/* Wave hero */ - -#wave-hero { - position: relative; - background-color: #7289DA; - color: #fff; - height: 32vw; - min-height: 270px; - max-height: 500px; - overflow-x: hidden; - width: 100%; - padding: 0; -} - -#wave-hero .container { - z-index: 4; /* keep hero contents above wave animations */ -} - -@media screen and (min-width: 769px) and (max-width: 1023px) { - #wave-hero .columns { - margin: 0 1em 0 1em; /* Stop cards touching canvas edges in table-view */ - } -} - -#wave-hero iframe { - box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); - transition: all 0.3s cubic-bezier(.25,.8,.25,1); - border-radius: 10px; - margin-top: 1em; - border: none; -} - -#wave-hero iframe:hover { - box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22); -} - -#wave-hero-centered { - margin: auto auto; -} - -#wave-hero-right img { - border-radius: 10px; - box-shadow: 0 1px 6px rgba(0,0,0,0.16), 0 1px 6px rgba(0,0,0,0.23); - margin-top: 1em; - text-align: right; - transition: all 0.3s cubic-bezier(.25,.8,.25,1); -} - -#wave-hero-right img:hover { - box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22); -} - -#wave-hero .wave { - background: url(../../images/waves/wave_dark.svg) repeat-x; - position: absolute; - bottom: 0; - width: 6400px; - animation-name: wave; - animation-timing-function: cubic-bezier(.36,.45,.63,.53); - animation-iteration-count: infinite; - transform: translate3d(0,0,0); /* Trigger 3D acceleration for smoother animation */ -} - -#front-wave { - animation-duration: 60s; - animation-delay: -50s; - opacity: 0.5; - height: 178px; -} - -#back-wave { - animation-duration: 65s; - height: 198px; -} - -#bottom-wave { - animation-duration: 50s; - animation-delay: -10s; - background: url(../../images/waves/wave_white.svg) repeat-x !important; - height: 26px; - z-index: 3; -} - -@keyframes wave { - 0% { - margin-left: 0; - } - 100% { - margin-left: -1600px; - } -} - -/* Showcase */ - -#showcase { - margin: 0 1em; -} - -#showcase .mini-timeline { - height: 3px; - position: relative; - margin: 50px 0 50px 0; - background: linear-gradient(to right, #ffffff00, #666666ff, #ffffff00); - text-align: center; -} - -#showcase .mini-timeline i { - display: inline-block; - vertical-align: middle; - width: 30px; - height: 30px; - border-radius: 50%; - position: relative; - top: -14px; - margin: 0 4% 0 4%; - background-color: #3EB2EF; - color: white; - line-height: 31px; - border:none; - box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); - transition: all 0.3s cubic-bezier(.25,.8,.25,1); -} - -#showcase .mini-timeline i:hover { - box-shadow: 0 2px 5px rgba(0,0,0,0.16), 0 2px 5px rgba(0,0,0,0.23); - transform: scale(1.5); -} - -/* Projects */ - -#projects { - padding-top: 0; -} - -#projects .card { - box-shadow: none; - border: #d1d5da 1px solid; - border-radius: 3px; - transition: all 0.2s cubic-bezier(.25,.8,.25,1); - height: 100%; - display: flex; - flex-direction: column; -} - -#projects .card:hover { - box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); -} - -#projects .card-header { - box-shadow: none; - font-size: 1.25rem; - padding: 1.5rem 1.5rem 0 1.5rem; -} - -#projects .card-header-icon { - font-size: 1.5rem; - padding: 0 1rem 0 0; -} - -#projects .card-header-title { - padding: 0; - color: #7289DA; -} - -#projects .card:hover .card-header-title { - color: #363636; -} - -#projects .card-content { - padding-top: 8px; - padding-bottom: 1rem; -} - -#projects .card-footer { - margin-top: auto; - border: none; -} - -#projects .card-footer-item { - border: none; -} - -#projects .card-footer-item i { - margin-right: 0.5rem; -} - -#projects .repo-language-dot { - border-radius: 50%; - height: 12px; - width: 12px; - top: -1px; - display: inline-block; - position: relative; -} - -#projects .repo-language-dot.python { background-color: #3572A5; } -#projects .repo-language-dot.html { background-color: #e34c26; } -#projects .repo-language-dot.css { background-color: #563d7c; } -#projects .repo-language-dot.javascript { background-color: #f1e05a; } - -/* Sponsors */ - -#sponsors .hero-body { - padding-top: 2rem; - padding-bottom: 3rem; - - text-align: center; -} - -#sponsors .columns { - display: block; - justify-content: center; - margin: auto; - max-width: 80%; -} - -#sponsors a { - margin: auto; - display: inline-block; -} - -#sponsors img { - width: auto; - height: auto; - - max-height: 5rem; -} diff --git a/pydis_site/static/css/home/timeline.css b/pydis_site/static/css/home/timeline.css deleted file mode 100644 index 0a4dfbb6..00000000 --- a/pydis_site/static/css/home/timeline.css +++ /dev/null @@ -1,3823 +0,0 @@ -body { - background-color: hsl(0, 0%, 100%); - background-color: var(--color-bg, white) -} - -h2 { - font-size: 2em; -} - -@media (max-width: 500px) { - h2 { - font-size: 1em; - } -} - -article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section, main, form legend { - display: block -} - -ol, ul { - list-style: none -} - -blockquote, q { - quotes: none -} - -button, input, textarea, select { - margin: 0 -} - -.pastel-red { - background-color: #FF7878 !important; -} - -.pastel-orange { - background-color: #FFBF76 !important; -} - -.pastel-green { - background-color: #8bd6a7 !important; -} - -.pastel-blue { - background-color: #8edbec !important; -} - -.pastel-purple { - background-color: #CBB1FF !important; -} - -.pastel-pink { - background-color: #F6ACFF !important; -} - -.pastel-lime { - background-color: #b6df3a !important; -} - -.pastel-dark-blue { - background-color: #576297 !important; -} - -.pydis-logo-banner { - background-color: #7289DA !important; - border-radius: 10px; -} - -.pydis-logo-banner img { - padding-right: 20px; -} - -.btn, .form-control, .link, .reset { - background-color: transparent; - padding: 0; - border: 0; - border-radius: 0; - color: inherit; - line-height: inherit; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none -} - -select.form-control::-ms-expand { - display: none -} - -textarea { - resize: vertical; - overflow: auto; - vertical-align: top -} - -input::-ms-clear { - display: none -} - -table { - border-collapse: collapse; - border-spacing: 0 -} - -img, video, svg { - max-width: 100% -} - -[data-theme] { - background-color: hsl(0, 0%, 100%); - background-color: var(--color-bg, #fff); - color: hsl(240, 4%, 20%); - color: var(--color-contrast-high, #313135) -} - -:root { - --space-unit: 1em; - --space-xxxxs: calc(0.125*var(--space-unit)); - --space-xxxs: calc(0.25*var(--space-unit)); - --space-xxs: calc(0.375*var(--space-unit)); - --space-xs: calc(0.5*var(--space-unit)); - --space-sm: calc(0.75*var(--space-unit)); - --space-md: calc(1.25*var(--space-unit)); - --space-lg: calc(2*var(--space-unit)); - --space-xl: calc(3.25*var(--space-unit)); - --space-xxl: calc(5.25*var(--space-unit)); - --space-xxxl: calc(8.5*var(--space-unit)); - --space-xxxxl: calc(13.75*var(--space-unit)); - --component-padding: var(--space-md) -} - -:root { - --max-width-xxs: 32rem; - --max-width-xs: 38rem; - --max-width-sm: 48rem; - --max-width-md: 64rem; - --max-width-lg: 80rem; - --max-width-xl: 90rem; - --max-width-xxl: 120rem -} - -.container { - width: calc(100% - 1.25em); - width: calc(100% - 2*var(--component-padding)); - margin-left: auto; - margin-right: auto -} - -.max-width-xxs { - max-width: 32rem; - max-width: var(--max-width-xxs) -} - -.max-width-xs { - max-width: 38rem; - max-width: var(--max-width-xs) -} - -.max-width-sm { - max-width: 48rem; - max-width: var(--max-width-sm) -} - -.max-width-md { - max-width: 64rem; - max-width: var(--max-width-md) -} - -.max-width-lg { - max-width: 80rem; - max-width: var(--max-width-lg) -} - -.max-width-xl { - max-width: 90rem; - max-width: var(--max-width-xl) -} - -.max-width-xxl { - max-width: 120rem; - max-width: var(--max-width-xxl) -} - -.max-width-adaptive-sm { - max-width: 38rem; - max-width: var(--max-width-xs) -} - -@media (min-width: 64rem) { - .max-width-adaptive-sm { - max-width: 48rem; - max-width: var(--max-width-sm) - } -} - -.max-width-adaptive-md { - max-width: 38rem; - max-width: var(--max-width-xs) -} - -@media (min-width: 64rem) { - .max-width-adaptive-md { - max-width: 64rem; - max-width: var(--max-width-md) - } -} - -.max-width-adaptive, .max-width-adaptive-lg { - max-width: 38rem; - max-width: var(--max-width-xs) -} - -@media (min-width: 64rem) { - .max-width-adaptive, .max-width-adaptive-lg { - max-width: 64rem; - max-width: var(--max-width-md) - } -} - -@media (min-width: 90rem) { - .max-width-adaptive, .max-width-adaptive-lg { - max-width: 80rem; - max-width: var(--max-width-lg) - } -} - -.max-width-adaptive-xl { - max-width: 38rem; - max-width: var(--max-width-xs) -} - -@media (min-width: 64rem) { - .max-width-adaptive-xl { - max-width: 64rem; - max-width: var(--max-width-md) - } -} - -@media (min-width: 90rem) { - .max-width-adaptive-xl { - max-width: 90rem; - max-width: var(--max-width-xl) - } -} - -.grid { - --grid-gap: 0px; - display: -ms-flexbox; - display: flex; - -ms-flex-wrap: wrap; - flex-wrap: wrap -} - -.grid>* { - -ms-flex-preferred-size: 100%; - flex-basis: 100% -} - -[class*="grid-gap"] { - margin-bottom: 1em * -1; - margin-bottom: calc(var(--grid-gap, 1em)*-1); - margin-right: 1em * -1; - margin-right: calc(var(--grid-gap, 1em)*-1) -} - -[class*="grid-gap"]>* { - margin-bottom: 1em; - margin-bottom: var(--grid-gap, 1em); - margin-right: 1em; - margin-right: var(--grid-gap, 1em) -} - -.grid-gap-xxxxs { - --grid-gap: var(--space-xxxxs) -} - -.grid-gap-xxxs { - --grid-gap: var(--space-xxxs) -} - -.grid-gap-xxs { - --grid-gap: var(--space-xxs) -} - -.grid-gap-xs { - --grid-gap: var(--space-xs) -} - -.grid-gap-sm { - --grid-gap: var(--space-sm) -} - -.grid-gap-md { - --grid-gap: var(--space-md) -} - -.grid-gap-lg { - --grid-gap: var(--space-lg) -} - -.grid-gap-xl { - --grid-gap: var(--space-xl) -} - -.grid-gap-xxl { - --grid-gap: var(--space-xxl) -} - -.grid-gap-xxxl { - --grid-gap: var(--space-xxxl) -} - -.grid-gap-xxxxl { - --grid-gap: var(--space-xxxxl) -} - -.col { - -ms-flex-positive: 1; - flex-grow: 1; - -ms-flex-preferred-size: 0; - flex-basis: 0; - max-width: 100% -} - -.col-1 { - -ms-flex-preferred-size: calc(8.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(8.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(8.33% - 0.01px - 1em); - flex-basis: calc(8.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(8.33% - 0.01px - 1em); - max-width: calc(8.33% - 0.01px - var(--grid-gap, 1em)) -} - -.col-2 { - -ms-flex-preferred-size: calc(16.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(16.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(16.66% - 0.01px - 1em); - flex-basis: calc(16.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(16.66% - 0.01px - 1em); - max-width: calc(16.66% - 0.01px - var(--grid-gap, 1em)) -} - -.col-3 { - -ms-flex-preferred-size: calc(25% - 0.01px - 1em); - -ms-flex-preferred-size: calc(25% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(25% - 0.01px - 1em); - flex-basis: calc(25% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(25% - 0.01px - 1em); - max-width: calc(25% - 0.01px - var(--grid-gap, 1em)) -} - -.col-4 { - -ms-flex-preferred-size: calc(33.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(33.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(33.33% - 0.01px - 1em); - flex-basis: calc(33.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(33.33% - 0.01px - 1em); - max-width: calc(33.33% - 0.01px - var(--grid-gap, 1em)) -} - -.col-5 { - -ms-flex-preferred-size: calc(41.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(41.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(41.66% - 0.01px - 1em); - flex-basis: calc(41.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(41.66% - 0.01px - 1em); - max-width: calc(41.66% - 0.01px - var(--grid-gap, 1em)) -} - -.col-6 { - -ms-flex-preferred-size: calc(50% - 0.01px - 1em); - -ms-flex-preferred-size: calc(50% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(50% - 0.01px - 1em); - flex-basis: calc(50% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(50% - 0.01px - 1em); - max-width: calc(50% - 0.01px - var(--grid-gap, 1em)) -} - -.col-7 { - -ms-flex-preferred-size: calc(58.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(58.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(58.33% - 0.01px - 1em); - flex-basis: calc(58.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(58.33% - 0.01px - 1em); - max-width: calc(58.33% - 0.01px - var(--grid-gap, 1em)) -} - -.col-8 { - -ms-flex-preferred-size: calc(66.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(66.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(66.66% - 0.01px - 1em); - flex-basis: calc(66.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(66.66% - 0.01px - 1em); - max-width: calc(66.66% - 0.01px - var(--grid-gap, 1em)) -} - -.col-9 { - -ms-flex-preferred-size: calc(75% - 0.01px - 1em); - -ms-flex-preferred-size: calc(75% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(75% - 0.01px - 1em); - flex-basis: calc(75% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(75% - 0.01px - 1em); - max-width: calc(75% - 0.01px - var(--grid-gap, 1em)) -} - -.col-10 { - -ms-flex-preferred-size: calc(83.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(83.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(83.33% - 0.01px - 1em); - flex-basis: calc(83.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(83.33% - 0.01px - 1em); - max-width: calc(83.33% - 0.01px - var(--grid-gap, 1em)) -} - -.col-11 { - -ms-flex-preferred-size: calc(91.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(91.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(91.66% - 0.01px - 1em); - flex-basis: calc(91.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(91.66% - 0.01px - 1em); - max-width: calc(91.66% - 0.01px - var(--grid-gap, 1em)) -} - -.col-12 { - -ms-flex-preferred-size: calc(100% - 0.01px - 1em); - -ms-flex-preferred-size: calc(100% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(100% - 0.01px - 1em); - flex-basis: calc(100% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(100% - 0.01px - 1em); - max-width: calc(100% - 0.01px - var(--grid-gap, 1em)) -} - -@media (min-width: 32rem) { - .col\@xs { - -ms-flex-positive: 1; - flex-grow: 1; - -ms-flex-preferred-size: 0; - flex-basis: 0; - max-width: 100% - } - .col-1\@xs { - -ms-flex-preferred-size: calc(8.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(8.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(8.33% - 0.01px - 1em); - flex-basis: calc(8.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(8.33% - 0.01px - 1em); - max-width: calc(8.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-2\@xs { - -ms-flex-preferred-size: calc(16.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(16.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(16.66% - 0.01px - 1em); - flex-basis: calc(16.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(16.66% - 0.01px - 1em); - max-width: calc(16.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-3\@xs { - -ms-flex-preferred-size: calc(25% - 0.01px - 1em); - -ms-flex-preferred-size: calc(25% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(25% - 0.01px - 1em); - flex-basis: calc(25% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(25% - 0.01px - 1em); - max-width: calc(25% - 0.01px - var(--grid-gap, 1em)) - } - .col-4\@xs { - -ms-flex-preferred-size: calc(33.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(33.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(33.33% - 0.01px - 1em); - flex-basis: calc(33.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(33.33% - 0.01px - 1em); - max-width: calc(33.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-5\@xs { - -ms-flex-preferred-size: calc(41.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(41.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(41.66% - 0.01px - 1em); - flex-basis: calc(41.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(41.66% - 0.01px - 1em); - max-width: calc(41.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-6\@xs { - -ms-flex-preferred-size: calc(50% - 0.01px - 1em); - -ms-flex-preferred-size: calc(50% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(50% - 0.01px - 1em); - flex-basis: calc(50% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(50% - 0.01px - 1em); - max-width: calc(50% - 0.01px - var(--grid-gap, 1em)) - } - .col-7\@xs { - -ms-flex-preferred-size: calc(58.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(58.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(58.33% - 0.01px - 1em); - flex-basis: calc(58.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(58.33% - 0.01px - 1em); - max-width: calc(58.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-8\@xs { - -ms-flex-preferred-size: calc(66.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(66.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(66.66% - 0.01px - 1em); - flex-basis: calc(66.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(66.66% - 0.01px - 1em); - max-width: calc(66.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-9\@xs { - -ms-flex-preferred-size: calc(75% - 0.01px - 1em); - -ms-flex-preferred-size: calc(75% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(75% - 0.01px - 1em); - flex-basis: calc(75% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(75% - 0.01px - 1em); - max-width: calc(75% - 0.01px - var(--grid-gap, 1em)) - } - .col-10\@xs { - -ms-flex-preferred-size: calc(83.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(83.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(83.33% - 0.01px - 1em); - flex-basis: calc(83.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(83.33% - 0.01px - 1em); - max-width: calc(83.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-11\@xs { - -ms-flex-preferred-size: calc(91.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(91.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(91.66% - 0.01px - 1em); - flex-basis: calc(91.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(91.66% - 0.01px - 1em); - max-width: calc(91.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-12\@xs { - -ms-flex-preferred-size: calc(100% - 0.01px - 1em); - -ms-flex-preferred-size: calc(100% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(100% - 0.01px - 1em); - flex-basis: calc(100% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(100% - 0.01px - 1em); - max-width: calc(100% - 0.01px - var(--grid-gap, 1em)) - } -} - -@media (min-width: 48rem) { - .col\@sm { - -ms-flex-positive: 1; - flex-grow: 1; - -ms-flex-preferred-size: 0; - flex-basis: 0; - max-width: 100% - } - .col-1\@sm { - -ms-flex-preferred-size: calc(8.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(8.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(8.33% - 0.01px - 1em); - flex-basis: calc(8.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(8.33% - 0.01px - 1em); - max-width: calc(8.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-2\@sm { - -ms-flex-preferred-size: calc(16.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(16.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(16.66% - 0.01px - 1em); - flex-basis: calc(16.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(16.66% - 0.01px - 1em); - max-width: calc(16.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-3\@sm { - -ms-flex-preferred-size: calc(25% - 0.01px - 1em); - -ms-flex-preferred-size: calc(25% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(25% - 0.01px - 1em); - flex-basis: calc(25% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(25% - 0.01px - 1em); - max-width: calc(25% - 0.01px - var(--grid-gap, 1em)) - } - .col-4\@sm { - -ms-flex-preferred-size: calc(33.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(33.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(33.33% - 0.01px - 1em); - flex-basis: calc(33.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(33.33% - 0.01px - 1em); - max-width: calc(33.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-5\@sm { - -ms-flex-preferred-size: calc(41.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(41.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(41.66% - 0.01px - 1em); - flex-basis: calc(41.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(41.66% - 0.01px - 1em); - max-width: calc(41.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-6\@sm { - -ms-flex-preferred-size: calc(50% - 0.01px - 1em); - -ms-flex-preferred-size: calc(50% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(50% - 0.01px - 1em); - flex-basis: calc(50% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(50% - 0.01px - 1em); - max-width: calc(50% - 0.01px - var(--grid-gap, 1em)) - } - .col-7\@sm { - -ms-flex-preferred-size: calc(58.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(58.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(58.33% - 0.01px - 1em); - flex-basis: calc(58.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(58.33% - 0.01px - 1em); - max-width: calc(58.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-8\@sm { - -ms-flex-preferred-size: calc(66.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(66.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(66.66% - 0.01px - 1em); - flex-basis: calc(66.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(66.66% - 0.01px - 1em); - max-width: calc(66.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-9\@sm { - -ms-flex-preferred-size: calc(75% - 0.01px - 1em); - -ms-flex-preferred-size: calc(75% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(75% - 0.01px - 1em); - flex-basis: calc(75% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(75% - 0.01px - 1em); - max-width: calc(75% - 0.01px - var(--grid-gap, 1em)) - } - .col-10\@sm { - -ms-flex-preferred-size: calc(83.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(83.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(83.33% - 0.01px - 1em); - flex-basis: calc(83.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(83.33% - 0.01px - 1em); - max-width: calc(83.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-11\@sm { - -ms-flex-preferred-size: calc(91.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(91.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(91.66% - 0.01px - 1em); - flex-basis: calc(91.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(91.66% - 0.01px - 1em); - max-width: calc(91.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-12\@sm { - -ms-flex-preferred-size: calc(100% - 0.01px - 1em); - -ms-flex-preferred-size: calc(100% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(100% - 0.01px - 1em); - flex-basis: calc(100% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(100% - 0.01px - 1em); - max-width: calc(100% - 0.01px - var(--grid-gap, 1em)) - } -} - -@media (min-width: 64rem) { - .col\@md { - -ms-flex-positive: 1; - flex-grow: 1; - -ms-flex-preferred-size: 0; - flex-basis: 0; - max-width: 100% - } - .col-1\@md { - -ms-flex-preferred-size: calc(8.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(8.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(8.33% - 0.01px - 1em); - flex-basis: calc(8.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(8.33% - 0.01px - 1em); - max-width: calc(8.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-2\@md { - -ms-flex-preferred-size: calc(16.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(16.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(16.66% - 0.01px - 1em); - flex-basis: calc(16.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(16.66% - 0.01px - 1em); - max-width: calc(16.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-3\@md { - -ms-flex-preferred-size: calc(25% - 0.01px - 1em); - -ms-flex-preferred-size: calc(25% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(25% - 0.01px - 1em); - flex-basis: calc(25% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(25% - 0.01px - 1em); - max-width: calc(25% - 0.01px - var(--grid-gap, 1em)) - } - .col-4\@md { - -ms-flex-preferred-size: calc(33.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(33.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(33.33% - 0.01px - 1em); - flex-basis: calc(33.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(33.33% - 0.01px - 1em); - max-width: calc(33.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-5\@md { - -ms-flex-preferred-size: calc(41.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(41.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(41.66% - 0.01px - 1em); - flex-basis: calc(41.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(41.66% - 0.01px - 1em); - max-width: calc(41.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-6\@md { - -ms-flex-preferred-size: calc(50% - 0.01px - 1em); - -ms-flex-preferred-size: calc(50% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(50% - 0.01px - 1em); - flex-basis: calc(50% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(50% - 0.01px - 1em); - max-width: calc(50% - 0.01px - var(--grid-gap, 1em)) - } - .col-7\@md { - -ms-flex-preferred-size: calc(58.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(58.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(58.33% - 0.01px - 1em); - flex-basis: calc(58.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(58.33% - 0.01px - 1em); - max-width: calc(58.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-8\@md { - -ms-flex-preferred-size: calc(66.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(66.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(66.66% - 0.01px - 1em); - flex-basis: calc(66.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(66.66% - 0.01px - 1em); - max-width: calc(66.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-9\@md { - -ms-flex-preferred-size: calc(75% - 0.01px - 1em); - -ms-flex-preferred-size: calc(75% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(75% - 0.01px - 1em); - flex-basis: calc(75% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(75% - 0.01px - 1em); - max-width: calc(75% - 0.01px - var(--grid-gap, 1em)) - } - .col-10\@md { - -ms-flex-preferred-size: calc(83.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(83.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(83.33% - 0.01px - 1em); - flex-basis: calc(83.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(83.33% - 0.01px - 1em); - max-width: calc(83.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-11\@md { - -ms-flex-preferred-size: calc(91.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(91.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(91.66% - 0.01px - 1em); - flex-basis: calc(91.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(91.66% - 0.01px - 1em); - max-width: calc(91.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-12\@md { - -ms-flex-preferred-size: calc(100% - 0.01px - 1em); - -ms-flex-preferred-size: calc(100% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(100% - 0.01px - 1em); - flex-basis: calc(100% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(100% - 0.01px - 1em); - max-width: calc(100% - 0.01px - var(--grid-gap, 1em)) - } -} - -@media (min-width: 80rem) { - .col\@lg { - -ms-flex-positive: 1; - flex-grow: 1; - -ms-flex-preferred-size: 0; - flex-basis: 0; - max-width: 100% - } - .col-1\@lg { - -ms-flex-preferred-size: calc(8.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(8.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(8.33% - 0.01px - 1em); - flex-basis: calc(8.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(8.33% - 0.01px - 1em); - max-width: calc(8.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-2\@lg { - -ms-flex-preferred-size: calc(16.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(16.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(16.66% - 0.01px - 1em); - flex-basis: calc(16.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(16.66% - 0.01px - 1em); - max-width: calc(16.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-3\@lg { - -ms-flex-preferred-size: calc(25% - 0.01px - 1em); - -ms-flex-preferred-size: calc(25% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(25% - 0.01px - 1em); - flex-basis: calc(25% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(25% - 0.01px - 1em); - max-width: calc(25% - 0.01px - var(--grid-gap, 1em)) - } - .col-4\@lg { - -ms-flex-preferred-size: calc(33.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(33.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(33.33% - 0.01px - 1em); - flex-basis: calc(33.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(33.33% - 0.01px - 1em); - max-width: calc(33.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-5\@lg { - -ms-flex-preferred-size: calc(41.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(41.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(41.66% - 0.01px - 1em); - flex-basis: calc(41.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(41.66% - 0.01px - 1em); - max-width: calc(41.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-6\@lg { - -ms-flex-preferred-size: calc(50% - 0.01px - 1em); - -ms-flex-preferred-size: calc(50% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(50% - 0.01px - 1em); - flex-basis: calc(50% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(50% - 0.01px - 1em); - max-width: calc(50% - 0.01px - var(--grid-gap, 1em)) - } - .col-7\@lg { - -ms-flex-preferred-size: calc(58.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(58.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(58.33% - 0.01px - 1em); - flex-basis: calc(58.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(58.33% - 0.01px - 1em); - max-width: calc(58.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-8\@lg { - -ms-flex-preferred-size: calc(66.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(66.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(66.66% - 0.01px - 1em); - flex-basis: calc(66.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(66.66% - 0.01px - 1em); - max-width: calc(66.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-9\@lg { - -ms-flex-preferred-size: calc(75% - 0.01px - 1em); - -ms-flex-preferred-size: calc(75% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(75% - 0.01px - 1em); - flex-basis: calc(75% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(75% - 0.01px - 1em); - max-width: calc(75% - 0.01px - var(--grid-gap, 1em)) - } - .col-10\@lg { - -ms-flex-preferred-size: calc(83.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(83.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(83.33% - 0.01px - 1em); - flex-basis: calc(83.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(83.33% - 0.01px - 1em); - max-width: calc(83.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-11\@lg { - -ms-flex-preferred-size: calc(91.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(91.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(91.66% - 0.01px - 1em); - flex-basis: calc(91.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(91.66% - 0.01px - 1em); - max-width: calc(91.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-12\@lg { - -ms-flex-preferred-size: calc(100% - 0.01px - 1em); - -ms-flex-preferred-size: calc(100% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(100% - 0.01px - 1em); - flex-basis: calc(100% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(100% - 0.01px - 1em); - max-width: calc(100% - 0.01px - var(--grid-gap, 1em)) - } -} - -@media (min-width: 90rem) { - .col\@xl { - -ms-flex-positive: 1; - flex-grow: 1; - -ms-flex-preferred-size: 0; - flex-basis: 0; - max-width: 100% - } - .col-1\@xl { - -ms-flex-preferred-size: calc(8.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(8.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(8.33% - 0.01px - 1em); - flex-basis: calc(8.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(8.33% - 0.01px - 1em); - max-width: calc(8.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-2\@xl { - -ms-flex-preferred-size: calc(16.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(16.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(16.66% - 0.01px - 1em); - flex-basis: calc(16.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(16.66% - 0.01px - 1em); - max-width: calc(16.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-3\@xl { - -ms-flex-preferred-size: calc(25% - 0.01px - 1em); - -ms-flex-preferred-size: calc(25% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(25% - 0.01px - 1em); - flex-basis: calc(25% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(25% - 0.01px - 1em); - max-width: calc(25% - 0.01px - var(--grid-gap, 1em)) - } - .col-4\@xl { - -ms-flex-preferred-size: calc(33.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(33.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(33.33% - 0.01px - 1em); - flex-basis: calc(33.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(33.33% - 0.01px - 1em); - max-width: calc(33.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-5\@xl { - -ms-flex-preferred-size: calc(41.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(41.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(41.66% - 0.01px - 1em); - flex-basis: calc(41.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(41.66% - 0.01px - 1em); - max-width: calc(41.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-6\@xl { - -ms-flex-preferred-size: calc(50% - 0.01px - 1em); - -ms-flex-preferred-size: calc(50% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(50% - 0.01px - 1em); - flex-basis: calc(50% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(50% - 0.01px - 1em); - max-width: calc(50% - 0.01px - var(--grid-gap, 1em)) - } - .col-7\@xl { - -ms-flex-preferred-size: calc(58.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(58.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(58.33% - 0.01px - 1em); - flex-basis: calc(58.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(58.33% - 0.01px - 1em); - max-width: calc(58.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-8\@xl { - -ms-flex-preferred-size: calc(66.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(66.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(66.66% - 0.01px - 1em); - flex-basis: calc(66.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(66.66% - 0.01px - 1em); - max-width: calc(66.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-9\@xl { - -ms-flex-preferred-size: calc(75% - 0.01px - 1em); - -ms-flex-preferred-size: calc(75% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(75% - 0.01px - 1em); - flex-basis: calc(75% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(75% - 0.01px - 1em); - max-width: calc(75% - 0.01px - var(--grid-gap, 1em)) - } - .col-10\@xl { - -ms-flex-preferred-size: calc(83.33% - 0.01px - 1em); - -ms-flex-preferred-size: calc(83.33% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(83.33% - 0.01px - 1em); - flex-basis: calc(83.33% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(83.33% - 0.01px - 1em); - max-width: calc(83.33% - 0.01px - var(--grid-gap, 1em)) - } - .col-11\@xl { - -ms-flex-preferred-size: calc(91.66% - 0.01px - 1em); - -ms-flex-preferred-size: calc(91.66% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(91.66% - 0.01px - 1em); - flex-basis: calc(91.66% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(91.66% - 0.01px - 1em); - max-width: calc(91.66% - 0.01px - var(--grid-gap, 1em)) - } - .col-12\@xl { - -ms-flex-preferred-size: calc(100% - 0.01px - 1em); - -ms-flex-preferred-size: calc(100% - 0.01px - var(--grid-gap, 1em)); - flex-basis: calc(100% - 0.01px - 1em); - flex-basis: calc(100% - 0.01px - var(--grid-gap, 1em)); - max-width: calc(100% - 0.01px - 1em); - max-width: calc(100% - 0.01px - var(--grid-gap, 1em)) - } -} - -:root { - --radius-sm: calc(var(--radius, 0.25em)/2); - --radius-md: var(--radius, 0.25em); - --radius-lg: calc(var(--radius, 0.25em)*2); - --shadow-sm: 0 1px 2px rgba(0, 0, 0, .085), 0 1px 8px rgba(0, 0, 0, .1); - --shadow-md: 0 1px 8px rgba(0, 0, 0, .1), 0 8px 24px rgba(0, 0, 0, .15); - --shadow-lg: 0 1px 8px rgba(0, 0, 0, .1), 0 16px 48px rgba(0, 0, 0, .1), 0 24px 60px rgba(0, 0, 0, .1); - --bounce: cubic-bezier(0.175, 0.885, 0.32, 1.275); - --ease-in-out: cubic-bezier(0.645, 0.045, 0.355, 1); - --ease-in: cubic-bezier(0.55, 0.055, 0.675, 0.19); - --ease-out: cubic-bezier(0.215, 0.61, 0.355, 1) -} - -:root { - --body-line-height: 1.4; - --heading-line-height: 1.2 -} - -body { - color: hsl(240, 4%, 20%); - color: var(--color-contrast-high, #313135) -} - -h1, h2, h3, h4 { - color: hsl(240, 8%, 12%); - color: var(--color-contrast-higher, #1c1c21); - line-height: 1.2; - line-height: var(--heading-line-height, 1.2) -} - -.text-xxxl { - font-size: 2.48832em; - font-size: var(--text-xxxl, 2.488em) -} - -small, .text-sm { - font-size: 0.83333em; - font-size: var(--text-sm, 0.833em) -} - -.text-xs { - font-size: 0.69444em; - font-size: var(--text-xs, 0.694em) -} - -strong, .text-bold { - font-weight: bold -} - -s { - text-decoration: line-through -} - -u, .text-underline { - text-decoration: underline -} - - -.text-component h1, .text-component h2, .text-component h3, .text-component h4 { - line-height: 1.2; - line-height: var(--component-heading-line-height, 1.2); - margin-bottom: 0.25em; - margin-bottom: calc(var(--space-xxxs)*var(--text-vspace-multiplier, 1)) -} - -.text-component h2, .text-component h3, .text-component h4 { - margin-top: 0.75em; - margin-top: calc(var(--space-sm)*var(--text-vspace-multiplier, 1)) -} - -.text-component p, .text-component blockquote, .text-component ul li, .text-component ol li { - line-height: 1.4; - line-height: var(--component-body-line-height) -} - -.text-component ul, .text-component ol, .text-component p, .text-component blockquote, .text-component .text-component__block { - margin-bottom: 0.75em; - margin-bottom: calc(var(--space-sm)*var(--text-vspace-multiplier, 1)) -} - -.text-component ul, .text-component ol { - padding-left: 1em -} - -.text-component ul { - list-style-type: disc -} - -.text-component ol { - list-style-type: decimal -} - -.text-component img { - display: block; - margin: 0 auto -} - -.text-component figcaption { - text-align: center; - margin-top: 0.5em; - margin-top: var(--space-xs) -} - -.text-component em { - font-style: italic -} - -.text-component hr { - margin-top: 2em; - margin-top: calc(var(--space-lg)*var(--text-vspace-multiplier, 1)); - margin-bottom: 2em; - margin-bottom: calc(var(--space-lg)*var(--text-vspace-multiplier, 1)); - margin-left: auto; - margin-right: auto -} - -.text-component>*:first-child { - margin-top: 0 -} - -.text-component>*:last-child { - margin-bottom: 0 -} - -.text-component__block--full-width { - width: 100vw; - margin-left: calc(50% - 50vw) -} - -@media (min-width: 48rem) { - .text-component__block--left, .text-component__block--right { - width: 45% - } - .text-component__block--left img, .text-component__block--right img { - width: 100% - } - .text-component__block--left { - float: left; - margin-right: 0.75em; - margin-right: calc(var(--space-sm)*var(--text-vspace-multiplier, 1)) - } - .text-component__block--right { - float: right; - margin-left: 0.75em; - margin-left: calc(var(--space-sm)*var(--text-vspace-multiplier, 1)) - } -} - -@media (min-width: 90rem) { - .text-component__block--outset { - width: calc(100% + 10.5em); - width: calc(100% + 2*var(--space-xxl)) - } - .text-component__block--outset img { - width: 100% - } - .text-component__block--outset:not(.text-component__block--right) { - margin-left: -5.25em; - margin-left: calc(-1*var(--space-xxl)) - } - .text-component__block--left, .text-component__block--right { - width: 50% - } - .text-component__block--right.text-component__block--outset { - margin-right: -5.25em; - margin-right: calc(-1*var(--space-xxl)) - } -} - -:root { - --icon-xxs: 12px; - --icon-xs: 16px; - --icon-sm: 24px; - --icon-md: 32px; - --icon-lg: 48px; - --icon-xl: 64px; - --icon-xxl: 128px -} - -.icon--xxs { - font-size: 12px; - font-size: var(--icon-xxs) -} - -.icon--xs { - font-size: 16px; - font-size: var(--icon-xs) -} - -.icon--sm { - font-size: 24px; - font-size: var(--icon-sm) -} - -.icon--md { - font-size: 32px; - font-size: var(--icon-md) -} - -.icon--lg { - font-size: 48px; - font-size: var(--icon-lg) -} - -.icon--xl { - font-size: 64px; - font-size: var(--icon-xl) -} - -.icon--xxl { - font-size: 128px; - font-size: var(--icon-xxl) -} - -.icon--is-spinning { - -webkit-animation: icon-spin 1s infinite linear; - animation: icon-spin 1s infinite linear -} - -@-webkit-keyframes icon-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg) - } - 100% { - -webkit-transform: rotate(360deg); - transform: rotate(360deg) - } -} - -@keyframes icon-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg) - } - 100% { - -webkit-transform: rotate(360deg); - transform: rotate(360deg) - } -} - -.icon use { - color: inherit; - fill: currentColor -} - -.btn { - position: relative; - display: -ms-inline-flexbox; - display: inline-flex; - -ms-flex-pack: center; - justify-content: center; - -ms-flex-align: center; - align-items: center; - white-space: nowrap; - text-decoration: none; - line-height: 1; - font-size: 1em; - font-size: var(--btn-font-size, 1em); - padding-top: 0.5em; - padding-top: var(--btn-padding-y, 0.5em); - padding-bottom: 0.5em; - padding-bottom: var(--btn-padding-y, 0.5em); - padding-left: 0.75em; - padding-left: var(--btn-padding-x, 0.75em); - padding-right: 0.75em; - padding-right: var(--btn-padding-x, 0.75em); - border-radius: 0.25em; - border-radius: var(--btn-radius, 0.25em) -} - -.btn--primary { - background-color: hsl(220, 90%, 56%); - background-color: var(--color-primary, #2a6df4); - color: hsl(0, 0%, 100%); - color: var(--color-white, #fff) -} - -.btn--subtle { - background-color: hsl(240, 1%, 83%); - background-color: var(--color-contrast-low, #d3d3d4); - color: hsl(240, 8%, 12%); - color: var(--color-contrast-higher, #1c1c21) -} - -.btn--accent { - background-color: hsl(355, 90%, 61%); - background-color: var(--color-accent, #f54251); - color: hsl(0, 0%, 100%); - color: var(--color-white, #fff) -} - -.btn--disabled { - cursor: not-allowed -} - -.btn--sm { - font-size: 0.8em; - font-size: var(--btn-font-size-sm, 0.8em) -} - -.btn--md { - font-size: 1.2em; - font-size: var(--btn-font-size-md, 1.2em) -} - -.btn--lg { - font-size: 1.4em; - font-size: var(--btn-font-size-lg, 1.4em) -} - -.btn--icon { - padding: 0.5em; - padding: var(--btn-padding-y, 0.5em) -} - -.form-control { - background-color: hsl(0, 0%, 100%); - background-color: var(--color-bg, #f2f2f2); - padding-top: 0.5em; - padding-top: var(--form-control-padding-y, 0.5em); - padding-bottom: 0.5em; - padding-bottom: var(--form-control-padding-y, 0.5em); - padding-left: 0.75em; - padding-left: var(--form-control-padding-x, 0.75em); - padding-right: 0.75em; - padding-right: var(--form-control-padding-x, 0.75em); - border-radius: 0.25em; - border-radius: var(--form-control-radius, 0.25em) -} - -.form-control::-webkit-input-placeholder { - color: hsl(240, 1%, 48%); - color: var(--color-contrast-medium, #79797c) -} - -.form-control::-moz-placeholder { - opacity: 1; - color: hsl(240, 1%, 48%); - color: var(--color-contrast-medium, #79797c) -} - -.form-control:-ms-input-placeholder { - color: hsl(240, 1%, 48%); - color: var(--color-contrast-medium, #79797c) -} - -.form-control:-moz-placeholder { - color: hsl(240, 1%, 48%); - color: var(--color-contrast-medium, #79797c) -} - -.form-control[disabled], .form-control[readonly] { - cursor: not-allowed -} - -.form-legend { - color: hsl(240, 8%, 12%); - color: var(--color-contrast-higher, #1c1c21); - line-height: 1.2; - font-size: 1.2em; - font-size: var(--text-md, 1.2em); - margin-bottom: 0.375em; - margin-bottom: var(--space-xxs) -} - -.form-label { - display: inline-block -} - -.form__msg-error { - background-color: hsl(355, 90%, 61%); - background-color: var(--color-error, #f54251); - color: hsl(0, 0%, 100%); - color: var(--color-white, #fff); - font-size: 0.83333em; - font-size: var(--text-sm, 0.833em); - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - padding: 0.5em; - padding: var(--space-xs); - margin-top: 0.75em; - margin-top: var(--space-sm); - border-radius: 0.25em; - border-radius: var(--radius-md, 0.25em); - position: absolute; - clip: rect(1px, 1px, 1px, 1px) -} - -.form__msg-error::before { - content: ''; - position: absolute; - left: 0.75em; - left: var(--space-sm); - top: 0; - -webkit-transform: translateY(-100%); - -ms-transform: translateY(-100%); - transform: translateY(-100%); - width: 0; - height: 0; - border: 8px solid transparent; - border-bottom-color: hsl(355, 90%, 61%); - border-bottom-color: var(--color-error) -} - -.form__msg-error--is-visible { - position: relative; - clip: auto -} - -.radio-list>*, .checkbox-list>* { - position: relative; - display: -ms-flexbox; - display: flex; - -ms-flex-align: baseline; - align-items: baseline; - margin-bottom: 0.375em; - margin-bottom: var(--space-xxs) -} - -.radio-list>*:last-of-type, .checkbox-list>*:last-of-type { - margin-bottom: 0 -} - -.radio-list label, .checkbox-list label { - line-height: 1.4; - line-height: var(--body-line-height); - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none -} - -.radio-list input, .checkbox-list input { - vertical-align: top; - margin-right: 0.25em; - margin-right: var(--space-xxxs); - -ms-flex-negative: 0; - flex-shrink: 0 -} - -:root { - --zindex-header: 2; - --zindex-popover: 5; - --zindex-fixed-element: 10; - --zindex-overlay: 15 -} - -@media not all and (min-width: 32rem) { - .display\@xs { - display: none !important - } -} - -@media (min-width: 32rem) { - .hide\@xs { - display: none !important - } -} - -@media not all and (min-width: 48rem) { - .display\@sm { - display: none !important - } -} - -@media (min-width: 48rem) { - .hide\@sm { - display: none !important - } -} - -@media not all and (min-width: 64rem) { - .display\@md { - display: none !important - } -} - -@media (min-width: 64rem) { - .hide\@md { - display: none !important - } -} - -@media not all and (min-width: 80rem) { - .display\@lg { - display: none !important - } -} - -@media (min-width: 80rem) { - .hide\@lg { - display: none !important - } -} - -@media not all and (min-width: 90rem) { - .display\@xl { - display: none !important - } -} - -@media (min-width: 90rem) { - .hide\@xl { - display: none !important - } -} - -:root { - --display: block -} - -.is-visible { - display: block !important; - display: var(--display) !important -} - -.is-hidden { - display: none !important -} - -.sr-only { - position: absolute; - clip: rect(1px, 1px, 1px, 1px); - -webkit-clip-path: inset(50%); - clip-path: inset(50%); - width: 1px; - height: 1px; - overflow: hidden; - padding: 0; - border: 0; - white-space: nowrap -} - -.flex { - display: -ms-flexbox; - display: flex -} - -.inline-flex { - display: -ms-inline-flexbox; - display: inline-flex -} - -.flex-wrap { - -ms-flex-wrap: wrap; - flex-wrap: wrap -} - -.flex-column { - -ms-flex-direction: column; - flex-direction: column -} - -.flex-row { - -ms-flex-direction: row; - flex-direction: row -} - -.flex-center { - -ms-flex-pack: center; - justify-content: center; - -ms-flex-align: center; - align-items: center -} - -.justify-start { - -ms-flex-pack: start; - justify-content: flex-start -} - -.justify-end { - -ms-flex-pack: end; - justify-content: flex-end -} - -.justify-center { - -ms-flex-pack: center; - justify-content: center -} - -.justify-between { - -ms-flex-pack: justify; - justify-content: space-between -} - -.items-center { - -ms-flex-align: center; - align-items: center -} - -.items-start { - -ms-flex-align: start; - align-items: flex-start -} - -.items-end { - -ms-flex-align: end; - align-items: flex-end -} - -@media (min-width: 32rem) { - .flex-wrap\@xs { - -ms-flex-wrap: wrap; - flex-wrap: wrap - } - .flex-column\@xs { - -ms-flex-direction: column; - flex-direction: column - } - .flex-row\@xs { - -ms-flex-direction: row; - flex-direction: row - } - .flex-center\@xs { - -ms-flex-pack: center; - justify-content: center; - -ms-flex-align: center; - align-items: center - } - .justify-start\@xs { - -ms-flex-pack: start; - justify-content: flex-start - } - .justify-end\@xs { - -ms-flex-pack: end; - justify-content: flex-end - } - .justify-center\@xs { - -ms-flex-pack: center; - justify-content: center - } - .justify-between\@xs { - -ms-flex-pack: justify; - justify-content: space-between - } - .items-center\@xs { - -ms-flex-align: center; - align-items: center - } - .items-start\@xs { - -ms-flex-align: start; - align-items: flex-start - } - .items-end\@xs { - -ms-flex-align: end; - align-items: flex-end - } -} - -@media (min-width: 48rem) { - .flex-wrap\@sm { - -ms-flex-wrap: wrap; - flex-wrap: wrap - } - .flex-column\@sm { - -ms-flex-direction: column; - flex-direction: column - } - .flex-row\@sm { - -ms-flex-direction: row; - flex-direction: row - } - .flex-center\@sm { - -ms-flex-pack: center; - justify-content: center; - -ms-flex-align: center; - align-items: center - } - .justify-start\@sm { - -ms-flex-pack: start; - justify-content: flex-start - } - .justify-end\@sm { - -ms-flex-pack: end; - justify-content: flex-end - } - .justify-center\@sm { - -ms-flex-pack: center; - justify-content: center - } - .justify-between\@sm { - -ms-flex-pack: justify; - justify-content: space-between - } - .items-center\@sm { - -ms-flex-align: center; - align-items: center - } - .items-start\@sm { - -ms-flex-align: start; - align-items: flex-start - } - .items-end\@sm { - -ms-flex-align: end; - align-items: flex-end - } -} - -@media (min-width: 64rem) { - .flex-wrap\@md { - -ms-flex-wrap: wrap; - flex-wrap: wrap - } - .flex-column\@md { - -ms-flex-direction: column; - flex-direction: column - } - .flex-row\@md { - -ms-flex-direction: row; - flex-direction: row - } - .flex-center\@md { - -ms-flex-pack: center; - justify-content: center; - -ms-flex-align: center; - align-items: center - } - .justify-start\@md { - -ms-flex-pack: start; - justify-content: flex-start - } - .justify-end\@md { - -ms-flex-pack: end; - justify-content: flex-end - } - .justify-center\@md { - -ms-flex-pack: center; - justify-content: center - } - .justify-between\@md { - -ms-flex-pack: justify; - justify-content: space-between - } - .items-center\@md { - -ms-flex-align: center; - align-items: center - } - .items-start\@md { - -ms-flex-align: start; - align-items: flex-start - } - .items-end\@md { - -ms-flex-align: end; - align-items: flex-end - } -} - -@media (min-width: 80rem) { - .flex-wrap\@lg { - -ms-flex-wrap: wrap; - flex-wrap: wrap - } - .flex-column\@lg { - -ms-flex-direction: column; - flex-direction: column - } - .flex-row\@lg { - -ms-flex-direction: row; - flex-direction: row - } - .flex-center\@lg { - -ms-flex-pack: center; - justify-content: center; - -ms-flex-align: center; - align-items: center - } - .justify-start\@lg { - -ms-flex-pack: start; - justify-content: flex-start - } - .justify-end\@lg { - -ms-flex-pack: end; - justify-content: flex-end - } - .justify-center\@lg { - -ms-flex-pack: center; - justify-content: center - } - .justify-between\@lg { - -ms-flex-pack: justify; - justify-content: space-between - } - .items-center\@lg { - -ms-flex-align: center; - align-items: center - } - .items-start\@lg { - -ms-flex-align: start; - align-items: flex-start - } - .items-end\@lg { - -ms-flex-align: end; - align-items: flex-end - } -} - -@media (min-width: 90rem) { - .flex-wrap\@xl { - -ms-flex-wrap: wrap; - flex-wrap: wrap - } - .flex-column\@xl { - -ms-flex-direction: column; - flex-direction: column - } - .flex-row\@xl { - -ms-flex-direction: row; - flex-direction: row - } - .flex-center\@xl { - -ms-flex-pack: center; - justify-content: center; - -ms-flex-align: center; - align-items: center - } - .justify-start\@xl { - -ms-flex-pack: start; - justify-content: flex-start - } - .justify-end\@xl { - -ms-flex-pack: end; - justify-content: flex-end - } - .justify-center\@xl { - -ms-flex-pack: center; - justify-content: center - } - .justify-between\@xl { - -ms-flex-pack: justify; - justify-content: space-between - } - .items-center\@xl { - -ms-flex-align: center; - align-items: center - } - .items-start\@xl { - -ms-flex-align: start; - align-items: flex-start - } - .items-end\@xl { - -ms-flex-align: end; - align-items: flex-end - } -} - -.flex-grow { - -ms-flex-positive: 1; - flex-grow: 1 -} - -.flex-shrink-0 { - -ms-flex-negative: 0; - flex-shrink: 0 -} - -.flex-gap-xxxs { - margin-bottom: -0.25em; - margin-bottom: calc(-1*var(--space-xxxs)); - margin-right: -0.25em; - margin-right: calc(-1*var(--space-xxxs)) -} - -.flex-gap-xxxs>* { - margin-bottom: 0.25em; - margin-bottom: var(--space-xxxs); - margin-right: 0.25em; - margin-right: var(--space-xxxs) -} - -.flex-gap-xxs { - margin-bottom: -0.375em; - margin-bottom: calc(-1*var(--space-xxs)); - margin-right: -0.375em; - margin-right: calc(-1*var(--space-xxs)) -} - -.flex-gap-xxs>* { - margin-bottom: 0.375em; - margin-bottom: var(--space-xxs); - margin-right: 0.375em; - margin-right: var(--space-xxs) -} - -.flex-gap-xs { - margin-bottom: -0.5em; - margin-bottom: calc(-1*var(--space-xs)); - margin-right: -0.5em; - margin-right: calc(-1*var(--space-xs)) -} - -.flex-gap-xs>* { - margin-bottom: 0.5em; - margin-bottom: var(--space-xs); - margin-right: 0.5em; - margin-right: var(--space-xs) -} - -.flex-gap-sm { - margin-bottom: -0.75em; - margin-bottom: calc(-1*var(--space-sm)); - margin-right: -0.75em; - margin-right: calc(-1*var(--space-sm)) -} - -.flex-gap-sm>* { - margin-bottom: 0.75em; - margin-bottom: var(--space-sm); - margin-right: 0.75em; - margin-right: var(--space-sm) -} - -.flex-gap-md { - margin-bottom: -1.25em; - margin-bottom: calc(-1*var(--space-md)); - margin-right: -1.25em; - margin-right: calc(-1*var(--space-md)) -} - -.flex-gap-md>* { - margin-bottom: 1.25em; - margin-bottom: var(--space-md); - margin-right: 1.25em; - margin-right: var(--space-md) -} - -.flex-gap-lg { - margin-bottom: -2em; - margin-bottom: calc(-1*var(--space-lg)); - margin-right: -2em; - margin-right: calc(-1*var(--space-lg)) -} - -.flex-gap-lg>* { - margin-bottom: 2em; - margin-bottom: var(--space-lg); - margin-right: 2em; - margin-right: var(--space-lg) -} - -.flex-gap-xl { - margin-bottom: -3.25em; - margin-bottom: calc(-1*var(--space-xl)); - margin-right: -3.25em; - margin-right: calc(-1*var(--space-xl)) -} - -.flex-gap-xl>* { - margin-bottom: 3.25em; - margin-bottom: var(--space-xl); - margin-right: 3.25em; - margin-right: var(--space-xl) -} - -.flex-gap-xxl { - margin-bottom: -5.25em; - margin-bottom: calc(-1*var(--space-xxl)); - margin-right: -5.25em; - margin-right: calc(-1*var(--space-xxl)) -} - -.flex-gap-xxl>* { - margin-bottom: 5.25em; - margin-bottom: var(--space-xxl); - margin-right: 5.25em; - margin-right: var(--space-xxl) -} - -.margin-xxxxs { - margin: 0.125em; - margin: var(--space-xxxxs) -} - -.margin-xxxs { - margin: 0.25em; - margin: var(--space-xxxs) -} - -.margin-xxs { - margin: 0.375em; - margin: var(--space-xxs) -} - -.margin-xs { - margin: 0.5em; - margin: var(--space-xs) -} - -.margin-sm { - margin: 0.75em; - margin: var(--space-sm) -} - -.margin-md { - margin: 1.25em; - margin: var(--space-md) -} - -.margin-lg { - margin: 2em; - margin: var(--space-lg) -} - -.margin-xl { - margin: 3.25em; - margin: var(--space-xl) -} - -.margin-xxl { - margin: 5.25em; - margin: var(--space-xxl) -} - -.margin-xxxl { - margin: 8.5em; - margin: var(--space-xxxl) -} - -.margin-xxxxl { - margin: 13.75em; - margin: var(--space-xxxxl) -} - -.margin-auto { - margin: auto -} - -.margin-top-xxxxs { - margin-top: 0.125em; - margin-top: var(--space-xxxxs) -} - -.margin-top-xxxs { - margin-top: 0.25em; - margin-top: var(--space-xxxs) -} - -.margin-top-xxs { - margin-top: 0.375em; - margin-top: var(--space-xxs) -} - -.margin-top-xs { - margin-top: 0.5em; - margin-top: var(--space-xs) -} - -.margin-top-sm { - margin-top: 0.75em; - margin-top: var(--space-sm) -} - -.margin-top-md { - margin-top: 1.25em; - margin-top: var(--space-md) -} - -.margin-top-lg { - margin-top: 2em; - margin-top: var(--space-lg) -} - -.margin-top-xl { - margin-top: 3.25em; - margin-top: var(--space-xl) -} - -.margin-top-xxl { - margin-top: 5.25em; - margin-top: var(--space-xxl) -} - -.margin-top-xxxl { - margin-top: 8.5em; - margin-top: var(--space-xxxl) -} - -.margin-top-xxxxl { - margin-top: 13.75em; - margin-top: var(--space-xxxxl) -} - -.margin-top-auto { - margin-top: auto -} - -.margin-bottom-xxxxs { - margin-bottom: 0.125em; - margin-bottom: var(--space-xxxxs) -} - -.margin-bottom-xxxs { - margin-bottom: 0.25em; - margin-bottom: var(--space-xxxs) -} - -.margin-bottom-xxs { - margin-bottom: 0.375em; - margin-bottom: var(--space-xxs) -} - -.margin-bottom-xs { - margin-bottom: 0.5em; - margin-bottom: var(--space-xs) -} - -.margin-bottom-sm { - margin-bottom: 0.75em; - margin-bottom: var(--space-sm) -} - -.margin-bottom-md { - margin-bottom: 1.25em; - margin-bottom: var(--space-md) -} - -.margin-bottom-lg { - margin-bottom: 2em; - margin-bottom: var(--space-lg) -} - -.margin-bottom-xl { - margin-bottom: 3.25em; - margin-bottom: var(--space-xl) -} - -.margin-bottom-xxl { - margin-bottom: 5.25em; - margin-bottom: var(--space-xxl) -} - -.margin-bottom-xxxl { - margin-bottom: 8.5em; - margin-bottom: var(--space-xxxl) -} - -.margin-bottom-xxxxl { - margin-bottom: 13.75em; - margin-bottom: var(--space-xxxxl) -} - -.margin-bottom-auto { - margin-bottom: auto -} - -.margin-right-xxxxs { - margin-right: 0.125em; - margin-right: var(--space-xxxxs) -} - -.margin-right-xxxs { - margin-right: 0.25em; - margin-right: var(--space-xxxs) -} - -.margin-right-xxs { - margin-right: 0.375em; - margin-right: var(--space-xxs) -} - -.margin-right-xs { - margin-right: 0.5em; - margin-right: var(--space-xs) -} - -.margin-right-sm { - margin-right: 0.75em; - margin-right: var(--space-sm) -} - -.margin-right-md { - margin-right: 1.25em; - margin-right: var(--space-md) -} - -.margin-right-lg { - margin-right: 2em; - margin-right: var(--space-lg) -} - -.margin-right-xl { - margin-right: 3.25em; - margin-right: var(--space-xl) -} - -.margin-right-xxl { - margin-right: 5.25em; - margin-right: var(--space-xxl) -} - -.margin-right-xxxl { - margin-right: 8.5em; - margin-right: var(--space-xxxl) -} - -.margin-right-xxxxl { - margin-right: 13.75em; - margin-right: var(--space-xxxxl) -} - -.margin-right-auto { - margin-right: auto -} - -.margin-left-xxxxs { - margin-left: 0.125em; - margin-left: var(--space-xxxxs) -} - -.margin-left-xxxs { - margin-left: 0.25em; - margin-left: var(--space-xxxs) -} - -.margin-left-xxs { - margin-left: 0.375em; - margin-left: var(--space-xxs) -} - -.margin-left-xs { - margin-left: 0.5em; - margin-left: var(--space-xs) -} - -.margin-left-sm { - margin-left: 0.75em; - margin-left: var(--space-sm) -} - -.margin-left-md { - margin-left: 1.25em; - margin-left: var(--space-md) -} - -.margin-left-lg { - margin-left: 2em; - margin-left: var(--space-lg) -} - -.margin-left-xl { - margin-left: 3.25em; - margin-left: var(--space-xl) -} - -.margin-left-xxl { - margin-left: 5.25em; - margin-left: var(--space-xxl) -} - -.margin-left-xxxl { - margin-left: 8.5em; - margin-left: var(--space-xxxl) -} - -.margin-left-xxxxl { - margin-left: 13.75em; - margin-left: var(--space-xxxxl) -} - -.margin-left-auto { - margin-left: auto -} - -.margin-x-xxxxs { - margin-left: 0.125em; - margin-left: var(--space-xxxxs); - margin-right: 0.125em; - margin-right: var(--space-xxxxs) -} - -.margin-x-xxxs { - margin-left: 0.25em; - margin-left: var(--space-xxxs); - margin-right: 0.25em; - margin-right: var(--space-xxxs) -} - -.margin-x-xxs { - margin-left: 0.375em; - margin-left: var(--space-xxs); - margin-right: 0.375em; - margin-right: var(--space-xxs) -} - -.margin-x-xs { - margin-left: 0.5em; - margin-left: var(--space-xs); - margin-right: 0.5em; - margin-right: var(--space-xs) -} - -.margin-x-sm { - margin-left: 0.75em; - margin-left: var(--space-sm); - margin-right: 0.75em; - margin-right: var(--space-sm) -} - -.margin-x-md { - margin-left: 1.25em; - margin-left: var(--space-md); - margin-right: 1.25em; - margin-right: var(--space-md) -} - -.margin-x-lg { - margin-left: 2em; - margin-left: var(--space-lg); - margin-right: 2em; - margin-right: var(--space-lg) -} - -.margin-x-xl { - margin-left: 3.25em; - margin-left: var(--space-xl); - margin-right: 3.25em; - margin-right: var(--space-xl) -} - -.margin-x-xxl { - margin-left: 5.25em; - margin-left: var(--space-xxl); - margin-right: 5.25em; - margin-right: var(--space-xxl) -} - -.margin-x-xxxl { - margin-left: 8.5em; - margin-left: var(--space-xxxl); - margin-right: 8.5em; - margin-right: var(--space-xxxl) -} - -.margin-x-xxxxl { - margin-left: 13.75em; - margin-left: var(--space-xxxxl); - margin-right: 13.75em; - margin-right: var(--space-xxxxl) -} - -.margin-x-auto { - margin-left: auto; - margin-right: auto -} - -.margin-y-xxxxs { - margin-top: 0.125em; - margin-top: var(--space-xxxxs); - margin-bottom: 0.125em; - margin-bottom: var(--space-xxxxs) -} - -.margin-y-xxxs { - margin-top: 0.25em; - margin-top: var(--space-xxxs); - margin-bottom: 0.25em; - margin-bottom: var(--space-xxxs) -} - -.margin-y-xxs { - margin-top: 0.375em; - margin-top: var(--space-xxs); - margin-bottom: 0.375em; - margin-bottom: var(--space-xxs) -} - -.margin-y-xs { - margin-top: 0.5em; - margin-top: var(--space-xs); - margin-bottom: 0.5em; - margin-bottom: var(--space-xs) -} - -.margin-y-sm { - margin-top: 0.75em; - margin-top: var(--space-sm); - margin-bottom: 0.75em; - margin-bottom: var(--space-sm) -} - -.margin-y-md { - margin-top: 1.25em; - margin-top: var(--space-md); - margin-bottom: 1.25em; - margin-bottom: var(--space-md) -} - -.margin-y-lg { - margin-top: 2em; - margin-top: var(--space-lg); - margin-bottom: 2em; - margin-bottom: var(--space-lg) -} - -.margin-y-xl { - margin-top: 3.25em; - margin-top: var(--space-xl); - margin-bottom: 3.25em; - margin-bottom: var(--space-xl) -} - -.margin-y-xxl { - margin-top: 5.25em; - margin-top: var(--space-xxl); - margin-bottom: 5.25em; - margin-bottom: var(--space-xxl) -} - -.margin-y-xxxl { - margin-top: 8.5em; - margin-top: var(--space-xxxl); - margin-bottom: 8.5em; - margin-bottom: var(--space-xxxl) -} - -.margin-y-xxxxl { - margin-top: 13.75em; - margin-top: var(--space-xxxxl); - margin-bottom: 13.75em; - margin-bottom: var(--space-xxxxl) -} - -.margin-y-auto { - margin-top: auto; - margin-bottom: auto -} - -@media not all and (min-width: 32rem) { - .has-margin\@xs { - margin: 0 !important - } -} - -@media not all and (min-width: 48rem) { - .has-margin\@sm { - margin: 0 !important - } -} - -@media not all and (min-width: 64rem) { - .has-margin\@md { - margin: 0 !important - } -} - -@media not all and (min-width: 80rem) { - .has-margin\@lg { - margin: 0 !important - } -} - -@media not all and (min-width: 90rem) { - .has-margin\@xl { - margin: 0 !important - } -} - -.padding-md { - padding: 1.25em; - padding: var(--space-md) -} - -.padding-xxxxs { - padding: 0.125em; - padding: var(--space-xxxxs) -} - -.padding-xxxs { - padding: 0.25em; - padding: var(--space-xxxs) -} - -.padding-xxs { - padding: 0.375em; - padding: var(--space-xxs) -} - -.padding-xs { - padding: 0.5em; - padding: var(--space-xs) -} - -.padding-sm { - padding: 0.75em; - padding: var(--space-sm) -} - -.padding-lg { - padding: 2em; - padding: var(--space-lg) -} - -.padding-xl { - padding: 3.25em; - padding: var(--space-xl) -} - -.padding-xxl { - padding: 5.25em; - padding: var(--space-xxl) -} - -.padding-xxxl { - padding: 8.5em; - padding: var(--space-xxxl) -} - -.padding-xxxxl { - padding: 13.75em; - padding: var(--space-xxxxl) -} - -.padding-component { - padding: 1.25em; - padding: var(--component-padding) -} - -.padding-top-md { - padding-top: 1.25em; - padding-top: var(--space-md) -} - -.padding-top-xxxxs { - padding-top: 0.125em; - padding-top: var(--space-xxxxs) -} - -.padding-top-xxxs { - padding-top: 0.25em; - padding-top: var(--space-xxxs) -} - -.padding-top-xxs { - padding-top: 0.375em; - padding-top: var(--space-xxs) -} - -.padding-top-xs { - padding-top: 0.5em; - padding-top: var(--space-xs) -} - -.padding-top-sm { - padding-top: 0.75em; - padding-top: var(--space-sm) -} - -.padding-top-lg { - padding-top: 2em; - padding-top: var(--space-lg) -} - -.padding-top-xl { - padding-top: 3.25em; - padding-top: var(--space-xl) -} - -.padding-top-xxl { - padding-top: 5.25em; - padding-top: var(--space-xxl) -} - -.padding-top-xxxl { - padding-top: 8.5em; - padding-top: var(--space-xxxl) -} - -.padding-top-xxxxl { - padding-top: 13.75em; - padding-top: var(--space-xxxxl) -} - -.padding-top-component { - padding-top: 1.25em; - padding-top: var(--component-padding) -} - -.padding-bottom-md { - padding-bottom: 1.25em; - padding-bottom: var(--space-md) -} - -.padding-bottom-xxxxs { - padding-bottom: 0.125em; - padding-bottom: var(--space-xxxxs) -} - -.padding-bottom-xxxs { - padding-bottom: 0.25em; - padding-bottom: var(--space-xxxs) -} - -.padding-bottom-xxs { - padding-bottom: 0.375em; - padding-bottom: var(--space-xxs) -} - -.padding-bottom-xs { - padding-bottom: 0.5em; - padding-bottom: var(--space-xs) -} - -.padding-bottom-sm { - padding-bottom: 0.75em; - padding-bottom: var(--space-sm) -} - -.padding-bottom-lg { - padding-bottom: 2em; - padding-bottom: var(--space-lg) -} - -.padding-bottom-xl { - padding-bottom: 3.25em; - padding-bottom: var(--space-xl) -} - -.padding-bottom-xxl { - padding-bottom: 5.25em; - padding-bottom: var(--space-xxl) -} - -.padding-bottom-xxxl { - padding-bottom: 8.5em; - padding-bottom: var(--space-xxxl) -} - -.padding-bottom-xxxxl { - padding-bottom: 13.75em; - padding-bottom: var(--space-xxxxl) -} - -.padding-bottom-component { - padding-bottom: 1.25em; - padding-bottom: var(--component-padding) -} - -.padding-right-md { - padding-right: 1.25em; - padding-right: var(--space-md) -} - -.padding-right-xxxxs { - padding-right: 0.125em; - padding-right: var(--space-xxxxs) -} - -.padding-right-xxxs { - padding-right: 0.25em; - padding-right: var(--space-xxxs) -} - -.padding-right-xxs { - padding-right: 0.375em; - padding-right: var(--space-xxs) -} - -.padding-right-xs { - padding-right: 0.5em; - padding-right: var(--space-xs) -} - -.padding-right-sm { - padding-right: 0.75em; - padding-right: var(--space-sm) -} - -.padding-right-lg { - padding-right: 2em; - padding-right: var(--space-lg) -} - -.padding-right-xl { - padding-right: 3.25em; - padding-right: var(--space-xl) -} - -.padding-right-xxl { - padding-right: 5.25em; - padding-right: var(--space-xxl) -} - -.padding-right-xxxl { - padding-right: 8.5em; - padding-right: var(--space-xxxl) -} - -.padding-right-xxxxl { - padding-right: 13.75em; - padding-right: var(--space-xxxxl) -} - -.padding-right-component { - padding-right: 1.25em; - padding-right: var(--component-padding) -} - -.padding-left-md { - padding-left: 1.25em; - padding-left: var(--space-md) -} - -.padding-left-xxxxs { - padding-left: 0.125em; - padding-left: var(--space-xxxxs) -} - -.padding-left-xxxs { - padding-left: 0.25em; - padding-left: var(--space-xxxs) -} - -.padding-left-xxs { - padding-left: 0.375em; - padding-left: var(--space-xxs) -} - -.padding-left-xs { - padding-left: 0.5em; - padding-left: var(--space-xs) -} - -.padding-left-sm { - padding-left: 0.75em; - padding-left: var(--space-sm) -} - -.padding-left-lg { - padding-left: 2em; - padding-left: var(--space-lg) -} - -.padding-left-xl { - padding-left: 3.25em; - padding-left: var(--space-xl) -} - -.padding-left-xxl { - padding-left: 5.25em; - padding-left: var(--space-xxl) -} - -.padding-left-xxxl { - padding-left: 8.5em; - padding-left: var(--space-xxxl) -} - -.padding-left-xxxxl { - padding-left: 13.75em; - padding-left: var(--space-xxxxl) -} - -.padding-left-component { - padding-left: 1.25em; - padding-left: var(--component-padding) -} - -.padding-x-md { - padding-left: 1.25em; - padding-left: var(--space-md); - padding-right: 1.25em; - padding-right: var(--space-md) -} - -.padding-x-xxxxs { - padding-left: 0.125em; - padding-left: var(--space-xxxxs); - padding-right: 0.125em; - padding-right: var(--space-xxxxs) -} - -.padding-x-xxxs { - padding-left: 0.25em; - padding-left: var(--space-xxxs); - padding-right: 0.25em; - padding-right: var(--space-xxxs) -} - -.padding-x-xxs { - padding-left: 0.375em; - padding-left: var(--space-xxs); - padding-right: 0.375em; - padding-right: var(--space-xxs) -} - -.padding-x-xs { - padding-left: 0.5em; - padding-left: var(--space-xs); - padding-right: 0.5em; - padding-right: var(--space-xs) -} - -.padding-x-sm { - padding-left: 0.75em; - padding-left: var(--space-sm); - padding-right: 0.75em; - padding-right: var(--space-sm) -} - -.padding-x-lg { - padding-left: 2em; - padding-left: var(--space-lg); - padding-right: 2em; - padding-right: var(--space-lg) -} - -.padding-x-xl { - padding-left: 3.25em; - padding-left: var(--space-xl); - padding-right: 3.25em; - padding-right: var(--space-xl) -} - -.padding-x-xxl { - padding-left: 5.25em; - padding-left: var(--space-xxl); - padding-right: 5.25em; - padding-right: var(--space-xxl) -} - -.padding-x-xxxl { - padding-left: 8.5em; - padding-left: var(--space-xxxl); - padding-right: 8.5em; - padding-right: var(--space-xxxl) -} - -.padding-x-xxxxl { - padding-left: 13.75em; - padding-left: var(--space-xxxxl); - padding-right: 13.75em; - padding-right: var(--space-xxxxl) -} - -.padding-x-component { - padding-left: 1.25em; - padding-left: var(--component-padding); - padding-right: 1.25em; - padding-right: var(--component-padding) -} - -.padding-y-md { - padding-top: 1.25em; - padding-top: var(--space-md); - padding-bottom: 1.25em; - padding-bottom: var(--space-md) -} - -.padding-y-xxxxs { - padding-top: 0.125em; - padding-top: var(--space-xxxxs); - padding-bottom: 0.125em; - padding-bottom: var(--space-xxxxs) -} - -.padding-y-xxxs { - padding-top: 0.25em; - padding-top: var(--space-xxxs); - padding-bottom: 0.25em; - padding-bottom: var(--space-xxxs) -} - -.padding-y-xxs { - padding-top: 0.375em; - padding-top: var(--space-xxs); - padding-bottom: 0.375em; - padding-bottom: var(--space-xxs) -} - -.padding-y-xs { - padding-top: 0.5em; - padding-top: var(--space-xs); - padding-bottom: 0.5em; - padding-bottom: var(--space-xs) -} - -.padding-y-sm { - padding-top: 0.75em; - padding-top: var(--space-sm); - padding-bottom: 0.75em; - padding-bottom: var(--space-sm) -} - -.padding-y-lg { - padding-top: 2em; - padding-top: var(--space-lg); - padding-bottom: 2em; - padding-bottom: var(--space-lg) -} - -.padding-y-xl { - padding-top: 3.25em; - padding-top: var(--space-xl); - padding-bottom: 3.25em; - padding-bottom: var(--space-xl) -} - -.padding-y-xxl { - padding-top: 5.25em; - padding-top: var(--space-xxl); - padding-bottom: 5.25em; - padding-bottom: var(--space-xxl) -} - -.padding-y-xxxl { - padding-top: 8.5em; - padding-top: var(--space-xxxl); - padding-bottom: 8.5em; - padding-bottom: var(--space-xxxl) -} - -.padding-y-xxxxl { - padding-top: 13.75em; - padding-top: var(--space-xxxxl); - padding-bottom: 13.75em; - padding-bottom: var(--space-xxxxl) -} - -.padding-y-component { - padding-top: 1.25em; - padding-top: var(--component-padding); - padding-bottom: 1.25em; - padding-bottom: var(--component-padding) -} - -@media not all and (min-width: 32rem) { - .has-padding\@xs { - padding: 0 !important - } -} - -@media not all and (min-width: 48rem) { - .has-padding\@sm { - padding: 0 !important - } -} - -@media not all and (min-width: 64rem) { - .has-padding\@md { - padding: 0 !important - } -} - -@media not all and (min-width: 80rem) { - .has-padding\@lg { - padding: 0 !important - } -} - -@media not all and (min-width: 90rem) { - .has-padding\@xl { - padding: 0 !important - } -} - -.truncate { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap -} - -.text-replace { - overflow: hidden; - color: transparent; - text-indent: 100%; - white-space: nowrap -} - -.text-center { - text-align: center -} - -.text-left { - text-align: left -} - -.text-right { - text-align: right -} - -@media (min-width: 32rem) { - .text-center\@xs { - text-align: center - } - .text-left\@xs { - text-align: left - } - .text-right\@xs { - text-align: right - } -} - -@media (min-width: 48rem) { - .text-center\@sm { - text-align: center - } - .text-left\@sm { - text-align: left - } - .text-right\@sm { - text-align: right - } -} - -@media (min-width: 64rem) { - .text-center\@md { - text-align: center - } - .text-left\@md { - text-align: left - } - .text-right\@md { - text-align: right - } -} - -@media (min-width: 80rem) { - .text-center\@lg { - text-align: center - } - .text-left\@lg { - text-align: left - } - .text-right\@lg { - text-align: right - } -} - -@media (min-width: 90rem) { - .text-center\@xl { - text-align: center - } - .text-left\@xl { - text-align: left - } - .text-right\@xl { - text-align: right - } -} - -.color-inherit { - color: inherit -} - -.color-contrast-medium { - color: hsl(240, 1%, 48%); - color: var(--color-contrast-medium, #79797c) -} - -.color-contrast-high { - color: hsl(240, 4%, 20%); - color: var(--color-contrast-high, #313135) -} - -.color-contrast-higher { - color: hsl(240, 8%, 12%); - color: var(--color-contrast-higher, #1c1c21) -} - -.color-primary { - color: hsl(220, 90%, 56%); - color: var(--color-primary, #2a6df4) -} - -.color-accent { - color: hsl(355, 90%, 61%); - color: var(--color-accent, #f54251) -} - -.color-success { - color: hsl(94, 48%, 56%); - color: var(--color-success, #88c559) -} - -.color-warning { - color: hsl(46, 100%, 61%); - color: var(--color-warning, #ffd138) -} - -.color-error { - color: hsl(355, 90%, 61%); - color: var(--color-error, #f54251) -} - -.width-100\% { - width: 100% -} - -.height-100\% { - height: 100% -} - -.media-wrapper { - position: relative; - height: 0; - padding-bottom: 56.25% -} - -.media-wrapper iframe, .media-wrapper video, .media-wrapper img { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100% -} - -.media-wrapper video, .media-wrapper img { - -o-object-fit: cover; - object-fit: cover -} - -.media-wrapper--4\:3 { - padding-bottom: 75% -} - -:root, [data-theme="default"] { - --color-primary-darker: hsl(220, 90%, 36%); - --color-primary-darker-h: 220; - --color-primary-darker-s: 90%; - --color-primary-darker-l: 36%; - --color-primary-dark: hsl(220, 90%, 46%); - --color-primary-dark-h: 220; - --color-primary-dark-s: 90%; - --color-primary-dark-l: 46%; - --color-primary: hsl(220, 90%, 56%); - --color-primary-h: 220; - --color-primary-s: 90%; - --color-primary-l: 56%; - --color-primary-light: hsl(220, 90%, 66%); - --color-primary-light-h: 220; - --color-primary-light-s: 90%; - --color-primary-light-l: 66%; - --color-primary-lighter: hsl(220, 90%, 76%); - --color-primary-lighter-h: 220; - --color-primary-lighter-s: 90%; - --color-primary-lighter-l: 76%; - --color-accent-darker: hsl(355, 90%, 41%); - --color-accent-darker-h: 355; - --color-accent-darker-s: 90%; - --color-accent-darker-l: 41%; - --color-accent-dark: hsl(355, 90%, 51%); - --color-accent-dark-h: 355; - --color-accent-dark-s: 90%; - --color-accent-dark-l: 51%; - --color-accent: hsl(355, 90%, 61%); - --color-accent-h: 355; - --color-accent-s: 90%; - --color-accent-l: 61%; - --color-accent-light: hsl(355, 90%, 71%); - --color-accent-light-h: 355; - --color-accent-light-s: 90%; - --color-accent-light-l: 71%; - --color-accent-lighter: hsl(355, 90%, 81%); - --color-accent-lighter-h: 355; - --color-accent-lighter-s: 90%; - --color-accent-lighter-l: 81%; - --color-black: hsl(240, 8%, 12%); - --color-black-h: 240; - --color-black-s: 8%; - --color-black-l: 12%; - --color-white: hsl(0, 0%, 100%); - --color-white-h: 0; - --color-white-s: 0%; - --color-white-l: 100%; - --color-success-darker: hsl(94, 48%, 36%); - --color-success-darker-h: 94; - --color-success-darker-s: 48%; - --color-success-darker-l: 36%; - --color-success-dark: hsl(94, 48%, 46%); - --color-success-dark-h: 94; - --color-success-dark-s: 48%; - --color-success-dark-l: 46%; - --color-success: hsl(94, 48%, 56%); - --color-success-h: 94; - --color-success-s: 48%; - --color-success-l: 56%; - --color-success-light: hsl(94, 48%, 66%); - --color-success-light-h: 94; - --color-success-light-s: 48%; - --color-success-light-l: 66%; - --color-success-lighter: hsl(94, 48%, 76%); - --color-success-lighter-h: 94; - --color-success-lighter-s: 48%; - --color-success-lighter-l: 76%; - --color-error-darker: hsl(355, 90%, 41%); - --color-error-darker-h: 355; - --color-error-darker-s: 90%; - --color-error-darker-l: 41%; - --color-error-dark: hsl(355, 90%, 51%); - --color-error-dark-h: 355; - --color-error-dark-s: 90%; - --color-error-dark-l: 51%; - --color-error: hsl(355, 90%, 61%); - --color-error-h: 355; - --color-error-s: 90%; - --color-error-l: 61%; - --color-error-light: hsl(355, 90%, 71%); - --color-error-light-h: 355; - --color-error-light-s: 90%; - --color-error-light-l: 71%; - --color-error-lighter: hsl(355, 90%, 81%); - --color-error-lighter-h: 355; - --color-error-lighter-s: 90%; - --color-error-lighter-l: 81%; - --color-warning-darker: hsl(46, 100%, 41%); - --color-warning-darker-h: 46; - --color-warning-darker-s: 100%; - --color-warning-darker-l: 41%; - --color-warning-dark: hsl(46, 100%, 51%); - --color-warning-dark-h: 46; - --color-warning-dark-s: 100%; - --color-warning-dark-l: 51%; - --color-warning: hsl(46, 100%, 61%); - --color-warning-h: 46; - --color-warning-s: 100%; - --color-warning-l: 61%; - --color-warning-light: hsl(46, 100%, 71%); - --color-warning-light-h: 46; - --color-warning-light-s: 100%; - --color-warning-light-l: 71%; - --color-warning-lighter: hsl(46, 100%, 81%); - --color-warning-lighter-h: 46; - --color-warning-lighter-s: 100%; - --color-warning-lighter-l: 81%; - --color-bg: hsl(0, 0%, 100%); - --color-bg-h: 0; - --color-bg-s: 0%; - --color-bg-l: 100%; - --color-contrast-lower: hsl(0, 0%, 95%); - --color-contrast-lower-h: 0; - --color-contrast-lower-s: 0%; - --color-contrast-lower-l: 95%; - --color-contrast-low: hsl(240, 1%, 83%); - --color-contrast-low-h: 240; - --color-contrast-low-s: 1%; - --color-contrast-low-l: 83%; - --color-contrast-medium: hsl(240, 1%, 48%); - --color-contrast-medium-h: 240; - --color-contrast-medium-s: 1%; - --color-contrast-medium-l: 48%; - --color-contrast-high: hsl(240, 4%, 20%); - --color-contrast-high-h: 240; - --color-contrast-high-s: 4%; - --color-contrast-high-l: 20%; - --color-contrast-higher: hsl(240, 8%, 12%); - --color-contrast-higher-h: 240; - --color-contrast-higher-s: 8%; - --color-contrast-higher-l: 12% -} - -@supports (--css: variables) { - @media (min-width: 64rem) { - :root { - --space-unit: 1.25em - } - } -} - -:root { - --radius: 0.25em -} - -:root { - --font-primary: sans-serif; - --text-base-size: 1em; - --text-scale-ratio: 1.2; - --text-xs: calc(1em/var(--text-scale-ratio)/var(--text-scale-ratio)); - --text-sm: calc(var(--text-xs)*var(--text-scale-ratio)); - --text-md: calc(var(--text-sm)*var(--text-scale-ratio)*var(--text-scale-ratio)); - --text-lg: calc(var(--text-md)*var(--text-scale-ratio)); - --text-xl: calc(var(--text-lg)*var(--text-scale-ratio)); - --text-xxl: calc(var(--text-xl)*var(--text-scale-ratio)); - --text-xxxl: calc(var(--text-xxl)*var(--text-scale-ratio)); - --body-line-height: 1.4; - --heading-line-height: 1.2; - --font-primary-capital-letter: 1 -} - -@supports (--css: variables) { - @media (min-width: 64rem) { - :root { - --text-base-size: 1.25em; - --text-scale-ratio: 1.25 - } - } -} - -mark { - background-color: hsla(355, 90%, 61%, 0.2); - background-color: hsla(var(--color-accent-h), var(--color-accent-s), var(--color-accent-l), 0.2); - color: inherit -} - -.text-component { - --line-height-multiplier: 1; - --text-vspace-multiplier: 1 -} - -.text-component blockquote { - padding-left: 1em; - border-left: 4px solid hsl(240, 1%, 83%); - border-left: 4px solid var(--color-contrast-low) -} - -.text-component hr { - background: hsl(240, 1%, 83%); - background: var(--color-contrast-low); - height: 1px -} - -.text-component figcaption { - font-size: 0.83333em; - font-size: var(--text-sm); - color: hsl(240, 1%, 48%); - color: var(--color-contrast-medium) -} - -.article.text-component { - --line-height-multiplier: 1.13; - --text-vspace-multiplier: 1.2 -} - -:root { - --btn-font-size: 1em; - --btn-font-size-sm: calc(var(--btn-font-size) - 0.2em); - --btn-font-size-md: calc(var(--btn-font-size) + 0.2em); - --btn-font-size-lg: calc(var(--btn-font-size) + 0.4em); - --btn-radius: 0.25em; - --btn-padding-x: var(--space-sm); - --btn-padding-y: var(--space-xs) -} - -.btn { - --color-shadow: hsla(240, 8%, 12%, 0.15); - --color-shadow: hsla(var(--color-black-h), var(--color-black-s), var(--color-black-l), 0.15); - box-shadow: 0 4px 16px hsla(240, 8%, 12%, 0.15); - box-shadow: 0 4px 16px hsla(var(--color-black-h), var(--color-black-s), var(--color-black-l), 0.15); - cursor: pointer -} - -.btn--primary { - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale -} - -.btn--accent { - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale -} - -.btn--disabled { - opacity: 0.6 -} - -:root { - --form-control-padding-x: var(--space-sm); - --form-control-padding-y: var(--space-xs); - --form-control-radius: 0.25em -} - -.form-control { - border: 2px solid hsl(240, 1%, 83%); - border: 2px solid var(--color-contrast-low) -} - -.form-control:focus { - outline: none; - border-color: hsl(220, 90%, 56%); - border-color: var(--color-primary); - --color-shadow: hsla(220, 90%, 56%, 0.2); - --color-shadow: hsla(var(--color-primary-h), var(--color-primary-s), var(--color-primary-l), 0.2); - box-shadow: undefined; - box-shadow: 0 0 0 3px var(--color-shadow) -} - -.form-control:focus:focus { - box-shadow: 0 0 0 3px hsla(220, 90%, 56%, 0.2); - box-shadow: 0 0 0 3px var(--color-shadow) -} - -.form-control[aria-invalid="true"] { - border-color: hsl(355, 90%, 61%); - border-color: var(--color-error) -} - -.form-control[aria-invalid="true"]:focus { - --color-shadow: hsla(355, 90%, 61%, 0.2); - --color-shadow: hsla(var(--color-error-h), var(--color-error-s), var(--color-error-l), 0.2); - box-shadow: undefined; - box-shadow: 0 0 0 3px var(--color-shadow) -} - -.form-control[aria-invalid="true"]:focus:focus { - box-shadow: 0 0 0 3px hsla(355, 90%, 61%, 0.2); - box-shadow: 0 0 0 3px var(--color-shadow) -} - -.form-label { - font-size: 0.83333em; - font-size: var(--text-sm) -} - -:root { - --cd-color-1: hsl(206, 21%, 24%); - --cd-color-1-h: 206; - --cd-color-1-s: 21%; - --cd-color-1-l: 24%; - --cd-color-2: hsl(205, 38%, 89%); - --cd-color-2-h: 205; - --cd-color-2-s: 38%; - --cd-color-2-l: 89%; - --cd-color-3: hsl(207, 10%, 55%); - --cd-color-3-h: 207; - --cd-color-3-s: 10%; - --cd-color-3-l: 55%; - --cd-color-4: hsl(111, 51%, 60%); - --cd-color-4-h: 111; - --cd-color-4-s: 51%; - --cd-color-4-l: 60%; - --cd-color-5: hsl(356, 53%, 49%); - --cd-color-5-h: 356; - --cd-color-5-s: 53%; - --cd-color-5-l: 49%; - --cd-color-6: hsl(47, 85%, 61%); - --cd-color-6-h: 47; - --cd-color-6-s: 85%; - --cd-color-6-l: 61%; - --cd-header-height: 200px; - --font-primary: 'Droid Serif', serif; - --font-secondary: 'Open Sans', sans-serif -} - -@supports (--css: variables) { - @media (min-width: 64rem) { - :root { - --cd-header-height: 300px - } - } -} - -.cd-main-header { - height: 200px; - height: var(--cd-header-height); - background: hsl(206, 21%, 24%); - background: var(--cd-color-1); - color: hsl(0, 0%, 100%); - color: var(--color-white); - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale -} - -.cd-main-header h1 { - color: inherit -} - -.cd-timeline { - overflow: hidden; - padding: 2em 0; - padding: var(--space-lg) 0; - color: hsl(207, 10%, 55%); - color: var(--cd-color-3); - background-color: hsl(205, 38%, 93.45%); - background-color: hsl(var(--cd-color-2-h), var(--cd-color-2-s), calc(var(--cd-color-2-l)*1.05)); -} - -.cd-timeline h2 { - font-weight: 700 -} - -.cd-timeline__container { - position: relative; - padding: 1.25em 0; - padding: var(--space-md) 0 -} - -.cd-timeline__container::before { - content: ''; - position: absolute; - top: 0; - left: 18px; - height: 100%; - width: 4px; - background: hsl(205, 38%, 89%); - background: var(--cd-color-2) -} - -@media (min-width: 64rem) { - .cd-timeline__container::before { - left: 50%; - -webkit-transform: translateX(-50%); - -ms-transform: translateX(-50%); - transform: translateX(-50%) - } -} - -.cd-timeline__block { - display: -ms-flexbox; - display: flex; - position: relative; - z-index: 1; - margin-bottom: 2em; - margin-bottom: var(--space-lg) -} - -.cd-timeline__block:last-child { - margin-bottom: 0 -} - -@media (min-width: 64rem) { - .cd-timeline__block:nth-child(even) { - -ms-flex-direction: row-reverse; - flex-direction: row-reverse - } -} - -.cd-timeline__img { - display: -ms-flexbox; - display: flex; - -ms-flex-pack: center; - justify-content: center; - -ms-flex-align: center; - align-items: center; - -ms-flex-negative: 0; - flex-shrink: 0; - width: 30px; - height: 30px; - border-radius: 50%; - box-shadow: 0 0 0 4px hsl(0, 0%, 100%), inset 0 2px 0 rgba(0, 0, 0, 0.08), 0 3px 0 4px rgba(0, 0, 0, 0.05); - box-shadow: 0 0 0 4px var(--color-white), inset 0 2px 0 rgba(0, 0, 0, 0.08), 0 3px 0 4px rgba(0, 0, 0, 0.05) -} - -.cd-timeline__img i { - font-size: 1.5em; - color: white; -} - -@media (max-width: 64rem) { - .cd-timeline__img i { - font-size: 0.9em; - } -} - -.cd-timeline__img img { - width: 40px; - height: 40px; - margin-left: 2px; - margin-top: 2px; -} - -@media (max-width: 64rem) { - .cd-timeline__img img { - width: 20px; - height: 20px; - margin-left: 2px; - margin-top: 2px; - } -} - -@media (min-width: 64rem) { - .cd-timeline__img { - width: 60px; - height: 60px; - -ms-flex-order: 1; - order: 1; - margin-left: calc(5% - 30px); - will-change: transform; - } - - .cd-timeline__block:nth-child(even) .cd-timeline__img { - margin-right: calc(5% - 30px) - } -} - -.cd-timeline__img--picture { - background-color: #7289DA; -} - -.cd-timeline__img--movie { - background-color: hsl(356, 53%, 49%); - background-color: var(--cd-color-5) -} - -.cd-timeline__img--location { - background-color: hsl(47, 85%, 61%); - background-color: var(--cd-color-6) -} - -.cd-timeline__content { - -ms-flex-positive: 1; - flex-grow: 1; - position: relative; - margin-left: 1.25em; - margin-left: var(--space-md); - background: hsl(0, 0%, 100%); - background: var(--color-white); - border-radius: 0.25em; - border-radius: var(--radius-md); - padding: 1.25em; - padding: var(--space-md); - box-shadow: 0 3px 0 hsl(205, 38%, 89%); - box-shadow: 0 3px 0 var(--cd-color-2) -} - -.cd-timeline__content::before { - content: ''; - position: absolute; - top: 16px; - right: 100%; - width: 0; - height: 0; - border: 7px solid transparent; - border-right-color: hsl(0, 0%, 100%); - border-right-color: var(--color-white) -} - -.cd-timeline__content h2 { - color: hsl(206, 21%, 24%); - color: var(--cd-color-1) -} - -@media (min-width: 64rem) { - .cd-timeline__content { - width: 45%; - -ms-flex-positive: 0; - flex-grow: 0; - will-change: transform; - margin: 0; - font-size: 0.9em; - --line-height-multiplier: 1.2 - } - .cd-timeline__content::before { - top: 24px - } - .cd-timeline__block:nth-child(odd) .cd-timeline__content::before { - right: auto; - left: 100%; - width: 0; - height: 0; - border: 7px solid transparent; - border-left-color: hsl(0, 0%, 100%); - border-left-color: var(--color-white) - } -} - -.cd-timeline__date { - color: hsla(207, 10%, 55%, 0.7); - color: hsla(var(--cd-color-3-h), var(--cd-color-3-s), var(--cd-color-3-l), 0.7) -} - -@media (min-width: 64rem) { - .cd-timeline__date { - position: absolute; - width: 100%; - left: 120%; - top: 20px - } - .cd-timeline__block:nth-child(even) .cd-timeline__date { - left: auto; - right: 120%; - text-align: right - } -} - -@media (min-width: 64rem) { - .cd-timeline__img--hidden, .cd-timeline__content--hidden { - visibility: hidden - } - .cd-timeline__img--bounce-in { - -webkit-animation: cd-bounce-1 0.6s; - animation: cd-bounce-1 0.6s - } - .cd-timeline__content--bounce-in { - -webkit-animation: cd-bounce-2 0.6s; - animation: cd-bounce-2 0.6s - } - .cd-timeline__block:nth-child(even) .cd-timeline__content--bounce-in { - -webkit-animation-name: cd-bounce-2-inverse; - animation-name: cd-bounce-2-inverse - } - .cd-timeline__img--bounce-out { - -webkit-animation: cd-bounce-out-1 0.6s; - animation: cd-bounce-out-1 0.6s; - } - .cd-timeline__content--bounce-out { - -webkit-animation: cd-bounce-out-2 0.6s; - animation: cd-bounce-out-2 0.6s; - } -} - -@-webkit-keyframes cd-bounce-1 { - 0% { - opacity: 0; - -webkit-transform: scale(0.5); - transform: scale(0.5) - } - 60% { - opacity: 1; - -webkit-transform: scale(1.2); - transform: scale(1.2) - } - 100% { - -webkit-transform: scale(1); - transform: scale(1) - } -} - -@keyframes cd-bounce-1 { - 0% { - opacity: 0; - -webkit-transform: scale(0.5); - transform: scale(0.5) - } - 60% { - opacity: 1; - -webkit-transform: scale(1.2); - transform: scale(1.2) - } - 100% { - -webkit-transform: scale(1); - transform: scale(1) - } -} - -@-webkit-keyframes cd-bounce-2 { - 0% { - opacity: 0; - -webkit-transform: translateX(-100px); - transform: translateX(-100px) - } - 60% { - opacity: 1; - -webkit-transform: translateX(20px); - transform: translateX(20px) - } - 100% { - -webkit-transform: translateX(0); - transform: translateX(0) - } -} - -@keyframes cd-bounce-2 { - 0% { - opacity: 0; - -webkit-transform: translateX(-100px); - transform: translateX(-100px) - } - 60% { - opacity: 1; - -webkit-transform: translateX(20px); - transform: translateX(20px) - } - 100% { - -webkit-transform: translateX(0); - transform: translateX(0) - } -} - -@-webkit-keyframes cd-bounce-2-inverse { - 0% { - opacity: 0; - -webkit-transform: translateX(100px); - transform: translateX(100px) - } - 60% { - opacity: 1; - -webkit-transform: translateX(-20px); - transform: translateX(-20px) - } - 100% { - -webkit-transform: translateX(0); - transform: translateX(0) - } -} - -@keyframes cd-bounce-2-inverse { - 0% { - opacity: 0; - -webkit-transform: translateX(100px); - transform: translateX(100px) - } - 60% { - opacity: 1; - -webkit-transform: translateX(-20px); - transform: translateX(-20px) - } - 100% { - -webkit-transform: translateX(0); - transform: translateX(0) - } -} - -@-webkit-keyframes cd-bounce-out-1 { - 0% { - opacity: 1; - -webkit-transform: scale(1); - transform: scale(1) - } - - 60% { - -webkit-transform: scale(1.2); - transform: scale(1.2) - } - - 100% { - opacity: 0; - -webkit-transform: scale(0.5); - transform: scale(0.5) - } -} - -@keyframes cd-bounce-out-1 { - 0% { - opacity: 1; - -webkit-transform: scale(1); - transform: scale(1); - } - - 60% { - -webkit-transform: scale(1.2); - transform: scale(1.2); - } - - 100% { - opacity: 0; - -webkit-transform: scale(0.5); - transform: scale(0.5); - } -} - -@-webkit-keyframes cd-bounce-out-2 { - 0% { - opacity: 1; - -webkit-transform: translateX(0); - transform: translateX(0) - } - 60% { - -webkit-transform: translateX(20px); - transform: translateX(20px) - } - 100% { - opacity: 0; - -webkit-transform: translateX(-100px); - transform: translateX(-100px) - } -} - -@keyframes cd-bounce-out-2 { - 0% { - opacity: 1; - -webkit-transform: translateX(0); - transform: translateX(0) - } - 60% { - -webkit-transform: translateX(20px); - transform: translateX(20px) - } - 100% { - opacity: 0; - -webkit-transform: translateX(-100px); - transform: translateX(-100px) - } -} diff --git a/pydis_site/static/css/resources/resources.css b/pydis_site/static/css/resources/resources.css deleted file mode 100644 index 96d06111..00000000 --- a/pydis_site/static/css/resources/resources.css +++ /dev/null @@ -1,293 +0,0 @@ -/* Colors for icons */ -i.resource-icon.is-orangered { - color: #FE640A; -} -i.resource-icon.is-blurple { - color: #7289DA; -} -i.resource-icon.is-teal { - color: #95DBE5; -} -i.resource-icon.is-youtube-red { - color: #BB0000; -} -i.resource-icon.is-black { - color: #2c3334; -} - -/* Colors when icons are hovered */ -i.resource-icon.is-hoverable:hover { - filter: brightness(125%); -} -i.resource-icon.is-hoverable.is-black:hover { - filter: brightness(170%); -} -i.resource-icon.is-hoverable.is-teal:hover { - filter: brightness(80%); -} - -/* Icon padding */ -.breadcrumb-section { - padding: 1rem; -} -i.has-icon-padding { - padding: 0 10px 25px 0; -} -#tab-content p { - display: none; -} -#tab-content p.is-active { -display: block; -} - -/* Disable highlighting for all text in the filters. */ -.filter-checkbox, -.filter-panel label, -.card-header span { - user-select: none -} - -/* Remove pointless margin in panel header */ -#filter-panel-header { - margin-bottom: 0; -} - -/* Full width filter cards */ -#resource-filtering-panel .card .collapsible-content .card-content { - padding:0 -} - -/* Don't round the corners of the collapsibles */ -.filter-category-header { - border-radius: 0; -} - -/* Make the checkboxes indent under the filter headers */ -.filter-category-header .card-header .card-header-title { - padding-left: 0; -} -.filter-panel { - padding-left: 1.5rem; -} -.filter-checkbox { - margin-right: 0.25em !important; -} - -/* Style the search bar */ -#resource-search { - margin: 0.25em 0.25em 0 0.25em; -} - -/* Center the 404 div */ -.no-resources-found { - display: none; - flex-direction: column; - align-items: center; - margin-top: 1em; -} - -/* Make sure jQuery will use flex when setting `show()` again. */ -.no-resources-found[style*='display: block'] { - display: flex !important; -} - -/* By default, we hide the search tag. We'll add it only when there's a search happening. */ -.tag.search-query { - display: none; - min-width: fit-content; - max-width: fit-content; - padding-right: 2em; -} -.tag.search-query .inner { - display: inline-block; - padding: 0; - max-width: 16.5rem; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - line-height: 2em; -} -.tag.search-query i { - margin: 0 !important; - display: inline-block; - line-height: 2em; - float: left; - padding-right: 1em; -} - -/* Don't allow the tag pool to exceed its parent containers width. */ -#tag-pool { - max-width: 100%; -} - -/* Disable clicking on the checkbox itself. */ -/* Instead, we want to let the anchor tag handle clicks. */ -.filter-checkbox { - pointer-events: none; -} - -/* Blurple category icons */ -i.is-primary { - color: #7289da; -} - -/* A little space above the filter card, please! */ -.filter-tags { - padding-bottom: .5em; -} - -/* Style the close all filters button */ -.close-filters-button { - margin-left: auto; - display:none; -} -.close-filters-button a { - height: fit-content; - width: fit-content; - margin-right: 6px; -} -.close-filters-button a i { - color: #939bb3; -} -.close-filters-button a i:hover { - filter: brightness(115%); -} - -/* When hovering title anchors, just make the color a lighter primary, not black. */ -.resource-box a:hover { - filter: brightness(120%); - color: #7289DA; -} - -/* Set default display to inline-flex, for centering. */ -span.filter-box-tag { - display: none; - align-items: center; - cursor: pointer; - user-select: none; -} - -/* Make sure jQuery will use inline-flex when setting `show()` again. */ -span.filter-box-tag[style*='display: block'] { - display: inline-flex !important; -} - -/* Make resource tags clickable */ -.resource-tag { - cursor: pointer; - user-select: none; -} - -/* Give the resource tags a bit of breathing room */ -.resource-tag-container { - padding-left: 1.5rem; -} - -/* When hovering tags, brighten them a bit. */ -.resource-tag:hover, -.filter-box-tag:hover { - filter: brightness(95%); -} - -/* Move the x down 1 pixel to align center */ -button.delete { - margin-top: 1px; -} - -/* Colors for delete button x's */ -button.delete.is-primary::before, -button.delete.is-primary::after { - background-color: #2a45a2; -} -button.delete.is-success::before, -button.delete.is-success::after { - background-color: #2c9659; -} -button.delete.is-danger::before, -button.delete.is-danger::after { - background-color: #c32841; -} -button.delete.is-info::before, -button.delete.is-info::after { - background-color: #237fbd; -} - -/* Give outlines to active tags */ -span.filter-box-tag, -span.resource-tag.active, -.tag.search-query { - outline-width: 1px; - outline-style: solid; -} - -/* Disable transitions */ -.no-transition { - -webkit-transition: none !important; - -moz-transition: none !important; - -o-transition: none !important; - transition: none !important; -} - -/* Make filter tags sparkle when selected! */ -@keyframes glow_success { - from { box-shadow: 0 0 2px 2px #aef4af; } - 33% { box-shadow: 0 0 2px 2px #87af7a; } - 66% { box-shadow: 0 0 2px 2px #9ceaac; } - to { box-shadow: 0 0 2px 2px #7cbf64; } -} - -@keyframes glow_primary { - from { box-shadow: 0 0 2px 2px #aeb8f3; } - 33% { box-shadow: 0 0 2px 2px #909ed9; } - 66% { box-shadow: 0 0 2px 2px #6d7ed4; } - to { box-shadow: 0 0 2px 2px #6383b3; } -} - -@keyframes glow_danger { - from { box-shadow: 0 0 2px 2px #c9495f; } - 33% { box-shadow: 0 0 2px 2px #92486f; } - 66% { box-shadow: 0 0 2px 2px #d455ba; } - to { box-shadow: 0 0 2px 2px #ff8192; } -} -@keyframes glow_info { - from { box-shadow: 0 0 2px 2px #4592c9; } - 33% { box-shadow: 0 0 2px 2px #6196bb; } - 66% { box-shadow: 0 0 2px 2px #5adade; } - to { box-shadow: 0 0 2px 2px #6bcfdc; } -} - -span.resource-tag.active.is-primary { - animation: glow_primary 4s infinite alternate; -} -span.resource-tag.active.has-background-danger-light { - animation: glow_danger 4s infinite alternate; -} -span.resource-tag.active.has-background-success-light { - animation: glow_success 4s infinite alternate; -} -span.resource-tag.active.has-background-info-light { - animation: glow_info 4s infinite alternate; -} - -/* Smaller filter category headers when on mobile */ -@media screen and (max-width: 480px) { - .filter-category-header .card-header .card-header-title { - font-size: 14px; - padding: 0; - } - .filter-panel { - padding-top: 4px; - padding-bottom: 4px; - } - .tag.search-query .inner { - max-width: 16.2rem; - } -} - -/* Constrain the width of the filterbox */ -@media screen and (min-width: 769px) { - .filtering-column { - max-width: 25rem; - min-width: 18rem; - } -} diff --git a/pydis_site/static/css/staff/logs.css b/pydis_site/static/css/staff/logs.css deleted file mode 100644 index 56a12380..00000000 --- a/pydis_site/static/css/staff/logs.css +++ /dev/null @@ -1,258 +0,0 @@ -main.site-content { - background-color: hsl(220, 8%, 23%); - color: #dcddde; - font-size: 0.9375rem; - font-weight: 400; - line-height: 1.3; - letter-spacing: 0; - text-rendering: optimizeLegibility; - padding: 1rem; - font-family: Whitney, Helvetica Neue, Helvetica, Arial, Lucida Grande, sans-serif; -} - -.has-small-margin { - margin: 1rem 0; -} - -.deleted-header { - font-weight: 700; - margin-top: 1rem; -} - -.discord-message { - margin-bottom: 15px; -} - -.discord-message:first-child { - border-top: 1px; -} - -.discord-message-content { - overflow-wrap: break-word; -} - -.discord-message-header { - white-space: nowrap; - letter-spacing: 0; -} - -.discord-username { - font-size: 1rem; - font-weight: 600; -} - -.discord-message-metadata { - font-size: 0.75rem; - font-weight: 400; - margin: 0 .3rem; -} - -.discord-channel { - font-size: 0.9rem; - font-weight: 500; - color: #dcddde; -} - -.discord-id { - color: #dcddde; - font-weight: 300; - margin-left: 0.3rem; -} - -.discord-embed { - - position: relative; - margin-top: 5px; - max-width: 520px; - display: flex; -} - -.discord-embed a { - text-decoration: none; - color: hsl(197, 100%, 41%); -} - -.discord-embed a:hover { - text-decoration: underline; - color: hsl(197, 100%, 41%); -} - -.discord-embed-color { - width: 4px; - border-radius: 3px 0 0 3px; - flex-shrink: 0; -} - -.discord-embed-inner { - background-color: #34363b; - padding: 8px 10px; - border-radius: 0 3px 3px 0; - box-sizing: border-box; - border: 1px solid hsla(225, 8%, 20%, 0.6); - display: flex; - flex-direction: column; -} - -.discord-embed-content { - width: 100%; - display: flex; -} - -.discord-embed-main { - flex: 1; -} - -.discord-embed-thumbnail > img { - max-width: 80px; - max-height: 80px; - border-radius: 3px; - width: auto; - object-fit: contain; - margin-left: 20px; - flex-shrink: 0; - border-style: none; -} - -.discord-embed-author { - display: flex; - align-items: center; - margin-bottom: 5px; - font-weight: 600; - font-size: 14px; - line-height: 1.15; -} - -.discord-embed-author-icon { - margin-right: 9px; - width: 20px; - height: 20px; - object-fit: contain; - border-radius: 50%; -} - -.discord-embed-author a { - color: white; -} - -.discord-embed-author a:hover { - color: white; -} - -.discord-embed-title { - margin-bottom: 5px; - font-size: 14px; - display: inline-block; - font-weight: 600; -} - -.discord-embed-description { - margin-bottom: 10px; -} - -.discord-embed-fields { - display: flex; - flex-direction: row; - flex-wrap: wrap; - margin-top: -10px; - margin-bottom: 10px; -} - -.discord-embed-field { - flex: 0; - padding-top: 5px; - min-width: 100%; - max-width: 506px; -} - -.discord-embed-field-name { - margin-bottom: 4px; - font-weight: 600; -} - -.discord-embed-field-value { - font-weight: 500; -} - -.discord-embed-field-inline { - flex: 1; - min-width: 150px; - flex-basis: auto; -} - -.discord-embed-main > :last-child { - margin-bottom: 0 !important; -} - -.discord-embed-image { - position: relative; - display: inline-block; - margin-bottom: 10px; -} - -.discord-embed-image > img { - margin: 0; - vertical-align: bottom; - max-width: 300px; - display: flex; - overflow: hidden; - border-radius: 2px; -} - -.discord-embed-footer-text { - font-size: .70rem !important; - letter-spacing: 0; - display: inline-block; -} - -.discord-embed-footer-icon { - margin-right: 10px; - height: 18px; - width: 18px; - object-fit: contain; - float: left; - border-radius: 50%; -} - -.discord-embed-content { - margin-bottom: 10px; -} - -.discord-embed-inner > :last-child { - margin-bottom: 0 !important; -} - -/* Discord Font definitions */ -@font-face { - font-family: Whitney; - font-style: light; - font-weight: 300; - src: url(https://discordapp.com/assets/6c6374bad0b0b6d204d8d6dc4a18d820.woff) format("woff") -} - -@font-face { - font-family: Whitney; - font-style: normal; - font-weight: 500; - src: url(https://discordapp.com/assets/e8acd7d9bf6207f99350ca9f9e23b168.woff) format("woff") -} - -@font-face { - font-family: Whitney; - font-style: medium; - font-weight: 600; - src: url(https://discordapp.com/assets/3bdef1251a424500c1b3a78dea9b7e57.woff) format("woff") -} - -@font-face { - font-family: WhitneyMedium; - font-style: medium; - font-weight: 600; - src: url(https://discordapp.com/assets/be0060dafb7a0e31d2a1ca17c0708636.woff) format("woff") -} - -@font-face { - font-family: Whitney; - font-style: bold; - font-weight: 700; - src: url(https://discordapp.com/assets/8e12fb4f14d9c4592eb8ec9f22337b04.woff) format("woff") -} diff --git a/pydis_site/static/favicons/android-chrome-192x192.png b/pydis_site/static/favicons/android-chrome-192x192.png Binary files differdeleted file mode 100644 index 5b8e4dee..00000000 --- a/pydis_site/static/favicons/android-chrome-192x192.png +++ /dev/null diff --git a/pydis_site/static/favicons/android-chrome-512x512.png b/pydis_site/static/favicons/android-chrome-512x512.png Binary files differdeleted file mode 100644 index 260d57e2..00000000 --- a/pydis_site/static/favicons/android-chrome-512x512.png +++ /dev/null diff --git a/pydis_site/static/favicons/apple-touch-icon.png b/pydis_site/static/favicons/apple-touch-icon.png Binary files differdeleted file mode 100644 index bcee3b3b..00000000 --- a/pydis_site/static/favicons/apple-touch-icon.png +++ /dev/null diff --git a/pydis_site/static/favicons/browserconfig.xml b/pydis_site/static/favicons/browserconfig.xml deleted file mode 100644 index c0529c4a..00000000 --- a/pydis_site/static/favicons/browserconfig.xml +++ /dev/null @@ -1,9 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<browserconfig> - <msapplication> - <tile> - <square150x150logo src="/static/favicons/mstile-150x150.png?v=9B9vkE5q7O"/> - <TileColor>#7289da</TileColor> - </tile> - </msapplication> -</browserconfig> diff --git a/pydis_site/static/favicons/favicon-16x16.png b/pydis_site/static/favicons/favicon-16x16.png Binary files differdeleted file mode 100644 index 97d9d7e6..00000000 --- a/pydis_site/static/favicons/favicon-16x16.png +++ /dev/null diff --git a/pydis_site/static/favicons/favicon-32x32.png b/pydis_site/static/favicons/favicon-32x32.png Binary files differdeleted file mode 100644 index 9acca4ba..00000000 --- a/pydis_site/static/favicons/favicon-32x32.png +++ /dev/null diff --git a/pydis_site/static/favicons/favicon.ico b/pydis_site/static/favicons/favicon.ico Binary files differdeleted file mode 100644 index 6f2da14f..00000000 --- a/pydis_site/static/favicons/favicon.ico +++ /dev/null diff --git a/pydis_site/static/favicons/mstile-144x144.png b/pydis_site/static/favicons/mstile-144x144.png Binary files differdeleted file mode 100644 index 734c4d14..00000000 --- a/pydis_site/static/favicons/mstile-144x144.png +++ /dev/null diff --git a/pydis_site/static/favicons/mstile-150x150.png b/pydis_site/static/favicons/mstile-150x150.png Binary files differdeleted file mode 100644 index c0f2947d..00000000 --- a/pydis_site/static/favicons/mstile-150x150.png +++ /dev/null diff --git a/pydis_site/static/favicons/mstile-310x150.png b/pydis_site/static/favicons/mstile-310x150.png Binary files differdeleted file mode 100644 index 6806f404..00000000 --- a/pydis_site/static/favicons/mstile-310x150.png +++ /dev/null diff --git a/pydis_site/static/favicons/mstile-310x310.png b/pydis_site/static/favicons/mstile-310x310.png Binary files differdeleted file mode 100644 index 1be6d145..00000000 --- a/pydis_site/static/favicons/mstile-310x310.png +++ /dev/null diff --git a/pydis_site/static/favicons/mstile-70x70.png b/pydis_site/static/favicons/mstile-70x70.png Binary files differdeleted file mode 100644 index 1916fac6..00000000 --- a/pydis_site/static/favicons/mstile-70x70.png +++ /dev/null diff --git a/pydis_site/static/favicons/safari-pinned-tab.svg b/pydis_site/static/favicons/safari-pinned-tab.svg deleted file mode 100644 index 32064879..00000000 --- a/pydis_site/static/favicons/safari-pinned-tab.svg +++ /dev/null @@ -1 +0,0 @@ -<svg version="1" xmlns="http://www.w3.org/2000/svg" width="933.333" height="933.333" viewBox="0 0 700.000000 700.000000"><path d="M321.8.5c-.1.1-5.1.6-10.9.9-5.8.4-11.5.8-12.5 1-1 .2-5.5.7-9.9 1.1-4.4.4-8.9.9-10 1-31.2 5.2-44.4 8.9-60.5 16.7-13.5 6.7-22.9 15.5-28.2 26.8-3 6.3-5.4 13.2-4.8 14.2.1.2-.3 1.5-.8 2.9-1.8 4.6-2.5 19.7-2.6 56.1l-.1 35.8 3 .1c1.6 0 39.3.1 83.8.1H349l.1 2.9c0 1.6.1 5.8.1 9.4 0 3.6-.1 7.1-.1 7.8-.1 1-24.3 1.2-118.3 1.3-65.1 0-121.4.3-125.3.7-22.8 2.2-45.8 13.2-62.1 29.5-18.7 18.8-28.9 42.9-36 84.7-1.9 11.8-2.4 14.6-3 19.5-.3 3-.8 7.1-1.1 9-.9 6.9-1 40.7-.2 49 1.6 16.2 4.8 36.3 7.4 47 .8 3 1.6 6.6 1.8 8 2 10.6 7.7 28.1 12.8 38.8C38 492 56 508 81.8 515.2c7.2 2 10.1 2.2 43.2 2.2l35.5.1.2-42.5c.1-23.4.4-44.1.7-46 5.8-39.1 34.7-73.8 71.6-85.8 16.7-5.4 16.2-5.4 119-5.7 50.9-.1 94.1-.6 96-1 7.7-1.5 20.6-6.6 28.1-11.1 18.5-11.2 31.9-29.6 38.1-52.1 2.1-7.7 2.1-9.2 2.3-97.5.2-93.5.1-95.2-4.1-107.6-7.7-22.9-29.1-44.1-54.7-54.4-6.8-2.7-17.3-5.8-21.7-6.3-1.9-.3-5.7-.9-8.5-1.5-2.7-.5-7.2-1.2-10-1.6-2.7-.3-6.1-.7-7.5-.9-1.4-.2-6.1-.7-10.5-1.1-4.4-.4-9.3-.9-11-1.1-3.6-.5-66.2-1.3-66.7-.8zm-59.1 51.9c6.3 1.7 10.7 4.3 14.9 8.8 9.2 9.8 11.4 23.5 5.8 35.5-3.1 6.5-8 11.4-14.9 15.1-4.2 2.2-6.3 2.7-13 2.7s-8.9-.4-13.5-2.7c-9.4-4.6-14.9-11.1-17.3-20.8-4.4-17.1 6.4-34.9 23.7-39.1 4.9-1.1 8.7-1 14.3.5z"/><path d="M538.6 178.9c-.3.4-.6 19.5-.7 42.3-.1 36.1-.4 42.7-2 50.4-3.3 16.1-8.7 28.6-17.7 41.3-6.9 9.7-11.8 14.9-21.2 22.2-4.1 3.3-8 6.3-8.6 6.8-.6.5-5.8 3.2-11.5 5.9-10.7 5.2-21.3 8.5-30.9 9.8-3 .4-46.9.8-97.5.9-50.6.1-93.6.5-95.5.9-22.7 4.5-40 15.1-53.5 32.6-7.9 10.3-14.8 25.5-16 35.7-.4 2.6-.9 5.6-1.1 6.6-1.1 3.7-.5 174.1.6 180.2 1.4 7.7 1.2 7 3.9 14 10.2 27 40.7 48.6 85.7 60.6 12.3 3.3 12.2 3.2 23.4 5.5 25.9 5.3 63.4 6.6 89 3 29.7-4.1 58.5-12.3 79.5-22.6 29.8-14.5 46.8-34.3 51.3-59.5.4-2.2.7-20.4.7-40.5v-36.4l-83.5-.1h-83.5v-9.8c0-5.7.4-10.1 1-10.5.5-.4 59-.7 130-.8 71 0 131-.4 133.4-.8 20.3-3.4 35.2-13.4 47.8-32.4 5.8-8.6 14.4-26.8 18.8-39.5 1.3-4 3.2-9.2 4.1-11.7 4.9-13.7 10.4-40.1 12-57.5.8-9 .8-34.1 0-42.5-1.3-14.1-1.8-17.9-2.6-23-.5-3-1.1-7.1-1.4-9-.8-5.2-4.3-22.3-6.6-32-1.1-4.7-2.3-9.4-2.5-10.5-3.1-13.5-13.8-38-20.9-47.8-6.8-9.6-19.3-21.7-22.4-21.7-.6 0-1.2-.4-1.4-.8-.7-1.9-13.6-6.7-23.3-8.8-4.3-.9-76.4-1.4-76.9-.5zm-81.2 405.7c6.8 3.3 12.5 9.3 15.4 16.4 3.2 7.7 2.6 19.6-1.3 26.9-5.8 10.6-15.5 16.4-27.5 16.5-9 .1-16.7-3-22.5-9.1-9.1-9.3-11.6-24-6.3-35.5 7.5-16.2 26.3-23 42.2-15.2z"/></svg> diff --git a/pydis_site/static/favicons/site.webmanifest b/pydis_site/static/favicons/site.webmanifest deleted file mode 100644 index 1c59aa63..00000000 --- a/pydis_site/static/favicons/site.webmanifest +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "Python Discord", - "short_name": "Python Discord", - "icons": [ - { - "src": "/static/favicons/android-chrome-192x192.png?v=9B9vkE5q7O", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/static/favicons/android-chrome-512x512.png?v=9B9vkE5q7O", - "sizes": "512x512", - "type": "image/png" - } - ], - "theme_color": "#7289da", - "background_color": "#7289da", - "display": "standalone" -} diff --git a/pydis_site/static/images/content/contributing/docker_settings.png b/pydis_site/static/images/content/contributing/docker_settings.png Binary files differdeleted file mode 100644 index 03beeebf..00000000 --- a/pydis_site/static/images/content/contributing/docker_settings.png +++ /dev/null diff --git a/pydis_site/static/images/content/contributing/docker_shared_drives.png b/pydis_site/static/images/content/contributing/docker_shared_drives.png Binary files differdeleted file mode 100644 index c86bbdba..00000000 --- a/pydis_site/static/images/content/contributing/docker_shared_drives.png +++ /dev/null diff --git a/pydis_site/static/images/content/contributing/fork_button.png b/pydis_site/static/images/content/contributing/fork_button.png Binary files differdeleted file mode 100644 index 4eab9e0d..00000000 --- a/pydis_site/static/images/content/contributing/fork_button.png +++ /dev/null diff --git a/pydis_site/static/images/content/contributing/fork_clone.png b/pydis_site/static/images/content/contributing/fork_clone.png Binary files differdeleted file mode 100644 index ad966ee3..00000000 --- a/pydis_site/static/images/content/contributing/fork_clone.png +++ /dev/null diff --git a/pydis_site/static/images/content/contributing/fork_user.png b/pydis_site/static/images/content/contributing/fork_user.png Binary files differdeleted file mode 100644 index 513f8841..00000000 --- a/pydis_site/static/images/content/contributing/fork_user.png +++ /dev/null diff --git a/pydis_site/static/images/content/contributing/github_issues_tab.png b/pydis_site/static/images/content/contributing/github_issues_tab.png Binary files differdeleted file mode 100644 index 1d1fc617..00000000 --- a/pydis_site/static/images/content/contributing/github_issues_tab.png +++ /dev/null diff --git a/pydis_site/static/images/content/contributing/github_new_issue.png b/pydis_site/static/images/content/contributing/github_new_issue.png Binary files differdeleted file mode 100644 index 1d661428..00000000 --- a/pydis_site/static/images/content/contributing/github_new_issue.png +++ /dev/null diff --git a/pydis_site/static/images/content/contributing/github_sample_issue.png b/pydis_site/static/images/content/contributing/github_sample_issue.png Binary files differdeleted file mode 100644 index a8c2ac0a..00000000 --- a/pydis_site/static/images/content/contributing/github_sample_issue.png +++ /dev/null diff --git a/pydis_site/static/images/content/contributing/pull_request.png b/pydis_site/static/images/content/contributing/pull_request.png Binary files differdeleted file mode 100644 index 87b7ffbe..00000000 --- a/pydis_site/static/images/content/contributing/pull_request.png +++ /dev/null diff --git a/pydis_site/static/images/content/contributing/pycharm_branch.png b/pydis_site/static/images/content/contributing/pycharm_branch.png Binary files differdeleted file mode 100644 index d9cde97a..00000000 --- a/pydis_site/static/images/content/contributing/pycharm_branch.png +++ /dev/null diff --git a/pydis_site/static/images/content/contributing/pycharm_checkout.png b/pydis_site/static/images/content/contributing/pycharm_checkout.png Binary files differdeleted file mode 100644 index f719dfb7..00000000 --- a/pydis_site/static/images/content/contributing/pycharm_checkout.png +++ /dev/null diff --git a/pydis_site/static/images/content/contributing/pycharm_commit.png b/pydis_site/static/images/content/contributing/pycharm_commit.png Binary files differdeleted file mode 100644 index 03223688..00000000 --- a/pydis_site/static/images/content/contributing/pycharm_commit.png +++ /dev/null diff --git a/pydis_site/static/images/content/contributing/pycharm_commit_button.png b/pydis_site/static/images/content/contributing/pycharm_commit_button.png Binary files differdeleted file mode 100644 index ae2745b0..00000000 --- a/pydis_site/static/images/content/contributing/pycharm_commit_button.png +++ /dev/null diff --git a/pydis_site/static/images/content/contributing/pycharm_create_project.png b/pydis_site/static/images/content/contributing/pycharm_create_project.png Binary files differdeleted file mode 100644 index 63cd5626..00000000 --- a/pydis_site/static/images/content/contributing/pycharm_create_project.png +++ /dev/null diff --git a/pydis_site/static/images/content/contributing/pycharm_interpreter.png b/pydis_site/static/images/content/contributing/pycharm_interpreter.png Binary files differdeleted file mode 100644 index 66a0dc7f..00000000 --- a/pydis_site/static/images/content/contributing/pycharm_interpreter.png +++ /dev/null diff --git a/pydis_site/static/images/content/contributing/pycharm_poetry.png b/pydis_site/static/images/content/contributing/pycharm_poetry.png Binary files differdeleted file mode 100644 index cb5402b0..00000000 --- a/pydis_site/static/images/content/contributing/pycharm_poetry.png +++ /dev/null diff --git a/pydis_site/static/images/content/contributing/pycharm_poetry_success.png b/pydis_site/static/images/content/contributing/pycharm_poetry_success.png Binary files differdeleted file mode 100644 index 31d44a69..00000000 --- a/pydis_site/static/images/content/contributing/pycharm_poetry_success.png +++ /dev/null diff --git a/pydis_site/static/images/content/contributing/pycharm_pull.png b/pydis_site/static/images/content/contributing/pycharm_pull.png Binary files differdeleted file mode 100644 index 0640564e..00000000 --- a/pydis_site/static/images/content/contributing/pycharm_pull.png +++ /dev/null diff --git a/pydis_site/static/images/content/contributing/pycharm_push.png b/pydis_site/static/images/content/contributing/pycharm_push.png Binary files differdeleted file mode 100644 index 1b51c085..00000000 --- a/pydis_site/static/images/content/contributing/pycharm_push.png +++ /dev/null diff --git a/pydis_site/static/images/content/contributing/pycharm_remotes.png b/pydis_site/static/images/content/contributing/pycharm_remotes.png Binary files differdeleted file mode 100644 index 3fe565c8..00000000 --- a/pydis_site/static/images/content/contributing/pycharm_remotes.png +++ /dev/null diff --git a/pydis_site/static/images/content/contributing/pycharm_run_module.png b/pydis_site/static/images/content/contributing/pycharm_run_module.png Binary files differdeleted file mode 100644 index c5030519..00000000 --- a/pydis_site/static/images/content/contributing/pycharm_run_module.png +++ /dev/null diff --git a/pydis_site/static/images/content/contributing/pycharm_upstream.png b/pydis_site/static/images/content/contributing/pycharm_upstream.png Binary files differdeleted file mode 100644 index 2ccb5827..00000000 --- a/pydis_site/static/images/content/contributing/pycharm_upstream.png +++ /dev/null diff --git a/pydis_site/static/images/content/discord_colored_messages/ansi-colors.png b/pydis_site/static/images/content/discord_colored_messages/ansi-colors.png Binary files differdeleted file mode 100644 index d980ab4c..00000000 --- a/pydis_site/static/images/content/discord_colored_messages/ansi-colors.png +++ /dev/null diff --git a/pydis_site/static/images/content/discord_colored_messages/result.png b/pydis_site/static/images/content/discord_colored_messages/result.png Binary files differdeleted file mode 100644 index 41ed555c..00000000 --- a/pydis_site/static/images/content/discord_colored_messages/result.png +++ /dev/null diff --git a/pydis_site/static/images/content/discordpy_embed.png b/pydis_site/static/images/content/discordpy_embed.png Binary files differdeleted file mode 100644 index 9ed911a6..00000000 --- a/pydis_site/static/images/content/discordpy_embed.png +++ /dev/null diff --git a/pydis_site/static/images/content/fix-ssl-certificate/pem.png b/pydis_site/static/images/content/fix-ssl-certificate/pem.png Binary files differdeleted file mode 100644 index face520f..00000000 --- a/pydis_site/static/images/content/fix-ssl-certificate/pem.png +++ /dev/null diff --git a/pydis_site/static/images/content/help_channels/dormant_message.png b/pydis_site/static/images/content/help_channels/dormant_message.png Binary files differdeleted file mode 100644 index ac806459..00000000 --- a/pydis_site/static/images/content/help_channels/dormant_message.png +++ /dev/null diff --git a/pydis_site/static/images/content/help_channels/help-system-category.png b/pydis_site/static/images/content/help_channels/help-system-category.png Binary files differdeleted file mode 100644 index bea5a92c..00000000 --- a/pydis_site/static/images/content/help_channels/help-system-category.png +++ /dev/null diff --git a/pydis_site/static/images/content/help_channels/new-post-button.png b/pydis_site/static/images/content/help_channels/new-post-button.png Binary files differdeleted file mode 100644 index 4ceabf0f..00000000 --- a/pydis_site/static/images/content/help_channels/new-post-button.png +++ /dev/null diff --git a/pydis_site/static/images/content/help_channels/new-post-form.png b/pydis_site/static/images/content/help_channels/new-post-form.png Binary files differdeleted file mode 100644 index 3e90bf7d..00000000 --- a/pydis_site/static/images/content/help_channels/new-post-form.png +++ /dev/null diff --git a/pydis_site/static/images/content/help_channels/newly-created-thread-example.png b/pydis_site/static/images/content/help_channels/newly-created-thread-example.png Binary files differdeleted file mode 100644 index d7b1eed4..00000000 --- a/pydis_site/static/images/content/help_channels/newly-created-thread-example.png +++ /dev/null diff --git a/pydis_site/static/images/content/help_channels/question-example.png b/pydis_site/static/images/content/help_channels/question-example.png Binary files differdeleted file mode 100644 index da181351..00000000 --- a/pydis_site/static/images/content/help_channels/question-example.png +++ /dev/null diff --git a/pydis_site/static/images/content/help_channels/topical_channels.png b/pydis_site/static/images/content/help_channels/topical_channels.png Binary files differdeleted file mode 100644 index 43530cbe..00000000 --- a/pydis_site/static/images/content/help_channels/topical_channels.png +++ /dev/null diff --git a/pydis_site/static/images/content/mutability/hello_gets_eaten.png b/pydis_site/static/images/content/mutability/hello_gets_eaten.png Binary files differdeleted file mode 100644 index 08e9f2cb..00000000 --- a/pydis_site/static/images/content/mutability/hello_gets_eaten.png +++ /dev/null diff --git a/pydis_site/static/images/content/mutability/s_gets_assigned_to_HELLO.png b/pydis_site/static/images/content/mutability/s_gets_assigned_to_HELLO.png Binary files differdeleted file mode 100644 index 5f2baf0d..00000000 --- a/pydis_site/static/images/content/mutability/s_gets_assigned_to_HELLO.png +++ /dev/null diff --git a/pydis_site/static/images/content/mutability/s_refers_hello.png b/pydis_site/static/images/content/mutability/s_refers_hello.png Binary files differdeleted file mode 100644 index 59d84211..00000000 --- a/pydis_site/static/images/content/mutability/s_refers_hello.png +++ /dev/null diff --git a/pydis_site/static/images/content/mutability/s_upper_creates_HELLO.png b/pydis_site/static/images/content/mutability/s_upper_creates_HELLO.png Binary files differdeleted file mode 100644 index 6aa1e8d3..00000000 --- a/pydis_site/static/images/content/mutability/s_upper_creates_HELLO.png +++ /dev/null diff --git a/pydis_site/static/images/content/regenerating_token.jpg b/pydis_site/static/images/content/regenerating_token.jpg Binary files differdeleted file mode 100644 index 7b2588dc..00000000 --- a/pydis_site/static/images/content/regenerating_token.jpg +++ /dev/null diff --git a/pydis_site/static/images/events/100k.png b/pydis_site/static/images/events/100k.png Binary files differdeleted file mode 100644 index ae024d77..00000000 --- a/pydis_site/static/images/events/100k.png +++ /dev/null diff --git a/pydis_site/static/images/events/DO_Logo_Vertical_Blue.png b/pydis_site/static/images/events/DO_Logo_Vertical_Blue.png Binary files differdeleted file mode 100644 index ad528652..00000000 --- a/pydis_site/static/images/events/DO_Logo_Vertical_Blue.png +++ /dev/null diff --git a/pydis_site/static/images/events/Replit.png b/pydis_site/static/images/events/Replit.png Binary files differdeleted file mode 100644 index a8202641..00000000 --- a/pydis_site/static/images/events/Replit.png +++ /dev/null diff --git a/pydis_site/static/images/events/Tabnine.png b/pydis_site/static/images/events/Tabnine.png Binary files differdeleted file mode 100644 index eee42a5e..00000000 --- a/pydis_site/static/images/events/Tabnine.png +++ /dev/null diff --git a/pydis_site/static/images/events/summer_code_jam_2020.png b/pydis_site/static/images/events/summer_code_jam_2020.png Binary files differdeleted file mode 100644 index 63c311b0..00000000 --- a/pydis_site/static/images/events/summer_code_jam_2020.png +++ /dev/null diff --git a/pydis_site/static/images/events/summer_code_jam_2021/banner.png b/pydis_site/static/images/events/summer_code_jam_2021/banner.png Binary files differdeleted file mode 100644 index 778c7c90..00000000 --- a/pydis_site/static/images/events/summer_code_jam_2021/banner.png +++ /dev/null diff --git a/pydis_site/static/images/events/summer_code_jam_2021/cj8_asciimatics.png b/pydis_site/static/images/events/summer_code_jam_2021/cj8_asciimatics.png Binary files differdeleted file mode 100644 index ac52338e..00000000 --- a/pydis_site/static/images/events/summer_code_jam_2021/cj8_asciimatics.png +++ /dev/null diff --git a/pydis_site/static/images/events/summer_code_jam_2021/cj8_blessed.gif b/pydis_site/static/images/events/summer_code_jam_2021/cj8_blessed.gif Binary files differdeleted file mode 100644 index 8bdbf5b1..00000000 --- a/pydis_site/static/images/events/summer_code_jam_2021/cj8_blessed.gif +++ /dev/null diff --git a/pydis_site/static/images/events/summer_code_jam_2021/cj8_curses.png b/pydis_site/static/images/events/summer_code_jam_2021/cj8_curses.png Binary files differdeleted file mode 100644 index c1a177ff..00000000 --- a/pydis_site/static/images/events/summer_code_jam_2021/cj8_curses.png +++ /dev/null diff --git a/pydis_site/static/images/events/summer_code_jam_2021/cj8_prompttoolkit.png b/pydis_site/static/images/events/summer_code_jam_2021/cj8_prompttoolkit.png Binary files differdeleted file mode 100644 index a359a7af..00000000 --- a/pydis_site/static/images/events/summer_code_jam_2021/cj8_prompttoolkit.png +++ /dev/null diff --git a/pydis_site/static/images/events/summer_code_jam_2021/cj8_rich.gif b/pydis_site/static/images/events/summer_code_jam_2021/cj8_rich.gif Binary files differdeleted file mode 100644 index 3a0ffbf3..00000000 --- a/pydis_site/static/images/events/summer_code_jam_2021/cj8_rich.gif +++ /dev/null diff --git a/pydis_site/static/images/events/summer_code_jam_2021/cj8_urwid.png b/pydis_site/static/images/events/summer_code_jam_2021/cj8_urwid.png Binary files differdeleted file mode 100644 index 98a264b7..00000000 --- a/pydis_site/static/images/events/summer_code_jam_2021/cj8_urwid.png +++ /dev/null diff --git a/pydis_site/static/images/events/summer_code_jam_2021/front_page_banners/currently_live.png b/pydis_site/static/images/events/summer_code_jam_2021/front_page_banners/currently_live.png Binary files differdeleted file mode 100644 index 939aca2a..00000000 --- a/pydis_site/static/images/events/summer_code_jam_2021/front_page_banners/currently_live.png +++ /dev/null diff --git a/pydis_site/static/images/events/summer_code_jam_2021/front_page_banners/random_team.png b/pydis_site/static/images/events/summer_code_jam_2021/front_page_banners/random_team.png Binary files differdeleted file mode 100644 index ea48ef4b..00000000 --- a/pydis_site/static/images/events/summer_code_jam_2021/front_page_banners/random_team.png +++ /dev/null diff --git a/pydis_site/static/images/events/summer_code_jam_2021/front_page_banners/sign_up_now.png b/pydis_site/static/images/events/summer_code_jam_2021/front_page_banners/sign_up_now.png Binary files differdeleted file mode 100644 index 7258b1b9..00000000 --- a/pydis_site/static/images/events/summer_code_jam_2021/front_page_banners/sign_up_now.png +++ /dev/null diff --git a/pydis_site/static/images/events/summer_code_jam_2022/front_page_banners/live_now.png b/pydis_site/static/images/events/summer_code_jam_2022/front_page_banners/live_now.png Binary files differdeleted file mode 100644 index eb30bf7e..00000000 --- a/pydis_site/static/images/events/summer_code_jam_2022/front_page_banners/live_now.png +++ /dev/null diff --git a/pydis_site/static/images/events/summer_code_jam_2022/front_page_banners/qualifier_release.png b/pydis_site/static/images/events/summer_code_jam_2022/front_page_banners/qualifier_release.png Binary files differdeleted file mode 100644 index 1e45024b..00000000 --- a/pydis_site/static/images/events/summer_code_jam_2022/front_page_banners/qualifier_release.png +++ /dev/null diff --git a/pydis_site/static/images/events/summer_code_jam_2022/front_page_banners/sign_up.png b/pydis_site/static/images/events/summer_code_jam_2022/front_page_banners/sign_up.png Binary files differdeleted file mode 100644 index f807418e..00000000 --- a/pydis_site/static/images/events/summer_code_jam_2022/front_page_banners/sign_up.png +++ /dev/null diff --git a/pydis_site/static/images/events/summer_code_jam_2022/site_banner.png b/pydis_site/static/images/events/summer_code_jam_2022/site_banner.png Binary files differdeleted file mode 100644 index 30b3dfbc..00000000 --- a/pydis_site/static/images/events/summer_code_jam_2022/site_banner.png +++ /dev/null diff --git a/pydis_site/static/images/frontpage/welcome.jpg b/pydis_site/static/images/frontpage/welcome.jpg Binary files differdeleted file mode 100644 index 0eb8f672..00000000 --- a/pydis_site/static/images/frontpage/welcome.jpg +++ /dev/null diff --git a/pydis_site/static/images/navbar/discord.svg b/pydis_site/static/images/navbar/discord.svg deleted file mode 100644 index 2cf3d6cc..00000000 --- a/pydis_site/static/images/navbar/discord.svg +++ /dev/null @@ -1,244 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg - width="120mm" - height="30mm" - viewBox="0 0 120 30" - version="1.1" - id="svg8" - inkscape:version="1.2 (dc2aedaf03, 2022-05-15)" - sodipodi:docname="discord.svg" - xml:space="preserve" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns="http://www.w3.org/2000/svg" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:dc="http://purl.org/dc/elements/1.1/"><defs - id="defs2"><rect - x="75.819944" - y="98.265513" - width="25.123336" - height="7.8844509" - id="rect953" /><rect - x="75.819946" - y="98.265511" - width="25.123337" - height="7.8844509" - id="rect953-0" /><rect - x="75.819946" - y="98.265511" - width="25.123337" - height="7.8844509" - id="rect968" /><clipPath - id="clip0"><rect - width="71" - height="55" - fill="white" - id="rect716" /></clipPath><clipPath - id="clip0-9"><rect - width="71" - height="55" - fill="white" - id="rect852" /></clipPath><clipPath - id="clip0-6"><rect - width="292" - height="56.4706" - fill="white" - transform="translate(0 11.7646)" - id="rect159" /></clipPath><clipPath - id="clip1"><rect - width="292" - height="56.4706" - fill="white" - transform="translate(0 11.7646)" - id="rect162" /></clipPath><clipPath - id="clip0-65"><rect - width="292" - height="56.4706" - fill="white" - transform="translate(0 11.7646)" - id="rect338" /></clipPath><clipPath - clipPathUnits="userSpaceOnUse" - id="clipPath405"><rect - width="292" - height="56.470596" - fill="#ffffff" - id="rect407" - x="-8.4424901e-06" - y="11.764597" - style="stroke-width:1" /></clipPath><clipPath - clipPathUnits="userSpaceOnUse" - id="clipPath409"><rect - width="292" - height="56.470596" - fill="#ffffff" - id="rect411" - x="-8.4424901e-06" - y="11.764597" - style="stroke-width:1" /></clipPath><clipPath - clipPathUnits="userSpaceOnUse" - id="clipPath413"><rect - width="292" - height="56.470596" - fill="#ffffff" - id="rect415" - x="-8.4424901e-06" - y="11.764597" - style="stroke-width:1" /></clipPath><clipPath - clipPathUnits="userSpaceOnUse" - id="clipPath417"><rect - width="292" - height="56.470596" - fill="#ffffff" - id="rect419" - x="-8.4424901e-06" - y="11.764597" - style="stroke-width:1" /></clipPath><clipPath - clipPathUnits="userSpaceOnUse" - id="clipPath421"><rect - width="292" - height="56.470596" - fill="#ffffff" - id="rect423" - x="-8.4424901e-06" - y="11.764597" - style="stroke-width:1" /></clipPath><clipPath - clipPathUnits="userSpaceOnUse" - id="clipPath425"><rect - width="292" - height="56.470596" - fill="#ffffff" - id="rect427" - x="-8.4424901e-06" - y="11.764597" - style="stroke-width:1" /></clipPath><clipPath - clipPathUnits="userSpaceOnUse" - id="clipPath429"><rect - width="292" - height="56.470596" - fill="#ffffff" - id="rect431" - x="-8.4424901e-06" - y="11.764597" - style="stroke-width:1" /></clipPath><clipPath - clipPathUnits="userSpaceOnUse" - id="clipPath433"><rect - width="292" - height="56.470596" - fill="#ffffff" - id="rect435" - x="-8.4424901e-06" - y="11.764597" - style="stroke-width:1" /></clipPath><clipPath - clipPathUnits="userSpaceOnUse" - id="clipPath437"><rect - width="292" - height="56.470596" - fill="#ffffff" - id="rect439" - x="-8.4424901e-06" - y="11.764597" - style="stroke-width:1" /></clipPath></defs><sodipodi:namedview - id="base" - pagecolor="#ffffff" - bordercolor="#666666" - borderopacity="1.0" - inkscape:pageopacity="0" - inkscape:pageshadow="2" - inkscape:zoom="2.8" - inkscape:cx="226.07143" - inkscape:cy="53.035714" - inkscape:document-units="mm" - inkscape:current-layer="layer1" - showgrid="false" - inkscape:window-width="1920" - inkscape:window-height="1001" - inkscape:window-x="-9" - inkscape:window-y="-9" - inkscape:window-maximized="1" - fit-margin-top="0" - fit-margin-left="0" - fit-margin-right="0" - fit-margin-bottom="0" - inkscape:document-rotation="0" - inkscape:pagecheckerboard="false" - inkscape:showpageshadow="2" - inkscape:deskcolor="#d1d1d1" /><metadata - id="metadata5"><rdf:RDF><cc:Work - rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type - rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><g - inkscape:label="Layer 1" - inkscape:groupmode="layer" - id="layer1" - transform="translate(-52.233408,-75.88169)"><rect - style="fill:#ffffff;fill-opacity:1;stroke-width:0.137677;paint-order:stroke fill markers;stop-color:#000000" - id="rect832" - width="61.511906" - height="30" - x="52.23341" - y="75.881691" /><path - id="path4789-6" - class="" - d="m 168.39406,91.142768 -2.87283,2.872845 c -0.13868,0.138672 -0.36317,0.138672 -0.50172,0 l -0.33506,-0.335079 c -0.13831,-0.138321 -0.13867,-0.362493 -4.4e-4,-0.501129 l 2.27677,-2.287503 -2.27677,-2.287477 c -0.13796,-0.138672 -0.13778,-0.362808 4.4e-4,-0.501129 l 0.33506,-0.335052 c 0.13867,-0.138672 0.36318,-0.138672 0.50172,0 l 2.87283,2.872809 c 0.13867,0.138663 0.13867,0.363187 0,0.501715 z" - inkscape:connector-curvature="0" - style="fill:#ffffff;fill-opacity:1;stroke-width:0.0147822" /><path - id="path4789-6-2" - class="" - d="m 106.90861,91.142408 -2.87283,2.872845 c -0.13867,0.138672 -0.36317,0.138672 -0.50172,0 L 103.199,93.680174 c -0.13831,-0.138321 -0.13867,-0.362493 -4.7e-4,-0.501129 l 2.27677,-2.287503 -2.27677,-2.287477 c -0.13796,-0.138672 -0.13778,-0.362808 4.7e-4,-0.501129 l 0.33506,-0.335052 c 0.13868,-0.138672 0.36318,-0.138672 0.50172,0 l 2.87283,2.872809 c 0.13868,0.138663 0.13868,0.363187 0,0.501715 z" - inkscape:connector-curvature="0" - style="fill:#7289da;fill-opacity:1;stroke-width:0.0147822" /><g - style="fill:none" - id="g196" - transform="matrix(0.14732984,0,0,0.14732984,118.63341,84.998511)"><g - clip-path="url(#clip1)" - id="g155"><path - d="m 61.7958,16.494 c -4.7222,-2.2094 -9.7714,-3.8151 -15.0502,-4.7294 -0.6483,1.1721 -1.4057,2.7486 -1.9279,4.0027 -5.6115,-0.8439 -11.1714,-0.8439 -16.6797,0 -0.5221,-1.2541 -1.2967,-2.8306 -1.9508,-4.0027 -5.2845,0.9143 -10.3395,2.5259 -15.0617,4.7411 C 1.60078,30.8988 -0.981215,44.9344 0.309785,58.7707 6.62708,63.4883 12.7493,66.3541 18.7682,68.2294 c 1.4861,-2.0453 2.8115,-4.2195 3.9533,-6.5109 -2.1746,-0.8263 -4.2574,-1.846 -6.2254,-3.0298 0.5221,-0.3868 1.0328,-0.7912 1.5262,-1.2073 12.0034,5.6143 25.0454,5.6143 36.9054,0 0.4992,0.4161 1.0098,0.8205 1.5262,1.2073 -1.9738,1.1896 -4.0623,2.2093 -6.2369,3.0357 1.1418,2.2855 2.4615,4.4656 3.9533,6.5108 6.0247,-1.8753 12.1526,-4.741 18.4699,-9.4645 C 74.155,42.7309 70.0525,28.8242 61.7958,16.494 Z m -37.439,33.7675 c -3.6033,0 -6.5583,-3.3639 -6.5583,-7.4603 0,-4.0964 2.8919,-7.4661 6.5583,-7.4661 3.6665,0 6.6214,3.3638 6.5583,7.4661 0.0057,4.0964 -2.8918,7.4603 -6.5583,7.4603 z m 24.2364,0 c -3.6033,0 -6.5583,-3.3639 -6.5583,-7.4603 0,-4.0964 2.8918,-7.4661 6.5583,-7.4661 3.6664,0 6.6214,3.3638 6.5583,7.4661 0,4.0964 -2.8919,7.4603 -6.5583,7.4603 z" - fill="#ffffff" - id="path137" /><path - d="m 98.0293,26.1707 h 15.6637 c 3.776,0 6.966,0.6036 9.583,1.805 2.61,1.2013 4.567,2.8774 5.864,5.0223 1.296,2.1449 1.95,4.6004 1.95,7.3665 0,2.7075 -0.677,5.163 -2.031,7.3606 -1.354,2.2035 -3.414,3.9441 -6.185,5.2275 -2.771,1.2834 -6.203,1.928 -10.305,1.928 H 98.0293 Z m 14.3787,21.4138 c 2.542,0 4.499,-0.6505 5.864,-1.9457 1.366,-1.301 2.049,-3.0708 2.049,-5.3153 0,-2.0805 -0.609,-3.739 -1.825,-4.9814 -1.216,-1.2424 -3.058,-1.8694 -5.52,-1.8694 h -4.9 v 14.1118 z" - fill="#ffffff" - id="path139" /><path - d="m 154.541,54.8456 c -2.169,-0.5743 -4.126,-1.4065 -5.864,-2.5024 v -6.8097 c 1.314,1.0372 3.075,1.8929 5.284,2.5668 2.209,0.6681 4.344,1.0021 6.409,1.0021 0.964,0 1.693,-0.1289 2.186,-0.3868 0.494,-0.2578 0.741,-0.5684 0.741,-0.9259 0,-0.4102 -0.132,-0.7501 -0.402,-1.0256 -0.27,-0.2754 -0.792,-0.504 -1.566,-0.6974 l -4.82,-1.1076 c -2.76,-0.6563 -4.717,-1.5647 -5.881,-2.7309 -1.165,-1.1604 -1.745,-2.6841 -1.745,-4.5711 0,-1.5882 0.505,-2.9653 1.527,-4.1433 1.015,-1.1779 2.461,-2.0863 4.337,-2.7251 1.877,-0.6446 4.068,-0.9669 6.587,-0.9669 2.249,0 4.309,0.2461 6.186,0.7384 1.876,0.4923 3.425,1.1193 4.659,1.887 v 6.4406 c -1.263,-0.7677 -2.709,-1.3713 -4.361,-1.8285 -1.647,-0.4512 -3.339,-0.6739 -5.084,-0.6739 -2.519,0 -3.775,0.4395 -3.775,1.3127 0,0.4103 0.195,0.715 0.585,0.9201 0.39,0.2051 1.107,0.4161 2.146,0.6388 l 4.016,0.7384 c 2.623,0.463 4.579,1.2776 5.864,2.4379 1.286,1.1604 1.928,2.8775 1.928,5.1513 0,2.4906 -1.061,4.4656 -3.19,5.9307 -2.129,1.4651 -5.147,2.1976 -9.06,2.1976 -2.301,-0.0058 -4.538,-0.293 -6.707,-0.8673 z" - fill="#ffffff" - id="path141" /><path - d="m 182.978,53.9839 c -2.3,-1.1487 -4.039,-2.7075 -5.198,-4.6766 -1.159,-1.9691 -1.744,-4.1843 -1.744,-6.6457 0,-2.4613 0.602,-4.6648 1.807,-6.6046 1.205,-1.9398 2.972,-3.4635 5.302,-4.5711 2.329,-1.1076 5.112,-1.6585 8.354,-1.6585 4.016,0 7.35,0.8615 10.001,2.5844 v 7.5072 c -0.935,-0.6564 -2.026,-1.1897 -3.271,-1.5999 -1.245,-0.4102 -2.576,-0.6154 -3.999,-0.6154 -2.49,0 -4.435,0.463 -5.841,1.3948 -1.406,0.9318 -2.111,2.1449 -2.111,3.651 0,1.4768 0.682,2.6841 2.048,3.6335 1.366,0.9435 3.345,1.4182 5.944,1.4182 1.337,0 2.657,-0.1993 3.959,-0.5919 1.297,-0.3985 2.416,-0.8849 3.351,-1.4593 v 7.261 c -2.943,1.805 -6.357,2.7075 -10.242,2.7075 -3.27,-0.0117 -6.059,-0.586 -8.36,-1.7346 z" - fill="#ffffff" - id="path143" /><path - d="m 211.518,53.9841 c -2.318,-1.1486 -4.085,-2.7192 -5.302,-4.7176 -1.216,-1.9984 -1.83,-4.2253 -1.83,-6.6867 0,-2.4613 0.608,-4.659 1.83,-6.587 1.222,-1.9281 2.978,-3.4401 5.285,-4.536 2.3,-1.0959 5.049,-1.6409 8.233,-1.6409 3.185,0 5.933,0.545 8.234,1.6409 2.301,1.0959 4.057,2.5962 5.262,4.5125 1.205,1.9164 1.807,4.114 1.807,6.6047 0,2.4613 -0.602,4.6883 -1.807,6.6866 -1.205,1.9984 -2.967,3.569 -5.285,4.7176 -2.318,1.1487 -5.055,1.723 -8.216,1.723 -3.162,0 -5.899,-0.5685 -8.211,-1.7171 z m 12.204,-7.2786 c 0.976,-0.9962 1.469,-2.3148 1.469,-3.9557 0,-1.6409 -0.488,-2.9478 -1.469,-3.9148 -0.975,-0.9728 -2.307,-1.4592 -3.993,-1.4592 -1.716,0 -3.059,0.4864 -4.04,1.4592 -0.975,0.9729 -1.463,2.2739 -1.463,3.9148 0,1.6409 0.488,2.9595 1.463,3.9557 0.976,0.9963 2.324,1.5003 4.04,1.5003 1.686,-0.0059 3.018,-0.504 3.993,-1.5003 z" - fill="#ffffff" - id="path145" /><path - d="m 259.17,31.3395 v 8.8609 c -1.021,-0.6857 -2.341,-1.0256 -3.976,-1.0256 -2.141,0 -3.793,0.6623 -4.941,1.9867 -1.153,1.3245 -1.727,3.3873 -1.727,6.1768 v 7.5482 h -9.84 V 30.8883 h 9.64 v 7.6302 c 0.533,-2.7896 1.4,-4.8465 2.593,-6.1769 1.188,-1.3244 2.725,-1.9866 4.596,-1.9866 1.417,0 2.634,0.3282 3.655,0.9845 z" - fill="#ffffff" - id="path147" /><path - d="m 291.864,25.3503 v 29.5363 h -9.841 v -5.3739 c -0.832,2.0218 -2.094,3.5631 -3.792,4.6179 -1.699,1.0491 -3.799,1.5765 -6.289,1.5765 -2.226,0 -4.165,-0.5509 -5.824,-1.6585 -1.658,-1.1076 -2.937,-2.6254 -3.838,-4.5535 -0.895,-1.9281 -1.349,-4.1081 -1.349,-6.546 -0.028,-2.5141 0.448,-4.7704 1.429,-6.7688 0.976,-1.9984 2.358,-3.5572 4.137,-4.6766 1.779,-1.1193 3.81,-1.6819 6.088,-1.6819 4.688,0 7.832,2.0804 9.438,6.2354 V 25.3503 Z m -11.309,21.1912 c 1.004,-0.9963 1.503,-2.2914 1.503,-3.8737 0,-1.5296 -0.488,-2.7779 -1.463,-3.7331 -0.976,-0.9552 -2.313,-1.4358 -3.994,-1.4358 -1.658,0 -2.983,0.4864 -3.976,1.4592 -0.993,0.9729 -1.486,2.2328 -1.486,3.7917 0,1.5589 0.493,2.8306 1.486,3.8151 0.993,0.9845 2.301,1.4768 3.936,1.4768 1.658,-0.0058 2.989,-0.504 3.994,-1.5002 z" - fill="#ffffff" - id="path149" /><path - d="m 139.382,33.4432 c 2.709,0 4.906,-2.0151 4.906,-4.5008 0,-2.4857 -2.197,-4.5007 -4.906,-4.5007 -2.71,0 -4.906,2.015 -4.906,4.5007 0,2.4857 2.196,4.5008 4.906,4.5008 z" - fill="#ffffff" - id="path151" /><path - d="m 134.472,36.5435 c 3.006,1.3244 6.736,1.383 9.811,0 v 18.4719 h -9.811 z" - fill="#ffffff" - id="path153" /></g></g><path - d="m 61.7958,16.494 c -4.7222,-2.2094 -9.7714,-3.8151 -15.0502,-4.7294 -0.6483,1.1721 -1.4057,2.7486 -1.9279,4.0027 -5.6115,-0.8439 -11.1714,-0.8439 -16.6797,0 -0.5221,-1.2541 -1.2967,-2.8306 -1.9508,-4.0027 -5.2845,0.9143 -10.3395,2.5259 -15.0617,4.7411 C 1.60078,30.8988 -0.981215,44.9344 0.309785,58.7707 6.62708,63.4883 12.7493,66.3541 18.7682,68.2294 c 1.4861,-2.0453 2.8115,-4.2195 3.9533,-6.5109 -2.1746,-0.8263 -4.2574,-1.846 -6.2254,-3.0298 0.5221,-0.3868 1.0328,-0.7912 1.5262,-1.2073 12.0034,5.6143 25.0454,5.6143 36.9054,0 0.4992,0.4161 1.0098,0.8205 1.5262,1.2073 -1.9738,1.1896 -4.0623,2.2093 -6.2369,3.0357 1.1418,2.2855 2.4615,4.4656 3.9533,6.5108 6.0247,-1.8753 12.1526,-4.741 18.4699,-9.4645 C 74.155,42.7309 70.0525,28.8242 61.7958,16.494 Z m -37.439,33.7675 c -3.6033,0 -6.5583,-3.3639 -6.5583,-7.4603 0,-4.0964 2.8919,-7.4661 6.5583,-7.4661 3.6665,0 6.6214,3.3638 6.5583,7.4661 0.0057,4.0964 -2.8918,7.4603 -6.5583,7.4603 z m 24.2364,0 c -3.6033,0 -6.5583,-3.3639 -6.5583,-7.4603 0,-4.0964 2.8918,-7.4661 6.5583,-7.4661 3.6664,0 6.6214,3.3638 6.5583,7.4661 0,4.0964 -2.8919,7.4603 -6.5583,7.4603 z" - fill="#7289da" - id="path316" - transform="matrix(0.14732889,0,0,0.1473333,59.747728,84.998373)" - clip-path="url(#clipPath437)" - style="fill:#7289da;fill-opacity:1" /><g - aria-label="Join us" - id="text232" - style="font-weight:bold;font-stretch:ultra-expanded;font-size:5.98733px;font-family:'ABC Ginto Nord Bold';-inkscape-font-specification:'ABC Ginto Nord Bold, Bold Ultra-Expanded';fill:#7289da;stroke-width:0.264147"><path - d="m 75.055954,93.217428 c 1.059757,0 1.802186,-0.484974 1.802186,-1.616579 v -2.71226 h -1.496832 v 2.37697 c 0,0.484973 -0.305354,0.736441 -0.772366,0.736441 -0.347265,0 -0.562809,-0.131721 -0.69453,-0.24548 v 1.125618 c 0.185607,0.167645 0.60472,0.33529 1.161542,0.33529 z" - id="path892" /><path - d="m 79.624282,93.199466 c 1.454921,0 2.281172,-0.844213 2.281172,-1.915945 0,-1.07772 -0.826251,-1.86206 -2.281172,-1.86206 -1.454922,0 -2.28716,0.790327 -2.28716,1.86206 0,1.065744 0.832238,1.915945 2.28716,1.915945 z m 0,-1.095681 c -0.514911,0 -0.820265,-0.323316 -0.820265,-0.796315 0,-0.472999 0.305354,-0.78434 0.820265,-0.78434 0.508923,0 0.814277,0.311341 0.814277,0.78434 0,0.472999 -0.305354,0.796315 -0.814277,0.796315 z" - id="path894" /><path - d="m 83.144829,89.595094 c 0.461024,0 0.78434,-0.245481 0.78434,-0.598733 0,-0.341278 -0.323316,-0.586759 -0.78434,-0.586759 -0.467012,0 -0.802303,0.245481 -0.802303,0.586759 0,0.353252 0.335291,0.598733 0.802303,0.598733 z m 0.724466,3.484626 V 89.816625 H 82.4024 v 3.263095 z" - id="path896" /><path - d="m 87.407809,89.421461 c -0.730454,0 -1.173516,0.329303 -1.418997,1.029821 v -0.87415 h -1.448934 v 3.502588 h 1.460909 v -1.646516 c 0,-0.544847 0.24548,-0.826252 0.724467,-0.826252 0.431088,0 0.658606,0.275417 0.658606,0.802302 v 1.670466 h 1.466896 v -1.981807 c 0,-1.101668 -0.496948,-1.676452 -1.442947,-1.676452 z" - id="path898" /><path - d="m 93.832213,91.301483 c 0,0.532872 -0.239493,0.808289 -0.700517,0.808289 -0.44905,0 -0.652619,-0.24548 -0.652619,-0.778353 v -1.754287 h -1.466896 v 2.113527 c 0,0.969947 0.496948,1.514794 1.436959,1.514794 0.664594,0 1.149567,-0.293379 1.383073,-0.8502 v 0.724467 h 1.466896 v -3.502588 h -1.466896 z" - id="path900" /><path - d="m 97.729969,93.199466 c 1.191478,0 1.826135,-0.478986 1.826135,-1.185491 0,-0.69453 -0.419113,-0.975935 -1.161542,-1.107656 l -0.598733,-0.107772 c -0.287391,-0.05987 -0.407138,-0.0958 -0.407138,-0.227519 0,-0.119746 0.161658,-0.191594 0.562809,-0.191594 0.53886,0 1.047783,0.15567 1.407022,0.365227 V 89.80465 c -0.341277,-0.209556 -0.910074,-0.383189 -1.616579,-0.383189 -1.137592,0 -1.856072,0.44905 -1.856072,1.14358 0,0.532872 0.293379,0.868163 1.137593,1.065745 l 0.718479,0.161658 c 0.245481,0.05987 0.29338,0.143696 0.29338,0.251467 0,0.0958 -0.125734,0.191595 -0.437076,0.191595 -0.610707,0 -1.347149,-0.215544 -1.742313,-0.520898 v 0.993897 c 0.520898,0.317329 1.185492,0.490961 1.874035,0.490961 z" - id="path902" /></g></g><style - id="style834">.st0{fill:#FFFFFF;}</style></svg> diff --git a/pydis_site/static/images/navbar/pydis_banner_no_square.svg b/pydis_site/static/images/navbar/pydis_banner_no_square.svg deleted file mode 100644 index 114312c7..00000000 --- a/pydis_site/static/images/navbar/pydis_banner_no_square.svg +++ /dev/null @@ -1,151 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="512.94989mm" - height="144.47073mm" - version="1.1" - viewBox="0 0 512.94991 144.47073" - id="svg46" - sodipodi:docname="logo_site_banner_no_bg.min.svg" - inkscape:version="0.92.4 5da689c313, 2019-01-14" - style="enable-background:new"> - <defs - id="defs50" /> - <sodipodi:namedview - pagecolor="#ffffff" - bordercolor="#666666" - borderopacity="1" - objecttolerance="10" - gridtolerance="10" - guidetolerance="10" - inkscape:pageopacity="0" - inkscape:pageshadow="2" - inkscape:window-width="1920" - inkscape:window-height="1052" - id="namedview48" - showgrid="false" - inkscape:zoom="0.61673998" - inkscape:cx="964.4821" - inkscape:cy="35.843714" - inkscape:window-x="1200" - inkscape:window-y="840" - inkscape:window-maximized="1" - inkscape:current-layer="g12" /> - <metadata - id="metadata2"> - <rdf:RDF> - <cc:Work - rdf:about=""> - <dc:format>image/svg+xml</dc:format> - <dc:type - rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - <dc:title></dc:title> - </cc:Work> - </rdf:RDF> - </metadata> - <g - transform="matrix(0.35596,0,0,0.35596,-36.315728,-22.537909)" - id="g12"> - <rect - x="34.319" - y="34.319" - width="463.85999" - height="463.85999" - ry="52.765999" - id="rect4" - style="display:none;fill:#7289da" /> - <path - d="m 330.41,115.25 -11.506,3.3086 c -12.223,-1.8051 -24.757,-2.597 -36.895,-2.5078 -13.5,0.1 -26.498,1.1992 -37.898,3.1992 -3.6998,0.65161 -7.0474,1.386 -10.107,2.1992 h -35.893 v 18.801 h 4.5 v 7.6992 h 2.7832 c -0.63936,3.7142 -0.88476,7.7997 -0.88476,12.301 v 4 h -15.398 l -2.2012,11 17.6,15.014 v 0.0859 h 79.201 v 10 h -108.9 c -23,0 -43.2,13.8 -49.5,40 -7.3,30 -7.6,48.801 0,80.201 0.49734,2.0782 1.0605,4.0985 1.6836,6.0625 l -1.1836,10.438 13.346,11.549 c 7.032,7.5103 16.371,11.951 28.254,11.951 h 27.201 v -36 c 0,-26 22.6,-49 49.5,-49 h 79.199 c 22,0 39.6,-18.102 39.6,-40.102 v -75.199 c 0,-12.9 -6.5819,-23.831 -16.516,-31.273 z m 76.801,77.6 -14.301,7.4004 h -20.1 v 35.1 c 0,27.2 -23.1,50 -49.5,50 h -79.1 c -21.7,0 -39.6,18.5 -39.6,40.1 v 75.102 c 0,21.4 18.7,34 39.6,40.1 25.1,7.3 49.1,8.7 79.1,0 19.9,-5.7 39.6,-17.3 39.6,-40.1 v -30.102 h -0.11914 l -11.721,-10 h 51.439 c 23,0 31.602,-16 39.602,-40 8.3,-24.7 7.9,-48.499 0,-80.199 -3.6226,-14.491 -9.3525,-26.71 -18.947,-33.699 z m -123.4,167.6 h 57.5 v 10 h -57.5 z" - id="path6" - inkscape:connector-curvature="0" - style="fill-opacity:0.20554002" /> - <path - class="st2" - d="m 264.31,100.95 c -13.5,0.1 -26.5,1.2 -37.9,3.2 -33.5,5.9 -39.6,18.2 -39.6,41 v 30.1 h 79.2 v 10 h -108.9 c -23,0 -43.2,13.8 -49.5,40 -7.3,30 -7.6,48.8 0,80.2 5.6,23.4 19.1,40 42.1,40 h 27.2 v -36 c 0,-26 22.6,-49 49.5,-49 h 79.1 c 22,0 39.6,-18.1 39.6,-40.1 v -75.2 c 0,-21.4 -18.1,-37.4 -39.6,-41 -13.5,-2.3 -27.6,-3.3 -41.2,-3.2 z m -42.8,24.2 c 8.2,0 14.9,6.8 14.9,15.1 0,8.3 -6.7,15 -14.9,15 -8.2,0 -14.9,-6.7 -14.9,-15 0,-8.4 6.7,-15.1 14.9,-15.1 z" - id="path8" - inkscape:connector-curvature="0" - style="fill:#cad6ff" /> - <path - class="st3" - d="m 355.11,185.25 v 35 c 0,27.2 -23.1,50 -49.5,50 h -79.1 c -21.7,0 -39.6,18.5 -39.6,40.1 v 75.1 c 0,21.4 18.7,34 39.6,40.1 25.1,7.3 49.1,8.7 79.1,0 19.9,-5.7 39.6,-17.3 39.6,-40.1 v -30.1 h -79.1 v -10 h 118.7 c 23,0 31.6,-16 39.6,-40 8.3,-24.7 7.9,-48.5 0,-80.2 -5.7,-22.8 -16.6,-40 -39.6,-40 h -29.7 z m -44.5,190.2 c 8.2,0 14.9,6.7 14.9,15 0,8.3 -6.7,15.1 -14.9,15.1 -8.2,0 -14.9,-6.8 -14.9,-15.1 0,-8.3 6.7,-15 14.9,-15 z" - id="path10" - inkscape:connector-curvature="0" - style="fill:#ffffff" /> - </g> - <g - transform="matrix(1.8652,0,0,1.8652,-1255.8999,-246.51761)" - aria-label="PYTHON" - id="g26" - style="fill:#ffffff;stroke:#ffffff;stroke-width:1.07229996"> - <path - d="m 764.54,165.21 h 8.1038 v -9.3714 h 4.7536 c 7.7416,0 11.68,-5.6591 11.68,-11.363 0,-5.6591 -3.8934,-11.318 -11.726,-11.318 h -12.812 z m 8.1038,-16.57 v -7.9227 h 4.7084 c 4.8894,-0.0453 4.8894,7.968 0,7.9227 z" - id="path14" - inkscape:connector-curvature="0" /> - <path - d="m 810.56,165.21 v -12.631 l 11.228,-19.422 h -9.5072 l -5.7949,11.771 -5.7949,-11.771 h -9.4167 l 11.182,19.422 v 12.631 z" - id="path16" - inkscape:connector-curvature="0" /> - <path - d="m 849.96,140.81 v -7.6963 h -25.217 v 7.6963 h 8.5112 v 24.402 h 8.1943 V 140.81 Z" - id="path18" - inkscape:connector-curvature="0" /> - <path - d="m 864.13,153.13 h 9.2356 v 12.088 h 8.1491 v -32.053 h -8.1491 v 12.133 H 864.13 v -12.133 h -8.1491 v 32.053 h 8.1491 z" - id="path20" - inkscape:connector-curvature="0" /> - <path - d="m 888.61,154.66 c 0,7.3342 6.6551,11.001 13.31,11.001 6.6551,0 13.265,-3.6671 13.265,-11.001 v -10.956 c 0,-7.3342 -6.6551,-11.001 -13.31,-11.001 -6.6551,0 -13.265,3.6218 -13.265,11.001 z m 8.1038,-10.956 c 0,-2.3542 2.5353,-3.6218 5.0705,-3.6218 2.6258,0 5.2969,1.1318 5.2969,3.6218 v 10.956 c 0,2.3089 -2.5805,3.486 -5.1611,3.486 -2.5805,0 -5.2064,-1.1318 -5.2064,-3.486 z" - id="path22" - inkscape:connector-curvature="0" /> - <path - d="m 922.24,165.21 h 8.1038 v -12.812 l -1.0413,-6.2023 0.22636,-0.0453 2.8069,6.4287 7.2436,12.631 h 8.1943 v -32.053 h -8.0585 l -0.003,13.85 c 0,0 0.8633,6.2058 0.90857,6.2058 l -0.22636,0.0453 -2.7164,-6.4287 -7.7416,-13.672 h -7.6963 z" - id="path24" - inkscape:connector-curvature="0" /> - </g> - <style - type="text/css" - id="style28">.st0{fill:#7289DA;} - .st1{fill:#5B6DAE;} - .st2{fill:#CAD6FF;} - .st3{fill:#FFFFFF;}</style> - <g - transform="translate(-34.599928,-28.764122)" - aria-label="DISCORD" - id="g44" - style="fill:#cad6ff;stroke:#cad6ff;stroke-width:2.10770011"> - <path - d="m 204.75,111.65 v 59.69 h 22.341 c 11.972,0.0843 23.185,-5.9015 23.185,-19.644 v -20.065 c 0,-13.911 -11.213,-19.981 -23.185,-19.981 z m 15.175,45.526 v -31.447 h 7.672 c 5.0584,0 7.5034,2.4449 7.5034,6.323 v 18.632 c 0,3.8781 -2.3606,6.4917 -7.5034,6.4917 z" - id="path30" - inkscape:connector-curvature="0" /> - <path - d="m 275.33,111.65 h -15.091 v 59.69 h 15.091 z" - id="path32" - inkscape:connector-curvature="0" /> - <path - d="m 283.72,161.39 c 5.5643,7.1661 13.152,10.791 20.318,10.791 10.96,0 23.1,-6.323 23.606,-17.873 0.67445,-14.669 -9.9483,-18.379 -20.571,-20.318 -4.6369,-1.0117 -7.7563,-2.6978 -7.9249,-5.5643 0.25292,-6.9132 10.96,-7.1661 17.199,-0.50585 l 9.864,-7.5877 c -6.1544,-7.5034 -13.152,-9.5267 -20.402,-9.5267 -10.96,0 -21.583,6.1544 -21.583,17.873 0,11.381 8.768,17.452 18.379,18.969 4.8898,0.67446 10.37,2.6135 10.201,5.9858 -0.42154,6.4074 -13.573,6.0701 -19.559,-1.1803 z" - id="path34" - inkscape:connector-curvature="0" /> - <path - d="m 366.42,152.7 c -3.288,8.2621 -16.777,6.2388 -16.777,-0.92738 v -20.571 c 0,-7.419 13.321,-9.1052 17.367,-1.6862 l 12.309,-4.9741 c -4.8055,-10.623 -13.658,-13.742 -20.993,-13.742 -11.972,0 -23.775,6.9132 -23.775,20.402 v 20.571 c 0,13.573 11.803,20.402 23.522,20.402 7.5034,0 16.524,-3.7095 21.498,-13.405 z" - id="path36" - inkscape:connector-curvature="0" /> - <path - d="m 385.43,151.69 c 0,13.658 12.393,20.487 24.786,20.487 12.393,0 24.702,-6.8289 24.702,-20.487 v -20.402 c 0,-13.658 -12.393,-20.487 -24.786,-20.487 -12.393,0 -24.702,6.7446 -24.702,20.487 z m 15.091,-20.402 c 0,-4.384 4.7212,-6.7446 9.4424,-6.7446 4.8898,0 9.864,2.1077 9.864,6.7446 v 20.402 c 0,4.2997 -4.8055,6.4917 -9.611,6.4917 -4.8055,0 -9.6954,-2.1077 -9.6954,-6.4917 z" - id="path38" - inkscape:connector-curvature="0" /> - <path - d="m 476.44,171.34 h 18.801 L 478.97,150.853 c 7.2504,-2.2763 11.719,-8.515 11.719,-19.559 -0.33723,-13.995 -9.864,-19.644 -22.173,-19.644 h -23.859 v 59.69 h 15.26 v -18.969 h 2.6978 z M 459.916,139.388 V 125.73 h 8.8523 c 9.5267,0 9.5267,13.658 0,13.658 z" - id="path40" - inkscape:connector-curvature="0" /> - <path - d="m 500.97,111.65 v 59.69 h 22.341 c 11.972,0.0843 23.185,-5.9015 23.185,-19.644 v -20.065 c 0,-13.911 -11.213,-19.981 -23.185,-19.981 z m 15.175,45.526 v -31.447 h 7.672 c 5.0584,0 7.5034,2.4449 7.5034,6.323 v 18.632 c 0,3.8781 -2.3606,6.4917 -7.5034,6.4917 z" - id="path42" - inkscape:connector-curvature="0" /> - </g> -</svg> diff --git a/pydis_site/static/images/resources/duck_pond_404.jpg b/pydis_site/static/images/resources/duck_pond_404.jpg Binary files differdeleted file mode 100644 index 29bcf1d6..00000000 --- a/pydis_site/static/images/resources/duck_pond_404.jpg +++ /dev/null diff --git a/pydis_site/static/images/sponsors/jetbrains.png b/pydis_site/static/images/sponsors/jetbrains.png Binary files differdeleted file mode 100644 index b79e110a..00000000 --- a/pydis_site/static/images/sponsors/jetbrains.png +++ /dev/null diff --git a/pydis_site/static/images/sponsors/linode.png b/pydis_site/static/images/sponsors/linode.png Binary files differdeleted file mode 100644 index 22daa661..00000000 --- a/pydis_site/static/images/sponsors/linode.png +++ /dev/null diff --git a/pydis_site/static/images/sponsors/notion.png b/pydis_site/static/images/sponsors/notion.png Binary files differdeleted file mode 100644 index 44ae9244..00000000 --- a/pydis_site/static/images/sponsors/notion.png +++ /dev/null diff --git a/pydis_site/static/images/sponsors/sentry.png b/pydis_site/static/images/sponsors/sentry.png Binary files differdeleted file mode 100644 index ce185da2..00000000 --- a/pydis_site/static/images/sponsors/sentry.png +++ /dev/null diff --git a/pydis_site/static/images/sponsors/streamyard.png b/pydis_site/static/images/sponsors/streamyard.png Binary files differdeleted file mode 100644 index a1527e8d..00000000 --- a/pydis_site/static/images/sponsors/streamyard.png +++ /dev/null diff --git a/pydis_site/static/images/timeline/cd-icon-location.svg b/pydis_site/static/images/timeline/cd-icon-location.svg deleted file mode 100755 index 6128fecd..00000000 --- a/pydis_site/static/images/timeline/cd-icon-location.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg class="nc-icon glyph" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px" height="24px" viewBox="0 0 24 24"> -<path fill="#ffffff" d="M12,0C7.6,0,3,3.4,3,9c0,5.3,8,13.4,8.3,13.7c0.2,0.2,0.4,0.3,0.7,0.3s0.5-0.1,0.7-0.3C13,22.4,21,14.3,21,9 - C21,3.4,16.4,0,12,0z M12,12c-1.7,0-3-1.3-3-3s1.3-3,3-3s3,1.3,3,3S13.7,12,12,12z"></path> -</svg> diff --git a/pydis_site/static/images/timeline/cd-icon-movie.svg b/pydis_site/static/images/timeline/cd-icon-movie.svg deleted file mode 100755 index 498a93fa..00000000 --- a/pydis_site/static/images/timeline/cd-icon-movie.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg class="nc-icon glyph" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px" height="24px" viewBox="0 0 24 24"><g> -<path fill="#ffffff" d="M23.6,6.2c-0.3-0.2-0.6-0.2-0.9-0.1L17,8.5V5c0-0.6-0.4-1-1-1H1C0.4,4,0,4.4,0,5v14c0,0.6,0.4,1,1,1h15 - c0.6,0,1-0.4,1-1v-3.5l5.6,2.4C22.7,18,22.9,18,23,18c0.2,0,0.4-0.1,0.6-0.2c0.3-0.2,0.4-0.5,0.4-0.8V7C24,6.7,23.8,6.4,23.6,6.2z"></path> -</g></svg> diff --git a/pydis_site/static/images/timeline/cd-icon-picture.svg b/pydis_site/static/images/timeline/cd-icon-picture.svg deleted file mode 100755 index 015718a8..00000000 --- a/pydis_site/static/images/timeline/cd-icon-picture.svg +++ /dev/null @@ -1,72 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - enable-background="new 0 0 438 438" - version="1.1" - viewBox="0 0 346.16486 345.72064" - xml:space="preserve" - id="svg6016" - sodipodi:docname="logo_solo.svg" - inkscape:version="0.91 r13725" - inkscape:export-filename="/home/scragly/Github/PyDisBranding/logos/logo_solo/logo_full_512.png" - inkscape:export-xdpi="112.21918" - inkscape:export-ydpi="112.21918" - width="346.16486" - height="345.72064"><defs - id="defs6020" /><sodipodi:namedview - pagecolor="#ffffff" - bordercolor="#666666" - borderopacity="1" - objecttolerance="10" - gridtolerance="10" - guidetolerance="10" - inkscape:pageopacity="0" - inkscape:pageshadow="2" - inkscape:window-width="1859" - inkscape:window-height="1056" - id="namedview6018" - showgrid="false" - inkscape:zoom="2.1552511" - inkscape:cx="150.03331" - inkscape:cy="193.10048" - inkscape:window-x="61" - inkscape:window-y="24" - inkscape:window-maximized="1" - inkscape:current-layer="svg6016" - fit-margin-top="0" - fit-margin-left="0" - fit-margin-right="0" - fit-margin-bottom="0" /><metadata - id="metadata6004"><rdf:RDF><cc:Work - rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type - rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><style - type="text/css" - id="style6006"> - .st0{fill:#7289DA;} - .st1{fill:#5B6DAE;} - .st2{fill:#CAD6FF;} - .st3{fill:#FFFFFF;} -</style><path - d="m 228.38807,14.3068 -11.506,3.3086 c -12.223,-1.8051 -24.757,-2.5971 -36.895,-2.5078 -13.5,0.1 -26.498,1.1992 -37.898,3.1992 -3.6998,0.6516 -7.0474,1.386 -10.107,2.1992 l -35.893004,0 0,18.801 4.500004,0 0,7.6992 2.7832,0 c -0.63936,3.7142 -0.88476,7.7997 -0.88476,12.301 l 0,4 -15.398004,0 -2.2012,11 17.600004,15.014 0,0.0859 79.201,0 0,10 -108.900004,0 c -23,0 -43.2,13.8 -49.5,40 -7.3,30 -7.6,48.801 0,80.201 0.49734,2.0782 1.0605,4.0985 1.6836,6.0625 l -1.1836,10.438 13.346,11.549 c 7.032,7.5103 16.371,11.951 28.254,11.951 l 27.201,0 0,-36 c 0,-26 22.600004,-49 49.500004,-49 l 79.199,0 c 22,0 39.6,-18.102 39.6,-40.102 l 0,-75.199 c 0,-12.9 -6.5819,-23.831 -16.516,-31.273 z m 76.801,77.6 -14.301,7.4004 -20.1,0 0,35.1 c 0,27.2 -23.1,50 -49.5,50 l -79.1,0 c -21.7,0 -39.6,18.5 -39.6,40.1 l 0,75.102 c 0,21.4 18.7,34 39.6,40.1 25.1,7.3 49.1,8.7 79.1,0 19.9,-5.7 39.6,-17.3 39.6,-40.1 l 0,-30.102 -0.11914,0 -11.721,-10 51.439,0 c 23,0 31.602,-16 39.602,-40 8.3,-24.7 7.9,-48.499 0,-80.199 -3.6226,-14.491 -9.3525,-26.71 -18.947,-33.699 z m -123.4,167.6 57.5,0 0,10 -57.5,0 z" - id="path6010" - inkscape:label="shadow" - style="fill:#5b6dae;fill-opacity:1" - inkscape:connector-curvature="0" /><path - class="st2" - d="m 162.28807,0.00679951 c -13.5,0.1 -26.5,1.19999999 -37.9,3.19999999 C 90.888066,9.1067995 84.788066,21.4068 84.788066,44.2068 l 0,30.1 79.200004,0 0,10 -108.900004,0 c -23,0 -43.2,13.8 -49.4999998,40 -7.3,30 -7.6,48.8 0,80.2 5.5999998,23.4 19.0999998,40 42.0999998,40 l 27.2,0 0,-36 c 0,-26 22.6,-49 49.500004,-49 l 79.1,0 c 22,0 39.6,-18.1 39.6,-40.1 l 0,-75.2 c 0,-21.4 -18.1,-37.4000005 -39.6,-41.0000005 -13.5,-2.29999999 -27.6,-3.29999999 -41.2,-3.19999999 z m -42.8,24.20000049 c 8.2,0 14.9,6.8 14.9,15.1 0,8.3 -6.7,15 -14.9,15 -8.2,0 -14.9,-6.7 -14.9,-15 0,-8.4 6.7,-15.1 14.9,-15.1 z" - id="path6012" - inkscape:label="upper_snake" - inkscape:connector-curvature="0" - style="fill:#cad6ff" /><path - class="st3" - d="m 253.08807,84.3068 0,35 c 0,27.2 -23.1,50 -49.5,50 l -79.1,0 c -21.7,0 -39.600004,18.5 -39.600004,40.1 l 0,75.1 c 0,21.4 18.700004,34 39.600004,40.1 25.1,7.3 49.1,8.7 79.1,0 19.9,-5.7 39.6,-17.3 39.6,-40.1 l 0,-30.1 -79.1,0 0,-10 118.7,0 c 23,0 31.6,-16 39.6,-40 8.3,-24.7 7.9,-48.5 0,-80.2 -5.7,-22.8 -16.6,-40 -39.6,-40 l -29.7,0 z m -44.5,190.2 c 8.2,0 14.9,6.7 14.9,15 0,8.3 -6.7,15.1 -14.9,15.1 -8.2,0 -14.9,-6.8 -14.9,-15.1 0,-8.3 6.7,-15 14.9,-15 z" - id="path6014" - inkscape:label="lower_snake" - inkscape:connector-curvature="0" - style="fill:#ffffff" /></svg> diff --git a/pydis_site/static/images/waves/wave_dark.svg b/pydis_site/static/images/waves/wave_dark.svg deleted file mode 100644 index 35174c47..00000000 --- a/pydis_site/static/images/waves/wave_dark.svg +++ /dev/null @@ -1,73 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="1600" - height="198" - version="1.1" - id="svg11" - sodipodi:docname="wave.svg" - inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"> - <metadata - id="metadata15"> - <rdf:RDF> - <cc:Work - rdf:about=""> - <dc:format>image/svg+xml</dc:format> - <dc:type - rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - </cc:Work> - </rdf:RDF> - </metadata> - <sodipodi:namedview - pagecolor="#ffffff" - bordercolor="#666666" - borderopacity="1" - objecttolerance="10" - gridtolerance="10" - guidetolerance="10" - inkscape:pageopacity="0" - inkscape:pageshadow="2" - inkscape:window-width="2560" - inkscape:window-height="1409" - id="namedview13" - showgrid="false" - inkscape:zoom="1.44625" - inkscape:cx="757.49384" - inkscape:cy="107.38903" - inkscape:window-x="4880" - inkscape:window-y="677" - inkscape:window-maximized="1" - inkscape:current-layer="svg11" /> - <defs - id="defs7"> - <linearGradient - id="a" - x1="50%" - x2="50%" - y1="-10.959%" - y2="100%"> - <stop - stop-color="#57BBC1" - stop-opacity=".25" - offset="0%" - id="stop2" /> - <stop - stop-color="#015871" - offset="100%" - id="stop4" /> - </linearGradient> - </defs> - <path - fill="url(#a)" - fill-rule="evenodd" - d="M.005 121C311 121 409.898-.25 811 0c400 0 500 121 789 121v77H0s.005-48 .005-77z" - transform="matrix(-1 0 0 1 1600 0)" - id="path9" - style="fill:#5b6daf;fill-opacity:1" /> -</svg> diff --git a/pydis_site/static/images/waves/wave_white.svg b/pydis_site/static/images/waves/wave_white.svg deleted file mode 100644 index 441dacff..00000000 --- a/pydis_site/static/images/waves/wave_white.svg +++ /dev/null @@ -1,77 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="1600" - height="28.745832" - version="1.1" - id="svg11" - sodipodi:docname="wavew.svg" - inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"> - <metadata - id="metadata15"> - <rdf:RDF> - <cc:Work - rdf:about=""> - <dc:format>image/svg+xml</dc:format> - <dc:type - rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - <dc:title></dc:title> - </cc:Work> - </rdf:RDF> - </metadata> - <sodipodi:namedview - pagecolor="#ffffff" - bordercolor="#666666" - borderopacity="1" - objecttolerance="10" - gridtolerance="10" - guidetolerance="10" - inkscape:pageopacity="0" - inkscape:pageshadow="2" - inkscape:window-width="2560" - inkscape:window-height="1409" - id="namedview13" - showgrid="false" - inkscape:zoom="1.44625" - inkscape:cx="884.40031" - inkscape:cy="-61.865141" - inkscape:window-x="4880" - inkscape:window-y="677" - inkscape:window-maximized="1" - inkscape:current-layer="svg11" - fit-margin-top="0" - fit-margin-left="0" - fit-margin-right="0" - fit-margin-bottom="0" /> - <defs - id="defs7"> - <linearGradient - id="a" - x1="0.5" - x2="0.5" - y1="-0.10958999" - y2="1"> - <stop - stop-color="#57BBC1" - stop-opacity=".25" - offset="0%" - id="stop2" /> - <stop - stop-color="#015871" - offset="100%" - id="stop4" /> - </linearGradient> - </defs> - <path - fill="url(#a)" - fill-rule="evenodd" - d="M 1599.995,17.566918 C 1289,17.566918 1190.102,-0.03623696 789,5.6042811e-5 389,5.6042811e-5 289,17.566918 0,17.566918 v 11.178914 h 1600 c 0,0 -0.01,-6.968673 -0.01,-11.178914 z" - id="path9" - style="fill:#ffffff;fill-opacity:1;stroke-width:0.381026" /> -</svg> diff --git a/pydis_site/static/js/collapsibles.js b/pydis_site/static/js/collapsibles.js deleted file mode 100644 index 1df0b9fe..00000000 --- a/pydis_site/static/js/collapsibles.js +++ /dev/null @@ -1,67 +0,0 @@ -/* -A utility for creating simple collapsible cards. - -To see this in action, go to /resources or /pages/guides/pydis-guides/contributing/bot/ - -// HOW TO USE THIS // -First, import this file and the corresponding css file into your template. - - <link rel="stylesheet" href="{% static "css/collapsibles.css" %}"> - <script defer src="{% static "js/collapsibles.js" %}"></script> - -Next, you'll need some HTML that these scripts can interact with. - -<div class="card"> - <button type="button" class="card-header collapsible"> - <span class="card-header-title subtitle is-6 my-2 ml-2">Your headline</span> - <span class="card-header-icon"> - <i class="fas fa-fw fa-angle-down title is-5" aria-hidden="true"></i> - </span> - </button> - <div class="collapsible-content collapsed"> - <div class="card-content"> - You can put anything you want here. Lists, more divs, flexboxes, images, whatever. - </div> - </div> -</div> - -That's it! Collapsing stuff should now work. - */ - -document.addEventListener("DOMContentLoaded", () => { - const contentContainers = document.getElementsByClassName("collapsible-content"); - for (const container of contentContainers) { - // Close any collapsibles that are marked as initially collapsed - if (container.classList.contains("collapsed")) { - container.style.maxHeight = "0px"; - // Set maxHeight to the size of the container on all other containers. - } else { - container.style.maxHeight = container.scrollHeight + "px"; - } - } - - // Listen for click events, and collapse or explode - const headers = document.getElementsByClassName("collapsible"); - for (const header of headers) { - const content = header.nextElementSibling; - const icon = header.querySelector(".card-header-icon i"); - - // Any collapsibles that are not initially collapsed needs an icon switch. - if (!content.classList.contains("collapsed")) { - icon.classList.remove("fas", "fa-angle-down"); - icon.classList.add("far", "fa-window-minimize"); - } - - header.addEventListener("click", () => { - if (content.style.maxHeight !== "0px"){ - content.style.maxHeight = "0px"; - icon.classList.remove("far", "fa-window-minimize"); - icon.classList.add("fas", "fa-angle-down"); - } else { - content.style.maxHeight = content.scrollHeight + "px"; - icon.classList.remove("fas", "fa-angle-down"); - icon.classList.add("far", "fa-window-minimize"); - } - }); - } -}); diff --git a/pydis_site/static/js/content/listing.js b/pydis_site/static/js/content/listing.js deleted file mode 100644 index 4b722632..00000000 --- a/pydis_site/static/js/content/listing.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Trim a tag listing to only show a few lines of content. - */ -function trimTag() { - const containers = document.getElementsByClassName("tag-container"); - for (const container of containers) { - if (container.textContent.startsWith("Contains the following tags:")) { - // Tag group, no need to trim - continue; - } - - // Remove every element after the first two paragraphs - while (container.children.length > 2) { - container.removeChild(container.lastChild); - } - - // Trim down the elements if they are too long - const containerLength = container.textContent.length; - if (containerLength > 300) { - if (containerLength - container.firstChild.textContent.length > 300) { - // The first element alone takes up more than 300 characters - container.removeChild(container.lastChild); - } - - let last = container.lastChild.lastChild; - while (container.textContent.length > 300 && container.lastChild.childNodes.length > 0) { - last = container.lastChild.lastChild; - last.remove(); - } - - if (container.textContent.length > 300 && (last instanceof HTMLElement && last.tagName !== "CODE")) { - // Add back the final element (up to a period if possible) - const stop = last.textContent.indexOf("."); - last.textContent = last.textContent.slice(0, stop > 0 ? stop + 1: null); - container.lastChild.appendChild(last); - } - } - } -} - -trimTag(); diff --git a/pydis_site/static/js/fuzzysort/LICENSE.md b/pydis_site/static/js/fuzzysort/LICENSE.md deleted file mode 100644 index a3b9d9d7..00000000 --- a/pydis_site/static/js/fuzzysort/LICENSE.md +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2018 Stephen Kamenar - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/pydis_site/static/js/fuzzysort/fuzzysort.js b/pydis_site/static/js/fuzzysort/fuzzysort.js deleted file mode 100644 index ba01ae63..00000000 --- a/pydis_site/static/js/fuzzysort/fuzzysort.js +++ /dev/null @@ -1,636 +0,0 @@ -/* - fuzzysort.js https://github.com/farzher/fuzzysort - SublimeText-like Fuzzy Search - - fuzzysort.single('fs', 'Fuzzy Search') // {score: -16} - fuzzysort.single('test', 'test') // {score: 0} - fuzzysort.single('doesnt exist', 'target') // null - - fuzzysort.go('mr', [{file:'Monitor.cpp'}, {file:'MeshRenderer.cpp'}], {key:'file'}) - // [{score:-18, obj:{file:'MeshRenderer.cpp'}}, {score:-6009, obj:{file:'Monitor.cpp'}}] - - fuzzysort.go('mr', ['Monitor.cpp', 'MeshRenderer.cpp']) - // [{score: -18, target: "MeshRenderer.cpp"}, {score: -6009, target: "Monitor.cpp"}] - - fuzzysort.highlight(fuzzysort.single('fs', 'Fuzzy Search'), '<b>', '</b>') - // <b>F</b>uzzy <b>S</b>earch -*/ - -// UMD (Universal Module Definition) for fuzzysort -;(function(root, UMD) { - if(typeof define === 'function' && define.amd) define([], UMD) - else if(typeof module === 'object' && module.exports) module.exports = UMD() - else root.fuzzysort = UMD() -})(this, function UMD() { function fuzzysortNew(instanceOptions) { - - var fuzzysort = { - - single: function(search, target, options) { ;if(search=='farzher')return{target:"farzher was here (^-^*)/",score:0,indexes:[0,1,2,3,4,5,6]} - if(!search) return null - if(!isObj(search)) search = fuzzysort.getPreparedSearch(search) - - if(!target) return null - if(!isObj(target)) target = fuzzysort.getPrepared(target) - - var allowTypo = options && options.allowTypo!==undefined ? options.allowTypo - : instanceOptions && instanceOptions.allowTypo!==undefined ? instanceOptions.allowTypo - : true - var algorithm = allowTypo ? fuzzysort.algorithm : fuzzysort.algorithmNoTypo - return algorithm(search, target, search[0]) - }, - - go: function(search, targets, options) { ;if(search=='farzher')return[{target:"farzher was here (^-^*)/",score:0,indexes:[0,1,2,3,4,5,6],obj:targets?targets[0]:null}] - if(!search) return noResults - search = fuzzysort.prepareSearch(search) - var searchLowerCode = search[0] - - var threshold = options && options.threshold || instanceOptions && instanceOptions.threshold || -9007199254740991 - var limit = options && options.limit || instanceOptions && instanceOptions.limit || 9007199254740991 - var allowTypo = options && options.allowTypo!==undefined ? options.allowTypo - : instanceOptions && instanceOptions.allowTypo!==undefined ? instanceOptions.allowTypo - : true - var algorithm = allowTypo ? fuzzysort.algorithm : fuzzysort.algorithmNoTypo - var resultsLen = 0; var limitedCount = 0 - var targetsLen = targets.length - - // This code is copy/pasted 3 times for performance reasons [options.keys, options.key, no keys] - - // options.keys - if(options && options.keys) { - var scoreFn = options.scoreFn || defaultScoreFn - var keys = options.keys - var keysLen = keys.length - for(var i = targetsLen - 1; i >= 0; --i) { var obj = targets[i] - var objResults = new Array(keysLen) - for (var keyI = keysLen - 1; keyI >= 0; --keyI) { - var key = keys[keyI] - var target = getValue(obj, key) - if(!target) { objResults[keyI] = null; continue } - if(!isObj(target)) target = fuzzysort.getPrepared(target) - - objResults[keyI] = algorithm(search, target, searchLowerCode) - } - objResults.obj = obj // before scoreFn so scoreFn can use it - var score = scoreFn(objResults) - if(score === null) continue - if(score < threshold) continue - objResults.score = score - if(resultsLen < limit) { q.add(objResults); ++resultsLen } - else { - ++limitedCount - if(score > q.peek().score) q.replaceTop(objResults) - } - } - - // options.key - } else if(options && options.key) { - var key = options.key - for(var i = targetsLen - 1; i >= 0; --i) { var obj = targets[i] - var target = getValue(obj, key) - if(!target) continue - if(!isObj(target)) target = fuzzysort.getPrepared(target) - - var result = algorithm(search, target, searchLowerCode) - if(result === null) continue - if(result.score < threshold) continue - - // have to clone result so duplicate targets from different obj can each reference the correct obj - result = {target:result.target, _targetLowerCodes:null, _nextBeginningIndexes:null, score:result.score, indexes:result.indexes, obj:obj} // hidden - - if(resultsLen < limit) { q.add(result); ++resultsLen } - else { - ++limitedCount - if(result.score > q.peek().score) q.replaceTop(result) - } - } - - // no keys - } else { - for(var i = targetsLen - 1; i >= 0; --i) { var target = targets[i] - if(!target) continue - if(!isObj(target)) target = fuzzysort.getPrepared(target) - - var result = algorithm(search, target, searchLowerCode) - if(result === null) continue - if(result.score < threshold) continue - if(resultsLen < limit) { q.add(result); ++resultsLen } - else { - ++limitedCount - if(result.score > q.peek().score) q.replaceTop(result) - } - } - } - - if(resultsLen === 0) return noResults - var results = new Array(resultsLen) - for(var i = resultsLen - 1; i >= 0; --i) results[i] = q.poll() - results.total = resultsLen + limitedCount - return results - }, - - goAsync: function(search, targets, options) { - var canceled = false - var p = new Promise(function(resolve, reject) { ;if(search=='farzher')return resolve([{target:"farzher was here (^-^*)/",score:0,indexes:[0,1,2,3,4,5,6],obj:targets?targets[0]:null}]) - if(!search) return resolve(noResults) - search = fuzzysort.prepareSearch(search) - var searchLowerCode = search[0] - - var q = fastpriorityqueue() - var iCurrent = targets.length - 1 - var threshold = options && options.threshold || instanceOptions && instanceOptions.threshold || -9007199254740991 - var limit = options && options.limit || instanceOptions && instanceOptions.limit || 9007199254740991 - var allowTypo = options && options.allowTypo!==undefined ? options.allowTypo - : instanceOptions && instanceOptions.allowTypo!==undefined ? instanceOptions.allowTypo - : true - var algorithm = allowTypo ? fuzzysort.algorithm : fuzzysort.algorithmNoTypo - var resultsLen = 0; var limitedCount = 0 - function step() { - if(canceled) return reject('canceled') - - var startMs = Date.now() - - // This code is copy/pasted 3 times for performance reasons [options.keys, options.key, no keys] - - // options.keys - if(options && options.keys) { - var scoreFn = options.scoreFn || defaultScoreFn - var keys = options.keys - var keysLen = keys.length - for(; iCurrent >= 0; --iCurrent) { - if(iCurrent%1000/*itemsPerCheck*/ === 0) { - if(Date.now() - startMs >= 10/*asyncInterval*/) { - isNode?setImmediate(step):setTimeout(step) - return - } - } - - var obj = targets[iCurrent] - var objResults = new Array(keysLen) - for (var keyI = keysLen - 1; keyI >= 0; --keyI) { - var key = keys[keyI] - var target = getValue(obj, key) - if(!target) { objResults[keyI] = null; continue } - if(!isObj(target)) target = fuzzysort.getPrepared(target) - - objResults[keyI] = algorithm(search, target, searchLowerCode) - } - objResults.obj = obj // before scoreFn so scoreFn can use it - var score = scoreFn(objResults) - if(score === null) continue - if(score < threshold) continue - objResults.score = score - if(resultsLen < limit) { q.add(objResults); ++resultsLen } - else { - ++limitedCount - if(score > q.peek().score) q.replaceTop(objResults) - } - } - - // options.key - } else if(options && options.key) { - var key = options.key - for(; iCurrent >= 0; --iCurrent) { - if(iCurrent%1000/*itemsPerCheck*/ === 0) { - if(Date.now() - startMs >= 10/*asyncInterval*/) { - isNode?setImmediate(step):setTimeout(step) - return - } - } - - var obj = targets[iCurrent] - var target = getValue(obj, key) - if(!target) continue - if(!isObj(target)) target = fuzzysort.getPrepared(target) - - var result = algorithm(search, target, searchLowerCode) - if(result === null) continue - if(result.score < threshold) continue - - // have to clone result so duplicate targets from different obj can each reference the correct obj - result = {target:result.target, _targetLowerCodes:null, _nextBeginningIndexes:null, score:result.score, indexes:result.indexes, obj:obj} // hidden - - if(resultsLen < limit) { q.add(result); ++resultsLen } - else { - ++limitedCount - if(result.score > q.peek().score) q.replaceTop(result) - } - } - - // no keys - } else { - for(; iCurrent >= 0; --iCurrent) { - if(iCurrent%1000/*itemsPerCheck*/ === 0) { - if(Date.now() - startMs >= 10/*asyncInterval*/) { - isNode?setImmediate(step):setTimeout(step) - return - } - } - - var target = targets[iCurrent] - if(!target) continue - if(!isObj(target)) target = fuzzysort.getPrepared(target) - - var result = algorithm(search, target, searchLowerCode) - if(result === null) continue - if(result.score < threshold) continue - if(resultsLen < limit) { q.add(result); ++resultsLen } - else { - ++limitedCount - if(result.score > q.peek().score) q.replaceTop(result) - } - } - } - - if(resultsLen === 0) return resolve(noResults) - var results = new Array(resultsLen) - for(var i = resultsLen - 1; i >= 0; --i) results[i] = q.poll() - results.total = resultsLen + limitedCount - resolve(results) - } - - isNode?setImmediate(step):step() //setTimeout here is too slow - }) - p.cancel = function() { canceled = true } - return p - }, - - highlight: function(result, hOpen, hClose) { - if(typeof hOpen == 'function') return fuzzysort.highlightCallback(result, hOpen) - if(result === null) return null - if(hOpen === undefined) hOpen = '<b>' - if(hClose === undefined) hClose = '</b>' - var highlighted = '' - var matchesIndex = 0 - var opened = false - var target = result.target - var targetLen = target.length - var matchesBest = result.indexes - for(var i = 0; i < targetLen; ++i) { var char = target[i] - if(matchesBest[matchesIndex] === i) { - ++matchesIndex - if(!opened) { opened = true - highlighted += hOpen - } - - if(matchesIndex === matchesBest.length) { - highlighted += char + hClose + target.substr(i+1) - break - } - } else { - if(opened) { opened = false - highlighted += hClose - } - } - highlighted += char - } - - return highlighted - }, - highlightCallback: function(result, cb) { - if(result === null) return null - var target = result.target - var targetLen = target.length - var indexes = result.indexes - var highlighted = '' - var matchI = 0 - var indexesI = 0 - var opened = false - var result = [] - for(var i = 0; i < targetLen; ++i) { var char = target[i] - if(indexes[indexesI] === i) { - ++indexesI - if(!opened) { opened = true - result.push(highlighted); highlighted = '' - } - - if(indexesI === indexes.length) { - highlighted += char - result.push(cb(highlighted, matchI++)); highlighted = '' - result.push(target.substr(i+1)) - break - } - } else { - if(opened) { opened = false - result.push(cb(highlighted, matchI++)); highlighted = '' - } - } - highlighted += char - } - return result - }, - - prepare: function(target) { - if(!target) return {target: '', _targetLowerCodes: [0/*this 0 doesn't make sense. here because an empty array causes the algorithm to deoptimize and run 50% slower!*/], _nextBeginningIndexes: null, score: null, indexes: null, obj: null} // hidden - return {target:target, _targetLowerCodes:fuzzysort.prepareLowerCodes(target), _nextBeginningIndexes:null, score:null, indexes:null, obj:null} // hidden - }, - prepareSlow: function(target) { - if(!target) return {target: '', _targetLowerCodes: [0/*this 0 doesn't make sense. here because an empty array causes the algorithm to deoptimize and run 50% slower!*/], _nextBeginningIndexes: null, score: null, indexes: null, obj: null} // hidden - return {target:target, _targetLowerCodes:fuzzysort.prepareLowerCodes(target), _nextBeginningIndexes:fuzzysort.prepareNextBeginningIndexes(target), score:null, indexes:null, obj:null} // hidden - }, - prepareSearch: function(search) { - if(!search) search = '' - return fuzzysort.prepareLowerCodes(search) - }, - - - - // Below this point is only internal code - // Below this point is only internal code - // Below this point is only internal code - // Below this point is only internal code - - - - getPrepared: function(target) { - if(target.length > 999) return fuzzysort.prepare(target) // don't cache huge targets - var targetPrepared = preparedCache.get(target) - if(targetPrepared !== undefined) return targetPrepared - targetPrepared = fuzzysort.prepare(target) - preparedCache.set(target, targetPrepared) - return targetPrepared - }, - getPreparedSearch: function(search) { - if(search.length > 999) return fuzzysort.prepareSearch(search) // don't cache huge searches - var searchPrepared = preparedSearchCache.get(search) - if(searchPrepared !== undefined) return searchPrepared - searchPrepared = fuzzysort.prepareSearch(search) - preparedSearchCache.set(search, searchPrepared) - return searchPrepared - }, - - algorithm: function(searchLowerCodes, prepared, searchLowerCode) { - var targetLowerCodes = prepared._targetLowerCodes - var searchLen = searchLowerCodes.length - var targetLen = targetLowerCodes.length - var searchI = 0 // where we at - var targetI = 0 // where you at - var typoSimpleI = 0 - var matchesSimpleLen = 0 - - // very basic fuzzy match; to remove non-matching targets ASAP! - // walk through target. find sequential matches. - // if all chars aren't found then exit - for(;;) { - var isMatch = searchLowerCode === targetLowerCodes[targetI] - if(isMatch) { - matchesSimple[matchesSimpleLen++] = targetI - ++searchI; if(searchI === searchLen) break - searchLowerCode = searchLowerCodes[typoSimpleI===0?searchI : (typoSimpleI===searchI?searchI+1 : (typoSimpleI===searchI-1?searchI-1 : searchI))] - } - - ++targetI; if(targetI >= targetLen) { // Failed to find searchI - // Check for typo or exit - // we go as far as possible before trying to transpose - // then we transpose backwards until we reach the beginning - for(;;) { - if(searchI <= 1) return null // not allowed to transpose first char - if(typoSimpleI === 0) { // we haven't tried to transpose yet - --searchI - var searchLowerCodeNew = searchLowerCodes[searchI] - if(searchLowerCode === searchLowerCodeNew) continue // doesn't make sense to transpose a repeat char - typoSimpleI = searchI - } else { - if(typoSimpleI === 1) return null // reached the end of the line for transposing - --typoSimpleI - searchI = typoSimpleI - searchLowerCode = searchLowerCodes[searchI + 1] - var searchLowerCodeNew = searchLowerCodes[searchI] - if(searchLowerCode === searchLowerCodeNew) continue // doesn't make sense to transpose a repeat char - } - matchesSimpleLen = searchI - targetI = matchesSimple[matchesSimpleLen - 1] + 1 - break - } - } - } - - var searchI = 0 - var typoStrictI = 0 - var successStrict = false - var matchesStrictLen = 0 - - var nextBeginningIndexes = prepared._nextBeginningIndexes - if(nextBeginningIndexes === null) nextBeginningIndexes = prepared._nextBeginningIndexes = fuzzysort.prepareNextBeginningIndexes(prepared.target) - var firstPossibleI = targetI = matchesSimple[0]===0 ? 0 : nextBeginningIndexes[matchesSimple[0]-1] - - // Our target string successfully matched all characters in sequence! - // Let's try a more advanced and strict test to improve the score - // only count it as a match if it's consecutive or a beginning character! - if(targetI !== targetLen) for(;;) { - if(targetI >= targetLen) { - // We failed to find a good spot for this search char, go back to the previous search char and force it forward - if(searchI <= 0) { // We failed to push chars forward for a better match - // transpose, starting from the beginning - ++typoStrictI; if(typoStrictI > searchLen-2) break - if(searchLowerCodes[typoStrictI] === searchLowerCodes[typoStrictI+1]) continue // doesn't make sense to transpose a repeat char - targetI = firstPossibleI - continue - } - - --searchI - var lastMatch = matchesStrict[--matchesStrictLen] - targetI = nextBeginningIndexes[lastMatch] - - } else { - var isMatch = searchLowerCodes[typoStrictI===0?searchI : (typoStrictI===searchI?searchI+1 : (typoStrictI===searchI-1?searchI-1 : searchI))] === targetLowerCodes[targetI] - if(isMatch) { - matchesStrict[matchesStrictLen++] = targetI - ++searchI; if(searchI === searchLen) { successStrict = true; break } - ++targetI - } else { - targetI = nextBeginningIndexes[targetI] - } - } - } - - { // tally up the score & keep track of matches for highlighting later - if(successStrict) { var matchesBest = matchesStrict; var matchesBestLen = matchesStrictLen } - else { var matchesBest = matchesSimple; var matchesBestLen = matchesSimpleLen } - var score = 0 - var lastTargetI = -1 - for(var i = 0; i < searchLen; ++i) { var targetI = matchesBest[i] - // score only goes down if they're not consecutive - if(lastTargetI !== targetI - 1) score -= targetI - lastTargetI = targetI - } - if(!successStrict) { - score *= 1000 - if(typoSimpleI !== 0) score += -20/*typoPenalty*/ - } else { - if(typoStrictI !== 0) score += -20/*typoPenalty*/ - } - score -= targetLen - searchLen - prepared.score = score - prepared.indexes = new Array(matchesBestLen); for(var i = matchesBestLen - 1; i >= 0; --i) prepared.indexes[i] = matchesBest[i] - - return prepared - } - }, - - algorithmNoTypo: function(searchLowerCodes, prepared, searchLowerCode) { - var targetLowerCodes = prepared._targetLowerCodes - var searchLen = searchLowerCodes.length - var targetLen = targetLowerCodes.length - var searchI = 0 // where we at - var targetI = 0 // where you at - var matchesSimpleLen = 0 - - // very basic fuzzy match; to remove non-matching targets ASAP! - // walk through target. find sequential matches. - // if all chars aren't found then exit - for(;;) { - var isMatch = searchLowerCode === targetLowerCodes[targetI] - if(isMatch) { - matchesSimple[matchesSimpleLen++] = targetI - ++searchI; if(searchI === searchLen) break - searchLowerCode = searchLowerCodes[searchI] - } - ++targetI; if(targetI >= targetLen) return null // Failed to find searchI - } - - var searchI = 0 - var successStrict = false - var matchesStrictLen = 0 - - var nextBeginningIndexes = prepared._nextBeginningIndexes - if(nextBeginningIndexes === null) nextBeginningIndexes = prepared._nextBeginningIndexes = fuzzysort.prepareNextBeginningIndexes(prepared.target) - var firstPossibleI = targetI = matchesSimple[0]===0 ? 0 : nextBeginningIndexes[matchesSimple[0]-1] - - // Our target string successfully matched all characters in sequence! - // Let's try a more advanced and strict test to improve the score - // only count it as a match if it's consecutive or a beginning character! - if(targetI !== targetLen) for(;;) { - if(targetI >= targetLen) { - // We failed to find a good spot for this search char, go back to the previous search char and force it forward - if(searchI <= 0) break // We failed to push chars forward for a better match - - --searchI - var lastMatch = matchesStrict[--matchesStrictLen] - targetI = nextBeginningIndexes[lastMatch] - - } else { - var isMatch = searchLowerCodes[searchI] === targetLowerCodes[targetI] - if(isMatch) { - matchesStrict[matchesStrictLen++] = targetI - ++searchI; if(searchI === searchLen) { successStrict = true; break } - ++targetI - } else { - targetI = nextBeginningIndexes[targetI] - } - } - } - - { // tally up the score & keep track of matches for highlighting later - if(successStrict) { var matchesBest = matchesStrict; var matchesBestLen = matchesStrictLen } - else { var matchesBest = matchesSimple; var matchesBestLen = matchesSimpleLen } - var score = 0 - var lastTargetI = -1 - for(var i = 0; i < searchLen; ++i) { var targetI = matchesBest[i] - // score only goes down if they're not consecutive - if(lastTargetI !== targetI - 1) score -= targetI - lastTargetI = targetI - } - if(!successStrict) score *= 1000 - score -= targetLen - searchLen - prepared.score = score - prepared.indexes = new Array(matchesBestLen); for(var i = matchesBestLen - 1; i >= 0; --i) prepared.indexes[i] = matchesBest[i] - - return prepared - } - }, - - prepareLowerCodes: function(str) { - var strLen = str.length - var lowerCodes = [] // new Array(strLen) sparse array is too slow - var lower = str.toLowerCase() - for(var i = 0; i < strLen; ++i) lowerCodes[i] = lower.charCodeAt(i) - return lowerCodes - }, - prepareBeginningIndexes: function(target) { - var targetLen = target.length - var beginningIndexes = []; var beginningIndexesLen = 0 - var wasUpper = false - var wasAlphanum = false - for(var i = 0; i < targetLen; ++i) { - var targetCode = target.charCodeAt(i) - var isUpper = targetCode>=65&&targetCode<=90 - var isAlphanum = isUpper || targetCode>=97&&targetCode<=122 || targetCode>=48&&targetCode<=57 - var isBeginning = isUpper && !wasUpper || !wasAlphanum || !isAlphanum - wasUpper = isUpper - wasAlphanum = isAlphanum - if(isBeginning) beginningIndexes[beginningIndexesLen++] = i - } - return beginningIndexes - }, - prepareNextBeginningIndexes: function(target) { - var targetLen = target.length - var beginningIndexes = fuzzysort.prepareBeginningIndexes(target) - var nextBeginningIndexes = [] // new Array(targetLen) sparse array is too slow - var lastIsBeginning = beginningIndexes[0] - var lastIsBeginningI = 0 - for(var i = 0; i < targetLen; ++i) { - if(lastIsBeginning > i) { - nextBeginningIndexes[i] = lastIsBeginning - } else { - lastIsBeginning = beginningIndexes[++lastIsBeginningI] - nextBeginningIndexes[i] = lastIsBeginning===undefined ? targetLen : lastIsBeginning - } - } - return nextBeginningIndexes - }, - - cleanup: cleanup, - new: fuzzysortNew, - } - return fuzzysort -} // fuzzysortNew - -// This stuff is outside fuzzysortNew, because it's shared with instances of fuzzysort.new() -var isNode = typeof require !== 'undefined' && typeof window === 'undefined' -var MyMap = Map||function(){var s=Object.create(null);this.get=function(k){return s[k]};this.set=function(k,val){s[k]=val;return this};this.clear=function(){s=Object.create(null)}} -var preparedCache = new MyMap() -var preparedSearchCache = new MyMap() -var noResults = []; noResults.total = 0 -var matchesSimple = []; var matchesStrict = [] -function cleanup() { preparedCache.clear(); preparedSearchCache.clear(); matchesSimple = []; matchesStrict = [] } -function defaultScoreFn(a) { - var max = -9007199254740991 - for (var i = a.length - 1; i >= 0; --i) { - var result = a[i]; if(result === null) continue - var score = result.score - if(score > max) max = score - } - if(max === -9007199254740991) return null - return max -} - -// prop = 'key' 2.5ms optimized for this case, seems to be about as fast as direct obj[prop] -// prop = 'key1.key2' 10ms -// prop = ['key1', 'key2'] 27ms -function getValue(obj, prop) { - var tmp = obj[prop]; if(tmp !== undefined) return tmp - var segs = prop - if(!Array.isArray(prop)) segs = prop.split('.') - var len = segs.length - var i = -1 - while (obj && (++i < len)) obj = obj[segs[i]] - return obj -} - -function isObj(x) { return typeof x === 'object' } // faster as a function - -// Hacked version of https://github.com/lemire/FastPriorityQueue.js -var fastpriorityqueue=function(){var r=[],o=0,e={};function n(){for(var e=0,n=r[e],c=1;c<o;){var f=c+1;e=c,f<o&&r[f].score<r[c].score&&(e=f),r[e-1>>1]=r[e],c=1+(e<<1)}for(var a=e-1>>1;e>0&&n.score<r[a].score;a=(e=a)-1>>1)r[e]=r[a];r[e]=n}return e.add=function(e){var n=o;r[o++]=e;for(var c=n-1>>1;n>0&&e.score<r[c].score;c=(n=c)-1>>1)r[n]=r[c];r[n]=e},e.poll=function(){if(0!==o){var e=r[0];return r[0]=r[--o],n(),e}},e.peek=function(e){if(0!==o)return r[0]},e.replaceTop=function(o){r[0]=o,n()},e}; -var q = fastpriorityqueue() // reuse this, except for async, it needs to make its own - -return fuzzysortNew() -}) // UMD - -// TODO: (performance) wasm version!? -// TODO: (performance) threads? -// TODO: (performance) avoid cache misses -// TODO: (performance) preparedCache is a memory leak -// TODO: (like sublime) backslash === forwardslash -// TODO: (like sublime) spaces: "a b" should do 2 searches 1 for a and 1 for b -// TODO: (scoring) garbage in targets that allows most searches to strict match need a penality -// TODO: (performance) idk if allowTypo is optimized diff --git a/pydis_site/static/js/resources/resources.js b/pydis_site/static/js/resources/resources.js deleted file mode 100644 index d6cc8128..00000000 --- a/pydis_site/static/js/resources/resources.js +++ /dev/null @@ -1,367 +0,0 @@ -"use strict"; - -// Filters that are currently selected -var activeFilters = { - topics: [], - type: [], - "payment-tiers": [], - difficulty: [] -}; - -// Options for fuzzysort -const fuzzysortOptions = { - allowTypo: true, // Allow our users to make typos - titleThreshold: -10000, // The threshold for the fuzziness on title matches. Closer to 0 is stricter. - descriptionThreshold: -500, // The threshold for the fuzziness on description matches. -}; - -/* Add a filter, and update the UI */ -function addFilter(filterName, filterItem) { - var filterIndex = activeFilters[filterName].indexOf(filterItem); - if (filterIndex === -1) { - activeFilters[filterName].push(filterItem); - } - updateUI(); -} - -/* Remove all filters, and update the UI */ -function removeAllFilters() { - activeFilters = { - topics: [], - type: [], - "payment-tiers": [], - difficulty: [] - }; - $("#resource-search input").val(""); - updateUI(); -} - -/* Remove a filter, and update the UI */ -function removeFilter(filterName, filterItem) { - var filterIndex = activeFilters[filterName].indexOf(filterItem); - if (filterIndex !== -1) { - activeFilters[filterName].splice(filterIndex, 1); - } - updateUI(); -} - -/* Check if there are no filters */ -function noFilters() { - return ( - activeFilters.topics.length === 0 && - activeFilters.type.length === 0 && - activeFilters["payment-tiers"].length === 0 && - activeFilters.difficulty.length === 0 - ); -} - -/* Get the params out of the URL and use them. This is run when the page loads. */ -function deserializeURLParams() { - let searchParams = new window.URLSearchParams(window.location.search); - - // Add the search query to the search bar. - if (searchParams.has("search")) { - let searchQuery = searchParams.get("search"); - $("#resource-search input").val(searchQuery); - $(".close-filters-button").show(); - } - - // Work through the parameters and add them to the filter object - $.each(Object.keys(activeFilters), function(_, filterType) { - let paramFilterContent = searchParams.get(filterType); - - if (paramFilterContent !== null) { - // We use split here because we always want an array, not a string. - let paramFilterArray = paramFilterContent.split(","); - - // Update the corresponding filter UI, so it reflects the internal state. - let filterAdded = false; - $(paramFilterArray).each(function(_, filter) { - // Catch special cases. - if (String(filter) === "rickroll" && filterType === "type") { - window.location.href = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"; - } else if (String(filter) === "sneakers" && filterType === "topics") { - window.location.href = "https://www.youtube.com/watch?v=NNZscmNE9QI"; - - // If the filter is valid, mirror it to the UI. - } else if (validFilters.hasOwnProperty(filterType) && validFilters[filterType].includes(String(filter))) { - let checkbox = $(`.filter-checkbox[data-filter-name='${filterType}'][data-filter-item='${filter}']`); - let filterTag = $(`.filter-box-tag[data-filter-name='${filterType}'][data-filter-item='${filter}']`); - let resourceTags = $(`.resource-tag[data-filter-name='${filterType}'][data-filter-item='${filter}']`); - checkbox.prop("checked", true); - filterTag.show(); - resourceTags.addClass("active"); - activeFilters[filterType].push(filter); - filterAdded = true; - } - }); - - // Ditch all the params from the URL, and recalculate the URL params - updateURL(); - - // If we've added a filter, hide stuff - if (filterAdded) { - $(".no-tags-selected.tag").hide(); - $(".close-filters-button").show(); - } - } - }); -} - -/* Show or hide the duckies, depending on whether or not there are any resources visible. */ -function updateDuckies() { - let visibleResources = Boolean($(".resource-box:visible").length); - if (!visibleResources) { - $(".no-resources-found").show(); - } else { - $(".no-resources-found").hide(); - } -} - - -/* Update the URL with new parameters */ -function updateURL() { - let searchQuery = $("#resource-search input").val(); - - // If there's no active filtering parameters, we can return early. - if (noFilters() && searchQuery.length === 0) { - window.history.replaceState(null, document.title, './'); - return; - } - - // Iterate through and get rid of empty ones - let searchParams = new URLSearchParams(activeFilters); - $.each(activeFilters, function(filterType, filters) { - if (filters.length === 0) { - searchParams.delete(filterType); - } - }); - - // Add the search query, if necessary. - if (searchQuery.length > 0) { - searchParams.set("search", searchQuery); - } - - // Now update the URL - window.history.replaceState(null, document.title, `?${searchParams.toString()}`); -} - -/* Apply search terms */ -function filterBySearch(resourceItems) { - let searchQuery = $("#resource-search input").val(); - - /* Show and update the tag if there's a search query */ - if (searchQuery) { - let tag = $(".tag.search-query"); - let tagText = $(".tag.search-query span"); - tagText.text(`Search: ${searchQuery}`); - tag.show(); - $(".close-filters-button").show(); - } - - resourceItems.filter(function() { - // Get the resource title and description - let title = $(this).attr("data-resource-name"); - let description = $(this).find("p").text(); - - // Run a fuzzy search. Does the title or description match the query? - let titleMatch = fuzzysort.single(searchQuery, title, fuzzysortOptions); - titleMatch = Boolean(titleMatch) && titleMatch.score > fuzzysortOptions.titleThreshold; - - let descriptionMatch = fuzzysort.single(searchQuery, description, fuzzysortOptions); - descriptionMatch = Boolean(descriptionMatch) && descriptionMatch.score > fuzzysortOptions.descriptionThreshold; - - return titleMatch || descriptionMatch; - }).show(); -} - -/* Update the resources to match 'active_filters' */ -function updateUI() { - let resources = $('.resource-box'); - let filterTags = $('.filter-box-tag'); - let resourceTags = $('.resource-tag'); - let noTagsSelected = $(".no-tags-selected.tag"); - let closeFiltersButton = $(".close-filters-button"); - let searchQuery = $("#resource-search input").val(); - let searchTag = $(".tag.search-query"); - - // Update the URL to match the new filters. - updateURL(); - - // If there's nothing in the filters, we can return early. - if (noFilters()) { - // If we have a searchQuery, we need to run all resources through a search. - if (searchQuery.length > 0) { - resources.hide(); - noTagsSelected.hide(); - filterBySearch(resources); - } else { - resources.show(); - noTagsSelected.show(); - closeFiltersButton.hide(); - $(".tag.search-query").hide(); - } - - filterTags.hide(); - resourceTags.removeClass("active"); - $(`.filter-checkbox:checked`).prop("checked", false); - updateDuckies(); - - return; - } else { - // Hide everything - $('.filter-box-tag').hide(); - $('.resource-tag').removeClass("active"); - noTagsSelected.show(); - closeFiltersButton.hide(); - - // Now conditionally show the stuff we want - $.each(activeFilters, function(filterType, filters) { - $.each(filters, function(index, filter) { - // Show a corresponding filter box tag - $(`.filter-box-tag[data-filter-name=${filterType}][data-filter-item=${filter}]`).show(); - - // Make corresponding resource tags active - $(`.resource-tag[data-filter-name=${filterType}][data-filter-item=${filter}]`).addClass("active"); - - // Hide the "No filters selected" tag. - noTagsSelected.hide(); - - // Show the close filters button - closeFiltersButton.show(); - }); - }); - } - - // Otherwise, hide everything and then filter the resources to decide what to show. - resources.hide(); - let filteredResources = resources.filter(function() { - let validation = { - topics: false, - type: false, - 'payment-tiers': false, - difficulty: false - }; - let resourceBox = $(this); - - // Validate the filters - $.each(activeFilters, function(filterType, filters) { - // If the filter list is empty, this passes validation. - if (filters.length === 0) { - validation[filterType] = true; - return; - } - - // Otherwise, we need to check if one of the classes exist. - $.each(filters, function(index, filter) { - if (resourceBox.hasClass(`${filterType}-${filter}`)) { - validation[filterType] = true; - } - }); - }); - - // If validation passes, show the resource. - if (Object.values(validation).every(Boolean)) { - return true; - } else { - return false; - } - }); - - // Run the items we've found through the search filter, if necessary. - if (searchQuery.length > 0) { - filterBySearch(filteredResources); - } else { - filteredResources.show(); - searchTag.hide(); - } - - // Gotta update those duckies! - updateDuckies(); -} - -// Executed when the page has finished loading. -document.addEventListener("DOMContentLoaded", function () { - /* Check if the user has navigated to one of the old resource pages, - like pydis.com/resources/communities. In this case, we'll rewrite - the URL before we do anything else. */ - let resourceTypeInput = $("#resource-type-input").val(); - if (resourceTypeInput !== "None") { - window.history.replaceState(null, document.title, `../?type=${resourceTypeInput}`); - } - - // Update the filters on page load to reflect URL parameters. - $('.filter-box-tag').hide(); - deserializeURLParams(); - updateUI(); - - // If this is a mobile device, collapse all the categories to win back some screen real estate. - if (screen.width < 480) { - let categoryHeaders = $(".filter-category-header .collapsible-content"); - let icons = $('.filter-category-header button .card-header-icon i'); - categoryHeaders.addClass("no-transition collapsed"); - icons.removeClass(["far", "fa-window-minimize"]); - icons.addClass(["fas", "fa-angle-down"]); - - // Wait 10ms before removing this class, or else the transition will animate due to a race condition. - setTimeout(() => { categoryHeaders.removeClass("no-transition"); }, 10); - } - - // When you type into the search bar, trigger an UI update. - $("#resource-search input").on("input", function() { - updateUI(); - }); - - // If you click on the div surrounding the filter checkbox, it clicks the corresponding checkbox. - $('.filter-panel').on("click",function(event) { - let hitsCheckbox = Boolean(String(event.target)); - - if (!hitsCheckbox) { - let checkbox = $(this).find(".filter-checkbox"); - checkbox.prop("checked", !checkbox.prop("checked")); - checkbox.trigger("change"); - } - }); - - // If you click on one of the tags in the filter box, it unchecks the corresponding checkbox. - $('.filter-box-tag').on("click", function() { - let filterItem = this.dataset.filterItem; - let filterName = this.dataset.filterName; - let checkbox = $(`.filter-checkbox[data-filter-name='${filterName}'][data-filter-item='${filterItem}']`); - - removeFilter(filterName, filterItem); - checkbox.prop("checked", false); - }); - - // If you click on one of the tags in the resource cards, it clicks the corresponding checkbox. - $('.resource-tag').on("click", function() { - let filterItem = this.dataset.filterItem; - let filterName = this.dataset.filterName; - let checkbox = $(`.filter-checkbox[data-filter-name='${filterName}'][data-filter-item='${filterItem}']`); - - if (!$(this).hasClass("active")) { - addFilter(filterName, filterItem); - checkbox.prop("checked", true); - } else { - removeFilter(filterName, filterItem); - checkbox.prop("checked", false); - } - }); - - // When you click the little gray x, remove all filters. - $(".close-filters-button").on("click", function() { - removeAllFilters(); - }); - - // When checkboxes are toggled, trigger a filter update. - $('.filter-checkbox').on("change", function (event) { - let filterItem = this.dataset.filterItem; - let filterName = this.dataset.filterName; - - if (this.checked && !activeFilters[filterName].includes(filterItem)) { - addFilter(filterName, filterItem); - } else if (!this.checked && activeFilters[filterName].includes(filterItem)) { - removeFilter(filterName, filterItem); - } - }); -}); diff --git a/pydis_site/static/js/timeline/main.js b/pydis_site/static/js/timeline/main.js deleted file mode 100644 index 2ff7df57..00000000 --- a/pydis_site/static/js/timeline/main.js +++ /dev/null @@ -1,104 +0,0 @@ -(function(){ - // Vertical Timeline - by CodyHouse.co (modified) - function VerticalTimeline( element ) { - this.element = element; - this.blocks = this.element.getElementsByClassName("cd-timeline__block"); - this.images = this.element.getElementsByClassName("cd-timeline__img"); - this.contents = this.element.getElementsByClassName("cd-timeline__content"); - this.offset = 0.8; - this.hideBlocks(); - }; - - VerticalTimeline.prototype.hideBlocks = function() { - if ( !"classList" in document.documentElement ) { - return; // no animation on older browsers - } - //hide timeline blocks which are outside the viewport - var self = this; - for( var i = 0; i < this.blocks.length; i++) { - (function(i){ - if( self.blocks[i].getBoundingClientRect().top > window.innerHeight*self.offset ) { - self.images[i].classList.add("cd-timeline__img--hidden"); - self.contents[i].classList.add("cd-timeline__content--hidden"); - } - })(i); - } - }; - - VerticalTimeline.prototype.showBlocks = function() { - if ( ! "classList" in document.documentElement ) { - return; - } - var self = this; - for( var i = 0; i < this.blocks.length; i++) { - (function(i){ - if((self.contents[i].classList.contains("cd-timeline__content--hidden") || self.contents[i].classList.contains("cd-timeline__content--bounce-out")) && self.blocks[i].getBoundingClientRect().top <= window.innerHeight*self.offset ) { - // add bounce-in animation - self.images[i].classList.add("cd-timeline__img--bounce-in"); - self.contents[i].classList.add("cd-timeline__content--bounce-in"); - self.images[i].classList.remove("cd-timeline__img--hidden"); - self.contents[i].classList.remove("cd-timeline__content--hidden"); - self.images[i].classList.remove("cd-timeline__img--bounce-out"); - self.contents[i].classList.remove("cd-timeline__content--bounce-out"); - } - })(i); - } - }; - - VerticalTimeline.prototype.hideBlocksScroll = function () { - if ( ! "classList" in document.documentElement ) { - return; - } - var self = this; - for( var i = 0; i < this.blocks.length; i++) { - (function(i){ - if(self.contents[i].classList.contains("cd-timeline__content--bounce-in") && self.blocks[i].getBoundingClientRect().top > window.innerHeight*self.offset ) { - self.images[i].classList.remove("cd-timeline__img--bounce-in"); - self.contents[i].classList.remove("cd-timeline__content--bounce-in"); - self.images[i].classList.add("cd-timeline__img--bounce-out"); - self.contents[i].classList.add("cd-timeline__content--bounce-out"); - } - })(i); - } - } - - var verticalTimelines = document.getElementsByClassName("js-cd-timeline"), - verticalTimelinesArray = [], - scrolling = false; - if( verticalTimelines.length > 0 ) { - for( var i = 0; i < verticalTimelines.length; i++) { - (function(i){ - verticalTimelinesArray.push(new VerticalTimeline(verticalTimelines[i])); - })(i); - } - - //show timeline blocks on scrolling - window.addEventListener("scroll", function(event) { - if( !scrolling ) { - scrolling = true; - (!window.requestAnimationFrame) ? setTimeout(checkTimelineScroll, 250) : window.requestAnimationFrame(checkTimelineScroll); - } - }); - - function animationEnd(event) { - if (event.target.classList.contains("cd-timeline__img--bounce-out")) { - event.target.classList.add("cd-timeline__img--hidden"); - event.target.classList.remove("cd-timeline__img--bounce-out"); - } else if (event.target.classList.contains("cd-timeline__content--bounce-out")) { - event.target.classList.add("cd-timeline__content--hidden"); - event.target.classList.remove("cd-timeline__content--bounce-out"); - } - } - - window.addEventListener("animationend", animationEnd); - window.addEventListener("webkitAnimationEnd", animationEnd); - } - - function checkTimelineScroll() { - verticalTimelinesArray.forEach(function(timeline){ - timeline.showBlocks(); - timeline.hideBlocksScroll(); - }); - scrolling = false; - }; -})(); diff --git a/pydis_site/templates/404.html b/pydis_site/templates/404.html deleted file mode 100644 index 42e317d2..00000000 --- a/pydis_site/templates/404.html +++ /dev/null @@ -1,34 +0,0 @@ -{% load static %} - -<!DOCTYPE html> -<html lang="en"> - -<head> - <title>Python Discord | 404</title> - - <meta charset="UTF-8"> - - <link rel="preconnect" href="https://fonts.gstatic.com"> - <link href="https://fonts.googleapis.com/css2?family=Hind:wght@400;600&display=swap" rel="stylesheet"> - <link rel="stylesheet" href="{% static "css/error_pages.css" %}"> -</head> - -<body> - <div class="error-box"> - <div class="logo-box"> - <img src="https://raw.githubusercontent.com/python-discord/branding/b67897df93e572c1576a9026eb78c785a794d226/logos/logo_banner/logo_site_banner.svg" - alt="Python Discord banner" /> - </div> - <div class="content-box"> - <h1>404 — Not Found</h1> - <p>We couldn't find the page you're looking for. Here are a few things to try out:</p> - <ul> - <li>Double check the URL. Are you sure you typed it out correctly? - <li>Come join <a href="https://discord.gg/python">our Discord Server</a>. Maybe we can help you out over - there - </ul> - </div> - </div> -</body> - -</html> diff --git a/pydis_site/templates/500.html b/pydis_site/templates/500.html deleted file mode 100644 index 869892ec..00000000 --- a/pydis_site/templates/500.html +++ /dev/null @@ -1,29 +0,0 @@ -{% load static %} - -<!DOCTYPE html> -<html lang="en"> - -<head> - <title>Python Discord | 500</title> - - <meta charset="UTF-8"> - - <link rel="preconnect" href="https://fonts.gstatic.com"> - <link href="https://fonts.googleapis.com/css2?family=Hind:wght@400;600&display=swap" rel="stylesheet"> - <link rel="stylesheet" href="{% static "css/error_pages.css" %}"> -</head> - -<body> - <div class="error-box"> - <div class="logo-box"> - <img src="https://raw.githubusercontent.com/python-discord/branding/b67897df93e572c1576a9026eb78c785a794d226/logos/logo_banner/logo_site_banner.svg" - alt="Python Discord banner" /> - </div> - <div class="content-box"> - <h1>500 — Internal Server Error</h1> - <p>Something went wrong at our end. Please try again shortly, or if the problem persists, please let us know <a href="https://discord.gg/python">on Discord</a>.</p> - </div> - </div> -</body> - -</html> diff --git a/pydis_site/templates/base/base.html b/pydis_site/templates/base/base.html deleted file mode 100644 index b7322f12..00000000 --- a/pydis_site/templates/base/base.html +++ /dev/null @@ -1,46 +0,0 @@ -{# Base template, with a few basic style definitions. #} -{% load django_simple_bulma %} -{% load static %} - -<!DOCTYPE html> -<html lang="en"> -<head> - <meta charset="UTF-8"> - <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> - <meta name="description" - content="{% block meta-description %}We're a large, friendly community focused around the Python programming language. Our community is open to those who wish to learn the language, as well as those looking to help others.{% endblock %}"> - - {# Generated with https://realfavicongenerator.net/ #} - <link rel="apple-touch-icon" sizes="180x180" href="/static/favicons/apple-touch-icon.png?v=9B9vkE5q7O"> - <link rel="icon" type="image/png" sizes="32x32" href="/static/favicons/favicon-32x32.png?v=9B9vkE5q7O"> - <link rel="icon" type="image/png" sizes="16x16" href="/static/favicons/favicon-16x16.png?v=9B9vkE5q7O"> - <link rel="manifest" href="/static/favicons/site.webmanifest?v=9B9vkE5q7O"> - <link rel="mask-icon" href="/static/favicons/safari-pinned-tab.svg?v=9B9vkE5q7O" color="#7289da"> - <link rel="shortcut icon" href="/static/favicons/favicon.ico?v=9B9vkE5q7O"> - <meta name="msapplication-TileColor" content="#7289da"> - <meta name="msapplication-config" content="/static/favicons/browserconfig.xml?v=9B9vkE5q7O"> - <meta name="theme-color" content="#7289da"> - - <title>Python Discord | {% block title %}Website{% endblock %}</title> - - {% bulma %} - {% font_awesome %} - <link rel="stylesheet" href="{% static "css/base/base.css" %}"> - {% block head %}{% endblock %} - -</head> -<body class="site"> - <!-- Git hash for this release: {{ git_sha }} --> - -<main class="site-content"> - {% block content %} - {{ block.super }} - {% endblock %} -</main> - -{% block site_footer %} - {% include "base/footer.html" %} -{% endblock %} - -</body> -</html> diff --git a/pydis_site/templates/base/footer.html b/pydis_site/templates/base/footer.html deleted file mode 100644 index 0bc93578..00000000 --- a/pydis_site/templates/base/footer.html +++ /dev/null @@ -1,7 +0,0 @@ -<footer class="footer has-background-dark has-text-light"> - <div class="content has-text-centered"> - <p> - Powered by <a href="https://www.linode.com/?r=3bc18ce876ff43ea31f201b91e8e119c9753f085"><span id="linode-logo">Linode</span></a> and <a href="https://www.netcup.eu/"><span id="netcup-logo">netcup</span></a><br>Built with <a href="https://www.djangoproject.com/"><span id="django-logo">django</span></a> and <a href="https://bulma.io"><span id="bulma-logo">Bulma</span></a> <br/> © {% now "Y" %} <span id="pydis-text">Python Discord</span> - </p> - </div> -</footer> diff --git a/pydis_site/templates/base/navbar.html b/pydis_site/templates/base/navbar.html deleted file mode 100644 index 931693c8..00000000 --- a/pydis_site/templates/base/navbar.html +++ /dev/null @@ -1,108 +0,0 @@ -{% load static %} - -<nav class="navbar is-primary" role="navigation" aria-label="main navigation"> - <div class="navbar-brand"> - - <a id="navbar-banner" class="navbar-item has-left-margin-1" href="/"> - <img src="{% static "images/navbar/pydis_banner_no_square.svg" %}" alt="Python Discord logo"/> - </a> - - {# The navbar "burger" which appears when rendered on screens that can't fit the entire nav #} - <a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbar_menu"> - <span aria-hidden="true"></span> - <span aria-hidden="true"></span> - <span aria-hidden="true"></span> - </a> - </div> - - {# Content on the right side of the navbar #} - <div class="navbar-menu is-paddingless" id="navbar_menu"> - <div class="navbar-end"> - - {# Burger-menu Discord #} - <a class="navbar-item is-hidden-desktop" href="https://discord.gg/python"> - <span class="icon is-size-4 is-medium"><i class="fab fa-discord"></i></span> - <span> Discord</span> - </a> - - {# GitHub #} - <a class="navbar-item" href="https://github.com/python-discord"> - <span class="icon is-size-4 is-medium"><i class="fab fa-github"></i></span> - <span> GitHub</span> - </a> - - {# Reddit #} - <a class="navbar-item" href="https://reddit.com/r/python"> - <span class="icon is-size-4 is-medium"><i class="fab fa-reddit-alien"></i></span> - <span> Reddit</span> - </a> - - {# YouTube #} - <a class="navbar-item" href="https://youtube.com/pythondiscord"> - <span class="icon is-size-4 is-medium"><i class="fab fa-youtube"></i></span> - <span> YouTube</span> - </a> - - {# Patreon #} - <a class="navbar-item" href="https://patreon.com/python_discord"> - <span class="icon is-size-4 is-medium"><i class="fab fa-patreon"></i></span> - <span> Patreon</span> - </a> - - {# Merch #} - <a class="navbar-item" href="https://www.redbubble.com/people/PythonDiscord/shop"> - <span class="icon is-size-4 is-medium"><i class="fas fa-tshirt"></i></span> - <span> Merch</span> - </a> - - {# More #} - <div class="navbar-item has-dropdown is-hoverable"> - <span class="navbar-link is-hidden-touch"> - More - </span> - <span class="navbar-link is-arrowless is-hidden-desktop"> - More - </span> - <div class="navbar-dropdown"> - <a class="navbar-item" href="{% url "resources:index" %}"> - Resources - </a> - <a class="navbar-item" href="{% url "content:pages" %}"> - Content - </a> - <a class="navbar-item" href="{% url "events:index" %}"> - Events - </a> - <a class="navbar-item" href="{% url "content:page_category" location="guides/pydis-guides/contributing"%}"> - Contributing - </a> - <a class="navbar-item" href="{% url "content:page_category" location="frequently-asked-questions" %}"> - FAQ - </a> - <a class="navbar-item" href="{% url "content:page_category" location="guides" %}"> - Guides - </a> - <a class="navbar-item" href="{% url 'home:timeline' %}"> - Timeline - </a> - <a class="navbar-item" href="{% url "content:page_category" location="rules" %}"> - Rules - </a> - <a class="navbar-item" href="{% url "content:page_category" location="code-of-conduct" %}"> - Code of Conduct - </a> - <a class="navbar-item" href="{% url "content:page_category" location="privacy" %}"> - Privacy - </a> - </div> - </div> - - {# Desktop Nav Discord #} - <div id="discord-btn" class="buttons is-hidden-touch"> - <a href="https://discord.gg/python" class="button is-large is-primary">Discord</a> - </div> - - </div> - - </div> -</nav> diff --git a/pydis_site/templates/content/base.html b/pydis_site/templates/content/base.html deleted file mode 100644 index 2fd721a3..00000000 --- a/pydis_site/templates/content/base.html +++ /dev/null @@ -1,47 +0,0 @@ -{% extends 'base/base.html' %} -{% load static %} - -{% block title %}{{ page_title }}{% endblock %} -{% block head %} - <meta property="og:title" content="Python Discord - {{ page_title }}" /> - <meta property="og:type" content="website" /> - <meta property="og:description" content="{{ page_description }}" /> - <link rel="stylesheet" href="{% static "css/content/page.css" %}"> - <link rel="stylesheet" href="{% static "css/collapsibles.css" %}"> - <link rel="stylesheet" - href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.7.1/styles/atom-one-dark.min.css"> - <script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.7.1/highlight.min.js"></script> - <script>hljs.highlightAll();</script> - <script src="{% static "js/collapsibles.js" %}"></script> -{% endblock %} - -{% block content %} - {% include "base/navbar.html" %} - - <section class="breadcrumb-section section"> - <div class="container"> - {# Article breadcrumb #} - <nav class="breadcrumb is-pulled-left" aria-label="breadcrumbs"> - <ul> - {% for item in breadcrumb_items %} - <li><a href="{% url "content:page_category" location=item.path %}">{{ item.name }}</a></li> - {% endfor %} - <li class="is-active"><a href="#">{{ page_title }}</a></li> - </ul> - </nav> - {# Sub-Article dropdown for category pages #} - {% if subarticles %} - {% include "content/dropdown.html" %} - {% endif %} - </div> - </section> - - <section class="section"> - <div class="container"> - <div class="content"> - <h1 class="title">{% block title_element %}{{ page_title }}{% endblock %}</h1> - {% block page_content %}{% endblock %} - </div> - </div> - </section> -{% endblock %} diff --git a/pydis_site/templates/content/dropdown.html b/pydis_site/templates/content/dropdown.html deleted file mode 100644 index 13c89c68..00000000 --- a/pydis_site/templates/content/dropdown.html +++ /dev/null @@ -1,17 +0,0 @@ -<div class="dropdown is-pulled-right is-right" id="dropdown" style="z-index: 1"> - <div class="dropdown-trigger"> - <a aria-haspopup="true" aria-controls="subarticle-menu"> - <span>Sub-Articles</span> - <span class="icon is-small"> - <i class="fas fa-angle-down" aria-hidden="true"></i> - </span> - </a> - </div> - <div class="dropdown-menu" id="subarticle-menu" role="menu"> - <div class="dropdown-content"> - {% for page in subarticles|dictsort:"name" %} - <a href="{{ page.path }}" class="dropdown-item">{{ page.name }}</a> - {% endfor %} - </div> - </div> -</div> diff --git a/pydis_site/templates/content/listing.html b/pydis_site/templates/content/listing.html deleted file mode 100644 index 934b95f6..00000000 --- a/pydis_site/templates/content/listing.html +++ /dev/null @@ -1,50 +0,0 @@ -{# Base navigation screen for resources #} -{% extends 'content/base.html' %} -{% load static %} - -{# Show a GitHub button on tag pages #} -{% block title_element %} -{% if is_tag_listing %} - <link rel="stylesheet" href="{% static "css/content/color.css" %}"> - <div class="level"> - <div class="level-left">{{ block.super }}</div> - <div class="level-right"> - <a class="level-item fab fa-github" href="{{ tag_url }}"></a> - </div> - </div> -{% endif %} -{% endblock %} - -{% block page_content %} - {# Nested Categories #} - {% for category, data in categories.items %} - <div class="box" style="max-width: 800px;"> - <span class="icon is-size-4 is-medium"> - <i class="{{ data.icon|default:"fas fa-folder" }} is-size-3 is-black has-icon-padding" aria-hidden="true"></i> - </span> - - <a href="{% url "content:page_category" location=path|add:category %}"> - <span class="is-size-4 has-text-weight-bold">{{ data.title }}</span> - </a> - <p class="is-italic">{{ data.description }}</p> - </div> - {% endfor %} - - {# Single Pages #} - {% for page, data in pages.items %} - <div class="box" style="max-width: 800px;"> - <span class="icon is-size-4 is-medium"> - <i class="{{ data.icon|default:"fab fa-python" }} is-size-3 is-black has-icon-padding" aria-hidden="true"></i> - </span> - <a href="{% url app_name location=path|add:page %}"> - <span class="is-size-4 has-text-weight-bold">{{ data.title }}</span> - </a> - {% if is_tag_listing %} - <div class="tag-container">{{ data.description | safe }}</div> - {% else %} - <p class="is-italic">{{ data.description }}</p> - {% endif %} - </div> - {% endfor %} - <script src="{% static 'js/content/listing.js' %}"></script> -{% endblock %} diff --git a/pydis_site/templates/content/page.html b/pydis_site/templates/content/page.html deleted file mode 100644 index 679ecec6..00000000 --- a/pydis_site/templates/content/page.html +++ /dev/null @@ -1,33 +0,0 @@ -{% extends 'content/base.html' %} - -{% block page_content %} - {% if relevant_links or toc %} - <div class="columns is-variable is-8"> - <div class="column is-two-thirds"> - {{ page|safe }} - </div> - <div class="column"> - {% if toc %} - <div class="box"> - <p class="menu-label">Table of Contents</p> - <ul class="menu-list toc"> - {{ toc|safe }} - </ul> - </div> - {% endif %} - {% if relevant_links %} - <div class="box"> - <p class="menu-label">Relevant links</p> - <ul class="menu-list"> - {% for value, link in relevant_links.items %} - <li><a class="has-text-link" href="{{link}}">{{ value }}</a></li> - {% endfor %} - </ul> - </div> - {% endif %} - </div> - </div> - {% else %} - <div>{{ page|safe }}</div> - {% endif %} -{% endblock %} diff --git a/pydis_site/templates/content/tag.html b/pydis_site/templates/content/tag.html deleted file mode 100644 index fa9e44f5..00000000 --- a/pydis_site/templates/content/tag.html +++ /dev/null @@ -1,40 +0,0 @@ -{% extends "content/page.html" %} -{% load static %} - -{% block head %} - {{ block.super }} - <link rel="stylesheet" href="{% static 'css/content/color.css' %}"/> - <link rel="stylesheet" href="{% static 'css/content/tag.css' %}"/> - <title>{{ tag.name }}</title> -{% endblock %} - -{% block title_element %} - <div class="level mb-2"> - <div class="level-left">{{ block.super }}</div> - <div class="level-right"> - <a class="level-item fab fa-github" href="{{ tag.url }}"></a> - </div> - </div> - - <div class="dropdown is-size-6 is-hoverable"> - <div class="dropdown-trigger "> - <a aria-haspopup="menu" href="{{ tag.last_commit.url }}"> - <span class="update-time"> - Last Updated: {{ tag.last_commit.date | date:"F j, Y g:i A e" }} - </span> - </a> - </div> - <div class="dropdown-menu"> - <div class="dropdown-content"> - <div class="dropdown-item">Last edited by:</div> - {% for user in tag.last_commit.format_authors %} - <div class="dropdown-item">{{ user }}</div> - {% endfor %} - <div class="dropdown-divider"></div> - {% for line in tag.last_commit.lines %} - <div class="dropdown-item">{{ line }}</div> - {% endfor %} - </div> - </div> - </div> -{% endblock %} diff --git a/pydis_site/templates/events/_base.html b/pydis_site/templates/events/_base.html deleted file mode 100644 index ff78d944..00000000 --- a/pydis_site/templates/events/_base.html +++ /dev/null @@ -1,33 +0,0 @@ -{% extends "base/base.html" %} -{% load static %} - -{% block head %} - <link rel="stylesheet" href="{% static "css/events/base.css" %}"> - <link rel="stylesheet" - href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.4.0/styles/atom-one-dark.min.css"> - <script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.4.0/highlight.min.js"></script> - <script>hljs.initHighlightingOnLoad();</script> -{% endblock %} - -{% block content %} - {% include "base/navbar.html" %} - - <section class="breadcrumb-section section"> - <div class="container"> - <nav class="breadcrumb is-pulled-left" aria-label="breadcrumbs"> - <ul> - {% block breadcrumb %}{% endblock %} - </ul> - </nav> - </div> - </section> - - <section class="section"> - <div class="container"> - <div class="content"> - <h1>{% block title %}{% endblock %}</h1> - <div>{% block event_base_content %}{% endblock %}</div> - </div> - </div> - </section> -{% endblock %} diff --git a/pydis_site/templates/events/base.html b/pydis_site/templates/events/base.html deleted file mode 100644 index c9a963e7..00000000 --- a/pydis_site/templates/events/base.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends "events/_base.html" %} - -{% block event_base_content %} -<div> - {% block event_content %}{% endblock %} -</div> -{% endblock %} diff --git a/pydis_site/templates/events/base_sidebar.html b/pydis_site/templates/events/base_sidebar.html deleted file mode 100644 index 8ce6ad65..00000000 --- a/pydis_site/templates/events/base_sidebar.html +++ /dev/null @@ -1,12 +0,0 @@ -{% extends "events/base.html" %} - -{% block event_base_content %} -<div class="columns is-variable is-8"> - <div class="column is-two-thirds"> - {% block event_content %}{% endblock %} - </div> - <div class="column"> - {% block sidebar %}{% endblock %} - </div> -</div> -{% endblock %} diff --git a/pydis_site/templates/events/index.html b/pydis_site/templates/events/index.html deleted file mode 100644 index 796a2e34..00000000 --- a/pydis_site/templates/events/index.html +++ /dev/null @@ -1,100 +0,0 @@ -{% extends "events/base_sidebar.html" %} - -{% block title %}Events{% endblock %} - -{% block breadcrumb %} - <li class="is-active"><a href="#">Events</a></li> -{% endblock %} - -{% block event_content %} - <div class="box"> - <h2 class="title is-4"><a href="{% url "events:page" path="code-jams" %}">Code Jams</a></h2> - <p>Every year we hold a community-wide Summer Code Jam. For this event, members of our community are assigned to teams to collaborate and create something amazing using a technology we picked for them. One such technology that was picked for the Summer 2021 Code Jam was text user interfaces (TUIs), where teams could pick from a pre-approved list of frameworks.</p> - <p>To help fuel the creative process, we provide a specific theme, like <strong>Think Inside the Box</strong> or <strong>Early Internet</strong>. At the end of the Code Jam, the projects are judged by Python Discord server staff members and guest judges from the larger Python community. The judges will consider creativity, code quality, teamwork, and adherence to the theme.</p> - <p>If you want to read more about Code Jams, visit our <a href="{% url "events:page" path="code-jams" %}">Code Jam info page</a> or watch this video showcasing the best projects created during the <strong>Winter Code Jam 2020: Ancient Technology</strong>:</p> - <iframe width="560" height="315" src="https://www.youtube.com/embed/8fbZsGrqBzo" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe> - </div> - - <div class="box"> - <h2 class="title is-4">PyWeek</h2> - <div class="columns is-3" style="--columnGap: 0.75rem;"> - <div class="column"> - <p> - For the past 15 years, <a href="https://pyweek.org" target="_blank" rel="noopener">PyWeek</a> has been running a bi-annual game jam for the - Python language. As of 2020, we are excited to say we are officially partnered with PyWeek to co-run these - events. - </p> - <p> - During each PyWeek event, we open a special discussion channel in which our members can discuss their - submissions, meet other participants, and talk to PyWeek staff. The PyWeek organizer, - Daniel Pope (<a href="https://twitter.com/lordmauve" target="_blank" rel="noopener">@lordmauve</a>) will be present during the entire event to answer - questions and post announcements and information in our community. - </p> - <p> - Unlike our other events, the <strong>community</strong> will select the winner from all the submissions - during PyWeek. We may release YouTube content showcasing the best submissions after the events are finished. - </p> - </div> - <div class="column is-3"> - <img src="https://pyweek.readthedocs.io/en/latest/_static/pyweek.svg" style="border-radius: 10px;" alt=""> - </div> - </div> - </div> - - <div class="box"> - <h2 class="title is-4">Advent of Code</h2> - <div class="columns is-3" style="--columnGap: 0.75rem;"> - <div class="column"> - <p> - Each year, many of our members take part of an online coding competition called - <a href="https://adventofcode.com/" target="_blank" rel="noopener">Advent of Code</a> that takes place in December. Advent of Code is an - Advent calendar of small programming puzzles for a variety of skill sets and skill levels that can be solved - in any programming language you like, including Python. - </p> - <p> - During the event, we will open a special discussion channel in which our members can discuss the puzzles - and compare their solutions. We will also open a private leaderboard and may even reward the best scoring - members on that board with prizes! - </p> - <p> - However, this event isn't purely competitive. You can also join in just to see how far you'll get, to - challenge yourself, as a way of learning Python, or just for the fun of it. In 2019, almost 200 members - signed up for our private leaderboard, but many more took on the challenge without focusing on the - competitive aspect. - </p> - </div> - <div class="column is-3"> - <img src="https://raw.githubusercontent.com/python-discord/branding/main/events/christmas/server_icons/festive_256.gif" style="border-radius: 10px;" alt=""> - </div> - </div> - </div> - - <div class="box"> - <h2 class="title is-4">Game Jam</h2> - <div class="columns is-3" style="--columnGap: 0.75rem;"> - <div class="column"> - <p> - The Game Jam is similar to our Code Jams, but smaller in scope. Instead of having to complete a qualifier - and being teamed up with random strangers, members of our community can just sign-up individually or pair up - with whoever they like. - </p> - <p> - The participants will have ten days to create a game using the technology we've selected, and drawing - inspiration from a provided theme. After the event, a panel of judges will play all the games and select a - winner. The top 5 will featured in a special video on our <a href="https://www.youtube.com/channel/UCQsrA4xo6jvdgsJZhKaBL6w" target="_blank" rel="noopener">YouTube channel</a>. - </p> - <p> - The <a class="has-text-link" href="{% url "events:page" path="game-jams/2020" %}">first edition of the Game Jam</a> ran from - <strong>April 17, 2020 to April 26, 2020</strong>. - </p> - </div> - <div class="column is-3"> - <img src="https://user-images.githubusercontent.com/33516116/77593036-5fb09780-6eeb-11ea-9feb-336b2e5e23de.png" style="border-radius: 10px;" alt=""> - </div> - </div> - </div> -{% endblock %} - -{% block sidebar %} - {% include "events/sidebar/events-list.html" %} -{% endblock %} diff --git a/pydis_site/templates/events/pages/code-jams/1.html b/pydis_site/templates/events/pages/code-jams/1.html deleted file mode 100644 index bde4e0b4..00000000 --- a/pydis_site/templates/events/pages/code-jams/1.html +++ /dev/null @@ -1,82 +0,0 @@ -{% extends "events/base.html" %} - -{% block title %}Code Jam 1: Snakes{% endblock %} - -{% block breadcrumb %} - <li><a href="{% url "events:index" %}">Events</a></li> - <li><a href="{% url "events:page" path="code-jams" %}">Code Jams</a></li> - <li class="is-active"><a href="#">Code Jam 1: Snakes</a></li> -{% endblock %} - -{% block event_content %} - <p> - The theme of the code jam was <strong>snakes</strong> Participants were assigned a random partner, - and used Git to submit code to us. Staff members reviewed the code both during and after the code jam, - and provided suggestions for changes and improvements. - The best submissions were implemented into our community bot. - Winners recieved a special Code Jam Champion title. - </p> - - <h2 id="task-description" class="title is-4"><a href="#task-description">Task description</a></h2> - <p>Here is the original task description which was posted on the code-jam-1 GitHub repo:</p> - - <blockquote> - <p> - For this code jam, your task will be to create a Snake cog for a Discord.py rewrite bot. - <a href="https://discordpy.readthedocs.io/en/rewrite/">You can find the documentation for Discord.py rewrite here</a>. - The best cog commands will be added to the official Python Discord bot and made available to everyone on the server. - The overall best cog will be awarded custom Code Jam Champion roles, - but the best commands from the teams who did not win will also be added to our bot, - and any users who write something that ends up in the bot will be awarded Contributor roles on the server. - </p> - <p> - Your initial task will be to write <code>get_snek()</code>. This is the minimum requirement for this contest, - and everyone must do it. <code>get_snek()</code> should be a method that goes online and fetches information about a snake. - If you run it without providing an argument, it should fetch information about a random snake, - including the name of the snake, a picture of the snake, and various information about it. - Is it venomous? Where can it be found? What information you choose to get is up to you. - </p> - <p> - <code>get_snek()</code> should also take an optional argument name, which should be a string that represents the name of a snake. - For example, the call <code>get_snek('cobra')</code> should get information about a cobra. name should be case insensitive. - </p> - <p> - If <code>get_snek('Python')</code> is called, the method should instead return information about the programming language, - but making sure to return the same type of information as for all the other snakes. - Fill in this information in any way you want, try to have some fun with it. - </p> - <p> - Once you have finished <code>get_snek()</code>, you should make at least two bot commands. - The first command, <code>get()</code>, should simply call <code>get_snek()</code> with whatever arguments the user provided, - and then make a nice embed that it returns to Discord. - For example, if the user in the Discord channel says bot.snakes.get('anaconda'), - the bot should post an embed that shows a picture of an anaconda and some information about the snake. - </p> - <p> - The second command is entirely up to you. You can choose to use <code>get_snek</code> for this command as well, - or you can come up with something entirely different. - The only requirement is that it is snake related in some way or other. - Here is your chance to be creative. It is these commands that will win or lose you this code jam. - The best original ideas for these commands will walk away with the victory. - </p> - </blockquote> - - <h2 id="result" class="title is-4"><a href="#result">Result</a></h2> - <p><strong>The Winning Team: Team 23, kel and Momo!</strong></p> - <p> - These two experts worked together to create what can only be described as a flawless submission. - The staff were unable to find a single thing to complain about in the 1100 lines that were committed. - Here are some of the features they created for their snake cog: - </p> - <ul> - <li>A fully functional implementation of Snakes and Ladders</li> - <li>A feature that uses perlin noise to draw a random snake, and posts the picture in the chat.</li> - <li>Hatch your very own baby snake!</li> - <li>Snakify your post history with a Markov chain</li> - <li>Rattlesnake sound effects in the voice channels</li> - </ul> - <p> - These features, as well as the best ones from the other teams, - have been implemented into our community bot. Use <code>.snakes</code> in <code>#sir-lancebot-commands</code> to play with it. - </p> -{% endblock %} diff --git a/pydis_site/templates/events/pages/code-jams/2.html b/pydis_site/templates/events/pages/code-jams/2.html deleted file mode 100644 index 602a1c66..00000000 --- a/pydis_site/templates/events/pages/code-jams/2.html +++ /dev/null @@ -1,72 +0,0 @@ -{% extends "events/base.html" %} - -{% block title %}Code Jam 2: Mythological API{% endblock %} - -{% block breadcrumb %} - <li><a href="{% url "events:index" %}">Events</a></li> - <li><a href="{% url "events:page" path="code-jams" %}">Code Jams</a></li> - <li class="is-active"><a href="#">Code Jam 2: Mythological API</a></li> -{% endblock %} - -{% block event_content %} - <p> - The theme for this code jam will be <strong>mythology</strong>. - That means you're going to be creating a <strong>RESTful API in Flask with a mythology theme</strong>. - </p> - <p> - For example, you might create the The Mimír API, - an API used to look up information about norse gods and godesses. - </p> - - <pre><code class="lang-json">{ - "name": "Kvasir", - "parents": "Born from the saliva of the Æsir and the Vanir", - "type": "A god of wisdom and knowledge" -}</code></pre> - - <p> - The API must respond with json data, but this is not limited to text. - It could respond with bytedata, with a URL to an image, a video or some audio, - or with any other form of data you deem interesting. - </p> - <p> - The API should accept at least a GET or a POST request with JSON data. - You are allowed to use any third party libraries you want. - </p> - <p> - Remember, creativity counts. Try to have fun with it. - We're not necessarily looking for dead serious solutions only, - and it's okay if your solution is only tangentially related to the theme, - so long as there's a relationship of some sort. - </p> - - <h2 id="results" class="title is-4"><a href="#results">Results</a></h2> - <p>The winner of the second code jam is <strong>Defiant Sails</strong>, with Momo, WrongEnd, and SharpBit! Congratulations!</p> - - <p> - They've written a phenomenal API with OAuth authentication, a multiplayer RPG, - and a feature to procedurally generate a random mythologically themed story. - This story can be played as a video, - where they use Text-To-Speech to read the story out loud while epic music plays in the background. - The music is different for each play, and the audio is visualized in a video. - </p> - <p> - The team will receive the grand prize of 12-month PyCharm Pro licenses, - and will also receive the honorary Code Jam Champions role. - </p> - <hr> - <p> - In second place with another really excellent submission, we have the team <strong>Rude Interests</strong>, - consisting of Lord Bisk, Runew0lf and Nix. They've got procedurally generated rune images, - procedural mythologies with gods with procedural relationships, descriptions, genders, and names. - A super fun idea which nearly ended up taking the first place. - </p> - <hr> - <p> - In third place, we have <strong>Overconfident Ideas</strong>, with Martmists, Casterly and eivl. - They did relationship trees with a number of different visualization options, - a fantastic myth lookup feature to find a great deal of details about a mythological figure, - a mythology trivia feature, and a markov chain string generator. - It also had a very comprehensive framework and it was clear that a great deal of work had gone into it. - </p> -{% endblock %} diff --git a/pydis_site/templates/events/pages/code-jams/3.html b/pydis_site/templates/events/pages/code-jams/3.html deleted file mode 100644 index 0bd293db..00000000 --- a/pydis_site/templates/events/pages/code-jams/3.html +++ /dev/null @@ -1,81 +0,0 @@ -{% extends "events/base.html" %} - -{% block title %}Code Jam 3: Games!{% endblock %} - -{% block breadcrumb %} - <li><a href="{% url "events:index" %}">Events</a></li> - <li><a href="{% url "events:page" path="code-jams" %}">Code Jams</a></li> - <li class="is-active"><a href="#">Code Jam 3: Games!</a></li> -{% endblock %} - -{% block event_content %} - <p> - The theme for code jam 3 was creating a <strong>game</strong> with the theme <strong>the world ends in 10 minutes</strong>. - The teams of three could live out their creativity here - any form of game was fine, - as long as they fit the theme. The jam started at <strong>Monday, the 22nd October 2018, 12:00 PM UTC</strong>. - </p> - - <h2 id="task-description" class="title is-4"><a href="#task-description">Task Description</a></h2> - <p>The <a href="https://github.com/python-discord/code-jam-3">original task description</a> was as follows:</p> - <blockquote> - <p> - This task is a little more freeform than the other code jams we've had so far - - we're happy to accept games in any form, as long as they fit that theme. - You may use PyGame, Kivy, Curses/Unicurses, Pyxel, web frameworks like Flask, - or anything else your team desires. - Please provide instructions on how to set up and run your game within the README where necessary. - Remember that teamwork is paramount - You will need to work together. - For this jam, we've assigned a leader for each team based on their responses to the application form. - Remember to listen to your leader, and communicate with the rest of your team! - </p> - </blockquote> - - <h2 id="results" class="title is-4"><a href="#results">Results</a></h2> - <p> - The <strong>winning team</strong> for this jam was Certain Horses, consisting of Wattle, Scragly, and Mark. - They made a 2D platformer where the player had to collect lemons in order to earn points. - Below the player was an ocean of lemon juice that caused instant death if you touched it. - </p> - <p> - Every time you collected a lemon, the lemon juice water level would raise up, - so you had to be tactical about which lemons you could collect - without dying and which of the branching paths you'd have to select to avoid the sea of lemon juice rushing up to meet you. - </p> - <p> - You can play the game by yourself here: - <a href="https://github.com/MarkKoz/code-jam-3">https://github.com/MarkKoz/code-jam-3</a> - </p> - - <h2 id="runner-up-1" class="title is-4"><a href="#runner-up-1">Runner up 1: Successful Toads</a></h2> - <p> - The <strong>first runner up</strong> for this jam was team Successful Toads, - whose code can be found at <a href="https://gitlab.com/biskette/code-jam-3/">https://gitlab.com/biskette/code-jam-3/</a>. - They wrote a flask app game which was about deciding who to let into the last nuclear bunker on the planet. - Like Tinder, but for nuclear holocaust gatekeepers. - The game had procedurally generated graphics which were absolutely hilarious, - and you would swipe right or left to decide whether or not to let someone into your bunker. - Each person had certain traits which could either save or destroy your bunker. - </p> - <p> - In order to win, you had to balance stuff like medical expertise with combat experience - and make sure that your bunker would survive into the post-apocalypse. - </p> - - <h2 id="runner-up-2" class="title is-4"><a href="#runner-up-2">Runner up 2: Misty Hats</a></h2> - <p> - The second runner up for this jam was team Misty Hats, - with code available at <a href="https://gitlab.com/JannesJ/code-jam-3">https://gitlab.com/JannesJ/code-jam-3</a>. - They made an excellent shoot-em-up in the style of Gradius, - with a long intro cinematic, lots of original graphics assets, - and a whole bunch of different power-ups. Other features included: - </p> - <ul> - <li><p>Fighter enemy: A tiny spaceship that will follow you and try to take you down.</p></li> - <li><p>Pythonic mines: Space mines shaped like Python logos that damage you if you touch them.</p></li> - <li><p>Defensive Structures: Semi-Stationary defensive structures that will shoot you on sight</p></li> - <li><p>8 different power-ups: Extra damage, armor, hp, shield, double shot.. and more!</p></li> - <li><p>Timers: A ten minute timer and small timers for temporary power ups.</p></li> - <li><p>Different size/color blaster projectiles</p></li> - <li><p>Wave system</p></li> - </ul> -{% endblock %} diff --git a/pydis_site/templates/events/pages/code-jams/4.html b/pydis_site/templates/events/pages/code-jams/4.html deleted file mode 100644 index f3d750bc..00000000 --- a/pydis_site/templates/events/pages/code-jams/4.html +++ /dev/null @@ -1,93 +0,0 @@ -{% extends "events/base.html" %} - -{% block title %}Code Jam 4: This Apps Hates You{% endblock %} - -{% block breadcrumb %} - <li><a href="{% url "events:index" %}">Events</a></li> - <li><a href="{% url "events:page" path="code-jams" %}">Code Jams</a></li> - <li class="is-active"><a href="#">Code Jam 4: This Apps Hates You</a></li> -{% endblock %} - -{% block event_content %} - <p> - The theme for code jam 4 was creating a <strong>GUI application</strong> with the theme <strong>this app hates you</strong>. - 19 randomly assembled teams worked on this task for three days from the 22nd of February, 00:00 UTC. - </p> - - <h2 id="task-description" class="title is-4"><a href="#task-description">Task Description</a></h2> - <p> - The original task description, - as found in the <a href="https://github.com/python-discord/code-jam-4">repository</a> was as follows: - </p> - <blockquote> - <p> - The theme for this code jam will be <strong>This app hates you!</strong>. - You will be creating an application using a GUI library of your choice in Python. - The application must serve a real purpose, but must also fit the theme. - </p> - <p> - Here are a couple of examples of what we mean by an application that "serves a real purpose but also fits the theme": - </p> - <ul> - <li><p>A calculator app that calculates the right answers, but represents the answer in a way that's completely impractical.</p></li> - <li> - <p> - An image resizer where you have to specify which part of the image to resize, - specify how much force to apply to the resize operation in newtons, - and then manually resize the image by turning a crank. - </p> - </li> - <li> - <p> - An alarm clock app that plays a very loud sound effect every 5 minutes reminding you that your alarm will ring in 6 hours. - The closer it gets to the 6 hour mark, the lower the volume of the sound effect. - When the time is up, the sound effect is virtually inaudible. - </p> - </li> - </ul> - </blockquote> - - <h2 id="judging-stream" class="title is-4"><a href="#judging-stream">Judging stream</a></h2> - <p> - If you want to watch the original code jam judging stream, - you can find it on YouTube - all of the submissions are showcased in the stream. - The winning project is showcased at around the 40 minute mark. - </p> - <iframe width="560" height="315" src="https://www.youtube.com/embed/TlU6GPGbSuY?start=805" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> - - <h2 id="results" class="title is-4"><a href="#results">Results</a></h2> - <p> - The <strong>winning team</strong> for this code jam was the team <strong>Cool Crocodiles</strong>, - consisting of Runew0lf, gl4cial, and syntaxaire. - They created a text editor called CrocPad++ featuring an infuriating troubleshooter that would pop up every five or so characters, - sound effects when you type, the ability to insert symbols by turning a dial, - a theme "for blind users", and many more features. - </p> - - <h2 id="runner-up-1" class="title is-4"><a href="#runner-up-1">Runner up 1: Team Enthusiastic Electricians</a></h2> - <p> - The <strong>first runner up</strong> for this code jam was the team <strong>Enthusiastic Electricians</strong>, - who made a paint tool called <i>ArtiQule</i> where the pencil point would break, - colors would blend together on your palette, and the paint brush would drip onto the canvas. - </p> - - <h2 id="runner-up-2" class="title is-4"><a href="#runner-up-2">Runner up 2: Team Slithering Snacks</a></h2> - <p> - The <strong>second runner up</strong> for this code jam was the team <strong>Slithering Snacks</strong>. - They ade a media player where you had to fill out a CAPTCHA every time you wanted to load a file, - register an account, and confirm your password to log in by typing it with all the characters <i>in alphabetic order</i>. - </p> - - <h2 id="honorable-mentions" class="title is-4"><a href="#honorable-mentions">Honorable mentions</a></h2> - <p>While they didn't make it on top of the ladder, the following submissions have stood out exceptionally:</p> - <ul> - <li><p>Team <strong>Blue Buckets</strong> won best code quality with their <i>Tinder for Cats</i>.</p></li> - <li> - <p> - Team <strong>Overjoyed <a href="https://wiki.teamfortress.com/wiki/Otolaryngologist%27s_Mirror">Otolaryngologists</a></strong> - won best looking UI with their sleek <i>Minesweeper</i> game where you had to press each tile up to 100 times to break it and only got one flag. - </p> - </li> - <li><p>Team <strong>High Houses</strong> won best idea with an <i>on-screen keyboard</i> where you only got some of the keys and additional keys had to be unlocked by gaining XP, leveling up, and getting loot boxes.</p></li> - </ul> -{% endblock %} diff --git a/pydis_site/templates/events/pages/code-jams/5.html b/pydis_site/templates/events/pages/code-jams/5.html deleted file mode 100644 index ba6928c7..00000000 --- a/pydis_site/templates/events/pages/code-jams/5.html +++ /dev/null @@ -1,80 +0,0 @@ -{% extends "events/base.html" %} - -{% block title %}Code Jam 5: Climate Change{% endblock %} - -{% block breadcrumb %} - <li><a href="{% url "events:index" %}">Events</a></li> - <li><a href="{% url "events:page" path="code-jams" %}">Code Jams</a></li> - <li class="is-active"><a href="#"></a>Code Jam 5: Climate Change</li> -{% endblock %} - -{% block event_content %} - <p> - The theme for code jam 5 was <strong>climate change</strong>. - Similar to <a href="https://pythondiscord.com/pages/code-jams/code-jam-3/">code jam 3</a>, - teams could live out their creativity here, as long as it fits the theme. - The code jam started on July 20, 2019 at 12 PM UTC, - and with more than 117 sign-ups and 27 teams competing, - this was our biggest code jam ever (so far)! - </p> - - <h2 id="task-description" class="title is-4"><a href="#task-description">Task Description</a></h2> - <p>The original task description, as found in <a href="https://github.com/python-discord/code-jam-5">the repository</a>, was as follows:</p> - <blockquote> - <p> - Your theme for this code jam is <strong>climate change</strong>. - </p> - <p> - Because this is a free-for-all, you are free to make anything you want, - as long as it fits this theme. - We'd love if you created something that might help raise awareness, - but first and foremost, we want you to create something <i>fun</i>. - </p> - </blockquote> - - <h2 id="judging-stream" class="title is-4"><a href="#judging-stream">Judging stream</a></h2> - <p> - If you want to watch the original code jam judging stream, - you can find it on YouTube - all of the submissions are showcased in the stream. - The winning project is showcased at around the 30 minute mark. - </p> - <iframe width="560" height="315" src="https://www.youtube.com/embed/drBKNU73Ss4" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> - - <h2 id="results" class="title is-4"><a href="#results">Results</a></h2> - <p> - While it was a difficult decision, the winners were Team <strong>Combined Crusader</strong>, - consisting of Makusu, Mahabama, and missingfragment. - They made a really addicting <i>Climate Clicker game</i> that surprised us for being extremely responsive, - having beautiful, original art assets, and, most of all, being really fun to play. - The code quality of this project was good and they used Pythonic techniques like the LRU cache to make the game behave extremely snappy. - </p> - <p> - You can check out their entry in the - <a href="https://github.com/python-discord/code-jam-5/tree/master/combined_crusaders">repository</a> - and watch us doing so in the <a href="https://www.youtube.com/watch?v=drBKNU73Ss4&t=1815s">livestream</a>. - </p> - - <h2 id="runner-up-1" class="title is-4"><a href="#runner-up-1">Runner up 1: Various Vipers</a></h2> - <p> - Team <strong>Various Vipers</strong> produced a game consisting of various mini-games that had an excellent graphical user interface. - The fact that the project had accompanying game design documents showed how well-organized this project was. - Combined with good code quality, this thematic project deserves the second place in this code jam. - </p> - <p> - Feel free to check out their submission in the code jam <a href="https://github.com/python-discord/code-jam-5/tree/master/various_vipers">repository</a> - and watch us inspecting it in the <a href="https://www.youtube.com/watch?time_continue=1&v=drBKNU73Ss4&feature=emb_logo">livestream</a>. - </p> - - <h2 id="runner-up-2" class="title is-4"><a href="#runner-up-2">Runner up 2: Gentle Gnomes</a></h2> - <p> - The <strong>Gentle Gnomes</strong> ended up in the third spot. - Their web app allowed the user to search for a location and would then show climate-related statistics, - plots as well as projections for the future for the specified location. - The code quality of this project was outstanding and the web app was extremely useful and interesting. - </p> - <p> - Again, you can view their submission on the - <a href="https://github.com/python-discord/code-jam-5/tree/master/gentle_gnomes">repository</a> - or view it live on the <a href="https://www.youtube.com/watch?v=drBKNU73Ss4&t=7800s">livestream</a>. - </p> -{% endblock %} diff --git a/pydis_site/templates/events/pages/code-jams/6/_index.html b/pydis_site/templates/events/pages/code-jams/6/_index.html deleted file mode 100644 index 256914b6..00000000 --- a/pydis_site/templates/events/pages/code-jams/6/_index.html +++ /dev/null @@ -1,93 +0,0 @@ -{% extends "events/base_sidebar.html" %} - -{% block title %}Winter Code Jam 2020: Ancient Technology{% endblock %} - -{% block breadcrumb %} - <li><a href="{% url "events:index" %}">Events</a></li> - <li><a href="{% url "events:page" path="code-jams" %}">Code Jams</a></li> - <li class="is-active"><a href="#"></a>Winter Code Jam 2020: Ancient Technology</li> -{% endblock %} - -{% block event_content %} - <p> - The theme for the Winter Code Jam 2020, the sixth Python Discord Code Jam, - was utilizing the <strong>Kivy Framework</strong> to create <strong>Ancient Technology</strong>. - The jam lasted for 9 days from January 17, 2020 to January 26, 2020 with 16 teams competing against each other. - </p> - - <h2 id="task-description" class="title is-4"><a href="#task-description">Task Description</a></h2> - <p> - The original task description, as found in the <a href="https://github.com/python-discord/code-jam-6">repository</a>, was as follows: - </p> - <blockquote> - <p>By popular choice, the theme for this code jam is <strong>Ancient Technology</strong>.</p> - <p> - What you do with this theme or how you interpret it is up to you, - but it will be your task to come up with something fun using this theme. - </p> - </blockquote> - - <h2 id="judging-stream" class="title is-4"><a href="#judging-stream">Judging Stream</a></h2> - <p> - If you want to watch the original code jam judging stream, - you can find it on YouTube - all of the submissions are showcased in the stream. - The winning project is showcased at the <a href="https://youtu.be/I97L_Y3rhvc?t=13710">03:48:30</a> timestamp. - </p> - <iframe width="560" height="315" src="https://www.youtube.com/embed/I97L_Y3rhvc" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> - - <h2 id="results" class="title is-4"><a href="#results">Results</a></h2> - <p> - The <strong>winning team</strong> for this code jam was team <strong>Tactless Tricksters</strong>, - consisting of GoBig87, Kan, sposker, DomicidalManiac, and Inventor. - They created an incredible morse code chatting app, with a plethora of different features. - Morse code training, morse code audio encoding/decoding, audio input sensitivity calibration, - and a morse code messaging system are all features of this project. - The beautiful and intuitive UI paired with all these features was what won them this jam. - With the given amount of time during this jam, this was quite the impressive project. - </p> - <p> - You can check out their entry in the - <a href="https://github.com/python-discord/code-jam-6/tree/master/tactless-tricksters">repository</a> - and watch us doing so in the <a href="https://youtu.be/I97L_Y3rhvc?t=13710">livestream</a>. - The top 3 winning teams' projects are also displayed on the <a href="https://kivy.org#gallery">Kivy Gallery</a>. - </p> - - <h2 id="runner-up-1" class="title is-4"><a href="#runner-up-1">Runner up 1: Circumstancial Champions</a></h2> - <p> - The <strong>Circumstantial Champions</strong> (salt-die, david987, & Music) - created a game in which players go back in time to smash and carve out rocks, - just like cavemen. The combination of their execellent graphical interface, animations, - and satisfying sounds made this one of the most polished projects in the jam. - </p> - <p> - Check out their submission in the code jam - <a href="https://github.com/python-discord/code-jam-6/tree/master/circumstantial-companions">repository</a> - and view it live on the <a href="https://youtu.be/I97L_Y3rhvc?t=2854">livestream</a>. - </p> - - <h2 id="runner-up-2" class="title is-4"><a href="#runner-up-2">Runner up 2: Inquisitive Investigators</a></h2> - <p> - Team <strong>Inquisitive Investigators</strong> (f1re & Monika) ended up in third place with their retro TUI file explorer. - The simple, easy-to-use file explorer along with their built-in terminal, text editor, - and photo viewer made this a very feature packed project. - </p> - <p> - Feel free to view their submission on the - <a href="https://github.com/python-discord/code-jam-6/tree/master/inquisitive-investigators">repository</a> - and watch us review it on the <a href="https://youtu.be/I97L_Y3rhvc?t=6545">livestream</a>. - </p> - - <img src="https://raw.githubusercontent.com/python-discord/code-jam-6/master/ancient%20tech.png?token=AAQAKVPQ55SEFWYYLYO5YV26ETLTC" alt="Code Jam Banner" style="max-width:100%;"> -{% endblock %} - -{% block sidebar %} - {% include "events/sidebar/code-jams/6.html" %} - <div class="box"> - <p class="menu-label">Relevant Links</p> - <ul class="menu-list"> - <li><a class="has-text-link" href="{% url "events:page" path="code-jams/6/rules" %}">Rules</a></li> - <li><a class="has-text-link" href="https://github.com/python-discord/code-jam-6-qualifier">Qualifier</a></li> - <li><a class="has-text-link" href="{% url "events:page" path="code-jams" %}">About Code Jams</a></li> - </ul> - </div> -{% endblock %} diff --git a/pydis_site/templates/events/pages/code-jams/6/rules.html b/pydis_site/templates/events/pages/code-jams/6/rules.html deleted file mode 100644 index dfb1586f..00000000 --- a/pydis_site/templates/events/pages/code-jams/6/rules.html +++ /dev/null @@ -1,80 +0,0 @@ -{% extends "events/base_sidebar.html" %} - -{% block title %}Rules{% endblock %} - -{% block breadcrumb %} - <li><a href="{% url "events:index" %}">Events</a></li> - <li><a href="{% url "events:page" path="code-jams" %}">Code Jams</a></li> - <li><a href="{% url "events:page" path="code-jams/6" %}">Winter Code Jam 2020: Ancient Technology</a></li> - <li class="is-active"><a href="#">Rules</a></li> -{% endblock %} - -{% block event_content %} - <ol> - <li> - <p> - The majority of your project must be Python. - We will use the GitHub language details for this, - so just <strong>make sure your repo contains at least 51% Python</strong>, and you'll be okay. - </p> - </li> - <li> - <p> - Your solution must use the Kivy framework. - It is not permitted to work around this by e.g. using Kivy as a wrapper for another framework. - </p> - </li> - <li> - <p> - Your solution should be platform agnostic. - For example, if you use filepaths in your submission, - use <code>pathlib</code> to create platform agnostic Path objects instead of hardcoding the paths. - </p> - </li> - <li> - <p> - Your project must be feasible to run and simple to set up <strong>on a desktop computer</strong> - - which means you should almost certainly use some sort of dependency manager, - like <code>pipenv</code>, <code>poetry</code>, or a strictly pinned <code>requirements.txt</code>. - </p> - </li> - <li> - <p> - You must get contributions from every member of your team, - if you have an issue with someone on your team please contact a member of the administration team. - These contributions do not necessarily have to be code, - for example it's absolutely fine for someone to contribute management, documentation, graphics or audio. - <strong> - Team members that do not contribute will be removed from the Code Jam, - and will not receive their share of any prizes the team may win. - They may also be barred from entering future events. - </strong>. - </p> - </li> - <li> - <p>You must use GitHub as source control.</p> - </li> - <li> - <p> - All code must be written and committed within the time constrictions of the jam - <ul><li>Late commits may be reverted, so make sure you leave enough time to bug test your program</li></ul> - </p> - </li> - </ol> - - <blockquote> - Please note that our regular <a href="/pages/rules/">community rules</a> and <a href="/pages/code-of-conduct/">code of conduct</a> - also apply during the event and that we reserve the right to make changes to these rules at any time. - </blockquote> -{% endblock %} - -{% block sidebar %} - {% include "events/sidebar/code-jams/6.html" %} - <div class="box"> - <p class="menu-label">Relevant Links</p> - <ul class="menu-list"> - <li><a class="has-text-link" href="{% url "events:page" path="code-jams/6" %}">Winter Code Jam 2020</a></li> - <li><a class="has-text-link" href="https://github.com/python-discord/code-jam-6-qualifier">Code Jam Qualifier</a></li> - </ul> - </div> -{% endblock %} diff --git a/pydis_site/templates/events/pages/code-jams/7/_index.html b/pydis_site/templates/events/pages/code-jams/7/_index.html deleted file mode 100644 index 1f0b0909..00000000 --- a/pydis_site/templates/events/pages/code-jams/7/_index.html +++ /dev/null @@ -1,85 +0,0 @@ -{% extends "events/base_sidebar.html" %} - -{% block title %}Summer Code Jam 2020: Early Internet{% endblock %} - -{% block breadcrumb %} - <li><a href="{% url "events:index" %}">Events</a></li> - <li><a href="{% url "events:page" path="code-jams" %}">Code Jams</a></li> - <li class="is-active"><a href="#">Summer Code Jam 2020: Early Internet</a></li> -{% endblock %} - -{% block event_content %} - <p> - For Python Discord's 7<sup>th</sup> biannual code jam, contestants utilized the <strong>Django</strong> framework - to create a web application. The theme for this event was <strong>Early Internet</strong>, lasting from - July 31<sup>st</sup> to August 9<sup>th</sup>. This was our largest event ever, with over 50 teams competing - for prizes from our prize pool along with the illustrious role of <code>Code Jam Champions</code> on our server. - </p> - - <h3 id="task-description"><a href="#task-description">Task Description</a></h3> - <p> - The original task description, as found in the <a href="https://github.com/python-discord/summer-code-jam-2020">repository</a>, was as follows: - </p> - <blockquote> - <strong>The theme for this code jam is Early Internet.</strong> - <p> - What you do with this theme or how you interpret it is up to you, but it will be your task to come up with something fun using this theme. - </p> - - <strong>The chosen technology for this game jam is <a href="https://www.djangoproject.com/">Django</a>.</strong> - <p> - Django is a high-level Python Web framework that encourages rapid development and clean, pragmatic design. It comes "batteries included" with a slew of incredibly polished features, including database migrations, an excellent CRM, access control, security features to prevent you from making expensive mistakes, and - <a href="https://docs.djangoproject.com/en/3.0/">probably the best documentation on the internet</a>. - </p> - </blockquote> - - <h3 id="judging-stream"><a href="#judging-stream">Judging Stream</a></h3> - <p> - If you want to watch the original code jam judging stream, you can find it on YouTube - all of the submissions are showcased in the stream. The winning project is showcased at the <a href="https://youtu.be/OFtm8f2iu6c?t=6443">01:47:23</a> timestamp. - </p> - - <iframe width="560" height="315" src="https://www.youtube.com/embed/OFtm8f2iu6c" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> - - <h3 id="results"><a href="#results">Results</a></h3> - <p> - The <strong>winning team</strong> for this code jam was the <strong>Juicy Jaguars</strong>, consisting of charlotte, joshuqa, and dang. - They created an incredible Windows 95 web application, named Web95. It included a plethora of features, such as movable windows, - a start menu, a recreation of Internet Explorer, different themes & wallpapers, and a couple of fun easter eggs. The killer feature of this project was Internet Explorer. It processed the fonts, images, and colors of each webpage visited to make the experience feel as if - it were from the 90s! This was an incredibly novel idea, which was executed beautifully. - </p> - <p> - Check out their submission in our <a href="https://github.com/python-discord/summer-code-jam-2020/tree/master/juicy-jaguars">repository</a> - and watch us review it on the <a href="https://youtu.be/OFtm8f2iu6c?t=6443">livestream</a>! - </p> - - <h3 id="runner-up-1"><a href="#runner-up-1">Runner up 1: Annoyed Alligators</a></h3> - <p> - The <strong>Annoyed Alligators</strong> (i..kun, gkrou, SurvivingOnNaps, corner, and pykam) created a social media platform which is controllable by - a command line interface, named SoCommandLine Media (SoCL). Users can signup to create posts and view what others have posted already, all via the - command line. It evens supports direct messaging other users and getting the latest news articles for you to read! - </p> - <p> - Have a look at their submission in our <a href="https://github.com/python-discord/summer-code-jam-2020/tree/master/annoyed-alligators">repository</a> - and view it on the <a href="https://youtu.be/OFtm8f2iu6c?t=2200">livestream</a>! - </p> - - <h3 id="runner-up-2"><a href="#runner-up-2">Runner up 2: Mysterious Mice</a></h3> - <p> - The <strong>Mysterious Mice</strong> (rryyaann, onatoko, n0remac, and venturafranklin) created a social media site for NASA's rovers, named SpaceBook. It included profiles and photos of each rover, showed what the weather was like on Mars, and even had a text-based adventure game in a terminal! The beautiful, retro UI along with the animated space background made it a pleasure to use. - </p> - <p> - Feel free to check out their submission in our <a href="https://github.com/python-discord/summer-code-jam-2020/tree/master/mysterious-mice">repository</a> - and watch us use it on the <a href="https://youtu.be/OFtm8f2iu6c?t=7624">livestream</a>! - </p> -{% endblock %} - -{% block sidebar %} - {% include "events/sidebar/code-jams/7.html" %} - <div class="box"> - <p class="menu-label">Relevant Links</p> - <ul class="menu-list"> - <!-- Add other items when they become available --> - <li><a class="has-text-link" href="{% url "events:page" path="code-jams/7/rules" %}">Rules</a></li> - </ul> - </div> -{% endblock %} diff --git a/pydis_site/templates/events/pages/code-jams/7/rules.html b/pydis_site/templates/events/pages/code-jams/7/rules.html deleted file mode 100644 index 5979fbe6..00000000 --- a/pydis_site/templates/events/pages/code-jams/7/rules.html +++ /dev/null @@ -1,68 +0,0 @@ -{% extends "events/base_sidebar.html" %} - -{% block title %}Rules{% endblock %} - -{% block breadcrumb %} - <li><a href="{% url "events:index" %}">Events</a></li> - <li><a href="{% url "events:page" path="code-jams" %}">Code Jams</a></li> - <li><a href="{% url "events:page" path="code-jams/7" %}">Summer Code Jam 2020</a></li> - <li class="is-active"><a href="#">Rules</a></li> -{% endblock %} - -{% block event_content %} - <ol> - <li><p>Your solution must use the Django framework. It is not permitted to circumvent this rule by e.g. using Django as a wrapper for another framework.</p></li> - <li><p>Your solution should be platform agnostic. For example, if you use filepaths in your submission, use <code>pathlib</code> to create platform agnostic Path objects instead of hardcoding the paths.</p></li> - <li><p>Your project should be a web app which can be run in a <em>standard web browser</em>.</p></li> - <li> - <p> - You must document precisely how to install and run your project. - This should be as easy as possible, which means you should consider using dependency managers like <code>pipenv</code>, and <code>npm</code>. - We would also encourage you to use <code>docker</code> and <code>docker-compose</code> to containerize your project, but this isn't a requirement. - </p> - </li> - <li> - You must get contributions from every member of your team, if you have an issue with someone on your team please contact a member of the administration team. - These contributions do not necessarily have to be code, for example it's absolutely fine for someone to contribute management, documentation, graphics or audio. - <strong> - Team members that do not contribute will be removed from the Code Jam, and will not receive their share of any prizes the team may win. - They may also be barred from entering future events. - </strong> - </li> - <li><p>You must use GitHub as source control.</p></li> - <li> - <p> - All code and assets must be compatible with the <a href="https://en.wikipedia.org/wiki/MIT_License">MIT license</a>. - This is because we will be merging your submission into our <code>summer-code-jam-2020</code> repo at the end of the jam, - and this repo is licensed with the MIT license. - <strong>Projects that include assets that are incompatible with this license may be disqualified.</strong> - </p> - </li> - <li><p>All code must be written and committed within the time constrictions of the jam. Late commits may be reverted, so make sure you leave enough time to bug test your program.</p></li> - <li> - <p> - Use English as the main language for your project, including names, comments, documentation, and commit messages. - The text displayed in your web application should also be in English, - although you are allowed to provide the user with options for internationalisation and translation. - </p> - </li> - </ol> - - <!-- Change links after migrating them is done. --> - <blockquote> - Please note that our regular - <a href="/pages/rules">community rules</a> and <a href="/pages/code-of-conduct">code of conduct</a> - also apply during the event and that we reserve the right to make changes to these rules at any time. - </blockquote> -{% endblock %} - -{% block sidebar %} - {% include "events/sidebar/code-jams/7.html" %} - <div class="box"> - <p class="menu-label">Relevant Links</p> - <ul class="menu-list"> - <li><a class="has-text-link" href="{% url "events:page" path="code-jams/7" %}">Summer Code Jam 2020</a></li> - <li><a class="has-text-link" href="https://github.com/python-discord/summer-code-jam-2020-qualifier/">Code Jam Qualifier</a></li> - </ul> - </div> -{% endblock %} diff --git a/pydis_site/templates/events/pages/code-jams/8/_index.html b/pydis_site/templates/events/pages/code-jams/8/_index.html deleted file mode 100644 index 628a2c22..00000000 --- a/pydis_site/templates/events/pages/code-jams/8/_index.html +++ /dev/null @@ -1,174 +0,0 @@ -{% extends "events/base_sidebar.html" %} - -{% load static %} - -{% block title %}Summer Code Jam 2021{% endblock %} - -{% block breadcrumb %} - <li><a href="{% url "events:index" %}">Events</a></li> - <li><a href="{% url "events:page" path="code-jams" %}">Code Jams</a></li> - <li class="is-active"><a href="#">Summer Code Jam 2021</a></li> -{% endblock %} - -{% block event_content %} - <p>Twice a year we host a code jam for members of our server to participate in. The code jam is an event where we place you - in a team with 5 other random server members. You then have 8 days to code some sort of application or program in Python. - Your program must use the specified technology/framework and incorporate the theme chosen by the server. - </p> - <p> - After the 8 days is complete, your team has 3 days to finish documentation and create a video presentation showcasing - and walking through the program that your team has created. - </p> - - <h3 id="winners"><a href="#winners">Code Jam Winners</a></h3> - <p>Congratulations to our winners and the two runner ups! Check out their projects below.</p> - - <h4 class="mt-5 mb-2"><i class="fa fa-trophy"></i> Perceptive Porcupines: WTPython!?</h4> - <p class="my-1"><em>VV, Poppinawhile, ethansocal, Jeff Z, Cohan, ¯\_(ツ)_/¯</em></p> - <p class="my-1"> - What the Python (wtpython) is a simple terminal user interface that allows you to explore relevant answers on Stackoverflow without leaving your terminal or IDE. When you get an error, all you have to do is swap python for wtpython. When your code hits an error, you'll see a textual interface for exploring relevant answers allowing you to stay focused and ship faster! - </p> - <p> - <a href="https://www.youtube.com/watch?v=DV3uMdsw9KE" title="Perceptive Porcupines Demo Video" target="_blank" rel="noopener"><i class="fa fa-video"> </i> Demo video</a> - <br/> - <a href="https://github.com/what-the-python/wtpython" title="Perceptive Porcupines GitHub Repository" target="_blank" rel="noopener"><i class="fa fa-github"></i> GitHub Repository</a> - <br/> - - </p> - - <h4 class="mt-5 mb-2"><i class="fa fa-medal"></i> Lovable Lobsters: Ultimate Tic Tac Toe</h4> - <p class="my-1"><em>A5Rocks, Bast, Dacheat, mega_hirtz, CopOnTheRun, richphi</em></p> - <p class="my-1"> - Thinking inside a box, that is inside a box, that is inside yet another box. - - The terminal program created by the Lovable Lobsters allows you to play Ultimate Tic Tac Toe right form your terminal. The really impressive part though? You can play with your friends and family over your network! Their program has a server-client set-up that lets you play with your friends and family from different computers. - </p> - <p> - <a href="https://www.youtube.com/watch?v=WI9tgQeAfXw" title="Lovable Lobsters Demo Video" target="_blank" rel="noopener"><i class="fa fa-video"> </i> Demo video</a> - <br/> - <a href="https://github.com/A5rocks/code-jam-8" title="Lovable Lobsters GitHub Repository" target="_blank" rel="noopener"><i class="fa fa-github"></i> GitHub Repository</a> - <br/> - </p> - - <h4 class="mt-5 mb-2"><i class="fa fa-medal"></i> Robust Reindeer: Rubik's Cube</h4> - <p class="my-1"><em>Björn, aaronshenhao, mathsman, Dude Saber, 詭異, Keppo</em></p> - <p class="my-1"> - This submission is a Rubik's cube, rendered in a text user interface (that was a constraint) using the asciimatics package, and addressing the theme "thinking inside the box". - - Just like a real world Rubik's cube, you can move this cube around to look at it from all sides. And, of course, you can rotate the individual discs it is made up of to first scramble up the order and then to try and solve it into perfect ordering again. - </p> - <p> - <a href="https://github.com/bjoseru/pdcj8-robust-reindeer" title="Robust Reindeer GitHub Repository" target="_blank" rel="noopener"><i class="fa fa-github"></i> GitHub Repository</a> - <br/> - </p> - - <h3 id="submissions"><a href="#submissions">Submissions</a></h3> - <p> - 63 teams started out on July 9th 2021. By the end of the jam, 51 teams made project submissions. Check them all out here: - <div class="has-text-centered"><a class="button is-link" href="submissions">View Submissions</a></div> - </p> - - <h3 id="important-dates"><a href="#important-dates">Important Dates</a></h3> - <ul> - <li>Tuesday, June 15 - Form to submit theme suggestions opens</li> - <li>Monday, June 21 - <a href="https://github.com/python-discord/cj8-qualifier" target="_blank" rel="noopener">The Qualifier</a> is released</li> - <li>Friday, June 25 - Voting for the theme opens</li> - <li>Saturday, June 26 @ 4PM UTC- <a class="has-text-link" href="{% url "events:page" path="code-jams/8/github-bootcamp" %}">GitHub Bootcamp</a></li> - <li>Wednesday, July 1 - The Qualifier closes</li> - <li>Friday, July 9 @ 5PM UTC - Code Jam begins and the theme is announced</li> - <li>Saturday, July 17 @ 5PM UTC - Coding portion of the jam ends</li> - <li>Tuesday, July 20 - Code Jam submissions are closed and video presentation must be submitted</li> - </ul> - - <h3 id="technology"><a href="#technology">Technology</a></h3> - <p> - The chosen technology/tech stack for this year is <strong>Text User Interfaces</strong> (TUIs). - Each team must create a program with one of <a href="{% url "events:page" path="code-jams/8/frameworks" %}">the approved frameworks</a> that creates a user interface that is text based. - For more information of TUIs and what's involved with such an interface, check out <a href="https://en.wikipedia.org/wiki/Text-based_user_interface" target="_blank" rel="noopener">this wikipedia article</a>. - </p> - <h3 id="qualifier"><a href="#qualifier">The Qualifier</a></h3> - <p> - The qualifier is a coding challenge that you are required to complete before registering for the code jam. - This is meant as a basic assessment of your skills to ensure you have enough python knowledge to effectively contribute in a team environment. - </p> - <p class="has-text-centered"><a class="button is-link" href="https://github.com/python-discord/cj8-qualifier" target="_blank" rel="noopener">View the Qualifier</a></p> - <p> - Please note the requirements for the qualifier. - <ul> - <li>The qualifier must be completed using Python 3.9</li> - <li>No external modules are allowed, only those available through the standard library.</li> - <li>The Qualifier must be submitted through the Code Jam sign-up form.</li> - </ul> - </p> - - <h3 id="prizes"><a href="#prizes">Prizes</a></h3> - <p> - Our Code Jam Sponsors have provided prizes for the winners of the code jam. - Also, a big thank you to our Patreon patrons for supporting this server and allowing us - to provide our prizes as well. - </p> - <!-- This is going to be the sponsor section --> - <div class="card mb-4"> - <div class="card-content"> - <div class="media"> - <div class="media-left" style="max-width:150px"> - <img src="{% static "images/events/DO_Logo_Vertical_Blue.png" %}" alt="Digital Ocean"> - </div> - <div class="media-content"> - <p class="subtitle has-link"><a href="https://www.digitalocean.com/" target="_blank" rel="noopener">DigitalOcean</a></p> - <p class="is-italic"> - Scalable compute platform with add-on storage, security, and monitoring capabilities. - We make it simple to launch in the cloud and scale up as you grow—whether you’re running one virtual machine or ten thousand. - </p> - <p><strong>Prizes</strong><br> - $250 in DigitalOcean credits to the members of a winning team.</p> - </div> - </div> - </div> - </div> - - <div class="card mb-4"> - <div class="card-content"> - <div class="media"> - <div class="media-left" style="max-width:150px"> - <img src="{% static "images/sponsors/jetbrains.png" %}" alt="JetBrains"> - </div> - <div class="media-content"> - <p class="subtitle has-link"><a href="https://www.jetbrains.com/" target="_blank" rel="noopener">JetBrains</a></p> - <p class="is-italic"> - Whatever platform or language you work with, JetBrains has a development tool for you. - We help developers work faster by automating common, repetitive tasks to enable them to stay focused on code design and the big picture. - We provide tools to explore and familiarize with code bases faster. Our products make it easy for you to take care of quality during all stages of development and spend less time on maintenance tasks. - </p> - <p><strong>Prizes</strong><br> - 1-year JetBrain licenses to the members of a winning team.</p> - </div> - </div> - </div> - </div> - <div class="card mb"> - <div class="card-content"> - <div class="media"> - <div class="media-left" style="max-width:150px"> - <img src="{% static "images/events/Tabnine.png" %}" alt="Tabnine"> - </div> - <div class="media-content"> - <p class="subtitle has-link"><a href="https://www.tabnine.com/now?utm_source=discord&utm_medium=Ins&utm_campaign=PythonDis" target="_blank" rel="noopener">Tabnine</a></p> - <p class="is-italic">Tabnine is an AI-powered code completion tool used by millions of devs around the world every day - - Tabnine supports dozens of programming languages, in all of your favorite IDEs, saving you tons of time - so that you can type less and code more. - Tabnine comes as a plugin and has a free-forever basic plan, so you can get started with it right away! - </p> - <p><strong>Prizes</strong><br> - 1-year Pro Licenses to Tabnine to the members of a winning team.</p> - </div> - </div> - </div> - </div> - -{% endblock %} - -{% block sidebar %} - - {% include "events/sidebar/code-jams/8.html" %} - -{% endblock %} diff --git a/pydis_site/templates/events/pages/code-jams/8/frameworks.html b/pydis_site/templates/events/pages/code-jams/8/frameworks.html deleted file mode 100644 index 1c02e38a..00000000 --- a/pydis_site/templates/events/pages/code-jams/8/frameworks.html +++ /dev/null @@ -1,121 +0,0 @@ -{% extends "events/base_sidebar.html" %} - -{% load static %} - -{% block title %}Summer Code Jam 2021{% endblock %} - -{% block breadcrumb %} - <li><a href="{% url "events:index" %}">Events</a></li> - <li><a href="{% url "events:page" path="code-jams" %}">Code Jams</a></li> - <li><a href="{% url "events:page" path="code-jams/8" %}">Summer Code Jam 2021</a></li> - <li class="is-active"><a href="#">Approved Frameworks</a></li> -{% endblock %} - -{% block event_content %} - <p>Below is the list of approved frameworks that you can use for the code jam. Please take note of what frameworks are available for which platform. - Please work with your team to choose a library that everyone can develop on, whether it's cross platform or something you can use WSL or a Virtual Machine for. - </p> - <h3 id="urwid"><a href="#urwid">Urwid</a></h3> - <div class="columns"> - <div class="column"> - <ul> - <li><a href="https://urwid.org/" target="_blank">Documentation Link</a></li> - <li><strong>Supports:</strong> Linux, Mac, other unix-like OS</li> - <li>Somewhat in-depth tutorial</li> - <li>Uses widgets in a fairly straight forward design</li> - <li>Docs include tutorials of both functional and class-based examples</li> - </ul> - </div> - <div class="column"> - <img src="{% static "images/events/summer_code_jam_2021/cj8_urwid.png" %}" alt="urwid"> - </div> - </div> - <h3 id="curses"><a href="#curses">Curses</a></h3> - <div class="columns"> - <div class="column"> - <ul> - <li><a href="https://docs.python.org/3/howto/curses.html" target="_blank">Documentation Link</a></li> - <li><strong>Supports:</strong> Linux and other unix-like OS</li> - <li>Part of the standard library</li> - <li>Extensive how-to guide</li> - <li>Very basic, more effort to get working/looking good</li> - <li>To supplement curses the following libraries are approved, although no guarantees are made for stability.</li> - <ul> - <li><a href="https://pypi.org/project/UniCurses/" target="_blank">Unicurses</a> - a wrapper for Python 3.x that provides a unified set of Curses functions of all platforms.</li> - <li><a href="https://github.com/salt-die/nurses" target="_blank">Nurses</a> - a combination of curses and numpy, made by our very own salt-die</li> - </ul> - - </ul> - </div> - <div class="column"> - <img src="{% static "images/events/summer_code_jam_2021/cj8_curses.png" %}" alt="curses"> - </div> - </div> - <h3 id="blessed"><a href="#blessed">Blessed</a></h3> - <div class="columns"> - <div class="column"> - <ul> - <li><a href="https://blessed.readthedocs.io/en/latest/intro.html" target="_blank">Documentation Link</a></li> - <li><strong>Supports:</strong> Linux, Mac, and Windows</li> - <li>Sits on top of curses to add more pythonic bindings</li> - <li>Doesn't provide any widgets or layouts</li> - </ul> - </div> - <div class="column"> - <img src="{% static "images/events/summer_code_jam_2021/cj8_blessed.gif" %}" alt="blessed"> - </div> - </div> - <h3 id="rich"><a href="#rich">Rich</a></h3> - <div class="columns"> - <div class="column"> - <ul> - <li><a href="https://rich.readthedocs.io/en/stable/introduction.html" target="_blank">Documentation Link</a></li> - <li><strong>Supports:</strong> Linux, Mac, and Windows</li> - <li>Documentation is good and overall is very OOP focused</li> - <li>Robust with many features and example snippets</li> - <li>To supplement Rich the following library is approved, although no guarantees are made for stability.</li> - <ul> - <li><a href="https://github.com/willmcgugan/textual" target="_blank">Textual</a> - a TUI framework using Rich as the render. - <br>It is still under active development, is only available on Mac/Linux, and is not stable yet.</li> - </ul> - </ul> - </div> - <div class="column"> - <img src="{% static "images/events/summer_code_jam_2021/cj8_rich.gif" %}" alt="rich"> - </div> - </div> - <h3 id="asciimatics"><a href="#asciimatics">Asciimatics</a></h3> - <div class="columns"> - <div class="column"> - <ul> - <li><a href="https://asciimatics.readthedocs.io/en/stable/intro.html" target="_blank">Documentation Link</a></li> - <li><strong>Supports:</strong> Linux, Mac, and Windows</li> - <li>Documentation is well structured and straightforward to navigate</li> - </ul> - </div> - <div class="column"> - <img src="{% static "images/events/summer_code_jam_2021/cj8_asciimatics.png" %}" alt="asciimatics"> - </div> - </div> - <h3 id="prompt-toolkit"><a href="#prompt-toolkit">Python Prompt Toolkit</a></h3> - <div class="columns"> - <div class="column"> - <ul> - <li><a href="https://python-prompt-toolkit.readthedocs.io/en/stable/" target="_blank">Documentation Link</a></li> - <li><strong>Supports:</strong> Linux, Mac, and Windows</li> - <li>Pure python library</li> - </ul> - </div> - <div class="column"> - <img src="{% static "images/events/summer_code_jam_2021/cj8_prompttoolkit.png" %}" alt="python prompt toolkit"> - </div> - </div> - - -{% endblock %} - -{% block sidebar %} - - {% include "events/sidebar/code-jams/8.html" %} - -{% endblock %} diff --git a/pydis_site/templates/events/pages/code-jams/8/github-bootcamp.html b/pydis_site/templates/events/pages/code-jams/8/github-bootcamp.html deleted file mode 100644 index 836ed3ed..00000000 --- a/pydis_site/templates/events/pages/code-jams/8/github-bootcamp.html +++ /dev/null @@ -1,45 +0,0 @@ -{% extends "events/base_sidebar.html" %} - -{% load static %} - -{% block title %}Summer Code Jam 2021{% endblock %} - -{% block breadcrumb %} - <li><a href="{% url "events:index" %}">Events</a></li> - <li><a href="{% url "events:page" path="code-jams" %}">Code Jams</a></li> - <li><a href="{% url "events:page" path="code-jams/8" %}">Summer Code Jam 2021</a></li> - <li class="is-active"><a href="#">GitHub Bootcamp</a></li> -{% endblock %} - -{% block event_content %} - <p> - <strong>This year we'll be running a GitHub Bootcamp on Saturday, June 26th at 4PM UTC.</strong> - This bootcamp is intended to be an interactive workshop session where we help participants - get setup with git and using it in a team environment. If you are new to git or would like to brush up on it, - then this is the perfect event for you. - </p> - <p> - The instructional parts of this bootcamp will be recorded. We'll also be providing a cheatsheet / reference guide to all attendees that will - be made available here for download. - </p> - <h3>Session 1: Setting up Git</h3> - <p> - This first session will run for an hour starting at 4PM UTC. It will be focused on installing and setting up git for the first time. - We'll have helpers available to help you set up git and integrated with your editor of choice. - Whether it's PyCharm, VSCode, or vim, we can help you get rolling with version control. - </p> - <h3>Session 2: Using git and github in a team environment</h3> - <p> - This session will run for an hour starting at 5PM UTC. It will be focused on using git and GitHub in a team environment. - You'll be put in a group with other attendees and use the common git commands for working in a repo. - You'll learn how to set-up a GitHub repository with other contributors, how to make branches, make commits, pull down changes, and then make pull requests. - We'll also be going over different strategies for creating branches and best practices for commits and pull requests. - </p> - -{% endblock %} - -{% block sidebar %} - - {% include "events/sidebar/code-jams/8.html" %} - -{% endblock %} diff --git a/pydis_site/templates/events/pages/code-jams/8/rules.html b/pydis_site/templates/events/pages/code-jams/8/rules.html deleted file mode 100644 index b1220a2d..00000000 --- a/pydis_site/templates/events/pages/code-jams/8/rules.html +++ /dev/null @@ -1,69 +0,0 @@ -{% extends "events/base_sidebar.html" %} - -{% block title %}Summer Code Jam 2021{% endblock %} - -{% block breadcrumb %} - <li><a href="{% url "events:index" %}">Events</a></li> - <li><a href="{% url "events:page" path="code-jams" %}">Code Jams</a></li> - <li><a href="{% url "events:page" path="code-jams/8" %}">Summer Code Jam 2021</a></li> - <li class="is-active"><a href="#">Rules</a></li> -{% endblock %} - -{% block event_content %} -<ol> - <li><p>Your solution must use one of the approved frameworks.</p></li> - <li> - <p> - You must document precisely how to install and run your project. - This should be as easy as possible, which means you should consider using dependency managers like <code>pipenv</code> or <code>poetry</code>. - We would also encourage you to use <code>docker</code> and <code>docker-compose</code> to containerize your project, but this isn't a requirement. - </p> - </li> - <li> - You must get contributions from every member of your team, if you have an issue with someone on your team please contact a member of the administration team. - These contributions do not necessarily have to be code, for example it's absolutely fine for someone to contribute management, documentation, graphics or audio. - <strong> - Team members that do not contribute will be removed from the Code Jam, and will not receive their share of any prizes the team may win. - They may also be barred from entering future events. - </strong> - </li> - <li><p>You must use GitHub as source control.</p></li> - <li> - <p> - All code and assets must be compatible with the <a href="https://en.wikipedia.org/wiki/MIT_License">MIT license</a>. - This is because we will be merging your submission into our <code>summer-code-jam-2021</code> repo at the end of the jam, - and this repo is licensed with the MIT license. - <strong>Projects that include assets that are incompatible with this license may be disqualified.</strong> - </p> - </li> - <li><p>All code must be written and committed within the time constrictions of the jam. Late commits may be reverted, so make sure you leave enough time to bug test your program.</p></li> - <li> - <p> - Use English as the main language for your project, including names, comments, documentation, and commit messages. - The text displayed in your application should also be in English, - although you are allowed to provide the user with options for internationalisation and translation. - </p> - </li> - <li> - <p> - Your team, once the coding portion of the code jam is complete, must create a video presentation that showcases and explains your final product. - This must be in a video format and must be uploaded somewhere for the judges to view (i.e. unlisted YouTube video, Vimeo, etc.). - The video can be as simple as a screen recording with annotated text. - Teams who do not submit a final video presentation may be disqualified. - </p> - </li> -</ol> - -<!-- Change links after migrating them is done. --> -<blockquote> - Please note that our regular - <a href="/pages/rules">community rules</a> and <a href="/pages/code-of-conduct">code of conduct</a> - also apply during the event and that we reserve the right to make changes to these rules at any time. -</blockquote> -{% endblock %} - -{% block sidebar %} - - {% include "events/sidebar/code-jams/8.html" %} - -{% endblock %} diff --git a/pydis_site/templates/events/pages/code-jams/8/submissions.html b/pydis_site/templates/events/pages/code-jams/8/submissions.html deleted file mode 100644 index 16309bd3..00000000 --- a/pydis_site/templates/events/pages/code-jams/8/submissions.html +++ /dev/null @@ -1,484 +0,0 @@ -{% extends "events/base_sidebar.html" %} - -{% load static %} - -{% block title %}Summer Code Jam 2021{% endblock %} - -{% block breadcrumb %} - <li><a href="{% url "events:index" %}">Events</a></li> - <li><a href="{% url "events:page" path="code-jams" %}">Code Jams</a></li> - <li><a href="{% url "events:page" path="code-jams/8" %}">Summer Code Jam 2021</a></li> - <li class="is-active"><a href="#">Submissions</a></li> -{% endblock %} - -{% block event_content %} - <p> - Below is a list of all projects submitted by the end of Summer Code Jam 2021 - </p> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Acute Alligators</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/n0remac/Acute-Alligators-2021-Summer-Code-Jam" title="Acute Alligators' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Adaptable Antelopes</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/BoraxTheClean/adaptable-antelopes" title="Adaptable Antelopes' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Astounding Arapaimas</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/p0lygun/astounding-arapaimas" title="Astounding Arapaimas' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Beatific Bulldogs</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/beatific-bulldogs/code-jam" title="Beatific Bulldogs' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Benevolent Bonobos</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/Anand1310/summer-code-jam-2021" title="Benevolent Bonobos' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Blessed Badgers</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/StephD/CJ8-blessed-badgers" title="Blessed Badgers' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Bright Bluefins</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/DavinderJolly/bright-bluefins/" title="Bright Bluefins' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Businesslike Buffalo</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/Sahaj001/Businesslike_Buffalo" title="Businesslike Buffalo's Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Canny Capybaras</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/aiyayayaya/canny-capybaras-collab-code-contest" title="Canny Capybaras' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Cheerful Cheetahs</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/cj8-cheerful-cheetahs/project" title="Cheerful Cheetahs' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Classic Clownfish</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/Istalantar/SCJ-2021-classic-clownfish" title="Classic Clownfish's Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Considerate Coatis</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/yashkir/considerate-coatis" title="Considerate Coatis' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Dedicated Dugongs</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/Areking-RS/Code-jam-2021" title="Dedicated Dugongs' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Discrete Dingos</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/somthecoder/CodeJam-Discrete-Dingos" title="Discrete Dingos' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Enlightened Elks</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/GriceTurrble/enlightened-elks-codejam/" title="Enlightened Elks' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Esteemed Emus</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/Vthechamp22/esteemed-emus" title="Esteemed Emus' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Favorable Fishers</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/UntriexTv/favorable-fishers" title="Favorable Fishers' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Feisty Ferrets</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/ToxicKidz/summer-code-jam-8" title="Feisty Ferrets' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Gallant Grasshoppers</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/StackedQueries/gallant-grasshoppers" title="Gallant Grasshoppers' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Grand Geckos</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/imsofi/codejam-grand-geckos/" title="Grand Geckos' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Hospitable Hares</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/samarthkulshrestha/hospitable-hares_code-jam-8" title="Hospitable Hares' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Humorous Honeybees</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/mirandazellnik/code-jam-2021" title="Humorous Honeybees' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Jaunty Jackals</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/Jaunty-Jackals/jaunty-jackals" title="Jaunty Jackals' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Jazzed Jerboas</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/tomheaton/pcj8-jazzed-jerboas" title="Jazzed Jerboas' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Jubilant Jellyfish</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/Cheepsss/Jubilant-Jellyfish" title="Jubilant Jellyfish's Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Lovable Lobsters</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/A5rocks/code-jam-8" title="Lovable Lobsters' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Magical Muskrats</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/harjyotbagga/escape-room" title="Magical Muskrats' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Mature Magpies</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/Mature-Magpies/think-inside-the-box" title="Mature Magpies' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Merciful Millipedes</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/Reuben27/Merciful-Millipedes" title="Merciful Millipedes' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Meteoric Minks</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/meteoric-minks/code-jam" title="Meteoric Minks' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Modern Meerkats</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/Ahmed-Khaled-dev/modern-meerkats" title="Modern Meerkats' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Notable Newts</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/meysam81/notable-newts" title="Notable Newts' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Notorious Narwhals</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/smileyface12349/notorious-narwhals" title="Notorious Narwhals' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Patient Panthers</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/Willd14469/cj8-patient-panthers" title="Patient Panthers' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Perceptive Porcupines</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/what-the-python/wtpython" title="Perceptive Porcupines' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Poetic Pumas</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/roogla/poetic_pumas" title="Poetic Pumas' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Purposeful Pangolins</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/doodletaco/dataset-viewer" title="Purposeful Pangolins' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Quirky Quokkas</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/joshuacc1/Music-Player-CLI-Anywhere" title="Quirky Quokkas' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Respectful Racoons</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/manjunaath5583/respectful_racoons" title="Respectful Racoons' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Rhapsodic Rabbits</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/yummyyummybaguette/rhapsodic-rabbits" title="Rhapsodic Rabbits' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Robust Reindeer</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/bjoseru/pdcj8-robust-reindeer" title="Robust Reindeer's Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Scholarly Skunks</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/Davidy22/scholarlySkunkJam/" title="Scholarly Skunks' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Secretive Squirrels</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/dain-xyz/python-jam-2021-2" title="Secretive Squirrels' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Sleek Snails</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/Sleek-Snails/Snail-Snacks" title="Sleek Snails' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Spellbinding Squids</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/SystematicError/code-jam" title="Spellbinding Squids' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Stylish Salamanders</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/Darklight-user/code-jam-stylish-salamanders" title="Stylish Salamanders' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Tactful Tunas</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/Yagueteiro/code-jam-2021/" title="Tactful Tunas' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Transcendent Tarsiers</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/kronifer/cj8-repo" title="Transcendent Tarsiers' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Tubular Terriers</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/Tubular-Terriers/code-jam" title="Tubular Terriers' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Virtuous Vultures</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/iceCream-Taco/cj8-virtuous-vultures" title="Virtuous Vultures' Repository">GitHub</a></p> - </div> - </div> - - <div class="columns is-mobile is-centered"> - <div class="column is-half"> - <p class="has-text-centered">Whimsical Woolly Mammoths</p> - </div> - <div class="column is-half"> - <p class="has-text-centered"><a href="https://github.com/SilverSlashDiscord/whimsical-woolly-mammoths" title="Whimsical Woolly Mammoths' Repository">GitHub</a></p> - </div> - </div> - -{% endblock %} - -{% block sidebar %} - - {% include "events/sidebar/code-jams/8.html" %} - -{% endblock %} diff --git a/pydis_site/templates/events/pages/code-jams/9/_index.html b/pydis_site/templates/events/pages/code-jams/9/_index.html deleted file mode 100644 index ca7c4f90..00000000 --- a/pydis_site/templates/events/pages/code-jams/9/_index.html +++ /dev/null @@ -1,127 +0,0 @@ -{% extends "events/base_sidebar.html" %} - -{% load static %} - -{% block title %}Summer Code Jam 2022{% endblock %} - -{% block breadcrumb %} - <li><a href="{% url "events:index" %}">Events</a></li> - <li><a href="{% url "events:page" path="code-jams" %}">Code Jams</a></li> - <li class="is-active"><a href="#">Summer Code Jam 2022</a></li> -{% endblock %} - -{% block event_content %} - <p>Once a year we host a code jam for members of our server to participate in. The code jam is an event where we place you - in a team with 5 other random server members. You then have 11 days to code some sort of application or program in Python. - Your program must use the specified technology/framework and incorporate the theme chosen by the server. - </p> - <p> - After the 11 days are complete, your team has 4 days to finish documentation and create a video presentation showcasing - and walking through the program that your team has created. More details and specifics of this will be released within the next 2 weeks. - </p> - - <h3 id="important-dates"><a href="#important-dates">Important Dates</a></h3> - <ul> - <li><strike>Saturday, June 18 - Form to submit theme suggestions opens</strike></li> - <li><strike>Wednesday, June 29 - The Qualifier is released</strike></li> - <li><strike>Wednesday, July 6 - Voting for the theme opens</strike></li> - <li><strike>Wednesday, July 13 - The Qualifier closes</strike></li> - <li><strike>Thursday, July 21 - Code Jam Begins</strike></li> - <li><strike>Sunday, July 31 - Coding portion of the jam ends</strike></li> - <li><strike>Sunday, August 4 - Code Jam submissions are closed</strike></li> - </ul> - - <h3 id="qualifier"><a href="#how-to-join">The Qualifier</a></h3> - <p> - The qualifier is a coding challenge that you are required to complete before registering for the code jam. - This is meant as a basic assessment of your skills to ensure you have enough python knowledge to effectively contribute in a team environment. - </p> - <p class="has-text-centered"><a class="button is-link" href="https://github.com/python-discord/code-jam-qualifier-9/" target="_blank" rel="noopener">View the Qualifier</a></p> - <p> - Please note the requirements for the qualifier. - <ul> - <li>The qualifier must be completed using Python 3.10</li> - <li>No external modules are allowed, only those available through the standard library.</li> - <li>The Qualifier must be submitted through the Code Jam sign-up form.</li> - </ul> - </p> - - <h3 id="technology"><a href="#technology">Technology</a></h3> - <p> - The chosen technology/tech stack for this year is <strong>WebSockets</strong>. - Each team must make use of <a href="{% url "events:page" path="code-jams/9/frameworks" %}">the approved frameworks</a> to create a WebSockets-based app. - For more information of websockets, check out <a href="https://en.wikipedia.org/wiki/WebSocket" target="_blank" rel="noopener">this wikipedia article</a>. - </p> - - <h3 id="prizes"><a href="#prizes">Prizes</a></h3> - <p> - Our Code Jam Sponsors have provided prizes for the winners of the code jam. - Also, thanks to our Patreon patrons supporting this server, we are able to send members of the winning teams - Python Discord t-shirts and possibly other goodies. - </p> - - <div class="card mb-4"> - <div class="card-content"> - <div class="media"> - <div class="media-left" style="max-width:150px"> - <img src="{% static "images/events/DO_Logo_Vertical_Blue.png" %}" alt="Digital Ocean"> - </div> - <div class="media-content"> - <p class="subtitle has-link"><a href="https://www.digitalocean.com/" target="_blank" rel="noopener">DigitalOcean</a></p> - <p class="is-italic"> - Scalable compute platform with add-on storage, security, and monitoring capabilities. - We make it simple to launch in the cloud and scale up as you grow—whether you’re running one virtual machine or ten thousand. - </p> - <p><strong>Prizes</strong><br> - DigitalOcean credits to the members of a winning team.</p> - </div> - </div> - </div> - </div> - - <div class="card mb-4"> - <div class="card-content"> - <div class="media"> - <div class="media-left" style="max-width:150px"> - <img src="{% static "images/sponsors/jetbrains.png" %}" alt="JetBrains"> - </div> - <div class="media-content"> - <p class="subtitle has-link"><a href="https://www.jetbrains.com/" target="_blank" rel="noopener">JetBrains</a></p> - <p class="is-italic"> - Whatever platform or language you work with, JetBrains has a development tool for you. - We help developers work faster by automating common, repetitive tasks to enable them to stay focused on code design and the big picture. - We provide tools to explore and familiarize with code bases faster. Our products make it easy for you to take care of quality during all stages of development and spend less time on maintenance tasks. - </p> - <p><strong>Prizes</strong><br> - 1-year JetBrain licenses to the members of a winning team.</p> - </div> - </div> - </div> - </div> - - <div class="card mb"> - <div class="card-content"> - <div class="media"> - <div class="media-left" style="max-width:150px"> - <img src="{% static "images/events/Replit.png" %}" alt="Replit"> - </div> - <div class="media-content"> - <p class="subtitle has-link"><a href="https://www.replit.com" target="_blank" rel="noopener">Replit</a></p> - <p class="is-italic">Start coding instantly, right from your browser. - With GitHub integration and support for nearly every major programming language, Replit is the best place to code. - Our mission is to bring the next billion software creators online. - We build powerful, simple tools and platforms for learners, educators, and developers. - </p> - <p><strong>Prizes</strong><br> - Three months of the Replit hacker plan to the members of a winning team.</p> - </div> - </div> - </div> - </div> -{% endblock %} - -{% block sidebar %} - - {% include "events/sidebar/code-jams/9.html" %} - -{% endblock %} diff --git a/pydis_site/templates/events/pages/code-jams/9/frameworks.html b/pydis_site/templates/events/pages/code-jams/9/frameworks.html deleted file mode 100644 index b462c733..00000000 --- a/pydis_site/templates/events/pages/code-jams/9/frameworks.html +++ /dev/null @@ -1,148 +0,0 @@ -{% extends "events/base_sidebar.html" %} - -{% load static %} - -{% block title %}Summer Code Jam 2022{% endblock %} - -{% block breadcrumb %} - <li><a href="{% url "events:index" %}">Events</a></li> - <li><a href="{% url "events:page" path="code-jams" %}">Code Jams</a></li> - <li><a href="{% url "events:page" path="code-jams/9" %}">Summer Code Jam 2022</a></li> - <li class="is-active"><a href="#">Approved Frameworks</a></li> -{% endblock %} - -{% block event_content %} - <p>Below is the list of approved frameworks that you can use for the code jam. - Please work with your team to choose a library that everyone can and want to develop with. - If there is a library not listed below that you think should be here, you're welcome to discuss it with the Events Team over at <a href="https://discord.gg/HnGd3znxhJ">the server</a>. - </p> - - <div class="notification is-info is-light"> - <p>Most of the below frameworks implement what is called the ASGI Specification. - This specification documents how the frameworks should interact with ASGI servers. - You are also allowed to <strong>work with the ASGI specification directly</strong> without a framework, if your team so chooses to. - Refer to the <a href="https://asgi.readthedocs.io/en/latest/">specification online</a>. - </p> - </div> - - <h3 id="approved-frameworks"><a href="#approved-frameworks">Approved Frameworks</a></h3> - - <div class="card mb-4"> - <div class="card-content"> - <div class="content"> - <p class="subtitle">FastAPI</p> - <p>FastAPI is a modern web framework great for WebSockets based on standard Python type hints which provides great editor support.</p> - </div> - </div> - <div class="card-footer"> - <a href="https://fastapi.tiangolo.com/advanced/websockets" class="card-footer-item"><i class="fas fa-book"></i> Documentation</a> - <a href="https://github.com/tiangolo/fastapi" class="card-footer-item"><i class="fab fa-github"></i> GitHub</a> - </div> - </div> - - <div class="card mb-4"> - <div class="card-content"> - <div class="content"> - <p class="subtitle">Starlette</p> - <p>Starlette is a lightweight ASGI framework/toolkit, which is ideal for building async web services in Python. - </p> - </div> - </div> - <div class="card-footer"> - <a href="https://www.starlette.io/websockets" class="card-footer-item"><i class="fas fa-book"></i> Documentation</a> - <a href="https://github.com/encode/starlette" class="card-footer-item"><i class="fab fa-github"></i> GitHub</a> - </div> - </div> - - <div class="card mb-4"> - <div class="card-content"> - <div class="content"> - <p class="subtitle">websockets</p> - <p>websockets is a library for building both WebSocket clients and servers with focus on simplicity and performance. - </p> - </div> - </div> - <div class="card-footer"> - <a href="https://websockets.readthedocs.io/en/stable" class="card-footer-item"><i class="fas fa-book"></i> Documentation</a> - <a href="https://github.com/aaugustin/websockets" class="card-footer-item"><i class="fab fa-github"></i> GitHub</a> - </div> - </div> - - <div class="card mb-4"> - <div class="card-content"> - <div class="content"> - <p class="subtitle">aiohttp</p> - <p>aiohttp provides both a client and server WebSocket implementation, while avoiding callback-hell. - </p> - </div> - </div> - <div class="card-footer"> - <a href="https://docs.aiohttp.org/en/stable/client_quickstart.html#websockets" class="card-footer-item"><i class="fas fa-book"></i> Documentation</a> - <a href="https://github.com/aio-libs/aiohttp" class="card-footer-item"><i class="fab fa-github"></i> GitHub</a> - </div> - </div> - - <div class="card mb-4"> - <div class="card-content"> - <div class="content"> - <p class="subtitle">Django Channels</p> - <p>Django Channels adds WebSocket-support to Django - built on ASGI like other web frameworks. - </p> - </div> - </div> - <div class="card-footer"> - <a href="https://channels.readthedocs.io/en/stable" class="card-footer-item"><i class="fas fa-book"></i> Documentation</a> - <a href="https://github.com/django/channels" class="card-footer-item"><i class="fab fa-github"></i> GitHub</a> - </div> - </div> - - <div class="card mb-4"> - <div class="card-content"> - <div class="content"> - <p class="subtitle">Starlite</p> - <p>Starlite is a light and flexible ASGI API framework, using Starlette and Pydantic as foundations. - </p> - </div> - </div> - <div class="card-footer"> - <a href="https://starlite-api.github.io/starlite" class="card-footer-item"><i class="fas fa-book"></i> Documentation</a> - <a href="https://github.com/starlite-api/starlite" class="card-footer-item"><i class="fab fa-github"></i> GitHub</a> - </div> - </div> - - <div class="card mb-4"> - <div class="card-content"> - <div class="content"> - <p class="subtitle">Sanic</p> - <p>Sanic is an ASGI compliant web framework designed for speed and simplicity. - </p> - </div> - </div> - <div class="card-footer"> - <a href="https://sanic.dev/en/guide/advanced/websockets.html" class="card-footer-item"><i class="fas fa-book"></i> Documentation</a> - <a href="https://github.com/sanic-org/sanic" class="card-footer-item"><i class="fab fa-github"></i> GitHub</a> - </div> - </div> - - <div class="card mb-4"> - <div class="card-content"> - <div class="content"> - <p class="subtitle">wsproto</p> - <p>wsproto is a pure-Python WebSocket protocol stack written to be as flexible as possible by having the user build the bridge to the I/O. - </p> - </div> - </div> - <div class="card-footer"> - <a href="https://python-hyper.org/projects/wsproto/en/stable" class="card-footer-item"><i class="fas fa-book"></i> Documentation</a> - <a href="https://github.com/python-hyper/wsproto" class="card-footer-item"><i class="fab fa-github"></i> GitHub</a> - </div> - </div> - - -{% endblock %} - -{% block sidebar %} - - {% include "events/sidebar/code-jams/9.html" %} - -{% endblock %} diff --git a/pydis_site/templates/events/pages/code-jams/9/rules.html b/pydis_site/templates/events/pages/code-jams/9/rules.html deleted file mode 100644 index 9a28852f..00000000 --- a/pydis_site/templates/events/pages/code-jams/9/rules.html +++ /dev/null @@ -1,80 +0,0 @@ -{% extends "events/base_sidebar.html" %} - -{% block title %}Summer Code Jam 2022{% endblock %} - -{% block breadcrumb %} - <li><a href="{% url "events:index" %}">Events</a></li> - <li><a href="{% url "events:page" path="code-jams" %}">Code Jams</a></li> - <li><a href="{% url "events:page" path="code-jams/9" %}">Summer Code Jam 2022</a></li> - <li class="is-active"><a href="#">Rules</a></li> -{% endblock %} - -{% block event_content %} -<ol> - <li><p>Your solution must use one of the approved frameworks. It is not permitted to circumvent this rule by e.g. using the approved framework as a wrapper for another framework.</p></li> - <li> - <p> - <strong>The core of your project must use WebSockets as its communication protocol.</strong>. - This means that you are allowed to use other methods of communication where WebSockets cannot be implemented, however, that should be a non-significant portion of your project. - For example, serving static files for a website cannot be done over WebSockets and it does not pose as a significant portion of a project, therefore it is allowed. - </p> - - <p>This rule does not apply to databases and files when used for <i>storage purposes</i> even though that may be a significant portion of your project. Working with subprocesses (through stdin/stdout or <code>multiprocessing.Pool()</code>/<code>concurrent.futures.ProcessPoolExecutor()</code>) is also exempt from this rule.</p> - - <p>If you're unsure about your use of non-WebSocket communication, please reach out to the events team.</p> - </li> - <li><p>Your solution should be platform agnostic. For example, if you use filepaths in your submission, use <code>pathlib</code> to create platform agnostic Path objects instead of hardcoding the paths.</p></li> - <li> - <p> - You must document precisely how to install and run your project. - This should be as easy as possible, which means you should consider using dependency managers like <code>pipenv</code> or <code>poetry</code>. - We would also encourage you to use <code>docker</code> and <code>docker-compose</code> to containerize your project, but this isn't a requirement. - </p> - </li> - <li> - You must get contributions from every member of your team, if you have an issue with someone on your team please contact a member of the administration team. - These contributions do not necessarily have to be code, for example it's absolutely fine for someone to contribute management, documentation, graphics or audio. - <strong> - Team members that do not contribute will be removed from the Code Jam, and will not receive their share of any prizes the team may win. - They may also be barred from entering future events. - </strong> - </li> - <li><p>You must use GitHub as source control.</p></li> - <li> - <p> - All code and assets must be compatible with the <a href="https://en.wikipedia.org/wiki/MIT_License">MIT license</a>. - This is because we will be merging your submission into our <code>summer-code-jam-2022</code> repo at the end of the jam, - and this repo is licensed with the MIT license. - <strong>Projects that include assets that are incompatible with this license may be disqualified.</strong> - </p> - </li> - <li><p>All code must be written and committed within the time constrictions of the jam. Late commits may be reverted, so make sure you leave enough time to bug test your program.</p></li> - <li> - <p> - Use English as the main language for your project, including names, comments, documentation, and commit messages. - Any text displayed in your application should also be in English, - although you are allowed to provide the user with options for internationalisation and translation. - </p> - </li> - <li> - <p> - Your team, once the coding portion of the code jam is complete, must create a video presentation that showcases and explains your final product. - This must be in a video format and must be uploaded somewhere for the judges to view (i.e. unlisted Youtube video, Vimeo, etc.) - The video can be as simple as a screen recording with annotated text. - Teams who do not submit a final video presentation may be disqualified. - </p> - </li> -</ol> - -<blockquote> - Please note that our regular - <a href="/pages/rules">community rules</a> and <a href="/pages/code-of-conduct">code of conduct</a> - also apply during the event and that we reserve the right to make changes to these rules at any time. -</blockquote> -{% endblock %} - -{% block sidebar %} - - {% include "events/sidebar/code-jams/9.html" %} - -{% endblock %} diff --git a/pydis_site/templates/events/pages/code-jams/_index.html b/pydis_site/templates/events/pages/code-jams/_index.html deleted file mode 100644 index c7975679..00000000 --- a/pydis_site/templates/events/pages/code-jams/_index.html +++ /dev/null @@ -1,71 +0,0 @@ -{% extends "events/base_sidebar.html" %} - -{% block breadcrumb %} - <li><a href="{% url "events:index" %}">Events</a></li> - <li class="is-active"><a href="#">Code Jams</a></li> -{% endblock %} - -{% block title %}Code Jams{% endblock %} - -{% block event_content %} - <p> - If you've been around the server for a while, or you just happened to join at the right time, - you may have heard of something known as a Code Jam. - This page will help answer some questions you may have about them. - </p> - - <h2 class="title is-4" id="what-is-code-jam"><a href="#what-is-code-jam">What is a Code Jam?</a></h2> - <p> - This is the question you might be asking yourself right now. - A Code Jam is a chance to create something with a team. - In each jam, you are paired up with a group of other users just like yourself who will then be given a type of program to make and a theme to help guide it. - You then have a little over a week's time to create the best project you can. - </p> - - <h2 class="title is-4" id="how-do-i-join"><a href="#how-do-i-join">How do I join?</a></h2> - <p> - Before each Code Jam, we open up a Qualifier. - The Qualifier is a challenge program that you have to write yourself and has to meet certain criteria in order to, well, qualify. - The kinds of qualifiers can vary from something like making an RPG inventory layout to making a password generator that meets certain conditions. - </p> - - <h2 class="title is-4" id="how-often-do-these-happen"><a href="#how-often-do-these-happen">How often do these happen?</a></h2> - <p> - Our Code Jams happen once a year every summer. - </p> - - <h2 class="title is-4" id="what-happens-if-i-have-to-drop-out"><a href="#what-happens-if-i-have-to-drop-out">What happens if I have to drop out?</a></h2> - <p> - This does happen from time to time. - Life gets in the way, we find out that relative passed away or your significant other RSVP'd you to a wedding you didn't know about until a week before. - Ideally we'd like to know about dropouts before the jam starts. - But if something does come up, please make sure to communicate that fact to any of our Event Coordinators, Moderators, or Admins. - The worst thing you could do is simply abandon your team, and doing so may hurt your chances for participating in future jams. - </p> - - <h2 class="title is-4" id="how-experienced-do-i-need-to-be-to-join-a-code-jam"> - <a href="#how-experienced-do-i-need-to-be-to-join-a-code-jam">How experienced do I need to be to join a Code Jam?</a> - </h2> - <p> - This is a question that we get asked a lot but is very difficult to answer. - Creating something fun with a team can be a great experience for almost everyone, - including relative beginners and more experienced developers. - That said, to make it a fun experience for everyone in the team, - we do require participants to complete a qualifier task so we can ensure that everyone - who enters the jam knows enough of the basics to activily participate in the development process. - </p> - <p> - Participating in a Code Jam often turns out to be a great learning experience. - Not only do you typically get to use packages and libraries you haven't touched before, - it will also increase your experience with developing an application in a team. - If you have never really done this before, - then you'll notice that collaborating with others adds a whole new dimension to the development process. - Even if you do have a lot of experience with working in teams, - having to collaborate with relative strangers of varying skill levels may provide a different experience than what you're used to. - </p> -{% endblock %} - -{% block sidebar %} - {% include "events/sidebar/code-jams/previous-code-jams.html" %} - {% include "events/sidebar/code-jams/useful-information.html" %} -{% endblock %} diff --git a/pydis_site/templates/events/pages/code-jams/code-style-guide.html b/pydis_site/templates/events/pages/code-jams/code-style-guide.html deleted file mode 100644 index 4ff5baaf..00000000 --- a/pydis_site/templates/events/pages/code-jams/code-style-guide.html +++ /dev/null @@ -1,268 +0,0 @@ -{% extends "events/base_sidebar.html" %} - -{% block breadcrumb %} - <li><a href="{% url "events:index" %}">Events</a></li> - <li><a href="{% url "events:page" path="code-jams" %}">Code Jams</a></li> - <li class="is-active"><a href="#">The Code Style Guide</a></li> -{% endblock %} - -{% block title %}The Code Style Guide{% endblock %} - -{% block event_content %} - <p> - For end-users, the most important parts of the software are functionality and UI/UX. - But for developers, there is one more important aspect - code style. - While ugly code can do everything that it has to do, developing it further may be a difficult task, - especially if the developer didn't write the original code. - Which one of the following do you prefer to read and work with? - </p> - <pre><code class="language-python">MyPath = '/file.txt' -from pathlib import * -import os.path,sys -def check(p): - """Uses os.path.exist """ - return os.path.exists(p) - -def getF( - p): - """Not sure what this do, this just worked. - """ - return Path(p - ) -result=[check(MyPath),getF(MyPath)]</code></pre> - <p>or</p> - <pre><code class="language-python">import os.path -from pathlib import Path - -FILE_PATH = '/file.txt' - - -def check_file_exists(path: str) -> bool: - """Checks does file exists in path. Uses os.path.exists.""" - return os.path.exists(path) - - -def get_path_object(path: str) -> Path: - """ - Returns Path object of the path provided in arguments. - - This is here for backward compatibility, will be removed in the future. - """ - return Path(path) - -result = [ - check_file_exists(FILE_PATH), - get_path_object(FILE_PATH), -]</code></pre> - - <p> - The second is definitely easier to read and understand. - These scripts are small and even with the first code snippet you can understand what the code does pretty quickly, - but what if the project has thousands and thousands of files in a really complex folder structure? - Do you want to work with code that looks like the first example? - You can save hours sometimes if you write beautiful code that follows the style guidelines. - </p> - <p> - The most important code style document for Python is <b><a href="https://www.python.org/dev/peps/pep-0008/">PEP 8</a></b>. - This Python Enhancement Proposal lays out the majority of all Python code style guidelines. - This article will cover the most important aspects of PEP 8. - </p> - - <h2>Linters</h2> - <p> - But everyone makes mistakes and there are so many style rules that can be really difficult to remember and always follow. - Luckily, we have amazing tools that help us - linters. While there are many linters, - we'd like code jam participants to use <b><a href="https://flake8.pycqa.org/en/latest/">flake8</a></b>. - Flake8 points out to you rules what you did break in your code so you can fix them. - </p> - - <h2>Guidelines</h2> - <h3>Basics</h3> - <p>For indentation, you should use 4 spaces. Using tabs is not suggested, but if you do, you can't mix spaces and tabs.</p> - <p> - PEP 8 defines a maximum line length of 79 characters, however, - we are not so strict - teams are welcome to choose a maximum line length between 79 and 119 characters. - </p> - <p>2 blank lines should be left before functions and classes. Single blank lines are used to split sections and make logical breaks.</p> - - <h3>Naming</h3> - <p>Module, file, function, and variable names (except type variables) should be lowercase and use underscores.</p> - <pre><code class="language-python"># File: my_module.py/mymodule.py - -def my_function(): - my_variable = "value"</code></pre> - <p>Class and type variable names should use the PascalCase style.</p> - <pre><code class="language-python">from typing import List - - -class MyClass: - pass - -ListOfMyClass = List[MyClass]</code></pre> - <p>Constant names should use the SCREAMING_SNAKE_CASE style.</p> - <pre><code class="language-python">MY_CONSTANT = 1</code></pre> - <p> - You should avoid single-character names, as these might be confusing. - But if you still do, you should avoid characters that may look like zero or one in some fonts: - "O" (uppercase o), "l" (lowercase L), and "I" (uppercase i). - </p> - - <h3>Operators</h3> - <p> - If you have a chain of mathematic operations that you split into multiple lines, - you should put the operator at the beginning of the line and not the end of the line. - </p> - <pre><code class="language-python"># No -result = ( - 1 + - 2 * - 3 -) - -# Yes -result = ( - 1 - + 2 - * 3 -)</code></pre> - <p>If you ever check if something is equivalent to <code>None</code>, you should use <code>is</code> and <code>is not</code> instead of the <code>==</code> operator.</p> - <pre><code class="language-python"># No -if variable == None: - print("Variable is None") - -# Yes -if variable is None: - print("Variable is None")</code></pre> - <p> - You should prefer using <code><item one> is not <item two></code> over <code>not <item one> is <item two></code>. - Using the latter makes it harder to understand what the expression is trying to do. - </p> - <pre><code class="language-python"># No -if not variable is None: - print("Variable is not None") - -# Yes - it is much easier to read and understand this than previous -if variable is not None: - print("Variable is not None")</code></pre> - - <h3>Imports</h3> - <p>Imports should be at top of the file, the only things that should be before them are module comments and docstrings.</p> - <p>You shouldn't import multiple modules in one line, but give each module import its own line instead.</p> - <pre><code class="language-python"># No -import pathlib, os - -# Yes -import os -import pathlib</code></pre> - <p>Wildcard imports should be avoided in most cases. It clutters the namespace and makes it less clear where functions or classes are coming from.</p> - <pre><code class="language-python"># No -from pathlib import * - -# Yes -from pathlib import Path</code></pre> - <p>You should use <b><a href="https://pycqa.github.io/isort/">isort</a></b> imports order specification, which means:</p> - <ul> - <li> - <b>Group by type:</b> order of import types should be: <code>__future__</code> imports, standard library imports, - third-party library imports, and finally project imports. - </li> - <li> - <b>Group by import method:</b> inside each group, first should come imports in format <code>import <package></code> - and after them <code>from <package> import <items></code>. - </li> - <li> - <b>Order imports alphabetically:</b> inside each import method group, imports should be ordered by package names. - </li> - <li> - <b>Order individual import items by type and alphabetically:</b> in <code>from <package> import <items></code> format, - <code><items></code> should be ordered alphabetically, starting with bare module imports. - </li> - </ul> - - <h3>Comments</h3> - <p> - Comments are really important because they help everyone understand what code does. - In general, comments should explain <i>why</i> you are doing something if it's not obvious. - You should aim to write code that makes it obvious what it is doing and you can use the comments to explain why and provide some context. - </p> - <p> - Keep in mind that just as important as having comments, is making sure they stay up to date. - Out-of-date and incorrect comments confuse readers of your code (including future you). - </p> - <p>Comments content should start with a capital letter and be a full sentence(s).</p> - <p>There are three types of comments: block comments, inline comments, and docstrings.</p> - <ul> - <li> - <h4>Block comments</h4> - <p> - Probably most common comment type. Should be indented to the same level as the code they describe. - Each line in the block comment has to start with <code>#</code> and should be followed by a single space. - To separate paragraphs, use one line containing only <code>#</code>. - </p> - <pre><code class="language-python">if variable is None or variable == 1: - # If variable is None, something went wrong previously. - # - # Here starts a new important paragraph.</code></pre> - </li> - <li> - <h4>Inline comments</h4> - <p> - You should prefer block comments over inline comments and use inline comments only where it is really necessary. - Never use inline comments to explain obvious things like what a line does. - </p> - <p>If you want to use an inline comment on a variable, think first, maybe you can use a better variable name instead.</p> - <p> - After code and before the start of inline comments should be at least two spaces. - Just like block comments, inline comments also have to start with <code>#</code> followed by a single space. - </p> - <pre><code class="language-python"># Do not use inline comments to explain things -# that the reader can understand even without the inline comment. -my_variable = "Value!" # Assign value to my_variable - -# Here better variable name can be used like shown in the second line. -x = "Walmart" # Shop name -shop_name = "Walmart" - -# Sometimes, if something is not obvious, then inline comments are useful. -# Example is from PEP 8. -x = x + 1 # Compensate for border</code></pre> - </li> - <li> - <h4>Docstrings</h4> - <p> - Last, but not least important comment type is docstring, which is a short version of documentation string. - Docstring rules haven't been defined by PEP 8, but by <a href="https://www.python.org/dev/peps/pep-0257">PEP 257</a> instead. - </p> - <p>Docstrings should start and end with three quotes (""").</p> - <p>There are two types of docstrings: one-line docstrings and multiline docstrings.</p> - <p> - One-line docstrings have to start and end in the same line, while multiline docstrings start and end in different lines. - Multiline docstring has two parts: summary line and a longer description, which are separated by one empty line. - The multiline docstring start and end quotes should be on different lines than the content. - </p> - <pre><code class="language-python"># This is a one-line docstring. -"""This is one line module docstring.""" - - -# This is a multiline docstring. -def my_function(): - """ - This is the summary line. - - This is the description. - """</code></pre> - </li> - </ul> - - <h2>Too much for you?</h2> - <p> - Do all these style rules make your head explode? We have something for you! We have a song! - We have <a href="https://www.youtube.com/watch?v=hgI0p1zf31k">The PEP 8 Song (featuring lemonsaurus)</a>! - Great way to get started with writing beautiful code. - </p> - <iframe width="500" height="315" src="https://www.youtube.com/embed/hgI0p1zf31k"></iframe> -{% endblock %} - -{% block sidebar %} - {% include "events/sidebar/code-jams/useful-information.html" %} -{% endblock %} diff --git a/pydis_site/templates/events/pages/code-jams/judging.html b/pydis_site/templates/events/pages/code-jams/judging.html deleted file mode 100644 index 6c6531d9..00000000 --- a/pydis_site/templates/events/pages/code-jams/judging.html +++ /dev/null @@ -1,110 +0,0 @@ -{% extends "events/base_sidebar.html" %} - -{% block breadcrumb %} - <li><a href="{% url "events:index" %}">Events</a></li> - <li><a href="{% url "events:page" path="code-jams" %}">Code Jams</a></li> - <li class="is-active"><a href="#">How does judging work?</a></li> -{% endblock %} - -{% block title %}How does judging work?{% endblock %} - -{% block event_content %} - <p> - Here at Python Discord, judging the code jam is a process that continues throughout the entire event. - This document seeks to detail exactly what the judges will consider when attempting to determine the winner of the code jam. - </p> - <p> - Because programming is full of subtleties and judging is not an exact science, this document will inevitably fall short of being an exact guide for how to win, but it should at least give you an idea of what you should be mindful of as a participant. - </p> - - <h2 class="title is-4" id="style"><a href="#style">Style</a></h2> - <p> - Python is a language in which code style matters more than what someone might be used to coming from other languages. - We believe that good code is code that is easy to read, pleasant to work with, - and self-documenting. Here are some of the things we'll consider when judging your submission: - </p> - <ul> - <li> - Do variables have good names that make it obvious what they are going to be used for? - </li> - <li> - Are files organized in a way that makes the project easy to navigate? - </li> - <li> - Does the code follow the conventions of <a href="https://www.python.org/dev/peps/pep-0008/">the official Python style guide</a>? - </li> - <li> - Does your project have good git commit messages that explain not only what you were doing, but why? - </li> - <li> - Are there enough comments in the code? - A good rule of thumb for this is to make sure it's easy to <em>skim through the code</em>. - Another programmer should not have to read every line of code in a section in order to determine what it does. - </li> - <li> - Does your code use blank lines and block comments in order to split up large chunks of code into smaller, logically separated sections? - </li> - <li> - Are your comments and docstrings free of poor formulations and typos? - </li> - </ul> - <p> - If you can answer yes to all of the above, you will probably score well on style. - Using a linter (such as <code>flake8</code>) can be an easy way to greatly improve your style. - </p> - <p> - If your submission contains other languages than Python, - <em>please keep in mind that we will only review the Python code</em>. - This means that it is a good idea for you to solve as much of your submission as possible using Python. - For example, if you're making a webapp with a Python backend, make sure you solve all the interesting stuff in the backend, not in the frontend. - </p> - - <h2 class="title is-4" id="originality"><a href="#originality">Originality</a></h2> - <p> - In order to level the playing field a bit, we've decided that this is the most important factor of all in our code jams. - If your idea is an exceptionally good one and you are a beginner, - we will cut you some slack on style and execution. - You may be able to walk away victorious even though the other teams have far more experienced Python professionals. - It is therefore crucial that you consider <em>what to do more</em> carefully than exactly <em>how to do it</em>. - </p> - <p> - That said, experienced Python developers will always have an advantage, - so don't expect to win with a lazy or ugly implementation just because your idea is brilliant. - </p> - - <h2 class="title is-4" id="execution"><a href="#execution">Execution</a></h2> - <p> - An important factor is whether your solution is the best solution to the problem you are solving. - If your solution is overly convoluted, this will count against you in our evaluation. - </p> - <p> - While the code jam is ongoing, staff will be available to you both inside your team channel and in the common code jam channels. - Staff may drop by your team channel to make helpful suggestions, answer questions, or just ask you how it's going. - Feel free to grab a staff member and ask a question at any point if you're unsure about something. - </p> - <p> - Our talented team of Helpers will also be available during the code jam, - so if there is a part of your code you're unsure about, feel free to bring it to a help channel. - We will be happy to provide advice on how to improve it. - Do not, however, post your entire program; help requests should be bite-sized. - </p> - - <h2 class="title is-4" id="teamwork"><a href="#teamwork">Teamwork</a></h2> - <p> - During the code jam, you will be assigned teammates. It is crucial that you find a way to work with your teammates, - as we will be evaluating your ability to work as a team. - Team members who constantly bicker, complain about each other, - or fail to divide up tasks so that everyone can contribute will be penalized when we evaluate the code jam. - </p> - <p> - This does not, however, mean that a team that has a "bad egg" will automatically be unable to win the code-jam. - If a team member refuses to play well with the rest of the team or does not participate in the jam, - we will usually remove that person from the team and attempt to find a new teammate for the team to replace them. - This will also lead to an infraction against the offending team member, - which may make it impossible for them to join some (or all) future code jams. - </p> -{% endblock %} - -{% block sidebar %} - {% include "events/sidebar/code-jams/useful-information.html" %} -{% endblock %} diff --git a/pydis_site/templates/events/pages/code-jams/pull-request.html b/pydis_site/templates/events/pages/code-jams/pull-request.html deleted file mode 100644 index c276fd12..00000000 --- a/pydis_site/templates/events/pages/code-jams/pull-request.html +++ /dev/null @@ -1,235 +0,0 @@ -{% extends "events/base_sidebar.html" %} - -{% block breadcrumb %} - <li><a href="{% url "events:index" %}">Events</a></li> - <li><a href="{% url "events:page" path="code-jams" %}">Code Jams</a></li> - <li class="is-active"><a href="#">Opening a Pull Request</a></li> -{% endblock %} - -{% block title %}Opening a Pull Request{% endblock %} - -{% block event_content %} - <p> - When you start out with a jam we ask your team leader to fork the code jam brief repository to their account for your to work on your code. - </p> - <p> - In this guide we're going to go over: - </p> - <ul> - <li> - <a href="#forking-the-repository">Forking the repository</a> - </li> - <li> - <a href="#opening-a-pull-request">Opening a pull request back to the code jam brief on python-discord</a> - </li> - <li> - <a href="#adding-collaborators">Adding collaborators</a> - </li> - <li> - <a href="#adding-a-github-webhook">Adding a GitHub webhook to your teams Discord channel</a> - </li> - </ul> - - <h2 class="title is-4" id="forking-the-repository"><a href="#forking-the-repository">Forking the repository</a></h2> - <p> - When you browse to the code jam brief repository you will be presented with the project root. - </p> - <p> - You want to fork the repository to your account using the button shown in the image below: - </p> - <p> - <img alt="fork button" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/fork-button.png"> - </p> - <p> - If you see a dialog like this one, just select your user. - </p> - <p> - <img alt="where to fork" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/where-to-fork.png"> - </p> - <p> - Once you've done this you'll be redirected to a page like this: - </p> - <p> - <img alt="example fork" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/my-fork.png"> - </p> - <p> - And that's it! You've forked the repo! - </p> - - <h2 class="title is-4" id="opening-a-pull-request"><a href="#opening-a-pull-request">Opening a Pull Request</a></h2> - <p> - Before we can open a Pull Request you need to have something to actually compare against the brief repo. - </p> - <p> - In the actual jam you should fill in the Project information section of the README.md, but for now I'm just going to add a line to the README. - </p> - <p> - <img alt="image" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/commit.png"> - </p> - <p> - Once you've made your commit, navigate back to the project root where you will see this button above your commit bar: - </p> - <p> - <img alt="image" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/pull-request-button.png"> - </p> - <p> - You should click on this pull request button. When you do you will see something like this, just click the Create pull request button: - </p> - <p> - <img alt="image" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/compare-changes.png"> - </p> - <p> - Next up you'll see a form like this: - </p> - <p> - <img alt="image" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/pull-request-form.png"> - </p> - <p> - You should fill it in like so: - </p> - <li> - <strong>Title</strong> should be your team's name - </li> - <li> - <strong>Description</strong> does not have to be filled in - </li> - <li> - Make sure to <strong>select the "allow edits from maintainers" option</strong> - </li> - <li> - Make sure to open a pull request and not a draft pull request - </li> - <p> - That's it! You've opened your pull request and should see something like this: - </p> - <p> - <img alt="image" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/pull-request.png"> - </p> - - <h2 class="title is-4" id="adding-collaborators"><a href="#adding-collaborators">Adding collaborators</a></h2> - <p> - Obviously your fork is no good if your team members cannot make edits to it. - To allow your team mates to edit you need to add them to the project as collaborators. - </p> - <p> - We're going to start off by heading to our fork of the repository (this is the one with your username in it, not python-discord) and head to the settings page. - </p> - <p> - <img alt="image" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/settings-page.png"> - </p> - <p> - Click on collaborators. GitHub will ask you to enter your password now for security. Enter it and proceed. - </p> - <p> - You should see a page like this: - </p> - <p> - <img alt="image" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/collabs-page.png"> - </p> - <p> - Add your team members using this form and use the copy invite button to copy an invite link they can then use. - You should send this to them in your team channel on Discord. - </p> - <p> - <img alt="image" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/invites.png"> - </p> - <p> - <img alt="image" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/invited-discord.png"> - </p> - <p> - Once your team mates have joined you'll see this: - </p> - <p> - <img alt="image" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/accepted.png"> - </p> - <p> - That's all! Your teammates have access to the repository now! - </p> - - <h2 class="title is-4" id="adding-a-github-webhook"><a href="#adding-a-github-webhook">Add a GitHub webhook to Discord</a></h2> - <p> - It may be useful to keep up on your GitHub repository updates right from your Discord channel. - For that reason, we've assigned all team leaders webhook creation permissions inside your team channel. - </p> - <h3 class="title is-5">Webhook creation</h3> - <p> - To make use of this we're going to start out by making a webhook inside the channel. - </p> - <p> - To do this go to your team channel and click on the gear that appears when you mouse-over it. - </p> - <p> - <img alt="image" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/channel-gear.png"> - </p> - <p> - Navigate to the Webhooks section of the settings page and click <code>Create Webhook</code>. - </p> - <p> - <img alt="image" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/webhook-creation-page.png"> - </p> - <p> - You'll see something like this: - </p> - <p> - <img alt="image" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/webhook-modal.png"> - </p> - <p> - You can edit the name to 'GitHub' or anything, but GitHub will do this anyway. - </p> - <p> - Copy the link to the clipboard, you will get something like this: - </p> - <p> - <code>https://canary.discordapp.com/api/webhooks/548221675087462410/h7lkQ7cX5-95J8CLMNCGXTEBrWOmXYFWv7N4MqsFtV-D8F0W8Rf3rWj4dPAEFGspm7EZ</code> - </p> - <p> - By default this is not configured to respond to GitHub payloads so you will need to add a <code>/github</code> to the end of the URL. - </p> - <p> - Your new URL should look like: - </p> - <p> - <code>https://canary.discordapp.com/api/webhooks/548221675087462410/h7lkQ7cX5-95J8CLMNCGXTEBrWOmXYFWv7N4MqsFtV-D8F0W8Rf3rWj4dPAEFGspm7EZ/github</code> - </p> - <h3 class="title is-5">Adding to GitHub</h3> - <p> - Next up we will head over to our GitHub project settings (on our fork) and go to the webhooks section: - </p> - <p> - <img alt="image" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/webhooks-settings.png"> - </p> - <p> - Click on the Add Webhook button and paste in your URL to the <code>Payload URL</code> section. - </p> - <p> - Set the <code>Content Type</code> to <code>application/json</code> otherwise Discord cannot interpret your request. - </p> - <p> - You can select either just the push or everything depending on how much information you want. - </p> - <p> - Make sure <code>Active</code> is ticked and click <code>Add webhook</code>. - </p> - <p> - And that's it! You've added a Discord webhook to your GitHub repository! - </p> - <p> - <img alt="image" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/added.png"> - </p> - <p> - As you can see here, my teammate has commited a change to our fork: - </p> - <p> - <img alt="image" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/aperture-commit.png"> - </p> - <p> - In our Discord channel, you can see the following: - </p> - <p> - <img alt="image" src="https://raw.githubusercontent.com/wiki/python-discord/code-jam-5/images/webhook-channel.png"> - </p> -{% endblock %} - -{% block sidebar %} - {% include "events/sidebar/code-jams/useful-information.html" %} -{% endblock %} diff --git a/pydis_site/templates/events/pages/code-jams/using-git.html b/pydis_site/templates/events/pages/code-jams/using-git.html deleted file mode 100644 index 19c4a08f..00000000 --- a/pydis_site/templates/events/pages/code-jams/using-git.html +++ /dev/null @@ -1,61 +0,0 @@ -{% extends "events/base_sidebar.html" %} - -{% block breadcrumb %} - <li><a href="{% url "events:index" %}">Events</a></li> - <li><a href="{% url "events:page" path="code-jams" %}">Code Jams</a></li> - <li class="is-active"><a href="#">How to use git</a></li> -{% endblock %} - -{% block title %}How to use git{% endblock %} - -{% block event_content %} - <p> - We require all participants to use Git for source control. - This means that you will need to learn a bit of Git to participate in the code jam, - if you're not already familiar with it. Git is incredibly useful, - and an essential skill to master if you ever want to write code out there in the real world. - If you don't yet have Git installed... - </p> - - <ul> - <li> - <strong>Windows</strong>: Install git using <a href="https://gitforwindows.org/">Git for Windows</a> - you can leave all of the options at their defaults, if you wish - </li> - <li> - <strong>Mac</strong>: Install git using <a href="https://brew.sh/">Homebrew</a> - </li> - <li> - <strong>Linux</strong>: Install git using your Linux distro's package manager - </li> - </ul> - - <p> - In order to make the learning process easier, we've included some Git learning resources below... - </p> - <ul> - <li> - Roger Dudler has <a href="https://rogerdudler.github.io/git-guide/">an excellent crash</a> course that will show you the most important commands. This can be used as a cheat sheet once you're starting to actually use Git. - </li> - <li> - If you'd like a wordier introduction, Prithaj Nath (who participated in our first code jam) wrote a more in-depth guide called <a href="https://medium.com/@prithajnath/getting-started-with-git-7aae82dd3599">Let's Git Started!</a> for <a href="https://medium.com">medium.com</a> that might be worth a read. - </li> - <li> - The ever-excellent Corey Schafer has a 30-minute YouTube tutorial called <a href="https://www.youtube.com/watch?v=HVsySz-h9r4">Git Tutorial for Beginners</a> which should teach you everything you need to know to participate in our jams. - </li> - </ul> - - <p> - Optionally, certain IDEs and editors will be able to handle Git for you, - and tools like GitKraken are also permitted. - Make sure you familiarize yourself with the client of your choice before the code jam starts - so you don't need to spend time learning this while the clock is running. - </p> - <p> - For more information on GUI clients you can use, - <a href="https://about.gitlab.com/applications/#gui-git-clients">please see this page</a>. - </p> -{% endblock %} - -{% block sidebar %} - {% include "events/sidebar/code-jams/useful-information.html" %} -{% endblock %} diff --git a/pydis_site/templates/events/pages/game-jams/2020/_index.html b/pydis_site/templates/events/pages/game-jams/2020/_index.html deleted file mode 100644 index 136c9f4d..00000000 --- a/pydis_site/templates/events/pages/game-jams/2020/_index.html +++ /dev/null @@ -1,104 +0,0 @@ -{% extends "events/base_sidebar.html" %} - -{% block title %}Game Jam 2020: Three of a Kind{% endblock %} - -{% block breadcrumb %} - <li><a href="{% url "events:index" %}">Events</a></li> - <li><a href="{% url "events:page" path="game-jams" %}">Game Jams</a></li> - <li class="is-active"><a href="#">Game Jam 2020: Three of a Kind</a></li> -{% endblock %} - -{% block event_content %} - <p> - During the first ever Python Discord Game Jam, - 23 teams signed up to create a game using the <a href="https://arcade.academy/">Python Arcade Library</a>. - Each team consisted of 1-3 people and teammates were chosen by the participants themselves, - unlike in our <a href="{% url "events:page" path="code-jams" %}">Code Jams</a>. - The Game Jam ran from <strong>April 17, 12:00 UTC to April 26, 18:00 UTC</strong>. - </p> - - <h2 class="title is-4" id="task-description"><a href="#task-description">Task Description</a></h2> - <blockquote> - <p>The theme for this game jam is <strong>Three-of-a-kind</strong>.</p> - <p> - What you do with this theme or how you interpret it is up to you, - but it will be your task to come up with something fun using this theme. - </p> - </blockquote> - - <h2 class="title is-4" id="judging-stream"><a href="#judging-stream">Judging Stream and Interview</a></h2> - <p> - The original Game Jam live stream is shown below, - where the top ten submissions were reviewed and showcased. - There was also an interview with <strong>Paul Craven</strong>, - the creator of the <a href="https://arcade.academy/">Python Arcade Library</a>, - where he answered many questions about his origin/backstory, - the development of the Arcade library, and general programming. - </p> - <iframe width="560" height="315" src="https://www.youtube.com/embed/KkLXMvKfEgs" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> - - <blockquote> - <p> - You can also see the top ten submissions shown - <a href="https://arcade.academy/game_jam_2020.html">here</a> - on the Arcade website and all submissions were submitted - <a href="https://github.com/python-discord/game-jam-2020">here</a> to the repository. - </p> - </blockquote> - - <h2 class="title is-4" id="results"><a href="#results">Results</a></h2> - <p> - The <strong>winning team</strong> for our first ever Game Jam was team <strong>Score AAA</strong>, - consisting of one person, <strong>EmberDuck</strong>. He created a very polished game, - focusing in on one core idea and just nailing it perfectly. - This is a game where the player controls <strong>3</strong> different characters which need to jump over obstacles. - The concept itself may seem simple, but EmberDuck kept iterating on the idea, making it better and better, - taking it to the point of perfection. The graphics were phenomenal, the UI was beautiful and intuitive, and the animations were just fantastic. - The parallax effect in the background was mesmerizing, which was an element that was unique and different from other submissions. - Taking this project to another level, all art assets for this submission were made by EmberDuck himself. - This was an incredible project, especially for a one-man-team. <strong>Congratulations EmberDuck!</strong> - </p> - - <p> - Check out their submission - <a href="https://github.com/python-discord/game-jam-2020/tree/master/Finalists/Score_AAA/">here</a> - and watch it being played live <a href="https://youtu.be/KkLXMvKfEgs?t=7600">here</a>! - </p> - - <h2 class="title is-4" id="runner-up-1"><a href="#runner-up-1">Runner Up 1: Gamer Gang</a></h2> - <p> - The <strong>Gamer Gang</strong> (SansPapyrus683, Blue Bird, & SimplyNarwell) created a platformer game where the player controls - <strong>3</strong> different slimes to get through 6 different levels. - The slimes can either move together in a stack, or the player can split them apart to control each of them one by one. - The physics in this game was a nice addition and tied the game together nicely. - The graphics in this game were quite beautiful and the UI complemented it well. - This game scored quite high on many factors, and was a very fun game overall. Excellent job! - </p> - <p> - Feel free to look at their submission - <a href="https://github.com/python-discord/game-jam-2020/tree/master/Finalists/gamer_gang/">here</a> - and watch it being played on stream <a href="https://youtu.be/KkLXMvKfEgs?t=3812">here</a>! - </p> - - <h2 class="title is-4" id="runner-up-2"><a href="#runner-up-2">Runner Up 2: Monkeys and Frogs on Fire</a></h2> - <p> - Team <strong>Monkeys and Frogs on Fire</strong> (BrainDead, F4zi, & f1re) - created a dungeon-crawler type of game where the player can switch between - <strong>3</strong> different wizards, each having their own special abilities. - The red wizard has great strength, while the green wizard has a lot of defense and the blue wizard is quick and fast. - A unique and exceptional feature of this game was the backend server which allowed a player to register/login. - The goal of this game was to survive as long as you can to get the highest score, - which is then uploaded to the server and can be seen on the leaderboard. - This game had a crisp and smooth UI along with nice graphics and fantastic animations, - bringing this game together. Phenomenol work! - </p> - <p> - Have a look at this submission <a href="https://github.com/Den4200/game-jam-2020">here</a> - and watch this game played live on stream <a href="https://youtu.be/KkLXMvKfEgs?t=5188">here</a>! - </p> - <img src="https://user-images.githubusercontent.com/33516116/77762078-e02ce080-7030-11ea-947a-9c6733d4f33d.png" alt=""> -{% endblock %} - -{% block sidebar %} - {% include "events/sidebar/game-jams/2020.html" %} -{% endblock %} diff --git a/pydis_site/templates/events/pages/game-jams/2020/judging.html b/pydis_site/templates/events/pages/game-jams/2020/judging.html deleted file mode 100644 index 1ec836ac..00000000 --- a/pydis_site/templates/events/pages/game-jams/2020/judging.html +++ /dev/null @@ -1,68 +0,0 @@ -{% extends "events/base_sidebar.html" %} - -{% block title %}Judging{% endblock %} - -{% block breadcrumb %} - <li><a href="{% url "events:index" %}">Events</a></li> - <li><a href="{% url "events:page" path="game-jams" %}">Game Jams</a></li> - <li><a href="{% url "events:page" path="game-jams/2020" %}">Game Jam 2020: Three of a Kind</a></li> - <li class="is-active"><a href="#">Judging</a></li> -{% endblock %} - -{% block event_content %} - <img src="https://user-images.githubusercontent.com/33516116/77762078-e02ce080-7030-11ea-947a-9c6733d4f33d.png"> - <p> - The game jam has a greatly simplified judging process compared to the <a href="/events/code-jams/">Code Jam events</a>. We will not be evaluating your code quality, your commit history, or your teamwork. We will instead evaluate your project based on a few simple factors, which are outlined in this document. - </p> - - - <!-- Novelty --> - <h2>Creativity & Novelty Factor</h2> - <p> - The theme and the tech that we provide are provided as limitations, because we believe that <em>limitations breed creativity</em>. One of the big factors we consider when we judge these projects is the novelty factor. If you've created something truly unique, you've got a leg up on someone who made a very polished version of something that's very familiar. This means that even if you're not an expert programmer or experienced game dev, the right idea (and that ideas' adherence to the theme) might still net you a win! - </p> - <p> - Please try to spend some time coming up with the most fun and novel idea you can before diving into your actual programming. If you're not sure how, try googling for <strong>game dev ideation techniques</strong>. Professional game developers frequently make use of these techniques in order to help spin the wheels of creativity. - </p> - <p> - For example, you can try to write down a bunch of different adjectives (like "yellow" or "terrified") on red notes, nouns (like "lemon" or "snowman") on blue notes, and modifiers (like "in space!" or "the floor is lava!") on yellow notes. Then select a random note from each color, put them together in different orders and think about how you can create a game idea around it that fits the theme. - </p> - - <!-- Execution--> - <h2>Execution</h2> - <p> - We will of course be evaluating the execution. How well does the game perform? How is the user experience? Is it fun? Is it easy to play? Does it look good, sound good, and feel good? The performance and interaction are probably the most important factors, but polish is nice, too! - </p> - <p> - We're going to try to do this in an objective way, but stuff like fun is always going to be a somewhat subjective thing to evaluate, and for that reason, this is not the single most critical criteria in the judging process, like it might have been in other game jams. To be more precise - A really fun and novel idea that ticks all the other boxes but has somewhat mediocre execution will still have a very good chance of doing well in this event. - </p> - - <!-- Adherence--> - <h2>Theme Adherence</h2> - <p>Your submission must adhere to the theme we will present on the day the jam starts. There are different degrees of adherence here, and quite a lot of room to be creative, but a deeply thematic submission will be scored higher than a barely thematic submission, and a submission that does not adhere to the theme at all <strong>may be disqualified</strong>. - </p> - <p> - We love seeing multiple <em>layers</em> of thematic adherence - For example, your theme could play a part in your team name, your art style, your story, your sound effects, and of course, in the gameplay. You might even try to inject your theme into your documentation! - </p> - - <!-- Documentation via README.md--> - <h2>Documentation</h2> - <p>We require that you make the project easy to comprehend, easy to install, and easy to run. To ensure this, your submission must contain a <code>README.md</code> in your team folder which clearly documents how to interact with it. - </p> - <p> - First of all, you need to include instructions on exactly how to get your game running. We require that you use a <strong>dependency manager</strong> and encourage you to make a <strong>click-and-play option</strong>, and these should be documented in this section. Please see <a href="/events/game-jam-2020/technical-requirements/">our technical requirements page</a> for more information on this. Remember, we have to judge dozens of submissions, so if every submission can be set up and run by just doing <code>pipenv install</code> and then <code>pipenv run start</code>, it will make our job significantly easier. - </p> - <p> - The readme should also contain information about the game, screenshots, a logo if you got it, your team name, instructions on how to play and anything else that helps provide a high-level overview over the game. This is also extremely helpful when we are judging so many projects, so that we can easily see which is which by looking through readmes. For an example of an excellent readme, check out <a href="https://github.com/python-discord/code-jam-6/blob/master/amphibian-alchemists/README.md">this readme created by the Amphibian Alchemists team during Code Jam 6</a>. - </p> - - <!-- Team size--> - <h2>Team size</h2> - <p> - For this game jam, we are allowing you to sign up either by yourself, or with one or two friends. But, of course, it wouldn't be fair if we judged all entries the same regardless of team size - so we will be expecting slightly more from a team of 3 than from a solo team. To be more precise, we will not be expecting <em>three times as much</em> - working in a team has its own set of unique setbacks as well as advantages, and two developers does not equal 200% the output of one - but we will be expecting a full team to be able to deliver more content in ten days than a single person can. - </p> -{% endblock %} - -{% block sidebar %} - {% include "events/sidebar/game-jams/2020.html" %} -{% endblock %} diff --git a/pydis_site/templates/events/pages/game-jams/2020/project-setup.html b/pydis_site/templates/events/pages/game-jams/2020/project-setup.html deleted file mode 100644 index 962cd556..00000000 --- a/pydis_site/templates/events/pages/game-jams/2020/project-setup.html +++ /dev/null @@ -1,57 +0,0 @@ -{% extends "events/base_sidebar.html" %} - -{% block title %}Project Setup{% endblock %} - -{% block breadcrumb %} - <li><a href="{% url "events:index" %}">Events</a></li> - <li><a href="{% url "events:page" path="game-jams" %}">Game Jams</a></li> - <li><a href="{% url "events:page" path="game-jams/2020" %}">Game Jam 2020: Three of a Kind</a></li> - <li class="is-active"><a href="#">Project Setup</a></li> -{% endblock %} - -{% block event_content %} - <img src="https://user-images.githubusercontent.com/33516116/77762078-e02ce080-7030-11ea-947a-9c6733d4f33d.png"> - <br></br> - <p> - The first step is to fork our repository: <a href="https://github.com/python-discord/game-jam-2020/">python-discord/game-jam-2020</a>. You can learn how to do this in our - <a href="https://pythondiscord.com/pages/guides/pydis-guides/contributing/forking-repository/">contributing guidelines</a>. - </p> - - <p> - Alternatively, GitHub has a <a href="https://help.github.com/en/github/getting-started-with-github/fork-a-repo">detailed suite of tutorials explaining forking</a> as well as other technical terms that you may come across. If you get - stuck at any point, you are always welcome to ask for help at Python Discord. - </p> - - <p>Once the fork is created, each team will make a directory for their project. The directory should be named after the team. For example, team <strong>Precise Rabbits</strong> will set up a directory as follows:</p> - - <pre><code>game-jam-2020/ - └── precise-rabbits/ - └── game/ - ├── __init__.py - ├── __main__.py - └── rabbits.py - ├── config.json - ├── Pipfile - ├── Pipfile.lock - └── README.md</code></pre> - - <p><strong>Do not make any changes to other teams' project directories.</strong></p> - - <p> - Each team will then develop their game within their fork. You are encouraged to use git to its full potential - feature branches will help you keep your commit history clean, and prevent annoying conflicts. Feel free to refer to - <a href="https://pythondiscord.com/pages/guides/pydis-guides/contributing/working-with-git/">our guidelines on the topic</a>, or any other resources that you may find useful. Version control is an important aspect of software development, and code jams are - a great opportunity to improve skills in the area. - </p> - - <p> - Once the project is finished, a pull request will be opened against the upstream repository. All of your projects will be merged to the upstream repository, which means that you will be credited as a contributor to an open source - project. The final version <strong>must be committed by the jam's deadline</strong>. Any changes beyond this point will not be considered in the judging process. - </p> - - <p>Please ensure that your final version is well tested, and satisfies our <a href="https://pythondiscord.com/events/game-jams/2020/technical-requirements/">technical requirements</a>.</p> - -{% endblock %} - -{% block sidebar %} - {% include "events/sidebar/game-jams/2020.html" %} -{% endblock %} diff --git a/pydis_site/templates/events/pages/game-jams/2020/rules.html b/pydis_site/templates/events/pages/game-jams/2020/rules.html deleted file mode 100644 index 33eccf63..00000000 --- a/pydis_site/templates/events/pages/game-jams/2020/rules.html +++ /dev/null @@ -1,31 +0,0 @@ -{% extends "events/base_sidebar.html" %} - -{% block title %}Game Jam Rules{% endblock %} - -{% block breadcrumb %} - <li><a href="{% url "events:index" %}">Events</a></li> - <li><a href="{% url "events:page" path="game-jams" %}">Game Jams</a></li> - <li><a href="{% url "events:page" path="game-jams/2020" %}">Game Jam 2020: Three of a Kind</a></li> - <li class="is-active"><a href="#">Game Jam Rules</a></li> -{% endblock %} - -{% block event_content %} - <img src="https://user-images.githubusercontent.com/33516116/77762078-e02ce080-7030-11ea-947a-9c6733d4f33d.png"> - <ol> - <li>You may enter individually or with up to two friends.</li> - <li>All members of your team must be members of our Discord community.</li> - <li>Your project must use the <a href="https://arcade.academy/">Python Arcade Library</a>. It is not permitted to work around this by, e.g., using the Python Arcade Libary as a wrapper for another framework.</li> - <li>The majority of your project must be written in Python.</li> - <li>Your project must be feasible to run and simple to set up on a <strong>desktop computer</strong>.</li> - <li>You are allowed to use existing assets, like images and sound effects, as long as the licenses of those assets permit it. Typically, this means that the assets are licensed under an OSI-approved or Creative Commons license, or is in the public domain.</li> - <li>All projects should start from scratch and all code must be written within the time constrictions of the jam.</li> - <li>You retain all copyrights to entries you submit. By submitting your entry, you grant a transferrable, irrevocable license to redistribute, copy and run your entry without modification, and to distribute screenshots and other forms of gameplay captures of the entry, provided no fee is charged.</li> - </ol> - <blockquote> - Please note that our regular <a href="/pages/rules/">community rules</a> and <a href="/pages/code-of-conduct/">code of conduct</a> also apply during the event and that we reserve the right to make changes to these rules at any time. - </blockquote> -{% endblock %} - -{% block sidebar %} - {% include "events/sidebar/game-jams/2020.html" %} -{% endblock %} diff --git a/pydis_site/templates/events/pages/game-jams/2020/technical-requirements.html b/pydis_site/templates/events/pages/game-jams/2020/technical-requirements.html deleted file mode 100644 index 4a44e7db..00000000 --- a/pydis_site/templates/events/pages/game-jams/2020/technical-requirements.html +++ /dev/null @@ -1,41 +0,0 @@ -{% extends "events/base_sidebar.html" %} - -{% block title %}Technical Requirements{% endblock %} - -{% block breadcrumb %} - <li><a href="{% url "events:index" %}">Events</a></li> - <li><a href="{% url "events:page" path="game-jams" %}">Game Jams</a></li> - <li><a href="{% url "events:page" path="game-jams/2020" %}">Game Jam 2020: Three of a Kind</a></li> - <li class="is-active"><a href="#">Technical Requirements</a></li> -{% endblock %} - -{% block event_content %} - <img src="https://user-images.githubusercontent.com/33516116/77762078-e02ce080-7030-11ea-947a-9c6733d4f33d.png"> - - <br></br> - - <p>The technological theme of this game jam is <a href="https://arcade.academy/">Arcade</a>. As per <a href="https://pythondiscord.com/events/game-jams/2020/rules/">rule 3</a>, please ensure that you use Arcade as your project's main framework. Your project can use other dependencies, as long as they do not violate rule 3. If you're unsure about a potential dependency, consult us in the discussion channel.</p> - - <h2>Dependency management</h2> - - <p>Participating teams are asked to provide a <em>click-and-play</em> option to launch their project. There should be no complicated process to install dependencies, or otherwise prepare the environment, before the project can be started. This will allow us to spend more time evaluating the actual game. Ideally, projects will make use of <a href="https://github.com/pypa/pipenv">Pipenv</a> to both install dependencies and launch the game, but it is also acceptable to solve this with other dependency managers, or even with a <code>requirements.txt</code> file and a great readme. <strong>Ideally, it should only take a judge 2-3 commands to get your game ready to run, and these should be clearly documented in your readme.</strong></p> - - <p>Working with Pipenv is not difficult, and existing Python Discord projects can be used for reference. For example, <a href="https://github.com/python-discord/seasonalbot/">Seasonalbot's</a> <code>Pipfile</code> and <code>Pipfile.lock</code> list all necessary dependencies and provide a run script. As a result, a virtual environment with all necessary dependencies can be conveniently created with <code>pipenv install</code>, and the project can be launched with <code>pipenv run start</code>.</p> - - <p>In simple terms, emulating this workflow would be the ideal way to handle dependency management for this game jam.</p> - - <p>If you're unfamiliar with Pipenv, we recommend Corey Schafer's excellent video as a learning resource.</p> - - <iframe width="560" height="315" src="https://www.youtube.com/embed/zDYL22QNiWk" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> - - </br> - </br> - - <h2>Target platform</h2> - - <p>All submissions will be evaluated on Windows 10. This does not necessarily mean that all development must take place on Windows, but all submissions should be functional and well tested on the target platform. This is also something to keep in mind in the initial stages of development - make sure you don't accidentally build your project around a dependency that isn't available on Windows.</p> -{% endblock %} - -{% block sidebar %} - {% include "events/sidebar/game-jams/2020.html" %} -{% endblock %} diff --git a/pydis_site/templates/events/pages/game-jams/_index.html b/pydis_site/templates/events/pages/game-jams/_index.html deleted file mode 100644 index 85048cf1..00000000 --- a/pydis_site/templates/events/pages/game-jams/_index.html +++ /dev/null @@ -1,12 +0,0 @@ -{% extends "events/base.html" %} - -{% block title %}Game Jams{% endblock %} - -{% block breadcrumb %} - <li><a href="{% url "events:index" %}">Events</a></li> - <li class="is-active"><a href="#">Game Jams</a></li> -{% endblock %} - -{% block event_content %} - <p>Sorry, this page is not ready yet.</p> -{% endblock %} diff --git a/pydis_site/templates/events/sidebar/code-jams/6.html b/pydis_site/templates/events/sidebar/code-jams/6.html deleted file mode 100644 index 95a1f247..00000000 --- a/pydis_site/templates/events/sidebar/code-jams/6.html +++ /dev/null @@ -1,6 +0,0 @@ -<div class="box"> - <img src="https://raw.githubusercontent.com/python-discord/branding/master/events/winer_code_jam_2020/code%20jam%206%20-%20website%20banner.png" alt=""> - <p class="menu-label">Sponsors:</p> - <p><a href="https://do.co/devmeetup" target="_new"><img src="https://d9hhrg4mnvzow.cloudfront.net/try.digitalocean.com/developer-cloud/3f06d1cb-do-logo-blue-svg.svg" style="width: 100%;" alt=""></a></p> - <p><a href="https://jetbrains.com" target="_new"><img src="https://pythondiscord.com/static/images/sponsors/jetbrains.png" alt=""></a></p> -</div> diff --git a/pydis_site/templates/events/sidebar/code-jams/7.html b/pydis_site/templates/events/sidebar/code-jams/7.html deleted file mode 100644 index 4aefdbd9..00000000 --- a/pydis_site/templates/events/sidebar/code-jams/7.html +++ /dev/null @@ -1,12 +0,0 @@ -{% load static %} - -<div class="box"> - <img src="https://raw.githubusercontent.com/python-discord/branding/master/jams/summer_code_jam_2020/summer%20cj%202020%20discord%20banner.png" alt="Summer Code Jam 2020"> - <p class="menu-label">Sponsors</p> - <a href="https://www.djangoproject.com/" target="_blank"> - <img src="https://static.djangoproject.com/img/logos/django-logo-positive.png" alt="Django"> - </a> - <a href="https://jetbrains.com" target="_blank"> - <img src="{% static "images/sponsors/jetbrains.png" %}" alt="JetBrains"> - </a> -</div> diff --git a/pydis_site/templates/events/sidebar/code-jams/8.html b/pydis_site/templates/events/sidebar/code-jams/8.html deleted file mode 100644 index 36fad680..00000000 --- a/pydis_site/templates/events/sidebar/code-jams/8.html +++ /dev/null @@ -1,23 +0,0 @@ -{% load static %} -<div class="panel"> - <p class="panel-heading">Important Links</p> - <a class="panel-block has-text-link" href="{% url "events:page" path="code-jams/8/rules" %}">Rules</a> - <a class="panel-block has-text-link" href="{% url "events:page" path="code-jams/8/frameworks" %}">Approved Frameworks</a> - <a class="panel-block has-text-link" href="{% url "events:page" path="code-jams/8/github-bootcamp" %}">GitHub Bootcamp</a> - <a class="panel-block has-text-link" href="{% url "events:page" path="code-jams/8/submissions" %}">Submissions</a> - <a class="panel-block has-text-link" href="{% url "events:page" path="code-jams/code-style-guide" %}">The Code Style Guide</a> - </ul> -</div> -<div class="box"> - <img src="{% static "images/events/summer_code_jam_2021/banner.png" %}" alt="Summer Code Jam 2021"> - <h4 class="menu-label">Our Sponsors</h4> - <a href="https://www.digitalocean.com/" target="_blank"> - <img src="{% static "images/events/DO_Logo_Vertical_Blue.png" %}" alt="Digital Ocean"> - </a> - <a href="https://jetbrains.com" target="_blank"> - <img src="{% static "images/sponsors/jetbrains.png" %}" alt="JetBrains"> - </a> - <a href="https://www.tabnine.com/now?utm_source=discord&utm_medium=Ins&utm_campaign=PythonDis" target="_blank"> - <img src="{% static "images/events/Tabnine.png" %}" alt="Tabnine"> - </a> -</div> diff --git a/pydis_site/templates/events/sidebar/code-jams/9.html b/pydis_site/templates/events/sidebar/code-jams/9.html deleted file mode 100644 index 2351973f..00000000 --- a/pydis_site/templates/events/sidebar/code-jams/9.html +++ /dev/null @@ -1,21 +0,0 @@ -{% load static %} -<div class="panel"> - <p class="panel-heading">Important Links</p> - <a class="panel-block has-text-link" href="{% url "events:page" path="code-jams/9/rules" %}">Rules</a> - <a class="panel-block has-text-link" href="{% url "events:page" path="code-jams/9/frameworks" %}">Approved Frameworks</a> - <a class="panel-block has-text-link" href="{% url "events:page" path="code-jams/code-style-guide" %}">The Code Style Guide</a> - </ul> -</div> -<div class="box"> - <img src="{% static "images/events/summer_code_jam_2022/site_banner.png" %}" alt="Summer Code Jam 2022"> - <h4 class="menu-label">Our Sponsors</h4> - <a href="https://www.digitalocean.com/" target="_blank"> - <img src="{% static "images/events/DO_Logo_Vertical_Blue.png" %}" alt="Digital Ocean"> - </a> - <a href="https://jetbrains.com" target="_blank"> - <img src="{% static "images/sponsors/jetbrains.png" %}" alt="JetBrains"> - </a> - <a href="https://replit.com/" target="_blank"> - <img src="{% static "images/events/Replit.png" %}" alt="Replit"> - </a> -</div> diff --git a/pydis_site/templates/events/sidebar/code-jams/ongoing-code-jam.html b/pydis_site/templates/events/sidebar/code-jams/ongoing-code-jam.html deleted file mode 100644 index 37569e57..00000000 --- a/pydis_site/templates/events/sidebar/code-jams/ongoing-code-jam.html +++ /dev/null @@ -1,8 +0,0 @@ -{% load static %} - -<div class="box"> - <h4 class="menu-label">Upcoming Code Jam</h4> - <a href="{% url "events:page" path="code-jams/9" %}"> - <img src="{% static "images/events/summer_code_jam_2022/banner.png" %}" alt="Summer Code Jam 2022"> - </a> -</div> diff --git a/pydis_site/templates/events/sidebar/code-jams/previous-code-jams.html b/pydis_site/templates/events/sidebar/code-jams/previous-code-jams.html deleted file mode 100644 index 28412c53..00000000 --- a/pydis_site/templates/events/sidebar/code-jams/previous-code-jams.html +++ /dev/null @@ -1,14 +0,0 @@ -<div class="box"> - <p class="menu-label">Previous Code Jams</p> - <ul class="menu-list"> - <li><a class="has-text-link" href="{% url "events:page" path="code-jams/9" %}">Code Jam 9: It's Not A Bug, It's A Feature</a></li> - <li><a class="has-text-link" href="{% url "events:page" path="code-jams/8" %}">Code Jam 8: Think Inside the Box</a></li> - <li><a class="has-text-link" href="{% url "events:page" path="code-jams/7" %}">Code Jam 7: Early Internet</a></li> - <li><a class="has-text-link" href="{% url "events:page" path="code-jams/6" %}">Code Jam 6: Ancient Technology</a></li> - <li><a class="has-text-link" href="{% url "events:page" path="code-jams/5" %}">Code Jam 5: Climate Change</a></li> - <li><a class="has-text-link" href="{% url "events:page" path="code-jams/4" %}">Code Jam 4: This App Hates You</a></li> - <li><a class="has-text-link" href="{% url "events:page" path="code-jams/3" %}">Code Jam 3: Games!</a></li> - <li><a class="has-text-link" href="{% url "events:page" path="code-jams/2" %}">Code Jam 2: Mythology API</a></li> - <li><a class="has-text-link" href="{% url "events:page" path="code-jams/1" %}">Code Jam 1: Snake Bot</a></li> - </ul> -</div> diff --git a/pydis_site/templates/events/sidebar/code-jams/useful-information.html b/pydis_site/templates/events/sidebar/code-jams/useful-information.html deleted file mode 100644 index 87a92ade..00000000 --- a/pydis_site/templates/events/sidebar/code-jams/useful-information.html +++ /dev/null @@ -1,9 +0,0 @@ -<div class="box"> - <p class="menu-label">Useful information for code jam participants</p> - <ul class="menu-list"> - <li><a class="has-text-link" href="{% url "events:page" path="code-jams/using-git" %}">How to use git</a></li> - <li><a class="has-text-link" href="{% url "events:page" path="code-jams/judging" %}">How does judging work?</a></li> - <li><a class="has-text-link" href="{% url "events:page" path="code-jams/pull-request" %}">Opening a Pull Request</a></li> - <li><a class="has-text-link" href="{% url "events:page" path="code-jams/code-style-guide" %}">The Code Style Guide</a></li> - </ul> -</div> diff --git a/pydis_site/templates/events/sidebar/events-list.html b/pydis_site/templates/events/sidebar/events-list.html deleted file mode 100644 index 819fb485..00000000 --- a/pydis_site/templates/events/sidebar/events-list.html +++ /dev/null @@ -1,17 +0,0 @@ -<div class="box"> - <p class="menu-label">Event Calendar 2023</p> - <ul class="menu-list"> - <li><a class="has-text-black" style="cursor: default;">March: PyWeek 35</a></li> - <li><a class="has-text-black" style="cursor: default;">TBD: Summer Code Jam</a></li> - <li><a class="has-text-black" style="cursor: default;">September: PyWeek 36</a></li> - <li><a class="has-text-black" style="cursor: default;">TBD: Pixels</a></li> - <li><a class="has-text-black" style="cursor: default;">December: Advent of Code</a></li> - </ul> -</div> - -<div class="box"> - <p class="menu-label">Related Links</p> - <ul class="menu-list"> - <li><a class="has-text-link" href="{% url "events:page" path="code-jams" %}">Code Jams</a></li> - </ul> -</div> diff --git a/pydis_site/templates/events/sidebar/game-jams/2020.html b/pydis_site/templates/events/sidebar/game-jams/2020.html deleted file mode 100644 index 30042d2b..00000000 --- a/pydis_site/templates/events/sidebar/game-jams/2020.html +++ /dev/null @@ -1,14 +0,0 @@ -<div class="box"> - <p class="menu-label">Partners</p> - <p><a href="https://arcade.academy/" target="_new"><img src="https://i.imgur.com/CtY3Siv.png" style="width: 100%;" alt=""></a></p> -</div> - -<div class="box"> - <p class="menu-label">Relevant Links</p> - <ul class="menu-list"> - <li><a class="has-text-link" href="{% url "events:page" path="game-jams/2020/rules" %}">Rules</a></li> - <li><a class="has-text-link" href="{% url "events:page" path="game-jams/2020/project-setup" %}">Project Setup</a></li> - <li><a class="has-text-link" href="{% url "events:page" path="game-jams/2020/technical-requirements" %}">Technical Requirements</a></li> - <li><a class="has-text-link" href="{% url "events:page" path="game-jams/2020/judging" %}">Judging</a></li> - </ul> -</div> diff --git a/pydis_site/templates/events/sidebar/ongoing-event.html b/pydis_site/templates/events/sidebar/ongoing-event.html deleted file mode 100644 index e375fa38..00000000 --- a/pydis_site/templates/events/sidebar/ongoing-event.html +++ /dev/null @@ -1,8 +0,0 @@ -{% load static %} - -<div class="box"> - <p class="menu-label">Upcoming Event</p> - <a href="{% url "events:page" path="code-jams/9" %}"> - <img src="{% static "images/events/summer_code_jam_2022/banner.png" %}" alt="Summer Code Jam 2022"> - </a> -</div> diff --git a/pydis_site/templates/events/test-pages/my-event/_index.html b/pydis_site/templates/events/test-pages/my-event/_index.html deleted file mode 100644 index 1a859fdd..00000000 --- a/pydis_site/templates/events/test-pages/my-event/_index.html +++ /dev/null @@ -1 +0,0 @@ -{% extends "events/base.html" %} diff --git a/pydis_site/templates/events/test-pages/my-event/subpage.html b/pydis_site/templates/events/test-pages/my-event/subpage.html deleted file mode 100644 index 1a859fdd..00000000 --- a/pydis_site/templates/events/test-pages/my-event/subpage.html +++ /dev/null @@ -1 +0,0 @@ -{% extends "events/base.html" %} diff --git a/pydis_site/templates/home/index.html b/pydis_site/templates/home/index.html deleted file mode 100644 index cf6ff8cd..00000000 --- a/pydis_site/templates/home/index.html +++ /dev/null @@ -1,220 +0,0 @@ -{% extends 'base/base.html' %} -{% load static %} - -{% block title %}Home{% endblock %} -{% block head %} - <link rel="stylesheet" href="{% static "css/home/index.css" %}"> -{% endblock %} - -{% block content %} - {% include "base/navbar.html" %} - - <!-- Mobile-only Code Jam Banner --> - <section id="mobile-notice" class="is-primary is-hidden-tablet"> - <a href="/events/code-jams/9/"> - <img src="{% static "images/events/summer_code_jam_2022/site_banner.png" %}" alt="Summer Code Jam 2022"> - </a> - </section> - - <!-- Wave Hero --> - <section id="wave-hero" class="section is-hidden-mobile"> - - <div class="container"> - <div class="columns is-variable is-8 is-centered"> - - {# Embedded Welcome video #} - <div id="wave-hero-left" class="column is-half "> - <div class="force-aspect-container"> - <iframe - class="force-aspect-content" - src="https://www.youtube.com/embed/ZH26PuX3re0" - srcdoc=" - <style> - *{padding:0;margin:0;overflow:hidden} - html,body{height:100%} - img,span{position:absolute;width:100%;top:0;bottom:0;margin:auto} - span{height:1.5em;text-align:center;font:68px/1.5 sans-serif;color:#FFFFFFEE;text-shadow:0 0 0.1em #00000020} - </style> - <a href=https://www.youtube.com/embed/ZH26PuX3re0?autoplay=1> - <img src='{% static "images/frontpage/welcome.jpg" %}' alt='Welcome to Python Discord'> - <span>▶</span> - </a>" - allow="autoplay; accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture" - allowfullscreen - ></iframe> - </div> - </div> - - {# Code Jam Banner #} - <div id="wave-hero-right" class="column is-half"> - <a href="/events/code-jams/9/"> - <img src="{% static "images/events/summer_code_jam_2022/site_banner.png" %}" alt="Summer Code Jam 2022"> - </a> - </div> - </div> - - </div> - - {# Animated wave elements #} - <span id="front-wave" class="wave"></span> - <span id="back-wave" class="wave"></span> - <span id="bottom-wave" class="wave"></span> - - </section> - - <!-- Main Body --> - <section id="body" class="section"> - - <div class="container"> - <h1 class="is-size-1">Who are we?</h1> - - <div class="columns is-desktop"> - <div class="column is-half-desktop content"> - <p> - We're a large community focused around the Python programming language. - We believe anyone can learn to code, and are very dedicated to helping - novice developers take their first steps into the world of programming. We also - attract a lot of expert developers who are seeking friendships, collaborators, - and who wish to hone their craft by teaching and getting involved in the community. - </p> - <p> - We organise regular community events such as code jams, open-source hackathons, - seasonal events, and community challenges. Through our sponsorships and donations, - many of our events even have prizes to win! - </p> - <p> - You can find help with most Python-related problems in one of our help channels. - Our staff of over 100 dedicated expert Helpers are available around the clock - in every timezone. Whether you're looking to learn the language or working on a - complex project, we've got someone who can help you if you get stuck. - </p> - </div> - - {# Showcase box #} - <section id="showcase" class="column is-half-desktop has-text-centered"> - <article class="box"> - - <header class="title">Interactive timeline</header> - - <div class="mini-timeline"> - <i class="fa fa-asterisk"></i> - <i class="fa fa-code"></i> - <i class="fab fa-lg fa-python"></i> - <i class="fab fa-discord"></i> - <i class="fa fa-sm fa-terminal"></i> - <i class="fa fa-bug"></i> - </div> - - <p class="subtitle"> - Discover the history of our community, and learn about the events that made our community what it is today. - </p> - - <div class="buttons are-large is-centered"> - <a href="{% url 'home:timeline' %}" class="button is-primary"> - <span>Check it out!</span> - <span class="icon"> - <i class="fas fa-arrow-right"></i> - </span> - </a> - </div> - - </article> - </section> - - </div> - </div> - </section> - - <!-- Projects --> - {% if repo_data %} - <section id="projects" class="section"> - <div class="container"> - <h1 class="is-size-1">Projects</h1> - - <div class="columns is-multiline is-tablet"> - - {# Generate project data from HomeView.repos #} - {% for repo in repo_data %} - <div class="column is-one-third-desktop is-half-tablet"> - - <a href="https://github.com/{{ repo.repo_name }}"> - <article class="card"> - - <header class="card-header"> - <span class="card-header-icon"> - <span class="icon"><i class="fab fa-github"></i></span> - </span> - <div class="card-header-title"> - {{ repo.repo_name|cut:"python-discord/" }} - </div> - </header> - - <p class="card-content"> - {{ repo.description }} - </p> - - <footer class="card-footer"> - <div class="card-footer-item"> - <i class="repo-language-dot {{ repo.language | lower }}"></i> - {{ repo.language }} - </div> - <div class="card-footer-item"> - <i class="fas fa-star"></i> - {{ repo.stargazers }} - </div> - <div class="card-footer-item"> - <i class="fas fa-code-branch"></i> - {{ repo.forks }} - </div> - </footer> - - </article> - </a> - - </div> - {% endfor %} - - </div> - - </div> - </section> - {% endif %} - - <!-- Sponsors --> - <section id="sponsors" class="hero is-light"> - <div class="hero-body"> - <div class="container"> - <h1 class="title is-6 has-text-grey"> - Sponsors - </h1> - <div class="columns is-mobile is-multiline"> - <a href="https://www.netcup.eu/" class="column is-narrow"> - <img src="{% static "images/sponsors/netcup.png" %}" alt="netcup"/> - </a> - <a href="https://www.linode.com/?r=3bc18ce876ff43ea31f201b91e8e119c9753f085" class="column is-narrow"> - <img src="{% static "images/sponsors/linode.png" %}" alt="Linode"/> - </a> - <a href="https://jetbrains.com" class="column is-narrow"> - <img src="{% static "images/sponsors/jetbrains.png" %}" alt="JetBrains"/> - </a> - <a href="https://sentry.io" class="column is-narrow"> - <img src="{% static "images/sponsors/sentry.png" %}" alt="Sentry"/> - </a> - <a href="https://notion.so" class="column is-narrow"> - <img src="{% static "images/sponsors/notion.png" %}" alt="Notion"/> - </a> - <a href="https://streamyard.com" class="column is-narrow"> - <img src="{% static "images/sponsors/streamyard.png" %}" alt="StreamYard"/> - </a> - <a href="https://www.netlify.com/" class="column is-narrow"> - <img src="{% static "images/sponsors/netlify.png" %}" alt="Netlify"/> - </a> - <a href="https://www.cloudflare.com/" class="column is-narrow"> - <img src="{% static "images/sponsors/cloudflare.png" %}" alt="Cloudflare"/> - </a> - </div> - </div> - </div> - </section> - -{% endblock %} diff --git a/pydis_site/templates/home/timeline.html b/pydis_site/templates/home/timeline.html deleted file mode 100644 index 8b152f61..00000000 --- a/pydis_site/templates/home/timeline.html +++ /dev/null @@ -1,692 +0,0 @@ -{% extends 'base/base.html' %} -{% load static %} - -{% block title %}Timeline{% endblock %} -{% block head %} - <link rel="stylesheet" href="{% static "css/home/timeline.css" %}"> - <link rel="stylesheet" href="{% static "css/home/index.css" %}"> -{% endblock %} - -{% block content %} - {% include "base/navbar.html" %} - - <section class="cd-timeline js-cd-timeline"> - <div class="container max-width-lg cd-timeline__container"> - <div class="cd-timeline__block"> - <div class="cd-timeline__img pastel-red cd-timeline__img--picture"> - <i class="fa fa-youtube-play"></i> - </div> - - <div class="cd-timeline__content text-component"> - <h2>Summer Code Jam 2020 Highlights</h2> - <p class="color-contrast-medium"> - We release a new video to our YouTube showing the best projects from the Summer Code Jam 2020. - Better late than never! - </p> - - <div class="force-aspect-container"> - <iframe class="force-aspect-content" src="https://www.youtube.com/embed/g9cnp4W0P54" frameborder="0" - allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" - allowfullscreen></iframe> - </div> - - <div class="flex justify-between items-center"> - <span class="cd-timeline__date">Mar 21st, 2021</span> - </div> - </div> - </div> - - <div class="cd-timeline__block"> - <div class="cd-timeline__img pastel-purple cd-timeline__img--picture"> - <i class="fa fa-comment"></i> - </div> - - <div class="cd-timeline__content text-component"> - <h2>New feature: Weekly discussion channel</h2> - <p class="color-contrast-medium">Every week (or two weeks), we'll be posting a new topic to discuss in a - channel called <b>#weekly-topic-discussion</b>. Our inaugural topic is a PyCon talk by Anthony Shaw called - <b>Wily Python: Writing simpler and more maintainable Python.</b></a>. - </p> - - <div class="force-aspect-container"> - <iframe class="force-aspect-content" src="https://www.youtube.com/embed/dqdsNoApJ80" frameborder="0" - allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" - allowfullscreen></iframe> - </div> - - <div class="flex justify-between items-center"> - <span class="cd-timeline__date">Mar 13th, 2021</span> - </div> - </div> - </div> - - <div class="cd-timeline__block"> - <div class="cd-timeline__img pastel-pink cd-timeline__img--picture"> - <i class="fa fa-microphone"></i> - </div> - - <div class="cd-timeline__content text-component"> - <h2>We're on the Teaching Python podcast!</h2> - <p class="color-contrast-medium">Leon joins Sean and Kelly on the Teaching Python podcast to discuss how the pandemic has - changed the way we learn, and what role communities like Python Discord can play in this new world. - You can find the episode <a href="https://teachingpython.fm/63"> at teachingpython.fm</a>. - </p> - - <iframe width="100%" height="166" frameborder="0" scrolling="no" - src="https://player.fireside.fm/v2/UIYXtbeL+qOjGAsKi?theme=dark" - ></iframe> - - <div class="flex justify-between items-center"> - <span class="cd-timeline__date">Mar 13th, 2021</span> - </div> - </div> - </div> - - <div class="cd-timeline__block"> - <div class="cd-timeline__img pastel-green cd-timeline__img--picture"> - <i class="fa fa-microphone"></i> - </div> - - <div class="cd-timeline__content text-component"> - <h2>Leon Sandøy appears on Talk Python To Me</h2> - <p class="color-contrast-medium">Leon goes on the Talk Python to Me podcast with Michael Kennedy - to discuss the history of Python Discord, the critical importance of culture, and how to run a massive - community. You can find the episode <a href="https://talkpython.fm/episodes/show/305/python-community-at-python-discord"> at talkpython.fm</a>. - </p> - - <iframe width="100%" height="166" scrolling="no" frameborder="no" - src="https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/996083146&color=ff5500&auto_play=false&hide_related=false&show_comments=true&show_user=true&show_reposts=false"> - </iframe> - - <div class="flex justify-between items-center"> - <span class="cd-timeline__date">Mar 1st, 2021</span> - </div> - </div> - </div> - - <div class="cd-timeline__block"> - <div class="cd-timeline__img pastel-dark-blue cd-timeline__img--picture"> - <i class="fa fa-users"></i> - </div> - - <div class="cd-timeline__content text-component"> - <h2>We now have 150,000 members!</h2> - <p class="color-contrast-medium">Our growth continues to accelerate.</p> - - <div class="flex justify-between items-center"> - <span class="cd-timeline__date">Feb 18th, 2021</span> - </div> - </div> - </div> - - <div class="cd-timeline__block"> - <div class="cd-timeline__img pastel-blue cd-timeline__img--picture"> - <i class="fa fa-music"></i> - </div> - - <div class="cd-timeline__content text-component"> - <h2>We release The PEP 8 song</h2> - <p class="color-contrast-medium">We release the PEP 8 song on our YouTube channel, which finds tens of - thousands of listeners!</p> - - <div class="force-aspect-container"> - <iframe class="force-aspect-content" src="https://www.youtube.com/embed/hgI0p1zf31k" frameborder="0" - allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" - allowfullscreen></iframe> - </div> - - <div class="flex justify-between items-center"> - <span class="cd-timeline__date">February 8th, 2021</span> - </div> - </div> - </div> - - <div class="cd-timeline__block"> - <div class="cd-timeline__img pastel-red cd-timeline__img--picture"> - <i class="fa fa-snowflake-o"></i> - </div> - - <div class="cd-timeline__content text-component"> - <h2>Advent of Code attracts hundreds of participants</h2> - <p class="color-contrast-medium"> - A total of 443 Python Discord members sign up to be part of - <a href="https://adventofcode.com/">Eric Wastl's excellent Advent of Code event</a>. - As always, we provide dedicated announcements, scoreboards, bot commands and channels for our members - to enjoy the event in. - - </p> - - <div class="flex justify-between items-center"> - <span class="cd-timeline__date">December 1st - 25th, 2020</span> - </div> - </div> - </div> - - <div class="cd-timeline__block"> - <div class="cd-timeline__img pastel-orange cd-timeline__img--picture"> - <i class="fa fa-wrench"></i> - </div> - - <div class="cd-timeline__content text-component"> - <h2>We migrate all our infrastructure to Kubernetes</h2> - <p class="color-contrast-medium">As our tech stack grows, we decide to migrate all our services over to a - container orchestration paradigm via Kubernetes. This gives us better control and scalability. - <b>Joe Banks</b> takes on the role as DevOps Lead. - </p> - - <div class="flex justify-between items-center"> - <span class="cd-timeline__date">Nov 29th, 2020</span> - </div> - </div> - </div> - - <div class="cd-timeline__block"> - <div class="cd-timeline__img pastel-dark-blue cd-timeline__img--picture"> - <i class="fa fa-users"></i> - </div> - - <div class="cd-timeline__content text-component"> - <h2>Python Discord hits 100,000 members!</h2> - <p class="color-contrast-medium">Only six months after hitting 40,000 users, we hit 100,000 users. A - monumental milestone, - and one we're very proud of. To commemorate it, we create this timeline.</p> - - <div class="flex justify-between items-center"> - <span class="cd-timeline__date">Oct 22nd, 2020</span> - </div> - </div> - </div> - - <div class="cd-timeline__block"> - <div class="cd-timeline__img cd-timeline__img--picture"> - <img src="{% static "images/timeline/cd-icon-picture.svg" %}" alt="Picture"> - </div> - - <div class="cd-timeline__content text-component"> - <h2>Python Discord hosts the 2020 CPython Core Developer Q&A</h2> - <div class="force-aspect-container"> - <iframe class="force-aspect-content" src="https://www.youtube.com/embed/gXMdfBTcOfQ" frameborder="0" - allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" - allowfullscreen></iframe> - </div> - - <div class="flex justify-between items-center"> - <span class="cd-timeline__date">Oct 21st, 2020</span> - </div> - </div> - </div> - - <div class="cd-timeline__block"> - <div class="cd-timeline__img pastel-pink cd-timeline__img--picture"> - <i class="fa fa-handshake"></i> - </div> - - <div class="cd-timeline__content text-component"> - <h2>Python Discord is now the new home of the PyWeek event!</h2> - <p class="color-contrast-medium">PyWeek, a game jam that has been running since 2005, joins Python - Discord as one of our official events. Find more information about PyWeek on <a - href="https://pyweek.org/">their official website</a>.</p> - - <div class="flex justify-between items-center"> - <span class="cd-timeline__date">Aug 16th, 2020</span> - </div> - </div> - </div> - - <div class="cd-timeline__block"> - <div class="cd-timeline__img pastel-blue cd-timeline__img--picture"> - <i class="fa fa-dice"></i> - </div> - - <div class="cd-timeline__content text-component"> - <h2>PyDis summer code jam 2020 with the theme “Early Internet” and Django as the technology</h2> - <p class="color-contrast-medium">Sponsored by the Django Software Foundation and JetBrains, the Summer - Code Jam for 2020 attracts hundreds of participants, and sees the creation of some fantastic - projects. Check them out in our judge stream below:</p> - - <div class="force-aspect-container"> - <iframe class="force-aspect-content" src="https://www.youtube.com/embed/OFtm8f2iu6c" frameborder="0" - allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" - allowfullscreen></iframe> - </div> - - <div class="flex justify-between items-center"> - <span class="cd-timeline__date">Jul 31st, 2020</span> - </div> - </div> - </div> - - <div class="cd-timeline__block"> - <div class="cd-timeline__img pastel-red cd-timeline__img--picture"> - <i class="fa fa-chart-bar"></i> - </div> - - <div class="cd-timeline__content text-component"> - <h2>Python Discord Public Statistics are now live</h2> - <p class="color-contrast-medium">After getting numerous requests to publish beautiful data on member - count and channel use, we create <a href="https://stats.pythondiscord.com/">stats.pythondiscord.com</a> for - all to enjoy.</p> - - <div class="flex justify-between items-center"> - <span class="cd-timeline__date">Jun 4th, 2020</span> - </div> - </div> - </div> - - <div class="cd-timeline__block"> - <div class="cd-timeline__img pastel-pink cd-timeline__img--picture"> - <i class="fa fa-handshake"></i> - </div> - - <div class="cd-timeline__content text-component"> - <h2>Python Discord is now listed on python.org/community</h2> - <p class="color-contrast-medium">After working towards this goal for months, we finally work out an - arrangement with the PSF that allows us to be listed on that most holiest of websites: - https://python.org/. <a href="https://youtu.be/yciX2meIkXI?t=3">There was much rejoicing.</a></p> - - <div class="flex justify-between items-center"> - <span class="cd-timeline__date">May 28th, 2020</span> - </div> - </div> - </div> - - <div class="cd-timeline__block"> - <div class="cd-timeline__img pastel-green cd-timeline__img--picture"> - <i class="fa fa-comments"></i> - </div> - - <div class="cd-timeline__content text-component"> - <h2>ModMail is now live</h2> - <p class="color-contrast-medium">Having originally planned to write our own ModMail bot from scratch, we - come across an exceptionally good <a href="https://github.com/kyb3r/modmail">ModMail bot by - kyb3r</a> and decide to just self-host that one instead.</p> - - <div class="flex justify-between items-center"> - <span class="cd-timeline__date">May 25th, 2020</span> - </div> - </div> - </div> - - <div class="cd-timeline__block"> - <div class="cd-timeline__img pastel-purple cd-timeline__img--picture"> - <i class="fa fa-gamepad"></i> - </div> - - <div class="cd-timeline__content text-component"> - <h2>PyDis Game Jam 2020 with the “Three of a Kind” theme and Arcade as the technology</h2> - <p class="color-contrast-medium">The creator of Arcade, Paul Vincent Craven, joins us as a judge. - Several of the Code Jam participants also end up getting involved contributing to the Arcade - repository.</p> - - <div class="force-aspect-container"> - <iframe class="force-aspect-content" src="https://www.youtube.com/embed/KkLXMvKfEgs" frameborder="0" - allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" - allowfullscreen></iframe> - </div> - - <div class="flex justify-between items-center"> - <span class="cd-timeline__date">Apr 17th, 2020</span> - </div> - </div> - </div> - - <div class="cd-timeline__block"> - <div class="cd-timeline__img pastel-dark-blue cd-timeline__img--picture"> - <i class="fa fa-users"></i> - </div> - - <div class="cd-timeline__content text-component"> - <h2>Python Discord hits 40,000 members, and is now bigger than Liechtenstein.</h2> - <p class="color-contrast-medium"><img - src="https://cdn.discordapp.com/attachments/354619224620138496/699666518476324954/unknown.png"> - </p> - - <div class="flex justify-between items-center"> - <span class="cd-timeline__date">Apr 14, 2020</span> - </div> - </div> - </div> - - <div class="cd-timeline__block"> - <div class="cd-timeline__img pastel-green cd-timeline__img--picture"> - <i class="fa fa-comments"></i> - </div> - - <div class="cd-timeline__content text-component"> - <h2>The new help channel system is live</h2> - <p class="color-contrast-medium">We release our dynamic help-channel system, which allows you to claim - your very own help channel instead of fighting over the static help channels. We release a <a - href="https://pythondiscord.com/pages/resources/guides/help-channels/">Help Channel Guide</a> to - help our members fully understand how the system works.</p> - - <div class="flex justify-between items-center"> - <span class="cd-timeline__date">Apr 5th, 2020</span> - </div> - </div> - </div> - - <div class="cd-timeline__block"> - <div class="cd-timeline__img pastel-blue cd-timeline__img--picture"> - <i class="fa fa-dice"></i> - </div> - - <div class="cd-timeline__content text-component"> - <h2>PyDis sixth code jam with the theme “Ancient technology” and the technology Kivy</h2> - <p class="color-contrast-medium">Our Code Jams are becoming an increasingly big deal, and the Kivy core - developers join us to judge the event and help out our members during the event. One of them, - @tshirtman, even joins our staff!</p> - - <div class="force-aspect-container"> - <iframe class="force-aspect-content" src="https://www.youtube.com/embed/8fbZsGrqBzo" frameborder="0" - allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" - allowfullscreen></iframe> - </div> - - <div class="flex justify-between items-center"> - <span class="cd-timeline__date">Jan 17, 2020</span> - </div> - </div> - </div> - - <div class="cd-timeline__block"> - <div class="cd-timeline__img pastel-dark-blue cd-timeline__img--picture"> - <i class="fa fa-users"></i> - </div> - - <div class="cd-timeline__content text-component"> - <h2>PyDis hits 30,000 members</h2> - <p class="color-contrast-medium">More than tripling in size since the year before, the community hits - 30000 users. At this point, we're probably the largest Python chat community on the planet.</p> - - <div class="flex justify-between items-center"> - <span class="cd-timeline__date">Dec 22nd, 2019</span> - </div> - </div> - </div> - - <div class="cd-timeline__block"> - <div class="cd-timeline__img cd-timeline__img--picture"> - <img src={% static "images/timeline/cd-icon-picture.svg" %} alt="Picture"> - </div> - - <div class="cd-timeline__content text-component"> - <h2>Sebastiaan Zeef becomes an owner</h2> - <p class="color-contrast-medium">After being a long time active contributor to our projects and the driving - force behind many of our events, Sebastiaan Zeef joins the Owners Team alongside Joe & Leon.</p> - - <div class="flex justify-between items-center"> - <span class="cd-timeline__date">Sept 22nd, 2019</span> - </div> - </div> - </div> - - <div class="cd-timeline__block"> - <div class="cd-timeline__img pastel-lime cd-timeline__img--picture"> - <i class="fa fa-scroll"></i> - </div> - - <div class="cd-timeline__content text-component"> - <h2>The code of conduct is created</h2> - <p class="color-contrast-medium">Inspired by the Adafruit, Rust and Django communities, an essential - community pillar is created; Our <a href="https://pythondiscord.com/pages/code-of-conduct/">Code of - Conduct.</a></p> - - <div class="flex justify-between items-center"> - <span class="cd-timeline__date">Oct 26th, 2019</span> - </div> - </div> - </div> - - <div class="cd-timeline__block"> - <div class="cd-timeline__img pastel-orange cd-timeline__img--picture"> - <i class="fa fa-code"></i> - </div> - - <div class="cd-timeline__content text-component"> - <h2>The Django rewrite of pythondiscord.com is now live!</h2> - <p class="color-contrast-medium">The site is getting more and more complex, and it's time for a rewrite. - We decide to go for a different stack, and build a website based on Django, DRF, Bulma and - PostgreSQL.</p> - - <div class="flex justify-between items-center"> - <span class="cd-timeline__date">Sep 15, 2019</span> - </div> - </div> - </div> - - <div class="cd-timeline__block"> - <div class="cd-timeline__img pastel-dark-blue cd-timeline__img--picture"> - <i class="fa fa-users"></i> - </div> - - <div class="cd-timeline__content text-component"> - <h2>PyDis hits 15,000 members; the “hot ones special” video is released</h2> - <div class="force-aspect-container"> - <iframe class="force-aspect-content" src="https://www.youtube.com/embed/DIBXg8Qh7bA" frameborder="0" - allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" - allowfullscreen></iframe> - </div> - - <div class="flex justify-between items-center"> - <span class="cd-timeline__date">Apr 8th, 2019</span> - </div> - </div> - </div> - - <div class="cd-timeline__block"> - <div class="cd-timeline__img pastel-orange cd-timeline__img--picture"> - <i class="fa fa-code"></i> - </div> - - <div class="cd-timeline__content text-component"> - <h2>django-simple-bulma is released on PyPi</h2> - <p class="color-contrast-medium">Our very first package on PyPI, <a - href="https://pypi.org/project/django-simple-bulma/">django-simple-bulma</a> is a package that - sets up the Bulma CSS framework for your Django application and lets you configure everything in - settings.py.</p> - - <div class="flex justify-between items-center"> - <span class="cd-timeline__date">Dec 19th, 2018</span> - </div> - </div> - </div> - - <div class="cd-timeline__block"> - <div class="cd-timeline__img pastel-dark-blue cd-timeline__img--picture"> - <i class="fa fa-users"></i> - </div> - - <div class="cd-timeline__content text-component"> - <h2>PyDis hits 10,000 members</h2> - <p class="color-contrast-medium">We partner with RLBot, move from GitLab to GitHub, and start putting - together the first Advent of Code event.</p> - - <div class="flex justify-between items-center"> - <span class="cd-timeline__date">Nov 24th, 2018</span> - </div> - </div> - </div> - - <div class="cd-timeline__block"> - <div class="cd-timeline__img pastel-blue cd-timeline__img--picture"> - <i class="fa fa-dice"></i> - </div> - - <div class="cd-timeline__content text-component"> - <h2>First Hacktoberfest PyDis event; @Sir Lancebot is created</h2> - <p class="color-contrast-medium">We create a second bot for our community and fill it up with simple, - fun and relatively easy issues. The idea is to create an approachable arena for our members to cut - their open-source teeth on, and to provide lots of help and hand-holding for those who get stuck. - We're training our members to be productive contributors in the open-source ecosystem.</p> - - <div class="flex justify-between items-center"> - <span class="cd-timeline__date">Oct 1st, 2018</span> - </div> - </div> - </div> - - <div class="cd-timeline__block"> - <div class="cd-timeline__img pastel-pink cd-timeline__img--picture"> - <i class="fa fa-handshake"></i> - </div> - - <div class="cd-timeline__content text-component"> - <h2>PyDis is now partnered with Discord; the vanity URL discord.gg/python is created</h2> - <p class="color-contrast-medium">After being rejected for their Partner program several times, we - finally get approved. The recent partnership with the r/Python subreddit plays a significant role in - qualifying us for this partnership.</p> - - <div class="flex justify-between items-center"> - <span class="cd-timeline__date">Jul 10th, 2018</span> - </div> - </div> - </div> - - <div class="cd-timeline__block"> - <div class="cd-timeline__img pastel-dark-blue cd-timeline__img--picture"> - <i class="fa fa-users"></i> - </div> - - <div class="cd-timeline__content text-component"> - <h2>PyDis hits 5,000 members and partners with r/Python</h2> - <p class="color-contrast-medium">As we continue to grow, we approach the r/Python subreddit and ask to - become their official Discord community. They agree, and we become listed in their sidebar, giving - us yet another source of new members.</p> - - <div class="flex justify-between items-center"> - <span class="cd-timeline__date">Jun 20th, 2018</span> - </div> - </div> - </div> - - <div class="cd-timeline__block"> - <div class="cd-timeline__img pastel-pink cd-timeline__img--picture"> - <i class="fa fa-handshake"></i> - </div> - - <div class="cd-timeline__content text-component"> - <h2>Do You Even Python and PyDis merger</h2> - <p class="color-contrast-medium">At this point in time, there are only two serious Python communities on - Discord - Ours, and one called Do You Even Python. We approach the owners of DYEP with a bold - proposal - let's shut down their community, replace it with links to ours, and in return we will let - their staff join our staff. This gives us a big boost in members, and eventually leads to @eivl and - @Mr. Hemlock joining our Admin team</p> - - <div class="flex justify-between items-center"> - <span class="cd-timeline__date">Jun 9th, 2018</span> - </div> - </div> - </div> - - <div class="cd-timeline__block"> - <div class="cd-timeline__img pastel-lime cd-timeline__img--picture"> - <i class="fa fa-scroll"></i> - </div> - - <div class="cd-timeline__content text-component"> - <h2>The privacy policy is created</h2> - <p class="color-contrast-medium">Since data privacy is quite important to us, we create a privacy page - pretty much as soon as our new bot and site starts collecting some data. To this day, we keep <a - href="https://pythondiscord.com/pages/privacy/">our privacy policy</a> up to date with all - changes, and since April 2020 we've started doing <a - href="https://pythondiscord.notion.site/6784e3a9752444e89d19e65fd4510d8d">monthly data reviews</a>.</p> - - <div class="flex justify-between items-center"> - <span class="cd-timeline__date">May 21st, 2018</span> - </div> - </div> - </div> - - <div class="cd-timeline__block"> - <div class="cd-timeline__img pastel-blue cd-timeline__img--picture"> - <i class="fa fa-dice"></i> - </div> - - <div class="cd-timeline__content text-component"> - <h2>First code jam with the theme “snakes”</h2> - <p class="color-contrast-medium">Our very first Code Jam attracts a handful of users who work in random - teams of 2. We ask our participants to write a snake-themed Discord bot. Most of the code written - for this jam still lives on in Sir Lancebot, and you can play with it by using the - <code>.snakes</code> command. For more information on this event, see <a - href="https://pythondiscord.com/pages/code-jams/code-jam-1-snakes-bot/">the event page</a></p> - - <div class="flex justify-between items-center"> - <span class="cd-timeline__date">Mar 23rd, 2018</span> - </div> - </div> - </div> - - <div class="cd-timeline__block"> - <div class="cd-timeline__img pastel-dark-blue cd-timeline__img--picture"> - <i class="fa fa-users"></i> - </div> - - <div class="cd-timeline__content text-component"> - <h2>PyDis hits 2,000 members; pythondiscord.com and @Python are live</h2> - <p class="color-contrast-medium">The public moderation bot we're using at the time, Rowboat, announces - it will be shutting down. We decide that we'll write our own bot to handle moderation, so that we - can have more control over its features. We also buy a domain and start making a website in Flask. - </p> - - <div class="flex justify-between items-center"> - <span class="cd-timeline__date">Mar 4th, 2018</span> - </div> - </div> - </div> - - <div class="cd-timeline__block"> - <div class="cd-timeline__img cd-timeline__img--picture"> - <img src={% static "images/timeline/cd-icon-picture.svg" %} alt="Picture"> - </div> - - <div class="cd-timeline__content text-component"> - <h2>Our logo is born. Thanks @Aperture!</h2> - <p class="pydis-logo-banner"><img - src="https://raw.githubusercontent.com/python-discord/branding/main/logos/logo_banner/logo_site_banner.svg"> - </p> - - <div class="flex justify-between items-center"> - <span class="cd-timeline__date">Feb 3rd, 2018</span> - </div> - </div> - </div> - - <div class="cd-timeline__block"> - <div class="cd-timeline__img pastel-dark-blue cd-timeline__img--picture"> - <i class="fa fa-users"></i> - </div> - - <div class="cd-timeline__content text-component"> - <h2>Python Discord hits 1,000 members</h2> - <p class="color-contrast-medium">Our main source of new users at this point is a post on Reddit that - happens to get very good SEO. We are one of the top 10 search engine hits for the search term - "python discord".</p> - - <div class="flex justify-between items-center"> - <span class="cd-timeline__date">Nov 10th, 2017</span> - </div> - </div> - </div> - - <div class="cd-timeline__block"> - <div class="cd-timeline__img cd-timeline__img--picture"> - <img src="{% static "images/timeline/cd-icon-picture.svg" %}" alt="Picture"> - </div> - - <div class="cd-timeline__content text-component"> - <h2>Python Discord is created</h2> - <p class="color-contrast-medium"><strong>Joe Banks</strong> becomes one of the owners around 3 days after it - is created, and <strong>Leon Sandøy</strong> (lemon) joins the owner team later in the year, when the community - has around 300 members.</p> - - <div class="flex justify-between items-center"> - <span class="cd-timeline__date">Jan 8th, 2017</span> - </div> - </div> - </div> - - </div> - </section> - - <script src="{% static "js/timeline/main.js" %}"></script> -{% endblock %} diff --git a/pydis_site/templates/resources/resource_box.html b/pydis_site/templates/resources/resource_box.html deleted file mode 100644 index 5ca46296..00000000 --- a/pydis_site/templates/resources/resource_box.html +++ /dev/null @@ -1,79 +0,0 @@ -{% load as_icon %} -{% load to_kebabcase %} -{% load get_category_icon %} - -<div class="box resource-box {{ resource.css_classes }}" data-resource-name="{{ resource.name }}"> - {% if 'title_url' in resource %} - <a href="{{ resource.title_url }}"> - {% include "resources/resource_box_header.html" %} - </a> - {% else %} - {% include "resources/resource_box_header.html" %} - {% endif %} - - <p>{{ resource.description|safe }}</p> - - <div class="is-flex is-align-items-center"> - {# Add primary link #} - {% if "title_url" in resource %} - <span class="icon is-size-4" style="margin: 5px;"> - <a href="{{ resource.title_url }}"> - <i class="fas fa-external-link-alt fa-fw is-size-4 resource-icon is-hoverable is-primary"></i> - </a> - </span> - {% endif %} - - {# Add all additional icon #} - {% for icon in resource.urls %} - <span class="icon is-size-4" style="margin: 5px;"> - <a href="{{ icon.url }}"> - <i class="{{ icon.icon|as_icon }} fa-fw is-size-4 resource-icon is-hoverable is-{{ icon.color }}"></i> - </a> - </span> - {% endfor %} - - {# Tags #} - <div class="resource-tag-container is-flex ml-auto is-flex-wrap-wrap is-justify-content-end"> - {% for tag in resource.tags.topics %} - <span - class="tag resource-tag is-primary is-light ml-2 mt-2" - data-filter-name="topics" - data-filter-item="{{ tag|to_kebabcase }}" - > - <i class="{{ tag|title|get_category_icon }} mr-1"></i> - {{ tag|title }} - </span> - {% endfor %} - {% for tag in resource.tags.type %} - <span - class="tag resource-tag has-background-success-light has-text-success-dark ml-2 mt-2" - data-filter-name="type" - data-filter-item="{{ tag|to_kebabcase }}" - > - <i class="{{ tag|title|get_category_icon }} mr-1"></i> - {{ tag|title }} - </span> - {% endfor %} - {% for tag in resource.tags.payment_tiers %} - <span - class="tag resource-tag has-background-danger-light has-text-danger-dark ml-2 mt-2" - data-filter-name="payment-tiers" - data-filter-item="{{ tag|to_kebabcase }}" - > - <i class="{{ tag|title|get_category_icon }} mr-1"></i> - {{ tag|title }} - </span> - {% endfor %} - {% for tag in resource.tags.difficulty %} - <span - class="tag resource-tag has-background-info-light has-text-info-dark ml-2 mt-2" - data-filter-name="difficulty" - data-filter-item="{{ tag|to_kebabcase }}" - > - <i class="{{ tag|title|get_category_icon }} mr-1"></i> - {{ tag|title }} - </span> - {% endfor %} - </div> - </div> -</div> diff --git a/pydis_site/templates/resources/resource_box_header.html b/pydis_site/templates/resources/resource_box_header.html deleted file mode 100644 index dfbdd92f..00000000 --- a/pydis_site/templates/resources/resource_box_header.html +++ /dev/null @@ -1,23 +0,0 @@ -{% load as_icon %} - -{# Icon section #} -{% if 'icon_image' in resource %} - <img src="{{ resource.icon_image }}" alt="" style="height: {{ resource.icon_size|default:30 }}px;"> -{% elif 'title_icon' in resource %} - <span class="icon is-size-4 is-medium"> - <i class="{{ resource.title_icon|as_icon }} is-size-3 resource-icon has-icon-padding is-{{ resource.title_icon_color }}"></i> - </span> -{% elif 'default_icon' in category_info %} - <span class="icon is-size-4 is-medium"> - <i class="{{ category_info.default_icon|as_icon }} is-size-3 resource-icon has-icon-padding is-{{ category_info.default_icon_color }}"></i> - </span> -{% endif %} - -{# Title section #} -<span class="is-size-4 has-text-weight-bold"> - {% if 'title_image' in resource %} - <img src="{{ resource.title_image }}" alt="" style="height: 50px; {{ resource.title_image_style }}"> - {% elif 'name' in resource %} - {{ resource.name }} - {% endif %} -</span> diff --git a/pydis_site/templates/resources/resources.html b/pydis_site/templates/resources/resources.html deleted file mode 100644 index 101f9965..00000000 --- a/pydis_site/templates/resources/resources.html +++ /dev/null @@ -1,180 +0,0 @@ -{% extends 'base/base.html' %} -{% load as_icon %} -{% load to_kebabcase %} -{% load get_category_icon %} -{% load static %} - -{% block title %}Resources{% endblock %} -{% block head %} - {# Inject a JSON object of all valid filter types from the view #} - <script> - const validFilters = {{ valid_filters | safe }} - </script> - - <link rel="stylesheet" href="{% static "css/resources/resources.css" %}"> - <link rel="stylesheet" href="{% static "css/collapsibles.css" %}"> - <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> - <script defer src="{% static "js/resources/resources.js" %}"></script> - <script defer src="{% static "js/collapsibles.js" %}"></script> - <script defer src="{% static "js/fuzzysort/fuzzysort.js" %}"></script> -{% endblock %} - -{% block content %} - {% include "base/navbar.html" %} - <input type="hidden" id="resource-type-input" value="{{ resource_type }}"> - <section class="section"> - <div class="columns is-variable is-6 is-centered"> - {# Filtering toolbox #} - <div class="column filtering-column is-one-third"> - <div class="content is-justify-content-center"> - <nav id="resource-filtering-panel" class="panel is-primary"> - <p class="panel-heading has-text-centered" id="filter-panel-header">Filter resources</p> - - {# Search bar #} - <p id="resource-search" class="control has-icons-left"> - <input class="input" placeholder="Search resources "> - <span class="icon is-small is-left"> - <i class="fas fa-magnifying-glass"></i> - </span> - </p> - - - {# Filter box tags #} - <div class="card filter-tags"> - <div class="is-flex ml-auto"> - <div id="tag-pool"> - {# A filter tag for when there are no filters active #} - <span class="tag no-tags-selected is-secondary ml-2 mt-2"> - <i class="fas fa-fw fa-ban mr-1"></i> - No filters selected - </span> - - {# A filter tag for search queries #} - <span class="tag search-query is-secondary ml-2 mt-2"> - <i class="fas fa-fw fa-magnifying-glass mr-1"></i> - <span class="tag inner">Search: ...</span> - </span> - - {% for filter_name, filter_data in filters.items %} - {% for filter_item in filter_data.filters %} - {% if filter_name == "Difficulty" %} - <span - class="filter-box-tag tag has-background-info-light has-text-info-dark ml-2 mt-2" - data-filter-name="{{ filter_name|to_kebabcase }}" - data-filter-item="{{ filter_item|to_kebabcase }}" - > - <i class="{{ filter_item|title|get_category_icon }} mr-1"></i> - {{ filter_item|title }} - <button class="delete is-small is-info has-background-info-light"></button> - </span> - {% endif %} - {% if filter_name == "Type" %} - <span - class="filter-box-tag tag has-background-success-light has-text-success-dark ml-2 mt-2" - data-filter-name="{{ filter_name|to_kebabcase }}" - data-filter-item="{{ filter_item|to_kebabcase }}" - > - <i class="{{ filter_item|title|get_category_icon }} mr-1"></i> - {{ filter_item|title }} - <button class="delete is-small is-success has-background-success-light"></button> - </span> - {% endif %} - {% if filter_name == "Payment tiers" %} - <span - class="filter-box-tag tag has-background-danger-light has-text-danger-dark ml-2 mt-2" - data-filter-name="{{ filter_name|to_kebabcase }}" - data-filter-item="{{ filter_item|to_kebabcase }}" - > - <i class="{{ filter_item|title|get_category_icon }} mr-1"></i> - {{ filter_item|title }} - <button class="delete is-small is-danger has-background-danger-light"></button> - </span> - {% endif %} - {% if filter_name == "Topics" %} - <span - class="filter-box-tag tag is-primary is-light ml-2 mt-2" - data-filter-name="{{ filter_name|to_kebabcase }}" - data-filter-item="{{ filter_item|to_kebabcase }}" - > - <i class="{{ filter_item|title|get_category_icon }} mr-1"></i> - {{ filter_item|title }} - <button class="delete is-small is-primary has-background-primary-light"></button> - </span> - {% endif %} - {% endfor %} - {% endfor %} - </div> - <div class="close-filters-button"> - {# A little x in the top right, visible only when filters are active, which removes all filters. #} - <a class="icon"> - <i class="fas fa-window-close"></i> - </a> - - </div> - </div> - </div> - - {# Filter checkboxes #} - {% for filter_name, filter_data in filters.items %} - <div class="card filter-category-header"> - <button type="button" class="card-header collapsible"> - <span class="card-header-title subtitle is-6 my-2 ml-2"> - <i class="fa-fw {{ filter_data.icon }} is-primary" aria-hidden="true"></i>  {{ filter_name }} - </span> - <span class="card-header-icon"> - {% if not filter_data.hidden %} - <i class="far fa-fw fa-window-minimize is-6 title" aria-hidden="true"></i> - {% else %} - <i class="fas fa-fw fa-angle-down is-6 title" aria-hidden="true"></i> - {% endif %} - </span> - </button> - - {# Checkboxes #} - {% if filter_data.hidden %} - <div class="collapsible-content collapsed"> - {% else %} - <div class="collapsible-content"> - {% endif %} - <div class="card-content"> - {% for filter_item in filter_data.filters %} - <a class="panel-block filter-panel"> - <label class="checkbox"> - <input - class="filter-checkbox" - type="checkbox" - data-filter-name="{{ filter_name|to_kebabcase }}" - data-filter-item="{{ filter_item|to_kebabcase }}" - > - {{ filter_item }} - </label> - </a> - {% endfor %} - </div> - </div> - </div> - {% endfor %} - </nav> - </div> - </div> - - <div class="column is-two-thirds"> - {# Message to display when there are no hits #} - <div class="no-resources-found"> - <h2 class="title is-3 has-text-centered pt-0 pb-6">No matching resources found!</h2> - <img src="{% static "images/resources/duck_pond_404.jpg" %}"> - </div> - - - {# Resource cards #} - <div class="content is-flex is-justify-content-center"> - <div class="container is-fullwidth"> - {% for resource in resources.values %} - {% include "resources/resource_box.html" %} - {% endfor %} - </div> - </div> - </div> - </div> - </section> -{% endblock %} diff --git a/pydis_site/templates/staff/logs.html b/pydis_site/templates/staff/logs.html deleted file mode 100644 index 5e2a200b..00000000 --- a/pydis_site/templates/staff/logs.html +++ /dev/null @@ -1,106 +0,0 @@ -{% extends 'base/base.html' %} -{% load static %} -{% load deletedmessage_filters %} - -{% block title %}Logs for Deleted Message Context {{ message_context.id }}{% endblock %} - -{% block head %} - <link rel="stylesheet" href="{% static "css/staff/logs.css" %}"> -{% endblock %} - -{% block content %} - <ul class="is-size-7"> - <li>Deleted by: <span style="color: {{ deletion_context.actor.top_role.colour | hex_colour }}">{{ deletion_context.actor }}</span></li> - <li>Date: {{ deletion_context.creation }}</li> - </ul> - <div class="is-divider has-small-margin"></div> - {% for message in deletion_context.deletedmessage_set.all reversed %} - <div class="discord-message"> - <div class="discord-message-header"> - <span class="discord-username" - style="color: {{ message.author.top_role.colour | hex_colour }}">{{ message.author }} - </span> - <span class="discord-message-metadata has-text-grey"> - User ID: {{ message.author.id }}<br> - {{ message.timestamp }} (Channel ID-Message ID: {{ message.channel_id }}-{{ message.id }}) - </span> - </div> - <div class="discord-message-content"> - {{ message.content | escape | visible_newlines | safe }} - </div> - <div class="discord-message-attachments"> - {% for attachment in message.attachments %} - <img alt="Attachment" class="discord-attachment" src="{{ attachment }}"> - {% endfor %} - </div> - {% for embed in message.embeds %} - <div class="discord-embed is-size-7"> - <div class="discord-embed-color" style="background-color: {% if embed.color %}{{ embed.color | hex_colour }}{% else %}#cacbce{% endif %}"></div> - <div class="discord-embed-inner"> - <div class="discord-embed-content"> - <div class="discord-embed-main"> - {% if embed.author %} - <div class="discord-embed-author"> - {% if embed.author.icon_url %} - <img alt="Author Icon" class="discord-embed-author-icon" - src="{{ embed.author.icon_url }}">{% endif %} - {% if embed.author.url %}<a class="discord-embed-author-url" - href="{{ embed.author.url }}">{% endif %} - <span class="discord-embed-author-name">{{ embed.author.name }}</span> - {% if embed.author.url %}</a>{% endif %} - </div> - {% endif %} - {% if embed.title %} - <div class="discord-embed-title"> - {% if embed.url %}<a href="{{ embed.url }}">{% endif %} - {{ embed.title }} - {% if embed.url %}</a>{% endif %} - </div> - {% endif %} - {% if embed.description %} - <div class="discord-embed-description"> - {{ embed.description | linebreaksbr }} - </div> - {% endif %} - {% if embed.fields %} - <div class="discord-embed-fields"> - {% for field in embed.fields %} - <div class="discord-embed-field{% if field.inline %} discord-embed-field-inline{% endif %}"> - <div class="discord-embed-field-name">{{ field.name }}</div> - <div class="discord-embed-field-value">{{ field.value }}</div> - </div> - {% endfor %} - </div> - {% endif %} - {% if embed.image %} - <div class="discord-embed-image"> - <img alt="Discord Embed Image" src="{{ embed.image.url }}"> - </div> - {% endif %} - </div> - {% if embed.thumbnail %} - <div class="discord-embed-thumbnail"> - <img alt="Embed thumbnail" src="{{ embed.thumbnail.url }}"> - </div> - {% endif %} - </div> - {% if embed.footer or embed.timestamp %} - <div class="discord-embed-footer"> - {% if embed.footer.icon_url %} - <img class="discord-embed-footer-icon" alt="Footer Icon" - src="{{ embed.footer.icon_url }}"> - {% endif %} - {% if embed.footer.text or embed.timestamp %} - <span class="discord-embed-footer-text">{% endif %} - {% if embed.footer.text %}{{ embed.footer.text }}{% endif %} - {% if embed.footer.text and embed.timestamp %} • {% endif %} - {% if embed.timestamp %}{{ embed.timestamp | footer_datetime }}{% endif %} - {% if embed.footer.text or embed.timestamp %}</span>{% endif %} - </div> - {% endif %} - </div> - </div> - {% endfor %} - </div> - {% endfor %} -{% endblock %} diff --git a/pydis_site/urls.py b/pydis_site/urls.py deleted file mode 100644 index 0f2f6aeb..00000000 --- a/pydis_site/urls.py +++ /dev/null @@ -1,35 +0,0 @@ -from django.contrib import admin -from django.urls import include, path - -from pydis_site import settings - -NON_STATIC_PATTERNS = [ - path('admin/', admin.site.urls), - - # External API ingress (over the net) - path('api/', include('pydis_site.apps.api.urls', namespace='api')), - # Internal API ingress (cluster local) - path('pydis-api/', include('pydis_site.apps.api.urls', namespace='internal_api')), - - path('', include('django_prometheus.urls')), -] if not settings.STATIC_BUILD else [] - - -urlpatterns = ( - *NON_STATIC_PATTERNS, - - # This must be mounted before the `content` app to prevent Django - # from wildcard matching all requests to `pages/...`. - path('', include('pydis_site.apps.redirect.urls')), - - path('pages/', include('pydis_site.apps.content.urls', namespace='content')), - path('resources/', include('pydis_site.apps.resources.urls')), - path('events/', include('pydis_site.apps.events.urls', namespace='events')), - path('', include('pydis_site.apps.home.urls', namespace='home')), -) - - -if not settings.STATIC_BUILD: - urlpatterns += ( - path('staff/', include('pydis_site.apps.staff.urls', namespace='staff')), - ) diff --git a/pydis_site/wsgi.py b/pydis_site/wsgi.py deleted file mode 100644 index 853e56f1..00000000 --- a/pydis_site/wsgi.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -WSGI config for pydis_site project. - -It exposes the WSGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/ -""" - -import os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'pydis_site.settings') - -application = get_wsgi_application() |