mirror of
				https://github.com/zulip/zulip.git
				synced 2025-10-31 03:53:50 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			217 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			ReStructuredText
		
	
	
	
	
	
			
		
		
	
	
			217 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			ReStructuredText
		
	
	
	
	
	
| ====================
 | |
| New Feature Tutorial
 | |
| ====================
 | |
| 
 | |
| 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 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.
 | |
| 
 |