├── .gitignore ├── .venv ├── README.md ├── dashboard.py ├── keywordreply ├── __init__.py ├── admin.py ├── models.py ├── tests.py └── views.py ├── lib └── __init__.py ├── manage.py ├── msg.py ├── nginx.conf ├── plugins ├── __init__.py ├── dormlight.py ├── keywordreplyplugin.py ├── sample.py ├── thustudent.py ├── voteplugin.py └── yourls.py ├── requirments.txt ├── response.py ├── vote ├── __init__.py ├── admin.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20140905_0909.py │ ├── 0003_auto_20140905_0912.py │ └── __init__.py ├── models.py ├── templates │ ├── confirm_delete.html │ └── index.html ├── tests.py ├── urls.py └── views.py └── weixin ├── __init__.py ├── messages.py ├── settings.py ├── urls.py ├── utils.py ├── views.py └── wsgi.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.pot 3 | *.pyc 4 | local_settings.py 5 | -------------------------------------------------------------------------------- /.venv: -------------------------------------------------------------------------------- 1 | wx.api 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 微信公众平台+Django 2 | 3 | 详见[官方wiki](http://mp.weixin.qq.com/wiki/index.php) 4 | 5 | ## How to run 6 | 7 | - 在mp.weixin.qq.com申请公众账号,使用“开发模式”,配置`Token`,在`settings.py`中填入自己的`Token` 8 | - 配置服务器地址,`python2 ./manage.py runserver`运行 9 | 10 | ## Plugins 11 | 12 | - 在`plugins`添加文件,参见`sample.py`。 13 | - 在`plugins/__init__.py`的`__all__`中加入添加的插件文件名 14 | -------------------------------------------------------------------------------- /dashboard.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file was generated with the customdashboard management command, it 3 | contains the two classes for the main dashboard and app index dashboard. 4 | You can customize these classes as you want. 5 | 6 | To activate your index dashboard add the following to your settings.py:: 7 | ADMIN_TOOLS_INDEX_DASHBOARD = 'weixin.api.dashboard.CustomIndexDashboard' 8 | 9 | And to activate the app index dashboard:: 10 | ADMIN_TOOLS_APP_INDEX_DASHBOARD = 'weixin.api.dashboard.CustomAppIndexDashboard' 11 | """ 12 | 13 | from django.utils.translation import ugettext_lazy as _ 14 | from django.core.urlresolvers import reverse 15 | 16 | from admin_tools.dashboard import modules, Dashboard, AppIndexDashboard 17 | 18 | 19 | class CustomIndexDashboard(Dashboard): 20 | """ 21 | Custom index dashboard for weixin.api. 22 | """ 23 | def init_with_context(self, context): 24 | self.children.append(modules.LinkList( 25 | _('Vote'), 26 | layout='inline', 27 | children=[ 28 | [_('Chart'), reverse('vote.index')], 29 | [_('Reset'), reverse('vote.delete')], 30 | ] 31 | )) 32 | 33 | # append an app list module for "Applications" 34 | self.children.append(modules.AppList( 35 | _('Applications'), 36 | exclude=('django.contrib.*',), 37 | )) 38 | 39 | # append an app list module for "Administration" 40 | self.children.append(modules.AppList( 41 | _('Administration'), 42 | models=('django.contrib.*',), 43 | )) 44 | 45 | # append a recent actions module 46 | self.children.append(modules.RecentActions(_('Recent Actions'), 5)) 47 | 48 | # append another link list module for "support". 49 | self.children.append(modules.LinkList( 50 | _('Support'), 51 | children=[ 52 | { 53 | 'title': _('Django documentation'), 54 | 'url': 'http://docs.djangoproject.com/', 55 | 'external': True, 56 | }, 57 | { 58 | 'title': _('Django "django-users" mailing list'), 59 | 'url': 'http://groups.google.com/group/django-users', 60 | 'external': True, 61 | }, 62 | { 63 | 'title': _('Django irc channel'), 64 | 'url': 'irc://irc.freenode.net/django', 65 | 'external': True, 66 | }, 67 | ] 68 | )) 69 | 70 | 71 | class CustomAppIndexDashboard(AppIndexDashboard): 72 | """ 73 | Custom app index dashboard for weixin.api. 74 | """ 75 | 76 | # we disable title because its redundant with the model list module 77 | title = '' 78 | 79 | def __init__(self, *args, **kwargs): 80 | AppIndexDashboard.__init__(self, *args, **kwargs) 81 | 82 | # append a model list module and a recent actions module 83 | self.children += [ 84 | modules.ModelList(self.app_title, self.models), 85 | modules.RecentActions( 86 | _('Recent Actions'), 87 | include_list=self.get_app_content_types(), 88 | limit=5 89 | ) 90 | ] 91 | 92 | def init_with_context(self, context): 93 | """ 94 | Use this method if you need to access the request context. 95 | """ 96 | return super(CustomAppIndexDashboard, self).init_with_context(context) 97 | -------------------------------------------------------------------------------- /keywordreply/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blahgeek/weixin-mp/1ab2de079bfef9df1d5ada87d011bc0786e9fcfa/keywordreply/__init__.py -------------------------------------------------------------------------------- /keywordreply/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import KeywordReplyModel, ImageReplyModel 3 | 4 | # Register your models here. 5 | admin.site.register(KeywordReplyModel) 6 | admin.site.register(ImageReplyModel) 7 | -------------------------------------------------------------------------------- /keywordreply/models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding=UTF-8 -*- 3 | 4 | from django.db import models 5 | 6 | # Create your models here. 7 | 8 | class ImageReplyModel(models.Model): 9 | title = models.TextField(help_text='Message title') 10 | description = models.TextField(help_text='Message description') 11 | picurl = models.CharField(max_length=1024, 12 | help_text=u'图片链接,支持JPG、PNG格式,较好的效果为大图360*200,小图200*200') 13 | url = models.CharField(max_length=1024, 14 | help_text=u'点击图文消息跳转链接') 15 | 16 | def __unicode__(self): 17 | return self.title 18 | 19 | 20 | 21 | class KeywordReplyModel(models.Model): 22 | keyword = models.CharField(max_length=255, help_text="Keyword for match", db_index=True) 23 | reply = models.TextField(help_text=u"回复的文本消息,如果下面的图文消息被选中,则该项被忽略") 24 | count = models.IntegerField(default=0, editable=False, 25 | help_text="How many times this keyword was matched, it's automaticlly counted") 26 | 27 | imagereply = models.ManyToManyField(ImageReplyModel) 28 | 29 | def __unicode__(self): 30 | return 'Keyword: %s; Match Count: %d' % (self.keyword, self.count) 31 | 32 | -------------------------------------------------------------------------------- /keywordreply/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /keywordreply/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Created by i@BlahGeek.com at 2014-04-12 4 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "weixin.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /msg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Created by i@BlahGeek.com at 2014-04-12 4 | 5 | NO_HANDLER = 'No handler found.' 6 | NULL_RESPONSE = 'Null response' 7 | HANDLE_ERROR = 'Error when handling.' 8 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | listen [::]:80; 4 | server_name weixin.api.blahgeek.com; 5 | 6 | proxy_intercept_errors on; 7 | 8 | location /static/ { 9 | root /srv/http/weixin.api.blahgeek.com/; 10 | } 11 | 12 | location /favicon.ico { 13 | log_not_found off; 14 | } 15 | 16 | location / { 17 | include uwsgi_params; 18 | uwsgi_pass unix:/srv/http/weixin.api.blahgeek.com/uwsgi.sock; 19 | } 20 | 21 | error_page 404 /404.html; 22 | location = /404.html { 23 | root /usr/share/nginx/html; 24 | } 25 | 26 | # redirect server error pages to the static page /50x.html 27 | # 28 | error_page 500 502 503 504 /50x.html; 29 | location = /50x.html { 30 | root /usr/share/nginx/html; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /plugins/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Created by i@BlahGeek.com at 2014-04-20 4 | 5 | 6 | class PluginMeta(type): 7 | 8 | def __init__(cls, name, bases, attrs): 9 | super(PluginMeta, cls).__init__(name, bases, attrs) 10 | if not hasattr(cls, '_plugins'): 11 | cls._plugins = set() 12 | cls._plugins.add(cls) 13 | cls._plugins -= set(bases) 14 | 15 | __iter__ = lambda cls: iter(cls._plugins) 16 | 17 | 18 | class Plugin(object): 19 | ''' Plugin base class ''' 20 | 21 | __metaclass__ = PluginMeta 22 | 23 | def predict(self, text): 24 | ''' @text: unicode 25 | return: 0 - 100 ''' 26 | raise NotImplementedError() 27 | 28 | def handle(self, text, userid): 29 | ''' @text: unicode 30 | return unicode ''' 31 | raise NotImplementedError() 32 | 33 | __all__ = ['sample', 'keywordreplyplugin', 'voteplugin', 'thustudent', 'dormlight', 'yourls'] 34 | -------------------------------------------------------------------------------- /plugins/dormlight.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Created by i@BlahGeek.com at 2014-11-06 4 | 5 | from . import Plugin 6 | import requests 7 | import random 8 | 9 | class DormLightPlugin(Plugin): 10 | 11 | action = None 12 | 13 | replys = (u'哦', u'嗯', u'好…', u'好了…',) 14 | 15 | ON_URL = 'http://dorm.blahgeek.com:4242/turnon505A' 16 | OFF_URL = 'http://dorm.blahgeek.com:4242/turnoff505A' 17 | 18 | def predict(self, text): 19 | if text == u'求开灯': 20 | self.action = True 21 | return 100 22 | if text == u'求关灯': 23 | self.action = False 24 | return 100 25 | return 0 26 | 27 | def handle(self, text, userid): 28 | try: 29 | requests.get(self.ON_URL if self.action else self.OFF_URL, 30 | allow_redirects=False, 31 | timeout=3) 32 | except requests.Timeout: 33 | return 'Timeout' 34 | return random.choice(self.replys) 35 | 36 | -------------------------------------------------------------------------------- /plugins/keywordreplyplugin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Created by i@BlahGeek.com at 2014-09-05 4 | 5 | from . import Plugin 6 | from keywordreply.models import KeywordReplyModel 7 | 8 | class KeywordReplyPlugin(Plugin): 9 | 10 | reply = None 11 | 12 | def predict(self, text): 13 | text = text.strip() 14 | try: 15 | self.reply = KeywordReplyModel.objects.filter(keyword=text).order_by('?')[0] 16 | except IndexError: 17 | self.reply = None 18 | return 0 19 | return 50 20 | 21 | def handle(self, text, userid): 22 | assert self.reply is not None 23 | self.reply.count += 1 24 | self.reply.save() 25 | 26 | imagereply = self.reply.imagereply 27 | if imagereply.count() == 0: 28 | return self.reply.reply 29 | return [(x.picurl, x.title, x.description, x.url) for x in imagereply.all()[:10]] 30 | -------------------------------------------------------------------------------- /plugins/sample.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Created by i@BlahGeek.com at 2014-04-12 4 | 5 | from . import Plugin 6 | 7 | 8 | class SamplePlugin(Plugin): 9 | 10 | def predict(self, text): 11 | ''' text: unicode 12 | return: number, 0-100 ''' 13 | return 10 14 | 15 | def handle(self, text, userid): 16 | ''' return unicode ''' 17 | return 'I got: %s from %s, Yeah!' % (text, userid) 18 | -------------------------------------------------------------------------------- /plugins/thustudent.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Created by i@BlahGeek.com at 2014-10-11 4 | 5 | import sqlite3 6 | from . import Plugin 7 | import os 8 | import re 9 | 10 | path = os.path.dirname(__file__) 11 | path = os.path.join(path, 'student.sqlite3') 12 | 13 | class THUStudentPlugin(Plugin): 14 | 15 | def predict(self, text): 16 | if text.endswith(u'是谁'): 17 | self.name = re.sub(u'是谁$', '', text) 18 | return 90 19 | if text.startswith(u'谁是'): 20 | self.name = re.sub(u'^谁是', '', text) 21 | return 90 22 | return 0 23 | 24 | def make_str(self, data): 25 | ret = '%s(%s,%s)' % (data[1], data[0], data[7]) 26 | ret += u',' + data[2] + u',' + (data[5] if data[6] is None else data[6]) 27 | ret += u',' + '%s(%s)' % (data[4], data[3]) 28 | ret += u',身份证' + data[8] 29 | if data[9] and data[9] != 'NULL': 30 | ret += u',邮箱' + data[9] 31 | if data[10] and data[10] != 'NULL': 32 | ret += u',人人' + data[10] 33 | return ret 34 | 35 | def handle(self, text, userid): 36 | conn = sqlite3.connect(path) 37 | cursor = conn.cursor() 38 | 39 | name = self.name.strip() 40 | if re.match('^[a-z]+[0-9]+$', name) is not None: 41 | cursor.execute('select * from student where username = ?', (name, )) 42 | elif re.match('^[0-9]{10}$', name) is not None: 43 | cursor.execute('select * from student where id = ?', (name, )) 44 | else: 45 | cursor.execute('select * from student where name = ?', (name, )) 46 | results = cursor.fetchall() 47 | if not results: 48 | return u'未找到结果!' 49 | elif len(results) == 1: 50 | return self.make_str(results[0]) 51 | else: 52 | return u'多个结果:' + u','.join([x[0] for x in results]) 53 | -------------------------------------------------------------------------------- /plugins/voteplugin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Created by i@BlahGeek.com at 2014-09-05 4 | 5 | from . import Plugin 6 | from vote.models import VoteModel, VoteConfiguration, VotedUserModel 7 | 8 | 9 | class VotePlugin(Plugin): 10 | 11 | def __init__(self): 12 | self.conf = VoteConfiguration.objects.get() 13 | self.prefix = self.conf.prefix 14 | 15 | def predict(self, text): 16 | if text.strip().startswith(self.prefix): 17 | return 100 18 | else: 19 | return 0 20 | 21 | def handle(self, text, userid): 22 | if VotedUserModel.objects.filter(userid=userid).count() > 0: 23 | return self.conf.dup_msg 24 | numbers = text.strip().split(' ')[1:] 25 | numbers = filter(lambda x: len(x.strip()), numbers) 26 | try: 27 | numbers = map(int, numbers) 28 | except ValueError: 29 | return self.conf.fail_msg 30 | if len(set(numbers)) != len(numbers) or \ 31 | len(numbers) > self.conf.max_vote_per_user or \ 32 | len(numbers) == 0: 33 | return self.conf.fail_msg 34 | for x in numbers: 35 | if x <= 0 or x > self.conf.candidate_number: 36 | return self.conf.fail_msg 37 | usermodel = VotedUserModel(userid=userid) 38 | usermodel.save() 39 | 40 | for x in numbers: 41 | try: 42 | model = VoteModel.objects.get(candidate=x) 43 | except VoteModel.DoesNotExist: 44 | model = VoteModel(candidate=x) 45 | model.count += 1 46 | model.save() 47 | 48 | return self.conf.ok_msg 49 | -------------------------------------------------------------------------------- /plugins/yourls.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Created by i@BlahGeek.com at 2015-01-18 4 | 5 | from . import Plugin 6 | import requests 7 | import re 8 | 9 | class YOURLSPlugin(Plugin): 10 | 11 | regex = re.compile( 12 | r'^(?:http|ftp)s?://' # http:// or https:// 13 | r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' #domain... 14 | r'localhost|' #localhost... 15 | r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip 16 | r'(?::\d+)?' # optional port 17 | r'(?:/?|[/?]\S+)$', re.IGNORECASE) 18 | 19 | signature = 'f4531f65ca' 20 | url = 'http://blaa.ml/s/yourls-api.php' 21 | 22 | def predict(self, text): 23 | if self.regex.match(text): 24 | return 90 25 | return 0 26 | 27 | def handle(self, text, userid): 28 | try: 29 | r = requests.get(self.url, params={ 30 | 'action': 'shorturl', 31 | 'signature': self.signature, 32 | 'url': text, 33 | 'format': 'json' 34 | }, timeout=5).json() 35 | if not 'shorturl' in r: 36 | return 'Failed! Not a valid URL?' 37 | details = r.get('url', {}) 38 | return '%s -> %s' % (r['shorturl'], 39 | details.get('url')) 40 | except requests.Timeout: 41 | return 'Timeout' 42 | -------------------------------------------------------------------------------- /requirments.txt: -------------------------------------------------------------------------------- 1 | django 2 | django-admin-tools 3 | django-solo 4 | django-bower 5 | uwsgi 6 | requests 7 | -------------------------------------------------------------------------------- /response.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding=UTF-8 -*- 3 | # Created at Jul 16 15:59 by BlahGeek@Gmail.com 4 | 5 | import sys 6 | import os 7 | import logging 8 | from msg import NO_HANDLER, HANDLE_ERROR 9 | 10 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "weixin.settings") 11 | 12 | from plugins import Plugin 13 | from plugins import * 14 | 15 | def response(text, userid): 16 | plugins = map(lambda x: x(), iter(Plugin)) 17 | try: 18 | plugin = max(plugins, key=lambda x: x.predict(text)) 19 | except ValueError: 20 | logging.warn('No handler available.') 21 | return NO_HANDLER 22 | logging.info('Using handler: %s' % str(plugin)) 23 | try: 24 | ret = plugin.handle(text, userid) 25 | except RuntimeError: 26 | ret = HANDLE_ERROR 27 | return ret 28 | 29 | if __name__ == '__main__': 30 | logging.basicConfig(level=logging.DEBUG) 31 | import django 32 | django.setup() 33 | print response(sys.argv[1].decode('utf8'), 34 | 'default' if len(sys.argv) < 3 else sys.argv[2]) 35 | -------------------------------------------------------------------------------- /vote/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blahgeek/weixin-mp/1ab2de079bfef9df1d5ada87d011bc0786e9fcfa/vote/__init__.py -------------------------------------------------------------------------------- /vote/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | from solo.admin import SingletonModelAdmin 5 | from vote.models import VoteConfiguration 6 | 7 | admin.site.register(VoteConfiguration, SingletonModelAdmin) 8 | -------------------------------------------------------------------------------- /vote/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='VoteConfiguration', 15 | fields=[ 16 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 17 | ('vote_name', models.CharField(default=b'4th IDEA Challenge', help_text=b'Vote title', max_length=255)), 18 | ('max_vote_per_user', models.IntegerField(default=3, help_text=b'Max votes a user can submit')), 19 | ('candidate_number', models.IntegerField(default=10, help_text=b'How many candidate do we have?')), 20 | ], 21 | options={ 22 | 'abstract': False, 23 | }, 24 | bases=(models.Model,), 25 | ), 26 | migrations.CreateModel( 27 | name='VoteModel', 28 | fields=[ 29 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 30 | ('candidate', models.IntegerField(db_index=True)), 31 | ('count', models.IntegerField(default=1)), 32 | ], 33 | options={ 34 | }, 35 | bases=(models.Model,), 36 | ), 37 | ] 38 | -------------------------------------------------------------------------------- /vote/migrations/0002_auto_20140905_0909.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('vote', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='VotedUserModel', 16 | fields=[ 17 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 18 | ('userid', models.CharField(max_length=1024, db_index=True)), 19 | ], 20 | options={ 21 | }, 22 | bases=(models.Model,), 23 | ), 24 | migrations.AddField( 25 | model_name='voteconfiguration', 26 | name='dup_msg', 27 | field=models.CharField(default='\u6295\u7968\u5931\u8d25\uff01\u60a8\u5df2\u6295\u8fc7\uff01', help_text=b'Fail message for duplicate', max_length=1024), 28 | preserve_default=True, 29 | ), 30 | migrations.AddField( 31 | model_name='voteconfiguration', 32 | name='fail_msg', 33 | field=models.CharField(default='\u6295\u7968\u5931\u8d25\uff01\u6295\u7968\u683c\u5f0f\uff1a\u201c\u6295\u7968 3 4 5\u201d\uff0c\u6700\u591a\u53ef\u62953\u4e2a\u4e0d\u540c\u7684\u4f5c\u54c1\uff0c\u4f5c\u54c1\u8303\u56f4\u4e3a1-10', help_text=b'Fail message', max_length=1024), 34 | preserve_default=True, 35 | ), 36 | migrations.AddField( 37 | model_name='voteconfiguration', 38 | name='ok_msg', 39 | field=models.CharField(default='\u6295\u7968\u6210\u529f\uff01', help_text=b'Success message', max_length=1024), 40 | preserve_default=True, 41 | ), 42 | migrations.AddField( 43 | model_name='voteconfiguration', 44 | name='prefix', 45 | field=models.CharField(default='\u6295\u7968', help_text=b'Prefix for votes for detecting', max_length=255), 46 | preserve_default=True, 47 | ), 48 | migrations.AlterField( 49 | model_name='votemodel', 50 | name='count', 51 | field=models.IntegerField(default=0), 52 | ), 53 | ] 54 | -------------------------------------------------------------------------------- /vote/migrations/0003_auto_20140905_0912.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('vote', '0002_auto_20140905_0909'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='voteconfiguration', 16 | name='dup_msg', 17 | field=models.TextField(default='\u6295\u7968\u5931\u8d25\uff01\u60a8\u5df2\u6295\u8fc7\uff01', help_text=b'Fail message for duplicate'), 18 | ), 19 | migrations.AlterField( 20 | model_name='voteconfiguration', 21 | name='fail_msg', 22 | field=models.TextField(default='\u6295\u7968\u5931\u8d25\uff01\u6295\u7968\u683c\u5f0f\uff1a\u201c\u6295\u7968 3 4 5\u201d\uff0c\u6700\u591a\u53ef\u62953\u4e2a\u4e0d\u540c\u7684\u4f5c\u54c1\uff0c\u4f5c\u54c1\u8303\u56f4\u4e3a1-10', help_text=b'Fail message'), 23 | ), 24 | migrations.AlterField( 25 | model_name='voteconfiguration', 26 | name='ok_msg', 27 | field=models.TextField(default='\u6295\u7968\u6210\u529f\uff01', help_text=b'Success message'), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /vote/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blahgeek/weixin-mp/1ab2de079bfef9df1d5ada87d011bc0786e9fcfa/vote/migrations/__init__.py -------------------------------------------------------------------------------- /vote/models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding=UTF-8 -*- 3 | 4 | from django.db import models 5 | from solo.models import SingletonModel 6 | 7 | 8 | class VoteConfiguration(SingletonModel): 9 | 10 | def __unicode__(self): 11 | return 'Vote Configuration' 12 | 13 | vote_name = models.CharField(max_length=255, default='4th IDEA Challenge', 14 | help_text='Vote title') 15 | max_vote_per_user = models.IntegerField(default=3, 16 | help_text='Max votes a user can submit') 17 | candidate_number = models.IntegerField(default=10, 18 | help_text='How many candidate do we have?') 19 | prefix = models.CharField(max_length=255, default=u'投票', 20 | help_text='Prefix for votes for detecting') 21 | 22 | fail_msg = models.TextField(default=u'投票失败!投票格式:“投票 3 4 5”,最多可投3个不同的作品,作品范围为1-10', 23 | help_text='Fail message') 24 | dup_msg = models.TextField(default=u'投票失败!您已投过!', 25 | help_text='Fail message for duplicate') 26 | ok_msg = models.TextField(default=u'投票成功!', 27 | help_text='Success message') 28 | 29 | 30 | class VoteModel(models.Model): 31 | candidate = models.IntegerField(db_index=True) 32 | count = models.IntegerField(default=0) 33 | 34 | class VotedUserModel(models.Model): 35 | userid = models.CharField(db_index=True, max_length=1024) 36 | -------------------------------------------------------------------------------- /vote/templates/confirm_delete.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | Are You Sure To Reset Votes? 8 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /vote/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% load static %} 9 | 10 | 11 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /vote/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /vote/urls.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Created by i@BlahGeek.com at 2014-09-05 4 | 5 | from django.conf.urls import patterns, url 6 | 7 | urlpatterns = patterns('', 8 | url(r'^getjson/$', 'vote.views.getJson', name='vote.getjson'), 9 | url(r'^$', 'vote.views.index', name='vote.index'), 10 | url(r'^delete/$', 'vote.views.delete', name='vote.delete'), 11 | url(r'^delete_real/$', 'vote.views.real_delete', name='vote.real_delete'), 12 | ) 13 | -------------------------------------------------------------------------------- /vote/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | import json 3 | from django.http import HttpResponse 4 | from django.contrib.admin.views.decorators import staff_member_required 5 | from django.views.decorators.http import require_http_methods 6 | 7 | from django.shortcuts import redirect 8 | 9 | from vote.models import VoteConfiguration, VoteModel, VotedUserModel 10 | 11 | # Create your views here. 12 | 13 | @staff_member_required 14 | def getJson(req): 15 | conf = VoteConfiguration.objects.get() 16 | values = map(lambda x: [x.candidate, x.count], VoteModel.objects.all()) 17 | user_count = VotedUserModel.objects.count() 18 | ret = { 19 | 'title': conf.vote_name, 20 | 'values': values, 21 | 'user_count': user_count, 22 | } 23 | return HttpResponse(json.dumps(ret), content_type='application/json') 24 | 25 | 26 | @staff_member_required 27 | def index(req): 28 | return render(req, 'index.html') 29 | 30 | 31 | @staff_member_required 32 | def delete(req): 33 | return render(req, 'confirm_delete.html') 34 | 35 | 36 | @staff_member_required 37 | @require_http_methods(['POST', ]) 38 | def real_delete(req): 39 | VoteModel.objects.all().delete() 40 | VotedUserModel.objects.all().delete() 41 | return redirect('vote.index') 42 | -------------------------------------------------------------------------------- /weixin/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blahgeek/weixin-mp/1ab2de079bfef9df1d5ada87d011bc0786e9fcfa/weixin/__init__.py -------------------------------------------------------------------------------- /weixin/messages.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding=UTF-8 -*- 3 | # Created at May 03 13:37 by BlahGeek@Gmail.com 4 | 5 | import time 6 | import xml.etree.ElementTree as ET 7 | from .utils import makeXml 8 | 9 | def makeMsg(req_msg, dic): 10 | dic.update({ 11 | 'ToUserName': req_msg['FromUserName'], 12 | 'FromUserName': req_msg['ToUserName'], 13 | 'CreateTime': str(int(time.time())), 14 | 'FuncFlag': '0', 15 | }) 16 | return ET.tostring(makeXml(dic), 'utf8') 17 | 18 | def makeTextMsg(req_msg, text): 19 | return makeMsg(req_msg, { 20 | 'MsgType': 'text', 21 | 'Content': text, 22 | }) 23 | 24 | def makeMusicMsg(req_msg, url, hqurl = None): 25 | return makeMsg(req_msg, { 26 | 'MsgType': 'music', 27 | 'MusicUrl': url, 28 | 'HQMusicUrl': hqurl if hqurl else url, 29 | }) 30 | 31 | def makeImageMsg(req_msg, images): 32 | # images := ((picurl, title, desc, url), ...) 33 | articles = ET.Element('Articles') 34 | for i in images: 35 | articles.append(makeXml({ 36 | 'PicUrl': i[0], 37 | 'Title': i[1], 38 | 'Description': i[2], 39 | 'Url': i[3]}, 'item')) 40 | return makeMsg(req_msg, { 41 | 'MsgType': 'news', 42 | 'ArticleCount': str(len(images)), 43 | 'Articles': articles, 44 | }) 45 | -------------------------------------------------------------------------------- /weixin/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for weixin project. 3 | 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/1.6/topics/settings/ 6 | 7 | For the full list of settings and their values, see 8 | https://docs.djangoproject.com/en/1.6/ref/settings/ 9 | """ 10 | 11 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 12 | import os 13 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 14 | 15 | WEIXIN_TOKEN = 'blahgeek' 16 | 17 | 18 | # Quick-start development settings - unsuitable for production 19 | # See https://docs.djangoproject.com/en/1.6/howto/deployment/checklist/ 20 | 21 | # SECURITY WARNING: keep the secret key used in production secret! 22 | SECRET_KEY = 'kx+g$0a&&9y7r$^647a#@jhf^1@%omx#u2fzd*-uc4mq21@9%#' 23 | 24 | # SECURITY WARNING: don't run with debug turned on in production! 25 | DEBUG = True 26 | 27 | TEMPLATE_DEBUG = True 28 | 29 | ALLOWED_HOSTS = [] 30 | 31 | 32 | # Application definition 33 | 34 | INSTALLED_APPS = ( 35 | 'admin_tools', 36 | 'admin_tools.theming', 37 | 'admin_tools.menu', 38 | 'admin_tools.dashboard', 39 | 'django.contrib.admin', 40 | 'django.contrib.auth', 41 | 'django.contrib.contenttypes', 42 | 'django.contrib.sessions', 43 | 'django.contrib.messages', 44 | 'django.contrib.staticfiles', 45 | 'djangobower', 46 | 'solo', 47 | 'weixin', 48 | 'keywordreply', 49 | 'vote', 50 | ) 51 | 52 | MIDDLEWARE_CLASSES = ( 53 | 'django.contrib.sessions.middleware.SessionMiddleware', 54 | 'django.middleware.common.CommonMiddleware', 55 | 'django.middleware.csrf.CsrfViewMiddleware', 56 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 57 | 'django.contrib.messages.middleware.MessageMiddleware', 58 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 59 | ) 60 | 61 | TEMPLATE_CONTEXT_PROCESSORS = ( 62 | 'django.core.context_processors.request', 63 | "django.contrib.auth.context_processors.auth", 64 | "django.core.context_processors.debug", 65 | "django.core.context_processors.i18n", 66 | "django.core.context_processors.media", 67 | "django.core.context_processors.static", 68 | "django.core.context_processors.tz", 69 | "django.contrib.messages.context_processors.messages") 70 | 71 | STATICFILES_FINDERS = ( 72 | "django.contrib.staticfiles.finders.FileSystemFinder", 73 | "django.contrib.staticfiles.finders.AppDirectoriesFinder", 74 | 'djangobower.finders.BowerFinder', 75 | ) 76 | 77 | BOWER_COMPONENTS_ROOT = os.path.join(BASE_DIR, 'components') 78 | 79 | BOWER_INSTALLED_APPS = ( 80 | 'jquery#1.9', 81 | 'highcharts', 82 | ) 83 | 84 | ADMIN_TOOLS_INDEX_DASHBOARD = 'dashboard.CustomIndexDashboard' 85 | ADMIN_TOOLS_APP_INDEX_DASHBOARD = 'dashboard.CustomAppIndexDashboard' 86 | 87 | ROOT_URLCONF = 'weixin.urls' 88 | 89 | WSGI_APPLICATION = 'weixin.wsgi.application' 90 | 91 | 92 | # Database 93 | # https://docs.djangoproject.com/en/1.6/ref/settings/#databases 94 | 95 | DATABASES = { 96 | 'default': { 97 | 'ENGINE': 'django.db.backends.sqlite3', 98 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 99 | } 100 | } 101 | 102 | # Internationalization 103 | # https://docs.djangoproject.com/en/1.6/topics/i18n/ 104 | 105 | LANGUAGE_CODE = 'en-us' 106 | 107 | TIME_ZONE = 'UTC' 108 | 109 | USE_I18N = True 110 | 111 | USE_L10N = True 112 | 113 | USE_TZ = True 114 | 115 | 116 | # Static files (CSS, JavaScript, Images) 117 | # https://docs.djangoproject.com/en/1.6/howto/static-files/ 118 | 119 | STATIC_URL = '/static/' 120 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 121 | -------------------------------------------------------------------------------- /weixin/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, url 2 | from django.conf.urls import include 3 | 4 | # Uncomment the next two lines to enable the admin: 5 | from django.contrib import admin 6 | admin.autodiscover() 7 | 8 | urlpatterns = patterns('', 9 | url(r'^$', 'weixin.views.index', name='index'), 10 | 11 | url(r'^vote/', include('vote.urls')), 12 | # Examples: 13 | # url(r'^$', 'weixin.views.home', name='home'), 14 | # url(r'^weixin/', include('weixin.foo.urls')), 15 | 16 | # Uncomment the admin/doc line below to enable admin documentation: 17 | url(r'^admin/doc/', include('django.contrib.admindocs.urls')), 18 | 19 | # Uncomment the next line to enable the admin: 20 | url(r'^admin/', include(admin.site.urls)), 21 | url(r'^admin_tools/', include('admin_tools.urls')), 22 | ) 23 | -------------------------------------------------------------------------------- /weixin/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding=UTF-8 -*- 3 | # Created at May 03 13:36 by BlahGeek@Gmail.com 4 | 5 | from weixin.settings import WEIXIN_TOKEN 6 | import hashlib 7 | import xml.etree.ElementTree as ET 8 | 9 | def checkSig(req): 10 | signature = req.GET.get("signature", '') 11 | timestamp = req.GET.get("timestamp", '') 12 | nonce = req.GET.get("nonce", '') 13 | tmp = ''.join(sorted([WEIXIN_TOKEN, timestamp, nonce])) 14 | tmp = hashlib.sha1(tmp).hexdigest() 15 | return tmp == signature 16 | 17 | def parseXml(req): 18 | xml_tree = ET.fromstring(req.body) 19 | content = dict() 20 | for i in xml_tree: 21 | content[i.tag] = i.text 22 | return content 23 | 24 | def makeXml(dic, root_tag = 'xml'): 25 | xml_tree = ET.Element(root_tag) 26 | def _make_node(root, key, val): 27 | if type(val) == ET.Element: 28 | root.append(val) 29 | return 30 | node = ET.SubElement(root, key) 31 | if type(val) == str or type(val) == unicode: 32 | node.text = val 33 | elif type(val) == dict: 34 | for i in val: 35 | _make_node(node, i, val[i]) 36 | for i in dic: 37 | _make_node(xml_tree, i, dic[i]) 38 | return xml_tree 39 | # return ET.tostring(xml_tree) 40 | -------------------------------------------------------------------------------- /weixin/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding=UTF-8 -*- 3 | # Created at May 03 12:43 by BlahGeek@Gmail.com 4 | 5 | from django.http import HttpResponse, HttpResponseForbidden 6 | from django.views.decorators.csrf import csrf_exempt 7 | from .utils import checkSig, parseXml 8 | from .messages import makeTextMsg, makeImageMsg 9 | from response import response 10 | from msg import NULL_RESPONSE 11 | 12 | @csrf_exempt 13 | def index(req): 14 | if not checkSig(req): 15 | return HttpResponseForbidden() 16 | if req.method == 'GET': 17 | return HttpResponse(req.GET.get('echostr', '')) 18 | req_msg = parseXml(req) 19 | ret = NULL_RESPONSE 20 | 21 | if req_msg.get('MsgType', '') == 'text': 22 | ret = response(req_msg.get('Content', ''), req_msg['FromUserName']) 23 | 24 | if isinstance(ret, unicode) or isinstance(ret, str): 25 | ret = makeTextMsg(req_msg, ret) 26 | elif isinstance(ret, list) or isinstance(ret, tuple): 27 | ret = makeImageMsg(req_msg, ret) 28 | 29 | return HttpResponse(ret) 30 | -------------------------------------------------------------------------------- /weixin/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for weixin project. 3 | 4 | This module contains the WSGI application used by Django's development server 5 | and any production WSGI deployments. It should expose a module-level variable 6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 7 | this application via the ``WSGI_APPLICATION`` setting. 8 | 9 | Usually you will have the standard Django WSGI application here, but it also 10 | might make sense to replace the whole Django WSGI application with a custom one 11 | that later delegates to the Django one. For example, you could introduce WSGI 12 | middleware here, or combine a Django application with an application of another 13 | framework. 14 | 15 | """ 16 | import os 17 | 18 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "weixin.settings") 19 | 20 | # This application object is used by any WSGI server configured to use this 21 | # file. This includes Django's development server, if the WSGI_APPLICATION 22 | # setting points here. 23 | from django.core.wsgi import get_wsgi_application 24 | application = get_wsgi_application() 25 | 26 | # Apply WSGI middleware here. 27 | # from helloworld.wsgi import HelloWorldApplication 28 | # application = HelloWorldApplication(application) 29 | --------------------------------------------------------------------------------