aboutsummaryrefslogtreecommitdiffstats
path: root/97suifangqa/apps/sfaccount
diff options
context:
space:
mode:
Diffstat (limited to '97suifangqa/apps/sfaccount')
-rw-r--r--97suifangqa/apps/sfaccount/README.txt23
-rw-r--r--97suifangqa/apps/sfaccount/__init__.py0
-rw-r--r--97suifangqa/apps/sfaccount/forms.py141
-rw-r--r--97suifangqa/apps/sfaccount/functional/__init__.py21
-rw-r--r--97suifangqa/apps/sfaccount/functional/mail.py45
-rw-r--r--97suifangqa/apps/sfaccount/management/__init__.py0
-rw-r--r--97suifangqa/apps/sfaccount/management/commands/__init__.py0
-rw-r--r--97suifangqa/apps/sfaccount/management/commands/cleanupaccounts.py22
-rw-r--r--97suifangqa/apps/sfaccount/models.py172
-rw-r--r--97suifangqa/apps/sfaccount/tasks.py10
-rw-r--r--97suifangqa/apps/sfaccount/templates/sfaccount/activate.html98
-rw-r--r--97suifangqa/apps/sfaccount/templates/sfaccount/activation_email_body.txt9
-rw-r--r--97suifangqa/apps/sfaccount/templates/sfaccount/activation_email_subject.txt1
-rw-r--r--97suifangqa/apps/sfaccount/templates/sfaccount/login.html57
-rw-r--r--97suifangqa/apps/sfaccount/templates/sfaccount/logout.html35
-rw-r--r--97suifangqa/apps/sfaccount/templates/sfaccount/password_change.html32
-rw-r--r--97suifangqa/apps/sfaccount/templates/sfaccount/password_change_done.html35
-rw-r--r--97suifangqa/apps/sfaccount/templates/sfaccount/password_reset.html36
-rw-r--r--97suifangqa/apps/sfaccount/templates/sfaccount/password_reset_complete.html37
-rw-r--r--97suifangqa/apps/sfaccount/templates/sfaccount/password_reset_confirm.html53
-rw-r--r--97suifangqa/apps/sfaccount/templates/sfaccount/password_reset_done.html45
-rw-r--r--97suifangqa/apps/sfaccount/templates/sfaccount/password_reset_email.html25
-rw-r--r--97suifangqa/apps/sfaccount/templates/sfaccount/password_reset_email.txt13
-rw-r--r--97suifangqa/apps/sfaccount/templates/sfaccount/password_reset_subject.txt1
-rw-r--r--97suifangqa/apps/sfaccount/templates/sfaccount/signup.html38
-rw-r--r--97suifangqa/apps/sfaccount/tests.py16
-rw-r--r--97suifangqa/apps/sfaccount/urls.py77
-rw-r--r--97suifangqa/apps/sfaccount/views.py143
28 files changed, 1185 insertions, 0 deletions
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/apps/sfaccount/__init__.py b/97suifangqa/apps/sfaccount/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ 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/apps/sfaccount/management/__init__.py b/97suifangqa/apps/sfaccount/management/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/97suifangqa/apps/sfaccount/management/__init__.py
diff --git a/97suifangqa/apps/sfaccount/management/commands/__init__.py b/97suifangqa/apps/sfaccount/management/commands/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ 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, '&amp;')
+ .replace(/"/g, '&quot;')
+ .replace(/'/g, '&#39;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;');
+ }
+ </script>
+{% endblock %}
+
+{% block body %}
+ <h2>加入97随访 &emsp; 科学了解乙肝治疗</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随访 &emsp; 科学了解乙肝治疗</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"/>
+ &emsp; | &emsp;
+ <a href="{% url signup %}">还没有帐号?</a>
+ &emsp; | &emsp;
+ <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随访 &emsp; 科学了解乙肝治疗</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随访 &emsp; 科学了解乙肝治疗</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随访 &emsp; 科学了解乙肝治疗</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随访 &emsp; 科学了解乙肝治疗</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随访 &emsp; 科学了解乙肝治疗</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随访 &emsp; 科学了解乙肝治疗</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随访 &emsp; 科学了解乙肝治疗</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/apps/sfaccount/templates/sfaccount/signup.html b/97suifangqa/apps/sfaccount/templates/sfaccount/signup.html
new file mode 100644
index 0000000..bf6c193
--- /dev/null
+++ b/97suifangqa/apps/sfaccount/templates/sfaccount/signup.html
@@ -0,0 +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随访 &emsp; 科学了解乙肝治疗</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"/>
+ &emsp; | &emsp;
+ <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)
+# }}}
+