diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 0000000..3165ebe --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,17 @@ +name: Tests + +on: + push: + branches: + - main + pull_request: + +jobs: + app-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Run tests + run: | + cd dashboard + make test diff --git a/README.md b/README.md index 71a6ef4..9e26183 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ The application will run in a Kubernetes cluster, so we need a container image f Creating the container image might take several minutes, so this is a good point to take a break. When you return, you should see the following output: > ``` -> Packed dashboard_0.23_amd64.rock +> Packed dashboard_0.24_amd64.rock > ``` ### Create a charm @@ -109,10 +109,10 @@ Creating the charm might take several minutes, so this is another good point to ``` { name=deploy-dashboard } cd ~/dashboard rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \ - oci-archive:dashboard_0.23_amd64.rock \ - docker://localhost:32000/dashboard:0.23 + oci-archive:dashboard_0.24_amd64.rock \ + docker://localhost:32000/dashboard:0.24 juju deploy ./charm/dashboard_ubuntu-22.04-amd64.charm \ - --resource django-app-image=localhost:32000/dashboard:0.23 + --resource django-app-image=localhost:32000/dashboard:0.24 ``` The `rockcraft.skopeo` command makes the container image available to Juju. diff --git a/dashboard/Makefile b/dashboard/Makefile index 098ed8d..a4ce109 100644 --- a/dashboard/Makefile +++ b/dashboard/Makefile @@ -34,6 +34,7 @@ run: init test: install $(PIP) install -r requirements-dev.txt + $(VENV)/bin/python -m playwright install $(PYTEST) clean: diff --git a/dashboard/dashboard/static/base-styles.css b/dashboard/dashboard/static/base-styles.css index d0a391b..dbcea28 100644 --- a/dashboard/dashboard/static/base-styles.css +++ b/dashboard/dashboard/static/base-styles.css @@ -1,5 +1,18 @@ body {font-family: Helvetica, sans-serif; font-size: 12px;} +div.auth a { + border-radius: 999em; + background: orange; + padding: 10px; + border: 1px solid black; + text-decoration: none; + font-weight: bold; + color: black; + display: inline-block; +} + +div.auth {margin-bottom: 1em;} + /* general table styles */ th {text-align: left;} diff --git a/dashboard/initial_data.yaml b/dashboard/initial_data.yaml index 9ff91dd..07295de 100644 --- a/dashboard/initial_data.yaml +++ b/dashboard/initial_data.yaml @@ -478,6 +478,7 @@ pk: 1 fields: name: Nuclear + url: '' group: 1 owner: A. Scientist driver: A. Technician @@ -488,16 +489,18 @@ pk: 2 fields: name: Chemistry + url: https://nuclear.nut group: 1 owner: A. Chemist driver: null - last_review: 2025-02-02 + last_review: null last_review_status: 1 agreement_status: 3 - model: projects.project pk: 3 fields: name: Early modern + url: '' group: 2 owner: A. Researcher driver: Some Musician @@ -508,6 +511,7 @@ pk: 4 fields: name: Deserts and oceans + url: '' group: 4 owner: A.N. Explorer driver: A. Traveller @@ -518,6 +522,7 @@ pk: 5 fields: name: Buses + url: '' group: 4 owner: Some Company driver: Bus Driver @@ -528,6 +533,7 @@ pk: 6 fields: name: Sea creatures + url: '' group: 1 owner: A.N. Ocean driver: null @@ -538,6 +544,7 @@ pk: 7 fields: name: Food packaging + url: '' group: 3 owner: A. Chemist driver: Some Packager @@ -548,6 +555,7 @@ pk: 9 fields: name: Aeroplanes + url: '' group: 4 owner: An Aviator driver: null @@ -558,6 +566,7 @@ pk: 10 fields: name: Restaurants + url: '' group: 5 owner: A. Gourmand driver: null @@ -568,6 +577,7 @@ pk: 11 fields: name: Kinds of pasta + url: '' group: 5 owner: A. Gourmand driver: null @@ -578,6 +588,7 @@ pk: 12 fields: name: Cafés + url: '' group: 5 owner: A. Director driver: null @@ -588,6 +599,7 @@ pk: 13 fields: name: Switches + url: '' group: 6 owner: A.N. Electrician driver: null @@ -598,6 +610,7 @@ pk: 14 fields: name: Plugs + url: '' group: 6 owner: A.N. Electrician driver: null @@ -608,6 +621,7 @@ pk: 15 fields: name: Sockets + url: '' group: 6 owner: A.N. Electrician driver: null @@ -618,6 +632,7 @@ pk: 16 fields: name: Photography + url: '' group: 7 owner: A. Lens driver: null @@ -628,6 +643,7 @@ pk: 17 fields: name: Sculpture + url: '' group: 7 owner: null driver: null @@ -638,6 +654,7 @@ pk: 18 fields: name: Collage + url: '' group: 7 owner: A.N. Artist driver: null @@ -661,7 +678,7 @@ fields: project: 3 objective: 1 - unstarted_reason: null + unstarted_reason: 2 - model: projects.projectobjective pk: 4 fields: @@ -1773,7 +1790,7 @@ objective: 1 condition: 1 done: true - not_applicable: false + not_applicable: true candidate: false - model: projects.projectobjectivecondition pk: 2 @@ -1781,8 +1798,8 @@ project: 2 objective: 1 condition: 1 - done: true - not_applicable: false + done: false + not_applicable: true candidate: false - model: projects.projectobjectivecondition pk: 3 @@ -1790,7 +1807,7 @@ project: 3 objective: 1 condition: 1 - done: true + done: false not_applicable: false candidate: false - model: projects.projectobjectivecondition @@ -1808,9 +1825,9 @@ project: 2 objective: 1 condition: 2 - done: true + done: false not_applicable: false - candidate: false + candidate: true - model: projects.projectobjectivecondition pk: 6 fields: @@ -1819,7 +1836,7 @@ condition: 2 done: true not_applicable: false - candidate: false + candidate: true - model: projects.projectobjectivecondition pk: 7 fields: @@ -1970,9 +1987,9 @@ project: 3 objective: 1 condition: 6 - done: true + done: false not_applicable: false - candidate: false + candidate: true - model: projects.projectobjectivecondition pk: 24 fields: @@ -26094,7 +26111,7 @@ project: 1 objective: 1 level: 1 - committed: false + committed: true - model: projects.commitment pk: 2984 fields: @@ -26270,7 +26287,7 @@ project: 3 objective: 1 level: 1 - committed: false + committed: true - model: projects.commitment pk: 3006 fields: @@ -35579,666 +35596,35 @@ project: 17 workcycle: 7 value: 0 -- model: auth.permission - pk: 1 - fields: - name: Can add project group - content_type: 1 - codename: add_projectgroup -- model: auth.permission - pk: 2 - fields: - name: Can change project group - content_type: 1 - codename: change_projectgroup -- model: auth.permission - pk: 3 - fields: - name: Can delete project group - content_type: 1 - codename: delete_projectgroup -- model: auth.permission - pk: 4 - fields: - name: Can view project group - content_type: 1 - codename: view_projectgroup -- model: auth.permission - pk: 5 - fields: - name: Can add project - content_type: 2 - codename: add_project -- model: auth.permission - pk: 6 - fields: - name: Can change project - content_type: 2 - codename: change_project -- model: auth.permission - pk: 7 - fields: - name: Can delete project - content_type: 2 - codename: delete_project -- model: auth.permission - pk: 8 - fields: - name: Can view project - content_type: 2 - codename: view_project -- model: auth.permission - pk: 9 - fields: - name: Can add project objective - content_type: 3 - codename: add_projectobjective -- model: auth.permission - pk: 10 - fields: - name: Can change project objective - content_type: 3 - codename: change_projectobjective -- model: auth.permission - pk: 11 - fields: - name: Can delete project objective - content_type: 3 - codename: delete_projectobjective -- model: auth.permission - pk: 12 - fields: - name: Can view project objective - content_type: 3 - codename: view_projectobjective -- model: auth.permission - pk: 13 - fields: - name: Can add project objective condition - content_type: 4 - codename: add_projectobjectivecondition -- model: auth.permission - pk: 14 - fields: - name: Can change project objective condition - content_type: 4 - codename: change_projectobjectivecondition -- model: auth.permission - pk: 15 - fields: - name: Can delete project objective condition - content_type: 4 - codename: delete_projectobjectivecondition -- model: auth.permission - pk: 16 - fields: - name: Can view project objective condition - content_type: 4 - codename: view_projectobjectivecondition -- model: auth.permission - pk: 17 - fields: - name: Can add qi - content_type: 5 - codename: add_qi -- model: auth.permission - pk: 18 - fields: - name: Can change qi - content_type: 5 - codename: change_qi -- model: auth.permission - pk: 19 - fields: - name: Can delete qi - content_type: 5 - codename: delete_qi -- model: auth.permission - pk: 20 - fields: - name: Can view qi - content_type: 5 - codename: view_qi -- model: auth.permission - pk: 21 - fields: - name: Can add level commitment - content_type: 6 - codename: add_commitment -- model: auth.permission - pk: 22 - fields: - name: Can change level commitment - content_type: 6 - codename: change_commitment -- model: auth.permission - pk: 23 - fields: - name: Can delete level commitment - content_type: 6 - codename: delete_commitment -- model: auth.permission - pk: 24 - fields: - name: Can view level commitment - content_type: 6 - codename: view_commitment -- model: auth.permission - pk: 25 - fields: - name: Can add level - content_type: 7 - codename: add_level -- model: auth.permission - pk: 26 - fields: - name: Can change level - content_type: 7 - codename: change_level -- model: auth.permission - pk: 27 - fields: - name: Can delete level - content_type: 7 - codename: delete_level -- model: auth.permission - pk: 28 - fields: - name: Can view level - content_type: 7 - codename: view_level -- model: auth.permission - pk: 29 - fields: - name: Can add objective - content_type: 8 - codename: add_objective -- model: auth.permission - pk: 30 - fields: - name: Can change objective - content_type: 8 - codename: change_objective -- model: auth.permission - pk: 31 - fields: - name: Can delete objective - content_type: 8 - codename: delete_objective -- model: auth.permission - pk: 32 - fields: - name: Can view objective - content_type: 8 - codename: view_objective -- model: auth.permission - pk: 33 - fields: - name: Can add objective group - content_type: 9 - codename: add_objectivegroup -- model: auth.permission - pk: 34 - fields: - name: Can change objective group - content_type: 9 - codename: change_objectivegroup -- model: auth.permission - pk: 35 - fields: - name: Can delete objective group - content_type: 9 - codename: delete_objectivegroup -- model: auth.permission - pk: 36 - fields: - name: Can view objective group - content_type: 9 - codename: view_objectivegroup -- model: auth.permission - pk: 37 - fields: - name: Can add project status - content_type: 10 - codename: add_projectstatus -- model: auth.permission - pk: 38 - fields: - name: Can change project status - content_type: 10 - codename: change_projectstatus -- model: auth.permission - pk: 39 - fields: - name: Can delete project status - content_type: 10 - codename: delete_projectstatus -- model: auth.permission - pk: 40 - fields: - name: Can view project status - content_type: 10 - codename: view_projectstatus -- model: auth.permission - pk: 41 - fields: - name: Can add work cycle - content_type: 11 - codename: add_workcycle -- model: auth.permission - pk: 42 - fields: - name: Can change work cycle - content_type: 11 - codename: change_workcycle -- model: auth.permission - pk: 43 - fields: - name: Can delete work cycle - content_type: 11 - codename: delete_workcycle -- model: auth.permission - pk: 44 - fields: - name: Can view work cycle - content_type: 11 - codename: view_workcycle -- model: auth.permission - pk: 45 - fields: - name: Can add condition - content_type: 12 - codename: add_condition -- model: auth.permission - pk: 46 - fields: - name: Can change condition - content_type: 12 - codename: change_condition -- model: auth.permission - pk: 47 - fields: - name: Can delete condition - content_type: 12 - codename: delete_condition -- model: auth.permission - pk: 48 - fields: - name: Can view condition - content_type: 12 - codename: view_condition -- model: auth.permission - pk: 49 - fields: - name: Can add reason - content_type: 13 - codename: add_reason -- model: auth.permission - pk: 50 - fields: - name: Can change reason - content_type: 13 - codename: change_reason -- model: auth.permission - pk: 51 - fields: - name: Can delete reason - content_type: 13 - codename: delete_reason -- model: auth.permission - pk: 52 - fields: - name: Can view reason - content_type: 13 - codename: view_reason -- model: auth.permission - pk: 53 - fields: - name: Can add log entry - content_type: 14 - codename: add_logentry -- model: auth.permission - pk: 54 - fields: - name: Can change log entry - content_type: 14 - codename: change_logentry -- model: auth.permission - pk: 55 - fields: - name: Can delete log entry - content_type: 14 - codename: delete_logentry -- model: auth.permission - pk: 56 - fields: - name: Can view log entry - content_type: 14 - codename: view_logentry -- model: auth.permission - pk: 57 - fields: - name: Can add permission - content_type: 15 - codename: add_permission -- model: auth.permission - pk: 58 - fields: - name: Can change permission - content_type: 15 - codename: change_permission -- model: auth.permission - pk: 59 - fields: - name: Can delete permission - content_type: 15 - codename: delete_permission -- model: auth.permission - pk: 60 - fields: - name: Can view permission - content_type: 15 - codename: view_permission -- model: auth.permission - pk: 61 - fields: - name: Can add group - content_type: 16 - codename: add_group -- model: auth.permission - pk: 62 - fields: - name: Can change group - content_type: 16 - codename: change_group -- model: auth.permission - pk: 63 - fields: - name: Can delete group - content_type: 16 - codename: delete_group -- model: auth.permission - pk: 64 - fields: - name: Can view group - content_type: 16 - codename: view_group -- model: auth.permission - pk: 65 - fields: - name: Can add user - content_type: 17 - codename: add_user -- model: auth.permission - pk: 66 - fields: - name: Can change user - content_type: 17 - codename: change_user -- model: auth.permission - pk: 67 - fields: - name: Can delete user - content_type: 17 - codename: delete_user -- model: auth.permission - pk: 68 - fields: - name: Can view user - content_type: 17 - codename: view_user -- model: auth.permission - pk: 69 - fields: - name: Can add content type - content_type: 18 - codename: add_contenttype -- model: auth.permission - pk: 70 - fields: - name: Can change content type - content_type: 18 - codename: change_contenttype -- model: auth.permission - pk: 71 - fields: - name: Can delete content type - content_type: 18 - codename: delete_contenttype -- model: auth.permission - pk: 72 - fields: - name: Can view content type - content_type: 18 - codename: view_contenttype -- model: auth.permission - pk: 73 - fields: - name: Can add session - content_type: 19 - codename: add_session -- model: auth.permission - pk: 74 - fields: - name: Can change session - content_type: 19 - codename: change_session -- model: auth.permission - pk: 75 - fields: - name: Can delete session - content_type: 19 - codename: delete_session -- model: auth.permission - pk: 76 - fields: - name: Can view session - content_type: 19 - codename: view_session -- model: auth.permission - pk: 77 - fields: - name: Can add agreement status - content_type: 20 - codename: add_agreementstatus -- model: auth.permission - pk: 78 - fields: - name: Can change agreement status - content_type: 20 - codename: change_agreementstatus -- model: auth.permission - pk: 79 - fields: - name: Can delete agreement status - content_type: 20 - codename: delete_agreementstatus -- model: auth.permission - pk: 80 - fields: - name: Can view agreement status - content_type: 20 - codename: view_agreementstatus -- model: auth.permission - pk: 81 - fields: - name: Can add agreement status - content_type: 14 - codename: add_agreementstatus -- model: auth.permission - pk: 82 - fields: - name: Can change agreement status - content_type: 14 - codename: change_agreementstatus -- model: auth.permission - pk: 83 - fields: - name: Can delete agreement status - content_type: 14 - codename: delete_agreementstatus -- model: auth.permission - pk: 84 - fields: - name: Can view agreement status - content_type: 14 - codename: view_agreementstatus -- model: auth.permission - pk: 85 - fields: - name: Can add log entry - content_type: 15 - codename: add_logentry -- model: auth.permission - pk: 86 - fields: - name: Can change log entry - content_type: 15 - codename: change_logentry -- model: auth.permission - pk: 87 - fields: - name: Can delete log entry - content_type: 15 - codename: delete_logentry -- model: auth.permission - pk: 88 - fields: - name: Can view log entry - content_type: 15 - codename: view_logentry -- model: auth.permission - pk: 89 - fields: - name: Can add permission - content_type: 16 - codename: add_permission -- model: auth.permission - pk: 90 - fields: - name: Can change permission - content_type: 16 - codename: change_permission -- model: auth.permission - pk: 91 - fields: - name: Can delete permission - content_type: 16 - codename: delete_permission -- model: auth.permission - pk: 92 - fields: - name: Can view permission - content_type: 16 - codename: view_permission -- model: auth.permission - pk: 93 - fields: - name: Can add group - content_type: 17 - codename: add_group -- model: auth.permission - pk: 94 - fields: - name: Can change group - content_type: 17 - codename: change_group -- model: auth.permission - pk: 95 - fields: - name: Can delete group - content_type: 17 - codename: delete_group -- model: auth.permission - pk: 96 - fields: - name: Can view group - content_type: 17 - codename: view_group -- model: auth.permission - pk: 97 - fields: - name: Can add user - content_type: 18 - codename: add_user -- model: auth.permission - pk: 98 - fields: - name: Can change user - content_type: 18 - codename: change_user -- model: auth.permission - pk: 99 - fields: - name: Can delete user - content_type: 18 - codename: delete_user -- model: auth.permission - pk: 100 - fields: - name: Can view user - content_type: 18 - codename: view_user -- model: auth.permission - pk: 101 - fields: - name: Can add content type - content_type: 19 - codename: add_contenttype -- model: auth.permission - pk: 102 - fields: - name: Can change content type - content_type: 19 - codename: change_contenttype -- model: auth.permission - pk: 103 - fields: - name: Can delete content type - content_type: 19 - codename: delete_contenttype -- model: auth.permission - pk: 104 - fields: - name: Can view content type - content_type: 19 - codename: view_contenttype -- model: auth.permission - pk: 105 - fields: - name: Can add session - content_type: 20 - codename: add_session -- model: auth.permission - pk: 106 - fields: - name: Can change session - content_type: 20 - codename: change_session -- model: auth.permission - pk: 107 - fields: - name: Can delete session - content_type: 20 - codename: delete_session -- model: auth.permission - pk: 108 - fields: - name: Can view session - content_type: 20 - codename: view_session - model: auth.user pk: 1 fields: - password: pbkdf2_sha256$870000$wrLgDLm6LnfRMMx70aSref$+/mKdBWGs7a62LOYowsqKLv+QpJ71gCk/+cd3vPEuNY= - last_login: 2025-09-04 19:50:49.016048+00:00 + password: pbkdf2_sha256$870000$hT8SyrMNY9MlEyhUPlumxb$VfCsnVHt11Gxk2ERdWf0dbrzv3CwsxFt2HOS/pZzCNk= + last_login: 2025-09-12 06:21:50.612823+00:00 is_superuser: true - username: test + username: superuser first_name: '' last_name: '' email: '' is_staff: true is_active: true - date_joined: 2025-02-02 00:10:09.583882+00:00 + date_joined: 2025-09-12 06:19:30.852188+00:00 groups: [] user_permissions: [] +- model: auth.user + pk: 2 + fields: + password: pbkdf2_sha256$870000$bj2XMSwNv6RSmyDjdE9I8C$6VPMgYpLnHI4QoBjbIyDkmDa5BuihSG1vQAZ3casJVg= + last_login: 2025-09-12 06:20:53+00:00 + is_superuser: false + username: driver + first_name: '' + last_name: '' + email: '' + is_staff: true + is_active: true + date_joined: 2025-09-12 06:19:47+00:00 + groups: [] + user_permissions: + - 22 + - 14 diff --git a/dashboard/projects/admin.py b/dashboard/projects/admin.py index 09c4559..d95c3d8 100644 --- a/dashboard/projects/admin.py +++ b/dashboard/projects/admin.py @@ -113,7 +113,7 @@ class ProjectAdmin(admin.ModelAdmin): None, { "fields": ( - ("name", "group"), + ("name", "url", "group"), ("owner", "driver"), ("agreement_status"), ("last_review", "last_review_status"), diff --git a/dashboard/projects/forms.py b/dashboard/projects/forms.py index 1453262..5a8ebf7 100644 --- a/dashboard/projects/forms.py +++ b/dashboard/projects/forms.py @@ -1,13 +1,17 @@ from . import models -from django.forms import ModelForm -from django.forms import inlineformset_factory +from django import forms +class DatePickerInput(forms.DateInput): + input_type = 'date' + + +class ProjectDetailForm(forms.ModelForm): -class ProjectDetailForm(ModelForm): class Meta: model = models.Project fields = [ "name", + "url", "group", "owner", "driver", @@ -15,9 +19,14 @@ class Meta: "last_review", "last_review_status", ] + widgets = { + 'last_review' : DatePickerInput(), + } + + # last_review = forms.DateField(widget=forms.DateInput) -class ProjectObjectiveForm(ModelForm): +class ProjectObjectiveForm(forms.ModelForm): class Meta: model = models.ProjectObjective fields = ["unstarted_reason"] @@ -25,13 +34,13 @@ class Meta: help_texts = {"unstarted_reason": ""} -class ProjectObjectiveConditionForm(ModelForm): +class ProjectObjectiveConditionForm(forms.ModelForm): class Meta: model = models.ProjectObjectiveCondition fields = ["done"] -class CommitmentForm(ModelForm): +class CommitmentForm(forms.ModelForm): class Meta: model = models.Commitment fields = ["committed"] diff --git a/dashboard/projects/migrations/0010_alter_projectobjective_options_project_url.py b/dashboard/projects/migrations/0010_alter_projectobjective_options_project_url.py new file mode 100644 index 0000000..30c25f9 --- /dev/null +++ b/dashboard/projects/migrations/0010_alter_projectobjective_options_project_url.py @@ -0,0 +1,22 @@ +# Generated by Django 5.1.5 on 2025-09-11 18:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("projects", "0009_rename_levelcommitment_commitment"), + ] + + operations = [ + migrations.AlterModelOptions( + name="projectobjective", + options={"ordering": ["objective"]}, + ), + migrations.AddField( + model_name="project", + name="url", + field=models.URLField(default="", null=True), + ), + ] diff --git a/dashboard/projects/migrations/0011_alter_project_url.py b/dashboard/projects/migrations/0011_alter_project_url.py new file mode 100644 index 0000000..6027339 --- /dev/null +++ b/dashboard/projects/migrations/0011_alter_project_url.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.5 on 2025-09-11 18:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("projects", "0010_alter_projectobjective_options_project_url"), + ] + + operations = [ + migrations.AlterField( + model_name="project", + name="url", + field=models.URLField(blank=True, default=""), + ), + ] diff --git a/dashboard/projects/models.py b/dashboard/projects/models.py index 7a459c7..08929bb 100644 --- a/dashboard/projects/models.py +++ b/dashboard/projects/models.py @@ -25,6 +25,7 @@ def __str__(self): class Project(models.Model): name = models.CharField(max_length=200) + url = models.URLField(blank=True, default="") group = models.ForeignKey( ProjectGroup, null=True, blank=True, on_delete=models.SET_NULL ) @@ -132,6 +133,7 @@ def achieved_level(self): objective=self.objective, condition__level=level, done=False, + not_applicable=False ).exists(): level_achieved = level else: diff --git a/dashboard/projects/templates/projects/partial_condition.html b/dashboard/projects/templates/projects/partial_condition.html index e75bcce..b0ab392 100644 --- a/dashboard/projects/templates/projects/partial_condition.html +++ b/dashboard/projects/templates/projects/partial_condition.html @@ -27,7 +27,7 @@ {% else %} class="condition not-applicable has-perms" hx-put="{% url 'projects:action_condition_toggle_not_applicable' condition.id %}" - hx-trigger="click" + hx-trigger="click delay:.5s" hx-target="closest tr" hx-swap="outerHTML" {% endif %}>na @@ -37,7 +37,7 @@ {% else %} class="condition candidate has-perms" hx-put="{% url 'projects:action_condition_toggle_candidate' condition.id %}" - hx-trigger="click" + hx-trigger="click delay:.5s" hx-target="closest tr" hx-swap="outerHTML" {% endif %}>candidate diff --git a/dashboard/projects/templates/projects/partial_objectivestatus.html b/dashboard/projects/templates/projects/partial_objectivestatus.html index f6638d1..ef7d328 100644 --- a/dashboard/projects/templates/projects/partial_objectivestatus.html +++ b/dashboard/projects/templates/projects/partial_objectivestatus.html @@ -13,7 +13,7 @@ {% else %} - {% endif %} - - +
+ + {% if not user.is_authenticated %} +
Log in to edit
+ {% endif %} + +
+
+ + + {% if project.url %} + + + + + {% endif %} + + {% for field in basics_form %} + + {% endfor %} + +
Website{{ project.url }}
{{ field.label_tag }}{{ field }}
+ + {% if perms.projects.change_projectobjectivecondition %} + + {% endif %} + +
+
+ diff --git a/dashboard/projects/templates/projects/partial_projectobjectives.html b/dashboard/projects/templates/projects/partial_projectobjectives.html index a59787e..67cd26d 100644 --- a/dashboard/projects/templates/projects/partial_projectobjectives.html +++ b/dashboard/projects/templates/projects/partial_projectobjectives.html @@ -8,66 +8,62 @@ - - {{ projectobjectives_formset.management_form }} + {% for projectobjectivegroup, projectobjectives in projectobjectivegroups %} + + {% if projectobjectivegroup %} + + + + {% endif %} + + {% for projectobjective in projectobjectives %} + + + + + + + + + + + + {% include "projects/partial_objectivestatus.html" %} + + {% for work_cycle in work_cycles %} + + {% endfor %} - {% for projectobjectivegroup, projectobjectives in projectobjectivegroups %} + - {% if projectobjectivegroup %} - - - - {% endif %} + {% regroup projectobjective.projectobjectiveconditions by level as levels %} + {% regroup projectobjective.commitments by level as commitments %} + {% pack levels commitments as level_collections %} - {% for projectobjective in projectobjectives %} + {% for condition, commitment in level_collections %} - - - + + - - - - - - - {% include "projects/partial_objectivestatus.html" %} - - {% for work_cycle in work_cycles %} - + {% for commitment in commitment.list %} + {% include "projects/partial_commitment.html" %} {% endfor %} - {% regroup projectobjective.projectobjectiveconditions by level as levels %} - {% regroup projectobjective.commitments by level as commitments %} - {% pack levels commitments as level_collections %} - - {% for condition, commitment in level_collections %} - - - - - {% for commitment in commitment.list %} - {% include "projects/partial_commitment.html" %} - {% endfor %} - - - - {% for condition in condition.list %} - {% include "projects/partial_condition.html" %} - {% endfor %} - + {% for condition in condition.list %} + {% include "projects/partial_condition.html" %} {% endfor %} + {% endfor %} {% endfor %} - + {% endfor %}
+ {{ projectobjectivegroup }} +
+ {{ projectobjective.objective }} +
+ {{ projectobjective.objective.description|default:"" }} +
{{ work_cycle }}
- {{ projectobjectivegroup }} -
- {{ projectobjective.objective }} -
{{ condition.grouper }}
- {{ projectobjective.objective.description|default:"" }} -
{{ work_cycle }}
{{ condition.grouper }}
diff --git a/dashboard/requirements-dev.txt b/dashboard/requirements-dev.txt index ec7b1a2..3333980 100644 --- a/dashboard/requirements-dev.txt +++ b/dashboard/requirements-dev.txt @@ -1,5 +1,4 @@ pytest==8.3.4 -pytest_django==4.10.0 +pytest-django==4.10.0 pytest-playwright==0.7.0 -pytest-django==4.9.0 pytest-env==1.1.5 diff --git a/dashboard/staticfiles/base-styles.css b/dashboard/staticfiles/base-styles.css index d0a391b..dbcea28 100644 --- a/dashboard/staticfiles/base-styles.css +++ b/dashboard/staticfiles/base-styles.css @@ -1,5 +1,18 @@ body {font-family: Helvetica, sans-serif; font-size: 12px;} +div.auth a { + border-radius: 999em; + background: orange; + padding: 10px; + border: 1px solid black; + text-decoration: none; + font-weight: bold; + color: black; + display: inline-block; +} + +div.auth {margin-bottom: 1em;} + /* general table styles */ th {text-align: left;} diff --git a/dashboard/tests.py b/dashboard/tests.py new file mode 100644 index 0000000..69e3fbe --- /dev/null +++ b/dashboard/tests.py @@ -0,0 +1,101 @@ +import time + +import pytest + +from playwright.sync_api import Page, expect + +from django.core.management import call_command +from django.urls import reverse +from django.contrib.auth.models import User +from django.test.client import Client +from django.conf import settings + +from projects.models import ( + Project, + ProjectObjective, + ProjectObjectiveCondition, + Commitment, +) + +from framework.models import Level + +@pytest.fixture(scope="session") +def django_db_setup(django_db_setup, django_db_blocker): + with django_db_blocker.unblock(): + call_command("loaddata", "initial_data.yaml") + +def reverse_url( + live_server, viewname, urlconf=None, args=None, kwargs=None, current_app=None +): + end = reverse(viewname, urlconf, args, kwargs, current_app) + return f"{live_server.url}{end}" + +@pytest.fixture +def page(page, live_server): + + c = Client() + c.login(username="superuser", password="superuser") + session_cookie = c.cookies[settings.SESSION_COOKIE_NAME] + page.context.add_cookies([{ + "name": settings.SESSION_COOKIE_NAME, + "value": session_cookie.value, + "url": live_server.url, + }]) + return page + + +def test_toggling_conditions(live_server, page): + + url = reverse_url(live_server, viewname="projects:project", args=[1]) + page.goto(url) + + # check ProjectObjectiveCondition + # check that the expected conditions are represented on the page + assert ProjectObjectiveCondition.objects.count() == 697 + assert ProjectObjectiveCondition.objects.get(id=94).done == True + assert ProjectObjectiveCondition.objects.get(id=102).done == False + + # after toggling, the new conditions should be saved in the database + page.get_by_test_id("toggle_condition_94").uncheck() + page.get_by_test_id("toggle_condition_102").check() + time.sleep(0.5) # Temporary workaround. Find a better solution. + + assert ProjectObjectiveCondition.objects.get(id=94).done == False + assert ProjectObjectiveCondition.objects.get(id=102).done == True + + # check Commitment + # check that the expected commitments are represented on the page + assert Commitment.objects.get(id=705).committed == False + assert Commitment.objects.get(id=474).committed == True + + page.get_by_test_id("toggle_commitment_705").check() + page.get_by_test_id("toggle_commitment_474").uncheck() + time.sleep(0.5) # Temporary workaround. Find a better solution. + + assert Commitment.objects.get(id=705).committed == True + assert Commitment.objects.get(id=474).committed == False + + # check Status + # check that the expected conditions and status are represented on the page + assert ProjectObjectiveCondition.objects.get(id=1).done == True + assert ProjectObjectiveCondition.objects.get(id=6).done == True + assert ProjectObjectiveCondition.objects.get(id=10).done == False + assert ProjectObjectiveCondition.objects.get(id=10).not_applicable == False + assert ( + ProjectObjectiveCondition.objects.get(id=10).projectobjective().status() == None + ) + expect(page.get_by_test_id("projectobjective_status_1")).to_contain_text("") + + # check the remaining box to get to Started + page.get_by_test_id("toggle_condition_10").check() + assert ProjectObjectiveCondition.objects.get( + id=10 + ).projectobjective().status() == Level.objects.get(id=1) + expect(page.get_by_test_id("projectobjective_status_1")).to_contain_text("Started") + + # check one more to get to First results + page.get_by_test_id("toggle_condition_14").check() + assert ProjectObjectiveCondition.objects.get( + id=14 + ).projectobjective().status() == Level.objects.get(id=1) + expect(page.get_by_test_id("projectobjective_status_1")).to_contain_text("Started") diff --git a/rockcraft.yaml b/rockcraft.yaml index d9348d2..070933a 100644 --- a/rockcraft.yaml +++ b/rockcraft.yaml @@ -2,7 +2,7 @@ name: dashboard # see https://documentation.ubuntu.com/rockcraft/en/1.8.0/explanation/bases/ # for more information about bases and using 'bare' bases for chiselled rocks base: ubuntu@22.04 # the base environment for this Django application -version: "0.23" # just for humans. Semantic versioning is recommended +version: "0.24" # just for humans. Semantic versioning is recommended summary: A summary of your Django application # 79 char long summary description: | Dashboard is a Django application to track quality and progress of multiple