From 2007921434d1b9dca2a291fb9fa656f1f8bdcb70 Mon Sep 17 00:00:00 2001 From: Dylann Cordel Date: Tue, 16 Jul 2024 18:51:42 +0200 Subject: [PATCH] Allow use with non-Page models #43 --- docs/customizing/index.rst | 1 + docs/customizing/use-on-snippet.rst | 80 + testproject/home/apps.py | 9 + .../home/migrations/0005_badwolf_mysnippet.py | 1732 +++++++++++++++++ ...ter_badwolf_struct_org_actions_and_more.py | 35 + testproject/home/models.py | 45 +- testproject/home/tests.py | 91 +- testproject/home/views.py | 13 + testproject/testproject/urls.py | 10 +- wagtailseo/models.py | 104 +- 10 files changed, 2083 insertions(+), 37 deletions(-) create mode 100644 docs/customizing/use-on-snippet.rst create mode 100644 testproject/home/apps.py create mode 100644 testproject/home/migrations/0005_badwolf_mysnippet.py create mode 100644 testproject/home/migrations/0006_alter_badwolf_struct_org_actions_and_more.py create mode 100644 testproject/home/views.py diff --git a/docs/customizing/index.rst b/docs/customizing/index.rst index 6a9955c..5be1707 100644 --- a/docs/customizing/index.rst +++ b/docs/customizing/index.rst @@ -5,5 +5,6 @@ Advanced Customization :maxdepth: 2 Custom Metadata + Use on Snippet Custom Editor Interface Django Settings diff --git a/docs/customizing/use-on-snippet.rst b/docs/customizing/use-on-snippet.rst new file mode 100644 index 0000000..383e1b6 --- /dev/null +++ b/docs/customizing/use-on-snippet.rst @@ -0,0 +1,80 @@ +Use on Snippet models +===================== + +If you use your own custom models which do not inherit from Page (aka `Snippet` +in Wagtail), you can inherit from `SeoMixinBase`. + +You'll have to override some properties and method to match your own seo +fields or fallbacks: + +.. code-block:: python + + class MySnippet(SeoMixinBase, models.Model): + promote_panels = SeoMixinBase.seo_panels + + def __str__(self): + return f"my snippet {self.pk or 0}" + + def get_full_url(self) -> str: + """ + Return the full URL (including protocol / domain) to this snippet + """ + path = reverse("my_snippet_detail", kwargs={"pk": self.pk}) + return f"{self.get_site().root_url}/{path}" + + @property + def seo_author(self) -> str: + """ + Gets the name of the author of this page. + """ + return "Tux" + + @property + def seo_published_at(self) -> datetime: + """ + Gets the date this snippet was first published. + """ + return datetime(1122, 1, 1) + + @property + def seo_modified_at(self) -> datetime: + """ + Gets the date this snippet was first published. + """ + return datetime(1204, 3, 31) + + def get_site(self): + """ + Return the Site object that this snippet belongs to. + """ + return Site.objects.first() + +Do not forget to add your instance accessible via a `self` into context data +of your template: + + +.. code-block:: python + + class MySnippetDetailView(DetailView): + model = MySnippet + template_name = "my_snippet.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["self"] = context["object"] + return context + +Now, ensure your template includes meta and struct_data into the head and body: + +.. code-block:: html + + + + + {% include "wagtailseo/meta.html" %} + + + {% include "wagtailseo/struct_data.html" %} +

My Snippet {{ self.pk }}

+ + diff --git a/testproject/home/apps.py b/testproject/home/apps.py new file mode 100644 index 0000000..50f68c8 --- /dev/null +++ b/testproject/home/apps.py @@ -0,0 +1,9 @@ +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class HomeAppConfig(AppConfig): + name = "home" + label = "home" + verbose_name = _("home") + default_auto_field = "django.db.models.AutoField" diff --git a/testproject/home/migrations/0005_badwolf_mysnippet.py b/testproject/home/migrations/0005_badwolf_mysnippet.py new file mode 100644 index 0000000..263e8e2 --- /dev/null +++ b/testproject/home/migrations/0005_badwolf_mysnippet.py @@ -0,0 +1,1732 @@ +# Generated by Django 5.0.7 on 2024-07-16 17:05 + +import django.db.models.deletion +import wagtail.blocks +import wagtail.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("home", "0004_alter_articlepage_struct_org_logo_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="BadWolf", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "canonical_url", + models.URLField( + blank=True, + help_text="Leave blank to use the page's URL.", + max_length=255, + verbose_name="Canonical URL", + ), + ), + ( + "struct_org_type", + models.CharField( + blank=True, + choices=[ + ("Organization", "Organization"), + ("Airline", "Organization > Airline"), + ("Corporation", "Organization > Corporation"), + ( + "EducationalOrganization", + "Organization > EducationalOrganization", + ), + ( + "CollegeOrUniversity", + "Organization > EducationalOrganization > CollegeOrUniversity", + ), + ( + "ElementarySchool", + "Organization > EducationalOrganization > ElementarySchool", + ), + ( + "HighSchool", + "Organization > EducationalOrganization > HighSchool", + ), + ( + "MiddleSchool", + "Organization > EducationalOrganization > MiddleSchool", + ), + ( + "Preschool", + "Organization > EducationalOrganization > Preschool", + ), + ( + "School", + "Organization > EducationalOrganization > School", + ), + ( + "GovernmentOrganization", + "Organization > GovernmentOrganization", + ), + ("LocalBusiness", "Organization > LocalBusiness"), + ( + "AnimalShelter", + "Organization > LocalBusiness > AnimalShelter", + ), + ( + "AutomotiveBusiness", + "Organization > LocalBusiness > AutomotiveBusiness", + ), + ( + "AutoBodyShop", + "Organization > LocalBusiness > AutomotiveBusiness > AutoBodyShop", + ), + ( + "AutoDealer", + "Organization > LocalBusiness > AutomotiveBusiness > AutoDealer", + ), + ( + "AutoPartsStore", + "Organization > LocalBusiness > AutomotiveBusiness > AutoPartsStore", + ), + ( + "AutoRental", + "Organization > LocalBusiness > AutomotiveBusiness > AutoRental", + ), + ( + "AutoRepair", + "Organization > LocalBusiness > AutomotiveBusiness > AutoRepair", + ), + ( + "AutoWash", + "Organization > LocalBusiness > AutomotiveBusiness > AutoWash", + ), + ( + "GasStation", + "Organization > LocalBusiness > AutomotiveBusiness > GasStation", + ), + ( + "MotorcycleDealer", + "Organization > LocalBusiness > AutomotiveBusiness > MotorcycleDealer", + ), + ( + "MotorcycleRepair", + "Organization > LocalBusiness > AutomotiveBusiness > MotorcycleRepair", + ), + ("ChildCare", "Organization > LocalBusiness > ChildCare"), + ("Dentist", "Organization > LocalBusiness > Dentist"), + ( + "DryCleaningOrLaundry", + "Organization > LocalBusiness > DryCleaningOrLaundry", + ), + ( + "EmergencyService", + "Organization > LocalBusiness > EmergencyService", + ), + ( + "FireStation", + "Organization > LocalBusiness > EmergencyService > FireStation", + ), + ( + "Hospital", + "Organization > LocalBusiness > EmergencyService > Hospital", + ), + ( + "PoliceStation", + "Organization > LocalBusiness > EmergencyService > PoliceStation", + ), + ( + "EmploymentAgency", + "Organization > LocalBusiness > EmploymentAgency", + ), + ( + "EntertainmentBusiness", + "Organization > LocalBusiness > EntertainmentBusiness", + ), + ( + "AdultEntertainment", + "Organization > LocalBusiness > EntertainmentBusiness > AdultEntertainment", + ), + ( + "AmusementPark", + "Organization > LocalBusiness > EntertainmentBusiness > AmusementPark", + ), + ( + "ArtGallery", + "Organization > LocalBusiness > EntertainmentBusiness > ArtGallery", + ), + ( + "Casino", + "Organization > LocalBusiness > EntertainmentBusiness > Casino", + ), + ( + "ComedyClub", + "Organization > LocalBusiness > EntertainmentBusiness > ComedyClub", + ), + ( + "MovieTheater", + "Organization > LocalBusiness > EntertainmentBusiness > MovieTheater", + ), + ( + "NightClub", + "Organization > LocalBusiness > EntertainmentBusiness > NightClub", + ), + ( + "FinancialService", + "Organization > LocalBusiness > FinancialService", + ), + ( + "AccountingService", + "Organization > LocalBusiness > FinancialService > AccountingService", + ), + ( + "AutomatedTeller", + "Organization > LocalBusiness > FinancialService > AutomatedTeller", + ), + ( + "BankOrCreditUnion", + "Organization > LocalBusiness > FinancialService > BankOrCreditUnion", + ), + ( + "InsuranceAgency", + "Organization > LocalBusiness > FinancialService > InsuranceAgency", + ), + ( + "FoodEstablishment", + "Organization > LocalBusiness > FoodEstablishment", + ), + ( + "Bakery", + "Organization > LocalBusiness > FoodEstablishment > Bakery", + ), + ( + "BarOrPub", + "Organization > LocalBusiness > FoodEstablishment > BarOrPub", + ), + ( + "Brewery", + "Organization > LocalBusiness > FoodEstablishment > Brewery", + ), + ( + "CafeOrCoffeeShop", + "Organization > LocalBusiness > FoodEstablishment > CafeOrCoffeeShop", + ), + ( + "FastFoodRestaurant", + "Organization > LocalBusiness > FoodEstablishment > FastFoodRestaurant", + ), + ( + "IceCreamShop", + "Organization > LocalBusiness > FoodEstablishment > IceCreamShop", + ), + ( + "Restaurant", + "Organization > LocalBusiness > FoodEstablishment > Restaurant", + ), + ( + "Winery", + "Organization > LocalBusiness > FoodEstablishment > Winery", + ), + ( + "GovernmentOffice", + "Organization > LocalBusiness > GovernmentOffice", + ), + ( + "PostOffice", + "Organization > LocalBusiness > GovernmentOffice > PostOffice", + ), + ( + "HealthAndBeautyBusiness", + "Organization > LocalBusiness > HealthAndBeautyBusiness", + ), + ( + "BeautySalon", + "Organization > LocalBusiness > HealthAndBeautyBusiness > BeautySalon", + ), + ( + "DaySpa", + "Organization > LocalBusiness > HealthAndBeautyBusiness > DaySpa", + ), + ( + "HairSalon", + "Organization > LocalBusiness > HealthAndBeautyBusiness > HairSalon", + ), + ( + "HealthClub", + "Organization > LocalBusiness > HealthAndBeautyBusiness > HealthClub", + ), + ( + "NailSalon", + "Organization > LocalBusiness > HealthAndBeautyBusiness > NailSalon", + ), + ( + "TattooParlor", + "Organization > LocalBusiness > HealthAndBeautyBusiness > TattooParlor", + ), + ( + "HomeAndConstructionBusiness", + "Organization > LocalBusiness > HomeAndConstructionBusiness", + ), + ( + "Electrician", + "Organization > LocalBusiness > HomeAndConstructionBusiness > Electrician", + ), + ( + "GeneralContractor", + "Organization > LocalBusiness > HomeAndConstructionBusiness > GeneralContractor", + ), + ( + "HVACBusiness", + "Organization > LocalBusiness > HomeAndConstructionBusiness > HVACBusiness", + ), + ( + "HousePainter", + "Organization > LocalBusiness > HomeAndConstructionBusiness > HousePainter", + ), + ( + "Locksmith", + "Organization > LocalBusiness > HomeAndConstructionBusiness > Locksmith", + ), + ( + "MovingCompany", + "Organization > LocalBusiness > HomeAndConstructionBusiness > MovingCompany", + ), + ( + "Plumber", + "Organization > LocalBusiness > HomeAndConstructionBusiness > Plumber", + ), + ( + "RoofingContractor", + "Organization > LocalBusiness > HomeAndConstructionBusiness > RoofingContractor", + ), + ( + "InternetCafe", + "Organization > LocalBusiness > InternetCafe", + ), + ( + "LegalService", + "Organization > LocalBusiness > LegalService", + ), + ( + "Attorney", + "Organization > LocalBusiness > LegalService > Attorney", + ), + ( + "Notary", + "Organization > LocalBusiness > LegalService > Notary", + ), + ("Library", "Organization > LocalBusiness > Library"), + ( + "LodgingBusiness", + "Organization > LocalBusiness > LodgingBusiness", + ), + ( + "BedAndBreakfast", + "Organization > LocalBusiness > LodgingBusiness > BedAndBreakfast", + ), + ( + "Campground", + "Organization > LocalBusiness > LodgingBusiness > Campground", + ), + ( + "Hostel", + "Organization > LocalBusiness > LodgingBusiness > Hostel", + ), + ( + "Hotel", + "Organization > LocalBusiness > LodgingBusiness > Hotel", + ), + ( + "Motel", + "Organization > LocalBusiness > LodgingBusiness > Motel", + ), + ( + "Resort", + "Organization > LocalBusiness > LodgingBusiness > Resort", + ), + ( + "ProfessionalService", + "Organization > LocalBusiness > ProfessionalService", + ), + ( + "RadioStation", + "Organization > LocalBusiness > RadioStation", + ), + ( + "RealEstateAgent", + "Organization > LocalBusiness > RealEstateAgent", + ), + ( + "RecyclingCenter", + "Organization > LocalBusiness > RecyclingCenter", + ), + ( + "SelfStorage", + "Organization > LocalBusiness > SelfStorage", + ), + ( + "ShoppingCenter", + "Organization > LocalBusiness > ShoppingCenter", + ), + ( + "SportsActivityLocation", + "Organization > LocalBusiness > SportsActivityLocation", + ), + ( + "BowlingAlley", + "Organization > LocalBusiness > SportsActivityLocation > BowlingAlley", + ), + ( + "ExerciseGym", + "Organization > LocalBusiness > SportsActivityLocation > ExerciseGym", + ), + ( + "GolfCourse", + "Organization > LocalBusiness > SportsActivityLocation > GolfCourse", + ), + ( + "HealthClub", + "Organization > LocalBusiness > SportsActivityLocation > HealthClub", + ), + ( + "PublicSwimmingPool", + "Organization > LocalBusiness > SportsActivityLocation > PublicSwimmingPool", + ), + ( + "SkiResort", + "Organization > LocalBusiness > SportsActivityLocation > SkiResort", + ), + ( + "SportsClub", + "Organization > LocalBusiness > SportsActivityLocation > SportsClub", + ), + ( + "StadiumOrArena", + "Organization > LocalBusiness > SportsActivityLocation > StadiumOrArena", + ), + ( + "TennisComplex", + "Organization > LocalBusiness > SportsActivityLocation > TennisComplex", + ), + ("Store", "Organization > LocalBusiness > Store"), + ( + "AutoPartsStore", + "Organization > LocalBusiness > Store > AutoPartsStore", + ), + ( + "BikeStore", + "Organization > LocalBusiness > Store > BikeStore", + ), + ( + "BookStore", + "Organization > LocalBusiness > Store > BookStore", + ), + ( + "ClothingStore", + "Organization > LocalBusiness > Store > ClothingStore", + ), + ( + "ComputerStore", + "Organization > LocalBusiness > Store > ComputerStore", + ), + ( + "ConvenienceStore", + "Organization > LocalBusiness > Store > ConvenienceStore", + ), + ( + "DepartmentStore", + "Organization > LocalBusiness > Store > DepartmentStore", + ), + ( + "ElectronicsStore", + "Organization > LocalBusiness > Store > ElectronicsStore", + ), + ( + "Florist", + "Organization > LocalBusiness > Store > Florist", + ), + ( + "FurnitureStore", + "Organization > LocalBusiness > Store > FurnitureStore", + ), + ( + "GardenStore", + "Organization > LocalBusiness > Store > GardenStore", + ), + ( + "GroceryStore", + "Organization > LocalBusiness > Store > GroceryStore", + ), + ( + "HardwareStore", + "Organization > LocalBusiness > Store > HardwareStore", + ), + ( + "HobbyShop", + "Organization > LocalBusiness > Store > HobbyShop", + ), + ( + "HomeGoodsStore", + "Organization > LocalBusiness > Store > HomeGoodsStore", + ), + ( + "JewelryStore", + "Organization > LocalBusiness > Store > JewelryStore", + ), + ( + "LiquorStore", + "Organization > LocalBusiness > Store > LiquorStore", + ), + ( + "MensClothingStore", + "Organization > LocalBusiness > Store > MensClothingStore", + ), + ( + "MobilePhoneStore", + "Organization > LocalBusiness > Store > MobilePhoneStore", + ), + ( + "MovieRentalStore", + "Organization > LocalBusiness > Store > MovieRentalStore", + ), + ( + "MusicStore", + "Organization > LocalBusiness > Store > MusicStore", + ), + ( + "OfficeEquipmentStore", + "Organization > LocalBusiness > Store > OfficeEquipmentStore", + ), + ( + "OutletStore", + "Organization > LocalBusiness > Store > OutletStore", + ), + ( + "PawnShop", + "Organization > LocalBusiness > Store > PawnShop", + ), + ( + "PetStore", + "Organization > LocalBusiness > Store > PetStore", + ), + ( + "ShoeStore", + "Organization > LocalBusiness > Store > ShoeStore", + ), + ( + "SportingGoodsStore", + "Organization > LocalBusiness > Store > SportingGoodsStore", + ), + ( + "TireShop", + "Organization > LocalBusiness > Store > TireShop", + ), + ( + "ToyStore", + "Organization > LocalBusiness > Store > ToyStore", + ), + ( + "WholesaleStore", + "Organization > LocalBusiness > Store > WholesaleStore", + ), + ( + "TelevisionStation", + "Organization > LocalBusiness > TelevisionStation", + ), + ( + "TouristInformationCenter", + "Organization > LocalBusiness > TouristInformationCenter", + ), + ( + "TravelAgency", + "Organization > LocalBusiness > TravelAgency", + ), + ( + "MedicalOrganization", + "Organization > MedicalOrganization", + ), + ("Dentist", "Organization > MedicalOrganization > Dentist"), + ( + "Hospital", + "Organization > MedicalOrganization > Hospital", + ), + ( + "Pharmacy", + "Organization > MedicalOrganization > Pharmacy", + ), + ( + "Physician", + "Organization > MedicalOrganization > Physician", + ), + ("NGO", "Organization > NGO"), + ("PerformingGroup", "Organization > PerformingGroup"), + ( + "DanceGroup", + "Organization > PerformingGroup > DanceGroup", + ), + ( + "MusicGroup", + "Organization > PerformingGroup > MusicGroup", + ), + ( + "TheaterGroup", + "Organization > PerformingGroup > TheaterGroup", + ), + ("SportsOrganization", "Organization > SportsOrganization"), + ( + "SportsTeam", + "Organization > SportsOrganization > SportsTeam", + ), + ], + default="", + help_text="If blank, no structured data will be used on this page.", + max_length=255, + verbose_name="Organization type", + ), + ), + ( + "struct_org_name", + models.CharField( + blank=True, + default="", + help_text="Leave blank to use the site name in Settings > Sites", + max_length=255, + verbose_name="Organization name", + ), + ), + ( + "struct_org_phone", + models.CharField( + blank=True, + help_text="Include country code for best results. For example: +1-216-555-8000", + max_length=255, + verbose_name="Telephone number", + ), + ), + ( + "struct_org_address_street", + models.CharField( + blank=True, + help_text="House number and street. For example, 55 Public Square Suite 1710", + max_length=255, + verbose_name="Street address", + ), + ), + ( + "struct_org_address_locality", + models.CharField( + blank=True, + help_text="City or locality. For example, Cleveland", + max_length=255, + verbose_name="City", + ), + ), + ( + "struct_org_address_region", + models.CharField( + blank=True, + help_text="State, province, county, or region. For example, OH", + max_length=255, + verbose_name="State", + ), + ), + ( + "struct_org_address_postal", + models.CharField( + blank=True, + help_text="Zip or postal code. For example, 44113", + max_length=255, + verbose_name="Postal code", + ), + ), + ( + "struct_org_address_country", + models.CharField( + blank=True, + help_text="For example, USA. Two-letter ISO 3166-1 alpha-2 country code is also acceptable https://en.wikipedia.org/wiki/ISO_3166-1", + max_length=255, + verbose_name="Country", + ), + ), + ( + "struct_org_geo_lat", + models.DecimalField( + blank=True, + decimal_places=8, + max_digits=11, + null=True, + verbose_name="Geographic latitude", + ), + ), + ( + "struct_org_geo_lng", + models.DecimalField( + blank=True, + decimal_places=8, + max_digits=11, + null=True, + verbose_name="Geographic longitude", + ), + ), + ( + "struct_org_hours", + wagtail.fields.StreamField( + [ + ( + "hours", + wagtail.blocks.StructBlock( + [ + ( + "days", + wagtail.blocks.MultipleChoiceBlock( + choices=[ + ("Monday", "Monday"), + ("Tuesday", "Tuesday"), + ("Wednesday", "Wednesday"), + ("Thursday", "Thursday"), + ("Friday", "Friday"), + ("Saturday", "Saturday"), + ("Sunday", "Sunday"), + ], + help_text="For late night hours past 23:59, define each day in a separate block.", + verbose_name="Days", + ), + ), + ( + "start_time", + wagtail.blocks.TimeBlock( + verbose_name="Opening time" + ), + ), + ( + "end_time", + wagtail.blocks.TimeBlock( + verbose_name="Closing time" + ), + ), + ] + ), + ) + ], + blank=True, + verbose_name="Hours of operation", + ), + ), + ( + "struct_org_actions", + wagtail.fields.StreamField( + [ + ( + "actions", + wagtail.blocks.StructBlock( + [ + ( + "action_type", + wagtail.blocks.ChoiceBlock( + choices=[ + ("OrderAction", "OrderAction"), + ("ReserveAction", "ReserveAction"), + ], + verbose_name="Action Type", + ), + ), + ( + "target", + wagtail.blocks.URLBlock( + verbose_name="Target URL" + ), + ), + ( + "language", + wagtail.blocks.CharBlock( + default="en-US", + help_text="If the action is offered in multiple languages, create separate actions for each language.", + verbose_name="Language", + ), + ), + ( + "result_type", + wagtail.blocks.ChoiceBlock( + choices=[ + ("Reservation", "Reservation"), + ( + "BusReservation", + "BusReservation", + ), + ( + "EventReservation", + "EventReservation", + ), + ( + "FlightReservation", + "FlightReservation", + ), + ( + "FoodEstablishmentReservation", + "FoodEstablishmentReservation", + ), + ( + "LodgingReservation", + "LodgingReservation", + ), + ( + "RentalCarReservation", + "RentalCarReservation", + ), + ( + "ReservationPackage", + "ReservationPackage", + ), + ( + "TaxiReservation", + "TaxiReservation", + ), + ( + "TrainReservation", + "TrainReservation", + ), + ], + help_text="Leave blank for OrderAction", + required=False, + verbose_name="Result Type", + ), + ), + ( + "result_name", + wagtail.blocks.CharBlock( + help_text='Example: "Reserve a table", "Book an appointment", etc.', + required=False, + verbose_name="Result Name", + ), + ), + ( + "extra_json", + wagtail.blocks.RawHTMLBlock( + form_classname="monospace", + help_text="Additional JSON-LD inserted into the Action dictionary. Must be properties of https://schema.org/Action.", + required=False, + verbose_name="Additional action markup", + ), + ), + ] + ), + ) + ], + blank=True, + verbose_name="Actions", + ), + ), + ( + "struct_org_extra_json", + models.TextField( + blank=True, + help_text="Additional JSON-LD inserted into the Organization dictionary. Must be properties of https://schema.org/Organization or the selected organization type.", + verbose_name="Additional Organization markup", + ), + ), + ( + "og_image", + models.ForeignKey( + blank=True, + help_text="Shown when linking to this page on social media.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="wagtailimages.image", + verbose_name="Preview image", + ), + ), + ( + "struct_org_image", + models.ForeignKey( + blank=True, + help_text="A photo of the facility. This photo will be cropped to 1:1, 4:3, and 16:9 aspect ratios automatically.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="wagtailimages.image", + verbose_name="Photo of Organization", + ), + ), + ( + "struct_org_logo", + models.ForeignKey( + blank=True, + help_text="Logo representative of the organisation. Must be 112x112px minimum; take note that it will be displayed on a white background", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="wagtailimages.image", + verbose_name="Organization logo", + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="MySnippet", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "canonical_url", + models.URLField( + blank=True, + help_text="Leave blank to use the page's URL.", + max_length=255, + verbose_name="Canonical URL", + ), + ), + ( + "struct_org_type", + models.CharField( + blank=True, + choices=[ + ("Organization", "Organization"), + ("Airline", "Organization > Airline"), + ("Corporation", "Organization > Corporation"), + ( + "EducationalOrganization", + "Organization > EducationalOrganization", + ), + ( + "CollegeOrUniversity", + "Organization > EducationalOrganization > CollegeOrUniversity", + ), + ( + "ElementarySchool", + "Organization > EducationalOrganization > ElementarySchool", + ), + ( + "HighSchool", + "Organization > EducationalOrganization > HighSchool", + ), + ( + "MiddleSchool", + "Organization > EducationalOrganization > MiddleSchool", + ), + ( + "Preschool", + "Organization > EducationalOrganization > Preschool", + ), + ( + "School", + "Organization > EducationalOrganization > School", + ), + ( + "GovernmentOrganization", + "Organization > GovernmentOrganization", + ), + ("LocalBusiness", "Organization > LocalBusiness"), + ( + "AnimalShelter", + "Organization > LocalBusiness > AnimalShelter", + ), + ( + "AutomotiveBusiness", + "Organization > LocalBusiness > AutomotiveBusiness", + ), + ( + "AutoBodyShop", + "Organization > LocalBusiness > AutomotiveBusiness > AutoBodyShop", + ), + ( + "AutoDealer", + "Organization > LocalBusiness > AutomotiveBusiness > AutoDealer", + ), + ( + "AutoPartsStore", + "Organization > LocalBusiness > AutomotiveBusiness > AutoPartsStore", + ), + ( + "AutoRental", + "Organization > LocalBusiness > AutomotiveBusiness > AutoRental", + ), + ( + "AutoRepair", + "Organization > LocalBusiness > AutomotiveBusiness > AutoRepair", + ), + ( + "AutoWash", + "Organization > LocalBusiness > AutomotiveBusiness > AutoWash", + ), + ( + "GasStation", + "Organization > LocalBusiness > AutomotiveBusiness > GasStation", + ), + ( + "MotorcycleDealer", + "Organization > LocalBusiness > AutomotiveBusiness > MotorcycleDealer", + ), + ( + "MotorcycleRepair", + "Organization > LocalBusiness > AutomotiveBusiness > MotorcycleRepair", + ), + ("ChildCare", "Organization > LocalBusiness > ChildCare"), + ("Dentist", "Organization > LocalBusiness > Dentist"), + ( + "DryCleaningOrLaundry", + "Organization > LocalBusiness > DryCleaningOrLaundry", + ), + ( + "EmergencyService", + "Organization > LocalBusiness > EmergencyService", + ), + ( + "FireStation", + "Organization > LocalBusiness > EmergencyService > FireStation", + ), + ( + "Hospital", + "Organization > LocalBusiness > EmergencyService > Hospital", + ), + ( + "PoliceStation", + "Organization > LocalBusiness > EmergencyService > PoliceStation", + ), + ( + "EmploymentAgency", + "Organization > LocalBusiness > EmploymentAgency", + ), + ( + "EntertainmentBusiness", + "Organization > LocalBusiness > EntertainmentBusiness", + ), + ( + "AdultEntertainment", + "Organization > LocalBusiness > EntertainmentBusiness > AdultEntertainment", + ), + ( + "AmusementPark", + "Organization > LocalBusiness > EntertainmentBusiness > AmusementPark", + ), + ( + "ArtGallery", + "Organization > LocalBusiness > EntertainmentBusiness > ArtGallery", + ), + ( + "Casino", + "Organization > LocalBusiness > EntertainmentBusiness > Casino", + ), + ( + "ComedyClub", + "Organization > LocalBusiness > EntertainmentBusiness > ComedyClub", + ), + ( + "MovieTheater", + "Organization > LocalBusiness > EntertainmentBusiness > MovieTheater", + ), + ( + "NightClub", + "Organization > LocalBusiness > EntertainmentBusiness > NightClub", + ), + ( + "FinancialService", + "Organization > LocalBusiness > FinancialService", + ), + ( + "AccountingService", + "Organization > LocalBusiness > FinancialService > AccountingService", + ), + ( + "AutomatedTeller", + "Organization > LocalBusiness > FinancialService > AutomatedTeller", + ), + ( + "BankOrCreditUnion", + "Organization > LocalBusiness > FinancialService > BankOrCreditUnion", + ), + ( + "InsuranceAgency", + "Organization > LocalBusiness > FinancialService > InsuranceAgency", + ), + ( + "FoodEstablishment", + "Organization > LocalBusiness > FoodEstablishment", + ), + ( + "Bakery", + "Organization > LocalBusiness > FoodEstablishment > Bakery", + ), + ( + "BarOrPub", + "Organization > LocalBusiness > FoodEstablishment > BarOrPub", + ), + ( + "Brewery", + "Organization > LocalBusiness > FoodEstablishment > Brewery", + ), + ( + "CafeOrCoffeeShop", + "Organization > LocalBusiness > FoodEstablishment > CafeOrCoffeeShop", + ), + ( + "FastFoodRestaurant", + "Organization > LocalBusiness > FoodEstablishment > FastFoodRestaurant", + ), + ( + "IceCreamShop", + "Organization > LocalBusiness > FoodEstablishment > IceCreamShop", + ), + ( + "Restaurant", + "Organization > LocalBusiness > FoodEstablishment > Restaurant", + ), + ( + "Winery", + "Organization > LocalBusiness > FoodEstablishment > Winery", + ), + ( + "GovernmentOffice", + "Organization > LocalBusiness > GovernmentOffice", + ), + ( + "PostOffice", + "Organization > LocalBusiness > GovernmentOffice > PostOffice", + ), + ( + "HealthAndBeautyBusiness", + "Organization > LocalBusiness > HealthAndBeautyBusiness", + ), + ( + "BeautySalon", + "Organization > LocalBusiness > HealthAndBeautyBusiness > BeautySalon", + ), + ( + "DaySpa", + "Organization > LocalBusiness > HealthAndBeautyBusiness > DaySpa", + ), + ( + "HairSalon", + "Organization > LocalBusiness > HealthAndBeautyBusiness > HairSalon", + ), + ( + "HealthClub", + "Organization > LocalBusiness > HealthAndBeautyBusiness > HealthClub", + ), + ( + "NailSalon", + "Organization > LocalBusiness > HealthAndBeautyBusiness > NailSalon", + ), + ( + "TattooParlor", + "Organization > LocalBusiness > HealthAndBeautyBusiness > TattooParlor", + ), + ( + "HomeAndConstructionBusiness", + "Organization > LocalBusiness > HomeAndConstructionBusiness", + ), + ( + "Electrician", + "Organization > LocalBusiness > HomeAndConstructionBusiness > Electrician", + ), + ( + "GeneralContractor", + "Organization > LocalBusiness > HomeAndConstructionBusiness > GeneralContractor", + ), + ( + "HVACBusiness", + "Organization > LocalBusiness > HomeAndConstructionBusiness > HVACBusiness", + ), + ( + "HousePainter", + "Organization > LocalBusiness > HomeAndConstructionBusiness > HousePainter", + ), + ( + "Locksmith", + "Organization > LocalBusiness > HomeAndConstructionBusiness > Locksmith", + ), + ( + "MovingCompany", + "Organization > LocalBusiness > HomeAndConstructionBusiness > MovingCompany", + ), + ( + "Plumber", + "Organization > LocalBusiness > HomeAndConstructionBusiness > Plumber", + ), + ( + "RoofingContractor", + "Organization > LocalBusiness > HomeAndConstructionBusiness > RoofingContractor", + ), + ( + "InternetCafe", + "Organization > LocalBusiness > InternetCafe", + ), + ( + "LegalService", + "Organization > LocalBusiness > LegalService", + ), + ( + "Attorney", + "Organization > LocalBusiness > LegalService > Attorney", + ), + ( + "Notary", + "Organization > LocalBusiness > LegalService > Notary", + ), + ("Library", "Organization > LocalBusiness > Library"), + ( + "LodgingBusiness", + "Organization > LocalBusiness > LodgingBusiness", + ), + ( + "BedAndBreakfast", + "Organization > LocalBusiness > LodgingBusiness > BedAndBreakfast", + ), + ( + "Campground", + "Organization > LocalBusiness > LodgingBusiness > Campground", + ), + ( + "Hostel", + "Organization > LocalBusiness > LodgingBusiness > Hostel", + ), + ( + "Hotel", + "Organization > LocalBusiness > LodgingBusiness > Hotel", + ), + ( + "Motel", + "Organization > LocalBusiness > LodgingBusiness > Motel", + ), + ( + "Resort", + "Organization > LocalBusiness > LodgingBusiness > Resort", + ), + ( + "ProfessionalService", + "Organization > LocalBusiness > ProfessionalService", + ), + ( + "RadioStation", + "Organization > LocalBusiness > RadioStation", + ), + ( + "RealEstateAgent", + "Organization > LocalBusiness > RealEstateAgent", + ), + ( + "RecyclingCenter", + "Organization > LocalBusiness > RecyclingCenter", + ), + ( + "SelfStorage", + "Organization > LocalBusiness > SelfStorage", + ), + ( + "ShoppingCenter", + "Organization > LocalBusiness > ShoppingCenter", + ), + ( + "SportsActivityLocation", + "Organization > LocalBusiness > SportsActivityLocation", + ), + ( + "BowlingAlley", + "Organization > LocalBusiness > SportsActivityLocation > BowlingAlley", + ), + ( + "ExerciseGym", + "Organization > LocalBusiness > SportsActivityLocation > ExerciseGym", + ), + ( + "GolfCourse", + "Organization > LocalBusiness > SportsActivityLocation > GolfCourse", + ), + ( + "HealthClub", + "Organization > LocalBusiness > SportsActivityLocation > HealthClub", + ), + ( + "PublicSwimmingPool", + "Organization > LocalBusiness > SportsActivityLocation > PublicSwimmingPool", + ), + ( + "SkiResort", + "Organization > LocalBusiness > SportsActivityLocation > SkiResort", + ), + ( + "SportsClub", + "Organization > LocalBusiness > SportsActivityLocation > SportsClub", + ), + ( + "StadiumOrArena", + "Organization > LocalBusiness > SportsActivityLocation > StadiumOrArena", + ), + ( + "TennisComplex", + "Organization > LocalBusiness > SportsActivityLocation > TennisComplex", + ), + ("Store", "Organization > LocalBusiness > Store"), + ( + "AutoPartsStore", + "Organization > LocalBusiness > Store > AutoPartsStore", + ), + ( + "BikeStore", + "Organization > LocalBusiness > Store > BikeStore", + ), + ( + "BookStore", + "Organization > LocalBusiness > Store > BookStore", + ), + ( + "ClothingStore", + "Organization > LocalBusiness > Store > ClothingStore", + ), + ( + "ComputerStore", + "Organization > LocalBusiness > Store > ComputerStore", + ), + ( + "ConvenienceStore", + "Organization > LocalBusiness > Store > ConvenienceStore", + ), + ( + "DepartmentStore", + "Organization > LocalBusiness > Store > DepartmentStore", + ), + ( + "ElectronicsStore", + "Organization > LocalBusiness > Store > ElectronicsStore", + ), + ( + "Florist", + "Organization > LocalBusiness > Store > Florist", + ), + ( + "FurnitureStore", + "Organization > LocalBusiness > Store > FurnitureStore", + ), + ( + "GardenStore", + "Organization > LocalBusiness > Store > GardenStore", + ), + ( + "GroceryStore", + "Organization > LocalBusiness > Store > GroceryStore", + ), + ( + "HardwareStore", + "Organization > LocalBusiness > Store > HardwareStore", + ), + ( + "HobbyShop", + "Organization > LocalBusiness > Store > HobbyShop", + ), + ( + "HomeGoodsStore", + "Organization > LocalBusiness > Store > HomeGoodsStore", + ), + ( + "JewelryStore", + "Organization > LocalBusiness > Store > JewelryStore", + ), + ( + "LiquorStore", + "Organization > LocalBusiness > Store > LiquorStore", + ), + ( + "MensClothingStore", + "Organization > LocalBusiness > Store > MensClothingStore", + ), + ( + "MobilePhoneStore", + "Organization > LocalBusiness > Store > MobilePhoneStore", + ), + ( + "MovieRentalStore", + "Organization > LocalBusiness > Store > MovieRentalStore", + ), + ( + "MusicStore", + "Organization > LocalBusiness > Store > MusicStore", + ), + ( + "OfficeEquipmentStore", + "Organization > LocalBusiness > Store > OfficeEquipmentStore", + ), + ( + "OutletStore", + "Organization > LocalBusiness > Store > OutletStore", + ), + ( + "PawnShop", + "Organization > LocalBusiness > Store > PawnShop", + ), + ( + "PetStore", + "Organization > LocalBusiness > Store > PetStore", + ), + ( + "ShoeStore", + "Organization > LocalBusiness > Store > ShoeStore", + ), + ( + "SportingGoodsStore", + "Organization > LocalBusiness > Store > SportingGoodsStore", + ), + ( + "TireShop", + "Organization > LocalBusiness > Store > TireShop", + ), + ( + "ToyStore", + "Organization > LocalBusiness > Store > ToyStore", + ), + ( + "WholesaleStore", + "Organization > LocalBusiness > Store > WholesaleStore", + ), + ( + "TelevisionStation", + "Organization > LocalBusiness > TelevisionStation", + ), + ( + "TouristInformationCenter", + "Organization > LocalBusiness > TouristInformationCenter", + ), + ( + "TravelAgency", + "Organization > LocalBusiness > TravelAgency", + ), + ( + "MedicalOrganization", + "Organization > MedicalOrganization", + ), + ("Dentist", "Organization > MedicalOrganization > Dentist"), + ( + "Hospital", + "Organization > MedicalOrganization > Hospital", + ), + ( + "Pharmacy", + "Organization > MedicalOrganization > Pharmacy", + ), + ( + "Physician", + "Organization > MedicalOrganization > Physician", + ), + ("NGO", "Organization > NGO"), + ("PerformingGroup", "Organization > PerformingGroup"), + ( + "DanceGroup", + "Organization > PerformingGroup > DanceGroup", + ), + ( + "MusicGroup", + "Organization > PerformingGroup > MusicGroup", + ), + ( + "TheaterGroup", + "Organization > PerformingGroup > TheaterGroup", + ), + ("SportsOrganization", "Organization > SportsOrganization"), + ( + "SportsTeam", + "Organization > SportsOrganization > SportsTeam", + ), + ], + default="", + help_text="If blank, no structured data will be used on this page.", + max_length=255, + verbose_name="Organization type", + ), + ), + ( + "struct_org_name", + models.CharField( + blank=True, + default="", + help_text="Leave blank to use the site name in Settings > Sites", + max_length=255, + verbose_name="Organization name", + ), + ), + ( + "struct_org_phone", + models.CharField( + blank=True, + help_text="Include country code for best results. For example: +1-216-555-8000", + max_length=255, + verbose_name="Telephone number", + ), + ), + ( + "struct_org_address_street", + models.CharField( + blank=True, + help_text="House number and street. For example, 55 Public Square Suite 1710", + max_length=255, + verbose_name="Street address", + ), + ), + ( + "struct_org_address_locality", + models.CharField( + blank=True, + help_text="City or locality. For example, Cleveland", + max_length=255, + verbose_name="City", + ), + ), + ( + "struct_org_address_region", + models.CharField( + blank=True, + help_text="State, province, county, or region. For example, OH", + max_length=255, + verbose_name="State", + ), + ), + ( + "struct_org_address_postal", + models.CharField( + blank=True, + help_text="Zip or postal code. For example, 44113", + max_length=255, + verbose_name="Postal code", + ), + ), + ( + "struct_org_address_country", + models.CharField( + blank=True, + help_text="For example, USA. Two-letter ISO 3166-1 alpha-2 country code is also acceptable https://en.wikipedia.org/wiki/ISO_3166-1", + max_length=255, + verbose_name="Country", + ), + ), + ( + "struct_org_geo_lat", + models.DecimalField( + blank=True, + decimal_places=8, + max_digits=11, + null=True, + verbose_name="Geographic latitude", + ), + ), + ( + "struct_org_geo_lng", + models.DecimalField( + blank=True, + decimal_places=8, + max_digits=11, + null=True, + verbose_name="Geographic longitude", + ), + ), + ( + "struct_org_hours", + wagtail.fields.StreamField( + [ + ( + "hours", + wagtail.blocks.StructBlock( + [ + ( + "days", + wagtail.blocks.MultipleChoiceBlock( + choices=[ + ("Monday", "Monday"), + ("Tuesday", "Tuesday"), + ("Wednesday", "Wednesday"), + ("Thursday", "Thursday"), + ("Friday", "Friday"), + ("Saturday", "Saturday"), + ("Sunday", "Sunday"), + ], + help_text="For late night hours past 23:59, define each day in a separate block.", + verbose_name="Days", + ), + ), + ( + "start_time", + wagtail.blocks.TimeBlock( + verbose_name="Opening time" + ), + ), + ( + "end_time", + wagtail.blocks.TimeBlock( + verbose_name="Closing time" + ), + ), + ] + ), + ) + ], + blank=True, + verbose_name="Hours of operation", + ), + ), + ( + "struct_org_actions", + wagtail.fields.StreamField( + [ + ( + "actions", + wagtail.blocks.StructBlock( + [ + ( + "action_type", + wagtail.blocks.ChoiceBlock( + choices=[ + ("OrderAction", "OrderAction"), + ("ReserveAction", "ReserveAction"), + ], + verbose_name="Action Type", + ), + ), + ( + "target", + wagtail.blocks.URLBlock( + verbose_name="Target URL" + ), + ), + ( + "language", + wagtail.blocks.CharBlock( + default="en-US", + help_text="If the action is offered in multiple languages, create separate actions for each language.", + verbose_name="Language", + ), + ), + ( + "result_type", + wagtail.blocks.ChoiceBlock( + choices=[ + ("Reservation", "Reservation"), + ( + "BusReservation", + "BusReservation", + ), + ( + "EventReservation", + "EventReservation", + ), + ( + "FlightReservation", + "FlightReservation", + ), + ( + "FoodEstablishmentReservation", + "FoodEstablishmentReservation", + ), + ( + "LodgingReservation", + "LodgingReservation", + ), + ( + "RentalCarReservation", + "RentalCarReservation", + ), + ( + "ReservationPackage", + "ReservationPackage", + ), + ( + "TaxiReservation", + "TaxiReservation", + ), + ( + "TrainReservation", + "TrainReservation", + ), + ], + help_text="Leave blank for OrderAction", + required=False, + verbose_name="Result Type", + ), + ), + ( + "result_name", + wagtail.blocks.CharBlock( + help_text='Example: "Reserve a table", "Book an appointment", etc.', + required=False, + verbose_name="Result Name", + ), + ), + ( + "extra_json", + wagtail.blocks.RawHTMLBlock( + form_classname="monospace", + help_text="Additional JSON-LD inserted into the Action dictionary. Must be properties of https://schema.org/Action.", + required=False, + verbose_name="Additional action markup", + ), + ), + ] + ), + ) + ], + blank=True, + verbose_name="Actions", + ), + ), + ( + "struct_org_extra_json", + models.TextField( + blank=True, + help_text="Additional JSON-LD inserted into the Organization dictionary. Must be properties of https://schema.org/Organization or the selected organization type.", + verbose_name="Additional Organization markup", + ), + ), + ( + "og_image", + models.ForeignKey( + blank=True, + help_text="Shown when linking to this page on social media.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="wagtailimages.image", + verbose_name="Preview image", + ), + ), + ( + "struct_org_image", + models.ForeignKey( + blank=True, + help_text="A photo of the facility. This photo will be cropped to 1:1, 4:3, and 16:9 aspect ratios automatically.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="wagtailimages.image", + verbose_name="Photo of Organization", + ), + ), + ( + "struct_org_logo", + models.ForeignKey( + blank=True, + help_text="Logo representative of the organisation. Must be 112x112px minimum; take note that it will be displayed on a white background", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="wagtailimages.image", + verbose_name="Organization logo", + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/testproject/home/migrations/0006_alter_badwolf_struct_org_actions_and_more.py b/testproject/home/migrations/0006_alter_badwolf_struct_org_actions_and_more.py new file mode 100644 index 0000000..deee025 --- /dev/null +++ b/testproject/home/migrations/0006_alter_badwolf_struct_org_actions_and_more.py @@ -0,0 +1,35 @@ +# Generated by Django 4.0.10 on 2024-07-16 17:09 + +from django.db import migrations +import wagtail.blocks +import wagtail.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0005_badwolf_mysnippet'), + ] + + operations = [ + migrations.AlterField( + model_name='badwolf', + name='struct_org_actions', + field=wagtail.fields.StreamField([('actions', wagtail.blocks.StructBlock([('action_type', wagtail.blocks.ChoiceBlock(choices=[('OrderAction', 'OrderAction'), ('ReserveAction', 'ReserveAction')], verbose_name='Action Type')), ('target', wagtail.blocks.URLBlock(verbose_name='Target URL')), ('language', wagtail.blocks.CharBlock(default='en-US', help_text='If the action is offered in multiple languages, create separate actions for each language.', verbose_name='Language')), ('result_type', wagtail.blocks.ChoiceBlock(choices=[('Reservation', 'Reservation'), ('BusReservation', 'BusReservation'), ('EventReservation', 'EventReservation'), ('FlightReservation', 'FlightReservation'), ('FoodEstablishmentReservation', 'FoodEstablishmentReservation'), ('LodgingReservation', 'LodgingReservation'), ('RentalCarReservation', 'RentalCarReservation'), ('ReservationPackage', 'ReservationPackage'), ('TaxiReservation', 'TaxiReservation'), ('TrainReservation', 'TrainReservation')], help_text='Leave blank for OrderAction', required=False, verbose_name='Result Type')), ('result_name', wagtail.blocks.CharBlock(help_text='Example: "Reserve a table", "Book an appointment", etc.', required=False, verbose_name='Result Name')), ('extra_json', wagtail.blocks.RawHTMLBlock(form_classname='monospace', help_text='Additional JSON-LD inserted into the Action dictionary. Must be properties of https://schema.org/Action.', required=False, verbose_name='Additional action markup'))]))], blank=True, use_json_field=True, verbose_name='Actions'), + ), + migrations.AlterField( + model_name='badwolf', + name='struct_org_hours', + field=wagtail.fields.StreamField([('hours', wagtail.blocks.StructBlock([('days', wagtail.blocks.MultipleChoiceBlock(choices=[('Monday', 'Monday'), ('Tuesday', 'Tuesday'), ('Wednesday', 'Wednesday'), ('Thursday', 'Thursday'), ('Friday', 'Friday'), ('Saturday', 'Saturday'), ('Sunday', 'Sunday')], help_text='For late night hours past 23:59, define each day in a separate block.', verbose_name='Days')), ('start_time', wagtail.blocks.TimeBlock(verbose_name='Opening time')), ('end_time', wagtail.blocks.TimeBlock(verbose_name='Closing time'))]))], blank=True, use_json_field=True, verbose_name='Hours of operation'), + ), + migrations.AlterField( + model_name='mysnippet', + name='struct_org_actions', + field=wagtail.fields.StreamField([('actions', wagtail.blocks.StructBlock([('action_type', wagtail.blocks.ChoiceBlock(choices=[('OrderAction', 'OrderAction'), ('ReserveAction', 'ReserveAction')], verbose_name='Action Type')), ('target', wagtail.blocks.URLBlock(verbose_name='Target URL')), ('language', wagtail.blocks.CharBlock(default='en-US', help_text='If the action is offered in multiple languages, create separate actions for each language.', verbose_name='Language')), ('result_type', wagtail.blocks.ChoiceBlock(choices=[('Reservation', 'Reservation'), ('BusReservation', 'BusReservation'), ('EventReservation', 'EventReservation'), ('FlightReservation', 'FlightReservation'), ('FoodEstablishmentReservation', 'FoodEstablishmentReservation'), ('LodgingReservation', 'LodgingReservation'), ('RentalCarReservation', 'RentalCarReservation'), ('ReservationPackage', 'ReservationPackage'), ('TaxiReservation', 'TaxiReservation'), ('TrainReservation', 'TrainReservation')], help_text='Leave blank for OrderAction', required=False, verbose_name='Result Type')), ('result_name', wagtail.blocks.CharBlock(help_text='Example: "Reserve a table", "Book an appointment", etc.', required=False, verbose_name='Result Name')), ('extra_json', wagtail.blocks.RawHTMLBlock(form_classname='monospace', help_text='Additional JSON-LD inserted into the Action dictionary. Must be properties of https://schema.org/Action.', required=False, verbose_name='Additional action markup'))]))], blank=True, use_json_field=True, verbose_name='Actions'), + ), + migrations.AlterField( + model_name='mysnippet', + name='struct_org_hours', + field=wagtail.fields.StreamField([('hours', wagtail.blocks.StructBlock([('days', wagtail.blocks.MultipleChoiceBlock(choices=[('Monday', 'Monday'), ('Tuesday', 'Tuesday'), ('Wednesday', 'Wednesday'), ('Thursday', 'Thursday'), ('Friday', 'Friday'), ('Saturday', 'Saturday'), ('Sunday', 'Sunday')], help_text='For late night hours past 23:59, define each day in a separate block.', verbose_name='Days')), ('start_time', wagtail.blocks.TimeBlock(verbose_name='Opening time')), ('end_time', wagtail.blocks.TimeBlock(verbose_name='Closing time'))]))], blank=True, use_json_field=True, verbose_name='Hours of operation'), + ), + ] diff --git a/testproject/home/models.py b/testproject/home/models.py index 25804ef..4afbfad 100644 --- a/testproject/home/models.py +++ b/testproject/home/models.py @@ -1,8 +1,10 @@ -from wagtail.models import Page +from datetime import datetime -from wagtailseo.models import SeoMixin -from wagtailseo.models import SeoType -from wagtailseo.models import TwitterCard +from django.db import models +from django.urls import reverse +from wagtail.models import Page, Site + +from wagtailseo.models import SeoMixin, SeoMixinBase, SeoType, TwitterCard class WagtailPage(Page): @@ -35,3 +37,38 @@ class ArticlePage(SeoMixin, Page): seo_twitter_card = TwitterCard.LARGE promote_panels = SeoMixin.seo_panels + + +class BadWolf(SeoMixinBase, models.Model): + pass + + +class MySnippet(SeoMixinBase, models.Model): + template = "home/page.html" + + seo_content_type = SeoType.ARTICLE + seo_twitter_card = TwitterCard.LARGE + + promote_panels = SeoMixinBase.seo_panels + + def __str__(self): + return f"my snippet {self.pk or 0}" + + def get_full_url(self) -> str: + path = reverse("my_snippet_detail", kwargs={"pk": self.pk}) + return f"{self.get_site().root_url}/{path}" + + @property + def seo_author(self) -> str: + return "Tux" + + @property + def seo_published_at(self) -> datetime: + return datetime(1122, 1, 1) + + @property + def seo_modified_at(self) -> datetime: + return datetime(1204, 3, 31) + + def get_site(self): + return Site.objects.first() diff --git a/testproject/home/tests.py b/testproject/home/tests.py index 84d9a16..34a0974 100644 --- a/testproject/home/tests.py +++ b/testproject/home/tests.py @@ -3,21 +3,16 @@ from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType -from django.test import override_settings -from django.test import TestCase +from django.test import TestCase, override_settings from django.urls import reverse from django.utils import timezone from django.utils.text import capfirst -from wagtail.images.tests.utils import get_test_image_file -from wagtail.images.tests.utils import Image +from wagtail.images.tests.utils import Image, get_test_image_file from wagtail.models import Page from wagtail.test.utils import WagtailTestUtils -from home.models import ArticlePage -from home.models import SeoPage -from home.models import WagtailPage -from wagtailseo import schema -from wagtailseo import utils +from home.models import ArticlePage, BadWolf, MySnippet, SeoPage, WagtailPage +from wagtailseo import schema, utils from wagtailseo.models import SeoSettings @@ -101,12 +96,27 @@ def setUpClass(cls): content_type=cls.get_content_type("articlepage"), ) + cls.my_snippet = MySnippet( + og_image=Image.objects.create( + title="Snippet OG Image", + file=get_test_image_file(), + ), + ) + + cls.bad_wolf = BadWolf( + og_image=Image.objects.create( + title="BadWolf OG Image", + file=get_test_image_file(), + ), + ) + # Add to home page. cls.page_home = Page.objects.get(slug="home") cls.page_home.add_child(instance=cls.page_wagtail) cls.page_home.add_child(instance=cls.page_lowseo) cls.page_home.add_child(instance=cls.page_fullseo) cls.page_home.add_child(instance=cls.page_article) + cls.my_snippet.save() # Set site name site = cls.page_home.get_site() @@ -322,6 +332,69 @@ def test_custom_sep(self): response.content.decode("utf8"), ) + def test_struct_snippet(self): + """ + A page with SeoMixin set to article type should render correct + structured data. + """ + my_snippet = self.my_snippet + + # Manually render the JSON and match against page HTML. + # Get images to compare against rendered content. + base_url = utils.get_absolute_media_url(my_snippet.get_site()) + img1x1 = base_url + my_snippet.seo_image.get_rendition("fill-10000x10000").url + img4x3 = base_url + my_snippet.seo_image.get_rendition("fill-40000x30000").url + img16x9 = base_url + my_snippet.seo_image.get_rendition("fill-16000x9000").url + expected_dict = { + "@context": "http://schema.org", + "@type": "Article", + "mainEntityOfPage": { + "@type": "WebPage", + "@id": my_snippet.seo_canonical_url, + }, + "headline": str(my_snippet), + "description": "", + "datePublished": my_snippet.seo_published_at, + "dateModified": my_snippet.seo_modified_at, + "author": { + "@type": "Person", + "name": "Tux", + }, + "image": [img1x1, img4x3, img16x9], + } + expected_json = json.dumps(expected_dict, cls=utils.StructDataEncoder) + + # GET the page and check its JSON against expected JSON. + response = self.client.get( + reverse("my_snippet_detail", kwargs={"pk": my_snippet.pk}) + ) + self.assertEqual(response.status_code, 200) + self.maxDiff = None + print(response.content.decode("utf8")) + self.assertInHTML( + f""" + + """, # noqa + response.content.decode("utf8"), + ) + + def test_not_implemented(self): + """ + Ensure NotImplemented Errors are raised on required properties + """ + with self.assertRaises(NotImplementedError): + self.bad_wolf.seo_author + with self.assertRaises(NotImplementedError): + self.bad_wolf.seo_canonical_url + with self.assertRaises(NotImplementedError): + self.bad_wolf.seo_published_at + with self.assertRaises(NotImplementedError): + self.bad_wolf.seo_modified_at + with self.assertRaises(NotImplementedError): + self.bad_wolf.get_site() + class TestSettingMenu(WagtailTestUtils, TestCase): """ diff --git a/testproject/home/views.py b/testproject/home/views.py new file mode 100644 index 0000000..ea2571c --- /dev/null +++ b/testproject/home/views.py @@ -0,0 +1,13 @@ +from django.views.generic import DetailView + +from .models import MySnippet + + +class MySnippetDetailView(DetailView): + model = MySnippet + template_name = MySnippet.template + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["self"] = context["object"] + return context diff --git a/testproject/testproject/urls.py b/testproject/testproject/urls.py index 928f2f9..71ea0d9 100644 --- a/testproject/testproject/urls.py +++ b/testproject/testproject/urls.py @@ -1,12 +1,11 @@ from django.conf import settings from django.contrib import admin -from django.urls import include -from django.urls import path +from django.urls import include, path +from home.views import MySnippetDetailView from wagtail import urls as wagtail_urls from wagtail.admin import urls as wagtailadmin_urls from wagtail.documents import urls as wagtaildocs_urls - urlpatterns = [ path("django-admin/", admin.site.urls), path("admin/", include(wagtailadmin_urls)), @@ -14,6 +13,11 @@ # For anything not caught by a more specific rule above, hand over to # Wagtail's page serving mechanism. This should be the last pattern in # the list: + path( + "my-snippet/)/", + MySnippetDetailView.as_view(), + name="my_snippet_detail", + ), path("", include(wagtail_urls)), ] diff --git a/wagtailseo/models.py b/wagtailseo/models.py index 7aee70f..0898bfc 100644 --- a/wagtailseo/models.py +++ b/wagtailseo/models.py @@ -6,21 +6,15 @@ from django.db import models from django.utils.translation import gettext_lazy as _ from wagtail import VERSION as WAG_VERSION -from wagtail.admin.panels import FieldPanel -from wagtail.admin.panels import HelpPanel -from wagtail.admin.panels import MultiFieldPanel +from wagtail.admin.panels import FieldPanel, HelpPanel, MultiFieldPanel from wagtail.contrib.settings.models import register_setting from wagtail.fields import StreamField from wagtail.images import get_image_model_string from wagtail.images.models import AbstractImage from wagtail.models import Page -from wagtailseo import schema -from wagtailseo import settings -from wagtailseo import utils -from wagtailseo.blocks import OpenHoursBlock -from wagtailseo.blocks import StructuredDataActionBlock - +from wagtailseo import schema, settings, utils +from wagtailseo.blocks import OpenHoursBlock, StructuredDataActionBlock # Wagtail 3 if WAG_VERSION[0] == 3: @@ -50,7 +44,7 @@ class TwitterCard(Enum): SUMMARY = "summary" -class SeoMixin(Page): +class SeoMixinBase(models.Model): """ Contains fields for SEO-related attributes on a Page model. """ @@ -227,9 +221,7 @@ def seo_author(self) -> str: Gets the name of the author of this page. Override in your Page model as necessary. """ - if self.owner: - return self.owner.get_full_name() - return "" + raise NotImplementedError @property def seo_canonical_url(self) -> str: @@ -242,7 +234,13 @@ def seo_canonical_url(self) -> str: url = getattr(self, attr) if url: return url - return self.get_full_url() + get_full_url = getattr(self, "get_full_url", None) + if callable(get_full_url): + return get_full_url() + raise NotImplementedError( + "You need to provide a get_full_url method " + "or override the seo_canonical_url property." + ) @property def seo_description(self) -> str: @@ -333,7 +331,7 @@ def seo_pagetitle(self) -> str: # Fallback to wagtail.Page.title plus site name. return "{0} {1} {2}".format( - self.title, settings.get("WAGTAILSEO_SEP"), self.seo_sitename + str(self), settings.get("WAGTAILSEO_SEP"), self.seo_sitename ) @property @@ -342,7 +340,15 @@ def seo_published_at(self) -> datetime: Gets the date this page was first published. Override in your Page model as necessary. """ - return self.first_published_at + raise NotImplementedError + + @property + def seo_modified_at(self) -> datetime: + """ + Gets the date this page was first published. + Override in your Page model as necessary. + """ + raise NotImplementedError @property def seo_twitter_card_content(self) -> str: @@ -494,10 +500,10 @@ def seo_struct_article_dict(self) -> dict: "@type": "WebPage", "@id": self.seo_canonical_url, }, - "headline": self.title, + "headline": str(self), "description": self.seo_description, "datePublished": self.seo_published_at, - "dateModified": self.last_published_at, + "dateModified": self.seo_modified_at, "author": { "@type": "Person", "name": self.seo_author, @@ -523,9 +529,6 @@ def seo_struct_article_json(self) -> str: seo_meta_panels = [ MultiFieldPanel( [ - FieldPanel("slug", **slug_field_kwargs), - FieldPanel("seo_title"), - FieldPanel("search_description"), FieldPanel("canonical_url"), FieldPanel("og_image"), ], @@ -571,6 +574,65 @@ def seo_struct_article_json(self) -> str: seo_panels = seo_meta_panels + seo_menu_panels + seo_struct_panels + def get_site(self): + if hasattr(super(), "get_site"): + return super().get_site() + raise NotImplementedError + + +class SeoMixin(SeoMixinBase, Page): + class Meta: + abstract = True + + @property + def seo_author(self) -> str: + """ + Gets the name of the author of this page. + Override in your Page model as necessary. + """ + if self.owner: + return self.owner.get_full_name() + return "" + + @property + def seo_published_at(self) -> datetime: + """ + Gets the date this page was first published. + Override in your Page model as necessary. + """ + return self.first_published_at + + @property + def seo_modified_at(self) -> datetime: + """ + Gets the date this page was first published. + Override in your Page model as necessary. + """ + return self.last_published_at + + seo_meta_panels = [ + MultiFieldPanel( + [ + FieldPanel("slug", **slug_field_kwargs), + FieldPanel("seo_title"), + FieldPanel("search_description"), + ] + + SeoMixinBase.seo_meta_panels[0].children, + _("Search and Social Previews"), + ), + ] + + seo_menu_panels = [ + MultiFieldPanel( + [ + FieldPanel("show_in_menus"), + ], + _("Navigation"), + ), + ] + + seo_panels = seo_meta_panels + seo_menu_panels + SeoMixinBase.seo_struct_panels + @register_setting(icon="wagtailseo-line-chart") class SeoSettings(BaseSiteSetting):