diff options
author | Alvin Li <liweitianux@gmail.com> | 2013-10-04 23:56:35 +0800 |
---|---|---|
committer | Alvin Li <liweitianux@gmail.com> | 2013-10-04 23:56:35 +0800 |
commit | f552b41f4b337e6844f71c29ff177915abbfa417 (patch) | |
tree | 7ade59430c6767a5b379c7a8cb95af3387622b13 | |
parent | 816730ff659e1338ab3e37a1d45ea337e337b3dd (diff) | |
download | 97dev-f552b41f4b337e6844f71c29ff177915abbfa417.tar.bz2 |
* indicator/static/javascripts/card_chart.js:
improved the display position of 'detail_card_info'
* indicator/templates/indicator/SheetDefault.html:
destroy 'qtip' when close card
* added new app 'apps/sfaccount'
* implemented 'signup' and 'activate' functions
* implemented async sending activation mail
(using 'django-celery' and 'redis')
* moved 'registration/*' templates to 'sfaccount/templates'
* implemented 'password_change' function:
o password_change
o password_change_done
* implemented 'password_reset' function
o password_reset
o password_reset_done
o password_reset_confirm
o password_reset_complete
o re-write 'sfaccount.fomrs.SFPasswordResetForm'
o re-write 'sfaccount.views.password_reset_view'
* improved 'sfaccount.functional' send mail functions
o to send 'multipart' mail
* added 'README.txt'
* added app 'apps/recommend':
for comparing with the SCI papers and then recommending
most related papers for user.
57 files changed, 1435 insertions, 140 deletions
diff --git a/97suifangqa/README.txt b/97suifangqa/README.txt new file mode 100644 index 0000000..76c5533 --- /dev/null +++ b/97suifangqa/README.txt @@ -0,0 +1,8 @@ +## async send mail: +-> sfaccount/README.txt + +## run redis & celery +$ redis-server +$ python manage.py celeryd worker -E +$ python manage.py celerycam (for monitoring) + diff --git a/97suifangqa/apps/indicator/fixtures/initial_data.json b/97suifangqa/apps/indicator/fixtures_bak/initial_data.json index d26f8b9..d26f8b9 100644 --- a/97suifangqa/apps/indicator/fixtures/initial_data.json +++ b/97suifangqa/apps/indicator/fixtures_bak/initial_data.json diff --git a/97suifangqa/apps/indicator/static/javascripts/card_chart.js b/97suifangqa/apps/indicator/static/javascripts/card_chart.js index 92a91c0..ef2eb24 100644 --- a/97suifangqa/apps/indicator/static/javascripts/card_chart.js +++ b/97suifangqa/apps/indicator/static/javascripts/card_chart.js @@ -92,6 +92,22 @@ $(document).ready(function(){ // update 'detail_card_id' detail_card_id = $(this).closest(".index_card").attr("id").replace('index_card_', ''); var card = $("#index_card_"+detail_card_id); + // determine the position to show the 'detail_card' + var pos_to_insertafter = null; + if (card.hasClass("index_card_sec")) { + // this card in on the right + pos_to_insertafter = card; + } + else if (card.nextAll(".index_card_sec").length) { + // this card is on the left + pos_to_insertafter = card.nextAll(".index_card_sec").first(); + } + else { + // this card is the last card + pos_to_insertafter = card.nextAll(".act_card_container"); + } + // move 'detail_card_info' div + $(".detail_card_info").insertAfter(pos_to_insertafter); // check if this card has data (class "record_empty") if (card.hasClass("record_empty")) { $(".detail_card_info").hide(); @@ -142,7 +158,7 @@ $(document).ready(function(){ getdata_type, getdata_num, begin_date_str, end_date_str ); - $(".act_card_container").addClass("move_div_2_left"); + //$(".act_card_container").addClass("move_div_2_left"); return false; }); @@ -215,7 +231,7 @@ $(document).ready(function(){ //初始化详细卡片id detail_card_id = "-1"; //添加删除div位置初始化 - $(".act_card_container").removeClass("move_div_2_left"); + //$(".act_card_container").removeClass("move_div_2_left"); return false; }); }); diff --git a/97suifangqa/apps/indicator/static/javascripts/delete_card_tip.js b/97suifangqa/apps/indicator/static/javascripts/delete_card_tip.js index 9365d46..da5c896 100644 --- a/97suifangqa/apps/indicator/static/javascripts/delete_card_tip.js +++ b/97suifangqa/apps/indicator/static/javascripts/delete_card_tip.js @@ -1,32 +1,36 @@ // global js var: card_2_delete_id (type: string) $(document).ready(function(){ -// console.log(parent.card_2_delete_id); //要取消关注的 卡片id 的获取方法 - //点大叉、继续关注按钮,关闭弹层页面 - $(".delete_card_tip_close, .action_confirm_ignore").bind("click", function(){ - parent.TB_remove(); - return false; - }); - - //取消关注,关闭弹层页面 - $(".action_confirm_cancel").bind("click", function(){ - // ajax process to unfollow the indicator - // indicator_id -> parseInt(parent.card_2_delete_id) - // 底层数据层取消关注(ajax) - var date = new Date(); - var time = date.getTime(); - $.ajax({ - type: 'get', - url: parent.indicator_url + 'ajax/unfollow_indicator', - data: 'indicator_id='+parent.card_2_delete_id+'&time='+time, - success: function(data) { - if (data == 'success') { - parent.delete_card(); - parent.TB_remove(); - } - }, - }); - - return false; - }); + // console.log(parent.card_2_delete_id); //要取消关注的 卡片id 的获取方法 + //点大叉、继续关注按钮,关闭弹层页面 + $(".delete_card_tip_close, .action_confirm_ignore").bind("click", function(){ + parent.TB_remove(); + return false; + }); + + //取消关注,关闭弹层页面 + $(".action_confirm_cancel").bind("click", function(){ + // ajax process to unfollow the indicator + // indicator_id -> parseInt(parent.card_2_delete_id) + // 底层数据层取消关注(ajax) + var date = new Date(); + var time = date.getTime(); + $.ajax({ + type: 'get', + url: parent.indicator_url + 'ajax/unfollow_indicator', + data: 'indicator_id='+parent.card_2_delete_id+'&time='+time, + success: function(data) { + if (data == 'success') { + // destroy the 'qtip' if exist + //$('#index_card_'+parent.card_2_delete_id, window.parent.document).qtip('destroy', true); + // remove card + parent.delete_card(); + parent.TB_remove(); + } + }, + }); + + return false; + }); }); +// vim: set ts=4 sw=4 tw=0 fenc=utf-8 ft=javascript: // diff --git a/97suifangqa/apps/indicator/templates/indicator/SheetDefault.html b/97suifangqa/apps/indicator/templates/indicator/SheetDefault.html index 07cf2f0..5a6659f 100644 --- a/97suifangqa/apps/indicator/templates/indicator/SheetDefault.html +++ b/97suifangqa/apps/indicator/templates/indicator/SheetDefault.html @@ -284,12 +284,12 @@ {% block page %} {% csrf_token %} - <iframe align="left" width="420" height="1020" src="{% url indicator_sidebar %}" style="position:fixed;left:0;top:0;z-index:99;" frameborder="no" border="0" marginwidth="0" marginheight="0" scrolling="no"></iframe> + <iframe align="left" width="420" height="1020" src="{% url indicator_sidebar %}" style="position:fixed;left:0;top:0;z-index:99;" frameborder="no" marginwidth="0" marginheight="0" scrolling="no"></iframe> <!-- TODO --> <div id="login_container"> <div class="logged-in" id="login"> - <img class="user-photo" alt="" src="{% static "images/userPhoto.jpg" %}" /> + <img class="user-photo" alt="user_photo" src="{% static "images/userPhoto.jpg" %}" /> <span class="number-block green-block user-level">6</span> <span class="drop-down-area"> <span class="user-name">username</span> @@ -344,14 +344,14 @@ <span class="data_value">{{ ind|dict_get:"last_record"|dict_get:"value_html"|safe }}</span> <span class="data_unit">{% if ind|dict_get:"std_unit_symbol" %}({{ ind|dict_get:"std_unit_symbol" }}){% endif %}</span> </div> - <img class="small_edit_icon" src="{% static "images/pen.png" %}" /> + <img class="small_edit_icon" alt="edit_icon" src="{% static "images/pen.png" %}" /> <!-- explain_icon --> {% if ind|dict_get:"record_empty" %} - <img class="explain_icon nodata_icon" src="{% static "images/nodata.png" %}" style="display: block;" /> - <img class="explain_icon lastdata_icon" src="{% static "images/last_edit_data.png" %}" style="display: none;" /> + <img class="explain_icon nodata_icon" alt="nodata_icon" src="{% static "images/nodata.png" %}" style="display: block;" /> + <img class="explain_icon lastdata_icon" alt="lastdata_icon" src="{% static "images/last_edit_data.png" %}" style="display: none;" /> {% else %} - <img class="explain_icon nodata_icon" src="{% static "images/nodata.png" %}" style="display: none;" /> - <img class="explain_icon lastdata_icon" src="{% static "images/last_edit_data.png" %}" style="display: block;" /> + <img class="explain_icon nodata_icon" alt="nodata_icon" src="{% static "images/nodata.png" %}" style="display: none;" /> + <img class="explain_icon lastdata_icon" alt="lastdata_icon" src="{% static "images/last_edit_data.png" %}" style="display: block;" /> {% endif %} <div style="clear:both;"></div> <!-- last edit date --> @@ -433,7 +433,7 @@ {% endif %} <div class="card_bottom"> - <div class="understand_index"><a class="thickbox" href="{% url indicator_indexdesc %}?card_id={{ ind|dict_get:"id" }}&url_type=html&no_title=true&TB_iframe=true&height=367&width=630">了解该指标</a></div> + <div class="understand_index"><a class="thickbox" href="{% url indicator_indexdesc %}?card_id={{ ind|dict_get:"id" }}&url_type=html&no_title=true&TB_iframe=true&height=367&width=630">了解该指标</a></div> <!-- TODO --> <div class="simulation_sheet"><a href="{% static "images/demo_sheet.png" %}" class="thickbox">仿真化验单</a></div> <div class="detail_history"> @@ -450,7 +450,7 @@ width参数为弹出层页面宽度+2, card_id参数为 "卡片id" {% endcomment %} - <a class="card_delete_icon card_delete thickbox" href="{% url indicator_deletecardtip %}?card_id={{ ind|dict_get:"id" }}&url_type=html&no_title=true&TB_iframe=true&height=166&width=630"></a> + <a class="card_delete_icon card_delete thickbox" href="{% url indicator_deletecardtip %}?card_id={{ ind|dict_get:"id" }}&url_type=html&no_title=true&TB_iframe=true&height=166&width=630"></a> </div> <!-- end: index_card --> {% endfor %} {# end: indicators #} diff --git a/97suifangqa/apps/info/fixtures/initial_data.json b/97suifangqa/apps/info/fixtures_bak/initial_data.json index 4440490..4440490 100644 --- a/97suifangqa/apps/info/fixtures/initial_data.json +++ b/97suifangqa/apps/info/fixtures_bak/initial_data.json diff --git a/97suifangqa/apps/location/fixtures/initial_data.json b/97suifangqa/apps/location/fixtures_bak/initial_data.json index a06ec34..a06ec34 100644 --- a/97suifangqa/apps/location/fixtures/initial_data.json +++ b/97suifangqa/apps/location/fixtures_bak/initial_data.json diff --git a/97suifangqa/apps/profile/models.py b/97suifangqa/apps/profile/models.py index 2168f39..b926d31 100644 --- a/97suifangqa/apps/profile/models.py +++ b/97suifangqa/apps/profile/models.py @@ -10,6 +10,8 @@ from .storage import OverwriteStorage from .utils import avatar_by_user from .image import crop +from sfaccount import models as am + class Profile(models.Model): @@ -25,11 +27,14 @@ class Profile(models.Model): (3, u"硕士"), (4, u"博士")) - user = models.OneToOneField(User, null=True, blank=True) - name = models.CharField(u"用户名", max_length=20, null=True, blank=True) - avatar = models.ImageField(u"头像", upload_to="uploads/avatar/", storage=OverwriteStorage()) + account = models.OneToOneField(am.Account, verbose_name=u"账户") + screen_name = models.CharField(u"显示名称", + max_length=15, blank=True) + #user = models.OneToOneField(User, null=True, blank=True) + #name = models.CharField(u"用户名", max_length=20, null=True, blank=True) + #avatar = models.ImageField(u"头像", upload_to="uploads/avatar/", storage=OverwriteStorage()) education = models.IntegerField(u"学历", choices=education_choices) - email = models.EmailField(u"邮箱", primary_key=True) + #email = models.EmailField(u"邮箱", primary_key=True) gender = models.IntegerField(u"性别", choices=gender_choices, default=0) user_level= models.IntegerField(u"等级", default=0) medicines = models.ManyToManyField("medicine.Medicine", related_name="users", verbose_name= u"药物", null=True, blank=True) diff --git a/97suifangqa/apps/profile/urls.py b/97suifangqa/apps/profile/urls.py index cbc453d..e6c396d 100644 --- a/97suifangqa/apps/profile/urls.py +++ b/97suifangqa/apps/profile/urls.py @@ -1,9 +1,10 @@ +# -*- coding: utf-8 -*- + from django.conf.urls import patterns, url -from .views import * -urlpatterns = patterns('', - url(r'^login/?$', login, name = "login"), - url(r'^logout/?$', logout, name = "logout"), - url(r'^signup/?$', signup, name = "signup"), - ) +urlpatterns = patterns('profile.views', + url(r'^(?P<username>[a-zA-Z_][a-zA-Z0-9_]*)/$', + 'profile_view', name='profile_home'), +) + diff --git a/97suifangqa/apps/profile/views.py b/97suifangqa/apps/profile/views.py index c41e62b..e7d17e9 100644 --- a/97suifangqa/apps/profile/views.py +++ b/97suifangqa/apps/profile/views.py @@ -1,30 +1,15 @@ # -*- coding: utf-8 -*- from django.http import HttpResponse, HttpResponseRedirect -from django.conf import settings +from django.conf import settings from django.shortcuts import render -from django.contrib.auth.views import login, logout -from django.contrib.auth import login as auth_login -from .forms import UserCreationForm - - -def signup(request): - u''' - 用户注册 - ''' - if request.user.is_authenticated(): - return HttpResponseRedirect(settings.LOGIN_REDIRECT_URL) - - if request.method == 'POST': - form = UserCreationForm(request.POST) - if form.is_valid(): - user = form.save() - return HttpResponseRedirect(request.REQUEST.get('next')) - else: - form = UserCreationForm() - - return render(request, 'registration/signup.html', - locals()) +# profile home {{{ +def profile_view(request, username): + """ + show profile of given user + """ + return HttpResponse('Hi, %s' % username) +# }}} diff --git a/97suifangqa/templates/registration/logout.html b/97suifangqa/apps/recommend/__init__.py index e69de29..e69de29 100644 --- a/97suifangqa/templates/registration/logout.html +++ b/97suifangqa/apps/recommend/__init__.py diff --git a/97suifangqa/apps/recommend/models.py b/97suifangqa/apps/recommend/models.py new file mode 100644 index 0000000..98ae64d --- /dev/null +++ b/97suifangqa/apps/recommend/models.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- + +""" +models for apps/recommend +""" + + +from django.db import models +from django.contrib import admin + + +class TreatRespnse(models.Model): # {{{ + """ + 治疗反应/结果的描述,以及结果的价值/权重 + """ + name = models.CharField(u"名称", max_length=100) + description = models.TextField(u"详细描述", blank=True) + weight = models.FloatField(u"权重", help_text=u"范围:0-10") + # datetime + created_at = models.DateTimeField(u"创建时间", auto_now_add=True) + updated_at = models.DateTimeField(u"更新时间", + auto_now_add=True, auto_now=True) + + class Meta: + verbose_name_plural = u"治疗反应" + + def __unicode__(self): + return u"< TreatRespnse: %s >" % self.name + + def save(self, **kwargs): + if self.is_valid(): + super(TreatRespnse, self).save(**kwargs) + else: + return self + + def is_valid(self, **kwargs): + # check weight range + if (self.weight < 0.0) or (self.weight > 10.0): + print u"Error: weight < 0.0 / weight > 10.0" + return False + # + return True +# }}} + + +# admin +admin.site.register([ + TreatRespnse, +]) + diff --git a/97suifangqa/apps/recommend/tests.py b/97suifangqa/apps/recommend/tests.py new file mode 100644 index 0000000..501deb7 --- /dev/null +++ b/97suifangqa/apps/recommend/tests.py @@ -0,0 +1,16 @@ +""" +This file demonstrates writing tests using the unittest module. These will pass +when you run "manage.py test". + +Replace this with more appropriate tests for your application. +""" + +from django.test import TestCase + + +class SimpleTest(TestCase): + def test_basic_addition(self): + """ + Tests that 1 + 1 always equals 2. + """ + self.assertEqual(1 + 1, 2) diff --git a/97suifangqa/apps/recommend/urls.py b/97suifangqa/apps/recommend/urls.py new file mode 100644 index 0000000..09dfed4 --- /dev/null +++ b/97suifangqa/apps/recommend/urls.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- + +""" +URL configuration for apps/recommend +""" + +from django.conf.urls.defaults import * + +from recommend import models as rm + + +urlpatterns = patterns('recommend.views', + # app index + url(r'^$', + 'recommend_index', + name='recommend_index'), +) + diff --git a/97suifangqa/apps/recommend/views.py b/97suifangqa/apps/recommend/views.py new file mode 100644 index 0000000..02d5616 --- /dev/null +++ b/97suifangqa/apps/recommend/views.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- + +""" +views for apps/recommend +""" + +from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden, Http404 +from django.shortcuts import render, get_object_or_404 + + +# index +def recommend_index(request): + """ + index view for apps/recommend + """ + return HttpResponse("recommend index") + diff --git a/97suifangqa/apps/sciblog/fixtures/initial_data.json b/97suifangqa/apps/sciblog/fixtures_bak/initial_data.json index 6038ffe..6038ffe 100644 --- a/97suifangqa/apps/sciblog/fixtures/initial_data.json +++ b/97suifangqa/apps/sciblog/fixtures_bak/initial_data.json diff --git a/97suifangqa/apps/sfaccount/README.txt b/97suifangqa/apps/sfaccount/README.txt new file mode 100644 index 0000000..44eda9e --- /dev/null +++ b/97suifangqa/apps/sfaccount/README.txt @@ -0,0 +1,23 @@ +using 'django-celery' and 'redis' to implement the function +of 'async sending email' with the activation key for +newly registered user. + +REF: +(1) use Celery in Django with a Redis backend + http://killtheyak.com/django-celery-redis/ + +HOWTO run: +1) pip install django-celery redis +2) OS install package 'redis' (maybe 'redis-server') +3) add 'djcelery' to 'INSTALLED_APPS' +4) add settings for 'redis' & 'djcelery' in 'settings.py' + SF_MAIL +5) system: $ redis-server +6) ./manage.py syncdb +7) ./manage.py celeryd worker -E + +TEST: +a) ./manage.py shell + >>> from sfaccount.tasks import send_mail + >>> send_mail(to, subject, body) + diff --git a/97suifangqa/templates/registration/password_change_done.html b/97suifangqa/apps/sfaccount/__init__.py index e69de29..e69de29 100644 --- a/97suifangqa/templates/registration/password_change_done.html +++ b/97suifangqa/apps/sfaccount/__init__.py diff --git a/97suifangqa/apps/sfaccount/forms.py b/97suifangqa/apps/sfaccount/forms.py new file mode 100644 index 0000000..d2a3bf1 --- /dev/null +++ b/97suifangqa/apps/sfaccount/forms.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- + +from django import forms +from django.template import loader +from django.utils.http import int_to_base36 +from django.contrib.auth.models import User +from django.contrib.auth.tokens import default_token_generator +from django.contrib.sites.models import get_current_site + +from django.utils.translation import ugettext, ugettext_lazy as _ + +from sfaccount.tasks import send_mail + +import re + + +# AccountForm {{{ +class AccountForm(forms.Form): + """ + form for signing up a new account + """ + username = forms.RegexField(regex=r'^[A-Za-z0-9_-]+$', + max_length=30, label=u"用户名", + help_text=u"由字母、数字和下划线组成,长度6-30位", + error_messages={'invalid': u"用户名仅能包含字母、数字和下划线"}, + ) + email = forms.EmailField(max_length=75, label=u"邮箱") + password1 = forms.CharField(label=u"密码", max_length=30, + help_text=u"密码长度6-30位", + widget=forms.PasswordInput) + password2 = forms.CharField(label=u"确认密码", max_length=30, + widget=forms.PasswordInput) + + def clean_username(self): + username = self.cleaned_data['username'] + # check length + if len(username) < 6: + raise forms.ValidationError(u'用户名长度需大于6位') + # check first letter + p = re.compile('[a-zA-Z_]') + if p.match(username[0]): + pass + else: + raise forms.ValidationError(u'首字母必须是字母或下划线') + # check if exists + try: + User.objects.get(username=username) + except User.DoesNotExist: + return username + raise forms.ValidationError(u'用户名已经被占用') + + def clean_email(self): + try: + User.objects.get(email__iexact=self.cleaned_data['email']) + except User.DoesNotExist: + return self.cleaned_data['email'] + raise forms.ValidationError(u'邮箱地址已经被占用') + + def clean_password1(self): + password1 = self.cleaned_data['password1'] + if len(password1) < 6: + raise forms.ValidationError(u'密码长度需大于6位') + return password1 + + def clean(self): + cd = self.cleaned_data + if 'password1' in cd and 'password2' in cd: + if cd['password1'] != cd['password2']: + raise forms.ValidationError(u'两次输入的密码不一致') + # + return cd +# }}} + + +# SFPasswordResetForm {{{ +class SFPasswordResetForm(forms.Form): + """ + to replace django's 'PasswordResetForm' + to use djcelery's async send mail + """ + error_messages = { + 'unknown': _("That e-mail address doesn't have an associated " + "user account. Are you sure you've registered?"), + 'unusable': _("The user account associated with this e-mail " + "address cannot reset the password."), + } + email = forms.EmailField(label=_("E-mail"), max_length=75) + + def save(self, domain_override=None, + subject_template_name='registration/password_reset_subject.txt', + email_template_name='registration/password_reset_email.txt', + use_https=False, token_generator=default_token_generator, + from_email=None, request=None, + html_email_template_name=None): + """ + Generates a one-use only link for resetting password + and sends to the user. + """ + # validate first + if not self.is_valid(): + return self + # validated: has 'self.cleaned_data' + email = self.cleaned_data['email'] + users = User.objects.filter(email__iexact=email) + if not len(users): + raise forms.ValidationError(self.error_messages['unknown']) + for user in users: + # make sure that no email is sent to a user that actually + # has a password marked as unusable + if not user.has_usable_password(): + continue + if not domain_override: + current_site = get_current_site(request) + site_name = current_site.name + domain = current_site.domain + else: + site_name = domain = domain_override + c = { + 'email': user.email, + 'domain': domain, + 'site_name': site_name, + 'uid': int_to_base36(user.id), + 'user': user, + 'token': token_generator.make_token(user), + 'protocol': 'https' if use_https else 'http', + } + subject = loader.render_to_string(subject_template_name, c) + # Email subject *must not* contain newlines + subject = ''.join(subject.splitlines()) + body_text = loader.render_to_string(email_template_name, c) + # html email + if html_email_template_name: + body_html = loader.render_to_string(html_email_template_name, c) + else: + body_html = None + # send mail + to = user.email + send_mail(to, subject, body_text, body_html) +# }}} + + diff --git a/97suifangqa/apps/sfaccount/functional/__init__.py b/97suifangqa/apps/sfaccount/functional/__init__.py new file mode 100644 index 0000000..6cfbcbf --- /dev/null +++ b/97suifangqa/apps/sfaccount/functional/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +from django.conf import settings +from sfaccount.functional.mail import send_mail_multipart + +EMAIL = settings.SF_EMAIL + +def send_mail(to, subject, content_text=None, content_html=None): + send_mail_multipart( + host=EMAIL['smtp_host'], + port=EMAIL['smtp_port'], + username=EMAIL['username'], + password=EMAIL['password'], + mail_from=EMAIL['from'], + mail_to=to, + subject=subject, + content_text=content_text, + content_html=content_html, + display_from=EMAIL['display_from'] + ) + diff --git a/97suifangqa/apps/sfaccount/functional/mail.py b/97suifangqa/apps/sfaccount/functional/mail.py new file mode 100644 index 0000000..30b1701 --- /dev/null +++ b/97suifangqa/apps/sfaccount/functional/mail.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- + +import smtplib + +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart + + +def send_mail_multipart(host, + port, + username, + password, + mail_from, + mail_to, + subject, + content_text=None, + content_html=None, + display_from=None): + # create message container + # correct MIME type is 'multipart/alternative' + msg = MIMEMultipart('alternative') + # from & to + msg['From'] = display_from or mail_from + if isinstance(mail_to, (list, tuple)): + msg['To'] = ', '.join(mail_to) + else: + msg['To'] = mail_to + # subject + msg['Subject'] = subject + # body (utf-8 encode required) + if isinstance(content_text, unicode): + content_text = content_text.encode('utf-8') + if isinstance(content_html, unicode): + content_html = content_html.encode('utf-8') + text_part = MIMEText(content_text, 'plain') + html_part = MIMEText(content_html, 'html') + msg.attach(text_part) + msg.attach(html_part) + # send + s = smtplib.SMTP() + s.connect(host, port) + s.login(username, password) + s.sendmail(mail_from, mail_to, msg.as_string()) + s.quit() + diff --git a/97suifangqa/templates/registration/password_change_form.html b/97suifangqa/apps/sfaccount/management/__init__.py index e69de29..e69de29 100644 --- a/97suifangqa/templates/registration/password_change_form.html +++ b/97suifangqa/apps/sfaccount/management/__init__.py diff --git a/97suifangqa/templates/registration/password_reset_complete.html b/97suifangqa/apps/sfaccount/management/commands/__init__.py index e69de29..e69de29 100644 --- a/97suifangqa/templates/registration/password_reset_complete.html +++ b/97suifangqa/apps/sfaccount/management/commands/__init__.py diff --git a/97suifangqa/apps/sfaccount/management/commands/cleanupaccounts.py b/97suifangqa/apps/sfaccount/management/commands/cleanupaccounts.py new file mode 100644 index 0000000..c79e037 --- /dev/null +++ b/97suifangqa/apps/sfaccount/management/commands/cleanupaccounts.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- + +""" +A management command which deletes expired accounts (e.g., +accounts which signed up but never activated) from the database. + +Calls ``Account.objects.delete_expired_accounts()'', +which contains the actual logic for determining which +accounts are deleted. +""" + +from django.core.management.base import NoArgsCommand + +from accounts.models import Account + + +class Command(NoArgsCommand): + help = "Delete expired accounts from the database" + + def handle_noargs(self, **options): + Account.objects.delete_expired_accounts() + diff --git a/97suifangqa/apps/sfaccount/models.py b/97suifangqa/apps/sfaccount/models.py new file mode 100644 index 0000000..bb1fe29 --- /dev/null +++ b/97suifangqa/apps/sfaccount/models.py @@ -0,0 +1,172 @@ +# -*- coding: utf-8 -*- + +from django.conf import settings +from django.db import models +from django.contrib import admin +from django.contrib.auth.models import User +from django.utils.hashcompat import sha_constructor +from django.utils.timezone import utc +from django.template.loader import render_to_string + +from sfaccount.tasks import send_mail + +import re +import random +import datetime + + +# SHA1 Hash regex +SHA1 = re.compile('^[a-f0-9]{40}$') + + +class AccountManager(models.Manager): # {{{ + """ + custom manager for 'Account' model + """ + def activate(self, activation_key): + """ + validate an activation key and activate the corresponding + 'User' if valid. + + if the key is valid and not expired, return the 'Account' + if the key is invalid or expired, return 'False' + if the key is valid but the 'User' is already activated, + return 'False' + + reset the key string to prevent reactivation of an account + which has been deactivated by the admin + """ + if SHA1.search(activation_key): + try: + account = self.get(activation_key=activation_key) + except self.model.DoesNotExist: + return False + if not account.activation_key_expired(): + user = account.user + user.is_active = True + user.save() + account.activation_key = self.model.ACTIVATED + account.save() + return account + return False + + def create_inactive_account(self, username, email, password, + send_email=True): + """ + create a new, *local*, inactive 'User', + and generate an 'Account' and + email the activation key. return the new 'User' + + the activation key is a SHA1 hash, generated from + a combination of the 'username' and a random slat + """ + new_user = User.objects.create_user(username, email, password) + new_user.is_active = False + new_user.save() + # create corresponding 'Account' + salt = sha_constructor(str(random.random())).hexdigest()[:5] + activation_key = sha_constructor(salt+username).hexdigest() + new_account = self.create(user=new_user, is_social=False, + activation_key=activation_key) + new_account.save() + # send email + if send_email: + new_account.send_activation_email() + return new_account + + def delete_expired_accounts(self): + """ + Remove expired instances of 'Account's and their + associated 'User's. + """ + for account in self.all(): + if account.activation_key_expired(): + user = account.user + if not user.is_active: + user.delete() + account.delete() +# }}} + + +class Account(models.Model): # {{{ + """ + Account model for 97suifang + """ + ACTIVATED = u'ALREADY_ACTIVATED' + + user = models.OneToOneField(User, related_name="account") + # username -> user.username + # date_joined -> user.date_joined + screen_name = models.CharField(u"昵称", max_length=30, + null=True, blank=True) + avatar = models.ImageField(u"头像", upload_to="uploads/avatars/", + null=True, blank=True) + # if social account + is_social = models.BooleanField(default=False) + # activation (SHA1 hash) + activation_key = models.CharField(u"激活密钥", max_length=40) + + objects = AccountManager() + + class Meta: + verbose_name_plural = u"账户信息" + + def __unicode__(self): + if self.is_social: + type = u"social" + else: + type = u"local" + if self.user.is_active: + state = u"activated" + else: + state = u"nonactivated" + # + return u'< Account: %s, %s, %s >' % (self.user.username, + type, state) + + def activation_key_expired(self): + """ + determine whether the activation key has expired + + Key expiration is determined by a two-step process: + + 1. If the user has already activated, the key will have been + reset to the string constant ``ACTIVATED``. Re-activating + is not permitted, and so this method returns ``True`` in + this case. + + 2. Otherwise, the date the user signed up is incremented by + the number of days specified in the setting + ``ACCOUNT_ACTIVATION_DAYS`` (which should be the number of + days after signup during which a user is allowed to + activate their account); if the result is less than or + equal to the current date, the key has expired and this + method returns ``True``. + """ + expiration_days = datetime.timedelta( + days=settings.ACCOUNT_ACTIVATION_DAYS) + now_utc = datetime.datetime.utcnow().replace(tzinfo=utc) + return self.user.is_active or ( + self.user.date_joined + expiration_days <= now_utc) + + def send_activation_email(self): + """ + send an activation email to the newly registered user + """ + ctx_dict = { + 'username': self.user.username, + 'activation_key': self.activation_key, + 'expiration_days': settings.ACCOUNT_ACTIVATION_DAYS, + } + subject = render_to_string('sfaccount/activation_email_subject.txt', ctx_dict) + subject = ''.join(subject.splitlines()) + body_text = render_to_string('sfaccount/activation_email_body.txt', ctx_dict).encode('utf-8') + to = self.user.email + # send email + send_mail.delay(to, subject, body_text, None) +# }}} + + +admin.site.register([Account]) + +# vim: set ts=4 sw=4 tw=0 fenc=utf-8 ft=python: # diff --git a/97suifangqa/apps/sfaccount/tasks.py b/97suifangqa/apps/sfaccount/tasks.py new file mode 100644 index 0000000..94b520a --- /dev/null +++ b/97suifangqa/apps/sfaccount/tasks.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- + +from celery import task + +from sfaccount.functional import send_mail as _send_mail + +@task +def send_mail(to, subject, content_text, content_html): + _send_mail(to, subject, content_text, content_html) + diff --git a/97suifangqa/apps/sfaccount/templates/sfaccount/activate.html b/97suifangqa/apps/sfaccount/templates/sfaccount/activate.html new file mode 100644 index 0000000..a81af6d --- /dev/null +++ b/97suifangqa/apps/sfaccount/templates/sfaccount/activate.html @@ -0,0 +1,98 @@ +{% extends "picture-base.html" %} +{% load staticfiles %} + +{% block title %} +激活账户 | 97 随访 +{% endblock %} + +{% block bodyclasses %}{{ block.super }} registration signup{% endblock %} +{% block othercss %} +<link rel="stylesheet" href="{% static "stylesheets/sass/registration.css" %}"> +{% endblock %} + +{% block scripts %} + <script type="text/javascript"> + // activate account url + var activate_url = '{% url activate %}'; + + $(document).ready(function(){ + // validate key input + $("#activation_key").focus(function() { + $(this).removeClass("valid invalid"); + }); + $("#activation_key").on("validate", null, function() { + var sha1_regex = /^[0-9a-f]{40}$/; + var key_raw = $(this).val(); + var key = htmlEscape(key_raw.toLowerCase()); + if (sha1_regex.test(key)) { + // key in valid format + $(this).removeClass("invalid"); + $(this).addClass("valid"); + } + else { + $(this).removeClass("valid"); + $(this).addClass("invalid"); + } + }); + + // sumbit key to activate account + $("#activate_account").on('submit', null, function() { + // validate key first + $("#activation_key").trigger("validate"); + if ($(".invalid").length) { + // there exists invalid input + return false; + } + else { + // submit + var key_raw = $("#activation_key").val(); + var key = htmlEscape(key_raw.toLowerCase()); + var target_url = activate_url + key + '/'; + //console.log(target_url); + window.location.href = target_url; + } + }); + $("#activate_account").on('click', null, function() { + $(this).trigger('submit'); + }); + $("#activation_key").on('keypress', null, function(e) { + var keycode = (e.keyCode ? e.keyCode : e.which); + if (keycode == 13) { + $("#activate_account").trigger('submit'); + return false; + } + }); + + }); + + function htmlEscape(str) { + return String(str) + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/</g, '<') + .replace(/>/g, '>'); + } + </script> +{% endblock %} + +{% block body %} + <h2>加入97随访   科学了解乙肝治疗</h2> + + <div class="activate_failed" style="display: {% if activate_failed %}block{% else %}none{% endif %} ;"> + <h4>激活账户失败</h4> + 请检查激活码或激活链接。 + <br /> + 您也可以直接在下方输入激活码来完成账户激活。 + </div> + + <div class="activation"> + <span class="prompt">激活码</span> + <input type="text" id="activation_key" /> + <br /> + <input type="button" id="activate_account" value="激活账户" /> + </div> + +{% endblock body %} + +<!-- vim: set ts=8 sw=2 tw=0 fenc=utf-8 ft=htmldjango.html: --> diff --git a/97suifangqa/apps/sfaccount/templates/sfaccount/activation_email_body.txt b/97suifangqa/apps/sfaccount/templates/sfaccount/activation_email_body.txt new file mode 100644 index 0000000..32be3e9 --- /dev/null +++ b/97suifangqa/apps/sfaccount/templates/sfaccount/activation_email_body.txt @@ -0,0 +1,9 @@ +尊敬的 {{ username }}, + +感谢您注册97随访(97suifang.com)。 + +您的激活码为 {{ activation_key }},请在 {{ expiration_days }} 天内激活账户,直接打开以下链接进行激活: +http://www.97suifang.com/accounts/activate/{{ activation_key }}/ + + +97随访 diff --git a/97suifangqa/apps/sfaccount/templates/sfaccount/activation_email_subject.txt b/97suifangqa/apps/sfaccount/templates/sfaccount/activation_email_subject.txt new file mode 100644 index 0000000..9a9a040 --- /dev/null +++ b/97suifangqa/apps/sfaccount/templates/sfaccount/activation_email_subject.txt @@ -0,0 +1 @@ +97随访(97suifang.com)账户激活 diff --git a/97suifangqa/apps/sfaccount/templates/sfaccount/login.html b/97suifangqa/apps/sfaccount/templates/sfaccount/login.html new file mode 100644 index 0000000..f5c7942 --- /dev/null +++ b/97suifangqa/apps/sfaccount/templates/sfaccount/login.html @@ -0,0 +1,57 @@ +{% extends "picture-base.html" %} +{% load staticfiles %} +{% block bodyclasses %}{{ block.super }} registration login{% endblock %} +{% block othercss %} +<link rel="stylesheet" href="{% static "stylesheets/sass/registration.css" %}"> +{% endblock %} + +{% block title %} +登录 | 97随访 +{% endblock %} + +{% block body %} +<h2>加入97随访   科学了解乙肝治疗</h2> +<p></p> +<p> + <img src="{% static "images/sinalogo.png" %}" alt="" style="width: 2.5em; vertical-align: middle;"> + 新浪微博账号登录 + </p> + + <p> + <img src="{% static "images/qqlogo.png" %}" alt="" style="width: 2.5em; vertical-align: middle;"> + 腾讯QQ账号登录 + </p> + + <form method="post">{% csrf_token %} + <table class="reg-form login-form"> + <tr> + <!-- + <td class="login-prompt"> + <span class="prompt">或者直接用邮箱登陆</span> + </td> + <td></td> + --> + </tr> + <tr> + <td> + <input type="text" name="{{ form.username.name }}" value="{{ form.username.value|default_if_none:"" }}" maxlength="80" placeholder="{{ form.username.label }}" class="username "> + </td> + <td rowspan="2" class="error"> + {{form.non_field_errors|first}} + </td> + </tr> + <tr> + <td><input type="password" name="{{ form.password.name }}" placeholder="{{ form.password.label }}" class="password"></td> + </tr> + </table> + + <input type="hidden" name="next" value="{{ next }}" /> + <input type="submit" value="登 录" class="submit login"/> +   |   + <a href="{% url signup %}">还没有帐号?</a> +   |   + <a href="{% url password_reset %}">忘记密码?</a> + </form> +{% endblock body%} + +{# vim: set ts=2 sw=2 tw=0 fenc=utf-8 ft=htmldjango.html: #} diff --git a/97suifangqa/apps/sfaccount/templates/sfaccount/logout.html b/97suifangqa/apps/sfaccount/templates/sfaccount/logout.html new file mode 100644 index 0000000..0d05ae5 --- /dev/null +++ b/97suifangqa/apps/sfaccount/templates/sfaccount/logout.html @@ -0,0 +1,35 @@ +{% extends "picture-base.html" %} +{% load staticfiles %} + +{% block title %} +注销 | 97 随访 +{% endblock %} + +{% block othercss %} +<link rel="stylesheet" href="{% static "stylesheets/sass/registration.css" %}"> +{% endblock %} + +{% block scripts %} + <script type="text/javascript"> + // login url + var login_url = '{% url login %}'; + + $(document).ready(function() { + $("#re-login").bind("click", function() { + window.location.href = login_url; + }); + }); + </script> +{% endblock %} + +{% block body %} + <h2>加入97随访   科学了解乙肝治疗</h2> + + <p>感谢您使用97随访!</p> + + <p> + <input type="button" id="re-login" value="重新登录" /> + </p> +{% endblock body %} + +<!-- vim: set ts=8 sw=2 tw=0 fenc=utf-8 ft=htmldjango.html: --> diff --git a/97suifangqa/apps/sfaccount/templates/sfaccount/password_change.html b/97suifangqa/apps/sfaccount/templates/sfaccount/password_change.html new file mode 100644 index 0000000..7918016 --- /dev/null +++ b/97suifangqa/apps/sfaccount/templates/sfaccount/password_change.html @@ -0,0 +1,32 @@ +{% extends "picture-base.html" %} +{% load staticfiles %} +{% block bodyclasses %}{{ block.super }} registration login{% endblock %} +{% block othercss %} +<link rel="stylesheet" href="{% static "stylesheets/sass/registration.css" %}"> +{% endblock %} + +{% block title %} +修改密码 | 97随访 +{% endblock %} + +{% block body %} + <h2>加入97随访   科学了解乙肝治疗</h2> + + <h3>修改密码</h3> + <form action="" method="post">{% csrf_token %} + <table class="change-password-form login-form"> + {% for item in form %} + <tr> + <td> + <input type="{{ item.field.widget.input_type }}" name="{{ item.name }}" {% if item.field.widget.input_type == "text" %}value="{{ item.value|default_if_none:"" }}"{% endif %} placeholder="{{ item.label }}" class="{{ item.name }}" /> + </td> + <td class="error">{{ item.errors|join:"" }}</td> + </tr> + {% endfor %} + </table> + + <input type="submit" value="修改密码" class="submit change-password" /> + </form> +{% endblock body%} + +{# vim: set ts=2 sw=2 tw=0 fenc=utf-8 ft=htmldjango.html: #} diff --git a/97suifangqa/apps/sfaccount/templates/sfaccount/password_change_done.html b/97suifangqa/apps/sfaccount/templates/sfaccount/password_change_done.html new file mode 100644 index 0000000..ed91216 --- /dev/null +++ b/97suifangqa/apps/sfaccount/templates/sfaccount/password_change_done.html @@ -0,0 +1,35 @@ +{% extends "picture-base.html" %} +{% load staticfiles %} + +{% block title %} +修改密码 | 97 随访 +{% endblock %} + +{% block othercss %} +<link rel="stylesheet" href="{% static "stylesheets/sass/registration.css" %}"> +{% endblock %} + +{% block scripts %} + <script type="text/javascript"> + // login url + var home_url = '{% url go_home %}'; + + $(document).ready(function() { + $("#go-home").bind("click", function() { + window.location.href = home_url; + }); + }); + </script> +{% endblock %} + +{% block body %} + <h2>加入97随访   科学了解乙肝治疗</h2> + + <h4>密码修改成功!</h4> + + <p> + <input type="button" id="go-home" value="返回个人主页" /> + </p> +{% endblock body %} + +<!-- vim: set ts=8 sw=2 tw=0 fenc=utf-8 ft=htmldjango.html: --> diff --git a/97suifangqa/apps/sfaccount/templates/sfaccount/password_reset.html b/97suifangqa/apps/sfaccount/templates/sfaccount/password_reset.html new file mode 100644 index 0000000..87421d3 --- /dev/null +++ b/97suifangqa/apps/sfaccount/templates/sfaccount/password_reset.html @@ -0,0 +1,36 @@ +{% extends "picture-base.html" %} +{% load staticfiles %} + +{% block title %} +重设密码 | 97 随访 +{% endblock %} + +{% block othercss %} +<link rel="stylesheet" href="{% static "stylesheets/sass/registration.css" %}"> +{% endblock %} + +{% block body %} + <h2>加入97随访   科学了解乙肝治疗</h2> + + <p> + 忘记了密码? + </p> + <p> + 请在下面输入您注册时使用的邮箱地址, + 我们将把重设密码的链接通过邮件发给您。 + </p> + + <form action="" method="post">{% csrf_token %} + <table class="password-reset"> + <tr> + <td> + <input type="text" name="email" value="{{ form.email.value|default_if_none:"" }}" placeholder="您注册时的邮箱地址" class="email"> + </td> + <td class="error">{{ form.email.errors|join:"" }}</td> + </tr> + </table> + <input type="submit" value="申请重设密码" /> + </form> +{% endblock %} + +<!-- vim: set ts=8 sw=2 tw=0 fenc=utf-8 ft=htmldjango.html: --> diff --git a/97suifangqa/apps/sfaccount/templates/sfaccount/password_reset_complete.html b/97suifangqa/apps/sfaccount/templates/sfaccount/password_reset_complete.html new file mode 100644 index 0000000..2027cd2 --- /dev/null +++ b/97suifangqa/apps/sfaccount/templates/sfaccount/password_reset_complete.html @@ -0,0 +1,37 @@ +{% extends "picture-base.html" %} +{% load staticfiles %} + +{% block title %} +重设密码 | 97 随访 +{% endblock %} + +{% block othercss %} +<link rel="stylesheet" href="{% static "stylesheets/sass/registration.css" %}"> +{% endblock %} + +{% block scripts %} + <script type="text/javascript"> + // login url + var login_url = '{% url login %}'; + + $(document).ready(function() { + $("#login").bind("click", function() { + window.location.href = login_url; + }); + }); + </script> +{% endblock %} + +{% block body %} + <h2>加入97随访   科学了解乙肝治疗</h2> + + <h4>重设密码成功!</h4> + + <p> + 您的密码已经重新设置,现在您可以继续登录账户。 + </p> + + <input type="button" id="login" value="登录账户" /> +{% endblock %} + +<!-- vim: set ts=8 sw=2 tw=0 fenc=utf-8 ft=htmldjango.html: --> diff --git a/97suifangqa/apps/sfaccount/templates/sfaccount/password_reset_confirm.html b/97suifangqa/apps/sfaccount/templates/sfaccount/password_reset_confirm.html new file mode 100644 index 0000000..8522af5 --- /dev/null +++ b/97suifangqa/apps/sfaccount/templates/sfaccount/password_reset_confirm.html @@ -0,0 +1,53 @@ +{% extends "picture-base.html" %} +{% load staticfiles %} + +{% block title %} +重设密码 | 97随访 +{% endblock %} + +{% block othercss %} +<link rel="stylesheet" href="{% static "stylesheets/sass/registration.css" %}"> +{% endblock %} + +{% block scripts %} + <script type="text/javascript"> + // password_reset url + var password_reset_url = '{% url password_reset %}'; + + $(document).ready(function() { + $("#password-reset").bind("click", function() { + window.location.href = password_reset_url; + }); + }); + </script> +{% endblock %} + +{% block body %} + <h2>加入97随访   科学了解乙肝治疗</h2> + + {% if validlink %} + <h4>请设置新密码</h4> + + <form action="" method="post">{% csrf_token %} + <table class="password-reset"> + {% for item in form %} + <tr> + <td> + <input type="{{ item.field.widget.input_type }}" name="{{ item.name }}" {% if item.field.widget.input_type == "text" %}value="{{ item.value|default_if_none:"" }}"{% endif %} placeholder="{{ item.label }}" class="{{ item.name }}"> + </td> + <td class="error">{{ item.errors|join:"" }}</td> + </tr> + {% endfor %} + </table> + <input type="submit" class="sumbit password-reset" value="重设密码" /> + </form> + {% else %} + <h4>重设密码失败</h4> + + <p>您使用的密码重设链接无效,可能因为该链接已被使用过。</p> + <p>您可以尝试重新申请重设密码。</p> + <input type="button" id="password-reset" value="申请重设密码" /> + {% endif %} +{% endblock %} + +<!-- vim: set ts=8 sw=2 tw=0 fenc=utf-8 ft=htmldjango.html: --> diff --git a/97suifangqa/apps/sfaccount/templates/sfaccount/password_reset_done.html b/97suifangqa/apps/sfaccount/templates/sfaccount/password_reset_done.html new file mode 100644 index 0000000..c7bd9a3 --- /dev/null +++ b/97suifangqa/apps/sfaccount/templates/sfaccount/password_reset_done.html @@ -0,0 +1,45 @@ +{% extends "picture-base.html" %} +{% load staticfiles %} + +{% block title %} +重设密码 | 97 随访 +{% endblock %} + +{% block othercss %} +<link rel="stylesheet" href="{% static "stylesheets/sass/registration.css" %}"> +{% endblock %} + +{% block scripts %} + <script type="text/javascript"> + // password_reset url + var password_reset_url = '{% url password_reset %}'; + + $(document).ready(function() { + $("#password-reset").bind("click", function() { + window.location.href = password_reset_url; + }); + }); + </script> +{% endblock %} + +{% block body %} + <h2>加入97随访   科学了解乙肝治疗</h2> + + <h4> + 密码重设邮件已发送! + </h4> + <p> + 我们已经向您提交的邮箱地址发送了密码重设说明, + 请注意查收邮件,并按邮件说明来重新设置密码。 + </p> + + <p> + 还没收到邮件?您可以尝试再次申请重设密码。 + </p> + <p> + <input type="button" id="password-reset" value="申请重设密码" /> + </p> + +{% endblock %} + +<!-- vim: set ts=8 sw=2 tw=0 fenc=utf-8 ft=htmldjango.html: --> diff --git a/97suifangqa/apps/sfaccount/templates/sfaccount/password_reset_email.html b/97suifangqa/apps/sfaccount/templates/sfaccount/password_reset_email.html new file mode 100644 index 0000000..beae46f --- /dev/null +++ b/97suifangqa/apps/sfaccount/templates/sfaccount/password_reset_email.html @@ -0,0 +1,25 @@ +{% autoescape off %} +<html> + <head></head> + <body> + <p>尊敬的 {{ user.username }},</p> + + <p>您收到该邮件是因为您已请求重设97随访({{ domain }})账户的密码。</p> + + <p>请打开以下链接来为您的账户设置新密码:<br /> + {% block reset_link %} + {{ protocol }}://{{ domain }}{% url django.contrib.auth.views.password_reset_confirm uidb36=uid, token=token %} + {% endblock %} + </p> + + <p>您的登录用户名为: {{ user.username }}</p> + <br /> + + + <p>感谢您使用我们的产品!</p> + <br /> + + <p>97随访 团队</p> + </body> +</html> +{% endautoescape %} diff --git a/97suifangqa/apps/sfaccount/templates/sfaccount/password_reset_email.txt b/97suifangqa/apps/sfaccount/templates/sfaccount/password_reset_email.txt new file mode 100644 index 0000000..20c817c --- /dev/null +++ b/97suifangqa/apps/sfaccount/templates/sfaccount/password_reset_email.txt @@ -0,0 +1,13 @@ +尊敬的 {{ user.username }}, + +您收到该邮件是因为您已请求重设97随访({{ domain }})账户的密码。 + +请打开以下链接来为您的账户设置新密码: +{{ protocol }}://{{ domain }}{% url django.contrib.auth.views.password_reset_confirm uidb36=uid, token=token %} + +您的登录用户名为: {{ user.username }} + + +感谢您使用我们的产品! + +97随访 团队 diff --git a/97suifangqa/apps/sfaccount/templates/sfaccount/password_reset_subject.txt b/97suifangqa/apps/sfaccount/templates/sfaccount/password_reset_subject.txt new file mode 100644 index 0000000..b980ba1 --- /dev/null +++ b/97suifangqa/apps/sfaccount/templates/sfaccount/password_reset_subject.txt @@ -0,0 +1 @@ +97随访(97suifang.com)密码重设 diff --git a/97suifangqa/templates/registration/signup.html b/97suifangqa/apps/sfaccount/templates/sfaccount/signup.html index 935dde2..bf6c193 100644 --- a/97suifangqa/templates/registration/signup.html +++ b/97suifangqa/apps/sfaccount/templates/sfaccount/signup.html @@ -1,24 +1,38 @@ {% extends "picture-base.html" %} {% load staticfiles %} + +{% block title %} +注册账户 | 97随访 +{% endblock %} + {% block bodyclasses %}{{ block.super }} registration signup{% endblock %} {% block othercss %} <link rel="stylesheet" href="{% static "stylesheets/sass/registration.css" %}"> {% endblock %} {% block body %} -<h2>注册97随访 科学了解乙肝治疗</h2> -<form method="post">{% csrf_token %} +<h2>加入97随访   科学了解乙肝治疗</h2> +<form action="" method="post">{% csrf_token %} <table class="reg-form register-form"> {% for item in form %} <tr> <td> <input type="{{ item.field.widget.input_type }}" name="{{ item.name }}" {% if item.field.widget.input_type == "text" %}value="{{ item.value|default_if_none:"" }}"{% endif %} placeholder="{{ item.label }}" class="{{ item.name }}"> </td> + <td class="help">{{ item.help_text }}</td> <td class="error">{{ item.errors|join:"" }}</td> </tr> {% endfor %} </table> + <table class="form-errors"> + {% for error in form.non_field_errors %} + <tr> + <td>{{ error }}</td> + </tr> + {% endfor %} + </table> - <input type="submit" value="注册完毕" class="submit register"/>   | -   <a href="{% url apps.profile.views.login %}">已有账号</a> + <input type="submit" value="提交注册" class="submit register"/> +   |   + <a href="{% url login %}">已有账号</a> </form> {% endblock body %} diff --git a/97suifangqa/apps/sfaccount/tests.py b/97suifangqa/apps/sfaccount/tests.py new file mode 100644 index 0000000..501deb7 --- /dev/null +++ b/97suifangqa/apps/sfaccount/tests.py @@ -0,0 +1,16 @@ +""" +This file demonstrates writing tests using the unittest module. These will pass +when you run "manage.py test". + +Replace this with more appropriate tests for your application. +""" + +from django.test import TestCase + + +class SimpleTest(TestCase): + def test_basic_addition(self): + """ + Tests that 1 + 1 always equals 2. + """ + self.assertEqual(1 + 1, 2) diff --git a/97suifangqa/apps/sfaccount/urls.py b/97suifangqa/apps/sfaccount/urls.py new file mode 100644 index 0000000..f2a930b --- /dev/null +++ b/97suifangqa/apps/sfaccount/urls.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- + +from django.core.urlresolvers import reverse +from django.conf import settings +from django.conf.urls import patterns, url +from django.views.generic.simple import direct_to_template + +from django.contrib.auth import views as auth_views + + +urlpatterns = patterns('sfaccount.views', + url(r'^signup/$', 'signup_view', name='signup'), + # activate account + url(r'^activate/$', 'activate_view', name='activate'), + url(r'^activate/(?P<activation_key>.+)/$', + 'activate_view'), + # go home + url(r'^home/$', 'go_home_view', name='go_home'), +) + +urlpatterns += patterns('', + # login & logout + url(r'^login/$', + auth_views.login, + {'template_name': 'sfaccount/login.html'}, + name='login'), + url(r'^logout/$', + auth_views.logout, + {'template_name': 'sfaccount/logout.html'}, + name='logout'), + # change password + url(r'^password/change/$', + auth_views.password_change, + {'template_name': 'sfaccount/password_change.html'}, + name='password_change'), + url(r'^password/change/done/$', + auth_views.password_change_done, + {'template_name': 'sfaccount/password_change_done.html'}, + name='password_change_done'), + # reset password + # use own 'password_reset_view' to able to send multipart mail + # use own 'SFPasswordResetForm' to use 'djcelery' to send email + url(r'^password/reset/$', + 'sfaccount.views.password_reset_view', + name='password_reset'), + url(r'^password/reset/done/$', + auth_views.password_reset_done, + {'template_name': 'sfaccount/password_reset_done.html'}, + name='password_reset_done'), + url(r'^password/reset/confirm/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/$', + auth_views.password_reset_confirm, + {'template_name': 'sfaccount/password_reset_confirm.html'}, + name='password_reset_confirm'), + url(r'^password/reset/complete/$', + auth_views.password_reset_complete, + {'template_name': 'sfaccount/password_reset_complete.html'}, + name='password_reset_complete'), +) + + +USING_SOCIAL_LOGIN = getattr(settings, 'USING_SOCIAL_LOGIN', False) +if USING_SOCIAL_LOGIN: + urlpatterns += patterns('sfaccount.views', + url(r'^oauth/(?P<sitename>\w+)/$', + 'social_login_callback', name='social_login_callback'), + ) + + +# test view +urlpatterns += patterns('', + ## test + url(r'^test/$', + direct_to_template, + { 'template': 'sfaccount/logout.html' }, + name='sfaccount_test'), +) + diff --git a/97suifangqa/apps/sfaccount/views.py b/97suifangqa/apps/sfaccount/views.py new file mode 100644 index 0000000..94670a6 --- /dev/null +++ b/97suifangqa/apps/sfaccount/views.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- + +from django.conf import settings +from django.http import HttpResponse, HttpResponseRedirect +from django.template.response import TemplateResponse +from django.core.urlresolvers import reverse +from django.views.decorators.csrf import csrf_protect +from django.utils.translation import ugettext as _ +from django.shortcuts import render, redirect + +from django.contrib.auth.tokens import default_token_generator + +from sfaccount.models import Account +from sfaccount.forms import AccountForm, SFPasswordResetForm + +# email address shown in the sent mail +FROM_EMAIL = getattr(settings, 'SF_EMAIL').get('display_from') + + +# go_home {{{ +def go_home_view(request): + """ + go to home page (profile) + """ + if request.user.is_authenticated(): + username = request.user.username + return redirect(reverse('profile_home', + kwargs={'username': username})) + else: + # not logged in + return redirect(reverse('login')) +# }}} + + +# signup {{{ +def signup_view(request): + """ + 用户注册 + """ + if request.user.is_authenticated(): + return HttpResponseRedirect(settings.LOGIN_REDIRECT_URL) + + if request.method == 'POST': + form = AccountForm(data=request.POST) + if form.is_valid(): + cd = form.cleaned_data + new_account = Account.objects.create_inactive_account( + username=cd['username'], + email=cd['email'], + password=cd['password1'], + send_email=True + ) + return HttpResponseRedirect(request.REQUEST.get('next')) + else: + form = AccountForm() + + data = { + 'form': form, + } + return render(request, 'sfaccount/signup.html', data) +# }}} + + +# activate {{{ +def activate_view(request, activation_key=None): + """ + activate account + + if activation_key=None, then render a page ask user + to provide the activation key; + otherwise, directly activate the account and redirect + """ + if activation_key: + account = Account.objects.activate(activation_key) + if account: + # activated + home_url = '/profile/%s/' % account.user.username + return HttpResponseRedirect(home_url) + else: + # activate failed + data = {'activate_failed': True} + return render(request, 'sfaccount/activate.html', data) + else: + # ask user for the 'activation_key' + return render(request, 'sfaccount/activate.html') +# }}} + + +# password_reset_view {{{ +# own password_reset_view: enable to send multipart email +@csrf_protect +def password_reset_view(request, is_admin_site=False, + template_name='sfaccount/password_reset.html', + email_template_name='sfaccount/password_reset_email.txt', + subject_template_name='sfaccount/password_reset_subject.txt', + password_reset_form=SFPasswordResetForm, + token_generator=default_token_generator, + post_reset_redirect=None, + from_email=FROM_EMAIL, + current_app=None, + extra_context=None, + html_email_template_name='sfaccount/password_reset_email.html'): + """ + re-write view to replace django's one + able to send multipart email by using + own 'SFPasswordResetForm' and 'send_mail' + """ + if post_reset_redirect is None: + post_reset_redirect = reverse('password_reset_done') + if request.method == "POST": + form = password_reset_form(request.POST) + if form.is_valid(): + opts = { + 'use_https': request.is_secure(), + 'token_generator': token_generator, + 'from_email': from_email, + 'email_template_name': email_template_name, + 'subject_template_name': subject_template_name, + 'request': request, + 'html_email_template_name': html_email_template_name, + } + if is_admin_site: + opts = dict(opts, domain_override=request.get_host()) + form.save(**opts) + return HttpResponseRedirect(post_reset_redirect) + else: + form = password_reset_form() + context = { + 'form': form, + 'title': _('Password reset'), + } + if extra_context is not None: + context.update(extra_context) + return TemplateResponse(request, template_name, context, + current_app=current_app) +# }}} + + +# social_login_callback {{{ +def social_login_callback(request, sitename): + return HttpResponse('%s' % sitename) +# }}} + diff --git a/97suifangqa/env/requirements.pip b/97suifangqa/env/requirements.pip index 686397e..067fcfc 100644 --- a/97suifangqa/env/requirements.pip +++ b/97suifangqa/env/requirements.pip @@ -8,3 +8,6 @@ lxml pysolr uwsgi git+https://github.com/toastdriven/django-haystack.git@master#egg=django-haystack +django-celery +redis +socialoauth diff --git a/97suifangqa/isuifangqa.db b/97suifangqa/isuifangqa.db Binary files differindex 72b3b02..aa285d8 100644 --- a/97suifangqa/isuifangqa.db +++ b/97suifangqa/isuifangqa.db diff --git a/97suifangqa/mail_settings.py.example b/97suifangqa/mail_settings.py.example new file mode 100644 index 0000000..cb8a1e6 --- /dev/null +++ b/97suifangqa/mail_settings.py.example @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- + +## smtp settings to send email +SF_EMAIL = { + 'smtp_host': 'smtp.example.com', + 'smtp_port': 25, + 'username': 'username', + 'password': 'password', + 'from': 'username@example.com', + 'display_from': 'account@97suifang.com', +} + + diff --git a/97suifangqa/settings.py b/97suifangqa/settings.py index 9dd3fc9..3245266 100644 --- a/97suifangqa/settings.py +++ b/97suifangqa/settings.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -import os,sys +import os, sys DEBUG = True @@ -144,6 +144,8 @@ INSTALLED_APPS = ( # Uncomment the next line to enable admin documentation: # 'django.contrib.admindocs', 'haystack', + 'djcelery', + 'sfaccount', 'profile', 'location', 'indicator', @@ -152,15 +154,40 @@ INSTALLED_APPS = ( 'subjects', 'sciblog', 'info', + 'recommend', #'97suifangqa', ) LOGIN_REDIRECT_URL = '/blog/index' -# django-haystack settings +## +ACCOUNT_ACTIVATION_DAYS = 3 + +## avatar +AVATAR_DIR = os.path.join(PROJECT_ROOT, 'uploads/avatars') + +## socialoauth settings +USING_SOCIAL_LOGIN = True +try: + from socialoauth_settings import * +except ImportError: + pass + +## email +BROKER_URL = 'redis://127.0.0.1:6379/0' +CELERY_RESULT_BACKEND = 'redis://127.0.0.1:6379/0' + +import djcelery +djcelery.setup_loader() + +## mail server settings +from mail_settings import * + + +## django-haystack settings from haystack_settings import * -# auto reload when deployed under uWSGI +## auto reload when deployed under uWSGI try: import uwsgi from uwsgidecorators import timer @@ -172,3 +199,5 @@ try: uwsgi.reload() except: pass + + diff --git a/97suifangqa/socialoauth_settings.py b/97suifangqa/socialoauth_settings.py new file mode 100644 index 0000000..f51d5ef --- /dev/null +++ b/97suifangqa/socialoauth_settings.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- + +""" +97suifang local settings +""" + + +## social accounts settings +SOCIALOAUTH_SITES = ( + ('weibo', 'socialoauth.sites.weibo.Weibo', '新浪微博', { + 'redirect_uri': 'http://www.97suifang.com/account/oauth/weibo', + 'client_id': 'weibo_app_id', + 'client_secret': 'weibo_app_secret', + } + ), +) + diff --git a/97suifangqa/templates/base.html b/97suifangqa/templates/base.html index ae80e81..a93f207 100644 --- a/97suifangqa/templates/base.html +++ b/97suifangqa/templates/base.html @@ -80,7 +80,7 @@ {% block js %} {% block jquery %} - <script src="{% static "javascripts/jquery-1.9.1.min.js" %}"></script> + <script type="text/javascript" src="{% static "javascripts/jquery-1.9.1.min.js" %}"></script> {% endblock %} {% block scripts %} diff --git a/97suifangqa/templates/registration/login.html b/97suifangqa/templates/registration/login.html deleted file mode 100644 index d0e98fc..0000000 --- a/97suifangqa/templates/registration/login.html +++ /dev/null @@ -1,47 +0,0 @@ -{% extends "picture-base.html" %} -{% load staticfiles %} -{% block bodyclasses %}{{ block.super }} registration login{% endblock %} -{% block othercss %} -<link rel="stylesheet" href="{% static "stylesheets/sass/registration.css" %}"> -{% endblock %} -{% block body %} -<h2>登录97随访 科学了解乙肝治疗</h2> -<p></p> -<p> - <img src="{% static "images/sinalogo.png" %}" alt="" style="width: 2.5em; vertical-align: middle;"> - 新浪微博账号登录 -</p> - - -<p> - <img src="{% static "images/qqlogo.png" %}" alt="" style="width: 2.5em; vertical-align: middle;"> - 腾讯QQ账号登录 -</p> - -<form method="post">{% csrf_token %} - - <table class="reg-form login-form"> - <tr> - <td class="login-prompt"> - <span class="prompt">或者直接用邮箱登陆</span> - </td> - <td></td> - </tr> - <tr> - <td> - <input type="text" name="{{ form.username.name }}" value="{{ form.username.value|default_if_none:"" }}" maxlength="80" placeholder="{{ form.username.label }}" class="username "> - </td> - <td rowspan="2" class="error"> - {{form.non_field_errors|first}} - </td> - </tr> - <tr> - <td><input type="password" name="{{ form.password.name }}" placeholder="{{ form.password.label }}" class="password"></td> - </tr> - </table> - - <input type="hidden" name="next" value="{{ next }}" /> - <input type="submit" value="登 录" class="submit login"/>   |   - <a href="{% url profile.views.signup %}">还没有帐号?</a> -</form> -{% endblock body%} diff --git a/97suifangqa/templates/registration/password_reset_confirm.html b/97suifangqa/templates/registration/password_reset_confirm.html deleted file mode 100644 index e69de29..0000000 --- a/97suifangqa/templates/registration/password_reset_confirm.html +++ /dev/null diff --git a/97suifangqa/templates/registration/password_reset_done.html b/97suifangqa/templates/registration/password_reset_done.html deleted file mode 100644 index e69de29..0000000 --- a/97suifangqa/templates/registration/password_reset_done.html +++ /dev/null diff --git a/97suifangqa/templates/registration/password_reset_email.html b/97suifangqa/templates/registration/password_reset_email.html deleted file mode 100644 index e69de29..0000000 --- a/97suifangqa/templates/registration/password_reset_email.html +++ /dev/null diff --git a/97suifangqa/templates/registration/password_reset_form.html b/97suifangqa/templates/registration/password_reset_form.html deleted file mode 100644 index e69de29..0000000 --- a/97suifangqa/templates/registration/password_reset_form.html +++ /dev/null diff --git a/97suifangqa/templates/registration/password_reset_subject.txt b/97suifangqa/templates/registration/password_reset_subject.txt deleted file mode 100644 index e69de29..0000000 --- a/97suifangqa/templates/registration/password_reset_subject.txt +++ /dev/null diff --git a/97suifangqa/urls.py b/97suifangqa/urls.py index fd7d7a9..12b62da 100644 --- a/97suifangqa/urls.py +++ b/97suifangqa/urls.py @@ -4,7 +4,7 @@ from django.contrib import admin from django.conf.urls.defaults import patterns, include, url from django.views.generic.simple import direct_to_template from django.contrib.auth import logout, views as auth_views -from django.shortcuts import redirect, render_to_response +from django.shortcuts import redirect, render from django.conf import settings @@ -24,26 +24,25 @@ def _logout(request, **kwargs): return redirect('/') urlpatterns += patterns("", - url(r"^$", direct_to_template, {"template": "index.html"}, name="index"), + url(r"^$", direct_to_template, + {"template": "index.html"}, name="index"), ) urlpatterns += patterns("info.views", - url(r"^query\/?$", "query"),) - -urlpatterns += patterns("", - url(r"^blog/", include('sciblog.urls')), - url(r"^accounts/", include('profile.urls')), - ) -def page_not_found(request): - return render_to_response('./templates/404.html') - - + url(r"^query\/?$", "query"), +) -## apps/indicator urlpatterns += patterns('', + url(r'^blog/', include('sciblog.urls')), + url(r'^accounts/', include('sfaccount.urls')), + url(r'^profile/', include('profile.urls')), url(r'^indicator/', include('indicator.urls')), + url(r'^recommend/', include('recommend.urls')), ) +def page_not_found(request): + return render(request, './templates/404.html') + ## search (haystack) urlpatterns += patterns('', |