From 75847834ed019c2b055e35198ef6f4faf082c825 Mon Sep 17 00:00:00 2001 From: Bill Huneke Date: Fri, 5 Jul 2013 20:21:42 +0000 Subject: [PATCH 01/27] Direct people to the correct source code repository (on github) rather than the old one (on code.google.com) --- README | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README b/README index 90d9827..a0c00bb 100644 --- a/README +++ b/README @@ -76,6 +76,7 @@ Other Resources ~~~~~~~~~~~~~~~ * Python Cheeseshop page: http://pypi.python.org/pypi/django-crowdsourcing - * Source code repository and ticket tracker: http://code.google.com/p/django-crowdsourcing/ + * NEW Source code repository and ticket tracker (as of 1.1.35): https://github.com/wnyc/django-crowdsourcing + * OLD Source code repository and ticket tracker: http://code.google.com/p/django-crowdsourcing/ * Discussion group: http://groups.google.com/group/django-crowdsourcing/ From a7d13eff7ca00b2a421de937a69131224d2bf3a8 Mon Sep 17 00:00:00 2001 From: Bill Huneke Date: Fri, 5 Jul 2013 20:43:21 +0000 Subject: [PATCH 02/27] Add 2 new fields to survey model. These fields allow the addition of 'arbitrary' responses for multiple choice and multi-choice survey questions --- crowdsourcing/forms.py | 12 +++++++++++- crowdsourcing/models.py | 11 +++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/crowdsourcing/forms.py b/crowdsourcing/forms.py index 9a34b65..0ecddc3 100644 --- a/crowdsourcing/forms.py +++ b/crowdsourcing/forms.py @@ -19,6 +19,7 @@ Select, Textarea, ValidationError, + TextInput, ) from django.forms.forms import BoundField from django.forms.formsets import BaseFormSet @@ -182,14 +183,23 @@ def __init__(self, *args, **kwargs): mark_safe(x))) if not self.question.required and not isinstance(self, OptionCheckbox): choices = [('', '---------',)] + choices + if self.question.allow_arbitrary: + choices.append(('arbitrary_answer',self.question.arbitrary_label)) self.fields['answer'].choices = choices + if self.question.allow_arbitrary: + choice_control_name = self.add_prefix('answer') + widget = TextInput(attrs={'arb_boundto':choice_control_name,'arb_choice':'arbitrary_answer','class':'arbitrary_textbox'}) + self.fields['answer_arbitrary'] = CharField(label="", widget=widget, required=False) def clean_answer(self): key = self.cleaned_data['answer'] if not key and self.fields['answer'].required: raise ValidationError, _('This field is required.') if not isinstance(key, (list, tuple)): - key = (key,) + key = [key,] + if self.question.allow_arbitrary and 'arbitrary_answer' in key: + where = key.index('arbitrary_answer') + key[where] = self._raw_value('answer_arbitrary') return key def save(self, commit=True): diff --git a/crowdsourcing/models.py b/crowdsourcing/models.py index 9445c64..a711b67 100644 --- a/crowdsourcing/models.py +++ b/crowdsourcing/models.py @@ -330,6 +330,17 @@ class Question(models.Model): 'Use one option per line. On a live survey you can modify the ' 'order of these options. You can, at your own risk, add new ' 'options, but you must not change or remove options.')) + allow_arbitrary = models.BooleanField( + default=False, + help_text=_('Applicable to multiple choice or checkbox list type questions. ' + 'If this option is selected then another option is automatically ' + 'created with a text box that allows the respondent to enter' + 'arbitrary data')) + arbitrary_label = models.TextField( + default="Other value:", + help_text=_('This is the option label that will appear next to the text box ' + 'for arbitrary text entry. This is only applicable if Allow Arbitrary is ' + 'enabled.')) map_icons = models.TextField( blank=True, default='', From bc58c6549e26762016a3d6235902512af5671710 Mon Sep 17 00:00:00 2001 From: Michele Mattioni Date: Mon, 18 Feb 2013 18:39:00 +0000 Subject: [PATCH 03/27] Adding the static files directly in the app. This avoids to copy the files in any new app, but they are autodiscovered by django.static. --- crowdsourcing/static/enlarge-icon.png | Bin 0 -> 595 bytes crowdsourcing/static/enlarge.css | 59 ++ crowdsourcing/static/enlarge.js | 128 ++++ crowdsourcing/static/google_maps.css | 44 ++ crowdsourcing/static/google_maps.js | 132 ++++ crowdsourcing/static/img/loading.gif | Bin 0 -> 2545 bytes crowdsourcing/static/jquery.form.js | 665 +++++++++++++++++++ crowdsourcing/static/jquery.jcarousel.min.js | 21 + crowdsourcing/static/styles.css | 99 +++ crowdsourcing/static/survey.js | 250 +++++++ 10 files changed, 1398 insertions(+) create mode 100644 crowdsourcing/static/enlarge-icon.png create mode 100644 crowdsourcing/static/enlarge.css create mode 100644 crowdsourcing/static/enlarge.js create mode 100644 crowdsourcing/static/google_maps.css create mode 100644 crowdsourcing/static/google_maps.js create mode 100644 crowdsourcing/static/img/loading.gif create mode 100644 crowdsourcing/static/jquery.form.js create mode 100755 crowdsourcing/static/jquery.jcarousel.min.js create mode 100644 crowdsourcing/static/styles.css create mode 100644 crowdsourcing/static/survey.js diff --git a/crowdsourcing/static/enlarge-icon.png b/crowdsourcing/static/enlarge-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d27068a149c9e60f7e6e42566f4c84077994d2b8 GIT binary patch literal 595 zcmV-Z0<8UsP);D2g^Q3n3vAckNwRqTBu~1c_=TVhVypi`oQ1ixxo;gyOOZL?Iy*M@=QH zMyAnGd$%|9=FNNey>TO+&HZ=Icfb36=L+u5jUX66f;qY(zmO>5&XEkt@%WH^q#;DF z3L=X2K4fz#oE#zg+((*O5GW;$J!@Q*^hTBzi7*h~kPy)@D2sV0vlC%Coh|8O;62k%$I8?)kwqo934dv*1Fg zpi@cwPz9G`RU|iaWe7EPOK**@A+o6U").appendTo($("body")).attr("id", "enlarge_bg"); + background.css("opacity", 0.5).click(hideEnlarge); + var enlarge = $("
").attr("id", "enlarge").appendTo($("body")).hide(); + var close1 = $("
").addClass("close").append($("").html("Close")); + close1.click(hideEnlarge); + enlarge.append(close1); + var imgElement = $("").appendTo($("
").addClass("image")); + imgElement.appendTo(enlarge).click(hideEnlarge); + var loading = $("
").addClass("loading").html("Loading..."); + loading.appendTo(enlarge); + var imgCaption = $("
").addClass("caption").appendTo(enlarge); + var imgCredits = $("
").addClass("credit").appendTo(enlarge); + + var caption = img.attr("title"); + imgCaption.text(caption); + imgCaption.hide(); /* Caption shown after image width determined */ + var strippedHtml = function(element, credits) { + if ("undefined" != typeof element.strippedHtml) { + element.strippedHtml(credits); + } else { + element.text(credits); + } + return element; + }; + if (credits_url) { + var a = $("").attr("href", credits_url); + if ("undefined" != typeof isExternal && isExternal(credits_url)) { + a.attr("target", "_blank"); + } + imgCredits.append(strippedHtml(a, credits)); + } else { + strippedHtml(imgCredits, credits); + } + if (credits) { + imgCredits.show(); + } else { + imgCredits.hide(); + } + var top = $(window).scrollTop() + 10; + enlarge.css({top: top}); + loading.width("").height("").show(); + imgElement.css({position: "absolute", top: 0, left: -9999}); + imgElement.attr("title", caption).attr("alt", img.attr("alt")); + enlarge.show(); + setEnlargeLeft(enlarge.outerWidth(), 0); + imgLoaded = false; + // tags float above the popup divs. + $("embed").css("visibility", "hidden"); + /* If the image has to load, then just the next line is necessary. Now, in + * Chrome the image does not need to load at all the second time, so the + * load event doesn't fire. In IE8, I've observed the load event just not + * firing. Hence, call imgLoad right away, and call it periodically until our + * image is finally loaded. */ + imgElement.load(imgLoad).attr("src", url); + repeatImgLoad(); +} + +function setEnlargeLeft(enlargeWidth, animateTime) { + var left = ($(window).width() - enlargeWidth) / 2; + $("#enlarge").animate({left: left}, animateTime); +} + +function hideEnlarge() { + $("#enlarge_bg").detach(); + $("#enlarge").detach(); + $("embed").css("visibility", "visible"); +} + +var imgLoaded = false; +function imgLoad() { + var animateTime = 250; + var imgElement = $("#enlarge img"); + var enlarge = $("#enlarge"); + var loading = $("#enlarge .loading"); + var imgCaption = $("#enlarge .caption"); + + if (!imgLoaded && imgElement[0].complete) { + imgLoaded = true; + imgCaption.show(); + var enlargeWidth = imgElement.outerWidth() + edgeWidth(enlarge); + setEnlargeLeft(enlargeWidth, animateTime); + var attrs = {width: imgElement.width(), height: imgElement.height()}; + loading.animate(attrs, animateTime, "linear", function() { + loading.hide(); + imgElement.css({position: "static"}); + }); + } +} + +function edgeWidth(element) { + var returnValue = 0; + for (attribute in {"margin": "", "padding": ""}) { + for (direction in {"right": "", "left": ""}) { + var att = attribute + "-" + direction; + var value = parseInt(element.css(att).replace("px", "")); + returnValue += value ? value : 0; + } + } + return returnValue; +} + +function repeatImgLoad() { + if (!imgLoaded) { + imgLoad(); + window.setTimeout(repeatImgLoad, 100); + } +} diff --git a/crowdsourcing/static/google_maps.css b/crowdsourcing/static/google_maps.css new file mode 100644 index 0000000..d56eca4 --- /dev/null +++ b/crowdsourcing/static/google_maps.css @@ -0,0 +1,44 @@ +.google_map { + height: 300px; + width: 480px; +} + +.map_story { + height: 284px; + width: 464px; + margin: 8px; + background-color: #FFFFFF; + border: 1px solid #DFDFDF; + display: none; + overflow-x: hidden; + overflow-y: scroll; + position: absolute; + top: 0px; + left: 0px; + z-index: 500; +} + +.google_map_wrapper { + position: relative; +} + +.google_map_wrapper .loading { + margin: 140px 230px 140px 230px; +} + +.google_map_wrapper fieldset { + clear: both; + left: 0; + position: absolute; + z-index: 100; + margin: 20px 0 0; + padding: 5px; + background-color: #F6F6F6; + border: 1px solid #C6C6C6; + width: 288px; +} + +.google_map_wrapper .map_tools li { + float: left; + margin-right: 10px; +} diff --git a/crowdsourcing/static/google_maps.js b/crowdsourcing/static/google_maps.js new file mode 100644 index 0000000..7ad8742 --- /dev/null +++ b/crowdsourcing/static/google_maps.js @@ -0,0 +1,132 @@ +var latestMap = null; +function setupMap( + div_id, + details_id, + results_url, + center_lat, + center_lng, + zoom) { + var onAPILoaded = function() { + if (GBrowserIsCompatible()) { + $(function() { + var params = queryParametersAsLookup(); + $.getJSON(results_url, params, function(data, status) { + if (status != "success") { + $("#" + div_id).html("The crowdsourcing API is experiencing " + + "problems. It returned status " + status + "."); + return; + } + if (!data.entries.length) { + $("#" + div_id).html("There aren't any locations to show on " + + "this map, but as soon as we get some we'll put a map here."); + return; + } + var map = initializeMap(div_id, data, center_lat, center_lng, zoom); + latestMap = map; + var createClickClosure = function(url) { + return function() { + showSubmission(url, div_id, details_id); + }; + }; + for (entry_i in data.entries) { + var entry = data.entries[entry_i]; + var icon = G_DEFAULT_ICON; + if (entry.icon) { + icon = new GIcon(G_DEFAULT_ICON, entry.icon); + } + var marker = new GMarker(new GLatLng(entry.lat, entry.lng), icon); + map.addOverlay(marker); + createClickClosure(marker, entry.url); + GEvent.addListener(marker, "click", createClickClosure(entry.url)); + } + }); + }); + } else { + $("#" + div_id).html("Sorry! Your browser doesn't support Google Maps."); + } + } + googleMapCallbacks.push(onAPILoaded); +} + +function showSubmission(url, div_id, details_id) { + var img = ''; + var details = $("#" + details_id); + details.html(img); + var offset = $("#" + div_id).offset; + details.fadeIn(); + $.get(url, function(results) { + var get_close = function(css) { + var div = $("
").addClass(css); + $("").text("Close").click(function(event) { + event.preventDefault(); + details.hide(); + }).attr("href", "#").appendTo(div); + return div; + } + details.empty(); + details.append(get_close("top_close")); + $(results).appendTo(details); + details.append(get_close("bottom_close")); + initEnlargeable(details); + }); +} + +function initializeMap(div_id, data, center_lat, center_lng, zoom) { + var map = new GMap2(document.getElementById(div_id)); + if (null != center_lat && null != center_lng) { + var center = new GLatLng(center_lat, center_lng); + } else { + var corners = minMaxLatLong(data.entries); + var center = new GLatLng((corners[0] + corners[2]) / 2, + (corners[1] + corners[3]) / 2); + } + var use_zoom = 13; + if (null != zoom) { + use_zoom = zoom; + } + map.setCenter(center, use_zoom); + map.setUIToDefault(); + if (null == zoom) { + setZoom(map, data.entries) + } + return map; +} + +function setZoom(map, entries) { + var corners = minMaxLatLong(entries); + var lower = new GLatLng(corners[0], corners[1]); + var upper = new GLatLng(corners[2], corners[3]); + bounds = new GLatLngBounds(lower, upper); + map.setZoom(map.getBoundsZoomLevel(bounds)); +} + +function minMaxLatLong(entries) { + var max_lat = max_long = -91.0; + var min_lat = min_long = 91.0; + for (entry_i in entries) { + var entry = entries[entry_i]; + min_lat = entry.lat < min_lat ? entry.lat : min_lat; + min_long = entry.lng < min_long ? entry.lng : min_long; + max_lat = entry.lat > max_lat ? entry.lat : max_lat; + max_long = entry.lng > max_long ? entry.lng : max_long; + } + return [min_lat, min_long, max_lat, max_long]; +} + +function setupMapEmbed() { + $(".map_embed_link").each(function() { + var wrapper = $(this).parents(".google_map_wrapper"); + var click = function(event) { + event.preventDefault(); + wrapper.find("fieldset").toggle(); + }; + $(this).click(click); + wrapper.find(".close-map-button").click(click); + wrapper.find("textarea").click(function() { + this.select(); + }); + }); +} + +$(setupMapEmbed); diff --git a/crowdsourcing/static/img/loading.gif b/crowdsourcing/static/img/loading.gif new file mode 100644 index 0000000000000000000000000000000000000000..53dd589fa194f5db985e4301c7a73ed4f1b9ad99 GIT binary patch literal 2545 zcma*pX;2hr8VB%~zPqQp=@B)`y1PS96NXtx1_oS2g;AJ+5se@Q;|&UOs2qwM!p4Ca zhJiUal?ExBdstaT~?f3m?cZgNh{frmzMrfcJ8)3 z;P)BJevgQNtw(cfF&90SnBnnr_M&xm@O%6 zzG;!w8(-4o!w_SKEsx_t8i2dHA{$xug+5j_t}~5!rEMsQGpY^uf8#e!ez!6*c<$`# z%2he_t-h<5RT`BLN|GpKWC}6HY^k+5Dom~L=dCyXf!71bXd=Do;)LbM zv+gGZ*&l+=TYuPh^HJ+{Um3v9W8Dqi zZsIDi!j*rKg;1w`oCOc;={#&)!ZF-E-<0u^i2Q3W!xltD%v-T4#;x)CfF(1Ouv)`p z>uUosDu9aCbKX*^H)pSF(Clw+wK}`3LC8Vd5wn~@j##n&8u(Or9|$_$-*q^|8tR^Ze!v`8XRU12fiFxN&Z^HeB_6{>ZFruBWn5U6U_WQI--B zG~o}Ys+mlEb+PC3o7FVVvN+9%m7%O}Y_c3WoYls|wT3|PzzV{wM+0F|$H`%9CT0Y^ za*mZIUl3}$c$+|-?~lU1Lc0B}sn(uvcimZ5#)MR#za@NWsrn^X=yb^$k3_>|w9gkR z>#sW<_Ea0KR$g0dg}fy7K69z6?%wR47d!o@ zGKEctobW(W=VY|Az1{V$NH$hYU5!_r3z~t<1_3#$IhT`+-0m8Lu3WANjbGvNg1vn1 z=E&h$lM^*FtCrg64J+q9{~$Yc&oK@)FJ9|wdu`CBEMKgD->DcQRuxrUW36azX6kqE z(WAIz&9a^4uqfQXZ?4*+k}M^TI=80q9GA}G8+>bb(fyeR`Q-S93>Q-=OX7E#U^f4~jWk3u>uE&r}55)J9D8+*U}{&~xy z+xheT3lqZ$_0RMat&nbp<_ki!f?+E=jik2kBu@#UnX z0AcCVU-1x`*#iFAGPG4?H)8U+YtUJCS?At3FP6 zWjMo76`C~5oga!z%d+xx$x-ljIYP1$9YjNGd0H&jsWL7XsM^)8O;wB>PVnYYXxtSh|$ifidD)RGQ}R zwHJrNIdVjTqdEklY&;>>dZRxDK`F>#B8Ki@pp8f7rwM2mppszvcw{I`Q1%>g09l6Ekr2-S;;<|g2awum zsj!Q?)Pbyi5x+M!F@WFk6jsdhHiry`x0GIzjy;Yj%rtk*Y|Rg9EEqf3wBpTM<@>(0 zk0}s;bd@I0{9$O~|I{qUIC4vLX&EGrCS`%OyAe_n3}9mSiO7}(QE&#Jdx176W`mkS qJBjK|(6Aas*VCO0)PD%QJkkj;=v4CXc=>?~L(zU= 0 ? '&' : '?') + q; + options.data = null; // data is null for 'get' + } + else + options.data = q; // data is the query string for 'post' + + var $form = this, callbacks = []; + if (options.resetForm) callbacks.push(function() { $form.resetForm(); }); + if (options.clearForm) callbacks.push(function() { $form.clearForm(); }); + + // perform a load on the target only if dataType is not provided + if (!options.dataType && options.target) { + var oldSuccess = options.success || function(){}; + callbacks.push(function(data) { + $(options.target).html(data).each(oldSuccess, arguments); + }); + } + else if (options.success) + callbacks.push(options.success); + + options.success = function(data, status, xhr) { // jQuery 1.4+ passes xhr as 3rd arg + for (var i=0, max=callbacks.length; i < max; i++) + callbacks[i].apply(options, [data, status, xhr || $form, $form]); + }; + + // are there files to upload? + var files = $('input:file', this).fieldValue(); + var found = false; + for (var j=0; j < files.length; j++) + if (files[j]) + found = true; + + var multipart = false; +// var mp = 'multipart/form-data'; +// multipart = ($form.attr('enctype') == mp || $form.attr('encoding') == mp); + + // options.iframe allows user to force iframe mode + // 06-NOV-09: now defaulting to iframe mode if file input is detected + if ((files.length && options.iframe !== false) || options.iframe || found || multipart) { + // hack to fix Safari hang (thanks to Tim Molendijk for this) + // see: http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d + if (options.closeKeepAlive) + $.get(options.closeKeepAlive, fileUpload); + else + fileUpload(); + } + else + $.ajax(options); + + // fire 'notify' event + this.trigger('form-submit-notify', [this, options]); + return this; + + + // private function for handling file uploads (hat tip to YAHOO!) + function fileUpload() { + var form = $form[0]; + + if ($(':input[name=submit]', form).length) { + alert('Error: Form elements must not be named "submit".'); + return; + } + + var opts = $.extend({}, $.ajaxSettings, options); + var s = $.extend(true, {}, $.extend(true, {}, $.ajaxSettings), opts); + + var id = 'jqFormIO' + (new Date().getTime()); + var $io = $('