From bdc2baa3535e95500952f17891c23042e1ccd645 Mon Sep 17 00:00:00 2001 From: Bryan Grigorie Date: Thu, 26 Feb 2026 18:21:16 -0500 Subject: [PATCH 1/3] build: pre-commit hooks and initial run --- .github/workflows/deploy.yml | 4 ++-- .gitmodules | 1 - .pre-commit-config.yaml | 17 +++++++++++++++++ README.md | 2 ++ api/serializers.py | 2 +- api/urls.py | 2 +- assets/base.scss | 8 ++++---- assets/htmx_global.js | 2 +- assets/index.js | 2 +- core/utils/turnstile_utils.py | 2 +- docker-compose.yml | 4 ++-- home/admin.py | 2 +- home/models.py | 18 +++++++++--------- home/templates/home/blog.html | 2 +- home/templates/home/blog_post.html | 2 +- .../home/partials/blog_post_card.html | 2 +- .../home/partials/blog_post_list.html | 2 +- .../home/partials/credential_card.html | 2 +- home/templates/home/partials/pagination.html | 6 +++--- home/templates/home/partials/project_card.html | 6 +++--- home/templates/home/partials/project_list.html | 2 +- home/templates/home/projects.html | 2 +- home/tests/test_models.py | 2 +- portfolio/admin.py | 2 +- portfolio/apps.py | 2 +- portfolio/settings.py | 8 ++++---- sponsors/models.py | 2 +- sponsors/serializers.py | 2 +- static/admin/js/preview_text_word_count.js | 8 ++++---- static/css/base.css | 2 +- static/home/css/blog_post.css | 2 +- static/home/css/index.css | 2 +- static/sponsors/css/index.css | 2 +- templates/base.html | 10 +++++----- users/admin.py | 2 +- users/auth_backends.py | 5 ++--- users/context_processors.py | 2 +- users/templates/users/login.html | 2 +- users/templates/users/partials/login_form.html | 4 ++-- users/views.py | 4 ++-- 40 files changed, 86 insertions(+), 69 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 521efe4..5f0a869 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -11,7 +11,7 @@ on: jobs: redeploy: runs-on: ubuntu-latest - if: > + if: > github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' steps: @@ -23,4 +23,4 @@ jobs: -H "Content-Type: application/json" \ -H "X-Hub-Signature-256: sha256=$hash" \ -d @payload.json \ - ${{ secrets.WEBHOOK_URL }} \ No newline at end of file + ${{ secrets.WEBHOOK_URL }} diff --git a/.gitmodules b/.gitmodules index 8b13789..e69de29 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1 +0,0 @@ - diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..29da577 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,17 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-yaml + args: [--unsafe] + - id: end-of-file-fixer + - id: trailing-whitespace + - id: detect-private-key + - repo: https://github.com/astral-sh/uv-pre-commit + rev: 0.10.6 + hooks: + - id: uv-lock + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.3 + hooks: + - id: ruff-check diff --git a/README.md b/README.md index cef4e53..cea8466 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,12 @@ Personal website that includes a portfolio and a blog. Made with [Django](https: ### Dependencies - ``pyproject.toml`` - ``package.json`` +- ``.pre-commit-config.yaml`` (Git hooks) #### Installation - ``uv sync [--group dev]`` - ``npm install`` +- ``uvx pre-commit install`` (Git hooks) ### Environment variables #### Always required diff --git a/api/serializers.py b/api/serializers.py index b752193..aa4c191 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -16,4 +16,4 @@ class Meta: 'project_download_url', 'image_url', 'date', - ] \ No newline at end of file + ] diff --git a/api/urls.py b/api/urls.py index 317fc53..4dc3355 100644 --- a/api/urls.py +++ b/api/urls.py @@ -4,4 +4,4 @@ urlpatterns = [ path('project//', views.get_project), -] \ No newline at end of file +] diff --git a/assets/base.scss b/assets/base.scss index 1b64a35..b7a08d5 100644 --- a/assets/base.scss +++ b/assets/base.scss @@ -99,7 +99,7 @@ $md-breakpoint: map-get($grid-breakpoints, "md"); @media (min-width: $md-breakpoint) { padding-top: 3rem; } - + .heading-anchor { text-decoration: none; @@ -107,7 +107,7 @@ $md-breakpoint: map-get($grid-breakpoints, "md"); text-decoration: underline; } } - + .badge { &:hover { box-shadow: 0 1px 2px 1px rgba(0, 0, 0, 0.15); @@ -215,7 +215,7 @@ $md-breakpoint: map-get($grid-breakpoints, "md"); margin-bottom: 0.75rem; color: $body-color; } - + .card-subtitle { letter-spacing: 0.075em; } @@ -291,4 +291,4 @@ $md-breakpoint: map-get($grid-breakpoints, "md"); --bs-btn-active-bg: #{darken($white-accent, 15%)}; --bs-btn-active-border-color: #{darken($white-accent, 15%)}; -} \ No newline at end of file +} diff --git a/assets/htmx_global.js b/assets/htmx_global.js index 58a7ac1..43766a4 100644 --- a/assets/htmx_global.js +++ b/assets/htmx_global.js @@ -1 +1 @@ -window.htmx = require('htmx.org'); \ No newline at end of file +window.htmx = require('htmx.org'); diff --git a/assets/index.js b/assets/index.js index 13f4fc0..e46db7d 100644 --- a/assets/index.js +++ b/assets/index.js @@ -1,4 +1,4 @@ import 'bootstrap/dist/js/bootstrap.bundle.min.js'; import './base.scss'; import 'htmx.org/dist/htmx.min.js'; -import './htmx_global.js'; \ No newline at end of file +import './htmx_global.js'; diff --git a/core/utils/turnstile_utils.py b/core/utils/turnstile_utils.py index 36e7c89..8fa967a 100644 --- a/core/utils/turnstile_utils.py +++ b/core/utils/turnstile_utils.py @@ -9,7 +9,7 @@ def validate_turnstile(token, secret, remoteip=None): } if remoteip: data['remoteip'] = remoteip - + try: response = requests.post(url, data=data, timeout=10) response.raise_for_status() diff --git a/docker-compose.yml b/docker-compose.yml index 6e694fa..7420bdd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,10 +1,10 @@ services: portfolio: - build: + build: context: . dockerfile: Dockerfile target: development - command: > + command: > sh -c " npm run dev && python manage.py migrate && diff --git a/home/admin.py b/home/admin.py index 9835e90..cf18715 100644 --- a/home/admin.py +++ b/home/admin.py @@ -5,7 +5,7 @@ class BlogPostAdmin(admin.ModelAdmin): fieldsets = [ - ('Post content', + ('Post content', {'fields': ['post_title', 'slug', 'preview_text', 'post_body']} ), ('Date information', {'fields': ['date_published']}), diff --git a/home/models.py b/home/models.py index 43a2f05..c78cdfd 100644 --- a/home/models.py +++ b/home/models.py @@ -27,13 +27,13 @@ def is_about_content(self): @property def sorted_tags(self): return self.tags.all().order_by('name') - + @property def url(self): if self.slug: return reverse('home:blog_post_by_slug', kwargs={'slug': self.slug}) return reverse('home:blog_post_by_id', kwargs={'id': self.id}) - + def save(self, *args, **kwargs): if not self.preview_text: soup = BeautifulSoup(self.post_body, 'html.parser') @@ -83,15 +83,15 @@ def __str__(self): return self.skill_title def level_text(self): - if self.skill_level == 1: + if self.skill_level == 1: return "Basic knowledge" - elif self.skill_level == 2: + elif self.skill_level == 2: return "Novice" - elif self.skill_level == 3: + elif self.skill_level == 3: return "Intermediate" - elif self.skill_level == 4: + elif self.skill_level == 4: return "Advanced" - elif self.skill_level == 5: + elif self.skill_level == 5: return "Expert" @@ -113,7 +113,7 @@ class Project(models.Model): def __str__(self): return self.project_title - + @property def sorted_tags(self): return self.tags.all().order_by('name') @@ -130,7 +130,7 @@ class Experience(models.Model): def __str__(self): return f"{self.position} at {self.employer}" - + @property def sorted_tags(self): return self.tags.all().order_by('name') diff --git a/home/templates/home/blog.html b/home/templates/home/blog.html index 9ca2bd1..37fde42 100644 --- a/home/templates/home/blog.html +++ b/home/templates/home/blog.html @@ -25,4 +25,4 @@

Blog

window.scrollTo({top: 0, behavior: 'smooth'}); }); -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/home/templates/home/blog_post.html b/home/templates/home/blog_post.html index aa368b9..826df99 100644 --- a/home/templates/home/blog_post.html +++ b/home/templates/home/blog_post.html @@ -37,4 +37,4 @@

{{post.post_title}}

-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/home/templates/home/partials/blog_post_card.html b/home/templates/home/partials/blog_post_card.html index bc01235..c9588e6 100644 --- a/home/templates/home/partials/blog_post_card.html +++ b/home/templates/home/partials/blog_post_card.html @@ -33,4 +33,4 @@
- \ No newline at end of file + diff --git a/home/templates/home/partials/blog_post_list.html b/home/templates/home/partials/blog_post_list.html index 1d273a3..53355ba 100644 --- a/home/templates/home/partials/blog_post_list.html +++ b/home/templates/home/partials/blog_post_list.html @@ -5,4 +5,4 @@ {% endfor %}
{% include 'home/partials/pagination.html' with page=posts hx_target="#blog-post-list" %} -
\ No newline at end of file + diff --git a/home/templates/home/partials/credential_card.html b/home/templates/home/partials/credential_card.html index c5f1b7f..6dca2bf 100644 --- a/home/templates/home/partials/credential_card.html +++ b/home/templates/home/partials/credential_card.html @@ -30,4 +30,4 @@
{{credential.name}}
{% endif %} - \ No newline at end of file + diff --git a/home/templates/home/partials/pagination.html b/home/templates/home/partials/pagination.html index 4f2d0d6..30763df 100644 --- a/home/templates/home/partials/pagination.html +++ b/home/templates/home/partials/pagination.html @@ -1,7 +1,7 @@ \ No newline at end of file + diff --git a/home/templates/home/partials/project_card.html b/home/templates/home/partials/project_card.html index fb85eaf..b604007 100644 --- a/home/templates/home/partials/project_card.html +++ b/home/templates/home/partials/project_card.html @@ -19,11 +19,11 @@
{{ project.project_role }}

{% for tag in project.sorted_tags %} - {{tag.name}} @@ -57,4 +57,4 @@
{{ project.project_role }}
{% endif %} - \ No newline at end of file + diff --git a/home/templates/home/partials/project_list.html b/home/templates/home/partials/project_list.html index 3a97292..f691056 100644 --- a/home/templates/home/partials/project_list.html +++ b/home/templates/home/partials/project_list.html @@ -5,4 +5,4 @@ {% endfor %}
{% include 'home/partials/pagination.html' with page=projects hx_target="#project-list" %} -
\ No newline at end of file + diff --git a/home/templates/home/projects.html b/home/templates/home/projects.html index 3f719be..3b929b6 100644 --- a/home/templates/home/projects.html +++ b/home/templates/home/projects.html @@ -25,4 +25,4 @@

Projects

window.scrollTo({top: 0, behavior: 'smooth'}); }); -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/home/tests/test_models.py b/home/tests/test_models.py index 6263dcc..0fc08c0 100644 --- a/home/tests/test_models.py +++ b/home/tests/test_models.py @@ -14,7 +14,7 @@ def setUp(self): ) BlogPost.objects.create( date_published=datetime.now(), - post_title="Post with 51 words", + post_title="Post with 51 words", post_body="
" + ("word " * 51).strip() + "
", ) BlogPost.objects.create( diff --git a/portfolio/admin.py b/portfolio/admin.py index fd3f117..4776178 100644 --- a/portfolio/admin.py +++ b/portfolio/admin.py @@ -14,7 +14,7 @@ class CustomAdminSite(admin.AdminSite): @property def site_title(self): return f'{getattr(config, 'BRAND')} Admin' - + @property def site_header(self): return self.site_title diff --git a/portfolio/apps.py b/portfolio/apps.py index 5d78764..8303645 100644 --- a/portfolio/apps.py +++ b/portfolio/apps.py @@ -2,4 +2,4 @@ class CustomAdminConfig(AdminConfig): - default_site = 'portfolio.admin.CustomAdminSite' \ No newline at end of file + default_site = 'portfolio.admin.CustomAdminSite' diff --git a/portfolio/settings.py b/portfolio/settings.py index dfc97dc..c1b8d98 100644 --- a/portfolio/settings.py +++ b/portfolio/settings.py @@ -29,9 +29,9 @@ ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='.localhost', cast=Csv()) -# FIXING CSRF ISSUE -SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') -# https://stackoverflow.com/a/71482883/1615284 +# FIXING CSRF ISSUE +SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') +# https://stackoverflow.com/a/71482883/1615284 # https://docs.djangoproject.com/en/5.1/ref/settings/#secure-proxy-ssl-header USE_X_FORWARDED_HOST = True @@ -250,5 +250,5 @@ 'Global': ('BRAND', 'EMAIL', 'DEFAULT_CURRENCY'), 'Links': ('GITHUB_URL', 'LINKEDIN_URL', 'UPWORK_URL', 'RESUME_URL', 'KOFI_URL', 'PAYPAL_URL'), 'Hero Section': ('HERO_HEADING', 'HERO_SUBHEADING'), - 'Sponsors': ('SPONSOR_DURATION', 'SPONSOR_DONATION_URL', 'SPONSOR_MESSAGE_MINIMUM_CONTRIBUTION'), + 'Sponsors': ('SPONSOR_DURATION', 'SPONSOR_DONATION_URL', 'SPONSOR_MESSAGE_MINIMUM_CONTRIBUTION'), } diff --git a/sponsors/models.py b/sponsors/models.py index 21dd30e..fb6a560 100644 --- a/sponsors/models.py +++ b/sponsors/models.py @@ -21,7 +21,7 @@ class Sponsorship(models.Model): def __str__(self): return f"{self.name}, {self.amount}, {self.currency}, {self.start_date} - {self.expiration_date}, {self.platform}" - + @property def display_eligible(self): # TODO determine eligibility for other currencies diff --git a/sponsors/serializers.py b/sponsors/serializers.py index 76b2565..55f2b43 100644 --- a/sponsors/serializers.py +++ b/sponsors/serializers.py @@ -31,7 +31,7 @@ class Meta: 'is_public', 'timestamp', ] - + def create(self, validated_data): start_date = validated_data.get('start_date') expiration_date = start_date + timedelta(days=getattr(config, 'SPONSOR_DURATION')) diff --git a/static/admin/js/preview_text_word_count.js b/static/admin/js/preview_text_word_count.js index 076b06c..e925848 100644 --- a/static/admin/js/preview_text_word_count.js +++ b/static/admin/js/preview_text_word_count.js @@ -17,18 +17,18 @@ let over = wordCount - 50; msg = `${msg} (${over} over the limit)`; } - + let $display = $(selector); if (!$display.length) { const $newDisplay = $(`

-

+

`); $targetContainer.append($newDisplay); $display = $(selector); } - + $display.text(`${wordCount} ${msg}`); } // Initialize word count @@ -37,4 +37,4 @@ // Update word count on events $field.on('keyup change input', updateWordCount); }); -})(django.jQuery); \ No newline at end of file +})(django.jQuery); diff --git a/static/css/base.css b/static/css/base.css index f8bd446..9d9e5a0 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -36,4 +36,4 @@ nav a:hover { #footer { height: 50px; } -*/ \ No newline at end of file +*/ diff --git a/static/home/css/blog_post.css b/static/home/css/blog_post.css index 52bfc74..c00e4e0 100644 --- a/static/home/css/blog_post.css +++ b/static/home/css/blog_post.css @@ -5,4 +5,4 @@ .post-tags a { text-decoration: none; -} \ No newline at end of file +} diff --git a/static/home/css/index.css b/static/home/css/index.css index 51b99a1..0ad9a41 100644 --- a/static/home/css/index.css +++ b/static/home/css/index.css @@ -5,4 +5,4 @@ .blog-post-card { min-height: 18.25rem; -} \ No newline at end of file +} diff --git a/static/sponsors/css/index.css b/static/sponsors/css/index.css index 489655c..cfd9223 100644 --- a/static/sponsors/css/index.css +++ b/static/sponsors/css/index.css @@ -1,4 +1,4 @@ #hero p { line-height: 1.5; font-size: 1rem; -} \ No newline at end of file +} diff --git a/templates/base.html b/templates/base.html index d6bd1fd..d9442eb 100644 --- a/templates/base.html +++ b/templates/base.html @@ -22,7 +22,7 @@ {% block head_script %} {% if ADSENSE_CLIENT %} - {% comment %} {% endcomment %} {% endif %} {% if TURNSTILE_SITE_KEY %} @@ -32,7 +32,7 @@ {% block extra_title %}{% endblock %}{{ config.BRAND }} - +
@@ -118,7 +118,7 @@ {% block content %} {% endblock %}
- +
@@ -166,7 +166,7 @@
- + {% block body_script %} @@ -174,4 +174,4 @@ {% block extra_scripts %}{% endblock %} {% endblock %} - \ No newline at end of file + diff --git a/users/admin.py b/users/admin.py index 8fa2ccc..7816387 100644 --- a/users/admin.py +++ b/users/admin.py @@ -17,4 +17,4 @@ class CustomUserAdmin(UserAdmin): admin.site.unregister(User) -admin.site.register(User, CustomUserAdmin) \ No newline at end of file +admin.site.register(User, CustomUserAdmin) diff --git a/users/auth_backends.py b/users/auth_backends.py index 6ea3796..6c8cff9 100644 --- a/users/auth_backends.py +++ b/users/auth_backends.py @@ -31,7 +31,7 @@ def filter_users_by_claims(self, claims): except User.DoesNotExist: pass return User.objects.none() - + def create_user(self, claims): user = super().create_user(claims) preferred_username = claims.get('preferred_username') @@ -47,7 +47,7 @@ def create_user(self, claims): iss=claims.get('iss'), ) return user - + def update_user(self, user, claims): profile, created = OIDCProfile.objects.get_or_create(user=user) if created: @@ -59,4 +59,3 @@ def update_user(self, user, claims): profile.iss = claims.get('iss') profile.save() return user - diff --git a/users/context_processors.py b/users/context_processors.py index cd74c25..9b52b8d 100644 --- a/users/context_processors.py +++ b/users/context_processors.py @@ -5,4 +5,4 @@ def global_settings(request): 'OIDC_CONFIGURED': getattr(settings, 'OIDC_CONFIGURED', False), 'OIDC_OP_DISPLAY_NAME': getattr(settings, 'OIDC_OP_DISPLAY_NAME', None) or 'SSO', 'TURNSTILE_SITE_KEY': getattr(settings, 'TURNSTILE_SITE_KEY', None), - } \ No newline at end of file + } diff --git a/users/templates/users/login.html b/users/templates/users/login.html index 714d7ce..7da13c7 100644 --- a/users/templates/users/login.html +++ b/users/templates/users/login.html @@ -83,4 +83,4 @@
Log in
renderTurnstileWidget(); }); -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/users/templates/users/partials/login_form.html b/users/templates/users/partials/login_form.html index 7968e2f..96cb880 100644 --- a/users/templates/users/partials/login_form.html +++ b/users/templates/users/partials/login_form.html @@ -1,4 +1,4 @@ -
-
\ No newline at end of file + diff --git a/users/views.py b/users/views.py index 7feb350..b05c351 100644 --- a/users/views.py +++ b/users/views.py @@ -10,7 +10,7 @@ def login(request): if request.user.is_authenticated: return redirect(reverse('home:index')) - + if request.method == "POST": form = AuthenticationForm(request, data=request.POST) @@ -20,7 +20,7 @@ def login(request): remoteip = request.headers.get('CF-Connecting-IP') or \ request.headers.get('X-Forwarded-For') or \ request.META.get('REMOTE_ADDR') - + if not token or not validate_turnstile(token, settings.TURNSTILE_SECRET_KEY, remoteip)['success']: form.add_error(None, "Security challenge failed. Please try again.") From e1f9928c111f935ec37781a6aa4199fbdfe011c3 Mon Sep 17 00:00:00 2001 From: Bryan Grigorie Date: Thu, 26 Feb 2026 18:32:24 -0500 Subject: [PATCH 2/3] build: add commitlint hook --- .pre-commit-config.yaml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 29da577..9bf2adb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,6 +12,12 @@ repos: hooks: - id: uv-lock - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.3 + rev: v0.15.4 hooks: - id: ruff-check + - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook + rev: v9.24.0 + hooks: + - id: commitlint + stages: [commit-msg] + additional_dependencies: ['@commitlint/config-conventional'] From a70d69fe97e583ceaed62fe67c9b5a7964575672 Mon Sep 17 00:00:00 2001 From: Bryan Grigorie Date: Thu, 26 Feb 2026 18:41:16 -0500 Subject: [PATCH 3/3] docs(README): PEP 8 import order --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cea8466..4ea3836 100644 --- a/README.md +++ b/README.md @@ -48,9 +48,10 @@ Personal website that includes a portfolio and a blog. Made with [Django](https: ## File structure ### Python imports -1. Third-party libraries -2. Python standard library -3. Custom modules +1. Python standard library +2. Third-party libraries +3. Local modules +> See: [PEP 8](https://peps.python.org/pep-0008/#imports) ### Django project apps - ``api``: REST API.