From 29b6dd8ed2c25454993df57a0e6531d889e6031d Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Tue, 28 Mar 2023 15:43:25 +0600 Subject: [PATCH 01/37] Bump version of jQuery to 3.6.4 & updated ref links (#8909) --- rest_framework/static/rest_framework/js/jquery-3.6.4.min.js | 2 ++ rest_framework/templates/rest_framework/admin.html | 2 +- rest_framework/templates/rest_framework/base.html | 2 +- rest_framework/templates/rest_framework/docs/error.html | 2 +- rest_framework/templates/rest_framework/docs/index.html | 2 +- 5 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 rest_framework/static/rest_framework/js/jquery-3.6.4.min.js diff --git a/rest_framework/static/rest_framework/js/jquery-3.6.4.min.js b/rest_framework/static/rest_framework/js/jquery-3.6.4.min.js new file mode 100644 index 000000000..0de648ed3 --- /dev/null +++ b/rest_framework/static/rest_framework/js/jquery-3.6.4.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.6.4 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,y=n.hasOwnProperty,a=y.toString,l=a.call(Object),v={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.4",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&v(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!y||!y.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ve(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace(B,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ye(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ve(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.cssHas=ce(function(){try{return C.querySelector(":has(*,:jqfake)"),!1}catch(e){return!0}}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],y=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||y.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||y.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||y.push(".#.+[+~]"),e.querySelectorAll("\\\f"),y.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),d.cssHas||y.push(":has"),y=y.length&&new RegExp(y.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),v=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType&&e.documentElement||e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&v(p,e)?-1:t==C||t.ownerDocument==p&&v(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!y||!y.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),v.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",v.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",v.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Ut,Xt=[],Vt=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Xt.pop()||S.expando+"_"+Ct.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Vt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Vt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Vt,"$1"+r):!1!==e.jsonp&&(e.url+=(Et.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Xt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),v.createHTMLDocument=((Ut=E.implementation.createHTMLDocument("").body).innerHTML="
",2===Ut.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(v.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return B(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=_e(v.pixelPosition,function(e,t){if(t)return t=Be(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return B(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0 - + diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 533db1378..53c964f23 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -293,7 +293,7 @@ "csrfToken": "{% if request %}{{ csrf_token }}{% endif %}" } - + diff --git a/rest_framework/templates/rest_framework/docs/error.html b/rest_framework/templates/rest_framework/docs/error.html index 694f88a15..8e67238b9 100644 --- a/rest_framework/templates/rest_framework/docs/error.html +++ b/rest_framework/templates/rest_framework/docs/error.html @@ -66,6 +66,6 @@ at rest_framework/docs/error.html.

- + diff --git a/rest_framework/templates/rest_framework/docs/index.html b/rest_framework/templates/rest_framework/docs/index.html index dfd363772..8f8536fbe 100644 --- a/rest_framework/templates/rest_framework/docs/index.html +++ b/rest_framework/templates/rest_framework/docs/index.html @@ -38,7 +38,7 @@ {% include "rest_framework/docs/auth/basic.html" %} {% include "rest_framework/docs/auth/session.html" %} - + From 59430111bdecf9e2c7488d599c18a91d5ac89240 Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Tue, 28 Mar 2023 16:08:27 +0600 Subject: [PATCH 02/37] Update tox with django 4.2rc1 (#8920) --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 7027612e0..10b52063e 100644 --- a/tox.ini +++ b/tox.ini @@ -21,7 +21,7 @@ deps = django32: Django>=3.2,<4.0 django40: Django>=4.0,<4.1 django41: Django>=4.1,<4.2 - django42: Django>=4.2b1,<5.0 + django42: Django>=4.2rc1,<5.0 djangomain: https://github.com/django/django/archive/main.tar.gz -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt From 6b73acc1735631df1e666cc87eee0d14de6ae018 Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Tue, 28 Mar 2023 16:35:44 +0600 Subject: [PATCH 03/37] Update requirements-packaging.txt (#8921) --- requirements/requirements-packaging.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/requirements-packaging.txt b/requirements/requirements-packaging.txt index a9733185b..81f22a35a 100644 --- a/requirements/requirements-packaging.txt +++ b/requirements/requirements-packaging.txt @@ -1,8 +1,8 @@ # Wheel for PyPI installs. -wheel>=0.35.1,<0.36 +wheel>=0.36.2,<0.40.0 # Twine for secured PyPI uploads. -twine>=3.2.0,<3.3 +twine>=3.4.2,<4.0.2 # Transifex client for managing translation resources. transifex-client From b60fbf3374efcf0abfccb10597736329f891d4fc Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Mon, 3 Apr 2023 22:35:11 +0600 Subject: [PATCH 04/37] test django 4.2 stable release (#8932) --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 10b52063e..2b8733d7d 100644 --- a/tox.ini +++ b/tox.ini @@ -21,7 +21,7 @@ deps = django32: Django>=3.2,<4.0 django40: Django>=4.0,<4.1 django41: Django>=4.1,<4.2 - django42: Django>=4.2rc1,<5.0 + django42: Django>=4.2,<5.0 djangomain: https://github.com/django/django/archive/main.tar.gz -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt From 3428cec194060f9896be90f00f2d316557acd00f Mon Sep 17 00:00:00 2001 From: Christian Franke Date: Tue, 4 Apr 2023 09:38:23 +0200 Subject: [PATCH 05/37] Use consistent spelling for "authorization" (#8929) Apart from a few exceptions, django-rest-framework uses the American English spelling "authorization"/"authorized". $ git grep -oi authorised | wc -l 2 $ git grep -oi authorized | wc -l 30 Replace the few occurences of the British English spelling with the American English one. --- docs/api-guide/permissions.md | 2 +- rest_framework/templates/rest_framework/docs/error.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index 27f7c5adb..e70cc63be 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -165,7 +165,7 @@ This permission is suitable if you want your API to only be accessible to a subs ## IsAuthenticatedOrReadOnly -The `IsAuthenticatedOrReadOnly` will allow authenticated users to perform any request. Requests for unauthorised users will only be permitted if the request method is one of the "safe" methods; `GET`, `HEAD` or `OPTIONS`. +The `IsAuthenticatedOrReadOnly` will allow authenticated users to perform any request. Requests for unauthorized users will only be permitted if the request method is one of the "safe" methods; `GET`, `HEAD` or `OPTIONS`. This permission is suitable if you want to your API to allow read permissions to anonymous users, and only allow write permissions to authenticated users. diff --git a/rest_framework/templates/rest_framework/docs/error.html b/rest_framework/templates/rest_framework/docs/error.html index 8e67238b9..0c369e9e8 100644 --- a/rest_framework/templates/rest_framework/docs/error.html +++ b/rest_framework/templates/rest_framework/docs/error.html @@ -30,7 +30,7 @@ being applied unexpectedly?

Your response status code is: {{ response.status_code }}

-

401 Unauthorised.

+

401 Unauthorized.

  • Do you have SessionAuthentication enabled?
  • Are you logged in?
  • From ea03e95174f46003e7e917b623c5316247b8b316 Mon Sep 17 00:00:00 2001 From: Christian Franke Date: Tue, 4 Apr 2023 10:44:59 +0200 Subject: [PATCH 06/37] docs: Fix authentication / authorization mixup (#8930) `IsAuthenticatedOrReadOnly` authorizes users that are not authenticated for read-only access to resources. Therefore, they are unauthenticated users, not unauthorized users. --- docs/api-guide/permissions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index e70cc63be..5e0b6a153 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -165,7 +165,7 @@ This permission is suitable if you want your API to only be accessible to a subs ## IsAuthenticatedOrReadOnly -The `IsAuthenticatedOrReadOnly` will allow authenticated users to perform any request. Requests for unauthorized users will only be permitted if the request method is one of the "safe" methods; `GET`, `HEAD` or `OPTIONS`. +The `IsAuthenticatedOrReadOnly` will allow authenticated users to perform any request. Requests for unauthenticated users will only be permitted if the request method is one of the "safe" methods; `GET`, `HEAD` or `OPTIONS`. This permission is suitable if you want to your API to allow read permissions to anonymous users, and only allow write permissions to authenticated users. From 959085c1455a7075a8c237a0283c2a6e35dfcd76 Mon Sep 17 00:00:00 2001 From: Arnab Kumar Shil Date: Sat, 8 Apr 2023 08:27:14 +0200 Subject: [PATCH 07/37] Handle Nested Relation in SlugRelatedField when many=False (#8922) * Update relations.py Currently if you define the slug field as a nested relationship in a `SlugRelatedField` while many=False, it will cause an attribute error. For example: For this code: ``` class SomeSerializer(serializers.ModelSerializer): some_field= serializers.SlugRelatedField(queryset=SomeClass.objects.all(), slug_field="foo__bar") ``` The POST request (or save operation) should work just fine, but if you use GET, then it will fail with Attribute error: > AttributeError: 'SomeClass' object has no attribute 'foo__bar' Thus I am handling nested relation here. Reference: https://stackoverflow.com/questions/75878103/drf-attributeerror-when-trying-to-creating-a-instance-with-slugrelatedfield-and/75882424#75882424 * Fixed test cases * code comment changes related to slugrelatedfield * changes based on pre-commit and removed comma which was added accidentally * fixed primary keys of the mock object * added more test cases based on review --------- Co-authored-by: Arnab Shil --- rest_framework/relations.py | 8 ++- tests/test_relations.py | 136 ++++++++++++++++++++++++++++++++++++ tests/utils.py | 5 +- 3 files changed, 147 insertions(+), 2 deletions(-) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 62da685fb..53ea2113b 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -1,6 +1,7 @@ import contextlib import sys from collections import OrderedDict +from operator import attrgetter from urllib import parse from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist @@ -71,6 +72,7 @@ class PKOnlyObject: instance, but still want to return an object with a .pk attribute, in order to keep the same interface as a regular model instance. """ + def __init__(self, pk): self.pk = pk @@ -464,7 +466,11 @@ class SlugRelatedField(RelatedField): self.fail('invalid') def to_representation(self, obj): - return getattr(obj, self.slug_field) + slug = self.slug_field + if "__" in slug: + # handling nested relationship if defined + slug = slug.replace('__', '.') + return attrgetter(slug)(obj) class ManyRelatedField(Field): diff --git a/tests/test_relations.py b/tests/test_relations.py index 7a4db1c48..b9ab15789 100644 --- a/tests/test_relations.py +++ b/tests/test_relations.py @@ -342,6 +342,142 @@ class TestSlugRelatedField(APISimpleTestCase): field.to_internal_value(self.instance.name) +class TestNestedSlugRelatedField(APISimpleTestCase): + def setUp(self): + self.queryset = MockQueryset([ + MockObject( + pk=1, name='foo', nested=MockObject( + pk=2, name='bar', nested=MockObject( + pk=7, name="foobar" + ) + ) + ), + MockObject( + pk=3, name='hello', nested=MockObject( + pk=4, name='world', nested=MockObject( + pk=8, name="helloworld" + ) + ) + ), + MockObject( + pk=5, name='harry', nested=MockObject( + pk=6, name='potter', nested=MockObject( + pk=9, name="harrypotter" + ) + ) + ) + ]) + self.instance = self.queryset.items[2] + self.field = serializers.SlugRelatedField( + slug_field='name', queryset=self.queryset + ) + self.nested_field = serializers.SlugRelatedField( + slug_field='nested__name', queryset=self.queryset + ) + + self.nested_nested_field = serializers.SlugRelatedField( + slug_field='nested__nested__name', queryset=self.queryset + ) + + # testing nested inside nested relations + def test_slug_related_nested_nested_lookup_exists(self): + instance = self.nested_nested_field.to_internal_value( + self.instance.nested.nested.name + ) + assert instance is self.instance + + def test_slug_related_nested_nested_lookup_does_not_exist(self): + with pytest.raises(serializers.ValidationError) as excinfo: + self.nested_nested_field.to_internal_value('doesnotexist') + msg = excinfo.value.detail[0] + assert msg == \ + 'Object with nested__nested__name=doesnotexist does not exist.' + + def test_slug_related_nested_nested_lookup_invalid_type(self): + with pytest.raises(serializers.ValidationError) as excinfo: + self.nested_nested_field.to_internal_value(BadType()) + msg = excinfo.value.detail[0] + assert msg == 'Invalid value.' + + def test_nested_nested_representation(self): + representation =\ + self.nested_nested_field.to_representation(self.instance) + assert representation == self.instance.nested.nested.name + + def test_nested_nested_overriding_get_queryset(self): + qs = self.queryset + + class NoQuerySetSlugRelatedField(serializers.SlugRelatedField): + def get_queryset(self): + return qs + + field = NoQuerySetSlugRelatedField(slug_field='nested__nested__name') + field.to_internal_value(self.instance.nested.nested.name) + + # testing nested relations + def test_slug_related_nested_lookup_exists(self): + instance = \ + self.nested_field.to_internal_value(self.instance.nested.name) + assert instance is self.instance + + def test_slug_related_nested_lookup_does_not_exist(self): + with pytest.raises(serializers.ValidationError) as excinfo: + self.nested_field.to_internal_value('doesnotexist') + msg = excinfo.value.detail[0] + assert msg == 'Object with nested__name=doesnotexist does not exist.' + + def test_slug_related_nested_lookup_invalid_type(self): + with pytest.raises(serializers.ValidationError) as excinfo: + self.nested_field.to_internal_value(BadType()) + msg = excinfo.value.detail[0] + assert msg == 'Invalid value.' + + def test_nested_representation(self): + representation = self.nested_field.to_representation(self.instance) + assert representation == self.instance.nested.name + + def test_nested_overriding_get_queryset(self): + qs = self.queryset + + class NoQuerySetSlugRelatedField(serializers.SlugRelatedField): + def get_queryset(self): + return qs + + field = NoQuerySetSlugRelatedField(slug_field='nested__name') + field.to_internal_value(self.instance.nested.name) + + # testing non-nested relations + def test_slug_related_lookup_exists(self): + instance = self.field.to_internal_value(self.instance.name) + assert instance is self.instance + + def test_slug_related_lookup_does_not_exist(self): + with pytest.raises(serializers.ValidationError) as excinfo: + self.field.to_internal_value('doesnotexist') + msg = excinfo.value.detail[0] + assert msg == 'Object with name=doesnotexist does not exist.' + + def test_slug_related_lookup_invalid_type(self): + with pytest.raises(serializers.ValidationError) as excinfo: + self.field.to_internal_value(BadType()) + msg = excinfo.value.detail[0] + assert msg == 'Invalid value.' + + def test_representation(self): + representation = self.field.to_representation(self.instance) + assert representation == self.instance.name + + def test_overriding_get_queryset(self): + qs = self.queryset + + class NoQuerySetSlugRelatedField(serializers.SlugRelatedField): + def get_queryset(self): + return qs + + field = NoQuerySetSlugRelatedField(slug_field='name') + field.to_internal_value(self.instance.name) + + class TestManyRelatedField(APISimpleTestCase): def setUp(self): self.instance = MockObject(pk=1, name='foo') diff --git a/tests/utils.py b/tests/utils.py index 06e5b9abe..4ceb35309 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,3 +1,5 @@ +from operator import attrgetter + from django.core.exceptions import ObjectDoesNotExist from django.urls import NoReverseMatch @@ -26,7 +28,7 @@ class MockQueryset: def get(self, **lookup): for item in self.items: if all([ - getattr(item, key, None) == value + attrgetter(key.replace('__', '.'))(item) == value for key, value in lookup.items() ]): return item @@ -39,6 +41,7 @@ class BadType: will raise a `TypeError`, as occurs in Django when making queryset lookups with an incorrect type for the lookup value. """ + def __eq__(self): raise TypeError() From 4842ad1b6ae9a2d08cb479c5d254aa3276ea2352 Mon Sep 17 00:00:00 2001 From: Nikita Reznikov <63803175+rnv812@users.noreply.github.com> Date: Sat, 8 Apr 2023 11:56:49 +0300 Subject: [PATCH 08/37] Add username search field for TokenAdmin (#8927) (#8934) * Add username search field for TokenAdmin (#8927) * Sort imports in a proper order (#8927) --- rest_framework/authtoken/admin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rest_framework/authtoken/admin.py b/rest_framework/authtoken/admin.py index e41eb0002..163328eb0 100644 --- a/rest_framework/authtoken/admin.py +++ b/rest_framework/authtoken/admin.py @@ -4,6 +4,7 @@ from django.contrib.admin.views.main import ChangeList from django.contrib.auth import get_user_model from django.core.exceptions import ValidationError from django.urls import reverse +from django.utils.translation import gettext_lazy as _ from rest_framework.authtoken.models import Token, TokenProxy @@ -23,6 +24,8 @@ class TokenChangeList(ChangeList): class TokenAdmin(admin.ModelAdmin): list_display = ('key', 'user', 'created') fields = ('user',) + search_fields = ('user__username',) + search_help_text = _('Username') ordering = ('-created',) actions = None # Actions not compatible with mapped IDs. autocomplete_fields = ("user",) From 62abf6ac1f20b48809c55f92d086d5f06d4c6c55 Mon Sep 17 00:00:00 2001 From: Maxwell Muoto Date: Sat, 8 Apr 2023 04:16:00 -0500 Subject: [PATCH 09/37] Use ZoneInfo as primary source of timezone data (#8924) * Use ZoneInfo as primary source of timezone data * Update tests/test_fields.py --------- Co-authored-by: Asif Saif Uddin --- rest_framework/fields.py | 8 ++++- rest_framework/utils/timezone.py | 25 ++++++++++++++++ setup.py | 2 +- tests/test_fields.py | 51 ++++++++++++++++++++++++-------- 4 files changed, 72 insertions(+), 14 deletions(-) create mode 100644 rest_framework/utils/timezone.py diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 613bd325a..e41b56fb0 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -35,6 +35,7 @@ from rest_framework.exceptions import ErrorDetail, ValidationError from rest_framework.settings import api_settings from rest_framework.utils import html, humanize_datetime, json, representation from rest_framework.utils.formatting import lazy_format +from rest_framework.utils.timezone import valid_datetime from rest_framework.validators import ProhibitSurrogateCharactersValidator @@ -1154,7 +1155,12 @@ class DateTimeField(Field): except OverflowError: self.fail('overflow') try: - return timezone.make_aware(value, field_timezone) + dt = timezone.make_aware(value, field_timezone) + # When the resulting datetime is a ZoneInfo instance, it won't necessarily + # throw given an invalid datetime, so we need to specifically check. + if not valid_datetime(dt): + self.fail('make_aware', timezone=field_timezone) + return dt except InvalidTimeError: self.fail('make_aware', timezone=field_timezone) elif (field_timezone is None) and timezone.is_aware(value): diff --git a/rest_framework/utils/timezone.py b/rest_framework/utils/timezone.py new file mode 100644 index 000000000..3257c8e27 --- /dev/null +++ b/rest_framework/utils/timezone.py @@ -0,0 +1,25 @@ +from datetime import datetime, timezone, tzinfo + + +def datetime_exists(dt): + """Check if a datetime exists. Taken from: https://pytz-deprecation-shim.readthedocs.io/en/latest/migration.html""" + # There are no non-existent times in UTC, and comparisons between + # aware time zones always compare absolute times; if a datetime is + # not equal to the same datetime represented in UTC, it is imaginary. + return dt.astimezone(timezone.utc) == dt + + +def datetime_ambiguous(dt: datetime): + """Check whether a datetime is ambiguous. Taken from: https://pytz-deprecation-shim.readthedocs.io/en/latest/migration.html""" + # If a datetime exists and its UTC offset changes in response to + # changing `fold`, it is ambiguous in the zone specified. + return datetime_exists(dt) and ( + dt.replace(fold=not dt.fold).utcoffset() != dt.utcoffset() + ) + + +def valid_datetime(dt): + """Returns True if the datetime is not ambiguous or imaginary, False otherwise.""" + if isinstance(dt.tzinfo, tzinfo) and not datetime_ambiguous(dt): + return True + return False diff --git a/setup.py b/setup.py index 9a5b272f3..d9002fdb9 100755 --- a/setup.py +++ b/setup.py @@ -82,7 +82,7 @@ setup( author_email='tom@tomchristie.com', # SEE NOTE BELOW (*) packages=find_packages(exclude=['tests*']), include_package_data=True, - install_requires=["django>=3.0", "pytz"], + install_requires=["django>=3.0", "pytz", 'backports.zoneinfo;python_version<"3.9"'], python_requires=">=3.6", zip_safe=False, classifiers=[ diff --git a/tests/test_fields.py b/tests/test_fields.py index 56e2a45ba..5804d7b3b 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -5,6 +5,7 @@ import re import sys import uuid from decimal import ROUND_DOWN, ROUND_UP, Decimal +from unittest.mock import patch import pytest import pytz @@ -21,6 +22,11 @@ from rest_framework.fields import ( ) from tests.models import UUIDForeignKeyTarget +if sys.version_info >= (3, 9): + from zoneinfo import ZoneInfo +else: + from backports.zoneinfo import ZoneInfo + utc = datetime.timezone.utc # Tests for helper functions. @@ -651,7 +657,7 @@ class FieldValues: """ Base class for testing valid and invalid input values. """ - def test_valid_inputs(self): + def test_valid_inputs(self, *args): """ Ensure that valid values return the expected validated data. """ @@ -659,7 +665,7 @@ class FieldValues: assert self.field.run_validation(input_value) == expected_output, \ 'input value: {}'.format(repr(input_value)) - def test_invalid_inputs(self): + def test_invalid_inputs(self, *args): """ Ensure that invalid values raise the expected validation error. """ @@ -669,7 +675,7 @@ class FieldValues: assert exc_info.value.detail == expected_failure, \ 'input value: {}'.format(repr(input_value)) - def test_outputs(self): + def test_outputs(self, *args): for output_value, expected_output in get_items(self.outputs): assert self.field.to_representation(output_value) == expected_output, \ 'output value: {}'.format(repr(output_value)) @@ -1505,12 +1511,12 @@ class TestTZWithDateTimeField(FieldValues): @classmethod def setup_class(cls): # use class setup method, as class-level attribute will still be evaluated even if test is skipped - kolkata = pytz.timezone('Asia/Kolkata') + kolkata = ZoneInfo('Asia/Kolkata') cls.valid_inputs = { - '2016-12-19T10:00:00': kolkata.localize(datetime.datetime(2016, 12, 19, 10)), - '2016-12-19T10:00:00+05:30': kolkata.localize(datetime.datetime(2016, 12, 19, 10)), - datetime.datetime(2016, 12, 19, 10): kolkata.localize(datetime.datetime(2016, 12, 19, 10)), + '2016-12-19T10:00:00': datetime.datetime(2016, 12, 19, 10, tzinfo=kolkata), + '2016-12-19T10:00:00+05:30': datetime.datetime(2016, 12, 19, 10, tzinfo=kolkata), + datetime.datetime(2016, 12, 19, 10): datetime.datetime(2016, 12, 19, 10, tzinfo=kolkata), } cls.invalid_inputs = {} cls.outputs = { @@ -1529,7 +1535,7 @@ class TestDefaultTZDateTimeField(TestCase): @classmethod def setup_class(cls): cls.field = serializers.DateTimeField() - cls.kolkata = pytz.timezone('Asia/Kolkata') + cls.kolkata = ZoneInfo('Asia/Kolkata') def assertUTC(self, tzinfo): """ @@ -1551,18 +1557,17 @@ class TestDefaultTZDateTimeField(TestCase): self.assertUTC(self.field.default_timezone()) -@pytest.mark.skipif(pytz is None, reason='pytz not installed') @override_settings(TIME_ZONE='UTC', USE_TZ=True) class TestCustomTimezoneForDateTimeField(TestCase): @classmethod def setup_class(cls): - cls.kolkata = pytz.timezone('Asia/Kolkata') + cls.kolkata = ZoneInfo('Asia/Kolkata') cls.date_format = '%d/%m/%Y %H:%M' def test_should_render_date_time_in_default_timezone(self): field = serializers.DateTimeField(default_timezone=self.kolkata, format=self.date_format) - dt = datetime.datetime(2018, 2, 8, 14, 15, 16, tzinfo=pytz.utc) + dt = datetime.datetime(2018, 2, 8, 14, 15, 16, tzinfo=ZoneInfo("UTC")) with override(self.kolkata): rendered_date = field.to_representation(dt) @@ -1572,7 +1577,8 @@ class TestCustomTimezoneForDateTimeField(TestCase): assert rendered_date == rendered_date_in_timezone -class TestNaiveDayLightSavingTimeTimeZoneDateTimeField(FieldValues): +@pytest.mark.skipif(pytz is None, reason="As Django 4.0 has deprecated pytz, this test should eventually be able to get removed.") +class TestPytzNaiveDayLightSavingTimeTimeZoneDateTimeField(FieldValues): """ Invalid values for `DateTimeField` with datetime in DST shift (non-existing or ambiguous) and timezone with DST. Timezone America/New_York has DST shift from 2017-03-12T02:00:00 to 2017-03-12T03:00:00 and @@ -1596,6 +1602,27 @@ class TestNaiveDayLightSavingTimeTimeZoneDateTimeField(FieldValues): field = serializers.DateTimeField(default_timezone=MockTimezone()) +@patch('rest_framework.utils.timezone.datetime_ambiguous', return_value=True) +class TestNaiveDayLightSavingTimeTimeZoneDateTimeField(FieldValues): + """ + Invalid values for `DateTimeField` with datetime in DST shift (non-existing or ambiguous) and timezone with DST. + Timezone America/New_York has DST shift from 2017-03-12T02:00:00 to 2017-03-12T03:00:00 and + from 2017-11-05T02:00:00 to 2017-11-05T01:00:00 in 2017. + """ + valid_inputs = {} + invalid_inputs = { + '2017-03-12T02:30:00': ['Invalid datetime for the timezone "America/New_York".'], + '2017-11-05T01:30:00': ['Invalid datetime for the timezone "America/New_York".'] + } + outputs = {} + + class MockZoneInfoTimezone(datetime.tzinfo): + def __str__(self): + return 'America/New_York' + + field = serializers.DateTimeField(default_timezone=MockZoneInfoTimezone()) + + class TestTimeField(FieldValues): """ Valid and invalid values for `TimeField`. From b1cec517ff33d633d3ebcf5794a5f0f0583fabe6 Mon Sep 17 00:00:00 2001 From: ddelange <14880945+ddelange@users.noreply.github.com> Date: Sat, 8 Apr 2023 12:42:28 +0200 Subject: [PATCH 10/37] Ensure CursorPagination respects nulls in the ordering field (#8912) * Ensure CursorPagination respects nulls in the ordering field * Lint * Fix pagination tests * Add test_ascending with nulls * Push tests for nulls * Test pass * Add comment * Fix test for django30 --- rest_framework/pagination.py | 16 +++-- tests/test_pagination.py | 134 ++++++++++++++++++++++++++++++++++- 2 files changed, 142 insertions(+), 8 deletions(-) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index f5c5b913b..34d71f828 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -10,6 +10,7 @@ from urllib import parse from django.core.paginator import InvalidPage from django.core.paginator import Paginator as DjangoPaginator +from django.db.models import Q from django.template import loader from django.utils.encoding import force_str from django.utils.translation import gettext_lazy as _ @@ -620,7 +621,7 @@ class CursorPagination(BasePagination): queryset = queryset.order_by(*self.ordering) # If we have a cursor with a fixed position then filter by that. - if current_position is not None: + if str(current_position) != 'None': order = self.ordering[0] is_reversed = order.startswith('-') order_attr = order.lstrip('-') @@ -631,7 +632,12 @@ class CursorPagination(BasePagination): else: kwargs = {order_attr + '__gt': current_position} - queryset = queryset.filter(**kwargs) + filter_query = Q(**kwargs) + # If some records contain a null for the ordering field, don't lose them. + # When reverse ordering, nulls will come last and need to be included. + if (reverse and not is_reversed) or is_reversed: + filter_query |= Q(**{order_attr + '__isnull': True}) + queryset = queryset.filter(filter_query) # If we have an offset cursor then offset the entire page by that amount. # We also always fetch an extra item in order to determine if there is a @@ -704,7 +710,7 @@ class CursorPagination(BasePagination): # The item in this position and the item following it # have different positions. We can use this position as # our marker. - has_item_with_unique_position = True + has_item_with_unique_position = position is not None break # The item in this position has the same position as the item @@ -757,7 +763,7 @@ class CursorPagination(BasePagination): # The item in this position and the item following it # have different positions. We can use this position as # our marker. - has_item_with_unique_position = True + has_item_with_unique_position = position is not None break # The item in this position has the same position as the item @@ -883,7 +889,7 @@ class CursorPagination(BasePagination): attr = instance[field_name] else: attr = getattr(instance, field_name) - return str(attr) + return None if attr is None else str(attr) def get_paginated_response(self, data): return Response(OrderedDict([ diff --git a/tests/test_pagination.py b/tests/test_pagination.py index 2812c4489..8f9b20a0d 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -951,17 +951,24 @@ class TestCursorPagination(CursorPaginationTestsMixin): def __init__(self, items): self.items = items - def filter(self, created__gt=None, created__lt=None): + def filter(self, q): + q_args = dict(q.deconstruct()[1]) + if not q_args: + # django 3.0.x artifact + q_args = dict(q.deconstruct()[2]) + created__gt = q_args.get('created__gt') + created__lt = q_args.get('created__lt') + if created__gt is not None: return MockQuerySet([ item for item in self.items - if item.created > int(created__gt) + if item.created is None or item.created > int(created__gt) ]) assert created__lt is not None return MockQuerySet([ item for item in self.items - if item.created < int(created__lt) + if item.created is None or item.created < int(created__lt) ]) def order_by(self, *ordering): @@ -1080,6 +1087,127 @@ class TestCursorPaginationWithValueQueryset(CursorPaginationTestsMixin, TestCase return (previous, current, next, previous_url, next_url) +class NullableCursorPaginationModel(models.Model): + created = models.IntegerField(null=True) + + +class TestCursorPaginationWithNulls(TestCase): + """ + Unit tests for `pagination.CursorPagination` with ordering on a nullable field. + """ + + def setUp(self): + class ExamplePagination(pagination.CursorPagination): + page_size = 1 + ordering = 'created' + + self.pagination = ExamplePagination() + data = [ + None, None, 3, 4 + ] + for idx in data: + NullableCursorPaginationModel.objects.create(created=idx) + + self.queryset = NullableCursorPaginationModel.objects.all() + + get_pages = TestCursorPagination.get_pages + + def test_ascending(self): + """Test paginating one row at a time, current should go 1, 2, 3, 4, 3, 2, 1.""" + (previous, current, next, previous_url, next_url) = self.get_pages('/') + + assert previous is None + assert current == [None] + assert next == [None] + + (previous, current, next, previous_url, next_url) = self.get_pages(next_url) + + assert previous == [None] + assert current == [None] + assert next == [3] + + (previous, current, next, previous_url, next_url) = self.get_pages(next_url) + + assert previous == [3] # [None] paging artifact documented at https://github.com/ddelange/django-rest-framework/blob/3.14.0/rest_framework/pagination.py#L789 + assert current == [3] + assert next == [4] + + (previous, current, next, previous_url, next_url) = self.get_pages(next_url) + + assert previous == [3] + assert current == [4] + assert next is None + assert next_url is None + + (previous, current, next, previous_url, next_url) = self.get_pages(previous_url) + + assert previous == [None] + assert current == [3] + assert next == [4] + + (previous, current, next, previous_url, next_url) = self.get_pages(previous_url) + + assert previous == [None] + assert current == [None] + assert next == [None] # [3] paging artifact documented at https://github.com/ddelange/django-rest-framework/blob/3.14.0/rest_framework/pagination.py#L731 + + (previous, current, next, previous_url, next_url) = self.get_pages(previous_url) + + assert previous is None + assert current == [None] + assert next == [None] + + def test_descending(self): + """Test paginating one row at a time, current should go 4, 3, 2, 1, 2, 3, 4.""" + self.pagination.ordering = ('-created',) + (previous, current, next, previous_url, next_url) = self.get_pages('/') + + assert previous is None + assert current == [4] + assert next == [3] + + (previous, current, next, previous_url, next_url) = self.get_pages(next_url) + + assert previous == [None] # [4] paging artifact + assert current == [3] + assert next == [None] + + (previous, current, next, previous_url, next_url) = self.get_pages(next_url) + + assert previous == [None] # [3] paging artifact + assert current == [None] + assert next == [None] + + (previous, current, next, previous_url, next_url) = self.get_pages(next_url) + + assert previous == [None] + assert current == [None] + assert next is None + assert next_url is None + + (previous, current, next, previous_url, next_url) = self.get_pages(previous_url) + + assert previous == [3] + assert current == [None] + assert next == [None] + + (previous, current, next, previous_url, next_url) = self.get_pages(previous_url) + + assert previous == [None] + assert current == [3] + assert next == [3] # [4] paging artifact documented at https://github.com/ddelange/django-rest-framework/blob/3.14.0/rest_framework/pagination.py#L731 + + # skip back artifact + (previous, current, next, previous_url, next_url) = self.get_pages(previous_url) + (previous, current, next, previous_url, next_url) = self.get_pages(previous_url) + + (previous, current, next, previous_url, next_url) = self.get_pages(previous_url) + + assert previous is None + assert current == [4] + assert next == [3] + + def test_get_displayed_page_numbers(): """ Test our contextual page display function. From 0d6ef034d2eed788f4fe6f9721148bf3874802ec Mon Sep 17 00:00:00 2001 From: Maxwell Muoto Date: Sun, 9 Apr 2023 03:53:47 -0500 Subject: [PATCH 11/37] Implement `__eq__` for validators (#8925) * Implement equality operator and add test coverage * Add documentation on implementation --- docs/api-guide/validators.md | 2 +- rest_framework/validators.py | 37 ++++++++++++++++++++++++++++++++++++ tests/test_validators.py | 11 +++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/validators.md b/docs/api-guide/validators.md index bb8466a2c..dac937d9b 100644 --- a/docs/api-guide/validators.md +++ b/docs/api-guide/validators.md @@ -53,7 +53,7 @@ If we open up the Django shell using `manage.py shell` we can now The interesting bit here is the `reference` field. We can see that the uniqueness constraint is being explicitly enforced by a validator on the serializer field. -Because of this more explicit style REST framework includes a few validator classes that are not available in core Django. These classes are detailed below. +Because of this more explicit style REST framework includes a few validator classes that are not available in core Django. These classes are detailed below. REST framework validators, like their Django counterparts, implement the `__eq__` method, allowing you to compare instances for equality. --- diff --git a/rest_framework/validators.py b/rest_framework/validators.py index a5cb75a84..07ad11b47 100644 --- a/rest_framework/validators.py +++ b/rest_framework/validators.py @@ -79,6 +79,15 @@ class UniqueValidator: smart_repr(self.queryset) ) + def __eq__(self, other): + if not isinstance(other, self.__class__): + return NotImplemented + return (self.message == other.message + and self.requires_context == other.requires_context + and self.queryset == other.queryset + and self.lookup == other.lookup + ) + class UniqueTogetherValidator: """ @@ -166,6 +175,16 @@ class UniqueTogetherValidator: smart_repr(self.fields) ) + def __eq__(self, other): + if not isinstance(other, self.__class__): + return NotImplemented + return (self.message == other.message + and self.requires_context == other.requires_context + and self.missing_message == other.missing_message + and self.queryset == other.queryset + and self.fields == other.fields + ) + class ProhibitSurrogateCharactersValidator: message = _('Surrogate characters are not allowed: U+{code_point:X}.') @@ -177,6 +196,13 @@ class ProhibitSurrogateCharactersValidator: message = self.message.format(code_point=ord(surrogate_character)) raise ValidationError(message, code=self.code) + def __eq__(self, other): + if not isinstance(other, self.__class__): + return NotImplemented + return (self.message == other.message + and self.code == other.code + ) + class BaseUniqueForValidator: message = None @@ -230,6 +256,17 @@ class BaseUniqueForValidator: self.field: message }, code='unique') + def __eq__(self, other): + if not isinstance(other, self.__class__): + return NotImplemented + return (self.message == other.message + and self.missing_message == other.missing_message + and self.requires_context == other.requires_context + and self.queryset == other.queryset + and self.field == other.field + and self.date_field == other.date_field + ) + def __repr__(self): return '<%s(queryset=%s, field=%s, date_field=%s)>' % ( self.__class__.__name__, diff --git a/tests/test_validators.py b/tests/test_validators.py index 35fef6f26..1cf42ed07 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -1,4 +1,5 @@ import datetime +from unittest.mock import MagicMock import pytest from django.db import DataError, models @@ -787,3 +788,13 @@ class ValidatorsTests(TestCase): validator.filter_queryset( attrs=None, queryset=None, field_name='', date_field_name='' ) + + def test_equality_operator(self): + mock_queryset = MagicMock() + validator = BaseUniqueForValidator(queryset=mock_queryset, field='foo', + date_field='bar') + validator2 = BaseUniqueForValidator(queryset=mock_queryset, field='foo', + date_field='bar') + assert validator == validator2 + validator2.date_field = "bar2" + assert validator != validator2 From 684522807f370d6dca731d1d34bffeaa051fe505 Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Thu, 13 Apr 2023 21:48:45 +0600 Subject: [PATCH 12/37] test codecov gha (#8946) --- .github/workflows/main.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f245f6964..e3435208b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,7 +9,7 @@ on: jobs: tests: name: Python ${{ matrix.python-version }} - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: matrix: @@ -34,7 +34,7 @@ jobs: run: python -m pip install --upgrade pip setuptools virtualenv wheel - name: Install dependencies - run: python -m pip install --upgrade codecov tox + run: python -m pip install --upgrade tox - name: Install tox-py if: ${{ matrix.python-version == '3.6' }} @@ -54,5 +54,8 @@ jobs: tox -e base,dist,docs - name: Upload coverage - run: | - codecov -e TOXENV,DJANGO + - uses: codecov/codecov-action@v3.1.0 + with: + flags: unittests # optional + fail_ci_if_error: true # optional (default = false) + verbose: true # optional (default = false) From 38a74b42da10576857d6bf8bd82a73b15d12a7ed Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Sat, 15 Apr 2023 12:11:35 +0600 Subject: [PATCH 13/37] Revert "test codecov gha (#8946)" (#8947) This reverts commit 684522807f370d6dca731d1d34bffeaa051fe505. --- .github/workflows/main.yml | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e3435208b..f245f6964 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,7 +9,7 @@ on: jobs: tests: name: Python ${{ matrix.python-version }} - runs-on: ubuntu-22.04 + runs-on: ubuntu-20.04 strategy: matrix: @@ -34,7 +34,7 @@ jobs: run: python -m pip install --upgrade pip setuptools virtualenv wheel - name: Install dependencies - run: python -m pip install --upgrade tox + run: python -m pip install --upgrade codecov tox - name: Install tox-py if: ${{ matrix.python-version == '3.6' }} @@ -54,8 +54,5 @@ jobs: tox -e base,dist,docs - name: Upload coverage - - uses: codecov/codecov-action@v3.1.0 - with: - flags: unittests # optional - fail_ci_if_error: true # optional (default = false) - verbose: true # optional (default = false) + run: | + codecov -e TOXENV,DJANGO From 1ce0853ac51635526dc1a285e6b83c9848002f0e Mon Sep 17 00:00:00 2001 From: Mahdi Rahimi <31624047+mahdirahimi1999@users.noreply.github.com> Date: Thu, 27 Apr 2023 07:54:13 +0330 Subject: [PATCH 14/37] Refactor get_field_info method to include max_digits and decimal_places attributes in SimpleMetadata class (#8943) * Refactor get_field_info method to include max_digits and decimal_places attributes in SimpleMetadata class * Add new test to check decimal_field_info_type * Update rest_framework/metadata.py --------- Co-authored-by: Mahdi Co-authored-by: Asif Saif Uddin --- rest_framework/metadata.py | 3 ++- tests/test_metadata.py | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/rest_framework/metadata.py b/rest_framework/metadata.py index 015587897..c400b1b79 100644 --- a/rest_framework/metadata.py +++ b/rest_framework/metadata.py @@ -124,7 +124,8 @@ class SimpleMetadata(BaseMetadata): attrs = [ 'read_only', 'label', 'help_text', 'min_length', 'max_length', - 'min_value', 'max_value' + 'min_value', 'max_value', + 'max_digits', 'decimal_places' ] for attr in attrs: diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 4abc0fc07..1bdc8697c 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -324,6 +324,13 @@ class TestSimpleMetadataFieldInfo(TestCase): ) assert 'choices' not in field_info + def test_decimal_field_info_type(self): + options = metadata.SimpleMetadata() + field_info = options.get_field_info(serializers.DecimalField(max_digits=18, decimal_places=4)) + assert field_info['type'] == 'decimal' + assert field_info['max_digits'] == 18 + assert field_info['decimal_places'] == 4 + class TestModelSerializerMetadata(TestCase): def test_read_only_primary_key_related_field(self): From 54307a4394820173f7bfeaed53a675c00563bf18 Mon Sep 17 00:00:00 2001 From: suayip uzulmez <17948971+realsuayip@users.noreply.github.com> Date: Sun, 30 Apr 2023 12:20:02 +0300 Subject: [PATCH 15/37] Replaced `OrderedDict` with `dict` (#8964) --- rest_framework/fields.py | 9 ++--- rest_framework/metadata.py | 26 ++++++------ rest_framework/pagination.py | 36 ++++++++--------- rest_framework/relations.py | 11 ++--- rest_framework/renderers.py | 3 +- rest_framework/routers.py | 6 +-- rest_framework/schemas/coreapi.py | 12 +++--- rest_framework/schemas/openapi.py | 3 +- rest_framework/serializers.py | 40 +++++++++---------- rest_framework/templatetags/rest_framework.py | 9 ++--- rest_framework/utils/model_meta.py | 21 ++++------ rest_framework/utils/serializer_helpers.py | 5 +-- rest_framework/viewsets.py | 3 +- tests/test_model_serializer.py | 5 +-- tests/test_renderers.py | 7 ++-- tests/test_viewsets.py | 33 ++++++++------- 16 files changed, 105 insertions(+), 124 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index e41b56fb0..1d77ce4ab 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -6,7 +6,6 @@ import functools import inspect import re import uuid -from collections import OrderedDict from collections.abc import Mapping from django.conf import settings @@ -143,7 +142,7 @@ def to_choices_dict(choices): # choices = [1, 2, 3] # choices = [(1, 'First'), (2, 'Second'), (3, 'Third')] # choices = [('Category', ((1, 'First'), (2, 'Second'))), (3, 'Third')] - ret = OrderedDict() + ret = {} for choice in choices: if not isinstance(choice, (list, tuple)): # single choice @@ -166,7 +165,7 @@ def flatten_choices_dict(choices): flatten_choices_dict({1: '1st', 2: '2nd'}) -> {1: '1st', 2: '2nd'} flatten_choices_dict({'Group': {1: '1st', 2: '2nd'}}) -> {1: '1st', 2: '2nd'} """ - ret = OrderedDict() + ret = {} for key, value in choices.items(): if isinstance(value, dict): # grouped choices (category, sub choices) @@ -1649,7 +1648,7 @@ class ListField(Field): def run_child_validation(self, data): result = [] - errors = OrderedDict() + errors = {} for idx, item in enumerate(data): try: @@ -1713,7 +1712,7 @@ class DictField(Field): def run_child_validation(self, data): result = {} - errors = OrderedDict() + errors = {} for key, value in data.items(): key = str(key) diff --git a/rest_framework/metadata.py b/rest_framework/metadata.py index c400b1b79..364ca5b14 100644 --- a/rest_framework/metadata.py +++ b/rest_framework/metadata.py @@ -6,8 +6,6 @@ some fairly ad-hoc information about the view. Future implementations might use JSON schema or other definitions in order to return this information in a more standardized way. """ -from collections import OrderedDict - from django.core.exceptions import PermissionDenied from django.http import Http404 from django.utils.encoding import force_str @@ -59,11 +57,12 @@ class SimpleMetadata(BaseMetadata): }) def determine_metadata(self, request, view): - metadata = OrderedDict() - metadata['name'] = view.get_view_name() - metadata['description'] = view.get_view_description() - metadata['renders'] = [renderer.media_type for renderer in view.renderer_classes] - metadata['parses'] = [parser.media_type for parser in view.parser_classes] + metadata = { + "name": view.get_view_name(), + "description": view.get_view_description(), + "renders": [renderer.media_type for renderer in view.renderer_classes], + "parses": [parser.media_type for parser in view.parser_classes], + } if hasattr(view, 'get_serializer'): actions = self.determine_actions(request, view) if actions: @@ -106,20 +105,21 @@ class SimpleMetadata(BaseMetadata): # If this is a `ListSerializer` then we want to examine the # underlying child serializer instance instead. serializer = serializer.child - return OrderedDict([ - (field_name, self.get_field_info(field)) + return { + field_name: self.get_field_info(field) for field_name, field in serializer.fields.items() if not isinstance(field, serializers.HiddenField) - ]) + } def get_field_info(self, field): """ Given an instance of a serializer field, return a dictionary of metadata about it. """ - field_info = OrderedDict() - field_info['type'] = self.label_lookup[field] - field_info['required'] = getattr(field, 'required', False) + field_info = { + "type": self.label_lookup[field], + "required": getattr(field, "required", False), + } attrs = [ 'read_only', 'label', 'help_text', diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 34d71f828..68ab9c786 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -5,7 +5,7 @@ be used for paginated responses. import contextlib from base64 import b64decode, b64encode -from collections import OrderedDict, namedtuple +from collections import namedtuple from urllib import parse from django.core.paginator import InvalidPage @@ -225,12 +225,12 @@ class PageNumberPagination(BasePagination): return page_number def get_paginated_response(self, data): - return Response(OrderedDict([ - ('count', self.page.paginator.count), - ('next', self.get_next_link()), - ('previous', self.get_previous_link()), - ('results', data) - ])) + return Response({ + 'count': self.page.paginator.count, + 'next': self.get_next_link(), + 'previous': self.get_previous_link(), + 'results': data, + }) def get_paginated_response_schema(self, schema): return { @@ -395,12 +395,12 @@ class LimitOffsetPagination(BasePagination): return list(queryset[self.offset:self.offset + self.limit]) def get_paginated_response(self, data): - return Response(OrderedDict([ - ('count', self.count), - ('next', self.get_next_link()), - ('previous', self.get_previous_link()), - ('results', data) - ])) + return Response({ + 'count': self.count, + 'next': self.get_next_link(), + 'previous': self.get_previous_link(), + 'results': data + }) def get_paginated_response_schema(self, schema): return { @@ -892,11 +892,11 @@ class CursorPagination(BasePagination): return None if attr is None else str(attr) def get_paginated_response(self, data): - return Response(OrderedDict([ - ('next', self.get_next_link()), - ('previous', self.get_previous_link()), - ('results', data) - ])) + return Response({ + 'next': self.get_next_link(), + 'previous': self.get_previous_link(), + 'results': data, + }) def get_paginated_response_schema(self, schema): return { diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 53ea2113b..4409bce77 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -1,6 +1,5 @@ import contextlib import sys -from collections import OrderedDict from operator import attrgetter from urllib import parse @@ -199,13 +198,9 @@ class RelatedField(Field): if cutoff is not None: queryset = queryset[:cutoff] - return OrderedDict([ - ( - self.to_representation(item), - self.display_value(item) - ) - for item in queryset - ]) + return { + self.to_representation(item): self.display_value(item) for item in queryset + } @property def choices(self): diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 8de0a77a1..8e8c3a9b3 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -9,7 +9,6 @@ REST framework also provides an HTML renderer that renders the browsable API. import base64 import contextlib -from collections import OrderedDict from urllib import parse from django import forms @@ -653,7 +652,7 @@ class BrowsableAPIRenderer(BaseRenderer): raw_data_patch_form = self.get_raw_data_form(data, view, 'PATCH', request) raw_data_put_or_patch_form = raw_data_put_form or raw_data_patch_form - response_headers = OrderedDict(sorted(response.items())) + response_headers = dict(sorted(response.items())) renderer_content_type = '' if renderer: renderer_content_type = '%s' % renderer.media_type diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 722fc50a6..fa5d16922 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -14,7 +14,7 @@ For example, you might have a `urls.py` that looks something like this: urlpatterns = router.urls """ import itertools -from collections import OrderedDict, namedtuple +from collections import namedtuple from django.core.exceptions import ImproperlyConfigured from django.urls import NoReverseMatch, path, re_path @@ -321,7 +321,7 @@ class APIRootView(views.APIView): def get(self, request, *args, **kwargs): # Return a plain {"name": "hyperlink"} response. - ret = OrderedDict() + ret = {} namespace = request.resolver_match.namespace for key, url_name in self.api_root_dict.items(): if namespace: @@ -365,7 +365,7 @@ class DefaultRouter(SimpleRouter): """ Return a basic root view. """ - api_root_dict = OrderedDict() + api_root_dict = {} list_name = self.routes[0].name for prefix, viewset, basename in self.registry: api_root_dict[prefix] = list_name.format(basename=basename) diff --git a/rest_framework/schemas/coreapi.py b/rest_framework/schemas/coreapi.py index 908518e9d..0713e0cb8 100644 --- a/rest_framework/schemas/coreapi.py +++ b/rest_framework/schemas/coreapi.py @@ -1,5 +1,5 @@ import warnings -from collections import Counter, OrderedDict +from collections import Counter from urllib import parse from django.db import models @@ -54,7 +54,7 @@ to customise schema structure. """ -class LinkNode(OrderedDict): +class LinkNode(dict): def __init__(self): self.links = [] self.methods_counter = Counter() @@ -268,11 +268,11 @@ def field_to_schema(field): ) elif isinstance(field, serializers.Serializer): return coreschema.Object( - properties=OrderedDict([ - (key, field_to_schema(value)) + properties={ + key: field_to_schema(value) for key, value in field.fields.items() - ]), + }, title=title, description=description ) @@ -549,7 +549,7 @@ class AutoSchema(ViewInspector): if not update_with: return fields - by_name = OrderedDict((f.name, f) for f in fields) + by_name = {f.name: f for f in fields} for f in update_with: by_name[f.name] = f fields = list(by_name.values()) diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py index ea220c631..d8707e1e1 100644 --- a/rest_framework/schemas/openapi.py +++ b/rest_framework/schemas/openapi.py @@ -1,6 +1,5 @@ import re import warnings -from collections import OrderedDict from decimal import Decimal from operator import attrgetter from urllib.parse import urljoin @@ -340,7 +339,7 @@ class AutoSchema(ViewInspector): return paginator.get_schema_operation_parameters(view) def map_choicefield(self, field): - choices = list(OrderedDict.fromkeys(field.choices)) # preserve order and remove duplicates + choices = list(dict.fromkeys(field.choices)) # preserve order and remove duplicates if all(isinstance(choice, bool) for choice in choices): type = 'boolean' elif all(isinstance(choice, int) for choice in choices): diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index e27f8a47c..01bebf5fc 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -15,7 +15,7 @@ import contextlib import copy import inspect import traceback -from collections import OrderedDict, defaultdict +from collections import defaultdict from collections.abc import Mapping from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured @@ -308,7 +308,7 @@ class SerializerMetaclass(type): for name, f in base._declared_fields.items() if name not in known ] - return OrderedDict(base_fields + fields) + return dict(base_fields + fields) def __new__(cls, name, bases, attrs): attrs['_declared_fields'] = cls._get_declared_fields(bases, attrs) @@ -393,20 +393,20 @@ class Serializer(BaseSerializer, metaclass=SerializerMetaclass): if hasattr(self, 'initial_data'): # initial_data may not be a valid type if not isinstance(self.initial_data, Mapping): - return OrderedDict() + return {} - return OrderedDict([ - (field_name, field.get_value(self.initial_data)) + return { + field_name: field.get_value(self.initial_data) for field_name, field in self.fields.items() if (field.get_value(self.initial_data) is not empty) and not field.read_only - ]) + } - return OrderedDict([ - (field.field_name, field.get_initial()) + return { + field.field_name: field.get_initial() for field in self.fields.values() if not field.read_only - ]) + } def get_value(self, dictionary): # We override the default field access in order to support @@ -441,7 +441,7 @@ class Serializer(BaseSerializer, metaclass=SerializerMetaclass): if (field.read_only) and (field.default != empty) and (field.source != '*') and ('.' not in field.source) ] - defaults = OrderedDict() + defaults = {} for field in fields: try: default = field.get_default() @@ -474,8 +474,8 @@ class Serializer(BaseSerializer, metaclass=SerializerMetaclass): api_settings.NON_FIELD_ERRORS_KEY: [message] }, code='invalid') - ret = OrderedDict() - errors = OrderedDict() + ret = {} + errors = {} fields = self._writable_fields for field in fields: @@ -503,7 +503,7 @@ class Serializer(BaseSerializer, metaclass=SerializerMetaclass): """ Object instance -> Dict of primitive datatypes. """ - ret = OrderedDict() + ret = {} fields = self._readable_fields for field in fields: @@ -1061,7 +1061,7 @@ class ModelSerializer(Serializer): ) # Determine the fields that should be included on the serializer. - fields = OrderedDict() + fields = {} for field_name in field_names: # If the field is explicitly declared on the class then use that. @@ -1546,16 +1546,16 @@ class ModelSerializer(Serializer): # which may map onto a model field. Any dotted field name lookups # cannot map to a field, and must be a traversal, so we're not # including those. - field_sources = OrderedDict( - (field.field_name, field.source) for field in self._writable_fields + field_sources = { + field.field_name: field.source for field in self._writable_fields if (field.source != '*') and ('.' not in field.source) - ) + } # Special Case: Add read_only fields with defaults. - field_sources.update(OrderedDict( - (field.field_name, field.source) for field in self.fields.values() + field_sources.update({ + field.field_name: field.source for field in self.fields.values() if (field.read_only) and (field.default != empty) and (field.source != '*') and ('.' not in field.source) - )) + }) # Invert so we can find the serializer field names that correspond to # the model field names in the unique_together sets. This also allows diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index ccd9430b4..53916d3f2 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -1,5 +1,4 @@ import re -from collections import OrderedDict from django import template from django.template import loader @@ -49,10 +48,10 @@ def with_location(fields, location): @register.simple_tag def form_for_link(link): import coreschema - properties = OrderedDict([ - (field.name, field.schema or coreschema.String()) + properties = { + field.name: field.schema or coreschema.String() for field in link.fields - ]) + } required = [ field.name for field in link.fields @@ -272,7 +271,7 @@ def schema_links(section, sec_key=None): links.update(new_links) if sec_key is not None: - new_links = OrderedDict() + new_links = {} for link_key, link in links.items(): new_key = NESTED_FORMAT % (sec_key, link_key) new_links.update({new_key: link}) diff --git a/rest_framework/utils/model_meta.py b/rest_framework/utils/model_meta.py index 4cc93b8ef..bd5d9177c 100644 --- a/rest_framework/utils/model_meta.py +++ b/rest_framework/utils/model_meta.py @@ -5,7 +5,7 @@ relationships and their associated metadata. Usage: `get_field_info(model)` returns a `FieldInfo` instance. """ -from collections import OrderedDict, namedtuple +from collections import namedtuple FieldInfo = namedtuple('FieldResult', [ 'pk', # Model field instance @@ -58,7 +58,7 @@ def _get_pk(opts): def _get_fields(opts): - fields = OrderedDict() + fields = {} for field in [field for field in opts.fields if field.serialize and not field.remote_field]: fields[field.name] = field @@ -71,9 +71,9 @@ def _get_to_field(field): def _get_forward_relationships(opts): """ - Returns an `OrderedDict` of field names to `RelationInfo`. + Returns a dict of field names to `RelationInfo`. """ - forward_relations = OrderedDict() + forward_relations = {} for field in [field for field in opts.fields if field.serialize and field.remote_field]: forward_relations[field.name] = RelationInfo( model_field=field, @@ -103,9 +103,9 @@ def _get_forward_relationships(opts): def _get_reverse_relationships(opts): """ - Returns an `OrderedDict` of field names to `RelationInfo`. + Returns a dict of field names to `RelationInfo`. """ - reverse_relations = OrderedDict() + reverse_relations = {} all_related_objects = [r for r in opts.related_objects if not r.field.many_to_many] for relation in all_related_objects: accessor_name = relation.get_accessor_name() @@ -139,19 +139,14 @@ def _get_reverse_relationships(opts): def _merge_fields_and_pk(pk, fields): - fields_and_pk = OrderedDict() - fields_and_pk['pk'] = pk - fields_and_pk[pk.name] = pk + fields_and_pk = {'pk': pk, pk.name: pk} fields_and_pk.update(fields) return fields_and_pk def _merge_relationships(forward_relations, reverse_relations): - return OrderedDict( - list(forward_relations.items()) + - list(reverse_relations.items()) - ) + return {**forward_relations, **reverse_relations} def is_abstract_model(model): diff --git a/rest_framework/utils/serializer_helpers.py b/rest_framework/utils/serializer_helpers.py index 6ca794584..0e59aa66a 100644 --- a/rest_framework/utils/serializer_helpers.py +++ b/rest_framework/utils/serializer_helpers.py @@ -1,6 +1,5 @@ import contextlib import sys -from collections import OrderedDict from collections.abc import Mapping, MutableMapping from django.utils.encoding import force_str @@ -8,7 +7,7 @@ from django.utils.encoding import force_str from rest_framework.utils import json -class ReturnDict(OrderedDict): +class ReturnDict(dict): """ Return object from `serializer.data` for the `Serializer` class. Includes a backlink to the serializer instance for renderers @@ -161,7 +160,7 @@ class BindingDict(MutableMapping): def __init__(self, serializer): self.serializer = serializer - self.fields = OrderedDict() + self.fields = {} def __setitem__(self, key, field): self.fields[key] = field diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py index 1c56f61e8..2eba17b4a 100644 --- a/rest_framework/viewsets.py +++ b/rest_framework/viewsets.py @@ -16,7 +16,6 @@ automatically. router.register(r'users', UserViewSet, 'user') urlpatterns = router.urls """ -from collections import OrderedDict from functools import update_wrapper from inspect import getmembers @@ -183,7 +182,7 @@ class ViewSetMixin: This method will noop if `detail` was not provided as a view initkwarg. """ - action_urls = OrderedDict() + action_urls = {} # exit early if `detail` has not been provided if self.detail is None: diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 419eae632..c5ac888f5 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -10,7 +10,6 @@ import decimal import json # noqa import sys import tempfile -from collections import OrderedDict import django import pytest @@ -762,7 +761,7 @@ class TestRelationalFieldDisplayValue(TestCase): fields = '__all__' serializer = TestSerializer() - expected = OrderedDict([(1, 'Red Color'), (2, 'Yellow Color'), (3, 'Green Color')]) + expected = {1: 'Red Color', 2: 'Yellow Color', 3: 'Green Color'} self.assertEqual(serializer.fields['color'].choices, expected) def test_custom_display_value(self): @@ -778,7 +777,7 @@ class TestRelationalFieldDisplayValue(TestCase): fields = '__all__' serializer = TestSerializer() - expected = OrderedDict([(1, 'My Red Color'), (2, 'My Yellow Color'), (3, 'My Green Color')]) + expected = {1: 'My Red Color', 2: 'My Yellow Color', 3: 'My Green Color'} self.assertEqual(serializer.fields['color'].choices, expected) diff --git a/tests/test_renderers.py b/tests/test_renderers.py index 8271608e1..71d734c86 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -1,5 +1,4 @@ import re -from collections import OrderedDict from collections.abc import MutableMapping import pytest @@ -457,12 +456,12 @@ class CacheRenderTest(TestCase): class TestJSONIndentationStyles: def test_indented(self): renderer = JSONRenderer() - data = OrderedDict([('a', 1), ('b', 2)]) + data = {"a": 1, "b": 2} assert renderer.render(data) == b'{"a":1,"b":2}' def test_compact(self): renderer = JSONRenderer() - data = OrderedDict([('a', 1), ('b', 2)]) + data = {"a": 1, "b": 2} context = {'indent': 4} assert ( renderer.render(data, renderer_context=context) == @@ -472,7 +471,7 @@ class TestJSONIndentationStyles: def test_long_form(self): renderer = JSONRenderer() renderer.compact = False - data = OrderedDict([('a', 1), ('b', 2)]) + data = {"a": 1, "b": 2} assert renderer.render(data) == b'{"a": 1, "b": 2}' diff --git a/tests/test_viewsets.py b/tests/test_viewsets.py index 8842b0b1c..8e439c86e 100644 --- a/tests/test_viewsets.py +++ b/tests/test_viewsets.py @@ -1,4 +1,3 @@ -from collections import OrderedDict from functools import wraps import pytest @@ -261,11 +260,11 @@ class GetExtraActionUrlMapTests(TestCase): response = self.client.get('/api/actions/') view = response.view - expected = OrderedDict([ - ('Custom list action', 'http://testserver/api/actions/custom_list_action/'), - ('List action', 'http://testserver/api/actions/list_action/'), - ('Wrapped list action', 'http://testserver/api/actions/wrapped_list_action/'), - ]) + expected = { + 'Custom list action': 'http://testserver/api/actions/custom_list_action/', + 'List action': 'http://testserver/api/actions/list_action/', + 'Wrapped list action': 'http://testserver/api/actions/wrapped_list_action/', + } self.assertEqual(view.get_extra_action_url_map(), expected) @@ -273,28 +272,28 @@ class GetExtraActionUrlMapTests(TestCase): response = self.client.get('/api/actions/1/') view = response.view - expected = OrderedDict([ - ('Custom detail action', 'http://testserver/api/actions/1/custom_detail_action/'), - ('Detail action', 'http://testserver/api/actions/1/detail_action/'), - ('Wrapped detail action', 'http://testserver/api/actions/1/wrapped_detail_action/'), + expected = { + 'Custom detail action': 'http://testserver/api/actions/1/custom_detail_action/', + 'Detail action': 'http://testserver/api/actions/1/detail_action/', + 'Wrapped detail action': 'http://testserver/api/actions/1/wrapped_detail_action/', # "Unresolvable detail action" excluded, since it's not resolvable - ]) + } self.assertEqual(view.get_extra_action_url_map(), expected) def test_uninitialized_view(self): - self.assertEqual(ActionViewSet().get_extra_action_url_map(), OrderedDict()) + self.assertEqual(ActionViewSet().get_extra_action_url_map(), {}) def test_action_names(self): # Action 'name' and 'suffix' kwargs should be respected response = self.client.get('/api/names/1/') view = response.view - expected = OrderedDict([ - ('Custom Name', 'http://testserver/api/names/1/named_action/'), - ('Action Names Custom Suffix', 'http://testserver/api/names/1/suffixed_action/'), - ('Unnamed action', 'http://testserver/api/names/1/unnamed_action/'), - ]) + expected = { + 'Custom Name': 'http://testserver/api/names/1/named_action/', + 'Action Names Custom Suffix': 'http://testserver/api/names/1/suffixed_action/', + 'Unnamed action': 'http://testserver/api/names/1/unnamed_action/', + } self.assertEqual(view.get_extra_action_url_map(), expected) From f1a11d41cbd1d16871e8d8426cbf290da57dfb6d Mon Sep 17 00:00:00 2001 From: fdomain Date: Tue, 2 May 2023 02:55:59 +0200 Subject: [PATCH 16/37] fix: fallback on CursorPagination ordering if unset on the view (#8954) * this commit fixes the usage of a CursorPagination combined with a view implementing an ordering filter, without a default ordering value. * former behavior was to fetch the ordering value from the filter, and raises an error if the value was None, preventing the fallback on the ordering set on the CursorPagination class itself. * we reversed the logic by getting first the value set on the class, and override it by the ordering filter if the parameter is present --- rest_framework/pagination.py | 37 +++++++++++++++++------------------- tests/test_pagination.py | 18 ++++++++++++++++++ 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 68ab9c786..af508bef6 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -801,6 +801,10 @@ class CursorPagination(BasePagination): """ Return a tuple of strings, that may be used in an `order_by` method. """ + # The default case is to check for an `ordering` attribute + # on this pagination instance. + ordering = self.ordering + ordering_filters = [ filter_cls for filter_cls in getattr(view, 'filter_backends', []) if hasattr(filter_cls, 'get_ordering') @@ -811,26 +815,19 @@ class CursorPagination(BasePagination): # then we defer to that filter to determine the ordering. filter_cls = ordering_filters[0] filter_instance = filter_cls() - ordering = filter_instance.get_ordering(request, queryset, view) - assert ordering is not None, ( - 'Using cursor pagination, but filter class {filter_cls} ' - 'returned a `None` ordering.'.format( - filter_cls=filter_cls.__name__ - ) - ) - else: - # The default case is to check for an `ordering` attribute - # on this pagination instance. - ordering = self.ordering - assert ordering is not None, ( - 'Using cursor pagination, but no ordering attribute was declared ' - 'on the pagination class.' - ) - assert '__' not in ordering, ( - 'Cursor pagination does not support double underscore lookups ' - 'for orderings. Orderings should be an unchanging, unique or ' - 'nearly-unique field on the model, such as "-created" or "pk".' - ) + ordering_from_filter = filter_instance.get_ordering(request, queryset, view) + if ordering_from_filter: + ordering = ordering_from_filter + + assert ordering is not None, ( + 'Using cursor pagination, but no ordering attribute was declared ' + 'on the pagination class.' + ) + assert '__' not in ordering, ( + 'Cursor pagination does not support double underscore lookups ' + 'for orderings. Orderings should be an unchanging, unique or ' + 'nearly-unique field on the model, such as "-created" or "pk".' + ) assert isinstance(ordering, (str, list, tuple)), ( 'Invalid ordering. Expected string or tuple, but got {type}'.format( diff --git a/tests/test_pagination.py b/tests/test_pagination.py index 8f9b20a0d..d606986ab 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -632,6 +632,24 @@ class CursorPaginationTestsMixin: ordering = self.pagination.get_ordering(request, [], MockView()) assert ordering == ('created',) + def test_use_with_ordering_filter_without_ordering_default_value(self): + class MockView: + filter_backends = (filters.OrderingFilter,) + ordering_fields = ['username', 'created'] + + request = Request(factory.get('/')) + ordering = self.pagination.get_ordering(request, [], MockView()) + # it gets the value of `ordering` provided by CursorPagination + assert ordering == ('created',) + + request = Request(factory.get('/', {'ordering': 'username'})) + ordering = self.pagination.get_ordering(request, [], MockView()) + assert ordering == ('username',) + + request = Request(factory.get('/', {'ordering': 'invalid'})) + ordering = self.pagination.get_ordering(request, [], MockView()) + assert ordering == ('created',) + def test_cursor_pagination(self): (previous, current, next, previous_url, next_url) = self.get_pages('/') From d14eb7555da4c8986c8a501836943f9fcbc99fb4 Mon Sep 17 00:00:00 2001 From: Mahdi Rahimi <31624047+mahdirahimi1999@users.noreply.github.com> Date: Tue, 2 May 2023 19:39:19 +0330 Subject: [PATCH 17/37] Refactor read function to use context manager for file handling (#8967) Co-authored-by: Mahdi --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d9002fdb9..533ffa97f 100755 --- a/setup.py +++ b/setup.py @@ -37,7 +37,8 @@ an older version of Django REST Framework: def read(f): - return open(f, 'r', encoding='utf-8').read() + with open(f, 'r', encoding='utf-8') as file: + return file.read() def get_version(package): From e08e606c82afd0d5ec82b2c58badec11a4ce825e Mon Sep 17 00:00:00 2001 From: Saad Aleem Date: Wed, 3 May 2023 12:08:07 +0500 Subject: [PATCH 18/37] Fix mapping for choice values (#8968) * fix mapping for choice values * fix tests for ChoiceField IntegerChoices * fix imports * fix imports in tests * Check for multiple choice enums * fix formatting * add tests for text choices class --- rest_framework/fields.py | 13 ++++++++++- tests/test_fields.py | 50 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 1d77ce4ab..f4fd26539 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -16,6 +16,7 @@ from django.core.validators import ( MinValueValidator, ProhibitNullCharactersValidator, RegexValidator, URLValidator, ip_address_validators ) +from django.db.models import IntegerChoices, TextChoices from django.forms import FilePathField as DjangoFilePathField from django.forms import ImageField as DjangoImageField from django.utils import timezone @@ -1397,6 +1398,10 @@ class ChoiceField(Field): if data == '' and self.allow_blank: return '' + if isinstance(data, (IntegerChoices, TextChoices)) and str(data) != \ + str(data.value): + data = data.value + try: return self.choice_strings_to_values[str(data)] except KeyError: @@ -1405,6 +1410,11 @@ class ChoiceField(Field): def to_representation(self, value): if value in ('', None): return value + + if isinstance(value, (IntegerChoices, TextChoices)) and str(value) != \ + str(value.value): + value = value.value + return self.choice_strings_to_values.get(str(value), value) def iter_options(self): @@ -1428,7 +1438,8 @@ class ChoiceField(Field): # Allows us to deal with eg. integer choices while supporting either # integer or string input, but still get the correct datatype out. self.choice_strings_to_values = { - str(key): key for key in self.choices + str(key.value) if isinstance(key, (IntegerChoices, TextChoices)) + and str(key) != str(key.value) else str(key): key for key in self.choices } choices = property(_get_choices, _set_choices) diff --git a/tests/test_fields.py b/tests/test_fields.py index 5804d7b3b..bf25b71b8 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -5,11 +5,13 @@ import re import sys import uuid from decimal import ROUND_DOWN, ROUND_UP, Decimal +from enum import auto from unittest.mock import patch import pytest import pytz from django.core.exceptions import ValidationError as DjangoValidationError +from django.db.models import IntegerChoices, TextChoices from django.http import QueryDict from django.test import TestCase, override_settings from django.utils.timezone import activate, deactivate, override @@ -1824,6 +1826,54 @@ class TestChoiceField(FieldValues): field.run_validation(2) assert exc_info.value.detail == ['"2" is not a valid choice.'] + def test_integer_choices(self): + class ChoiceCase(IntegerChoices): + first = auto() + second = auto() + # Enum validate + choices = [ + (ChoiceCase.first, "1"), + (ChoiceCase.second, "2") + ] + + field = serializers.ChoiceField(choices=choices) + assert field.run_validation(1) == 1 + assert field.run_validation(ChoiceCase.first) == 1 + assert field.run_validation("1") == 1 + + choices = [ + (ChoiceCase.first.value, "1"), + (ChoiceCase.second.value, "2") + ] + + field = serializers.ChoiceField(choices=choices) + assert field.run_validation(1) == 1 + assert field.run_validation(ChoiceCase.first) == 1 + assert field.run_validation("1") == 1 + + def test_text_choices(self): + class ChoiceCase(TextChoices): + first = auto() + second = auto() + # Enum validate + choices = [ + (ChoiceCase.first, "first"), + (ChoiceCase.second, "second") + ] + + field = serializers.ChoiceField(choices=choices) + assert field.run_validation(ChoiceCase.first) == "first" + assert field.run_validation("first") == "first" + + choices = [ + (ChoiceCase.first.value, "first"), + (ChoiceCase.second.value, "second") + ] + + field = serializers.ChoiceField(choices=choices) + assert field.run_validation(ChoiceCase.first) == "first" + assert field.run_validation("first") == "first" + class TestChoiceFieldWithType(FieldValues): """ From 99e8b4033efa44930ace40fb48a4d7bcd224f9fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ismael=20Jim=C3=A9nez=20S=C3=A1nchez?= Date: Tue, 9 May 2023 16:50:29 +0200 Subject: [PATCH 19/37] feat: enforce Decimal type in min_value and max_value arguments of DecimalField (#8972) * add warning when min_value and max_value are not decimal * remove redundant module name in log --------- Co-authored-by: ismaeljs <> --- rest_framework/fields.py | 8 ++++++++ tests/test_fields.py | 11 +++++++++++ 2 files changed, 19 insertions(+) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index f4fd26539..4f82e4a10 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -4,6 +4,7 @@ import datetime import decimal import functools import inspect +import logging import re import uuid from collections.abc import Mapping @@ -38,6 +39,8 @@ from rest_framework.utils.formatting import lazy_format from rest_framework.utils.timezone import valid_datetime from rest_framework.validators import ProhibitSurrogateCharactersValidator +logger = logging.getLogger("rest_framework.fields") + class empty: """ @@ -990,6 +993,11 @@ class DecimalField(Field): self.max_value = max_value self.min_value = min_value + if self.max_value is not None and not isinstance(self.max_value, decimal.Decimal): + logger.warning("max_value in DecimalField should be Decimal type.") + if self.min_value is not None and not isinstance(self.min_value, decimal.Decimal): + logger.warning("min_value in DecimalField should be Decimal type.") + if self.max_digits is not None and self.decimal_places is not None: self.max_whole_digits = self.max_digits - self.decimal_places else: diff --git a/tests/test_fields.py b/tests/test_fields.py index bf25b71b8..bcf388441 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1216,6 +1216,17 @@ class TestMinMaxDecimalField(FieldValues): min_value=10, max_value=20 ) + def test_warning_when_not_decimal_types(self, caplog): + import logging + serializers.DecimalField( + max_digits=3, decimal_places=1, + min_value=10, max_value=20 + ) + assert caplog.record_tuples == [ + ("rest_framework.fields", logging.WARNING, "max_value in DecimalField should be Decimal type."), + ("rest_framework.fields", logging.WARNING, "min_value in DecimalField should be Decimal type.") + ] + class TestAllowEmptyStrDecimalFieldWithValidators(FieldValues): """ From 001d6ec2c907e638496fb4ef0cffa915bf0718fd Mon Sep 17 00:00:00 2001 From: Mathieu Dupuy Date: Sun, 14 May 2023 02:00:13 +0200 Subject: [PATCH 20/37] remove django 2.2 from docs index (#8982) --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index cab5511ac..ad241c0a3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -86,7 +86,7 @@ continued development by **[signing up for a paid plan][funding]**. REST framework requires the following: * Python (3.6, 3.7, 3.8, 3.9, 3.10, 3.11) -* Django (2.2, 3.0, 3.1, 3.2, 4.0, 4.1) +* Django (3.0, 3.1, 3.2, 4.0, 4.1) We **highly recommend** and only officially support the latest patch release of each Python and Django series. From 7bebe9772488fcf67949e1221e70af642ed0cb74 Mon Sep 17 00:00:00 2001 From: Mehraz Hossain Rumman <59512321+MehrazRumman@users.noreply.github.com> Date: Mon, 15 May 2023 21:02:17 +0600 Subject: [PATCH 21/37] Declared Django 4.2 support in README.md (#8985) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e8375291d..c2975a418 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ There is a live example API for testing purposes, [available here][sandbox]. # Requirements * Python 3.6+ -* Django 4.1, 4.0, 3.2, 3.1, 3.0 +* Django 4.2, 4.1, 4.0, 3.2, 3.1, 3.0 We **highly recommend** and only officially support the latest patch release of each Python and Django series. From 332e9560ab0b3a1b8c0ab8f68e95c09bc2d8999f Mon Sep 17 00:00:00 2001 From: Dominik Bruhn Date: Wed, 17 May 2023 07:46:48 +0200 Subject: [PATCH 22/37] Fix Links in Documentation to Django `reverse` and `reverse_lazy` (#8986) * Fix Django Docs url in reverse.md Django URLs of the documentation of `reverse` and `reverse_lazy` were wrong. * Update reverse.md --- docs/api-guide/reverse.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/reverse.md b/docs/api-guide/reverse.md index 82dd16881..c3fa52f21 100644 --- a/docs/api-guide/reverse.md +++ b/docs/api-guide/reverse.md @@ -54,5 +54,5 @@ As with the `reverse` function, you should **include the request as a keyword ar api_root = reverse_lazy('api-root', request=request) [cite]: https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm#sec_5_1_5 -[reverse]: https://docs.djangoproject.com/en/stable/topics/http/urls/#reverse -[reverse-lazy]: https://docs.djangoproject.com/en/stable/topics/http/urls/#reverse-lazy +[reverse]: https://docs.djangoproject.com/en/stable/ref/urlresolvers/#reverse +[reverse-lazy]: https://docs.djangoproject.com/en/stable/ref/urlresolvers/#reverse-lazy From a25aac7d6739c43a4997829a0ff79cce12e8c0de Mon Sep 17 00:00:00 2001 From: jornvanwier Date: Thu, 18 May 2023 05:46:40 +0200 Subject: [PATCH 23/37] fix URLPathVersioning reverse fallback (#7247) * fix URLPathVersioning reverse fallback * add test for URLPathVersioning reverse fallback * Update tests/test_versioning.py --------- Co-authored-by: Jorn van Wier Co-authored-by: Asif Saif Uddin --- rest_framework/versioning.py | 6 ++++-- tests/test_versioning.py | 10 ++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/rest_framework/versioning.py b/rest_framework/versioning.py index 78cfc9dc8..c2764c7a4 100644 --- a/rest_framework/versioning.py +++ b/rest_framework/versioning.py @@ -81,8 +81,10 @@ class URLPathVersioning(BaseVersioning): def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra): if request.version is not None: - kwargs = {} if (kwargs is None) else kwargs - kwargs[self.version_param] = request.version + kwargs = { + self.version_param: request.version, + **(kwargs or {}) + } return super().reverse( viewname, args, kwargs, request, format, **extra diff --git a/tests/test_versioning.py b/tests/test_versioning.py index 93f61d2be..b21646184 100644 --- a/tests/test_versioning.py +++ b/tests/test_versioning.py @@ -152,6 +152,8 @@ class TestURLReversing(URLPatternsTestCase, APITestCase): path('v1/', include((included, 'v1'), namespace='v1')), path('another/', dummy_view, name='another'), re_path(r'^(?P[v1|v2]+)/another/$', dummy_view, name='another'), + re_path(r'^(?P.+)/unversioned/$', dummy_view, name='unversioned'), + ] def test_reverse_unversioned(self): @@ -198,6 +200,14 @@ class TestURLReversing(URLPatternsTestCase, APITestCase): response = view(request) assert response.data == {'url': 'http://testserver/another/'} + # Test fallback when kwargs is not None + request = factory.get('/v1/endpoint/') + request.versioning_scheme = scheme() + request.version = 'v1' + + reversed_url = reverse('unversioned', request=request, kwargs={'foo': 'bar'}) + assert reversed_url == 'http://testserver/bar/unversioned/' + def test_reverse_namespace_versioning(self): class FakeResolverMatch(ResolverMatch): namespace = 'v1' From d252d22343b9c9688db77b59aa72dabd540bd252 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Beaul=C3=A9?= Date: Wed, 24 May 2023 10:59:42 -0300 Subject: [PATCH 24/37] Make set_value a method within `Serializer` (#8001) * Make set_value a static method for Serializers As an alternative to #7671, let the method be overridden if needed. As the function is only used for serializers, it has a better place in the Serializer class. * Set `set_value` as an object (non-static) method * Add tests for set_value() These tests follow the examples given in the method. --- rest_framework/fields.py | 21 --------------------- rest_framework/serializers.py | 24 ++++++++++++++++++++++-- tests/test_serializer.py | 21 +++++++++++++++++++++ 3 files changed, 43 insertions(+), 23 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 4f82e4a10..623e72e0a 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -113,27 +113,6 @@ def get_attribute(instance, attrs): return instance -def set_value(dictionary, keys, value): - """ - Similar to Python's built in `dictionary[key] = value`, - but takes a list of nested keys instead of a single key. - - set_value({'a': 1}, [], {'b': 2}) -> {'a': 1, 'b': 2} - set_value({'a': 1}, ['x'], 2) -> {'a': 1, 'x': 2} - set_value({'a': 1}, ['x', 'y'], 2) -> {'a': 1, 'x': {'y': 2}} - """ - if not keys: - dictionary.update(value) - return - - for key in keys[:-1]: - if key not in dictionary: - dictionary[key] = {} - dictionary = dictionary[key] - - dictionary[keys[-1]] = value - - def to_choices_dict(choices): """ Convert choices into key/value dicts. diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 01bebf5fc..a3d68b03d 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -28,7 +28,7 @@ from django.utils.translation import gettext_lazy as _ from rest_framework.compat import postgres_fields from rest_framework.exceptions import ErrorDetail, ValidationError -from rest_framework.fields import get_error_detail, set_value +from rest_framework.fields import get_error_detail from rest_framework.settings import api_settings from rest_framework.utils import html, model_meta, representation from rest_framework.utils.field_mapping import ( @@ -346,6 +346,26 @@ class Serializer(BaseSerializer, metaclass=SerializerMetaclass): 'invalid': _('Invalid data. Expected a dictionary, but got {datatype}.') } + def set_value(self, dictionary, keys, value): + """ + Similar to Python's built in `dictionary[key] = value`, + but takes a list of nested keys instead of a single key. + + set_value({'a': 1}, [], {'b': 2}) -> {'a': 1, 'b': 2} + set_value({'a': 1}, ['x'], 2) -> {'a': 1, 'x': 2} + set_value({'a': 1}, ['x', 'y'], 2) -> {'a': 1, 'x': {'y': 2}} + """ + if not keys: + dictionary.update(value) + return + + for key in keys[:-1]: + if key not in dictionary: + dictionary[key] = {} + dictionary = dictionary[key] + + dictionary[keys[-1]] = value + @cached_property def fields(self): """ @@ -492,7 +512,7 @@ class Serializer(BaseSerializer, metaclass=SerializerMetaclass): except SkipField: pass else: - set_value(ret, field.source_attrs, validated_value) + self.set_value(ret, field.source_attrs, validated_value) if errors: raise ValidationError(errors) diff --git a/tests/test_serializer.py b/tests/test_serializer.py index 1d9efaa43..10fa8afb9 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -762,3 +762,24 @@ class Test8301Regression: assert (s.data | {}).__class__ == s.data.__class__ assert ({} | s.data).__class__ == s.data.__class__ + + +class TestSetValueMethod: + # Serializer.set_value() modifies the first parameter in-place. + + s = serializers.Serializer() + + def test_no_keys(self): + ret = {'a': 1} + self.s.set_value(ret, [], {'b': 2}) + assert ret == {'a': 1, 'b': 2} + + def test_one_key(self): + ret = {'a': 1} + self.s.set_value(ret, ['x'], 2) + assert ret == {'a': 1, 'x': 2} + + def test_nested_key(self): + ret = {'a': 1} + self.s.set_value(ret, ['x', 'y'], 2) + assert ret == {'a': 1, 'x': {'y': 2}} From e2a4559c03247f69e85b2e0e7ede4e8b6201adc2 Mon Sep 17 00:00:00 2001 From: Saad Aleem Date: Mon, 29 May 2023 19:03:11 +0500 Subject: [PATCH 25/37] Fix validation for ListSerializer (#8979) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: Make the instance variable of child serializer point to the correct list object instead of the entire list when validating ListSerializer * fix formatting issues for list serializer validation fix * fix imports sorting for list serializer tests * remove django 2.2 from docs index (#8982) * Declared Django 4.2 support in README.md (#8985) * Fix Links in Documentation to Django `reverse` and `reverse_lazy` (#8986) * Fix Django Docs url in reverse.md Django URLs of the documentation of `reverse` and `reverse_lazy` were wrong. * Update reverse.md * fix URLPathVersioning reverse fallback (#7247) * fix URLPathVersioning reverse fallback * add test for URLPathVersioning reverse fallback * Update tests/test_versioning.py --------- Co-authored-by: Jorn van Wier Co-authored-by: Asif Saif Uddin * Make set_value a method within `Serializer` (#8001) * Make set_value a static method for Serializers As an alternative to #7671, let the method be overridden if needed. As the function is only used for serializers, it has a better place in the Serializer class. * Set `set_value` as an object (non-static) method * Add tests for set_value() These tests follow the examples given in the method. * fix: Make the instance variable of child serializer point to the correct list object instead of the entire list when validating ListSerializer * Make set_value a method within `Serializer` (#8001) * Make set_value a static method for Serializers As an alternative to #7671, let the method be overridden if needed. As the function is only used for serializers, it has a better place in the Serializer class. * Set `set_value` as an object (non-static) method * Add tests for set_value() These tests follow the examples given in the method. * fix: Make the instance variable of child serializer point to the correct list object instead of the entire list when validating ListSerializer * fix: Make the instance variable of child serializer point to the correct list object instead of the entire list when validating ListSerializer * fix formatting issues for list serializer validation fix * fix: Make the instance variable of child serializer point to the correct list object instead of the entire list when validating ListSerializer * fix formatting issues for list serializer validation fix * fix linting * Update rest_framework/serializers.py Co-authored-by: Sergei Shishov * Update rest_framework/serializers.py Co-authored-by: Sergei Shishov * fix: instance variable in list serializer, remove commented code --------- Co-authored-by: Mathieu Dupuy Co-authored-by: Mehraz Hossain Rumman <59512321+MehrazRumman@users.noreply.github.com> Co-authored-by: Dominik Bruhn Co-authored-by: jornvanwier Co-authored-by: Jorn van Wier Co-authored-by: Asif Saif Uddin Co-authored-by: Étienne Beaulé Co-authored-by: Sergei Shishov --- rest_framework/serializers.py | 14 +++++++- tests/test_serializer.py | 61 +++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index a3d68b03d..56fa918dc 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -609,6 +609,12 @@ class ListSerializer(BaseSerializer): self.min_length = kwargs.pop('min_length', None) assert self.child is not None, '`child` is a required argument.' assert not inspect.isclass(self.child), '`child` has not been instantiated.' + + instance = kwargs.get('instance', []) + data = kwargs.get('data', []) + if instance and data: + assert len(data) == len(instance), 'Data and instance should have same length' + super().__init__(*args, **kwargs) self.child.bind(field_name='', parent=self) @@ -683,7 +689,13 @@ class ListSerializer(BaseSerializer): ret = [] errors = [] - for item in data: + for idx, item in enumerate(data): + if ( + hasattr(self, 'instance') + and self.instance + and len(self.instance) > idx + ): + self.child.instance = self.instance[idx] try: validated = self.child.run_validation(item) except ValidationError as exc: diff --git a/tests/test_serializer.py b/tests/test_serializer.py index 10fa8afb9..39d9238ef 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -2,6 +2,7 @@ import inspect import pickle import re import sys +import unittest from collections import ChainMap from collections.abc import Mapping @@ -783,3 +784,63 @@ class TestSetValueMethod: ret = {'a': 1} self.s.set_value(ret, ['x', 'y'], 2) assert ret == {'a': 1, 'x': {'y': 2}} + + +class MyClass(models.Model): + name = models.CharField(max_length=100) + value = models.CharField(max_length=100, blank=True) + + app_label = "test" + + @property + def is_valid(self): + return self.name == 'valid' + + +class MyClassSerializer(serializers.ModelSerializer): + class Meta: + model = MyClass + fields = ('id', 'name', 'value') + + def validate_value(self, value): + if value and not self.instance.is_valid: + raise serializers.ValidationError( + 'Status cannot be set for invalid instance') + return value + + +class TestMultipleObjectsValidation(unittest.TestCase): + def setUp(self): + self.objs = [ + MyClass(name='valid'), + MyClass(name='invalid'), + MyClass(name='other'), + ] + + def test_multiple_objects_are_validated_separately(self): + + serializer = MyClassSerializer( + data=[{'value': 'set', 'id': instance.id} for instance in + self.objs], + instance=self.objs, + many=True, + partial=True, + ) + + assert not serializer.is_valid() + assert serializer.errors == [ + {}, + {'value': ['Status cannot be set for invalid instance']}, + {'value': ['Status cannot be set for invalid instance']} + ] + + def test_exception_raised_when_data_and_instance_length_different(self): + + with self.assertRaises(AssertionError): + MyClassSerializer( + data=[{'value': 'set', 'id': instance.id} for instance in + self.objs], + instance=self.objs[:-1], + many=True, + partial=True, + ) From ff5f647df01d83f1bad32e37ed8d9181da21538c Mon Sep 17 00:00:00 2001 From: Vladimir Kasatkin Date: Wed, 31 May 2023 07:36:21 +0300 Subject: [PATCH 26/37] Fix example of `requires_context` attribute (#8952) --- docs/api-guide/validators.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/api-guide/validators.md b/docs/api-guide/validators.md index dac937d9b..2a1e3e6b3 100644 --- a/docs/api-guide/validators.md +++ b/docs/api-guide/validators.md @@ -295,13 +295,14 @@ To write a class-based validator, use the `__call__` method. Class-based validat In some advanced cases you might want a validator to be passed the serializer field it is being used with as additional context. You can do so by setting -a `requires_context = True` attribute on the validator. The `__call__` method +a `requires_context = True` attribute on the validator class. The `__call__` method will then be called with the `serializer_field` or `serializer` as an additional argument. - requires_context = True + class MultipleOf: + requires_context = True - def __call__(self, value, serializer_field): - ... + def __call__(self, value, serializer_field): + ... [cite]: https://docs.djangoproject.com/en/stable/ref/validators/ From 03e2ecc9a5afcb33c9069efcb39b5a04a49d7774 Mon Sep 17 00:00:00 2001 From: Alasdair Nicol Date: Fri, 2 Jun 2023 01:29:11 +0100 Subject: [PATCH 27/37] Add NullBooleanField deprecation to docs (#8999) --- docs/community/3.12-announcement.md | 10 ++++++++++ docs/community/3.14-announcement.md | 10 ++++++++++ docs/community/release-notes.md | 1 + 3 files changed, 21 insertions(+) diff --git a/docs/community/3.12-announcement.md b/docs/community/3.12-announcement.md index 4a589e39c..3bfeb6576 100644 --- a/docs/community/3.12-announcement.md +++ b/docs/community/3.12-announcement.md @@ -143,6 +143,16 @@ class PublisherSearchView(generics.ListAPIView): --- +## Deprecations + +### `serializers.NullBooleanField` + +`serializers.NullBooleanField` is now pending deprecation, and will be removed in 3.14. + +Instead use `serializers.BooleanField` field and set `allow_null=True` which does the same thing. + +--- + ## Funding REST framework is a *collaboratively funded project*. If you use diff --git a/docs/community/3.14-announcement.md b/docs/community/3.14-announcement.md index 0543d0d6d..e7b10ede1 100644 --- a/docs/community/3.14-announcement.md +++ b/docs/community/3.14-announcement.md @@ -60,3 +60,13 @@ See Pull Request [#7522](https://github.com/encode/django-rest-framework/pull/75 ## Minor fixes and improvements There are a number of minor fixes and improvements in this release. See the [release notes](release-notes.md) page for a complete listing. + +--- + +## Deprecations + +### `serializers.NullBooleanField` + +`serializers.NullBooleanField` was moved to pending deprecation in 3.12, and deprecated in 3.13. It has now been removed from the core framework. + +Instead use `serializers.BooleanField` field and set `allow_null=True` which does the same thing. diff --git a/docs/community/release-notes.md b/docs/community/release-notes.md index 887cae3b4..fba7f63d6 100644 --- a/docs/community/release-notes.md +++ b/docs/community/release-notes.md @@ -157,6 +157,7 @@ Date: 28th September 2020 * Fix `PrimaryKeyRelatedField` and `HyperlinkedRelatedField` when source field is actually a property. [#7142] * `Token.generate_key` is now a class method. [#7502] * `@action` warns if method is wrapped in a decorator that does not preserve information using `@functools.wraps`. [#7098] +* Deprecate `serializers.NullBooleanField` in favour of `serializers.BooleanField` with `allow_null=True` [#7122] --- From 376a5cbbba3f8df9c9db8c03a7c8fa2a6e6c05f4 Mon Sep 17 00:00:00 2001 From: Mathieu Dupuy Date: Sun, 4 Jun 2023 07:24:07 +0200 Subject: [PATCH 28/37] remove dependency on pytz (#8984) * remove pytz * Revert "remove pytz" This reverts commit 393609dfaadbc4d82f760e6e4df8a6de6de24f9b. * remove pytz, more subtly --- rest_framework/fields.py | 12 +++++++++--- setup.py | 2 +- tests/test_fields.py | 22 ++++++++++++++-------- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 623e72e0a..7079e3332 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -29,7 +29,11 @@ from django.utils.encoding import is_protected_type, smart_str from django.utils.formats import localize_input, sanitize_separators from django.utils.ipv6 import clean_ipv6_address from django.utils.translation import gettext_lazy as _ -from pytz.exceptions import InvalidTimeError + +try: + import pytz +except ImportError: + pytz = None from rest_framework import ISO_8601 from rest_framework.exceptions import ErrorDetail, ValidationError @@ -1148,8 +1152,10 @@ class DateTimeField(Field): if not valid_datetime(dt): self.fail('make_aware', timezone=field_timezone) return dt - except InvalidTimeError: - self.fail('make_aware', timezone=field_timezone) + except Exception as e: + if pytz and isinstance(e, pytz.exceptions.InvalidTimeError): + self.fail('make_aware', timezone=field_timezone) + raise e elif (field_timezone is None) and timezone.is_aware(value): return timezone.make_naive(value, datetime.timezone.utc) return value diff --git a/setup.py b/setup.py index 533ffa97f..6afd5e05e 100755 --- a/setup.py +++ b/setup.py @@ -83,7 +83,7 @@ setup( author_email='tom@tomchristie.com', # SEE NOTE BELOW (*) packages=find_packages(exclude=['tests*']), include_package_data=True, - install_requires=["django>=3.0", "pytz", 'backports.zoneinfo;python_version<"3.9"'], + install_requires=["django>=3.0", 'backports.zoneinfo;python_version<"3.9"'], python_requires=">=3.6", zip_safe=False, classifiers=[ diff --git a/tests/test_fields.py b/tests/test_fields.py index bcf388441..50d9d7793 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -9,7 +9,12 @@ from enum import auto from unittest.mock import patch import pytest -import pytz + +try: + import pytz +except ImportError: + pytz = None + from django.core.exceptions import ValidationError as DjangoValidationError from django.db.models import IntegerChoices, TextChoices from django.http import QueryDict @@ -1604,15 +1609,16 @@ class TestPytzNaiveDayLightSavingTimeTimeZoneDateTimeField(FieldValues): } outputs = {} - class MockTimezone(pytz.BaseTzInfo): - @staticmethod - def localize(value, is_dst): - raise pytz.InvalidTimeError() + if pytz: + class MockTimezone(pytz.BaseTzInfo): + @staticmethod + def localize(value, is_dst): + raise pytz.InvalidTimeError() - def __str__(self): - return 'America/New_York' + def __str__(self): + return 'America/New_York' - field = serializers.DateTimeField(default_timezone=MockTimezone()) + field = serializers.DateTimeField(default_timezone=MockTimezone()) @patch('rest_framework.utils.timezone.datetime_ambiguous', return_value=True) From 02d9bfc2ddee0ddf1d6e9cb0e23a52b80fdb4242 Mon Sep 17 00:00:00 2001 From: Niyaz Date: Mon, 12 Jun 2023 16:28:28 +0300 Subject: [PATCH 29/37] Fixes `BrowsableAPIRenderer` for usage with `ListSerializer`. (#7530) Renders list of items in raw_data_form and does not renders form in template while using with `ListSerializer` (`many=True`). --- rest_framework/renderers.py | 6 ++++ tests/test_renderers.py | 59 +++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 8e8c3a9b3..0a3b03729 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -506,6 +506,9 @@ class BrowsableAPIRenderer(BaseRenderer): return self.render_form_for_serializer(serializer) def render_form_for_serializer(self, serializer): + if isinstance(serializer, serializers.ListSerializer): + return None + if hasattr(serializer, 'initial_data'): serializer.is_valid() @@ -555,10 +558,13 @@ class BrowsableAPIRenderer(BaseRenderer): context['indent'] = 4 # strip HiddenField from output + is_list_serializer = isinstance(serializer, serializers.ListSerializer) + serializer = serializer.child if is_list_serializer else serializer data = serializer.data.copy() for name, field in serializer.fields.items(): if isinstance(field, serializers.HiddenField): data.pop(name, None) + data = [data] if is_list_serializer else data content = renderer.render(data, accepted, context) # Renders returns bytes, but CharField expects a str. content = content.decode() diff --git a/tests/test_renderers.py b/tests/test_renderers.py index 71d734c86..247737576 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -633,6 +633,9 @@ class BrowsableAPIRendererTests(URLPatternsTestCase): class AuthExampleViewSet(ExampleViewSet): permission_classes = [permissions.IsAuthenticated] + class SimpleSerializer(serializers.Serializer): + name = serializers.CharField() + router = SimpleRouter() router.register('examples', ExampleViewSet, basename='example') router.register('auth-examples', AuthExampleViewSet, basename='auth-example') @@ -640,6 +643,62 @@ class BrowsableAPIRendererTests(URLPatternsTestCase): def setUp(self): self.renderer = BrowsableAPIRenderer() + self.renderer.accepted_media_type = '' + self.renderer.renderer_context = {} + + def test_render_form_for_serializer(self): + with self.subTest('Serializer'): + serializer = BrowsableAPIRendererTests.SimpleSerializer(data={'name': 'Name'}) + form = self.renderer.render_form_for_serializer(serializer) + assert isinstance(form, str), 'Must return form for serializer' + + with self.subTest('ListSerializer'): + list_serializer = BrowsableAPIRendererTests.SimpleSerializer(data=[{'name': 'Name'}], many=True) + form = self.renderer.render_form_for_serializer(list_serializer) + assert form is None, 'Must not return form for list serializer' + + def test_get_raw_data_form(self): + with self.subTest('Serializer'): + class DummyGenericViewsetLike(APIView): + def get_serializer(self, **kwargs): + return BrowsableAPIRendererTests.SimpleSerializer(**kwargs) + + def get(self, request): + response = Response() + response.view = self + return response + + post = get + + view = DummyGenericViewsetLike.as_view() + _request = APIRequestFactory().get('/') + request = Request(_request) + response = view(_request) + view = response.view + + raw_data_form = self.renderer.get_raw_data_form({'name': 'Name'}, view, 'POST', request) + assert raw_data_form['_content'].initial == '{\n "name": ""\n}' + + with self.subTest('ListSerializer'): + class DummyGenericViewsetLike(APIView): + def get_serializer(self, **kwargs): + return BrowsableAPIRendererTests.SimpleSerializer(many=True, **kwargs) # returns ListSerializer + + def get(self, request): + response = Response() + response.view = self + return response + + post = get + + view = DummyGenericViewsetLike.as_view() + _request = APIRequestFactory().get('/') + request = Request(_request) + response = view(_request) + view = response.view + + raw_data_form = self.renderer.get_raw_data_form([{'name': 'Name'}], view, 'POST', request) + assert raw_data_form['_content'].initial == '[\n {\n "name": ""\n }\n]' def test_get_description_returns_empty_string_for_401_and_403_statuses(self): assert self.renderer.get_description({}, status_code=401) == '' From a180bde0fd965915718b070932418cabc831cee1 Mon Sep 17 00:00:00 2001 From: Nancy Eckenthal Date: Mon, 12 Jun 2023 11:21:18 -0400 Subject: [PATCH 30/37] Permit mixed casing of string values for BooleanField validation (#8970) * be more permissive of mixed casing in validating strings for BooleanField values * undo unnecessary change * lint --- rest_framework/fields.py | 51 ++++++++++++++++++++++++---------------- tests/test_fields.py | 32 +++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 20 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 7079e3332..4ce9c79c3 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -664,22 +664,27 @@ class BooleanField(Field): default_empty_html = False initial = False TRUE_VALUES = { - 't', 'T', - 'y', 'Y', 'yes', 'Yes', 'YES', - 'true', 'True', 'TRUE', - 'on', 'On', 'ON', - '1', 1, - True + 't', + 'y', + 'yes', + 'true', + 'on', + '1', + 1, + True, } FALSE_VALUES = { - 'f', 'F', - 'n', 'N', 'no', 'No', 'NO', - 'false', 'False', 'FALSE', - 'off', 'Off', 'OFF', - '0', 0, 0.0, - False + 'f', + 'n', + 'no', + 'false', + 'off', + '0', + 0, + 0.0, + False, } - NULL_VALUES = {'null', 'Null', 'NULL', '', None} + NULL_VALUES = {'null', '', None} def __init__(self, **kwargs): if kwargs.get('allow_null', False): @@ -687,22 +692,28 @@ class BooleanField(Field): self.initial = None super().__init__(**kwargs) + @staticmethod + def _lower_if_str(value): + if isinstance(value, str): + return value.lower() + return value + def to_internal_value(self, data): with contextlib.suppress(TypeError): - if data in self.TRUE_VALUES: + if self._lower_if_str(data) in self.TRUE_VALUES: return True - elif data in self.FALSE_VALUES: + elif self._lower_if_str(data) in self.FALSE_VALUES: return False - elif data in self.NULL_VALUES and self.allow_null: + elif self._lower_if_str(data) in self.NULL_VALUES and self.allow_null: return None - self.fail('invalid', input=data) + self.fail("invalid", input=data) def to_representation(self, value): - if value in self.TRUE_VALUES: + if self._lower_if_str(value) in self.TRUE_VALUES: return True - elif value in self.FALSE_VALUES: + elif self._lower_if_str(value) in self.FALSE_VALUES: return False - if value in self.NULL_VALUES and self.allow_null: + if self._lower_if_str(value) in self.NULL_VALUES and self.allow_null: return None return bool(value) diff --git a/tests/test_fields.py b/tests/test_fields.py index 50d9d7793..03584431e 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -695,8 +695,24 @@ class TestBooleanField(FieldValues): Valid and invalid values for `BooleanField`. """ valid_inputs = { + 'True': True, + 'TRUE': True, + 'tRuE': True, + 't': True, + 'T': True, 'true': True, + 'on': True, + 'ON': True, + 'oN': True, + 'False': False, + 'FALSE': False, + 'fALse': False, + 'f': False, + 'F': False, 'false': False, + 'off': False, + 'OFF': False, + 'oFf': False, '1': True, '0': False, 1: True, @@ -709,8 +725,24 @@ class TestBooleanField(FieldValues): None: ['This field may not be null.'] } outputs = { + 'True': True, + 'TRUE': True, + 'tRuE': True, + 't': True, + 'T': True, 'true': True, + 'on': True, + 'ON': True, + 'oN': True, + 'False': False, + 'FALSE': False, + 'fALse': False, + 'f': False, + 'F': False, 'false': False, + 'off': False, + 'OFF': False, + 'oFf': False, '1': True, '0': False, 1: True, From 833313496c8ebbdc3509d87895764c822bfc5dc1 Mon Sep 17 00:00:00 2001 From: Lenno Nagel Date: Tue, 13 Jun 2023 07:27:37 +0300 Subject: [PATCH 31/37] Removed usage of field.choices that triggered full table load (#8950) Removed the `{{ field.choices|yesno:",disabled" }}` block because this triggers the loading of full database table worth of objects just to determine whether the multi-select widget should be set as disabled or not. Since this "disabled" marking feature is not present in the normal select field, then I propose to remove it also from the multi-select. --- .../templates/rest_framework/horizontal/select_multiple.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/templates/rest_framework/horizontal/select_multiple.html b/rest_framework/templates/rest_framework/horizontal/select_multiple.html index 36ff9fd0d..12e781cc6 100644 --- a/rest_framework/templates/rest_framework/horizontal/select_multiple.html +++ b/rest_framework/templates/rest_framework/horizontal/select_multiple.html @@ -11,7 +11,7 @@ {% endif %}
    - {% for select in field.iter_options %} {% if select.start_option_group %} From a16dbfd11018fa01ceaf6dee7df34ab0430282cf Mon Sep 17 00:00:00 2001 From: David Smith <39445562+smithdc1@users.noreply.github.com> Date: Tue, 13 Jun 2023 07:55:22 +0100 Subject: [PATCH 32/37] Added Deprecation Warnings for CoreAPI (#7519) * Added Deprecation Warnings for CoreAPI * Bumped removal to DRF315 * Update rest_framework/__init__.py * Update rest_framework/filters.py * Update rest_framework/filters.py * Update tests/schemas/test_coreapi.py * Update rest_framework/filters.py * Update rest_framework/filters.py * Update tests/schemas/test_coreapi.py * Update tests/schemas/test_coreapi.py * Update tests/schemas/test_coreapi.py * Update tests/schemas/test_coreapi.py * Update rest_framework/pagination.py * Update rest_framework/pagination.py * Update rest_framework/pagination.py * Update rest_framework/pagination.py * Update rest_framework/schemas/coreapi.py * Update rest_framework/schemas/coreapi.py * Update rest_framework/schemas/coreapi.py * Update rest_framework/schemas/coreapi.py * Update rest_framework/schemas/coreapi.py * Update tests/schemas/test_coreapi.py * Update setup.cfg * Update tests/schemas/test_coreapi.py * Update tests/schemas/test_coreapi.py * Update tests/schemas/test_coreapi.py * Update tests/schemas/test_coreapi.py * Update tests/schemas/test_coreapi.py * Update tests/schemas/test_coreapi.py * Update rest_framework/pagination.py --------- Co-authored-by: Asif Saif Uddin --- rest_framework/__init__.py | 4 +++ rest_framework/filters.py | 8 +++++ rest_framework/pagination.py | 11 +++++++ rest_framework/schemas/coreapi.py | 12 ++++++- setup.cfg | 2 ++ tests/schemas/test_coreapi.py | 55 +++++++++++++++++++++++++++++-- 6 files changed, 89 insertions(+), 3 deletions(-) diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index cc24ce46c..da7b88dfa 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -31,3 +31,7 @@ if django.VERSION < (3, 2): class RemovedInDRF315Warning(DeprecationWarning): pass + + +class RemovedInDRF317Warning(PendingDeprecationWarning): + pass diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 1ffd9edc0..17e6975eb 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -3,6 +3,7 @@ Provides generic filtering backends that can be used to filter the results returned by list views. """ import operator +import warnings from functools import reduce from django.core.exceptions import ImproperlyConfigured @@ -12,6 +13,7 @@ from django.template import loader from django.utils.encoding import force_str from django.utils.translation import gettext_lazy as _ +from rest_framework import RemovedInDRF317Warning from rest_framework.compat import coreapi, coreschema, distinct from rest_framework.settings import api_settings @@ -29,6 +31,8 @@ class BaseFilterBackend: def get_schema_fields(self, view): assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' + if coreapi is not None: + warnings.warn('CoreAPI compatibility is deprecated and will be removed in DRF 3.17', RemovedInDRF317Warning) assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`' return [] @@ -146,6 +150,8 @@ class SearchFilter(BaseFilterBackend): def get_schema_fields(self, view): assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' + if coreapi is not None: + warnings.warn('CoreAPI compatibility is deprecated and will be removed in DRF 3.17', RemovedInDRF317Warning) assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`' return [ coreapi.Field( @@ -306,6 +312,8 @@ class OrderingFilter(BaseFilterBackend): def get_schema_fields(self, view): assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' + if coreapi is not None: + warnings.warn('CoreAPI compatibility is deprecated and will be removed in DRF 3.17', RemovedInDRF317Warning) assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`' return [ coreapi.Field( diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index af508bef6..ce8778547 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -4,6 +4,8 @@ be used for paginated responses. """ import contextlib +import warnings + from base64 import b64decode, b64encode from collections import namedtuple from urllib import parse @@ -15,6 +17,7 @@ from django.template import loader from django.utils.encoding import force_str from django.utils.translation import gettext_lazy as _ +from rest_framework import RemovedInDRF317Warning from rest_framework.compat import coreapi, coreschema from rest_framework.exceptions import NotFound from rest_framework.response import Response @@ -152,6 +155,8 @@ class BasePagination: def get_schema_fields(self, view): assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' + if coreapi is not None: + warnings.warn('CoreAPI compatibility is deprecated and will be removed in DRF 3.17', RemovedInDRF317Warning) return [] def get_schema_operation_parameters(self, view): @@ -311,6 +316,8 @@ class PageNumberPagination(BasePagination): def get_schema_fields(self, view): assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' + if coreapi is not None: + warnings.warn('CoreAPI compatibility is deprecated and will be removed in DRF 3.17', RemovedInDRF317Warning) assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`' fields = [ coreapi.Field( @@ -525,6 +532,8 @@ class LimitOffsetPagination(BasePagination): def get_schema_fields(self, view): assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' + if coreapi is not None: + warnings.warn('CoreAPI compatibility is deprecated and will be removed in DRF 3.17', RemovedInDRF317Warning) assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`' return [ coreapi.Field( @@ -930,6 +939,8 @@ class CursorPagination(BasePagination): def get_schema_fields(self, view): assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' + if coreapi is not None: + warnings.warn('CoreAPI compatibility is deprecated and will be removed in DRF 3.17', RemovedInDRF317Warning) assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`' fields = [ coreapi.Field( diff --git a/rest_framework/schemas/coreapi.py b/rest_framework/schemas/coreapi.py index 0713e0cb8..582aba196 100644 --- a/rest_framework/schemas/coreapi.py +++ b/rest_framework/schemas/coreapi.py @@ -5,7 +5,7 @@ from urllib import parse from django.db import models from django.utils.encoding import force_str -from rest_framework import exceptions, serializers +from rest_framework import RemovedInDRF317Warning, exceptions, serializers from rest_framework.compat import coreapi, coreschema, uritemplate from rest_framework.settings import api_settings @@ -118,6 +118,8 @@ class SchemaGenerator(BaseSchemaGenerator): def __init__(self, title=None, url=None, description=None, patterns=None, urlconf=None, version=None): assert coreapi, '`coreapi` must be installed for schema support.' + if coreapi is not None: + warnings.warn('CoreAPI compatibility is deprecated and will be removed in DRF 3.17', RemovedInDRF317Warning) assert coreschema, '`coreschema` must be installed for schema support.' super().__init__(title, url, description, patterns, urlconf) @@ -351,6 +353,9 @@ class AutoSchema(ViewInspector): will be added to auto-generated fields, overwriting on `Field.name` """ super().__init__() + if coreapi is not None: + warnings.warn('CoreAPI compatibility is deprecated and will be removed in DRF 3.17', RemovedInDRF317Warning) + if manual_fields is None: manual_fields = [] self._manual_fields = manual_fields @@ -592,6 +597,9 @@ class ManualSchema(ViewInspector): * `description`: String description for view. Optional. """ super().__init__() + if coreapi is not None: + warnings.warn('CoreAPI compatibility is deprecated and will be removed in DRF 3.17', RemovedInDRF317Warning) + assert all(isinstance(f, coreapi.Field) for f in fields), "`fields` must be a list of coreapi.Field instances" self._fields = fields self._description = description @@ -613,4 +621,6 @@ class ManualSchema(ViewInspector): def is_enabled(): """Is CoreAPI Mode enabled?""" + if coreapi is not None: + warnings.warn('CoreAPI compatibility is deprecated and will be removed in DRF 3.17', RemovedInDRF317Warning) return issubclass(api_settings.DEFAULT_SCHEMA_CLASS, AutoSchema) diff --git a/setup.cfg b/setup.cfg index 294e9afdd..487d99db9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,6 +3,8 @@ license_files = LICENSE.md [tool:pytest] addopts=--tb=short --strict-markers -ra +testspath = tests +filterwarnings = ignore:CoreAPI compatibility is deprecated*:rest_framework.RemovedInDRF317Warning [flake8] ignore = E501,W503,W504 diff --git a/tests/schemas/test_coreapi.py b/tests/schemas/test_coreapi.py index eddc5243e..98fd46f9f 100644 --- a/tests/schemas/test_coreapi.py +++ b/tests/schemas/test_coreapi.py @@ -7,16 +7,24 @@ from django.test import TestCase, override_settings from django.urls import include, path from rest_framework import ( - filters, generics, pagination, permissions, serializers + RemovedInDRF317Warning, filters, generics, pagination, permissions, + serializers ) from rest_framework.compat import coreapi, coreschema from rest_framework.decorators import action, api_view, schema +from rest_framework.filters import ( + BaseFilterBackend, OrderingFilter, SearchFilter +) +from rest_framework.pagination import ( + BasePagination, CursorPagination, LimitOffsetPagination, + PageNumberPagination +) from rest_framework.request import Request from rest_framework.routers import DefaultRouter, SimpleRouter from rest_framework.schemas import ( AutoSchema, ManualSchema, SchemaGenerator, get_schema_view ) -from rest_framework.schemas.coreapi import field_to_schema +from rest_framework.schemas.coreapi import field_to_schema, is_enabled from rest_framework.schemas.generators import EndpointEnumerator from rest_framework.schemas.utils import is_list_view from rest_framework.test import APIClient, APIRequestFactory @@ -1433,3 +1441,46 @@ def test_schema_handles_exception(): response.render() assert response.status_code == 403 assert b"You do not have permission to perform this action." in response.content + + +@pytest.mark.skipif(not coreapi, reason='coreapi is not installed') +def test_coreapi_deprecation(): + with pytest.warns(RemovedInDRF317Warning): + SchemaGenerator() + + with pytest.warns(RemovedInDRF317Warning): + AutoSchema() + + with pytest.warns(RemovedInDRF317Warning): + ManualSchema({}) + + with pytest.warns(RemovedInDRF317Warning): + deprecated_filter = OrderingFilter() + deprecated_filter.get_schema_fields({}) + + with pytest.warns(RemovedInDRF317Warning): + deprecated_filter = BaseFilterBackend() + deprecated_filter.get_schema_fields({}) + + with pytest.warns(RemovedInDRF317Warning): + deprecated_filter = SearchFilter() + deprecated_filter.get_schema_fields({}) + + with pytest.warns(RemovedInDRF317Warning): + paginator = BasePagination() + paginator.get_schema_fields({}) + + with pytest.warns(RemovedInDRF317Warning): + paginator = PageNumberPagination() + paginator.get_schema_fields({}) + + with pytest.warns(RemovedInDRF317Warning): + paginator = LimitOffsetPagination() + paginator.get_schema_fields({}) + + with pytest.warns(RemovedInDRF317Warning): + paginator = CursorPagination() + paginator.get_schema_fields({}) + + with pytest.warns(RemovedInDRF317Warning): + is_enabled() From aed7761a8d7e1691a4f4bbf9c83a447dac44d92a Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Tue, 13 Jun 2023 15:01:29 +0600 Subject: [PATCH 33/37] Update copy right timeline --- rest_framework/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index da7b88dfa..b9e3f9817 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -13,7 +13,7 @@ __title__ = 'Django REST framework' __version__ = '3.14.0' __author__ = 'Tom Christie' __license__ = 'BSD 3-Clause' -__copyright__ = 'Copyright 2011-2019 Encode OSS Ltd' +__copyright__ = 'Copyright 2011-2023 Encode OSS Ltd' # Version synonym VERSION = __version__ From 71f87a586400074f1840276c5cf36fc7da1c2c4c Mon Sep 17 00:00:00 2001 From: Konstantin Kuchkov Date: Wed, 14 Jun 2023 06:24:09 -0700 Subject: [PATCH 34/37] Fix NamespaceVersioning ignoring DEFAULT_VERSION on non-None namespaces (#7278) * Fix the case where if the namespace is not None and there's no match, NamespaceVersioning always raises NotFound even if DEFAULT_VERSION is set or None is in ALLOWED_VERSIONS * Add test cases --- rest_framework/versioning.py | 17 +++---- tests/test_versioning.py | 93 +++++++++++++++++++++++++++++++++++- 2 files changed, 101 insertions(+), 9 deletions(-) diff --git a/rest_framework/versioning.py b/rest_framework/versioning.py index c2764c7a4..a1c0ce4d7 100644 --- a/rest_framework/versioning.py +++ b/rest_framework/versioning.py @@ -119,15 +119,16 @@ class NamespaceVersioning(BaseVersioning): def determine_version(self, request, *args, **kwargs): resolver_match = getattr(request, 'resolver_match', None) - if resolver_match is None or not resolver_match.namespace: - return self.default_version + if resolver_match is not None and resolver_match.namespace: + # Allow for possibly nested namespaces. + possible_versions = resolver_match.namespace.split(':') + for version in possible_versions: + if self.is_allowed_version(version): + return version - # Allow for possibly nested namespaces. - possible_versions = resolver_match.namespace.split(':') - for version in possible_versions: - if self.is_allowed_version(version): - return version - raise exceptions.NotFound(self.invalid_version_message) + if not self.is_allowed_version(self.default_version): + raise exceptions.NotFound(self.invalid_version_message) + return self.default_version def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra): if request.version is not None: diff --git a/tests/test_versioning.py b/tests/test_versioning.py index b21646184..1ccecae0b 100644 --- a/tests/test_versioning.py +++ b/tests/test_versioning.py @@ -272,7 +272,7 @@ class TestInvalidVersion: assert response.status_code == status.HTTP_404_NOT_FOUND -class TestAllowedAndDefaultVersion: +class TestAcceptHeaderAllowedAndDefaultVersion: def test_missing_without_default(self): scheme = versioning.AcceptHeaderVersioning view = AllowedVersionsView.as_view(versioning_class=scheme) @@ -318,6 +318,97 @@ class TestAllowedAndDefaultVersion: assert response.data == {'version': 'v2'} +class TestNamespaceAllowedAndDefaultVersion: + def test_no_namespace_without_default(self): + class FakeResolverMatch: + namespace = None + + scheme = versioning.NamespaceVersioning + view = AllowedVersionsView.as_view(versioning_class=scheme) + + request = factory.get('/endpoint/') + request.resolver_match = FakeResolverMatch + response = view(request) + assert response.status_code == status.HTTP_404_NOT_FOUND + + def test_no_namespace_with_default(self): + class FakeResolverMatch: + namespace = None + + scheme = versioning.NamespaceVersioning + view = AllowedAndDefaultVersionsView.as_view(versioning_class=scheme) + + request = factory.get('/endpoint/') + request.resolver_match = FakeResolverMatch + response = view(request) + assert response.status_code == status.HTTP_200_OK + assert response.data == {'version': 'v2'} + + def test_no_match_without_default(self): + class FakeResolverMatch: + namespace = 'no_match' + + scheme = versioning.NamespaceVersioning + view = AllowedVersionsView.as_view(versioning_class=scheme) + + request = factory.get('/endpoint/') + request.resolver_match = FakeResolverMatch + response = view(request) + assert response.status_code == status.HTTP_404_NOT_FOUND + + def test_no_match_with_default(self): + class FakeResolverMatch: + namespace = 'no_match' + + scheme = versioning.NamespaceVersioning + view = AllowedAndDefaultVersionsView.as_view(versioning_class=scheme) + + request = factory.get('/endpoint/') + request.resolver_match = FakeResolverMatch + response = view(request) + assert response.status_code == status.HTTP_200_OK + assert response.data == {'version': 'v2'} + + def test_with_default(self): + class FakeResolverMatch: + namespace = 'v1' + + scheme = versioning.NamespaceVersioning + view = AllowedAndDefaultVersionsView.as_view(versioning_class=scheme) + + request = factory.get('/endpoint/') + request.resolver_match = FakeResolverMatch + response = view(request) + assert response.status_code == status.HTTP_200_OK + assert response.data == {'version': 'v1'} + + def test_no_match_without_default_but_none_allowed(self): + class FakeResolverMatch: + namespace = 'no_match' + + scheme = versioning.NamespaceVersioning + view = AllowedWithNoneVersionsView.as_view(versioning_class=scheme) + + request = factory.get('/endpoint/') + request.resolver_match = FakeResolverMatch + response = view(request) + assert response.status_code == status.HTTP_200_OK + assert response.data == {'version': None} + + def test_no_match_with_default_and_none_allowed(self): + class FakeResolverMatch: + namespace = 'no_match' + + scheme = versioning.NamespaceVersioning + view = AllowedWithNoneAndDefaultVersionsView.as_view(versioning_class=scheme) + + request = factory.get('/endpoint/') + request.resolver_match = FakeResolverMatch + response = view(request) + assert response.status_code == status.HTTP_200_OK + assert response.data == {'version': 'v2'} + + class TestHyperlinkedRelatedField(URLPatternsTestCase, APITestCase): included = [ path('namespaced//', dummy_pk_view, name='namespaced'), From 9cfa4bd7cca19df0bc8e456d906c3ab7ce285cf4 Mon Sep 17 00:00:00 2001 From: rizwanshaikh Date: Sat, 17 Jun 2023 08:48:25 +0530 Subject: [PATCH 35/37] Fix OpenAPI Schema yaml rendering for timedelta (#9007) * fix OpenAPIRenderer for timedelta * added test for rendering openapi with timedelta * fix OpenAPIRenderer for timedelta * added test for rendering openapi with timedelta * Removed usage of field.choices that triggered full table load (#8950) Removed the `{{ field.choices|yesno:",disabled" }}` block because this triggers the loading of full database table worth of objects just to determine whether the multi-select widget should be set as disabled or not. Since this "disabled" marking feature is not present in the normal select field, then I propose to remove it also from the multi-select. * Added Deprecation Warnings for CoreAPI (#7519) * Added Deprecation Warnings for CoreAPI * Bumped removal to DRF315 * Update rest_framework/__init__.py * Update rest_framework/filters.py * Update rest_framework/filters.py * Update tests/schemas/test_coreapi.py * Update rest_framework/filters.py * Update rest_framework/filters.py * Update tests/schemas/test_coreapi.py * Update tests/schemas/test_coreapi.py * Update setup.cfg * Update rest_framework/pagination.py --------- Co-authored-by: Asif Saif Uddin * Update copy right timeline * Fix NamespaceVersioning ignoring DEFAULT_VERSION on non-None namespaces (#7278) * Fix the case where if the namespace is not None and there's no match, NamespaceVersioning always raises NotFound even if DEFAULT_VERSION is set or None is in ALLOWED_VERSIONS * Add test cases * fix OpenAPIRenderer for timedelta * added test for rendering openapi with timedelta * added testcase for rendering yaml with minvalidator for duration field (timedelta) --------- Co-authored-by: Rizwan Shaikh Co-authored-by: Lenno Nagel Co-authored-by: David Smith <39445562+smithdc1@users.noreply.github.com> Co-authored-by: Asif Saif Uddin Co-authored-by: Konstantin Kuchkov --- rest_framework/renderers.py | 2 ++ rest_framework/utils/encoders.py | 11 +++++++++++ tests/schemas/test_openapi.py | 25 +++++++++++++++++++++++++ tests/schemas/views.py | 5 +++++ 4 files changed, 43 insertions(+) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 0a3b03729..db1fdd128 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -9,6 +9,7 @@ REST framework also provides an HTML renderer that renders the browsable API. import base64 import contextlib +import datetime from urllib import parse from django import forms @@ -1062,6 +1063,7 @@ class OpenAPIRenderer(BaseRenderer): def ignore_aliases(self, data): return True Dumper.add_representer(SafeString, Dumper.represent_str) + Dumper.add_representer(datetime.timedelta, encoders.CustomScalar.represent_timedelta) return yaml.dump(data, default_flow_style=False, sort_keys=False, Dumper=Dumper).encode('utf-8') diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py index 35a89eb09..aa4542286 100644 --- a/rest_framework/utils/encoders.py +++ b/rest_framework/utils/encoders.py @@ -65,3 +65,14 @@ class JSONEncoder(json.JSONEncoder): elif hasattr(obj, '__iter__'): return tuple(item for item in obj) return super().default(obj) + + +class CustomScalar: + """ + CustomScalar that knows how to encode timedelta that renderer + can understand. + """ + @classmethod + def represent_timedelta(cls, dumper, data): + value = str(data.total_seconds()) + return dumper.represent_scalar('tag:yaml.org,2002:str', value) diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index 0ea6d1ff9..1eb5b84b7 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -1162,6 +1162,31 @@ class TestGenerator(TestCase): assert b'"openapi": "' in ret assert b'"default": "0.0"' in ret + def test_schema_rendering_to_yaml(self): + patterns = [ + path('example/', views.ExampleGenericAPIView.as_view()), + ] + generator = SchemaGenerator(patterns=patterns) + + request = create_request('/') + schema = generator.get_schema(request=request) + ret = OpenAPIRenderer().render(schema) + assert b"openapi: " in ret + assert b"default: '0.0'" in ret + + def test_schema_rendering_timedelta_to_yaml_with_validator(self): + + patterns = [ + path('example/', views.ExampleValidatedAPIView.as_view()), + ] + generator = SchemaGenerator(patterns=patterns) + + request = create_request('/') + schema = generator.get_schema(request=request) + ret = OpenAPIRenderer().render(schema) + assert b"openapi: " in ret + assert b"duration:\n type: string\n minimum: \'10.0\'\n" in ret + def test_schema_with_no_paths(self): patterns = [] generator = SchemaGenerator(patterns=patterns) diff --git a/tests/schemas/views.py b/tests/schemas/views.py index f1ed0bd4e..c08208bf2 100644 --- a/tests/schemas/views.py +++ b/tests/schemas/views.py @@ -134,6 +134,11 @@ class ExampleValidatedSerializer(serializers.Serializer): ip4 = serializers.IPAddressField(protocol='ipv4') ip6 = serializers.IPAddressField(protocol='ipv6') ip = serializers.IPAddressField() + duration = serializers.DurationField( + validators=( + MinValueValidator(timedelta(seconds=10)), + ) + ) class ExampleValidatedAPIView(generics.GenericAPIView): From 8b7e6f2e3405d6e71488db53f2dda515d5a336c6 Mon Sep 17 00:00:00 2001 From: Samiul Sk Date: Tue, 20 Jun 2023 17:13:33 +0530 Subject: [PATCH 36/37] Update pre-commit.yml (#9012) --- .github/workflows/pre-commit.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 9c29ed056..36d356493 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -8,17 +8,17 @@ on: jobs: pre-commit: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v4 with: - python-version: 3.9 + python-version: "3.10" - - uses: pre-commit/action@v2.0.0 + - uses: pre-commit/action@v3.0.0 with: token: ${{ secrets.GITHUB_TOKEN }} From 8dd4250d0234ddf6c6a19a806639678ef7786468 Mon Sep 17 00:00:00 2001 From: Samiul Sk Date: Wed, 21 Jun 2023 10:35:44 +0530 Subject: [PATCH 37/37] remove unnecessary line which was causing isort error (#9014) --- rest_framework/pagination.py | 1 - 1 file changed, 1 deletion(-) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index ce8778547..7303890b0 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -5,7 +5,6 @@ be used for paginated responses. import contextlib import warnings - from base64 import b64decode, b64encode from collections import namedtuple from urllib import parse