first commit
18
.gitignore
vendored
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# VS Code configuration files
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# flake8
|
||||||
|
tox.ini
|
||||||
|
|
||||||
|
# ignore migrations for now
|
||||||
|
migrations/
|
||||||
|
|
||||||
|
# docs/
|
||||||
|
|
||||||
|
# Local sqlite3 db
|
||||||
|
*.sqlite3
|
0
boofilsic/__init__.py
Normal file
16
boofilsic/asgi.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
ASGI config for boofilsic project.
|
||||||
|
|
||||||
|
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'boofilsic.settings')
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
169
boofilsic/settings.py
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
"""
|
||||||
|
Django settings for boofilsic project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 3.0.5.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/3.0/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/3.0/ref/settings/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import psycopg2.extensions
|
||||||
|
|
||||||
|
|
||||||
|
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||||
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
|
||||||
|
# Quick-start development settings - unsuitable for production
|
||||||
|
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = '***REMOVED***'
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = ['*']
|
||||||
|
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
'django.contrib.admin',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
'markdownx',
|
||||||
|
'users.apps.UsersConfig',
|
||||||
|
'common.apps.CommonConfig',
|
||||||
|
'books.apps.BooksConfig',
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'boofilsic.urls'
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': [],
|
||||||
|
'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',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = 'boofilsic.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
|
||||||
|
|
||||||
|
if DEBUG:
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.postgresql',
|
||||||
|
'NAME': 'test',
|
||||||
|
'USER': 'donotban',
|
||||||
|
'PASSWORD': 'donotbansilvousplait',
|
||||||
|
'HOST': '192.168.136.5',
|
||||||
|
'OPTIONS': {
|
||||||
|
'client_encoding': 'UTF8',
|
||||||
|
# 'isolation_level': psycopg2.extensions.ISOLATION_LEVEL_DEFAULT,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.postgresql',
|
||||||
|
'NAME': 'boofilsic',
|
||||||
|
'USER': 'doubaniux',
|
||||||
|
'PASSWORD': 'password',
|
||||||
|
'HOST': 'localhost',
|
||||||
|
'OPTIONS': {
|
||||||
|
'client_encoding': 'UTF8',
|
||||||
|
# 'isolation_level': psycopg2.extensions.ISOLATION_LEVEL_DEFAULT,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Customized auth backend, glue OAuth2 and Django User model together
|
||||||
|
# https://docs.djangoproject.com/en/3.0/topics/auth/customizing/#authentication-backends
|
||||||
|
|
||||||
|
AUTHENTICATION_BACKENDS = [
|
||||||
|
'users.auth.OAuth2Backend',
|
||||||
|
# for admin to login admin site
|
||||||
|
# 'django.contrib.auth.backends.ModelBackend'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/3.0/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = 'zh-hans'
|
||||||
|
|
||||||
|
TIME_ZONE = 'Asia/Shanghai'
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_L10N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/3.0/howto/static-files/
|
||||||
|
|
||||||
|
STATIC_URL = '/static/'
|
||||||
|
STATIC_ROOT = ''
|
||||||
|
|
||||||
|
AUTH_USER_MODEL = 'users.User'
|
||||||
|
|
||||||
|
MEDIA_URL = '/media/'
|
||||||
|
MEDIA_ROOT = 'E:\\temp'
|
||||||
|
|
||||||
|
CLIENT_ID = '3U57sjR7uvCu8suyFlp-fiBVj9-pKt3-jd7F2gLF6EE'
|
||||||
|
CLIENT_SECRET = 'HZohdI-xR8lUyTs_bM0G3l9Na0W6bZ6DfMK3b84_E0g'
|
||||||
|
|
||||||
|
# Path to save report related images, ends without slash
|
||||||
|
REPORT_MEDIA_PATH_ROOT = 'report/'
|
||||||
|
MARKDOWNX_MEDIA_PATH = 'review/'
|
||||||
|
BOOK_MEDIA_PATH_ROOT = 'book/'
|
||||||
|
DEFAULT_BOOK_IMAGE = os.path.join(MEDIA_ROOT, BOOK_MEDIA_PATH_ROOT, 'default.jpg')
|
||||||
|
|
||||||
|
# Mastodon domain name
|
||||||
|
MASTODON_DOMAIN_NAME = 'cmx-im.work'
|
||||||
|
|
||||||
|
# Default password for each user. since assword is not used any way,
|
||||||
|
# any string that is not empty is ok
|
||||||
|
DEFAULT_PASSWORD = 'eBRM1DETkYgiqPgq'
|
||||||
|
|
||||||
|
# Default redirect loaction when access login required view
|
||||||
|
LOGIN_URL = '/users/login/'
|
||||||
|
|
||||||
|
# Admin site root url
|
||||||
|
ADMIN_URL = 'lpLuTqX72Bt2hLfxxRYKeTZdE59Y2hLfpLuTqX72BtxxResXulIui1ayY2hLfpLuTqX72BtxxRejYKej1aNejYKeTZdE59sXuljYKej1aN1ZdE59sXulINSGMXTY9IIui1ayY2hLfxxRejYKej1aN1ZdE59sXulINSGMXTY9ID4tYEmjrHd'
|
||||||
|
|
||||||
|
# https://django-debug-toolbar.readthedocs.io/en/latest/
|
||||||
|
# maybe benchmarking before deployment
|
29
boofilsic/urls.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
"""boofilsic URL Configuration
|
||||||
|
|
||||||
|
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||||
|
https://docs.djangoproject.com/en/3.0/topics/http/urls/
|
||||||
|
Examples:
|
||||||
|
Function views
|
||||||
|
1. Add an import: from my_app import views
|
||||||
|
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||||
|
Class-based views
|
||||||
|
1. Add an import: from other_app.views import Home
|
||||||
|
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||||
|
Including another URLconf
|
||||||
|
1. Import the include() function: from django.urls import include, path
|
||||||
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
|
"""
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import path, include
|
||||||
|
from django.conf import settings
|
||||||
|
from users.views import login
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(settings.ADMIN_URL + '/', admin.site.urls),
|
||||||
|
path('login/', login),
|
||||||
|
path('markdownx/', include('markdownx.urls')),
|
||||||
|
path('users/', include('users.urls')),
|
||||||
|
path('books/', include('books.urls')),
|
||||||
|
path('', include('common.urls')),
|
||||||
|
|
||||||
|
]
|
16
boofilsic/wsgi.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
WSGI config for boofilsic 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/3.0/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'boofilsic.settings')
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
0
books/__init__.py
Normal file
3
books/admin.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
5
books/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class BooksConfig(AppConfig):
|
||||||
|
name = 'books'
|
76
books/forms.py
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
from django import forms
|
||||||
|
from common.forms import KeyValueInput
|
||||||
|
from django.contrib.postgres.forms import SimpleArrayField
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from .models import Book, BookMark, BookReview
|
||||||
|
|
||||||
|
|
||||||
|
class BookForm(forms.ModelForm):
|
||||||
|
pub_year = forms.IntegerField(required=False, max_value=9999, min_value=0, label=_("出版年份"))
|
||||||
|
pub_month = forms.IntegerField(required=False, max_value=12, min_value=1, label=_("出版月份"))
|
||||||
|
class Meta:
|
||||||
|
model = Book
|
||||||
|
fields = [
|
||||||
|
'title',
|
||||||
|
'isbn',
|
||||||
|
'author',
|
||||||
|
'pub_house',
|
||||||
|
'subtitle',
|
||||||
|
'translator',
|
||||||
|
'orig_title',
|
||||||
|
'language',
|
||||||
|
'pub_month',
|
||||||
|
'pub_year',
|
||||||
|
'binding',
|
||||||
|
'price',
|
||||||
|
'pages',
|
||||||
|
'cover',
|
||||||
|
'brief',
|
||||||
|
'other_info',
|
||||||
|
]
|
||||||
|
labels = {
|
||||||
|
'title': _("书名"),
|
||||||
|
'isbn': _("ISBN"),
|
||||||
|
'author': _("作者"),
|
||||||
|
'pub_house': _("出版社"),
|
||||||
|
'subtitle': _("副标题"),
|
||||||
|
'translator': _("译者"),
|
||||||
|
'orig_title': _("原作名"),
|
||||||
|
'language': _("语言"),
|
||||||
|
'pub_month': _("出版月份"),
|
||||||
|
'pub_year': _("出版年份"),
|
||||||
|
'binding': _("装帧"),
|
||||||
|
'price': _("定价"),
|
||||||
|
'pages': _("页数"),
|
||||||
|
'cover': _("封面"),
|
||||||
|
'brief': _("简介"),
|
||||||
|
'other_info': _("其他信息"),
|
||||||
|
}
|
||||||
|
widgets = {
|
||||||
|
'author': forms.TextInput(attrs={'placeholder': _("多个作者使用英文逗号分隔")}),
|
||||||
|
'translator': forms.TextInput(attrs={'placeholder': _("多个译者使用英文逗号分隔")}),
|
||||||
|
'other_info': KeyValueInput(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class BookMarkForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = BookMark
|
||||||
|
fields = [
|
||||||
|
'book',
|
||||||
|
'status',
|
||||||
|
'rating',
|
||||||
|
'text',
|
||||||
|
'is_private',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class BookReviewForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = BookReview
|
||||||
|
fields = [
|
||||||
|
'book',
|
||||||
|
'title',
|
||||||
|
'content',
|
||||||
|
'is_private'
|
||||||
|
]
|
82
books/models.py
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
import django.contrib.postgres.fields as postgres
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.db import models
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
from common.models import Resource, Mark, Review
|
||||||
|
from boofilsic.settings import BOOK_MEDIA_PATH_ROOT, DEFAULT_BOOK_IMAGE
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
def book_cover_path(instance, filename):
|
||||||
|
raise NotImplementedError("UUID!!!!!!!!!!!")
|
||||||
|
root = ''
|
||||||
|
if BOOK_MEDIA_PATH_ROOT.endswith('/'):
|
||||||
|
root = BOOK_MEDIA_PATH_ROOT
|
||||||
|
else:
|
||||||
|
root = BOOK_MEDIA_PATH_ROOT + '/'
|
||||||
|
return root + datetime.now().strftime('%Y/%m/%d') + f'{filename}'
|
||||||
|
|
||||||
|
|
||||||
|
class Book(Resource):
|
||||||
|
# widely recognized name, usually in Chinese
|
||||||
|
title = models.CharField(_("title"), max_length=200)
|
||||||
|
subtitle = models.CharField(_("subtitle"), blank=True, default='', max_length=200)
|
||||||
|
# original name, for books in foreign language
|
||||||
|
orig_title = models.CharField(_("original title"), blank=True, default='', max_length=200)
|
||||||
|
|
||||||
|
author = postgres.ArrayField(
|
||||||
|
models.CharField(_("author"), blank=True, default='', max_length=100),
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
default=list,
|
||||||
|
)
|
||||||
|
translator = postgres.ArrayField(
|
||||||
|
models.CharField(_("translator"), blank=True, default='', max_length=100),
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
default=list,
|
||||||
|
)
|
||||||
|
language = models.CharField(_("language"), blank=True, default='', max_length=10)
|
||||||
|
pub_house = models.CharField(_("publishing house"), blank=True, default='', max_length=200)
|
||||||
|
pub_year = models.IntegerField(_("published year"), null=True, blank=True)
|
||||||
|
pub_month = models.IntegerField(_("published month"), null=True, blank=True)
|
||||||
|
binding = models.CharField(_("binding"), blank=True, default='', max_length=50)
|
||||||
|
# since data origin is not formatted and might be CNY USD or other currency, use char instead
|
||||||
|
price = models.CharField(_("pricing"), blank=True, default='', max_length=50)
|
||||||
|
pages = models.PositiveIntegerField(_("pages"), null=True, blank=True)
|
||||||
|
isbn = models.CharField(_("ISBN"), blank=True, max_length=20, unique=True, db_index=True)
|
||||||
|
# to store previously scrapped data
|
||||||
|
img_url = models.URLField(max_length=300)
|
||||||
|
cover = models.ImageField(_("cover picture"), upload_to=book_cover_path, default=DEFAULT_BOOK_IMAGE, blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
# more info: https://docs.djangoproject.com/en/2.2/ref/models/options/
|
||||||
|
# set managed=False if the model represents an existing table or
|
||||||
|
# a database view that has been created by some other means.
|
||||||
|
# check the link above for further info
|
||||||
|
# managed = True
|
||||||
|
# db_table = 'book'
|
||||||
|
constraints = [
|
||||||
|
models.CheckConstraint(check=models.Q(pub_year__gte=0), name='pub_year_lowerbound'),
|
||||||
|
models.CheckConstraint(check=models.Q(pub_month__lte=12), name='pub_month_upperbound'),
|
||||||
|
models.CheckConstraint(check=models.Q(pub_month__gte=1), name='pub_month_lowerbound'),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
|
||||||
|
class BookMark(Mark):
|
||||||
|
book = models.ForeignKey(Book, on_delete=models.SET_NULL, related_name='book_marks', null=True)
|
||||||
|
class Meta:
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(fields=['owner', 'book'], name="unique_book_mark")
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class BookReview(Review):
|
||||||
|
book = models.ForeignKey(Book, on_delete=models.SET_NULL, related_name='book_reviews', null=True)
|
||||||
|
class Meta:
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(fields=['owner', 'book'], name="unique_book_review")
|
||||||
|
]
|
76
books/templates/books/create_update.html
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load admin_url %}
|
||||||
|
{% load mastodon %}
|
||||||
|
{% load oauth_token %}
|
||||||
|
{% load truncate %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% trans 'Boofilsic - 添加图书' %}</title>
|
||||||
|
<script src="https://cdn.staticfile.org/jquery/3.5.0/jquery.min.js"></script>
|
||||||
|
<script src="{% static 'js/create_update.js' %}"></script>
|
||||||
|
<link rel="stylesheet" href="{% static 'lib/css/milligram.css' %}">
|
||||||
|
<link rel="stylesheet" href="{% static 'css/boofilsic_edit.css' %}">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="page-wrapper">
|
||||||
|
<div id="content-wrapper">
|
||||||
|
<section id="navbar" class="navbar">
|
||||||
|
<div class="container">
|
||||||
|
<nav class="clearfix">
|
||||||
|
<a href="{% url 'common:home' %}">
|
||||||
|
<img src="{% static 'img/logo.svg' %}" alt="" class="logo">
|
||||||
|
</a>
|
||||||
|
<h4 class="nav-title">{{ title }}</h4>
|
||||||
|
|
||||||
|
<a class="nav-link" id="logoutLink" href="{% url 'users:logout' %}">{% trans '登出' %}</a>
|
||||||
|
<a class="nav-link" href="{% url 'common:home' %}">{% trans '主页' %}</a>
|
||||||
|
<a class="nav-link" href="{% admin_url %}">{% trans '后台' %}</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="content" class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div id="main">
|
||||||
|
<form action="{% url 'books:create' %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form }}
|
||||||
|
<button type="submit">{% trans '提交' %}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<footer class="container">
|
||||||
|
<a href="">whitiewhite@donotban.com</a>
|
||||||
|
<a href="" id="githubLink">Github</a>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{% comment %}
|
||||||
|
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
|
||||||
|
<div id="mastodonURI" hidden="true">{% mastodon %}</div>
|
||||||
|
<!--current user mastodon id-->
|
||||||
|
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$("#searchInput").on('keyup', function (e) {
|
||||||
|
if (e.keyCode === 13) {
|
||||||
|
let keywords = $(this).val();
|
||||||
|
if (keywords)
|
||||||
|
location.href = "{% url 'common:search' %}" + "?keywords=" + keywords;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
|
||||||
|
</html>
|
0
books/templates/books/delete.html
Normal file
140
books/templates/books/detail.html
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load admin_url %}
|
||||||
|
{% load mastodon %}
|
||||||
|
{% load oauth_token %}
|
||||||
|
{% load truncate %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% trans 'Boofilsic - 书籍详情' %} | {{ book.title }}</title>
|
||||||
|
<script src="https://cdn.staticfile.org/jquery/3.5.0/jquery.min.js"></script>
|
||||||
|
<script src="{% static 'lib/js/rating-star.js' %}"></script>
|
||||||
|
<script src="{% static 'js/detail.js' %}"></script>
|
||||||
|
<link rel="stylesheet" href="{% static 'css/boofilsic_browse.css' %}">
|
||||||
|
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
|
||||||
|
<link rel="stylesheet" href="{% static 'lib/css/milligram.css' %}">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="page-wrapper">
|
||||||
|
<div id="content-wrapper">
|
||||||
|
<section id="navbar" class="navbar">
|
||||||
|
<div class="container">
|
||||||
|
<nav class="clearfix">
|
||||||
|
<a href="{% url 'common:home' %}">
|
||||||
|
<img src="{% static 'img/logo.svg' %}" alt="" class="logo">
|
||||||
|
</a>
|
||||||
|
<input type="search" class="search-box" name="keywords" id="searchInput" required="true" placeholder="{% trans '搜索书影音,多个关键字以空格分割' %}">
|
||||||
|
<a class="nav-link" id="logoutLink" href="{% url 'users:logout' %}">{% trans '登出' %}</a>
|
||||||
|
<a class="nav-link" href="{% url 'common:home' %}">{% trans '主页' %}</a>
|
||||||
|
<a class="nav-link" href="{% admin_url %}">{% trans '后台' %}</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="content" class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div id="main">
|
||||||
|
<div class="clearfix">
|
||||||
|
<img src="{% static 'img/default.jpg' %}" class="display-image" alt="">
|
||||||
|
<div class="display-info">
|
||||||
|
<h5 class="display-title">
|
||||||
|
{{ book.title }}
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<div class="display-info-detail">
|
||||||
|
<div>{% if book.isbn %}{% trans 'ISBN:' %}{{ book.isbn }}{% endif %}</div>
|
||||||
|
<div>{% if book.author %}{% trans '作者:' %}
|
||||||
|
{% for author in book.author %}
|
||||||
|
<span>{{ author }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}</div>
|
||||||
|
<div>{% if book.pub_house %}{% trans '出版社:' %}{{ book.pub_house }}{% endif %}</div>
|
||||||
|
<div>{% if book.subtitle %}{% trans '副标题:' %}{{ book.subtitle }}{% endif %}</div>
|
||||||
|
<div>{% if book.translator %}{% trans '译者:' %}
|
||||||
|
{% for translator in book.translator %}
|
||||||
|
<span>{{ translator }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}</div>
|
||||||
|
<div>{% if book.orig_title %}{% trans '原作名:' %}{{ book.orig_title }}{% endif %}</div>
|
||||||
|
<div>{% if book.language %}{% trans '语言:' %}{{ book.language }}{% endif %}</div>
|
||||||
|
<div>{%if book.pub_year %}{% trans '出版时间:' %}{{ book.pub_year }}{% trans '年' %}{% if book.pub_month %}{{ book.pub_month }}{% trans '月' %}{% endif %}{% endif %}</div>
|
||||||
|
</div>
|
||||||
|
<div class="display-info-detail">
|
||||||
|
{% if book.rating %}
|
||||||
|
<span class="rating-star" data-rating-score="{{ book.rating | floatformat:"0" }}">
|
||||||
|
|
||||||
|
</span>
|
||||||
|
<span class="rating-score"> {{ book.rating }} </span>
|
||||||
|
{% else %}
|
||||||
|
<span> {% trans '评分:暂无评分' %}</span>
|
||||||
|
{% endif %}
|
||||||
|
<div>{% if book.binding %}{% trans '装帧:' %}{{ book.binding }}{% endif %}</div>
|
||||||
|
<div>{% if book.price %}{% trans '定价:' %}{{ book.price }}{% endif %}</div>
|
||||||
|
<div>{% if book.pages %}{% trans '页数' %}{{ book.pages }}{% endif %}</div>
|
||||||
|
{% if book.other_info %}
|
||||||
|
{% for k, v in book.other_info.items %}
|
||||||
|
<div>
|
||||||
|
{{k}}:{{v}}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% comment %}
|
||||||
|
{% url 'users:home' book.last_editor %}
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
<div>{% trans '最近编辑者:' %}<a href="">someone</a></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dividing-line"></div>
|
||||||
|
<div class="set">
|
||||||
|
<h5 class="set-title">{% trans '简介' %}</h5>
|
||||||
|
|
||||||
|
{% if book.brief %}
|
||||||
|
<p class="set-content">{{ book.brief }}</p>
|
||||||
|
{% else %}
|
||||||
|
<p class="set-empty">{% trans '暂无简介' %}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="aside">
|
||||||
|
<div class="aside-card mast-user">
|
||||||
|
<p class="info-brief mast-brief"></p>
|
||||||
|
<!-- <a href="#" class="follow">{% trans '关注TA' %}</a> -->
|
||||||
|
<!-- <a href="#" class="report">{% trans '举报用户' %}</a> -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<footer class="container">
|
||||||
|
<a href="">whitiewhite@donotban.com</a>
|
||||||
|
<a href="" id="githubLink">Github</a>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$("#searchInput").on('keyup', function (e) {
|
||||||
|
if (e.keyCode === 13) {
|
||||||
|
let keywords = $(this).val();
|
||||||
|
if (keywords)
|
||||||
|
location.href = "{% url 'common:search' %}" + "?keywords=" + keywords;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
|
||||||
|
</html>
|
3
books/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
9
books/urls.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
from django.urls import path
|
||||||
|
from .views import *
|
||||||
|
|
||||||
|
|
||||||
|
app_name = 'books'
|
||||||
|
urlpatterns = [
|
||||||
|
path('create/', create, name='create'),
|
||||||
|
path('<int:id>/', retrieve, name='retrieve'),
|
||||||
|
]
|
46
books/views.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
from django.shortcuts import render, get_object_or_404, redirect, reverse
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.http import HttpResponseBadRequest
|
||||||
|
from .models import *
|
||||||
|
from .forms import *
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def create(request):
|
||||||
|
if request.method == 'GET':
|
||||||
|
form = BookForm()
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
'books/create_update.html',
|
||||||
|
{
|
||||||
|
'form': form,
|
||||||
|
'title': _('添加书籍')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
elif request.method == 'POST':
|
||||||
|
# check user credential in post data, must be the login user
|
||||||
|
pass
|
||||||
|
form = BookForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
form.instance.last_editor = request.user
|
||||||
|
form.save()
|
||||||
|
|
||||||
|
return redirect(reverse("books:retrieve", args=[form.instance.id]))
|
||||||
|
else:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def retrieve(request, id):
|
||||||
|
if request.method == 'GET':
|
||||||
|
book = get_object_or_404(Book, pk=id)
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
'books/detail.html',
|
||||||
|
{
|
||||||
|
'book': book,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return HttpResponseBadRequest()
|
0
common/__init__.py
Normal file
3
common/admin.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
5
common/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class CommonConfig(AppConfig):
|
||||||
|
name = 'common'
|
30
common/forms.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
from django import forms
|
||||||
|
from django.contrib.postgres.forms import JSONField
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
class KeyValueInput(forms.Widget):
|
||||||
|
""" jQeury required """
|
||||||
|
template_name = 'widgets/key_value.html'
|
||||||
|
|
||||||
|
def get_context(self, name, value, attrs):
|
||||||
|
""" called when rendering """
|
||||||
|
context = {}
|
||||||
|
context['widget'] = {
|
||||||
|
'name': name,
|
||||||
|
'is_hidden': self.is_hidden,
|
||||||
|
'required': self.is_required,
|
||||||
|
'value': self.format_value(value),
|
||||||
|
'attrs': self.build_attrs(self.attrs, attrs),
|
||||||
|
'template_name': self.template_name,
|
||||||
|
'keyvalue_pairs': {},
|
||||||
|
}
|
||||||
|
if context['widget']['value']:
|
||||||
|
key_value_pairs = json.loads(context['widget']['value'])
|
||||||
|
# for kv in key_value_pairs:
|
||||||
|
|
||||||
|
context['widget']['keyvalue_pairs'] = {
|
||||||
|
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
|
0
common/mastodon/__init__.py
Normal file
22
common/mastodon/api.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# See https://docs.joinmastodon.org/methods/accounts/
|
||||||
|
|
||||||
|
# returns user info
|
||||||
|
# retruns the same info as verify account credentials
|
||||||
|
# GET
|
||||||
|
ACCOUNT = '/api/v1/accounts/:id'
|
||||||
|
|
||||||
|
# returns user info if valid, 401 if invalid
|
||||||
|
# GET
|
||||||
|
VERIFY_ACCOUNT_CREDENTIALS = '/api/v1/accounts/verify_credentials'
|
||||||
|
|
||||||
|
# obtain token
|
||||||
|
# GET
|
||||||
|
OAUTH_TOKEN = '/oauth/token'
|
||||||
|
|
||||||
|
# obatin auth code
|
||||||
|
# GET
|
||||||
|
OAUTH_AUTHORIZE = '/oauth/authorize'
|
||||||
|
|
||||||
|
# revoke token
|
||||||
|
# POST
|
||||||
|
REVOKE_TOKEN = '/oauth/revoke'
|
76
common/models.py
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import django.contrib.postgres.fields as postgres
|
||||||
|
from decimal import *
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.db import models
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
from markdownx.models import MarkdownxField
|
||||||
|
from users.models import User
|
||||||
|
|
||||||
|
|
||||||
|
# abstract base classes
|
||||||
|
###################################
|
||||||
|
class Resource(models.Model):
|
||||||
|
|
||||||
|
rating_total_score = models.PositiveIntegerField(null=True, blank=True)
|
||||||
|
rating_number = models.PositiveIntegerField(null=True, blank=True)
|
||||||
|
rating = models.DecimalField(null=True, blank=True, max_digits=2, decimal_places=1)
|
||||||
|
created_time = models.DateTimeField(auto_now_add=True)
|
||||||
|
edited_time = models.DateTimeField(auto_now_add=True)
|
||||||
|
last_editor = models.ForeignKey(User, on_delete=models.SET_NULL, related_name='%(class)s_last_editor', null=True, blank=False)
|
||||||
|
brief = models.TextField(blank=True, default="")
|
||||||
|
other_info = postgres.JSONField(blank=True, null=True, encoder=DjangoJSONEncoder, default=dict)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
constraints = [
|
||||||
|
models.CheckConstraint(check=models.Q(rating__gte=0), name='%(class)s_rating_lowerbound'),
|
||||||
|
models.CheckConstraint(check=models.Q(rating__lte=10), name='%(class)s_rating_upperbound'),
|
||||||
|
]
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
""" update rating before save to db """
|
||||||
|
if self.rating_number and self.rating_total_score:
|
||||||
|
self.rating = Decimal(str(round(self.rating_total_score / self.rating_number ), 1))
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class UserOwnedEntity(models.Model):
|
||||||
|
is_private = models.BooleanField()
|
||||||
|
owner = models.ForeignKey(User, on_delete=models.SET_NULL, related_name='user_%(class)ss', null=True)
|
||||||
|
created_time = models.DateTimeField(auto_now_add=True)
|
||||||
|
edited_time = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
|
# commonly used entity classes
|
||||||
|
###################################
|
||||||
|
class MarkStatusEnum(models.IntegerChoices):
|
||||||
|
DO = 1, _('Do')
|
||||||
|
WISH = 2, _('Wish')
|
||||||
|
COLLECT = 3, _('Collect')
|
||||||
|
|
||||||
|
|
||||||
|
class Mark(UserOwnedEntity):
|
||||||
|
status = models.SmallIntegerField(choices=MarkStatusEnum.choices)
|
||||||
|
rating = models.PositiveSmallIntegerField(blank=True, null=True)
|
||||||
|
text = models.CharField(max_length=150, blank=True, default='')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
constraints = [
|
||||||
|
models.CheckConstraint(check=models.Q(rating__gte=0), name='mark_rating_lowerbound'),
|
||||||
|
models.CheckConstraint(check=models.Q(rating__lte=10), name='mark_rating_upperbound'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Review(UserOwnedEntity):
|
||||||
|
title = models.CharField(max_length=120)
|
||||||
|
content = MarkdownxField()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
416
common/static/css/boofilsic_browse.css
Normal file
|
@ -0,0 +1,416 @@
|
||||||
|
/* Global */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--primary: #9b4dca;
|
||||||
|
--secondary: #606c76;
|
||||||
|
--light: #ccc;
|
||||||
|
--bright: rgb(250, 250, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
/* background-color: #eee; */
|
||||||
|
font-size: 55%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#page-wrapper {
|
||||||
|
position: relative;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content-wrapper {
|
||||||
|
padding-bottom: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
input[type=text]::-ms-clear { display: none; width : 0; height: 0; }
|
||||||
|
input[type=text]::-ms-reveal { display: none; width : 0; height: 0; }
|
||||||
|
input[type="search"]::-webkit-search-decoration,
|
||||||
|
input[type="search"]::-webkit-search-cancel-button,
|
||||||
|
input[type="search"]::-webkit-search-results-button,
|
||||||
|
input[type="search"]::-webkit-search-results-decoration { display: none; }
|
||||||
|
|
||||||
|
footer {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
margin-bottom: 4px !important;
|
||||||
|
|
||||||
|
border-top: var(--bright) solid 2px;
|
||||||
|
|
||||||
|
position: absolute !important;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer a {
|
||||||
|
margin: 0 15px;
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.emoji {
|
||||||
|
height: 20px !important;
|
||||||
|
width: 20px !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
object-fit: contain;
|
||||||
|
position: relative;
|
||||||
|
top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-link {
|
||||||
|
font-size: small;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.dividing-line {
|
||||||
|
height: 0;
|
||||||
|
width: 100%;
|
||||||
|
margin: 40px 0 15px 0;
|
||||||
|
border-top: solid 1px var(--light);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Nav Bar */
|
||||||
|
|
||||||
|
section#navbar {
|
||||||
|
background-color: var(--bright);
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 18px 0;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
border-bottom: var(--light) 0.5px solid;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar img.logo {
|
||||||
|
width: 100px;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar .search-box {
|
||||||
|
height: 32px;
|
||||||
|
margin: 5px 0 0 16px;
|
||||||
|
width: 50%;
|
||||||
|
float: left;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar .search-box::placeholder {
|
||||||
|
color: var(--light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar a.nav-link, .navbar a.nav-link:visited {
|
||||||
|
font-size: 80%;
|
||||||
|
float: right;
|
||||||
|
margin-top: 10px;
|
||||||
|
color: var(--secondary);
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar a.nav-link:hover, .navbar a.nav-link:hover:visited {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content Section */
|
||||||
|
|
||||||
|
section#content div#main {
|
||||||
|
padding: 32px 35px;
|
||||||
|
background-color: var(--bright);
|
||||||
|
margin-right: 40px;
|
||||||
|
width: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Aside Content Section */
|
||||||
|
|
||||||
|
section#content div#aside {
|
||||||
|
width: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.set .set-title {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.set .set-empty {
|
||||||
|
font-size: small;
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.set .set-item-list {
|
||||||
|
padding: 0 25px 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.set .set-item {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.set .set-item-image {
|
||||||
|
max-width: 90px;
|
||||||
|
height: 110px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.set .set-item-title, .set .set-item-title:visited {
|
||||||
|
width: 80%;
|
||||||
|
margin: auto;
|
||||||
|
line-height: 1.3em;
|
||||||
|
font-size: small;
|
||||||
|
display: block;
|
||||||
|
color: var(--secondary);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.set .set-item-title:hover, .set .set-item-title:visited:hover {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.set .set-content {
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Info Card */
|
||||||
|
|
||||||
|
.aside-card {
|
||||||
|
background-color: var(--bright);
|
||||||
|
padding: 20px 24px 15px 24px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aside-card .clearfix {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aside-card .info-avatar {
|
||||||
|
width: 72px;
|
||||||
|
object-fit: contain;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aside-card .info-name {
|
||||||
|
margin: 0;
|
||||||
|
max-width: 120px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-left: 8px;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aside-card .info-brief {
|
||||||
|
line-height: 2rem;
|
||||||
|
font-size: small;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
hyphens: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aside-card a.report {
|
||||||
|
color: var(--light);
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aside-card a.follow {
|
||||||
|
font-size: small;
|
||||||
|
color: var(--secondary);
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aside-card .button.add-button {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Relation Card */
|
||||||
|
|
||||||
|
.relation-card {
|
||||||
|
padding: 2px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relation-card .relation-label {
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relation-card .relation-user .relation-avatar {
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relation-card .relation-user .relation-name {
|
||||||
|
display: block;
|
||||||
|
font-size: small;
|
||||||
|
line-height: 1.8rem;
|
||||||
|
/* white-space: nowrap; */
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
/* max-width: 100px; */
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relation-card .relation-user-list {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relation-card .relation-user-list .relation-user {
|
||||||
|
list-style-type: none;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 0px !important;
|
||||||
|
justify-items: center;
|
||||||
|
padding: 0 3px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Report Card */
|
||||||
|
|
||||||
|
.report-card {
|
||||||
|
padding: 2px;
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-card .report-label {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-card .report-user-link {
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-card .report-message {
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search Result */
|
||||||
|
|
||||||
|
.result-item {
|
||||||
|
list-style: none;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.result-item .result-info {
|
||||||
|
margin-left: 20px;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item img.result-book-cover {
|
||||||
|
float: left;
|
||||||
|
object-fit: contain;
|
||||||
|
height: 150px;
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item .result-info .rating-empty {
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item .result-info .rating-star{
|
||||||
|
cursor: unset;
|
||||||
|
display: inline;
|
||||||
|
position: relative;
|
||||||
|
top: 3px;
|
||||||
|
left: -3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* overwrite rating-star plugin */
|
||||||
|
.result-item .result-info .rating-star .jq-star {
|
||||||
|
cursor: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item .result-info .result-book-info {
|
||||||
|
font-size: 80%;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 420px;
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
position: relative;
|
||||||
|
top: .55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item .result-info .rating-score {
|
||||||
|
font-size: 80%;
|
||||||
|
display: inline;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item .result-info .result-book-title {
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 500px;
|
||||||
|
display: block;
|
||||||
|
overflow: hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item .result-info .result-book-brief {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-size: small;
|
||||||
|
width: 565px;
|
||||||
|
display: block;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
/* white-space: nowrap; */
|
||||||
|
height: 90px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pagination */
|
||||||
|
.pagination {
|
||||||
|
margin-top: 50px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination a {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination .button {
|
||||||
|
padding: 0 5px;
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination .page-index {
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Display Page */
|
||||||
|
|
||||||
|
img.display-image {
|
||||||
|
height: 210px;
|
||||||
|
object-fit: contain;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-info {
|
||||||
|
max-width: 530px;
|
||||||
|
float: left;
|
||||||
|
margin-left: 20px;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-title {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 1; /* number of lines to show */
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-info-detail {
|
||||||
|
font-size: small;
|
||||||
|
width: 250px;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-info-detail .rating-star {
|
||||||
|
position: relative;
|
||||||
|
left: -5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-info-detail .rating-score {
|
||||||
|
position: relative;
|
||||||
|
top: -2.5px;
|
||||||
|
}
|
329
common/static/css/boofilsic_edit.css
Normal file
|
@ -0,0 +1,329 @@
|
||||||
|
/* Global */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--primary: #9b4dca;
|
||||||
|
--secondary: #606c76;
|
||||||
|
--light: #ccc;
|
||||||
|
--bright: rgb(250, 250, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
/* background-color: #eee; */
|
||||||
|
font-size: 55%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#page-wrapper {
|
||||||
|
position: relative;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content-wrapper {
|
||||||
|
padding-bottom: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
input[type=text]::-ms-clear { display: none; width : 0; height: 0; }
|
||||||
|
input[type=text]::-ms-reveal { display: none; width : 0; height: 0; }
|
||||||
|
input[type="search"]::-webkit-search-decoration,
|
||||||
|
input[type="search"]::-webkit-search-cancel-button,
|
||||||
|
input[type="search"]::-webkit-search-results-button,
|
||||||
|
input[type="search"]::-webkit-search-results-decoration { display: none; }
|
||||||
|
|
||||||
|
footer {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
margin-bottom: 4px !important;
|
||||||
|
|
||||||
|
border-top: var(--bright) solid 2px;
|
||||||
|
|
||||||
|
position: absolute !important;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer a {
|
||||||
|
margin: 0 15px;
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.emoji {
|
||||||
|
height: 20px !important;
|
||||||
|
width: 20px !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
object-fit: contain;
|
||||||
|
position: relative;
|
||||||
|
top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-link {
|
||||||
|
font-size: small;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Nav Bar */
|
||||||
|
|
||||||
|
section#navbar {
|
||||||
|
background-color: var(--bright);
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 18px 0;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
border-bottom: var(--light) 0.5px solid;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar img.logo {
|
||||||
|
width: 100px;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar h4.nav-title {
|
||||||
|
display: inline;
|
||||||
|
margin-left: 16px;
|
||||||
|
position: relative;
|
||||||
|
top: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar a.nav-link, .navbar a.nav-link:visited {
|
||||||
|
font-size: 80%;
|
||||||
|
float: right;
|
||||||
|
margin-top: 10px;
|
||||||
|
color: var(--secondary);
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar a.nav-link:hover, .navbar a.nav-link:hover:visited {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content Section */
|
||||||
|
|
||||||
|
section#content div#main {
|
||||||
|
padding: 32px 35px;
|
||||||
|
background-color: var(--bright);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div#main form label {
|
||||||
|
font-size: small;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
div#main form input::placeholder {
|
||||||
|
color: var(--light);
|
||||||
|
}
|
||||||
|
|
||||||
|
div#main form button[type="submit"] {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div#main form input[type="file"] {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Info Card */
|
||||||
|
|
||||||
|
.aside-card {
|
||||||
|
background-color: var(--bright);
|
||||||
|
padding: 20px 24px 15px 24px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aside-card .clearfix {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aside-card .info-avatar {
|
||||||
|
width: 72px;
|
||||||
|
object-fit: contain;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aside-card .info-name {
|
||||||
|
margin: 0;
|
||||||
|
max-width: 120px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-left: 8px;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aside-card .info-brief {
|
||||||
|
line-height: 2rem;
|
||||||
|
font-size: small;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
hyphens: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aside-card a.report {
|
||||||
|
color: var(--light);
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aside-card a.follow {
|
||||||
|
font-size: small;
|
||||||
|
color: var(--secondary);
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aside-card .button.add-button {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Relation Card */
|
||||||
|
|
||||||
|
.relation-card {
|
||||||
|
padding: 2px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relation-card .relation-label {
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relation-card .relation-user .relation-avatar {
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relation-card .relation-user .relation-name {
|
||||||
|
display: block;
|
||||||
|
font-size: small;
|
||||||
|
line-height: 1.8rem;
|
||||||
|
/* white-space: nowrap; */
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
/* max-width: 100px; */
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relation-card .relation-user-list {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relation-card .relation-user-list .relation-user {
|
||||||
|
list-style-type: none;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 0px !important;
|
||||||
|
justify-items: center;
|
||||||
|
padding: 0 3px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Report Card */
|
||||||
|
|
||||||
|
.report-card {
|
||||||
|
padding: 2px;
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-card .report-label {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-card .report-user-link {
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-card .report-message {
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search Result */
|
||||||
|
|
||||||
|
.result-item {
|
||||||
|
list-style: none;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.result-item .result-info {
|
||||||
|
margin-left: 20px;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item img.result-book-cover {
|
||||||
|
float: left;
|
||||||
|
object-fit: contain;
|
||||||
|
height: 150px;
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item .result-info .rating-empty {
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item .result-info .rating-star{
|
||||||
|
cursor: unset;
|
||||||
|
display: inline;
|
||||||
|
position: relative;
|
||||||
|
top: 3px;
|
||||||
|
left: -3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* overwrite rating-star plugin */
|
||||||
|
.result-item .result-info .rating-star .jq-star {
|
||||||
|
cursor: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item .result-info .result-book-info {
|
||||||
|
font-size: 80%;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 420px;
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
position: relative;
|
||||||
|
top: .55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item .result-info .rating-score {
|
||||||
|
font-size: 80%;
|
||||||
|
display: inline;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item .result-info .result-book-title {
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 500px;
|
||||||
|
display: block;
|
||||||
|
overflow: hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item .result-info .result-book-brief {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-size: small;
|
||||||
|
width: 565px;
|
||||||
|
display: block;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
/* white-space: nowrap; */
|
||||||
|
height: 90px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pagination */
|
||||||
|
.pagination {
|
||||||
|
margin-top: 50px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination a {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination .button {
|
||||||
|
padding: 0 5px;
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination .page-index {
|
||||||
|
font-size: small;
|
||||||
|
}
|
BIN
common/static/img/default.jpg
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
common/static/img/default_avatar.jpg
Normal file
After Width: | Height: | Size: 6.2 KiB |
1
common/static/img/logo.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 102.13 41.38"><title>logo</title><path d="M1.73,32.77H4.26c1.63,0,2.83.49,2.83,1.95A1.77,1.77,0,0,1,6,36.43v0a1.81,1.81,0,0,1,1.52,1.88c0,1.59-1.3,2.33-3,2.33H1.73ZM4.14,36c1.1,0,1.57-.43,1.57-1.12s-.51-1.06-1.54-1.06h-1V36Zm.18,3.55c1.17,0,1.82-.41,1.82-1.3s-.63-1.2-1.82-1.2H3.14v2.5Z" style="fill:#9b4dca"/><path d="M13.87,36.7c0-2.57,1.43-4.08,3.5-4.08s3.49,1.52,3.49,4.08-1.43,4.13-3.49,4.13S13.87,39.26,13.87,36.7Zm5.55,0c0-1.78-.81-2.86-2.05-2.86s-2.05,1.08-2.05,2.86.8,2.91,2.05,2.91S19.42,38.47,19.42,36.7Z" style="fill:#9b4dca"/><path d="M27.37,36.7c0-2.57,1.43-4.08,3.49-4.08s3.5,1.52,3.5,4.08-1.43,4.13-3.5,4.13S27.37,39.26,27.37,36.7Zm5.54,0c0-1.78-.8-2.86-2-2.86s-2.05,1.08-2.05,2.86.81,2.91,2.05,2.91S32.91,38.47,32.91,36.7Z" style="fill:#9b4dca"/><path d="M41.28,32.77h4.79V34H42.69V36.2h2.88v1.18H42.69v3.31H41.28Z" style="fill:#9b4dca"/><path d="M52.78,32.77h1.41v7.92H52.78Z" style="fill:#9b4dca"/><path d="M61.53,32.77h1.41v6.74h3.29v1.18h-4.7Z" style="fill:#9b4dca"/><path d="M72.1,39.67l.82-1a3.1,3.1,0,0,0,2.06.9c.89,0,1.38-.4,1.38-1s-.51-.86-1.23-1.16L74.05,37a2.29,2.29,0,0,1-1.6-2.11,2.41,2.41,0,0,1,2.66-2.23,3.47,3.47,0,0,1,2.44,1l-.72.89a2.55,2.55,0,0,0-1.72-.68c-.75,0-1.24.35-1.24.93s.6.85,1.26,1.12l1.07.45a2.2,2.2,0,0,1,1.6,2.14c0,1.28-1.07,2.35-2.85,2.35A4.07,4.07,0,0,1,72.1,39.67Z" style="fill:#9b4dca"/><path d="M84.57,32.77H86v7.92h-1.4Z" style="fill:#9b4dca"/><path d="M92.9,36.75c0-2.59,1.62-4.13,3.63-4.13a3.18,3.18,0,0,1,2.28,1l-.75.91a2,2,0,0,0-1.5-.69c-1.29,0-2.21,1.09-2.21,2.87s.85,2.9,2.18,2.9a2.23,2.23,0,0,0,1.69-.81l.75.88a3.15,3.15,0,0,1-2.5,1.15C94.48,40.83,92.9,39.38,92.9,36.75Z" style="fill:#9b4dca"/><path d="M15.67,26.75h-.15L1.58,23.81A.7.7,0,0,1,1,23.12V2.72A.71.71,0,0,1,1.87,2L15.81,5a.72.72,0,0,1,.56.69v20.4a.74.74,0,0,1-.26.55A.71.71,0,0,1,15.67,26.75ZM2.42,22.56,15,25.19v-19L2.42,3.58Z" style="fill:#9b4dca"/><path d="M15.67,26.75a.74.74,0,0,1-.45-.15.73.73,0,0,1-.25-.55V5.65A.71.71,0,0,1,15.52,5L29.47,2a.68.68,0,0,1,.58.15.67.67,0,0,1,.26.54v20.4a.7.7,0,0,1-.56.69L15.81,26.74Zm.7-20.53v19l12.54-2.63v-19Zm13.24,16.9h0Z" style="fill:#9b4dca"/><path d="M87.93,26.88A13.17,13.17,0,1,1,101.1,13.71,13.18,13.18,0,0,1,87.93,26.88Zm0-24.94A11.77,11.77,0,1,0,99.7,13.71,11.78,11.78,0,0,0,87.93,1.94Z" style="fill:#9b4dca"/><path d="M87.93,18.78A5.07,5.07,0,1,1,93,13.71,5.08,5.08,0,0,1,87.93,18.78Zm0-8.74a3.67,3.67,0,1,0,3.68,3.67A3.67,3.67,0,0,0,87.93,10Z" style="fill:#9b4dca"/><path d="M60.82,23H36.4a.7.7,0,0,1-.7-.7V5.09a.7.7,0,0,1,.7-.7H60.82a.7.7,0,0,1,.7.7V22.33A.7.7,0,0,1,60.82,23ZM37.1,21.63h23V5.79h-23Z" style="fill:#9b4dca"/><path d="M69.36,23a.68.68,0,0,1-.31-.08L60.5,18.66a.69.69,0,0,1-.38-.62V9.38a.71.71,0,0,1,.38-.63l8.55-4.29a.72.72,0,0,1,.68,0,.7.7,0,0,1,.33.6V22.33a.7.7,0,0,1-.33.6A.79.79,0,0,1,69.36,23Zm-7.84-5.42,7.14,3.58v-15L61.52,9.81Z" style="fill:#9b4dca"/></svg>
|
After Width: | Height: | Size: 2.8 KiB |
23
common/static/js/create_update.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
$(document).ready( function() {
|
||||||
|
// assume there is only one input[file] on page
|
||||||
|
$("input[type='file']").each(function() {
|
||||||
|
$(this).after('<img src="#" alt="" id="previewImage" style="margin:10px 0;"/>');
|
||||||
|
})
|
||||||
|
|
||||||
|
// mark required
|
||||||
|
$("input[required]").each(function() {
|
||||||
|
$(this).prev().prepend("*");
|
||||||
|
})
|
||||||
|
|
||||||
|
$("input[type='file']").change(function() {
|
||||||
|
if (this.files && this.files[0]) {
|
||||||
|
var reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = function (e) {
|
||||||
|
$('#previewImage').attr('src', e.target.result);
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.readAsDataURL(this.files[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
12
common/static/js/detail.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
$(document).ready( function() {
|
||||||
|
// readonly star rating
|
||||||
|
let ratingLabels = $(".rating-star");
|
||||||
|
$(ratingLabels).each( function(index, value) {
|
||||||
|
let ratingScore = $(this).data("rating-score") / 2;
|
||||||
|
$(this).starRating({
|
||||||
|
initialRating: ratingScore,
|
||||||
|
readOnly: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
83
common/static/js/home.js
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
|
||||||
|
$(document).ready( function() {
|
||||||
|
let token = $("#oauth2Token").text();
|
||||||
|
let mast_uri = $("#mastodonURI").text();
|
||||||
|
let id = $("#userMastodonID").text();
|
||||||
|
|
||||||
|
$(".mast-following-more").hide();
|
||||||
|
$(".mast-followers-more").hide();
|
||||||
|
|
||||||
|
getUserInfo(
|
||||||
|
id,
|
||||||
|
mast_uri,
|
||||||
|
token,
|
||||||
|
function(userData) {
|
||||||
|
let userName;
|
||||||
|
if (userData.display_name) {
|
||||||
|
userName = translateEmojis(userData.display_name, userData.emojis);
|
||||||
|
} else {
|
||||||
|
userName = userData.username;
|
||||||
|
}
|
||||||
|
$(".mast-user .mast-avatar").attr("src", userData.avatar);
|
||||||
|
$(".mast-user .mast-displayname").html(userName);
|
||||||
|
$(".mast-user .mast-brief").text($(userData.note).text());
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
getFollowers(
|
||||||
|
id,
|
||||||
|
mast_uri,
|
||||||
|
token,
|
||||||
|
function(userList) {
|
||||||
|
if (userList.length == 0) {
|
||||||
|
$(".mast-followers").hide();
|
||||||
|
} else {
|
||||||
|
if (userList.length > 4){
|
||||||
|
userList = userList.slice(0, 4);
|
||||||
|
$(".mast-followers-more").show();
|
||||||
|
}
|
||||||
|
let template = $(".mast-followers li").clone();
|
||||||
|
$(".mast-followers").html("");
|
||||||
|
userList.forEach(data => {
|
||||||
|
temp = $(template).clone();
|
||||||
|
temp.find("img").attr("src", data.avatar);
|
||||||
|
if (data.display_name) {
|
||||||
|
temp.find("a").text(data.display_name);
|
||||||
|
} else {
|
||||||
|
temp.find("a").text(data.username);
|
||||||
|
}
|
||||||
|
$(".mast-followers").append(temp);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
getFollowing(
|
||||||
|
id,
|
||||||
|
mast_uri,
|
||||||
|
token,
|
||||||
|
function(userList) {
|
||||||
|
if (userList.length == 0) {
|
||||||
|
$(".mast-following").hide();
|
||||||
|
} else {
|
||||||
|
if (userList.length > 4){
|
||||||
|
userList = userList.slice(0, 4);
|
||||||
|
$(".mast-following-more").show();
|
||||||
|
}
|
||||||
|
let template = $(".mast-following li").clone();
|
||||||
|
$(".mast-following").html("");
|
||||||
|
userList.forEach(data => {
|
||||||
|
temp = $(template).clone()
|
||||||
|
temp.find("img").attr("src", data.avatar);
|
||||||
|
if (data.display_name) {
|
||||||
|
temp.find("a").text(data.display_name);
|
||||||
|
} else {
|
||||||
|
temp.find("a").text(data.username);
|
||||||
|
}
|
||||||
|
$(".mast-following").append(temp);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
});
|
144
common/static/js/mastodon.js
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
// .replace(":id", "")
|
||||||
|
|
||||||
|
// GET
|
||||||
|
const API_FOLLOWERS = "/api/v1/accounts/:id/followers"
|
||||||
|
// GET
|
||||||
|
const API_FOLLOWING = "/api/v1/accounts/:id/following"
|
||||||
|
// GET
|
||||||
|
const API_ACCOUNT = '/api/v1/accounts/:id'
|
||||||
|
|
||||||
|
|
||||||
|
// [
|
||||||
|
// {
|
||||||
|
// "id": "1020382",
|
||||||
|
// "username": "atul13061987",
|
||||||
|
// "acct": "atul13061987",
|
||||||
|
// "display_name": "",
|
||||||
|
// "locked": false,
|
||||||
|
// "bot": false,
|
||||||
|
// "created_at": "2019-12-04T07:17:02.745Z",
|
||||||
|
// "note": "<p></p>",
|
||||||
|
// "url": "https://mastodon.social/@atul13061987",
|
||||||
|
// "avatar": "https://mastodon.social/avatars/original/missing.png",
|
||||||
|
// "avatar_static": "https://mastodon.social/avatars/original/missing.png",
|
||||||
|
// "header": "https://mastodon.social/headers/original/missing.png",
|
||||||
|
// "header_static": "https://mastodon.social/headers/original/missing.png",
|
||||||
|
// "followers_count": 0,
|
||||||
|
// "following_count": 2,
|
||||||
|
// "statuses_count": 0,
|
||||||
|
// "last_status_at": null,
|
||||||
|
// "emojis": [],
|
||||||
|
// "fields": []
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "id": "1020381",
|
||||||
|
// "username": "linuxliner",
|
||||||
|
// "acct": "linuxliner",
|
||||||
|
// "display_name": "",
|
||||||
|
// "locked": false,
|
||||||
|
// "bot": false,
|
||||||
|
// "created_at": "2019-12-04T07:15:56.426Z",
|
||||||
|
// "note": "<p></p>",
|
||||||
|
// "url": "https://mastodon.social/@linuxliner",
|
||||||
|
// "avatar": "https://mastodon.social/avatars/original/missing.png",
|
||||||
|
// "avatar_static": "https://mastodon.social/avatars/original/missing.png",
|
||||||
|
// "header": "https://mastodon.social/headers/original/missing.png",
|
||||||
|
// "header_static": "https://mastodon.social/headers/original/missing.png",
|
||||||
|
// "followers_count": 0,
|
||||||
|
// "following_count": 2,
|
||||||
|
// "statuses_count": 0,
|
||||||
|
// "last_status_at": null,
|
||||||
|
// "emojis": [],
|
||||||
|
// "fields": []
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
function getFollowers(id, mastodonURI, token, callback) {
|
||||||
|
let url = mastodonURI + API_FOLLOWERS.replace(":id", id);
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + token,
|
||||||
|
},
|
||||||
|
success: function(data){
|
||||||
|
callback(data);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFollowing(id, mastodonURI, token, callback) {
|
||||||
|
let url = mastodonURI + API_FOLLOWING.replace(":id", id);
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + token,
|
||||||
|
},
|
||||||
|
success: function(data){
|
||||||
|
callback(data);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// {
|
||||||
|
// "id": "1",
|
||||||
|
// "username": "Gargron",
|
||||||
|
// "acct": "Gargron",
|
||||||
|
// "display_name": "Eugen",
|
||||||
|
// "locked": false,
|
||||||
|
// "bot": false,
|
||||||
|
// "created_at": "2016-03-16T14:34:26.392Z",
|
||||||
|
// "note": "<p>Developer of Mastodon and administrator of mastodon.social. I post service announcements, development updates, and personal stuff.</p>",
|
||||||
|
// "url": "https://mastodon.social/@Gargron",
|
||||||
|
// "avatar": "https://files.mastodon.social/accounts/avatars/000/000/001/original/d96d39a0abb45b92.jpg",
|
||||||
|
// "avatar_static": "https://files.mastodon.social/accounts/avatars/000/000/001/original/d96d39a0abb45b92.jpg",
|
||||||
|
// "header": "https://files.mastodon.social/accounts/headers/000/000/001/original/c91b871f294ea63e.png",
|
||||||
|
// "header_static": "https://files.mastodon.social/accounts/headers/000/000/001/original/c91b871f294ea63e.png",
|
||||||
|
// "followers_count": 318699,
|
||||||
|
// "following_count": 453,
|
||||||
|
// "statuses_count": 61013,
|
||||||
|
// "last_status_at": "2019-11-30T20:02:08.277Z",
|
||||||
|
// "emojis": [],
|
||||||
|
// "fields": [
|
||||||
|
// {
|
||||||
|
// "name": "Patreon",
|
||||||
|
// "value": "<a href=\"https://www.patreon.com/mastodon\" rel=\"me nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://www.</span><span class=\"\">patreon.com/mastodon</span><span class=\"invisible\"></span></a>",
|
||||||
|
// "verified_at": null
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "name": "Homepage",
|
||||||
|
// "value": "<a href=\"https://zeonfederated.com\" rel=\"me nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"\">zeonfederated.com</span><span class=\"invisible\"></span></a>",
|
||||||
|
// "verified_at": "2019-07-15T18:29:57.191+00:00"
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
function getUserInfo(id, mastodonURI, token, callback) {
|
||||||
|
let url = mastodonURI + API_ACCOUNT.replace(":id", id);
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + token,
|
||||||
|
},
|
||||||
|
success: function(data){
|
||||||
|
callback(data);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEmojiDict(emoji_list) {
|
||||||
|
let dict = new Object;
|
||||||
|
emoji_list.forEach(pair => {
|
||||||
|
dict[":" + pair.shortcode + ":"] = pair.url;
|
||||||
|
});
|
||||||
|
return dict;
|
||||||
|
}
|
||||||
|
|
||||||
|
function translateEmojis(text, emoji_list) {
|
||||||
|
let dict = getEmojiDict(emoji_list);
|
||||||
|
let regex = /:(.*?):/g;
|
||||||
|
let translation = text.replace(regex, function (match) {
|
||||||
|
return "<img src=" + dict[match] + " class=emoji alt=" + match + ">";
|
||||||
|
});
|
||||||
|
return translation;
|
||||||
|
}
|
11
common/static/js/search_result.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
$(document).ready( function() {
|
||||||
|
let ratingLabels = $(".rating-star");
|
||||||
|
$(ratingLabels).each( function(index, value) {
|
||||||
|
let ratingScore = $(this).data("rating-score") / 2;
|
||||||
|
$(this).starRating({
|
||||||
|
initialRating: ratingScore,
|
||||||
|
readOnly: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
602
common/static/lib/css/milligram.css
Normal file
|
@ -0,0 +1,602 @@
|
||||||
|
/*!
|
||||||
|
* Milligram v1.3.0
|
||||||
|
* https://milligram.github.io
|
||||||
|
*
|
||||||
|
* Copyright (c) 2017 CJ Patoilo
|
||||||
|
* Licensed under the MIT license
|
||||||
|
*/
|
||||||
|
|
||||||
|
*,
|
||||||
|
*:after,
|
||||||
|
*:before {
|
||||||
|
box-sizing: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 62.5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
color: #606c76;
|
||||||
|
font-family: 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
|
||||||
|
font-size: 1.6em;
|
||||||
|
font-weight: 300;
|
||||||
|
letter-spacing: .01em;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
border-left: 0.3rem solid #d1d1d1;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote *:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button,
|
||||||
|
button,
|
||||||
|
input[type='button'],
|
||||||
|
input[type='reset'],
|
||||||
|
input[type='submit'] {
|
||||||
|
background-color: #9b4dca;
|
||||||
|
border: 0.1rem solid #9b4dca;
|
||||||
|
border-radius: .4rem;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
height: 3.8rem;
|
||||||
|
letter-spacing: .1rem;
|
||||||
|
line-height: 3.8rem;
|
||||||
|
padding: 0 3.0rem;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: uppercase;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:focus, .button:hover,
|
||||||
|
button:focus,
|
||||||
|
button:hover,
|
||||||
|
input[type='button']:focus,
|
||||||
|
input[type='button']:hover,
|
||||||
|
input[type='reset']:focus,
|
||||||
|
input[type='reset']:hover,
|
||||||
|
input[type='submit']:focus,
|
||||||
|
input[type='submit']:hover {
|
||||||
|
background-color: #606c76;
|
||||||
|
border-color: #606c76;
|
||||||
|
color: #fff;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button[disabled],
|
||||||
|
button[disabled],
|
||||||
|
input[type='button'][disabled],
|
||||||
|
input[type='reset'][disabled],
|
||||||
|
input[type='submit'][disabled] {
|
||||||
|
cursor: default;
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button[disabled]:focus, .button[disabled]:hover,
|
||||||
|
button[disabled]:focus,
|
||||||
|
button[disabled]:hover,
|
||||||
|
input[type='button'][disabled]:focus,
|
||||||
|
input[type='button'][disabled]:hover,
|
||||||
|
input[type='reset'][disabled]:focus,
|
||||||
|
input[type='reset'][disabled]:hover,
|
||||||
|
input[type='submit'][disabled]:focus,
|
||||||
|
input[type='submit'][disabled]:hover {
|
||||||
|
background-color: #9b4dca;
|
||||||
|
border-color: #9b4dca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.button-outline,
|
||||||
|
button.button-outline,
|
||||||
|
input[type='button'].button-outline,
|
||||||
|
input[type='reset'].button-outline,
|
||||||
|
input[type='submit'].button-outline {
|
||||||
|
background-color: transparent;
|
||||||
|
color: #9b4dca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.button-outline:focus, .button.button-outline:hover,
|
||||||
|
button.button-outline:focus,
|
||||||
|
button.button-outline:hover,
|
||||||
|
input[type='button'].button-outline:focus,
|
||||||
|
input[type='button'].button-outline:hover,
|
||||||
|
input[type='reset'].button-outline:focus,
|
||||||
|
input[type='reset'].button-outline:hover,
|
||||||
|
input[type='submit'].button-outline:focus,
|
||||||
|
input[type='submit'].button-outline:hover {
|
||||||
|
background-color: transparent;
|
||||||
|
border-color: #606c76;
|
||||||
|
color: #606c76;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.button-outline[disabled]:focus, .button.button-outline[disabled]:hover,
|
||||||
|
button.button-outline[disabled]:focus,
|
||||||
|
button.button-outline[disabled]:hover,
|
||||||
|
input[type='button'].button-outline[disabled]:focus,
|
||||||
|
input[type='button'].button-outline[disabled]:hover,
|
||||||
|
input[type='reset'].button-outline[disabled]:focus,
|
||||||
|
input[type='reset'].button-outline[disabled]:hover,
|
||||||
|
input[type='submit'].button-outline[disabled]:focus,
|
||||||
|
input[type='submit'].button-outline[disabled]:hover {
|
||||||
|
border-color: inherit;
|
||||||
|
color: #9b4dca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.button-clear,
|
||||||
|
button.button-clear,
|
||||||
|
input[type='button'].button-clear,
|
||||||
|
input[type='reset'].button-clear,
|
||||||
|
input[type='submit'].button-clear {
|
||||||
|
background-color: transparent;
|
||||||
|
border-color: transparent;
|
||||||
|
color: #9b4dca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.button-clear:focus, .button.button-clear:hover,
|
||||||
|
button.button-clear:focus,
|
||||||
|
button.button-clear:hover,
|
||||||
|
input[type='button'].button-clear:focus,
|
||||||
|
input[type='button'].button-clear:hover,
|
||||||
|
input[type='reset'].button-clear:focus,
|
||||||
|
input[type='reset'].button-clear:hover,
|
||||||
|
input[type='submit'].button-clear:focus,
|
||||||
|
input[type='submit'].button-clear:hover {
|
||||||
|
background-color: transparent;
|
||||||
|
border-color: transparent;
|
||||||
|
color: #606c76;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.button-clear[disabled]:focus, .button.button-clear[disabled]:hover,
|
||||||
|
button.button-clear[disabled]:focus,
|
||||||
|
button.button-clear[disabled]:hover,
|
||||||
|
input[type='button'].button-clear[disabled]:focus,
|
||||||
|
input[type='button'].button-clear[disabled]:hover,
|
||||||
|
input[type='reset'].button-clear[disabled]:focus,
|
||||||
|
input[type='reset'].button-clear[disabled]:hover,
|
||||||
|
input[type='submit'].button-clear[disabled]:focus,
|
||||||
|
input[type='submit'].button-clear[disabled]:hover {
|
||||||
|
color: #9b4dca;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background: #f4f5f6;
|
||||||
|
border-radius: .4rem;
|
||||||
|
font-size: 86%;
|
||||||
|
margin: 0 .2rem;
|
||||||
|
padding: .2rem .5rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background: #f4f5f6;
|
||||||
|
border-left: 0.3rem solid #9b4dca;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre > code {
|
||||||
|
border-radius: 0;
|
||||||
|
display: block;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
border-top: 0.1rem solid #f4f5f6;
|
||||||
|
margin: 3.0rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='email'],
|
||||||
|
input[type='number'],
|
||||||
|
input[type='password'],
|
||||||
|
input[type='search'],
|
||||||
|
input[type='tel'],
|
||||||
|
input[type='text'],
|
||||||
|
input[type='url'],
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 0.1rem solid #d1d1d1;
|
||||||
|
border-radius: .4rem;
|
||||||
|
box-shadow: none;
|
||||||
|
box-sizing: inherit;
|
||||||
|
height: 3.8rem;
|
||||||
|
padding: .6rem 1.0rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='email']:focus,
|
||||||
|
input[type='number']:focus,
|
||||||
|
input[type='password']:focus,
|
||||||
|
input[type='search']:focus,
|
||||||
|
input[type='tel']:focus,
|
||||||
|
input[type='text']:focus,
|
||||||
|
input[type='url']:focus,
|
||||||
|
textarea:focus,
|
||||||
|
select:focus {
|
||||||
|
border-color: #9b4dca;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#d1d1d1" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>') center right no-repeat;
|
||||||
|
padding-right: 3.0rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
select:focus {
|
||||||
|
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#9b4dca" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>');
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
min-height: 6.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
label,
|
||||||
|
legend {
|
||||||
|
display: block;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
border-width: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='checkbox'],
|
||||||
|
input[type='radio'] {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-inline {
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: normal;
|
||||||
|
margin-left: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 112.0rem;
|
||||||
|
padding: 0 2.0rem;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.row-no-padding {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.row-no-padding > .column {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.row-wrap {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.row-top {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.row-bottom {
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.row-center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.row-stretch {
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.row-baseline {
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column {
|
||||||
|
display: block;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
margin-left: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-offset-10 {
|
||||||
|
margin-left: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-offset-20 {
|
||||||
|
margin-left: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-offset-25 {
|
||||||
|
margin-left: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-offset-33, .row .column.column-offset-34 {
|
||||||
|
margin-left: 33.3333%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-offset-50 {
|
||||||
|
margin-left: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-offset-66, .row .column.column-offset-67 {
|
||||||
|
margin-left: 66.6666%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-offset-75 {
|
||||||
|
margin-left: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-offset-80 {
|
||||||
|
margin-left: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-offset-90 {
|
||||||
|
margin-left: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-10 {
|
||||||
|
flex: 0 0 10%;
|
||||||
|
max-width: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-20 {
|
||||||
|
flex: 0 0 20%;
|
||||||
|
max-width: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-25 {
|
||||||
|
flex: 0 0 25%;
|
||||||
|
max-width: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-33, .row .column.column-34 {
|
||||||
|
flex: 0 0 33.3333%;
|
||||||
|
max-width: 33.3333%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-40 {
|
||||||
|
flex: 0 0 40%;
|
||||||
|
max-width: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-50 {
|
||||||
|
flex: 0 0 50%;
|
||||||
|
max-width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-60 {
|
||||||
|
flex: 0 0 60%;
|
||||||
|
max-width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-66, .row .column.column-67 {
|
||||||
|
flex: 0 0 66.6666%;
|
||||||
|
max-width: 66.6666%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-75 {
|
||||||
|
flex: 0 0 75%;
|
||||||
|
max-width: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-80 {
|
||||||
|
flex: 0 0 80%;
|
||||||
|
max-width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-90 {
|
||||||
|
flex: 0 0 90%;
|
||||||
|
max-width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column .column-top {
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column .column-bottom {
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column .column-center {
|
||||||
|
-ms-grid-row-align: center;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 40rem) {
|
||||||
|
.row {
|
||||||
|
flex-direction: row;
|
||||||
|
margin-left: -1.0rem;
|
||||||
|
width: calc(100% + 2.0rem);
|
||||||
|
}
|
||||||
|
.row .column {
|
||||||
|
margin-bottom: inherit;
|
||||||
|
padding: 0 1.0rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #9b4dca;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:focus, a:hover {
|
||||||
|
color: #606c76;
|
||||||
|
}
|
||||||
|
|
||||||
|
dl,
|
||||||
|
ol,
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
margin-top: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dl dl,
|
||||||
|
dl ol,
|
||||||
|
dl ul,
|
||||||
|
ol dl,
|
||||||
|
ol ol,
|
||||||
|
ol ul,
|
||||||
|
ul dl,
|
||||||
|
ul ol,
|
||||||
|
ul ul {
|
||||||
|
font-size: 90%;
|
||||||
|
margin: 1.5rem 0 1.5rem 3.0rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol {
|
||||||
|
list-style: decimal inside;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: circle inside;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button,
|
||||||
|
button,
|
||||||
|
dd,
|
||||||
|
dt,
|
||||||
|
li {
|
||||||
|
margin-bottom: 1.0rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset,
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote,
|
||||||
|
dl,
|
||||||
|
figure,
|
||||||
|
form,
|
||||||
|
ol,
|
||||||
|
p,
|
||||||
|
pre,
|
||||||
|
table,
|
||||||
|
ul {
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-spacing: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
border-bottom: 0.1rem solid #e1e1e1;
|
||||||
|
padding: 1.2rem 1.5rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
td:first-child,
|
||||||
|
th:first-child {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
td:last-child,
|
||||||
|
th:last-child {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
font-weight: 300;
|
||||||
|
letter-spacing: -.1rem;
|
||||||
|
margin-bottom: 2.0rem;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 4.6rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 3.6rem;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 2.8rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
letter-spacing: -.08rem;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
letter-spacing: -.05rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6 {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
letter-spacing: 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clearfix:after {
|
||||||
|
clear: both;
|
||||||
|
content: ' ';
|
||||||
|
display: table;
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-left {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-right {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*# sourceMappingURL=milligram.css.map */
|
38
common/static/lib/css/rating-star.css
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
.jq-stars {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jq-rating-label {
|
||||||
|
font-size: 22px;
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: top;
|
||||||
|
font-family: helvetica, arial, verdana;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jq-star {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
display: inline-block;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jq-star-svg {
|
||||||
|
padding-left: 3px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100% ;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jq-star:hover .fs-star-svg path {
|
||||||
|
}
|
||||||
|
|
||||||
|
.jq-star-svg path {
|
||||||
|
/* stroke: #000; */
|
||||||
|
stroke-linejoin: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* un-used */
|
||||||
|
.jq-shadow {
|
||||||
|
-webkit-filter: drop-shadow( -2px -2px 2px #888 );
|
||||||
|
filter: drop-shadow( -2px -2px 2px #888 );
|
||||||
|
}
|
313
common/static/lib/js/rating-star.js
Normal file
|
@ -0,0 +1,313 @@
|
||||||
|
/*
|
||||||
|
* jQuery StarRatingSvg v1.2.0
|
||||||
|
*
|
||||||
|
* http://github.com/nashio/star-rating-svg
|
||||||
|
* http://nashio.github.io/star-rating-svg/demo/
|
||||||
|
* Author: Ignacio Chavez
|
||||||
|
* hello@ignaciochavez.com
|
||||||
|
* Licensed under MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
;(function ( $, window, document, undefined ) {
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Create the defaults once
|
||||||
|
var pluginName = 'starRating';
|
||||||
|
var noop = function(){};
|
||||||
|
var defaults = {
|
||||||
|
totalStars: 5,
|
||||||
|
useFullStars: false,
|
||||||
|
starShape: 'straight',
|
||||||
|
emptyColor: 'lightgray',
|
||||||
|
hoverColor: 'gold',
|
||||||
|
activeColor: 'gold',
|
||||||
|
ratedColor: 'gold',
|
||||||
|
useGradient: false,
|
||||||
|
readOnly: false,
|
||||||
|
disableAfterRate: false,
|
||||||
|
baseUrl: false,
|
||||||
|
starGradient: {
|
||||||
|
start: '#FEF7CD',
|
||||||
|
end: '#FF9511'
|
||||||
|
},
|
||||||
|
strokeWidth: 0,
|
||||||
|
strokeColor: 'black',
|
||||||
|
initialRating: 0,
|
||||||
|
starSize: 18,
|
||||||
|
callback: noop,
|
||||||
|
onHover: noop,
|
||||||
|
onLeave: noop
|
||||||
|
};
|
||||||
|
|
||||||
|
// The actual plugin constructor
|
||||||
|
var Plugin = function( element, options ) {
|
||||||
|
var _rating;
|
||||||
|
this.element = element;
|
||||||
|
this.$el = $(element);
|
||||||
|
this.settings = $.extend( {}, defaults, options );
|
||||||
|
|
||||||
|
// grab rating if defined on the element
|
||||||
|
_rating = this.$el.data('rating') || this.settings.initialRating;
|
||||||
|
this._state = {
|
||||||
|
// round to the nearest half
|
||||||
|
rating: (Math.round( _rating * 2 ) / 2).toFixed(1)
|
||||||
|
};
|
||||||
|
|
||||||
|
// create unique id for stars
|
||||||
|
this._uid = Math.floor( Math.random() * 999 );
|
||||||
|
|
||||||
|
// override gradient if not used
|
||||||
|
if( !options.starGradient && !this.settings.useGradient ){
|
||||||
|
this.settings.starGradient.start = this.settings.starGradient.end = this.settings.activeColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._defaults = defaults;
|
||||||
|
this._name = pluginName;
|
||||||
|
this.init();
|
||||||
|
};
|
||||||
|
|
||||||
|
var methods = {
|
||||||
|
init: function () {
|
||||||
|
this.renderMarkup();
|
||||||
|
this.addListeners();
|
||||||
|
this.initRating();
|
||||||
|
},
|
||||||
|
|
||||||
|
addListeners: function(){
|
||||||
|
if( this.settings.readOnly ){ return; }
|
||||||
|
this.$stars.on('mouseover', this.hoverRating.bind(this));
|
||||||
|
this.$stars.on('mouseout', this.restoreState.bind(this));
|
||||||
|
this.$stars.on('click', this.handleRating.bind(this));
|
||||||
|
},
|
||||||
|
|
||||||
|
// apply styles to hovered stars
|
||||||
|
hoverRating: function(e){
|
||||||
|
var index = this.getIndex(e);
|
||||||
|
this.paintStars(index, 'hovered');
|
||||||
|
this.settings.onHover(index + 1, this._state.rating, this.$el);
|
||||||
|
},
|
||||||
|
|
||||||
|
// clicked on a rate, apply style and state
|
||||||
|
handleRating: function(e){
|
||||||
|
var index = this.getIndex(e);
|
||||||
|
var rating = index + 1;
|
||||||
|
|
||||||
|
this.applyRating(rating, this.$el);
|
||||||
|
this.executeCallback( rating, this.$el );
|
||||||
|
|
||||||
|
if(this.settings.disableAfterRate){
|
||||||
|
this.$stars.off();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
applyRating: function(rating){
|
||||||
|
var index = rating - 1;
|
||||||
|
// paint selected and remove hovered color
|
||||||
|
this.paintStars(index, 'rated');
|
||||||
|
this._state.rating = index + 1;
|
||||||
|
this._state.rated = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
restoreState: function(e){
|
||||||
|
var index = this.getIndex(e);
|
||||||
|
var rating = this._state.rating || -1;
|
||||||
|
// determine star color depending on manually rated
|
||||||
|
var colorType = this._state.rated ? 'rated' : 'active';
|
||||||
|
this.paintStars(rating - 1, colorType);
|
||||||
|
this.settings.onLeave(index + 1, this._state.rating, this.$el);
|
||||||
|
},
|
||||||
|
|
||||||
|
getIndex: function(e){
|
||||||
|
var $target = $(e.currentTarget);
|
||||||
|
var width = $target.width();
|
||||||
|
var side = $(e.target).attr('data-side');
|
||||||
|
|
||||||
|
// hovered outside the star, calculate by pixel instead
|
||||||
|
side = (!side) ? this.getOffsetByPixel(e, $target, width) : side;
|
||||||
|
side = (this.settings.useFullStars) ? 'right' : side ;
|
||||||
|
|
||||||
|
// get index for half or whole star
|
||||||
|
var index = $target.index() - ((side === 'left') ? 0.5 : 0);
|
||||||
|
|
||||||
|
// pointer is way to the left, rating should be none
|
||||||
|
index = ( index < 0.5 && (e.offsetX < width / 4) ) ? -1 : index;
|
||||||
|
return index;
|
||||||
|
},
|
||||||
|
|
||||||
|
getOffsetByPixel: function(e, $target, width){
|
||||||
|
var leftX = e.pageX - $target.offset().left;
|
||||||
|
return ( leftX <= (width / 2) && !this.settings.useFullStars) ? 'left' : 'right';
|
||||||
|
},
|
||||||
|
|
||||||
|
initRating: function(){
|
||||||
|
this.paintStars(this._state.rating - 1, 'active');
|
||||||
|
},
|
||||||
|
|
||||||
|
paintStars: function(endIndex, stateClass){
|
||||||
|
var $polygonLeft;
|
||||||
|
var $polygonRight;
|
||||||
|
var leftClass;
|
||||||
|
var rightClass;
|
||||||
|
|
||||||
|
$.each(this.$stars, function(index, star){
|
||||||
|
$polygonLeft = $(star).find('[data-side="left"]');
|
||||||
|
$polygonRight = $(star).find('[data-side="right"]');
|
||||||
|
leftClass = rightClass = (index <= endIndex) ? stateClass : 'empty';
|
||||||
|
|
||||||
|
// has another half rating, add half star
|
||||||
|
leftClass = ( index - endIndex === 0.5 ) ? stateClass : leftClass;
|
||||||
|
|
||||||
|
$polygonLeft.attr('class', 'svg-' + leftClass + '-' + this._uid);
|
||||||
|
$polygonRight.attr('class', 'svg-' + rightClass + '-' + this._uid);
|
||||||
|
|
||||||
|
}.bind(this));
|
||||||
|
},
|
||||||
|
|
||||||
|
renderMarkup: function () {
|
||||||
|
var s = this.settings;
|
||||||
|
var baseUrl = s.baseUrl ? location.href.split('#')[0] : '';
|
||||||
|
|
||||||
|
// inject an svg manually to have control over attributes
|
||||||
|
var star = '<div class="jq-star" style="width:' + s.starSize+ 'px; height:' + s.starSize + 'px;"><svg version="1.0" class="jq-star-svg" shape-rendering="geometricPrecision" xmlns="http://www.w3.org/2000/svg" ' + this.getSvgDimensions(s.starShape) + ' stroke-width:' + s.strokeWidth + 'px;" xml:space="preserve"><style type="text/css">.svg-empty-' + this._uid + '{fill:url(' + baseUrl + '#' + this._uid + '_SVGID_1_);}.svg-hovered-' + this._uid + '{fill:url(' + baseUrl + '#' + this._uid + '_SVGID_2_);}.svg-active-' + this._uid + '{fill:url(' + baseUrl + '#' + this._uid + '_SVGID_3_);}.svg-rated-' + this._uid + '{fill:' + s.ratedColor + ';}</style>' +
|
||||||
|
|
||||||
|
this.getLinearGradient(this._uid + '_SVGID_1_', s.emptyColor, s.emptyColor, s.starShape) +
|
||||||
|
this.getLinearGradient(this._uid + '_SVGID_2_', s.hoverColor, s.hoverColor, s.starShape) +
|
||||||
|
this.getLinearGradient(this._uid + '_SVGID_3_', s.starGradient.start, s.starGradient.end, s.starShape) +
|
||||||
|
this.getVectorPath(this._uid, {
|
||||||
|
starShape: s.starShape,
|
||||||
|
strokeWidth: s.strokeWidth,
|
||||||
|
strokeColor: s.strokeColor
|
||||||
|
} ) +
|
||||||
|
'</svg></div>';
|
||||||
|
|
||||||
|
// inject svg markup
|
||||||
|
var starsMarkup = '';
|
||||||
|
for( var i = 0; i < s.totalStars; i++){
|
||||||
|
starsMarkup += star;
|
||||||
|
}
|
||||||
|
this.$el.append(starsMarkup);
|
||||||
|
this.$stars = this.$el.find('.jq-star');
|
||||||
|
},
|
||||||
|
|
||||||
|
getVectorPath: function(id, attrs){
|
||||||
|
return (attrs.starShape === 'rounded') ?
|
||||||
|
this.getRoundedVectorPath(id, attrs) : this.getSpikeVectorPath(id, attrs);
|
||||||
|
},
|
||||||
|
|
||||||
|
getSpikeVectorPath: function(id, attrs){
|
||||||
|
return '<polygon data-side="center" class="svg-empty-' + id + '" points="281.1,129.8 364,55.7 255.5,46.8 214,-59 172.5,46.8 64,55.4 146.8,129.7 121.1,241 212.9,181.1 213.9,181 306.5,241 " style="fill: transparent; stroke: ' + attrs.strokeColor + ';" />' +
|
||||||
|
'<polygon data-side="left" class="svg-empty-' + id + '" points="281.1,129.8 364,55.7 255.5,46.8 214,-59 172.5,46.8 64,55.4 146.8,129.7 121.1,241 213.9,181.1 213.9,181 306.5,241 " style="stroke-opacity: 0;" />' +
|
||||||
|
'<polygon data-side="right" class="svg-empty-' + id + '" points="364,55.7 255.5,46.8 214,-59 213.9,181 306.5,241 281.1,129.8 " style="stroke-opacity: 0;" />';
|
||||||
|
},
|
||||||
|
|
||||||
|
getRoundedVectorPath: function(id, attrs){
|
||||||
|
var fullPoints = 'M520.9,336.5c-3.8-11.8-14.2-20.5-26.5-22.2l-140.9-20.5l-63-127.7 c-5.5-11.2-16.8-18.2-29.3-18.2c-12.5,0-23.8,7-29.3,18.2l-63,127.7L28,314.2C15.7,316,5.4,324.7,1.6,336.5S1,361.3,9.9,370 l102,99.4l-24,140.3c-2.1,12.3,2.9,24.6,13,32c5.7,4.2,12.4,6.2,19.2,6.2c5.2,0,10.5-1.2,15.2-3.8l126-66.3l126,66.2 c4.8,2.6,10,3.8,15.2,3.8c6.8,0,13.5-2.1,19.2-6.2c10.1-7.3,15.1-19.7,13-32l-24-140.3l102-99.4 C521.6,361.3,524.8,348.3,520.9,336.5z';
|
||||||
|
|
||||||
|
return '<path data-side="center" class="svg-empty-' + id + '" d="' + fullPoints + '" style="stroke: ' + attrs.strokeColor + '; fill: transparent; " /><path data-side="right" class="svg-empty-' + id + '" d="' + fullPoints + '" style="stroke-opacity: 0;" /><path data-side="left" class="svg-empty-' + id + '" d="M121,648c-7.3,0-14.1-2.2-19.8-6.4c-10.4-7.6-15.6-20.3-13.4-33l24-139.9l-101.6-99 c-9.1-8.9-12.4-22.4-8.6-34.5c3.9-12.1,14.6-21.1,27.2-23l140.4-20.4L232,164.6c5.7-11.6,17.3-18.8,30.2-16.8c0.6,0,1,0.4,1,1 v430.1c0,0.4-0.2,0.7-0.5,0.9l-126,66.3C132,646.6,126.6,648,121,648z" style="stroke: ' + attrs.strokeColor + '; stroke-opacity: 0;" />';
|
||||||
|
},
|
||||||
|
|
||||||
|
getSvgDimensions: function(starShape){
|
||||||
|
return (starShape === 'rounded') ? 'width="550px" height="500.2px" viewBox="0 146.8 550 500.2" style="enable-background:new 0 0 550 500.2;' : 'x="0px" y="0px" width="305px" height="305px" viewBox="60 -62 309 309" style="enable-background:new 64 -59 305 305;';
|
||||||
|
},
|
||||||
|
|
||||||
|
getLinearGradient: function(id, startColor, endColor, starShape){
|
||||||
|
var height = (starShape === 'rounded') ? 500 : 250;
|
||||||
|
return '<linearGradient id="' + id + '" gradientUnits="userSpaceOnUse" x1="0" y1="-50" x2="0" y2="' + height + '"><stop offset="0" style="stop-color:' + startColor + '"/><stop offset="1" style="stop-color:' + endColor + '"/> </linearGradient>';
|
||||||
|
},
|
||||||
|
|
||||||
|
executeCallback: function(rating, $el){
|
||||||
|
var callback = this.settings.callback;
|
||||||
|
callback(rating, $el);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
var publicMethods = {
|
||||||
|
|
||||||
|
unload: function() {
|
||||||
|
var _name = 'plugin_' + pluginName;
|
||||||
|
var $el = $(this);
|
||||||
|
var $starSet = $el.data(_name).$stars;
|
||||||
|
$starSet.off();
|
||||||
|
$el.removeData(_name).remove();
|
||||||
|
},
|
||||||
|
|
||||||
|
setRating: function(rating, round) {
|
||||||
|
var _name = 'plugin_' + pluginName;
|
||||||
|
var $el = $(this);
|
||||||
|
var $plugin = $el.data(_name);
|
||||||
|
if( rating > $plugin.settings.totalStars || rating < 0 ) { return; }
|
||||||
|
if( round ){
|
||||||
|
rating = Math.round(rating);
|
||||||
|
}
|
||||||
|
$plugin.applyRating(rating);
|
||||||
|
},
|
||||||
|
|
||||||
|
getRating: function() {
|
||||||
|
var _name = 'plugin_' + pluginName;
|
||||||
|
var $el = $(this);
|
||||||
|
var $starSet = $el.data(_name);
|
||||||
|
return $starSet._state.rating;
|
||||||
|
},
|
||||||
|
|
||||||
|
resize: function(newSize) {
|
||||||
|
var _name = 'plugin_' + pluginName;
|
||||||
|
var $el = $(this);
|
||||||
|
var $starSet = $el.data(_name);
|
||||||
|
var $stars = $starSet.$stars;
|
||||||
|
|
||||||
|
if(newSize <= 1 || newSize > 200) {
|
||||||
|
console.log('star size out of bounds');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stars = Array.prototype.slice.call($stars);
|
||||||
|
$stars.forEach(function(star){
|
||||||
|
$(star).css({
|
||||||
|
'width': newSize + 'px',
|
||||||
|
'height': newSize + 'px'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setReadOnly: function(flag) {
|
||||||
|
var _name = 'plugin_' + pluginName;
|
||||||
|
var $el = $(this);
|
||||||
|
var $plugin = $el.data(_name);
|
||||||
|
if(flag === true){
|
||||||
|
$plugin.$stars.off('mouseover mouseout click');
|
||||||
|
} else {
|
||||||
|
$plugin.settings.readOnly = false;
|
||||||
|
$plugin.addListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Avoid Plugin.prototype conflicts
|
||||||
|
$.extend(Plugin.prototype, methods);
|
||||||
|
|
||||||
|
$.fn[ pluginName ] = function ( options ) {
|
||||||
|
|
||||||
|
// if options is a public method
|
||||||
|
if( !$.isPlainObject(options) ){
|
||||||
|
if( publicMethods.hasOwnProperty(options) ){
|
||||||
|
return publicMethods[options].apply(this, Array.prototype.slice.call(arguments, 1));
|
||||||
|
} else {
|
||||||
|
$.error('Method '+ options +' does not exist on ' + pluginName + '.js');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.each(function() {
|
||||||
|
// preventing against multiple instantiations
|
||||||
|
if ( !$.data( this, 'plugin_' + pluginName ) ) {
|
||||||
|
$.data( this, 'plugin_' + pluginName, new Plugin( this, options ) );
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
})( jQuery, window, document );
|
||||||
|
|
||||||
|
|
187
common/templates/common/home.html
Normal file
|
@ -0,0 +1,187 @@
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load admin_url %}
|
||||||
|
{% load mastodon %}
|
||||||
|
{% load oauth_token %}
|
||||||
|
{% load truncate %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% trans 'Boofilsic - 主页' %}</title>
|
||||||
|
<script src="https://cdn.staticfile.org/jquery/3.5.0/jquery.min.js"></script>
|
||||||
|
<script src="{% static 'js/mastodon.js' %}"></script>
|
||||||
|
<script src="{% static 'js/home.js' %}"></script>
|
||||||
|
<link rel="stylesheet" href="{% static 'css/boofilsic_browse.css' %}">
|
||||||
|
<link rel="stylesheet" href="{% static 'lib/css/milligram.css' %}">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="page-wrapper">
|
||||||
|
<div id="content-wrapper">
|
||||||
|
<section id="navbar" class="navbar">
|
||||||
|
<div class="container">
|
||||||
|
<nav class="clearfix">
|
||||||
|
<a href="{% url 'common:home' %}">
|
||||||
|
<img src="{% static 'img/logo.svg' %}" alt="" class="logo">
|
||||||
|
</a>
|
||||||
|
<input type="search" class="search-box" name="keywords" id="searchInput" required="true" placeholder="{% trans '搜索书影音,多个关键字以空格分割' %}">
|
||||||
|
<a class="nav-link" id="logoutLink" href="{% url 'users:logout' %}">{% trans '登出' %}</a>
|
||||||
|
<a class="nav-link" href="{% url 'common:home' %}">{% trans '主页' %}</a>
|
||||||
|
<a class="nav-link" href="{% admin_url %}">{% trans '后台' %}</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="content" class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div id="main">
|
||||||
|
|
||||||
|
<div class="set" id="bookWish">
|
||||||
|
<h5 class="set-title">
|
||||||
|
{% trans '我想看的书' %}
|
||||||
|
</h5>
|
||||||
|
{% if wish_books_more %}
|
||||||
|
<a href="" class="more-link">{% trans '更多' %}</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<ul class="row set-item-list">
|
||||||
|
{% for wish_book in wish_books %}
|
||||||
|
<li class="column column-20 set-item">
|
||||||
|
<!-- <img src="{{ wish_book.cover.url }}" alt="{{ wish_book.title }}"> -->
|
||||||
|
<a href="{% url 'books:retrieve' wish_book.id %}" >
|
||||||
|
<img src="{% static 'img/default.jpg' %}" alt="" class="set-item-image">
|
||||||
|
<span class="set-item-title">{{ wish_book.title | truncate:9 }}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% empty %}
|
||||||
|
<p class="set-empty">暂无记录</p>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="set" id="bookDo">
|
||||||
|
<h5 class="set-title">
|
||||||
|
{% trans '我在看的书' %}
|
||||||
|
</h5>
|
||||||
|
{% if do_books_more %}
|
||||||
|
<a href="" class="more-link">{% trans '更多' %}</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<ul class="row set-item-list">
|
||||||
|
{% for do_book in do_books %}
|
||||||
|
<li class="column column-20 set-item">
|
||||||
|
<!-- <img src="{{ do_book.cover.url }}" alt="{{ do_book.title }}"> -->
|
||||||
|
<a href="{% url 'books:retrieve' do_book.id %}" >
|
||||||
|
<img src="{% static 'img/default.jpg' %}" alt="" class="set-item-image">
|
||||||
|
<span class="set-item-title">{{ do_book.title | truncate:9 }}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% empty %}
|
||||||
|
<p class="set-empty">暂无记录</p>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="set" id="bookCollect">
|
||||||
|
<h5 class="set-title">
|
||||||
|
{% trans '我看过的书' %}
|
||||||
|
</h5>
|
||||||
|
{% if collect_books_more %}
|
||||||
|
<a href="" class="more-link">{% trans '更多' %}</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<ul class="row set-item-list">
|
||||||
|
{% for collect_book in collect_books %}
|
||||||
|
<li class="column column-20 set-item">
|
||||||
|
<!-- <img src="{{ collect_book.cover.url }}" alt="{{ collect_book.title }}"> -->
|
||||||
|
<a href="{% url 'books:retrieve' collect_book.id %}" >
|
||||||
|
<img src="{% static 'img/default.jpg' %}" alt="" class="set-item-image">
|
||||||
|
<span class="set-item-title">{{ collect_book.title | truncate:9 }}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% empty %}
|
||||||
|
<p class="set-empty">暂无记录</p>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="aside">
|
||||||
|
<div class="aside-card mast-user" id="userInfoCard">
|
||||||
|
<div class="clearfix">
|
||||||
|
<img src="" class="info-avatar mast-avatar" alt="{{ user.username }}">
|
||||||
|
<a href="">
|
||||||
|
<h5 class="info-name mast-displayname"></h5>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<p class="info-brief mast-brief"></p>
|
||||||
|
<!-- <a href="#" class="follow">{% trans '关注TA' %}</a> -->
|
||||||
|
<!-- <a href="#" class="report">{% trans '举报用户' %}</a> -->
|
||||||
|
</div>
|
||||||
|
<div class="relation-card" id="userRelationCard">
|
||||||
|
<h5 class="relation-label">
|
||||||
|
{% trans '我关注的人' %}
|
||||||
|
</h5>
|
||||||
|
<a href="" class="more-link mast-following-more">{% trans '更多' %}</a>
|
||||||
|
<ul class="row mast-following relation-user-list">
|
||||||
|
<li class="column column-25 relation-user">
|
||||||
|
<img src="" alt="" class="relation-avatar">
|
||||||
|
<a class="relation-name"></a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<h5 class="relation-label">
|
||||||
|
{% trans '关注我的人' %}
|
||||||
|
</h5>
|
||||||
|
<a href="" class="more-link mast-followers-more">{% trans '更多' %}</a>
|
||||||
|
<ul class="row mast-followers relation-user-list">
|
||||||
|
<li class="column column-25 relation-user">
|
||||||
|
<img src="" alt="" class="relation-avatar">
|
||||||
|
<a class="relation-name"></a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if user.is_staff %}
|
||||||
|
<div class="report-card" id="reportMessageCard">
|
||||||
|
<h5 class="report-label">{% trans '举报信息' %}</h5>
|
||||||
|
<ul class="report-list">
|
||||||
|
{% for report in reports %}
|
||||||
|
<li class="report-message">
|
||||||
|
<a href="" class="report-user-link">{{ report.submit_user }}</a>{% trans '举报了' %}<a href="">{{ report.reported_user }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<footer class="container">
|
||||||
|
<a href="">whitiewhite@donotban.com</a>
|
||||||
|
<a href="" id="githubLink">Github</a>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
|
||||||
|
<div id="mastodonURI" hidden="true">{% mastodon %}</div>
|
||||||
|
<!--current user mastodon id-->
|
||||||
|
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
|
||||||
|
<script>
|
||||||
|
$("#searchInput").on('keyup', function (e) {
|
||||||
|
if (e.keyCode === 13) {
|
||||||
|
let keywords = $(this).val();
|
||||||
|
if (keywords)
|
||||||
|
location.href = "{% url 'common:search' %}" + "?keywords=" + keywords;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
|
||||||
|
</html>
|
189
common/templates/common/search_result.html
Normal file
|
@ -0,0 +1,189 @@
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load admin_url %}
|
||||||
|
{% load mastodon %}
|
||||||
|
{% load oauth_token %}
|
||||||
|
{% load truncate %}
|
||||||
|
{% load highlight %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% trans 'Boofilsic - 搜索结果' %}</title>
|
||||||
|
<script src="https://cdn.staticfile.org/jquery/3.5.0/jquery.min.js"></script>
|
||||||
|
<script src="{% static 'lib/js/rating-star.js' %}"></script>
|
||||||
|
<script src="{% static 'js/search_result.js' %}"></script>
|
||||||
|
<link rel="stylesheet" href="{% static 'css/boofilsic_browse.css' %}">
|
||||||
|
<link rel="stylesheet" href="{% static 'lib/css/rating-star.css' %}">
|
||||||
|
<link rel="stylesheet" href="{% static 'lib/css/milligram.css' %}">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="page-wrapper">
|
||||||
|
<div id="content-wrapper">
|
||||||
|
<section id="navbar" class="navbar">
|
||||||
|
<div class="container">
|
||||||
|
<nav class="clearfix">
|
||||||
|
<a href="{% url 'common:home' %}">
|
||||||
|
<img src="{% static 'img/logo.svg' %}" alt="" class="logo">
|
||||||
|
</a>
|
||||||
|
<input type="search" class="search-box" name="keywords"
|
||||||
|
value="{% if request.GET.keywords %}{{ request.GET.keywords }}{% endif %}" id="searchInput" required="true" placeholder="{% trans '搜索书影音,多个关键字以空格分割' %}">
|
||||||
|
<a class="nav-link" id="logoutLink" href="{% url 'users:logout' %}">{% trans '登出' %}</a>
|
||||||
|
<a class="nav-link" href="{% url 'common:home' %}">{% trans '主页' %}</a>
|
||||||
|
<a class="nav-link" href="{% admin_url %}">{% trans '后台' %}</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="content" class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div id="main">
|
||||||
|
<ul class="result-items">
|
||||||
|
|
||||||
|
{% for book in items %}
|
||||||
|
|
||||||
|
<li class="result-item clearfix">
|
||||||
|
<img src="{% static 'img/default.jpg' %}" alt="" class="result-book-cover">
|
||||||
|
<div class="result-info">
|
||||||
|
|
||||||
|
<a href="{% url 'books:retrieve' book.id %}" class="result-book-title">
|
||||||
|
{% if request.GET.keywords %}
|
||||||
|
{{ book.title | highlight:request.GET.keywords }}
|
||||||
|
{% else %}
|
||||||
|
{{ book.title }}
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
{% if book.rating %}
|
||||||
|
|
||||||
|
<div class="rating-star" data-rating-score="{{ book.rating | floatformat:"0" }}"></div>
|
||||||
|
<span class="rating-score">
|
||||||
|
{{ book.rating }}
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="rating-empty"> {% trans '暂无评分' %}</span>
|
||||||
|
{% endif %}
|
||||||
|
<span class="result-book-info">
|
||||||
|
{% if book.pub_year %}
|
||||||
|
{{ book.pub_year }}{% trans '年' %} /
|
||||||
|
{% if book.pub_month %}
|
||||||
|
{{book.pub_month }}{% trans '月' %} /
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if book.author %}
|
||||||
|
{% trans '作者' %}
|
||||||
|
{% for author in book.author %}
|
||||||
|
{{ author }}{% if not forloop.last %},{% endif %}
|
||||||
|
{% endfor %}/
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if book.translator %}
|
||||||
|
{% trans '译者' %}
|
||||||
|
{% for translator in book.translator %}
|
||||||
|
{{ translator }}{% if not forloop.last %},{% endif %}
|
||||||
|
{% endfor %}/
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if book.orig_title %}
|
||||||
|
{% trans '原名' %}
|
||||||
|
{{ book.orig_title }}
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
<p class="result-book-brief">
|
||||||
|
{{ book.brief | truncate:170 }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% empty %}
|
||||||
|
{% trans '无结果' %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
<div class="pagination" >
|
||||||
|
|
||||||
|
<a
|
||||||
|
{% if items.has_previous %}
|
||||||
|
href="?page=1&keywords={% if request.GET.keywords %}{{ request.GET.keywords }}{% endif %}"
|
||||||
|
{%else %}
|
||||||
|
disabled
|
||||||
|
{% endif %}>
|
||||||
|
<button {% if not items.has_previous %}disabled{% endif %} class="button button-clear">{% trans "首页" %}</button>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
{% if items.has_previous %}
|
||||||
|
href="?page={{ items.previous_page_number }}&keywords={% if request.GET.keywords %}{{ request.GET.keywords }}{% endif %}"
|
||||||
|
{%else %}
|
||||||
|
disabled
|
||||||
|
{% endif %}>
|
||||||
|
<button {% if not items.has_previous %}disabled{% endif %} class="button button-clear">{% trans "上一页" %}</button>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<span class="page-index">
|
||||||
|
{% trans "第" %}{% if request.GET.page %}{{ request.GET.page }}{% else %}1{% endif %}{% trans "页" %}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<a
|
||||||
|
{% if items.has_next %}
|
||||||
|
href="?page={{ items.next_page_number }}&keywords={% if request.GET.keywords %}{{ request.GET.keywords }}{% endif %}"
|
||||||
|
{% else %}
|
||||||
|
disabled
|
||||||
|
{% endif %}
|
||||||
|
>
|
||||||
|
<button {% if not items.has_next %}disabled{% endif %} class="button button-clear">{% trans "下一页" %}</button>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
{% if items.has_next %}
|
||||||
|
href="?page={{ items.paginator.num_pages }}&keywords={% if request.GET.keywords %}{{ request.GET.keywords }}{% endif %}"
|
||||||
|
{%else %}
|
||||||
|
disabled
|
||||||
|
{% endif %}>
|
||||||
|
<button {% if not items.has_next %}disabled{% endif %} class="button button-clear">{% trans "末页" %}</button>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="aside">
|
||||||
|
<div class="aside-card">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
{% trans '没有想要的结果?' %}
|
||||||
|
</div>
|
||||||
|
<a href="{% url 'books:create' %}" class="button add-button">{% trans '添加一个条目' %}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<footer class="container">
|
||||||
|
<a href="">whitiewhite@donotban.com</a>
|
||||||
|
<a href="" id="githubLink">Github</a>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{% comment %}
|
||||||
|
<div id="oauth2Token" hidden="true">{% oauth_token %}</div>
|
||||||
|
<div id="mastodonURI" hidden="true">{% mastodon %}</div>
|
||||||
|
<!--current user mastodon id-->
|
||||||
|
<div id="userMastodonID" hidden="true">{{ user.mastodon_id }}</div>
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$("#searchInput").on('keyup', function (e) {
|
||||||
|
if (e.keyCode === 13) {
|
||||||
|
let keywords = $(this).val();
|
||||||
|
if (keywords)
|
||||||
|
location.href = "{% url 'common:search' %}" + "?keywords=" + keywords;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
|
||||||
|
</html>
|
63
common/templates/widgets/key_value.html
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
<style>
|
||||||
|
.widget-value-key-input input:nth-child(odd) {
|
||||||
|
width: 49%;
|
||||||
|
margin-right: 1%;
|
||||||
|
}
|
||||||
|
.widget-value-key-input input:nth-child(even) {
|
||||||
|
width: 49%;
|
||||||
|
margin-left: 1%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="widget-value-key-input"></div>
|
||||||
|
<input type="text" class="widget-value-key-input-data" hidden name="{{ widget.name }}">
|
||||||
|
<script>
|
||||||
|
//init
|
||||||
|
$(".widget-value-key-input").append('<input type="text"><input type="text">');
|
||||||
|
// add new input pair
|
||||||
|
$(".widget-value-key-input").on('input', ':nth-last-child(1)', function() {
|
||||||
|
let newInputPair = $('<input type="text"><input type="text">');
|
||||||
|
if ($(this).val() && $(this).prev().val()) {
|
||||||
|
$(".widget-value-key-input").append($(newInputPair).clone());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$(".widget-value-key-input").on('input', ':nth-last-child(2)', function() {
|
||||||
|
let newInputPair = $('<input type="text"><input type="text">');
|
||||||
|
if ($(this).val() && $(this).next().val()) {
|
||||||
|
$(".widget-value-key-input").append($(newInputPair).clone());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$(".widget-value-key-input").on('input', ':nth-last-child(4)', function() {
|
||||||
|
if (!$(this).val() && !$(this).next().val() && $(".widget-value-key-input input").length > 2) {
|
||||||
|
$(this).next().remove();
|
||||||
|
$(this).remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$(".widget-value-key-input").on('input', ':nth-last-child(3)', function() {
|
||||||
|
if (!$(this).val() && !$(this).prev().val() && $(".widget-value-key-input input").length > 2) {
|
||||||
|
$(this).prev().remove();
|
||||||
|
$(this).remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".widget-value-key-input").on('input', function() {
|
||||||
|
let keys = $(this).children(":nth-child(odd)").map(function() {
|
||||||
|
if ($(this).val()) {
|
||||||
|
return $(this).val();
|
||||||
|
}
|
||||||
|
}).get();
|
||||||
|
let values = $(this).children(":nth-child(even)").map(function() {
|
||||||
|
if ($(this).val()) {
|
||||||
|
return $(this).val();
|
||||||
|
}
|
||||||
|
}).get();
|
||||||
|
if (keys.length == values.length) {
|
||||||
|
let json = new Object;
|
||||||
|
keys.forEach(function(key, i) {
|
||||||
|
json[key] = values[i];
|
||||||
|
});
|
||||||
|
$("input.widget-value-key-input-data").val(JSON.stringify(json));
|
||||||
|
} else {
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
0
common/templatetags/__init__.py
Normal file
16
common/templatetags/admin_url.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
from django import template
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils.html import format_html
|
||||||
|
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def admin_url():
|
||||||
|
url = settings.ADMIN_URL
|
||||||
|
if not url.startswith('/'):
|
||||||
|
url = '/' + url
|
||||||
|
if not url.endswith('/'):
|
||||||
|
url += '/'
|
||||||
|
return format_html(url)
|
13
common/templatetags/highlight.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
from django import template
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
from django.template.defaultfilters import stringfilter
|
||||||
|
from django.utils.html import format_html
|
||||||
|
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
@stringfilter
|
||||||
|
def highlight(text, search):
|
||||||
|
highlighted = text.replace(search, '<span class="highlight">{}</span>'.format(search))
|
||||||
|
return mark_safe(highlighted)
|
12
common/templatetags/mastodon.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
from django import template
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils.html import format_html
|
||||||
|
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def mastodon():
|
||||||
|
url = 'https://' + settings.MASTODON_DOMAIN_NAME
|
||||||
|
return format_html(url)
|
16
common/templatetags/oauth_token.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
from django import template
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils.html import format_html
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
class OAuthTokenNode(template.Node):
|
||||||
|
def render(self, context):
|
||||||
|
request = context.get('request')
|
||||||
|
oauth_token = request.session.get('oauth_token', default='')
|
||||||
|
return format_html(oauth_token)
|
||||||
|
|
||||||
|
|
||||||
|
@register.tag
|
||||||
|
def oauth_token(parser, token):
|
||||||
|
return OAuthTokenNode()
|
17
common/templatetags/truncate.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
from django import template
|
||||||
|
from django.template.defaultfilters import stringfilter
|
||||||
|
from django.utils.text import Truncator
|
||||||
|
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(is_safe=True)
|
||||||
|
@stringfilter
|
||||||
|
def truncate(value, arg):
|
||||||
|
"""Truncate a string after `arg` number of characters."""
|
||||||
|
try:
|
||||||
|
length = int(arg)
|
||||||
|
except ValueError: # Invalid literal for int().
|
||||||
|
return value # Fail silently.
|
||||||
|
return Truncator(value).chars(length, truncate="...")
|
3
common/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
10
common/urls.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
from django.urls import path
|
||||||
|
from .views import *
|
||||||
|
|
||||||
|
|
||||||
|
app_name = 'common'
|
||||||
|
urlpatterns = [
|
||||||
|
path('', home),
|
||||||
|
path('home/', home, name='home'),
|
||||||
|
path('search/', search, name='search'),
|
||||||
|
]
|
77
common/views.py
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
from django.shortcuts import render
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from books.models import Book
|
||||||
|
from common.models import MarkStatusEnum
|
||||||
|
from users.models import Report, User
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.http import HttpResponseBadRequest
|
||||||
|
|
||||||
|
|
||||||
|
# how many books have in each set at the home page
|
||||||
|
BOOKS_PER_SET = 5
|
||||||
|
|
||||||
|
# how many items are showed in one search result page
|
||||||
|
ITEMS_PER_PAGE = 20
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def home(request):
|
||||||
|
if request.method == 'GET':
|
||||||
|
books = Book.objects.filter(book_marks__owner=request.user)
|
||||||
|
|
||||||
|
do_books = books.filter(book_marks__status=MarkStatusEnum.DO)
|
||||||
|
do_books_more = True if do_books.count() > BOOKS_PER_SET else False
|
||||||
|
wish_books = books.filter(book_marks__status=MarkStatusEnum.WISH)
|
||||||
|
wish_books_more = True if wish_books.count() > BOOKS_PER_SET else False
|
||||||
|
collect_books = books.filter(book_marks__status=MarkStatusEnum.COLLECT)
|
||||||
|
collect_books_more = True if collect_books.count() > BOOKS_PER_SET else False
|
||||||
|
|
||||||
|
reports = Report.objects.order_by('-submitted_time').filter(is_read=False)
|
||||||
|
# reports = Report.objects.latest('submitted_time').filter(is_read=False)
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
'common/home.html',
|
||||||
|
{
|
||||||
|
'do_books': do_books[:BOOKS_PER_SET],
|
||||||
|
'wish_books': wish_books[:BOOKS_PER_SET],
|
||||||
|
'collect_books': collect_books[:BOOKS_PER_SET],
|
||||||
|
'do_books_more': do_books_more,
|
||||||
|
'wish_books_more': wish_books_more,
|
||||||
|
'collect_books_more': collect_books_more,
|
||||||
|
'reports': reports,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
|
||||||
|
def search(request):
|
||||||
|
if request.method == 'GET':
|
||||||
|
# in the future when more modules are added...
|
||||||
|
# category = request.GET.get("category")
|
||||||
|
q = Q()
|
||||||
|
keywords = request.GET.get("keywords", default='').split()
|
||||||
|
query_args = []
|
||||||
|
for keyword in keywords:
|
||||||
|
q = q | Q(title__icontains=keyword)
|
||||||
|
q = q | Q(subtitle__istartswith=keyword)
|
||||||
|
q = q | Q(orig_title__icontains=keyword)
|
||||||
|
query_args.append(q)
|
||||||
|
queryset = Book.objects.filter(*query_args)
|
||||||
|
|
||||||
|
paginator = Paginator(queryset, ITEMS_PER_PAGE)
|
||||||
|
page_number = request.GET.get('page', default=1)
|
||||||
|
items = paginator.get_page(page_number)
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"common/search_result.html",
|
||||||
|
{
|
||||||
|
"items": items,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
return HttpResponseBadRequest()
|
BIN
docs/ui_layout/ui (1).jpg
Normal file
After Width: | Height: | Size: 94 KiB |
BIN
docs/ui_layout/ui (10).jpg
Normal file
After Width: | Height: | Size: 89 KiB |
BIN
docs/ui_layout/ui (11).jpg
Normal file
After Width: | Height: | Size: 77 KiB |
BIN
docs/ui_layout/ui (12).jpg
Normal file
After Width: | Height: | Size: 114 KiB |
BIN
docs/ui_layout/ui (2).jpg
Normal file
After Width: | Height: | Size: 65 KiB |
BIN
docs/ui_layout/ui (3).jpg
Normal file
After Width: | Height: | Size: 73 KiB |
BIN
docs/ui_layout/ui (4).jpg
Normal file
After Width: | Height: | Size: 85 KiB |
BIN
docs/ui_layout/ui (5).jpg
Normal file
After Width: | Height: | Size: 75 KiB |
BIN
docs/ui_layout/ui (6).jpg
Normal file
After Width: | Height: | Size: 78 KiB |
BIN
docs/ui_layout/ui (7).jpg
Normal file
After Width: | Height: | Size: 65 KiB |
BIN
docs/ui_layout/ui (8).jpg
Normal file
After Width: | Height: | Size: 70 KiB |
BIN
docs/ui_layout/ui (9).jpg
Normal file
After Width: | Height: | Size: 93 KiB |
21
manage.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'boofilsic.settings')
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"
|
||||||
|
) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
0
users/__init__.py
Normal file
3
users/admin.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
5
users/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class UsersConfig(AppConfig):
|
||||||
|
name = 'users'
|
88
users/auth.py
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
import requests
|
||||||
|
from django.shortcuts import reverse
|
||||||
|
from django.contrib.auth.backends import ModelBackend, UserModel
|
||||||
|
from boofilsic.settings import MASTODON_DOMAIN_NAME, CLIENT_ID, CLIENT_SECRET
|
||||||
|
from common.mastodon.api import *
|
||||||
|
|
||||||
|
|
||||||
|
class OAuth2Backend(ModelBackend):
|
||||||
|
""" Used to glue OAuth2 and Django User model """
|
||||||
|
# "authenticate() should check the credentials it gets and returns
|
||||||
|
# a user object that matches those credentials."
|
||||||
|
# arg request is an interface specification, not used in this implementation
|
||||||
|
def authenticate(self, request, token=None, username=None, **kwargs):
|
||||||
|
""" when username is provided, assume that token is newly obtained and valid """
|
||||||
|
if token is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if username is None:
|
||||||
|
user_data = get_user_data(token)
|
||||||
|
if user_data:
|
||||||
|
username = user_data['username']
|
||||||
|
else:
|
||||||
|
# aquiring user data fail means token is invalid thus auth fail
|
||||||
|
return None
|
||||||
|
|
||||||
|
# when username is provided, assume that token is newly obtained and valid
|
||||||
|
try:
|
||||||
|
user = UserModel._default_manager.get_by_natural_key(user_data['username'])
|
||||||
|
except UserModel.DoesNotExist:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
if self.user_can_authenticate(user):
|
||||||
|
return user
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def obtain_token(request, code):
|
||||||
|
""" Returns token if success else None. """
|
||||||
|
payload = {
|
||||||
|
'client_id': CLIENT_ID,
|
||||||
|
'client_secret': CLIENT_SECRET,
|
||||||
|
'redirect_uri': f"http://{request.get_host()}{reverse('users:OAuth2_login')}",
|
||||||
|
'grant_type': 'authorization_code',
|
||||||
|
'code': code,
|
||||||
|
'scope': 'read write'
|
||||||
|
}
|
||||||
|
url = 'https://' + MASTODON_DOMAIN_NAME + OAUTH_TOKEN
|
||||||
|
response = requests.post(url, data=payload)
|
||||||
|
if response.status_code != 200:
|
||||||
|
return
|
||||||
|
data = response.json()
|
||||||
|
return data.get('access_token')
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_data(token):
|
||||||
|
url = 'https://' + MASTODON_DOMAIN_NAME + VERIFY_ACCOUNT_CREDENTIALS
|
||||||
|
headers = {
|
||||||
|
'Authorization': f'Bearer {token}'
|
||||||
|
}
|
||||||
|
response = requests.get(url, headers=headers)
|
||||||
|
if response.status_code != 200:
|
||||||
|
return None
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
|
||||||
|
def revoke_token(token):
|
||||||
|
payload = {
|
||||||
|
'client_id': CLIENT_ID,
|
||||||
|
'client_secret': CLIENT_SECRET,
|
||||||
|
'scope': token
|
||||||
|
}
|
||||||
|
url = 'https://' + MASTODON_DOMAIN_NAME + REVOKE_TOKEN
|
||||||
|
response = requests.post(url, data=payload)
|
||||||
|
|
||||||
|
|
||||||
|
def verify_token(token):
|
||||||
|
""" Check if the token is valid and is of local instance. """
|
||||||
|
url = 'https://' + MASTODON_DOMAIN_NAME + VERIFY_ACCOUNT_CREDENTIALS
|
||||||
|
headers = {
|
||||||
|
'Authorization': f'Bearer {token}'
|
||||||
|
}
|
||||||
|
response = requests.get(url, headers=headers)
|
||||||
|
if response.status_code == 200:
|
||||||
|
res_data = response.json()
|
||||||
|
# check if is local instance user
|
||||||
|
if res_data['acct'] == res_data['username']:
|
||||||
|
return True
|
||||||
|
return False
|
34
users/models.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
from django.db import models
|
||||||
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
from datetime import datetime
|
||||||
|
from boofilsic.settings import REPORT_MEDIA_PATH_ROOT, DEFAULT_PASSWORD
|
||||||
|
|
||||||
|
|
||||||
|
def report_image_path(instance, filename):
|
||||||
|
raise NotImplementedError("UUID!!!!!!!!!!!")
|
||||||
|
root = ''
|
||||||
|
if REPORT_MEDIA_PATH_ROOT.endswith('/'):
|
||||||
|
root = REPORT_MEDIA_PATH_ROOT
|
||||||
|
else:
|
||||||
|
root = REPORT_MEDIA_PATH_ROOT + '/'
|
||||||
|
return root + datetime.now().strftime('%Y/%m/%d') + f'{filename}'
|
||||||
|
|
||||||
|
|
||||||
|
class User(AbstractUser):
|
||||||
|
mastodon_id = models.IntegerField()
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
""" Automatically populate password field with DEFAULT_PASSWORD before saving."""
|
||||||
|
self.set_password(DEFAULT_PASSWORD)
|
||||||
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class Report(models.Model):
|
||||||
|
submit_user = models.ForeignKey(User, on_delete=models.SET_NULL, related_name='sumbitted_reports', null=True)
|
||||||
|
reported_user = models.ForeignKey(User, on_delete=models.SET_NULL, related_name='accused_reports', null=True)
|
||||||
|
image = models.ImageField(upload_to=report_image_path, height_field=None, width_field=None, max_length=None, blank=True, default='')
|
||||||
|
is_read = models.BooleanField(default=False)
|
||||||
|
submitted_time = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
27
users/templates/users/login.html
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
|
<title>Document</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="loginBox">
|
||||||
|
<div id="logo">
|
||||||
|
<img src="" alt="boofilsic logo">
|
||||||
|
</div>
|
||||||
|
<div id="loginButton">
|
||||||
|
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<a href="{% url 'common:home' %}"">{% trans '前往我的主页' %}</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ oauth_auth_url }}">{% trans '使用长毛象授权登录' %}</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
16
users/templates/users/register.html
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Register</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
Are you sure you wanna join?
|
||||||
|
<form action="{% url 'users:register' %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="radio" name="confirm" id="confirmRadio">
|
||||||
|
<button type="submit">Cut the sh*t get me in!</button>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
3
users/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
11
users/urls.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
from django.urls import path
|
||||||
|
from .views import *
|
||||||
|
|
||||||
|
app_name = 'users'
|
||||||
|
urlpatterns = [
|
||||||
|
path('login/', login, name='login'),
|
||||||
|
path('register/', register, name='register'),
|
||||||
|
path('logout/', logout, name='logout'),
|
||||||
|
path('delete/', delete, name='delete'),
|
||||||
|
path('OAuth2_login/', OAuth2_login, name='OAuth2_login'),
|
||||||
|
]
|
110
users/views.py
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
from django.shortcuts import reverse, redirect, render
|
||||||
|
from django.http import HttpResponseBadRequest, HttpResponse
|
||||||
|
from django.contrib import auth
|
||||||
|
from django.contrib.auth import authenticate
|
||||||
|
from .models import User
|
||||||
|
from .auth import *
|
||||||
|
from boofilsic.settings import MASTODON_DOMAIN_NAME, CLIENT_ID, CLIENT_SECRET
|
||||||
|
from common.mastodon.api import *
|
||||||
|
|
||||||
|
|
||||||
|
# Views
|
||||||
|
########################################
|
||||||
|
|
||||||
|
# no page rendered
|
||||||
|
def OAuth2_login(request):
|
||||||
|
""" oauth authentication and logging user into django system """
|
||||||
|
if request.method == 'GET':
|
||||||
|
code = request.GET.get('code')
|
||||||
|
# Network IO
|
||||||
|
token = obtain_token(request, code)
|
||||||
|
if token:
|
||||||
|
# oauth is completed when token aquired
|
||||||
|
user = authenticate(request, token=token)
|
||||||
|
if user:
|
||||||
|
auth_login(request, user, token)
|
||||||
|
return redirect(reverse('common:home'))
|
||||||
|
else:
|
||||||
|
# will be passed to register page
|
||||||
|
request.session['new_user_token'] = token
|
||||||
|
return redirect(reverse('users:register'))
|
||||||
|
else:
|
||||||
|
# TODO better fail result page
|
||||||
|
return HttpResponse(content="Authentication failed.")
|
||||||
|
else:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
|
||||||
|
# the 'login' page that user can see
|
||||||
|
def login(request):
|
||||||
|
if request.method == 'GET':
|
||||||
|
# TODO NOTE replace http with https!!!!
|
||||||
|
auth_url = f"https://{MASTODON_DOMAIN_NAME}{OAUTH_AUTHORIZE}?" +\
|
||||||
|
f"client_id={CLIENT_ID}&scope=read+write&" +\
|
||||||
|
f"redirect_uri=http://{request.get_host()}{reverse('users:OAuth2_login')}" +\
|
||||||
|
"&response_type=code"
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
'users/login.html',
|
||||||
|
{
|
||||||
|
'oauth_auth_url': auth_url
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
|
||||||
|
def logout(request):
|
||||||
|
if request.method == 'GET':
|
||||||
|
revoke_token(request.session['oauth_token'])
|
||||||
|
auth_logout(request)
|
||||||
|
return redirect(reverse("users:login"))
|
||||||
|
else:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
|
||||||
|
def register(request):
|
||||||
|
""" register confirm page """
|
||||||
|
if request.method == 'GET':
|
||||||
|
if request.session.get('oauth_token'):
|
||||||
|
return redirect(reverse('common:home'))
|
||||||
|
elif request.session.get('new_user_token'):
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
'users/register.html'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
elif request.method == 'POST':
|
||||||
|
token = request.session['new_user_token']
|
||||||
|
user_data = get_user_data(token)
|
||||||
|
new_user = User(
|
||||||
|
username=user_data['username'],
|
||||||
|
mastodon_id=user_data['id']
|
||||||
|
)
|
||||||
|
new_user.save()
|
||||||
|
del request.session['new_user_token']
|
||||||
|
auth_login(request, new_user, token)
|
||||||
|
return redirect(reverse('common:home'))
|
||||||
|
else:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
|
||||||
|
def delete(request):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
# Utils
|
||||||
|
########################################
|
||||||
|
|
||||||
|
def auth_login(request, user, token):
|
||||||
|
""" Decorates django ``login()``. Attach token to session."""
|
||||||
|
request.session['oauth_token'] = token
|
||||||
|
auth.login(request, user)
|
||||||
|
|
||||||
|
|
||||||
|
def auth_logout(request):
|
||||||
|
""" Decorates django ``logout()``. Release token in session."""
|
||||||
|
del request.session['oauth_token']
|
||||||
|
auth.logout(request)
|