Skip to content

Commit

Permalink
refactor: possible solution for selection of users (#188)
Browse files Browse the repository at this point in the history
* refactor: possible solution for selection of users

* chore: custom dropdown with checkboxes

* fix: fixed integrity error bug

* chore: made dropdown searchable and scrollable

* refactor: refactor code

* refactor: refactor code

* fix: validation error handling and data reload

* fix: fixed optional fields

---------

Co-authored-by: Sebastian Blechmann <[email protected]>
  • Loading branch information
sbanoeon and SBlechmann authored Sep 4, 2024
1 parent 10a4383 commit 669e197
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 39 deletions.
99 changes: 60 additions & 39 deletions app/Entirety/projects/forms.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit
from django import forms
from django.core.exceptions import ValidationError
from django.db.models import Q
Expand Down Expand Up @@ -30,17 +29,26 @@ def __init__(self, user, *args, **kwargs):
"title"
] = "The owner is assigned automatically on project creation. It can only be updated by a server admin."

self.fields["users"].widget = forms.CheckboxSelectMultiple(
attrs={
"data-bs-toggle": "tooltip",
"data-bs-placement": "left",
"title": "Include or exclude users into project",
}
self.fields["logo"].required = False
self.fields["webpage_url"].required = False

self.fields["viewers"] = forms.ModelMultipleChoiceField(
queryset=(
User.objects.exclude(id=self.instance.owner_id)
& User.objects.exclude(id=user.id)
).filter(is_server_admin=False),
widget=forms.CheckboxSelectMultiple,
required=False,
)

self.fields["users"] = forms.ModelMultipleChoiceField(
widget=forms.CheckboxSelectMultiple,
queryset=(
User.objects.exclude(id=self.instance.owner_id)
& User.objects.exclude(id=user.id)
).filter(is_server_admin=False),
required=False,
)
self.fields["users"].queryset = (
User.objects.exclude(id=self.instance.owner_id)
& User.objects.exclude(id=user.id)
).filter(is_server_admin=False)

if user in self.instance.maintainers.all():
self.fields["maintainers"].disabled = True
Expand All @@ -52,34 +60,38 @@ def __init__(self, user, *args, **kwargs):
"title"
] = "Inclusion or exclusion of maintainers into project can by done by project owners only."
else:
self.fields["maintainers"].widget = forms.CheckboxSelectMultiple(
attrs={
"data-bs-toggle": "tooltip",
"data-bs-placement": "left",
"title": "Include or exclude maintainers into project",
}
self.fields["maintainers"] = forms.ModelMultipleChoiceField(
queryset=(
User.objects.exclude(id=self.instance.owner_id)
& User.objects.exclude(id=user.id)
).filter(is_server_admin=False),
widget=forms.CheckboxSelectMultiple,
required=False,
)
self.fields["maintainers"].queryset = (
User.objects.exclude(id=self.instance.owner_id)
& User.objects.exclude(id=user.id)
).filter(is_server_admin=False)

self.fields["viewers"].widget = forms.CheckboxSelectMultiple(
attrs={
"data-bs-toggle": "tooltip",
"data-bs-placement": "left",
"title": "Include or exclude viewers into project",
}
)
self.fields["viewers"].queryset = (
User.objects.exclude(id=self.instance.owner_id)
& User.objects.exclude(id=user.id)
).filter(is_server_admin=False)

self.helper.layout.append(Submit(name="save", value="Save"))
self.helper.form_tag = False

self.fields["logo"].required = False
self.fields["webpage_url"].required = False
if self.is_bound:
self.fields["viewers"].initial = [
int(id) for id in self.data.getlist("viewers")
]
self.fields["users"].initial = [
int(id) for id in self.data.getlist("users")
]
self.fields["maintainers"].initial = [
int(id) for id in self.data.getlist("maintainers")
]
else:
if self.instance.pk:
self.fields["viewers"].initial = list(
self.instance.viewers.values_list("id", flat=True)
)
self.fields["users"].initial = list(
self.instance.users.values_list("id", flat=True)
)
self.fields["maintainers"].initial = list(
self.instance.maintainers.values_list("id", flat=True)
)

def clean(self):
cleaned_data = super().clean()
Expand All @@ -89,6 +101,18 @@ def clean(self):
except Exception as e:
raise ValidationError(e)

def save(self, commit=True):
instance = super().save(commit=True)

instance.viewers.set(self.cleaned_data["viewers"])
instance.users.set(self.cleaned_data["users"])
instance.maintainers.set(self.cleaned_data["maintainers"])

if commit:
instance.save()

return instance

class Meta:
model = Project
fields = [
Expand All @@ -98,9 +122,6 @@ class Meta:
"webpage_url",
"logo",
"owner",
"maintainers",
"users",
"viewers",
]
widgets = {
"name": forms.TextInput(
Expand Down
77 changes: 77 additions & 0 deletions app/Entirety/projects/templates/projects/update.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,83 @@ <h2 class="my-4">{% if project.name %} Edit {{ project.name }} {% else %} Create
{% if form_submitted %}class="was-validated" {% endif %}>
{% csrf_token %}
{% crispy form %}
<div class="btn-toolbar">
<div class="dropdown w-25">
<button class="btn btn-outline-secondary dropdown-toggle w-75" type="button" id="dropdownMenuButton"
data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-people"></i>
Viewers
</button>
<div class="dropdown-menu dropdown-scrollable-checkboxes" aria-labelledby="dropdownMenuButton">
<div class="px-3 py-2">
<input type="text" class="form-control" id="viewerSearch" placeholder="Search viewers...">
<div id="viewerList" class="mt-2">
{% for viewer in form.viewers.field.queryset %}
<div class="form-check">
<input class="form-check-input" type="checkbox" name="viewers"
id="viewer_{{ viewer.id }}" value="{{ viewer.id }}"
{% if viewer.id in form.fields.viewers.initial %}checked{% endif %}>
<label class="form-check-label" for="viewer_{{ viewer.id }}">
{{ viewer.username }}
</label>
</div>
{% endfor %}
</div>
</div>
</div>
</div>

<div class="dropdown w-25">
<button class="btn btn-outline-secondary dropdown-toggle w-75" type="button" id="dropdownMenuButton"
data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-people"></i>
Users
</button>
<div class="dropdown-menu dropdown-scrollable-checkboxes" aria-labelledby="dropdownMenuButton">
<div class="px-3 py-2">
<input type="text" class="form-control" id="userSearch" placeholder="Search users...">
<div id="userList" class="mt-2">
{% for user in form.users.field.queryset %}
<div class="form-check">
<input class="form-check-input" type="checkbox" name="users"
id="user_{{ viewer.id }}"
value="{{ user.id }}" {% if user.id in form.fields.users.initial %}checked{% endif %}>
<label class="form-check-label" for="user_{{ user.id }}">
{{ user.username }}
</label>
</div>
{% endfor %}
</div>
</div>
</div>
</div>

<div class="dropdown w-25">
<button class="btn btn-outline-secondary dropdown-toggle w-75" type="button" id="dropdownMenuButton"
data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-people"></i>
Maintainers
</button>
<div class="dropdown-menu dropdown-scrollable-checkboxes" aria-labelledby="dropdownMenuButton">
<div class="px-3 py-2">
<input type="text" class="form-control" id="maintainerSearch" placeholder="Search maintainers...">
<div id="maintainerList" class="mt-2">
{% for maintainer in form.maintainers.field.queryset %}
<div class="form-check">
<input class="form-check-input" type="checkbox" name="maintainers"
id="maintainer_{{ maintainer.id }}" value="{{ maintainer.id }}"
{% if maintainer.id in form.fields.maintainers.initial %}checked{% endif %}>
<label class="form-check-label" for="viewer_{{ maintainer.id }}">
{{ maintainer.username }}
</label>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
<input class="btn btn-primary mt-2" name="save" value="Save" type="submit" id="submit-id-save">
</form>
</div>

Expand Down
23 changes: 23 additions & 0 deletions app/Entirety/static/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,26 @@
}
})
})()

document.addEventListener('DOMContentLoaded', function() {
const searchListPairs = [
{ search: 'viewerSearch', list: 'viewerList' },
{ search: 'userSearch', list: 'userList' },
{ search: 'maintainerSearch', list: 'maintainerList' }
];

searchListPairs.forEach(function(pair) {
const searchElement = document.getElementById(pair.search);
const listElement = document.getElementById(pair.list);

searchElement.addEventListener('input', function() {
const query = this.value.toLowerCase();
const checkboxes = listElement.querySelectorAll('.form-check');

checkboxes.forEach(function(checkbox) {
const label = checkbox.querySelector('label').textContent.toLowerCase();
checkbox.style.display = label.includes(query) ? 'block' : 'none';
});
});
});
});
10 changes: 10 additions & 0 deletions app/Entirety/static/scss/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,13 @@
h1{
color: $primary
}

.dropdown-scrollable-checkboxes {
@extend .dropdown-scrollable !optional;
max-height: 150px;
overflow-y: auto;
}

.form-check[aria-hidden="false"] {
display: block;
}

0 comments on commit 669e197

Please sign in to comment.