├── .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 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% if profile.source.flag >= 100 %} 23 | 24 | 25 | {% else %} 26 | 27 | 28 | {% endif %} 29 | 30 |
{{ profile.source.name }}
{{ profile.user.username }}
{{ profile.user.oauth.uid }}{{ profile.user.email }}
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 |
13 | 编辑头像 14 |
15 | {% csrf_token %} 16 | 17 | 18 | 19 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
20 | {% if profile.mugshot %} 21 | 22 | {% else %} 23 | 无 24 | {% endif %} 25 |
{{ form.mugshot }}
36 |
37 |
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 |
11 | 修改密码 12 |
13 | {% csrf_token %} 14 | {{ form.non_field_errors }} 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
{{ form.old_password.label_tag }}{{ form.old_password }}{{ form.old_password.errors }}
{{ form.new_password1.label_tag }}{{ form.new_password1 }}{{ form.new_password1.errors }}
{{ form.new_password2.label_tag }}{{ form.new_password2 }}{{ form.new_password2.errors }}
33 |
34 |
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 |
8 | {% trans "Reset Password" %} 9 |
10 | {% csrf_token %} 11 | 12 | 13 | 14 | 18 | 19 | 20 | 21 | 24 | 25 |
22 | 23 |
26 |
27 |
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 |
8 | {% trans "Reset Password" %} 9 |
10 | {% csrf_token %} 11 | {{ form.non_field_errors }} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
{{ form.old_password.label_tag }}{{ form.old_password }}{{ form.old_password.errors }}
{{ form.new_password1.label_tag }}{{ form.new_password1 }}{{ form.new_password1.errors }}
{{ form.new_password2.label_tag }}{{ form.new_password2 }}{{ form.new_password2.errors }}
30 |
31 |
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 |
15 | 登录 16 |
17 | {% csrf_token %} 18 | 19 | 20 | 21 | {{ form.non_field_errors }} 22 | 23 | 24 | 25 | 29 | 30 | 31 | 32 | 33 | 38 | 39 | 40 | 41 | 45 | 46 | 54 |
34 | 35 | 忘记密码 36 | {{ form.password.errors }} 37 |
42 | 43 | 44 |
55 |
56 |
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 |
10 | 注册新用户 11 |
12 | {% csrf_token %} 13 | {{ form.non_field_errors }} 14 | 15 | 16 | 17 | 21 | 22 | 23 | 24 | 25 | 29 | 30 | 31 | 32 | 33 | 37 | 38 | 39 | 40 | 41 | 45 | 46 | 47 | 48 | 49 | 52 | 53 |
18 | 19 | {{ form.username.errors }} 20 |
34 | 35 | {{ form.password1.errors }} 36 |
42 | 43 | {{ form.password2.errors }} 44 |
50 | 51 |
54 |
55 |
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 |

Hi, 欢迎来到 www.30lol.com

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 |
46 | 47 |
48 | 49 | 55 |
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 | 131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 | 139 |
140 | 141 |
142 |
143 |
144 |
145 |
146 |
147 | 148 | {% for presenter in presenter_list %} 149 | 150 | 155 | 187 | 207 | 216 | 223 | 224 | {% endfor %} 225 |
151 |
152 | 153 |
154 |
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 |
188 | {% if presenter.introduce %} 189 |

{{ presenter.introduce }}

190 | {% else %} 191 |

暂无介绍

192 | {% endif %} 193 |
194 |
195 | {% if request.user.is_superuser %} 196 | 编辑 197 | {% endif %} 198 | 199 | 200 | 评论() 201 | 202 | 203 | 204 |
205 |
206 |
217 | 222 |
226 | 227 | 255 |
256 | 257 |
258 |
259 | {{ num_all_presenters }}全部 260 | 261 | {% for platform in platform_list %} 262 | {% if request.GET.platform == platform.name %} 263 | {{ platform.num_presenters }}{{ platform.name }} 264 | {% else %} 265 | {{ platform.num_presenters }}{{ platform.name }} 266 | {% endif %} 267 | {% endfor %} 268 |
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' 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 |
34 | 74 |
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 | --------------------------------------------------------------------------------