Welcome to Django Opt-out application’s documentation!¶
Contents:
Django Opt-out application¶
Allow everybody to unsubscribe your messages, user accounts are not required.
- Free software: MIT license
- Documentation: https://django-opt-out.readthedocs.io.
Features¶
- A single page form for opt-out feedback submission
- Feedback options text controlled from django admin
- Predefined feedback defaults available from django manage command
- Feedback translations done in django admin
- Feedback options selection based on tags supplied to the opt-out url
- Ability to preselect a feedback option
- Ability to change selected feedback options after submission
- Ability to set tag:value pair on opt-out url and store them on submission with user feedback
- Signal to modify opt-out form before rendering
- Signal on opt-out feedback submission
- Easily overridable thank you / goodbye view
- Opt-out form with a easily overridable base template
Demo¶
To run an example project for this django reusable app, click the button below and start a demo serwer on Heroku


Quickstart¶
Install Django Opt-out application:
pip install django-opt-out
Add it to your INSTALLED_APPS:
INSTALLED_APPS = (
...
'django_opt_out.apps.DjangoOptOutConfig',
...
)
Add Django Opt-out application’s URL patterns:
from django_opt_out import urls as django_opt_out_urls
urlpatterns = [
...
url(r'^', include(django_opt_out_urls)),
...
]
Add unsubscribe links to your emails:
from django_opt_out.utils import get_opt_out_path
email='Django Opt-out <django-opt-out@niepodam.pl>'
unsubscribe = get_opt_out_path(email, 'some', 'tags', 'controlling', 'questionnaire')
# unsubscribe link will not have a domain name and scheme
# you can build prefix from request, but I prefer to set it in settings
from django.conf import settings
unsubscribe = settings.BASE_URL + unsubscribe
body = 'Hello, Regards\n\nUnsubscribe: ' + unsubscribe
from django.core import mail
message = mail.EmailMultiAlternatives(body=body, to=[email])
message.extra_headers['List-Unsubscribe'] = "<{}>".format(unsubscribe)
message.send()
Running Tests¶
Does the code actually work?
source <YOURVIRTUALENV>/bin/activate
(myenv) $ pip install tox
(myenv) $ tox
Credits¶
This package was created with Cookiecutter and the wooyek/cookiecutter-django-app project template.
Installation¶
Stable release¶
To install Django Opt-out application, run this command in your terminal:
$ pip install django-opt-out
This is the preferred method to install Django Opt-out application, as it will always install the most recent stable release.
If you don’t have pip installed, this Python installation guide can guide you through the process.
From sources¶
You can either clone the public repository:
$ git clone git://github.com/wooyek/django-opt-out
Or download the download source from project website. Once you have a copy of the source, you can install it with:
$ python setup.py install
Usage¶
To use Django Opt-out application in a project, add it to your INSTALLED_APPS:
INSTALLED_APPS = (
...
'django_opt_out.apps.DjangoOptOutConfig',
...
)
Add Django Opt-out application’s URL patterns:
from django_opt_out import urls as django_opt_out_urls
urlpatterns = [
...
url(r'^', include(django_opt_out_urls)),
...
]
Api docs¶
django_opt_out package¶
Subpackages¶
django_opt_out.management package¶
Subpackages¶
-
class
django_opt_out.management.commands.opt_out_feedback_defaults.
Command
(stdout=None, stderr=None, no_color=False)[source]¶ Bases:
django.core.management.base.BaseCommand
-
data_files
= (('tags.csv', <class 'django_opt_out.resources.OptOutTagResource'>), ('feedback.csv', <class 'django_opt_out.resources.OptOutFeedbackResource'>), ('feedback-translations.csv', <class 'django_opt_out.resources.OptOutFeedbackTranslationResource'>))¶
-
handle
(*args, **options)[source]¶ The actual logic of the command. Subclasses must implement this method.
-
help
= 'Imports default opt-out feedback options to empty database'¶
-
Module contents¶
django_opt_out.plugins package¶
Subpackages¶
-
django_opt_out.plugins.sparkpost.hooks.
create_opt_out
(sender, request, email, data, **kwargs)[source]¶
Module contents¶
Submodules¶
django_opt_out.admin module¶
-
class
django_opt_out.admin.
OptOutAdmin
(model, admin_site)[source]¶ Bases:
import_export.admin.ImportExportMixin
,django.contrib.admin.options.ModelAdmin
-
date_hierarchy
= 'ts'¶
-
list_display
= ('ts', 'email', 'ip', 'host', 'ua')¶
-
list_filter
= ('ip', 'host', 'ua', 'feedback', 'tags')¶
-
media
¶
-
readonly_fields
= ('email', 'ts', 'comment', 'ip', 'host', 'ua', 'cookies', 'data', 'ssl')¶
-
resource_class
¶
-
-
class
django_opt_out.admin.
OptOutFeedbackAdmin
(model, admin_site)[source]¶ Bases:
import_export.admin.ImportExportMixin
,django.contrib.admin.options.ModelAdmin
-
inlines
= [<class 'django_opt_out.admin.OptOutFeedbackTranslationInline'>]¶
-
list_display
= ('text', 'slug', 'default', 'ordinal', 'all_tag_names')¶
-
list_filter
= ('tags', 'default')¶
-
media
¶
-
resource_class
¶
-
-
class
django_opt_out.admin.
OptOutFeedbackTranslationAdmin
(model, admin_site)[source]¶ Bases:
import_export.admin.ImportExportMixin
,django.contrib.admin.options.ModelAdmin
-
list_display
= ('text', 'language', 'feedback')¶
-
media
¶
-
resource_class
¶ alias of
django_opt_out.resources.OptOutFeedbackTranslationResource
-
django_opt_out.app_settings module¶
django_opt_out.apps module¶
django_opt_out.cli module¶
Console script for django-opt-out.
django_opt_out.factories module¶
-
class
django_opt_out.factories.
OptOutFactory
[source]¶ Bases:
factory.django.DjangoModelFactory
-
email
= <factory.faker.Faker object>¶
-
ts
= <factory.declarations.LazyFunction object>¶
-
-
class
django_opt_out.factories.
OptOutFeedbackFactory
[source]¶ Bases:
factory.django.DjangoModelFactory
-
text
= <factory.faker.Faker object>¶
-
-
class
django_opt_out.factories.
OptOutFeedbackTranslationFactory
[source]¶ Bases:
factory.django.DjangoModelFactory
-
feedback
= <factory.declarations.SubFactory object>¶
-
language
= 'pl'¶
-
text
= <factory.faker.Faker object>¶
-
-
class
django_opt_out.factories.
OptOutTagFactory
[source]¶ Bases:
factory.django.DjangoModelFactory
-
name
= <factory.faker.Faker object>¶
-
-
class
django_opt_out.factories.
OptOutTagValueFactory
[source]¶ Bases:
factory.django.DjangoModelFactory
-
opt_out
= <factory.declarations.SubFactory object>¶
-
tag
= <factory.declarations.SubFactory object>¶
-
-
class
django_opt_out.factories.
UserFactory
[source]¶ Bases:
factory.django.DjangoModelFactory
-
email
= <factory.faker.Faker object>¶
-
first_name
= <factory.faker.Faker object>¶
-
is_active
= True¶
-
is_staff
= False¶
-
last_name
= <factory.faker.Faker object>¶
-
username
= <factory.declarations.Sequence object>¶
-
django_opt_out.forms module¶
-
class
django_opt_out.forms.
OptOutForm
(data=None, files=None, auto_id='id_%s', prefix=None, initial=None, error_class=<class 'django.forms.utils.ErrorList'>, label_suffix=None, empty_permitted=False, instance=None, use_required_attribute=None)[source]¶ Bases:
django.forms.models.ModelForm
-
class
Meta
[source]¶ Bases:
object
-
fields
= ('email', 'feedback', 'comment')¶
-
labels
= {'comment': 'Please tell us what can we do better'}¶
-
model
¶ alias of
django_opt_out.models.OptOut
-
-
base_fields
= {'comment': <django.forms.fields.CharField object at 0x7fbb2bff9400>, 'email': <django.forms.fields.EmailField object at 0x7fbb2bff92b0>, 'feedback': <django_opt_out.forms.TranslatedMultipleChoiceField object at 0x7fbb2bff0f98>}¶
-
declared_fields
= {'feedback': <django_opt_out.forms.TranslatedMultipleChoiceField object at 0x7fbb2bff0f98>}¶
-
media
¶
-
required_css_class
= 'required'¶
-
class
django_opt_out.models module¶
-
class
django_opt_out.models.
OptOut
(id, email, ts, confirmed, data, comment, secret, ssl, host, ip, ua, cookies)[source]¶ Bases:
django_powerbank.db.models.base.BaseModel
-
exception
DoesNotExist
¶ Bases:
django.core.exceptions.ObjectDoesNotExist
-
exception
MultipleObjectsReturned
¶ Bases:
django.core.exceptions.MultipleObjectsReturned
-
comment
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
confirmed
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
data
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
email
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
feedback
¶ Accessor to the related objects manager on the forward and reverse sides of a many-to-many relation.
In the example:
class Pizza(Model): toppings = ManyToManyField(Topping, related_name='pizzas')
Pizza.toppings
andTopping.pizzas
areManyToManyDescriptor
instances.Most of the implementation is delegated to a dynamically defined manager class built by
create_forward_many_to_many_manager()
defined below.
-
get_next_by_ts
(*, field=<django.db.models.fields.DateTimeField: ts>, is_next=True, **kwargs)¶
-
get_previous_by_ts
(*, field=<django.db.models.fields.DateTimeField: ts>, is_next=False, **kwargs)¶
-
host
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
id
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
ip
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
objects
= <django.db.models.manager.Manager object>¶
-
save
(force_insert=False, force_update=False, using=None, update_fields=None)[source]¶ Save the current instance. Override this in a subclass if you want to control the saving process.
The ‘force_insert’ and ‘force_update’ parameters can be used to insist that the “save” must be an SQL insert or update (or equivalent for non-SQL backends), respectively. Normally, they should not be set.
-
secret
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
ssl
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
Accessor to the related objects manager on the reverse side of a many-to-one relation.
In the example:
class Child(Model): parent = ForeignKey(Parent, related_name='children')
Parent.children
is aReverseManyToOneDescriptor
instance.Most of the implementation is delegated to a dynamically defined manager class built by
create_forward_many_to_many_manager()
defined below.
-
ts
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
ua
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
exception
-
class
django_opt_out.models.
OptOutFeedback
(id, text, slug, default, ordinal)[source]¶ Bases:
django_powerbank.db.models.base.BaseModel
-
exception
DoesNotExist
¶ Bases:
django.core.exceptions.ObjectDoesNotExist
-
exception
MultipleObjectsReturned
¶ Bases:
django.core.exceptions.MultipleObjectsReturned
-
default
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
id
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
objects
= <django.db.models.manager.Manager object>¶
-
ordinal
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
out_outs
¶ Accessor to the related objects manager on the forward and reverse sides of a many-to-many relation.
In the example:
class Pizza(Model): toppings = ManyToManyField(Topping, related_name='pizzas')
Pizza.toppings
andTopping.pizzas
areManyToManyDescriptor
instances.Most of the implementation is delegated to a dynamically defined manager class built by
create_forward_many_to_many_manager()
defined below.
-
slug
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
Accessor to the related objects manager on the forward and reverse sides of a many-to-many relation.
In the example:
class Pizza(Model): toppings = ManyToManyField(Topping, related_name='pizzas')
Pizza.toppings
andTopping.pizzas
areManyToManyDescriptor
instances.Most of the implementation is delegated to a dynamically defined manager class built by
create_forward_many_to_many_manager()
defined below.
-
text
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
translations
¶ Accessor to the related objects manager on the reverse side of a many-to-one relation.
In the example:
class Child(Model): parent = ForeignKey(Parent, related_name='children')
Parent.children
is aReverseManyToOneDescriptor
instance.Most of the implementation is delegated to a dynamically defined manager class built by
create_forward_many_to_many_manager()
defined below.
-
exception
-
class
django_opt_out.models.
OptOutFeedbackTranslation
(id, feedback, language, text)[source]¶ Bases:
django_powerbank.db.models.base.BaseModel
-
exception
DoesNotExist
¶ Bases:
django.core.exceptions.ObjectDoesNotExist
-
exception
MultipleObjectsReturned
¶ Bases:
django.core.exceptions.MultipleObjectsReturned
-
feedback
¶ Accessor to the related object on the forward side of a many-to-one or one-to-one (via ForwardOneToOneDescriptor subclass) relation.
In the example:
class Child(Model): parent = ForeignKey(Parent, related_name='children')
Child.parent
is aForwardManyToOneDescriptor
instance.
-
feedback_id
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
get_language_display
(*, field=<django.db.models.fields.CharField: language>)¶
-
id
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
language
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
objects
= <django.db.models.manager.Manager object>¶
-
text
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
exception
-
class
django_opt_out.models.
OptOutTag
(id, name, description)[source]¶ Bases:
django_powerbank.db.models.base.BaseModel
-
exception
DoesNotExist
¶ Bases:
django.core.exceptions.ObjectDoesNotExist
-
exception
MultipleObjectsReturned
¶ Bases:
django.core.exceptions.MultipleObjectsReturned
-
description
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
feedback
¶ Accessor to the related objects manager on the forward and reverse sides of a many-to-many relation.
In the example:
class Pizza(Model): toppings = ManyToManyField(Topping, related_name='pizzas')
Pizza.toppings
andTopping.pizzas
areManyToManyDescriptor
instances.Most of the implementation is delegated to a dynamically defined manager class built by
create_forward_many_to_many_manager()
defined below.
-
id
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
name
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
objects
= <django.db.models.manager.Manager object>¶
Accessor to the related objects manager on the reverse side of a many-to-one relation.
In the example:
class Child(Model): parent = ForeignKey(Parent, related_name='children')
Parent.children
is aReverseManyToOneDescriptor
instance.Most of the implementation is delegated to a dynamically defined manager class built by
create_forward_many_to_many_manager()
defined below.
-
exception
-
class
django_opt_out.models.
OptOutTagValue
(id, opt_out, tag, value)[source]¶ Bases:
django_powerbank.db.models.base.BaseModel
-
exception
DoesNotExist
¶ Bases:
django.core.exceptions.ObjectDoesNotExist
-
exception
MultipleObjectsReturned
¶ Bases:
django.core.exceptions.MultipleObjectsReturned
-
id
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
objects
= <django.db.models.manager.Manager object>¶
-
opt_out
¶ Accessor to the related object on the forward side of a many-to-one or one-to-one (via ForwardOneToOneDescriptor subclass) relation.
In the example:
class Child(Model): parent = ForeignKey(Parent, related_name='children')
Child.parent
is aForwardManyToOneDescriptor
instance.
-
opt_out_id
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
tag
¶ Accessor to the related object on the forward side of a many-to-one or one-to-one (via ForwardOneToOneDescriptor subclass) relation.
In the example:
class Child(Model): parent = ForeignKey(Parent, related_name='children')
Child.parent
is aForwardManyToOneDescriptor
instance.
-
tag_id
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
value
¶ A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
-
exception
django_opt_out.resources module¶
-
class
django_opt_out.resources.
OptOutFeedbackResource
[source]¶ Bases:
import_export.resources.ModelResource
-
class
Meta
[source]¶ Bases:
object
-
export_order
= ('text',)¶
-
fields
= ('id', 'text', 'slug', 'default', 'ordinal', 'tags')¶
-
model
¶ alias of
django_opt_out.models.OptOutFeedback
-
-
fields
= {'default': <import_export.fields.Field: default>, 'id': <import_export.fields.Field: id>, 'ordinal': <import_export.fields.Field: ordinal>, 'slug': <import_export.fields.Field: slug>, 'tags': <import_export.fields.Field: tags>, 'text': <import_export.fields.Field: text>}¶
-
class
-
class
django_opt_out.resources.
OptOutFeedbackTranslationResource
[source]¶ Bases:
import_export.resources.ModelResource
-
class
Meta
[source]¶ Bases:
object
-
export_order
= ('text',)¶
-
fields
= ('id', 'feedback', 'text', 'language')¶
-
model
¶
-
-
fields
= {'feedback': <import_export.fields.Field: feedback>, 'id': <import_export.fields.Field: id>, 'language': <import_export.fields.Field: language>, 'text': <import_export.fields.Field: text>}¶
-
class
-
class
django_opt_out.resources.
OptOutResource
[source]¶ Bases:
import_export.resources.ModelResource
-
class
Meta
[source]¶ Bases:
object
-
export_order
= ('ts',)¶
-
fields
= ('email', 'ts', 'confirmed', 'data', 'comment', 'feedback', 'secret', 'ssl', 'ip', 'ua', 'cookies')¶
-
model
¶ alias of
django_opt_out.models.OptOut
-
-
fields
= {'comment': <import_export.fields.Field: comment>, 'confirmed': <import_export.fields.Field: confirmed>, 'cookies': <import_export.fields.Field: cookies>, 'data': <import_export.fields.Field: data>, 'email': <import_export.fields.Field: email>, 'feedback': <import_export.fields.Field: tags>, 'ip': <import_export.fields.Field: ip>, 'secret': <import_export.fields.Field: secret>, 'ssl': <import_export.fields.Field: ssl>, 'ts': <import_export.fields.Field: ts>, 'ua': <import_export.fields.Field: ua>}¶
-
class
-
class
django_opt_out.resources.
OptOutTagResource
[source]¶ Bases:
import_export.resources.ModelResource
-
class
Meta
[source]¶ Bases:
object
-
export_order
= ('name',)¶
-
fields
= ('id', 'name')¶
-
model
¶ alias of
django_opt_out.models.OptOutTag
-
-
fields
= {'id': <import_export.fields.Field: id>, 'name': <import_export.fields.Field: name>}¶
-
class
-
class
django_opt_out.resources.
OptOutTagValueResource
[source]¶ Bases:
import_export.resources.ModelResource
-
class
Meta
[source]¶ Bases:
object
-
export_order
= ('text',)¶
-
fields
= ('opt_out', 'tag', 'value')¶
-
model
¶ alias of
django_opt_out.models.OptOutTagValue
-
-
fields
= {'opt_out': <import_export.fields.Field: tags>, 'tag': <import_export.fields.Field: tags>, 'value': <import_export.fields.Field: value>}¶
-
class
django_opt_out.signals module¶
django_opt_out.urls module¶
django_opt_out.utils module¶
django_opt_out.views module¶
-
class
django_opt_out.views.
OptOutBase
(**kwargs)[source]¶ Bases:
django_powerbank.views.auth.AbstractAccessView
-
class
django_opt_out.views.
OptOutConfirm
(**kwargs)[source]¶ Bases:
pascal_templates.views.CreateView
-
form_class
¶ alias of
django_opt_out.forms.OptOutForm
-
model
¶ alias of
django_opt_out.models.OptOut
-
template_name
= 'django_opt_out/OptOut/form.html'¶
-
-
class
django_opt_out.views.
OptOutRemoved
(**kwargs)[source]¶ Bases:
django.views.generic.base.TemplateView
-
template_name
= 'django_opt_out/OptOut/removed.html'¶
-
-
class
django_opt_out.views.
OptOutSuccess
(**kwargs)[source]¶ Bases:
django_opt_out.views.OptOutBase
,pascal_templates.views.DetailView
-
model
¶ alias of
django_opt_out.models.OptOut
-
template_name
= 'django_opt_out/OptOut/success.html'¶
-
-
class
django_opt_out.views.
OptOutUpdate
(**kwargs)[source]¶ Bases:
django_opt_out.views.OptOutBase
,pascal_templates.views.UpdateView
-
form_class
¶ alias of
django_opt_out.forms.OptOutForm
-
model
¶ alias of
django_opt_out.models.OptOut
-
template_name
= 'django_opt_out/OptOut/form.html'¶
-
Module contents¶
Top-level package for Django Opt-out application.
Contributing¶
Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given.
You can contribute in many ways:
Types of Contributions¶
Report Bugs¶
Report bugs at https://github.com/wooyek/django-opt-out/issues.
If you are reporting a bug, please include:
- Your operating system name and version.
- Any details about your local setup that might be helpful in troubleshooting.
- Detailed steps to reproduce the bug.
Fix Bugs¶
Look through the GitHub issues for bugs. Anything tagged with “bug” and “help wanted” is open to whoever wants to implement it.
Implement Features¶
Look through the GitHub issues for features. Anything tagged with “enhancement” and “help wanted” is open to whoever wants to implement it.
Write Documentation¶
Django Opt-out application could always use more documentation, whether as part of the official Django Opt-out application docs, in docstrings, or even on the web in blog posts, articles, and such.
Submit Feedback¶
The best way to send feedback is to file an issue at https://github.com/wooyek/django-opt-out/issues.
If you are proposing a feature:
- Explain in detail how it would work.
- Keep the scope as narrow as possible, to make it easier to implement.
- Remember that this is a volunteer-driven project, and that contributions are welcome :)
Get Started!¶
Ready to contribute? Here’s how to set up django-opt-out for local development.
Fork the django-opt-out repo on github.com
Clone your fork locally:
$ git clone git@github.com:your_name_here/django-opt-out.git
Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:
$ mkvirtualenv django-opt-out $ cd django-opt-out/ $ python setup.py develop
Create a branch for local development:
$ git checkout -b name-of-your-bugfix-or-feature
Now you can make your changes locally.
When you’re done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:
$ flake8 django-opt-out tests $ tox -e check $ python setup.py test or py.test $ tox
To get flake8 and tox, just pip install them into your virtualenv.
Commit your changes and push your branch to GitHub:
$ git add . $ git commit -m "Your detailed description of your changes." $ git push origin name-of-your-bugfix-or-feature
Submit a pull request through the GitHub website.
Pull Request Guidelines¶
Before you submit a pull request, check that it meets these guidelines:
- The pull request should include tests.
- If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in README.rst.
- The pull request should work for Python versions mentioned in tox.ini file. Check https://travis-ci.org/wooyek/django-opt-out/pull_requests and make sure that the tests pass for all supported Python versions.
Credits¶
Development Lead¶
- @wooyek: Janusz Skonieczny <js+pypi@bravelabs.pl>
Contributors¶
None yet. Why not be the first?