@@ -0,0 +1,29 @@ |
||
| 1 |
+# EditorConfig is awesome: http://EditorConfig.org |
|
| 2 |
+ |
|
| 3 |
+# top-most EditorConfig file |
|
| 4 |
+root = true |
|
| 5 |
+ |
|
| 6 |
+# Unix-style newlines with a newline ending every file |
|
| 7 |
+[*] |
|
| 8 |
+end_of_line = lf |
|
| 9 |
+insert_final_newline = false |
|
| 10 |
+ |
|
| 11 |
+# 4 space indentation |
|
| 12 |
+[*.py] |
|
| 13 |
+indent_style = space |
|
| 14 |
+indent_size = 4 |
|
| 15 |
+ |
|
| 16 |
+# Tab indentation (no size specified) |
|
| 17 |
+[*.js] |
|
| 18 |
+indent_style = space |
|
| 19 |
+indent_size = 4 |
|
| 20 |
+ |
|
| 21 |
+# Tab indentation (no size specified) |
|
| 22 |
+[*.html] |
|
| 23 |
+indent_style = space |
|
| 24 |
+indent_size = 4 |
|
| 25 |
+ |
|
| 26 |
+# Matches the exact files either package.json or .travis.yml |
|
| 27 |
+[{package.json,.travis.yml}]
|
|
| 28 |
+indent_style = space |
|
| 29 |
+indent_size = 2 |
@@ -0,0 +1,66 @@ |
||
| 1 |
+# Byte-compiled / optimized / DLL files |
|
| 2 |
+__pycache__/ |
|
| 3 |
+*.py[cod] |
|
| 4 |
+*.swp |
|
| 5 |
+# C extensions |
|
| 6 |
+*.so |
|
| 7 |
+ |
|
| 8 |
+# Distribution / packaging |
|
| 9 |
+bin/ |
|
| 10 |
+build/ |
|
| 11 |
+develop-eggs/ |
|
| 12 |
+dist/ |
|
| 13 |
+eggs/ |
|
| 14 |
+lib/ |
|
| 15 |
+lib64/ |
|
| 16 |
+parts/ |
|
| 17 |
+sdist/ |
|
| 18 |
+venv/ |
|
| 19 |
+var/ |
|
| 20 |
+static/upload/ |
|
| 21 |
+*.egg-info/ |
|
| 22 |
+.installed.cfg |
|
| 23 |
+*.egg |
|
| 24 |
+*.sublime* |
|
| 25 |
+ |
|
| 26 |
+# Installer logs |
|
| 27 |
+pip-log.txt |
|
| 28 |
+pip-delete-this-directory.txt |
|
| 29 |
+ |
|
| 30 |
+# Unit test / coverage reports |
|
| 31 |
+.tox/ |
|
| 32 |
+.coverage |
|
| 33 |
+.cache |
|
| 34 |
+nosetests.xml |
|
| 35 |
+coverage.xml |
|
| 36 |
+ |
|
| 37 |
+# Translations |
|
| 38 |
+# *.mo |
|
| 39 |
+ |
|
| 40 |
+# Mr Developer |
|
| 41 |
+.mr.developer.cfg |
|
| 42 |
+.project |
|
| 43 |
+.pydevproject |
|
| 44 |
+.settings |
|
| 45 |
+# Rope |
|
| 46 |
+.ropeproject |
|
| 47 |
+ |
|
| 48 |
+# Django stuff: |
|
| 49 |
+*.log |
|
| 50 |
+*.pot |
|
| 51 |
+ |
|
| 52 |
+# Sphinx documentation |
|
| 53 |
+docs/_build/ |
|
| 54 |
+ |
|
| 55 |
+ |
|
| 56 |
+# Ignore For zhTimer |
|
| 57 |
+.DS_Store |
|
| 58 |
+db.sqlite3 |
|
| 59 |
+local_settings.py |
|
| 60 |
+ |
|
| 61 |
+.idea/ |
|
| 62 |
+media/ |
|
| 63 |
+collect_static/ |
|
| 64 |
+ |
|
| 65 |
+# Special File |
|
| 66 |
+*download.html |
@@ -0,0 +1,8 @@ |
||
| 1 |
+# See the menu of settings available here: |
|
| 2 |
+# https://github.com/timothycrosley/isort/wiki/isort-Settings |
|
| 3 |
+ |
|
| 4 |
+[settings] |
|
| 5 |
+indent=' ' |
|
| 6 |
+line_length=120 |
|
| 7 |
+lines_after_imports=2 |
|
| 8 |
+skip=migrations |
@@ -0,0 +1,15 @@ |
||
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+ |
|
| 3 |
+from djadmin import ReadonlyModelAdmin |
|
| 4 |
+from django.contrib import admin |
|
| 5 |
+ |
|
| 6 |
+from account.models import UserInfo |
|
| 7 |
+ |
|
| 8 |
+ |
|
| 9 |
+class UserInfoAdmin(ReadonlyModelAdmin, admin.ModelAdmin): |
|
| 10 |
+ list_display = ('user_id', 'unionid', 'openid', 'name', 'sex', 'nickname', 'phone', 'country', 'province', 'city', 'location', 'status', 'created_at', 'updated_at')
|
|
| 11 |
+ list_filter = ('sex', 'status')
|
|
| 12 |
+ actions = None |
|
| 13 |
+ |
|
| 14 |
+ |
|
| 15 |
+admin.site.register(UserInfo, UserInfoAdmin) |
@@ -0,0 +1,43 @@ |
||
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+# Generated by Django 1.11.3 on 2017-09-24 14:50 |
|
| 3 |
+from __future__ import unicode_literals |
|
| 4 |
+ |
|
| 5 |
+from django.db import migrations, models |
|
| 6 |
+import shortuuidfield.fields |
|
| 7 |
+ |
|
| 8 |
+ |
|
| 9 |
+class Migration(migrations.Migration): |
|
| 10 |
+ |
|
| 11 |
+ initial = True |
|
| 12 |
+ |
|
| 13 |
+ dependencies = [ |
|
| 14 |
+ ] |
|
| 15 |
+ |
|
| 16 |
+ operations = [ |
|
| 17 |
+ migrations.CreateModel( |
|
| 18 |
+ name='UserInfo', |
|
| 19 |
+ fields=[ |
|
| 20 |
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
| 21 |
+ ('status', models.BooleanField(db_index=True, default=True, help_text='\u72b6\u6001', verbose_name='status')),
|
|
| 22 |
+ ('created_at', models.DateTimeField(auto_now_add=True, help_text='\u521b\u5efa\u65f6\u95f4', verbose_name='created_at')),
|
|
| 23 |
+ ('updated_at', models.DateTimeField(auto_now=True, help_text='\u66f4\u65b0\u65f6\u95f4', verbose_name='updated_at')),
|
|
| 24 |
+ ('user_id', shortuuidfield.fields.ShortUUIDField(blank=True, db_index=True, editable=False, help_text='\u7528\u6237\u552f\u4e00\u6807\u8bc6', max_length=22, unique=True)),
|
|
| 25 |
+ ('unionid', models.CharField(blank=True, db_index=True, help_text='\u5fae\u4fe1 Unionid', max_length=255, null=True, unique=True, verbose_name='unionid')),
|
|
| 26 |
+ ('openid', models.CharField(blank=True, db_index=True, help_text='\u5fae\u4fe1 Openid', max_length=255, null=True, unique=True, verbose_name='openid')),
|
|
| 27 |
+ ('name', models.CharField(blank=True, help_text='\u7528\u6237\u59d3\u540d', max_length=255, null=True, verbose_name='name')),
|
|
| 28 |
+ ('sex', models.IntegerField(choices=[(1, '\u7537'), (0, '\u5973')], default=1, help_text='\u7528\u6237\u6027\u522b', verbose_name='sex')),
|
|
| 29 |
+ ('nickname', models.CharField(blank=True, help_text='\u7528\u6237\u6635\u79f0', max_length=255, null=True, verbose_name='nickname')),
|
|
| 30 |
+ ('avatar', models.CharField(blank=True, help_text='\u7528\u6237\u5934\u50cf', max_length=255, null=True, verbose_name='avatar')),
|
|
| 31 |
+ ('phone', models.CharField(blank=True, db_index=True, help_text='\u7528\u6237\u7535\u8bdd', max_length=255, null=True, unique=True, verbose_name='phone')),
|
|
| 32 |
+ ('country', models.CharField(blank=True, help_text='\u7528\u6237\u56fd\u5bb6', max_length=255, null=True, verbose_name='country')),
|
|
| 33 |
+ ('province', models.CharField(blank=True, help_text='\u7528\u6237\u7701\u4efd', max_length=255, null=True, verbose_name='province')),
|
|
| 34 |
+ ('city', models.CharField(blank=True, help_text='\u7528\u6237\u57ce\u5e02', max_length=255, null=True, verbose_name='city')),
|
|
| 35 |
+ ('location', models.CharField(blank=True, help_text='\u7528\u6237\u5730\u5740', max_length=255, null=True, verbose_name='location')),
|
|
| 36 |
+ ('user_status', models.IntegerField(choices=[(0, '\u672a\u9a8c\u8bc1'), (1, '\u5df2\u6fc0\u6d3b'), (2, '\u5df2\u7981\u7528'), (3, '\u5df2\u5220\u9664')], default=0, verbose_name='user_status')),
|
|
| 37 |
+ ], |
|
| 38 |
+ options={
|
|
| 39 |
+ 'verbose_name': 'userinfo', |
|
| 40 |
+ 'verbose_name_plural': 'userinfo', |
|
| 41 |
+ }, |
|
| 42 |
+ ), |
|
| 43 |
+ ] |
@@ -0,0 +1,62 @@ |
||
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+ |
|
| 3 |
+from django.db import models |
|
| 4 |
+from django.utils.translation import ugettext_lazy as _ |
|
| 5 |
+from shortuuidfield import ShortUUIDField |
|
| 6 |
+ |
|
| 7 |
+from course.basemodels import CreateUpdateMixin |
|
| 8 |
+ |
|
| 9 |
+ |
|
| 10 |
+class UserInfo(CreateUpdateMixin): |
|
| 11 |
+ UNVERIFIED = 0 |
|
| 12 |
+ ACTIVATED = 1 |
|
| 13 |
+ DISABLED = 2 |
|
| 14 |
+ DELETED = 3 |
|
| 15 |
+ |
|
| 16 |
+ USER_STATUS = ( |
|
| 17 |
+ (UNVERIFIED, u'未验证'), |
|
| 18 |
+ (ACTIVATED, u'已激活'), |
|
| 19 |
+ (DISABLED, u'已禁用'), |
|
| 20 |
+ (DELETED, u'已删除'), |
|
| 21 |
+ ) |
|
| 22 |
+ |
|
| 23 |
+ MALE = 1 |
|
| 24 |
+ FEMALE = 0 |
|
| 25 |
+ |
|
| 26 |
+ SEX_TYPE = ( |
|
| 27 |
+ (MALE, u'男'), |
|
| 28 |
+ (FEMALE, u'女'), |
|
| 29 |
+ ) |
|
| 30 |
+ |
|
| 31 |
+ user_id = ShortUUIDField(_(u'user_id'), max_length=255, help_text=u'用户唯一标识', db_index=True, unique=True) |
|
| 32 |
+ |
|
| 33 |
+ # 微信授权用户 |
|
| 34 |
+ unionid = models.CharField(_(u'unionid'), max_length=255, blank=True, null=True, help_text=u'微信 Unionid', db_index=True, unique=True) |
|
| 35 |
+ openid = models.CharField(_(u'openid'), max_length=255, blank=True, null=True, help_text=u'微信 Openid', db_index=True, unique=True) |
|
| 36 |
+ # 用户基本信息 |
|
| 37 |
+ name = models.CharField(_(u'name'), max_length=255, blank=True, null=True, help_text=u'用户姓名') |
|
| 38 |
+ sex = models.IntegerField(_(u'sex'), choices=SEX_TYPE, default=MALE, help_text=u'用户性别') |
|
| 39 |
+ nickname = models.CharField(_(u'nickname'), max_length=255, blank=True, null=True, help_text=u'用户昵称') |
|
| 40 |
+ avatar = models.CharField(_(u'avatar'), max_length=255, blank=True, null=True, help_text=u'用户头像') |
|
| 41 |
+ phone = models.CharField(_(u'phone'), max_length=255, blank=True, null=True, help_text=u'用户电话', db_index=True, unique=True) |
|
| 42 |
+ country = models.CharField(_(u'country'), max_length=255, blank=True, null=True, help_text=u'用户国家') |
|
| 43 |
+ province = models.CharField(_(u'province'), max_length=255, blank=True, null=True, help_text=u'用户省份') |
|
| 44 |
+ city = models.CharField(_(u'city'), max_length=255, blank=True, null=True, help_text=u'用户城市') |
|
| 45 |
+ location = models.CharField(_(u'location'), max_length=255, blank=True, null=True, help_text=u'用户地址') |
|
| 46 |
+ |
|
| 47 |
+ user_status = models.IntegerField(_(u'user_status'), choices=USER_STATUS, default=UNVERIFIED) |
|
| 48 |
+ |
|
| 49 |
+ class Meta: |
|
| 50 |
+ verbose_name = _(u'userinfo') |
|
| 51 |
+ verbose_name_plural = _(u'userinfo') |
|
| 52 |
+ |
|
| 53 |
+ def __unicode__(self): |
|
| 54 |
+ return unicode(self.pk) |
|
| 55 |
+ |
|
| 56 |
+ @property |
|
| 57 |
+ def data(self): |
|
| 58 |
+ return {
|
|
| 59 |
+ 'user_id': self.user_id, |
|
| 60 |
+ 'nickname': self.nickname, |
|
| 61 |
+ 'avatar': self.avatar, |
|
| 62 |
+ } |
@@ -0,0 +1,4 @@ |
||
| 1 |
+from django.test import TestCase |
|
| 2 |
+ |
|
| 3 |
+ |
|
| 4 |
+# Create your tests here. |
@@ -0,0 +1,4 @@ |
||
| 1 |
+from django.shortcuts import render |
|
| 2 |
+ |
|
| 3 |
+ |
|
| 4 |
+# Create your views here. |
@@ -0,0 +1,4 @@ |
||
| 1 |
+from django.contrib import admin |
|
| 2 |
+ |
|
| 3 |
+ |
|
| 4 |
+# Register your models here. |
@@ -0,0 +1,4 @@ |
||
| 1 |
+from django.db import models |
|
| 2 |
+ |
|
| 3 |
+ |
|
| 4 |
+# Create your models here. |
@@ -0,0 +1,35 @@ |
||
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+ |
|
| 3 |
+from __future__ import division |
|
| 4 |
+ |
|
| 5 |
+from django.conf import settings |
|
| 6 |
+from django.core.urlresolvers import reverse |
|
| 7 |
+from django.db import transaction |
|
| 8 |
+from django.shortcuts import redirect |
|
| 9 |
+from furl import furl |
|
| 10 |
+from logit import logit |
|
| 11 |
+ |
|
| 12 |
+from account.models import UserInfo |
|
| 13 |
+from utils.redis.connect import r |
|
| 14 |
+ |
|
| 15 |
+ |
|
| 16 |
+@logit |
|
| 17 |
+@transaction.atomic |
|
| 18 |
+def oauth_redirect(request): |
|
| 19 |
+ unique_identifier = request.GET.get(settings.WECHAT_UNIQUE_IDENTIFICATION, '') |
|
| 20 |
+ |
|
| 21 |
+ user, created = UserInfo.objects.select_for_update().get_or_create(**{settings.WECHAT_UNIQUE_IDENTIFICATION: unique_identifier})
|
|
| 22 |
+ user.unionid = request.GET.get('unionid', '')
|
|
| 23 |
+ user.openid = request.GET.get('openid', '')
|
|
| 24 |
+ user.nickname = request.GET.get('nickname', '')
|
|
| 25 |
+ user.avatar = request.GET.get('headimgurl', '')
|
|
| 26 |
+ user.save() |
|
| 27 |
+ |
|
| 28 |
+ token_check_key = user.user_id |
|
| 29 |
+ |
|
| 30 |
+ query_params = {
|
|
| 31 |
+ settings.TOKEN_CHECK_KEY: token_check_key, |
|
| 32 |
+ 'vtoken': r.token(token_check_key, ex=False, buf=False), |
|
| 33 |
+ } |
|
| 34 |
+ |
|
| 35 |
+ return redirect(furl(reverse('page:user_oauth')).add(request.GET).add(query_params).url)
|
@@ -0,0 +1,4 @@ |
||
| 1 |
+from django.test import TestCase |
|
| 2 |
+ |
|
| 3 |
+ |
|
| 4 |
+# Create your tests here. |
@@ -0,0 +1,16 @@ |
||
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+ |
|
| 3 |
+from django.conf.urls import url |
|
| 4 |
+ |
|
| 5 |
+from api import oauth_views |
|
| 6 |
+from page import code_views, info_views, list_views |
|
| 7 |
+ |
|
| 8 |
+ |
|
| 9 |
+urlpatterns = [ |
|
| 10 |
+ url(r'^code/exchange$', code_views.code_exchange, name='code_exchange'), |
|
| 11 |
+] |
|
| 12 |
+ |
|
| 13 |
+urlpatterns += [ |
|
| 14 |
+ url(r'^3rd/or$', oauth_views.oauth_redirect, name='3rd_or'), |
|
| 15 |
+ url(r'^3rd/oauth_redirect$', oauth_views.oauth_redirect, name='3rd_oauth_redirect'), |
|
| 16 |
+] |
@@ -0,0 +1,4 @@ |
||
| 1 |
+from django.shortcuts import render |
|
| 2 |
+ |
|
| 3 |
+ |
|
| 4 |
+# Create your views here. |
@@ -0,0 +1,9 @@ |
||
| 1 |
+#!/bin/bash |
|
| 2 |
+ |
|
| 3 |
+echo '>> iSort' |
|
| 4 |
+./isort.sh |
|
| 5 |
+echo |
|
| 6 |
+ |
|
| 7 |
+echo '>> PEP8' |
|
| 8 |
+./pep8.sh |
|
| 9 |
+echo |
@@ -0,0 +1,14 @@ |
||
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+ |
|
| 3 |
+from djadmin import ExportExcelModelAdmin, ReadonlyModelAdmin |
|
| 4 |
+from django.contrib import admin |
|
| 5 |
+ |
|
| 6 |
+from codes.models import CourseCodeInfo |
|
| 7 |
+ |
|
| 8 |
+ |
|
| 9 |
+class CourseCodeInfoAdmin(ExportExcelModelAdmin, admin.ModelAdmin): |
|
| 10 |
+ list_display = ('code', 'exchanged', 'user_id', 'status', 'created_at', 'updated_at')
|
|
| 11 |
+ list_filter = ('exchanged', 'status')
|
|
| 12 |
+ |
|
| 13 |
+ |
|
| 14 |
+admin.site.register(CourseCodeInfo, CourseCodeInfoAdmin) |
@@ -0,0 +1,8 @@ |
||
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+from __future__ import unicode_literals |
|
| 3 |
+ |
|
| 4 |
+from django.apps import AppConfig |
|
| 5 |
+ |
|
| 6 |
+ |
|
| 7 |
+class CodesConfig(AppConfig): |
|
| 8 |
+ name = 'codes' |
@@ -0,0 +1,32 @@ |
||
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+# Generated by Django 1.11.3 on 2017-09-24 14:50 |
|
| 3 |
+from __future__ import unicode_literals |
|
| 4 |
+ |
|
| 5 |
+from django.db import migrations, models |
|
| 6 |
+ |
|
| 7 |
+ |
|
| 8 |
+class Migration(migrations.Migration): |
|
| 9 |
+ |
|
| 10 |
+ initial = True |
|
| 11 |
+ |
|
| 12 |
+ dependencies = [ |
|
| 13 |
+ ] |
|
| 14 |
+ |
|
| 15 |
+ operations = [ |
|
| 16 |
+ migrations.CreateModel( |
|
| 17 |
+ name='CourseCodeInfo', |
|
| 18 |
+ fields=[ |
|
| 19 |
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
| 20 |
+ ('status', models.BooleanField(db_index=True, default=True, help_text='\u72b6\u6001', verbose_name='status')),
|
|
| 21 |
+ ('created_at', models.DateTimeField(auto_now_add=True, help_text='\u521b\u5efa\u65f6\u95f4', verbose_name='created_at')),
|
|
| 22 |
+ ('updated_at', models.DateTimeField(auto_now=True, help_text='\u66f4\u65b0\u65f6\u95f4', verbose_name='updated_at')),
|
|
| 23 |
+ ('code', models.CharField(blank=True, db_index=True, help_text='\u5151\u6362\u7801', max_length=255, null=True, verbose_name='code')),
|
|
| 24 |
+ ('exchanged', models.BooleanField(db_index=True, default=False, help_text='\u5151\u6362\u72b6\u6001', verbose_name='exchanged')),
|
|
| 25 |
+ ('user_id', models.CharField(blank=True, db_index=True, help_text='\u7528\u6237\u552f\u4e00\u6807\u8bc6', max_length=255, null=True, unique=True, verbose_name='user_id')),
|
|
| 26 |
+ ], |
|
| 27 |
+ options={
|
|
| 28 |
+ 'verbose_name': 'coursecodeinfo', |
|
| 29 |
+ 'verbose_name_plural': 'coursecodeinfo', |
|
| 30 |
+ }, |
|
| 31 |
+ ), |
|
| 32 |
+ ] |
@@ -0,0 +1,20 @@ |
||
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+ |
|
| 3 |
+from django.db import models |
|
| 4 |
+from django.utils.translation import ugettext_lazy as _ |
|
| 5 |
+ |
|
| 6 |
+from course.basemodels import CreateUpdateMixin |
|
| 7 |
+ |
|
| 8 |
+ |
|
| 9 |
+class CourseCodeInfo(CreateUpdateMixin): |
|
| 10 |
+ code = models.CharField(_(u'code'), max_length=255, blank=True, null=True, help_text=u'兑换码', db_index=True) |
|
| 11 |
+ exchanged = models.BooleanField(_(u'exchanged'), default=False, help_text=_(u'兑换状态'), db_index=True) |
|
| 12 |
+ |
|
| 13 |
+ user_id = models.CharField(_(u'user_id'), max_length=255, blank=True, null=True, help_text=u'用户唯一标识', db_index=True, unique=True) |
|
| 14 |
+ |
|
| 15 |
+ class Meta: |
|
| 16 |
+ verbose_name = _(u'coursecodeinfo') |
|
| 17 |
+ verbose_name_plural = _(u'coursecodeinfo') |
|
| 18 |
+ |
|
| 19 |
+ def __unicode__(self): |
|
| 20 |
+ return unicode(self.pk) |
@@ -0,0 +1,7 @@ |
||
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+from __future__ import unicode_literals |
|
| 3 |
+ |
|
| 4 |
+from django.test import TestCase |
|
| 5 |
+ |
|
| 6 |
+ |
|
| 7 |
+# Create your tests here. |
@@ -0,0 +1,7 @@ |
||
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+from __future__ import unicode_literals |
|
| 3 |
+ |
|
| 4 |
+from django.shortcuts import render |
|
| 5 |
+ |
|
| 6 |
+ |
|
| 7 |
+# Create your views here. |
@@ -0,0 +1,26 @@ |
||
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+ |
|
| 3 |
+from django.db import models |
|
| 4 |
+from django.utils.translation import ugettext_lazy as _ |
|
| 5 |
+ |
|
| 6 |
+ |
|
| 7 |
+class CreateUpdateMixin(models.Model): |
|
| 8 |
+ status = models.BooleanField(_(u'status'), default=True, help_text=_(u'状态'), db_index=True) |
|
| 9 |
+ created_at = models.DateTimeField(_(u'created_at'), auto_now_add=True, editable=True, help_text=_(u'创建时间')) |
|
| 10 |
+ updated_at = models.DateTimeField(_(u'updated_at'), auto_now=True, editable=True, help_text=_(u'更新时间')) |
|
| 11 |
+ |
|
| 12 |
+ class Meta: |
|
| 13 |
+ abstract = True |
|
| 14 |
+ |
|
| 15 |
+ |
|
| 16 |
+class SexChoicesMixin(models.Model): |
|
| 17 |
+ MALE = 1 |
|
| 18 |
+ FEMALE = 0 |
|
| 19 |
+ |
|
| 20 |
+ SEX_TYPE = ( |
|
| 21 |
+ (MALE, u'男'), |
|
| 22 |
+ (FEMALE, u'女'), |
|
| 23 |
+ ) |
|
| 24 |
+ |
|
| 25 |
+ class Meta: |
|
| 26 |
+ abstract = True |
@@ -0,0 +1,36 @@ |
||
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+ |
|
| 3 |
+from functools import wraps |
|
| 4 |
+ |
|
| 5 |
+from django.conf import settings |
|
| 6 |
+from django.shortcuts import redirect, render |
|
| 7 |
+from furl import furl |
|
| 8 |
+from pywe_oauth import get_oauth_redirect_url |
|
| 9 |
+ |
|
| 10 |
+from utils.redis.connect import r |
|
| 11 |
+ |
|
| 12 |
+ |
|
| 13 |
+def check_token(func=None): |
|
| 14 |
+ def decorator(func): |
|
| 15 |
+ @wraps(func) |
|
| 16 |
+ def returned_wrapper(request, *args, **kwargs): |
|
| 17 |
+ vtoken = request.GET.get('vtoken', '') or request.POST.get('vtoken', '')
|
|
| 18 |
+ if not settings.DEBUG: |
|
| 19 |
+ if not request.wechat: |
|
| 20 |
+ return render(request, 'django_we/errmsg.html', {'title': '错误', 'errmsg': '请在微信中打开'})
|
|
| 21 |
+ token_check_key = request.GET.get(settings.TOKEN_CHECK_KEY, '') or request.POST.get(settings.TOKEN_CHECK_KEY, '') |
|
| 22 |
+ if not r.token_exists(token_check_key, vtoken): |
|
| 23 |
+ # 3rd OAuth |
|
| 24 |
+ return redirect(settings.WECHAT_OAUTH2_REDIRECT_URL) |
|
| 25 |
+ # Current OAuth |
|
| 26 |
+ redirect_url = furl(settings.WECHAT_OAUTH2_REDIRECT_ENTRY).add({}).url
|
|
| 27 |
+ return redirect(get_oauth_redirect_url(settings.WECHAT_OAUTH2_REDIRECT_URI, 'snsapi_userinfo', redirect_url)) |
|
| 28 |
+ return func(request, *args, **kwargs) |
|
| 29 |
+ return returned_wrapper |
|
| 30 |
+ |
|
| 31 |
+ if not func: |
|
| 32 |
+ def foo(func): |
|
| 33 |
+ return decorator(func) |
|
| 34 |
+ return foo |
|
| 35 |
+ |
|
| 36 |
+ return decorator(func) |
@@ -0,0 +1,27 @@ |
||
| 1 |
+# course_uwsgi.ini file |
|
| 2 |
+[uwsgi] |
|
| 3 |
+ |
|
| 4 |
+# Django-related settings |
|
| 5 |
+# the base directory (full path) |
|
| 6 |
+chdir = /home/paiai/work/course |
|
| 7 |
+# Django's wsgi file |
|
| 8 |
+module = course.wsgi |
|
| 9 |
+# the virtualenv (full path) |
|
| 10 |
+# home = /path/to/virtualenv |
|
| 11 |
+ |
|
| 12 |
+# process-related settings |
|
| 13 |
+# master |
|
| 14 |
+master = true |
|
| 15 |
+# maximum number of worker processes |
|
| 16 |
+processes = 10 |
|
| 17 |
+# the socket (use the full path to be safe |
|
| 18 |
+socket = /home/paiai/work/course/course/deploy/course.sock |
|
| 19 |
+# ... with appropriate permissions - may be needed |
|
| 20 |
+chmod-socket = 777 |
|
| 21 |
+# clear environment on exit |
|
| 22 |
+vacuum = true |
|
| 23 |
+ |
|
| 24 |
+# 11: Resource temporarily unavailable |
|
| 25 |
+reload-mercy = 64 |
|
| 26 |
+max-requests = 8192 |
|
| 27 |
+listen = 4096 |
@@ -0,0 +1,40 @@ |
||
| 1 |
+# course_nginx.conf |
|
| 2 |
+ |
|
| 3 |
+# the upstream component nginx needs to connect to |
|
| 4 |
+upstream course {
|
|
| 5 |
+ # server unix:///home/paiai/work/course/course/deploy/course.sock; # for a file socket |
|
| 6 |
+ server 127.0.0.1:8888; # for a web port socket (we'll use this first) |
|
| 7 |
+} |
|
| 8 |
+ |
|
| 9 |
+# configuration of the server |
|
| 10 |
+server {
|
|
| 11 |
+ # the port your site will be served on |
|
| 12 |
+ listen 80; |
|
| 13 |
+ # the domain name it will serve for |
|
| 14 |
+ server_name .a.com; # substitute your machine's IP address or FQDN |
|
| 15 |
+ charset utf-8; |
|
| 16 |
+ |
|
| 17 |
+ # max upload size |
|
| 18 |
+ client_max_body_size 75M; # adjust to taste |
|
| 19 |
+ |
|
| 20 |
+ # JS接口安全域名 & 业务域名 验证 |
|
| 21 |
+ location /xxx.txt {
|
|
| 22 |
+ alias /home/paiai/work/course/docs/we/xxx.txt; |
|
| 23 |
+ } |
|
| 24 |
+ |
|
| 25 |
+ # Django media |
|
| 26 |
+ location /media {
|
|
| 27 |
+ alias /home/paiai/work/course/media; # your Django project's media files - amend as required |
|
| 28 |
+ } |
|
| 29 |
+ |
|
| 30 |
+ location /static {
|
|
| 31 |
+ alias /home/paiai/work/course/collect_static; # your Django project's static files - amend as required |
|
| 32 |
+ } |
|
| 33 |
+ |
|
| 34 |
+ # Finally, send all non-media requests to the Django server. |
|
| 35 |
+ location / {
|
|
| 36 |
+ # uwsgi_pass course; |
|
| 37 |
+ proxy_pass http://course; |
|
| 38 |
+ include /home/paiai/work/course/course/deploy/uwsgi_params; # the uwsgi_params file you installed |
|
| 39 |
+ } |
|
| 40 |
+} |
@@ -0,0 +1,10 @@ |
||
| 1 |
+[program:course] |
|
| 2 |
+command=/home/paiai/env/bin/uwsgi --ini /home/paiai/work/course/course/deploy/course.ini |
|
| 3 |
+autostart=true |
|
| 4 |
+autorestart=true |
|
| 5 |
+startretries=3 |
|
| 6 |
+exitcodes=0,1,2 |
|
| 7 |
+stopsignal=QUIT |
|
| 8 |
+stdout_logfile=/var/log/supervisor_course_access.log |
|
| 9 |
+stderr_logfile=/var/log/supervisor_course_error.log |
|
| 10 |
+user=diors |
@@ -0,0 +1,15 @@ |
||
| 1 |
+uwsgi_param QUERY_STRING $query_string; |
|
| 2 |
+uwsgi_param REQUEST_METHOD $request_method; |
|
| 3 |
+uwsgi_param CONTENT_TYPE $content_type; |
|
| 4 |
+uwsgi_param CONTENT_LENGTH $content_length; |
|
| 5 |
+ |
|
| 6 |
+uwsgi_param REQUEST_URI $request_uri; |
|
| 7 |
+uwsgi_param PATH_INFO $document_uri; |
|
| 8 |
+uwsgi_param DOCUMENT_ROOT $document_root; |
|
| 9 |
+uwsgi_param SERVER_PROTOCOL $server_protocol; |
|
| 10 |
+uwsgi_param UWSGI_SCHEME $scheme; |
|
| 11 |
+ |
|
| 12 |
+uwsgi_param REMOTE_ADDR $remote_addr; |
|
| 13 |
+uwsgi_param REMOTE_PORT $remote_port; |
|
| 14 |
+uwsgi_param SERVER_PORT $server_port; |
|
| 15 |
+uwsgi_param SERVER_NAME $server_name; |
@@ -0,0 +1,16 @@ |
||
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+ |
|
| 3 |
+import redis_extensions as redis |
|
| 4 |
+ |
|
| 5 |
+ |
|
| 6 |
+def redis_conf(conf): |
|
| 7 |
+ return {
|
|
| 8 |
+ 'host': conf.get('HOST', 'localhost'),
|
|
| 9 |
+ 'port': conf.get('PORT', 6379),
|
|
| 10 |
+ 'password': '{0}:{1}'.format(conf.get('USER', ''), conf.get('PASSWORD', '')) if conf.get('USER') else '',
|
|
| 11 |
+ 'db': conf.get('db', 0),
|
|
| 12 |
+ } |
|
| 13 |
+ |
|
| 14 |
+ |
|
| 15 |
+def redis_connect(conf): |
|
| 16 |
+ return redis.StrictRedisExtensions(connection_pool=redis.ConnectionPool(**redis_conf(conf))) |
@@ -0,0 +1,43 @@ |
||
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+ |
|
| 3 |
+import os |
|
| 4 |
+ |
|
| 5 |
+ |
|
| 6 |
+# DEBUG = False |
|
| 7 |
+ |
|
| 8 |
+ALLOWED_HOSTS = ['127.0.0.1', 'localhost'] |
|
| 9 |
+ |
|
| 10 |
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
|
| 11 |
+PROJ_DIR = os.path.abspath(os.path.dirname(os.path.abspath(__file__))) |
|
| 12 |
+ |
|
| 13 |
+TEMPLATES = [ |
|
| 14 |
+ {
|
|
| 15 |
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates', |
|
| 16 |
+ 'DIRS': [os.path.join(BASE_DIR, 'templates')], |
|
| 17 |
+ # 'APP_DIRS': True, |
|
| 18 |
+ 'OPTIONS': {
|
|
| 19 |
+ 'context_processors': [ |
|
| 20 |
+ 'django.template.context_processors.debug', |
|
| 21 |
+ 'django.template.context_processors.request', |
|
| 22 |
+ 'django.contrib.auth.context_processors.auth', |
|
| 23 |
+ 'django.contrib.messages.context_processors.messages', |
|
| 24 |
+ ], |
|
| 25 |
+ 'loaders': [ |
|
| 26 |
+ 'django.template.loaders.filesystem.Loader', |
|
| 27 |
+ 'django.template.loaders.app_directories.Loader', |
|
| 28 |
+ ], |
|
| 29 |
+ }, |
|
| 30 |
+ }, |
|
| 31 |
+] |
|
| 32 |
+ |
|
| 33 |
+# DOMAIN |
|
| 34 |
+DOMAIN = 'http://a.com' |
|
| 35 |
+ |
|
| 36 |
+# 邮件设置 |
|
| 37 |
+# 只有当 DEBUG = False 的时候,才会邮件发送报错信息 |
|
| 38 |
+SERVER_EMAIL = 'error.notify@exmail.com' |
|
| 39 |
+EMAIL_HOST_USER = 'error.notify@exmail.com' |
|
| 40 |
+EMAIL_HOST_PASSWORD = '<^_^>pwd<^_^>' |
|
| 41 |
+DEFAULT_FROM_EMAIL = 'error.notify <error.notify@exmail.com>' |
|
| 42 |
+ADMINS = [('Zhang San', 'san.zhang@exmail.com'), ('Li Si', 'si.li@exmail.com')]
|
|
| 43 |
+EMAIL_SUBJECT_PREFIX = u'[Templet] ' |
@@ -0,0 +1,33 @@ |
||
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+ |
|
| 3 |
+ |
|
| 4 |
+def DJANGO_WE_CFG_FUNC(request, state=None): |
|
| 5 |
+ """ WeChat CFG Callback Func """ |
|
| 6 |
+ |
|
| 7 |
+ |
|
| 8 |
+def DJANGO_WE_BASE_FUNC(code, state, access_info=None): |
|
| 9 |
+ """ WeChat Base Redirect Callback Func """ |
|
| 10 |
+ |
|
| 11 |
+ |
|
| 12 |
+def DJANGO_WE_USERINFO_FUNC(code, state, access_info=None, userinfo=None): |
|
| 13 |
+ """ WeChat Userinfo Redirect Callback Func """ |
|
| 14 |
+ from account.models import UserInfo |
|
| 15 |
+ from django.conf import settings |
|
| 16 |
+ from utils.redis.connect import r |
|
| 17 |
+ |
|
| 18 |
+ # Save profile or something else |
|
| 19 |
+ unique_identifier = userinfo.get(settings.WECHAT_UNIQUE_IDENTIFICATION, '') |
|
| 20 |
+ |
|
| 21 |
+ user, created = UserInfo.objects.select_for_update().get_or_create(**{settings.WECHAT_UNIQUE_IDENTIFICATION: unique_identifier})
|
|
| 22 |
+ user.unionid = userinfo.get('unionid', '')
|
|
| 23 |
+ user.openid = userinfo.get('openid', '')
|
|
| 24 |
+ user.nickname = userinfo.get('nickname', '')
|
|
| 25 |
+ user.avatar = userinfo.get('headimgurl', '')
|
|
| 26 |
+ user.save() |
|
| 27 |
+ |
|
| 28 |
+ token_check_key = user.user_id |
|
| 29 |
+ |
|
| 30 |
+ return {
|
|
| 31 |
+ settings.TOKEN_CHECK_KEY: token_check_key, |
|
| 32 |
+ 'vtoken': r.token(token_check_key, ex=False, buf=False), |
|
| 33 |
+ } |
@@ -0,0 +1,266 @@ |
||
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+ |
|
| 3 |
+""" |
|
| 4 |
+Django settings for course project. |
|
| 5 |
+ |
|
| 6 |
+Generated by 'django-admin startproject' using Django 1.11.3. |
|
| 7 |
+ |
|
| 8 |
+For more information on this file, see |
|
| 9 |
+https://docs.djangoproject.com/en/1.11/topics/settings/ |
|
| 10 |
+ |
|
| 11 |
+For the full list of settings and their values, see |
|
| 12 |
+https://docs.djangoproject.com/en/1.11/ref/settings/ |
|
| 13 |
+""" |
|
| 14 |
+ |
|
| 15 |
+import os |
|
| 16 |
+ |
|
| 17 |
+ |
|
| 18 |
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...) |
|
| 19 |
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
|
| 20 |
+PROJ_DIR = os.path.abspath(os.path.dirname(os.path.abspath(__file__))) |
|
| 21 |
+ |
|
| 22 |
+ |
|
| 23 |
+# Quick-start development settings - unsuitable for production |
|
| 24 |
+# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ |
|
| 25 |
+ |
|
| 26 |
+# SECURITY WARNING: keep the secret key used in production secret! |
|
| 27 |
+SECRET_KEY = 'b*_bo@vg6jv1!7lhfci-)7^155c+%83y69zgf6)@s^9=_32edi' |
|
| 28 |
+ |
|
| 29 |
+# SECURITY WARNING: don't run with debug turned on in production! |
|
| 30 |
+DEBUG = True |
|
| 31 |
+ |
|
| 32 |
+ALLOWED_HOSTS = [] |
|
| 33 |
+ |
|
| 34 |
+ |
|
| 35 |
+# Application definition |
|
| 36 |
+ |
|
| 37 |
+INSTALLED_APPS = [ |
|
| 38 |
+ 'django.contrib.admin', |
|
| 39 |
+ 'django.contrib.auth', |
|
| 40 |
+ 'django.contrib.contenttypes', |
|
| 41 |
+ 'django.contrib.sessions', |
|
| 42 |
+ 'django.contrib.messages', |
|
| 43 |
+ 'django.contrib.staticfiles', |
|
| 44 |
+ 'django_uniapi', |
|
| 45 |
+ 'django_we', |
|
| 46 |
+ 'account', |
|
| 47 |
+ 'api', |
|
| 48 |
+ 'codes', |
|
| 49 |
+ 'courses', |
|
| 50 |
+ 'page', |
|
| 51 |
+] |
|
| 52 |
+ |
|
| 53 |
+MIDDLEWARE = [ |
|
| 54 |
+ 'django.middleware.security.SecurityMiddleware', |
|
| 55 |
+ 'django.contrib.sessions.middleware.SessionMiddleware', |
|
| 56 |
+ 'django.middleware.common.CommonMiddleware', |
|
| 57 |
+ # 'django.middleware.csrf.CsrfViewMiddleware', |
|
| 58 |
+ 'django.contrib.auth.middleware.AuthenticationMiddleware', |
|
| 59 |
+ 'django.contrib.messages.middleware.MessageMiddleware', |
|
| 60 |
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware', |
|
| 61 |
+ 'detect.middleware.UserAgentDetectionMiddleware', |
|
| 62 |
+] |
|
| 63 |
+ |
|
| 64 |
+ROOT_URLCONF = 'course.urls' |
|
| 65 |
+ |
|
| 66 |
+TEMPLATES = [ |
|
| 67 |
+ {
|
|
| 68 |
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates', |
|
| 69 |
+ 'DIRS': [os.path.join(BASE_DIR, 'templates')], |
|
| 70 |
+ # 'APP_DIRS': True, |
|
| 71 |
+ 'OPTIONS': {
|
|
| 72 |
+ 'context_processors': [ |
|
| 73 |
+ 'django.template.context_processors.debug', |
|
| 74 |
+ 'django.template.context_processors.request', |
|
| 75 |
+ 'django.contrib.auth.context_processors.auth', |
|
| 76 |
+ 'django.contrib.messages.context_processors.messages', |
|
| 77 |
+ ], |
|
| 78 |
+ 'loaders': [ |
|
| 79 |
+ ('django.template.loaders.cached.Loader', [
|
|
| 80 |
+ 'django.template.loaders.filesystem.Loader', |
|
| 81 |
+ 'django.template.loaders.app_directories.Loader', |
|
| 82 |
+ ]), |
|
| 83 |
+ ], |
|
| 84 |
+ }, |
|
| 85 |
+ }, |
|
| 86 |
+] |
|
| 87 |
+ |
|
| 88 |
+WSGI_APPLICATION = 'course.wsgi.application' |
|
| 89 |
+ |
|
| 90 |
+ |
|
| 91 |
+# Database |
|
| 92 |
+# https://docs.djangoproject.com/en/1.11/ref/settings/#databases |
|
| 93 |
+ |
|
| 94 |
+DATABASES = {
|
|
| 95 |
+ 'default': {
|
|
| 96 |
+ 'ENGINE': 'django.db.backends.mysql', |
|
| 97 |
+ 'NAME': 'course', |
|
| 98 |
+ 'USER': 'root', |
|
| 99 |
+ 'PASSWORD': '', |
|
| 100 |
+ 'CONN_MAX_AGE': 600, |
|
| 101 |
+ 'OPTIONS': {
|
|
| 102 |
+ # Utf8mb4 for Emoji |
|
| 103 |
+ # |
|
| 104 |
+ # Nickname |
|
| 105 |
+ # |
|
| 106 |
+ # account.WechatInfo ==> nickname |
|
| 107 |
+ # ALTER TABLE account_wechatinfo MODIFY COLUMN nickname VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; |
|
| 108 |
+ 'charset': 'utf8mb4', |
|
| 109 |
+ }, |
|
| 110 |
+ } |
|
| 111 |
+} |
|
| 112 |
+ |
|
| 113 |
+ |
|
| 114 |
+# Password validation |
|
| 115 |
+# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators |
|
| 116 |
+ |
|
| 117 |
+AUTH_PASSWORD_VALIDATORS = [ |
|
| 118 |
+ {
|
|
| 119 |
+ 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', |
|
| 120 |
+ }, |
|
| 121 |
+ {
|
|
| 122 |
+ 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', |
|
| 123 |
+ }, |
|
| 124 |
+ {
|
|
| 125 |
+ 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', |
|
| 126 |
+ }, |
|
| 127 |
+ {
|
|
| 128 |
+ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', |
|
| 129 |
+ }, |
|
| 130 |
+] |
|
| 131 |
+ |
|
| 132 |
+ |
|
| 133 |
+# Internationalization |
|
| 134 |
+# https://docs.djangoproject.com/en/1.11/topics/i18n/ |
|
| 135 |
+ |
|
| 136 |
+LANGUAGE_CODE = 'zh-Hans' |
|
| 137 |
+ |
|
| 138 |
+TIME_ZONE = 'Asia/Shanghai' |
|
| 139 |
+ |
|
| 140 |
+USE_I18N = True |
|
| 141 |
+ |
|
| 142 |
+USE_L10N = True |
|
| 143 |
+ |
|
| 144 |
+USE_TZ = True |
|
| 145 |
+ |
|
| 146 |
+ |
|
| 147 |
+# Static files (CSS, JavaScript, Images) |
|
| 148 |
+# https://docs.djangoproject.com/en/1.11/howto/static-files/ |
|
| 149 |
+ |
|
| 150 |
+STATICFILES_DIRS = ( |
|
| 151 |
+ os.path.join(PROJ_DIR, 'static').replace('\\', '/'),
|
|
| 152 |
+) |
|
| 153 |
+ |
|
| 154 |
+STATIC_ROOT = os.path.join(BASE_DIR, 'collect_static').replace('\\', '/')
|
|
| 155 |
+ |
|
| 156 |
+STATIC_URL = '/static/' |
|
| 157 |
+ |
|
| 158 |
+STATICFILES_FINDERS = ( |
|
| 159 |
+ 'django.contrib.staticfiles.finders.FileSystemFinder', |
|
| 160 |
+ 'django.contrib.staticfiles.finders.AppDirectoriesFinder', |
|
| 161 |
+ # 'django.contrib.staticfiles.finders.DefaultStorageFinder', |
|
| 162 |
+) |
|
| 163 |
+ |
|
| 164 |
+MEDIA_ROOT = os.path.join(BASE_DIR, 'media').replace('\\', '/')
|
|
| 165 |
+ |
|
| 166 |
+MEDIA_URL = '/media/' |
|
| 167 |
+ |
|
| 168 |
+# DOMAIN |
|
| 169 |
+DOMAIN = 'http://a.com' |
|
| 170 |
+ |
|
| 171 |
+# Redis 设置 |
|
| 172 |
+REDIS = {
|
|
| 173 |
+ 'default': {
|
|
| 174 |
+ 'HOST': '127.0.0.1', |
|
| 175 |
+ 'PORT': 6379, |
|
| 176 |
+ 'USER': '', |
|
| 177 |
+ 'PASSWORD': '', |
|
| 178 |
+ 'db': 0, |
|
| 179 |
+ } |
|
| 180 |
+} |
|
| 181 |
+ |
|
| 182 |
+# 微信设置 |
|
| 183 |
+WECHAT = {
|
|
| 184 |
+ 'JSAPI': {
|
|
| 185 |
+ 'token': '5201314', |
|
| 186 |
+ 'appID': '', |
|
| 187 |
+ 'appsecret': '', |
|
| 188 |
+ 'mchID': '', |
|
| 189 |
+ 'apiKey': '', |
|
| 190 |
+ 'mch_cert': '', |
|
| 191 |
+ 'mch_key': '', |
|
| 192 |
+ 'redpack': {
|
|
| 193 |
+ |
|
| 194 |
+ } |
|
| 195 |
+ }, |
|
| 196 |
+} |
|
| 197 |
+ |
|
| 198 |
+# 微信唯一标识 |
|
| 199 |
+# Choices: 'unionid' or 'openid' |
|
| 200 |
+# |
|
| 201 |
+# models.py |
|
| 202 |
+# 'unique_identifier': self.unionid if settings.WECHAT_UNIQUE_IDENTIFICATION == 'unionid' else self.openid, |
|
| 203 |
+# views.py |
|
| 204 |
+# unique_identifier = request.POST.get(settings.WECHAT_UNIQUE_IDENTIFICATION, '') |
|
| 205 |
+# profile = Profile.objects.get(**{settings.WECHAT_UNIQUE_IDENTIFICATION: unique_identifier})
|
|
| 206 |
+WECHAT_UNIQUE_IDENTIFICATION = 'unionid' |
|
| 207 |
+ |
|
| 208 |
+# Token 错误重授权设置 |
|
| 209 |
+TOKEN_CHECK_KEY = 'user_id' |
|
| 210 |
+WECHAT_OAUTH2_REDIRECT_ENTRY = '' |
|
| 211 |
+WECHAT_OAUTH2_REDIRECT_URL = '' |
|
| 212 |
+ |
|
| 213 |
+# 错误信息邮件设置 |
|
| 214 |
+# Email address that error messages come from. |
|
| 215 |
+SERVER_EMAIL = 'kimi@pai.ai' |
|
| 216 |
+# The email backend to use. For possible shortcuts see django.core.mail. |
|
| 217 |
+# The default is to use the SMTP backend. |
|
| 218 |
+# Third-party backends can be specified by providing a Python path |
|
| 219 |
+# to a module that defines an EmailBackend class. |
|
| 220 |
+EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' |
|
| 221 |
+# Host for sending email. |
|
| 222 |
+EMAIL_HOST = 'smtp.exmail.qq.com' |
|
| 223 |
+# Port for sending email. |
|
| 224 |
+EMAIL_PORT = 25 |
|
| 225 |
+# Optional SMTP authentication information for EMAIL_HOST. |
|
| 226 |
+EMAIL_HOST_USER = 'kimi@pai.ai' |
|
| 227 |
+EMAIL_HOST_PASSWORD = '<^_^>pwd<^_^>' |
|
| 228 |
+EMAIL_USE_TLS = False |
|
| 229 |
+EMAIL_USE_SSL = False |
|
| 230 |
+EMAIL_SSL_CERTFILE = None |
|
| 231 |
+EMAIL_SSL_KEYFILE = None |
|
| 232 |
+EMAIL_TIMEOUT = None |
|
| 233 |
+# Default email address to use for various automated correspondence from |
|
| 234 |
+# the site managers. |
|
| 235 |
+DEFAULT_FROM_EMAIL = 'Kimi <kimi@pai.ai>' |
|
| 236 |
+# People who get code error notifications. |
|
| 237 |
+# In the format [('Full Name', 'email@example.com'), ('Full Name', 'anotheremail@example.com')]
|
|
| 238 |
+ADMINS = [('Kimi', 'kimi@pai.ai')]
|
|
| 239 |
+# Not-necessarily-technical managers of the site. They get broken link |
|
| 240 |
+# notifications and other various emails. |
|
| 241 |
+MANAGERS = ADMINS |
|
| 242 |
+# Subject-line prefix for email messages send with django.core.mail.mail_admins |
|
| 243 |
+# or ...mail_managers. Make sure to include the trailing space. |
|
| 244 |
+EMAIL_SUBJECT_PREFIX = u'[Course] ' |
|
| 245 |
+ |
|
| 246 |
+# Admin Settings |
|
| 247 |
+DISABLE_ACTION = False |
|
| 248 |
+ |
|
| 249 |
+try: |
|
| 250 |
+ from local_settings import * |
|
| 251 |
+except ImportError: |
|
| 252 |
+ pass |
|
| 253 |
+ |
|
| 254 |
+# 依赖 local_settings 中的配置 |
|
| 255 |
+# 微信授权设置 |
|
| 256 |
+WECHAT_OAUTH2_REDIRECT_URI = '{0}/we/oauth2?scope={{0}}redirect_url={{1}}'.format(DOMAIN)
|
|
| 257 |
+WECHAT_BASE_REDIRECT_URI = '{0}/we/base_redirect'.format(DOMAIN)
|
|
| 258 |
+WECHAT_USERINFO_REDIRECT_URI = '{0}/we/userinfo_redirect'.format(DOMAIN)
|
|
| 259 |
+WECHAT_DIRECT_BASE_REDIRECT_URI = '{0}/we/direct_base_redirect'.format(DOMAIN)
|
|
| 260 |
+WECHAT_DIRECT_USERINFO_REDIRECT_URI = '{0}/we/direct_userinfo_redirect'.format(DOMAIN)
|
|
| 261 |
+ |
|
| 262 |
+try: |
|
| 263 |
+ from func_settings import redis_connect |
|
| 264 |
+ REDIS_CACHE = redis_connect(REDIS.get('default', {}))
|
|
| 265 |
+except ImportError: |
|
| 266 |
+ REDIS_CACHE = None |
@@ -0,0 +1,331 @@ |
||
| 1 |
+!(function(e, t) {
|
|
| 2 |
+ var config = {
|
|
| 3 |
+ wxconfig: 'http://api.pai.ai/wx/jsapi_signature', |
|
| 4 |
+ callback: 'callback' |
|
| 5 |
+ }, wxData = {
|
|
| 6 |
+ debug: false, |
|
| 7 |
+ imgUrl: '', |
|
| 8 |
+ link: '', |
|
| 9 |
+ desc: '', |
|
| 10 |
+ title: '', |
|
| 11 |
+ timeLine: '' |
|
| 12 |
+ }, wxConfig = {
|
|
| 13 |
+ hide: false, |
|
| 14 |
+ close: false |
|
| 15 |
+ }, jsApiList = [ |
|
| 16 |
+ 'checkJsApi', |
|
| 17 |
+ 'onMenuShareTimeline', |
|
| 18 |
+ 'onMenuShareAppMessage', |
|
| 19 |
+ 'onMenuShareQQ', |
|
| 20 |
+ 'onMenuShareWeibo', |
|
| 21 |
+ 'hideMenuItems', |
|
| 22 |
+ 'showMenuItems', |
|
| 23 |
+ 'hideAllNonBaseMenuItem', |
|
| 24 |
+ 'showAllNonBaseMenuItem', |
|
| 25 |
+ 'translateVoice', |
|
| 26 |
+ 'startRecord', |
|
| 27 |
+ 'stopRecord', |
|
| 28 |
+ 'onRecordEnd', |
|
| 29 |
+ 'playVoice', |
|
| 30 |
+ 'pauseVoice', |
|
| 31 |
+ 'stopVoice', |
|
| 32 |
+ 'uploadVoice', |
|
| 33 |
+ 'downloadVoice', |
|
| 34 |
+ 'chooseImage', |
|
| 35 |
+ 'previewImage', |
|
| 36 |
+ 'uploadImage', |
|
| 37 |
+ 'downloadImage', |
|
| 38 |
+ 'getNetworkType', |
|
| 39 |
+ 'openLocation', |
|
| 40 |
+ 'getLocation', |
|
| 41 |
+ 'hideOptionMenu', |
|
| 42 |
+ 'showOptionMenu', |
|
| 43 |
+ 'closeWindow', |
|
| 44 |
+ 'scanQRCode', |
|
| 45 |
+ 'chooseWXPay', |
|
| 46 |
+ 'openEnterpriseRedPacket', |
|
| 47 |
+ 'openProductSpecificView', |
|
| 48 |
+ 'addCard', |
|
| 49 |
+ 'chooseCard', |
|
| 50 |
+ 'openCard' |
|
| 51 |
+ ], wxApiFun |
|
| 52 |
+ |
|
| 53 |
+ function isOpenOnPC() { // 判断当前网页是否在 PC 浏览器中打开
|
|
| 54 |
+ var ua = navigator.userAgent |
|
| 55 |
+ return /windows nt/i.test(ua) || /macintosh/i.test(ua) || /linux x86_64/i.test(ua) |
|
| 56 |
+ } |
|
| 57 |
+ |
|
| 58 |
+ function isOpenInWeixin() { // 判断当前网页是否在微信内置浏览器中打开
|
|
| 59 |
+ return /micromessenger/i.test(navigator.userAgent) |
|
| 60 |
+ } |
|
| 61 |
+ |
|
| 62 |
+ function getWeixinVersion() {
|
|
| 63 |
+ var ua = navigator.userAgent, |
|
| 64 |
+ mt = ua.match(/micromessenger\/([\d.]+)/i) |
|
| 65 |
+ return (mt ? mt[1] : '') |
|
| 66 |
+ } |
|
| 67 |
+ |
|
| 68 |
+ // This function checks whether Wechat is the appointed version or not |
|
| 69 |
+ // Cmp: http://jsperf.com/regexp-test-vs-indexof-ignore-upper-and-lower |
|
| 70 |
+ function isWeixinVersion(version) {
|
|
| 71 |
+ // return new RegExp('micromessenger/' + version , 'i').test(navigator.userAgent)
|
|
| 72 |
+ return navigator.userAgent.toLowerCase().indexOf('micromessenger/' + version) != -1
|
|
| 73 |
+ } |
|
| 74 |
+ |
|
| 75 |
+ function hideOptionMenu() {
|
|
| 76 |
+ wxConfig.hide = true |
|
| 77 |
+ fixedWxData() |
|
| 78 |
+ } |
|
| 79 |
+ |
|
| 80 |
+ function showOptionMenu() {
|
|
| 81 |
+ wxConfig.hide = false |
|
| 82 |
+ fixedWxData() |
|
| 83 |
+ } |
|
| 84 |
+ |
|
| 85 |
+ function closeWindow() {
|
|
| 86 |
+ wxConfig.close = true |
|
| 87 |
+ fixedWxData() |
|
| 88 |
+ } |
|
| 89 |
+ |
|
| 90 |
+ function wxReady(data) {
|
|
| 91 |
+ data = typeof data === 'object' ? data : JSON.parse(data) |
|
| 92 |
+ wx.config({
|
|
| 93 |
+ debug: wxData.debug, |
|
| 94 |
+ appId: data.appId, |
|
| 95 |
+ timestamp: data.timestamp, |
|
| 96 |
+ nonceStr: data.nonceStr, |
|
| 97 |
+ signature: data.signature, |
|
| 98 |
+ jsApiList: jsApiList |
|
| 99 |
+ }) |
|
| 100 |
+ |
|
| 101 |
+ var callbacks = {
|
|
| 102 |
+ trigger: function (res) {
|
|
| 103 |
+ // alert('用户点击发送给朋友')
|
|
| 104 |
+ if (JSWE.wxTrigger) {JSWE.wxTrigger(res)}
|
|
| 105 |
+ }, |
|
| 106 |
+ success: function (res) {
|
|
| 107 |
+ // alert('已分享')
|
|
| 108 |
+ if (JSWE.wxSuccess) {JSWE.wxSuccess(res)}
|
|
| 109 |
+ }, |
|
| 110 |
+ cancel: function (res) {
|
|
| 111 |
+ // alert('已取消')
|
|
| 112 |
+ if (JSWE.wxCancel) {JSWE.wxCancel(res)}
|
|
| 113 |
+ }, |
|
| 114 |
+ fail: function (res) {
|
|
| 115 |
+ // alert(JSON.stringify(res)) |
|
| 116 |
+ if (JSWE.wxFail) {JSWE.wxFail(res)}
|
|
| 117 |
+ } |
|
| 118 |
+ }, shareInfo = function(flag) {
|
|
| 119 |
+ var _share = {
|
|
| 120 |
+ title: flag ? wxData.title : (wxData.timeLine || wxData.desc), |
|
| 121 |
+ link: wxData.link, |
|
| 122 |
+ imgUrl: wxData.imgUrl, |
|
| 123 |
+ trigger: callbacks.trigger, |
|
| 124 |
+ success: callbacks.success, |
|
| 125 |
+ cancel: callbacks.cancel, |
|
| 126 |
+ fail: callbacks.fail |
|
| 127 |
+ } |
|
| 128 |
+ if (flag) _share.desc = wxData.desc |
|
| 129 |
+ return _share |
|
| 130 |
+ }, wxShareApi = function() {
|
|
| 131 |
+ // 2. 分享接口 |
|
| 132 |
+ // 2.1 监听“分享给朋友”,按钮点击、自定义分享内容及分享结果接口 |
|
| 133 |
+ wx.onMenuShareAppMessage(shareInfo(1)) |
|
| 134 |
+ // 2.2 监听“分享到朋友圈”按钮点击、自定义分享内容及分享结果接口 |
|
| 135 |
+ wx.onMenuShareTimeline(shareInfo(0)) |
|
| 136 |
+ // 2.3 监听“分享到QQ”按钮点击、自定义分享内容及分享结果接口 |
|
| 137 |
+ wx.onMenuShareQQ(shareInfo(1)) |
|
| 138 |
+ // 2.4 监听“分享到微博”按钮点击、自定义分享内容及分享结果接口 |
|
| 139 |
+ wx.onMenuShareWeibo(shareInfo(1)) |
|
| 140 |
+ }, wxMenuApi = function () {
|
|
| 141 |
+ // 8. 界面操作接口 |
|
| 142 |
+ // 8.1 隐藏右上角菜单 |
|
| 143 |
+ // 8.2 显示右上角菜单 |
|
| 144 |
+ if (wxConfig.hide) {wx.hideOptionMenu()} else {wx.showOptionMenu()}
|
|
| 145 |
+ // 8.7 关闭当前窗口 |
|
| 146 |
+ if (wxConfig.close) {wx.closeWindow()}
|
|
| 147 |
+ }, wxApi = function () {
|
|
| 148 |
+ wxShareApi() |
|
| 149 |
+ wxMenuApi() |
|
| 150 |
+ } |
|
| 151 |
+ |
|
| 152 |
+ wx.ready(wxApi) |
|
| 153 |
+ |
|
| 154 |
+ return wxApiFun = wxApi |
|
| 155 |
+ } |
|
| 156 |
+ |
|
| 157 |
+ if (isOpenInWeixin() || isOpenOnPC()) {
|
|
| 158 |
+ if ('undefined' !== typeof JSWE_CONF_UPDATE) JSWE_CONF_UPDATE(config)
|
|
| 159 |
+ $.ajax({
|
|
| 160 |
+ url: config.wxconfig, |
|
| 161 |
+ type: 'get', |
|
| 162 |
+ dataType: 'jsonp', |
|
| 163 |
+ jsonpCallback: config.callback, |
|
| 164 |
+ data: {
|
|
| 165 |
+ url: window.location.href.split('#')[0]
|
|
| 166 |
+ }, |
|
| 167 |
+ success: wxReady |
|
| 168 |
+ }) |
|
| 169 |
+ } |
|
| 170 |
+ |
|
| 171 |
+ function initWxData(data, flag) {
|
|
| 172 |
+ for(var d in data) {if (d in wxData) wxData[d] = data[d]}
|
|
| 173 |
+ if (flag) fixedWxData() |
|
| 174 |
+ } |
|
| 175 |
+ |
|
| 176 |
+ function changeWxData(key, value, flag) {
|
|
| 177 |
+ if (key in falDwxDataata) {wxData[key] = value}
|
|
| 178 |
+ if (flag) fixedWxData() |
|
| 179 |
+ } |
|
| 180 |
+ |
|
| 181 |
+ function fixedWxData() {
|
|
| 182 |
+ if ('undefined' !== typeof wxApiFun) wxApiFun()
|
|
| 183 |
+ } |
|
| 184 |
+ |
|
| 185 |
+ // 5 图片接口 |
|
| 186 |
+ // 5.1 拍照、本地选图 |
|
| 187 |
+ var images = {
|
|
| 188 |
+ localIds: [], |
|
| 189 |
+ serverIds: [] |
|
| 190 |
+ }; |
|
| 191 |
+ // function chooseImage(count, directUpload, isShowProgressTips) {
|
|
| 192 |
+ function chooseImage(choose_params) {
|
|
| 193 |
+ if ('undefined' === typeof choose_params) choose_params = {}
|
|
| 194 |
+ wx.chooseImage({
|
|
| 195 |
+ count: choose_params.count || 9, // 默认9 |
|
| 196 |
+ sizeType: choose_params.sizeType || ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有 |
|
| 197 |
+ sourceType: choose_params.sourceType || ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有 |
|
| 198 |
+ success: function (res) {
|
|
| 199 |
+ images.localIds = res.localIds; // 返回选定照片的本地ID列表,localId可以作为img标签的src属性显示图片 |
|
| 200 |
+ // 判断是否直接上传 |
|
| 201 |
+ if (choose_params.directUpload) {setTimeout(uploadImages({localIds: images.localIds, isShowProgressTips: choose_params.isShowProgressTips || 1}), 100)}
|
|
| 202 |
+ // 拍照、本地选图成功后的回调函数 |
|
| 203 |
+ if (JSWE.wxChooseImageSuccess) {JSWE.wxChooseImageSuccess(res)}
|
|
| 204 |
+ } |
|
| 205 |
+ }); |
|
| 206 |
+ } |
|
| 207 |
+ |
|
| 208 |
+ // 5.2 图片预览 |
|
| 209 |
+ function previewImage(preview_params) {
|
|
| 210 |
+ wx.previewImage({
|
|
| 211 |
+ current: preview_params.current, // 当前显示图片的链接,不填则默认为 urls 的第一张 |
|
| 212 |
+ urls: preview_params.urls // 需要预览的图片链接列表 |
|
| 213 |
+ }); |
|
| 214 |
+ } |
|
| 215 |
+ |
|
| 216 |
+ // 5.3 上传图片 |
|
| 217 |
+ // function uploadImage(localId, isShowProgressTips) {
|
|
| 218 |
+ function uploadImage(upload_params) {
|
|
| 219 |
+ // 上传图片为异步处理,重复上传同一图片,返回的serverId也是不同的 |
|
| 220 |
+ wx.uploadImage({
|
|
| 221 |
+ localId: upload_params.localId, // 需要上传的图片的本地ID,由chooseImage接口获得 |
|
| 222 |
+ isShowProgressTips: upload_params.isShowProgressTips || 1, // 默认为1,显示进度提示 |
|
| 223 |
+ success: function (res) {
|
|
| 224 |
+ images.serverIds.push(res.serverId); // 返回图片的服务器端ID |
|
| 225 |
+ // 上传图片成功后的回调函数 |
|
| 226 |
+ if (JSWE.wxUploadImageSuccess) {JSWE.wxUploadImageSuccess(res)}
|
|
| 227 |
+ } |
|
| 228 |
+ }); |
|
| 229 |
+ } |
|
| 230 |
+ |
|
| 231 |
+ // function uploadImages(localIds, isShowProgressTips) {
|
|
| 232 |
+ function uploadImages(upload_params) {
|
|
| 233 |
+ var localIds = upload_params.localIds, isShowProgressTips = upload_params.isShowProgressTips || 1 |
|
| 234 |
+ images.serverIds = []; |
|
| 235 |
+ for (var idx in localIds) {uploadImage({localId: localIds[idx], isShowProgressTips: isShowProgressTips})}
|
|
| 236 |
+ } |
|
| 237 |
+ |
|
| 238 |
+ // 9 微信原生接口 |
|
| 239 |
+ // 9.1.1 扫描二维码并返回结果 |
|
| 240 |
+ // 9.1.2 扫描二维码并返回结果 |
|
| 241 |
+ function scanQRCode(scan_params) {
|
|
| 242 |
+ if ('undefined' === typeof scan_params) scan_params = {}
|
|
| 243 |
+ wx.scanQRCode({
|
|
| 244 |
+ needResult: scan_params.needResult || 0, // 默认为0,0扫描结果由微信处理,1直接返回扫描结果 |
|
| 245 |
+ scanType: scan_params.scanType || ['qrCode', 'barCode'], // 可以指定扫二维码还是一维码,默认二者都有 |
|
| 246 |
+ success: function (res) { // 当 needResult 为 1 时,扫码返回的结果
|
|
| 247 |
+ if (JSWE.wxScanQRCodeSuccess) {JSWE.wxScanQRCodeSuccess(res)}
|
|
| 248 |
+ } |
|
| 249 |
+ }); |
|
| 250 |
+ } |
|
| 251 |
+ |
|
| 252 |
+ // QRCode & BarCode is different |
|
| 253 |
+ function parseScanQRCodeResultStr(resultStr) {
|
|
| 254 |
+ var strs = resultStr.split(',')
|
|
| 255 |
+ return strs[strs.length - 1] |
|
| 256 |
+ } |
|
| 257 |
+ |
|
| 258 |
+ // 10 微信支付接口 |
|
| 259 |
+ // 10.1 发起一个支付请求 |
|
| 260 |
+ function chooseWXPay(wxpay_params) {
|
|
| 261 |
+ wx.chooseWXPay({
|
|
| 262 |
+ timestamp: wxpay_params.timeStamp, // 支付签名时间戳,注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符 |
|
| 263 |
+ nonceStr: wxpay_params.nonceStr, // 支付签名随机串,不长于 32 位 |
|
| 264 |
+ package: wxpay_params.package, // 统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=***) |
|
| 265 |
+ signType: wxpay_params.signType, // 签名方式,默认为'SHA1',使用新版支付需传入'MD5' |
|
| 266 |
+ paySign: wxpay_params.paySign, // 支付签名 |
|
| 267 |
+ success: function (res) {
|
|
| 268 |
+ // 支付成功后的回调函数 |
|
| 269 |
+ if (JSWE.wxPaySuccess) {JSWE.wxPaySuccess(res)}
|
|
| 270 |
+ } |
|
| 271 |
+ }) |
|
| 272 |
+ } |
|
| 273 |
+ |
|
| 274 |
+ // xx 微信原生企业红包接口 |
|
| 275 |
+ // xx.1 发起一个发送原生企业红包请求 |
|
| 276 |
+ function openEnterpriseRedPacket(wxredpack_params) {
|
|
| 277 |
+ wx.openEnterpriseRedPacket({
|
|
| 278 |
+ timeStamp: wxredpack_params.timeStamp, // 红包签名时间戳,注意原生企业红包接口timeStamp字段名需大写其中的S字符,而支付接口timeStamp字段名无需大写其中的S字符。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符 |
|
| 279 |
+ nonceStr: wxredpack_params.nonceStr, // 红包签名随机串,不长于 32 位 |
|
| 280 |
+ package: encodeURIComponent(wxredpack_params.package), // 发放红包接口返回的prepay_id参数值,提交格式如:prepay_id=***) |
|
| 281 |
+ signType: wxredpack_params.signType, // 签名方式,默认为'SHA1',使用新版支付需传入'MD5' |
|
| 282 |
+ paySign: wxredpack_params.paySign, // 红包签名 |
|
| 283 |
+ success: function (res) {
|
|
| 284 |
+ // 发送原生企业红包成功后的回调函数 |
|
| 285 |
+ if (JSWE.wxEnterpriseRedPacketSuccess) {JSWE.wxEnterpriseRedPacketSuccess(res)}
|
|
| 286 |
+ } |
|
| 287 |
+ }) |
|
| 288 |
+ } |
|
| 289 |
+ |
|
| 290 |
+ var v = {
|
|
| 291 |
+ version: '1.0.5', |
|
| 292 |
+ |
|
| 293 |
+ // Basic Vars |
|
| 294 |
+ config: config, |
|
| 295 |
+ wxData: wxData, |
|
| 296 |
+ jsApiList: jsApiList, |
|
| 297 |
+ |
|
| 298 |
+ // Weixin Function |
|
| 299 |
+ isOpenInWeixin: isOpenInWeixin, |
|
| 300 |
+ getWeixinVersion: getWeixinVersion, |
|
| 301 |
+ isWeixinVersion: isWeixinVersion, |
|
| 302 |
+ |
|
| 303 |
+ // Menu Function |
|
| 304 |
+ hideOptionMenu: hideOptionMenu, |
|
| 305 |
+ showOptionMenu: showOptionMenu, |
|
| 306 |
+ closeWindow: closeWindow, |
|
| 307 |
+ |
|
| 308 |
+ // Share Function |
|
| 309 |
+ initWxData: initWxData, |
|
| 310 |
+ changeWxData: changeWxData, |
|
| 311 |
+ fixedWxData: fixedWxData, |
|
| 312 |
+ |
|
| 313 |
+ // Image Function |
|
| 314 |
+ images: images, |
|
| 315 |
+ chooseImage: chooseImage, |
|
| 316 |
+ previewImage: previewImage, |
|
| 317 |
+ uploadImage: uploadImage, |
|
| 318 |
+ uploadImages: uploadImages, |
|
| 319 |
+ |
|
| 320 |
+ // Scan Function |
|
| 321 |
+ scanQRCode: scanQRCode, |
|
| 322 |
+ parseScanQRCodeResultStr: parseScanQRCodeResultStr, |
|
| 323 |
+ |
|
| 324 |
+ // Pay Function |
|
| 325 |
+ chooseWXPay: chooseWXPay, |
|
| 326 |
+ |
|
| 327 |
+ // EnterpriseRedPacket Function |
|
| 328 |
+ openEnterpriseRedPacket: openEnterpriseRedPacket |
|
| 329 |
+ } |
|
| 330 |
+ e.JSWE = e.V = v |
|
| 331 |
+})(window) |
@@ -0,0 +1,36 @@ |
||
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+ |
|
| 3 |
+"""course URL Configuration |
|
| 4 |
+ |
|
| 5 |
+The `urlpatterns` list routes URLs to views. For more information please see: |
|
| 6 |
+ https://docs.djangoproject.com/en/1.11/topics/http/urls/ |
|
| 7 |
+Examples: |
|
| 8 |
+Function views |
|
| 9 |
+ 1. Add an import: from my_app import views |
|
| 10 |
+ 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') |
|
| 11 |
+Class-based views |
|
| 12 |
+ 1. Add an import: from other_app.views import Home |
|
| 13 |
+ 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') |
|
| 14 |
+Including another URLconf |
|
| 15 |
+ 1. Import the include() function: from django.conf.urls import url, include |
|
| 16 |
+ 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
|
|
| 17 |
+""" |
|
| 18 |
+from django.conf import settings |
|
| 19 |
+from django.conf.urls import include, url |
|
| 20 |
+from django.conf.urls.static import static |
|
| 21 |
+from django.contrib import admin |
|
| 22 |
+ |
|
| 23 |
+ |
|
| 24 |
+urlpatterns = [ |
|
| 25 |
+ url(r'^courseadmin/', admin.site.urls), |
|
| 26 |
+ url(r'^api/', include('api.urls', namespace='api')),
|
|
| 27 |
+ url(r'^uniapi/', include('django_uniapi.urls', namespace='uniapi')),
|
|
| 28 |
+ url(r'^we/', include('django_we.urls', namespace='wechat')),
|
|
| 29 |
+ |
|
| 30 |
+ url(r'^page/', include('page.urls', namespace='page')),
|
|
| 31 |
+] |
|
| 32 |
+ |
|
| 33 |
+urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) |
|
| 34 |
+urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) |
|
| 35 |
+ |
|
| 36 |
+admin.site.site_header = u'合作课程系统' |
@@ -0,0 +1,17 @@ |
||
| 1 |
+""" |
|
| 2 |
+WSGI config for course project. |
|
| 3 |
+ |
|
| 4 |
+It exposes the WSGI callable as a module-level variable named ``application``. |
|
| 5 |
+ |
|
| 6 |
+For more information on this file, see |
|
| 7 |
+https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ |
|
| 8 |
+""" |
|
| 9 |
+ |
|
| 10 |
+import os |
|
| 11 |
+ |
|
| 12 |
+from django.core.wsgi import get_wsgi_application |
|
| 13 |
+ |
|
| 14 |
+ |
|
| 15 |
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "course.settings")
|
|
| 16 |
+ |
|
| 17 |
+application = get_wsgi_application() |
@@ -0,0 +1,20 @@ |
||
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+ |
|
| 3 |
+from djadmin import ExportExcelModelAdmin, ReadonlyModelAdmin |
|
| 4 |
+from django.contrib import admin |
|
| 5 |
+ |
|
| 6 |
+from courses.models import CourseInfo, CourseVideoInfo |
|
| 7 |
+ |
|
| 8 |
+ |
|
| 9 |
+class CourseInfoAdmin(ExportExcelModelAdmin, admin.ModelAdmin): |
|
| 10 |
+ list_display = ('course_id', 'course_name', 'course_time', 'course_cover', 'status', 'created_at', 'updated_at')
|
|
| 11 |
+ list_filter = ('status', )
|
|
| 12 |
+ |
|
| 13 |
+ |
|
| 14 |
+class CourseVideoInfoAdmin(ExportExcelModelAdmin, admin.ModelAdmin): |
|
| 15 |
+ list_display = ('course', 'course_video_id', 'course_video_type', 'course_video_name', 'course_video_desc', 'course_video_time', 'course_video_cover', 'course_video_position', 'status', 'created_at', 'updated_at')
|
|
| 16 |
+ list_filter = ('course', 'status')
|
|
| 17 |
+ |
|
| 18 |
+ |
|
| 19 |
+admin.site.register(CourseInfo, CourseInfoAdmin) |
|
| 20 |
+admin.site.register(CourseVideoInfo, CourseVideoInfoAdmin) |
@@ -0,0 +1,8 @@ |
||
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+from __future__ import unicode_literals |
|
| 3 |
+ |
|
| 4 |
+from django.apps import AppConfig |
|
| 5 |
+ |
|
| 6 |
+ |
|
| 7 |
+class CoursesConfig(AppConfig): |
|
| 8 |
+ name = 'courses' |
@@ -0,0 +1,57 @@ |
||
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+# Generated by Django 1.11.3 on 2017-09-25 07:47 |
|
| 3 |
+from __future__ import unicode_literals |
|
| 4 |
+ |
|
| 5 |
+import courses.models |
|
| 6 |
+from django.db import migrations, models |
|
| 7 |
+import django.db.models.deletion |
|
| 8 |
+import shortuuidfield.fields |
|
| 9 |
+ |
|
| 10 |
+ |
|
| 11 |
+class Migration(migrations.Migration): |
|
| 12 |
+ |
|
| 13 |
+ initial = True |
|
| 14 |
+ |
|
| 15 |
+ dependencies = [ |
|
| 16 |
+ ] |
|
| 17 |
+ |
|
| 18 |
+ operations = [ |
|
| 19 |
+ migrations.CreateModel( |
|
| 20 |
+ name='CourseInfo', |
|
| 21 |
+ fields=[ |
|
| 22 |
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
| 23 |
+ ('status', models.BooleanField(db_index=True, default=True, help_text='\u72b6\u6001', verbose_name='status')),
|
|
| 24 |
+ ('created_at', models.DateTimeField(auto_now_add=True, help_text='\u521b\u5efa\u65f6\u95f4', verbose_name='created_at')),
|
|
| 25 |
+ ('updated_at', models.DateTimeField(auto_now=True, help_text='\u66f4\u65b0\u65f6\u95f4', verbose_name='updated_at')),
|
|
| 26 |
+ ('course_id', shortuuidfield.fields.ShortUUIDField(blank=True, db_index=True, editable=False, help_text='\u8bfe\u7a0b\u552f\u4e00\u6807\u8bc6', max_length=22, unique=True)),
|
|
| 27 |
+ ('course_name', models.CharField(blank=True, help_text='\u8bfe\u7a0b\u6807\u9898', max_length=255, null=True, verbose_name='course_name')),
|
|
| 28 |
+ ('course_time', models.IntegerField(default=0, help_text='\u8bfe\u7a0b\u65f6\u95f4', verbose_name='course_time')),
|
|
| 29 |
+ ('course_cover', models.ImageField(blank=True, help_text='\u8bfe\u7a0b\u89c6\u9891\u7f29\u7565\u56fe', null=True, upload_to=courses.models.upload_path, verbose_name='course_cover')),
|
|
| 30 |
+ ], |
|
| 31 |
+ options={
|
|
| 32 |
+ 'verbose_name': 'courseinfo', |
|
| 33 |
+ 'verbose_name_plural': 'courseinfo', |
|
| 34 |
+ }, |
|
| 35 |
+ ), |
|
| 36 |
+ migrations.CreateModel( |
|
| 37 |
+ name='CourseVideoInfo', |
|
| 38 |
+ fields=[ |
|
| 39 |
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
| 40 |
+ ('status', models.BooleanField(db_index=True, default=True, help_text='\u72b6\u6001', verbose_name='status')),
|
|
| 41 |
+ ('created_at', models.DateTimeField(auto_now_add=True, help_text='\u521b\u5efa\u65f6\u95f4', verbose_name='created_at')),
|
|
| 42 |
+ ('updated_at', models.DateTimeField(auto_now=True, help_text='\u66f4\u65b0\u65f6\u95f4', verbose_name='updated_at')),
|
|
| 43 |
+ ('course_video_id', shortuuidfield.fields.ShortUUIDField(blank=True, db_index=True, editable=False, help_text='\u8bfe\u7a0b\u89c6\u9891\u552f\u4e00\u6807\u8bc6', max_length=22, unique=True)),
|
|
| 44 |
+ ('course_video_type', models.CharField(blank=True, help_text='\u8bfe\u7a0b\u89c6\u9891\u7c7b\u578b', max_length=255, null=True, verbose_name='course_video_type')),
|
|
| 45 |
+ ('course_video_name', models.CharField(blank=True, help_text='\u8bfe\u7a0b\u89c6\u9891\u6807\u9898', max_length=255, null=True, verbose_name='course_video_name')),
|
|
| 46 |
+ ('course_video_desc', models.TextField(blank=True, help_text='\u8bfe\u7a0b\u89c6\u9891\u63cf\u8ff0', null=True, verbose_name='course_video_desc')),
|
|
| 47 |
+ ('course_video_time', models.IntegerField(default=0, help_text='\u8bfe\u7a0b\u89c6\u9891\u65f6\u95f4', verbose_name='course_video_time')),
|
|
| 48 |
+ ('course_video_cover', models.ImageField(blank=True, help_text='\u8bfe\u7a0b\u89c6\u9891\u7f29\u7565\u56fe', null=True, upload_to=courses.models.upload_path, verbose_name='course_video_cover')),
|
|
| 49 |
+ ('course_video_position', models.IntegerField(default=0, help_text='\u8bfe\u7a0b\u89c6\u9891\u6392\u5e8f', verbose_name='course_video_position')),
|
|
| 50 |
+ ('course', models.ForeignKey(blank=True, help_text='\u8bfe\u7a0b', null=True, on_delete=django.db.models.deletion.CASCADE, to='courses.CourseInfo', verbose_name='course')),
|
|
| 51 |
+ ], |
|
| 52 |
+ options={
|
|
| 53 |
+ 'verbose_name': 'coursevideoinfo', |
|
| 54 |
+ 'verbose_name_plural': 'coursevideoinfo', |
|
| 55 |
+ }, |
|
| 56 |
+ ), |
|
| 57 |
+ ] |
@@ -0,0 +1,21 @@ |
||
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+# Generated by Django 1.11.3 on 2017-09-25 08:55 |
|
| 3 |
+from __future__ import unicode_literals |
|
| 4 |
+ |
|
| 5 |
+import courses.models |
|
| 6 |
+from django.db import migrations, models |
|
| 7 |
+ |
|
| 8 |
+ |
|
| 9 |
+class Migration(migrations.Migration): |
|
| 10 |
+ |
|
| 11 |
+ dependencies = [ |
|
| 12 |
+ ('courses', '0001_initial'),
|
|
| 13 |
+ ] |
|
| 14 |
+ |
|
| 15 |
+ operations = [ |
|
| 16 |
+ migrations.AddField( |
|
| 17 |
+ model_name='coursevideoinfo', |
|
| 18 |
+ name='course_video', |
|
| 19 |
+ field=models.FileField(blank=True, help_text='\u8bfe\u7a0b\u89c6\u9891', null=True, upload_to=courses.models.upload_path, verbose_name='course_video'), |
|
| 20 |
+ ), |
|
| 21 |
+ ] |
@@ -0,0 +1,87 @@ |
||
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+ |
|
| 3 |
+import os |
|
| 4 |
+ |
|
| 5 |
+from django.db import models |
|
| 6 |
+from django.utils.translation import ugettext_lazy as _ |
|
| 7 |
+from shortuuidfield import ShortUUIDField |
|
| 8 |
+from TimeConvert import TimeConvert as tc |
|
| 9 |
+ |
|
| 10 |
+from course.basemodels import CreateUpdateMixin |
|
| 11 |
+from utils.url_utils import upload_file_url |
|
| 12 |
+ |
|
| 13 |
+ |
|
| 14 |
+def upload_path(instance, old_filename): |
|
| 15 |
+ return 'file/{ym}/{stamp}{ext}'.format(
|
|
| 16 |
+ ym=tc.local_string(format='%Y%m'), |
|
| 17 |
+ stamp=tc.local_timestamp(ms=True), |
|
| 18 |
+ ext=os.path.splitext(old_filename)[1].lower(), |
|
| 19 |
+ ) |
|
| 20 |
+ |
|
| 21 |
+ |
|
| 22 |
+class CourseInfo(CreateUpdateMixin): |
|
| 23 |
+ course_id = ShortUUIDField(_(u'course_id'), max_length=255, help_text=u'课程唯一标识', db_index=True, unique=True) |
|
| 24 |
+ course_name = models.CharField(_(u'course_name'), max_length=255, blank=True, null=True, help_text=u'课程标题') |
|
| 25 |
+ course_time = models.IntegerField(_(u'course_time'), default=0, help_text=u'课程时间') |
|
| 26 |
+ course_cover = models.ImageField(_(u'course_cover'), upload_to=upload_path, blank=True, null=True, help_text=u'课程视频缩略图') |
|
| 27 |
+ |
|
| 28 |
+ class Meta: |
|
| 29 |
+ verbose_name = _(u'courseinfo') |
|
| 30 |
+ verbose_name_plural = _(u'courseinfo') |
|
| 31 |
+ |
|
| 32 |
+ def __unicode__(self): |
|
| 33 |
+ return unicode(self.course_name) |
|
| 34 |
+ |
|
| 35 |
+ @property |
|
| 36 |
+ def course_cover_url(self): |
|
| 37 |
+ return upload_file_url(self.course_cover) |
|
| 38 |
+ |
|
| 39 |
+ @property |
|
| 40 |
+ def data(self): |
|
| 41 |
+ return {
|
|
| 42 |
+ 'course_id': self.course_id, |
|
| 43 |
+ 'course_name': self.course_name, |
|
| 44 |
+ 'course_time': self.course_time, |
|
| 45 |
+ 'course_cover_url': self.course_cover_url, |
|
| 46 |
+ } |
|
| 47 |
+ |
|
| 48 |
+ |
|
| 49 |
+class CourseVideoInfo(CreateUpdateMixin): |
|
| 50 |
+ course = models.ForeignKey(CourseInfo, verbose_name=_(u'course'), blank=True, null=True, help_text=u'课程', db_index=True) |
|
| 51 |
+ course_video_id = ShortUUIDField(_(u'course_video_id'), max_length=255, help_text=u'课程视频唯一标识', db_index=True, unique=True) |
|
| 52 |
+ course_video_type = models.CharField(_(u'course_video_type'), max_length=255, blank=True, null=True, help_text=u'课程视频类型') |
|
| 53 |
+ course_video_name = models.CharField(_(u'course_video_name'), max_length=255, blank=True, null=True, help_text=u'课程视频标题') |
|
| 54 |
+ course_video_desc = models.TextField(_(u'course_video_desc'), blank=True, null=True, help_text=u'课程视频描述') |
|
| 55 |
+ course_video_time = models.IntegerField(_(u'course_video_time'), default=0, help_text=u'课程视频时间') |
|
| 56 |
+ course_video_cover = models.ImageField(_(u'course_video_cover'), upload_to=upload_path, blank=True, null=True, help_text=u'课程视频缩略图') |
|
| 57 |
+ course_video = models.FileField(_(u'course_video'), upload_to=upload_path, blank=True, null=True, help_text=u'课程视频') |
|
| 58 |
+ course_video_position = models.IntegerField(_(u'course_video_position'), default=0, help_text=u'课程视频排序') |
|
| 59 |
+ |
|
| 60 |
+ class Meta: |
|
| 61 |
+ verbose_name = _(u'coursevideoinfo') |
|
| 62 |
+ verbose_name_plural = _(u'coursevideoinfo') |
|
| 63 |
+ |
|
| 64 |
+ def __unicode__(self): |
|
| 65 |
+ return unicode(self.pk) |
|
| 66 |
+ |
|
| 67 |
+ @property |
|
| 68 |
+ def course_video_cover_url(self): |
|
| 69 |
+ return upload_file_url(self.course_video_cover) |
|
| 70 |
+ |
|
| 71 |
+ @property |
|
| 72 |
+ def course_video_url(self): |
|
| 73 |
+ return upload_file_url(self.course_video) |
|
| 74 |
+ |
|
| 75 |
+ @property |
|
| 76 |
+ def data(self): |
|
| 77 |
+ return {
|
|
| 78 |
+ 'course_id': self.course.course_id, |
|
| 79 |
+ 'course_video_id': self.course_video_id, |
|
| 80 |
+ 'course_video_type': self.course_video_type, |
|
| 81 |
+ 'course_video_name': self.course_video_name, |
|
| 82 |
+ 'course_video_desc': self.course_video_desc, |
|
| 83 |
+ 'course_video_time': self.course_video_time, |
|
| 84 |
+ 'course_video_cover_url': self.course_video_cover_url, |
|
| 85 |
+ 'course_video_url': self.course_video_url, |
|
| 86 |
+ 'course_video_position': self.course_video_position, |
|
| 87 |
+ } |
@@ -0,0 +1,7 @@ |
||
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+from __future__ import unicode_literals |
|
| 3 |
+ |
|
| 4 |
+from django.test import TestCase |
|
| 5 |
+ |
|
| 6 |
+ |
|
| 7 |
+# Create your tests here. |
@@ -0,0 +1,7 @@ |
||
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+from __future__ import unicode_literals |
|
| 3 |
+ |
|
| 4 |
+from django.shortcuts import render |
|
| 5 |
+ |
|
| 6 |
+ |
|
| 7 |
+# Create your views here. |
@@ -0,0 +1,3 @@ |
||
| 1 |
+#!/bin/bash |
|
| 2 |
+ |
|
| 3 |
+isort -rc -sp . . |
@@ -0,0 +1,23 @@ |
||
| 1 |
+#!/usr/bin/env python |
|
| 2 |
+import os |
|
| 3 |
+import sys |
|
| 4 |
+ |
|
| 5 |
+ |
|
| 6 |
+if __name__ == "__main__": |
|
| 7 |
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "course.settings")
|
|
| 8 |
+ try: |
|
| 9 |
+ from django.core.management import execute_from_command_line |
|
| 10 |
+ except ImportError: |
|
| 11 |
+ # The above import may fail for some other reason. Ensure that the |
|
| 12 |
+ # issue is really that Django is missing to avoid masking other |
|
| 13 |
+ # exceptions on Python 2. |
|
| 14 |
+ try: |
|
| 15 |
+ import django |
|
| 16 |
+ except ImportError: |
|
| 17 |
+ raise ImportError( |
|
| 18 |
+ "Couldn't import Django. Are you sure it's installed and " |
|
| 19 |
+ "available on your PYTHONPATH environment variable? Did you " |
|
| 20 |
+ "forget to activate a virtual environment?" |
|
| 21 |
+ ) |
|
| 22 |
+ raise |
|
| 23 |
+ execute_from_command_line(sys.argv) |
@@ -0,0 +1,4 @@ |
||
| 1 |
+from django.contrib import admin |
|
| 2 |
+ |
|
| 3 |
+ |
|
| 4 |
+# Register your models here. |
@@ -0,0 +1,67 @@ |
||
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+ |
|
| 3 |
+from __future__ import division |
|
| 4 |
+ |
|
| 5 |
+from django.conf import settings |
|
| 6 |
+from django.core.urlresolvers import reverse |
|
| 7 |
+from django.db import transaction |
|
| 8 |
+from django.shortcuts import redirect, render |
|
| 9 |
+from furl import furl |
|
| 10 |
+ |
|
| 11 |
+from account.models import UserInfo |
|
| 12 |
+from codes.models import CourseCodeInfo |
|
| 13 |
+from course.decorators import check_token |
|
| 14 |
+from utils.error.errno_utils import CourseCodeStatusCode, ProfileStatusCode |
|
| 15 |
+from utils.error.response_utils import response |
|
| 16 |
+ |
|
| 17 |
+ |
|
| 18 |
+@check_token |
|
| 19 |
+@transaction.atomic |
|
| 20 |
+def course_code(request): |
|
| 21 |
+ unique_identifier = request.GET.get(settings.WECHAT_UNIQUE_IDENTIFICATION, '') |
|
| 22 |
+ |
|
| 23 |
+ user, created = UserInfo.objects.select_for_update().get_or_create(**{settings.WECHAT_UNIQUE_IDENTIFICATION: unique_identifier})
|
|
| 24 |
+ user.unionid = request.GET.get('unionid', '')
|
|
| 25 |
+ user.openid = request.GET.get('openid', '')
|
|
| 26 |
+ user.nickname = request.GET.get('nickname', '')
|
|
| 27 |
+ user.avatar = request.GET.get('headimgurl', '')
|
|
| 28 |
+ user.save() |
|
| 29 |
+ |
|
| 30 |
+ try: |
|
| 31 |
+ course_code = CourseCodeInfo.objects.get(user_id=user.user_id, exchanged=True, status=True) |
|
| 32 |
+ except CourseCodeInfo.DoesNotExist: |
|
| 33 |
+ course_code = None |
|
| 34 |
+ |
|
| 35 |
+ if course_code: |
|
| 36 |
+ return redirect(furl(reverse('page:course_list')).add(request.GET).add({'user_id': user.user_id}).url)
|
|
| 37 |
+ |
|
| 38 |
+ return render(request, 'page/course_code.html', {
|
|
| 39 |
+ 'domain': settings.DOMAIN, |
|
| 40 |
+ 'user_info': user.data, |
|
| 41 |
+ 'params': 'user_id={}&vtoken={}'.format(user.user_id, request.GET.get('vtoken', '')),
|
|
| 42 |
+ }) |
|
| 43 |
+ |
|
| 44 |
+ |
|
| 45 |
+@transaction.atomic |
|
| 46 |
+def code_exchange(request): |
|
| 47 |
+ user_id = request.POST.get('user_id', '')
|
|
| 48 |
+ code = request.POST.get('code', '')
|
|
| 49 |
+ |
|
| 50 |
+ try: |
|
| 51 |
+ user = UserInfo.objects.select_for_update().get(user_id=user_id) |
|
| 52 |
+ except UserInfo.DoesNotExist: |
|
| 53 |
+ return response(ProfileStatusCode.PROFILE_NOT_FOUND) |
|
| 54 |
+ |
|
| 55 |
+ try: |
|
| 56 |
+ course_code = CourseCodeInfo.objects.select_for_update().get(code=code, status=True) |
|
| 57 |
+ except CourseCodeInfo.DoesNotExist: |
|
| 58 |
+ return response(CourseCodeStatusCode.COURSE_CODE_NOT_FOUND) |
|
| 59 |
+ |
|
| 60 |
+ if course_code.exchanged: |
|
| 61 |
+ return response(CourseCodeStatusCode.COURSE_CODE_HAS_EXCHANGED) |
|
| 62 |
+ |
|
| 63 |
+ course_code.user_id = user.user_id |
|
| 64 |
+ course_code.exchanged = True |
|
| 65 |
+ course_code.save() |
|
| 66 |
+ |
|
| 67 |
+ return response(200, 'Course Code Exchanged Success', u'课程兑换码兑换成功') |
@@ -0,0 +1,47 @@ |
||
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+ |
|
| 3 |
+from __future__ import division |
|
| 4 |
+ |
|
| 5 |
+from django.conf import settings |
|
| 6 |
+from django.shortcuts import render |
|
| 7 |
+ |
|
| 8 |
+from account.models import UserInfo |
|
| 9 |
+from codes.models import CourseCodeInfo |
|
| 10 |
+from course.decorators import check_token |
|
| 11 |
+from courses.models import CourseInfo, CourseVideoInfo |
|
| 12 |
+from utils.error.errno_utils import CourseCodeStatusCode, CourseStatusCode, ProfileStatusCode |
|
| 13 |
+from utils.error.response_utils import response |
|
| 14 |
+ |
|
| 15 |
+ |
|
| 16 |
+@check_token |
|
| 17 |
+def course_info(request): |
|
| 18 |
+ user_id = request.GET.get('user_id', '')
|
|
| 19 |
+ course_id = request.GET.get('course_id', '')
|
|
| 20 |
+ |
|
| 21 |
+ try: |
|
| 22 |
+ user = UserInfo.objects.get(user_id=user_id, status=True) |
|
| 23 |
+ except UserInfo.DoesNotExist: |
|
| 24 |
+ return response(ProfileStatusCode.PROFILE_NOT_FOUND) |
|
| 25 |
+ |
|
| 26 |
+ try: |
|
| 27 |
+ course_code = CourseCodeInfo.objects.get(user_id=user.user_id, exchanged=True, status=True) |
|
| 28 |
+ except CourseCodeInfo.DoesNotExist: |
|
| 29 |
+ response(CourseCodeStatusCode.COURSE_CODE_NOT_FOUND) |
|
| 30 |
+ |
|
| 31 |
+ try: |
|
| 32 |
+ course = CourseInfo.objects.get(course_id=course_id) |
|
| 33 |
+ except CourseInfo.DoesNotExist: |
|
| 34 |
+ response(CourseStatusCode.COURSE_NOT_FOUND) |
|
| 35 |
+ |
|
| 36 |
+ videos = CourseVideoInfo.objects.filter(course=course, status=True).order_by('course_video_position')
|
|
| 37 |
+ videos = [video.data for video in videos] |
|
| 38 |
+ |
|
| 39 |
+ video_count = len(videos) |
|
| 40 |
+ |
|
| 41 |
+ return render(request, 'page/course_info.html', {
|
|
| 42 |
+ 'domain': settings.DOMAIN, |
|
| 43 |
+ 'video_default': videos[0] if video_count else '', |
|
| 44 |
+ 'video_count': video_count, |
|
| 45 |
+ 'videos': videos, |
|
| 46 |
+ 'params': 'user_id={}&vtoken={}'.format(user_id, request.GET.get('vtoken', '')),
|
|
| 47 |
+ }) |
@@ -0,0 +1,37 @@ |
||
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+ |
|
| 3 |
+from __future__ import division |
|
| 4 |
+ |
|
| 5 |
+from django.conf import settings |
|
| 6 |
+from django.shortcuts import render |
|
| 7 |
+ |
|
| 8 |
+from account.models import UserInfo |
|
| 9 |
+from codes.models import CourseCodeInfo |
|
| 10 |
+from course.decorators import check_token |
|
| 11 |
+from courses.models import CourseInfo |
|
| 12 |
+from utils.error.errno_utils import CourseCodeStatusCode, ProfileStatusCode |
|
| 13 |
+from utils.error.response_utils import response |
|
| 14 |
+ |
|
| 15 |
+ |
|
| 16 |
+@check_token |
|
| 17 |
+def course_list(request): |
|
| 18 |
+ user_id = request.GET.get('user_id', '')
|
|
| 19 |
+ |
|
| 20 |
+ try: |
|
| 21 |
+ user = UserInfo.objects.get(user_id=user_id, status=True) |
|
| 22 |
+ except UserInfo.DoesNotExist: |
|
| 23 |
+ return response(ProfileStatusCode.PROFILE_NOT_FOUND) |
|
| 24 |
+ |
|
| 25 |
+ try: |
|
| 26 |
+ course_code = CourseCodeInfo.objects.get(user_id=user.user_id, exchanged=True, status=True) |
|
| 27 |
+ except CourseCodeInfo.DoesNotExist: |
|
| 28 |
+ response(CourseCodeStatusCode.COURSE_CODE_NOT_FOUND) |
|
| 29 |
+ |
|
| 30 |
+ courses = CourseInfo.objects.filter(status=True) |
|
| 31 |
+ courses = [course.data for course in courses] |
|
| 32 |
+ |
|
| 33 |
+ return render(request, 'page/course_list.html', {
|
|
| 34 |
+ 'domain': settings.DOMAIN, |
|
| 35 |
+ 'courses': courses, |
|
| 36 |
+ 'params': 'user_id={}&vtoken={}'.format(user_id, request.GET.get('vtoken', '')),
|
|
| 37 |
+ }) |
@@ -0,0 +1,31 @@ |
||
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+# Generated by Django 1.11.3 on 2017-09-24 14:50 |
|
| 3 |
+from __future__ import unicode_literals |
|
| 4 |
+ |
|
| 5 |
+from django.db import migrations, models |
|
| 6 |
+import page.models |
|
| 7 |
+ |
|
| 8 |
+ |
|
| 9 |
+class Migration(migrations.Migration): |
|
| 10 |
+ |
|
| 11 |
+ initial = True |
|
| 12 |
+ |
|
| 13 |
+ dependencies = [ |
|
| 14 |
+ ] |
|
| 15 |
+ |
|
| 16 |
+ operations = [ |
|
| 17 |
+ migrations.CreateModel( |
|
| 18 |
+ name='CourseCodeSettingInfo', |
|
| 19 |
+ fields=[ |
|
| 20 |
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
| 21 |
+ ('status', models.BooleanField(db_index=True, default=True, help_text='\u72b6\u6001', verbose_name='status')),
|
|
| 22 |
+ ('created_at', models.DateTimeField(auto_now_add=True, help_text='\u521b\u5efa\u65f6\u95f4', verbose_name='created_at')),
|
|
| 23 |
+ ('updated_at', models.DateTimeField(auto_now=True, help_text='\u66f4\u65b0\u65f6\u95f4', verbose_name='updated_at')),
|
|
| 24 |
+ ('cover_image', models.ImageField(blank=True, help_text='\u5151\u6362\u8bfe\u7a0b\u9875\u56fe\u7247', null=True, upload_to=page.models.upload_path, verbose_name='cover_image')),
|
|
| 25 |
+ ], |
|
| 26 |
+ options={
|
|
| 27 |
+ 'verbose_name': 'coursecodesettinginfo', |
|
| 28 |
+ 'verbose_name_plural': 'coursecodesettinginfo', |
|
| 29 |
+ }, |
|
| 30 |
+ ), |
|
| 31 |
+ ] |
@@ -0,0 +1,33 @@ |
||
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+ |
|
| 3 |
+import os |
|
| 4 |
+ |
|
| 5 |
+from django.db import models |
|
| 6 |
+from django.utils.translation import ugettext_lazy as _ |
|
| 7 |
+from TimeConvert import TimeConvert as tc |
|
| 8 |
+ |
|
| 9 |
+from course.basemodels import CreateUpdateMixin |
|
| 10 |
+from utils.url_utils import upload_file_url |
|
| 11 |
+ |
|
| 12 |
+ |
|
| 13 |
+def upload_path(instance, old_filename): |
|
| 14 |
+ return 'file/{ym}/{stamp}{ext}'.format(
|
|
| 15 |
+ ym=tc.local_string(format='%Y%m'), |
|
| 16 |
+ stamp=tc.local_timestamp(ms=True), |
|
| 17 |
+ ext=os.path.splitext(old_filename)[1].lower(), |
|
| 18 |
+ ) |
|
| 19 |
+ |
|
| 20 |
+ |
|
| 21 |
+class CourseCodeSettingInfo(CreateUpdateMixin): |
|
| 22 |
+ cover_image = models.ImageField(_(u'cover_image'), upload_to=upload_path, blank=True, null=True, help_text=u'兑换课程页图片') |
|
| 23 |
+ |
|
| 24 |
+ class Meta: |
|
| 25 |
+ verbose_name = _(u'coursecodesettinginfo') |
|
| 26 |
+ verbose_name_plural = _(u'coursecodesettinginfo') |
|
| 27 |
+ |
|
| 28 |
+ def __unicode__(self): |
|
| 29 |
+ return unicode(self.pk) |
|
| 30 |
+ |
|
| 31 |
+ @property |
|
| 32 |
+ def cover_image_url(self): |
|
| 33 |
+ return upload_file_url(self.cover_image) |
@@ -0,0 +1,67 @@ |
||
| 1 |
+/* Input valid or invalid */ |
|
| 2 |
+input:required:invalid {
|
|
| 3 |
+ color: #E64340; |
|
| 4 |
+} |
|
| 5 |
+input:required:valid {
|
|
| 6 |
+ color: rgb(0, 0, 0); |
|
| 7 |
+} |
|
| 8 |
+/* Input Placeholder */ |
|
| 9 |
+ input::-webkit-input-placeholder, textarea::-webkit-input-placeholder {
|
|
| 10 |
+ font-size: 13px; |
|
| 11 |
+} |
|
| 12 |
+input:-moz-placeholder, textarea:-moz-placeholder {
|
|
| 13 |
+ font-size: 13px; |
|
| 14 |
+} |
|
| 15 |
+input::-moz-placeholder, textarea::-moz-placeholder {
|
|
| 16 |
+ font-size: 13px; |
|
| 17 |
+} |
|
| 18 |
+input:-ms-input-placeholder, textarea:-ms-input-placeholder {
|
|
| 19 |
+ font-size: 13px; |
|
| 20 |
+} |
|
| 21 |
+/* Radio Cells */ |
|
| 22 |
+.radio_cells {
|
|
| 23 |
+ margin-top: 0; |
|
| 24 |
+ margin-left: 15px; |
|
| 25 |
+} |
|
| 26 |
+.radio_cells label {
|
|
| 27 |
+ padding: 8px 10px; |
|
| 28 |
+ font-size: 15px; |
|
| 29 |
+} |
|
| 30 |
+/*.radio_cells>div:first-child .quartern:after {*/
|
|
| 31 |
+ /*border-left: none;*/ |
|
| 32 |
+/*}*/ |
|
| 33 |
+.radio_cells>div:last-child .quartern:after {
|
|
| 34 |
+ border-right: none; |
|
| 35 |
+} |
|
| 36 |
+/* Quartern */ |
|
| 37 |
+.quartern {
|
|
| 38 |
+ width: 25%; |
|
| 39 |
+ box-sizing: border-box; |
|
| 40 |
+ text-align: center; |
|
| 41 |
+ border-radius: 5px; |
|
| 42 |
+ float: left; |
|
| 43 |
+} |
|
| 44 |
+.quartern:after {
|
|
| 45 |
+ content: " "; |
|
| 46 |
+ width: 200%; |
|
| 47 |
+ height: 200%; |
|
| 48 |
+ position: absolute; |
|
| 49 |
+ top: 0; |
|
| 50 |
+ left: 0; |
|
| 51 |
+ border-right: 1px solid rgba(0, 0, 0, 0.2); |
|
| 52 |
+ /*border-width: 0 1px 0 1px;*/ |
|
| 53 |
+ /*border-color: rgba(0, 0, 0, 0.2);*/ |
|
| 54 |
+ /*border-style: solid;*/ |
|
| 55 |
+ -webkit-transform: scale(0.5); |
|
| 56 |
+ transform: scale(0.5); |
|
| 57 |
+ -webkit-transform-origin: 0 0; |
|
| 58 |
+ transform-origin: 0 0; |
|
| 59 |
+ box-sizing: border-box; |
|
| 60 |
+ border-radius: 10px; |
|
| 61 |
+} |
|
| 62 |
+/* Radio Checked Relative */ |
|
| 63 |
+.weui_check:checked + .quartern {
|
|
| 64 |
+ color: white; |
|
| 65 |
+ background: #04BE02; |
|
| 66 |
+ border-width: 0; |
|
| 67 |
+} |
@@ -0,0 +1,135 @@ |
||
| 1 |
+{% load staticfiles %}
|
|
| 2 |
+ |
|
| 3 |
+<!DOCTYPE html> |
|
| 4 |
+<html lang="zh-CN"> |
|
| 5 |
+ <head> |
|
| 6 |
+ <meta charset="utf-8"> |
|
| 7 |
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> |
|
| 8 |
+ <meta name="format-detection" content="telephone=no,email=no,address=no"> |
|
| 9 |
+ <meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"> |
|
| 10 |
+ <title>课程兑换</title> |
|
| 11 |
+ |
|
| 12 |
+ <link href="//res.wx.qq.com/open/libs/weui/0.4.3/weui.min.css" rel="stylesheet" type="text/css" /> |
|
| 13 |
+{# <link href="{% static 'page/css/weui.ext.css' %}?v=1" rel="stylesheet" type="text/css" />#}
|
|
| 14 |
+ |
|
| 15 |
+ <style> |
|
| 16 |
+ .code-cover, .code-cover>img {
|
|
| 17 |
+ width: 100%; |
|
| 18 |
+ } |
|
| 19 |
+ .code-area {
|
|
| 20 |
+ padding: 5px 10px; |
|
| 21 |
+ } |
|
| 22 |
+ .code-label {
|
|
| 23 |
+ width: 70%; |
|
| 24 |
+ color: #c6c6c6; |
|
| 25 |
+ font-size: 13px; |
|
| 26 |
+ margin: 15px auto; |
|
| 27 |
+ } |
|
| 28 |
+ .code-input, .code-input>input, .code-submit {
|
|
| 29 |
+ width: 80%; |
|
| 30 |
+ height: 40px; |
|
| 31 |
+ line-height: 40px; |
|
| 32 |
+ margin: 5px auto; |
|
| 33 |
+ border-radius: 25px; |
|
| 34 |
+ box-sizing: border-box; |
|
| 35 |
+ } |
|
| 36 |
+ .code-input>input {
|
|
| 37 |
+ width: 100%; |
|
| 38 |
+ border: 1px solid #c6c6c6; |
|
| 39 |
+ padding: 0 15px; |
|
| 40 |
+ outline: medium; |
|
| 41 |
+ } |
|
| 42 |
+ .code-submit {
|
|
| 43 |
+ text-align: center; |
|
| 44 |
+ background: #20a1f5; |
|
| 45 |
+ color: #c6eaf9; |
|
| 46 |
+ margin-top: 15px; |
|
| 47 |
+ } |
|
| 48 |
+ </style> |
|
| 49 |
+ </head> |
|
| 50 |
+ <body> |
|
| 51 |
+ <div class="container"> |
|
| 52 |
+ <div class="code-cover"><img src="{% static 'page/img/code_cover.png' %}"></div>
|
|
| 53 |
+ <div class="code-area"> |
|
| 54 |
+ <div class="code-label">输入兑换码兑换课程</div> |
|
| 55 |
+ <div class="code-input"><input id="code" placeholder="请输入兑换码"></div> |
|
| 56 |
+ <div id="submit" class="code-submit">确认兑换</div> |
|
| 57 |
+ </div> |
|
| 58 |
+ |
|
| 59 |
+ <div class="weui_dialog_alert" id="dialog" style="display: none"> |
|
| 60 |
+ <div class="weui_mask"></div> |
|
| 61 |
+ <div class="weui_dialog"> |
|
| 62 |
+ <div class="weui_dialog_hd"><strong id="title" class="weui_dialog_title">弹窗标题</strong></div> |
|
| 63 |
+ <div id="content" class="weui_dialog_bd">弹窗内容,告知当前页面信息等</div> |
|
| 64 |
+ <div class="weui_dialog_ft"> |
|
| 65 |
+ <a href="javascript:;" class="weui_btn_dialog primary">确定</a> |
|
| 66 |
+ </div> |
|
| 67 |
+ </div> |
|
| 68 |
+ </div> |
|
| 69 |
+ </div> |
|
| 70 |
+ |
|
| 71 |
+ <script src="//cdn.bootcss.com/zepto/1.1.6/zepto.min.js"></script> |
|
| 72 |
+ <script> |
|
| 73 |
+ $(function() {
|
|
| 74 |
+ function show_error_dialog(title, content) {
|
|
| 75 |
+ $('#dialog #title').text(title);
|
|
| 76 |
+ $('#dialog #content').text(content);
|
|
| 77 |
+ $('#dialog').show();
|
|
| 78 |
+ } |
|
| 79 |
+ |
|
| 80 |
+ function data_check() {
|
|
| 81 |
+ var user_id = '{{ user_info.user_id }}';
|
|
| 82 |
+ if (!user_id) {
|
|
| 83 |
+ show_error_dialog('微信授权', '微信授权失败,请重新打开页面');
|
|
| 84 |
+ return false; |
|
| 85 |
+ } |
|
| 86 |
+ |
|
| 87 |
+ var code = $('#code').val();
|
|
| 88 |
+ if (!code) {
|
|
| 89 |
+ show_error_dialog('兑换码', '兑换码错误,请检查重新输入');
|
|
| 90 |
+ return false; |
|
| 91 |
+ } |
|
| 92 |
+ |
|
| 93 |
+ return {
|
|
| 94 |
+ user_id: user_id, |
|
| 95 |
+ code: code, |
|
| 96 |
+ } |
|
| 97 |
+ } |
|
| 98 |
+ |
|
| 99 |
+ $('#submit').click(function () {
|
|
| 100 |
+ var check_result = data_check(); |
|
| 101 |
+ if (check_result){
|
|
| 102 |
+ $.ajax({
|
|
| 103 |
+ type: 'POST', |
|
| 104 |
+ url: '{{ domain }}/api/code/exchange',
|
|
| 105 |
+ data: check_result, |
|
| 106 |
+ success: function(data) {
|
|
| 107 |
+ if (data.status == 200) {
|
|
| 108 |
+ window.location.href = '{{ domain }}/page/course/list?{{ params|safe }}';
|
|
| 109 |
+ } else {
|
|
| 110 |
+ show_error_dialog('错误', data.description);
|
|
| 111 |
+ } |
|
| 112 |
+ } |
|
| 113 |
+ }) |
|
| 114 |
+ } |
|
| 115 |
+ }); |
|
| 116 |
+ |
|
| 117 |
+ $('#dialog .weui_btn_dialog').click(function () {
|
|
| 118 |
+ $('#dialog').hide();
|
|
| 119 |
+ }) |
|
| 120 |
+ }); |
|
| 121 |
+ </script> |
|
| 122 |
+ <script type="text/javascript" src="//res.wx.qq.com/open/js/jweixin-1.2.0.js"></script> |
|
| 123 |
+ <script type="text/javascript" src="{% static 'course/js/jswe-0.0.1.js' %}"></script>
|
|
| 124 |
+ <script> |
|
| 125 |
+ V.initWxData({
|
|
| 126 |
+ imgUrl: 'http://pai.ai/static/pai2/img/paiai_96_96.png', |
|
| 127 |
+ link: 'http://api.pai.ai/wx_oauth2?redirect_url=http://tamron.xfoto.com.cn/page/clerk', |
|
| 128 |
+ desc: '店员授权', |
|
| 129 |
+ title: '店员授权', |
|
| 130 |
+ timeLine: '' |
|
| 131 |
+ }, true); |
|
| 132 |
+{# V.hideOptionMenu();#}
|
|
| 133 |
+ </script> |
|
| 134 |
+ </body> |
|
| 135 |
+</html> |
@@ -0,0 +1,124 @@ |
||
| 1 |
+{% load staticfiles %}
|
|
| 2 |
+ |
|
| 3 |
+<!DOCTYPE html> |
|
| 4 |
+<html lang="zh-CN"> |
|
| 5 |
+ <head> |
|
| 6 |
+ <meta charset="utf-8"> |
|
| 7 |
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> |
|
| 8 |
+ <meta name="format-detection" content="telephone=no,email=no,address=no"> |
|
| 9 |
+ <meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"> |
|
| 10 |
+ <title>课程详情</title> |
|
| 11 |
+ |
|
| 12 |
+ <link href="//res.wx.qq.com/open/libs/weui/0.4.3/weui.min.css" rel="stylesheet" type="text/css" /> |
|
| 13 |
+ <link href="{% static 'page/css/weui.ext.css' %}?v=1" rel="stylesheet" type="text/css" />
|
|
| 14 |
+ |
|
| 15 |
+ <style> |
|
| 16 |
+ .container {
|
|
| 17 |
+ position: absolute; |
|
| 18 |
+ top: 0; |
|
| 19 |
+ right: 0; |
|
| 20 |
+ bottom: 0; |
|
| 21 |
+ left: 0; |
|
| 22 |
+ background: #efefef; |
|
| 23 |
+ } |
|
| 24 |
+ .video_wrapper, .video_select {
|
|
| 25 |
+ width: 100%; |
|
| 26 |
+ background: #fff; |
|
| 27 |
+ margin-bottom: 10px; |
|
| 28 |
+ box-sizing: border-box; |
|
| 29 |
+ } |
|
| 30 |
+ .video_text, .video_select {
|
|
| 31 |
+ padding: 15px; |
|
| 32 |
+ } |
|
| 33 |
+ .course_video_name, .video_select_text {
|
|
| 34 |
+ font-size: 18px; |
|
| 35 |
+ font-weight: bold; |
|
| 36 |
+ color: #020001; |
|
| 37 |
+ padding-bottom: 10px; |
|
| 38 |
+ } |
|
| 39 |
+ .course_video_desc {
|
|
| 40 |
+ font-size: 12px; |
|
| 41 |
+ color: #999; |
|
| 42 |
+ } |
|
| 43 |
+ .video_select_item {
|
|
| 44 |
+ width: 100%; |
|
| 45 |
+ text-align: center; |
|
| 46 |
+ height: 40px; |
|
| 47 |
+ line-height: 40px; |
|
| 48 |
+ color: #020001; |
|
| 49 |
+ border: 1px solid #e1e1e1; |
|
| 50 |
+ border-radius: 5px; |
|
| 51 |
+ margin-bottom: 15px; |
|
| 52 |
+ } |
|
| 53 |
+ .video_selected {
|
|
| 54 |
+ color: #ce8f8a !important; |
|
| 55 |
+ border: 1px solid #ce8f8a; |
|
| 56 |
+ } |
|
| 57 |
+ </style> |
|
| 58 |
+ </head> |
|
| 59 |
+ <body> |
|
| 60 |
+ <div class="container" > |
|
| 61 |
+ <div class="video_wrapper"> |
|
| 62 |
+ <video id="video" width="100%" height="100%" autoplay controls x-webkit-airplay="true" webkit-playsinline="" playsinline="true" preload="none" poster="" src="{{ video_default.course_video_url }}" data-cursrc="1"></video>
|
|
| 63 |
+ <div class="video_text"> |
|
| 64 |
+ <div class="course_video_name">{{ video_default.course_video_name }}</div>
|
|
| 65 |
+ <div class="course_video_desc">{{ video_default.course_video_desc }}</div>
|
|
| 66 |
+ </div> |
|
| 67 |
+ </div> |
|
| 68 |
+ |
|
| 69 |
+ <div class="video_select"> |
|
| 70 |
+ <div class="video_select_text">选择视频</div> |
|
| 71 |
+ {% for video in videos %}
|
|
| 72 |
+ <div id="video{{ forloop.counter }}" class="video_select_item {% ifequal forloop.counter 1 %}video_selected{% endifequal %}" data-src="{{ video.course_video_url }}">{{ video.course_video_type }}</div>
|
|
| 73 |
+ {% endfor %}
|
|
| 74 |
+ </div> |
|
| 75 |
+ </div> |
|
| 76 |
+ |
|
| 77 |
+ <script src="//cdn.bootcss.com/zepto/1.1.6/zepto.min.js"></script> |
|
| 78 |
+ <script src="//cdn.bootcss.com/video.js/6.2.8/video.min.js"></script> |
|
| 79 |
+ <script> |
|
| 80 |
+ $(function() {
|
|
| 81 |
+ var video_count = {{ video_count }};
|
|
| 82 |
+ |
|
| 83 |
+ $('.video_select_item').click(function () {
|
|
| 84 |
+ $this = $(this); |
|
| 85 |
+ $('.video_select_item').removeClass('video_selected');
|
|
| 86 |
+ $this.addClass('video_selected');
|
|
| 87 |
+ $('#video').attr('src', $this.attr('data-src'));
|
|
| 88 |
+ }) |
|
| 89 |
+ |
|
| 90 |
+ $('#video')[0].onended = function() {
|
|
| 91 |
+ var curscr = $(this).attr('data-cursrc');
|
|
| 92 |
+ if (curscr >= video_count) {
|
|
| 93 |
+ return |
|
| 94 |
+ } |
|
| 95 |
+ var next_video = $('#video' + (parseInt(curscr) + 1));
|
|
| 96 |
+ $('#video').attr('src', next_video.attr('data-src'));
|
|
| 97 |
+ $('.video_select_item').removeClass('video_selected');
|
|
| 98 |
+ next_video.addClass('video_selected');
|
|
| 99 |
+ }; |
|
| 100 |
+ }); |
|
| 101 |
+ </script> |
|
| 102 |
+ <script type="text/javascript" src="//res.wx.qq.com/open/js/jweixin-1.2.0.js"></script> |
|
| 103 |
+ <script type="text/javascript" src="{% static 'course/js/jswe-0.0.1.js' %}"></script>
|
|
| 104 |
+ <script> |
|
| 105 |
+ V.initWxData({
|
|
| 106 |
+ imgUrl: 'http://pai.ai/static/pai2/img/paiai_96_96.png', |
|
| 107 |
+ link: 'http://api.pai.ai/wx_oauth2?redirect_url=http://tamron.xfoto.com.cn/page/clerk', |
|
| 108 |
+ desc: '店员授权', |
|
| 109 |
+ title: '店员授权', |
|
| 110 |
+ timeLine: '' |
|
| 111 |
+ }, true); |
|
| 112 |
+ V.hideOptionMenu(); |
|
| 113 |
+ |
|
| 114 |
+ $('#scan').click(function () {
|
|
| 115 |
+ V.scanQRCode({
|
|
| 116 |
+ needResult: 1 |
|
| 117 |
+ }); |
|
| 118 |
+ }); |
|
| 119 |
+ V.wxScanQRCodeSuccess = function (res) {
|
|
| 120 |
+ $('#code').val(V.parseScanQRCodeResultStr(res.resultStr));
|
|
| 121 |
+ } |
|
| 122 |
+ </script> |
|
| 123 |
+ </body> |
|
| 124 |
+</html> |
@@ -0,0 +1,77 @@ |
||
| 1 |
+{% load staticfiles %}
|
|
| 2 |
+ |
|
| 3 |
+<!DOCTYPE html> |
|
| 4 |
+<html lang="zh-CN"> |
|
| 5 |
+ <head> |
|
| 6 |
+ <meta charset="utf-8"> |
|
| 7 |
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> |
|
| 8 |
+ <meta name="format-detection" content="telephone=no,email=no,address=no"> |
|
| 9 |
+ <meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"> |
|
| 10 |
+ <title>课程列表</title> |
|
| 11 |
+ |
|
| 12 |
+ <link href="//res.wx.qq.com/open/libs/weui/0.4.3/weui.min.css" rel="stylesheet" type="text/css" /> |
|
| 13 |
+ <link href="{% static 'page/css/weui.ext.css' %}?v=1" rel="stylesheet" type="text/css" />
|
|
| 14 |
+ |
|
| 15 |
+ <style> |
|
| 16 |
+ .container {
|
|
| 17 |
+ position: absolute; |
|
| 18 |
+ top: 0; |
|
| 19 |
+ right: 0; |
|
| 20 |
+ bottom: 0; |
|
| 21 |
+ left: 0; |
|
| 22 |
+ background: #efefef; |
|
| 23 |
+ } |
|
| 24 |
+ .course_wrapper {
|
|
| 25 |
+ text-align: center; |
|
| 26 |
+ background: white; |
|
| 27 |
+ margin-bottom: 20px; |
|
| 28 |
+ padding: 15px 0; |
|
| 29 |
+ } |
|
| 30 |
+ .course_name {
|
|
| 31 |
+ font-size: 18px; |
|
| 32 |
+ font-weight: bold; |
|
| 33 |
+ color: #020001; |
|
| 34 |
+ } |
|
| 35 |
+ .course_time {
|
|
| 36 |
+ font-size: 12px; |
|
| 37 |
+ color: #999; |
|
| 38 |
+ } |
|
| 39 |
+ .course_cover>img {
|
|
| 40 |
+ width: 80%; |
|
| 41 |
+ border-radius: 5px; |
|
| 42 |
+ } |
|
| 43 |
+ </style> |
|
| 44 |
+ </head> |
|
| 45 |
+ <body> |
|
| 46 |
+ <div class="container" > |
|
| 47 |
+ {% for course in courses %}
|
|
| 48 |
+ <div class="course_wrapper" data-courseid="{{ course.course_id }}">
|
|
| 49 |
+ <div class="course_name">{{ course.course_name }}</div>
|
|
| 50 |
+ <div class="course_time">{{ course.course_time }}分钟</div>
|
|
| 51 |
+ <div class="course_cover"><img src="{{ course.course_cover_url }}"></div>
|
|
| 52 |
+ </div> |
|
| 53 |
+ {% endfor %}
|
|
| 54 |
+ </div> |
|
| 55 |
+ |
|
| 56 |
+ <script src="//cdn.bootcss.com/zepto/1.1.6/zepto.min.js"></script> |
|
| 57 |
+ <script> |
|
| 58 |
+ $(function() {
|
|
| 59 |
+ $('.course_wrapper').click(function () {
|
|
| 60 |
+ window.location.href = '{{ domain }}/page/course/info?course_id=' + $(this).attr('data-courseid') + '&{{ params|safe }}';
|
|
| 61 |
+ }) |
|
| 62 |
+ }); |
|
| 63 |
+ </script> |
|
| 64 |
+ <script type="text/javascript" src="//res.wx.qq.com/open/js/jweixin-1.2.0.js"></script> |
|
| 65 |
+ <script type="text/javascript" src="{% static 'course/js/jswe-0.0.1.js' %}"></script>
|
|
| 66 |
+ <script> |
|
| 67 |
+ V.initWxData({
|
|
| 68 |
+ imgUrl: 'http://pai.ai/static/pai2/img/paiai_96_96.png', |
|
| 69 |
+ link: 'http://api.pai.ai/wx_oauth2?redirect_url=http://tamron.xfoto.com.cn/page/clerk', |
|
| 70 |
+ desc: '店员授权', |
|
| 71 |
+ title: '店员授权', |
|
| 72 |
+ timeLine: '' |
|
| 73 |
+ }, true); |
|
| 74 |
+{# V.hideOptionMenu();#}
|
|
| 75 |
+ </script> |
|
| 76 |
+ </body> |
|
| 77 |
+</html> |
@@ -0,0 +1,4 @@ |
||
| 1 |
+from django.test import TestCase |
|
| 2 |
+ |
|
| 3 |
+ |
|
| 4 |
+# Create your tests here. |
@@ -0,0 +1,12 @@ |
||
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+ |
|
| 3 |
+from django.conf.urls import url |
|
| 4 |
+ |
|
| 5 |
+from page import code_views, info_views, list_views |
|
| 6 |
+ |
|
| 7 |
+ |
|
| 8 |
+urlpatterns = [ |
|
| 9 |
+ url(r'^course/code$', code_views.course_code, name='course_code'), |
|
| 10 |
+ url(r'^course/list$', list_views.course_list, name='course_list'), |
|
| 11 |
+ url(r'^course/info$', info_views.course_info, name='course_info'), |
|
| 12 |
+] |
@@ -0,0 +1,4 @@ |
||
| 1 |
+from django.shortcuts import render |
|
| 2 |
+ |
|
| 3 |
+ |
|
| 4 |
+# Create your views here. |
@@ -0,0 +1,9 @@ |
||
| 1 |
+#!/bin/bash |
|
| 2 |
+ |
|
| 3 |
+# Ignoring autogenerated files |
|
| 4 |
+# -- Migration directories |
|
| 5 |
+# Ignoring error codes |
|
| 6 |
+# -- E128 continuation line under-indented for visual indent |
|
| 7 |
+# -- E501 line too long |
|
| 8 |
+ |
|
| 9 |
+pep8 --exclude=migrations --ignore=E128,E501 . |
@@ -0,0 +1,40 @@ |
||
| 1 |
+-e git+https://github.com/Brightcells/django-q.git#egg=django-q |
|
| 2 |
+CodeConvert==2.0.4 |
|
| 3 |
+Django==1.11.3 |
|
| 4 |
+MySQL-python==1.2.5 |
|
| 5 |
+Pillow==3.4.2 |
|
| 6 |
+StatusCode==1.0.0 |
|
| 7 |
+TimeConvert==1.4.1 |
|
| 8 |
+cryptography==2.0.3 |
|
| 9 |
+django-curtail-uuid==1.0.0 |
|
| 10 |
+django-detect==1.0.5 |
|
| 11 |
+django-file-md5==1.0.1 |
|
| 12 |
+django-ip==1.0.1 |
|
| 13 |
+django-json-response==1.1.5 |
|
| 14 |
+django-logit==1.0.6 |
|
| 15 |
+django-multidomain==1.1.4 |
|
| 16 |
+django-paginator2==1.0.3 |
|
| 17 |
+django-rlog==1.0.7 |
|
| 18 |
+django-shortuuidfield==0.1.3 |
|
| 19 |
+django-six==1.0.2 |
|
| 20 |
+django-uniapi==1.0.0 |
|
| 21 |
+django-we==1.0.14 |
|
| 22 |
+djangorestframework==3.6.3 |
|
| 23 |
+furl==1.0.1 |
|
| 24 |
+hiredis==0.2.0 |
|
| 25 |
+isoweek==1.3.3 |
|
| 26 |
+jsonfield==2.0.2 |
|
| 27 |
+mock==2.0.0 |
|
| 28 |
+pep8==1.7.0 |
|
| 29 |
+pysnippets==1.0.4 |
|
| 30 |
+pywe-miniapp==1.0.0 |
|
| 31 |
+pywe-oauth==1.0.5 |
|
| 32 |
+pywe-response==1.0.1 |
|
| 33 |
+qiniu==7.1.5 |
|
| 34 |
+redis==2.10.6 |
|
| 35 |
+redis-extensions==1.1.1 |
|
| 36 |
+requests==2.18.4 |
|
| 37 |
+rlog==0.2 |
|
| 38 |
+shortuuid==0.5.0 |
|
| 39 |
+uWSGI==2.0.15 |
|
| 40 |
+versions==0.10.0 |
@@ -0,0 +1,51 @@ |
||
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+ |
|
| 3 |
+from StatusCode import BaseStatusCode, StatusCodeField |
|
| 4 |
+ |
|
| 5 |
+ |
|
| 6 |
+class ProfileStatusCode(BaseStatusCode): |
|
| 7 |
+ """ 用户相关错误码 4000xx """ |
|
| 8 |
+ PROFILE_NOT_FOUND = StatusCodeField(400001, 'Profile Not Found', description=u'用户不存在') |
|
| 9 |
+ |
|
| 10 |
+ |
|
| 11 |
+class CourseCodeStatusCode(BaseStatusCode): |
|
| 12 |
+ """ 课程兑换码相关错误码 4001xx """ |
|
| 13 |
+ COURSE_CODE_NOT_FOUND = StatusCodeField(400101, 'Course Code Not Found', description=u'课程兑换码不存在') |
|
| 14 |
+ COURSE_CODE_HAS_EXCHANGED = StatusCodeField(400102, 'Course Code Has Exchanged', description=u'课程兑换码已兑换') |
|
| 15 |
+ |
|
| 16 |
+ |
|
| 17 |
+class CourseStatusCode(BaseStatusCode): |
|
| 18 |
+ """ 课程相关错误码 4002xx """ |
|
| 19 |
+ COURSE_NOT_FOUND = StatusCodeField(400201, 'Course Not Found', description=u'课程不存在') |
|
| 20 |
+ |
|
| 21 |
+ |
|
| 22 |
+class OrderStatusCode(BaseStatusCode): |
|
| 23 |
+ """ 订单/支付相关错误码 4040xx """ |
|
| 24 |
+ WX_UNIFIED_ORDER_FAIL = StatusCodeField(404000, 'WX Unified Order Fail', description=u'微信统一下单失败') |
|
| 25 |
+ WX_ORDER_NOT_FOUND = StatusCodeField(404001, 'WX Order Not Found', description=u'订单不存在') |
|
| 26 |
+ WX_ORDER_NOT_PAY = StatusCodeField(404002, 'WX Order Not Pay', description=u'订单未支付') |
|
| 27 |
+ WX_ORDER_PAYING = StatusCodeField(404003, 'WX Order Paying', description=u'订单支付中') |
|
| 28 |
+ WX_ORDER_PAY_FAIL = StatusCodeField(404009, 'WX Order Pay Fail', description=u'微信支付失败') |
|
| 29 |
+ SIGN_CHECK_FAIL = StatusCodeField(404010, 'Sign Check Fail', description=u'签名校验失败') |
|
| 30 |
+ FEE_CHECK_FAIL = StatusCodeField(404011, 'FEE Check Fail', description=u'金额校验失败') |
|
| 31 |
+ NO_DETAIL_PERMISSION = StatusCodeField(404015, 'No Detail Permission', description=u'无详情权限') |
|
| 32 |
+ WX_ORDER_PAID_ALREADY_EXISTS = StatusCodeField(404020, 'WX Order Paid Already Exists', description=u'照片已购买') |
|
| 33 |
+ |
|
| 34 |
+ |
|
| 35 |
+class PayStatusCode(BaseStatusCode): |
|
| 36 |
+ """ 支付相关错误码 4041xx """ |
|
| 37 |
+ |
|
| 38 |
+ |
|
| 39 |
+class WithdrawStatusCode(BaseStatusCode): |
|
| 40 |
+ """ 提现相关错误码 4042xx """ |
|
| 41 |
+ BALANCE_NOT_ENOUGH = StatusCodeField(404200, 'Balance Not Enough', description=u'提现金额不足') |
|
| 42 |
+ |
|
| 43 |
+ |
|
| 44 |
+class MessageStatusCode(BaseStatusCode): |
|
| 45 |
+ """ 消息相关错误码 4090xx """ |
|
| 46 |
+ MESSAGE_NOT_FOUND = StatusCodeField(409001, 'Message Not Found', description=u'消息不存在') |
|
| 47 |
+ |
|
| 48 |
+ |
|
| 49 |
+class TokenStatusCode(BaseStatusCode): |
|
| 50 |
+ """ 票据相关错误码 4090xx """ |
|
| 51 |
+ TOKEN_NOT_FOUND = StatusCodeField(409901, 'Token Not Found', description=u'票据不存在') |
@@ -0,0 +1,18 @@ |
||
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+ |
|
| 3 |
+from django.http import JsonResponse |
|
| 4 |
+from StatusCode import StatusCodeField |
|
| 5 |
+ |
|
| 6 |
+ |
|
| 7 |
+def response_data(status_code=200, message=None, description=None, data={}, **kwargs):
|
|
| 8 |
+ return dict({
|
|
| 9 |
+ 'status': status_code, |
|
| 10 |
+ 'message': message, |
|
| 11 |
+ 'description': description, |
|
| 12 |
+ 'data': data, |
|
| 13 |
+ }, **kwargs) |
|
| 14 |
+ |
|
| 15 |
+ |
|
| 16 |
+def response(status_code=200, message=None, description=None, data={}, **kwargs):
|
|
| 17 |
+ message, description = (message or status_code.message, description or status_code.description) if isinstance(status_code, StatusCodeField) else (message, description) |
|
| 18 |
+ return JsonResponse(response_data(status_code, message, description, data, **kwargs), safe=False) |
@@ -0,0 +1,6 @@ |
||
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+ |
|
| 3 |
+from django.conf import settings |
|
| 4 |
+ |
|
| 5 |
+ |
|
| 6 |
+r = settings.REDIS_CACHE |
@@ -0,0 +1,68 @@ |
||
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+ |
|
| 3 |
+# 唯一标识相关 |
|
| 4 |
+UUID_LIST = 'uuid:list' # List,唯一标识列表 |
|
| 5 |
+ |
|
| 6 |
+# 用户相关 |
|
| 7 |
+PROFILE_INFO = 'profile:info:%s' # STRING,用户信息,user_id |
|
| 8 |
+ |
|
| 9 |
+# 导游相关 |
|
| 10 |
+TOUR_GUIDE_GROUP_GEO_INFO = 'tour:guide:group:geo:info:%s' # ZSET,旅游团地理位置信息,group_id |
|
| 11 |
+TOUR_GUIDE_GROUP_GEO_SUBMIT_DT = 'tour:guide:group:geo:submit:dt:%s' # ZSET,旅游团地理位置最后上传时间,group_id |
|
| 12 |
+TOUR_GUIDE_GROUP_CUR_SESSION = 'tour:guide:group:cur:session:%s' # STRING,旅游团当前Session,group_id,导游设置集合时间的时候更新 |
|
| 13 |
+TOUR_GUIDE_GROUP_CUR_GATHER_INFO = 'tour:guide:group:cur:gather:info:%s' # STRING,旅游团当前Session,group_id,导游设置集合时间的时候更新 |
|
| 14 |
+TOUR_GUIDE_GROUP_USER_GEO_LIST = 'tour:guide:group:user:geo:list:%s:%s:%s' # LIST,旅游团当前用户地理位置列表,group_id、session_id、user_id |
|
| 15 |
+ |
|
| 16 |
+TOUR_GUIDE_GROUP_USER_OWN = 'tour:guide:group:user:own:%s' # STRING,导游当前拥有的旅行团,user_id,导游创建旅行团的时候更新 |
|
| 17 |
+TOUR_GUIDE_GROUP_USER_BELONG = 'tour:guide:group:user:belong:%s' # STRING,用户当前所属旅行团,user_id,用户加入旅行团的时候更新 |
|
| 18 |
+ |
|
| 19 |
+# 群组相关 |
|
| 20 |
+GROUP_INFO = 'group:info:%s' # STRING,群组信息,group_id |
|
| 21 |
+ |
|
| 22 |
+# 群组用户相关 |
|
| 23 |
+GROUP_USERS_INFO = 'group:users:info:%s' # STRING,群组用户信息,group_id |
|
| 24 |
+GROUP_USERS_KV_INFO = 'group:users:kv:info:%s' # STRING,群组用户信息,group_id |
|
| 25 |
+GROUP_USERS_APPLYING_SET = 'group:users:applying:set:%s' # SET,群组用户申请集合,group_id |
|
| 26 |
+GROUP_USERS_PASSED_SET = 'group:users:passed:set:%s' # SET,群组用户通过集合,group_id |
|
| 27 |
+GROUP_USERS_REFUSED_SET = 'group:users:refused:set:%s' # SET,群组用户拒绝集合,group_id |
|
| 28 |
+GROUP_USERS_DELETED_SET = 'group:users:deleted:set:%s' # SET,群组用户移除集合,group_id |
|
| 29 |
+GROUP_USERS_QUIT_SET = 'group:users:quit:set:%s' # SET,群组用户退出集合,group_id |
|
| 30 |
+ |
|
| 31 |
+# 群组照片相关 |
|
| 32 |
+GROUP_PHOTO_DATA = 'group:photo:data:%s' # STRING,群组数据记录,group_id |
|
| 33 |
+GROUP_PHOTO_THUMB_UP = 'group:photo:thumb:up:%s:%s' # STRING,群组照片用户点赞记录,photo_id、user_id |
|
| 34 |
+GROUP_PHOTO_COMMENT_LIST = 'group:photo:comment:list:%s' # STRING,群组照片用户评论列表,photo_id |
|
| 35 |
+GROUP_PHOTO_THUMB_UP_LIST = 'group:photo:thumb:up:list:%s' # STRING,群组照片用户点赞列表,photo_id |
|
| 36 |
+GROUP_PHOTO_WATCHER_SET = 'group:photo:watcher:set:%s' # SET,群组照片用户关注集合,photo_id,关注即评论点赞 |
|
| 37 |
+GROUP_LAST_PHOTO_PK = 'group:last:photo:pk:%s' # STRING,群组最后一张照片PK,group_id |
|
| 38 |
+ |
|
| 39 |
+# 摄影师照片相关 |
|
| 40 |
+LENSMAN_PHOTO_ORDER_RECORD = 'lensman:photo:order:record:%s:%s' # STRING,摄影师照片购买记录,photo_id、user_id |
|
| 41 |
+ |
|
| 42 |
+# 摄影师简报相关 |
|
| 43 |
+# 收入 |
|
| 44 |
+TOTAL_INCOME = 'total:income:%s:%s' # STRING,总收入,user_id、photo_type |
|
| 45 |
+WEEK_INCOME = 'week:income:%s:%s:%s' # STRING,周收入,user_id、photo_type、Week.thisweek().isoformat() |
|
| 46 |
+TODAY_INCOME = 'today:income:%s:%s:%s' # STRING,日收入,user_id、photo_type、tc.local_string(format='%Y%m%d') |
|
| 47 |
+# 上传 |
|
| 48 |
+TODAY_UPLOAD_PHOTO_AMOUNT = 'today:upload:photo:amount:%s:%s' # STRING,日上传照片数量,user_id、tc.local_string(format='%Y%m%d') |
|
| 49 |
+# 售出 |
|
| 50 |
+WEEK_SOLD = 'week:sold:%s:%s:%s' # STRING,周售出,user_id、photo_type、Week.thisweek().isoformat() |
|
| 51 |
+ |
|
| 52 |
+# 摄影师定价相关 |
|
| 53 |
+LENSMAN_PHOTO_PRICE_FIXED = 'lensman:photo:price:fixed:%s' # STRING,摄影师照片定价(单位:分),user_id |
|
| 54 |
+ |
|
| 55 |
+# 系统消息相关 |
|
| 56 |
+SYSTEM_MESSAGE_READ_INFO = 'system:message:read:info:%s' # STRING,系统消息读取信息,user_id |
|
| 57 |
+SYSTEM_MESSAGE_DELETED_INFO = 'system:message:deleted:info:%s' # STRING,系统消息删除信息,user_id |
|
| 58 |
+ |
|
| 59 |
+# 游客入口相关 |
|
| 60 |
+GUEST_ENTRANCE_CONTROL_INFO = 'guest:entrance:control:info:%s' # STRING,游客入口控制信息,src |
|
| 61 |
+ |
|
| 62 |
+# APP 相关 |
|
| 63 |
+LATEST_APP_INFO = 'latest:app:info:%s' # STRING,最新 APP 信息,src |
|
| 64 |
+APP_SETTINGS_INFO = 'app:settings:info:%s:%s:%s' # STRING,APP 设置信息,platform、channel、version |
|
| 65 |
+APP_PATCH_INFO = 'app:patch:info:%s:%s:%s' # STRING,APP 补丁信息,platform、version、src |
|
| 66 |
+ |
|
| 67 |
+# BOX 相关 |
|
| 68 |
+BOX_PROGRAM_VERSION_INFO = 'box:program:version:info' # STRING,BOX 程序版本信息 |
@@ -0,0 +1,7 @@ |
||
| 1 |
+# -*- coding: utf-8 -*- |
|
| 2 |
+ |
|
| 3 |
+from django.conf import settings |
|
| 4 |
+ |
|
| 5 |
+ |
|
| 6 |
+def upload_file_url(file_path): |
|
| 7 |
+ return file_path and ('{}{}'.format(settings.DOMAIN, file_path.url)) or ''
|