@ -0,0 +1 @@
|
|||||||
|
# 项目文档
|
||||||
Binary file not shown.
Binary file not shown.
@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
from django import forms
|
||||||
|
from django.contrib.auth.admin import UserAdmin
|
||||||
|
from django.contrib.auth.forms import UserChangeForm
|
||||||
|
from django.contrib.auth.forms import UsernameField
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
|
from .models import BlogUser
|
||||||
|
|
||||||
|
|
||||||
|
class BlogUserCreationForm(forms.ModelForm):
|
||||||
|
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
|
||||||
|
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = BlogUser
|
||||||
|
fields = ('email',)
|
||||||
|
|
||||||
|
def clean_password2(self):
|
||||||
|
# Check that the two password entries match
|
||||||
|
password1 = self.cleaned_data.get("password1")
|
||||||
|
password2 = self.cleaned_data.get("password2")
|
||||||
|
if password1 and password2 and password1 != password2:
|
||||||
|
raise forms.ValidationError(_("passwords do not match"))
|
||||||
|
return password2
|
||||||
|
|
||||||
|
def save(self, commit=True):
|
||||||
|
# Save the provided password in hashed format
|
||||||
|
user = super().save(commit=False)
|
||||||
|
user.set_password(self.cleaned_data["password1"])
|
||||||
|
if commit:
|
||||||
|
user.source = 'adminsite'
|
||||||
|
user.save()
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
class BlogUserChangeForm(UserChangeForm):
|
||||||
|
class Meta:
|
||||||
|
model = BlogUser
|
||||||
|
fields = '__all__'
|
||||||
|
field_classes = {'username': UsernameField}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class BlogUserAdmin(UserAdmin):
|
||||||
|
form = BlogUserChangeForm
|
||||||
|
add_form = BlogUserCreationForm
|
||||||
|
list_display = (
|
||||||
|
'id',
|
||||||
|
'nickname',
|
||||||
|
'username',
|
||||||
|
'email',
|
||||||
|
'last_login',
|
||||||
|
'date_joined',
|
||||||
|
'source')
|
||||||
|
list_display_links = ('id', 'username')
|
||||||
|
ordering = ('-id',)
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AccountsConfig(AppConfig):
|
||||||
|
name = 'accounts'
|
||||||
@ -0,0 +1,117 @@
|
|||||||
|
from django import forms
|
||||||
|
from django.contrib.auth import get_user_model, password_validation
|
||||||
|
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.forms import widgets
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from . import utils
|
||||||
|
from .models import BlogUser
|
||||||
|
|
||||||
|
|
||||||
|
class LoginForm(AuthenticationForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(LoginForm, self).__init__(*args, **kwargs)
|
||||||
|
self.fields['username'].widget = widgets.TextInput(
|
||||||
|
attrs={'placeholder': "username", "class": "form-control"})
|
||||||
|
self.fields['password'].widget = widgets.PasswordInput(
|
||||||
|
attrs={'placeholder': "password", "class": "form-control"})
|
||||||
|
|
||||||
|
|
||||||
|
class RegisterForm(UserCreationForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(RegisterForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.fields['username'].widget = widgets.TextInput(
|
||||||
|
attrs={'placeholder': "username", "class": "form-control"})
|
||||||
|
self.fields['email'].widget = widgets.EmailInput(
|
||||||
|
attrs={'placeholder': "email", "class": "form-control"})
|
||||||
|
self.fields['password1'].widget = widgets.PasswordInput(
|
||||||
|
attrs={'placeholder': "password", "class": "form-control"})
|
||||||
|
self.fields['password2'].widget = widgets.PasswordInput(
|
||||||
|
attrs={'placeholder': "repeat password", "class": "form-control"})
|
||||||
|
|
||||||
|
def clean_email(self):
|
||||||
|
email = self.cleaned_data['email']
|
||||||
|
if get_user_model().objects.filter(email=email).exists():
|
||||||
|
raise ValidationError(_("email already exists"))
|
||||||
|
return email
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = get_user_model()
|
||||||
|
fields = ("username", "email")
|
||||||
|
|
||||||
|
|
||||||
|
class ForgetPasswordForm(forms.Form):
|
||||||
|
new_password1 = forms.CharField(
|
||||||
|
label=_("New password"),
|
||||||
|
widget=forms.PasswordInput(
|
||||||
|
attrs={
|
||||||
|
"class": "form-control",
|
||||||
|
'placeholder': _("New password")
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
new_password2 = forms.CharField(
|
||||||
|
label="确认密码",
|
||||||
|
widget=forms.PasswordInput(
|
||||||
|
attrs={
|
||||||
|
"class": "form-control",
|
||||||
|
'placeholder': _("Confirm password")
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
email = forms.EmailField(
|
||||||
|
label='邮箱',
|
||||||
|
widget=forms.TextInput(
|
||||||
|
attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': _("Email")
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
code = forms.CharField(
|
||||||
|
label=_('Code'),
|
||||||
|
widget=forms.TextInput(
|
||||||
|
attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': _("Code")
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean_new_password2(self):
|
||||||
|
password1 = self.data.get("new_password1")
|
||||||
|
password2 = self.data.get("new_password2")
|
||||||
|
if password1 and password2 and password1 != password2:
|
||||||
|
raise ValidationError(_("passwords do not match"))
|
||||||
|
password_validation.validate_password(password2)
|
||||||
|
|
||||||
|
return password2
|
||||||
|
|
||||||
|
def clean_email(self):
|
||||||
|
user_email = self.cleaned_data.get("email")
|
||||||
|
if not BlogUser.objects.filter(
|
||||||
|
email=user_email
|
||||||
|
).exists():
|
||||||
|
# todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改
|
||||||
|
raise ValidationError(_("email does not exist"))
|
||||||
|
return user_email
|
||||||
|
|
||||||
|
def clean_code(self):
|
||||||
|
code = self.cleaned_data.get("code")
|
||||||
|
error = utils.verify(
|
||||||
|
email=self.cleaned_data.get("email"),
|
||||||
|
code=code,
|
||||||
|
)
|
||||||
|
if error:
|
||||||
|
raise ValidationError(error)
|
||||||
|
return code
|
||||||
|
|
||||||
|
|
||||||
|
class ForgetPasswordCodeForm(forms.Form):
|
||||||
|
email = forms.EmailField(
|
||||||
|
label=_('Email'),
|
||||||
|
)
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
# Generated by Django 4.1.7 on 2023-03-02 07:14
|
||||||
|
|
||||||
|
import django.contrib.auth.models
|
||||||
|
import django.contrib.auth.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('auth', '0012_alter_user_first_name_max_length'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='BlogUser',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
|
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||||
|
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||||
|
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||||
|
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||||
|
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||||
|
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
|
||||||
|
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||||
|
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||||
|
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||||
|
('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')),
|
||||||
|
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
|
||||||
|
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
|
||||||
|
('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')),
|
||||||
|
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||||
|
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '用户',
|
||||||
|
'verbose_name_plural': '用户',
|
||||||
|
'ordering': ['-id'],
|
||||||
|
'get_latest_by': 'id',
|
||||||
|
},
|
||||||
|
managers=[
|
||||||
|
('objects', django.contrib.auth.models.UserManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
# Generated by Django 4.2.5 on 2023-09-06 13:13
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='bloguser',
|
||||||
|
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'},
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='bloguser',
|
||||||
|
name='created_time',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='bloguser',
|
||||||
|
name='last_mod_time',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='bloguser',
|
||||||
|
name='creation_time',
|
||||||
|
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='bloguser',
|
||||||
|
name='last_modify_time',
|
||||||
|
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='bloguser',
|
||||||
|
name='nickname',
|
||||||
|
field=models.CharField(blank=True, max_length=100, verbose_name='nick name'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='bloguser',
|
||||||
|
name='source',
|
||||||
|
field=models.CharField(blank=True, max_length=100, verbose_name='create source'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from djangoblog.utils import get_current_site
|
||||||
|
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
|
|
||||||
|
class BlogUser(AbstractUser):
|
||||||
|
nickname = models.CharField(_('nick name'), max_length=100, blank=True)
|
||||||
|
creation_time = models.DateTimeField(_('creation time'), default=now)
|
||||||
|
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
|
||||||
|
source = models.CharField(_('create source'), max_length=100, blank=True)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse(
|
||||||
|
'blog:author_detail', kwargs={
|
||||||
|
'author_name': self.username})
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.email
|
||||||
|
|
||||||
|
def get_full_url(self):
|
||||||
|
site = get_current_site().domain
|
||||||
|
url = "https://{site}{path}".format(site=site,
|
||||||
|
path=self.get_absolute_url())
|
||||||
|
return url
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['-id']
|
||||||
|
verbose_name = _('user')
|
||||||
|
verbose_name_plural = verbose_name
|
||||||
|
get_latest_by = 'id'
|
||||||
@ -0,0 +1,207 @@
|
|||||||
|
from django.test import Client, RequestFactory, TestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from accounts.models import BlogUser
|
||||||
|
from blog.models import Article, Category
|
||||||
|
from djangoblog.utils import *
|
||||||
|
from . import utils
|
||||||
|
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
|
|
||||||
|
class AccountTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.client = Client()
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
self.blog_user = BlogUser.objects.create_user(
|
||||||
|
username="test",
|
||||||
|
email="admin@admin.com",
|
||||||
|
password="12345678"
|
||||||
|
)
|
||||||
|
self.new_test = "xxx123--="
|
||||||
|
|
||||||
|
def test_validate_account(self):
|
||||||
|
site = get_current_site().domain
|
||||||
|
user = BlogUser.objects.create_superuser(
|
||||||
|
email="liangliangyy1@gmail.com",
|
||||||
|
username="liangliangyy1",
|
||||||
|
password="qwer!@#$ggg")
|
||||||
|
testuser = BlogUser.objects.get(username='liangliangyy1')
|
||||||
|
|
||||||
|
loginresult = self.client.login(
|
||||||
|
username='liangliangyy1',
|
||||||
|
password='qwer!@#$ggg')
|
||||||
|
self.assertEqual(loginresult, True)
|
||||||
|
response = self.client.get('/admin/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
category = Category()
|
||||||
|
category.name = "categoryaaa"
|
||||||
|
category.creation_time = timezone.now()
|
||||||
|
category.last_modify_time = timezone.now()
|
||||||
|
category.save()
|
||||||
|
|
||||||
|
article = Article()
|
||||||
|
article.title = "nicetitleaaa"
|
||||||
|
article.body = "nicecontentaaa"
|
||||||
|
article.author = user
|
||||||
|
article.category = category
|
||||||
|
article.type = 'a'
|
||||||
|
article.status = 'p'
|
||||||
|
article.save()
|
||||||
|
|
||||||
|
response = self.client.get(article.get_admin_url())
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_validate_register(self):
|
||||||
|
self.assertEquals(
|
||||||
|
0, len(
|
||||||
|
BlogUser.objects.filter(
|
||||||
|
email='user123@user.com')))
|
||||||
|
response = self.client.post(reverse('account:register'), {
|
||||||
|
'username': 'user1233',
|
||||||
|
'email': 'user123@user.com',
|
||||||
|
'password1': 'password123!q@wE#R$T',
|
||||||
|
'password2': 'password123!q@wE#R$T',
|
||||||
|
})
|
||||||
|
self.assertEquals(
|
||||||
|
1, len(
|
||||||
|
BlogUser.objects.filter(
|
||||||
|
email='user123@user.com')))
|
||||||
|
user = BlogUser.objects.filter(email='user123@user.com')[0]
|
||||||
|
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
|
||||||
|
path = reverse('accounts:result')
|
||||||
|
url = '{path}?type=validation&id={id}&sign={sign}'.format(
|
||||||
|
path=path, id=user.id, sign=sign)
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
self.client.login(username='user1233', password='password123!q@wE#R$T')
|
||||||
|
user = BlogUser.objects.filter(email='user123@user.com')[0]
|
||||||
|
user.is_superuser = True
|
||||||
|
user.is_staff = True
|
||||||
|
user.save()
|
||||||
|
delete_sidebar_cache()
|
||||||
|
category = Category()
|
||||||
|
category.name = "categoryaaa"
|
||||||
|
category.creation_time = timezone.now()
|
||||||
|
category.last_modify_time = timezone.now()
|
||||||
|
category.save()
|
||||||
|
|
||||||
|
article = Article()
|
||||||
|
article.category = category
|
||||||
|
article.title = "nicetitle333"
|
||||||
|
article.body = "nicecontentttt"
|
||||||
|
article.author = user
|
||||||
|
|
||||||
|
article.type = 'a'
|
||||||
|
article.status = 'p'
|
||||||
|
article.save()
|
||||||
|
|
||||||
|
response = self.client.get(article.get_admin_url())
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.get(reverse('account:logout'))
|
||||||
|
self.assertIn(response.status_code, [301, 302, 200])
|
||||||
|
|
||||||
|
response = self.client.get(article.get_admin_url())
|
||||||
|
self.assertIn(response.status_code, [301, 302, 200])
|
||||||
|
|
||||||
|
response = self.client.post(reverse('account:login'), {
|
||||||
|
'username': 'user1233',
|
||||||
|
'password': 'password123'
|
||||||
|
})
|
||||||
|
self.assertIn(response.status_code, [301, 302, 200])
|
||||||
|
|
||||||
|
response = self.client.get(article.get_admin_url())
|
||||||
|
self.assertIn(response.status_code, [301, 302, 200])
|
||||||
|
|
||||||
|
def test_verify_email_code(self):
|
||||||
|
to_email = "admin@admin.com"
|
||||||
|
code = generate_code()
|
||||||
|
utils.set_code(to_email, code)
|
||||||
|
utils.send_verify_email(to_email, code)
|
||||||
|
|
||||||
|
err = utils.verify("admin@admin.com", code)
|
||||||
|
self.assertEqual(err, None)
|
||||||
|
|
||||||
|
err = utils.verify("admin@123.com", code)
|
||||||
|
self.assertEqual(type(err), str)
|
||||||
|
|
||||||
|
def test_forget_password_email_code_success(self):
|
||||||
|
resp = self.client.post(
|
||||||
|
path=reverse("account:forget_password_code"),
|
||||||
|
data=dict(email="admin@admin.com")
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
self.assertEqual(resp.content.decode("utf-8"), "ok")
|
||||||
|
|
||||||
|
def test_forget_password_email_code_fail(self):
|
||||||
|
resp = self.client.post(
|
||||||
|
path=reverse("account:forget_password_code"),
|
||||||
|
data=dict()
|
||||||
|
)
|
||||||
|
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
|
||||||
|
|
||||||
|
resp = self.client.post(
|
||||||
|
path=reverse("account:forget_password_code"),
|
||||||
|
data=dict(email="admin@com")
|
||||||
|
)
|
||||||
|
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
|
||||||
|
|
||||||
|
def test_forget_password_email_success(self):
|
||||||
|
code = generate_code()
|
||||||
|
utils.set_code(self.blog_user.email, code)
|
||||||
|
data = dict(
|
||||||
|
new_password1=self.new_test,
|
||||||
|
new_password2=self.new_test,
|
||||||
|
email=self.blog_user.email,
|
||||||
|
code=code,
|
||||||
|
)
|
||||||
|
resp = self.client.post(
|
||||||
|
path=reverse("account:forget_password"),
|
||||||
|
data=data
|
||||||
|
)
|
||||||
|
self.assertEqual(resp.status_code, 302)
|
||||||
|
|
||||||
|
# 验证用户密码是否修改成功
|
||||||
|
blog_user = BlogUser.objects.filter(
|
||||||
|
email=self.blog_user.email,
|
||||||
|
).first() # type: BlogUser
|
||||||
|
self.assertNotEqual(blog_user, None)
|
||||||
|
self.assertEqual(blog_user.check_password(data["new_password1"]), True)
|
||||||
|
|
||||||
|
def test_forget_password_email_not_user(self):
|
||||||
|
data = dict(
|
||||||
|
new_password1=self.new_test,
|
||||||
|
new_password2=self.new_test,
|
||||||
|
email="123@123.com",
|
||||||
|
code="123456",
|
||||||
|
)
|
||||||
|
resp = self.client.post(
|
||||||
|
path=reverse("account:forget_password"),
|
||||||
|
data=data
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
|
def test_forget_password_email_code_error(self):
|
||||||
|
code = generate_code()
|
||||||
|
utils.set_code(self.blog_user.email, code)
|
||||||
|
data = dict(
|
||||||
|
new_password1=self.new_test,
|
||||||
|
new_password2=self.new_test,
|
||||||
|
email=self.blog_user.email,
|
||||||
|
code="111111",
|
||||||
|
)
|
||||||
|
resp = self.client.post(
|
||||||
|
path=reverse("account:forget_password"),
|
||||||
|
data=data
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from django.urls import re_path
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
from .forms import LoginForm
|
||||||
|
|
||||||
|
app_name = "accounts"
|
||||||
|
|
||||||
|
urlpatterns = [re_path(r'^login/$',
|
||||||
|
views.LoginView.as_view(success_url='/'),
|
||||||
|
name='login',
|
||||||
|
kwargs={'authentication_form': LoginForm}),
|
||||||
|
re_path(r'^register/$',
|
||||||
|
views.RegisterView.as_view(success_url="/"),
|
||||||
|
name='register'),
|
||||||
|
re_path(r'^logout/$',
|
||||||
|
views.LogoutView.as_view(),
|
||||||
|
name='logout'),
|
||||||
|
path(r'account/result.html',
|
||||||
|
views.account_result,
|
||||||
|
name='result'),
|
||||||
|
re_path(r'^forget_password/$',
|
||||||
|
views.ForgetPasswordView.as_view(),
|
||||||
|
name='forget_password'),
|
||||||
|
re_path(r'^forget_password_code/$',
|
||||||
|
views.ForgetPasswordEmailCode.as_view(),
|
||||||
|
name='forget_password_code'),
|
||||||
|
]
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.backends import ModelBackend
|
||||||
|
|
||||||
|
|
||||||
|
class EmailOrUsernameModelBackend(ModelBackend):
|
||||||
|
"""
|
||||||
|
允许使用用户名或邮箱登录
|
||||||
|
"""
|
||||||
|
|
||||||
|
def authenticate(self, request, username=None, password=None, **kwargs):
|
||||||
|
if '@' in username:
|
||||||
|
kwargs = {'email': username}
|
||||||
|
else:
|
||||||
|
kwargs = {'username': username}
|
||||||
|
try:
|
||||||
|
user = get_user_model().objects.get(**kwargs)
|
||||||
|
if user.check_password(password):
|
||||||
|
return user
|
||||||
|
except get_user_model().DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_user(self, username):
|
||||||
|
try:
|
||||||
|
return get_user_model().objects.get(pk=username)
|
||||||
|
except get_user_model().DoesNotExist:
|
||||||
|
return None
|
||||||
@ -0,0 +1,112 @@
|
|||||||
|
from django import forms
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.html import format_html
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
|
from .models import Article
|
||||||
|
|
||||||
|
|
||||||
|
class ArticleForm(forms.ModelForm):
|
||||||
|
# body = forms.CharField(widget=AdminPagedownWidget())
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Article
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
|
def makr_article_publish(modeladmin, request, queryset):
|
||||||
|
queryset.update(status='p')
|
||||||
|
|
||||||
|
|
||||||
|
def draft_article(modeladmin, request, queryset):
|
||||||
|
queryset.update(status='d')
|
||||||
|
|
||||||
|
|
||||||
|
def close_article_commentstatus(modeladmin, request, queryset):
|
||||||
|
queryset.update(comment_status='c')
|
||||||
|
|
||||||
|
|
||||||
|
def open_article_commentstatus(modeladmin, request, queryset):
|
||||||
|
queryset.update(comment_status='o')
|
||||||
|
|
||||||
|
|
||||||
|
makr_article_publish.short_description = _('Publish selected articles')
|
||||||
|
draft_article.short_description = _('Draft selected articles')
|
||||||
|
close_article_commentstatus.short_description = _('Close article comments')
|
||||||
|
open_article_commentstatus.short_description = _('Open article comments')
|
||||||
|
|
||||||
|
|
||||||
|
class ArticlelAdmin(admin.ModelAdmin):
|
||||||
|
list_per_page = 20
|
||||||
|
search_fields = ('body', 'title')
|
||||||
|
form = ArticleForm
|
||||||
|
list_display = (
|
||||||
|
'id',
|
||||||
|
'title',
|
||||||
|
'author',
|
||||||
|
'link_to_category',
|
||||||
|
'creation_time',
|
||||||
|
'views',
|
||||||
|
'status',
|
||||||
|
'type',
|
||||||
|
'article_order')
|
||||||
|
list_display_links = ('id', 'title')
|
||||||
|
list_filter = ('status', 'type', 'category')
|
||||||
|
filter_horizontal = ('tags',)
|
||||||
|
exclude = ('creation_time', 'last_modify_time')
|
||||||
|
view_on_site = True
|
||||||
|
actions = [
|
||||||
|
makr_article_publish,
|
||||||
|
draft_article,
|
||||||
|
close_article_commentstatus,
|
||||||
|
open_article_commentstatus]
|
||||||
|
|
||||||
|
def link_to_category(self, obj):
|
||||||
|
info = (obj.category._meta.app_label, obj.category._meta.model_name)
|
||||||
|
link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
|
||||||
|
return format_html(u'<a href="%s">%s</a>' % (link, obj.category.name))
|
||||||
|
|
||||||
|
link_to_category.short_description = _('category')
|
||||||
|
|
||||||
|
def get_form(self, request, obj=None, **kwargs):
|
||||||
|
form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
|
||||||
|
form.base_fields['author'].queryset = get_user_model(
|
||||||
|
).objects.filter(is_superuser=True)
|
||||||
|
return form
|
||||||
|
|
||||||
|
def save_model(self, request, obj, form, change):
|
||||||
|
super(ArticlelAdmin, self).save_model(request, obj, form, change)
|
||||||
|
|
||||||
|
def get_view_on_site_url(self, obj=None):
|
||||||
|
if obj:
|
||||||
|
url = obj.get_full_url()
|
||||||
|
return url
|
||||||
|
else:
|
||||||
|
from djangoblog.utils import get_current_site
|
||||||
|
site = get_current_site().domain
|
||||||
|
return site
|
||||||
|
|
||||||
|
|
||||||
|
class TagAdmin(admin.ModelAdmin):
|
||||||
|
exclude = ('slug', 'last_mod_time', 'creation_time')
|
||||||
|
|
||||||
|
|
||||||
|
class CategoryAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'parent_category', 'index')
|
||||||
|
exclude = ('slug', 'last_mod_time', 'creation_time')
|
||||||
|
|
||||||
|
|
||||||
|
class LinksAdmin(admin.ModelAdmin):
|
||||||
|
exclude = ('last_mod_time', 'creation_time')
|
||||||
|
|
||||||
|
|
||||||
|
class SideBarAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'content', 'is_enable', 'sequence')
|
||||||
|
exclude = ('last_mod_time', 'creation_time')
|
||||||
|
|
||||||
|
|
||||||
|
class BlogSettingsAdmin(admin.ModelAdmin):
|
||||||
|
pass
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class BlogConfig(AppConfig):
|
||||||
|
name = 'blog'
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from djangoblog.utils import cache, get_blog_setting
|
||||||
|
from .models import Category, Article
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def seo_processor(requests):
|
||||||
|
key = 'seo_processor'
|
||||||
|
value = cache.get(key)
|
||||||
|
if value:
|
||||||
|
return value
|
||||||
|
else:
|
||||||
|
logger.info('set processor cache.')
|
||||||
|
setting = get_blog_setting()
|
||||||
|
value = {
|
||||||
|
'SITE_NAME': setting.site_name,
|
||||||
|
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense,
|
||||||
|
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes,
|
||||||
|
'SITE_SEO_DESCRIPTION': setting.site_seo_description,
|
||||||
|
'SITE_DESCRIPTION': setting.site_description,
|
||||||
|
'SITE_KEYWORDS': setting.site_keywords,
|
||||||
|
'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/',
|
||||||
|
'ARTICLE_SUB_LENGTH': setting.article_sub_length,
|
||||||
|
'nav_category_list': Category.objects.all(),
|
||||||
|
'nav_pages': Article.objects.filter(
|
||||||
|
type='p',
|
||||||
|
status='p'),
|
||||||
|
'OPEN_SITE_COMMENT': setting.open_site_comment,
|
||||||
|
'BEIAN_CODE': setting.beian_code,
|
||||||
|
'ANALYTICS_CODE': setting.analytics_code,
|
||||||
|
"BEIAN_CODE_GONGAN": setting.gongan_beiancode,
|
||||||
|
"SHOW_GONGAN_CODE": setting.show_gongan_code,
|
||||||
|
"CURRENT_YEAR": timezone.now().year,
|
||||||
|
"GLOBAL_HEADER": setting.global_header,
|
||||||
|
"GLOBAL_FOOTER": setting.global_footer,
|
||||||
|
"COMMENT_NEED_REVIEW": setting.comment_need_review,
|
||||||
|
}
|
||||||
|
cache.set(key, value, 60 * 60 * 10)
|
||||||
|
return value
|
||||||
@ -0,0 +1,213 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
|
import elasticsearch.client
|
||||||
|
from django.conf import settings
|
||||||
|
from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean
|
||||||
|
from elasticsearch_dsl.connections import connections
|
||||||
|
|
||||||
|
from blog.models import Article
|
||||||
|
|
||||||
|
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
|
||||||
|
|
||||||
|
if ELASTICSEARCH_ENABLED:
|
||||||
|
connections.create_connection(
|
||||||
|
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
|
||||||
|
from elasticsearch import Elasticsearch
|
||||||
|
|
||||||
|
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
|
||||||
|
from elasticsearch.client import IngestClient
|
||||||
|
|
||||||
|
c = IngestClient(es)
|
||||||
|
try:
|
||||||
|
c.get_pipeline('geoip')
|
||||||
|
except elasticsearch.exceptions.NotFoundError:
|
||||||
|
c.put_pipeline('geoip', body='''{
|
||||||
|
"description" : "Add geoip info",
|
||||||
|
"processors" : [
|
||||||
|
{
|
||||||
|
"geoip" : {
|
||||||
|
"field" : "ip"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}''')
|
||||||
|
|
||||||
|
|
||||||
|
class GeoIp(InnerDoc):
|
||||||
|
continent_name = Keyword()
|
||||||
|
country_iso_code = Keyword()
|
||||||
|
country_name = Keyword()
|
||||||
|
location = GeoPoint()
|
||||||
|
|
||||||
|
|
||||||
|
class UserAgentBrowser(InnerDoc):
|
||||||
|
Family = Keyword()
|
||||||
|
Version = Keyword()
|
||||||
|
|
||||||
|
|
||||||
|
class UserAgentOS(UserAgentBrowser):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UserAgentDevice(InnerDoc):
|
||||||
|
Family = Keyword()
|
||||||
|
Brand = Keyword()
|
||||||
|
Model = Keyword()
|
||||||
|
|
||||||
|
|
||||||
|
class UserAgent(InnerDoc):
|
||||||
|
browser = Object(UserAgentBrowser, required=False)
|
||||||
|
os = Object(UserAgentOS, required=False)
|
||||||
|
device = Object(UserAgentDevice, required=False)
|
||||||
|
string = Text()
|
||||||
|
is_bot = Boolean()
|
||||||
|
|
||||||
|
|
||||||
|
class ElapsedTimeDocument(Document):
|
||||||
|
url = Keyword()
|
||||||
|
time_taken = Long()
|
||||||
|
log_datetime = Date()
|
||||||
|
ip = Keyword()
|
||||||
|
geoip = Object(GeoIp, required=False)
|
||||||
|
useragent = Object(UserAgent, required=False)
|
||||||
|
|
||||||
|
class Index:
|
||||||
|
name = 'performance'
|
||||||
|
settings = {
|
||||||
|
"number_of_shards": 1,
|
||||||
|
"number_of_replicas": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
doc_type = 'ElapsedTime'
|
||||||
|
|
||||||
|
|
||||||
|
class ElaspedTimeDocumentManager:
|
||||||
|
@staticmethod
|
||||||
|
def build_index():
|
||||||
|
from elasticsearch import Elasticsearch
|
||||||
|
client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
|
||||||
|
res = client.indices.exists(index="performance")
|
||||||
|
if not res:
|
||||||
|
ElapsedTimeDocument.init()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def delete_index():
|
||||||
|
from elasticsearch import Elasticsearch
|
||||||
|
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
|
||||||
|
es.indices.delete(index='performance', ignore=[400, 404])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create(url, time_taken, log_datetime, useragent, ip):
|
||||||
|
ElaspedTimeDocumentManager.build_index()
|
||||||
|
ua = UserAgent()
|
||||||
|
ua.browser = UserAgentBrowser()
|
||||||
|
ua.browser.Family = useragent.browser.family
|
||||||
|
ua.browser.Version = useragent.browser.version_string
|
||||||
|
|
||||||
|
ua.os = UserAgentOS()
|
||||||
|
ua.os.Family = useragent.os.family
|
||||||
|
ua.os.Version = useragent.os.version_string
|
||||||
|
|
||||||
|
ua.device = UserAgentDevice()
|
||||||
|
ua.device.Family = useragent.device.family
|
||||||
|
ua.device.Brand = useragent.device.brand
|
||||||
|
ua.device.Model = useragent.device.model
|
||||||
|
ua.string = useragent.ua_string
|
||||||
|
ua.is_bot = useragent.is_bot
|
||||||
|
|
||||||
|
doc = ElapsedTimeDocument(
|
||||||
|
meta={
|
||||||
|
'id': int(
|
||||||
|
round(
|
||||||
|
time.time() *
|
||||||
|
1000))
|
||||||
|
},
|
||||||
|
url=url,
|
||||||
|
time_taken=time_taken,
|
||||||
|
log_datetime=log_datetime,
|
||||||
|
useragent=ua, ip=ip)
|
||||||
|
doc.save(pipeline="geoip")
|
||||||
|
|
||||||
|
|
||||||
|
class ArticleDocument(Document):
|
||||||
|
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
|
||||||
|
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
|
||||||
|
author = Object(properties={
|
||||||
|
'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
|
||||||
|
'id': Integer()
|
||||||
|
})
|
||||||
|
category = Object(properties={
|
||||||
|
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
|
||||||
|
'id': Integer()
|
||||||
|
})
|
||||||
|
tags = Object(properties={
|
||||||
|
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
|
||||||
|
'id': Integer()
|
||||||
|
})
|
||||||
|
|
||||||
|
pub_time = Date()
|
||||||
|
status = Text()
|
||||||
|
comment_status = Text()
|
||||||
|
type = Text()
|
||||||
|
views = Integer()
|
||||||
|
article_order = Integer()
|
||||||
|
|
||||||
|
class Index:
|
||||||
|
name = 'blog'
|
||||||
|
settings = {
|
||||||
|
"number_of_shards": 1,
|
||||||
|
"number_of_replicas": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
doc_type = 'Article'
|
||||||
|
|
||||||
|
|
||||||
|
class ArticleDocumentManager():
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.create_index()
|
||||||
|
|
||||||
|
def create_index(self):
|
||||||
|
ArticleDocument.init()
|
||||||
|
|
||||||
|
def delete_index(self):
|
||||||
|
from elasticsearch import Elasticsearch
|
||||||
|
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
|
||||||
|
es.indices.delete(index='blog', ignore=[400, 404])
|
||||||
|
|
||||||
|
def convert_to_doc(self, articles):
|
||||||
|
return [
|
||||||
|
ArticleDocument(
|
||||||
|
meta={
|
||||||
|
'id': article.id},
|
||||||
|
body=article.body,
|
||||||
|
title=article.title,
|
||||||
|
author={
|
||||||
|
'nickname': article.author.username,
|
||||||
|
'id': article.author.id},
|
||||||
|
category={
|
||||||
|
'name': article.category.name,
|
||||||
|
'id': article.category.id},
|
||||||
|
tags=[
|
||||||
|
{
|
||||||
|
'name': t.name,
|
||||||
|
'id': t.id} for t in article.tags.all()],
|
||||||
|
pub_time=article.pub_time,
|
||||||
|
status=article.status,
|
||||||
|
comment_status=article.comment_status,
|
||||||
|
type=article.type,
|
||||||
|
views=article.views,
|
||||||
|
article_order=article.article_order) for article in articles]
|
||||||
|
|
||||||
|
def rebuild(self, articles=None):
|
||||||
|
ArticleDocument.init()
|
||||||
|
articles = articles if articles else Article.objects.all()
|
||||||
|
docs = self.convert_to_doc(articles)
|
||||||
|
for doc in docs:
|
||||||
|
doc.save()
|
||||||
|
|
||||||
|
def update_docs(self, docs):
|
||||||
|
for doc in docs:
|
||||||
|
doc.save()
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from haystack.forms import SearchForm
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class BlogSearchForm(SearchForm):
|
||||||
|
querydata = forms.CharField(required=True)
|
||||||
|
|
||||||
|
def search(self):
|
||||||
|
datas = super(BlogSearchForm, self).search()
|
||||||
|
if not self.is_valid():
|
||||||
|
return self.no_query_found()
|
||||||
|
|
||||||
|
if self.cleaned_data['querydata']:
|
||||||
|
logger.info(self.cleaned_data['querydata'])
|
||||||
|
return datas
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedTimeDocumentManager, \
|
||||||
|
ELASTICSEARCH_ENABLED
|
||||||
|
|
||||||
|
|
||||||
|
# TODO 参数化
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'build search index'
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
if ELASTICSEARCH_ENABLED:
|
||||||
|
ElaspedTimeDocumentManager.build_index()
|
||||||
|
manager = ElapsedTimeDocument()
|
||||||
|
manager.init()
|
||||||
|
manager = ArticleDocumentManager()
|
||||||
|
manager.delete_index()
|
||||||
|
manager.rebuild()
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from blog.models import Tag, Category
|
||||||
|
|
||||||
|
|
||||||
|
# TODO 参数化
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'build search words'
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
datas = set([t.name for t in Tag.objects.all()] +
|
||||||
|
[t.name for t in Category.objects.all()])
|
||||||
|
print('\n'.join(datas))
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from djangoblog.utils import cache
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'clear the whole cache'
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
cache.clear()
|
||||||
|
self.stdout.write(self.style.SUCCESS('Cleared cache\n'))
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.hashers import make_password
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from blog.models import Article, Tag, Category
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'create test datas'
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
user = get_user_model().objects.get_or_create(
|
||||||
|
email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0]
|
||||||
|
|
||||||
|
pcategory = Category.objects.get_or_create(
|
||||||
|
name='我是父类目', parent_category=None)[0]
|
||||||
|
|
||||||
|
category = Category.objects.get_or_create(
|
||||||
|
name='子类目', parent_category=pcategory)[0]
|
||||||
|
|
||||||
|
category.save()
|
||||||
|
basetag = Tag()
|
||||||
|
basetag.name = "标签"
|
||||||
|
basetag.save()
|
||||||
|
for i in range(1, 20):
|
||||||
|
article = Article.objects.get_or_create(
|
||||||
|
category=category,
|
||||||
|
title='nice title ' + str(i),
|
||||||
|
body='nice content ' + str(i),
|
||||||
|
author=user)[0]
|
||||||
|
tag = Tag()
|
||||||
|
tag.name = "标签" + str(i)
|
||||||
|
tag.save()
|
||||||
|
article.tags.add(tag)
|
||||||
|
article.tags.add(basetag)
|
||||||
|
article.save()
|
||||||
|
|
||||||
|
from djangoblog.utils import cache
|
||||||
|
cache.clear()
|
||||||
|
self.stdout.write(self.style.SUCCESS('created test datas \n'))
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from djangoblog.spider_notify import SpiderNotify
|
||||||
|
from djangoblog.utils import get_current_site
|
||||||
|
from blog.models import Article, Tag, Category
|
||||||
|
|
||||||
|
site = get_current_site().domain
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'notify baidu url'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
'data_type',
|
||||||
|
type=str,
|
||||||
|
choices=[
|
||||||
|
'all',
|
||||||
|
'article',
|
||||||
|
'tag',
|
||||||
|
'category'],
|
||||||
|
help='article : all article,tag : all tag,category: all category,all: All of these')
|
||||||
|
|
||||||
|
def get_full_url(self, path):
|
||||||
|
url = "https://{site}{path}".format(site=site, path=path)
|
||||||
|
return url
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
type = options['data_type']
|
||||||
|
self.stdout.write('start get %s' % type)
|
||||||
|
|
||||||
|
urls = []
|
||||||
|
if type == 'article' or type == 'all':
|
||||||
|
for article in Article.objects.filter(status='p'):
|
||||||
|
urls.append(article.get_full_url())
|
||||||
|
if type == 'tag' or type == 'all':
|
||||||
|
for tag in Tag.objects.all():
|
||||||
|
url = tag.get_absolute_url()
|
||||||
|
urls.append(self.get_full_url(url))
|
||||||
|
if type == 'category' or type == 'all':
|
||||||
|
for category in Category.objects.all():
|
||||||
|
url = category.get_absolute_url()
|
||||||
|
urls.append(self.get_full_url(url))
|
||||||
|
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(
|
||||||
|
'start notify %d urls' %
|
||||||
|
len(urls)))
|
||||||
|
SpiderNotify.baidu_notify(urls)
|
||||||
|
self.stdout.write(self.style.SUCCESS('finish notify'))
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
import requests
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.templatetags.static import static
|
||||||
|
|
||||||
|
from djangoblog.utils import save_user_avatar
|
||||||
|
from oauth.models import OAuthUser
|
||||||
|
from oauth.oauthmanager import get_manager_by_type
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'sync user avatar'
|
||||||
|
|
||||||
|
def test_picture(self, url):
|
||||||
|
try:
|
||||||
|
if requests.get(url, timeout=2).status_code == 200:
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
static_url = static("../")
|
||||||
|
users = OAuthUser.objects.all()
|
||||||
|
self.stdout.write(f'开始同步{len(users)}个用户头像')
|
||||||
|
for u in users:
|
||||||
|
self.stdout.write(f'开始同步:{u.nickname}')
|
||||||
|
url = u.picture
|
||||||
|
if url:
|
||||||
|
if url.startswith(static_url):
|
||||||
|
if self.test_picture(url):
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if u.metadata:
|
||||||
|
manage = get_manager_by_type(u.type)
|
||||||
|
url = manage.get_picture(u.metadata)
|
||||||
|
url = save_user_avatar(url)
|
||||||
|
else:
|
||||||
|
url = static('blog/img/avatar.png')
|
||||||
|
else:
|
||||||
|
url = save_user_avatar(url)
|
||||||
|
else:
|
||||||
|
url = static('blog/img/avatar.png')
|
||||||
|
if url:
|
||||||
|
self.stdout.write(
|
||||||
|
f'结束同步:{u.nickname}.url:{url}')
|
||||||
|
u.picture = url
|
||||||
|
u.save()
|
||||||
|
self.stdout.write('结束同步')
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
from ipware import get_client_ip
|
||||||
|
from user_agents import parse
|
||||||
|
|
||||||
|
from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class OnlineMiddleware(object):
|
||||||
|
def __init__(self, get_response=None):
|
||||||
|
self.get_response = get_response
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
''' page render time '''
|
||||||
|
start_time = time.time()
|
||||||
|
response = self.get_response(request)
|
||||||
|
http_user_agent = request.META.get('HTTP_USER_AGENT', '')
|
||||||
|
ip, _ = get_client_ip(request)
|
||||||
|
user_agent = parse(http_user_agent)
|
||||||
|
if not response.streaming:
|
||||||
|
try:
|
||||||
|
cast_time = time.time() - start_time
|
||||||
|
if ELASTICSEARCH_ENABLED:
|
||||||
|
time_taken = round((cast_time) * 1000, 2)
|
||||||
|
url = request.path
|
||||||
|
from django.utils import timezone
|
||||||
|
ElaspedTimeDocumentManager.create(
|
||||||
|
url=url,
|
||||||
|
time_taken=time_taken,
|
||||||
|
log_datetime=timezone.now(),
|
||||||
|
useragent=user_agent,
|
||||||
|
ip=ip)
|
||||||
|
response.content = response.content.replace(
|
||||||
|
b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5]))
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Error OnlineMiddleware: %s" % e)
|
||||||
|
|
||||||
|
return response
|
||||||
@ -0,0 +1,137 @@
|
|||||||
|
# Generated by Django 4.1.7 on 2023-03-02 07:14
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
import mdeditor.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='BlogSettings',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('sitename', models.CharField(default='', max_length=200, verbose_name='网站名称')),
|
||||||
|
('site_description', models.TextField(default='', max_length=1000, verbose_name='网站描述')),
|
||||||
|
('site_seo_description', models.TextField(default='', max_length=1000, verbose_name='网站SEO描述')),
|
||||||
|
('site_keywords', models.TextField(default='', max_length=1000, verbose_name='网站关键字')),
|
||||||
|
('article_sub_length', models.IntegerField(default=300, verbose_name='文章摘要长度')),
|
||||||
|
('sidebar_article_count', models.IntegerField(default=10, verbose_name='侧边栏文章数目')),
|
||||||
|
('sidebar_comment_count', models.IntegerField(default=5, verbose_name='侧边栏评论数目')),
|
||||||
|
('article_comment_count', models.IntegerField(default=5, verbose_name='文章页面默认显示评论数目')),
|
||||||
|
('show_google_adsense', models.BooleanField(default=False, verbose_name='是否显示谷歌广告')),
|
||||||
|
('google_adsense_codes', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='广告内容')),
|
||||||
|
('open_site_comment', models.BooleanField(default=True, verbose_name='是否打开网站评论功能')),
|
||||||
|
('beiancode', models.CharField(blank=True, default='', max_length=2000, null=True, verbose_name='备案号')),
|
||||||
|
('analyticscode', models.TextField(default='', max_length=1000, verbose_name='网站统计代码')),
|
||||||
|
('show_gongan_code', models.BooleanField(default=False, verbose_name='是否显示公安备案号')),
|
||||||
|
('gongan_beiancode', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='公安备案号')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '网站配置',
|
||||||
|
'verbose_name_plural': '网站配置',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Links',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=30, unique=True, verbose_name='链接名称')),
|
||||||
|
('link', models.URLField(verbose_name='链接地址')),
|
||||||
|
('sequence', models.IntegerField(unique=True, verbose_name='排序')),
|
||||||
|
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
|
||||||
|
('show_type', models.CharField(choices=[('i', '首页'), ('l', '列表页'), ('p', '文章页面'), ('a', '全站'), ('s', '友情链接页面')], default='i', max_length=1, verbose_name='显示类型')),
|
||||||
|
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
|
||||||
|
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '友情链接',
|
||||||
|
'verbose_name_plural': '友情链接',
|
||||||
|
'ordering': ['sequence'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SideBar',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100, verbose_name='标题')),
|
||||||
|
('content', models.TextField(verbose_name='内容')),
|
||||||
|
('sequence', models.IntegerField(unique=True, verbose_name='排序')),
|
||||||
|
('is_enable', models.BooleanField(default=True, verbose_name='是否启用')),
|
||||||
|
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
|
||||||
|
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '侧边栏',
|
||||||
|
'verbose_name_plural': '侧边栏',
|
||||||
|
'ordering': ['sequence'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Tag',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||||
|
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
|
||||||
|
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
|
||||||
|
('name', models.CharField(max_length=30, unique=True, verbose_name='标签名')),
|
||||||
|
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '标签',
|
||||||
|
'verbose_name_plural': '标签',
|
||||||
|
'ordering': ['name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Category',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||||
|
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
|
||||||
|
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
|
||||||
|
('name', models.CharField(max_length=30, unique=True, verbose_name='分类名')),
|
||||||
|
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
|
||||||
|
('index', models.IntegerField(default=0, verbose_name='权重排序-越大越靠前')),
|
||||||
|
('parent_category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='父级分类')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '分类',
|
||||||
|
'verbose_name_plural': '分类',
|
||||||
|
'ordering': ['-index'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Article',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||||
|
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
|
||||||
|
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
|
||||||
|
('title', models.CharField(max_length=200, unique=True, verbose_name='标题')),
|
||||||
|
('body', mdeditor.fields.MDTextField(verbose_name='正文')),
|
||||||
|
('pub_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='发布时间')),
|
||||||
|
('status', models.CharField(choices=[('d', '草稿'), ('p', '发表')], default='p', max_length=1, verbose_name='文章状态')),
|
||||||
|
('comment_status', models.CharField(choices=[('o', '打开'), ('c', '关闭')], default='o', max_length=1, verbose_name='评论状态')),
|
||||||
|
('type', models.CharField(choices=[('a', '文章'), ('p', '页面')], default='a', max_length=1, verbose_name='类型')),
|
||||||
|
('views', models.PositiveIntegerField(default=0, verbose_name='浏览量')),
|
||||||
|
('article_order', models.IntegerField(default=0, verbose_name='排序,数字越大越靠前')),
|
||||||
|
('show_toc', models.BooleanField(default=False, verbose_name='是否显示toc目录')),
|
||||||
|
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
|
||||||
|
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='分类')),
|
||||||
|
('tags', models.ManyToManyField(blank=True, to='blog.tag', verbose_name='标签集合')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '文章',
|
||||||
|
'verbose_name_plural': '文章',
|
||||||
|
'ordering': ['-article_order', '-pub_time'],
|
||||||
|
'get_latest_by': 'id',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 4.1.7 on 2023-03-29 06:08
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('blog', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='blogsettings',
|
||||||
|
name='global_footer',
|
||||||
|
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='blogsettings',
|
||||||
|
name='global_header',
|
||||||
|
field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 4.2.1 on 2023-05-09 07:45
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('blog', '0002_blogsettings_global_footer_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='blogsettings',
|
||||||
|
name='comment_need_review',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='评论是否需要审核'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
# Generated by Django 4.2.1 on 2023-05-09 07:51
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('blog', '0003_blogsettings_comment_need_review'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='blogsettings',
|
||||||
|
old_name='analyticscode',
|
||||||
|
new_name='analytics_code',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='blogsettings',
|
||||||
|
old_name='beiancode',
|
||||||
|
new_name='beian_code',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='blogsettings',
|
||||||
|
old_name='sitename',
|
||||||
|
new_name='site_name',
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,300 @@
|
|||||||
|
# Generated by Django 4.2.5 on 2023-09-06 13:13
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
import mdeditor.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='article',
|
||||||
|
options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='category',
|
||||||
|
options={'ordering': ['-index'], 'verbose_name': 'category', 'verbose_name_plural': 'category'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='links',
|
||||||
|
options={'ordering': ['sequence'], 'verbose_name': 'link', 'verbose_name_plural': 'link'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='sidebar',
|
||||||
|
options={'ordering': ['sequence'], 'verbose_name': 'sidebar', 'verbose_name_plural': 'sidebar'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='tag',
|
||||||
|
options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'},
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='article',
|
||||||
|
name='created_time',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='article',
|
||||||
|
name='last_mod_time',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='category',
|
||||||
|
name='created_time',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='category',
|
||||||
|
name='last_mod_time',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='links',
|
||||||
|
name='created_time',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='sidebar',
|
||||||
|
name='created_time',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='tag',
|
||||||
|
name='created_time',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='tag',
|
||||||
|
name='last_mod_time',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='article',
|
||||||
|
name='creation_time',
|
||||||
|
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='article',
|
||||||
|
name='last_modify_time',
|
||||||
|
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='category',
|
||||||
|
name='creation_time',
|
||||||
|
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='category',
|
||||||
|
name='last_modify_time',
|
||||||
|
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='links',
|
||||||
|
name='creation_time',
|
||||||
|
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sidebar',
|
||||||
|
name='creation_time',
|
||||||
|
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='tag',
|
||||||
|
name='creation_time',
|
||||||
|
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='tag',
|
||||||
|
name='last_modify_time',
|
||||||
|
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='article',
|
||||||
|
name='article_order',
|
||||||
|
field=models.IntegerField(default=0, verbose_name='order'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='article',
|
||||||
|
name='author',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='article',
|
||||||
|
name='body',
|
||||||
|
field=mdeditor.fields.MDTextField(verbose_name='body'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='article',
|
||||||
|
name='category',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='category'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='article',
|
||||||
|
name='comment_status',
|
||||||
|
field=models.CharField(choices=[('o', 'Open'), ('c', 'Close')], default='o', max_length=1, verbose_name='comment status'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='article',
|
||||||
|
name='pub_time',
|
||||||
|
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='publish time'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='article',
|
||||||
|
name='show_toc',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='show toc'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='article',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(choices=[('d', 'Draft'), ('p', 'Published')], default='p', max_length=1, verbose_name='status'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='article',
|
||||||
|
name='tags',
|
||||||
|
field=models.ManyToManyField(blank=True, to='blog.tag', verbose_name='tag'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='article',
|
||||||
|
name='title',
|
||||||
|
field=models.CharField(max_length=200, unique=True, verbose_name='title'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='article',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('a', 'Article'), ('p', 'Page')], default='a', max_length=1, verbose_name='type'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='article',
|
||||||
|
name='views',
|
||||||
|
field=models.PositiveIntegerField(default=0, verbose_name='views'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='blogsettings',
|
||||||
|
name='article_comment_count',
|
||||||
|
field=models.IntegerField(default=5, verbose_name='article comment count'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='blogsettings',
|
||||||
|
name='article_sub_length',
|
||||||
|
field=models.IntegerField(default=300, verbose_name='article sub length'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='blogsettings',
|
||||||
|
name='google_adsense_codes',
|
||||||
|
field=models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='adsense code'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='blogsettings',
|
||||||
|
name='open_site_comment',
|
||||||
|
field=models.BooleanField(default=True, verbose_name='open site comment'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='blogsettings',
|
||||||
|
name='show_google_adsense',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='show adsense'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='blogsettings',
|
||||||
|
name='sidebar_article_count',
|
||||||
|
field=models.IntegerField(default=10, verbose_name='sidebar article count'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='blogsettings',
|
||||||
|
name='sidebar_comment_count',
|
||||||
|
field=models.IntegerField(default=5, verbose_name='sidebar comment count'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='blogsettings',
|
||||||
|
name='site_description',
|
||||||
|
field=models.TextField(default='', max_length=1000, verbose_name='site description'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='blogsettings',
|
||||||
|
name='site_keywords',
|
||||||
|
field=models.TextField(default='', max_length=1000, verbose_name='site keywords'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='blogsettings',
|
||||||
|
name='site_name',
|
||||||
|
field=models.CharField(default='', max_length=200, verbose_name='site name'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='blogsettings',
|
||||||
|
name='site_seo_description',
|
||||||
|
field=models.TextField(default='', max_length=1000, verbose_name='site seo description'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='category',
|
||||||
|
name='index',
|
||||||
|
field=models.IntegerField(default=0, verbose_name='index'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='category',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=30, unique=True, verbose_name='category name'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='category',
|
||||||
|
name='parent_category',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='parent category'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='links',
|
||||||
|
name='is_enable',
|
||||||
|
field=models.BooleanField(default=True, verbose_name='is show'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='links',
|
||||||
|
name='last_mod_time',
|
||||||
|
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='links',
|
||||||
|
name='link',
|
||||||
|
field=models.URLField(verbose_name='link'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='links',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=30, unique=True, verbose_name='link name'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='links',
|
||||||
|
name='sequence',
|
||||||
|
field=models.IntegerField(unique=True, verbose_name='order'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='links',
|
||||||
|
name='show_type',
|
||||||
|
field=models.CharField(choices=[('i', 'index'), ('l', 'list'), ('p', 'post'), ('a', 'all'), ('s', 'slide')], default='i', max_length=1, verbose_name='show type'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sidebar',
|
||||||
|
name='content',
|
||||||
|
field=models.TextField(verbose_name='content'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sidebar',
|
||||||
|
name='is_enable',
|
||||||
|
field=models.BooleanField(default=True, verbose_name='is enable'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sidebar',
|
||||||
|
name='last_mod_time',
|
||||||
|
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sidebar',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=100, verbose_name='title'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='sidebar',
|
||||||
|
name='sequence',
|
||||||
|
field=models.IntegerField(unique=True, verbose_name='order'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='tag',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=30, unique=True, verbose_name='tag name'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 4.2.7 on 2024-01-26 02:41
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('blog', '0005_alter_article_options_alter_category_options_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='blogsettings',
|
||||||
|
options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'},
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,376 @@
|
|||||||
|
import logging
|
||||||
|
import re
|
||||||
|
from abc import abstractmethod
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from mdeditor.fields import MDTextField
|
||||||
|
from uuslug import slugify
|
||||||
|
|
||||||
|
from djangoblog.utils import cache_decorator, cache
|
||||||
|
from djangoblog.utils import get_current_site
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class LinkShowType(models.TextChoices):
|
||||||
|
I = ('i', _('index'))
|
||||||
|
L = ('l', _('list'))
|
||||||
|
P = ('p', _('post'))
|
||||||
|
A = ('a', _('all'))
|
||||||
|
S = ('s', _('slide'))
|
||||||
|
|
||||||
|
|
||||||
|
class BaseModel(models.Model):
|
||||||
|
id = models.AutoField(primary_key=True)
|
||||||
|
creation_time = models.DateTimeField(_('creation time'), default=now)
|
||||||
|
last_modify_time = models.DateTimeField(_('modify time'), default=now)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
is_update_views = isinstance(
|
||||||
|
self,
|
||||||
|
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
|
||||||
|
if is_update_views:
|
||||||
|
Article.objects.filter(pk=self.pk).update(views=self.views)
|
||||||
|
else:
|
||||||
|
if 'slug' in self.__dict__:
|
||||||
|
slug = getattr(
|
||||||
|
self, 'title') if 'title' in self.__dict__ else getattr(
|
||||||
|
self, 'name')
|
||||||
|
setattr(self, 'slug', slugify(slug))
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def get_full_url(self):
|
||||||
|
site = get_current_site().domain
|
||||||
|
url = "https://{site}{path}".format(site=site,
|
||||||
|
path=self.get_absolute_url())
|
||||||
|
return url
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_absolute_url(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Article(BaseModel):
|
||||||
|
"""文章"""
|
||||||
|
STATUS_CHOICES = (
|
||||||
|
('d', _('Draft')),
|
||||||
|
('p', _('Published')),
|
||||||
|
)
|
||||||
|
COMMENT_STATUS = (
|
||||||
|
('o', _('Open')),
|
||||||
|
('c', _('Close')),
|
||||||
|
)
|
||||||
|
TYPE = (
|
||||||
|
('a', _('Article')),
|
||||||
|
('p', _('Page')),
|
||||||
|
)
|
||||||
|
title = models.CharField(_('title'), max_length=200, unique=True)
|
||||||
|
body = MDTextField(_('body'))
|
||||||
|
pub_time = models.DateTimeField(
|
||||||
|
_('publish time'), blank=False, null=False, default=now)
|
||||||
|
status = models.CharField(
|
||||||
|
_('status'),
|
||||||
|
max_length=1,
|
||||||
|
choices=STATUS_CHOICES,
|
||||||
|
default='p')
|
||||||
|
comment_status = models.CharField(
|
||||||
|
_('comment status'),
|
||||||
|
max_length=1,
|
||||||
|
choices=COMMENT_STATUS,
|
||||||
|
default='o')
|
||||||
|
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
|
||||||
|
views = models.PositiveIntegerField(_('views'), default=0)
|
||||||
|
author = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
verbose_name=_('author'),
|
||||||
|
blank=False,
|
||||||
|
null=False,
|
||||||
|
on_delete=models.CASCADE)
|
||||||
|
article_order = models.IntegerField(
|
||||||
|
_('order'), blank=False, null=False, default=0)
|
||||||
|
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False)
|
||||||
|
category = models.ForeignKey(
|
||||||
|
'Category',
|
||||||
|
verbose_name=_('category'),
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
blank=False,
|
||||||
|
null=False)
|
||||||
|
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True)
|
||||||
|
|
||||||
|
def body_to_string(self):
|
||||||
|
return self.body
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['-article_order', '-pub_time']
|
||||||
|
verbose_name = _('article')
|
||||||
|
verbose_name_plural = verbose_name
|
||||||
|
get_latest_by = 'id'
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('blog:detailbyid', kwargs={
|
||||||
|
'article_id': self.id,
|
||||||
|
'year': self.creation_time.year,
|
||||||
|
'month': self.creation_time.month,
|
||||||
|
'day': self.creation_time.day
|
||||||
|
})
|
||||||
|
|
||||||
|
@cache_decorator(60 * 60 * 10)
|
||||||
|
def get_category_tree(self):
|
||||||
|
tree = self.category.get_category_tree()
|
||||||
|
names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))
|
||||||
|
|
||||||
|
return names
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def viewed(self):
|
||||||
|
self.views += 1
|
||||||
|
self.save(update_fields=['views'])
|
||||||
|
|
||||||
|
def comment_list(self):
|
||||||
|
cache_key = 'article_comments_{id}'.format(id=self.id)
|
||||||
|
value = cache.get(cache_key)
|
||||||
|
if value:
|
||||||
|
logger.info('get article comments:{id}'.format(id=self.id))
|
||||||
|
return value
|
||||||
|
else:
|
||||||
|
comments = self.comment_set.filter(is_enable=True).order_by('-id')
|
||||||
|
cache.set(cache_key, comments, 60 * 100)
|
||||||
|
logger.info('set article comments:{id}'.format(id=self.id))
|
||||||
|
return comments
|
||||||
|
|
||||||
|
def get_admin_url(self):
|
||||||
|
info = (self._meta.app_label, self._meta.model_name)
|
||||||
|
return reverse('admin:%s_%s_change' % info, args=(self.pk,))
|
||||||
|
|
||||||
|
@cache_decorator(expiration=60 * 100)
|
||||||
|
def next_article(self):
|
||||||
|
# 下一篇
|
||||||
|
return Article.objects.filter(
|
||||||
|
id__gt=self.id, status='p').order_by('id').first()
|
||||||
|
|
||||||
|
@cache_decorator(expiration=60 * 100)
|
||||||
|
def prev_article(self):
|
||||||
|
# 前一篇
|
||||||
|
return Article.objects.filter(id__lt=self.id, status='p').first()
|
||||||
|
|
||||||
|
def get_first_image_url(self):
|
||||||
|
"""
|
||||||
|
Get the first image url from article.body.
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
|
||||||
|
if match:
|
||||||
|
return match.group(1)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
class Category(BaseModel):
|
||||||
|
"""文章分类"""
|
||||||
|
name = models.CharField(_('category name'), max_length=30, unique=True)
|
||||||
|
parent_category = models.ForeignKey(
|
||||||
|
'self',
|
||||||
|
verbose_name=_('parent category'),
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=models.CASCADE)
|
||||||
|
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
|
||||||
|
index = models.IntegerField(default=0, verbose_name=_('index'))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['-index']
|
||||||
|
verbose_name = _('category')
|
||||||
|
verbose_name_plural = verbose_name
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse(
|
||||||
|
'blog:category_detail', kwargs={
|
||||||
|
'category_name': self.slug})
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
@cache_decorator(60 * 60 * 10)
|
||||||
|
def get_category_tree(self):
|
||||||
|
"""
|
||||||
|
递归获得分类目录的父级
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
categorys = []
|
||||||
|
|
||||||
|
def parse(category):
|
||||||
|
categorys.append(category)
|
||||||
|
if category.parent_category:
|
||||||
|
parse(category.parent_category)
|
||||||
|
|
||||||
|
parse(self)
|
||||||
|
return categorys
|
||||||
|
|
||||||
|
@cache_decorator(60 * 60 * 10)
|
||||||
|
def get_sub_categorys(self):
|
||||||
|
"""
|
||||||
|
获得当前分类目录所有子集
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
categorys = []
|
||||||
|
all_categorys = Category.objects.all()
|
||||||
|
|
||||||
|
def parse(category):
|
||||||
|
if category not in categorys:
|
||||||
|
categorys.append(category)
|
||||||
|
childs = all_categorys.filter(parent_category=category)
|
||||||
|
for child in childs:
|
||||||
|
if category not in categorys:
|
||||||
|
categorys.append(child)
|
||||||
|
parse(child)
|
||||||
|
|
||||||
|
parse(self)
|
||||||
|
return categorys
|
||||||
|
|
||||||
|
|
||||||
|
class Tag(BaseModel):
|
||||||
|
"""文章标签"""
|
||||||
|
name = models.CharField(_('tag name'), max_length=30, unique=True)
|
||||||
|
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
|
||||||
|
|
||||||
|
@cache_decorator(60 * 60 * 10)
|
||||||
|
def get_article_count(self):
|
||||||
|
return Article.objects.filter(tags__name=self.name).distinct().count()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['name']
|
||||||
|
verbose_name = _('tag')
|
||||||
|
verbose_name_plural = verbose_name
|
||||||
|
|
||||||
|
|
||||||
|
class Links(models.Model):
|
||||||
|
"""友情链接"""
|
||||||
|
|
||||||
|
name = models.CharField(_('link name'), max_length=30, unique=True)
|
||||||
|
link = models.URLField(_('link'))
|
||||||
|
sequence = models.IntegerField(_('order'), unique=True)
|
||||||
|
is_enable = models.BooleanField(
|
||||||
|
_('is show'), default=True, blank=False, null=False)
|
||||||
|
show_type = models.CharField(
|
||||||
|
_('show type'),
|
||||||
|
max_length=1,
|
||||||
|
choices=LinkShowType.choices,
|
||||||
|
default=LinkShowType.I)
|
||||||
|
creation_time = models.DateTimeField(_('creation time'), default=now)
|
||||||
|
last_mod_time = models.DateTimeField(_('modify time'), default=now)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['sequence']
|
||||||
|
verbose_name = _('link')
|
||||||
|
verbose_name_plural = verbose_name
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class SideBar(models.Model):
|
||||||
|
"""侧边栏,可以展示一些html内容"""
|
||||||
|
name = models.CharField(_('title'), max_length=100)
|
||||||
|
content = models.TextField(_('content'))
|
||||||
|
sequence = models.IntegerField(_('order'), unique=True)
|
||||||
|
is_enable = models.BooleanField(_('is enable'), default=True)
|
||||||
|
creation_time = models.DateTimeField(_('creation time'), default=now)
|
||||||
|
last_mod_time = models.DateTimeField(_('modify time'), default=now)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['sequence']
|
||||||
|
verbose_name = _('sidebar')
|
||||||
|
verbose_name_plural = verbose_name
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class BlogSettings(models.Model):
|
||||||
|
"""blog的配置"""
|
||||||
|
site_name = models.CharField(
|
||||||
|
_('site name'),
|
||||||
|
max_length=200,
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
default='')
|
||||||
|
site_description = models.TextField(
|
||||||
|
_('site description'),
|
||||||
|
max_length=1000,
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
default='')
|
||||||
|
site_seo_description = models.TextField(
|
||||||
|
_('site seo description'), max_length=1000, null=False, blank=False, default='')
|
||||||
|
site_keywords = models.TextField(
|
||||||
|
_('site keywords'),
|
||||||
|
max_length=1000,
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
default='')
|
||||||
|
article_sub_length = models.IntegerField(_('article sub length'), default=300)
|
||||||
|
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10)
|
||||||
|
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5)
|
||||||
|
article_comment_count = models.IntegerField(_('article comment count'), default=5)
|
||||||
|
show_google_adsense = models.BooleanField(_('show adsense'), default=False)
|
||||||
|
google_adsense_codes = models.TextField(
|
||||||
|
_('adsense code'), max_length=2000, null=True, blank=True, default='')
|
||||||
|
open_site_comment = models.BooleanField(_('open site comment'), default=True)
|
||||||
|
global_header = models.TextField("公共头部", null=True, blank=True, default='')
|
||||||
|
global_footer = models.TextField("公共尾部", null=True, blank=True, default='')
|
||||||
|
beian_code = models.CharField(
|
||||||
|
'备案号',
|
||||||
|
max_length=2000,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
default='')
|
||||||
|
analytics_code = models.TextField(
|
||||||
|
"网站统计代码",
|
||||||
|
max_length=1000,
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
default='')
|
||||||
|
show_gongan_code = models.BooleanField(
|
||||||
|
'是否显示公安备案号', default=False, null=False)
|
||||||
|
gongan_beiancode = models.TextField(
|
||||||
|
'公安备案号',
|
||||||
|
max_length=2000,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
default='')
|
||||||
|
comment_need_review = models.BooleanField(
|
||||||
|
'评论是否需要审核', default=False, null=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Website configuration')
|
||||||
|
verbose_name_plural = verbose_name
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.site_name
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
if BlogSettings.objects.exclude(id=self.id).count():
|
||||||
|
raise ValidationError(_('There can only be one configuration'))
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
from djangoblog.utils import cache
|
||||||
|
cache.clear()
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
from haystack import indexes
|
||||||
|
|
||||||
|
from blog.models import Article
|
||||||
|
|
||||||
|
|
||||||
|
class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
|
||||||
|
text = indexes.CharField(document=True, use_template=True)
|
||||||
|
|
||||||
|
def get_model(self):
|
||||||
|
return Article
|
||||||
|
|
||||||
|
def index_queryset(self, using=None):
|
||||||
|
return self.get_model().objects.filter(status='p')
|
||||||
@ -0,0 +1,232 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
|
from django.core.management import call_command
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.templatetags.static import static
|
||||||
|
from django.test import Client, RequestFactory, TestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from accounts.models import BlogUser
|
||||||
|
from blog.forms import BlogSearchForm
|
||||||
|
from blog.models import Article, Category, Tag, SideBar, Links
|
||||||
|
from blog.templatetags.blog_tags import load_pagination_info, load_articletags
|
||||||
|
from djangoblog.utils import get_current_site, get_sha256
|
||||||
|
from oauth.models import OAuthUser, OAuthConfig
|
||||||
|
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
|
|
||||||
|
class ArticleTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.client = Client()
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
|
||||||
|
def test_validate_article(self):
|
||||||
|
site = get_current_site().domain
|
||||||
|
user = BlogUser.objects.get_or_create(
|
||||||
|
email="liangliangyy@gmail.com",
|
||||||
|
username="liangliangyy")[0]
|
||||||
|
user.set_password("liangliangyy")
|
||||||
|
user.is_staff = True
|
||||||
|
user.is_superuser = True
|
||||||
|
user.save()
|
||||||
|
response = self.client.get(user.get_absolute_url())
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
response = self.client.get('/admin/servermanager/emailsendlog/')
|
||||||
|
response = self.client.get('admin/admin/logentry/')
|
||||||
|
s = SideBar()
|
||||||
|
s.sequence = 1
|
||||||
|
s.name = 'test'
|
||||||
|
s.content = 'test content'
|
||||||
|
s.is_enable = True
|
||||||
|
s.save()
|
||||||
|
|
||||||
|
category = Category()
|
||||||
|
category.name = "category"
|
||||||
|
category.creation_time = timezone.now()
|
||||||
|
category.last_mod_time = timezone.now()
|
||||||
|
category.save()
|
||||||
|
|
||||||
|
tag = Tag()
|
||||||
|
tag.name = "nicetag"
|
||||||
|
tag.save()
|
||||||
|
|
||||||
|
article = Article()
|
||||||
|
article.title = "nicetitle"
|
||||||
|
article.body = "nicecontent"
|
||||||
|
article.author = user
|
||||||
|
article.category = category
|
||||||
|
article.type = 'a'
|
||||||
|
article.status = 'p'
|
||||||
|
|
||||||
|
article.save()
|
||||||
|
self.assertEqual(0, article.tags.count())
|
||||||
|
article.tags.add(tag)
|
||||||
|
article.save()
|
||||||
|
self.assertEqual(1, article.tags.count())
|
||||||
|
|
||||||
|
for i in range(20):
|
||||||
|
article = Article()
|
||||||
|
article.title = "nicetitle" + str(i)
|
||||||
|
article.body = "nicetitle" + str(i)
|
||||||
|
article.author = user
|
||||||
|
article.category = category
|
||||||
|
article.type = 'a'
|
||||||
|
article.status = 'p'
|
||||||
|
article.save()
|
||||||
|
article.tags.add(tag)
|
||||||
|
article.save()
|
||||||
|
from blog.documents import ELASTICSEARCH_ENABLED
|
||||||
|
if ELASTICSEARCH_ENABLED:
|
||||||
|
call_command("build_index")
|
||||||
|
response = self.client.get('/search', {'q': 'nicetitle'})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.get(article.get_absolute_url())
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
from djangoblog.spider_notify import SpiderNotify
|
||||||
|
SpiderNotify.notify(article.get_absolute_url())
|
||||||
|
response = self.client.get(tag.get_absolute_url())
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.get(category.get_absolute_url())
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.get('/search', {'q': 'django'})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
s = load_articletags(article)
|
||||||
|
self.assertIsNotNone(s)
|
||||||
|
|
||||||
|
self.client.login(username='liangliangyy', password='liangliangyy')
|
||||||
|
|
||||||
|
response = self.client.get(reverse('blog:archives'))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
p = Paginator(Article.objects.all(), settings.PAGINATE_BY)
|
||||||
|
self.check_pagination(p, '', '')
|
||||||
|
|
||||||
|
p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY)
|
||||||
|
self.check_pagination(p, '分类标签归档', tag.slug)
|
||||||
|
|
||||||
|
p = Paginator(
|
||||||
|
Article.objects.filter(
|
||||||
|
author__username='liangliangyy'), settings.PAGINATE_BY)
|
||||||
|
self.check_pagination(p, '作者文章归档', 'liangliangyy')
|
||||||
|
|
||||||
|
p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY)
|
||||||
|
self.check_pagination(p, '分类目录归档', category.slug)
|
||||||
|
|
||||||
|
f = BlogSearchForm()
|
||||||
|
f.search()
|
||||||
|
# self.client.login(username='liangliangyy', password='liangliangyy')
|
||||||
|
from djangoblog.spider_notify import SpiderNotify
|
||||||
|
SpiderNotify.baidu_notify([article.get_full_url()])
|
||||||
|
|
||||||
|
from blog.templatetags.blog_tags import gravatar_url, gravatar
|
||||||
|
u = gravatar_url('liangliangyy@gmail.com')
|
||||||
|
u = gravatar('liangliangyy@gmail.com')
|
||||||
|
|
||||||
|
link = Links(
|
||||||
|
sequence=1,
|
||||||
|
name="lylinux",
|
||||||
|
link='https://wwww.lylinux.net')
|
||||||
|
link.save()
|
||||||
|
response = self.client.get('/links.html')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.get('/feed/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.get('/sitemap.xml')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
self.client.get("/admin/blog/article/1/delete/")
|
||||||
|
self.client.get('/admin/servermanager/emailsendlog/')
|
||||||
|
self.client.get('/admin/admin/logentry/')
|
||||||
|
self.client.get('/admin/admin/logentry/1/change/')
|
||||||
|
|
||||||
|
def check_pagination(self, p, type, value):
|
||||||
|
for page in range(1, p.num_pages + 1):
|
||||||
|
s = load_pagination_info(p.page(page), type, value)
|
||||||
|
self.assertIsNotNone(s)
|
||||||
|
if s['previous_url']:
|
||||||
|
response = self.client.get(s['previous_url'])
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
if s['next_url']:
|
||||||
|
response = self.client.get(s['next_url'])
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_image(self):
|
||||||
|
import requests
|
||||||
|
rsp = requests.get(
|
||||||
|
'https://www.python.org/static/img/python-logo.png')
|
||||||
|
imagepath = os.path.join(settings.BASE_DIR, 'python.png')
|
||||||
|
with open(imagepath, 'wb') as file:
|
||||||
|
file.write(rsp.content)
|
||||||
|
rsp = self.client.post('/upload')
|
||||||
|
self.assertEqual(rsp.status_code, 403)
|
||||||
|
sign = get_sha256(get_sha256(settings.SECRET_KEY))
|
||||||
|
with open(imagepath, 'rb') as file:
|
||||||
|
imgfile = SimpleUploadedFile(
|
||||||
|
'python.png', file.read(), content_type='image/jpg')
|
||||||
|
form_data = {'python.png': imgfile}
|
||||||
|
rsp = self.client.post(
|
||||||
|
'/upload?sign=' + sign, form_data, follow=True)
|
||||||
|
self.assertEqual(rsp.status_code, 200)
|
||||||
|
os.remove(imagepath)
|
||||||
|
from djangoblog.utils import save_user_avatar, send_email
|
||||||
|
send_email(['qq@qq.com'], 'testTitle', 'testContent')
|
||||||
|
save_user_avatar(
|
||||||
|
'https://www.python.org/static/img/python-logo.png')
|
||||||
|
|
||||||
|
def test_errorpage(self):
|
||||||
|
rsp = self.client.get('/eee')
|
||||||
|
self.assertEqual(rsp.status_code, 404)
|
||||||
|
|
||||||
|
def test_commands(self):
|
||||||
|
user = BlogUser.objects.get_or_create(
|
||||||
|
email="liangliangyy@gmail.com",
|
||||||
|
username="liangliangyy")[0]
|
||||||
|
user.set_password("liangliangyy")
|
||||||
|
user.is_staff = True
|
||||||
|
user.is_superuser = True
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
c = OAuthConfig()
|
||||||
|
c.type = 'qq'
|
||||||
|
c.appkey = 'appkey'
|
||||||
|
c.appsecret = 'appsecret'
|
||||||
|
c.save()
|
||||||
|
|
||||||
|
u = OAuthUser()
|
||||||
|
u.type = 'qq'
|
||||||
|
u.openid = 'openid'
|
||||||
|
u.user = user
|
||||||
|
u.picture = static("/blog/img/avatar.png")
|
||||||
|
u.metadata = '''
|
||||||
|
{
|
||||||
|
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
|
||||||
|
}'''
|
||||||
|
u.save()
|
||||||
|
|
||||||
|
u = OAuthUser()
|
||||||
|
u.type = 'qq'
|
||||||
|
u.openid = 'openid1'
|
||||||
|
u.picture = 'https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30'
|
||||||
|
u.metadata = '''
|
||||||
|
{
|
||||||
|
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
|
||||||
|
}'''
|
||||||
|
u.save()
|
||||||
|
|
||||||
|
from blog.documents import ELASTICSEARCH_ENABLED
|
||||||
|
if ELASTICSEARCH_ENABLED:
|
||||||
|
call_command("build_index")
|
||||||
|
call_command("ping_baidu", "all")
|
||||||
|
call_command("create_testdata")
|
||||||
|
call_command("clear_cache")
|
||||||
|
call_command("sync_user_avatar")
|
||||||
|
call_command("build_search_words")
|
||||||
@ -0,0 +1,62 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from django.views.decorators.cache import cache_page
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = "blog"
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
r'',
|
||||||
|
views.IndexView.as_view(),
|
||||||
|
name='index'),
|
||||||
|
path(
|
||||||
|
r'page/<int:page>/',
|
||||||
|
views.IndexView.as_view(),
|
||||||
|
name='index_page'),
|
||||||
|
path(
|
||||||
|
r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html',
|
||||||
|
views.ArticleDetailView.as_view(),
|
||||||
|
name='detailbyid'),
|
||||||
|
path(
|
||||||
|
r'category/<slug:category_name>.html',
|
||||||
|
views.CategoryDetailView.as_view(),
|
||||||
|
name='category_detail'),
|
||||||
|
path(
|
||||||
|
r'category/<slug:category_name>/<int:page>.html',
|
||||||
|
views.CategoryDetailView.as_view(),
|
||||||
|
name='category_detail_page'),
|
||||||
|
path(
|
||||||
|
r'author/<author_name>.html',
|
||||||
|
views.AuthorDetailView.as_view(),
|
||||||
|
name='author_detail'),
|
||||||
|
path(
|
||||||
|
r'author/<author_name>/<int:page>.html',
|
||||||
|
views.AuthorDetailView.as_view(),
|
||||||
|
name='author_detail_page'),
|
||||||
|
path(
|
||||||
|
r'tag/<slug:tag_name>.html',
|
||||||
|
views.TagDetailView.as_view(),
|
||||||
|
name='tag_detail'),
|
||||||
|
path(
|
||||||
|
r'tag/<slug:tag_name>/<int:page>.html',
|
||||||
|
views.TagDetailView.as_view(),
|
||||||
|
name='tag_detail_page'),
|
||||||
|
path(
|
||||||
|
'archives.html',
|
||||||
|
cache_page(
|
||||||
|
60 * 60)(
|
||||||
|
views.ArchivesView.as_view()),
|
||||||
|
name='archives'),
|
||||||
|
path(
|
||||||
|
'links.html',
|
||||||
|
views.LinkListView.as_view(),
|
||||||
|
name='links'),
|
||||||
|
path(
|
||||||
|
r'upload',
|
||||||
|
views.fileupload,
|
||||||
|
name='upload'),
|
||||||
|
path(
|
||||||
|
r'clean',
|
||||||
|
views.clean_cache_view,
|
||||||
|
name='clean'),
|
||||||
|
]
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.html import format_html
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
def disable_commentstatus(modeladmin, request, queryset):
|
||||||
|
queryset.update(is_enable=False)
|
||||||
|
|
||||||
|
|
||||||
|
def enable_commentstatus(modeladmin, request, queryset):
|
||||||
|
queryset.update(is_enable=True)
|
||||||
|
|
||||||
|
|
||||||
|
disable_commentstatus.short_description = _('Disable comments')
|
||||||
|
enable_commentstatus.short_description = _('Enable comments')
|
||||||
|
|
||||||
|
|
||||||
|
class CommentAdmin(admin.ModelAdmin):
|
||||||
|
list_per_page = 20
|
||||||
|
list_display = (
|
||||||
|
'id',
|
||||||
|
'body',
|
||||||
|
'link_to_userinfo',
|
||||||
|
'link_to_article',
|
||||||
|
'is_enable',
|
||||||
|
'creation_time')
|
||||||
|
list_display_links = ('id', 'body', 'is_enable')
|
||||||
|
list_filter = ('is_enable',)
|
||||||
|
exclude = ('creation_time', 'last_modify_time')
|
||||||
|
actions = [disable_commentstatus, enable_commentstatus]
|
||||||
|
|
||||||
|
def link_to_userinfo(self, obj):
|
||||||
|
info = (obj.author._meta.app_label, obj.author._meta.model_name)
|
||||||
|
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
|
||||||
|
return format_html(
|
||||||
|
u'<a href="%s">%s</a>' %
|
||||||
|
(link, obj.author.nickname if obj.author.nickname else obj.author.email))
|
||||||
|
|
||||||
|
def link_to_article(self, obj):
|
||||||
|
info = (obj.article._meta.app_label, obj.article._meta.model_name)
|
||||||
|
link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,))
|
||||||
|
return format_html(
|
||||||
|
u'<a href="%s">%s</a>' % (link, obj.article.title))
|
||||||
|
|
||||||
|
link_to_userinfo.short_description = _('User')
|
||||||
|
link_to_article.short_description = _('Article')
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class CommentsConfig(AppConfig):
|
||||||
|
name = 'comments'
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
from django import forms
|
||||||
|
from django.forms import ModelForm
|
||||||
|
|
||||||
|
from .models import Comment
|
||||||
|
|
||||||
|
|
||||||
|
class CommentForm(ModelForm):
|
||||||
|
parent_comment_id = forms.IntegerField(
|
||||||
|
widget=forms.HiddenInput, required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Comment
|
||||||
|
fields = ['body']
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
# Generated by Django 4.1.7 on 2023-03-02 07:14
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('blog', '0001_initial'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Comment',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('body', models.TextField(max_length=300, verbose_name='正文')),
|
||||||
|
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
|
||||||
|
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
|
||||||
|
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
|
||||||
|
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='文章')),
|
||||||
|
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
|
||||||
|
('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '评论',
|
||||||
|
'verbose_name_plural': '评论',
|
||||||
|
'ordering': ['-id'],
|
||||||
|
'get_latest_by': 'id',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.1.7 on 2023-04-24 13:48
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('comments', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='comment',
|
||||||
|
name='is_enable',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='是否显示'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
# Generated by Django 4.2.5 on 2023-09-06 13:13
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('blog', '0005_alter_article_options_alter_category_options_and_more'),
|
||||||
|
('comments', '0002_alter_comment_is_enable'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='comment',
|
||||||
|
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'comment', 'verbose_name_plural': 'comment'},
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='comment',
|
||||||
|
name='created_time',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='comment',
|
||||||
|
name='last_mod_time',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='comment',
|
||||||
|
name='creation_time',
|
||||||
|
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='comment',
|
||||||
|
name='last_modify_time',
|
||||||
|
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='comment',
|
||||||
|
name='article',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='comment',
|
||||||
|
name='author',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='comment',
|
||||||
|
name='is_enable',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='enable'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='comment',
|
||||||
|
name='parent_comment',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='parent comment'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from blog.models import Article
|
||||||
|
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
|
|
||||||
|
class Comment(models.Model):
|
||||||
|
body = models.TextField('正文', max_length=300)
|
||||||
|
creation_time = models.DateTimeField(_('creation time'), default=now)
|
||||||
|
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
|
||||||
|
author = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
verbose_name=_('author'),
|
||||||
|
on_delete=models.CASCADE)
|
||||||
|
article = models.ForeignKey(
|
||||||
|
Article,
|
||||||
|
verbose_name=_('article'),
|
||||||
|
on_delete=models.CASCADE)
|
||||||
|
parent_comment = models.ForeignKey(
|
||||||
|
'self',
|
||||||
|
verbose_name=_('parent comment'),
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=models.CASCADE)
|
||||||
|
is_enable = models.BooleanField(_('enable'),
|
||||||
|
default=False, blank=False, null=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['-id']
|
||||||
|
verbose_name = _('comment')
|
||||||
|
verbose_name_plural = verbose_name
|
||||||
|
get_latest_by = 'id'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.body
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
from django import template
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def parse_commenttree(commentlist, comment):
|
||||||
|
"""获得当前评论子评论的列表
|
||||||
|
用法: {% parse_commenttree article_comments comment as childcomments %}
|
||||||
|
"""
|
||||||
|
datas = []
|
||||||
|
|
||||||
|
def parse(c):
|
||||||
|
childs = commentlist.filter(parent_comment=c, is_enable=True)
|
||||||
|
for child in childs:
|
||||||
|
datas.append(child)
|
||||||
|
parse(child)
|
||||||
|
|
||||||
|
parse(comment)
|
||||||
|
return datas
|
||||||
|
|
||||||
|
|
||||||
|
@register.inclusion_tag('comments/tags/comment_item.html')
|
||||||
|
def show_comment_item(comment, ischild):
|
||||||
|
"""评论"""
|
||||||
|
depth = 1 if ischild else 2
|
||||||
|
return {
|
||||||
|
'comment_item': comment,
|
||||||
|
'depth': depth
|
||||||
|
}
|
||||||
@ -0,0 +1,109 @@
|
|||||||
|
from django.test import Client, RequestFactory, TransactionTestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from accounts.models import BlogUser
|
||||||
|
from blog.models import Category, Article
|
||||||
|
from comments.models import Comment
|
||||||
|
from comments.templatetags.comments_tags import *
|
||||||
|
from djangoblog.utils import get_max_articleid_commentid
|
||||||
|
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
|
|
||||||
|
class CommentsTest(TransactionTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.client = Client()
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
from blog.models import BlogSettings
|
||||||
|
value = BlogSettings()
|
||||||
|
value.comment_need_review = True
|
||||||
|
value.save()
|
||||||
|
|
||||||
|
self.user = BlogUser.objects.create_superuser(
|
||||||
|
email="liangliangyy1@gmail.com",
|
||||||
|
username="liangliangyy1",
|
||||||
|
password="liangliangyy1")
|
||||||
|
|
||||||
|
def update_article_comment_status(self, article):
|
||||||
|
comments = article.comment_set.all()
|
||||||
|
for comment in comments:
|
||||||
|
comment.is_enable = True
|
||||||
|
comment.save()
|
||||||
|
|
||||||
|
def test_validate_comment(self):
|
||||||
|
self.client.login(username='liangliangyy1', password='liangliangyy1')
|
||||||
|
|
||||||
|
category = Category()
|
||||||
|
category.name = "categoryccc"
|
||||||
|
category.save()
|
||||||
|
|
||||||
|
article = Article()
|
||||||
|
article.title = "nicetitleccc"
|
||||||
|
article.body = "nicecontentccc"
|
||||||
|
article.author = self.user
|
||||||
|
article.category = category
|
||||||
|
article.type = 'a'
|
||||||
|
article.status = 'p'
|
||||||
|
article.save()
|
||||||
|
|
||||||
|
comment_url = reverse(
|
||||||
|
'comments:postcomment', kwargs={
|
||||||
|
'article_id': article.id})
|
||||||
|
|
||||||
|
response = self.client.post(comment_url,
|
||||||
|
{
|
||||||
|
'body': '123ffffffffff'
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
|
article = Article.objects.get(pk=article.pk)
|
||||||
|
self.assertEqual(len(article.comment_list()), 0)
|
||||||
|
self.update_article_comment_status(article)
|
||||||
|
|
||||||
|
self.assertEqual(len(article.comment_list()), 1)
|
||||||
|
|
||||||
|
response = self.client.post(comment_url,
|
||||||
|
{
|
||||||
|
'body': '123ffffffffff',
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
|
article = Article.objects.get(pk=article.pk)
|
||||||
|
self.update_article_comment_status(article)
|
||||||
|
self.assertEqual(len(article.comment_list()), 2)
|
||||||
|
parent_comment_id = article.comment_list()[0].id
|
||||||
|
|
||||||
|
response = self.client.post(comment_url,
|
||||||
|
{
|
||||||
|
'body': '''
|
||||||
|
# Title1
|
||||||
|
|
||||||
|
```python
|
||||||
|
import os
|
||||||
|
```
|
||||||
|
|
||||||
|
[url](https://www.lylinux.net/)
|
||||||
|
|
||||||
|
[ddd](http://www.baidu.com)
|
||||||
|
|
||||||
|
|
||||||
|
''',
|
||||||
|
'parent_comment_id': parent_comment_id
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.update_article_comment_status(article)
|
||||||
|
article = Article.objects.get(pk=article.pk)
|
||||||
|
self.assertEqual(len(article.comment_list()), 3)
|
||||||
|
comment = Comment.objects.get(id=parent_comment_id)
|
||||||
|
tree = parse_commenttree(article.comment_list(), comment)
|
||||||
|
self.assertEqual(len(tree), 1)
|
||||||
|
data = show_comment_item(comment, True)
|
||||||
|
self.assertIsNotNone(data)
|
||||||
|
s = get_max_articleid_commentid()
|
||||||
|
self.assertIsNotNone(s)
|
||||||
|
|
||||||
|
from comments.utils import send_comment_email
|
||||||
|
send_comment_email(comment)
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = "comments"
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
'article/<int:article_id>/postcomment',
|
||||||
|
views.CommentPostView.as_view(),
|
||||||
|
name='postcomment'),
|
||||||
|
]
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from djangoblog.utils import get_current_site
|
||||||
|
from djangoblog.utils import send_email
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def send_comment_email(comment):
|
||||||
|
site = get_current_site().domain
|
||||||
|
subject = _('Thanks for your comment')
|
||||||
|
article_url = f"https://{site}{comment.article.get_absolute_url()}"
|
||||||
|
html_content = _("""<p>Thank you very much for your comments on this site</p>
|
||||||
|
You can visit <a href="%(article_url)s" rel="bookmark">%(article_title)s</a>
|
||||||
|
to review your comments,
|
||||||
|
Thank you again!
|
||||||
|
<br />
|
||||||
|
If the link above cannot be opened, please copy this link to your browser.
|
||||||
|
%(article_url)s""") % {'article_url': article_url, 'article_title': comment.article.title}
|
||||||
|
tomail = comment.author.email
|
||||||
|
send_email([tomail], subject, html_content)
|
||||||
|
try:
|
||||||
|
if comment.parent_comment:
|
||||||
|
html_content = _("""Your comment on <a href="%(article_url)s" rel="bookmark">%(article_title)s</a><br/> has
|
||||||
|
received a reply. <br/> %(comment_body)s
|
||||||
|
<br/>
|
||||||
|
go check it out!
|
||||||
|
<br/>
|
||||||
|
If the link above cannot be opened, please copy this link to your browser.
|
||||||
|
%(article_url)s
|
||||||
|
""") % {'article_url': article_url, 'article_title': comment.article.title,
|
||||||
|
'comment_body': comment.parent_comment.body}
|
||||||
|
tomail = comment.parent_comment.author.email
|
||||||
|
send_email([tomail], subject, html_content)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
# Create your views here.
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views.decorators.csrf import csrf_protect
|
||||||
|
from django.views.generic.edit import FormView
|
||||||
|
|
||||||
|
from accounts.models import BlogUser
|
||||||
|
from blog.models import Article
|
||||||
|
from .forms import CommentForm
|
||||||
|
from .models import Comment
|
||||||
|
|
||||||
|
|
||||||
|
class CommentPostView(FormView):
|
||||||
|
form_class = CommentForm
|
||||||
|
template_name = 'blog/article_detail.html'
|
||||||
|
|
||||||
|
@method_decorator(csrf_protect)
|
||||||
|
def dispatch(self, *args, **kwargs):
|
||||||
|
return super(CommentPostView, self).dispatch(*args, **kwargs)
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
article_id = self.kwargs['article_id']
|
||||||
|
article = get_object_or_404(Article, pk=article_id)
|
||||||
|
url = article.get_absolute_url()
|
||||||
|
return HttpResponseRedirect(url + "#comments")
|
||||||
|
|
||||||
|
def form_invalid(self, form):
|
||||||
|
article_id = self.kwargs['article_id']
|
||||||
|
article = get_object_or_404(Article, pk=article_id)
|
||||||
|
|
||||||
|
return self.render_to_response({
|
||||||
|
'form': form,
|
||||||
|
'article': article
|
||||||
|
})
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
"""提交的数据验证合法后的逻辑"""
|
||||||
|
user = self.request.user
|
||||||
|
author = BlogUser.objects.get(pk=user.pk)
|
||||||
|
article_id = self.kwargs['article_id']
|
||||||
|
article = get_object_or_404(Article, pk=article_id)
|
||||||
|
|
||||||
|
if article.comment_status == 'c' or article.status == 'c':
|
||||||
|
raise ValidationError("该文章评论已关闭.")
|
||||||
|
comment = form.save(False)
|
||||||
|
comment.article = article
|
||||||
|
from djangoblog.utils import get_blog_setting
|
||||||
|
settings = get_blog_setting()
|
||||||
|
if not settings.comment_need_review:
|
||||||
|
comment.is_enable = True
|
||||||
|
comment.author = author
|
||||||
|
|
||||||
|
if form.cleaned_data['parent_comment_id']:
|
||||||
|
parent_comment = Comment.objects.get(
|
||||||
|
pk=form.cleaned_data['parent_comment_id'])
|
||||||
|
comment.parent_comment = parent_comment
|
||||||
|
|
||||||
|
comment.save(True)
|
||||||
|
return HttpResponseRedirect(
|
||||||
|
"%s#div-comment-%d" %
|
||||||
|
(article.get_absolute_url(), comment.pk))
|
||||||
@ -0,0 +1 @@
|
|||||||
|
default_app_config = 'djangoblog.apps.DjangoblogAppConfig'
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
from django.contrib.admin import AdminSite
|
||||||
|
from django.contrib.admin.models import LogEntry
|
||||||
|
from django.contrib.sites.admin import SiteAdmin
|
||||||
|
from django.contrib.sites.models import Site
|
||||||
|
|
||||||
|
from accounts.admin import *
|
||||||
|
from blog.admin import *
|
||||||
|
from blog.models import *
|
||||||
|
from comments.admin import *
|
||||||
|
from comments.models import *
|
||||||
|
from djangoblog.logentryadmin import LogEntryAdmin
|
||||||
|
from oauth.admin import *
|
||||||
|
from oauth.models import *
|
||||||
|
from owntracks.admin import *
|
||||||
|
from owntracks.models import *
|
||||||
|
from servermanager.admin import *
|
||||||
|
from servermanager.models import *
|
||||||
|
|
||||||
|
|
||||||
|
class DjangoBlogAdminSite(AdminSite):
|
||||||
|
site_header = 'djangoblog administration'
|
||||||
|
site_title = 'djangoblog site admin'
|
||||||
|
|
||||||
|
def __init__(self, name='admin'):
|
||||||
|
super().__init__(name)
|
||||||
|
|
||||||
|
def has_permission(self, request):
|
||||||
|
return request.user.is_superuser
|
||||||
|
|
||||||
|
# def get_urls(self):
|
||||||
|
# urls = super().get_urls()
|
||||||
|
# from django.urls import path
|
||||||
|
# from blog.views import refresh_memcache
|
||||||
|
#
|
||||||
|
# my_urls = [
|
||||||
|
# path('refresh/', self.admin_view(refresh_memcache), name="refresh"),
|
||||||
|
# ]
|
||||||
|
# return urls + my_urls
|
||||||
|
|
||||||
|
|
||||||
|
admin_site = DjangoBlogAdminSite(name='admin')
|
||||||
|
|
||||||
|
admin_site.register(Article, ArticlelAdmin)
|
||||||
|
admin_site.register(Category, CategoryAdmin)
|
||||||
|
admin_site.register(Tag, TagAdmin)
|
||||||
|
admin_site.register(Links, LinksAdmin)
|
||||||
|
admin_site.register(SideBar, SideBarAdmin)
|
||||||
|
admin_site.register(BlogSettings, BlogSettingsAdmin)
|
||||||
|
|
||||||
|
admin_site.register(commands, CommandsAdmin)
|
||||||
|
admin_site.register(EmailSendLog, EmailSendLogAdmin)
|
||||||
|
|
||||||
|
admin_site.register(BlogUser, BlogUserAdmin)
|
||||||
|
|
||||||
|
admin_site.register(Comment, CommentAdmin)
|
||||||
|
|
||||||
|
admin_site.register(OAuthUser, OAuthUserAdmin)
|
||||||
|
admin_site.register(OAuthConfig, OAuthConfigAdmin)
|
||||||
|
|
||||||
|
admin_site.register(OwnTrackLog, OwnTrackLogsAdmin)
|
||||||
|
|
||||||
|
admin_site.register(Site, SiteAdmin)
|
||||||
|
|
||||||
|
admin_site.register(LogEntry, LogEntryAdmin)
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
class DjangoblogAppConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'djangoblog'
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
super().ready()
|
||||||
|
# Import and load plugins here
|
||||||
|
from .plugin_manage.loader import load_plugins
|
||||||
|
load_plugins()
|
||||||
@ -0,0 +1,122 @@
|
|||||||
|
import _thread
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import django.dispatch
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.admin.models import LogEntry
|
||||||
|
from django.contrib.auth.signals import user_logged_in, user_logged_out
|
||||||
|
from django.core.mail import EmailMultiAlternatives
|
||||||
|
from django.db.models.signals import post_save
|
||||||
|
from django.dispatch import receiver
|
||||||
|
|
||||||
|
from comments.models import Comment
|
||||||
|
from comments.utils import send_comment_email
|
||||||
|
from djangoblog.spider_notify import SpiderNotify
|
||||||
|
from djangoblog.utils import cache, expire_view_cache, delete_sidebar_cache, delete_view_cache
|
||||||
|
from djangoblog.utils import get_current_site
|
||||||
|
from oauth.models import OAuthUser
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
oauth_user_login_signal = django.dispatch.Signal(['id'])
|
||||||
|
send_email_signal = django.dispatch.Signal(
|
||||||
|
['emailto', 'title', 'content'])
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(send_email_signal)
|
||||||
|
def send_email_signal_handler(sender, **kwargs):
|
||||||
|
emailto = kwargs['emailto']
|
||||||
|
title = kwargs['title']
|
||||||
|
content = kwargs['content']
|
||||||
|
|
||||||
|
msg = EmailMultiAlternatives(
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||||
|
to=emailto)
|
||||||
|
msg.content_subtype = "html"
|
||||||
|
|
||||||
|
from servermanager.models import EmailSendLog
|
||||||
|
log = EmailSendLog()
|
||||||
|
log.title = title
|
||||||
|
log.content = content
|
||||||
|
log.emailto = ','.join(emailto)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = msg.send()
|
||||||
|
log.send_result = result > 0
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"失败邮箱号: {emailto}, {e}")
|
||||||
|
log.send_result = False
|
||||||
|
log.save()
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(oauth_user_login_signal)
|
||||||
|
def oauth_user_login_signal_handler(sender, **kwargs):
|
||||||
|
id = kwargs['id']
|
||||||
|
oauthuser = OAuthUser.objects.get(id=id)
|
||||||
|
site = get_current_site().domain
|
||||||
|
if oauthuser.picture and not oauthuser.picture.find(site) >= 0:
|
||||||
|
from djangoblog.utils import save_user_avatar
|
||||||
|
oauthuser.picture = save_user_avatar(oauthuser.picture)
|
||||||
|
oauthuser.save()
|
||||||
|
|
||||||
|
delete_sidebar_cache()
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save)
|
||||||
|
def model_post_save_callback(
|
||||||
|
sender,
|
||||||
|
instance,
|
||||||
|
created,
|
||||||
|
raw,
|
||||||
|
using,
|
||||||
|
update_fields,
|
||||||
|
**kwargs):
|
||||||
|
clearcache = False
|
||||||
|
if isinstance(instance, LogEntry):
|
||||||
|
return
|
||||||
|
if 'get_full_url' in dir(instance):
|
||||||
|
is_update_views = update_fields == {'views'}
|
||||||
|
if not settings.TESTING and not is_update_views:
|
||||||
|
try:
|
||||||
|
notify_url = instance.get_full_url()
|
||||||
|
SpiderNotify.baidu_notify([notify_url])
|
||||||
|
except Exception as ex:
|
||||||
|
logger.error("notify sipder", ex)
|
||||||
|
if not is_update_views:
|
||||||
|
clearcache = True
|
||||||
|
|
||||||
|
if isinstance(instance, Comment):
|
||||||
|
if instance.is_enable:
|
||||||
|
path = instance.article.get_absolute_url()
|
||||||
|
site = get_current_site().domain
|
||||||
|
if site.find(':') > 0:
|
||||||
|
site = site[0:site.find(':')]
|
||||||
|
|
||||||
|
expire_view_cache(
|
||||||
|
path,
|
||||||
|
servername=site,
|
||||||
|
serverport=80,
|
||||||
|
key_prefix='blogdetail')
|
||||||
|
if cache.get('seo_processor'):
|
||||||
|
cache.delete('seo_processor')
|
||||||
|
comment_cache_key = 'article_comments_{id}'.format(
|
||||||
|
id=instance.article.id)
|
||||||
|
cache.delete(comment_cache_key)
|
||||||
|
delete_sidebar_cache()
|
||||||
|
delete_view_cache('article_comments', [str(instance.article.pk)])
|
||||||
|
|
||||||
|
_thread.start_new_thread(send_comment_email, (instance,))
|
||||||
|
|
||||||
|
if clearcache:
|
||||||
|
cache.clear()
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(user_logged_in)
|
||||||
|
@receiver(user_logged_out)
|
||||||
|
def user_auth_callback(sender, request, user, **kwargs):
|
||||||
|
if user and user.username:
|
||||||
|
logger.info(user)
|
||||||
|
delete_sidebar_cache()
|
||||||
|
# cache.clear()
|
||||||
@ -0,0 +1,183 @@
|
|||||||
|
from django.utils.encoding import force_str
|
||||||
|
from elasticsearch_dsl import Q
|
||||||
|
from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query
|
||||||
|
from haystack.forms import ModelSearchForm
|
||||||
|
from haystack.models import SearchResult
|
||||||
|
from haystack.utils import log as logging
|
||||||
|
|
||||||
|
from blog.documents import ArticleDocument, ArticleDocumentManager
|
||||||
|
from blog.models import Article
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ElasticSearchBackend(BaseSearchBackend):
|
||||||
|
def __init__(self, connection_alias, **connection_options):
|
||||||
|
super(
|
||||||
|
ElasticSearchBackend,
|
||||||
|
self).__init__(
|
||||||
|
connection_alias,
|
||||||
|
**connection_options)
|
||||||
|
self.manager = ArticleDocumentManager()
|
||||||
|
self.include_spelling = True
|
||||||
|
|
||||||
|
def _get_models(self, iterable):
|
||||||
|
models = iterable if iterable and iterable[0] else Article.objects.all()
|
||||||
|
docs = self.manager.convert_to_doc(models)
|
||||||
|
return docs
|
||||||
|
|
||||||
|
def _create(self, models):
|
||||||
|
self.manager.create_index()
|
||||||
|
docs = self._get_models(models)
|
||||||
|
self.manager.rebuild(docs)
|
||||||
|
|
||||||
|
def _delete(self, models):
|
||||||
|
for m in models:
|
||||||
|
m.delete()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _rebuild(self, models):
|
||||||
|
models = models if models else Article.objects.all()
|
||||||
|
docs = self.manager.convert_to_doc(models)
|
||||||
|
self.manager.update_docs(docs)
|
||||||
|
|
||||||
|
def update(self, index, iterable, commit=True):
|
||||||
|
|
||||||
|
models = self._get_models(iterable)
|
||||||
|
self.manager.update_docs(models)
|
||||||
|
|
||||||
|
def remove(self, obj_or_string):
|
||||||
|
models = self._get_models([obj_or_string])
|
||||||
|
self._delete(models)
|
||||||
|
|
||||||
|
def clear(self, models=None, commit=True):
|
||||||
|
self.remove(None)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_suggestion(query: str) -> str:
|
||||||
|
"""获取推荐词, 如果没有找到添加原搜索词"""
|
||||||
|
|
||||||
|
search = ArticleDocument.search() \
|
||||||
|
.query("match", body=query) \
|
||||||
|
.suggest('suggest_search', query, term={'field': 'body'}) \
|
||||||
|
.execute()
|
||||||
|
|
||||||
|
keywords = []
|
||||||
|
for suggest in search.suggest.suggest_search:
|
||||||
|
if suggest["options"]:
|
||||||
|
keywords.append(suggest["options"][0]["text"])
|
||||||
|
else:
|
||||||
|
keywords.append(suggest["text"])
|
||||||
|
|
||||||
|
return ' '.join(keywords)
|
||||||
|
|
||||||
|
@log_query
|
||||||
|
def search(self, query_string, **kwargs):
|
||||||
|
logger.info('search query_string:' + query_string)
|
||||||
|
|
||||||
|
start_offset = kwargs.get('start_offset')
|
||||||
|
end_offset = kwargs.get('end_offset')
|
||||||
|
|
||||||
|
# 推荐词搜索
|
||||||
|
if getattr(self, "is_suggest", None):
|
||||||
|
suggestion = self.get_suggestion(query_string)
|
||||||
|
else:
|
||||||
|
suggestion = query_string
|
||||||
|
|
||||||
|
q = Q('bool',
|
||||||
|
should=[Q('match', body=suggestion), Q('match', title=suggestion)],
|
||||||
|
minimum_should_match="70%")
|
||||||
|
|
||||||
|
search = ArticleDocument.search() \
|
||||||
|
.query('bool', filter=[q]) \
|
||||||
|
.filter('term', status='p') \
|
||||||
|
.filter('term', type='a') \
|
||||||
|
.source(False)[start_offset: end_offset]
|
||||||
|
|
||||||
|
results = search.execute()
|
||||||
|
hits = results['hits'].total
|
||||||
|
raw_results = []
|
||||||
|
for raw_result in results['hits']['hits']:
|
||||||
|
app_label = 'blog'
|
||||||
|
model_name = 'Article'
|
||||||
|
additional_fields = {}
|
||||||
|
|
||||||
|
result_class = SearchResult
|
||||||
|
|
||||||
|
result = result_class(
|
||||||
|
app_label,
|
||||||
|
model_name,
|
||||||
|
raw_result['_id'],
|
||||||
|
raw_result['_score'],
|
||||||
|
**additional_fields)
|
||||||
|
raw_results.append(result)
|
||||||
|
facets = {}
|
||||||
|
spelling_suggestion = None if query_string == suggestion else suggestion
|
||||||
|
|
||||||
|
return {
|
||||||
|
'results': raw_results,
|
||||||
|
'hits': hits,
|
||||||
|
'facets': facets,
|
||||||
|
'spelling_suggestion': spelling_suggestion,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ElasticSearchQuery(BaseSearchQuery):
|
||||||
|
def _convert_datetime(self, date):
|
||||||
|
if hasattr(date, 'hour'):
|
||||||
|
return force_str(date.strftime('%Y%m%d%H%M%S'))
|
||||||
|
else:
|
||||||
|
return force_str(date.strftime('%Y%m%d000000'))
|
||||||
|
|
||||||
|
def clean(self, query_fragment):
|
||||||
|
"""
|
||||||
|
Provides a mechanism for sanitizing user input before presenting the
|
||||||
|
value to the backend.
|
||||||
|
|
||||||
|
Whoosh 1.X differs here in that you can no longer use a backslash
|
||||||
|
to escape reserved characters. Instead, the whole word should be
|
||||||
|
quoted.
|
||||||
|
"""
|
||||||
|
words = query_fragment.split()
|
||||||
|
cleaned_words = []
|
||||||
|
|
||||||
|
for word in words:
|
||||||
|
if word in self.backend.RESERVED_WORDS:
|
||||||
|
word = word.replace(word, word.lower())
|
||||||
|
|
||||||
|
for char in self.backend.RESERVED_CHARACTERS:
|
||||||
|
if char in word:
|
||||||
|
word = "'%s'" % word
|
||||||
|
break
|
||||||
|
|
||||||
|
cleaned_words.append(word)
|
||||||
|
|
||||||
|
return ' '.join(cleaned_words)
|
||||||
|
|
||||||
|
def build_query_fragment(self, field, filter_type, value):
|
||||||
|
return value.query_string
|
||||||
|
|
||||||
|
def get_count(self):
|
||||||
|
results = self.get_results()
|
||||||
|
return len(results) if results else 0
|
||||||
|
|
||||||
|
def get_spelling_suggestion(self, preferred_query=None):
|
||||||
|
return self._spelling_suggestion
|
||||||
|
|
||||||
|
def build_params(self, spelling_query=None):
|
||||||
|
kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query)
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
|
class ElasticSearchModelSearchForm(ModelSearchForm):
|
||||||
|
|
||||||
|
def search(self):
|
||||||
|
# 是否建议搜索
|
||||||
|
self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no"
|
||||||
|
sqs = super().search()
|
||||||
|
return sqs
|
||||||
|
|
||||||
|
|
||||||
|
class ElasticSearchEngine(BaseEngine):
|
||||||
|
backend = ElasticSearchBackend
|
||||||
|
query = ElasticSearchQuery
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.syndication.views import Feed
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.feedgenerator import Rss201rev2Feed
|
||||||
|
|
||||||
|
from blog.models import Article
|
||||||
|
from djangoblog.utils import CommonMarkdown
|
||||||
|
|
||||||
|
|
||||||
|
class DjangoBlogFeed(Feed):
|
||||||
|
feed_type = Rss201rev2Feed
|
||||||
|
|
||||||
|
description = '大巧无工,重剑无锋.'
|
||||||
|
title = "且听风吟 大巧无工,重剑无锋. "
|
||||||
|
link = "/feed/"
|
||||||
|
|
||||||
|
def author_name(self):
|
||||||
|
return get_user_model().objects.first().nickname
|
||||||
|
|
||||||
|
def author_link(self):
|
||||||
|
return get_user_model().objects.first().get_absolute_url()
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5]
|
||||||
|
|
||||||
|
def item_title(self, item):
|
||||||
|
return item.title
|
||||||
|
|
||||||
|
def item_description(self, item):
|
||||||
|
return CommonMarkdown.get_markdown(item.body)
|
||||||
|
|
||||||
|
def feed_copyright(self):
|
||||||
|
now = timezone.now()
|
||||||
|
return "Copyright© {year} 且听风吟".format(year=now.year)
|
||||||
|
|
||||||
|
def item_link(self, item):
|
||||||
|
return item.get_absolute_url()
|
||||||
|
|
||||||
|
def item_guid(self, item):
|
||||||
|
return
|
||||||
@ -0,0 +1,91 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.admin.models import DELETION
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.urls import reverse, NoReverseMatch
|
||||||
|
from django.utils.encoding import force_str
|
||||||
|
from django.utils.html import escape
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class LogEntryAdmin(admin.ModelAdmin):
|
||||||
|
list_filter = [
|
||||||
|
'content_type'
|
||||||
|
]
|
||||||
|
|
||||||
|
search_fields = [
|
||||||
|
'object_repr',
|
||||||
|
'change_message'
|
||||||
|
]
|
||||||
|
|
||||||
|
list_display_links = [
|
||||||
|
'action_time',
|
||||||
|
'get_change_message',
|
||||||
|
]
|
||||||
|
list_display = [
|
||||||
|
'action_time',
|
||||||
|
'user_link',
|
||||||
|
'content_type',
|
||||||
|
'object_link',
|
||||||
|
'get_change_message',
|
||||||
|
]
|
||||||
|
|
||||||
|
def has_add_permission(self, request):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def has_change_permission(self, request, obj=None):
|
||||||
|
return (
|
||||||
|
request.user.is_superuser or
|
||||||
|
request.user.has_perm('admin.change_logentry')
|
||||||
|
) and request.method != 'POST'
|
||||||
|
|
||||||
|
def has_delete_permission(self, request, obj=None):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def object_link(self, obj):
|
||||||
|
object_link = escape(obj.object_repr)
|
||||||
|
content_type = obj.content_type
|
||||||
|
|
||||||
|
if obj.action_flag != DELETION and content_type is not None:
|
||||||
|
# try returning an actual link instead of object repr string
|
||||||
|
try:
|
||||||
|
url = reverse(
|
||||||
|
'admin:{}_{}_change'.format(content_type.app_label,
|
||||||
|
content_type.model),
|
||||||
|
args=[obj.object_id]
|
||||||
|
)
|
||||||
|
object_link = '<a href="{}">{}</a>'.format(url, object_link)
|
||||||
|
except NoReverseMatch:
|
||||||
|
pass
|
||||||
|
return mark_safe(object_link)
|
||||||
|
|
||||||
|
object_link.admin_order_field = 'object_repr'
|
||||||
|
object_link.short_description = _('object')
|
||||||
|
|
||||||
|
def user_link(self, obj):
|
||||||
|
content_type = ContentType.objects.get_for_model(type(obj.user))
|
||||||
|
user_link = escape(force_str(obj.user))
|
||||||
|
try:
|
||||||
|
# try returning an actual link instead of object repr string
|
||||||
|
url = reverse(
|
||||||
|
'admin:{}_{}_change'.format(content_type.app_label,
|
||||||
|
content_type.model),
|
||||||
|
args=[obj.user.pk]
|
||||||
|
)
|
||||||
|
user_link = '<a href="{}">{}</a>'.format(url, user_link)
|
||||||
|
except NoReverseMatch:
|
||||||
|
pass
|
||||||
|
return mark_safe(user_link)
|
||||||
|
|
||||||
|
user_link.admin_order_field = 'user'
|
||||||
|
user_link.short_description = _('user')
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
queryset = super(LogEntryAdmin, self).get_queryset(request)
|
||||||
|
return queryset.prefetch_related('content_type')
|
||||||
|
|
||||||
|
def get_actions(self, request):
|
||||||
|
actions = super(LogEntryAdmin, self).get_actions(request)
|
||||||
|
if 'delete_selected' in actions:
|
||||||
|
del actions['delete_selected']
|
||||||
|
return actions
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class BasePlugin:
|
||||||
|
# 插件元数据
|
||||||
|
PLUGIN_NAME = None
|
||||||
|
PLUGIN_DESCRIPTION = None
|
||||||
|
PLUGIN_VERSION = None
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
if not all([self.PLUGIN_NAME, self.PLUGIN_DESCRIPTION, self.PLUGIN_VERSION]):
|
||||||
|
raise ValueError("Plugin metadata (PLUGIN_NAME, PLUGIN_DESCRIPTION, PLUGIN_VERSION) must be defined.")
|
||||||
|
self.init_plugin()
|
||||||
|
self.register_hooks()
|
||||||
|
|
||||||
|
def init_plugin(self):
|
||||||
|
"""
|
||||||
|
插件初始化逻辑
|
||||||
|
子类可以重写此方法来实现特定的初始化操作
|
||||||
|
"""
|
||||||
|
logger.info(f'{self.PLUGIN_NAME} initialized.')
|
||||||
|
|
||||||
|
def register_hooks(self):
|
||||||
|
"""
|
||||||
|
注册插件钩子
|
||||||
|
子类可以重写此方法来注册特定的钩子
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_plugin_info(self):
|
||||||
|
"""
|
||||||
|
获取插件信息
|
||||||
|
:return: 包含插件元数据的字典
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'name': self.PLUGIN_NAME,
|
||||||
|
'description': self.PLUGIN_DESCRIPTION,
|
||||||
|
'version': self.PLUGIN_VERSION
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
ARTICLE_DETAIL_LOAD = 'article_detail_load'
|
||||||
|
ARTICLE_CREATE = 'article_create'
|
||||||
|
ARTICLE_UPDATE = 'article_update'
|
||||||
|
ARTICLE_DELETE = 'article_delete'
|
||||||
|
|
||||||
|
ARTICLE_CONTENT_HOOK_NAME = "the_content"
|
||||||
|
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_hooks = {}
|
||||||
|
|
||||||
|
|
||||||
|
def register(hook_name: str, callback: callable):
|
||||||
|
"""
|
||||||
|
注册一个钩子回调。
|
||||||
|
"""
|
||||||
|
if hook_name not in _hooks:
|
||||||
|
_hooks[hook_name] = []
|
||||||
|
_hooks[hook_name].append(callback)
|
||||||
|
logger.debug(f"Registered hook '{hook_name}' with callback '{callback.__name__}'")
|
||||||
|
|
||||||
|
|
||||||
|
def run_action(hook_name: str, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
执行一个 Action Hook。
|
||||||
|
它会按顺序执行所有注册到该钩子上的回调函数。
|
||||||
|
"""
|
||||||
|
if hook_name in _hooks:
|
||||||
|
logger.debug(f"Running action hook '{hook_name}'")
|
||||||
|
for callback in _hooks[hook_name]:
|
||||||
|
try:
|
||||||
|
callback(*args, **kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error running action hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_filters(hook_name: str, value, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
执行一个 Filter Hook。
|
||||||
|
它会把 value 依次传递给所有注册的回调函数进行处理。
|
||||||
|
"""
|
||||||
|
if hook_name in _hooks:
|
||||||
|
logger.debug(f"Applying filter hook '{hook_name}'")
|
||||||
|
for callback in _hooks[hook_name]:
|
||||||
|
try:
|
||||||
|
value = callback(value, *args, **kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
|
||||||
|
return value
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def load_plugins():
|
||||||
|
"""
|
||||||
|
Dynamically loads and initializes plugins from the 'plugins' directory.
|
||||||
|
This function is intended to be called when the Django app registry is ready.
|
||||||
|
"""
|
||||||
|
for plugin_name in settings.ACTIVE_PLUGINS:
|
||||||
|
plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name)
|
||||||
|
if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')):
|
||||||
|
try:
|
||||||
|
__import__(f'plugins.{plugin_name}.plugin')
|
||||||
|
logger.info(f"Successfully loaded plugin: {plugin_name}")
|
||||||
|
except ImportError as e:
|
||||||
|
logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)
|
||||||
@ -0,0 +1,343 @@
|
|||||||
|
"""
|
||||||
|
Django settings for djangoblog project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 1.10.2.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/1.10/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/1.10/ref/settings/
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
def env_to_bool(env, default):
|
||||||
|
str_val = os.environ.get(env)
|
||||||
|
return default if str_val is None else str_val == 'True'
|
||||||
|
|
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
# Quick-start development settings - unsuitable for production
|
||||||
|
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = os.environ.get(
|
||||||
|
'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6'
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = env_to_bool('DJANGO_DEBUG', True)
|
||||||
|
# DEBUG = False
|
||||||
|
TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test'
|
||||||
|
|
||||||
|
# ALLOWED_HOSTS = []
|
||||||
|
ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com']
|
||||||
|
# django 4.0新增配置
|
||||||
|
CSRF_TRUSTED_ORIGINS = ['http://example.com']
|
||||||
|
# Application definition
|
||||||
|
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
# 'django.contrib.admin',
|
||||||
|
'django.contrib.admin.apps.SimpleAdminConfig',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
'django.contrib.sites',
|
||||||
|
'django.contrib.sitemaps',
|
||||||
|
'mdeditor',
|
||||||
|
'haystack',
|
||||||
|
'blog',
|
||||||
|
'accounts',
|
||||||
|
'comments',
|
||||||
|
'oauth',
|
||||||
|
'servermanager',
|
||||||
|
'owntracks',
|
||||||
|
'compressor',
|
||||||
|
'djangoblog'
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.locale.LocaleMiddleware',
|
||||||
|
'django.middleware.gzip.GZipMiddleware',
|
||||||
|
# 'django.middleware.cache.UpdateCacheMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
# 'django.middleware.cache.FetchFromCacheMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
'django.middleware.http.ConditionalGetMiddleware',
|
||||||
|
'blog.middleware.OnlineMiddleware'
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'djangoblog.urls'
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': [os.path.join(BASE_DIR, 'templates')],
|
||||||
|
'APP_DIRS': True,
|
||||||
|
'OPTIONS': {
|
||||||
|
'context_processors': [
|
||||||
|
'django.template.context_processors.debug',
|
||||||
|
'django.template.context_processors.request',
|
||||||
|
'django.contrib.auth.context_processors.auth',
|
||||||
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
'blog.context_processors.seo_processor'
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = 'djangoblog.wsgi.application'
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
|
||||||
|
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.mysql',
|
||||||
|
'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog',
|
||||||
|
'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root',
|
||||||
|
'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or 'root',
|
||||||
|
'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1',
|
||||||
|
'PORT': int(
|
||||||
|
os.environ.get('DJANGO_MYSQL_PORT') or 3306),
|
||||||
|
'OPTIONS': {
|
||||||
|
'charset': 'utf8mb4'},
|
||||||
|
}}
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
LANGUAGES = (
|
||||||
|
('en', _('English')),
|
||||||
|
('zh-hans', _('Simplified Chinese')),
|
||||||
|
('zh-hant', _('Traditional Chinese')),
|
||||||
|
)
|
||||||
|
LOCALE_PATHS = (
|
||||||
|
os.path.join(BASE_DIR, 'locale'),
|
||||||
|
)
|
||||||
|
|
||||||
|
LANGUAGE_CODE = 'zh-hans'
|
||||||
|
|
||||||
|
TIME_ZONE = 'Asia/Shanghai'
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_L10N = True
|
||||||
|
|
||||||
|
USE_TZ = False
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/1.10/howto/static-files/
|
||||||
|
|
||||||
|
|
||||||
|
HAYSTACK_CONNECTIONS = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine',
|
||||||
|
'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
# Automatically update searching index
|
||||||
|
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
|
||||||
|
# Allow user login with username and password
|
||||||
|
AUTHENTICATION_BACKENDS = [
|
||||||
|
'accounts.user_login_backend.EmailOrUsernameModelBackend']
|
||||||
|
|
||||||
|
STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic')
|
||||||
|
|
||||||
|
STATIC_URL = '/static/'
|
||||||
|
STATICFILES = os.path.join(BASE_DIR, 'static')
|
||||||
|
|
||||||
|
AUTH_USER_MODEL = 'accounts.BlogUser'
|
||||||
|
LOGIN_URL = '/login/'
|
||||||
|
|
||||||
|
TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
|
||||||
|
DATE_TIME_FORMAT = '%Y-%m-%d'
|
||||||
|
|
||||||
|
# bootstrap color styles
|
||||||
|
BOOTSTRAP_COLOR_TYPES = [
|
||||||
|
'default', 'primary', 'success', 'info', 'warning', 'danger'
|
||||||
|
]
|
||||||
|
|
||||||
|
# paginate
|
||||||
|
PAGINATE_BY = 10
|
||||||
|
# http cache timeout
|
||||||
|
CACHE_CONTROL_MAX_AGE = 2592000
|
||||||
|
# cache setting
|
||||||
|
CACHES = {
|
||||||
|
'default': {
|
||||||
|
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||||
|
'TIMEOUT': 10800,
|
||||||
|
'LOCATION': 'unique-snowflake',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# 使用redis作为缓存
|
||||||
|
if os.environ.get("DJANGO_REDIS_URL"):
|
||||||
|
CACHES = {
|
||||||
|
'default': {
|
||||||
|
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
|
||||||
|
'LOCATION': f'redis://{os.environ.get("DJANGO_REDIS_URL")}',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SITE_ID = 1
|
||||||
|
BAIDU_NOTIFY_URL = os.environ.get('DJANGO_BAIDU_NOTIFY_URL') \
|
||||||
|
or 'http://data.zz.baidu.com/urls?site=https://www.lylinux.net&token=1uAOGrMsUm5syDGn'
|
||||||
|
|
||||||
|
# Email:
|
||||||
|
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||||
|
EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False)
|
||||||
|
EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True)
|
||||||
|
EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') or 'smtp.mxhichina.com'
|
||||||
|
EMAIL_PORT = int(os.environ.get('DJANGO_EMAIL_PORT') or 465)
|
||||||
|
EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER')
|
||||||
|
EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD')
|
||||||
|
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
|
||||||
|
SERVER_EMAIL = EMAIL_HOST_USER
|
||||||
|
# Setting debug=false did NOT handle except email notifications
|
||||||
|
ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')]
|
||||||
|
# WX ADMIN password(Two times md5)
|
||||||
|
WXADMIN = os.environ.get(
|
||||||
|
'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7'
|
||||||
|
|
||||||
|
LOG_PATH = os.path.join(BASE_DIR, 'logs')
|
||||||
|
if not os.path.exists(LOG_PATH):
|
||||||
|
os.makedirs(LOG_PATH, exist_ok=True)
|
||||||
|
|
||||||
|
LOGGING = {
|
||||||
|
'version': 1,
|
||||||
|
'disable_existing_loggers': False,
|
||||||
|
'root': {
|
||||||
|
'level': 'INFO',
|
||||||
|
'handlers': ['console', 'log_file'],
|
||||||
|
},
|
||||||
|
'formatters': {
|
||||||
|
'verbose': {
|
||||||
|
'format': '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d %(module)s] %(message)s',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'filters': {
|
||||||
|
'require_debug_false': {
|
||||||
|
'()': 'django.utils.log.RequireDebugFalse',
|
||||||
|
},
|
||||||
|
'require_debug_true': {
|
||||||
|
'()': 'django.utils.log.RequireDebugTrue',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'handlers': {
|
||||||
|
'log_file': {
|
||||||
|
'level': 'INFO',
|
||||||
|
'class': 'logging.handlers.TimedRotatingFileHandler',
|
||||||
|
'filename': os.path.join(LOG_PATH, 'djangoblog.log'),
|
||||||
|
'when': 'D',
|
||||||
|
'formatter': 'verbose',
|
||||||
|
'interval': 1,
|
||||||
|
'delay': True,
|
||||||
|
'backupCount': 5,
|
||||||
|
'encoding': 'utf-8'
|
||||||
|
},
|
||||||
|
'console': {
|
||||||
|
'level': 'DEBUG',
|
||||||
|
'filters': ['require_debug_true'],
|
||||||
|
'class': 'logging.StreamHandler',
|
||||||
|
'formatter': 'verbose'
|
||||||
|
},
|
||||||
|
'null': {
|
||||||
|
'class': 'logging.NullHandler',
|
||||||
|
},
|
||||||
|
'mail_admins': {
|
||||||
|
'level': 'ERROR',
|
||||||
|
'filters': ['require_debug_false'],
|
||||||
|
'class': 'django.utils.log.AdminEmailHandler'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'loggers': {
|
||||||
|
'djangoblog': {
|
||||||
|
'handlers': ['log_file', 'console'],
|
||||||
|
'level': 'INFO',
|
||||||
|
'propagate': True,
|
||||||
|
},
|
||||||
|
'django.request': {
|
||||||
|
'handlers': ['mail_admins'],
|
||||||
|
'level': 'ERROR',
|
||||||
|
'propagate': False,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
STATICFILES_FINDERS = (
|
||||||
|
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||||
|
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||||
|
# other
|
||||||
|
'compressor.finders.CompressorFinder',
|
||||||
|
)
|
||||||
|
COMPRESS_ENABLED = True
|
||||||
|
# COMPRESS_OFFLINE = True
|
||||||
|
|
||||||
|
|
||||||
|
COMPRESS_CSS_FILTERS = [
|
||||||
|
# creates absolute urls from relative ones
|
||||||
|
'compressor.filters.css_default.CssAbsoluteFilter',
|
||||||
|
# css minimizer
|
||||||
|
'compressor.filters.cssmin.CSSMinFilter'
|
||||||
|
]
|
||||||
|
COMPRESS_JS_FILTERS = [
|
||||||
|
'compressor.filters.jsmin.JSMinFilter'
|
||||||
|
]
|
||||||
|
|
||||||
|
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
|
||||||
|
MEDIA_URL = '/media/'
|
||||||
|
X_FRAME_OPTIONS = 'SAMEORIGIN'
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
|
if os.environ.get('DJANGO_ELASTICSEARCH_HOST'):
|
||||||
|
ELASTICSEARCH_DSL = {
|
||||||
|
'default': {
|
||||||
|
'hosts': os.environ.get('DJANGO_ELASTICSEARCH_HOST')
|
||||||
|
},
|
||||||
|
}
|
||||||
|
HAYSTACK_CONNECTIONS = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Plugin System
|
||||||
|
PLUGINS_DIR = BASE_DIR / 'plugins'
|
||||||
|
ACTIVE_PLUGINS = [
|
||||||
|
'article_copyright',
|
||||||
|
'reading_time',
|
||||||
|
'external_links',
|
||||||
|
'view_count',
|
||||||
|
'seo_optimizer'
|
||||||
|
]
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
from django.contrib.sitemaps import Sitemap
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from blog.models import Article, Category, Tag
|
||||||
|
|
||||||
|
|
||||||
|
class StaticViewSitemap(Sitemap):
|
||||||
|
priority = 0.5
|
||||||
|
changefreq = 'daily'
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
return ['blog:index', ]
|
||||||
|
|
||||||
|
def location(self, item):
|
||||||
|
return reverse(item)
|
||||||
|
|
||||||
|
|
||||||
|
class ArticleSiteMap(Sitemap):
|
||||||
|
changefreq = "monthly"
|
||||||
|
priority = "0.6"
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
return Article.objects.filter(status='p')
|
||||||
|
|
||||||
|
def lastmod(self, obj):
|
||||||
|
return obj.last_modify_time
|
||||||
|
|
||||||
|
|
||||||
|
class CategorySiteMap(Sitemap):
|
||||||
|
changefreq = "Weekly"
|
||||||
|
priority = "0.6"
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
return Category.objects.all()
|
||||||
|
|
||||||
|
def lastmod(self, obj):
|
||||||
|
return obj.last_modify_time
|
||||||
|
|
||||||
|
|
||||||
|
class TagSiteMap(Sitemap):
|
||||||
|
changefreq = "Weekly"
|
||||||
|
priority = "0.3"
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
return Tag.objects.all()
|
||||||
|
|
||||||
|
def lastmod(self, obj):
|
||||||
|
return obj.last_modify_time
|
||||||
|
|
||||||
|
|
||||||
|
class UserSiteMap(Sitemap):
|
||||||
|
changefreq = "Weekly"
|
||||||
|
priority = "0.3"
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
return list(set(map(lambda x: x.author, Article.objects.all())))
|
||||||
|
|
||||||
|
def lastmod(self, obj):
|
||||||
|
return obj.date_joined
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SpiderNotify():
|
||||||
|
@staticmethod
|
||||||
|
def baidu_notify(urls):
|
||||||
|
try:
|
||||||
|
data = '\n'.join(urls)
|
||||||
|
result = requests.post(settings.BAIDU_NOTIFY_URL, data=data)
|
||||||
|
logger.info(result.text)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def notify(url):
|
||||||
|
SpiderNotify.baidu_notify(url)
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from djangoblog.utils import *
|
||||||
|
|
||||||
|
|
||||||
|
class DjangoBlogTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_utils(self):
|
||||||
|
md5 = get_sha256('test')
|
||||||
|
self.assertIsNotNone(md5)
|
||||||
|
c = CommonMarkdown.get_markdown('''
|
||||||
|
# Title1
|
||||||
|
|
||||||
|
```python
|
||||||
|
import os
|
||||||
|
```
|
||||||
|
|
||||||
|
[url](https://www.lylinux.net/)
|
||||||
|
|
||||||
|
[ddd](http://www.baidu.com)
|
||||||
|
|
||||||
|
|
||||||
|
''')
|
||||||
|
self.assertIsNotNone(c)
|
||||||
|
d = {
|
||||||
|
'd': 'key1',
|
||||||
|
'd2': 'key2'
|
||||||
|
}
|
||||||
|
data = parse_dict_to_url(d)
|
||||||
|
self.assertIsNotNone(data)
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
"""djangoblog URL Configuration
|
||||||
|
|
||||||
|
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||||
|
https://docs.djangoproject.com/en/1.10/topics/http/urls/
|
||||||
|
Examples:
|
||||||
|
Function views
|
||||||
|
1. Add an import: from my_app import views
|
||||||
|
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
|
||||||
|
Class-based views
|
||||||
|
1. Add an import: from other_app.views import Home
|
||||||
|
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
|
||||||
|
Including another URLconf
|
||||||
|
1. Import the include() function: from django.conf.urls import url, include
|
||||||
|
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
|
||||||
|
"""
|
||||||
|
from django.conf import settings
|
||||||
|
from django.conf.urls.i18n import i18n_patterns
|
||||||
|
from django.conf.urls.static import static
|
||||||
|
from django.contrib.sitemaps.views import sitemap
|
||||||
|
from django.urls import path, include
|
||||||
|
from django.urls import re_path
|
||||||
|
from haystack.views import search_view_factory
|
||||||
|
|
||||||
|
from blog.views import EsSearchView
|
||||||
|
from djangoblog.admin_site import admin_site
|
||||||
|
from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm
|
||||||
|
from djangoblog.feeds import DjangoBlogFeed
|
||||||
|
from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap
|
||||||
|
|
||||||
|
sitemaps = {
|
||||||
|
|
||||||
|
'blog': ArticleSiteMap,
|
||||||
|
'Category': CategorySiteMap,
|
||||||
|
'Tag': TagSiteMap,
|
||||||
|
'User': UserSiteMap,
|
||||||
|
'static': StaticViewSitemap
|
||||||
|
}
|
||||||
|
|
||||||
|
handler404 = 'blog.views.page_not_found_view'
|
||||||
|
handler500 = 'blog.views.server_error_view'
|
||||||
|
handle403 = 'blog.views.permission_denied_view'
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('i18n/', include('django.conf.urls.i18n')),
|
||||||
|
]
|
||||||
|
urlpatterns += i18n_patterns(
|
||||||
|
re_path(r'^admin/', admin_site.urls),
|
||||||
|
re_path(r'', include('blog.urls', namespace='blog')),
|
||||||
|
re_path(r'mdeditor/', include('mdeditor.urls')),
|
||||||
|
re_path(r'', include('comments.urls', namespace='comment')),
|
||||||
|
re_path(r'', include('accounts.urls', namespace='account')),
|
||||||
|
re_path(r'', include('oauth.urls', namespace='oauth')),
|
||||||
|
re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
|
||||||
|
name='django.contrib.sitemaps.views.sitemap'),
|
||||||
|
re_path(r'^feed/$', DjangoBlogFeed()),
|
||||||
|
re_path(r'^rss/$', DjangoBlogFeed()),
|
||||||
|
re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm),
|
||||||
|
name='search'),
|
||||||
|
re_path(r'', include('servermanager.urls', namespace='servermanager')),
|
||||||
|
re_path(r'', include('owntracks.urls', namespace='owntracks'))
|
||||||
|
, prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||||
|
if settings.DEBUG:
|
||||||
|
urlpatterns += static(settings.MEDIA_URL,
|
||||||
|
document_root=settings.MEDIA_ROOT)
|
||||||
@ -0,0 +1,232 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# encoding: utf-8
|
||||||
|
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
import uuid
|
||||||
|
from hashlib import sha256
|
||||||
|
|
||||||
|
import bleach
|
||||||
|
import markdown
|
||||||
|
import requests
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.sites.models import Site
|
||||||
|
from django.core.cache import cache
|
||||||
|
from django.templatetags.static import static
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_max_articleid_commentid():
|
||||||
|
from blog.models import Article
|
||||||
|
from comments.models import Comment
|
||||||
|
return (Article.objects.latest().pk, Comment.objects.latest().pk)
|
||||||
|
|
||||||
|
|
||||||
|
def get_sha256(str):
|
||||||
|
m = sha256(str.encode('utf-8'))
|
||||||
|
return m.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def cache_decorator(expiration=3 * 60):
|
||||||
|
def wrapper(func):
|
||||||
|
def news(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
view = args[0]
|
||||||
|
key = view.get_cache_key()
|
||||||
|
except:
|
||||||
|
key = None
|
||||||
|
if not key:
|
||||||
|
unique_str = repr((func, args, kwargs))
|
||||||
|
|
||||||
|
m = sha256(unique_str.encode('utf-8'))
|
||||||
|
key = m.hexdigest()
|
||||||
|
value = cache.get(key)
|
||||||
|
if value is not None:
|
||||||
|
# logger.info('cache_decorator get cache:%s key:%s' % (func.__name__, key))
|
||||||
|
if str(value) == '__default_cache_value__':
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
else:
|
||||||
|
logger.debug(
|
||||||
|
'cache_decorator set cache:%s key:%s' %
|
||||||
|
(func.__name__, key))
|
||||||
|
value = func(*args, **kwargs)
|
||||||
|
if value is None:
|
||||||
|
cache.set(key, '__default_cache_value__', expiration)
|
||||||
|
else:
|
||||||
|
cache.set(key, value, expiration)
|
||||||
|
return value
|
||||||
|
|
||||||
|
return news
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def expire_view_cache(path, servername, serverport, key_prefix=None):
|
||||||
|
'''
|
||||||
|
刷新视图缓存
|
||||||
|
:param path:url路径
|
||||||
|
:param servername:host
|
||||||
|
:param serverport:端口
|
||||||
|
:param key_prefix:前缀
|
||||||
|
:return:是否成功
|
||||||
|
'''
|
||||||
|
from django.http import HttpRequest
|
||||||
|
from django.utils.cache import get_cache_key
|
||||||
|
|
||||||
|
request = HttpRequest()
|
||||||
|
request.META = {'SERVER_NAME': servername, 'SERVER_PORT': serverport}
|
||||||
|
request.path = path
|
||||||
|
|
||||||
|
key = get_cache_key(request, key_prefix=key_prefix, cache=cache)
|
||||||
|
if key:
|
||||||
|
logger.info('expire_view_cache:get key:{path}'.format(path=path))
|
||||||
|
if cache.get(key):
|
||||||
|
cache.delete(key)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@cache_decorator()
|
||||||
|
def get_current_site():
|
||||||
|
site = Site.objects.get_current()
|
||||||
|
return site
|
||||||
|
|
||||||
|
|
||||||
|
class CommonMarkdown:
|
||||||
|
@staticmethod
|
||||||
|
def _convert_markdown(value):
|
||||||
|
md = markdown.Markdown(
|
||||||
|
extensions=[
|
||||||
|
'extra',
|
||||||
|
'codehilite',
|
||||||
|
'toc',
|
||||||
|
'tables',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
body = md.convert(value)
|
||||||
|
toc = md.toc
|
||||||
|
return body, toc
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_markdown_with_toc(value):
|
||||||
|
body, toc = CommonMarkdown._convert_markdown(value)
|
||||||
|
return body, toc
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_markdown(value):
|
||||||
|
body, toc = CommonMarkdown._convert_markdown(value)
|
||||||
|
return body
|
||||||
|
|
||||||
|
|
||||||
|
def send_email(emailto, title, content):
|
||||||
|
from djangoblog.blog_signals import send_email_signal
|
||||||
|
send_email_signal.send(
|
||||||
|
send_email.__class__,
|
||||||
|
emailto=emailto,
|
||||||
|
title=title,
|
||||||
|
content=content)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_code() -> str:
|
||||||
|
"""生成随机数验证码"""
|
||||||
|
return ''.join(random.sample(string.digits, 6))
|
||||||
|
|
||||||
|
|
||||||
|
def parse_dict_to_url(dict):
|
||||||
|
from urllib.parse import quote
|
||||||
|
url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/'))
|
||||||
|
for k, v in dict.items()])
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
|
def get_blog_setting():
|
||||||
|
value = cache.get('get_blog_setting')
|
||||||
|
if value:
|
||||||
|
return value
|
||||||
|
else:
|
||||||
|
from blog.models import BlogSettings
|
||||||
|
if not BlogSettings.objects.count():
|
||||||
|
setting = BlogSettings()
|
||||||
|
setting.site_name = 'djangoblog'
|
||||||
|
setting.site_description = '基于Django的博客系统'
|
||||||
|
setting.site_seo_description = '基于Django的博客系统'
|
||||||
|
setting.site_keywords = 'Django,Python'
|
||||||
|
setting.article_sub_length = 300
|
||||||
|
setting.sidebar_article_count = 10
|
||||||
|
setting.sidebar_comment_count = 5
|
||||||
|
setting.show_google_adsense = False
|
||||||
|
setting.open_site_comment = True
|
||||||
|
setting.analytics_code = ''
|
||||||
|
setting.beian_code = ''
|
||||||
|
setting.show_gongan_code = False
|
||||||
|
setting.comment_need_review = False
|
||||||
|
setting.save()
|
||||||
|
value = BlogSettings.objects.first()
|
||||||
|
logger.info('set cache get_blog_setting')
|
||||||
|
cache.set('get_blog_setting', value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def save_user_avatar(url):
|
||||||
|
'''
|
||||||
|
保存用户头像
|
||||||
|
:param url:头像url
|
||||||
|
:return: 本地路径
|
||||||
|
'''
|
||||||
|
logger.info(url)
|
||||||
|
|
||||||
|
try:
|
||||||
|
basedir = os.path.join(settings.STATICFILES, 'avatar')
|
||||||
|
rsp = requests.get(url, timeout=2)
|
||||||
|
if rsp.status_code == 200:
|
||||||
|
if not os.path.exists(basedir):
|
||||||
|
os.makedirs(basedir)
|
||||||
|
|
||||||
|
image_extensions = ['.jpg', '.png', 'jpeg', '.gif']
|
||||||
|
isimage = len([i for i in image_extensions if url.endswith(i)]) > 0
|
||||||
|
ext = os.path.splitext(url)[1] if isimage else '.jpg'
|
||||||
|
save_filename = str(uuid.uuid4().hex) + ext
|
||||||
|
logger.info('保存用户头像:' + basedir + save_filename)
|
||||||
|
with open(os.path.join(basedir, save_filename), 'wb+') as file:
|
||||||
|
file.write(rsp.content)
|
||||||
|
return static('avatar/' + save_filename)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
return static('blog/img/avatar.png')
|
||||||
|
|
||||||
|
|
||||||
|
def delete_sidebar_cache():
|
||||||
|
from blog.models import LinkShowType
|
||||||
|
keys = ["sidebar" + x for x in LinkShowType.values]
|
||||||
|
for k in keys:
|
||||||
|
logger.info('delete sidebar key:' + k)
|
||||||
|
cache.delete(k)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_view_cache(prefix, keys):
|
||||||
|
from django.core.cache.utils import make_template_fragment_key
|
||||||
|
key = make_template_fragment_key(prefix, keys)
|
||||||
|
cache.delete(key)
|
||||||
|
|
||||||
|
|
||||||
|
def get_resource_url():
|
||||||
|
if settings.STATIC_URL:
|
||||||
|
return settings.STATIC_URL
|
||||||
|
else:
|
||||||
|
site = get_current_site()
|
||||||
|
return 'http://' + site.domain + '/static/'
|
||||||
|
|
||||||
|
|
||||||
|
ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1',
|
||||||
|
'h2', 'p']
|
||||||
|
ALLOWED_ATTRIBUTES = {'a': ['href', 'title'], 'abbr': ['title'], 'acronym': ['title']}
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_html(html):
|
||||||
|
return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES)
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
WSGI config for djangoblog project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings")
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
||||||
@ -0,0 +1 @@
|
|||||||
|
# 源代码目录
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
# Register your models here.
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.html import format_html
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthUserAdmin(admin.ModelAdmin):
|
||||||
|
search_fields = ('nickname', 'email')
|
||||||
|
list_per_page = 20
|
||||||
|
list_display = (
|
||||||
|
'id',
|
||||||
|
'nickname',
|
||||||
|
'link_to_usermodel',
|
||||||
|
'show_user_image',
|
||||||
|
'type',
|
||||||
|
'email',
|
||||||
|
)
|
||||||
|
list_display_links = ('id', 'nickname')
|
||||||
|
list_filter = ('author', 'type',)
|
||||||
|
readonly_fields = []
|
||||||
|
|
||||||
|
def get_readonly_fields(self, request, obj=None):
|
||||||
|
return list(self.readonly_fields) + \
|
||||||
|
[field.name for field in obj._meta.fields] + \
|
||||||
|
[field.name for field in obj._meta.many_to_many]
|
||||||
|
|
||||||
|
def has_add_permission(self, request):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def link_to_usermodel(self, obj):
|
||||||
|
if obj.author:
|
||||||
|
info = (obj.author._meta.app_label, obj.author._meta.model_name)
|
||||||
|
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
|
||||||
|
return format_html(
|
||||||
|
u'<a href="%s">%s</a>' %
|
||||||
|
(link, obj.author.nickname if obj.author.nickname else obj.author.email))
|
||||||
|
|
||||||
|
def show_user_image(self, obj):
|
||||||
|
img = obj.picture
|
||||||
|
return format_html(
|
||||||
|
u'<img src="%s" style="width:50px;height:50px"></img>' %
|
||||||
|
(img))
|
||||||
|
|
||||||
|
link_to_usermodel.short_description = '用户'
|
||||||
|
show_user_image.short_description = '用户头像'
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthConfigAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('type', 'appkey', 'appsecret', 'is_enable')
|
||||||
|
list_filter = ('type',)
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class OauthConfig(AppConfig):
|
||||||
|
name = 'oauth'
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
from django.contrib.auth.forms import forms
|
||||||
|
from django.forms import widgets
|
||||||
|
|
||||||
|
|
||||||
|
class RequireEmailForm(forms.Form):
|
||||||
|
email = forms.EmailField(label='电子邮箱', required=True)
|
||||||
|
oauthid = forms.IntegerField(widget=forms.HiddenInput, required=False)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(RequireEmailForm, self).__init__(*args, **kwargs)
|
||||||
|
self.fields['email'].widget = widgets.EmailInput(
|
||||||
|
attrs={'placeholder': "email", "class": "form-control"})
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
# Generated by Django 4.1.7 on 2023-03-07 09:53
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='OAuthConfig',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('type', models.CharField(choices=[('weibo', '微博'), ('google', '谷歌'), ('github', 'GitHub'), ('facebook', 'FaceBook'), ('qq', 'QQ')], default='a', max_length=10, verbose_name='类型')),
|
||||||
|
('appkey', models.CharField(max_length=200, verbose_name='AppKey')),
|
||||||
|
('appsecret', models.CharField(max_length=200, verbose_name='AppSecret')),
|
||||||
|
('callback_url', models.CharField(default='http://www.baidu.com', max_length=200, verbose_name='回调地址')),
|
||||||
|
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
|
||||||
|
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
|
||||||
|
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'oauth配置',
|
||||||
|
'verbose_name_plural': 'oauth配置',
|
||||||
|
'ordering': ['-created_time'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='OAuthUser',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('openid', models.CharField(max_length=50)),
|
||||||
|
('nickname', models.CharField(max_length=50, verbose_name='昵称')),
|
||||||
|
('token', models.CharField(blank=True, max_length=150, null=True)),
|
||||||
|
('picture', models.CharField(blank=True, max_length=350, null=True)),
|
||||||
|
('type', models.CharField(max_length=50)),
|
||||||
|
('email', models.CharField(blank=True, max_length=50, null=True)),
|
||||||
|
('metadata', models.TextField(blank=True, null=True)),
|
||||||
|
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
|
||||||
|
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
|
||||||
|
('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='用户')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'oauth用户',
|
||||||
|
'verbose_name_plural': 'oauth用户',
|
||||||
|
'ordering': ['-created_time'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
# Generated by Django 4.2.5 on 2023-09-06 13:13
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('oauth', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='oauthconfig',
|
||||||
|
options={'ordering': ['-creation_time'], 'verbose_name': 'oauth配置', 'verbose_name_plural': 'oauth配置'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='oauthuser',
|
||||||
|
options={'ordering': ['-creation_time'], 'verbose_name': 'oauth user', 'verbose_name_plural': 'oauth user'},
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='oauthconfig',
|
||||||
|
name='created_time',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='oauthconfig',
|
||||||
|
name='last_mod_time',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='oauthuser',
|
||||||
|
name='created_time',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='oauthuser',
|
||||||
|
name='last_mod_time',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='oauthconfig',
|
||||||
|
name='creation_time',
|
||||||
|
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='oauthconfig',
|
||||||
|
name='last_modify_time',
|
||||||
|
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='oauthuser',
|
||||||
|
name='creation_time',
|
||||||
|
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='oauthuser',
|
||||||
|
name='last_modify_time',
|
||||||
|
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='oauthconfig',
|
||||||
|
name='callback_url',
|
||||||
|
field=models.CharField(default='', max_length=200, verbose_name='callback url'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='oauthconfig',
|
||||||
|
name='is_enable',
|
||||||
|
field=models.BooleanField(default=True, verbose_name='is enable'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='oauthconfig',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('weibo', 'weibo'), ('google', 'google'), ('github', 'GitHub'), ('facebook', 'FaceBook'), ('qq', 'QQ')], default='a', max_length=10, verbose_name='type'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='oauthuser',
|
||||||
|
name='author',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='oauthuser',
|
||||||
|
name='nickname',
|
||||||
|
field=models.CharField(max_length=50, verbose_name='nickname'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.2.7 on 2024-01-26 02:41
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('oauth', '0002_alter_oauthconfig_options_alter_oauthuser_options_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='oauthuser',
|
||||||
|
name='nickname',
|
||||||
|
field=models.CharField(max_length=50, verbose_name='nick name'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
# Create your models here.
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthUser(models.Model):
|
||||||
|
author = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
verbose_name=_('author'),
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=models.CASCADE)
|
||||||
|
openid = models.CharField(max_length=50)
|
||||||
|
nickname = models.CharField(max_length=50, verbose_name=_('nick name'))
|
||||||
|
token = models.CharField(max_length=150, null=True, blank=True)
|
||||||
|
picture = models.CharField(max_length=350, blank=True, null=True)
|
||||||
|
type = models.CharField(blank=False, null=False, max_length=50)
|
||||||
|
email = models.CharField(max_length=50, null=True, blank=True)
|
||||||
|
metadata = models.TextField(null=True, blank=True)
|
||||||
|
creation_time = models.DateTimeField(_('creation time'), default=now)
|
||||||
|
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.nickname
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('oauth user')
|
||||||
|
verbose_name_plural = verbose_name
|
||||||
|
ordering = ['-creation_time']
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthConfig(models.Model):
|
||||||
|
TYPE = (
|
||||||
|
('weibo', _('weibo')),
|
||||||
|
('google', _('google')),
|
||||||
|
('github', 'GitHub'),
|
||||||
|
('facebook', 'FaceBook'),
|
||||||
|
('qq', 'QQ'),
|
||||||
|
)
|
||||||
|
type = models.CharField(_('type'), max_length=10, choices=TYPE, default='a')
|
||||||
|
appkey = models.CharField(max_length=200, verbose_name='AppKey')
|
||||||
|
appsecret = models.CharField(max_length=200, verbose_name='AppSecret')
|
||||||
|
callback_url = models.CharField(
|
||||||
|
max_length=200,
|
||||||
|
verbose_name=_('callback url'),
|
||||||
|
blank=False,
|
||||||
|
default='')
|
||||||
|
is_enable = models.BooleanField(
|
||||||
|
_('is enable'), default=True, blank=False, null=False)
|
||||||
|
creation_time = models.DateTimeField(_('creation time'), default=now)
|
||||||
|
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
if OAuthConfig.objects.filter(
|
||||||
|
type=self.type).exclude(id=self.id).count():
|
||||||
|
raise ValidationError(_(self.type + _('already exists')))
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.type
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = 'oauth配置'
|
||||||
|
verbose_name_plural = verbose_name
|
||||||
|
ordering = ['-creation_time']
|
||||||
@ -0,0 +1,504 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import urllib.parse
|
||||||
|
from abc import ABCMeta, abstractmethod
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from djangoblog.utils import cache_decorator
|
||||||
|
from oauth.models import OAuthUser, OAuthConfig
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthAccessTokenException(Exception):
|
||||||
|
'''
|
||||||
|
oauth授权失败异常
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
class BaseOauthManager(metaclass=ABCMeta):
|
||||||
|
"""获取用户授权"""
|
||||||
|
AUTH_URL = None
|
||||||
|
"""获取token"""
|
||||||
|
TOKEN_URL = None
|
||||||
|
"""获取用户信息"""
|
||||||
|
API_URL = None
|
||||||
|
'''icon图标名'''
|
||||||
|
ICON_NAME = None
|
||||||
|
|
||||||
|
def __init__(self, access_token=None, openid=None):
|
||||||
|
self.access_token = access_token
|
||||||
|
self.openid = openid
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_access_token_set(self):
|
||||||
|
return self.access_token is not None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_authorized(self):
|
||||||
|
return self.is_access_token_set and self.access_token is not None and self.openid is not None
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_authorization_url(self, nexturl='/'):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_access_token_by_code(self, code):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_oauth_userinfo(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_picture(self, metadata):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def do_get(self, url, params, headers=None):
|
||||||
|
rsp = requests.get(url=url, params=params, headers=headers)
|
||||||
|
logger.info(rsp.text)
|
||||||
|
return rsp.text
|
||||||
|
|
||||||
|
def do_post(self, url, params, headers=None):
|
||||||
|
rsp = requests.post(url, params, headers=headers)
|
||||||
|
logger.info(rsp.text)
|
||||||
|
return rsp.text
|
||||||
|
|
||||||
|
def get_config(self):
|
||||||
|
value = OAuthConfig.objects.filter(type=self.ICON_NAME)
|
||||||
|
return value[0] if value else None
|
||||||
|
|
||||||
|
|
||||||
|
class WBOauthManager(BaseOauthManager):
|
||||||
|
AUTH_URL = 'https://api.weibo.com/oauth2/authorize'
|
||||||
|
TOKEN_URL = 'https://api.weibo.com/oauth2/access_token'
|
||||||
|
API_URL = 'https://api.weibo.com/2/users/show.json'
|
||||||
|
ICON_NAME = 'weibo'
|
||||||
|
|
||||||
|
def __init__(self, access_token=None, openid=None):
|
||||||
|
config = self.get_config()
|
||||||
|
self.client_id = config.appkey if config else ''
|
||||||
|
self.client_secret = config.appsecret if config else ''
|
||||||
|
self.callback_url = config.callback_url if config else ''
|
||||||
|
super(
|
||||||
|
WBOauthManager,
|
||||||
|
self).__init__(
|
||||||
|
access_token=access_token,
|
||||||
|
openid=openid)
|
||||||
|
|
||||||
|
def get_authorization_url(self, nexturl='/'):
|
||||||
|
params = {
|
||||||
|
'client_id': self.client_id,
|
||||||
|
'response_type': 'code',
|
||||||
|
'redirect_uri': self.callback_url + '&next_url=' + nexturl
|
||||||
|
}
|
||||||
|
url = self.AUTH_URL + "?" + urllib.parse.urlencode(params)
|
||||||
|
return url
|
||||||
|
|
||||||
|
def get_access_token_by_code(self, code):
|
||||||
|
|
||||||
|
params = {
|
||||||
|
'client_id': self.client_id,
|
||||||
|
'client_secret': self.client_secret,
|
||||||
|
'grant_type': 'authorization_code',
|
||||||
|
'code': code,
|
||||||
|
'redirect_uri': self.callback_url
|
||||||
|
}
|
||||||
|
rsp = self.do_post(self.TOKEN_URL, params)
|
||||||
|
|
||||||
|
obj = json.loads(rsp)
|
||||||
|
if 'access_token' in obj:
|
||||||
|
self.access_token = str(obj['access_token'])
|
||||||
|
self.openid = str(obj['uid'])
|
||||||
|
return self.get_oauth_userinfo()
|
||||||
|
else:
|
||||||
|
raise OAuthAccessTokenException(rsp)
|
||||||
|
|
||||||
|
def get_oauth_userinfo(self):
|
||||||
|
if not self.is_authorized:
|
||||||
|
return None
|
||||||
|
params = {
|
||||||
|
'uid': self.openid,
|
||||||
|
'access_token': self.access_token
|
||||||
|
}
|
||||||
|
rsp = self.do_get(self.API_URL, params)
|
||||||
|
try:
|
||||||
|
datas = json.loads(rsp)
|
||||||
|
user = OAuthUser()
|
||||||
|
user.metadata = rsp
|
||||||
|
user.picture = datas['avatar_large']
|
||||||
|
user.nickname = datas['screen_name']
|
||||||
|
user.openid = datas['id']
|
||||||
|
user.type = 'weibo'
|
||||||
|
user.token = self.access_token
|
||||||
|
if 'email' in datas and datas['email']:
|
||||||
|
user.email = datas['email']
|
||||||
|
return user
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
logger.error('weibo oauth error.rsp:' + rsp)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_picture(self, metadata):
|
||||||
|
datas = json.loads(metadata)
|
||||||
|
return datas['avatar_large']
|
||||||
|
|
||||||
|
|
||||||
|
class ProxyManagerMixin:
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
if os.environ.get("HTTP_PROXY"):
|
||||||
|
self.proxies = {
|
||||||
|
"http": os.environ.get("HTTP_PROXY"),
|
||||||
|
"https": os.environ.get("HTTP_PROXY")
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
self.proxies = None
|
||||||
|
|
||||||
|
def do_get(self, url, params, headers=None):
|
||||||
|
rsp = requests.get(url=url, params=params, headers=headers, proxies=self.proxies)
|
||||||
|
logger.info(rsp.text)
|
||||||
|
return rsp.text
|
||||||
|
|
||||||
|
def do_post(self, url, params, headers=None):
|
||||||
|
rsp = requests.post(url, params, headers=headers, proxies=self.proxies)
|
||||||
|
logger.info(rsp.text)
|
||||||
|
return rsp.text
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager):
|
||||||
|
AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth'
|
||||||
|
TOKEN_URL = 'https://www.googleapis.com/oauth2/v4/token'
|
||||||
|
API_URL = 'https://www.googleapis.com/oauth2/v3/userinfo'
|
||||||
|
ICON_NAME = 'google'
|
||||||
|
|
||||||
|
def __init__(self, access_token=None, openid=None):
|
||||||
|
config = self.get_config()
|
||||||
|
self.client_id = config.appkey if config else ''
|
||||||
|
self.client_secret = config.appsecret if config else ''
|
||||||
|
self.callback_url = config.callback_url if config else ''
|
||||||
|
super(
|
||||||
|
GoogleOauthManager,
|
||||||
|
self).__init__(
|
||||||
|
access_token=access_token,
|
||||||
|
openid=openid)
|
||||||
|
|
||||||
|
def get_authorization_url(self, nexturl='/'):
|
||||||
|
params = {
|
||||||
|
'client_id': self.client_id,
|
||||||
|
'response_type': 'code',
|
||||||
|
'redirect_uri': self.callback_url,
|
||||||
|
'scope': 'openid email',
|
||||||
|
}
|
||||||
|
url = self.AUTH_URL + "?" + urllib.parse.urlencode(params)
|
||||||
|
return url
|
||||||
|
|
||||||
|
def get_access_token_by_code(self, code):
|
||||||
|
params = {
|
||||||
|
'client_id': self.client_id,
|
||||||
|
'client_secret': self.client_secret,
|
||||||
|
'grant_type': 'authorization_code',
|
||||||
|
'code': code,
|
||||||
|
|
||||||
|
'redirect_uri': self.callback_url
|
||||||
|
}
|
||||||
|
rsp = self.do_post(self.TOKEN_URL, params)
|
||||||
|
|
||||||
|
obj = json.loads(rsp)
|
||||||
|
|
||||||
|
if 'access_token' in obj:
|
||||||
|
self.access_token = str(obj['access_token'])
|
||||||
|
self.openid = str(obj['id_token'])
|
||||||
|
logger.info(self.ICON_NAME + ' oauth ' + rsp)
|
||||||
|
return self.access_token
|
||||||
|
else:
|
||||||
|
raise OAuthAccessTokenException(rsp)
|
||||||
|
|
||||||
|
def get_oauth_userinfo(self):
|
||||||
|
if not self.is_authorized:
|
||||||
|
return None
|
||||||
|
params = {
|
||||||
|
'access_token': self.access_token
|
||||||
|
}
|
||||||
|
rsp = self.do_get(self.API_URL, params)
|
||||||
|
try:
|
||||||
|
|
||||||
|
datas = json.loads(rsp)
|
||||||
|
user = OAuthUser()
|
||||||
|
user.metadata = rsp
|
||||||
|
user.picture = datas['picture']
|
||||||
|
user.nickname = datas['name']
|
||||||
|
user.openid = datas['sub']
|
||||||
|
user.token = self.access_token
|
||||||
|
user.type = 'google'
|
||||||
|
if datas['email']:
|
||||||
|
user.email = datas['email']
|
||||||
|
return user
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
logger.error('google oauth error.rsp:' + rsp)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_picture(self, metadata):
|
||||||
|
datas = json.loads(metadata)
|
||||||
|
return datas['picture']
|
||||||
|
|
||||||
|
|
||||||
|
class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager):
|
||||||
|
AUTH_URL = 'https://github.com/login/oauth/authorize'
|
||||||
|
TOKEN_URL = 'https://github.com/login/oauth/access_token'
|
||||||
|
API_URL = 'https://api.github.com/user'
|
||||||
|
ICON_NAME = 'github'
|
||||||
|
|
||||||
|
def __init__(self, access_token=None, openid=None):
|
||||||
|
config = self.get_config()
|
||||||
|
self.client_id = config.appkey if config else ''
|
||||||
|
self.client_secret = config.appsecret if config else ''
|
||||||
|
self.callback_url = config.callback_url if config else ''
|
||||||
|
super(
|
||||||
|
GitHubOauthManager,
|
||||||
|
self).__init__(
|
||||||
|
access_token=access_token,
|
||||||
|
openid=openid)
|
||||||
|
|
||||||
|
def get_authorization_url(self, next_url='/'):
|
||||||
|
params = {
|
||||||
|
'client_id': self.client_id,
|
||||||
|
'response_type': 'code',
|
||||||
|
'redirect_uri': f'{self.callback_url}&next_url={next_url}',
|
||||||
|
'scope': 'user'
|
||||||
|
}
|
||||||
|
url = self.AUTH_URL + "?" + urllib.parse.urlencode(params)
|
||||||
|
return url
|
||||||
|
|
||||||
|
def get_access_token_by_code(self, code):
|
||||||
|
params = {
|
||||||
|
'client_id': self.client_id,
|
||||||
|
'client_secret': self.client_secret,
|
||||||
|
'grant_type': 'authorization_code',
|
||||||
|
'code': code,
|
||||||
|
|
||||||
|
'redirect_uri': self.callback_url
|
||||||
|
}
|
||||||
|
rsp = self.do_post(self.TOKEN_URL, params)
|
||||||
|
|
||||||
|
from urllib import parse
|
||||||
|
r = parse.parse_qs(rsp)
|
||||||
|
if 'access_token' in r:
|
||||||
|
self.access_token = (r['access_token'][0])
|
||||||
|
return self.access_token
|
||||||
|
else:
|
||||||
|
raise OAuthAccessTokenException(rsp)
|
||||||
|
|
||||||
|
def get_oauth_userinfo(self):
|
||||||
|
|
||||||
|
rsp = self.do_get(self.API_URL, params={}, headers={
|
||||||
|
"Authorization": "token " + self.access_token
|
||||||
|
})
|
||||||
|
try:
|
||||||
|
datas = json.loads(rsp)
|
||||||
|
user = OAuthUser()
|
||||||
|
user.picture = datas['avatar_url']
|
||||||
|
user.nickname = datas['name']
|
||||||
|
user.openid = datas['id']
|
||||||
|
user.type = 'github'
|
||||||
|
user.token = self.access_token
|
||||||
|
user.metadata = rsp
|
||||||
|
if 'email' in datas and datas['email']:
|
||||||
|
user.email = datas['email']
|
||||||
|
return user
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
logger.error('github oauth error.rsp:' + rsp)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_picture(self, metadata):
|
||||||
|
datas = json.loads(metadata)
|
||||||
|
return datas['avatar_url']
|
||||||
|
|
||||||
|
|
||||||
|
class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager):
|
||||||
|
AUTH_URL = 'https://www.facebook.com/v16.0/dialog/oauth'
|
||||||
|
TOKEN_URL = 'https://graph.facebook.com/v16.0/oauth/access_token'
|
||||||
|
API_URL = 'https://graph.facebook.com/me'
|
||||||
|
ICON_NAME = 'facebook'
|
||||||
|
|
||||||
|
def __init__(self, access_token=None, openid=None):
|
||||||
|
config = self.get_config()
|
||||||
|
self.client_id = config.appkey if config else ''
|
||||||
|
self.client_secret = config.appsecret if config else ''
|
||||||
|
self.callback_url = config.callback_url if config else ''
|
||||||
|
super(
|
||||||
|
FaceBookOauthManager,
|
||||||
|
self).__init__(
|
||||||
|
access_token=access_token,
|
||||||
|
openid=openid)
|
||||||
|
|
||||||
|
def get_authorization_url(self, next_url='/'):
|
||||||
|
params = {
|
||||||
|
'client_id': self.client_id,
|
||||||
|
'response_type': 'code',
|
||||||
|
'redirect_uri': self.callback_url,
|
||||||
|
'scope': 'email,public_profile'
|
||||||
|
}
|
||||||
|
url = self.AUTH_URL + "?" + urllib.parse.urlencode(params)
|
||||||
|
return url
|
||||||
|
|
||||||
|
def get_access_token_by_code(self, code):
|
||||||
|
params = {
|
||||||
|
'client_id': self.client_id,
|
||||||
|
'client_secret': self.client_secret,
|
||||||
|
# 'grant_type': 'authorization_code',
|
||||||
|
'code': code,
|
||||||
|
|
||||||
|
'redirect_uri': self.callback_url
|
||||||
|
}
|
||||||
|
rsp = self.do_post(self.TOKEN_URL, params)
|
||||||
|
|
||||||
|
obj = json.loads(rsp)
|
||||||
|
if 'access_token' in obj:
|
||||||
|
token = str(obj['access_token'])
|
||||||
|
self.access_token = token
|
||||||
|
return self.access_token
|
||||||
|
else:
|
||||||
|
raise OAuthAccessTokenException(rsp)
|
||||||
|
|
||||||
|
def get_oauth_userinfo(self):
|
||||||
|
params = {
|
||||||
|
'access_token': self.access_token,
|
||||||
|
'fields': 'id,name,picture,email'
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
rsp = self.do_get(self.API_URL, params)
|
||||||
|
datas = json.loads(rsp)
|
||||||
|
user = OAuthUser()
|
||||||
|
user.nickname = datas['name']
|
||||||
|
user.openid = datas['id']
|
||||||
|
user.type = 'facebook'
|
||||||
|
user.token = self.access_token
|
||||||
|
user.metadata = rsp
|
||||||
|
if 'email' in datas and datas['email']:
|
||||||
|
user.email = datas['email']
|
||||||
|
if 'picture' in datas and datas['picture'] and datas['picture']['data'] and datas['picture']['data']['url']:
|
||||||
|
user.picture = str(datas['picture']['data']['url'])
|
||||||
|
return user
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_picture(self, metadata):
|
||||||
|
datas = json.loads(metadata)
|
||||||
|
return str(datas['picture']['data']['url'])
|
||||||
|
|
||||||
|
|
||||||
|
class QQOauthManager(BaseOauthManager):
|
||||||
|
AUTH_URL = 'https://graph.qq.com/oauth2.0/authorize'
|
||||||
|
TOKEN_URL = 'https://graph.qq.com/oauth2.0/token'
|
||||||
|
API_URL = 'https://graph.qq.com/user/get_user_info'
|
||||||
|
OPEN_ID_URL = 'https://graph.qq.com/oauth2.0/me'
|
||||||
|
ICON_NAME = 'qq'
|
||||||
|
|
||||||
|
def __init__(self, access_token=None, openid=None):
|
||||||
|
config = self.get_config()
|
||||||
|
self.client_id = config.appkey if config else ''
|
||||||
|
self.client_secret = config.appsecret if config else ''
|
||||||
|
self.callback_url = config.callback_url if config else ''
|
||||||
|
super(
|
||||||
|
QQOauthManager,
|
||||||
|
self).__init__(
|
||||||
|
access_token=access_token,
|
||||||
|
openid=openid)
|
||||||
|
|
||||||
|
def get_authorization_url(self, next_url='/'):
|
||||||
|
params = {
|
||||||
|
'response_type': 'code',
|
||||||
|
'client_id': self.client_id,
|
||||||
|
'redirect_uri': self.callback_url + '&next_url=' + next_url,
|
||||||
|
}
|
||||||
|
url = self.AUTH_URL + "?" + urllib.parse.urlencode(params)
|
||||||
|
return url
|
||||||
|
|
||||||
|
def get_access_token_by_code(self, code):
|
||||||
|
params = {
|
||||||
|
'grant_type': 'authorization_code',
|
||||||
|
'client_id': self.client_id,
|
||||||
|
'client_secret': self.client_secret,
|
||||||
|
'code': code,
|
||||||
|
'redirect_uri': self.callback_url
|
||||||
|
}
|
||||||
|
rsp = self.do_get(self.TOKEN_URL, params)
|
||||||
|
if rsp:
|
||||||
|
d = urllib.parse.parse_qs(rsp)
|
||||||
|
if 'access_token' in d:
|
||||||
|
token = d['access_token']
|
||||||
|
self.access_token = token[0]
|
||||||
|
return token
|
||||||
|
else:
|
||||||
|
raise OAuthAccessTokenException(rsp)
|
||||||
|
|
||||||
|
def get_open_id(self):
|
||||||
|
if self.is_access_token_set:
|
||||||
|
params = {
|
||||||
|
'access_token': self.access_token
|
||||||
|
}
|
||||||
|
rsp = self.do_get(self.OPEN_ID_URL, params)
|
||||||
|
if rsp:
|
||||||
|
rsp = rsp.replace(
|
||||||
|
'callback(', '').replace(
|
||||||
|
')', '').replace(
|
||||||
|
';', '')
|
||||||
|
obj = json.loads(rsp)
|
||||||
|
openid = str(obj['openid'])
|
||||||
|
self.openid = openid
|
||||||
|
return openid
|
||||||
|
|
||||||
|
def get_oauth_userinfo(self):
|
||||||
|
openid = self.get_open_id()
|
||||||
|
if openid:
|
||||||
|
params = {
|
||||||
|
'access_token': self.access_token,
|
||||||
|
'oauth_consumer_key': self.client_id,
|
||||||
|
'openid': self.openid
|
||||||
|
}
|
||||||
|
rsp = self.do_get(self.API_URL, params)
|
||||||
|
logger.info(rsp)
|
||||||
|
obj = json.loads(rsp)
|
||||||
|
user = OAuthUser()
|
||||||
|
user.nickname = obj['nickname']
|
||||||
|
user.openid = openid
|
||||||
|
user.type = 'qq'
|
||||||
|
user.token = self.access_token
|
||||||
|
user.metadata = rsp
|
||||||
|
if 'email' in obj:
|
||||||
|
user.email = obj['email']
|
||||||
|
if 'figureurl' in obj:
|
||||||
|
user.picture = str(obj['figureurl'])
|
||||||
|
return user
|
||||||
|
|
||||||
|
def get_picture(self, metadata):
|
||||||
|
datas = json.loads(metadata)
|
||||||
|
return str(datas['figureurl'])
|
||||||
|
|
||||||
|
|
||||||
|
@cache_decorator(expiration=100 * 60)
|
||||||
|
def get_oauth_apps():
|
||||||
|
configs = OAuthConfig.objects.filter(is_enable=True).all()
|
||||||
|
if not configs:
|
||||||
|
return []
|
||||||
|
configtypes = [x.type for x in configs]
|
||||||
|
applications = BaseOauthManager.__subclasses__()
|
||||||
|
apps = [x() for x in applications if x().ICON_NAME.lower() in configtypes]
|
||||||
|
return apps
|
||||||
|
|
||||||
|
|
||||||
|
def get_manager_by_type(type):
|
||||||
|
applications = get_oauth_apps()
|
||||||
|
if applications:
|
||||||
|
finds = list(
|
||||||
|
filter(
|
||||||
|
lambda x: x.ICON_NAME.lower() == type.lower(),
|
||||||
|
applications))
|
||||||
|
if finds:
|
||||||
|
return finds[0]
|
||||||
|
return None
|
||||||
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
from django import template
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from oauth.oauthmanager import get_oauth_apps
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.inclusion_tag('oauth/oauth_applications.html')
|
||||||
|
def load_oauth_applications(request):
|
||||||
|
applications = get_oauth_apps()
|
||||||
|
if applications:
|
||||||
|
baseurl = reverse('oauth:oauthlogin')
|
||||||
|
path = request.get_full_path()
|
||||||
|
|
||||||
|
apps = list(map(lambda x: (x.ICON_NAME, '{baseurl}?type={type}&next_url={next}'.format(
|
||||||
|
baseurl=baseurl, type=x.ICON_NAME, next=path)), applications))
|
||||||
|
else:
|
||||||
|
apps = []
|
||||||
|
return {
|
||||||
|
'apps': apps
|
||||||
|
}
|
||||||
@ -0,0 +1,249 @@
|
|||||||
|
import json
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib import auth
|
||||||
|
from django.test import Client, RequestFactory, TestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from djangoblog.utils import get_sha256
|
||||||
|
from oauth.models import OAuthConfig
|
||||||
|
from oauth.oauthmanager import BaseOauthManager
|
||||||
|
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
|
class OAuthConfigTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.client = Client()
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
|
||||||
|
def test_oauth_login_test(self):
|
||||||
|
c = OAuthConfig()
|
||||||
|
c.type = 'weibo'
|
||||||
|
c.appkey = 'appkey'
|
||||||
|
c.appsecret = 'appsecret'
|
||||||
|
c.save()
|
||||||
|
|
||||||
|
response = self.client.get('/oauth/oauthlogin?type=weibo')
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertTrue("api.weibo.com" in response.url)
|
||||||
|
|
||||||
|
response = self.client.get('/oauth/authorize?type=weibo&code=code')
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, '/')
|
||||||
|
|
||||||
|
|
||||||
|
class OauthLoginTest(TestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.client = Client()
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
self.apps = self.init_apps()
|
||||||
|
|
||||||
|
def init_apps(self):
|
||||||
|
applications = [p() for p in BaseOauthManager.__subclasses__()]
|
||||||
|
for application in applications:
|
||||||
|
c = OAuthConfig()
|
||||||
|
c.type = application.ICON_NAME.lower()
|
||||||
|
c.appkey = 'appkey'
|
||||||
|
c.appsecret = 'appsecret'
|
||||||
|
c.save()
|
||||||
|
return applications
|
||||||
|
|
||||||
|
def get_app_by_type(self, type):
|
||||||
|
for app in self.apps:
|
||||||
|
if app.ICON_NAME.lower() == type:
|
||||||
|
return app
|
||||||
|
|
||||||
|
@patch("oauth.oauthmanager.WBOauthManager.do_post")
|
||||||
|
@patch("oauth.oauthmanager.WBOauthManager.do_get")
|
||||||
|
def test_weibo_login(self, mock_do_get, mock_do_post):
|
||||||
|
weibo_app = self.get_app_by_type('weibo')
|
||||||
|
assert weibo_app
|
||||||
|
url = weibo_app.get_authorization_url()
|
||||||
|
mock_do_post.return_value = json.dumps({"access_token": "access_token",
|
||||||
|
"uid": "uid"
|
||||||
|
})
|
||||||
|
mock_do_get.return_value = json.dumps({
|
||||||
|
"avatar_large": "avatar_large",
|
||||||
|
"screen_name": "screen_name",
|
||||||
|
"id": "id",
|
||||||
|
"email": "email",
|
||||||
|
})
|
||||||
|
userinfo = weibo_app.get_access_token_by_code('code')
|
||||||
|
self.assertEqual(userinfo.token, 'access_token')
|
||||||
|
self.assertEqual(userinfo.openid, 'id')
|
||||||
|
|
||||||
|
@patch("oauth.oauthmanager.GoogleOauthManager.do_post")
|
||||||
|
@patch("oauth.oauthmanager.GoogleOauthManager.do_get")
|
||||||
|
def test_google_login(self, mock_do_get, mock_do_post):
|
||||||
|
google_app = self.get_app_by_type('google')
|
||||||
|
assert google_app
|
||||||
|
url = google_app.get_authorization_url()
|
||||||
|
mock_do_post.return_value = json.dumps({
|
||||||
|
"access_token": "access_token",
|
||||||
|
"id_token": "id_token",
|
||||||
|
})
|
||||||
|
mock_do_get.return_value = json.dumps({
|
||||||
|
"picture": "picture",
|
||||||
|
"name": "name",
|
||||||
|
"sub": "sub",
|
||||||
|
"email": "email",
|
||||||
|
})
|
||||||
|
token = google_app.get_access_token_by_code('code')
|
||||||
|
userinfo = google_app.get_oauth_userinfo()
|
||||||
|
self.assertEqual(userinfo.token, 'access_token')
|
||||||
|
self.assertEqual(userinfo.openid, 'sub')
|
||||||
|
|
||||||
|
@patch("oauth.oauthmanager.GitHubOauthManager.do_post")
|
||||||
|
@patch("oauth.oauthmanager.GitHubOauthManager.do_get")
|
||||||
|
def test_github_login(self, mock_do_get, mock_do_post):
|
||||||
|
github_app = self.get_app_by_type('github')
|
||||||
|
assert github_app
|
||||||
|
url = github_app.get_authorization_url()
|
||||||
|
self.assertTrue("github.com" in url)
|
||||||
|
self.assertTrue("client_id" in url)
|
||||||
|
mock_do_post.return_value = "access_token=gho_16C7e42F292c6912E7710c838347Ae178B4a&scope=repo%2Cgist&token_type=bearer"
|
||||||
|
mock_do_get.return_value = json.dumps({
|
||||||
|
"avatar_url": "avatar_url",
|
||||||
|
"name": "name",
|
||||||
|
"id": "id",
|
||||||
|
"email": "email",
|
||||||
|
})
|
||||||
|
token = github_app.get_access_token_by_code('code')
|
||||||
|
userinfo = github_app.get_oauth_userinfo()
|
||||||
|
self.assertEqual(userinfo.token, 'gho_16C7e42F292c6912E7710c838347Ae178B4a')
|
||||||
|
self.assertEqual(userinfo.openid, 'id')
|
||||||
|
|
||||||
|
@patch("oauth.oauthmanager.FaceBookOauthManager.do_post")
|
||||||
|
@patch("oauth.oauthmanager.FaceBookOauthManager.do_get")
|
||||||
|
def test_facebook_login(self, mock_do_get, mock_do_post):
|
||||||
|
facebook_app = self.get_app_by_type('facebook')
|
||||||
|
assert facebook_app
|
||||||
|
url = facebook_app.get_authorization_url()
|
||||||
|
self.assertTrue("facebook.com" in url)
|
||||||
|
mock_do_post.return_value = json.dumps({
|
||||||
|
"access_token": "access_token",
|
||||||
|
})
|
||||||
|
mock_do_get.return_value = json.dumps({
|
||||||
|
"name": "name",
|
||||||
|
"id": "id",
|
||||||
|
"email": "email",
|
||||||
|
"picture": {
|
||||||
|
"data": {
|
||||||
|
"url": "url"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
token = facebook_app.get_access_token_by_code('code')
|
||||||
|
userinfo = facebook_app.get_oauth_userinfo()
|
||||||
|
self.assertEqual(userinfo.token, 'access_token')
|
||||||
|
|
||||||
|
@patch("oauth.oauthmanager.QQOauthManager.do_get", side_effect=[
|
||||||
|
'access_token=access_token&expires_in=3600',
|
||||||
|
'callback({"client_id":"appid","openid":"openid"} );',
|
||||||
|
json.dumps({
|
||||||
|
"nickname": "nickname",
|
||||||
|
"email": "email",
|
||||||
|
"figureurl": "figureurl",
|
||||||
|
"openid": "openid",
|
||||||
|
})
|
||||||
|
])
|
||||||
|
def test_qq_login(self, mock_do_get):
|
||||||
|
qq_app = self.get_app_by_type('qq')
|
||||||
|
assert qq_app
|
||||||
|
url = qq_app.get_authorization_url()
|
||||||
|
self.assertTrue("qq.com" in url)
|
||||||
|
token = qq_app.get_access_token_by_code('code')
|
||||||
|
userinfo = qq_app.get_oauth_userinfo()
|
||||||
|
self.assertEqual(userinfo.token, 'access_token')
|
||||||
|
|
||||||
|
@patch("oauth.oauthmanager.WBOauthManager.do_post")
|
||||||
|
@patch("oauth.oauthmanager.WBOauthManager.do_get")
|
||||||
|
def test_weibo_authoriz_login_with_email(self, mock_do_get, mock_do_post):
|
||||||
|
|
||||||
|
mock_do_post.return_value = json.dumps({"access_token": "access_token",
|
||||||
|
"uid": "uid"
|
||||||
|
})
|
||||||
|
mock_user_info = {
|
||||||
|
"avatar_large": "avatar_large",
|
||||||
|
"screen_name": "screen_name1",
|
||||||
|
"id": "id",
|
||||||
|
"email": "email",
|
||||||
|
}
|
||||||
|
mock_do_get.return_value = json.dumps(mock_user_info)
|
||||||
|
|
||||||
|
response = self.client.get('/oauth/oauthlogin?type=weibo')
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertTrue("api.weibo.com" in response.url)
|
||||||
|
|
||||||
|
response = self.client.get('/oauth/authorize?type=weibo&code=code')
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, '/')
|
||||||
|
|
||||||
|
user = auth.get_user(self.client)
|
||||||
|
assert user.is_authenticated
|
||||||
|
self.assertTrue(user.is_authenticated)
|
||||||
|
self.assertEqual(user.username, mock_user_info['screen_name'])
|
||||||
|
self.assertEqual(user.email, mock_user_info['email'])
|
||||||
|
self.client.logout()
|
||||||
|
|
||||||
|
response = self.client.get('/oauth/authorize?type=weibo&code=code')
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, '/')
|
||||||
|
|
||||||
|
user = auth.get_user(self.client)
|
||||||
|
assert user.is_authenticated
|
||||||
|
self.assertTrue(user.is_authenticated)
|
||||||
|
self.assertEqual(user.username, mock_user_info['screen_name'])
|
||||||
|
self.assertEqual(user.email, mock_user_info['email'])
|
||||||
|
|
||||||
|
@patch("oauth.oauthmanager.WBOauthManager.do_post")
|
||||||
|
@patch("oauth.oauthmanager.WBOauthManager.do_get")
|
||||||
|
def test_weibo_authoriz_login_without_email(self, mock_do_get, mock_do_post):
|
||||||
|
|
||||||
|
mock_do_post.return_value = json.dumps({"access_token": "access_token",
|
||||||
|
"uid": "uid"
|
||||||
|
})
|
||||||
|
mock_user_info = {
|
||||||
|
"avatar_large": "avatar_large",
|
||||||
|
"screen_name": "screen_name1",
|
||||||
|
"id": "id",
|
||||||
|
}
|
||||||
|
mock_do_get.return_value = json.dumps(mock_user_info)
|
||||||
|
|
||||||
|
response = self.client.get('/oauth/oauthlogin?type=weibo')
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertTrue("api.weibo.com" in response.url)
|
||||||
|
|
||||||
|
response = self.client.get('/oauth/authorize?type=weibo&code=code')
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
|
oauth_user_id = int(response.url.split('/')[-1].split('.')[0])
|
||||||
|
self.assertEqual(response.url, f'/oauth/requireemail/{oauth_user_id}.html')
|
||||||
|
|
||||||
|
response = self.client.post(response.url, {'email': 'test@gmail.com', 'oauthid': oauth_user_id})
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
sign = get_sha256(settings.SECRET_KEY +
|
||||||
|
str(oauth_user_id) + settings.SECRET_KEY)
|
||||||
|
|
||||||
|
url = reverse('oauth:bindsuccess', kwargs={
|
||||||
|
'oauthid': oauth_user_id,
|
||||||
|
})
|
||||||
|
self.assertEqual(response.url, f'{url}?type=email')
|
||||||
|
|
||||||
|
path = reverse('oauth:email_confirm', kwargs={
|
||||||
|
'id': oauth_user_id,
|
||||||
|
'sign': sign
|
||||||
|
})
|
||||||
|
response = self.client.get(path)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, f'/oauth/bindsuccess/{oauth_user_id}.html?type=success')
|
||||||
|
user = auth.get_user(self.client)
|
||||||
|
from oauth.models import OAuthUser
|
||||||
|
oauth_user = OAuthUser.objects.get(author=user)
|
||||||
|
self.assertTrue(user.is_authenticated)
|
||||||
|
self.assertEqual(user.username, mock_user_info['screen_name'])
|
||||||
|
self.assertEqual(user.email, 'test@gmail.com')
|
||||||
|
self.assertEqual(oauth_user.pk, oauth_user_id)
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = "oauth"
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
r'oauth/authorize',
|
||||||
|
views.authorize),
|
||||||
|
path(
|
||||||
|
r'oauth/requireemail/<int:oauthid>.html',
|
||||||
|
views.RequireEmailView.as_view(),
|
||||||
|
name='require_email'),
|
||||||
|
path(
|
||||||
|
r'oauth/emailconfirm/<int:id>/<sign>.html',
|
||||||
|
views.emailconfirm,
|
||||||
|
name='email_confirm'),
|
||||||
|
path(
|
||||||
|
r'oauth/bindsuccess/<int:oauthid>.html',
|
||||||
|
views.bindsuccess,
|
||||||
|
name='bindsuccess'),
|
||||||
|
path(
|
||||||
|
r'oauth/oauthlogin',
|
||||||
|
views.oauthlogin,
|
||||||
|
name='oauthlogin')]
|
||||||
@ -0,0 +1,253 @@
|
|||||||
|
import logging
|
||||||
|
# Create your views here.
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth import login
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.db import transaction
|
||||||
|
from django.http import HttpResponseForbidden
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.shortcuts import render
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.views.generic import FormView
|
||||||
|
|
||||||
|
from djangoblog.blog_signals import oauth_user_login_signal
|
||||||
|
from djangoblog.utils import get_current_site
|
||||||
|
from djangoblog.utils import send_email, get_sha256
|
||||||
|
from oauth.forms import RequireEmailForm
|
||||||
|
from .models import OAuthUser
|
||||||
|
from .oauthmanager import get_manager_by_type, OAuthAccessTokenException
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_redirecturl(request):
|
||||||
|
nexturl = request.GET.get('next_url', None)
|
||||||
|
if not nexturl or nexturl == '/login/' or nexturl == '/login':
|
||||||
|
nexturl = '/'
|
||||||
|
return nexturl
|
||||||
|
p = urlparse(nexturl)
|
||||||
|
if p.netloc:
|
||||||
|
site = get_current_site().domain
|
||||||
|
if not p.netloc.replace('www.', '') == site.replace('www.', ''):
|
||||||
|
logger.info('非法url:' + nexturl)
|
||||||
|
return "/"
|
||||||
|
return nexturl
|
||||||
|
|
||||||
|
|
||||||
|
def oauthlogin(request):
|
||||||
|
type = request.GET.get('type', None)
|
||||||
|
if not type:
|
||||||
|
return HttpResponseRedirect('/')
|
||||||
|
manager = get_manager_by_type(type)
|
||||||
|
if not manager:
|
||||||
|
return HttpResponseRedirect('/')
|
||||||
|
nexturl = get_redirecturl(request)
|
||||||
|
authorizeurl = manager.get_authorization_url(nexturl)
|
||||||
|
return HttpResponseRedirect(authorizeurl)
|
||||||
|
|
||||||
|
|
||||||
|
def authorize(request):
|
||||||
|
type = request.GET.get('type', None)
|
||||||
|
if not type:
|
||||||
|
return HttpResponseRedirect('/')
|
||||||
|
manager = get_manager_by_type(type)
|
||||||
|
if not manager:
|
||||||
|
return HttpResponseRedirect('/')
|
||||||
|
code = request.GET.get('code', None)
|
||||||
|
try:
|
||||||
|
rsp = manager.get_access_token_by_code(code)
|
||||||
|
except OAuthAccessTokenException as e:
|
||||||
|
logger.warning("OAuthAccessTokenException:" + str(e))
|
||||||
|
return HttpResponseRedirect('/')
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
rsp = None
|
||||||
|
nexturl = get_redirecturl(request)
|
||||||
|
if not rsp:
|
||||||
|
return HttpResponseRedirect(manager.get_authorization_url(nexturl))
|
||||||
|
user = manager.get_oauth_userinfo()
|
||||||
|
if user:
|
||||||
|
if not user.nickname or not user.nickname.strip():
|
||||||
|
user.nickname = "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S')
|
||||||
|
try:
|
||||||
|
temp = OAuthUser.objects.get(type=type, openid=user.openid)
|
||||||
|
temp.picture = user.picture
|
||||||
|
temp.metadata = user.metadata
|
||||||
|
temp.nickname = user.nickname
|
||||||
|
user = temp
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
pass
|
||||||
|
# facebook的token过长
|
||||||
|
if type == 'facebook':
|
||||||
|
user.token = ''
|
||||||
|
if user.email:
|
||||||
|
with transaction.atomic():
|
||||||
|
author = None
|
||||||
|
try:
|
||||||
|
author = get_user_model().objects.get(id=user.author_id)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
pass
|
||||||
|
if not author:
|
||||||
|
result = get_user_model().objects.get_or_create(email=user.email)
|
||||||
|
author = result[0]
|
||||||
|
if result[1]:
|
||||||
|
try:
|
||||||
|
get_user_model().objects.get(username=user.nickname)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
author.username = user.nickname
|
||||||
|
else:
|
||||||
|
author.username = "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S')
|
||||||
|
author.source = 'authorize'
|
||||||
|
author.save()
|
||||||
|
|
||||||
|
user.author = author
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
oauth_user_login_signal.send(
|
||||||
|
sender=authorize.__class__, id=user.id)
|
||||||
|
login(request, author)
|
||||||
|
return HttpResponseRedirect(nexturl)
|
||||||
|
else:
|
||||||
|
user.save()
|
||||||
|
url = reverse('oauth:require_email', kwargs={
|
||||||
|
'oauthid': user.id
|
||||||
|
})
|
||||||
|
|
||||||
|
return HttpResponseRedirect(url)
|
||||||
|
else:
|
||||||
|
return HttpResponseRedirect(nexturl)
|
||||||
|
|
||||||
|
|
||||||
|
def emailconfirm(request, id, sign):
|
||||||
|
if not sign:
|
||||||
|
return HttpResponseForbidden()
|
||||||
|
if not get_sha256(settings.SECRET_KEY +
|
||||||
|
str(id) +
|
||||||
|
settings.SECRET_KEY).upper() == sign.upper():
|
||||||
|
return HttpResponseForbidden()
|
||||||
|
oauthuser = get_object_or_404(OAuthUser, pk=id)
|
||||||
|
with transaction.atomic():
|
||||||
|
if oauthuser.author:
|
||||||
|
author = get_user_model().objects.get(pk=oauthuser.author_id)
|
||||||
|
else:
|
||||||
|
result = get_user_model().objects.get_or_create(email=oauthuser.email)
|
||||||
|
author = result[0]
|
||||||
|
if result[1]:
|
||||||
|
author.source = 'emailconfirm'
|
||||||
|
author.username = oauthuser.nickname.strip() if oauthuser.nickname.strip(
|
||||||
|
) else "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S')
|
||||||
|
author.save()
|
||||||
|
oauthuser.author = author
|
||||||
|
oauthuser.save()
|
||||||
|
oauth_user_login_signal.send(
|
||||||
|
sender=emailconfirm.__class__,
|
||||||
|
id=oauthuser.id)
|
||||||
|
login(request, author)
|
||||||
|
|
||||||
|
site = 'http://' + get_current_site().domain
|
||||||
|
content = _('''
|
||||||
|
<p>Congratulations, you have successfully bound your email address. You can use
|
||||||
|
%(oauthuser_type)s to directly log in to this website without a password.</p>
|
||||||
|
You are welcome to continue to follow this site, the address is
|
||||||
|
<a href="%(site)s" rel="bookmark">%(site)s</a>
|
||||||
|
Thank you again!
|
||||||
|
<br />
|
||||||
|
If the link above cannot be opened, please copy this link to your browser.
|
||||||
|
%(site)s
|
||||||
|
''') % {'oauthuser_type': oauthuser.type, 'site': site}
|
||||||
|
|
||||||
|
send_email(emailto=[oauthuser.email, ], title=_('Congratulations on your successful binding!'), content=content)
|
||||||
|
url = reverse('oauth:bindsuccess', kwargs={
|
||||||
|
'oauthid': id
|
||||||
|
})
|
||||||
|
url = url + '?type=success'
|
||||||
|
return HttpResponseRedirect(url)
|
||||||
|
|
||||||
|
|
||||||
|
class RequireEmailView(FormView):
|
||||||
|
form_class = RequireEmailForm
|
||||||
|
template_name = 'oauth/require_email.html'
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
oauthid = self.kwargs['oauthid']
|
||||||
|
oauthuser = get_object_or_404(OAuthUser, pk=oauthid)
|
||||||
|
if oauthuser.email:
|
||||||
|
pass
|
||||||
|
# return HttpResponseRedirect('/')
|
||||||
|
|
||||||
|
return super(RequireEmailView, self).get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
oauthid = self.kwargs['oauthid']
|
||||||
|
return {
|
||||||
|
'email': '',
|
||||||
|
'oauthid': oauthid
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
oauthid = self.kwargs['oauthid']
|
||||||
|
oauthuser = get_object_or_404(OAuthUser, pk=oauthid)
|
||||||
|
if oauthuser.picture:
|
||||||
|
kwargs['picture'] = oauthuser.picture
|
||||||
|
return super(RequireEmailView, self).get_context_data(**kwargs)
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
email = form.cleaned_data['email']
|
||||||
|
oauthid = form.cleaned_data['oauthid']
|
||||||
|
oauthuser = get_object_or_404(OAuthUser, pk=oauthid)
|
||||||
|
oauthuser.email = email
|
||||||
|
oauthuser.save()
|
||||||
|
sign = get_sha256(settings.SECRET_KEY +
|
||||||
|
str(oauthuser.id) + settings.SECRET_KEY)
|
||||||
|
site = get_current_site().domain
|
||||||
|
if settings.DEBUG:
|
||||||
|
site = '127.0.0.1:8000'
|
||||||
|
path = reverse('oauth:email_confirm', kwargs={
|
||||||
|
'id': oauthid,
|
||||||
|
'sign': sign
|
||||||
|
})
|
||||||
|
url = "http://{site}{path}".format(site=site, path=path)
|
||||||
|
|
||||||
|
content = _("""
|
||||||
|
<p>Please click the link below to bind your email</p>
|
||||||
|
|
||||||
|
<a href="%(url)s" rel="bookmark">%(url)s</a>
|
||||||
|
|
||||||
|
Thank you again!
|
||||||
|
<br />
|
||||||
|
If the link above cannot be opened, please copy this link to your browser.
|
||||||
|
<br />
|
||||||
|
%(url)s
|
||||||
|
""") % {'url': url}
|
||||||
|
send_email(emailto=[email, ], title=_('Bind your email'), content=content)
|
||||||
|
url = reverse('oauth:bindsuccess', kwargs={
|
||||||
|
'oauthid': oauthid
|
||||||
|
})
|
||||||
|
url = url + '?type=email'
|
||||||
|
return HttpResponseRedirect(url)
|
||||||
|
|
||||||
|
|
||||||
|
def bindsuccess(request, oauthid):
|
||||||
|
type = request.GET.get('type', None)
|
||||||
|
oauthuser = get_object_or_404(OAuthUser, pk=oauthid)
|
||||||
|
if type == 'email':
|
||||||
|
title = _('Bind your email')
|
||||||
|
content = _(
|
||||||
|
'Congratulations, the binding is just one step away. '
|
||||||
|
'Please log in to your email to check the email to complete the binding. Thank you.')
|
||||||
|
else:
|
||||||
|
title = _('Binding successful')
|
||||||
|
content = _(
|
||||||
|
"Congratulations, you have successfully bound your email address. You can use %(oauthuser_type)s"
|
||||||
|
" to directly log in to this website without a password. You are welcome to continue to follow this site." % {
|
||||||
|
'oauthuser_type': oauthuser.type})
|
||||||
|
return render(request, 'oauth/bindsuccess.html', {
|
||||||
|
'title': title,
|
||||||
|
'content': content
|
||||||
|
})
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue