Welcome to Zamboni’s documentation!¶
Zamboni is one of the codebases for https://marketplace.firefox.com/
The source lives at https://github.com/mozilla/zamboni
Installation¶
Before you install zamboni, we strongly recommend you start with the Marketplace Documentation which illustrates how the Marketplace is comprised of multiple components, one of which is zamboni.
What are you waiting for?! Install Zamboni!
Want to know about how development at Mozilla works, including style guides? Mozilla Bootcamp
Contents¶
Install Zamboni¶
Installing Zamboni¶
We’re going to use all the hottest tools to set up a nice environment. Skip steps at your own peril. Here we go!
Need help?¶
Come talk to us on irc://irc.mozilla.org/marketplace if you have questions, issues, or compliments.
1. Installing dependencies¶
On OS X¶
The best solution for installing UNIX tools on OS X is Homebrew.
The following packages will get you set for zamboni:
brew install python libxml2 mysql openssl swig304 jpeg pngcrush redis
On Ubuntu¶
The following command will install the required development files on Ubuntu or, if you’re running a recent version, you can install them automatically:
sudo aptitude install python-dev python-virtualenv libxml2-dev libxslt1-dev libmysqlclient-dev libssl-dev swig openssl curl pngcrush redis-server
Services¶
Zamboni has three dependencies you must install and have running:
- MySQL should require no configuration.
- Redis should require no configuration.
- Seee elasticsearch for setup and configuration.
2. Grab the source¶
Grab zamboni from github with:
git clone --recursive git://github.com/mozilla/zamboni.git
cd zamboni
zamboni.git
is all the source code. updating is detailed later on.
If at any point you realize you forgot to clone with the recursive flag, you can fix that by running:
git submodule update --init --recursive
3. Setup a virtualenv¶
virtualenv is a tool to create isolated Python environments. This will let you put all of Zamboni’s dependencies in a single directory rather than your global Python directory. For ultimate convenience, we’ll also use virtualenvwrapper which adds commands to your shell.
Since each shell setup is different, you can install everything you need and configure your shell using the virtualenv-burrito. Type this:
curl -sL https://raw.githubusercontent.com/brainsik/virtualenv-burrito/master/virtualenv-burrito.sh | $SHELL
Open a new shell to test it out. You should have the workon
and
mkvirtualenv
commands.
4. Getting Packages¶
Now we’re ready to go, so create an environment for zamboni:
mkvirtualenv zamboni
That creates a clean environment named zamboni using Python 2.7. You can get
out of the environment by restarting your shell or calling deactivate
.
To get back into the zamboni environment later, type:
workon zamboni # requires virtualenvwrapper
Note
Zamboni requires at least Python 2.7.0, production is using Python 2.7.5.
Note
If you want to use a different Python binary, pass the name (if it is
on your path) or the full path to mkvirtualenv with --python
:
mkvirtualenv --python=/usr/local/bin/python2.7 zamboni
Note
If you are using an older version of virtualenv that defaults to
using system packages you might need to pass --no-site-packages
:
mkvirtualenv --no-site-packages zamboni
First make sure you have a recent `pip`_ for security reasons. From inside your activated virtualenv, install the required python packages:
make update_deps
Issues at this point? See Trouble-shooting the development installation.
5. Settings¶
Most of zamboni is already configured in mkt.settings.py
, but there’s one thing
you’ll need to configure locally, the database. The easiest way to do that
is by setting an environment variable (see next section).
Optionally you can create a local settings file and place anything custom
into settings_local.py
.
Any file that looks like settings_local*
is for local use only; it will be
ignored by git.
Environment settings¶
Out of the box, zamboni should work without any need for settings changes. Some settings are configurable from the environment. See the marketplace docs for information on the environment variables and how they affect zamboni.
6. Setting up a Mysql Database¶
Django provides commands to create the database and tables needed, and load essential data:
./manage.py migrate
./manage.py loaddata init
Database Migrations¶
Each incremental change we add to the database is done with Django migrations. To keep your local DB fresh and up to date, run migrations like this:
./manage.py migrate
Loading Test Apps¶
Fake apps and feed collections can be created by running:
./manage.py generate_feed
Specific example applications can be loaded by running:
./manage.py generate_apps_from_spec data/apps/test_apps.json
See Fake App Data for details of the JSON format.
If you just want a certain number of public apps in various categories to be created, run:
./manage.py generate_apps N
where N is the number of apps you want created in your database.
7. Check it works¶
If you’ve gotten the system requirements, downloaded zamboni
,
set up your virtualenv with the compiled packages, and
configured your settings and database, you’re good to go:
./manage.py runserver
Hit:
http://localhost:2600/services/monitor
This will report any errors or issues in your installation.
8. Create an admin user¶
Chances are that for development, you’ll want an admin account.
After logging in, run this management command:
./manage.py addusertogroup <your email> 1
9. Setting up the pages¶
To set up the assets for the developer hub, reviewer tools, or admin pages:
npm install
python manage.py compress_assets
For local development, it would also be good to set:
TEMPLATE_DEBUG = True
Post installation¶
To keep your zamboni up to date with the latest changes in source files, requrirements and database migrations run:
make full_update
Advanced Installation¶
In production we use things like memcached, rabbitmq + celery and Stylus. Learn more about installing these on the Optional installs page.
Note
Although we make an effort to keep advanced items as optional installs you might need to install some components in order to run tests or start up the development server.
Optional installs¶
MySQL¶
On your dev machine, MySQL probably needs some tweaks. Locate your my.cnf (or create one) then, at the very least, make UTF8 the default encoding:
[mysqld]
character-set-server=utf8
Here are some other helpful settings:
[mysqld]
default-storage-engine=innodb
character-set-server=utf8
skip-sync-frm=OFF
innodb_file_per_table
On Mac OS X with homebrew, put my.cnf in /usr/local/Cellar/mysql/5.5.15/my.cnf
then restart like:
launchctl unload -w ~/Library/LaunchAgents/com.mysql.mysqld.plist
launchctl load -w ~/Library/LaunchAgents/com.mysql.mysqld.plist
Note
some of the options above were renamed between MySQL versions
Here are more tips for optimizing MySQL on your dev machine.
Memcached¶
By default zamboni uses an in memory cache. To install memcached
libmemcached-dev
on Ubuntu and libmemcached
on OS X. Alter your
local settings file to use:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': ['localhost:11211'],
'TIMEOUT': 500,
}
}
RabbitMQ and Celery¶
By default zamboni automatically processes jobs without needing Celery.
See the Celery page for installation instructions. The
example settings set CELERY_ALWAYS_EAGER = True
.
If you’re setting up RabbitMQ and want to use celery worker
you will need to
alter your local settings file to set this up.
See Celery for more instructions.
Node.js¶
Node.js is needed for Stylus and LESS, which in turn are needed to precompile the CSS files.
If you want to serve the CSS files from another domain than the webserver, you
will need to precompile them. Otherwise you can have them compiled on the fly,
using javascript in your browser, if you set LESS_PREPROCESS = False
in
your local settings.
First, we need to install node and npm:
brew install node
curl http://npmjs.org/install.sh | sh
Optionally make the local scripts available on your path if you don’t already have this in your profile:
export PATH="./node_modules/.bin/:${PATH}"
- Not working?
- If you’re having trouble installing node, try http://shapeshed.com/journal/setting-up-nodejs-and-npm-on-mac-osx/. You need brew, which we used earlier.
- If you’re having trouble with npm, check out the README on https://github.com/isaacs/npm
Stylus CSS¶
Learn about Stylus at http://learnboost.github.com/stylus/
cd zamboni
npm install
In your settings_local.py
(or settings_local_mkt.py
) ensure you are
pointing to the correct executable for stylus
:
STYLUS_BIN = path('node_modules/stylus/bin/stylus')
Celery¶
Celery is a task queue powered by RabbitMQ. You can use it for anything that doesn’t need to complete in the current request-response cycle. Or use it wherever Les tells you to use it.
For example, each addon has a current_version
cached property. This query
on initial run causes strain on our database. We can create a denormalized
database field called current_version
on the addons
table.
We’ll need to populate regularly so it has fairly up-to-date data. We can do this in a process outside the request-response cycle. This is where Celery comes in.
Installation¶
RabbitMQ¶
Celery depends on RabbitMQ. If you use homebrew
you can install this:
brew install rabbitmq
Setting up rabbitmq involves some configuration. You may want to define the following
# On a Mac, you can find this in System Preferences > Sharing
export HOSTNAME='<laptop name>.local'
Then run the following commands:
# Set your host up so it's semi-permanent
sudo scutil --set HostName $HOSTNAME
# Update your hosts by either:
# 1) Manually editing /etc/hosts
# 2) `echo 127.0.0.1 $HOSTNAME >> /etc/hosts`
# RabbitMQ insists on writing to /var
sudo rabbitmq-server -detached
# Setup rabitty things (sudo is required to read the cookie file)
sudo rabbitmqctl add_user zamboni zamboni
sudo rabbitmqctl add_vhost zamboni
sudo rabbitmqctl set_permissions -p zamboni zamboni ".*" ".*" ".*"
Back in safe and happy django-land you should be able to run:
./manage.py celery worker -Q priority,devhub,images,limited $OPTIONS
Celery understands python and any tasks that you have defined in your app are now runnable asynchronously.
Celery Tasks¶
Any python function can be set as a celery task. For example, let’s say we want
to update our current_version
but we don’t care how quickly it happens, just
that it happens. We can define it like so:
@task(rate_limit='2/m')
def _update_addons_current_version(data, **kw):
task_log.debug("[%s@%s] Updating addons current_versions." %
(len(data), _update_addons_current_version.rate_limit))
for pk in data:
try:
addon = Webapp.objects.get(pk=pk)
addon.update_version()
except Webapp.DoesNotExist:
task_log.debug("Missing addon: %d" % pk)
@task
is a decorator for Celery to find our tasks. We can specify a
rate_limit
like 2/m
which means celery worker
will only run
this command 2 times a minute at most. This keeps write-heavy tasks from
killing your database.
If we run this command like so:
from celery.task.sets import TaskSet
all_pks = Webapp.objects.all().values_list('pk', flat=True)
ts = [_update_addons_current_version.subtask(args=[pks])
for pks in mkt.site.utils.chunked(all_pks, 300)]
TaskSet(ts).apply_async()
All the Webapps with ids in pks
will (eventually) have their
current_versions
updated.
Cron Jobs¶
This is all good, but let’s automate this. In Zamboni we can create cron jobs like so:
@cronjobs.register
def update_addons_current_version():
"""Update the current_version field of the addons."""
d = Webapp.objects.valid().values_list('id', flat=True)
with establish_connection() as conn:
for chunk in chunked(d, 1000):
print chunk
_update_addons_current_version.apply_async(args=[chunk],
connection=conn)
This job will hit all the addons and run the task we defined in small batches of 1000.
We’ll need to add this to both the prod
and preview
crontabs so that
they can be run in production.
Better than Cron¶
Of course, cron is old school. We want to do better than cron, or at least not rely on brute force tactics.
For a surgical strike, we can call _update_addons_current_version
any time
we add a new version to that addon. Celery will execute it at the prescribed
rate, and your data will be updated ... eventually.
During Development¶
celery worker
only knows about code as it was defined at instantiation time.
If you change your @task
function, you’ll need to HUP
the process.
However, if you’ve got the @task
running perfectly you can tweak all the
code, including cron jobs that call it without need of restart.
Elasticsearch¶
Elasticsearch is a search server. Documents (key-values) get stored, configurable queries come in, Elasticsearch scores these documents, and returns the most relevant hits.
Installation¶
You can download the Elasticsearch code and run elasticsearch directly from this folder. This makes it easy to upgrade or test new versions as needed. Optionally you can install Elasticsearch using your preferred system package manager.
We are currently using Elasticsearch version 1.6.2. You can install by doing the following:
curl -O https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-1.6.2.tar.gz
tar -xvzf elasticsearch-1.6.2.tar.gz
cd elasticsearch-1.6.2
For running Marketplace you must install the ICU Analysis Plugin:
./bin/plugin -install elasticsearch/elasticsearch-analysis-icu/2.6.0
For more about the ICU plugin, see the ICU Github Page.
Settings¶
cluster.name: wooyeah
# Don't try to cluster with other machines during local development.
# Remove the following 3 lines to enable default clustering.
network.host: localhost
discovery.zen.ping.multicast.enabled: false
discovery.zen.ping.unicast.hosts: ["localhost"]
script.disable_dynamic: false
path:
logs: /usr/local/var/log
data: /usr/local/var/data
We use a custom analyzer for indexing add-on names since they’re a little different from normal text.
To get the same results as our servers, configure Elasticsearch by copying the
scripts/elasticsearch/elasticsearch.yml (available in the
scripts/elasticsearch/
folder of your install) to your system.
For example, copy it to the local directory so it’s nearby when you launch Elasticsearch:
cp /path/to/zamboni/scripts/elasticsearch/elasticsearch.yml .
If you don’t do this your results will be slightly different, but you probably won’t notice.
Launching and Setting Up¶
Launch the Elasticsearch service:
./bin/elasticsearch -Des.config=elasticsearch.yml
Zamboni has commands that sets up mappings and indexes for you. Setting up the mappings is analagous to defining the structure of a table, indexing is analagous to storing rows.
It is worth noting that the index is maintained incrementally through post_save and post_delete hooks.
Use this to create the apps index and index apps:
./manage.py reindex --index=apps
Or you could use the makefile target (using the settings_local.py
file):
make reindex
If you need to use another settings file and add arguments:
make SETTINGS=settings_other ARGS='--force' reindex
Querying Elasticsearch in Django¶
We use Elasticsearch DSL, a Python library that gives us a search API to elasticsearch.
On Marketplace, apps use mkt/webapps/indexers:WebappIndexer
as its
interface to Elasticsearch:
query_results = WebappIndexer.search().query(...).filter(...).execute()
Testing with Elasticsearch¶
All test cases using Elasticsearch should inherit from mkt.site.tests.ESTestCase
.
All such tests will be skipped by the test runner unless:
RUN_ES_TESTS = True
This is done as a performance optimization to keep the run time of the test suite down, unless necessary.
Troubleshooting¶
I got a CircularReference error on .search() - check that a whole object is not being passed into the filters, but rather just a field’s value.
I indexed something into Elasticsearch, but my query returns nothing - check whether the query contains upper-case letters or hyphens. If so, try lowercasing your query filter. For hyphens, set the field’s mapping to not be analyzed:
'my_field': {'type': 'string', 'index': 'not_analyzed'}
Packaging in Zamboni¶
There are two ways of getting packages for zamboni. The first is to install everything using pip. We have our packages separated into three files:
- requirements/compiled.txt
- All packages that require (or go faster with) compilation. These can’t be distributed cross-platform, so they need to be installed through your system’s package manager or pip.
- requirements/prod.txt
- The minimal set of packages you need to run zamboni in production. You
also need to get
requirements/compiled.txt
. - requirements/dev.txt
- All the packages needed for running tests and development servers. This
automatically includes
requirements/prod.txt
.
Installing through pip¶
You can get a development environment with
pip install --no-deps -r requirements/dev.txt
Adding new packages¶
Note: this is deprecated, all packages should be added in requirements.
The vendor repo was seeded with
pip install --no-install --build=vendor/packages --src=vendor/src -I -r requirements/dev.txt
Then I added everything in /packages
and set up submodules in /src
(see
below). We’ll be keeping this up to date through Hudson, but if you add new
packages you should seed them yourself.
If we wanted to add a new dependency called cheeseballs
to zamboni, you
would add it to requirements/prod.txt
or requirements/dev.txt
and then
do
pip install --no-install --build=vendor/packages --src=vendor/src -I cheeseballs
Then you need to update vendor/zamboni.pth
. Python uses .pth
files to
dynamically add directories to sys.path
(docs).
I created zamboni.pth
with this:
find packages src -type d -depth 1 > zamboni.pth
html5lib
and selenium
are troublesome, so they need to be sourced with
packages/html5lib/src
and packages/selenium/src
. Hopefully you won’t
hit any snags like that.
Adding submodules¶
Note: this is deprecated, all packages should be added in requirements.
for f in src/*
pushd $f >/dev/null && REPO=$(git config remote.origin.url) && popd > /dev/null && git submodule add $REPO $f
Holy readability batman!
Trouble-shooting the development installation¶
M2Crypto installation¶
If you are on a Linux box and get a compilation error while installing M2Crypto like the following:
SWIG/_m2crypto_wrap.c:6116:1: error: unknown type name ‘STACK’
... snip a very long output of errors around STACK...
SWIG/_m2crypto_wrap.c:23497:20: error: expected expression before ‘)’ token
result = (STACK *)pkcs7_get0_signers(arg1,arg2,arg3);
^
error: command 'gcc' failed with exit status 1
It may be because of a few reasons:
comment the line starting with
M2Crypto
inrequirements/compiled.txt
install the patched package from the Debian repositories (replace
x86_64-linux-gnu
byi386-linux-gnu
if you’re on a 32bits platform):DEB_HOST_MULTIARCH=x86_64-linux-gnu pip install -I --exists-action=w "git+git://anonscm.debian.org/collab-maint/m2crypto.git@debian/0.21.1-3#egg=M2Crypto" pip install --no-deps -r requirements/dev.txt
revert your changes to
requirements/compiled.txt
:git checkout requirements/compiled.txt
Pillow¶
As of Mac OS X Mavericks, you might see this error when pip builds Pillow:
clang: error: unknown argument: '-mno-fused-madd' [-Wunused-command-line-argument-hard-error-in-future]
clang: note: this will be a hard error (cannot be downgraded to a warning) in the future
error: command 'cc' failed with exit status 1
You can solve this by setting these environment variables in your shell
before running pip install ...
:
export CFLAGS=-Qunused-arguments
export CPPFLAGS=-Qunused-arguments
pip install ...
More info: http://stackoverflow.com/questions/22334776/installing-pillow-pil-on-mavericks/22365032
Image processing isn’t working¶
If adding images to apps or extensions doesn’t seem to work then there’s a couple of settings you should check.
Checking your PIL installation (Ubuntu)¶
When you run you should see at least JPEG and ZLIB are supported
If that’s the case you should see this in the output of pip install -I PIL
:
--------------------------------------------------------------------
*** TKINTER support not available
--- JPEG support available
--- ZLIB (PNG/ZIP) support available
*** FREETYPE2 support not available
*** LITTLECMS support not available
--------------------------------------------------------------------
If you don’t then this suggests PIL can’t find your image libraries:
To fix this double-check you have the necessary development libraries
installed first (e.g: sudo apt-get install libjpeg-dev zlib1g-dev
)
Now run the following for 32bit:
sudo ln -s /usr/lib/i386-linux-gnu/libz.so /usr/lib
sudo ln -s /usr/lib/i386-linux-gnu/libjpeg.so /usr/lib
Or this if your running 64bit:
sudo ln -s /usr/lib/x86_64-linux-gnu/libz.so /usr/lib
sudo ln -s /usr/lib/x86_64-linux-gnu/libjpeg.so /usr/lib
Note
If you don’t know what arch you are running run uname -m
if the
output is x86_64
then it’s 64-bit, otherwise it’s 32bit
e.g. i686
Now re-install PIL:
pip install -I PIL
And you should see the necessary image libraries are now working with PIL correctly.
ES is timing out¶
This can be caused by number_of_replicas
not being set to 0 for the local indexes.
To check the settings run:
curl -s localhost:9200/_cluster/state\?pretty | fgrep number_of_replicas -B 5
If you see any that aren’t 0 do the following:
Set ES_DEFAULT_NUM_REPLICAS
to 0
in your local settings.
To set them to zero immediately run:
curl -XPUT localhost:9200/_settings -d '{ "index" : { "number_of_replicas" : 0 } }'
Hacking¶
Testing¶
We’re using a mix of Django’s Unit Testing and nose
.
Running Tests¶
To run the whole shebang use:
python manage.py test
There are a lot of options you can pass to adjust the output. Read the docs for the full set, but some common ones are:
-P
to prevent nose adding the lib directory to the path.--noinput
tells Django not to ask about creating or destroying test databases.--logging-clear-handlers
tells nose that you don’t want to see any logging output. Without this, our debug logging will spew all over your console during test runs. This can be useful for debugging, but it’s not that great most of the time. See the docs for more stuff you can do withnose and logging
.
Our continuous integration server adds some additional flags for other features (for example, coverage statistics). To see what those commands are check out the .travis.yml file.
There are a few useful makefile targets that you can use:
Run all the tests:
make test
If you need to rebuild the database:
make test_force_db
To fail and stop running tests on the first failure:
make tdd
If you wish to add arguments, or run a specific test, overload the variables (check the Makefile for more information):
make SETTINGS=settings_mkt ARGS='--verbosity 2 zamboni.mkt.site.tests.test_url_prefix:MiddlewareTest.test_get_app' test
Those targets include some useful options, like the --with-id
which allows
you to re-run only the tests failed from the previous run:
make test_failed
Database Setup¶
If you want to re-use your database instead of making a new one every time you
run tests, set the environment variable REUSE_DB
.
REUSE_DB=1 python manage.py test
Writing Tests¶
We support two types of automated tests right now and there are some details below but remember, if you’re confused look at existing tests for examples.
Unit/Functional Tests¶
Most tests are in this category. Our test classes extend
django.test.TestCase
and follow the standard rules for unit tests.
We’re using JSON fixtures for the data.
External calls¶
Connecting to remote services in tests is not recommended, developers should mock_ out those calls instead.
To enforce this we run Jenkins with the nose-blockage plugin, that will raise errors if you have an HTTP calls in your tests apart from calls to the domains 127.0.0.1 and localhost.
Why Tests Fail¶
Tests usually fail for one of two reasons: The code has changed or the data has changed. An third reason is time. Some tests have time-dependent data usually in the fixtues. For example, some featured items have expiration dates.
We can usually save our future-selves time by setting these expirations far in the future.
Localization Tests¶
If you want test that your localization works then you can add in locales
in the test directory. For an example see devhub/tests/locale
. These locales
are not in the normal path so should not show up unless you add them to the
LOCALE_PATH. If you change the .po files for these test locales, you will
need to recompile the .mo files manually, for example:
msgfmt --check-format -o django.mo django.po
Testing emails¶
By default in a non-production enviroment the setting REAL_EMAIL is set to
False, which prevents emails from being sent to addresses during testing with
live data. The contents of the emails are saved in the database instead and
can be read with the Fake email admin tool at /admin/mail
.
Sending actual email¶
In some circumstance you want to still recieve some emails, even when
REAL_EMAIL is False. To allow addresses to receive emails, rather
than be redirected to /admin/mail
, use mkt.zadmin.models.set_config
to
set the real_email_allowed_regex
key to a comma separated list of valid
emails in regex format:
from mkt.zadmin.models import set_config
set_config('real_email_allowed_regex', '.+@mozilla\.com$,you@who\.ca$')
Contributing¶
The easiest way to let us know about your awesome work is to send a pull request on github or in IRC. Point us to a branch with your new code and we’ll go from there. You can attach a patch to a bug if you’re more comfortable that way.
Please read the style.
The Perfect Git Configuration¶
We’re going to talk about two git repositories:
- origin will be the main zamboni repo at http://github.com/mozilla/zamboni.
- mine will be your fork at http://github.com/:user/zamboni.
There should be something like this in your .git/config
already:
[remote "origin"]
url = git://github.com/mozilla/zamboni.git
fetch = +refs/heads/*:refs/remotes/origin/*
Now we’ll set up your master to pull directly from the upstream zamboni:
[branch "master"]
remote = origin
merge = master
rebase = true
This can also be done through the git config
command (e.g.
git config branch.master.remote origin
) but editing .git/config
is
often easier.
After you’ve forked the repository on github, tell git about your new repo:
git remote add -f mine git@github.com:user/zamboni.git
Make sure to replace user with your name.
Working on a Branch¶
Let’s work on a bug in a branch called my-bug:
git checkout -b my-bug master
Now we’re switched to a new branch that was copied from master. We like to work on feature branches, but the master is still moving along. How do we keep up?
git fetch origin && git rebase origin/master
If you want to keep the master branch up to date, do it this way:
git checkout master && git pull && git checkout @{-1} && git rebase master
That updated master and then switched back to update our branch.
Publishing your Branch¶
The syntax is git push <repository> <branch>
. Here’s how to push the
my-bug
branch to your clone:
git push mine my-bug
Push From Master¶
We deploy from the master branch once a week. If you commit something to master that needs additional QA time, be sure to use a waffle feature flag.
Local Branches¶
Most new code is developed in local one-off branches, usually encompassing one or two patches to fix a bug. Upstream doesn’t care how you do local development, but we don’t want to see a merge commit every time you merge a single patch from a branch. Merge commits make for a noisy history, which is not fun to look at and can make it difficult to cherry-pick hotfixes to a release branch. We do like to see merge commits when you’re committing a set of related patches from a feature branch. The rule of thumb is to rebase and use fast-forward merge for single patches or a branch of unrelated bug fixes, but to use a merge commit if you have multiple commits that form a cohesive unit.
Here are some tips on Using topic branches and interactive rebasing effectively.
Access Control Lists¶
ACL versus Django Permissions¶
Currently we use the is_superuser
flag in the User
model to indicate that a
user can access the admin site.
Outside of that we use the GroupUser
to define what
access groups a user is a part of. We store this in request.groups
.
How permissions work¶
Permissions that you can use as filters can be either explicit or general.
For example Admin:EditAddons
means only someone with that permission will
validate.
If you simply require that a user has some permission in the Admin group
you can use Admin:%
. The %
means “any.”
Similarly a user might be in a group that has explicit or general permissions.
They may have Admin:EditAddons
which means they can see things with that
same permission, or things that require Admin:%
.
If a user has a wildcard, they will have more permissions. For example,
Admin:*
means they have permission to see anything that begins with
Admin:
.
The notion of a superuser has a permission of *:*
and therefore they can
see everything.
Fake App Data¶
The generate_apps_from_spec command-line tool loads a JSON file containing an array of fake app objects. The fields that can be specified in these objects are:
- type
- Required. One of “hosted”, “web”, or “privileged”, to specify a hosted app, unprivileged packaged app, or privileged packaged app.
- status
- Required. A string representing the app status, as listed in
mkt.constants.base.STATUS_CHOICES_API
. - name
- The display name for the app.
- num_ratings
- Number of user ratings to create for this app.
- num_previews
- Number of screenshots to create for this app.
- preview_files
- List of paths (relative to the JSON file) of preview images.
- video_files
- List of paths (relative to the JSON file) of preview videos in webm format.
- num_locales
- Number of locales to localize this app’s name in (max 5).
- versions
- An array of objects with optional version-specific fields: “status”, “type”, and “permissions”. Additional versions with these fields are created, oldest first.
- permissions
- An array of app permissions, to be placed in the manifest.
- manifest_file
- Path (relative to the JSON file) of a manifest to create this app from.
- description
- Description text for the app.
- categories
- A list of category names to create the app in.
- developer_name
- Display name for the app author.
- developer_email
- An email address for the app author.
- device_types
- A list of devices the app is available on: one or more of “desktop”, “mobile”, “tablet”, and “firefoxos”.
- premium_type
- Payment status for the app. One of “free”, “premium”, “premium-inapp”, “free-inapp”, or “other”.
- price
- The price (in dollars) for the app.
- privacy_policy
- Privacy policy text.
- rereview
- Boolean indicating whether to place app in rereview queue.
- special_regions
- An object with region names as keys, and status strings as values. Adds app to special region with given status.
Logging¶
Logging is fun. We all want to be lumberjacks. My muscle-memory wants to put
print
statements everywhere, but it’s better to use log.debug
instead.
Plus, django-debug-toolbar
can hijack the logger and show all the log
statements generated during the last request. When DEBUG = True
, all logs
will be printed to the development console where you started the server. In
production, we’re piping everything into syslog
.
Configuration¶
The root logger is set up from log_settings.py
in the base of zamboni’s
tree. It sets up sensible defaults, but you can twiddle with these settings:
LOG_LEVEL
This setting is required, and defaults to
loggging.DEBUG
, which will let just about anything pass through. To reconfigure, import logging in your settings file and pick a different level:import logging LOG_LEVEL = logging.WARN
HAS_SYSLOG
- Set this to
False
if you don’t want logging sent to syslog whenDEBUG
isFalse
. LOGGING
See PEP 391 and log_settings.py for formatting help. Each section of LOGGING will get merged into the corresponding section of log_settings.py. Handlers and log levels are set up automatically based on LOG_LEVEL and DEBUG unless you set them here. Messages will not propagate through a logger unless propagate: True is set.
LOGGING = { 'loggers': { 'foobar': {'handlers': ['null']}, }, }
If you want to add more to this in
settings_local.py
, do something like this:LOGGING['loggers'].update({ 'z.paypal': { 'level': logging.DEBUG, }, 'z.elasticsearch': { 'handlers': ['null'], }, })
Using Loggers¶
The logging
package uses global objects to make the same logging
configuration available to all code loaded in the interpreter. Loggers are
created in a pseudo-namespace structure, so app-level loggers can inherit
settings from a root logger. zamboni’s root namespace is just "z"
, in the
interest of brevity. In the foobar package, we create a logger that inherits
the configuration by naming it "z.foobar"
:
import commonware.log
log = commonware.log.getLogger('z.foobar')
log.debug("I'm in the foobar package.")
Logs can be nested as much as you want. Maintaining log namespaces is useful because we can turn up the logging output for a particular section of zamboni without becoming overwhelmed with logging from all other parts.
commonware.log vs. logging¶
commonware.log.getLogger
should be used inside the request cycle. It
returns a LoggingAdapter
that inserts the current user’s IP address into
the log message.
Complete logging docs: http://docs.python.org/library/logging.html
Services¶
Services contain a couple of scripts that are run as seperate wsgi instances on the services. Usually they are hosted on seperate domains. They are stand alone wsgi scripts. The goal is to avoid a whole pile of Django imports, middleware, sessions and so on that we really don’t need.
To run the scripts you’ll want a wsgi server. You can do this using gunicorn, for example:
pip install gunicorn
Then you can do:
cd services
gunicorn --log-level=DEBUG -c wsgi/receiptverify.py -b 127.0.0.1:9000 --debug verify:application
To test:
curl -d "this is a bogus receipt" http://127.0.0.1:9000/verify/123
Translations¶
gettext in JavaScript¶
We have gettext in JavaScript! Just mark your strings with gettext()
or
ngettext()
. There isn’t an _
alias right now, since underscore.js has
that. If we end up with a lot of JS translations, we can fix that. Check it
out:
cd locale
./extract-po.py -d javascript
pybabel init -l en_US -d . -i javascript.pot -D javascript
perl -pi -e 's/fuzzy//' en_US/LC_MESSAGES/javascript.po
pybabel compile -d . -D javascript
open http://0:8000/en-US/jsi18n/
Model fields¶
The translations
app defines a Translation
model, but for the most part, you shouldn’t have to use that directly. When you
want to create a foreign key to the translations
table, use
translations.fields.TranslatedField
. This subclasses Django’s
django.db.models.ForeignKey
to make it work with our special handling
of translation rows.
A minimal model with translations in zamboni would look like this:
from django.db import models
import mkt.site.models
import mkt.translations
class MyModel(mkt.site.models.ModelBase):
description = translations.fieldsTranslatedField()
models.signals.pre_save.connect(translations.fields.save_signal,
sender=MyModel,
dispatch_uid='mymodel_translations')
How it works behind the scenes¶
As mentioned above, a TranslatedField
is actually a ForeignKey
to the
translations
table. However, to support multiple languages, we use a
special feature of MySQL allowing you to have a ForeignKey
pointing to
multiple rows.
When querying¶
Our base manager has a _with_translations()
method that is automatically
called when you instanciate a queryset. It does 2 things:
- Stick an extra lang=lang in the query to prevent query caching from returning objects in the wrong language
- Call
translations.transformers.get_trans()
which does the black magic.
get_trans()
is called, and calls in turn translations.transformer.build_query()
and builds a custom SQL query. This query is the heart of the magic. For each
field, it setups a join on the translations table, trying to find a translation
in the current language (using translation.get_language()
) and then in the
language returned by get_fallback()
on the instance (for addons, that’s
default_locale
; if the get_fallback()
method doesn’t exist, it will
use settings.LANGUAGE_CODE
, which should be en-US
in zamboni).
Only those 2 languages are considered, and a double join + IF
/ ELSE
is
done every time, for each field.
This query is then ran on the slave (get_trans()
gets a cursor using
connections[multidb.get_slave()]
) to fetch the translations, and some
Translation objects are instantiated from the results and set on the
instance(s) of the original query.
To complete the mechanism, TranslationDescriptor.__get__
returns the
Translation
, and Translations.__unicode__
returns the translated string
as you’d expect, making the whole thing transparent.
When setting¶
Everytime you set a translated field to a string value, TranslationDescriptor
__set__
method is called. It determines which method to call (because you
can also assign a dict with multiple translations in multiple languages at the
same time). In this case, it calls translation_from_string()
method, still
on the “hidden” TranslationDescriptor
instance. The current language is
passed at this point, using translation_utils.get_language()
.
From there, translation_from_string()
figures out whether it’s a new
translation of a field we had no translation for, a new translation of a
field we already had but in a new language, or an update to an existing
translation.
It instantiates a new Translation
object with the correct values if
necessary, or just updates the correct one. It then places that object in a
queue of Translation instances to be saved later.
When you eventually call obj.save()
, the pre_save
signal is sent. If
you followed the example above, that means translations.fields.save_signal
is then called, and it unqueues all Translation objects and saves them. It’s
important to do this on pre_save
to prevent foreign key constraint errors.
When deleting¶
Deleting all translations for a field is done using delete_translation()
.
It sets the field to NULL
and then deletes all the attached translations.
Deleting a specific translation (like a translation in spanish, but keeping the english one intact) is implemented but not recommended at the moment. The reason why is twofold:
- MySQL doesn’t let you delete something that still has a FK pointing to it,
even if there are other rows that match the FK. When you call
delete()
on a translation, if it was the last translation for that field, we set the FK toNULL
and delete the translation normally. However, if there were any other translations, instead we temporarily disable the constraints to let you delete just the one you want. - Remember how fetching works? If you deleted a translation that is part of
the fallback, then when you fetch that object, depending on your locale
you’ll get an empty string for that field, even if there are
Translation
objects in other languages available!
For additional discussion on this topic, see https://bugzilla.mozilla.org/show_bug.cgi?id=902435
Additional tricks¶
In addition to the above, apps/translations/__init__.py
monkeypatches
Django to bypass errors thrown because we have a ForeignKey
pointing to
multiple rows.
Also, you might be interested in translations.query.order_by_translation
.
Like the name suggests, it allows you to order a QuerySet
by a translated
field, honoring the current and fallback locales like it’s done when querying.
How to build these docs¶
To simply build the docs:
make docs
If you’re working on the docs, use make loop
to keep your built pages
up-to-date:
cd docs && make loop