Projet

Général

Profil

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

Lauréline Guérin, 03 mars 2020 14:49

Télécharger (32,1 ko)

Voir les différences:

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

 combo/data/apps.py                            |   9 +
 combo/data/models.py                          | 155 +++++++++--
 .../combo/manager/link-list-cell-form.html    |  11 +-
 combo/manager/static/css/combo.manager.css    |   4 +-
 combo/manager/views.py                        |   7 +-
 combo/public/views.py                         |   2 +-
 tests/test_cells.py                           | 241 +++++++++++++++++-
 tests/test_manager.py                         |  20 +-
 tests/test_public.py                          |   5 +-
 9 files changed, 423 insertions(+), 31 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
768 768
        return validity_info
769 769

  
770 770
    def mark_as_valid(self, save=True):
771
        validity_info = self.validity_info.all().first()
771
        validity_info = self.get_validity_info()
772 772
        if validity_info is None:
773 773
            return
774 774
        if save:
......
782 782
            return self.prefetched_validity_info[0]
783 783
        return self.validity_info.all().first()
784 784

  
785
    def set_validity_from_url(
786
            self, resp,
787
            not_found_code='url_not_found', invalid_code='url_invalid',
788
            save=True):
789
        if resp is None:
790
            # can not retrieve data, don't report cell as invalid
791
            self.mark_as_valid(save=save)
792
        elif resp.status_code == 404:
793
            self.mark_as_invalid(not_found_code, save=save)
794
        elif 400 <= resp.status_code < 500:
795
            # 4xx error, cell is invalid
796
            self.mark_as_invalid(invalid_code, save=save)
797
        else:
798
            # 2xx or 3xx: cell is valid
799
            # 5xx error: can not retrieve data, don't report cell as invalid
800
            self.mark_as_valid(save=save)
801

  
785 802
    def get_invalid_reason(self):
786 803
        validity_info = self.get_validity_info()
787 804
        if validity_info is None:
......
1015 1032
    edit_link_label = _('Edit link')
1016 1033
    add_as_link_code = 'link'
1017 1034

  
1035
    invalid_reason_codes = {
1036
        'data_url_not_defined': _('No link set'),
1037
        'data_url_not_found': _('URL seems to unexist'),
1038
        'data_url_invalid': _('URL seems to be invalid'),
1039
    }
1040

  
1018 1041
    class Meta:
1019 1042
        verbose_name = _('Link')
1020 1043

  
1044
    def save(self, *args, **kwargs):
1045
        if 'update_fields' in kwargs:
1046
            # don't check validity
1047
            return super(LinkCell, self).save(*args, **kwargs)
1048

  
1049
        result = super(LinkCell, self).save(*args, **kwargs)
1050
        # check validity
1051
        self.check_validity()
1052
        return result
1053

  
1021 1054
    def get_additional_label(self):
1022 1055
        title = self.title
1023 1056
        if not title and self.link_page:
......
1026 1059
            return None
1027 1060
        return utils.ellipsize(title)
1028 1061

  
1062
    def get_url(self, context=None):
1063
        context = context or {}
1064
        if self.link_page:
1065
            url = self.link_page.get_online_url()
1066
        else:
1067
            url = utils.get_templated_url(self.url, context=context)
1068
        if self.anchor:
1069
            url += '#' + self.anchor
1070
        return url
1071

  
1029 1072
    def get_cell_extra_context(self, context):
1030 1073
        render_skeleton = context.get('render_skeleton')
1031 1074
        request = context.get('request')
1032 1075
        extra_context = super(LinkCell, self).get_cell_extra_context(context)
1033 1076
        if self.link_page:
1034
            extra_context['url'] = self.link_page.get_online_url()
1035 1077
            extra_context['title'] = self.title or self.link_page.title
1036 1078
        else:
1037
            extra_context['url'] = utils.get_templated_url(self.url, context=context)
1038 1079
            extra_context['title'] = self.title or self.url
1039
        if self.anchor:
1040
            extra_context['url'] += '#' + self.anchor
1041
        if render_skeleton and not urlparse.urlparse(extra_context['url']).netloc:
1080
        url = self.get_url(context)
1081
        if render_skeleton and not urlparse.urlparse(url).netloc:
1042 1082
            # create full URL when used in a skeleton
1043
            extra_context['url'] = request.build_absolute_uri(extra_context['url'])
1083
            url = request.build_absolute_uri(url)
1084
        extra_context['url'] = url
1044 1085
        return extra_context
1045 1086

  
1046 1087
    def get_default_form_class(self):
......
1058 1099
            return [link_data]
1059 1100
        return []
1060 1101

  
1102
    def check_validity(self):
1103
        if self.link_page:
1104
            self.mark_as_valid()
1105
            return
1106
        if not self.url:
1107
            self.mark_as_invalid('data_url_not_defined')
1108
            return
1109

  
1110
        resp = None
1111
        try:
1112
            resp = requests.get(self.get_url(), timeout=settings.REQUESTS_TIMEOUT)
1113
            resp.raise_for_status()
1114
        except (requests.exceptions.RequestException):
1115
            pass
1116

  
1117
        self.set_validity_from_url(resp, not_found_code='data_url_not_found', invalid_code='data_url_invalid')
1118

  
1061 1119

  
1062 1120
@register_cell_class
1063 1121
class LinkListCell(CellBase):
......
1066 1124
    template_name = 'combo/link-list-cell.html'
1067 1125
    manager_form_template = 'combo/manager/link-list-cell-form.html'
1068 1126

  
1127
    invalid_reason_codes = {
1128
        'data_link_invalid': _('Invalid link'),
1129
    }
1130

  
1069 1131
    class Meta:
1070 1132
        verbose_name = _('List of links')
1071 1133

  
......
1073 1135
    def link_placeholder(self):
1074 1136
        return '_linkslist:{}'.format(self.pk)
1075 1137

  
1076
    def get_items(self):
1138
    def get_items(self, prefetch_validity_info=False):
1077 1139
        return CellBase.get_cells(
1078 1140
            page=self.page,
1079 1141
            placeholder=self.link_placeholder,
1080
            cell_filter=lambda x: hasattr(x, 'add_as_link_label'))
1142
            cell_filter=lambda x: hasattr(x, 'add_as_link_label'),
1143
            prefetch_validity_info=prefetch_validity_info)
1144

  
1145
    def get_items_with_prefetch(self):
1146
        return self.get_items(prefetch_validity_info=True)
1081 1147

  
1082 1148
    def get_additional_label(self):
1083 1149
        title = self.title
......
1129 1195
        for link in self.get_items():
1130 1196
            link.duplicate(page_target=new_cell.page, placeholder=new_cell.link_placeholder)
1131 1197

  
1198
    def check_validity(self):
1199
        for link in self.get_items(prefetch_validity_info=True):
1200
            validity_info = link.get_validity_info()
1201
            if validity_info is not None:
1202
                self.mark_as_invalid('data_link_invalid')
1203
                return
1204
        self.mark_as_valid()
1205

  
1132 1206

  
1133 1207
@register_cell_class
1134 1208
class FeedCell(CellBase):
1135 1209
    title = models.CharField(_('Title'), max_length=150, blank=True)
1136 1210
    url = models.CharField(_('URL'), blank=True, max_length=200)
1137
    limit = models.PositiveSmallIntegerField(_('Maximum number of entries'),
1138
            null=True, blank=True)
1211
    limit = models.PositiveSmallIntegerField(
1212
        _('Maximum number of entries'),
1213
        null=True, blank=True)
1139 1214

  
1140 1215
    manager_form_factory_kwargs = {'field_classes': {'url': TemplatableURLField}}
1141 1216
    template_name = 'combo/feed-cell.html'
1142 1217

  
1218
    invalid_reason_codes = {
1219
        'data_url_not_defined': _('No URL set'),
1220
        'data_url_not_found': _('URL seems to unexist'),
1221
        'data_url_invalid': _('URL seems to be invalid'),
1222
    }
1223

  
1143 1224
    class Meta:
1144 1225
        verbose_name = _('RSS/Atom Feed')
1145 1226

  
......
1148 1229

  
1149 1230
    def get_cell_extra_context(self, context):
1150 1231
        extra_context = super(FeedCell, self).get_cell_extra_context(context)
1232

  
1233
        if not self.url:
1234
            self.mark_as_invalid('data_url_not_defined')
1235
            return extra_context
1236

  
1151 1237
        if context.get('placeholder_search_mode'):
1152 1238
            # don't call webservices when we're just looking for placeholders
1153 1239
            return extra_context
1240

  
1154 1241
        cache_key = hashlib.md5(smart_bytes(self.url)).hexdigest()
1155 1242
        feed_content = cache.get(cache_key)
1156 1243
        if not feed_content:
1244
            feed_response = None
1157 1245
            try:
1158 1246
                feed_response = requests.get(utils.get_templated_url(self.url))
1159 1247
                feed_response.raise_for_status()
......
1163 1251
                if feed_response.status_code == 200:
1164 1252
                    feed_content = feed_response.content
1165 1253
                    cache.set(cache_key, feed_content, 600)
1254
            self.set_validity_from_url(feed_response, not_found_code='data_url_not_found', invalid_code='data_url_invalid')
1166 1255
        if feed_content:
1167 1256
            extra_context['feed'] = feedparser.parse(feed_content)
1168 1257
            if self.limit:
......
1240 1329

  
1241 1330
    _json_content = None
1242 1331

  
1332
    invalid_reason_codes = {
1333
        'data_url_not_found': _('URL seems to unexist'),
1334
        'data_url_invalid': _('URL seems to be invalid'),
1335
    }
1336

  
1243 1337
    class Meta:
1244 1338
        abstract = True
1245 1339

  
......
1291 1385
            if not url:
1292 1386
                continue
1293 1387
            try:
1294
                json_response = utils.requests.get(url,
1295
                        headers={'Accept': 'application/json'},
1296
                        remote_service='auto',
1297
                        cache_duration=data_url_dict.get('cache_duration', self.cache_duration),
1298
                        without_user=True,
1299
                        raise_if_not_cached=not(context.get('synchronous')),
1300
                        invalidate_cache=invalidate_cache,
1301
                        log_errors=log_errors,
1302
                        timeout=data_url_dict.get('timeout', self.timeout),
1303
                        django_request=context.get('request'),
1304
                        )
1388
                json_response = utils.requests.get(
1389
                    url,
1390
                    headers={'Accept': 'application/json'},
1391
                    remote_service='auto',
1392
                    cache_duration=data_url_dict.get('cache_duration', self.cache_duration),
1393
                    without_user=True,
1394
                    raise_if_not_cached=not(context.get('synchronous')),
1395
                    invalidate_cache=invalidate_cache,
1396
                    log_errors=log_errors,
1397
                    timeout=data_url_dict.get('timeout', self.timeout),
1398
                    django_request=context.get('request'),
1399
                )
1305 1400
            except requests.RequestException as e:
1306 1401
                extra_context[data_key + '_status'] = -1
1307 1402
                extra_context[data_key + '_error'] = force_text(e)
......
1335 1430
            # templated URLs
1336 1431
            context[data_key] = extra_context[data_key]
1337 1432

  
1433
        if not self._meta.abstract:
1434
            returns = [extra_context.get(d['key'] + '_status') for d in data_urls]
1435
            returns = set([s for s in returns if s is not None])
1436
            if returns and 200 not in returns:  # not a single valid answer
1437
                if 404 in returns:
1438
                    self.mark_as_invalid('data_url_not_found')
1439
                elif any([400 <= r < 500 for r in returns]):
1440
                    # at least 4xx errors, report the cell as invalid
1441
                    self.mark_as_invalid('data_url_invalid')
1442
                else:
1443
                    # 2xx or 3xx: cell is valid
1444
                    # 5xx error: can not retrieve data, don't report cell as invalid
1445
                    self.mark_as_valid()
1446
            else:
1447
                self.mark_as_valid()
1448

  
1338 1449
        # keep cache of first response as it may be used to find the
1339 1450
        # appropriate template.
1340 1451
        self._json_content = extra_context[self.first_data_key]
combo/data/templates/combo/manager/link-list-cell-form.html
3 3

  
4 4
{% block cell-form %}
5 5
{{ form.as_p }}
6
{% with cell.get_items as links %}
6
{% with cell.get_items_with_prefetch as links %}
7 7
{% if links %}
8 8
<p><label>{% trans "Links:" %}</label></p>
9 9
<div>
......
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
     {% with link.get_invalid_reason as invalid_reason %}
17
     {% if invalid_reason %}
18
     <span class="invalid"><span class="invalid-icon"></span>{{ link.get_invalid_reason }}</span>
19
     {% endif %}
20
     {% endwith %}
21
   </span>
15 22
       <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 23
       <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 24
   </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
.invalid-icon {
combo/manager/views.py
97 97
        select_related=['page'],
98 98
        page__snapshot__isnull=True,
99 99
        validity_info__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):
combo/public/views.py
518 518
        if redirect_url:
519 519
            return HttpResponseRedirect(redirect_url)
520 520

  
521
    cells = CellBase.get_cells(page=page)
521
    cells = CellBase.get_cells(page=page, prefetch_validity_info=True)
522 522
    extend_with_parent_cells(cells, hierarchy=pages)
523 523
    cells = [x for x in cells if x.is_visible(user=request.user)]
524 524
    mark_duplicated_slugs(cells)
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, ValidityInfo
10
    JsonCell, ConfigJsonCell, LinkListCell, FeedCell, ValidityInfo
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
    validity_info = ValidityInfo.objects.latest('pk')
159
    assert validity_info.invalid_reason_code == 'data_url_not_defined'
160
    assert validity_info.invalid_since is not None
161

  
162
    # internal link - no check
163
    cell.link_page = page
164
    with mock.patch('combo.data.models.requests.get') as requests_get:
165
        mock_json = mock.Mock(status_code=404)
166
        requests_get.return_value = mock_json
167
        cell.save()
168
    assert requests_get.call_args_list == []
169
    assert ValidityInfo.objects.exists() is False
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
    validity_info = ValidityInfo.objects.latest('pk')
179
    assert validity_info.invalid_reason_code == 'data_url_not_found'
180
    assert validity_info.invalid_since is not None
181

  
182
    with mock.patch('combo.data.models.requests.get') as requests_get:
183
        mock_json = mock.Mock(status_code=200)
184
        requests_get.return_value = mock_json
185
        cell.save()
186
    assert ValidityInfo.objects.exists() is False
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 ValidityInfo.objects.exists() is False
193

  
194
    with mock.patch('combo.data.models.requests.get') as requests_get:
195
        mock_json = mock.Mock(status_code=400)
196
        requests_get.return_value = mock_json
197
        cell.save()
198
    validity_info = ValidityInfo.objects.latest('pk')
199
    assert validity_info.invalid_reason_code == 'data_url_invalid'
200
    assert validity_info.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 ValidityInfo.objects.exists() is False
243

  
244
    item.mark_as_invalid('foo_bar_reason')
245
    cell.check_validity()
246
    validity_info = ValidityInfo.objects.latest('pk')
247
    assert validity_info.invalid_reason_code == 'data_link_invalid'
248
    assert validity_info.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
    validity_info = ValidityInfo.objects.latest('pk')
257
    assert validity_info.invalid_reason_code == 'data_url_not_defined'
258
    assert validity_info.invalid_since is not None
259

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

  
270
    with mock.patch('combo.data.models.requests.get') as requests_get:
271
        mock_json = mock.Mock(status_code=200, content='')
272
        requests_get.return_value = mock_json
273
        cell.get_cell_extra_context(context)
274
    assert ValidityInfo.objects.exists() is False
275

  
276
    with mock.patch('combo.data.models.requests.get') as requests_get:
277
        mock_json = mock.Mock(status_code=500)
278
        requests_get.return_value = mock_json
279
        cell.get_cell_extra_context(context)
280
    assert ValidityInfo.objects.exists() is False
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
    validity_info = ValidityInfo.objects.latest('pk')
287
    assert validity_info.invalid_reason_code == 'data_url_invalid'
288
    assert validity_info.invalid_since is not None
289

  
290

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

  
512

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

  
521
    with mock.patch('combo.data.models.requests.get') as requests_get:
522
        cell.get_cell_extra_context(context)
523
    assert requests_get.call_args_list == []  # invalid context
524
    assert ValidityInfo.objects.exists() is False
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
    validity_info = ValidityInfo.objects.latest('pk')
534
    assert validity_info.invalid_reason_code == 'data_url_not_found'
535
    assert validity_info.invalid_since is not None
536

  
537
    with mock.patch('combo.utils.requests.get') as requests_get:
538
        data = {'data': []}
539
        requests_get.return_value = mock_json_response(content=json.dumps(data), status_code=200)
540
        cell.get_cell_extra_context(context)
541
    assert ValidityInfo.objects.exists() is False
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 ValidityInfo.objects.exists() is False
548

  
549
    with mock.patch('combo.utils.requests.get') as requests_get:
550
        mock_json = mock.Mock(status_code=400)
551
        requests_get.return_value = mock_json
552
        cell.get_cell_extra_context(context)
553
    validity_info = ValidityInfo.objects.latest('pk')
554
    assert validity_info.invalid_reason_code == 'data_url_invalid'
555
    assert validity_info.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 ValidityInfo.objects.exists() is False
702

  
703
    context['var1'] = 'foo'
704
    context['var2'] = 'bar'
705
    context['synchronous'] = True  # to get fresh content
706
    with mock.patch('combo.utils.requests.get') as requests_get:
707
        mock_json = mock.Mock(status_code=404)
708
        requests_get.side_effect = [mock_json]
709
        cell.get_cell_extra_context(context)
710
    validity_info = ValidityInfo.objects.latest('pk')
711
    assert validity_info.invalid_reason_code == 'data_url_not_found'
712
    assert validity_info.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 ValidityInfo.objects.exists() is False
719

  
720
    with mock.patch('combo.utils.requests.get') as requests_get:
721
        mock_json = mock.Mock(status_code=500)
722
        requests_get.return_value = mock_json
723
        cell.get_cell_extra_context(context)
724
    assert ValidityInfo.objects.exists() is False
725

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

  
734

  
511 735
def test_json_force_async():
512 736
    cell = JsonCellBase()
513 737
    cell.url = 'http://example.net/test-force-async'
......
807 1031
    validity_info.invalid_since = now() - datetime.timedelta(days=2)
808 1032
    validity_info.save()
809 1033
    assert cell.is_visible() is False
1034

  
1035

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

  
185
    cell2 = LinkListCell.objects.create(order=0, placeholder='content', page=page)
186
    item = LinkCell.objects.create(page=page, placeholder=cell2.link_placeholder, order=0)
187
    item.mark_as_invalid('foo_bar_reason')
188
    cell2.check_validity()
189
    resp = app.get('/manage/pages/%s/' % page.pk)
190
    assert '<span class="invalid"><span class="invalid-icon"></span>Invalid link</span>' in resp.text
191
    assert '<span class="invalid"><span class="invalid-icon"></span>foo_bar_reason</span>' in resp.text
192

  
185 193

  
186 194
def test_edit_page_optional_placeholder(app, admin_user):
187 195
    Page.objects.all().delete()
......
367 375
    app.get('/manage/pages/%s/' % page.pk)  # load once to populate caches
368 376
    with CaptureQueriesContext(connection) as ctx:
369 377
        app.get('/manage/pages/%s/' % page.pk)
370
        assert len(ctx.captured_queries) == 29
378
        assert len(ctx.captured_queries) == 31
371 379

  
372 380

  
373 381
def test_delete_page(app, admin_user):
......
602 610
    assert '<a href="/manage/pages/{}/">{}</a>'.format(page.pk, page.title) in resp.text
603 611
    assert '<a href="/manage/pages/{}/#cell-{}">{}</a>'.format(page.pk, cell.get_reference(), cell.get_label()) in resp.text
604 612

  
613
    # cells from snapshot are not reported
605 614
    snapshot = PageSnapshot.objects.create(page=page)
606 615
    page.snapshot = snapshot
607 616
    page.save()
608 617
    resp = app.get('/manage/cells/invalid-report/')
609 618
    assert resp.context['object_list'] == []
610 619

  
620
    # cells used in LinkListCell are not reported
621
    page.snapshot = None
622
    page.save()
623
    cell2 = LinkListCell.objects.create(order=0, placeholder='content', page=page)
624
    item = LinkCell.objects.create(page=page, placeholder=cell2.link_placeholder, order=0)
625
    item.mark_as_invalid('foo_bar_reason')
626
    resp = app.get('/manage/cells/invalid-report/')
627
    assert resp.context['object_list'] == [cell]
628

  
611 629

  
612 630
def test_duplicate_page(app, admin_user):
613 631
    page = Page.objects.create(title='One', slug='one', template_name='standard', exclude_from_navigation=False)
tests/test_public.py
180 180
        assert resp.text.count('BARFOO') == 1
181 181
        assert resp.text.count('BAR2FOO') == 1
182 182
        queries_count_third = len(ctx.captured_queries)
183
        assert queries_count_third == queries_count_second
183
        # +2 for validity info of parent page
184
        assert queries_count_third == queries_count_second + 2
184 185

  
185 186
    with CaptureQueriesContext(connection) as ctx:
186 187
        resp = app.get('/second/third/fourth/', status=200)
......
188 189
        assert resp.text.count('BAR2FOO') == 1
189 190
        queries_count_fourth = len(ctx.captured_queries)
190 191
        # +1 for get_parents_and_self()
191
        assert queries_count_fourth == queries_count_second + 1
192
        assert queries_count_fourth == queries_count_second + 2 + 1
192 193

  
193 194
    # check footer doesn't get duplicated in real index children
194 195
    page6 = Page(title='Sixth', slug='sixth', template_name='standard', parent=page_index)
195
-