From 681b98ee9f76d639c2b4430f0051a14419a839e1 Mon Sep 17 00:00:00 2001 From: Vladyslav Zherebkin Date: Tue, 6 Apr 2021 13:16:46 +0300 Subject: [PATCH] Implemented Reviews Tab on lms --- lms/djangoapps/reviews/__init__.py | 0 lms/djangoapps/reviews/apps.py | 17 ++ lms/djangoapps/reviews/migrations/__init__.py | 0 lms/djangoapps/reviews/plugins.py | 37 +++ .../reviews/static/reviews/css/delete.gif | Bin 0 -> 752 bytes .../reviews/static/reviews/css/rateit.css | 148 +++++++++ .../reviews/static/reviews/css/star.gif | Bin 0 -> 2460 bytes .../static/reviews/js/jquery.rateit.min.js | 5 + .../reviews/static/reviews/js/reviews.js | 21 ++ .../reviews/templates/reviews/reviews.html | 280 ++++++++++++++++++ lms/djangoapps/reviews/urls.py | 6 + lms/djangoapps/reviews/views.py | 100 +++++++ .../core/djangoapps/models/course_details.py | 2 +- requirements/edx/base.txt | 1 + requirements/edx/development.txt | 1 + setup.py | 2 + 16 files changed, 619 insertions(+), 1 deletion(-) create mode 100644 lms/djangoapps/reviews/__init__.py create mode 100644 lms/djangoapps/reviews/apps.py create mode 100644 lms/djangoapps/reviews/migrations/__init__.py create mode 100644 lms/djangoapps/reviews/plugins.py create mode 100644 lms/djangoapps/reviews/static/reviews/css/delete.gif create mode 100644 lms/djangoapps/reviews/static/reviews/css/rateit.css create mode 100644 lms/djangoapps/reviews/static/reviews/css/star.gif create mode 100644 lms/djangoapps/reviews/static/reviews/js/jquery.rateit.min.js create mode 100644 lms/djangoapps/reviews/static/reviews/js/reviews.js create mode 100644 lms/djangoapps/reviews/templates/reviews/reviews.html create mode 100644 lms/djangoapps/reviews/urls.py create mode 100644 lms/djangoapps/reviews/views.py diff --git a/lms/djangoapps/reviews/__init__.py b/lms/djangoapps/reviews/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/lms/djangoapps/reviews/apps.py b/lms/djangoapps/reviews/apps.py new file mode 100644 index 000000000000..23f99511de23 --- /dev/null +++ b/lms/djangoapps/reviews/apps.py @@ -0,0 +1,17 @@ +from django.apps import AppConfig +from edx_django_utils.plugins import PluginSettings, PluginURLs +from openedx.core.constants import COURSE_ID_PATTERN +from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType + + +class ReviewsConfig(AppConfig): + name = 'lms.djangoapps.reviews' + plugin_app = { + PluginURLs.CONFIG: { + ProjectType.LMS: { + PluginURLs.NAMESPACE: u'', + PluginURLs.REGEX: u'^courses/{}/reviews/'.format(COURSE_ID_PATTERN), + PluginURLs.RELATIVE_PATH: u'urls', + } + } + } diff --git a/lms/djangoapps/reviews/migrations/__init__.py b/lms/djangoapps/reviews/migrations/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/lms/djangoapps/reviews/plugins.py b/lms/djangoapps/reviews/plugins.py new file mode 100644 index 000000000000..62389e7ac731 --- /dev/null +++ b/lms/djangoapps/reviews/plugins.py @@ -0,0 +1,37 @@ +""" +Definition of the course review feature. +""" + +from django.utils.translation import ugettext_noop +from lms.djangoapps.courseware.tabs import EnrolledTab +from xmodule.tabs import TabFragmentViewMixin +from openedx.core.djangoapps.content.course_overviews.models import CourseOverview + + +class ReviewsTab(TabFragmentViewMixin, EnrolledTab): + """ + The representation of the course reviews view type. + """ + name = "reviews" + tab_id = "reviews" + + type = "reviews" + title = ugettext_noop("Reviews") + body_class = "reviews-tab" + is_hideable = False + is_default = True + fragment_view_name = 'lms.djangoapps.reviews.views.ReviewsTabFragmentView' + + @classmethod + def is_enabled(cls, course, user=None): + """Returns true if the reviews feature is enabled in the course. + + Args: + course (CourseDescriptor): the course using the feature + user (User): the user interacting with the course + """ + if not super(ReviewsTab, cls).is_enabled(course, user=user): + return False + + course = CourseOverview.get_from_id(course.id) + return course.allow_review diff --git a/lms/djangoapps/reviews/static/reviews/css/delete.gif b/lms/djangoapps/reviews/static/reviews/css/delete.gif new file mode 100644 index 0000000000000000000000000000000000000000..43c6ca8763d79bde87bcf437e497af00c8be562d GIT binary patch literal 752 zcmZ?wbhEHb6kt$bc*el6GthYN-n}zt&b)vB{*)i+-#|NQy$ zABXln&Q1FL`t{#CH*SY|{oJwc*Qz;h;)0))7aYq9ewP~fc2e)_P3vAY)$9wl{IzJp zy{ydlr;a^HiGHwZ{*SH8FZkJhE{gei`OM?;ir*_{?@ji478&@mD*tSH_`A&5@6Vqe z@G<&xeCOwiq-WvY-eS5f?;vgCP8#IHGBw=+^cY}xvy zx8c*8mDl2ff1W;kCeZrNm9vlQYJg5Caki?(^G9DBs4DA0KlA4-ZT3fl8 z8F_gb*}2-1v>0WWwY1oIg_&Emc-aM+Wn|bpRc1GF^$N-etzA1qr9X*XNNWw_j-BjG zGEKc(x)1gUF{(82E@ad^eMXp9hUxg0Q)f<}lVRDncBb&%dq*yAR+(|l-D@bZwvw%g)cL7VG z)An_f*+XqOetkIP*eg{wS7K(+qJ{-x0ue3?v&y~1d1To>MqD_2keNGNE=Pc|!BJ7b fV_(F^#N=Z_Jo9)pKRPly^YODPxtB div, .rateit-font .rateit-range > span { + background: none; + overflow: hidden; + cursor: default; + } + +.rateit.rateit-font .rateit-reset { + font-size: inherit; + background: none; + width: 16px; + height: 16px; + margin-top: 0.2em; + background: gray; + border-radius: 50%; + position: relative; +} + + .rateit.rateit-font .rateit-reset span { + display: block; + font-weight: bold; + color: #fff; + height: 2px; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + position: absolute; + background: #fff; + width: 70%; + } + + +.rateit.rateit-font .rateit-reset:hover, .rateit.rateit-font button.rateit-reset:focus { + background: #e6574b; /* Old browsers */ + background: radial-gradient(ellipse at center, #e6574b 55%,#f6836b 77%,#f9d3cc 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */ + background-position: 0 0; +} diff --git a/lms/djangoapps/reviews/static/reviews/css/star.gif b/lms/djangoapps/reviews/static/reviews/css/star.gif new file mode 100644 index 0000000000000000000000000000000000000000..cd147295cadbc8ed6a76257fb5eb96567002f478 GIT binary patch literal 2460 zcmeHI`CC(08V;8wiMRm83W|COpdd?b03`_^*=1EW2PUI7Aqfy53rW~fNCZIv1q&)_ zTb2&)m8n7%bwCt|11eOcxBxC}0YXU1QU_a_IAZ5{W|+TV&JXvV^WFD-zwbNez4z|$ zqhWDO6Bq$DR}Jg$?{D_^ZwU=;qtiRZ;)e+dsul-Tvcm%gV<-_jaT_&MkDZ7|HIhgp zBO^~TGsn{@16+)<&~I{ba{LZO`N(hJDrT&mHq*aX{Sd9Vwr-|!@7Q75?Ch*czDu2j zR^{(fFxDx{=tEbq-8ng$0_yZLdLP4K@aomR^A3uG#K##K>SJNbPP+P0n4*<9eTJ^- zqz=VkhtqL$-M1$X5TAWzH=IFICPQ-y#qb@H>dU}qyY1#CZckLEO*~F}H#w<#8mKuL zsd*Zy>DjGphSWDtPu?S{U!J}}p=eI-eo|zEPQ_9!u-te`=3?se^CuL;wIYK z`YuujZ~2eaIjXlAH)(*pxtL`%)r5sE1AO}+c% zy@1guyGjp_@ke=mJnBd@{p}6%Y)#Z?t(~T&cKl)aloU69xqPY}_v9FA_H|TGxnoO9 zi~2>?T*KM1SjXueg63sJSAnIny}gOSQ2uyn@JRI71L;Jg?_5{iPsfidZ(LH}sv2!* z7_Z4wwDGUdXd`7J#R>k%VY=cP*x|lnaKHb=e&6eXf%7xHbNy!r??gZGMX6tAy%D;1 zWUsjB<1;A>SXfvXxi1>~M%v%d(9_e?5gpyp(V-2({y+X>2do#2BZGoL3RA+=-o1QM zg-ljFM*>E1Vz_*7lJKIYP{yiu_di4c!Qr=+B~ zq`0{VL@{U#4u{iexVk!P5zgW?zJ!_T%op1%Do{9LmWV5qa0Pr&r^t*FBuTtc+DiY` z0#Eo!mM?bs(2fgBz(c1pg=mZmq-$vr7!>rsp*-Fvv{(|#`J>+dDl8696LQd@9I+rt z#Nue-Ho8CfgoXV7lUI35`j2YAOy)_Tfqn}pDjoce?SKX;r;kx36sy_ z_))x3TD}XH%f?fPkQB* zj&AzNwA#fd+jIC@uZy&%)?4`P*Z1@PeD}-T?9BAf8ugTFQaLd`rWhR=9vXc6=Jl(A z{=SzloIC3~Y?@(^eL76l= z>%jiZjP$hBl;k9dSd=If@OcUGaopILXbzhd#bkW3Z||PJ(El76@u%J4VWAa2u-ehNMgR%xUZdh-%Zmp%o zn$K68e`aQCvTEgu<;Kg92)L1Lj-z(NMaAN@inFDwV{m%#6 z%a1OjW;emUw@$%I|Lv=L|qjO_yHi55K5}i(wnQ6Axxl(>;XLhC=>;vx?%DX zeFMa5%lxKmtDty(6E1s1niT+#B{UFM=Is0}Zr}biYV)QT!-*oVrd=e!@cF*&o`lfm zR$y&0SZaG17KGf2wjQN)mFMKy9soQq zDK7Pu432jqNDn62UOxmk{kEnAj#0tPpBV*p5-Ly&dz%BTbe7YVn@)<>Kn6cY?g z3!9r)Ca~3a;pMJPO%^6{6FqZ)`GxgeqjMW7;t(ZrA#hp04>YK$KV$+JY=)%8rdtq9 zM}2GCVi<^g*Q5u>b_bI;MB5hANJZIA0K&r-COx;u_PQYgdA2rxv)2?zwm?dcAc%>P Q8ebCMH(XB8(}Q{b4wH715&!@I literal 0 HcmV?d00001 diff --git a/lms/djangoapps/reviews/static/reviews/js/jquery.rateit.min.js b/lms/djangoapps/reviews/static/reviews/js/jquery.rateit.min.js new file mode 100644 index 000000000000..63a4c7d24e93 --- /dev/null +++ b/lms/djangoapps/reviews/static/reviews/js/jquery.rateit.min.js @@ -0,0 +1,5 @@ +/*! RateIt | v1.1.5 / 03/10/2021 + https://github.com/gjunge/rateit.js | Twitter: @gjunge +*/ +!function(M){function I(e){var t=e.originalEvent.changedTouches[0],a="";switch(e.type){case"touchmove":a="mousemove";break;case"touchend":a="mouseup";break;default:return}var i=document.createEvent("MouseEvent");i.initMouseEvent(a,!0,!0,window,1,t.screenX,t.screenY,t.clientX,t.clientY,!1,!1,!1,!1,0,null),t.target.dispatchEvent(i),e.preventDefault()}M.rateit={aria:{resetLabel:"reset rating",ratingLabel:"rating"}},M.fn.rateit=function(w,N){var y=1,C={},k="init",E=function(e){return e.charAt(0).toUpperCase()+e.substr(1)};if(0===this.length)return this;var e=typeof w;if("object"==e||null==w)C=M.extend({},M.fn.rateit.defaults,w);else{if("string"==e&&"reset"!==w&&void 0===N)return this.data("rateit"+E(w));"string"==e&&(k="setvalue")}return this.each(function(){var r=M(this),n=function(e,t){if(null!=t){var a="aria-value"+("value"==e?"now":e),i=r.find(".rateit-range");null!=i.attr(a)&&i.attr(a,t)}return e="rateit"+E(e),r.data.apply(r,arguments)};if("reset"==w){var e=n("init");for(var t in e)r.data(t,e[t]);if(n("backingfld"))"SELECT"==(a=M(n("backingfld")))[0].nodeName&&"index"===a[0].getAttribute("data-rateit-valuesrc")?a.prop("selectedIndex",n("value")):a.val(n("value")),a.trigger("change"),a[0].min&&(a[0].min=n("min")),a[0].max&&(a[0].max=n("max")),a[0].step&&(a[0].step=n("step"));r.trigger("reset")}r.hasClass("rateit")||r.addClass("rateit");var i="rtl"!=r.css("direction");if("setvalue"==k){if(!n("init"))throw"Can't set value before init";if("readonly"!=w||1!=N||n("readonly")||(r.find(".rateit-range").off(),n("wired",!1)),"value"==w&&(N=null==N?n("min"):Math.max(n("min"),Math.min(n("max"),N))),n("backingfld"))"SELECT"==(a=M(n("backingfld")))[0].nodeName&&"index"===a[0].getAttribute("data-rateit-valuesrc")?"value"==w&&a.prop("selectedIndex",N):"value"==w&&a.val(N),"min"==w&&a[0].min&&(a[0].min=N),"max"==w&&a[0].max&&(a[0].max=N),"step"==w&&a[0].step&&(a[0].step=N);n(w,N)}if(!n("init")){var a;if(n("mode",n("mode")||C.mode),n("icon",n("icon")||C.icon),n("min",isNaN(n("min"))?C.min:n("min")),n("max",isNaN(n("max"))?C.max:n("max")),n("step",n("step")||C.step),n("readonly",void 0!==n("readonly")?n("readonly"):C.readonly),n("resetable",void 0!==n("resetable")?n("resetable"):C.resetable),n("backingfld",n("backingfld")||C.backingfld),n("starwidth",n("starwidth")||C.starwidth),n("starheight",n("starheight")||C.starheight),n("value",Math.max(n("min"),Math.min(n("max"),isNaN(n("value"))?isNaN(C.value)?C.min:C.value:n("value")))),n("ispreset",void 0!==n("ispreset")?n("ispreset"):C.ispreset),n("backingfld"))if(((a=M(n("backingfld")).hide()).attr("disabled")||a.attr("readonly"))&&n("readonly",!0),"INPUT"==a[0].nodeName&&("range"!=a[0].type&&"text"!=a[0].type||(n("min",parseInt(a.attr("min"))||n("min")),n("max",parseInt(a.attr("max"))||n("max")),n("step",parseInt(a.attr("step"))||n("step")))),"SELECT"==a[0].nodeName&&1<{{element}} id="rateit-range-{{index}}" class="rateit-range"'+(1==n("readonly")?"":' tabindex="0"')+' role="slider" aria-label="'+M.rateit.aria.ratingLabel+'" aria-owns="rateit-reset-{{index}}" aria-valuemin="'+n("min")+'" aria-valuemax="'+n("max")+'" aria-valuenow="'+n("value")+'"><{{element}} class="rateit-empty"><{{element}} class="rateit-selected"><{{element}} class="rateit-hover">';r.append(l.replace(/{{index}}/gi,y).replace(/{{element}}/gi,d)),i||(r.find(".rateit-reset").css("float","right"),r.find(".rateit-selected").addClass("rateit-selected-rtl"),r.find(".rateit-hover").addClass("rateit-hover-rtl")),"font"==n("mode")?r.addClass("rateit-font").removeClass("rateit-bg"):r.addClass("rateit-bg").removeClass("rateit-font"),n("init",JSON.parse(JSON.stringify(r.data())))}var o="font"==n("mode");o||r.find(".rateit-selected, .rateit-hover").height(n("starheight"));var u=r.find(".rateit-range");if(o){for(var m=n("icon"),v=n("max")-n("min"),h="",c=0;c *").text(h),n("starwidth",u.width()/(n("max")-n("min")))}else u.width(n("starwidth")*(n("max")-n("min"))).height(n("starheight"));var g="rateit-preset"+(i?"":"-rtl");if(n("ispreset")?r.find(".rateit-selected").addClass(g):r.find(".rateit-selected").removeClass(g),null!=n("value")){var f=(n("value")-n("min"))*n("starwidth");r.find(".rateit-selected").width(f)}var p=r.find(".rateit-reset");!0!==p.data("wired")&&p.on("click",function(e){e.preventDefault(),p.trigger("blur");var t=M.Event("beforereset");if(r.trigger(t),t.isDefaultPrevented())return!1;r.rateit("value",null),r.trigger("reset")}).data("wired",!0);var b=function(e,t){var a=(t.changedTouches?t.changedTouches[0].pageX:t.pageX)-M(e).offset().left;return i||(a=u.width()-a),a>u.width()&&(a=u.width()),a<0&&(a=0),f=Math.ceil(a/n("starwidth")*(1/n("step")))},x=function(e){var t=M.Event("beforerated");return r.trigger(t,[e]),!t.isDefaultPrevented()&&(n("value",e),n("backingfld")&&("SELECT"==a[0].nodeName&&"index"===a[0].getAttribute("data-rateit-valuesrc")?M(n("backingfld")).prop("selectedIndex",e).trigger("change"):M(n("backingfld")).val(e).trigger("change")),n("ispreset")&&(u.find(".rateit-selected").removeClass(g),n("ispreset",!1)),u.find(".rateit-hover").hide(),u.find(".rateit-selected").width(e*n("starwidth")-n("min")*n("starwidth")).show(),r.trigger("hover",[null]).trigger("over",[null]).trigger("rated",[e]),!0)};n("readonly")?p.hide():(n("resetable")||p.hide(),n("wired")||(u.on("touchmove touchend",I),u.on("mousemove",function(e){!function(e){var t=e*n("starwidth")*n("step"),a=u.find(".rateit-hover");if(a.data("width")!=t){u.find(".rateit-selected").hide(),a.width(t).show().data("width",t);var i=[e*n("step")+n("min")];r.trigger("hover",i).trigger("over",i)}}(b(this,e))}),u.on("mouseleave",function(e){u.find(".rateit-hover").hide().width(0).data("width",""),r.trigger("hover",[null]).trigger("over",[null]),u.find(".rateit-selected").show()}),u.on("mouseup",function(e){var t=b(this,e)*n("step")+n("min");x(t),u.trigger("blur")}),u.on("keyup",function(e){38!=e.which&&e.which!=(i?39:37)||x(Math.min(n("value")+n("step"),n("max"))),40!=e.which&&e.which!=(i?37:39)||x(Math.max(n("value")-n("step"),n("min")))}),n("wired",!0)),n("resetable")&&p.show()),u.attr("aria-readonly",n("readonly"))})},M.fn.rateit.defaults={min:0,max:5,step:.5,mode:"bg",icon:"★",starwidth:16,starheight:16,readonly:!1,resetable:!0,ispreset:!1},M(function(){M("div.rateit, span.rateit").rateit()})}(jQuery); +//# sourceMappingURL=jquery.rateit.min.js.map diff --git a/lms/djangoapps/reviews/static/reviews/js/reviews.js b/lms/djangoapps/reviews/static/reviews/js/reviews.js new file mode 100644 index 000000000000..0c49049cc90f --- /dev/null +++ b/lms/djangoapps/reviews/static/reviews/js/reviews.js @@ -0,0 +1,21 @@ + +(function ($) { + $(document).ready(function () { + $('#submitRating').on("click", function() { + let rating = $('#rateit-range-3').attr('aria-valuenow'); + let review = $('#feedbackTextarea').val(); + let url = $('#submitRating').data('url'); + $.ajax({ + type: "POST", + url: url, + data: { + "rating": rating, + "review": review, + }, + success: function (){ + location.reload(); + } + }); + }) + }); +}(jQuery)); diff --git a/lms/djangoapps/reviews/templates/reviews/reviews.html b/lms/djangoapps/reviews/templates/reviews/reviews.html new file mode 100644 index 000000000000..4554a3ceba3e --- /dev/null +++ b/lms/djangoapps/reviews/templates/reviews/reviews.html @@ -0,0 +1,280 @@ +## mako + +<%page expression_filter="h"/> + +<%! +import arrow +import json +from django.utils.translation import ugettext as _ + +from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string +from openedx.core.djangolib.markup import HTML + +%> + + +
+
+
+ + ${_("Student Feedback")} + +
+
+
+

${average_rating}

+
+
${_("Overall Course Rating")}
+
+
+
+
+ % for key, value in reversed(rating_percent_dict.items()): +
+
+
+
+
+
+
+ % for point in range(5): + % if point >= key: + + % else: + + % endif + % endfor + ${value}% +
+
+ % endfor +
+
+
+ % if not is_commented: +

${_("Leave your review")}

+
+
${user.username}
+
+ ${_("Rate your experience: ")} +
+
+ +
+
+
+
+ +
+ +
+
+ % endif +

${_("Reviews")}

+ % for review in reviews: +
+
+ +
+
+
${review.user_id.username}
+
+
+ % for point in range(5): + % if point >= review.rating: + + % else: + + % endif + % endfor +
+
${arrow.get(review.created_at).humanize()}
+
+
${review.review}
+
+
+
+ % endfor +
+
+
diff --git a/lms/djangoapps/reviews/urls.py b/lms/djangoapps/reviews/urls.py new file mode 100644 index 000000000000..ab3d3f5a54dd --- /dev/null +++ b/lms/djangoapps/reviews/urls.py @@ -0,0 +1,6 @@ +from django.conf.urls import url +from .views import create_review + +urlpatterns = [ + url(r"^create_review/$", create_review, name='create_review'), +] diff --git a/lms/djangoapps/reviews/views.py b/lms/djangoapps/reviews/views.py new file mode 100644 index 000000000000..700802db2798 --- /dev/null +++ b/lms/djangoapps/reviews/views.py @@ -0,0 +1,100 @@ +from django.db.models import Avg +from django.db import IntegrityError +from django.template.loader import render_to_string +from web_fragments.views import FragmentView +from web_fragments.fragment import Fragment +from opaque_keys.edx.keys import CourseKey +from lms.djangoapps.courseware.courses import get_course_with_access, get_course_overview_with_access +from common.djangoapps.feedback.models import CourseReview +from collections import OrderedDict +from django.contrib.staticfiles.storage import staticfiles_storage +from django.http import JsonResponse +from django.urls import reverse +from django.contrib.auth.decorators import login_required +from django.views.decorators.http import require_http_methods + + +class ReviewsTabFragmentView(FragmentView): + """ + Fragment view implementation of the reviews tab. + """ + def render_to_fragment(self, request, course_id=None, **kwargs): + """ + Render the reviews tab to a fragment. + Args: + request: The Django request. + course_id: The id of the course. + Returns: + Fragment: The fragment representing the reviews tab. + """ + course_key = CourseKey.from_string(course_id) + course = get_course_with_access(request.user, "load", course_key) + reviews = CourseReview.objects.filter(course_id=course_id) + average_rating = round(reviews.aggregate(Avg('rating'))['rating__avg'] or 0.00, 2) + is_commented = CourseReview.objects.filter(course_id=course_id, user_id=request.user).exists() + num_reviews = reviews.count() + rating_percent_dict = OrderedDict() + + for index in range(1, 6): + rating_type_value = reviews.filter(rating=index).count() + + try: + percent = round(rating_type_value * 100 / num_reviews, 2) + except ZeroDivisionError: + percent = 0 + + rating_percent_dict.update({index: percent}) + + context = { + 'course': course, + 'user': request.user, + 'reviews': reviews, + 'average_rating': average_rating, + 'create_review_url': reverse("create_review", kwargs={"course_id": course_id}), + 'is_commented': is_commented, + 'rating_percent_dict': rating_percent_dict + } + html = render_to_string('reviews/reviews.html', context) + fragment = Fragment(html) + fragment.add_javascript_url(staticfiles_storage.url('reviews/js/reviews.js')) + fragment.add_javascript_url(staticfiles_storage.url('reviews/js/jquery.rateit.min.js')) + fragment.add_css_url(staticfiles_storage.url('reviews/css/rateit.css')) + fragment.add_css_url(staticfiles_storage.url('reviews/css/star.gif')) + fragment.add_css_url(staticfiles_storage.url('reviews/css/delete.gif')) + + return fragment + + +@login_required +@require_http_methods(['POST']) +def create_review(request, course_id): + rating = request.POST.get('rating') + review = request.POST.get('review') + course_key = CourseKey.from_string(course_id) + course = get_course_overview_with_access(request.user, "load", course_key) + + try: + rating = float(rating) + except (TypeError, ValueError): + return JsonResponse({'message': 'Set the rating'}, status=400) + + if not rating: + return JsonResponse({'message': 'Set the rating'}, status=400) + + if not review: + return JsonResponse({'message': 'Give a review'}, status=400) + + try: + CourseReview.objects.create( + user_id=request.user, + course_id=course, + rating=rating, + review=review + ) + except IntegrityError: + return JsonResponse({'message': 'Review is already existed'}, status=400) + + return JsonResponse({'message': 'Success'}, status=200) + + + diff --git a/openedx/core/djangoapps/models/course_details.py b/openedx/core/djangoapps/models/course_details.py index 30008502eb64..c2a58b60a127 100644 --- a/openedx/core/djangoapps/models/course_details.py +++ b/openedx/core/djangoapps/models/course_details.py @@ -93,7 +93,7 @@ def __init__(self, org, course_id, run): self.self_paced = None self.learning_info = [] self.instructor_info = [] - self.allow_review = True + self.allow_review = "true" @classmethod def fetch_about_attribute(cls, course_key, attribute): diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 34578a480702..0d3ac3ed96c8 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -250,6 +250,7 @@ xss-utils==0.1.3 # via -r requirements/edx/base.in zipp==1.0.0 # via -c requirements/edx/../constraints.txt, -r requirements/edx/paver.txt, importlib-metadata django-ckeditor==6.0.0 django-fs-trumbowyg==0.1.4 +arrow==1.0.3 # The following packages are considered to be unsafe in a requirements file: # setuptools diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 03bbaf067a22..01723a493a18 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -322,6 +322,7 @@ xblock==1.4.0 # via -r requirements/edx/testing.txt, acid-xblock, cr xmlsec==1.3.9 # via -r requirements/edx/testing.txt, python3-saml xss-utils==0.1.3 # via -r requirements/edx/testing.txt zipp==1.0.0 # via -c requirements/edx/../constraints.txt, -r requirements/edx/testing.txt, importlib-metadata, importlib-resources +arrow==1.0.3 # The following packages are considered to be unsafe in a requirements file: # setuptools diff --git a/setup.py b/setup.py index f21177e1b04d..991051f9f63f 100644 --- a/setup.py +++ b/setup.py @@ -35,6 +35,7 @@ "teams = lms.djangoapps.teams.plugins:TeamsTab", "textbooks = lms.djangoapps.courseware.tabs:TextbookTabs", "wiki = lms.djangoapps.course_wiki.tab:WikiTab", + "reviews = lms.djangoapps.reviews.plugins:ReviewsTab", ], "openedx.course_tool": [ "calendar_sync_toggle = openedx.features.calendar_sync.plugins:CalendarSyncToggleTool", @@ -91,6 +92,7 @@ "user_authn = openedx.core.djangoapps.user_authn.apps:UserAuthnConfig", "program_enrollments = lms.djangoapps.program_enrollments.apps:ProgramEnrollmentsConfig", "courseware_api = openedx.core.djangoapps.courseware_api.apps:CoursewareAPIConfig", + "reviews = lms.djangoapps.reviews.apps:ReviewsConfig", ], "cms.djangoapp": [ "announcements = openedx.features.announcements.apps:AnnouncementsConfig",