Search

This weekend, after looking at how cmsdesignresource.com handles user reviews and ratings, I started thinking about how this could be implemented in Symphony. Here’s what I’ve got so far:

Sections

First we need a section that contains the individual item Reviews submitted by viewers. It always has one Rating Average field, and always one or more Rating Input fields.

Second, we have another section that contains the Items to be reviewed. It contains a Rating Link field which can be directed to the Rating Average field in the Reviews section.

Fields

The Rating Input field stores the rating data as a percentage. When data is saved to this field it would automatically tell the Rating Average field to recalculate its value.

The Rating Average field is used to generate an average of each Rating Input field per entry. When this field is saved, it will tell the Rating Link to recalculate its value. It also controls how the Rating Input fields will be displayed: either as a percentage, or as any number of stars between 2-10.

In the field settings form, there would be an option like:

[ ] Show as percentage or as [ 5] stars.

These setting will both be used to display the field in the publish area, and also to affect the output XML.

The Rating Link works like a normal link, except that it also stores an average of all entries it is linked to. This way you can sort by the overall rating of an Item. In its XML output it provides a summary of each Rating Average and Rating Input field in Reviews section.

Thoughts

Anyhow, that’s my plan for now, I hope to get some time to do this in the next couple of weeks. I also hope it all made sense!

Would the Rating Link, when it stores “an average of all entries it is linked to,” do that on a per-rating input-field basis? i.e. could an item show not only an overall average but also the average from each “category” (rating input)?

The Rating Link field would only show the average of each Rating Average field. This is because you can only have one column per field in Symphony.

Of course, in the XML it would display the average of each Rating Average and Rating Input field. Otherwise it wouldn’t be very useful. :)

Actually, it might as well have a full summary when you edit the item being reviewed, since there should be plenty of room for it.

Cool. Look forward to seeing what you come up with…

We’ve just implemented something similar, (albeit just with a single rating for each review, 1—5) but didn’t consider making it into a field type. We store the 1—5 value for each review, then schedule a script to update the average into a Number field in the reviewed item entry.

Having this as a native field type would be splendiferous!

Could the new average also be returned in an Event? So when updating (perhaps via a clicky rating stars AJAX script) an Event saves the rating and returns the new average to pin the stars to? I suppose this could be achieved with a DS to return the average on the same page.

In my opinion, it is bad practice to store derived data. Instead of storing the percentage, average, or number of stars, store the amount of votes and the cumulative rating.

Example for a 1-10 scale rating for one item: In the DB you have 18 votes with a cumulative rating of 130. This enables you to calculate (with XSLT) that the average vote is 130/18 = 7,222 = 72,22% = 7 stars. With every new vote, you increase the vote count by 1 and add the rating to the cumulative.

I don’t see the added benefit of making this a specific field type. Storing it as a number would allow everything you need, or not?

The benefit of a new field type is that these calculations don’t need to be performed by the developer. If we have Items and Reviews, the event to save a Review would also then need to update the total/cumulative values in the Item. Rowan’s suggestion means it’s trivial to add additional voting criteria within a single review (i.e. not just a single 1—5, but scoring other criteria as well).

Behind the scenes Rowan’s suggested fields could work in the same manner (storing total votes and cumulative) but I’m of the opinion that pre-processing these values is the correct way to go. Without storing the average, how would one go about querying the entries and ordering by highest rated?

Isn’t something possible like this in XSLT: SELECT cumulative/votes AS average FROM ratings ORDER BY average DESC

I do agree that it would be easier from the back-end to see the calculated average, though. I think it would be great if it were possible to add additional columns to the administration screen based on calculations and input of other screens, similar to the reflection field extension but not actually storing the data (which is redundant).

@carsten while that may have some advantages, it would also run the risk of being too slow to be of any use.

Any updates on a ratings/review system?

It's certainly possible within Symphony already, but probably will require a couple custom datasources. I'm going to be in need of something like this as well in the future, but will have to hire/beg someone to help me. If I ever do get something working, I'll share it on the site, but I don't have any particular timeline right now.

Is there already an update about this system? I'm also interested in this rating system for my site.

Give that this thread started more than two years ago, I imagine the concept never had enough traction to warrant development beyond the idea.

@nickdunn, that's true, but as TheJester12 said in his post before, Symphony maybe can handle the rating function.
Do you know if there is a way to rate for example sites?

I suppose it depends entirely on the type of rating you need, things like:

  • do users need to be logged in?
  • is it a star rating, if so, what is the range?
  • do you allow for half stars?
  • do you want to use the mean, median or mode?
  • if rating time-limited, does it switch off after a time?
  • can a user rate more than once, if not, how are you tracking who they are?
  • will you need to sort entries based on this rating, or just display it (i.e. does it need to be calculated and stored at the MySQL paging/sorting level, or can it be determined by XSLT calculations)?

Symphony definitely provides the tools to build a rating system (with a little customisation here and there), since at its core it's storing and counting entry values. My guess is that this was never made into an extension because each rating implementation is likely to have nuances and subtleties that no extension can be generic enough to accommodate.**

I am now working on a project that requires a rating system and it's a great exercise to make the setup work.

Here's my approach, in case you want to share thoughts:

I decided to think "simple", since I am a newbie with Symphony, and I like the way it's coming out.

The rating options:

  1. Created a section "Rating options" with a single text input field "Rating", then created my entries. In my case - numbers from 1 to 10. But I can create 5 and then add more, it won't break my templates. One thing, though: I started by creating the lowest entry (1) and proceeded without breaking the sequence. I use Sort by System ID in the data source.

  2. Created a data source for the above section - "Rating options". Set the sorting by System ID and selected "descending".

The content to rate:

  1. Content - my setup has a section called "Stories" that stores story entries. I am rating each story. I've got two data sources for this section - "Stories" (to use for a list of stories) and "Story" (for the individual story). The output options for both data sources is set to use "System ID" field so that the parameters $ds-stories and $ds-story are added to the pool. DS Story also filters by $title, which is a required parameter and I need it for the navigation.

    The rating "system":

  2. Created a section "Ratings" with input fields "Story id" and "Rating". I will eventually add a "Rater id" field but haven't got to that part yet. This section will be populated with the ratings - tied to individual stories and submitted by the raters.

  3. Created a data source "Ratings" for the above section. It filters by {$ds-story:$ds-stories}.

The front-end:

  1. Created the front-end pages. I'll concentrate on the individual story page. The setup includes the "Ratings options" and "Ratings" data sources.

  2. Created an image to use as a rating option with both states (top and bottom). In my case, the image has a width of 26px (the width of the .png file), and the height of each state is 22px (the height is only needed for the css). My state is a square 22x22px but I have 2px transparent empty space on each side, that's why the width is 26px.

  3. Created the templates. I decided to keep the rating stuff in a separate .xsl, where I've got two templates:

Rating module - that's the form the users will use to rate the stories:

<xsl:template name="ratingModule">
        <div id="ratingModule">
        <form id="ratingForm" name="ratingForm" method="post" action="" enctype="multipart/form-data" class="form">
                <fieldset class="fieldset">
                    <ul class="ratingLinks" style="width: {26*count(rating-options/entry)}px" onclick="document.ratingForm.submit.click();">
                        <xsl:apply-templates select="rating-options/entry" />
                    </ul>
                </fieldset>
                <div class="clear"></div>
                <input name="fields[story-id]" type="hidden" value="{story/entry/@id}" />
                <input id="submit" name="action[submit-rating]" type="submit" value="Submit" class="submit" />
            </form>
        </div>
</xsl:template>

<xsl:template match="rating-options/entry">
    <li><a style="width: {26*(rating)}px">
        <label class="label" style="left: {26*count(/data/rating-options/entry)+10}px">Rate: <xsl:value-of select="rating" />/<xsl:value-of select="count(/data/rating-options/entry)" /></label>
        <input type="radio" name="fields[rating]" class="option" value="{rating}" />
    </a></li>
</xsl:template>

Story current rating - I use this one to show the current rating of a story. The image I use here is smaller, because this rating will be shown in a list next to each story. Sizes here are 18px .png width and 16px state height.

EDIT: I'm still struggling with this template to make it work when there's a list of stories.

EDIT2: Here is the fixed version of the template. Must be called from inside the entry being rated (in my case stories/entry for the list of stories and story/entry for the individual story view).

<xsl:template name="storyCurrentRating">
        <div id="storyCurrentRating">
            <ul class="storyCurrentRating" style="width: {18*count(/data/rating-options/entry)}px">
                <li><a style="width: {18*(format-number(round(100*(sum(/data/ratings/entry[story-id = current()/@id]/rating/@handle) div count(/data/ratings/entry[story-id = current()/@id]))) div 100, '##.00'))}px;"><xsl:value-of select="format-number(round(100*(sum(/data/ratings/entry[story-id = current()/@id]/rating/@handle) div count(/data/ratings/entry[story-id = current()/@id]))) div 100, '##.00')" />/<xsl:value-of select="count(/data/rating-options/entry)" /></a></li>
            </ul>
            <xsl:choose>
                <xsl:when test="count(/data/ratings/entry[story-id = current()/@id]) = '0'">
                    <p class="notice">( no votes submitted yet )</p>
                </xsl:when>
                <xsl:when test="count(/data/ratings/entry[story-id = current()/@id]) = '1'">
                    <p class="notice">( <xsl:value-of select="format-number(round(100*(sum(/data/ratings/entry[story-id = current()/@id]/rating/@handle) div count(/data/ratings/entry[story-id = current()/@id]))) div 100, '##.00')" /> / <xsl:value-of select="count(/data/rating-options/entry)" /> with <xsl:value-of select="count(/data/ratings/entry[story-id = current()/@id])" /> vote )</p>
                </xsl:when>
                <xsl:otherwise>
                    <p class="notice">( <xsl:value-of select="format-number(round(100*(sum(/data/ratings/entry[story-id = current()/@id]/rating/@handle) div count(/data/ratings/entry[story-id = current()/@id]))) div 100, '##.00')" /> / <xsl:value-of select="count(/data/rating-options/entry)" /> with <xsl:value-of select="count(/data/ratings/entry[story-id = current()/@id])" /> votes )</p>
                </xsl:otherwise>
            </xsl:choose>
        </div>
</xsl:template>

As you see, all calculations are made with xslt. I am newbie with it too, and the way I've setup the templates is probably not the best approach, but I am testing it now and it calculates correctly. I am formatting the number to include only one digit after the point, and also making sure it's rounding correctly.

In the current rating template I am using only a single list item. It's width is calculated based on the total rating, so the rating will be visually shown - partially coloured options.

I am not going to post styles, but the idea is that the ul's width is based on the number of options available, with the .png repeating horizontally. The a is positioned absolutely relative to the ul, thus stacking the options one above the other (1 being on top and 10 in the bottom) and its width varies based on the rating it represents. On hover, the a gets the same .png as background, but this time showing full color and covering the ul's background.

I am using some JavaScript to allow the form submit onclick, without the need of the button. However, the templates do not rely on it. In case the user doesn't have JS enabled, the submit button will be visible and the rating can be submitted by first selecting an option and then clicking submit. (if JS is enabled a hidden class is added to the button and a transparent class to the radio options).

That's it, that's what I've got setup for now. Of course I'm missing a fundamental feature - permitting only one vote per rater. I'll be using the members extension so I intend to tie them somehow. But one step at a time!

Please, help me out with the XSL...

If I have this XML:

<stories>
    <entry id="1">
        ...        
    </entry>
    <entry id="2">
        ...        
    </entry>
</stories>
<ratings>
    <entry>
        <story-id handle="1">1</story-id>
        <rating handle="4">4</rating>
    </entry>
    <entry>
        <story-id handle="1">1</story-id>
        <rating handle="6">6</rating>
    </entry>
    <entry>
        <story-id handle="2">2</story-id>
        <rating handle="9">9</rating>
    </entry>
</ratings>

How to modify this template to calculate only the relevant ratings for each story, and not all ratings?

<xsl:template name="storyCurrentRating">
        <div id="storyCurrentRating">
            <ul class="storyCurrentRating" style="width: {18*count(/data/rating-options/entry)}px">
                <li><a style="width: {18*(format-number(round(100*(sum(/data/ratings/entry/rating/@handle) div count(/data/ratings/entry))) div 100, '##.0'))}px;"><xsl:value-of select="format-number(round(100*(sum(/data/ratings/entry/rating/@handle) div count(/data/ratings/entry))) div 100, '##.0')" />/<xsl:value-of select="count(/data/rating-options/entry)" /></a></li>
            </ul>
        </div>
</xsl:template>

Should I use a variable to first calculate the total number of ratings per story (count) and another variable for the sum of the ratings for that story (sum) and then use these in the template?

Please help me write this down, as I have been trying in vain for hours...

How to modify this template to calculate only the relevant ratings for each story, and not all ratings?

I believe you want to look into Data Source Chaining for something like this.

Should I use a variable to first calculate the total number of ratings per story (count) and another variable for the sum of the ratings for that story (sum) and then use these in the template?

If you have Include a count of entries in associated sections checked in your Stories datasource, you should get a number of ratings for each entry.

I don't see the XML for the rating-options, but it looks like you need a predicate to test the entry ID before performing calculations. Something like this (untested) code:

<xsl:template match="/data">
    <xsl:apply-templates select="stories/entry" />
</xsl:template>

<xsl:template match="stories/entry">
    <div id="storyCurrentRating">
        <ul class="storyCurrentRating" style="width: {18*count(/data/rating-options/entry)}px">
            <li><a style="width: {18*(format-number(round(100*(sum(/data/ratings/entry/[@story-id = current()/@id]rating/@handle) div count(/data/ratings/entry[@story-id = current()/@id]))) div 100, '##.0'))}px;"><xsl:value-of select="format-number(round(100*(sum(/data/ratings/entry/[@story-id = current()/@id]rating/@handle) div count(/data/ratings/entry[@story-id = current()/@id]))) div 100, '##.0')" />/<xsl:value-of select="count(/data/rating-options/entry)" /></a></li>
        </ul>
    </div>
</xsl:template>

Actually I am using output parameters and the ratings are working correctly on the individual story page - the correct average rating is calculated and displayed with this template.

But I have a hard time with the list of stories, because the template doesn't take into account that there are more than one story on the page and that it first needs to test if (ratings/entry/story-id/@handle)=(stories/entry/@id). But I can't figure out how to write this in XSL.

Only ratings relevant to the stories in the list are outputed, so I don't think there is anything not needed in the XML to filter further with a DS...

EDIT: Aaah, didn't see your post, bauhouse. Will try this right away.

Create an account or sign in to comment.

Symphony • Open Source XSLT CMS

Server Requirements

  • PHP 5.3-5.6 or 7.0-7.3
  • PHP's LibXML module, with the XSLT extension enabled (--with-xsl)
  • MySQL 5.5 or above
  • An Apache or Litespeed webserver
  • Apache's mod_rewrite module or equivalent

Compatible Hosts

Sign in

Login details