Forum Discussion

smontero's avatar
smontero
Helper
3 years ago

TemplateModel properties/keys

Hello everyone I am trying to see the page.context.thread.board properties because I did not find anything on the documentation.
Basically what I am currently doing is logging this value into console but I got lithium.eval.velocity.ThreadTemplateModel@7a6fcf85 .

I have tried with javascript Object.keys function but I am getting the string into an array and no relevant keys/properties.

My main goal is to find a way to get whole thread kudos count without using v1 API call. Is there any other way to achieve that? I tried with v2 but is getting the topicMessage count only.

7 Replies

  • MattV's avatar
    MattV
    Khoros Staff
    3 years ago

    What a fun rabbit hole you've found!

    Sometimes you will stumble upon an undocumented Freemarker context object. First step is to let our Documentation team know ๐Ÿ™‚

    Next, for the impatient and curious, is to figure out what is available to us via Freemarker. The challenge with freemarker, is that an object can be multiple datatypes, and we don't know which it is or which one we want. And freemarker will throw an exception and stop processing if you get it wrong.

    I've gone down this path before, and this is imperfect, but this is the code I was using to help me figure out the objects available on the message context objects.

    <#assign context = "env.context.message." />
    <#list env.context.message as key,value>
    <#assign val = (context+key)?eval!"" />
    <#if val?is_enumerable>
     <#assign val = "(sequence)" />
    <#elseif val?is_method>
     <#assign val = "(method)" />
    <#elseif val?is_boolean>
     <#assign val = val?c />
    <#elseif val?is_hash />
     <#assign val = "(extended hash)" />
    <#else>
     <#assign val = "(unknown)" />
    </#if>
    <#if key != "product">
    ${key?index} - ${key} - ${val}<p>
    </#if>
    </#list>
    <ul>
    
    <#list env.context.message?keys as key>
    	<li>${key?index} - ${key}
    	<#if key != "class">
    		<ul>
    		<#assign obj = (context+key)?eval!"" />
    		<#if !obj?is_string && !obj?is_enumerable>
    			<#list obj?keys as key>
    				${key}
    			</#list>
    		<#else>
    			${obj?string}
    		</#if>
    		</ul>
    	</#if>
    	</li>
    </#list>
    </ul>

     

  • IanKl's avatar
    IanKl
    Khoros Alumni (Retired)
    2 years ago

    smontero Are you saying that you want to get the total kudos of both the topic itself and the kudos from all the replies? 

  • I have found that the FreeMarker context objects that refer to community data often correlate with API v1, so for your example, you are looking at a thread, look at a thread object here for example:

    https://community.khoros.com/restapi/v1/boards/id/technology/threads?restapi.response_format=json

    here's an extract of the v1 response:

     

    "threads": {
    	"thread": [
    	{
    		"type": "thread",
    		"href": "/threads/id/729728",
    		"messages": {},
    		"solutions": {},
    		"title": {},
    		"board": {
    			"type": "board",
    			"href": "/boards/id/technology"
    		},
    		"id": {
    			"type": "int",
    			"$": 729728
    		},
    		"interaction_style": {
    			"type": "string",
    			"$": "board"
    		}
    	},
    	...more thread objects
    }

     

     

    notice that "board" part?

     

    		"board": {
    			"type": "board",
    			"href": "/boards/id/technology"
    		},

     

    So the first thing i would try is to access properties available in the API v1 response, in your case page.context.thread.board.@href (don't be fooled by the JSON format it can be misleading as the real format is actually XML, @ stands for "attribute" as in an XML attribute, see the "real" v1 data by removing that URL parameter ?restapi.response_format=json from the query link above). Both type and href here are attributes, the actual XML values are identified by the "$" property when viewing it as JSON...

    ah well, why describe it when i can simply show the real XML ๐Ÿ˜ƒ

    <board type="board" href="/boards/id/technology"/>


    So as tempting as it seems, I think that thread context object probably does not contain anything useful but the "href" attribute (which you could use to make further API v1 queries, which is definitely not the recommended way...).

    The next thing you can try is to query a board via API v1 and see what other properties a board has and try to access those through the context object, here is a full board API v1 response:

    {
        "response": {
            "status": "success",
            "board": {
                "type": "board",
                "href": "\\/boards\\/id\\/technology",
                "interaction_style": {
                    "type": "string",
                    "$": "board"
                },
                "blog": {
                    "type": "boolean",
                    "$": false
                },
                "user_created": {
                    "type": "boolean",
                    "$": false
                },
                "owner": {
                    "type": "user",
                    "null": true,
                    "$": null
                },
                "id": {
                    "type": "string",
                    "$": "technology"
                },
                "short_title": {
                    "type": "string",
                    "$": "Communities Product Discussions"
                },
                "title": {
                    "type": "string",
                    "$": "Khoros Community Product Discussions"
                },
                "description": {
                    "type": "string",
                    "$": "Have questions about the Khoros Community product? Ask them here."
                }
            }
        }
    }

     

    So even if the thread context object's board property contains all of the above, none of it would give you the data you want (Kudos).

    Useful data you can probably access through the context object are

    board.id

    board.title

    board.short_title

    board.description

    board.interaction_style

     

    but not much beyond that...

    note that this is all speculation as i don't have time to dig through old component code where i certainly used this "hidden" (it's not so hidden =D)/undocumented part of the context object.

     

  • To add to the general topic of opaque FreeMarker context objects: I know the pain of debugging those and i have come up with something like an "inspector" for FreeMarker objects (the code of which I'm not able to share [too convoluted/complicated], but I gladly run any context objects through it if you ask nicely ๐Ÿ˜˜). The code MattV posted above goes in the same directions, but you'll eventually hit roadblocks, promise, been there, done that...

    Back to it, the following example output of my "inspector" is for the coreNode context object:

     

     

     

    "coreNode": {
    	"name": "lithium.eval.velocity.CommunityTemplateModel@d7a4737b",
    	"type": "string, hash, hash_ex",
    	"properties": {
    		"parent": "null",
    		"shortTitle": "redacted",
    		"permissions": "lithium.eval.velocity.NodePermissionsTemplateModel@2acc5ec1",
    		"ancestorNodes": {
    			"type": "sequence, enumerable, indexable",
    			"values": []
    		},
    		"id": "redacted",
    		"ancestors": {
    			"type": "sequence, enumerable, indexable",
    			"values": []
    		},
    		"settings": "lithium.eval.velocity.SettingsTemplateModel@88b91bb",
    		"nodeType": "community",
    		"nodeTypeName": "Community",
    		"webUi": "lithium.eval.velocity.CoreNodeWebUiTemplateModel@1de296b0",
    		"displayId": "redacted",
    		"nodeId": 1,
    		"nodeCreationDate": "02.07.2013",
    		"description": "redacted",
    		"membership": "lithium.eval.velocity.MembershipTemplateModel@3e413c48",
    		"title": "redacted",
    		"urls": "lithium.eval.velocity.urls.CommunityUrlsTemplateModel@6a137472",
    		"authenticationUrls": "lithium.eval.velocity.urls.AuthenticationManagerUrlsTemplateModel@f62e1e0"
    	},
    	"methods": {
    		"getClass()": "method,indexable",
    		"getWebUi()": "method,indexable",
    		"getNodeCreationDate()": "method,indexable",
    		"getAncestorNodes()": "method,indexable",
    		"getMembership()": "method,indexable",
    		"getTitle()": "method,indexable",
    		"getNodeType()": "method,indexable",
    		"getRootCategory()": "method,indexable",
    		"rawNodeIsA()": "method,indexable",
    		"getNodeId()": "method,indexable",
    		"getPermissions()": "method,indexable",
    		"rawNodeAsA()": "method,indexable",
    		"getAncestors()": "method,indexable",
    		"hasAncestor()": "method,indexable",
    		"getSettings()": "method,indexable",
    		"getShortTitle()": "method,indexable",
    		"getId()": "method,indexable",
    		"getDescription()": "method,indexable",
    		"getNodeCreationDateForUser()": "method,indexable",
    		"getNodeTypeName()": "method,indexable",
    		"hashCode()": "method,indexable",
    		"getUrls()": "method,indexable"
    	}
    }

     

     

     

     

    I always find it interesting to look at that and compare it to the official docs ๐Ÿ˜‰...

    Let's do the same for your page object:

     

     

     

    {
    	"name": "lithium.web2.data.page.PageTemplateModel@6c1dbccc",
    	"type": "string, hash, hash_ex",
    	"methods": {
    		"getVersion()": "method,indexable",
    		"getClass()": "method,indexable",
    		"getName()": "method,indexable",
    		"getContent()": "method,indexable",
    		"getInteractionStyle()": "method,indexable",
    		"hashCode()": "method,indexable",
    		"equals()": "method,indexable",
    		"toString()": "method,indexable"
    	},
    	"properties": {
    		"rtl": false,
    		"version": "v2.0",
    		"content": "lithium.web2.data.page.PageContentTemplateModel@c8a65f5",
    		"name": "BizAppsPage",
    		"context": "PageContextTemplateModel{threadTemplateModel=null, messageTemplateModel=null, userTemplateModel=null, restV2EntityTemplateModel=null}",
    		"interactionStyle": "none"
    	}
    }

     

     

     

    and for page.context:

     

     

    {
    	"name": "PageContextTemplateModel{threadTemplateModel=lithium.eval.velocity.ThreadTemplateModel@db22dc20, messageTemplateModel=lithium.eval.velocity.MessageTemplateModel@5a60cda0, userTemplateModel=null, restV2EntityTemplateModel=null}",
    	"type": "string, hash, hash_ex",
    	"methods": {
    		"getClass()": "method,indexable",
    		"getUser()": "method,indexable",
    		"getMessage()": "method,indexable",
    		"getEntity()": "method,indexable",
    		"hashCode()": "method,indexable",
    		"equals()": "method,indexable",
    		"getThread()": "method,indexable",
    		"toString()": "method,indexable"
    	},
    	"properties": {
    		"thread": "lithium.eval.velocity.ThreadTemplateModel@db22dc20",
    		"message": "lithium.eval.velocity.MessageTemplateModel@5a60cda0",
    		"user": "null" // afaik only available on profile page
    	}
    }

     

     


    have to go level by level, now for context.page.thread:

     

     

    {
    	"name": "lithium.eval.velocity.ThreadTemplateModel@db22dc20",
    	"type": "string, hash, hash_ex",
    	"properties": {
    		"discussionStyle": "forum",
    		"topicMessage": "lithium.eval.velocity.MessageTemplateModel@78d2c42d",
    		"webUi": "lithium.eval.velocity.ThreadWebUiTemplateModel@1c5d086f",
    		"lastReply": "lithium.eval.velocity.MessageTemplateModel@1b2c0f44"
    	},
    	"methods": {
    		"getClass()": "method,indexable",
    		"getBoard()": "method,indexable",
    		"getTopicMessage()": "method,indexable",
    		"getWebUi()": "method,indexable",
    		"getLastReply()": "method,indexable",
    		"hashCode()": "method,indexable",
    		"equals()": "method,indexable",
    		"getDiscussionStyle()": "method,indexable",
    		"toString()": "method,indexable"
    	}
    }

     

     


    I'm not seeing a property "board" here, but maybe it's not enumerable? I see the method "getBoard()" though, so lets see if that returns something (EDIT: I did try to call page.context.thread.board directly and it returned the same, so it seems that this might be the reason it is not documented, because it does not even show up as a property when doing this kind of "code analysis"):

     

     

    {
    	"name": "lithium.eval.velocity.BoardTemplateModel@2fbe5e84",
    	"type": "string, hash, hash_ex",
    	"properties": {
    		"parent": "lithium.eval.velocity.CoreNodeTemplateModel@bf665dc6",
    		"shortTitle": "redacted",
    		"permissions": "lithium.eval.velocity.NodePermissionsTemplateModel@6923aecb",
    		"ancestorNodes": {
    			"type": "sequence, enumerable, indexable",
    			"values": []
    		},
    		"id": "redacted",
    		"ancestors": {
    			"type": "sequence, enumerable, indexable",
    			"values": []
    		},
    		"settings": "lithium.eval.velocity.SettingsTemplateModel@2eb40570",
    		"nodeType": "board",
    		"nodeTypeName": "Board",
    		"webUi": "lithium.eval.velocity.CoreNodeWebUiTemplateModel@ea82e1a",
    		"displayId": "redacted",
    		"nodeId": 197,
    		"nodeCreationDate": "11.11.2013",
    		"discussionStyle": "forum",
    		"description": "",
    		"membership": "lithium.eval.velocity.MembershipTemplateModel@7b20459e",
    		"title": "redacted"
    	},
    	"methods": {
    		"getClass()": "method,indexable",
    		"getWebUi()": "method,indexable",
    		"getNodeCreationDate()": "method,indexable",
    		"getAncestorNodes()": "method,indexable",
    		"getMembership()": "method,indexable",
    		"getTitle()": "method,indexable",
    		"getNodeType()": "method,indexable",
    		"getRootCategory()": "method,indexable",
    		"rawNodeIsA()": "method,indexable",
    		"getNodeId()": "method,indexable",
    		"getPermissions()": "method,indexable",
    		"rawNodeAsA()": "method,indexable",
    		"getDiscussionStyle()": "method,indexable",
    		"getAncestors()": "method,indexable",
    		"hasAncestor()": "method,indexable",
    		"getSettings()": "method,indexable",
    		"getShortTitle()": "method,indexable",
    		"getId()": "method,indexable",
    		"getDescription()": "method,indexable",
    		"getDiscussionStyleString()": "method,indexable",
    		"getNodeCreationDateForUser()": "method,indexable",
    		"getNodeTypeName()": "method,indexable",
    		"hashCode()": "method,indexable"
    	}
    }

     

     


    that should give you a decent idea of what is available in terms of properties and methods (which in most cases should never be called directly btw).

     

  • luk I think I speak for all of us when I say I don't care how complicated or convoluted your inspector is, I would still like to have to code to use on my own without having to bother you and wait for a reply. Is there a proprietary reason you don't want to share this? I would be very grateful....

  • luk's avatar
    luk
    Boss
    2 years ago

    Yes there is, the inspector makes use of a whole bunch of other stuff from our proprietary Khoros helper "framework" (if you want to call it like that), so the inspect function itself would not be helpful without all those additional methods it makes use of.

    Additionally, even if I were to extract all needed code to post it here, it might be considered a violation of the terms of this community which state that you can't:

    (2) reverse engineer, decompile, disassemble, disclose, or otherwise attempt to discover the source code, object code, or underlying structure, ideas, or algorithms of Khorosโ€™s platforms and applications
  • luk I'm pretty sure that you can ask if it were to be a violation. And I'm pretty sure that Khoros won't mind a tool like that. Do you want to sell it, I would love to get a copy.