Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement DatasetWizardView with skippable steps and refactor form handling #441

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ You are encouraged to try Daisy for yourself using our [DEMO deployment](https:/
1. Create initial data in the database

```bash
docker-compose exec web bash -c "apt update && apt install -y wget"
docker-compose exec web bash -c "cd core/fixtures/ && wget https://git-r3lab.uni.lu/pinar.alper/metadata-tools/raw/master/metadata_tools/resources/edda.json && wget https://git-r3lab.uni.lu/pinar.alper/metadata-tools/raw/master/metadata_tools/resources/hpo.json && wget https://git-r3lab.uni.lu/pinar.alper/metadata-tools/raw/master/metadata_tools/resources/hdo.json && wget https://git-r3lab.uni.lu/pinar.alper/metadata-tools/raw/master/metadata_tools/resources/hgnc.json"
docker-compose exec web python manage.py load_initial_data
```
Expand All @@ -69,7 +70,7 @@ You are encouraged to try Daisy for yourself using our [DEMO deployment](https:/
```bash
docker-compose exec web python manage.py load_demo_data
```
This will create mock datasets, projects and create an demo admin account.
This will create mock datasets, projects and create a demo admin account.

1. Optional - import users from an active directory instance

Expand Down Expand Up @@ -170,7 +171,7 @@ where ${JSON_FILE} is the path to a json file that will be produced. In additio

```bash
docker-compose exec web python manage.py rebuild_index -u default
```
```
1. Reimport the users (optional).

If LDAP was used during initial setup to import users, they have to be imported again:
Expand Down Expand Up @@ -235,6 +236,10 @@ The following command will install the test dependencies and execute the tests:
```bash
python setup.py pytest
```
run test for a specific file:
```bash
python setup.py pytest --addopts web/tests/test_dataset.py
```

If tests dependencies are already installed, one can also run the tests just by executing:

Expand Down
10 changes: 6 additions & 4 deletions core/forms/__init__.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
from .cohort import CohortForm, CohortFormEdit
from .contact import ContactForm, PickContactForm
from .contract import ContractForm, ContractFormEdit, PartnerRoleForm
from .dataset import DatasetForm, SkipFieldValidationMixin
from .use_restriction import UseRestrictionForm
from .data_declaration import DataDeclarationForm, DataDeclarationSubFormOther, DataDeclarationSubFormNew, \
DataDeclarationEditForm, DataDeclarationSubFormFromExisting
from .dataset import DatasetForm
from .document import DocumentForm
from .partner import PartnerForm
from .permission import UserPermFormSet
from .legal_basis import LegalBasisForm
from .publication import PublicationForm, PickPublicationForm
from .share import ShareForm


from .access import AccessForm

__all__ = [
"ContractForm",
Expand All @@ -34,5 +34,7 @@
"PublicationForm",
"ShareForm",
"PickPublicationForm",
"UseRestrictionForm"
"UseRestrictionForm",
"AccessForm",
"SkipFieldValidationMixin",
]
8 changes: 5 additions & 3 deletions core/forms/access.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from django.forms import ModelForm, DateInput, Textarea

from core.forms import SkipFieldValidationMixin
from core.models import Access, Contact


class AccessForm(ModelForm):
class AccessForm(SkipFieldValidationMixin, ModelForm):
class Meta:
model = Access
fields = "__all__"
Expand All @@ -15,7 +15,9 @@ class Meta:
# Textareas
"access_notes": Textarea(attrs={"rows": 2, "cols": 40}),
}

heading = "Access"
heading_help = "Specify who can access the data, the duration, and any relevant notes. Ensure accuracy for data security."

def __init__(self, *args, **kwargs):
dataset = kwargs.pop("dataset", None)
super().__init__(*args, **kwargs)
Expand Down
58 changes: 29 additions & 29 deletions core/forms/data_declaration.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,40 @@
from django import forms
from django.forms import ValidationError
from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy

from core.forms.use_restriction import UseRestrictionForm
from core.forms import UseRestrictionForm, SkipFieldValidationMixin
from core.models import DataDeclaration, Partner, Contract, GDPRRole
from core.models.contract import PartnerRole


class DataDeclarationEditForm(forms.ModelForm):

class Meta:
model = DataDeclaration
fields = [
'title',
'cohorts',
'partner',
'contract',
'data_declarations_parents',
'comments',
'data_types_generated',
'data_types_received',
'deidentification_method',
'has_special_subjects',
'subjects_category',
'consent_status',
'special_subjects_description',
'end_of_storage_duration',
'storage_duration_criteria',
'embargo_date',
'data_types_notes'
]
widgets = {
# Date pickers
'end_of_storage_duration': forms.DateInput(attrs={'class': 'datepicker'}),
'embargo_date': forms.DateInput(attrs={'class': 'datepicker'}),
}
model = DataDeclaration
fields = [
'title',
'cohorts',
'partner',
'contract',
'data_declarations_parents',
'comments',
'data_types_generated',
'data_types_received',
'deidentification_method',
'has_special_subjects',
'subjects_category',
'consent_status',
'special_subjects_description',
'end_of_storage_duration',
'storage_duration_criteria',
'embargo_date',
'data_types_notes'
]
widgets = {
# Date pickers
'end_of_storage_duration': forms.DateInput(attrs={'class': 'datepicker'}),
'embargo_date': forms.DateInput(attrs={'class': 'datepicker'}),
}

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand All @@ -45,7 +44,6 @@ def __init__(self, *args, **kwargs):
self.fields['data_declarations_parents'].widget.attrs['class'] = 'ontocomplete'+ ' '+ self.fields['data_declarations_parents'].widget.attrs.get('class','')
self.fields['data_declarations_parents'].widget.attrs['data-url'] = reverse_lazy('data_dec_paginated_search')


def clean(self):
"""
Override to check selected Partner and Contract match
Expand Down Expand Up @@ -169,10 +167,12 @@ def after_save(self, data_declaration):
data_declaration.save()


class DataDeclarationForm(forms.ModelForm):
class DataDeclarationForm(SkipFieldValidationMixin, forms.ModelForm):
class Meta:
model = DataDeclaration
fields = ['title']
heading = "Data declaration"
heading_help = "Detail your data's origin for clarity. Knowing its provenance enhances its value and trust."

def __init__(self, *args, **kwargs):
self.dataset = kwargs.pop('dataset')
Expand Down
30 changes: 29 additions & 1 deletion core/forms/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,33 @@
from core.models import Dataset, User, Project
from core.models.contract import Contract

from typing import Any


class SkipFieldValidationMixin:
def __init__(self, *args: Any, **kwargs: Any) -> None:
"""
Initialize the mixin. Adds a hidden Boolean field named `skip_wizard`
to the form which determines if validation should be skipped.

Args:
*args: Variable arguments to pass to the superclass.
**kwargs: Keyword arguments to pass to the superclass.
"""
super().__init__(*args, **kwargs)
self.fields['skip_wizard'] = forms.BooleanField(initial=False, required=False, widget=forms.HiddenInput())

def is_valid(self) -> bool:
"""
Check if the form is valid. Validation is skipped if `skip_wizard` is True.

Returns:
bool: True if the form is valid or if `skip_wizard` is True, otherwise False.
"""
if self.data.get(f'{self.prefix}-skip_wizard') == 'True':
return True
return super().is_valid()


class DatasetForm(forms.ModelForm):
class Meta:
Expand All @@ -13,7 +40,8 @@ class Meta:
widgets = {
'comments': forms.Textarea(attrs={'rows': 2, 'cols': 40}),
}

heading = "Dataset"
heading_help = "Complete the form to enhance dataset quality. Your careful input benefits our community!"

def __init__(self, *args, **kwargs):
dataset = None
Expand Down
5 changes: 4 additions & 1 deletion core/forms/legal_basis.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
from django.forms import ModelForm

from core.forms import SkipFieldValidationMixin
from core.models import LegalBasis


class LegalBasisForm(ModelForm):
class LegalBasisForm(SkipFieldValidationMixin,ModelForm):
class Meta:
model = LegalBasis
fields = '__all__'
exclude = []
heading = "Data Legal Basis"
heading_help = "Define the legal grounds for processing. Ensure your data complies with relevant regulations."

def __init__(self, *args, **kwargs):
dataset = kwargs.pop('dataset', None)
Expand Down
8 changes: 6 additions & 2 deletions core/forms/storage_location.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
from django import forms
from core.models.storage_location import DataLocation

from core.forms import SkipFieldValidationMixin
from core.models import DataLocation

class StorageLocationForm(forms.ModelForm):

class StorageLocationForm(SkipFieldValidationMixin, forms.ModelForm):
class Meta:
model = DataLocation
fields = '__all__'
exclude = []
heading = "Add a new storage location"
heading_help = "Specify your data's home. Clear storage details ensure easy retrieval and management."

def __init__(self, *args, **kwargs):
dataset = kwargs.pop('dataset', None)
Expand Down
109 changes: 109 additions & 0 deletions web/static/css/dataset_wizard.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
.step-wizard-list {
background: #fff;
color: #333;
list-style-type: none;
border-radius: 10px;
display: flex;
padding: 20px 10px;
position: relative;
z-index: 10;
}

.step-wizard-item {
padding: 0 20px;
flex-basis: 0;
-webkit-box-flex: 1;
-ms-flex-positive: 1;
flex-grow: 1;
max-width: 100%;
display: flex;
flex-direction: column;
text-align: center;
min-width: 170px;
position: relative;
}

.step-wizard-item + .step-wizard-item:after {
content: "";
position: absolute;
left: 0;
top: 19px;
background: #023452;
width: 100%;
height: 2px;
transform: translateX(-50%);
z-index: -10;
}

.progress-count {
height: 40px;
width: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
font-weight: 600;
margin: 0 auto;
position: relative;
z-index: 10;
color: transparent;
}

.progress-count:after {
content: "";
height: 40px;
width: 40px;
background: #023452;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
border-radius: 50%;
z-index: -10;
}

.progress-count:before {
content: "";
height: 10px;
width: 20px;
border-left: 3px solid #fff;
border-bottom: 3px solid #fff;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -60%) rotate(-45deg);
transform-origin: center center;
}

.progress-label {
font-size: 14px;
font-weight: 600;
margin-top: 10px;
}

.current-item .progress-count:before,
.current-item ~ .step-wizard-item .progress-count:before {
display: none;
}

.current-item ~ .step-wizard-item .progress-count:after {
height: 10px;
width: 10px;
}

.current-item ~ .step-wizard-item .progress-label {
opacity: 0.5;
}

.current-item .progress-count:after {
background: #fff;
border: 2px solid #023452;
}

.current-item .progress-count {
color: #023452;
}

.skipped:after {
background: #6c757d;
}
Loading
Loading