{"id":13937086,"url":"https://github.com/neuman/python-carteblanche","last_synced_at":"2025-04-23T20:43:19.643Z","repository":{"id":12915486,"uuid":"15592909","full_name":"neuman/python-carteblanche","owner":"neuman","description":"Module to align code with thoughts of users and designers.  Also magically handles navigation and permissions.","archived":false,"fork":false,"pushed_at":"2015-09-02T00:06:30.000Z","size":984,"stargazers_count":40,"open_issues_count":5,"forks_count":3,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-04-01T15:53:52.178Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/neuman.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2014-01-02T20:24:07.000Z","updated_at":"2024-11-28T16:28:33.000Z","dependencies_parsed_at":"2022-07-12T15:05:06.072Z","dependency_job_id":null,"html_url":"https://github.com/neuman/python-carteblanche","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/neuman%2Fpython-carteblanche","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/neuman%2Fpython-carteblanche/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/neuman%2Fpython-carteblanche/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/neuman%2Fpython-carteblanche/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/neuman","download_url":"https://codeload.github.com/neuman/python-carteblanche/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250513380,"owners_count":21443200,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-07T23:03:16.676Z","updated_at":"2025-04-23T20:43:19.627Z","avatar_url":"https://github.com/neuman.png","language":"Python","readme":"This readme will introduce you to Carteblanche and walk you through an example app, please refer to [carteblanche-django-starter](https://github.com/neuman/carteblanche-django-starter) for the full example project.\n\npython-carteblanche\n===================\n\nA menuing system of unlimited power. Holds and serializes the in-memory relationship between urls and objects and users.\n\n\nInstallation\n------------\nYou can obtain the source code for carteblanche from here:\n\n    https://github.com/neuman/python-carteblanche\n\nOr install it with pip from the console\n\n\tpip install carteblanche\n\nAdd 'carteblanche' to your django settings INSTALLED_APPS array.\n\nChanges\n-------\nThis version contains significant changes from the original version and substantially breaks the API.  The decision to break the original API was not made lightly, but was deemed necesary.  The original version was created to see if there was interest in such a tool and was met with TONS of downloads.  From now on I will attempt to keep the API as in tact as possible.\n\nFundamentally carteblanche is designed to make the process of developing an MVC app closer to the proccess of designing one.  Designers, product owners and users tend to think of software in terms of:\n\n* What are the objects\n* What actions are available on those objects\n* Who can perform which actions on which objects at what time\n\nCarteblanche makes it possible to structure your project in a way that directly mirrors that thought process.  By breaking your permissions out into reusable verb classes that include permission conditions, it becomes possible to automatically:\n\n* Dynamically generate navigation appropriate for each user\n* Dynamically allow or block each user's access to specific views\n* Avoid spaghetti code by consistently handing condition variables down from model to view to template\n\nThere are three major components in Carteblanche:\n\n* Verb class\n* Noun mixin\n* NounView mixin\n\nA verb class glues together a view, its named URL, a particular model, and a function that determines if the view is available to the requesting user.\n\nThe Noun mixin is intended to be mixed into a model class and along with the attribute 'verb_classes' makes the model aware of what verbs to check availability for.\n\nThe NounView mixin is intended to be mixed in with a View class and allows the view to be aware of what noun it represents.\n\n\nGuided Intro\n------------\n\nYour first step is to create a verbs.py file. This is typically done at the same level as your models.py file.  This file will contain the conditional permissions for your entire app in once centralized location which makes for very easy adjustments later on when your designer/product owner/user decides they want a change.\n\nStart out by importing the carteblanche basics, and putting together a class that includes some simple info for convenience.  The 'condition_name' attribute exists to avoid repeatedly running the same 'is_available' function.  If a Verb has a 'condition_name', Carteblanche stores the result of its 'is_available' function temporarily and skips running 'is_available' for any following Verbs with the same 'condition_name' on the Noun being checked.  This sounds super complicated, but in practice is achieved by simple inheritance. \n\n```python\nfrom django.core.urlresolvers import reverse\nfrom carteblanche.base import Noun\nfrom carteblanche.mixins import DjangoVerb, availability_login_required\n\nAPPNAME = 'core'\n\nclass CoreVerb(DjangoVerb):\n    app = APPNAME\n    condition_name = 'is_public'\n```\n\nNext, add base classes for simple 'is_authenticated' and 'is_not_authenticated' conditions.  These may become a part of the next release, but because avoiding colisions in condition names are so critical, it's left up to you to explicitly write your own for now. Inherit from these for any Verbs that should only be available to users who are either logged in or not such as 'SiteJoinVerb' and 'SiteLoginVerb' below.\n\n```python\nclass AuthenticatedVerb(CoreVerb):\n    '''\n    abstract class for all verbs only visible to authenticated users\n    '''\n    condition_name = 'is_authenticated'\n    required = True\n\n    def is_available(self, user):\n        return user.is_authenticated()\n\n\nclass NotAuthenticatedVerb(CoreVerb):\n    '''\n    abstract class for all verbs only visible to users who are not authenticated\n    '''\n    condition_name = 'is_not_authenticated'\n    required = True\n\n    def is_available(self, user):\n        #only available to non-logged in users\n        if user.is_authenticated():\n            return False\n        return True\n\nclass SiteJoinVerb(NotAuthenticatedVerb):\n    display_name = \"Join Indiepen\"\n    view_name='user_create'\n\n\nclass SiteLoginVerb(NotAuthenticatedVerb):\n    display_name = \"Login\"\n    view_name='user_login'\n```\n\nLets assume that our app has a model called 'Sprocket' that has a ManyToMany of it's members.  Our urls.py file has named urls for:\n\n* 'sprocket_detail' which should be available to anyone logged in\n* 'sprocket_update' which should be available only to users listed in the Sprocket's members\n* 'sprocket_delete' which should be available only to users listed in the Sprocket's members\n\nYou would add the following to our verbs.py file.  You can see how easy it is to avoid running the same 'is_available' (by having SprocketUpdateVerb and SprocketUpdateVerb inherit from SprocketeerVerb they share a 'condition_name').\n\n```python\nclass SprocketDetailVerb(AuthenticatedVerb):\n    display_name = \"View Sprocket\"\n    view_name = 'sprocket_detail'\n\n    def get_url(self):\n        return reverse(viewname=self.view_name, args=[self.noun.id], current_app=self.app)\n\nclass SprocketeerVerb(CoreVerb):\n    '''\n    abstract class for all verbs available only to a sprocket's sprocketeers\n    '''\n    denied_message = \"You must be one of the sprocket's sprocketeers to upload to this post.\"\n    condition_name = \"is_sprocketeer\"\n\n    @availability_login_required\n    def is_available(self, user):\n        return self.noun.is_sprocketeer(user)\n\n    def get_url(self):\n        return reverse(viewname=self.view_name, args=[self.noun.id], current_app=self.app)\n\n\nclass SprocketUpdateVerb(SprocketeerVerb):\n    display_name = \"Update Sprocket\"\n    view_name = 'sprocket_update'\n\n\nclass SprocketDeleteVerb(SprocketeerVerb):\n    display_name = \"Delete Sprocket\"\n    view_name = 'sprocket_delete'\n```\n\nIn order to make these Verbs actually work for us, you must link them to a Noun.  Make an existing model into a Noun by adding the mixin after models.Model in the inheritance chain, and adding a 'verb_classes' attribute as seen below.\n\n```python\nfrom django.db import models\nfrom django.contrib.auth.models import User\nfrom django.core.urlresolvers import reverse\nfrom carteblanche.base import Noun\nfrom core.verbs import *\n\nclass Sprocket(models.Model, Noun):\n    sprocketeers = models.ManyToManyField(User)\n    title = models.CharField(max_length=300)\n    verb_classes = [SprocketDetailVerb, SprocketUpdateVerb, SprocketListVerb]\n\n    def __str__(self):\n        return self.title\n\n    def is_sprocketeer(self, user):\n        return self.sprocketeers.filter(id=user.id).count() \u003e 0\n\n    def get_absolute_url(self):\n        return SprocketDetailVerb(self).get_url()\n\n```\n\nNow that your model has become a Noun, any views pertaining to it need to become NounViews.  Add the NounView mixin before View in the inheritance chain.  You must also add a 'get_noun' function that returns the instance of the model this view pertains to.  Look how clean these views are!\n\n```python\nfrom django.views.generic.base import TemplateView\nfrom django.views.generic.edit import CreateView, UpdateView\nfrom carteblanche.mixins import NounView\nimport core.models as cm\n\nclass SprocketView(NounView):\n\n    def get_noun(self, **kwargs):\n        return cm.Sprocket.objects.get(id=self.kwargs['pk'])\n\n\nclass SprocketDetailView(SprocketView, TemplateView):\n    template_name = 'base.html'\n\n\nclass SprocketUpdateView(SprocketView, UpdateView):\n    model = cm.Sprocket\n    template_name = 'form.html'\n    success_url = '/'\n\n    def get_success_url(self):\n        return cm.SprocketDetailVerb(self.noun).get_url()\n```\n\nAt this point, all of the above views should automatically allow access to users who are members of a given sprocket and deny access to everyone else, but what about those other verbs we defined earlier that don't actually have a Noun?  SiteJoinVerb and SiteLoginVerb are actions pertaining to the site itself rather than a particular model, so we'll just create a Noun for the site along with a few more verbs that are available at the siteroot.  Add the following to your verbs.py file.\n\n```python\nclass SprocketCreateVerb(CoreVerb):\n    display_name = \"Create New Sprocket\"\n    view_name='sprocket_create'\n    condition_name = 'is_authenticated'\n    required = True\n\n    @availability_login_required\n    def is_available(self, user):\n        return True\n\nclass SprocketListVerb(AuthenticatedVerb):\n    display_name = \"List Sprockets\"\n    view_name = 'sprocket_list'\n\nclass SiteRoot(Noun):\n    '''\n    A convenient hack that lets pages that have no actual noun have verbs and verb-based permissions. \n    '''\n    verb_classes = [SiteJoinVerb, SiteLoginVerb, SprocketCreateVerb]\n\n    def __unicode__(self):\n        return 'Site Root'\n\n    class Meta:\n        abstract = True\n```\n\nNow add the following to your views.py file.\n\n```python\n\nclass SiteRootView(NounView):\n    def get_noun(self, **kwargs):\n        siteroot = cm.SiteRoot()\n        return siteroot\n\nclass IndexView(SiteRootView, TemplateView):\n    template_name = 'index.html'\n\n#this login/user create stuff might be better off in a different app\nclass UserCreateView(SiteRootView, CreateView):\n    model = User\n    template_name = 'form.html'\n    form_class = cf.RegistrationForm\n    success_url = '/'\n\n    def form_valid(self, form):\n        user = User.objects.create_user(uuid4().hex, form.cleaned_data['email'], form.cleaned_data['password1'])\n        user.first_name = form.cleaned_data['first_name']\n        user.last_name = form.cleaned_data['first_name']\n        user.save()\n        user = authenticate(username=user.username, password=form.cleaned_data['password1'])\n        login(self.request, user)\n        form.instance = user\n        return super(UserCreateView, self).form_valid(form)\n\n\nclass UserLoginView(SiteRootView, FormView):\n    template_name = 'form.html'\n    form_class = cf.LoginForm\n    success_url = '/'\n\n    def form_valid(self, form):\n        user = form.user_cache\n        login(self.request, user)\n        form.instance = user\n        return super(UserLoginView, self).form_valid(form)    \n\nclass SprocketCreateView(SiteRootView, CreateView):\n    model = cm.Sprocket\n    template_name = 'form.html'\n    form_class = cf.SprocketForm\n    success_url = '/'\n\n    def get_success_url(self):\n        self.object.sprocketeers.add(self.request.user)\n        return cm.SprocketDetailVerb(self.object).get_url()\n```\n\n###Displaying in a Template\nThe NounView mixin automatically includes 'available_verbs', 'conditions' and 'noun' in the cointext it hands to the template renderer.  All you have to do to display the dynamically rendered navigation menu is include the following somewhere in your template.\n```html\n\u003cul\u003e\n  {% for verb in noun.get_available_verbs %}\n      \u003cli\u003e\u003ca href=\"{{ verb.url }}\"\u003e{{ verb.display_name }}\u003c/a\u003e\u003c/li\u003e\n  {% endfor %}\n\u003c/ul\u003e\n```\n\nWhen view access is denied to a user, Carteblanche uses django's messaging system to display the appropriate Verb's 'denied_message'.  You can set 'MESSAGES_TEMPLATE' to a custom template in your settings file.  The messages template should include something similar to the following:\n\n```html\n{% for message in messages %}\n    \u003cdiv class=\"alert alert-{{ message.tags }}\"\u003e{{ message }}\u003c/div\u003e\n{% endfor %}\n```","funding_links":[],"categories":["Python","Awesome Python"],"sub_categories":["Permissions"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fneuman%2Fpython-carteblanche","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fneuman%2Fpython-carteblanche","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fneuman%2Fpython-carteblanche/lists"}