mirror of
https://github.com/zulip/zulip.git
synced 2025-11-14 19:06:09 +00:00
Expand new feature tutorial.
This commit is contained in:
@@ -2,84 +2,215 @@
|
|||||||
New Feature Tutorial
|
New Feature Tutorial
|
||||||
====================
|
====================
|
||||||
|
|
||||||
.. attention::
|
The changes needed to add a new feature will vary, of course, but this document
|
||||||
This tutorial is an unfinished work -- contributions welcome!
|
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.
|
||||||
|
|
||||||
The changes needed to add a new feature will vary, of course. We give an
|
General Process
|
||||||
example here that illustrates some of the common steps needed. We describe
|
|
||||||
the process of adding a new setting for admins that restricts inviting new
|
|
||||||
users to admins only.
|
|
||||||
|
|
||||||
Backend Changes
|
|
||||||
===============
|
===============
|
||||||
|
|
||||||
Adding a field to the database
|
Adding a field to the database
|
||||||
------------------------------
|
------------------------------
|
||||||
|
|
||||||
The server accesses the underlying database in `zerver/models.py`. Add
|
**Update the model:** The server accesses the underlying database in `zerver/
|
||||||
a new field in the appropriate class, `realm_invite_by_admins_only`
|
models.py`. Add a new field in the appropriate class.
|
||||||
in the `Realm` class in this case.
|
|
||||||
|
|
||||||
Once you do so, you need to create the migration and run it; the
|
**Create and run the migration:** To create and apply a migration, run: ::
|
||||||
process is documented at:
|
|
||||||
https://docs.djangoproject.com/en/1.8/topics/migrations/
|
|
||||||
|
|
||||||
Once you've run the migration, to test your changes, you'll want to
|
./manage.py makemigrations
|
||||||
restart memcached on your development server (``/etc/init.d/memcached restart``) and
|
./manage.py migrate
|
||||||
then restart ``run-dev.py`` to avoid interacting with cached objects.
|
|
||||||
|
**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
|
Backend changes
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
You should add code in `zerver/lib/actions.py` to interact with the database,
|
**Database interaction:** Add any necessary code for updating and interacting
|
||||||
that actually updates the relevant field. In this case, `do_set_realm_invite_by_admins_only`
|
with the database in ``zerver/lib/actions.py``. It should update the database and
|
||||||
is a function that actually updates the field in the database, and sends
|
send an event announcing the change.
|
||||||
an event announcing that this change has been made.
|
|
||||||
|
|
||||||
You then need update the `fetch_initial_state_data` and `apply_events` functions
|
**Application state:** Modify the ``fetch_initial_state_data`` and ``apply_events``
|
||||||
in `zerver/lib/actions.py` to update the state based on the event you just created.
|
functions in ``zerver/lib/actions.py`` to update the state based on the event you
|
||||||
In this case, we add a line
|
just created.
|
||||||
|
|
||||||
::
|
**Backend implementation:** Make any other modifications to the backend required for
|
||||||
|
your change.
|
||||||
|
|
||||||
state['realm_invite_by_admins_only'] = user_profile.realm.invite_by_admins_only`
|
**Testing:** At the very least, add a test of your event data flowing through
|
||||||
|
the system in ``test_events.py``.
|
||||||
to the `fetch_initial_state_data` function. The `apply_events` function
|
|
||||||
doesn't need to be updated since
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
elif event['type'] == 'realm':
|
|
||||||
field = 'realm_' + event['property']
|
|
||||||
state[field] = event['value']
|
|
||||||
|
|
||||||
already took care of our event.
|
|
||||||
|
|
||||||
Then update `zerver/views/__init__.py` to actually call your function.
|
|
||||||
In the dictionary which sets the javascript `page_params` dictionary,
|
|
||||||
add a value for your feature.
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
realm_invite_by_admins_only = register_ret['realm_invite_by_admins_only']
|
|
||||||
|
|
||||||
Perhaps your new option controls some other backend rendering: in our case
|
|
||||||
we test for this option in the `home` method for adding a variable to the response.
|
|
||||||
The functions in this file control the generation of various pages served
|
|
||||||
(along with the Django templates).
|
|
||||||
Our new feature also shows up in the administration tab (as a checkbox),
|
|
||||||
so we need to update the `update_realm` function.
|
|
||||||
|
|
||||||
|
|
||||||
Finally, add tests for your backend changes; at the very least you
|
|
||||||
should add a test of your event data flowing through the system in
|
|
||||||
``test_events.py``.
|
|
||||||
|
|
||||||
|
|
||||||
Frontend changes
|
Frontend changes
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
You need to change various things on the front end. In this case, the relevant files
|
**JavaScript:** Zulip's JavaScript is located in the directory ``static/js/``.
|
||||||
are `static/js/server_events.js`, `static/js/admin.js`, `static/styles/zulip.css
|
The exact files you may need to change depend on your feature. If you've added a
|
||||||
and `static/templates/admin_tab.handlebars`.
|
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 Django templates
|
||||||
|
located in ``template/server``. 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 ``zerver/tests/frontend/tests/``. The unit
|
||||||
|
tests use Node's ``assert`` module are located in ``zerver/tests/frontend/node/``.
|
||||||
|
For more information on writing and running tests see the :doc:`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 changes. In this case
|
||||||
|
there was an exisiting ``realm|update`` event type 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 case, we can add out code to the
|
||||||
|
exisiting view instead of creating a 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.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user