snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
This commit is contained in:
13
docs/tutorials/index.md
Normal file
13
docs/tutorials/index.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Developer tutorials
|
||||
|
||||
```{toctree}
|
||||
---
|
||||
maxdepth: 3
|
||||
---
|
||||
|
||||
new-feature-tutorial
|
||||
writing-views
|
||||
life-of-a-request
|
||||
reading-list
|
||||
shell-tips
|
||||
```
|
||||
217
docs/tutorials/life-of-a-request.md
Normal file
217
docs/tutorials/life-of-a-request.md
Normal file
@@ -0,0 +1,217 @@
|
||||
# Life of a request
|
||||
|
||||
It can sometimes be confusing to figure out how to write a new feature,
|
||||
or debug an existing one. Let us try to follow a request through the
|
||||
Zulip codebase, and dive deep into how each part works.
|
||||
|
||||
We will use as our example the creation of users through the API, but we
|
||||
will also highlight how alternative requests are handled.
|
||||
|
||||
## A request is sent to the server, and handled by [nginx](https://nginx.org/en/docs/)
|
||||
|
||||
When Zulip is deployed in production, all requests go through nginx.
|
||||
For the most part we don't need to know how this works, except for when
|
||||
it isn't working. nginx does the first level of routing--deciding which
|
||||
application will serve the request (or deciding to serve the request
|
||||
itself for static content).
|
||||
|
||||
In development, `tools/run-dev` fills the role of nginx. Static files
|
||||
are in your Git checkout under `static`, and are served unminified.
|
||||
|
||||
## Static files are [served directly][served-directly] by nginx
|
||||
|
||||
[served-directly]: https://github.com/zulip/zulip/blob/main/puppet/zulip/files/nginx/zulip-include-frontend/app
|
||||
|
||||
Static files include JavaScript, CSS, and static assets (like emoji, avatars).
|
||||
|
||||
File not found errors (404) are served using a Django URL, so that we
|
||||
can use configuration variables (like whether the user is logged in)
|
||||
in the 404 error page.
|
||||
|
||||
```nginx
|
||||
location /static/ {
|
||||
alias /home/zulip/prod-static/;
|
||||
# Set a nonexistent path, so we just serve the nice Django 404 page.
|
||||
error_page 404 /django_static_404.html;
|
||||
}
|
||||
```
|
||||
|
||||
## nginx routes other requests [between Django and Tornado][tornado-django]
|
||||
|
||||
[tornado-django]: ../overview/architecture-overview.md#django-and-tornado
|
||||
|
||||
All our connected clients hold open long-polling connections so that
|
||||
they can receive events (messages, presence notifications, and so on) in
|
||||
real-time. Events are served by Zulip's `tornado` application.
|
||||
|
||||
Nearly every other kind of request is served by the `zerver` Django
|
||||
application.
|
||||
|
||||
[Here is the relevant nginx routing configuration.][nginx-config-link]
|
||||
|
||||
[nginx-config-link]: https://github.com/zulip/zulip/blob/main/puppet/zulip/files/nginx/zulip-include-frontend/app
|
||||
|
||||
## Django routes the request to a view in urls.py files
|
||||
|
||||
There are various
|
||||
[urls.py](https://docs.djangoproject.com/en/5.0/topics/http/urls/)
|
||||
files throughout the server codebase, which are covered in more detail
|
||||
in
|
||||
[the directory structure doc](../subsystems/directory-structure.md).
|
||||
|
||||
The main Zulip Django app is `zerver`. The routes are found in
|
||||
`zproject/urls.py`.
|
||||
|
||||
There are HTML-serving, REST API, and webhook url patterns. We
|
||||
will look at how each of these types of requests are handled, and focus
|
||||
on how the REST API handles our user creation example.
|
||||
|
||||
## Views serving HTML are internationalized by server path
|
||||
|
||||
If we look in
|
||||
[zproject/urls.py](https://github.com/zulip/zulip/blob/main/zproject/urls.py),
|
||||
we can see something called `i18n_urls`. These urls show up in the
|
||||
address bar of the browser, and serve HTML.
|
||||
|
||||
For example, the `/features` page (preview
|
||||
[here](https://zulip.com/features/)) gets translated in Chinese at
|
||||
`zh-hans/features/` (preview
|
||||
[here](https://zulip.com/zh-hans/features/)).
|
||||
|
||||
Note the `zh-hans` prefix--that url pattern gets added by `i18n_patterns`.
|
||||
|
||||
## API endpoints use [REST](https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm)
|
||||
|
||||
Our example is a REST API endpoint. It's a PUT to `/users`.
|
||||
|
||||
With the exception of incoming webhooks (which we do not usually control the
|
||||
format of) and logged-out endpoints, Zulip uses REST
|
||||
for its API. This means that we use:
|
||||
|
||||
- POST for creating something new where we don't have a unique
|
||||
ID. Also used as a catch-all if no other verb is appropriate.
|
||||
- PUT for creating something for which we have a unique ID.
|
||||
- DELETE for deleting something
|
||||
- PATCH for updating or editing attributes of something.
|
||||
- GET to get something (read-only)
|
||||
- HEAD to check the existence of something to GET, without getting it;
|
||||
useful to check a link without downloading a potentially large link
|
||||
- OPTIONS (handled automatically, see more below)
|
||||
|
||||
Of these, PUT, DELETE, HEAD, OPTIONS, and GET are _idempotent_, which
|
||||
means that we can send the request multiple times and get the same
|
||||
state on the server. You might get a different response after the first
|
||||
request, as we like to give our clients an error so they know that no
|
||||
new change was made by the extra requests.
|
||||
|
||||
POST is not idempotent--if I send a message multiple times, Zulip will
|
||||
show my message multiple times. PATCH is special--it can be
|
||||
idempotent, and we like to write API endpoints in an idempotent fashion,
|
||||
as much as possible.
|
||||
|
||||
This [cookbook](http://restcookbook.com/) and
|
||||
[tutorial](https://www.restapitutorial.com/) can be helpful if you are
|
||||
new to REST web applications.
|
||||
|
||||
### PUT is only for creating new things
|
||||
|
||||
If you're used to using PUT to update or modify resources, you might
|
||||
find our convention a little strange.
|
||||
|
||||
We use PUT to create resources with unique identifiers, POST to create
|
||||
resources without unique identifiers (like sending a message with the
|
||||
same content multiple times), and PATCH to modify resources.
|
||||
|
||||
In our example, `create_user_backend` uses PUT, because there's a unique
|
||||
identifier, the user's email.
|
||||
|
||||
### OPTIONS
|
||||
|
||||
The OPTIONS method will yield the allowed methods.
|
||||
|
||||
This request:
|
||||
`OPTIONS https://chat.zulip.org/api/v1/users`
|
||||
yields a response with this HTTP header:
|
||||
`Allow: GET, HEAD, POST`
|
||||
|
||||
We can see this reflected in [zproject/urls.py](https://github.com/zulip/zulip/blob/main/zproject/urls.py):
|
||||
|
||||
```python
|
||||
rest_path('users',
|
||||
GET=get_members_backend,
|
||||
POST=create_user_backend),
|
||||
```
|
||||
|
||||
In this way, the API is partially self-documenting.
|
||||
|
||||
### Incoming webhook integrations may not be RESTful
|
||||
|
||||
Zulip endpoints that are called by other services for integrations have
|
||||
to conform to the service's request format. They are likely to use
|
||||
only POST.
|
||||
|
||||
## Django calls rest_dispatch for REST endpoints, and authenticates
|
||||
|
||||
For requests that correspond to a REST url pattern, Zulip configures
|
||||
its url patterns (see
|
||||
[zerver/lib/rest.py](https://github.com/zulip/zulip/blob/main/zerver/lib/rest.py))
|
||||
so that the action called is `rest_dispatch`. This method will
|
||||
authenticate the user, either through a session token from a cookie,
|
||||
or from an `email:api-key` string given via HTTP basic auth for API
|
||||
clients.
|
||||
|
||||
It will then look up what HTTP verb was used (GET, POST, etc) to make
|
||||
the request, and then figure out which view to show from that.
|
||||
|
||||
In our example,
|
||||
|
||||
```python
|
||||
GET=get_members_backend,
|
||||
PUT=create_user_backend
|
||||
```
|
||||
|
||||
are supplied as arguments to `rest_path`, along with the
|
||||
[HTTPRequest](https://docs.djangoproject.com/en/5.0/ref/request-response/).
|
||||
The request has the HTTP verb `PUT`, which `rest_dispatch` can use to
|
||||
find the correct view to show:
|
||||
`zerver.views.users.create_user_backend`.
|
||||
|
||||
## The view will authorize the user, extract request variables, and validate them
|
||||
|
||||
This is covered in good detail in the [writing views doc](writing-views.md).
|
||||
|
||||
## Results are given as JSON
|
||||
|
||||
Our API works on JSON requests and responses. Every API endpoint should
|
||||
`raise JsonableError` in the case of an error, which gives a JSON string:
|
||||
|
||||
```json
|
||||
{"result": "error", "msg": "<some error message>", "code": "BAD_REQUEST"}
|
||||
```
|
||||
|
||||
in a [Django HttpResponse
|
||||
object](https://docs.djangoproject.com/en/5.0/ref/request-response/)
|
||||
with a `Content-Type` of 'application/json'.
|
||||
|
||||
To pass back data from the server to the calling client, in the event of
|
||||
a successfully handled request, we use `json_success(request, data)`.
|
||||
|
||||
The `request` argument is a [Django HttpRequest
|
||||
object](https://docs.djangoproject.com/en/5.0/ref/request-response/).
|
||||
The `data` argument is a Python object which can be converted to a JSON
|
||||
string and has a default value of an empty Python dictionary.
|
||||
|
||||
Zulip stores additional metadata it has associated with that HTTP
|
||||
request in a `RequestNotes` object, which is primarily accessed in
|
||||
common code used in all requests (middleware, logging, rate limiting,
|
||||
etc.).
|
||||
|
||||
This will result in a JSON string:
|
||||
|
||||
```json
|
||||
{"result": "success", "msg": "", "data": {"var_name1": "var_value1", "var_name2": "var_value2"}}
|
||||
```
|
||||
|
||||
with an HTTP 200 status and a `Content-Type` of 'application/json'.
|
||||
|
||||
That's it!
|
||||
760
docs/tutorials/new-feature-tutorial.md
Normal file
760
docs/tutorials/new-feature-tutorial.md
Normal file
@@ -0,0 +1,760 @@
|
||||
# Writing 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.
|
||||
|
||||
As you read this, you may find you need to learn about Zulip's
|
||||
real-time push system; the
|
||||
[real-time push and events](../subsystems/events-system.md)
|
||||
documentation has a detailed explanation of how everything works. You
|
||||
may also find it beneficial to read Zulip's
|
||||
[architectural overview](../overview/architecture-overview.md).
|
||||
Zulip is a web application built using the
|
||||
[Django framework](https://www.djangoproject.com/), and some of the
|
||||
processes listed in this tutorial, such as database migrations and
|
||||
tests, use Django's tooling.
|
||||
|
||||
Zulip's [directory structure](../subsystems/directory-structure.md)
|
||||
will also be helpful to review when creating a new feature. Many
|
||||
aspects of the structure will be familiar to Django developers. Visit
|
||||
[Django's documentation](https://docs.djangoproject.com/en/5.0/#index-first-steps)
|
||||
for more information about how Django projects are typically
|
||||
organized. And finally, the
|
||||
[message sending](../subsystems/sending-messages.md) documentation on
|
||||
the additional complexity involved in sending messages.
|
||||
|
||||
## General process
|
||||
|
||||
### Files impacted
|
||||
|
||||
This tutorial will walk through adding a new feature to a Realm (an
|
||||
organization in Zulip). The following files are involved in the process:
|
||||
|
||||
**Backend**
|
||||
|
||||
- `zerver/models/realms.py`: Defines the database model.
|
||||
- `zerver/views/realm.py`: Contains the view function that implements the
|
||||
API endpoint for editing Realm objects.
|
||||
- `zerver/actions/realm_settings.py`: Contains code for updating and
|
||||
interacting with the database.
|
||||
- `zerver/lib/events.py`: Ensures that the state Zulip sends to clients
|
||||
is always consistent and correct.
|
||||
|
||||
**Frontend**
|
||||
|
||||
- `web/src/state_data.ts`: Defines the expected state of the data that
|
||||
is received from the Zulip server.
|
||||
- `web/templates/settings/organization_settings_admin.hbs`: Defines
|
||||
the structure of the organization settings admin modal.
|
||||
- `web/src/admin.ts`: Handles building the organization settings admin
|
||||
modal.
|
||||
- `web/src/settings_org.ts`: Handles form submission for changes to an
|
||||
organization's settings.
|
||||
- `web/src/server_events_dispatch.js`: Handles events coming from the
|
||||
server.
|
||||
|
||||
**Backend testing**
|
||||
|
||||
- `zerver/tests/test_realm.py`: Contains end-to-end API tests for
|
||||
updating realm settings.
|
||||
- `zerver/tests/test_events.py`: Tests for possible race bugs in the
|
||||
implementation of events.
|
||||
|
||||
**Frontend testing**
|
||||
|
||||
- `web/e2e-tests/admin.test.ts`: Contains end-to-end tests for the
|
||||
organization admin settings pages.
|
||||
- `web/tests/dispatch.test.cjs`: Unit tests for handling of events.
|
||||
|
||||
**Documentation**
|
||||
|
||||
- `zerver/openapi/zulip.yaml`: Contains OpenAPI definitions for the
|
||||
Zulip REST API.
|
||||
- `api_docs/changelog.md`: Documentation listing all changes to the
|
||||
Zulip Server API.
|
||||
- `starlight_help/...`: The user-facing documentation (help center)
|
||||
for the application.
|
||||
|
||||
### Adding a field to the database
|
||||
|
||||
**Update the model:** The server accesses the underlying database in
|
||||
`zerver/models/realms.py`. Add a new field in the appropriate class.
|
||||
|
||||
**Create and run the migration:** To create and apply a migration, run the
|
||||
following commands:
|
||||
|
||||
```bash
|
||||
./manage.py makemigrations
|
||||
./manage.py migrate
|
||||
```
|
||||
|
||||
It's highly recommended to read our
|
||||
[database migration documentation](../subsystems/schema-migrations.md)
|
||||
to learn more about creating and applying database migrations.
|
||||
|
||||
**Test your changes:** Once you've run the migration, [restart the
|
||||
development
|
||||
server](../development/remote.md#running-the-development-server).
|
||||
|
||||
### Backend changes
|
||||
|
||||
We have a framework that automatically handles many of the steps for the
|
||||
most common types of UserProfile and Realm settings. We refer to this as the
|
||||
`property_types` framework. However, it is valuable to understand
|
||||
the flow of events even if the `property_types` framework means you don't
|
||||
have to write much code for a new setting.
|
||||
|
||||
**Database interaction:** Add any necessary code for updating and
|
||||
interacting with the database in `zerver/actions/realm_settings.py`.
|
||||
It should update the database and send an event announcing the change.
|
||||
|
||||
**Application state:** Modify the `fetch_initial_state_data` and
|
||||
`apply_event` functions in `zerver/lib/events.py` to update the state
|
||||
based on the field you just created.
|
||||
|
||||
**Backend implementation:** Make any other modifications to the backend
|
||||
required for your feature to do what it's supposed to do (this will
|
||||
be unique to the feature you're implementing).
|
||||
|
||||
**New views:** Add any new application views to `zproject/urls.py`, or
|
||||
update the appropriate existing view in `zerver/views/...`. This
|
||||
includes both views that serve HTML (new pages on Zulip) as well as new
|
||||
API endpoints that serve JSON-formatted data.
|
||||
|
||||
**Testing:** At the very least, add a test of your event data flowing
|
||||
through the system in `test_events.py` and an API test (e.g., for a
|
||||
Realm setting, in `test_realm.py`).
|
||||
|
||||
### Frontend changes
|
||||
|
||||
**JavaScript/TypeScript:** Zulip's JavaScript and TypeScript sources are
|
||||
located in the directory `web/src/`. 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 in `web/src/server_events_dispatch.js`.
|
||||
|
||||
**CSS:** The primary CSS file is `web/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/app`. For JavaScript, Zulip uses
|
||||
Handlebars templates located in `web/templates`. Templates are
|
||||
precompiled as part of the build/deploy process.
|
||||
|
||||
Zulip is fully internationalized, so when writing both HTML templates
|
||||
or JavaScript/TypeScript/Python code that generates user-facing strings,
|
||||
be sure to [tag those strings for translation](../translating/translating.md).
|
||||
|
||||
**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 Chromium browser using Puppeteer and are located in
|
||||
`web/e2e-tests/`. The unit tests use Node's `assert` module are located
|
||||
in `web/tests/`. For more information on writing and running tests, see
|
||||
the [testing documentation](../testing/testing.md).
|
||||
|
||||
### Documentation changes
|
||||
|
||||
After implementing the new feature, you should document it and update
|
||||
any existing documentation that might be relevant to the new feature.
|
||||
For detailed information on the kinds of documentation Zulip has, see
|
||||
[Documentation](../documentation/overview.md).
|
||||
|
||||
**Help center documentation:** You will likely need to at least update,
|
||||
extend and link to articles in the `starlight_help/` directory that are
|
||||
related to your new feature. [Writing help center articles](../documentation/helpcenter.md)
|
||||
provides more detailed information about writing and editing feature
|
||||
`starlight_help/` directory articles.
|
||||
|
||||
**API documentation:** A new feature will probably impact the REST API
|
||||
documentation as well, which will mean updating `zerver/openapi/zulip.yaml`
|
||||
and modifying `api_docs/changelog.md` for a new feature
|
||||
level. [Documenting REST API endpoints](../documentation/api.md)
|
||||
explains Zulip's API documentation system and provides a step by step
|
||||
guide to adding or updating documentation for an API endpoint.
|
||||
|
||||
## Example feature
|
||||
|
||||
This example describes the process of adding a new realm setting to Zulip.
|
||||
For the purposes of this tutorial, this new setting will be called
|
||||
`my_fantastic_feature`.
|
||||
|
||||
:::{tip}
|
||||
|
||||
A useful tool that we'll highlight throughout this tutorial is
|
||||
`git grep`. If you're unfamiliar with using `git grep`, then check out
|
||||
[this blog post](https://laurynmm.github.io/2021/12/22/git-grep.html)
|
||||
for an introduction to it, as well as the [official `git grep`
|
||||
documentation](https://git-scm.com/docs/git-grep.)
|
||||
|
||||
:::
|
||||
|
||||
### Update the model
|
||||
|
||||
First, update the database and model to store the new setting. Add a new
|
||||
boolean field, `my_fantastic_feature`, to the Realm model in
|
||||
`zerver/models/realms.py`.
|
||||
|
||||
```diff
|
||||
# zerver/models/realms.py
|
||||
|
||||
class Realm(models.Model):
|
||||
# ...
|
||||
require_unique_names = models.BooleanField(default=False)
|
||||
name_changes_disabled = models.BooleanField(default=False)
|
||||
email_changes_disabled = models.BooleanField(default=False)
|
||||
avatar_changes_disabled = models.BooleanField(default=False)
|
||||
|
||||
+ # My fantastic feature for the new feature tutorial.
|
||||
+ my_fantastic_feature = models.BooleanField(default=False)
|
||||
```
|
||||
|
||||
The Realm model also contains an attribute, `property_types`, which
|
||||
other backend functions use to handle most realm settings without any
|
||||
custom code for the setting (more on this process below). The attribute
|
||||
is a dictionary, where the key is the name of the realm field and the
|
||||
value is the field's type. Add the new field to the `property_types`
|
||||
dictionary.
|
||||
|
||||
```diff
|
||||
# zerver/models/realms.py
|
||||
|
||||
class Realm(models.Model)
|
||||
# ...
|
||||
# Define the types of the various automatically managed properties
|
||||
property_types: dict[str, type | UnionType] = dict(
|
||||
allow_message_editing=bool,
|
||||
avatar_changes_disabled=bool,
|
||||
# ...
|
||||
+ my_fantastic_feature=bool,
|
||||
# ...
|
||||
```
|
||||
|
||||
**The majority of realm settings can be included in `property_types`.**
|
||||
However, there are some properties that need custom logic and thus cannot
|
||||
use this framework. Any properties that define a relationship to another
|
||||
model (e.g., a `ForeignKey` field) do not use the `property_types`
|
||||
framework, for example `moderation_request_channel` property and the
|
||||
`can_add_custom_emoji_group` property.
|
||||
|
||||
When creating a realm property that is not a boolean, character or
|
||||
integer field, or when adding a field that is dependent on other fields,
|
||||
do not add the field to the `property_types` dictionary. The steps below
|
||||
will point out where to write additional code for these cases.
|
||||
|
||||
### Create the migration
|
||||
|
||||
Create the migration file using the Django `makemigrations` command:
|
||||
`./manage.py makemigrations`. Make sure to commit the generated file to
|
||||
git: `git add zerver/migrations/NNNN_my_fantastic_feature.py` (NNNN is a
|
||||
number that is equal to the current number of migrations.)
|
||||
|
||||
If you run into problems, the
|
||||
[Django migration documentation](https://docs.djangoproject.com/en/5.0/topics/migrations/)
|
||||
is helpful.
|
||||
|
||||
### Test your migration changes
|
||||
|
||||
Apply the migration using Django's `migrate` command, `./manage.py migrate`:
|
||||
|
||||
```console
|
||||
shell $ ./manage.py migrate
|
||||
Operations to perform:
|
||||
Synchronize unmigrated apps: staticfiles, analytics, pipeline
|
||||
Apply all migrations: zilencer, confirmation, sessions, guardian, zerver, sites, auth, contenttypes
|
||||
Synchronizing apps without migrations:
|
||||
Creating tables...
|
||||
Running deferred SQL...
|
||||
Installing custom SQL...
|
||||
Running migrations:
|
||||
Rendering model states... DONE
|
||||
Applying zerver.NNNN_my_fantastic_feature... OK
|
||||
```
|
||||
|
||||
Once you've run the migration, [restart the development
|
||||
server](../development/remote.md#running-the-development-server).
|
||||
|
||||
### Handle database interactions
|
||||
|
||||
Next, we will implement the backend part of this feature. Like typical
|
||||
apps, we will need our backend to update the database and send some
|
||||
response to the client that made the request.
|
||||
|
||||
Beyond that, we need to orchestrate notifications about the setting
|
||||
change to _other_ clients (or other users, if you will). Clients find out
|
||||
about settings through two closely related code paths. When a client
|
||||
first contacts the server, the server sends the client its initial state.
|
||||
Subsequently, clients subscribe to "events," which can (among other
|
||||
things) indicate that settings have changed.
|
||||
|
||||
For the backend piece, we will need our action to make a call to
|
||||
`send_event_on_commit` to send the event to clients that are active
|
||||
(The event is only sent after the current database transaction
|
||||
commits, hence the name). We will also need to modify
|
||||
`fetch_initial_state_data` so that the new field is passed to
|
||||
clients. See [our event system docs](../subsystems/events-system.md)
|
||||
for all the gory details.
|
||||
|
||||
Getting back to implementation details...
|
||||
|
||||
If you are working on a feature that is in the realm `property_types`
|
||||
dictionary, you will not need to add code to
|
||||
`zerver/actions/realm_settings.py`, but we will describe what the
|
||||
process in that file does:
|
||||
|
||||
In `zerver/actions/realm_settings.py`, the function `do_set_realm_property`
|
||||
takes in the name of a realm property to update and the value it should
|
||||
have. This function updates the database and triggers an event to
|
||||
notify clients about the change. It uses the field's type, specified
|
||||
in the `Realm.property_types` dictionary, to validate the type of the
|
||||
value before updating the property; this is primarily an assertion to
|
||||
help catch coding mistakes, not to check for bad user input.
|
||||
|
||||
After updating the given realm field, `do_set_realm_property` creates
|
||||
an 'update' event with the name of the property and the new value. It
|
||||
then calls `send_event_on_commit`, passing the event and the list of
|
||||
users whose browser sessions should be notified as the second argument.
|
||||
The latter argument can be a single user (if the setting is a personal
|
||||
one, like time display format), members in a particular channel only or
|
||||
all active users in an realm.
|
||||
|
||||
:::{tip}
|
||||
|
||||
Use `git grep` to find and read the `do_set_realm_property`
|
||||
function. Are there `property_types` that are handled differently in
|
||||
that function? Which ones? Does the function indicate why?
|
||||
|
||||
:::
|
||||
|
||||
If the new realm property being added does not fit into the
|
||||
`property_types` framework (such as the `moderation_request_channel`
|
||||
field), you'll need to create a new function to explicitly update this
|
||||
field and send an event. For example:
|
||||
|
||||
```python
|
||||
# zerver/actions/realm_settings.py
|
||||
|
||||
def do_set_realm_stream(
|
||||
realm: Realm,
|
||||
field: Literal[
|
||||
"moderation_request_channel",
|
||||
"new_stream_announcements_stream",
|
||||
"signup_announcements_stream",
|
||||
"zulip_update_announcements_stream",
|
||||
],
|
||||
stream: Stream | None,
|
||||
stream_id: int,
|
||||
*,
|
||||
acting_user: UserProfile | None,
|
||||
) -> None:
|
||||
# We could calculate more of these variables from `field`, but
|
||||
# it's probably more readable to not do so.
|
||||
if field == "moderation_request_channel":
|
||||
old_value = realm.moderation_request_channel_id
|
||||
realm.moderation_request_channel = stream
|
||||
property = "moderation_request_channel_id"
|
||||
```
|
||||
|
||||
### Update application state
|
||||
|
||||
`zerver/lib/events.py` contains code to ensure that your new setting is
|
||||
included in the data sent down to clients: both when a new client is
|
||||
loaded and when changes happen. This file also automatically handles
|
||||
realm settings in the `property_types` dictionary, so you would not need
|
||||
to change this file if your setting fits that framework.
|
||||
|
||||
The `fetch_initial_state_data` function is responsible for sending data
|
||||
when a client is loaded (data added to the `state` here will be available
|
||||
both in `page_params` in the browser, as well as to API clients like the
|
||||
mobile app). The `apply_event` function in `zerver/lib/events.py` is
|
||||
important for making sure the `state` is always correct, even in the
|
||||
event of rare race conditions.
|
||||
|
||||
```python
|
||||
# zerver/lib/events.py
|
||||
|
||||
def fetch_initial_state_data(
|
||||
user_profile: UserProfile | None,
|
||||
# ...
|
||||
if want("realm"):
|
||||
# The realm bundle includes both realm properties and server
|
||||
# properties, since it's rare that one would want one and not
|
||||
# the other. We expect most clients to want it.
|
||||
# ...
|
||||
for property_name in Realm.property_types:
|
||||
state["realm_" + property_name] = getattr(realm, property_name)
|
||||
|
||||
for setting_name in Realm.REALM_PERMISSION_GROUP_SETTINGS:
|
||||
setting_group_id = getattr(realm, setting_name + "_id")
|
||||
state["realm_" + setting_name] = get_group_setting_value_for_register_api(
|
||||
setting_group_id, anonymous_group_membership_data_dict
|
||||
)
|
||||
# ...
|
||||
|
||||
def apply_event(
|
||||
user_profile: UserProfile,
|
||||
# ...
|
||||
) -> None:
|
||||
# ...
|
||||
elif event["type"] == "realm":
|
||||
if event["op"] == "update":
|
||||
field = "realm_" + event["property"]
|
||||
state[field] = event["value"]
|
||||
# ...
|
||||
```
|
||||
|
||||
If your new realm property fits the `property_types` framework, you don't
|
||||
need to change `fetch_initial_state_data` or `apply_event`. However, if
|
||||
you are adding a property that is handled separately, you will need to
|
||||
explicitly add the property to the `state` dictionary in the
|
||||
`fetch_initial_state_data` function.
|
||||
|
||||
For our example, one won't need to change `apply_event` since its default
|
||||
code for `realm` event types handles this case correctly, but for a
|
||||
totally new type of feature, a few lines in that function may be needed.
|
||||
|
||||
### Add a new view
|
||||
|
||||
You will need to add a view for clients to access that will call the
|
||||
`actions.py` code to update the database. This example feature
|
||||
adds a new parameter that will be sent to clients when the
|
||||
application loads and should be accessible via JavaScript. There is
|
||||
already a view that does this for related flags: `update_realm` in
|
||||
`zerver/views/realm.py`. So in this case, we can add our code to the
|
||||
existing view instead of creating a new one.
|
||||
|
||||
You'll need to add a parameter for the new field to the `update_realm`
|
||||
function in `zerver/views/realm.py` (and add the appropriate mypy type
|
||||
annotation).
|
||||
|
||||
```diff
|
||||
# zerver/views/realm.py
|
||||
|
||||
@require_realm_admin
|
||||
@typed_endpoint
|
||||
def update_realm(
|
||||
request: HttpRequest,
|
||||
user_profile: UserProfile,
|
||||
*,
|
||||
allow_message_editing: Json[bool] | None = None,
|
||||
authentication_methods: Json[dict[str, Any]] | None = None,
|
||||
# ...
|
||||
+ my_fantastic_feature: Json[bool] | None = None,
|
||||
# ...
|
||||
):
|
||||
# ...
|
||||
```
|
||||
|
||||
If this feature fits the `property_types` framework and does not
|
||||
require additional validation, this is the only change to make to
|
||||
`zerver/views/realm.py`.
|
||||
|
||||
Text fields or other realm properties that need additional validation
|
||||
can be handled at the beginning of `update_realm`.
|
||||
|
||||
```python
|
||||
# zerver/views/realm.py
|
||||
|
||||
# Additional validation/error checking beyond types go here, so
|
||||
# the entire request can succeed or fail atomically.
|
||||
if default_language is not None and default_language not in get_available_language_codes():
|
||||
raise JsonableError(_("Invalid language '{language}'").format(language=default_language))
|
||||
# ...
|
||||
```
|
||||
|
||||
The code in `update_realm` loops through the `property_types` dictionary
|
||||
and calls `do_set_realm_property` on any property to be updated from
|
||||
the request.
|
||||
|
||||
If the new feature is not in `property_types`, you will need to write code
|
||||
to call the function you wrote in `actions.py` that updates the database
|
||||
with the new value. E.g., for `moderation_request_channel`, we created
|
||||
`do_set_realm_moderation_request_channel`, which we will call here:
|
||||
|
||||
```python
|
||||
# zerver/views/realm.py
|
||||
|
||||
# import do_set_realm_authentication_methods from actions.py
|
||||
from zerver.actions.realm_settings import (
|
||||
# ...
|
||||
do_set_realm_moderation_request_channel,
|
||||
# ...
|
||||
)
|
||||
# ...
|
||||
# ...
|
||||
# Channel-valued settings are not yet fully supported by the
|
||||
# property_types framework, and thus have explicit blocks here.
|
||||
if moderation_request_channel_id is not None and (
|
||||
realm.moderation_request_channel is None
|
||||
or realm.moderation_request_channel.id != moderation_request_channel_id
|
||||
):
|
||||
new_moderation_request_channel_id = None
|
||||
if moderation_request_channel_id >= 0:
|
||||
(new_moderation_request_channel_id, sub) = access_stream_by_id(
|
||||
user_profile, moderation_request_channel_id, require_content_access=False
|
||||
)
|
||||
do_set_realm_moderation_request_channel(
|
||||
realm,
|
||||
new_moderation_request_channel_id,
|
||||
moderation_request_channel_id,
|
||||
acting_user=user_profile,
|
||||
)
|
||||
data["moderation_request_channel_id"] = moderation_request_channel_id
|
||||
# ...
|
||||
```
|
||||
|
||||
This completes the backend implementation. A great next step is to
|
||||
write automated backend tests for your new feature.
|
||||
|
||||
### Backend tests
|
||||
|
||||
To test the new setting syncs correctly with the `property_types`
|
||||
framework, one usually just needs to add a line in each of
|
||||
`zerver/tests/test_events.py` and `zerver/tests/test_realm.py` with a
|
||||
list of values to switch between in the test. In the case of a boolean
|
||||
field, no action is required, because those tests will correctly assume
|
||||
that the only values to test are `True` and `False`.
|
||||
|
||||
In `test_events.py`, the function that runs tests for the `property_types`
|
||||
framework is `do_set_realm_property_test`, and in `test_realm.py`, it is
|
||||
`do_test_realm_update_api`.
|
||||
|
||||
One would still need to add tests for whether the setting actually
|
||||
controls the feature it is supposed to control. At this time, our example,
|
||||
`my_fantastic_feature`, does not change any behavior on the backend, so
|
||||
we don't need to add any additional tests. If you're using this tutorial
|
||||
as a guide for creating an actual new feature, then at this point you
|
||||
should add any necessary backend tests.
|
||||
|
||||
Visit Zulip's [Django testing](../testing/testing-with-django.md)
|
||||
documentation to learn more about the backend testing framework.
|
||||
|
||||
:::{tip}
|
||||
|
||||
Pick one of the other `Realm.property_types` settings and
|
||||
use `git grep` to find examples of backend tests that were written for
|
||||
that setting: `git grep REALM_SETTING zerver/tests/`.
|
||||
|
||||
:::
|
||||
|
||||
Also note that you may already need to update the API documentation for
|
||||
your new feature to pass new or existing backend tests at this point.
|
||||
The tutorial for [writing REST API endpoints](../documentation/api.md)
|
||||
can be a helpful resource, especially the section on [debugging schema
|
||||
validation errors](../documentation//api.md#debugging-schema-validation-errors).
|
||||
|
||||
### What is my fantastic feature?
|
||||
|
||||
Since you've now set up the backend for your new fantastic feature, let's
|
||||
take a moment to discuss what that feature actually will be. Here's where
|
||||
you get to be creative!
|
||||
|
||||
Think of some fun, visual change that you could make to the Zulip web app
|
||||
in your development environment. Maybe toggling `my_fantastic_feature` will
|
||||
change the app font to [Dingbats](https://en.wikipedia.org/wiki/Dingbat).
|
||||
Maybe it will change the color of the font in the message feed. Maybe it
|
||||
will make all user status emoji render upside down 🙃. That's up to you!
|
||||
|
||||
The next part of this tutorial will go through the process of setting up
|
||||
the toggle for turning your feature on and off via the organization
|
||||
settings admin modal in the web-app. After that, your final challenge will
|
||||
be to make the changes to the codebase to make your fantastic idea into a
|
||||
reality in your development environment.
|
||||
|
||||
### Update the frontend
|
||||
|
||||
After completing the process of adding a new realm setting on the backend,
|
||||
you'll want make the required frontend changes.
|
||||
|
||||
Since we've updated the data sent for a realm to include a new property,
|
||||
we need to update the `realm_schema` on the frontend to match our changes
|
||||
in `zerver/lib/events.py`. This can be done in `web/src/state_data.ts`:
|
||||
|
||||
```diff
|
||||
// web/src/state_data.ts
|
||||
|
||||
// Sync this with zerver.lib.events.do_events_register.
|
||||
export const realm_schema = z.object({
|
||||
// ...
|
||||
realm_move_messages_within_stream_limit_seconds: z.nullable(z.number()),
|
||||
+ realm_my_fantastic_feature: z.boolean(),
|
||||
realm_name_changes_disabled: z.boolean(),
|
||||
// ...
|
||||
```
|
||||
|
||||
In order for an admin user to enable and disable our new realm setting, a
|
||||
checkbox needs to be added to the organization settings 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 organization settings admin modal, you'll need
|
||||
to modify one of the templates for the organization settings modal:
|
||||
`web/templates/settings/organization...`. If you're adding a non-checkbox
|
||||
field, you'll need to specify the type of the field via the
|
||||
`data-setting-widget-type` attribute in the Handlebars template.
|
||||
|
||||
For simplicity, let's add the checkbox for `realm_my_fantastic_feature` to
|
||||
the "Message feed" section in `web/templates/settings/organization_settings_admin.hbs`.
|
||||
Since there are a number of realm settings controlled via checkbox, there
|
||||
is a `settings_checkbox` partial template already set up for you to reuse.
|
||||
|
||||
:::{tip}
|
||||
|
||||
Use `git grep` to see where the other realm settings in the
|
||||
"Message feed" section of that template are in the `web/` directory.
|
||||
|
||||
Also, if you're not familiar with the organization settings modal, log in
|
||||
as Iago in your development environment and follow the Zulip help center
|
||||
instructions for [customizing organization settings](https://zulip.com/help/customize-organization-settings).
|
||||
|
||||
:::
|
||||
|
||||
Then add the new form control in `web/src/admin.ts`.
|
||||
|
||||
```diff
|
||||
// web/src/admin.ts
|
||||
|
||||
export function build_page() {
|
||||
const options = {
|
||||
custom_profile_field_types: realm.custom_profile_field_types,
|
||||
full_name: current_user.full_name,
|
||||
// ...
|
||||
+ realm_my_fantastic_feature: realm.realm_my_fantastic_feature,
|
||||
// ...
|
||||
```
|
||||
|
||||
You'll also want to add a label to the `admin_settings_label` object
|
||||
in that same file. Note that these labels are marked for
|
||||
[translation](../translating/internationalization.md).
|
||||
|
||||
Finally, update `server_events_dispatch.js` to handle related events
|
||||
coming from the server. There is an object, `realm_settings`, in the
|
||||
function `dispatch_normal_event`. The keys in this object are setting
|
||||
names and the values are the UI updating functions to run when an event
|
||||
has occurred.
|
||||
|
||||
If there is no relevant UI change to make other than in settings page
|
||||
itself, the value should be `noop`. However, if you are writing a
|
||||
function to update the UI after a given setting has changed, your
|
||||
function should be referenced in the `realm_settings` of
|
||||
`server_events_dispatch.js`. See for example
|
||||
`settings_emoji.update_custom_emoji_ui`.
|
||||
|
||||
```diff
|
||||
// web/src/server_events_dispatch.js
|
||||
|
||||
function dispatch_normal_event(event) {
|
||||
switch (event.type) {
|
||||
// ...
|
||||
case 'realm':
|
||||
var realm_settings = {
|
||||
add_custom_emoji_policy: settings_emoji.update_custom_emoji_ui,
|
||||
allow_edit_history: noop,
|
||||
// ...
|
||||
+ my_fantastic_feature: ???,
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
Checkboxes and other common input elements handle the UI updates
|
||||
automatically through the logic in `settings_org.sync_realm_settings`.
|
||||
|
||||
The rest of the `dispatch_normal_events` function updates the state of the
|
||||
application if an update event has occurred on a realm property and runs
|
||||
the associated function to update the application's UI, if necessary.
|
||||
|
||||
Now that you've got the admin checkbox set up to toggle your new feature
|
||||
on and off, you should work on implementing your fantastic idea! Remember
|
||||
to use `git grep` to help as you become familiar with the codebase.
|
||||
|
||||
### Manually test
|
||||
|
||||
Here are few important cases you should consider when manually testing
|
||||
your changes:
|
||||
|
||||
- For organization settings where we have a "save/discard" model, make
|
||||
sure both the "Save" and "Discard changes" buttons are working
|
||||
properly when toggling the realm setting.
|
||||
|
||||
- If your setting is dependent on another setting, carefully check
|
||||
that both are properly synchronized. For example, the input element
|
||||
for `realm_waiting_period_threshold_custom_input` is shown only when
|
||||
we have selected the custom time limit option in the
|
||||
`realm_waiting_period_threshold` dropdown.
|
||||
|
||||
- Do some manual testing for the real-time synchronization of input
|
||||
elements across the browsers and just like "Discard changes" button,
|
||||
check whether dependent settings are synchronized properly (this is
|
||||
easy to do by opening two browser windows to the settings page, and
|
||||
making changes in one while watching the other).
|
||||
|
||||
- Each subsection has independent "Save" and "Discard changes"
|
||||
buttons, so changes and saving in one subsection shouldn't affect
|
||||
the others.
|
||||
|
||||
### Frontend tests
|
||||
|
||||
A great next step is to write frontend tests. There are two types of
|
||||
frontend tests: [node-based unit tests](../testing/testing-with-node.md) and
|
||||
[Puppeteer end-to-end tests](../testing/testing-with-puppeteer.md).
|
||||
|
||||
At the minimum, you'll want to update `web/tests/lib/example_realm.cjs`
|
||||
for the new realm setting that you've added.
|
||||
|
||||
Beyond that, you should add any applicable tests that verify the
|
||||
behavior of the setting you just created.
|
||||
|
||||
:::{tip}
|
||||
|
||||
If you've identified a similar feature on the frontend to the
|
||||
one that you're creating, use your `git grep` skills to find and read
|
||||
through the frontend tests for that feature.
|
||||
|
||||
:::
|
||||
|
||||
### Update documentation
|
||||
|
||||
If you're using this tutorial as a guide to add an actual new feature to
|
||||
Zulip, it's really important to make sure that your new feature is well
|
||||
documented.
|
||||
|
||||
A recommended way to document a new feature is to update and/or augment
|
||||
Zulip's existing [help center documentation](https://zulip.com/help/)
|
||||
to reflect your changes and additions.
|
||||
|
||||
At the very least, this will involve modifying (or adding) a Markdown
|
||||
file documenting the feature in the `starlight_help/` directory of the
|
||||
main Zulip server repository, where the source for Zulip's end user
|
||||
documentation is stored. Details about writing, editing and testing these
|
||||
files can be found in:
|
||||
[Writing help center articles](../documentation/helpcenter.md).
|
||||
|
||||
Also, new features will often impact Zulip's REST API documentation,
|
||||
which is found in `zerver/openapi/zulip.yaml`. You may have noticed
|
||||
this during the testing process as the Zulip test suite should fail if
|
||||
there is a change to the API without a corresponding update to the
|
||||
documentation.
|
||||
|
||||
The best way to understand writing and updating Zulip's API
|
||||
documentation is to read more about Zulip's
|
||||
[REST API documentation process](../documentation/api.md)
|
||||
and [OpenAPI configuration](../documentation/openapi.md).
|
||||
|
||||
In particular, if there is an API change, you should use the
|
||||
`create-api-changelog` tool to create a file for the API changelog
|
||||
entry for your new feature. The API feature level allows the
|
||||
developers of mobile clients and other tools using the Zulip API to
|
||||
programmatically determine whether the Zulip server they are
|
||||
interacting with supports a given feature; see the
|
||||
[Zulip release lifecycle](../overview/release-lifecycle.md).
|
||||
|
||||
### Share your fantastic feature
|
||||
|
||||
If you're using this tutorial to learn about the codebase and have
|
||||
created a fun, fantastic feature, share a screenshot and/or a GIF
|
||||
from your development environment of your unique changes to the
|
||||
web-app UI in the Zulip development community's
|
||||
[new members](https://chat.zulip.org/#topics/channel/95-new-members)
|
||||
channel!
|
||||
138
docs/tutorials/reading-list.md
Normal file
138
docs/tutorials/reading-list.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# Reading list
|
||||
|
||||
Here you can find a list of relevant resources that you may find useful for
|
||||
learning new skills, or polishing the ones you already have.
|
||||
|
||||
The list covers a wide variety of topics, from basic Python coding to
|
||||
general development guidelines.
|
||||
|
||||
Feel free to create a pull request in Zulip's [GitHub repository](https://github.com/zulip/zulip/) with any interesting books, articles or
|
||||
videos you would like to see in this list.
|
||||
|
||||
Some titles have been shortened for organizational purposes.
|
||||
|
||||
## General programming/IT
|
||||
|
||||
_Book_ - [Clean Code: A Handbook of Agile Software Craftsmanship](https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882) _(Not free!)_
|
||||
|
||||
_Books_ - [Free programming books list](https://github.com/vhf/free-programming-books)
|
||||
|
||||
_Blog_ - [Free Code Camp blog](https://medium.freecodecamp.com)
|
||||
|
||||
_Blog_ - [Idle Words talks transcripts](https://idlewords.com/talks)
|
||||
|
||||
_Tutorial_ - [HTTP Can Do That?!, by Sumana Harihareswara (PyCon 2016)](https://youtu.be/HsLrXt2l-kg)
|
||||
|
||||
_Video_ - [Minimum Viable Documentation, by Matthew Lyon (WriteTheDocs 2014)](https://youtu.be/bEZcodengwk)
|
||||
|
||||
_Video_ - [NoOps, by Kelsey Hightower (DepOpsDays 2016)](https://youtu.be/ajT90pC3ris)
|
||||
|
||||
_Video_ - [The mind behind Linux (TED interview)](https://youtu.be/o8NPllzkFhE)
|
||||
|
||||
_Tutorial_ - [Learn code the hard way](https://learncodethehardway.org)
|
||||
|
||||
_Tutorial_ - [What happens when...](https://github.com/alex/what-happens-when)
|
||||
|
||||
_Article_ - [An Interview With Linus Torvalds](https://techcrunch.com/2012/04/19/an-interview-with-millenium-technology-prize-finalist-linus-torvalds)
|
||||
|
||||
_Article_ - [Effective Learning Strategies for Programmers](https://akaptur.com/blog/2015/10/10/effective-learning-strategies-for-programmers/)
|
||||
|
||||
_Article_ - [Readme Driven Development](https://tom.preston-werner.com/2010/08/23/readme-driven-development.html)
|
||||
|
||||
_Article_ - [Systematic Debugging](https://akaptur.com/blog/2013/07/24/systematic-debugging)
|
||||
|
||||
_Paper_ - [Floating-Point Arithmetic](https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html)
|
||||
|
||||
## Python
|
||||
|
||||
_Video_ - [Intro to Python for beginners, by Jessica McKellar (PyCon 2013)](https://youtu.be/rkx5_MRAV3A)
|
||||
|
||||
_Video_ - [Breaking the rules, by Jessica McKellar (PyCon Sweden)](https://youtu.be/C0fnHhY9UOc)
|
||||
|
||||
_Video_ - [Build & break a Python sandbox, by Jessica McKellar (PyCon 2014)](https://pyvideo.org/pycon-us-2014/building-and-breaking-a-python-sandbox.html)
|
||||
|
||||
_Video_ - [Cache me if you can, by Guillaume Ardaud (PyCon 2014)](https://pyvideo.org/pycon-us-2014/cache-me-if-you-can-memcached-caching-patterns.html)
|
||||
|
||||
_Video_ - [Loop like a native, by Ned Batchelder (PyCon 2013)](https://youtu.be/EnSu9hHGq5o)
|
||||
|
||||
_Video_ - [Modern Dictionaries, by Raymond Hettinger (SF Python)](https://youtu.be/p33CVV29OG8)
|
||||
|
||||
_Video_ - [Python Language, by Guido van Rossum (PyCon 2016)](https://youtu.be/YgtL4S7Hrwo)
|
||||
|
||||
_Video_ - [The Mighty Dictionary, by Brandon Rhodes (PyCon 2010)](https://pyvideo.org/pycon-us-2010/the-mighty-dictionary-55.html)
|
||||
|
||||
_Article_ - [Static types in Python, oh my(py)!](https://blog.zulip.org/2016/10/13/static-types-in-python-oh-mypy)
|
||||
|
||||
_Guide_ - [The Hitchhiker’s Guide to Python!](https://docs.python-guide.org/)
|
||||
|
||||
## Java/Android
|
||||
|
||||
_Course_ - [Android Development for Beginners](https://www.udacity.com/course/android-development-for-beginners--ud837)
|
||||
|
||||
_Blog_ - [Java Tutorials for Beginners](https://www.geeksforgeeks.org/java/)
|
||||
|
||||
## JavaScript/ECMAScript
|
||||
|
||||
_Tutorial_ - [clean-code-javascript Software engineering principles](https://github.com/ryanmcdermott/clean-code-javascript)
|
||||
|
||||
_Course_ - [React native and redux course](https://www.udemy.com/course/the-complete-react-native-and-redux-course/) (_Not free!_)
|
||||
|
||||
_Video_ - [TypeScript vs. CoffeeScript vs. ES6](https://www.youtube.com/watch?v=Ae4h9GC9cCg)
|
||||
|
||||
## TypeScript
|
||||
|
||||
_Tutorial_ - [TypeScript handbook section on base types](https://www.typescriptlang.org/docs/handbook/basic-types.html)
|
||||
|
||||
_Book_ - [TypeScript Deep Dive](https://basarat.gitbooks.io/typescript/)
|
||||
|
||||
_Guide_ - [TypeScript Declaration Files Introduction](https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html)
|
||||
|
||||
## Git/version control systems (VCS)
|
||||
|
||||
You may want to take a look first at our [Git and GitHub guide](../git/index.md).
|
||||
|
||||
## Computer science/algorithms
|
||||
|
||||
_Blog_ - [GeeksforGeeks](https://www.geeksforgeeks.org)
|
||||
|
||||
_Book_ [Introduction to Algorithms](https://mitpress.mit.edu/books/introduction-algorithms) (_Not free!_)
|
||||
|
||||
_Blog_ - [Setosa data visualization and visual explanations](https://setosa.io)
|
||||
|
||||
_Course_ - [Algorithms, Part I](https://www.coursera.org/learn/algorithms-part1)
|
||||
|
||||
_Course_ - [Open Source Society University](https://ossu.firebaseapp.com)
|
||||
|
||||
_Course_ - [MIT CSAIL 6.828: Operative Systems Engineering](https://pdos.csail.mit.edu/6.828/2016)
|
||||
|
||||
## Community experience
|
||||
|
||||
_Book_ - [Producing Open Source Software](https://producingoss.com/en/)
|
||||
|
||||
_Article_ - [Advice on Starting And Running A New Open Source Project](https://www.harihareswara.net/sumana/2016/08/04/1)
|
||||
|
||||
_Article_ - [How to ask good questions](https://jvns.ca/blog/good-questions)
|
||||
|
||||
_Article_ - [Notes for New FLOSS Contributors](https://www.harihareswara.net/sumana/2016/10/12/0)
|
||||
|
||||
_Article_ - [To be mentored](https://trueskawka.github.io/zulip/outreachy/blog/2017/01/02/to-be-mentored.html)
|
||||
|
||||
_Article_ - [To mentor](https://trueskawka.github.io/zulip/outreachy/gci/blog/2017/01/03/to-mentor.html)
|
||||
|
||||
[List of good projects for new contributors](https://github.com/MunGell/awesome-for-beginners)
|
||||
|
||||
## Competitions/camps
|
||||
|
||||
[CodeForces](https://codeforces.com)
|
||||
|
||||
[Free Code Camp](https://www.freecodecamp.com)
|
||||
|
||||
## Massive open online courses (MOOC) platforms
|
||||
|
||||
[Coursera](https://www.coursera.org)
|
||||
|
||||
[edX](https://www.edx.org)
|
||||
|
||||
[MIT OpenCourseWare](https://ocw.mit.edu)
|
||||
|
||||
[Udacity](https://www.udacity.com)
|
||||
350
docs/tutorials/shell-tips.md
Normal file
350
docs/tutorials/shell-tips.md
Normal file
@@ -0,0 +1,350 @@
|
||||
# Shell tips
|
||||
|
||||
The _shell_ is a **command line interpreter**. To use it you can open a
|
||||
_terminal_ (sometimes called a _console_). This is how most terminal windows
|
||||
look like:
|
||||
|
||||

|
||||
|
||||
If you haven't used it before, you should probably take a look at
|
||||
[this tutorial](http://linuxcommand.org/lc3_learning_the_shell.php).
|
||||
|
||||
If you're using Windows,
|
||||
[these videos](https://www.youtube.com/playlist?list=PL6gx4Cwl9DGDV6SnbINlVUd0o2xT4JbMu)
|
||||
may be useful too, but keep in mind that the following tips only apply to
|
||||
Linux/macOS environments (Unix shells). You can also use a tool, for example
|
||||
[Cygwin](https://www.cygwin.com/), to have a Unix-like shell on Windows.
|
||||
|
||||
## The prompt (`$`)
|
||||
|
||||
When searching Google, or Zulip's docs, you'll find commands that begin
|
||||
with a dollar sign `$` or a dollar sign preceded by some text
|
||||
(e.g., `(venv)john@laptop:~$`).
|
||||
|
||||
This is called the **prompt**, and it's only an indicator that the shell is
|
||||
awaiting new orders. The prompt can contain useful information, let's look
|
||||
at `(venv)john@laptop:~$`:
|
||||
|
||||
- `(venv)` informs the user that they're currently in a virtual environment
|
||||
(more on [Python virtual
|
||||
environments](https://docs.python-guide.org/dev/virtualenvs/))
|
||||
- the `john` before `@` is the username
|
||||
- the `laptop` is the host machine name
|
||||
- the `~` after the colon informs the user they're currently in the home
|
||||
folder of the user `john`
|
||||
|
||||
You shouldn't type the prompt or the text preceding it, since it isn't a
|
||||
part of the commands.
|
||||
|
||||
## Tilde character (`~`)
|
||||
|
||||
It's very frequent to see the tilde (`~`) in paths. The tilde is an
|
||||
abbreviation for your home directory (`/home/YOUR_USERNAME` most of the times).
|
||||
|
||||
That's why the following is exactly the same, if the user running it is
|
||||
`john`:
|
||||
|
||||
```console
|
||||
$ cd ~
|
||||
$ cd /home/john
|
||||
```
|
||||
|
||||
## Change directory (`cd`)
|
||||
|
||||
When you're using the shell, you work inside a directory (the one specified in
|
||||
the prompt). This way you can point to files relative to your current
|
||||
directory, instead of writing the whole path.
|
||||
|
||||
Imagine you have a file called `ideas.txt` inside `/home/john/notes/`, and
|
||||
you want to edit it using `nano`. You could use:
|
||||
|
||||
```console
|
||||
$ nano /home/john/notes/ideas.txt
|
||||
```
|
||||
|
||||
However, that isn't very practical, especially if you are working with
|
||||
longer paths.
|
||||
|
||||
That's why it's very useful to change the path where you are currently
|
||||
located (usually known as **working directory**). To do that, you use `cd`
|
||||
(**c**hange **d**irectory):
|
||||
|
||||
```console
|
||||
$ cd /home/john/notes/
|
||||
~/notes$ nano ideas.txt
|
||||
```
|
||||
|
||||
Or, if you're the user `john`: `cd ~/notes`.
|
||||
|
||||
You can now access to all the files inside `/home/john/notes` directly, without
|
||||
needing to type the whole path.
|
||||
|
||||
Relative paths make it much easier to move through files and
|
||||
directories, too.
|
||||
|
||||
## Running commands as root (`sudo`)
|
||||
|
||||
You may have noticed that many commands begin with `sudo`. This informs the
|
||||
shell that the following command must be run as the root - a user that by
|
||||
default has access to all commands and files on a Unix operating system (i.e.
|
||||
a user with administrator privileges). That's why you may be asked for a
|
||||
password in those cases: the system verifies you have permission to act as
|
||||
the _root_ user.
|
||||
|
||||
In case you were wondering, the name `sudo` comes from **s**uper **u**ser
|
||||
**do**.
|
||||
|
||||
## Escaping characters
|
||||
|
||||
Some characters cannot be used directly in the shell, because they have a
|
||||
special meaning. Consider the following example:
|
||||
|
||||
```console
|
||||
$ echo "He said hello"
|
||||
He said hello
|
||||
```
|
||||
|
||||
What if you wanted to display double quotes? You can't use
|
||||
`echo "He said "hello""`, because in that case you're using the
|
||||
double quotes for two different purposes:
|
||||
|
||||
- Delimiting the string you want to use, from `He` to `"hello"`.
|
||||
- Quoting something, by literally printing `"`.
|
||||
|
||||
You have to specify which double quotes are used in each case. When you want
|
||||
one of those "special characters" to be literally printed, that's called
|
||||
**character escaping**. To escape a character, simply add a backslash (`\`)
|
||||
before it.
|
||||
|
||||
Returning to our example:
|
||||
|
||||
```console
|
||||
$ echo "He said \"hello\""
|
||||
He said "hello"
|
||||
```
|
||||
|
||||
As you can see, the double quotes with the backslash are shown, but the ones
|
||||
without it are used as string delimiters.
|
||||
|
||||
Double quotes aren't the only case of special characters. Some others are `$`,
|
||||
`#`, `{` or `}`, but there are many more. The backslash itself can be escaped
|
||||
as well, using the same procedure: `\\`.
|
||||
|
||||
## Sequencing commands
|
||||
|
||||
It's also possible to run multiple commands in a single line. For that purpose,
|
||||
the shell provides two different separators:
|
||||
|
||||
- **Semicolon `;`**: runs a command, and once it has finished, runs the next
|
||||
one:
|
||||
|
||||
```console
|
||||
$ echo "Hello"; echo "World!"
|
||||
Hello
|
||||
World!
|
||||
```
|
||||
|
||||
- **Double ampersand `&&`**: runs a command, and **only if** it finished
|
||||
without errors, it proceeds with the next one:
|
||||
|
||||
```console
|
||||
$ qwfvijwe && echo "Hello"
|
||||
qwfvijwe: command not found
|
||||
```
|
||||
|
||||
Notice that it doesn't print `Hello` at the end, because the previous
|
||||
command (`qwfvijwe`) returned an error.
|
||||
|
||||
When using an incorrect command with a semicolon, the `Hello` will still
|
||||
be printed:
|
||||
|
||||
```console
|
||||
$ qwfvijwe; echo "Hello"
|
||||
qwfvijwe: command not found
|
||||
Hello
|
||||
```
|
||||
|
||||
## Splitting commands into multiple lines
|
||||
|
||||
Sometimes you end up with a very long command, that is hard to read and may
|
||||
be unclear. This is a problem, especially if you want to share that command,
|
||||
e.g., in a documentation file.
|
||||
|
||||
In those cases, you can use a backslash at the end of each line, to inform the
|
||||
shell "wait, there's more on the next line".
|
||||
|
||||
This is an example, taken from the docs on how to install the Zulip development
|
||||
environment:
|
||||
|
||||
```bash
|
||||
sudo apt-get -y purge vagrant && \
|
||||
curl -fLO https://releases.hashicorp.com/vagrant/2.0.2/vagrant_2.0.2_x86_64.deb && \
|
||||
sudo dpkg -i vagrant*.deb && \
|
||||
sudo apt-get -y install build-essential git ruby lxc lxc-templates cgroup-lite redir && \
|
||||
vagrant plugin install vagrant-lxc && \
|
||||
vagrant lxc sudoers
|
||||
```
|
||||
|
||||
It's all a single command, joined using the double ampersand explained in
|
||||
[Sequencing commands](#sequencing-commands). If you're typing it manually,
|
||||
you don't need to include the backslashes, just write it all on the same line,
|
||||
and hit <kbd>ENTER</kbd>/<kbd>RETURN</kbd> at the end.
|
||||
|
||||
If you think about it, what is happening here is actually another case of
|
||||
character escaping. The newline character (the one that appears when you hit
|
||||
<kbd>ENTER</kbd>) usually means "read this command". However, here we want to
|
||||
literally have the newline character, and thus the `\<newline>`.
|
||||
|
||||
The newline character is invisible (we only see a line break), but it's still
|
||||
there!
|
||||
|
||||
## Arguments
|
||||
|
||||
Most commands need additional data to work, like a path or a file. That extra
|
||||
information is called an **argument**, and it's specified after the name of the
|
||||
command, like this:
|
||||
|
||||
```console
|
||||
$ cd /home/john/notes
|
||||
```
|
||||
|
||||
Here, the command is `cd`, and the first (and only) argument is
|
||||
`/home/john/notes`:
|
||||
|
||||
- `cd` - _command_: changes your current directory.
|
||||
|
||||
- `/home/john/notes` - _argument_: the directory where you want to go.
|
||||
|
||||
In each command the arguments are specified in different ways, and have
|
||||
different meanings.
|
||||
|
||||
Sometimes, a command can accept arguments indicated with dashes. Here's another
|
||||
example of arguments usage:
|
||||
|
||||
```console
|
||||
$ nano -C /home/john/backups --mouse todo.txt
|
||||
```
|
||||
|
||||
As you can see, some arguments imply that more information has to be specified,
|
||||
while others don't.
|
||||
|
||||
In this case, we're saying: "Bash, use the app `nano` to open the file
|
||||
`todo.txt`, enabling mouse support, and saving the backup files to
|
||||
`/home/john/backups`". The different parts are:
|
||||
|
||||
- `nano` - _command_: program that allows editing text easily.
|
||||
|
||||
- `-C` - _argument_: needs you to indicate where the backups should be stored,
|
||||
and thus you have to add an additional argument after it, to specify the
|
||||
directory (`/home/john/backups` in the example).
|
||||
|
||||
- `--mouse` - _argument_: is just an option you set, `nano` doesn't need
|
||||
anything else to make it work. Thus, there isn't any extra argument for that.
|
||||
|
||||
Note that the `todo.txt` is the file we want to open! It has nothing to do with
|
||||
the previous argument. This will probably clarify it (taken from `nano`'s
|
||||
help):
|
||||
|
||||
```console
|
||||
Usage: nano [OPTIONS] [FILE]...
|
||||
```
|
||||
|
||||
So, in the options you indicate the arguments, and `FILE` is... well, the file.
|
||||
|
||||
Don't worry, you don't have to memorize the meaning of
|
||||
all the arguments for every single command. There are
|
||||
[tools](#understanding-commands) that help you with that :wink:.
|
||||
|
||||
## Shebang
|
||||
|
||||
You can run some files directly, without specifying a program to interpret
|
||||
them.
|
||||
|
||||
That's why you may have seen cases, in the Zulip codebase or
|
||||
elsewhere, when some Python scripts are called with `python`:
|
||||
|
||||
```console
|
||||
$ python my_program.py
|
||||
```
|
||||
|
||||
While other times, `python` isn't used:
|
||||
|
||||
```console
|
||||
$ ./my_program.py
|
||||
```
|
||||
|
||||
In the latter case, the operating system will look at the beginning of
|
||||
the script to identify what interpreter (like `python` in this
|
||||
example) to run it with.
|
||||
|
||||
The note telling the OS how to interpret the file goes on the file's
|
||||
very first line, and it's called a **shebang**. In our Python scripts,
|
||||
it looks like this:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
```
|
||||
|
||||
With this, you're telling the operating system: "if I tell you to run
|
||||
this, ask `/usr/bin/env python3` how to understand it".
|
||||
|
||||
The result is that the operating system is asked to run the script, it
|
||||
runs the command specified in the shebang, with the script filename
|
||||
added as a command-line argument. So, returning to our example with
|
||||
`my_program.py`, when you run `./my_program.py`, what happens under
|
||||
the hood is equivalent to:
|
||||
|
||||
```console
|
||||
$ /usr/bin/env python3 ./my_program.py
|
||||
```
|
||||
|
||||
The purpose of `/usr/bin/env` in our shebangs is as a way to locate
|
||||
the `python3` program in your current environment, the same one the
|
||||
shell would use if you ran `python3 my_program.py`. You may see
|
||||
Python scripts outside of Zulip with a shebang like
|
||||
`#!/usr/bin/python3`; but because of the way Python virtualenvs work,
|
||||
this has the effect of running the script outside of any currently
|
||||
activated virtualenv. We use `/usr/bin/env` to keep our scripts
|
||||
running inside the virtualenv where we've installed all our
|
||||
dependencies.
|
||||
|
||||
Another example of a shebang is the one used in Bash scripts. In those
|
||||
cases, `#!/bin/bash` or `#!/bin/sh` is used.
|
||||
|
||||
## Understanding commands
|
||||
|
||||
Frequently, you may find commands that you don't understand, or don't
|
||||
know what they do. You can use `man <command>` to see the **man**ual page for
|
||||
that specific command. Also, you may find useful
|
||||
[explainshell](https://explainshell.com/), a webpage that explains what most
|
||||
commands do, part by part.
|
||||
Finally, [docopt](http://docopt.org/) can help you understand the syntax used
|
||||
by command-line tools to describe their interface, and make sense of strings like
|
||||
`cd: usage: cd [-L|[-P [-e]] [-@]] [dir]`).
|
||||
|
||||
## Cheatsheet
|
||||
|
||||
There are many more commands in the shell, besides the ones explained in this
|
||||
file.
|
||||
[Here](https://www.git-tower.com/blog/command-line-cheat-sheet/) you can find
|
||||
a simple yet useful cheatsheet, created by Tower, that could help you
|
||||
understand and remember what other common commands do (e.g., `ls`).
|
||||
|
||||
## Git
|
||||
|
||||
Probably at this point you've heard about Git. It's basically a tool that most
|
||||
developers use to manage all the changes in their code.
|
||||
|
||||
At first it seems like magic, but once you get the basic concepts you find it
|
||||
extremely useful and even easy to use (at least the 99% of the time).
|
||||
|
||||
To learn more about how to use it, read
|
||||
[our docs](../git/index.md) on Git and
|
||||
GitHub.
|
||||
|
||||
[This cheatsheet][git-cheat-detailed]
|
||||
will be useful in your journey, as well.
|
||||
|
||||

|
||||
|
||||
[git-cheat-detailed]: ../git/cheat-sheet.md#detailed-cheat-sheet
|
||||
415
docs/tutorials/writing-views.md
Normal file
415
docs/tutorials/writing-views.md
Normal file
@@ -0,0 +1,415 @@
|
||||
# Writing views in Zulip
|
||||
|
||||
## What this covers
|
||||
|
||||
This page documents how views work in Zulip. You may want to read the
|
||||
[new feature tutorial](new-feature-tutorial.md)
|
||||
and treat this as a reference.
|
||||
|
||||
If you have experience with Django, much of this will be familiar, but
|
||||
you may want to read about how REST requests are dispatched, and how
|
||||
request authentication works.
|
||||
|
||||
This document supplements the [new feature tutorial](new-feature-tutorial.md)
|
||||
and the [testing](../testing/testing.md)
|
||||
documentation.
|
||||
|
||||
## What is a view?
|
||||
|
||||
A view in Zulip is everything that helps implement a server endpoint.
|
||||
Every path that the Zulip server supports (doesn't show a 404 page
|
||||
for) is a view. The obvious ones are those you can visit in your
|
||||
browser, for example
|
||||
[/integrations](https://zulip.com/integrations/), which shows the
|
||||
integration documentation. These paths show up in the address bar of
|
||||
the browser. There are other views that are only seen by software,
|
||||
namely the API views. They are used to build the various clients that
|
||||
Zulip has, namely the web client (which is also used by the desktop
|
||||
client) and the mobile clients.
|
||||
|
||||
## Modifying urls.py
|
||||
|
||||
A view is anything with an entry in the appropriate urls.py, usually
|
||||
`zproject/urls.py`. Zulip views either serve HTML (pages for browsers)
|
||||
or JSON (data for Zulip clients on all platforms, custom bots, and
|
||||
integrations).
|
||||
|
||||
The format of the URL patterns in Django is [documented
|
||||
here](https://docs.djangoproject.com/en/5.0/topics/http/urls/), and
|
||||
the Zulip specific details for these are discussed in detail in the
|
||||
[life of a request doc](life-of-a-request.md#options).
|
||||
|
||||
We have two Zulip-specific conventions we use for internationalization and for
|
||||
our REST API, respectively.
|
||||
|
||||
## Writing human-readable views
|
||||
|
||||
If you're writing a new page for the website, make sure to add it
|
||||
to `i18n_urls` in `zproject/urls.py`
|
||||
|
||||
```diff
|
||||
i18n_urls = [
|
||||
...
|
||||
+ path('quote-of-the-day', TemplateView.as_view(template_name='zerver/qotd.html')),
|
||||
+ path('postcards', 'zerver.views.postcards'),
|
||||
]
|
||||
```
|
||||
|
||||
As an example, if a request comes in for Spanish, language code `es`,
|
||||
the server path will be something like: `es/features/`.
|
||||
|
||||
### Decorators used for webpage views
|
||||
|
||||
This section documents a few simple decorators that we use for webpage
|
||||
views, as an introduction to view decorators.
|
||||
|
||||
`require_post`:
|
||||
|
||||
```py
|
||||
|
||||
@require_post
|
||||
def accounts_register(request: HttpRequest) -> HttpResponse:
|
||||
```
|
||||
|
||||
This decorator ensures that the request was a POST--here, we're
|
||||
checking that the registration submission page is requested with a
|
||||
post, and inside the function, we'll check the form data. If you
|
||||
request this page with GET, you'll get an HTTP 405 METHOD NOT ALLOWED
|
||||
error.
|
||||
|
||||
`zulip_login_required`:
|
||||
|
||||
This decorator verifies that the browser is logged in (i.e. has a
|
||||
valid session cookie) before providing the view for this route, or
|
||||
redirects the browser to a login page. This is used in the root path
|
||||
(`/`) of the website for the web client. If a request comes from a
|
||||
browser without a valid session cookie, they are redirected to a login
|
||||
page. It is a small fork of Django's
|
||||
[login_required][login-required-link], adding a few extra checks
|
||||
specific to Zulip.
|
||||
|
||||
```py
|
||||
@zulip_login_required
|
||||
def home(request: HttpRequest) -> HttpResponse:
|
||||
```
|
||||
|
||||
[login-required-link]: https://docs.djangoproject.com/en/5.0/topics/auth/default/#django.contrib.auth.decorators.login_required
|
||||
|
||||
### Writing a template
|
||||
|
||||
Templates for the main website are found in
|
||||
[templates/zerver/app](https://github.com/zulip/zulip/tree/main/templates/zerver/app).
|
||||
|
||||
## Writing API REST endpoints
|
||||
|
||||
These are code-parsable views that take x-www-form-urlencoded or JSON
|
||||
request bodies, and return JSON-string responses. Almost all Zulip
|
||||
view code is in the implementations of API REST endpoints.
|
||||
|
||||
The REST API does authentication of the user through `rest_dispatch`,
|
||||
which is documented in detail at
|
||||
[zerver/lib/rest.py](https://github.com/zulip/zulip/blob/main/zerver/lib/rest.py).
|
||||
This method will authenticate the user either through a session token
|
||||
from a cookie on the browser, or from a base64 encoded `email:api-key`
|
||||
string given via HTTP basic auth for API clients.
|
||||
|
||||
```py
|
||||
>>> import requests
|
||||
>>> r = requests.get('https://api.github.com/user', auth=('hello@example.com', '0123456789abcdeFGHIJKLmnopQRSTUV'))
|
||||
>>> r.status_code
|
||||
-> 200
|
||||
```
|
||||
|
||||
### Request variables
|
||||
|
||||
Most API views will have some arguments that are passed as part of the
|
||||
request to control the behavior of the view. In any well-engineered
|
||||
view, you need to write code to parse and validate that the arguments
|
||||
exist and have the correct form. For many applications, this leads to
|
||||
one of several bad outcomes:
|
||||
|
||||
- The code isn't written, so arguments aren't validated, leading to
|
||||
bugs and confusing error messages for users of the API.
|
||||
- Every function starts with a long list of semi-redundant validation
|
||||
code, usually with highly inconsistent error messages.
|
||||
- Every view function comes with another function that does the
|
||||
validation that has the problems from the last bullet point.
|
||||
|
||||
In Zulip, we solve this problem with a special decorator called
|
||||
`typed_endpoint` which allows a developer to declare the
|
||||
arguments a view function takes and validate their types all within
|
||||
the `def` line of the function. This framework uses
|
||||
[Pydantic V2](https://docs.pydantic.dev/dev/) to perform data validation
|
||||
and parsing for the view arguments. We like this framework because we
|
||||
have found it makes the validation code compact, readable, and
|
||||
conveniently located in the same place as the method it is validating
|
||||
arguments for.
|
||||
|
||||
Here's an example:
|
||||
|
||||
```py
|
||||
from zerver.decorator import require_realm_admin
|
||||
from zerver.lib.typed_endpoint import typed_endpoint
|
||||
|
||||
@require_realm_admin
|
||||
@typed_endpoint
|
||||
def create_user_backend(
|
||||
request: HttpRequest,
|
||||
user_profile: UserProfile,
|
||||
*,
|
||||
email: str,
|
||||
password: str,
|
||||
full_name: str,
|
||||
):
|
||||
# ... code here
|
||||
```
|
||||
|
||||
The `typed_endpoint` decorator parses the declared
|
||||
[keyword-only arguments](https://docs.python.org/3/glossary.html#term-parameter)
|
||||
of the decorated function, and for each argument that has been declared,
|
||||
it extracts the HTTP parameter with that name from the request,
|
||||
parses it according to the type annotation, and then passes it to
|
||||
the function. It will return an nicely JSON formatted HTTP 400 error
|
||||
in the event that an argument is missing, doesn't parse as JSON, or
|
||||
otherwise is invalid.
|
||||
|
||||
`require_realm_admin` is another decorator which checks the
|
||||
authorization of the given `user_profile` to make sure it belongs to a
|
||||
realm administrator (and thus has permission to create a user); we
|
||||
show it here primarily to show how `typed_endpoint` should be
|
||||
the inner decorator.
|
||||
|
||||
The implementation of `typed_endpoint` is documented in detail
|
||||
in
|
||||
[zerver/lib/typed_endpoint.py](https://github.com/zulip/zulip/blob/main/zerver/lib/typed_endpoint.py)
|
||||
|
||||
Pydantic also helps us with request variable validation. For example:
|
||||
|
||||
- `msg_ids: Json[list[int]]` will check that the `msg_ids`
|
||||
HTTP parameter is a list of integers, marshalled as JSON,
|
||||
and pass it into the function as the `msg_ids` Python
|
||||
keyword argument.
|
||||
|
||||
- `streams_raw: Annotated[Json[list[str]], ApiParamConfig("subscriptions")]`
|
||||
will check that the "subscriptions" HTTP parameter is a list of
|
||||
strings, marshalled as JSON, and pass it into the function with the
|
||||
Python keyword argument `streams_raw`.
|
||||
|
||||
- `message_id: Json[NonNegativeInt]` will check that the `message_id`
|
||||
HTTP parameter is a string containing a JSON encoded non-negative
|
||||
integer.
|
||||
|
||||
[Annotated](https://docs.python.org/3/library/typing.html#typing.Annotated)
|
||||
can be used in combination with
|
||||
[Pydantic's validators](https://docs.pydantic.dev/latest/api/functional_validators/)
|
||||
to provide additional validation for the arguments.
|
||||
|
||||
- `name: Annotated[str, StringConstraints(max_length=60)]` will check that the
|
||||
`name` HTTP parameter is a string containing up to 60 characters.
|
||||
|
||||
- Since there is no need to JSON-encode strings
|
||||
(lists, integers, bools and complex objects require JSON encoding), usually simply
|
||||
`my_string: str` is correct. One can pass, for example,
|
||||
`Annotated[str, check_string_in_validator(...)]` where one wants to run a
|
||||
validator on the value of a string.
|
||||
|
||||
Default values can be specified for optional arguments similar to how we would specify
|
||||
default values in regular python function.
|
||||
|
||||
- `is_default_stream: Json[bool] = False` will assign False to the `is_default_stream` argument
|
||||
if no value is specified when making a request to the endpoint.
|
||||
|
||||
- We can use `None` as the default value for optional arguments when we don't
|
||||
want to specify any specific default value, for example,
|
||||
`narrow: Json[list[NarrowParameter]] | None = None`. This does not allow the
|
||||
caller to pass `None` as the value, the only way `narrow` can be set to `None` is
|
||||
by using the default value.
|
||||
|
||||
[Pydantic models](https://docs.pydantic.dev/latest/concepts/models/) can be used to
|
||||
define the schema of complex objects that can be passed to the endpoint.
|
||||
|
||||
Here's an example:
|
||||
|
||||
```py
|
||||
from typing import Annotated
|
||||
|
||||
from pydantic import BaseModel, StringConstraints, model_validator
|
||||
|
||||
class AddSubscriptionData(BaseModel):
|
||||
name: str
|
||||
color: str | None = None
|
||||
description: (
|
||||
Annotated[str, StringConstraints(max_length=Stream.MAX_DESCRIPTION_LENGTH)] | None
|
||||
) = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_terms(self) -> "AddSubscriptionData":
|
||||
# ... Validation logic here
|
||||
return self
|
||||
```
|
||||
|
||||
- `add: Json[list[AddSubscriptionData]]` will require the `add` argument to be a list of objects
|
||||
having the keys that are specified in the `AddSubscriptionData` model.
|
||||
|
||||
- `@model_validator` can be used to specify additional validation logic for the model.
|
||||
|
||||
See
|
||||
[zerver/lib/typed_endpoint_validators.py](https://github.com/zulip/zulip/blob/main/zerver/lib/typed_endpoint_validators.py)
|
||||
for more validators and their documentation.
|
||||
|
||||
### Deciding which HTTP verb to use
|
||||
|
||||
When writing a new API view, you should writing a view to do just one
|
||||
type of thing. Usually that's either a read or write operation.
|
||||
|
||||
If you're reading data, GET is the best option. Other read-only verbs
|
||||
are HEAD, which should be used for testing if a resource is available to
|
||||
be read with GET, without the expense of the full GET. OPTIONS is also
|
||||
read-only, and used by clients to determine which HTTP verbs are
|
||||
available for a given path. This isn't something you need to write, as
|
||||
it happens automatically in the implementation of `rest_dispatch`--see
|
||||
[zerver/lib/rest.py](https://github.com/zulip/zulip/blob/main/zerver/lib/rest.py)
|
||||
for more.
|
||||
|
||||
If you're creating new data, try to figure out if the thing you are
|
||||
creating is uniquely identifiable. For example, if you're creating a
|
||||
user, there's only one user per email. If you can find a unique ID,
|
||||
you should use PUT for the view. If you want to create the data multiple
|
||||
times for multiple requests (for example, requesting the send_message
|
||||
view multiple times with the same content should send multiple
|
||||
messages), you should use POST.
|
||||
|
||||
If you're updating existing data, use PATCH.
|
||||
|
||||
If you're removing data, use DELETE.
|
||||
|
||||
### Idempotency
|
||||
|
||||
When writing a new API endpoint, with the exception of things like
|
||||
sending messages, requests should be safe to repeat, without impacting
|
||||
the state of the server. This is _idempotency_.
|
||||
|
||||
You will often want to return an error if a request to change
|
||||
something would do nothing because the state is already as desired, to
|
||||
make debugging Zulip clients easier. This means that the response for
|
||||
repeated requests may not be the same, but the repeated requests won't
|
||||
change the server more than once or cause unwanted side effects.
|
||||
|
||||
### Making changes to the database
|
||||
|
||||
If the view does any modification to the database, that change is done
|
||||
in a helper function in `zerver/actions/*.py`. Those functions are
|
||||
responsible for doing a complete update to the state of the server,
|
||||
which often entails both updating the database and sending any events
|
||||
to notify clients about the state change. When possible, we prefer to
|
||||
design a clean boundary between the view function and the actions
|
||||
function is such that all user input validation happens in the view
|
||||
code (i.e. all 400 type errors are thrown there), and the actions code
|
||||
is responsible for atomically executing the change (this is usually
|
||||
signalled by having the actions function have a name starting with
|
||||
`do_`). So in most cases, errors in an actions function will be the
|
||||
result of an operational problem (e.g., lost connection to the
|
||||
database) and lead to a 500 error. If an actions function is
|
||||
responsible for validation as well, it should have a name starting
|
||||
with `check_`.
|
||||
|
||||
For example, in [zerver/views/realm.py](https://github.com/zulip/zulip/blob/main/zerver/views/realm.py):
|
||||
|
||||
```py
|
||||
@require_realm_admin
|
||||
@typed_endpoint
|
||||
def update_realm(
|
||||
request: HttpRequest,
|
||||
user_profile: UserProfile,
|
||||
*,
|
||||
name: Annotated[str | None, StringConstraints(max_length=Realm.MAX_REALM_NAME_LENGTH)] = None,
|
||||
# ...
|
||||
):
|
||||
realm = user_profile.realm
|
||||
# ...
|
||||
do_set_realm_property(realm, k, v, acting_user=user_profile)
|
||||
# ...
|
||||
```
|
||||
|
||||
`realm.save()` actually saves the changes to the realm to the
|
||||
database, and `send_event_on_commit` sends the event to active clients
|
||||
belonging to the provided list of users (in this case, all active
|
||||
users in the Zulip realm), once the current transaction completes.
|
||||
|
||||
### Calling from the web application
|
||||
|
||||
You should always use `channel.<method>` to make an `HTTP <method>` call
|
||||
to the Zulip JSON API. As an example, in
|
||||
[web/src/admin.ts](https://github.com/zulip/zulip/blob/main/web/src/admin.ts)
|
||||
|
||||
```js
|
||||
var url = "/json/realm";
|
||||
var data = {
|
||||
name: JSON.stringify(new_name),
|
||||
}
|
||||
channel.patch({
|
||||
url: url,
|
||||
data: data,
|
||||
success: function (response_data) {
|
||||
if (response_data.name !== undefined) {
|
||||
ui_report.success($t({defaultMessage: "Name changed!"}), name_status);
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
### Calling from an API client
|
||||
|
||||
Here's how you might manually make a call from python:
|
||||
|
||||
```py
|
||||
payload = {'name': new_name}
|
||||
|
||||
# email and API key
|
||||
api_auth = ('hello@example.com', '0123456789abcdeFGHIJKLmnopQRSTUV')
|
||||
|
||||
r = requests.patch(SERVER_URL + 'api/v1/realm',
|
||||
data=json.dumps(payload),
|
||||
auth=api_auth,
|
||||
)
|
||||
```
|
||||
|
||||
This is simply an illustration; we recommend making use of the [Zulip
|
||||
Python API bindings](https://zulip.com/api/) since they provide
|
||||
a nice interface for accessing the API.
|
||||
|
||||
## Legacy endpoints used by the web client
|
||||
|
||||
New features should conform the REST API style. The legacy, web-only
|
||||
endpoints can't effectively enforce usage of a browser, so they aren't
|
||||
preferable from a security perspective, and it is generally a good idea
|
||||
to make your feature available to other clients, especially the mobile
|
||||
clients.
|
||||
|
||||
These endpoints make use the older authentication decorator
|
||||
`authenticated_json_view`, so you may see it in the code.
|
||||
|
||||
## Incoming webhook integrations
|
||||
|
||||
Incoming webhooks are called by other services, often to send a message as part
|
||||
of those services' integrations. They are most often POST requests, and
|
||||
often there is very little you can customize about them. Usually you can
|
||||
expect that the webhook for a service will allow specification for the
|
||||
target server for the webhook, and an API key.
|
||||
|
||||
If the webhook does not have an option to provide a bot email, use the
|
||||
`webhook_view` decorator, to fill in the `user_profile` and
|
||||
`request.client` fields of a request:
|
||||
|
||||
```py
|
||||
@webhook_view("PagerDuty", all_event_types=ALL_EVENT_TYPES)
|
||||
@typed_endpoint
|
||||
def api_pagerduty_webhook(
|
||||
request: HttpRequest,
|
||||
user_profile: UserProfile,
|
||||
*,
|
||||
payload: JsonBodyPayload[WildValue],
|
||||
```
|
||||
|
||||
`request.client` will be the result of `get_client("ZulipPagerDutyWebhook")`
|
||||
in this example and it will be passed to `check_send_stream_message`. For
|
||||
more information, see [Clients in Zulip](../subsystems/client.md).
|
||||
Reference in New Issue
Block a user