diff --git a/apps/customercare/helpers.py b/apps/customercare/helpers.py new file mode 100644 index 00000000000..46229f7109e --- /dev/null +++ b/apps/customercare/helpers.py @@ -0,0 +1,10 @@ +from datetime import datetime + +from django.template import defaultfilters + +from jingo import register + + +@register.filter +def utctimesince(time): + return defaultfilters.timesince(time, datetime.utcnow()) diff --git a/apps/customercare/templates/customercare/base.html b/apps/customercare/templates/customercare/base.html new file mode 100644 index 00000000000..eec00df0eef --- /dev/null +++ b/apps/customercare/templates/customercare/base.html @@ -0,0 +1,4 @@ +{# vim: set ts=2 et sts=2 sw=2: #} +{% extends "common/base.html" %} +{% set styles = ('customercare',) %} +{% set scripts = ('customercare',) %} diff --git a/apps/customercare/templates/customercare/landing.html b/apps/customercare/templates/customercare/landing.html new file mode 100644 index 00000000000..06ce9fbfefa --- /dev/null +++ b/apps/customercare/templates/customercare/landing.html @@ -0,0 +1,50 @@ +{# vim: set ts=2 et sts=2 sw=2: #} +{% extends "customercare/base.html" %} +{% set title = _('Join our Army of Awesome') %} + +{% block breadcrumbs %}{% endblock %} + +{% block content_area %} +
+

Join our
Army of Awesome

+

Love Firefox and have a few moments to help? Help other Firefox users on Twitter. Good things will come to those who tweet!

+
+ +
+
    +
  1. Choose a tweet below
  2. + +
  3. Respond to the tweet!
  4. +
+
+
+ +
+
+

Choose a tweet to help

+
+ +
+ + +
+ +
+ {% include 'customercare/reply_modal.html' %} +
+ +
+

Sign in with your Twitter account

+

Before you join the Army of Awesome, you need to log in so you can respond to tweets. You will now be redirected to Twitter to log in.

+ Sign in + Cancel +
+{% endblock %} diff --git a/apps/customercare/templates/customercare/reply_modal.html b/apps/customercare/templates/customercare/reply_modal.html new file mode 100644 index 00000000000..24a1ec0a6da --- /dev/null +++ b/apps/customercare/templates/customercare/reply_modal.html @@ -0,0 +1,47 @@ +
+
+ + + + + + +
+ +
+

What is your reply about?

+
+ {% for resp in canned_responses %} +

{{ resp.title }}

+
+
    + {% for topic in resp.responses.all() %} +
  • + {{ topic.title }} + {{ topic.response }} #fxhelp +
  • + {% endfor %} +
+
+ {% endfor %} +
+ + +
+ +
+

Get personal

+
+
140
+
+ + +
+ + Your message was sent! + +
+
+
+
+ diff --git a/apps/customercare/urls.py b/apps/customercare/urls.py index 25f27cf3a46..4acd2e50aad 100644 --- a/apps/customercare/urls.py +++ b/apps/customercare/urls.py @@ -1,5 +1,6 @@ from django.conf.urls.defaults import patterns, url urlpatterns = patterns('customercare.views', - url(r'^$', 'landing', name='customercare.landing'), + url(r'/twitter_auth', 'twitter_auth', name="customercare.twitter_auth"), + url(r'', 'landing', name='customercare.landing'), ) diff --git a/apps/customercare/views.py b/apps/customercare/views.py index 94edcd5d660..175a7956780 100644 --- a/apps/customercare/views.py +++ b/apps/customercare/views.py @@ -1,6 +1,124 @@ +from datetime import datetime +from email.Utils import parsedate +import json +import logging +from uuid import uuid4 + from django import http +from django.conf import settings +from django.core.cache import cache + +import jingo +import tweepy + +from .models import CannedCategory, Tweet + + +log = logging.getLogger('custcare') + +token_cache_prefix = 'custcare_token_' +key_prefix = token_cache_prefix + 'key_' +secret_prefix = token_cache_prefix + 'secret_' + +# cookie names are duplicated in js/cusomtercare.js +access_cookie_name = 'custcare_twitter_access_id' +redirect_cookie_name = 'custcare_twitter_redirect_flag' + + +def auth_factory(request): + return tweepy.OAuthHandler(settings.TWITTER_CONSUMER_KEY, + settings.TWITTER_CONSUMER_SECRET, + 'https://{0}/{1}/customercare/'.format( + request.get_host(), request.locale)) + + +def set_access_cookie(resp, id): + resp.set_cookie(redirect_cookie_name, '1', httponly=True) + resp.set_cookie(access_cookie_name, id, secure=True) + + +def set_tokens(id, key, secret): + cache.set(key_prefix + id, key) + cache.set(secret_prefix + id, secret) + + +def get_tokens(id): + key = cache.get(key_prefix + id) + secret = cache.get(secret_prefix + id) + return key, secret def landing(request): """Customer Care Landing page.""" - return http.HttpResponse(landing.__doc__) + + canned_responses = CannedCategory.objects.all() + tweets = [] + for tweet in Tweet.objects.filter(locale='en')[:10]: + data = json.loads(tweet.raw_json) + parsed_date = parsedate(data['created_at']) + date = datetime(*parsed_date[0:6]) + tweets.append({ + 'profile_img': data['profile_image_url'], + 'user': data['from_user'], + 'text': tweet, + 'date': date, + }) + + resp = jingo.render(request, 'customercare/landing.html', { + 'canned_responses': canned_responses, + 'tweets': tweets, + 'now': datetime.utcnow(), + }) + + # TODO HTTP redirect flag checking? + if request.COOKIES.get(redirect_cookie_name): + return http.HttpResponseRedirect('https://{0}/{1}'.format( + request.get_host(), request.get_full_path())) + + # if GET[oauth_verifier] exists, we're handling an OAuth login + verifier = request.GET.get('oauth_verifier') + if verifier: + auth = auth_factory(request) + request_key = request.COOKIES.get('request_token_key') + request_secret = request.COOKIES.get('request_token_secret') + if request_key and request_secret: + resp.delete_cookie('request_token_key') + resp.delete_cookie('request_token_secret') + auth.set_request_token(request_key, request_secret) + + try: + auth.get_access_token(verifier) + except tweepy.TweepError: + log.warning('Tweepy Error with verifier token') + pass + else: + access_id = uuid4().hex + set_access_cookie(resp, access_id) + set_tokens(access_id, auth.access_token.key, auth.access_token.secret) + + return resp + + +def twitter_post(request): + # access_id = request.COOKIES.get(access_cookie_name) + # if access_id: + # key, secret = get_tokens(access_id) + # authed = True + # resp.write('key: %s sec: %s' % (key, secret)) + # set_access_cookie(resp, access_id) + pass + + +def twitter_auth(request): + auth = auth_factory(request) + + try: + redirect_url = auth.get_authorization_url() + except tweepy.TweepError: + log.warning('Tweepy error while getting authorization url') + return http.HttpReponseServerError() + + resp = http.HttpResponseRedirect(redirect_url) + resp.set_cookie('request_token_key', auth.request_token.key, max_age=3600, secure=True) + resp.set_cookie('request_token_secret', auth.request_token.secret, max_age=3600, secure=True) + return resp diff --git a/media/css/customercare.css b/media/css/customercare.css new file mode 100644 index 00000000000..4e4b6f54200 --- /dev/null +++ b/media/css/customercare.css @@ -0,0 +1,351 @@ +body { + background-position: 50% 0; +} + +.feature-contents { + width: 640px; + background: url("../img/customercare/nurse.png") no-repeat scroll right 20px transparent; + padding: 30px 0 0 25px; +} + +.feature-contents h2, .feature-contents h3 { + line-height: 1em; + margin-right: 250px; +} + +.feature-contents h2 { + color: #FFF; + font-size: 46px; +} + +.feature-contents h3 { + color: #68645b; + line-height: 1.5em; + font-size: 150%; + margin-top: 15px; +} +#speach-bubbles { + padding: 60px 0 15px 25px; +} +#speach-bubbles li { + list-style-type: none; + float: left; + text-indent: -99999px; + width: 177px; + height: 97px; +} +#speach-bubbles li.choose { + background: url("../img/customercare/bubble-choose.png") no-repeat scroll top left transparent; +} +#speach-bubbles li.signin { + background: url("../img/customercare/bubble-signin.png") no-repeat scroll top left transparent; +} +#speach-bubbles li.respond { + background: url("../img/customercare/bubble-respond.png") no-repeat scroll top left transparent; +} +#tweetcontainer { + border: 1px solid #f5f5f5; + padding: 2px; + clear: both; +} +#tweetcontainer h2 { + margin: 0px; + padding: 0px; + float: left; + line-height: 3em; + font-weight: bold; + font-size: 20px; + background: none; +} +#tweetcontainer img { + float: left; + padding: 4px; +} +.tweets-header { + padding-left: 15px; +} +#tweets { + margin: 0 0 15px 15px; +} +#tweets ul { + margin: 0px; +} +#tweets li { + width: 630px; + border-top: 1px solid #ccc; + overflow: auto; + margin-bottom: 0px; + padding: 10px; + cursor: pointer; +} +#tweets li.alt { + background: -moz-linear-gradient(top, #fff, #f5f5f5); +} +#tweets li:hover { + border-top: 1px solid #fef1ad; + background: -moz-linear-gradient(top, #fffffd, #fdefa6); +} +#tweets li img { + float: left; + margin-right: 1em; + border: solid 1px #cacccb; + padding: 0px; + width: 48px; + height: 48px; +} +#tweets li span { + display: block; +} +#tweets li span { + color: #307fc1; + text-decoration: none; + font-weight: bold; + font-size: 13px; + line-height: 1.4em; +} +#tweets li span .time { + float: right; + font-weight: normal; + font-style: italic; + font-size: 12px; + color: #afaba3; +} +#tweets li span .text { + font-weight: normal; + font-size: 14px; + color: #69645b; +} +.tweets-buttons { + float: right; + line-height: 4em; + padding-right: 15px; +} +.tweet a, .tweet a:hover { + color: #69645b; + text-decoration: none; +} +.hidden { + display: none; +} + +/* reply page styling */ +#reply-modal { display: none; } + +#reply-container { + background-color: #e6f2f6; + width: 444px; + margin: 0 auto; + padding: 30px 24px; + top: 86px; + font-family: Verdana, sans-serif; + text-align: left; + + -moz-box-shadow: 0 0 10px #aaa; + box-shadow: 0 0 10px #aaa; +} + +#reply-container, +#reply-container #replies { + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +#initial-tweet img { + border: 1px solid #bbb; + float: left; +} + +#initial-tweet .box { + border: 1px solid #aaa; + display: block; + margin-left: 70px; + text-align: left; + padding: 6px 9px 10px 9px; + color: #69645b; + font-size: 80%; + background: #fff; +} + +#initial-tweet a { + color: #2276bb; + display: block; + text-decoration: none; + font-weight: bold; +} + +#initial-tweet .box a { + padding-bottom: 4px; +} + +#initial-tweet #arrow { + border: 0; + position: absolute; + left: 88px; + top: 65px; +} + +#reply-container h4 { + font-family: Verdana, sans-serif; + color: #4b4740; + font-size: 125%; + font-weight: normal; + margin-bottom: 15px; +} + +#reply-container #replies { + background: #8fc1da; + background: -moz-linear-gradient(center top , #8fc1da, #e5f1f7) repeat; + margin-top: 20px; + padding: 15px 20px; +} + +#reply-container h3.ui-accordion-header { + background: #f0f0f0 url('../img/customercare/expander-closed.png') 8px 50% no-repeat; + border: 1px solid #b2b2b2; + font-family: Verdana, sans-serif; + font-size: 12px; + padding: 4px 0 5px 24px; + margin: 0 0 9px 0; + + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +#reply-container h3.ui-accordion-header a { + color: #afaba3; + display: block; +} + +#reply-container h3.ui-accordion-header a:hover { + text-decoration: none; +} + +#reply-container #replies ul { + margin: 0 0 10px 24px; + list-style-type: none; +} + +#reply-container #replies li a { + display: block; + color: #0489b7; + padding-bottom: 2px; +} + +#reply-container h3.ui-accordion-header.ui-state-default { + background: #f0f0f0 url('../img/customercare/expander-closed.png') 8px 50% no-repeat; +} + +#reply-container h3.ui-accordion-header.ui-state-active { + background: #f0f0f0 url('../img/customercare/expander-open.png') 8px 50% no-repeat; +} + +#reply-container h3.ui-accordion-header.ui-state-default a { + font-weight: normal; + color: #afaba3; +} + +#reply-container h3.ui-accordion-header.ui-state-active a { + font-weight: bold; + color: #69645b; +} + +#accordion .ui-widget-content { + background: none; + border: none; +} + +.topics .snippet { + display: none; +} + +#reply { + padding-bottom: 34px; +} + +#reply .reply-message { + width: 310px; + height: 65px; + border: 1px solid #aaa; + padding: 10px 15px; + color: #afaba3; + background: #fff; + font-size: .9em; +} + +#reply #reply-arrow { + position: relative; + left: 30px; + top: 4px; + display: inline; +} + +#reply .submit-message { + display: none; + background: url('../img/customercare/reply-check.png') right top no-repeat; + font-weight: bold; + color: #4b4740; + height: 21px; + padding-right: 23px; + padding-top: 3px; + float: left; + margin-top: 14px; +} + +#reply .container { + position: relative; + top: -12px; +} + +#reply .submitButton { + background: -moz-linear-gradient(center top, #eed052 15px, #ebb04f 18px, #e79f30, #e0752b); + border-left: 1px solid #d4661a; + border-right: 1px solid #d4661a; + border-top: 1px solid #d19f7c; + border-bottom: 1px solid #ba5c1c; + color: #b01a04; + width: 140px; + height: 36px; + margin-top: 10px; + float: right; + + text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.6); + -moz-box-shadow: 0 0 5px #777; + box-shadow: 0 0 5px #777; + + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + + cursor: pointer; +} + +#reply .character-counter { + width: 50px; + color: #fff; + font-weight: bold; + font-size: 24px; + float: right; + position: relative; + top: 33px; +} + +.hrbreak { + background: url('../img/customercare/hr-line.png') center 20px no-repeat; + height: 50px; +} +/* end of reply page styling */ + +.notice { + background-color:#FFFFCC; + border:2px solid orange; + margin:1em 0; + padding:1em; + text-align:center; + float: left; +} + +#twitter-modal { + display: none; +} diff --git a/media/img/customercare/bubble-choose.png b/media/img/customercare/bubble-choose.png new file mode 100644 index 00000000000..651109e989a Binary files /dev/null and b/media/img/customercare/bubble-choose.png differ diff --git a/media/img/customercare/bubble-respond.png b/media/img/customercare/bubble-respond.png new file mode 100644 index 00000000000..c1d4721f760 Binary files /dev/null and b/media/img/customercare/bubble-respond.png differ diff --git a/media/img/customercare/bubble-signin.png b/media/img/customercare/bubble-signin.png new file mode 100644 index 00000000000..54121f9e358 Binary files /dev/null and b/media/img/customercare/bubble-signin.png differ diff --git a/media/img/customercare/expander-closed.png b/media/img/customercare/expander-closed.png new file mode 100755 index 00000000000..e6a96f0eda1 Binary files /dev/null and b/media/img/customercare/expander-closed.png differ diff --git a/media/img/customercare/expander-open.png b/media/img/customercare/expander-open.png new file mode 100755 index 00000000000..8aa3ee101dd Binary files /dev/null and b/media/img/customercare/expander-open.png differ diff --git a/media/img/customercare/hr-line.png b/media/img/customercare/hr-line.png new file mode 100755 index 00000000000..e402051aaa5 Binary files /dev/null and b/media/img/customercare/hr-line.png differ diff --git a/media/img/customercare/initial-tweet-arrow.png b/media/img/customercare/initial-tweet-arrow.png new file mode 100755 index 00000000000..e001e9844de Binary files /dev/null and b/media/img/customercare/initial-tweet-arrow.png differ diff --git a/media/img/customercare/nurse.png b/media/img/customercare/nurse.png new file mode 100644 index 00000000000..29fc8323c6f Binary files /dev/null and b/media/img/customercare/nurse.png differ diff --git a/media/img/customercare/popup-background.png b/media/img/customercare/popup-background.png new file mode 100755 index 00000000000..81abc796c6a Binary files /dev/null and b/media/img/customercare/popup-background.png differ diff --git a/media/img/customercare/reply-arrow.png b/media/img/customercare/reply-arrow.png new file mode 100755 index 00000000000..0b8d990455c Binary files /dev/null and b/media/img/customercare/reply-arrow.png differ diff --git a/media/img/customercare/reply-check.png b/media/img/customercare/reply-check.png new file mode 100755 index 00000000000..18258ead16f Binary files /dev/null and b/media/img/customercare/reply-check.png differ diff --git a/media/img/customercare/sumo-logo.png b/media/img/customercare/sumo-logo.png new file mode 100644 index 00000000000..c6a3880f54a Binary files /dev/null and b/media/img/customercare/sumo-logo.png differ diff --git a/media/img/customercare/twitter-icon.png b/media/img/customercare/twitter-icon.png new file mode 100644 index 00000000000..ab47769c9cf Binary files /dev/null and b/media/img/customercare/twitter-icon.png differ diff --git a/media/img/customercare/wantgetinvolved-back.png b/media/img/customercare/wantgetinvolved-back.png new file mode 100644 index 00000000000..47517d31760 Binary files /dev/null and b/media/img/customercare/wantgetinvolved-back.png differ diff --git a/media/js/customercare.js b/media/js/customercare.js new file mode 100644 index 00000000000..1815e18af56 --- /dev/null +++ b/media/js/customercare.js @@ -0,0 +1,56 @@ +var has_twitter_access = false; +// cookie names are duplicated in apps/customercare/views.py +if ($.cookie('custcare_twitter_access_id')) + has_twitter_access = true; + + +$(document).ready(function() { + $('.reply-message').NobleCount('.character-counter'); + + $('.reply-message').autoPlaceholderText(); + + $('#accordion').accordion({ + 'icons': false, + 'autoHeight': false, + }); + + $('.tweet').click(function() { + if (!has_twitter_access) { + $('#twitter-modal').dialog({ + 'modal': 'true', + 'position': 'top', + }); + $('#twitter-modal .cancel').click(function(e) { + $('#twitter-modal').dialog('close'); + e.preventDefault(); + return false; + }); + return; + } + + var avatar_href = $(this).find('.avatar').attr('href'); + var avatar_img = $(this).find('.avatar img').attr('src'); + var twittername = $(this).find('.twittername').text(); + var text = $(this).find('.text').text(); + + var modal = $('#reply-modal'); + modal.find('.avatar').attr('href', avatar_href); + modal.find('.avatar img').attr('src', avatar_img); + modal.find('.twittername').text(twittername); + modal.find('.text').text(text); + modal.dialog({ + 'modal': true, + 'position': 'top', + 'width': 500, + }); + }); + + $('.reply-topic').click(function(e) { + snippet = $(this).next('.snippet').text(); + $('.reply-message').val(snippet); + $('.reply-message').trigger('keydown'); + + e.preventDefault(); + return false; + }); +}); diff --git a/media/js/libs/jquery.NobleCount.js b/media/js/libs/jquery.NobleCount.js new file mode 100644 index 00000000000..5ccecb802a2 --- /dev/null +++ b/media/js/libs/jquery.NobleCount.js @@ -0,0 +1,480 @@ +/****************************************************************************************************** + + jQuery.NobleCount + + Author Jeremy Horn + Version 1.0 + Date: 3/21/2010 + + Copyright (c) 2010 Jeremy Horn- jeremydhorn(at)gmail(dot)c0m | http://tpgblog.com + Dual licensed under MIT and GPL. + + DESCRIPTION + NobleCount... for a more 'proper' count of the characters remaining. + + NobleCount is a customizable jQuery plugin for a more the improved counting of the remaining + characters, and resulting behaviors, of a text entry object, e.g. input textfield, textarea. + + As text is entered into the target text area an object for the purposes of tracking + the total number of characters remaining, defined as the maximum number of characters + minus the current total number of characters within the text entry object, and storing + that information visually and/or within the DOM as an HTML 5 compliant data-* attribute. + + Events and CSS Class alterations, if defined, are triggered based on current user + interaction with the target text entry object as well as the current state (positive or + negative) of the character remaining value. + + NobleCount supports pre-existing text within the text object. + NobleCount supports jQuery chaining. + + Within NobleCount context... + NEGATIVE is defined as Integers < 0 + POSITIVE is defined as Integers >= 0 [on_positive will fire when char_rem == 0] + + BY DEFAULT + - maximum characters EQUAL 140 characters + - no events defined + - no class changes defined + - no DOM attributes are created/altered + - user permitted to type past the maximum number of characters limit, resulting in + negative number of characters remaining + + IMPLEMENTATION + + $('#textarea1').NobleCount('#characters_remaining1'); + $('#textfield2').NobleCount('#characters_remaining2', { / * OPTIONS * / }); + + COMPATIBILITY + + Tested in FF3.5, IE7 + With jQuery 1.3.x, 1.4.x + + METHOD(S) + To properly intialize, both the text entry object and the object that will store the + total number of characters remaining must exist and be passed to NobleCount. + + $(TEXT_ENTRY_OBJECT).NobleCount(CHARACTERS_REMAINING_OBJECT); + + Any callback functions assigned to any of the availale events are passed the following + parameters: t_obj, char_area, c_settings, char_rem + + t_obj text entry object + + char_area selection of the characters remaining object + + c_settings result of the options passed into NobleCount at time of + initialization merged with the default options + + ** this is a GREAT way to pass in and remember other state + information that will be needed upon the triggering of + NobleCount events ** + + char_rem integer representation of the total number of characters + remaining resulting from the calculated difference between + the target maximum number of characters and the current + number of characters currently within t_obj + + Both TEXT_ENTRY_OBJECT and CHARACTERS_REMAINING_OBJECT must be specified and valid. + + Upon successful initialization, all appropriate events and classes are applied to + the CHARACTERS_REMAINING_OBJECT, including the storage (if not disabled) visually + or only in the DOM (if enabled) of the integer representing the number of characters + remaining. + + The target maximum number of characters (max_chars) are determined by the following + precedence rules.... + + if max_chars passed via constructor + max_chars = max_chars passed + else if number exists within characters_remaining object and number > 0 + max_chars = number within the text() of characters_remaining object + else use the NobleCount's default max_chars + + CUSTOMIZATION + + NobleCount(c_obj, ) + e.g. $(t_obj).NobleCount(c_obj, {max_chars:100px}); + + + on_negative class (STRING) or FUNCTION that is applied/called + when characters remaining is negative IF DEFINED + + on_positive class (STRING) or FUNCTION that is applied/called + when characters remaining is positive IF DEFINED + + on_update FUNCTION that is called when characters remaining changes + + max_chars target maximum number of characters + + block_negative if TRUE, then all attempts are made to block entering + more than max_characters; not effective against user + pasting in blocks of text that exceed the max_chars value + otherwise, text area will let individual entering the text + to exceed max_chars limit (characters remaining becomes + negative) + + cloak: false, if TRUE, then no visual updates of characters remaining + object (c_obj) will occur; this does not have any effect + on the char_rem value returned via any event callbacks + otherwise, the text within c_obj is constantly updated to + represent the total number of characters remaining until + the max_chars limit has been reached + + in_dom: false if TRUE and cloak is ALSO TRUE, then the number of characters + remaining are stored as the attribute of c_obj + named 'data-noblecount' + + !NOTE: if enabled, due to constant updating of a DOM element + attribute user experience can appear sluggish while + the individual is modifying the text entry object (t_obj) + + + EXAMPLE OPTIONS = + { + on_negative: 'go_red', + on_positive: 'go_green', + max_chars: 25, + on_update: function(t_obj, char_area, c_settings, char_rem){ + if ((char_rem % 10) == 0) { + char_area.css('font-weight', 'bold'); + char_area.css('font-size', '300%'); + } else { + char_area.css('font-weight', 'normal'); + char_area.css('font-size', '100%'); + } + } + }; + + MORE + + For more details about NobleCount, its implementation, usage, and examples, go to: + http://tpgblog.com/noblecount/ + +******************************************************************************************************/ + +(function($) { + + /********************************************************************************** + + FUNCTION + NobleCount + + DESCRIPTION + NobleCount method constructor + + allows for customization of maximum length and related update/length + behaviors + + e.g. $(text_obj).NobleCount(characters_remaining_obj); + + REQUIRED: c_obj + OPTIONAL: options + + **********************************************************************************/ + + $.fn.NobleCount = function(c_obj, options) { + var c_settings; + var mc_passed = false; + + // if c_obj is not specified, then nothing to do here + if (typeof c_obj == 'string') { + // check for new & valid options + c_settings = $.extend({}, $.fn.NobleCount.settings, options); + + // was max_chars passed via options parameter? + if (typeof options != 'undefined') { + mc_passed = ((typeof options.max_chars == 'number') ? true : false); + } + + // process all provided objects + return this.each(function(){ + var $this = $(this); + + // attach events to c_obj + attach_nobility($this, c_obj, c_settings, mc_passed); + }); + } + + return this; + }; + + + /********************************************************************************** + + FUNCTION + NobleCount.settings + + DESCRIPTION + publically accessible data stucture containing the max_chars and + event handling specifications for NobleCount + + can be directly accessed by '$.fn.NobleCount.settings = ... ;' + + **********************************************************************************/ + $.fn.NobleCount.settings = { + + on_negative: null, // class (STRING) or FUNCTION that is applied/called + // when characters remaining is negative + on_positive: null, // class (STRING) or FUNCTION that is applied/called + // when characters remaining is positive + on_update: null, // FUNCTION that is called when characters remaining + // changes + max_chars: 140, // maximum number of characters + block_negative: false, // if true, then all attempts are made to block entering + // more than max_characters + cloak: false, // if true, then no visual updates of characters + // remaining (c_obj) occur + in_dom: false // if true and cloak == true, then number of characters + // remaining are stored as the attribute + // 'data-noblecount' of c_obj + + }; + + + ////////////////////////////////////////////////////////////////////////////////// + + // private functions and settings + + /********************************************************************************** + + FUNCTION + attach_nobility + + DESCRIPTION + performs all initialization routines and display initiation + + assigns both the keyup and keydown events to the target text entry + object; both keyup and keydown are used to provide the smoothest + user experience + + if max_chars_passed via constructor + max_chars = max_chars_passed + else if number exists within counting_object (and number > 0) + max_chars = counting_object.number + else use default max_chars + + PRE + t_obj and c_obj EXIST + c_settings and mc_passed initialized + + POST + maximum number of characters for t_obj calculated and stored in max_char + key events attached to t_obj + + **********************************************************************************/ + + function attach_nobility(t_obj, c_obj, c_settings, mc_passed){ + var max_char = c_settings.max_chars; + var char_area = $(c_obj); + + // first determine if max_char needs adjustment + if (!mc_passed) { + var tmp_num = char_area.text(); + var isPosNumber = (/^[1-9]\d*$/).test(tmp_num); + + if (isPosNumber) { + max_char = tmp_num; + } + } + + // initialize display of characters remaining + // * note: initializing should not trigger on_update + event_internals(t_obj, char_area, c_settings, max_char, true); + + // then attach the events -- seem to work better than keypress + $(t_obj).keydown(function(e) { + event_internals(t_obj, char_area, c_settings, max_char, false); + + // to block text entry, return false + if (check_block_negative(e, t_obj, c_settings, max_char) == false) { + return false; + } + }); + + $(t_obj).keyup(function(e) { + event_internals(t_obj, char_area, c_settings, max_char, false); + + // to block text entry, return false + if (check_block_negative(e, t_obj, c_settings, max_char) == false) { + return false; + } + }); + } + + + /********************************************************************************** + + FUNCTION + check_block_negative + + DESCRIPTION + determines whether or not text entry within t_obj should be prevented + + PRE + e EXISTS + t_obj VALID + c_settings and max_char initialized / calculated + + POST + if t_obj text entry should be prevented FALSE is returned + otherwise TRUE returned + + TODO + improve selection detection and permissible behaviors experience + ALSO + doesnt CURRENTLY block from the pasting of large chunks of text that + exceed max_char + + **********************************************************************************/ + + function check_block_negative(e, t_obj, c_settings, max_char){ + if (c_settings.block_negative) { + var char_code = e.which; + var selected; + + // goofy handling required to work in both IE and FF + if (typeof document.selection != 'undefined') { + selected = (document.selection.createRange().text.length > 0); + } else { + selected = (t_obj[0].selectionStart != t_obj[0].selectionEnd); + } + + //return false if can't write more + if ((!((find_remaining(t_obj, max_char) < 1) && + (char_code > 47 || char_code == 32 || char_code == 0 || char_code == 13) && + !e.ctrlKey && + !e.altKey && + !selected)) == false) { + + // block text entry + return false; + } + } + + // allow text entry + return true; + } + + + /********************************************************************************** + + FUNCTION + find_remaining + + DESCRIPTION + determines of the number of characters permitted (max_char), the number of + characters remaining until that limit has been reached + + PRE + t_obj and max_char EXIST and are VALID + + POST + returns integer of the difference between max_char and total number of + characters within the text entry object (t_obj) + + **********************************************************************************/ + + function find_remaining(t_obj, max_char){ + return max_char - ($(t_obj).val()).length; + } + + + /********************************************************************************** + + FUNCTION + event_internals + + DESCRIPTION + primarily used for the calculation of appropriate behavior resulting from + any event attached to the text entry object (t_obj) + + whenever the char_rem and related display and/or DOM information needs + updating this function is called + + if cloaking is being used, then no visual representation of the characters + remaining, nor attempt by this plugin to change any of its visual + characteristics will occur + + if cloaking and in_dom are both TRUE, then the number of characters + remaining are stored within the HTML 5 compliant attribute of the + character count remaining object (c_obj) labeled 'data-noblecount' + + PRE + c_settings, init_disp initialized + + POST + performs all updates to the DOM visual and otherwise required + performs all relevant function calls + + **********************************************************************************/ + + function event_internals(t_obj, char_area, c_settings, max_char, init_disp) { + var char_rem = find_remaining(t_obj, max_char); + + // is chararacters remaining positive or negative + if (char_rem < 0) { + toggle_states(c_settings.on_negative, c_settings.on_positive, t_obj, char_area, c_settings, char_rem); + } else { + toggle_states(c_settings.on_positive, c_settings.on_negative, t_obj, char_area, c_settings, char_rem); + } + + // determine whether or not to update the text of the char_area (or c_obj) + if (c_settings.cloak) { + // this slows stuff down quite a bit; TODO: implement better method of publically accessible data storage + if (c_settings.in_dom) { + char_area.attr('data-noblecount', char_rem); + } + } else { + // show the numbers of characters remaining + char_area.text(char_rem); + } + + // if event_internals isn't being called for initialization purposes and + // on_update is a properly defined function then call it on this update + if (!init_disp && jQuery.isFunction(c_settings.on_update)) { + c_settings.on_update(t_obj, char_area, c_settings, char_rem); + } + } + + + /********************************************************************************** + + FUNCTION + toggle_states + + DESCRIPTION + performs the toggling operations between the watched positive and negative + characteristics + + first, enables/triggers/executes the toggle_on behavior/class + second, disables the trigger_off class + + PRE + toggle_on, toggle_off + IF DEFINED, + must be a string representation of a VALID class + OR + must be a VALID function + + POST + toggle_on objects have been applied/executed + toggle_off class has been removed (if it is a class) + + **********************************************************************************/ + + function toggle_states(toggle_on, toggle_off, t_obj, char_area, c_settings, char_rem){ + if (toggle_on != null) { + if (typeof toggle_on == 'string') { + char_area.addClass(toggle_on); + } else if (jQuery.isFunction(toggle_on)) { + toggle_on(t_obj, char_area, c_settings, char_rem); + } + } + + if (toggle_off != null) { + if (typeof toggle_off == 'string') { + char_area.removeClass(toggle_off); + } + } + } +})(jQuery); diff --git a/media/js/libs/jqueryui.min.js b/media/js/libs/jqueryui.min.js index c85da15b282..f604e06f0a4 100644 --- a/media/js/libs/jqueryui.min.js +++ b/media/js/libs/jqueryui.min.js @@ -230,4 +230,34 @@ c=this._daylightSavingAdjust(new Date(c,e+(b<0?b:f[0]*f[1]),1));b<0&&c.setDate(t "dayNamesShort"),dayNames:this._get(a,"dayNames"),monthNamesShort:this._get(a,"monthNamesShort"),monthNames:this._get(a,"monthNames")}},_formatDate:function(a,b,c,e){if(!b){a.currentDay=a.selectedDay;a.currentMonth=a.selectedMonth;a.currentYear=a.selectedYear}b=b?typeof b=="object"?b:this._daylightSavingAdjust(new Date(e,c,b)):this._daylightSavingAdjust(new Date(a.currentYear,a.currentMonth,a.currentDay));return this.formatDate(this._get(a,"dateFormat"),b,this._getFormatConfig(a))}});d.fn.datepicker= function(a){if(!d.datepicker.initialized){d(document).mousedown(d.datepicker._checkExternalClick).find("body").append(d.datepicker.dpDiv);d.datepicker.initialized=true}var b=Array.prototype.slice.call(arguments,1);if(typeof a=="string"&&(a=="isDisabled"||a=="getDate"||a=="widget"))return d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this[0]].concat(b));if(a=="option"&&arguments.length==2&&typeof arguments[1]=="string")return d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this[0]].concat(b)); return this.each(function(){typeof a=="string"?d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this].concat(b)):d.datepicker._attachDatepicker(this,a)})};d.datepicker=new L;d.datepicker.initialized=false;d.datepicker.uuid=(new Date).getTime();d.datepicker.version="1.8.4";window["DP_jQuery_"+y]=d})(jQuery); -; \ No newline at end of file +; +/* + * jQuery UI Accordion 1.8.5 + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Accordion + * + * Depends: + * jquery.ui.core.js + * jquery.ui.widget.js + */ +(function(c){c.widget("ui.accordion",{options:{active:0,animated:"slide",autoHeight:true,clearStyle:false,collapsible:false,event:"click",fillSpace:false,header:"> li > :first-child,> :not(li):even",icons:{header:"ui-icon-triangle-1-e",headerSelected:"ui-icon-triangle-1-s"},navigation:false,navigationFilter:function(){return this.href.toLowerCase()===location.href.toLowerCase()}},_create:function(){var a=this,b=a.options;a.running=0;a.element.addClass("ui-accordion ui-widget ui-helper-reset").children("li").addClass("ui-accordion-li-fix"); +a.headers=a.element.find(b.header).addClass("ui-accordion-header ui-helper-reset ui-state-default ui-corner-all").bind("mouseenter.accordion",function(){b.disabled||c(this).addClass("ui-state-hover")}).bind("mouseleave.accordion",function(){b.disabled||c(this).removeClass("ui-state-hover")}).bind("focus.accordion",function(){b.disabled||c(this).addClass("ui-state-focus")}).bind("blur.accordion",function(){b.disabled||c(this).removeClass("ui-state-focus")});a.headers.next().addClass("ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom"); +if(b.navigation){var d=a.element.find("a").filter(b.navigationFilter).eq(0);if(d.length){var f=d.closest(".ui-accordion-header");a.active=f.length?f:d.closest(".ui-accordion-content").prev()}}a.active=a._findActive(a.active||b.active).addClass("ui-state-default ui-state-active").toggleClass("ui-corner-all ui-corner-top");a.active.next().addClass("ui-accordion-content-active");a._createIcons();a.resize();a.element.attr("role","tablist");a.headers.attr("role","tab").bind("keydown.accordion",function(g){return a._keydown(g)}).next().attr("role", +"tabpanel");a.headers.not(a.active||"").attr({"aria-expanded":"false",tabIndex:-1}).next().hide();a.active.length?a.active.attr({"aria-expanded":"true",tabIndex:0}):a.headers.eq(0).attr("tabIndex",0);c.browser.safari||a.headers.find("a").attr("tabIndex",-1);b.event&&a.headers.bind(b.event.split(" ").join(".accordion ")+".accordion",function(g){a._clickHandler.call(a,g,this);g.preventDefault()})},_createIcons:function(){var a=this.options;if(a.icons){c("").addClass("ui-icon "+a.icons.header).prependTo(this.headers); +this.active.children(".ui-icon").toggleClass(a.icons.header).toggleClass(a.icons.headerSelected);this.element.addClass("ui-accordion-icons")}},_destroyIcons:function(){this.headers.children(".ui-icon").remove();this.element.removeClass("ui-accordion-icons")},destroy:function(){var a=this.options;this.element.removeClass("ui-accordion ui-widget ui-helper-reset").removeAttr("role");this.headers.unbind(".accordion").removeClass("ui-accordion-header ui-accordion-disabled ui-helper-reset ui-state-default ui-corner-all ui-state-active ui-state-disabled ui-corner-top").removeAttr("role").removeAttr("aria-expanded").removeAttr("tabIndex"); +this.headers.find("a").removeAttr("tabIndex");this._destroyIcons();var b=this.headers.next().css("display","").removeAttr("role").removeClass("ui-helper-reset ui-widget-content ui-corner-bottom ui-accordion-content ui-accordion-content-active ui-accordion-disabled ui-state-disabled");if(a.autoHeight||a.fillHeight)b.css("height","");return c.Widget.prototype.destroy.call(this)},_setOption:function(a,b){c.Widget.prototype._setOption.apply(this,arguments);a=="active"&&this.activate(b);if(a=="icons"){this._destroyIcons(); +b&&this._createIcons()}if(a=="disabled")this.headers.add(this.headers.next())[b?"addClass":"removeClass"]("ui-accordion-disabled ui-state-disabled")},_keydown:function(a){if(!(this.options.disabled||a.altKey||a.ctrlKey)){var b=c.ui.keyCode,d=this.headers.length,f=this.headers.index(a.target),g=false;switch(a.keyCode){case b.RIGHT:case b.DOWN:g=this.headers[(f+1)%d];break;case b.LEFT:case b.UP:g=this.headers[(f-1+d)%d];break;case b.SPACE:case b.ENTER:this._clickHandler({target:a.target},a.target); +a.preventDefault()}if(g){c(a.target).attr("tabIndex",-1);c(g).attr("tabIndex",0);g.focus();return false}return true}},resize:function(){var a=this.options,b;if(a.fillSpace){if(c.browser.msie){var d=this.element.parent().css("overflow");this.element.parent().css("overflow","hidden")}b=this.element.parent().height();c.browser.msie&&this.element.parent().css("overflow",d);this.headers.each(function(){b-=c(this).outerHeight(true)});this.headers.next().each(function(){c(this).height(Math.max(0,b-c(this).innerHeight()+ +c(this).height()))}).css("overflow","auto")}else if(a.autoHeight){b=0;this.headers.next().each(function(){b=Math.max(b,c(this).height("").height())}).height(b)}return this},activate:function(a){this.options.active=a;a=this._findActive(a)[0];this._clickHandler({target:a},a);return this},_findActive:function(a){return a?typeof a==="number"?this.headers.filter(":eq("+a+")"):this.headers.not(this.headers.not(a)):a===false?c([]):this.headers.filter(":eq(0)")},_clickHandler:function(a,b){var d=this.options; +if(!d.disabled)if(a.target){a=c(a.currentTarget||b);b=a[0]===this.active[0];d.active=d.collapsible&&b?false:this.headers.index(a);if(!(this.running||!d.collapsible&&b)){this.active.removeClass("ui-state-active ui-corner-top").addClass("ui-state-default ui-corner-all").children(".ui-icon").removeClass(d.icons.headerSelected).addClass(d.icons.header);if(!b){a.removeClass("ui-state-default ui-corner-all").addClass("ui-state-active ui-corner-top").children(".ui-icon").removeClass(d.icons.header).addClass(d.icons.headerSelected); +a.next().addClass("ui-accordion-content-active")}h=a.next();f=this.active.next();g={options:d,newHeader:b&&d.collapsible?c([]):a,oldHeader:this.active,newContent:b&&d.collapsible?c([]):h,oldContent:f};d=this.headers.index(this.active[0])>this.headers.index(a[0]);this.active=b?c([]):a;this._toggle(h,f,g,b,d)}}else if(d.collapsible){this.active.removeClass("ui-state-active ui-corner-top").addClass("ui-state-default ui-corner-all").children(".ui-icon").removeClass(d.icons.headerSelected).addClass(d.icons.header); +this.active.next().addClass("ui-accordion-content-active");var f=this.active.next(),g={options:d,newHeader:c([]),oldHeader:d.active,newContent:c([]),oldContent:f},h=this.active=c([]);this._toggle(h,f,g)}},_toggle:function(a,b,d,f,g){var h=this,e=h.options;h.toShow=a;h.toHide=b;h.data=d;var j=function(){if(h)return h._completed.apply(h,arguments)};h._trigger("changestart",null,h.data);h.running=b.size()===0?a.size():b.size();if(e.animated){d={};d=e.collapsible&&f?{toShow:c([]),toHide:b,complete:j, +down:g,autoHeight:e.autoHeight||e.fillSpace}:{toShow:a,toHide:b,complete:j,down:g,autoHeight:e.autoHeight||e.fillSpace};if(!e.proxied)e.proxied=e.animated;if(!e.proxiedDuration)e.proxiedDuration=e.duration;e.animated=c.isFunction(e.proxied)?e.proxied(d):e.proxied;e.duration=c.isFunction(e.proxiedDuration)?e.proxiedDuration(d):e.proxiedDuration;f=c.ui.accordion.animations;var i=e.duration,k=e.animated;if(k&&!f[k]&&!c.easing[k])k="slide";f[k]||(f[k]=function(l){this.slide(l,{easing:k,duration:i||700})}); +f[k](d)}else{if(e.collapsible&&f)a.toggle();else{b.hide();a.show()}j(true)}b.prev().attr({"aria-expanded":"false",tabIndex:-1}).blur();a.prev().attr({"aria-expanded":"true",tabIndex:0}).focus()},_completed:function(a){this.running=a?0:--this.running;if(!this.running){this.options.clearStyle&&this.toShow.add(this.toHide).css({height:"",overflow:""});this.toHide.removeClass("ui-accordion-content-active");this._trigger("change",null,this.data)}}});c.extend(c.ui.accordion,{version:"1.8.5",animations:{slide:function(a, +b){a=c.extend({easing:"swing",duration:300},a,b);if(a.toHide.size())if(a.toShow.size()){var d=a.toShow.css("overflow"),f=0,g={},h={},e;b=a.toShow;e=b[0].style.width;b.width(parseInt(b.parent().width(),10)-parseInt(b.css("paddingLeft"),10)-parseInt(b.css("paddingRight"),10)-(parseInt(b.css("borderLeftWidth"),10)||0)-(parseInt(b.css("borderRightWidth"),10)||0));c.each(["height","paddingTop","paddingBottom"],function(j,i){h[i]="hide";j=(""+c.css(a.toShow[0],i)).match(/^([\d+-.]+)(.*)$/);g[i]={value:j[1], +unit:j[2]||"px"}});a.toShow.css({height:0,overflow:"hidden"}).show();a.toHide.filter(":hidden").each(a.complete).end().filter(":visible").animate(h,{step:function(j,i){if(i.prop=="height")f=i.end-i.start===0?0:(i.now-i.start)/(i.end-i.start);a.toShow[0].style[i.prop]=f*g[i.prop].value+g[i.prop].unit},duration:a.duration,easing:a.easing,complete:function(){a.autoHeight||a.toShow.css("height","");a.toShow.css({width:e,overflow:d});a.complete()}})}else a.toHide.animate({height:"hide",paddingTop:"hide", +paddingBottom:"hide"},a);else a.toShow.animate({height:"show",paddingTop:"show",paddingBottom:"show"},a)},bounceslide:function(a){this.slide(a,{easing:a.down?"easeOutBounce":"swing",duration:a.down?1E3:200})}}})})(jQuery); diff --git a/migrations/44-cannedresponses-initialdata.sql b/migrations/44-cannedresponses-initialdata.sql index b1af0f4eaab..6c2f807fc2a 100644 --- a/migrations/44-cannedresponses-initialdata.sql +++ b/migrations/44-cannedresponses-initialdata.sql @@ -1,31 +1,31 @@ INSERT INTO `customercare_cannedcategory` (`id`, `title`, `weight`) VALUES -(1, 'Welcome and Thanks', 0), -(2, 'Using Firefox', 1), -(3, 'Firefox Beta', 2), -(4, 'Support', 3), -(5, 'Get Involved', 4); +(1, "Welcome and Thanks", 0), +(2, "Using Firefox", 1), +(3, "Firefox Beta", 2), +(4, "Support", 3), +(5, "Get Involved", 4); INSERT INTO `customercare_cannedresponse` (`id`, `title`, `response`) VALUES -(1, 'Welcome to our community', 'Thanks for joining Mozilla! You're now part of our global community. We're here if you need help: http://mzl.la/bMDof6'), -(2, 'Thanks for using Firefox', 'Thanks for using Firefox! You’re not just a user to us, but part of a community that''s 400M strong: http://mzl.la/9whtWo'), -(3, 'We’re a non-profit organization', 'Hey, I’m a Mozilla volunteer. Did you know Mozilla is made up of 1000s of us worldwide? More here: http://mzl.la/cvlwvd'), -(4, 'Tip & tricks', 'Need help getting started with Firefox? Here are some tips & tricks for getting the most out of it: http://mzl.la/c0B9P2'), -(5, 'Customize Firefox with add-ons', 'Have you tried add-ons? Cool apps for shopping, music, news, whatever you do online. Start here: http://mzl.la/blOuoD'), -(6, 'Add-on reviews', 'Just getting started with Firefox? Add-ons personalize it with cool features & function. User reviews: http://mzl.la/cGypVI'), -(7, 'Upgrade Firefox', 'Hey, maybe you need to upgrade Firefox? New version is speedier with a lot more going on. Download: http://mzl.la/9wJe30'), -(8, 'Update plugins and add-ons', 'Have you updated your plug-ins and add-ons? Should work out the kinks. Here’s the place to refresh: http://mzl.la/cGCg12'), -(9, 'Try the Beta', 'Try the future of Firefox! We need your help testing the new beta. Hop on board: http://mzl.la/d23n7a'), -(10, 'Firefox Sync', 'Tried Firefox Sync? It’s awesome! Switch computers & it saves open tabs, pwords, history. Try it: http://mzl.la/aHHUYA'), -(11, 'Firefox Panorama', 'Heard about Firefox Panorama? It groups and displays your tabs, eliminating clutter. Give it a whirl! http://mzl.la/d21MyY'), -(12, 'Fix crashes', 'Sorry your Firefox is crashing. Here’s a list of quick fixes to prevent that from happening again: http://mzl.la/atSsFt'), -(13, 'Slow Firefox startup', 'Slow start could mean your Firefox just needs a refresh. Here are tips to make Firefox load faster: http://mzl.la/9bB1FY'), -(14, 'Quick Firefox fixes', 'Have you tried Firefox support? If their quick fixes don’t solve it, volunteers are ready to help out: http://mzl.la/9V9uWd'), -(15, 'Ask SUMO', 'Maybe ask SUMO about this issue? Firefox’s community support team. They’ll know what’s up: http://mzl.la/bMDof6'), -(16, 'Get involved with Mozilla', 'Want a better web? Join the Mozilla movement. There is something to do for everyone. Get started: http://mzl.la/cufJmX'), -(17, 'Join Drumbeat', 'Want to spark a movement? Mozilla Drumbeat is your chance to keep the web open and free. More info: http://mzl.la/aIXCLA'), -(18, 'Become a beta tester', 'Become a beta tester! Help develop the next Firefox. You don’t have to be a techie to contribute: http://mzl.la/d23n7a'), -(19, 'Mozilla Developer Network', 'Open up the web & make it better! Build web pages, apps and add-ons here: Mozilla Developer Network http://mzl.la/9gQfrn'), -(20, 'Report a bug', 'Thanks for finding a bug. It makes everyone’s Firefox experience better if you report it. It''s easy: http://mzl.la/bcujVc'); +(1, "Welcome to our community", "Thanks for joining Mozilla! You're now part of our global community. We're here if you need help: http://mzl.la/bMDof6r"), +(2, "Thanks for using Firefox", "Thanks for using Firefox! You're not just a user to us, but part of a community that's 400M strong: http://mzl.la/9whtWor"), +(3, "We're a non-profit organization", "Hey, I'm a Mozilla volunteer. Did you know Mozilla is made up of 1000s of us worldwide? More here: http://mzl.la/cvlwvd"), +(4, "Tip & tricks", "Need help getting started with Firefox? Here are some tips & tricks for getting the most out of it: http://mzl.la/c0B9P2"), +(5, "Customize Firefox with add-ons", "Have you tried add-ons? Cool apps for shopping, music, news, whatever you do online. Start here: http://mzl.la/blOuoD"), +(6, "Add-on reviews", "Just getting started with Firefox? Add-ons personalize it with cool features & function. User reviews: http://mzl.la/cGypVI"), +(7, "Upgrade Firefox", "Hey, maybe you need to upgrade Firefox? New version is speedier with a lot more going on. Download: http://mzl.la/9wJe30"), +(8, "Update plugins and add-ons", "Have you updated your plug-ins and add-ons? Should work out the kinks. Here's the place to refresh: http://mzl.la/cGCg12"), +(9, "Try the Beta", "Try the future of Firefox! We need your help testing the new beta. Hop on board: http://mzl.la/d23n7a"), +(10, "Firefox Sync", "Tried Firefox Sync? It's awesome! Switch computers & it saves open tabs, pwords, history. Try it: http://mzl.la/aHHUYA"), +(11, "Firefox Panorama", "Heard about Firefox Panorama? It groups and displays your tabs, eliminating clutter. Give it a whirl! http://mzl.la/d21MyY"), +(12, "Fix crashes", "Sorry your Firefox is crashing. Here's a list of quick fixes to prevent that from happening again: http://mzl.la/atSsFt"), +(13, "Slow Firefox startup", "Slow start could mean your Firefox just needs a refresh. Here are tips to make Firefox load faster: http://mzl.la/9bB1FY"), +(14, "Quick Firefox fixes", "Have you tried Firefox support? If their quick fixes don't solve it, volunteers are ready to help out: http://mzl.la/9V9uWd"), +(15, "Ask SUMO", "Maybe ask SUMO about this issue? Firefox's community support team. They'll know what's up: http://mzl.la/bMDof6"), +(16, "Get involved with Mozilla", "Want a better web? Join the Mozilla movement. There is something to do for everyone. Get started: http://mzl.la/cufJmX"), +(17, "Join Drumbeat", "Want to spark a movement? Mozilla Drumbeat is your chance to keep the web open and free. More info: http://mzl.la/aIXCLA"), +(18, "Become a beta tester", "Become a beta tester! Help develop the next Firefox. You don't have to be a techie to contribute: http://mzl.la/d23n7a"), +(19, "Mozilla Developer Network", "Open up the web & make it better! Build web pages, apps and add-ons here: Mozilla Developer Network http://mzl.la/9gQfrn"), +(20, "Report a bug", "Thanks for finding a bug. It makes everyone's Firefox experience better if you report it. It''s easy: http://mzl.la/bcujVc"); INSERT INTO `customercare_categorymembership` (`id`, `category_id`, `response_id`, `weight`) VALUES (1, 1, 1, 0), diff --git a/requirements/prod.txt b/requirements/prod.txt index f42d2a403c1..a4f5676294f 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -22,3 +22,5 @@ pytz -e git://github.com/jsocol/commonware.git#egg=commonware -e git://github.com/pcraciunoiu/py-wikimarkup.git#egg=py-wikimarkup -e hg+http://bitbucket.org/danfairs/django-authority#egg=django_authority + +-e git://github.com/joshthecoder/tweepy.git#egg=tweepy diff --git a/settings.py b/settings.py index 04c301ca502..3ad289be6db 100644 --- a/settings.py +++ b/settings.py @@ -291,6 +291,11 @@ def JINJA_CONFIG(): 'ie': ( 'css/ie.css', ), + 'customercare': ( + 'css/jqueryui/jquery.ui.core.css', + 'css/jqueryui/jquery.ui.theme.css', + 'css/customercare.css', + ), }, 'js': { 'common': ( @@ -322,6 +327,12 @@ def JINJA_CONFIG(): ), 'gallery': ( ), + 'customercare': ( + 'js/libs/jqueryui.min.js', + 'js/libs/jquery.NobleCount.js', + 'js/libs/jquery.cookie.js', + 'js/customercare.js', + ), }, } @@ -426,3 +437,6 @@ def JINJA_CONFIG(): # Customare care tweet collection settings CC_MAX_TWEETS = 500 # Max. no. of tweets in DB CC_TWEETS_PERPAGE = 100 # How many tweets to collect in one go. Max: 100. + +TWITTER_CONSUMER_KEY = '' +TWITTER_CONSUMER_SECRET = '' diff --git a/urls.py b/urls.py index 406ec5228c0..0872a4e6cc1 100644 --- a/urls.py +++ b/urls.py @@ -19,7 +19,7 @@ (r'^upload', include('upload.urls')), (r'^kb', include('wiki.urls')), (r'^gallery', include('gallery.urls')), - (r'^customercare', include('customercare.urls')), + (r'^army-of-awesome', include('customercare.urls')), # Kitsune admin (not Django admin). (r'^admin/', include('kadmin.urls')),