GithubHelp home page GithubHelp logo

wagtail-nest / wagtail-airtable Goto Github PK

View Code? Open in Web Editor NEW
46.0 6.0 15.0 1.36 MB

Airtable import and export support for Wagtail pages and Django models.

License: BSD 3-Clause "New" or "Revised" License

Python 95.91% HTML 4.09%

wagtail-airtable's Introduction

Wagtail/Airtable

An extension for Wagtail allowing content to be transferred between Airtable sheets and your Wagtail/Django models.

Developed by Torchbox and sponsored by The Motley Fool.

Wagtail Airtable demo

How it works

When you setup a model to "map" to an Airtable sheet, every time you save the model it will attempt to update the row in Airtable. If a row is not found, it will create a new row in your Airtable.

When you want to sync your Airtable data to your Wagtail website, you can go to Settings -> Airtable Import. You can then import entire tables into your Wagtail instance with the click of a button. If you see "{Your Model} is not setup with the correct Airtable settings" you will need to double check your settings. By default the import page can be found at http://yourwebsite.com/admin/airtable-import/, or if you use a custom /admin/ url it'll be http://yourwebsite.com/{custom_admin_url}/airtable-import/.

Behind the scenes...

This package will attempt to match a model object against row in Airtable using a record_id. If a model does not have a record_id value, it will look for a match using the AIRTABLE_UNIQUE_IDENTIFIER to try and match a unique value in the Airtable to the unique value in your model. Should that succeed your model object will be "paired" with the row in Airtable. But should the record-search fail, a new row in Airtable will be created when you save your model, or a new model object will attempt to be created when you import a model from Airtable.

Note: Object creation can fail when importing from Airtable. This is expected behaviour as an Airtable might not have all the data a model requires. For instance, a Wagtail Page uses django-treebeard, with path as a required field. If the page model import settings do not include the path field, or a path column isn't present in Airtable, the page cannot be created. This same rule applies to other required fields on any Django model including other required fields on a Wagtail Page.

Installation & Configuration

  • Install the package with pip install wagtail-airtable

  • Add 'wagtail_airtable' to your project's INSTALLED_APPS.

    • On Wagtail 5.x, to enable the snippet-specific import button on the Snippet list view make sure wagtail_airtable is above wagtail.snippets in your INSTALLED_APPS
  • In your settings you will need to map Django models to Airtable settings. Every model you want to map to an Airtable sheet will need:

    • An AIRTABLE_BASE_KEY. You can find the base key in the Airtable API docs when you're signed in to Airtable.com
    • An AIRTABLE_TABLE_NAME to determine which table to connect to.
    • An AIRTABLE_UNIQUE_IDENTIFIER. This can either be a string or a dictionary mapping the Airtable column name to your unique field in your model.
      • ie. AIRTABLE_UNIQUE_IDENTIFIER: 'slug', this will match the slug field on your model with the slug column name in Airtable. Use this option if your model field and your Airtable column name are identical.
      • ie. AIRTABLE_UNIQUE_IDENTIFIER: {'Airtable Column Name': 'model_field_name'}, this will map the Airtable Column Name to a model field called model_field_name. Use this option if your Airtable column name and your model field name are different.
    • An AIRTABLE_SERIALIZER that takes a string path to your serializer. This helps map incoming data from Airtable to your model fields. Django Rest Framework is required for this. See the examples/ directory for serializer examples.
  • Lastly make sure you enable wagtail-airtable with WAGTAIL_AIRTABLE_ENABLED = True. By default this is disabled so data in your Wagtail site and your Airtable sheets aren't accidentally overwritten. Data is hard to recover, this option helps prevent accidental data loss.

Example Base Configuration

Below is a base configuration or ModelName and OtherModelName (both are registered Wagtail snippets), along with HomePage.

# your settings.py
AIRTABLE_API_KEY = 'yourSuperSecretKey'
WAGTAIL_AIRTABLE_ENABLED = True
AIRTABLE_IMPORT_SETTINGS = {
    'appname.ModelName': {
        'AIRTABLE_BASE_KEY': 'app3ds912jFam032S',
        'AIRTABLE_TABLE_NAME': 'Your Airtable Table Name',
        'AIRTABLE_UNIQUE_IDENTIFIER': 'slug', # Must match the Airtable Column name
        'AIRTABLE_SERIALIZER': 'path.to.your.model.serializer.CustomModelSerializer'
    },
    'appname.OtherModelName': {
        'AIRTABLE_BASE_KEY': 'app4ds902jFam035S',
        'AIRTABLE_TABLE_NAME': 'Your Airtable Table Name',
        'AIRTABLE_UNIQUE_IDENTIFIER': {
            'Page Slug': 'slug', # 'Page Slug' column name in Airtable, 'slug' field name in Wagtail.
        },
        'AIRTABLE_SERIALIZER': 'path.to.your.model.serializer.OtherCustomModelSerializer'
    },
    'pages.HomePage': {
        'AIRTABLE_BASE_KEY': 'app2ds123jP23035Z',
        'AIRTABLE_TABLE_NAME': 'Wagtail Page Tracking Table',
        'AIRTABLE_UNIQUE_IDENTIFIER': {
            'Wagtail Page ID': 'pk',
        },
        'AIRTABLE_SERIALIZER': 'path.to.your.pages.serializer.PageSerializer',
        # Below are OPTIONAL settings.
        # By disabling `AIRTABLE_IMPORT_ALLOWED` you can prevent Airtable imports
        # Use cases may be:
        #   - disabling page imports since they are difficult to setup and maintain,
        #   - one-way sync to Airtable only (ie. when a model/Page is saved)
        # Default is True
        'AIRTABLE_IMPORT_ALLOWED': True,
        # Add the AIRTABLE_BASE_URL setting if you would like to provide a nice link
        # to the Airtable Record after a snippet or Page has been saved.
        # To get this URL open your Airtable base on Airtable.com and paste the link.
        # The recordId will be automatically added so please don't add that
        # You can add the below setting. This is optional and disabled by default.
        'AIRTABLE_BASE_URL': 'https://airtable.com/tblxXxXxXxXxXxXx/viwxXxXxXxXxXxXx',
        # The PARENT_PAGE_ID setting is used for creating new Airtable Pages. Every
        # Wagtail Page requires a "parent" page. This setting can either be:
        # 1. A callable (ie `my_function` without the parentheses)'
        # Example:
        # 'PARENT_PAGE_ID': custom_function,
        # 2. A path to a function. (ie. 'appname.directory.filename.my_function')
        # Example:
        # 'PARENT_PAGE_ID': 'path.to.function',
        # 3. A raw integer.
        # Example:
        # 'PARENT_PAGE_ID': 3,

        # If you choose option #1 (callable) or option #2 (path to a function)
        # Your function needs to return an integer which will represent the Parent
        # Page ID where all imported pages will be created as child pages.
        # Callables and path-to-functions (option #1 and option #2 in the above docs)
        # Take an `instance` kwarg as of v0.2.1. Example below:
        #   def custom_parent_page_id_function(instance=None):
        #       if instance and isinstance(instance, Page):
        #           return Page.objects.get(pk=instance.id).get_parent()
        'PARENT_PAGE_ID': 'path.to.function',
        # The `AUTO_PUBLISH_NEW_PAGES` setting will tell this package to either
        # Automatically publish a newly created page, or set to draft.
        # True = auto publishing is on. False = auto publish is off (pages will be drafts)
        'AUTO_PUBLISH_NEW_PAGES': False,
    },
    # ...
}
Have multiple models with the same Airtable settings?

The most common approach will likely be to support a handful of models, in which case using the below example would be faster and cleaner. Write a config dictionary once to prevent config bloat.

AIRTABLE_API_KEY = 'yourSuperSecretKey'
WAGTAIL_AIRTABLE_ENABLED = True
CUSTOM_PAGE_SETTINGS = {
    'AIRTABLE_BASE_KEY': 'app3ds912jFam032S',
    'AIRTABLE_TABLE_NAME': 'Your Airtable Table Name',
    'AIRTABLE_UNIQUE_IDENTIFIER': 'slug', # Must match the Airtable Column name
    'AIRTABLE_SERIALIZER': 'path.to.your.model.serializer.CustomModelSerializer'
},
AIRTABLE_IMPORT_SETTINGS = {
    'home.HomePage': CUSTOM_PAGE_SETTINGS,
    'blog.BlogPage': CUSTOM_PAGE_SETTINGS,
    'appname.YourModel': CUSTOM_PAGE_SETTINGS,
}

Wagtail Page creation on Airtable Imports

This feature was sponsored by The Mozilla Foundation.

In wagtail-airtable v0.1.6 and up you can create Wagtail Pages from Airtable imports.

A few settings need to be set for this to work as you would expect. Read through the following code to see which settings are needed:

AIRTABLE_IMPORT_SETTINGS = {
    'pages.HomePage': {
        'AIRTABLE_BASE_KEY': 'app2ds123jP23035Z',
        'AIRTABLE_TABLE_NAME': 'Wagtail Page Tracking Table',
        'AIRTABLE_UNIQUE_IDENTIFIER': {
            'Wagtail Page ID': 'pk',
        },
        'AIRTABLE_SERIALIZER': 'path.to.your.pages.serializer.PageSerializer',
        'AIRTABLE_IMPORT_ALLOWED': True,  # This must be set
        'PARENT_PAGE_ID': 'path.to.function.that.returns.an.integer',  # This must be set
    },
}

Once your settings are ready, you can start creating new Pages in Airtable and import those pages via the Wagtail Admin (found in the setting menu).

Caveats: In the above code we see {'Wagtail Page ID': 'pk',}, this means there's a column in Airtable named "Wagtail Page ID" and it mapped to a Page pk. When you create a new Wagtail Page inside of an Airtable sheet, keep this cell blank in your new row. It will auto-update when it gets imported. This happens because Airtable (and the editors) likely don't know what the new Page ID is going to be, so we let Wagtail set it, and then update the Airtable again.

Hooks

Hooks are a way to execute code once an action has happened. This mimics (and internally uses) Wagtail's hook feature.

Note: When using hooks it will add processing time to your requests. If you're using Heroku with a 30s timeout you may want to use a management command to avoid hitting a server timeout.

Updated record

To take an action when a record is updated, you can write a hook like this in your wagtail_hooks.py file:

@hooks.register('airtable_import_record_updated')
def airtable_record_updated(instance, is_wagtail_page, record_id):
    # Instance is the page or model instance
    # is_wagtail_page is a boolean to determine if the object is a wagtail page. This is a shortcut for `isinstance(instance, wagtail.models.Page)`
    # record_id is the wagtail record ID. You can use this to perform additional actions against Airtable using the airtable-python-wrapper package.
    pass

Management Commands

python manage.py import_airtable appname.ModelName secondapp.SecondModel

Optionally you can turn up the verbosity for better debugging with the --verbosity=2 flag.

import_airtable command

This command will look for any appname.ModelNames you provide it and use the mapping settings to find data in the Airtable. See the "Behind the scenes" section for more details on how importing works.

skipping django signals

By default the import_airtable command adds an additional attribute to the models being saved called _skip_signals - which is set to True you can use this to bypass any post_save or pre_save signals you might have on the models being imported so those don't run. e.g.

@receiver(post_save, sender=MyModel)
def post_save_function(sender, **kwargs):
    if sender._skip_signals:
        # rest of logic

if you don't do these checks on your signal, the save will run normally.

Local Testing Advice

Note: Be careful not to use the production settings as you could overwrite Wagtail or Airtable data.

Because Airtable doesn't provide a testing environment, you'll need to test against a live table. The best way to do this is to copy your live table to a new table (renaming it will help avoid naming confusion), and update your local settings. With this method, you can test to everything safely against a throw-away Airtable. Should something become broken beyond repair, delete the testing table and re-copy the original one.

Local debugging

Due to the complexity and fragility of connecting Wagtail and Airtable (because an Airtable column can be almost any value) you may need some help debugging your setup. To turn on higher verbosity output, you can enable the Airtable debug setting WAGTAIL_AIRTABLE_DEBUG = True. All this does is increase the default verbosity when running the management command. In a standard Django management command you could run python manage.py import_airtable appname.ModelName --verbosity=2 however when you import from Airtable using the Wagtail admin import page you won't have access to this verbosity argument. But enabling WAGTAIL_AIRTABLE_DEBUG you can manually increase the verbosity.

Note: This only only work while DEBUG = True in your settings as to not accidentally flood your production logs.

Airtable Best Practice

Airtable columns can be one of numerous "types", very much like a Python data type or Django field. You can have email columns, url columns, single line of text, checkbox, etc.

To help maintain proper data synchronisation between your Django/Wagtail instance and your Airtable Base's, you should set the column types to be as similar to your Django fields as possible.

For example, if you have a BooleanField in a Django model (or Wagtail Page) and you want to support pushing that data to Airtable amd support importing that same data from Airtable, you should set the column type in Airtable to be a Checkbox (because it can only be on/off, much like how a BooleanField can only be True/False).

In other cases such as Airtables Phone Number column type: if you are using a 3rd party package to handle phone numbers and phone number validation, you'll want to write a custom serializer to handle the incoming value from Airtable (when you import from Airtable). The data will likely come through to Wagtail as a string and you'll want to adjust the string value to be a proper phone number format for internal Wagtail/Django storage. (You may also need to convert the phone number to a standard string when exporting to Airtable as well)

Running Tests

Clone the project and cd into the wagtail-airtable/ directory. Then run python runtests.py tests. This project is using standard Django unit tests.

To target a specific test you can run python runtests.py tests.test_file.TheTestClass.test_specific_model

Tests are written against Wagtail 2.10 and later.

Customizing the save method

In some cases you may want to customize how saving works, like making the save to airtable asynchronous for example.

To do so, set: WAGTAIL_AIRTABLE_SAVE_SYNC=False in your settings.py file.

This escapes out of the original save method and requires you enable the asynchronous part of this on your own.

An example of how you might set this up using the signal after_page_publish with django_rq

#settings.py
WAGTAIL_AIRTABLE_SAVE_SYNC=False
WAGTAIL_AIRTABLE_PUSH_MESSAGE="Airtable save happening in background"

#wagtail_hooks.py
from django.dispatch import receiver
from wagtail.models import Page

@job('airtable')
def async_airtable_save(page_id):
    my_page = Page.objects.get(page_id).specific
    my_page.save_to_airtable()
    
    
@receiver('page_published')
def upload_page_to_airtable(request, page):
    async_airtable_save.delay(page.pk)

The messaging will be off if you do this, so another setting has been made available so you may change the messaging to anything you'd like: WAGTAIL_AIRTABLE_PUSH_MESSAGE - set this to whatever you'd like the messaging to be e.g. WAGTAIL_AIRTABLE_PUSH_MESSAGE='Airtable save is happening in the background'

Adding an Import action to the snippet list view (Wagtail 6.x)

As of Wagtail 6.0, the Import action is no longer automatically shown on the snippet listing view (although it is still available through Settings -> Airtable import). To add it back, first ensure that your snippet model is registered with an explicit viewset. Then, ensure that the index view for that viewset inherits from SnippetImportActionMixin:

from wagtail.snippets.models import register_snippet
from wagtail.snippets.views.snippets import IndexView, SnippetViewSet
from wagtail_airtable.mixins import SnippetImportActionMixin
from .models import Advert


class AdvertIndexView(SnippetImportActionMixin, IndexView):
    pass


class AdvertViewSet(SnippetViewSet):
    model = Advert
    index_view_class = AdvertIndexView

register_snippet(Advert, viewset=AdvertViewSet)

Trouble Shooting Tips

Duplicates happening on import

Ensure that your serializer matches your field definition exactly, and in cases of CharField's that have blank=True or null=True setting required=False on the serializer is also important.

In some cases 2 Models may get the same Airtable ID. To circumvent this error on imports the first one found will be set as the "real" one and the "impostors" will be set to "" - this may create duplicate models in your system, if this is happening a lot. Make sure your export method and serializer import are set correctly.

wagtail-airtable's People

Contributors

bmoe872 avatar dependabot[bot] avatar fabienheureux avatar gasman avatar jacobtoppm avatar jams2 avatar kalobtaulien avatar katdom13 avatar nabilmostafa avatar realorangeone avatar sharmainelim avatar stormheg avatar zerolab avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

wagtail-airtable's Issues

Add attachment support for "Attachment" columns in Airtable

I noticed recently there's a new "attachment" column type in Airtable. I haven't looked into using it yet, but I suspect we can use it to support images and documents.

Acceptance Criteria:

  • Must support images as this is the highest demanded feature right now
  • Must support documents as that's part of the "attachment" field.

Feature Request: Ability to save Async through cache

Since wagtail airtable overrides the save method, saving can take a long time to happen. It would be great to have a method, or setting, that enabled the save to airtable to happen asynchronously through something like Celery or one of the many alternatives for doing things asynchronously.

Error Handling for 503 from Airtable

Airtable was momentarily down, and returned a 503:

503 Server Error: Service Temporarily Unavailable for url: https://api.airtable.com/v0/apphAzl2LmaALyWdx/Master%20Page%20List?filterByFormula=%7BWagtail+Page+ID%7D%3D2908

But since that didn't return an actual error response from Airtable, this error message parsing failed:

    code = int(error.split(":", 1)[0].split(" ")[0])
    error_json = error.split("[Error: ")[1].rstrip("]")

with an IndexOutofRange error on the split into error_json in mixins.py line 273.

Decimal Fixing

For decimal fields, when exporting, they need to be converted to a string. This could be automated on the API request to Airtable. Perform a quick check on each outgoing value and if there's a Decimal convert it to a data type that works.

Lots of room for improvement here and is a good first issue.

Bump for wagtail 2.14.1

It looks like wagtail 2.14.1 is not currently allowed, can this version number be bumped?

wagtail_airtable/wagtail_hooks.py uses deprecated classnames when creating a MenuItem

Issue Summary

I am in the process of upgrading a project to wagtail 5.2 and updated all custom menu items in my project from classnames= to classname= but am still seeing the deprecation warning:

RemovedInWagtail60Warning: The `classnames` kwarg for MenuItem is deprecated - use `classname` instead.

Relevant Information

I have wagtail-airtable=0.6.0 installed, and when I put a breakpoint on the warning, I can see it traces back to wagtail-airtable:

image

I believe if you change Line 29 of wagtail_airtable/wagtail_hooks.py to use classname the deprecation warning will go away.

Technical details

  • Python version: 3.11.1.
  • Django version: 4.1.13
  • Wagtail version: 5.2
  • Wagtail Airtable version: 0.6.0
  • Browser version: Chrome 119

Gracefully handle missing Airtable name

In the settings you specify an AIRTABLE_TABLE_NAME. If that name is wrong, it throws this:

 [Error: {'type': 'TABLE_NOT_FOUND', 'message': 'Could not find table Wagtail Page Demo in application appxxxxxxx'}]

We need to gracefully handle that.

ModelAdmin snippet import button support

Snippets have an "Import" button in the object listing template.

Model Admin templates could have the save in order to create a more consistent user experience with snippets vs. model admin views.

Index error on error handling due to updated Airtable error messaging.

I think Airtable updated how they are handling errors, as we recently started getting problems on this line:

error_json = error.split("[Error: ")[1].rstrip("]")

Which is found in wagtail_airtable/mixins.py

The error above is coming in as:

'422 Client Error: Unprocessable Entity for url: https://api.airtable.com/v0/<base_name>/<table_name>'

Which is different from how the error messages used to come, which included brackets that the error was splitting on.

Support exact record usage url via API

The AIRTABLE_BASE_URL setting is not a fantastic way to implement an exact record URL on Airtable.com (not the API url, the browser-friendly url).

The URL should look like this:
https://airtable.com/tblxXxXxXxXxXxXx/viwtxXxXxXxXxXxXx/recxXxXxXxXxXxXx

Unfortunately the Airtable API doesn't seem to support providing the view or base ID natively. The currently implement is rough, and if Airtable ever supports getting a record_usage_url that should be implemented over the existing method.

Django 4.0 support

Django 4.0 removes this import: from django.conf.urls import url

Suggested change:
from django.urls import re_path as url

PK Value coming across as none on newly created db objects

if you have the AIRTABLE_UNIQUE_IDENTIFIER set as something like {'wagtail_id': 'pk'} then the pk should, in theory, be exported through the export_mapping.

However, due to the order of operations, if it's a brand new model in the DB the pk isn't assigned until after the model has already been sent off to Airtable, meaning Airtable doesn't get the PK value until you save again.

I think all we need to do, to get around this is call super().save() before going into the airtable logic.

Gracefully handle authentication errors

When saving a page or model using the AirtableMixin it looks for the Airtable API key. If that key is wrong, there's a authentication error such as:

[Error: {'type': 'AUTHENTICATION_REQUIRED', 'message': 'Authentication required'}]") 

We need to handle that authentication a little more gracefully.

Wrap a try/catch around the split in the line referenced in this PR

When there's an error, we split the error and parse it. But if there arent 2 parts to the split() it will fail spectacularly.

Below is the line that needs a try/catch around it, will a fallback error message.
https://github.com/wagtail/wagtail-airtable/blob/938484513bd43828973ebcd61f7a055005dff393/wagtail_airtable/mixins.py#L288

Example error:

('503 Server Error: Service Unavailable for url: https://api.airtable.com/v0/appXXXXXXXX/BaseName', '503 Server Error: Service Unavailable for url: https://api.airtable.com/v0/appXXXXXXX/BaseName')

create_or_update() method

In mixins.py there is AirtableMixin.create_record() and AirtableMixin.update_record().

These should ideally be merged together. Originally there were valid reasons for keeping these separate, but after further development they can merged.

Merging the two methods into a create_or_update() method would reduce logic in AirtableMixin.save() and we could likely remove some of the unit tests and merge those together as well.

Add a setting to disable django messages

Currently when you save a snippet or Wagtail Page a Django Message will appear (either green for success or red for failure).

Models will still save on your site even if Airtable isnt updated.

In some cases you may not want to show two message bars to your editors:
image

A good first issue and enhancement would be to allow these to be turned off globally so only the regular Wagtail notification messages appear, and the Airtable notification messages are silenced.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.