2016-05-15 18:28:38 +02:00
|
|
|
How to write a new application feature
|
|
|
|
======================================
|
|
|
|
|
|
|
|
The changes needed to add a new feature will vary, of course, but this
|
|
|
|
document provides a general outline of what you may need to do, as well
|
|
|
|
as an example of the specific steps needed to add a new feature: adding
|
|
|
|
a new option to the application that is dynamically synced through the
|
|
|
|
data system in real-time to all browsers the user may have open.
|
|
|
|
|
|
|
|
General Process
|
|
|
|
---------------
|
|
|
|
|
|
|
|
### Adding a field to the database
|
|
|
|
|
|
|
|
**Update the model:** The server accesses the underlying database in
|
|
|
|
`zerver/ models.py`. Add a new field in the appropriate class.
|
|
|
|
|
|
|
|
**Create and run the migration:** To create and apply a migration, run:
|
|
|
|
:
|
|
|
|
|
|
|
|
./manage.py makemigrations ./manage.py migrate
|
|
|
|
|
|
|
|
**Test your changes:** Once you've run the migration, restart memcached
|
|
|
|
on your development server (`/etc/init.d/memcached restart`) and then
|
|
|
|
restart `run-dev.py` to avoid interacting with cached objects.
|
|
|
|
|
|
|
|
### Backend changes
|
|
|
|
|
|
|
|
**Database interaction:** Add any necessary code for updating and
|
|
|
|
interacting with the database in `zerver/lib/actions.py`. It should
|
|
|
|
update the database and send an event announcing the change.
|
|
|
|
|
|
|
|
**Application state:** Modify the `fetch_initial_state_data` and
|
|
|
|
`apply_events` functions in `zerver/lib/actions.py` to update the state
|
|
|
|
based on the event you just created.
|
|
|
|
|
|
|
|
**Backend implementation:** Make any other modifications to the backend
|
|
|
|
required for your change.
|
|
|
|
|
|
|
|
**Testing:** At the very least, add a test of your event data flowing
|
|
|
|
through the system in `test_events.py`.
|
|
|
|
|
|
|
|
### Frontend changes
|
|
|
|
|
|
|
|
**JavaScript:** Zulip's JavaScript is located in the directory
|
|
|
|
`static/js/`. The exact files you may need to change depend on your
|
|
|
|
feature. If you've added a new event that is sent to clients, be sure to
|
|
|
|
add a handler for it to `static/js/server_events.js`.
|
|
|
|
|
|
|
|
**CSS:** The primary CSS file is `static/styles/zulip.css`. If your new
|
|
|
|
feature requires UI changes, you may need to add additional CSS to this
|
|
|
|
file.
|
|
|
|
|
|
|
|
**Templates:** The initial page structure is rendered via Jinja2
|
|
|
|
templates located in `templates/zerver`. For JavaScript, Zulip uses
|
|
|
|
Handlebars templates located in `static/templates`. Templates are
|
|
|
|
precompiled as part of the build/deploy process.
|
|
|
|
|
|
|
|
**Testing:** There are two types of frontend tests: node-based unit
|
|
|
|
tests and blackbox end-to-end tests. The blackbox tests are run in a
|
|
|
|
headless browser using Casper.js and are located in
|
|
|
|
`frontend_tests/casper_tests/`. The unit tests use Node's `assert`
|
|
|
|
module are located in `frontend_tests/node_tests/`. For more information
|
|
|
|
on writing and running tests see the testing documentation \<testing\>.
|
|
|
|
|
|
|
|
Example Feature
|
|
|
|
---------------
|
|
|
|
|
|
|
|
This example describes the process of adding a new setting to Zulip: a
|
|
|
|
flag that restricts inviting new users to admins only (the default
|
|
|
|
behavior is that any user can invite other users). It is based on an
|
|
|
|
actual Zulip feature, and you can review [the original commit in the
|
|
|
|
Zulip git
|
|
|
|
repo](https://github.com/zulip/zulip/commit/5b7f3466baee565b8e5099bcbd3e1ccdbdb0a408).
|
|
|
|
(Note that Zulip has since been upgraded from Django 1.6 to 1.8, so the
|
|
|
|
migration format has changed.)
|
|
|
|
|
|
|
|
First, update the database and model to store the new setting. Add a new
|
|
|
|
boolean field, `realm_invite_by_admins_only`, to the Realm model in
|
|
|
|
`zerver/models.py`.
|
|
|
|
|
|
|
|
Then create a Django migration that adds a new field,
|
|
|
|
`invite_by_admins_only`, to the `zerver_realm` table.
|
|
|
|
|
|
|
|
In `zerver/lib/actions.py`, create a new function named
|
|
|
|
`do_set_realm_invite_by_admins_only`. This function will update the
|
|
|
|
database and trigger an event to notify clients when this setting
|
2016-06-02 19:16:56 +02:00
|
|
|
changes. In this case there was an existing `realm|update` event type
|
2016-05-15 18:28:38 +02:00
|
|
|
which was used for setting similar flags on the Realm model, so it was
|
|
|
|
possible to add a new property to that event rather than creating a new
|
|
|
|
one. The property name matches the database field to make it easy to
|
|
|
|
understand what it indicates.
|
|
|
|
|
|
|
|
The second argument to `send_event` is the list of users whose browser
|
|
|
|
sessions should be notified. Depending on the setting, this can be a
|
|
|
|
single user (if the setting is a personal one, like time display
|
|
|
|
format), only members in a particular stream or all active users in a
|
|
|
|
realm. :
|
|
|
|
|
|
|
|
# zerver/lib/actions.py
|
|
|
|
|
|
|
|
def do_set_realm_invite_by_admins_only(realm, invite_by_admins_only):
|
|
|
|
realm.invite_by_admins_only = invite_by_admins_only
|
|
|
|
realm.save(update_fields=['invite_by_admins_only'])
|
|
|
|
event = dict(
|
|
|
|
type="realm",
|
|
|
|
op="update",
|
|
|
|
property='invite_by_admins_only',
|
|
|
|
value=invite_by_admins_only,
|
|
|
|
)
|
|
|
|
send_event(event, active_user_ids(realm))
|
|
|
|
return {}
|
|
|
|
|
|
|
|
You then need to add code that will handle the event and update the
|
|
|
|
application state. In `zerver/lib/actions.py` update the
|
|
|
|
`fetch_initial_state` and `apply_events` functions. :
|
|
|
|
|
|
|
|
def fetch_initial_state_data(user_profile, event_types, queue_id):
|
|
|
|
# ...
|
|
|
|
state['realm_invite_by_admins_only'] = user_profile.realm.invite_by_admins_only`
|
|
|
|
|
|
|
|
In this case you don't need to change `apply_events` because there is
|
|
|
|
already code that will correctly handle the realm update event type: :
|
|
|
|
|
|
|
|
def apply_events(state, events, user_profile):
|
|
|
|
for event in events:
|
|
|
|
# ...
|
|
|
|
elif event['type'] == 'realm':
|
|
|
|
field = 'realm_' + event['property']
|
|
|
|
state[field] = event['value']
|
|
|
|
|
|
|
|
You then need to add a view for clients to access that will call the
|
|
|
|
newly-added `actions.py` code to update the database. This example
|
|
|
|
feature adds a new parameter that should be sent to clients when the
|
|
|
|
application loads and be accessible via JavaScript, and there is already
|
|
|
|
a view that does this for related flags: `update_realm`. So in this
|
2016-06-02 19:16:56 +02:00
|
|
|
case, we can add out code to the existing view instead of creating a
|
2016-05-15 18:28:38 +02:00
|
|
|
new one. :
|
|
|
|
|
|
|
|
# zerver/views/__init__.py
|
|
|
|
|
|
|
|
def home(request):
|
|
|
|
# ...
|
|
|
|
page_params = dict(
|
|
|
|
# ...
|
|
|
|
realm_invite_by_admins_only = register_ret['realm_invite_by_admins_only'],
|
|
|
|
# ...
|
|
|
|
)
|
|
|
|
|
|
|
|
Since this feature also adds a checkbox to the admin page, and adds a
|
|
|
|
new property the Realm model that can be modified from there, you also
|
|
|
|
need to make changes to the `update_realm` function in the same file: :
|
|
|
|
|
|
|
|
# zerver/views/__init__.py
|
|
|
|
|
|
|
|
def update_realm(request, user_profile,
|
|
|
|
name=REQ(validator=check_string, default=None),
|
|
|
|
restricted_to_domain=REQ(validator=check_bool, default=None),
|
|
|
|
invite_by_admins_only=REQ(validator=check_bool,default=None)):
|
|
|
|
|
|
|
|
# ...
|
|
|
|
|
|
|
|
if invite_by_admins_only is not None and
|
|
|
|
realm.invite_by_admins_only != invite_by_admins_only:
|
|
|
|
do_set_realm_invite_by_admins_only(realm, invite_by_admins_only)
|
|
|
|
data['invite_by_admins_only'] = invite_by_admins_only
|
|
|
|
|
|
|
|
Then make the required front end changes: in this case a checkbox needs
|
|
|
|
to be added to the admin page (and its value added to the data sent back
|
|
|
|
to server when a realm is updated) and the change event needs to be
|
|
|
|
handled on the client.
|
|
|
|
|
|
|
|
To add the checkbox to the admin page, modify the relevant template,
|
|
|
|
`static/templates/admin_tab.handlebars` (omitted here since it is
|
|
|
|
relatively straightforward). Then add code to handle changes to the new
|
|
|
|
form control in `static/js/admin.js`. :
|
|
|
|
|
|
|
|
var url = "/json/realm";
|
|
|
|
var new_invite_by_admins_only =
|
|
|
|
$("#id_realm_invite_by_admins_only").prop("checked");
|
|
|
|
data[invite_by_admins_only] = JSON.stringify(new_invite_by_admins_only);
|
|
|
|
|
|
|
|
channel.patch({
|
|
|
|
url: url,
|
|
|
|
data: data,
|
|
|
|
success: function (data) {
|
|
|
|
# ...
|
|
|
|
if (data.invite_by_admins_only) {
|
|
|
|
ui.report_success("New users must be invited by an admin!", invite_by_admins_only_status);
|
|
|
|
} else {
|
|
|
|
ui.report_success("Any user may now invite new users!", invite_by_admins_only_status);
|
|
|
|
}
|
|
|
|
# ...
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
Finally, update `server_events.js` to handle related events coming from
|
|
|
|
the server. :
|
|
|
|
|
|
|
|
# static/js/server_events.js
|
|
|
|
|
|
|
|
function get_events_success(events) {
|
|
|
|
# ...
|
|
|
|
var dispatch_event = function dispatch_event(event) {
|
|
|
|
switch (event.type) {
|
|
|
|
# ...
|
|
|
|
case 'realm':
|
|
|
|
if (event.op === 'update' && event.property === 'invite_by_admins_only') {
|
|
|
|
page_params.realm_invite_by_admins_only = event.value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Any code needed to update the UI should be placed in `dispatch_event`
|
|
|
|
callback (rather than the `channel.patch`) function. This ensures the
|
|
|
|
appropriate code will run even if the changes are made in another
|
|
|
|
browser window. In this example most of the changes are on the backend,
|
|
|
|
so no UI updates are required.
|