diff --git a/.github/workflows/ruff-check.yaml b/.github/workflows/ruff-check.yaml new file mode 100644 index 0000000..ecc1d2c --- /dev/null +++ b/.github/workflows/ruff-check.yaml @@ -0,0 +1,18 @@ +name: ruff-check +on: push +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install ruff + # Update output format to enable automatic inline annotations. + - name: Run Ruff + run: ruff check --output-format=github diff --git a/pyproject.toml b/pyproject.toml index 49fdf52..0793262 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,3 +28,6 @@ dependencies = [ "django-mathfilters>=1.0.0", "django-humanize>=0.1.2", ] + +[tool.ruff.lint.per-file-ignores] +"*/migrations/*.py" = ["RUF012"] diff --git a/slack_bot/slack.py b/slack_bot/slack.py deleted file mode 100644 index e69de29..0000000 diff --git a/twisted/.env.example b/twisted/.env.example index 624d072..5e6abc6 100644 --- a/twisted/.env.example +++ b/twisted/.env.example @@ -18,9 +18,7 @@ HACKATIME_CLIENT_SECRET = HACKATIME_REDIRECT_URI = https://localhost:8000/auth/hackatime_callback SLACK_TOKEN = xoxb-*** -SLACK_SIGNING_SECRET = -SLACK_APP_TOKEN = xapp-*** -SLACK_MODE = https +SLACK_LOG_CHANNEL = C*** ALLOWED_HOSTS = 127.0.0.1, localhost CSRF_TRUSTED_ORIGINS = http://127.0.0.1:8000, http://localhost:8000 diff --git a/twisted/common/admin.py b/twisted/common/admin.py index 8c38f3f..b97a94f 100644 --- a/twisted/common/admin.py +++ b/twisted/common/admin.py @@ -1,3 +1,2 @@ -from django.contrib import admin # Register your models here. diff --git a/twisted/common/apps.py b/twisted/common/apps.py index 5f2f078..3ce3894 100644 --- a/twisted/common/apps.py +++ b/twisted/common/apps.py @@ -2,4 +2,4 @@ class CommonConfig(AppConfig): - name = 'common' + name = "common" diff --git a/twisted/common/models.py b/twisted/common/models.py index 71a8362..35e0d64 100644 --- a/twisted/common/models.py +++ b/twisted/common/models.py @@ -1,3 +1,2 @@ -from django.db import models # Create your models here. diff --git a/twisted/common/tests.py b/twisted/common/tests.py index 7ce503c..4929020 100644 --- a/twisted/common/tests.py +++ b/twisted/common/tests.py @@ -1,3 +1,2 @@ -from django.test import TestCase # Create your tests here. diff --git a/twisted/common/views.py b/twisted/common/views.py index f15120b..8b13789 100644 --- a/twisted/common/views.py +++ b/twisted/common/views.py @@ -1,7 +1 @@ -import os -from django.shortcuts import render -from django.contrib.auth.decorators import login_required -from django.http import HttpResponse -from django.utils.translation import gettext_lazy as _ -import requests -import json + diff --git a/twisted/manage.py b/twisted/manage.py index a7da667..43fe8a5 100755 --- a/twisted/manage.py +++ b/twisted/manage.py @@ -1,12 +1,13 @@ #!/usr/bin/env python """Django's command-line utility for administrative tasks.""" + import os import sys def main(): """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -18,5 +19,5 @@ def main(): execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/twisted/mysite/asgi.py b/twisted/mysite/asgi.py index 540f6e4..3801320 100644 --- a/twisted/mysite/asgi.py +++ b/twisted/mysite/asgi.py @@ -11,6 +11,6 @@ from django.core.asgi import get_asgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") application = get_asgi_application() diff --git a/twisted/mysite/middleware.py b/twisted/mysite/middleware.py index 19e33d5..b11417e 100644 --- a/twisted/mysite/middleware.py +++ b/twisted/mysite/middleware.py @@ -1,6 +1,8 @@ import zoneinfo + from django.utils import timezone + class TimezoneMiddleware: def __init__(self, get_response): self.get_response = get_response @@ -13,7 +15,7 @@ def __call__(self, request): timezone.activate(zoneinfo.ZoneInfo(tzname)) else: timezone.deactivate() - except Exception as e: + except (zoneinfo.ZoneInfoNotFoundError, ValueError): timezone.deactivate() return self.get_response(request) diff --git a/twisted/mysite/settings.py b/twisted/mysite/settings.py index 6933734..21e2b0a 100644 --- a/twisted/mysite/settings.py +++ b/twisted/mysite/settings.py @@ -9,11 +9,13 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/6.0/ref/settings/ """ -from dotenv import load_dotenv + import os from pathlib import Path -if not os.environ.get('ALLOWED_HOSTS'): +from dotenv import load_dotenv + +if not os.environ.get("ALLOWED_HOSTS"): load_dotenv() # Build paths inside the project like this: BASE_DIR / 'subdir'. @@ -24,14 +26,14 @@ # See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = os.environ['SECRET_KEY'] +SECRET_KEY = os.environ["SECRET_KEY"] # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = os.environ.get('DEBUG', 'false').lower() in ['true', 'on', '1'] -DEBUG_REVIEW = os.environ.get('DEBUG_REVIEW', str(DEBUG)).lower() in ['true', 'on', '1'] +DEBUG = os.environ.get("DEBUG", "false").lower() in ["true", "on", "1"] +DEBUG_REVIEW = os.environ.get("DEBUG_REVIEW", str(DEBUG)).lower() in ["true", "on", "1"] USE_X_FORWARDED_HOST = True -SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") # Behind the TLS-terminating proxy above, cookies should never travel over plain # HTTP once we're not in local dev. @@ -42,40 +44,42 @@ SECURE_HSTS_INCLUDE_SUBDOMAINS = not DEBUG SECURE_HSTS_PRELOAD = not DEBUG -X_FRAME_OPTIONS = 'SAMEORIGIN' +X_FRAME_OPTIONS = "SAMEORIGIN" allowed_hosts_raw = os.getenv("ALLOWED_HOSTS", "127.0.0.1,localhost") ALLOWED_HOSTS = [host.strip() for host in allowed_hosts_raw.split(",") if host.strip()] -csrf_origins_raw = os.getenv("CSRF_TRUSTED_ORIGINS", "http://127.0.0.1:8000,http://localhost:8000") -CSRF_TRUSTED_ORIGINS = [origin.strip() for origin in csrf_origins_raw.split(",") if origin.strip()] +csrf_origins_raw = os.getenv( + "CSRF_TRUSTED_ORIGINS", "http://127.0.0.1:8000,http://localhost:8000" +) +CSRF_TRUSTED_ORIGINS = [ + origin.strip() for origin in csrf_origins_raw.split(",") if origin.strip() +] # Application definition -TAILWIND_APP_NAME = 'tailwindcsstheme' +TAILWIND_APP_NAME = "tailwindcsstheme" INSTALLED_APPS = [ # 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django.contrib.humanize', - + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "django.contrib.humanize", # Your apps - 'common', - 'twisted_site', - + "common", + "twisted_site", # 3rd party apps - 'django_cotton', - 'django_cotton_ui', - 'tailwind', + "django_cotton", + "django_cotton_ui", + "tailwind", TAILWIND_APP_NAME, - 'django_htmx', - 'django_extensions', - 'mathfilters', - 'django_humanize' + "django_htmx", + "django_extensions", + "mathfilters", + "django_humanize", ] if DEBUG: @@ -83,15 +87,15 @@ INSTALLED_APPS += ["django_browser_reload"] MIDDLEWARE = [ - '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', - 'mysite.middleware.TimezoneMiddleware', + "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", + "mysite.middleware.TimezoneMiddleware", "django_htmx.middleware.HtmxMiddleware", ] @@ -101,24 +105,24 @@ "django_browser_reload.middleware.BrowserReloadMiddleware", ] -ROOT_URLCONF = 'mysite.urls' +ROOT_URLCONF = "mysite.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'mysite.wsgi.application' +WSGI_APPLICATION = "mysite.wsgi.application" # Database @@ -141,16 +145,16 @@ AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] @@ -158,9 +162,9 @@ # Internationalization # https://docs.djangoproject.com/en/6.0/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True USE_L10N = True @@ -171,8 +175,8 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/6.0/howto/static-files/ -STATIC_URL = 'static/' -STATIC_ROOT = BASE_DIR / 'static' +STATIC_URL = "static/" +STATIC_ROOT = BASE_DIR / "static" CSRF_COOKIE_HTTPONLY = False NPM_BIN_PATH = os.environ.get("NPM_BIN_PATH", "npm") @@ -182,23 +186,28 @@ # Logging LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, # Keeps Gunicorn's loggers active - 'handlers': { - 'console': { - 'class': 'logging.StreamHandler', + "version": 1, + "disable_existing_loggers": False, # Keeps Gunicorn's loggers active + "handlers": { + "console": { + "class": "logging.StreamHandler", }, }, - 'root': { - 'handlers': ['console'], - 'level': 'INFO', + "root": { + "handlers": ["console"], + "level": "INFO", }, } # Ari (Review) # https://ari.hackclub.com/docs/webhooks -ARI_INGEST_ENDPOINT = os.environ.get('ARI_INGEST_ENDPOINT') -ARI_SIGNING_SECRET = os.environ.get('ARI_SIGNING_SECRET') +ARI_INGEST_ENDPOINT = os.environ.get("ARI_INGEST_ENDPOINT") +ARI_SIGNING_SECRET = os.environ.get("ARI_SIGNING_SECRET") # Separate from ARI_SIGNING_SECRET: signs deliveries Ari sends to us (Settings -> Webhooks), # not requests we send to Ari. -ARI_WEBHOOK_SECRET = os.environ.get('ARI_WEBHOOK_SECRET') \ No newline at end of file +ARI_WEBHOOK_SECRET = os.environ.get('ARI_WEBHOOK_SECRET') + + +# Slack +SLACK_TOKEN = os.environ.get('SLACK_TOKEN') +SLACK_LOG_CHANNEL = os.environ.get('SLACK_LOG_CHANNEL') diff --git a/twisted/mysite/urls.py b/twisted/mysite/urls.py index c5b2bf4..797f10b 100644 --- a/twisted/mysite/urls.py +++ b/twisted/mysite/urls.py @@ -15,9 +15,9 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ -from django.urls import include, path from django.conf import settings from django.conf.urls.static import static +from django.urls import include, path urlpatterns = [ path("", include("twisted_site.urls")), diff --git a/twisted/mysite/wsgi.py b/twisted/mysite/wsgi.py index ba5b07b..96cd3c5 100644 --- a/twisted/mysite/wsgi.py +++ b/twisted/mysite/wsgi.py @@ -11,6 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") application = get_wsgi_application() diff --git a/twisted/tailwindcsstheme/apps.py b/twisted/tailwindcsstheme/apps.py index 7e9716d..bb2220b 100644 --- a/twisted/tailwindcsstheme/apps.py +++ b/twisted/tailwindcsstheme/apps.py @@ -2,4 +2,4 @@ class TailwindcssthemeConfig(AppConfig): - name = 'tailwindcsstheme' + name = "tailwindcsstheme" diff --git a/twisted/twisted_site/admin.py b/twisted/twisted_site/admin.py index 8c38f3f..b97a94f 100644 --- a/twisted/twisted_site/admin.py +++ b/twisted/twisted_site/admin.py @@ -1,3 +1,2 @@ -from django.contrib import admin # Register your models here. diff --git a/twisted/twisted_site/apps.py b/twisted/twisted_site/apps.py index 0f020b2..1551da6 100644 --- a/twisted/twisted_site/apps.py +++ b/twisted/twisted_site/apps.py @@ -2,4 +2,4 @@ class TwistedSiteConfig(AppConfig): - name = 'twisted_site' + name = "twisted_site" diff --git a/twisted/twisted_site/ari.py b/twisted/twisted_site/ari.py index 682182e..ecfd902 100644 --- a/twisted/twisted_site/ari.py +++ b/twisted/twisted_site/ari.py @@ -10,7 +10,6 @@ from .models import Journal, Project, ProjectShip - ARI_INGEST_ENDPOINT = settings.ARI_INGEST_ENDPOINT ARI_SIGNING_SECRET = settings.ARI_SIGNING_SECRET ARI_WEBHOOK_SECRET = settings.ARI_WEBHOOK_SECRET @@ -19,7 +18,9 @@ WEBHOOK_MAX_AGE_SECONDS = 5 * 60 -def verify_webhook_signature(body: bytes, timestamp: str, delivery_id: str, signature: str) -> bool: +def verify_webhook_signature( + body: bytes, timestamp: str, delivery_id: str, signature: str +) -> bool: """Verifies an outbound delivery from Ari (the X-Ari-Signature/-Timestamp/-Delivery-Id headers on review.* and ship.updated webhooks). Signed with ARI_WEBHOOK_SECRET, which is separate from ARI_SIGNING_SECRET (that one signs requests we send to Ari).""" @@ -33,7 +34,7 @@ def verify_webhook_signature(body: bytes, timestamp: str, delivery_id: str, sign return False key_bytes = ARI_WEBHOOK_SECRET.encode("utf-8") - message_bytes = f"{timestamp}.{delivery_id}.".encode("utf-8") + body + message_bytes = f"{timestamp}.{delivery_id}.".encode() + body expected_signature = hmac.new(key_bytes, message_bytes, hashlib.sha256).hexdigest() return hmac.compare_digest(expected_signature, signature) @@ -43,7 +44,7 @@ def get_hex_signature(content): key_bytes = ARI_SIGNING_SECRET.encode("utf-8") try: message_bytes = content.encode("utf-8") - except Exception: + except AttributeError: message_bytes = content hmac_object = hmac.new(key_bytes, message_bytes, hashlib.sha256) @@ -51,11 +52,12 @@ def get_hex_signature(content): return hex_signature + def send_request(method: Literal["GET", "POST"], data=None, endpoint="", jsonify=True): if jsonify or data is None: data = json.dumps(data) - if method == 'POST': + if method == "POST": headers = { "X-Ari-Signature": get_hex_signature(data), "Content-Type": "application/json", @@ -63,9 +65,7 @@ def send_request(method: Literal["GET", "POST"], data=None, endpoint="", jsonify message_bytes = data.encode("utf-8") else: message_bytes = None - headers = { - "Authorization": f"Bearer {ARI_SIGNING_SECRET}" - } + headers = {"Authorization": f"Bearer {ARI_SIGNING_SECRET}"} req = requests.request( method, ARI_INGEST_ENDPOINT + endpoint, @@ -74,6 +74,7 @@ def send_request(method: Literal["GET", "POST"], data=None, endpoint="", jsonify ) return req + # external_id = "twisted-{project.id}" def send_ship(ship: ProjectShip): if settings.DEBUG_REVIEW: @@ -89,7 +90,7 @@ def send_ship(ship: ProjectShip): "email": ship.project.user.email, "name": ship.project.user.profile.slack_username, "slack_id": ship.project.user.profile.slack_id, - "program_hours": untracked_time/60 + "program_hours": untracked_time / 60, } title = ship.project.project_name @@ -136,20 +137,21 @@ def send_ship(ship: ProjectShip): "shipped_at": shipped_at, "thumbnail_url": thumbnail_url, "hackatime_projects": hackatime_projects, - "evidence": ['commits', 'elapsed', 'devlog'], + "evidence": ["commits", "elapsed", "devlog"], "journals": journals, - "meta": meta + "meta": meta, }, ) - resp = r.content r.raise_for_status() + def get_project_status(project: Project): - r = send_request('GET', endpoint=f"/status?external_id=twisted-{project.id}") + r = send_request("GET", endpoint=f"/status?external_id=twisted-{project.id}") _resp = r.content r.raise_for_status() return r.json() + # ARI's phases go: (processing | fraud_review | review | under_review) -- reviewer # hasn't decided yet -- then second_pass -- reviewer decided, an organizer still has # to confirm it -- then reviewed, where `decision` is locked in. withdrawn/reverted diff --git a/twisted/twisted_site/hackatime.py b/twisted/twisted_site/hackatime.py index 7fb491e..44b70b7 100644 --- a/twisted/twisted_site/hackatime.py +++ b/twisted/twisted_site/hackatime.py @@ -1,6 +1,7 @@ -from datetime import datetime, UTC -import requests from dataclasses import dataclass +from datetime import UTC, datetime + +import requests HACKATIME_ROOT_URL = "https://hackatime.hackclub.com" @@ -23,7 +24,10 @@ class HackatimeProject: languages: list[str] -def authhelper(access_token, headers={}): +def authhelper(access_token, headers=None): + if headers is None: + headers = {} + return {"Authorization": f"Bearer {access_token}", **headers} diff --git a/twisted/twisted_site/migrations/0001_initial.py b/twisted/twisted_site/migrations/0001_initial.py index d927c66..5ba0a49 100644 --- a/twisted/twisted_site/migrations/0001_initial.py +++ b/twisted/twisted_site/migrations/0001_initial.py @@ -6,7 +6,6 @@ class Migration(migrations.Migration): - initial = True dependencies = [ @@ -15,14 +14,38 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Profile', + name="Profile", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('verification_status', models.CharField(blank=True, default='', max_length=64)), - ('slack_id', models.CharField(blank=True, default='', max_length=64)), - ('slack_username', models.CharField(blank=True, default='', max_length=64)), - ('slack_pfp_url', models.CharField(blank=True, default='', max_length=200)), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='hackclub_profile', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "verification_status", + models.CharField(blank=True, default="", max_length=64), + ), + ("slack_id", models.CharField(blank=True, default="", max_length=64)), + ( + "slack_username", + models.CharField(blank=True, default="", max_length=64), + ), + ( + "slack_pfp_url", + models.CharField(blank=True, default="", max_length=200), + ), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="hackclub_profile", + to=settings.AUTH_USER_MODEL, + ), + ), ], ), ] diff --git a/twisted/twisted_site/migrations/0002_alter_profile_user.py b/twisted/twisted_site/migrations/0002_alter_profile_user.py index ace1184..d6eda1a 100644 --- a/twisted/twisted_site/migrations/0002_alter_profile_user.py +++ b/twisted/twisted_site/migrations/0002_alter_profile_user.py @@ -6,16 +6,19 @@ class Migration(migrations.Migration): - dependencies = [ - ('twisted_site', '0001_initial'), + ("twisted_site", "0001_initial"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.AlterField( - model_name='profile', - name='user', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL), + model_name="profile", + name="user", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="profile", + to=settings.AUTH_USER_MODEL, + ), ), ] diff --git a/twisted/twisted_site/migrations/0003_profile_hackatime_access_token_and_more.py b/twisted/twisted_site/migrations/0003_profile_hackatime_access_token_and_more.py index b933bfd..4a5bbd2 100644 --- a/twisted/twisted_site/migrations/0003_profile_hackatime_access_token_and_more.py +++ b/twisted/twisted_site/migrations/0003_profile_hackatime_access_token_and_more.py @@ -6,43 +6,70 @@ class Migration(migrations.Migration): - dependencies = [ - ('twisted_site', '0002_alter_profile_user'), + ("twisted_site", "0002_alter_profile_user"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.AddField( - model_name='profile', - name='hackatime_access_token', - field=models.CharField(blank=True, default='', max_length=2000), + model_name="profile", + name="hackatime_access_token", + field=models.CharField(blank=True, default="", max_length=2000), ), migrations.AddField( - model_name='profile', - name='hackatime_state', - field=models.CharField(blank=True, default='', max_length=100), + model_name="profile", + name="hackatime_state", + field=models.CharField(blank=True, default="", max_length=100), ), migrations.CreateModel( - name='Project', + name="Project", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('project_name', models.CharField(max_length=100)), - ('project_description', models.TextField(max_length=2000)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("project_name", models.CharField(max_length=100)), + ("project_description", models.TextField(max_length=2000)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), ], ), migrations.CreateModel( - name='Journal', + name="Journal", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('content', models.TextField(max_length=2000)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('minutes_worked', models.IntegerField()), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='twisted_site.project')), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("content", models.TextField(max_length=2000)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("minutes_worked", models.IntegerField()), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="twisted_site.project", + ), + ), ], ), ] diff --git a/twisted/twisted_site/migrations/0004_alter_journal_project_alter_project_user.py b/twisted/twisted_site/migrations/0004_alter_journal_project_alter_project_user.py index c56d02c..8cef060 100644 --- a/twisted/twisted_site/migrations/0004_alter_journal_project_alter_project_user.py +++ b/twisted/twisted_site/migrations/0004_alter_journal_project_alter_project_user.py @@ -6,21 +6,28 @@ class Migration(migrations.Migration): - dependencies = [ - ('twisted_site', '0003_profile_hackatime_access_token_and_more'), + ("twisted_site", "0003_profile_hackatime_access_token_and_more"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.AlterField( - model_name='journal', - name='project', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='journals', to='twisted_site.project'), + model_name="journal", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="journals", + to="twisted_site.project", + ), ), migrations.AlterField( - model_name='project', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='projects', to=settings.AUTH_USER_MODEL), + model_name="project", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="projects", + to=settings.AUTH_USER_MODEL, + ), ), ] diff --git a/twisted/twisted_site/migrations/0005_project_project_type.py b/twisted/twisted_site/migrations/0005_project_project_type.py index ad35707..935c3d4 100644 --- a/twisted/twisted_site/migrations/0005_project_project_type.py +++ b/twisted/twisted_site/migrations/0005_project_project_type.py @@ -4,16 +4,19 @@ class Migration(migrations.Migration): - dependencies = [ - ('twisted_site', '0004_alter_journal_project_alter_project_user'), + ("twisted_site", "0004_alter_journal_project_alter_project_user"), ] operations = [ migrations.AddField( - model_name='project', - name='project_type', - field=models.CharField(choices=[('software', 'Software'), ('hardware', 'Hardware')], default='', max_length=100), + model_name="project", + name="project_type", + field=models.CharField( + choices=[("software", "Software"), ("hardware", "Hardware")], + default="", + max_length=100, + ), preserve_default=False, ), ] diff --git a/twisted/twisted_site/migrations/0006_alter_project_project_name.py b/twisted/twisted_site/migrations/0006_alter_project_project_name.py index 0e32694..e6c60b6 100644 --- a/twisted/twisted_site/migrations/0006_alter_project_project_name.py +++ b/twisted/twisted_site/migrations/0006_alter_project_project_name.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('twisted_site', '0005_project_project_type'), + ("twisted_site", "0005_project_project_type"), ] operations = [ migrations.AlterField( - model_name='project', - name='project_name', + model_name="project", + name="project_name", field=models.CharField(max_length=50), ), ] diff --git a/twisted/twisted_site/migrations/0007_project_hackatime_project_name.py b/twisted/twisted_site/migrations/0007_project_hackatime_project_name.py index c0dbfa5..7ffe3a8 100644 --- a/twisted/twisted_site/migrations/0007_project_hackatime_project_name.py +++ b/twisted/twisted_site/migrations/0007_project_hackatime_project_name.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('twisted_site', '0006_alter_project_project_name'), + ("twisted_site", "0006_alter_project_project_name"), ] operations = [ migrations.AddField( - model_name='project', - name='hackatime_project_name', - field=models.CharField(blank=True, default='', max_length=200), + model_name="project", + name="hackatime_project_name", + field=models.CharField(blank=True, default="", max_length=200), ), ] diff --git a/twisted/twisted_site/migrations/0008_journal_reduced_minutes_alter_journal_content.py b/twisted/twisted_site/migrations/0008_journal_reduced_minutes_alter_journal_content.py index 1c31100..2a978f2 100644 --- a/twisted/twisted_site/migrations/0008_journal_reduced_minutes_alter_journal_content.py +++ b/twisted/twisted_site/migrations/0008_journal_reduced_minutes_alter_journal_content.py @@ -4,21 +4,20 @@ class Migration(migrations.Migration): - dependencies = [ - ('twisted_site', '0007_project_hackatime_project_name'), + ("twisted_site", "0007_project_hackatime_project_name"), ] operations = [ migrations.AddField( - model_name='journal', - name='reduced_minutes', + model_name="journal", + name="reduced_minutes", field=models.IntegerField(default=0), preserve_default=False, ), migrations.AlterField( - model_name='journal', - name='content', + model_name="journal", + name="content", field=models.TextField(), ), ] diff --git a/twisted/twisted_site/migrations/0009_uploadedimage.py b/twisted/twisted_site/migrations/0009_uploadedimage.py index 7a1e1d1..99a60ab 100644 --- a/twisted/twisted_site/migrations/0009_uploadedimage.py +++ b/twisted/twisted_site/migrations/0009_uploadedimage.py @@ -6,22 +6,35 @@ class Migration(migrations.Migration): - dependencies = [ - ('twisted_site', '0008_journal_reduced_minutes_alter_journal_content'), + ("twisted_site", "0008_journal_reduced_minutes_alter_journal_content"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name='UploadedImage', + name="UploadedImage", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('link', models.CharField(max_length=500)), - ('cdn_response', models.JSONField()), - ('uploaded_thru', models.CharField(max_length=500)), - ('filesize', models.IntegerField()), - ('uploaded_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("link", models.CharField(max_length=500)), + ("cdn_response", models.JSONField()), + ("uploaded_thru", models.CharField(max_length=500)), + ("filesize", models.IntegerField()), + ( + "uploaded_by", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), ], ), ] diff --git a/twisted/twisted_site/migrations/0010_rename_uploadedimage_uploadedfile.py b/twisted/twisted_site/migrations/0010_rename_uploadedimage_uploadedfile.py index d5a2236..f399628 100644 --- a/twisted/twisted_site/migrations/0010_rename_uploadedimage_uploadedfile.py +++ b/twisted/twisted_site/migrations/0010_rename_uploadedimage_uploadedfile.py @@ -5,15 +5,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('twisted_site', '0009_uploadedimage'), + ("twisted_site", "0009_uploadedimage"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.RenameModel( - old_name='UploadedImage', - new_name='UploadedFile', + old_name="UploadedImage", + new_name="UploadedFile", ), ] diff --git a/twisted/twisted_site/migrations/0011_journal_type.py b/twisted/twisted_site/migrations/0011_journal_type.py index b99b43d..2aebb4f 100644 --- a/twisted/twisted_site/migrations/0011_journal_type.py +++ b/twisted/twisted_site/migrations/0011_journal_type.py @@ -4,16 +4,23 @@ class Migration(migrations.Migration): - dependencies = [ - ('twisted_site', '0010_rename_uploadedimage_uploadedfile'), + ("twisted_site", "0010_rename_uploadedimage_uploadedfile"), ] operations = [ migrations.AddField( - model_name='journal', - name='type', - field=models.CharField(choices=[('hackatime', 'Hackatime'), ('lookout', 'Lookout'), ('untracked', 'Untracked')], default='hackatime', max_length=100), + model_name="journal", + name="type", + field=models.CharField( + choices=[ + ("hackatime", "Hackatime"), + ("lookout", "Lookout"), + ("untracked", "Untracked"), + ], + default="hackatime", + max_length=100, + ), preserve_default=False, ), ] diff --git a/twisted/twisted_site/migrations/0012_project_repo_url.py b/twisted/twisted_site/migrations/0012_project_repo_url.py index 511ccec..41d3044 100644 --- a/twisted/twisted_site/migrations/0012_project_repo_url.py +++ b/twisted/twisted_site/migrations/0012_project_repo_url.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('twisted_site', '0011_journal_type'), + ("twisted_site", "0011_journal_type"), ] operations = [ migrations.AddField( - model_name='project', - name='repo_url', - field=models.CharField(blank=True, default='', max_length=200), + model_name="project", + name="repo_url", + field=models.CharField(blank=True, default="", max_length=200), ), ] diff --git a/twisted/twisted_site/migrations/0013_projectship.py b/twisted/twisted_site/migrations/0013_projectship.py index b799efc..f88b200 100644 --- a/twisted/twisted_site/migrations/0013_projectship.py +++ b/twisted/twisted_site/migrations/0013_projectship.py @@ -5,24 +5,50 @@ class Migration(migrations.Migration): - dependencies = [ - ('twisted_site', '0012_project_repo_url'), + ("twisted_site", "0012_project_repo_url"), ] operations = [ migrations.CreateModel( - name='ProjectShip', + name="ProjectShip", fields=[ - ('id', models.BigAutoField(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)), - ('t1_updated_at', models.DateTimeField(default=None, null=True)), - ('t2_updated_at', models.DateTimeField(default=None, null=True)), - ('t1_message', models.TextField(blank=True, default='')), - ('t2_message', models.TextField(blank=True, default='')), - ('status', models.CharField(choices=[('created', 'Newly created'), ('rejected', 'Rejected ship'), ('reqchecked', 'Checked by T1'), ('approved', 'Approved by T2')], default='created', max_length=200)), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='ships', to='twisted_site.project')), + ( + "id", + models.BigAutoField( + 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)), + ("t1_updated_at", models.DateTimeField(default=None, null=True)), + ("t2_updated_at", models.DateTimeField(default=None, null=True)), + ("t1_message", models.TextField(blank=True, default="")), + ("t2_message", models.TextField(blank=True, default="")), + ( + "status", + models.CharField( + choices=[ + ("created", "Newly created"), + ("rejected", "Rejected ship"), + ("reqchecked", "Checked by T1"), + ("approved", "Approved by T2"), + ], + default="created", + max_length=200, + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="ships", + to="twisted_site.project", + ), + ), ], ), ] diff --git a/twisted/twisted_site/migrations/0014_profile_is_staff.py b/twisted/twisted_site/migrations/0014_profile_is_staff.py index cdf75cb..32041c5 100644 --- a/twisted/twisted_site/migrations/0014_profile_is_staff.py +++ b/twisted/twisted_site/migrations/0014_profile_is_staff.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('twisted_site', '0013_projectship'), + ("twisted_site", "0013_projectship"), ] operations = [ migrations.AddField( - model_name='profile', - name='is_staff', + model_name="profile", + name="is_staff", field=models.BooleanField(default=False), ), ] diff --git a/twisted/twisted_site/migrations/0015_profile_dark_theme.py b/twisted/twisted_site/migrations/0015_profile_dark_theme.py index dce9602..bff2cfa 100644 --- a/twisted/twisted_site/migrations/0015_profile_dark_theme.py +++ b/twisted/twisted_site/migrations/0015_profile_dark_theme.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('twisted_site', '0014_profile_is_staff'), + ("twisted_site", "0014_profile_is_staff"), ] operations = [ migrations.AddField( - model_name='profile', - name='dark_theme', + model_name="profile", + name="dark_theme", field=models.BooleanField(default=True), ), ] diff --git a/twisted/twisted_site/migrations/0016_profile_ysws_eligible.py b/twisted/twisted_site/migrations/0016_profile_ysws_eligible.py index 9342104..97cf527 100644 --- a/twisted/twisted_site/migrations/0016_profile_ysws_eligible.py +++ b/twisted/twisted_site/migrations/0016_profile_ysws_eligible.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('twisted_site', '0015_profile_dark_theme'), + ("twisted_site", "0015_profile_dark_theme"), ] operations = [ migrations.AddField( - model_name='profile', - name='ysws_eligible', + model_name="profile", + name="ysws_eligible", field=models.BooleanField(default=False), ), ] diff --git a/twisted/twisted_site/migrations/0017_remove_profile_dark_theme_profile_is_allowed_and_more.py b/twisted/twisted_site/migrations/0017_remove_profile_dark_theme_profile_is_allowed_and_more.py index 950fa21..46b24c8 100644 --- a/twisted/twisted_site/migrations/0017_remove_profile_dark_theme_profile_is_allowed_and_more.py +++ b/twisted/twisted_site/migrations/0017_remove_profile_dark_theme_profile_is_allowed_and_more.py @@ -4,24 +4,33 @@ class Migration(migrations.Migration): - dependencies = [ - ('twisted_site', '0016_profile_ysws_eligible'), + ("twisted_site", "0016_profile_ysws_eligible"), ] operations = [ migrations.RemoveField( - model_name='profile', - name='dark_theme', + model_name="profile", + name="dark_theme", ), migrations.AddField( - model_name='profile', - name='is_allowed', + model_name="profile", + name="is_allowed", field=models.BooleanField(default=False), ), migrations.AlterField( - model_name='projectship', - name='status', - field=models.CharField(choices=[('created', 'Awaiting review'), ('rejected', 'Rejected ship'), ('requested_changes', 'Requested Changes'), ('reqchecked', 'Checked by T1'), ('approved', 'Approved by T2')], default='created', max_length=200), + model_name="projectship", + name="status", + field=models.CharField( + choices=[ + ("created", "Awaiting review"), + ("rejected", "Rejected ship"), + ("requested_changes", "Requested Changes"), + ("reqchecked", "Checked by T1"), + ("approved", "Approved by T2"), + ], + default="created", + max_length=200, + ), ), ] diff --git a/twisted/twisted_site/migrations/0018_pathway.py b/twisted/twisted_site/migrations/0018_pathway.py index 84d114a..cc3eee9 100644 --- a/twisted/twisted_site/migrations/0018_pathway.py +++ b/twisted/twisted_site/migrations/0018_pathway.py @@ -4,22 +4,29 @@ class Migration(migrations.Migration): - dependencies = [ - ('twisted_site', '0017_remove_profile_dark_theme_profile_is_allowed_and_more'), + ("twisted_site", "0017_remove_profile_dark_theme_profile_is_allowed_and_more"), ] operations = [ migrations.CreateModel( - name='Pathway', + name="Pathway", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('start', models.DateTimeField()), - ('end', models.DateTimeField()), - ('name', models.CharField(max_length=200)), - ('min_mins', models.IntegerField(default=300)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("start", models.DateTimeField()), + ("end", models.DateTimeField()), + ("name", models.CharField(max_length=200)), + ("min_mins", models.IntegerField(default=300)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), ], ), ] diff --git a/twisted/twisted_site/migrations/0019_remove_projectship_status_projectship_final_message_and_more.py b/twisted/twisted_site/migrations/0019_remove_projectship_status_projectship_final_message_and_more.py index d8c0efb..2facb58 100644 --- a/twisted/twisted_site/migrations/0019_remove_projectship_status_projectship_final_message_and_more.py +++ b/twisted/twisted_site/migrations/0019_remove_projectship_status_projectship_final_message_and_more.py @@ -4,54 +4,89 @@ class Migration(migrations.Migration): - dependencies = [ - ('twisted_site', '0018_pathway'), + ("twisted_site", "0018_pathway"), ] operations = [ migrations.RemoveField( - model_name='projectship', - name='status', + model_name="projectship", + name="status", ), migrations.AddField( - model_name='projectship', - name='final_message', - field=models.TextField(blank=True, default=''), + model_name="projectship", + name="final_message", + field=models.TextField(blank=True, default=""), ), migrations.AddField( - model_name='projectship', - name='final_status', - field=models.CharField(choices=[('pending', 'Awaiting review'), ('requested_changes', 'Changes Requested'), ('rejected', 'Rejected'), ('approved', 'Approved')], default='pending', max_length=200), + model_name="projectship", + name="final_status", + field=models.CharField( + choices=[ + ("pending", "Awaiting review"), + ("requested_changes", "Changes Requested"), + ("rejected", "Rejected"), + ("approved", "Approved"), + ], + default="pending", + max_length=200, + ), ), migrations.AddField( - model_name='projectship', - name='final_updated_at', + model_name="projectship", + name="final_updated_at", field=models.DateTimeField(default=None, null=True), ), migrations.AddField( - model_name='projectship', - name='fraud_message', - field=models.TextField(blank=True, default=''), + model_name="projectship", + name="fraud_message", + field=models.TextField(blank=True, default=""), ), migrations.AddField( - model_name='projectship', - name='fraud_status', - field=models.CharField(choices=[('pending', 'Awaiting review'), ('requested_changes', 'Changes Requested'), ('rejected', 'Rejected'), ('approved', 'Approved')], default='pending', max_length=200), + model_name="projectship", + name="fraud_status", + field=models.CharField( + choices=[ + ("pending", "Awaiting review"), + ("requested_changes", "Changes Requested"), + ("rejected", "Rejected"), + ("approved", "Approved"), + ], + default="pending", + max_length=200, + ), ), migrations.AddField( - model_name='projectship', - name='fraud_updated_at', + model_name="projectship", + name="fraud_updated_at", field=models.DateTimeField(default=None, null=True), ), migrations.AddField( - model_name='projectship', - name='t1_status', - field=models.CharField(choices=[('pending', 'Awaiting review'), ('requested_changes', 'Changes Requested'), ('rejected', 'Rejected'), ('approved', 'Approved')], default='pending', max_length=200), + model_name="projectship", + name="t1_status", + field=models.CharField( + choices=[ + ("pending", "Awaiting review"), + ("requested_changes", "Changes Requested"), + ("rejected", "Rejected"), + ("approved", "Approved"), + ], + default="pending", + max_length=200, + ), ), migrations.AddField( - model_name='projectship', - name='t2_status', - field=models.CharField(choices=[('pending', 'Awaiting review'), ('requested_changes', 'Changes Requested'), ('rejected', 'Rejected'), ('approved', 'Approved')], default='pending', max_length=200), + model_name="projectship", + name="t2_status", + field=models.CharField( + choices=[ + ("pending", "Awaiting review"), + ("requested_changes", "Changes Requested"), + ("rejected", "Rejected"), + ("approved", "Approved"), + ], + default="pending", + max_length=200, + ), ), ] diff --git a/twisted/twisted_site/migrations/0020_profile_twists.py b/twisted/twisted_site/migrations/0020_profile_twists.py index fd7fe54..28b6d9d 100644 --- a/twisted/twisted_site/migrations/0020_profile_twists.py +++ b/twisted/twisted_site/migrations/0020_profile_twists.py @@ -4,15 +4,17 @@ class Migration(migrations.Migration): - dependencies = [ - ('twisted_site', '0019_remove_projectship_status_projectship_final_message_and_more'), + ( + "twisted_site", + "0019_remove_projectship_status_projectship_final_message_and_more", + ), ] operations = [ migrations.AddField( - model_name='profile', - name='twists', + model_name="profile", + name="twists", field=models.IntegerField(default=0), ), ] diff --git a/twisted/twisted_site/migrations/0021_profile_my_referral_code_profile_referred_by.py b/twisted/twisted_site/migrations/0021_profile_my_referral_code_profile_referred_by.py index 83e9e3b..9523794 100644 --- a/twisted/twisted_site/migrations/0021_profile_my_referral_code_profile_referred_by.py +++ b/twisted/twisted_site/migrations/0021_profile_my_referral_code_profile_referred_by.py @@ -5,20 +5,24 @@ class Migration(migrations.Migration): - dependencies = [ - ('twisted_site', '0020_profile_twists'), + ("twisted_site", "0020_profile_twists"), ] operations = [ migrations.AddField( - model_name='profile', - name='my_referral_code', - field=models.CharField(blank=True, default='', max_length=200), + model_name="profile", + name="my_referral_code", + field=models.CharField(blank=True, default="", max_length=200), ), migrations.AddField( - model_name='profile', - name='referred_by', - field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='twisted_site.profile'), + model_name="profile", + name="referred_by", + field=models.ForeignKey( + default=None, + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="twisted_site.profile", + ), ), ] diff --git a/twisted/twisted_site/migrations/0022_alter_profile_referred_by.py b/twisted/twisted_site/migrations/0022_alter_profile_referred_by.py index b378f21..549292a 100644 --- a/twisted/twisted_site/migrations/0022_alter_profile_referred_by.py +++ b/twisted/twisted_site/migrations/0022_alter_profile_referred_by.py @@ -5,15 +5,20 @@ class Migration(migrations.Migration): - dependencies = [ - ('twisted_site', '0021_profile_my_referral_code_profile_referred_by'), + ("twisted_site", "0021_profile_my_referral_code_profile_referred_by"), ] operations = [ migrations.AlterField( - model_name='profile', - name='referred_by', - field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='referrals', to='twisted_site.profile'), + model_name="profile", + name="referred_by", + field=models.ForeignKey( + default=None, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="referrals", + to="twisted_site.profile", + ), ), ] diff --git a/twisted/twisted_site/migrations/0023_auditlog.py b/twisted/twisted_site/migrations/0023_auditlog.py index 5cf5e6c..1728c96 100644 --- a/twisted/twisted_site/migrations/0023_auditlog.py +++ b/twisted/twisted_site/migrations/0023_auditlog.py @@ -6,22 +6,36 @@ class Migration(migrations.Migration): - dependencies = [ - ('twisted_site', '0022_alter_profile_referred_by'), + ("twisted_site", "0022_alter_profile_referred_by"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name='AuditLog', + name="AuditLog", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('path', models.CharField(max_length=400)), - ('post', models.BooleanField()), - ('pii', models.BooleanField(default=False)), - ('additional_context', models.JSONField(default={})), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='audit_logs', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("path", models.CharField(max_length=400)), + ("post", models.BooleanField()), + ("pii", models.BooleanField(default=False)), + ("additional_context", models.JSONField(default={})), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="audit_logs", + to=settings.AUTH_USER_MODEL, + ), + ), ], ), ] diff --git a/twisted/twisted_site/migrations/0024_auditlog_timestamp.py b/twisted/twisted_site/migrations/0024_auditlog_timestamp.py index e21ce8c..a337043 100644 --- a/twisted/twisted_site/migrations/0024_auditlog_timestamp.py +++ b/twisted/twisted_site/migrations/0024_auditlog_timestamp.py @@ -5,16 +5,17 @@ class Migration(migrations.Migration): - dependencies = [ - ('twisted_site', '0023_auditlog'), + ("twisted_site", "0023_auditlog"), ] operations = [ migrations.AddField( - model_name='auditlog', - name='timestamp', - field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + model_name="auditlog", + name="timestamp", + field=models.DateTimeField( + auto_now_add=True, default=django.utils.timezone.now + ), preserve_default=False, ), ] diff --git a/twisted/twisted_site/migrations/0025_alter_auditlog_additional_context.py b/twisted/twisted_site/migrations/0025_alter_auditlog_additional_context.py index 4df221c..871d202 100644 --- a/twisted/twisted_site/migrations/0025_alter_auditlog_additional_context.py +++ b/twisted/twisted_site/migrations/0025_alter_auditlog_additional_context.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('twisted_site', '0024_auditlog_timestamp'), + ("twisted_site", "0024_auditlog_timestamp"), ] operations = [ migrations.AlterField( - model_name='auditlog', - name='additional_context', + model_name="auditlog", + name="additional_context", field=models.JSONField(default=None, null=True), ), ] diff --git a/twisted/twisted_site/migrations/0026_project_playable_url.py b/twisted/twisted_site/migrations/0026_project_playable_url.py index 32151ad..d34b493 100644 --- a/twisted/twisted_site/migrations/0026_project_playable_url.py +++ b/twisted/twisted_site/migrations/0026_project_playable_url.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('twisted_site', '0025_alter_auditlog_additional_context'), + ("twisted_site", "0025_alter_auditlog_additional_context"), ] operations = [ migrations.AddField( - model_name='project', - name='playable_url', - field=models.CharField(blank=True, default='', max_length=200), + model_name="project", + name="playable_url", + field=models.CharField(blank=True, default="", max_length=200), ), ] diff --git a/twisted/twisted_site/migrations/0027_profile_hca_access_token_project_screenshot_url.py b/twisted/twisted_site/migrations/0027_profile_hca_access_token_project_screenshot_url.py index d88875c..2768c59 100644 --- a/twisted/twisted_site/migrations/0027_profile_hca_access_token_project_screenshot_url.py +++ b/twisted/twisted_site/migrations/0027_profile_hca_access_token_project_screenshot_url.py @@ -4,20 +4,19 @@ class Migration(migrations.Migration): - dependencies = [ - ('twisted_site', '0026_project_playable_url'), + ("twisted_site", "0026_project_playable_url"), ] operations = [ migrations.AddField( - model_name='profile', - name='hca_access_token', - field=models.CharField(blank=True, default='', max_length=2000), + model_name="profile", + name="hca_access_token", + field=models.CharField(blank=True, default="", max_length=2000), ), migrations.AddField( - model_name='project', - name='screenshot_url', - field=models.CharField(blank=True, default='', max_length=500), + model_name="project", + name="screenshot_url", + field=models.CharField(blank=True, default="", max_length=500), ), ] diff --git a/twisted/twisted_site/migrations/0028_remove_projectship_final_message_and_more.py b/twisted/twisted_site/migrations/0028_remove_projectship_final_message_and_more.py index de3123b..28f5b74 100644 --- a/twisted/twisted_site/migrations/0028_remove_projectship_final_message_and_more.py +++ b/twisted/twisted_site/migrations/0028_remove_projectship_final_message_and_more.py @@ -4,83 +4,91 @@ class Migration(migrations.Migration): - dependencies = [ - ('twisted_site', '0027_profile_hca_access_token_project_screenshot_url'), + ("twisted_site", "0027_profile_hca_access_token_project_screenshot_url"), ] operations = [ migrations.RemoveField( - model_name='projectship', - name='final_message', + model_name="projectship", + name="final_message", ), migrations.RemoveField( - model_name='projectship', - name='final_status', + model_name="projectship", + name="final_status", ), migrations.RemoveField( - model_name='projectship', - name='final_updated_at', + model_name="projectship", + name="final_updated_at", ), migrations.RemoveField( - model_name='projectship', - name='fraud_message', + model_name="projectship", + name="fraud_message", ), migrations.RemoveField( - model_name='projectship', - name='fraud_status', + model_name="projectship", + name="fraud_status", ), migrations.RemoveField( - model_name='projectship', - name='fraud_updated_at', + model_name="projectship", + name="fraud_updated_at", ), migrations.RemoveField( - model_name='projectship', - name='t1_message', + model_name="projectship", + name="t1_message", ), migrations.RemoveField( - model_name='projectship', - name='t1_status', + model_name="projectship", + name="t1_status", ), migrations.RemoveField( - model_name='projectship', - name='t1_updated_at', + model_name="projectship", + name="t1_updated_at", ), migrations.RemoveField( - model_name='projectship', - name='t2_message', + model_name="projectship", + name="t2_message", ), migrations.RemoveField( - model_name='projectship', - name='t2_status', + model_name="projectship", + name="t2_status", ), migrations.RemoveField( - model_name='projectship', - name='t2_updated_at', + model_name="projectship", + name="t2_updated_at", ), migrations.AddField( - model_name='projectship', - name='audit_note', - field=models.TextField(blank=True, default=''), + model_name="projectship", + name="audit_note", + field=models.TextField(blank=True, default=""), ), migrations.AddField( - model_name='projectship', - name='deflation_reason', - field=models.CharField(blank=True, default='', max_length=255), + model_name="projectship", + name="deflation_reason", + field=models.CharField(blank=True, default="", max_length=255), ), migrations.AddField( - model_name='projectship', - name='note_to_maker', - field=models.TextField(blank=True, default=''), + model_name="projectship", + name="note_to_maker", + field=models.TextField(blank=True, default=""), ), migrations.AddField( - model_name='projectship', - name='status', - field=models.CharField(choices=[('pending', 'Awaiting review'), ('requested_changes', 'Changes Requested'), ('rejected', 'Rejected'), ('approved', 'Approved')], default='pending', max_length=200), + model_name="projectship", + name="status", + field=models.CharField( + choices=[ + ("pending", "Awaiting review"), + ("requested_changes", "Changes Requested"), + ("rejected", "Rejected"), + ("approved", "Approved"), + ], + default="pending", + max_length=200, + ), ), migrations.AddField( - model_name='projectship', - name='technical_features', - field=models.CharField(blank=True, default='', max_length=255), + model_name="projectship", + name="technical_features", + field=models.CharField(blank=True, default="", max_length=255), ), ] diff --git a/twisted/twisted_site/migrations/0029_projectship_final_audit_note_and_more.py b/twisted/twisted_site/migrations/0029_projectship_final_audit_note_and_more.py index d16c85c..6041ee2 100644 --- a/twisted/twisted_site/migrations/0029_projectship_final_audit_note_and_more.py +++ b/twisted/twisted_site/migrations/0029_projectship_final_audit_note_and_more.py @@ -4,25 +4,33 @@ class Migration(migrations.Migration): - dependencies = [ - ('twisted_site', '0028_remove_projectship_final_message_and_more'), + ("twisted_site", "0028_remove_projectship_final_message_and_more"), ] operations = [ migrations.AddField( - model_name='projectship', - name='final_audit_note', - field=models.TextField(blank=True, default=''), + model_name="projectship", + name="final_audit_note", + field=models.TextField(blank=True, default=""), ), migrations.AddField( - model_name='projectship', - name='final_note_to_maker', - field=models.TextField(blank=True, default=''), + model_name="projectship", + name="final_note_to_maker", + field=models.TextField(blank=True, default=""), ), migrations.AddField( - model_name='projectship', - name='final_status', - field=models.CharField(choices=[('pending', 'Awaiting review'), ('requested_changes', 'Changes Requested'), ('rejected', 'Rejected'), ('approved', 'Approved')], default='pending', max_length=200), + model_name="projectship", + name="final_status", + field=models.CharField( + choices=[ + ("pending", "Awaiting review"), + ("requested_changes", "Changes Requested"), + ("rejected", "Rejected"), + ("approved", "Approved"), + ], + default="pending", + max_length=200, + ), ), ] diff --git a/twisted/twisted_site/migrations/0030_alter_journal_minutes_worked_and_more.py b/twisted/twisted_site/migrations/0030_alter_journal_minutes_worked_and_more.py index 2405650..54e04b7 100644 --- a/twisted/twisted_site/migrations/0030_alter_journal_minutes_worked_and_more.py +++ b/twisted/twisted_site/migrations/0030_alter_journal_minutes_worked_and_more.py @@ -5,20 +5,23 @@ class Migration(migrations.Migration): - dependencies = [ - ('twisted_site', '0029_projectship_final_audit_note_and_more'), + ("twisted_site", "0029_projectship_final_audit_note_and_more"), ] operations = [ migrations.AlterField( - model_name='journal', - name='minutes_worked', - field=models.IntegerField(validators=[django.core.validators.MinValueValidator(0)]), + model_name="journal", + name="minutes_worked", + field=models.IntegerField( + validators=[django.core.validators.MinValueValidator(0)] + ), ), migrations.AlterField( - model_name='journal', - name='reduced_minutes', - field=models.IntegerField(validators=[django.core.validators.MinValueValidator(0)]), + model_name="journal", + name="reduced_minutes", + field=models.IntegerField( + validators=[django.core.validators.MinValueValidator(0)] + ), ), ] diff --git a/twisted/twisted_site/migrations/0031_profilestaffpermissions_profile_staff_permissions.py b/twisted/twisted_site/migrations/0031_profilestaffpermissions_profile_staff_permissions.py new file mode 100644 index 0000000..244d4fa --- /dev/null +++ b/twisted/twisted_site/migrations/0031_profilestaffpermissions_profile_staff_permissions.py @@ -0,0 +1,26 @@ +# Generated by Django 6.0.7 on 2026-09-08 15:55 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('twisted_site', '0030_alter_journal_minutes_worked_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='ProfileStaffPermissions', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('superuser', models.BooleanField(default=False)), + ], + ), + migrations.AddField( + model_name='profile', + name='staff_permissions', + field=models.OneToOneField(default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='twisted_site.profilestaffpermissions'), + ), + ] diff --git a/twisted/twisted_site/migrations/0032_profilestaffpermissions_manage_announcements_and_more.py b/twisted/twisted_site/migrations/0032_profilestaffpermissions_manage_announcements_and_more.py new file mode 100644 index 0000000..358899e --- /dev/null +++ b/twisted/twisted_site/migrations/0032_profilestaffpermissions_manage_announcements_and_more.py @@ -0,0 +1,58 @@ +# Generated by Django 6.0.7 on 2026-09-08 16:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('twisted_site', '0031_profilestaffpermissions_profile_staff_permissions'), + ] + + operations = [ + migrations.AddField( + model_name='profilestaffpermissions', + name='manage_announcements', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='profilestaffpermissions', + name='manage_fulfillments', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='profilestaffpermissions', + name='manage_pathways', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='profilestaffpermissions', + name='manage_review', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='profilestaffpermissions', + name='manage_shop', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='profilestaffpermissions', + name='view_auditlogs', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='profilestaffpermissions', + name='view_pathways', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='profilestaffpermissions', + name='view_review', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='profilestaffpermissions', + name='view_users', + field=models.BooleanField(default=False), + ), + ] diff --git a/twisted/twisted_site/models.py b/twisted/twisted_site/models.py index 1941cb7..85a3816 100644 --- a/twisted/twisted_site/models.py +++ b/twisted/twisted_site/models.py @@ -1,20 +1,13 @@ -from django.db.models import TextField from django.contrib.auth import get_user_model from django.core.validators import MinValueValidator from django.db import models +from django.db.models import TextField from django.utils import timezone + from . import hackatime -from .slack import slack_bot User = get_user_model() -JOURNAL_TYPES = { - "hackatime": "Hackatime", - "lookout": "Lookout", - "untracked": "Untracked", -} - - class UploadedFile(models.Model): uploaded_by = models.ForeignKey(User, on_delete=models.PROTECT) link = models.CharField(max_length=500) @@ -40,12 +33,19 @@ class Profile(models.Model): hca_access_token = models.CharField(max_length=2000, blank=True, default="") - is_staff = models.BooleanField(default=False) is_allowed = models.BooleanField(default=False) + is_staff = models.BooleanField(default=False) + staff_permissions = models.OneToOneField('twisted_site.ProfileStaffPermissions', on_delete=models.PROTECT, default=None, null=True) twists = models.IntegerField(default=0) - - referred_by = models.ForeignKey('twisted_site.Profile', on_delete=models.PROTECT, null=True, default=None, related_name="referrals") + + referred_by = models.ForeignKey( + "twisted_site.Profile", + on_delete=models.PROTECT, + null=True, + default=None, + related_name="referrals", + ) my_referral_code = models.CharField(max_length=200, blank=True, default="") def shipped_projects(self): @@ -71,6 +71,26 @@ def __str__(self): return self.user.username # ty:ignore[unresolved-attribute] +class ProfileStaffPermissions(models.Model): + superuser = models.BooleanField(default=False) + + view_users = models.BooleanField(default=False) + + view_pathways = models.BooleanField(default=False) + manage_pathways = models.BooleanField(default=False) + + manage_fulfillments = models.BooleanField(default=False) + + manage_shop = models.BooleanField(default=False) + + view_review = models.BooleanField(default=False) + manage_review = models.BooleanField(default=False) + + manage_announcements = models.BooleanField(default=False) + + view_auditlogs = models.BooleanField(default=False) + + PROJECT_TYPE_CHOICES = {"software": "Software", "hardware": "Hardware"} @@ -134,21 +154,26 @@ def hackatime_time_unjournaled(self): return self.time_spent() - self.hackatime_logged(include_all_minutes=True) def latest_ship(self): - ship = self.ships.order_by('-created_at').first() + ship = self.ships.order_by("-created_at").first() return ship - + def is_shipped(self): latest_ship = self.latest_ship() if latest_ship is None: return False - return latest_ship.status != 'requested_changes' + return latest_ship.status != "requested_changes" def is_approved(self): latest_ship = self.latest_ship() if latest_ship is None: return False - return latest_ship.status == 'approved' + return latest_ship.status == "approved" +JOURNAL_TYPES = { + "hackatime": "Hackatime", + "lookout": "Lookout", + "untracked": "Untracked", +} class Journal(models.Model): project = models.ForeignKey( @@ -182,14 +207,18 @@ class ProjectShip(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) - status = models.CharField(default="pending", choices=PROJECT_SHIP_STATUSES, max_length=200) + status = models.CharField( + default="pending", choices=PROJECT_SHIP_STATUSES, max_length=200 + ) note_to_maker = models.TextField(blank=True, default="") audit_note = models.TextField(blank=True, default="") technical_features = models.CharField(blank=True, default="", max_length=255) deflation_reason = models.CharField(blank=True, default="", max_length=255) - final_status = models.CharField(default="pending", choices=PROJECT_SHIP_STATUSES, max_length=200) + final_status = models.CharField( + default="pending", choices=PROJECT_SHIP_STATUSES, max_length=200 + ) final_note_to_maker = models.TextField(blank=True, default="") final_audit_note = models.TextField(blank=True, default="") @@ -218,36 +247,40 @@ def in_progress(self): def status(self): if self.ended(): - return 'ended' + return "ended" if self.didnt_start(): - return 'awaiting' + return "awaiting" if self.in_progress(): - return 'in progress' - + return "in progress" + def mins_spent(self, user: User): - pathways = Pathway.objects.order_by('start').values('id', 'start', 'end', 'min_mins') + pathways = Pathway.objects.order_by("start").values( + "id", "start", "end", "min_mins" + ) if not pathways: return 0 - - pathway_totals = {p['id']: 0 for p in pathways} - journals = Journal.objects.filter( - project__user=user - ).order_by('created_at').values_list('created_at', 'reduced_minutes') + pathway_totals = {p["id"]: 0 for p in pathways} + + journals = ( + Journal.objects.filter(project__user=user) + .order_by("created_at") + .values_list("created_at", "reduced_minutes") + ) for j_created, j_mins in journals: mins_remaining = j_mins for pathway in pathways: if mins_remaining <= 0: break - + # Check if journal falls within the pathway window - if pathway['start'] > j_created or pathway['end'] < j_created: + if pathway["start"] > j_created or pathway["end"] < j_created: continue - - p_id = pathway['id'] + + p_id = pathway["id"] mins_completed = pathway_totals.get(p_id, 0) - mins_required = pathway['min_mins'] + mins_required = pathway["min_mins"] if mins_completed >= mins_required: continue @@ -259,7 +292,7 @@ def mins_spent(self, user: User): pathway_totals[p_id] = mins_completed + mins_donated return pathway_totals[self.id] - + def mins_spent_per_participant(self) -> dict[int, int]: """ Calculates the minutes spent on this specific pathway for all participants. @@ -268,24 +301,28 @@ def mins_spent_per_participant(self) -> dict[int, int]: dict: {user_id: mins_spent} """ # Fetch all pathways to accurately model the sequential time donation - pathways = list(Pathway.objects.order_by('start').values('id', 'start', 'end', 'min_mins')) + pathways = list( + Pathway.objects.order_by("start").values("id", "start", "end", "min_mins") + ) if not pathways: return {} # Fetch journals from all users that fit within this pathway's active time frame - journals = Journal.objects.filter( - created_at__gte=self.start, - created_at__lte=self.end, - reduced_minutes__gt=0 - ).order_by('project__user_id', 'created_at').values_list( - 'project__user_id', 'created_at', 'reduced_minutes' + journals = ( + Journal.objects.filter( + created_at__gte=self.start, + created_at__lte=self.end, + reduced_minutes__gt=0, + ) + .order_by("project__user_id", "created_at") + .values_list("project__user_id", "created_at", "reduced_minutes") ) user_pathway_totals = {} for user_id, j_created, j_mins in journals: if user_id not in user_pathway_totals: - user_pathway_totals[user_id] = {p['id']: 0 for p in pathways} + user_pathway_totals[user_id] = {p["id"]: 0 for p in pathways} pathway_totals = user_pathway_totals[user_id] mins_remaining = j_mins @@ -294,12 +331,12 @@ def mins_spent_per_participant(self) -> dict[int, int]: if mins_remaining <= 0: break - if pathway['start'] > j_created or pathway['end'] < j_created: + if pathway["start"] > j_created or pathway["end"] < j_created: continue - p_id = pathway['id'] + p_id = pathway["id"] mins_completed = pathway_totals[p_id] - mins_required = pathway['min_mins'] + mins_required = pathway["min_mins"] if mins_completed >= mins_required: continue @@ -315,7 +352,7 @@ def mins_spent_per_participant(self) -> dict[int, int]: user_id: totals.get(self.id, 0) for user_id, totals in user_pathway_totals.items() } - + def qualified_participants(self): per_part = self.mins_spent_per_participant() qualified = [] @@ -323,18 +360,19 @@ def qualified_participants(self): if mins >= self.min_mins: qualified.append(User.objects.get(id=userid)) return qualified - + def __str__(self): return self.name + class AuditLog(models.Model): timestamp = models.DateTimeField(auto_now_add=True) - user = models.ForeignKey(User, on_delete=models.PROTECT, related_name='audit_logs') + user = models.ForeignKey(User, on_delete=models.PROTECT, related_name="audit_logs") path = models.CharField(max_length=400) post = models.BooleanField() pii = models.BooleanField(default=False) additional_context = models.JSONField(null=True, default=None) - + def __str__(self): return f"Audit log for {self.user.profile.slack_username}. PII: {self.pii}" diff --git a/twisted/twisted_site/slack.py b/twisted/twisted_site/slack.py index 4238e0d..15ad87b 100644 --- a/twisted/twisted_site/slack.py +++ b/twisted/twisted_site/slack.py @@ -1,190 +1,15 @@ -import os from typing import Any +from django.conf import settings from slack_sdk import WebClient +SLACK_TOKEN = settings.SLACK_TOKEN +SLACK_LOG_CHANNEL = settings.SLACK_LOG_CHANNEL -class SlackBot: - def __init__( - self, - token: str | None = None, - cc_group_id: str | None = None, - ): - self.token = token or os.getenv("SLACK_TOKEN") +slack_bot = WebClient(token=SLACK_TOKEN) - if not self.token: - raise ValueError("SLACK_TOKEN must be set") - - self.client = WebClient(token=self.token) - self._cc_group_id = cc_group_id or os.getenv("SLACK_CC_GROUP_ID") - - def post_message( - self, - *, - channel: str, - text: str, - blocks: list[dict[str, Any]] | None = None, - **kwargs: Any, - ): - payload = { - "channel": channel, - "text": text, - **kwargs, - } - - if blocks is not None: - payload["blocks"] = blocks - - return self.client.chat_postMessage(**payload) - - def send_message( - self, - *, - channel: str, - text: str, - **kwargs: Any, - ): - return self.post_message( - channel=channel, - text=text, - **kwargs, - ) - - def send_blocks( - self, - *, - channel: str, - blocks: list[dict[str, Any]], - text: str = " ", - **kwargs: Any, - ): - return self.post_message( - channel=channel, - text=text, - blocks=blocks, - **kwargs, - ) - - def dm_user( - self, - *, - user: str, - text: str, - blocks: list[dict[str, Any]] | None = None, - **kwargs: Any, - ): - """ - Send a DM to a Slack user using their U... user ID. - - No D... DM channel ID is required. - """ - return self.post_message( - channel=user, - text=text, - blocks=blocks, - **kwargs, - ) - - def dm_user_blocks( - self, - *, - user: str, - blocks: list[dict[str, Any]], - text: str = " ", - **kwargs: Any, - ): - """Send Block Kit directly to a user using their U... ID.""" - return self.dm_user( - user=user, - text=text, - blocks=blocks, - **kwargs, - ) - - def users_info(self, *, user: str): - return self.client.users_info(user=user) - - def get_user_profile(self, user: str) -> dict[str, Any]: - response = self.client.users_info(user=user) - - user_data = response["user"] - profile = user_data.get("profile", {}) - - return { - "id": user_data["id"], - "name": user_data.get("name"), - "real_name": ( - user_data.get("real_name") - or profile.get("real_name") - ), - "display_name": ( - profile.get("display_name") - or user_data.get("name") - ), - "image_24": profile.get("image_24"), - "image_32": profile.get("image_32"), - "image_48": profile.get("image_48"), - "image_72": profile.get("image_72"), - "image_192": profile.get("image_192"), - "image_512": profile.get("image_512"), - } - - def error_log( - self, - *, - channel: str, - error: str, - title: str = "Error Log", - **kwargs: Any, - ): - group_id = self._cc_group_id - mention = f"<@{group_id}>" if group_id else "" - - parent = self.client.chat_postMessage( - channel=channel, - text=title, - blocks=[ - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": f"*{title}*", - }, - } - ], - **kwargs, - ) - - # Break up any backticks in the error text so it can't close the code - # fence early and have the remainder render as live Slack mrkdwn. - safe_error = error.replace("`", "`") - thread_text = f"```{safe_error}```" - - if mention: - thread_text += f"\n\nCC: {mention}" - - return self.client.chat_postMessage( - channel=channel, - thread_ts=parent["ts"], - text=thread_text, - blocks=[ - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": thread_text, - }, - } - ], - ) - - -slack_token = os.getenv("SLACK_TOKEN") -cc_group_id = os.getenv("SLACK_CC_GROUP_ID") - -slack_bot = SlackBot( - token=slack_token, - cc_group_id=cc_group_id, -) - -slack_client = slack_bot.client \ No newline at end of file +def log_to_channel(message): + if settings.DEBUG: + slack_bot.chat_postMessage(channel=SLACK_LOG_CHANNEL, text=message, username="[DEBUG]") + else: + slack_bot.chat_postMessage(channel=SLACK_LOG_CHANNEL, text=message) \ No newline at end of file diff --git a/twisted/twisted_site/static/icons/gift.png b/twisted/twisted_site/static/icons/gift.png new file mode 100644 index 0000000..c2e3334 Binary files /dev/null and b/twisted/twisted_site/static/icons/gift.png differ diff --git a/twisted/twisted_site/static/icons/pathways.png b/twisted/twisted_site/static/icons/pathways.png new file mode 100644 index 0000000..df51426 Binary files /dev/null and b/twisted/twisted_site/static/icons/pathways.png differ diff --git a/twisted/twisted_site/templates/admin/user.html b/twisted/twisted_site/templates/admin/user.html index d270d5d..d39fe08 100644 --- a/twisted/twisted_site/templates/admin/user.html +++ b/twisted/twisted_site/templates/admin/user.html @@ -14,7 +14,7 @@
+ This user hasn't referred anyone yet! +
+