Skip to content

Commit

Permalink
Refactor visibility to use the Visbility model and restrict to site_a…
Browse files Browse the repository at this point in the history
…dmins
  • Loading branch information
jochenklar committed Dec 6, 2024
1 parent 6ebb805 commit d7e251e
Show file tree
Hide file tree
Showing 27 changed files with 807 additions and 144 deletions.
3 changes: 3 additions & 0 deletions rdmo/core/templates/core/bootstrap_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@
{% include 'core/bootstrap_form_fields.html' %}

<input type="submit" value="{{ submit }}" class="btn btn-primary" />
{% 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
7 changes: 0 additions & 7 deletions rdmo/projects/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,3 @@
(ROLE_AUTHOR, _('Author')),
(ROLE_GUEST, _('Guest')),
)

VISIBILITY_PRIVATE = 'private'
VISIBILITY_INTERNAL = 'internal'
VISIBILITY_CHOICES = (
(VISIBILITY_PRIVATE, _('Private')),
(VISIBILITY_INTERNAL, _('Internal'))
)
41 changes: 36 additions & 5 deletions 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 @@ -80,8 +80,6 @@ class Meta:
fields = ['title', 'description', 'catalog']
if settings.NESTED_PROJECTS:
fields += ['parent']
if settings.PROJECT_VISIBILITY:
fields += ['visibility']

field_classes = {
'catalog': CatalogChoiceField
Expand All @@ -104,9 +102,42 @@ 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 = Project
fields = ('visibility', )
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):
Expand Down
8 changes: 5 additions & 3 deletions rdmo/projects/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
from rdmo.accounts.utils import is_site_manager
from rdmo.core.managers import CurrentSiteManagerMixin

from .constants import VISIBILITY_INTERNAL


class ProjectQuerySet(TreeQuerySet):

Expand All @@ -31,7 +29,11 @@ def filter_user(self, user):
return self.none()

def filter_visibility(self, user):
return self.filter(Q(user=user) | models.Q(visibility=VISIBILITY_INTERNAL))
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
18 changes: 0 additions & 18 deletions rdmo/projects/migrations/0062_project_visibility.py

This file was deleted.

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
21 changes: 0 additions & 21 deletions rdmo/projects/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
from rdmo.tasks.models import Task
from rdmo.views.models import View

from ..constants import VISIBILITY_CHOICES, VISIBILITY_INTERNAL, VISIBILITY_PRIVATE
from ..managers import ProjectManager


Expand Down Expand Up @@ -73,11 +72,6 @@ class Project(MPTTModel, Model):
verbose_name=_('Progress count'),
help_text=_('The number of values for the progress bar.')
)
visibility = models.CharField(
max_length=8, choices=VISIBILITY_CHOICES, default=VISIBILITY_PRIVATE,
verbose_name=_('visibility'),
help_text=_('The visibility for this project.')
)

class Meta:
ordering = ('tree_id', 'level', 'title')
Expand Down Expand Up @@ -134,21 +128,6 @@ def file_size(self):
queryset = self.values.filter(snapshot=None).exclude(models.Q(file='') | models.Q(file=None))
return sum([value.file.size for value in queryset])

@property
def is_private(self):
return self.visibility == VISIBILITY_PRIVATE

@property
def is_internal(self):
return self.visibility == VISIBILITY_INTERNAL

@property
def get_visibility_help(self):
if self.is_private:
return _('Project access must be granted explicitly to each user.')
elif self.is_internal:
return _('The project can be accessed by any logged in user.')

def get_members(self, role):
try:
# membership_list is created by the Prefetch call in the viewset
Expand Down
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 d7e251e

Please sign in to comment.