Projet

Général

Profil

0006-cells-data-cells-invalid-report-38009.patch

Lauréline Guérin, 21 février 2020 11:14

Télécharger (27,9 ko)

Voir les différences:

Subject: [PATCH 6/6] cells: data cells invalid report (#38009)

 combo/data/apps.py                            |   9 +
 combo/data/models.py                          | 142 ++++++++--
 .../combo/manager/link-list-cell-form.html    |   7 +-
 combo/manager/static/css/combo.manager.css    |   4 +-
 combo/manager/views.py                        |   7 +-
 tests/test_cells.py                           | 242 +++++++++++++++++-
 tests/test_manager.py                         |  18 ++
 7 files changed, 406 insertions(+), 23 deletions(-)
combo/data/apps.py
20 20
class DataConfig(AppConfig):
21 21
    name = 'combo.data'
22 22
    verbose_name = 'data'
23

  
24
    def hourly(self):
25
        from combo.data.library import get_cell_classes
26
        from combo.data.models import CellBase
27

  
28
        cell_classes = [c for c in self.get_models() if c in get_cell_classes()]
29
        for cell in CellBase.get_cells(cell_filter=lambda x: x in cell_classes, page__snapshot__isnull=True):
30
            if hasattr(cell, 'check_validity'):
31
                cell.check_validity()
combo/data/models.py
754 754
        if save:
755 755
            self.save(update_fields=['invalid_reason_code', 'invalid_since'])
756 756

  
757
    def set_validity_from_url(
758
            self, resp,
759
            not_found_code='url_not_found', invalid_code='url_invalid',
760
            save=True):
761
        if resp is None:
762
            # can not retrieve data, don't report cell as invalid
763
            self.mark_as_valid(save=save)
764
        elif resp.status_code == 404:
765
            self.mark_as_invalid(not_found_code, save=save)
766
        elif 400 <= resp.status_code < 500:
767
            # 4xx error, cell is invalid
768
            self.mark_as_invalid(invalid_code, save=save)
769
        else:
770
            # 2xx or 3xx: cell is valid
771
            # 5xx error: can not retrieve data, don't report cell as invalid
772
            self.mark_as_valid(save=save)
773

  
757 774
    def get_invalid_reason(self):
758 775
        if not self.invalid_since:
759 776
            return None
......
983 1000
    edit_link_label = _('Edit link')
984 1001
    add_as_link_code = 'link'
985 1002

  
1003
    invalid_reason_codes = {
1004
        'data_url_not_defined': _('No link set'),
1005
        'data_url_not_found': _('URL seems to unexist'),
1006
        'data_url_invalid': _('URL seems to be invalid'),
1007
    }
1008

  
986 1009
    class Meta:
987 1010
        verbose_name = _('Link')
988 1011

  
1012
    def save(self, *args, **kwargs):
1013
        if 'update_fields' in kwargs:
1014
            # don't check validity
1015
            return super(LinkCell, self).save(*args, **kwargs)
1016

  
1017
        # check validity
1018
        self.check_validity(save=False)
1019
        return super(LinkCell, self).save(*args, **kwargs)
1020

  
989 1021
    def get_additional_label(self):
990 1022
        title = self.title
991 1023
        if not title and self.link_page:
......
994 1026
            return None
995 1027
        return utils.ellipsize(title)
996 1028

  
1029
    def get_url(self, context=None):
1030
        context = context or {}
1031
        if self.link_page:
1032
            url = self.link_page.get_online_url()
1033
        else:
1034
            url = utils.get_templated_url(self.url, context=context)
1035
        if self.anchor:
1036
            url += '#' + self.anchor
1037
        return url
1038

  
997 1039
    def get_cell_extra_context(self, context):
998 1040
        render_skeleton = context.get('render_skeleton')
999 1041
        request = context.get('request')
1000 1042
        extra_context = super(LinkCell, self).get_cell_extra_context(context)
1001 1043
        if self.link_page:
1002
            extra_context['url'] = self.link_page.get_online_url()
1003 1044
            extra_context['title'] = self.title or self.link_page.title
1004 1045
        else:
1005
            extra_context['url'] = utils.get_templated_url(self.url, context=context)
1006 1046
            extra_context['title'] = self.title or self.url
1007
        if self.anchor:
1008
            extra_context['url'] += '#' + self.anchor
1009
        if render_skeleton and not urlparse.urlparse(extra_context['url']).netloc:
1047
        url = self.get_url(context)
1048
        if render_skeleton and not urlparse.urlparse(url).netloc:
1010 1049
            # create full URL when used in a skeleton
1011
            extra_context['url'] = request.build_absolute_uri(extra_context['url'])
1050
            url = request.build_absolute_uri(url)
1051
        extra_context['url'] = url
1012 1052
        return extra_context
1013 1053

  
1014 1054
    def get_default_form_class(self):
......
1026 1066
            return [link_data]
1027 1067
        return []
1028 1068

  
1069
    def check_validity(self, save=True):
1070
        if self.link_page:
1071
            self.mark_as_valid(save=save)
1072
            return
1073
        if not self.url:
1074
            self.mark_as_invalid('data_url_not_defined', save=save)
1075
            return
1076

  
1077
        resp = None
1078
        try:
1079
            resp = requests.get(self.get_url())
1080
            resp.raise_for_status()
1081
        except (requests.exceptions.RequestException):
1082
            pass
1083

  
1084
        self.set_validity_from_url(resp, not_found_code='data_url_not_found', invalid_code='data_url_invalid')
1085

  
1029 1086

  
1030 1087
@register_cell_class
1031 1088
class LinkListCell(CellBase):
......
1034 1091
    template_name = 'combo/link-list-cell.html'
1035 1092
    manager_form_template = 'combo/manager/link-list-cell-form.html'
1036 1093

  
1094
    invalid_reason_codes = {
1095
        'data_link_invalid': _('Invalid link'),
1096
    }
1097

  
1037 1098
    class Meta:
1038 1099
        verbose_name = _('List of links')
1039 1100

  
......
1097 1158
        for link in self.get_items():
1098 1159
            link.duplicate(page_target=new_cell.page, placeholder=new_cell.link_placeholder)
1099 1160

  
1161
    def check_validity(self):
1162
        for link in self.get_items():
1163
            if link.invalid_since is not None:
1164
                self.mark_as_invalid('data_link_invalid')
1165
                return
1166
        self.mark_as_valid()
1167

  
1100 1168

  
1101 1169
@register_cell_class
1102 1170
class FeedCell(CellBase):
1103 1171
    title = models.CharField(_('Title'), max_length=150, blank=True)
1104 1172
    url = models.CharField(_('URL'), blank=True, max_length=200)
1105
    limit = models.PositiveSmallIntegerField(_('Maximum number of entries'),
1106
            null=True, blank=True)
1173
    limit = models.PositiveSmallIntegerField(
1174
        _('Maximum number of entries'),
1175
        null=True, blank=True)
1107 1176

  
1108 1177
    manager_form_factory_kwargs = {'field_classes': {'url': TemplatableURLField}}
1109 1178
    template_name = 'combo/feed-cell.html'
1110 1179

  
1180
    invalid_reason_codes = {
1181
        'data_url_not_defined': _('No URL set'),
1182
        'data_url_not_found': _('URL seems to unexist'),
1183
        'data_url_invalid': _('URL seems to be invalid'),
1184
    }
1185

  
1111 1186
    class Meta:
1112 1187
        verbose_name = _('RSS/Atom Feed')
1113 1188

  
......
1116 1191

  
1117 1192
    def get_cell_extra_context(self, context):
1118 1193
        extra_context = super(FeedCell, self).get_cell_extra_context(context)
1194

  
1195
        if not self.url:
1196
            self.mark_as_invalid('data_url_not_defined')
1197
            return extra_context
1198

  
1119 1199
        if context.get('placeholder_search_mode'):
1120 1200
            # don't call webservices when we're just looking for placeholders
1121 1201
            return extra_context
1202

  
1122 1203
        cache_key = hashlib.md5(smart_bytes(self.url)).hexdigest()
1123 1204
        feed_content = cache.get(cache_key)
1124 1205
        if not feed_content:
1206
            feed_response = None
1125 1207
            try:
1126 1208
                feed_response = requests.get(utils.get_templated_url(self.url))
1127 1209
                feed_response.raise_for_status()
......
1131 1213
                if feed_response.status_code == 200:
1132 1214
                    feed_content = feed_response.content
1133 1215
                    cache.set(cache_key, feed_content, 600)
1216
            self.set_validity_from_url(feed_response, not_found_code='data_url_not_found', invalid_code='data_url_invalid')
1134 1217
        if feed_content:
1135 1218
            extra_context['feed'] = feedparser.parse(feed_content)
1136 1219
            if self.limit:
......
1208 1291

  
1209 1292
    _json_content = None
1210 1293

  
1294
    invalid_reason_codes = {
1295
        'data_url_not_found': _('URL seems to unexist'),
1296
        'data_url_invalid': _('URL seems to be invalid'),
1297
    }
1298

  
1211 1299
    class Meta:
1212 1300
        abstract = True
1213 1301

  
......
1259 1347
            if not url:
1260 1348
                continue
1261 1349
            try:
1262
                json_response = utils.requests.get(url,
1263
                        headers={'Accept': 'application/json'},
1264
                        remote_service='auto',
1265
                        cache_duration=data_url_dict.get('cache_duration', self.cache_duration),
1266
                        without_user=True,
1267
                        raise_if_not_cached=not(context.get('synchronous')),
1268
                        invalidate_cache=invalidate_cache,
1269
                        log_errors=log_errors,
1270
                        timeout=data_url_dict.get('timeout', self.timeout),
1271
                        django_request=context.get('request'),
1272
                        )
1350
                json_response = utils.requests.get(
1351
                    url,
1352
                    headers={'Accept': 'application/json'},
1353
                    remote_service='auto',
1354
                    cache_duration=data_url_dict.get('cache_duration', self.cache_duration),
1355
                    without_user=True,
1356
                    raise_if_not_cached=not(context.get('synchronous')),
1357
                    invalidate_cache=invalidate_cache,
1358
                    log_errors=log_errors,
1359
                    timeout=data_url_dict.get('timeout', self.timeout),
1360
                    django_request=context.get('request'),
1361
                )
1273 1362
            except requests.RequestException as e:
1274 1363
                extra_context[data_key + '_status'] = -1
1275 1364
                extra_context[data_key + '_error'] = force_text(e)
......
1303 1392
            # templated URLs
1304 1393
            context[data_key] = extra_context[data_key]
1305 1394

  
1395
        returns = [extra_context.get(d['key'] + '_status') for d in data_urls]
1396
        returns = set([s for s in returns if s is not None])
1397
        if returns and 200 not in returns:  # not a single valid answer
1398
            if 404 in returns:
1399
                self.mark_as_invalid('data_url_not_found')
1400
            elif any([400 <= r < 500 for r in returns]):
1401
                # at least 4xx errors, report the cell as invalid
1402
                self.mark_as_invalid('data_url_invalid')
1403
            else:
1404
                # 2xx or 3xx: cell is valid
1405
                # 5xx error: can not retrieve data, don't report cell as invalid
1406
                self.mark_as_valid()
1407
        else:
1408
            self.mark_as_valid()
1409

  
1306 1410
        # keep cache of first response as it may be used to find the
1307 1411
        # appropriate template.
1308 1412
        self._json_content = extra_context[self.first_data_key]
combo/data/templates/combo/manager/link-list-cell-form.html
11 11
     data-link-list-order-url="{% url 'combo-manager-link-list-order' page_pk=page.pk cell_reference=cell.get_reference %}">
12 12
   {% for link in links %}
13 13
   <li data-link-item-id="{{ link.pk }}"><span class="handle">⣿</span>
14
   <span title="{{ link }}">{{ link|truncatechars:100 }}</span>
14
   <span title="{{ link }}">
15
     {{ link|truncatechars:100 }}
16
     {% if link.invalid_since %}
17
     <span class="invalid">/!\ {{ link.get_invalid_reason }}</span>
18
     {% endif %}
19
   </span>
15 20
       <a rel="popup" title="{% trans "Edit" %}" class="link-action-icon edit" href="{% url 'combo-manager-page-list-cell-edit-link' page_pk=page.id cell_reference=cell.get_reference link_cell_reference=link.get_reference %}">{% trans "Edit" %}</a>
16 21
       <a rel="popup" title="{% trans "Delete" %}" class="link-action-icon delete" href="{% url 'combo-manager-page-list-cell-delete-link' page_pk=page.id cell_reference=cell.get_reference link_cell_reference=link.get_reference %}">{% trans "Delete" %}</a>
17 22
   </li>
combo/manager/static/css/combo.manager.css
81 81

  
82 82
div.cell h3 span.additional-label,
83 83
div.cell h3 span.invalid,
84
ul.list-of-links span.invalid,
84 85
div.cell h3 span.visibility-summary,
85 86
div.page span.visibility-summary {
86 87
	font-size: 80%;
......
112 113
	max-width: 30%;
113 114
}
114 115

  
115
div.cell h3 span.invalid {
116
div.cell h3 span.invalid,
117
ul.list-of-links span.invalid {
116 118
	color: red;
117 119
}
118 120

  
combo/manager/views.py
97 97
        select_related=['page'],
98 98
        page__snapshot__isnull=True,
99 99
        invalid_since__isnull=False)
100
    invalid_cells = [c for c in invalid_cells if c.placeholder and not c.placeholder.startswith('_')]
100 101
    context = {
101 102
        'object_list': invalid_cells,
102 103
    }
......
647 648
        else:
648 649
            form.instance.order = 1
649 650
        PageSnapshot.take(self.cell.page, request=self.request, comment=_('changed cell "%s"') % self.cell)
650
        return super(PageListCellAddLinkView, self).form_valid(form)
651
        response = super(PageListCellAddLinkView, self).form_valid(form)
652
        self.cell.check_validity()
653
        return response
651 654

  
652 655
    def get_success_url(self):
653 656
        return '%s#cell-%s' % (
......
688 691
        else:
689 692
            response = super(PageListCellEditLinkView, self).form_valid(form)
690 693
        PageSnapshot.take(self.cell.page, request=self.request, comment=_('changed cell "%s"') % self.cell)
694
        self.cell.check_validity()
691 695
        return response
692 696

  
693 697
    def get_success_url(self):
......
722 726
    def delete(self, request, *args, **kwargs):
723 727
        response = super(PageListCellDeleteLinkView, self).delete(request, *args, **kwargs)
724 728
        PageSnapshot.take(self.cell.page, request=self.request, comment=_('changed cell "%s"') % self.cell)
729
        self.cell.check_validity()
725 730
        return response
726 731

  
727 732
    def get_success_url(self):
tests/test_cells.py
7 7

  
8 8
from combo.data.models import (
9 9
    Page, CellBase, TextCell, LinkCell, MenuCell, JsonCellBase,
10
    JsonCell, ConfigJsonCell, LinkListCell
10
    JsonCell, ConfigJsonCell, LinkListCell, FeedCell
11 11
)
12
from django.apps import apps
12 13
from django.conf import settings
13 14
from django.db import connection
14 15
from django.forms.widgets import Media
......
28 29

  
29 30
pytestmark = pytest.mark.django_db
30 31

  
32

  
33
@pytest.fixture
34
def context():
35
    ctx = {'request': RequestFactory().get('/')}
36
    ctx['request'].user = None
37
    ctx['request'].session = {}
38
    return ctx
39

  
40

  
31 41
def mock_json_response(content, **kwargs):
32 42
    content = force_bytes(content)
33 43
    text = force_text(content)
......
136 146
    assert cell.render(ctx).strip() == '<a href="http://example.net/#anchor">altertitle</a>'
137 147

  
138 148

  
149
def test_link_cell_validity():
150
    page = Page.objects.create(title='example page', slug='example-page')
151
    cell = LinkCell.objects.create(
152
        page=page,
153
        title='Example Site',
154
        order=0,
155
    )
156

  
157
    # no link defined
158
    assert cell.invalid_reason_code == 'data_url_not_defined'
159
    assert cell.invalid_since is not None
160

  
161
    # internal link - no check
162
    cell.link_page = page
163
    with mock.patch('combo.data.models.requests.get') as requests_get:
164
        mock_json = mock.Mock(status_code=404)
165
        requests_get.return_value = mock_json
166
        cell.save()
167
    assert requests_get.call_args_list == []
168
    assert cell.invalid_reason_code is None
169
    assert cell.invalid_since is None
170

  
171
    # external link
172
    cell.link_page = None
173
    cell.url = 'http://example.net/'
174
    with mock.patch('combo.data.models.requests.get') as requests_get:
175
        mock_json = mock.Mock(status_code=404)
176
        requests_get.return_value = mock_json
177
        cell.save()
178
    assert cell.invalid_reason_code == 'data_url_not_found'
179
    assert cell.invalid_since is not None
180

  
181
    with mock.patch('combo.data.models.requests.get') as requests_get:
182
        mock_json = mock.Mock(status_code=200)
183
        requests_get.return_value = mock_json
184
        cell.save()
185
    assert cell.invalid_reason_code is None
186
    assert cell.invalid_since is None
187

  
188
    with mock.patch('combo.data.models.requests.get') as requests_get:
189
        mock_json = mock.Mock(status_code=500)
190
        requests_get.return_value = mock_json
191
        cell.save()
192
    assert cell.invalid_reason_code is None
193
    assert cell.invalid_since is None
194

  
195
    with mock.patch('combo.data.models.requests.get') as requests_get:
196
        mock_json = mock.Mock(status_code=400)
197
        requests_get.return_value = mock_json
198
        cell.save()
199
    assert cell.invalid_reason_code == 'data_url_invalid'
200
    assert cell.invalid_since is not None
201

  
202

  
139 203
def test_link_list_cell():
140 204
    page = Page.objects.create(title='example page', slug='example-page')
141 205

  
......
167 231
    assert '<ul><li><a href="http://example.net/#anchor">altertitle</a></li></ul>' in cell.render(ctx)
168 232

  
169 233

  
234
def test_link_list_cell_validity():
235
    page = Page.objects.create(title='example page', slug='example-page')
236

  
237
    cell = LinkListCell.objects.create(order=0, page=page)
238
    item = LinkCell.objects.create(page=page, placeholder=cell.link_placeholder, order=0)
239

  
240
    item.mark_as_valid()
241
    cell.check_validity()
242
    assert cell.invalid_reason_code is None
243
    assert cell.invalid_since is None
244

  
245
    item.mark_as_invalid('foo_bar_reason')
246
    cell.check_validity()
247
    assert cell.invalid_reason_code == 'data_link_invalid'
248
    assert cell.invalid_since is not None
249

  
250

  
251
def test_feed_cell_validity(context):
252
    page = Page.objects.create(title='example page', slug='example-page')
253
    cell = FeedCell.objects.create(page=page, placeholder='content', order=1)
254

  
255
    cell.get_cell_extra_context(context)
256
    assert cell.invalid_reason_code == 'data_url_not_defined'
257
    assert cell.invalid_since is not None
258

  
259
    cell.url = 'http://example.net/'
260
    cell.save()
261
    with mock.patch('combo.data.models.requests.get') as requests_get:
262
        mock_json = mock.Mock(status_code=404)
263
        requests_get.return_value = mock_json
264
        cell.get_cell_extra_context(context)
265
    assert cell.invalid_reason_code == 'data_url_not_found'
266
    assert cell.invalid_since is not None
267

  
268
    with mock.patch('combo.data.models.requests.get') as requests_get:
269
        mock_json = mock.Mock(status_code=200, content='')
270
        requests_get.return_value = mock_json
271
        cell.get_cell_extra_context(context)
272
    assert cell.invalid_reason_code is None
273
    assert cell.invalid_since is None
274

  
275
    with mock.patch('combo.data.models.requests.get') as requests_get:
276
        mock_json = mock.Mock(status_code=500)
277
        requests_get.return_value = mock_json
278
        cell.get_cell_extra_context(context)
279
    assert cell.invalid_reason_code is None
280
    assert cell.invalid_since is None
281

  
282
    with mock.patch('combo.data.models.requests.get') as requests_get:
283
        mock_json = mock.Mock(status_code=400)
284
        requests_get.return_value = mock_json
285
        cell.get_cell_extra_context(context)
286
    assert cell.invalid_reason_code == 'data_url_invalid'
287
    assert cell.invalid_since is not None
288

  
289

  
170 290
def test_menu_cell():
171 291
    Page.objects.all().delete()
172 292
    parent = Page.objects.create(
......
388 508
        assert '/var1=foo/' in resp.text
389 509
        assert '/var2=bar/' in resp.text
390 510

  
511

  
512
def test_json_cell_validity(context):
513
    page = Page.objects.create(title='example page', slug='example-page')
514
    cell = JsonCell.objects.create(
515
        page=page, placeholder='content', order=1,
516
        varnames_str='var1, var2, ',
517
        url='http://foo?varone=[var1]&vartwo=[var2]',
518
        template_string='/var1={{var1}}/var2={{var2}}/')
519

  
520
    with mock.patch('combo.data.models.requests.get') as requests_get:
521
        cell.get_cell_extra_context(context)
522
    assert requests_get.call_args_list == []  # invalid context
523
    assert cell.invalid_reason_code is None
524
    assert cell.invalid_since is None
525

  
526
    context['var1'] = 'foo'
527
    context['var2'] = 'bar'
528
    context['synchronous'] = True  # to get fresh content
529
    with mock.patch('combo.utils.requests.get') as requests_get:
530
        mock_json = mock.Mock(status_code=404)
531
        requests_get.side_effect = [mock_json]
532
        cell.get_cell_extra_context(context)
533
    assert cell.invalid_reason_code == 'data_url_not_found'
534
    assert cell.invalid_since is not None
535

  
536
    with mock.patch('combo.utils.requests.get') as requests_get:
537
        data = {'data': []}
538
        requests_get.return_value = mock_json_response(content=json.dumps(data), status_code=200)
539
        cell.get_cell_extra_context(context)
540
    assert cell.invalid_reason_code is None
541
    assert cell.invalid_since is None
542

  
543
    with mock.patch('combo.utils.requests.get') as requests_get:
544
        mock_json = mock.Mock(status_code=500)
545
        requests_get.return_value = mock_json
546
        cell.get_cell_extra_context(context)
547
    assert cell.invalid_reason_code is None
548
    assert cell.invalid_since is None
549

  
550
    with mock.patch('combo.utils.requests.get') as requests_get:
551
        mock_json = mock.Mock(status_code=400)
552
        requests_get.return_value = mock_json
553
        cell.get_cell_extra_context(context)
554
    assert cell.invalid_reason_code == 'data_url_invalid'
555
    assert cell.invalid_since is not None
556

  
557

  
391 558
def test_config_json_cell():
392 559
    page = Page(title='example page', slug='example-page')
393 560
    page.save()
......
508 675
            assert requests_get.call_args[-1]['log_errors'] == False
509 676
            assert requests_get.call_args[-1]['timeout'] == 42
510 677

  
678

  
679
def test_config_json_cell_validity(settings, context):
680
    settings.JSON_CELL_TYPES = {
681
        'test-config-json-cell': {
682
            'name': 'Foobar',
683
            'url': 'http://foo?varone=[var1]&vartwo=[var2]',
684
            'varnames': ['var1', 'var2']
685
        },
686
    }
687
    templates_settings = [settings.TEMPLATES[0].copy()]
688
    templates_settings[0]['DIRS'] = ['%s/templates-1' % os.path.abspath(os.path.dirname(__file__))]
689
    settings.TEMPLATES = templates_settings
690

  
691
    page = Page.objects.create(title='example page', slug='example-page')
692
    cell = ConfigJsonCell.objects.create(
693
        page=page, placeholder='content', order=1,
694
        key='test-config-json-cell',
695
        parameters={'identifier': 'plop'})
696
    assert cell.varnames == ['var1', 'var2']
697

  
698
    with mock.patch('combo.data.models.requests.get') as requests_get:
699
        cell.get_cell_extra_context(context)
700
    assert requests_get.call_args_list == []  # invalid context
701
    assert cell.invalid_reason_code is None
702
    assert cell.invalid_since is None
703

  
704
    context['var1'] = 'foo'
705
    context['var2'] = 'bar'
706
    context['synchronous'] = True  # to get fresh content
707
    with mock.patch('combo.utils.requests.get') as requests_get:
708
        mock_json = mock.Mock(status_code=404)
709
        requests_get.side_effect = [mock_json]
710
        cell.get_cell_extra_context(context)
711
    assert cell.invalid_reason_code == 'data_url_not_found'
712
    assert cell.invalid_since is not None
713

  
714
    with mock.patch('combo.utils.requests.get') as requests_get:
715
        data = {'data': []}
716
        requests_get.return_value = mock_json_response(content=json.dumps(data), status_code=200)
717
        cell.get_cell_extra_context(context)
718
    assert cell.invalid_reason_code is None
719
    assert cell.invalid_since is None
720

  
721
    with mock.patch('combo.utils.requests.get') as requests_get:
722
        mock_json = mock.Mock(status_code=500)
723
        requests_get.return_value = mock_json
724
        cell.get_cell_extra_context(context)
725
    assert cell.invalid_reason_code is None
726
    assert cell.invalid_since is None
727

  
728
    with mock.patch('combo.utils.requests.get') as requests_get:
729
        mock_json = mock.Mock(status_code=400)
730
        requests_get.return_value = mock_json
731
        cell.get_cell_extra_context(context)
732
    assert cell.invalid_reason_code == 'data_url_invalid'
733
    assert cell.invalid_since is not None
734

  
735

  
511 736
def test_json_force_async():
512 737
    cell = JsonCellBase()
513 738
    cell.url = 'http://example.net/test-force-async'
......
803 1028
    # invalid cell since two days
804 1029
    cell.invalid_since = now() - datetime.timedelta(days=2)
805 1030
    assert cell.is_visible() is False
1031

  
1032

  
1033
def test_hourly():
1034
    appconfig = apps.get_app_config('data')
1035
    page = Page.objects.create(title='xxx', slug='test_current_forms_cell_render', template_name='standard')
1036
    cell_classes = [c for c in appconfig.get_models() if c in get_cell_classes()]
1037
    for klass in cell_classes:
1038
        klass.objects.create(page=page, placeholder='content', order=0)
1039
    for klass in cell_classes:
1040
        if klass in [LinkCell, LinkListCell]:
1041
            with mock.patch('combo.data.models.%s.check_validity' % klass.__name__) as check_validity:
1042
                appconfig.hourly()
1043
            assert check_validity.call_args_list == [mock.call()]
1044
        else:
1045
            assert hasattr(klass, 'check_validity') is False
tests/test_manager.py
176 176
    resp = app.get('/manage/pages/%s/' % page.pk)
177 177
    assert '<span class="invalid">/!\\ foo_bar_reason</span>' not in resp.text
178 178

  
179
    cell2 = LinkListCell.objects.create(order=0, placeholder='content', page=page)
180
    item = LinkCell.objects.create(page=page, placeholder=cell2.link_placeholder, order=0)
181
    item.mark_as_invalid('foo_bar_reason')
182
    cell2.check_validity()
183
    resp = app.get('/manage/pages/%s/' % page.pk)
184
    assert '<span class="invalid">/!\\ Invalid link</span>' in resp.text
185
    assert '<span class="invalid">/!\\ foo_bar_reason</span>' in resp.text
186

  
179 187

  
180 188
def test_edit_page_optional_placeholder(app, admin_user):
181 189
    Page.objects.all().delete()
......
596 604
    assert '<a href="/manage/pages/{}/">{}</a>'.format(page.pk, page.title) in resp.text
597 605
    assert '<a href="/manage/pages/{}/#cell-{}">{}</a>'.format(page.pk, cell.get_reference(), cell.get_label()) in resp.text
598 606

  
607
    # cells from snapshot are not reported
599 608
    snapshot = PageSnapshot.objects.create(page=page)
600 609
    page.snapshot = snapshot
601 610
    page.save()
602 611
    resp = app.get('/manage/cells/invalid-report/')
603 612
    assert resp.context['object_list'] == []
604 613

  
614
    # cells used in LinkListCell are not reported
615
    page.snapshot = None
616
    page.save()
617
    cell2 = LinkListCell.objects.create(order=0, placeholder='content', page=page)
618
    item = LinkCell.objects.create(page=page, placeholder=cell2.link_placeholder, order=0)
619
    item.mark_as_invalid('foo_bar_reason')
620
    resp = app.get('/manage/cells/invalid-report/')
621
    assert resp.context['object_list'] == [cell]
622

  
605 623

  
606 624
def test_duplicate_page(app, admin_user):
607 625
    page = Page.objects.create(title='One', slug='one', template_name='standard', exclude_from_navigation=False)
608
-