From 8a5a4f3362ab329d00850589c7487170d5b0ea3f Mon Sep 17 00:00:00 2001 From: xiaoyu <763691951@qq.com> Date: Sun, 18 Sep 2016 14:28:34 +0800 Subject: [PATCH 1/4] fix #14 --- apps/static/css/style.css | 4 +-- apps/static/js/plugins/toastr/toastr.js.map | 1 - apps/static/js/plugins/toastr/toastr.min.js | 1 - apps/templates/base.html | 23 ++++++++++-- apps/users/api.py | 15 ++++++-- apps/users/forms.py | 4 ++- apps/users/models.py | 1 + apps/users/serializers.py | 19 ++++++---- .../templates/users/_user_reset_pk_modal.html | 8 ----- .../users/_user_update_pk_modal.html | 8 +++++ apps/users/templates/users/first_login.html | 2 ++ apps/users/templates/users/user_detail.html | 36 +++++++++++++++++++ apps/users/urls.py | 1 + apps/users/views.py | 15 ++++++-- 14 files changed, 113 insertions(+), 25 deletions(-) delete mode 100644 apps/static/js/plugins/toastr/toastr.js.map delete mode 100644 apps/users/templates/users/_user_reset_pk_modal.html create mode 100644 apps/users/templates/users/_user_update_pk_modal.html diff --git a/apps/static/css/style.css b/apps/static/css/style.css index df14bf9aa..fb30bde06 100644 --- a/apps/static/css/style.css +++ b/apps/static/css/style.css @@ -1,5 +1,5 @@ -@import url("https://fonts.useso.com/css?family=Open+Sans:300,400,600,700"); -@import url("https://fonts.useso.com/css?family=Roboto:400,300,500,700"); +@import url("https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700"); +@import url("https://fonts.googleapis.com/css?family=Roboto:400,300,500,700"); /* * * INSPINIA - Responsive Admin Theme diff --git a/apps/static/js/plugins/toastr/toastr.js.map b/apps/static/js/plugins/toastr/toastr.js.map deleted file mode 100644 index 07b5237f9..000000000 --- a/apps/static/js/plugins/toastr/toastr.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["toastr.js"],"names":["define","$","error","message","title","optionsOverride","notify","type","toastType","iconClass","getOptions","iconClasses","getContainer","options","create","$container","containerId","length","createContainer","info","subscribe","callback","listener","success","warning","clear","$toastElement","clearOptions","clearToast","clearContainer","remove","removeToast","children","toastsToClear","i","force","hideMethod","duration","hideDuration","easing","hideEasing","complete","attr","addClass","positionClass","appendTo","target","getDefaults","tapToDismiss","toastClass","debug","showMethod","showDuration","showEasing","onShown","undefined","onHidden","closeMethod","closeDuration","closeEasing","closeOnHover","extendedTimeOut","timeOut","titleClass","messageClass","escapeHtml","closeHtml","closeClass","newestOnTop","preventDuplicates","progressBar","progressClass","rtl","publish","args","map","source","replace","personalizeToast","setIcon","setTitle","setMessage","setCloseButton","setProgressBar","setRTL","setSequence","setAria","ariaValue","handleEvents","hover","stickAround","delayedHideToast","onclick","click","hideToast","closeButton","$closeElement","event","stopPropagation","cancelBubble","onCloseClick","displayToast","hide","intervalId","setTimeout","maxHideTime","parseFloat","hideEta","Date","getTime","setInterval","updateProgress","prepend","append","suffix","$titleElement","$messageElement","$progressElement","shouldExit","previousToast","override","method","clearTimeout","response","state","endTime","stop","percentage","width","extend","toastId","startTime","console","log","toastr","is","version","amd","deps","factory","module","exports","require","window","jQuery"],"mappings":"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","file":"toastr.js","sourcesContent":["/*\n * Toastr\n * Copyright 2012-2015\n * Authors: John Papa, Hans Fjällemark, and Tim Ferrell.\n * All Rights Reserved.\n * Use, reproduction, distribution, and modification of this code is subject to the terms and\n * conditions of the MIT license, available at http://www.opensource.org/licenses/mit-license.php\n *\n * ARIA Support: Greta Krafsig\n *\n * Project: https://github.com/CodeSeven/toastr\n */\n/* global define */\n(function (define) {\n define(['jquery'], function ($) {\n return (function () {\n var $container;\n var listener;\n var toastId = 0;\n var toastType = {\n error: 'error',\n info: 'info',\n success: 'success',\n warning: 'warning'\n };\n\n var toastr = {\n clear: clear,\n remove: remove,\n error: error,\n getContainer: getContainer,\n info: info,\n options: {},\n subscribe: subscribe,\n success: success,\n version: '2.1.3',\n warning: warning\n };\n\n var previousToast;\n\n return toastr;\n\n ////////////////\n\n function error(message, title, optionsOverride) {\n return notify({\n type: toastType.error,\n iconClass: getOptions().iconClasses.error,\n message: message,\n optionsOverride: optionsOverride,\n title: title\n });\n }\n\n function getContainer(options, create) {\n if (!options) { options = getOptions(); }\n $container = $('#' + options.containerId);\n if ($container.length) {\n return $container;\n }\n if (create) {\n $container = createContainer(options);\n }\n return $container;\n }\n\n function info(message, title, optionsOverride) {\n return notify({\n type: toastType.info,\n iconClass: getOptions().iconClasses.info,\n message: message,\n optionsOverride: optionsOverride,\n title: title\n });\n }\n\n function subscribe(callback) {\n listener = callback;\n }\n\n function success(message, title, optionsOverride) {\n return notify({\n type: toastType.success,\n iconClass: getOptions().iconClasses.success,\n message: message,\n optionsOverride: optionsOverride,\n title: title\n });\n }\n\n function warning(message, title, optionsOverride) {\n return notify({\n type: toastType.warning,\n iconClass: getOptions().iconClasses.warning,\n message: message,\n optionsOverride: optionsOverride,\n title: title\n });\n }\n\n function clear($toastElement, clearOptions) {\n var options = getOptions();\n if (!$container) { getContainer(options); }\n if (!clearToast($toastElement, options, clearOptions)) {\n clearContainer(options);\n }\n }\n\n function remove($toastElement) {\n var options = getOptions();\n if (!$container) { getContainer(options); }\n if ($toastElement && $(':focus', $toastElement).length === 0) {\n removeToast($toastElement);\n return;\n }\n if ($container.children().length) {\n $container.remove();\n }\n }\n\n // internal functions\n\n function clearContainer (options) {\n var toastsToClear = $container.children();\n for (var i = toastsToClear.length - 1; i >= 0; i--) {\n clearToast($(toastsToClear[i]), options);\n }\n }\n\n function clearToast ($toastElement, options, clearOptions) {\n var force = clearOptions && clearOptions.force ? clearOptions.force : false;\n if ($toastElement && (force || $(':focus', $toastElement).length === 0)) {\n $toastElement[options.hideMethod]({\n duration: options.hideDuration,\n easing: options.hideEasing,\n complete: function () { removeToast($toastElement); }\n });\n return true;\n }\n return false;\n }\n\n function createContainer(options) {\n $container = $('
')\n .attr('id', options.containerId)\n .addClass(options.positionClass);\n\n $container.appendTo($(options.target));\n return $container;\n }\n\n function getDefaults() {\n return {\n tapToDismiss: true,\n toastClass: 'toast',\n containerId: 'toast-container',\n debug: false,\n\n showMethod: 'fadeIn', //fadeIn, slideDown, and show are built into jQuery\n showDuration: 300,\n showEasing: 'swing', //swing and linear are built into jQuery\n onShown: undefined,\n hideMethod: 'fadeOut',\n hideDuration: 1000,\n hideEasing: 'swing',\n onHidden: undefined,\n closeMethod: false,\n closeDuration: false,\n closeEasing: false,\n closeOnHover: true,\n\n extendedTimeOut: 1000,\n iconClasses: {\n error: 'toast-error',\n info: 'toast-info',\n success: 'toast-success',\n warning: 'toast-warning'\n },\n iconClass: 'toast-info',\n positionClass: 'toast-top-right',\n timeOut: 5000, // Set timeOut and extendedTimeOut to 0 to make it sticky\n titleClass: 'toast-title',\n messageClass: 'toast-message',\n escapeHtml: false,\n target: 'body',\n closeHtml: '',\n closeClass: 'toast-close-button',\n newestOnTop: true,\n preventDuplicates: false,\n progressBar: false,\n progressClass: 'toast-progress',\n rtl: false\n };\n }\n\n function publish(args) {\n if (!listener) { return; }\n listener(args);\n }\n\n function notify(map) {\n var options = getOptions();\n var iconClass = map.iconClass || options.iconClass;\n\n if (typeof (map.optionsOverride) !== 'undefined') {\n options = $.extend(options, map.optionsOverride);\n iconClass = map.optionsOverride.iconClass || iconClass;\n }\n\n if (shouldExit(options, map)) { return; }\n\n toastId++;\n\n $container = getContainer(options, true);\n\n var intervalId = null;\n var $toastElement = $('
');\n var $titleElement = $('
');\n var $messageElement = $('
');\n var $progressElement = $('
');\n var $closeElement = $(options.closeHtml);\n var progressBar = {\n intervalId: null,\n hideEta: null,\n maxHideTime: null\n };\n var response = {\n toastId: toastId,\n state: 'visible',\n startTime: new Date(),\n options: options,\n map: map\n };\n\n personalizeToast();\n\n displayToast();\n\n handleEvents();\n\n publish(response);\n\n if (options.debug && console) {\n console.log(response);\n }\n\n return $toastElement;\n\n function escapeHtml(source) {\n if (source == null) {\n source = '';\n }\n\n return source\n .replace(/&/g, '&')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n .replace(//g, '>');\n }\n\n function personalizeToast() {\n setIcon();\n setTitle();\n setMessage();\n setCloseButton();\n setProgressBar();\n setRTL();\n setSequence();\n setAria();\n }\n\n function setAria() {\n var ariaValue = '';\n switch (map.iconClass) {\n case 'toast-success':\n case 'toast-info':\n ariaValue = 'polite';\n break;\n default:\n ariaValue = 'assertive';\n }\n $toastElement.attr('aria-live', ariaValue);\n }\n\n function handleEvents() {\n if (options.closeOnHover) {\n $toastElement.hover(stickAround, delayedHideToast);\n }\n\n if (!options.onclick && options.tapToDismiss) {\n $toastElement.click(hideToast);\n }\n\n if (options.closeButton && $closeElement) {\n $closeElement.click(function (event) {\n if (event.stopPropagation) {\n event.stopPropagation();\n } else if (event.cancelBubble !== undefined && event.cancelBubble !== true) {\n event.cancelBubble = true;\n }\n\n if (options.onCloseClick) {\n options.onCloseClick(event);\n }\n\n hideToast(true);\n });\n }\n\n if (options.onclick) {\n $toastElement.click(function (event) {\n options.onclick(event);\n hideToast();\n });\n }\n }\n\n function displayToast() {\n $toastElement.hide();\n\n $toastElement[options.showMethod](\n {duration: options.showDuration, easing: options.showEasing, complete: options.onShown}\n );\n\n if (options.timeOut > 0) {\n intervalId = setTimeout(hideToast, options.timeOut);\n progressBar.maxHideTime = parseFloat(options.timeOut);\n progressBar.hideEta = new Date().getTime() + progressBar.maxHideTime;\n if (options.progressBar) {\n progressBar.intervalId = setInterval(updateProgress, 10);\n }\n }\n }\n\n function setIcon() {\n if (map.iconClass) {\n $toastElement.addClass(options.toastClass).addClass(iconClass);\n }\n }\n\n function setSequence() {\n if (options.newestOnTop) {\n $container.prepend($toastElement);\n } else {\n $container.append($toastElement);\n }\n }\n\n function setTitle() {\n if (map.title) {\n var suffix = map.title;\n if (options.escapeHtml) {\n suffix = escapeHtml(map.title);\n }\n $titleElement.append(suffix).addClass(options.titleClass);\n $toastElement.append($titleElement);\n }\n }\n\n function setMessage() {\n if (map.message) {\n var suffix = map.message;\n if (options.escapeHtml) {\n suffix = escapeHtml(map.message);\n }\n $messageElement.append(suffix).addClass(options.messageClass);\n $toastElement.append($messageElement);\n }\n }\n\n function setCloseButton() {\n if (options.closeButton) {\n $closeElement.addClass(options.closeClass).attr('role', 'button');\n $toastElement.prepend($closeElement);\n }\n }\n\n function setProgressBar() {\n if (options.progressBar) {\n $progressElement.addClass(options.progressClass);\n $toastElement.prepend($progressElement);\n }\n }\n\n function setRTL() {\n if (options.rtl) {\n $toastElement.addClass('rtl');\n }\n }\n\n function shouldExit(options, map) {\n if (options.preventDuplicates) {\n if (map.message === previousToast) {\n return true;\n } else {\n previousToast = map.message;\n }\n }\n return false;\n }\n\n function hideToast(override) {\n var method = override && options.closeMethod !== false ? options.closeMethod : options.hideMethod;\n var duration = override && options.closeDuration !== false ?\n options.closeDuration : options.hideDuration;\n var easing = override && options.closeEasing !== false ? options.closeEasing : options.hideEasing;\n if ($(':focus', $toastElement).length && !override) {\n return;\n }\n clearTimeout(progressBar.intervalId);\n return $toastElement[method]({\n duration: duration,\n easing: easing,\n complete: function () {\n removeToast($toastElement);\n clearTimeout(intervalId);\n if (options.onHidden && response.state !== 'hidden') {\n options.onHidden();\n }\n response.state = 'hidden';\n response.endTime = new Date();\n publish(response);\n }\n });\n }\n\n function delayedHideToast() {\n if (options.timeOut > 0 || options.extendedTimeOut > 0) {\n intervalId = setTimeout(hideToast, options.extendedTimeOut);\n progressBar.maxHideTime = parseFloat(options.extendedTimeOut);\n progressBar.hideEta = new Date().getTime() + progressBar.maxHideTime;\n }\n }\n\n function stickAround() {\n clearTimeout(intervalId);\n progressBar.hideEta = 0;\n $toastElement.stop(true, true)[options.showMethod](\n {duration: options.showDuration, easing: options.showEasing}\n );\n }\n\n function updateProgress() {\n var percentage = ((progressBar.hideEta - (new Date().getTime())) / progressBar.maxHideTime) * 100;\n $progressElement.width(percentage + '%');\n }\n }\n\n function getOptions() {\n return $.extend({}, getDefaults(), toastr.options);\n }\n\n function removeToast($toastElement) {\n if (!$container) { $container = getContainer(); }\n if ($toastElement.is(':visible')) {\n return;\n }\n $toastElement.remove();\n $toastElement = null;\n if ($container.children().length === 0) {\n $container.remove();\n previousToast = undefined;\n }\n }\n\n })();\n });\n}(typeof define === 'function' && define.amd ? define : function (deps, factory) {\n if (typeof module !== 'undefined' && module.exports) { //Node\n module.exports = factory(require('jquery'));\n } else {\n window.toastr = factory(window.jQuery);\n }\n}));\n"],"sourceRoot":"/source/"} \ No newline at end of file diff --git a/apps/static/js/plugins/toastr/toastr.min.js b/apps/static/js/plugins/toastr/toastr.min.js index 7c0c07c2a..9d17638cc 100644 --- a/apps/static/js/plugins/toastr/toastr.min.js +++ b/apps/static/js/plugins/toastr/toastr.min.js @@ -1,2 +1 @@ !function(e){e(["jquery"],function(e){return function(){function t(e,t,n){return g({type:O.error,iconClass:m().iconClasses.error,message:e,optionsOverride:n,title:t})}function n(t,n){return t||(t=m()),v=e("#"+t.containerId),v.length?v:(n&&(v=d(t)),v)}function o(e,t,n){return g({type:O.info,iconClass:m().iconClasses.info,message:e,optionsOverride:n,title:t})}function s(e){C=e}function i(e,t,n){return g({type:O.success,iconClass:m().iconClasses.success,message:e,optionsOverride:n,title:t})}function a(e,t,n){return g({type:O.warning,iconClass:m().iconClasses.warning,message:e,optionsOverride:n,title:t})}function r(e,t){var o=m();v||n(o),u(e,o,t)||l(o)}function c(t){var o=m();return v||n(o),t&&0===e(":focus",t).length?void h(t):void(v.children().length&&v.remove())}function l(t){for(var n=v.children(),o=n.length-1;o>=0;o--)u(e(n[o]),t)}function u(t,n,o){var s=!(!o||!o.force)&&o.force;return!(!t||!s&&0!==e(":focus",t).length)&&(t[n.hideMethod]({duration:n.hideDuration,easing:n.hideEasing,complete:function(){h(t)}}),!0)}function d(t){return v=e("
").attr("id",t.containerId).addClass(t.positionClass),v.appendTo(e(t.target)),v}function p(){return{tapToDismiss:!0,toastClass:"toast",containerId:"toast-container",debug:!1,showMethod:"fadeIn",showDuration:300,showEasing:"swing",onShown:void 0,hideMethod:"fadeOut",hideDuration:1e3,hideEasing:"swing",onHidden:void 0,closeMethod:!1,closeDuration:!1,closeEasing:!1,closeOnHover:!0,extendedTimeOut:1e3,iconClasses:{error:"toast-error",info:"toast-info",success:"toast-success",warning:"toast-warning"},iconClass:"toast-info",positionClass:"toast-top-right",timeOut:5e3,titleClass:"toast-title",messageClass:"toast-message",escapeHtml:!1,target:"body",closeHtml:'',closeClass:"toast-close-button",newestOnTop:!0,preventDuplicates:!1,progressBar:!1,progressClass:"toast-progress",rtl:!1}}function f(e){C&&C(e)}function g(t){function o(e){return null==e&&(e=""),e.replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">")}function s(){c(),u(),d(),p(),g(),C(),l(),i()}function i(){var e="";switch(t.iconClass){case"toast-success":case"toast-info":e="polite";break;default:e="assertive"}I.attr("aria-live",e)}function a(){E.closeOnHover&&I.hover(H,D),!E.onclick&&E.tapToDismiss&&I.click(b),E.closeButton&&j&&j.click(function(e){e.stopPropagation?e.stopPropagation():void 0!==e.cancelBubble&&e.cancelBubble!==!0&&(e.cancelBubble=!0),E.onCloseClick&&E.onCloseClick(e),b(!0)}),E.onclick&&I.click(function(e){E.onclick(e),b()})}function r(){I.hide(),I[E.showMethod]({duration:E.showDuration,easing:E.showEasing,complete:E.onShown}),E.timeOut>0&&(k=setTimeout(b,E.timeOut),F.maxHideTime=parseFloat(E.timeOut),F.hideEta=(new Date).getTime()+F.maxHideTime,E.progressBar&&(F.intervalId=setInterval(x,10)))}function c(){t.iconClass&&I.addClass(E.toastClass).addClass(y)}function l(){E.newestOnTop?v.prepend(I):v.append(I)}function u(){if(t.title){var e=t.title;E.escapeHtml&&(e=o(t.title)),M.append(e).addClass(E.titleClass),I.append(M)}}function d(){if(t.message){var e=t.message;E.escapeHtml&&(e=o(t.message)),B.append(e).addClass(E.messageClass),I.append(B)}}function p(){E.closeButton&&(j.addClass(E.closeClass).attr("role","button"),I.prepend(j))}function g(){E.progressBar&&(q.addClass(E.progressClass),I.prepend(q))}function C(){E.rtl&&I.addClass("rtl")}function O(e,t){if(e.preventDuplicates){if(t.message===w)return!0;w=t.message}return!1}function b(t){var n=t&&E.closeMethod!==!1?E.closeMethod:E.hideMethod,o=t&&E.closeDuration!==!1?E.closeDuration:E.hideDuration,s=t&&E.closeEasing!==!1?E.closeEasing:E.hideEasing;if(!e(":focus",I).length||t)return clearTimeout(F.intervalId),I[n]({duration:o,easing:s,complete:function(){h(I),clearTimeout(k),E.onHidden&&"hidden"!==P.state&&E.onHidden(),P.state="hidden",P.endTime=new Date,f(P)}})}function D(){(E.timeOut>0||E.extendedTimeOut>0)&&(k=setTimeout(b,E.extendedTimeOut),F.maxHideTime=parseFloat(E.extendedTimeOut),F.hideEta=(new Date).getTime()+F.maxHideTime)}function H(){clearTimeout(k),F.hideEta=0,I.stop(!0,!0)[E.showMethod]({duration:E.showDuration,easing:E.showEasing})}function x(){var e=(F.hideEta-(new Date).getTime())/F.maxHideTime*100;q.width(e+"%")}var E=m(),y=t.iconClass||E.iconClass;if("undefined"!=typeof t.optionsOverride&&(E=e.extend(E,t.optionsOverride),y=t.optionsOverride.iconClass||y),!O(E,t)){T++,v=n(E,!0);var k=null,I=e("
"),M=e("
"),B=e("
"),q=e("
"),j=e(E.closeHtml),F={intervalId:null,hideEta:null,maxHideTime:null},P={toastId:T,state:"visible",startTime:new Date,options:E,map:t};return s(),r(),a(),f(P),E.debug&&console&&console.log(P),I}}function m(){return e.extend({},p(),b.options)}function h(e){v||(v=n()),e.is(":visible")||(e.remove(),e=null,0===v.children().length&&(v.remove(),w=void 0))}var v,C,w,T=0,O={error:"error",info:"info",success:"success",warning:"warning"},b={clear:r,remove:c,error:t,getContainer:n,info:o,options:{},subscribe:s,success:i,version:"2.1.3",warning:a};return b}()})}("function"==typeof define&&define.amd?define:function(e,t){"undefined"!=typeof module&&module.exports?module.exports=t(require("jquery")):window.toastr=t(window.jQuery)}); -//# sourceMappingURL=toastr.js.map diff --git a/apps/templates/base.html b/apps/templates/base.html index 283eac9ba..f13a96dc9 100644 --- a/apps/templates/base.html +++ b/apps/templates/base.html @@ -1,4 +1,4 @@ -{% load static %} +{% load static i18n %} @@ -20,6 +20,25 @@
{% include '_header_bar.html' %} {% include '_message.html' %} + {% block first_login_message %} + {% if user.is_authenticated and user.is_first_login %} +
+ {% url 'users:user-first-login' as the_url %} + {% blocktrans %} + Your information was incomplete. Please click this link to complete your information. + {% endblocktrans %} +
+ {% endif %} + {% endblock %} + {% block update_public_key_message %} + {% if user.is_authenticated and not user.is_public_key_valid %} +
+ {% blocktrans %} + Your ssh-public-key has been expired. Please click this link to update your ssh-public-key. + {% endblocktrans %} +
+ {% endif %} + {% endblock %} {% block content %}{% endblock %} {% include '_footer.html' %}
@@ -28,4 +47,4 @@ {% include '_foot_js.html' %} {% block custom_foot_js %} {% endblock %} - \ No newline at end of file + diff --git a/apps/users/api.py b/apps/users/api.py index a2e196456..39044d2e7 100644 --- a/apps/users/api.py +++ b/apps/users/api.py @@ -5,7 +5,8 @@ import logging from rest_framework import generics -from .serializers import UserSerializer, UserGroupSerializer, UserAttributeSerializer, UserGroupEditSerializer +from .serializers import UserSerializer, UserGroupSerializer, UserAttributeSerializer, UserGroupEditSerializer, \ + UserPKUpdateSerializer from .models import User, UserGroup @@ -72,7 +73,17 @@ class UserResetPKApi(generics.UpdateAPIView): def perform_update(self, serializer): user = self.get_object() - user._public_key = '' + user.is_public_key_valid = False user.save() from .utils import send_reset_ssh_key_mail send_reset_ssh_key_mail(user) + + +class UserUpdatePKApi(generics.UpdateAPIView): + queryset = User.objects.all() + serializer_class = UserPKUpdateSerializer + + def perform_update(self, serializer): + user = self.get_object() + user.private_key = serializer.validated_data['_public_key'] + user.save() diff --git a/apps/users/forms.py b/apps/users/forms.py index 8db540e28..8c21011ce 100644 --- a/apps/users/forms.py +++ b/apps/users/forms.py @@ -79,9 +79,11 @@ class UserKeyForm(forms.Form): help_text=_('Paste your id_ras.pub here.')) def clean_public_key(self): + public_key = self.cleaned_data['public_key'] + if self.user._public_key and public_key == self.user.public_key: + raise forms.ValidationError(_('Public key should not be the same as your old one.')) from sshpubkeys import SSHKey from sshpubkeys.exceptions import InvalidKeyException - public_key = self.cleaned_data['public_key'] ssh = SSHKey(public_key) try: ssh.parse() diff --git a/apps/users/models.py b/apps/users/models.py index 0b27009ca..da29568bc 100644 --- a/apps/users/models.py +++ b/apps/users/models.py @@ -80,6 +80,7 @@ class User(AbstractUser): date_expired = models.DateTimeField(default=date_expired_default, blank=True, null=True, verbose_name=_('Date expired')) created_by = models.CharField(max_length=30, default='', verbose_name=_('Created by')) + is_public_key_valid = models.BooleanField(default=False) @property def password_raw(self): diff --git a/apps/users/serializers.py b/apps/users/serializers.py index 7e70049f8..06db1b95e 100644 --- a/apps/users/serializers.py +++ b/apps/users/serializers.py @@ -46,11 +46,18 @@ class UserPKUpdateSerializer(serializers.ModelSerializer): class Meta: model = User - fields = ['id', '_private_key'] + fields = ['id', '_public_key'] - def validate__private_key(self, value): - from users.utils import validate_ssh_pk - checked, reason = validate_ssh_pk(value) - if not checked: - raise serializers.ValidationError(_('Not a valid ssh private key.')) + def validate__public_key(self, value): + from sshpubkeys import SSHKey + from sshpubkeys.exceptions import InvalidKeyException + ssh = SSHKey(value) + try: + ssh.parse() + except InvalidKeyException as e: + print e + raise serializers.ValidationError(_('Not a valid ssh public key')) + except NotImplementedError as e: + print e + raise serializers.ValidationError(_('Not a valid ssh public key')) return value diff --git a/apps/users/templates/users/_user_reset_pk_modal.html b/apps/users/templates/users/_user_reset_pk_modal.html deleted file mode 100644 index bd6b62715..000000000 --- a/apps/users/templates/users/_user_reset_pk_modal.html +++ /dev/null @@ -1,8 +0,0 @@ -{% extends '_modal.html' %} -{% load i18n %} -{% block modal_id %}user_reset_pk_modal{% endblock %} -{% block modal_title%}{% trans 'Reset User SSH Private Key' %}{% endblock %} -{% block modal_body %} - -{% endblock %} -{% block modal_confirm_id %}btn_user_reset_pk{% endblock %} diff --git a/apps/users/templates/users/_user_update_pk_modal.html b/apps/users/templates/users/_user_update_pk_modal.html new file mode 100644 index 000000000..a7ed525c4 --- /dev/null +++ b/apps/users/templates/users/_user_update_pk_modal.html @@ -0,0 +1,8 @@ +{% extends '_modal.html' %} +{% load i18n %} +{% block modal_id %}user_update_pk_modal{% endblock %} +{% block modal_title%}{% trans "Update User SSH Public Key" %}{% endblock %} +{% block modal_body %} + +{% endblock %} +{% block modal_confirm_id %}btn_user_update_pk{% endblock %} diff --git a/apps/users/templates/users/first_login.html b/apps/users/templates/users/first_login.html index 915a1f425..80b13cec2 100644 --- a/apps/users/templates/users/first_login.html +++ b/apps/users/templates/users/first_login.html @@ -3,10 +3,12 @@ {% load i18n %} {% load bootstrap %} + {% block custom_head_css_js %} {{ wizard.form.media }} {% endblock %} +{% block first_login_message %}{% endblock %} {% block content %}
diff --git a/apps/users/templates/users/user_detail.html b/apps/users/templates/users/user_detail.html index b0034e9c8..a4bcfe88a 100644 --- a/apps/users/templates/users/user_detail.html +++ b/apps/users/templates/users/user_detail.html @@ -160,6 +160,14 @@ + + {% trans 'Update ssh key' %}: + + + + + +
@@ -207,6 +215,7 @@
+ {% include 'users/_user_update_pk_modal.html' %} {% endblock %} {% block custom_foot_js %} {% endblock %} diff --git a/apps/users/urls.py b/apps/users/urls.py index d2a6690e0..96a45b074 100644 --- a/apps/users/urls.py +++ b/apps/users/urls.py @@ -43,6 +43,7 @@ urlpatterns += [ api.UserAttributeApi.as_view(), name='user-patch-api'), url(r'^v1/users/(?P\d+)/reset-password/$', api.UserResetPasswordApi.as_view(), name='user-reset-password-api'), url(r'^v1/users/(?P\d+)/reset-pk/$', api.UserResetPKApi.as_view(), name='user-reset-pk-api'), + url(r'^v1/users/(?P\d+)/update-pk/$', api.UserUpdatePKApi.as_view(), name='user-update-pk-api'), url(r'^v1/user-groups$', api.UserGroupListAddApi.as_view(), name='user-group-list-api'), url(r'^v1/user-groups/(?P[0-9]+)$', api.UserGroupDetailDeleteUpdateApi.as_view(), name='user-group-detail-api'), diff --git a/apps/users/views.py b/apps/users/views.py index 319d67ddf..2aa20efa5 100644 --- a/apps/users/views.py +++ b/apps/users/views.py @@ -19,7 +19,7 @@ from django.views.decorators.debug import sensitive_post_parameters from django.views.generic.base import TemplateView from django.views.generic.list import ListView from django.views.generic.edit import CreateView, DeleteView, UpdateView, FormView, SingleObjectMixin, \ - FormMixin, ModelFormMixin, ProcessFormView, BaseCreateView + FormMixin from django.views.generic.detail import DetailView from formtools.wizard.views import SessionWizardView @@ -332,6 +332,7 @@ class UserFirstLoginView(LoginRequiredMixin, SessionWizardView): if field.name == 'enable_otp': user.enable_otp = field.value() user.is_first_login = False + user.is_public_key_valid = True user.save() return redirect(reverse('index')) @@ -351,6 +352,16 @@ class UserFirstLoginView(LoginRequiredMixin, SessionWizardView): } return super(UserFirstLoginView, self).get_form_initial(step) + def get_form(self, step=None, data=None, files=None): + form = super(UserFirstLoginView, self).get_form(step, data, files) + + if step is None: + step = self.steps.current + + if step == '1': + form.user = self.request.user + return form + class UserAssetPermissionView(AdminUserRequiredMixin, FormMixin, SingleObjectMixin, ListView): paginate_by = settings.CONFIG.DISPLAY_PER_PAGE @@ -376,7 +387,7 @@ class UserAssetPermissionView(AdminUserRequiredMixin, FormMixin, SingleObjectMix def get_queryset(self): asset_permissions = set(self.object.asset_permissions.all()) \ - | self.get_asset_permission_inherit_from_user_group() + | self.get_asset_permission_inherit_from_user_group() return list(asset_permissions) def get_context_data(self, **kwargs): From 4e2a41f4cf72724ed57702fe8f3e99f0b9f3e235 Mon Sep 17 00:00:00 2001 From: "xiaokong1937@gmail.com" <763691951@qq.com> Date: Sun, 18 Sep 2016 21:00:07 +0800 Subject: [PATCH 2/4] user-group draft --- .../templates/users/user_group_detail.html | 111 ++++++++++++++++++ .../templates/users/user_group_list.html | 27 ++--- apps/users/templates/users/user_update.html | 2 +- apps/users/views.py | 16 +-- 4 files changed, 132 insertions(+), 24 deletions(-) create mode 100644 apps/users/templates/users/user_group_detail.html diff --git a/apps/users/templates/users/user_group_detail.html b/apps/users/templates/users/user_group_detail.html new file mode 100644 index 000000000..5c6fb6639 --- /dev/null +++ b/apps/users/templates/users/user_group_detail.html @@ -0,0 +1,111 @@ +{% extends 'base.html' %} +{% load static %} +{% load i18n %} + +{% block custom_head_css_js %} + + + + +{% endblock %} +{% block content %} +
+
+
+
+ +
+
+
+
+ {{ object.name }} + +
+
+ + + + + + + + + + + + + + +
{% trans 'Name' %}:{{ object.name }}
{% trans 'Comment' %}:{{ object.comment }}
{% trans 'Created at:' %}:{{ object.date_created }}
+
+
+
+
+
+
+ {% trans 'Quick modify' %} +
+
+ + + + + + + + + + + + + + + +
{% trans 'Active' %}: +
+
+ + +
+
+
{% trans 'Enable OTP' %}: +
+
+ + +
+
+
{% trans 'Reset password' %}: + + + +
+
+
+
+
+
+
+
+
+{% endblock %} diff --git a/apps/users/templates/users/user_group_list.html b/apps/users/templates/users/user_group_list.html index 851291880..9449e1e3a 100644 --- a/apps/users/templates/users/user_group_list.html +++ b/apps/users/templates/users/user_group_list.html @@ -1,18 +1,17 @@ {% extends '_list_base.html' %} {% load i18n %} -{% load common_tags %} {% block content_left_head %} - 添加用户组 +{% trans "Add User Group" %} {% endblock %} {% block table_head %} - 名称 - 用户数量 - 资产数量 - 描述 + {% trans "Name" %} + {% trans "User Amount" %} + {% trans "Asset Amount" %} + {% trans "Comment" %} {% endblock %} @@ -27,8 +26,8 @@ {{ user_group.name }} - {{ user_group.users.all|length }} - 数量 + {{ user_group.users.count }} + 999 {{ user_group.comment|truncatewords:8 }} 编辑 @@ -42,16 +41,12 @@ diff --git a/apps/users/templates/users/user_update.html b/apps/users/templates/users/user_update.html index f0a0907e2..36294a01f 100644 --- a/apps/users/templates/users/user_update.html +++ b/apps/users/templates/users/user_update.html @@ -5,7 +5,7 @@
- +
{% endblock %} diff --git a/apps/users/views.py b/apps/users/views.py index 2aa20efa5..b59f222c3 100644 --- a/apps/users/views.py +++ b/apps/users/views.py @@ -133,7 +133,7 @@ class UserUpdateView(AdminUserRequiredMixin, UpdateView): model = User form_class = UserUpdateForm template_name = 'users/user_update.html' - context_object_name = 'user' + context_object_name = 'user_object' success_url = reverse_lazy('users:user-list') def form_valid(self, form): @@ -146,10 +146,6 @@ class UserUpdateView(AdminUserRequiredMixin, UpdateView): user.set_password(password) return super(UserUpdateView, self).form_valid(form) - def form_invalid(self, form): - print(form.errors) - return super(UserUpdateView, self).form_invalid(form) - def get_context_data(self, **kwargs): context = super(UserUpdateView, self).get_context_data(**kwargs) context.update({'app': _('Users'), 'action': _('Update user')}) @@ -238,8 +234,14 @@ class UserGroupUpdateView(UpdateView): pass -class UserGroupDetailView(DetailView): - pass +class UserGroupDetailView(AdminUserRequiredMixin, DetailView): + model = UserGroup + template_name = 'users/user_group_detail.html' + + def get_context_data(self, **kwargs): + context = {'app': _('Users'), 'action': _('User Group Detail')} + kwargs.update(context) + return super(UserGroupDetailView, self).get_context_data(**kwargs) class UserGroupDeleteView(DeleteView): From f00da207649924820c61192e7a96d25610e71a10 Mon Sep 17 00:00:00 2001 From: xiaoyu <763691951@qq.com> Date: Mon, 19 Sep 2016 15:47:58 +0800 Subject: [PATCH 3/4] user-group delete implement --- apps/common/mixins.py | 38 ++++++++++++++ apps/users/api.py | 7 ++- apps/users/models.py | 46 +++++++++-------- apps/users/serializers.py | 8 ++- .../templates/users/user_group_list.html | 51 +++++++++++++++++-- apps/users/urls.py | 6 +-- 6 files changed, 126 insertions(+), 30 deletions(-) create mode 100644 apps/common/mixins.py diff --git a/apps/common/mixins.py b/apps/common/mixins.py new file mode 100644 index 000000000..997f2fbba --- /dev/null +++ b/apps/common/mixins.py @@ -0,0 +1,38 @@ +# coding: utf-8 + +from django.db import models +from django.utils.timezone import now +from django.utils.translation import ugettext_lazy as _ + + +class NoDeleteQuerySet(models.query.QuerySet): + + def delete(self): + return self.update(is_discard=True, discard_time=now()) + + +class NoDeleteManager(models.Manager): + + def get_all(self): + return NoDeleteQuerySet(self.model, using=self._db) + + def get_queryset(self): + return NoDeleteQuerySet(self.model, using=self._db).filter(is_discard=False) + + def get_deleted(self): + return NoDeleteQuerySet(self.model, using=self._db).filter(is_discard=True) + + +class NoDeleteModelMixin(models.Model): + is_discard = models.BooleanField(verbose_name=_("is discard"), default=False) + discard_time = models.DateTimeField(verbose_name=_("discard time"), null=True, blank=True) + + objects = NoDeleteManager() + + class Meta: + abstract = True + + def delete(self): + self.is_discard = True + self.discard_time = now() + return self.save() diff --git a/apps/users/api.py b/apps/users/api.py index 39044d2e7..2f683dcb6 100644 --- a/apps/users/api.py +++ b/apps/users/api.py @@ -6,7 +6,7 @@ import logging from rest_framework import generics from .serializers import UserSerializer, UserGroupSerializer, UserAttributeSerializer, UserGroupEditSerializer, \ - UserPKUpdateSerializer + GroupEditSerializer, UserPKUpdateSerializer from .models import User, UserGroup @@ -87,3 +87,8 @@ class UserUpdatePKApi(generics.UpdateAPIView): user = self.get_object() user.private_key = serializer.validated_data['_public_key'] user.save() + + +class GroupDeleteApi(generics.DestroyAPIView): + queryset = UserGroup.objects.all() + serializer_class = GroupEditSerializer diff --git a/apps/users/models.py b/apps/users/models.py index da29568bc..88d5ebdd3 100644 --- a/apps/users/models.py +++ b/apps/users/models.py @@ -4,19 +4,22 @@ from __future__ import unicode_literals from django.conf import settings from django.contrib.auth.hashers import make_password +from django.utils import timezone +from django.db import models from django.contrib.auth.models import AbstractUser -from django.core import signing -from django.db import models, IntegrityError from django.db.models.signals import post_save from django.dispatch import receiver -from django.utils import timezone +from django.db import IntegrityError from django.utils.translation import ugettext_lazy as _ +from django.core import signing + from rest_framework.authtoken.models import Token from common.utils import encrypt, decrypt, date_expired_default +from common.mixins import NoDeleteModelMixin -class UserGroup(models.Model): +class UserGroup(NoDeleteModelMixin): name = models.CharField(max_length=100, unique=True, verbose_name=_('Name')) comment = models.TextField(blank=True, verbose_name=_('Comment')) date_created = models.DateTimeField(auto_now_add=True) @@ -30,14 +33,20 @@ class UserGroup(models.Model): return True return False + def delete(self): + if self.name != 'Default': + self.users.clear() + return super(UserGroup, self).delete() + return True + class Meta: - db_table = 'user_group' + db_table = 'user-group' @classmethod def initial(cls): - group_or_create = cls.objects.get_or_create(name='Default', comment='Default user group for all user', - created_by='System') - return group_or_create[0] + group, created = cls.objects.get_or_create(name='Default', comment='Default user group for all user', + created_by='System') + return group @classmethod def generate_fake(cls, count=100): @@ -48,8 +57,7 @@ class UserGroup(models.Model): for i in range(count): group = cls(name=forgery_py.name.full_name(), comment=forgery_py.lorem_ipsum.sentence(), - created_by=choice(User.objects.all()).username - ) + created_by=choice(User.objects.all()).username) try: group.save() except IntegrityError: @@ -76,11 +84,10 @@ class User(AbstractUser): _private_key = models.CharField(max_length=5000, blank=True, verbose_name=_('ssh private key')) _public_key = models.CharField(max_length=1000, blank=True, verbose_name=_('ssh public key')) comment = models.TextField(max_length=200, blank=True, verbose_name=_('Comment')) - is_first_login = models.BooleanField(default=True) + is_first_login = models.BooleanField(default=False) date_expired = models.DateTimeField(default=date_expired_default, blank=True, null=True, verbose_name=_('Date expired')) created_by = models.CharField(max_length=30, default='', verbose_name=_('Created by')) - is_public_key_valid = models.BooleanField(default=False) @property def password_raw(self): @@ -144,17 +151,13 @@ class User(AbstractUser): pass def save(self, *args, **kwargs): - # If user not set name, it's default equal username if not self.name: self.name = self.username super(User, self).save(*args, **kwargs) - # Set user default group 'All' - # Todo: It's have bug + # Add the current user to the default group. group = UserGroup.initial() - if group not in self.groups.all(): - self.groups.add(group) - # super(User, self).save(*args, **kwargs) + self.groups.add(group) @property def private_token(self): @@ -227,8 +230,7 @@ class User(AbstractUser): role=choice(dict(User.ROLE_CHOICES).keys()), wechat=forgery_py.internet.user_name(True), comment=forgery_py.lorem_ipsum.sentence(), - created_by=choice(cls.objects.all()).username, - ) + created_by=choice(cls.objects.all()).username) try: user.save() except IntegrityError: @@ -240,13 +242,13 @@ class User(AbstractUser): def init_all_models(): for model in (UserGroup, User): - if hasattr(model, b'initial'): + if hasattr(model, 'initial'): model.initial() def generate_fake(): for model in (UserGroup, User): - if hasattr(model, b'generate_fake'): + if hasattr(model, 'generate_fake'): model.generate_fake() diff --git a/apps/users/serializers.py b/apps/users/serializers.py index 06db1b95e..8e117a90d 100644 --- a/apps/users/serializers.py +++ b/apps/users/serializers.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- # - from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers @@ -27,6 +26,13 @@ class UserGroupSerializer(serializers.ModelSerializer): fields = '__all__' +class GroupEditSerializer(serializers.ModelSerializer): + + class Meta: + model = UserGroup + fields = ['id', 'name', 'comment', 'date_created', 'created_by'] + + class UserAttributeSerializer(serializers.ModelSerializer): class Meta: diff --git a/apps/users/templates/users/user_group_list.html b/apps/users/templates/users/user_group_list.html index 9449e1e3a..8294a238f 100644 --- a/apps/users/templates/users/user_group_list.html +++ b/apps/users/templates/users/user_group_list.html @@ -1,5 +1,10 @@ {% extends '_list_base.html' %} -{% load i18n %} +{% load i18n static %} +{% block custom_head_css_js %} + + +{% endblock %} + {% block content_left_head %} {% trans "Add User Group" %} {% endblock %} @@ -30,8 +35,9 @@ 999 {{ user_group.comment|truncatewords:8 }} - 编辑 - 删除 + {% trans "Edit" %} + {% trans "Delete" %} {% endfor %} @@ -52,3 +58,42 @@ {% endblock %} +{% block custom_foot_js %} + +{% endblock %} diff --git a/apps/users/urls.py b/apps/users/urls.py index 96a45b074..effa6fa39 100644 --- a/apps/users/urls.py +++ b/apps/users/urls.py @@ -22,8 +22,7 @@ urlpatterns = [ name='user-asset-permission-create'), url(r'^user/(?P[0-9]+)/granted-asset', views.UserGrantedAssetView.as_view(), name='user-granted-asset'), url(r'^user/(?P[0-9]+)/login-history', views.UserDetailView.as_view(), name='user-login-history'), - url(r'^first-login/$', views.UserFirstLoginView.as_view(), name='user-first-login'), - url(r'^user/(?P[0-9]+)/assets-perm$', views.UserDetailView.as_view(), name='user-detail'), + url(r'^first-login/$', views.UserFirstLoginView.as_view(), name='user-first-login'), url(r'^user/(?P[0-9]+)/assets-perm$', views.UserDetailView.as_view(), name='user-detail'), url(r'^user/create$', views.UserCreateView.as_view(), name='user-create'), url(r'^user/(?P[0-9]+)/update$', views.UserUpdateView.as_view(), name='user-update'), url(r'^user/(?P[0-9]+)/delete$', views.UserDeleteView.as_view(), name='user-delete'), @@ -31,7 +30,6 @@ urlpatterns = [ url(r'^user-group/(?P[0-9]+)$', views.UserGroupDetailView.as_view(), name='user-group-detail'), url(r'^user-group/create$', views.UserGroupCreateView.as_view(), name='user-group-create'), url(r'^user-group/(?P[0-9]+)/update$', views.UserGroupUpdateView.as_view(), name='user-group-update'), - url(r'^user-group/(?P[0-9]+)/delete$', views.UserGroupDeleteView.as_view(), name='user-group-delete'), ] @@ -49,4 +47,6 @@ urlpatterns += [ api.UserGroupDetailDeleteUpdateApi.as_view(), name='user-group-detail-api'), url(r'^v1/user-groups/(?P[0-9]+)/edit$', api.UserGroupEditApi.as_view(), name='user-group-edit-api'), + url(r'^v1/user-groups/(?P[0-9]+)/delete/$', api.GroupDeleteApi.as_view(), + name='user-group-delete-api'), ] From 9a81057d95eef230e86dfc8d8a8b1d6548c02452 Mon Sep 17 00:00:00 2001 From: xiaoyu <763691951@qq.com> Date: Mon, 19 Sep 2016 15:51:28 +0800 Subject: [PATCH 4/4] fix a typo --- apps/users/urls.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/users/urls.py b/apps/users/urls.py index effa6fa39..e4ccce63e 100644 --- a/apps/users/urls.py +++ b/apps/users/urls.py @@ -22,7 +22,8 @@ urlpatterns = [ name='user-asset-permission-create'), url(r'^user/(?P[0-9]+)/granted-asset', views.UserGrantedAssetView.as_view(), name='user-granted-asset'), url(r'^user/(?P[0-9]+)/login-history', views.UserDetailView.as_view(), name='user-login-history'), - url(r'^first-login/$', views.UserFirstLoginView.as_view(), name='user-first-login'), url(r'^user/(?P[0-9]+)/assets-perm$', views.UserDetailView.as_view(), name='user-detail'), + url(r'^first-login/$', views.UserFirstLoginView.as_view(), name='user-first-login'), + url(r'^user/(?P[0-9]+)/assets-perm$', views.UserDetailView.as_view(), name='user-detail'), url(r'^user/create$', views.UserCreateView.as_view(), name='user-create'), url(r'^user/(?P[0-9]+)/update$', views.UserUpdateView.as_view(), name='user-update'), url(r'^user/(?P[0-9]+)/delete$', views.UserDeleteView.as_view(), name='user-delete'),