Skip to content

Commit 910e6e7

Browse files
Merge pull request #1 from cpt-kernel-afk/feature/dev-tooling-ruff-ci
Add ruff linting + CI lint job and fix lint findings
2 parents c43f6f3 + 893028b commit 910e6e7

6 files changed

Lines changed: 68 additions & 17 deletions

File tree

.github/workflows/ci.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,23 @@ on:
77
branches: [main]
88

99
jobs:
10+
lint:
11+
name: Lint
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- name: Set up Python
17+
uses: actions/setup-python@v5
18+
with:
19+
python-version: "3.12"
20+
21+
- name: Install ruff
22+
run: pip install "$(grep -E '^ruff' requirements-dev.txt)"
23+
24+
- name: Ruff lint
25+
run: ruff check --output-format=github .
26+
1027
test:
1128
name: Tests
1229
runs-on: ubuntu-latest

requirements-dev.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Development-only tooling. Install with: pip install -r requirements-dev.txt
2+
-r requirements.txt
3+
ruff>=0.6,<1.0

ruff.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Ruff config for Time Tracker. See https://docs.astral.sh/ruff/
2+
target-version = "py312"
3+
line-length = 99
4+
indent-width = 4
5+
6+
# Generated migrations don't need to follow all rules.
7+
extend-exclude = ["*/migrations/*", ".venv", "static"]
8+
9+
[lint]
10+
# E/W: pycodestyle, F: pyflakes, I: isort, B: bugbear, UP: pyupgrade, DJ: flake8-django
11+
select = ["E", "W", "F", "I", "B", "UP", "DJ"]
12+
13+
[lint.per-file-ignores]
14+
# Django settings legitimately use star imports and long lines.
15+
"config/settings.py" = ["E501", "F403", "F405"]
16+
17+
[format]
18+
quote-style = "double"
19+
indent-style = "space"

tracker/models.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
from django.db import models
2525
from django.utils import timezone
2626

27-
2827
HEX_COLOR_VALIDATOR = RegexValidator(
2928
regex=r"^#(?:[0-9a-fA-F]{3}){1,2}$",
3029
message="Color must be a hex value like #0d6efd or #abc.",
@@ -75,10 +74,11 @@ def total_hours(self, since=None, until=None) -> Decimal:
7574
def total_cost(self, since=None, until=None) -> Decimal | None:
7675
if self.hourly_rate is None:
7776
return None
78-
return (self.total_hours(since=since, until=until) * self.hourly_rate).quantize(Decimal("0.01"))
77+
total = self.total_hours(since=since, until=until) * self.hourly_rate
78+
return total.quantize(Decimal("0.01"))
7979

8080
@property
81-
def active_timer(self) -> "TimeEntry | None":
81+
def active_timer(self) -> TimeEntry | None:
8282
"""Return the currently-running timer for this project, if any."""
8383
return self.entries.filter(end_time__isnull=True).order_by("-start_time").first()
8484

@@ -104,6 +104,15 @@ class Meta:
104104
def __str__(self) -> str:
105105
return f"{self.project.name} · {self.date} · {self.duration_display}"
106106

107+
def save(self, *args, **kwargs):
108+
# Auto-compute duration when both timestamps are set
109+
if self.start_time and self.end_time:
110+
delta: timedelta = self.end_time - self.start_time
111+
self.duration_minutes = max(0, int(delta.total_seconds() // 60))
112+
if not self.date:
113+
self.date = timezone.localdate(self.start_time)
114+
super().save(*args, **kwargs)
115+
107116
@property
108117
def is_running(self) -> bool:
109118
return self.start_time is not None and self.end_time is None
@@ -124,12 +133,3 @@ def current_duration_minutes(self) -> int:
124133
def clean(self):
125134
if self.start_time and self.end_time and self.end_time < self.start_time:
126135
raise ValidationError("End time must be after start time.")
127-
128-
def save(self, *args, **kwargs):
129-
# Auto-compute duration when both timestamps are set
130-
if self.start_time and self.end_time:
131-
delta: timedelta = self.end_time - self.start_time
132-
self.duration_minutes = max(0, int(delta.total_seconds() // 60))
133-
if not self.date:
134-
self.date = timezone.localdate(self.start_time)
135-
super().save(*args, **kwargs)

tracker/tests.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ def test_totals_with_no_entries(self):
5656
def test_totals_aggregate_completed_entries(self):
5757
p = Project.objects.create(name="Sum", hourly_rate=Decimal("60.00"))
5858
now = timezone.now()
59-
TimeEntry.objects.create(project=p, start_time=now - timedelta(hours=2), end_time=now, date=now.date())
59+
TimeEntry.objects.create(
60+
project=p, start_time=now - timedelta(hours=2), end_time=now, date=now.date()
61+
)
6062
TimeEntry.objects.create(
6163
project=p,
6264
start_time=now - timedelta(minutes=30),

tracker/views.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
from django.http import HttpResponse
3131
from django.shortcuts import get_object_or_404, redirect, render, resolve_url
3232
from django.template.loader import render_to_string
33-
from django.urls import reverse
3433
from django.utils import timezone
3534
from django.utils.http import url_has_allowed_host_and_scheme
3635
from django.views.decorators.http import require_POST
@@ -39,7 +38,6 @@
3938
from .forms import ProjectForm, ReportFilterForm, TimeEntryForm
4039
from .models import Project, TimeEntry
4140

42-
4341
# ---------- Helpers ----------
4442

4543
def _safe_next(request, fallback: str = "tracker:dashboard") -> str:
@@ -98,7 +96,9 @@ def dashboard(request):
9896
@login_required
9997
def project_list(request):
10098
show_archived = request.GET.get("archived") == "1"
101-
projects = Project.objects.all() if show_archived else Project.objects.filter(is_archived=False)
99+
projects = (
100+
Project.objects.all() if show_archived else Project.objects.filter(is_archived=False)
101+
)
102102
projects = projects.annotate(total_minutes_db=Sum("entries__duration_minutes"))
103103
return render(request, "tracker/project_list.html", {
104104
"projects": projects,
@@ -342,7 +342,17 @@ def report_csv(request):
342342
filename = f"timetracker-report-{timezone.localdate().isoformat()}.csv"
343343
response["Content-Disposition"] = f'attachment; filename="{filename}"'
344344
writer = csv.writer(response)
345-
writer.writerow(["Project", "Date", "Start", "End", "Duration (minutes)", "Duration (hours)", "Description"])
345+
writer.writerow(
346+
[
347+
"Project",
348+
"Date",
349+
"Start",
350+
"End",
351+
"Duration (minutes)",
352+
"Duration (hours)",
353+
"Description",
354+
]
355+
)
346356
for block in context["project_blocks"]:
347357
for e in block["entries"]:
348358
hours = (Decimal(e.duration_minutes) / Decimal(60)).quantize(Decimal("0.01"))

0 commit comments

Comments
 (0)