mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-11-25 11:01:12 +00:00
commit
d90e8e56d5
58 changed files with 1202 additions and 358 deletions
|
@ -400,11 +400,11 @@ def get_representative():
|
||||||
to sign outgoing HTTP GET requests"""
|
to sign outgoing HTTP GET requests"""
|
||||||
return models.User.objects.get_or_create(
|
return models.User.objects.get_or_create(
|
||||||
username=f"{INSTANCE_ACTOR_USERNAME}@{DOMAIN}",
|
username=f"{INSTANCE_ACTOR_USERNAME}@{DOMAIN}",
|
||||||
defaults=dict(
|
defaults={
|
||||||
email="bookwyrm@localhost",
|
"email": "bookwyrm@localhost",
|
||||||
local=True,
|
"local": True,
|
||||||
localname=INSTANCE_ACTOR_USERNAME,
|
"localname": INSTANCE_ACTOR_USERNAME,
|
||||||
),
|
},
|
||||||
)[0]
|
)[0]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -145,7 +145,9 @@ def load_more_data(connector_id: str, book_id: str) -> None:
|
||||||
"""background the work of getting all 10,000 editions of LoTR"""
|
"""background the work of getting all 10,000 editions of LoTR"""
|
||||||
connector_info = models.Connector.objects.get(id=connector_id)
|
connector_info = models.Connector.objects.get(id=connector_id)
|
||||||
connector = load_connector(connector_info)
|
connector = load_connector(connector_info)
|
||||||
book = models.Book.objects.select_subclasses().get(id=book_id)
|
book = models.Book.objects.select_subclasses().get( # type: ignore[no-untyped-call]
|
||||||
|
id=book_id
|
||||||
|
)
|
||||||
connector.expand_book_data(book)
|
connector.expand_book_data(book)
|
||||||
|
|
||||||
|
|
||||||
|
@ -156,7 +158,9 @@ def create_edition_task(
|
||||||
"""separate task for each of the 10,000 editions of LoTR"""
|
"""separate task for each of the 10,000 editions of LoTR"""
|
||||||
connector_info = models.Connector.objects.get(id=connector_id)
|
connector_info = models.Connector.objects.get(id=connector_id)
|
||||||
connector = load_connector(connector_info)
|
connector = load_connector(connector_info)
|
||||||
work = models.Work.objects.select_subclasses().get(id=work_id)
|
work = models.Work.objects.select_subclasses().get( # type: ignore[no-untyped-call]
|
||||||
|
id=work_id
|
||||||
|
)
|
||||||
connector.create_edition_from_data(work, data)
|
connector.create_edition_from_data(work, data)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -229,7 +229,7 @@ class Connector(AbstractConnector):
|
||||||
data = get_data(url)
|
data = get_data(url)
|
||||||
except ConnectorException:
|
except ConnectorException:
|
||||||
return ""
|
return ""
|
||||||
return data.get("extract", "")
|
return str(data.get("extract", ""))
|
||||||
|
|
||||||
def get_remote_id_from_model(self, obj: models.BookDataModel) -> str:
|
def get_remote_id_from_model(self, obj: models.BookDataModel) -> str:
|
||||||
"""use get_remote_id to figure out the link from a model obj"""
|
"""use get_remote_id to figure out the link from a model obj"""
|
||||||
|
|
|
@ -14,15 +14,10 @@ class CalibreImporter(Importer):
|
||||||
def __init__(self, *args: Any, **kwargs: Any):
|
def __init__(self, *args: Any, **kwargs: Any):
|
||||||
# Add timestamp to row_mappings_guesses for date_added to avoid
|
# Add timestamp to row_mappings_guesses for date_added to avoid
|
||||||
# integrity error
|
# integrity error
|
||||||
row_mappings_guesses = []
|
self.row_mappings_guesses = [
|
||||||
|
(field, mapping + (["timestamp"] if field == "date_added" else []))
|
||||||
for field, mapping in self.row_mappings_guesses:
|
for field, mapping in self.row_mappings_guesses
|
||||||
if field in ("date_added",):
|
]
|
||||||
row_mappings_guesses.append((field, mapping + ["timestamp"]))
|
|
||||||
else:
|
|
||||||
row_mappings_guesses.append((field, mapping))
|
|
||||||
|
|
||||||
self.row_mappings_guesses = row_mappings_guesses
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def get_shelf(self, normalized_row: dict[str, Optional[str]]) -> Optional[str]:
|
def get_shelf(self, normalized_row: dict[str, Optional[str]]) -> Optional[str]:
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
""" Makes the app aware of the users timezone """
|
""" Makes the app aware of the users timezone """
|
||||||
import pytz
|
import zoneinfo
|
||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
@ -12,9 +12,7 @@ class TimezoneMiddleware:
|
||||||
|
|
||||||
def __call__(self, request):
|
def __call__(self, request):
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
timezone.activate(pytz.timezone(request.user.preferred_timezone))
|
timezone.activate(zoneinfo.ZoneInfo(request.user.preferred_timezone))
|
||||||
else:
|
else:
|
||||||
timezone.activate(pytz.utc)
|
timezone.deactivate()
|
||||||
response = self.get_response(request)
|
return self.get_response(request)
|
||||||
timezone.deactivate()
|
|
||||||
return response
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ class Migration(migrations.Migration):
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
# The new timezones are "Factory" and "localtime"
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="user",
|
model_name="user",
|
||||||
name="preferred_timezone",
|
name="preferred_timezone",
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
# Generated by Django 3.2.23 on 2024-01-28 02:49
|
# Generated by Django 3.2.23 on 2024-01-28 02:49
|
||||||
|
|
||||||
import bookwyrm.storage_backends
|
|
||||||
import django.core.serializers.json
|
import django.core.serializers.json
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
from django.core.files.storage import storages
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
@ -30,7 +30,7 @@ class Migration(migrations.Migration):
|
||||||
name="export_data",
|
name="export_data",
|
||||||
field=models.FileField(
|
field=models.FileField(
|
||||||
null=True,
|
null=True,
|
||||||
storage=bookwyrm.storage_backends.ExportsFileStorage,
|
storage=storages["exports"],
|
||||||
upload_to="",
|
upload_to="",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
# Generated by Django 4.2.11 on 2024-03-29 19:25
|
||||||
|
|
||||||
|
import bookwyrm.models.fields
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0198_book_search_vector_author_aliases"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="userblocks",
|
||||||
|
name="user_object",
|
||||||
|
field=bookwyrm.models.fields.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="%(class)s_user_object",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="userblocks",
|
||||||
|
name="user_subject",
|
||||||
|
field=bookwyrm.models.fields.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="%(class)s_user_subject",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="userfollowrequest",
|
||||||
|
name="user_object",
|
||||||
|
field=bookwyrm.models.fields.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="%(class)s_user_object",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="userfollowrequest",
|
||||||
|
name="user_subject",
|
||||||
|
field=bookwyrm.models.fields.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="%(class)s_user_subject",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="userfollows",
|
||||||
|
name="user_object",
|
||||||
|
field=bookwyrm.models.fields.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="%(class)s_user_object",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="userfollows",
|
||||||
|
name="user_subject",
|
||||||
|
field=bookwyrm.models.fields.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="%(class)s_user_subject",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
633
bookwyrm/migrations/0200_alter_user_preferred_timezone.py
Normal file
633
bookwyrm/migrations/0200_alter_user_preferred_timezone.py
Normal file
|
@ -0,0 +1,633 @@
|
||||||
|
# Generated by Django 4.2.11 on 2024-04-01 20:19
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0199_alter_userblocks_user_object_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
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/Ciudad_Juarez", "America/Ciudad_Juarez"),
|
||||||
|
("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"),
|
||||||
|
("Factory", "Factory"),
|
||||||
|
("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"),
|
||||||
|
("localtime", "localtime"),
|
||||||
|
],
|
||||||
|
default="UTC",
|
||||||
|
max_length=255,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,39 @@
|
||||||
|
# Generated by Django 4.2.11 on 2024-04-01 21:09
|
||||||
|
|
||||||
|
import bookwyrm.models.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.contrib.postgres.operations import CreateCollation
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0200_alter_user_preferred_timezone"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
CreateCollation(
|
||||||
|
"case_insensitive",
|
||||||
|
provider="icu",
|
||||||
|
locale="und-u-ks-level2",
|
||||||
|
deterministic=False,
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="hashtag",
|
||||||
|
name="name",
|
||||||
|
field=bookwyrm.models.fields.CharField(
|
||||||
|
db_collation="case_insensitive", max_length=256
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="user",
|
||||||
|
name="localname",
|
||||||
|
field=models.CharField(
|
||||||
|
db_collation="case_insensitive",
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
unique=True,
|
||||||
|
validators=[bookwyrm.models.fields.validate_localname],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
13
bookwyrm/migrations/0205_merge_20240410_2022.py
Normal file
13
bookwyrm/migrations/0205_merge_20240410_2022.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# Generated by Django 4.2.11 on 2024-04-10 20:22
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0201_alter_hashtag_name_alter_user_localname"),
|
||||||
|
("bookwyrm", "0204_merge_20240409_1042"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = []
|
13
bookwyrm/migrations/0206_merge_20240415_1537.py
Normal file
13
bookwyrm/migrations/0206_merge_20240415_1537.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# Generated by Django 4.2.11 on 2024-04-15 15:37
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookwyrm", "0205_merge_20240410_2022"),
|
||||||
|
("bookwyrm", "0205_merge_20240413_0232"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = []
|
|
@ -169,7 +169,7 @@ class ActivitypubMixin:
|
||||||
# filter users first by whether they're using the desired software
|
# filter users first by whether they're using the desired software
|
||||||
# this lets us send book updates only to other bw servers
|
# this lets us send book updates only to other bw servers
|
||||||
if software:
|
if software:
|
||||||
queryset = queryset.filter(bookwyrm_user=(software == "bookwyrm"))
|
queryset = queryset.filter(bookwyrm_user=software == "bookwyrm")
|
||||||
# if there's a user, we only want to send to the user's followers
|
# if there's a user, we only want to send to the user's followers
|
||||||
if user:
|
if user:
|
||||||
queryset = queryset.filter(following=user)
|
queryset = queryset.filter(following=user)
|
||||||
|
@ -206,14 +206,10 @@ class ObjectMixin(ActivitypubMixin):
|
||||||
created: Optional[bool] = None,
|
created: Optional[bool] = None,
|
||||||
software: Any = None,
|
software: Any = None,
|
||||||
priority: str = BROADCAST,
|
priority: str = BROADCAST,
|
||||||
|
broadcast: bool = True,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""broadcast created/updated/deleted objects as appropriate"""
|
"""broadcast created/updated/deleted objects as appropriate"""
|
||||||
broadcast = kwargs.get("broadcast", True)
|
|
||||||
# this bonus kwarg would cause an error in the base save method
|
|
||||||
if "broadcast" in kwargs:
|
|
||||||
del kwargs["broadcast"]
|
|
||||||
|
|
||||||
created = created or not bool(self.id)
|
created = created or not bool(self.id)
|
||||||
# first off, we want to save normally no matter what
|
# first off, we want to save normally no matter what
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
""" database schema for info about authors """
|
""" database schema for info about authors """
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from typing import Tuple, Any
|
from typing import Any
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.contrib.postgres.indexes import GinIndex
|
from django.contrib.postgres.indexes import GinIndex
|
||||||
|
@ -45,12 +45,12 @@ class Author(BookDataModel):
|
||||||
)
|
)
|
||||||
bio = fields.HtmlField(null=True, blank=True)
|
bio = fields.HtmlField(null=True, blank=True)
|
||||||
|
|
||||||
def save(self, *args: Tuple[Any, ...], **kwargs: dict[str, Any]) -> None:
|
def save(self, *args: Any, **kwargs: Any) -> None:
|
||||||
"""normalize isni format"""
|
"""normalize isni format"""
|
||||||
if self.isni:
|
if self.isni is not None:
|
||||||
self.isni = re.sub(r"\s", "", self.isni)
|
self.isni = re.sub(r"\s", "", self.isni)
|
||||||
|
|
||||||
return super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def isni_link(self):
|
def isni_link(self):
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
import re
|
import re
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict, Optional, Iterable
|
||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
|
|
||||||
from django.contrib.postgres.search import SearchVectorField
|
from django.contrib.postgres.search import SearchVectorField
|
||||||
|
@ -27,7 +27,7 @@ from bookwyrm.settings import (
|
||||||
ENABLE_PREVIEW_IMAGES,
|
ENABLE_PREVIEW_IMAGES,
|
||||||
ENABLE_THUMBNAIL_GENERATION,
|
ENABLE_THUMBNAIL_GENERATION,
|
||||||
)
|
)
|
||||||
from bookwyrm.utils.db import format_trigger
|
from bookwyrm.utils.db import format_trigger, add_update_fields
|
||||||
|
|
||||||
from .activitypub_mixin import OrderedCollectionPageMixin, ObjectMixin
|
from .activitypub_mixin import OrderedCollectionPageMixin, ObjectMixin
|
||||||
from .base_model import BookWyrmModel
|
from .base_model import BookWyrmModel
|
||||||
|
@ -96,14 +96,19 @@ class BookDataModel(ObjectMixin, BookWyrmModel):
|
||||||
|
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
def save(self, *args: Any, **kwargs: Any) -> None:
|
def save(
|
||||||
|
self, *args: Any, update_fields: Optional[Iterable[str]] = None, **kwargs: Any
|
||||||
|
) -> None:
|
||||||
"""ensure that the remote_id is within this instance"""
|
"""ensure that the remote_id is within this instance"""
|
||||||
if self.id:
|
if self.id:
|
||||||
self.remote_id = self.get_remote_id()
|
self.remote_id = self.get_remote_id()
|
||||||
|
update_fields = add_update_fields(update_fields, "remote_id")
|
||||||
else:
|
else:
|
||||||
self.origin_id = self.remote_id
|
self.origin_id = self.remote_id
|
||||||
self.remote_id = None
|
self.remote_id = None
|
||||||
return super().save(*args, **kwargs)
|
update_fields = add_update_fields(update_fields, "origin_id", "remote_id")
|
||||||
|
|
||||||
|
super().save(*args, update_fields=update_fields, **kwargs)
|
||||||
|
|
||||||
# pylint: disable=arguments-differ
|
# pylint: disable=arguments-differ
|
||||||
def broadcast(self, activity, sender, software="bookwyrm", **kwargs):
|
def broadcast(self, activity, sender, software="bookwyrm", **kwargs):
|
||||||
|
@ -323,7 +328,7 @@ class Book(BookDataModel):
|
||||||
if not isinstance(self, (Edition, Work)):
|
if not isinstance(self, (Edition, Work)):
|
||||||
raise ValueError("Books should be added as Editions or Works")
|
raise ValueError("Books should be added as Editions or Works")
|
||||||
|
|
||||||
return super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def get_remote_id(self):
|
def get_remote_id(self):
|
||||||
"""editions and works both use "book" instead of model_name"""
|
"""editions and works both use "book" instead of model_name"""
|
||||||
|
@ -400,10 +405,11 @@ class Work(OrderedCollectionPageMixin, Book):
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""set some fields on the edition object"""
|
"""set some fields on the edition object"""
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
# set rank
|
# set rank
|
||||||
for edition in self.editions.all():
|
for edition in self.editions.all():
|
||||||
edition.save()
|
edition.save()
|
||||||
return super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def default_edition(self):
|
def default_edition(self):
|
||||||
|
@ -509,33 +515,48 @@ class Edition(Book):
|
||||||
# max rank is 9
|
# max rank is 9
|
||||||
return rank
|
return rank
|
||||||
|
|
||||||
def save(self, *args: Any, **kwargs: Any) -> None:
|
def save(
|
||||||
|
self, *args: Any, update_fields: Optional[Iterable[str]] = None, **kwargs: Any
|
||||||
|
) -> None:
|
||||||
"""set some fields on the edition object"""
|
"""set some fields on the edition object"""
|
||||||
# calculate isbn 10/13
|
# calculate isbn 10/13
|
||||||
if self.isbn_13 and self.isbn_13[:3] == "978" and not self.isbn_10:
|
if (
|
||||||
|
self.isbn_10 is None
|
||||||
|
and self.isbn_13 is not None
|
||||||
|
and self.isbn_13[:3] == "978"
|
||||||
|
):
|
||||||
self.isbn_10 = isbn_13_to_10(self.isbn_13)
|
self.isbn_10 = isbn_13_to_10(self.isbn_13)
|
||||||
if self.isbn_10 and not self.isbn_13:
|
update_fields = add_update_fields(update_fields, "isbn_10")
|
||||||
|
if self.isbn_13 is None and self.isbn_10 is not None:
|
||||||
self.isbn_13 = isbn_10_to_13(self.isbn_10)
|
self.isbn_13 = isbn_10_to_13(self.isbn_10)
|
||||||
|
update_fields = add_update_fields(update_fields, "isbn_13")
|
||||||
|
|
||||||
# normalize isbn format
|
# normalize isbn format
|
||||||
if self.isbn_10:
|
if self.isbn_10 is not None:
|
||||||
self.isbn_10 = normalize_isbn(self.isbn_10)
|
self.isbn_10 = normalize_isbn(self.isbn_10)
|
||||||
if self.isbn_13:
|
if self.isbn_13 is not None:
|
||||||
self.isbn_13 = normalize_isbn(self.isbn_13)
|
self.isbn_13 = normalize_isbn(self.isbn_13)
|
||||||
|
|
||||||
# set rank
|
# set rank
|
||||||
self.edition_rank = self.get_rank()
|
if (new := self.get_rank()) != self.edition_rank:
|
||||||
|
self.edition_rank = new
|
||||||
# clear author cache
|
update_fields = add_update_fields(update_fields, "edition_rank")
|
||||||
if self.id:
|
|
||||||
for author_id in self.authors.values_list("id", flat=True):
|
|
||||||
cache.delete(f"author-books-{author_id}")
|
|
||||||
|
|
||||||
# Create sort title by removing articles from title
|
# Create sort title by removing articles from title
|
||||||
if self.sort_title in [None, ""]:
|
if self.sort_title in [None, ""]:
|
||||||
self.sort_title = self.guess_sort_title()
|
self.sort_title = self.guess_sort_title()
|
||||||
|
update_fields = add_update_fields(update_fields, "sort_title")
|
||||||
|
|
||||||
return super().save(*args, **kwargs)
|
super().save(*args, update_fields=update_fields, **kwargs)
|
||||||
|
|
||||||
|
# clear author cache
|
||||||
|
if self.id:
|
||||||
|
cache.delete_many(
|
||||||
|
[
|
||||||
|
f"author-books-{author_id}"
|
||||||
|
for author_id in self.authors.values_list("id", flat=True)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def repair(self):
|
def repair(self):
|
||||||
|
|
|
@ -10,9 +10,9 @@ from django.db.models import BooleanField, FileField, JSONField
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.utils.module_loading import import_string
|
from django.core.files.storage import storages
|
||||||
|
|
||||||
from bookwyrm import settings, storage_backends
|
from bookwyrm import settings
|
||||||
|
|
||||||
from bookwyrm.models import AnnualGoal, ReadThrough, ShelfBook, ListItem
|
from bookwyrm.models import AnnualGoal, ReadThrough, ShelfBook, ListItem
|
||||||
from bookwyrm.models import Review, Comment, Quotation
|
from bookwyrm.models import Review, Comment, Quotation
|
||||||
|
@ -35,8 +35,7 @@ class BookwyrmAwsSession(BotoSession):
|
||||||
|
|
||||||
def select_exports_storage():
|
def select_exports_storage():
|
||||||
"""callable to allow for dependency on runtime configuration"""
|
"""callable to allow for dependency on runtime configuration"""
|
||||||
cls = import_string(settings.EXPORTS_STORAGE)
|
return storages["exports"]
|
||||||
return cls()
|
|
||||||
|
|
||||||
|
|
||||||
class BookwyrmExportJob(ParentJob):
|
class BookwyrmExportJob(ParentJob):
|
||||||
|
@ -116,7 +115,7 @@ def create_archive_task(job_id):
|
||||||
|
|
||||||
if settings.USE_S3:
|
if settings.USE_S3:
|
||||||
# Storage for writing temporary files
|
# Storage for writing temporary files
|
||||||
exports_storage = storage_backends.ExportsS3Storage()
|
exports_storage = storages["exports"]
|
||||||
|
|
||||||
# Handle for creating the final archive
|
# Handle for creating the final archive
|
||||||
s3_tar = S3Tar(
|
s3_tar = S3Tar(
|
||||||
|
@ -136,7 +135,7 @@ def create_archive_task(job_id):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add images to TAR
|
# Add images to TAR
|
||||||
images_storage = storage_backends.ImagesStorage()
|
images_storage = storages["default"]
|
||||||
|
|
||||||
if user.avatar:
|
if user.avatar:
|
||||||
add_file_to_s3_tar(s3_tar, images_storage, user.avatar)
|
add_file_to_s3_tar(s3_tar, images_storage, user.avatar)
|
||||||
|
|
|
@ -2,18 +2,19 @@
|
||||||
from bookwyrm import activitypub
|
from bookwyrm import activitypub
|
||||||
from .activitypub_mixin import ActivitypubMixin
|
from .activitypub_mixin import ActivitypubMixin
|
||||||
from .base_model import BookWyrmModel
|
from .base_model import BookWyrmModel
|
||||||
from .fields import CICharField
|
from .fields import CharField
|
||||||
|
|
||||||
|
|
||||||
class Hashtag(ActivitypubMixin, BookWyrmModel):
|
class Hashtag(ActivitypubMixin, BookWyrmModel):
|
||||||
"a hashtag which can be used in statuses"
|
"a hashtag which can be used in statuses"
|
||||||
|
|
||||||
name = CICharField(
|
name = CharField(
|
||||||
max_length=256,
|
max_length=256,
|
||||||
blank=False,
|
blank=False,
|
||||||
null=False,
|
null=False,
|
||||||
activitypub_field="name",
|
activitypub_field="name",
|
||||||
deduplication_field=True,
|
deduplication_field=True,
|
||||||
|
db_collation="case_insensitive",
|
||||||
)
|
)
|
||||||
|
|
||||||
name_field = "name"
|
name_field = "name"
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
""" outlink data """
|
""" outlink data """
|
||||||
|
from typing import Optional, Iterable
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
|
@ -6,6 +7,7 @@ from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from bookwyrm import activitypub
|
from bookwyrm import activitypub
|
||||||
|
from bookwyrm.utils.db import add_update_fields
|
||||||
from .activitypub_mixin import ActivitypubMixin
|
from .activitypub_mixin import ActivitypubMixin
|
||||||
from .base_model import BookWyrmModel
|
from .base_model import BookWyrmModel
|
||||||
from . import fields
|
from . import fields
|
||||||
|
@ -34,17 +36,19 @@ class Link(ActivitypubMixin, BookWyrmModel):
|
||||||
"""link name via the associated domain"""
|
"""link name via the associated domain"""
|
||||||
return self.domain.name
|
return self.domain.name
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, update_fields: Optional[Iterable[str]] = None, **kwargs):
|
||||||
"""create a link"""
|
"""create a link"""
|
||||||
# get or create the associated domain
|
# get or create the associated domain
|
||||||
if not self.domain:
|
if not self.domain:
|
||||||
domain = urlparse(self.url).hostname
|
domain = urlparse(self.url).hostname
|
||||||
self.domain, _ = LinkDomain.objects.get_or_create(domain=domain)
|
self.domain, _ = LinkDomain.objects.get_or_create(domain=domain)
|
||||||
|
update_fields = add_update_fields(update_fields, "domain")
|
||||||
|
|
||||||
# this is never broadcast, the owning model broadcasts an update
|
# this is never broadcast, the owning model broadcasts an update
|
||||||
if "broadcast" in kwargs:
|
if "broadcast" in kwargs:
|
||||||
del kwargs["broadcast"]
|
del kwargs["broadcast"]
|
||||||
return super().save(*args, **kwargs)
|
|
||||||
|
super().save(*args, update_fields=update_fields, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
AvailabilityChoices = [
|
AvailabilityChoices = [
|
||||||
|
@ -88,8 +92,10 @@ class LinkDomain(BookWyrmModel):
|
||||||
return
|
return
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, update_fields: Optional[Iterable[str]] = None, **kwargs):
|
||||||
"""set a default name"""
|
"""set a default name"""
|
||||||
if not self.name:
|
if not self.name:
|
||||||
self.name = self.domain
|
self.name = self.domain
|
||||||
super().save(*args, **kwargs)
|
update_fields = add_update_fields(update_fields, "name")
|
||||||
|
|
||||||
|
super().save(*args, update_fields=update_fields, **kwargs)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
""" make a list of books!! """
|
""" make a list of books!! """
|
||||||
|
from typing import Optional, Iterable
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
|
@ -8,6 +9,7 @@ from django.utils import timezone
|
||||||
|
|
||||||
from bookwyrm import activitypub
|
from bookwyrm import activitypub
|
||||||
from bookwyrm.settings import BASE_URL
|
from bookwyrm.settings import BASE_URL
|
||||||
|
from bookwyrm.utils.db import add_update_fields
|
||||||
|
|
||||||
from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin
|
from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin
|
||||||
from .base_model import BookWyrmModel
|
from .base_model import BookWyrmModel
|
||||||
|
@ -124,11 +126,13 @@ class List(OrderedCollectionMixin, BookWyrmModel):
|
||||||
group=None, curation="closed"
|
group=None, curation="closed"
|
||||||
)
|
)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, update_fields: Optional[Iterable[str]] = None, **kwargs):
|
||||||
"""on save, update embed_key and avoid clash with existing code"""
|
"""on save, update embed_key and avoid clash with existing code"""
|
||||||
if not self.embed_key:
|
if not self.embed_key:
|
||||||
self.embed_key = uuid.uuid4()
|
self.embed_key = uuid.uuid4()
|
||||||
super().save(*args, **kwargs)
|
update_fields = add_update_fields(update_fields, "embed_key")
|
||||||
|
|
||||||
|
super().save(*args, update_fields=update_fields, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class ListItem(CollectionItemMixin, BookWyrmModel):
|
class ListItem(CollectionItemMixin, BookWyrmModel):
|
||||||
|
|
|
@ -48,24 +48,21 @@ class MoveUser(Move):
|
||||||
"""update user info and broadcast it"""
|
"""update user info and broadcast it"""
|
||||||
|
|
||||||
# only allow if the source is listed in the target's alsoKnownAs
|
# only allow if the source is listed in the target's alsoKnownAs
|
||||||
if self.user in self.target.also_known_as.all():
|
if self.user not in self.target.also_known_as.all():
|
||||||
self.user.also_known_as.add(self.target.id)
|
|
||||||
self.user.update_active_date()
|
|
||||||
self.user.moved_to = self.target.remote_id
|
|
||||||
self.user.save(update_fields=["moved_to"])
|
|
||||||
|
|
||||||
if self.user.local:
|
|
||||||
kwargs[
|
|
||||||
"broadcast"
|
|
||||||
] = True # Only broadcast if we are initiating the Move
|
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
for follower in self.user.followers.all():
|
|
||||||
if follower.local:
|
|
||||||
Notification.notify(
|
|
||||||
follower, self.user, notification_type=NotificationType.MOVE
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
|
|
||||||
|
self.user.also_known_as.add(self.target.id)
|
||||||
|
self.user.update_active_date()
|
||||||
|
self.user.moved_to = self.target.remote_id
|
||||||
|
self.user.save(update_fields=["moved_to"])
|
||||||
|
|
||||||
|
if self.user.local:
|
||||||
|
kwargs["broadcast"] = True # Only broadcast if we are initiating the Move
|
||||||
|
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
for follower in self.user.followers.all():
|
||||||
|
if follower.local:
|
||||||
|
Notification.notify(
|
||||||
|
follower, self.user, notification_type=NotificationType.MOVE
|
||||||
|
)
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
""" progress in a book """
|
""" progress in a book """
|
||||||
|
from typing import Optional, Iterable
|
||||||
|
|
||||||
from django.core import validators
|
from django.core import validators
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import F, Q
|
from django.db.models import F, Q
|
||||||
|
|
||||||
|
from bookwyrm.utils.db import add_update_fields
|
||||||
|
|
||||||
from .base_model import BookWyrmModel
|
from .base_model import BookWyrmModel
|
||||||
|
|
||||||
|
|
||||||
|
@ -30,14 +34,17 @@ class ReadThrough(BookWyrmModel):
|
||||||
stopped_date = models.DateTimeField(blank=True, null=True)
|
stopped_date = models.DateTimeField(blank=True, null=True)
|
||||||
is_active = models.BooleanField(default=True)
|
is_active = models.BooleanField(default=True)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, update_fields: Optional[Iterable[str]] = None, **kwargs):
|
||||||
"""update user active time"""
|
"""update user active time"""
|
||||||
cache.delete(f"latest_read_through-{self.user_id}-{self.book_id}")
|
|
||||||
self.user.update_active_date()
|
|
||||||
# an active readthrough must have an unset finish date
|
# an active readthrough must have an unset finish date
|
||||||
if self.finish_date or self.stopped_date:
|
if self.finish_date or self.stopped_date:
|
||||||
self.is_active = False
|
self.is_active = False
|
||||||
super().save(*args, **kwargs)
|
update_fields = add_update_fields(update_fields, "is_active")
|
||||||
|
|
||||||
|
super().save(*args, update_fields=update_fields, **kwargs)
|
||||||
|
|
||||||
|
cache.delete(f"latest_read_through-{self.user_id}-{self.book_id}")
|
||||||
|
self.user.update_active_date()
|
||||||
|
|
||||||
def create_update(self):
|
def create_update(self):
|
||||||
"""add update to the readthrough"""
|
"""add update to the readthrough"""
|
||||||
|
|
|
@ -38,14 +38,16 @@ class UserRelationship(BookWyrmModel):
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""clear the template cache"""
|
"""clear the template cache"""
|
||||||
clear_cache(self.user_subject, self.user_object)
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
clear_cache(self.user_subject, self.user_object)
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
"""clear the template cache"""
|
"""clear the template cache"""
|
||||||
clear_cache(self.user_subject, self.user_object)
|
|
||||||
super().delete(*args, **kwargs)
|
super().delete(*args, **kwargs)
|
||||||
|
|
||||||
|
clear_cache(self.user_subject, self.user_object)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""relationships should be unique"""
|
"""relationships should be unique"""
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
""" puttin' books on shelves """
|
""" puttin' books on shelves """
|
||||||
import re
|
import re
|
||||||
|
from typing import Optional, Iterable
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
@ -8,6 +9,7 @@ from django.utils import timezone
|
||||||
from bookwyrm import activitypub
|
from bookwyrm import activitypub
|
||||||
from bookwyrm.settings import BASE_URL
|
from bookwyrm.settings import BASE_URL
|
||||||
from bookwyrm.tasks import BROADCAST
|
from bookwyrm.tasks import BROADCAST
|
||||||
|
from bookwyrm.utils.db import add_update_fields
|
||||||
from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin
|
from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin
|
||||||
from .base_model import BookWyrmModel
|
from .base_model import BookWyrmModel
|
||||||
from . import fields
|
from . import fields
|
||||||
|
@ -44,8 +46,9 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
|
||||||
"""set the identifier"""
|
"""set the identifier"""
|
||||||
super().save(*args, priority=priority, **kwargs)
|
super().save(*args, priority=priority, **kwargs)
|
||||||
if not self.identifier:
|
if not self.identifier:
|
||||||
|
# this needs the auto increment ID from the save() above
|
||||||
self.identifier = self.get_identifier()
|
self.identifier = self.get_identifier()
|
||||||
super().save(*args, **kwargs, broadcast=False)
|
super().save(*args, **kwargs, broadcast=False, update_fields={"identifier"})
|
||||||
|
|
||||||
def get_identifier(self):
|
def get_identifier(self):
|
||||||
"""custom-shelf-123 for the url"""
|
"""custom-shelf-123 for the url"""
|
||||||
|
@ -100,10 +103,21 @@ class ShelfBook(CollectionItemMixin, BookWyrmModel):
|
||||||
activity_serializer = activitypub.ShelfItem
|
activity_serializer = activitypub.ShelfItem
|
||||||
collection_field = "shelf"
|
collection_field = "shelf"
|
||||||
|
|
||||||
def save(self, *args, priority=BROADCAST, **kwargs):
|
def save(
|
||||||
|
self,
|
||||||
|
*args,
|
||||||
|
priority=BROADCAST,
|
||||||
|
update_fields: Optional[Iterable[str]] = None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
if not self.user:
|
if not self.user:
|
||||||
self.user = self.shelf.user
|
self.user = self.shelf.user
|
||||||
if self.id and self.user.local:
|
update_fields = add_update_fields(update_fields, "user")
|
||||||
|
|
||||||
|
is_update = self.id is not None
|
||||||
|
super().save(*args, priority=priority, update_fields=update_fields, **kwargs)
|
||||||
|
|
||||||
|
if is_update and self.user.local:
|
||||||
# remove all caches related to all editions of this book
|
# remove all caches related to all editions of this book
|
||||||
cache.delete_many(
|
cache.delete_many(
|
||||||
[
|
[
|
||||||
|
@ -111,7 +125,6 @@ class ShelfBook(CollectionItemMixin, BookWyrmModel):
|
||||||
for book in self.book.parent_work.editions.all()
|
for book in self.book.parent_work.editions.all()
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
super().save(*args, priority=priority, **kwargs)
|
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
if self.id and self.user.local:
|
if self.id and self.user.local:
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
""" the particulars for this instance of BookWyrm """
|
""" the particulars for this instance of BookWyrm """
|
||||||
import datetime
|
import datetime
|
||||||
|
from typing import Optional, Iterable
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
@ -15,6 +16,7 @@ from bookwyrm.preview_images import generate_site_preview_image_task
|
||||||
from bookwyrm.settings import BASE_URL, ENABLE_PREVIEW_IMAGES, STATIC_FULL_URL
|
from bookwyrm.settings import BASE_URL, ENABLE_PREVIEW_IMAGES, STATIC_FULL_URL
|
||||||
from bookwyrm.settings import RELEASE_API
|
from bookwyrm.settings import RELEASE_API
|
||||||
from bookwyrm.tasks import app, MISC
|
from bookwyrm.tasks import app, MISC
|
||||||
|
from bookwyrm.utils.db import add_update_fields
|
||||||
from .base_model import BookWyrmModel, new_access_code
|
from .base_model import BookWyrmModel, new_access_code
|
||||||
from .user import User
|
from .user import User
|
||||||
from .fields import get_absolute_url
|
from .fields import get_absolute_url
|
||||||
|
@ -136,16 +138,19 @@ class SiteSettings(SiteModel):
|
||||||
return get_absolute_url(uploaded)
|
return get_absolute_url(uploaded)
|
||||||
return urljoin(STATIC_FULL_URL, default_path)
|
return urljoin(STATIC_FULL_URL, default_path)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, update_fields: Optional[Iterable[str]] = None, **kwargs):
|
||||||
"""if require_confirm_email is disabled, make sure no users are pending,
|
"""if require_confirm_email is disabled, make sure no users are pending,
|
||||||
if enabled, make sure invite_question_text is not empty"""
|
if enabled, make sure invite_question_text is not empty"""
|
||||||
|
if not self.invite_question_text:
|
||||||
|
self.invite_question_text = "What is your favourite book?"
|
||||||
|
update_fields = add_update_fields(update_fields, "invite_question_text")
|
||||||
|
|
||||||
|
super().save(*args, update_fields=update_fields, **kwargs)
|
||||||
|
|
||||||
if not self.require_confirm_email:
|
if not self.require_confirm_email:
|
||||||
User.objects.filter(is_active=False, deactivation_reason="pending").update(
|
User.objects.filter(is_active=False, deactivation_reason="pending").update(
|
||||||
is_active=True, deactivation_reason=None
|
is_active=True, deactivation_reason=None
|
||||||
)
|
)
|
||||||
if not self.invite_question_text:
|
|
||||||
self.invite_question_text = "What is your favourite book?"
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class Theme(SiteModel):
|
class Theme(SiteModel):
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
""" models for storing different kinds of Activities """
|
""" models for storing different kinds of Activities """
|
||||||
from dataclasses import MISSING
|
from dataclasses import MISSING
|
||||||
from typing import Optional
|
from typing import Optional, Iterable
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
@ -20,6 +20,7 @@ from model_utils.managers import InheritanceManager
|
||||||
from bookwyrm import activitypub
|
from bookwyrm import activitypub
|
||||||
from bookwyrm.preview_images import generate_edition_preview_image_task
|
from bookwyrm.preview_images import generate_edition_preview_image_task
|
||||||
from bookwyrm.settings import ENABLE_PREVIEW_IMAGES
|
from bookwyrm.settings import ENABLE_PREVIEW_IMAGES
|
||||||
|
from bookwyrm.utils.db import add_update_fields
|
||||||
from .activitypub_mixin import ActivitypubMixin, ActivityMixin
|
from .activitypub_mixin import ActivitypubMixin, ActivityMixin
|
||||||
from .activitypub_mixin import OrderedCollectionPageMixin
|
from .activitypub_mixin import OrderedCollectionPageMixin
|
||||||
from .base_model import BookWyrmModel
|
from .base_model import BookWyrmModel
|
||||||
|
@ -85,12 +86,13 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||||
models.Index(fields=["thread_id"]),
|
models.Index(fields=["thread_id"]),
|
||||||
]
|
]
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, update_fields: Optional[Iterable[str]] = None, **kwargs):
|
||||||
"""save and notify"""
|
"""save and notify"""
|
||||||
if self.reply_parent:
|
if self.thread_id is None and self.reply_parent:
|
||||||
self.thread_id = self.reply_parent.thread_id or self.reply_parent_id
|
self.thread_id = self.reply_parent.thread_id or self.reply_parent_id
|
||||||
|
update_fields = add_update_fields(update_fields, "thread_id")
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, update_fields=update_fields, **kwargs)
|
||||||
|
|
||||||
if not self.reply_parent:
|
if not self.reply_parent:
|
||||||
self.thread_id = self.id
|
self.thread_id = self.id
|
||||||
|
@ -459,9 +461,10 @@ class Review(BookStatus):
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""clear rating caches"""
|
"""clear rating caches"""
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
if self.book.parent_work:
|
if self.book.parent_work:
|
||||||
cache.delete(f"book-rating-{self.book.parent_work.id}")
|
cache.delete(f"book-rating-{self.book.parent_work.id}")
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class ReviewRating(Review):
|
class ReviewRating(Review):
|
||||||
|
|
|
@ -1,18 +1,20 @@
|
||||||
""" database schema for user data """
|
""" database schema for user data """
|
||||||
|
import datetime
|
||||||
import re
|
import re
|
||||||
|
import zoneinfo
|
||||||
|
from typing import Optional, Iterable
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
from django.contrib.postgres.fields import ArrayField, CICharField
|
from django.contrib.postgres.fields import ArrayField as DjangoArrayField
|
||||||
from django.core.exceptions import PermissionDenied, ObjectDoesNotExist
|
from django.core.exceptions import PermissionDenied, ObjectDoesNotExist
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.db import models, transaction, IntegrityError
|
from django.db import models, transaction, IntegrityError
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from model_utils import FieldTracker
|
from model_utils import FieldTracker
|
||||||
import pytz
|
|
||||||
|
|
||||||
from bookwyrm import activitypub
|
from bookwyrm import activitypub
|
||||||
from bookwyrm.connectors import get_data, ConnectorException
|
from bookwyrm.connectors import get_data, ConnectorException
|
||||||
|
@ -23,6 +25,7 @@ from bookwyrm.settings import BASE_URL, ENABLE_PREVIEW_IMAGES, LANGUAGES
|
||||||
from bookwyrm.signatures import create_key_pair
|
from bookwyrm.signatures import create_key_pair
|
||||||
from bookwyrm.tasks import app, MISC
|
from bookwyrm.tasks import app, MISC
|
||||||
from bookwyrm.utils import regex
|
from bookwyrm.utils import regex
|
||||||
|
from bookwyrm.utils.db import add_update_fields
|
||||||
from .activitypub_mixin import OrderedCollectionPageMixin, ActivitypubMixin
|
from .activitypub_mixin import OrderedCollectionPageMixin, ActivitypubMixin
|
||||||
from .base_model import BookWyrmModel, DeactivationReason, new_access_code
|
from .base_model import BookWyrmModel, DeactivationReason, new_access_code
|
||||||
from .federated_server import FederatedServer
|
from .federated_server import FederatedServer
|
||||||
|
@ -75,11 +78,12 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
||||||
summary = fields.HtmlField(null=True, blank=True)
|
summary = fields.HtmlField(null=True, blank=True)
|
||||||
local = models.BooleanField(default=False)
|
local = models.BooleanField(default=False)
|
||||||
bookwyrm_user = fields.BooleanField(default=True)
|
bookwyrm_user = fields.BooleanField(default=True)
|
||||||
localname = CICharField(
|
localname = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
null=True,
|
null=True,
|
||||||
unique=True,
|
unique=True,
|
||||||
validators=[fields.validate_localname],
|
validators=[fields.validate_localname],
|
||||||
|
db_collation="case_insensitive",
|
||||||
)
|
)
|
||||||
# name is your display name, which you can change at will
|
# name is your display name, which you can change at will
|
||||||
name = fields.CharField(max_length=100, null=True, blank=True)
|
name = fields.CharField(max_length=100, null=True, blank=True)
|
||||||
|
@ -156,7 +160,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
||||||
show_guided_tour = models.BooleanField(default=True)
|
show_guided_tour = models.BooleanField(default=True)
|
||||||
|
|
||||||
# feed options
|
# feed options
|
||||||
feed_status_types = ArrayField(
|
feed_status_types = DjangoArrayField(
|
||||||
models.CharField(max_length=10, blank=False, choices=FeedFilterChoices),
|
models.CharField(max_length=10, blank=False, choices=FeedFilterChoices),
|
||||||
size=8,
|
size=8,
|
||||||
default=get_feed_filter_choices,
|
default=get_feed_filter_choices,
|
||||||
|
@ -165,8 +169,8 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
||||||
summary_keys = models.JSONField(null=True)
|
summary_keys = models.JSONField(null=True)
|
||||||
|
|
||||||
preferred_timezone = models.CharField(
|
preferred_timezone = models.CharField(
|
||||||
choices=[(str(tz), str(tz)) for tz in pytz.all_timezones],
|
choices=[(str(tz), str(tz)) for tz in sorted(zoneinfo.available_timezones())],
|
||||||
default=str(pytz.utc),
|
default=str(datetime.timezone.utc),
|
||||||
max_length=255,
|
max_length=255,
|
||||||
)
|
)
|
||||||
preferred_language = models.CharField(
|
preferred_language = models.CharField(
|
||||||
|
@ -336,13 +340,14 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
||||||
]
|
]
|
||||||
return activity_object
|
return activity_object
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, update_fields: Optional[Iterable[str]] = None, **kwargs):
|
||||||
"""populate fields for new local users"""
|
"""populate fields for new local users"""
|
||||||
created = not bool(self.id)
|
created = not bool(self.id)
|
||||||
if not self.local and not re.match(regex.FULL_USERNAME, self.username):
|
if not self.local and not re.match(regex.FULL_USERNAME, self.username):
|
||||||
# generate a username that uses the domain (webfinger format)
|
# generate a username that uses the domain (webfinger format)
|
||||||
actor_parts = urlparse(self.remote_id)
|
actor_parts = urlparse(self.remote_id)
|
||||||
self.username = f"{self.username}@{actor_parts.hostname}"
|
self.username = f"{self.username}@{actor_parts.hostname}"
|
||||||
|
update_fields = add_update_fields(update_fields, "username")
|
||||||
|
|
||||||
# this user already exists, no need to populate fields
|
# this user already exists, no need to populate fields
|
||||||
if not created:
|
if not created:
|
||||||
|
@ -351,12 +356,12 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
||||||
elif not self.deactivation_date:
|
elif not self.deactivation_date:
|
||||||
self.deactivation_date = timezone.now()
|
self.deactivation_date = timezone.now()
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, update_fields=update_fields, **kwargs)
|
||||||
return
|
return
|
||||||
|
|
||||||
# this is a new remote user, we need to set their remote server field
|
# this is a new remote user, we need to set their remote server field
|
||||||
if not self.local:
|
if not self.local:
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, update_fields=update_fields, **kwargs)
|
||||||
transaction.on_commit(lambda: set_remote_server(self.id))
|
transaction.on_commit(lambda: set_remote_server(self.id))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -368,8 +373,17 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
||||||
self.shared_inbox = f"{BASE_URL}/inbox"
|
self.shared_inbox = f"{BASE_URL}/inbox"
|
||||||
self.outbox = f"{self.remote_id}/outbox"
|
self.outbox = f"{self.remote_id}/outbox"
|
||||||
|
|
||||||
|
update_fields = add_update_fields(
|
||||||
|
update_fields,
|
||||||
|
"remote_id",
|
||||||
|
"followers_url",
|
||||||
|
"inbox",
|
||||||
|
"shared_inbox",
|
||||||
|
"outbox",
|
||||||
|
)
|
||||||
|
|
||||||
# an id needs to be set before we can proceed with related models
|
# an id needs to be set before we can proceed with related models
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, update_fields=update_fields, **kwargs)
|
||||||
|
|
||||||
# make users editors by default
|
# make users editors by default
|
||||||
try:
|
try:
|
||||||
|
@ -520,14 +534,19 @@ class KeyPair(ActivitypubMixin, BookWyrmModel):
|
||||||
# self.owner is set by the OneToOneField on User
|
# self.owner is set by the OneToOneField on User
|
||||||
return f"{self.owner.remote_id}/#main-key"
|
return f"{self.owner.remote_id}/#main-key"
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, update_fields: Optional[Iterable[str]] = None, **kwargs):
|
||||||
"""create a key pair"""
|
"""create a key pair"""
|
||||||
# no broadcasting happening here
|
# no broadcasting happening here
|
||||||
if "broadcast" in kwargs:
|
if "broadcast" in kwargs:
|
||||||
del kwargs["broadcast"]
|
del kwargs["broadcast"]
|
||||||
|
|
||||||
if not self.public_key:
|
if not self.public_key:
|
||||||
self.private_key, self.public_key = create_key_pair()
|
self.private_key, self.public_key = create_key_pair()
|
||||||
return super().save(*args, **kwargs)
|
update_fields = add_update_fields(
|
||||||
|
update_fields, "private_key", "public_key"
|
||||||
|
)
|
||||||
|
|
||||||
|
super().save(*args, update_fields=update_fields, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@app.task(queue=MISC)
|
@app.task(queue=MISC)
|
||||||
|
|
|
@ -257,11 +257,8 @@ if env.bool("USE_DUMMY_CACHE", False):
|
||||||
else:
|
else:
|
||||||
CACHES = {
|
CACHES = {
|
||||||
"default": {
|
"default": {
|
||||||
"BACKEND": "django_redis.cache.RedisCache",
|
"BACKEND": "django.core.cache.backends.redis.RedisCache",
|
||||||
"LOCATION": REDIS_ACTIVITY_URL,
|
"LOCATION": REDIS_ACTIVITY_URL,
|
||||||
"OPTIONS": {
|
|
||||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"file_resubmit": {
|
"file_resubmit": {
|
||||||
"BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
|
"BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
|
||||||
|
@ -347,8 +344,6 @@ TIME_ZONE = "UTC"
|
||||||
|
|
||||||
USE_I18N = True
|
USE_I18N = True
|
||||||
|
|
||||||
USE_L10N = True
|
|
||||||
|
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
# Imagekit generated thumbnails
|
# Imagekit generated thumbnails
|
||||||
|
@ -371,6 +366,7 @@ if (USE_HTTPS and PORT == 443) or (not USE_HTTPS and PORT == 80):
|
||||||
else:
|
else:
|
||||||
NETLOC = f"{DOMAIN}:{PORT}"
|
NETLOC = f"{DOMAIN}:{PORT}"
|
||||||
BASE_URL = f"{PROTOCOL}://{NETLOC}"
|
BASE_URL = f"{PROTOCOL}://{NETLOC}"
|
||||||
|
CSRF_TRUSTED_ORIGINS = [BASE_URL]
|
||||||
|
|
||||||
USER_AGENT = f"BookWyrm (BookWyrm/{VERSION}; +{BASE_URL})"
|
USER_AGENT = f"BookWyrm (BookWyrm/{VERSION}; +{BASE_URL})"
|
||||||
|
|
||||||
|
@ -391,18 +387,40 @@ if USE_S3:
|
||||||
AWS_DEFAULT_ACL = "public-read"
|
AWS_DEFAULT_ACL = "public-read"
|
||||||
AWS_S3_OBJECT_PARAMETERS = {"CacheControl": "max-age=86400"}
|
AWS_S3_OBJECT_PARAMETERS = {"CacheControl": "max-age=86400"}
|
||||||
AWS_S3_URL_PROTOCOL = env("AWS_S3_URL_PROTOCOL", f"{PROTOCOL}:")
|
AWS_S3_URL_PROTOCOL = env("AWS_S3_URL_PROTOCOL", f"{PROTOCOL}:")
|
||||||
|
# Storages
|
||||||
|
STORAGES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "storages.backends.s3.S3Storage",
|
||||||
|
"OPTIONS": {
|
||||||
|
"location": "images",
|
||||||
|
"default_acl": "public-read",
|
||||||
|
"file_overwrite": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"staticfiles": {
|
||||||
|
"BACKEND": "storages.backends.s3.S3Storage",
|
||||||
|
"OPTIONS": {
|
||||||
|
"location": "static",
|
||||||
|
"default_acl": "public-read",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
"BACKEND": "storages.backends.s3.S3Storage",
|
||||||
|
"OPTIONS": {
|
||||||
|
"location": "images",
|
||||||
|
"default_acl": None,
|
||||||
|
"file_overwrite": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
# S3 Static settings
|
# S3 Static settings
|
||||||
STATIC_LOCATION = "static"
|
STATIC_LOCATION = "static"
|
||||||
STATIC_URL = f"{AWS_S3_URL_PROTOCOL}//{AWS_S3_CUSTOM_DOMAIN}/{STATIC_LOCATION}/"
|
STATIC_URL = f"{AWS_S3_URL_PROTOCOL}//{AWS_S3_CUSTOM_DOMAIN}/{STATIC_LOCATION}/"
|
||||||
STATIC_FULL_URL = STATIC_URL
|
STATIC_FULL_URL = STATIC_URL
|
||||||
STATICFILES_STORAGE = "bookwyrm.storage_backends.StaticStorage"
|
|
||||||
# S3 Media settings
|
# S3 Media settings
|
||||||
MEDIA_LOCATION = "images"
|
MEDIA_LOCATION = "images"
|
||||||
MEDIA_URL = f"{AWS_S3_URL_PROTOCOL}//{AWS_S3_CUSTOM_DOMAIN}/{MEDIA_LOCATION}/"
|
MEDIA_URL = f"{AWS_S3_URL_PROTOCOL}//{AWS_S3_CUSTOM_DOMAIN}/{MEDIA_LOCATION}/"
|
||||||
MEDIA_FULL_URL = MEDIA_URL
|
MEDIA_FULL_URL = MEDIA_URL
|
||||||
DEFAULT_FILE_STORAGE = "bookwyrm.storage_backends.ImagesStorage"
|
|
||||||
# S3 Exports settings
|
|
||||||
EXPORTS_STORAGE = "bookwyrm.storage_backends.ExportsS3Storage"
|
|
||||||
# Content Security Policy
|
# Content Security Policy
|
||||||
CSP_DEFAULT_SRC = [
|
CSP_DEFAULT_SRC = [
|
||||||
"'self'",
|
"'self'",
|
||||||
|
@ -422,36 +440,62 @@ elif USE_AZURE:
|
||||||
AZURE_ACCOUNT_KEY = env("AZURE_ACCOUNT_KEY")
|
AZURE_ACCOUNT_KEY = env("AZURE_ACCOUNT_KEY")
|
||||||
AZURE_CONTAINER = env("AZURE_CONTAINER")
|
AZURE_CONTAINER = env("AZURE_CONTAINER")
|
||||||
AZURE_CUSTOM_DOMAIN = env("AZURE_CUSTOM_DOMAIN")
|
AZURE_CUSTOM_DOMAIN = env("AZURE_CUSTOM_DOMAIN")
|
||||||
|
# Storages
|
||||||
|
STORAGES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "storages.backends.azure_storage.AzureStorage",
|
||||||
|
"OPTIONS": {
|
||||||
|
"location": "images",
|
||||||
|
"overwrite_files": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"staticfiles": {
|
||||||
|
"BACKEND": "storages.backends.azure_storage.AzureStorage",
|
||||||
|
"OPTIONS": {
|
||||||
|
"location": "static",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
"BACKEND": None, # not implemented yet
|
||||||
|
},
|
||||||
|
}
|
||||||
# Azure Static settings
|
# Azure Static settings
|
||||||
STATIC_LOCATION = "static"
|
STATIC_LOCATION = "static"
|
||||||
STATIC_URL = (
|
STATIC_URL = (
|
||||||
f"{PROTOCOL}://{AZURE_CUSTOM_DOMAIN}/{AZURE_CONTAINER}/{STATIC_LOCATION}/"
|
f"{PROTOCOL}://{AZURE_CUSTOM_DOMAIN}/{AZURE_CONTAINER}/{STATIC_LOCATION}/"
|
||||||
)
|
)
|
||||||
STATIC_FULL_URL = STATIC_URL
|
STATIC_FULL_URL = STATIC_URL
|
||||||
STATICFILES_STORAGE = "bookwyrm.storage_backends.AzureStaticStorage"
|
|
||||||
# Azure Media settings
|
# Azure Media settings
|
||||||
MEDIA_LOCATION = "images"
|
MEDIA_LOCATION = "images"
|
||||||
MEDIA_URL = (
|
MEDIA_URL = (
|
||||||
f"{PROTOCOL}://{AZURE_CUSTOM_DOMAIN}/{AZURE_CONTAINER}/{MEDIA_LOCATION}/"
|
f"{PROTOCOL}://{AZURE_CUSTOM_DOMAIN}/{AZURE_CONTAINER}/{MEDIA_LOCATION}/"
|
||||||
)
|
)
|
||||||
MEDIA_FULL_URL = MEDIA_URL
|
MEDIA_FULL_URL = MEDIA_URL
|
||||||
DEFAULT_FILE_STORAGE = "bookwyrm.storage_backends.AzureImagesStorage"
|
|
||||||
# Azure Exports settings
|
|
||||||
EXPORTS_STORAGE = None # not implemented yet
|
|
||||||
# Content Security Policy
|
# Content Security Policy
|
||||||
CSP_DEFAULT_SRC = ["'self'", AZURE_CUSTOM_DOMAIN] + CSP_ADDITIONAL_HOSTS
|
CSP_DEFAULT_SRC = ["'self'", AZURE_CUSTOM_DOMAIN] + CSP_ADDITIONAL_HOSTS
|
||||||
CSP_SCRIPT_SRC = ["'self'", AZURE_CUSTOM_DOMAIN] + CSP_ADDITIONAL_HOSTS
|
CSP_SCRIPT_SRC = ["'self'", AZURE_CUSTOM_DOMAIN] + CSP_ADDITIONAL_HOSTS
|
||||||
else:
|
else:
|
||||||
|
# Storages
|
||||||
|
STORAGES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||||
|
},
|
||||||
|
"staticfiles": {
|
||||||
|
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||||
|
"OPTIONS": {
|
||||||
|
"location": "exports",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
# Static settings
|
# Static settings
|
||||||
STATIC_URL = "/static/"
|
STATIC_URL = "/static/"
|
||||||
STATIC_FULL_URL = BASE_URL + STATIC_URL
|
STATIC_FULL_URL = BASE_URL + STATIC_URL
|
||||||
STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage"
|
|
||||||
# Media settings
|
# Media settings
|
||||||
MEDIA_URL = "/images/"
|
MEDIA_URL = "/images/"
|
||||||
MEDIA_FULL_URL = BASE_URL + MEDIA_URL
|
MEDIA_FULL_URL = BASE_URL + MEDIA_URL
|
||||||
DEFAULT_FILE_STORAGE = "django.core.files.storage.FileSystemStorage"
|
|
||||||
# Exports settings
|
|
||||||
EXPORTS_STORAGE = "bookwyrm.storage_backends.ExportsFileStorage"
|
|
||||||
# Content Security Policy
|
# Content Security Policy
|
||||||
CSP_DEFAULT_SRC = ["'self'"] + CSP_ADDITIONAL_HOSTS
|
CSP_DEFAULT_SRC = ["'self'"] + CSP_ADDITIONAL_HOSTS
|
||||||
CSP_SCRIPT_SRC = ["'self'"] + CSP_ADDITIONAL_HOSTS
|
CSP_SCRIPT_SRC = ["'self'"] + CSP_ADDITIONAL_HOSTS
|
||||||
|
|
|
@ -1,79 +0,0 @@
|
||||||
"""Handles backends for storages"""
|
|
||||||
import os
|
|
||||||
from tempfile import SpooledTemporaryFile
|
|
||||||
from django.core.files.storage import FileSystemStorage
|
|
||||||
from storages.backends.s3boto3 import S3Boto3Storage
|
|
||||||
from storages.backends.azure_storage import AzureStorage
|
|
||||||
|
|
||||||
|
|
||||||
class StaticStorage(S3Boto3Storage): # pylint: disable=abstract-method
|
|
||||||
"""Storage class for Static contents"""
|
|
||||||
|
|
||||||
location = "static"
|
|
||||||
default_acl = "public-read"
|
|
||||||
|
|
||||||
|
|
||||||
class ImagesStorage(S3Boto3Storage): # pylint: disable=abstract-method
|
|
||||||
"""Storage class for Image files"""
|
|
||||||
|
|
||||||
location = "images"
|
|
||||||
default_acl = "public-read"
|
|
||||||
file_overwrite = False
|
|
||||||
|
|
||||||
"""
|
|
||||||
This is our custom version of S3Boto3Storage that fixes a bug in
|
|
||||||
boto3 where the passed in file is closed upon upload.
|
|
||||||
From:
|
|
||||||
https://github.com/matthewwithanm/django-imagekit/issues/391#issuecomment-275367006
|
|
||||||
https://github.com/boto/boto3/issues/929
|
|
||||||
https://github.com/matthewwithanm/django-imagekit/issues/391
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _save(self, name, content):
|
|
||||||
"""
|
|
||||||
We create a clone of the content file as when this is passed to
|
|
||||||
boto3 it wrongly closes the file upon upload where as the storage
|
|
||||||
backend expects it to still be open
|
|
||||||
"""
|
|
||||||
# Seek our content back to the start
|
|
||||||
content.seek(0, os.SEEK_SET)
|
|
||||||
|
|
||||||
# Create a temporary file that will write to disk after a specified
|
|
||||||
# size. This file will be automatically deleted when closed by
|
|
||||||
# boto3 or after exiting the `with` statement if the boto3 is fixed
|
|
||||||
with SpooledTemporaryFile() as content_autoclose:
|
|
||||||
|
|
||||||
# Write our original content into our copy that will be closed by boto3
|
|
||||||
content_autoclose.write(content.read())
|
|
||||||
|
|
||||||
# Upload the object which will auto close the
|
|
||||||
# content_autoclose instance
|
|
||||||
return super()._save(name, content_autoclose)
|
|
||||||
|
|
||||||
|
|
||||||
class AzureStaticStorage(AzureStorage): # pylint: disable=abstract-method
|
|
||||||
"""Storage class for Static contents"""
|
|
||||||
|
|
||||||
location = "static"
|
|
||||||
|
|
||||||
|
|
||||||
class AzureImagesStorage(AzureStorage): # pylint: disable=abstract-method
|
|
||||||
"""Storage class for Image files"""
|
|
||||||
|
|
||||||
location = "images"
|
|
||||||
overwrite_files = False
|
|
||||||
|
|
||||||
|
|
||||||
class ExportsFileStorage(FileSystemStorage): # pylint: disable=abstract-method
|
|
||||||
"""Storage class for exports contents with local files"""
|
|
||||||
|
|
||||||
location = "exports"
|
|
||||||
overwrite_files = False
|
|
||||||
|
|
||||||
|
|
||||||
class ExportsS3Storage(S3Boto3Storage): # pylint: disable=abstract-method
|
|
||||||
"""Storage class for exports contents with S3"""
|
|
||||||
|
|
||||||
location = "exports"
|
|
||||||
default_acl = None
|
|
||||||
overwrite_files = False
|
|
|
@ -137,14 +137,14 @@ def get_file_size(nbytes):
|
||||||
raw_size = float(nbytes)
|
raw_size = float(nbytes)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
return repr(nbytes)
|
return repr(nbytes)
|
||||||
else:
|
|
||||||
if raw_size < 1024:
|
if raw_size < 1024:
|
||||||
return f"{raw_size} bytes"
|
return f"{raw_size} bytes"
|
||||||
if raw_size < 1024**2:
|
if raw_size < 1024**2:
|
||||||
return f"{raw_size/1024:.2f} KB"
|
return f"{raw_size/1024:.2f} KB"
|
||||||
if raw_size < 1024**3:
|
if raw_size < 1024**3:
|
||||||
return f"{raw_size/1024**2:.2f} MB"
|
return f"{raw_size/1024**2:.2f} MB"
|
||||||
return f"{raw_size/1024**3:.2f} GB"
|
return f"{raw_size/1024**3:.2f} GB"
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name="get_user_permission")
|
@register.filter(name="get_user_permission")
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
""" testing activitystreams """
|
""" testing activitystreams """
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
from bookwyrm import activitystreams, models
|
from bookwyrm import activitystreams, models
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
""" testing activitystreams """
|
""" testing activitystreams """
|
||||||
from datetime import datetime, timedelta
|
import datetime
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
@ -71,8 +71,8 @@ class ActivitystreamsSignals(TestCase):
|
||||||
user=self.remote_user,
|
user=self.remote_user,
|
||||||
content="hi",
|
content="hi",
|
||||||
privacy="public",
|
privacy="public",
|
||||||
created_date=datetime(2022, 5, 16, tzinfo=timezone.utc),
|
created_date=datetime.datetime(2022, 5, 16, tzinfo=datetime.timezone.utc),
|
||||||
published_date=datetime(2022, 5, 14, tzinfo=timezone.utc),
|
published_date=datetime.datetime(2022, 5, 14, tzinfo=datetime.timezone.utc),
|
||||||
)
|
)
|
||||||
with patch("bookwyrm.activitystreams.add_status_task.apply_async") as mock:
|
with patch("bookwyrm.activitystreams.add_status_task.apply_async") as mock:
|
||||||
activitystreams.add_status_on_create_command(models.Status, status, False)
|
activitystreams.add_status_on_create_command(models.Status, status, False)
|
||||||
|
@ -87,7 +87,7 @@ class ActivitystreamsSignals(TestCase):
|
||||||
user=self.remote_user,
|
user=self.remote_user,
|
||||||
content="hi",
|
content="hi",
|
||||||
privacy="public",
|
privacy="public",
|
||||||
published_date=timezone.now() - timedelta(days=1),
|
published_date=timezone.now() - datetime.timedelta(days=1),
|
||||||
)
|
)
|
||||||
with patch("bookwyrm.activitystreams.add_status_task.apply_async") as mock:
|
with patch("bookwyrm.activitystreams.add_status_task.apply_async") as mock:
|
||||||
activitystreams.add_status_on_create_command(models.Status, status, False)
|
activitystreams.add_status_on_create_command(models.Status, status, False)
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
import pathlib
|
import pathlib
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
import datetime
|
import datetime
|
||||||
import pytz
|
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
|
@ -13,7 +12,7 @@ from bookwyrm.models.import_job import handle_imported_book
|
||||||
|
|
||||||
def make_date(*args):
|
def make_date(*args):
|
||||||
"""helper function to easily generate a date obj"""
|
"""helper function to easily generate a date obj"""
|
||||||
return datetime.datetime(*args, tzinfo=pytz.UTC)
|
return datetime.datetime(*args, tzinfo=datetime.timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
|
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
|
||||||
|
|
|
@ -3,7 +3,6 @@ from collections import namedtuple
|
||||||
import pathlib
|
import pathlib
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
import datetime
|
import datetime
|
||||||
import pytz
|
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
import responses
|
import responses
|
||||||
|
@ -16,7 +15,7 @@ from bookwyrm.models.import_job import handle_imported_book
|
||||||
|
|
||||||
def make_date(*args):
|
def make_date(*args):
|
||||||
"""helper function to easily generate a date obj"""
|
"""helper function to easily generate a date obj"""
|
||||||
return datetime.datetime(*args, tzinfo=pytz.UTC)
|
return datetime.datetime(*args, tzinfo=datetime.timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
|
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
import pathlib
|
import pathlib
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
import datetime
|
import datetime
|
||||||
import pytz
|
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
|
@ -13,7 +12,7 @@ from bookwyrm.models.import_job import handle_imported_book
|
||||||
|
|
||||||
def make_date(*args):
|
def make_date(*args):
|
||||||
"""helper function to easily generate a date obj"""
|
"""helper function to easily generate a date obj"""
|
||||||
return datetime.datetime(*args, tzinfo=pytz.UTC)
|
return datetime.datetime(*args, tzinfo=datetime.timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
|
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
import pathlib
|
import pathlib
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
import datetime
|
import datetime
|
||||||
import pytz
|
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
|
@ -13,7 +12,7 @@ from bookwyrm.models.import_job import handle_imported_book
|
||||||
|
|
||||||
def make_date(*args):
|
def make_date(*args):
|
||||||
"""helper function to easily generate a date obj"""
|
"""helper function to easily generate a date obj"""
|
||||||
return datetime.datetime(*args, tzinfo=pytz.UTC)
|
return datetime.datetime(*args, tzinfo=datetime.timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
|
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
import pathlib
|
import pathlib
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
import datetime
|
import datetime
|
||||||
import pytz
|
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
|
@ -13,7 +12,7 @@ from bookwyrm.models.import_job import handle_imported_book
|
||||||
|
|
||||||
def make_date(*args):
|
def make_date(*args):
|
||||||
"""helper function to easily generate a date obj"""
|
"""helper function to easily generate a date obj"""
|
||||||
return datetime.datetime(*args, tzinfo=pytz.UTC)
|
return datetime.datetime(*args, tzinfo=datetime.timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
|
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
""" testing models """
|
""" testing models """
|
||||||
import datetime
|
import datetime
|
||||||
|
from datetime import timezone
|
||||||
import json
|
import json
|
||||||
import pathlib
|
import pathlib
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from django.utils import timezone
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
import responses
|
import responses
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
""" style fixes and lookups for templates """
|
""" style fixes and lookups for templates """
|
||||||
from datetime import datetime
|
import datetime
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
@ -95,14 +95,18 @@ class StatusDisplayTags(TestCase):
|
||||||
|
|
||||||
def test_get_published_date(self, *_):
|
def test_get_published_date(self, *_):
|
||||||
"""date formatting"""
|
"""date formatting"""
|
||||||
date = datetime(2020, 1, 1, 0, 0, tzinfo=timezone.utc)
|
date = datetime.datetime(2020, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
|
||||||
with patch("django.utils.timezone.now") as timezone_mock:
|
with patch("django.utils.timezone.now") as timezone_mock:
|
||||||
timezone_mock.return_value = datetime(2022, 1, 1, 0, 0, tzinfo=timezone.utc)
|
timezone_mock.return_value = datetime.datetime(
|
||||||
|
2022, 1, 1, 0, 0, tzinfo=datetime.timezone.utc
|
||||||
|
)
|
||||||
result = status_display.get_published_date(date)
|
result = status_display.get_published_date(date)
|
||||||
self.assertEqual(result, "Jan. 1, 2020")
|
self.assertEqual(result, "Jan. 1, 2020")
|
||||||
|
|
||||||
date = datetime(2022, 1, 1, 0, 0, tzinfo=timezone.utc)
|
date = datetime.datetime(2022, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
|
||||||
with patch("django.utils.timezone.now") as timezone_mock:
|
with patch("django.utils.timezone.now") as timezone_mock:
|
||||||
timezone_mock.return_value = datetime(2022, 1, 8, 0, 0, tzinfo=timezone.utc)
|
timezone_mock.return_value = datetime.datetime(
|
||||||
|
2022, 1, 8, 0, 0, tzinfo=datetime.timezone.utc
|
||||||
|
)
|
||||||
result = status_display.get_published_date(date)
|
result = status_display.get_published_date(date)
|
||||||
self.assertEqual(result, "Jan 1")
|
self.assertEqual(result, "Jan 1")
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
""" test searching for books """
|
""" test searching for books """
|
||||||
import datetime
|
import datetime
|
||||||
|
from datetime import timezone
|
||||||
|
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
from bookwyrm import book_search, models
|
from bookwyrm import book_search, models
|
||||||
from bookwyrm.connectors.abstract_connector import AbstractMinimalConnector
|
from bookwyrm.connectors.abstract_connector import AbstractMinimalConnector
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
""" test partial_date module """
|
""" test partial_date module """
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
from datetime import timezone
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.utils import timezone
|
|
||||||
from django.utils import translation
|
from django.utils import translation
|
||||||
|
|
||||||
from bookwyrm.utils import partial_date
|
from bookwyrm.utils import partial_date
|
||||||
|
|
|
@ -72,12 +72,12 @@ class Signature(TestCase):
|
||||||
urlsplit(self.rat.inbox).path,
|
urlsplit(self.rat.inbox).path,
|
||||||
data=data,
|
data=data,
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
**{
|
headers={
|
||||||
"HTTP_DATE": now,
|
"date": now,
|
||||||
"HTTP_SIGNATURE": signature,
|
"signature": signature,
|
||||||
"HTTP_DIGEST": digest,
|
"digest": digest,
|
||||||
"HTTP_CONTENT_TYPE": "application/activity+json; charset=utf-8",
|
"content-type": "application/activity+json; charset=utf-8",
|
||||||
"HTTP_HOST": NETLOC,
|
"host": NETLOC,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ def validate_html(html):
|
||||||
e for e in errors.split("\n") if not any(exclude in e for exclude in excluded)
|
e for e in errors.split("\n") if not any(exclude in e for exclude in excluded)
|
||||||
)
|
)
|
||||||
if errors:
|
if errors:
|
||||||
raise Exception(errors)
|
raise ValueError(errors)
|
||||||
|
|
||||||
validator = HtmlValidator()
|
validator = HtmlValidator()
|
||||||
# will raise exceptions
|
# will raise exceptions
|
||||||
|
@ -62,6 +62,6 @@ class HtmlValidator(HTMLParser): # pylint: disable=abstract-method
|
||||||
and "noreferrer" in value
|
and "noreferrer" in value
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
raise Exception(
|
raise ValueError(
|
||||||
'Links to a new tab must have rel="nofollow noopener noreferrer"'
|
'Links to a new tab must have rel="nofollow noopener noreferrer"'
|
||||||
)
|
)
|
||||||
|
|
|
@ -123,8 +123,8 @@ class ImportViews(TestCase):
|
||||||
"""Give people a sense of the timing"""
|
"""Give people a sense of the timing"""
|
||||||
models.ImportJob.objects.create(
|
models.ImportJob.objects.create(
|
||||||
user=self.local_user,
|
user=self.local_user,
|
||||||
created_date=datetime.datetime(2000, 1, 1),
|
created_date=datetime.datetime(2000, 1, 1, tzinfo=datetime.timezone.utc),
|
||||||
updated_date=datetime.datetime(2001, 1, 1),
|
updated_date=datetime.datetime(2001, 1, 1, tzinfo=datetime.timezone.utc),
|
||||||
status="complete",
|
status="complete",
|
||||||
complete=True,
|
complete=True,
|
||||||
mappings={},
|
mappings={},
|
||||||
|
|
|
@ -134,7 +134,10 @@ class Inbox(TestCase):
|
||||||
"""check for blocked servers"""
|
"""check for blocked servers"""
|
||||||
request = self.factory.post(
|
request = self.factory.post(
|
||||||
"",
|
"",
|
||||||
HTTP_USER_AGENT="http.rb/4.4.1 (Mastodon/3.3.0; +https://mastodon.social/)",
|
headers={
|
||||||
|
# pylint: disable-next=line-too-long
|
||||||
|
"user-agent": "http.rb/4.4.1 (Mastodon/3.3.0; +https://mastodon.social/)",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
self.assertIsNone(views.inbox.raise_is_blocked_user_agent(request))
|
self.assertIsNone(views.inbox.raise_is_blocked_user_agent(request))
|
||||||
|
|
||||||
|
|
|
@ -78,7 +78,7 @@ class DeleteUserViews(TestCase):
|
||||||
form.data["password"] = "password"
|
form.data["password"] = "password"
|
||||||
request = self.factory.post("", form.data)
|
request = self.factory.post("", form.data)
|
||||||
request.user = self.local_user
|
request.user = self.local_user
|
||||||
middleware = SessionMiddleware()
|
middleware = SessionMiddleware(request)
|
||||||
middleware.process_request(request)
|
middleware.process_request(request)
|
||||||
request.session.save()
|
request.session.save()
|
||||||
|
|
||||||
|
@ -105,7 +105,7 @@ class DeleteUserViews(TestCase):
|
||||||
view = views.DeactivateUser.as_view()
|
view = views.DeactivateUser.as_view()
|
||||||
request = self.factory.post("")
|
request = self.factory.post("")
|
||||||
request.user = self.local_user
|
request.user = self.local_user
|
||||||
middleware = SessionMiddleware()
|
middleware = SessionMiddleware(request)
|
||||||
middleware.process_request(request)
|
middleware.process_request(request)
|
||||||
request.session.save()
|
request.session.save()
|
||||||
|
|
||||||
|
@ -137,7 +137,7 @@ class DeleteUserViews(TestCase):
|
||||||
form.data["password"] = "password"
|
form.data["password"] = "password"
|
||||||
request = self.factory.post("", form.data)
|
request = self.factory.post("", form.data)
|
||||||
request.user = self.local_user
|
request.user = self.local_user
|
||||||
middleware = SessionMiddleware()
|
middleware = SessionMiddleware(request)
|
||||||
middleware.process_request(request)
|
middleware.process_request(request)
|
||||||
request.session.save()
|
request.session.save()
|
||||||
|
|
||||||
|
@ -159,7 +159,7 @@ class DeleteUserViews(TestCase):
|
||||||
form.data["password"] = "password"
|
form.data["password"] = "password"
|
||||||
request = self.factory.post("", form.data)
|
request = self.factory.post("", form.data)
|
||||||
request.user = self.local_user
|
request.user = self.local_user
|
||||||
middleware = SessionMiddleware()
|
middleware = SessionMiddleware(request)
|
||||||
middleware.process_request(request)
|
middleware.process_request(request)
|
||||||
request.session.save()
|
request.session.save()
|
||||||
|
|
||||||
|
|
|
@ -101,7 +101,7 @@ class ViewsHelpers(TestCase):
|
||||||
|
|
||||||
request = self.factory.post("", form.data)
|
request = self.factory.post("", form.data)
|
||||||
request.user = self.local_user
|
request.user = self.local_user
|
||||||
middleware = SessionMiddleware()
|
middleware = SessionMiddleware(request)
|
||||||
middleware.process_request(request)
|
middleware.process_request(request)
|
||||||
request.session.save()
|
request.session.save()
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
"""testing the annual summary page"""
|
"""testing the annual summary page"""
|
||||||
from datetime import datetime
|
import datetime
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
import pytz
|
|
||||||
|
|
||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
|
@ -15,7 +14,7 @@ from bookwyrm.tests.validate_html import validate_html
|
||||||
|
|
||||||
def make_date(*args):
|
def make_date(*args):
|
||||||
"""helper function to easily generate a date obj"""
|
"""helper function to easily generate a date obj"""
|
||||||
return datetime(*args, tzinfo=pytz.UTC)
|
return datetime.datetime(*args, tzinfo=datetime.timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
class AnnualSummary(TestCase):
|
class AnnualSummary(TestCase):
|
||||||
|
|
|
@ -113,11 +113,20 @@ class ViewsHelpers(TestCase): # pylint: disable=too-many-public-methods
|
||||||
request = self.factory.get(
|
request = self.factory.get(
|
||||||
"",
|
"",
|
||||||
{"q": "Test Book"},
|
{"q": "Test Book"},
|
||||||
HTTP_USER_AGENT="http.rb/4.4.1 (Mastodon/3.3.0; +https://mastodon.social/)",
|
headers={
|
||||||
|
# pylint: disable-next=line-too-long
|
||||||
|
"user-agent": "http.rb/4.4.1 (Mastodon/3.3.0; +https://mastodon.social/)",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
self.assertFalse(views.helpers.is_bookwyrm_request(request))
|
self.assertFalse(views.helpers.is_bookwyrm_request(request))
|
||||||
|
|
||||||
request = self.factory.get("", {"q": "Test Book"}, HTTP_USER_AGENT=USER_AGENT)
|
request = self.factory.get(
|
||||||
|
"",
|
||||||
|
{"q": "Test Book"},
|
||||||
|
headers={
|
||||||
|
"user-agent": USER_AGENT,
|
||||||
|
},
|
||||||
|
)
|
||||||
self.assertTrue(views.helpers.is_bookwyrm_request(request))
|
self.assertTrue(views.helpers.is_bookwyrm_request(request))
|
||||||
|
|
||||||
def test_handle_remote_webfinger_invalid(self, *_):
|
def test_handle_remote_webfinger_invalid(self, *_):
|
||||||
|
@ -271,8 +280,12 @@ class ViewsHelpers(TestCase): # pylint: disable=too-many-public-methods
|
||||||
|
|
||||||
def test_redirect_to_referer_outside_domain(self, *_):
|
def test_redirect_to_referer_outside_domain(self, *_):
|
||||||
"""safely send people on their way"""
|
"""safely send people on their way"""
|
||||||
request = self.factory.get("/path")
|
request = self.factory.get(
|
||||||
request.META = {"HTTP_REFERER": "http://outside.domain/name"}
|
"/path",
|
||||||
|
headers={
|
||||||
|
"referer": "http://outside.domain/name",
|
||||||
|
},
|
||||||
|
)
|
||||||
result = views.helpers.redirect_to_referer(
|
result = views.helpers.redirect_to_referer(
|
||||||
request, "user-feed", self.local_user.localname
|
request, "user-feed", self.local_user.localname
|
||||||
)
|
)
|
||||||
|
@ -280,21 +293,33 @@ class ViewsHelpers(TestCase): # pylint: disable=too-many-public-methods
|
||||||
|
|
||||||
def test_redirect_to_referer_outside_domain_with_fallback(self, *_):
|
def test_redirect_to_referer_outside_domain_with_fallback(self, *_):
|
||||||
"""invalid domain with regular params for the redirect function"""
|
"""invalid domain with regular params for the redirect function"""
|
||||||
request = self.factory.get("/path")
|
request = self.factory.get(
|
||||||
request.META = {"HTTP_REFERER": "https://outside.domain/name"}
|
"/path",
|
||||||
|
headers={
|
||||||
|
"referer": "http://outside.domain/name",
|
||||||
|
},
|
||||||
|
)
|
||||||
result = views.helpers.redirect_to_referer(request)
|
result = views.helpers.redirect_to_referer(request)
|
||||||
self.assertEqual(result.url, "/")
|
self.assertEqual(result.url, "/")
|
||||||
|
|
||||||
def test_redirect_to_referer_valid_domain(self, *_):
|
def test_redirect_to_referer_valid_domain(self, *_):
|
||||||
"""redirect to within the app"""
|
"""redirect to within the app"""
|
||||||
request = self.factory.get("/path")
|
request = self.factory.get(
|
||||||
request.META = {"HTTP_REFERER": f"{BASE_URL}/and/a/path"}
|
"/path",
|
||||||
|
headers={
|
||||||
|
"referer": f"{BASE_URL}/and/a/path",
|
||||||
|
},
|
||||||
|
)
|
||||||
result = views.helpers.redirect_to_referer(request)
|
result = views.helpers.redirect_to_referer(request)
|
||||||
self.assertEqual(result.url, f"{BASE_URL}/and/a/path")
|
self.assertEqual(result.url, f"{BASE_URL}/and/a/path")
|
||||||
|
|
||||||
def test_redirect_to_referer_with_get_args(self, *_):
|
def test_redirect_to_referer_with_get_args(self, *_):
|
||||||
"""if the path has get params (like sort) they are preserved"""
|
"""if the path has get params (like sort) they are preserved"""
|
||||||
request = self.factory.get("/path")
|
request = self.factory.get(
|
||||||
request.META = {"HTTP_REFERER": f"{BASE_URL}/and/a/path?sort=hello"}
|
"/path",
|
||||||
|
headers={
|
||||||
|
"referer": f"{BASE_URL}/and/a/path?sort=hello",
|
||||||
|
},
|
||||||
|
)
|
||||||
result = views.helpers.redirect_to_referer(request)
|
result = views.helpers.redirect_to_referer(request)
|
||||||
self.assertEqual(result.url, f"{BASE_URL}/and/a/path?sort=hello")
|
self.assertEqual(result.url, f"{BASE_URL}/and/a/path?sort=hello")
|
||||||
|
|
|
@ -122,7 +122,7 @@ class OutboxView(TestCase):
|
||||||
privacy="public",
|
privacy="public",
|
||||||
)
|
)
|
||||||
|
|
||||||
request = self.factory.get("", {"page": 1}, HTTP_USER_AGENT=USER_AGENT)
|
request = self.factory.get("", {"page": 1}, headers={"user-agent": USER_AGENT})
|
||||||
result = views.Outbox.as_view()(request, "mouse")
|
result = views.Outbox.as_view()(request, "mouse")
|
||||||
|
|
||||||
data = json.loads(result.content)
|
data = json.loads(result.content)
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
""" tests updating reading progress """
|
""" tests updating reading progress """
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
from django.test import TestCase, Client
|
from django.test import TestCase, Client
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
""" Database utilities """
|
""" Database utilities """
|
||||||
|
|
||||||
from typing import cast
|
from typing import Optional, Iterable, Set, cast
|
||||||
import sqlparse # type: ignore
|
import sqlparse # type: ignore[import-untyped]
|
||||||
|
|
||||||
|
|
||||||
def format_trigger(sql: str) -> str:
|
def format_trigger(sql: str) -> str:
|
||||||
|
@ -21,3 +21,15 @@ def format_trigger(sql: str) -> str:
|
||||||
identifier_case="lower",
|
identifier_case="lower",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def add_update_fields(
|
||||||
|
update_fields: Optional[Iterable[str]], *fields: str
|
||||||
|
) -> Optional[Set[str]]:
|
||||||
|
"""
|
||||||
|
Helper for adding fields to the update_fields kwarg when modifying an object
|
||||||
|
in a model's save() method.
|
||||||
|
|
||||||
|
https://docs.djangoproject.com/en/5.0/releases/4.2/#setting-update-fields-in-model-save-may-now-be-required
|
||||||
|
"""
|
||||||
|
return set(fields).union(update_fields) if update_fields is not None else None
|
||||||
|
|
|
@ -222,17 +222,17 @@ class PartialDateDescriptor:
|
||||||
return [("DAY", "Day prec."), ("MONTH", "Month prec."), ("YEAR", "Year prec.")]
|
return [("DAY", "Day prec."), ("MONTH", "Month prec."), ("YEAR", "Year prec.")]
|
||||||
|
|
||||||
|
|
||||||
class PartialDateModel(models.DateTimeField): # type: ignore
|
class PartialDateModel(models.DateTimeField): # type: ignore[type-arg]
|
||||||
"""a date field for Django models, using PartialDate as values"""
|
"""a date field for Django models, using PartialDate as values"""
|
||||||
|
|
||||||
descriptor_class = PartialDateDescriptor
|
descriptor_class = PartialDateDescriptor
|
||||||
|
|
||||||
def formfield(self, **kwargs): # type: ignore
|
def formfield(self, **kwargs): # type: ignore[no-untyped-def]
|
||||||
kwargs.setdefault("form_class", PartialDateFormField)
|
kwargs.setdefault("form_class", PartialDateFormField)
|
||||||
return super().formfield(**kwargs)
|
return super().formfield(**kwargs)
|
||||||
|
|
||||||
# pylint: disable-next=arguments-renamed
|
# pylint: disable-next=arguments-renamed,line-too-long
|
||||||
def contribute_to_class(self, model, our_name_in_model, **kwargs): # type: ignore
|
def contribute_to_class(self, model, our_name_in_model, **kwargs): # type: ignore[no-untyped-def]
|
||||||
# Define precision field.
|
# Define precision field.
|
||||||
descriptor = self.descriptor_class(self)
|
descriptor = self.descriptor_class(self)
|
||||||
precision: models.Field[Optional[str], Optional[str]] = models.CharField(
|
precision: models.Field[Optional[str], Optional[str]] = models.CharField(
|
||||||
|
|
|
@ -6,7 +6,7 @@ def clean(input_text: str) -> str:
|
||||||
"""Run through "bleach" """
|
"""Run through "bleach" """
|
||||||
return bleach.clean(
|
return bleach.clean(
|
||||||
input_text,
|
input_text,
|
||||||
tags=[
|
tags={
|
||||||
"p",
|
"p",
|
||||||
"blockquote",
|
"blockquote",
|
||||||
"br",
|
"br",
|
||||||
|
@ -20,7 +20,7 @@ def clean(input_text: str) -> str:
|
||||||
"ul",
|
"ul",
|
||||||
"ol",
|
"ol",
|
||||||
"li",
|
"li",
|
||||||
],
|
},
|
||||||
attributes=["href", "rel", "src", "alt", "data-mention"],
|
attributes=["href", "rel", "src", "alt", "data-mention"],
|
||||||
strip=True,
|
strip=True,
|
||||||
)
|
)
|
||||||
|
|
|
@ -225,4 +225,4 @@ def get_goal_status(user, year):
|
||||||
if goal.privacy != "public":
|
if goal.privacy != "public":
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return dict(**goal.progress, **{"goal": goal.goal})
|
return {**goal.progress, **{"goal": goal.goal}}
|
||||||
|
|
|
@ -231,7 +231,7 @@ def maybe_redirect_local_path(request, model):
|
||||||
def redirect_to_referer(request, *args, **kwargs):
|
def redirect_to_referer(request, *args, **kwargs):
|
||||||
"""Redirect to the referrer, if it's in our domain, with get params"""
|
"""Redirect to the referrer, if it's in our domain, with get params"""
|
||||||
# make sure the refer is part of this instance
|
# make sure the refer is part of this instance
|
||||||
validated = validate_url_domain(request.META.get("HTTP_REFERER"))
|
validated = validate_url_domain(request.headers.get("referer", ""))
|
||||||
|
|
||||||
if validated:
|
if validated:
|
||||||
return redirect(validated)
|
return redirect(validated)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
""" class views for login/register views """
|
""" class views for login/register views """
|
||||||
import pytz
|
import zoneinfo
|
||||||
from django.contrib.auth import login
|
from django.contrib.auth import login
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
|
@ -57,9 +57,11 @@ class Register(View):
|
||||||
email = form.data["email"]
|
email = form.data["email"]
|
||||||
password = form.data["password"]
|
password = form.data["password"]
|
||||||
try:
|
try:
|
||||||
preferred_timezone = pytz.timezone(form.data.get("preferred_timezone"))
|
preferred_timezone = zoneinfo.ZoneInfo(
|
||||||
except pytz.exceptions.UnknownTimeZoneError:
|
form.data.get("preferred_timezone", "")
|
||||||
preferred_timezone = pytz.utc
|
)
|
||||||
|
except (ValueError, zoneinfo.ZoneInfoNotFoundError):
|
||||||
|
preferred_timezone = zoneinfo.ZoneInfo("UTC")
|
||||||
|
|
||||||
# make sure the email isn't blocked as spam
|
# make sure the email isn't blocked as spam
|
||||||
email_domain = email.split("@")[-1]
|
email_domain = email.split("@")[-1]
|
||||||
|
|
|
@ -14,9 +14,9 @@ from django.urls import reverse
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
|
|
||||||
from storages.backends.s3boto3 import S3Boto3Storage
|
from storages.backends.s3 import S3Storage
|
||||||
|
|
||||||
from bookwyrm import models, storage_backends
|
from bookwyrm import models
|
||||||
from bookwyrm.models.bookwyrm_export_job import BookwyrmExportJob
|
from bookwyrm.models.bookwyrm_export_job import BookwyrmExportJob
|
||||||
from bookwyrm import settings
|
from bookwyrm import settings
|
||||||
|
|
||||||
|
@ -220,17 +220,16 @@ class ExportUser(View):
|
||||||
class ExportArchive(View):
|
class ExportArchive(View):
|
||||||
"""Serve the archive file"""
|
"""Serve the archive file"""
|
||||||
|
|
||||||
# TODO: how do we serve s3 files?
|
|
||||||
def get(self, request, archive_id):
|
def get(self, request, archive_id):
|
||||||
"""download user export file"""
|
"""download user export file"""
|
||||||
export = BookwyrmExportJob.objects.get(task_id=archive_id, user=request.user)
|
export = BookwyrmExportJob.objects.get(task_id=archive_id, user=request.user)
|
||||||
|
|
||||||
if isinstance(export.export_data.storage, storage_backends.ExportsS3Storage):
|
if settings.USE_S3:
|
||||||
# make custom_domain None so we can sign the url
|
# make custom_domain None so we can sign the url
|
||||||
# see https://github.com/jschneier/django-storages/issues/944
|
# see https://github.com/jschneier/django-storages/issues/944
|
||||||
storage = S3Boto3Storage(querystring_auth=True, custom_domain=None)
|
storage = S3Storage(querystring_auth=True, custom_domain=None)
|
||||||
try:
|
try:
|
||||||
url = S3Boto3Storage.url(
|
url = S3Storage.url(
|
||||||
storage,
|
storage,
|
||||||
f"/exports/{export.task_id}.tar.gz",
|
f"/exports/{export.task_id}.tar.gz",
|
||||||
expire=settings.S3_SIGNED_URL_EXPIRY,
|
expire=settings.S3_SIGNED_URL_EXPIRY,
|
||||||
|
@ -239,16 +238,18 @@ class ExportArchive(View):
|
||||||
raise Http404()
|
raise Http404()
|
||||||
return redirect(url)
|
return redirect(url)
|
||||||
|
|
||||||
if isinstance(export.export_data.storage, storage_backends.ExportsFileStorage):
|
if settings.USE_AZURE:
|
||||||
try:
|
# not implemented
|
||||||
return HttpResponse(
|
return HttpResponseServerError()
|
||||||
export.export_data,
|
|
||||||
content_type="application/gzip",
|
|
||||||
headers={
|
|
||||||
"Content-Disposition": 'attachment; filename="bookwyrm-account-export.tar.gz"' # pylint: disable=line-too-long
|
|
||||||
},
|
|
||||||
)
|
|
||||||
except FileNotFoundError:
|
|
||||||
raise Http404()
|
|
||||||
|
|
||||||
return HttpResponseServerError()
|
try:
|
||||||
|
return HttpResponse(
|
||||||
|
export.export_data,
|
||||||
|
content_type="application/gzip",
|
||||||
|
headers={
|
||||||
|
# pylint: disable=line-too-long
|
||||||
|
"Content-Disposition": 'attachment; filename="bookwyrm-account-export.tar.gz"'
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except FileNotFoundError:
|
||||||
|
raise Http404()
|
||||||
|
|
|
@ -1,64 +1,64 @@
|
||||||
aiohttp==3.9.4
|
aiohttp==3.9.4
|
||||||
bleach==5.0.1
|
bleach==6.1.0
|
||||||
boto3==1.26.57
|
boto3==1.34.74
|
||||||
bw-file-resubmit==0.6.0rc2
|
bw-file-resubmit==0.6.0rc2
|
||||||
celery==5.3.1
|
celery==5.3.6
|
||||||
colorthief==0.2.1
|
colorthief==0.2.1
|
||||||
Django==3.2.25
|
Django==4.2.11
|
||||||
django-celery-beat==2.5.0
|
django-celery-beat==2.6.0
|
||||||
django-compressor==4.4
|
django-compressor==4.4
|
||||||
django-csp==3.7
|
django-csp==3.8
|
||||||
django-imagekit==4.1.0
|
django-imagekit==5.0.0
|
||||||
django-model-utils==4.3.1
|
django-model-utils==4.4.0
|
||||||
django-oauth-toolkit==2.3.0
|
django-oauth-toolkit==2.3.0
|
||||||
django-pgtrigger==4.11.0
|
django-pgtrigger==4.11.0
|
||||||
django-redis==5.2.0
|
django-sass-processor==1.4
|
||||||
django-sass-processor==1.2.2
|
django-storages==1.14.2
|
||||||
django-storages==1.13.2
|
|
||||||
django-storages[azure]
|
django-storages[azure]
|
||||||
environs==9.5.0
|
environs==11.0.0
|
||||||
flower==2.0.1
|
flower==2.0.1
|
||||||
grpcio==1.57.0 # Not a direct dependency, pinned to get a security fix
|
hiredis==2.3.2
|
||||||
libsass==0.22.0
|
libsass==0.23.0
|
||||||
Markdown==3.4.1
|
Markdown==3.6
|
||||||
opentelemetry-api==1.16.0
|
opentelemetry-api==1.24.0
|
||||||
opentelemetry-exporter-otlp-proto-grpc==1.16.0
|
opentelemetry-exporter-otlp-proto-grpc==1.24.0
|
||||||
opentelemetry-instrumentation-celery==0.37b0
|
opentelemetry-instrumentation-celery==0.45b0
|
||||||
opentelemetry-instrumentation-django==0.37b0
|
opentelemetry-instrumentation-django==0.45b0
|
||||||
opentelemetry-instrumentation-psycopg2==0.37b0
|
opentelemetry-instrumentation-psycopg2==0.45b0
|
||||||
opentelemetry-sdk==1.16.0
|
opentelemetry-sdk==1.24.0
|
||||||
Pillow==10.3.0
|
Pillow==10.3.0
|
||||||
pilkit>=3.0 # dependency of django-imagekit, 2.0 is incompatible with Pillow>=10
|
pilkit>=3.0 # dependency of django-imagekit, 2.0 is incompatible with Pillow>=10
|
||||||
protobuf==3.20.*
|
psycopg2==2.9.9
|
||||||
psycopg2==2.9.5
|
pycryptodome==3.20.0
|
||||||
pycryptodome==3.19.1
|
pyotp==2.9.0
|
||||||
pyotp==2.8.0
|
python-dateutil==2.9.0.post0
|
||||||
python-dateutil==2.8.2
|
qrcode==7.4.2
|
||||||
pytz>=2022.7
|
redis==5.0.3
|
||||||
qrcode==7.3.1
|
|
||||||
redis==4.5.4
|
|
||||||
requests==2.32.0
|
requests==2.32.0
|
||||||
responses==0.22.0
|
responses==0.25.0
|
||||||
s3-tar==0.1.13
|
s3-tar==0.1.13
|
||||||
setuptools>=65.5.1 # Not a direct dependency, pinned to get a security fix
|
|
||||||
tornado==6.3.3 # Not a direct dependency, pinned to get a security fix
|
# Indirect dependencies with version constraints for security fixes
|
||||||
|
grpcio>=1.57.0
|
||||||
|
setuptools>=65.5.1
|
||||||
|
tornado>=6.3.3
|
||||||
|
|
||||||
# Dev
|
# Dev
|
||||||
black==22.*
|
black==22.*
|
||||||
celery-types==0.18.0
|
celery-types==0.22.0
|
||||||
django-stubs[compatible-mypy]==4.2.4
|
django-stubs[compatible-mypy]==4.2.7
|
||||||
mypy==1.5.1
|
mypy==1.7.1
|
||||||
pylint==2.15.0
|
pylint==2.17.7
|
||||||
pytest==6.2.5
|
pytest==8.1.1
|
||||||
pytest-cov==2.10.1
|
pytest-cov==5.0.0
|
||||||
pytest-django==4.1.0
|
pytest-django==4.8.0
|
||||||
pytest-env==0.6.2
|
pytest-env==1.1.3
|
||||||
pytest-xdist==2.3.0
|
pytest-xdist==3.5.0
|
||||||
pytidylib==0.3.2
|
pytidylib==0.3.2
|
||||||
types-bleach==6.0.0.4
|
types-bleach==6.1.0.20240331
|
||||||
types-dataclasses==0.6.6
|
types-dataclasses==0.6.6
|
||||||
types-Markdown==3.4.2.10
|
types-Markdown==3.6.0.20240316
|
||||||
types-Pillow==10.2.0.20240311
|
types-Pillow==10.2.0.20240331
|
||||||
types-psycopg2==2.9.21.11
|
types-psycopg2==2.9.21.20240311
|
||||||
types-python-dateutil==2.8.19.14
|
types-python-dateutil==2.9.0.20240316
|
||||||
types-requests==2.31.0.2
|
types-requests==2.31.0.20240311
|
||||||
|
|
Loading…
Reference in a new issue