diff --git a/.gitignore b/.gitignore index 9fa0ad93..49400a8f 100644 --- a/.gitignore +++ b/.gitignore @@ -109,6 +109,9 @@ venv.bak/ pip-wheel-metadata TODO.md +# pycharm +.idea/ + node_modules/ tags staticfiles/ \ No newline at end of file diff --git a/django_unicorn/components/unicorn_template_response.py b/django_unicorn/components/unicorn_template_response.py index d9b2a0a1..1e62057e 100644 --- a/django_unicorn/components/unicorn_template_response.py +++ b/django_unicorn/components/unicorn_template_response.py @@ -152,12 +152,16 @@ def render(self): self.component._init_script = init_script self.component._json_tag = json_tag else: - json_tags = [] - json_tags.append(json_tag) - - for child in self.component.children: - init_script = f"{init_script} {child._init_script}" - json_tags.append(child._json_tag) + json_tags = [json_tag] + + descendants = [] + descendants.append(self.component) + while descendants: + descendant = descendants.pop() + for child in descendant.children: + init_script = f"{init_script} {child._init_script}" + json_tags.append(child._json_tag) + descendants.append(child) script_tag = soup.new_tag("script") script_tag["type"] = "module" diff --git a/django_unicorn/components/unicorn_view.py b/django_unicorn/components/unicorn_view.py index 63ee6dd8..4d6cdcc2 100644 --- a/django_unicorn/components/unicorn_view.py +++ b/django_unicorn/components/unicorn_view.py @@ -28,7 +28,7 @@ UnicornCacheError, ) from ..settings import get_setting -from ..utils import get_cacheable_component, is_non_string_sequence +from ..utils import CacheableComponent, is_non_string_sequence from .fields import UnicornField from .unicorn_template_response import UnicornTemplateResponse @@ -166,26 +166,35 @@ def construct_component( class UnicornView(TemplateView): - response_class = UnicornTemplateResponse + # These class variables are required to set these via kwargs component_name: str = "" component_key: str = "" component_id: str = "" - request = None - parent = None - children = [] - # Caches to reduce the amount of time introspecting the class - _methods_cache: Dict[str, Callable] = {} - _attribute_names_cache: List[str] = [] - _hook_methods_cache: List[str] = [] + def __init__(self, **kwargs): + self.response_class = UnicornTemplateResponse - # Dictionary with key: attribute name; value: pickled attribute value - _resettable_attributes_cache: Dict[str, Any] = {} + self.component_name: str = "" + self.component_key: str = "" + self.component_id: str = "" - # JavaScript method calls - calls = [] + # Without these instance variables calling UnicornView() outside the + # Django view/template logic (i.e. in unit tests) results in odd results. + self.request: HttpRequest = None + self.parent: UnicornView = None + self.children: List[UnicornView] = [] + + # Caches to reduce the amount of time introspecting the class + self._methods_cache: Dict[str, Callable] = {} + self._attribute_names_cache: List[str] = [] + self._hook_methods_cache: List[str] = [] + + # Dictionary with key: attribute name; value: pickled attribute value + self._resettable_attributes_cache: Dict[str, Any] = {} + + # JavaScript method calls + self.calls = [] - def __init__(self, **kwargs): super().__init__(**kwargs) assert self.component_name, "Component name is required" @@ -395,27 +404,17 @@ def _cache_component(self, request: HttpRequest, parent=None, **kwargs): # Put the component's class in a module cache views_cache[self.component_id] = (self.__class__, parent, kwargs) - cacheable_component = None - # Put the instantiated component into a module cache and the Django cache try: - cacheable_component = get_cacheable_component(self) + with CacheableComponent(self): + if COMPONENTS_MODULE_CACHE_ENABLED: + constructed_views_cache[self.component_id] = self + + cache = caches[get_cache_alias()] + cache.set(self.component_cache_key, self) except UnicornCacheError as e: logger.warning(e) - if cacheable_component: - if COMPONENTS_MODULE_CACHE_ENABLED: - constructed_views_cache[self.component_id] = cacheable_component - - cache = caches[get_cache_alias()] - cache.set(cacheable_component.component_cache_key, cacheable_component) - - # Re-set `request` on the component that got removed when making it cacheable - self.request = request - - for child in self.children: - child.request = request - @timed def get_frontend_context_variables(self) -> str: """ @@ -509,6 +508,7 @@ def get_context_data(self, **kwargs): "component_id": self.component_id, "component_name": self.component_name, "component_key": self.component_key, + "component": self, "errors": self.errors, } } diff --git a/django_unicorn/static/unicorn/js/component.js b/django_unicorn/static/unicorn/js/component.js index 309c09a7..49e6a69e 100644 --- a/django_unicorn/static/unicorn/js/component.js +++ b/django_unicorn/static/unicorn/js/component.js @@ -259,7 +259,7 @@ export class Component { } else { // Can hard-code `forceModelUpdate` to `true` since it is always required for // `callMethod` actions - this.setModelValues(triggeringElements, true); + this.setModelValues(triggeringElements, true, true); } }); } @@ -448,9 +448,10 @@ export class Component { * Sets all model values. * @param {[Element]} triggeringElements The elements that triggered the event. */ - setModelValues(triggeringElements, forceModelUpdates) { + setModelValues(triggeringElements, forceModelUpdates, updateParents) { triggeringElements = triggeringElements || []; forceModelUpdates = forceModelUpdates || false; + updateParents = updateParents || false; let lastTriggeringElement = null; @@ -490,6 +491,13 @@ export class Component { this.setValue(element); } }); + + if (updateParents) { + const parent = this.getParentComponent(); + if (parent) { + parent.setModelValues(triggeringElements, forceModelUpdates, updateParents); + } + } } /** diff --git a/django_unicorn/static/unicorn/js/eventListeners.js b/django_unicorn/static/unicorn/js/eventListeners.js index 6ff9b890..01894d1c 100644 --- a/django_unicorn/static/unicorn/js/eventListeners.js +++ b/django_unicorn/static/unicorn/js/eventListeners.js @@ -287,7 +287,7 @@ export function addModelEventListener(component, element, eventType) { triggeringElements.push(element); } - component.setModelValues(triggeringElements, forceModelUpdate); + component.setModelValues(triggeringElements, forceModelUpdate, true); } } ); diff --git a/django_unicorn/static/unicorn/js/messageSender.js b/django_unicorn/static/unicorn/js/messageSender.js index 2759b8ec..dd14c58e 100644 --- a/django_unicorn/static/unicorn/js/messageSender.js +++ b/django_unicorn/static/unicorn/js/messageSender.js @@ -135,7 +135,7 @@ export function send(component, callback) { component.return = responseJson.return || {}; component.hash = responseJson.hash; - const parent = responseJson.parent || {}; + let parent = responseJson.parent || {}; const rerenderedComponent = responseJson.dom || {}; const partials = responseJson.partials || []; const { checksum } = responseJson; @@ -160,7 +160,7 @@ export function send(component, callback) { } // Refresh the parent component if there is one - if (hasValue(parent) && hasValue(parent.id)) { + while (hasValue(parent) && hasValue(parent.id)) { const parentComponent = component.getParentComponent(parent.id); if (parentComponent && parentComponent.id === parent.id) { @@ -193,6 +193,7 @@ export function send(component, callback) { child.refreshEventListeners(); }); } + parent = parent.parent || {}; } if (partials.length > 0) { diff --git a/django_unicorn/static/unicorn/js/unicorn.min.js b/django_unicorn/static/unicorn/js/unicorn.min.js index 72051506..c73962c6 100644 --- a/django_unicorn/static/unicorn/js/unicorn.min.js +++ b/django_unicorn/static/unicorn/js/unicorn.min.js @@ -1,2 +1,2 @@ -/* Version: 0.49.2 - November 17, 2022 07:52:48 */ -var Unicorn=function(e){"use strict";function t(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(e,t){for(var i=0;ie.length)&&(t=e.length);for(var i=0,n=new Array(t);i-1}function h(e,t){return void 0===t&&(t=document),t.querySelector(e)}var d={acceptNode:function(e){return NodeFilter.FILTER_ACCEPT}},f={acceptNode:function(e){return e.getAttribute("unicorn:checksum")?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT}};function m(e,t){for(var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:d,n=document.createTreeWalker(e,NodeFilter.SHOW_ELEMENT,i,!1);n.nextNode();)t(n.currentNode)}var v=function(){function e(i){t(this,e),this.attribute=i,this.name=this.attribute.name,this.value=this.attribute.value,this.isUnicorn=!1,this.isModel=!1,this.isPoll=!1,this.isLoading=!1,this.isTarget=!1,this.isPartial=!1,this.isDirty=!1,this.isVisible=!1,this.isKey=!1,this.isError=!1,this.modifiers={},this.eventType=null,this.init()}return n(e,[{key:"init",value:function(){var e=this;if(this.name.startsWith("unicorn:")||this.name.startsWith("u:")){if(this.isUnicorn=!0,c(this.name,":model"))this.isModel=!0;else if(c(this.name,":poll.disable"))this.isPollDisable=!0;else if(c(this.name,":poll"))this.isPoll=!0;else if(c(this.name,":loading"))this.isLoading=!0;else if(c(this.name,":target"))this.isTarget=!0;else if(c(this.name,":partial"))this.isPartial=!0;else if(c(this.name,":dirty"))this.isDirty=!0;else if(c(this.name,":visible"))this.isVisible=!0;else if("unicorn:key"===this.name||"u:key"===this.name)this.isKey=!0;else if(c(this.name,":error:"))this.isError=!0;else{var t=this.name.replace("unicorn:","").replace("u:","");"id"!==t&&"name"!==t&&"checksum"!==t&&(this.eventType=t)}var i=this.name;this.eventType&&(i=this.eventType),i.split(".").slice(1).forEach((function(t){var i=t.split("-");e.modifiers[i[0]]=!(i.length>1)||i[1],e.eventType&&(e.eventType=e.eventType.replace(".".concat(t),""))}))}}}]),e}(),p=function(){function e(i){t(this,e),this.el=i,this.init()}return n(e,[{key:"init",value:function(){var t=this;if(this.id=this.el.id,this.isUnicorn=!1,this.attributes=[],this.value=this.getValue(),this.parent=null,this.el.parentElement&&(this.parent=new e(this.el.parentElement)),this.model={},this.poll={},this.loading={},this.dirty={},this.actions=[],this.partials=[],this.target=null,this.visibility={},this.key=null,this.events=[],this.errors=[],this.el.attributes)for(var i=function(e){var i=new v(t.el.attributes[e]);if(t.attributes.push(i),i.isUnicorn&&(t.isUnicorn=!0),i.isModel){var n="model";t[n].name=i.value,t[n].eventType=i.modifiers.lazy?"blur":"input",t[n].isLazy=!!i.modifiers.lazy,t[n].isDefer=!!i.modifiers.defer,t[n].debounceTime=i.modifiers.debounce&&parseInt(i.modifiers.debounce,10)||-1}else if(i.isPoll){t.poll.method=i.value?i.value:"refresh",t.poll.timing=2e3,t.poll.disable=!1;var r=i.name.split("-").slice(1);r.length>0&&(t.poll.timing=parseInt(r[0],10)||2e3)}else if(i.isPollDisable)t.poll.disableData=i.value;else if(i.isLoading||i.isDirty){var o="dirty";i.isLoading&&(o="loading"),i.modifiers.attr?t[o].attr=i.value:i.modifiers.class&&i.modifiers.remove?t[o].removeClasses=i.value.split(" "):i.modifiers.class?t[o].classes=i.value.split(" "):i.isLoading&&i.modifiers.remove?t.loading.hide=!0:i.isLoading&&(t.loading.show=!0)}else if(i.isTarget)t.target=i.value;else if(i.isPartial)i.modifiers.id?t.partials.push({id:i.value}):i.modifiers.key?t.partials.push({key:i.value}):t.partials.push({target:i.value});else if(i.isVisible){var a=i.modifiers.threshold||0;a>1&&(a/=100),t.visibility.method=i.value,t.visibility.threshold=a,t.visibility.debounceTime=i.modifiers.debounce&&parseInt(i.modifiers.debounce,10)||0}else if(i.eventType){var s={};s.name=i.value,s.eventType=i.eventType,s.isPrevent=!1,s.isStop=!1,s.isDiscard=!1,s.debounceTime=0,i.modifiers&&Object.keys(i.modifiers).forEach((function(e){"prevent"===e?s.isPrevent=!0:"stop"===e?s.isStop=!0:"discard"===e?s.isDiscard=!0:"debounce"===e?s.debounceTime=i.modifiers.debounce&&parseInt(i.modifiers.debounce,10)||0:s.key=e})),t.actions.push(s)}if(i.isKey&&(t.key=i.value),i.isError){var l=i.name.replace("unicorn:error:","");t.errors.push({code:l,message:i.value})}},n=0;n0&&t in e.actionEvents&&e.actionEvents[t].forEach((function(t){var r=t.action,o=t.element;n.isSame(o)&&(e.walker(o.el,(function(t){e.modelEls.filter((function(e){return e.isSameEl(t)})).forEach((function(t){if(l(t.model)&&t.model.isLazy){var i={type:"syncInput",payload:{name:t.model.name,value:t.getValue()}};e.actionQueue.push(i)}}))})),r.isPrevent&&i.preventDefault(),r.isStop&&i.stopPropagation(),r.isDiscard&&(e.actionQueue=[]),function(e){if(!c(e=e.trim(),"(")||!e.endsWith(")"))return[];e=e.slice(e.indexOf("(")+1,e.length-1);for(var t=[],i="",n=!1,r=!1,o=0,a=0,s=0;s-1&&e.actionQueue.splice(a))}e.actionQueue.push(o),e.queueMessage(t.model.debounceTime,(function(i,n,r){r?console.error(r):((i=i||[]).some((function(e){return e.isSame(t)}))||i.push(t),e.setModelValues(i,n))}))}))}var k={},T={};function w(e){return{childrenOnly:!1,getNodeKey:function(e){if(e.attributes){var t=e.getAttribute("unicorn:key")||e.getAttribute("u:key")||e.id;if(t)return t}},onBeforeElUpdated:function(t,i){if(t.isEqualNode(i))return!1;if(e&&"SCRIPT"===t.nodeName&&"SCRIPT"===i.nodeName){var n=document.createElement("script");return r(i.attributes).forEach((function(e){n.setAttribute(e.nodeName,e.nodeValue)})),n.innerHTML=i.innerHTML,t.replaceWith(n),!1}return!0},onNodeAdded:function(t){if(e&&"SCRIPT"===t.nodeName){var i=document.createElement("script");r(t.attributes).forEach((function(e){i.setAttribute(e.nodeName,e.nodeValue)})),i.innerHTML=t.innerHTML,t.replaceWith(i)}}}}function A(e,t){if(0!==e.actionQueue.length&&e.currentActionQueue!==e.actionQueue){var i=e.actionQueue.some((function(e){return"callMethod"===e.type}));e.currentActionQueue=e.actionQueue,e.actionQueue=[];var n={id:e.id,data:e.data,checksum:e.checksum,actionQueue:e.currentActionQueue,epoch:Date.now(),hash:e.hash},r={Accept:"application/json","X-Requested-With":"XMLHttpRequest"};r[e.csrfTokenHeaderName]=function(e){var t="csrftoken=",i=e.document.cookie.split(";").filter((function(e){return e.trim().startsWith(t)}));if(i.length>0)return i[0].replace(t,"");var n=e.document.getElementsByName("csrfmiddlewaretoken");if(n&&n.length>0)return n[0].getAttribute("value");throw Error("CSRF token is missing. Do you need to add {% csrf_token %}?")}(e),fetch(e.syncUrl,{method:"POST",headers:r,body:JSON.stringify(n)}).then((function(t){if(t.ok)return t.json();if(e.loadingEls.forEach((function(e){e.loading.hide?e.show():e.loading.show&&e.hide(),e.handleLoading(!0),e.handleDirty(!0)})),304===t.status)return null;throw Error("Error when getting response: ".concat(t.statusText," (").concat(t.status,")"))})).then((function(n){if(n&&(!n.queued||!0!==n.queued)){if(n.error)throw"Checksum does not match"===n.error&&u(t)&&t([],!0,null),Error(n.error);if(n.redirect)if(n.redirect.url){if(!n.redirect.refresh)return void(e.window.location.href=n.redirect.url);n.redirect.title&&(e.window.document.title=n.redirect.title),e.window.history.pushState({},"",n.redirect.url)}else n.redirect.hash&&(e.window.location.hash=n.redirect.hash);e.modelEls.forEach((function(e){e.init(),e.removeErrors(),e.handleDirty(!0)})),Object.keys(n.data||{}).forEach((function(t){e.data[t]=n.data[t]})),e.errors=n.errors||{},e.return=n.return||{},e.hash=n.hash;var r=n.parent||{},o=n.dom||{},a=n.partials||[],s=n.checksum,c=n.poll||{};if(l(c)&&(e.poll.timer&&clearInterval(e.poll.timer),c.timing&&(e.poll.timing=c.timing),c.method&&(e.poll.method=c.method),e.poll.disable=c.disable||!1,e.startPolling()),l(r)&&l(r.id)){var d=e.getParentComponent(r.id);d&&d.id===r.id&&(l(r.data)&&(d.data=r.data),r.dom&&e.morphdom(d.root,r.dom,w(e.reloadScriptElements)),r.checksum&&(d.root.setAttribute("unicorn:checksum",r.checksum),d.refreshChecksum()),d.refreshEventListeners(),d.getChildrenComponents().forEach((function(e){e.init(),e.refreshEventListeners()})))}if(a.length>0){for(var f=0;f=97?r===o.toUpperCase():n<=90&&i>=97&&o===r.toUpperCase())}function I(e,t,i){e[i]!==t[i]&&(e[i]=t[i],e[i]?e.setAttribute(i,""):e.removeAttribute(i))}var V={OPTION:function(e,t){var i=e.parentNode;if(i){var n=i.nodeName.toUpperCase();"OPTGROUP"===n&&(n=(i=i.parentNode)&&i.nodeName.toUpperCase()),"SELECT"!==n||i.hasAttribute("multiple")||(e.hasAttribute("selected")&&!t.selected&&(e.setAttribute("selected","selected"),e.removeAttribute("selected")),i.selectedIndex=-1)}I(e,t,"selected")},INPUT:function(e,t){I(e,t,"checked"),I(e,t,"disabled"),e.value!==t.value&&(e.value=t.value),t.hasAttribute("value")||e.removeAttribute("value")},TEXTAREA:function(e,t){var i=t.value;e.value!==i&&(e.value=i);var n=e.firstChild;if(n){var r=n.nodeValue;if(r==i||!i&&r==e.placeholder)return;n.nodeValue=i}},SELECT:function(e,t){if(!t.hasAttribute("multiple")){for(var i,n,r=-1,o=0,a=e.firstChild;a;)if("OPTGROUP"===(n=a.nodeName&&a.nodeName.toUpperCase()))a=(i=a).firstChild;else{if("OPTION"===n){if(a.hasAttribute("selected")){r=o;break}o++}!(a=a.nextSibling)&&i&&(a=i.nextSibling,i=null)}e.selectedIndex=r}}};function M(){}function U(e){if(e)return e.getAttribute&&e.getAttribute("id")||e.id}var D=function(e){return function(t,i,n){if(n||(n={}),"string"==typeof i)if("#document"===t.nodeName||"HTML"===t.nodeName||"BODY"===t.nodeName){var r=i;(i=C.createElement("html")).innerHTML=r}else i=P(i);var o=n.getNodeKey||U,a=n.onBeforeNodeAdded||M,s=n.onNodeAdded||M,l=n.onBeforeElUpdated||M,u=n.onElUpdated||M,c=n.onBeforeNodeDiscarded||M,h=n.onNodeDiscarded||M,d=n.onBeforeElChildrenUpdated||M,f=!0===n.childrenOnly,m=Object.create(null),v=[];function p(e){v.push(e)}function y(e,t){if(1===e.nodeType)for(var i=e.firstChild;i;){var n=void 0;t&&(n=o(i))?p(n):(h(i),i.firstChild&&y(i,t)),i=i.nextSibling}}function g(e,t,i){!1!==c(e)&&(t&&t.removeChild(e),h(e),y(e,i))}function E(e){s(e);for(var t=e.firstChild;t;){var i=t.nextSibling,n=o(t);if(n){var r=m[n];r&&O(t,r)?(t.parentNode.replaceChild(r,t),b(r,t)):E(t)}else E(t);t=i}}function b(t,i,n){var r=o(i);if(r&&delete m[r],!n){if(!1===l(t,i))return;if(t.hasAttribute("u:ignore")||t.hasAttribute("unicorn:ignore"))return;if(e(t,i),u(t),!1===d(t,i))return}"TEXTAREA"!==t.nodeName?function(e,t){var i,n,r,s,l,u=t.firstChild,c=e.firstChild;e:for(;u;){for(s=u.nextSibling,i=o(u);c;){if(r=c.nextSibling,u.isSameNode&&u.isSameNode(c)){u=s,c=r;continue e}n=o(c);var h=c.nodeType,d=void 0;if(h===u.nodeType&&(1===h?(i?i!==n&&((l=m[i])?r===l?d=!1:(e.insertBefore(l,c),n?p(n):g(c,e,!0),c=l):d=!1):n&&(d=!1),(d=!1!==d&&O(c,u))&&b(c,u)):3!==h&&8!=h||(d=!0,c.nodeValue!==u.nodeValue&&(c.nodeValue=u.nodeValue))),d){u=s,c=r;continue e}n?p(n):g(c,e,!0),c=r}if(i&&(l=m[i])&&O(l,u))e.appendChild(l),b(l,u);else{var f=a(u);!1!==f&&(f&&(u=f),u.actualize&&(u=u.actualize(e.ownerDocument||C)),e.appendChild(u),E(u))}u=s,c=r}!function(e,t,i){for(;t;){var n=t.nextSibling;(i=o(t))?p(i):g(t,e,!0),t=n}}(e,c,n);var v=V[e.nodeName];v&&v(e,t)}(t,i):t.innerHTML!=i.innerHTML&&V.TEXTAREA(t,i)}!function e(t){if(1===t.nodeType||11===t.nodeType)for(var i=t.firstChild;i;){var n=o(i);n&&(m[n]=i),e(i),i=i.nextSibling}}(t);var k,T,w=t,A=w.nodeType,N=i.nodeType;if(!f)if(1===A)1===N?O(t,i)||(h(t),w=function(e,t){for(var i=e.firstChild;i;){var n=i.nextSibling;t.appendChild(i),i=n}return t}(t,(k=i.nodeName,(T=i.namespaceURI)&&"http://www.w3.org/1999/xhtml"!==T?C.createElementNS(T,k):C.createElement(k)))):w=i;else if(3===A||8===A){if(N===A)return w.nodeValue!==i.nodeValue&&(w.nodeValue=i.nodeValue),w;w=i}if(w===i)h(t);else{if(i.isSameNode&&i.isSameNode(w))return;if(b(w,i,f),v)for(var S=0,L=v.length;S=0;s--)n=(i=a[s]).name,r=i.namespaceURI,o=i.value,r?(n=i.localName||n,e.getAttributeNS(r,n)!==o&&("xmlns"===i.prefix&&(n=i.name),e.setAttributeNS(r,n,o))):e.getAttribute(n)!==o&&e.setAttribute(n,o);for(var l=e.attributes,u=l.length-1;u>=0;u--)n=(i=l[u]).name,(r=i.namespaceURI)?(n=i.localName||n,t.hasAttributeNS(r,n)||e.removeAttributeNS(r,n)):t.hasAttribute(n)||e.removeAttribute(n)}})),R=function(){function e(i){t(this,e),this.id=i.id,this.name=i.name,this.key=i.key,this.messageUrl=i.messageUrl,this.csrfTokenHeaderName=i.csrfTokenHeaderName,this.reloadScriptElements=i.reloadScriptElements,this.hash=i.hash,this.data=i.data||{},this.syncUrl="".concat(this.messageUrl,"/").concat(this.name),this.document=i.document||document,this.walker=i.walker||m,this.window=i.window||window,this.morphdom=i.morphdom||D,this.root=void 0,this.modelEls=[],this.loadingEls=[],this.keyEls=[],this.visibilityEls=[],this.errors={},this.return={},this.poll={},this.actionQueue=[],this.currentActionQueue=null,this.lastTriggeringElements=[],this.actionEvents={},this.attachedEventTypes=[],this.attachedModelEvents=[],this.init(),this.refreshEventListeners(),this.initVisibility(),this.initPolling(),this.callCalls(i.calls)}return n(e,[{key:"init",value:function(){if(this.root=h('[unicorn\\:id="'.concat(this.id,'"]'),this.document),!this.root)throw Error("No id found");this.refreshChecksum()}},{key:"getChildrenComponents",value:function(){var e=this,t=[];return this.walker(this.root,(function(i){if(!i.isSameNode(e.root)){var n=i.getAttribute("unicorn:id");if(n){var r=k[n]||null;r&&t.push(r)}}})),t}},{key:"getParentComponent",value:function(e){if(void 0!==e)return k[e]||null;for(var t=this.root,i=null;!i&&null!==t.parentElement;){var n=(t=t.parentElement).getAttribute("unicorn:id");n&&(i=k[n]||null)}return i}},{key:"callCalls",value:function(e){var t=this,i=[];return(e=e||[]).forEach((function(e){var n,o=e.fn,a=t.window;(e.fn.split(".").forEach((function(t,i){i0){var r=!1;l(n=e.slice(-1)[0])&&l(n.model)&&!n.model.isLazy&&["id","key"].forEach((function(e){i.modelEls.forEach((function(t){r||n[e]&&n[e]===t[e]&&(t.focus(),r=!0)}))}))}this.modelEls.forEach((function(e){!t&&n&&n.isSame(e)||i.setValue(e)}))}},{key:"queueMessage",value:function(e,t){-1===e?a(A,250,!1)(this,t):a(A,e,!1)(this,t)}},{key:"triggerLifecycleEvent",value:function(e){var t=this;e in T&&T[e].forEach((function(e){return e(t)}))}},{key:"trigger",value:function(e){this.modelEls.forEach((function(t){if(t.key===e){var i=t.model.isLazy?"blur":"input";t.el.dispatchEvent(new Event(i))}}))}}]),e}(),x="",Q=!1,j="X-CSRFToken";function H(e){var t;if(Object.keys(k).forEach((function(i){if(s(t)){var n=k[i];n.key===e&&(t=n)}})),s(t)&&Object.keys(k).forEach((function(i){if(s(t)){var n=k[i];n.name===e&&(t=n)}})),!t)throw Error("No component found for: ".concat(e));return t}return e.addEventListener=function(e,t){e in T||(T[e]=[]),T[e].push(t)},e.call=function(e,t){for(var i=H(e),n="",r=arguments.length,o=new Array(r>2?r-2:0),a=2;ae.length)&&(t=e.length);for(var i=0,n=new Array(t);i-1}function h(e,t){return void 0===t&&(t=document),t.querySelector(e)}var d={acceptNode:function(e){return NodeFilter.FILTER_ACCEPT}},f={acceptNode:function(e){return e.getAttribute("unicorn:checksum")?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT}};function m(e,t){for(var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:d,n=document.createTreeWalker(e,NodeFilter.SHOW_ELEMENT,i,!1);n.nextNode();)t(n.currentNode)}var v=function(){function e(i){t(this,e),this.attribute=i,this.name=this.attribute.name,this.value=this.attribute.value,this.isUnicorn=!1,this.isModel=!1,this.isPoll=!1,this.isLoading=!1,this.isTarget=!1,this.isPartial=!1,this.isDirty=!1,this.isVisible=!1,this.isKey=!1,this.isError=!1,this.modifiers={},this.eventType=null,this.init()}return n(e,[{key:"init",value:function(){var e=this;if(this.name.startsWith("unicorn:")||this.name.startsWith("u:")){if(this.isUnicorn=!0,c(this.name,":model"))this.isModel=!0;else if(c(this.name,":poll.disable"))this.isPollDisable=!0;else if(c(this.name,":poll"))this.isPoll=!0;else if(c(this.name,":loading"))this.isLoading=!0;else if(c(this.name,":target"))this.isTarget=!0;else if(c(this.name,":partial"))this.isPartial=!0;else if(c(this.name,":dirty"))this.isDirty=!0;else if(c(this.name,":visible"))this.isVisible=!0;else if("unicorn:key"===this.name||"u:key"===this.name)this.isKey=!0;else if(c(this.name,":error:"))this.isError=!0;else{var t=this.name.replace("unicorn:","").replace("u:","");"id"!==t&&"name"!==t&&"checksum"!==t&&(this.eventType=t)}var i=this.name;this.eventType&&(i=this.eventType),i.split(".").slice(1).forEach((function(t){var i=t.split("-");e.modifiers[i[0]]=!(i.length>1)||i[1],e.eventType&&(e.eventType=e.eventType.replace(".".concat(t),""))}))}}}]),e}(),p=function(){function e(i){t(this,e),this.el=i,this.init()}return n(e,[{key:"init",value:function(){var t=this;if(this.id=this.el.id,this.isUnicorn=!1,this.attributes=[],this.value=this.getValue(),this.parent=null,this.el.parentElement&&(this.parent=new e(this.el.parentElement)),this.model={},this.poll={},this.loading={},this.dirty={},this.actions=[],this.partials=[],this.target=null,this.visibility={},this.key=null,this.events=[],this.errors=[],this.el.attributes)for(var i=function(e){var i=new v(t.el.attributes[e]);if(t.attributes.push(i),i.isUnicorn&&(t.isUnicorn=!0),i.isModel){var n="model";t[n].name=i.value,t[n].eventType=i.modifiers.lazy?"blur":"input",t[n].isLazy=!!i.modifiers.lazy,t[n].isDefer=!!i.modifiers.defer,t[n].debounceTime=i.modifiers.debounce&&parseInt(i.modifiers.debounce,10)||-1}else if(i.isPoll){t.poll.method=i.value?i.value:"refresh",t.poll.timing=2e3,t.poll.disable=!1;var r=i.name.split("-").slice(1);r.length>0&&(t.poll.timing=parseInt(r[0],10)||2e3)}else if(i.isPollDisable)t.poll.disableData=i.value;else if(i.isLoading||i.isDirty){var o="dirty";i.isLoading&&(o="loading"),i.modifiers.attr?t[o].attr=i.value:i.modifiers.class&&i.modifiers.remove?t[o].removeClasses=i.value.split(" "):i.modifiers.class?t[o].classes=i.value.split(" "):i.isLoading&&i.modifiers.remove?t.loading.hide=!0:i.isLoading&&(t.loading.show=!0)}else if(i.isTarget)t.target=i.value;else if(i.isPartial)i.modifiers.id?t.partials.push({id:i.value}):i.modifiers.key?t.partials.push({key:i.value}):t.partials.push({target:i.value});else if(i.isVisible){var a=i.modifiers.threshold||0;a>1&&(a/=100),t.visibility.method=i.value,t.visibility.threshold=a,t.visibility.debounceTime=i.modifiers.debounce&&parseInt(i.modifiers.debounce,10)||0}else if(i.eventType){var s={};s.name=i.value,s.eventType=i.eventType,s.isPrevent=!1,s.isStop=!1,s.isDiscard=!1,s.debounceTime=0,i.modifiers&&Object.keys(i.modifiers).forEach((function(e){"prevent"===e?s.isPrevent=!0:"stop"===e?s.isStop=!0:"discard"===e?s.isDiscard=!0:"debounce"===e?s.debounceTime=i.modifiers.debounce&&parseInt(i.modifiers.debounce,10)||0:s.key=e})),t.actions.push(s)}if(i.isKey&&(t.key=i.value),i.isError){var l=i.name.replace("unicorn:error:","");t.errors.push({code:l,message:i.value})}},n=0;n0&&t in e.actionEvents&&e.actionEvents[t].forEach((function(t){var r=t.action,o=t.element;n.isSame(o)&&(e.walker(o.el,(function(t){e.modelEls.filter((function(e){return e.isSameEl(t)})).forEach((function(t){if(l(t.model)&&t.model.isLazy){var i={type:"syncInput",payload:{name:t.model.name,value:t.getValue()}};e.actionQueue.push(i)}}))})),r.isPrevent&&i.preventDefault(),r.isStop&&i.stopPropagation(),r.isDiscard&&(e.actionQueue=[]),function(e){if(!c(e=e.trim(),"(")||!e.endsWith(")"))return[];e=e.slice(e.indexOf("(")+1,e.length-1);for(var t=[],i="",n=!1,r=!1,o=0,a=0,s=0;s-1&&e.actionQueue.splice(a))}e.actionQueue.push(o),e.queueMessage(t.model.debounceTime,(function(i,n,r){r?console.error(r):((i=i||[]).some((function(e){return e.isSame(t)}))||i.push(t),e.setModelValues(i,n,!0))}))}))}var k={},T={};function w(e){return{childrenOnly:!1,getNodeKey:function(e){if(e.attributes){var t=e.getAttribute("unicorn:key")||e.getAttribute("u:key")||e.id;if(t)return t}},onBeforeElUpdated:function(t,i){if(t.isEqualNode(i))return!1;if(e&&"SCRIPT"===t.nodeName&&"SCRIPT"===i.nodeName){var n=document.createElement("script");return r(i.attributes).forEach((function(e){n.setAttribute(e.nodeName,e.nodeValue)})),n.innerHTML=i.innerHTML,t.replaceWith(n),!1}return!0},onNodeAdded:function(t){if(e&&"SCRIPT"===t.nodeName){var i=document.createElement("script");r(t.attributes).forEach((function(e){i.setAttribute(e.nodeName,e.nodeValue)})),i.innerHTML=t.innerHTML,t.replaceWith(i)}}}}function A(e,t){if(0!==e.actionQueue.length&&e.currentActionQueue!==e.actionQueue){var i=e.actionQueue.some((function(e){return"callMethod"===e.type}));e.currentActionQueue=e.actionQueue,e.actionQueue=[];var n={id:e.id,data:e.data,checksum:e.checksum,actionQueue:e.currentActionQueue,epoch:Date.now(),hash:e.hash},r={Accept:"application/json","X-Requested-With":"XMLHttpRequest"};r[e.csrfTokenHeaderName]=function(e){var t="csrftoken=",i=e.document.cookie.split(";").filter((function(e){return e.trim().startsWith(t)}));if(i.length>0)return i[0].replace(t,"");var n=e.document.getElementsByName("csrfmiddlewaretoken");if(n&&n.length>0)return n[0].getAttribute("value");throw Error("CSRF token is missing. Do you need to add {% csrf_token %}?")}(e),fetch(e.syncUrl,{method:"POST",headers:r,body:JSON.stringify(n)}).then((function(t){if(t.ok)return t.json();if(e.loadingEls.forEach((function(e){e.loading.hide?e.show():e.loading.show&&e.hide(),e.handleLoading(!0),e.handleDirty(!0)})),304===t.status)return null;throw Error("Error when getting response: ".concat(t.statusText," (").concat(t.status,")"))})).then((function(n){if(n&&(!n.queued||!0!==n.queued)){if(n.error)throw"Checksum does not match"===n.error&&u(t)&&t([],!0,null),Error(n.error);if(n.redirect)if(n.redirect.url){if(!n.redirect.refresh)return void(e.window.location.href=n.redirect.url);n.redirect.title&&(e.window.document.title=n.redirect.title),e.window.history.pushState({},"",n.redirect.url)}else n.redirect.hash&&(e.window.location.hash=n.redirect.hash);e.modelEls.forEach((function(e){e.init(),e.removeErrors(),e.handleDirty(!0)})),Object.keys(n.data||{}).forEach((function(t){e.data[t]=n.data[t]})),e.errors=n.errors||{},e.return=n.return||{},e.hash=n.hash;var r=n.parent||{},o=n.dom||{},a=n.partials||[],s=n.checksum,c=n.poll||{};for(l(c)&&(e.poll.timer&&clearInterval(e.poll.timer),c.timing&&(e.poll.timing=c.timing),c.method&&(e.poll.method=c.method),e.poll.disable=c.disable||!1,e.startPolling());l(r)&&l(r.id);){var d=e.getParentComponent(r.id);d&&d.id===r.id&&(l(r.data)&&(d.data=r.data),r.dom&&e.morphdom(d.root,r.dom,w(e.reloadScriptElements)),r.checksum&&(d.root.setAttribute("unicorn:checksum",r.checksum),d.refreshChecksum()),d.refreshEventListeners(),d.getChildrenComponents().forEach((function(e){e.init(),e.refreshEventListeners()}))),r=r.parent||{}}if(a.length>0){for(var f=0;f=97?r===o.toUpperCase():n<=90&&i>=97&&o===r.toUpperCase())}function V(e,t,i){e[i]!==t[i]&&(e[i]=t[i],e[i]?e.setAttribute(i,""):e.removeAttribute(i))}var I={OPTION:function(e,t){var i=e.parentNode;if(i){var n=i.nodeName.toUpperCase();"OPTGROUP"===n&&(n=(i=i.parentNode)&&i.nodeName.toUpperCase()),"SELECT"!==n||i.hasAttribute("multiple")||(e.hasAttribute("selected")&&!t.selected&&(e.setAttribute("selected","selected"),e.removeAttribute("selected")),i.selectedIndex=-1)}V(e,t,"selected")},INPUT:function(e,t){V(e,t,"checked"),V(e,t,"disabled"),e.value!==t.value&&(e.value=t.value),t.hasAttribute("value")||e.removeAttribute("value")},TEXTAREA:function(e,t){var i=t.value;e.value!==i&&(e.value=i);var n=e.firstChild;if(n){var r=n.nodeValue;if(r==i||!i&&r==e.placeholder)return;n.nodeValue=i}},SELECT:function(e,t){if(!t.hasAttribute("multiple")){for(var i,n,r=-1,o=0,a=e.firstChild;a;)if("OPTGROUP"===(n=a.nodeName&&a.nodeName.toUpperCase()))a=(i=a).firstChild;else{if("OPTION"===n){if(a.hasAttribute("selected")){r=o;break}o++}!(a=a.nextSibling)&&i&&(a=i.nextSibling,i=null)}e.selectedIndex=r}}};function M(){}function U(e){if(e)return e.getAttribute&&e.getAttribute("id")||e.id}var D=function(e){return function(t,i,n){if(n||(n={}),"string"==typeof i)if("#document"===t.nodeName||"HTML"===t.nodeName||"BODY"===t.nodeName){var r=i;(i=C.createElement("html")).innerHTML=r}else i=P(i);var o=n.getNodeKey||U,a=n.onBeforeNodeAdded||M,s=n.onNodeAdded||M,l=n.onBeforeElUpdated||M,u=n.onElUpdated||M,c=n.onBeforeNodeDiscarded||M,h=n.onNodeDiscarded||M,d=n.onBeforeElChildrenUpdated||M,f=!0===n.childrenOnly,m=Object.create(null),v=[];function p(e){v.push(e)}function y(e,t){if(1===e.nodeType)for(var i=e.firstChild;i;){var n=void 0;t&&(n=o(i))?p(n):(h(i),i.firstChild&&y(i,t)),i=i.nextSibling}}function g(e,t,i){!1!==c(e)&&(t&&t.removeChild(e),h(e),y(e,i))}function E(e){s(e);for(var t=e.firstChild;t;){var i=t.nextSibling,n=o(t);if(n){var r=m[n];r&&O(t,r)?(t.parentNode.replaceChild(r,t),b(r,t)):E(t)}else E(t);t=i}}function b(t,i,n){var r=o(i);if(r&&delete m[r],!n){if(!1===l(t,i))return;if(t.hasAttribute("u:ignore")||t.hasAttribute("unicorn:ignore"))return;if(e(t,i),u(t),!1===d(t,i))return}"TEXTAREA"!==t.nodeName?function(e,t){var i,n,r,s,l,u=t.firstChild,c=e.firstChild;e:for(;u;){for(s=u.nextSibling,i=o(u);c;){if(r=c.nextSibling,u.isSameNode&&u.isSameNode(c)){u=s,c=r;continue e}n=o(c);var h=c.nodeType,d=void 0;if(h===u.nodeType&&(1===h?(i?i!==n&&((l=m[i])?r===l?d=!1:(e.insertBefore(l,c),n?p(n):g(c,e,!0),c=l):d=!1):n&&(d=!1),(d=!1!==d&&O(c,u))&&b(c,u)):3!==h&&8!=h||(d=!0,c.nodeValue!==u.nodeValue&&(c.nodeValue=u.nodeValue))),d){u=s,c=r;continue e}n?p(n):g(c,e,!0),c=r}if(i&&(l=m[i])&&O(l,u))e.appendChild(l),b(l,u);else{var f=a(u);!1!==f&&(f&&(u=f),u.actualize&&(u=u.actualize(e.ownerDocument||C)),e.appendChild(u),E(u))}u=s,c=r}!function(e,t,i){for(;t;){var n=t.nextSibling;(i=o(t))?p(i):g(t,e,!0),t=n}}(e,c,n);var v=I[e.nodeName];v&&v(e,t)}(t,i):t.innerHTML!=i.innerHTML&&I.TEXTAREA(t,i)}!function e(t){if(1===t.nodeType||11===t.nodeType)for(var i=t.firstChild;i;){var n=o(i);n&&(m[n]=i),e(i),i=i.nextSibling}}(t);var k,T,w=t,A=w.nodeType,N=i.nodeType;if(!f)if(1===A)1===N?O(t,i)||(h(t),w=function(e,t){for(var i=e.firstChild;i;){var n=i.nextSibling;t.appendChild(i),i=n}return t}(t,(k=i.nodeName,(T=i.namespaceURI)&&"http://www.w3.org/1999/xhtml"!==T?C.createElementNS(T,k):C.createElement(k)))):w=i;else if(3===A||8===A){if(N===A)return w.nodeValue!==i.nodeValue&&(w.nodeValue=i.nodeValue),w;w=i}if(w===i)h(t);else{if(i.isSameNode&&i.isSameNode(w))return;if(b(w,i,f),v)for(var S=0,L=v.length;S=0;s--)n=(i=a[s]).name,r=i.namespaceURI,o=i.value,r?(n=i.localName||n,e.getAttributeNS(r,n)!==o&&("xmlns"===i.prefix&&(n=i.name),e.setAttributeNS(r,n,o))):e.getAttribute(n)!==o&&e.setAttribute(n,o);for(var l=e.attributes,u=l.length-1;u>=0;u--)n=(i=l[u]).name,(r=i.namespaceURI)?(n=i.localName||n,t.hasAttributeNS(r,n)||e.removeAttributeNS(r,n)):t.hasAttribute(n)||e.removeAttribute(n)}})),R=function(){function e(i){t(this,e),this.id=i.id,this.name=i.name,this.key=i.key,this.messageUrl=i.messageUrl,this.csrfTokenHeaderName=i.csrfTokenHeaderName,this.reloadScriptElements=i.reloadScriptElements,this.hash=i.hash,this.data=i.data||{},this.syncUrl="".concat(this.messageUrl,"/").concat(this.name),this.document=i.document||document,this.walker=i.walker||m,this.window=i.window||window,this.morphdom=i.morphdom||D,this.root=void 0,this.modelEls=[],this.loadingEls=[],this.keyEls=[],this.visibilityEls=[],this.errors={},this.return={},this.poll={},this.actionQueue=[],this.currentActionQueue=null,this.lastTriggeringElements=[],this.actionEvents={},this.attachedEventTypes=[],this.attachedModelEvents=[],this.init(),this.refreshEventListeners(),this.initVisibility(),this.initPolling(),this.callCalls(i.calls)}return n(e,[{key:"init",value:function(){if(this.root=h('[unicorn\\:id="'.concat(this.id,'"]'),this.document),!this.root)throw Error("No id found");this.refreshChecksum()}},{key:"getChildrenComponents",value:function(){var e=this,t=[];return this.walker(this.root,(function(i){if(!i.isSameNode(e.root)){var n=i.getAttribute("unicorn:id");if(n){var r=k[n]||null;r&&t.push(r)}}})),t}},{key:"getParentComponent",value:function(e){if(void 0!==e)return k[e]||null;for(var t=this.root,i=null;!i&&null!==t.parentElement;){var n=(t=t.parentElement).getAttribute("unicorn:id");n&&(i=k[n]||null)}return i}},{key:"callCalls",value:function(e){var t=this,i=[];return(e=e||[]).forEach((function(e){var n,o=e.fn,a=t.window;(e.fn.split(".").forEach((function(t,i){i0){var o=!1;l(r=e.slice(-1)[0])&&l(r.model)&&!r.model.isLazy&&["id","key"].forEach((function(e){n.modelEls.forEach((function(t){o||r[e]&&r[e]===t[e]&&(t.focus(),o=!0)}))}))}if(this.modelEls.forEach((function(e){!t&&r&&r.isSame(e)||n.setValue(e)})),i){var a=this.getParentComponent();a&&a.setModelValues(e,t,i)}}},{key:"queueMessage",value:function(e,t){-1===e?a(A,250,!1)(this,t):a(A,e,!1)(this,t)}},{key:"triggerLifecycleEvent",value:function(e){var t=this;e in T&&T[e].forEach((function(e){return e(t)}))}},{key:"trigger",value:function(e){this.modelEls.forEach((function(t){if(t.key===e){var i=t.model.isLazy?"blur":"input";t.el.dispatchEvent(new Event(i))}}))}}]),e}(),x="",Q=!1,j="X-CSRFToken";function H(e){var t;if(Object.keys(k).forEach((function(i){if(s(t)){var n=k[i];n.key===e&&(t=n)}})),s(t)&&Object.keys(k).forEach((function(i){if(s(t)){var n=k[i];n.name===e&&(t=n)}})),!t)throw Error("No component found for: ".concat(e));return t}return e.addEventListener=function(e,t){e in T||(T[e]=[]),T[e].push(t)},e.call=function(e,t){for(var i=H(e),n="",r=arguments.length,o=new Array(r>2?r-2:0),a=2;a bool: return is_valid -def get_cacheable_component( - component: "django_unicorn.views.UnicornView", -) -> "django_unicorn.views.UnicornView": +class CacheableComponent: """ - Converts a component into something that is cacheable/pickleable. + Updates a component into something that is cacheable/pickleable. Use in a `with` statement + or explicitly call `__enter__` `__exit__` to use. It will restore the original component + on exit. """ - component.request = None - - if component.extra_context: - component.extra_context = None - - if component.parent: - component.parent = get_cacheable_component(component.parent) - - for child in component.children: - if child.request is not None: - child = get_cacheable_component(child) - - try: - pickle.dumps(component) - except (TypeError, AttributeError, NotImplementedError, pickle.PicklingError) as e: - raise UnicornCacheError( - f"Cannot cache component '{type(component)}' because it is not picklable: {type(e)}: {e}" - ) from e - - return component + def __init__(self, component: "django_unicorn.views.UnicornView"): + self._state = {} + self.cacheable_component = component + + def __enter__(self): + components = [] + components.append(self.cacheable_component) + while components: + component = components.pop() + if component.component_id in self._state: + continue + if hasattr(component, "extra_context"): + extra_context = component.extra_context + component.extra_context = None + else: + extra_context = None + request = component.request + component.request = None + self._state[component.component_id] = (component, request, extra_context) + if component.parent: + components.append(component.parent) + for child in component.children: + components.append(child) + + for component, _, _ in self._state.values(): + try: + pickle.dumps(component) + except ( + TypeError, + AttributeError, + NotImplementedError, + pickle.PicklingError, + ) as e: + raise UnicornCacheError( + f"Cannot cache component '{type(component)}' because it is not picklable: {type(e)}: {e}" + ) from e + + def __exit__(self, *args): + for component, request, extra_context in self._state.values(): + component.request = request + if extra_context: + component.extra_context = extra_context def get_type_hints(obj) -> Dict: diff --git a/django_unicorn/views/__init__.py b/django_unicorn/views/__init__.py index da7595e1..29a60489 100644 --- a/django_unicorn/views/__init__.py +++ b/django_unicorn/views/__init__.py @@ -24,7 +24,7 @@ get_serial_enabled, get_serial_timeout, ) -from django_unicorn.utils import generate_checksum, get_cacheable_component +from django_unicorn.utils import CacheableComponent, generate_checksum from django_unicorn.views.action_parsers import call_method, sync_input from django_unicorn.views.objects import ComponentRequest from django_unicorn.views.utils import set_property_from_data @@ -207,7 +207,8 @@ def _process_component_request( cache = caches[get_cache_alias()] try: - cache.set(component.component_cache_key, get_cacheable_component(component)) + with CacheableComponent(component): + cache.set(component.component_cache_key, component) except UnicornCacheError as e: logger.warning(e) @@ -307,8 +308,9 @@ def _process_component_request( ) parent_component = component.parent + parent_res = res - if parent_component: + while parent_component: # TODO: Should parent_component.hydrate() be called? parent_frontend_context_variables = loads( parent_component.get_frontend_context_variables() @@ -325,10 +327,12 @@ def _process_component_request( component.parent_rendered(parent_dom) try: - cache.set( - parent_component.component_cache_key, - get_cacheable_component(parent_component), - ) + + with CacheableComponent(parent_component): + cache.set( + parent_component.component_cache_key, + parent_component, + ) except UnicornCacheError as e: logger.warning(e) @@ -340,7 +344,10 @@ def _process_component_request( } ) - res.update({"parent": parent}) + parent_res.update({"parent": parent}) + component = parent_component + parent_component = parent_component.parent + parent_res = parent return res diff --git a/docs/source/changelog.md b/docs/source/changelog.md index 4b836bd0..c9d6afac 100644 --- a/docs/source/changelog.md +++ b/docs/source/changelog.md @@ -1,5 +1,11 @@ # Changelog +## v0.49.3 + +- Fix: Support nested children past the first level ([#476](https://github.com/adamghill/django-unicorn/pull/507). + +[All changes since 0.49.2](https://github.com/adamghill/django-unicorn/compare/0.49.2...0.49.3). + ## v0.49.2 - Fix: Calling methods with a model typehint would fail after being called multiple times ([#476](https://github.com/adamghill/django-unicorn/pull/476) by [stat1c-void](https://github.com/stat1c-void)). diff --git a/example/coffee/management/commands/import_flavors.py b/example/coffee/management/commands/import_flavors.py index b2459b4f..ade34aa3 100644 --- a/example/coffee/management/commands/import_flavors.py +++ b/example/coffee/management/commands/import_flavors.py @@ -2,7 +2,7 @@ from django.core.management.base import BaseCommand, CommandError -from ...models import Flavor +from ...models import Favorite, Flavor class Command(BaseCommand): @@ -20,3 +20,4 @@ def handle(self, *args, **options): parent = Flavor.objects.filter(name=parent_name).first() flavor = Flavor(name=name, label=label, parent=parent) flavor.save() + Favorite.objects.create(flavor=flavor) diff --git a/example/coffee/migrations/0006_favorite.py b/example/coffee/migrations/0006_favorite.py new file mode 100644 index 00000000..6d5de550 --- /dev/null +++ b/example/coffee/migrations/0006_favorite.py @@ -0,0 +1,22 @@ +# Generated by Django 3.2.16 on 2023-02-14 00:02 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('coffee', '0005_auto_20221110_0400'), + ] + + operations = [ + migrations.CreateModel( + name='Favorite', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_favorite', models.BooleanField(default=False)), + ('flavor', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='coffee.flavor')), + ], + ), + ] diff --git a/example/coffee/models.py b/example/coffee/models.py index 31d80ab4..14a13c9e 100644 --- a/example/coffee/models.py +++ b/example/coffee/models.py @@ -21,6 +21,11 @@ def __str__(self): return self.name +class Favorite(models.Model): + is_favorite = models.BooleanField(default=False) + flavor = models.OneToOneField(Flavor, on_delete=models.CASCADE) + + class Taste(models.Model): name = models.CharField(max_length=255) flavor = models.ManyToManyField(Flavor) diff --git a/example/unicorn/components/nested/favorite.py b/example/unicorn/components/nested/favorite.py new file mode 100644 index 00000000..b472daf1 --- /dev/null +++ b/example/unicorn/components/nested/favorite.py @@ -0,0 +1,15 @@ +from django_unicorn.components import UnicornView +from example.coffee.models import Favorite + + +class FavoriteView(UnicornView): + model: Favorite = None + is_editing = False + + def updated(self, name, value): + if not self.model: + self.model = Favorite(flavor_id=self.parent.model.id, is_favorite=value) + + self.model.save() + self.parent.is_updated_by_child = value + self.parent.parent.favorite_count += 1 if value else -1 diff --git a/example/unicorn/components/nested/row.py b/example/unicorn/components/nested/row.py index bf31a7c9..07ad8434 100644 --- a/example/unicorn/components/nested/row.py +++ b/example/unicorn/components/nested/row.py @@ -5,6 +5,7 @@ class RowView(UnicornView): model: Flavor = None is_editing = False + is_updated_by_child = False def edit(self): self.is_editing = True diff --git a/example/unicorn/components/nested/table.py b/example/unicorn/components/nested/table.py index a22712d1..b2cdd449 100644 --- a/example/unicorn/components/nested/table.py +++ b/example/unicorn/components/nested/table.py @@ -7,6 +7,7 @@ class TableView(UnicornView): original_name = None flavors = Flavor.objects.none() is_editing = False + favorite_count = 0 def edit(self): self.is_editing = True @@ -26,8 +27,20 @@ def mount(self): self.load_table() def load_table(self): - self.flavors = Flavor.objects.all()[10:20] + self.flavors = Flavor.objects.select_related("favorite").all()[10:20] + self.favorite_count = sum( + [ + 1 + for f in self.flavors + if hasattr(f, "favorite") and f.favorite.is_favorite + ] + ) + + def set_unedit(c): + if hasattr(c, "is_editing"): + c.is_editing = False + for cc in c.children: + set_unedit(cc) for child in self.children: - if hasattr(child, "is_editing"): - child.is_editing = False + set_unedit(child) diff --git a/example/unicorn/templates/unicorn/nested/favorite.html b/example/unicorn/templates/unicorn/nested/favorite.html new file mode 100644 index 00000000..5fd1385a --- /dev/null +++ b/example/unicorn/templates/unicorn/nested/favorite.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/example/unicorn/templates/unicorn/nested/row.html b/example/unicorn/templates/unicorn/nested/row.html index 97cfe565..2afa0c77 100644 --- a/example/unicorn/templates/unicorn/nested/row.html +++ b/example/unicorn/templates/unicorn/nested/row.html @@ -1,33 +1,37 @@ +{% load unicorn %} - - {% if is_editing %} - - {% else %} - {{ model.name }} - {% endif %} - - - {% if is_editing %} - - {% else %} - {{ model.label }} - {% endif %} - - - {% if is_editing %} - - {% elif model.datetime %} - {{ model.datetime }} - {% else %} - n/a - {% endif %} + + {% if is_editing %} + + {% else %} + {{ model.name }} + {% endif %} + + + {% if is_editing %} + + {% else %} + {{ model.label }} + {% endif %} + + + {% if is_editing %} + + {% elif model.datetime %} + {{ model.datetime }} + {% else %} + n/a + {% endif %} + + + {% unicorn 'nested.favorite' key=model.favorite.id model=model.favorite %} - {% if is_editing %} - - - {% else %} - - {% endif %} + {% if is_editing %} + + + {% else %} + + {% endif %} - \ No newline at end of file + diff --git a/example/unicorn/templates/unicorn/nested/table.html b/example/unicorn/templates/unicorn/nested/table.html index 055d0202..092f22d8 100644 --- a/example/unicorn/templates/unicorn/nested/table.html +++ b/example/unicorn/templates/unicorn/nested/table.html @@ -16,6 +16,8 @@

{{ name }}

+
Favorite count: {{ favorite_count }}
+ {% unicorn 'nested.filter' parent=view %} @@ -24,6 +26,7 @@

{{ name }}

+ diff --git a/package.json b/package.json index c83103e6..96787e7a 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.49.2", + "version": "0.49.3", "name": "django-unicorn", "scripts": { "build": "npx rollup -c", diff --git a/tests/components/test_component.py b/tests/components/test_component.py index dc0c3846..e3aa30ef 100644 --- a/tests/components/test_component.py +++ b/tests/components/test_component.py @@ -161,6 +161,17 @@ def test_get_context_data(component): assert isinstance(context_data.get("get_name"), types.MethodType) +def test_get_context_data_component(): + class TestComponent(UnicornView): + pass + + component = TestComponent(component_id="asdf1234", component_name="hello-world") + actual = component.get_context_data() + + assert actual["unicorn"] + assert actual["unicorn"]["component"] == component + + def test_get_context_data_component_id(): class TestComponent(UnicornView): pass diff --git a/tests/templates/test_component_child_implicit.html b/tests/templates/test_component_child_implicit.html new file mode 100644 index 00000000..15faddd1 --- /dev/null +++ b/tests/templates/test_component_child_implicit.html @@ -0,0 +1,4 @@ +
+ ==child== + has_parent:{{ has_parent }} +
diff --git a/tests/templates/test_component_parent_implicit.html b/tests/templates/test_component_parent_implicit.html new file mode 100644 index 00000000..660906ab --- /dev/null +++ b/tests/templates/test_component_parent_implicit.html @@ -0,0 +1,6 @@ +{% load unicorn %} + +
+ --parent-- + {% unicorn 'tests.templatetags.test_unicorn_render.FakeComponentChildImplicit' %} +
diff --git a/tests/templates/test_parent_implicit_template.html b/tests/templates/test_parent_implicit_template.html new file mode 100644 index 00000000..855da672 --- /dev/null +++ b/tests/templates/test_parent_implicit_template.html @@ -0,0 +1,3 @@ +{% load unicorn %} + +{% unicorn 'tests.templatetags.test_unicorn_render.FakeComponentParentImplicit' %} diff --git a/tests/templatetags/test_unicorn_render.py b/tests/templatetags/test_unicorn_render.py index c9dd0a31..1c67951d 100644 --- a/tests/templatetags/test_unicorn_render.py +++ b/tests/templatetags/test_unicorn_render.py @@ -30,10 +30,21 @@ class FakeComponentParent(UnicornView): template_name = "templates/test_component_parent.html" +class FakeComponentParentImplicit(UnicornView): + template_name = "templates/test_component_parent_implicit.html" + + class FakeComponentChild(UnicornView): template_name = "templates/test_component_child.html" +class FakeComponentChildImplicit(UnicornView): + template_name = "templates/test_component_child_implicit.html" + + def has_parent(self): + return self.parent is not None + + class FakeComponentKwargs(UnicornView): template_name = "templates/test_component_kwargs.html" hello = "world" @@ -127,6 +138,26 @@ def test_unicorn_template_renders_with_parent_and_child_with_templateview(client assert '
Name Label DatetimeFavorite Action