Skip to content

Commit

Permalink
Merge 64f1fec into 4e64e34
Browse files Browse the repository at this point in the history
  • Loading branch information
MyPyDavid committed Jan 10, 2024
2 parents 4e64e34 + 64f1fec commit 29abfdb
Show file tree
Hide file tree
Showing 22 changed files with 285 additions and 28 deletions.
23 changes: 23 additions & 0 deletions rdmo/core/permissions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import logging

from django.core.exceptions import FieldDoesNotExist

from rest_framework.permissions import DjangoModelPermissions, DjangoObjectPermissions

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -74,3 +76,24 @@ def has_permission(self, request, view):
return True
else:
return False


class CanToggleElementCurrentSite(DjangoModelPermissions):

perms_map = {
'GET': ['%(app_label)s.view_%(model_name)s'],
'PUT': ['%(app_label)s.change_%(model_name)s_toggle_own_site'],
}

@log_result
def has_permission(self, request, view):

try:
# check for existence of field sites
queryset = self._queryset(view)
model_cls = queryset.model
model_cls._meta.get_field('sites')
except FieldDoesNotExist:
return False

return super().has_permission(request, view)
29 changes: 28 additions & 1 deletion rdmo/core/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,15 @@
'bar-user': 404, 'bar-reviewer': 403, 'bar-editor': 204,
'user': 404, 'example-reviewer': 403, 'example-editor': 204,
'anonymous': 401, 'reviewer': 403, 'editor': 204,
}
},
'toggle-site': {
# foo-editor is not permitted to apply own site(foo.com) in test run(example.com)
'foo-user': 403, 'foo-reviewer': 403, 'foo-editor': 403,
# bar-editor is not permitted to apply own site(bar.com) in test run(example.com)
'bar-user': 403, 'bar-reviewer': 403, 'bar-editor': 403,
'user': 403, 'example-reviewer': 403, 'example-editor': 200,
'anonymous': 401, 'reviewer': 403, 'editor': 200,
},
}


Expand Down Expand Up @@ -115,6 +123,25 @@
'example-reviewer': 404, 'example-editor': 404,
}
},
'toggle-site': {
'all-element': {
# foo-editor can not apply own site(foo.com) in test run(example.com)
'foo-reviewer': 403, 'foo-editor': 403,
# bar-editor can not apply own site(bar.com) in test run(example.com)
'bar-reviewer': 403, 'bar-editor': 403,
'example-reviewer': 403, 'example-editor': 200,
},
'foo-element': {
'foo-reviewer': 403, 'foo-editor': 403,
'bar-reviewer': 403, 'bar-editor': 403,
'example-reviewer': 403, 'example-editor': 200,
},
'bar-element': {
'foo-reviewer': 403, 'foo-editor': 403,
'bar-reviewer': 403, 'bar-editor': 403,
'example-reviewer': 403, 'example-editor': 200,
}
}
}


Expand Down
4 changes: 3 additions & 1 deletion rdmo/core/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
('reviewer', 'reviewer'),
('user', 'user'),
('api', 'api'),
('example-editor', 'example-editor'),
('example-reviewer', 'example-reviewer'),
)


Expand Down Expand Up @@ -57,7 +59,7 @@ def test_i18n_switcher(db, client):
def test_can_view_management(db, client, username, password):
client.login(username=username, password=password)
response = client.get(reverse('management'))
if username in ('editor', 'reviewer', 'api'):
if username in ('editor', 'reviewer', 'api', 'example-editor', 'example-reviewer'):
assert response.status_code == 200
else:
assert response.status_code == 403
16 changes: 8 additions & 8 deletions rdmo/management/assets/js/actions/elementActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -342,14 +342,15 @@ export function fetchElementError(error) {

// store element

export function storeElement(elementType, element, back) {
export function storeElement(elementType, element, back, elementAction=null) {
return function(dispatch, getState) {
dispatch(storeElementInit(element))

dispatch(storeElementInit(element, elementAction))

let action
switch (elementType) {
case 'catalogs':
action = () => QuestionsApi.storeCatalog(element)
action = () => QuestionsApi.storeCatalog(element, elementAction)
break

case 'sections':
Expand Down Expand Up @@ -385,11 +386,11 @@ export function storeElement(elementType, element, back) {
break

case 'tasks':
action = () => TasksApi.storeTask(element)
action = () => TasksApi.storeTask(element, elementAction)
break

case 'views':
action = () => ViewsApi.storeView(element)
action = () => ViewsApi.storeView(element, elementAction)
break
}

Expand All @@ -406,8 +407,8 @@ export function storeElement(elementType, element, back) {
}
}

export function storeElementInit(element) {
return {type: 'elements/storeElementInit', element}
export function storeElementInit(element, elementAction) {
return {type: 'elements/storeElementInit', element, elementAction}
}

export function storeElementSuccess(element) {
Expand Down Expand Up @@ -574,7 +575,6 @@ export function deleteElement(elementType, element) {
case 'catalogs':
action = () => QuestionsApi.deleteCatalog(element)
break

case 'sections':
action = () => QuestionsApi.deleteSection(element)
break
Expand Down
8 changes: 6 additions & 2 deletions rdmo/management/assets/js/api/QuestionsApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,15 @@ class QuestionsApi extends BaseApi {
return this.get(url)
}

static storeCatalog(catalog) {
static storeCatalog(catalog, action) {
if (isNil(catalog.id)) {
return this.post('/api/v1/questions/catalogs/', catalog)
} else {
return this.put(`/api/v1/questions/catalogs/${catalog.id}/`, catalog)
let url = `/api/v1/questions/catalogs/${catalog.id}/`
if (['add-site', 'remove-site'].includes(action)) {
url = `/api/v1/questions/catalog-toggle-site/${catalog.id}/${action}/`
}
return this.put(url, catalog)
}
}

Expand Down
8 changes: 6 additions & 2 deletions rdmo/management/assets/js/api/TasksApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@ class TasksApi extends BaseApi {
return this.get(`/api/v1/tasks/tasks/${id}/`)
}

static storeTask(task) {
static storeTask(task, action) {
if (isNil(task.id)) {
return this.post('/api/v1/tasks/tasks/', task)
} else {
return this.put(`/api/v1/tasks/tasks/${task.id}/`, task)
let url = `/api/v1/tasks/tasks/${task.id}/`
if (['add-site', 'remove-site'].includes(action)) {
url = `/api/v1/tasks/task-toggle-site/${task.id}/${action}/`
}
return this.put(url, task)
}
}

Expand Down
8 changes: 6 additions & 2 deletions rdmo/management/assets/js/api/ViewsApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@ class ViewsApi extends BaseApi {
return this.get(`/api/v1/views/views/${id}/`)
}

static storeView(view) {
static storeView(view, action) {
if (isNil(view.id)) {
return this.post('/api/v1/views/views/', view)
} else {
return this.put(`/api/v1/views/views/${view.id}/`, view)
let url= `/api/v1/views/views/${view.id}/`
if (['add-site', 'remove-site'].includes(action)) {
url = `/api/v1/views/view-toggle-site/${view.id}/${action}/`
}
return this.put(url, view)
}
}

Expand Down
22 changes: 21 additions & 1 deletion rdmo/management/assets/js/components/common/Links.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,26 @@ LockedLink.propTypes = {
disabled: PropTypes.bool
}

const ToggleCurrentSiteLink = ({ has_current_site, locked, onClick, disabled }) => {
const className = classNames({
'element-btn-link fa': true,
'fa-plus-square-o': !has_current_site,
'fa-minus-square-o': has_current_site,
})
const title = has_current_site ? gettext('Remove your site'): gettext('Add your site')

return <LinkButton className={className} title={locked ? gettext('Locked') : title}
disabled={locked || disabled} onClick={onClick} />
}

ToggleCurrentSiteLink.propTypes = {
has_current_site: PropTypes.bool.isRequired,
locked: PropTypes.bool.isRequired,
onClick: PropTypes.func.isRequired,
disabled: PropTypes.bool
}


const ShowElementsLink = ({ showElements, show, onClick }) => {
const className = classNames({
'element-btn-link fa': true,
Expand Down Expand Up @@ -242,5 +262,5 @@ ShowLink.propTypes = {
onClick: PropTypes.func.isRequired
}

export { EditLink, CopyLink, AddLink, AvailableLink, LockedLink, ShowElementsLink,
export { EditLink, CopyLink, AddLink, AvailableLink, ToggleCurrentSiteLink, LockedLink, ShowElementsLink,
NestedLink, ExportLink, ExtendLink, CodeLink, ErrorLink, WarningLink, ShowLink }
9 changes: 7 additions & 2 deletions rdmo/management/assets/js/components/element/Catalog.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { filterElement } from '../../utils/filter'
import { buildPath } from '../../utils/location'

import { ElementErrors } from '../common/Errors'
import { EditLink, CopyLink, AddLink, AvailableLink, LockedLink, NestedLink,
import { EditLink, CopyLink, AddLink, AvailableLink, ToggleCurrentSiteLink, LockedLink, NestedLink,
ExportLink, CodeLink } from '../common/Links'
import { ReadOnlyIcon } from '../common/Icons'

Expand All @@ -26,12 +26,17 @@ const Catalog = ({ config, catalog, elementActions, display='list',

const toggleAvailable = () => elementActions.storeElement('catalogs', {...catalog, available: !catalog.available })
const toggleLocked = () => elementActions.storeElement('catalogs', {...catalog, locked: !catalog.locked })

const addCurrentSite = () => elementActions.storeElement('catalogs', catalog, null, 'add-site')
const removeCurrentSite = () => elementActions.storeElement('catalogs', catalog, null, 'remove-site')
let has_current_site = config.settings.multisite ? catalog.sites.includes(config.currentSite.id) : true
const createSection = () => elementActions.createElement('sections', { catalog })

const elementNode = (
<div className="element">
<div className="pull-right">
<ToggleCurrentSiteLink has_current_site={has_current_site}
locked={catalog.locked} onClick={has_current_site ? removeCurrentSite : addCurrentSite}
show={config.settings.multisite}/>
<ReadOnlyIcon title={gettext('This catalog is read only')} show={catalog.read_only} />
<NestedLink title={gettext('View catalog nested')} href={nestedUrl} onClick={fetchNested} />
<EditLink title={gettext('Edit catalog')} href={editUrl} onClick={fetchEdit} />
Expand Down
8 changes: 7 additions & 1 deletion rdmo/management/assets/js/components/element/Task.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { filterElement } from '../../utils/filter'
import { buildPath } from '../../utils/location'

import { ElementErrors } from '../common/Errors'
import { EditLink, CopyLink, AvailableLink, LockedLink, ExportLink, CodeLink } from '../common/Links'
import { EditLink, CopyLink, AvailableLink, LockedLink, ExportLink, CodeLink, ToggleCurrentSiteLink } from '../common/Links'
import { ReadOnlyIcon } from '../common/Icons'

const Task = ({ config, task, elementActions, filter=false, filterSites=false, filterEditors=false }) => {
Expand All @@ -21,13 +21,19 @@ const Task = ({ config, task, elementActions, filter=false, filterSites=false, f
const fetchCopy = () => elementActions.fetchElement('tasks', task.id, 'copy')
const toggleAvailable = () => elementActions.storeElement('tasks', {...task, available: !task.available })
const toggleLocked = () => elementActions.storeElement('tasks', {...task, locked: !task.locked })
const addCurrentSite = () => elementActions.storeElement('tasks', task, null, 'add-site')
const removeCurrentSite = () => elementActions.storeElement('tasks', task, null, 'remove-site')
let has_current_site = config.settings.multisite ? task.sites.includes(config.currentSite.id) : true

const fetchCondition = (index) => elementActions.fetchElement('conditions', task.conditions[index])

return showElement && (
<li className="list-group-item">
<div className="element">
<div className="pull-right">
<ToggleCurrentSiteLink has_current_site={has_current_site} locked={task.locked}
onClick={has_current_site ? removeCurrentSite : addCurrentSite}
show={config.settings.multisite}/>
<ReadOnlyIcon title={gettext('This task is read only')} show={task.read_only} />
<EditLink title={gettext('Edit task')} href={editUrl} onClick={fetchEdit} />
<CopyLink title={gettext('Copy task')} href={copyUrl} onClick={fetchCopy} />
Expand Down
8 changes: 7 additions & 1 deletion rdmo/management/assets/js/components/element/View.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { filterElement } from '../../utils/filter'
import { buildPath } from '../../utils/location'

import { ElementErrors } from '../common/Errors'
import { EditLink, CopyLink, AvailableLink, LockedLink, ExportLink, CodeLink } from '../common/Links'
import { EditLink, CopyLink, AvailableLink, LockedLink, ExportLink, CodeLink, ToggleCurrentSiteLink } from '../common/Links'
import { ReadOnlyIcon } from '../common/Icons'

const View = ({ config, view, elementActions, filter=false, filterSites=false, filterEditors=false }) => {
Expand All @@ -20,11 +20,17 @@ const View = ({ config, view, elementActions, filter=false, filterSites=false, f
const fetchCopy = () => elementActions.fetchElement('views', view.id, 'copy')
const toggleAvailable = () => elementActions.storeElement('views', {...view, available: !view.available })
const toggleLocked = () => elementActions.storeElement('views', {...view, locked: !view.locked })
const addCurrentSite = () => elementActions.storeElement('views', view, null, 'add-site')
const removeCurrentSite = () => elementActions.storeElement('views', view, null, 'remove-site')
let has_current_site = config.settings.multisite ? view.sites.includes(config.currentSite.id) : true

return showElement && (
<li className="list-group-item">
<div className="element">
<div className="pull-right">
<ToggleCurrentSiteLink has_current_site={has_current_site} locked={view.locked}
onClick={has_current_site ? removeCurrentSite : addCurrentSite}
show={config.settings.multisite}/>
<ReadOnlyIcon title={gettext('This view is read only')} show={view.read_only} />
<EditLink title={gettext('Edit view')} href={editUrl} onClick={fetchEdit} />
<CopyLink title={gettext('Copy view')} href={copyUrl} onClick={fetchCopy} />
Expand Down
22 changes: 18 additions & 4 deletions rdmo/management/rules.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import logging

from django.contrib.sites.models import Site

import rules
from rules.predicates import is_authenticated, is_superuser

Expand All @@ -13,16 +15,19 @@ def is_editor(user) -> bool:


@rules.predicate
def is_editor_for_current_site(user, site) -> bool:
def is_editor_for_current_site(user) -> bool:
''' Checks if any editor role exists for the user '''
if not is_editor(user):
return False # if the user is not an editor, return False
return user.role.editor.filter(pk=site.pk).exists()
current_site = Site.objects.get_current()
return user.role.editor.filter(id=current_site.id).exists()


@rules.predicate
def is_element_editor(user, obj) -> bool:
''' Checks if the user is an editor for the sites to which this element is editable '''
if obj is None:
return False

if obj.id is None: # for _add_object permissions
# if the element does not exist yet, it can be created by all users with an editor role
Expand All @@ -43,16 +48,19 @@ def is_reviewer(user) -> bool:


@rules.predicate
def is_reviewer_for_current_site(user, site) -> bool:
def is_reviewer_for_current_site(user) -> bool:
''' Checks if any reviewer role exists for the user '''
if not is_reviewer(user):
return False # if the user is not an reviewer, return False
return user.role.reviewer.filter(pk=site.pk).exists()
current_site = Site.objects.get_current()
return user.role.reviewer.filter(id=current_site.id).exists()


@rules.predicate
def is_element_reviewer(user, obj) -> bool:
''' Checks if the user is an reviewer for the sites to which this element is editable '''
if obj is None:
return False

# if the element has no editors, it is reviewable by all reviewers
if not obj.editors.exists():
Expand Down Expand Up @@ -144,6 +152,8 @@ def is_legacy_reviewer(user) -> bool:
rules.add_perm('tasks.add_task_object', is_element_editor)
rules.add_perm('tasks.change_task_object', is_element_editor)
rules.add_perm('tasks.delete_task_object', is_element_editor)
# toggle current site field perm
rules.add_perm('tasks.change_task_toggle_own_site', is_element_editor | is_editor_for_current_site)

# Model Permissions for views
rules.add_perm('views.view_view', is_editor | is_reviewer)
Expand All @@ -154,6 +164,8 @@ def is_legacy_reviewer(user) -> bool:
rules.add_perm('views.add_view_object', is_element_editor)
rules.add_perm('views.change_view_object', is_element_editor)
rules.add_perm('views.delete_view_object', is_element_editor)
# toggle current site field perm
rules.add_perm('views.change_view_toggle_own_site', is_element_editor | is_editor_for_current_site)

# Model permissions for catalogs
rules.add_perm('questions.view_catalog', is_editor | is_reviewer)
Expand All @@ -164,6 +176,8 @@ def is_legacy_reviewer(user) -> bool:
rules.add_perm('questions.add_catalog_object', is_element_editor)
rules.add_perm('questions.change_catalog_object', is_element_editor)
rules.add_perm('questions.delete_catalog_object', is_element_editor)
# toggle current site field perm
rules.add_perm('questions.change_catalog_toggle_own_site', is_element_editor | is_editor_for_current_site)

# Model permissions for sections
rules.add_perm('questions.view_section', is_editor | is_reviewer)
Expand Down
Loading

0 comments on commit 29abfdb

Please sign in to comment.