From a1727f551f5596cf96a09a1656f39751e293fd08 Mon Sep 17 00:00:00 2001 From: Jonathan Sundqvist Date: Sat, 17 Oct 2020 22:26:06 +0200 Subject: [PATCH] Add the possibility of updating html through morphs --- cypress/integration/websocket_spec.js | 8 ++++ sockpuppet/consumer.py | 28 +++++++++-- sockpuppet/reflex.py | 60 ++++++++++++++++++++---- tests/example/reflexes/example_reflex.py | 5 ++ tests/example/templates/example.html | 5 ++ 5 files changed, 94 insertions(+), 12 deletions(-) diff --git a/cypress/integration/websocket_spec.js b/cypress/integration/websocket_spec.js index 95ccfdb..54aaf42 100644 --- a/cypress/integration/websocket_spec.js +++ b/cypress/integration/websocket_spec.js @@ -47,4 +47,12 @@ describe("Integration tests", () => { cy.get('#decrementor').click() cy.get('#decrementor-counter').should('have.text', '-1') }) + + it("can send a morph in a reflex", () => { + cy.visit('/test') + cy.wait(200) + cy.get('#morph-button').click() + + cy.get('#morph').should('have.text', 'I got morphed!') + }) }) diff --git a/sockpuppet/consumer.py b/sockpuppet/consumer.py index a47b252..e3ad747 100644 --- a/sockpuppet/consumer.py +++ b/sockpuppet/consumer.py @@ -148,14 +148,30 @@ def reflex_message(self, data, **kwargs): url = data['url'] selectors = data['selectors'] if data['selectors'] else ['body'] target = data['target'] + identifier = data['identifier'] reflex_name, method_name = target.split('#') reflex_name = classify(reflex_name) arguments = data['args'] if data.get('args') else [] params = dict(parse_qsl(data['formData'])) element = Element(data['attrs']) + + # TODO can be removed once stimulus-reflex has increased a couple of versions + permanent_attribute_name = data.get('permanent_attribute_name') + if not permanent_attribute_name: + # Used in stimulus-reflex >= 3.4 + permanent_attribute_name = data['permanentAttributeName'] + try: ReflexClass = self.reflexes.get(reflex_name) - reflex = ReflexClass(self, url=url, element=element, selectors=selectors, params=params) + reflex = ReflexClass( + self, url=url, + element=element, + selectors=selectors, + identifier=identifier, + params=params, + reflex_id=data['reflexId'], + permanent_attribute_name=permanent_attribute_name + ) self.delegate_call_to_reflex(reflex, method_name, arguments) except TypeError: if not self.reflexes.get(reflex_name): @@ -223,6 +239,10 @@ def receive_json(self, data, **kwargs): print('Unsupported') def render_page_and_broadcast_morph(self, reflex, selectors, data): + if reflex.is_morph: + # The reflex has already sent a message so consumer doesn't need to. + return + html = self.render_page(reflex) if html: self.broadcast_morphs(selectors, data, html, reflex) @@ -252,7 +272,7 @@ def render_page(self, reflex): def broadcast_morphs(self, selectors, data, html, reflex): document, selectors = get_document_and_selectors(html, selectors) - channel = Channel(reflex.get_channel_id(), identifier=data['identifier']) + broadcaster = Channel(reflex.get_channel_id(), identifier=data['identifier']) logger.debug('Broadcasting to %s', reflex.get_channel_id()) # TODO can be removed once stimulus-reflex has increased a couple of versions @@ -264,14 +284,14 @@ def broadcast_morphs(self, selectors, data, html, reflex): for selector in selectors: # cssselect has an attribute css plain_selector = getattr(selector, 'css', selector) - channel.morph({ + broadcaster.morph({ 'selector': plain_selector, 'html': parse_out_html(document, selector), 'children_only': True, 'permanent_attribute_name': permanent_attribute_name, 'stimulus_reflex': {**data} }) - channel.broadcast() + broadcaster.broadcast() def delegate_call_to_reflex(self, reflex, method_name, arguments): method = getattr(reflex, method_name) diff --git a/sockpuppet/reflex.py b/sockpuppet/reflex.py index 339326a..6e631a9 100644 --- a/sockpuppet/reflex.py +++ b/sockpuppet/reflex.py @@ -1,8 +1,13 @@ +from django.template.loader import render_to_string +from django.template.backends.django import Template from django.test import RequestFactory +from .channel import Channel + PROTECTED_VARIABLES = [ 'consumer', 'element', + 'is_morph', 'selectors', 'session', 'url', @@ -10,20 +15,20 @@ class Reflex: - def __init__(self, consumer, url, element, selectors, params): + def __init__( + self, consumer, url, element, selectors, params, identifier='', + permanent_attribute_name=None, reflex_id=None + ): self.consumer = consumer self.url = url self.element = element self.selectors = selectors self.session = consumer.scope['session'] self.params = params - - def get_channel_id(self): - ''' - Override this to make the reflex send to a different channel - other than the session_key of the user - ''' - return self.session.session_key + self.identifier = identifier + self.is_morph = False + self.reflex_id = reflex_id + self.permanent_attribute_name = permanent_attribute_name @property def request(self): @@ -37,3 +42,42 @@ def request(self): def reload(self): """A default reflex to force a refresh""" pass + + def get_channel_id(self): + ''' + Override this to make the reflex send to a different channel + other than the session_key of the user + ''' + return self.session.session_key + + def morph(self, selector='', html='', template='', context={}): + """ + If a morph is executed without any arguments, nothing is executed + and the reflex won't send over any data to the frontend. + """ + self.is_morph = True + no_arguments = [not selector, not html, (not template and not context)] + if all(no_arguments) and not selector: + # an empty morph, nothing is sent ever. + return + + if html: + html = html + elif isinstance(template, Template): + html = template.render(context) + else: + html = render_to_string(template, context) + + broadcaster = Channel(self.get_channel_id(), identifier=self.identifier) + broadcaster.morph({ + 'selector': selector, + 'html': html, + 'children_only': True, + 'permanent_attribute_name': self.permanent_attribute_name, + 'stimulus_reflex': { + 'morph': 'selector', + 'reflexId': self.reflex_id, + 'url': self.url + } + }) + broadcaster.broadcast() diff --git a/tests/example/reflexes/example_reflex.py b/tests/example/reflexes/example_reflex.py index 8722416..ddb9f44 100644 --- a/tests/example/reflexes/example_reflex.py +++ b/tests/example/reflexes/example_reflex.py @@ -19,3 +19,8 @@ def change_word(self): class FormReflex(Reflex): def submit(self): self.text_output = self.request.POST['text-input'] + + +class MorphReflex(Reflex): + def morph_me(self): + self.morph('#morph', 'I got morphed!') diff --git a/tests/example/templates/example.html b/tests/example/templates/example.html index b4193b9..d41f319 100644 --- a/tests/example/templates/example.html +++ b/tests/example/templates/example.html @@ -35,4 +35,9 @@ {{ text_output }} + + + + +