diff --git a/.kiro/specs/black-to-ruff-migration/design.md b/.kiro/specs/black-to-ruff-migration/design.md new file mode 100644 index 00000000..b4b2a9e5 --- /dev/null +++ b/.kiro/specs/black-to-ruff-migration/design.md @@ -0,0 +1,153 @@ +# Design Document + +## Overview + +This design outlines the migration from Black and isort to Ruff for the We All Code Django project. Ruff is a fast Python linter and formatter written in Rust that can replace both Black (formatting) and isort (import sorting) with a single tool. The migration will maintain existing code style preferences while consolidating tooling and improving performance. + +## Architecture + +### Tool Replacement Strategy + +The migration follows a direct replacement approach: + +- **Black** → **Ruff formatter** (maintains Black-compatible formatting) +- **isort** → **Ruff import sorting** (maintains isort-compatible import organization) + +### Configuration Approach + +Ruff will be configured in `pyproject.toml` using the `[tool.ruff]` section with subsections for: + +- General settings (line length, target Python version, exclusions) +- Formatter settings (`[tool.ruff.format]`) +- Import sorting settings (`[tool.ruff.isort]`) +- Linting rules (`[tool.ruff.lint]`) + +## Components and Interfaces + +### Configuration Files + +#### pyproject.toml Updates + +**Rationale**: Centralizing all tool configuration in pyproject.toml follows Python packaging standards and simplifies maintenance. + +- Remove `[tool.black]` section +- Remove `[tool.isort]` section +- Add comprehensive `[tool.ruff]` configuration +- Add Ruff as a development dependency in the "# Development & Debugging" section alongside django-debug-toolbar +- Remove Black and isort from dependencies if present + +#### Pre-commit Configuration Updates + +**Rationale**: Maintaining pre-commit integration ensures code quality checks remain automated and consistent across the development team. + +- Replace Black hook (currently using psf/black rev 23.3.0) with Ruff formatter hook +- Replace isort hook (currently using pycqa/isort rev 5.12.0) with Ruff import sorting hook +- Maintain existing exclusion patterns for migrations and .vscode folders +- Keep all other pre-commit hooks unchanged (trailing-whitespace, end-of-file-fixer, etc.) + +#### Documentation Updates + +**Rationale**: Comprehensive documentation updates ensure all team members and new contributors understand the current tooling and maintain consistency across the project. + +- Update `.kiro/steering/tech.md` Code Quality Tools section to reference Ruff instead of Black and isort +- Update `.kiro/steering/structure.md` Development Conventions section to reference Ruff formatting +- Check and update `README.md` if it contains references to Black or isort +- Update any developer setup instructions to include Ruff-specific commands +- Ensure all documentation maintains consistency with Ruff usage + +### Ruff Configuration Sections + +Based on the current Black and isort configuration, Ruff will be configured as follows: + +#### Core Settings + +```toml +[tool.ruff] +target-version = "py311" +exclude = [migrations, build artifacts, etc.] +``` + +#### Import Sorting Settings + +**Rationale**: Django-aware import sorting maintains the existing project's import organization patterns while leveraging Ruff's performance benefits. This ensures proper separation of Django imports from other third-party libraries, maintaining the project's existing import organization standards. + + + +## Data Models + +No data models are affected by this migration as it only changes development tooling configuration. + +## Error Handling + +### Migration Validation + +- Verify Ruff produces equivalent formatting to Black on existing codebase +- Ensure import sorting maintains Django-aware section organization +- Test pre-commit hooks function correctly with new configuration + +### Rollback Strategy + +- Keep backup of original Black/isort configuration +- Document steps to revert if issues are discovered +- Maintain git history for easy rollback + +## Testing Strategy + +### Configuration Testing + +1. **Format Consistency Test**: Run Ruff formatter on existing codebase and verify minimal changes +2. **Import Sorting Test**: Verify Ruff import sorting maintains Django section organization +3. **Pre-commit Integration Test**: Test pre-commit hooks with Ruff configuration +4. **Exclusion Pattern Test**: Verify migrations and other excluded files are not processed + +### Validation Steps + +**Rationale**: These validation steps ensure the migration maintains code quality and formatting consistency while verifying all requirements are met. + +1. Install Ruff and configure in pyproject.toml +2. Run `docker compose run --rm app uv run ruff format --check .` on codebase to verify compatibility (respects pyproject.toml settings) +3. Run `docker compose run --rm app uv run ruff check --select I .` to test import sorting (respects pyproject.toml settings) +4. Test pre-commit hooks in containerized environment using `docker compose run --rm app pre-commit run --all-files` +5. Compare output with existing Black/isort formatting to ensure consistency +6. Verify uv commands work correctly with Ruff (addresses Requirement 4.4) +7. Confirm migrations are properly excluded from formatting and linting + +### Performance Verification + +- Measure formatting speed improvement with Ruff vs Black+isort +- Verify pre-commit hook execution time improvement + +## Implementation Considerations + +### Dependency Management + +**Rationale**: Proper dependency management ensures Ruff is available in all development environments and follows the project's existing organizational patterns. + +- Ruff will be added to the "# Development & Debugging" section in pyproject.toml dependencies (addresses Requirement 4.1, 4.3) +- Black and isort configurations will be removed from pyproject.toml (addresses Requirement 4.2) +- uv will handle Ruff installation and version management (addresses Requirement 4.4) +- Ruff will be placed appropriately within the development tools section to maintain logical grouping + +### Backward Compatibility + +- Ruff's Black-compatible formatter ensures existing code style is maintained +- Django-aware import sorting preserves current import organization +- Line length and exclusion patterns remain unchanged + +### Team Adoption + +- Developers will need to update their local pre-commit hooks +- IDE integrations may need to be updated to use Ruff instead of Black +- Documentation will guide developers through the transition + +## Requirements Traceability + +This design addresses all requirements from the requirements document: + +**Requirement 1 (Tool Replacement)**: Addressed through pyproject.toml configuration sections that replace Black and isort with Ruff while maintaining migration exclusions, and Django-aware import sorting. + +**Requirement 2 (Pre-commit Integration)**: Addressed through pre-commit configuration updates that replace Black and isort hooks with Ruff equivalents while maintaining existing exclusion patterns. + +**Requirement 3 (Documentation Updates)**: Addressed through systematic updates to steering documents, README, and developer setup instructions to reflect Ruff usage consistently. + +**Requirement 4 (Dependency Management)**: Addressed through adding Ruff to development dependencies and removing Black/isort configurations, with uv handling installation and version management. diff --git a/.kiro/specs/black-to-ruff-migration/requirements.md b/.kiro/specs/black-to-ruff-migration/requirements.md new file mode 100644 index 00000000..4602d76e --- /dev/null +++ b/.kiro/specs/black-to-ruff-migration/requirements.md @@ -0,0 +1,52 @@ +# Requirements Document + +## Introduction + +This feature involves migrating the We All Code Django project from using Black (code formatter) and isort (import sorter) to Ruff, which is a faster, all-in-one Python linter and formatter that can replace both tools. Ruff provides the same formatting capabilities as Black while also offering linting and import sorting functionality in a single, faster tool. + +## Requirements + +### Requirement 1: Tool Replacement + +**User Story:** As a developer, I want to use Ruff instead of Black and isort, so that I have faster code formatting and linting with a single tool. + +#### Acceptance Criteria + +1. WHEN the project is configured THEN Ruff SHALL replace Black as the code formatter +2. WHEN the project is configured THEN Ruff SHALL replace isort for import sorting +3. WHEN Ruff is configured THEN it SHALL exclude migrations from formatting (same as current Black config) +4. WHEN Ruff is configured THEN it SHALL maintain Django-aware import sorting sections with proper separation of Django imports from other third-party libraries + +### Requirement 2: Pre-commit Integration + +**User Story:** As a developer, I want the pre-commit hooks updated to use Ruff, so that code quality checks run automatically before commits. + +#### Acceptance Criteria + +1. WHEN pre-commit hooks are updated THEN they SHALL use Ruff instead of Black and isort +2. WHEN pre-commit runs THEN it SHALL format code using Ruff +3. WHEN pre-commit runs THEN it SHALL sort imports using Ruff +4. WHEN pre-commit runs THEN it SHALL maintain the same exclusion patterns as before + +### Requirement 3: Documentation Updates + +**User Story:** As a developer, I want all project documentation updated to reflect the Ruff migration, so that new contributors understand the current tooling and existing developers have accurate reference materials. + +#### Acceptance Criteria + +1. WHEN documentation is updated THEN .kiro/steering/tech.md SHALL reference Ruff instead of Black and isort in the Code Quality Tools section +2. WHEN documentation is updated THEN .kiro/steering/structure.md SHALL reference Ruff formatting conventions instead of Black +3. WHEN documentation is updated THEN README.md SHALL be updated if it contains references to Black or isort +4. WHEN documentation is updated THEN any developer setup instructions SHALL include Ruff-specific commands +5. WHEN documentation is updated THEN all references to code formatting tools SHALL be consistent with Ruff usage + +### Requirement 4: Dependency Management + +**User Story:** As a developer, I want Ruff to be added as a project dependency, so that it's available in the development environment. + +#### Acceptance Criteria + +1. WHEN dependencies are updated THEN Ruff SHALL be added to pyproject.toml +2. WHEN dependencies are updated THEN Black and isort SHALL be removed from dependencies (if present) +3. WHEN Ruff is added THEN it SHALL be in the appropriate dependency group for development tools +4. WHEN the configuration is complete THEN Ruff SHALL be usable via uv commands diff --git a/.kiro/specs/black-to-ruff-migration/tasks.md b/.kiro/specs/black-to-ruff-migration/tasks.md new file mode 100644 index 00000000..fdc00915 --- /dev/null +++ b/.kiro/specs/black-to-ruff-migration/tasks.md @@ -0,0 +1,66 @@ +# Implementation Plan + +- [x] 1. Configure Ruff in pyproject.toml + + - Remove existing [tool.black] and [tool.isort] configuration sections + - Add comprehensive [tool.ruff] configuration with general settings (target-version = "py311", exclude migrations) + - Add Ruff as a development dependency in the "# Development & Debugging" section alongside django-debug-toolbar + - Add [tool.ruff.lint] section with comprehensive linting rules + - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 4.1, 4.2, 4.3, 4.4_ + +- [x] 2. Update pre-commit configuration + + - Replace Black hook (psf/black) with Ruff formatter hook (charliermarsh/ruff-pre-commit) + - Replace isort hook (pycqa/isort) with Ruff import sorting hook using ruff-check + - Maintain existing exclusion patterns for migrations and .vscode folders + - Keep all other pre-commit hooks unchanged (trailing-whitespace, end-of-file-fixer, etc.) + - _Requirements: 2.1, 2.2, 2.3, 2.4_ + +- [x] 3. Update documentation files +- [x] 3.1 Update .kiro/steering/tech.md + + - Replace Black and isort references with Ruff in Code Quality Tools section + - Update tool descriptions to reflect Ruff's combined formatting and linting capabilities + - _Requirements: 3.1, 3.6_ + +- [x] 3.2 Update .kiro/steering/structure.md + + - Replace Black formatter references with Ruff in Development Conventions section + - Update code style documentation to reference Ruff instead of Black and isort + - Maintain Django-aware import sorting documentation + - _Requirements: 3.2, 3.6_ + +- [x] 3.3 Check and update README.md if needed + + - Search for any references to Black or isort in README.md (none found) + - Update developer setup instructions to include Ruff-specific commands if present (none needed) + - Ensure consistency with Ruff usage throughout documentation + - _Requirements: 3.3, 3.4, 3.5_ + +- [-] 4. Enhance Ruff configuration for complete migration +- [ ] 4.1 Complete pyproject.toml Ruff configuration + + - Add exclude patterns for migrations and other directories + - Add [tool.ruff.isort] section with Django-aware import sorting (known-django, section-order, combine-as-imports) + - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5_ + +- [x] 5. Test Ruff configuration +- [x] 5.1 Validate formatting compatibility + + - Run `docker compose run --rm app uv run ruff format --check .` on existing codebase to verify minimal changes + - Compare Ruff output with current formatting to ensure consistency + - Verify migrations are excluded from formatting (configured in pyproject.toml) + - _Requirements: 1.1, 1.3, 1.4_ + +- [x] 5.2 Validate import sorting + + - Run `docker compose run --rm app uv run ruff check --select I .` to test import sorting functionality + - Verify Django-aware section organization is maintained + - Test that import sorting follows isort-compatible behavior using `docker compose run --rm app uv run ruff check --select I --fix .` + - _Requirements: 1.2, 1.5_ + +- [x] 5.3 Test pre-commit integration + - Run `docker compose run --rm app pre-commit run --all-files` to test pre-commit hooks in containerized environment + - Test that Ruff hooks execute correctly and maintain exclusion patterns + - Verify pre-commit performance improvement with Ruff using `docker compose run --rm app pre-commit run ruff-format ruff-check` + - _Requirements: 2.1, 2.2, 2.3, 2.4_ diff --git a/.kiro/steering/structure.md b/.kiro/steering/structure.md index 15af98f1..c6f77284 100644 --- a/.kiro/steering/structure.md +++ b/.kiro/steering/structure.md @@ -91,8 +91,8 @@ Extends django-allauth for custom authentication: ### Code Style -- Black formatter with 79 character line length -- isort for import organization with Django-aware sections +- Ruff formatter and linter with 79 character line length +- Ruff import sorting with Django-aware sections - Migrations excluded from formatting ### Database diff --git a/.kiro/steering/tech.md b/.kiro/steering/tech.md index 1629cb1d..3ec41e09 100644 --- a/.kiro/steering/tech.md +++ b/.kiro/steering/tech.md @@ -25,8 +25,7 @@ ## Code Quality Tools -- **Black** - Code formatter (line length: 79) -- **isort** - Import sorting with Black profile +- **Ruff** - Fast Python linter and formatter with import sorting - **django-nose** - Test runner ## Common Commands diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3b866a49..9dae4a87 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,36 +1,39 @@ +exclude: "^docs/|/migrations/|devcontainer.json" +default_stages: [pre-commit] +minimum_pre_commit_version: "3.2.0" + default_language_version: python: python3.11 -# Ignore all 'migration' folders and .vscode folder -exclude: '^(\.vscode\/?)|(.*\/migrations\/.*)$' - repos: # pre-commit hooks - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.2.0 + rev: v5.0.0 hooks: - - id: no-commit-to-branch - args: [--branch, main] - id: trailing-whitespace - id: end-of-file-fixer - - id: check-yaml - id: check-json - id: check-toml + - id: check-xml + - id: check-yaml + - id: debug-statements + - id: check-builtin-literals + - id: check-case-conflict + - id: check-docstring-first + - id: detect-private-key + - id: no-commit-to-branch + args: [--branch, main] - id: check-added-large-files - id: check-merge-conflict - - id: detect-private-key - id: mixed-line-ending args: [--fix=lf] - # isort - - repo: https://github.com/pycqa/isort - rev: 5.12.0 - hooks: - - id: isort - - # black - - repo: https://github.com/psf/black - rev: 23.3.0 + # Run the Ruff linter. + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.12.5 hooks: - - id: black - args: [--preview] + # Linter + - id: ruff-check + args: [--fix, --exit-non-zero-on-fix] + # Formatter + - id: ruff-format diff --git a/accounts/urls.py b/accounts/urls.py index c72ddc2c..a85f8e99 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -1,11 +1,9 @@ from django.conf.urls import include from django.urls import path -from .views import ( - AccountHomeView, - LoginView, - SignupView, -) +from .views import AccountHomeView +from .views import LoginView +from .views import SignupView urlpatterns = [ path("", AccountHomeView.as_view(), name="account_home"), diff --git a/accounts/views.py b/accounts/views.py index eb7787c1..159ea1e7 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -1,34 +1,25 @@ -from django.conf import settings +from allauth.account.views import LoginView as AllAuthLoginView +from allauth.account.views import SignupView as AllAuthSignupView from django.contrib import messages from django.contrib.auth.decorators import login_required -from django.shortcuts import ( - get_object_or_404, - redirect, - render, -) +from django.shortcuts import get_object_or_404 +from django.shortcuts import redirect +from django.shortcuts import render +from django.urls import reverse from django.utils import timezone from django.utils.decorators import method_decorator from django.views.generic import TemplateView - -from allauth.account.views import ( - LoginView as AllAuthLoginView, - SignupView as AllAuthSignupView, -) from meta.views import MetadataMixin -from coderdojochi.forms import ( - CDCModelForm, - GuardianForm, - MentorForm, -) -from coderdojochi.models import ( - Guardian, - MeetingOrder, - Mentor, - MentorOrder, - Order, - Student, -) +from coderdojochi.forms import CDCModelForm +from coderdojochi.forms import GuardianForm +from coderdojochi.forms import MentorForm +from coderdojochi.models import Guardian +from coderdojochi.models import MeetingOrder +from coderdojochi.models import Mentor +from coderdojochi.models import MentorOrder +from coderdojochi.models import Order +from coderdojochi.models import Student class SignupView(MetadataMixin, AllAuthSignupView): @@ -65,13 +56,12 @@ def dispatch(self, *args, **kwargs): if not self.request.user.role: if "next" in self.request.GET: return redirect( - f"{reverse('welcome')}?next={self.request.GET['next']}" - ) - else: - messages.warning( - self.request, - "Tell us a little about yourself before going on account.", + f"{reverse('welcome')}?next={self.request.GET['next']}", ) + messages.warning( + self.request, + "Tell us a little about yourself before going on account.", + ) return redirect("welcome") return super().dispatch(*args, **kwargs) @@ -103,15 +93,17 @@ def get_context_data_for_mentor(self): ) upcoming_sessions = orders.filter( - is_active=True, session__start_date__gte=timezone.now() + is_active=True, + session__start_date__gte=timezone.now(), ).order_by("session__start_date") past_sessions = orders.filter( - is_active=True, session__start_date__lte=timezone.now() + is_active=True, + session__start_date__lte=timezone.now(), ).order_by("session__start_date") meeting_orders = MeetingOrder.objects.select_related().filter( - mentor=mentor + mentor=mentor, ) upcoming_meetings = meeting_orders.filter( @@ -207,11 +199,15 @@ def post_for_mentor(self, **kwargs): mentor = context["mentor"] form = MentorForm( - self.request.POST, self.request.FILES, instance=mentor + self.request.POST, + self.request.FILES, + instance=mentor, ) user_form = CDCModelForm( - self.request.POST, self.request.FILES, instance=mentor.user + self.request.POST, + self.request.FILES, + instance=mentor.user, ) if form.is_valid() and user_form.is_valid(): @@ -221,10 +217,10 @@ def post_for_mentor(self, **kwargs): return redirect("account_home") - else: - messages.error( - self.request, "There was an error. Please try again." - ) + messages.error( + self.request, + "There was an error. Please try again.", + ) context["form"] = form context["user_form"] = user_form @@ -247,10 +243,10 @@ def post_for_guardian(self, **kwargs): return redirect("account_home") - else: - messages.error( - self.request, "There was an error. Please try again." - ) + messages.error( + self.request, + "There was an error. Please try again.", + ) context["form"] = form context["user_form"] = user_form diff --git a/coderdojochi/admin.py b/coderdojochi/admin.py index ceb86344..d5052cbf 100644 --- a/coderdojochi/admin.py +++ b/coderdojochi/admin.py @@ -3,39 +3,32 @@ from django.contrib import admin from django.contrib.auth import get_user_model -from django.db.models import ( - Case, - Count, - When, -) +from django.db.models import Case +from django.db.models import Count +from django.db.models import When from django.urls import reverse from django.utils import timezone from django.utils.html import format_html - from import_export import resources -from import_export.admin import ( - ImportExportActionModelAdmin, - ImportExportMixin, -) +from import_export.admin import ImportExportActionModelAdmin +from import_export.admin import ImportExportMixin from import_export.fields import Field -from .models import ( - Course, - Donation, - Equipment, - EquipmentType, - Guardian, - Location, - Meeting, - MeetingOrder, - MeetingType, - Mentor, - MentorOrder, - Order, - RaceEthnicity, - Session, - Student, -) +from .models import Course +from .models import Donation +from .models import Equipment +from .models import EquipmentType +from .models import Guardian +from .models import Location +from .models import Meeting +from .models import MeetingOrder +from .models import MeetingType +from .models import Mentor +from .models import MentorOrder +from .models import Order +from .models import RaceEthnicity +from .models import Session +from .models import Student User = get_user_model() @@ -95,7 +88,7 @@ def role_link(self, obj): query=obj.email, role=obj.role, ) - elif obj.role == "guardian": + if obj.role == "guardian": return format_html( '{role}', url=reverse("admin:coderdojochi_guardian_changelist"), @@ -174,9 +167,9 @@ def get_queryset(self, request): When( mentororder__is_active=True, then=1, - ) - ) - ) + ), + ), + ), ) return qs @@ -195,7 +188,8 @@ def user_link(self, obj): return format_html( '{user}', url=reverse( - "admin:coderdojochi_cdcuser_change", args=(obj.user.id,) + "admin:coderdojochi_cdcuser_change", + args=(obj.user.id,), ), user=obj.user, ) @@ -305,7 +299,7 @@ def get_queryset(self, request): qs = super(GuardianAdmin, self).get_queryset(request) qs = qs.select_related() qs = qs.annotate(student_count=Count("student")).order_by( - "-student_count" + "-student_count", ) return qs @@ -313,7 +307,8 @@ def user_link(self, obj): return format_html( '{name}', url=reverse( - "admin:coderdojochi_cdcuser_change", args=(obj.user.id,) + "admin:coderdojochi_cdcuser_change", + args=(obj.user.id,), ), name=obj.user, ) @@ -387,7 +382,7 @@ def import_obj(self, obj, data, dry_run): obj.guardian = Guardian.objects.get(user__email=guardian_email) except Guardian.DoesNotExist: raise ImportError( - f"guardian with email {guardian_email} not found" + f"guardian with email {guardian_email} not found", ) if not dry_run: @@ -467,9 +462,9 @@ def get_queryset(self, request): When( order__is_active=True, then=1, - ) - ) - ) + ), + ), + ), ) return qs @@ -477,7 +472,8 @@ def guardian_link(self, obj): return format_html( '{name}', url=reverse( - "admin:coderdojochi_guardian_change", args=(obj.guardian.id,) + "admin:coderdojochi_guardian_change", + args=(obj.guardian.id,), ), name=obj.guardian.full_name, ) @@ -624,7 +620,7 @@ class SessionAdmin(ImportExportMixin, ImportExportActionModelAdmin): "cost", "minimum_cost", "maximum_cost", - ) + ), }, ), ( @@ -673,7 +669,8 @@ def mentor_count_link(self, obj): url=reverse("admin:coderdojochi_mentororder_changelist"), query=obj.id, count=MentorOrder.objects.filter( - session__id=obj.id, is_active=True + session__id=obj.id, + is_active=True, ).count(), ) @@ -762,7 +759,8 @@ def get_student_link(self, obj): return format_html( '{student}', url=reverse( - "admin:coderdojochi_student_change", args=(obj.student.id,) + "admin:coderdojochi_student_change", + args=(obj.student.id,), ), student=obj.student, ) @@ -773,7 +771,8 @@ def get_guardian_link(self, obj): return format_html( '{guardian}', url=reverse( - "admin:coderdojochi_guardian_change", args=(obj.guardian.id,) + "admin:coderdojochi_guardian_change", + args=(obj.guardian.id,), ), guardian=obj.guardian, ) diff --git a/coderdojochi/cron.py b/coderdojochi/cron.py index dfcea363..a16a6672 100644 --- a/coderdojochi/cron.py +++ b/coderdojochi/cron.py @@ -1,19 +1,14 @@ from datetime import timedelta +import arrow from django.conf import settings from django.utils import timezone +from django_cron import CronJobBase +from django_cron import Schedule -import arrow -from django_cron import ( - CronJobBase, - Schedule, -) - -from coderdojochi.models import ( - MentorOrder, - Order, - Session, -) +from coderdojochi.models import MentorOrder +from coderdojochi.models import Order +from coderdojochi.models import Session from coderdojochi.util import email @@ -69,9 +64,7 @@ def do(self): .format("dddd, MMMM D, YYYY") ), "class_start_time": ( - arrow.get(order.session.start_date) - .to("local") - .format("h:mma") + arrow.get(order.session.start_date).to("local").format("h:mma") ), "class_end_date": ( arrow.get(order.session.end_date) @@ -79,9 +72,7 @@ def do(self): .format("dddd, MMMM D, YYYY") ), "class_end_time": ( - arrow.get(order.session.end_date) - .to("local") - .format("h:mma") + arrow.get(order.session.end_date).to("local").format("h:mma") ), "class_location_name": order.session.location.name, "class_location_address": order.session.location.address, @@ -89,9 +80,7 @@ def do(self): "class_location_state": order.session.location.state, "class_location_zip": order.session.location.zip, "class_additional_info": order.session.additional_info, - "class_url": ( - f"{settings.SITE_URL}{order.session.get_absolute_url()}" - ), + "class_url": (f"{settings.SITE_URL}{order.session.get_absolute_url()}"), "class_calendar_url": ( f"{settings.SITE_URL}{order.session.get_calendar_url()}" ), @@ -103,9 +92,7 @@ def do(self): ), "order_id": order.id, "online_video_link": order.session.online_video_link, - "online_video_description": ( - order.session.online_video_description - ), + "online_video_description": (order.session.online_video_description), } email( @@ -141,9 +128,7 @@ def do(self): .format("dddd, MMMM D, YYYY") ), "class_start_time": ( - arrow.get(order.session.start_date) - .to("local") - .format("h:mma") + arrow.get(order.session.start_date).to("local").format("h:mma") ), "class_end_date": ( arrow.get(order.session.end_date) @@ -151,9 +136,7 @@ def do(self): .format("dddd, MMMM D, YYYY") ), "class_end_time": ( - arrow.get(order.session.end_date) - .to("local") - .format("h:mma") + arrow.get(order.session.end_date).to("local").format("h:mma") ), "class_location_name": order.session.location.name, "class_location_address": order.session.location.address, @@ -161,9 +144,7 @@ def do(self): "class_location_state": order.session.location.state, "class_location_zip": order.session.location.zip, "class_additional_info": order.session.additional_info, - "class_url": ( - f"{settings.SITE_URL}{order.session.get_absolute_url()}" - ), + "class_url": (f"{settings.SITE_URL}{order.session.get_absolute_url()}"), "class_calendar_url": ( f"{settings.SITE_URL}{order.session.get_calendar_url()}" ), @@ -175,9 +156,7 @@ def do(self): ), "order_id": order.id, "online_video_link": order.session.online_video_link, - "online_video_description": ( - order.session.online_video_description - ), + "online_video_description": (order.session.online_video_description), } email( @@ -237,14 +216,10 @@ def do(self): "class_url": f"{settings.SITE_URL}{order.session.get_absolute_url()}", "class_calendar_url": f"{settings.SITE_URL}{order.session.get_calendar_url()}", "microdata_start_date": ( - arrow.get(order.session.start_date) - .to("local") - .isoformat() + arrow.get(order.session.start_date).to("local").isoformat() ), "microdata_end_date": ( - arrow.get(order.session.end_date) - .to("local") - .isoformat() + arrow.get(order.session.end_date).to("local").isoformat() ), "order_id": order.id, "online_video_link": order.session.online_video_link, @@ -305,14 +280,10 @@ def do(self): "class_url": f"{settings.SITE_URL}{order.session.get_absolute_url()}", "class_calendar_url": f"{settings.SITE_URL}{order.session.get_calendar_url()}", "microdata_start_date": ( - arrow.get(order.session.start_date) - .to("local") - .isoformat() + arrow.get(order.session.start_date).to("local").isoformat() ), "microdata_end_date": ( - arrow.get(order.session.end_date) - .to("local") - .isoformat() + arrow.get(order.session.end_date).to("local").isoformat() ), "order_id": order.id, "online_video_link": order.session.online_video_link, diff --git a/coderdojochi/custom_storages.py b/coderdojochi/custom_storages.py index ecbf465f..7d4962bd 100644 --- a/coderdojochi/custom_storages.py +++ b/coderdojochi/custom_storages.py @@ -1,5 +1,4 @@ from django.conf import settings - from storages.backends.s3boto import S3BotoStorage diff --git a/coderdojochi/factories.py b/coderdojochi/factories.py index c40049c3..a42aec41 100644 --- a/coderdojochi/factories.py +++ b/coderdojochi/factories.py @@ -3,14 +3,12 @@ import factory from pytz import utc -from .models import ( - CDCUser, - Course, - Location, - Mentor, - PartnerPasswordAccess, - Session, -) +from .models import CDCUser +from .models import Course +from .models import Location +from .models import Mentor +from .models import PartnerPasswordAccess +from .models import Session class CourseFactory(factory.DjangoModelFactory): diff --git a/coderdojochi/forms.py b/coderdojochi/forms.py index 7db7c0c6..597481dd 100644 --- a/coderdojochi/forms.py +++ b/coderdojochi/forms.py @@ -1,37 +1,30 @@ import re +import html5.forms.widgets as html5_widgets +from dateutil.relativedelta import relativedelta from django import forms from django.contrib.auth import get_user_model from django.core.files.images import get_image_dimensions -from django.forms import ( - FileField, - Form, - ModelForm, - ValidationError, -) +from django.forms import FileField +from django.forms import Form +from django.forms import ModelForm +from django.forms import ValidationError from django.urls import reverse_lazy -from django.utils import ( - dateformat, - timezone, -) +from django.utils import dateformat +from django.utils import timezone from django.utils.html import format_html from django.utils.safestring import mark_safe from django.utils.text import format_lazy - -import html5.forms.widgets as html5_widgets from django_recaptcha.fields import ReCaptchaField from django_recaptcha.widgets import ReCaptchaV3 -from dateutil.relativedelta import relativedelta -from coderdojochi.models import ( - CDCUser, - Donation, - Guardian, - Mentor, - RaceEthnicity, - Session, - Student, -) +from coderdojochi.models import CDCUser +from coderdojochi.models import Donation +from coderdojochi.models import Guardian +from coderdojochi.models import Mentor +from coderdojochi.models import RaceEthnicity +from coderdojochi.models import Session +from coderdojochi.models import Student class CDCForm(Form): @@ -42,18 +35,19 @@ def _clean_fields(self): # Each widget type knows how to retrieve its own data, because some # widgets split data over several HTML fields. value = field.widget.value_from_datadict( - self.data, self.files, self.add_prefix(name) + self.data, + self.files, + self.add_prefix(name), ) try: if isinstance(field, FileField): initial = self.initial.get(name, field.initial) value = field.clean(value, initial) + elif isinstance(value, str): + value = field.clean(value.strip()) else: - if isinstance(value, str): - value = field.clean(value.strip()) - else: - value = field.clean(value) + value = field.clean(value) self.cleaned_data[name] = value @@ -73,22 +67,23 @@ def _clean_fields(self): # Each widget type knows how to retrieve its own data, because some # widgets split data over several HTML fields. value = field.widget.value_from_datadict( - self.data, self.files, self.add_prefix(name) + self.data, + self.files, + self.add_prefix(name), ) try: if isinstance(field, FileField): initial = self.initial.get(name, field.initial) value = field.clean(value, initial) + elif isinstance(value, str): + # regex normalizes carriage return + # and cuts them to two at most + value = re.sub(r"\r\n", "\n", value) + value = re.sub(r"\n{3,}", "\n\n", value) + value = field.clean(value.strip()) else: - if isinstance(value, str): - # regex normalizes carriage return - # and cuts them to two at most - value = re.sub(r"\r\n", "\n", value) - value = re.sub(r"\n{3,}", "\n\n", value) - value = field.clean(value.strip()) - else: - value = field.clean(value) + value = field.clean(value) self.cleaned_data[name] = value @@ -135,7 +130,7 @@ class MentorForm(CDCModelForm): "placeholder": "Short Bio", "class": "form-control", "rows": 4, - } + }, ), label="Short Bio", required=False, @@ -143,7 +138,7 @@ class MentorForm(CDCModelForm): gender = forms.CharField( widget=forms.TextInput( - attrs={"placeholder": "", "class": "form-control"} + attrs={"placeholder": "", "class": "form-control"}, ), label="Gender", required=True, @@ -162,7 +157,7 @@ class MentorForm(CDCModelForm): work_place = forms.CharField( widget=forms.TextInput( - attrs={"placeholder": "", "class": "form-control"} + attrs={"placeholder": "", "class": "form-control"}, ), label="Work Place", required=False, @@ -170,7 +165,7 @@ class MentorForm(CDCModelForm): phone = forms.CharField( widget=forms.TextInput( - attrs={"placeholder": "", "class": "form-control"} + attrs={"placeholder": "", "class": "form-control"}, ), label="Phone", required=False, @@ -178,7 +173,7 @@ class MentorForm(CDCModelForm): home_address = forms.CharField( widget=forms.TextInput( - attrs={"placeholder": "", "class": "form-control"} + attrs={"placeholder": "", "class": "form-control"}, ), label="Home Address", required=False, @@ -211,29 +206,27 @@ def clean_avatar(self): if w > max_width or h > max_height: raise forms.ValidationError( f"Please use an image that is {max_width} x {max_height}px" - " or smaller." + " or smaller.", ) min_width = min_height = 500 if w < min_width or h < min_height: raise forms.ValidationError( f"Please use an image that is {min_width} x {min_height}px" - " or larger." + " or larger.", ) # validate content type main, sub = avatar.content_type.split("/") - if not ( - main == "image" and sub in ["jpeg", "pjpeg", "gif", "png"] - ): + if not (main == "image" and sub in ["jpeg", "pjpeg", "gif", "png"]): raise forms.ValidationError( - "Please use a JPEG, GIF or PNG image." + "Please use a JPEG, GIF or PNG image.", ) # validate file size if len(avatar) > (2000 * 1024): raise forms.ValidationError( - "Avatar file size may not exceed 2MB." + "Avatar file size may not exceed 2MB.", ) except AttributeError: @@ -248,21 +241,21 @@ def clean_avatar(self): class GuardianForm(CDCModelForm): phone = forms.CharField( widget=forms.TextInput( - attrs={"placeholder": "Phone Number", "class": "form-control"} + attrs={"placeholder": "Phone Number", "class": "form-control"}, ), label="Phone Number", ) zip = forms.CharField( widget=forms.TextInput( - attrs={"placeholder": "Zip Code", "class": "form-control"} + attrs={"placeholder": "Zip Code", "class": "form-control"}, ), label="Zip Code", ) gender = forms.CharField( widget=forms.TextInput( - attrs={"placeholder": "", "class": "form-control"} + attrs={"placeholder": "", "class": "form-control"}, ), label="Gender", required=True, @@ -354,12 +347,14 @@ class StudentForm(CDCModelForm): attrs={ "class": "form-control", "min": dateformat.format( - timezone.now() - relativedelta(years=19), "Y-m-d" + timezone.now() - relativedelta(years=19), + "Y-m-d", ), "max": dateformat.format( - timezone.now() - relativedelta(years=5), "Y-m-d" + timezone.now() - relativedelta(years=5), + "Y-m-d", ), - } + }, ), ) @@ -369,14 +364,13 @@ class StudentForm(CDCModelForm): "placeholder": "List any medications currently being taken.", "class": "form-control hidden", "rows": 5, - } + }, ), label=format_html( "{0} {1}", "Medications", mark_safe( - 'expand' + 'expand', ), ), required=False, @@ -394,8 +388,7 @@ class StudentForm(CDCModelForm): "{0} {1}", "Medical Conditions", mark_safe( - 'expand' + 'expand', ), ), required=False, @@ -448,7 +441,8 @@ class DonationForm(ModelForm): required=True, ) user = forms.ModelChoiceField( - queryset=CDCUser.objects.all(), required=True + queryset=CDCUser.objects.all(), + required=True, ) amount = forms.CharField(label="Amount (dollars)") diff --git a/coderdojochi/mixins.py b/coderdojochi/mixins.py index 9c9f2837..ee8a8648 100644 --- a/coderdojochi/mixins.py +++ b/coderdojochi/mixins.py @@ -3,7 +3,7 @@ from django.urls import reverse -class RoleTemplateMixin(object): +class RoleTemplateMixin: def get_template_names(self): if self.request.user.is_authenticated: if self.request.user.role == "mentor": @@ -16,7 +16,7 @@ def get_template_names(self): return [template_name] -class RoleRedirectMixin(object): +class RoleRedirectMixin: def dispatch(self, request, *args, **kwargs): session_obj = kwargs.get("session_obj") user = request.user @@ -27,9 +27,7 @@ def dispatch(self, request, *args, **kwargs): "Please select one of the following options to continue.", ) - next_url = ( - f"{reverse('welcome')}?next={session_obj.get_absolute_url()}" - ) + next_url = f"{reverse('welcome')}?next={session_obj.get_absolute_url()}" if "enroll" in request.GET: next_url += "&enroll=True" @@ -37,5 +35,7 @@ def dispatch(self, request, *args, **kwargs): return redirect(next_url) return super(RoleRedirectMixin, self).dispatch( - request, *args, **kwargs + request, + *args, + **kwargs, ) diff --git a/coderdojochi/models/course.py b/coderdojochi/models/course.py index 366709f0..db075078 100644 --- a/coderdojochi/models/course.py +++ b/coderdojochi/models/course.py @@ -1,9 +1,7 @@ from datetime import timedelta -from django.core.validators import ( - MaxValueValidator, - MinValueValidator, -) +from django.core.validators import MaxValueValidator +from django.core.validators import MinValueValidator from django.db import models from .common import CommonInfo diff --git a/coderdojochi/models/donation.py b/coderdojochi/models/donation.py index 6478c630..e971d595 100644 --- a/coderdojochi/models/donation.py +++ b/coderdojochi/models/donation.py @@ -61,23 +61,20 @@ def get_admin_url(self): def get_first_name(self): if self.user: return self.user.first_name - else: - return self.first_name + return self.first_name get_first_name.short_description = "First Name" def get_last_name(self): if self.user: return self.user.last_name - else: - return self.last_name + return self.last_name get_last_name.short_description = "Last Name" def get_email(self): if self.user: return self.user.email - else: - return self.email + return self.email get_email.short_description = "Email" diff --git a/coderdojochi/models/meeting.py b/coderdojochi/models/meeting.py index 011eb350..8ca6efbf 100644 --- a/coderdojochi/models/meeting.py +++ b/coderdojochi/models/meeting.py @@ -133,7 +133,7 @@ def get_current_mentors(self): meeting=self, ).values( "mentor__id", - ) + ), ) def get_mentor_count(self): diff --git a/coderdojochi/models/mentor.py b/coderdojochi/models/mentor.py index 334733cf..6afde80b 100644 --- a/coderdojochi/models/mentor.py +++ b/coderdojochi/models/mentor.py @@ -2,14 +2,10 @@ from django.db import models from django.urls import reverse - from stdimage.models import StdImageField -from ..notifications import ( - NewMentorBgCheckNotification, - NewMentorNotification, - NewMentorOrderNotification, -) +from ..notifications import NewMentorBgCheckNotification +from ..notifications import NewMentorNotification from .common import CommonInfo from .race_ethnicity import RaceEthnicity from .user import CDCUser @@ -143,7 +139,7 @@ def get_absolute_url(self): ) def get_avatar(self): - if self.avatar and self.avatar_approved == True: + if self.avatar and self.avatar_approved: return { "url": f"{self.avatar.url}", "thumbnail": { @@ -164,14 +160,14 @@ def get_avatar(self): "d": "mp", "r": "pg", "s": str(320), - } + }, ) full_params = urlencode( { "d": "mp", "r": "pg", "s": str(500), - } + }, ) slug_url = f"https://www.gravatar.com/avatar/{email_encoded}" diff --git a/coderdojochi/models/mentor_order.py b/coderdojochi/models/mentor_order.py index 92e60202..fb2a0501 100644 --- a/coderdojochi/models/mentor_order.py +++ b/coderdojochi/models/mentor_order.py @@ -1,5 +1,3 @@ -import os - from django.db import models from ..notifications import NewMentorOrderNotification @@ -57,7 +55,7 @@ def is_checked_in(self): def save(self, *args, **kwargs): num_orders = MentorOrder.objects.filter( - mentor__id=self.mentor.id + mentor__id=self.mentor.id, ).count() if self.pk is None and num_orders == 0: diff --git a/coderdojochi/models/session.py b/coderdojochi/models/session.py index d9655190..bf113fda 100644 --- a/coderdojochi/models/session.py +++ b/coderdojochi/models/session.py @@ -1,13 +1,10 @@ from datetime import timedelta -from django.core.validators import ( - MaxValueValidator, - MinValueValidator, -) +from django.core.validators import MaxValueValidator +from django.core.validators import MinValueValidator from django.db import models from django.urls.base import reverse from django.utils import formats -from django.utils.functional import cached_property from .common import CommonInfo @@ -99,7 +96,9 @@ class Session(CommonInfo): # Extra additional_info = models.TextField( - blank=True, null=True, help_text="Basic HTML allowed" + blank=True, + null=True, + help_text="Basic HTML allowed", ) waitlist_mentors = models.ManyToManyField( Mentor, @@ -200,8 +199,7 @@ class Session(CommonInfo): online_video_description = models.TextField( "Online Video Description", help_text=( - "Information on how to connect to the video call. Basic HTML" - " allowed." + "Information on how to connect to the video call. Basic HTML allowed." ), blank=True, null=True, @@ -274,11 +272,12 @@ def save(self, *args, **kwargs): if self.mentor_capacity is None: self.mentor_capacity = int(self.capacity / 2) - if self.mentor_capacity < 0: + # Ensure mentor_capacity is not negative + if self.mentor_capacity is None or self.mentor_capacity < 0: self.mentor_capacity = 0 - # Capacity check - if self.capacity < 0: + # Ensure capacity is not negative + if self.capacity is None or self.capacity < 0: self.capacity = 0 super(Session, self).save(*args, **kwargs) @@ -311,7 +310,9 @@ def get_checked_in_mentor_orders(self): from .mentor_order import MentorOrder return MentorOrder.objects.filter( - session=self, is_active=True, check_in__isnull=False + session=self, + is_active=True, + check_in__isnull=False, ).order_by("mentor__user__last_name") def get_current_orders(self, checked_in=None): @@ -326,11 +327,14 @@ def get_current_orders(self, checked_in=None): ) else: orders = Order.objects.filter( - is_active=True, session=self, check_in=None + is_active=True, + session=self, + check_in=None, ).order_by("student__last_name") else: orders = Order.objects.filter( - is_active=True, session=self + is_active=True, + session=self, ).order_by("check_in", "student__last_name") return orders @@ -339,9 +343,7 @@ def get_active_student_count(self): from .order import Order return ( - Order.objects.filter(is_active=True, session=self) - .values("student") - .count() + Order.objects.filter(is_active=True, session=self).values("student").count() ) def get_checked_in_students(self): diff --git a/coderdojochi/models/student.py b/coderdojochi/models/student.py index ee5451c9..22976e35 100644 --- a/coderdojochi/models/student.py +++ b/coderdojochi/models/student.py @@ -88,10 +88,7 @@ def get_age(self, date=timezone.now()): return ( date.year - self.birthday.year - - ( - (date.month, date.day) - < (self.birthday.month, self.birthday.day) - ) + - ((date.month, date.day) < (self.birthday.month, self.birthday.day)) ) get_age.short_description = "Age" @@ -102,10 +99,9 @@ def get_clean_gender(self): if self.gender.lower() in MALE: return "male" - elif self.gender.lower() in FEMALE: + if self.gender.lower() in FEMALE: return "female" - else: - return "other" + return "other" get_clean_gender.short_description = "Clean Gender" @@ -114,29 +110,28 @@ def get_clean_gender_short(self): if gender == "male": return "m" - elif gender == "female": + if gender == "female": return "f" - else: - return "o" + return "o" get_clean_gender_short.short_description = "Clean Gender Short" # returns True if the student age is between minimum_age and maximum_age def is_within_age_range( - self, minimum_age, maximum_age, date=timezone.now() + self, + minimum_age, + maximum_age, + date=timezone.now(), ): age = self.get_age(date) if age >= minimum_age and age <= maximum_age: return True - else: - return False + return False def is_within_gender_limitation(self, limitation): if limitation: if self.get_clean_gender() in [limitation.lower(), "other"]: return True - else: - return False - else: - return True + return False + return True diff --git a/coderdojochi/notifications.py b/coderdojochi/notifications.py index 3720d7a5..deb4e4c4 100644 --- a/coderdojochi/notifications.py +++ b/coderdojochi/notifications.py @@ -1,8 +1,7 @@ import logging -from django.conf import settings - import requests +from django.conf import settings logger = logging.getLogger(__name__) @@ -14,7 +13,7 @@ class SlackNotification: } def __init__(self): - self.payload = DEFAULT_PAYLOAD + self.payload = self.DEFAULT_PAYLOAD.copy() def send(self): res = requests.post( @@ -27,7 +26,7 @@ def send(self): { "msg": "Unable to send Slack notification", "error": res.content, - } + }, ) @@ -56,7 +55,7 @@ def __init__(self, mentor): }, ], }, - ] + ], } @@ -89,7 +88,7 @@ def __init__(self, mentor_order): {"type": "mrkdwn", "text": f"*Date*: \n{start_date}"}, ], }, - ] + ], } @@ -118,5 +117,5 @@ def __init__(self, mentor): }, ], }, - ] + ], } diff --git a/coderdojochi/old_views.py b/coderdojochi/old_views.py index 787b71c8..a4b5a340 100644 --- a/coderdojochi/old_views.py +++ b/coderdojochi/old_views.py @@ -4,47 +4,38 @@ from datetime import timedelta from functools import reduce +import arrow from django.conf import settings from django.contrib import messages from django.contrib.auth import get_user_model from django.contrib.auth.decorators import login_required from django.core.exceptions import ObjectDoesNotExist -from django.db.models import ( - Case, - Count, - IntegerField, - When, -) +from django.db.models import Case +from django.db.models import Count +from django.db.models import IntegerField +from django.db.models import When from django.http import HttpResponse -from django.shortcuts import ( - get_object_or_404, - redirect, - render, -) +from django.shortcuts import get_object_or_404 +from django.shortcuts import redirect +from django.shortcuts import render from django.urls import reverse from django.utils import timezone from django.views.decorators.cache import never_cache from django.views.decorators.csrf import csrf_exempt -import arrow - -from coderdojochi.forms import ( - DonationForm, - StudentForm, -) -from coderdojochi.models import ( - Donation, - Equipment, - EquipmentType, - Guardian, - Meeting, - MeetingOrder, - Mentor, - MentorOrder, - Order, - Session, - Student, -) +from coderdojochi.forms import DonationForm +from coderdojochi.forms import StudentForm +from coderdojochi.models import Donation +from coderdojochi.models import Equipment +from coderdojochi.models import EquipmentType +from coderdojochi.models import Guardian +from coderdojochi.models import Meeting +from coderdojochi.models import MeetingOrder +from coderdojochi.models import Mentor +from coderdojochi.models import MentorOrder +from coderdojochi.models import Order +from coderdojochi.models import Session +from coderdojochi.models import Student from coderdojochi.util import email logger = logging.getLogger(__name__) @@ -65,7 +56,9 @@ def home(request, template_name="home.html"): upcoming_classes = upcoming_classes[:3] return render( - request, template_name, {"upcoming_classes": upcoming_classes} + request, + template_name, + {"upcoming_classes": upcoming_classes}, ) @@ -83,7 +76,9 @@ def volunteer(request, template_name="volunteer.html"): ) upcoming_meetings = Meeting.objects.filter( - is_active=True, is_public=True, end_date__gte=timezone.now() + is_active=True, + is_public=True, + end_date__gte=timezone.now(), ).order_by("start_date")[:3] return render( @@ -99,11 +94,12 @@ def mentor_approve_avatar(request, pk=None): if not request.user.is_staff: messages.error( - request, "You do not have permissions to moderate content." + request, + "You do not have permissions to moderate content.", ) return redirect( - f"{reverse('account_login')}?next={mentor.get_approve_avatar_url()}" + f"{reverse('account_login')}?next={mentor.get_approve_avatar_url()}", ) mentor.avatar_approved = True @@ -112,24 +108,20 @@ def mentor_approve_avatar(request, pk=None): if mentor.background_check: messages.success( request, - ( - f"{mentor.full_name}'s avatar approved and their account is" - " now public." - ), + (f"{mentor.full_name}'s avatar approved and their account is now public."), ) return redirect(f"{reverse('mentors')}{mentor.id}") - else: - messages.success( - request, - ( - f"{mentor.full_name}'s avatar approved but they have yet to" - " fill out the 'background search' form." - ), - ) + messages.success( + request, + ( + f"{mentor.full_name}'s avatar approved but they have yet to" + " fill out the 'background search' form." + ), + ) - return redirect("mentors") + return redirect("mentors") @login_required @@ -138,11 +130,12 @@ def mentor_reject_avatar(request, pk=None): if not request.user.is_staff: messages.error( - request, "You do not have permissions to moderate content." + request, + "You do not have permissions to moderate content.", ) return redirect( - f"{reverse('account_login')}?next={mentor.get_reject_avatar_url()}" + f"{reverse('account_login')}?next={mentor.get_reject_avatar_url()}", ) mentor.avatar_approved = False @@ -170,7 +163,9 @@ def mentor_reject_avatar(request, pk=None): @login_required def student_detail( - request, student_id=False, template_name="student_detail.html" + request, + student_id=False, + template_name="student_detail.html", ): access = True @@ -196,7 +191,8 @@ def student_detail( if not access: return redirect("account_home") messages.error( - request, "You do not have permissions to edit this student." + request, + "You do not have permissions to edit this student.", ) if request.method == "POST": @@ -204,7 +200,8 @@ def student_detail( student.is_active = False student.save() messages.success( - request, f'Student "{student.full_name}" Deleted.' + request, + f'Student "{student.full_name}" Deleted.', ) return redirect("account_home") @@ -221,7 +218,8 @@ def student_detail( def cdc_admin(request, template_name="admin.html"): if not request.user.is_staff: messages.error( - request, "You do not have permission to access this page." + request, + "You do not have permission to access this page.", ) return redirect("weallcode-home") @@ -230,7 +228,7 @@ def cdc_admin(request, template_name="admin.html"): .annotate( num_orders=Count("order"), num_attended=Count( - Case(When(order__check_in__isnull=False, then=1)) + Case(When(order__check_in__isnull=False, then=1)), ), is_future=Case( When(start_date__gte=timezone.now(), then=1), @@ -246,7 +244,7 @@ def cdc_admin(request, template_name="admin.html"): .annotate( num_orders=Count("meetingorder"), num_attended=Count( - Case(When(meetingorder__check_in__isnull=False, then=1)) + Case(When(meetingorder__check_in__isnull=False, then=1)), ), is_future=Case( When(end_date__gte=timezone.now(), then=1), @@ -262,26 +260,23 @@ def cdc_admin(request, template_name="admin.html"): total_past_orders = orders.filter(is_active=True) total_past_orders_count = total_past_orders.count() total_checked_in_orders = orders.filter( - is_active=True, check_in__isnull=False + is_active=True, + check_in__isnull=False, ) total_checked_in_orders_count = total_checked_in_orders.count() # Genders gender_count = list( - Counter( - e.student.get_clean_gender() for e in total_checked_in_orders - ).items() + Counter(e.student.get_clean_gender() for e in total_checked_in_orders).items(), ) gender_count = sorted( - list(dict(gender_count).items()), key=operator.itemgetter(1) + list(dict(gender_count).items()), + key=operator.itemgetter(1), ) # Ages ages = sorted( - list( - e.student.get_age(e.session.start_date) - for e in total_checked_in_orders - ) + list(e.student.get_age(e.session.start_date) for e in total_checked_in_orders), ) age_count = sorted( list(dict(list(Counter(ages).items())).items()), @@ -318,7 +313,8 @@ def cdc_admin(request, template_name="admin.html"): def session_stats(request, pk, template_name="session_stats.html"): if not request.user.is_staff: messages.error( - request, "You do not have permission to access this page." + request, + "You do not have permission to access this page.", ) return redirect("weallcode-home") @@ -334,7 +330,7 @@ def session_stats(request, pk, template_name="session_stats.html"): float(current_orders_checked_in.count()) / float(session_obj.get_active_student_count()) ) - * 100 + * 100, ) else: @@ -343,13 +339,13 @@ def session_stats(request, pk, template_name="session_stats.html"): # Genders gender_count = list( Counter( - e.student.get_clean_gender() - for e in session_obj.get_current_orders() - ).items() + e.student.get_clean_gender() for e in session_obj.get_current_orders() + ).items(), ) gender_count = sorted( - list(dict(gender_count).items()), key=operator.itemgetter(1) + list(dict(gender_count).items()), + key=operator.itemgetter(1), ) # Ages @@ -357,7 +353,7 @@ def session_stats(request, pk, template_name="session_stats.html"): list( e.student.get_age(e.session.start_date) for e in session_obj.get_current_orders() - ) + ), ) age_count = sorted( @@ -371,11 +367,11 @@ def session_stats(request, pk, template_name="session_stats.html"): student_ages = [] for order in current_orders_checked_in: student_ages.append( - order.student.get_age(order.session.start_date) + order.student.get_age(order.session.start_date), ) average_age = reduce(lambda x, y: x + y, student_ages) / len( - student_ages + student_ages, ) return render( @@ -397,7 +393,8 @@ def session_stats(request, pk, template_name="session_stats.html"): def session_check_in(request, pk, template_name="session_check_in.html"): if not request.user.is_staff: messages.error( - request, "You do not have permission to access this page." + request, + "You do not have permission to access this page.", ) return redirect("weallcode-home") @@ -415,9 +412,7 @@ def session_check_in(request, pk, template_name="session_check_in.html"): f"{order.guardian.full_name}" != request.POST["order_alternate_guardian"] ): - order.alternate_guardian = request.POST[ - "order_alternate_guardian" - ] + order.alternate_guardian = request.POST["order_alternate_guardian"] order.save() else: @@ -438,22 +433,23 @@ def session_check_in(request, pk, template_name="session_check_in.html"): .filter(session_id=pk) .annotate( num_attended=Count( - Case(When(student__order__check_in__isnull=False, then=1)) + Case(When(student__order__check_in__isnull=False, then=1)), ), num_missed=Count( - Case(When(student__order__check_in__isnull=True, then=1)) + Case(When(student__order__check_in__isnull=True, then=1)), ), ) ) if active_session: active_orders = orders.filter(is_active=True).order_by( - "student__first_name" + "student__first_name", ) else: active_orders = orders.filter( - is_active=True, check_in__isnull=False + is_active=True, + check_in__isnull=False, ).order_by("student__first_name") inactive_orders = orders.filter(is_active=False).order_by("-updated_at") @@ -469,16 +465,16 @@ def session_check_in(request, pk, template_name="session_check_in.html"): list( Counter( e.student.get_clean_gender() for e in active_orders - ).items() - ) - ).items() + ).items(), + ), + ).items(), ), key=operator.itemgetter(1), ) # Ages ages = sorted( - list(e.student.get_age(e.session.start_date) for e in active_orders) + list(e.student.get_age(e.session.start_date) for e in active_orders), ) age_count = sorted( @@ -512,11 +508,14 @@ def session_check_in(request, pk, template_name="session_check_in.html"): @login_required @never_cache def session_check_in_mentors( - request, pk, template_name="session_check_in_mentors.html" + request, + pk, + template_name="session_check_in_mentors.html", ): if not request.user.is_staff: messages.error( - request, "You do not have permission to access this page." + request, + "You do not have permission to access this page.", ) return redirect("weallcode-home") @@ -546,12 +545,13 @@ def session_check_in_mentors( if active_session: active_orders = orders.filter(is_active=True).order_by( - "mentor__user__first_name" + "mentor__user__first_name", ) else: active_orders = orders.filter( - is_active=True, check_in__isnull=False + is_active=True, + check_in__isnull=False, ).order_by("mentor__user__first_name") inactive_orders = orders.filter(is_active=False).order_by("-updated_at") @@ -580,7 +580,8 @@ def session_donations(request, pk, template_name="session_donations.html"): # TODO: we should really turn this into a decorator if not request.user.is_staff: messages.error( - request, "You do not have permission to access this page." + request, + "You do not have permission to access this page.", ) return redirect("account_home") @@ -589,8 +590,9 @@ def session_donations(request, pk, template_name="session_donations.html"): default_form = DonationForm(initial={"session": session}) default_form.fields["user"].queryset = User.objects.filter( id__in=Order.objects.filter(session=session).values_list( - "guardian__user__id", flat=True - ) + "guardian__user__id", + flat=True, + ), ) form = default_form @@ -614,18 +616,22 @@ def session_donations(request, pk, template_name="session_donations.html"): @login_required @never_cache def meeting_check_in( - request, meeting_id, template_name="meeting_check_in.html" + request, + meeting_id, + template_name="meeting_check_in.html", ): if not request.user.is_staff: messages.error( - request, "You do not have permission to access this page." + request, + "You do not have permission to access this page.", ) return redirect("account_home") if request.method == "POST": if "order_id" in request.POST: order = get_object_or_404( - MeetingOrder, id=request.POST["order_id"] + MeetingOrder, + id=request.POST["order_id"], ) if order.check_in: @@ -664,7 +670,8 @@ def meeting_check_in( def session_announce_mentors(request, pk): if not request.user.is_staff: messages.error( - request, "You do not have permission to access this page." + request, + "You do not have permission to access this page.", ) return redirect("home") @@ -682,14 +689,10 @@ def session_announce_mentors(request, pk): .format("dddd, MMMM D, YYYY") ), "class_start_time": ( - arrow.get(session_obj.mentor_start_date) - .to("local") - .format("h:mma") + arrow.get(session_obj.mentor_start_date).to("local").format("h:mma") ), "class_end_date": ( - arrow.get(session_obj.end_date) - .to("local") - .format("dddd, MMMM D, YYYY") + arrow.get(session_obj.end_date).to("local").format("dddd, MMMM D, YYYY") ), "class_end_time": ( arrow.get(session_obj.end_date).to("local").format("h:mma") @@ -702,9 +705,7 @@ def session_announce_mentors(request, pk): "class_location_state": session_obj.location.state, "class_location_zip": session_obj.location.zip, "class_additional_info": session_obj.additional_info, - "class_url": ( - f"{settings.SITE_URL}{session_obj.get_absolute_url()}" - ), + "class_url": (f"{settings.SITE_URL}{session_obj.get_absolute_url()}"), "class_calendar_url": ( f"{settings.SITE_URL}{session_obj.get_calendar_url()}" ), @@ -731,8 +732,7 @@ def session_announce_mentors(request, pk): merge_global_data=merge_global_data, recipients=recipients, preheader=( - "Help us make a huge difference! A brand new class was just" - " announced." + "Help us make a huge difference! A brand new class was just announced." ), unsub_group_id=settings.SENDGRID_UNSUB_CLASSANNOUNCE, ) @@ -741,11 +741,12 @@ def session_announce_mentors(request, pk): session_obj.save() messages.success( - request, f"Session announced to {mentors.count()} mentors." + request, + f"Session announced to {mentors.count()} mentors.", ) else: - messages.warning(request, f"Session already announced.") + messages.warning(request, "Session already announced.") return redirect("cdc-admin") @@ -754,7 +755,8 @@ def session_announce_mentors(request, pk): def session_announce_guardians(request, pk): if not request.user.is_staff: messages.error( - request, "You do not have permission to access this page." + request, + "You do not have permission to access this page.", ) return redirect("home") @@ -775,9 +777,7 @@ def session_announce_guardians(request, pk): arrow.get(session_obj.start_date).to("local").format("h:mma") ), "class_end_date": ( - arrow.get(session_obj.end_date) - .to("local") - .format("dddd, MMMM D, YYYY") + arrow.get(session_obj.end_date).to("local").format("dddd, MMMM D, YYYY") ), "class_end_time": ( arrow.get(session_obj.end_date).to("local").format("h:mma") @@ -790,9 +790,7 @@ def session_announce_guardians(request, pk): "class_location_state": session_obj.location.state, "class_location_zip": session_obj.location.zip, "class_additional_info": session_obj.additional_info, - "class_url": ( - f"{settings.SITE_URL}{session_obj.get_absolute_url()}" - ), + "class_url": (f"{settings.SITE_URL}{session_obj.get_absolute_url()}"), "class_calendar_url": ( f"{settings.SITE_URL}{session_obj.get_calendar_url()}" ), @@ -828,7 +826,8 @@ def session_announce_guardians(request, pk): session_obj.save() messages.success( - request, f"Session announced to {guardians.count()} guardians!" + request, + f"Session announced to {guardians.count()} guardians!", ) else: @@ -867,7 +866,8 @@ def check_system(request): equipmentType = EquipmentType.objects.get(name="Laptop") if equipmentType: equipment, created = Equipment.objects.get_or_create( - uuid=uuid, defaults={"equipment_type": equipmentType} + uuid=uuid, + defaults={"equipment_type": equipmentType}, ) # check for blank values of last_system_update. diff --git a/coderdojochi/settings.py b/coderdojochi/settings.py index 1c007279..ad596422 100644 --- a/coderdojochi/settings.py +++ b/coderdojochi/settings.py @@ -12,11 +12,10 @@ import os -from django.conf.locale.en import formats as en_formats - import dj_database_url import django_heroku import environ +from django.conf.locale.en import formats as en_formats env = environ.Env() @@ -63,13 +62,15 @@ SECURE_HSTS_SECONDS = 518400 # https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-include-subdomains SECURE_HSTS_INCLUDE_SUBDOMAINS = env.bool( - "DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS", default=True + "DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS", + default=True, ) # https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-preload SECURE_HSTS_PRELOAD = env.bool("DJANGO_SECURE_HSTS_PRELOAD", default=True) # https://docs.djangoproject.com/en/dev/ref/middleware/#x-content-type-options-nosniff SECURE_CONTENT_TYPE_NOSNIFF = env.bool( - "DJANGO_SECURE_CONTENT_TYPE_NOSNIFF", default=True + "DJANGO_SECURE_CONTENT_TYPE_NOSNIFF", + default=True, ) # https://docs.djangoproject.com/en/dev/ref/settings/#secure-browser-xss-filter SECURE_BROWSER_XSS_FILTER = True @@ -122,7 +123,6 @@ "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", "django.contrib.redirects.middleware.RedirectFallbackMiddleware", - # Add the account middleware: "allauth.account.middleware.AccountMiddleware", ] @@ -169,7 +169,7 @@ "PASSWORD": os.environ.get("DB_PASSWORD"), "HOST": os.environ.get("DB_HOST"), "PORT": os.environ.get("DB_PORT"), - } + }, } DATABASES["default"]["ATOMIC_REQUESTS"] = True @@ -194,19 +194,13 @@ "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"), }, ] @@ -260,7 +254,7 @@ # STORAGES # ------------------------------------------------------------------------------ # https://django-storages.readthedocs.io/en/latest/#installation - INSTALLED_APPS += ["storages"] # noqa F405 + INSTALLED_APPS += ["storages"] # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings AWS_ACCESS_KEY_ID = env("AWS_ACCESS_KEY_ID") @@ -290,11 +284,8 @@ # ------------------------------------------------------------------------------ # region http://stackoverflow.com/questions/10390244/ from django.contrib.staticfiles.storage import ManifestFilesMixin - - from storages.backends.s3boto3 import ( # noqa E402 - S3Boto3Storage, - SpooledTemporaryFile, - ) + from storages.backends.s3boto3 import S3Boto3Storage + from storages.backends.s3boto3 import SpooledTemporaryFile # ManifestFilesSafeMixin = lambda: ManifestFilesMixin(manifest_strict=False) # Taken from an issue in django-storages: @@ -316,7 +307,9 @@ def _save_content(self, obj, content, parameters): # Upload the object which will auto close the content_autoclose instance super(CustomS3Storage, self)._save_content( - obj, content_autoclose, parameters + obj, + content_autoclose, + parameters, ) # Cleanup if this is fixed upstream our duplicate should always close @@ -368,9 +361,7 @@ def MediaRootS3BotoStorage(): ACCOUNT_AUTHENTICATION_METHOD = "email" ACCOUNT_USERNAME_REQUIRED = False ACCOUNT_SIGNUP_FORM_CLASS = "coderdojochi.forms.SignupForm" -SOCIALACCOUNT_ADAPTER = ( - "coderdojochi.social_account_adapter.SocialAccountAdapter" -) +SOCIALACCOUNT_ADAPTER = "coderdojochi.social_account_adapter.SocialAccountAdapter" # Email diff --git a/coderdojochi/social_account_adapter.py b/coderdojochi/social_account_adapter.py index 37580e23..52e6698b 100644 --- a/coderdojochi/social_account_adapter.py +++ b/coderdojochi/social_account_adapter.py @@ -1,6 +1,5 @@ -from django.contrib.auth import get_user_model - from allauth.socialaccount.adapter import DefaultSocialAccountAdapter +from django.contrib.auth import get_user_model User = get_user_model() diff --git a/coderdojochi/templatetags/coderdojochi_extras.py b/coderdojochi/templatetags/coderdojochi_extras.py index 1cfd422f..350394ff 100644 --- a/coderdojochi/templatetags/coderdojochi_extras.py +++ b/coderdojochi/templatetags/coderdojochi_extras.py @@ -2,6 +2,7 @@ from django import template from django.template import Template +from django.urls import NoReverseMatch from django.urls import reverse from coderdojochi.models import Order @@ -28,7 +29,9 @@ def student_session_order_count(student, session): @register.simple_tag(takes_context=True) def student_register_link(context, student, session): orders = Order.objects.filter( - student=student, session=session, is_active=True + student=student, + session=session, + is_active=True, ) url = reverse( @@ -50,16 +53,20 @@ def student_register_link(context, student, session): button_msg = "Can't make it" elif not student.is_within_age_range( - session.minimum_age, session.maximum_age, session.start_date + session.minimum_age, + session.maximum_age, + session.start_date, ) or not student.is_within_gender_limitation(session.gender_limitation): button_modifier = "btn-default" button_additional_attributes = "disabled" button_tag = "span" if not student.is_within_age_range( - session.minimum_age, session.maximum_age, session.start_date + session.minimum_age, + session.maximum_age, + session.start_date, ) and not student.is_within_gender_limitation( - session.gender_limitation + session.gender_limitation, ): title = "Limited event." message = ( @@ -75,7 +82,9 @@ def student_register_link(context, student, session): ) elif not student.is_within_age_range( - session.minimum_age, session.maximum_age, session.start_date + session.minimum_age, + session.maximum_age, + session.start_date, ): title = "Age-limited event." message = ( @@ -91,7 +100,7 @@ def student_register_link(context, student, session): ) elif not student.is_within_gender_limitation( - session.gender_limitation + session.gender_limitation, ): if session.gender_limitation == "female": title = "Girls-only event." @@ -130,8 +139,7 @@ def menu_is_active(context, pattern_or_urlname, css_class="active"): if re.search(pattern, context["request"].path): return css_class - else: - return "" + return "" @register.filter(name="phone_number") diff --git a/coderdojochi/tests/test_mentor_updates.py b/coderdojochi/tests/test_mentor_updates.py index 8294a010..6aeebc3c 100644 --- a/coderdojochi/tests/test_mentor_updates.py +++ b/coderdojochi/tests/test_mentor_updates.py @@ -1,6 +1,6 @@ -from django.test import TransactionTestCase +from unittest import mock -import mock +from django.test import TransactionTestCase from coderdojochi.models import Mentor @@ -8,7 +8,7 @@ class TestMentorAvatarUpdates(TransactionTestCase): @mock.patch("coderdojochi.signals_handlers.EmailMultiAlternatives") def test_new_mentor_no_avatar(self, mock_email): - mentor = Mentor.objects.create() + Mentor.objects.create() self.fail() # @mock.patch('coderdojochi.signals_handlers.EmailMultiAlternatives') diff --git a/coderdojochi/tests/test_password_session.py b/coderdojochi/tests/test_password_session.py index f39f0dd9..412295bd 100644 --- a/coderdojochi/tests/test_password_session.py +++ b/coderdojochi/tests/test_password_session.py @@ -1,15 +1,11 @@ from django.contrib.auth import get_user_model from django.http import HttpResponseRedirect -from django.test import ( - Client, - TestCase, -) +from django.test import Client +from django.test import TestCase from django.urls import reverse -from coderdojochi.factories import ( - PartnerPasswordAccessFactory, - SessionFactory, -) +from coderdojochi.factories import PartnerPasswordAccessFactory +from coderdojochi.factories import SessionFactory from coderdojochi.models import PartnerPasswordAccess User = get_user_model() @@ -38,7 +34,8 @@ def test_session_password_no_password(self): def test_session_password_valid_password_unauthed(self): response = self.client.post( - self.url, data={"password": self.partner_session.password} + self.url, + data={"password": self.partner_session.password}, ) self.assertIsInstance(response, HttpResponseRedirect) @@ -53,14 +50,17 @@ def test_session_password_valid_password_unauthed(self): def test_session_password_valid_password_authed(self): user = User.objects.create_user( - "user", email="email@email.com", password="pass123" + "user", + email="email@email.com", + password="pass123", ) self.assertTrue( - self.client.login(email="email@email.com", password="pass123") + self.client.login(email="email@email.com", password="pass123"), ) response = self.client.post( - self.url, data={"password": self.partner_session.password} + self.url, + data={"password": self.partner_session.password}, ) self.assertIsInstance(response, HttpResponseRedirect) @@ -68,7 +68,8 @@ def test_session_password_valid_password_authed(self): self.assertEqual(response.url, detail_url) partner_password_access = PartnerPasswordAccess.objects.get( - session=self.partner_session, user=user + session=self.partner_session, + user=user, ) self.assertIsNotNone(partner_password_access) @@ -99,10 +100,12 @@ def test_redirect_password_unauthed(self): def test_redirect_password_authed(self): User.objects.create_user( - "user", email="email@email.com", password="pass123" + "user", + email="email@email.com", + password="pass123", ) self.assertTrue( - self.client.login(email="email@email.com", password="pass123") + self.client.login(email="email@email.com", password="pass123"), ) response = self.client.get(self.url) @@ -113,14 +116,17 @@ def test_redirect_password_authed(self): def test_redirect_password_partner_password_access(self): user = User.objects.create_user( - "user", email="email@email.com", password="pass123" + "user", + email="email@email.com", + password="pass123", ) self.assertTrue( - self.client.login(email="email@email.com", password="pass123") + self.client.login(email="email@email.com", password="pass123"), ) PartnerPasswordAccessFactory.create( - user=user, session=self.partner_session + user=user, + session=self.partner_session, ) response = self.client.get(self.url) detail_url = reverse("session_password", kwargs=self.url_kwargs) diff --git a/coderdojochi/urls.py b/coderdojochi/urls.py index eb5f96f6..72162815 100644 --- a/coderdojochi/urls.py +++ b/coderdojochi/urls.py @@ -8,22 +8,18 @@ from django.views.generic import RedirectView from . import old_views -from .views import ( # SessionDetailView, - MeetingCalendarView, - MeetingDetailView, - MeetingsView, - PasswordSessionView, - SessionCalendarView, - SessionDetailView, - SessionSignUpView, - WelcomeView, - meeting_announce, - meeting_sign_up, -) -from .views.public import ( - MentorDetailView, - MentorListView, -) +from .views import MeetingCalendarView +from .views import MeetingDetailView +from .views import MeetingsView +from .views import PasswordSessionView +from .views import SessionCalendarView +from .views import SessionDetailView +from .views import SessionSignUpView +from .views import WelcomeView +from .views import meeting_announce +from .views import meeting_sign_up +from .views.public import MentorDetailView +from .views.public import MentorListView admin.autodiscover() @@ -81,10 +77,10 @@ MeetingCalendarView.as_view(), name="meeting-calendar", ), - ] + ], ), ), - ] + ], ), ), ] @@ -137,7 +133,7 @@ old_views.session_donations, name="donations", ), - ] + ], ), ), path( @@ -150,17 +146,19 @@ old_views.meeting_check_in, name="meeting-check-in", ), - ] + ], ), ), # Admin Check System # /admin/checksystem/ path( - "checksystem/", old_views.check_system, name="check-system" + "checksystem/", + old_views.check_system, + name="check-system", ), - ] + ], ), - ) + ), ] # Sessions @@ -223,7 +221,7 @@ SessionSignUpView.as_view(), name="session-sign-up", ), - ] + ], ), ), ] @@ -257,7 +255,7 @@ old_views.mentor_approve_avatar, name="mentor-approve-avatar", ), - ] + ], ), ), ] @@ -291,12 +289,10 @@ path( "robots.txt", lambda r: HttpResponse( - "User-agent: *\nDisallow:\nSitemap: " - + settings.SITE_URL - + "/sitemap.xml", + "User-agent: *\nDisallow:\nSitemap: " + settings.SITE_URL + "/sitemap.xml", content_type="text/plain", ), - ) + ), ] # Anymail @@ -337,5 +333,5 @@ import debug_toolbar urlpatterns = [ - path("__debug__/", include(debug_toolbar.urls)) + path("__debug__/", include(debug_toolbar.urls)), ] + urlpatterns diff --git a/coderdojochi/util.py b/coderdojochi/util.py index 034fd6d7..d9b2ab0b 100644 --- a/coderdojochi/util.py +++ b/coderdojochi/util.py @@ -1,12 +1,11 @@ import logging +from anymail.message import AnymailMessage from django.conf import settings from django.contrib.auth import get_user_model from django.template.loader import render_to_string from django.utils import timezone -from anymail.message import AnymailMessage - logger = logging.getLogger(__name__) User = get_user_model() @@ -27,7 +26,7 @@ def email( unsub_group_id=None, ): if not (subject and template_name and recipients): - raise NameError() + raise ValueError("Missing required parameters: 'subject', 'template_name', and 'recipients' are all required.") if not isinstance(recipients, list): raise TypeError("recipients must be a list") @@ -108,6 +107,20 @@ def email( user.save() -def batches(l, n): - for i in range(0, len(l), n): - yield l[i : i + n] +def batches(items, batch_size): + """ + Split a list into smaller batches of a specified size. + + Args: + items (list): The list of items to be split into batches + batch_size (int): The maximum number of items per batch + + Yields: + list: A batch containing up to batch_size items from the original list + + Example: + >>> list(batches([1, 2, 3, 4, 5], 2)) + [[1, 2], [3, 4], [5]] + """ + for start_index in range(0, len(items), batch_size): + yield items[start_index : start_index + batch_size] diff --git a/coderdojochi/views/calendar.py b/coderdojochi/views/calendar.py index a9778c2a..930841e6 100644 --- a/coderdojochi/views/calendar.py +++ b/coderdojochi/views/calendar.py @@ -1,14 +1,11 @@ +import arrow from django.conf import settings from django.http import HttpResponse from django.shortcuts import get_object_or_404 from django.views.generic import View - -import arrow -from icalendar import ( - Calendar, - Event, - vText, -) +from icalendar import Calendar +from icalendar import Event +from icalendar import vText class CalendarView(View): @@ -33,7 +30,8 @@ def get_location(self, request, event_obj): def get(self, request, *args, **kwargs): event_obj = get_object_or_404( - self.event_class, id=kwargs[self.event_kwarg] + self.event_class, + id=kwargs[self.event_kwarg], ) cal = Calendar() @@ -43,9 +41,7 @@ def get(self, request, *args, **kwargs): event = Event() - event["uid"] = ( - f"{self.event_type.upper()}{event_obj.id:04}@weallcode.org" - ) + event["uid"] = f"{self.event_type.upper()}{event_obj.id:04}@weallcode.org" event["summary"] = self.get_summary(request, event_obj) event["dtstart"] = self.get_dtstart(request, event_obj) event["dtend"] = self.get_dtend(request, event_obj) @@ -69,11 +65,11 @@ def get(self, request, *args, **kwargs): # Return the ICS formatted calendar response = HttpResponse( - cal.to_ical(), content_type="text/calendar", charset="utf-8" + cal.to_ical(), + content_type="text/calendar", + charset="utf-8", ) - response["Content-Disposition"] = ( - f"attachment;filename={event_slug}.ics" - ) + response["Content-Disposition"] = f"attachment;filename={event_slug}.ics" return response diff --git a/coderdojochi/views/guardian/sessions.py b/coderdojochi/views/guardian/sessions.py index c1af9bdb..39e90e4f 100644 --- a/coderdojochi/views/guardian/sessions.py +++ b/coderdojochi/views/guardian/sessions.py @@ -1,16 +1,13 @@ +import arrow from django.conf import settings from django.shortcuts import get_object_or_404 from django.views.generic import DetailView -import arrow - -from ...models import ( - Guardian, - Mentor, - MentorOrder, - Order, - Session, -) +from ...models import Guardian +from ...models import Mentor +from ...models import MentorOrder +from ...models import Order +from ...models import Session class SessionDetailView(DetailView): @@ -27,8 +24,9 @@ def get_context_data(self, **kwargs): ) > 0 context["active_mentors"] = Mentor.objects.filter( id__in=MentorOrder.objects.filter( - session=self.object, is_active=True - ).values("mentor__id") + session=self.object, + is_active=True, + ).values("mentor__id"), ) context["has_students_enrolled"] = Order.objects.filter( @@ -40,15 +38,15 @@ def get_context_data(self, **kwargs): NOW = arrow.now() session_start_time = arrow.get(self.object.start_date).to( - settings.TIME_ZONE + settings.TIME_ZONE, ) # MAX_DAYS_FOR_PARENTS (30) days before the class start time open_signup_time = session_start_time.shift( - days=-settings.MAX_DAYS_FOR_PARENTS + days=-settings.MAX_DAYS_FOR_PARENTS, ) - if NOW < open_signup_time: + if open_signup_time > NOW: context["class_not_open_for_signups"] = True context["class_time_until_open"] = open_signup_time.humanize(NOW) diff --git a/coderdojochi/views/meetings.py b/coderdojochi/views/meetings.py index 045d7099..cb67298a 100644 --- a/coderdojochi/views/meetings.py +++ b/coderdojochi/views/meetings.py @@ -1,28 +1,21 @@ import logging +import arrow from django.conf import settings from django.contrib import messages from django.contrib.auth import get_user_model from django.http import Http404 -from django.shortcuts import ( - get_object_or_404, - redirect, - render, -) +from django.shortcuts import get_object_or_404 +from django.shortcuts import redirect +from django.shortcuts import render from django.utils import timezone from django.utils.html import strip_tags -from django.views.generic import ( - DetailView, - ListView, -) - -import arrow +from django.views.generic import DetailView +from django.views.generic import ListView -from coderdojochi.models import ( - Meeting, - MeetingOrder, - Mentor, -) +from coderdojochi.models import Meeting +from coderdojochi.models import MeetingOrder +from coderdojochi.models import Mentor from coderdojochi.util import email from coderdojochi.views.calendar import CalendarView @@ -81,11 +74,12 @@ def get_context_data(self, **kwargs): mentor = get_object_or_404(Mentor, user=self.request.user) active_meeting_orders = MeetingOrder.objects.filter( - meeting=self.object, is_active=True + meeting=self.object, + is_active=True, ) context["active_meeting_orders"] = active_meeting_orders context["mentor_signed_up"] = active_meeting_orders.filter( - mentor=mentor + mentor=mentor, ).exists() return context @@ -124,7 +118,8 @@ def meeting_sign_up(request, pk, template_name="meeting_sign_up.html"): mentor = get_object_or_404(Mentor, user=request.user) meeting_orders = MeetingOrder.objects.filter( - meeting=meeting_obj, is_active=True + meeting=meeting_obj, + is_active=True, ) user_meeting_order = meeting_orders.filter(mentor=mentor) @@ -136,7 +131,9 @@ def meeting_sign_up(request, pk, template_name="meeting_sign_up.html"): if request.method == "POST": if user_signed_up: meeting_order = get_object_or_404( - MeetingOrder, meeting=meeting_obj, mentor=mentor + MeetingOrder, + meeting=meeting_obj, + mentor=mentor, ) meeting_order.is_active = False meeting_order.save() @@ -145,15 +142,13 @@ def meeting_sign_up(request, pk, template_name="meeting_sign_up.html"): else: if not settings.DEBUG: - ip = ( - request.META["HTTP_X_FORWARDED_FOR"] - or request.META["REMOTE_ADDR"] - ) + ip = request.META["HTTP_X_FORWARDED_FOR"] or request.META["REMOTE_ADDR"] else: ip = request.META["REMOTE_ADDR"] meeting_order, created = MeetingOrder.objects.get_or_create( - mentor=mentor, meeting=meeting_obj + mentor=mentor, + meeting=meeting_obj, ) meeting_order.ip = ip @@ -174,9 +169,7 @@ def meeting_sign_up(request, pk, template_name="meeting_sign_up.html"): .format("dddd, MMMM D, YYYY") ), "meeting_start_time": ( - arrow.get(meeting_obj.start_date) - .to("local") - .format("h:mma") + arrow.get(meeting_obj.start_date).to("local").format("h:mma") ), "meeting_end_date": ( arrow.get(meeting_obj.end_date) @@ -192,9 +185,7 @@ def meeting_sign_up(request, pk, template_name="meeting_sign_up.html"): "meeting_location_state": meeting_obj.location.state, "meeting_location_zip": meeting_obj.location.zip, "meeting_additional_info": meeting_obj.additional_info, - "meeting_url": ( - f"{settings.SITE_URL}{meeting_obj.get_absolute_url()}" - ), + "meeting_url": (f"{settings.SITE_URL}{meeting_obj.get_absolute_url()}"), "meeting_calendar_url": ( f"{settings.SITE_URL}{meeting_obj.get_calendar_url()}" ), @@ -230,7 +221,8 @@ def meeting_sign_up(request, pk, template_name="meeting_sign_up.html"): def meeting_announce(request, pk): if not request.user.is_staff: messages.error( - request, "You do not have permission to access this page." + request, + "You do not have permission to access this page.", ) return redirect("home") @@ -250,9 +242,7 @@ def meeting_announce(request, pk): arrow.get(meeting_obj.start_date).to("local").format("h:mma") ), "meeting_end_date": ( - arrow.get(meeting_obj.end_date) - .to("local") - .format("dddd, MMMM D, YYYY") + arrow.get(meeting_obj.end_date).to("local").format("dddd, MMMM D, YYYY") ), "meeting_end_time": ( arrow.get(meeting_obj.end_date).to("local").format("h:mma") @@ -263,9 +253,7 @@ def meeting_announce(request, pk): "meeting_location_state": meeting_obj.location.state, "meeting_location_zip": meeting_obj.location.zip, "meeting_additional_info": meeting_obj.additional_info, - "meeting_url": ( - f"{settings.SITE_URL}{meeting_obj.get_absolute_url()}" - ), + "meeting_url": (f"{settings.SITE_URL}{meeting_obj.get_absolute_url()}"), "meeting_calendar_url": ( f"{settings.SITE_URL}{meeting_obj.get_calendar_url()}" ), @@ -291,8 +279,7 @@ def meeting_announce(request, pk): merge_global_data=merge_global_data, recipients=recipients, preheader=( - "A new meeting has been announced. Come join us for some" - " amazing fun!" + "A new meeting has been announced. Come join us for some amazing fun!" ), ) @@ -300,7 +287,8 @@ def meeting_announce(request, pk): meeting_obj.save() messages.success( - request, f"Meeting announced to {mentors.count()} mentors." + request, + f"Meeting announced to {mentors.count()} mentors.", ) else: messages.warning(request, "Meeting already announced.") diff --git a/coderdojochi/views/mentor/sessions.py b/coderdojochi/views/mentor/sessions.py index 7e9e6756..0b2d887d 100644 --- a/coderdojochi/views/mentor/sessions.py +++ b/coderdojochi/views/mentor/sessions.py @@ -1,11 +1,9 @@ from django.shortcuts import get_object_or_404 from django.views.generic import DetailView -from ...models import ( - Mentor, - MentorOrder, - Session, -) +from ...models import Mentor +from ...models import MentorOrder +from ...models import Session class SessionDetailView(DetailView): @@ -23,18 +21,16 @@ def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["mentor_signed_up"] = session_orders.filter( - mentor=mentor + mentor=mentor, ).exists() - context["spots_remaining"] = ( - session.mentor_capacity - session_orders.count() - ) + context["spots_remaining"] = session.mentor_capacity - session_orders.count() context["account"] = mentor context["active_mentors"] = Mentor.objects.filter( id__in=MentorOrder.objects.filter( session=self.object, is_active=True, - ).values("mentor__id") + ).values("mentor__id"), ) return context diff --git a/coderdojochi/views/profile.py b/coderdojochi/views/profile.py index 9b465a73..8b39edb6 100644 --- a/coderdojochi/views/profile.py +++ b/coderdojochi/views/profile.py @@ -3,22 +3,16 @@ from django.contrib import messages from django.contrib.auth import get_user_model from django.contrib.auth.decorators import login_required -from django.shortcuts import ( - get_object_or_404, - redirect, -) +from django.shortcuts import get_object_or_404 +from django.shortcuts import redirect from django.utils import timezone from django.utils.decorators import method_decorator from django.views.generic import TemplateView -from coderdojochi.forms import ( - CDCModelForm, - MentorForm, -) -from coderdojochi.models import ( - Mentor, - MentorOrder, -) +from coderdojochi.forms import CDCModelForm +from coderdojochi.forms import MentorForm +from coderdojochi.models import Mentor +from coderdojochi.models import MentorOrder logger = logging.getLogger(__name__) @@ -49,7 +43,8 @@ def get_context_data(self, **kwargs): # ) past_sessions = orders.filter( - is_active=True, session__start_date__lte=timezone.now() + is_active=True, + session__start_date__lte=timezone.now(), ).order_by("session__start_date") # meeting_orders = MeetingOrder.objects.select_related().filter(mentor=mentor) @@ -76,7 +71,9 @@ def post(self, request, *args, **kwargs): form = MentorForm(request.POST, request.FILES, instance=mentor) user_form = CDCModelForm( - request.POST, request.FILES, instance=mentor.user + request.POST, + request.FILES, + instance=mentor.user, ) if form.is_valid() and user_form.is_valid(): @@ -86,5 +83,4 @@ def post(self, request, *args, **kwargs): return redirect("account_home") - else: - messages.error(request, "There was an error. Please try again.") + messages.error(request, "There was an error. Please try again.") diff --git a/coderdojochi/views/public/mentor.py b/coderdojochi/views/public/mentor.py index af12b95f..c3f49728 100644 --- a/coderdojochi/views/public/mentor.py +++ b/coderdojochi/views/public/mentor.py @@ -1,7 +1,5 @@ -from django.views.generic import ( - DetailView, - ListView, -) +from django.views.generic import DetailView +from django.views.generic import ListView from ...models import Mentor diff --git a/coderdojochi/views/public/sessions.py b/coderdojochi/views/public/sessions.py index 007e9be8..d9e44afa 100644 --- a/coderdojochi/views/public/sessions.py +++ b/coderdojochi/views/public/sessions.py @@ -1,11 +1,8 @@ -from django.shortcuts import get_object_or_404 from django.views.generic import DetailView -from ...models import ( - Mentor, - MentorOrder, - Session, -) +from ...models import Mentor +from ...models import MentorOrder +from ...models import Session class SessionDetailView(DetailView): @@ -17,8 +14,9 @@ def get_context_data(self, **kwargs): context["active_mentors"] = Mentor.objects.filter( id__in=MentorOrder.objects.filter( - session=self.object, is_active=True - ).values("mentor__id") + session=self.object, + is_active=True, + ).values("mentor__id"), ) return context diff --git a/coderdojochi/views/sessions.py b/coderdojochi/views/sessions.py index 7979e1cd..e6d6d3c2 100644 --- a/coderdojochi/views/sessions.py +++ b/coderdojochi/views/sessions.py @@ -1,48 +1,33 @@ import logging +import arrow from django.conf import settings from django.contrib import messages from django.contrib.auth import get_user_model from django.contrib.auth.decorators import login_required -from django.shortcuts import ( - get_object_or_404, - redirect, - render, -) +from django.shortcuts import get_object_or_404 +from django.shortcuts import redirect +from django.shortcuts import render from django.urls import reverse -from django.utils import timezone from django.utils.decorators import method_decorator from django.utils.html import strip_tags -from django.views.generic import ( - DetailView, - TemplateView, - View, -) -from django.views.generic.base import RedirectView - -import arrow - -from coderdojochi.mixins import ( - RoleRedirectMixin, - RoleTemplateMixin, -) -from coderdojochi.models import ( - Guardian, - Mentor, - MentorOrder, - Order, - PartnerPasswordAccess, - Session, - Student, - guardian, -) +from django.views.generic import TemplateView +from django.views.generic import View + +from coderdojochi.mixins import RoleRedirectMixin +from coderdojochi.mixins import RoleTemplateMixin +from coderdojochi.models import Guardian +from coderdojochi.models import Mentor +from coderdojochi.models import MentorOrder +from coderdojochi.models import Order +from coderdojochi.models import PartnerPasswordAccess +from coderdojochi.models import Session +from coderdojochi.models import Student from coderdojochi.util import email -from . import ( - guardian, - mentor, - public, -) +from . import guardian +from . import mentor +from . import public from .calendar import CalendarView logger = logging.getLogger(__name__) @@ -64,9 +49,7 @@ def session_confirm_mentor(request, session_obj, order): .format("dddd, MMMM D, YYYY") ), "class_start_time": ( - arrow.get(session_obj.mentor_start_date) - .to("local") - .format("h:mma") + arrow.get(session_obj.mentor_start_date).to("local").format("h:mma") ), "class_end_date": ( arrow.get(session_obj.mentor_end_date) @@ -83,9 +66,7 @@ def session_confirm_mentor(request, session_obj, order): "class_location_zip": session_obj.location.zip, "class_additional_info": session_obj.additional_info, "class_url": f"{settings.SITE_URL}{session_obj.get_absolute_url()}", - "class_calendar_url": ( - f"{settings.SITE_URL}{session_obj.get_calendar_url()}" - ), + "class_calendar_url": (f"{settings.SITE_URL}{session_obj.get_calendar_url()}"), "microdata_start_date": ( arrow.get(session_obj.mentor_start_date).to("local").isoformat() ), @@ -99,9 +80,7 @@ def session_confirm_mentor(request, session_obj, order): email( subject="Mentoring confirmation for {} class".format( - arrow.get(session_obj.mentor_start_date) - .to("local") - .format("MMMM D"), + arrow.get(session_obj.mentor_start_date).to("local").format("MMMM D"), ), template_name="class_confirm_mentor", merge_global_data=merge_global_data, @@ -120,21 +99,15 @@ def session_confirm_guardian(request, session_obj, order, student): "class_title": session_obj.course.title, "class_description": session_obj.course.description, "class_start_date": ( - arrow.get(session_obj.start_date) - .to("local") - .format("dddd, MMMM D, YYYY") + arrow.get(session_obj.start_date).to("local").format("dddd, MMMM D, YYYY") ), "class_start_time": ( arrow.get(session_obj.start_date).to("local").format("h:mma") ), "class_end_date": ( - arrow.get(session_obj.end_date) - .to("local") - .format("dddd, MMMM D, YYYY") - ), - "class_end_time": ( - arrow.get(session_obj.end_date).to("local").format("h:mma") + arrow.get(session_obj.end_date).to("local").format("dddd, MMMM D, YYYY") ), + "class_end_time": (arrow.get(session_obj.end_date).to("local").format("h:mma")), "class_location_name": session_obj.location.name, "class_location_address": session_obj.location.address, "class_location_city": session_obj.location.city, @@ -146,9 +119,7 @@ def session_confirm_guardian(request, session_obj, order, student): "microdata_start_date": ( arrow.get(session_obj.start_date).to("local").isoformat() ), - "microdata_end_date": ( - arrow.get(session_obj.end_date).to("local").isoformat() - ), + "microdata_end_date": (arrow.get(session_obj.end_date).to("local").isoformat()), "order_id": order.id, "online_video_link": session_obj.online_video_link, "online_video_description": session_obj.online_video_description, @@ -172,19 +143,23 @@ def get(self, request, *args, **kwargs): session = get_object_or_404(Session, id=pk) if session.password and not self.validate_partner_session_access( - request, pk + request, + pk, ): return redirect(reverse("session-password", kwargs=kwargs)) if request.user.is_authenticated: if request.user.role == "mentor": return mentor.SessionDetailView.as_view()( - request, *args, **kwargs - ) - else: - return guardian.SessionDetailView.as_view()( - request, *args, **kwargs + request, + *args, + **kwargs, ) + return guardian.SessionDetailView.as_view()( + request, + *args, + **kwargs, + ) return public.SessionDetailView.as_view()(request, *args, **kwargs) def validate_partner_session_access(self, request, pk): @@ -193,14 +168,16 @@ def validate_partner_session_access(self, request, pk): if authed_sessions and pk in authed_sessions: if request.user.is_authenticated: PartnerPasswordAccess.objects.get_or_create( - session_id=pk, user=request.user + session_id=pk, + user=request.user, ) return True if request.user.is_authenticated: try: PartnerPasswordAccess.objects.get( - session_id=pk, user_id=request.user.id + session_id=pk, + user_id=request.user.id, ) except PartnerPasswordAccess.DoesNotExist: return False @@ -221,21 +198,23 @@ def dispatch(self, request, *args, **kwargs): if request.user.role == "mentor": session_orders = MentorOrder.objects.filter( - session=session_obj, is_active=True + session=session_obj, + is_active=True, ) kwargs["mentor"] = get_object_or_404(Mentor, user=request.user) kwargs["user_signed_up"] = session_orders.filter( - mentor=kwargs["mentor"] + mentor=kwargs["mentor"], ).exists() elif request.user.role == "guardian": kwargs["guardian"] = get_object_or_404(Guardian, user=request.user) kwargs["student"] = get_object_or_404( - Student, id=kwargs["student_id"] + Student, + id=kwargs["student_id"], + ) + kwargs["user_signed_up"] = kwargs["student"].is_registered_for_session( + session_obj ) - kwargs["user_signed_up"] = kwargs[ - "student" - ].is_registered_for_session(session_obj) access_dict = self.check_access(request, *args, **kwargs) @@ -248,7 +227,9 @@ def dispatch(self, request, *args, **kwargs): return redirect(access_dict["redirect"]) return super(SessionSignUpView, self).dispatch( - request, *args, **kwargs + request, + *args, + **kwargs, ) def check_access(self, request, *args, **kwargs): @@ -283,7 +264,7 @@ def check_access(self, request, *args, **kwargs): def student_limitations(self, student, session_obj, user_signed_up): if not student.is_within_gender_limitation( - session_obj.gender_limitation + session_obj.gender_limitation, ): return ( "Sorry, this class is limited to" @@ -291,7 +272,8 @@ def student_limitations(self, student, session_obj, user_signed_up): ) if not student.is_within_age_range( - session_obj.minimum_age, session_obj.maximum_age + session_obj.minimum_age, + session_obj.maximum_age, ): return ( "Sorry, this class is limited to students between ages" @@ -326,7 +308,9 @@ def post(self, request, *args, **kwargs): if user_signed_up: if mentor: order = get_object_or_404( - MentorOrder, mentor=mentor, session=session_obj + MentorOrder, + mentor=mentor, + session=session_obj, ) elif student: order = get_object_or_404( @@ -343,10 +327,7 @@ def post(self, request, *args, **kwargs): ip = request.META["REMOTE_ADDR"] if not settings.DEBUG: - ip = ( - request.META["HTTP_X_FORWARDED_FOR"] - or request.META["REMOTE_ADDR"] - ) + ip = request.META["HTTP_X_FORWARDED_FOR"] or request.META["REMOTE_ADDR"] if mentor: order, created = MentorOrder.objects.get_or_create( @@ -402,7 +383,8 @@ def post(self, request, *args, **kwargs): # Get from user session or create an empty set authed_partner_sessions = request.session.get( - "authed_partner_sessions", [] + "authed_partner_sessions", + [], ) # Add course session id to user session @@ -416,7 +398,8 @@ def post(self, request, *args, **kwargs): if request.user.is_authenticated: PartnerPasswordAccess.objects.get_or_create( - session=session_obj, user=request.user + session=session_obj, + user=request.user, ) return redirect(session_obj) @@ -428,17 +411,15 @@ class SessionCalendarView(CalendarView): event_class = Session def get_summary(self, request, event_obj): - return ( - f"We All Code: {event_obj.course.code} - {event_obj.course.title}" - ) + return f"We All Code: {event_obj.course.code} - {event_obj.course.title}" def get_dtstart(self, request, event_obj): - dtstart = ( - f"{arrow.get(event_obj.start_date).format('YYYYMMDDTHHmmss')}Z" - ) + dtstart = f"{arrow.get(event_obj.start_date).format('YYYYMMDDTHHmmss')}Z" if request.user.is_authenticated and request.user.role == "mentor": - dtstart = f"{arrow.get(event_obj.mentor_start_date).format('YYYYMMDDTHHmmss')}Z" + dtstart = ( + f"{arrow.get(event_obj.mentor_start_date).format('YYYYMMDDTHHmmss')}Z" + ) return dtstart @@ -461,7 +442,9 @@ def get_location(self, request, event_obj): try: mentor = Mentor.objects.get(user=self.request.user) mentor_signed_up = MentorOrder.objects.filter( - session=event_obj, is_active=True, mentor=mentor + session=event_obj, + is_active=True, + mentor=mentor, ).exists() if mentor_signed_up: diff --git a/coderdojochi/views/welcome.py b/coderdojochi/views/welcome.py index 8a4a5ce9..84e8b5f8 100644 --- a/coderdojochi/views/welcome.py +++ b/coderdojochi/views/welcome.py @@ -3,26 +3,20 @@ from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import login_required -from django.shortcuts import ( - get_object_or_404, - redirect, - render, -) +from django.shortcuts import get_object_or_404 +from django.shortcuts import redirect +from django.shortcuts import render from django.urls import reverse from django.utils.decorators import method_decorator from django.views.generic import TemplateView -from coderdojochi.forms import ( - GuardianForm, - MentorForm, - StudentForm, -) -from coderdojochi.models import ( - Guardian, - Meeting, - Mentor, - Session, -) +from coderdojochi.forms import GuardianForm +from coderdojochi.forms import MentorForm +from coderdojochi.forms import StudentForm +from coderdojochi.models import Guardian +from coderdojochi.models import Meeting +from coderdojochi.models import Mentor +from coderdojochi.models import Session from coderdojochi.util import email logger = logging.getLogger(__name__) @@ -36,17 +30,13 @@ def dispatch(self, request, *args, **kwargs): next_url = request.GET.get("next") kwargs["next_url"] = next_url # Check for redirect condition on mentor, otherwise pass as kwarg - if ( - getattr(request.user, "role", False) == "mentor" - and request.method == "GET" - ): + if getattr(request.user, "role", False) == "mentor" and request.method == "GET": mentor = get_object_or_404(Mentor, user=request.user) if mentor.first_name: if next_url: return redirect(next_url) - else: - return redirect("account_home") + return redirect("account_home") kwargs["mentor"] = mentor return super().dispatch(request, *args, **kwargs) @@ -72,7 +62,7 @@ def get_context_data(self, **kwargs): else: context["add_student"] = True context["form"] = StudentForm( - initial={"guardian": guardian.pk} + initial={"guardian": guardian.pk}, ) if account.first_name and account.get_students(): @@ -97,8 +87,7 @@ def post(self, request, *args, **kwargs): return self.update_account(request, account, next_url) return self.add_student(request, account, next_url) - else: - return self.create_new_user(request, user, next_url) + return self.create_new_user(request, user, next_url) def update_account(self, request, account, next_url): if isinstance(account, Mentor): @@ -113,11 +102,10 @@ def update_account(self, request, account, next_url): if next_url: if "enroll" in request.GET: next_url = f"{next_url}?enroll=True" + elif isinstance(account, Mentor): + next_url = "account_home" else: - if isinstance(account, Mentor): - next_url = "account_home" - else: - next_url = "welcome" + next_url = "welcome" return redirect(next_url) return render( @@ -140,9 +128,7 @@ def add_student(self, request, account, next_url): messages.success(request, "Student Registered.") if next_url: if "enroll" in request.GET: - next_url = ( - f"{next_url}?enroll=True&student={new_student.id}" - ) + next_url = f"{next_url}?enroll=True&student={new_student.id}" else: next_url = "welcome" return redirect(next_url) @@ -206,7 +192,7 @@ def create_new_user(self, request, user, next_url): email( subject="Welcome!", - template_name=f"welcome_mentor", + template_name="welcome_mentor", merge_global_data=merge_global_data, recipients=[user.email], preheader="Welcome to We All Code! Let's get started..", @@ -214,9 +200,7 @@ def create_new_user(self, request, user, next_url): else: # check for next upcoming class next_class = ( - Session.objects.filter(is_active=True) - .order_by("start_date") - .first() + Session.objects.filter(is_active=True).order_by("start_date").first() ) if next_class: @@ -232,7 +216,7 @@ def create_new_user(self, request, user, next_url): email( subject="Welcome!", - template_name=f"welcome_guardian", + template_name="welcome_guardian", merge_global_data=merge_global_data, recipients=[user.email], preheader="Your adventure awaits!", diff --git a/manage.py b/manage.py index b3c6b29a..6a428432 100644 --- a/manage.py +++ b/manage.py @@ -10,6 +10,6 @@ raise ImportError( "Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" + "forget to activate a virtual environment?", ) from exc execute_from_command_line(sys.argv) diff --git a/pyproject.toml b/pyproject.toml index cc353430..f9d7235e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ dependencies = [ # Development & Debugging "django-debug-toolbar>=5.2.0,<6.0.0", + "ruff>=0.8.0,<1.0.0", # Testing "django-nose>=1.4.7,<2.0.0", @@ -75,37 +76,91 @@ dependencies = [ Repository = "https://github.com/weallcode/website" Homepage = "https://github.com/weallcode/website" -[tool.black] -line-length = 79 -target-version = ["py311"] -include = '\.pyi?$' -exclude = ''' -/( - \.eggs - | \.git - | \.hg - | \.mypy_cache - | \.tox - | \.venv - | _build - | buck-out - | build - | dist - | migrations -)/ -''' - -[tool.isort] -profile = "black" -line_length = 79 -multi_line_output = 3 -include_trailing_comma = true -combine_as_imports = true -force_grid_wrap = 2 -use_parentheses = true -remove_redundant_aliases = true -known_django = "django" -sections = "FUTURE,STDLIB,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER" -skip_gitignore = true -skip = [".gitignore", ".dockerignore", ".venv"] -skip_glob = ["**/migrations/*.py"] +[tool.ruff] +# Use Ruff's recommended defaults +target-version = "py311" +# Exclude a variety of commonly ignored directories. +extend-exclude = [ + "*/migrations/*.py", + "staticfiles/*", +] + +[tool.ruff.lint] +select = [ + "F", + "E", + "W", + "C90", + "I", + "N", + "UP", + "YTT", + # "ANN", # flake8-annotations: we should support this in the future but 100+ errors atm + "ASYNC", + "S", + "BLE", + "FBT", + "B", + "A", + "C4", + "DTZ", + "T10", + "DJ", + "EM", + "EXE", + "FA", + 'ISC', + "ICN", + "G", + 'INP', + 'PIE', + "T20", + 'PYI', + 'PT', + "Q", + "RSE", + "RET", + "SLF", + "SLOT", + "SIM", + "TID", + "TC", + "INT", + # "ARG", # Unused function argument + "PTH", + "ERA", + "PD", + "PGH", + "PL", + "TRY", + "FLY", + # "NPY", + # "AIR", + "PERF", + # "FURB", + # "LOG", + "RUF", +] +ignore = [ + "S101", # Use of assert detected https://docs.astral.sh/ruff/rules/assert/ + "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` + "SIM102", # sometimes it's better to nest + "UP038", # Checks for uses of isinstance/issubclass that take a tuple + # of types for comparison. + # Deactivated because it can make the code slow: + # https://github.com/astral-sh/ruff/issues/7871 + "COM812", # Trailing comma missing - conflicts with formatter +] +# The fixes in extend-unsafe-fixes will require +# provide the `--unsafe-fixes` flag when fixing. +extend-unsafe-fixes = [ + "UP038", +] + +[tool.ruff.lint.isort] +force-single-line = true + +[dependency-groups] +dev = [ + "pre-commit>=4.2.0", +] diff --git a/tasks.py b/tasks.py index ecf9c5f2..b0ef42c9 100644 --- a/tasks.py +++ b/tasks.py @@ -14,8 +14,7 @@ def release(ctx): @task(help={"port": "Port to use when serving traffic. Defaults to $PORT."}) def start(ctx, port=env.int("PORT", default=8000)): ctx.run( - f"gunicorn coderdojochi.wsgi -w 2 -b 0.0.0.0:{port} --reload" - " --log-file -" + f"gunicorn coderdojochi.wsgi -w 2 -b 0.0.0.0:{port} --reload --log-file -", ) diff --git a/uv.lock b/uv.lock index 24ca1777..834ce08e 100644 --- a/uv.lock +++ b/uv.lock @@ -84,6 +84,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" }, ] +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, +] + [[package]] name = "charset-normalizer" version = "3.4.2" @@ -156,6 +165,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/bb/2aa9b46a01197398b901e458974c20ed107935c26e44e37ad5b0e5511e44/diff_match_patch-20241021-py3-none-any.whl", hash = "sha256:93cea333fb8b2bc0d181b0de5e16df50dd344ce64828226bda07728818936782", size = 43252, upload-time = "2024-10-21T19:41:19.914Z" }, ] +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + [[package]] name = "dj-database-url" version = "3.0.1" @@ -435,6 +453,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/26/1c/b909a055be556c11f13cf058cfa0e152f9754d803ff3694a937efe300709/faker-37.4.2-py3-none-any.whl", hash = "sha256:b70ed1af57bfe988cbcd0afd95f4768c51eaf4e1ce8a30962e127ac5c139c93f", size = 1943179, upload-time = "2025-07-15T16:38:23.053Z" }, ] +[[package]] +name = "filelock" +version = "3.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, +] + [[package]] name = "gunicorn" version = "23.0.0" @@ -460,6 +487,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6c/25/b5fc00e85d2dfaf5c806ac8b5f1de072fa11630c5b15b4ae5bbc228abd51/icalendar-6.3.1-py3-none-any.whl", hash = "sha256:7ea1d1b212df685353f74cdc6ec9646bf42fa557d1746ea645ce8779fdfbecdd", size = 242349, upload-time = "2025-05-20T07:42:48.589Z" }, ] +[[package]] +name = "identify" +version = "2.6.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/88/d193a27416618628a5eea64e3223acd800b40749a96ffb322a9b55a49ed1/identify-2.6.12.tar.gz", hash = "sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6", size = 99254, upload-time = "2025-05-23T20:37:53.3Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/cd/18f8da995b658420625f7ef13f037be53ae04ec5ad33f9b718240dcfd48c/identify-2.6.12-py2.py3-none-any.whl", hash = "sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2", size = 99145, upload-time = "2025-05-23T20:37:51.495Z" }, +] + [[package]] name = "idna" version = "3.10" @@ -502,6 +538,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bd/d9/617e6af809bf3a1d468e0d58c3997b1dc219a9a9202e650d30c2fc85d481/mock-5.2.0-py3-none-any.whl", hash = "sha256:7ba87f72ca0e915175596069dbbcc7c75af7b5e9b9bc107ad6349ede0819982f", size = 31617, upload-time = "2025-03-03T12:31:41.518Z" }, ] +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, +] + [[package]] name = "nose" version = "1.3.7" @@ -555,6 +600,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" }, ] +[[package]] +name = "platformdirs" +version = "4.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424, upload-time = "2025-03-18T21:35:20.987Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" }, +] + [[package]] name = "psycopg" version = "3.2.9" @@ -636,6 +706,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, +] + [[package]] name = "requests" version = "2.32.4" @@ -651,6 +738,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, ] +[[package]] +name = "ruff" +version = "0.12.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/cd/01015eb5034605fd98d829c5839ec2c6b4582b479707f7c1c2af861e8258/ruff-0.12.5.tar.gz", hash = "sha256:b209db6102b66f13625940b7f8c7d0f18e20039bb7f6101fbdac935c9612057e", size = 5170722, upload-time = "2025-07-24T13:26:37.456Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/de/ad2f68f0798ff15dd8c0bcc2889558970d9a685b3249565a937cd820ad34/ruff-0.12.5-py3-none-linux_armv6l.whl", hash = "sha256:1de2c887e9dec6cb31fcb9948299de5b2db38144e66403b9660c9548a67abd92", size = 11819133, upload-time = "2025-07-24T13:25:56.369Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fc/c6b65cd0e7fbe60f17e7ad619dca796aa49fbca34bb9bea5f8faf1ec2643/ruff-0.12.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d1ab65e7d8152f519e7dea4de892317c9da7a108da1c56b6a3c1d5e7cf4c5e9a", size = 12501114, upload-time = "2025-07-24T13:25:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/c5/de/c6bec1dce5ead9f9e6a946ea15e8d698c35f19edc508289d70a577921b30/ruff-0.12.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:962775ed5b27c7aa3fdc0d8f4d4433deae7659ef99ea20f783d666e77338b8cf", size = 11716873, upload-time = "2025-07-24T13:26:01.496Z" }, + { url = "https://files.pythonhosted.org/packages/a1/16/cf372d2ebe91e4eb5b82a2275c3acfa879e0566a7ac94d331ea37b765ac8/ruff-0.12.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73b4cae449597e7195a49eb1cdca89fd9fbb16140c7579899e87f4c85bf82f73", size = 11958829, upload-time = "2025-07-24T13:26:03.721Z" }, + { url = "https://files.pythonhosted.org/packages/25/bf/cd07e8f6a3a6ec746c62556b4c4b79eeb9b0328b362bb8431b7b8afd3856/ruff-0.12.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b13489c3dc50de5e2d40110c0cce371e00186b880842e245186ca862bf9a1ac", size = 11626619, upload-time = "2025-07-24T13:26:06.118Z" }, + { url = "https://files.pythonhosted.org/packages/d8/c9/c2ccb3b8cbb5661ffda6925f81a13edbb786e623876141b04919d1128370/ruff-0.12.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1504fea81461cf4841778b3ef0a078757602a3b3ea4b008feb1308cb3f23e08", size = 13221894, upload-time = "2025-07-24T13:26:08.292Z" }, + { url = "https://files.pythonhosted.org/packages/6b/58/68a5be2c8e5590ecdad922b2bcd5583af19ba648f7648f95c51c3c1eca81/ruff-0.12.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c7da4129016ae26c32dfcbd5b671fe652b5ab7fc40095d80dcff78175e7eddd4", size = 14163909, upload-time = "2025-07-24T13:26:10.474Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d1/ef6b19622009ba8386fdb792c0743f709cf917b0b2f1400589cbe4739a33/ruff-0.12.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ca972c80f7ebcfd8af75a0f18b17c42d9f1ef203d163669150453f50ca98ab7b", size = 13583652, upload-time = "2025-07-24T13:26:13.381Z" }, + { url = "https://files.pythonhosted.org/packages/62/e3/1c98c566fe6809a0c83751d825a03727f242cdbe0d142c9e292725585521/ruff-0.12.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8dbbf9f25dfb501f4237ae7501d6364b76a01341c6f1b2cd6764fe449124bb2a", size = 12700451, upload-time = "2025-07-24T13:26:15.488Z" }, + { url = "https://files.pythonhosted.org/packages/24/ff/96058f6506aac0fbc0d0fc0d60b0d0bd746240a0594657a2d94ad28033ba/ruff-0.12.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c47dea6ae39421851685141ba9734767f960113d51e83fd7bb9958d5be8763a", size = 12937465, upload-time = "2025-07-24T13:26:17.808Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d3/68bc5e7ab96c94b3589d1789f2dd6dd4b27b263310019529ac9be1e8f31b/ruff-0.12.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c5076aa0e61e30f848846f0265c873c249d4b558105b221be1828f9f79903dc5", size = 11771136, upload-time = "2025-07-24T13:26:20.422Z" }, + { url = "https://files.pythonhosted.org/packages/52/75/7356af30a14584981cabfefcf6106dea98cec9a7af4acb5daaf4b114845f/ruff-0.12.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a5a4c7830dadd3d8c39b1cc85386e2c1e62344f20766be6f173c22fb5f72f293", size = 11601644, upload-time = "2025-07-24T13:26:22.928Z" }, + { url = "https://files.pythonhosted.org/packages/c2/67/91c71d27205871737cae11025ee2b098f512104e26ffd8656fd93d0ada0a/ruff-0.12.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:46699f73c2b5b137b9dc0fc1a190b43e35b008b398c6066ea1350cce6326adcb", size = 12478068, upload-time = "2025-07-24T13:26:26.134Z" }, + { url = "https://files.pythonhosted.org/packages/34/04/b6b00383cf2f48e8e78e14eb258942fdf2a9bf0287fbf5cdd398b749193a/ruff-0.12.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5a655a0a0d396f0f072faafc18ebd59adde8ca85fb848dc1b0d9f024b9c4d3bb", size = 12991537, upload-time = "2025-07-24T13:26:28.533Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b9/053d6445dc7544fb6594785056d8ece61daae7214859ada4a152ad56b6e0/ruff-0.12.5-py3-none-win32.whl", hash = "sha256:dfeb2627c459b0b78ca2bbdc38dd11cc9a0a88bf91db982058b26ce41714ffa9", size = 11751575, upload-time = "2025-07-24T13:26:30.835Z" }, + { url = "https://files.pythonhosted.org/packages/bc/0f/ab16e8259493137598b9149734fec2e06fdeda9837e6f634f5c4e35916da/ruff-0.12.5-py3-none-win_amd64.whl", hash = "sha256:ae0d90cf5f49466c954991b9d8b953bd093c32c27608e409ae3564c63c5306a5", size = 12882273, upload-time = "2025-07-24T13:26:32.929Z" }, + { url = "https://files.pythonhosted.org/packages/00/db/c376b0661c24cf770cb8815268190668ec1330eba8374a126ceef8c72d55/ruff-0.12.5-py3-none-win_arm64.whl", hash = "sha256:48cdbfc633de2c5c37d9f090ba3b352d1576b0015bfc3bc98eaf230275b7e805", size = 11951564, upload-time = "2025-07-24T13:26:34.994Z" }, +] + [[package]] name = "s3transfer" version = "0.13.1" @@ -739,6 +851,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, ] +[[package]] +name = "virtualenv" +version = "20.32.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/96/0834f30fa08dca3738614e6a9d42752b6420ee94e58971d702118f7cfd30/virtualenv-20.32.0.tar.gz", hash = "sha256:886bf75cadfdc964674e6e33eb74d787dff31ca314ceace03ca5810620f4ecf0", size = 6076970, upload-time = "2025-07-21T04:09:50.985Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/c6/f8f28009920a736d0df434b52e9feebfb4d702ba942f15338cb4a83eafc1/virtualenv-20.32.0-py3-none-any.whl", hash = "sha256:2c310aecb62e5aa1b06103ed7c2977b81e042695de2697d01017ff0f1034af56", size = 6057761, upload-time = "2025-07-21T04:09:48.059Z" }, +] + [[package]] name = "we-all-code" version = "0.0.1" @@ -775,9 +901,15 @@ dependencies = [ { name = "mock" }, { name = "pillow" }, { name = "psycopg", extra = ["binary"] }, + { name = "ruff" }, { name = "sentry-sdk" }, ] +[package.dev-dependencies] +dev = [ + { name = "pre-commit" }, +] + [package.metadata] requires-dist = [ { name = "arrow", specifier = ">=1.3.0,<2.0.0" }, @@ -811,9 +943,13 @@ requires-dist = [ { name = "mock", specifier = ">=5.2.0,<6.0.0" }, { name = "pillow", specifier = ">=11.3.0,<12.0.0" }, { name = "psycopg", extras = ["binary"], specifier = ">=3.2.9,<4.0.0" }, + { name = "ruff", specifier = ">=0.8.0,<1.0.0" }, { name = "sentry-sdk", specifier = ">=2.32.0,<3.0.0" }, ] +[package.metadata.requires-dev] +dev = [{ name = "pre-commit", specifier = ">=4.2.0" }] + [[package]] name = "whitenoise" version = "6.9.0" diff --git a/weallcode/admin.py b/weallcode/admin.py index c6cf533d..fd6309e2 100644 --- a/weallcode/admin.py +++ b/weallcode/admin.py @@ -1,11 +1,9 @@ from django.contrib import admin from django.utils.html import format_html -from .models import ( - AssociateBoardMember, - BoardMember, - StaffMember, -) +from .models import AssociateBoardMember +from .models import BoardMember +from .models import StaffMember @admin.register(StaffMember) diff --git a/weallcode/forms.py b/weallcode/forms.py index 9265879e..e8be72c2 100644 --- a/weallcode/forms.py +++ b/weallcode/forms.py @@ -1,6 +1,5 @@ from django import forms from django.conf import settings - from django_recaptcha.fields import ReCaptchaField from django_recaptcha.widgets import ReCaptchaV3 diff --git a/weallcode/models/associate_board_member.py b/weallcode/models/associate_board_member.py index d6771ee1..014d3c1e 100644 --- a/weallcode/models/associate_board_member.py +++ b/weallcode/models/associate_board_member.py @@ -1,12 +1,7 @@ -from collections import defaultdict -from itertools import chain - from django.db import models -from .common import ( - CommonBoardMemberManager, - CommonInfo, -) +from .common import CommonBoardMemberManager +from .common import CommonInfo class AssociateBoardMember(CommonInfo): diff --git a/weallcode/models/board_member.py b/weallcode/models/board_member.py index 85f3c43e..c314a7a9 100644 --- a/weallcode/models/board_member.py +++ b/weallcode/models/board_member.py @@ -1,9 +1,7 @@ from django.db import models -from .common import ( - CommonBoardMemberManager, - CommonInfo, -) +from .common import CommonBoardMemberManager +from .common import CommonInfo class BoardMember(CommonInfo): diff --git a/weallcode/models/common.py b/weallcode/models/common.py index e83aa2c6..dcb66b93 100644 --- a/weallcode/models/common.py +++ b/weallcode/models/common.py @@ -1,12 +1,8 @@ -from collections import defaultdict from datetime import date -from itertools import chain from django.db import models -from django.db.models import ( - Case, - When, -) +from django.db.models import Case +from django.db.models import When CHAIR = "Chair" VICE_CHAIR = "Vice Chair" @@ -73,12 +69,14 @@ def get_sorted(self): roles = [CHAIR, VICE_CHAIR, TREASURER, SECRETARY, DIRECTOR] order = Case( - *[When(role=role, then=pos) for pos, role in enumerate(roles)] + *[When(role=role, then=pos) for pos, role in enumerate(roles)], ) return ( self.get_queryset() .filter( - is_active=True, departure_date__isnull=True, role__in=roles + is_active=True, + departure_date__isnull=True, + role__in=roles, ) .order_by(order, "join_date", "name") ) diff --git a/weallcode/urls.py b/weallcode/urls.py index 8b24e2f3..44f8052f 100644 --- a/weallcode/urls.py +++ b/weallcode/urls.py @@ -3,18 +3,16 @@ from django.urls import path from django.views.generic import RedirectView -from weallcode.views import ( - AssociateBoardView, - CreditsView, - HomeView, - JoinUsView, - OurStoryView, - PrivacyView, - ProgramsSummerCampsView, - ProgramsView, - StaticSitemapView, - TeamView, -) +from weallcode.views import AssociateBoardView +from weallcode.views import CreditsView +from weallcode.views import HomeView +from weallcode.views import JoinUsView +from weallcode.views import OurStoryView +from weallcode.views import PrivacyView +from weallcode.views import ProgramsSummerCampsView +from weallcode.views import ProgramsView +from weallcode.views import StaticSitemapView +from weallcode.views import TeamView sitemaps = { "static": StaticSitemapView, @@ -33,7 +31,7 @@ ProgramsSummerCampsView.as_view(), name="weallcode-programs-summer-camps", ), - ] + ], ), ), path("team/", TeamView.as_view(), name="weallcode-team"), @@ -47,7 +45,7 @@ AssociateBoardView.as_view(), name="weallcode-associate-board", ), - ] + ], ), ), path("privacy/", PrivacyView.as_view(), name="weallcode-privacy"), @@ -65,7 +63,8 @@ ), # Redirect /get-involved/ to weallcode-join-us path( - "get-involved/", RedirectView.as_view(pattern_name="weallcode-join-us") + "get-involved/", + RedirectView.as_view(pattern_name="weallcode-join-us"), ), ] @@ -74,7 +73,7 @@ # Sentry Testing def trigger_error(request): - division_by_zero = 1 / 0 + 1 / 0 # Intentional division by zero for Sentry testing urlpatterns += [ diff --git a/weallcode/views/associate_board.py b/weallcode/views/associate_board.py index 2573777d..77a7dc7f 100644 --- a/weallcode/views/associate_board.py +++ b/weallcode/views/associate_board.py @@ -8,4 +8,4 @@ class AssociateBoardView(DefaultMetaTags, TemplateView): template_name = "weallcode/associate_board.html" url = reverse_lazy("weallcode-associate-board") - title = f"Join our Associate Board | We All Code" + title = "Join our Associate Board | We All Code" diff --git a/weallcode/views/common.py b/weallcode/views/common.py index 6c725a29..b0efa92f 100644 --- a/weallcode/views/common.py +++ b/weallcode/views/common.py @@ -1,13 +1,14 @@ from django.conf import settings from django.contrib.auth import get_user_model - +from django.shortcuts import render from meta.views import MetadataMixin from sentry_sdk import capture_message +from sentry_sdk import last_event_id User = get_user_model() -def page_not_found_view(*args, **kwargs): +def page_not_found_view(request, exception=None): print("page_not_found_view") options = {} diff --git a/weallcode/views/credits.py b/weallcode/views/credits.py index 77a8d473..3527ffa8 100644 --- a/weallcode/views/credits.py +++ b/weallcode/views/credits.py @@ -1,4 +1,3 @@ -from django.urls import reverse_lazy from django.views.generic import TemplateView from .common import DefaultMetaTags @@ -8,4 +7,4 @@ class CreditsView(DefaultMetaTags, TemplateView): template_name = "weallcode/credits.html" # url = reverse_lazy("weallcode-credits") - title = f"Credits & Attributions | We All Code" + title = "Credits & Attributions | We All Code" diff --git a/weallcode/views/home.py b/weallcode/views/home.py index b7c4081d..d5ac115e 100644 --- a/weallcode/views/home.py +++ b/weallcode/views/home.py @@ -15,7 +15,8 @@ def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) sessions = Session.objects.filter( - is_active=True, start_date__gte=timezone.now() + is_active=True, + start_date__gte=timezone.now(), ).order_by("start_date") if ( diff --git a/weallcode/views/join_us.py b/weallcode/views/join_us.py index 07260515..b9beeac8 100644 --- a/weallcode/views/join_us.py +++ b/weallcode/views/join_us.py @@ -12,7 +12,7 @@ class JoinUsView(DefaultMetaTags, FormView): url = reverse_lazy("weallcode-join-us") success_url = reverse_lazy("weallcode-join-us") - title = f"Join Us | We All Code" + title = "Join Us | We All Code" def form_valid(self, form): # This method is called when valid form data has been POSTed. @@ -20,10 +20,7 @@ def form_valid(self, form): form.send_email() messages.success( self.request, - ( - "Thank you for contacting us! We will respond as soon as" - " possible." - ), + ("Thank you for contacting us! We will respond as soon as possible."), ) return super().form_valid(form) diff --git a/weallcode/views/our_story.py b/weallcode/views/our_story.py index ebc547ab..aff6366f 100644 --- a/weallcode/views/our_story.py +++ b/weallcode/views/our_story.py @@ -8,4 +8,4 @@ class OurStoryView(DefaultMetaTags, TemplateView): template_name = "weallcode/our_story.html" url = reverse_lazy("weallcode-our-story") - title = f"Our Story | We All Code" + title = "Our Story | We All Code" diff --git a/weallcode/views/privacy.py b/weallcode/views/privacy.py index 9c00bba8..28eb0d5e 100644 --- a/weallcode/views/privacy.py +++ b/weallcode/views/privacy.py @@ -8,4 +8,4 @@ class PrivacyView(DefaultMetaTags, TemplateView): template_name = "weallcode/privacy.html" url = reverse_lazy("weallcode-privacy") - title = f"Privacy & Terms | We All Code" + title = "Privacy & Terms | We All Code" diff --git a/weallcode/views/programs.py b/weallcode/views/programs.py index 2c16e7f9..b3ed2ac7 100644 --- a/weallcode/views/programs.py +++ b/weallcode/views/programs.py @@ -1,14 +1,11 @@ +import arrow from django.conf import settings from django.urls import reverse_lazy from django.utils import timezone from django.views.generic import TemplateView -import arrow - -from coderdojochi.models import ( - Course, - Session, -) +from coderdojochi.models import Course +from coderdojochi.models import Session from .common import DefaultMetaTags @@ -17,20 +14,14 @@ class ProgramsView(DefaultMetaTags, TemplateView): template_name = "weallcode/programs.html" url = reverse_lazy("weallcode-programs") - title = f"Programs | We All Code" + title = "Programs | We All Code" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) user = self.request.user - IS_PARENT = ( - True - if user.is_authenticated and user.role == "guardian" - else False - ) - IS_MENTOR = ( - True if user.is_authenticated and user.role == "mentor" else False - ) + IS_PARENT = True if user.is_authenticated and user.role == "guardian" else False + IS_MENTOR = True if user.is_authenticated and user.role == "mentor" else False NOW = arrow.now() # region WEEKEND CLASSES @@ -56,8 +47,7 @@ def get_context_data(self, **kwargs): if ( session.mentor_capacity - and len(session.get_mentor_orders()) - >= session.mentor_capacity + and len(session.get_mentor_orders()) >= session.mentor_capacity ): session.class_status = "Class Full" else: @@ -65,13 +55,13 @@ def get_context_data(self, **kwargs): else: session.start_time = arrow.get(session.start_date).to( - settings.TIME_ZONE + settings.TIME_ZONE, ) session.end_time = session.end_date # MAX_DAYS_FOR_PARENTS (30) days before the class start time open_signup_time = session.start_time.shift( - days=-settings.MAX_DAYS_FOR_PARENTS + days=-settings.MAX_DAYS_FOR_PARENTS, ) if IS_PARENT and NOW < open_signup_time: diff --git a/weallcode/views/programs_summer_camps.py b/weallcode/views/programs_summer_camps.py index a56a91d2..43ee8acd 100644 --- a/weallcode/views/programs_summer_camps.py +++ b/weallcode/views/programs_summer_camps.py @@ -1,6 +1,10 @@ from django.urls import reverse_lazy +from django.utils import timezone from django.views.generic import TemplateView +from coderdojochi.models import Course +from coderdojochi.models import Session + from .common import DefaultMetaTags @@ -8,7 +12,7 @@ class ProgramsSummerCampsView(DefaultMetaTags, TemplateView): template_name = "weallcode/programs_summer_camps.html" url = reverse_lazy("weallcode-programs-summer-camps") - title = f"Summer Camps | We All Code" + title = "Summer Camps | We All Code" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) diff --git a/weallcode/views/team.py b/weallcode/views/team.py index ef583373..fa433c3e 100644 --- a/weallcode/views/team.py +++ b/weallcode/views/team.py @@ -1,6 +1,3 @@ -from collections import defaultdict -from itertools import chain - from django.db.models.aggregates import Count from django.urls import reverse_lazy from django.views.generic import TemplateView @@ -17,7 +14,7 @@ class TeamView(DefaultMetaTags, TemplateView): template_name = "weallcode/team.html" url = reverse_lazy("weallcode-team") - title = f"Team | We All Code" + title = "Team | We All Code" # Instructors def get_instructors(self, context, volunteers): @@ -30,7 +27,7 @@ def get_instructors(self, context, volunteers): # Volunteers def get_volunteers(self, context, volunteers): all_volunteers = volunteers.annotate( - session_count=Count("mentororder") + session_count=Count("mentororder"), ).order_by("-user__role", "-session_count") mentors = []