Skip to content

Commit 5980e7b

Browse files
authored
Merge pull request #1 from toggle-corp/feature/aadd-ci
Add linting and docker build
2 parents 447c035 + cde480c commit 5980e7b

22 files changed

+493
-136
lines changed

.github/dependabot.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# To get started with Dependabot version updates, you'll need to specify which
2+
# package ecosystems to update and where the package manifests are located.
3+
# Please see the documentation for all configuration options:
4+
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5+
6+
version: 2
7+
updates:
8+
- package-ecosystem: "pip" # See documentation for possible values
9+
directory: "/" # Location of package manifests
10+
schedule:
11+
interval: "weekly"
12+
ignore:
13+
- dependency-name: "*"
14+
update-types: ["version-update:semver-major"]

.github/pull_request_template.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
Addresses
2+
- #example-issue-number
3+
4+
Depends on
5+
- #example-Issue/PR-number
6+
7+
NOTE: **Mention related users here if any.**
8+
9+
## Changes
10+
11+
* Detailed list or prose of changes
12+
* Breaking changes
13+
* Changes to configurations
14+
15+
## This PR doesn't introduce any:
16+
17+
- [ ] temporary files, auto-generated files or secret keys
18+
- [ ] n+1 queries
19+
- [ ] flake8 issues
20+
- [ ] `print`
21+
- [ ] typos
22+
- [ ] unwanted comments
23+
24+
## This PR contains valid:
25+
26+
- [ ] tests
27+
- [ ] permission checks (tests here too)
28+
- [ ] translations

.github/workflows/ci.yml

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
name: Run test
2+
3+
on:
4+
push:
5+
branches:
6+
- develop
7+
pull_request:
8+
9+
10+
jobs:
11+
test:
12+
name: 🚴 Test 🚴
13+
runs-on: ubuntu-latest
14+
15+
steps:
16+
- uses: actions/checkout@main
17+
18+
- name: 🐳 Prepare Docker
19+
id: prep
20+
run: |
21+
TAG=$(echo $GITHUB_SHA | head -c7)
22+
IMAGE="backend"
23+
echo "tagged_image=${IMAGE}:${TAG}" >> $GITHUB_OUTPUT
24+
echo "tag=${TAG}" >> $GITHUB_OUTPUT
25+
- name: 🐳 Set up Docker Buildx
26+
id: buildx
27+
uses: docker/setup-buildx-action@v2
28+
29+
- name: 🐳 Cache Docker layers
30+
uses: actions/cache@v3
31+
with:
32+
path: /tmp/.buildx-cache
33+
key: ${{ runner.os }}-buildx-${{ github.ref }}
34+
restore-keys: |
35+
${{ runner.os }}-buildx-refs/develop
36+
${{ runner.os }}-buildx-
37+
38+
- name: 🐳 Build image
39+
uses: docker/build-push-action@v4
40+
with:
41+
context: .
42+
builder: ${{ steps.buildx.outputs.name }}
43+
file: Dockerfile
44+
push: false
45+
load: true
46+
tags: ${{ steps.prep.outputs.tagged_image }}
47+
cache-from: type=local,src=/tmp/.buildx-cache
48+
cache-to: type=local,dest=/tmp/.buildx-cache-new
49+
50+
- name: 🕮 Validate latest graphql schema.
51+
env:
52+
DOCKER_IMAGE_BACKEND: ${{ steps.prep.outputs.tagged_image }}
53+
run: |
54+
docker-compose -f ./gh-docker-compose.yml run --rm web bash -c 'wait-for-it db:5432 && ./manage.py graphql_schema --out /ci-share/schema-latest.graphql' &&
55+
cmp --silent schema.graphql ./ci-share/schema-latest.graphql || {
56+
echo 'The schema.graphql is not up to date with the latest changes. Please update and push latest';
57+
diff schema.graphql ./ci-share/schema-latest.graphql;
58+
exit 1;
59+
}
60+
61+
- name: 🕮 Validate if there are no pending django migrations.
62+
env:
63+
DOCKER_IMAGE_BACKEND: ${{ steps.prep.outputs.tagged_image }}
64+
run: |
65+
docker-compose -f ./gh-docker-compose.yml run --rm web bash -c 'wait-for-it db:5432 && ./manage.py makemigrations --check --dry-run' || {
66+
echo 'There are some changes to be reflected in the migration. Make sure to run makemigrations';
67+
exit 1;
68+
}
69+
70+
# TODO: Run this for CI
71+
# - name: 🤞 Run Test 🧪 & Publish coverage to code climate
72+
# env:
73+
# DOCKER_IMAGE_BACKEND: ${{ steps.prep.outputs.tagged_image }}
74+
# run: docker-compose -f gh-docker-compose.yml run --rm web /code/scripts/run_tests.sh
75+
76+
# Temp fix
77+
# https://github.com/docker/build-push-action/blob/master/docs/advanced/cache.md#github-cache
78+
# https://github.com/docker/build-push-action/issues/252
79+
# https://github.com/moby/buildkit/issues/1896
80+
- name: 🐳 Move docker cache (🧙 Hack fix)
81+
run: |
82+
rm -rf /tmp/.buildx-cache
83+
mv /tmp/.buildx-cache-new /tmp/.buildx-cache

.github/workflows/lint.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: Python lint
2+
3+
on:
4+
push:
5+
branches:
6+
- develop
7+
pull_request:
8+
9+
10+
jobs:
11+
pre_commit_checks:
12+
name: 🚴 Pre-Commit checks 🚴
13+
runs-on: ubuntu-latest
14+
15+
steps:
16+
- uses: actions/checkout@main
17+
- name: Install poetry
18+
run: pipx install poetry
19+
- uses: actions/setup-python@main
20+
with:
21+
cache: 'poetry'
22+
- run: poetry install
23+
- uses: pre-commit/action@main
24+
Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
1-
# Generated by Django 4.2.13 on 2024-06-16 06:25
1+
# Generated by Django 4.2.13 on 2024-06-18 10:58
22

3-
from django.conf import settings
43
from django.db import migrations, models
5-
import django.db.models.deletion
64

75

86
class Migration(migrations.Migration):
97

108
initial = True
119

1210
dependencies = [
13-
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
1411
]
1512

1613
operations = [
@@ -21,11 +18,6 @@ class Migration(migrations.Migration):
2118
('date', models.DateField()),
2219
('leave_type', models.PositiveSmallIntegerField(blank=True, choices=[(1, 'Full'), (2, 'First Half'), (3, 'Second Half')], null=True)),
2320
('journal_text', models.TextField(blank=True)),
24-
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL)),
2521
],
26-
options={
27-
'indexes': [models.Index(fields=['date'], name='journal_jou_date_eb308c_idx')],
28-
'unique_together': {('user', 'date')},
29-
},
3022
),
3123
]
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Generated by Django 4.2.13 on 2024-06-18 10:58
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
initial = True
11+
12+
dependencies = [
13+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14+
('journal', '0001_initial'),
15+
]
16+
17+
operations = [
18+
migrations.AddField(
19+
model_name='journal',
20+
name='user',
21+
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL),
22+
),
23+
migrations.AddIndex(
24+
model_name='journal',
25+
index=models.Index(fields=['date'], name='journal_jou_date_eb308c_idx'),
26+
),
27+
migrations.AlterUniqueTogether(
28+
name='journal',
29+
unique_together={('user', 'date')},
30+
),
31+
]

apps/project/migrations/0001_initial.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Generated by Django 4.2.13 on 2024-06-14 17:50
1+
# Generated by Django 4.2.13 on 2024-06-18 10:58
22

33
from django.db import migrations, models
44
import django.db.models.deletion

apps/project/migrations/0002_initial.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Generated by Django 4.2.13 on 2024-06-14 17:50
1+
# Generated by Django 4.2.13 on 2024-06-18 10:58
22

33
from django.conf import settings
44
from django.db import migrations, models

apps/track/admin.py

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
from django.db import models
44
from django.http import HttpRequest
55

6-
from .models import Milestone, Task, TimeTrack
6+
from .models import Contract, Task, TimeTrack
77

88

9-
@admin.register(Milestone)
10-
class MilestoneAdmin(admin.ModelAdmin):
9+
@admin.register(Contract)
10+
class ContractAdmin(admin.ModelAdmin):
1111
search_fields = ("name",)
1212
list_filter = (
1313
AutocompleteFilterFactory("Project", "project"),
@@ -17,7 +17,7 @@ class MilestoneAdmin(admin.ModelAdmin):
1717
autocomplete_fields = ("project",)
1818
list_display = ("name", "get_project", "is_archived")
1919

20-
def get_queryset(self, request: HttpRequest) -> models.QuerySet[Milestone]:
20+
def get_queryset(self, request: HttpRequest) -> models.QuerySet[Contract]:
2121
return super().get_queryset(request).select_related("project")
2222

2323
@admin.display(ordering="project__name", description="Project")
@@ -29,24 +29,24 @@ def get_project(self, obj):
2929
class TaskAdmin(admin.ModelAdmin):
3030
search_fields = ("name",)
3131
list_filter = (
32-
AutocompleteFilterFactory("Project", "milestone__project"),
33-
AutocompleteFilterFactory("Milestone", "milestone"),
32+
AutocompleteFilterFactory("Project", "contract__project"),
33+
AutocompleteFilterFactory("Contract", "contract"),
3434
AutocompleteFilterFactory("Created By", "created_by"),
3535
"is_archived",
3636
)
37-
autocomplete_fields = ("milestone",)
38-
list_display = ("name", "get_project", "get_milestone", "is_archived")
37+
autocomplete_fields = ("contract",)
38+
list_display = ("name", "get_project", "get_contract", "is_archived")
3939

40-
def get_queryset(self, request: HttpRequest) -> models.QuerySet[Milestone]:
41-
return super().get_queryset(request).select_related("milestone", "milestone__project")
40+
def get_queryset(self, request: HttpRequest) -> models.QuerySet[Contract]:
41+
return super().get_queryset(request).select_related("contract", "contract__project")
4242

4343
@admin.display(ordering="project__name", description="Project")
4444
def get_project(self, obj):
45-
return obj.milestone.project.name
45+
return obj.contract.project.name
4646

47-
@admin.display(ordering="milestone__name", description="Milestone")
48-
def get_milestone(self, obj):
49-
return obj.milestone.name
47+
@admin.display(ordering="contract__name", description="Contract")
48+
def get_contract(self, obj):
49+
return obj.contract.name
5050

5151

5252
@admin.register(TimeTrack)
@@ -55,14 +55,14 @@ class TimeTrackAdmin(admin.ModelAdmin):
5555
"date",
5656
"task_type",
5757
"is_done",
58-
AutocompleteFilterFactory("Project", "task__milestone__project"),
59-
AutocompleteFilterFactory("Milestone", "task__milestone"),
58+
AutocompleteFilterFactory("Project", "task__contract__project"),
59+
AutocompleteFilterFactory("Contract", "task__contract"),
6060
AutocompleteFilterFactory("Task", "task"),
6161
AutocompleteFilterFactory("User", "user"),
6262
)
6363
autocomplete_fields = ("task",)
6464
list_display = (
65-
"get_milestone",
65+
"get_contract",
6666
"get_project",
6767
"get_task",
6868
"get_user",
@@ -72,16 +72,16 @@ class TimeTrackAdmin(admin.ModelAdmin):
7272
"is_done",
7373
)
7474

75-
def get_queryset(self, request: HttpRequest) -> models.QuerySet[Milestone]:
76-
return super().get_queryset(request).select_related("user", "task", "task__milestone", "task__milestone__project")
75+
def get_queryset(self, request: HttpRequest) -> models.QuerySet[Contract]:
76+
return super().get_queryset(request).select_related("user", "task", "task__contract", "task__contract__project")
7777

7878
@admin.display(ordering="project__name", description="Project")
7979
def get_project(self, obj):
80-
return obj.task.milestone.project.name
80+
return obj.task.contract.project.name
8181

82-
@admin.display(ordering="milestone__name", description="Milestone")
83-
def get_milestone(self, obj):
84-
return obj.task.milestone.name
82+
@admin.display(ordering="contract__name", description="Contract")
83+
def get_contract(self, obj):
84+
return obj.task.contract.name
8585

8686
@admin.display(ordering="task__name", description="Task")
8787
def get_task(self, obj):

apps/track/dataloaders.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,40 @@
11
import typing
22

33
from asgiref.sync import sync_to_async
4+
from django.db import models
45
from django.utils.functional import cached_property
56
from strawberry.dataloader import DataLoader
67

78
from apps.common.dataloaders import load_model_objects
89

9-
from .models import Milestone, Task
10+
from .models import Contract, Task
1011

1112
if typing.TYPE_CHECKING:
12-
from .types import MilestoneType, TaskType
13+
from .types import ContractType, TaskType
1314

1415

1516
def load_task(keys: list[int]) -> list["TaskType"]:
1617
return load_model_objects(Task, keys) # type: ignore[reportReturnType]
1718

1819

19-
def load_milestone(keys: list[int]) -> list["MilestoneType"]:
20-
return load_model_objects(Milestone, keys) # type: ignore[reportReturnType]
20+
def load_contract(keys: list[int]) -> list["ContractType"]:
21+
return load_model_objects(Contract, keys) # type: ignore[reportReturnType]
22+
23+
24+
def load_total_tasks_estimated_hours_by_contract(keys: list[int]) -> list[float]:
25+
task_qs = (
26+
Task.objects.filter(contract__in=keys)
27+
.order_by()
28+
.values("contract")
29+
.annotate(
30+
total_estimated_hours=models.Sum("estimated_hours"),
31+
)
32+
)
33+
_map = {
34+
contract_id: total_estimated_hours
35+
for contract_id, total_estimated_hours in task_qs.values_list("contract", "total_estimated_hours")
36+
}
37+
return [_map.get(key, 0) for key in keys]
2138

2239

2340
class TrackDataLoader:
@@ -27,5 +44,9 @@ def load_task(self):
2744
return DataLoader(load_fn=sync_to_async(load_task))
2845

2946
@cached_property
30-
def load_milestone(self):
31-
return DataLoader(load_fn=sync_to_async(load_milestone))
47+
def load_contract(self):
48+
return DataLoader(load_fn=sync_to_async(load_contract))
49+
50+
@cached_property
51+
def load_total_tasks_estimated_hours_by_contract(self):
52+
return DataLoader(load_fn=sync_to_async(load_total_tasks_estimated_hours_by_contract))

0 commit comments

Comments
 (0)