From 8d95f00c06a739564b47a44126bc2864b0924618 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Sat, 15 Aug 2020 17:00:39 +0200
Subject: [PATCH] general: add support for a readonly marker in objects
 (#45924)

---
 wcs/admin/blocks.py                           |  9 ++-
 wcs/admin/data_sources.py                     |  6 +-
 wcs/admin/fields.py                           | 14 +++-
 wcs/admin/forms.py                            | 28 +++++--
 wcs/admin/workflows.py                        | 75 ++++++++++---------
 wcs/admin/wscalls.py                          | 12 ++-
 wcs/backoffice/cards.py                       |  3 +
 wcs/blocks.py                                 |  1 +
 wcs/data_sources.py                           |  1 +
 wcs/formdef.py                                |  1 +
 wcs/qommon/storage.py                         |  5 ++
 wcs/templates/wcs/backoffice/data-source.html |  2 +
 wcs/wf/form.py                                |  2 +
 wcs/workflows.py                              |  7 ++
 wcs/wscalls.py                                |  1 +
 15 files changed, 112 insertions(+), 55 deletions(-)

diff --git a/wcs/admin/blocks.py b/wcs/admin/blocks.py
index 518d706cc..e5014639a 100644
--- a/wcs/admin/blocks.py
+++ b/wcs/admin/blocks.py
@@ -23,7 +23,7 @@ from quixote.html import TemplateIO, htmltext
 from wcs.blocks import BlockDef, BlockdefImportError
 
 from wcs.qommon.form import Form, StringWidget, HtmlWidget, FileWidget
-from wcs.qommon import _, misc, template
+from wcs.qommon import _, N_, misc, template
 from wcs.qommon.backoffice.menu import html_top
 
 from wcs.admin.fields import FieldDefPage, FieldsDirectory
@@ -43,6 +43,7 @@ class BlockDirectory(FieldsDirectory):
     field_def_page_class = BlockFieldDefPage
     blacklisted_types = ['page', 'table', 'table-select', 'tablerows', 'ranked-items', 'blocks']
     support_import = False
+    readonly_message = N_('This block of fields is readonly.')
 
     def __init__(self, section, *args, **kwargs):
         self.section = section
@@ -57,7 +58,8 @@ class BlockDirectory(FieldsDirectory):
         r += htmltext('<div id="appbar">')
         r += htmltext('<h2>%s</h2>') % self.objectdef.name
         r += htmltext('<span class="actions">')
-        r += htmltext('<a href="delete" rel="popup">%s</a>') % _('Delete')
+        if not self.objectdef.is_readonly():
+            r += htmltext('<a href="delete" rel="popup">%s</a>') % _('Delete')
         r += htmltext('<a href="export">%s</a>') % _('Export')
         r += htmltext('<a href="settings" rel="popup">%s</a>') % _('Settings')
         r += htmltext('</span>')
@@ -137,7 +139,8 @@ class BlockDirectory(FieldsDirectory):
         form.add(
             StringWidget, 'digest_template', title=_('Digest Template'), value=self.objectdef.digest_template, size=50
         )
-        form.add_submit('submit', _('Submit'))
+        if not self.objectdef.is_readonly():
+            form.add_submit('submit', _('Submit'))
         form.add_submit('cancel', _('Cancel'))
 
         if form.get_widget('cancel').parse():
diff --git a/wcs/admin/data_sources.py b/wcs/admin/data_sources.py
index b53392241..75a4f0c4b 100644
--- a/wcs/admin/data_sources.py
+++ b/wcs/admin/data_sources.py
@@ -120,7 +120,8 @@ class NamedDataSourceUI(object):
                     title=_('Identifier'),
                     required=True, advanced=True,
                     )
-        form.add_submit('submit', _('Submit'))
+        if not self.datasource.is_readonly():
+            form.add_submit('submit', _('Submit'))
         form.add_submit('cancel', _('Cancel'))
         return form
 
@@ -167,6 +168,9 @@ class NamedDataSourcePage(Directory):
 
     def _q_index(self):
         html_top('datasources', title=self.datasource.name)
+        if self.datasource.is_readonly():
+            get_response().filter['sidebar'] = htmltext(
+                    '<div class="infonotice"><p>%s</p></div>') % _('This data source is readonly.')
         return template.QommonTemplateResponse(
                 templates=['wcs/backoffice/data-source.html'],
                 context={'view': self, 'datasource': self.datasource})
diff --git a/wcs/admin/fields.py b/wcs/admin/fields.py
index 34b69680b..583cc4856 100644
--- a/wcs/admin/fields.py
+++ b/wcs/admin/fields.py
@@ -56,7 +56,8 @@ class FieldDefPage(Directory):
         self.field.fill_admin_form(form)
         form.widgets = [x for x in form.widgets
                 if getattr(x, 'name', None) not in self.blacklisted_attributes]
-        form.add_submit('submit', _('Submit'))
+        if not self.objectdef.is_readonly():
+            form.add_submit('submit', _('Submit'))
         form.add_submit('cancel', _('Cancel'))
         return form
 
@@ -201,6 +202,7 @@ class FieldsDirectory(Directory):
     blacklisted_types = []
     page_id = None
     field_var_prefix = '..._'
+    readonly_message = N_('The fields are readonly.')
 
     support_import = True
 
@@ -313,12 +315,16 @@ class FieldsDirectory(Directory):
                         r += command_icon('pages/%s/' % field.id, 'view',
                                 label = _('Limit display to this page'))
                     r += command_icon('%s/' % field.id, 'edit')
-                r += command_icon('%s/duplicate' % field.id, 'duplicate')
-                r += command_icon('%s/delete' % field.id, 'remove', popup = True)
+                if not self.objectdef.is_readonly():
+                    r += command_icon('%s/duplicate' % field.id, 'duplicate')
+                    r += command_icon('%s/delete' % field.id, 'remove', popup=True)
                 r += htmltext('</p></li>')
             r += htmltext('</ul>')
 
-        get_response().filter['sidebar'] = str(self.get_new_field_form(self.page_id))
+        if self.objectdef.is_readonly():
+            get_response().filter['sidebar'] = '<div class="infonotice"><p>%s</p></div>' % _(self.readonly_message)
+        else:
+            get_response().filter['sidebar'] = str(self.get_new_field_form(self.page_id))
         r += self.index_bottom()
         return r.getvalue()
 
diff --git a/wcs/admin/forms.py b/wcs/admin/forms.py
index 5278734d3..d56d175c8 100644
--- a/wcs/admin/forms.py
+++ b/wcs/admin/forms.py
@@ -90,7 +90,8 @@ class FormDefUI(object):
             form.add(SingleSelectWidget, 'workflow_id', title=_('Workflow'),
                     value=formdef.workflow_id,
                     options=workflows)
-        form.add_submit('submit', _('Submit'))
+        if not formdef.is_readonly():
+            form.add_submit('submit', _('Submit'))
         form.add_submit('cancel', _('Cancel'))
         return form
 
@@ -136,6 +137,7 @@ class FieldDefPage(FieldDefPage):
 class FieldsDirectory(FieldsDirectory):
     field_def_page_class = FieldDefPage
     field_var_prefix = 'form_var_'
+    readonly_message = N_('This form is readonly.')
 
     def index_bottom(self):
         if hasattr(self.objectdef, str('disabled')) and self.objectdef.disabled:
@@ -277,7 +279,8 @@ class OptionsDirectory(Directory):
         return result
 
     def handle(self, form, title):
-        form.add_submit('submit', _('Submit'))
+        if not self.formdef.is_readonly():
+            form.add_submit('submit', _('Submit'))
         form.add_submit('cancel', _('Cancel'))
         if form.get_widget('cancel').parse():
             return redirect('..')
@@ -330,7 +333,8 @@ class WorkflowRoleDirectory(Directory):
         options.extend(get_user_roles())
         form = Form(enctype='multipart/form-data')
         form.add(SingleSelectWidget, 'role_id', value=role_id, options=options)
-        form.add_submit('submit', _('Submit'))
+        if not self.formdef.is_readonly():
+            form.add_submit('submit', _('Submit'))
         form.add_submit('cancel', _('Cancel'))
         if form.get_widget('cancel').parse():
             return redirect('.')
@@ -404,7 +408,8 @@ class FormDefPage(Directory):
         r += htmltext('<div id="appbar">')
         r += htmltext('<h2>%s</h2>') % self.formdef.name
         r += htmltext('<span class="actions">')
-        r += htmltext('<a rel="popup" href="title">%s</a>') % _('change title')
+        if not self.formdef.is_readonly():
+            r += htmltext('<a rel="popup" href="title">%s</a>') % _('change title')
         r += htmltext('</span>')
         r += htmltext('</div>')
 
@@ -604,6 +609,9 @@ class FormDefPage(Directory):
 
     def get_sidebar(self):
         r = TemplateIO(html=True)
+        if self.formdef.is_readonly():
+            r += htmltext('<div class="infonotice"><p>%s</p></div>') % _('This form is readonly.')
+            return r.getvalue()
         r += htmltext('<ul id="sidebar-actions">')
         r += htmltext('<li><a href="delete" rel="popup">%s</a></li>') % _('Delete')
         r += htmltext('<li><a href="duplicate">%s</a></li>') % _('Duplicate')
@@ -658,7 +666,8 @@ class FormDefPage(Directory):
         form.add(SingleSelectWidget, 'category_id',
                         value=self.formdef.category_id,
                         options=[(None, '---', '')] + categories)
-        form.add_submit('submit', _('Submit'))
+        if not self.formdef.is_readonly():
+            form.add_submit('submit', _('Submit'))
         form.add_submit('cancel', _('Cancel'))
         if form.get_widget('cancel').parse():
             return redirect('.')
@@ -697,7 +706,8 @@ class FormDefPage(Directory):
                     title=_('Required authentication contexts'),
                     value=self.formdef.required_authentication_contexts,
                     options=list(auth_contexts.items()))
-        form.add_submit('submit', _('Submit'))
+        if not self.formdef.is_readonly():
+            form.add_submit('submit', _('Submit'))
         form.add_submit('cancel', _('Cancel'))
         if form.get_widget('cancel').parse():
             return redirect('.')
@@ -750,7 +760,8 @@ class FormDefPage(Directory):
         form.add(ValidatedStringWidget, 'url_name', title=_('Identifier in URLs'),
                 size=40, required=True, value=self.formdef.url_name,
                 regex=r'^[a-zA-Z0-9_-]+', **kwargs)
-        form.add_submit('submit', _('Submit'))
+        if not self.formdef.is_readonly():
+            form.add_submit('submit', _('Submit'))
         form.add_submit('cancel', _('Cancel'))
         if form.get_widget('cancel').parse():
             return redirect('.')
@@ -789,7 +800,8 @@ class FormDefPage(Directory):
         form.add(SingleSelectWidget, 'workflow_id',
                         value=self.formdef.workflow_id,
                         options=workflows)
-        form.add_submit('submit', _('Submit'))
+        if not self.formdef.is_readonly():
+            form.add_submit('submit', _('Submit'))
         form.add_submit('cancel', _('Cancel'))
         if form.get_widget('cancel').parse():
             return redirect('.')
diff --git a/wcs/admin/workflows.py b/wcs/admin/workflows.py
index d7ce30143..19b2cd5cb 100644
--- a/wcs/admin/workflows.py
+++ b/wcs/admin/workflows.py
@@ -454,18 +454,16 @@ class WorkflowStatusPage(Directory):
                 r += htmltext('<ul id="items-list" class="biglist sortable">')
             for i, item in enumerate(self.status.items):
                 r += htmltext('<li class="biglistitem" id="itemId_%s">') % item.id
-                if str(self.workflow.id).startswith(str('_')):
-                    r += item.render_as_line()
+                if hasattr(item, str('fill_admin_form')):
+                    r += htmltext('<a href="items/%s/">%s</a>') % (item.id, item.render_as_line())
                 else:
-                    if hasattr(item, str('fill_admin_form')):
-                        r += htmltext('<a href="items/%s/">%s</a>') % (item.id, item.render_as_line())
-                    else:
-                        r += item.render_as_line()
-                    r += htmltext('<p class="commands">')
-                    if hasattr(item, str('fill_admin_form')):
+                    r += item.render_as_line()
+                r += htmltext('<p class="commands">')
+                if not self.workflow.is_readonly():
+                    if hasattr(item, 'fill_admin_form'):
                         r += command_icon('items/%s/' % item.id, 'edit')
                     r += command_icon('items/%s/delete' % item.id, 'remove', popup = True)
-                    r += htmltext('</p>')
+                r += htmltext('</p>')
                 r += htmltext('</li>')
             r += htmltext('</ul>')
             r += htmltext('</div>') # bo-block
@@ -509,11 +507,13 @@ class WorkflowStatusPage(Directory):
     def get_sidebar(self):
         get_response().add_javascript(['popup.js', 'jquery.colourpicker.js'])
         r = TemplateIO(html=True)
-        if str(self.workflow.id).startswith(str('_')):
+        if self.workflow.is_default():
             r += htmltext('<p>')
             r += _('''This is the default workflow, you cannot edit it but you can
                  duplicate it to base your own workflow on it.''')
             r += htmltext('</p>')
+        elif self.workflow.is_readonly():
+            r += htmltext('<div class="infonotice"><p>%s</p></div>') % _('This workflow is readonly.')
         else:
             r += htmltext('<ul id="sidebar-actions">')
             r += htmltext('<li><a href="edit" rel="popup">%s</a></li>') % _('Change Status Name')
@@ -885,6 +885,7 @@ class WorkflowVariablesFieldsDirectory(FieldsDirectory):
     support_import = False
     blacklisted_types = ['page']
     field_var_prefix = 'form_option_'
+    readonly_message = N_('This workflow is readonly.')
 
     def index_top(self):
         r = TemplateIO(html=True)
@@ -908,6 +909,7 @@ class WorkflowBackofficeFieldsDirectory(FieldsDirectory):
     blacklisted_types = ['page']
     blacklisted_attributes = ['condition']
     field_var_prefix = 'form_var_'
+    readonly_message = N_('This workflow is readonly.')
 
     def index_top(self):
         r = TemplateIO(html=True)
@@ -1165,7 +1167,7 @@ class GlobalActionPage(WorkflowStatusPage):
         if not self.action.items:
             r += htmltext('<p>%s</p>') % _('There are not yet any items in this action.')
         else:
-            if str(self.workflow.id).startswith('_'):
+            if self.workflow.is_readonly():
                 r += htmltext('<ul id="items-list" class="biglist">')
             else:
                 r += htmltext('<p class="items">')
@@ -1174,7 +1176,7 @@ class GlobalActionPage(WorkflowStatusPage):
                 r += htmltext('<ul id="items-list" class="biglist sortable">')
             for i, item in enumerate(self.action.items):
                 r += htmltext('<li class="biglistitem" id="itemId_%s">') % item.id
-                if str(self.workflow.id).startswith('_'):
+                if self.workflow.is_readonly():
                     r += item.render_as_line()
                 else:
                     if hasattr(item, 'fill_admin_form'):
@@ -1259,7 +1261,7 @@ class GlobalActionPage(WorkflowStatusPage):
     def get_sidebar(self):
         get_response().add_javascript(['popup.js', 'jquery.colourpicker.js'])
         r = TemplateIO(html=True)
-        if str(self.workflow.id).startswith('_'):
+        if self.workflow.is_default():
             r += htmltext('<p>')
             r += _('''This is the default workflow, you cannot edit it but you can
                  duplicate it to base your own workflow on it.''')
@@ -1394,7 +1396,7 @@ class WorkflowPage(Directory):
             'svg-pan-zoom.js', 'jquery.colourpicker.js'])
         r += htmltext('<div id="appbar">')
         r += htmltext('<h2>%s</h2>') % self.workflow.name
-        if not str(self.workflow.id).startswith('_'):
+        if not self.workflow.is_readonly():
             r += htmltext('<span class="actions">')
             r += htmltext('<a rel="popup" href="edit">%s</a>') % _('change title')
             r += htmltext('</span>')
@@ -1411,7 +1413,7 @@ class WorkflowPage(Directory):
         if not self.workflow.possible_status:
             r += htmltext('<p>%s</p>') % _('There are not yet any status defined in this workflow.')
         else:
-            if not str(self.workflow.id).startswith(str('_')):
+            if not self.workflow.is_readonly():
                 r += htmltext('<p class="hint">')
                 r += _('Use drag and drop with the handles to reorder status.')
                 r += htmltext('</p>')
@@ -1438,7 +1440,7 @@ class WorkflowPage(Directory):
         r += htmltext('<div class="splitcontent-right">')
         r += htmltext('<div class="bo-block">')
         r += htmltext('<h3>%s') % _('Workflow Functions')
-        if not str(self.workflow.id).startswith('_'):
+        if not self.workflow.is_readonly():
             r += htmltext(' <span class="change">(<a rel="popup" href="functions/new">%s</a>)</span>') % _('add function')
         r += htmltext('</h3>')
         r += htmltext('<ul id="roles-list" class="biglist">')
@@ -1446,7 +1448,7 @@ class WorkflowPage(Directory):
         workflow_roles.sort(key=lambda x: '' if x[0] == '_receiver' else misc.simplify(x[1]))
         for key, label in workflow_roles:
             r += htmltext('<li class="biglistitem">')
-            if not str(self.workflow.id).startswith('_'):
+            if not self.workflow.is_readonly():
                 r += htmltext('<a rel="popup" href="functions/%s">%s</a>') % (key[1:], label)
             else:
                 r += htmltext('<a>%s</a>') % label
@@ -1454,10 +1456,12 @@ class WorkflowPage(Directory):
         r += htmltext('</ul>')
         r += htmltext('</div>')
 
-        if not str(self.workflow.id).startswith('_'):
+        if not self.workflow.is_default():
             r += htmltext('<div class="bo-block">')
             r += htmltext('<h3>%s') % _('Workflow Variables')
-            r += htmltext(' <span class="change">(<a href="variables/">%s</a>)</span></h3>') % _('change')
+            if not self.workflow.is_readonly():
+                r += htmltext(' <span class="change">(<a href="variables/">%s</a>)</span>') % _('change')
+            r += htmltext('</h3>')
             if self.workflow.variables_formdef:
                 r += htmltext('<ul class="biglist">')
                 for field in self.workflow.variables_formdef.fields:
@@ -1470,14 +1474,14 @@ class WorkflowPage(Directory):
                 r += htmltext('</ul>')
             r += htmltext('</div>')
 
-        if not str(self.workflow.id).startswith('_'):
+        if not self.workflow.is_default():
             r += htmltext('<div class="bo-block">')
             r += htmltext('<h3>%s') % _('Global Actions')
-            if not str(self.workflow.id).startswith('_'):
+            if not self.workflow.is_readonly():
                 r += htmltext(' <span class="change">(<a rel="popup" href="global-actions/new">%s</a>)</span>') % _('add global action')
             r += htmltext('</h3>')
 
-            if not str(self.workflow.id).startswith('_'):
+            if not self.workflow.is_readonly():
                 r += htmltext('<ul id="status-list" class="biglist sortable" '
                               'data-order-function="update_actions_order">')
             else:
@@ -1485,19 +1489,16 @@ class WorkflowPage(Directory):
 
             for action in (self.workflow.global_actions or []):
                 r += htmltext('<li class="biglistitem" id="itemId_%s">' % action.id)
-                if not str(self.workflow.id).startswith('_'):
-                    r += htmltext('<a href="global-actions/%s/">%s</a>') % (
+                r += htmltext('<a href="global-actions/%s/">%s</a>') % (
                             action.id, action.name)
-                else:
-                    r += htmltext('<a>%s</a>') % action.name
                 r += htmltext('</li>')
             r += htmltext('</ul>')
             r += htmltext('</div>')
 
-        if not str(self.workflow.id).startswith('_'):
+        if not self.workflow.is_default():
             r += htmltext('<div class="bo-block">')
             r += htmltext('<h3>%s') % _('Criticality Levels')
-            if not str(self.workflow.id).startswith('_'):
+            if not self.workflow.is_readonly():
                 r += htmltext(' <span class="change">'
                               '(<a rel="popup" href="criticality-levels/new">'
                               '%s</a>)</span>') % _('add criticality level')
@@ -1509,19 +1510,17 @@ class WorkflowPage(Directory):
                 if level.colour:
                     style = 'style="border-left-color: #%s"' % level.colour
                 r += htmltext('<li class="biglistitem" id="itemId_%s" %s>' % (level.id, style))
-                if not str(self.workflow.id).startswith('_'):
-                    r += htmltext('<a rel="popup" href="criticality-levels/%s">%s</a>') % (
+                r += htmltext('<a rel="popup" href="criticality-levels/%s">%s</a>') % (
                             level.id, level.name)
-                else:
-                    r += htmltext('<a>%s</a>') % level.name
                 r += htmltext('</li>')
             r += htmltext('</ul>')
             r += htmltext('</div>')
 
-        if not str(self.workflow.id).startswith('_'):
+        if not self.workflow.is_default():
             r += htmltext('<div class="bo-block">')
             r += htmltext('<h3>%s') % _('Backoffice Fields')
-            r += htmltext(' <span class="change">(<a href="backoffice-fields/">%s</a>)</span></h3>') % _('change')
+            if not self.workflow.is_readonly():
+                r += htmltext(' <span class="change">(<a href="backoffice-fields/">%s</a>)</span></h3>') % _('change')
             if self.workflow.backoffice_fields_formdef:
                 r += htmltext('<ul class="biglist">')
                 for field in self.workflow.backoffice_fields_formdef.fields:
@@ -1558,14 +1557,18 @@ class WorkflowPage(Directory):
 
     def get_sidebar(self):
         r = TemplateIO(html=True)
-        if str(self.workflow.id).startswith(str('_')):
+
+        if self.workflow.is_default():
             r += htmltext('<p>')
             r += _('''This is the default workflow, you cannot edit it but you can
                  duplicate it to base your own workflow on it.''')
             r += htmltext('</p>')
+        elif self.workflow.is_readonly():
+            r += htmltext('<div class="infonotice"><p>%s</p></div>') % _('This workflow is readonly.')
+            return r.getvalue()
 
         r += htmltext('<ul id="sidebar-actions">')
-        if not str(self.workflow.id).startswith(str('_')):
+        if not self.workflow.is_default():
             r += htmltext('<li><a href="delete" rel="popup">%s</a></li>') % _('Delete')
         r += htmltext('<li><a href="duplicate">%s</a></li>') % _('Duplicate')
         r += htmltext('<li><a href="export">%s</a></li>') % _('Export')
diff --git a/wcs/admin/wscalls.py b/wcs/admin/wscalls.py
index 941817f63..f3d7eb0c4 100644
--- a/wcs/admin/wscalls.py
+++ b/wcs/admin/wscalls.py
@@ -50,7 +50,8 @@ class NamedWsCallUI(object):
                 value=self.wscall.request,
                 title=_('Request'), required=True)
 
-        form.add_submit('submit', _('Submit'))
+        if not self.wscall.is_readonly():
+            form.add_submit('submit', _('Submit'))
         form.add_submit('cancel', _('Cancel'))
         return form
 
@@ -94,13 +95,18 @@ class NamedWsCallPage(Directory):
         html_top('wscalls', title=self.wscall.name)
         r = TemplateIO(html=True)
 
+        if self.wscall.is_readonly():
+            get_response().filter['sidebar'] = htmltext(
+                    '<div class="infonotice"><p>%s</p></div>') % _('This webservice call is readonly.')
+
         r += htmltext('<div id="appbar">')
         r += htmltext('<h2>%s - ') % _('Webservice Call')
         r += self.wscall.name
         r += htmltext('</h2>')
         r += htmltext('<span class="actions">')
-        r += htmltext('<a href="delete" rel="popup">%s</a>') % _('Delete')
-        r += htmltext('<a href="edit">%s</a>') % _('Edit')
+        if not self.wscall.is_readonly():
+            r += htmltext('<a href="delete" rel="popup">%s</a>') % _('Delete')
+            r += htmltext('<a href="edit">%s</a>') % _('Edit')
         r += htmltext('</span>')
         r += htmltext('</div>')
 
diff --git a/wcs/backoffice/cards.py b/wcs/backoffice/cards.py
index 0e57eebfc..7daf58b52 100644
--- a/wcs/backoffice/cards.py
+++ b/wcs/backoffice/cards.py
@@ -173,6 +173,9 @@ class CardDefPage(FormDefPage):
 
     def get_sidebar(self):
         r = TemplateIO(html=True)
+        if self.formdef.is_readonly():
+            r += htmltext('This card model is readonly.')
+            return r.getvalue()
         r += htmltext('<ul id="sidebar-actions">')
         r += htmltext('<li><a href="delete" rel="popup">%s</a></li>') % _('Delete')
         r += htmltext('<li><a href="duplicate">%s</a></li>') % _('Duplicate')
diff --git a/wcs/blocks.py b/wcs/blocks.py
index bf8306d5f..354e3a93e 100644
--- a/wcs/blocks.py
+++ b/wcs/blocks.py
@@ -57,6 +57,7 @@ class BlockDef(StorableObject):
         self.fields = []
 
     def store(self):
+        assert not self.is_readonly()
         if self.slug is None:
             # set slug if it's not yet there
             self.slug = self.get_new_slug()
diff --git a/wcs/data_sources.py b/wcs/data_sources.py
index 20f640d25..239c826e7 100644
--- a/wcs/data_sources.py
+++ b/wcs/data_sources.py
@@ -354,6 +354,7 @@ class NamedDataSource(XmlStorableObject):
             self.store()
 
     def store(self):
+        assert not self.is_readonly()
         if self.slug is None:
             # set slug if it's not yet there
             self.slug = self.get_new_slug()
diff --git a/wcs/formdef.py b/wcs/formdef.py
index f8a85dd27..d5054fdde 100644
--- a/wcs/formdef.py
+++ b/wcs/formdef.py
@@ -366,6 +366,7 @@ class FormDef(StorableObject):
         sql.formdef_wipe()
 
     def store(self):
+        assert not self.is_readonly()
         if self.url_name is None:
             # set url name if it's not yet there
             self.url_name = self.get_new_url_name()
diff --git a/wcs/qommon/storage.py b/wcs/qommon/storage.py
index 2a28ead0d..6578d3c73 100644
--- a/wcs/qommon/storage.py
+++ b/wcs/qommon/storage.py
@@ -292,6 +292,9 @@ class StorableObject(object):
     def __init__(self, id = None):
         self.id = id
 
+    def is_readonly(self):
+        return getattr(self, 'readonly', False)
+
     @classmethod
     def get_table_name(cls):
         return cls._names
@@ -594,6 +597,7 @@ class StorableObject(object):
         return pickle.dumps(object, protocol=2)
 
     def store(self, async_op=False):
+        assert not self.is_readonly()
         objects_dir = self.get_objects_dir()
         new_object = False
         if self._filename:
@@ -810,6 +814,7 @@ class StorableObject(object):
         os.unlink(os.path.join(objects_dir, fix_key(id)))
 
     def remove_self(self):
+        assert not self.is_readonly()
         self.remove_object(self.id)
 
     @classmethod
diff --git a/wcs/templates/wcs/backoffice/data-source.html b/wcs/templates/wcs/backoffice/data-source.html
index 5bad7e5f8..a1b8cb6b1 100644
--- a/wcs/templates/wcs/backoffice/data-source.html
+++ b/wcs/templates/wcs/backoffice/data-source.html
@@ -5,8 +5,10 @@
 <h2>{% trans "Data Source" %} - {{ datasource.name }}</h2>
 <span class="actions">
   <a href="export">{% trans "Export" %}</a>
+  {% if not datasource.is_readonly %}
   <a href="delete" rel="popup">{% trans "Delete" %}</a>
   <a href="edit">{% trans "Edit" %}</a>
+  {% endif %}
 </span>
 </div>
 
diff --git a/wcs/wf/form.py b/wcs/wf/form.py
index dee8da5ee..6eb3a7a2a 100644
--- a/wcs/wf/form.py
+++ b/wcs/wf/form.py
@@ -155,6 +155,8 @@ class FormWorkflowStatusItem(WorkflowStatusItem):
         if component == 'fields':
             if not self.formdef:
                 self.formdef = WorkflowFormFieldsFormDef(item=self)
+            if workflow.is_readonly():
+                self.formdef.readonly = True
             fields_directory = WorkflowFormFieldsDirectory(self.formdef)
             if self.varname:
                 fields_directory.field_var_prefix = '%s_var_' % self.varname
diff --git a/wcs/workflows.py b/wcs/workflows.py
index da6e1856a..5159eb3a0 100644
--- a/wcs/workflows.py
+++ b/wcs/workflows.py
@@ -348,6 +348,7 @@ class Workflow(StorableObject):
             self.store()
 
     def store(self):
+        assert not self.is_readonly()
         must_update = False
         if self.id:
             old_self = self.get(self.id, ignore_errors=True, ignore_migration=True)
@@ -849,6 +850,12 @@ class Workflow(StorableObject):
 
         return workflow
 
+    def is_default(self):
+        return str(self.id).startswith(str('_'))
+
+    def is_readonly(self):
+        return self.is_default() or super().is_readonly()
+
     def formdefs(self, **kwargs):
         return list(FormDef.select(lambda x: x.workflow_id == self.id, **kwargs))
 
diff --git a/wcs/wscalls.py b/wcs/wscalls.py
index 4760f3f1b..992f6b8e3 100644
--- a/wcs/wscalls.py
+++ b/wcs/wscalls.py
@@ -176,6 +176,7 @@ class NamedWsCall(XmlStorableObject):
         return request
 
     def store(self):
+        assert not self.is_readonly()
         if self.slug is None:
             # set slug if it's not yet there
             self.slug = self.get_new_slug()
-- 
2.28.0

