diff --git a/pyproject.toml b/pyproject.toml index 0793262..d4ff9cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,3 +31,10 @@ dependencies = [ [tool.ruff.lint.per-file-ignores] "*/migrations/*.py" = ["RUF012"] + +[tool.pyright] +venvPath = "." +venv = ".venv" + +[tool.pyrefly] +infer-with-first-use = false diff --git a/twisted/common/admin.py b/twisted/common/admin.py index b97a94f..846f6b4 100644 --- a/twisted/common/admin.py +++ b/twisted/common/admin.py @@ -1,2 +1 @@ - # Register your models here. diff --git a/twisted/common/models.py b/twisted/common/models.py index 35e0d64..6b20219 100644 --- a/twisted/common/models.py +++ b/twisted/common/models.py @@ -1,2 +1 @@ - # Create your models here. diff --git a/twisted/common/tests.py b/twisted/common/tests.py index 4929020..a39b155 100644 --- a/twisted/common/tests.py +++ b/twisted/common/tests.py @@ -1,2 +1 @@ - # Create your tests here. diff --git a/twisted/mysite/settings.py b/twisted/mysite/settings.py index 21e2b0a..a09c0ac 100644 --- a/twisted/mysite/settings.py +++ b/twisted/mysite/settings.py @@ -205,9 +205,9 @@ 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') +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') +SLACK_TOKEN = os.environ.get("SLACK_TOKEN") +SLACK_LOG_CHANNEL = os.environ.get("SLACK_LOG_CHANNEL") diff --git a/twisted/twisted_site/admin.py b/twisted/twisted_site/admin.py index b97a94f..846f6b4 100644 --- a/twisted/twisted_site/admin.py +++ b/twisted/twisted_site/admin.py @@ -1,2 +1 @@ - # Register your models here. diff --git a/twisted/twisted_site/ari.py b/twisted/twisted_site/ari.py index ecfd902..367179b 100644 --- a/twisted/twisted_site/ari.py +++ b/twisted/twisted_site/ari.py @@ -3,7 +3,7 @@ import json import time from collections.abc import Iterable -from typing import Literal +from typing import Any, Literal import requests from django.conf import settings @@ -83,13 +83,13 @@ def send_ship(ship: ProjectShip): untracked_time = 0 if ship.project.project_type == "hardware": - for journal in ship.project.journals.filter(type="untracked"): + for journal in ship.project.journals.filter(type="untracked"): # pyrefly: ignore[missing-attribute] untracked_time += journal.reduced_minutes maker = { - "email": ship.project.user.email, - "name": ship.project.user.profile.slack_username, - "slack_id": ship.project.user.profile.slack_id, + "email": ship.project.user.email, # pyrefly: ignore[missing-attribute] + "name": ship.project.user.profile.slack_username, # pyrefly: ignore[missing-attribute] + "slack_id": ship.project.user.profile.slack_id, # pyrefly: ignore[missing-attribute] "program_hours": untracked_time / 60, } @@ -112,9 +112,9 @@ def send_ship(ship: ProjectShip): } journals = [] - orm_journals: Iterable[Journal] = ship.project.journals.all() + orm_journals: Iterable[Journal] = ship.project.journals.all() # pyrefly: ignore[missing-attribute] for journal in orm_journals: - content = f"# Journal type: {journal.get_type_display()}\n\n{journal.content}" + content = f"# Journal type: {journal.get_type_display()}\n\n{journal.content}" # ty:ignore[unresolved-attribute] # pyright: ignore[reportAttributeAccessIssue] journals.append( { "at": journal.created_at.isoformat(), @@ -146,7 +146,7 @@ def send_ship(ship: ProjectShip): 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}") # ty:ignore[unresolved-attribute] # pyright: ignore[reportAttributeAccessIssue] _resp = r.content r.raise_for_status() return r.json() @@ -163,14 +163,15 @@ def get_project_status(project: Project): } -def ship_passes_from_status(status: dict | None) -> tuple[str, str]: +def ship_passes_from_status(status: dict[str, Any] | None) -> tuple[str, str]: """Maps an ARI /status response into (first_pass_status, second_pass_status), using the PROJECT_SHIP_STATUSES vocabulary (pending/approved/rejected/requested_changes).""" if not status: return "pending", "pending" phase = status.get("phase") - decision = _ARI_DECISION_TO_SHIP_STATUS.get(status.get("decision"), "pending") + raw_decision = status.get("decision", "pending") + decision = _ARI_DECISION_TO_SHIP_STATUS.get(raw_decision, "pending") if phase == "second_pass": return decision, "pending" diff --git a/twisted/twisted_site/migrations/0031_profilestaffpermissions_profile_staff_permissions.py b/twisted/twisted_site/migrations/0031_profilestaffpermissions_profile_staff_permissions.py index 244d4fa..d661f9e 100644 --- a/twisted/twisted_site/migrations/0031_profilestaffpermissions_profile_staff_permissions.py +++ b/twisted/twisted_site/migrations/0031_profilestaffpermissions_profile_staff_permissions.py @@ -5,22 +5,34 @@ class Migration(migrations.Migration): - dependencies = [ - ('twisted_site', '0030_alter_journal_minutes_worked_and_more'), + ("twisted_site", "0030_alter_journal_minutes_worked_and_more"), ] operations = [ migrations.CreateModel( - name='ProfileStaffPermissions', + name="ProfileStaffPermissions", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('superuser', models.BooleanField(default=False)), + ( + "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'), + 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 index 358899e..197b069 100644 --- a/twisted/twisted_site/migrations/0032_profilestaffpermissions_manage_announcements_and_more.py +++ b/twisted/twisted_site/migrations/0032_profilestaffpermissions_manage_announcements_and_more.py @@ -4,55 +4,54 @@ class Migration(migrations.Migration): - dependencies = [ - ('twisted_site', '0031_profilestaffpermissions_profile_staff_permissions'), + ("twisted_site", "0031_profilestaffpermissions_profile_staff_permissions"), ] operations = [ migrations.AddField( - model_name='profilestaffpermissions', - name='manage_announcements', + model_name="profilestaffpermissions", + name="manage_announcements", field=models.BooleanField(default=False), ), migrations.AddField( - model_name='profilestaffpermissions', - name='manage_fulfillments', + model_name="profilestaffpermissions", + name="manage_fulfillments", field=models.BooleanField(default=False), ), migrations.AddField( - model_name='profilestaffpermissions', - name='manage_pathways', + model_name="profilestaffpermissions", + name="manage_pathways", field=models.BooleanField(default=False), ), migrations.AddField( - model_name='profilestaffpermissions', - name='manage_review', + model_name="profilestaffpermissions", + name="manage_review", field=models.BooleanField(default=False), ), migrations.AddField( - model_name='profilestaffpermissions', - name='manage_shop', + model_name="profilestaffpermissions", + name="manage_shop", field=models.BooleanField(default=False), ), migrations.AddField( - model_name='profilestaffpermissions', - name='view_auditlogs', + model_name="profilestaffpermissions", + name="view_auditlogs", field=models.BooleanField(default=False), ), migrations.AddField( - model_name='profilestaffpermissions', - name='view_pathways', + model_name="profilestaffpermissions", + name="view_pathways", field=models.BooleanField(default=False), ), migrations.AddField( - model_name='profilestaffpermissions', - name='view_review', + model_name="profilestaffpermissions", + name="view_review", field=models.BooleanField(default=False), ), migrations.AddField( - model_name='profilestaffpermissions', - name='view_users', + 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 85a3816..d0d3177 100644 --- a/twisted/twisted_site/models.py +++ b/twisted/twisted_site/models.py @@ -1,4 +1,5 @@ from django.contrib.auth import get_user_model +from django.contrib.auth.base_user import AbstractBaseUser from django.core.validators import MinValueValidator from django.db import models from django.db.models import TextField @@ -8,6 +9,7 @@ User = get_user_model() + class UploadedFile(models.Model): uploaded_by = models.ForeignKey(User, on_delete=models.PROTECT) link = models.CharField(max_length=500) @@ -16,7 +18,7 @@ class UploadedFile(models.Model): filesize = models.IntegerField() def __str__(self): - return f"{self.cdn_response['filename']} uploaded by {self.uploaded_by.profile.slack_username}" + return f"{self.cdn_response['filename']} uploaded by {self.uploaded_by.profile.slack_username}" # pyrefly: ignore[missing-attribute] # Create your models here. @@ -32,11 +34,16 @@ class Profile(models.Model): hackatime_state = models.CharField(max_length=100, blank=True, default="") hca_access_token = models.CharField(max_length=2000, blank=True, default="") - + 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) - + staff_permissions = models.OneToOneField( + "twisted_site.ProfileStaffPermissions", + on_delete=models.PROTECT, + default=None, + null=True, + ) + twists = models.IntegerField(default=0) referred_by = models.ForeignKey( @@ -50,14 +57,14 @@ class Profile(models.Model): def shipped_projects(self): shipped_projects = [] - for project in self.user.projects.all(): + for project in self.user.projects.all(): # pyrefly: ignore[missing-attribute] if project.is_shipped(): shipped_projects.append(project) return shipped_projects def time_logged(self): time_logged = 0 - for project in self.user.projects.all(): + for project in self.user.projects.all(): # pyrefly: ignore[missing-attribute] time_logged += project.time_logged() return time_logged @@ -68,24 +75,24 @@ def time_shipped(self): return time_shipped def __str__(self): - return self.user.username # ty:ignore[unresolved-attribute] + return self.user.username # pyrefly: ignore[missing-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) @@ -116,7 +123,7 @@ def __str__(self): def get_hackatime_project(self) -> hackatime.HackatimeProject | None: if not self.hackatime_project_name: return - projects = hackatime.projects(self.user.profile.hackatime_access_token) + projects = hackatime.projects(self.user.profile.hackatime_access_token) # pyrefly: ignore[missing-attribute] for project in projects: if project.name == self.hackatime_project_name: return project @@ -124,7 +131,7 @@ def get_hackatime_project(self) -> hackatime.HackatimeProject | None: def time_logged(self, include_all_minutes=False): minutes = 0 - for journal in self.journals.all(): # ty:ignore[unresolved-attribute] + for journal in self.journals.all(): # ty:ignore[unresolved-attribute] # pyright: ignore[reportAttributeAccessIssue] # pyrefly: ignore[missing-attribute] if include_all_minutes: # django-orm-lens-disable-next-line DOL007 minutes += journal.minutes_worked @@ -134,7 +141,7 @@ def time_logged(self, include_all_minutes=False): def hackatime_logged(self, include_all_minutes=False): minutes = 0 - for journal in self.journals.all(): # ty:ignore[unresolved-attribute] + for journal in self.journals.all(): # ty:ignore[unresolved-attribute] # pyright: ignore[reportAttributeAccessIssue] # pyrefly: ignore[missing-attribute] if journal.type != "hackatime": continue if include_all_minutes: @@ -154,7 +161,7 @@ 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() # ty:ignore[unresolved-attribute] # pyright: ignore[reportAttributeAccessIssue] # pyrefly: ignore[missing-attribute] return ship def is_shipped(self): @@ -169,12 +176,14 @@ def is_approved(self): return False return latest_ship.status == "approved" + JOURNAL_TYPES = { "hackatime": "Hackatime", "lookout": "Lookout", "untracked": "Untracked", } + class Journal(models.Model): project = models.ForeignKey( Project, on_delete=models.PROTECT, related_name="journals" @@ -223,7 +232,7 @@ class ProjectShip(models.Model): final_audit_note = models.TextField(blank=True, default="") def __str__(self): - return f"Ship created at {self.created_at} ({self.get_status_display()})" # ty:ignore[unresolved-attribute] + return f"Ship created at {self.created_at} ({self.get_status_display()})" # ty:ignore[unresolved-attribute] # pyright: ignore[reportAttributeAccessIssue] class Pathway(models.Model): @@ -253,7 +262,7 @@ def status(self): if self.in_progress(): return "in progress" - def mins_spent(self, user: User): + def mins_spent(self, user: AbstractBaseUser): pathways = Pathway.objects.order_by("start").values( "id", "start", "end", "min_mins" ) @@ -291,7 +300,7 @@ def mins_spent(self, user: User): mins_remaining -= mins_donated pathway_totals[p_id] = mins_completed + mins_donated - return pathway_totals[self.id] + return pathway_totals[self.id] # ty:ignore[unresolved-attribute] # pyright: ignore[reportAttributeAccessIssue] def mins_spent_per_participant(self) -> dict[int, int]: """ @@ -349,7 +358,7 @@ def mins_spent_per_participant(self) -> dict[int, int]: # Extract only this pathway's result for each participant return { - user_id: totals.get(self.id, 0) + user_id: totals.get(self.id, 0) # ty:ignore[unresolved-attribute] # pyright: ignore[reportAttributeAccessIssue] for user_id, totals in user_pathway_totals.items() } @@ -375,4 +384,4 @@ class AuditLog(models.Model): additional_context = models.JSONField(null=True, default=None) def __str__(self): - return f"Audit log for {self.user.profile.slack_username}. PII: {self.pii}" + return f"Audit log for {self.user.profile.slack_username}. PII: {self.pii}" # pyrefly: ignore[missing-attribute] diff --git a/twisted/twisted_site/slack.py b/twisted/twisted_site/slack.py index 15ad87b..6a1000a 100644 --- a/twisted/twisted_site/slack.py +++ b/twisted/twisted_site/slack.py @@ -1,5 +1,3 @@ -from typing import Any - from django.conf import settings from slack_sdk import WebClient @@ -8,8 +6,11 @@ slack_bot = WebClient(token=SLACK_TOKEN) + def log_to_channel(message): if settings.DEBUG: - slack_bot.chat_postMessage(channel=SLACK_LOG_CHANNEL, text=message, username="[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 + slack_bot.chat_postMessage(channel=SLACK_LOG_CHANNEL, text=message) diff --git a/twisted/twisted_site/templates/client/homepage.html b/twisted/twisted_site/templates/client/homepage.html index af9384d..82d3b40 100644 --- a/twisted/twisted_site/templates/client/homepage.html +++ b/twisted/twisted_site/templates/client/homepage.html @@ -85,20 +85,30 @@ -