001: /*
002: * $Id$ $Revision$ $Date$
003: *
004: * ==============================================================================
005: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
006: * use this file except in compliance with the License. You may obtain a copy of
007: * the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
013: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
014: * License for the specific language governing permissions and limitations under
015: * the License.
016: */
017: package wicket.extensions.rating;
018:
019: import wicket.Component;
020: import wicket.ResourceReference;
021: import wicket.ajax.AjaxRequestTarget;
022: import wicket.ajax.markup.html.AjaxFallbackLink;
023: import wicket.behavior.HeaderContributor;
024: import wicket.behavior.SimpleAttributeModifier;
025: import wicket.markup.html.WebMarkupContainer;
026: import wicket.markup.html.basic.Label;
027: import wicket.markup.html.list.Loop;
028: import wicket.markup.html.panel.Panel;
029: import wicket.model.IModel;
030: import wicket.model.Model;
031: import wicket.model.StringResourceModel;
032:
033: /**
034: * Rating component that generates a number of stars where a user can click on
035: * to rate something. Subclasses should implement
036: * {@link #onRated(int, AjaxRequestTarget)} to provide the calculation of the
037: * rating, and {@link #onIsStarActive(int)} to indicate whether to render an
038: * active star or an inactive star.
039: * <p>
040: * Active stars are the stars that show the rating, inactive stars are the left
041: * overs. E.G. a rating of 3.4 on a scale of 5 stars will render 3 active stars,
042: * and 2 inactive stars (provided that the {@link #onIsStarActive(int)} returns
043: * <code>true</code> for each of the first three stars).
044: * <p>
045: * Use this component in the following way:
046: *
047: * <pre>
048: * add(new RatingPanel("rating", new PropertyModel(rating, "rating"), 5)
049: * {
050: * protected boolean onIsStarActive(int star)
051: * {
052: * return rating.isActive(star);
053: * }
054: *
055: * protected void onRated(int rating, AjaxRequestTarget target)
056: * {
057: * rating1.addRating(rating);
058: * }
059: * });
060: * </pre>
061: *
062: * The user of this component is responsible for creating a model that supplies
063: * a Double (or Float) value for the rating message, however the rating panel
064: * doesn't necessarily have to contain a float or number rating value.
065: * <p>
066: * Though not obligatory, you could also supply a value for the number of votes
067: * cast, which allows the component to render a more complete message in the
068: * rating label.
069: *
070: * <h2>Customizing the rating value and label</h2>
071: * To customize the rating value, one should override the
072: * {@link #newRatingLabel(String, IModel, IModel)} method and create another
073: * label instead, based on the provided models. If you do so, and use another
074: * system of rating than returning a Float or Double, then you should also
075: * customize the rating resource bundle to reflect your message. The default
076: * resource bundle assumes a numeric value for the rating.
077: *
078: * <h2>Resource bundle</h2>
079: * This component uses two types of messages: rating.simple and rating.complete.
080: * The first message is used when no model is given for the number of cast
081: * votes. The complete message shows the text 'Rating xx.yy from zz votes'.
082: *
083: * <pre>
084: * rating.simple=Rated {0,number,#.#}
085: * rating.complete=Rated {0,number,#.#} from {1,number,#} votes
086: * </pre>
087: *
088: * <h2>Customizing the star images</h2>
089: * To customize the images shown, override the {@link #getActiveStarUrl(int)}
090: * and {@link #getInactiveStarUrl(int)} methods. Using the iteration parameter
091: * it is possible to use a different image for each star, creating a fade effect
092: * or something similar.
093: *
094: * @author Martijn Dashorst
095: */
096: public abstract class RatingPanel extends Panel {
097: /**
098: * Renders the stars and the links necessary for rating.
099: */
100: private final class RatingStarBar extends Loop {
101: /** For serialization. */
102: private static final long serialVersionUID = 1L;
103:
104: private RatingStarBar(String id, IModel model) {
105: super (id, model);
106: }
107:
108: protected void populateItem(LoopItem item) {
109: // Use an AjaxFallbackLink for rating to make voting work even
110: // without Ajax.
111: AjaxFallbackLink link = new AjaxFallbackLink("link") {
112: private static final long serialVersionUID = 1L;
113:
114: public void onClick(AjaxRequestTarget target) {
115: LoopItem item = (LoopItem) getParent();
116:
117: // adjust the rating, and provide the target to the subclass
118: // of our rating component, so other components can also get
119: // updated in case of an AJAX event.
120:
121: onRated(item.getIteration() + 1, target);
122:
123: // if we process an AJAX event, update this panel
124: if (target != null) {
125: target.addComponent(RatingPanel.this
126: .get("rater"));
127: }
128: }
129:
130: public boolean isEnabled() {
131: return !((Boolean) hasVoted
132: .getObject(RatingPanel.this ))
133: .booleanValue();
134: }
135: };
136:
137: int iteration = item.getIteration();
138:
139: // add the star image, which is either active (highlighted) or
140: // inactive (no star)
141: link
142: .add(new WebMarkupContainer("star")
143: .add(new SimpleAttributeModifier(
144: "src",
145: (onIsStarActive(iteration) ? getActiveStarUrl(iteration)
146: : getInactiveStarUrl(iteration)))));
147: item.add(link);
148: }
149: }
150:
151: /** For serialization. */
152: private static final long serialVersionUID = 1L;
153:
154: /**
155: * Star image for no selected star
156: */
157: public static final ResourceReference STAR0 = new ResourceReference(
158: RatingPanel.class, "star0.gif");
159:
160: /**
161: * Star image for selected star
162: */
163: public static final ResourceReference STAR1 = new ResourceReference(
164: RatingPanel.class, "star1.gif");
165:
166: /**
167: * The number of stars that need to be shown, should result in an Integer
168: * object.
169: */
170: private IModel nrOfStars = new Model(new Integer(5));
171:
172: /**
173: * The number of votes that have been cast, should result in an Integer
174: * object.
175: */
176: private IModel nrOfVotes;
177:
178: /**
179: * The flag on whether the current user has voted already.
180: */
181: private IModel hasVoted;
182:
183: /**
184: * Handle to the rating label to set the visibility.
185: */
186: private Component ratingLabel;
187:
188: /**
189: * Constructs a rating component with 5 stars, using a compound property
190: * model as its model to retrieve the rating.
191: *
192: * @param id
193: * the component id.
194: */
195: public RatingPanel(String id) {
196: this (id, null, 5, true);
197: }
198:
199: /**
200: * Constructs a rating component with 5 stars, using the rating for
201: * retrieving the rating.
202: *
203: * @param id
204: * the component id
205: * @param rating
206: * the model to get the rating
207: */
208: public RatingPanel(String id, IModel rating) {
209: this (id, rating, new Model(new Integer(5)), null, new Model(
210: Boolean.FALSE), true);
211: }
212:
213: /**
214: * Constructs a rating component with nrOfStars stars, using a compound
215: * property model as its model to retrieve the rating.
216: *
217: * @param id
218: * the component id
219: * @param nrOfStars
220: * the number of stars to display
221: */
222: public RatingPanel(String id, int nrOfStars) {
223: this (id, null, 5, true);
224: }
225:
226: /**
227: * Constructs a rating component with nrOfStars stars, using the rating for
228: * retrieving the rating.
229: *
230: * @param id
231: * the component id
232: * @param rating
233: * the model to get the rating
234: * @param nrOfStars
235: * the number of stars to display
236: * @param addDefaultCssStyle
237: * should this component render its own default CSS style?
238: */
239: public RatingPanel(String id, IModel rating, int nrOfStars,
240: boolean addDefaultCssStyle) {
241: this (id, rating, new Model(new Integer(nrOfStars)), null,
242: new Model(Boolean.FALSE), addDefaultCssStyle);
243: }
244:
245: /**
246: * Constructs a rating panel with nrOfStars stars, where the rating model is
247: * used to retrieve the rating, the nrOfVotes model to retrieve the number
248: * of casted votes. This panel doens't keep track of whether the user has
249: * already voted.
250: *
251: * @param id
252: * the component id
253: * @param rating
254: * the model to get the rating
255: * @param nrOfStars
256: * the number of stars to display
257: * @param nrOfVotes
258: * the number of cast votes
259: * @param addDefaultCssStyle
260: * should this component render its own default CSS style?
261: */
262: public RatingPanel(String id, IModel rating, int nrOfStars,
263: IModel nrOfVotes, boolean addDefaultCssStyle) {
264: this (id, rating, new Model(new Integer(nrOfStars)), nrOfVotes,
265: new Model(Boolean.FALSE), addDefaultCssStyle);
266: }
267:
268: /**
269: * Constructs a rating panel with nrOfStars stars, where the rating model is
270: * used to retrieve the rating, the nrOfVotes model used to retrieve the
271: * number of votes cast and the hasVoted model to retrieve whether the user
272: * already had cast a vote.
273: *
274: * @param id
275: * the component id.
276: * @param rating
277: * the (calculated) rating, i.e. 3.4
278: * @param nrOfStars
279: * the number of stars to display
280: * @param nrOfVotes
281: * the number of cast votes
282: * @param hasVoted
283: * has the user already voted?
284: * @param addDefaultCssStyle
285: * should this component render its own default CSS style?
286: */
287: public RatingPanel(String id, IModel rating, IModel nrOfStars,
288: IModel nrOfVotes, IModel hasVoted,
289: boolean addDefaultCssStyle) {
290: super (id, rating);
291:
292: this .nrOfStars = nrOfStars;
293: this .nrOfVotes = nrOfVotes;
294: this .hasVoted = hasVoted;
295:
296: WebMarkupContainer rater = new WebMarkupContainer("rater");
297: rater.add(newRatingStarBar("element", nrOfStars));
298:
299: // add the text label for the message 'Rating 4.5 out of 25 votes'
300: rater.add(ratingLabel = newRatingLabel("rating", rating,
301: nrOfVotes));
302:
303: // set auto generation of the markup id on, such that ajax calls work.
304: rater.setOutputMarkupId(true);
305:
306: add(rater);
307:
308: // don't render the outer tags in the target document, just the div that
309: // is inside the panel.
310: setRenderBodyOnly(true);
311: if (addDefaultCssStyle) {
312: addDefaultCssStyle();
313: }
314: }
315:
316: /**
317: * Will let the rating panel contribute a CSS include to the page's header.
318: * It will add RatingPanel.css from this package. This method is typically
319: * called by the class that creates the rating panel.
320: */
321: public final void addDefaultCssStyle() {
322: add(HeaderContributor.forCss(RatingPanel.class,
323: "RatingPanel.css"));
324: }
325:
326: /**
327: * Creates a new bar filled with stars to click on.
328: *
329: * @param id
330: * the bar id
331: * @param nrOfStars
332: * the number of stars to generate
333: * @return the bar with rating stars
334: */
335: protected Component newRatingStarBar(String id, IModel nrOfStars) {
336: return new RatingStarBar(id, nrOfStars);
337: }
338:
339: /**
340: * Creates a new rating label, showing a message like 'Rated 5.4 from 53
341: * votes'.
342: *
343: * @param id
344: * the id of the label
345: * @param rating
346: * the model containing the rating
347: * @param nrOfVotes
348: * the model containing the number of votes (may be null)
349: * @return the label component showing the message.
350: */
351: protected Component newRatingLabel(String id, IModel rating,
352: IModel nrOfVotes) {
353: IModel model = null;
354: if (nrOfVotes == null) {
355: Object[] parameters = new Object[] { rating };
356: model = new StringResourceModel("rating.simple", this ,
357: null, parameters);
358: } else {
359: Object[] parameters = new Object[] { rating, nrOfVotes };
360: model = new StringResourceModel("rating.complete", this ,
361: null, parameters);
362: }
363: return new Label(id, model);
364: }
365:
366: /**
367: * Returns the url pointing to the image of active stars, is used to set the
368: * URL for the image of an active star. Override this method to provide your
369: * own images.
370: *
371: * @param iteration
372: * the sequence number of the star
373: * @return the url pointing to the image for active stars.
374: */
375: protected String getActiveStarUrl(int iteration) {
376: return getRequestCycle().urlFor(STAR1).toString();
377: }
378:
379: /**
380: * Returns the url pointing to the image of inactive stars, is used to set
381: * the URL for the image of an inactive star. Override this method to
382: * provide your own images.
383: *
384: * @param iteration
385: * the sequence number of the star
386: * @return the url pointing to the image for inactive stars.
387: */
388: protected String getInactiveStarUrl(int iteration) {
389: return getRequestCycle().urlFor(STAR0).toString();
390: }
391:
392: /**
393: * Sets the visibility of the rating label.
394: *
395: * @param visible
396: * true when the label should be visible
397: * @return this for chaining.
398: */
399: public RatingPanel setRatingLabelVisible(boolean visible) {
400: ratingLabel.setVisible(visible);
401: return this ;
402: }
403:
404: /**
405: * Returns <code>true</code> when the star identified by its sequence
406: * number should be shown as active.
407: *
408: * @param star
409: * the sequence number of the star (ranging from 0 to nrOfStars)
410: * @return <code>true</code> when the star should be rendered as active
411: */
412: protected abstract boolean onIsStarActive(int star);
413:
414: /**
415: * Notification of a click on a rating star. Add your own components to the
416: * request target when you want to have them updated in the Ajax request.
417: * <strong>NB</strong> the target may be null when the click isn't handled
418: * using AJAX, but using a fallback scenario.
419: *
420: * @param rating
421: * the number of the star that is clicked, ranging from 1 to
422: * nrOfStars
423: * @param target
424: * the request target, null if the request is a regular, non-AJAX
425: * request.
426: */
427: protected abstract void onRated(int rating, AjaxRequestTarget target);
428: }
|