wcs: add cell to display card details (#46767)

This commit is contained in:
Lauréline Guérin 2020-09-22 15:23:42 +02:00
parent e642164112
commit f9881c7a5b
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
6 changed files with 311 additions and 27 deletions

View File

@ -111,8 +111,8 @@ class AppConfig(django.apps.AppConfig):
def update_db_cache(self):
from combo.data.models import CellBase
from .models import WcsFormCell, WcsCategoryCell, WcsFormsOfCategoryCell, WcsCardsCell
models_to_update = [WcsFormCell, WcsCategoryCell, WcsFormsOfCategoryCell, WcsCardsCell]
from .models import WcsFormCell, WcsCategoryCell, WcsFormsOfCategoryCell, WcsCardsCell, WcsCardInfosCell
models_to_update = [WcsFormCell, WcsCategoryCell, WcsFormsOfCategoryCell, WcsCardsCell, WcsCardInfosCell]
for cell in CellBase.get_cells(cell_filter=lambda x: x in models_to_update):
cell.save()

View File

@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import combo.apps.wcs.models
from django.db import migrations, models
import django.db.models.deletion
import jsonfield.fields
class Migration(migrations.Migration):
dependencies = [
('auth', '0008_alter_user_username_max_length'),
('data', '0045_link_list_limit'),
('wcs', '0020_cards'),
]
operations = [
migrations.CreateModel(
name='WcsCardInfosCell',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('placeholder', models.CharField(max_length=20)),
('order', models.PositiveIntegerField()),
('slug', models.SlugField(blank=True, verbose_name='Slug')),
('extra_css_class', models.CharField(blank=True, max_length=100, verbose_name='Extra classes for CSS styling')),
('public', models.BooleanField(default=True, verbose_name='Public')),
('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')),
('last_update_timestamp', models.DateTimeField(auto_now=True)),
('carddef_reference', models.CharField(max_length=150, verbose_name='Card Model')),
('cached_title', models.CharField(max_length=150, verbose_name='Title')),
('cached_json', jsonfield.fields.JSONField(blank=True, default=dict)),
('groups', models.ManyToManyField(blank=True, to='auth.Group', verbose_name='Groups')),
('page', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='data.Page')),
],
options={
'verbose_name': 'Card Information Cell',
},
bases=(combo.apps.wcs.models.CardMixin, models.Model),
),
]

View File

@ -710,22 +710,43 @@ class CategoriesCell(WcsDataBaseCell):
verbose_name = _('Form Categories')
class CardMixin(object):
invalid_reason_codes = invalid_reason_codes
def is_relevant(self, context):
return bool(self.carddef_reference)
def get_default_form_class(self):
from .forms import WcsCardsCellForm
return WcsCardsCellForm
@property
def wcs_site(self):
return self.carddef_reference.split(':')[0]
def get_additional_label(self):
if not self.cached_title:
return
return self.cached_title
@register_cell_class
class WcsCardsCell(CellBase, WcsBlurpMixin):
class WcsCardsCell(CardMixin, WcsBlurpMixin, CellBase):
carddef_reference = models.CharField(_('Card Model'), max_length=150)
cached_title = models.CharField(_('Title'), max_length=150)
template_name = 'combo/wcs/cards.html'
variable_name = 'cards'
invalid_reason_codes = invalid_reason_codes
class Meta:
verbose_name = _('Cards')
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if 'update_fields' in kwargs:
# don't populate the cache
return super().save(*args, **kwargs)
return
def populate_cache():
if self.carddef_reference:
@ -754,23 +775,11 @@ class WcsCardsCell(CellBase, WcsBlurpMixin):
else:
return self.mark_as_invalid('wcs_card_not_defined')
super().save(*args, **kwargs)
populate_cache()
def is_relevant(self, context):
return bool(self.carddef_reference)
@property
def wcs_site(self):
return self.carddef_reference.split(':')[0]
def get_api_url(self, context):
return 'api/cards/%s/list' % self.carddef_reference.split(':')[1]
def get_default_form_class(self):
from .forms import WcsCardsCellForm
return WcsCardsCellForm
def get_cell_extra_context(self, context):
extra_context = super().get_cell_extra_context(context)
extra_context.update(WcsBlurpMixin.get_cell_extra_context(self, context))
@ -790,15 +799,82 @@ class WcsCardsCell(CellBase, WcsBlurpMixin):
return extra_context
def get_additional_label(self):
if not self.cached_title:
return
return self.cached_title
def render_for_search(self):
return ''
@register_cell_class
class WcsCardInfosCell(CardMixin, CellBase):
carddef_reference = models.CharField(_('Card Model'), max_length=150)
cached_title = models.CharField(_('Title'), max_length=150)
cached_json = JSONField(blank=True)
is_enabled = classmethod(is_wcs_enabled)
template_name = 'combo/wcs/card.html'
class Meta:
verbose_name = _('Card Information Cell')
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if 'update_fields' in kwargs:
# don't populate the cache
return
def populate_cache():
if self.carddef_reference:
wcs_key, card_slug = self.carddef_reference.split(':')
wcs_site = get_wcs_services().get(wcs_key)
card_schema = get_wcs_json(wcs_site, 'api/cards/%s/@schema' % card_slug)
if not card_schema:
# can not retrieve data, don't report cell as invalid
self.mark_as_valid()
return
if card_schema.get('err') == 1:
if card_schema.get('err_class') == 'Page not found':
self.mark_as_invalid('wcs_card_not_found')
return
self.cached_title = card_schema['name']
self.cached_json = card_schema
self.save(update_fields=['cached_title', 'cached_json'])
self.mark_as_valid()
else:
self.mark_as_invalid('wcs_card_not_defined')
populate_cache()
def get_cell_extra_context(self, context):
extra_context = super().get_cell_extra_context(context)
extra_context['title'] = self.cached_title
extra_context['schema'] = self.cached_json
card_slug = self.carddef_reference.split(':')[1]
card_id = '%s_id' % card_slug
if not context.get(card_id):
return extra_context
api_url = 'api/cards/%s/%s/' % (card_slug, context.get(card_id))
wcs_site = get_wcs_services().get(self.wcs_site)
response = requests.get(
api_url,
remote_service=wcs_site,
user=getattr(context.get('request'), 'user', None),
cache_duration=5,
raise_if_not_cached=not(context.get('synchronous')),
log_errors=False)
if response.status_code == 200:
extra_context['card'] = response.json()
return extra_context
@register_cell_class
class TrackingCodeInputCell(CellBase):
is_enabled = classmethod(is_wcs_enabled)

View File

@ -0,0 +1,31 @@
{% load assets i18n %}
{% block cell-content %}
<div class="card">
{% block cell-header %}
<h2>{{ title }}{% if card %} - {{ card.text }}{% endif %}</h2>
{% include "combo/asset_picture_fragment.html" %}
{% endblock %}
{% if card %}
{% for field in schema.fields %}
{% with card.fields|get:field.varname as value %}
<p>
<span class="label">{{ field.label }}</span>
{% if field.type == "date" %}
<span class="value">{{ value|date }}</span>
{% elif field.type == "bool" and value is not None %}
<span class="value">{{ value|yesno }}</span>
{% else %}
<span class="value">{{ value|default:"" }}</span>
{% endif %}
</p>
{% endwith %}
{% endfor %}
{% else %}
<p>{% trans 'Unknown Card' %}</p>
{% endif %}
</div>
{% endblock %}

View File

@ -815,7 +815,7 @@ def test_index_site_num_queries(settings, app):
assert IndexedCell.objects.count() == 50
with CaptureQueriesContext(connection) as ctx:
index_site()
assert len(ctx.captured_queries) == 222
assert len(ctx.captured_queries) == 223
SearchCell.objects.create(
page=page, placeholder='content', order=0,

View File

@ -24,7 +24,7 @@ from combo.apps.wcs.models import (
WcsFormCell, WcsCurrentFormsCell,
WcsFormsOfCategoryCell, WcsCurrentDraftsCell, WcsCategoryCell,
TrackingCodeInputCell, BackofficeSubmissionCell, WcsCareFormsCell,
WcsCardsCell)
WcsCardsCell, WcsCardInfosCell)
from combo.apps.search.models import SearchCell
from combo.apps.search.utils import index_site, search_site
@ -115,6 +115,33 @@ WCS_CARDS_DATA = [
},
]
WCS_CARDDEF_SCHEMA = {
'name': 'Card Model 1',
'fields': [
{'label': 'Field A', 'varname': 'fielda', 'type': 'string'},
{'label': 'Field B', 'varname': 'fieldb', 'type': 'bool'},
{'label': 'Field C', 'varname': 'fieldc', 'type': 'date'},
{'label': 'Related', 'varname': 'related', 'type': 'item'},
]
}
WCS_CARD_DATA = {
'id': 11,
'display_id': '10-11',
'display_name': 'Card Model 1 - n°10-11',
'digest': 'a a a',
'text': 'aa',
'url': '/backoffice/data/card_model_1/11/',
'fields': {
'fielda': 'a',
'fieldb': True,
'fieldc': '2020-09-28',
'related': 'Foo Bar',
'related_raw': 42,
'related_structured': {'id': 42, 'text': 'blah'},
}
}
class MockUser(object):
email = 'foo@example.net'
@ -154,7 +181,11 @@ def get_data_from_url(url):
return WCS_USER_DRAFTS_DATA
if '/api/cards/@list' in url:
return WCS_CARDDEFS_DATA
if 'api/cards/' in url:
if '/api/cards/card_model_1/@schema' in url:
return WCS_CARDDEF_SCHEMA
if 'api/cards/card_model_1/11/' in url:
return WCS_CARD_DATA
if 'api/cards/card_model_1/list' in url:
return WCS_CARDS_DATA
return []
@ -162,6 +193,10 @@ def get_data_from_url(url):
def mocked_requests_send(request, **kwargs):
request_url = urlparse.urlparse(request.url)
data = copy.deepcopy(get_data_from_url(request_url.path))
if not isinstance(data, list):
return MockedRequestResponse(content=json.dumps(data))
for elem in data:
for key in ['url', 'form_url_backoffice']:
if key not in elem:
@ -897,9 +932,10 @@ def test_manager_current_forms(app, admin_user):
settings.KNOWN_SERVICES = temp_settings
@pytest.mark.parametrize('cell_class', [WcsCardsCell, WcsCardInfosCell])
@mock.patch('combo.apps.wcs.utils.requests.send', side_effect=mocked_requests_send)
def test_cards_cell_setup(mock_send):
cell = WcsCardsCell()
def test_card_cells_setup(mock_send, cell_class):
cell = cell_class()
form_class = cell.get_default_form_class()
form = form_class()
assert form.fields['carddef_reference'].widget.choices == [
@ -1009,6 +1045,106 @@ def test_cards_cell_render(mock_send, context):
assert '<a href="/foo/13"><span class="card-title">cc</span></a>' in result
@mock.patch('combo.apps.wcs.utils.requests.send', side_effect=mocked_requests_send)
def test_card_cell_save_cache(mock_send):
page = Page.objects.create(title='xxx', slug='test_cards_cell_save_cache', template_name='standard')
cell = WcsCardInfosCell(page=page, placeholder='content', order=0)
assert cell.get_additional_label() is None
cell.carddef_reference = 'default:card_model_1'
cell.save()
assert cell.cached_title == 'Card Model 1'
assert cell.cached_json != {}
assert cell.get_additional_label() == 'Card Model 1'
# make sure cached attributes are removed from serialized pages
assert 'cached_' not in json.dumps(page.get_serialized_page())
# artificially change title and json
WcsCardInfosCell.objects.filter(pk=cell.pk).update(cached_title='XXX', cached_json={})
assert WcsCardInfosCell.objects.get(pk=cell.pk).cached_title == 'XXX'
assert WcsCardInfosCell.objects.get(pk=cell.pk).cached_json == {}
# run update db cache
appconfig = apps.get_app_config('wcs')
appconfig.update_db_cache()
assert WcsCardInfosCell.objects.get(pk=cell.pk).cached_title == 'Card Model 1'
assert WcsCardInfosCell.objects.get(pk=cell.pk).cached_json != {}
@mock.patch('combo.apps.wcs.utils.requests.send', side_effect=mocked_requests_send)
def test_card_cell_validity(mock_send):
page = Page.objects.create(title='xxx', template_name='standard')
cell = WcsCardInfosCell.objects.create(page=page, placeholder='content', order=0)
validity_info = ValidityInfo.objects.latest('pk')
assert validity_info.invalid_reason_code == 'wcs_card_not_defined'
assert validity_info.invalid_since is not None
cell.carddef_reference = 'default:card_model_1'
cell.save()
assert ValidityInfo.objects.exists() is False
# can not retrieve carddefs, don't set cell as invalid
with mock.patch('combo.apps.wcs.models.requests.get') as requests_get:
mock_json = mock.Mock(status_code=404)
requests_get.return_value = mock_json
cell.save()
assert ValidityInfo.objects.exists() is False
mock_send.side_effect = lambda *a, **k: MockedRequestResponse(content=json.dumps({'err': 1, 'err_class': 'Page not found'}))
cell.carddef_reference = 'default:foobar'
cell.save()
validity_info = ValidityInfo.objects.latest('pk')
assert validity_info.invalid_reason_code == 'wcs_card_not_found'
assert validity_info.invalid_since is not None
@mock.patch('combo.apps.wcs.utils.requests.send', side_effect=mocked_requests_send)
def test_card_cell_load(mock_send):
page = Page.objects.create(title='xxx', slug='test_cards', template_name='standard')
cell = WcsCardInfosCell(page=page, placeholder='content', order=0)
cell.carddef_reference = 'default:card_model_1'
cell.save()
site_export = [page.get_serialized_page()]
cell.delete()
assert not Page.objects.get(pk=page.pk).get_cells()
Page.load_serialized_pages(site_export)
page = Page.objects.get(slug='test_cards')
cells = page.get_cells()
assert len(cells) == 1
cell = cells[0]
assert cell.cached_title == 'Card Model 1'
@mock.patch('combo.apps.wcs.utils.requests.send', side_effect=mocked_requests_send)
def test_card_cell_render(mock_send, context):
page = Page.objects.create(title='xxx', template_name='standard')
cell = WcsCardInfosCell(page=page, placeholder='content', order=0)
cell.carddef_reference = u'default:card_model_1'
cell.save()
# card id not in context
assert 'card_model_1_id' not in context
result = cell.render(context)
assert '<h2>Card Model 1</h2>' in result
assert '<p>Unknown Card</p>' in result
context['card_model_1_id'] = 11
# query should fail as nothing is cached
cache.clear()
with pytest.raises(NothingInCacheException):
result = cell.render(context)
context['synchronous'] = True # to get fresh content
result = cell.render(context)
assert '<h2>Card Model 1 - aa</h2>' in result
assert '<span class="label">Field A</span>\n \n <span class="value">a</span>' in result
assert '<span class="label">Field B</span>\n \n <span class="value">yes</span>' in result
assert '<span class="label">Field C</span>\n \n <span class="value">Sept. 28, 2020</span>' in result
assert '<span class="label">Related</span>\n \n <span class="value">Foo Bar</span>' in result
assert 'related_raw' not in result
assert 'related_structured' not in result
def test_tracking_code_cell(app, nocache):
page = Page(title='One', slug='index', template_name='standard')
page.save()