Skip to content

Commit

Permalink
Merge pull request #1121 from rdmorganiser/interview-visibility
Browse files Browse the repository at this point in the history
feat(interview): Project visibility [1]
  • Loading branch information
jochenklar authored Dec 12, 2024
2 parents 612c989 + 12798ce commit 5d3bc73
Show file tree
Hide file tree
Showing 43 changed files with 1,678 additions and 451 deletions.
3 changes: 3 additions & 0 deletions rdmo/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@
'MULTISITE',
'GROUPS',
'EXPORT_FORMATS',
'PROJECT_VISIBILITY',
'PROJECT_ISSUES',
'PROJECT_VIEWS',
'PROJECT_EXPORTS',
Expand Down Expand Up @@ -296,6 +297,8 @@

PROJECT_TABLE_PAGE_SIZE = 20

PROJECT_VISIBILITY = True

PROJECT_ISSUES = True

PROJECT_ISSUE_PROVIDERS = []
Expand Down
5 changes: 5 additions & 0 deletions rdmo/core/templates/core/bootstrap_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@

{% include 'core/bootstrap_form_fields.html' %}

{% if submit %}
<input type="submit" value="{{ submit }}" class="btn btn-primary" />
{% endif %}
{% if delete %}
<input type="submit" name="delete" value="{{ delete }}" class="btn btn-danger" />
{% endif %}
<input type="submit" name="cancel" value="{% trans 'Cancel' %}" class="btn" />
</form>
7 changes: 7 additions & 0 deletions rdmo/core/templates/core/bootstrap_form_field.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{% load i18n %}
{% load widget_tweaks %}
{% load core_tags %}

Expand Down Expand Up @@ -61,6 +62,12 @@

{% render_field field class="form-control" %}

{% if type == 'selectmultiple' %}
<p class="help-block">
{% trans 'Hold down "Control", or "Command" on a Mac, to select more than one.' %}
</p>
{% endif %}

{% endif %}
{% endwith %}

Expand Down
3 changes: 3 additions & 0 deletions rdmo/core/templatetags/core_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ def bootstrap_form(context, **kwargs):
if 'submit' in kwargs:
form_context['submit'] = kwargs['submit']

if 'delete' in kwargs:
form_context['delete'] = kwargs['delete']

return render_to_string('core/bootstrap_form.html', form_context, request=context.request)


Expand Down
21 changes: 21 additions & 0 deletions rdmo/projects/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from django.db.models import Prefetch
from django.urls import reverse
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _

from .models import (
Continuation,
Expand All @@ -15,6 +16,7 @@
Project,
Snapshot,
Value,
Visibility,
)
from .validators import ProjectParentValidator

Expand Down Expand Up @@ -71,6 +73,25 @@ class ContinuationAdmin(admin.ModelAdmin):
list_display = ('project', 'user', 'page')


@admin.register(Visibility)
class VisibilityAdmin(admin.ModelAdmin):
search_fields = ('project__title', 'sites', 'groups')
list_display = ('project', 'sites_list_display', 'groups_list_display')
filter_horizontal = ('sites', 'groups')

@admin.display(description=_('Sites'))
def sites_list_display(self, obj):
return _('all Sites') if obj.sites.count() == 0 else ', '.join([
site.domain for site in obj.sites.all()
])

@admin.display(description=_('Groups'))
def groups_list_display(self, obj):
return _('all Groups') if obj.groups.count() == 0 else ', '.join([
group.name for group in obj.groups.all()
])


@admin.register(Integration)
class IntegrationAdmin(admin.ModelAdmin):
search_fields = ('project__title', 'provider_key')
Expand Down
17 changes: 17 additions & 0 deletions rdmo/projects/filters.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from django.contrib.auth.models import User
from django.db.models import F, OuterRef, Q, Subquery
from django.db.models.functions import Concat
from django.utils.dateparse import parse_datetime
Expand All @@ -18,6 +19,22 @@ class Meta:
fields = ('title', 'catalog')


class ProjectUserFilterBackend(BaseFilterBackend):

def filter_queryset(self, request, queryset, view):
if view.detail:
return queryset

user_id = request.GET.get('user')
user_username = request.GET.get('username')
if user_id or user_username:
user = User.objects.filter(Q(id=user_id) | Q(username=user_username)).first()
if user:
queryset = queryset.filter_visibility(user)

return queryset


class ProjectSearchFilterBackend(SearchFilter):

def filter_queryset(self, request, queryset, view):
Expand Down
44 changes: 43 additions & 1 deletion rdmo/projects/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from rdmo.core.utils import markdown2html

from .constants import ROLE_CHOICES
from .models import Integration, IntegrationOption, Invite, Membership, Project, Snapshot
from .models import Integration, IntegrationOption, Invite, Membership, Project, Snapshot, Visibility
from .validators import ProjectParentValidator


Expand Down Expand Up @@ -98,6 +98,48 @@ class Meta:
fields = ('title', 'description')


class ProjectUpdateVisibilityForm(forms.ModelForm):

use_required_attribute = False

def __init__(self, *args, **kwargs):
self.project = kwargs.pop('instance')
try:
instance = self.project.visibility
except Visibility.DoesNotExist:
instance = None

super().__init__(*args, instance=instance, **kwargs)

# remove the sites or group sets if they are not needed, doing this in Meta would break tests
if not settings.MULTISITE:
self.fields.pop('sites')
if not settings.GROUPS:
self.fields.pop('groups')

class Meta:
model = Visibility
fields = ('sites', 'groups')

def save(self, *args, **kwargs):
if 'cancel' in self.data:
pass
elif 'delete' in self.data:
self.instance.delete()
else:
visibility, created = Visibility.objects.update_or_create(project=self.project)

sites = self.cleaned_data.get('sites')
if sites is not None:
visibility.sites.set(sites)

groups = self.cleaned_data.get('groups')
if groups is not None:
visibility.groups.set(groups)

return self.project


class ProjectUpdateCatalogForm(forms.ModelForm):

use_required_attribute = False
Expand Down
12 changes: 11 additions & 1 deletion rdmo/projects/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,20 @@ def filter_user(self, user):
elif is_site_manager(user):
return self.filter_current_site()
else:
queryset = self.filter(user=user)
queryset = self.filter_visibility(user)
for instance in queryset:
queryset |= instance.get_descendants()
return queryset.distinct()
else:
return self.none()

def filter_visibility(self, user):
groups = user.groups.all()
sites_filter = Q(visibility__sites=None) | Q(visibility__sites=settings.SITE_ID)
groups_filter = Q(visibility__groups=None) | Q(visibility__groups__in=groups)
visibility_filter = Q(visibility__isnull=False) & sites_filter & groups_filter
return self.filter(Q(user=user) | visibility_filter)


class MembershipQuerySet(models.QuerySet):

Expand Down Expand Up @@ -157,6 +164,9 @@ def get_queryset(self):
def filter_user(self, user):
return self.get_queryset().filter_user(user)

def filter_visibility(self, user):
return self.get_queryset().filter_visibility(user)


class MembershipManager(CurrentSiteManagerMixin, models.Manager):

Expand Down
32 changes: 32 additions & 0 deletions rdmo/projects/migrations/0062_visibility.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Generated by Django 4.2.16 on 2024-12-06 10:11

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


class Migration(migrations.Migration):

dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
('sites', '0002_alter_domain_unique'),
('projects', '0061_alter_value_value_type'),
]

operations = [
migrations.CreateModel(
name='Visibility',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(editable=False, verbose_name='created')),
('updated', models.DateTimeField(editable=False, verbose_name='updated')),
('groups', models.ManyToManyField(blank=True, help_text='The groups for which the project is visible.', to='auth.group', verbose_name='Group')),
('project', models.OneToOneField(help_text='The project for this visibility.', on_delete=django.db.models.deletion.CASCADE, to='projects.project', verbose_name='Project')),
('sites', models.ManyToManyField(blank=True, help_text='The sites for which the project is visible (in a multi site setup).', to='sites.site', verbose_name='Sites')),
],
options={
'verbose_name': 'Visibility',
'verbose_name_plural': 'Visibilities',
'ordering': ('project',),
},
),
]
1 change: 1 addition & 0 deletions rdmo/projects/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
from .project import Project
from .snapshot import Snapshot
from .value import Value
from .visibility import Visibility
66 changes: 66 additions & 0 deletions rdmo/projects/models/visibility.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from django.conf import settings
from django.contrib.auth.models import Group
from django.contrib.sites.models import Site
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ngettext_lazy

from rdmo.core.models import Model


class Visibility(Model):

project = models.OneToOneField(
'Project', on_delete=models.CASCADE,
verbose_name=_('Project'),
help_text=_('The project for this visibility.')
)
sites = models.ManyToManyField(
Site, blank=True,
verbose_name=_('Sites'),
help_text=_('The sites for which the project is visible (in a multi site setup).')
)
groups = models.ManyToManyField(
Group, blank=True,
verbose_name=_('Group'),
help_text=_('The groups for which the project is visible.')
)

class Meta:
ordering = ('project', )
verbose_name = _('Visibility')
verbose_name_plural = _('Visibilities')

def __str__(self):
return str(self.project)

def is_visible(self, user):
return (
not self.sites.exists() or self.sites.filter(id=settings.SITE_ID).exists()
) and (
not self.groups.exists() or self.groups.filter(id__in=[group.id for group in user.groups.all()]).exists()
)

def get_help_display(self):
sites = self.sites.values_list('domain', flat=True)
groups = self.groups.values_list('name', flat=True)

if sites and groups:
return ngettext_lazy(
'This project can be accessed by all users on %s or in the group %s.',
'This project can be accessed by all users on %s or in the groups %s.',
len(groups)
) % (
', '.join(sites),
', '.join(groups)
)
elif sites:
return _('This project can be accessed by all users on %s.') % ', '.join(sites)
elif groups:
return ngettext_lazy(
'This project can be accessed by all users in the group %s.',
'This project can be accessed by all users in the groups %s.',
len(groups)
) % ', '.join(groups)
else:
return _('This project can be accessed by all users.')
22 changes: 22 additions & 0 deletions rdmo/projects/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,25 @@ def get_required_object_permissions(self, method, model_cls):
return ('projects.change_project_progress_object', )
else:
return ('projects.view_project_object', )


class HasProjectVisibilityModelPermission(HasModelPermission):

def get_required_permissions(self, method, model_cls):
if method == 'POST':
return ('projects.change_visibility', )
elif method == 'DELETE':
return ('projects.delete_visibility', )
else:
return ('projects.view_visibility', )


class HasProjectVisibilityObjectPermission(HasProjectPermission):

def get_required_object_permissions(self, method, model_cls):
if method == 'POST':
return ('projects.change_visibility_object', )
elif method == 'DELETE':
return ('projects.delete_visibility_object', )
else:
return ('projects.view_visibility_object', )
Loading

0 comments on commit 5d3bc73

Please sign in to comment.