├── .gitignore
├── LICENSE
├── README.md
├── db.sqlite3
├── index.html
├── log
└── presenters.log
├── media
├── favicon.ico
└── platform_logos
│ ├── 1.jpg
│ ├── 2.jpg
│ ├── 3.jpg
│ ├── 4.jpg
│ └── 5.jpg
├── src
├── accounts
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── backends.py
│ ├── forms.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ └── __init__.py
│ ├── models.py
│ ├── static
│ │ └── accounts
│ │ │ └── css
│ │ │ ├── styles.css
│ │ │ └── styles.less
│ ├── templates
│ │ └── accounts
│ │ │ ├── base_profile.html
│ │ │ ├── detail.html
│ │ │ ├── edit.html
│ │ │ ├── favourite.html
│ │ │ ├── password.html
│ │ │ ├── password_reset.html
│ │ │ ├── password_reset_confirm.html
│ │ │ ├── signin.html
│ │ │ └── signup.html
│ ├── tests.py
│ ├── urls.py
│ └── views.py
├── manage.py
├── presenters
│ ├── __init__.py
│ ├── admin.py
│ ├── api
│ │ ├── __init__.py
│ │ └── resources.py
│ ├── apps.py
│ ├── jieba_userdict.txt
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ ├── 0002_auto_20151117_1721.py
│ │ ├── 0003_auto_20151211_1838.py
│ │ ├── 0004_auto_20151212_2110.py
│ │ └── __init__.py
│ ├── models.py
│ ├── search_indexes.py
│ ├── static
│ │ └── presenters
│ │ │ ├── css
│ │ │ ├── styles.css
│ │ │ └── styles.less
│ │ │ └── img
│ │ │ ├── audience.png
│ │ │ └── status.png
│ ├── templates
│ │ ├── presenters
│ │ │ ├── about.html
│ │ │ ├── base_presenters.html
│ │ │ ├── detail.html
│ │ │ ├── feedback.html
│ │ │ ├── index.html
│ │ │ └── reactjs.html
│ │ └── search
│ │ │ └── indexes
│ │ │ └── presenters
│ │ │ └── presenter_text.txt
│ ├── templatetags
│ │ ├── __init__.py
│ │ ├── presenters_filters.py
│ │ └── settings_value.py
│ ├── tests.py
│ ├── urls.py
│ ├── views.py
│ └── whoosh_cn_backend.py
├── requirements.txt
└── thirtylol
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
└── templates
├── base.html
└── userena
└── base_userena.html
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 |
5 | # C extensions
6 | *.so
7 |
8 | # Distribution / packaging
9 | .Python
10 | env/
11 | # build/
12 | develop-eggs/
13 | # dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | # lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | *.egg-info/
23 | .installed.cfg
24 | *.egg
25 |
26 | # PyInstaller
27 | # Usually these files are written by a python script from a template
28 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
29 | *.manifest
30 | *.spec
31 |
32 | # Installer logs
33 | pip-log.txt
34 | pip-delete-this-directory.txt
35 |
36 | # Unit test / coverage reports
37 | htmlcov/
38 | .tox/
39 | .coverage
40 | .coverage.*
41 | .cache
42 | nosetests.xml
43 | coverage.xml
44 | *,cover
45 |
46 | # Translations
47 | *.mo
48 | *.pot
49 |
50 | # Django stuff:
51 | # *.log
52 |
53 | # Sphinx documentation
54 | docs/_build/
55 |
56 | # PyBuilder
57 | target/
58 |
59 | # VisualStudio_PTVS
60 | *.user
61 | *.suo
62 | bin/
63 | obj/
64 | *.*~
65 | .module-cache/
66 | log/
67 | whoosh_index/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 infinity1207
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 30lol
2 |
3 | [三十撸啊撸](http://www.30lol.com) 网站源码
4 |
5 | ## 安装
6 | * 请确保当前环境已经安装python、pip
7 |
8 | * 获得源码
9 |
10 | `git clone https://github.com/infinity1207/thirtylol.git`
11 |
12 | * 进入src目录,假设代码放到 d:\thirtylol,打开windows命令行工具
13 |
14 | `d:`
15 |
16 | `cd thirtylol\src`
17 |
18 | * 安装依赖库
19 |
20 | `pip install -r requirements.txt`
21 |
22 | * 启动
23 |
24 | `python manage.py runserver`
25 |
26 | * 打开浏览器,地址栏输入 127.0.0.1:8000 进行访问
27 |
28 | * 源码中附带了一个包含了演示数据的数据库,默认添加了一个管理员账户,登录信息为
29 |
30 | `用户名: admin`
31 |
32 | `密码: 1234`
33 |
34 | ## 进阶设置
35 |
36 | * 使用用户管理模块
37 |
38 | `python manage.py check_permissions`
39 |
40 | * 全文检索
41 |
42 | `python manage.py rebuild_index`
43 |
44 | * 新浪微博登录
45 |
46 | ```python
47 | #setting.py
48 | WEIBO_OAUTH_VERIFY = ...
49 | WEIBO_OAUTH_APP_KEY = ...
50 | WEIBO_OAUTH_APP_SECRET = ...
51 | ```
52 |
53 | * 更新Presenter信息
54 | 打开浏览器,访问http://127.0.0.1:8000/presenters/fetch, 请先在admin中配置每个Platform的Login param,如何获取Login param请查看presenters\views.py的相关注释及代码
55 |
--------------------------------------------------------------------------------
/db.sqlite3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinity1207/thirtylol/7b3e3472ba589c7af1f53475ff160dabb8d04129/db.sqlite3
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 30lol_游戏直播从这里开始
5 |
6 |
7 | 对不起,站长正在更新网站,请稍后访问......
8 |
9 |
--------------------------------------------------------------------------------
/log/presenters.log:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinity1207/thirtylol/7b3e3472ba589c7af1f53475ff160dabb8d04129/log/presenters.log
--------------------------------------------------------------------------------
/media/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinity1207/thirtylol/7b3e3472ba589c7af1f53475ff160dabb8d04129/media/favicon.ico
--------------------------------------------------------------------------------
/media/platform_logos/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinity1207/thirtylol/7b3e3472ba589c7af1f53475ff160dabb8d04129/media/platform_logos/1.jpg
--------------------------------------------------------------------------------
/media/platform_logos/2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinity1207/thirtylol/7b3e3472ba589c7af1f53475ff160dabb8d04129/media/platform_logos/2.jpg
--------------------------------------------------------------------------------
/media/platform_logos/3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinity1207/thirtylol/7b3e3472ba589c7af1f53475ff160dabb8d04129/media/platform_logos/3.jpg
--------------------------------------------------------------------------------
/media/platform_logos/4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinity1207/thirtylol/7b3e3472ba589c7af1f53475ff160dabb8d04129/media/platform_logos/4.jpg
--------------------------------------------------------------------------------
/media/platform_logos/5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinity1207/thirtylol/7b3e3472ba589c7af1f53475ff160dabb8d04129/media/platform_logos/5.jpg
--------------------------------------------------------------------------------
/src/accounts/__init__.py:
--------------------------------------------------------------------------------
1 | default_app_config = 'accounts.apps.AccountsConfig'
--------------------------------------------------------------------------------
/src/accounts/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | # Register your models here.
4 |
--------------------------------------------------------------------------------
/src/accounts/apps.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from django.apps import AppConfig
4 | from django.db.models.signals import post_migrate
5 |
6 | def init_db(sender, **kwargs):
7 | _datas = [
8 | (0, u'管理员添加'),
9 | (10, u'网站注册'),
10 | (100, u'微博登录'),
11 | ]
12 | for data in _datas:
13 | my_module = sender.get_model('UserSource')
14 | try:
15 | us = my_module.objects.get(flag=data[0])
16 | except my_module.DoesNotExist:
17 | us = my_module(flag=data[0], name=data[1])
18 | us.save()
19 |
20 | class AccountsConfig(AppConfig):
21 | name = 'accounts'
22 |
23 | def ready(self):
24 | post_migrate.connect(init_db, sender=self)
25 |
--------------------------------------------------------------------------------
/src/accounts/backends.py:
--------------------------------------------------------------------------------
1 | from .models import OAuth
2 | from django.utils import timezone
3 | from datetime import timedelta
4 | from django.contrib.auth.models import User
5 |
6 | class OAuthAuthenticationBackend(object):
7 | def authenticate(self, token=None, uid=None, expire_in=None):
8 | """
9 | check if uid aleady exists, create user if not exist
10 | """
11 | if token and uid:
12 | expired = self._get_expired_time(expire_in)
13 | try:
14 | user = User.objects.get(oauth__uid=uid)
15 | user.oauth.token = token
16 | user.oauth.expired = expired
17 | user.oauth.save()
18 | except User.DoesNotExist:
19 | user = User()
20 | user.username = "weibo_%s" % uid
21 | user.save()
22 |
23 | user.oauth = OAuth(user=user, token=token, uid=uid, expired=expired)
24 | user.oauth.save()
25 |
26 | return user
27 |
28 | return None
29 |
30 | def _get_expired_time(self, expire_in):
31 | return timezone.now() + timedelta(seconds=expire_in)
32 |
33 | def get_user(self, user_id):
34 | try:
35 | user = User.objects.get(pk=user_id)
36 | # check if OAuth has expired
37 | if user.oauth.expired >= timezone.now():
38 | return user
39 | except User.DoesNotExist:
40 | return None
41 |
--------------------------------------------------------------------------------
/src/accounts/forms.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from django import forms
4 | from .models import UserProfile
5 |
6 | class EditProfileForm(forms.ModelForm):
7 | class Meta:
8 | model = UserProfile
9 | fields = ['mugshot']
--------------------------------------------------------------------------------
/src/accounts/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models, migrations
5 | import easy_thumbnails.fields
6 | from django.conf import settings
7 | import userena.models
8 |
9 |
10 | class Migration(migrations.Migration):
11 |
12 | dependencies = [
13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14 | ('presenters', '0002_auto_20151117_1721'),
15 | ]
16 |
17 | operations = [
18 | migrations.CreateModel(
19 | name='OAuth',
20 | fields=[
21 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
22 | ('token', models.CharField(max_length=128)),
23 | ('uid', models.CharField(max_length=128)),
24 | ('expired', models.DateTimeField()),
25 | ('user', models.OneToOneField(to=settings.AUTH_USER_MODEL)),
26 | ],
27 | ),
28 | migrations.CreateModel(
29 | name='UserProfile',
30 | fields=[
31 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
32 | ('mugshot', easy_thumbnails.fields.ThumbnailerImageField(help_text='A personal image displayed in your profile.', upload_to=userena.models.upload_to_mugshot, verbose_name='mugshot', blank=True)),
33 | ('privacy', models.CharField(default=b'registered', help_text='Designates who can view your profile.', max_length=15, verbose_name='privacy', choices=[(b'open', 'Open'), (b'registered', 'Registered'), (b'closed', 'Closed')])),
34 | ('favourite_snack', models.CharField(max_length=5, verbose_name='favourite snack')),
35 | ('follows', models.ManyToManyField(to='presenters.Presenter')),
36 | ],
37 | options={
38 | 'abstract': False,
39 | 'permissions': (('view_profile', 'Can view profile'),),
40 | },
41 | ),
42 | migrations.CreateModel(
43 | name='UserSource',
44 | fields=[
45 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
46 | ('flag', models.IntegerField()),
47 | ('name', models.CharField(max_length=50)),
48 | ],
49 | ),
50 | migrations.AddField(
51 | model_name='userprofile',
52 | name='source',
53 | field=models.ForeignKey(to='accounts.UserSource', null=True),
54 | ),
55 | migrations.AddField(
56 | model_name='userprofile',
57 | name='user',
58 | field=models.OneToOneField(related_name='my_profile', verbose_name='user', to=settings.AUTH_USER_MODEL),
59 | ),
60 | ]
61 |
--------------------------------------------------------------------------------
/src/accounts/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinity1207/thirtylol/7b3e3472ba589c7af1f53475ff160dabb8d04129/src/accounts/migrations/__init__.py
--------------------------------------------------------------------------------
/src/accounts/models.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from django.db import models
4 | from django.utils.translation import ugettext_lazy as _
5 | # from django.utils.crypto import salted_hmac
6 | from django.contrib.auth.models import User
7 | from django.dispatch import receiver
8 | from userena.models import UserenaBaseProfile
9 | # from userena.models import UserenaSignup
10 | from userena.signals import signup_complete
11 | from userena.utils import get_user_profile
12 |
13 | class OAuth(models.Model):
14 | user = models.OneToOneField(User)
15 |
16 | # account = models.CharField(max_length=50)
17 | token = models.CharField(max_length=128)
18 | uid = models.CharField(max_length=128)
19 | expired = models.DateTimeField()
20 |
21 |
22 | class UserSource(models.Model):
23 | flag = models.IntegerField() # 0: 管理员添加, 10: 网站注册,大于等于100: OAuth
24 | name = models.CharField(max_length=50)
25 |
26 | def __unicode__(self):
27 | return self.name
28 |
29 |
30 | class UserProfile(UserenaBaseProfile):
31 | user = models.OneToOneField(User,
32 | unique=True,
33 | verbose_name=_('user'),
34 | related_name='my_profile')
35 |
36 | source = models.ForeignKey('UserSource', null=True)
37 |
38 | favourite_snack = models.CharField(_('favourite snack'),
39 | max_length=5)
40 |
41 | follows = models.ManyToManyField('presenters.Presenter')
42 |
43 | def __init__(self, *args, **kwargs):
44 | super(UserProfile, self).__init__(*args, **kwargs)
45 | if self.pk is None:
46 | self.source = UserSource.objects.get(flag=0)
47 |
48 |
49 | @receiver(signup_complete, sender=None)
50 | def update_user_source(sender, user, *args, **kwargs):
51 | profile = get_user_profile(user)
52 | profile.source = UserSource.objects.get(flag=10)
53 | profile.save()
54 |
55 |
--------------------------------------------------------------------------------
/src/accounts/static/accounts/css/styles.css:
--------------------------------------------------------------------------------
1 | div.form-account{margin:0 auto;width:400px}div.form-account table td:nth-child(1){width:100px}div.form-account label{margin-top:5px}table.non-border td{border-top:none!important}div.signin a.signup{margin-left:10px}div.password table td:nth-child(1),div.personal table td:nth-child(1),div.personal-edit table td:nth-child(1){width:150px}ul.errorlist{color:red}div.follow-list{margin-top:10px}div.follow-list div.presenter-cell{float:left;width:300px;height:100px}div.follow-list div.presenter-cell div.avatar-middle{float:left}div.follow-list div.presenter-cell div.presenter-info{margin-left:15px;float:left}tr.password a.password-reset{margin-left:220px}
--------------------------------------------------------------------------------
/src/accounts/static/accounts/css/styles.less:
--------------------------------------------------------------------------------
1 | div.form-account {
2 | margin: 0 auto;
3 | width: 400px;
4 | table td:nth-child(1) {
5 | width: 100px;
6 | }
7 | label {
8 | margin-top: 5px;
9 | }
10 | }
11 |
12 | table.non-border {
13 | td {
14 | border-top: none !important;
15 | }
16 | }
17 |
18 | div.signin {
19 | a.signup {
20 | margin-left: 10px;
21 | }
22 | }
23 |
24 | div.personal, div.password, div.personal-edit {
25 | table td:nth-child(1) {
26 | width: 150px;
27 | }
28 | }
29 |
30 | ul.errorlist {
31 | color: rgb(255, 0, 0);
32 | }
33 |
34 | div.follow-list {
35 | margin-top: 10px;
36 | div.presenter-cell {
37 | float: left;
38 | width: 300px;
39 | height: 100px;
40 | div.avatar-middle {
41 | float: left;
42 | }
43 | div.presenter-info {
44 | margin-left: 15px;
45 | float: left;
46 | }
47 | }
48 | }
49 |
50 | tr.password {
51 | a.password-reset {
52 | margin-left: 220px;
53 | }
54 | }
55 |
56 |
--------------------------------------------------------------------------------
/src/accounts/templates/accounts/base_profile.html:
--------------------------------------------------------------------------------
1 | {% extends 'userena/base_userena.html' %}
2 | {% load i18n %}
3 | {% load url from future %}
4 | {% load staticfiles %}
5 | {% load settings_value %}
6 |
7 | {% block title %}{% blocktrans with profile.user.username as username %}{{ username }}'s profile.{% endblocktrans %}{% endblock %}
8 | {% block content_title %}{{ profile.user.username }} {% if profile.user.get_full_name %}({{ profile.user.get_full_name }}){% endif %}
{% endblock %}
9 |
10 | {% block static_ref %}
11 | {{ block.super }}
12 |
13 |
14 | {% endblock static_ref %}
15 |
16 | {% block content %}
17 |
18 |
19 |
20 | {% block tab %}
21 |
31 | {% endblock tab %}
32 |
33 |
34 |
35 |
36 | {% block tab_page %}
37 | {% endblock tab_page %}
38 |
39 |
40 |
41 |
42 | {% endblock %}
--------------------------------------------------------------------------------
/src/accounts/templates/accounts/detail.html:
--------------------------------------------------------------------------------
1 | {% extends 'accounts/base_profile.html' %}
2 |
3 | {% block js_extra %}
4 | $(document).ready(function(){
5 | $("ul#profile-nav > li:eq(0)").attr("class", "active");
6 | });
7 | {% endblock js_extra %}
8 |
9 | {% block tab_page %}
10 |
11 |
12 |
13 |
14 | |
15 | {{ profile.source.name }} |
16 |
17 |
18 | |
19 | {{ profile.user.username }} |
20 |
21 |
22 | {% if profile.source.flag >= 100 %}
23 | |
24 | {{ profile.user.oauth.uid }} |
25 | {% else %}
26 | |
27 | {{ profile.user.email }} |
28 | {% endif %}
29 |
30 |
31 |
32 | {% endblock tab_page %}
--------------------------------------------------------------------------------
/src/accounts/templates/accounts/edit.html:
--------------------------------------------------------------------------------
1 | {% extends 'accounts/base_profile.html' %}
2 |
3 | {% load thumbnail %}
4 |
5 | {% block js_extra %}
6 | $(document).ready(function(){
7 | $("ul#profile-nav > li:eq(1)").attr("class", "active");
8 | });
9 | {% endblock js_extra %}
10 |
11 | {% block tab_page %}
12 |
38 | {% endblock tab_page %}
--------------------------------------------------------------------------------
/src/accounts/templates/accounts/favourite.html:
--------------------------------------------------------------------------------
1 | {% extends 'accounts/base_profile.html' %}
2 | {% load presenters_filters %}
3 |
4 | {% block js_extra %}
5 | $(document).ready(function(){
6 | $("ul#profile-nav > li#profile-3").attr("class", "active");
7 | });
8 | {% endblock js_extra %}
9 |
10 | {% block tab_page %}
11 |
12 | {% if not presenter_list %}
13 |
你没有关注任何主播,快去首页添加你喜欢的主播吧!
14 | {% else %}
15 | {% for presenter in presenter_list %}
16 |
17 |
18 |

19 |
20 |
21 |
{{ presenter.nickname }}
22 |
23 |
24 | {% if presenter.presenterdetail.showing %}
25 |
正在直播
26 | {% else %}
27 |
未开播
28 | {% endif %}
29 |
30 |
31 | {# display room's title if living else display last live end time.#}
32 | {% if presenter.presenterdetail.showing %}
33 |
34 | {{ presenter.presenterdetail.audience_count|abbreviation_number }}
35 |
36 |
37 |
38 |
{{ presenter.presenterdetail.room_title }}
39 |
40 | {% else %}
41 |
42 |
43 | {% if presenter.presenterdetail.last_show_end %}
44 |
结束于 {{ presenter.presenterdetail.last_show_end|timesince }} 前
45 | {% endif %}
46 |
47 | {% endif %}
48 |
49 |
50 |
51 |
52 | {% endfor %}
53 | {% endif %}
54 |
55 | {% endblock tab_page %}
--------------------------------------------------------------------------------
/src/accounts/templates/accounts/password.html:
--------------------------------------------------------------------------------
1 | {% extends 'accounts/base_profile.html' %}
2 |
3 | {% block js_extra %}
4 | $(document).ready(function(){
5 | $("ul#profile-nav > li#profile-2").attr("class", "active");
6 | });
7 | {% endblock js_extra %}
8 |
9 | {% block tab_page %}
10 |
35 | {% endblock %}
--------------------------------------------------------------------------------
/src/accounts/templates/accounts/password_reset.html:
--------------------------------------------------------------------------------
1 | {% extends 'accounts/base_profile.html' %}
2 | {% load i18n %}
3 |
4 | {% block title %}{% trans "Reset password" %}{% endblock %}
5 |
6 | {% block content %}
7 |
28 | {% endblock %}
--------------------------------------------------------------------------------
/src/accounts/templates/accounts/password_reset_confirm.html:
--------------------------------------------------------------------------------
1 | {% extends 'accounts/base_profile.html' %}
2 | {% load i18n %}
3 |
4 | {% block title %}{% trans "Reset password" %}{% endblock %}
5 |
6 | {% block content %}
7 |
32 | {% endblock %}
33 |
--------------------------------------------------------------------------------
/src/accounts/templates/accounts/signin.html:
--------------------------------------------------------------------------------
1 | {% extends 'accounts/base_profile.html' %}
2 | {% load i18n %}
3 | {% load url from future %}
4 | {% load staticfiles %}
5 | {% load settings_value %}
6 |
7 | {% block title %}{% trans "Signin" %}{% endblock %}
8 |
9 | {% block content %}
10 | {% if request.user.is_authenticated %}
11 | {{ request.user.username }}
12 | 退出
13 | {% else %}
14 |
57 | {% endif %}
58 | {% endblock %}
--------------------------------------------------------------------------------
/src/accounts/templates/accounts/signup.html:
--------------------------------------------------------------------------------
1 | {# 登录 #}
2 |
3 | {% extends 'accounts/base_profile.html' %}
4 |
5 | {% load i18n %}
6 | {% block title %}{% trans "Signup" %}{% endblock %}
7 |
8 | {% block content %}
9 |
56 | {% endblock %}
--------------------------------------------------------------------------------
/src/accounts/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/src/accounts/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls import url
2 | from django.contrib.auth import views as auth_views
3 | from userena import views as userena_views
4 | from userena import settings as userena_settings
5 | from userena.compat import auth_views_compat_quirks, password_reset_uid_kwarg
6 | from thirtylol import settings
7 | from . import views
8 |
9 | def merged_dict(dict_a, dict_b):
10 | """Merges two dicts and returns output. It's purpose is to ease use of
11 | ``auth_views_compat_quirks``
12 | """
13 | dict_a.update(dict_b)
14 | return dict_a
15 |
16 | urlpatterns = [
17 | url('^follow/$', views.follow, name='userena_follow'),
18 | url('^unfollow/$', views.unfollow, name='userena_unfollow'),
19 | url('^oauth/$', views.oauth, name='userena_oauth'),
20 |
21 | # override userena's urls
22 |
23 | # Signup, signin and signout
24 | url(r'^signup/$',
25 | userena_views.signup,
26 | {'template_name': 'accounts/signup.html',},
27 | name='userena_signup'),
28 | url(r'^signin/$',
29 | views.signin,
30 | name='userena_signin'),
31 | url(r'^signout/$',
32 | userena_views.signout,
33 | {'next_page': '/',},
34 | name='userena_signout'),
35 |
36 | # Reset password
37 | url(r'^password/reset/$',
38 | auth_views.password_reset,
39 | merged_dict({'template_name': 'accounts/password_reset.html',
40 | 'email_template_name': 'userena/emails/password_reset_message.txt',
41 | 'extra_context': {'without_usernames': userena_settings.USERENA_WITHOUT_USERNAMES}
42 | }, auth_views_compat_quirks['userena_password_reset']),
43 | name='userena_password_reset'),
44 | url(r'^password/reset/done/$',
45 | auth_views.password_reset_done,
46 | {'template_name': 'userena/password_reset_done.html',},
47 | name='userena_password_reset_done'),
48 |
49 | url(r'^password/reset/confirm/(?P<%s>[0-9A-Za-z]+)-(?P.+)/$' % password_reset_uid_kwarg,
50 | auth_views.password_reset_confirm,
51 | merged_dict(
52 | {'template_name': 'accounts/password_reset_confirm.html',},
53 | auth_views_compat_quirks['userena_password_reset_confirm']
54 | ),
55 | name='userena_password_reset_confirm'),
56 | url(r'^password/reset/confirm/complete/$',
57 | auth_views.password_reset_complete,
58 | {'template_name': 'userena/password_reset_complete.html'},
59 | name='userena_password_reset_complete'),
60 |
61 | # Signup
62 | url(r'^(?P[\@\.\w-]+)/signup/complete/$',
63 | userena_views.direct_to_user_template,
64 | {'template_name': 'userena/signup_complete.html',
65 | 'extra_context': {'userena_activation_required': userena_settings.USERENA_ACTIVATION_REQUIRED,
66 | 'userena_activation_days': userena_settings.USERENA_ACTIVATION_DAYS}},
67 | name='userena_signup_complete'),
68 |
69 | # Activate
70 | url(r'^activate/(?P\w+)/$',
71 | userena_views.activate,
72 | name='userena_activate'),
73 |
74 | # Retry activation
75 | url(r'^activate/retry/(?P\w+)/$',
76 | userena_views.activate_retry,
77 | name='userena_activate_retry'),
78 |
79 | # Change email and confirm it
80 | # url(r'^(?P[\@\.\w-]+)/email/$',
81 | # userena_views.email_change,
82 | # name='userena_email_change'),
83 | # url(r'^(?P[\@\.\w-]+)/email/complete/$',
84 | # userena_views.direct_to_user_template,
85 | # {'template_name': 'userena/email_change_complete.html'},
86 | # name='userena_email_change_complete'),
87 | # url(r'^(?P[\@\.\w-]+)/confirm-email/complete/$',
88 | # userena_views.direct_to_user_template,
89 | # {'template_name': 'userena/email_confirm_complete.html'},
90 | # name='userena_email_confirm_complete'),
91 | # url(r'^confirm-email/(?P\w+)/$',
92 | # userena_views.email_confirm,
93 | # name='userena_email_confirm'),
94 |
95 | # Disabled account
96 | url(r'^(?P[\@\.\w-]+)/disabled/$',
97 | userena_views.disabled_account,
98 | {'template_name': 'userena/disabled.html'},
99 | name='userena_disabled'),
100 |
101 | # Change password
102 | url(r'^(?P[\@\.\w-]+)/password/$',
103 | userena_views.password_change,
104 | {'template_name': 'accounts/password.html'},
105 | name='userena_password_change'),
106 | url(r'^(?P[\@\.\w-]+)/password/complete/$',
107 | userena_views.direct_to_user_template,
108 | {'template_name': 'userena/password_complete.html'},
109 | name='userena_password_change_complete'),
110 |
111 | # Edit profile
112 | url(r'^(?P[\@\.\w-]+)/edit/$',
113 | # userena_views.profile_edit,
114 | views.profile_edit,
115 | name='userena_profile_edit'),
116 |
117 | # View profiles
118 | url(r'^(?P(?!signout|signup|signin)[\@\.\w-]+)/$',
119 | # userena_views.profile_detail,
120 | views.profile_detail,
121 | name='userena_profile_detail'),
122 |
123 | url('^(?P[\@\.\w-]+)/favourite/$',
124 | views.favourite,
125 | name='userena_favourite'),
126 |
127 | # url(r'^page/(?P[0-9]+)/$',
128 | # userena_views.ProfileListView.as_view(),
129 | # name='userena_profile_list_paginated'),
130 |
131 | # url(r'^$',
132 | # userena_views.ProfileListView.as_view(),
133 | # name='userena_profile_list'),
134 | ]
135 |
--------------------------------------------------------------------------------
/src/accounts/views.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from django.http import HttpResponse, HttpResponseRedirect
4 | # from django.core.urlresolvers import reverse
5 | # from django.http import Http404
6 | from django.shortcuts import render
7 | from django.contrib.auth import authenticate, login as auth_login
8 | from django.contrib.auth.models import User
9 | from django.shortcuts import get_object_or_404
10 | from django.http import Http404
11 | from django.core.urlresolvers import reverse
12 | from userena.utils import signin_redirect, get_user_profile
13 | from userena import views as userena_views
14 | import json
15 | from .models import UserSource
16 | from .forms import EditProfileForm
17 | from presenters.models import Presenter
18 | from thirtylol import settings
19 | import urllib, urllib2
20 |
21 | def oauth(request):
22 | try:
23 | code = request.GET.get('code', '')
24 | token_str = _access_token(request, code)
25 | token_json = json.loads(token_str)
26 | user = authenticate(token=token_json['access_token'], uid=token_json['uid'], expire_in=(60 * 60 * 24))
27 | if user:
28 | try:
29 | user_str = _show_user(token_json['access_token'], token_json['uid'])
30 | user_json = json.loads(user_str)
31 | user.username = user_json['screen_name']
32 | user.save()
33 | except:
34 | pass
35 |
36 | auth_login(request, user)
37 | profile = get_user_profile(user)
38 | profile.source = UserSource.objects.get(flag=100)
39 | profile.save()
40 | return HttpResponseRedirect(signin_redirect(user=user))
41 | except:
42 | return HttpResponse("很抱歉,使用新浪微博认证登录失败,请尝试从网站注册!")
43 |
44 | def request(url, method='GET', headers={}, params=None):
45 | data = None
46 | if params:
47 | data = urllib.urlencode(params)
48 |
49 | if method.upper() == 'GET':
50 | url = url + '?' + data
51 | data = None
52 |
53 | req = urllib2.Request(url, data=data, headers=headers)
54 | resp = urllib2.urlopen(req)
55 | return resp.info(), resp.read()
56 |
57 | def _show_user(access_token, uid):
58 | show_user_url = 'https://api.weibo.com/2/users/show.json'
59 | params = {
60 | 'access_token': access_token,
61 | 'uid': uid,
62 | }
63 | return request(show_user_url, params=params)[1]
64 |
65 |
66 | def _access_token(request, code):
67 | access_token_url = 'https://api.weibo.com/oauth2/access_token'
68 | params = {
69 | 'client_id': settings.WEIBO_OAUTH_APP_KEY,
70 | 'client_secret': settings.WEIBO_OAUTH_APP_SECRET,
71 | 'grant_type': 'authorization_code',
72 | 'code': code,
73 | 'redirect_uri': "http://%s/oauth/" % (request.get_host()),
74 | }
75 | req = urllib2.Request(access_token_url, data=urllib.urlencode(params))
76 | return urllib2.urlopen(req).read()
77 |
78 | def signin(request):
79 | if request.user.is_authenticated():
80 | return HttpResponseRedirect(reverse('userena_profile_detail', kwargs={'username':request.user.username}))
81 | return userena_views.signin(request, template_name='accounts/signin.html')
82 |
83 | def profile_edit(request, username):
84 | user = get_object_or_404(User, username=username)
85 | profile = get_user_profile(user)
86 |
87 | if request.method == 'POST':
88 | print request.POST
89 | form = EditProfileForm(request.POST, request.FILES)
90 | if form.is_valid():
91 | mugshot = form.cleaned_data['mugshot']
92 | if mugshot:
93 | profile.mugshot = form.cleaned_data['mugshot']
94 | profile.save()
95 | else:
96 | form = EditProfileForm()
97 |
98 | context = {
99 | 'profile': profile,
100 | 'form': form,
101 | }
102 | return render(request, 'accounts/edit.html', context)
103 |
104 | def profile_detail(request, username):
105 | user = get_object_or_404(User, username=username)
106 | profile = get_user_profile(user)
107 | context = {
108 | 'profile': profile,
109 | }
110 | return render(request, 'accounts/detail.html', context)
111 |
112 | def favourite(request, username):
113 | user = get_object_or_404(User, username=username)
114 | profile = get_user_profile(user)
115 |
116 | presenter_list = user.my_profile.follows.order_by('-presenterdetail__showing', '-presenterdetail__audience_count');
117 | context = {
118 | 'profile': profile,
119 | 'presenter_list': presenter_list,
120 | }
121 | return render(request, 'accounts/favourite.html', context)
122 |
123 | def follow(request):
124 | if not request.is_ajax():
125 | raise Http404()
126 |
127 | user_id = request.POST.get('user_id', None)
128 | presenter_id = request.POST.get('presenter_id', None)
129 |
130 | user = User.objects.get(id=user_id)
131 | presenter = Presenter.objects.get(id=presenter_id)
132 |
133 | if presenter not in user.my_profile.follows.all():
134 | user.my_profile.follows.add(presenter)
135 | user.my_profile.save()
136 |
137 | return HttpResponse(json.dumps({'num_follows':presenter.userprofile_set.count()}), content_type='application/json')
138 |
139 | def unfollow(request):
140 | if not request.is_ajax():
141 | raise Http404()
142 |
143 | user_id = request.POST.get('user_id', None)
144 | presenter_id = request.POST.get('presenter_id', None)
145 |
146 | user = User.objects.get(id=user_id)
147 | presenter = Presenter.objects.get(id=presenter_id)
148 |
149 | if presenter in user.my_profile.follows.all():
150 | user.my_profile.follows.remove(presenter)
151 | user.my_profile.save()
152 |
153 | return HttpResponse(json.dumps({'num_follows':presenter.userprofile_set.count()}), content_type='application/json')
--------------------------------------------------------------------------------
/src/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """
3 | Command-line utility for administrative tasks.
4 | """
5 |
6 | import os
7 | import sys
8 |
9 | if __name__ == "__main__":
10 | os.environ.setdefault(
11 | "DJANGO_SETTINGS_MODULE",
12 | "thirtylol.settings"
13 | )
14 |
15 | from django.core.management import execute_from_command_line
16 |
17 | execute_from_command_line(sys.argv)
18 |
--------------------------------------------------------------------------------
/src/presenters/__init__.py:
--------------------------------------------------------------------------------
1 | default_app_config = 'presenters.apps.PresentersConfig'
--------------------------------------------------------------------------------
/src/presenters/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from .models import Presenter, PresenterDetail, Platform, Tag
3 |
4 | admin.site.register(Presenter)
5 | admin.site.register(PresenterDetail)
6 | admin.site.register(Platform)
7 | admin.site.register(Tag)
8 |
--------------------------------------------------------------------------------
/src/presenters/api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinity1207/thirtylol/7b3e3472ba589c7af1f53475ff160dabb8d04129/src/presenters/api/__init__.py
--------------------------------------------------------------------------------
/src/presenters/api/resources.py:
--------------------------------------------------------------------------------
1 | from tastypie import fields
2 | from tastypie.resources import ModelResource
3 | from presenters.models import Presenter, Platform, Tag, PresenterDetail
4 | from tastypie.constants import ALL
5 |
6 |
7 | class PlatformResource(ModelResource):
8 | class Meta:
9 | queryset = Platform.objects.all()
10 | allowed_methods = ['get']
11 |
12 |
13 | class TagResource(ModelResource):
14 | class Meta:
15 | queryset = Tag.objects.all()
16 | allowed_methods = ['get']
17 |
18 |
19 | class PresenterDetailResource(ModelResource):
20 | class Meta:
21 | queryset = PresenterDetail.objects.all()
22 | allowed_methods = ['get']
23 | # resource_name = 'presenter_detail'
24 |
25 |
26 | class PresenterResource(ModelResource):
27 | platform = fields.ForeignKey(PlatformResource, 'platform', full=True)
28 | tags = fields.ToManyField(TagResource, 'tag', full=True)
29 | detail = fields.OneToOneField(
30 | PresenterDetailResource,
31 | 'presenterdetail',
32 | full=True)
33 |
34 | def apply_sorting(self, obj_list, options=None):
35 | if options and "sort" in options:
36 | if options['sort'] == 'showing':
37 | return obj_list.order_by('-presenterdetail__showing')
38 | else:
39 | return obj_list.order_by(options['sort'])
40 | return super(PresenterResource, self).apply_sorting(obj_list, options)
41 |
42 | # showing = fields.BooleanField(readonly=True)
43 | # def dehydrate_showing(self, bundle):
44 | # print type(bundle.obj), type(bundle.data)
45 | # print dir(type(bundle.obj))
46 | # return bundle.obj.presenterdetail.showing
47 |
48 | # def build_filters(self, filters=None):
49 | # print filters
50 | # if filters is None:
51 | # filters = {}
52 |
53 | # orm_filters = super(PresenterResource, self).build_filters(filters)
54 | # print type(orm_filters), orm_filters
55 | # if 'q' in filters:
56 | # pass
57 | # return orm_filters
58 | class Meta:
59 | queryset = Presenter.objects.all()
60 | allowed_methods = ['get']
61 |
62 | filtering = {
63 | "nickname": ALL,
64 | }
65 |
--------------------------------------------------------------------------------
/src/presenters/apps.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from django.apps import AppConfig
3 | from django.db.models.signals import post_migrate
4 | from django.db.models.fields.files import ImageFieldFile
5 | from django.shortcuts import get_object_or_404
6 | from django.http import Http404
7 |
8 | def init_platform(sender, **kwargs):
9 | platforms = (
10 | {'name':u'斗鱼', 'url':'http://www.douyu.com/', 'intro':u'当下最火的游戏直播平台', 'logo':'platform_logos/1.jpg'},
11 | {'name':u'虎牙', 'url':'http://www.huya.com/', 'intro':u'YY旗下直播网站', 'logo':'platform_logos/2.jpg'},
12 | {'name':u'战旗', 'url':'http://www.zhanqi.com/', 'intro':u'浙报传媒与边锋网络共同打造', 'logo':'platform_logos/3.jpg'},
13 | {'name':u'龙珠', 'url':'http://www.longzhu.com/', 'intro':u'腾讯旗下', 'logo':'platform_logos/4.jpg'},
14 | {'name':u'熊猫', 'url':'http://www.panda.tv/', 'intro':u'王思聪投资', 'logo':'platform_logos/5.jpg'},
15 | )
16 | for item in platforms:
17 | my_module = sender.get_model('Platform')
18 | try:
19 | p = get_object_or_404(my_module, name=item['name'])
20 | except Http404:
21 | p = my_module()
22 | p.name = item['name']
23 | p.url = item['url']
24 | p.introduce = item['intro']
25 | p.logo = ImageFieldFile(p, p.logo, item['logo'])
26 | p.save()
27 |
28 | def init_game(sender, **kwargs):
29 | games = (u'英雄联盟', u'格斗游戏', u'DOTA', u'炉石传说')
30 | for item in games:
31 | my_module = sender.get_model('Game')
32 | try:
33 | game = get_object_or_404(my_module, name=item)
34 | except Http404:
35 | game = my_module(name=item)
36 | game.save()
37 |
38 | def init_db(sender, **kwargs):
39 | init_platform(sender, **kwargs)
40 | init_game(sender, **kwargs)
41 |
42 |
43 | class PresentersConfig(AppConfig):
44 | name = 'presenters'
45 |
46 | def ready(self):
47 | post_migrate.connect(init_db, sender=self)
--------------------------------------------------------------------------------
/src/presenters/jieba_userdict.txt:
--------------------------------------------------------------------------------
1 | 贾克斯
2 | 发条
3 | 蛮王
4 | 赏金
5 | 上单
6 | 中单
7 | 打野
8 | 小飒
9 | 董小飒
--------------------------------------------------------------------------------
/src/presenters/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models, migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ]
11 |
12 | operations = [
13 | migrations.CreateModel(
14 | name='Platform',
15 | fields=[
16 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
17 | ('name', models.CharField(max_length=50)),
18 | ('url', models.CharField(max_length=50)),
19 | ('intro', models.TextField()),
20 | ('logo', models.ImageField(upload_to=b'platform_logos', blank=True)),
21 | ('login_param', models.CharField(max_length=255)),
22 | ('fetch_status', models.BooleanField(default=False)),
23 | ],
24 | ),
25 | migrations.CreateModel(
26 | name='Presenter',
27 | fields=[
28 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
29 | ('id_in_platform', models.IntegerField(default=0)),
30 | ('nickname', models.CharField(max_length=50)),
31 | ('introduce', models.TextField()),
32 | ('room_url', models.CharField(max_length=255, blank=True)),
33 | ('gender', models.CharField(default=b'M', max_length=1, choices=[(b'F', b'Female'), (b'M', b'Male')])),
34 | ('avatar_url', models.CharField(max_length=255, blank=True)),
35 | ('join_date', models.DateField(auto_now_add=True)),
36 | ('invalid', models.BooleanField(default=True)),
37 | ('platform', models.ForeignKey(to='presenters.Platform')),
38 | ],
39 | ),
40 | migrations.CreateModel(
41 | name='PresenterDetail',
42 | fields=[
43 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
44 | ('showing', models.BooleanField(default=False)),
45 | ('room_title', models.CharField(max_length=255, blank=True)),
46 | ('duration', models.IntegerField(default=0)),
47 | ('audience_count', models.IntegerField(default=0)),
48 | ('last_show_end', models.DateTimeField(null=True, blank=True)),
49 | ('presenter', models.OneToOneField(to='presenters.Presenter')),
50 | ],
51 | ),
52 | migrations.CreateModel(
53 | name='Tag',
54 | fields=[
55 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
56 | ('name', models.CharField(max_length=50)),
57 | ],
58 | ),
59 | migrations.AddField(
60 | model_name='presenter',
61 | name='tag',
62 | field=models.ManyToManyField(to='presenters.Tag', blank=True),
63 | ),
64 | ]
65 |
--------------------------------------------------------------------------------
/src/presenters/migrations/0002_auto_20151117_1721.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models, migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('presenters', '0001_initial'),
11 | ]
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name='ShowHistory',
16 | fields=[
17 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
18 | ('start', models.DateTimeField()),
19 | ('stop', models.DateTimeField()),
20 | ('duration', models.IntegerField()),
21 | ('presenter', models.ForeignKey(to='presenters.Presenter')),
22 | ],
23 | ),
24 | migrations.RemoveField(
25 | model_name='presenterdetail',
26 | name='duration',
27 | ),
28 | migrations.AddField(
29 | model_name='presenterdetail',
30 | name='start',
31 | field=models.DateTimeField(null=True, blank=True),
32 | ),
33 | ]
34 |
--------------------------------------------------------------------------------
/src/presenters/migrations/0003_auto_20151211_1838.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models, migrations
5 | import presenters.models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('presenters', '0002_auto_20151117_1721'),
12 | ]
13 |
14 | operations = [
15 | migrations.CreateModel(
16 | name='Game',
17 | fields=[
18 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
19 | ('name', models.CharField(max_length=50)),
20 | ],
21 | ),
22 | migrations.AddField(
23 | model_name='presenter',
24 | name='game',
25 | field=models.ForeignKey(default=presenters.models.get_default_game, to='presenters.Game'),
26 | ),
27 | ]
28 |
--------------------------------------------------------------------------------
/src/presenters/migrations/0004_auto_20151212_2110.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models, migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('presenters', '0003_auto_20151211_1838'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='platform',
16 | name='login_param',
17 | field=models.TextField(),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/src/presenters/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinity1207/thirtylol/7b3e3472ba589c7af1f53475ff160dabb8d04129/src/presenters/migrations/__init__.py
--------------------------------------------------------------------------------
/src/presenters/models.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from django.db import models
4 | from django.db.models.signals import post_save
5 | from django.dispatch import receiver
6 | import shutil
7 | import platform as pf
8 | from django.utils import timezone
9 |
10 | GENDER_CHOICES = (
11 | ('F', 'Female'),
12 | ('M', 'Male'),
13 | )
14 |
15 |
16 | class Platform(models.Model):
17 | name = models.CharField(max_length=50)
18 | url = models.CharField(max_length=50)
19 | intro = models.TextField()
20 | logo = models.ImageField(upload_to='platform_logos', blank=True)
21 |
22 | login_param = models.TextField()
23 | fetch_status = models.BooleanField(default=False)
24 |
25 | def __unicode__(self):
26 | return self.name
27 |
28 | # @receiver(post_save, sender=Platform)
29 | def handle_platform_logo_filename(sender, instance, created, **kwargs):
30 | name = instance.logo.name
31 | if name:
32 | last_slash_pos = name.rindex('/')
33 | last_dot_pos = name.rindex('.')
34 | name_without_extension = name[last_slash_pos + 1:last_dot_pos]
35 | prefix = name[:last_slash_pos]
36 | extension = name[last_dot_pos + 1:]
37 | if name_without_extension == str(instance.id):
38 | return
39 |
40 | new_name = "%s/%s.%s" % (prefix, instance.id, extension)
41 | image_file_path = instance.logo.path
42 | if pf.system() == 'Windows':
43 | image_file_path_prefix = image_file_path[:image_file_path.rindex('\\')]
44 | new_image_file_path = "%s\\%s.%s" % (image_file_path_prefix, instance.id, extension)
45 | else:
46 | image_file_path_prefix = image_file_path[:image_file_path.rindex('/')]
47 | new_image_file_path = "%s/%s.%s" % (image_file_path_prefix, instance.id, extension)
48 |
49 | shutil.move(image_file_path, new_image_file_path)
50 | instance.logo.name = new_name
51 | instance.save()
52 |
53 | class Tag(models.Model):
54 | name = models.CharField(max_length=50)
55 |
56 | def __unicode__(self):
57 | return self.name
58 |
59 |
60 | class Game(models.Model):
61 | name = models.CharField(max_length=50)
62 |
63 | def __unicode__(self):
64 | return self.name
65 |
66 | def get_default_game():
67 | return Game.objects.get_or_create(name=u"英雄联盟")[0].id
68 |
69 | class Presenter(models.Model):
70 | game = models.ForeignKey('Game', default=get_default_game)
71 | platform = models.ForeignKey('Platform')
72 | id_in_platform = models.IntegerField(default=0)
73 | tag = models.ManyToManyField('Tag', blank=True)
74 | nickname = models.CharField(max_length=50)
75 | introduce = models.TextField()
76 | room_url = models.CharField(max_length=255, blank=True)
77 | gender = models.CharField(max_length=1, choices=GENDER_CHOICES, default='M')
78 | avatar_url = models.CharField(max_length=255, blank=True)
79 |
80 | join_date = models.DateField(auto_now_add=True)
81 |
82 | invalid = models.BooleanField(default=True)
83 |
84 | def __unicode__(self):
85 | return self.nickname
86 |
87 |
88 | class RecommendPresenter(object):
89 | platform = models.ForeignKey('Platform')
90 | nickname = models.CharField(max_length=50)
91 | introduce = models.TextField()
92 | approve = models.NullBooleanField()
93 |
94 | email = models.EmailField()
95 |
96 | def __unicode__(self):
97 | return self.nickname
98 |
99 |
100 | class PresenterDetail(models.Model):
101 | presenter = models.OneToOneField('Presenter')
102 |
103 | showing = models.BooleanField(default=False)
104 | room_title = models.CharField(max_length=255, blank=True)
105 | audience_count = models.IntegerField(default=0)
106 |
107 | start = models.DateTimeField(null=True, blank=True)
108 | last_show_end = models.DateTimeField(null=True, blank=True)
109 |
110 | def save(self, *args, **kwargs):
111 | if self.showing and not self.start: # 开始直播
112 | self.start = timezone.now()
113 | self.last_show_end = None
114 | elif not self.showing and self.start: # 结束直播
115 | self.auto_create_showhistory()
116 | self.last_show_end = timezone.now()
117 | self.start = None
118 |
119 | super(PresenterDetail, self).save()
120 |
121 | # 结束直播时记录本次直播信息
122 | def auto_create_showhistory(self):
123 | history = ShowHistory(presenter=self.presenter, start=self.start, stop=timezone.now())
124 | history.duration = (history.stop - history.start).total_seconds() / 60
125 | history.save()
126 |
127 | def __unicode__(self):
128 | return self.presenter.nickname
129 |
130 | class ShowHistory(models.Model):
131 | presenter = models.ForeignKey('Presenter')
132 |
133 | start = models.DateTimeField()
134 | stop = models.DateTimeField()
135 | duration = models.IntegerField()
136 |
137 |
138 | @receiver(post_save, sender=Presenter)
139 | def auto_create_presenter_detail(sender, instance, created, *args, **kwargs):
140 | if not created:
141 | return
142 | detail = PresenterDetail(presenter=instance)
143 | detail.save()
144 |
145 | # post_save.connect(auto_create_presenter_detail, sender=Presenter)
146 |
--------------------------------------------------------------------------------
/src/presenters/search_indexes.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from .models import Presenter
3 | from haystack import indexes
4 |
5 | class PresenterIndex(indexes.SearchIndex, indexes.Indexable):
6 | text = indexes.CharField(document=True, use_template=True)
7 |
8 | nickname = indexes.CharField(model_attr='nickname')
9 | introduce = indexes.CharField(model_attr='introduce')
10 |
11 | def get_model(self):
12 | return Presenter
13 |
14 | def index_queryset(self, using=None):
15 | return self.get_model().objects.all()
--------------------------------------------------------------------------------
/src/presenters/static/presenters/css/styles.css:
--------------------------------------------------------------------------------
1 | div.avatar-middle{width:64px;height:64px}div.avatar-middle img{width:inherit;height:inherit}div.game-panel{margin-bottom:20px}table.presenter-list td.avatar{width:74px}table.presenter-list td.presenter-info{width:220px}table.presenter-list td.platform{width:120px}table.presenter-list td.follow{width:100px}div.living div.living-status{float:left}div.living div.living-status a{text-decoration:none}div.living div.living-status a:hover{text-decoration:none}div.living div.living-audience{margin-left:10px;float:left;margin-top:-2px;background:url(../img/audience.png);background-repeat:no-repeat;background-position:0 0}div.living div.living-audience span{margin-left:28px}div.living div.last-show-end{margin-top:5px;font-size:80%}div.living div.living-room-title{font-size:80%}div.intro-bottom{float:right;margin-right:20px}div.platform-logo img{width:84px;height:52px}div.content{margin-top:70px}div.comment{margin-top:50px}td.follow div.follow{margin-top:20px}td.follow button{width:80px;height:30px}
--------------------------------------------------------------------------------
/src/presenters/static/presenters/css/styles.less:
--------------------------------------------------------------------------------
1 | @avatarSize: 64px;
2 | @avatarRightGap: 20px;
3 | @personColumnWidth: 320px;
4 | @livingColumnWidth: 120px;
5 | @presenterListHeadBackgroundColor: #F5F6F7;
6 | @presenterListHeadColor: #8A98A5;
7 | @personNameColor: #292f33;
8 | @platformLogoWidth: 86px;
9 | @platformLogoHeight: 32px;
10 |
11 | .backgroundImage (@url, @xPos, @yPos) {
12 | background:@url;
13 | background-repeat:no-repeat;
14 | background-position:@xPos @yPos;
15 | }
16 |
17 | // .navbar-default {
18 | // background-color: rgba(255, 255, 255, 0.95);
19 | // -webkit-box-shadow: 0 5px 4px -4px rgba(190,190,190,.1);
20 | // -moz-box-shadow: 0 5px 4px -4px rgba(190,190,190,.1);
21 | // box-shadow: 0 5px 4px -4px rgba(190,190,190,.1);
22 | // border-bottom: 1px solid #f5f5f5;
23 | // }
24 |
25 | // .navbar-default .navbar-nav > li > a:hover,
26 | // .navbar-default .navbar-nav > li > a:focus,
27 | // .navbar-default .navbar-nav > li.active > a,
28 | // .navbar-default .navbar-nav > li.active > a:hover,
29 | // .navbar-default .navbar-nav > li.active > a:focus {
30 | // color: white;
31 | // background-color: #2CBA68;
32 | // }
33 |
34 | // .navbar-default .navbar-nav > .dropdown:hover .dropdown-menu {
35 | // display: block;
36 | // }
37 |
38 | div.avatar-middle {
39 | width: @avatarSize;
40 | height: @avatarSize;
41 | img{
42 | width:inherit;
43 | height:inherit;
44 | }
45 | }
46 |
47 | div.game-panel {
48 | margin-bottom: 20px;
49 | }
50 |
51 | table.presenter-list {
52 | td.avatar {
53 | width: @avatarSize + 10px;
54 | }
55 | td.presenter-info {
56 | width: 220px;
57 | }
58 | td.platform {
59 | width: 120px;
60 | }
61 | td.follow {
62 | width: 100px;
63 | }
64 | }
65 |
66 | div.living {
67 | div.living-status {
68 | float: left;
69 | a {
70 | text-decoration: none;
71 | }
72 | a:hover {
73 | text-decoration: none;
74 | }
75 | }
76 |
77 | div.living-audience {
78 | margin-left: 10px;
79 | float: left;
80 | margin-top: -2px;
81 |
82 | .backgroundImage(url(../img/audience.png), 0, 0);
83 |
84 | span {
85 | margin-left: 28px;
86 | }
87 | }
88 |
89 | div.last-show-end {
90 | margin-top: 5px;
91 | font-size: 80%;
92 | }
93 | div.living-room-title {
94 | font-size: 80%;
95 | }
96 | }
97 |
98 | div.intro-bottom {
99 | float: right;
100 | margin-right: 20px;
101 | }
102 |
103 | div.platform-logo {
104 | img{
105 | width: 84px;
106 | height: 52px;
107 | }
108 | }
109 |
110 | div.content {
111 | margin-top: 70px;
112 | }
113 |
114 | div.comment {
115 | margin-top: 50px;
116 | }
117 |
118 | // follow button
119 | td.follow {
120 | div.follow {
121 | margin-top: 20px;
122 | }
123 | button {
124 | width: 80px;
125 | height: 30px;
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/src/presenters/static/presenters/img/audience.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinity1207/thirtylol/7b3e3472ba589c7af1f53475ff160dabb8d04129/src/presenters/static/presenters/img/audience.png
--------------------------------------------------------------------------------
/src/presenters/static/presenters/img/status.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinity1207/thirtylol/7b3e3472ba589c7af1f53475ff160dabb8d04129/src/presenters/static/presenters/img/status.png
--------------------------------------------------------------------------------
/src/presenters/templates/presenters/about.html:
--------------------------------------------------------------------------------
1 | {% extends "presenters/base_presenters.html" %}
2 |
3 | {% block js_extra %}
4 | $(document).ready(function(){
5 | $("ul#site-nav > li:eq(3)").attr("class", "active");
6 | });
7 | {% endblock js_extra %}
8 |
9 | {% load staticfiles %}
10 | {% block content %}
11 |
12 |
13 |
14 |
15 |
16 | 站长是一名80后的老码农,自幼喜欢电子游戏,中小学期间的课余时光大多数都在街机厅度过,大学期间也曾沉迷于传奇夜夜通宵不睡,参加工作之后玩的最投入的游戏是PSP上的怪物猎人,记得为了过村长天地每天都刷到凌晨4~5点,持续了一周的时间才把天地双杀。
17 |
18 |
19 | 随着结婚生女,忙碌的工作和生活渐渐冲淡了对游戏的热情,期间虽然也挤出时间通关了屈指可数的几款主机游戏,但是对网络游戏是一点点都不感冒了,直到今年夏天已过而立之年的我在基友的怂恿下接触了英雄联盟,从此一发不可收,至今已经打了1000多场,无奈技艺不精仍处于青铜水平,尽管如此每天最期待的时候就是下班回到家,吃完饭洗完碗,对着电脑撸上几把。
20 |
21 |
22 | 英雄联盟这个游戏就像人生一样,大家都从弄不清楚春哥红叉是什么的小白开始,幸幸苦苦的匹配到30级后在同一个起点为了名誉打拼,极少数有天赋的人脱颖而出成为最强王者来到金字塔的顶端,少数人通过努力在钻石白金站稳脚跟,大多数的人则默默无闻的在白银青铜徘徊。好在随着直播平台的兴起,当你在上分遇到瓶颈时可以近距离的感受到大神的操作、意识和大局观,相信很多人通过观看一些优秀的主播实力有所提升,当你觉得枯燥时有各式各样的美女让你大饱眼福,不过带来的副作用是晚上常常看直播看到很晚。
23 |
24 |
25 | 最近由于主播们频繁的转会,导致我想找喜欢的主播越来越麻烦,遂萌生了做一个网站将优秀的主播全部收录起来的想法,于是有了本站的诞生,希望本站能给大家带来方便!
26 |
27 |
28 | 未完,待续……………………
29 |
30 |
31 |
如果你有任何问题或建议,请在此留言
32 |
33 |
34 |
35 | {% endblock content %}
--------------------------------------------------------------------------------
/src/presenters/templates/presenters/base_presenters.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% load staticfiles %}
4 | {% load settings_value %}
5 |
6 | {% block static_ref %}
7 | {{ block.super }}
8 |
9 | {% endblock static_ref %}
10 |
11 | {% block body %}
12 |
13 | {% block content %}{% endblock content %}
14 |
15 | {% endblock body %}
16 |
--------------------------------------------------------------------------------
/src/presenters/templates/presenters/detail.html:
--------------------------------------------------------------------------------
1 | {% extends "presenters/base_presenters.html" %}
2 | {% load staticfiles %}
3 |
4 | {% block content %}
5 |
6 |
7 |
8 |
9 |
10 |

11 |
12 |
13 |
14 |
15 |
{{ presenter.nickname }}
16 | {% if presenter.introduce %}
17 |
{{ presenter.introduce }}
18 | {% else %}
19 |
暂无介绍
20 | {% endif %}
21 |
22 |
23 |
24 | {% if presenter.presenterdetail.showing %}
25 |
正在直播
26 | {% else %}
27 |
未开播
28 | {% endif %}
29 |
30 |
31 | {% if presenter.presenterdetail.showing %}
32 |
33 | {{ presenter.presenterdetail.audience_count }}
34 |
35 | {% endif %}
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
56 |
57 |
58 | {% endblock content %}
--------------------------------------------------------------------------------
/src/presenters/templates/presenters/feedback.html:
--------------------------------------------------------------------------------
1 | {% extends "presenters/base_presenters.html" %}
2 |
3 | {% block js_extra %}
4 | $(document).ready(function(){
5 | $("ul#site-nav li:eq(1)").attr("class", "active");
6 | });
7 | {% endblock js_extra %}
8 |
9 | {% load staticfiles %}
10 | {% block content %}
11 |
12 |
13 |
14 |
15 |
如果你有任何问题或建议,请在此留言
16 |
17 |
18 |
19 |
20 |
26 |
27 |
28 |
29 | {% endblock content %}
--------------------------------------------------------------------------------
/src/presenters/templates/presenters/index.html:
--------------------------------------------------------------------------------
1 | {% extends "presenters/base_presenters.html" %}
2 |
3 | {% load staticfiles %}
4 | {% load i18n %}
5 | {% load settings_value %}
6 | {% load presenters_filters %}
7 |
8 | {% block meta %}
9 |
10 | {% endblock meta %}
11 |
12 | {% block js_extra %}
13 | function getCookie(name) {
14 | var cookieValue = null;
15 | if (document.cookie && document.cookie != '') {
16 | var cookies = document.cookie.split(';');
17 | for (var i = 0; i < cookies.length; i++) {
18 | var cookie = jQuery.trim(cookies[i]);
19 | // Does this cookie string begin with the name we want?
20 | if (cookie.substring(0, name.length + 1) == (name + '=')) {
21 | cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
22 | break;
23 | }
24 | }
25 | }
26 | return cookieValue;
27 | }
28 | var csrftoken = getCookie('csrftoken');
29 |
30 | function csrfSafeMethod(method) {
31 | // these HTTP methods do not require CSRF protection
32 | return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
33 | }
34 |
35 | $.ajaxSetup({
36 | beforeSend: function(xhr, settings) {
37 | if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
38 | xhr.setRequestHeader("X-CSRFToken", csrftoken);
39 | }
40 | }
41 | });
42 |
43 | function replaceAttr(obj, old_val, new_val, attr_name) {
44 | if (attr_name == undefined) {
45 | attr_name = "class";
46 | }
47 | var l = obj.attr(attr_name).split(" ");
48 | var i = l.indexOf(old_val);
49 | if (i >= 0) {
50 | l[i] = new_val
51 | obj.attr(attr_name, l.join(' '));
52 | }
53 | }
54 |
55 | function onMouseEnter() {
56 | replaceAttr($(this), "btn-info", "btn-danger");
57 | $(this).text("取消关注");
58 | }
59 |
60 | function onMouseLeave() {
61 | replaceAttr($(this), "btn-danger", "btn-info");
62 | $(this).text("正在关注");
63 | }
64 |
65 | function follow() {
66 | {% if request.user.is_authenticated %}
67 | var url = "{% url 'userena_follow' %}";
68 |
69 | params = {
70 | user_id: {{ request.user.id }},
71 | presenter_id: $(this).parents("tr").attr('presenter-id'),
72 | };
73 | $.post(url, params, function(data){
74 | replaceAttr($(this), "unfollow", "following");
75 | replaceAttr($(this), "btn-default", "btn-info");
76 | $(this).text("正在关注");
77 |
78 | $(this).unbind()
79 | $(this).bind("click", unfollow);
80 | $(this).bind("mouseenter", onMouseEnter);
81 | $(this).bind("mouseleave", onMouseLeave);
82 | }.bind(this));
83 | {% else %}
84 | // redirect signin page
85 | $(window.location).attr("href", "{% url 'userena_signin' %}?next={{ request.get_full_path }}");
86 | {% endif %}
87 | }
88 |
89 | function unfollow() {
90 | var url = "{% url 'userena_unfollow' %}";
91 | params = {
92 | user_id: {{ request.user.id }},
93 | presenter_id: $(this).parents("tr").attr('presenter-id'),
94 | };
95 | $.post(url, params, function(data){
96 | replaceAttr($(this), "following", "unfollow");
97 | replaceAttr($(this), "btn-danger", "btn-default");
98 | $(this).text("关注");
99 |
100 | $(this).unbind();
101 | $(this).bind("click", follow);
102 | }.bind(this));
103 | }
104 |
105 | $(document).ready(function(){
106 |
107 | $("ul#site-nav li:eq(0)").attr("class", "active");
108 |
109 | $("button.following").bind("mouseenter", onMouseEnter);
110 | $("button.following").bind("mouseleave", onMouseLeave);
111 |
112 | $("button.unfollow").bind("click", follow);
113 | $("button.following").bind("click", unfollow);
114 | });
115 | {% endblock js_extra %}
116 |
117 | {% block content %}
118 |
119 |
120 |
121 |
122 | - 全部
123 | {% for game in game_list %}
124 | {% if request.GET.game == game.name %}
125 | - {{ game.name }}
126 | {% else %}
127 | - {{ game.name }}
128 | {% endif %}
129 | {% endfor %}
130 |
131 |
132 |
133 |
145 |
146 |
147 |
148 | {% for presenter in presenter_list %}
149 |
150 |
151 |
152 | 
153 |
154 | |
155 |
156 |
157 | {{ presenter.nickname }}
158 |
159 |
160 | {% if presenter.presenterdetail.showing %}
161 | 正在直播
162 | {% else %}
163 | 未开播
164 | {% endif %}
165 |
166 |
167 | {# display room's title if living else display last live end time.#}
168 | {% if presenter.presenterdetail.showing %}
169 |
170 | {{ presenter.presenterdetail.audience_count|abbreviation_number }}
171 |
172 |
173 |
174 | {{ presenter.presenterdetail.room_title }}
175 |
176 | {% else %}
177 |
178 |
179 | {% if presenter.presenterdetail.last_show_end %}
180 | 结束于 {{ presenter.presenterdetail.last_show_end|timesince }} 前
181 | {% endif %}
182 |
183 | {% endif %}
184 |
185 |
186 | |
187 |
188 | {% if presenter.introduce %}
189 | {{ presenter.introduce }}
190 | {% else %}
191 | 暂无介绍
192 | {% endif %}
193 |
206 | |
207 |
208 |
209 | {% if presenter in request.user.my_profile.follows.all %}
210 |
211 | {% else %}
212 |
213 | {% endif %}
214 |
215 | |
216 |
217 |
222 | |
223 |
224 | {% endfor %}
225 |
226 |
227 |
255 |
256 |
257 |
258 |
269 |
270 |
目前仅收录了站长常看的一些主播,如果大家有喜欢的主播希望添加进来,请在反馈建议里给我留言
271 |
欢迎大家对喜欢的主播进行评论,优秀的评论将会出现在主播介绍里
272 |
本站点的源码已经放到github上,地址在这里,感兴趣朋友可以看一下。
273 |
274 |
275 |
276 |
277 |
278 | {% endblock content %}
--------------------------------------------------------------------------------
/src/presenters/templates/presenters/reactjs.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {% load staticfiles %}
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | hello, world!
13 |
14 |
121 |
122 |
123 |
--------------------------------------------------------------------------------
/src/presenters/templates/search/indexes/presenters/presenter_text.txt:
--------------------------------------------------------------------------------
1 | {{ object.nickname }}
2 | {{ object.introduce }}
--------------------------------------------------------------------------------
/src/presenters/templatetags/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinity1207/thirtylol/7b3e3472ba589c7af1f53475ff160dabb8d04129/src/presenters/templatetags/__init__.py
--------------------------------------------------------------------------------
/src/presenters/templatetags/presenters_filters.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from django import template
3 |
4 | register = template.Library()
5 |
6 | @register.filter
7 | def abbreviation_number(value):
8 | """
9 | 一万以内的数值原样输出,超过一万的数值以万为单位,保留一位小数点输出
10 | 例:134567将输出为13.4万
11 | """
12 | try:
13 | f_value = float(value)
14 | if f_value < 10000:
15 | return value
16 | return u"%.1f万" % (f_value / 10000)
17 | except:
18 | return value
19 |
20 | @register.filter
21 | def modulo(value, arg):
22 | try:
23 | i_value = int(value)
24 | i_arg = int(arg)
25 | return i_value % i_arg
26 | except:
27 | return value
28 |
--------------------------------------------------------------------------------
/src/presenters/templatetags/settings_value.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django import template
3 |
4 | register = template.Library()
5 |
6 | # setting value
7 | @register.simple_tag
8 | def settings_value(name):
9 | return getattr(settings, name, "")
10 |
--------------------------------------------------------------------------------
/src/presenters/tests.py:
--------------------------------------------------------------------------------
1 | """
2 | This file demonstrates writing tests using the unittest module. These will pass
3 | when you run "manage.py test".
4 |
5 | Replace this with more appropriate tests for your application.
6 | """
7 |
8 | import django
9 | from django.test import TestCase
10 |
11 | # TODO: Configure your database in settings.py and sync before running tests.
12 |
13 | class SimpleTest(TestCase):
14 | """Tests for the application views."""
15 |
16 | if django.VERSION[:2] >= (1, 7):
17 | # Django 1.7 requires an explicit setup() when running tests in PTVS
18 | @classmethod
19 | def setUpClass(cls):
20 | django.setup()
21 |
22 | def test_basic_addition(self):
23 | """
24 | Tests that 1 + 1 always equals 2.
25 | """
26 | self.assertEqual(1 + 1, 2)
27 |
--------------------------------------------------------------------------------
/src/presenters/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls import url
2 | from . import views
3 |
4 | urlpatterns = [
5 | url('^$', views.index, name='index'),
6 | url('^(?P\d+)$', views.detail, name='detail'),
7 | url('^feedback$', views.feedback, name='feedback'),
8 | url('^about$', views.about, name='about'),
9 | url('^fetch$', views.fetch, name='fetch'),
10 | ]
11 |
--------------------------------------------------------------------------------
/src/presenters/views.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from django.shortcuts import render
4 | from django.shortcuts import get_object_or_404
5 | from django.http import HttpResponse, Http404
6 | from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
7 | from django.db.models import Count
8 | from .models import Platform, Presenter, Game
9 | import urllib, urllib2, cookielib, json
10 | import time
11 | import logging
12 | from haystack.query import SearchQuerySet
13 |
14 | logger = logging.getLogger(__name__)
15 |
16 | def index(request):
17 | presenter_list = Presenter.objects.order_by('-presenterdetail__showing', '-presenterdetail__audience_count')
18 |
19 | # 根据平台过滤
20 | platform_name = request.GET.get('platform')
21 | if platform_name:
22 | presenter_list = presenter_list.filter(platform__name=platform_name)
23 |
24 | game_name = request.GET.get('game')
25 | if game_name:
26 | presenter_list = presenter_list.filter(game__name=game_name)
27 |
28 | # 根据用户输入的查询条件过滤
29 | q = request.GET.get('q')
30 | if q:
31 | sqs = SearchQuerySet().auto_query(q)
32 | q_result = []
33 | for item in sqs:
34 | if item.model_name == 'presenter':
35 | q_result.append(item.pk)
36 | presenter_list = presenter_list.filter(pk__in=q_result)
37 |
38 | # 分页
39 | per_page = 10;
40 | paginator = Paginator(presenter_list, per_page)
41 | page_number = request.GET.get('page') # start from 1
42 | if not page_number:
43 | page_number = 1
44 |
45 | try:
46 | presenter_list = paginator.page(page_number)
47 | except PageNotAnInteger:
48 | page_number = 1
49 | presenter_list = paginator.page(1)
50 | except EmptyPage:
51 | page_number = paginator.num_pages
52 | presenter_list = paginator.page(paginator.num_pages)
53 |
54 | # 直播平台的分组信息
55 | platform_list = Platform.objects.annotate(num_presenters=Count('presenter')).order_by('-num_presenters')
56 | game_list = Game.objects.annotate(num_presenters=Count('presenter'))
57 |
58 | # 不包含页码的GET字符串
59 | get_list = []
60 | without_page_number_GET = ''
61 | for (k,v) in request.GET.iteritems():
62 | if k != 'page':
63 | get_list.append("%s=%s" % (k,v))
64 | if len(get_list) > 0:
65 | without_page_number_GET = "?" + "&".join(get_list)
66 |
67 | context = {
68 | 'presenter_list': presenter_list,
69 | 'page_number': int(page_number),
70 | 'num_pages': paginator.num_pages,
71 | 'platform_list': platform_list,
72 | 'num_all_presenters': Presenter.objects.count(),
73 | 'without_page_number_GET': without_page_number_GET,
74 | 'game_list': game_list,
75 | }
76 |
77 | return render(request, 'presenters/index.html', context)
78 |
79 | def detail(request, presenter_id):
80 | p = get_object_or_404(Presenter, id=presenter_id)
81 | context = {
82 | 'presenter': p,
83 | }
84 | return render(request, 'presenters/detail.html', context)
85 |
86 | def about(request):
87 | # print dir(request)
88 | return render(request, 'presenters/about.html')
89 |
90 | def feedback(request):
91 | return render(request, 'presenters/feedback.html')
92 |
93 | def fetch_huya(requst):
94 | # 虎牙直播
95 | # 貌似没有加密措施,登录后会得到一个uid
96 | result = []
97 | platform = Platform.objects.get(name=u'虎牙')
98 | try:
99 | params = json.loads(platform.login_param)
100 | except:
101 | logger.error("\tfetch presenter data from huya failed, error message: {%s}", '无效的fetch参数')
102 | return result
103 |
104 | huya_url = 'http://phone.huya.com/api/acts/reserved?uid=%s' % params['uid']
105 | response = urllib2.urlopen(huya_url)
106 | json_data = json.loads(response.read())
107 | if json_data['code'] != 0:
108 | logger.error("\tfetch presenter data from huya failed, error message: {%s}", json_data['message'])
109 | return result
110 |
111 | for data in json_data['data']:
112 | item = {}
113 | item['platform'] = u'虎牙'
114 | item['id_in_platform'] = data['aid']
115 | item['nickname'] = data['name']
116 | item['avatar_url'] = data['thumb']
117 | item['showing'] = data['isLiving']
118 | item['audience_count'] = data['users']
119 | item['room_title'] = data['contentIntro']
120 | result.append(item)
121 |
122 | logger.info("\tfetch presenter data from huya successful")
123 | return result
124 |
125 | def fetch_zhanqi(request):
126 | # 战旗直播
127 | # Cookie有效期为1个月,ipad客户端退出后需要重新登录并抓取
128 | # 需要使用POST请求
129 | result = []
130 | platform = Platform.objects.get(name=u'战旗')
131 | try:
132 | params = json.loads(platform.login_param)
133 | except:
134 | logger.error("\tfetch presenter data from zhanqi failed, error message: {%s}", '无效的fetch参数')
135 | return result
136 | _headers = {
137 | "Cookie": "PHPSESSID=%s; tj_uid=%s; ZQ_GUID=%s; ZQ_GUID_C=%s" % (params['PHPSESSID'], params['tj_uid'], params['ZQ_GUID'], params['ZQ_GUID_C']),
138 | "User-Agent": "Zhanqi.tv Api Client",
139 | }
140 |
141 | # 传入一个空的data,urllib2函数识别到data参数则会使用POST请求
142 | _data = {
143 | }
144 | _rand = int(time.time())
145 | zhanqi_url = 'http://www.zhanqi.tv/api/user/follow.listall?_rand=%s' % (_rand)
146 | req = urllib2.Request(zhanqi_url, headers=_headers, data=urllib.urlencode(_data))
147 | response = urllib2.urlopen(req)
148 | json_data = json.loads(response.read())
149 |
150 | if json_data['code'] != 0:
151 | logger.error("\tfetch presenter data from zhanqi failed, error message: {%s}", json_data['message'])
152 | return result
153 |
154 | for data in json_data['data']:
155 | item = {}
156 | item['platform'] = u'战旗'
157 | item['id_in_platform'] = data['uid']
158 | item['nickname'] = data['nickname']
159 | item['avatar_url'] = data['avatar'] + '-medium'
160 | item['gender'] = 'M' if data['gender'] == 2 else 'F'
161 | item['room_url'] = 'http://www.zhanqi.tv' + data['roomUrl']
162 | item['showing'] = True if data['status'] == "4" else False
163 | item['audience_count'] = int(data['online'])
164 | item['room_title'] = data['title']
165 |
166 | result.append(item)
167 |
168 | logger.info("\tfetch presenter data from zhanqi successful")
169 | return result
170 |
171 | def fetch_panda(request, status=2):
172 | result = []
173 |
174 | panda_url = 'http://api.m.panda.tv/ajax_get_follow_rooms'
175 | platform = Platform.objects.get(name=u'熊猫')
176 | try:
177 | params = json.loads(platform.login_param)
178 | except:
179 | logger.error("\tfetch presenter data from panda failed, error message: {%s}", '无效的fetch参数')
180 | return result
181 |
182 | _data = {
183 | '__plat': 'iOS',
184 | '__version': '1.0.0.1048',
185 | 'pageno': 1,
186 | 'pagenum': 10,
187 | 'pt_sign': params['pt_sign'],
188 | 'pt_time': int(time.time()),
189 | 'status': status,
190 | }
191 |
192 | M = params['M']
193 | R = params['R']
194 | # SESSCYPHP = '36d5c36ffb93c9121cc9a4ee4d959e05'
195 | _headers = {
196 | # 'Cookie': 'M=%s; R=%s; SESSCYPHP=%s' % (M, R, SESSCYPHP),
197 | 'Cookie': 'M=%s; R=%s;' % (M, R),
198 | 'User-Agent': 'PandaTV-ios/1.0.0 (iPhone; iOS 9.1; Scale/3.00)',
199 | 'Xiaozhangdepandatv': 1,
200 | 'Connection': 'keep-alive',
201 | }
202 |
203 | req = urllib2.Request(panda_url, headers=_headers, data=urllib.urlencode(_data))
204 | response = urllib2.urlopen(req)
205 | json_data = json.loads(response.read())
206 |
207 | for data in json_data['data']['items']:
208 | item = {}
209 | item['platform'] = u'熊猫'
210 | item['id_in_platform'] = data['userinfo']['rid']
211 | item['nickname'] = data['userinfo']['nickName']
212 | item['avatar_url'] = data['userinfo']['avatar']
213 | item['room_url'] = "http://www.panda.tv/%s" % data['id']
214 | item['showing'] = True if status == 2 else False
215 | item['audience_count'] = data['person_num']
216 | item['room_title'] = data['name']
217 | result.append(item)
218 |
219 | logger.info("\tfetch presenter data from panda successful")
220 | return result
221 |
222 |
223 | def fetch_douyu(request):
224 | result = []
225 |
226 | platform = Platform.objects.get(name=u'斗鱼')
227 | try:
228 | params = json.loads(platform.login_param)
229 | except:
230 | logger.error("\tfetch presenter data from douyu failed, error message: {%s}", '无效的fetch参数')
231 | return result
232 |
233 | _time = int(time.time())
234 | douyu_url = 'http://www.douyutv.com/api/v1/follow?aid=ios&limit=100&time=%s&client_sys=ios&offset=0&count=28&token=%s&auth=%s' % (_time, params['token'], params['auth'])
235 | req = urllib2.Request(douyu_url)
236 | response = urllib2.urlopen(req)
237 | json_data = json.loads(response.read())
238 | if 'error' in json_data and json_data['error'] > 0:
239 | logger.error("\tfetch presenter data from douyu failed, error message: {%s}", json_data['data'])
240 | return result
241 |
242 | for data in json_data['data']:
243 | item = {}
244 | item['platform'] = u'斗鱼'
245 | item['id_in_platform'] = data['owner_uid']
246 | item['nickname'] = data['nickname']
247 | item['avatar_url'] = "http://uc.douyutv.com/avatar.php?uid=%s&size=middle" % data['owner_uid']
248 | item['room_url'] = 'http://www.douyutv.com' + data['url']
249 | item['showing'] = True if data['show_status'] == '1' else False
250 | item['audience_count'] = data['online']
251 | item['room_title'] = data['room_name']
252 | result.append(item)
253 |
254 | logger.info("\tfetch presenter data from douyu successful")
255 | return result
256 |
257 | def fetch_longzhu(request):
258 | # 龙珠直播
259 | # 发送json请求时会在Cookie里放入plu_id
260 | # plu_id采取登录后从Charles中抓取
261 | result = []
262 | platform = Platform.objects.get(name=u'龙珠')
263 | try:
264 | params = json.loads(platform.login_param)
265 | except:
266 | logger.error("\tfetch presenter data from longzhu failed, error message: {%s}", '无效的fetch参数')
267 | return result
268 |
269 | plu_id = params['plu_id']
270 | longzhu_url = 'http://star.api.plu.cn/RoomSubscription/UserSubsciptionListForAll?isLive=0&liveSource=0&pageIndex=1&pageSize=10'
271 | _headers = {
272 | "Cookie": 'p1u_id=%s' % (plu_id)
273 | }
274 | cj = cookielib.CookieJar()
275 | opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj))
276 | req = urllib2.Request(longzhu_url, headers=_headers)
277 | response = opener.open(req)
278 | json_data = json.loads(response.read())
279 |
280 | for data in json_data['Data']:
281 | item = {}
282 | item['platform'] = Platform.objects.get(name=u'龙珠')
283 | item['id_in_platform'] = data['RoomId']
284 | item['nickname'] = data['RoomName']
285 | item['avatar_url'] = data['Avatar']
286 | item['room_url'] = 'http://star.longzhu.com/%s'% data['Domain']
287 | item['showing'] = data['IsLive']
288 |
289 | if item['showing']:
290 | # 获取房间信息(观众人数)
291 | PLULOGINSESSID = params['PLULOGINSESSID']
292 | room_detail = _fetch_longzhu_room_detail(plu_id, PLULOGINSESSID, data['Domain'])
293 | item['audience_count'] = room_detail['OnlineCount']
294 | item['room_title'] = room_detail['BaseRoomInfo']['BoardCastTitle']
295 | else:
296 | item['audience_count'] = 0
297 |
298 | result.append(item)
299 |
300 | logger.info("\tfetch presenter data from longzhu successful")
301 | return result
302 |
303 | def _fetch_longzhu_room_detail(plu_id, plu_login_sessid, domain):
304 | # 获得房间详细信息(观众人数……)
305 | room_info_url = 'http://star.apicdn.plu.cn/room/GetInfoJsonp?domain=%s' % domain
306 | _headers = {
307 | "Cookie": 'p1u_id=%s; PLULOGINSESSID=%s' % (plu_id, plu_login_sessid)
308 | }
309 | req = urllib2.Request(room_info_url, headers=_headers)
310 | response = urllib2.urlopen(req)
311 | return json.loads(response.read())
312 |
313 | def fetch(request):
314 | logger.info('fetch presenter data start')
315 |
316 | result = []
317 | result.extend(fetch_douyu(request))
318 | result.extend(fetch_huya(request))
319 | result.extend(fetch_zhanqi(request))
320 | result.extend(fetch_longzhu(request))
321 | result.extend(fetch_panda(request,status=2))
322 | result.extend(fetch_panda(request,status=3))
323 |
324 | for item in result:
325 | try:
326 | p = get_object_or_404(
327 | Presenter,
328 | platform__name=item['platform'],
329 | id_in_platform=item['id_in_platform'])
330 | except Http404:
331 | p = Presenter()
332 | p.platform = Platform.objects.get(name=item['platform'])
333 | p.id_in_platform = item['id_in_platform']
334 | p.nickname = item['nickname']
335 | p.avatar_url = item.get('avatar_url')
336 | p.gender = item.get('gender', 'M')
337 | p.room_url = item.get('room_url', 'http://www.30lol.com')
338 | p.save()
339 |
340 | p.presenterdetail.showing = item['showing']
341 | p.presenterdetail.room_title = item.get('room_title', '')
342 | p.presenterdetail.audience_count = item['audience_count']
343 | p.presenterdetail.save()
344 |
345 | logger.info('fetch presenter data done')
346 |
347 | return HttpResponse('fetch complete...')
348 |
--------------------------------------------------------------------------------
/src/presenters/whoosh_cn_backend.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | from __future__ import absolute_import, division, print_function, unicode_literals
4 |
5 | import os
6 | import re
7 | import shutil
8 | import threading
9 | import warnings
10 |
11 | from django.conf import settings
12 | from django.core.exceptions import ImproperlyConfigured
13 | from django.utils import six
14 | from django.utils.datetime_safe import datetime
15 |
16 | from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, EmptyResults, log_query
17 | from haystack.constants import DJANGO_CT, DJANGO_ID, ID
18 | from haystack.exceptions import MissingDependency, SearchBackendError, SkipDocument
19 | from haystack.inputs import Clean, Exact, PythonData, Raw
20 | from haystack.models import SearchResult
21 | from haystack.utils import log as logging
22 | from haystack.utils import get_identifier, get_model_ct
23 | from haystack.utils.app_loading import haystack_get_model
24 | from jieba.analyse import ChineseAnalyzer
25 | import jieba
26 | from os import path
27 |
28 | jieba.load_userdict(path.join(path.dirname(__file__), 'jieba_userdict.txt'))
29 |
30 | try:
31 | import json
32 | except ImportError:
33 | try:
34 | import simplejson as json
35 | except ImportError:
36 | from django.utils import simplejson as json
37 |
38 | try:
39 | from django.utils.encoding import force_text
40 | except ImportError:
41 | from django.utils.encoding import force_unicode as force_text
42 |
43 | try:
44 | import whoosh
45 | except ImportError:
46 | raise MissingDependency("The 'whoosh' backend requires the installation of 'Whoosh'. Please refer to the documentation.")
47 |
48 | # Handle minimum requirement.
49 | if not hasattr(whoosh, '__version__') or whoosh.__version__ < (2, 5, 0):
50 | raise MissingDependency("The 'whoosh' backend requires version 2.5.0 or greater.")
51 |
52 | # Bubble up the correct error.
53 | from whoosh import index
54 | from whoosh.analysis import StemmingAnalyzer
55 | from whoosh.fields import ID as WHOOSH_ID
56 | from whoosh.fields import BOOLEAN, DATETIME, IDLIST, KEYWORD, NGRAM, NGRAMWORDS, NUMERIC, Schema, TEXT
57 | from whoosh.filedb.filestore import FileStorage, RamStorage
58 | from whoosh.highlight import highlight as whoosh_highlight
59 | from whoosh.highlight import ContextFragmenter, HtmlFormatter
60 | from whoosh.qparser import QueryParser
61 | from whoosh.searching import ResultsPage
62 | from whoosh.writing import AsyncWriter
63 |
64 |
65 | DATETIME_REGEX = re.compile('^(?P\d{4})-(?P\d{2})-(?P\d{2})T(?P\d{2}):(?P\d{2}):(?P\d{2})(\.\d{3,6}Z?)?$')
66 | LOCALS = threading.local()
67 | LOCALS.RAM_STORE = None
68 |
69 |
70 | class WhooshHtmlFormatter(HtmlFormatter):
71 | """
72 | This is a HtmlFormatter simpler than the whoosh.HtmlFormatter.
73 | We use it to have consistent results across backends. Specifically,
74 | Solr, Xapian and Elasticsearch are using this formatting.
75 | """
76 | template = '<%(tag)s>%(t)s%(tag)s>'
77 |
78 |
79 | class WhooshSearchBackend(BaseSearchBackend):
80 | # Word reserved by Whoosh for special use.
81 | RESERVED_WORDS = (
82 | 'AND',
83 | 'NOT',
84 | 'OR',
85 | 'TO',
86 | )
87 |
88 | # Characters reserved by Whoosh for special use.
89 | # The '\\' must come first, so as not to overwrite the other slash replacements.
90 | RESERVED_CHARACTERS = (
91 | '\\', '+', '-', '&&', '||', '!', '(', ')', '{', '}',
92 | '[', ']', '^', '"', '~', '*', '?', ':', '.',
93 | )
94 |
95 | def __init__(self, connection_alias, **connection_options):
96 | super(WhooshSearchBackend, self).__init__(connection_alias, **connection_options)
97 | self.setup_complete = False
98 | self.use_file_storage = True
99 | self.post_limit = getattr(connection_options, 'POST_LIMIT', 128 * 1024 * 1024)
100 | self.path = connection_options.get('PATH')
101 |
102 | if connection_options.get('STORAGE', 'file') != 'file':
103 | self.use_file_storage = False
104 |
105 | if self.use_file_storage and not self.path:
106 | raise ImproperlyConfigured("You must specify a 'PATH' in your settings for connection '%s'." % connection_alias)
107 |
108 | self.log = logging.getLogger('haystack')
109 |
110 | def setup(self):
111 | """
112 | Defers loading until needed.
113 | """
114 | from haystack import connections
115 | new_index = False
116 |
117 | # Make sure the index is there.
118 | if self.use_file_storage and not os.path.exists(self.path):
119 | os.makedirs(self.path)
120 | new_index = True
121 |
122 | if self.use_file_storage and not os.access(self.path, os.W_OK):
123 | raise IOError("The path to your Whoosh index '%s' is not writable for the current user/group." % self.path)
124 |
125 | if self.use_file_storage:
126 | self.storage = FileStorage(self.path)
127 | else:
128 | global LOCALS
129 |
130 | if LOCALS.RAM_STORE is None:
131 | LOCALS.RAM_STORE = RamStorage()
132 |
133 | self.storage = LOCALS.RAM_STORE
134 |
135 | self.content_field_name, self.schema = self.build_schema(connections[self.connection_alias].get_unified_index().all_searchfields())
136 | self.parser = QueryParser(self.content_field_name, schema=self.schema)
137 |
138 | if new_index is True:
139 | self.index = self.storage.create_index(self.schema)
140 | else:
141 | try:
142 | self.index = self.storage.open_index(schema=self.schema)
143 | except index.EmptyIndexError:
144 | self.index = self.storage.create_index(self.schema)
145 |
146 | self.setup_complete = True
147 |
148 | def build_schema(self, fields):
149 | schema_fields = {
150 | ID: WHOOSH_ID(stored=True, unique=True),
151 | DJANGO_CT: WHOOSH_ID(stored=True),
152 | DJANGO_ID: WHOOSH_ID(stored=True),
153 | }
154 | # Grab the number of keys that are hard-coded into Haystack.
155 | # We'll use this to (possibly) fail slightly more gracefully later.
156 | initial_key_count = len(schema_fields)
157 | content_field_name = ''
158 |
159 | for field_name, field_class in fields.items():
160 | if field_class.is_multivalued:
161 | if field_class.indexed is False:
162 | schema_fields[field_class.index_fieldname] = IDLIST(stored=True, field_boost=field_class.boost)
163 | else:
164 | schema_fields[field_class.index_fieldname] = KEYWORD(stored=True, commas=True, scorable=True, field_boost=field_class.boost)
165 | elif field_class.field_type in ['date', 'datetime']:
166 | schema_fields[field_class.index_fieldname] = DATETIME(stored=field_class.stored, sortable=True)
167 | elif field_class.field_type == 'integer':
168 | schema_fields[field_class.index_fieldname] = NUMERIC(stored=field_class.stored, numtype=int, field_boost=field_class.boost)
169 | elif field_class.field_type == 'float':
170 | schema_fields[field_class.index_fieldname] = NUMERIC(stored=field_class.stored, numtype=float, field_boost=field_class.boost)
171 | elif field_class.field_type == 'boolean':
172 | # Field boost isn't supported on BOOLEAN as of 1.8.2.
173 | schema_fields[field_class.index_fieldname] = BOOLEAN(stored=field_class.stored)
174 | elif field_class.field_type == 'ngram':
175 | schema_fields[field_class.index_fieldname] = NGRAM(minsize=3, maxsize=15, stored=field_class.stored, field_boost=field_class.boost)
176 | elif field_class.field_type == 'edge_ngram':
177 | schema_fields[field_class.index_fieldname] = NGRAMWORDS(minsize=2, maxsize=15, at='start', stored=field_class.stored, field_boost=field_class.boost)
178 | else:
179 | # schema_fields[field_class.index_fieldname] = TEXT(stored=True, analyzer=StemmingAnalyzer(), field_boost=field_class.boost, sortable=True)
180 | schema_fields[field_class.index_fieldname] = TEXT(stored=True,
181 | analyzer=ChineseAnalyzer(),
182 | field_boost=field_class.boost,
183 | sortable=True)
184 |
185 | if field_class.document is True:
186 | content_field_name = field_class.index_fieldname
187 | schema_fields[field_class.index_fieldname].spelling = True
188 |
189 | # Fail more gracefully than relying on the backend to die if no fields
190 | # are found.
191 | if len(schema_fields) <= initial_key_count:
192 | raise SearchBackendError("No fields were found in any search_indexes. Please correct this before attempting to search.")
193 |
194 | return (content_field_name, Schema(**schema_fields))
195 |
196 | def update(self, index, iterable, commit=True):
197 | if not self.setup_complete:
198 | self.setup()
199 |
200 | self.index = self.index.refresh()
201 | writer = AsyncWriter(self.index)
202 |
203 | for obj in iterable:
204 | try:
205 | doc = index.full_prepare(obj)
206 | except SkipDocument:
207 | self.log.debug(u"Indexing for object `%s` skipped", obj)
208 | else:
209 | # Really make sure it's unicode, because Whoosh won't have it any
210 | # other way.
211 | for key in doc:
212 | doc[key] = self._from_python(doc[key])
213 |
214 | # Document boosts aren't supported in Whoosh 2.5.0+.
215 | if 'boost' in doc:
216 | del doc['boost']
217 |
218 | try:
219 | writer.update_document(**doc)
220 | except Exception as e:
221 | if not self.silently_fail:
222 | raise
223 |
224 | # We'll log the object identifier but won't include the actual object
225 | # to avoid the possibility of that generating encoding errors while
226 | # processing the log message:
227 | self.log.error(u"%s while preparing object for update" % e.__class__.__name__, exc_info=True, extra={
228 | "data": {
229 | "index": index,
230 | "object": get_identifier(obj)
231 | }
232 | })
233 |
234 | if len(iterable) > 0:
235 | # For now, commit no matter what, as we run into locking issues otherwise.
236 | writer.commit()
237 |
238 | def remove(self, obj_or_string, commit=True):
239 | if not self.setup_complete:
240 | self.setup()
241 |
242 | self.index = self.index.refresh()
243 | whoosh_id = get_identifier(obj_or_string)
244 |
245 | try:
246 | self.index.delete_by_query(q=self.parser.parse(u'%s:"%s"' % (ID, whoosh_id)))
247 | except Exception as e:
248 | if not self.silently_fail:
249 | raise
250 |
251 | self.log.error("Failed to remove document '%s' from Whoosh: %s", whoosh_id, e)
252 |
253 | def clear(self, models=[], commit=True):
254 | if not self.setup_complete:
255 | self.setup()
256 |
257 | self.index = self.index.refresh()
258 |
259 | try:
260 | if not models:
261 | self.delete_index()
262 | else:
263 | models_to_delete = []
264 |
265 | for model in models:
266 | models_to_delete.append(u"%s:%s" % (DJANGO_CT, get_model_ct(model)))
267 |
268 | self.index.delete_by_query(q=self.parser.parse(u" OR ".join(models_to_delete)))
269 | except Exception as e:
270 | if not self.silently_fail:
271 | raise
272 |
273 | self.log.error("Failed to clear documents from Whoosh: %s", e)
274 |
275 | def delete_index(self):
276 | # Per the Whoosh mailing list, if wiping out everything from the index,
277 | # it's much more efficient to simply delete the index files.
278 | if self.use_file_storage and os.path.exists(self.path):
279 | shutil.rmtree(self.path)
280 | elif not self.use_file_storage:
281 | self.storage.clean()
282 |
283 | # Recreate everything.
284 | self.setup()
285 |
286 | def optimize(self):
287 | if not self.setup_complete:
288 | self.setup()
289 |
290 | self.index = self.index.refresh()
291 | self.index.optimize()
292 |
293 | def calculate_page(self, start_offset=0, end_offset=None):
294 | # Prevent against Whoosh throwing an error. Requires an end_offset
295 | # greater than 0.
296 | if not end_offset is None and end_offset <= 0:
297 | end_offset = 1
298 |
299 | # Determine the page.
300 | page_num = 0
301 |
302 | if end_offset is None:
303 | end_offset = 1000000
304 |
305 | if start_offset is None:
306 | start_offset = 0
307 |
308 | page_length = end_offset - start_offset
309 |
310 | if page_length and page_length > 0:
311 | page_num = int(start_offset / page_length)
312 |
313 | # Increment because Whoosh uses 1-based page numbers.
314 | page_num += 1
315 | return page_num, page_length
316 |
317 | @log_query
318 | def search(self, query_string, sort_by=None, start_offset=0, end_offset=None,
319 | fields='', highlight=False, facets=None, date_facets=None, query_facets=None,
320 | narrow_queries=None, spelling_query=None, within=None,
321 | dwithin=None, distance_point=None, models=None,
322 | limit_to_registered_models=None, result_class=None, **kwargs):
323 | if not self.setup_complete:
324 | self.setup()
325 |
326 | # A zero length query should return no results.
327 | if len(query_string) == 0:
328 | return {
329 | 'results': [],
330 | 'hits': 0,
331 | }
332 |
333 | query_string = force_text(query_string)
334 |
335 | # A one-character query (non-wildcard) gets nabbed by a stopwords
336 | # filter and should yield zero results.
337 | if len(query_string) <= 1 and query_string != u'*':
338 | return {
339 | 'results': [],
340 | 'hits': 0,
341 | }
342 |
343 | reverse = False
344 |
345 | if sort_by is not None:
346 | # Determine if we need to reverse the results and if Whoosh can
347 | # handle what it's being asked to sort by. Reversing is an
348 | # all-or-nothing action, unfortunately.
349 | sort_by_list = []
350 | reverse_counter = 0
351 |
352 | for order_by in sort_by:
353 | if order_by.startswith('-'):
354 | reverse_counter += 1
355 |
356 | if reverse_counter and reverse_counter != len(sort_by):
357 | raise SearchBackendError("Whoosh requires all order_by fields"
358 | " to use the same sort direction")
359 |
360 | for order_by in sort_by:
361 | if order_by.startswith('-'):
362 | sort_by_list.append(order_by[1:])
363 |
364 | if len(sort_by_list) == 1:
365 | reverse = True
366 | else:
367 | sort_by_list.append(order_by)
368 |
369 | if len(sort_by_list) == 1:
370 | reverse = False
371 |
372 | sort_by = sort_by_list[0]
373 |
374 | if facets is not None:
375 | warnings.warn("Whoosh does not handle faceting.", Warning, stacklevel=2)
376 |
377 | if date_facets is not None:
378 | warnings.warn("Whoosh does not handle date faceting.", Warning, stacklevel=2)
379 |
380 | if query_facets is not None:
381 | warnings.warn("Whoosh does not handle query faceting.", Warning, stacklevel=2)
382 |
383 | narrowed_results = None
384 | self.index = self.index.refresh()
385 |
386 | if limit_to_registered_models is None:
387 | limit_to_registered_models = getattr(settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True)
388 |
389 | if models and len(models):
390 | model_choices = sorted(get_model_ct(model) for model in models)
391 | elif limit_to_registered_models:
392 | # Using narrow queries, limit the results to only models handled
393 | # with the current routers.
394 | model_choices = self.build_models_list()
395 | else:
396 | model_choices = []
397 |
398 | if len(model_choices) > 0:
399 | if narrow_queries is None:
400 | narrow_queries = set()
401 |
402 | narrow_queries.add(' OR '.join(['%s:%s' % (DJANGO_CT, rm) for rm in model_choices]))
403 |
404 | narrow_searcher = None
405 |
406 | if narrow_queries is not None:
407 | # Potentially expensive? I don't see another way to do it in Whoosh...
408 | narrow_searcher = self.index.searcher()
409 |
410 | for nq in narrow_queries:
411 | recent_narrowed_results = narrow_searcher.search(self.parser.parse(force_text(nq)),
412 | limit=None)
413 |
414 | if len(recent_narrowed_results) <= 0:
415 | return {
416 | 'results': [],
417 | 'hits': 0,
418 | }
419 |
420 | if narrowed_results:
421 | narrowed_results.filter(recent_narrowed_results)
422 | else:
423 | narrowed_results = recent_narrowed_results
424 |
425 | self.index = self.index.refresh()
426 |
427 | if self.index.doc_count():
428 | searcher = self.index.searcher()
429 | parsed_query = self.parser.parse(query_string)
430 |
431 | # In the event of an invalid/stopworded query, recover gracefully.
432 | if parsed_query is None:
433 | return {
434 | 'results': [],
435 | 'hits': 0,
436 | }
437 |
438 | page_num, page_length = self.calculate_page(start_offset, end_offset)
439 |
440 | search_kwargs = {
441 | 'pagelen': page_length,
442 | 'sortedby': sort_by,
443 | 'reverse': reverse,
444 | }
445 |
446 | # Handle the case where the results have been narrowed.
447 | if narrowed_results is not None:
448 | search_kwargs['filter'] = narrowed_results
449 |
450 | try:
451 | raw_page = searcher.search_page(
452 | parsed_query,
453 | page_num,
454 | **search_kwargs
455 | )
456 | except ValueError:
457 | if not self.silently_fail:
458 | raise
459 |
460 | return {
461 | 'results': [],
462 | 'hits': 0,
463 | 'spelling_suggestion': None,
464 | }
465 |
466 | # Because as of Whoosh 2.5.1, it will return the wrong page of
467 | # results if you request something too high. :(
468 | if raw_page.pagenum < page_num:
469 | return {
470 | 'results': [],
471 | 'hits': 0,
472 | 'spelling_suggestion': None,
473 | }
474 |
475 | results = self._process_results(raw_page, highlight=highlight, query_string=query_string, spelling_query=spelling_query, result_class=result_class)
476 | searcher.close()
477 |
478 | if hasattr(narrow_searcher, 'close'):
479 | narrow_searcher.close()
480 |
481 | return results
482 | else:
483 | if self.include_spelling:
484 | if spelling_query:
485 | spelling_suggestion = self.create_spelling_suggestion(spelling_query)
486 | else:
487 | spelling_suggestion = self.create_spelling_suggestion(query_string)
488 | else:
489 | spelling_suggestion = None
490 |
491 | return {
492 | 'results': [],
493 | 'hits': 0,
494 | 'spelling_suggestion': spelling_suggestion,
495 | }
496 |
497 | def more_like_this(self, model_instance, additional_query_string=None,
498 | start_offset=0, end_offset=None, models=None,
499 | limit_to_registered_models=None, result_class=None, **kwargs):
500 | if not self.setup_complete:
501 | self.setup()
502 |
503 | # Deferred models will have a different class ("RealClass_Deferred_fieldname")
504 | # which won't be in our registry:
505 | model_klass = model_instance._meta.concrete_model
506 |
507 | field_name = self.content_field_name
508 | narrow_queries = set()
509 | narrowed_results = None
510 | self.index = self.index.refresh()
511 |
512 | if limit_to_registered_models is None:
513 | limit_to_registered_models = getattr(settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True)
514 |
515 | if models and len(models):
516 | model_choices = sorted(get_model_ct(model) for model in models)
517 | elif limit_to_registered_models:
518 | # Using narrow queries, limit the results to only models handled
519 | # with the current routers.
520 | model_choices = self.build_models_list()
521 | else:
522 | model_choices = []
523 |
524 | if len(model_choices) > 0:
525 | if narrow_queries is None:
526 | narrow_queries = set()
527 |
528 | narrow_queries.add(' OR '.join(['%s:%s' % (DJANGO_CT, rm) for rm in model_choices]))
529 |
530 | if additional_query_string and additional_query_string != '*':
531 | narrow_queries.add(additional_query_string)
532 |
533 | narrow_searcher = None
534 |
535 | if narrow_queries is not None:
536 | # Potentially expensive? I don't see another way to do it in Whoosh...
537 | narrow_searcher = self.index.searcher()
538 |
539 | for nq in narrow_queries:
540 | recent_narrowed_results = narrow_searcher.search(self.parser.parse(force_text(nq)),
541 | limit=None)
542 |
543 | if len(recent_narrowed_results) <= 0:
544 | return {
545 | 'results': [],
546 | 'hits': 0,
547 | }
548 |
549 | if narrowed_results:
550 | narrowed_results.filter(recent_narrowed_results)
551 | else:
552 | narrowed_results = recent_narrowed_results
553 |
554 | page_num, page_length = self.calculate_page(start_offset, end_offset)
555 |
556 | self.index = self.index.refresh()
557 | raw_results = EmptyResults()
558 |
559 | if self.index.doc_count():
560 | query = "%s:%s" % (ID, get_identifier(model_instance))
561 | searcher = self.index.searcher()
562 | parsed_query = self.parser.parse(query)
563 | results = searcher.search(parsed_query)
564 |
565 | if len(results):
566 | raw_results = results[0].more_like_this(field_name, top=end_offset)
567 |
568 | # Handle the case where the results have been narrowed.
569 | if narrowed_results is not None and hasattr(raw_results, 'filter'):
570 | raw_results.filter(narrowed_results)
571 |
572 | try:
573 | raw_page = ResultsPage(raw_results, page_num, page_length)
574 | except ValueError:
575 | if not self.silently_fail:
576 | raise
577 |
578 | return {
579 | 'results': [],
580 | 'hits': 0,
581 | 'spelling_suggestion': None,
582 | }
583 |
584 | # Because as of Whoosh 2.5.1, it will return the wrong page of
585 | # results if you request something too high. :(
586 | if raw_page.pagenum < page_num:
587 | return {
588 | 'results': [],
589 | 'hits': 0,
590 | 'spelling_suggestion': None,
591 | }
592 |
593 | results = self._process_results(raw_page, result_class=result_class)
594 | searcher.close()
595 |
596 | if hasattr(narrow_searcher, 'close'):
597 | narrow_searcher.close()
598 |
599 | return results
600 |
601 | def _process_results(self, raw_page, highlight=False, query_string='', spelling_query=None, result_class=None):
602 | from haystack import connections
603 | results = []
604 |
605 | # It's important to grab the hits first before slicing. Otherwise, this
606 | # can cause pagination failures.
607 | hits = len(raw_page)
608 |
609 | if result_class is None:
610 | result_class = SearchResult
611 |
612 | facets = {}
613 | spelling_suggestion = None
614 | unified_index = connections[self.connection_alias].get_unified_index()
615 | indexed_models = unified_index.get_indexed_models()
616 |
617 | for doc_offset, raw_result in enumerate(raw_page):
618 | score = raw_page.score(doc_offset) or 0
619 | app_label, model_name = raw_result[DJANGO_CT].split('.')
620 | additional_fields = {}
621 | model = haystack_get_model(app_label, model_name)
622 |
623 | if model and model in indexed_models:
624 | for key, value in raw_result.items():
625 | index = unified_index.get_index(model)
626 | string_key = str(key)
627 |
628 | if string_key in index.fields and hasattr(index.fields[string_key], 'convert'):
629 | # Special-cased due to the nature of KEYWORD fields.
630 | if index.fields[string_key].is_multivalued:
631 | if value is None or len(value) is 0:
632 | additional_fields[string_key] = []
633 | else:
634 | additional_fields[string_key] = value.split(',')
635 | else:
636 | additional_fields[string_key] = index.fields[string_key].convert(value)
637 | else:
638 | additional_fields[string_key] = self._to_python(value)
639 |
640 | del(additional_fields[DJANGO_CT])
641 | del(additional_fields[DJANGO_ID])
642 |
643 | if highlight:
644 | sa = StemmingAnalyzer()
645 | formatter = WhooshHtmlFormatter('em')
646 | terms = [token.text for token in sa(query_string)]
647 |
648 | whoosh_result = whoosh_highlight(
649 | additional_fields.get(self.content_field_name),
650 | terms,
651 | sa,
652 | ContextFragmenter(),
653 | formatter
654 | )
655 | additional_fields['highlighted'] = {
656 | self.content_field_name: [whoosh_result],
657 | }
658 |
659 | result = result_class(app_label, model_name, raw_result[DJANGO_ID], score, **additional_fields)
660 | results.append(result)
661 | else:
662 | hits -= 1
663 |
664 | if self.include_spelling:
665 | if spelling_query:
666 | spelling_suggestion = self.create_spelling_suggestion(spelling_query)
667 | else:
668 | spelling_suggestion = self.create_spelling_suggestion(query_string)
669 |
670 | return {
671 | 'results': results,
672 | 'hits': hits,
673 | 'facets': facets,
674 | 'spelling_suggestion': spelling_suggestion,
675 | }
676 |
677 | def create_spelling_suggestion(self, query_string):
678 | spelling_suggestion = None
679 | reader = self.index.reader()
680 | corrector = reader.corrector(self.content_field_name)
681 | cleaned_query = force_text(query_string)
682 |
683 | if not query_string:
684 | return spelling_suggestion
685 |
686 | # Clean the string.
687 | for rev_word in self.RESERVED_WORDS:
688 | cleaned_query = cleaned_query.replace(rev_word, '')
689 |
690 | for rev_char in self.RESERVED_CHARACTERS:
691 | cleaned_query = cleaned_query.replace(rev_char, '')
692 |
693 | # Break it down.
694 | query_words = cleaned_query.split()
695 | suggested_words = []
696 |
697 | for word in query_words:
698 | suggestions = corrector.suggest(word, limit=1)
699 |
700 | if len(suggestions) > 0:
701 | suggested_words.append(suggestions[0])
702 |
703 | spelling_suggestion = ' '.join(suggested_words)
704 | return spelling_suggestion
705 |
706 | def _from_python(self, value):
707 | """
708 | Converts Python values to a string for Whoosh.
709 |
710 | Code courtesy of pysolr.
711 | """
712 | if hasattr(value, 'strftime'):
713 | if not hasattr(value, 'hour'):
714 | value = datetime(value.year, value.month, value.day, 0, 0, 0)
715 | elif isinstance(value, bool):
716 | if value:
717 | value = 'true'
718 | else:
719 | value = 'false'
720 | elif isinstance(value, (list, tuple)):
721 | value = u','.join([force_text(v) for v in value])
722 | elif isinstance(value, (six.integer_types, float)):
723 | # Leave it alone.
724 | pass
725 | else:
726 | value = force_text(value)
727 | return value
728 |
729 | def _to_python(self, value):
730 | """
731 | Converts values from Whoosh to native Python values.
732 |
733 | A port of the same method in pysolr, as they deal with data the same way.
734 | """
735 | if value == 'true':
736 | return True
737 | elif value == 'false':
738 | return False
739 |
740 | if value and isinstance(value, six.string_types):
741 | possible_datetime = DATETIME_REGEX.search(value)
742 |
743 | if possible_datetime:
744 | date_values = possible_datetime.groupdict()
745 |
746 | for dk, dv in date_values.items():
747 | date_values[dk] = int(dv)
748 |
749 | return datetime(date_values['year'], date_values['month'], date_values['day'], date_values['hour'], date_values['minute'], date_values['second'])
750 |
751 | try:
752 | # Attempt to use json to load the values.
753 | converted_value = json.loads(value)
754 |
755 | # Try to handle most built-in types.
756 | if isinstance(converted_value, (list, tuple, set, dict, six.integer_types, float, complex)):
757 | return converted_value
758 | except:
759 | # If it fails (SyntaxError or its ilk) or we don't trust it,
760 | # continue on.
761 | pass
762 |
763 | return value
764 |
765 |
766 | class WhooshSearchQuery(BaseSearchQuery):
767 | def _convert_datetime(self, date):
768 | if hasattr(date, 'hour'):
769 | return force_text(date.strftime('%Y%m%d%H%M%S'))
770 | else:
771 | return force_text(date.strftime('%Y%m%d000000'))
772 |
773 | def clean(self, query_fragment):
774 | """
775 | Provides a mechanism for sanitizing user input before presenting the
776 | value to the backend.
777 |
778 | Whoosh 1.X differs here in that you can no longer use a backslash
779 | to escape reserved characters. Instead, the whole word should be
780 | quoted.
781 | """
782 | words = query_fragment.split()
783 | cleaned_words = []
784 |
785 | for word in words:
786 | if word in self.backend.RESERVED_WORDS:
787 | word = word.replace(word, word.lower())
788 |
789 | for char in self.backend.RESERVED_CHARACTERS:
790 | if char in word:
791 | word = "'%s'" % word
792 | break
793 |
794 | cleaned_words.append(word)
795 |
796 | return ' '.join(cleaned_words)
797 |
798 | def build_query_fragment(self, field, filter_type, value):
799 | from haystack import connections
800 | query_frag = ''
801 | is_datetime = False
802 |
803 | if not hasattr(value, 'input_type_name'):
804 | # Handle when we've got a ``ValuesListQuerySet``...
805 | if hasattr(value, 'values_list'):
806 | value = list(value)
807 |
808 | if hasattr(value, 'strftime'):
809 | is_datetime = True
810 |
811 | if isinstance(value, six.string_types) and value != ' ':
812 | # It's not an ``InputType``. Assume ``Clean``.
813 | value = Clean(value)
814 | else:
815 | value = PythonData(value)
816 |
817 | # Prepare the query using the InputType.
818 | prepared_value = value.prepare(self)
819 |
820 | if not isinstance(prepared_value, (set, list, tuple)):
821 | # Then convert whatever we get back to what pysolr wants if needed.
822 | prepared_value = self.backend._from_python(prepared_value)
823 |
824 | # 'content' is a special reserved word, much like 'pk' in
825 | # Django's ORM layer. It indicates 'no special field'.
826 | if field == 'content':
827 | index_fieldname = ''
828 | else:
829 | index_fieldname = u'%s:' % connections[self._using].get_unified_index().get_index_fieldname(field)
830 |
831 | filter_types = {
832 | 'contains': '%s',
833 | 'startswith': "%s*",
834 | 'exact': '%s',
835 | 'gt': "{%s to}",
836 | 'gte': "[%s to]",
837 | 'lt': "{to %s}",
838 | 'lte': "[to %s]",
839 | }
840 |
841 | if value.post_process is False:
842 | query_frag = prepared_value
843 | else:
844 | if filter_type in ['contains', 'startswith']:
845 | if value.input_type_name == 'exact':
846 | query_frag = prepared_value
847 | else:
848 | # Iterate over terms & incorportate the converted form of each into the query.
849 | terms = []
850 |
851 | if isinstance(prepared_value, six.string_types):
852 | possible_values = prepared_value.split(' ')
853 | else:
854 | if is_datetime is True:
855 | prepared_value = self._convert_datetime(prepared_value)
856 |
857 | possible_values = [prepared_value]
858 |
859 | for possible_value in possible_values:
860 | terms.append(filter_types[filter_type] % self.backend._from_python(possible_value))
861 |
862 | if len(terms) == 1:
863 | query_frag = terms[0]
864 | else:
865 | query_frag = u"(%s)" % " AND ".join(terms)
866 | elif filter_type == 'in':
867 | in_options = []
868 |
869 | for possible_value in prepared_value:
870 | is_datetime = False
871 |
872 | if hasattr(possible_value, 'strftime'):
873 | is_datetime = True
874 |
875 | pv = self.backend._from_python(possible_value)
876 |
877 | if is_datetime is True:
878 | pv = self._convert_datetime(pv)
879 |
880 | if isinstance(pv, six.string_types) and not is_datetime:
881 | in_options.append('"%s"' % pv)
882 | else:
883 | in_options.append('%s' % pv)
884 |
885 | query_frag = "(%s)" % " OR ".join(in_options)
886 | elif filter_type == 'range':
887 | start = self.backend._from_python(prepared_value[0])
888 | end = self.backend._from_python(prepared_value[1])
889 |
890 | if hasattr(prepared_value[0], 'strftime'):
891 | start = self._convert_datetime(start)
892 |
893 | if hasattr(prepared_value[1], 'strftime'):
894 | end = self._convert_datetime(end)
895 |
896 | query_frag = u"[%s to %s]" % (start, end)
897 | elif filter_type == 'exact':
898 | if value.input_type_name == 'exact':
899 | query_frag = prepared_value
900 | else:
901 | prepared_value = Exact(prepared_value).prepare(self)
902 | query_frag = filter_types[filter_type] % prepared_value
903 | else:
904 | if is_datetime is True:
905 | prepared_value = self._convert_datetime(prepared_value)
906 |
907 | query_frag = filter_types[filter_type] % prepared_value
908 |
909 | if len(query_frag) and not isinstance(value, Raw):
910 | if not query_frag.startswith('(') and not query_frag.endswith(')'):
911 | query_frag = "(%s)" % query_frag
912 |
913 | return u"%s%s" % (index_fieldname, query_frag)
914 |
915 |
916 | # if not filter_type in ('in', 'range'):
917 | # # 'in' is a bit of a special case, as we don't want to
918 | # # convert a valid list/tuple to string. Defer handling it
919 | # # until later...
920 | # value = self.backend._from_python(value)
921 |
922 |
923 | class WhooshEngine(BaseEngine):
924 | backend = WhooshSearchBackend
925 | query = WhooshSearchQuery
926 |
--------------------------------------------------------------------------------
/src/requirements.txt:
--------------------------------------------------------------------------------
1 | django==1.8.4
2 | django-tastypie==0.12.2
3 | Pillow==2.9.0
4 | gunicorn==19.3.0
5 | jieba==0.37
6 | Whoosh==2.7.0
7 | django-haystack==2.4.0
8 | django-userena==1.4.1
--------------------------------------------------------------------------------
/src/thirtylol/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Package for thirtylol.
3 | """
4 |
--------------------------------------------------------------------------------
/src/thirtylol/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for thirtylol project.
3 | """
4 |
5 | from os import path
6 |
7 | SRC_ROOT = path.dirname(path.abspath(path.dirname(__file__)))
8 | PROJECT_ROOT = path.join(SRC_ROOT, '..')
9 |
10 | DEBUG = True
11 |
12 | ALLOWED_HOSTS = (
13 | '127.0.0.1',
14 | )
15 |
16 | ADMINS = (
17 | # ('Your Name', 'your_email@example.com'),
18 | )
19 |
20 | MANAGERS = ADMINS
21 |
22 | DATABASES = {
23 | 'default': {
24 | 'ENGINE': 'django.db.backends.sqlite3',
25 | 'NAME': path.join(PROJECT_ROOT, 'db.sqlite3'),
26 | 'USER': '',
27 | 'PASSWORD': '',
28 | 'HOST': '',
29 | 'PORT': '',
30 | }
31 | }
32 |
33 | LOGIN_URL = '/login'
34 |
35 | # Local time zone for this installation. Choices can be found here:
36 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
37 | # although not all choices may be available on all operating systems.
38 | # On Unix systems, a value of None will cause Django to use the same
39 | # timezone as the operating system.
40 | # If running in a Windows environment this must be set to the same as your
41 | # system time zone.
42 | TIME_ZONE = 'Asia/Shanghai'
43 |
44 | # Language code for this installation. All choices can be found here:
45 | # http://www.i18nguy.com/unicode/language-identifiers.html
46 | LANGUAGE_CODE = 'zh-cn'
47 | # LANGUAGE_CODE = 'zh-hans'
48 |
49 | SITE_ID = 1
50 |
51 | # If you set this to False, Django will make some optimizations so as not
52 | # to load the internationalization machinery.
53 | USE_I18N = True
54 |
55 | # If you set this to False, Django will not format dates, numbers and
56 | # calendars according to the current locale.
57 | USE_L10N = True
58 |
59 | # If you set this to False, Django will not use timezone-aware datetimes.
60 | USE_TZ = True
61 |
62 | # Absolute filesystem path to the directory that will hold user-uploaded files.
63 | # Example: "/home/media/media.lawrence.com/media/"
64 | MEDIA_ROOT = path.join(PROJECT_ROOT, 'media').replace('\\', '/')
65 |
66 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a
67 | # trailing slash.
68 | # Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
69 | MEDIA_URL = '/media/'
70 |
71 | # Absolute path to the directory static files should be collected to.
72 | # Don't put anything in this directory yourself; store your static files
73 | # in apps' "static/" subdirectories and in STATICFILES_DIRS.
74 | # Example: "/home/media/media.lawrence.com/static/"
75 | STATIC_ROOT = path.join(PROJECT_ROOT, 'static').replace('\\', '/')
76 |
77 | # URL prefix for static files.
78 | # Example: "http://media.lawrence.com/static/"
79 | STATIC_URL = '/static/'
80 |
81 | # Additional locations of static files
82 | STATICFILES_DIRS = (
83 | # Put strings here, like "/home/html/static" or "C:/www/django/static".
84 | # Always use forward slashes, even on Windows.
85 | # Don't forget to use absolute paths, not relative paths.
86 | )
87 |
88 | # List of finder classes that know how to find static files in
89 | # various locations.
90 | STATICFILES_FINDERS = (
91 | 'django.contrib.staticfiles.finders.FileSystemFinder',
92 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
93 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder',
94 | )
95 |
96 | # Make this unique, and don't share it with anybody.
97 | SECRET_KEY = 'n(bd1f1c%e8=_xad02x5qtfn%wgwpi492e$8_erx+d)!tpeoim'
98 |
99 | MIDDLEWARE_CLASSES = (
100 | 'django.middleware.common.CommonMiddleware',
101 | 'django.contrib.sessions.middleware.SessionMiddleware',
102 | 'django.middleware.csrf.CsrfViewMiddleware',
103 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
104 | 'django.contrib.messages.middleware.MessageMiddleware',
105 | # Uncomment the next line for simple clickjacking protection:
106 | # 'django.middleware.clickjacking.XFrameOptionsMiddleware',
107 | )
108 |
109 | ROOT_URLCONF = 'thirtylol.urls'
110 |
111 | # Python dotted path to the WSGI application used by Django's runserver.
112 | WSGI_APPLICATION = 'thirtylol.wsgi.application'
113 |
114 | TEMPLATES = [
115 | {
116 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
117 | 'DIRS': [path.join(PROJECT_ROOT, 'templates').replace('\\', '/')],
118 | 'APP_DIRS': True,
119 | 'OPTIONS': {
120 | 'context_processors': [
121 | 'django.template.context_processors.debug',
122 | 'django.template.context_processors.request',
123 | 'django.contrib.auth.context_processors.auth',
124 | 'django.contrib.messages.context_processors.messages',
125 | ],
126 | },
127 | },
128 | ]
129 |
130 | HAYSTACK_CONNECTIONS = {
131 | 'default': {
132 | 'ENGINE': 'presenters.whoosh_cn_backend.WhooshEngine',
133 | 'PATH': path.join(PROJECT_ROOT, 'whoosh_index').replace('\\', '/'),
134 | }
135 | }
136 |
137 | INSTALLED_APPS = (
138 | 'django.contrib.auth',
139 | 'django.contrib.contenttypes',
140 | 'django.contrib.sessions',
141 | 'django.contrib.sites',
142 | 'django.contrib.messages',
143 | 'django.contrib.staticfiles',
144 | 'django.contrib.admin',
145 | # 'tastypie',
146 | 'haystack',
147 | 'userena',
148 | 'guardian',
149 | 'easy_thumbnails',
150 | 'accounts',
151 | 'presenters',
152 | )
153 |
154 | # userena setting
155 | ANONYMOUS_USER_ID = -1
156 | AUTH_PROFILE_MODULE = 'accounts.UserProfile'
157 | AUTHENTICATION_BACKENDS = (
158 | 'accounts.backends.OAuthAuthenticationBackend',
159 | 'userena.backends.UserenaAuthenticationBackend',
160 | 'guardian.backends.ObjectPermissionBackend',
161 | 'django.contrib.auth.backends.ModelBackend',
162 | )
163 | USERENA_SIGNIN_REDIRECT_URL = '/accounts/%(username)s/'
164 | LOGIN_URL = '/accounts/signin/'
165 | LOGOUT_URL = '/accounts/signout/'
166 | USERENA_ACTIVATION_REQUIRED = False
167 |
168 | USERENA_MUGSHOT_SIZE = 48
169 | USERENA_MUGSHOT_CROP_TYPE = True
170 |
171 |
172 | # A sample logging configuration. The only tangible logging
173 | # performed by this configuration is to send an email to
174 | # the site admins on every HTTP 500 error when DEBUG=False.
175 | # See http://docs.djangoproject.com/en/dev/topics/logging for
176 | # more details on how to customize your logging configuration.
177 | LOGGING = {
178 | 'version': 1,
179 | 'disable_existing_loggers': False,
180 | 'loggers': {
181 | 'presenters': {
182 | 'handlers': ['file'],
183 | 'level': 'DEBUG',
184 | 'propagate': True,
185 | },
186 | },
187 | 'handlers': {
188 | 'file': {
189 | 'level': 'DEBUG',
190 | 'class': 'logging.FileHandler',
191 | 'filename': path.join(PROJECT_ROOT, 'log', 'presenters.log').replace('\\', '/'),
192 | 'formatter': 'verbose',
193 | },
194 | },
195 | 'formatters': {
196 | 'verbose': {
197 | 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s'
198 | },
199 | 'simple': {
200 | 'format': '%(levelname)s %(message)s'
201 | },
202 | },
203 | }
204 |
205 | # Specify the default test runner.
206 | TEST_RUNNER = 'django.test.runner.DiscoverRunner'
207 |
--------------------------------------------------------------------------------
/src/thirtylol/urls.py:
--------------------------------------------------------------------------------
1 | """
2 | Definition of urls for thirtylol.
3 | """
4 |
5 | from django.conf.urls import patterns, url
6 | from django.conf import settings
7 | from django.conf.urls.static import static
8 | from django.conf.urls import include
9 | from django.contrib import admin
10 |
11 | # from tastypie.api import Api
12 | # from presenters.api.resources import PresenterResource,PlatformResource,TagResource,PresenterDetailResource
13 | # v1_api = Api(api_name='v1')
14 | # v1_api.register(PresenterResource())
15 | # v1_api.register(PlatformResource())
16 | # v1_api.register(TagResource())
17 | # v1_api.register(PresenterDetailResource())
18 |
19 | admin.autodiscover()
20 |
21 | urlpatterns = patterns(
22 | '',
23 | url(r'^$', 'presenters.views.index', name='index'),
24 | url(r'^admin/', include(admin.site.urls)),
25 | url(r'^about/', 'presenters.views.about', name='about'),
26 | url(r'^feedback/', 'presenters.views.feedback', name='feedback'),
27 | url(r'^presenters/', include('presenters.urls', namespace='presenters')),
28 | # url(r'api/', include(v1_api.urls)),
29 | url(r'^search/', include('haystack.urls')),
30 | url(r'^accounts/', include('accounts.urls')),
31 |
32 | ) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
--------------------------------------------------------------------------------
/src/thirtylol/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for thirtylol project.
3 |
4 | This module contains the WSGI application used by Django's development server
5 | and any production WSGI deployments. It should expose a module-level variable
6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover
7 | this application via the ``WSGI_APPLICATION`` setting.
8 |
9 | Usually you will have the standard Django WSGI application here, but it also
10 | might make sense to replace the whole Django WSGI application with a custom one
11 | that later delegates to the Django one. For example, you could introduce WSGI
12 | middleware here, or combine a Django application with an application of another
13 | framework.
14 |
15 | """
16 | import os
17 |
18 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "thirtylol.settings")
19 |
20 | # This application object is used by any WSGI server configured to use this
21 | # file. This includes Django's development server, if the WSGI_APPLICATION
22 | # setting points here.
23 | from django.core.wsgi import get_wsgi_application
24 | application = get_wsgi_application()
25 |
26 | # Apply WSGI middleware here.
27 | # from helloworld.wsgi import HelloWorldApplication
28 | # application = HelloWorldApplication(application)
29 |
--------------------------------------------------------------------------------
/templates/base.html:
--------------------------------------------------------------------------------
1 |
2 | {% load staticfiles %}
3 | {% load settings_value %}
4 |
5 |
6 | {% block title %}30lol_游戏直播从这里开始{% endblock title %}
7 | {% block static_ref %}
8 |
9 |
10 |
11 |
12 | {% endblock static_ref %}
13 |
14 | {% block meta %}{% endblock meta %}
15 |
16 |
30 |
31 |
32 | {% block navbar %}
33 |
75 | {% endblock navbar %}
76 |
77 | {% block body %}{% endblock body %}
78 |
79 |
--------------------------------------------------------------------------------
/templates/userena/base_userena.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block body %}
4 |
5 | {% block content %}{% endblock content %}
6 |
7 | {% endblock body %}
8 |
--------------------------------------------------------------------------------