├── .gitignore ├── ChangeLog ├── LICENSE ├── MANIFEST.in ├── README.md ├── README.txt ├── example ├── app │ ├── __init__.py │ ├── models.py │ ├── siteuser_custom.py │ ├── static │ │ ├── bootstrap │ │ │ ├── css │ │ │ │ └── bootstrap.min.css │ │ │ ├── img │ │ │ │ ├── glyphicons-halflings-white.png │ │ │ │ └── glyphicons-halflings.png │ │ │ └── js │ │ │ │ └── bootstrap.min.js │ │ └── jquery-1.9.0.min.js │ ├── templates │ │ ├── account_settings.html │ │ ├── base.html │ │ ├── change_password.html │ │ ├── home.html │ │ ├── login.html │ │ ├── notify.html │ │ ├── register.html │ │ └── reset_password.html │ ├── tests.py │ ├── urls.py │ └── views.py ├── example │ ├── __init__.py │ ├── local_settings.py.example │ ├── settings.py │ ├── urls.py │ └── wsgi.py └── manage.py ├── requirements.txt ├── setup.py └── siteuser ├── __init__.py ├── context_processors.py ├── decorators.py ├── functional ├── __init__.py └── mail.py ├── middleware.py ├── notify ├── __init__.py ├── models.py ├── tests.py ├── urls.py └── views.py ├── settings.py ├── templates └── siteuser │ ├── base.html │ ├── change_password.html │ ├── login.html │ ├── notify.html │ ├── register.html │ ├── reset_password.html │ ├── reset_password_email.html │ └── upload_avatar.html ├── upload_avatar ├── __init__.py ├── models.py ├── signals.py ├── static │ ├── imgareaselect │ │ ├── css │ │ │ ├── border-anim-h.gif │ │ │ ├── border-anim-v.gif │ │ │ ├── border-h.gif │ │ │ ├── border-v.gif │ │ │ └── imgareaselect-default.css │ │ └── jquery.imgareaselect.min.js │ └── js │ │ ├── upload_avatar.js │ │ └── upload_avatar.min.js ├── tests.py ├── urls.py └── views.py ├── urls.py ├── users ├── __init__.py ├── models.py ├── static │ └── js │ │ └── siteuser.js ├── tasks.py ├── tests.py ├── urls.py └── views.py └── utils ├── __init__.py └── load_user_define.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | *.bak 4 | *.db 5 | 6 | local_settings.py 7 | env 8 | avatar 9 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | 2012-07-18 2 | 3 | * Version 0.1.2 4 | * Fix open file mode from 'w' to 'wb', For windows compatibility 5 | * Fix updating Social User's name and avatar. Thanks for Leejaen 6 | 7 | 8 | 2012-06-21 9 | 10 | * Version 0.1.0, First release 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) <2013>, 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 3. All advertising materials mentioning features or use of this software 12 | must display the following acknowledgement: 13 | This product includes software developed by the . 14 | 4. Neither the name of the nor the 15 | names of its contributors may be used to endorse or promote products 16 | derived from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY ''AS IS'' AND ANY 19 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 22 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include MANIFEST.in 3 | include ChangeLog 4 | include README.txt 5 | recursive-include siteuser/templates/siteuser * 6 | recursive-include siteuser/upload_avatar/static * 7 | recursive-include siteuser/users/static * 8 | recursive-include example * 9 | recursive-exclude example/avatar * 10 | recursive-exclude * *.pyc 11 | exclude example/test.db 12 | exclude example/example/local_settings.py 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django-siteuser 2 | 3 | 集成用户注册,登录,上传头像,第三方登录等功能。此项目的目标是当建立新站点的时候,不再重头写用户系统。 4 | 5 | 6 | ## 注意 7 | 8 | * 项目中的js依赖jQuery,所以请确保你的站点引入了jQuery 9 | * 只实现了第三方帐号登录,并没有实现一个本地帐号绑定多个社交帐号的功能。 10 | * siteuser并没有使用Django自带的User系统。 11 | 12 | 13 | ## 功能 14 | 15 | * 注册,登录 16 | * 登录用户修改密码 17 | * 忘记密码时重置密码 18 | * 上传头像 (带有剪裁预览功能) 19 | * 第三方帐号登录 20 | * 消息通知 21 | 22 | 23 | ## 如何使用 24 | 25 | #### 安装 26 | 27 | ```bash 28 | pip install django-siteuser 29 | ``` 30 | 31 | 同时会安装此项目的依赖: 32 | 33 | * [socialoauth](https://github.com/yueyoum/social-oauth) - 第三方登录 34 | * [django-celery](https://github.com/celery/django-celery) - 异步发送邮件 35 | 36 | 37 | 38 | #### 在你的模板中引入必要的js文件 39 | 40 | ```html 41 | 42 | 43 | ``` 44 | 45 | 46 | #### 设置settings.py文件 47 | 48 | * 首先设置 [django-celery](https://github.com/celery/django-celery) 49 | 50 | * 将 `siteuser` 加入到 `INSTALLED_APPS` 中 51 | ```python 52 | INSTALLED_APPS = ( 53 | # ... 54 | 'djcelery', 55 | 'siteuser.users', 56 | 'siteuser.upload_avatar', 57 | 'siteuser.notify', 58 | ) 59 | ``` 60 | 61 | `siteuser.users` 为 **必须添加** 的app,另外两个一个用于上传头像,一个是简单的通知系统,**可以不添加** 62 | 63 | 64 | * 将 `siteuser.SITEUSER_TEMPLATE` 加入 `TEMPLATE_DIRS` 65 | 66 | **注意**: 这里不是字符串,所以你需要在`settings.py` 文件中 先 `import siteuser` 67 | 68 | 69 | * 将 `'siteuser.context_processors.social_sites'` 加入 `TEMPLATE_CONTEXT_PROCESSORS` 70 | * 将 `'siteuser.middleware.User'` 加入 `MIDDLEWARE_CLASSES` 71 | * 将 `url(r'', include('siteuser.urls'))` 加入到项目的 `urls.py` 72 | * `AVATAR_DIR` - 告诉siteuser将上传的头像放置到哪个位置 73 | * `USING_SOCIAL_LOGIN` 是否开启第三方帐号登录功能。**若不设置,默认为 False** 74 | * `SOCIALOAUTH_SITES` - 仅在 `USING_SOCIAL_LOGIN`为True的情况下需要设置。第三方登录所需的配置。[见socialoauth文档](https://github.com/yueyoum/social-oauth/blob/master/doc.md#-settingspy) 75 | * `SITEUSER_EXTEND_MODEL` 76 | 不设置此项,example一样可以运行,但实际项目中,肯定会根据项目本身来设定用户字段. 77 | 默认的字段请查看 [SiteUser](/siteuser/users/models.py#L108). 78 | 79 | 支持两种方式来扩展SiteUser字段 80 | * 直接在`settings.py`中定义 81 | 82 | ```python 83 | # project settings.py 84 | from django.db import models 85 | class SITEUSER_EXTEND_MODEL(models.Model): 86 | # some fields... 87 | class Meta: 88 | abstract = True 89 | ``` 90 | 91 | * 将此model的定义写在其他文件中,然后在settings.py中指定。 92 | 93 | 94 | `example`使用的第二种,具体可以查看`example`项目. 95 | 96 | * `SITEUSER_ACCOUNT_MIXIN` 97 | siteuser 提供了登录,注册的template,但只是登录,注册form的模板, 98 | 并且siteuser不知道如何将这个form模板嵌入到你的站点中,以及不知道在渲染模板的时候还要传入什么额外的context, 99 | 所以你需要在自己定义这个设置。 100 | 101 | 与 `SITEUSER_EXTEND_MODEL` 一样,同样有两种方法定义, 102 | * 直接在 `settings.py` 中定义 103 | 104 | ```python 105 | class SITEUSER_ACCOUNT_MIXIN(object): 106 | login_template = 'login.html' # 你项目的登录页面模板 107 | register_template = 'register.html' # 你项目的注册页面模板 108 | reset_passwd_template = 'reset_password.html' # 忘记密码的重置密码模板 109 | change_passwd_template = 'change_password.html' # 登录用户修改密码的模板 110 | reset_passwd_email_title = u'重置密码' # 重置密码发送电子邮件的标题 111 | reset_passwd_link_expired_in = 24 # 重置密码链接多少小时后失效 112 | 113 | def get_login_context(self, request): 114 | return {} 115 | 116 | def get_register_context(self, request): 117 | return {} 118 | ``` 119 | 120 | 这两个方法正如其名,request是django传递给view的request,你在这里返回需要传递到模板中的context即可 121 | 在这里查看默认的 [SiteUserMixIn](/siteuser/users/views.py#L73) 122 | 123 | * 第二中方法是将此Mixin定义在一个文件中,然后在settings.py中指定 124 | 125 | `example`使用的第二种,具体可以查看`example`项目. 126 | 127 | 128 | * `SITEUSER_EMAIL` 129 | 130 | siteuser自己写了发邮件的方法,所以也没使用Django自己的EMAI设置 131 | 所以你需要设置`SITEUSER_EMAIL`, 见 [local_settings.py.example](/example/example/local_settings.py.example) 132 | 133 | 其中需要说明的是 `display_from` 这个设置, 134 | 如果你没有自己架设SMTP SERVER,也没有购买发送邮件的服务, 135 | 而是直接在gmail, 163, sohu等email提供商那儿免费申请了个邮箱, 136 | 那么你的邮箱肯定是 name@gmail.com这种形式的。 137 | 这时候如果你想在别人的收件箱里显示 `abc@def.com` 138 | 那么就把 `display_from` 设置成此值。如果不设置,那么默认就是`from` 139 | 140 | 141 | #### 模板 142 | 143 | 你需要自己完成 login.html, register.html, account_settings.html 模板。(名字可以自己取,只要在代码中 144 | 等对应起来就行),你只需要干一件事情,就是在你的模板的 `include` 相应的siteuser模板即可。 145 | 146 | 比如 login.html 中在你定义的位置 `{% include 'siteuser/login.html' %}`, 147 | 148 | account_settings.html 中 `{% include 'siteuser/upload_avatar.html' %}` 149 | 150 | 具体可以参考`example`项目 151 | 152 | 做完上面的设置后, `python manage.py validate` 检测无误后,syncdb,runserver 就可以测试注册,登录,头像,第三方登录 153 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | Django-SiteUser 2 | 3 | A Django app for maintain accounts. 4 | 5 | * Register 6 | * Login 7 | * Third Accounts Login 8 | * Change Password 9 | * Reset Password 10 | * Upload Avatar 11 | * User Notification 12 | 13 | 14 | Author: Wang Chao 15 | 16 | Author_email: yueyoum@gmail.com 17 | 18 | Usage: 19 | 20 | See README.md & example 21 | 22 | github: https://github.com/yueyoum/django-siteuser -------------------------------------------------------------------------------- /example/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yueyoum/django-siteuser/cb97b011c135aa756c262f12b0cb0a127a6933f9/example/app/__init__.py -------------------------------------------------------------------------------- /example/app/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /example/app/siteuser_custom.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | class SiteUserExtend(models.Model): 4 | score = models.IntegerField(default=0) 5 | nimei = models.CharField(max_length=12) 6 | 7 | class Meta: 8 | abstract = True 9 | 10 | 11 | class AccountMixIn(object): 12 | login_template = 'login.html' 13 | register_template = 'register.html' 14 | reset_passwd_template = 'reset_password.html' 15 | change_passwd_template = 'change_password.html' 16 | notify_template = 'notify.html' 17 | -------------------------------------------------------------------------------- /example/app/static/bootstrap/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yueyoum/django-siteuser/cb97b011c135aa756c262f12b0cb0a127a6933f9/example/app/static/bootstrap/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /example/app/static/bootstrap/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yueyoum/django-siteuser/cb97b011c135aa756c262f12b0cb0a127a6933f9/example/app/static/bootstrap/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /example/app/static/bootstrap/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap.js by @fat & @mdo 3 | * Copyright 2012 Twitter, Inc. 4 | * http://www.apache.org/licenses/LICENSE-2.0.txt 5 | */ 6 | !function(e){"use strict";e(function(){e.support.transition=function(){var e=function(){var e=document.createElement("bootstrap"),t={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"},n;for(n in t)if(e.style[n]!==undefined)return t[n]}();return e&&{end:e}}()})}(window.jQuery),!function(e){"use strict";var t='[data-dismiss="alert"]',n=function(n){e(n).on("click",t,this.close)};n.prototype.close=function(t){function s(){i.trigger("closed").remove()}var n=e(this),r=n.attr("data-target"),i;r||(r=n.attr("href"),r=r&&r.replace(/.*(?=#[^\s]*$)/,"")),i=e(r),t&&t.preventDefault(),i.length||(i=n.hasClass("alert")?n:n.parent()),i.trigger(t=e.Event("close"));if(t.isDefaultPrevented())return;i.removeClass("in"),e.support.transition&&i.hasClass("fade")?i.on(e.support.transition.end,s):s()};var r=e.fn.alert;e.fn.alert=function(t){return this.each(function(){var r=e(this),i=r.data("alert");i||r.data("alert",i=new n(this)),typeof t=="string"&&i[t].call(r)})},e.fn.alert.Constructor=n,e.fn.alert.noConflict=function(){return e.fn.alert=r,this},e(document).on("click.alert.data-api",t,n.prototype.close)}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.button.defaults,n)};t.prototype.setState=function(e){var t="disabled",n=this.$element,r=n.data(),i=n.is("input")?"val":"html";e+="Text",r.resetText||n.data("resetText",n[i]()),n[i](r[e]||this.options[e]),setTimeout(function(){e=="loadingText"?n.addClass(t).attr(t,t):n.removeClass(t).removeAttr(t)},0)},t.prototype.toggle=function(){var e=this.$element.closest('[data-toggle="buttons-radio"]');e&&e.find(".active").removeClass("active"),this.$element.toggleClass("active")};var n=e.fn.button;e.fn.button=function(n){return this.each(function(){var r=e(this),i=r.data("button"),s=typeof n=="object"&&n;i||r.data("button",i=new t(this,s)),n=="toggle"?i.toggle():n&&i.setState(n)})},e.fn.button.defaults={loadingText:"loading..."},e.fn.button.Constructor=t,e.fn.button.noConflict=function(){return e.fn.button=n,this},e(document).on("click.button.data-api","[data-toggle^=button]",function(t){var n=e(t.target);n.hasClass("btn")||(n=n.closest(".btn")),n.button("toggle")})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.$indicators=this.$element.find(".carousel-indicators"),this.options=n,this.options.pause=="hover"&&this.$element.on("mouseenter",e.proxy(this.pause,this)).on("mouseleave",e.proxy(this.cycle,this))};t.prototype={cycle:function(t){return t||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(e.proxy(this.next,this),this.options.interval)),this},getActiveIndex:function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},to:function(t){var n=this.getActiveIndex(),r=this;if(t>this.$items.length-1||t<0)return;return this.sliding?this.$element.one("slid",function(){r.to(t)}):n==t?this.pause().cycle():this.slide(t>n?"next":"prev",e(this.$items[t]))},pause:function(t){return t||(this.paused=!0),this.$element.find(".next, .prev").length&&e.support.transition.end&&(this.$element.trigger(e.support.transition.end),this.cycle(!0)),clearInterval(this.interval),this.interval=null,this},next:function(){if(this.sliding)return;return this.slide("next")},prev:function(){if(this.sliding)return;return this.slide("prev")},slide:function(t,n){var r=this.$element.find(".item.active"),i=n||r[t](),s=this.interval,o=t=="next"?"left":"right",u=t=="next"?"first":"last",a=this,f;this.sliding=!0,s&&this.pause(),i=i.length?i:this.$element.find(".item")[u](),f=e.Event("slide",{relatedTarget:i[0],direction:o});if(i.hasClass("active"))return;this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid",function(){var t=e(a.$indicators.children()[a.getActiveIndex()]);t&&t.addClass("active")}));if(e.support.transition&&this.$element.hasClass("slide")){this.$element.trigger(f);if(f.isDefaultPrevented())return;i.addClass(t),i[0].offsetWidth,r.addClass(o),i.addClass(o),this.$element.one(e.support.transition.end,function(){i.removeClass([t,o].join(" ")).addClass("active"),r.removeClass(["active",o].join(" ")),a.sliding=!1,setTimeout(function(){a.$element.trigger("slid")},0)})}else{this.$element.trigger(f);if(f.isDefaultPrevented())return;r.removeClass("active"),i.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return s&&this.cycle(),this}};var n=e.fn.carousel;e.fn.carousel=function(n){return this.each(function(){var r=e(this),i=r.data("carousel"),s=e.extend({},e.fn.carousel.defaults,typeof n=="object"&&n),o=typeof n=="string"?n:s.slide;i||r.data("carousel",i=new t(this,s)),typeof n=="number"?i.to(n):o?i[o]():s.interval&&i.pause().cycle()})},e.fn.carousel.defaults={interval:5e3,pause:"hover"},e.fn.carousel.Constructor=t,e.fn.carousel.noConflict=function(){return e.fn.carousel=n,this},e(document).on("click.carousel.data-api","[data-slide], [data-slide-to]",function(t){var n=e(this),r,i=e(n.attr("data-target")||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,"")),s=e.extend({},i.data(),n.data()),o;i.carousel(s),(o=n.attr("data-slide-to"))&&i.data("carousel").pause().to(o).cycle(),t.preventDefault()})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.collapse.defaults,n),this.options.parent&&(this.$parent=e(this.options.parent)),this.options.toggle&&this.toggle()};t.prototype={constructor:t,dimension:function(){var e=this.$element.hasClass("width");return e?"width":"height"},show:function(){var t,n,r,i;if(this.transitioning||this.$element.hasClass("in"))return;t=this.dimension(),n=e.camelCase(["scroll",t].join("-")),r=this.$parent&&this.$parent.find("> .accordion-group > .in");if(r&&r.length){i=r.data("collapse");if(i&&i.transitioning)return;r.collapse("hide"),i||r.data("collapse",null)}this.$element[t](0),this.transition("addClass",e.Event("show"),"shown"),e.support.transition&&this.$element[t](this.$element[0][n])},hide:function(){var t;if(this.transitioning||!this.$element.hasClass("in"))return;t=this.dimension(),this.reset(this.$element[t]()),this.transition("removeClass",e.Event("hide"),"hidden"),this.$element[t](0)},reset:function(e){var t=this.dimension();return this.$element.removeClass("collapse")[t](e||"auto")[0].offsetWidth,this.$element[e!==null?"addClass":"removeClass"]("collapse"),this},transition:function(t,n,r){var i=this,s=function(){n.type=="show"&&i.reset(),i.transitioning=0,i.$element.trigger(r)};this.$element.trigger(n);if(n.isDefaultPrevented())return;this.transitioning=1,this.$element[t]("in"),e.support.transition&&this.$element.hasClass("collapse")?this.$element.one(e.support.transition.end,s):s()},toggle:function(){this[this.$element.hasClass("in")?"hide":"show"]()}};var n=e.fn.collapse;e.fn.collapse=function(n){return this.each(function(){var r=e(this),i=r.data("collapse"),s=e.extend({},e.fn.collapse.defaults,r.data(),typeof n=="object"&&n);i||r.data("collapse",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.collapse.defaults={toggle:!0},e.fn.collapse.Constructor=t,e.fn.collapse.noConflict=function(){return e.fn.collapse=n,this},e(document).on("click.collapse.data-api","[data-toggle=collapse]",function(t){var n=e(this),r,i=n.attr("data-target")||t.preventDefault()||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,""),s=e(i).data("collapse")?"toggle":n.data();n[e(i).hasClass("in")?"addClass":"removeClass"]("collapsed"),e(i).collapse(s)})}(window.jQuery),!function(e){"use strict";function r(){e(t).each(function(){i(e(this)).removeClass("open")})}function i(t){var n=t.attr("data-target"),r;n||(n=t.attr("href"),n=n&&/#/.test(n)&&n.replace(/.*(?=#[^\s]*$)/,"")),r=n&&e(n);if(!r||!r.length)r=t.parent();return r}var t="[data-toggle=dropdown]",n=function(t){var n=e(t).on("click.dropdown.data-api",this.toggle);e("html").on("click.dropdown.data-api",function(){n.parent().removeClass("open")})};n.prototype={constructor:n,toggle:function(t){var n=e(this),s,o;if(n.is(".disabled, :disabled"))return;return s=i(n),o=s.hasClass("open"),r(),o||s.toggleClass("open"),n.focus(),!1},keydown:function(n){var r,s,o,u,a,f;if(!/(38|40|27)/.test(n.keyCode))return;r=e(this),n.preventDefault(),n.stopPropagation();if(r.is(".disabled, :disabled"))return;u=i(r),a=u.hasClass("open");if(!a||a&&n.keyCode==27)return n.which==27&&u.find(t).focus(),r.click();s=e("[role=menu] li:not(.divider):visible a",u);if(!s.length)return;f=s.index(s.filter(":focus")),n.keyCode==38&&f>0&&f--,n.keyCode==40&&f').appendTo(document.body),this.$backdrop.click(this.options.backdrop=="static"?e.proxy(this.$element[0].focus,this.$element[0]):e.proxy(this.hide,this)),i&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in");if(!t)return;i?this.$backdrop.one(e.support.transition.end,t):t()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),e.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(e.support.transition.end,t):t()):t&&t()}};var n=e.fn.modal;e.fn.modal=function(n){return this.each(function(){var r=e(this),i=r.data("modal"),s=e.extend({},e.fn.modal.defaults,r.data(),typeof n=="object"&&n);i||r.data("modal",i=new t(this,s)),typeof n=="string"?i[n]():s.show&&i.show()})},e.fn.modal.defaults={backdrop:!0,keyboard:!0,show:!0},e.fn.modal.Constructor=t,e.fn.modal.noConflict=function(){return e.fn.modal=n,this},e(document).on("click.modal.data-api",'[data-toggle="modal"]',function(t){var n=e(this),r=n.attr("href"),i=e(n.attr("data-target")||r&&r.replace(/.*(?=#[^\s]+$)/,"")),s=i.data("modal")?"toggle":e.extend({remote:!/#/.test(r)&&r},i.data(),n.data());t.preventDefault(),i.modal(s).one("hide",function(){n.focus()})})}(window.jQuery),!function(e){"use strict";var t=function(e,t){this.init("tooltip",e,t)};t.prototype={constructor:t,init:function(t,n,r){var i,s,o,u,a;this.type=t,this.$element=e(n),this.options=this.getOptions(r),this.enabled=!0,o=this.options.trigger.split(" ");for(a=o.length;a--;)u=o[a],u=="click"?this.$element.on("click."+this.type,this.options.selector,e.proxy(this.toggle,this)):u!="manual"&&(i=u=="hover"?"mouseenter":"focus",s=u=="hover"?"mouseleave":"blur",this.$element.on(i+"."+this.type,this.options.selector,e.proxy(this.enter,this)),this.$element.on(s+"."+this.type,this.options.selector,e.proxy(this.leave,this)));this.options.selector?this._options=e.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},getOptions:function(t){return t=e.extend({},e.fn[this.type].defaults,this.$element.data(),t),t.delay&&typeof t.delay=="number"&&(t.delay={show:t.delay,hide:t.delay}),t},enter:function(t){var n=e.fn[this.type].defaults,r={},i;this._options&&e.each(this._options,function(e,t){n[e]!=t&&(r[e]=t)},this),i=e(t.currentTarget)[this.type](r).data(this.type);if(!i.options.delay||!i.options.delay.show)return i.show();clearTimeout(this.timeout),i.hoverState="in",this.timeout=setTimeout(function(){i.hoverState=="in"&&i.show()},i.options.delay.show)},leave:function(t){var n=e(t.currentTarget)[this.type](this._options).data(this.type);this.timeout&&clearTimeout(this.timeout);if(!n.options.delay||!n.options.delay.hide)return n.hide();n.hoverState="out",this.timeout=setTimeout(function(){n.hoverState=="out"&&n.hide()},n.options.delay.hide)},show:function(){var t,n,r,i,s,o,u=e.Event("show");if(this.hasContent()&&this.enabled){this.$element.trigger(u);if(u.isDefaultPrevented())return;t=this.tip(),this.setContent(),this.options.animation&&t.addClass("fade"),s=typeof this.options.placement=="function"?this.options.placement.call(this,t[0],this.$element[0]):this.options.placement,t.detach().css({top:0,left:0,display:"block"}),this.options.container?t.appendTo(this.options.container):t.insertAfter(this.$element),n=this.getPosition(),r=t[0].offsetWidth,i=t[0].offsetHeight;switch(s){case"bottom":o={top:n.top+n.height,left:n.left+n.width/2-r/2};break;case"top":o={top:n.top-i,left:n.left+n.width/2-r/2};break;case"left":o={top:n.top+n.height/2-i/2,left:n.left-r};break;case"right":o={top:n.top+n.height/2-i/2,left:n.left+n.width}}this.applyPlacement(o,s),this.$element.trigger("shown")}},applyPlacement:function(e,t){var n=this.tip(),r=n[0].offsetWidth,i=n[0].offsetHeight,s,o,u,a;n.offset(e).addClass(t).addClass("in"),s=n[0].offsetWidth,o=n[0].offsetHeight,t=="top"&&o!=i&&(e.top=e.top+i-o,a=!0),t=="bottom"||t=="top"?(u=0,e.left<0&&(u=e.left*-2,e.left=0,n.offset(e),s=n[0].offsetWidth,o=n[0].offsetHeight),this.replaceArrow(u-r+s,s,"left")):this.replaceArrow(o-i,o,"top"),a&&n.offset(e)},replaceArrow:function(e,t,n){this.arrow().css(n,e?50*(1-e/t)+"%":"")},setContent:function(){var e=this.tip(),t=this.getTitle();e.find(".tooltip-inner")[this.options.html?"html":"text"](t),e.removeClass("fade in top bottom left right")},hide:function(){function i(){var t=setTimeout(function(){n.off(e.support.transition.end).detach()},500);n.one(e.support.transition.end,function(){clearTimeout(t),n.detach()})}var t=this,n=this.tip(),r=e.Event("hide");this.$element.trigger(r);if(r.isDefaultPrevented())return;return n.removeClass("in"),e.support.transition&&this.$tip.hasClass("fade")?i():n.detach(),this.$element.trigger("hidden"),this},fixTitle:function(){var e=this.$element;(e.attr("title")||typeof e.attr("data-original-title")!="string")&&e.attr("data-original-title",e.attr("title")||"").attr("title","")},hasContent:function(){return this.getTitle()},getPosition:function(){var t=this.$element[0];return e.extend({},typeof t.getBoundingClientRect=="function"?t.getBoundingClientRect():{width:t.offsetWidth,height:t.offsetHeight},this.$element.offset())},getTitle:function(){var e,t=this.$element,n=this.options;return e=t.attr("data-original-title")||(typeof n.title=="function"?n.title.call(t[0]):n.title),e},tip:function(){return this.$tip=this.$tip||e(this.options.template)},arrow:function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},validate:function(){this.$element[0].parentNode||(this.hide(),this.$element=null,this.options=null)},enable:function(){this.enabled=!0},disable:function(){this.enabled=!1},toggleEnabled:function(){this.enabled=!this.enabled},toggle:function(t){var n=t?e(t.currentTarget)[this.type](this._options).data(this.type):this;n.tip().hasClass("in")?n.hide():n.show()},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}};var n=e.fn.tooltip;e.fn.tooltip=function(n){return this.each(function(){var r=e(this),i=r.data("tooltip"),s=typeof n=="object"&&n;i||r.data("tooltip",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.tooltip.Constructor=t,e.fn.tooltip.defaults={animation:!0,placement:"top",selector:!1,template:'
',trigger:"hover focus",title:"",delay:0,html:!1,container:!1},e.fn.tooltip.noConflict=function(){return e.fn.tooltip=n,this}}(window.jQuery),!function(e){"use strict";var t=function(e,t){this.init("popover",e,t)};t.prototype=e.extend({},e.fn.tooltip.Constructor.prototype,{constructor:t,setContent:function(){var e=this.tip(),t=this.getTitle(),n=this.getContent();e.find(".popover-title")[this.options.html?"html":"text"](t),e.find(".popover-content")[this.options.html?"html":"text"](n),e.removeClass("fade top bottom left right in")},hasContent:function(){return this.getTitle()||this.getContent()},getContent:function(){var e,t=this.$element,n=this.options;return e=(typeof n.content=="function"?n.content.call(t[0]):n.content)||t.attr("data-content"),e},tip:function(){return this.$tip||(this.$tip=e(this.options.template)),this.$tip},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}});var n=e.fn.popover;e.fn.popover=function(n){return this.each(function(){var r=e(this),i=r.data("popover"),s=typeof n=="object"&&n;i||r.data("popover",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.popover.Constructor=t,e.fn.popover.defaults=e.extend({},e.fn.tooltip.defaults,{placement:"right",trigger:"click",content:"",template:'

'}),e.fn.popover.noConflict=function(){return e.fn.popover=n,this}}(window.jQuery),!function(e){"use strict";function t(t,n){var r=e.proxy(this.process,this),i=e(t).is("body")?e(window):e(t),s;this.options=e.extend({},e.fn.scrollspy.defaults,n),this.$scrollElement=i.on("scroll.scroll-spy.data-api",r),this.selector=(this.options.target||(s=e(t).attr("href"))&&s.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.$body=e("body"),this.refresh(),this.process()}t.prototype={constructor:t,refresh:function(){var t=this,n;this.offsets=e([]),this.targets=e([]),n=this.$body.find(this.selector).map(function(){var n=e(this),r=n.data("target")||n.attr("href"),i=/^#\w/.test(r)&&e(r);return i&&i.length&&[[i.position().top+(!e.isWindow(t.$scrollElement.get(0))&&t.$scrollElement.scrollTop()),r]]||null}).sort(function(e,t){return e[0]-t[0]}).each(function(){t.offsets.push(this[0]),t.targets.push(this[1])})},process:function(){var e=this.$scrollElement.scrollTop()+this.options.offset,t=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,n=t-this.$scrollElement.height(),r=this.offsets,i=this.targets,s=this.activeTarget,o;if(e>=n)return s!=(o=i.last()[0])&&this.activate(o);for(o=r.length;o--;)s!=i[o]&&e>=r[o]&&(!r[o+1]||e<=r[o+1])&&this.activate(i[o])},activate:function(t){var n,r;this.activeTarget=t,e(this.selector).parent(".active").removeClass("active"),r=this.selector+'[data-target="'+t+'"],'+this.selector+'[href="'+t+'"]',n=e(r).parent("li").addClass("active"),n.parent(".dropdown-menu").length&&(n=n.closest("li.dropdown").addClass("active")),n.trigger("activate")}};var n=e.fn.scrollspy;e.fn.scrollspy=function(n){return this.each(function(){var r=e(this),i=r.data("scrollspy"),s=typeof n=="object"&&n;i||r.data("scrollspy",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.scrollspy.Constructor=t,e.fn.scrollspy.defaults={offset:10},e.fn.scrollspy.noConflict=function(){return e.fn.scrollspy=n,this},e(window).on("load",function(){e('[data-spy="scroll"]').each(function(){var t=e(this);t.scrollspy(t.data())})})}(window.jQuery),!function(e){"use strict";var t=function(t){this.element=e(t)};t.prototype={constructor:t,show:function(){var t=this.element,n=t.closest("ul:not(.dropdown-menu)"),r=t.attr("data-target"),i,s,o;r||(r=t.attr("href"),r=r&&r.replace(/.*(?=#[^\s]*$)/,""));if(t.parent("li").hasClass("active"))return;i=n.find(".active:last a")[0],o=e.Event("show",{relatedTarget:i}),t.trigger(o);if(o.isDefaultPrevented())return;s=e(r),this.activate(t.parent("li"),n),this.activate(s,s.parent(),function(){t.trigger({type:"shown",relatedTarget:i})})},activate:function(t,n,r){function o(){i.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),t.addClass("active"),s?(t[0].offsetWidth,t.addClass("in")):t.removeClass("fade"),t.parent(".dropdown-menu")&&t.closest("li.dropdown").addClass("active"),r&&r()}var i=n.find("> .active"),s=r&&e.support.transition&&i.hasClass("fade");s?i.one(e.support.transition.end,o):o(),i.removeClass("in")}};var n=e.fn.tab;e.fn.tab=function(n){return this.each(function(){var r=e(this),i=r.data("tab");i||r.data("tab",i=new t(this)),typeof n=="string"&&i[n]()})},e.fn.tab.Constructor=t,e.fn.tab.noConflict=function(){return e.fn.tab=n,this},e(document).on("click.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(t){t.preventDefault(),e(this).tab("show")})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.typeahead.defaults,n),this.matcher=this.options.matcher||this.matcher,this.sorter=this.options.sorter||this.sorter,this.highlighter=this.options.highlighter||this.highlighter,this.updater=this.options.updater||this.updater,this.source=this.options.source,this.$menu=e(this.options.menu),this.shown=!1,this.listen()};t.prototype={constructor:t,select:function(){var e=this.$menu.find(".active").attr("data-value");return this.$element.val(this.updater(e)).change(),this.hide()},updater:function(e){return e},show:function(){var t=e.extend({},this.$element.position(),{height:this.$element[0].offsetHeight});return this.$menu.insertAfter(this.$element).css({top:t.top+t.height,left:t.left}).show(),this.shown=!0,this},hide:function(){return this.$menu.hide(),this.shown=!1,this},lookup:function(t){var n;return this.query=this.$element.val(),!this.query||this.query.length"+t+""})},render:function(t){var n=this;return t=e(t).map(function(t,r){return t=e(n.options.item).attr("data-value",r),t.find("a").html(n.highlighter(r)),t[0]}),t.first().addClass("active"),this.$menu.html(t),this},next:function(t){var n=this.$menu.find(".active").removeClass("active"),r=n.next();r.length||(r=e(this.$menu.find("li")[0])),r.addClass("active")},prev:function(e){var t=this.$menu.find(".active").removeClass("active"),n=t.prev();n.length||(n=this.$menu.find("li").last()),n.addClass("active")},listen:function(){this.$element.on("focus",e.proxy(this.focus,this)).on("blur",e.proxy(this.blur,this)).on("keypress",e.proxy(this.keypress,this)).on("keyup",e.proxy(this.keyup,this)),this.eventSupported("keydown")&&this.$element.on("keydown",e.proxy(this.keydown,this)),this.$menu.on("click",e.proxy(this.click,this)).on("mouseenter","li",e.proxy(this.mouseenter,this)).on("mouseleave","li",e.proxy(this.mouseleave,this))},eventSupported:function(e){var t=e in this.$element;return t||(this.$element.setAttribute(e,"return;"),t=typeof this.$element[e]=="function"),t},move:function(e){if(!this.shown)return;switch(e.keyCode){case 9:case 13:case 27:e.preventDefault();break;case 38:e.preventDefault(),this.prev();break;case 40:e.preventDefault(),this.next()}e.stopPropagation()},keydown:function(t){this.suppressKeyPressRepeat=~e.inArray(t.keyCode,[40,38,9,13,27]),this.move(t)},keypress:function(e){if(this.suppressKeyPressRepeat)return;this.move(e)},keyup:function(e){switch(e.keyCode){case 40:case 38:case 16:case 17:case 18:break;case 9:case 13:if(!this.shown)return;this.select();break;case 27:if(!this.shown)return;this.hide();break;default:this.lookup()}e.stopPropagation(),e.preventDefault()},focus:function(e){this.focused=!0},blur:function(e){this.focused=!1,!this.mousedover&&this.shown&&this.hide()},click:function(e){e.stopPropagation(),e.preventDefault(),this.select(),this.$element.focus()},mouseenter:function(t){this.mousedover=!0,this.$menu.find(".active").removeClass("active"),e(t.currentTarget).addClass("active")},mouseleave:function(e){this.mousedover=!1,!this.focused&&this.shown&&this.hide()}};var n=e.fn.typeahead;e.fn.typeahead=function(n){return this.each(function(){var r=e(this),i=r.data("typeahead"),s=typeof n=="object"&&n;i||r.data("typeahead",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.typeahead.defaults={source:[],items:8,menu:'',item:'
  • ',minLength:1},e.fn.typeahead.Constructor=t,e.fn.typeahead.noConflict=function(){return e.fn.typeahead=n,this},e(document).on("focus.typeahead.data-api",'[data-provide="typeahead"]',function(t){var n=e(this);if(n.data("typeahead"))return;n.typeahead(n.data())})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.options=e.extend({},e.fn.affix.defaults,n),this.$window=e(window).on("scroll.affix.data-api",e.proxy(this.checkPosition,this)).on("click.affix.data-api",e.proxy(function(){setTimeout(e.proxy(this.checkPosition,this),1)},this)),this.$element=e(t),this.checkPosition()};t.prototype.checkPosition=function(){if(!this.$element.is(":visible"))return;var t=e(document).height(),n=this.$window.scrollTop(),r=this.$element.offset(),i=this.options.offset,s=i.bottom,o=i.top,u="affix affix-top affix-bottom",a;typeof i!="object"&&(s=o=i),typeof o=="function"&&(o=i.top()),typeof s=="function"&&(s=i.bottom()),a=this.unpin!=null&&n+this.unpin<=r.top?!1:s!=null&&r.top+this.$element.height()>=t-s?"bottom":o!=null&&n<=o?"top":!1;if(this.affixed===a)return;this.affixed=a,this.unpin=a=="bottom"?r.top-n:null,this.$element.removeClass(u).addClass("affix"+(a?"-"+a:""))};var n=e.fn.affix;e.fn.affix=function(n){return this.each(function(){var r=e(this),i=r.data("affix"),s=typeof n=="object"&&n;i||r.data("affix",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.affix.Constructor=t,e.fn.affix.defaults={offset:0},e.fn.affix.noConflict=function(){return e.fn.affix=n,this},e(window).on("load",function(){e('[data-spy="affix"]').each(function(){var t=e(this),n=t.data();n.offset=n.offset||{},n.offsetBottom&&(n.offset.bottom=n.offsetBottom),n.offsetTop&&(n.offset.top=n.offsetTop),t.affix(n)})})}(window.jQuery); -------------------------------------------------------------------------------- /example/app/templates/account_settings.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 首页 5 | 修改密码 6 | {% include "siteuser/upload_avatar.html" %} 7 | {% endblock %} -------------------------------------------------------------------------------- /example/app/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Django Siteuser 6 | 7 | 8 | 9 | 10 | 11 | {% block css %} 12 | {% endblock %} 13 | 14 | 15 | {% block js %} 16 | {% endblock %} 17 | 18 | 19 | 20 |
    21 | {% block content %} 22 | {% endblock %} 23 |
    24 | 25 | 26 | -------------------------------------------------------------------------------- /example/app/templates/change_password.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | {% include "siteuser/change_password.html" %} 5 | {% endblock %} -------------------------------------------------------------------------------- /example/app/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | {% if users %} 5 | 6 | 7 | 8 | 9 | {% for u in users %} 10 | 11 | 12 | 19 | 20 | 29 | 36 | 37 | {% endfor %} 38 |
    uid第三方帐号名字头像当前登录用户
    {{ u.id }} 13 | {% if u.social %} 14 | {{ u.social }} 15 | {% else %} 16 | 17 | {% endif %} 18 | {{ u.username }} 21 |
    22 | {% if u.avatar %} 23 | 24 | {% else %} 25 | 26 | {% endif %} 27 |
    28 |
    30 | {% if u.current %} 31 | 32 | {% else %} 33 | 34 | {% endif %} 35 |
    39 | {% else %} 40 |

    No users

    41 | {% endif %} 42 | 43 |
    44 | {% if request.siteuser %} 45 | 通知 46 | 47 |
    48 | {% if not request.siteuser.is_social %} 49 | Settings 50 | {% endif %} 51 | Logout 52 | {% else %} 53 | Login 54 | Register 55 | {% endif %} 56 |
    57 | {% endblock %} -------------------------------------------------------------------------------- /example/app/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | {% include "siteuser/login.html" %} 5 | {% endblock %} -------------------------------------------------------------------------------- /example/app/templates/notify.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | {% include "siteuser/notify.html" %} 5 | {% endblock %} -------------------------------------------------------------------------------- /example/app/templates/register.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | {% include "siteuser/register.html" %} 5 | {% endblock %} -------------------------------------------------------------------------------- /example/app/templates/reset_password.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | {% include "siteuser/reset_password.html" %} 5 | {% endblock %} -------------------------------------------------------------------------------- /example/app/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file demonstrates writing tests using the unittest module. These will pass 3 | when you run "manage.py test". 4 | 5 | Replace this with more appropriate tests for your application. 6 | """ 7 | 8 | from django.test import TestCase 9 | 10 | 11 | class SimpleTest(TestCase): 12 | def test_basic_addition(self): 13 | """ 14 | Tests that 1 + 1 always equals 2. 15 | """ 16 | self.assertEqual(1 + 1, 2) 17 | -------------------------------------------------------------------------------- /example/app/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.conf.urls import patterns, url 4 | 5 | from . import views 6 | 7 | urlpatterns = patterns('', 8 | url(r'^$', views.home, name="home"), 9 | url(r'^account/settings/?$', views.account_settings, name="account_settings"), 10 | ) -------------------------------------------------------------------------------- /example/app/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.conf import settings 4 | from django.http import HttpResponse 5 | from django.shortcuts import render_to_response 6 | from django.template import RequestContext 7 | 8 | from siteuser.users.models import SiteUser 9 | from siteuser import settings as siteuser_settings 10 | 11 | if siteuser_settings.USING_SOCIAL_LOGIN: 12 | from socialoauth import SocialSites 13 | 14 | def home(request): 15 | if siteuser_settings.USING_SOCIAL_LOGIN: 16 | socialsites = SocialSites(settings.SOCIALOAUTH_SITES) 17 | 18 | def _make_user_info(u): 19 | info = {} 20 | info['id'] = u.id 21 | info['social'] = u.is_social 22 | 23 | if siteuser_settings.USING_SOCIAL_LOGIN and info['social']: 24 | info['social'] = socialsites.get_site_object_by_name(u.social_user.site_name).site_name_zh 25 | 26 | info['username'] = u.username 27 | info['avatar'] = u.avatar 28 | info['current'] = request.siteuser and request.siteuser.id == u.id 29 | return info 30 | 31 | all_users = SiteUser.objects.all() 32 | users = map(_make_user_info, all_users) 33 | 34 | return render_to_response( 35 | 'home.html', 36 | { 37 | 'users': users, 38 | }, 39 | context_instance = RequestContext(request) 40 | ) 41 | 42 | def account_settings(request): 43 | return render_to_response( 44 | 'account_settings.html', context_instance=RequestContext(request) 45 | ) 46 | 47 | def _test(request, *args, **kwargs): 48 | return HttpResponse("here") -------------------------------------------------------------------------------- /example/example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yueyoum/django-siteuser/cb97b011c135aa756c262f12b0cb0a127a6933f9/example/example/__init__.py -------------------------------------------------------------------------------- /example/example/local_settings.py.example: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | SITEUSER_EMAIL = { 4 | 'smtp_host': 'smtp.gmail.com', 5 | 'smtp_port': 25, 6 | 'username': 'xxx', 7 | 'password': 'xxx', 8 | 'from': 'xxx@gmail.com', 9 | 'display_from': '', 10 | } 11 | 12 | SOCIALOAUTH_SITES = ( 13 | ('renren', 'socialoauth.sites.renren.RenRen', '人人', 14 | { 15 | 'redirect_uri': 'http://test.codeshift.org/account/oauth/renren', 16 | 'client_id': 'YOUR ID', 17 | 'client_secret': 'YOUR SECRET', 18 | } 19 | ), 20 | 21 | ('weibo', 'socialoauth.sites.weibo.Weibo', '新浪微博', 22 | { 23 | 'redirect_uri': 'http://test.codeshift.org/account/oauth/weibo', 24 | 'client_id': 'YOUR ID', 25 | 'client_secret': 'YOUR SECRET', 26 | } 27 | ), 28 | 29 | ('qq', 'socialoauth.sites.qq.QQ', 'QQ', 30 | { 31 | 'redirect_uri': 'http://test.codeshift.org/account/oauth/qq', 32 | 'client_id': 'YOUR ID', 33 | 'client_secret': 'YOUR SECRET', 34 | } 35 | ), 36 | 37 | ('douban', 'socialoauth.sites.douban.DouBan', '豆瓣', 38 | { 39 | 'redirect_uri': 'http://test.codeshift.org/account/oauth/douban', 40 | 'client_id': 'YOUR ID', 41 | 'client_secret': 'YOUR SECRET', 42 | 'scope': ['douban_basic_common'] 43 | } 44 | ), 45 | ) 46 | -------------------------------------------------------------------------------- /example/example/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for example project. 2 | import os 3 | CURRENT_PATH = os.path.dirname(os.path.realpath(__file__)) 4 | EXAMPLE_PATH = os.path.dirname(CURRENT_PATH) 5 | PROJECT_PATH = os.path.dirname(EXAMPLE_PATH) 6 | 7 | import djcelery 8 | djcelery.setup_loader() 9 | 10 | try: 11 | import siteuser 12 | except ImportError: 13 | import sys 14 | sys.path.append(PROJECT_PATH) 15 | import siteuser 16 | 17 | DEBUG = True 18 | TEMPLATE_DEBUG = DEBUG 19 | 20 | ADMINS = ( 21 | # ('Your Name', 'your_email@example.com'), 22 | ) 23 | 24 | MANAGERS = ADMINS 25 | 26 | DATABASES = { 27 | 'default': { 28 | 'ENGINE': 'django.db.backends.mysql', 29 | #'NAME': os.path.join(EXAMPLE_PATH, 'test.db'), 30 | 'NAME': 'siteuser', 31 | # The following settings are not used with sqlite3: 32 | 'USER': 'root', 33 | 'PASSWORD': '', 34 | 'HOST': '127.0.0.1', 35 | 'PORT': '3306', 36 | } 37 | } 38 | 39 | # Hosts/domain names that are valid for this site; required if DEBUG is False 40 | # See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts 41 | ALLOWED_HOSTS = [] 42 | 43 | # Local time zone for this installation. Choices can be found here: 44 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 45 | # although not all choices may be available on all operating systems. 46 | # In a Windows environment this must be set to your system time zone. 47 | TIME_ZONE = 'Asia/Shanghai' 48 | 49 | # Language code for this installation. All choices can be found here: 50 | # http://www.i18nguy.com/unicode/language-identifiers.html 51 | LANGUAGE_CODE = 'en-us' 52 | 53 | SITE_ID = 1 54 | 55 | USE_I18N = False 56 | USE_L10N = False 57 | USE_TZ = True 58 | 59 | # Absolute filesystem path to the directory that will hold user-uploaded files. 60 | # Example: "/var/www/example.com/media/" 61 | MEDIA_ROOT = '' 62 | 63 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 64 | # trailing slash. 65 | # Examples: "http://example.com/media/", "http://media.example.com/" 66 | MEDIA_URL = '' 67 | 68 | # Absolute path to the directory static files should be collected to. 69 | # Don't put anything in this directory yourself; store your static files 70 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 71 | # Example: "/var/www/example.com/static/" 72 | STATIC_ROOT = '' 73 | 74 | # URL prefix for static files. 75 | # Example: "http://example.com/static/", "http://static.example.com/" 76 | STATIC_URL = '/static/' 77 | 78 | # Additional locations of static files 79 | STATICFILES_DIRS = ( 80 | # Put strings here, like "/home/html/static" or "C:/www/django/static". 81 | # Always use forward slashes, even on Windows. 82 | # Don't forget to use absolute paths, not relative paths. 83 | ) 84 | 85 | # List of finder classes that know how to find static files in 86 | # various locations. 87 | STATICFILES_FINDERS = ( 88 | 'django.contrib.staticfiles.finders.FileSystemFinder', 89 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 90 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 91 | ) 92 | 93 | # Make this unique, and don't share it with anybody. 94 | SECRET_KEY = 'ye01zdn5rdgbi(#s^krsd#$oqc_7azv9l!a@&=eb3pwwy7m6u*' 95 | 96 | # List of callables that know how to import templates from various sources. 97 | TEMPLATE_LOADERS = ( 98 | 'django.template.loaders.filesystem.Loader', 99 | 'django.template.loaders.app_directories.Loader', 100 | # 'django.template.loaders.eggs.Loader', 101 | ) 102 | 103 | TEMPLATE_CONTEXT_PROCESSORS = ( 104 | #'django.contrib.auth.context_processors.auth', 105 | 'django.core.context_processors.debug', 106 | #'django.core.context_processors.i18n', 107 | 'django.core.context_processors.media', 108 | 'django.core.context_processors.static', 109 | 'django.core.context_processors.tz', 110 | 'django.core.context_processors.request', 111 | #'django.contrib.messages.context_processors.messages', 112 | 'siteuser.context_processors.social_sites', 113 | ) 114 | 115 | 116 | MIDDLEWARE_CLASSES = ( 117 | 'django.middleware.common.CommonMiddleware', 118 | 'django.contrib.sessions.middleware.SessionMiddleware', 119 | 'django.middleware.csrf.CsrfViewMiddleware', 120 | #'django.contrib.auth.middleware.AuthenticationMiddleware', 121 | #'django.contrib.messages.middleware.MessageMiddleware', 122 | # Uncomment the next line for simple clickjacking protection: 123 | # 'django.middleware.clickjacking.XFrameOptionsMiddleware', 124 | 'siteuser.middleware.User', 125 | ) 126 | 127 | ROOT_URLCONF = 'example.urls' 128 | 129 | # Python dotted path to the WSGI application used by Django's runserver. 130 | WSGI_APPLICATION = 'example.wsgi.application' 131 | 132 | TEMPLATE_DIRS = ( 133 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". 134 | # Always use forward slashes, even on Windows. 135 | # Don't forget to use absolute paths, not relative paths. 136 | siteuser.SITEUSER_TEMPLATE, 137 | ) 138 | 139 | INSTALLED_APPS = ( 140 | #'django.contrib.auth', 141 | #'django.contrib.contenttypes', 142 | 'django.contrib.sessions', 143 | #'django.contrib.sites', 144 | #'django.contrib.messages', 145 | #'django.contrib.staticfiles', 146 | # Uncomment the next line to enable the admin: 147 | # 'django.contrib.admin', 148 | # Uncomment the next line to enable admin documentation: 149 | # 'django.contrib.admindocs', 150 | 'djcelery', 151 | 'app', 152 | 'siteuser.users', 153 | 'siteuser.upload_avatar', 154 | 'siteuser.notify', 155 | ) 156 | 157 | # A sample logging configuration. The only tangible logging 158 | # performed by this configuration is to send an email to 159 | # the site admins on every HTTP 500 error when DEBUG=False. 160 | # See http://docs.djangoproject.com/en/dev/topics/logging for 161 | # more details on how to customize your logging configuration. 162 | LOGGING = { 163 | 'version': 1, 164 | 'disable_existing_loggers': False, 165 | 'filters': { 166 | 'require_debug_false': { 167 | '()': 'django.utils.log.RequireDebugFalse' 168 | } 169 | }, 170 | 'handlers': { 171 | 'mail_admins': { 172 | 'level': 'ERROR', 173 | 'filters': ['require_debug_false'], 174 | 'class': 'django.utils.log.AdminEmailHandler' 175 | } 176 | }, 177 | 'loggers': { 178 | 'django.request': { 179 | 'handlers': ['mail_admins'], 180 | 'level': 'ERROR', 181 | 'propagate': True, 182 | }, 183 | } 184 | } 185 | 186 | BROKER_URL = 'redis://localhost:6379/0' 187 | CELERY_RESULT_BACKEND = 'redis://localhost:6379/0' 188 | 189 | USING_SOCIAL_LOGIN = False 190 | AVATAR_DIR = os.path.join(EXAMPLE_PATH, 'app/static/avatar') 191 | 192 | SITEUSER_ACCOUNT_MIXIN = 'app.siteuser_custom.AccountMixIn' 193 | SITEUSER_EXTEND_MODEL = 'app.siteuser_custom.SiteUserExtend' 194 | 195 | USER_LINK = lambda uid: '/user/{0}'.format(uid) 196 | 197 | try: 198 | from local_settings import * 199 | except ImportError: 200 | pass 201 | 202 | SITEUSER_EMAIL = { 203 | 'smtp_host': 'smtp.gmail.com', 204 | 'smtp_port': 25, 205 | 'username': 'xxx', 206 | 'password': 'xxx', 207 | 'from': 'xxx@gmail.com', 208 | 'display_from': '', 209 | } -------------------------------------------------------------------------------- /example/example/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include, url 2 | 3 | 4 | urlpatterns = patterns('', 5 | url(r'', include('siteuser.urls')), 6 | url(r'', include('app.urls')), 7 | ) 8 | 9 | 10 | from django.contrib.staticfiles.urls import staticfiles_urlpatterns 11 | urlpatterns += staticfiles_urlpatterns() 12 | 13 | # for test 14 | from app.views import _test 15 | urlpatterns += patterns('', 16 | url(r'^.+/?$', _test), 17 | ) -------------------------------------------------------------------------------- /example/example/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for example project. 3 | 4 | This module contains the WSGI application used by Django's development server 5 | and any production WSGI deployments. It should expose a module-level variable 6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 7 | this application via the ``WSGI_APPLICATION`` setting. 8 | 9 | Usually you will have the standard Django WSGI application here, but it also 10 | might make sense to replace the whole Django WSGI application with a custom one 11 | that later delegates to the Django one. For example, you could introduce WSGI 12 | middleware here, or combine a Django application with an application of another 13 | framework. 14 | 15 | """ 16 | import os 17 | 18 | # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks 19 | # if running multiple sites in the same mod_wsgi process. To fix this, use 20 | # mod_wsgi daemon mode with each site in its own daemon process, or use 21 | # os.environ["DJANGO_SETTINGS_MODULE"] = "example.settings" 22 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") 23 | 24 | # This application object is used by any WSGI server configured to use this 25 | # file. This includes Django's development server, if the WSGI_APPLICATION 26 | # setting points here. 27 | from django.core.wsgi import get_wsgi_application 28 | application = get_wsgi_application() 29 | 30 | # Apply WSGI middleware here. 31 | # from helloworld.wsgi import HelloWorldApplication 32 | # application = HelloWorldApplication(application) 33 | -------------------------------------------------------------------------------- /example/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | socialoauth 2 | redis 3 | django-celery 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | from siteuser import VERSION 4 | 5 | install_requires = [ 6 | 'socialoauth', 7 | 'django-celery', 8 | ] 9 | 10 | setup( 11 | name='django-siteuser', 12 | version = VERSION, 13 | license = 'BSD', 14 | description = 'A Django APP for Universal Maintain Accounts', 15 | long_description = open('README.txt').read(), 16 | author = 'Wang Chao', 17 | author_email = 'yueyoum@gmail.com', 18 | url = 'https://github.com/yueyoum/django-siteuser', 19 | keywords = 'django, account, login, register, social, avatar', 20 | packages = find_packages(exclude=('example',)), 21 | install_requires = install_requires, 22 | include_package_data = True, 23 | classifiers = [ 24 | 'Development Status :: 4 - Beta', 25 | 'Topic :: Internet', 26 | 'License :: OSI Approved :: BSD License', 27 | 'Operating System :: OS Independent', 28 | 'Programming Language :: Python', 29 | ], 30 | ) 31 | -------------------------------------------------------------------------------- /siteuser/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | 5 | version_info = (0, 1, 2) 6 | VERSION = __version__ = '.'.join( map(str, version_info) ) 7 | 8 | CURRENT_PATH = os.path.dirname(os.path.realpath(__file__)) 9 | SITEUSER_TEMPLATE = os.path.join(CURRENT_PATH, 'templates') -------------------------------------------------------------------------------- /siteuser/context_processors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from siteuser.utils import LazyList 4 | from siteuser.settings import USING_SOCIAL_LOGIN, SOCIALOAUTH_SITES 5 | 6 | if USING_SOCIAL_LOGIN: 7 | from socialoauth import SocialSites 8 | 9 | # add 'siteuser.context_processors.social_sites' in TEMPLATE_CONTEXT_PROCESSORS 10 | # then in template, you can get this sites via {% for s in social_sites %} ... {% endfor %} 11 | # Don't worry about the performance, 12 | # `social_sites` is a lazy object, it readly called just access the `social_sites` 13 | 14 | 15 | def social_sites(request): 16 | def _social_sites(): 17 | def make_site(site_class): 18 | s = socialsites.get_site_object_by_class(site_class) 19 | return { 20 | 'site_name': s.site_name, 21 | 'site_name_zh': s.site_name_zh, 22 | 'authorize_url': s.authorize_url, 23 | } 24 | socialsites = SocialSites(SOCIALOAUTH_SITES) 25 | return [make_site(site_class) for site_class in socialsites.list_sites_class()] 26 | 27 | if SOCIALOAUTH_SITES: 28 | return {'social_sites': LazyList(_social_sites)} 29 | return {'social_sites': []} 30 | -------------------------------------------------------------------------------- /siteuser/decorators.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from functools import wraps 3 | 4 | from django.http import HttpResponseRedirect 5 | from django.http import Http404 6 | 7 | def login_needed(login_url=None): 8 | def deco(func): 9 | @wraps(func) 10 | def wrap(request, *args, **kwargs): 11 | if not request.siteuser: 12 | # No login 13 | if login_url: 14 | return HttpResponseRedirect(login_url) 15 | raise Http404 16 | 17 | return func(request, *args, **kwargs) 18 | return wrap 19 | return deco 20 | -------------------------------------------------------------------------------- /siteuser/functional/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.conf import settings 4 | from siteuser.functional.mail import send_mail 5 | 6 | SITEUSER_EMAIL = settings.SITEUSER_EMAIL 7 | 8 | def send_html_mail(to, subject, content): 9 | send_mail( 10 | SITEUSER_EMAIL['smtp_host'], 11 | SITEUSER_EMAIL['smtp_port'], 12 | SITEUSER_EMAIL['username'], 13 | SITEUSER_EMAIL['password'], 14 | SITEUSER_EMAIL['from'], 15 | to, 16 | subject, 17 | content, 18 | 'html', 19 | SITEUSER_EMAIL['display_from'] 20 | ) 21 | -------------------------------------------------------------------------------- /siteuser/functional/mail.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import smtplib 3 | from email.mime.text import MIMEText 4 | 5 | 6 | def send_mail(host, port, username, password, mail_from, mail_to, mail_subject, mail_content, mail_type, display_from=None): 7 | if isinstance(mail_content, unicode): 8 | mail_content = mail_content.encode('utf-8') 9 | content = MIMEText(mail_content, mail_type, 'utf-8') 10 | content['From'] = display_from or mail_from 11 | if isinstance(mail_to, (list, tuple)): 12 | content['To'] = ', '.join(mail_to) 13 | else: 14 | content['To'] = mail_to 15 | content['Subject'] = mail_subject 16 | 17 | s = smtplib.SMTP() 18 | s.connect(host, port) 19 | s.login(username, password) 20 | s.sendmail(mail_from, mail_to, content.as_string()) 21 | s.quit() 22 | -------------------------------------------------------------------------------- /siteuser/middleware.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.utils.functional import SimpleLazyObject 4 | 5 | from siteuser.users.models import SiteUser 6 | 7 | # add 'siteuser.middleware.User' in MIDDLEWARE_CLASSES 8 | # then the request object will has a `siteuser` property 9 | # 10 | # you can using it like this: 11 | # if request.siteuser: 12 | # # there has a logged user, 13 | # uid = request.siteuser.id 14 | # else: 15 | # # no one is logged 16 | # 17 | # Don't worry about the performance, 18 | # `siteuser` is a lazy object, it readly called just access the `request.siteuser` 19 | 20 | 21 | 22 | class User(object): 23 | def process_request(self, request): 24 | def get_user(): 25 | uid = request.session.get('uid', None) 26 | if not uid: 27 | return None 28 | 29 | try: 30 | user = SiteUser.objects.get(id=int(uid)) 31 | except SiteUser.DoesNotExist: 32 | return None 33 | 34 | if not user.is_active: 35 | user = None 36 | return user 37 | 38 | request.siteuser = SimpleLazyObject(get_user) 39 | -------------------------------------------------------------------------------- /siteuser/notify/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yueyoum/django-siteuser/cb97b011c135aa756c262f12b0cb0a127a6933f9/siteuser/notify/__init__.py -------------------------------------------------------------------------------- /siteuser/notify/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.db import models 4 | from django.utils import timezone 5 | 6 | 7 | class Notify(models.Model): 8 | """ 9 | link - 此通知链接到的页面 10 | text - 此通知的文字,也就是展示出来的内容 11 | """ 12 | user = models.ForeignKey('users.SiteUser', related_name='notifies') 13 | sender = models.ForeignKey('users.SiteUser') 14 | link = models.CharField(max_length=255) 15 | text = models.CharField(max_length=255) 16 | notify_at = models.DateTimeField() 17 | has_read = models.BooleanField(default=False) 18 | 19 | def __unicode__(self): 20 | return u'' % self.id 21 | 22 | @classmethod 23 | def create(cls, **kwargs): 24 | kwargs['notify_at'] = timezone.now() 25 | cls.objects.create(**kwargs) -------------------------------------------------------------------------------- /siteuser/notify/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file demonstrates writing tests using the unittest module. These will pass 3 | when you run "manage.py test". 4 | 5 | Replace this with more appropriate tests for your application. 6 | """ 7 | 8 | from django.test import TestCase 9 | 10 | 11 | class SimpleTest(TestCase): 12 | def test_basic_addition(self): 13 | """ 14 | Tests that 1 + 1 always equals 2. 15 | """ 16 | self.assertEqual(1 + 1, 2) 17 | -------------------------------------------------------------------------------- /siteuser/notify/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.conf.urls import patterns, url 4 | from siteuser.notify import views 5 | 6 | urlpatterns = patterns('', 7 | # ajax 获取通知 8 | url(r'^notifies.json/$', views.notifies_json), 9 | # 普通页面浏览获取通知 10 | url(r'^notifies/$', views.get_notifies, name="siteuser_nofities"), 11 | 12 | # 点击一个通知 13 | url(r'^notify/confirm/(?P\d+)/$', views.notify_confirm, name='siteuser_notify_confirm') 14 | ) -------------------------------------------------------------------------------- /siteuser/notify/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | 5 | from django.conf import settings 6 | from django.http import HttpResponse, HttpResponseRedirect, Http404 7 | from django.shortcuts import render_to_response 8 | from django.core.urlresolvers import reverse 9 | from django.core.exceptions import ImproperlyConfigured 10 | from django.template import RequestContext 11 | 12 | from siteuser.notify.models import Notify 13 | from siteuser.utils import load_user_define 14 | 15 | """ 16 | 这只是一个简单的通知系统,产生的通知格式如下: 17 | [谁]在[哪个条目/帖子]中回复了你 18 | 因为在生成的通知里有 [谁] 这个用户链接,所以用户必须自己在settings.py中定义 USER_LINK 19 | 这个方法,它接受一个参数:用户id,然后返回用户个人页面的url 20 | 21 | 有两种方式获取通知: 22 | 1. GET /notifies.json/ 返回的是未读的通知,只要用js将返回的html组织在合适dom元素中即可 23 | 2. GET /notifies/ 用一个页面来展示全部的通知。包括已经处理过的通知 24 | 25 | 所以就必须设置 SITEUSER_ACCOUNT_MIXIN, 在其中指定 notify_template 26 | 27 | 点击一个未读的通知: 28 | GET /notify/confirm// 如果正确,就会跳转到相应的页面 29 | """ 30 | 31 | 32 | user_define = load_user_define.user_defined_mixin()() 33 | notify_template = getattr(user_define, 'notify_template', None) 34 | if not notify_template: 35 | raise ImproperlyConfigured('SITEUSER_ACCOUNT_MIXIN has no attribute "notify_template"') 36 | 37 | get_notify_context = getattr(user_define, 'get_notify_context', None) 38 | if not get_notify_context: 39 | get_notify_context = lambda x: {} 40 | 41 | def notifies_json(request): 42 | """由Ajax获取的未读通知""" 43 | user = request.siteuser 44 | if not user: 45 | return HttpResponse(json.dumps([]), mimetype='application/json') 46 | 47 | notifies = Notify.objects.filter(user=user, has_read=False).select_related('sender').order_by('-notify_at') 48 | def _make_html(n): 49 | return u'{1}{3} 中回复了你'.format( 50 | settings.USER_LINK(n.sender.id), 51 | n.sender.username, 52 | reverse('siteuser_notify_confirm', kwargs={'notify_id': n.id}), 53 | n.text, 54 | ) 55 | html = [_make_html(n) for n in notifies] 56 | return HttpResponse(json.dumps(html), mimetype='application/json') 57 | 58 | 59 | def get_notifies(request): 60 | """页面展示全部通知""" 61 | user = request.siteuser 62 | if not user: 63 | return HttpResponseRedirect(reverse('siteuser_login')) 64 | 65 | notifies = Notify.objects.filter(user=user).select_related('sender').order_by('-notify_at') 66 | # TODO 分页 67 | ctx = get_notify_context(request) 68 | ctx['notifies'] = notifies 69 | return render_to_response( 70 | notify_template, 71 | ctx, 72 | context_instance=RequestContext(request) 73 | ) 74 | 75 | 76 | def notify_confirm(request, notify_id): 77 | """点击通知上的链接,将此通知设置为has_read=True,然后转至此通知的link""" 78 | try: 79 | n = Notify.objects.get(id=notify_id) 80 | except Notify.DoesNotExist: 81 | raise Http404 82 | 83 | if not n.has_read: 84 | n.has_read = True 85 | n.save() 86 | 87 | return HttpResponseRedirect(n.link) -------------------------------------------------------------------------------- /siteuser/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | from django.conf import settings 4 | 5 | # 默认不打开第三方登录 6 | USING_SOCIAL_LOGIN = getattr(settings, 'USING_SOCIAL_LOGIN', False) 7 | 8 | if USING_SOCIAL_LOGIN: 9 | ### 第三方帐号登录 - 配置见soicaloauth文档和例子 10 | SOCIALOAUTH_SITES = settings.SOCIALOAUTH_SITES 11 | else: 12 | SOCIALOAUTH_SITES = None 13 | 14 | ### 头像目录 - 需要在项目的settings.py中设置 15 | AVATAR_DIR = settings.AVATAR_DIR 16 | 17 | # 上传的原始图片目录, 默认和头像目录相同 18 | AVATAR_UPLOAD_DIR = getattr(settings, 'AVATAR_UPLOAD_DIR', AVATAR_DIR) 19 | 20 | # 默认头像的文件名,需要将其放入AVATAR_DIR 头像目录 21 | DEFAULT_AVATAR = getattr(settings, 'DEFAULT_AVATAR', 'default_avatar.png') 22 | 23 | if not os.path.isdir(AVATAR_DIR): 24 | os.mkdir(AVATAR_DIR) 25 | if not os.path.isdir(AVATAR_UPLOAD_DIR): 26 | os.mkdir(AVATAR_UPLOAD_DIR) 27 | 28 | # 头像url的前缀 29 | AVATAR_URL_PREFIX = getattr(settings, 'AVATAR_URL_PREFIX', '/static/avatar/') 30 | 31 | # 原始上传的图片url前缀,用于在裁剪选择区域显示原始图片 32 | AVATAR_UPLOAD_URL_PREFIX = getattr(settings, 'AVATAR_UPLOAD_URL_PREFIX', AVATAR_URL_PREFIX) 33 | 34 | # 最大可上传图片大小 MB 35 | AVATAR_UPLOAD_MAX_SIZE = getattr(settings, 'AVATAR_UPLOAD_MAX_SIZE', 5) 36 | 37 | # 剪裁后的大小 px 38 | AVATAR_RESIZE_SIZE = getattr(settings, 'AVATAR_RESIZE_SIZE', 50) 39 | 40 | # 头像处理完毕后保存的格式和质量, 格式还可以是 jpep, gif 41 | AVATAR_SAVE_FORMAT = getattr(settings, 'AVATAR_SAVE_FORMAT', 'png') 42 | AVATAR_SAVE_QUALITY = getattr(settings, 'AVATAR_SAVE_QUALITY', 90) 43 | 44 | 45 | # 注册用户的电子邮件最大长度 46 | MAX_EMAIL_LENGTH = getattr(settings, 'MAX_EMAIL_LENGTH', 128) 47 | 48 | # 注册用户的用户名最大长度 49 | MAX_USERNAME_LENGTH = getattr(settings, 'MAX_USERNAME_LENGTH', 12) 50 | 51 | 52 | # 第三方帐号授权成功后跳转的URL 53 | SOCIAL_LOGIN_DONE_REDIRECT_URL = getattr(settings, 'SOCIAL_LOGIN_DONE_REDIRECT_URL', '/') 54 | # 授权失败后跳转的URL 55 | SOCIAL_LOGIN_ERROR_REDIRECT_URL = getattr(settings, 'SOCIAL_LOGIN_ERROR_REDIRECT_URL', '/') -------------------------------------------------------------------------------- /siteuser/templates/siteuser/base.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |
    5 | {% block siteuserform %}{% endblock %} 6 |
    7 |
    8 | {% if social_sites %} 9 |
    10 |
    11 | {% for s in social_sites %} 12 |

    {{ s.site_name_zh }}登录

    13 | {% endfor %} 14 |
    15 |
    16 | {% endif %} 17 |
    18 |
    -------------------------------------------------------------------------------- /siteuser/templates/siteuser/change_password.html: -------------------------------------------------------------------------------- 1 |
    2 | {% if done %} 3 |

    修改密码成功,现在就 登录

    4 | {% else %} 5 |
    {% csrf_token %} 6 |
    7 | 8 | 9 | 10 | 11 |
    12 | 13 |
    14 | {% if error_msg %} 15 |
    {{ error_msg }}
    16 | {% endif %} 17 | {% endif %} 18 |
    -------------------------------------------------------------------------------- /siteuser/templates/siteuser/login.html: -------------------------------------------------------------------------------- 1 | {% extends "siteuser/base.html" %} 2 | 3 | {% block siteuserform %} 4 |
    {% csrf_token %} 5 |
    6 | 7 | 8 | 9 | 10 |
    11 | 12 |
    13 | 14 |

    忘记密码?

    15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /siteuser/templates/siteuser/notify.html: -------------------------------------------------------------------------------- 1 |
    2 | {% if not notifies %} 3 |

    还没有通知

    4 | {% else %} 5 | 6 | {% for n in notifies %} 7 | 8 | 9 | 14 | 15 | 16 | {% endfor %} 17 |
    {{ n.sender.username }} 10 | 11 | {{ n.text }} 12 | 中回复了你 13 | {{ n.notify_at }}
    18 | {% endif %} 19 |
    -------------------------------------------------------------------------------- /siteuser/templates/siteuser/register.html: -------------------------------------------------------------------------------- 1 | {% extends "siteuser/base.html" %} 2 | 3 | {% block siteuserform %} 4 |
    {% csrf_token %} 5 |
    6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
    15 | 16 |
    17 | 19 | {% endblock %} -------------------------------------------------------------------------------- /siteuser/templates/siteuser/reset_password.html: -------------------------------------------------------------------------------- 1 |
    2 | {% if step1 %} 3 |
    {% csrf_token %} 4 |
    5 | 6 | 7 |
    8 | 9 |
    10 | {% if error_msg %} 11 |
    {{ error_msg }}
    12 | {% endif %} 13 | {% endif %} 14 | 15 | {% if step1_done %} 16 |

    请您查收电子邮件,并打开邮件中的链接进行重置密码的操作

    17 |

    如果在收件箱中没找到邮件,请在垃圾邮件中查找

    18 |

    如果没有收到邮件,请点击 这里,重新填写电子邮件并发送

    19 | {% endif %} 20 | 21 | {% if step2 %} 22 | {% if expired %} 23 |

    此重置链接已过期,请 重新申请 得到新的链接

    24 | {% else %} 25 |

    请重新设置密码

    26 |
    {% csrf_token %} 27 |
    28 | 29 | 30 | 31 | 32 |
    33 | 34 |
    35 | {% if error_msg %} 36 |
    {{ error_msg }}
    37 | {% endif %} 38 | {% endif %} 39 | {% endif %} 40 | 41 | {% if step2_done %} 42 |

    已完成密码重置,立即登录

    43 | {% endif %} 44 |
    -------------------------------------------------------------------------------- /siteuser/templates/siteuser/reset_password_email.html: -------------------------------------------------------------------------------- 1 |

    重置密码

    2 |

    亲爱的用户,我们收到了您要重置密码的请求,请点击下面的链接完成重置操作。此链接在{{ hour }}小时内有效。

    3 | {{ link }} 4 |
    5 |

    如果无法点击链接,请复制地址,然后用浏览器打开

    6 |
    7 |

    8 | 注意: 9 | 如果你没有提交过重置申请,或者不想重置,忽略此邮件即可。 10 |

    -------------------------------------------------------------------------------- /siteuser/templates/siteuser/upload_avatar.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | 6 | 13 | 14 |
    15 |
    16 | {% csrf_token %} 17 | 18 | 19 |
    20 |
    21 | 22 | 23 |
    24 |
    25 |
    26 |

    预览

    27 |
    28 |
    29 |
    30 |
    31 | 32 |
    33 | 34 |
    35 | 36 | 37 |
    38 | -------------------------------------------------------------------------------- /siteuser/upload_avatar/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | -------------------------------------------------------------------------------- /siteuser/upload_avatar/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | 4 | from django.db import models 5 | from django.db.models.signals import post_delete 6 | 7 | 8 | from siteuser.settings import ( 9 | AVATAR_DIR, 10 | ) 11 | 12 | 13 | class UploadedImage(models.Model): 14 | uid = models.IntegerField(unique=True) 15 | image = models.CharField(max_length=128) 16 | 17 | def get_image_path(self): 18 | path = os.path.join(AVATAR_DIR, self.image) 19 | if not os.path.exists(path): 20 | return None 21 | return path 22 | 23 | def delete_image(self): 24 | path = self.get_image_path() 25 | if path: 26 | try: 27 | os.unlink(path) 28 | except OSError: 29 | pass 30 | 31 | 32 | def _delete_avatar_on_disk(sender, instance, *args, **kwargs): 33 | instance.delete_image() 34 | 35 | 36 | post_delete.connect(_delete_avatar_on_disk, sender=UploadedImage) 37 | -------------------------------------------------------------------------------- /siteuser/upload_avatar/signals.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.dispatch import Signal 4 | 5 | avatar_upload_done = Signal(providing_args=['uid', 'avatar_name']) 6 | avatar_crop_done = Signal(providing_args=['uid', 'avatar_name']) -------------------------------------------------------------------------------- /siteuser/upload_avatar/static/imgareaselect/css/border-anim-h.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yueyoum/django-siteuser/cb97b011c135aa756c262f12b0cb0a127a6933f9/siteuser/upload_avatar/static/imgareaselect/css/border-anim-h.gif -------------------------------------------------------------------------------- /siteuser/upload_avatar/static/imgareaselect/css/border-anim-v.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yueyoum/django-siteuser/cb97b011c135aa756c262f12b0cb0a127a6933f9/siteuser/upload_avatar/static/imgareaselect/css/border-anim-v.gif -------------------------------------------------------------------------------- /siteuser/upload_avatar/static/imgareaselect/css/border-h.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yueyoum/django-siteuser/cb97b011c135aa756c262f12b0cb0a127a6933f9/siteuser/upload_avatar/static/imgareaselect/css/border-h.gif -------------------------------------------------------------------------------- /siteuser/upload_avatar/static/imgareaselect/css/border-v.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yueyoum/django-siteuser/cb97b011c135aa756c262f12b0cb0a127a6933f9/siteuser/upload_avatar/static/imgareaselect/css/border-v.gif -------------------------------------------------------------------------------- /siteuser/upload_avatar/static/imgareaselect/css/imgareaselect-default.css: -------------------------------------------------------------------------------- 1 | /* 2 | * imgAreaSelect default style 3 | */ 4 | 5 | .imgareaselect-border1 { 6 | background: url(border-v.gif) repeat-y left top; 7 | } 8 | 9 | .imgareaselect-border2 { 10 | background: url(border-h.gif) repeat-x left top; 11 | } 12 | 13 | .imgareaselect-border3 { 14 | background: url(border-v.gif) repeat-y right top; 15 | } 16 | 17 | .imgareaselect-border4 { 18 | background: url(border-h.gif) repeat-x left bottom; 19 | } 20 | 21 | .imgareaselect-border1, .imgareaselect-border2, 22 | .imgareaselect-border3, .imgareaselect-border4 { 23 | filter: alpha(opacity=50); 24 | opacity: 0.5; 25 | } 26 | 27 | .imgareaselect-handle { 28 | background-color: #fff; 29 | border: solid 1px #000; 30 | filter: alpha(opacity=50); 31 | opacity: 0.5; 32 | } 33 | 34 | .imgareaselect-outer { 35 | background-color: #000; 36 | filter: alpha(opacity=50); 37 | opacity: 0.5; 38 | } 39 | 40 | .imgareaselect-selection { 41 | } -------------------------------------------------------------------------------- /siteuser/upload_avatar/static/imgareaselect/jquery.imgareaselect.min.js: -------------------------------------------------------------------------------- 1 | (function($){var abs=Math.abs,max=Math.max,min=Math.min,round=Math.round;function div(){return $('
    ')}$.imgAreaSelect=function(img,options){var $img=$(img),imgLoaded,$box=div(),$area=div(),$border=div().add(div()).add(div()).add(div()),$outer=div().add(div()).add(div()).add(div()),$handles=$([]),$areaOpera,left,top,imgOfs={left:0,top:0},imgWidth,imgHeight,$parent,parOfs={left:0,top:0},zIndex=0,position='absolute',startX,startY,scaleX,scaleY,resize,minWidth,minHeight,maxWidth,maxHeight,aspectRatio,shown,x1,y1,x2,y2,selection={x1:0,y1:0,x2:0,y2:0,width:0,height:0},docElem=document.documentElement,ua=navigator.userAgent,$p,d,i,o,w,h,adjusted;function viewX(x){return x+imgOfs.left-parOfs.left}function viewY(y){return y+imgOfs.top-parOfs.top}function selX(x){return x-imgOfs.left+parOfs.left}function selY(y){return y-imgOfs.top+parOfs.top}function evX(event){return event.pageX-parOfs.left}function evY(event){return event.pageY-parOfs.top}function getSelection(noScale){var sx=noScale||scaleX,sy=noScale||scaleY;return{x1:round(selection.x1*sx),y1:round(selection.y1*sy),x2:round(selection.x2*sx),y2:round(selection.y2*sy),width:round(selection.x2*sx)-round(selection.x1*sx),height:round(selection.y2*sy)-round(selection.y1*sy)}}function setSelection(x1,y1,x2,y2,noScale){var sx=noScale||scaleX,sy=noScale||scaleY;selection={x1:round(x1/sx||0),y1:round(y1/sy||0),x2:round(x2/sx||0),y2:round(y2/sy||0)};selection.width=selection.x2-selection.x1;selection.height=selection.y2-selection.y1}function adjust(){if(!imgLoaded||!$img.width())return;imgOfs={left:round($img.offset().left),top:round($img.offset().top)};imgWidth=$img.innerWidth();imgHeight=$img.innerHeight();imgOfs.top+=($img.outerHeight()-imgHeight)>>1;imgOfs.left+=($img.outerWidth()-imgWidth)>>1;minWidth=round(options.minWidth/scaleX)||0;minHeight=round(options.minHeight/scaleY)||0;maxWidth=round(min(options.maxWidth/scaleX||1<<24,imgWidth));maxHeight=round(min(options.maxHeight/scaleY||1<<24,imgHeight));if($().jquery=='1.3.2'&&position=='fixed'&&!docElem['getBoundingClientRect']){imgOfs.top+=max(document.body.scrollTop,docElem.scrollTop);imgOfs.left+=max(document.body.scrollLeft,docElem.scrollLeft)}parOfs=/absolute|relative/.test($parent.css('position'))?{left:round($parent.offset().left)-$parent.scrollLeft(),top:round($parent.offset().top)-$parent.scrollTop()}:position=='fixed'?{left:$(document).scrollLeft(),top:$(document).scrollTop()}:{left:0,top:0};left=viewX(0);top=viewY(0);if(selection.x2>imgWidth||selection.y2>imgHeight)doResize()}function update(resetKeyPress){if(!shown)return;$box.css({left:viewX(selection.x1),top:viewY(selection.y1)}).add($area).width(w=selection.width).height(h=selection.height);$area.add($border).add($handles).css({left:0,top:0});$border.width(max(w-$border.outerWidth()+$border.innerWidth(),0)).height(max(h-$border.outerHeight()+$border.innerHeight(),0));$($outer[0]).css({left:left,top:top,width:selection.x1,height:imgHeight});$($outer[1]).css({left:left+selection.x1,top:top,width:w,height:selection.y1});$($outer[2]).css({left:left+selection.x2,top:top,width:imgWidth-selection.x2,height:imgHeight});$($outer[3]).css({left:left+selection.x1,top:top+selection.y2,width:w,height:imgHeight-selection.y2});w-=$handles.outerWidth();h-=$handles.outerHeight();switch($handles.length){case 8:$($handles[4]).css({left:w>>1});$($handles[5]).css({left:w,top:h>>1});$($handles[6]).css({left:w>>1,top:h});$($handles[7]).css({top:h>>1});case 4:$handles.slice(1,3).css({left:w});$handles.slice(2,4).css({top:h})}if(resetKeyPress!==false){if($.imgAreaSelect.onKeyPress!=docKeyPress)$(document).unbind($.imgAreaSelect.keyPress,$.imgAreaSelect.onKeyPress);if(options.keys)$(document)[$.imgAreaSelect.keyPress]($.imgAreaSelect.onKeyPress=docKeyPress)}if(msie&&$border.outerWidth()-$border.innerWidth()==2){$border.css('margin',0);setTimeout(function(){$border.css('margin','auto')},0)}}function doUpdate(resetKeyPress){adjust();update(resetKeyPress);x1=viewX(selection.x1);y1=viewY(selection.y1);x2=viewX(selection.x2);y2=viewY(selection.y2)}function hide($elem,fn){options.fadeSpeed?$elem.fadeOut(options.fadeSpeed,fn):$elem.hide()}function areaMouseMove(event){var x=selX(evX(event))-selection.x1,y=selY(evY(event))-selection.y1;if(!adjusted){adjust();adjusted=true;$box.one('mouseout',function(){adjusted=false})}resize='';if(options.resizable){if(y<=options.resizeMargin)resize='n';else if(y>=selection.height-options.resizeMargin)resize='s';if(x<=options.resizeMargin)resize+='w';else if(x>=selection.width-options.resizeMargin)resize+='e'}$box.css('cursor',resize?resize+'-resize':options.movable?'move':'');if($areaOpera)$areaOpera.toggle()}function docMouseUp(event){$('body').css('cursor','');if(options.autoHide||selection.width*selection.height==0)hide($box.add($outer),function(){$(this).hide()});$(document).unbind('mousemove',selectingMouseMove);$box.mousemove(areaMouseMove);options.onSelectEnd(img,getSelection())}function areaMouseDown(event){if(event.which!=1)return false;adjust();if(resize){$('body').css('cursor',resize+'-resize');x1=viewX(selection[/w/.test(resize)?'x2':'x1']);y1=viewY(selection[/n/.test(resize)?'y2':'y1']);$(document).mousemove(selectingMouseMove).one('mouseup',docMouseUp);$box.unbind('mousemove',areaMouseMove)}else if(options.movable){startX=left+selection.x1-evX(event);startY=top+selection.y1-evY(event);$box.unbind('mousemove',areaMouseMove);$(document).mousemove(movingMouseMove).one('mouseup',function(){options.onSelectEnd(img,getSelection());$(document).unbind('mousemove',movingMouseMove);$box.mousemove(areaMouseMove)})}else $img.mousedown(event);return false}function fixAspectRatio(xFirst){if(aspectRatio)if(xFirst){x2=max(left,min(left+imgWidth,x1+abs(y2-y1)*aspectRatio*(x2>x1||-1)));y2=round(max(top,min(top+imgHeight,y1+abs(x2-x1)/aspectRatio*(y2>y1||-1))));x2=round(x2)}else{y2=max(top,min(top+imgHeight,y1+abs(x2-x1)/aspectRatio*(y2>y1||-1)));x2=round(max(left,min(left+imgWidth,x1+abs(y2-y1)*aspectRatio*(x2>x1||-1))));y2=round(y2)}}function doResize(){x1=min(x1,left+imgWidth);y1=min(y1,top+imgHeight);if(abs(x2-x1)left+imgWidth)x1=left+imgWidth-minWidth}if(abs(y2-y1)top+imgHeight)y1=top+imgHeight-minHeight}x2=max(left,min(x2,left+imgWidth));y2=max(top,min(y2,top+imgHeight));fixAspectRatio(abs(x2-x1)maxWidth){x2=x1-maxWidth*(x2maxHeight){y2=y1-maxHeight*(y2=0)$handles.width(5).height(5);if(o=options.borderWidth)$handles.css({borderWidth:o,borderStyle:'solid'});styleOptions($handles,{borderColor1:'border-color',borderColor2:'background-color',borderOpacity:'opacity'})}scaleX=options.imageWidth/imgWidth||1;scaleY=options.imageHeight/imgHeight||1;if(newOptions.x1!=null){setSelection(newOptions.x1,newOptions.y1,newOptions.x2,newOptions.y2);newOptions.show=!newOptions.hide}if(newOptions.keys)options.keys=$.extend({shift:1,ctrl:'resize'},newOptions.keys);$outer.addClass(options.classPrefix+'-outer');$area.addClass(options.classPrefix+'-selection');for(i=0;i++<4;)$($border[i-1]).addClass(options.classPrefix+'-border'+i);styleOptions($area,{selectionColor:'background-color',selectionOpacity:'opacity'});styleOptions($border,{borderOpacity:'opacity',borderWidth:'border-width'});styleOptions($outer,{outerColor:'background-color',outerOpacity:'opacity'});if(o=options.borderColor1)$($border[0]).css({borderStyle:'solid',borderColor:o});if(o=options.borderColor2)$($border[1]).css({borderStyle:'dashed',borderColor:o});$box.append($area.add($border).add($areaOpera)).append($handles);if(msie){if(o=($outer.css('filter')||'').match(/opacity=(\d+)/))$outer.css('opacity',o[1]/100);if(o=($border.css('filter')||'').match(/opacity=(\d+)/))$border.css('opacity',o[1]/100)}if(newOptions.hide)hide($box.add($outer));else if(newOptions.show&&imgLoaded){shown=true;$box.add($outer).fadeIn(options.fadeSpeed||0);doUpdate()}aspectRatio=(d=(options.aspectRatio||'').split(/:/))[0]/d[1];$img.add($outer).unbind('mousedown',imgMouseDown);if(options.disable||options.enable===false){$box.unbind('mousemove',areaMouseMove).unbind('mousedown',areaMouseDown);$(window).unbind('resize',windowResize)}else{if(options.enable||options.disable===false){if(options.resizable||options.movable)$box.mousemove(areaMouseMove).mousedown(areaMouseDown);$(window).resize(windowResize)}if(!options.persistent)$img.add($outer).mousedown(imgMouseDown)}options.enable=options.disable=undefined}this.remove=function(){setOptions({disable:true});$box.add($outer).remove()};this.getOptions=function(){return options};this.setOptions=setOptions;this.getSelection=getSelection;this.setSelection=setSelection;this.cancelSelection=cancelSelection;this.update=doUpdate;var msie=(/msie ([\w.]+)/i.exec(ua)||[])[1],opera=/opera/i.test(ua),safari=/webkit/i.test(ua)&&!/chrome/i.test(ua);$p=$img;while($p.length){zIndex=max(zIndex,!isNaN($p.css('z-index'))?$p.css('z-index'):zIndex);if($p.css('position')=='fixed')position='fixed';$p=$p.parent(':not(body)')}zIndex=options.zIndex||zIndex;if(msie)$img.attr('unselectable','on');$.imgAreaSelect.keyPress=msie||safari?'keydown':'keypress';if(opera)$areaOpera=div().css({width:'100%',height:'100%',position:'absolute',zIndex:zIndex+2||2});$box.add($outer).css({visibility:'hidden',position:position,overflow:'hidden',zIndex:zIndex||'0'});$box.css({zIndex:zIndex+2||2});$area.add($border).css({position:'absolute',fontSize:0});img.complete||img.readyState=='complete'||!$img.is('img')?imgLoad():$img.one('load',imgLoad);if(!imgLoaded&&msie&&msie>=7)img.src=img.src};$.fn.imgAreaSelect=function(options){options=options||{};this.each(function(){if($(this).data('imgAreaSelect')){if(options.remove){$(this).data('imgAreaSelect').remove();$(this).removeData('imgAreaSelect')}else $(this).data('imgAreaSelect').setOptions(options)}else if(!options.remove){if(options.enable===undefined&&options.disable===undefined)options.enable=true;$(this).data('imgAreaSelect',new $.imgAreaSelect(this,options))}});if(options.instance)return $(this).data('imgAreaSelect');return this}})(jQuery); -------------------------------------------------------------------------------- /siteuser/upload_avatar/static/js/upload_avatar.js: -------------------------------------------------------------------------------- 1 | /*jslint browser: true*/ 2 | /*global $*/ 3 | "use strict"; 4 | 5 | $(function () { 6 | var img_obj, p, fileanme, x1, y1, x2, y2; 7 | 8 | $('#uploadAvatarInputFile').change(function () { 9 | if ($(this).val() === "") { return; } 10 | var $last_img = $('#uploadAvatarSelectArea img'); 11 | if ($last_img.length) { 12 | img_obj = $last_img.imgAreaSelect({instance: true}); 13 | img_obj.remove(); 14 | } 15 | $('#uploadAvatarSelectArea').empty(); 16 | $('#uploadAvatarPreviewArea div').empty(); 17 | p = new RegExp(/\.(jpg|jpeg|png|gif)$/); 18 | fileanme = $(this).val().toLowerCase().replace(/^\s+|\s+$/g, ''); 19 | if (!p.test(fileanme)) { window.alert("请选择图片上传"); return; } 20 | $('#uploadAvatarForm').submit(); 21 | $(this).val(''); 22 | $('#uploadAvatarCropSubmit').removeAttr('disabled'); 23 | }); 24 | 25 | $('#uploadAvatarCropSubmit').click(function () { 26 | $(this).attr('disabled', 'disabled'); 27 | x1 = $('#uploadAvatarValueX1').val(); 28 | y1 = $('#uploadAvatarValueY1').val(); 29 | x2 = $('#uploadAvatarValueX2').val(); 30 | y2 = $('#uploadAvatarValueY2').val(); 31 | if (x1 === "" || y1 === "" || x2 === "" || y2 === "") { 32 | $(this).removeAttr('disabled'); 33 | return false; 34 | } 35 | 36 | $('#uploadAvatarCropForm').submit(); 37 | $('#uploadAvatarValueX1').val(''); 38 | $('#uploadAvatarValueY1').val(''); 39 | $('#uploadAvatarValueX2').val(''); 40 | $('#uploadAvatarValueY2').val(''); 41 | 42 | $(this).removeAttr('disabled'); 43 | return false; 44 | }); 45 | }); 46 | 47 | function upload_avatar_error(msg) { 48 | $("#uploadAvatarCropResult").hide(100).removeClass('alert-success').addClass('alert-error').text(msg).show(200); 49 | } 50 | 51 | function crop_avatar_success(msg) { 52 | $("#uploadAvatarCropResult").hide(100).removeClass('alert-error').addClass('alert-success').text(msg).show(200); 53 | } 54 | 55 | function updatePreview50(img, selection) { 56 | if (parseInt(selection.width, 10) > 0) { 57 | var ratiox = 50 / selection.width; 58 | $("#uploadAvatarPreviewArea50 img").css({ 59 | width: Math.round(ratiox * img.width) + 'px', 60 | marginLeft: '-' + Math.round(ratiox * selection.x1) + 'px', 61 | marginTop: '-' + Math.round(ratiox * selection.y1) + 'px' 62 | }); 63 | } 64 | } 65 | 66 | function updatePreview120(img, selection) { 67 | if (parseInt(selection.width, 10) > 0) { 68 | var ratiox = 120 / selection.width; 69 | $("#uploadAvatarPreviewArea120 img").css({ 70 | width: Math.round(ratiox * img.width) + 'px', 71 | marginLeft: '-' + Math.round(ratiox * selection.x1) + 'px', 72 | marginTop: '-' + Math.round(ratiox * selection.y1) + 'px' 73 | }); 74 | } 75 | } 76 | 77 | function updateCoors(img, selection) { 78 | $("#uploadAvatarValueX1").val(selection.x1); 79 | $("#uploadAvatarValueY1").val(selection.y1); 80 | $("#uploadAvatarValueX2").val(selection.x2); 81 | $("#uploadAvatarValueY2").val(selection.y2); 82 | updatePreview50(img, selection); 83 | updatePreview120(img, selection); 84 | } 85 | 86 | 87 | 88 | function upload_avatar_success(image_url) { 89 | $('#uploadAvatarSelectArea').empty(); 90 | $('#uploadAvatarPreviewArea div').empty(); 91 | $('#uploadAvatarPreviewArea div').append(''); 92 | $('#uploadAvatarPreviewArea div img').attr('src', image_url).css('max-width', 'none'); 93 | 94 | $('#uploadAvatarSelectArea').append(''); 95 | $('#uploadAvatarSelectArea img').attr('src', image_url).load(function () { 96 | $(this).unbind('load'); 97 | 98 | var img_width, img_height, sel, crop_image_area_size = 300; 99 | img_width = $(this).width(); 100 | img_height = $(this).height(); 101 | 102 | if (img_width > crop_image_area_size || img_height > crop_image_area_size) { 103 | if (img_width >= img_height) { 104 | $(this).css('width', crop_image_area_size + "px"); 105 | } else { 106 | $(this).css('height', crop_image_area_size + "px"); 107 | } 108 | } 109 | 110 | img_width = $(this).width(); 111 | img_height = $(this).height(); 112 | 113 | sel = {}; 114 | sel.x1 = Math.round(img_width / 2 - 25 > 0 ? img_width / 2 - 25 : 0); 115 | sel.y1 = Math.round(img_height / 2 - 25 > 0 ? img_height / 2 - 25 : 0); 116 | sel.x2 = Math.round(img_width / 2 + 25 > img_width ? img_width : img_width / 2 + 25); 117 | sel.y2 = Math.round(img_height / 2 + 25 > img_height ? img_height : img_height / 2 + 25); 118 | sel.width = 50; 119 | 120 | $(this).imgAreaSelect({ 121 | handles: true, 122 | aspectRatio: "1:1", 123 | fadeSpeed: 100, 124 | minHeight: 50, 125 | minWidth: 50, 126 | x1: sel.x1, 127 | y1: sel.y1, 128 | x2: sel.x2, 129 | y2: sel.y2, 130 | onSelectChange: updateCoors 131 | }); 132 | 133 | updateCoors({'width': img_width}, sel); 134 | }); 135 | } 136 | 137 | -------------------------------------------------------------------------------- /siteuser/upload_avatar/static/js/upload_avatar.min.js: -------------------------------------------------------------------------------- 1 | "use strict";$(function(){var img_obj,p,fileanme,x1,y1,x2,y2;$("#uploadAvatarInputFile").change(function(){if($(this).val()===""){return}var $last_img=$("#uploadAvatarSelectArea img");if($last_img.length){img_obj=$last_img.imgAreaSelect({instance:true});img_obj.remove()}$("#uploadAvatarSelectArea").empty();$("#uploadAvatarPreviewArea div").empty();p=new RegExp(/\.(jpg|jpeg|png|gif)$/);fileanme=$(this).val().toLowerCase().replace(/^\s+|\s+$/g,"");if(!p.test(fileanme)){window.alert("请选择图片上传");return}$("#uploadAvatarForm").submit();$(this).val("");$("#uploadAvatarCropSubmit").removeAttr("disabled")});$("#uploadAvatarCropSubmit").click(function(){$(this).attr("disabled","disabled");x1=$("#uploadAvatarValueX1").val();y1=$("#uploadAvatarValueY1").val();x2=$("#uploadAvatarValueX2").val();y2=$("#uploadAvatarValueY2").val();if(x1===""||y1===""||x2===""||y2===""){$(this).removeAttr("disabled");return false}$("#uploadAvatarCropForm").submit();$("#uploadAvatarValueX1").val("");$("#uploadAvatarValueY1").val("");$("#uploadAvatarValueX2").val("");$("#uploadAvatarValueY2").val("");$(this).removeAttr("disabled");return false})});function upload_avatar_error(msg){$("#uploadAvatarCropResult").hide(100).removeClass("alert-success").addClass("alert-error").text(msg).show(200)}function crop_avatar_success(msg){$("#uploadAvatarCropResult").hide(100).removeClass("alert-error").addClass("alert-success").text(msg).show(200)}function updatePreview50(img,selection){if(parseInt(selection.width,10)>0){var ratiox=50/selection.width;$("#uploadAvatarPreviewArea50 img").css({width:Math.round(ratiox*img.width)+"px",marginLeft:"-"+Math.round(ratiox*selection.x1)+"px",marginTop:"-"+Math.round(ratiox*selection.y1)+"px"})}}function updatePreview120(img,selection){if(parseInt(selection.width,10)>0){var ratiox=120/selection.width;$("#uploadAvatarPreviewArea120 img").css({width:Math.round(ratiox*img.width)+"px",marginLeft:"-"+Math.round(ratiox*selection.x1)+"px",marginTop:"-"+Math.round(ratiox*selection.y1)+"px"})}}function updateCoors(img,selection){$("#uploadAvatarValueX1").val(selection.x1);$("#uploadAvatarValueY1").val(selection.y1);$("#uploadAvatarValueX2").val(selection.x2);$("#uploadAvatarValueY2").val(selection.y2);updatePreview50(img,selection);updatePreview120(img,selection)}function upload_avatar_success(image_url){$("#uploadAvatarSelectArea").empty();$("#uploadAvatarPreviewArea div").empty();$("#uploadAvatarPreviewArea div").append("");$("#uploadAvatarPreviewArea div img").attr("src",image_url).css("max-width","none");$("#uploadAvatarSelectArea").append("");$("#uploadAvatarSelectArea img").attr("src",image_url).load(function(){$(this).unbind("load");var img_width,img_height,sel,crop_image_area_size=300;img_width=$(this).width();img_height=$(this).height();if(img_width>crop_image_area_size||img_height>crop_image_area_size){if(img_width>=img_height){$(this).css("width",crop_image_area_size+"px")}else{$(this).css("height",crop_image_area_size+"px")}}img_width=$(this).width();img_height=$(this).height();sel={};sel.x1=Math.round(img_width/2-25>0?img_width/2-25:0);sel.y1=Math.round(img_height/2-25>0?img_height/2-25:0);sel.x2=Math.round(img_width/2+25>img_width?img_width:img_width/2+25);sel.y2=Math.round(img_height/2+25>img_height?img_height:img_height/2+25);sel.width=50;$(this).imgAreaSelect({handles:true,aspectRatio:"1:1",fadeSpeed:100,minHeight:50,minWidth:50,x1:sel.x1,y1:sel.y1,x2:sel.x2,y2:sel.y2,onSelectChange:updateCoors});updateCoors({width:img_width},sel)})} -------------------------------------------------------------------------------- /siteuser/upload_avatar/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file demonstrates writing tests using the unittest module. These will pass 3 | when you run "manage.py test". 4 | 5 | Replace this with more appropriate tests for your application. 6 | """ 7 | 8 | from django.test import TestCase 9 | 10 | 11 | class SimpleTest(TestCase): 12 | def test_basic_addition(self): 13 | """ 14 | Tests that 1 + 1 always equals 2. 15 | """ 16 | self.assertEqual(1 + 1, 2) 17 | -------------------------------------------------------------------------------- /siteuser/upload_avatar/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.conf.urls import patterns, url 4 | 5 | from siteuser.upload_avatar import views 6 | 7 | urlpatterns = patterns('', 8 | url(r'^uploadavatar_upload/?$', views.upload_avatar, name="uploadavatar_upload"), 9 | url(r'^uploadavatar_crop/?$', views.crop_avatar, name="uploadavatar_crop"), 10 | ) -------------------------------------------------------------------------------- /siteuser/upload_avatar/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import hashlib 5 | import time 6 | from functools import wraps 7 | 8 | from PIL import Image 9 | 10 | from django.http import HttpResponse 11 | from django.utils.crypto import get_random_string 12 | 13 | from siteuser.settings import ( 14 | AVATAR_UPLOAD_MAX_SIZE, 15 | AVATAR_UPLOAD_DIR, 16 | AVATAR_DIR, 17 | AVATAR_UPLOAD_URL_PREFIX, 18 | AVATAR_RESIZE_SIZE, 19 | AVATAR_SAVE_FORMAT, 20 | AVATAR_SAVE_QUALITY, 21 | ) 22 | 23 | from siteuser.upload_avatar.signals import avatar_upload_done, avatar_crop_done 24 | from siteuser.upload_avatar.models import UploadedImage 25 | 26 | """ 27 | 用户上传图片分三步: 28 | 1. 上传一张图片,服务器将其保存起来,并且把url返回给浏览器 29 | 2. 浏览器在预览区域显示这张图片,然后加载js库来剪裁图片。 30 | 3. 其实js库只是获取了选框四个顶点对应于图片的坐标,然后还要将这个坐标发送到服务器,服务器剪裁图片 31 | """ 32 | 33 | border_size = 300 34 | 35 | test_func = lambda request: request.method == 'POST' and request.siteuser 36 | get_uid = lambda request: request.siteuser.id 37 | 38 | 39 | class UploadAvatarError(Exception): 40 | pass 41 | 42 | 43 | def protected(func): 44 | @wraps(func) 45 | def deco(request, *args, **kwargs): 46 | if not test_func(request): 47 | return HttpResponse( 48 | "" % '禁止操作' 49 | ) 50 | try: 51 | return func(request, *args, **kwargs) 52 | except UploadAvatarError as e: 53 | return HttpResponse( 54 | "" % e 55 | ) 56 | return deco 57 | 58 | 59 | @protected 60 | def upload_avatar(request): 61 | """上传图片""" 62 | try: 63 | uploaded_file = request.FILES['uploadavatarfile'] 64 | except KeyError: 65 | raise UploadAvatarError('请正确上传图片') 66 | 67 | if uploaded_file.size > AVATAR_UPLOAD_MAX_SIZE * 1024 * 1024: 68 | raise UploadAvatarError('图片不能大于{0}MB'.format(AVATAR_UPLOAD_MAX_SIZE)) 69 | 70 | name, ext = os.path.splitext(uploaded_file.name) 71 | new_name = hashlib.md5('{0}{1}'.format(get_random_string(), time.time())).hexdigest() 72 | new_name = '%s%s' % (new_name, ext.lower()) 73 | 74 | fpath = os.path.join(AVATAR_UPLOAD_DIR, new_name) 75 | 76 | try: 77 | with open(fpath, 'wb') as f: 78 | for c in uploaded_file.chunks(10240): 79 | f.write(c) 80 | except IOError: 81 | raise UploadAvatarError('发生错误,稍后再试') 82 | 83 | try: 84 | Image.open(fpath) 85 | except IOError: 86 | try: 87 | os.unlink(fpath) 88 | except: 89 | pass 90 | raise UploadAvatarError('请正确上传图片') 91 | 92 | # uploaed image has been saved on disk, now save it's name in db 93 | if UploadedImage.objects.filter(uid=get_uid(request)).exists(): 94 | _obj = UploadedImage.objects.get(uid=get_uid(request)) 95 | _obj.delete_image() 96 | _obj.image = new_name 97 | _obj.save() 98 | else: 99 | UploadedImage.objects.create(uid=get_uid(request), image=new_name) 100 | 101 | # 上传完毕 102 | avatar_upload_done.send(sender=None, 103 | uid=get_uid(request), 104 | avatar_name=new_name, 105 | dispatch_uid='siteuser_avatar_upload_done' 106 | ) 107 | 108 | return HttpResponse( 109 | "" % ( 110 | AVATAR_UPLOAD_URL_PREFIX + new_name 111 | ) 112 | ) 113 | 114 | 115 | @protected 116 | def crop_avatar(request): 117 | """剪裁头像""" 118 | try: 119 | upim = UploadedImage.objects.get(uid=get_uid(request)) 120 | except UploadedImage.DoesNotExist: 121 | raise UploadAvatarError('请先上传图片') 122 | 123 | image_orig = upim.get_image_path() 124 | if not image_orig: 125 | raise UploadAvatarError('请先上传图片') 126 | 127 | try: 128 | x1 = int(float(request.POST['x1'])) 129 | y1 = int(float(request.POST['y1'])) 130 | x2 = int(float(request.POST['x2'])) 131 | y2 = int(float(request.POST['y2'])) 132 | except: 133 | raise UploadAvatarError('发生错误,稍后再试') 134 | 135 | 136 | try: 137 | orig = Image.open(image_orig) 138 | except IOError: 139 | raise UploadAvatarError('发生错误,请重新上传图片') 140 | 141 | orig_w, orig_h = orig.size 142 | if orig_w <= border_size and orig_h <= border_size: 143 | ratio = 1 144 | else: 145 | if orig_w > orig_h: 146 | ratio = float(orig_w) / border_size 147 | else: 148 | ratio = float(orig_h) / border_size 149 | 150 | box = [int(x * ratio) for x in [x1, y1, x2, y2]] 151 | avatar = orig.crop(box) 152 | avatar_name, _ = os.path.splitext(upim.image) 153 | 154 | 155 | size = AVATAR_RESIZE_SIZE 156 | try: 157 | res = avatar.resize((size, size), Image.ANTIALIAS) 158 | res_name = '%s-%d.%s' % (avatar_name, size, AVATAR_SAVE_FORMAT) 159 | res_path = os.path.join(AVATAR_DIR, res_name) 160 | res.save(res_path, AVATAR_SAVE_FORMAT, quality=AVATAR_SAVE_QUALITY) 161 | except: 162 | raise UploadAvatarError('发生错误,请稍后重试') 163 | 164 | 165 | avatar_crop_done.send(sender = None, 166 | uid = get_uid(request), 167 | avatar_name = res_name, 168 | dispatch_uid = 'siteuser_avatar_crop_done' 169 | ) 170 | 171 | return HttpResponse( 172 | "" % '成功' 173 | ) 174 | -------------------------------------------------------------------------------- /siteuser/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | from siteuser.users.urls import urlpatterns 4 | from siteuser.upload_avatar.urls import urlpatterns as upurls 5 | from siteuser.notify.urls import urlpatterns as nourls 6 | 7 | siteuser_url_table = { 8 | 'siteuser.upload_avatar': upurls, 9 | 'siteuser.notify': nourls, 10 | } 11 | 12 | for app in settings.INSTALLED_APPS: 13 | if app in siteuser_url_table: 14 | urlpatterns += siteuser_url_table[app] 15 | -------------------------------------------------------------------------------- /siteuser/users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yueyoum/django-siteuser/cb97b011c135aa756c262f12b0cb0a127a6933f9/siteuser/users/__init__.py -------------------------------------------------------------------------------- /siteuser/users/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | 4 | from django.conf import settings 5 | from django.db import models 6 | from django.utils import timezone 7 | 8 | from siteuser.settings import ( 9 | MAX_EMAIL_LENGTH, 10 | MAX_USERNAME_LENGTH, 11 | AVATAR_URL_PREFIX, 12 | DEFAULT_AVATAR, 13 | AVATAR_DIR, 14 | ) 15 | 16 | from siteuser.upload_avatar.signals import avatar_crop_done 17 | 18 | """ 19 | siteuser的核心, 20 | SocialUser - 保存第三方帐号 21 | InnerUser - 网站自身注册用户 22 | SiteUser - 用户信息表 23 | 24 | 目前 SocialUser, InnerUser 都不支持扩展,方法直接写死。 25 | 这种在只支持第三方登录的应用情况下,是够用的。 26 | 27 | SiteUser 之定义了最基本的数据,用户可以自由的扩展字段。 28 | 需要注意的是, SiteUser中的 username 不能设置为 unique = True 29 | 因为第三方社交帐号的username也保存在这个表里, 30 | 然而不同社交站点的用户完全有可能重名。 31 | """ 32 | 33 | 34 | 35 | class SiteUserManager(models.Manager): 36 | def create(self, is_social, **kwargs): 37 | if 'user' not in kwargs and 'user_id' not in kwargs: 38 | siteuser_kwargs = { 39 | 'is_social': is_social, 40 | 'username': kwargs.pop('username'), 41 | 'date_joined': timezone.now(), 42 | } 43 | if 'avatar_url' in kwargs: 44 | siteuser_kwargs['avatar_url'] = kwargs.pop('avatar_url') 45 | user = SiteUser.objects.create(**siteuser_kwargs) 46 | kwargs['user_id'] = user.id 47 | 48 | return super(SiteUserManager, self).create(**kwargs) 49 | 50 | 51 | class SocialUserManager(SiteUserManager): 52 | def create(self, **kwargs): 53 | return super(SocialUserManager, self).create(True, **kwargs) 54 | 55 | 56 | class InnerUserManager(SiteUserManager): 57 | def create(self, **kwargs): 58 | return super(InnerUserManager, self).create(False, **kwargs) 59 | 60 | 61 | class SocialUser(models.Model): 62 | """第三方帐号""" 63 | user = models.OneToOneField('SiteUser', related_name='social_user') 64 | site_uid = models.CharField(max_length=128) 65 | site_name = models.CharField(max_length=32) 66 | 67 | objects = SocialUserManager() 68 | 69 | class Meta: 70 | unique_together = (('site_uid', 'site_name'),) 71 | 72 | 73 | class InnerUser(models.Model): 74 | """自身注册用户""" 75 | user = models.OneToOneField('SiteUser', related_name='inner_user') 76 | email = models.CharField(max_length=MAX_EMAIL_LENGTH, unique=True) 77 | passwd = models.CharField(max_length=40) 78 | 79 | objects = InnerUserManager() 80 | 81 | 82 | def _siteuser_extend(): 83 | siteuser_extend_model = getattr(settings, 'SITEUSER_EXTEND_MODEL', None) 84 | if not siteuser_extend_model: 85 | return models.Model 86 | 87 | if isinstance(siteuser_extend_model, models.base.ModelBase): 88 | # 直接定义的 SITEUSER_EXTEND_MODEL 89 | if not siteuser_extend_model._meta.abstract: 90 | raise AttributeError("%s must be an abstract model" % siteuser_extend_model.__name__) 91 | return siteuser_extend_model 92 | 93 | # 以string的方式定义的 SITEUSER_EXTEND_MODEL 94 | _module, _model = siteuser_extend_model.rsplit('.', 1) 95 | try: 96 | m = __import__(_module, fromlist=['.']) 97 | _model = getattr(m, _model) 98 | except: 99 | m = __import__(_module + '.models', fromlist=['.']) 100 | _model = getattr(m, _model) 101 | 102 | if not _model._meta.abstract: 103 | raise AttributeError("%s must be an abstract model" % siteuser_extend_model) 104 | return _model 105 | 106 | 107 | 108 | class SiteUser(_siteuser_extend()): 109 | """用户信息,如果需要(大部分情况也确实是需要)扩展SiteUser的字段, 110 | 需要定义SITEUSER_EXTEND_MODEL.此model必须设置 abstract=True 111 | """ 112 | is_social = models.BooleanField() 113 | is_active = models.BooleanField(default=True) 114 | date_joined = models.DateTimeField() 115 | 116 | username = models.CharField(max_length=MAX_USERNAME_LENGTH, db_index=True) 117 | # avatar_url for social user 118 | avatar_url = models.CharField(max_length=255, blank=True) 119 | # avatar_name for inner user uploaded avatar 120 | avatar_name = models.CharField(max_length=64, blank=True) 121 | 122 | def __unicode__(self): 123 | return u'' % (self.id, self.username) 124 | 125 | @property 126 | def avatar(self): 127 | if not self.avatar_url and not self.avatar_name: 128 | return AVATAR_URL_PREFIX + DEFAULT_AVATAR 129 | if self.is_social: 130 | return self.avatar_url 131 | return AVATAR_URL_PREFIX + self.avatar_name 132 | 133 | 134 | def _save_avatar_in_db(sender, uid, avatar_name, **kwargs): 135 | if not SiteUser.objects.filter(id=uid, is_social=False).exists(): 136 | return 137 | 138 | old_avatar_name = SiteUser.objects.get(id=uid).avatar_name 139 | if old_avatar_name == avatar_name: 140 | # 上传一张图片后,连续剪裁的情况 141 | return 142 | 143 | if old_avatar_name: 144 | _path = os.path.join(AVATAR_DIR, old_avatar_name) 145 | try: 146 | os.unlink(_path) 147 | except: 148 | pass 149 | 150 | SiteUser.objects.filter(id=uid).update(avatar_name=avatar_name) 151 | 152 | 153 | avatar_crop_done.connect(_save_avatar_in_db) 154 | -------------------------------------------------------------------------------- /siteuser/users/static/js/siteuser.js: -------------------------------------------------------------------------------- 1 | (function(window, $){ 2 | $(function(){ 3 | $('#siteuserLogin').click(function(e){ 4 | e.preventDefault(); 5 | var email, passwd, referer; 6 | email = $('#siteuserLoginEmail').val(); 7 | passwd = $('#siteuserLoginPassword').val(); 8 | email = strip(email); 9 | passwd = strip(passwd); 10 | 11 | if(email.length===0 || passwd.length===0) { 12 | make_warning('#siteuserLoginWarning', '请填写电子邮件和密码'); 13 | return; 14 | } 15 | 16 | $.ajax( 17 | { 18 | type: 'POST', 19 | url: '/account/login/', 20 | data: { 21 | email: email, 22 | passwd: passwd, 23 | csrfmiddlewaretoken: get_csrf() 24 | }, 25 | dateType: 'json', 26 | async: false, 27 | success: function(data){ 28 | if(data.ok) { 29 | referer = $('#siteuserLogin').attr('referer'); 30 | if(referer==="" || referer===undefined) { 31 | window.location.reload(); 32 | } else { 33 | window.location.href = referer; 34 | } 35 | } 36 | else { 37 | make_warning('#siteuserLoginWarning', data.msg); 38 | } 39 | }, 40 | error: function(XmlHttprequest, textStatus, errorThrown){ 41 | make_warning('#siteuserLoginWarning', '发生错误,请稍后再试'); 42 | } 43 | } 44 | ); 45 | }); 46 | 47 | // register 48 | $('#siteuserRegister').click(function(e){ 49 | e.preventDefault(); 50 | var email, username, passwd, passwd2, _tmp_email, referer; 51 | email = $('#siteuserRegEmail').val(); 52 | username = $('#siteuserRegUsername').val(); 53 | passwd = $('#siteuserRegPassword').val(); 54 | passwd2 = $('#siteuserRegPassword2').val(); 55 | email = strip(email); 56 | username = strip(username); 57 | passwd = strip(passwd); 58 | passwd2 = strip(passwd2); 59 | 60 | _tmp_email = email.replace(/^.+@.+\..+$/, ''); 61 | if(_tmp_email.length>0){ 62 | make_warning('#siteuserRegisterWarning', '目测邮箱格式不正确啊'); 63 | return; 64 | } 65 | 66 | if(email.length === 0 || username.length === 0 || passwd.length === 0 || passwd2.length === 0) { 67 | make_warning('#siteuserRegisterWarning', '请完整填写注册信息'); 68 | return; 69 | } 70 | 71 | if(passwd != passwd2) { 72 | make_warning('#siteuserRegisterWarning', '两次密码不一致'); 73 | return; 74 | } 75 | 76 | $.ajax( 77 | { 78 | type: 'POST', 79 | url: '/account/register/', 80 | data: { 81 | email: email, 82 | username: username, 83 | passwd: passwd, 84 | csrfmiddlewaretoken: get_csrf() 85 | }, 86 | dateType: 'json', 87 | async: false, 88 | success: function(data){ 89 | if(data.ok) { 90 | referer = $('#siteuserRegister').attr('referer'); 91 | if(referer==="" || referer===undefined) { 92 | window.location.reload(); 93 | } else { 94 | window.location.href = referer; 95 | } 96 | } 97 | else { 98 | make_warning('#siteuserRegisterWarning', data.msg); 99 | } 100 | }, 101 | error: function(XmlHttprequest, textStatus, errorThrown){ 102 | make_warning('#siteuserRegisterWarning', '发生错误,请稍后再试'); 103 | } 104 | } 105 | ); 106 | }); 107 | 108 | //logout 109 | $('#siteuserLogout').click(function(e){ 110 | e.preventDefault(); 111 | $.ajax( 112 | { 113 | type: 'GET', 114 | url: '/account/logout/', 115 | async: false, 116 | success: function(data){ 117 | window.location.reload(); 118 | }, 119 | error: function(XmlHttprequest, textStatus, errorThrown){ 120 | window.location.reload(); 121 | } 122 | } 123 | ); 124 | }); 125 | 126 | get_notifies(); 127 | 128 | }); 129 | 130 | 131 | function strip(value){ 132 | return value.replace(/(^\s+|\s+$)/, ''); 133 | } 134 | 135 | function get_csrf(){ 136 | return $('input[name=csrfmiddlewaretoken]').attr('value'); 137 | } 138 | 139 | 140 | function make_warning(obj, text) { 141 | $(obj).text(text); 142 | $(obj).show(100); 143 | } 144 | 145 | // get notifies 146 | function get_notifies() { 147 | $.ajax( 148 | { 149 | type: 'GET', 150 | url: '/notifies.json/', 151 | data: {}, 152 | dateType: 'json', 153 | async: true, 154 | success: function(data){ 155 | if(data.length>0) { 156 | var len = data.length > 10 ? '9+' : data.length; 157 | $('#siteuserNotify').text(len); 158 | var $ul = $('
      '); 159 | $ul.attr('id', 'siteusernotifyul'); 160 | data.forEach(function(item){ 161 | var $li = $('
    • '); 162 | $li.append(item); 163 | $li.appendTo($ul); 164 | }); 165 | $('#siteuserNotify').after($ul); 166 | bind_notify_click_event(); 167 | } 168 | else { 169 | $('#siteuserNotify').text(0); 170 | } 171 | }, 172 | error: function(XmlHttprequest, textStatus, errorThrown){ 173 | } 174 | } 175 | ); 176 | } 177 | 178 | function bind_notify_click_event() { 179 | $('#siteusernotifyul>li>a:odd').click(function(){ 180 | $(this).parent().hide(); 181 | }); 182 | } 183 | 184 | })(window, jQuery); 185 | 186 | -------------------------------------------------------------------------------- /siteuser/users/tasks.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from celery import task 4 | 5 | from siteuser.functional import send_html_mail as _send_mail 6 | 7 | @task 8 | def send_mail(to, subject, context): 9 | _send_mail(to, subject, context) 10 | -------------------------------------------------------------------------------- /siteuser/users/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file demonstrates writing tests using the unittest module. These will pass 3 | when you run "manage.py test". 4 | 5 | Replace this with more appropriate tests for your application. 6 | """ 7 | 8 | from django.test import TestCase 9 | 10 | 11 | class SimpleTest(TestCase): 12 | def test_basic_addition(self): 13 | """ 14 | Tests that 1 + 1 always equals 2. 15 | """ 16 | self.assertEqual(1 + 1, 2) 17 | -------------------------------------------------------------------------------- /siteuser/users/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.conf.urls import patterns, url 4 | 5 | from siteuser.users import views 6 | from siteuser.settings import USING_SOCIAL_LOGIN 7 | 8 | urlpatterns = patterns('', 9 | url(r'^account/login/$', views.SiteUserLoginView.as_view(), name='siteuser_login'), 10 | url(r'^account/register/$', views.SiteUserRegisterView.as_view(), name='siteuser_register'), 11 | 12 | # 丢失密码,重置第一步,填写注册邮件 13 | url(r'^account/resetpw/step1/$', views.SiteUserResetPwStepOneView.as_view(), name='siteuser_reset_step1'), 14 | url(r'^account/resetpw/step1/done/$', views.SiteUserResetPwStepOneDoneView.as_view(), name='siteuser_reset_step1_done'), 15 | 16 | # 第二布,重置密码。token是django.core.signing模块生成的带时间戳的加密字符串 17 | url(r'^account/resetpw/step2/done/$', views.SiteUserResetPwStepTwoDoneView.as_view(), name='siteuser_reset_step2_done'), 18 | url(r'^account/resetpw/step2/(?P.+)/$', views.SiteUserResetPwStepTwoView.as_view(), name='siteuser_reset_step2'), 19 | 20 | # 登录用户修改密码 21 | url(r'^account/changepw/$', views.SiteUserChangePwView.as_view(), name='siteuser_changepw'), 22 | url(r'^account/changepw/done/$', views.SiteUserChangePwDoneView.as_view(), name='siteuser_changepw_done'), 23 | 24 | # 以上关于密码管理的url只能有本网站注册用户才能访问,第三方帐号不需要此功能 25 | 26 | 27 | url(r'^account/logout/$', views.logout, name='siteuser_logout'), 28 | ) 29 | 30 | 31 | # 只有设置 USING_SOCIAL_LOGIN = True 的情况下,才会开启第三方登录功能 32 | if USING_SOCIAL_LOGIN: 33 | urlpatterns += patterns('', 34 | url(r'^account/oauth/(?P\w+)/?$', views.social_login_callback), 35 | ) 36 | -------------------------------------------------------------------------------- /siteuser/users/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import re 3 | import json 4 | import hashlib 5 | from functools import wraps 6 | 7 | from django.core import signing 8 | from django.core.urlresolvers import reverse 9 | from django.http import HttpResponse, HttpResponseRedirect, Http404 10 | from django.shortcuts import render_to_response 11 | from django.template import loader, RequestContext 12 | from django.views.generic import View 13 | 14 | 15 | from siteuser.users.models import InnerUser, SiteUser, SocialUser 16 | from siteuser.users.tasks import send_mail 17 | from siteuser.settings import ( 18 | USING_SOCIAL_LOGIN, 19 | MAX_EMAIL_LENGTH, 20 | MAX_USERNAME_LENGTH, 21 | SOCIALOAUTH_SITES, 22 | SOCIAL_LOGIN_DONE_REDIRECT_URL, 23 | SOCIAL_LOGIN_ERROR_REDIRECT_URL, 24 | ) 25 | from siteuser.utils.load_user_define import user_defined_mixin 26 | 27 | if USING_SOCIAL_LOGIN: 28 | from socialoauth import SocialSites, SocialAPIError, SocialSitesConfigError 29 | 30 | # 注册,登录,退出等都通过 ajax 的方式进行 31 | 32 | EMAIL_PATTERN = re.compile('^.+@.+\..+$') 33 | 34 | class InnerAccoutError(Exception): 35 | pass 36 | 37 | make_password = lambda passwd: hashlib.sha1(passwd).hexdigest() 38 | 39 | def inner_account_ajax_guard(func): 40 | @wraps(func) 41 | def deco(self, request, *args, **kwargs): 42 | dump = lambda d: HttpResponse(json.dumps(d), content_type='application/json') 43 | if request.siteuser: 44 | return dump({'ok': False, 'msg': '你已登录'}) 45 | 46 | try: 47 | func(self, request, *args, **kwargs) 48 | except InnerAccoutError as e: 49 | return dump({'ok': False, 'msg': str(e)}) 50 | 51 | return dump({'ok': True}) 52 | return deco 53 | 54 | def inner_account_http_guard(func): 55 | @wraps(func) 56 | def deco(self, request, *args, **kwargs): 57 | if request.siteuser: 58 | return HttpResponseRedirect('/') 59 | try: 60 | return func(self, request, *args, **kwargs) 61 | except InnerAccoutError as e: 62 | ctx = self.ctx_getter(request) 63 | ctx.update(getattr(self, 'ctx', {})) 64 | ctx.update({'error_msg': e}) 65 | return render_to_response( 66 | self.tpl, 67 | ctx, 68 | context_instance=RequestContext(request) 69 | ) 70 | return deco 71 | 72 | 73 | class SiteUserMixIn(object): 74 | """用户可以自定义 SITEUSER_ACCOUNT_MIXIN 来覆盖这些配置""" 75 | login_template = 'siteuser/login.html' 76 | register_template = 'siteuser/register.html' 77 | reset_passwd_template = 'siteuser/reset_password.html' 78 | change_passwd_template = 'siteuser/change_password.html' 79 | 80 | # 用于生成重置密码链接的key,用于加密解密 81 | sign_key = 'siteuser_signkey' 82 | 83 | # 重置密码邮件的标题 84 | reset_passwd_email_title = u'重置密码' 85 | reset_passwd_email_template = 'siteuser/reset_password_email.html' 86 | 87 | # 多少小时后重置密码的链接失效 88 | reset_passwd_link_expired_in = 24 89 | 90 | # 在渲染这些模板的时候,如果你有额外的context需要传入,请重写这些方法 91 | def get_login_context(self, request): 92 | return {} 93 | 94 | def get_register_context(self, request): 95 | return {} 96 | 97 | def get_reset_passwd_context(self, request): 98 | return {} 99 | 100 | def get_change_passwd_context(self, request): 101 | return {} 102 | 103 | def get(self, request, *args, **kwargs): 104 | """使用此get方法的Class,必须制定这两个属性: 105 | self.tpl - 此view要渲染的模板名 106 | self.ctx_getter - 渲染模板是获取额外context的方法名 107 | """ 108 | if request.siteuser: 109 | return HttpResponseRedirect('/') 110 | ctx = self.ctx_getter(request) 111 | ctx.update(getattr(self, 'ctx', {})) 112 | return render_to_response( 113 | self.tpl, 114 | ctx, 115 | context_instance=RequestContext(request) 116 | ) 117 | 118 | def _reset_passwd_default_ctx(self): 119 | return { 120 | 'step1': False, 121 | 'step1_done': False, 122 | 'step2': False, 123 | 'step2_done': False, 124 | 'expired': False, 125 | } 126 | 127 | def _normalize_referer(self, request): 128 | referer = request.META.get('HTTP_REFERER', '/') 129 | if referer.endswith('done/'): 130 | referer = '/' 131 | return referer 132 | 133 | 134 | 135 | class SiteUserLoginView(user_defined_mixin(), SiteUserMixIn, View): 136 | """登录""" 137 | def __init__(self, **kwargs): 138 | self.tpl = self.login_template 139 | self.ctx_getter = self.get_login_context 140 | super(SiteUserLoginView, self).__init__(**kwargs) 141 | 142 | def get_login_context(self, request): 143 | """注册和登录都是通过ajax进行的,这里渲染表单模板的时候传入referer, 144 | 当ajax post返回成功标识的时候,js就到此referer的页面。 145 | 以此来完成注册/登录完毕后自动回到上个页面 146 | """ 147 | ctx = super(SiteUserLoginView, self).get_login_context(request) 148 | ctx['referer'] = self._normalize_referer(request) 149 | return ctx 150 | 151 | @inner_account_ajax_guard 152 | def post(self, request, *args, **kwargs): 153 | email = request.POST.get('email', None) 154 | passwd = request.POST.get('passwd', None) 155 | 156 | if not email or not passwd: 157 | raise InnerAccoutError('请填写email和密码') 158 | 159 | try: 160 | user = InnerUser.objects.get(email=email) 161 | except InnerUser.DoesNotExist: 162 | raise InnerAccoutError('用户不存在') 163 | 164 | if user.passwd != hashlib.sha1(passwd).hexdigest(): 165 | raise InnerAccoutError('密码错误') 166 | 167 | request.session['uid'] = user.user.id 168 | 169 | 170 | class SiteUserRegisterView(user_defined_mixin(), SiteUserMixIn, View): 171 | """注册""" 172 | def __init__(self, **kwargs): 173 | self.tpl = self.register_template 174 | self.ctx_getter = self.get_register_context 175 | super(SiteUserRegisterView, self).__init__(**kwargs) 176 | 177 | def get_register_context(self, request): 178 | ctx = super(SiteUserRegisterView, self).get_register_context(request) 179 | ctx['referer'] = self._normalize_referer(request) 180 | return ctx 181 | 182 | @inner_account_ajax_guard 183 | def post(self, request, *args, **kwargs): 184 | email = request.POST.get('email', None) 185 | username = request.POST.get('username', None) 186 | passwd = request.POST.get('passwd', None) 187 | 188 | if not email or not username or not passwd: 189 | raise InnerAccoutError('请完整填写注册信息') 190 | 191 | if len(email) > MAX_EMAIL_LENGTH: 192 | raise InnerAccoutError('电子邮件地址太长') 193 | 194 | if EMAIL_PATTERN.search(email) is None: 195 | raise InnerAccoutError('电子邮件格式不正确') 196 | 197 | if InnerUser.objects.filter(email=email).exists(): 198 | raise InnerAccoutError('此电子邮件已被占用') 199 | 200 | if len(username) > MAX_USERNAME_LENGTH: 201 | raise InnerAccoutError('用户名太长,不要超过{0}个字符'.format(MAX_USERNAME_LENGTH)) 202 | 203 | if SiteUser.objects.filter(username=username).exists(): 204 | raise InnerAccoutError('用户名已存在') 205 | 206 | passwd = make_password(passwd) 207 | user = InnerUser.objects.create(email=email, passwd=passwd, username=username) 208 | request.session['uid'] = user.user.id 209 | 210 | 211 | class SiteUserResetPwStepOneView(user_defined_mixin(), SiteUserMixIn, View): 212 | """丢失密码重置第一步,填写注册时的电子邮件""" 213 | def __init__(self, **kwargs): 214 | self.tpl = self.reset_passwd_template 215 | self.ctx_getter = self.get_reset_passwd_context 216 | self.ctx = self._reset_passwd_default_ctx() 217 | self.ctx['step1'] = True 218 | super(SiteUserResetPwStepOneView, self).__init__(**kwargs) 219 | 220 | @inner_account_http_guard 221 | def post(self, request, *args, **kwargs): 222 | email = request.POST.get('email', None) 223 | if not email: 224 | raise InnerAccoutError('请填写电子邮件') 225 | if EMAIL_PATTERN.search(email) is None: 226 | raise InnerAccoutError('电子邮件格式不正确') 227 | try: 228 | user = InnerUser.objects.get(email=email) 229 | except InnerUser.DoesNotExist: 230 | raise InnerAccoutError('请填写您注册时的电子邮件地址') 231 | 232 | token = signing.dumps(user.user.id, key=self.sign_key) 233 | link = reverse('siteuser_reset_step2', kwargs={'token': token}) 234 | link = request.build_absolute_uri(link) 235 | context = { 236 | 'hour': self.reset_passwd_link_expired_in, 237 | 'link': link 238 | } 239 | body = loader.render_to_string(self.reset_passwd_email_template, context) 240 | # 异步发送邮件 241 | body = unicode(body) 242 | send_mail.delay(email, self.reset_passwd_email_title, body) 243 | return HttpResponseRedirect(reverse('siteuser_reset_step1_done')) 244 | 245 | 246 | class SiteUserResetPwStepOneDoneView(user_defined_mixin(), SiteUserMixIn, View): 247 | """发送重置邮件完成""" 248 | def __init__(self, **kwargs): 249 | self.tpl = self.reset_passwd_template 250 | self.ctx_getter = self.get_reset_passwd_context 251 | self.ctx = self._reset_passwd_default_ctx() 252 | self.ctx['step1_done'] = True 253 | super(SiteUserResetPwStepOneDoneView, self).__init__(**kwargs) 254 | 255 | 256 | class SiteUserResetPwStepTwoView(user_defined_mixin(), SiteUserMixIn, View): 257 | """丢失密码重置第二步,填写新密码""" 258 | def __init__(self, **kwargs): 259 | self.tpl = self.reset_passwd_template 260 | self.ctx_getter = self.get_reset_passwd_context 261 | self.ctx = self._reset_passwd_default_ctx() 262 | self.ctx['step2'] = True 263 | super(SiteUserResetPwStepTwoView, self).__init__(**kwargs) 264 | 265 | def get(self, request, *args, **kwargs): 266 | token = kwargs['token'] 267 | try: 268 | self.uid = signing.loads(token, key=self.sign_key, max_age=self.reset_passwd_link_expired_in*3600) 269 | except signing.SignatureExpired: 270 | # 通过context来控制到底显示表单还是过期信息 271 | self.ctx['expired'] = True 272 | except signing.BadSignature: 273 | raise Http404 274 | return super(SiteUserResetPwStepTwoView, self).get(request, *args, **kwargs) 275 | 276 | 277 | @inner_account_http_guard 278 | def post(self, request, *args, **kwargs): 279 | password = request.POST.get('password', None) 280 | password1 = request.POST.get('password1', None) 281 | if not password or not password1: 282 | raise InnerAccoutError('请填写密码') 283 | if password != password1: 284 | raise InnerAccoutError('两次密码不一致') 285 | uid = signing.loads(kwargs['token'], key=self.sign_key) 286 | password = make_password(password) 287 | InnerUser.objects.filter(user_id=uid).update(passwd=password) 288 | return HttpResponseRedirect(reverse('siteuser_reset_step2_done')) 289 | 290 | 291 | class SiteUserResetPwStepTwoDoneView(user_defined_mixin(), SiteUserMixIn, View): 292 | """重置完成""" 293 | def __init__(self, **kwargs): 294 | self.tpl = self.reset_passwd_template 295 | self.ctx_getter = self.get_reset_passwd_context 296 | self.ctx = self._reset_passwd_default_ctx() 297 | self.ctx['step2_done'] = True 298 | super(SiteUserResetPwStepTwoDoneView, self).__init__(**kwargs) 299 | 300 | 301 | class SiteUserChangePwView(user_defined_mixin(), SiteUserMixIn, View): 302 | """已登录用户修改密码""" 303 | def render_to_response(self, request, **kwargs): 304 | ctx = self.get_change_passwd_context(request) 305 | ctx['done'] = False 306 | ctx.update(kwargs) 307 | return render_to_response( 308 | self.change_passwd_template, 309 | ctx, 310 | context_instance=RequestContext(request) 311 | ) 312 | 313 | def get(self, request, *args, **kwargs): 314 | if not request.siteuser: 315 | return HttpResponseRedirect('/') 316 | if not request.siteuser.is_active or request.siteuser.is_social: 317 | return HttpResponseRedirect('/') 318 | return self.render_to_response(request) 319 | 320 | def post(self, request, *args, **kwargs): 321 | if not request.siteuser: 322 | return HttpResponseRedirect('/') 323 | if not request.siteuser.is_active or request.siteuser.is_social: 324 | return HttpResponseRedirect('/') 325 | 326 | password = request.POST.get('password', None) 327 | password1 = request.POST.get('password1', None) 328 | if not password or not password1: 329 | return self.render_to_response(request, error_msg='请填写新密码') 330 | if password != password1: 331 | return self.render_to_response(request, error_msg='两次密码不一致') 332 | password = make_password(password) 333 | if request.siteuser.inner_user.passwd == password: 334 | return self.render_to_response(request, error_msg='不能与旧密码相同') 335 | InnerUser.objects.filter(user_id=request.siteuser.id).update(passwd=password) 336 | # 清除登录状态 337 | try: 338 | del request.session['uid'] 339 | except: 340 | pass 341 | 342 | return HttpResponseRedirect(reverse('siteuser_changepw_done')) 343 | 344 | 345 | class SiteUserChangePwDoneView(user_defined_mixin(), SiteUserMixIn, View): 346 | """已登录用户修改密码成功""" 347 | def get(self, request, *args, **kwargs): 348 | if request.siteuser: 349 | return HttpResponseRedirect('/') 350 | ctx = self.get_change_passwd_context(request) 351 | ctx['done'] = True 352 | return render_to_response( 353 | self.change_passwd_template, 354 | ctx, 355 | context_instance=RequestContext(request) 356 | ) 357 | 358 | 359 | def logout(request): 360 | """登出,ajax请求,然后刷新页面""" 361 | try: 362 | del request.session['uid'] 363 | except: 364 | pass 365 | 366 | return HttpResponse('', content_type='application/json') 367 | 368 | 369 | 370 | def social_login_callback(request, sitename): 371 | """第三方帐号OAuth认证登录,只有设置了USING_SOCIAL_LOGIN=True才会使用到此功能""" 372 | code = request.GET.get('code', None) 373 | if not code: 374 | return HttpResponseRedirect(SOCIAL_LOGIN_ERROR_REDIRECT_URL) 375 | 376 | socialsites = SocialSites(SOCIALOAUTH_SITES) 377 | try: 378 | site = socialsites.get_site_object_by_name(sitename) 379 | site.get_access_token(code) 380 | except(SocialSitesConfigError, SocialAPIError): 381 | return HttpResponseRedirect(SOCIAL_LOGIN_ERROR_REDIRECT_URL) 382 | 383 | # 首先根据site_name和site uid查找此用户是否已经在自身网站认证, 384 | # 如果查不到,表示这个用户第一次认证登陆,创建新用户记录 385 | # 如果查到,就跟新其用户名和头像 386 | try: 387 | user = SocialUser.objects.get(site_uid=site.uid, site_name=site.site_name) 388 | SiteUser.objects.filter(id=user.user.id).update(username=site.name, avatar_url=site.avatar) 389 | except SocialUser.DoesNotExist: 390 | user = SocialUser.objects.create( 391 | site_uid=site.uid, 392 | site_name=site.site_name, 393 | username=site.name, 394 | avatar_url=site.avatar 395 | ) 396 | 397 | # set uid in session, then this user will be auto login 398 | request.session['uid'] = user.user.id 399 | return HttpResponseRedirect(SOCIAL_LOGIN_DONE_REDIRECT_URL) 400 | -------------------------------------------------------------------------------- /siteuser/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.utils.functional import empty, SimpleLazyObject 4 | 5 | class LazyList(SimpleLazyObject): 6 | def __iter__(self): 7 | if self._wrapped is empty: 8 | self._setup() 9 | 10 | for i in self._wrapped: 11 | yield i 12 | 13 | def __len__(self): 14 | if self._wrapped is empty: 15 | self._setup() 16 | 17 | return len(self._wrapped) -------------------------------------------------------------------------------- /siteuser/utils/load_user_define.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.conf import settings 4 | from django.core.exceptions import ImproperlyConfigured 5 | 6 | class UserNotDefined(object):pass 7 | 8 | def user_defined_mixin(): 9 | mixin = getattr(settings, 'SITEUSER_ACCOUNT_MIXIN', UserNotDefined) 10 | if mixin is UserNotDefined: 11 | raise ImproperlyConfigured("No Settings For SITEUSER_ACCOUNT_MIXIN") 12 | if mixin is object: 13 | raise ImproperlyConfigured("Invalid SITEUSER_ACCOUNT_MIXIN") 14 | if isinstance(mixin, type): 15 | return mixin 16 | 17 | _module, _class = mixin.rsplit('.', 1) 18 | m = __import__(_module, fromlist=['.']) 19 | return getattr(m, _class) --------------------------------------------------------------------------------