Projet

Général

Profil

0001-backoffice-svg-of-carddefs-relations-57953.patch

Lauréline Guérin, 25 octobre 2021 15:02

Télécharger (14,1 ko)

Voir les différences:

Subject: [PATCH] backoffice: svg of carddefs relations (#57953)

 tests/admin_pages/test_card.py | 153 +++++++++++++++++++++++++++++++++
 wcs/admin/categories.py        |   9 +-
 wcs/backoffice/cards.py        |  12 ++-
 wcs/carddef.py                 |  76 ++++++++++++++++
 4 files changed, 246 insertions(+), 4 deletions(-)
tests/admin_pages/test_card.py
6 6

  
7 7
from wcs import fields
8 8
from wcs.admin.settings import UserFieldsFormDef
9
from wcs.blocks import BlockDef
9 10
from wcs.carddef import CardDef
10 11
from wcs.categories import CardDefCategory
11 12
from wcs.formdef import FormDef
......
636 637
    resp = app.get(carddata.get_backoffice_url())
637 638
    assert 'inspect' in resp.text
638 639
    resp = app.get(carddata.get_backoffice_url() + 'inspect')
640

  
641

  
642
def test_cards_svg(pub):
643
    create_superuser(pub)
644

  
645
    CardDefCategory.wipe()
646
    cat1 = CardDefCategory(name='Foo')
647
    cat1.store()
648
    cat2 = CardDefCategory(name='Foo')
649
    cat2.store()
650

  
651
    ds1 = {'type': 'carddef:card-1-2'}
652
    ds2 = {'type': 'carddef:card-2-2'}
653
    ds3 = {'type': 'carddef:card-3-2'}
654

  
655
    BlockDef.wipe()
656
    block1 = BlockDef()
657
    block1.name = 'block 1'
658
    block1.fields = [
659
        fields.StringField(id='1', label='string', type='string', varname='block foo 1', data_source=ds1),
660
        fields.ItemField(id='2', label='item', type='item', varname='block foo 2', data_source=ds1),
661
        fields.ItemsField(id='3', label='items', type='items', varname='block foo 3', data_source=ds1),
662
        fields.StringField(id='10', label='string', type='string', varname='block fooo 10', data_source=ds2),
663
        fields.ItemField(id='20', label='item', type='item', varname='block fooo 20', data_source=ds2),
664
        fields.ItemsField(id='30', label='items', type='items', varname='block fooo 30', data_source=ds2),
665
    ]
666
    block1.store()
667
    block2 = BlockDef()
668
    block2.name = 'block 2'
669
    block2.fields = [
670
        fields.StringField(id='1', label='string', type='string', varname='block bar 1', data_source=ds2),
671
        fields.ItemField(id='2', label='item', type='item', varname='block bar 2', data_source=ds2),
672
        fields.ItemsField(id='3', label='items', type='items', varname='block bar 3', data_source=ds2),
673
    ]
674
    block2.store()
675
    block3 = BlockDef()
676
    block3.name = 'block 3'
677
    block3.fields = [
678
        fields.StringField(id='1', label='string', type='string', varname='block baz 1', data_source=ds3),
679
        fields.ItemField(id='2', label='item', type='item', varname='block baz 2', data_source=ds3),
680
        fields.ItemsField(id='3', label='items', type='items', varname='block baz 3', data_source=ds3),
681
    ]
682
    block3.store()
683

  
684
    CardDef.wipe()
685

  
686
    carddef11 = CardDef()
687
    carddef11.name = 'card 1-1'
688
    carddef11.category_id = cat1.id
689
    carddef11.fields = [
690
        fields.StringField(id='1', label='string', type='string', varname='foo 1', data_source=ds1),
691
        fields.ItemField(id='2', label='item', type='item', varname='foo 2', data_source=ds1),
692
        fields.ItemsField(id='3', label='items', type='items', varname='foo 3', data_source=ds1),
693
        fields.BlockField(id='4', label='block', type='block:%s' % block1.slug),
694
        fields.StringField(id='10', label='string', type='string', varname='fooo 10', data_source=ds2),
695
        fields.ItemField(id='20', label='item', type='item', varname='fooo 20', data_source=ds2),
696
        fields.ItemsField(id='30', label='items', type='items', varname='fooo 30', data_source=ds2),
697
        fields.BlockField(id='40', label='block', type='block:%s' % block2.slug),
698
    ]
699
    carddef11.store()
700
    carddef12 = CardDef()
701
    carddef12.name = 'card 1-2'
702
    carddef12.category_id = cat1.id
703
    carddef12.fields = []
704
    carddef12.store()
705
    carddef13 = CardDef()
706
    carddef13.name = 'card 1-3'
707
    carddef13.category_id = cat1.id
708
    carddef13.fields = []
709
    carddef13.store()
710

  
711
    carddef21 = CardDef()
712
    carddef21.name = 'card 2-1'
713
    carddef21.category_id = cat2.id
714
    carddef21.fields = [
715
        fields.StringField(id='1', label='string', type='string', varname='bar 1', data_source=ds2),
716
        fields.ItemField(id='2', label='item', type='item', varname='bar 2', data_source=ds2),
717
        fields.ItemsField(id='3', label='items', type='items', varname='bar 3', data_source=ds2),
718
        fields.BlockField(id='4', label='block', type='block:%s' % block2.slug),
719
    ]
720
    carddef21.store()
721
    carddef22 = CardDef()
722
    carddef22.name = 'card 2-2'
723
    carddef22.category_id = cat2.id
724
    carddef22.fields = []
725
    carddef22.store()
726

  
727
    carddef31 = CardDef()
728
    carddef31.name = 'card 3-1'
729
    carddef31.fields = [
730
        fields.StringField(id='1', label='string', type='string', varname='baz 1', data_source=ds3),
731
        fields.ItemField(id='2', label='item', type='item', varname='baz 2', data_source=ds3),
732
        fields.ItemsField(id='3', label='items', type='items', varname='baz 3', data_source=ds3),
733
        fields.BlockField(id='4', label='block', type='block:%s' % block3.slug),
734
    ]
735
    carddef31.store()
736
    carddef32 = CardDef()
737
    carddef32.name = 'card 3-2'
738
    carddef32.fields = []
739
    carddef32.store()
740

  
741
    app = login(get_app(pub))
742

  
743
    resp = app.get('/backoffice/cards/svg')
744
    # cards
745
    assert '<title>card_card_1_1</title' in resp
746
    assert '<title>card_card_1_2</title' in resp
747
    assert '<title>card_card_1_3</title' not in resp
748
    assert '<title>card_card_2_1</title' in resp
749
    assert '<title>card_card_2_2</title' in resp
750
    assert '<title>card_card_3_1</title' in resp
751
    assert '<title>card_card_3_2</title' in resp
752
    # and relations
753
    assert resp.text.count('<title>card_card_1_1&#45;&gt;card_card_1_2</title>') == 6
754
    assert resp.text.count('<title>card_card_1_1&#45;&gt;card_card_2_2</title>') == 9
755
    assert resp.text.count('<title>card_card_2_1&#45;&gt;card_card_2_2</title>') == 6
756
    assert resp.text.count('<title>card_card_3_1&#45;&gt;card_card_3_2</title>') == 6
757

  
758
    resp = app.get('/backoffice/cards/svg?show-orphans=on')
759
    assert '<title>card_card_1_3</title' in resp
760

  
761
    resp = app.get('/backoffice/cards/categories/%s/svg' % cat1.id)
762
    # cards
763
    assert '<title>card_card_1_1</title' in resp
764
    assert '<title>card_card_1_2</title' in resp
765
    assert '<title>card_card_1_3</title' not in resp
766
    assert '<title>card_card_2_1</title' not in resp
767
    assert '<title>card_card_2_2</title' not in resp
768
    assert '<title>card_card_3_1</title' not in resp
769
    assert '<title>card_card_3_2</title' not in resp
770
    # and relations
771
    assert resp.text.count('<title>card_card_1_1&#45;&gt;card_card_1_2</title>') == 6
772
    assert resp.text.count('<title>card_card_1_1&#45;&gt;card_card_2_2</title>') == 0
773
    assert resp.text.count('<title>card_card_2_1&#45;&gt;card_card_2_2</title>') == 0
774
    assert resp.text.count('<title>card_card_3_1&#45;&gt;card_card_3_2</title>') == 0
775

  
776
    resp = app.get('/backoffice/cards/categories/%s/svg?show-orphans=on' % cat1.id)
777
    assert '<title>card_card_1_3</title' in resp
778

  
779
    resp = app.get('/backoffice/cards/categories/%s/svg' % cat2.id)
780
    # cards
781
    assert '<title>card_card_1_1</title' not in resp
782
    assert '<title>card_card_1_2</title' not in resp
783
    assert '<title>card_card_2_1</title' in resp
784
    assert '<title>card_card_2_2</title' in resp
785
    assert '<title>card_card_3_1</title' not in resp
786
    assert '<title>card_card_3_2</title' not in resp
787
    # and relations
788
    assert resp.text.count('<title>card_card_1_1&#45;&gt;card_card_1_2</title>') == 0
789
    assert resp.text.count('<title>card_card_1_1&#45;&gt;card_card_2_2</title>') == 0
790
    assert resp.text.count('<title>card_card_2_1&#45;&gt;card_card_2_2</title>') == 6
791
    assert resp.text.count('<title>card_card_3_1&#45;&gt;card_card_3_2</title>') == 0
wcs/admin/categories.py
18 18
from quixote.directory import Directory
19 19
from quixote.html import TemplateIO, htmltext
20 20

  
21
from wcs.carddef import CardDef
21
from wcs.carddef import CardDef, get_cards_graph
22 22
from wcs.categories import CardDefCategory, Category, WorkflowCategory
23 23
from wcs.formdef import FormDef
24 24
from wcs.qommon import _, misc, template
......
253 253
    object_class = CardDef
254 254
    usage_title = _('Card models in this category')
255 255
    empty_message = _('No card model associated to this category.')
256
    _q_exports = CategoryPage._q_exports + ['svg']
257

  
258
    def svg(self):
259
        response = get_response()
260
        response.set_content_type('image/svg+xml')
261
        show_orphans = get_request().form.get('show-orphans') == 'on'
262
        return get_cards_graph(category=self.category, show_orphans=show_orphans)
256 263

  
257 264

  
258 265
class WorkflowCategoryPage(CategoryPage):
wcs/backoffice/cards.py
14 14
# You should have received a copy of the GNU General Public License
15 15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16 16

  
17
from quixote import get_publisher, get_response, get_session, redirect
17
from quixote import get_publisher, get_request, get_response, get_session, redirect
18 18
from quixote.html import TemplateIO, htmltext
19 19

  
20 20
from wcs.admin import utils
21 21
from wcs.admin.categories import CardDefCategoriesDirectory
22 22
from wcs.admin.forms import FormDefPage, FormDefUI, FormsDirectory, OptionsDirectory
23
from wcs.carddef import CardDef
23
from wcs.carddef import CardDef, get_cards_graph
24 24
from wcs.categories import CardDefCategory
25 25
from wcs.workflows import Workflow
26 26

  
......
225 225

  
226 226

  
227 227
class CardsDirectory(FormsDirectory):
228
    _q_exports = ['', 'new', ('import', 'p_import'), 'categories']
228
    _q_exports = ['', 'new', ('import', 'p_import'), 'categories', 'svg']
229 229

  
230 230
    category_class = CardDefCategory
231 231
    categories = CardDefCategoriesDirectory()
......
293 293
            self.imported_formdef.store()
294 294
        return response
295 295

  
296
    def svg(self):
297
        response = get_response()
298
        response.set_content_type('image/svg+xml')
299
        show_orphans = get_request().form.get('show-orphans') == 'on'
300
        return get_cards_graph(show_orphans=show_orphans)
301

  
296 302
    def _q_lookup(self, component):
297 303
        return self.formdef_page_class(component)
wcs/carddef.py
14 14
# You should have received a copy of the GNU General Public License
15 15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16 16

  
17
import io
17 18
import sys
18 19
import types
20
from subprocess import PIPE, Popen
19 21

  
22
from django.utils.encoding import force_bytes
20 23
from quixote import get_publisher
21 24

  
22 25
from wcs.carddata import CardData
......
282 285
                continue
283 286
            varnames.extend(Field.get_referenced_varnames(formdef, criteria.value))
284 287
        return varnames
288

  
289

  
290
def get_cards_graph(category=None, show_orphans=False):
291
    out = io.StringIO()
292
    out.write('digraph main {\n')
293
    out.write('node [shape=box,style=filled];\n')
294
    out.write('edge [];\n')
295

  
296
    criterias = []
297
    if category is not None:
298
        criterias = [Equal('category_id', str(category.id))]
299
    carddefs = CardDef.select(clause=criterias)
300
    carddefs_slugs = [c.url_name for c in carddefs]
301

  
302
    def check_relations(carddef_ref, fields, check_blocks=True, prefix=''):
303
        cardinality = {
304
            'string': '1..n',
305
            'item': '1..n',
306
            'items': 'n..n',
307
        }
308
        for field in fields:
309
            data_source = getattr(field, 'data_source', None)
310
            if data_source and data_source['type'].startswith('carddef:'):
311
                slug = field.data_source['type'].split(':')[1]
312
                if not show_orphans and slug not in carddefs_slugs:
313
                    # don't report extra category relations
314
                    continue
315
                label = '%s%s %s' % (prefix, field.varname or field.label, cardinality.get(field.key))
316
                yield '%s -> card_%s [label="%s"];' % (
317
                    carddef_ref,
318
                    slug.replace('-', '_'),
319
                    label,
320
                )
321
            if check_blocks and field.key == 'block':
322
                yield from check_relations(
323
                    carddef_ref,
324
                    field.block.fields,
325
                    check_blocks=False,
326
                    prefix='%s (block) ' % (field.varname or field.label),
327
                )
328

  
329
    records = []
330
    relations = []
331

  
332
    for carddef in carddefs:
333
        carddef_ref = 'card_%s' % carddef.url_name.replace('-', '_')
334
        record = '%s [shape=record,label="<card>%s' % (carddef_ref, carddef.name)
335
        relations += list(check_relations(carddef_ref, carddef.get_all_fields()))
336
        record += '"];'
337
        records.append(record)
338
    if not show_orphans:
339
        for record in records[:]:
340
            if not [x for x in relations if record.split()[0] in x.split()]:
341
                records.remove(record)
342

  
343
    for record in records:
344
        out.write('%s\n' % record)
345

  
346
    for relation in relations:
347
        out.write('%s\n' % relation)
348

  
349
    out.write('}\n')
350

  
351
    out = out.getvalue()
352
    try:
353
        with Popen(['dot', '-Tsvg'], stdin=PIPE, stdout=PIPE) as process:
354
            out = process.communicate(force_bytes(out))[0]
355
            if process.returncode != 0:
356
                return ''
357
    except OSError:
358
        return ''
359

  
360
    return out
285
-