Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Patterns for Extensibility (PyTN 2015)

Patterns for Extensibility (PyTN 2015)

Avatar for Patrick Altman

Patrick Altman

February 07, 2015

More Decks by Patrick Altman

Other Decks in Programming

Transcript

  1. 73

  2. Why care about extensibility? • It is the foundation of

    reusability • We have a chance to delight our users • We can make the world a better place
  3. Hooksets: Example 1 • django-user-accounts has long sent a number

    of emails for various functions • Eldarion’s client needed to control the content of the email templates via a third party solution • We needed the ability to integrate with this third party’s API rather than send the email directly • Creating hooksets in django-user-accounts preserved default behavior while providing a way for us to override the email sending in the site
  4. Hooksets: Example 1 # account/hooks.py class AccountDefaultHookSet(object): # Methods sending

    emails the standard way def send_invitation_email(to, ctx): class HookProxy(object): def __getattr__(self, attr): return getattr(settings.ACCOUNT_HOOKSET, attr) hookset = HookProxy()
  5. Hooksets: Example 1 # account/conf.py class AccountAppConf(AppConf): HOOKSET = "account.hooks.AccountDefaultHookSet"

    def configure_hookset(self, value): return load_path_attr(value)() # settings.py ACCOUNT_HOOKSET = "your_site.hooks.AccountOverrideHookSet"
  6. Hooksets: Example 1 # account/models.py from account.hooks import hookset class

    SignupCode(models.Model): def send(self, **kwargs): # building up the context hookset.send_invitation_email([self.email], ctx) # finish things up
  7. Hooksets: Example 2 • Eldarion has several web apps that

    have a team focus (ThoughtStreams, KPITree, Gondor, to name a few) • Each of these sites has slightly different requirements about how some of the internals work for team operations. • Building the team url was one that was added to be able to support subdomains in some cases. • There is also changing how the team management works both in the format of the auto-complete results as well as the filtering requirements being different by site.
  8. Hooksets: Example 2 # hooks.py class TeamDefaultHookset(object): def build_team_url(self, url_name,

    team_slug): return reverse(url_name, args=[team_slug]) def get_autocomplete_result(self, user): return {“pk": user.pk, "email": user.email, "name": user.get_full_name()} def search_queryset(self, query, users): return users.filter( Q(email__icontains=query) | Q(username__icontains=query) | Q(first_name__icontains=query) | Q(last_name__icontains=query) )
  9. Hooksets: Example 2 # thoughtstreams/hooks/teams.py def get_autocomplete_result(self, user): profile =

    user.profile_set.get(team__isnull=True) try: avatar_url = profile.medium_avatar() except InvalidImageFormatError: avatar_url = "<url to default png>" return { "pk": user.pk, "name": profile.name, "username": user.username, "avatar_url": avatar_url }
  10. Class Based Views: Example 1 • Another Eldarion client had

    very specific requirements around password complexity and profile creation at signup. • Out of the box, django-user-accounts has password validation that works great on most sites but didn’t fit this client’s requirements. • We were able to easily leverage all that django-user- accounts gives us by customizing the SignupView and ChangePasswordForm objects with very little code.
  11. Class Based Views: Example 1 # views.py from account.views import

    SignupView as AccountSignupView from .forms import SignupForm class SignupView(AccountSignupView): form_class = SignupForm def generate_username(self, form): # custom username def create_user(self, form, commit=True, **kwargs): # create the user with first_name / last_name from form def after_signup(self, form): # Create profile and process any coupon codes
  12. Class Based Views: Example 1 # forms.py from account.forms import

    ( SignupForm as AccountSignupForm ChangePasswordForm as AccountChangePasswordForm, PasswordResetTokenForm as AcccountPasswordResetTokenForm ) class SecurePasswordMixin(object): def assert_strong_password(self, data): # raise forms.ValidationError if rules are broken class ChangePasswordForm(SecurePasswordMixin, AccountChangePasswordForm): class PasswordResetTokenForm(SecurePasswordMixin, AcccountPasswordResetTokenForm): class SignupForm(SecurePasswordMixin, AccountSignupForm): # Add extra fields for profile
  13. Class Based Views: Example 1 # urls.py url( r"^account/password/$", ChangePasswordView.as_view(form_class=ChangePasswordForm),

    name="account_password" ), url( r"^account/password/reset/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/$", PasswordResetTokenView.as_view(form_class=PasswordResetTokenForm), name="account_password_reset_token" ), url( r"^account/signup/$", SignupView.as_view(), name="account_signup" ), url(r"^account/", include("account.urls")),
  14. Plugins: Example 1 • One of Eldarion’s sites that is

    still in early development, KPITree, needed a way to easily extend the number of integrations it could have with 3rd party sources of metrics. • We created an app that will be open sourced and accept pull requests for new integration plugins. • This will enable functionality to be added to KPITree with the site only knowing about the Plugin interface and the Plugin author not having to know about any of the KPITree internals.
  15. Plugins: Example 1 # base.py class PluginRegistry(object): def __init__(self): self._registry

    = {} def register(self, plugin): self._registry[plugin.label] = { "plugin": plugin } def get(self, label, default=None) def __getitem__(self, label) def choices(self) registry = PluginRegistry() del PluginRegistry
  16. Plugins: Example 1 # base.py class Registerable(type): def __new__(cls, clsname,

    bases, attrs): newclass = super(Registerable, cls).__new__( cls, clsname, bases, attrs) if newclass.label is not None: registry.register(newclass) return newclass class Plugin(object): __metaclass__ = Registerable label = None display_name = None def inputs(self) def outputs(self) def fetch(self, start, end)
  17. Plugins: Example 1 # uservoice.py from .base import Plugin class

    UservoicePlugin(Plugin): label = "uservoice" def inputs(self): def outputs(self): def fetch(self, start, end):
  18. Plugins: Example 1 # kpitree/collections/models.py def clean(self): super(AutoCollection, self).clean() for

    inp in self.plugin.inputs: if self.inputs.get(inp) is None: raise ValidationError( "The required input, {0}, is missing.".format( inp ) ) @property def plugin(self): return registry.get(self.provider)(**self.inputs)
  19. Plugins: Example 1 # kpitree/collections/models.py def process(self): for period in

    self.periods_due(): start, end = period_start_end(period) data = self.plugin.fetch(start, end) metrics = self.autocollectionmetric_set.all() for collection_metric in metrics: collection_metric.add_measure(period, data) if self.point_in_time: self.logs.create(timestamp=period) else: self.logs.create(period=period)
  20. Plugins: Example 1 # apps.py def load_submodules(module): mod = import_module(module)

    for loader, name, is_pkg in pkgutil.walk_packages(mod.__path__): import_module("{0}.{1}".format(module, name)) class KPITreeConfig(AppConfig): def ready(self): load_submodules("kpitree.integrations")