Merge branch 'main' into form-perms
This commit is contained in:
commit
b0236b95bd
88 changed files with 4213 additions and 2669 deletions
|
@ -117,6 +117,17 @@ class ActivityStream(RedisStore):
|
|||
Q(id=status.user.id) # if the user is the post's author
|
||||
| Q(id__in=status.mention_users.all()) # if the user is mentioned
|
||||
)
|
||||
|
||||
# don't show replies to statuses the user can't see
|
||||
elif status.reply_parent and status.reply_parent.privacy == "followers":
|
||||
audience = audience.filter(
|
||||
Q(id=status.user.id) # if the user is the post's author
|
||||
| Q(id=status.reply_parent.user.id) # if the user is the OG author
|
||||
| (
|
||||
Q(following=status.user) & Q(following=status.reply_parent.user)
|
||||
) # if the user is following both authors
|
||||
).distinct()
|
||||
|
||||
# only visible to the poster's followers and tagged users
|
||||
elif status.privacy == "followers":
|
||||
audience = audience.filter(
|
||||
|
|
|
@ -7,6 +7,7 @@ from django.contrib.postgres.search import SearchRank, SearchQuery
|
|||
from django.db.models import OuterRef, Subquery, F, Q
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm import connectors
|
||||
from bookwyrm.settings import MEDIA_FULL_URL
|
||||
|
||||
|
||||
|
@ -30,7 +31,9 @@ def isbn_search(query):
|
|||
"""search your local database"""
|
||||
if not query:
|
||||
return []
|
||||
|
||||
# Up-case the ISBN string to ensure any 'X' check-digit is correct
|
||||
# If the ISBN has only 9 characters, prepend missing zero
|
||||
query = query.strip().upper().rjust(10, "0")
|
||||
filters = [{f: query} for f in ["isbn_10", "isbn_13"]]
|
||||
results = models.Edition.objects.filter(
|
||||
reduce(operator.or_, (Q(**f) for f in filters))
|
||||
|
@ -72,6 +75,10 @@ def format_search_result(search_result):
|
|||
|
||||
def search_identifiers(query, *filters, return_first=False):
|
||||
"""tries remote_id, isbn; defined as dedupe fields on the model"""
|
||||
if connectors.maybe_isbn(query):
|
||||
# Oh did you think the 'S' in ISBN stood for 'standard'?
|
||||
normalized_isbn = query.strip().upper().rjust(10, "0")
|
||||
query = normalized_isbn
|
||||
# pylint: disable=W0212
|
||||
or_filters = [
|
||||
{f.name: query}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
""" bring connectors into the namespace """
|
||||
from .settings import CONNECTORS
|
||||
from .abstract_connector import ConnectorException
|
||||
from .abstract_connector import get_data, get_image
|
||||
from .abstract_connector import get_data, get_image, maybe_isbn
|
||||
|
||||
from .connector_manager import search, first_search_result
|
||||
|
|
|
@ -42,8 +42,10 @@ class AbstractMinimalConnector(ABC):
|
|||
"""format the query url"""
|
||||
# Check if the query resembles an ISBN
|
||||
if maybe_isbn(query) and self.isbn_search_url and self.isbn_search_url != "":
|
||||
return f"{self.isbn_search_url}{query}"
|
||||
|
||||
# Up-case the ISBN string to ensure any 'X' check-digit is correct
|
||||
# If the ISBN has only 9 characters, prepend missing zero
|
||||
normalized_query = query.strip().upper().rjust(10, "0")
|
||||
return f"{self.isbn_search_url}{normalized_query}"
|
||||
# NOTE: previously, we tried searching isbn and if that produces no results,
|
||||
# searched as free text. This, instead, only searches isbn if it's isbn-y
|
||||
return f"{self.search_url}{query}"
|
||||
|
@ -325,4 +327,11 @@ def unique_physical_format(format_text):
|
|||
def maybe_isbn(query):
|
||||
"""check if a query looks like an isbn"""
|
||||
isbn = re.sub(r"[\W_]", "", query) # removes filler characters
|
||||
return len(isbn) in [10, 13] # ISBN10 or ISBN13
|
||||
# ISBNs must be numeric except an ISBN10 checkdigit can be 'X'
|
||||
if not isbn.upper().rstrip("X").isnumeric():
|
||||
return False
|
||||
return len(isbn) in [
|
||||
9,
|
||||
10,
|
||||
13,
|
||||
] # ISBN10 or ISBN13, or maybe ISBN10 missing a leading zero
|
||||
|
|
647
bookwyrm/migrations/0157_auto_20220909_2338.py
Normal file
647
bookwyrm/migrations/0157_auto_20220909_2338.py
Normal file
|
@ -0,0 +1,647 @@
|
|||
# Generated by Django 3.2.15 on 2022-09-09 23:38
|
||||
|
||||
import bookwyrm.models.fields
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0156_alter_user_preferred_language"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="review",
|
||||
name="rating",
|
||||
field=bookwyrm.models.fields.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=2,
|
||||
default=None,
|
||||
max_digits=3,
|
||||
null=True,
|
||||
validators=[
|
||||
django.core.validators.MinValueValidator(0.5),
|
||||
django.core.validators.MaxValueValidator(5),
|
||||
],
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="preferred_timezone",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("Africa/Abidjan", "Africa/Abidjan"),
|
||||
("Africa/Accra", "Africa/Accra"),
|
||||
("Africa/Addis_Ababa", "Africa/Addis_Ababa"),
|
||||
("Africa/Algiers", "Africa/Algiers"),
|
||||
("Africa/Asmara", "Africa/Asmara"),
|
||||
("Africa/Asmera", "Africa/Asmera"),
|
||||
("Africa/Bamako", "Africa/Bamako"),
|
||||
("Africa/Bangui", "Africa/Bangui"),
|
||||
("Africa/Banjul", "Africa/Banjul"),
|
||||
("Africa/Bissau", "Africa/Bissau"),
|
||||
("Africa/Blantyre", "Africa/Blantyre"),
|
||||
("Africa/Brazzaville", "Africa/Brazzaville"),
|
||||
("Africa/Bujumbura", "Africa/Bujumbura"),
|
||||
("Africa/Cairo", "Africa/Cairo"),
|
||||
("Africa/Casablanca", "Africa/Casablanca"),
|
||||
("Africa/Ceuta", "Africa/Ceuta"),
|
||||
("Africa/Conakry", "Africa/Conakry"),
|
||||
("Africa/Dakar", "Africa/Dakar"),
|
||||
("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"),
|
||||
("Africa/Djibouti", "Africa/Djibouti"),
|
||||
("Africa/Douala", "Africa/Douala"),
|
||||
("Africa/El_Aaiun", "Africa/El_Aaiun"),
|
||||
("Africa/Freetown", "Africa/Freetown"),
|
||||
("Africa/Gaborone", "Africa/Gaborone"),
|
||||
("Africa/Harare", "Africa/Harare"),
|
||||
("Africa/Johannesburg", "Africa/Johannesburg"),
|
||||
("Africa/Juba", "Africa/Juba"),
|
||||
("Africa/Kampala", "Africa/Kampala"),
|
||||
("Africa/Khartoum", "Africa/Khartoum"),
|
||||
("Africa/Kigali", "Africa/Kigali"),
|
||||
("Africa/Kinshasa", "Africa/Kinshasa"),
|
||||
("Africa/Lagos", "Africa/Lagos"),
|
||||
("Africa/Libreville", "Africa/Libreville"),
|
||||
("Africa/Lome", "Africa/Lome"),
|
||||
("Africa/Luanda", "Africa/Luanda"),
|
||||
("Africa/Lubumbashi", "Africa/Lubumbashi"),
|
||||
("Africa/Lusaka", "Africa/Lusaka"),
|
||||
("Africa/Malabo", "Africa/Malabo"),
|
||||
("Africa/Maputo", "Africa/Maputo"),
|
||||
("Africa/Maseru", "Africa/Maseru"),
|
||||
("Africa/Mbabane", "Africa/Mbabane"),
|
||||
("Africa/Mogadishu", "Africa/Mogadishu"),
|
||||
("Africa/Monrovia", "Africa/Monrovia"),
|
||||
("Africa/Nairobi", "Africa/Nairobi"),
|
||||
("Africa/Ndjamena", "Africa/Ndjamena"),
|
||||
("Africa/Niamey", "Africa/Niamey"),
|
||||
("Africa/Nouakchott", "Africa/Nouakchott"),
|
||||
("Africa/Ouagadougou", "Africa/Ouagadougou"),
|
||||
("Africa/Porto-Novo", "Africa/Porto-Novo"),
|
||||
("Africa/Sao_Tome", "Africa/Sao_Tome"),
|
||||
("Africa/Timbuktu", "Africa/Timbuktu"),
|
||||
("Africa/Tripoli", "Africa/Tripoli"),
|
||||
("Africa/Tunis", "Africa/Tunis"),
|
||||
("Africa/Windhoek", "Africa/Windhoek"),
|
||||
("America/Adak", "America/Adak"),
|
||||
("America/Anchorage", "America/Anchorage"),
|
||||
("America/Anguilla", "America/Anguilla"),
|
||||
("America/Antigua", "America/Antigua"),
|
||||
("America/Araguaina", "America/Araguaina"),
|
||||
(
|
||||
"America/Argentina/Buenos_Aires",
|
||||
"America/Argentina/Buenos_Aires",
|
||||
),
|
||||
("America/Argentina/Catamarca", "America/Argentina/Catamarca"),
|
||||
(
|
||||
"America/Argentina/ComodRivadavia",
|
||||
"America/Argentina/ComodRivadavia",
|
||||
),
|
||||
("America/Argentina/Cordoba", "America/Argentina/Cordoba"),
|
||||
("America/Argentina/Jujuy", "America/Argentina/Jujuy"),
|
||||
("America/Argentina/La_Rioja", "America/Argentina/La_Rioja"),
|
||||
("America/Argentina/Mendoza", "America/Argentina/Mendoza"),
|
||||
(
|
||||
"America/Argentina/Rio_Gallegos",
|
||||
"America/Argentina/Rio_Gallegos",
|
||||
),
|
||||
("America/Argentina/Salta", "America/Argentina/Salta"),
|
||||
("America/Argentina/San_Juan", "America/Argentina/San_Juan"),
|
||||
("America/Argentina/San_Luis", "America/Argentina/San_Luis"),
|
||||
("America/Argentina/Tucuman", "America/Argentina/Tucuman"),
|
||||
("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"),
|
||||
("America/Aruba", "America/Aruba"),
|
||||
("America/Asuncion", "America/Asuncion"),
|
||||
("America/Atikokan", "America/Atikokan"),
|
||||
("America/Atka", "America/Atka"),
|
||||
("America/Bahia", "America/Bahia"),
|
||||
("America/Bahia_Banderas", "America/Bahia_Banderas"),
|
||||
("America/Barbados", "America/Barbados"),
|
||||
("America/Belem", "America/Belem"),
|
||||
("America/Belize", "America/Belize"),
|
||||
("America/Blanc-Sablon", "America/Blanc-Sablon"),
|
||||
("America/Boa_Vista", "America/Boa_Vista"),
|
||||
("America/Bogota", "America/Bogota"),
|
||||
("America/Boise", "America/Boise"),
|
||||
("America/Buenos_Aires", "America/Buenos_Aires"),
|
||||
("America/Cambridge_Bay", "America/Cambridge_Bay"),
|
||||
("America/Campo_Grande", "America/Campo_Grande"),
|
||||
("America/Cancun", "America/Cancun"),
|
||||
("America/Caracas", "America/Caracas"),
|
||||
("America/Catamarca", "America/Catamarca"),
|
||||
("America/Cayenne", "America/Cayenne"),
|
||||
("America/Cayman", "America/Cayman"),
|
||||
("America/Chicago", "America/Chicago"),
|
||||
("America/Chihuahua", "America/Chihuahua"),
|
||||
("America/Coral_Harbour", "America/Coral_Harbour"),
|
||||
("America/Cordoba", "America/Cordoba"),
|
||||
("America/Costa_Rica", "America/Costa_Rica"),
|
||||
("America/Creston", "America/Creston"),
|
||||
("America/Cuiaba", "America/Cuiaba"),
|
||||
("America/Curacao", "America/Curacao"),
|
||||
("America/Danmarkshavn", "America/Danmarkshavn"),
|
||||
("America/Dawson", "America/Dawson"),
|
||||
("America/Dawson_Creek", "America/Dawson_Creek"),
|
||||
("America/Denver", "America/Denver"),
|
||||
("America/Detroit", "America/Detroit"),
|
||||
("America/Dominica", "America/Dominica"),
|
||||
("America/Edmonton", "America/Edmonton"),
|
||||
("America/Eirunepe", "America/Eirunepe"),
|
||||
("America/El_Salvador", "America/El_Salvador"),
|
||||
("America/Ensenada", "America/Ensenada"),
|
||||
("America/Fort_Nelson", "America/Fort_Nelson"),
|
||||
("America/Fort_Wayne", "America/Fort_Wayne"),
|
||||
("America/Fortaleza", "America/Fortaleza"),
|
||||
("America/Glace_Bay", "America/Glace_Bay"),
|
||||
("America/Godthab", "America/Godthab"),
|
||||
("America/Goose_Bay", "America/Goose_Bay"),
|
||||
("America/Grand_Turk", "America/Grand_Turk"),
|
||||
("America/Grenada", "America/Grenada"),
|
||||
("America/Guadeloupe", "America/Guadeloupe"),
|
||||
("America/Guatemala", "America/Guatemala"),
|
||||
("America/Guayaquil", "America/Guayaquil"),
|
||||
("America/Guyana", "America/Guyana"),
|
||||
("America/Halifax", "America/Halifax"),
|
||||
("America/Havana", "America/Havana"),
|
||||
("America/Hermosillo", "America/Hermosillo"),
|
||||
("America/Indiana/Indianapolis", "America/Indiana/Indianapolis"),
|
||||
("America/Indiana/Knox", "America/Indiana/Knox"),
|
||||
("America/Indiana/Marengo", "America/Indiana/Marengo"),
|
||||
("America/Indiana/Petersburg", "America/Indiana/Petersburg"),
|
||||
("America/Indiana/Tell_City", "America/Indiana/Tell_City"),
|
||||
("America/Indiana/Vevay", "America/Indiana/Vevay"),
|
||||
("America/Indiana/Vincennes", "America/Indiana/Vincennes"),
|
||||
("America/Indiana/Winamac", "America/Indiana/Winamac"),
|
||||
("America/Indianapolis", "America/Indianapolis"),
|
||||
("America/Inuvik", "America/Inuvik"),
|
||||
("America/Iqaluit", "America/Iqaluit"),
|
||||
("America/Jamaica", "America/Jamaica"),
|
||||
("America/Jujuy", "America/Jujuy"),
|
||||
("America/Juneau", "America/Juneau"),
|
||||
("America/Kentucky/Louisville", "America/Kentucky/Louisville"),
|
||||
("America/Kentucky/Monticello", "America/Kentucky/Monticello"),
|
||||
("America/Knox_IN", "America/Knox_IN"),
|
||||
("America/Kralendijk", "America/Kralendijk"),
|
||||
("America/La_Paz", "America/La_Paz"),
|
||||
("America/Lima", "America/Lima"),
|
||||
("America/Los_Angeles", "America/Los_Angeles"),
|
||||
("America/Louisville", "America/Louisville"),
|
||||
("America/Lower_Princes", "America/Lower_Princes"),
|
||||
("America/Maceio", "America/Maceio"),
|
||||
("America/Managua", "America/Managua"),
|
||||
("America/Manaus", "America/Manaus"),
|
||||
("America/Marigot", "America/Marigot"),
|
||||
("America/Martinique", "America/Martinique"),
|
||||
("America/Matamoros", "America/Matamoros"),
|
||||
("America/Mazatlan", "America/Mazatlan"),
|
||||
("America/Mendoza", "America/Mendoza"),
|
||||
("America/Menominee", "America/Menominee"),
|
||||
("America/Merida", "America/Merida"),
|
||||
("America/Metlakatla", "America/Metlakatla"),
|
||||
("America/Mexico_City", "America/Mexico_City"),
|
||||
("America/Miquelon", "America/Miquelon"),
|
||||
("America/Moncton", "America/Moncton"),
|
||||
("America/Monterrey", "America/Monterrey"),
|
||||
("America/Montevideo", "America/Montevideo"),
|
||||
("America/Montreal", "America/Montreal"),
|
||||
("America/Montserrat", "America/Montserrat"),
|
||||
("America/Nassau", "America/Nassau"),
|
||||
("America/New_York", "America/New_York"),
|
||||
("America/Nipigon", "America/Nipigon"),
|
||||
("America/Nome", "America/Nome"),
|
||||
("America/Noronha", "America/Noronha"),
|
||||
("America/North_Dakota/Beulah", "America/North_Dakota/Beulah"),
|
||||
("America/North_Dakota/Center", "America/North_Dakota/Center"),
|
||||
(
|
||||
"America/North_Dakota/New_Salem",
|
||||
"America/North_Dakota/New_Salem",
|
||||
),
|
||||
("America/Nuuk", "America/Nuuk"),
|
||||
("America/Ojinaga", "America/Ojinaga"),
|
||||
("America/Panama", "America/Panama"),
|
||||
("America/Pangnirtung", "America/Pangnirtung"),
|
||||
("America/Paramaribo", "America/Paramaribo"),
|
||||
("America/Phoenix", "America/Phoenix"),
|
||||
("America/Port-au-Prince", "America/Port-au-Prince"),
|
||||
("America/Port_of_Spain", "America/Port_of_Spain"),
|
||||
("America/Porto_Acre", "America/Porto_Acre"),
|
||||
("America/Porto_Velho", "America/Porto_Velho"),
|
||||
("America/Puerto_Rico", "America/Puerto_Rico"),
|
||||
("America/Punta_Arenas", "America/Punta_Arenas"),
|
||||
("America/Rainy_River", "America/Rainy_River"),
|
||||
("America/Rankin_Inlet", "America/Rankin_Inlet"),
|
||||
("America/Recife", "America/Recife"),
|
||||
("America/Regina", "America/Regina"),
|
||||
("America/Resolute", "America/Resolute"),
|
||||
("America/Rio_Branco", "America/Rio_Branco"),
|
||||
("America/Rosario", "America/Rosario"),
|
||||
("America/Santa_Isabel", "America/Santa_Isabel"),
|
||||
("America/Santarem", "America/Santarem"),
|
||||
("America/Santiago", "America/Santiago"),
|
||||
("America/Santo_Domingo", "America/Santo_Domingo"),
|
||||
("America/Sao_Paulo", "America/Sao_Paulo"),
|
||||
("America/Scoresbysund", "America/Scoresbysund"),
|
||||
("America/Shiprock", "America/Shiprock"),
|
||||
("America/Sitka", "America/Sitka"),
|
||||
("America/St_Barthelemy", "America/St_Barthelemy"),
|
||||
("America/St_Johns", "America/St_Johns"),
|
||||
("America/St_Kitts", "America/St_Kitts"),
|
||||
("America/St_Lucia", "America/St_Lucia"),
|
||||
("America/St_Thomas", "America/St_Thomas"),
|
||||
("America/St_Vincent", "America/St_Vincent"),
|
||||
("America/Swift_Current", "America/Swift_Current"),
|
||||
("America/Tegucigalpa", "America/Tegucigalpa"),
|
||||
("America/Thule", "America/Thule"),
|
||||
("America/Thunder_Bay", "America/Thunder_Bay"),
|
||||
("America/Tijuana", "America/Tijuana"),
|
||||
("America/Toronto", "America/Toronto"),
|
||||
("America/Tortola", "America/Tortola"),
|
||||
("America/Vancouver", "America/Vancouver"),
|
||||
("America/Virgin", "America/Virgin"),
|
||||
("America/Whitehorse", "America/Whitehorse"),
|
||||
("America/Winnipeg", "America/Winnipeg"),
|
||||
("America/Yakutat", "America/Yakutat"),
|
||||
("America/Yellowknife", "America/Yellowknife"),
|
||||
("Antarctica/Casey", "Antarctica/Casey"),
|
||||
("Antarctica/Davis", "Antarctica/Davis"),
|
||||
("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"),
|
||||
("Antarctica/Macquarie", "Antarctica/Macquarie"),
|
||||
("Antarctica/Mawson", "Antarctica/Mawson"),
|
||||
("Antarctica/McMurdo", "Antarctica/McMurdo"),
|
||||
("Antarctica/Palmer", "Antarctica/Palmer"),
|
||||
("Antarctica/Rothera", "Antarctica/Rothera"),
|
||||
("Antarctica/South_Pole", "Antarctica/South_Pole"),
|
||||
("Antarctica/Syowa", "Antarctica/Syowa"),
|
||||
("Antarctica/Troll", "Antarctica/Troll"),
|
||||
("Antarctica/Vostok", "Antarctica/Vostok"),
|
||||
("Arctic/Longyearbyen", "Arctic/Longyearbyen"),
|
||||
("Asia/Aden", "Asia/Aden"),
|
||||
("Asia/Almaty", "Asia/Almaty"),
|
||||
("Asia/Amman", "Asia/Amman"),
|
||||
("Asia/Anadyr", "Asia/Anadyr"),
|
||||
("Asia/Aqtau", "Asia/Aqtau"),
|
||||
("Asia/Aqtobe", "Asia/Aqtobe"),
|
||||
("Asia/Ashgabat", "Asia/Ashgabat"),
|
||||
("Asia/Ashkhabad", "Asia/Ashkhabad"),
|
||||
("Asia/Atyrau", "Asia/Atyrau"),
|
||||
("Asia/Baghdad", "Asia/Baghdad"),
|
||||
("Asia/Bahrain", "Asia/Bahrain"),
|
||||
("Asia/Baku", "Asia/Baku"),
|
||||
("Asia/Bangkok", "Asia/Bangkok"),
|
||||
("Asia/Barnaul", "Asia/Barnaul"),
|
||||
("Asia/Beirut", "Asia/Beirut"),
|
||||
("Asia/Bishkek", "Asia/Bishkek"),
|
||||
("Asia/Brunei", "Asia/Brunei"),
|
||||
("Asia/Calcutta", "Asia/Calcutta"),
|
||||
("Asia/Chita", "Asia/Chita"),
|
||||
("Asia/Choibalsan", "Asia/Choibalsan"),
|
||||
("Asia/Chongqing", "Asia/Chongqing"),
|
||||
("Asia/Chungking", "Asia/Chungking"),
|
||||
("Asia/Colombo", "Asia/Colombo"),
|
||||
("Asia/Dacca", "Asia/Dacca"),
|
||||
("Asia/Damascus", "Asia/Damascus"),
|
||||
("Asia/Dhaka", "Asia/Dhaka"),
|
||||
("Asia/Dili", "Asia/Dili"),
|
||||
("Asia/Dubai", "Asia/Dubai"),
|
||||
("Asia/Dushanbe", "Asia/Dushanbe"),
|
||||
("Asia/Famagusta", "Asia/Famagusta"),
|
||||
("Asia/Gaza", "Asia/Gaza"),
|
||||
("Asia/Harbin", "Asia/Harbin"),
|
||||
("Asia/Hebron", "Asia/Hebron"),
|
||||
("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"),
|
||||
("Asia/Hong_Kong", "Asia/Hong_Kong"),
|
||||
("Asia/Hovd", "Asia/Hovd"),
|
||||
("Asia/Irkutsk", "Asia/Irkutsk"),
|
||||
("Asia/Istanbul", "Asia/Istanbul"),
|
||||
("Asia/Jakarta", "Asia/Jakarta"),
|
||||
("Asia/Jayapura", "Asia/Jayapura"),
|
||||
("Asia/Jerusalem", "Asia/Jerusalem"),
|
||||
("Asia/Kabul", "Asia/Kabul"),
|
||||
("Asia/Kamchatka", "Asia/Kamchatka"),
|
||||
("Asia/Karachi", "Asia/Karachi"),
|
||||
("Asia/Kashgar", "Asia/Kashgar"),
|
||||
("Asia/Kathmandu", "Asia/Kathmandu"),
|
||||
("Asia/Katmandu", "Asia/Katmandu"),
|
||||
("Asia/Khandyga", "Asia/Khandyga"),
|
||||
("Asia/Kolkata", "Asia/Kolkata"),
|
||||
("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"),
|
||||
("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"),
|
||||
("Asia/Kuching", "Asia/Kuching"),
|
||||
("Asia/Kuwait", "Asia/Kuwait"),
|
||||
("Asia/Macao", "Asia/Macao"),
|
||||
("Asia/Macau", "Asia/Macau"),
|
||||
("Asia/Magadan", "Asia/Magadan"),
|
||||
("Asia/Makassar", "Asia/Makassar"),
|
||||
("Asia/Manila", "Asia/Manila"),
|
||||
("Asia/Muscat", "Asia/Muscat"),
|
||||
("Asia/Nicosia", "Asia/Nicosia"),
|
||||
("Asia/Novokuznetsk", "Asia/Novokuznetsk"),
|
||||
("Asia/Novosibirsk", "Asia/Novosibirsk"),
|
||||
("Asia/Omsk", "Asia/Omsk"),
|
||||
("Asia/Oral", "Asia/Oral"),
|
||||
("Asia/Phnom_Penh", "Asia/Phnom_Penh"),
|
||||
("Asia/Pontianak", "Asia/Pontianak"),
|
||||
("Asia/Pyongyang", "Asia/Pyongyang"),
|
||||
("Asia/Qatar", "Asia/Qatar"),
|
||||
("Asia/Qostanay", "Asia/Qostanay"),
|
||||
("Asia/Qyzylorda", "Asia/Qyzylorda"),
|
||||
("Asia/Rangoon", "Asia/Rangoon"),
|
||||
("Asia/Riyadh", "Asia/Riyadh"),
|
||||
("Asia/Saigon", "Asia/Saigon"),
|
||||
("Asia/Sakhalin", "Asia/Sakhalin"),
|
||||
("Asia/Samarkand", "Asia/Samarkand"),
|
||||
("Asia/Seoul", "Asia/Seoul"),
|
||||
("Asia/Shanghai", "Asia/Shanghai"),
|
||||
("Asia/Singapore", "Asia/Singapore"),
|
||||
("Asia/Srednekolymsk", "Asia/Srednekolymsk"),
|
||||
("Asia/Taipei", "Asia/Taipei"),
|
||||
("Asia/Tashkent", "Asia/Tashkent"),
|
||||
("Asia/Tbilisi", "Asia/Tbilisi"),
|
||||
("Asia/Tehran", "Asia/Tehran"),
|
||||
("Asia/Tel_Aviv", "Asia/Tel_Aviv"),
|
||||
("Asia/Thimbu", "Asia/Thimbu"),
|
||||
("Asia/Thimphu", "Asia/Thimphu"),
|
||||
("Asia/Tokyo", "Asia/Tokyo"),
|
||||
("Asia/Tomsk", "Asia/Tomsk"),
|
||||
("Asia/Ujung_Pandang", "Asia/Ujung_Pandang"),
|
||||
("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"),
|
||||
("Asia/Ulan_Bator", "Asia/Ulan_Bator"),
|
||||
("Asia/Urumqi", "Asia/Urumqi"),
|
||||
("Asia/Ust-Nera", "Asia/Ust-Nera"),
|
||||
("Asia/Vientiane", "Asia/Vientiane"),
|
||||
("Asia/Vladivostok", "Asia/Vladivostok"),
|
||||
("Asia/Yakutsk", "Asia/Yakutsk"),
|
||||
("Asia/Yangon", "Asia/Yangon"),
|
||||
("Asia/Yekaterinburg", "Asia/Yekaterinburg"),
|
||||
("Asia/Yerevan", "Asia/Yerevan"),
|
||||
("Atlantic/Azores", "Atlantic/Azores"),
|
||||
("Atlantic/Bermuda", "Atlantic/Bermuda"),
|
||||
("Atlantic/Canary", "Atlantic/Canary"),
|
||||
("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"),
|
||||
("Atlantic/Faeroe", "Atlantic/Faeroe"),
|
||||
("Atlantic/Faroe", "Atlantic/Faroe"),
|
||||
("Atlantic/Jan_Mayen", "Atlantic/Jan_Mayen"),
|
||||
("Atlantic/Madeira", "Atlantic/Madeira"),
|
||||
("Atlantic/Reykjavik", "Atlantic/Reykjavik"),
|
||||
("Atlantic/South_Georgia", "Atlantic/South_Georgia"),
|
||||
("Atlantic/St_Helena", "Atlantic/St_Helena"),
|
||||
("Atlantic/Stanley", "Atlantic/Stanley"),
|
||||
("Australia/ACT", "Australia/ACT"),
|
||||
("Australia/Adelaide", "Australia/Adelaide"),
|
||||
("Australia/Brisbane", "Australia/Brisbane"),
|
||||
("Australia/Broken_Hill", "Australia/Broken_Hill"),
|
||||
("Australia/Canberra", "Australia/Canberra"),
|
||||
("Australia/Currie", "Australia/Currie"),
|
||||
("Australia/Darwin", "Australia/Darwin"),
|
||||
("Australia/Eucla", "Australia/Eucla"),
|
||||
("Australia/Hobart", "Australia/Hobart"),
|
||||
("Australia/LHI", "Australia/LHI"),
|
||||
("Australia/Lindeman", "Australia/Lindeman"),
|
||||
("Australia/Lord_Howe", "Australia/Lord_Howe"),
|
||||
("Australia/Melbourne", "Australia/Melbourne"),
|
||||
("Australia/NSW", "Australia/NSW"),
|
||||
("Australia/North", "Australia/North"),
|
||||
("Australia/Perth", "Australia/Perth"),
|
||||
("Australia/Queensland", "Australia/Queensland"),
|
||||
("Australia/South", "Australia/South"),
|
||||
("Australia/Sydney", "Australia/Sydney"),
|
||||
("Australia/Tasmania", "Australia/Tasmania"),
|
||||
("Australia/Victoria", "Australia/Victoria"),
|
||||
("Australia/West", "Australia/West"),
|
||||
("Australia/Yancowinna", "Australia/Yancowinna"),
|
||||
("Brazil/Acre", "Brazil/Acre"),
|
||||
("Brazil/DeNoronha", "Brazil/DeNoronha"),
|
||||
("Brazil/East", "Brazil/East"),
|
||||
("Brazil/West", "Brazil/West"),
|
||||
("CET", "CET"),
|
||||
("CST6CDT", "CST6CDT"),
|
||||
("Canada/Atlantic", "Canada/Atlantic"),
|
||||
("Canada/Central", "Canada/Central"),
|
||||
("Canada/Eastern", "Canada/Eastern"),
|
||||
("Canada/Mountain", "Canada/Mountain"),
|
||||
("Canada/Newfoundland", "Canada/Newfoundland"),
|
||||
("Canada/Pacific", "Canada/Pacific"),
|
||||
("Canada/Saskatchewan", "Canada/Saskatchewan"),
|
||||
("Canada/Yukon", "Canada/Yukon"),
|
||||
("Chile/Continental", "Chile/Continental"),
|
||||
("Chile/EasterIsland", "Chile/EasterIsland"),
|
||||
("Cuba", "Cuba"),
|
||||
("EET", "EET"),
|
||||
("EST", "EST"),
|
||||
("EST5EDT", "EST5EDT"),
|
||||
("Egypt", "Egypt"),
|
||||
("Eire", "Eire"),
|
||||
("Etc/GMT", "Etc/GMT"),
|
||||
("Etc/GMT+0", "Etc/GMT+0"),
|
||||
("Etc/GMT+1", "Etc/GMT+1"),
|
||||
("Etc/GMT+10", "Etc/GMT+10"),
|
||||
("Etc/GMT+11", "Etc/GMT+11"),
|
||||
("Etc/GMT+12", "Etc/GMT+12"),
|
||||
("Etc/GMT+2", "Etc/GMT+2"),
|
||||
("Etc/GMT+3", "Etc/GMT+3"),
|
||||
("Etc/GMT+4", "Etc/GMT+4"),
|
||||
("Etc/GMT+5", "Etc/GMT+5"),
|
||||
("Etc/GMT+6", "Etc/GMT+6"),
|
||||
("Etc/GMT+7", "Etc/GMT+7"),
|
||||
("Etc/GMT+8", "Etc/GMT+8"),
|
||||
("Etc/GMT+9", "Etc/GMT+9"),
|
||||
("Etc/GMT-0", "Etc/GMT-0"),
|
||||
("Etc/GMT-1", "Etc/GMT-1"),
|
||||
("Etc/GMT-10", "Etc/GMT-10"),
|
||||
("Etc/GMT-11", "Etc/GMT-11"),
|
||||
("Etc/GMT-12", "Etc/GMT-12"),
|
||||
("Etc/GMT-13", "Etc/GMT-13"),
|
||||
("Etc/GMT-14", "Etc/GMT-14"),
|
||||
("Etc/GMT-2", "Etc/GMT-2"),
|
||||
("Etc/GMT-3", "Etc/GMT-3"),
|
||||
("Etc/GMT-4", "Etc/GMT-4"),
|
||||
("Etc/GMT-5", "Etc/GMT-5"),
|
||||
("Etc/GMT-6", "Etc/GMT-6"),
|
||||
("Etc/GMT-7", "Etc/GMT-7"),
|
||||
("Etc/GMT-8", "Etc/GMT-8"),
|
||||
("Etc/GMT-9", "Etc/GMT-9"),
|
||||
("Etc/GMT0", "Etc/GMT0"),
|
||||
("Etc/Greenwich", "Etc/Greenwich"),
|
||||
("Etc/UCT", "Etc/UCT"),
|
||||
("Etc/UTC", "Etc/UTC"),
|
||||
("Etc/Universal", "Etc/Universal"),
|
||||
("Etc/Zulu", "Etc/Zulu"),
|
||||
("Europe/Amsterdam", "Europe/Amsterdam"),
|
||||
("Europe/Andorra", "Europe/Andorra"),
|
||||
("Europe/Astrakhan", "Europe/Astrakhan"),
|
||||
("Europe/Athens", "Europe/Athens"),
|
||||
("Europe/Belfast", "Europe/Belfast"),
|
||||
("Europe/Belgrade", "Europe/Belgrade"),
|
||||
("Europe/Berlin", "Europe/Berlin"),
|
||||
("Europe/Bratislava", "Europe/Bratislava"),
|
||||
("Europe/Brussels", "Europe/Brussels"),
|
||||
("Europe/Bucharest", "Europe/Bucharest"),
|
||||
("Europe/Budapest", "Europe/Budapest"),
|
||||
("Europe/Busingen", "Europe/Busingen"),
|
||||
("Europe/Chisinau", "Europe/Chisinau"),
|
||||
("Europe/Copenhagen", "Europe/Copenhagen"),
|
||||
("Europe/Dublin", "Europe/Dublin"),
|
||||
("Europe/Gibraltar", "Europe/Gibraltar"),
|
||||
("Europe/Guernsey", "Europe/Guernsey"),
|
||||
("Europe/Helsinki", "Europe/Helsinki"),
|
||||
("Europe/Isle_of_Man", "Europe/Isle_of_Man"),
|
||||
("Europe/Istanbul", "Europe/Istanbul"),
|
||||
("Europe/Jersey", "Europe/Jersey"),
|
||||
("Europe/Kaliningrad", "Europe/Kaliningrad"),
|
||||
("Europe/Kiev", "Europe/Kiev"),
|
||||
("Europe/Kirov", "Europe/Kirov"),
|
||||
("Europe/Kyiv", "Europe/Kyiv"),
|
||||
("Europe/Lisbon", "Europe/Lisbon"),
|
||||
("Europe/Ljubljana", "Europe/Ljubljana"),
|
||||
("Europe/London", "Europe/London"),
|
||||
("Europe/Luxembourg", "Europe/Luxembourg"),
|
||||
("Europe/Madrid", "Europe/Madrid"),
|
||||
("Europe/Malta", "Europe/Malta"),
|
||||
("Europe/Mariehamn", "Europe/Mariehamn"),
|
||||
("Europe/Minsk", "Europe/Minsk"),
|
||||
("Europe/Monaco", "Europe/Monaco"),
|
||||
("Europe/Moscow", "Europe/Moscow"),
|
||||
("Europe/Nicosia", "Europe/Nicosia"),
|
||||
("Europe/Oslo", "Europe/Oslo"),
|
||||
("Europe/Paris", "Europe/Paris"),
|
||||
("Europe/Podgorica", "Europe/Podgorica"),
|
||||
("Europe/Prague", "Europe/Prague"),
|
||||
("Europe/Riga", "Europe/Riga"),
|
||||
("Europe/Rome", "Europe/Rome"),
|
||||
("Europe/Samara", "Europe/Samara"),
|
||||
("Europe/San_Marino", "Europe/San_Marino"),
|
||||
("Europe/Sarajevo", "Europe/Sarajevo"),
|
||||
("Europe/Saratov", "Europe/Saratov"),
|
||||
("Europe/Simferopol", "Europe/Simferopol"),
|
||||
("Europe/Skopje", "Europe/Skopje"),
|
||||
("Europe/Sofia", "Europe/Sofia"),
|
||||
("Europe/Stockholm", "Europe/Stockholm"),
|
||||
("Europe/Tallinn", "Europe/Tallinn"),
|
||||
("Europe/Tirane", "Europe/Tirane"),
|
||||
("Europe/Tiraspol", "Europe/Tiraspol"),
|
||||
("Europe/Ulyanovsk", "Europe/Ulyanovsk"),
|
||||
("Europe/Uzhgorod", "Europe/Uzhgorod"),
|
||||
("Europe/Vaduz", "Europe/Vaduz"),
|
||||
("Europe/Vatican", "Europe/Vatican"),
|
||||
("Europe/Vienna", "Europe/Vienna"),
|
||||
("Europe/Vilnius", "Europe/Vilnius"),
|
||||
("Europe/Volgograd", "Europe/Volgograd"),
|
||||
("Europe/Warsaw", "Europe/Warsaw"),
|
||||
("Europe/Zagreb", "Europe/Zagreb"),
|
||||
("Europe/Zaporozhye", "Europe/Zaporozhye"),
|
||||
("Europe/Zurich", "Europe/Zurich"),
|
||||
("GB", "GB"),
|
||||
("GB-Eire", "GB-Eire"),
|
||||
("GMT", "GMT"),
|
||||
("GMT+0", "GMT+0"),
|
||||
("GMT-0", "GMT-0"),
|
||||
("GMT0", "GMT0"),
|
||||
("Greenwich", "Greenwich"),
|
||||
("HST", "HST"),
|
||||
("Hongkong", "Hongkong"),
|
||||
("Iceland", "Iceland"),
|
||||
("Indian/Antananarivo", "Indian/Antananarivo"),
|
||||
("Indian/Chagos", "Indian/Chagos"),
|
||||
("Indian/Christmas", "Indian/Christmas"),
|
||||
("Indian/Cocos", "Indian/Cocos"),
|
||||
("Indian/Comoro", "Indian/Comoro"),
|
||||
("Indian/Kerguelen", "Indian/Kerguelen"),
|
||||
("Indian/Mahe", "Indian/Mahe"),
|
||||
("Indian/Maldives", "Indian/Maldives"),
|
||||
("Indian/Mauritius", "Indian/Mauritius"),
|
||||
("Indian/Mayotte", "Indian/Mayotte"),
|
||||
("Indian/Reunion", "Indian/Reunion"),
|
||||
("Iran", "Iran"),
|
||||
("Israel", "Israel"),
|
||||
("Jamaica", "Jamaica"),
|
||||
("Japan", "Japan"),
|
||||
("Kwajalein", "Kwajalein"),
|
||||
("Libya", "Libya"),
|
||||
("MET", "MET"),
|
||||
("MST", "MST"),
|
||||
("MST7MDT", "MST7MDT"),
|
||||
("Mexico/BajaNorte", "Mexico/BajaNorte"),
|
||||
("Mexico/BajaSur", "Mexico/BajaSur"),
|
||||
("Mexico/General", "Mexico/General"),
|
||||
("NZ", "NZ"),
|
||||
("NZ-CHAT", "NZ-CHAT"),
|
||||
("Navajo", "Navajo"),
|
||||
("PRC", "PRC"),
|
||||
("PST8PDT", "PST8PDT"),
|
||||
("Pacific/Apia", "Pacific/Apia"),
|
||||
("Pacific/Auckland", "Pacific/Auckland"),
|
||||
("Pacific/Bougainville", "Pacific/Bougainville"),
|
||||
("Pacific/Chatham", "Pacific/Chatham"),
|
||||
("Pacific/Chuuk", "Pacific/Chuuk"),
|
||||
("Pacific/Easter", "Pacific/Easter"),
|
||||
("Pacific/Efate", "Pacific/Efate"),
|
||||
("Pacific/Enderbury", "Pacific/Enderbury"),
|
||||
("Pacific/Fakaofo", "Pacific/Fakaofo"),
|
||||
("Pacific/Fiji", "Pacific/Fiji"),
|
||||
("Pacific/Funafuti", "Pacific/Funafuti"),
|
||||
("Pacific/Galapagos", "Pacific/Galapagos"),
|
||||
("Pacific/Gambier", "Pacific/Gambier"),
|
||||
("Pacific/Guadalcanal", "Pacific/Guadalcanal"),
|
||||
("Pacific/Guam", "Pacific/Guam"),
|
||||
("Pacific/Honolulu", "Pacific/Honolulu"),
|
||||
("Pacific/Johnston", "Pacific/Johnston"),
|
||||
("Pacific/Kanton", "Pacific/Kanton"),
|
||||
("Pacific/Kiritimati", "Pacific/Kiritimati"),
|
||||
("Pacific/Kosrae", "Pacific/Kosrae"),
|
||||
("Pacific/Kwajalein", "Pacific/Kwajalein"),
|
||||
("Pacific/Majuro", "Pacific/Majuro"),
|
||||
("Pacific/Marquesas", "Pacific/Marquesas"),
|
||||
("Pacific/Midway", "Pacific/Midway"),
|
||||
("Pacific/Nauru", "Pacific/Nauru"),
|
||||
("Pacific/Niue", "Pacific/Niue"),
|
||||
("Pacific/Norfolk", "Pacific/Norfolk"),
|
||||
("Pacific/Noumea", "Pacific/Noumea"),
|
||||
("Pacific/Pago_Pago", "Pacific/Pago_Pago"),
|
||||
("Pacific/Palau", "Pacific/Palau"),
|
||||
("Pacific/Pitcairn", "Pacific/Pitcairn"),
|
||||
("Pacific/Pohnpei", "Pacific/Pohnpei"),
|
||||
("Pacific/Ponape", "Pacific/Ponape"),
|
||||
("Pacific/Port_Moresby", "Pacific/Port_Moresby"),
|
||||
("Pacific/Rarotonga", "Pacific/Rarotonga"),
|
||||
("Pacific/Saipan", "Pacific/Saipan"),
|
||||
("Pacific/Samoa", "Pacific/Samoa"),
|
||||
("Pacific/Tahiti", "Pacific/Tahiti"),
|
||||
("Pacific/Tarawa", "Pacific/Tarawa"),
|
||||
("Pacific/Tongatapu", "Pacific/Tongatapu"),
|
||||
("Pacific/Truk", "Pacific/Truk"),
|
||||
("Pacific/Wake", "Pacific/Wake"),
|
||||
("Pacific/Wallis", "Pacific/Wallis"),
|
||||
("Pacific/Yap", "Pacific/Yap"),
|
||||
("Poland", "Poland"),
|
||||
("Portugal", "Portugal"),
|
||||
("ROC", "ROC"),
|
||||
("ROK", "ROK"),
|
||||
("Singapore", "Singapore"),
|
||||
("Turkey", "Turkey"),
|
||||
("UCT", "UCT"),
|
||||
("US/Alaska", "US/Alaska"),
|
||||
("US/Aleutian", "US/Aleutian"),
|
||||
("US/Arizona", "US/Arizona"),
|
||||
("US/Central", "US/Central"),
|
||||
("US/East-Indiana", "US/East-Indiana"),
|
||||
("US/Eastern", "US/Eastern"),
|
||||
("US/Hawaii", "US/Hawaii"),
|
||||
("US/Indiana-Starke", "US/Indiana-Starke"),
|
||||
("US/Michigan", "US/Michigan"),
|
||||
("US/Mountain", "US/Mountain"),
|
||||
("US/Pacific", "US/Pacific"),
|
||||
("US/Samoa", "US/Samoa"),
|
||||
("UTC", "UTC"),
|
||||
("Universal", "Universal"),
|
||||
("W-SU", "W-SU"),
|
||||
("WET", "WET"),
|
||||
("Zulu", "Zulu"),
|
||||
],
|
||||
default="UTC",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -363,7 +363,7 @@ class Review(BookStatus):
|
|||
default=None,
|
||||
null=True,
|
||||
blank=True,
|
||||
validators=[MinValueValidator(1), MaxValueValidator(5)],
|
||||
validators=[MinValueValidator(0.5), MaxValueValidator(5)],
|
||||
decimal_places=2,
|
||||
max_digits=3,
|
||||
)
|
||||
|
|
|
@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _
|
|||
env = Env()
|
||||
env.read_env()
|
||||
DOMAIN = env("DOMAIN")
|
||||
VERSION = "0.4.4"
|
||||
VERSION = "0.4.5"
|
||||
|
||||
RELEASE_API = env(
|
||||
"RELEASE_API",
|
||||
|
|
|
@ -23,7 +23,9 @@
|
|||
<p class="subtitle notification has-background-primary-highlight">
|
||||
{% blocktrans trimmed with site_name=site.name %}
|
||||
{{ site_name }} is part of <em>BookWyrm</em>, a network of independent, self-directed communities for readers.
|
||||
While you can interact seamlessly with users anywhere in the <a href="https://joinbookwyrm.com/instances/" target="_blank">BookWyrm network</a>, this community is unique.
|
||||
While you can interact seamlessly with users anywhere in the
|
||||
<a href="https://joinbookwyrm.com/instances/" target="_blank" rel="nofollow noopener noreferrer">BookWyrm network</a>,
|
||||
this community is unique.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
</div>
|
||||
|
@ -88,7 +90,10 @@
|
|||
</div>
|
||||
|
||||
<p>
|
||||
{% trans "Track your reading, talk about books, write reviews, and discover what to read next. Always ad-free, anti-corporate, and community-oriented, BookWyrm is human-scale software, designed to stay small and personal. If you have feature requests, bug reports, or grand dreams, <a href='https://joinbookwyrm.com/get-involved' target='_blank'>reach out</a> and make yourself heard." %}
|
||||
{% blocktrans trimmed %}
|
||||
Track your reading, talk about books, write reviews, and discover what to read next. Always ad-free, anti-corporate, and community-oriented, BookWyrm is human-scale software, designed to stay small and personal.
|
||||
If you have feature requests, bug reports, or grand dreams, <a href="https://joinbookwyrm.com/get-involved" target="_blank" rel="nofollow noopener noreferrer">reach out</a> and make yourself heard.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
</section>
|
||||
|
|
|
@ -66,7 +66,7 @@
|
|||
<div class="box">
|
||||
{% if author.wikipedia_link %}
|
||||
<div>
|
||||
<a itemprop="sameAs" href="{{ author.wikipedia_link }}" rel="noopener noreferrer" target="_blank">
|
||||
<a itemprop="sameAs" href="{{ author.wikipedia_link }}" rel="nofollow noopener noreferrer" target="_blank">
|
||||
{% trans "Wikipedia" %}
|
||||
</a>
|
||||
</div>
|
||||
|
@ -74,7 +74,7 @@
|
|||
|
||||
{% if author.isni %}
|
||||
<div class="mt-1">
|
||||
<a itemprop="sameAs" href="{{ author.isni_link }}" rel="noopener noreferrer" target="_blank">
|
||||
<a itemprop="sameAs" href="{{ author.isni_link }}" rel="nofollow noopener noreferrer" target="_blank">
|
||||
{% trans "View ISNI record" %}
|
||||
</a>
|
||||
</div>
|
||||
|
@ -83,7 +83,7 @@
|
|||
{% trans "Load data" as button_text %}
|
||||
{% if author.openlibrary_key %}
|
||||
<div class="mt-1 is-flex">
|
||||
<a class="mr-3" itemprop="sameAs" href="{{ author.openlibrary_link }}" target="_blank" rel="noopener noreferrer">
|
||||
<a class="mr-3" itemprop="sameAs" href="{{ author.openlibrary_link }}" target="_blank" rel="nofollow noopener noreferrer">
|
||||
{% trans "View on OpenLibrary" %}
|
||||
</a>
|
||||
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
|
||||
|
@ -98,7 +98,7 @@
|
|||
|
||||
{% if author.inventaire_id %}
|
||||
<div class="mt-1 is-flex">
|
||||
<a class="mr-3" itemprop="sameAs" href="{{ author.inventaire_link }}" target="_blank" rel="noopener noreferrer">
|
||||
<a class="mr-3" itemprop="sameAs" href="{{ author.inventaire_link }}" target="_blank" rel="nofollow noopener noreferrer">
|
||||
{% trans "View on Inventaire" %}
|
||||
</a>
|
||||
|
||||
|
@ -114,7 +114,7 @@
|
|||
|
||||
{% if author.librarything_key %}
|
||||
<div class="mt-1">
|
||||
<a itemprop="sameAs" href="https://www.librarything.com/author/{{ author.librarything_key }}" target="_blank" rel="noopener noreferrer">
|
||||
<a itemprop="sameAs" href="https://www.librarything.com/author/{{ author.librarything_key }}" target="_blank" rel="nofollow noopener noreferrer">
|
||||
{% trans "View on LibraryThing" %}
|
||||
</a>
|
||||
</div>
|
||||
|
@ -122,7 +122,7 @@
|
|||
|
||||
{% if author.goodreads_key %}
|
||||
<div>
|
||||
<a itemprop="sameAs" href="https://www.goodreads.com/author/show/{{ author.goodreads_key }}" target="_blank" rel="noopener noreferrer">
|
||||
<a itemprop="sameAs" href="https://www.goodreads.com/author/show/{{ author.goodreads_key }}" target="_blank" rel="nofollow noopener noreferrer">
|
||||
{% trans "View on Goodreads" %}
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -131,7 +131,7 @@
|
|||
{% trans "Load data" as button_text %}
|
||||
{% if book.openlibrary_key %}
|
||||
<p>
|
||||
<a href="{{ book.openlibrary_link }}" target="_blank" rel="noopener noreferrer">
|
||||
<a href="{{ book.openlibrary_link }}" target="_blank" rel="nofollow noopener noreferrer">
|
||||
{% trans "View on OpenLibrary" %}
|
||||
</a>
|
||||
{% if request.user.is_authenticated and perms.bookwyrm.edit_book %}
|
||||
|
@ -145,7 +145,7 @@
|
|||
{% endif %}
|
||||
{% if book.inventaire_id %}
|
||||
<p>
|
||||
<a href="{{ book.inventaire_link }}" target="_blank" rel="noopener noreferrer">
|
||||
<a href="{{ book.inventaire_link }}" target="_blank" rel="nofollow noopener noreferrer">
|
||||
{% trans "View on Inventaire" %}
|
||||
</a>
|
||||
|
||||
|
|
|
@ -78,9 +78,13 @@
|
|||
<p class="help ml-5 mb-2">
|
||||
{% with book_title=match.book_set.first.title alt_title=match.bio %}
|
||||
{% if book_title %}
|
||||
<a href="{{ match.local_path }}" target="_blank">{% trans "Author of " %}<em>{{ book_title }}</em></a>
|
||||
{% else %}
|
||||
<a href="{{ match.id }}" target="_blank">{% if alt_title %}{% trans "Author of " %}<em>{{ alt_title }}</em>{% else %} {% trans "Find more information at isni.org" %}{% endif %}</a>
|
||||
<a href="{{ match.local_path }}" target="_blank" rel="nofollow noopener noreferrer">{% blocktrans trimmed %}
|
||||
Author of <em>{{ book_title }}</em>
|
||||
{% endblocktrans %}</a>
|
||||
{% else %}
|
||||
<a href="{{ match.id }}" target="_blank" rel="nofollow noopener noreferrer">{% if alt_title %}{% blocktrans trimmed %}
|
||||
Author of <em>{{ alt_title }}</em>
|
||||
{% endblocktrans %}{% else %}{% trans "Find more information at isni.org" %}{% endif %}</a>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</p>
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
{% for link in links %}
|
||||
<tr>
|
||||
<td class="overflow-wrap-anywhere">
|
||||
<a href="{{ link.url }}" target="_blank" rel="noopener noreferrer">{{ link.url }}</a>
|
||||
<a href="{{ link.url }}" target="_blank" rel="nofollow noopener noreferrer">{{ link.url }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if link.added_by %}
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
{% for link in links.all %}
|
||||
{% join "verify" link.id as verify_modal %}
|
||||
<li>
|
||||
<a href="{{ link.url }}" rel="noopener noreferrer" target="_blank" title="{{ link.url }}" data-modal-open="{{ verify_modal }}">{{ link.name }}</a>
|
||||
<a href="{{ link.url }}" rel="nofollow noopener noreferrer" target="_blank" title="{{ link.url }}" data-modal-open="{{ verify_modal }}">{{ link.name }}</a>
|
||||
({{ link.filetype }})
|
||||
|
||||
{% if link.availability != "free" %}
|
||||
|
|
|
@ -23,7 +23,7 @@ Is that where you'd like to go?
|
|||
</div>
|
||||
|
||||
<button type="button" class="button" data-modal-close>{% trans "Cancel" %}</button>
|
||||
<a href="{{ link.url }}" target="_blank" rel="noopener noreferrer" class="button is-primary">{% trans "Continue" %}</a>
|
||||
<a href="{{ link.url }}" target="_blank" rel="nofollow noopener noreferrer" noreferrer" class="button is-primary">{% trans "Continue" %}</a>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -39,7 +39,11 @@
|
|||
</div>
|
||||
|
||||
<p class="help" id="desc_source">
|
||||
{% trans 'You can download your Goodreads data from the <a href="https://www.goodreads.com/review/import" target="_blank" rel="noopener noreferrer">Import/Export page</a> of your Goodreads account.' %}
|
||||
{% blocktrans trimmed %}
|
||||
You can download your Goodreads data from the
|
||||
<a href="https://www.goodreads.com/review/import" target="_blank" rel="nofollow noopener noreferrer">Import/Export page</a>
|
||||
of your Goodreads account.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
</dl>
|
||||
</div>
|
||||
|
||||
{% if not job.complete %}
|
||||
{% if not job.complete and show_progress %}
|
||||
<div class="box is-processing">
|
||||
<div class="block">
|
||||
<span class="icon icon-spinner is-pulled-left" aria-hidden="true"></span>
|
||||
|
@ -94,7 +94,7 @@
|
|||
<div class="block">
|
||||
{% block actions %}{% endblock %}
|
||||
<div class="table-container">
|
||||
<table class="table is-striped">
|
||||
<table class="table is-striped is-fullwidth">
|
||||
<tr>
|
||||
<th>
|
||||
{% trans "Row" %}
|
||||
|
@ -137,6 +137,13 @@
|
|||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
{% if not items %}
|
||||
<tr>
|
||||
<td colspan="6">
|
||||
<em>{% trans "No items currently need review" %}</em>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% for item in items %}
|
||||
<tr>
|
||||
{% block index_col %}
|
||||
|
@ -169,7 +176,7 @@
|
|||
<p>{{ item.review|truncatechars:100 }}</p>
|
||||
{% endif %}
|
||||
{% if item.linked_review %}
|
||||
<a href="{{ item.linked_review.remote_id }}" target="_blank">{% trans "View imported review" %}</a>
|
||||
<a href="{{ item.linked_review.remote_id }}" target="_blank" rel="nofollow noopener noreferrer">{% trans "View imported review" %}</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% block import_cols %}
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
<div class="columns is-mobile">
|
||||
{% with guess=item.book_guess %}
|
||||
<div class="column is-narrow">
|
||||
<a href="{{ item.book.local_path }}" target="_blank">
|
||||
<a href="{{ item.book.local_path }}" target="_blank" rel="nofollow noopener noreferrer">
|
||||
{% include 'snippets/book_cover.html' with book=guess cover_class='is-h-s' size='small' %}
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -67,9 +67,27 @@
|
|||
</form>
|
||||
{% include "search/barcode_modal.html" with id="barcode-scanner-modal" %}
|
||||
|
||||
<button type="button" tabindex="0" class="navbar-burger pulldown-menu my-4" data-controls="main_nav" aria-expanded="false">
|
||||
<i class="icon icon-dots-three-vertical" aria-hidden="true"></i>
|
||||
<span class="is-sr-only">{% trans "Main navigation menu" %}</span>
|
||||
<button
|
||||
type="button"
|
||||
tabindex="0"
|
||||
class="navbar-burger pulldown-menu my-4 is-flex-touch is-align-items-center is-justify-content-center"
|
||||
data-controls="main_nav"
|
||||
aria-expanded="false"
|
||||
aria-label="{% trans 'Main navigation menu' %}"
|
||||
>
|
||||
<i class="icon-dots-three-vertical" aria-hidden="true"></i>
|
||||
|
||||
{% with request.user.unread_notification_count as notification_count %}
|
||||
<strong
|
||||
class="{% if not notification_count %}is-hidden {% elif request.user.has_unread_mentions %}is-danger {% else %}is-primary {% endif %} tag is-small px-1"
|
||||
data-poll-wrapper
|
||||
>
|
||||
<span class="is-sr-only">{% trans "Notifications" %}</span>
|
||||
<strong data-poll="notifications" class="has-text-white">
|
||||
{{ notification_count }}
|
||||
</strong>
|
||||
</strong>
|
||||
{% endwith %}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
@ -200,11 +218,17 @@
|
|||
{% if site.support_link %}
|
||||
<p>
|
||||
<span class="icon icon-heart"></span>
|
||||
{% blocktrans with site_name=site.name support_link=site.support_link support_title=site.support_title %}Support {{ site_name }} on <a href="{{ support_link }}" target="_blank">{{ support_title }}</a>{% endblocktrans %}
|
||||
{% blocktrans trimmed with site_name=site.name support_link=site.support_link support_title=site.support_title %}
|
||||
Support {{ site_name }} on
|
||||
<a href="{{ support_link }}" target="_blank" rel="nofollow noopener noreferrer">{{ support_title }}</a>
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<p>
|
||||
{% blocktrans %}BookWyrm's source code is freely available. You can contribute or report issues on <a href="https://github.com/mouse-reeve/bookwyrm">GitHub</a>.{% endblocktrans %}
|
||||
{% blocktrans trimmed %}
|
||||
BookWyrm's source code is freely available. You can contribute or report issues on
|
||||
<a href="https://github.com/bookwyrm-social/bookwyrm" target="_blank" rel="nofollow noopener noreferrer">GitHub</a>.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
</div>
|
||||
{% if site.footer_item %}
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
<h1 class="title">{% trans "Notifications" %}</h1>
|
||||
</div>
|
||||
|
||||
{% if notifications %}
|
||||
<form name="clear" action="/notifications" method="POST" class="column is-narrow">
|
||||
{% csrf_token %}
|
||||
{% spaceless %}
|
||||
|
@ -19,6 +20,7 @@
|
|||
</button>
|
||||
{% endspaceless %}
|
||||
</form>
|
||||
{% endif %}
|
||||
</header>
|
||||
|
||||
<div class="block">
|
||||
|
|
|
@ -4,7 +4,14 @@
|
|||
|
||||
<div class="field mb-0">
|
||||
<div class="control">
|
||||
<a class="button is-small is-link" href="{% url 'remote-follow-page' %}?user={{ user.username }}" target="_blank" rel="noopener noreferrer" onclick="BookWyrm.displayPopUp(`{% url 'remote-follow-page' %}?user={{ user.username }}`, `remoteFollow`); return false;" aria-describedby="remote_follow_warning">
|
||||
<a
|
||||
class="button is-small is-link"
|
||||
href="{% url 'remote-follow-page' %}?user={{ user.username }}"
|
||||
target="_blank"
|
||||
rel="nofollow noopener noreferrer"
|
||||
onclick="BookWyrm.displayPopUp(`{% url 'remote-follow-page' %}?user={{ user.username }}`, `remoteFollow`); return false;"
|
||||
aria-describedby="remote_follow_warning"
|
||||
>
|
||||
{% blocktrans with username=user.localname %}Follow on Fediverse{% endblocktrans %}
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{% load layout %}
|
||||
{% load sass_tags %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load utilities %}
|
||||
|
@ -9,9 +10,7 @@
|
|||
<head>
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="{% static 'css/vendor/bulma.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/vendor/icons.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/bookwyrm.css' %}">
|
||||
<link href="{% sass_src site_theme %}" rel="stylesheet" type="text/css" />
|
||||
<script>
|
||||
function closeWindow() {
|
||||
window.close();
|
||||
|
|
|
@ -3,10 +3,9 @@
|
|||
|
||||
{% block panel %}
|
||||
|
||||
{% if results %}
|
||||
{% with results|first as local_results %}
|
||||
{% if results or remote_results %}
|
||||
<ul class="block">
|
||||
{% for result in local_results.results %}
|
||||
{% for result in results %}
|
||||
<li class="pd-4 mb-5 local-book-search-result" id="tour-local-book-search-result">
|
||||
<div class="columns is-mobile is-gapless mb-0">
|
||||
<div class="column is-cover">
|
||||
|
@ -29,25 +28,24 @@
|
|||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endwith %}
|
||||
|
||||
<div class="block">
|
||||
{% for result_set in results|slice:"1:" %}
|
||||
{% for result_set in remote_results %}
|
||||
{% if result_set.results %}
|
||||
<section class="mb-5">
|
||||
{% if not result_set.connector.local %}
|
||||
<details class="details-panel box" open>
|
||||
{% endif %}
|
||||
{% if not result_set.connector.local %}
|
||||
<summary class="is-flex is-align-items-center is-flex-wrap-wrap is-gap-2 remote-book-search-result" id="tour-remote-search-result">
|
||||
<span class="mb-0 title is-5">
|
||||
{% trans 'Results from' %}
|
||||
<a href="{{ result_set.connector.base_url }}" target="_blank">{{ result_set.connector.name|default:result_set.connector.identifier }}</a>
|
||||
<a
|
||||
href="{{ result_set.connector.base_url }}"
|
||||
target="_blank"
|
||||
rel="nofollow noopener noreferrer"
|
||||
>{{ result_set.connector.name|default:result_set.connector.identifier }}</a>
|
||||
</span>
|
||||
|
||||
<span class="details-close icon icon-x" aria-hidden="true"></span>
|
||||
</summary>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-5">
|
||||
<div class="is-flex is-flex-direction-row-reverse">
|
||||
|
@ -63,7 +61,7 @@
|
|||
<strong>
|
||||
<a
|
||||
href="{{ result.view_link|default:result.key }}"
|
||||
rel="noopener noreferrer"
|
||||
rel="nofollow noopener noreferrer"
|
||||
target="_blank"
|
||||
>{{ result.title }}</a>
|
||||
</strong>
|
||||
|
@ -88,17 +86,15 @@
|
|||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% if not result_set.connector.local %}
|
||||
</details>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block search_footer %}
|
||||
<p class="block">
|
||||
{% if request.user.is_authenticated %}
|
||||
{% if not remote %}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block title %}{% trans "Search" %}{% endblock %}
|
||||
|
||||
|
@ -53,17 +54,24 @@
|
|||
</nav>
|
||||
|
||||
<section class="block" id="search-results-block">
|
||||
{% if not results %}
|
||||
<p>
|
||||
<p class="block">
|
||||
{% if not results %}
|
||||
<em>{% blocktrans %}No results found for "{{ query }}"{% endblocktrans %}</em>
|
||||
{% else %}
|
||||
<em>{% blocktrans trimmed count counter=results.paginator.count with result_count=results.paginator.count|intcomma %}
|
||||
{{ result_count }} result found
|
||||
{% plural %}
|
||||
{{ result_count }} results found
|
||||
{% endblocktrans %}</em>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% block panel %}
|
||||
{% endblock %}
|
||||
|
||||
<div>
|
||||
<div class="block">
|
||||
{% include 'snippets/pagination.html' with page=results path=request.path %}
|
||||
</div>
|
||||
{% block search_footer %}{% endblock %}
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
|
|
109
bookwyrm/templates/settings/celery.html
Normal file
109
bookwyrm/templates/settings/celery.html
Normal file
|
@ -0,0 +1,109 @@
|
|||
{% extends 'settings/layout.html' %}
|
||||
{% load humanize %}
|
||||
{% load i18n %}
|
||||
{% load celery_tags %}
|
||||
|
||||
{% block title %}{% trans "Celery Status" %}{% endblock %}
|
||||
|
||||
{% block header %}{% trans "Celery Status" %}{% endblock %}
|
||||
|
||||
{% block panel %}
|
||||
|
||||
{% if queues %}
|
||||
<section class="block content">
|
||||
<h2>{% trans "Queues" %}</h2>
|
||||
<div class="columns has-text-centered">
|
||||
<div class="column is-4">
|
||||
<div class="notification">
|
||||
<p class="header">{% trans "Low priority" %}</p>
|
||||
<p class="title is-5">{{ queues.low_priority|intcomma }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="notification">
|
||||
<p class="header">{% trans "Medium priority" %}</p>
|
||||
<p class="title is-5">{{ queues.medium_priority|intcomma }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="notification">
|
||||
<p class="header">{% trans "High priority" %}</p>
|
||||
<p class="title is-5">{{ queues.high_priority|intcomma }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% else %}
|
||||
<div class="notification is-danger is-flex is-align-items-start">
|
||||
<span class="icon icon-warning is-size-4 pr-3" aria-hidden="true"></span>
|
||||
<span>
|
||||
{% trans "Could not connect to Redis broker" %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% if stats %}
|
||||
<section class="block content">
|
||||
<h2>{% trans "Active Tasks" %}</h2>
|
||||
{% for worker in active_tasks.values %}
|
||||
<div class="table-container">
|
||||
<table class="table is-striped is-fullwidth">
|
||||
<tr>
|
||||
<th>{% trans "ID" %}</th>
|
||||
<th>{% trans "Task name" %}</th>
|
||||
<th>{% trans "Run time" %}</th>
|
||||
<th>{% trans "Priority" %}</th>
|
||||
</tr>
|
||||
{% if not worker %}
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<em>{% trans "No active tasks" %}</em>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% for task in worker %}
|
||||
<tr>
|
||||
<td>{{ task.id }}</td>
|
||||
<td>{{ task.name|shortname }}</td>
|
||||
<td>{{ task.time_start|runtime }}</td>
|
||||
<td>{{ task.delivery_info.routing_key }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
||||
<section class="block content">
|
||||
<h2>{% trans "Workers" %}</h2>
|
||||
|
||||
{% for worker_name, worker in stats.items %}
|
||||
<div class="notification">
|
||||
<h3>{{ worker_name }}</h3>
|
||||
{% trans "Uptime:" %} {{ worker.uptime|uptime }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
||||
{% else %}
|
||||
|
||||
<div class="notification is-danger is-flex is-align-items-start">
|
||||
<span class="icon icon-warning is-size-4 pr-3" aria-hidden="true"></span>
|
||||
<span>
|
||||
{% trans "Could not connect to Celery" %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if errors %}
|
||||
<div class="block content">
|
||||
<h2>{% trans "Errors" %}</h2>
|
||||
{% for error in errors %}
|
||||
<pre>{{ error }}</pre>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
|
@ -57,10 +57,6 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if current_version %}
|
||||
{% include 'settings/dashboard/warnings/update_version.html' with warning_level="warning" fullwidth=True %}
|
||||
{% endif %}
|
||||
|
||||
{% if reports %}
|
||||
{% include 'settings/dashboard/warnings/reports.html' with warning_level="warning" %}
|
||||
{% endif %}
|
||||
|
|
|
@ -59,7 +59,9 @@
|
|||
<div class="field">
|
||||
<label class="label" for="id_file">JSON data:</label>
|
||||
<aside class="help">
|
||||
Expects a json file in the format provided by <a href="https://fediblock.org/" target="_blank" rel="noopener noreferrer">FediBlock</a>, with a list of entries that have <code>instance</code> and <code>url</code> fields. For example:
|
||||
{% blocktrans trimmed %}
|
||||
Expects a json file in the format provided by <a href="https://fediblock.org/" target="_blank" rel="nofollow noopener noreferrer">FediBlock</a>, with a list of entries that have <code>instance</code> and <code>url</code> fields. For example:
|
||||
{% endblocktrans %}
|
||||
<pre>
|
||||
[
|
||||
{
|
||||
|
|
|
@ -74,6 +74,15 @@
|
|||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if perms.edit_instance_settings %}
|
||||
<h2 class="menu-label">{% trans "System" %}</h2>
|
||||
<ul class="menu-list">
|
||||
<li>
|
||||
{% url 'settings-celery' as url %}
|
||||
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Celery status" %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if perms.bookwyrm.edit_instance_settings %}
|
||||
<h2 class="menu-label">{% trans "Instance Settings" %}</h2>
|
||||
<ul class="menu-list">
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
<header class="column">
|
||||
<h2 class="title is-5">
|
||||
{{ domain.name }}
|
||||
(<a href="http://{{ domain.domain }}" target="_blank" rel="noopener noreferrer">{{ domain.domain }}</a>)
|
||||
(<a href="http://{{ domain.domain }}" target="_blank" rel="nofollow noopener noreferrer">{{ domain.domain }}</a>)
|
||||
</h2>
|
||||
</header>
|
||||
<div class="column is-narrow">
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
{% for link in links %}
|
||||
<tr>
|
||||
<td class="overflow-wrap-anywhere">
|
||||
<a href="{{ link.url }}" target="_blank" rel="noopener noreferrer">{{ link.url }}</a>
|
||||
<a href="{{ link.url }}" target="_blank" rel="nofollow noopener noreferrer">{{ link.url }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if link.added_by %}
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
{% trans "Once the instance is set up, you can promote other users to moderator or admin roles from the admin panel." %}
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://docs.joinbookwyrm.com/moderation.html" target="_blank">
|
||||
<a href="https://docs.joinbookwyrm.com/moderation.html" target="_blank" rel="nofollow noopener noreferrer">
|
||||
{% trans "Learn more about moderation" %}
|
||||
</a>
|
||||
</p>
|
||||
|
|
|
@ -144,7 +144,7 @@
|
|||
{% blocktrans trimmed %}
|
||||
You can change your instance settings in the <code>.env</code> file on your server.
|
||||
{% endblocktrans %}
|
||||
<a href="https://docs.joinbookwyrm.com/install-prod.html" target="_blank">
|
||||
<a href="https://docs.joinbookwyrm.com/install-prod.html" target="_blank" rel="nofollow noopener noreferrer">
|
||||
{% trans "View installation instructions" %}
|
||||
</a>
|
||||
</p>
|
||||
|
|
|
@ -9,13 +9,17 @@
|
|||
<div class="container">
|
||||
<div class="navbar-brand is-flex-grow-1">
|
||||
<span class="navbar-item" href="/">
|
||||
<img class="image logo" src="{% if site.logo_small %}{% get_media_prefix %}{{ site.logo_small }}{% else %}{% static "images/logo-small.png" %}{% endif %}" alt="{% blocktrans with site_name=site.name %}{{ site_name }} home page{% endblocktrans %}">
|
||||
<img
|
||||
class="image logo"
|
||||
src="{% if site.logo_small %}{% get_media_prefix %}{{ site.logo_small }}{% else %}{% static "images/logo-small.png" %}{% endif %}"
|
||||
alt="{% blocktrans with site_name=site.name %}{{ site_name }} home page{% endblocktrans %}"
|
||||
>
|
||||
</span>
|
||||
<div class="navbar-item is-align-items-start pt-5 is-flex-grow-1">
|
||||
{% trans "Installing BookWyrm" %}
|
||||
</div>
|
||||
<div class="navbar-item is-align-items-start pt-5">
|
||||
<a href="https://joinbookwyrm.com/get-involved/#dev-chat" target="_blank">{% trans "Need help?" %}</a>
|
||||
<a href="https://joinbookwyrm.com/get-involved/#dev-chat" target="_blank" rel="nofollow noopener noreferrer">{% trans "Need help?" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
type="radio"
|
||||
name="rating"
|
||||
value="{{ forloop.counter0 }}.5"
|
||||
{% if default_rating == forloop.counter %}checked{% endif %}
|
||||
{% if default_rating > 0 and default_rating >= forloop.counter0 %}checked{% endif %}
|
||||
/>
|
||||
<input
|
||||
id="{{ type|slugify }}_book{{ book.id }}_star_{{ forloop.counter }}"
|
||||
|
@ -45,7 +45,7 @@
|
|||
type="radio"
|
||||
name="rating"
|
||||
value="{{ forloop.counter }}"
|
||||
{% if default_rating == forloop.counter %}checked{% endif %}
|
||||
{% if default_rating >= forloop.counter %}checked{% endif %}
|
||||
/>
|
||||
|
||||
<label
|
||||
|
|
|
@ -123,6 +123,7 @@
|
|||
<a
|
||||
href="{% get_media_prefix %}{{ attachment.image }}"
|
||||
target="_blank"
|
||||
rel="nofollow noopener noreferrer"
|
||||
aria-label="{% trans 'Open image in new window' %}"
|
||||
>
|
||||
<img
|
||||
|
|
|
@ -65,7 +65,7 @@
|
|||
<div class="columns is-mobile">
|
||||
<h2 class="title column">{% trans "User Activity" %}</h2>
|
||||
<div class="column is-narrow">
|
||||
<a target="_blank" href="{{ user.local_path }}/rss">
|
||||
<a target="_blank" href="{{ user.local_path }}/rss" rel="nofollow noopener noreferrer">
|
||||
<span class="icon icon-rss" aria-hidden="true"></span>
|
||||
<span class="is-hidden-mobile">{% trans "RSS feed" %}</span>
|
||||
</a>
|
||||
|
|
|
@ -8,7 +8,12 @@ register = template.Library()
|
|||
@register.filter(name="book_description")
|
||||
def get_book_description(book):
|
||||
"""use the work's text if the book doesn't have it"""
|
||||
return book.description or book.parent_work.description
|
||||
if book.description:
|
||||
return book.description
|
||||
if book.parent_work:
|
||||
# this shoud always be true
|
||||
return book.parent_work.description
|
||||
return None
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=False)
|
||||
|
|
24
bookwyrm/templatetags/celery_tags.py
Normal file
24
bookwyrm/templatetags/celery_tags.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
""" template filters for really common utilities """
|
||||
import datetime
|
||||
from django import template
|
||||
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter(name="uptime")
|
||||
def uptime(seconds):
|
||||
"""Seconds uptime to a readable format"""
|
||||
return str(datetime.timedelta(seconds=seconds))
|
||||
|
||||
|
||||
@register.filter(name="runtime")
|
||||
def runtime(timestamp):
|
||||
"""How long has it been?"""
|
||||
return datetime.datetime.now() - datetime.datetime.fromtimestamp(timestamp)
|
||||
|
||||
|
||||
@register.filter(name="shortname")
|
||||
def shortname(name):
|
||||
"""removes bookwyrm.celery..."""
|
||||
return ".".join(name.split(".")[-2:])
|
|
@ -28,6 +28,12 @@ class BookSearch(TestCase):
|
|||
openlibrary_key="hello",
|
||||
)
|
||||
|
||||
self.third_edition = models.Edition.objects.create(
|
||||
title="Edition with annoying ISBN",
|
||||
parent_work=self.work,
|
||||
isbn_10="022222222X",
|
||||
)
|
||||
|
||||
def test_search(self):
|
||||
"""search for a book in the db"""
|
||||
# title/author
|
||||
|
@ -57,6 +63,12 @@ class BookSearch(TestCase):
|
|||
self.assertEqual(len(results), 1)
|
||||
self.assertEqual(results[0], self.second_edition)
|
||||
|
||||
def test_search_identifiers_isbn_search(self):
|
||||
"""search by unique ID with slightly wonky ISBN"""
|
||||
results = book_search.search_identifiers("22222222x")
|
||||
self.assertEqual(len(results), 1)
|
||||
self.assertEqual(results[0], self.third_edition)
|
||||
|
||||
def test_search_identifiers_return_first(self):
|
||||
"""search by unique identifiers"""
|
||||
result = book_search.search_identifiers("hello", return_first=True)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
""" html validation on rendered templates """
|
||||
from html.parser import HTMLParser
|
||||
from tidylib import tidy_document
|
||||
|
||||
|
||||
|
@ -23,3 +24,32 @@ def validate_html(html):
|
|||
)
|
||||
if errors:
|
||||
raise Exception(errors)
|
||||
|
||||
validator = HtmlValidator()
|
||||
# will raise exceptions
|
||||
validator.feed(str(html.content))
|
||||
|
||||
|
||||
class HtmlValidator(HTMLParser): # pylint: disable=abstract-method
|
||||
"""Checks for custom html validation requirements"""
|
||||
|
||||
def __init__(self):
|
||||
HTMLParser.__init__(self)
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
"""check if the tag is valid"""
|
||||
# filter out everything besides links that open in new tabs
|
||||
if tag != "a" or ("target", "_blank") not in attrs:
|
||||
return
|
||||
|
||||
for attr, value in attrs:
|
||||
if (
|
||||
attr == "rel"
|
||||
and "nofollow" in value
|
||||
and "noopener" in value
|
||||
and "noreferrer" in value
|
||||
):
|
||||
return
|
||||
raise Exception(
|
||||
'Links to a new tab must have rel="nofollow noopener noreferrer"'
|
||||
)
|
||||
|
|
45
bookwyrm/tests/views/admin/test_celery.py
Normal file
45
bookwyrm/tests/views/admin/test_celery.py
Normal file
|
@ -0,0 +1,45 @@
|
|||
""" test for app action functionality """
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.auth.models import Group
|
||||
from django.template.response import TemplateResponse
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
from bookwyrm import models, views
|
||||
from bookwyrm.management.commands import initdb
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
||||
class CeleryStatusViews(TestCase):
|
||||
"""every response to a get request, html or json"""
|
||||
|
||||
def setUp(self):
|
||||
"""we need basic test data and mocks"""
|
||||
self.factory = RequestFactory()
|
||||
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
|
||||
"bookwyrm.activitystreams.populate_stream_task.delay"
|
||||
), patch("bookwyrm.lists_stream.populate_lists_task.delay"):
|
||||
self.local_user = models.User.objects.create_user(
|
||||
"mouse@local.com",
|
||||
"mouse@mouse.mouse",
|
||||
"password",
|
||||
local=True,
|
||||
localname="mouse",
|
||||
)
|
||||
initdb.init_groups()
|
||||
initdb.init_permissions()
|
||||
group = Group.objects.get(name="admin")
|
||||
self.local_user.groups.set([group])
|
||||
models.SiteSettings.objects.create()
|
||||
|
||||
def test_celery_status_get(self):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
view = views.CeleryStatus.as_view()
|
||||
request = self.factory.get("")
|
||||
request.user = self.local_user
|
||||
|
||||
result = view(request)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
|
@ -13,7 +13,7 @@ from bookwyrm.tests.validate_html import validate_html
|
|||
class LandingViews(TestCase):
|
||||
"""pages you land on without really trying"""
|
||||
|
||||
def setUp(self):
|
||||
def setUp(self): # pylint: disable=invalid-name
|
||||
"""we need basic test data and mocks"""
|
||||
self.factory = RequestFactory()
|
||||
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
|
||||
|
|
|
@ -7,13 +7,14 @@ from django.test import TestCase
|
|||
from django.test.client import RequestFactory
|
||||
|
||||
from bookwyrm import models, views
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
from bookwyrm.settings import DOMAIN
|
||||
|
||||
|
||||
class IsbnViews(TestCase):
|
||||
"""tag views"""
|
||||
|
||||
def setUp(self):
|
||||
def setUp(self): # pylint: disable=invalid-name
|
||||
"""we need basic test data and mocks"""
|
||||
self.factory = RequestFactory()
|
||||
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
|
||||
|
@ -58,4 +59,4 @@ class IsbnViews(TestCase):
|
|||
is_api.return_value = False
|
||||
response = view(request, isbn="1234567890123")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response.render()
|
||||
validate_html(response.render())
|
||||
|
|
|
@ -17,7 +17,7 @@ from bookwyrm.tests.validate_html import validate_html
|
|||
class Views(TestCase):
|
||||
"""tag views"""
|
||||
|
||||
def setUp(self):
|
||||
def setUp(self): # pylint: disable=invalid-name
|
||||
"""we need basic test data and mocks"""
|
||||
self.factory = RequestFactory()
|
||||
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
|
||||
|
@ -90,13 +90,29 @@ class Views(TestCase):
|
|||
|
||||
self.assertIsInstance(response, TemplateResponse)
|
||||
validate_html(response.render())
|
||||
connector_results = response.context_data["results"]
|
||||
self.assertEqual(len(connector_results), 2)
|
||||
self.assertEqual(connector_results[0]["results"][0].title, "Test Book")
|
||||
self.assertEqual(connector_results[1]["results"][0].title, "Mock Book")
|
||||
|
||||
# don't search remote
|
||||
local_results = response.context_data["results"]
|
||||
self.assertEqual(local_results[0].title, "Test Book")
|
||||
|
||||
connector_results = response.context_data["remote_results"]
|
||||
self.assertEqual(connector_results[0]["results"][0].title, "Mock Book")
|
||||
|
||||
def test_search_book_anonymous(self):
|
||||
"""Don't search remote for logged out user"""
|
||||
view = views.Search.as_view()
|
||||
|
||||
connector = models.Connector.objects.create(
|
||||
identifier="example.com",
|
||||
connector_file="openlibrary",
|
||||
base_url="https://example.com",
|
||||
books_url="https://example.com/books",
|
||||
covers_url="https://example.com/covers",
|
||||
search_url="https://example.com/search?q=",
|
||||
)
|
||||
mock_result = SearchResult(title="Mock Book", connector=connector, key="hello")
|
||||
|
||||
request = self.factory.get("", {"q": "Test Book", "remote": True})
|
||||
|
||||
anonymous_user = AnonymousUser
|
||||
anonymous_user.is_authenticated = False
|
||||
request.user = anonymous_user
|
||||
|
@ -107,11 +123,15 @@ class Views(TestCase):
|
|||
{"results": [mock_result], "connector": connector}
|
||||
]
|
||||
response = view(request)
|
||||
|
||||
self.assertIsInstance(response, TemplateResponse)
|
||||
validate_html(response.render())
|
||||
connector_results = response.context_data["results"]
|
||||
self.assertEqual(len(connector_results), 1)
|
||||
self.assertEqual(connector_results[0]["results"][0].title, "Test Book")
|
||||
|
||||
local_results = response.context_data["results"]
|
||||
self.assertEqual(local_results[0].title, "Test Book")
|
||||
|
||||
connector_results = response.context_data.get("remote_results")
|
||||
self.assertIsNone(connector_results)
|
||||
|
||||
def test_search_users(self):
|
||||
"""searches remote connectors"""
|
||||
|
|
|
@ -6,6 +6,7 @@ from django.test import TestCase
|
|||
from django.test.client import RequestFactory
|
||||
|
||||
from bookwyrm import forms, models, views
|
||||
from bookwyrm.views.status import find_mentions
|
||||
from bookwyrm.settings import DOMAIN
|
||||
from bookwyrm.tests.validate_html import validate_html
|
||||
|
||||
|
@ -34,6 +35,13 @@ class StatusViews(TestCase):
|
|||
localname="mouse",
|
||||
remote_id="https://example.com/users/mouse",
|
||||
)
|
||||
self.another_user = models.User.objects.create_user(
|
||||
f"nutria@{DOMAIN}",
|
||||
"nutria@nutria.com",
|
||||
"password",
|
||||
local=True,
|
||||
localname="nutria",
|
||||
)
|
||||
with patch("bookwyrm.models.user.set_remote_server"):
|
||||
self.remote_user = models.User.objects.create_user(
|
||||
"rat",
|
||||
|
@ -211,51 +219,66 @@ class StatusViews(TestCase):
|
|||
self.assertFalse(self.remote_user in reply.mention_users.all())
|
||||
self.assertTrue(self.local_user in reply.mention_users.all())
|
||||
|
||||
def test_find_mentions(self, *_):
|
||||
def test_find_mentions_local(self, *_):
|
||||
"""detect and look up @ mentions of users"""
|
||||
user = models.User.objects.create_user(
|
||||
f"nutria@{DOMAIN}",
|
||||
"nutria@nutria.com",
|
||||
"password",
|
||||
local=True,
|
||||
localname="nutria",
|
||||
)
|
||||
self.assertEqual(user.username, f"nutria@{DOMAIN}")
|
||||
result = find_mentions(self.local_user, "@nutria")
|
||||
self.assertEqual(result["@nutria"], self.another_user)
|
||||
self.assertEqual(result[f"@nutria@{DOMAIN}"], self.another_user)
|
||||
|
||||
result = find_mentions(self.local_user, f"@nutria@{DOMAIN}")
|
||||
self.assertEqual(result["@nutria"], self.another_user)
|
||||
self.assertEqual(result[f"@nutria@{DOMAIN}"], self.another_user)
|
||||
|
||||
result = find_mentions(self.local_user, "leading text @nutria")
|
||||
self.assertEqual(result["@nutria"], self.another_user)
|
||||
self.assertEqual(result[f"@nutria@{DOMAIN}"], self.another_user)
|
||||
|
||||
result = find_mentions(self.local_user, "leading @nutria trailing")
|
||||
self.assertEqual(result["@nutria"], self.another_user)
|
||||
self.assertEqual(result[f"@nutria@{DOMAIN}"], self.another_user)
|
||||
|
||||
self.assertEqual(find_mentions(self.local_user, "leading@nutria"), {})
|
||||
|
||||
def test_find_mentions_remote(self, *_):
|
||||
"""detect and look up @ mentions of users"""
|
||||
self.assertEqual(
|
||||
list(views.status.find_mentions("@nutria"))[0], ("@nutria", user)
|
||||
)
|
||||
self.assertEqual(
|
||||
list(views.status.find_mentions("leading text @nutria"))[0],
|
||||
("@nutria", user),
|
||||
)
|
||||
self.assertEqual(
|
||||
list(views.status.find_mentions("leading @nutria trailing text"))[0],
|
||||
("@nutria", user),
|
||||
)
|
||||
self.assertEqual(
|
||||
list(views.status.find_mentions("@rat@example.com"))[0],
|
||||
("@rat@example.com", self.remote_user),
|
||||
find_mentions(self.local_user, "@rat@example.com"),
|
||||
{"@rat@example.com": self.remote_user},
|
||||
)
|
||||
|
||||
multiple = list(views.status.find_mentions("@nutria and @rat@example.com"))
|
||||
self.assertEqual(multiple[0], ("@nutria", user))
|
||||
self.assertEqual(multiple[1], ("@rat@example.com", self.remote_user))
|
||||
def test_find_mentions_multiple(self, *_):
|
||||
"""detect and look up @ mentions of users"""
|
||||
multiple = find_mentions(self.local_user, "@nutria and @rat@example.com")
|
||||
self.assertEqual(multiple["@nutria"], self.another_user)
|
||||
self.assertEqual(multiple[f"@nutria@{DOMAIN}"], self.another_user)
|
||||
self.assertEqual(multiple["@rat@example.com"], self.remote_user)
|
||||
self.assertIsNone(multiple.get("@rat"))
|
||||
|
||||
def test_find_mentions_unknown(self, *_):
|
||||
"""detect and look up @ mentions of users"""
|
||||
multiple = find_mentions(self.local_user, "@nutria and @rdkjfgh")
|
||||
self.assertEqual(multiple["@nutria"], self.another_user)
|
||||
self.assertEqual(multiple[f"@nutria@{DOMAIN}"], self.another_user)
|
||||
|
||||
def test_find_mentions_blocked(self, *_):
|
||||
"""detect and look up @ mentions of users"""
|
||||
self.another_user.blocks.add(self.local_user)
|
||||
|
||||
result = find_mentions(self.local_user, "@nutria hello")
|
||||
self.assertEqual(result, {})
|
||||
|
||||
def test_find_mentions_unknown_remote(self, *_):
|
||||
"""mention a user that isn't in the database"""
|
||||
with patch("bookwyrm.views.status.handle_remote_webfinger") as rw:
|
||||
rw.return_value = self.local_user
|
||||
self.assertEqual(
|
||||
list(views.status.find_mentions("@beep@beep.com"))[0],
|
||||
("@beep@beep.com", self.local_user),
|
||||
)
|
||||
rw.return_value = self.another_user
|
||||
result = find_mentions(self.local_user, "@beep@beep.com")
|
||||
self.assertEqual(result["@nutria"], self.another_user)
|
||||
self.assertEqual(result[f"@nutria@{DOMAIN}"], self.another_user)
|
||||
|
||||
with patch("bookwyrm.views.status.handle_remote_webfinger") as rw:
|
||||
rw.return_value = None
|
||||
self.assertEqual(list(views.status.find_mentions("@beep@beep.com")), [])
|
||||
|
||||
self.assertEqual(
|
||||
list(views.status.find_mentions(f"@nutria@{DOMAIN}"))[0],
|
||||
(f"@nutria@{DOMAIN}", user),
|
||||
)
|
||||
result = find_mentions(self.local_user, "@beep@beep.com")
|
||||
self.assertEqual(result, {})
|
||||
|
||||
def test_format_links_simple_url(self, *_):
|
||||
"""find and format urls into a tags"""
|
||||
|
|
|
@ -291,6 +291,9 @@ urlpatterns = [
|
|||
views.Report.as_view(),
|
||||
name="report-link",
|
||||
),
|
||||
re_path(
|
||||
r"^settings/celery/?$", views.CeleryStatus.as_view(), name="settings-celery"
|
||||
),
|
||||
# landing pages
|
||||
re_path(r"^about/?$", views.about, name="about"),
|
||||
re_path(r"^privacy/?$", views.privacy, name="privacy"),
|
||||
|
@ -581,7 +584,7 @@ urlpatterns = [
|
|||
name="author-update-remote",
|
||||
),
|
||||
# isbn
|
||||
re_path(r"^isbn/(?P<isbn>\d+)(.json)?/?$", views.Isbn.as_view()),
|
||||
re_path(r"^isbn/(?P<isbn>[\dxX]+)(.json)?/?$", views.Isbn.as_view()),
|
||||
# author
|
||||
re_path(
|
||||
r"^author/(?P<author_id>\d+)(.json)?/?$", views.Author.as_view(), name="author"
|
||||
|
|
|
@ -4,7 +4,7 @@ DOMAIN = r"[\w_\-\.]+\.[a-z\-]{2,}"
|
|||
LOCALNAME = r"@?[a-zA-Z_\-\.0-9]+"
|
||||
STRICT_LOCALNAME = r"@[a-zA-Z_\-\.0-9]+"
|
||||
USERNAME = rf"{LOCALNAME}(@{DOMAIN})?"
|
||||
STRICT_USERNAME = rf"\B{STRICT_LOCALNAME}(@{DOMAIN})?\b"
|
||||
STRICT_USERNAME = rf"(\B{STRICT_LOCALNAME}(@{DOMAIN})?\b)"
|
||||
FULL_USERNAME = rf"{LOCALNAME}@{DOMAIN}\b"
|
||||
SLUG = r"/s/(?P<slug>[-_a-z0-9]*)"
|
||||
# should match (BookWyrm/1.0.0; or (BookWyrm/99.1.2;
|
||||
|
|
|
@ -4,6 +4,7 @@ from .admin.announcements import Announcements, Announcement
|
|||
from .admin.announcements import EditAnnouncement, delete_announcement
|
||||
from .admin.automod import AutoMod, automod_delete, run_automod
|
||||
from .admin.automod import schedule_automod_task, unschedule_automod_task
|
||||
from .admin.celery_status import CeleryStatus
|
||||
from .admin.dashboard import Dashboard
|
||||
from .admin.federation import Federation, FederatedServer
|
||||
from .admin.federation import AddFederatedServer, ImportServerBlocklist
|
||||
|
|
56
bookwyrm/views/admin/celery_status.py
Normal file
56
bookwyrm/views/admin/celery_status.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
""" celery status """
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
import redis
|
||||
|
||||
from celerywyrm import settings
|
||||
from bookwyrm.tasks import app as celery
|
||||
|
||||
r = redis.Redis(
|
||||
host=settings.REDIS_BROKER_HOST,
|
||||
port=settings.REDIS_BROKER_PORT,
|
||||
password=settings.REDIS_BROKER_PASSWORD,
|
||||
db=settings.REDIS_BROKER_DB_INDEX,
|
||||
)
|
||||
|
||||
# pylint: disable= no-self-use
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
@method_decorator(
|
||||
permission_required("bookwyrm.edit_instance_settings", raise_exception=True),
|
||||
name="dispatch",
|
||||
)
|
||||
class CeleryStatus(View):
|
||||
"""Are your tasks running? Well you'd better go catch them"""
|
||||
|
||||
def get(self, request):
|
||||
"""See workers and active tasks"""
|
||||
errors = []
|
||||
try:
|
||||
inspect = celery.control.inspect()
|
||||
stats = inspect.stats()
|
||||
active_tasks = inspect.active()
|
||||
# pylint: disable=broad-except
|
||||
except Exception as err:
|
||||
stats = active_tasks = None
|
||||
errors.append(err)
|
||||
|
||||
try:
|
||||
queues = {
|
||||
"low_priority": r.llen("low_priority"),
|
||||
"medium_priority": r.llen("medium_priority"),
|
||||
"high_priority": r.llen("high_priority"),
|
||||
}
|
||||
# pylint: disable=broad-except
|
||||
except Exception as err:
|
||||
queues = None
|
||||
errors.append(err)
|
||||
|
||||
data = {
|
||||
"stats": stats,
|
||||
"active_tasks": active_tasks,
|
||||
"queues": queues,
|
||||
"errors": errors,
|
||||
}
|
||||
return TemplateResponse(request, "settings/celery.html", data)
|
|
@ -59,7 +59,7 @@ def is_bookwyrm_request(request):
|
|||
return True
|
||||
|
||||
|
||||
def handle_remote_webfinger(query):
|
||||
def handle_remote_webfinger(query, unknown_only=False):
|
||||
"""webfingerin' other servers"""
|
||||
user = None
|
||||
|
||||
|
@ -75,6 +75,11 @@ def handle_remote_webfinger(query):
|
|||
|
||||
try:
|
||||
user = models.User.objects.get(username__iexact=query)
|
||||
|
||||
if unknown_only:
|
||||
# In this case, we only want to know about previously undiscovered users
|
||||
# So the fact that we found a match in the database means no results
|
||||
return None
|
||||
except models.User.DoesNotExist:
|
||||
url = f"https://{domain}/.well-known/webfinger?resource=acct:{query}"
|
||||
try:
|
||||
|
|
|
@ -47,6 +47,7 @@ class ImportStatus(View):
|
|||
"page_range": paginated.get_elided_page_range(
|
||||
page.number, on_each_side=2, on_ends=1
|
||||
),
|
||||
"show_progress": True,
|
||||
"item_count": item_count,
|
||||
"complete_count": item_count - pending_item_count,
|
||||
"percent": math.floor( # pylint: disable=c-extension-no-member
|
||||
|
|
|
@ -18,14 +18,17 @@ class Isbn(View):
|
|||
|
||||
if is_api_request(request):
|
||||
return JsonResponse(
|
||||
[book_search.format_search_result(r) for r in book_results], safe=False
|
||||
[book_search.format_search_result(r) for r in book_results[:10]],
|
||||
safe=False,
|
||||
)
|
||||
|
||||
paginated = Paginator(book_results, PAGE_LENGTH).get_page(
|
||||
request.GET.get("page")
|
||||
)
|
||||
paginated = Paginator(book_results, PAGE_LENGTH)
|
||||
page = paginated.get_page(request.GET.get("page"))
|
||||
data = {
|
||||
"results": [{"results": paginated}],
|
||||
"results": page,
|
||||
"page_range": paginated.get_elided_page_range(
|
||||
page.number, on_each_side=2, on_ends=1
|
||||
),
|
||||
"query": isbn,
|
||||
"type": "book",
|
||||
}
|
||||
|
|
|
@ -23,22 +23,14 @@ class Search(View):
|
|||
|
||||
def get(self, request):
|
||||
"""that search bar up top"""
|
||||
query = request.GET.get("q")
|
||||
# check if query is isbn
|
||||
query = isbn_check(query)
|
||||
min_confidence = request.GET.get("min_confidence", 0)
|
||||
search_type = request.GET.get("type")
|
||||
search_remote = (
|
||||
request.GET.get("remote", False) and request.user.is_authenticated
|
||||
)
|
||||
|
||||
if is_api_request(request):
|
||||
# only return local book results via json so we don't cascade
|
||||
book_results = search(query, min_confidence=min_confidence)
|
||||
return JsonResponse(
|
||||
[format_search_result(r) for r in book_results], safe=False
|
||||
)
|
||||
return api_book_search(request)
|
||||
|
||||
query = request.GET.get("q")
|
||||
if not query:
|
||||
return TemplateResponse(request, "search/book.html")
|
||||
|
||||
search_type = request.GET.get("type")
|
||||
if query and not search_type:
|
||||
search_type = "user" if "@" in query else "book"
|
||||
|
||||
|
@ -50,49 +42,67 @@ class Search(View):
|
|||
if not search_type in endpoints:
|
||||
search_type = "book"
|
||||
|
||||
data = {
|
||||
"query": query or "",
|
||||
"type": search_type,
|
||||
"remote": search_remote,
|
||||
}
|
||||
if query:
|
||||
results, search_remote = endpoints[search_type](
|
||||
query, request.user, min_confidence, search_remote
|
||||
)
|
||||
if results:
|
||||
paginated = Paginator(results, PAGE_LENGTH).get_page(
|
||||
request.GET.get("page")
|
||||
)
|
||||
data["results"] = paginated
|
||||
data["remote"] = search_remote
|
||||
|
||||
return TemplateResponse(request, f"search/{search_type}.html", data)
|
||||
return endpoints[search_type](request)
|
||||
|
||||
|
||||
def book_search(query, user, min_confidence, search_remote=False):
|
||||
def api_book_search(request):
|
||||
"""Return books via API response"""
|
||||
query = request.GET.get("q")
|
||||
query = isbn_check(query)
|
||||
min_confidence = request.GET.get("min_confidence", 0)
|
||||
# only return local book results via json so we don't cascade
|
||||
book_results = search(query, min_confidence=min_confidence)
|
||||
return JsonResponse(
|
||||
[format_search_result(r) for r in book_results[:10]], safe=False
|
||||
)
|
||||
|
||||
|
||||
def book_search(request):
|
||||
"""the real business is elsewhere"""
|
||||
query = request.GET.get("q")
|
||||
# check if query is isbn
|
||||
query = isbn_check(query)
|
||||
min_confidence = request.GET.get("min_confidence", 0)
|
||||
search_remote = request.GET.get("remote", False) and request.user.is_authenticated
|
||||
|
||||
# try a local-only search
|
||||
results = [{"results": search(query, min_confidence=min_confidence)}]
|
||||
if not user.is_authenticated or (results[0]["results"] and not search_remote):
|
||||
return results, False
|
||||
|
||||
# if there were no local results, or the request was for remote, search all sources
|
||||
results += connector_manager.search(query, min_confidence=min_confidence)
|
||||
return results, True
|
||||
local_results = search(query, min_confidence=min_confidence)
|
||||
paginated = Paginator(local_results, PAGE_LENGTH)
|
||||
page = paginated.get_page(request.GET.get("page"))
|
||||
data = {
|
||||
"query": query,
|
||||
"results": page,
|
||||
"type": "book",
|
||||
"remote": search_remote,
|
||||
"page_range": paginated.get_elided_page_range(
|
||||
page.number, on_each_side=2, on_ends=1
|
||||
),
|
||||
}
|
||||
# if a logged in user requested remote results or got no local results, try remote
|
||||
if request.user.is_authenticated and (not local_results or search_remote):
|
||||
data["remote_results"] = connector_manager.search(
|
||||
query, min_confidence=min_confidence
|
||||
)
|
||||
data["remote"] = True
|
||||
return TemplateResponse(request, "search/book.html", data)
|
||||
|
||||
|
||||
def user_search(query, viewer, *_):
|
||||
def user_search(request):
|
||||
"""cool kids members only user search"""
|
||||
viewer = request.user
|
||||
query = request.GET.get("q")
|
||||
query = query.strip()
|
||||
data = {"type": "user", "query": query}
|
||||
# logged out viewers can't search users
|
||||
if not viewer.is_authenticated:
|
||||
return models.User.objects.none(), None
|
||||
return TemplateResponse(request, "search/user.html", data)
|
||||
|
||||
# use webfinger for mastodon style account@domain.com username to load the user if
|
||||
# they don't exist locally (handle_remote_webfinger will check the db)
|
||||
if re.match(regex.FULL_USERNAME, query):
|
||||
handle_remote_webfinger(query)
|
||||
|
||||
return (
|
||||
results = (
|
||||
models.User.viewer_aware_objects(viewer)
|
||||
.annotate(
|
||||
similarity=Greatest(
|
||||
|
@ -104,14 +114,23 @@ def user_search(query, viewer, *_):
|
|||
similarity__gt=0.5,
|
||||
)
|
||||
.order_by("-similarity")
|
||||
), None
|
||||
)
|
||||
paginated = Paginator(results, PAGE_LENGTH)
|
||||
page = paginated.get_page(request.GET.get("page"))
|
||||
data["results"] = page
|
||||
data["page_range"] = paginated.get_elided_page_range(
|
||||
page.number, on_each_side=2, on_ends=1
|
||||
)
|
||||
return TemplateResponse(request, "search/user.html", data)
|
||||
|
||||
|
||||
def list_search(query, viewer, *_):
|
||||
def list_search(request):
|
||||
"""any relevent lists?"""
|
||||
return (
|
||||
query = request.GET.get("q")
|
||||
data = {"query": query, "type": "list"}
|
||||
results = (
|
||||
models.List.privacy_filter(
|
||||
viewer,
|
||||
request.user,
|
||||
privacy_levels=["public", "followers"],
|
||||
)
|
||||
.annotate(
|
||||
|
@ -124,7 +143,14 @@ def list_search(query, viewer, *_):
|
|||
similarity__gt=0.1,
|
||||
)
|
||||
.order_by("-similarity")
|
||||
), None
|
||||
)
|
||||
paginated = Paginator(results, PAGE_LENGTH)
|
||||
page = paginated.get_page(request.GET.get("page"))
|
||||
data["results"] = page
|
||||
data["page_range"] = paginated.get_elided_page_range(
|
||||
page.number, on_each_side=2, on_ends=1
|
||||
)
|
||||
return TemplateResponse(request, "search/list.html", data)
|
||||
|
||||
|
||||
def isbn_check(query):
|
||||
|
|
|
@ -6,6 +6,7 @@ from urllib.parse import urlparse
|
|||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.validators import URLValidator
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Q
|
||||
from django.http import HttpResponse, HttpResponseBadRequest, Http404
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
|
@ -16,7 +17,6 @@ from django.views.decorators.http import require_POST
|
|||
|
||||
from markdown import markdown
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm.settings import DOMAIN
|
||||
from bookwyrm.utils import regex, sanitizer
|
||||
from .helpers import handle_remote_webfinger, is_api_request
|
||||
from .helpers import load_date_in_user_tz_as_utc
|
||||
|
@ -93,14 +93,16 @@ class CreateStatus(View):
|
|||
|
||||
# inspect the text for user tags
|
||||
content = status.content
|
||||
for (mention_text, mention_user) in find_mentions(content):
|
||||
for (mention_text, mention_user) in find_mentions(
|
||||
request.user, content
|
||||
).items():
|
||||
# add them to status mentions fk
|
||||
status.mention_users.add(mention_user)
|
||||
|
||||
# turn the mention into a link
|
||||
content = re.sub(
|
||||
rf"{mention_text}([^@]|$)",
|
||||
rf'<a href="{mention_user.remote_id}">{mention_text}</a>\g<1>',
|
||||
rf"{mention_text}\b(?!@)",
|
||||
rf'<a href="{mention_user.remote_id}">{mention_text}</a>',
|
||||
content,
|
||||
)
|
||||
# add reply parent to mentions
|
||||
|
@ -195,22 +197,35 @@ def edit_readthrough(request):
|
|||
return redirect("/")
|
||||
|
||||
|
||||
def find_mentions(content):
|
||||
def find_mentions(user, content):
|
||||
"""detect @mentions in raw status content"""
|
||||
if not content:
|
||||
return
|
||||
for match in re.finditer(regex.STRICT_USERNAME, content):
|
||||
username = match.group().strip().split("@")[1:]
|
||||
if len(username) == 1:
|
||||
# this looks like a local user (@user), fill in the domain
|
||||
username.append(DOMAIN)
|
||||
username = "@".join(username)
|
||||
return {}
|
||||
# The regex has nested match groups, so the 0th entry has the full (outer) match
|
||||
# And beacuse the strict username starts with @, the username is 1st char onward
|
||||
usernames = [m[0][1:] for m in re.findall(regex.STRICT_USERNAME, content)]
|
||||
|
||||
mention_user = handle_remote_webfinger(username)
|
||||
known_users = (
|
||||
models.User.viewer_aware_objects(user)
|
||||
.filter(Q(username__in=usernames) | Q(localname__in=usernames))
|
||||
.distinct()
|
||||
)
|
||||
# Prepare a lookup based on both username and localname
|
||||
username_dict = {
|
||||
**{f"@{u.username}": u for u in known_users},
|
||||
**{f"@{u.localname}": u for u in known_users.filter(local=True)},
|
||||
}
|
||||
|
||||
# Users not captured here could be blocked or not yet loaded on the server
|
||||
not_found = set(usernames) - set(username_dict.keys())
|
||||
for username in not_found:
|
||||
mention_user = handle_remote_webfinger(username, unknown_only=True)
|
||||
if not mention_user:
|
||||
# we can ignore users we don't know about
|
||||
# this user is blocked or can't be found
|
||||
continue
|
||||
yield (match.group(), mention_user)
|
||||
username_dict[f"@{mention_user.username}"] = mention_user
|
||||
username_dict[f"@{mention_user.localname}"] = mention_user
|
||||
return username_dict
|
||||
|
||||
|
||||
def format_links(content):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue