Projet

Général

Profil

0002-misc-add-export-import-API-60698.patch

Frédéric Péters, 11 février 2022 10:41

Télécharger (25,2 ko)

Voir les différences:

Subject: [PATCH 2/2] misc: add export/import API (#60698)

 tests/api/test_export_import.py | 231 +++++++++++++++++++++++++++++
 wcs/api_export_import.py        | 253 ++++++++++++++++++++++++++++++++
 wcs/blocks.py                   |   5 +
 wcs/compat.py                   |   4 +-
 wcs/fields.py                   |  19 +++
 wcs/formdef.py                  |   7 +
 wcs/qommon/afterjobs.py         |   5 +-
 wcs/urls.py                     |  19 ++-
 wcs/wf/form.py                  |   5 +
 wcs/workflows.py                |  19 +++
 10 files changed, 563 insertions(+), 4 deletions(-)
 create mode 100644 tests/api/test_export_import.py
 create mode 100644 wcs/api_export_import.py
tests/api/test_export_import.py
1
import io
2
import json
3
import os
4
import tarfile
5
import xml.etree.ElementTree as ET
6

  
7
import pytest
8

  
9
from wcs.blocks import BlockDef
10
from wcs.categories import Category
11
from wcs.data_sources import NamedDataSource
12
from wcs.fields import BlockField, StringField
13
from wcs.formdef import FormDef
14
from wcs.wf.form import FormWorkflowStatusItem, WorkflowFormFieldsFormDef
15
from wcs.workflows import Workflow, WorkflowBackofficeFieldsFormDef, WorkflowVariablesFieldsFormDef
16

  
17
from ..utilities import clean_temporary_pub, create_temporary_pub, get_app
18
from .utils import sign_uri
19

  
20

  
21
@pytest.fixture
22
def pub():
23
    pub = create_temporary_pub(sql_mode=True)
24
    with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
25
        fd.write(
26
            '''\
27
[api-secrets]
28
coucou = 1234
29
'''
30
        )
31

  
32
    Category.wipe()
33
    FormDef.wipe()
34
    BlockDef.wipe()
35
    Workflow.wipe()
36

  
37
    return pub
38

  
39

  
40
def teardown_module(module):
41
    clean_temporary_pub()
42

  
43

  
44
def test_export_import_index(pub):
45
    get_app(pub).get('/api/export-import/', status=403)
46
    resp = get_app(pub).get(sign_uri('/api/export-import/'))
47
    assert resp.json['data'][0]['id'] == 'forms'
48
    assert resp.json['data'][0]['text'] == 'Forms'
49
    assert resp.json['data'][0]['urls']['list'] == 'http://example.net/api/export-import/forms/'
50

  
51

  
52
def test_export_import_list_forms(pub):
53
    resp = get_app(pub).get(sign_uri('/api/export-import/forms/'))
54
    assert not resp.json['data']
55

  
56
    formdef = FormDef()
57
    formdef.name = 'Test'
58
    formdef.store()
59

  
60
    resp = get_app(pub).get(sign_uri('/api/export-import/forms/'))
61
    assert resp.json['data'][0]['id'] == 'test'
62
    assert resp.json['data'][0]['text'] == 'Test'
63

  
64

  
65
def test_export_import_list_404(pub):
66
    get_app(pub).get(sign_uri('/api/export-import/xxx/'), status=404)
67

  
68

  
69
def test_export_import_form(pub):
70
    formdef = FormDef()
71
    formdef.name = 'Test'
72
    formdef.store()
73

  
74
    resp = get_app(pub).get(sign_uri('/api/export-import/forms/'))
75
    resp = get_app(pub).get(sign_uri(resp.json['data'][0]['urls']['export']))
76
    assert resp.text.startswith('<formdef ')
77

  
78

  
79
def test_export_import_form_404(pub):
80
    get_app(pub).get(sign_uri('/api/export-import/xxx/plop/'), status=404)
81

  
82

  
83
def test_export_import_dependencies(pub):
84
    formdef = FormDef()
85
    formdef.name = 'Test'
86
    formdef.store()
87

  
88
    resp = get_app(pub).get(sign_uri('/api/export-import/forms/'))
89
    resp = get_app(pub).get(sign_uri(resp.json['data'][0]['urls']['dependencies']))
90
    assert not resp.json['data']
91

  
92
    block = BlockDef(name='test')
93
    block.store()
94

  
95
    workflow = Workflow(name='test')
96
    workflow.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(workflow)
97
    workflow.backoffice_fields_formdef.fields = [
98
        BlockField(id='bo1', label='test', type='block:test'),
99
    ]
100
    workflow.variables_formdef = WorkflowVariablesFieldsFormDef(workflow=workflow)
101
    workflow.variables_formdef.fields = [StringField(label='Test', id='1')]
102

  
103
    status = workflow.add_status('New')
104
    form = FormWorkflowStatusItem()
105
    form.parent = status
106
    status.items.append(form)
107

  
108
    data_source = NamedDataSource(name='foobar')
109
    data_source.store()
110

  
111
    display_form = FormWorkflowStatusItem()
112
    display_form.id = '_x'
113
    display_form.formdef = WorkflowFormFieldsFormDef(item=display_form)
114
    display_form.formdef.fields.append(StringField(label='Test', data_source={'type': 'foobar'}))
115
    status.items.append(display_form)
116
    display_form.parent = status
117

  
118
    workflow.store()
119

  
120
    formdef.fields = [
121
        BlockField(id='1', label='test', type='block:test'),
122
    ]
123
    formdef.workflow = workflow
124
    formdef.store()
125

  
126
    resp = get_app(pub).get(sign_uri('/api/export-import/forms/'))
127
    resp = get_app(pub).get(sign_uri(resp.json['data'][0]['urls']['dependencies']))
128
    assert resp.json['data']
129
    assert {(x['id'], x['type']) for x in resp.json['data']} == {('test', 'workflows'), ('test', 'blocks')}
130

  
131
    resp = get_app(pub).get(sign_uri('/api/export-import/workflows/'))
132
    resp = get_app(pub).get(sign_uri(resp.json['data'][0]['urls']['dependencies']))
133
    assert resp.json['data']
134
    assert {(x['id'], x['type']) for x in resp.json['data']} == {
135
        ('foobar', 'data-sources'),
136
        ('test', 'blocks'),
137
    }
138

  
139

  
140
def create_bundle(*args):
141
    tar_io = io.BytesIO()
142
    with tarfile.open(mode='w', fileobj=tar_io) as tar:
143
        manifest_json = {
144
            'application': 'Test',
145
            'slug': 'test',
146
            'description': '',
147
            'elements': [
148
                {'type': 'forms', 'slug': 'test', 'name': 'test'},
149
                {'type': 'blocks', 'slug': 'test', 'name': 'test'},
150
                {'type': 'workflows', 'slug': 'test', 'name': 'test'},
151
                {'type': 'forms-categories', 'slug': 'test', 'name': 'test'},
152
            ],
153
        }
154
        manifest_fd = io.BytesIO(json.dumps(manifest_json, indent=2).encode())
155
        tarinfo = tarfile.TarInfo('manifest.json')
156
        tarinfo.size = len(manifest_fd.getvalue())
157
        tar.addfile(tarinfo, fileobj=manifest_fd)
158

  
159
        for (path, obj) in args:
160
            tarinfo = tarfile.TarInfo(path)
161
            xml_export = ET.tostring(obj.export_to_xml(include_id=True))
162
            tarinfo.size = len(xml_export)
163
            tar.addfile(tarinfo, fileobj=io.BytesIO(xml_export))
164

  
165
    return tar_io.getvalue()
166

  
167

  
168
def test_export_import_bundle_import(pub):
169
    workflow = Workflow(name='test')
170
    workflow.store()
171

  
172
    block = BlockDef(name='test')
173
    block.store()
174

  
175
    formdef = FormDef()
176
    formdef.name = 'Test'
177
    formdef.fields = [
178
        BlockField(id='1', label='test', type='block:test'),
179
    ]
180
    formdef.workflow = workflow
181
    formdef.disabled = False
182
    formdef.store()
183

  
184
    category = Category(name='Test')
185
    category.store()
186

  
187
    bundle = create_bundle(
188
        ('forms/test', formdef),
189
        ('blocks/test', block),
190
        ('workflows/test', workflow),
191
        ('forms-categories/test', category),
192
    )
193
    Category.wipe()
194
    FormDef.wipe()
195
    BlockDef.wipe()
196
    Workflow.wipe()
197

  
198
    resp = get_app(pub).put(sign_uri('/api/export-import/bundle-import/'), bundle)
199
    afterjob_url = resp.json['url']
200
    resp = get_app(pub).put(sign_uri(afterjob_url))
201
    assert resp.json['data']['status'] == 'completed'
202

  
203
    assert Category.count() == 1
204
    assert FormDef.count() == 1
205
    assert BlockDef.count() == 1
206
    assert Workflow.count() == 1
207
    assert FormDef.select()[0].fields[0].type == 'block:test'
208

  
209
    # run new import to check it doesn't duplicate objects
210
    resp = get_app(pub).put(sign_uri('/api/export-import/bundle-import/'), bundle)
211
    afterjob_url = resp.json['url']
212
    resp = get_app(pub).put(sign_uri(afterjob_url))
213
    assert resp.json['data']['status'] == 'completed'
214

  
215
    assert Category.count() == 1
216
    assert FormDef.count() == 1
217
    assert BlockDef.count() == 1
218
    assert Workflow.count() == 1
219

  
220
    # change immutable attribute and check it's not reset
221
    formdef = FormDef.select()[0]
222
    formdef.disabled = True
223
    formdef.store()
224

  
225
    resp = get_app(pub).put(sign_uri('/api/export-import/bundle-import/'), bundle)
226
    afterjob_url = resp.json['url']
227
    resp = get_app(pub).put(sign_uri(afterjob_url))
228
    assert resp.json['data']['status'] == 'completed'
229

  
230
    formdef = FormDef.select()[0]
231
    assert formdef.disabled is True
wcs/api_export_import.py
1
# w.c.s. - web application for online forms
2
# Copyright (C) 2005-2021  Entr'ouvert
3
#
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16

  
17
import io
18
import json
19
import tarfile
20
import xml.etree.ElementTree as ET
21

  
22
from django.http import Http404, HttpResponse, HttpResponseForbidden, JsonResponse
23
from django.urls import reverse
24

  
25
from wcs.api_utils import is_url_signed
26
from wcs.blocks import BlockDef
27
from wcs.carddef import CardDef
28
from wcs.categories import BlockCategory, CardDefCategory, Category, WorkflowCategory
29
from wcs.data_sources import NamedDataSource, StubNamedDataSource
30
from wcs.formdef import FormDef
31
from wcs.mail_templates import MailTemplate
32
from wcs.workflows import Workflow
33
from wcs.wscalls import NamedWsCall
34

  
35
from .qommon import _
36
from .qommon.afterjobs import AfterJob
37
from .qommon.misc import indent_xml, xml_node_text
38

  
39
klasses = {
40
    'blocks': BlockDef,
41
    'blocks-categories': BlockCategory,
42
    'cards': CardDef,
43
    'cards-categories': CardDefCategory,
44
    'data-sources': NamedDataSource,
45
    'forms-categories': Category,
46
    'forms': FormDef,
47
    'mail-templates': MailTemplate,
48
    'workflows-categories': WorkflowCategory,
49
    'workflows': Workflow,
50
    'wscalls': NamedWsCall,
51
}
52

  
53
klass_to_slug = {y: x for x, y in klasses.items()}
54

  
55

  
56
def signature_required(func):
57
    def f(*args, **kwargs):
58
        if not is_url_signed():
59
            return HttpResponseForbidden()
60
        return func(*args, **kwargs)
61

  
62
    return f
63

  
64

  
65
@signature_required
66
def index(request):
67
    response = [
68
        {'id': 'forms', 'text': _('Forms'), 'singular': _('Form')},
69
        {'id': 'cards', 'text': _('Cards'), 'singular': _('Card')},
70
        {'id': 'workflows', 'text': _('Workflows'), 'singular': _('Workflow')},
71
        {'id': 'blocks', 'text': _('Blocks'), 'singular': _('Block of fields'), 'minor': True},
72
        {'id': 'data-sources', 'text': _('Data Sources'), 'singular': _('Data Source'), 'minor': True},
73
        {'id': 'mail-templates', 'text': _('Mail Templates'), 'singular': _('Mail Template'), 'minor': True},
74
        {'id': 'wscalls', 'text': _('Webservice Calls'), 'singular': _('Webservice Call'), 'minor': True},
75
        {
76
            'id': 'blocks-categories',
77
            'text': _('Categories (blocks)'),
78
            'singular': _('Category (block)'),
79
            'minor': True,
80
        },
81
        {
82
            'id': 'cards-categories',
83
            'text': _('Categories (cards)'),
84
            'singular': _('Category (cards)'),
85
            'minor': True,
86
        },
87
        {
88
            'id': 'forms-categories',
89
            'text': _('Categories (forms)'),
90
            'singular': _('Category (forms)'),
91
            'minor': True,
92
        },
93
        {
94
            'id': 'workflow-categories',
95
            'text': _('Categories (workflows)'),
96
            'singular': _('Category (workflows)'),
97
            'minor': True,
98
        },
99
    ]
100
    for obj in response:
101
        obj['urls'] = {
102
            'list': request.build_absolute_uri(
103
                reverse('api-export-import-objects-list', kwargs={'objects': obj['id']})
104
            ),
105
        }
106
    return JsonResponse({'data': response})
107

  
108

  
109
@signature_required
110
def export_object_ref(request, obj):
111
    slug = obj.slug
112
    objects = klass_to_slug[obj.__class__]
113
    return {
114
        'id': slug,
115
        'text': obj.name,
116
        'type': objects,
117
        'urls': {
118
            'export': request.build_absolute_uri(
119
                reverse('api-export-import-object-export', kwargs={'objects': objects, 'slug': slug})
120
            ),
121
            'dependencies': request.build_absolute_uri(
122
                reverse('api-export-import-object-dependencies', kwargs={'objects': objects, 'slug': slug})
123
            ),
124
        },
125
    }
126

  
127

  
128
@signature_required
129
def objects_list(request, objects):
130
    klass = klasses.get(objects)
131
    if not klass:
132
        raise Http404()
133
    return JsonResponse({'data': [export_object_ref(request, x) for x in klass.select()]})
134

  
135

  
136
def get_object(objects, slug):
137
    klass = klasses.get(objects)
138
    if not klass:
139
        raise Http404()
140
    return klass.get_by_slug(slug)
141

  
142

  
143
@signature_required
144
def object_export(request, objects, slug):
145
    obj = get_object(objects, slug)
146
    etree = obj.export_to_xml(include_id=True)
147
    indent_xml(etree)
148
    return HttpResponse(ET.tostring(etree), content_type='text/xml')
149

  
150

  
151
@signature_required
152
def object_dependencies(request, objects, slug):
153
    obj = get_object(objects, slug)
154
    dependencies = []
155
    if hasattr(obj, 'get_dependencies'):
156
        for dependency in obj.get_dependencies():
157
            if dependency is None or isinstance(dependency, StubNamedDataSource):
158
                continue
159
            dependencies.append(export_object_ref(request, dependency))
160
    return JsonResponse({'data': dependencies})
161

  
162

  
163
class BundleImportJob(AfterJob):
164
    def __init__(self, tar_content, **kwargs):
165
        super().__init__(**kwargs)
166
        self.tar_content = tar_content
167

  
168
    def execute(self):
169
        tar_io = io.BytesIO(self.tar_content)
170
        with tarfile.open(fileobj=tar_io) as self.tar:
171
            manifest = json.loads(self.tar.extractfile('manifest.json').read().decode())
172
            self.app_name = manifest.get('application')
173

  
174
            # first pass on formdef/carddef/blockdef/workflows to create them empty
175
            # (name and slug); so they can be found for sure in import pass
176
            for type in ('forms', 'cards', 'blocks', 'workflows'):
177
                self.pre_install([x for x in manifest.get('elements') if x.get('type') == type])
178

  
179
            # real installation pass
180
            for type in (
181
                'blocks-categories',
182
                'cards-categories',
183
                'forms-categories',
184
                'workflows-categories',
185
                'data-sources',
186
                'wscalls',
187
                'mail-templates',
188
                'forms',
189
                'cards',
190
                'blocks',
191
                'workflows',
192
            ):
193
                self.install([x for x in manifest.get('elements') if x.get('type') == type])
194

  
195
    def pre_install(self, elements):
196
        for element in elements:
197
            element_klass = klasses[element['type']]
198
            element_content = self.tar.extractfile('%s/%s' % (element['type'], element['slug'])).read()
199
            tree = ET.fromstring(element_content)
200
            if hasattr(element_klass, 'url_name'):
201
                slug = xml_node_text(tree.find('url_name'))
202
            else:
203
                slug = xml_node_text(tree.find('slug'))
204
            try:
205
                existing_object = element_klass.get_by_slug(slug)
206
            except KeyError:
207
                pass
208
            else:
209
                if existing_object:
210
                    continue
211
            new_object = element_klass()
212
            new_object.slug = slug
213
            new_object.name = '[pre-import] %s' % xml_node_text(tree.find('name'))
214
            new_object.store(comment=_('Application (%s)') % self.app_name)
215

  
216
    def install(self, elements):
217
        for element in elements:
218
            element_klass = klasses[element['type']]
219
            element_content = self.tar.extractfile('%s/%s' % (element['type'], element['slug'])).read()
220
            new_object = element_klass.import_from_xml_tree(
221
                ET.fromstring(element_content), include_id=False, check_datasources=False
222
            )
223
            try:
224
                existing_object = element_klass.get_by_slug(new_object.slug)
225
                if existing_object is None:
226
                    raise KeyError()
227
            except KeyError:
228
                new_object.store(comment=_('Application (%s)') % self.app_name)
229
                continue
230
            # replace
231
            new_object.id = existing_object.id
232
            if element['type'] in ('forms', 'cards'):
233
                # keep some settings
234
                for attr in (
235
                    'workflow_options',
236
                    'workflow_roles',
237
                    'roles',
238
                    'required_authentication_contexts',
239
                    'backoffice_submission_roles',
240
                    'publication_date',
241
                    'expiration_date',
242
                    'disabled',
243
                ):
244
                    setattr(new_object, attr, getattr(existing_object, attr))
245
            new_object.store(comment=_('Application (%s) update') % self.app_name)
246

  
247

  
248
@signature_required
249
def bundle_import(request):
250
    job = BundleImportJob(tar_content=request.body)
251
    job.store()
252
    job.run(spool=True)
253
    return JsonResponse({'err': 0, 'url': job.get_api_status_url()})
wcs/blocks.py
110 110
        )
111 111
        return Template(self.digest_template, autoescape=False).render(context)
112 112

  
113
    def get_dependencies(self):
114
        yield self.category
115
        for field in self.fields or []:
116
            yield from field.get_dependencies()
117

  
113 118
    def export_to_xml(self, include_id=False):
114 119
        root = ET.Element(self.xml_root_node)
115 120
        if include_id and self.id:
wcs/compat.py
134 134
            upload.fp = upload_file.file
135 135
            self.form[k] = upload
136 136

  
137
    def build_absolute_uri(self):
138
        return self.django_request.build_absolute_uri()
137
    def build_absolute_uri(self, *args):
138
        return self.django_request.build_absolute_uri(*args)
139 139

  
140 140

  
141 141
class CompatWcsPublisher(WcsPublisher):
wcs/fields.py
659 659
        elif self.store_structured_value and '%s_structured' % self.id in data:
660 660
            del data['%s_structured' % self.id]
661 661

  
662
    def get_dependencies(self):
663
        if getattr(self, 'data_source', None):
664
            data_source_type = self.data_source.get('type')
665
            if data_source_type and data_source_type.startswith('carddef:'):
666
                from .carddef import CardDef
667

  
668
                carddef_slug = data_source_type.split(':')[1]
669
                try:
670
                    yield CardDef.get_by_urlname(carddef_slug)
671
                except KeyError:
672
                    pass
673
            else:
674
                from .data_sources import NamedDataSource
675

  
676
                yield NamedDataSource.get_by_slug(data_source_type, ignore_errors=True)
677

  
662 678
    def __repr__(self):
663 679
        return '<%s %s %r>' % (self.__class__.__name__, self.id, self.label and self.label[:64])
664 680

  
......
3425 3441
    def get_type_label(self):
3426 3442
        return _('Field Block (%s)') % self.block.name
3427 3443

  
3444
    def get_dependencies(self):
3445
        yield self.block
3446

  
3428 3447
    def fill_admin_form(self, form):
3429 3448
        super().fill_admin_form(form)
3430 3449
        if form.get_widget('prefill'):
wcs/formdef.py
671 671

  
672 672
    workflow = property(get_workflow, set_workflow)
673 673

  
674
    def get_dependencies(self):
675
        yield self.category
676
        if self.workflow_id:
677
            yield self.workflow
678
        for field in self.fields or []:
679
            yield from field.get_dependencies()
680

  
674 681
    @property
675 682
    def keywords_list(self):
676 683
        if not self.keywords:
wcs/qommon/afterjobs.py
19 19
import traceback
20 20
import uuid
21 21

  
22
from quixote import get_publisher, get_response
22
from quixote import get_publisher, get_request, get_response
23 23
from quixote.directory import Directory
24 24

  
25 25
from . import N_, _, errors, force_text
......
135 135
            return obj_dict
136 136
        return self.__dict__
137 137

  
138
    def get_api_status_url(self):
139
        return get_request().build_absolute_uri('/api/jobs/%s/' % self.id)
140

  
138 141
    def get_processing_url(self):
139 142
        return '/backoffice/processing?job=%s' % self.id
wcs/urls.py
16 16

  
17 17
from django.conf.urls import url
18 18

  
19
from . import api, compat, views
19
from . import api, api_export_import, compat, views
20 20
from .statistics import views as statistics_views
21 21

  
22 22
urlpatterns = [
......
24 24
    url(r'^i18n\.js$', views.i18n_js),
25 25
    url(r'^backoffice/', views.backoffice),
26 26
    url(r'^__provision__/$', api.provisionning),
27
    url(r'^api/export-import/$', api_export_import.index, name='api-export-import'),
28
    url(r'^api/export-import/bundle-import/$', api_export_import.bundle_import),
29
    url(
30
        r'^api/export-import/(?P<objects>[\w-]+)/$',
31
        api_export_import.objects_list,
32
        name='api-export-import-objects-list',
33
    ),
34
    url(
35
        r'^api/export-import/(?P<objects>[\w-]+)/(?P<slug>[\w_-]+)/$',
36
        api_export_import.object_export,
37
        name='api-export-import-object-export',
38
    ),
39
    url(
40
        r'^api/export-import/(?P<objects>[\w-]+)/(?P<slug>[\w_-]+)/dependencies/$',
41
        api_export_import.object_dependencies,
42
        name='api-export-import-object-dependencies',
43
    ),
27 44
    url(r'^api/validate-condition$', api.validate_condition, name='api-validate-condition'),
28 45
    url(r'^api/validate-expression$', api.validate_expression, name='api-validate-expression'),
29 46
    url(r'^api/reverse-geocoding$', api.reverse_geocoding, name='api-reverse-geocoding'),
wcs/wf/form.py
167 167
                changed |= field.migrate()
168 168
        return changed
169 169

  
170
    def get_dependencies(self):
171
        if self.formdef and self.formdef.fields:
172
            for field in self.formdef.fields:
173
                yield from field.get_dependencies()
174

  
170 175
    def export_to_xml(self, charset, include_id=False):
171 176
        item = WorkflowStatusItem.export_to_xml(self, charset, include_id=include_id)
172 177
        if not hasattr(self, 'formdef') or not self.formdef or not self.formdef.fields:
wcs/workflows.py
610 610
        base_url = get_publisher().get_backoffice_url()
611 611
        return '%s/workflows/%s/' % (base_url, self.id)
612 612

  
613
    def get_dependencies(self):
614
        yield self.category
615
        if self.variables_formdef and self.variables_formdef.fields:
616
            for field in self.variables_formdef.fields:
617
                yield from field.get_dependencies()
618
        if self.backoffice_fields_formdef and self.backoffice_fields_formdef.fields:
619
            for field in self.backoffice_fields_formdef.fields:
620
                yield from field.get_dependencies()
621
        if self.possible_status:
622
            for status in self.possible_status:
623
                yield from status.get_dependencies()
624

  
613 625
    @classmethod
614 626
    def get(cls, id, ignore_errors=False, ignore_migration=False):
615 627
        if id == '_default':
......
1893 1905
    def get_admin_url(self):
1894 1906
        return self.parent.get_admin_url() + 'status/%s/' % self.id
1895 1907

  
1908
    def get_dependencies(self):
1909
        for action in self.items or []:
1910
            yield from action.get_dependencies()
1911

  
1896 1912
    def evaluate_live_form(self, form, filled, user):
1897 1913
        for item in self.get_active_items(form, filled, user):
1898 1914
            item.evaluate_live_form(form, filled, user)
......
2185 2201
    def get_add_role_label(self):
2186 2202
        return self.parent.parent.get_add_role_label()
2187 2203

  
2204
    def get_dependencies(self):
2205
        return []
2206

  
2188 2207
    @noop_mark
2189 2208
    def perform(self, formdata):
2190 2209
        pass
2191
-