0001-wcs-add-cell-to-display-card-details-46767.patch
combo/apps/wcs/__init__.py | ||
---|---|---|
111 | 111 | |
112 | 112 |
def update_db_cache(self): |
113 | 113 |
from combo.data.models import CellBase |
114 |
from .models import WcsFormCell, WcsCategoryCell, WcsFormsOfCategoryCell, WcsCardsCell |
|
115 |
models_to_update = [WcsFormCell, WcsCategoryCell, WcsFormsOfCategoryCell, WcsCardsCell] |
|
114 |
from .models import WcsFormCell, WcsCategoryCell, WcsFormsOfCategoryCell, WcsCardsCell, WcsCardInfosCell
|
|
115 |
models_to_update = [WcsFormCell, WcsCategoryCell, WcsFormsOfCategoryCell, WcsCardsCell, WcsCardInfosCell]
|
|
116 | 116 |
for cell in CellBase.get_cells(cell_filter=lambda x: x in models_to_update): |
117 | 117 |
cell.save() |
118 | 118 |
combo/apps/wcs/migrations/0021_card.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
from __future__ import unicode_literals |
|
3 | ||
4 |
import combo.apps.wcs.models |
|
5 |
from django.db import migrations, models |
|
6 |
import django.db.models.deletion |
|
7 |
import jsonfield.fields |
|
8 | ||
9 | ||
10 |
class Migration(migrations.Migration): |
|
11 | ||
12 |
dependencies = [ |
|
13 |
('auth', '0008_alter_user_username_max_length'), |
|
14 |
('data', '0045_link_list_limit'), |
|
15 |
('wcs', '0020_cards'), |
|
16 |
] |
|
17 | ||
18 |
operations = [ |
|
19 |
migrations.CreateModel( |
|
20 |
name='WcsCardInfosCell', |
|
21 |
fields=[ |
|
22 |
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
|
23 |
('placeholder', models.CharField(max_length=20)), |
|
24 |
('order', models.PositiveIntegerField()), |
|
25 |
('slug', models.SlugField(blank=True, verbose_name='Slug')), |
|
26 |
('extra_css_class', models.CharField(blank=True, max_length=100, verbose_name='Extra classes for CSS styling')), |
|
27 |
('public', models.BooleanField(default=True, verbose_name='Public')), |
|
28 |
('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')), |
|
29 |
('last_update_timestamp', models.DateTimeField(auto_now=True)), |
|
30 |
('carddef_reference', models.CharField(max_length=150, verbose_name='Card Model')), |
|
31 |
('cached_title', models.CharField(max_length=150, verbose_name='Title')), |
|
32 |
('cached_json', jsonfield.fields.JSONField(blank=True, default=dict)), |
|
33 |
('groups', models.ManyToManyField(blank=True, to='auth.Group', verbose_name='Groups')), |
|
34 |
('page', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='data.Page')), |
|
35 |
], |
|
36 |
options={ |
|
37 |
'verbose_name': 'Card Information Cell', |
|
38 |
}, |
|
39 |
bases=(combo.apps.wcs.models.CardMixin, models.Model), |
|
40 |
), |
|
41 |
] |
combo/apps/wcs/models.py | ||
---|---|---|
710 | 710 |
verbose_name = _('Form Categories') |
711 | 711 | |
712 | 712 | |
713 |
class CardMixin(object): |
|
714 |
invalid_reason_codes = invalid_reason_codes |
|
715 | ||
716 |
def populate_cache(self, store_json=False): |
|
717 |
if self.carddef_reference: |
|
718 |
wcs_key, card_slug = self.carddef_reference.split(':') |
|
719 |
wcs_site = get_wcs_services().get(wcs_key) |
|
720 |
card_models = get_wcs_json(wcs_site, 'api/cards/@list') |
|
721 | ||
722 |
if not card_models: |
|
723 |
# can not retrieve data, don't report cell as invalid |
|
724 |
self.mark_as_valid() |
|
725 |
return |
|
726 | ||
727 |
card_found = False |
|
728 |
for card in card_models.get('data', []): |
|
729 |
slug = card['slug'] |
|
730 |
if slug == card_slug: |
|
731 |
self.cached_title = card['title'] |
|
732 |
if store_json: |
|
733 |
self.cached_json = card |
|
734 |
self.save(update_fields=['cached_title', 'cached_json']) |
|
735 |
else: |
|
736 |
self.save(update_fields=['cached_title']) |
|
737 |
card_found = True |
|
738 |
break |
|
739 |
if card_found: |
|
740 |
self.mark_as_valid() |
|
741 |
return |
|
742 |
else: |
|
743 |
return self.mark_as_invalid('wcs_card_not_found') |
|
744 |
else: |
|
745 |
return self.mark_as_invalid('wcs_card_not_defined') |
|
746 | ||
747 |
def is_relevant(self, context): |
|
748 |
return bool(self.carddef_reference) |
|
749 | ||
750 |
def get_default_form_class(self): |
|
751 |
from .forms import WcsCardsCellForm |
|
752 |
return WcsCardsCellForm |
|
753 | ||
754 |
@property |
|
755 |
def wcs_site(self): |
|
756 |
return self.carddef_reference.split(':')[0] |
|
757 | ||
758 |
def get_additional_label(self): |
|
759 |
if not self.cached_title: |
|
760 |
return |
|
761 |
return self.cached_title |
|
762 | ||
763 | ||
713 | 764 |
@register_cell_class |
714 |
class WcsCardsCell(CellBase, WcsBlurpMixin):
|
|
765 |
class WcsCardsCell(CardMixin, WcsBlurpMixin, CellBase):
|
|
715 | 766 |
carddef_reference = models.CharField(_('Card Model'), max_length=150) |
716 | 767 |
cached_title = models.CharField(_('Title'), max_length=150) |
717 | 768 | |
718 | 769 |
template_name = 'combo/wcs/cards.html' |
719 | 770 |
variable_name = 'cards' |
720 |
invalid_reason_codes = invalid_reason_codes |
|
721 | 771 | |
722 | 772 |
class Meta: |
723 | 773 |
verbose_name = _('Cards') |
724 | 774 | |
725 | 775 |
def save(self, *args, **kwargs): |
726 |
if 'update_fields' in kwargs: |
|
727 |
# don't populate the cache |
|
728 |
return super().save(*args, **kwargs) |
|
729 | ||
730 |
def populate_cache(): |
|
731 |
if self.carddef_reference: |
|
732 |
wcs_key, card_slug = self.carddef_reference.split(':') |
|
733 |
wcs_site = get_wcs_services().get(wcs_key) |
|
734 |
card_models = get_wcs_json(wcs_site, 'api/cards/@list') |
|
735 | ||
736 |
if not card_models: |
|
737 |
# can not retrieve data, don't report cell as invalid |
|
738 |
self.mark_as_valid() |
|
739 |
return |
|
740 | ||
741 |
card_found = False |
|
742 |
for card in card_models.get('data', []): |
|
743 |
slug = card['slug'] |
|
744 |
if slug == card_slug: |
|
745 |
self.cached_title = card['title'] |
|
746 |
self.save(update_fields=['cached_title']) |
|
747 |
card_found = True |
|
748 |
break |
|
749 |
if card_found: |
|
750 |
self.mark_as_valid() |
|
751 |
return |
|
752 |
else: |
|
753 |
return self.mark_as_invalid('wcs_card_not_found') |
|
754 |
else: |
|
755 |
return self.mark_as_invalid('wcs_card_not_defined') |
|
756 | ||
757 | 776 |
super().save(*args, **kwargs) |
758 |
populate_cache() |
|
759 | 777 | |
760 |
def is_relevant(self, context): |
|
761 |
return bool(self.carddef_reference) |
|
778 |
if 'update_fields' in kwargs: |
|
779 |
# don't populate the cache |
|
780 |
return |
|
762 | 781 | |
763 |
@property |
|
764 |
def wcs_site(self): |
|
765 |
return self.carddef_reference.split(':')[0] |
|
782 |
self.populate_cache() |
|
766 | 783 | |
767 | 784 |
def get_api_url(self, context): |
768 | 785 |
return 'api/cards/%s/list' % self.carddef_reference.split(':')[1] |
769 | 786 | |
770 |
def get_default_form_class(self): |
|
771 |
from .forms import WcsCardsCellForm |
|
772 |
return WcsCardsCellForm |
|
773 | ||
774 | 787 |
def get_cell_extra_context(self, context): |
775 | 788 |
extra_context = super().get_cell_extra_context(context) |
776 | 789 |
extra_context.update(WcsBlurpMixin.get_cell_extra_context(self, context)) |
... | ... | |
790 | 803 | |
791 | 804 |
return extra_context |
792 | 805 | |
793 |
def get_additional_label(self): |
|
794 |
if not self.cached_title: |
|
795 |
return |
|
796 |
return self.cached_title |
|
797 | ||
798 | 806 |
def render_for_search(self): |
799 | 807 |
return '' |
800 | 808 | |
801 | 809 | |
810 |
@register_cell_class |
|
811 |
class WcsCardInfosCell(CardMixin, CellBase): |
|
812 |
carddef_reference = models.CharField(_('Card Model'), max_length=150) |
|
813 | ||
814 |
cached_title = models.CharField(_('Title'), max_length=150) |
|
815 |
cached_json = JSONField(blank=True) |
|
816 | ||
817 |
is_enabled = classmethod(is_wcs_enabled) |
|
818 | ||
819 |
template_name = 'combo/wcs/card.html' |
|
820 | ||
821 |
class Meta: |
|
822 |
verbose_name = _('Card Information Cell') |
|
823 | ||
824 |
def save(self, *args, **kwargs): |
|
825 |
super().save(*args, **kwargs) |
|
826 | ||
827 |
if 'update_fields' in kwargs: |
|
828 |
# don't populate the cache |
|
829 |
return |
|
830 | ||
831 |
self.populate_cache(store_json=True) |
|
832 | ||
833 |
def get_cell_extra_context(self, context): |
|
834 |
extra_context = super().get_cell_extra_context(context) |
|
835 |
extra_context['title'] = self.cached_title |
|
836 | ||
837 |
card_id = '%s_id' % self.carddef_reference.split(':')[1] |
|
838 |
if not context.get(card_id): |
|
839 |
return extra_context |
|
840 |
api_url = 'api/cards/%s/%s/' % (self.carddef_reference.split(':')[1], context.get(card_id)) |
|
841 | ||
842 |
wcs_site = get_wcs_services().get(self.wcs_site) |
|
843 | ||
844 |
response = requests.get( |
|
845 |
api_url, |
|
846 |
remote_service=wcs_site, |
|
847 |
user=getattr(context.get('request'), 'user', None), |
|
848 |
cache_duration=5, |
|
849 |
raise_if_not_cached=not(context.get('synchronous')), |
|
850 |
log_errors=False) |
|
851 |
if response.status_code == 200: |
|
852 |
extra_context['card'] = response.json() |
|
853 | ||
854 |
return extra_context |
|
855 | ||
856 | ||
802 | 857 |
@register_cell_class |
803 | 858 |
class TrackingCodeInputCell(CellBase): |
804 | 859 |
is_enabled = classmethod(is_wcs_enabled) |
combo/apps/wcs/templates/combo/wcs/card.html | ||
---|---|---|
1 |
{% load assets i18n %} |
|
2 | ||
3 |
{% block cell-content %} |
|
4 | ||
5 |
<div class="card"> |
|
6 |
{% block cell-header %} |
|
7 |
<h2>{{ title }}{% if card %} - {{ card.text }}{% endif %}</h2> |
|
8 |
{% include "combo/asset_picture_fragment.html" %} |
|
9 |
{% endblock %} |
|
10 | ||
11 |
{% if card %} |
|
12 |
{% for key, value in card.fields.items|dictsort:0 %} |
|
13 |
{% if not key|endswith:'_raw' and not key|endswith:'_structured' %} |
|
14 |
<p><span class="label">{{ key }}</span> <span class="value">{{ value|default:"" }}</span></p> |
|
15 |
{% endif %} |
|
16 |
{% endfor %} |
|
17 |
{% else %} |
|
18 |
<p>{% trans 'Unknown Card' %}</p> |
|
19 |
{% endif %} |
|
20 |
</div> |
|
21 | ||
22 |
{% endblock %} |
tests/test_search.py | ||
---|---|---|
815 | 815 |
assert IndexedCell.objects.count() == 50 |
816 | 816 |
with CaptureQueriesContext(connection) as ctx: |
817 | 817 |
index_site() |
818 |
assert len(ctx.captured_queries) == 222
|
|
818 |
assert len(ctx.captured_queries) == 223
|
|
819 | 819 | |
820 | 820 |
SearchCell.objects.create( |
821 | 821 |
page=page, placeholder='content', order=0, |
tests/test_wcs.py | ||
---|---|---|
24 | 24 |
WcsFormCell, WcsCurrentFormsCell, |
25 | 25 |
WcsFormsOfCategoryCell, WcsCurrentDraftsCell, WcsCategoryCell, |
26 | 26 |
TrackingCodeInputCell, BackofficeSubmissionCell, WcsCareFormsCell, |
27 |
WcsCardsCell) |
|
27 |
WcsCardsCell, WcsCardInfosCell)
|
|
28 | 28 | |
29 | 29 |
from combo.apps.search.models import SearchCell |
30 | 30 |
from combo.apps.search.utils import index_site, search_site |
... | ... | |
115 | 115 |
}, |
116 | 116 |
] |
117 | 117 | |
118 |
WCS_CARD_DATA = { |
|
119 |
'id': 11, |
|
120 |
'display_id': '10-11', |
|
121 |
'display_name': 'Card Model 1 - n°10-11', |
|
122 |
'digest': 'a a a', |
|
123 |
'text': 'aa', |
|
124 |
'url': '/backoffice/data/card_model_1/11/', |
|
125 |
'fields': { |
|
126 |
'fielda': 'a', |
|
127 |
'fieldb': 'b', |
|
128 |
'related': 'Foo Bar', |
|
129 |
'related_raw': 42, |
|
130 |
'related_structured': {'id': 42, 'text': 'blah'}, |
|
131 |
} |
|
132 |
} |
|
133 | ||
118 | 134 | |
119 | 135 |
class MockUser(object): |
120 | 136 |
email = 'foo@example.net' |
... | ... | |
154 | 170 |
return WCS_USER_DRAFTS_DATA |
155 | 171 |
if '/api/cards/@list' in url: |
156 | 172 |
return WCS_CARDDEFS_DATA |
157 |
if 'api/cards/' in url: |
|
173 |
if 'api/cards/card_model_1/11/' in url: |
|
174 |
return WCS_CARD_DATA |
|
175 |
if 'api/cards/card_model_1/list' in url: |
|
158 | 176 |
return WCS_CARDS_DATA |
159 | 177 |
return [] |
160 | 178 | |
... | ... | |
162 | 180 |
def mocked_requests_send(request, **kwargs): |
163 | 181 |
request_url = urlparse.urlparse(request.url) |
164 | 182 |
data = copy.deepcopy(get_data_from_url(request_url.path)) |
183 | ||
184 |
if not isinstance(data, list): |
|
185 |
return MockedRequestResponse(content=json.dumps(data)) |
|
186 | ||
165 | 187 |
for elem in data: |
166 | 188 |
for key in ['url', 'form_url_backoffice']: |
167 | 189 |
if key not in elem: |
... | ... | |
897 | 919 |
settings.KNOWN_SERVICES = temp_settings |
898 | 920 | |
899 | 921 | |
922 |
@pytest.mark.parametrize('cell_class', [WcsCardsCell, WcsCardInfosCell]) |
|
900 | 923 |
@mock.patch('combo.apps.wcs.utils.requests.send', side_effect=mocked_requests_send) |
901 |
def test_cards_cell_setup(mock_send):
|
|
902 |
cell = WcsCardsCell()
|
|
924 |
def test_card_cells_setup(mock_send, cell_class):
|
|
925 |
cell = cell_class()
|
|
903 | 926 |
form_class = cell.get_default_form_class() |
904 | 927 |
form = form_class() |
905 | 928 |
assert form.fields['carddef_reference'].widget.choices == [ |
... | ... | |
936 | 959 |
assert WcsCardsCell.objects.get(pk=cell.pk).cached_title == 'Card Model 1' |
937 | 960 | |
938 | 961 | |
962 |
@pytest.mark.parametrize('cell_class', [WcsCardsCell, WcsCardInfosCell]) |
|
939 | 963 |
@mock.patch('combo.apps.wcs.utils.requests.send', side_effect=mocked_requests_send) |
940 |
def test_cards_cell_validity(mock_send):
|
|
964 |
def test_card_cells_validity(mock_send, cell_class):
|
|
941 | 965 |
page = Page.objects.create(title='xxx', template_name='standard') |
942 |
cell = WcsCardsCell.objects.create(page=page, placeholder='content', order=0)
|
|
966 |
cell = cell_class.objects.create(page=page, placeholder='content', order=0)
|
|
943 | 967 |
validity_info = ValidityInfo.objects.latest('pk') |
944 | 968 |
assert validity_info.invalid_reason_code == 'wcs_card_not_defined' |
945 | 969 |
assert validity_info.invalid_since is not None |
... | ... | |
1009 | 1033 |
assert '<a href="/foo/13"><span class="card-title">cc</span></a>' in result |
1010 | 1034 | |
1011 | 1035 | |
1036 |
@mock.patch('combo.apps.wcs.utils.requests.send', side_effect=mocked_requests_send) |
|
1037 |
def test_card_cell_save_cache(mock_send): |
|
1038 |
page = Page.objects.create(title='xxx', slug='test_cards_cell_save_cache', template_name='standard') |
|
1039 |
cell = WcsCardInfosCell(page=page, placeholder='content', order=0) |
|
1040 |
assert cell.get_additional_label() is None |
|
1041 |
cell.carddef_reference = 'default:card_model_1' |
|
1042 |
cell.save() |
|
1043 |
assert cell.cached_title == 'Card Model 1' |
|
1044 |
assert cell.cached_json == {'slug': 'card_model_1', 'title': 'Card Model 1'} |
|
1045 |
assert cell.get_additional_label() == 'Card Model 1' |
|
1046 |
# make sure cached attributes are removed from serialized pages |
|
1047 |
assert 'cached_' not in json.dumps(page.get_serialized_page()) |
|
1048 | ||
1049 |
# artificially change title and json |
|
1050 |
WcsCardInfosCell.objects.filter(pk=cell.pk).update(cached_title='XXX', cached_json={}) |
|
1051 |
assert WcsCardInfosCell.objects.get(pk=cell.pk).cached_title == 'XXX' |
|
1052 |
# run update db cache |
|
1053 |
appconfig = apps.get_app_config('wcs') |
|
1054 |
appconfig.update_db_cache() |
|
1055 |
assert WcsCardInfosCell.objects.get(pk=cell.pk).cached_title == 'Card Model 1' |
|
1056 |
assert WcsCardInfosCell.objects.get(pk=cell.pk).cached_json == {'slug': 'card_model_1', 'title': 'Card Model 1'} |
|
1057 | ||
1058 | ||
1059 |
@mock.patch('combo.apps.wcs.utils.requests.send', side_effect=mocked_requests_send) |
|
1060 |
def test_card_cell_load(mock_send): |
|
1061 |
page = Page.objects.create(title='xxx', slug='test_cards', template_name='standard') |
|
1062 |
cell = WcsCardInfosCell(page=page, placeholder='content', order=0) |
|
1063 |
cell.carddef_reference = 'default:card_model_1' |
|
1064 |
cell.save() |
|
1065 |
site_export = [page.get_serialized_page()] |
|
1066 |
cell.delete() |
|
1067 |
assert not Page.objects.get(pk=page.pk).get_cells() |
|
1068 |
Page.load_serialized_pages(site_export) |
|
1069 |
page = Page.objects.get(slug='test_cards') |
|
1070 |
cells = page.get_cells() |
|
1071 |
assert len(cells) == 1 |
|
1072 |
cell = cells[0] |
|
1073 |
assert cell.cached_title == 'Card Model 1' |
|
1074 |
assert cell.cached_json == {'slug': 'card_model_1', 'title': 'Card Model 1'} |
|
1075 | ||
1076 | ||
1077 |
@mock.patch('combo.apps.wcs.utils.requests.send', side_effect=mocked_requests_send) |
|
1078 |
def test_card_cell_render(mock_send, context): |
|
1079 |
page = Page.objects.create(title='xxx', template_name='standard') |
|
1080 |
cell = WcsCardInfosCell(page=page, placeholder='content', order=0) |
|
1081 |
cell.carddef_reference = u'default:card_model_1' |
|
1082 |
cell.save() |
|
1083 | ||
1084 |
# card id not in context |
|
1085 |
assert 'card_model_1_id' not in context |
|
1086 |
result = cell.render(context) |
|
1087 |
assert '<h2>Card Model 1</h2>' in result |
|
1088 |
assert '<p>Unknown Card</p>' in result |
|
1089 | ||
1090 |
context['card_model_1_id'] = 11 |
|
1091 | ||
1092 |
# query should fail as nothing is cached |
|
1093 |
cache.clear() |
|
1094 |
with pytest.raises(NothingInCacheException): |
|
1095 |
result = cell.render(context) |
|
1096 | ||
1097 |
context['synchronous'] = True # to get fresh content |
|
1098 | ||
1099 |
result = cell.render(context) |
|
1100 |
assert '<h2>Card Model 1 - aa</h2>' in result |
|
1101 |
assert '<span class="label">fielda</span> <span class="value">a</span>' in result |
|
1102 |
assert '<span class="label">fieldb</span> <span class="value">b</span>' in result |
|
1103 |
assert '<span class="label">related</span> <span class="value">Foo Bar</span>' in result |
|
1104 |
assert 'related_raw' not in result |
|
1105 |
assert 'related_structured' not in result |
|
1106 | ||
1107 | ||
1012 | 1108 |
def test_tracking_code_cell(app, nocache): |
1013 | 1109 |
page = Page(title='One', slug='index', template_name='standard') |
1014 | 1110 |
page.save() |
1015 |
- |