add/edit item page

This commit is contained in:
Your Name 2023-01-05 03:06:13 -05:00
parent 6a392ac954
commit 57ae96873e
31 changed files with 841 additions and 304 deletions

View file

@ -62,6 +62,7 @@ INSTALLED_APPS = [
"legacy.apps.LegacyConfig",
"easy_thumbnails",
"user_messages",
"jsoneditor",
]
MIDDLEWARE = [

View file

@ -37,3 +37,4 @@ if settings.DEBUG:
from django.conf.urls.static import static
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns.append(path("__debug__/", include("debug_toolbar.urls")))

View file

@ -17,6 +17,7 @@ work data seems asymmetric (a book links to a work, but may not listed in that w
"""
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.utils.translation import gettext_lazy as _
from catalog.common import *
@ -36,37 +37,70 @@ class Edition(Item):
METADATA_COPY_LIST = [
"title",
"brief",
# legacy fields
"subtitle",
"orig_title",
"language",
"author",
"translator",
"language",
"pub_house",
"pub_year",
"pub_month",
"binding",
"price",
"pages",
"contents",
"series",
"imprint",
"binding",
"pages",
"series",
"price",
"brief",
"contents",
]
subtitle = jsondata.CharField(null=True, blank=True, default=None)
orig_title = jsondata.CharField(null=True, blank=True, default=None)
author = jsondata.ArrayField(_("作者"), null=False, blank=False, default=list)
translator = jsondata.ArrayField(_("译者"), null=True, blank=True, default=list)
subtitle = jsondata.CharField(
_("副标题"), null=True, blank=True, default=None, max_length=500
)
orig_title = jsondata.CharField(
_("原名"), null=True, blank=True, default=None, max_length=500
)
author = jsondata.ArrayField(
verbose_name=_("作者"),
base_field=models.CharField(max_length=500),
null=False,
blank=False,
default=list,
)
translator = jsondata.ArrayField(
verbose_name=_("译者"),
base_field=models.CharField(max_length=500),
null=True,
blank=True,
default=list,
)
language = jsondata.CharField(_("语言"), null=True, blank=True, default=None)
pub_house = jsondata.CharField(_("出版方"), null=True, blank=True, default=None)
pub_year = jsondata.IntegerField(_("发表年份"), null=True, blank=True)
pub_month = jsondata.IntegerField(_("发表月份"), null=True, blank=True)
binding = jsondata.CharField(null=True, blank=True, default=None)
pages = jsondata.IntegerField(blank=True, default=None)
series = jsondata.CharField(null=True, blank=True, default=None)
contents = jsondata.CharField(null=True, blank=True, default=None)
price = jsondata.CharField(_("价格"), null=True, blank=True)
imprint = jsondata.CharField(_("发表月份"), null=True, blank=True)
pub_house = jsondata.CharField(
_("出版社"), null=True, blank=False, default=None, max_length=500
)
pub_year = jsondata.IntegerField(
_("出版年份"),
null=True,
blank=False,
validators=[MinValueValidator(1), MaxValueValidator(2999)],
)
pub_month = jsondata.IntegerField(
_("出版月份"),
null=True,
blank=False,
validators=[MinValueValidator(1), MaxValueValidator(12)],
)
binding = jsondata.CharField(
_("装订"), null=True, blank=True, default=None, max_length=500
)
pages = jsondata.IntegerField(_("页数"), blank=True, default=None)
series = jsondata.CharField(
_("丛书"), null=True, blank=True, default=None, max_length=500
)
contents = jsondata.TextField(
_("目录"), null=True, blank=True, default=None, max_length=500
)
price = jsondata.CharField(_("价格"), null=True, blank=True, max_length=500)
imprint = jsondata.CharField(_("出品方"), null=True, blank=True, max_length=500)
@property
def isbn10(self):
@ -76,6 +110,24 @@ class Edition(Item):
def isbn10(self, value):
self.isbn = isbn_10_to_13(value)
@classmethod
def lookup_id_type_choices(cls):
id_types = [
IdType.ISBN,
IdType.ASIN,
IdType.CUBN,
IdType.DoubanBook,
IdType.Goodreads,
IdType.GoogleBooks,
]
return [(i.value, i.label) for i in id_types]
@classmethod
def lookup_id_cleanup(cls, lookup_id_type, lookup_id_value):
if lookup_id_type in [IdType.ASIN.value, IdType.ISBN.value]:
return detect_isbn_asin(lookup_id_value)
return super().lookup_id_cleanup(lookup_id_type, lookup_id_value)
def update_linked_items_from_external_resource(self, resource):
"""add Work from resource.metadata['work'] if not yet"""
links = resource.required_resources + resource.related_resources

View file

@ -38,15 +38,15 @@ def isbn_13_to_10(isbn):
def is_isbn_13(isbn):
return re.match(r"\d{13}", isbn) is not None
return re.match(r"^\d{13}$", isbn) is not None
def is_isbn_10(isbn):
return re.match(r"\d{9}[X0-9]", isbn) is not None
return re.match(r"^\d{9}[X0-9]$", isbn) is not None
def is_asin(asin):
return re.match(r"B[A-Z0-9]{9}", asin) is not None
return re.match(r"^B[A-Z0-9]{9}$", asin) is not None
def detect_isbn_asin(s):

View file

@ -1,15 +1,18 @@
import copy
from datetime import date, datetime
from importlib import import_module
from functools import partialmethod
from django.utils.translation import gettext_lazy as _
import django
from django.conf import settings
from django.core.exceptions import FieldError
from django.db.models import fields
from django.utils import dateparse, timezone
from functools import partialmethod
from django.db.models import JSONField
from django.contrib.postgres.fields import ArrayField as DJANGO_ArrayField
# from django.db.models import JSONField as DJANGO_JSONField
from jsoneditor.fields.django3_jsonfield import JSONField as DJANGO_JSONField
__all__ = (
@ -18,6 +21,7 @@ __all__ = (
"DateField",
"DateTimeField",
"DecimalField",
"DurationField",
"EmailField",
"FloatField",
"IntegerField",
@ -28,6 +32,7 @@ __all__ = (
"TimeField",
"URLField",
"ArrayField",
"JSONField",
)
@ -226,5 +231,17 @@ class URLField(JSONFieldMixin, fields.URLField):
pass
class ArrayField(JSONFieldMixin, JSONField):
class ArrayField(JSONFieldMixin, DJANGO_ArrayField):
def __init__(self, *args, **kwargs):
kwargs["help_text"] = _("多项之间以英文逗号分隔")
super().__init__(*args, **kwargs)
pass
class JSONField(JSONFieldMixin, DJANGO_JSONField):
pass
class DurationField(JSONFieldMixin, fields.DurationField):
pass

View file

@ -8,7 +8,7 @@ from django.contrib.contenttypes.models import ContentType
from django.utils.baseconv import base62
from simple_history.models import HistoricalRecords
import uuid
from .utils import DEFAULT_ITEM_COVER, item_cover_path
from .utils import DEFAULT_ITEM_COVER, item_cover_path, resource_cover_path
from .mixins import SoftDeleteMixin
from django.conf import settings
from users.models import User
@ -177,19 +177,17 @@ class Item(SoftDeleteMixin, PolymorphicModel):
category = None # subclass must specify this
demonstrative = None # subclass must specify this
uid = models.UUIDField(default=uuid.uuid4, editable=False, db_index=True)
title = models.CharField(
_("title in primary language"), max_length=1000, default=""
)
title = models.CharField(_("标题"), max_length=1000, default="")
brief = models.TextField(_("简介"), blank=True, default="")
primary_lookup_id_type = models.CharField(
_("isbn/cubn/imdb"), blank=False, null=True, max_length=50
_("主要标识类型"), blank=False, null=True, max_length=50
)
primary_lookup_id_value = models.CharField(
_("1234/tt789"), blank=False, null=True, max_length=1000
_("主要标识数值"), blank=False, null=True, max_length=1000
)
metadata = models.JSONField(_("信息"), blank=True, null=True, default=dict)
metadata = models.JSONField(_("信息"), blank=True, null=True, default=dict)
cover = models.ImageField(
upload_to=item_cover_path, default=DEFAULT_ITEM_COVER, blank=True
_("封面"), upload_to=item_cover_path, default=DEFAULT_ITEM_COVER, blank=True
)
created_time = models.DateTimeField(auto_now_add=True)
edited_time = models.DateTimeField(auto_now=True)
@ -222,6 +220,16 @@ class Item(SoftDeleteMixin, PolymorphicModel):
def __str__(self):
return f"{self.id}|{self.uuid} {self.primary_lookup_id_type}:{self.primary_lookup_id_value if self.primary_lookup_id_value else ''} ({self.title})"
@classmethod
def lookup_id_type_choices(cls):
return IdType.choices
@classmethod
def lookup_id_cleanup(cls, lookup_id_type, lookup_id_value):
if not lookup_id_type or not lookup_id_value or not lookup_id_value.strip():
return None, None
return lookup_id_type, lookup_id_value.strip()
@classmethod
def get_best_lookup_id(cls, lookup_ids):
"""get best available lookup id, ideally commonly used"""
@ -302,16 +310,18 @@ class Item(SoftDeleteMixin, PolymorphicModel):
def has_cover(self):
return self.cover and self.cover != DEFAULT_ITEM_COVER
def merge_data_from_external_resources(self):
def merge_data_from_external_resources(self, ignore_existing_content=False):
"""Subclass may override this"""
lookup_ids = []
for p in self.external_resources.all():
lookup_ids.append((p.id_type, p.id_value))
lookup_ids += p.other_lookup_ids.items()
for k in self.METADATA_COPY_LIST:
if not getattr(self, k) and p.metadata.get(k):
if p.metadata.get(k) and (
not getattr(self, k) or ignore_existing_content
):
setattr(self, k, p.metadata.get(k))
if not self.has_cover() and p.cover:
if p.cover and (not self.has_cover() or ignore_existing_content):
self.cover = p.cover
self.update_lookup_ids(lookup_ids)
@ -351,15 +361,19 @@ class ExternalResource(models.Model):
_("url to the resource"), blank=False, max_length=1000, unique=True
)
cover = models.ImageField(
upload_to=item_cover_path, default=DEFAULT_ITEM_COVER, blank=True
upload_to=resource_cover_path, default=DEFAULT_ITEM_COVER, blank=True
)
other_lookup_ids = models.JSONField(default=dict)
metadata = models.JSONField(default=dict)
scraped_time = models.DateTimeField(null=True)
created_time = models.DateTimeField(auto_now_add=True)
edited_time = models.DateTimeField(auto_now=True)
required_resources = jsondata.ArrayField(null=False, blank=False, default=list)
related_resources = jsondata.ArrayField(null=False, blank=False, default=list)
required_resources = jsondata.ArrayField(
models.CharField(), null=False, blank=False, default=list
)
related_resources = jsondata.ArrayField(
models.CharField(), null=False, blank=False, default=list
)
class Meta:
unique_together = [["id_type", "id_value"]]

View file

@ -163,7 +163,7 @@ class AbstractSite:
if auto_save:
p.save()
if p.item:
p.item.merge_data_from_external_resources()
p.item.merge_data_from_external_resources(ignore_existing_content)
p.item.save()
if auto_link:
for linked_resource in p.required_resources:
@ -221,6 +221,10 @@ class SiteManager:
def get_site_by_resource(resource):
return SiteManager.get_site_by_id_type(resource.id_type)
@staticmethod
def get_all_sites():
return SiteManager.register.values()
ExternalResource.get_site = lambda resource: SiteManager.get_site_by_id_type(
resource.id_type

View file

@ -9,7 +9,7 @@ _logger = logging.getLogger(__name__)
DEFAULT_ITEM_COVER = "item/default.svg"
def item_cover_path(resource, filename):
def resource_cover_path(resource, filename):
fn = (
timezone.now().strftime("%Y/%m/%d/")
+ str(uuid.uuid4())
@ -17,3 +17,13 @@ def item_cover_path(resource, filename):
+ filename.split(".")[-1]
)
return "item/" + resource.id_type + "/" + fn
def item_cover_path(item, filename):
fn = (
timezone.now().strftime("%Y/%m/%d/")
+ str(uuid.uuid4())
+ "."
+ filename.split(".")[-1]
)
return "item/" + item.category + "/" + fn

57
catalog/forms.py Normal file
View file

@ -0,0 +1,57 @@
from django import forms
from django.utils.translation import gettext_lazy as _
from catalog.models import *
from common.forms import PreviewImageInput
CatalogForms = {}
def _EditForm(item_model: Item):
item_fields = (
["id"]
+ item_model.METADATA_COPY_LIST
+ ["cover"]
+ ["primary_lookup_id_type", "primary_lookup_id_value"]
)
if "media" in item_fields:
# FIXME not sure why this field is always duplicated
item_fields.remove("media")
class EditForm(forms.ModelForm):
id = forms.IntegerField(required=False, widget=forms.HiddenInput())
primary_lookup_id_type = forms.ChoiceField(
required=False,
choices=item_model.lookup_id_type_choices(),
label=_("主要标识类型"),
)
primary_lookup_id_value = forms.CharField(
required=False, label=_("主要标识数据通常由系统自动检测,请勿随意更改,不确定留空即可")
)
class Meta:
model = item_model
fields = item_fields
widgets = {
"cover": PreviewImageInput(),
}
def clean(self):
data = super().clean()
t, v = self.Meta.model.lookup_id_cleanup(
data.get("primary_lookup_id_type"), data.get("primary_lookup_id_value")
)
data["primary_lookup_id_type"] = t
data["primary_lookup_id_value"] = v
return data
return EditForm
def init_forms():
for cls in Item.__subclasses__():
CatalogForms[cls.__name__] = _EditForm(cls)
init_forms()

View file

@ -24,44 +24,64 @@ class Game(Item):
]
other_title = jsondata.ArrayField(
models.CharField(blank=True, default="", max_length=500),
base_field=models.CharField(blank=True, default="", max_length=500),
verbose_name=_("其他标题"),
null=True,
blank=True,
default=list,
)
developer = jsondata.ArrayField(
models.CharField(blank=True, default="", max_length=500),
base_field=models.CharField(blank=True, default="", max_length=500),
verbose_name=_("开发商"),
null=True,
blank=True,
default=list,
)
publisher = jsondata.ArrayField(
models.CharField(blank=True, default="", max_length=500),
base_field=models.CharField(blank=True, default="", max_length=500),
verbose_name=_("发行商"),
null=True,
blank=True,
default=list,
)
release_date = jsondata.DateField(
auto_now=False, auto_now_add=False, null=True, blank=True
verbose_name=_("发布日期"),
auto_now=False,
auto_now_add=False,
null=True,
blank=True,
)
genre = jsondata.ArrayField(
models.CharField(blank=True, default="", max_length=200),
verbose_name=_("类型"),
base_field=models.CharField(blank=True, default="", max_length=200),
null=True,
blank=True,
default=list,
)
platform = jsondata.ArrayField(
models.CharField(blank=True, default="", max_length=200),
verbose_name=_("平台"),
base_field=models.CharField(blank=True, default="", max_length=200),
null=True,
blank=True,
default=list,
)
official_site = jsondata.CharField(
verbose_name=_("官方网站"),
default="",
)
@classmethod
def lookup_id_type_choices(cls):
id_types = [
IdType.IGDB,
IdType.Steam,
IdType.DoubanGame,
IdType.Bangumi,
]
return [(i.value, i.label) for i in id_types]

View file

@ -25,53 +25,61 @@ class Movie(Item):
"language",
"year",
"duration",
"season_number",
"episodes",
"single_episode_length",
# "season_number",
# "episodes",
# "single_episode_length",
"brief",
]
orig_title = jsondata.CharField(
_("original title"), blank=True, default="", max_length=500
verbose_name=_("原始标题"), blank=True, default="", max_length=500
)
other_title = jsondata.ArrayField(
models.CharField(_("other title"), blank=True, default="", max_length=500),
base_field=models.CharField(blank=True, default="", max_length=500),
verbose_name=_("其他标题"),
null=True,
blank=True,
default=list,
)
director = jsondata.ArrayField(
models.CharField(_("director"), blank=True, default="", max_length=200),
verbose_name=_("导演"),
base_field=models.CharField(blank=True, default="", max_length=200),
null=True,
blank=True,
default=list,
)
playwright = jsondata.ArrayField(
models.CharField(_("playwright"), blank=True, default="", max_length=200),
verbose_name=_("编剧"),
base_field=models.CharField(blank=True, default="", max_length=200),
null=True,
blank=True,
default=list,
)
actor = jsondata.ArrayField(
models.CharField(_("actor"), blank=True, default="", max_length=200),
verbose_name=_("演员"),
base_field=models.CharField(blank=True, default="", max_length=200),
null=True,
blank=True,
default=list,
)
genre = jsondata.ArrayField(
models.CharField(_("genre"), blank=True, default="", max_length=50),
verbose_name=_("类型"),
base_field=models.CharField(blank=True, default="", max_length=50),
null=True,
blank=True,
default=list,
) # , choices=MovieGenreEnum.choices
showtime = jsondata.ArrayField(
showtime = jsondata.JSONField(
_("上映日期"),
null=True,
blank=True,
default=list,
)
site = jsondata.URLField(_("site url"), blank=True, default="", max_length=200)
site = jsondata.URLField(
verbose_name=_("官方网站"), blank=True, default="", max_length=200
)
area = jsondata.ArrayField(
models.CharField(
_("country or region"),
verbose_name=_("国家地区"),
base_field=models.CharField(
blank=True,
default="",
max_length=100,
@ -81,7 +89,8 @@ class Movie(Item):
default=list,
)
language = jsondata.ArrayField(
models.CharField(
verbose_name=_("语言"),
base_field=models.CharField(
blank=True,
default="",
max_length=100,
@ -90,8 +99,35 @@ class Movie(Item):
blank=True,
default=list,
)
year = jsondata.IntegerField(null=True, blank=True)
season_number = jsondata.IntegerField(null=True, blank=True)
episodes = jsondata.IntegerField(null=True, blank=True)
single_episode_length = jsondata.IntegerField(null=True, blank=True)
duration = jsondata.CharField(blank=True, default="", max_length=200)
year = jsondata.IntegerField(verbose_name=_("年份"), null=True, blank=True)
duration = jsondata.CharField(
verbose_name=_("片长"), blank=True, default="", max_length=200
)
season_number = jsondata.IntegerField(
null=True, blank=True
) # TODO remove after migration
episodes = jsondata.IntegerField(
null=True, blank=True
) # TODO remove after migration
single_episode_length = jsondata.IntegerField(
null=True, blank=True
) # TODO remove after migration
@classmethod
def lookup_id_type_choices(cls):
id_types = [
IdType.IMDB,
IdType.TMDB_Movie,
IdType.DoubanMovie,
IdType.Bangumi,
]
return [(i.value, i.label) for i in id_types]
@classmethod
def lookup_id_cleanup(cls, lookup_id_type, lookup_id_value):
if lookup_id_type == IdType.IMDB.value and lookup_id_value:
if lookup_id_value[:2] == "tt":
return lookup_id_type, lookup_id_value
else:
return None, None
return super().lookup_id_cleanup(lookup_id_type, lookup_id_value)

View file

@ -13,38 +13,47 @@ class Album(Item):
METADATA_COPY_LIST = [
"title",
"other_title",
"album_type",
"media",
"disc_count",
"artist",
"genre",
"release_date",
"duration",
"company",
"track_list",
"brief",
"album_type",
"media",
"disc_count",
"genre",
"release_date",
"duration",
"bandcamp_album_id",
]
release_date = jsondata.DateField(
_("发行日期"), auto_now=False, auto_now_add=False, null=True, blank=True
)
release_date = jsondata.DateField(_("发行日期"), null=True, blank=True)
duration = jsondata.IntegerField(_("时长"), null=True, blank=True)
artist = jsondata.ArrayField(
models.CharField(_("artist"), blank=True, default="", max_length=200),
null=True,
blank=True,
models.CharField(blank=True, default="", max_length=200),
verbose_name=_("艺术家"),
default=list,
)
genre = jsondata.CharField(_("流派"), blank=True, default="", max_length=100)
company = jsondata.ArrayField(
models.CharField(blank=True, default="", max_length=500),
verbose_name=_("发行方"),
null=True,
blank=True,
default=list,
)
track_list = jsondata.TextField(_("曲目"), blank=True, default="")
other_title = jsondata.CharField(blank=True, default="", max_length=500)
album_type = jsondata.CharField(blank=True, default="", max_length=500)
media = jsondata.CharField(blank=True, default="", max_length=500)
other_title = jsondata.CharField(_("其它标题"), blank=True, default="", max_length=500)
album_type = jsondata.CharField(_("专辑类型"), blank=True, default="", max_length=500)
media = jsondata.CharField(_("介质"), blank=True, default="", max_length=500)
bandcamp_album_id = jsondata.CharField(blank=True, default="", max_length=500)
disc_count = jsondata.IntegerField(blank=True, default="", max_length=500)
disc_count = jsondata.IntegerField(_("碟片数"), blank=True, default="", max_length=500)
@classmethod
def lookup_id_type_choices(cls):
id_types = [
IdType.GTIN,
IdType.ISRC,
IdType.Spotify_Album,
IdType.Bandcamp,
IdType.DoubanMusic,
]
return [(i.value, i.label) for i in id_types]

View file

@ -1,15 +1,40 @@
from catalog.common import *
from django.utils.translation import gettext_lazy as _
from django.db import models
class Performance(Item):
category = ItemCategory.Performance
url_path = "performance"
douban_drama = LookupIdDescriptor(IdType.DoubanDrama)
versions = jsondata.ArrayField(_("版本"), null=False, blank=False, default=list)
directors = jsondata.ArrayField(_("导演"), null=False, blank=False, default=list)
playwrights = jsondata.ArrayField(_("编剧"), null=False, blank=False, default=list)
actors = jsondata.ArrayField(_("主演"), null=False, blank=False, default=list)
versions = jsondata.ArrayField(
verbose_name=_("版本"),
base_field=models.CharField(),
null=False,
blank=False,
default=list,
)
directors = jsondata.ArrayField(
verbose_name=_("导演"),
base_field=models.CharField(),
null=False,
blank=False,
default=list,
)
playwrights = jsondata.ArrayField(
verbose_name=_("编剧"),
base_field=models.CharField(),
null=False,
blank=False,
default=list,
)
actors = jsondata.ArrayField(
verbose_name=_("主演"),
base_field=models.CharField(),
null=False,
blank=False,
default=list,
)
class Meta:
proxy = True

View file

@ -1,14 +1,17 @@
from catalog.common import *
from django.db import models
from django.utils.translation import gettext_lazy as _
class Podcast(Item):
category = ItemCategory.Podcast
url_path = "podcast"
demonstrative = _("这个播客")
feed_url = PrimaryLookupIdDescriptor(IdType.Feed)
apple_podcast = PrimaryLookupIdDescriptor(IdType.ApplePodcast)
# ximalaya = LookupIdDescriptor(IdType.Ximalaya)
# xiaoyuzhou = LookupIdDescriptor(IdType.Xiaoyuzhou)
hosts = jsondata.ArrayField(default=list)
hosts = jsondata.ArrayField(models.CharField(), default=list)
# class PodcastEpisode(Item):

195
catalog/search/views.py Normal file
View file

@ -0,0 +1,195 @@
import uuid
import logging
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required, permission_required
from django.utils.translation import gettext_lazy as _
from django.http import (
HttpResponseBadRequest,
HttpResponseServerError,
HttpResponse,
HttpResponseRedirect,
HttpResponseNotFound,
)
from django.contrib.auth.decorators import login_required, permission_required
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.db import IntegrityError, transaction
from django.db.models import Count
from django.utils import timezone
from django.core.paginator import Paginator
from polymorphic.base import django
from catalog.common.models import SiteName
from catalog.common.sites import AbstractSite, SiteManager
from mastodon import mastodon_request_included
from mastodon.models import MastodonApplication
from mastodon.api import share_mark, share_review
from ..models import *
from django.conf import settings
from django.utils.baseconv import base62
from journal.models import Mark, ShelfMember, Review
from journal.models import query_visible, query_following
from common.utils import PageLinksGenerator
from common.config import PAGE_LINK_NUMBER
from journal.models import ShelfTypeNames
import django_rq
from rq.job import Job
from .external import ExternalSources
_logger = logging.getLogger(__name__)
class HTTPResponseHXRedirect(HttpResponseRedirect):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self["HX-Redirect"] = self["Location"]
status_code = 200
@login_required
def fetch_refresh(request, job_id):
retry = request.GET
job = Job.fetch(id=job_id, connection=django_rq.get_connection("fetch"))
item_url = job.result if job else "-" # FIXME job.return_value() in rq 1.12
if item_url:
if item_url == "-":
return render(request, "fetch_failed.html")
else:
return HTTPResponseHXRedirect(item_url)
else:
retry = int(request.GET.get("retry", 0)) + 1
if retry > 10:
return render(request, "fetch_failed.html")
else:
return render(
request,
"fetch_refresh.html",
{"job_id": job_id, "retry": retry, "delay": retry * 2},
)
def fetch(request, url, is_refetch: bool = False, site: AbstractSite = None):
if not site:
site = SiteManager.get_site_by_url(url)
if not site:
return HttpResponseBadRequest()
item = site.get_item()
if item and not is_refetch:
return redirect(item.url)
job_id = uuid.uuid4().hex
django_rq.get_queue("fetch").enqueue(fetch_task, url, is_refetch, job_id=job_id)
return render(
request,
"fetch_pending.html",
{
"site": site,
"job_id": job_id,
},
)
def search(request):
category = request.GET.get("c", default="all").strip().lower()
if category == "all":
category = None
keywords = request.GET.get("q", default="").strip()
tag = request.GET.get("tag", default="").strip()
p = request.GET.get("page", default="1")
page_number = int(p) if p.isdigit() else 1
if not (keywords or tag):
return render(
request,
"common/search_result.html",
{
"items": None,
},
)
if request.user.is_authenticated and keywords.find("://") > 0:
site = SiteManager.get_site_by_url(keywords)
if site:
return fetch(request, keywords, site)
if settings.SEARCH_BACKEND is None:
# return limited results if no SEARCH_BACKEND
result = {
"items": Items.objects.filter(title__like=f"%{keywords}%")[:10],
"num_pages": 1,
}
else:
result = Indexer.search(keywords, page=page_number, category=category, tag=tag)
keys = []
items = []
urls = []
for i in result.items:
key = (
i.isbn
if hasattr(i, "isbn")
else (i.imdb_code if hasattr(i, "imdb_code") else None)
)
if key is None:
items.append(i)
elif key not in keys:
keys.append(key)
items.append(i)
for res in i.external_resources.all():
urls.append(res.url)
# if request.path.endswith(".json/"):
# return JsonResponse(
# {
# "num_pages": result.num_pages,
# "items": list(map(lambda i: i.get_json(), items)),
# }
# )
request.session["search_dedupe_urls"] = urls
return render(
request,
"search_results.html",
{
"items": items,
"pagination": PageLinksGenerator(
PAGE_LINK_NUMBER, page_number, result.num_pages
),
"categories": ["book", "movie", "music", "game"],
"sites": SiteName.labels,
"hide_category": category is not None,
},
)
@login_required
def external_search(request):
category = request.GET.get("c", default="all").strip().lower()
if category == "all":
category = None
keywords = request.GET.get("q", default="").strip()
page_number = int(request.GET.get("page", default=1))
items = ExternalSources.search(category, keywords, page_number) if keywords else []
dedupe_urls = request.session.get("search_dedupe_urls", [])
items = [i for i in items if i.source_url not in dedupe_urls]
return render(
request,
"external_search_results.html",
{
"external_items": items,
},
)
def refetch(request):
url = request.POST.get("url")
if not url:
return HttpResponseBadRequest()
return fetch(request, url, True)
def fetch_task(url, is_refetch):
item_url = "-"
try:
site = SiteManager.get_site_by_url(url)
site.get_resource_ready(ignore_existing_content=is_refetch)
item = site.get_item()
if item:
_logger.info(f"fetched {url} {item.url} {item}")
item_url = item.url
finally:
return item_url

View file

@ -9,6 +9,7 @@
{% load truncate %}
{% load strip_scheme %}
{% load thumb %}
{% load duration %}
<!-- class specific details -->
{% block details %}
@ -65,7 +66,7 @@
{% endif %}
</div>
<div>{% if item.duration %}
{% trans '时长:' %}{{ item.get_duration_display }}
{% trans '时长:' %}{{ item.duration|duration_format }}
{% endif %}
</div>
<div>{% if item.genre %}

View file

@ -0,0 +1,60 @@
{% 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>{{ site_name }} - {% if form.instance.id %}{% trans '编辑' %} {{ form.instance.title }} {% else %}{% trans '添加' %}{% endif %}</title>
{% include "common_libs.html" with jquery=1 %}
</head>
<body>
<div id="page-wrapper">
<div id="content-wrapper">
{% include "partial/_navbar.html" %}
<section id="content" class="container">
<div class="grid">
<div style="float:right;padding-left:16px">
<div class="aside-section-wrapper">
{% for res in form.instance.external_resources.all %}
<div class="action-panel">
<div class="action-panel__label">{% trans '源网站' %}: <a href="{{ res.url }}">{{ res.site_name.label }}</a></div>
<div class="action-panel__button-group">
<form method="post" action="{% url 'catalog:refetch' %}">
{% csrf_token %}
<input type="hidden" name="id" value="{{ res.id }}" >
<input type="hidden" name="url" value="{{ res.url }}" >
<input class="button" type="submit" value="{% trans '从源网站重新抓取' %}">
</form>
</div>
</div>
{% endfor %}
</div>
</div>
<div class="single-section-wrapper" id="main">
<form class="entity-form" action="{{ submit_url }}" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.media }}
{{ form }}
<hr/>
<input class="button" type="submit" value="{% trans '提交' %}">
<a href="javascript:history.go(-1)" style="float:right;">返回</a>
</form>
</div>
</section>
</div>
{% include "partial/_footer.html" %}
</div>
</body>
</html>

View file

@ -34,7 +34,7 @@
<div class="grid__main">
<div class="main-section-wrapper">
<div>
{% trans '正在连线' %}{{ site.SITE_NAME }}
{% trans '正在连线' %}{{ site.SITE_NAME.label }}
<div hx-get="{% url 'catalog:fetch_refresh' job_id %}" hx-trigger="load delay:2s" hx-swap="outerHTML"></div>
<div id="spinner">
<div class="spinner">

View file

@ -111,19 +111,30 @@
<div class="add-entity-entries__label">
{% trans '没有想要的结果?' %}
</div>
<a href="#">
<p>
如果在
{% for site in sites %}
{{ site }}
{% if not forloop.last %}/{% endif %}
{% endfor %}
找到了条目,可以在搜索栏中输入完整链接提交。
</p>
<p>
当然也可以手工创建条目。
</p>
<a href="{% url 'catalog:create' 'Edition' %}">
<button class="add-entity-entries__button">{% trans '添加书' %}</button>
</a>
<a href="#">
<a href="{% url 'catalog:create' 'Movie' %}">
<button class="add-entity-entries__button">{% trans '添加电影' %}</button>
</a>
<a href="#">
<a href="{% url 'catalog:create' 'TVShow' %}">
<button class="add-entity-entries__button">{% trans '添加剧集' %}</button>
</a>
<a href="#">
<a href="{% url 'catalog:create' 'Album' %}">
<button class="add-entity-entries__button">{% trans '添加专辑' %}</button>
</a>
<a href="#">
<a href="{% url 'catalog:create' 'Game' %}">
<button class="add-entity-entries__button">{% trans '添加游戏' %}</button>
</a>
</div>

View file

@ -36,8 +36,10 @@ class TVShow(Item):
imdb = PrimaryLookupIdDescriptor(IdType.IMDB)
tmdb_tv = PrimaryLookupIdDescriptor(IdType.TMDB_TV)
imdb = PrimaryLookupIdDescriptor(IdType.IMDB)
season_count = models.IntegerField(null=True)
episode_count = models.PositiveIntegerField(null=True)
season_count = models.IntegerField(verbose_name=_("总季数"), null=True, blank=True)
episode_count = models.PositiveIntegerField(
verbose_name=_("总集数"), null=True, blank=True
)
METADATA_COPY_LIST = [
"title",
@ -47,6 +49,7 @@ class TVShow(Item):
"director",
"playwright",
"actor",
"brief",
"genre",
"showtime",
"site",
@ -54,53 +57,59 @@ class TVShow(Item):
"language",
"year",
"duration",
"season_count",
"episode_count",
"single_episode_length",
"brief",
]
orig_title = jsondata.CharField(
_("original title"), blank=True, default="", max_length=500
verbose_name=_("原始标题"), blank=True, default="", max_length=500
)
other_title = jsondata.ArrayField(
models.CharField(_("other title"), blank=True, default="", max_length=500),
base_field=models.CharField(blank=True, default="", max_length=500),
verbose_name=_("其他标题"),
null=True,
blank=True,
default=list,
)
director = jsondata.ArrayField(
models.CharField(_("director"), blank=True, default="", max_length=200),
verbose_name=_("导演"),
base_field=models.CharField(blank=True, default="", max_length=200),
null=True,
blank=True,
default=list,
)
playwright = jsondata.ArrayField(
models.CharField(_("playwright"), blank=True, default="", max_length=200),
verbose_name=_("编剧"),
base_field=models.CharField(blank=True, default="", max_length=200),
null=True,
blank=True,
default=list,
)
actor = jsondata.ArrayField(
models.CharField(_("actor"), blank=True, default="", max_length=200),
verbose_name=_("演员"),
base_field=models.CharField(blank=True, default="", max_length=200),
null=True,
blank=True,
default=list,
)
genre = jsondata.ArrayField(
models.CharField(_("genre"), blank=True, default="", max_length=50),
verbose_name=_("类型"),
base_field=models.CharField(blank=True, default="", max_length=50),
null=True,
blank=True,
default=list,
) # , choices=MovieGenreEnum.choices
showtime = jsondata.ArrayField(
showtime = jsondata.JSONField(
_("播出日期"),
null=True,
blank=True,
default=list,
)
site = jsondata.URLField(_("site url"), blank=True, default="", max_length=200)
site = jsondata.URLField(
verbose_name=_("官方网站"), blank=True, default="", max_length=200
)
area = jsondata.ArrayField(
models.CharField(
_("country or region"),
verbose_name=_("国家地区"),
base_field=models.CharField(
blank=True,
default="",
max_length=100,
@ -110,7 +119,8 @@ class TVShow(Item):
default=list,
)
language = jsondata.ArrayField(
models.CharField(
verbose_name=_("语言"),
base_field=models.CharField(
blank=True,
default="",
max_length=100,
@ -119,27 +129,44 @@ class TVShow(Item):
blank=True,
default=list,
)
year = jsondata.IntegerField(null=True, blank=True)
season_number = jsondata.IntegerField(null=True, blank=True)
single_episode_length = jsondata.IntegerField(null=True, blank=True)
duration = jsondata.CharField(blank=True, default="", max_length=200)
year = jsondata.IntegerField(verbose_name=_("年份"), null=True, blank=True)
single_episode_length = jsondata.IntegerField(
verbose_name=_("单集长度"), null=True, blank=True
)
season_number = jsondata.IntegerField(
null=True, blank=True
) # TODO remove after migration
duration = jsondata.CharField(
blank=True, default="", max_length=200
) # TODO remove after migration
@classmethod
def lookup_id_type_choices(cls):
id_types = [
IdType.IMDB,
IdType.TMDB_TV,
IdType.DoubanMovie,
IdType.Bangumi,
]
return [(i.value, i.label) for i in id_types]
class TVSeason(Item):
category = ItemCategory.TV
url_path = "tv/season"
demonstrative = _("这部剧集")
demonstrative = _("剧集")
douban_movie = PrimaryLookupIdDescriptor(IdType.DoubanMovie)
imdb = PrimaryLookupIdDescriptor(IdType.IMDB)
tmdb_tvseason = PrimaryLookupIdDescriptor(IdType.TMDB_TVSeason)
show = models.ForeignKey(
TVShow, null=True, on_delete=models.SET_NULL, related_name="seasons"
)
season_number = models.PositiveIntegerField(null=True)
episode_count = models.PositiveIntegerField(null=True)
season_number = models.PositiveIntegerField(verbose_name=_("本季序号"), null=True)
episode_count = models.PositiveIntegerField(verbose_name=_("本季集数"), null=True)
METADATA_COPY_LIST = [
"title",
"season_number",
"orig_title",
"other_title",
"director",
@ -152,53 +179,60 @@ class TVSeason(Item):
"language",
"year",
"duration",
"season_number",
"episode_count",
"single_episode_length",
"brief",
]
orig_title = jsondata.CharField(
_("original title"), blank=True, default="", max_length=500
verbose_name=_("原始标题"), blank=True, default="", max_length=500
)
other_title = jsondata.ArrayField(
models.CharField(_("other title"), blank=True, default="", max_length=500),
verbose_name=_("其他标题"),
base_field=models.CharField(blank=True, default="", max_length=500),
null=True,
blank=True,
default=list,
)
director = jsondata.ArrayField(
models.CharField(_("director"), blank=True, default="", max_length=200),
verbose_name=_("导演"),
base_field=models.CharField(blank=True, default="", max_length=200),
null=True,
blank=True,
default=list,
)
playwright = jsondata.ArrayField(
models.CharField(_("playwright"), blank=True, default="", max_length=200),
verbose_name=_("编剧"),
base_field=models.CharField(blank=True, default="", max_length=200),
null=True,
blank=True,
default=list,
)
actor = jsondata.ArrayField(
models.CharField(_("actor"), blank=True, default="", max_length=200),
verbose_name=_("演员"),
base_field=models.CharField(blank=True, default="", max_length=200),
null=True,
blank=True,
default=list,
)
genre = jsondata.ArrayField(
models.CharField(_("genre"), blank=True, default="", max_length=50),
verbose_name=_("类型"),
base_field=models.CharField(blank=True, default="", max_length=50),
null=True,
blank=True,
default=list,
) # , choices=MovieGenreEnum.choices
showtime = jsondata.ArrayField(
showtime = jsondata.JSONField(
_("播出日期"),
null=True,
blank=True,
default=list,
)
site = jsondata.URLField(_("site url"), blank=True, default="", max_length=200)
site = jsondata.URLField(
verbose_name=_("官方网站"), blank=True, default="", max_length=200
)
area = jsondata.ArrayField(
models.CharField(
_("country or region"),
verbose_name=_("国家地区"),
base_field=models.CharField(
blank=True,
default="",
max_length=100,
@ -208,7 +242,8 @@ class TVSeason(Item):
default=list,
)
language = jsondata.ArrayField(
models.CharField(
verbose_name=_("语言"),
base_field=models.CharField(
blank=True,
default="",
max_length=100,
@ -217,9 +252,22 @@ class TVSeason(Item):
blank=True,
default=list,
)
year = jsondata.IntegerField(null=True, blank=True)
single_episode_length = jsondata.IntegerField(null=True, blank=True)
duration = jsondata.CharField(blank=True, default="", max_length=200)
year = jsondata.IntegerField(verbose_name=_("年份"), null=True, blank=True)
single_episode_length = jsondata.IntegerField(
verbose_name=_("单集长度"), null=True, blank=True
)
duration = jsondata.CharField(
blank=True, default="", max_length=200
) # TODO remove after migration
@classmethod
def lookup_id_type_choices(cls):
id_types = [
IdType.IMDB,
IdType.TMDB_TVSeason,
IdType.DoubanMovie,
]
return [(i.value, i.label) for i in id_types]
def update_linked_items_from_external_resource(self, resource):
"""add Work from resource.metadata['work'] if not yet"""

View file

@ -18,7 +18,7 @@ def _get_all_url_paths():
urlpatterns = [
re_path(
r"^item/(?P<item_uid>[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12})?$",
r"^item/(?P<item_uid>[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12})$",
retrieve_by_uuid,
name="retrieve_by_uuid",
),
@ -29,6 +29,7 @@ urlpatterns = [
retrieve,
name="retrieve",
),
path("catalog/create/<str:item_model>", create, name="create"),
re_path(
r"^(?P<item_path>"
+ _get_all_url_paths()
@ -60,5 +61,6 @@ urlpatterns = [
path("search/", search, name="search"),
path("search/external/", external_search, name="external_search"),
path("fetch_refresh/<str:job_id>", fetch_refresh, name="fetch_refresh"),
path("refetch", refetch, name="refetch"),
path("api/", api.urls),
]

View file

@ -32,6 +32,9 @@ from journal.models import ShelfTypeNames
import django_rq
from rq.job import Job
from .search.external import ExternalSources
from .forms import *
from .search.views import *
from pprint import pprint
_logger = logging.getLogger(__name__)
@ -40,14 +43,6 @@ NUM_REVIEWS_ON_ITEM_PAGE = 5
NUM_REVIEWS_ON_LIST_PAGE = 20
class HTTPResponseHXRedirect(HttpResponseRedirect):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self["HX-Redirect"] = self["Location"]
status_code = 200
def retrieve_by_uuid(request, item_uid):
item = get_object_or_404(Item, uid=item_uid)
return redirect(item.url)
@ -109,6 +104,74 @@ def retrieve(request, item_path, item_uuid):
return HttpResponseBadRequest()
@login_required
def create(request, item_model):
if request.method == "GET":
form_cls = CatalogForms[item_model]
form = form_cls()
return render(
request,
"catalog_edit.html",
{
"form": form,
},
)
elif request.method == "POST":
form_cls = CatalogForms[item_model]
form = form_cls(request.POST, request.FILES)
if form.is_valid():
form.instance.last_editor = request.user
form.instance.edited_time = timezone.now()
form.instance.save()
return redirect(form.instance.url)
else:
pprint(form.errors)
return HttpResponseBadRequest(form.errors)
else:
return HttpResponseBadRequest()
@login_required
def edit(request, item_path, item_uuid):
if request.method == "GET":
item = get_object_or_404(Item, uid=base62.decode(item_uuid))
form_cls = CatalogForms[item.__class__.__name__]
form = form_cls(instance=item)
if item.external_resources.all().count() > 0:
form.fields["primary_lookup_id_type"].disabled = True
form.fields["primary_lookup_id_value"].disabled = True
return render(
request,
"catalog_edit.html",
{
"form": form,
"is_update": True,
},
)
elif request.method == "POST":
item = get_object_or_404(Item, uid=base62.decode(item_uuid))
form_cls = CatalogForms[item.__class__.__name__]
form = form_cls(request.POST, request.FILES, instance=item)
if item.external_resources.all().count() > 0:
form.fields["primary_lookup_id_type"].disabled = True
form.fields["primary_lookup_id_value"].disabled = True
if form.is_valid():
form.instance.last_editor = request.user
form.instance.edited_time = timezone.now()
form.instance.save()
return redirect(form.instance.url)
else:
pprint(form.errors)
return HttpResponseBadRequest(form.errors)
else:
return HttpResponseBadRequest()
@login_required
def delete(request, item_path, item_uuid):
return HttpResponseBadRequest()
@login_required
def mark_list(request, item_path, item_uuid, following_only=False):
item = get_object_or_404(Item, uid=base62.decode(item_uuid))
@ -155,153 +218,3 @@ def review_list(request, item_path, item_uuid):
"item": item,
},
)
def fetch_task(url):
try:
site = SiteManager.get_site_by_url(url)
site.get_resource_ready()
item = site.get_item()
return item.url if item else "-"
except Exception:
return "-"
@login_required
def fetch_refresh(request, job_id):
retry = request.GET
job = Job.fetch(id=job_id, connection=django_rq.get_connection("fetch"))
item_url = job.result if job else "-" # FIXME job.return_value() in rq 1.12
if item_url:
if item_url == "-":
return render(request, "fetch_failed.html")
else:
return HTTPResponseHXRedirect(item_url)
else:
retry = int(request.GET.get("retry", 0)) + 1
if retry > 10:
return render(request, "fetch_failed.html")
else:
return render(
request,
"fetch_refresh.html",
{"job_id": job_id, "retry": retry, "delay": retry * 2},
)
@login_required
def fetch(request, url, site: AbstractSite = None):
if not site:
site = SiteManager.get_site_by_url(url)
if not site:
return HttpResponseBadRequest()
item = site.get_item()
if item:
return redirect(item.url)
job_id = uuid.uuid4().hex
django_rq.get_queue("fetch").enqueue(fetch_task, url, job_id=job_id)
return render(
request,
"fetch_pending.html",
{
"site": site,
"job_id": job_id,
},
)
def search(request):
category = request.GET.get("c", default="all").strip().lower()
if category == "all":
category = None
keywords = request.GET.get("q", default="").strip()
tag = request.GET.get("tag", default="").strip()
p = request.GET.get("page", default="1")
page_number = int(p) if p.isdigit() else 1
if not (keywords or tag):
return render(
request,
"common/search_result.html",
{
"items": None,
},
)
if request.user.is_authenticated and keywords.find("://") > 0:
site = SiteManager.get_site_by_url(keywords)
if site:
return fetch(request, keywords, site)
if settings.SEARCH_BACKEND is None:
# return limited results if no SEARCH_BACKEND
result = {
"items": Items.objects.filter(title__like=f"%{keywords}%")[:10],
"num_pages": 1,
}
else:
result = Indexer.search(keywords, page=page_number, category=category, tag=tag)
keys = []
items = []
urls = []
for i in result.items:
key = (
i.isbn
if hasattr(i, "isbn")
else (i.imdb_code if hasattr(i, "imdb_code") else None)
)
if key is None:
items.append(i)
elif key not in keys:
keys.append(key)
items.append(i)
for res in i.external_resources.all():
urls.append(res.url)
# if request.path.endswith(".json/"):
# return JsonResponse(
# {
# "num_pages": result.num_pages,
# "items": list(map(lambda i: i.get_json(), items)),
# }
# )
request.session["search_dedupe_urls"] = urls
return render(
request,
"search_results.html",
{
"items": items,
"pagination": PageLinksGenerator(
PAGE_LINK_NUMBER, page_number, result.num_pages
),
"categories": ["book", "movie", "music", "game"],
"hide_category": category is not None,
},
)
@login_required
def external_search(request):
category = request.GET.get("c", default="all").strip().lower()
if category == "all":
category = None
keywords = request.GET.get("q", default="").strip()
page_number = int(request.GET.get("page", default=1))
items = ExternalSources.search(category, keywords, page_number) if keywords else []
dedupe_urls = request.session.get("search_dedupe_urls", [])
items = [i for i in items if i.source_url not in dedupe_urls]
return render(
request,
"external_search_results.html",
{
"external_items": items,
},
)
@login_required
def edit(request, item_uuid):
return HttpResponseBadRequest()
@login_required
def delete(request, item_uuid):
return HttpResponseBadRequest()

View file

@ -30,6 +30,18 @@
cursor: pointer;
}
.helptext{
position: relative;
top: -1em;
}
/*** django-jsoneditor ***/
div.jsoneditor {
border-color: #ccc !important;;
}
div.jsoneditor-menu {
background-color: #606c76 !important;;
border-color: #606c76 !important;
}
/***** MODAL DIALOG ****/
#modal {

View file

@ -0,0 +1,14 @@
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 duration_format(value):
duration = int(value)
h = duration // 3600000
m = duration % 3600000 // 60000
return (f"{h}小时 " if h else "") + (f"{m}分钟" if m else "")

View file

@ -1,3 +1,30 @@
from django.test import TestCase
# from django.test import TestCase
# Create your tests here.
# from django.contrib.staticfiles.testing import StaticLiveServerTestCase
# from selenium.webdriver.common.by import By
# from selenium import webdriver
# from selenium.webdriver.firefox.service import Service as FirefoxService
# from webdriver_manager.firefox import GeckoDriverManager
# class MySeleniumTests(StaticLiveServerTestCase):
# @classmethod
# def setUpClass(cls):
# super().setUpClass()
# cls.selenium = webdriver.Firefox(
# service=FirefoxService(GeckoDriverManager().install())
# )
# cls.selenium.implicitly_wait(10)
# @classmethod
# def tearDownClass(cls):
# cls.selenium.quit()
# super().tearDownClass()
# def test_login(self):
# self.selenium.get("%s%s" % (self.live_server_url, "/404/"))
# username_input = self.selenium.find_element(By.NAME, "username")
# username_input.send_keys("myuser")
# password_input = self.selenium.find_element(By.NAME, "password")
# password_input.send_keys("secret")
# self.selenium.find_element(By.XPATH, '//input[@value="Log in"]').click()

View file

@ -4,13 +4,12 @@ This is a very basic guide with limited detail, contributions welcomed
Install
-------
Install PostgreSQL, Redis and Python if not yet
Install PostgreSQL, Redis and Python (3.10 or above) if not yet
Setup database
```
CREATE DATABASE neodb ENCODING 'UTF8' LC_COLLATE='en_US.UTF-8' LC_CTYPE='en_US.UTF-8' TEMPLATE template0;
\c neodb;
CREATE EXTENSION hstore WITH SCHEMA public;
CREATE ROLE neodb with LOGIN ENCRYPTED PASSWORD 'abadface';
GRANT ALL ON DATABASE neodb TO neodb;
```
@ -102,13 +101,11 @@ rq requeue --all --queue doufen
Run in Docker
```
docker-compose build
docker-compose up db && docker exec -it app_db_1 psql -U postgres postgres -c 'CREATE EXTENSION hstore WITH SCHEMA public;' # first time only
docker-compose up
```
Run Tests
```
psql template1 -c 'create extension hstore;' # first time only
coverage run --source='.' manage.py test
coverage report
```

View file

@ -647,9 +647,7 @@ class Collection(List):
url_path = "collection"
MEMBER_CLASS = CollectionMember
catalog_item = models.OneToOneField(CatalogCollection, on_delete=models.PROTECT)
title = models.CharField(
_("title in primary language"), max_length=1000, default=""
)
title = models.CharField(_("标题"), max_length=1000, default="")
brief = models.TextField(_("简介"), blank=True, default="")
cover = models.ImageField(
upload_to=item_cover_path, default=DEFAULT_ITEM_COVER, blank=True

View file

@ -74,7 +74,7 @@
<div class="aside-section-wrapper">
<div class="entity-card">
<div class="entity-card__img-wrapper">
<a href="{% url 'collection:retrieve' collection.id %}">
<a href="{{ collection.url }}">
<img src="{{ collection.cover|thumb:'normal' }}" alt="" class="entity-card__img">
</a>
</div>
@ -115,7 +115,7 @@
<div class="action-panel">
<div class="action-panel__button-group action-panel__button-group--center">
<form>
<button class="action-panel__button add-to-list" hx-get="{% url 'collection:share' collection.id %}" hx-target="body" hx-swap="beforeend">{% trans '分享到联邦网络' %}</button>
<button class="action-panel__button add-to-list" hx-get="{% url 'journal:collection_share' collection.uuid %}" hx-target="body" hx-swap="beforeend">{% trans '分享到联邦网络' %}</button>
</form>
</div>
</div>

View file

@ -40,6 +40,11 @@ urlpatterns = [
"collection/edit/<str:collection_uuid>", collection_edit, name="collection_edit"
),
path("collection/delete/<str:piece_uuid>", piece_delete, name="collection_delete"),
path(
"collection/share/<str:collection_uuid>",
collection_share,
name="collection_share",
),
path(
"collection/<str:collection_uuid>/items",
collection_retrieve_items,

View file

@ -175,6 +175,10 @@ def collection_retrieve(request, collection_uuid):
)
def collection_share(request, collection_uuid):
pass
def collection_retrieve_items(request, collection_uuid, edit=False):
collection = get_object_or_404(Collection, uid=base62.decode(collection_uuid))
if not collection.is_visible_to(request.user):

View file

@ -3,6 +3,7 @@ dateparser
django~=3.2.16
django-hstore
django-markdownx @ git+https://github.com/alphatownsman/django-markdownx.git@e69480c64ad9c5d0499f4a8625da78cf2bb7691b
django-jsoneditor @ git+https://github.com/alphatownsman/django-jsoneditor.git@fa2ae41aeeb34447bd8a808a520e843c853fd16e
django-sass
django-rq
django-simple-history