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 @@

{% if user.profile %}
{% if user.profile.is_staff %} - Staff + {{ user.profile.staff_permissions.superuser|yesno:"Admin,Staff" }} {% endif %} {% if login_maybe %} {{ user.profile.is_allowed|yesno:"Login allowed,Login disallowed" }} @@ -121,6 +121,47 @@

{% endif %}

+
+

Adminproceed with caution!

+ {% if user.profile.referrals.all %} + + + + + Permission + Value + Override Value + Save + + + + + + {% with user.profile.staff_permissions.superuser as val %} + Superuser + + + {{ val }} + + + + + {% endwith %} + + + + + {% else %} + +

+ No referrals found :( +

+

+ This user hasn't referred anyone yet! +

+
+ {% endif %} +
{% if login_maybe %}
diff --git a/twisted/twisted_site/templates/client/dashboard.html b/twisted/twisted_site/templates/client/dashboard.html index 8dbf98e..01daecd 100644 --- a/twisted/twisted_site/templates/client/dashboard.html +++ b/twisted/twisted_site/templates/client/dashboard.html @@ -21,20 +21,26 @@ Projects
-
🛣️
+ onclick="newWindow('Pathways', this.dataset.url)"> +
+ + + +
Pathways
-
👥
+
+ +
Referrals
-
+ {% comment %}
🧭
Discover -
+
{% endcomment %}
@@ -570,6 +576,17 @@ } } drawSpectrum(); + + setTimeout(() => { + console.log( + '%cConsole?', + 'color: #B7AFBC; background: #160D1E; font-size: 1.5rem; padding: 0.15rem 0.25rem; margin: 1rem; border: 2px solid #A79FAC;' + ); + console.log('WELCOME TO TWISTED!!!') + console.log('shh....') + + console.log('{{ request.scheme }}://{{ request.get_host }}{% url 'dashboard' %}?discover') + }, 500) {% if startup_windows %} diff --git a/twisted/twisted_site/templates/cotton/project_ship_mark.html b/twisted/twisted_site/templates/cotton/project_ship_mark.html index d32f965..533f23f 100644 --- a/twisted/twisted_site/templates/cotton/project_ship_mark.html +++ b/twisted/twisted_site/templates/cotton/project_ship_mark.html @@ -9,7 +9,9 @@ {% elif status == 'unavailable' %} -
?
+
+ ? +
{% else %}
No status found!
{% endif %} diff --git a/twisted/twisted_site/templatetags/maths.py b/twisted/twisted_site/templatetags/maths.py index b0b6fba..38cd856 100644 --- a/twisted/twisted_site/templatetags/maths.py +++ b/twisted/twisted_site/templatetags/maths.py @@ -1,6 +1,8 @@ from django import template + register = template.Library() + @register.filter def divide(x, y): - return x/y + return x / y diff --git a/twisted/twisted_site/templatetags/time_filters.py b/twisted/twisted_site/templatetags/time_filters.py index c164d85..cd5c846 100644 --- a/twisted/twisted_site/templatetags/time_filters.py +++ b/twisted/twisted_site/templatetags/time_filters.py @@ -1,24 +1,28 @@ -from django import template from datetime import timedelta + +from django import template + register = template.Library() + @register.filter def minutes_to_hours_minutes(minutes): try: total_minutes = int(minutes) except (ValueError, TypeError): return minutes # Return original value if it's not a valid integer - + hours = total_minutes // 60 remaining_minutes = total_minutes % 60 - + if hours > 0: if remaining_minutes == 0: return f"{hours}h" return f"{hours}h {remaining_minutes}m" return f"{int(minutes)}m" + @register.filter -def rounddelta(delta:timedelta, to=1): +def rounddelta(delta: timedelta, to=1): delta = timedelta(seconds=round(delta.total_seconds(), to)) - return delta \ No newline at end of file + return delta diff --git a/twisted/twisted_site/tests.py b/twisted/twisted_site/tests.py index 7ce503c..4929020 100644 --- a/twisted/twisted_site/tests.py +++ b/twisted/twisted_site/tests.py @@ -1,3 +1,2 @@ -from django.test import TestCase # Create your tests here. diff --git a/twisted/twisted_site/urls.py b/twisted/twisted_site/urls.py index 19ea57c..0bb238a 100644 --- a/twisted/twisted_site/urls.py +++ b/twisted/twisted_site/urls.py @@ -1,45 +1,105 @@ -from .views.client.auth import LoginView, AuthCallbackView, HackatimeCallbackView, LogoutView from django.urls import path -from .views import client -from .views import admin -from .views.image_upload import upload_file + +from .views import admin, client from .views.ari import AriView +from .views.client.auth import ( + AuthCallbackView, + HackatimeCallbackView, + LoginView, + LogoutView, +) +from .views.image_upload import upload_file urlpatterns = [ - path('', view=client.HomepageView.as_view(), name="homepage"), - path('faqs/', view=client.FaqsView.as_view(), name="faqs"), - + path("", view=client.HomepageView.as_view(), name="homepage"), + path("faqs/", view=client.FaqsView.as_view(), name="faqs"), path("api/upload_image/", upload_file, name="misc.upload_file"), path("api/ari/", AriView.as_view(), name="ari"), - path("auth/login/", LoginView.as_view(), name="login"), path("oauth/callback/", AuthCallbackView.as_view(), name="auth_callback"), - path("oauth/hackatime_callback/", HackatimeCallbackView.as_view(), name="auth_callback"), + path( + "oauth/hackatime_callback/", + HackatimeCallbackView.as_view(), + name="auth_callback", + ), path("auth/logout/", LogoutView.as_view(), name="logout"), - path("dashboard/", client.DashboardView.as_view(), name="dashboard"), - path("dashboard/frame/projects/", client.ListProjects.as_view(), name="fr.projects"), - path("dashboard/frame/projects/create/", client.CreateProject.as_view(), name="fr.projects.create"), - - path("dashboard/frame/projects//", client.ProjectDetail.as_view(), name="fr.projects.detail"), - path("dashboard/frame/projects/ship//", client.SubmitProject.as_view(), name="fr.projects.ship"), - path("dashboard/frame/projects//settings/", client.ProjectSettings.as_view(), name="fr.projects.settings"), - path("dashboard/frame/projects//journals/new/hackatime/", client.NewProjectHackatimeJournal.as_view(), name="fr.projects.journals.new.hackatime"), - path("dashboard/frame/projects//journals/new/untracked/", client.NewProjectUntrackedJournal.as_view(), name="fr.projects.journals.new.untracked"), - path("dashboard/frame/journals/delete//", client.DeleteJournal.as_view(), name="fr.projects.journals.delete"), - path("dashboard/frame/pathways/", client.PathwaysView.as_view(), name="fr.pathways"), - path("dashboard/frame/referrals/", client.ReferralsView.as_view(), name="fr.referrals"), - path("dashboard/frame/discover/", client.DiscoverView.as_view(), name="fr.discover"), - + path( + "dashboard/frame/projects/", client.ListProjects.as_view(), name="fr.projects" + ), + path( + "dashboard/frame/projects/create/", + client.CreateProject.as_view(), + name="fr.projects.create", + ), + path( + "dashboard/frame/projects//", + client.ProjectDetail.as_view(), + name="fr.projects.detail", + ), + path( + "dashboard/frame/projects/ship//", + client.SubmitProject.as_view(), + name="fr.projects.ship", + ), + path( + "dashboard/frame/projects//settings/", + client.ProjectSettings.as_view(), + name="fr.projects.settings", + ), + path( + "dashboard/frame/projects//journals/new/hackatime/", + client.NewProjectHackatimeJournal.as_view(), + name="fr.projects.journals.new.hackatime", + ), + path( + "dashboard/frame/projects//journals/new/untracked/", + client.NewProjectUntrackedJournal.as_view(), + name="fr.projects.journals.new.untracked", + ), + path( + "dashboard/frame/journals/delete//", + client.DeleteJournal.as_view(), + name="fr.projects.journals.delete", + ), + path( + "dashboard/frame/pathways/", client.PathwaysView.as_view(), name="fr.pathways" + ), + path( + "dashboard/frame/referrals/", + client.ReferralsView.as_view(), + name="fr.referrals", + ), + path( + "dashboard/frame/discover/", client.DiscoverView.as_view(), name="fr.discover" + ), path("admin/", admin.DashboardView.as_view(), name="admin.dash"), path("admin/users/", admin.UsersView.as_view(), name="admin.users"), - path("admin/users//", admin.UserDetailView.as_view(), name="admin.users.detail"), + path( + "admin/users//", + admin.UserDetailView.as_view(), + name="admin.users.detail", + ), path("admin/pathways/", admin.PathwayListView.as_view(), name="admin.pathways"), - path("admin/pathways/", admin.PathwayDetailView.as_view(), name="admin.pathways.detail"), - path("admin/pathways/new/", admin.PathwayCreateView.as_view(), name="admin.pathways.create"), - path("admin/fulfillment/", admin.FulfillmentView.as_view(), name="admin.fulfillment"), + path( + "admin/pathways/", + admin.PathwayDetailView.as_view(), + name="admin.pathways.detail", + ), + path( + "admin/pathways/new/", + admin.PathwayCreateView.as_view(), + name="admin.pathways.create", + ), + path( + "admin/fulfillment/", admin.FulfillmentView.as_view(), name="admin.fulfillment" + ), path("admin/shop/", admin.ShopView.as_view(), name="admin.shop"), path("admin/review/", admin.ReviewView.as_view(), name="admin.review"), - path("admin/announcements/", admin.AnnouncementsView.as_view(), name="admin.announcements"), + path( + "admin/announcements/", + admin.AnnouncementsView.as_view(), + name="admin.announcements", + ), path("admin/logs/", admin.AuditLogsView.as_view(), name="admin.logs"), ] diff --git a/twisted/twisted_site/views/admin/__init__.py b/twisted/twisted_site/views/admin/__init__.py index d2ffe18..8ef4e48 100644 --- a/twisted/twisted_site/views/admin/__init__.py +++ b/twisted/twisted_site/views/admin/__init__.py @@ -1,8 +1,22 @@ +from .announcements import AnnouncementsView +from .audit_logs import AuditLogsView from .dashboard import DashboardView -from .users import UsersView, UserDetailView -from .pathways import PathwayListView, PathwayDetailView, PathwayCreateView from .fulfillment import FulfillmentView -from .shop import ShopView +from .pathways import PathwayCreateView, PathwayDetailView, PathwayListView from .review import ReviewView -from .announcements import AnnouncementsView -from .audit_logs import AuditLogsView +from .shop import ShopView +from .users import UserDetailView, UsersView + +__all__ = [ + "AnnouncementsView", + "AuditLogsView", + "DashboardView", + "FulfillmentView", + "PathwayCreateView", + "PathwayDetailView", + "PathwayListView", + "ReviewView", + "ShopView", + "UserDetailView", + "UsersView", +] diff --git a/twisted/twisted_site/views/admin/admin.py b/twisted/twisted_site/views/admin/admin.py index ec23df6..709a8ca 100644 --- a/twisted/twisted_site/views/admin/admin.py +++ b/twisted/twisted_site/views/admin/admin.py @@ -1,10 +1,10 @@ +from django.shortcuts import resolve_url from django.views import View -from django.shortcuts import render, redirect, resolve_url from dataclasses import dataclass import json from typing import Literal from django_htmx.http import trigger_client_event -from ...models import AuditLog +from ...models import AuditLog, ProfileStaffPermissions @dataclass class SidebarLink: @@ -13,37 +13,82 @@ class SidebarLink: text: str href: str + # Create your views here. class AdminView(View): def get_context_data(self, page, subpage=None) -> dict: context = {} - context['page'] = page - context['subpage'] = subpage + context["page"] = page + context["subpage"] = subpage context["sidebar_links"] = [ - SidebarLink(name="dashboard", icon="analytics", text="Analytics", href=resolve_url('admin.dash')), - SidebarLink(name="users", icon="profile", text="Users", href=resolve_url('admin.users')), - SidebarLink(name="pathways", icon="controls", text="Pathways", href=resolve_url('admin.pathways')), - SidebarLink(name="fulfillment", icon="list", text="Fulfillment", href=resolve_url('admin.fulfillment')), - SidebarLink(name="shop", icon="bag-add", text="Shop", href=resolve_url('admin.shop')), - SidebarLink(name="review", icon="message-new", text="Review", href=resolve_url('admin.review')), - SidebarLink(name="announcements", icon="important", text="Announcements", href=resolve_url('admin.announcements')), - SidebarLink(name="logs", icon="view", text="Audit Logs", href=resolve_url('admin.logs')+"?page=1"), + SidebarLink( + name="dashboard", + icon="analytics", + text="Analytics", + href=resolve_url("admin.dash"), + ), + SidebarLink( + name="users", + icon="profile", + text="Users", + href=resolve_url("admin.users"), + ), + SidebarLink( + name="pathways", + icon="controls", + text="Pathways", + href=resolve_url("admin.pathways"), + ), + SidebarLink( + name="fulfillment", + icon="list", + text="Fulfillment", + href=resolve_url("admin.fulfillment"), + ), + SidebarLink( + name="shop", icon="bag-add", text="Shop", href=resolve_url("admin.shop") + ), + SidebarLink( + name="review", + icon="message-new", + text="Review", + href=resolve_url("admin.review"), + ), + SidebarLink( + name="announcements", + icon="important", + text="Announcements", + href=resolve_url("admin.announcements"), + ), + SidebarLink( + name="logs", + icon="view", + text="Audit Logs", + href=resolve_url("admin.logs") + "?page=1", + ), ] - context['profile'] = self.request.user.profile + context["profile"] = self.request.user.profile return context - def dispatch(self, request, *args, **kwargs): if request.user.is_anonymous: - return redirect('homepage') + return redirect("homepage") if not request.user.profile.is_staff: - return redirect('dashboard') + return redirect("dashboard") self.audit_log = AuditLog( user=request.user, path=self.request.get_full_path(), - post=(request.method.lower() == 'post'), - additional_context={} + post=(request.method.lower() == "post"), + additional_context={}, ) + + perms = self.request.user.profile.staff_permissions + if perms is None: + profile = self.request.user.profile + profile.staff_permissions = ProfileStaffPermissions.objects.create() + profile.save() + + self.perms = perms response = super().dispatch(request, *args, **kwargs) self.audit_log.save() - return response \ No newline at end of file + return response diff --git a/twisted/twisted_site/views/admin/announcements.py b/twisted/twisted_site/views/admin/announcements.py index 73c496a..8d928cf 100644 --- a/twisted/twisted_site/views/admin/announcements.py +++ b/twisted/twisted_site/views/admin/announcements.py @@ -1,11 +1,13 @@ +from django.shortcuts import redirect from django.template.response import TemplateResponse + from .admin import AdminView -from django.shortcuts import render, redirect + # Create your views here. class AnnouncementsView(AdminView): def get(self, request): - context = self.get_context_data(page='announcements') + context = self.get_context_data(page="announcements") if self.request.user.is_anonymous: - return redirect('homepage') + return redirect("homepage") return TemplateResponse(request, "admin/announcements.html", context=context) diff --git a/twisted/twisted_site/views/admin/audit_logs.py b/twisted/twisted_site/views/admin/audit_logs.py index 19e2f79..17b90cc 100644 --- a/twisted/twisted_site/views/admin/audit_logs.py +++ b/twisted/twisted_site/views/admin/audit_logs.py @@ -1,26 +1,27 @@ +from django.core.paginator import Paginator from django.db.models.query_utils import Q +from django.shortcuts import redirect from django.template.response import TemplateResponse -from .admin import AdminView -from django.shortcuts import render, redirect + from ...models import AuditLog -from django.core.paginator import Paginator +from .admin import AdminView + # Create your views here. class AuditLogsView(AdminView): def get(self, request): - page_number = request.GET.get('page') + page_number = request.GET.get("page") if page_number is None: - return redirect(self.request.get_full_path()+'?page=1') - - context = self.get_context_data(page='logs') - - auditlogs = AuditLog.objects.order_by('-timestamp') - - context_mode = request.GET.get('context_mode', 'false') == 'true' + return redirect(self.request.get_full_path() + "?page=1") + + context = self.get_context_data(page="logs") + + auditlogs = AuditLog.objects.order_by("-timestamp") + + context_mode = request.GET.get("context_mode", "false") == "true" if context_mode: auditlogs = auditlogs.exclude( - Q(additional_context__isnull=True) | - Q(additional_context={}) + Q(additional_context__isnull=True) | Q(additional_context={}) ) paginator = Paginator(auditlogs, 100, orphans=50) diff --git a/twisted/twisted_site/views/admin/dashboard.py b/twisted/twisted_site/views/admin/dashboard.py index edeb329..de3d177 100644 --- a/twisted/twisted_site/views/admin/dashboard.py +++ b/twisted/twisted_site/views/admin/dashboard.py @@ -1,41 +1,54 @@ +import json + +from django.shortcuts import redirect from django.template.response import TemplateResponse + +from ...models import Journal from .admin import AdminView -from django.shortcuts import render, redirect -from ...models import Journal, Project, ProjectShip -import json + + # Create your views here. class DashboardView(AdminView): def get(self, request): - context = self.get_context_data(page='dashboard') + context = self.get_context_data(page="dashboard") if self.request.user.is_anonymous: - return redirect('homepage') + return redirect("homepage") hours_logged = 0 hours_logged_chart = {} logged_project_type = {"Software": 0, "Hardware": 0} shipped_project_type = {"Software": 0, "Hardware": 0} hours_shipped = 0 hours_shipped_chart = {} - for journal in Journal.objects.all().prefetch_related('project'): + for journal in Journal.objects.all().prefetch_related("project"): hours = journal.reduced_minutes / 60 hours_logged += hours - + date = journal.created_at.date().strftime("%a, %d %b") hours_logged_chart[date] = hours_logged_chart.get(date, 0) + hours - + logged_project_type[journal.project.get_project_type_display()] += hours - + if journal.project.is_shipped(): hours_shipped += hours hours_shipped_chart[date] = hours_shipped_chart.get(date, 0) + hours - shipped_project_type[journal.project.get_project_type_display()] += hours - - context['hours_logged'] = round(hours_logged, 2) - context['hours_logged_chart'] = json.dumps([['Date', 'Hours']] + list(hours_logged_chart.items())) - context['logged_project_type'] = json.dumps([['Type', 'Hours']] + list(logged_project_type.items())) - - context['hours_shipped'] = round(hours_shipped, 2) - context['hours_shipped_chart'] = json.dumps([['Date', 'Hours']] + list(hours_shipped_chart.items())) - context['shipped_project_type'] = json.dumps([['Type', 'Hours']] + list(shipped_project_type.items())) - - - return TemplateResponse(request, "admin/dashboard.html", context=context) \ No newline at end of file + shipped_project_type[journal.project.get_project_type_display()] += ( + hours + ) + + context["hours_logged"] = round(hours_logged, 2) + context["hours_logged_chart"] = json.dumps( + [["Date", "Hours"]] + list(hours_logged_chart.items()) + ) + context["logged_project_type"] = json.dumps( + [["Type", "Hours"]] + list(logged_project_type.items()) + ) + + context["hours_shipped"] = round(hours_shipped, 2) + context["hours_shipped_chart"] = json.dumps( + [["Date", "Hours"]] + list(hours_shipped_chart.items()) + ) + context["shipped_project_type"] = json.dumps( + [["Type", "Hours"]] + list(shipped_project_type.items()) + ) + + return TemplateResponse(request, "admin/dashboard.html", context=context) diff --git a/twisted/twisted_site/views/admin/fulfillment.py b/twisted/twisted_site/views/admin/fulfillment.py index ba0be71..9d1bf37 100644 --- a/twisted/twisted_site/views/admin/fulfillment.py +++ b/twisted/twisted_site/views/admin/fulfillment.py @@ -1,8 +1,10 @@ +from django.shortcuts import render + from .admin import AdminView -from django.shortcuts import render, redirect + # Create your views here. class FulfillmentView(AdminView): def get(self, request): - context = self.get_context_data(page='fulfillment') + context = self.get_context_data(page="fulfillment") return render(request, "admin/fulfillment.html", context=context) diff --git a/twisted/twisted_site/views/admin/pathways.py b/twisted/twisted_site/views/admin/pathways.py index 7b479cb..5df538c 100644 --- a/twisted/twisted_site/views/admin/pathways.py +++ b/twisted/twisted_site/views/admin/pathways.py @@ -1,9 +1,10 @@ -from django.http import HttpResponse -from .admin import AdminView -from django.shortcuts import render, redirect, get_object_or_404 -from ...models import Pathway, User -from django.utils import timezone from django.contrib import messages +from django.shortcuts import get_object_or_404, redirect, render +from django.utils import timezone + +from ...models import Pathway, User +from .admin import AdminView + # Create your views here. class PathwayListView(AdminView): @@ -30,18 +31,20 @@ def get(self, request): context["past_pathways"] = past_pathways context["future_pathways"] = future_pathways - return render(request, "admin/pathways/list.html", context=context) class PathwayCreateView(AdminView): - def get(self, request, error=None, extracontext={}): + def get(self, request, error=None, extracontext=None): + if extracontext is None: + extracontext = {} + context = self.get_context_data(page="pathways", subpage="create") context.update(extracontext) - + if error: messages.error(request, error) - + return render(request, "admin/pathways/create.html", context=context) def post(self, request): @@ -52,75 +55,82 @@ def post(self, request): end_date = request.POST.get("endDate") end_time = request.POST.get("endTime") - + min_mins = int(request.POST.get("mins", "0")) errcontext = { - 'pathway_name': pathway_name, - 'start_date': start_date, - 'start_time': start_time, - 'end_date': end_date, - 'end_time': end_time, - 'min_mins': min_mins, + "pathway_name": pathway_name, + "start_date": start_date, + "start_time": start_time, + "end_date": end_date, + "end_time": end_time, + "min_mins": min_mins, } - + if "form validation": if not pathway_name: return self.get(request, "No pathway name typed!", errcontext) - + if not start_date: return self.get(request, "No start date selected!", errcontext) - + if not start_time: return self.get(request, "No start time selected!", errcontext) - + if not end_date: return self.get(request, "No end date selected!", errcontext) - + if not end_time: return self.get(request, "No end time selected!", errcontext) - + if min_mins <= 0: - return self.get(request, "Minimum minutes must be greater than zero!", errcontext) - - current_tz_offset = timezone.datetime.now(timezone.get_current_timezone()).strftime('%z') - + return self.get( + request, "Minimum minutes must be greater than zero!", errcontext + ) + + current_tz_offset = timezone.datetime.now( + timezone.get_current_timezone() + ).strftime("%z") + start = timezone.datetime.strptime( f"{start_date} {start_time} {current_tz_offset}", "%Y-%m-%d %H:%M %z" ) - + end = timezone.datetime.strptime( f"{end_date} {end_time} {current_tz_offset}", "%Y-%m-%d %H:%M %z" ) - + Pathway.objects.create( - start=start, - end=end, - name=pathway_name, - min_mins=min_mins + start=start, end=end, name=pathway_name, min_mins=min_mins ) - - messages.success(request, f"Successfully created Pathway for \"{pathway_name}\"!") - return redirect('admin.pathways') + messages.success(request, f'Successfully created Pathway for "{pathway_name}"!') + + return redirect("admin.pathways") class PathwayDetailView(AdminView): def get(self, request, id): context = self.get_context_data(page="pathways", subpage="detail") pathway = get_object_or_404(Pathway, id=id) - context['pathway'] = pathway + context["pathway"] = pathway + + self.audit_log.additional_context["pathway_name"] = pathway.name - self.audit_log.additional_context['pathway_name'] = pathway.name - mins_per_participant = pathway.mins_spent_per_participant() - users = User.objects.filter(id__in=mins_per_participant.keys()).select_related("profile") + users = User.objects.filter(id__in=mins_per_participant.keys()).select_related( + "profile" + ) participants = [ { "user": user, "mins": mins_per_participant[user.id], - "percent": min(100, round(mins_per_participant[user.id] / pathway.min_mins * 100)) if pathway.min_mins else 0, + "percent": min( + 100, round(mins_per_participant[user.id] / pathway.min_mins * 100) + ) + if pathway.min_mins + else 0, "qualified": mins_per_participant[user.id] >= pathway.min_mins, } for user in users diff --git a/twisted/twisted_site/views/admin/review.py b/twisted/twisted_site/views/admin/review.py index 4895ecd..9caeb04 100644 --- a/twisted/twisted_site/views/admin/review.py +++ b/twisted/twisted_site/views/admin/review.py @@ -1,43 +1,44 @@ -from django.contrib import messages -from django.http import HttpResponse -from .admin import AdminView -from django.shortcuts import render, redirect, get_object_or_404 from django.conf import settings +from django.contrib import messages +from django.shortcuts import get_object_or_404, redirect, render + from ...models import ProjectShip +from .admin import AdminView + # Create your views here. class ReviewView(AdminView): def get(self, request): if settings.DEBUG_REVIEW: return self.debug_get(request) - - context = self.get_context_data(page='review') + + context = self.get_context_data(page="review") return render(request, "admin/review.html", context=context) - + def post(self, request): if settings.DEBUG_REVIEW: return self.debug_post(request) - - context = self.get_context_data(page='review') + + self.get_context_data(page="review") return redirect(self.request.path_info) - + def debug_get(self, request): - context = self.get_context_data(page='review') - context['ships'] = ProjectShip.objects.all().order_by('-created_at') + context = self.get_context_data(page="review") + context["ships"] = ProjectShip.objects.all().order_by("-created_at") return render(request, "admin/debug/review.html", context=context) - + def debug_post(self, request): - id = request.POST['id'] + id = request.POST["id"] - status = request.POST['status'] - note_to_maker = request.POST['note_to_maker'] - audit_note = request.POST['audit_note'] - technical_features = request.POST['technical_features'] - deflation_reason = request.POST['deflation_reason'] + status = request.POST["status"] + note_to_maker = request.POST["note_to_maker"] + audit_note = request.POST["audit_note"] + technical_features = request.POST["technical_features"] + deflation_reason = request.POST["deflation_reason"] - final_status = request.POST['final_status'] - final_note_to_maker = request.POST['final_note_to_maker'] - final_audit_note = request.POST['final_audit_note'] + final_status = request.POST["final_status"] + final_note_to_maker = request.POST["final_note_to_maker"] + final_audit_note = request.POST["final_audit_note"] ship = get_object_or_404(ProjectShip, id=id) @@ -53,4 +54,4 @@ def debug_post(self, request): ship.save() messages.info(request, f"Ship with id {id} updated.") - return redirect(self.request.path_info) \ No newline at end of file + return redirect(self.request.path_info) diff --git a/twisted/twisted_site/views/admin/shop.py b/twisted/twisted_site/views/admin/shop.py index 3e563b7..2fb3397 100644 --- a/twisted/twisted_site/views/admin/shop.py +++ b/twisted/twisted_site/views/admin/shop.py @@ -1,8 +1,10 @@ +from django.shortcuts import render + from .admin import AdminView -from django.shortcuts import render, redirect + # Create your views here. class ShopView(AdminView): def get(self, request): - context = self.get_context_data(page='shop') + context = self.get_context_data(page="shop") return render(request, "admin/shop.html", context=context) diff --git a/twisted/twisted_site/views/admin/users.py b/twisted/twisted_site/views/admin/users.py index 4a836e4..d123791 100644 --- a/twisted/twisted_site/views/admin/users.py +++ b/twisted/twisted_site/views/admin/users.py @@ -1,65 +1,75 @@ +import json +import os + from django.contrib.sessions.models import Session +from django.db.models import Q +from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse -from django.http import HttpResponse + +from ...models import User from .admin import AdminView -from django.shortcuts import render, redirect, get_object_or_404 -from ...models import User, Profile -from django.db.models import Q -import os -import json + # Create your views here. class UsersView(AdminView): def get(self, request): - context = self.get_context_data(page='users') - if request.GET.get('search'): - query = request.GET['search'] - context['users'] = User.objects.all() - context['users'] = User.objects.filter( - Q(profile__slack_username__icontains=query) | - Q(profile__slack_id__icontains=query) | - Q(first_name__icontains=query) | - Q(last_name__icontains=query) - ).order_by('profile__slack_username') - context['search'] = True + context = self.get_context_data(page="users") + if request.GET.get("search"): + query = request.GET["search"] + context["users"] = User.objects.all() + context["users"] = User.objects.filter( + Q(profile__slack_username__icontains=query) + | Q(profile__slack_id__icontains=query) + | Q(first_name__icontains=query) + | Q(last_name__icontains=query) + ).order_by("profile__slack_username") + context["search"] = True else: - context['users'] = User.objects.all().order_by('profile__slack_username') + context["users"] = User.objects.all().order_by("profile__slack_username") return TemplateResponse(request, "admin/users.html", context) def post(self, request): - if request.POST.get('action') == 'logoutall': + if request.POST.get("action") == "logoutall": session_count = Session.objects.count() Session.objects.all().delete() - self.audit_log.additional_context['action'] = 'logoutall' - self.audit_log.additional_context['sessions_deleted'] = session_count + self.audit_log.additional_context["action"] = "logoutall" + self.audit_log.additional_context["sessions_deleted"] = session_count return redirect(self.request.path) - + + class UserDetailView(AdminView): def get(self, request, id): - context = self.get_context_data(page='users', subpage='detail') + context = self.get_context_data(page="users", subpage="detail") user = get_object_or_404(User, id=id) - self.audit_log.additional_context['user_pfp__img'] = user.profile.slack_pfp_url - self.audit_log.additional_context['user'] = user.profile.slack_username - - context['user'] = user - context['login_maybe'] = os.environ.get("LOGIN_ENABLED") == 'maybe' + self.audit_log.additional_context["user_pfp__img"] = user.profile.slack_pfp_url + self.audit_log.additional_context["user"] = user.profile.slack_username + + context["user"] = user + context["login_maybe"] = os.environ.get("LOGIN_ENABLED") == "maybe" return TemplateResponse(request, "admin/user.html", context) - + def post(self, request, id): user = get_object_or_404(User, id=id) - self.audit_log.additional_context['user_pfp__img'] = user.profile.slack_pfp_url - self.audit_log.additional_context['user'] = user.profile.slack_username + self.audit_log.additional_context["user_pfp__img"] = user.profile.slack_pfp_url + self.audit_log.additional_context["user"] = user.profile.slack_username - if request.POST.get('action') == 'toggle_is_allowed': + if request.POST.get("action") == "toggle_is_allowed": prof = user.profile prof.is_allowed = not prof.is_allowed - self.audit_log.additional_context['is_allowed'] = f"Set to {prof.is_allowed}" + self.audit_log.additional_context["is_allowed"] = ( + f"Set to {prof.is_allowed}" + ) prof.save() resp = redirect(self.request.path) - resp["HX-Trigger"] = json.dumps({ - "toast": {"message": f"Set is_allowed to {prof.is_allowed}", "variant": "success"} - }) + resp["HX-Trigger"] = json.dumps( + { + "toast": { + "message": f"Set is_allowed to {prof.is_allowed}", + "variant": "success", + } + } + ) return resp diff --git a/twisted/twisted_site/views/ari.py b/twisted/twisted_site/views/ari.py index be2784a..285dc93 100644 --- a/twisted/twisted_site/views/ari.py +++ b/twisted/twisted_site/views/ari.py @@ -1,21 +1,22 @@ import json -from django.http import HttpResponseNotAllowed, HttpResponse, HttpResponseBadRequest -from django.shortcuts import render + +from django.http import HttpResponse, HttpResponseBadRequest from django.utils.decorators import method_decorator from django.views import View from django.views.decorators.csrf import csrf_exempt + from ..ari import verify_webhook_signature -from ..models import ProjectShip, Project +from ..models import Project, ProjectShip from ..slack import slack_bot def _escape_mrkdwn(text): - return text.replace('&', '&').replace('<', '<').replace('>', '>') + return text.replace("&", "&").replace("<", "<").replace(">", ">") def _quote_block(value): - lines = _escape_mrkdwn(value).splitlines() or [''] - return '\n'.join(f'> {line}' for line in lines) + lines = _escape_mrkdwn(value).splitlines() or [""] + return "\n".join(f"> {line}" for line in lines) def _build_ship_update_blocks(project, changes): @@ -31,18 +32,20 @@ def _build_ship_update_blocks(project, changes): for change in changes: blocks.append({"type": "divider"}) - field_name = _escape_mrkdwn(change['field'].replace('_', ' ').title()) - blocks.append({ - "type": "section", - "text": { - "type": "mrkdwn", - "text": ( - f"*{field_name}* changed\n\n" - f"*Old:*\n{_quote_block(change['old_value'])}\n\n" - f"*New:*\n{_quote_block(change['new_value'])}" - ), - }, - }) + field_name = _escape_mrkdwn(change["field"].replace("_", " ").title()) + blocks.append( + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ( + f"*{field_name}* changed\n\n" + f"*Old:*\n{_quote_block(change['old_value'])}\n\n" + f"*New:*\n{_quote_block(change['new_value'])}" + ), + }, + } + ) return blocks @@ -87,13 +90,15 @@ def _build_review_approved_blocks(project, note_to_maker): ] if note_to_maker: blocks.append({"type": "divider"}) - blocks.append({ - "type": "section", - "text": { - "type": "mrkdwn", - "text": f"*Note from reviewer:*\n{_quote_block(note_to_maker)}", - }, - }) + blocks.append( + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": f"*Note from reviewer:*\n{_quote_block(note_to_maker)}", + }, + } + ) return blocks @@ -109,21 +114,25 @@ def _build_review_rejected_blocks(project, note_to_maker): ] if note_to_maker: blocks.append({"type": "divider"}) - blocks.append({ + blocks.append( + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": f"*Note from reviewer:*\n{_quote_block(note_to_maker)}", + }, + } + ) + blocks.append({"type": "divider"}) + blocks.append( + { "type": "section", "text": { "type": "mrkdwn", - "text": f"*Note from reviewer:*\n{_quote_block(note_to_maker)}", + "text": "Feel free to drop us a message over at #twisted-help if you think this is a mistake!", }, - }) - blocks.append({"type": "divider"}) - blocks.append({ - "type": "section", - "text": { - "type": "mrkdwn", - "text": "Feel free to drop us a message over at #twisted-help if you think this is a mistake!", - }, - }) + } + ) return blocks @@ -150,61 +159,62 @@ def _build_review_requeued_blocks(project): }, ] + # Create your views here. -@method_decorator(csrf_exempt, name='dispatch') +@method_decorator(csrf_exempt, name="dispatch") class AriView(View): def post(self, request): body = request.body if not verify_webhook_signature( body, - request.headers.get('X-Ari-Timestamp', ''), - request.headers.get('X-Ari-Delivery-Id', ''), - request.headers.get('X-Ari-Signature', ''), + request.headers.get("X-Ari-Timestamp", ""), + request.headers.get("X-Ari-Delivery-Id", ""), + request.headers.get("X-Ari-Signature", ""), ): return HttpResponse(status=401) data = json.loads(body) - external_id = data.get('external_id') + external_id = data.get("external_id") if not external_id: - return HttpResponseBadRequest('Missing external_id') + return HttpResponseBadRequest("Missing external_id") try: - project_id = int(external_id.removeprefix('twisted-')) + project_id = int(external_id.removeprefix("twisted-")) project = Project.objects.get(id=project_id) except (ValueError, Project.DoesNotExist): # ty:ignore[unresolved-attribute] - return HttpResponseBadRequest('Invalid external_id') + return HttpResponseBadRequest("Invalid external_id") - event = data.get('event') + event = data.get("event") if not event: - return HttpResponseBadRequest('Missing event') - - if data['event'] == 'ship.updated': - project.project_name = data['ship']['title'] - project.project_description = data['ship']['description'] - project.project_type = data['ship']['track'] - project.screenshot_url = data['ship']['thumbnail_url'] - project.repo_url = data['ship']['repo_url'] - project.playable_url = data['ship']['demo_url'] - project.hackatime_project_name = data['ship']['hackatime_projects'][0] + return HttpResponseBadRequest("Missing event") + + if data["event"] == "ship.updated": + project.project_name = data["ship"]["title"] + project.project_description = data["ship"]["description"] + project.project_type = data["ship"]["track"] + project.screenshot_url = data["ship"]["thumbnail_url"] + project.repo_url = data["ship"]["repo_url"] + project.playable_url = data["ship"]["demo_url"] + project.hackatime_project_name = data["ship"]["hackatime_projects"][0] project.save() slack_bot.send_blocks( channel=project.user.profile.slack_id, - blocks=_build_ship_update_blocks(project, data['changes']), + blocks=_build_ship_update_blocks(project, data["changes"]), text=f"Your ship for {project.project_name} has been updated by a reviewer!", ) - return HttpResponse('Request processed!') - - if data['event'] == 'review.changes': - if data['decision'] != 'changes': - return HttpResponse('Event ignored') - note_to_maker = data['review']['note_to_maker'] + return HttpResponse("Request processed!") - ship:ProjectShip = project.latest_ship() + if data["event"] == "review.changes": + if data["decision"] != "changes": + return HttpResponse("Event ignored") + note_to_maker = data["review"]["note_to_maker"] + + ship: ProjectShip = project.latest_ship() - ship.status = 'requested_changes' # ty:ignore[invalid-assignment] + ship.status = "requested_changes" # ty:ignore[invalid-assignment] ship.note_to_maker = note_to_maker ship.save() @@ -214,19 +224,19 @@ def post(self, request): text=f"Your ship for {project.project_name} needs some changes!", ) - return HttpResponse('Request processed!') + return HttpResponse("Request processed!") - if data['event'] == 'review.approved': - review = data['review'] - note_to_maker = review.get('note_to_maker', '') - justification = review.get('justification') or {} + if data["event"] == "review.approved": + review = data["review"] + note_to_maker = review.get("note_to_maker", "") + justification = review.get("justification") or {} ship: ProjectShip = project.latest_ship() - ship.status = 'approved' # ty:ignore[invalid-assignment] + ship.status = "approved" # ty:ignore[invalid-assignment] ship.note_to_maker = note_to_maker - ship.audit_note = review.get('audit_note', '') - ship.technical_features = justification.get('technical_features', '') - ship.deflation_reason = justification.get('deflation_reason', '') + ship.audit_note = review.get("audit_note", "") + ship.technical_features = justification.get("technical_features", "") + ship.deflation_reason = justification.get("deflation_reason", "") ship.save() slack_bot.send_blocks( @@ -235,19 +245,19 @@ def post(self, request): text=f"Your ship for {project.project_name} was approved!", ) - return HttpResponse('Request processed!') + return HttpResponse("Request processed!") - if data['event'] == 'review.rejected': - review = data['review'] - note_to_maker = review.get('note_to_maker', '') - justification = review.get('justification') or {} + if data["event"] == "review.rejected": + review = data["review"] + note_to_maker = review.get("note_to_maker", "") + justification = review.get("justification") or {} ship: ProjectShip = project.latest_ship() - ship.status = 'rejected' # ty:ignore[invalid-assignment] + ship.status = "rejected" # ty:ignore[invalid-assignment] ship.note_to_maker = note_to_maker - ship.audit_note = review.get('audit_note', '') - ship.technical_features = justification.get('technical_features', '') - ship.deflation_reason = justification.get('deflation_reason', '') + ship.audit_note = review.get("audit_note", "") + ship.technical_features = justification.get("technical_features", "") + ship.deflation_reason = justification.get("deflation_reason", "") ship.save() slack_bot.send_blocks( @@ -256,11 +266,11 @@ def post(self, request): text=f"Your ship for {project.project_name} was rejected.", ) - return HttpResponse('Request processed!') + return HttpResponse("Request processed!") - if data['event'] == 'review.reverted': + if data["event"] == "review.reverted": ship: ProjectShip = project.latest_ship() - ship.status = 'pending' # ty:ignore[invalid-assignment] + ship.status = "pending" # ty:ignore[invalid-assignment] ship.save() slack_bot.send_blocks( @@ -269,11 +279,11 @@ def post(self, request): text=f"The decision on your ship for {project.project_name} was reverted.", ) - return HttpResponse('Request processed!') + return HttpResponse("Request processed!") - if data['event'] == 'review.requeued': + if data["event"] == "review.requeued": ship: ProjectShip = project.latest_ship() - ship.status = 'pending' # ty:ignore[invalid-assignment] + ship.status = "pending" # ty:ignore[invalid-assignment] ship.save() slack_bot.send_blocks( @@ -282,6 +292,6 @@ def post(self, request): text=f"Your ship for {project.project_name} is back in the review queue.", ) - return HttpResponse('Request processed!') + return HttpResponse("Request processed!") - return HttpResponse('Event ignored') \ No newline at end of file + return HttpResponse("Event ignored") diff --git a/twisted/twisted_site/views/client/__init__.py b/twisted/twisted_site/views/client/__init__.py index 7e3545f..d066a43 100644 --- a/twisted/twisted_site/views/client/__init__.py +++ b/twisted/twisted_site/views/client/__init__.py @@ -1,8 +1,29 @@ -from .homepage import HomepageView, FaqsView from .dashboard import DashboardView -from .projects import ListProjects, CreateProject -from .project import ProjectDetail, ProjectSettings, SubmitProject -from .journal import NewProjectHackatimeJournal, NewProjectUntrackedJournal, DeleteJournal +from .discover import DiscoverView +from .homepage import FaqsView, HomepageView +from .journal import ( + DeleteJournal, + NewProjectHackatimeJournal, + NewProjectUntrackedJournal, +) from .pathways import PathwaysView +from .project import ProjectDetail, ProjectSettings, SubmitProject +from .projects import CreateProject, ListProjects from .referrals import ReferralsView -from .discover import DiscoverView \ No newline at end of file + +__all__ = [ + "CreateProject", + "DashboardView", + "DeleteJournal", + "DiscoverView", + "FaqsView", + "HomepageView", + "ListProjects", + "NewProjectHackatimeJournal", + "NewProjectUntrackedJournal", + "PathwaysView", + "ProjectDetail", + "ProjectSettings", + "ReferralsView", + "SubmitProject", +] diff --git a/twisted/twisted_site/views/client/auth.py b/twisted/twisted_site/views/client/auth.py index dc695db..bdf51a1 100644 --- a/twisted/twisted_site/views/client/auth.py +++ b/twisted/twisted_site/views/client/auth.py @@ -1,16 +1,22 @@ +import hmac +import logging +import os +import secrets + import requests -from django.http import JsonResponse +from authlib.integrations.django_client import OAuth from django.contrib.auth import get_user_model, login, logout +from django.http import JsonResponse from django.shortcuts import redirect -import os -from authlib.integrations.django_client import OAuth from django.views import View import secrets import hmac +from django.conf import settings +from ... import hackatime from ...models import Profile from ... import hackatime -from ...slack import slack_bot +from ...slack import slack_bot, SLACK_LOG_CHANNEL, log_to_channel oauth = OAuth() @@ -86,8 +92,8 @@ def get(self, request): ) avatar_url = slack_profile.get("image_512") - except Exception as e: - print("Slack profile fetch failed", e) + except Exception: + logger.exception("Slack profile fetch failed") display_name = name avatar_url = os.environ["DEFAULT_PFP"] @@ -97,22 +103,21 @@ def get(self, request): profile.slack_username = display_name profile.slack_pfp_url = avatar_url profile.ysws_eligible = ysws_eligible - profile.hca_access_token = token['access_token'] - - referral_code = self.request.COOKIES.get('referral') + profile.hca_access_token = token["access_token"] + + referral_code = self.request.COOKIES.get("referral") if created and referral_code: referral_profiles = Profile.objects.filter(my_referral_code=referral_code) if referral_profiles: referral_profile = referral_profiles.get() profile.referred_by = referral_profile - + profile.save() - if os.environ.get("LOGIN_ENABLED") == "maybe": - if not profile.is_allowed: - return JsonResponse( - {"error": "Not allowed! DM @kavyansh. if this is a mistake!"} - ) + if os.environ.get("LOGIN_ENABLED") == "maybe" and not profile.is_allowed: + return JsonResponse( + {"error": "Not allowed! DM @kavyansh. if this is a mistake!"} + ) login(request, user) @@ -128,6 +133,8 @@ def get(self, request): f"https://hackatime.hackclub.com/oauth/authorize?client_id={HACKATIME_CLIENT_ID}&redirect_uri={HACKATIME_REDIRECT_URI}&response_type=code&scope={scopes}&state={profile.hackatime_state}" ) + log_to_channel(f":ms-arrow-up-right: *{profile.slack_username}* just logged in!") + return redirect("dashboard") diff --git a/twisted/twisted_site/views/client/dashboard.py b/twisted/twisted_site/views/client/dashboard.py index dfe3ca0..25eb8d4 100644 --- a/twisted/twisted_site/views/client/dashboard.py +++ b/twisted/twisted_site/views/client/dashboard.py @@ -1,26 +1,29 @@ +from django.shortcuts import get_object_or_404, redirect, render, resolve_url from django.views import View -from django.shortcuts import render, redirect, resolve_url, get_object_or_404 + from ...models import Project + # Create your views here. class DashboardView(View): def get(self, request): if self.request.user.is_anonymous: - return redirect('homepage') + return redirect("homepage") profile = self.request.user.profile - - context = { - "profile": profile - } - + + context = {"profile": profile} + startup_windows = [] - - project_id = request.GET.get('project') - + + project_id = request.GET.get("project") + if project_id: project = get_object_or_404(Project, id=project_id) startup_windows.append({"href": resolve_url('fr.projects.detail', project.id), "title": project.project_name}) + if request.GET.get('discover') is not None: + startup_windows.append({"href": resolve_url('fr.discover'), "title": "discover"}) + context['startup_windows'] = startup_windows - return render(request, "client/dashboard.html", context) \ No newline at end of file + return render(request, "client/dashboard.html", context) diff --git a/twisted/twisted_site/views/client/discover.py b/twisted/twisted_site/views/client/discover.py index 442a8e9..76e0649 100644 --- a/twisted/twisted_site/views/client/discover.py +++ b/twisted/twisted_site/views/client/discover.py @@ -1,6 +1,7 @@ -from django.views import View -from django.shortcuts import render, redirect from django.core.paginator import Paginator +from django.shortcuts import redirect, render +from django.views import View + from ...models import Project PROJECTS_PER_PAGE = 120 diff --git a/twisted/twisted_site/views/client/homepage.py b/twisted/twisted_site/views/client/homepage.py index 9008576..f748503 100644 --- a/twisted/twisted_site/views/client/homepage.py +++ b/twisted/twisted_site/views/client/homepage.py @@ -1,7 +1,8 @@ -from django.views import View -from django.shortcuts import render import os +from django.shortcuts import render +from django.views import View + # Create your views here. class HomepageView(View): @@ -10,22 +11,22 @@ def get(self, request): login_enabled = False else: login_enabled = True - + referral_code = request.GET.get("ref") - + response = render( request, "client/homepage.html", {"login_enabled": login_enabled}, ) - + if referral_code: response.set_cookie( - 'referral', + "referral", referral_code, - max_age=60*60, # 1 hour + max_age=60 * 60, # 1 hour httponly=True, - samesite='Lax' + samesite="Lax", ) return response diff --git a/twisted/twisted_site/views/client/journal.py b/twisted/twisted_site/views/client/journal.py index 0a8d438..9c7d8dc 100644 --- a/twisted/twisted_site/views/client/journal.py +++ b/twisted/twisted_site/views/client/journal.py @@ -1,18 +1,23 @@ -from markdown_it.rules_inline import image -from django.http import JsonResponse +import math +import re + +from django.shortcuts import get_object_or_404, redirect, render from django.views import View -from django.shortcuts import render, redirect, get_object_or_404 +from django.shortcuts import render, redirect, get_object_or_404, resolve_url from ...models import Profile, Project, Journal from ... import hackatime import re import math HACKATIME_MAX_LOGGABLE_MINUTES = 6 * 60 -IMAGE_REGEX = r'!\[([^\]]*)\]\([^)]+\)' +IMAGE_REGEX = r"!\[([^\]]*)\]\([^)]+\)" class NewProjectHackatimeJournal(View): - def get(self, request, id, info=None, context={}): + def get(self, request, id, info=None, context=None): + if context is None: + context = {} + context["info"] = info if self.request.user.is_anonymous: return redirect("homepage") @@ -24,8 +29,7 @@ def get(self, request, id, info=None, context={}): context["project"] = project if project.is_shipped(): - return redirect('fr.projects.detail', id) - + return redirect("fr.projects.detail", id) log_minutes = project.hackatime_time_unjournaled() @@ -47,15 +51,15 @@ def post(self, request, id): ) if project.is_shipped(): - return redirect('fr.projects.detail', id) + return redirect("fr.projects.detail", id) content = request.POST["content"] - + image_count = len(re.findall(IMAGE_REGEX, content)) required_image_count = math.ceil(max(1, reduced_minutes / 180)) - - content_no_images = re.sub(IMAGE_REGEX, '', content) - content_length = len(' '.join(content_no_images.split())) + + content_no_images = re.sub(IMAGE_REGEX, "", content) + content_length = len(" ".join(content_no_images.split())) if image_count < required_image_count: return self.get( @@ -87,19 +91,23 @@ def post(self, request, id): UNTRACKED_MAX_LOGGABLE_MINUTES = 60 + class NewProjectUntrackedJournal(View): - def get(self, request, id, info=None, context={}): + def get(self, request, id, info=None, context=None): + if context is None: + context = {} + context["info"] = info if self.request.user.is_anonymous: return redirect("homepage") - + project = get_object_or_404(Project, id=id) if project.user != request.user: return redirect("dashboard") - if project.project_type == 'software': - return redirect('fr.projects.journals.new.hackatime') + if project.project_type == "software": + return redirect("fr.projects.journals.new.hackatime") context["project"] = project @@ -123,16 +131,15 @@ def post(self, request, id): project = get_object_or_404(Project, id=id) if project.user != request.user: return redirect("dashboard") - - if project.project_type == 'software': - return redirect('fr.projects.journals.new.hackatime') + + if project.project_type == "software": + return redirect("fr.projects.journals.new.hackatime") content = request.POST["content"] time_logged = int(request.POST["time_logged"]) - - content_no_images = re.sub(IMAGE_REGEX, '', content) - content_length = len(' '.join(content_no_images.split())) + content_no_images = re.sub(IMAGE_REGEX, "", content) + content_length = len(" ".join(content_no_images.split())) if time_logged > UNTRACKED_MAX_LOGGABLE_MINUTES: return self.get( @@ -149,7 +156,7 @@ def post(self, request, id): info="I dont understand, why do you wanna lose time :hs:", context={"content": content}, ) - + if content_length < min(100, time_logged * 2): return self.get( request, @@ -169,46 +176,45 @@ def post(self, request, id): return self.get(request, id, context={"success": True}) + class DeleteJournal(View): - def get(self, request, id, context={'success': False}): + def get(self, request, id, context=None): + if context is None: + context = {"success": False} + if request.user.is_anonymous: - return redirect('homepage') - + return redirect("homepage") + if id is not None: journal = get_object_or_404(Journal, id=id) if journal.project.is_shipped(): - return redirect('fr.projects.detail', journal.project.id) + return redirect("fr.projects.detail", journal.project.id) if journal.project.user != request.user: - return redirect('dashboard') - - if journal.type != 'untracked': - return redirect('dashboard') - - - context['journal'] = journal - - - return render( - request, "client/projects/journal/delete.html", context=context - ) + return redirect("dashboard") + + if journal.type != "untracked": + return redirect("dashboard") + + context["journal"] = journal + + return render(request, "client/projects/journal/delete.html", context=context) def post(self, request, id): if request.user.is_anonymous: - return redirect('homepage') + return redirect("homepage") journal = get_object_or_404(Journal, id=id) if journal.project.is_shipped(): - return redirect('fr.projects.detail', journal.project.id) + return redirect("fr.projects.detail", journal.project.id) if journal.project.user != request.user: - return redirect('dashboard') - - if journal.type != 'untracked': - return redirect('dashboard') - + return redirect("dashboard") + + if journal.type != "untracked": + return redirect("dashboard") + journal.delete() - - + return self.get(request, id=None, context={"success": True}) diff --git a/twisted/twisted_site/views/client/pathways.py b/twisted/twisted_site/views/client/pathways.py index 0223962..3d1ddb9 100644 --- a/twisted/twisted_site/views/client/pathways.py +++ b/twisted/twisted_site/views/client/pathways.py @@ -1,7 +1,6 @@ -from django.utils import timezone -from django.http import JsonResponse, HttpResponse +from django.shortcuts import redirect, render from django.views import View -from django.shortcuts import render, redirect + from ...models import Pathway diff --git a/twisted/twisted_site/views/client/project.py b/twisted/twisted_site/views/client/project.py index 0acddbb..2623d1f 100644 --- a/twisted/twisted_site/views/client/project.py +++ b/twisted/twisted_site/views/client/project.py @@ -1,9 +1,11 @@ -from requests import HTTPError, RequestException from itertools import chain -from markdown_it.rules_inline import image -from django.http import JsonResponse, HttpResponse + +from django.http import HttpResponse +from django.shortcuts import get_object_or_404, redirect, render from django.views import View -from django.shortcuts import render, redirect, get_object_or_404 +from django.shortcuts import render, redirect, get_object_or_404, resolve_url + +from ...slack import log_to_channel from ...models import Profile, Project, Journal, ProjectShip, PROJECT_TYPE_CHOICES from ... import hackatime from ... import ari @@ -30,6 +32,7 @@ def get(self, request, id): context["first_pass_status"] = "pending" context["second_pass_status"] = "pending" + if project.latest_ship() is not None: try: status = ari.get_project_status(project) @@ -107,11 +110,19 @@ def post(self, request, id): project.playable_url = request.POST.get("playable_url", "") project.screenshot_url = request.POST.get("screenshot_url", "") project.save() + + project_url = f"{self.request.scheme}://{self.request.get_host()}{resolve_url('dashboard')}?project={project.id}" + log_to_channel(f":settings: Updated settings for *<{project_url}|{project.project_name}>*!\n- *Description*: {project.project_description}\n- *Type*: {project_type}\n- *Hackatime*: {project.hackatime_project_name or 'None'}\n- *Repo*: {project.repo_url or 'None'}\n- *Demo*: {project.playable_url or 'None'}\n- *Screenshot*: {project.screenshot_url}") + + return redirect("fr.projects.detail", project.id) class SubmitProject(View): - def get(self, request, id, context={}): + def get(self, request, id, context=None): + if context is None: + context = {} + if self.request.user.is_anonymous: return redirect("homepage") @@ -133,13 +144,16 @@ def get(self, request, id, context={}): context["project"] = project return render(request, "client/projects/ship.html", context) - def post(self, request, id, context={}): + def post(self, request, id, context=None): + if context is None: + context = {} + if self.request.user.is_anonymous: return redirect("homepage") project = get_object_or_404(Project, id=id) if project.user != request.user: - return redirect('fr.projects.detail', project.id) + return redirect("fr.projects.detail", project.id) if project.is_shipped(): return self.get( @@ -154,12 +168,17 @@ def post(self, request, id, context={}): if not project.user.profile.ysws_eligible: return self.get(request, id) + ship = ProjectShip(project=project) ship.save() try: ari.send_ship(ship) - except Exception as e: + except Exception: ship.delete() - raise e + raise + + project_url = f"{self.request.scheme}://{self.request.get_host()}{resolve_url('dashboard')}?project={project.id}" + log_to_channel(f":shipitparrot: Project *<{project_url}|{project.name}> shipped with *{project.time_logged} minutes*") + return redirect('fr.projects.detail', project.id) diff --git a/twisted/twisted_site/views/client/projects.py b/twisted/twisted_site/views/client/projects.py index 4aa49ae..fd206e7 100644 --- a/twisted/twisted_site/views/client/projects.py +++ b/twisted/twisted_site/views/client/projects.py @@ -1,15 +1,16 @@ -from django.http import JsonResponse, HttpResponse +from django.http import HttpResponse +from django.shortcuts import redirect, render from django.views import View -from django.shortcuts import render, redirect +from django.shortcuts import render, redirect, resolve_url from ...models import Project, PROJECT_TYPE_CHOICES - +from ...slack import log_to_channel # Create your views here. class ListProjects(View): def get(self, request): if self.request.user.is_anonymous: - return redirect('homepage') - + return redirect("homepage") + profile = request.user.profile projects = request.user.projects.all() @@ -24,14 +25,14 @@ def get(self, request): class CreateProject(View): def get(self, request): if self.request.user.is_anonymous: - return redirect('homepage') - + return redirect("homepage") + return render(request, "client/projects/create.html") def post(self, request): if self.request.user.is_anonymous: - return redirect('homepage') - + return redirect("homepage") + project_name = request.POST["name"] project_description = request.POST["description"] project_type = request.POST["type"] @@ -39,11 +40,16 @@ def post(self, request): if project_type not in PROJECT_TYPE_CHOICES: return HttpResponse("naughty! you arent supposed to do this!") - Project.objects.create( + + project = Project.objects.create( user=request.user, project_name=project_name, project_description=project_description, project_type=project_type, ) + + project_url = f"{self.request.scheme}://{self.request.get_host()}{resolve_url('dashboard')}?project={project.id}" + + log_to_channel(f"*{request.user.profile.slack_username}* created a <{project_url}|new project>!\n- *Name*: {project_name}\n- *Description*: {project_description}\n- {project_type.title()}") - return redirect('fr.projects') + return redirect('fr.projects.detail', project.id) diff --git a/twisted/twisted_site/views/client/referrals.py b/twisted/twisted_site/views/client/referrals.py index 0ddeb28..09a2771 100644 --- a/twisted/twisted_site/views/client/referrals.py +++ b/twisted/twisted_site/views/client/referrals.py @@ -1,33 +1,33 @@ -from django.utils import timezone -from django.http import JsonResponse, HttpResponse -from django.views import View -from django.shortcuts import render, redirect -from ...models import Profile import random import string +from django.shortcuts import redirect, render +from django.views import View + +from ...models import Profile + # Create your views here. class ReferralsView(View): def get(self, request): if self.request.user.is_anonymous: return redirect("homepage") - + context = {} - - context['profile'] = profile = request.user.profile - + + context["profile"] = profile = request.user.profile + if not profile.my_referral_code: while True: - current_code = ''.join([random.choice(string.ascii_letters + string.digits) for _ in range(12)]) + current_code = "".join( + [ + random.choice(string.ascii_letters + string.digits) + for _ in range(12) + ] + ) if len(Profile.objects.filter(my_referral_code=current_code)) == 0: profile.my_referral_code = current_code profile.save() break - - return render( - request, - "client/referrals.html", - context=context - ) + return render(request, "client/referrals.html", context=context) diff --git a/twisted/twisted_site/views/image_upload.py b/twisted/twisted_site/views/image_upload.py index d325330..45ca6e5 100644 --- a/twisted/twisted_site/views/image_upload.py +++ b/twisted/twisted_site/views/image_upload.py @@ -1,16 +1,17 @@ -import json +import logging import os -import requests -from django.http import JsonResponse, HttpResponse -from django.views import View -from django.shortcuts import render, redirect -from ..models import Project, UploadedFile -from django.contrib.auth.decorators import login_required -from django.utils.text import slugify -import boto3 from pathlib import Path from uuid import uuid4 -from botocore.exceptions import ClientError, BotoCoreError + +import boto3 +from botocore.exceptions import BotoCoreError, ClientError +from django.contrib.auth.decorators import login_required +from django.http import JsonResponse +from django.utils.text import slugify + +from ..models import UploadedFile + +logger = logging.getLogger(__name__) ALLOWED_CONTENT_TYPES = {"image/png", "image/jpeg", "image/webp", "image/gif"} @@ -22,6 +23,7 @@ region_name="auto", ) + @login_required def upload_file(request): if request.method == "POST": @@ -30,7 +32,10 @@ def upload_file(request): if file.content_type not in ALLOWED_CONTENT_TYPES: return JsonResponse( - {"status": "error", "reason": "Only PNG, JPEG, WEBP, or GIF images are allowed!"} + { + "status": "error", + "reason": "Only PNG, JPEG, WEBP, or GIF images are allowed!", + } ) # The size limit is a server-side policy; never let the client raise it. @@ -46,23 +51,25 @@ def upload_file(request): # Handle upload errors if response_data.get("status") == "error": return JsonResponse(response_data) - + url = response_data["link"] filename = response_data["name"] UploadedFile.objects.create( uploaded_by=request.user, link=url, cdn_response=response_data, - uploaded_thru=request.POST.get('ref', 'unknown'), - filesize=file.size + uploaded_thru=request.POST.get("ref", "unknown"), + filesize=file.size, + ) + + return JsonResponse( + { + "status": "ok", + "link": url, + "name": filename, + "response": response_data, + } ) - - return JsonResponse({ - "status": "ok", - "link": url, - "name": filename, - "response": response_data, - }) return JsonResponse( {"status": "error", "reason": "Invalid request: No file found"} ) @@ -74,7 +81,7 @@ def upload_file(request): def _upload_fileobj(fileobj, filename, content_type, size): try: ext = Path(filename).suffix.lower() - stored_name = f"{str(uuid4())}-{size}/{slugify(Path(filename).stem)}{ext}" + stored_name = f"{uuid4()!s}-{size}/{slugify(Path(filename).stem)}{ext}" original_filename = Path(filename).stem s3.upload_fileobj( fileobj, @@ -93,15 +100,13 @@ def _upload_fileobj(fileobj, filename, content_type, size): } except (ClientError, BotoCoreError) as e: - return { - "status": "error", - "error": str(e) - } + return {"status": "error", "error": str(e)} except Exception as e: + logger.exception("Unknown error during file upload") return { "status": "error", - "error": f"Unknown Error Occurred: {str(e)}", + "error": f"Unknown Error Occurred: {e!s}", }