mirror of
https://github.com/zulip/zulip.git
synced 2025-11-02 04:53:36 +00:00
Compare commits
1 Commits
1.3.9
...
enterprise
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a67775c27 |
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -15,6 +15,6 @@
|
||||
/zproject/test_settings.py export-ignore
|
||||
/zerver/fixtures export-ignore
|
||||
/zerver/tests.py export-ignore
|
||||
/frontend_tests export-ignore
|
||||
/zerver/tests export-ignore
|
||||
/node_modules export-ignore
|
||||
/humbug export-ignore
|
||||
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -7,7 +7,7 @@
|
||||
/prod-static
|
||||
/errors/*
|
||||
*.sw[po]
|
||||
*.DS_Store
|
||||
.DS_Store
|
||||
event_queues.pickle
|
||||
stats/
|
||||
zerver/fixtures/available-migrations
|
||||
@@ -26,12 +26,3 @@ manage.log
|
||||
.kateproject.d/
|
||||
.kateproject
|
||||
*.kate-swp
|
||||
event_queues.json
|
||||
.vagrant
|
||||
/zproject/dev-secrets.conf
|
||||
static/js/bundle.js
|
||||
static/third/gemoji/
|
||||
static/third/zxcvbn/
|
||||
tools/emoji_dump/bitmaps/
|
||||
tools/emoji_dump/*.ttx
|
||||
node_modules
|
||||
|
||||
22
.travis.yml
22
.travis.yml
@@ -1,22 +0,0 @@
|
||||
before_install:
|
||||
- nvm install 0.10
|
||||
install:
|
||||
- tools/travis/setup-$TEST_SUITE
|
||||
cache:
|
||||
- apt: false
|
||||
env:
|
||||
- TEST_SUITE=frontend
|
||||
- TEST_SUITE=backend
|
||||
- TEST_SUITE=production
|
||||
- TEST_SUITE=py3k
|
||||
language: python
|
||||
python:
|
||||
- "2.7"
|
||||
# command to run tests
|
||||
script:
|
||||
- ./tools/travis/$TEST_SUITE
|
||||
sudo: required
|
||||
services:
|
||||
- docker
|
||||
addons:
|
||||
postgresql: "9.3"
|
||||
203
LICENSE
203
LICENSE
@@ -1,202 +1,5 @@
|
||||
Copyright <20> 2012-2013 Zulip, Inc.
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
This software is licensed under the Zulip Enterprise License Agreement.
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
Zulip can be reached at support@zulip.com.
|
||||
|
||||
363
README.dev.md
363
README.dev.md
@@ -1,363 +0,0 @@
|
||||
|
||||
Installing the Zulip Development environment
|
||||
============================================
|
||||
|
||||
You will need a machine with at least 2GB of RAM available (see
|
||||
https://github.com/zulip/zulip/issues/32 for a plan for how to
|
||||
dramatically reduce this requirement).
|
||||
|
||||
Start by cloning this repository: `git clone https://github.com/zulip/zulip.git`
|
||||
|
||||
Using Vagrant
|
||||
-------------
|
||||
|
||||
This is the recommended approach for all platforms, and will install
|
||||
the Zulip development environment inside a VM or container and works
|
||||
on any platform that supports Vagrant.
|
||||
|
||||
The best performing way to run the Zulip development environment is
|
||||
using an LXC container on a Linux host, but we support other platforms
|
||||
such as Mac via Virtualbox (but everything will be 2-3x slower).
|
||||
|
||||
* If your host is Ubuntu 15.04 or newer, you can install and configure
|
||||
the LXC Vagrant provider directly using apt:
|
||||
```
|
||||
sudo apt-get install vagrant lxc lxc-templates cgroup-lite redir
|
||||
vagrant plugin install vagrant-lxc
|
||||
```
|
||||
|
||||
* If your host is Ubuntu 14.04, you will need to [download a newer
|
||||
version of Vagrant](https://www.vagrantup.com/downloads.html), and
|
||||
then do the following:
|
||||
```
|
||||
sudo apt-get install lxc lxc-templates cgroup-lite redir
|
||||
sudo dpkg -i vagrant*.deb # in directory where you downloaded vagrant
|
||||
vagrant plugin install vagrant-lxc
|
||||
```
|
||||
|
||||
* For other Linux hosts with a kernel above 3.12, [follow the Vagrant
|
||||
LXC installation
|
||||
instructions](https://github.com/fgrehm/vagrant-lxc) to get Vagrant
|
||||
with LXC for your platform.
|
||||
|
||||
* If your host is OS X or older Linux, [download VirtualBox](https://www.virtualbox.org/wiki/Downloads),
|
||||
[download Vagrant](https://www.vagrantup.com/downloads.html), and install them both.
|
||||
|
||||
* If you're on OS X and have VMWare, it should be possible to patch
|
||||
Vagrantfile to use the VMWare vagrant provider which should perform
|
||||
much better than Virtualbox. Patches to do this by default if
|
||||
VMWare is available are welcome!
|
||||
|
||||
* On Windows: You can use Vagrant and Virtualbox/VMWare on Windows
|
||||
with Cygwin, similar to the Mac setup. Be sure to create your git
|
||||
clone using `git clone https://github.com/zulip/zulip.git -c
|
||||
core.autocrlf=false` to avoid Windows line endings being added to
|
||||
files (this causes weird errors).
|
||||
|
||||
Once that's done, simply change to your zulip directory and run
|
||||
`vagrant up` in your terminal to install the development server. This
|
||||
will take a long time on the first run because Vagrant needs to
|
||||
download the Ubuntu Trusty base image, but later you can run `vagrant
|
||||
destroy` and then `vagrant up` again to rebuild the environment and it
|
||||
will be much faster.
|
||||
|
||||
Once that finishes, you can run the development server as follows:
|
||||
|
||||
```
|
||||
vagrant ssh -- -L9991:localhost:9991
|
||||
# Now inside the container
|
||||
cd /srv/zulip
|
||||
source /srv/zulip-venv/bin/activate
|
||||
./tools/run-dev.py --interface=''
|
||||
```
|
||||
|
||||
To get shell access to the virtual machine running the server to run
|
||||
lint, management commands, etc., use `vagrant ssh`.
|
||||
|
||||
(A small note on tools/run-dev.py: the `--interface=''` option will make
|
||||
the development server listen on all network interfaces. While this
|
||||
is correct for the Vagrant guest sitting behind a NAT, you probably
|
||||
don't want to use that option when using run-dev.py in other environments).
|
||||
|
||||
At this point you should [read about using the development environment](https://github.com/zulip/zulip/blob/master/README.dev.md#using-the-development-environment).
|
||||
|
||||
|
||||
Using provision.py without Vagrant
|
||||
----------------------------------
|
||||
|
||||
If you'd like to install a Zulip development environment on a server
|
||||
that's already running Ubuntu 14.04 Trusty, you can do that by just
|
||||
running:
|
||||
|
||||
```
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y python-pbs
|
||||
python /srv/zulip/provision.py
|
||||
|
||||
cd /srv/zulip
|
||||
source /srv/zulip-venv/bin/activate
|
||||
./tools/run-dev.py
|
||||
```
|
||||
|
||||
Note that there is no supported uninstallation process without Vagrant
|
||||
(with Vagrant, you can just do `vagrant destroy` to clean up the
|
||||
development environment).
|
||||
|
||||
By hand
|
||||
-------
|
||||
If you really want to install everything by hand, the below
|
||||
instructions should work.
|
||||
|
||||
Install the following non-Python dependencies:
|
||||
* libffi-dev — needed for some Python extensions
|
||||
* postgresql 9.1 or later — our database (also install development headers)
|
||||
* memcached (and headers)
|
||||
* rabbitmq-server
|
||||
* libldap2-dev
|
||||
* python-dev
|
||||
* redis-server — rate limiting
|
||||
* tsearch-extras — better text search
|
||||
* libfreetype6-dev - needed before you pip install Pillow to properly generate emoji PNGs
|
||||
|
||||
### On Debian or Ubuntu systems:
|
||||
|
||||
```
|
||||
sudo apt-get install libffi-dev memcached rabbitmq-server libldap2-dev python-dev redis-server postgresql-server-dev-all libmemcached-dev libfreetype6-dev
|
||||
|
||||
# If on 12.04 or wheezy:
|
||||
sudo apt-get install postgresql-9.1
|
||||
wget https://dl.dropboxusercontent.com/u/283158365/zuliposs/postgresql-9.1-tsearch-extras_0.1.2_amd64.deb
|
||||
sudo dpkg -i postgresql-9.1-tsearch-extras_0.1.2_amd64.deb
|
||||
|
||||
# If on 14.04:
|
||||
sudo apt-get install postgresql-9.3
|
||||
wget https://dl.dropboxusercontent.com/u/283158365/zuliposs/postgresql-9.3-tsearch-extras_0.1.2_amd64.deb
|
||||
sudo dpkg -i postgresql-9.3-tsearch-extras_0.1.2_amd64.deb
|
||||
|
||||
# If on 15.04 or jessie:
|
||||
sudo apt-get install postgresql-9.4
|
||||
wget https://dl.dropboxusercontent.com/u/283158365/zuliposs/postgresql-9.4-tsearch-extras_0.1_amd64.deb
|
||||
sudo dpkg -i postgresql-9.4-tsearch-extras_0.1_amd64.deb
|
||||
```
|
||||
|
||||
Now continue with the "All systems" instructions below.
|
||||
|
||||
### On Fedora 22 (experimental):
|
||||
|
||||
These instructions are experimental and may have bugs; patches welcome!
|
||||
|
||||
```
|
||||
sudo dnf install libffi-devel memcached rabbitmq-server openldap-devel python-devel redis postgresql-server postgresql-devel postgresql libmemcached-devel freetype-devel
|
||||
```
|
||||
|
||||
Now continue with the Common to Fedora/CentOS instructions below.
|
||||
|
||||
### On CentOS 7 Core (experimental):
|
||||
|
||||
These instructions are experimental and may have bugs; patches welcome!
|
||||
|
||||
```
|
||||
# Add user zulip to the system (not necessary if you configured zulip as the administrator
|
||||
# user during the install process of CentOS 7).
|
||||
useradd zulip
|
||||
|
||||
# Create a password for zulip user
|
||||
passwd zulip
|
||||
|
||||
# Allow zulip to sudo
|
||||
visudo
|
||||
# Add this line after line `root ALL=(ALL) ALL`
|
||||
zulip ALL=(ALL) ALL
|
||||
|
||||
# Switch to zulip user
|
||||
su zulip
|
||||
|
||||
# Enable EPEL 7 repo so we can install rabbitmq-server, redis and other dependencies
|
||||
sudo yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
|
||||
|
||||
# Install dependencies
|
||||
sudo yum install libffi-devel memcached rabbitmq-server openldap-devel python-devel redis postgresql-server \
|
||||
postgresql-devel postgresql libmemcached-devel wget python-pip openssl-devel freetype-devel libjpeg-turbo-devel \
|
||||
zlib-devel nodejs
|
||||
|
||||
# We need these packages to compile tsearch-extras
|
||||
sudo yum groupinstall "Development Tools"
|
||||
|
||||
# clone Zulip's git repo and cd into it
|
||||
cd && git clone https://github.com/zulip/zulip && cd zulip/
|
||||
|
||||
## NEEDS TESTING: The next few DB setup items may not be required at all.
|
||||
# Initialize the postgres db
|
||||
sudo postgresql-setup initdb
|
||||
|
||||
# Edit the postgres settings:
|
||||
sudo vi /var/lib/pgsql/data/pg_hba.conf
|
||||
|
||||
# Change these lines:
|
||||
host all all 127.0.0.1/32 ident
|
||||
host all all ::1/128 ident
|
||||
# to this:
|
||||
host all all 127.0.0.1/32 md5
|
||||
host all all ::1/128 md5
|
||||
```
|
||||
|
||||
Now continue with the Common to Fedora/CentOS instructions below.
|
||||
|
||||
### Common to Fedora/CentOS instructions
|
||||
|
||||
```
|
||||
# Build and install postgres tsearch-extras module
|
||||
wget https://launchpad.net/~tabbott/+archive/ubuntu/zulip/+files/tsearch-extras_0.1.3.tar.gz
|
||||
tar xvzf tsearch-extras_0.1.3.tar.gz
|
||||
cd ts2
|
||||
make
|
||||
sudo make install
|
||||
|
||||
# Hack around missing dictionary files -- need to fix this to get
|
||||
# the proper dictionaries from what in debian is the hunspell-en-us package.
|
||||
sudo touch /usr/share/pgsql/tsearch_data/english.stop
|
||||
sudo touch /usr/share/pgsql/tsearch_data/en_us.dict
|
||||
sudo touch /usr/share/pgsql/tsearch_data/en_us.affix
|
||||
|
||||
# Edit the postgres settings:
|
||||
sudo vi /var/lib/pgsql/data/pg_hba.conf
|
||||
|
||||
# Add this line before the first uncommented line to enable password auth:
|
||||
host all all 127.0.0.1/32 md5
|
||||
|
||||
# Start the services
|
||||
sudo systemctl start redis memcached rabbitmq-server postgresql
|
||||
|
||||
# Enable automatic service startup after the system startup
|
||||
sudo systemctl enable redis rabbitmq-server memcached postgresql
|
||||
```
|
||||
|
||||
Finally continue with the All Systems instructions below.
|
||||
|
||||
### All Systems:
|
||||
|
||||
```
|
||||
pip install -r requirements.txt
|
||||
./tools/download-zxcvbn
|
||||
./tools/emoji_dump/build_emoji
|
||||
./scripts/setup/generate_secrets.py -d
|
||||
sudo cp ./puppet/zulip/files/postgresql/zulip_english.stop /usr/share/postgresql/9.3/tsearch_data/
|
||||
./scripts/setup/configure-rabbitmq
|
||||
./tools/postgres-init-dev-db
|
||||
./tools/do-destroy-rebuild-database
|
||||
./tools/postgres-init-test-db
|
||||
./tools/do-destroy-rebuild-test-database
|
||||
```
|
||||
|
||||
To start the development server:
|
||||
|
||||
```
|
||||
./tools/run-dev.py
|
||||
```
|
||||
|
||||
… and visit [http://localhost:9991/](http://localhost:9991/).
|
||||
|
||||
|
||||
Using the Development Environment
|
||||
=================================
|
||||
|
||||
Once the development environment is running, you can visit
|
||||
<http://localhost:9991/> in your browser. By default, the development
|
||||
server homepage just shows a list of the users that exist on the
|
||||
server and you can login as any of them by just clicking on a user.
|
||||
This setup saves time for the common case where you want to test
|
||||
something other than the login process; to test the login process
|
||||
you'll want to change AUTHENTICATION_BACKENDS in the not-PRODUCTION
|
||||
case of `zproject/settings.py` from zproject.backends.DevAuthBackend
|
||||
to use the auth method(s) you'd like to test.
|
||||
|
||||
While developing, it's helpful to watch the `run-dev.py` console
|
||||
output, which will show any errors your Zulip development server
|
||||
encounters.
|
||||
|
||||
When you make a change, here's a guide for what you need to do in
|
||||
order to see your change take effect in Development:
|
||||
|
||||
* If you change Javascript or CSS, you'll just need to reload the
|
||||
browser window to see changes take effect.
|
||||
|
||||
* If you change Python code used by the the main Django/Tornado server
|
||||
processes, these services are run on top of Django's [manage.py
|
||||
runserver](https://docs.djangoproject.com/en/1.8/ref/django-admin/#runserver-port-or-address-port),
|
||||
which will automatically restart the Zulip Django and Tornado servers
|
||||
whenever you save changes to Python code. You can watch this happen
|
||||
in the `run-dev.py` console to make sure the backend has reloaded.
|
||||
|
||||
* The Python queue workers don't automatically restart when you save
|
||||
changes (or when they stop running), so you will want to ctrl-C and
|
||||
then restart `run-dev.py` manually if you are testing changes to the
|
||||
queue workers or if a queue worker has crashed.
|
||||
|
||||
* If you change the database schema, you'll need to use the standard
|
||||
Django migrations process to create and then run your migrations; see
|
||||
the [new feature
|
||||
tutorial](http://zulip.readthedocs.org/en/latest/new-feature-tutorial.html)
|
||||
for an example. Additionally you should check out the [detailed
|
||||
testing docs](http://zulip.readthedocs.org/en/latest/testing.html) for
|
||||
how to run the tests properly after doing a migration.
|
||||
|
||||
(In production, everything runs under supervisord and thus will
|
||||
restart if it crashes, and `upgrade-zulip` will take care of running
|
||||
migrations and then cleanly restaring the server for you).
|
||||
|
||||
Running the test suite
|
||||
======================
|
||||
|
||||
For more details, check out the [detailed testing
|
||||
docs](http://zulip.readthedocs.org/en/latest/testing.html).
|
||||
|
||||
To run all the tests, do this:
|
||||
```
|
||||
./tools/test-all
|
||||
```
|
||||
|
||||
For the Vagrant environment, you'll want to first enter the environment:
|
||||
```
|
||||
vagrant ssh
|
||||
source /srv/zulip-venv/bin/activate
|
||||
cd /srv/zulip
|
||||
```
|
||||
|
||||
This runs the linter (`tools/lint-all`) plus all of our test suites;
|
||||
they can all be run separately (just read `tools/test-all` to see
|
||||
them). You can also run individual tests which can save you a lot of
|
||||
time debugging a test failure, e.g.:
|
||||
|
||||
```
|
||||
./tools/lint-all # Runs all the linters in parallel
|
||||
./tools/test-backend zerver.test_bugdown.BugdownTest.test_inline_youtube
|
||||
./tools/test-js-with-casper 10-navigation.js
|
||||
./tools/test-js-with-node # Runs all node tests but is very fast
|
||||
```
|
||||
|
||||
The above setup instructions include the first-time setup of test
|
||||
databases, but you may need to rebuild the test database occasionally
|
||||
if you're working on new database migrations. To do this, run:
|
||||
|
||||
```
|
||||
./tools/postgres-init-test-db
|
||||
./tools/do-destroy-rebuild-test-database
|
||||
```
|
||||
|
||||
Possible testing issues
|
||||
=======================
|
||||
|
||||
- When running the test suite, if you get an error like this:
|
||||
|
||||
```
|
||||
sqlalchemy.exc.ProgrammingError: (ProgrammingError) function ts_match_locs_array(unknown, text, tsquery) does not exist
|
||||
LINE 2: ...ECT message_id, flags, subject, rendered_content, ts_match_l...
|
||||
^
|
||||
```
|
||||
|
||||
… then you need to install tsearch-extras, described
|
||||
above. Afterwards, re-run the `init*-db` and the
|
||||
`do-destroy-rebuild*-database` scripts.
|
||||
|
||||
- When building the development environment using Vagrant and the LXC provider, if you encounter permissions errors, you may need to `chown -R 1000:$(whoami) /path/to/zulip` on the host before running `vagrant up` in order to ensure that the synced directory has the correct owner during provision. This issue will arise if you run `id username` on the host where `username` is the user running Vagrant and the output is anything but 1000.
|
||||
This seems to be caused by Vagrant behavior; more information can be found here https://github.com/fgrehm/vagrant-lxc/wiki/FAQ#help-my-shared-folders-have-the-wrong-owner
|
||||
149
README.md
149
README.md
@@ -1,149 +0,0 @@
|
||||
Zulip
|
||||
=====
|
||||
|
||||
Zulip is a powerful, open source group chat application. Written in
|
||||
Python and using the Django framework, Zulip supports both private
|
||||
messaging and group chats via conversation streams.
|
||||
|
||||
Zulip also supports fast search, drag-and-drop file uploads, image
|
||||
previews, group private messages, audible notifications,
|
||||
missed-message emails, desktop apps, and much more.
|
||||
|
||||
Further information on the Zulip project and its features can be found
|
||||
at https://www.zulip.org.
|
||||
|
||||
Installing the Zulip Development environment
|
||||
============================================
|
||||
|
||||
The Zulip development environment is the recommened option for folks
|
||||
interested in trying out Zulip. This is documented in
|
||||
[README.dev.md](README.dev.md).
|
||||
|
||||
Running Zulip in production
|
||||
===========================
|
||||
|
||||
Zulip in production only supports Ubuntu 14.04 right now, but work is
|
||||
ongoing on adding support for additional platforms. The installation
|
||||
process is documented in https://zulip.org/server.html and in more
|
||||
detail in [README.prod.md](README.prod.md).
|
||||
|
||||
Contributing to Zulip
|
||||
=====================
|
||||
|
||||
Zulip welcomes all forms of contributions! The page documents the
|
||||
Zulip development process.
|
||||
|
||||
* **Pull requests**. Before a pull request can be merged, you need to to sign the [Dropbox
|
||||
Contributor License Agreement](https://opensource.dropbox.com/cla/).
|
||||
Also, please skim our [commit message style
|
||||
guidelines](http://zulip.readthedocs.org/en/latest/code-style.html#commit-messages).
|
||||
|
||||
* **Testing**. The Zulip automated tests all run automatically when
|
||||
you submit a pull request, but you can also run them all in your
|
||||
development environment following the instructions in the [testing
|
||||
section](https://github.com/zulip/zulip#running-the-test-suite) below.
|
||||
|
||||
* **Developer Documentation**. Zulip has a growing collection of
|
||||
developer documentation on [Read The Docs](https://zulip.readthedocs.org/).
|
||||
Recommended reading for new contributors includes the
|
||||
[directory structure](http://zulip.readthedocs.org/en/latest/directory-structure.html) and
|
||||
[new feature tutorial](http://zulip.readthedocs.org/en/latest/new-feature-tutorial.html).
|
||||
|
||||
* **Mailing list and bug tracker** Zulip has a [development discussion
|
||||
mailing list](https://groups.google.com/forum/#!forum/zulip-devel) and
|
||||
uses [GitHub issues](https://github.com/zulip/zulip/issues). Feel
|
||||
free to send any questions or suggestions of areas where you'd love to
|
||||
see more documentation to the list! Please report any security issues
|
||||
you discover to support@zulip.com.
|
||||
|
||||
* **App codebases** This repository is for the Zulip server and web app; the
|
||||
[desktop](https://github.com/zulip/zulip-desktop),
|
||||
[Android](https://github.com/zulip/zulip-android), and
|
||||
[iOS](https://github.com/zulip/zulip-ios) apps are separate
|
||||
repositories.
|
||||
|
||||
How to get involved with contributing to Zulip
|
||||
==============================================
|
||||
|
||||
First, subscribe to the Zulip [development discussion mailing list](https://groups.google.com/forum/#!forum/zulip-devel).
|
||||
|
||||
The Zulip project uses a system of labels in our [issue
|
||||
tracker](https://github.com/zulip/zulip/issues) to make it easy to
|
||||
find a project if you don't have your own project idea in mind or want
|
||||
to get some experience with working on Zulip before embarking on a
|
||||
larger project you have in mind:
|
||||
|
||||
* [Bite Size](https://github.com/zulip/zulip/labels/bite%20size):
|
||||
Smaller projects that could be a great first contribution.
|
||||
* [Integrations](https://github.com/zulip/zulip/labels/integrations).
|
||||
Integrate Zulip with another piece of software and contribute it
|
||||
back to the community! Writing an integration can be a great
|
||||
started project. There's some brief documentation on the best way
|
||||
to write integrations at https://github.com/zulip/zulip/issues/70.
|
||||
* [Documentation](https://github.com/zulip/zulip/labels/documentation).
|
||||
The Zulip project loves contributions of new documentation.
|
||||
* [Help Wanted](https://github.com/zulip/zulip/labels/help%20wanted):
|
||||
A broader list of projects that nobody is currently working on.
|
||||
* [Platform support](https://github.com/zulip/zulip/labels/Platform%20support).
|
||||
These are open issues about making it possible to install Zulip on a wider
|
||||
range of platforms.
|
||||
* [Bugs](https://github.com/zulip/zulip/labels/bug). Open bugs.
|
||||
* [Feature requests](https://github.com/zulip/zulip/labels/enhancement).
|
||||
Browsing this list can be a great way to find feature ideas to implement that
|
||||
other Zulip users are excited about.
|
||||
|
||||
If you're excited about helping with an open issue, just post on the
|
||||
conversation thread that you're working on it. You're encouraged to
|
||||
ask questions on how to best implement or debug your changes -- the
|
||||
Zulip maintainers are excited to answer questions to help you stay
|
||||
unblocked and working efficiently.
|
||||
|
||||
We also welcome suggestions of features that you feel would be
|
||||
valuable or changes that you feel would make Zulip a better open
|
||||
source project, and are happy to support you in adding new features or
|
||||
other user experience improvements to Zulip.
|
||||
|
||||
If you have a new feature you'd like to add, we recommend you start by
|
||||
opening a GitHub issue about the feature idea explaining the problem
|
||||
that you're hoping to solve and that you're excited to work on it. A
|
||||
Zulip maintainer will usually reply within a day with feedback on the
|
||||
idea, notes on any important issues or concerns, and and often tips on
|
||||
how to implement or test it. Please feel free to ping the thread if
|
||||
you don't hear a response from the maintainers -- we try to be very
|
||||
responsive so this usually means we missed your message.
|
||||
|
||||
For significant changes to the visual design, user experience, data
|
||||
model, or architecture, we highly recommend posting a mockup,
|
||||
screenshot, or description of what you have in mind to zulip-devel@ to
|
||||
get broad feedback before you spend too much time on implementation
|
||||
details.
|
||||
|
||||
Finally, before implementing a larger feature, we highly recommend
|
||||
looking at the new feature tutorial and coding style guidelines on
|
||||
ReadTheDocs.
|
||||
|
||||
Feedback on how to make this development process more efficient, fun,
|
||||
and friendly to new contributors is very welcome! Just shoot an email
|
||||
to the Zulip Developers list with your thoughts.
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
Copyright 2011-2015 Dropbox, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
The software includes some works released by third parties under other
|
||||
free and open source licenses. Those works are redistributed under the
|
||||
license terms under which the works were received. For more details,
|
||||
see the ``THIRDPARTY`` file included with this distribution.
|
||||
510
README.prod.md
510
README.prod.md
@@ -1,510 +0,0 @@
|
||||
Zulip in production
|
||||
===================
|
||||
|
||||
This documents the process for installing Zulip in a production environment.
|
||||
|
||||
Note that if you just want to play around with Zulip and see what it
|
||||
looks like, it is easier to install it in a development environment
|
||||
following the instructions in README.dev, since then you don't need to
|
||||
worry about setting up SSL certificates and an authentication mechanism.
|
||||
|
||||
Recommended requirements:
|
||||
|
||||
* Server running Ubuntu Trusty
|
||||
* At least 2 CPUs for production use with 100+ users
|
||||
* At least 4GB of RAM for production use with 100+ users. We **strongly
|
||||
recommend against installing with less than 2GB of RAM**, as you will
|
||||
likely experience OOM issues. In the future we expect Zulip's RAM
|
||||
requirements to decrease to support smaller installations (see
|
||||
https://github.com/zulip/zulip/issues/32).
|
||||
* At least 10GB of free disk for production use (more may be required
|
||||
if you intend to store uploaded files locally rather than in S3
|
||||
and your team uses that feature extensively)
|
||||
* Outgoing HTTP(S) access to the public Internet.
|
||||
* SSL Certificate for the host you're putting this on
|
||||
(e.g. zulip.example.com). If you just want to see what
|
||||
Zulip looks like, we recommend installing the development
|
||||
environment detailed in README.md as that is easier to setup.
|
||||
* Email credentials Zulip can use to send outgoing emails to users
|
||||
(e.g. email address confirmation emails during the signup process,
|
||||
missed message notifications, password reminders if you're not using
|
||||
SSO, etc.).
|
||||
|
||||
|
||||
Installing Zulip in production
|
||||
==============================
|
||||
|
||||
These instructions should be followed as root.
|
||||
|
||||
(1) Install the SSL certificates for your machine to
|
||||
`/etc/ssl/private/zulip.key` and `/etc/ssl/certs/zulip.combined-chain.crt`.
|
||||
If you don't know how to generate an SSL certificate, you, you can
|
||||
do the following to generate a self-signed certificate:
|
||||
|
||||
```
|
||||
apt-get install openssl
|
||||
openssl genrsa -des3 -passout pass:x -out server.pass.key 4096
|
||||
openssl rsa -passin pass:x -in server.pass.key -out zulip.key
|
||||
rm server.pass.key
|
||||
openssl req -new -key zulip.key -out server.csr
|
||||
openssl x509 -req -days 365 -in server.csr -signkey zulip.key -out zulip.combined-chain.crt
|
||||
rm server.csr
|
||||
cp zulip.key /etc/ssl/private/zulip.key
|
||||
cp zulip.combined-chain.crt /etc/ssl/certs/zulip.combined-chain.crt
|
||||
```
|
||||
|
||||
You will eventually want to get a properly signed certificate (and
|
||||
note that at present the Zulip desktop app doesn't support
|
||||
self-signed certificates), but this will let you finish the
|
||||
installation process.
|
||||
|
||||
(2) Download [the latest built server tarball](https://www.zulip.com/dist/releases/zulip-server-latest.tar.gz)
|
||||
and unpack it to `/root/zulip`, e.g.
|
||||
```
|
||||
wget https://www.zulip.com/dist/releases/zulip-server-latest.tar.gz
|
||||
tar -xf zulip-server-latest.tar.gz
|
||||
mv zulip-server-1.3.6 /root/zulip
|
||||
```
|
||||
|
||||
(3) Run
|
||||
```
|
||||
/root/zulip/scripts/setup/install
|
||||
```
|
||||
This may take a while to run, since it will install a large number of
|
||||
packages via apt.
|
||||
|
||||
(4) Configure the Zulip server instance by filling in the settings in
|
||||
`/etc/zulip/settings.py`. Be sure to fill in all the mandatory
|
||||
settings, enable at least one authentication mechanism, and do the
|
||||
configuration required for that authentication mechanism to work.
|
||||
See the section on "Authentication" below for more detail on
|
||||
configuring authentication mechanisms.
|
||||
|
||||
(5) Run
|
||||
```
|
||||
su zulip -c /home/zulip/deployments/current/scripts/setup/initialize-database
|
||||
```
|
||||
This will report an error if you did not fill in all the mandatory
|
||||
settings from `/etc/zulip/settings.py`. Once this completes
|
||||
successfully, the main installation process will be complete, and if
|
||||
you are planning on using password authentication, you should be able
|
||||
to visit the URL for your server and register for an account.
|
||||
|
||||
(6) Subscribe to [the Zulip announcements Google Group](https://groups.google.com/forum/#!forum/zulip-announce)
|
||||
to get announcements about new releases, security issues, etc.
|
||||
|
||||
|
||||
Authentication and logging into Zulip the first time
|
||||
====================================================
|
||||
|
||||
(As you read and follow the instructions in this section, if you run
|
||||
into trouble, check out the troubleshooting advice in the next major
|
||||
section.)
|
||||
|
||||
Once you've finished installing Zulip, configuring your settings.py
|
||||
file, and initializing the database, it's time to login to your new
|
||||
installation. By default, initialize-database creates 1 realm that
|
||||
you can join, the `ADMIN_DOMAIN` realm (defined in
|
||||
`/etc/zulip/settings.py`).
|
||||
|
||||
The `ADMIN_DOMAIN` realm is by default configured with the following settings:
|
||||
* `restricted_to_domain=True`: Only people with emails ending with @ADMIN_DOMAIN can join.
|
||||
* `invite_required=False`: An invitation is not required to join the realm.
|
||||
* `invite_by_admin_only=False`: You don't need to be an admin user to invite other users.
|
||||
* `mandatory_topics=False`: Users are not required to specify a topic when sending messages.
|
||||
|
||||
If you would like to change these settings, you can do so using the
|
||||
Django management python shell (as the zulip user):
|
||||
|
||||
```
|
||||
cd /home/zulip/deployments/current
|
||||
./manage.py shell
|
||||
from zerver.models import *
|
||||
r = get_realm(settings.ADMIN_DOMAIN)
|
||||
r.restricted_to_domain=False # Now anyone anywhere can login
|
||||
r.save() # save to the database
|
||||
```
|
||||
|
||||
If you realize you set `ADMIN_DOMAIN` wrong, in addition to fixing the
|
||||
value in settings.py, you will also want to do a similar manage.py
|
||||
process to set `r.domain = "newexample.com"`. If you've already
|
||||
changed `ADMIN_DOMAIN` in settings.py, you can use
|
||||
`Realm.objects.all()` in the management shell to find the list of
|
||||
realms and pass the domain of the realm that is not "zulip.com" to
|
||||
`get_realm`.
|
||||
|
||||
Depending what authentication backend you're planning to use, you will
|
||||
need to do some additional setup documented in the `settings.py` template:
|
||||
|
||||
* For Google authentication, you need to follow the configuration
|
||||
instructions around `GOOGLE_OAUTH2_CLIENT_ID` and `GOOGLE_CLIENT_ID`.
|
||||
* For Email authentication, you will need to follow the configuration
|
||||
instructions around outgoing SMTP from Django.
|
||||
|
||||
You should be able to login now. If you get an error, check
|
||||
`/var/log/zulip/errors.log` for a traceback, and consult the next
|
||||
section for advice on how to debug. If you aren't able to figure it
|
||||
out, email zulip-help@googlegroups.com with the traceback and we'll
|
||||
try to help you out!
|
||||
|
||||
You will likely want to make your own user account an admin user,
|
||||
which you can do via the following management command:
|
||||
|
||||
```
|
||||
./manage.py knight username@example.com -f
|
||||
```
|
||||
|
||||
Now that you are an administrator, you will have a special
|
||||
"Administration" tab linked to from the upper-right gear menu in the
|
||||
Zulip app that lets you deactivate other users, manage streams, change
|
||||
the Realm settings you may have edited using manage.py shell above,
|
||||
etc.
|
||||
|
||||
You can also use `manage.py knight` with the
|
||||
`--permission=api_super_user` argument to create API super users,
|
||||
which are needed to mirror messages to streams from other users for
|
||||
the IRC and Jabber mirroring integrations (see
|
||||
`bots/irc-mirror.py` and `bots/jabber_mirror.py` for some detail on these).
|
||||
|
||||
There are a large number of useful management commands under
|
||||
`zerver/manangement/commands/`; you can also see them listed using
|
||||
`./manage.py` with no arguments.
|
||||
|
||||
One such command worth highlighting because it's a valuable feature
|
||||
with no UI in the Administration page is `./manage.py realm_filters`,
|
||||
which allows you to configure certain patterns in messages to be
|
||||
automatically linkified, e.g. whenever someone mentions "T1234" it
|
||||
could be auto-linkified to ticket 1234 in your team's Trac instance.
|
||||
|
||||
Checking Zulip is healthy and debugging the services it depends on
|
||||
==================================================================
|
||||
|
||||
You can check if the zulip application is running using:
|
||||
```
|
||||
supervisorctl status
|
||||
```
|
||||
|
||||
And checking for errors in the Zulip errors logs under
|
||||
`/var/log/zulip/`. That contains one log file for each service, plus
|
||||
`errors.log` (has all errors), `server.log` (logs from the Django and
|
||||
Tornado servers), and `workers.log` (combined logs from the queue
|
||||
workers).
|
||||
|
||||
After you change configuration in `/etc/zulip/settings.py` or fix a
|
||||
misconfiguration, you will often want to restart the Zulip application.
|
||||
You can restart Zulip using:
|
||||
|
||||
```
|
||||
supervisorctl restart all
|
||||
```
|
||||
|
||||
Similarly, you can stop Zulip using:
|
||||
|
||||
```
|
||||
supervisorctl stop all
|
||||
```
|
||||
|
||||
The Zulip application uses several major services to store and cache
|
||||
data, queue messages, and otherwise support the Zulip application:
|
||||
|
||||
* postgresql
|
||||
* rabbitmq-server
|
||||
* nginx
|
||||
* redis
|
||||
* memcached
|
||||
|
||||
If one of these services is not installed or functioning correctly,
|
||||
Zulip will not work. Below we detail some common configuration
|
||||
problems and how to resolve them:
|
||||
|
||||
* An AMQPConnectionError traceback or error running rabbitmqctl
|
||||
usually means that RabbitMQ is not running; to fix this, try:
|
||||
```
|
||||
service rabbitmq-server restart
|
||||
```
|
||||
If RabbitMQ fails to start, the problem is often that you are using
|
||||
a virtual machine with broken DNS configuration; you can often
|
||||
correct this by configuring `/etc/hosts` properly.
|
||||
|
||||
* If your browser reports no webserver is running, that is likely
|
||||
because nginx is not configured properly and thus failed to start.
|
||||
nginx will fail to start if you configured SSL incorrectly or did
|
||||
not provide SSL certificates. To fix this, configure them properly
|
||||
and then run:
|
||||
```
|
||||
service nginx restart
|
||||
```
|
||||
|
||||
If you run into additional problems, [please report
|
||||
them](https://github.com/zulip/zulip/issues) so that we can update
|
||||
these lists! The Zulip installation scripts logs its full output to
|
||||
`/var/log/zulip/install.log`, so please include the context for any
|
||||
tracebacks from that log.
|
||||
|
||||
|
||||
Making your Zulip instance awesome
|
||||
==================================
|
||||
|
||||
Once you've got Zulip setup, you'll likely want to configure it the
|
||||
way you like. There are four big things to focus on:
|
||||
|
||||
(1) Integrations. We recommend setting up integrations for the major
|
||||
tools that your team works with. For example, if you're a software
|
||||
development team, you may want to start with integrations for your
|
||||
version control, issue tracker, CI system, and monitoring tools.
|
||||
|
||||
Spend time configuring these integrations to be how you like them --
|
||||
if an integration is spammy, you may want to change it to not send
|
||||
messages that nobody cares about (E.g. for the zulip.com trac
|
||||
integration, some teams find they only want notifications when new
|
||||
tickets are opened, commented on, or closed, and not every time
|
||||
someone edits the metadata).
|
||||
|
||||
If Zulip doesn't have an integration you want, you can add your own!
|
||||
Most integrations are very easy to write, and even more complex
|
||||
integrations usually take less than a day's work to build. We very
|
||||
much appreciate contributions of new integrations; there is a brief
|
||||
draft integration writing guide [here](https://github.com/zulip/zulip/issues/70).
|
||||
|
||||
|
||||
It can often be valuable to integrate your own internal processes to
|
||||
send notifications into Zulip; e.g. notifications of new customer
|
||||
signups, new error reports, or daily reports on the team's key
|
||||
metrics; this can often spawn discussions in response to the data.
|
||||
|
||||
(2) Streams and Topics. If it feels like a stream has too much
|
||||
traffic about a topic only of interest to some of the subscribers,
|
||||
consider adding or renaming streams until you feel like your team is
|
||||
working productively.
|
||||
|
||||
Second, most users are not used to topics. It can require a bit of
|
||||
time for everyone to get used to topics and start benefitting from
|
||||
them, but usually once a team is using them well, everyone ends up
|
||||
enthusiastic about how much topics make life easier. Some tips on
|
||||
using topics:
|
||||
|
||||
* When replying to an existing conversation thread, just click on the
|
||||
message, or navigate to it with the arrow keys and hit "r" or
|
||||
"enter" to reply on the same topic
|
||||
* When you start a new conversation topic, even if it's related to the
|
||||
previous conversation, type a new topic in the compose box
|
||||
* You can edit topics to fix a thread that's already been started,
|
||||
which can be helpful when onboarding new batches of users to the platform.
|
||||
|
||||
Third, setting default streams for new users is a great way to get
|
||||
new users involved in conversations before they've accustomed
|
||||
themselves with joining streams on their own. You can use the
|
||||
[`set_default_streams`](https://github.com/zulip/zulip/blob/master/zerver/management/commands/set_default_streams.py)
|
||||
command to set default streams for users within a realm:
|
||||
|
||||
```
|
||||
python manage.py set_default_streams --domain=example.com --streams=foo,bar,...
|
||||
```
|
||||
|
||||
(3) Notification settings. Zulip gives you a great deal of control
|
||||
over which messages trigger desktop notifications; you can configure
|
||||
these extensively in the `/#settings` page (get there from the gear
|
||||
menu). If you find the desktop notifications annoying, consider
|
||||
changing the settings to only trigger desktop notifications when you
|
||||
receive a PM or are @-mentioned.
|
||||
|
||||
(4) The mobile and desktop apps. Currently, the Zulip Desktop app
|
||||
only supports talking to servers with a properly signed SSL
|
||||
certificate, so you may find that you get a blank screen when you
|
||||
connect to a Zulip server using a self-signed certificate.
|
||||
|
||||
The Zulip iOS and Android apps in their respective stores don't yet
|
||||
support talking to non-zulip.com servers; the iOS app is waiting on
|
||||
Apple's app store review, while the Android app is waiting on someone
|
||||
to do the small project of adding a field to specify what Zulip server
|
||||
to talk to.
|
||||
|
||||
These issues will likely all be addressed in the coming weeks; make
|
||||
sure to join the zulip-announce@googlegroups.com list so that you can
|
||||
receive the announcements when these become available.
|
||||
|
||||
(5) All the other features: Hotkeys, emoji, search filters,
|
||||
@-mentions, etc. Zulip has lots of great features, make sure your
|
||||
team knows they exist and how to use them effectively.
|
||||
|
||||
(6) Enjoy your Zulip installation! If you discover things that you
|
||||
wish had been documented, please contribute documentation suggestions
|
||||
either via a GitHub issue or pull request; we love even small
|
||||
contributions, and we'd love to make the Zulip documentation cover
|
||||
everything anyone might want to know about running Zulip in
|
||||
production.
|
||||
|
||||
|
||||
Maintaining and upgrading Zulip in production
|
||||
=============================================
|
||||
|
||||
We recommend reading this entire section before doing your first
|
||||
upgrade.
|
||||
|
||||
* To upgrade to a new version of the zulip server, download the
|
||||
appropriate release tarball from
|
||||
https://www.zulip.com/dist/releases/ to a path readable by the zulip
|
||||
user (e.g. /home/zulip), and then run as root:
|
||||
```
|
||||
/home/zulip/deployments/current/scripts/upgrade-zulip zulip-server-VERSION.tar.gz
|
||||
```
|
||||
Be sure to download to a path readable by the Zulip user (see
|
||||
https://github.com/zulip/zulip/issues/208 for details on this
|
||||
issue) but then run the upgrade as root.
|
||||
|
||||
The upgrade process will shut down the service, run `apt-get
|
||||
upgrade`, a puppet apply, and any database migrations, and then
|
||||
bring the service back up. This will result in some brief downtime
|
||||
for the service, which should be under 30 seconds unless there is an
|
||||
expensive transition involved. Unless you have tested the upgrade
|
||||
in advance, we recommend doing upgrades at off hours.
|
||||
|
||||
You can create your own release tarballs from a copy of zulip.git
|
||||
repository using `tools/build-release-tarball`.
|
||||
|
||||
* **Warning**: If you have modified configuration files installed by
|
||||
Zulip (e.g. the nginx configuration), the Zulip upgrade process will
|
||||
overwrite your configuration when it does the `puppet apply`. You
|
||||
can test whether this will happen assuming no upstream changes to
|
||||
the configuration using `scripts/zulip-puppet-apply` (without the
|
||||
`-f` option), which will do a test puppet run and output and changes
|
||||
it would make. Using this list, you can save a copy of any files
|
||||
that you've modified, do the upgrade, and then restore your
|
||||
configuration. If you need to do this, please report the issue so
|
||||
that we can make the Zulip puppet configuration flexible enough to
|
||||
handle your setup.
|
||||
|
||||
* The Zulip upgrade script automatically logs output to
|
||||
/var/log/zulip/upgrade.log; please use those logs to include output
|
||||
that shows all errors in any bug reports.
|
||||
|
||||
* The Zulip upgrade process works by creating a new deployment under
|
||||
/home/zulip/deployments/ containing a complete copy of the Zulip
|
||||
server code, and then moving the symlinks at
|
||||
`/home/zulip/deployments/current` and /root/zulip` as part of the
|
||||
upgrade process. This means that if the new version isn't working,
|
||||
you can quickly downgrade to the old version by using
|
||||
`/home/zulip/deployments/<date>/scripts/restart-server` to return to
|
||||
a previous version that you've deployed (the version is specified
|
||||
via the path to the copy of `restart-server` you call).
|
||||
|
||||
* To update your settings, simply edit `/etc/zulip/settings.py` and then
|
||||
run `/home/zulip/deployments/current/scripts/restart-server` to
|
||||
restart the server
|
||||
|
||||
* You are responsible for running `apt-get upgrade` on your system on
|
||||
a regular basis to ensure that it is up to date with the latest
|
||||
security patches.
|
||||
|
||||
* To use the Zulip API with your Zulip server, you will need to use the
|
||||
API endpoint of e.g. `https://zulip.example.com/api`. Our Python
|
||||
API example scripts support this via the
|
||||
`--site=https://zulip.example.com` argument. The API bindings
|
||||
support it via putting `site=https://zulip.example.com` in your
|
||||
.zuliprc.
|
||||
|
||||
Every Zulip integration supports this sort of argument (or e.g. a
|
||||
`ZULIP_SITE` variable in a zuliprc file or the environment), but this
|
||||
is not yet documented for some of the integrations (the included
|
||||
integration documentation on `/integrations` will properly document
|
||||
how to do this for most integrations). Pull requests welcome to
|
||||
document this for those integrations that don't discuss this!
|
||||
|
||||
* Similarly, you will need to instruct your users to specify the URL
|
||||
for your Zulip server when using the Zulip desktop and mobile apps.
|
||||
|
||||
* As a measure to mitigate the impact of potential memory leaks in one
|
||||
of the Zulip daemons, the service automatically restarts itself
|
||||
every Sunday early morning. See `/etc/cron.d/restart-zulip` for the
|
||||
precise configuration.
|
||||
|
||||
|
||||
Remote User SSO Authentication
|
||||
==============================
|
||||
|
||||
Zulip supports integrating with a corporate Single-Sign-On solution.
|
||||
There are a few ways to do it, but this section documents how to
|
||||
configure Zulip to use an SSO solution that best supports Apache and
|
||||
will set the `REMOTE_USER` variable:
|
||||
|
||||
(0) Check that `/etc/zulip/settings.py` has
|
||||
`zproject.backends.ZulipRemoteUserBackend` as the only enabled value
|
||||
in the `AUTHENTICATION_BACKENDS` list, and that `SSO_APPEND_DOMAIN` is
|
||||
correct set depending on whether your SSO system uses email addresses
|
||||
or just usernames in `REMOTE_USER`.
|
||||
|
||||
Make sure that you've restarted the Zulip server since making this
|
||||
configuration change.
|
||||
|
||||
(1) Edit `/etc/zulip/zulip.conf` and change the `puppet_classes` line to read:
|
||||
|
||||
```
|
||||
puppet_classes = zulip::voyager, zulip::apache_sso
|
||||
```
|
||||
|
||||
(2) As root, run `/home/zulip/deployments/current/scripts/zulip-puppet-apply`
|
||||
to install our SSO integration.
|
||||
|
||||
(3) To configure our SSO integration, edit
|
||||
`/etc/apache2/sites-available/zulip-sso.example` and fill in the
|
||||
configuration required for your SSO service to set `REMOTE_USER` and
|
||||
place your completed configuration file at `/etc/apache2/sites-available/zulip-sso`
|
||||
|
||||
(4) Run `a2ensite zulip-sso` to enable the Apache integration site.
|
||||
|
||||
Now you should be able to visit `https://zulip.example.com/` and
|
||||
login via the SSO solution.
|
||||
|
||||
|
||||
### Troubleshooting Remote User SSO
|
||||
|
||||
This system is a little finicky to networking setup (e.g. common
|
||||
issues have to do with /etc/hosts not mapping settings.EXTERNAL_HOST
|
||||
to the Apache listening on 127.0.0.1/localhost, for example). It can
|
||||
often help while debugging to temporarily change the Apache config in
|
||||
/etc/apache2/sites-available/zulip-sso to listen on all interfaces
|
||||
rather than just 127.0.0.1 as you debug this. It can also be helpful
|
||||
to change /etc/nginx/zulip-include/app.d/external-sso.conf to
|
||||
proxy_pass to a more explicit URL possibly not over HTTPS when
|
||||
debugging. The following log files can be helpful when debugging this
|
||||
setup:
|
||||
|
||||
* /var/log/zulip/{errors.log,server.log} (the usual places)
|
||||
* /var/log/nginx/access.log (nginx access logs)
|
||||
* /var/log/apache2/zulip_auth_access.log (you may want to change
|
||||
LogLevel to "debug" in the apache config file to make this more
|
||||
verbose)
|
||||
|
||||
Here's a summary of how the remote user SSO system works assuming
|
||||
you're using HTTP basic auth; this summary should help with
|
||||
understanding what's going on as you try to debug:
|
||||
|
||||
* Since you've configured /etc/zulip/settings.py to only define the
|
||||
zproject.backends.ZulipRemoteUserBackend, zproject/settings.py
|
||||
configures /accounts/login/sso as HOME_NOT_LOGGED_IN, which makes
|
||||
`https://zulip.example.com/` aka the homepage for the main Zulip
|
||||
Django app running behind nginx redirect to /accounts/login/sso if
|
||||
you're not logged in.
|
||||
|
||||
* nginx proxies requests to /accounts/login/sso/ to an Apache instance
|
||||
listening on localhost:8888 apache via the config in
|
||||
/etc/nginx/zulip-include/app.d/external-sso.conf (using the upstream
|
||||
localhost:8888 defined in /etc/nginx/zulip-include/upstreams).
|
||||
|
||||
* The Apache zulip-sso site which you've enabled listens on
|
||||
localhost:8888 and presents the htpasswd dialogue; you provide
|
||||
correct login information and the request reaches a second Zulip
|
||||
Django app instance that is running behind Apache with with
|
||||
REMOTE_USER set. That request is served by
|
||||
`zerver.views.remote_user_sso`, which just checks the REMOTE_USER
|
||||
variable and either logs in (sets a cookie) or registers the new
|
||||
user (depending whether they have an account).
|
||||
|
||||
* After succeeding, that redirects the user back to / on port 443
|
||||
(hosted by nginx); the main Zulip Django app sees the cookie and
|
||||
proceeds to load the site homepage with them logged in (just as if
|
||||
they'd logged in normally via username/password).
|
||||
|
||||
Again, most issues with this setup tend to be subtle issues with the
|
||||
hostname/DNS side of the configuration. Suggestions for how to
|
||||
improve this SSO setup documentation are very welcome!
|
||||
570
THIRDPARTY
570
THIRDPARTY
@@ -1,570 +0,0 @@
|
||||
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: Zulip
|
||||
Upstream-Contact: Zulip Development Discussion <zulip-devel@googlegroups.com>
|
||||
Source: https://zulip.org/
|
||||
Comment:
|
||||
Unless otherwise noted, the Zulip software is distributed under the Apache
|
||||
License, Version 2.0. The software includes some works released by third
|
||||
parties under other free and open source licenses. Those works are
|
||||
redistributed under the license terms under which the works were received.
|
||||
.
|
||||
While Dropbox has sought to provide complete and accurate licensing
|
||||
information for each FOSS package, Dropbox does not represent or warrant
|
||||
that the licensing information provided herein is correct or error-free.
|
||||
Recipients of the Zulip software should investigate the identified FOSS
|
||||
packages to confirm the accuracy of the licensing information provided.
|
||||
Recipients are also encouraged to notify Dropbox of any inaccurate
|
||||
information or errors found in these notices.
|
||||
|
||||
Files: *
|
||||
Copyright: 2011-2015 Dropbox, Inc.
|
||||
License: Apache-2
|
||||
|
||||
Files: api/*
|
||||
Copyright: 2012-2014 Dropbox, Inc
|
||||
License: Expat
|
||||
|
||||
Files: api/integrations/perforce/git_p4.py
|
||||
Copyright: 2007 Simon Hausmann <simon@lst.de>,
|
||||
2007 Trolltech ASA
|
||||
License: Expat
|
||||
Comment: https://raw.github.com/git/git/34022ba/git-p4.py
|
||||
|
||||
Files: bots/jabber_mirror_backend.py
|
||||
Copyright: 2013 Permabit, Inc., 2013-2014 Dropbox, Inc.
|
||||
License: Expat
|
||||
|
||||
Files: confirmation/*
|
||||
Copyright: 2008, Jarek Zgoda <jarek.zgoda@gmail.com>
|
||||
License: BSD-3-Clause
|
||||
|
||||
Files: puppet/apt/*
|
||||
Copyright: 2011, Evolving Web Inc.
|
||||
License: Expat
|
||||
|
||||
Files: puppet/common/*
|
||||
Copyright: 2007, David Schmitt
|
||||
License: BSD-3-Clause
|
||||
Comment: https://github.com/DavidS/puppet-common
|
||||
Distribution includes a file `lib/puppet/parser/functions/ip_to_cron.rb` which
|
||||
we removed due to unclear license
|
||||
|
||||
Files: puppet/stdlib/*
|
||||
Copyright: 2011, Krzysztof Wilczynski
|
||||
2011, Puppet Labs Inc
|
||||
License: Apache-2.0
|
||||
|
||||
File: puppet/zulip_internal/files/mediawiki/Auth_remoteuser.php
|
||||
Copyright: 2006 Otheus Shelling
|
||||
2007 Rusty Burchfield
|
||||
2009 James Kinsman
|
||||
2010 Daniel Thomas
|
||||
2010 Ian Ward Comfort
|
||||
License: GPL-2.0
|
||||
Comment: Not linked.
|
||||
|
||||
Files: puppet/zulip_internal/files/nagios_plugins/check_debian_packages
|
||||
Copyright: 2005 Francesc Guasch
|
||||
License: GPL-2.0
|
||||
Comment: Not linked.
|
||||
|
||||
Files: puppet/zulip_internal/files/nagios_plugins/check_postgres.pl
|
||||
Copyright: 2007-2015 Greg Sabino Mullane
|
||||
License: BSD-2-Clause
|
||||
|
||||
Files: puppet/zulip_internal/files/nagios_plugins/check_website_response.sh
|
||||
Copyright: 2011 Chris Freeman
|
||||
License: GPL-2.0
|
||||
|
||||
Files: puppet/zulip_internal/files/trac/cgi-bin/
|
||||
Copyright: 2003-2009 Edgewall Software
|
||||
2003-2004 Jonas Borgström <jonas@edgewall.com>
|
||||
License: BSD-3-Clause
|
||||
|
||||
Files: puppet/zulip_internal/files/pagerduty_nagios.pl
|
||||
Copyright: 2011, PagerDuty, Inc.
|
||||
License: BSD-3-Clause
|
||||
|
||||
Files: puppet/zulip_internal/files/zulip-ec2-configure-interfaces
|
||||
Copyright: 2013, Dropbox, Inc.
|
||||
License: Expat
|
||||
|
||||
Files: static/audio/zulip.*
|
||||
Copyright: 2011 Vidsyn
|
||||
License: CC-0-1.0
|
||||
|
||||
Files: static/styles/thirdparty-fonts.css
|
||||
Copyright: 2012-2013 Dave Gandy
|
||||
License: Expat
|
||||
|
||||
Files: static/third/fontawesome/*
|
||||
Copyright: 2012-2013 Dave Gandy
|
||||
License: SIL-OFL-1.1
|
||||
|
||||
Files: static/third/bootstrap/bootstrap-btn.css
|
||||
Copyright: 2011-2014 Twitter, Inc
|
||||
License: Expat
|
||||
|
||||
Files: static/third/bootstrap/css/bootstrap-responsive.css static/third/bootstrap/css/bootstrap.css
|
||||
Copyright: 2012 Twitter, Inc
|
||||
License: Apache-2.0
|
||||
Comment: The software has been modified.
|
||||
|
||||
Files: static/third/bootstrap/js/bootstrap.js
|
||||
Copyright: 2012 Twitter, Inc
|
||||
License: Apache-2.0
|
||||
Comment: The software has been modified.
|
||||
|
||||
Files: static/third/bootstrap-notify/*
|
||||
Copyright: 2013 Nijiko Yonskai
|
||||
2012 Goodybag, Inc.
|
||||
2012 Twitter, Inc
|
||||
License: Apache-2.0
|
||||
Comment: The software has been modified.
|
||||
|
||||
Files: static/third/gemoji/images/emoji/unicode/* tools/emoji_dump/*.ttf
|
||||
Copyright: Google, Inc.
|
||||
License: Apache-2.0
|
||||
Comment: These are actually Noto Emoji, not gemoji.
|
||||
|
||||
Files: static/third/html5-formdata/formdata.js
|
||||
Copyright: 2010 François de Metz
|
||||
License: Expat
|
||||
Comment: See https://github.com/francois2metz/html5-formdata
|
||||
|
||||
Files: src/zulip/static/third/jquery/*
|
||||
Copyright: 2011, John Resig
|
||||
2011, The Dojo Foundation
|
||||
License: Expat
|
||||
|
||||
Files: static/third/jquery-autosize/jquery.autosize.js
|
||||
Copyright: 2013 Jack Moore
|
||||
License: Expat
|
||||
|
||||
Files: static/third/jquery-caret/*
|
||||
Copyright: 2010 C.F., Wong
|
||||
License: Expat
|
||||
|
||||
Files: static/third/jquery-filedrop/jquery.filedrop.js
|
||||
Copyright: Resopollution
|
||||
License: Expat
|
||||
|
||||
Files: static/third/jquery-form/jquery.form.js
|
||||
Copyright: M. Alsup
|
||||
License: Expat or GPL-2.0
|
||||
|
||||
Files: static/third/jquery-idle/jquery.idle.js
|
||||
Copyright: 2011-2013 Henrique Boaventura
|
||||
License: Expat
|
||||
Comment: The software has been modified.
|
||||
|
||||
Files: static/third/jquery-mousewheel/jquery.mousewheel.js
|
||||
Copyright: 2011 Brandon Aaron
|
||||
License: Expat
|
||||
|
||||
Files: static/third/jquery-perfect-scrollbar/*
|
||||
Copyright: 2012 HyeonJe Jun
|
||||
License: Expat
|
||||
|
||||
Files: static/third/jquery-throttle-debounce/*
|
||||
Copyright: 2010 "Cowboy" Ben Alman
|
||||
License: Expat or GPL
|
||||
|
||||
Files: static/third/jquery-validate/*
|
||||
Copyright: 2006 - 2011 Jörn Zaefferer
|
||||
License: Expat
|
||||
|
||||
Files: src/zulip/static/third/lazyload/*
|
||||
Copyright: 2011 Ryan Grove
|
||||
License: Expat
|
||||
|
||||
Files: static/third/marked/*
|
||||
Copyright: 2011-2013, Christopher Jeffrey
|
||||
License: Expat
|
||||
|
||||
Files: static/third/sockjs/sockjs-0.3.4.js
|
||||
Copyright: 2011-2012 VMware, Inc.
|
||||
2012 Douglas Crockford
|
||||
License: Expat and public-domain
|
||||
|
||||
Files: static/third/sorttable/sorttable.js
|
||||
Copyright: 2007 Stuart Langridge
|
||||
License: X11
|
||||
|
||||
Files: static/third/sourcesans/*
|
||||
Copyright: 2010, 2012, 2014 Adobe Systems Incorporated
|
||||
License: SIL-OFL-1.1
|
||||
|
||||
Files: static/third/spectrum/*
|
||||
Copyright: 2013 Brian Grinstead
|
||||
License: Expat
|
||||
|
||||
Files: static/third/spin/spin.js
|
||||
Copyright: 2011-2013 Felix Gnass
|
||||
License: Expat
|
||||
|
||||
Files: static/third/underscore/underscore.js
|
||||
Copyright: 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
|
||||
License: Expat
|
||||
Comment: https://github.com/jashkenas/underscore/blob/master/LICENSE
|
||||
|
||||
Files: static/third/winchan/*
|
||||
Copyright: 2012 Lloyd Hilaiel
|
||||
License: Expat
|
||||
Comment: https://github.com/mozilla/winchan
|
||||
|
||||
Files: static/third/xdate/*
|
||||
Copyright: 2010 C. F., Wong
|
||||
License: Expat
|
||||
|
||||
Files: static/third/zocial/*
|
||||
Copyright: Sam Collins
|
||||
License: Expat
|
||||
|
||||
Files: tools/inject-messages/othello
|
||||
Copyright: Shakespeare
|
||||
License: public-domain
|
||||
|
||||
Files: tools/jslint/jslint.js
|
||||
Copyright: 2002 Douglas Crockford
|
||||
License: XXX-good-not-evil
|
||||
|
||||
Files: tools/python-proxy
|
||||
Copyright: 2009 F.bio Domingues
|
||||
License: Expat
|
||||
|
||||
Files: tools/review
|
||||
Copyright: 2010 Ksplice, Inc.
|
||||
License: Apache-2.0
|
||||
|
||||
Files: zerver/lib/bugdown/codehilite.py zerver/lib/bugdown/fenced_code.py
|
||||
Copyright: 2006-2008 Waylan Limberg
|
||||
License: BSD-3-Clause
|
||||
Comment: https://pypi.python.org/pypi/Markdown
|
||||
|
||||
Files: zerver/lib/ccache.py
|
||||
Copyright: 2013 David Benjamin and Alan Huang
|
||||
License: Expat
|
||||
|
||||
Files: frontend_tests/casperjs/*
|
||||
Copyright: 2011-2012 Nicolas Perriault
|
||||
Joyent, Inc. and other Node contributors
|
||||
License: Expat
|
||||
|
||||
Files: frontend_tests/casperjs/modules/vendors/*
|
||||
Copyright: 2011, Jeremy Ashkenas
|
||||
License: Expat
|
||||
|
||||
|
||||
License: Apache-2.0
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
.
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
.
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
.
|
||||
On Debian systems, the full text of the Apache License version 2 can
|
||||
be found in /usr/share/common-licenses/Apache-2.0.
|
||||
|
||||
License: BSD-2-clause
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice(s), this list of conditions and the following disclaimer
|
||||
unmodified other than the allowable addition of one or more
|
||||
copyright notices.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice(s), this list of conditions and the following disclaimer in
|
||||
the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
.
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) ``AS IS'' AND ANY
|
||||
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S) BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
||||
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
||||
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
License: BSD-3-Clause
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
.
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
.
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
.
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
||||
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
|
||||
OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
||||
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
||||
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
License: CC-0-1.0
|
||||
Creative Commons CC0 1.0 Universal
|
||||
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
||||
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
||||
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION
|
||||
ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE
|
||||
USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND
|
||||
DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT
|
||||
OR THE INFORMATION OR WORKS PROVIDED HEREUNDER.
|
||||
.
|
||||
Statement of Purpose
|
||||
.
|
||||
The laws of most jurisdictions throughout the world automatically confer
|
||||
exclusive Copyright and Related Rights (defined below) upon the creator
|
||||
and subsequent owner(s) (each and all, an "owner") of an original work
|
||||
of authorship and/or a database (each, a "Work").
|
||||
.
|
||||
Certain owners wish to permanently relinquish those rights to a Work for
|
||||
the purpose of contributing to a commons of creative, cultural and
|
||||
scientific works ("Commons") that the public can reliably and without
|
||||
fear of later claims of infringement build upon, modify, incorporate in
|
||||
other works, reuse and redistribute as freely as possible in any form
|
||||
whatsoever and for any purposes, including without limitation commercial
|
||||
purposes. These owners may contribute to the Commons to promote the
|
||||
ideal of a free culture and the further production of creative, cultural
|
||||
and scientific works, or to gain reputation or greater distribution for
|
||||
their Work in part through the use and efforts of others.
|
||||
.
|
||||
For these and/or other purposes and motivations, and without any
|
||||
expectation of additional consideration or compensation, the person
|
||||
associating CC0 with a Work (the "Affirmer"), to the extent that he or
|
||||
she is an owner of Copyright and Related Rights in the Work, voluntarily
|
||||
elects to apply CC0 to the Work and publicly distribute the Work under
|
||||
its terms, with knowledge of his or her Copyright and Related Rights in
|
||||
the Work and the meaning and intended legal effect of CC0 on those
|
||||
rights.
|
||||
.
|
||||
1. Copyright and Related Rights. A Work made available under CC0 may be
|
||||
protected by copyright and related or neighboring rights ("Copyright and
|
||||
Related Rights"). Copyright and Related Rights include, but are not
|
||||
limited to, the following:
|
||||
.
|
||||
i. the right to reproduce, adapt, distribute, perform, display,
|
||||
communicate, and translate a Work;
|
||||
.
|
||||
ii. moral rights retained by the original author(s) and/or performer(s);
|
||||
.
|
||||
iii. publicity and privacy rights pertaining to a person's image or
|
||||
likeness depicted in a Work;
|
||||
.
|
||||
iv. rights protecting against unfair competition in regards to a Work,
|
||||
subject to the limitations in paragraph 4(a), below;
|
||||
.
|
||||
v. rights protecting the extraction, dissemination, use and reuse of
|
||||
data in a Work;
|
||||
.
|
||||
vi. database rights (such as those arising under Directive 96/9/EC of
|
||||
the European Parliament and of the Council of 11 March 1996 on the legal
|
||||
protection of databases, and under any national implementation thereof,
|
||||
including any amended or successor version of such directive); and
|
||||
.
|
||||
vii. other similar, equivalent or corresponding rights throughout the
|
||||
world based on applicable law or treaty, and any national
|
||||
implementations thereof.
|
||||
.
|
||||
2. Waiver. To the greatest extent permitted by, but not in contravention
|
||||
of, applicable law, Affirmer hereby overtly, fully, permanently,
|
||||
irrevocably and unconditionally waives, abandons, and surrenders all of
|
||||
Affirmer's Copyright and Related Rights and associated claims and causes
|
||||
of action, whether now known or unknown (including existing as well as
|
||||
future claims and causes of action), in the Work (i) in all territories
|
||||
worldwide, (ii) for the maximum duration provided by applicable law or
|
||||
treaty (including future time extensions), (iii) in any current or
|
||||
future medium and for any number of copies, and (iv) for any purpose
|
||||
whatsoever, including without limitation commercial, advertising or
|
||||
promotional purposes (the "Waiver"). Affirmer makes the Waiver for the
|
||||
benefit of each member of the public at large and to the detriment of
|
||||
Affirmer's heirs and successors, fully intending that such Waiver shall
|
||||
not be subject to revocation, rescission, cancellation, termination, or
|
||||
any other legal or equitable action to disrupt the quiet enjoyment of
|
||||
the Work by the public as contemplated by Affirmer's express Statement
|
||||
of Purpose.
|
||||
.
|
||||
3. Public License Fallback. Should any part of the Waiver for any reason
|
||||
be judged legally invalid or ineffective under applicable law, then the
|
||||
Waiver shall be preserved to the maximum extent permitted taking into
|
||||
account Affirmer's express Statement of Purpose. In addition, to the
|
||||
extent the Waiver is so judged Affirmer hereby grants to each affected
|
||||
person a royalty-free, non transferable, non sublicensable, non
|
||||
exclusive, irrevocable and unconditional license to exercise Affirmer's
|
||||
Copyright and Related Rights in the Work (i) in all territories
|
||||
worldwide, (ii) for the maximum duration provided by applicable law or
|
||||
treaty (including future time extensions), (iii) in any current or
|
||||
future medium and for any number of copies, and (iv) for any purpose
|
||||
whatsoever, including without limitation commercial, advertising or
|
||||
promotional purposes (the "License"). The License shall be deemed
|
||||
effective as of the date CC0 was applied by Affirmer to the Work. Should
|
||||
any part of the License for any reason be judged legally invalid or
|
||||
ineffective under applicable law, such partial invalidity or
|
||||
ineffectiveness shall not invalidate the remainder of the License, and
|
||||
in such case Affirmer hereby affirms that he or she will not (i)
|
||||
exercise any of his or her remaining Copyright and Related Rights in the
|
||||
Work or (ii) assert any associated claims and causes of action with
|
||||
respect to the Work, in either case contrary to Affirmer's express
|
||||
Statement of Purpose.
|
||||
.
|
||||
4. Limitations and Disclaimers.
|
||||
.
|
||||
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
||||
surrendered, licensed or otherwise affected by this document.
|
||||
.
|
||||
b. Affirmer offers the Work as-is and makes no representations or
|
||||
warranties of any kind concerning the Work, express, implied, statutory
|
||||
or otherwise, including without limitation warranties of title,
|
||||
merchantability, fitness for a particular purpose, non infringement, or
|
||||
the absence of latent or other defects, accuracy, or the present or
|
||||
absence of errors, whether or not discoverable, all to the greatest
|
||||
extent permissible under applicable law.
|
||||
.
|
||||
c. Affirmer disclaims responsibility for clearing rights of other
|
||||
persons that may apply to the Work or any use thereof, including without
|
||||
limitation any person's Copyright and Related Rights in the Work.
|
||||
Further, Affirmer disclaims responsibility for obtaining any necessary
|
||||
consents, permissions or other rights required for any use of the Work.
|
||||
.
|
||||
d. Affirmer understands and acknowledges that Creative Commons is not a
|
||||
party to this document and has no duty or obligation with respect to
|
||||
this CC0 or use of the Work.
|
||||
|
||||
License: Expat
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
.
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
License: GPL-2.0
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; version 2, dated June, 1991.
|
||||
.
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
.
|
||||
On Debian systems, the complete text of the GNU General Public License
|
||||
can be found in /usr/share/common-licenses/GPL-2 file.
|
||||
|
||||
License: SIL-OFL-1.1
|
||||
---------------------------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
---------------------------------------------------------------------------
|
||||
.
|
||||
PREAMBLE
|
||||
.
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide development
|
||||
of collaborative font projects, to support the font creation efforts of academic
|
||||
and linguistic communities, and to provide a free and open framework in which
|
||||
fonts may be shared and improved in partnership with others.
|
||||
.
|
||||
The OFL allows the licensed fonts to be used, studied, modified and redistributed
|
||||
freely as long as they are not sold by themselves. The fonts, including any
|
||||
derivative works, can be bundled, embedded, redistributed and/or sold with any
|
||||
software provided that any reserved names are not used by derivative works. The
|
||||
fonts and derivatives, however, cannot be released under any other type of license.
|
||||
The requirement for fonts to remain under this license does not apply to any
|
||||
document created using the fonts or their derivatives.
|
||||
.
|
||||
DEFINITIONS
|
||||
.
|
||||
"Font Software" refers to the set of files released by the Copyright Holder(s) under
|
||||
this license and clearly marked as such. This may include source files, build
|
||||
scripts and documentation.
|
||||
.
|
||||
"Reserved Font Name" refers to any names specified as such after the copyright
|
||||
statement(s).
|
||||
.
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
.
|
||||
"Modified Version" refers to any derivative made by adding to, deleting, or
|
||||
substituting -- in part or in whole -- any of the components of the Original Version,
|
||||
by changing formats or by porting the Font Software to a new environment.
|
||||
.
|
||||
"Author" refers to any designer, engineer, programmer, technical writer or other
|
||||
person who contributed to the Font Software.
|
||||
.
|
||||
PERMISSION & CONDITIONS
|
||||
.
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of the
|
||||
Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell
|
||||
modified and unmodified copies of the Font Software, subject to the following
|
||||
conditions:
|
||||
.
|
||||
1) Neither the Font Software nor any of its individual components, in Original or
|
||||
Modified Versions, may be sold by itself.
|
||||
.
|
||||
2) Original or Modified Versions of the Font Software may be bundled, redistributed
|
||||
and/or sold with any software, provided that each copy contains the above copyright
|
||||
notice and this license. These can be included either as stand-alone text files,
|
||||
human-readable headers or in the appropriate machine-readable metadata fields within
|
||||
text or binary files as long as those fields can be easily viewed by the user.
|
||||
.
|
||||
3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless
|
||||
explicit written permission is granted by the corresponding Copyright Holder. This
|
||||
restriction only applies to the primary font name as presented to the users.
|
||||
.
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall
|
||||
not be used to promote, endorse or advertise any Modified Version, except to
|
||||
acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with
|
||||
their explicit written permission.
|
||||
.
|
||||
5) The Font Software, modified or unmodified, in part or in whole, must be distributed
|
||||
entirely under this license, and must not be distributed under any other license. The
|
||||
requirement for fonts to remain under this license does not apply to any document
|
||||
created using the Font Software.
|
||||
.
|
||||
TERMINATION
|
||||
.
|
||||
This license becomes null and void if any of the above conditions are not met.
|
||||
.
|
||||
DISCLAIMER
|
||||
.
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER
|
||||
RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR
|
||||
INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
34
Vagrantfile
vendored
34
Vagrantfile
vendored
@@ -1,34 +0,0 @@
|
||||
# -*- mode: ruby -*-
|
||||
|
||||
VAGRANTFILE_API_VERSION = "2"
|
||||
|
||||
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
||||
|
||||
# For LXC. VirtualBox hosts use a different box, described below.
|
||||
config.vm.box = "fgrehm/trusty64-lxc"
|
||||
|
||||
# The Zulip development environment runs on 9991 on the guest.
|
||||
config.vm.network "forwarded_port", guest: 9991, host: 9991, host_ip: "127.0.0.1"
|
||||
|
||||
config.vm.synced_folder ".", "/vagrant", disabled: true
|
||||
config.vm.synced_folder ".", "/srv/zulip"
|
||||
|
||||
config.vm.provider "virtualbox" do |vb, override|
|
||||
override.vm.box = "ubuntu/trusty64"
|
||||
# 2GiB seemed reasonable here. The VM OOMs with only 1024MiB.
|
||||
vb.memory = 2048
|
||||
end
|
||||
|
||||
$provision_script = <<SCRIPT
|
||||
set -x
|
||||
set -e
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y python-pbs
|
||||
python /srv/zulip/provision.py
|
||||
SCRIPT
|
||||
|
||||
config.vm.provision "shell",
|
||||
# We want provision.py to be run with the permissions of the vagrant user.
|
||||
privileged: false,
|
||||
inline: $provision_script
|
||||
end
|
||||
@@ -1,5 +1,4 @@
|
||||
from __future__ import absolute_import
|
||||
from __future__ import print_function
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from __future__ import absolute_import
|
||||
from __future__ import print_function
|
||||
|
||||
import datetime
|
||||
import pytz
|
||||
@@ -20,6 +19,6 @@ class Command(BaseCommand):
|
||||
date = datetime.datetime.now() - datetime.timedelta(days=1)
|
||||
else:
|
||||
date = datetime.datetime.strptime(options["date"], "%Y-%m-%d")
|
||||
print("Activity data for", date)
|
||||
print(activity_averages_during_day(date))
|
||||
print("Please note that the total registered user count is a total for today")
|
||||
print "Activity data for", date
|
||||
print activity_averages_during_day(date)
|
||||
print "Please note that the total registered user count is a total for today"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from __future__ import absolute_import
|
||||
from __future__ import print_function
|
||||
|
||||
from optparse import make_option
|
||||
from django.core.management.base import BaseCommand
|
||||
@@ -64,7 +63,7 @@ def compute_stats(log_level):
|
||||
logging.info("Top %6s | %s%%" % (size, round(top_percents[size], 1)))
|
||||
|
||||
grand_total = sum(total_counts.values())
|
||||
print(grand_total)
|
||||
print grand_total
|
||||
logging.info("%15s | %s" % ("Client", "Percentage"))
|
||||
for client in total_counts.keys():
|
||||
logging.info("%15s | %s%%" % (client, round(100. * total_counts[client] / grand_total, 1)))
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from __future__ import absolute_import
|
||||
from __future__ import print_function
|
||||
|
||||
from zerver.lib.statistics import seconds_usage_between
|
||||
|
||||
@@ -17,7 +16,7 @@ def analyze_activity(options):
|
||||
if options["realm"]:
|
||||
user_profile_query = user_profile_query.filter(realm__domain=options["realm"])
|
||||
|
||||
print("Per-user online duration:\n")
|
||||
print "Per-user online duration:\n"
|
||||
total_duration = datetime.timedelta(0)
|
||||
for user_profile in user_profile_query:
|
||||
duration = seconds_usage_between(user_profile, day_start, day_end)
|
||||
@@ -26,11 +25,11 @@ def analyze_activity(options):
|
||||
continue
|
||||
|
||||
total_duration += duration
|
||||
print("%-*s%s" % (37, user_profile.email, duration, ))
|
||||
print "%-*s%s" % (37, user_profile.email, duration, )
|
||||
|
||||
print("\nTotal Duration: %s" % (total_duration,))
|
||||
print("\nTotal Duration in minutes: %s" % (total_duration.total_seconds() / 60.,))
|
||||
print("Total Duration amortized to a month: %s" % (total_duration.total_seconds() * 30. / 60.,))
|
||||
print "\nTotal Duration: %s" % (total_duration,)
|
||||
print "\nTotal Duration in minutes: %s" % (total_duration.total_seconds() / 60.,)
|
||||
print "Total Duration amortized to a month: %s" % (total_duration.total_seconds() * 30. / 60.,)
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = """Report analytics of user activity on a per-user and realm basis.
|
||||
@@ -43,7 +42,7 @@ It will correctly not count server-initiated reloads in the activity statistics.
|
||||
|
||||
The duration flag can be used to control how many days to show usage duration for
|
||||
|
||||
Usage: python2.7 manage.py analyze_user_activity [--realm=zulip.com] [--date=2013-09-10] [--duration=1]
|
||||
Usage: python manage.py analyze_user_activity [--realm=zulip.com] [--date=2013-09-10] [--duration=1]
|
||||
|
||||
By default, if no date is selected 2013-09-10 is used. If no realm is provided, information
|
||||
is shown for all realms"""
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from __future__ import absolute_import
|
||||
from __future__ import print_function
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db.models import Count
|
||||
@@ -14,13 +13,9 @@ class Command(BaseCommand):
|
||||
|
||||
Usage examples:
|
||||
|
||||
python2.7 manage.py client_activity
|
||||
python2.7 manage.py client_activity zulip.com
|
||||
python2.7 manage.py client_activity jesstess@zulip.com"""
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('arg', metavar='<arg>', type=str, nargs='?', default=None,
|
||||
help="realm or user to estimate client activity for")
|
||||
python manage.py client_activity
|
||||
python manage.py client_activity zulip.com
|
||||
python manage.py client_activity jesstess@zulip.com"""
|
||||
|
||||
def compute_activity(self, user_activity_objects):
|
||||
# Report data from the past week.
|
||||
@@ -49,27 +44,26 @@ python2.7 manage.py client_activity jesstess@zulip.com"""
|
||||
counts.sort()
|
||||
|
||||
for count in counts:
|
||||
print("%25s %15d" % (count[1], count[0]))
|
||||
print("Total:", total)
|
||||
print "%25s %15d" % (count[1], count[0])
|
||||
print "Total:", total
|
||||
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if options['arg'] is None:
|
||||
if len(args) == 0:
|
||||
# Report global activity.
|
||||
self.compute_activity(UserActivity.objects.all())
|
||||
else:
|
||||
arg = options['arg']
|
||||
elif len(args) == 1:
|
||||
try:
|
||||
# Report activity for a user.
|
||||
user_profile = get_user_profile_by_email(arg)
|
||||
user_profile = get_user_profile_by_email(args[0])
|
||||
self.compute_activity(UserActivity.objects.filter(
|
||||
user_profile=user_profile))
|
||||
except UserProfile.DoesNotExist:
|
||||
try:
|
||||
# Report activity for a realm.
|
||||
realm = get_realm(arg)
|
||||
realm = get_realm(args[0])
|
||||
self.compute_activity(UserActivity.objects.filter(
|
||||
user_profile__realm=realm))
|
||||
except Realm.DoesNotExist:
|
||||
print("Unknown user or domain %s" % (arg,))
|
||||
print "Unknown user or domain %s" % (args[0],)
|
||||
exit(1)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from __future__ import absolute_import
|
||||
from __future__ import print_function
|
||||
|
||||
import datetime
|
||||
import pytz
|
||||
@@ -7,7 +6,7 @@ import pytz
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db.models import Count
|
||||
from zerver.models import UserProfile, Realm, Stream, Message, Recipient, UserActivity, \
|
||||
Subscription, UserMessage, get_realm
|
||||
Subscription, UserMessage
|
||||
|
||||
MOBILE_CLIENT_LIST = ["Android", "ios"]
|
||||
HUMAN_CLIENT_LIST = MOBILE_CLIENT_LIST + ["website"]
|
||||
@@ -17,10 +16,6 @@ human_messages = Message.objects.filter(sending_client__name__in=HUMAN_CLIENT_LI
|
||||
class Command(BaseCommand):
|
||||
help = "Generate statistics on realm activity."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('realms', metavar='<realm>', type=str, nargs='*',
|
||||
help="realm to generate statistics for")
|
||||
|
||||
def active_users(self, realm):
|
||||
# Has been active (on the website, for now) in the last 7 days.
|
||||
activity_cutoff = datetime.datetime.now(tz=pytz.utc) - datetime.timedelta(days=7)
|
||||
@@ -66,51 +61,52 @@ class Command(BaseCommand):
|
||||
fraction = 0.0
|
||||
else:
|
||||
fraction = numerator / float(denominator)
|
||||
print("%.2f%% of" % (fraction * 100,), text)
|
||||
print "%.2f%% of" % (fraction * 100,), text
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if options['realms']:
|
||||
if args:
|
||||
try:
|
||||
realms = [get_realm(domain) for domain in options['realms']]
|
||||
except Realm.DoesNotExist as e:
|
||||
print(e)
|
||||
realms = [Realm.objects.get(domain=domain) for domain in args]
|
||||
except Realm.DoesNotExist, e:
|
||||
print e
|
||||
exit(1)
|
||||
else:
|
||||
realms = Realm.objects.all()
|
||||
|
||||
for realm in realms:
|
||||
print(realm.domain)
|
||||
print realm.domain
|
||||
|
||||
user_profiles = UserProfile.objects.filter(realm=realm, is_active=True)
|
||||
active_users = self.active_users(realm)
|
||||
num_active = len(active_users)
|
||||
|
||||
print("%d active users (%d total)" % (num_active, len(user_profiles)))
|
||||
print "%d active users (%d total)" % (num_active, len(user_profiles))
|
||||
streams = Stream.objects.filter(realm=realm).extra(
|
||||
tables=['zerver_subscription', 'zerver_recipient'],
|
||||
where=['zerver_subscription.recipient_id = zerver_recipient.id',
|
||||
'zerver_recipient.type = 2',
|
||||
'zerver_recipient.type_id = zerver_stream.id',
|
||||
'zerver_subscription.active = true']).annotate(count=Count("name"))
|
||||
print("%d streams" % (streams.count(),))
|
||||
print "%d streams" % (streams.count(),)
|
||||
|
||||
for days_ago in (1, 7, 30):
|
||||
print("In last %d days, users sent:" % (days_ago,))
|
||||
print "In last %d days, users sent:" % (days_ago,)
|
||||
sender_quantities = [self.messages_sent_by(user, days_ago) for user in user_profiles]
|
||||
for quantity in sorted(sender_quantities, reverse=True):
|
||||
print(quantity, end=' ')
|
||||
print("")
|
||||
print quantity,
|
||||
print ""
|
||||
|
||||
print("%d stream messages" % (self.stream_messages(realm, days_ago),))
|
||||
print("%d one-on-one private messages" % (self.private_messages(realm, days_ago),))
|
||||
print("%d messages sent via the API" % (self.api_messages(realm, days_ago),))
|
||||
print("%d group private messages" % (self.group_private_messages(realm, days_ago),))
|
||||
print "%d stream messages" % (self.stream_messages(realm, days_ago),)
|
||||
print "%d one-on-one private messages" % (self.private_messages(realm, days_ago),)
|
||||
print "%d messages sent via the API" % (self.api_messages(realm, days_ago),)
|
||||
print "%d group private messages" % (self.group_private_messages(realm, days_ago),)
|
||||
|
||||
num_notifications_enabled = len([x for x in active_users if x.enable_desktop_notifications == True])
|
||||
num_notifications_enabled = len(filter(lambda x: x.enable_desktop_notifications == True,
|
||||
active_users))
|
||||
self.report_percentage(num_notifications_enabled, num_active,
|
||||
"active users have desktop notifications enabled")
|
||||
|
||||
num_enter_sends = len([x for x in active_users if x.enter_sends])
|
||||
num_enter_sends = len(filter(lambda x: x.enter_sends, active_users))
|
||||
self.report_percentage(num_enter_sends, num_active,
|
||||
"active users have enter-sends")
|
||||
|
||||
@@ -124,8 +120,8 @@ class Command(BaseCommand):
|
||||
starrers = UserMessage.objects.filter(user_profile__in=user_profiles,
|
||||
flags=UserMessage.flags.starred).values(
|
||||
"user_profile").annotate(count=Count("user_profile"))
|
||||
print("%d users have starred %d messages" % (
|
||||
len(starrers), sum([elt["count"] for elt in starrers])))
|
||||
print "%d users have starred %d messages" % (
|
||||
len(starrers), sum([elt["count"] for elt in starrers]))
|
||||
|
||||
active_user_subs = Subscription.objects.filter(
|
||||
user_profile__in=user_profiles, active=True)
|
||||
@@ -133,20 +129,20 @@ class Command(BaseCommand):
|
||||
# Streams not in home view
|
||||
non_home_view = active_user_subs.filter(in_home_view=False).values(
|
||||
"user_profile").annotate(count=Count("user_profile"))
|
||||
print("%d users have %d streams not in home view" % (
|
||||
len(non_home_view), sum([elt["count"] for elt in non_home_view])))
|
||||
print "%d users have %d streams not in home view" % (
|
||||
len(non_home_view), sum([elt["count"] for elt in non_home_view]))
|
||||
|
||||
# Code block markup
|
||||
markup_messages = human_messages.filter(
|
||||
sender__realm=realm, content__contains="~~~").values(
|
||||
"sender").annotate(count=Count("sender"))
|
||||
print("%d users have used code block markup on %s messages" % (
|
||||
len(markup_messages), sum([elt["count"] for elt in markup_messages])))
|
||||
print "%d users have used code block markup on %s messages" % (
|
||||
len(markup_messages), sum([elt["count"] for elt in markup_messages]))
|
||||
|
||||
# Notifications for stream messages
|
||||
notifications = active_user_subs.filter(notifications=True).values(
|
||||
"user_profile").annotate(count=Count("user_profile"))
|
||||
print("%d users receive desktop notifications for %d streams" % (
|
||||
len(notifications), sum([elt["count"] for elt in notifications])))
|
||||
print "%d users receive desktop notifications for %d streams" % (
|
||||
len(notifications), sum([elt["count"] for elt in notifications]))
|
||||
|
||||
print("")
|
||||
print ""
|
||||
|
||||
@@ -1,41 +1,36 @@
|
||||
from __future__ import absolute_import
|
||||
from __future__ import print_function
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db.models import Q
|
||||
from zerver.models import Realm, Stream, Message, Subscription, Recipient, get_realm
|
||||
from zerver.models import Realm, Stream, Message, Subscription, Recipient
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Generate statistics on the streams for a realm."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('realms', metavar='<realm>', type=str, nargs='*',
|
||||
help="realm to generate statistics for")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if options['realms']:
|
||||
if args:
|
||||
try:
|
||||
realms = [get_realm(domain) for domain in options['realms']]
|
||||
except Realm.DoesNotExist as e:
|
||||
print(e)
|
||||
realms = [Realm.objects.get(domain=domain) for domain in args]
|
||||
except Realm.DoesNotExist, e:
|
||||
print e
|
||||
exit(1)
|
||||
else:
|
||||
realms = Realm.objects.all()
|
||||
|
||||
for realm in realms:
|
||||
print(realm.domain)
|
||||
print("------------")
|
||||
print("%25s %15s %10s" % ("stream", "subscribers", "messages"))
|
||||
print realm.domain
|
||||
print "------------"
|
||||
print "%25s %15s %10s" % ("stream", "subscribers", "messages")
|
||||
streams = Stream.objects.filter(realm=realm).exclude(Q(name__istartswith="tutorial-"))
|
||||
invite_only_count = 0
|
||||
for stream in streams:
|
||||
if stream.invite_only:
|
||||
invite_only_count += 1
|
||||
continue
|
||||
print("%25s" % (stream.name,), end=' ')
|
||||
print "%25s" % (stream.name,),
|
||||
recipient = Recipient.objects.filter(type=Recipient.STREAM, type_id=stream.id)
|
||||
print("%10d" % (len(Subscription.objects.filter(recipient=recipient, active=True)),), end=' ')
|
||||
print "%10d" % (len(Subscription.objects.filter(recipient=recipient, active=True)),),
|
||||
num_messages = len(Message.objects.filter(recipient=recipient))
|
||||
print("%12d" % (num_messages,))
|
||||
print("%d invite-only streams" % (invite_only_count,))
|
||||
print("")
|
||||
print "%12d" % (num_messages,)
|
||||
print "%d invite-only streams" % (invite_only_count,)
|
||||
print ""
|
||||
|
||||
@@ -1,43 +1,37 @@
|
||||
from __future__ import absolute_import
|
||||
from __future__ import print_function
|
||||
|
||||
import datetime
|
||||
import pytz
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from zerver.models import UserProfile, Realm, Stream, Message, get_realm
|
||||
from six.moves import range
|
||||
from zerver.models import UserProfile, Realm, Stream, Message
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Generate statistics on user activity."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('realms', metavar='<realm>', type=str, nargs='*',
|
||||
help="realm to generate statistics for")
|
||||
|
||||
def messages_sent_by(self, user, week):
|
||||
start = datetime.datetime.now(tz=pytz.utc) - datetime.timedelta(days=(week + 1)*7)
|
||||
end = datetime.datetime.now(tz=pytz.utc) - datetime.timedelta(days=week*7)
|
||||
return Message.objects.filter(sender=user, pub_date__gt=start, pub_date__lte=end).count()
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if options['realms']:
|
||||
if args:
|
||||
try:
|
||||
realms = [get_realm(domain) for domain in options['realms']]
|
||||
except Realm.DoesNotExist as e:
|
||||
print(e)
|
||||
realms = [Realm.objects.get(domain=domain) for domain in args]
|
||||
except Realm.DoesNotExist, e:
|
||||
print e
|
||||
exit(1)
|
||||
else:
|
||||
realms = Realm.objects.all()
|
||||
|
||||
for realm in realms:
|
||||
print(realm.domain)
|
||||
print realm.domain
|
||||
user_profiles = UserProfile.objects.filter(realm=realm, is_active=True)
|
||||
print("%d users" % (len(user_profiles),))
|
||||
print("%d streams" % (len(Stream.objects.filter(realm=realm)),))
|
||||
print "%d users" % (len(user_profiles),)
|
||||
print "%d streams" % (len(Stream.objects.filter(realm=realm)),)
|
||||
|
||||
for user_profile in user_profiles:
|
||||
print("%35s" % (user_profile.email,), end=' ')
|
||||
print "%35s" % (user_profile.email,),
|
||||
for week in range(10):
|
||||
print("%5d" % (self.messages_sent_by(user_profile, week)), end=' ')
|
||||
print("")
|
||||
print "%5d" % (self.messages_sent_by(user_profile, week)),
|
||||
print ""
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from __future__ import absolute_import
|
||||
from django.db import connection
|
||||
from django.template import RequestContext, loader
|
||||
from django.utils.html import mark_safe
|
||||
@@ -16,10 +15,6 @@ import itertools
|
||||
import time
|
||||
import re
|
||||
import pytz
|
||||
from six.moves import filter
|
||||
from six.moves import map
|
||||
from six.moves import range
|
||||
from six.moves import zip
|
||||
eastern_tz = pytz.timezone('US/Eastern')
|
||||
|
||||
def make_table(title, cols, rows, has_row_class=False):
|
||||
@@ -27,7 +22,7 @@ def make_table(title, cols, rows, has_row_class=False):
|
||||
if not has_row_class:
|
||||
def fix_row(row):
|
||||
return dict(cells=row, row_class=None)
|
||||
rows = list(map(fix_row, rows))
|
||||
rows = map(fix_row, rows)
|
||||
|
||||
data = dict(title=title, cols=cols, rows=rows)
|
||||
|
||||
@@ -42,7 +37,7 @@ def dictfetchall(cursor):
|
||||
"Returns all rows from a cursor as a dict"
|
||||
desc = cursor.description
|
||||
return [
|
||||
dict(list(zip([col[0] for col in desc], row)))
|
||||
dict(zip([col[0] for col in desc], row))
|
||||
for row in cursor.fetchall()
|
||||
]
|
||||
|
||||
@@ -177,7 +172,9 @@ def realm_summary_table(realm_minutes):
|
||||
GROUP BY realm_id
|
||||
) at_risk_counts
|
||||
ON at_risk_counts.realm_id = realm.id
|
||||
WHERE EXISTS (
|
||||
WHERE
|
||||
realm.domain not in ('customer4.invalid', 'wdaher.com')
|
||||
AND EXISTS (
|
||||
SELECT *
|
||||
FROM zerver_useractivity ua
|
||||
JOIN zerver_userprofile up
|
||||
@@ -229,9 +226,14 @@ def realm_summary_table(realm_minutes):
|
||||
|
||||
# Count active sites
|
||||
def meets_goal(row):
|
||||
# The wdaher.com realm doesn't count toward company goals for
|
||||
# obvious reasons, and customer4.invalid is essentially a dup
|
||||
# for users.customer4.invalid.
|
||||
if row['domain'] in ['customer4.invalid', 'wdaher.com']:
|
||||
return False
|
||||
return row['active_user_count'] >= 5
|
||||
|
||||
num_active_sites = len(list(filter(meets_goal, rows)))
|
||||
num_active_sites = len(filter(meets_goal, rows))
|
||||
|
||||
# create totals
|
||||
total_active_user_count = 0
|
||||
@@ -380,7 +382,7 @@ def ad_hoc_queries():
|
||||
cursor = connection.cursor()
|
||||
cursor.execute(query)
|
||||
rows = cursor.fetchall()
|
||||
rows = list(map(list, rows))
|
||||
rows = map(list, rows)
|
||||
cursor.close()
|
||||
|
||||
def fix_rows(i, fixup_func):
|
||||
@@ -614,7 +616,7 @@ def raw_user_activity_table(records):
|
||||
format_date_for_activity_reports(record.last_visit)
|
||||
]
|
||||
|
||||
rows = list(map(row, records))
|
||||
rows = map(row, records)
|
||||
title = 'Raw Data'
|
||||
return make_table(title, cols, rows)
|
||||
|
||||
@@ -821,7 +823,7 @@ def get_realm_activity(request, realm):
|
||||
all_user_records = {}
|
||||
|
||||
try:
|
||||
admins = get_realm(realm).get_admin_users()
|
||||
admins = Realm.objects.get(domain=realm).get_admin_users()
|
||||
except Realm.DoesNotExist:
|
||||
return HttpResponseNotFound("Realm %s does not exist" % (realm,))
|
||||
|
||||
|
||||
@@ -30,25 +30,15 @@ file is as follows:
|
||||
[api]
|
||||
key=<api key from the web interface>
|
||||
email=<your email address>
|
||||
site=<your Zulip server's URI>
|
||||
insecure=<true or false, true means do not verify the server certificate>
|
||||
cert_bundle=<path to a file containing CA or server certificates to trust>
|
||||
|
||||
If omitted, these settings have the following defaults:
|
||||
If you are using Zulip Enterprise, you should also add
|
||||
|
||||
site=https://api.zulip.com
|
||||
insecure=false
|
||||
cert_bundle=<the default CA bundle trusted by Python>
|
||||
site=<your Zulip Enterprise server's URI>
|
||||
|
||||
Alternatively, you may explicitly use "--user" and "--api-key" in our
|
||||
examples, which is especially useful if you are running several bots
|
||||
which share a home directory.
|
||||
|
||||
The command line equivalents for other configuration options are:
|
||||
|
||||
--site=<your Zulip server's URI>
|
||||
--insecure
|
||||
--cert-bundle=<file>
|
||||
which share a home directory. There is also a "--site" option for
|
||||
setting the Zulip Enterprise server on the command line.
|
||||
|
||||
You can obtain your Zulip API key, create bots, and manage bots all
|
||||
from your Zulip [settings page](https://zulip.com/#settings).
|
||||
@@ -114,46 +104,3 @@ Alternatively, if you don't want to use your ~/.zuliprc file:
|
||||
--api-key a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5 \
|
||||
hamlet@example.com cordelia@example.com -m \
|
||||
"Conscience doth make cowards of us all."
|
||||
|
||||
#### Working with an untrusted server certificate
|
||||
|
||||
If your server has either a self-signed certificate, or a certificate signed
|
||||
by a CA that you don't wish to globally trust then by default the API will
|
||||
fail with an SSL verification error.
|
||||
|
||||
You can add `insecure=true` to your .zuliprc file.
|
||||
|
||||
[api]
|
||||
site=https://zulip.example.com
|
||||
insecure=true
|
||||
|
||||
This disables verification of the server certificate, so connections are
|
||||
encrypted but unauthenticated. This is not secure, but may be good enough
|
||||
for a development environment.
|
||||
|
||||
|
||||
You can explicitly trust the server certificate using `cert_bundle=<filename>`
|
||||
in your .zuliprc file.
|
||||
|
||||
[api]
|
||||
site=https://zulip.example.com
|
||||
cert_bundle=/home/bots/certs/zulip.example.com.crt
|
||||
|
||||
You can also explicitly trust a different set of Certificate Authorities from
|
||||
the default bundle that is trusted by Python. For example to trust a company
|
||||
internal CA.
|
||||
|
||||
[api]
|
||||
site=https://zulip.example.com
|
||||
cert_bundle=/home/bots/certs/example.com.ca-bundle
|
||||
|
||||
Save the server certificate (or the CA certificate) in its own file,
|
||||
converting to PEM format first if necessary.
|
||||
Verify that the certificate you have saved is the same as the one on the
|
||||
server.
|
||||
|
||||
The `cert_bundle` option trusts the server / CA certificate only for
|
||||
interaction with the zulip site, and is relatively secure.
|
||||
|
||||
Note that a certificate bundle is merely one or more certificates combined
|
||||
into a single file.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# zulip-send -- Sends a message to the specified recipients.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright © 2012-2014 Zulip, Inc.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright © 2012 Zulip, Inc.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright © 2012 Zulip, Inc.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright © 2014 Zulip, Inc.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright © 2012 Zulip, Inc.
|
||||
@@ -29,7 +29,7 @@ usage = """list-subscriptions --user=<bot's email address> --api-key=<bot's api
|
||||
|
||||
Prints out a list of the user's subscriptions.
|
||||
|
||||
Example: list-subscriptions --user=username@example.com --api-key=a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5
|
||||
Example: list-subscriptions --user=tabbott@zulip.com --api-key=a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5
|
||||
|
||||
You can omit --user and --api-key arguments if you have a properly set up ~/.zuliprc
|
||||
"""
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
#!/usr/bin/env python2.7
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright © 2012 Zulip, Inc.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
import sys
|
||||
import os
|
||||
import optparse
|
||||
|
||||
usage = """print-events --user=<bot's email address> --api-key=<bot's api key> [options]
|
||||
|
||||
Prints out certain events received by the indicated bot or user matching the filter below.
|
||||
|
||||
Example: print-events --user=username@example.com --api-key=a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5
|
||||
|
||||
You can omit --user and --api-key arguments if you have a properly set up ~/.zuliprc
|
||||
"""
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
|
||||
import zulip
|
||||
|
||||
parser = optparse.OptionParser(usage=usage)
|
||||
parser.add_option_group(zulip.generate_option_group(parser))
|
||||
(options, args) = parser.parse_args()
|
||||
|
||||
client = zulip.init_from_options(options)
|
||||
|
||||
def print_event(event):
|
||||
print event
|
||||
|
||||
# This is a blocking call, and will continuously poll for new events
|
||||
# Note also the filter here is messages to the stream Denmark; if you
|
||||
# don't specify event_types it'll print all events.
|
||||
client.call_on_each_event(print_event, event_types=["message"], narrow=[["stream", "Denmark"]])
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright © 2012 Zulip, Inc.
|
||||
@@ -29,7 +29,7 @@ usage = """print-messages --user=<bot's email address> --api-key=<bot's api key>
|
||||
|
||||
Prints out each message received by the indicated bot or user.
|
||||
|
||||
Example: print-messages --user=username@example.com --api-key=a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5
|
||||
Example: print-messages --user=tabbott@zulip.com --api-key=a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5
|
||||
|
||||
You can omit --user and --api-key arguments if you have a properly set up ~/.zuliprc
|
||||
"""
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright © 2012 Zulip, Inc.
|
||||
@@ -29,7 +29,7 @@ usage = """print-next-message --user=<bot's email address> --api-key=<bot's api
|
||||
|
||||
Prints out the next message received by the user.
|
||||
|
||||
Example: print-next-messages --user=username@example.com --api-key=a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5
|
||||
Example: print-next-messages --user=tabbott@zulip.com --api-key=a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5
|
||||
|
||||
You can omit --user and --api-key arguments if you have a properly set up ~/.zuliprc
|
||||
"""
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright © 2012 Zulip, Inc.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright © 2012 Zulip, Inc.
|
||||
@@ -29,8 +29,8 @@ usage = """subscribe --user=<bot's email address> --api-key=<bot's api key> [opt
|
||||
|
||||
Ensures the user is subscribed to the listed streams.
|
||||
|
||||
Examples: subscribe --user=username@example.com --api-key=a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5 --streams=foo
|
||||
subscribe --user=username@example.com --api-key=a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5 --streams='foo bar'
|
||||
Examples: subscribe --user=tabbott@zulip.com --api-key=a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5 --streams=foo
|
||||
subscribe --user=tabbott@zulip.com --api-key=a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5 --streams='foo bar'
|
||||
|
||||
You can omit --user and --api-key arguments if you have a properly set up ~/.zuliprc
|
||||
"""
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright © 2012 Zulip, Inc.
|
||||
@@ -29,8 +29,8 @@ usage = """unsubscribe --user=<bot's email address> --api-key=<bot's api key> [
|
||||
|
||||
Ensures the user is not subscribed to the listed streams.
|
||||
|
||||
Examples: unsubscribe --user=username@example.com --api-key=a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5 --streams=foo
|
||||
unsubscribe --user=username@example.com --api-key=a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5 --streams='foo bar'
|
||||
Examples: unsubscribe --user=tabbott@zulip.com --api-key=a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5 --streams=foo
|
||||
unsubscribe --user=tabbott@zulip.com --api-key=a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5 --streams='foo bar'
|
||||
|
||||
You can omit --user and --api-key arguments if you have a properly set up ~/.zuliprc
|
||||
"""
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright © 2014 Zulip, Inc.
|
||||
@@ -38,7 +38,7 @@ ZULIP_STREAM_NAME = "asana"
|
||||
### OPTIONAL CONFIGURATION ###
|
||||
|
||||
# Set to None for logging to stdout when testing, and to a file for
|
||||
# logging in production.
|
||||
# logging when deployed.
|
||||
#LOG_FILE = "/var/tmp/zulip_asana.log"
|
||||
LOG_FILE = None
|
||||
|
||||
@@ -49,7 +49,7 @@ RESUME_FILE = "/var/tmp/zulip_asana.state"
|
||||
# When initially started, how many hours of messages to include.
|
||||
ASANA_INITIAL_HISTORY_HOURS = 1
|
||||
|
||||
# Set this to your Zulip API server URI
|
||||
# If you're using Zulip Enterprise, set this to your Zulip Enterprise server
|
||||
ZULIP_SITE = "https://api.zulip.com"
|
||||
|
||||
# If properly installed, the Zulip API should be in your import
|
||||
|
||||
21
api/integrations/asana/zulip_asana_mirror
Executable file → Normal file
21
api/integrations/asana/zulip_asana_mirror
Executable file → Normal file
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Asana integration for Zulip
|
||||
@@ -22,17 +22,11 @@
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# The "zulip_asana_mirror" script is run continuously, possibly on a work computer
|
||||
# or preferably on a server.
|
||||
#
|
||||
# When restarted, it will attempt to pick up where it left off.
|
||||
#
|
||||
# python-dateutil is a dependency for this script.
|
||||
|
||||
import base64
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import dateutil.parser
|
||||
import dateutil.tz
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@@ -40,15 +34,6 @@ import time
|
||||
import urllib2
|
||||
|
||||
import sys
|
||||
|
||||
try:
|
||||
import dateutil.parser
|
||||
import dateutil.tz
|
||||
except ImportError, e:
|
||||
print >>sys.stderr, e
|
||||
print >>sys.stderr, "Please install the python-dateutil package."
|
||||
exit(1)
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
import zulip_asana_config as config
|
||||
VERSION = "0.9"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright © 2014 Zulip, Inc.
|
||||
@@ -41,7 +41,7 @@ ZULIP_STREAM_NAME = "basecamp"
|
||||
## path, but if not, set a custom path below
|
||||
ZULIP_API_PATH = None
|
||||
|
||||
# Set this to your Zulip API server URI
|
||||
# If you're using Zulip Enterprise, set this to your Zulip Enterprise server
|
||||
ZULIP_SITE = "https://api.zulip.com"
|
||||
|
||||
# If you wish to log to a file rather than stdout/stderr,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Zulip mirror of Basecamp activity
|
||||
@@ -49,7 +49,7 @@ client = zulip.Client(
|
||||
site=config.ZULIP_SITE,
|
||||
api_key=config.ZULIP_API_KEY,
|
||||
client="ZulipBasecamp/" + VERSION)
|
||||
user_agent = "Basecamp To Zulip Mirroring script (zulip-devel@googlegroups.com)"
|
||||
user_agent = "Basecamp To Zulip Mirroring script (support@zulip.com)"
|
||||
htmlParser = HTMLParser()
|
||||
|
||||
# find some form of JSON loader/dumper, with a preference order for speed.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright © 2014 Zulip, Inc.
|
||||
@@ -50,7 +50,7 @@ ZULIP_TICKETS_STREAM_NAME = "tickets"
|
||||
# path, but if not, set a custom path below
|
||||
ZULIP_API_PATH = None
|
||||
|
||||
# Set this to your Zulip API server URI
|
||||
# If you're using Zulip Enterprise, set this to your Zulip Enterprise server
|
||||
ZULIP_SITE = "https://api.zulip.com"
|
||||
|
||||
# If you wish to log to a file rather than stdout/stderr,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Zulip mirror of Codebase HQ activity
|
||||
@@ -22,12 +22,12 @@
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# The "zulip_codebase_mirror" script is run continuously, possibly on a work
|
||||
# computer or preferably on a server.
|
||||
# The "codebase-mirror.py" script is run continuously, possibly on a work computer
|
||||
# or preferably on a server.
|
||||
#
|
||||
# When restarted, it will attempt to pick up where it left off.
|
||||
#
|
||||
# python-dateutil is a dependency for this script.
|
||||
# You may need to install the python-requests library, as well as python-dateutil
|
||||
|
||||
import requests
|
||||
import logging
|
||||
@@ -37,13 +37,7 @@ import os
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
try:
|
||||
import dateutil.parser
|
||||
except ImportError, e:
|
||||
print >>sys.stderr, e
|
||||
print >>sys.stderr, "Please install the python-dateutil package."
|
||||
exit(1)
|
||||
import dateutil.parser
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
import zulip_codebase_config as config
|
||||
@@ -58,7 +52,7 @@ client = zulip.Client(
|
||||
site=config.ZULIP_SITE,
|
||||
api_key=config.ZULIP_API_KEY,
|
||||
client="ZulipCodebase/" + VERSION)
|
||||
user_agent = "Codebase To Zulip Mirroring script (zulip-devel@googlegroups.com)"
|
||||
user_agent = "Codebase To Zulip Mirroring script (support@zulip.com)"
|
||||
|
||||
# find some form of JSON loader/dumper, with a preference order for speed.
|
||||
json_implementations = ['ujson', 'cjson', 'simplejson', 'json']
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Zulip notification post-receive hook.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright © 2014 Zulip, Inc.
|
||||
@@ -61,5 +61,5 @@ def format_commit_message(author, subject, commit_id):
|
||||
## path, but if not, set a custom path below
|
||||
ZULIP_API_PATH = None
|
||||
|
||||
# Set this to your Zulip server's API URI
|
||||
# If you're using Zulip Enterprise, set this to your Zulip Enterprise server
|
||||
ZULIP_SITE = "https://api.zulip.com"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Zulip hook for Mercurial changeset pushes.
|
||||
# Copyright © 2012-2014 Zulip, Inc.
|
||||
# Copyright © 2012-2014 Zulip, Inc.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -25,10 +25,8 @@
|
||||
#
|
||||
# This hook is called when changesets are pushed to the master repository (ie
|
||||
# `hg push`). See https://zulip.com/integrations for installation instructions.
|
||||
from __future__ import absolute_import
|
||||
|
||||
import zulip
|
||||
from six.moves import range
|
||||
|
||||
VERSION = "0.9"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
import optparse
|
||||
import zulip
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright © 2012-2014 Zulip, Inc.
|
||||
@@ -33,7 +33,6 @@ For example:
|
||||
1234 //depot/security/src/
|
||||
|
||||
'''
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import sys
|
||||
@@ -60,12 +59,12 @@ try:
|
||||
changelist = int(sys.argv[1])
|
||||
changeroot = sys.argv[2]
|
||||
except IndexError:
|
||||
print("Wrong number of arguments.\n\n", end=' ', file=sys.stderr)
|
||||
print(__doc__, file=sys.stderr)
|
||||
print >> sys.stderr, "Wrong number of arguments.\n\n",
|
||||
print >> sys.stderr, __doc__
|
||||
sys.exit(-1)
|
||||
except ValueError:
|
||||
print("First argument must be an integer.\n\n", end=' ', file=sys.stderr)
|
||||
print(__doc__, file=sys.stderr)
|
||||
print >> sys.stderr, "First argument must be an integer.\n\n",
|
||||
print >> sys.stderr, __doc__
|
||||
sys.exit(-1)
|
||||
|
||||
metadata = git_p4.p4_describe(changelist)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright © 2014 Zulip, Inc.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# RSS integration for Zulip
|
||||
@@ -136,7 +136,7 @@ def strip_tags(html):
|
||||
def compute_entry_hash(entry):
|
||||
entry_time = entry.get("published", entry.get("updated"))
|
||||
entry_id = entry.get("id", entry.get("link"))
|
||||
return hashlib.md5(entry_id + str(entry_time)).hexdigest()
|
||||
return hashlib.md5(entry_id + entry_time).hexdigest()
|
||||
|
||||
def elide_subject(subject):
|
||||
MAX_TOPIC_LENGTH = 60
|
||||
@@ -184,7 +184,7 @@ for feed_url in feed_urls:
|
||||
entry_hash = compute_entry_hash(entry)
|
||||
# An entry has either been published or updated.
|
||||
entry_time = entry.get("published_parsed", entry.get("updated_parsed"))
|
||||
if entry_time is not None and (time.time() - calendar.timegm(entry_time)) > OLDNESS_THRESHOLD * 60 * 60 * 24:
|
||||
if (time.time() - calendar.timegm(entry_time)) > OLDNESS_THRESHOLD * 60 * 60 * 24:
|
||||
# As a safeguard against misbehaving feeds, don't try to process
|
||||
# entries older than some threshold.
|
||||
continue
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Zulip notification post-commit hook.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright © 2014 Zulip, Inc.
|
||||
@@ -53,5 +53,5 @@ def commit_notice_destination(path, commit):
|
||||
## path, but if not, set a custom path below
|
||||
ZULIP_API_PATH = None
|
||||
|
||||
# Set this to your Zulip server's API URI
|
||||
# If you're using Zulip Enterprise, set this to your Zulip Enterprise server
|
||||
ZULIP_SITE = "https://api.zulip.com"
|
||||
|
||||
@@ -107,8 +107,8 @@ class ZulipPlugin(Component):
|
||||
field_changes = []
|
||||
for key in old_values.keys():
|
||||
if key == "description":
|
||||
content += '- Changed %s from %s\n\nto %s' % (key, markdown_block(old_values.get(key)),
|
||||
markdown_block(ticket.values.get(key)))
|
||||
content += '- Changed %s from %s to %s' % (key, markdown_block(old_values.get(key)),
|
||||
markdown_block(ticket.values.get(key)))
|
||||
elif old_values.get(key) == "":
|
||||
field_changes.append('%s: => **%s**' % (key, ticket.values.get(key)))
|
||||
elif ticket.values.get(key) == "":
|
||||
|
||||
@@ -47,5 +47,5 @@ TRAC_NOTIFY_FIELDS = ["description", "summary", "resolution", "comment", "owner"
|
||||
## path, but if not, set a custom path below
|
||||
ZULIP_API_PATH = None
|
||||
|
||||
# Set this to your Zulip API server URI
|
||||
# If you're using Zulip Enterprise, set this to your Zulip Enterprise server
|
||||
ZULIP_SITE = "https://api.zulip.com"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Twitter integration for Zulip
|
||||
@@ -126,9 +126,9 @@ except ConfigParser.NoOptionError:
|
||||
user_id = options.twitter_id
|
||||
|
||||
client = zulip.Client(
|
||||
email=options.zulip_email,
|
||||
api_key=options.zulip_api_key,
|
||||
site=options.zulip_site,
|
||||
email=options.email,
|
||||
api_key=options.api_key,
|
||||
site=options.site,
|
||||
client="ZulipTwitter/" + VERSION,
|
||||
verbose=True)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Twitter search integration for Zulip
|
||||
|
||||
13
api/setup.py
13
api/setup.py
@@ -1,7 +1,6 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import print_function
|
||||
import os
|
||||
import sys
|
||||
|
||||
@@ -10,8 +9,8 @@ import itertools
|
||||
def version():
|
||||
version_py = os.path.join(os.path.dirname(__file__), "zulip", "__init__.py")
|
||||
with open(version_py) as in_handle:
|
||||
version_line = next(itertools.dropwhile(lambda x: not x.startswith("__version__"),
|
||||
in_handle))
|
||||
version_line = itertools.dropwhile(lambda x: not x.startswith("__version__"),
|
||||
in_handle).next()
|
||||
version = version_line.split('=')[-1].strip().replace('"', '')
|
||||
return version
|
||||
|
||||
@@ -27,7 +26,7 @@ package_info = dict(
|
||||
version=version(),
|
||||
description='Bindings for the Zulip message API',
|
||||
author='Zulip, Inc.',
|
||||
author_email='zulip-devel@googlegroups.com',
|
||||
author_email='support@zulip.com',
|
||||
classifiers=[
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Environment :: Web Environment',
|
||||
@@ -61,13 +60,13 @@ except ImportError:
|
||||
try:
|
||||
import simplejson
|
||||
except ImportError:
|
||||
print("simplejson is not installed", file=sys.stderr)
|
||||
print >>sys.stderr, "simplejson is not installed"
|
||||
sys.exit(1)
|
||||
try:
|
||||
import requests
|
||||
assert(LooseVersion(requests.__version__) >= LooseVersion('0.12.1'))
|
||||
except (ImportError, AssertionError):
|
||||
print("requests >=0.12.1 is not installed", file=sys.stderr)
|
||||
print >>sys.stderr, "requests >=0.12.1 is not installed"
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
from __future__ import print_function
|
||||
from __future__ import absolute_import
|
||||
import simplejson
|
||||
import requests
|
||||
import time
|
||||
@@ -31,19 +29,14 @@ import sys
|
||||
import os
|
||||
import optparse
|
||||
import platform
|
||||
import urllib
|
||||
import random
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from six.moves.configparser import SafeConfigParser
|
||||
from ConfigParser import SafeConfigParser
|
||||
import logging
|
||||
import six
|
||||
|
||||
|
||||
__version__ = "0.2.4"
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Check that we have a recent enough version
|
||||
# Older versions don't provide the 'json' attribute on responses.
|
||||
assert(LooseVersion(requests.__version__) >= LooseVersion('0.12.1'))
|
||||
@@ -52,117 +45,55 @@ requests_json_is_function = callable(requests.Response.json)
|
||||
|
||||
API_VERSTRING = "v1/"
|
||||
|
||||
class CountingBackoff(object):
|
||||
def __init__(self, maximum_retries=10, timeout_success_equivalent=None):
|
||||
self.number_of_retries = 0
|
||||
self.maximum_retries = maximum_retries
|
||||
self.timeout_success_equivalent = timeout_success_equivalent
|
||||
self.last_attempt_time = 0
|
||||
|
||||
def keep_going(self):
|
||||
self._check_success_timeout()
|
||||
return self.number_of_retries < self.maximum_retries
|
||||
|
||||
def succeed(self):
|
||||
self.number_of_retries = 0
|
||||
self.last_attempt_time = time.time()
|
||||
|
||||
def fail(self):
|
||||
self._check_success_timeout()
|
||||
self.number_of_retries = min(self.number_of_retries + 1,
|
||||
self.maximum_retries)
|
||||
self.last_attempt_time = time.time()
|
||||
|
||||
def _check_success_timeout(self):
|
||||
if (self.timeout_success_equivalent is not None
|
||||
and self.last_attempt_time != 0
|
||||
and time.time() - self.last_attempt_time > self.timeout_success_equivalent):
|
||||
self.number_of_retries = 0
|
||||
|
||||
class RandomExponentialBackoff(CountingBackoff):
|
||||
def fail(self):
|
||||
super(RandomExponentialBackoff, self).fail()
|
||||
# Exponential growth with ratio sqrt(2); compute random delay
|
||||
# between x and 2x where x is growing exponentially
|
||||
delay_scale = int(2 ** (self.number_of_retries / 2.0 - 1)) + 1
|
||||
delay = delay_scale + random.randint(1, delay_scale)
|
||||
message = "Sleeping for %ss [max %s] before retrying." % (delay, delay_scale * 2)
|
||||
try:
|
||||
logger.warning(message)
|
||||
except NameError:
|
||||
print(message)
|
||||
time.sleep(delay)
|
||||
|
||||
def _default_client():
|
||||
return "ZulipPython/" + __version__
|
||||
|
||||
def generate_option_group(parser, prefix=''):
|
||||
group = optparse.OptionGroup(parser, 'Zulip API configuration')
|
||||
group.add_option('--%ssite' % (prefix,),
|
||||
dest="zulip_site",
|
||||
help="Zulip server URI",
|
||||
def generate_option_group(parser):
|
||||
group = optparse.OptionGroup(parser, 'API configuration')
|
||||
group.add_option('--site',
|
||||
help="Zulip Enterprise server URI (if using Zulip Enterprise)",
|
||||
default=None)
|
||||
group.add_option('--%sapi-key' % (prefix,),
|
||||
dest="zulip_api_key",
|
||||
group.add_option('--api-key',
|
||||
action='store')
|
||||
group.add_option('--%suser' % (prefix,),
|
||||
dest='zulip_email',
|
||||
group.add_option('--user',
|
||||
dest='email',
|
||||
help='Email address of the calling bot or user.')
|
||||
group.add_option('--%sconfig-file' % (prefix,),
|
||||
group.add_option('--config-file',
|
||||
action='store',
|
||||
dest="zulip_config_file",
|
||||
help='Location of an ini file containing the\nabove information. (default ~/.zuliprc)')
|
||||
group.add_option('-v', '--verbose',
|
||||
action='store_true',
|
||||
help='Provide detailed output.')
|
||||
group.add_option('--%sclient' % (prefix,),
|
||||
group.add_option('--client',
|
||||
action='store',
|
||||
default=None,
|
||||
dest="zulip_client",
|
||||
help=optparse.SUPPRESS_HELP)
|
||||
group.add_option('--insecure',
|
||||
action='store_true',
|
||||
dest='insecure',
|
||||
help='''Do not verify the server certificate.
|
||||
The https connection will not be secure.''')
|
||||
group.add_option('--cert-bundle',
|
||||
action='store',
|
||||
dest='cert_bundle',
|
||||
help='''Specify a file containing either the
|
||||
server certificate, or a set of trusted
|
||||
CA certificates. This will be used to
|
||||
verify the server's identity. All
|
||||
certificates should be PEM encoded.''')
|
||||
return group
|
||||
|
||||
def init_from_options(options, client=None):
|
||||
if options.zulip_client is not None:
|
||||
client = options.zulip_client
|
||||
if options.client is not None:
|
||||
client = options.client
|
||||
elif client is None:
|
||||
client = _default_client()
|
||||
return Client(email=options.zulip_email, api_key=options.zulip_api_key,
|
||||
config_file=options.zulip_config_file, verbose=options.verbose,
|
||||
site=options.zulip_site, client=client,
|
||||
cert_bundle=options.cert_bundle, insecure=options.insecure)
|
||||
|
||||
def get_default_config_filename():
|
||||
config_file = os.path.join(os.environ["HOME"], ".zuliprc")
|
||||
if (not os.path.exists(config_file) and
|
||||
os.path.exists(os.path.join(os.environ["HOME"], ".humbugrc"))):
|
||||
raise RuntimeError("The Zulip API configuration file is now ~/.zuliprc; please run:\n\n mv ~/.humbugrc ~/.zuliprc\n")
|
||||
return config_file
|
||||
return Client(email=options.email, api_key=options.api_key,
|
||||
config_file=options.config_file, verbose=options.verbose,
|
||||
site=options.site, client=client)
|
||||
|
||||
class Client(object):
|
||||
def __init__(self, email=None, api_key=None, config_file=None,
|
||||
verbose=False, retry_on_errors=True,
|
||||
site=None, client=None,
|
||||
cert_bundle=None, insecure=None):
|
||||
site=None, client=None):
|
||||
if client is None:
|
||||
client = _default_client()
|
||||
|
||||
if config_file is None:
|
||||
config_file = get_default_config_filename()
|
||||
if os.path.exists(config_file):
|
||||
if None in (api_key, email):
|
||||
if config_file is None:
|
||||
config_file = os.path.join(os.environ["HOME"], ".zuliprc")
|
||||
if (not os.path.exists(config_file) and
|
||||
os.path.exists(os.path.join(os.environ["HOME"], ".humbugrc"))):
|
||||
raise RuntimeError("The Zulip API configuration file is now ~/.zuliprc; please run:\n\n mv ~/.humbugrc ~/.zuliprc\n")
|
||||
if not os.path.exists(config_file):
|
||||
raise RuntimeError("api_key or email not specified and %s does not exist"
|
||||
% (config_file,))
|
||||
config = SafeConfigParser()
|
||||
with file(config_file, 'r') as f:
|
||||
config.readfp(f, config_file)
|
||||
@@ -172,22 +103,6 @@ class Client(object):
|
||||
email = config.get("api", "email")
|
||||
if site is None and config.has_option("api", "site"):
|
||||
site = config.get("api", "site")
|
||||
if cert_bundle is None and config.has_option("api", "cert_bundle"):
|
||||
cert_bundle = config.get("api", "cert_bundle")
|
||||
if insecure is None and config.has_option("api", "insecure"):
|
||||
# Be quite strict about what is accepted so that users don't
|
||||
# disable security unintentionally.
|
||||
insecure_setting = config.get("api", "insecure").lower()
|
||||
if insecure_setting == "true":
|
||||
insecure = True
|
||||
elif insecure_setting == "false":
|
||||
insecure = False
|
||||
else:
|
||||
raise RuntimeError("insecure is set to '%s', it must be 'true' or 'false' if it is used in %s"
|
||||
% (insecure_setting, config_file))
|
||||
elif None in (api_key, email):
|
||||
raise RuntimeError("api_key or email not specified and %s does not exist"
|
||||
% (config_file,))
|
||||
|
||||
self.api_key = api_key
|
||||
self.email = email
|
||||
@@ -207,27 +122,9 @@ class Client(object):
|
||||
self.retry_on_errors = retry_on_errors
|
||||
self.client_name = client
|
||||
|
||||
if insecure:
|
||||
self.tls_verification=False
|
||||
elif cert_bundle is not None:
|
||||
if not os.path.isfile(cert_bundle):
|
||||
raise RuntimeError("tls bundle '%s' does not exist"
|
||||
%(cert_bundle,))
|
||||
self.tls_verification=cert_bundle
|
||||
else:
|
||||
# Default behavior: verify against system CA certificates
|
||||
self.tls_verification=True
|
||||
|
||||
def get_user_agent(self):
|
||||
vendor = ''
|
||||
vendor_version = ''
|
||||
try:
|
||||
vendor = platform.system()
|
||||
vendor_version = platform.release()
|
||||
except IOError:
|
||||
# If the calling process is handling SIGCHLD, platform.system() can
|
||||
# fail with an IOError. See http://bugs.python.org/issue9127
|
||||
pass
|
||||
vendor = platform.system()
|
||||
vendor_version = platform.release()
|
||||
|
||||
if vendor == "Linux":
|
||||
vendor, vendor_version, dummy = platform.linux_distribution()
|
||||
@@ -246,7 +143,7 @@ class Client(object):
|
||||
request = {}
|
||||
|
||||
for (key, val) in orig_request.iteritems():
|
||||
if not (isinstance(val, str) or isinstance(val, six.text_type)):
|
||||
if not (isinstance(val, str) or isinstance(val, unicode)):
|
||||
request[key] = simplejson.dumps(val)
|
||||
else:
|
||||
request[key] = val
|
||||
@@ -276,9 +173,9 @@ class Client(object):
|
||||
def end_error_retry(succeeded):
|
||||
if query_state["had_error_retry"] and self.verbose:
|
||||
if succeeded:
|
||||
print("Success!")
|
||||
print "Success!"
|
||||
else:
|
||||
print("Failed!")
|
||||
print "Failed!"
|
||||
|
||||
while True:
|
||||
try:
|
||||
@@ -292,7 +189,7 @@ class Client(object):
|
||||
urlparse.urljoin(self.base_url, url),
|
||||
auth=requests.auth.HTTPBasicAuth(self.email,
|
||||
self.api_key),
|
||||
verify=self.tls_verification, timeout=90,
|
||||
verify=True, timeout=90,
|
||||
headers={"User-agent": self.get_user_agent()},
|
||||
**kwargs)
|
||||
|
||||
@@ -344,17 +241,13 @@ class Client(object):
|
||||
|
||||
@classmethod
|
||||
def _register(cls, name, url=None, make_request=(lambda request={}: request),
|
||||
method="POST", computed_url=None, **query_kwargs):
|
||||
method="POST", **query_kwargs):
|
||||
if url is None:
|
||||
url = name
|
||||
def call(self, *args, **kwargs):
|
||||
request = make_request(*args, **kwargs)
|
||||
if computed_url is not None:
|
||||
req_url = computed_url(request)
|
||||
else:
|
||||
req_url = url
|
||||
return self.do_api_query(request, API_VERSTRING + req_url, method=method, **query_kwargs)
|
||||
call.__name__ = name
|
||||
return self.do_api_query(request, API_VERSTRING + url, method=method, **query_kwargs)
|
||||
call.func_name = name
|
||||
setattr(cls, name, call)
|
||||
|
||||
def call_on_each_event(self, callback, event_types=None, narrow=[]):
|
||||
@@ -367,7 +260,7 @@ class Client(object):
|
||||
|
||||
if 'error' in res.get('result'):
|
||||
if self.verbose:
|
||||
print("Server returned error:\n%s" % res['msg'])
|
||||
print "Server returned error:\n%s" % res['msg']
|
||||
time.sleep(1)
|
||||
else:
|
||||
return (res['queue_id'], res['last_event_id'])
|
||||
@@ -381,13 +274,13 @@ class Client(object):
|
||||
if 'error' in res.get('result'):
|
||||
if res["result"] == "http-error":
|
||||
if self.verbose:
|
||||
print("HTTP error fetching events -- probably a server restart")
|
||||
print "HTTP error fetching events -- probably a server restart"
|
||||
elif res["result"] == "connection-error":
|
||||
if self.verbose:
|
||||
print("Connection error fetching events -- probably server is temporarily down?")
|
||||
print "Connection error fetching events -- probably server is temporarily down?"
|
||||
else:
|
||||
if self.verbose:
|
||||
print("Server returned error:\n%s" % res["msg"])
|
||||
print "Server returned error:\n%s" % res["msg"]
|
||||
if res["msg"].startswith("Bad event queue id:"):
|
||||
# Our event queue went away, probably because
|
||||
# we were asleep or the server restarted
|
||||
@@ -459,7 +352,6 @@ Client._register('update_message', method='PATCH', url='messages', make_request=
|
||||
Client._register('get_messages', method='GET', url='messages/latest', longpolling=True)
|
||||
Client._register('get_events', url='events', method='GET', longpolling=True, make_request=(lambda **kwargs: kwargs))
|
||||
Client._register('register', make_request=_mk_events)
|
||||
Client._register('export', method='GET', url='export')
|
||||
Client._register('deregister', url="events", method="DELETE", make_request=_mk_deregister)
|
||||
Client._register('get_profile', method='GET', url='users/me')
|
||||
Client._register('get_streams', method='GET', url='streams', make_request=_kwargs_to_dict)
|
||||
@@ -467,8 +359,5 @@ Client._register('get_members', method='GET', url='users')
|
||||
Client._register('list_subscriptions', method='GET', url='users/me/subscriptions')
|
||||
Client._register('add_subscriptions', url='users/me/subscriptions', make_request=_mk_subs)
|
||||
Client._register('remove_subscriptions', method='PATCH', url='users/me/subscriptions', make_request=_mk_rm_subs)
|
||||
Client._register('get_subscribers', method='GET',
|
||||
computed_url=lambda request: 'streams/%s/members' % (urllib.quote(request['stream'], safe=''),),
|
||||
make_request=_kwargs_to_dict)
|
||||
Client._register('render_message', method='GET', url='messages/render')
|
||||
Client._register('create_user', method='POST', url='users')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
import xml.etree.ElementTree as ET
|
||||
import subprocess
|
||||
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,13 +0,0 @@
|
||||
This directory contains images adapted from the Noto project. For more detail
|
||||
about Noto, please refer to:
|
||||
|
||||
https://code.google.com/p/noto/
|
||||
|
||||
These images were generated from the git repository at
|
||||
<https://android.googlesource.com/platform/external/noto-fonts> as of
|
||||
90372d894b5d9c9f2a111315d2eb3b8de1979ee4
|
||||
|
||||
and
|
||||
|
||||
<https://android.googlesource.com/platform/frameworks/base> at
|
||||
07912f876c8639f811b06831465c14c4a3b17663.
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
from __future__ import absolute_import
|
||||
|
||||
import os
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
import sys
|
||||
import logging
|
||||
import os
|
||||
|
||||
@@ -11,5 +11,5 @@ ZULIP_DIR=/home/zulip/deployments/current
|
||||
STATE_DIR=/var/lib/nagios_state
|
||||
STATE_FILE=$STATE_DIR/check-rabbitmq-consumers-$queue
|
||||
|
||||
"$ZULIP_DIR/bots/check-rabbitmq-consumers" "--queue=$queue" &> "${STATE_FILE}-tmp";
|
||||
mv "${STATE_FILE}-tmp" "$STATE_FILE"
|
||||
$ZULIP_DIR/bots/check-rabbitmq-consumers --queue=$queue &> ${STATE_FILE}-tmp;
|
||||
mv ${STATE_FILE}-tmp $STATE_FILE
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
import sys
|
||||
import time
|
||||
import optparse
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
|
||||
import sys
|
||||
import time
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
|
||||
import sys
|
||||
import re
|
||||
@@ -6,14 +6,8 @@ import time
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
WARN_THRESHOLD_DEFAULT = 10
|
||||
WARN_THRESHOLD = {
|
||||
'missedmessage_emails': 45,
|
||||
}
|
||||
CRIT_THRESHOLD_DEFAULT = 50
|
||||
CRIT_THRESHOLD = {
|
||||
'missedmessage_emails': 70,
|
||||
}
|
||||
WARN_THRESHOLD = 10
|
||||
CRIT_THRESHOLD = 50
|
||||
|
||||
states = {
|
||||
0: "OK",
|
||||
@@ -39,10 +33,10 @@ for line in output.split("\n"):
|
||||
queue = m.group(1)
|
||||
count = int(m.group(2))
|
||||
this_status = 0
|
||||
if count > CRIT_THRESHOLD.get(queue, CRIT_THRESHOLD_DEFAULT):
|
||||
if count > CRIT_THRESHOLD:
|
||||
this_status = 2
|
||||
warn_queues.append(queue)
|
||||
elif count > WARN_THRESHOLD.get(queue, WARN_THRESHOLD_DEFAULT):
|
||||
elif count > WARN_THRESHOLD:
|
||||
this_status = max(status, 1)
|
||||
warn_queues.append(queue)
|
||||
|
||||
@@ -56,7 +50,7 @@ now_struct = time.gmtime(now)
|
||||
# While we are sending digest emails, at 11am each weekday, the mail queues can
|
||||
# get backed up; don't alert on those.
|
||||
if not set(warn_queues) - set(("missedmessage_emails", "digest_emails")) and \
|
||||
now_struct.tm_hour == 15 and now_struct.tm_min < 25:
|
||||
now_struct.tm_hour == 16 and now_struct.tm_min < 15:
|
||||
status = 0
|
||||
print("%s|%s|%s|processing digests, not alerting on elevated mail queues" % (
|
||||
now, status, states[status]))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
import time
|
||||
|
||||
def nagios_from_file(results_file):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
import sys
|
||||
import time
|
||||
import datetime
|
||||
@@ -47,7 +47,7 @@ parser.add_option_group(zulip.generate_option_group(parser))
|
||||
|
||||
(options, args) = parser.parse_args()
|
||||
|
||||
if not (options.zulip_email and options.calendar):
|
||||
if not (options.user and options.calendar):
|
||||
parser.error('You must specify --user and --calendar')
|
||||
|
||||
try:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#! /usr/bin/env python2.7
|
||||
#! /usr/bin/env python
|
||||
#
|
||||
# EXPERIMENTAL
|
||||
# IRC <=> Zulip mirroring bot
|
||||
@@ -6,18 +6,15 @@
|
||||
# Setup: First, you need to install python-irc version 8.5.3
|
||||
# (https://bitbucket.org/jaraco/irc)
|
||||
|
||||
from __future__ import print_function
|
||||
import irc.bot
|
||||
import irc.strings
|
||||
from irc.client import ip_numstr_to_quad, ip_quad_to_numstr
|
||||
import zulip
|
||||
import optparse
|
||||
|
||||
IRC_DOMAIN = "irc.example.com"
|
||||
|
||||
def zulip_sender(sender_string):
|
||||
nick = sender_string.split("!")[0]
|
||||
return nick + "@" + IRC_DOMAIN
|
||||
return nick + "@irc.zulip.com"
|
||||
|
||||
class IRCBot(irc.bot.SingleServerIRCBot):
|
||||
def __init__(self, channel, nickname, server, port=6667):
|
||||
@@ -50,33 +47,33 @@ class IRCBot(irc.bot.SingleServerIRCBot):
|
||||
def on_privmsg(self, c, e):
|
||||
content = e.arguments[0]
|
||||
sender = zulip_sender(e.source)
|
||||
if sender.endswith("_zulip@" + IRC_DOMAIN):
|
||||
if sender.endswith("_zulip@irc.zulip.com"):
|
||||
return
|
||||
|
||||
# Forward the PM to Zulip
|
||||
print(zulip_client.send_message({
|
||||
print zulip_client.send_message({
|
||||
"sender": sender,
|
||||
"type": "private",
|
||||
"to": "username@example.com",
|
||||
"to": "tabbott@zulip.com",
|
||||
"content": content,
|
||||
}))
|
||||
})
|
||||
|
||||
def on_pubmsg(self, c, e):
|
||||
content = e.arguments[0]
|
||||
stream = e.target
|
||||
sender = zulip_sender(e.source)
|
||||
if sender.endswith("_zulip@" + IRC_DOMAIN):
|
||||
if sender.endswith("_zulip@irc.zulip.com"):
|
||||
return
|
||||
|
||||
# Forward the stream message to Zulip
|
||||
print(zulip_client.send_message({
|
||||
print zulip_client.send_message({
|
||||
"forged": "yes",
|
||||
"sender": sender,
|
||||
"type": "stream",
|
||||
"to": stream,
|
||||
"subject": "IRC",
|
||||
"content": content,
|
||||
}))
|
||||
})
|
||||
|
||||
def on_dccmsg(self, c, e):
|
||||
c.privmsg("You said: " + e.arguments[0])
|
||||
@@ -93,12 +90,12 @@ class IRCBot(irc.bot.SingleServerIRCBot):
|
||||
return
|
||||
self.dcc_connect(address, port)
|
||||
|
||||
usage = """python2.7 irc-mirror.py --server=IRC_SERVER --channel=<CHANNEL> --nick-prefix=<NICK> [optional args]
|
||||
usage = """python irc-mirror.py --server=IRC_SERVER --channel=<CHANNEL> --nick-prefix=<NICK> [optional args]
|
||||
|
||||
Example:
|
||||
|
||||
python2.7 irc-mirror.py --irc-server=127.0.0.1 --channel='#test' --nick-prefix=username
|
||||
--site=https://zulip.example.com --user=irc-bot@example.com
|
||||
python irc-mirror.py --irc-server=127.0.0.1 --channel='#test' --nick-prefix=tabbott
|
||||
--site=https://staging.zulip.com --user=irc-bot@zulip.com
|
||||
--api-key=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
|
||||
Note that "_zulip" will be automatically appended to the IRC nick provided
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
#!/usr/bin/env python2.7
|
||||
# Copyright (C) 2014 Zulip, Inc.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person
|
||||
# obtaining a copy of this software and associated documentation files
|
||||
# (the "Software"), to deal in the Software without restriction,
|
||||
# including without limitation the rights to use, copy, modify, merge,
|
||||
# publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
# and to permit persons to whom the Software is furnished to do so,
|
||||
# subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from __future__ import print_function
|
||||
import sys
|
||||
import subprocess
|
||||
import os
|
||||
import traceback
|
||||
import signal
|
||||
from zulip import RandomExponentialBackoff
|
||||
|
||||
def die(signal, frame):
|
||||
# We actually want to exit, so run os._exit (so as not to be caught and restarted)
|
||||
os._exit(1)
|
||||
|
||||
signal.signal(signal.SIGINT, die)
|
||||
|
||||
args = [os.path.join(os.path.dirname(sys.argv[0]), "jabber_mirror_backend.py")]
|
||||
args.extend(sys.argv[1:])
|
||||
|
||||
backoff = RandomExponentialBackoff(timeout_success_equivalent=300)
|
||||
while backoff.keep_going():
|
||||
print("Starting Jabber mirroring bot")
|
||||
try:
|
||||
ret = subprocess.call(args)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
else:
|
||||
if ret == 2:
|
||||
# Don't try again on initial configuration errors
|
||||
sys.exit(ret)
|
||||
|
||||
backoff.fail()
|
||||
|
||||
print("")
|
||||
print("")
|
||||
print("ERROR: The Jabber mirroring bot is unable to continue mirroring Jabber.")
|
||||
print("Please contact zulip-devel@googlegroups.com if you need assistance.")
|
||||
print("")
|
||||
sys.exit(1)
|
||||
@@ -1,445 +0,0 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#
|
||||
# Copyright (C) 2013 Permabit, Inc.
|
||||
# Copyright (C) 2013--2014 Zulip, Inc.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person
|
||||
# obtaining a copy of this software and associated documentation files
|
||||
# (the "Software"), to deal in the Software without restriction,
|
||||
# including without limitation the rights to use, copy, modify, merge,
|
||||
# publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
# and to permit persons to whom the Software is furnished to do so,
|
||||
# subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
# The following is a table showing which kinds of messages are handled by the
|
||||
# mirror in each mode:
|
||||
#
|
||||
# Message origin/type --> | Jabber | Zulip
|
||||
# Mode/sender-, +-----+----+--------+----
|
||||
# V | MUC | PM | stream | PM
|
||||
# --------------+-------------+-----+----+--------+----
|
||||
# | other sender| | x | |
|
||||
# personal mode +-------------+-----+----+--------+----
|
||||
# | self sender | | x | x | x
|
||||
# ------------- +-------------+-----+----+--------+----
|
||||
# | other sender| x | | |
|
||||
# public mode +-------------+-----+----+--------+----
|
||||
# | self sender | | | |
|
||||
|
||||
import logging
|
||||
import threading
|
||||
import optparse
|
||||
|
||||
from sleekxmpp import ClientXMPP, InvalidJID, JID
|
||||
from sleekxmpp.exceptions import IqError, IqTimeout
|
||||
from six.moves.configparser import SafeConfigParser
|
||||
import os, sys, zulip, getpass
|
||||
import re
|
||||
|
||||
__version__ = "1.1"
|
||||
|
||||
def room_to_stream(room):
|
||||
return room + "/xmpp"
|
||||
|
||||
def stream_to_room(stream):
|
||||
return stream.lower().rpartition("/xmpp")[0]
|
||||
|
||||
def jid_to_zulip(jid):
|
||||
suffix = ''
|
||||
if not jid.username.endswith("-bot"):
|
||||
suffix = options.zulip_email_suffix
|
||||
return "%s%s@%s" % (jid.username, suffix, options.zulip_domain)
|
||||
|
||||
def zulip_to_jid(email, jabber_domain):
|
||||
jid = JID(email, domain=jabber_domain)
|
||||
if (options.zulip_email_suffix
|
||||
and options.zulip_email_suffix in jid.username
|
||||
and not jid.username.endswith("-bot")):
|
||||
jid.username = jid.username.rpartition(options.zulip_email_suffix)[0]
|
||||
return jid
|
||||
|
||||
class JabberToZulipBot(ClientXMPP):
|
||||
def __init__(self, jid, password, rooms):
|
||||
if jid.resource:
|
||||
self.nick = jid.resource
|
||||
else:
|
||||
self.nick = jid.username
|
||||
jid.resource = "zulip"
|
||||
ClientXMPP.__init__(self, jid, password)
|
||||
self.rooms = set()
|
||||
self.rooms_to_join = rooms
|
||||
self.add_event_handler("session_start", self.session_start)
|
||||
self.add_event_handler("message", self.message)
|
||||
self.zulip = None
|
||||
self.use_ipv6 = False
|
||||
|
||||
self.register_plugin('xep_0045') # Jabber chatrooms
|
||||
self.register_plugin('xep_0199') # XMPP Ping
|
||||
|
||||
def set_zulip_client(self, client):
|
||||
self.zulip = client
|
||||
|
||||
def session_start(self, event):
|
||||
self.get_roster()
|
||||
self.send_presence()
|
||||
for room in self.rooms_to_join:
|
||||
self.join_muc(room)
|
||||
|
||||
def join_muc(self, room):
|
||||
if room in self.rooms:
|
||||
return
|
||||
logging.debug("Joining " + room)
|
||||
self.rooms.add(room)
|
||||
muc_jid = JID(local=room, domain=options.conference_domain)
|
||||
xep0045 = self.plugin['xep_0045']
|
||||
try:
|
||||
xep0045.joinMUC(muc_jid, self.nick, wait=True)
|
||||
except InvalidJID:
|
||||
logging.error("Could not join room: " + str(muc_jid))
|
||||
return
|
||||
|
||||
# Configure the room. Really, we should only do this if the room is
|
||||
# newly created.
|
||||
form = None
|
||||
try:
|
||||
form = xep0045.getRoomConfig(muc_jid)
|
||||
except ValueError:
|
||||
pass
|
||||
if form:
|
||||
xep0045.configureRoom(muc_jid, form)
|
||||
else:
|
||||
logging.error("Could not configure room: " + str(muc_jid))
|
||||
|
||||
def leave_muc(self, room):
|
||||
if room not in self.rooms:
|
||||
return
|
||||
logging.debug("Leaving " + room)
|
||||
self.rooms.remove(room)
|
||||
muc_jid = JID(local=room, domain=options.conference_domain)
|
||||
self.plugin['xep_0045'].leaveMUC(muc_jid, self.nick)
|
||||
|
||||
def message(self, msg):
|
||||
try:
|
||||
if msg["type"] == "groupchat":
|
||||
return self.group(msg)
|
||||
elif msg["type"] == "chat":
|
||||
return self.private(msg)
|
||||
else:
|
||||
logging.warning("Got unexpected message type")
|
||||
logging.warning(msg)
|
||||
except Exception:
|
||||
logging.exception("Error forwarding Jabber => Zulip")
|
||||
|
||||
def private(self, msg):
|
||||
if options.mode == 'public' or msg['thread'] == u'\u1FFFE':
|
||||
return
|
||||
sender = jid_to_zulip(msg["from"])
|
||||
recipient = jid_to_zulip(msg["to"])
|
||||
|
||||
zulip_message = dict(
|
||||
sender = sender,
|
||||
type = "private",
|
||||
to = recipient,
|
||||
content = msg["body"],
|
||||
)
|
||||
ret = self.zulip.client.send_message(zulip_message)
|
||||
if ret.get("result") != "success":
|
||||
logging.error(ret)
|
||||
|
||||
def group(self, msg):
|
||||
if options.mode == 'personal' or msg["thread"] == u'\u1FFFE':
|
||||
return
|
||||
|
||||
subject = msg["subject"]
|
||||
if len(subject) == 0:
|
||||
subject = "(no topic)"
|
||||
stream = room_to_stream(msg['from'].local)
|
||||
sender_nick = msg.get_mucnick()
|
||||
if not sender_nick:
|
||||
# Messages from the room itself have no nickname. We should not try
|
||||
# to mirror these
|
||||
return
|
||||
jid = self.nickname_to_jid(msg.get_mucroom(), sender_nick)
|
||||
sender = jid_to_zulip(jid)
|
||||
zulip_message = dict(
|
||||
forged = "yes",
|
||||
sender = sender,
|
||||
type = "stream",
|
||||
subject = subject,
|
||||
to = stream,
|
||||
content = msg["body"],
|
||||
)
|
||||
ret = self.zulip.client.send_message(zulip_message)
|
||||
if ret.get("result") != "success":
|
||||
logging.error(ret)
|
||||
|
||||
def nickname_to_jid(self, room, nick):
|
||||
jid = self.plugin['xep_0045'].getJidProperty(room, nick, "jid")
|
||||
if (jid is None or jid == ''):
|
||||
return JID(local=nick.replace(' ', ''), domain=self.boundjid.domain)
|
||||
else:
|
||||
return jid
|
||||
|
||||
class ZulipToJabberBot(object):
|
||||
def __init__(self, zulip_client):
|
||||
self.client = zulip_client
|
||||
self.jabber = None
|
||||
|
||||
def set_jabber_client(self, client):
|
||||
self.jabber = client
|
||||
|
||||
def process_event(self, event):
|
||||
if event['type'] == 'message':
|
||||
message = event["message"]
|
||||
if message['sender_email'] != self.client.email:
|
||||
return
|
||||
|
||||
try:
|
||||
if message['type'] == 'stream':
|
||||
self.stream_message(message)
|
||||
elif message['type'] == 'private':
|
||||
self.private_message(message)
|
||||
except:
|
||||
logging.exception("Exception forwarding Zulip => Jabber")
|
||||
elif event['type'] == 'subscription':
|
||||
self.process_subscription(event)
|
||||
elif event['type'] == 'stream':
|
||||
self.process_stream(event)
|
||||
|
||||
def stream_message(self, msg):
|
||||
stream = msg['display_recipient']
|
||||
if not stream.endswith("/xmpp"):
|
||||
return
|
||||
|
||||
room = stream_to_room(stream)
|
||||
jabber_recipient = JID(local=room, domain=options.conference_domain)
|
||||
outgoing = self.jabber.make_message(
|
||||
mto = jabber_recipient,
|
||||
mbody = msg['content'],
|
||||
mtype = 'groupchat')
|
||||
outgoing['thread'] = u'\u1FFFE'
|
||||
outgoing.send()
|
||||
|
||||
def private_message(self, msg):
|
||||
for recipient in msg['display_recipient']:
|
||||
if recipient["email"] == self.client.email:
|
||||
continue
|
||||
if not recipient["is_mirror_dummy"]:
|
||||
continue
|
||||
recip_email = recipient['email']
|
||||
jabber_recipient = zulip_to_jid(recip_email, self.jabber.boundjid.domain)
|
||||
outgoing = self.jabber.make_message(
|
||||
mto = jabber_recipient,
|
||||
mbody = msg['content'],
|
||||
mtype = 'chat')
|
||||
outgoing['thread'] = u'\u1FFFE'
|
||||
outgoing.send()
|
||||
|
||||
def process_subscription(self, event):
|
||||
if event['op'] == 'add':
|
||||
streams = [s['name'].lower() for s in event['subscriptions']]
|
||||
streams = [s for s in streams if s.endswith("/xmpp")]
|
||||
for stream in streams:
|
||||
self.jabber.join_muc(stream_to_room(stream))
|
||||
if event['op'] == 'remove':
|
||||
streams = [s['name'].lower() for s in event['subscriptions']]
|
||||
streams = [s for s in streams if s.endswith("/xmpp")]
|
||||
for stream in streams:
|
||||
self.jabber.leave_muc(stream_to_room(stream))
|
||||
|
||||
def process_stream(self, event):
|
||||
if event['op'] == 'occupy':
|
||||
streams = [s['name'].lower() for s in event['streams']]
|
||||
streams = [s for s in streams if s.endswith("/xmpp")]
|
||||
for stream in streams:
|
||||
self.jabber.join_muc(stream_to_room(stream))
|
||||
if event['op'] == 'vacate':
|
||||
streams = [s['name'].lower() for s in event['streams']]
|
||||
streams = [s for s in streams if s.endswith("/xmpp")]
|
||||
for stream in streams:
|
||||
self.jabber.leave_muc(stream_to_room(stream))
|
||||
|
||||
def get_rooms(zulip):
|
||||
def get_stream_infos(key, method):
|
||||
ret = method()
|
||||
if ret.get("result") != "success":
|
||||
logging.error(ret)
|
||||
sys.exit("Could not get initial list of Zulip %s" % (key,))
|
||||
return ret[key]
|
||||
|
||||
if options.mode == 'public':
|
||||
stream_infos = get_stream_infos("streams", zulip.client.get_streams)
|
||||
else:
|
||||
stream_infos = get_stream_infos("subscriptions", zulip.client.list_subscriptions)
|
||||
|
||||
rooms = []
|
||||
for stream_info in stream_infos:
|
||||
stream = stream_info['name']
|
||||
if stream.endswith("/xmpp"):
|
||||
rooms.append(stream_to_room(stream))
|
||||
return rooms
|
||||
|
||||
def config_error(msg):
|
||||
sys.stderr.write("%s\n" % (msg,))
|
||||
sys.exit(2)
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = optparse.OptionParser(epilog=
|
||||
'''Most general and Jabber configuration options may also be specified in the
|
||||
zulip configuration file under the jabber_mirror section (exceptions are noted
|
||||
in their help sections). Keys have the same name as options with hyphens
|
||||
replaced with underscores. Zulip configuration options go in the api section,
|
||||
as normal.'''.replace("\n", " ")
|
||||
)
|
||||
parser.add_option('--mode',
|
||||
default=None,
|
||||
action='store',
|
||||
help= \
|
||||
'''Which mode to run in. Valid options are "personal" and "public". In
|
||||
"personal" mode, the mirror uses an individual users' credentials and mirrors
|
||||
all messages they send on Zulip to Jabber and all private Jabber messages to
|
||||
Zulip. In "public" mode, the mirror uses the credentials for a dedicated mirror
|
||||
user and mirrors messages sent to Jabber rooms to Zulip. Defaults to
|
||||
"personal"'''.replace("\n", " "))
|
||||
parser.add_option('--zulip-email-suffix',
|
||||
default=None,
|
||||
action='store',
|
||||
help= \
|
||||
'''Add the specified suffix to the local part of email addresses constructed
|
||||
from JIDs and nicks before sending requests to the Zulip server, and remove the
|
||||
suffix before sending requests to the Jabber server. For example, specifying
|
||||
"+foo" will cause messages that are sent to the "bar" room by nickname "qux" to
|
||||
be mirrored to the "bar/xmpp" stream in Zulip by user "qux+foo@example.com". This
|
||||
option does not affect login credentials.'''.replace("\n", " "))
|
||||
parser.add_option('-d', '--debug',
|
||||
help='set logging to DEBUG. Can not be set via config file.',
|
||||
action='store_const',
|
||||
dest='log_level',
|
||||
const=logging.DEBUG,
|
||||
default=logging.INFO)
|
||||
|
||||
jabber_group = optparse.OptionGroup(parser, "Jabber configuration")
|
||||
jabber_group.add_option('--jid',
|
||||
default=None,
|
||||
action='store',
|
||||
help="Your Jabber JID. If a resource is specified, "
|
||||
+ "it will be used as the nickname when joining MUCs. "
|
||||
+ "Specifying the nickname is mostly useful if you want "
|
||||
+ "to run the public mirror from a regular user instead of "
|
||||
+ "from a dedicated account.")
|
||||
jabber_group.add_option('--jabber-password',
|
||||
default=None,
|
||||
action='store',
|
||||
help="Your Jabber password")
|
||||
jabber_group.add_option('--conference-domain',
|
||||
default=None,
|
||||
action='store',
|
||||
help="Your Jabber conference domain (E.g. conference.jabber.example.com). "
|
||||
+ "If not specifed, \"conference.\" will be prepended to your JID's domain.")
|
||||
jabber_group.add_option('--no-use-tls',
|
||||
default=None,
|
||||
action='store_true')
|
||||
jabber_group.add_option('--jabber-server-address',
|
||||
default=None,
|
||||
action='store',
|
||||
help="The hostname of your Jabber server. This is only needed if "
|
||||
"your server is missing SRV records")
|
||||
jabber_group.add_option('--jabber-server-port',
|
||||
default='5222',
|
||||
action='store',
|
||||
help="The port of your Jabber server. This is only needed if "
|
||||
"your server is missing SRV records")
|
||||
|
||||
parser.add_option_group(jabber_group)
|
||||
parser.add_option_group(zulip.generate_option_group(parser, "zulip-"))
|
||||
(options, args) = parser.parse_args()
|
||||
|
||||
logging.basicConfig(level=options.log_level,
|
||||
format='%(levelname)-8s %(message)s')
|
||||
|
||||
if options.zulip_config_file is None:
|
||||
config_file = zulip.get_default_config_filename()
|
||||
else:
|
||||
config_file = options.zulip_config_file
|
||||
|
||||
config = SafeConfigParser()
|
||||
try:
|
||||
with file(config_file, 'r') as f:
|
||||
config.readfp(f, config_file)
|
||||
except IOError:
|
||||
pass
|
||||
for option in ("jid", "jabber_password", "conference_domain", "mode", "zulip_email_suffix", "jabber_server_address", "jabber_server_port"):
|
||||
if (getattr(options, option) is None
|
||||
and config.has_option("jabber_mirror", option)):
|
||||
setattr(options, option, config.get("jabber_mirror", option))
|
||||
|
||||
for option in ("no_use_tls",):
|
||||
if getattr(options, option) is None:
|
||||
if config.has_option("jabber_mirror", option):
|
||||
setattr(options, option, config.getboolean("jabber_mirror", option))
|
||||
else:
|
||||
setattr(options, option, False)
|
||||
|
||||
if options.mode is None:
|
||||
options.mode = "personal"
|
||||
|
||||
if options.zulip_email_suffix is None:
|
||||
options.zulip_email_suffix = ''
|
||||
|
||||
if options.mode not in ('public', 'personal'):
|
||||
config_error("Bad value for --mode: must be one of 'public' or 'personal'")
|
||||
|
||||
if None in (options.jid, options.jabber_password):
|
||||
config_error("You must specify your Jabber JID and Jabber password either "
|
||||
+ "in the Zulip configuration file or on the commandline")
|
||||
|
||||
zulip = ZulipToJabberBot(zulip.init_from_options(options, "JabberMirror/" + __version__))
|
||||
# This won't work for open realms that don't have a consistent domain
|
||||
options.zulip_domain = zulip.client.email.partition('@')[-1]
|
||||
|
||||
try:
|
||||
jid = JID(options.jid)
|
||||
except InvalidJID as e:
|
||||
config_error("Bad JID: %s: %s" % (options.jid, e.message))
|
||||
|
||||
if options.conference_domain is None:
|
||||
options.conference_domain = "conference.%s" % (jid.domain,)
|
||||
|
||||
xmpp = JabberToZulipBot(jid, options.jabber_password, get_rooms(zulip))
|
||||
|
||||
address = None
|
||||
if options.jabber_server_address:
|
||||
address = (options.jabber_server_address, options.jabber_server_port)
|
||||
|
||||
if not xmpp.connect(use_tls=not options.no_use_tls, address=address):
|
||||
sys.exit("Unable to connect to Jabber server")
|
||||
|
||||
xmpp.set_zulip_client(zulip)
|
||||
zulip.set_jabber_client(xmpp)
|
||||
|
||||
xmpp.process(block=False)
|
||||
if options.mode == 'public':
|
||||
event_types = ['stream']
|
||||
else:
|
||||
event_types = ['message', 'subscription']
|
||||
|
||||
try:
|
||||
logging.info("Connecting to Zulip.")
|
||||
zulip.client.call_on_each_event(zulip.process_event,
|
||||
event_types=event_types)
|
||||
except BaseException as e:
|
||||
logging.exception("Exception in main loop")
|
||||
xmpp.abort()
|
||||
sys.exit(1)
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/python
|
||||
import subprocess
|
||||
import os
|
||||
import sys
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/python
|
||||
import sys
|
||||
import subprocess
|
||||
import base64
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from __future__ import print_function
|
||||
# This is hacky code to analyze data on our support stream. The main
|
||||
# reusable bits are get_recent_messages and get_words.
|
||||
|
||||
@@ -32,7 +31,7 @@ def analyze_messages(msgs, word_count, email_count):
|
||||
if False:
|
||||
if ' ack' in msg['content']:
|
||||
name = msg['sender_full_name'].split()[0]
|
||||
print('ACK', name)
|
||||
print 'ACK', name
|
||||
m = re.search('ticket (Z....).*email: (\S+).*~~~(.*)', msg['content'], re.M | re.S)
|
||||
if m:
|
||||
ticket, email, req = m.groups()
|
||||
@@ -41,9 +40,9 @@ def analyze_messages(msgs, word_count, email_count):
|
||||
word_count[word] += 1
|
||||
email_count[email] += 1
|
||||
if False:
|
||||
print()
|
||||
print
|
||||
for k, v in msg.items():
|
||||
print('%-20s: %s' % (k, v))
|
||||
print '%-20s: %s' % (k, v)
|
||||
|
||||
def generate_support_stats():
|
||||
client = zulip.Client()
|
||||
@@ -65,16 +64,16 @@ def generate_support_stats():
|
||||
|
||||
if True:
|
||||
words = word_count.keys()
|
||||
words = [w for w in words if word_count[w] >= 10]
|
||||
words = [w for w in words if len(w) >= 5]
|
||||
words = filter(lambda w: word_count[w] >= 10, words)
|
||||
words = filter(lambda w: len(w) >= 5, words)
|
||||
words = sorted(words, key=lambda w: word_count[w], reverse=True)
|
||||
for word in words:
|
||||
print(word, word_count[word])
|
||||
print word, word_count[word]
|
||||
|
||||
if False:
|
||||
emails = email_count.keys()
|
||||
emails = sorted(emails, key=lambda w: email_count[w], reverse=True)
|
||||
for email in emails:
|
||||
print(email, email_count[email])
|
||||
print email, email_count[email]
|
||||
|
||||
generate_support_stats()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
import sys
|
||||
import os
|
||||
import logging
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
# Copyright (C) 2012 Zulip, Inc.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
# Copyright (C) 2012 Zulip, Inc.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person
|
||||
@@ -21,15 +21,14 @@
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import print_function
|
||||
import sys
|
||||
import subprocess
|
||||
import os
|
||||
import traceback
|
||||
import signal
|
||||
|
||||
from .zephyr_mirror_backend import parse_args
|
||||
from zephyr_mirror_backend import parse_args
|
||||
from zephyr_mirror_backend import RandomExponentialBackoff
|
||||
|
||||
def die(signal, frame):
|
||||
# We actually want to exit, so run os._exit (so as not to be caught and restarted)
|
||||
@@ -39,9 +38,6 @@ signal.signal(signal.SIGINT, die)
|
||||
|
||||
(options, args) = parse_args()
|
||||
|
||||
sys.path[:0] = [os.path.join(options.root_path, 'api')]
|
||||
from zulip import RandomExponentialBackoff
|
||||
|
||||
args = [os.path.join(options.root_path, "user_root", "zephyr_mirror_backend.py")]
|
||||
args.extend(sys.argv[1:])
|
||||
|
||||
@@ -54,30 +50,30 @@ if options.forward_class_messages and not options.noshard:
|
||||
if options.on_startup_command is not None:
|
||||
subprocess.call([options.on_startup_command])
|
||||
from zerver.lib.parallel import run_parallel
|
||||
print("Starting parallel zephyr class mirroring bot")
|
||||
print "Starting parallel zephyr class mirroring bot"
|
||||
jobs = list("0123456789abcdef")
|
||||
def run_job(shard):
|
||||
subprocess.call(args + ["--shard=%s" % (shard,)])
|
||||
return 0
|
||||
for (status, job) in run_parallel(run_job, jobs, threads=16):
|
||||
print("A mirroring shard died!")
|
||||
print "A mirroring shard died!"
|
||||
pass
|
||||
sys.exit(0)
|
||||
|
||||
backoff = RandomExponentialBackoff(timeout_success_equivalent=300)
|
||||
backoff = RandomExponentialBackoff()
|
||||
while backoff.keep_going():
|
||||
print("Starting zephyr mirroring bot")
|
||||
print "Starting zephyr mirroring bot"
|
||||
try:
|
||||
subprocess.call(args)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
backoff.fail()
|
||||
|
||||
print("")
|
||||
print("")
|
||||
print("ERROR: The Zephyr mirroring bot is unable to continue mirroring Zephyrs.")
|
||||
print("This is often caused by failing to maintain unexpired Kerberos tickets")
|
||||
print("or AFS tokens. See https://zulip.com/zephyr for documentation on how to")
|
||||
print("maintain unexpired Kerberos tickets and AFS tokens.")
|
||||
print("")
|
||||
print ""
|
||||
print ""
|
||||
print "ERROR: The Zephyr mirroring bot is unable to continue mirroring Zephyrs."
|
||||
print "This is often caused by failing to maintain unexpired Kerberos tickets"
|
||||
print "or AFS tokens. See https://zulip.com/zephyr for documentation on how to"
|
||||
print "maintain unexpired Kerberos tickets and AFS tokens."
|
||||
print ""
|
||||
sys.exit(1)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python2.7
|
||||
#!/usr/bin/env python
|
||||
# Copyright (C) 2012 Zulip, Inc.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person
|
||||
@@ -20,11 +20,8 @@
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
from __future__ import absolute_import
|
||||
|
||||
import sys
|
||||
from six.moves import map
|
||||
from six.moves import range
|
||||
try:
|
||||
import simplejson
|
||||
except ImportError:
|
||||
@@ -40,12 +37,43 @@ import signal
|
||||
import logging
|
||||
import hashlib
|
||||
import tempfile
|
||||
import random
|
||||
import select
|
||||
|
||||
class CountingBackoff(object):
|
||||
def __init__(self, maximum_retries=10):
|
||||
self.number_of_retries = 0
|
||||
self.maximum_retries = maximum_retries
|
||||
|
||||
def keep_going(self):
|
||||
return self.number_of_retries < self.maximum_retries
|
||||
|
||||
def succeed(self):
|
||||
self.number_of_retries = 0
|
||||
|
||||
def fail(self):
|
||||
self.number_of_retries = min(self.number_of_retries + 1,
|
||||
self.maximum_retries)
|
||||
|
||||
class RandomExponentialBackoff(CountingBackoff):
|
||||
def fail(self):
|
||||
self.number_of_retries = min(self.number_of_retries + 1,
|
||||
self.maximum_retries)
|
||||
# Exponential growth with ratio sqrt(2); compute random delay
|
||||
# between x and 2x where x is growing exponentially
|
||||
delay_scale = int(2 ** (self.number_of_retries / 2.0 - 1)) + 1
|
||||
delay = delay_scale + random.randint(1, delay_scale)
|
||||
message = "Sleeping for %ss [max %s] before retrying." % (delay, delay_scale * 2)
|
||||
try:
|
||||
logger.warning(message)
|
||||
except NameError:
|
||||
print message
|
||||
time.sleep(delay)
|
||||
|
||||
DEFAULT_SITE = "https://api.zulip.com"
|
||||
|
||||
class States(object):
|
||||
Startup, ZulipToZephyr, ZephyrToZulip, ChildSending = list(range(4))
|
||||
class States:
|
||||
Startup, ZulipToZephyr, ZephyrToZulip, ChildSending = range(4)
|
||||
CURRENT_STATE = States.Startup
|
||||
|
||||
def to_zulip_username(zephyr_username):
|
||||
@@ -373,7 +401,7 @@ def process_notice(notice, log):
|
||||
if is_personal:
|
||||
if body.startswith("CC:"):
|
||||
is_huddle = True
|
||||
# Map "CC: user1 user2" => "user1@mit.edu, user2@mit.edu"
|
||||
# Map "CC: sipbtest espuser" => "starnine@mit.edu,espuser@mit.edu"
|
||||
huddle_recipients = [to_zulip_username(x.strip()) for x in
|
||||
body.split("\n")[0][4:].split()]
|
||||
if notice.sender not in huddle_recipients:
|
||||
@@ -450,7 +478,7 @@ def quit_failed_initialization(message):
|
||||
sys.exit(1)
|
||||
|
||||
def zephyr_init_autoretry():
|
||||
backoff = zulip.RandomExponentialBackoff()
|
||||
backoff = RandomExponentialBackoff()
|
||||
while backoff.keep_going():
|
||||
try:
|
||||
# zephyr.init() tries to clear old subscriptions, and thus
|
||||
@@ -465,7 +493,7 @@ def zephyr_init_autoretry():
|
||||
quit_failed_initialization("Could not initialize Zephyr library, quitting!")
|
||||
|
||||
def zephyr_load_session_autoretry(session_path):
|
||||
backoff = zulip.RandomExponentialBackoff()
|
||||
backoff = RandomExponentialBackoff()
|
||||
while backoff.keep_going():
|
||||
try:
|
||||
session = file(session_path, "r").read()
|
||||
@@ -480,7 +508,7 @@ def zephyr_load_session_autoretry(session_path):
|
||||
quit_failed_initialization("Could not load saved Zephyr session, quitting!")
|
||||
|
||||
def zephyr_subscribe_autoretry(sub):
|
||||
backoff = zulip.RandomExponentialBackoff()
|
||||
backoff = RandomExponentialBackoff()
|
||||
while backoff.keep_going():
|
||||
try:
|
||||
zephyr.Subscriptions().add(sub)
|
||||
@@ -601,9 +629,7 @@ Feedback button or at support@zulip.com."""
|
||||
wrapped_content = "\n".join("\n".join(wrapper.wrap(line))
|
||||
for line in message["content"].replace("@", "@@").split("\n"))
|
||||
|
||||
zwrite_args = ["zwrite", "-n", "-s", message["sender_full_name"],
|
||||
"-F", "Zephyr error: See http://zephyr.1ts.org/wiki/df",
|
||||
"-x", "UTF-8"]
|
||||
zwrite_args = ["zwrite", "-n", "-s", message["sender_full_name"], "-F", "Zephyr error: See http://zephyr.1ts.org/wiki/df"]
|
||||
|
||||
# Hack to make ctl's fake username setup work :)
|
||||
if message['type'] == "stream" and zulip_account_email == "ctl@mit.edu":
|
||||
@@ -737,7 +763,7 @@ def maybe_forward_to_zephyr(message):
|
||||
logger.exception("Error forwarding message:")
|
||||
|
||||
def zulip_to_zephyr(options):
|
||||
# Sync messages from zulip to zephyr
|
||||
# Sync messages from zephyr to zulip
|
||||
logger.info("Starting syncing messages.")
|
||||
while True:
|
||||
try:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[program:zmirror-USERNAME]
|
||||
command=python /home/zulip/zulip/bots/zephyr_mirror_backend.py --root-path=/home/zulip/zulip --user=USERNAME --log-path=/home/zulip/logs/mirror-log-%(program_name)s --use-sessions --session-path=/home/zulip/zephyr_sessions/%(program_name)s --api-key-file=/home/zulip/api-keys/%(program_name)s --ignore-expired-tickets --nagios-path=/home/zulip/mirror_status/%(program_name)s --nagios-class=zulip-mirror-nagios
|
||||
command=python /home/zulip/zulip/bots/zephyr_mirror_backend.py --root-path=/home/zulip/zulip/bots --user=USERNAME --log-path=/home/zulip/logs/mirror-log-%(program_name)s --use-sessions --session-path=/home/zulip/zephyr_sessions/%(program_name)s --api-key-file=/home/zulip/api-keys/%(program_name)s --ignore-expired-tickets --nagios-path=/home/zulip/mirror_status/%(program_name)s --nagios-class=zulip-mirror-nagios
|
||||
priority=200 ; the relative start priority (default 999)
|
||||
autostart=true ; start at supervisord start (default: true)
|
||||
autorestart=true ; whether/when to restart (default: unexpected)
|
||||
|
||||
@@ -39,4 +39,4 @@ def format_commit_message(author, subject, commit_id):
|
||||
return '!avatar(%s) [%s](https://git.zulip.net/eng/zulip/commit/%s)\n' % (author, subject, commit_id)
|
||||
|
||||
ZULIP_API_PATH = "/home/zulip/zulip/api"
|
||||
ZULIP_SITE = "https://zulip.com"
|
||||
ZULIP_SITE = "https://staging.zulip.com"
|
||||
|
||||
@@ -11,4 +11,4 @@ TRAC_BASE_TICKET_URL = "https://trac.zulip.net/ticket"
|
||||
|
||||
TRAC_NOTIFY_FIELDS = ["description", "summary", "resolution", "comment", "owner"]
|
||||
ZULIP_API_PATH = "/home/zulip/zulip/api"
|
||||
ZULIP_SITE = "https://zulip.com"
|
||||
ZULIP_SITE = "https://staging.zulip.com"
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
[api]
|
||||
email = nagios-bot@zulip.com
|
||||
key = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
site = https://zulip.com
|
||||
site = https://staging.zulip.com
|
||||
|
||||
31
changelog.md
31
changelog.md
@@ -1,31 +0,0 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
[Unreleased]
|
||||
|
||||
[1.3.9] - 2015-11-16
|
||||
- Fixed buggy #! lines in upgrade scripts.
|
||||
|
||||
[1.3.8] - 2015-11-15
|
||||
- Added options to the Python api for working with untrusted server certificates.
|
||||
- Added a lot of documentation on the development environment and testing.
|
||||
- Added partial support for translating the Zulip UI.
|
||||
- Migrated installing Node dependencies to use npm.
|
||||
- Fixed LDAP integration breaking autocomplete of @-mentions.
|
||||
- Fixed admin panel reactivation/deactivation of bots.
|
||||
- Fixed inaccurate documentation for downloading the desktop apps.
|
||||
- Fixed various minor bugs in production installation process.
|
||||
- Fixed security issue where recent history on private streams might
|
||||
be visible to new users (to the Zulip team) who were invited with that
|
||||
private stream as one of their initial streams
|
||||
(https://github.com/zulip/zulip/issues/230).
|
||||
- Major preliminary progress towards supporting Python 3.
|
||||
|
||||
[1.3.7] - 2015-10-19
|
||||
- Turn off desktop and audible notifications for streams by default.
|
||||
- Added support for the LDAP authentication integration creating new users.
|
||||
- Added new endpoint to support Google auth on mobile.
|
||||
- Fixed desktop notifications in modern Firefox.
|
||||
- Fixed several installation issues for both production and development environments.
|
||||
- Improved documentation for outgoing SMTP and the email mirror integration.
|
||||
@@ -1,29 +1,45 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0001_initial'),
|
||||
]
|
||||
def forwards(self, orm):
|
||||
# Adding model 'Confirmation'
|
||||
db.create_table('confirmation_confirmation', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('content_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'])),
|
||||
('object_id', self.gf('django.db.models.fields.PositiveIntegerField')()),
|
||||
('date_sent', self.gf('django.db.models.fields.DateTimeField')()),
|
||||
('confirmation_key', self.gf('django.db.models.fields.CharField')(max_length=40)),
|
||||
))
|
||||
db.send_create_signal('confirmation', ['Confirmation'])
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Confirmation',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('object_id', models.PositiveIntegerField()),
|
||||
('date_sent', models.DateTimeField(verbose_name='sent')),
|
||||
('confirmation_key', models.CharField(max_length=40, verbose_name='activation key')),
|
||||
('content_type', models.ForeignKey(to='contenttypes.ContentType')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'confirmation email',
|
||||
'verbose_name_plural': 'confirmation emails',
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
]
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting model 'Confirmation'
|
||||
db.delete_table('confirmation_confirmation')
|
||||
|
||||
|
||||
models = {
|
||||
'confirmation.confirmation': {
|
||||
'Meta': {'object_name': 'Confirmation'},
|
||||
'confirmation_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'date_sent': ('django.db.models.fields.DateTimeField', [], {}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['confirmation']
|
||||
@@ -34,10 +34,9 @@ def generate_key():
|
||||
return generate_random_token(40)
|
||||
|
||||
def generate_activation_url(key):
|
||||
return u'%s%s%s' % (settings.EXTERNAL_URI_SCHEME,
|
||||
settings.EXTERNAL_HOST,
|
||||
reverse('confirmation.views.confirm',
|
||||
kwargs={'confirmation_key': key}))
|
||||
current_site = Site.objects.get_current()
|
||||
return u'https://%s%s' % (current_site.domain,
|
||||
reverse('confirmation.views.confirm', kwargs={'confirmation_key': key}))
|
||||
|
||||
|
||||
class ConfirmationManager(models.Manager):
|
||||
@@ -49,7 +48,7 @@ class ConfirmationManager(models.Manager):
|
||||
except self.model.DoesNotExist:
|
||||
return False
|
||||
obj = confirmation.content_object
|
||||
status_field = get_status_field(obj._meta.app_label, obj._meta.model_name)
|
||||
status_field = get_status_field(obj._meta.app_label, obj._meta.module_name)
|
||||
setattr(obj, status_field, getattr(settings, 'STATUS_ACTIVE', 1))
|
||||
obj.save()
|
||||
return obj
|
||||
@@ -75,7 +74,7 @@ class ConfirmationManager(models.Manager):
|
||||
if additional_context is not None:
|
||||
context.update(additional_context)
|
||||
templates = [
|
||||
'confirmation/%s_confirmation_email_subject.txt' % obj._meta.model_name,
|
||||
'confirmation/%s_confirmation_email_subject.txt' % obj._meta.module_name,
|
||||
'confirmation/confirmation_email_subject.txt',
|
||||
]
|
||||
if subject_template_path:
|
||||
@@ -84,7 +83,7 @@ class ConfirmationManager(models.Manager):
|
||||
template = loader.select_template(templates)
|
||||
subject = template.render(context).strip().replace(u'\n', u' ') # no newlines, please
|
||||
templates = [
|
||||
'confirmation/%s_confirmation_email_body.txt' % obj._meta.model_name,
|
||||
'confirmation/%s_confirmation_email_body.txt' % obj._meta.module_name,
|
||||
'confirmation/confirmation_email_body.txt',
|
||||
]
|
||||
if body_template_path:
|
||||
@@ -105,7 +104,7 @@ class Confirmation(models.Model):
|
||||
|
||||
objects = ConfirmationManager()
|
||||
|
||||
class Meta(object):
|
||||
class Meta:
|
||||
verbose_name = _('confirmation email')
|
||||
verbose_name_plural = _('confirmation emails')
|
||||
|
||||
|
||||
@@ -31,13 +31,13 @@ def confirm(request, confirmation_key):
|
||||
'key': confirmation_key,
|
||||
'full_name': request.GET.get("full_name", None),
|
||||
'support_email': settings.ZULIP_ADMINISTRATOR,
|
||||
'voyager': settings.VOYAGER
|
||||
'enterprise': settings.ENTERPRISE
|
||||
}
|
||||
templates = [
|
||||
'confirmation/confirm.html',
|
||||
]
|
||||
if obj:
|
||||
# if we have an object, we can use specific template
|
||||
templates.insert(0, 'confirmation/confirm_%s.html' % obj._meta.model_name)
|
||||
templates.insert(0, 'confirmation/confirm_%s.html' % obj._meta.module_name)
|
||||
return render_to_response(templates, ctx,
|
||||
context_instance=RequestContext(request))
|
||||
|
||||
@@ -2,8 +2,20 @@ from django.conf.urls import patterns, url
|
||||
from django.views.generic import TemplateView, RedirectView
|
||||
|
||||
urlpatterns = patterns('',
|
||||
# Job postings
|
||||
url(r'^jobs/$', TemplateView.as_view(template_name='corporate/jobs/index.html')),
|
||||
url(r'^jobs/lead-designer/$', TemplateView.as_view(template_name='corporate/jobs/lead-designer.html')),
|
||||
|
||||
# Zephyr/MIT
|
||||
url(r'^zephyr/$', TemplateView.as_view(template_name='corporate/zephyr.html')),
|
||||
url(r'^mit/$', TemplateView.as_view(template_name='corporate/mit.html')),
|
||||
url(r'^zephyr-mirror/$', TemplateView.as_view(template_name='corporate/zephyr-mirror.html')),
|
||||
|
||||
# Marketing
|
||||
url(r'^compare/$', TemplateView.as_view(template_name='corporate/compare.html')),
|
||||
# signup form
|
||||
url(r'^signup/$', TemplateView.as_view(template_name='corporate/signup.html'),
|
||||
name='signup'),
|
||||
# TODO: The beta signup view should probably be moved to corporate.
|
||||
url(r'^signup/sign-me-up$', 'zerver.views.beta_signup_submission', name='beta-signup-submission'),
|
||||
)
|
||||
|
||||
1
docs/.gitignore
vendored
1
docs/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
_build
|
||||
192
docs/Makefile
192
docs/Makefile
@@ -1,192 +0,0 @@
|
||||
# Makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
PAPER =
|
||||
BUILDDIR = _build
|
||||
|
||||
# User-friendly check for sphinx-build
|
||||
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
|
||||
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
|
||||
endif
|
||||
|
||||
# Internal variables.
|
||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
# the i18n builder cannot share the environment and doctrees with the others
|
||||
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
|
||||
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext
|
||||
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " dirhtml to make HTML files named index.html in directories"
|
||||
@echo " singlehtml to make a single large HTML file"
|
||||
@echo " pickle to make pickle files"
|
||||
@echo " json to make JSON files"
|
||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||
@echo " qthelp to make HTML files and a qthelp project"
|
||||
@echo " applehelp to make an Apple Help Book"
|
||||
@echo " devhelp to make HTML files and a Devhelp project"
|
||||
@echo " epub to make an epub"
|
||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
|
||||
@echo " text to make text files"
|
||||
@echo " man to make manual pages"
|
||||
@echo " texinfo to make Texinfo files"
|
||||
@echo " info to make Texinfo files and run them through makeinfo"
|
||||
@echo " gettext to make PO message catalogs"
|
||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||
@echo " xml to make Docutils-native XML files"
|
||||
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
@echo " coverage to run coverage check of the documentation (if enabled)"
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILDDIR)/*
|
||||
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
|
||||
dirhtml:
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||
|
||||
singlehtml:
|
||||
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||
|
||||
pickle:
|
||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||
@echo
|
||||
@echo "Build finished; now you can process the pickle files."
|
||||
|
||||
json:
|
||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||
@echo
|
||||
@echo "Build finished; now you can process the JSON files."
|
||||
|
||||
htmlhelp:
|
||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||
|
||||
qthelp:
|
||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/zulip-contributor-docs.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/zulip-contributor-docs.qhc"
|
||||
|
||||
applehelp:
|
||||
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
|
||||
@echo
|
||||
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
|
||||
@echo "N.B. You won't be able to view it unless you put it in" \
|
||||
"~/Library/Documentation/Help or install it in your application" \
|
||||
"bundle."
|
||||
|
||||
devhelp:
|
||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||
@echo
|
||||
@echo "Build finished."
|
||||
@echo "To view the help file:"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/zulip-contributor-docs"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/zulip-contributor-docs"
|
||||
@echo "# devhelp"
|
||||
|
||||
epub:
|
||||
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||
@echo
|
||||
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||
|
||||
latex:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo
|
||||
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||
"(use \`make latexpdf' here to do that automatically)."
|
||||
|
||||
latexpdf:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through pdflatex..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
latexpdfja:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through platex and dvipdfmx..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
text:
|
||||
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||
@echo
|
||||
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||
|
||||
man:
|
||||
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||
@echo
|
||||
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||
|
||||
texinfo:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo
|
||||
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||
"(use \`make info' here to do that automatically)."
|
||||
|
||||
info:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo "Running Texinfo files through makeinfo..."
|
||||
make -C $(BUILDDIR)/texinfo info
|
||||
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||
|
||||
gettext:
|
||||
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||
@echo
|
||||
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||
|
||||
changes:
|
||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||
@echo
|
||||
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||
|
||||
linkcheck:
|
||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||
@echo
|
||||
@echo "Link check complete; look for any errors in the above output " \
|
||||
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||
|
||||
doctest:
|
||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||
@echo "Testing of doctests in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/doctest/output.txt."
|
||||
|
||||
coverage:
|
||||
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
|
||||
@echo "Testing of coverage in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/coverage/python.txt."
|
||||
|
||||
xml:
|
||||
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
|
||||
@echo
|
||||
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
|
||||
|
||||
pseudoxml:
|
||||
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
|
||||
@echo
|
||||
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
|
||||
@@ -1,23 +0,0 @@
|
||||
These docs are written in rST, and are included on the zulip.org website
|
||||
as well as on each development installation. Many of these docs
|
||||
have been ported from the internal docs of Zulip Inc.,
|
||||
and may need to be updated for use in the open source project.
|
||||
|
||||
To generate HTML docs locally from rST:
|
||||
|
||||
* `pip install sphinx`
|
||||
* In this directory, `make html`. Output appears in a `_build/html` subdirectory.
|
||||
|
||||
To create rST from MediaWiki input:
|
||||
|
||||
* Use `pandoc -r mediawiki -w rst` on MediaWiki source.
|
||||
* Use unescape.py to remove any leftover HTML entities (often inside <pre>
|
||||
tags and the like).
|
||||
|
||||
We can use pandoc to translate mediawiki into reStructuredText, but some things need fixing up:
|
||||
|
||||
* Add page titles.
|
||||
* Review pages for formatting (especially inline code chunks) and content.
|
||||
* Fix wiki links?
|
||||
* Add pages to the table of contents (`index.rst`).
|
||||
|
||||
10
docs/_static/theme_overrides.css
vendored
10
docs/_static/theme_overrides.css
vendored
@@ -1,10 +0,0 @@
|
||||
/* override table width restrictions */
|
||||
.wy-table-responsive table td, .wy-table-responsive table th {
|
||||
/* !important prevents the common CSS stylesheets from
|
||||
overriding this as on RTD they are loaded after this stylesheet */
|
||||
white-space: normal !important;
|
||||
}
|
||||
|
||||
.wy-table-responsive {
|
||||
overflow: visible !important;
|
||||
}
|
||||
@@ -1,484 +0,0 @@
|
||||
==========================
|
||||
Code style and conventions
|
||||
==========================
|
||||
|
||||
Be consistent!
|
||||
==============
|
||||
|
||||
Look at the surrounding code, or a similar part of the project, and
|
||||
try to do the same thing. If you think the other code has actively bad
|
||||
style, fix it (in a separate commit).
|
||||
|
||||
When in doubt, send an email to zulip-devel@googlegroups.com with your
|
||||
question.
|
||||
|
||||
Lint tools
|
||||
==========
|
||||
|
||||
You can run them all at once with
|
||||
|
||||
::
|
||||
|
||||
./tools/lint-all
|
||||
|
||||
You can set this up as a local Git commit hook with
|
||||
|
||||
::
|
||||
|
||||
``tools/setup-git-repo``
|
||||
|
||||
The Vagrant setup process runs this for you.
|
||||
|
||||
``lint-all`` runs many lint checks in parallel, including
|
||||
|
||||
- Javascript (`JSLint <http://www.jslint.com/>`__)
|
||||
|
||||
``tools/jslint/check-all.js`` contains a pretty fine-grained set of
|
||||
JSLint options, rule exceptions, and allowed global variables. If you
|
||||
add a new global, you'll need to add it to the list.
|
||||
|
||||
- Python (`Pyflakes <http://pypi.python.org/pypi/pyflakes>`__)
|
||||
- templates
|
||||
- Puppet configuration
|
||||
- custom checks (e.g. trailing whitespace and spaces-not-tabs)
|
||||
|
||||
Secrets
|
||||
=======
|
||||
|
||||
Please don't put any passwords, secret access keys, etc. inline in the
|
||||
code. Instead, use the ``get_secret`` function in
|
||||
``zproject/settings.py`` to read secrets from ``/etc/zulip/secrets.conf``.
|
||||
|
||||
Dangerous constructs
|
||||
====================
|
||||
|
||||
Misuse of database queries
|
||||
--------------------------
|
||||
|
||||
Look out for Django code like this::
|
||||
|
||||
[Foo.objects.get(id=bar.x.id)
|
||||
for bar in Bar.objects.filter(...)
|
||||
if bar.baz < 7]
|
||||
|
||||
This will make one database query for each ``Bar``, which is slow in
|
||||
production (but not in local testing!). Instead of a list comprehension,
|
||||
write a single query using Django's `QuerySet
|
||||
API <https://docs.djangoproject.com/en/dev/ref/models/querysets/>`__.
|
||||
|
||||
If you can't rewrite it as a single query, that's a sign that something
|
||||
is wrong with the database schema. So don't defer this optimization when
|
||||
performing schema changes, or else you may later find that it's
|
||||
impossible.
|
||||
|
||||
UserProfile.objects.get() / Client.objects.get / etc.
|
||||
-----------------------------------------------------
|
||||
|
||||
In our Django code, never do direct
|
||||
``UserProfile.objects.get(email=foo)`` database queries. Instead always
|
||||
use ``get_user_profile_by_{email,id}``. There are 3 reasons for this:
|
||||
|
||||
#. It's guaranteed to correctly do a case-inexact lookup
|
||||
#. It fetches the user object from memcached, which is faster
|
||||
#. It always fetches a UserProfile object which has been queried using
|
||||
.selected\_related(), and thus will perform well when one later
|
||||
accesses related models like the Realm.
|
||||
|
||||
Similarly we have ``get_client`` and ``get_stream`` functions to fetch
|
||||
those commonly accessed objects via memcached.
|
||||
|
||||
Using Django model objects as keys in sets/dicts
|
||||
------------------------------------------------
|
||||
|
||||
Don't use Django model objects as keys in sets/dictionaries -- you will
|
||||
get unexpected behavior when dealing with objects obtained from
|
||||
different database queries:
|
||||
|
||||
For example,
|
||||
``UserProfile.objects.only("id").get(id=17) in set([UserProfile.objects.get(id=17)])``
|
||||
is False
|
||||
|
||||
You should work with the IDs instead.
|
||||
|
||||
user\_profile.save()
|
||||
--------------------
|
||||
|
||||
You should always pass the update\_fields keyword argument to .save()
|
||||
when modifying an existing Django model object. By default, .save() will
|
||||
overwrite every value in the column, which results in lots of race
|
||||
conditions where unrelated changes made by one thread can be
|
||||
accidentally overwritten by another thread that fetched its UserProfile
|
||||
object before the first thread wrote out its change.
|
||||
|
||||
Using raw saves to update important model objects
|
||||
-------------------------------------------------
|
||||
|
||||
In most cases, we already have a function in zephyr/lib/actions.py with
|
||||
a name like do\_activate\_user that will correctly handle lookups,
|
||||
caching, and notifying running browsers via the event system about your
|
||||
change. So please check whether such a function exists before writing
|
||||
new code to modify a model object, since your new code has a good chance
|
||||
of getting at least one of these things wrong.
|
||||
|
||||
``x.attr('zid')`` vs. ``rows.id(x)``
|
||||
------------------------------------
|
||||
|
||||
Our message row DOM elements have a custom attribute ``zid`` which
|
||||
contains the numerical message ID. **Don't access this directly as**
|
||||
``x.attr('zid')`` ! The result will be a string and comparisons (e.g.
|
||||
with ``<=``) will give the wrong result, occasionally, just enough to
|
||||
make a bug that's impossible to track down.
|
||||
|
||||
You should instead use the ``id`` function from the ``rows`` module, as
|
||||
in ``rows.id(x)``. This returns a number. Even in cases where you do
|
||||
want a string, use the ``id`` function, as it will simplify future code
|
||||
changes. In most contexts in JavaScript where a string is needed, you
|
||||
can pass a number without any explicit conversion.
|
||||
|
||||
Javascript var
|
||||
--------------
|
||||
|
||||
Always declare Javascript variables using ``var``::
|
||||
|
||||
var x = ...;
|
||||
|
||||
In a function, ``var`` is necessary or else ``x`` will be a global
|
||||
variable. For variables declared at global scope, this has no effect,
|
||||
but we do it for consistency.
|
||||
|
||||
Javascript has function scope only, not block scope. This means that a
|
||||
``var`` declaration inside a ``for`` or ``if`` acts the same as a
|
||||
``var`` declaration at the beginning of the surrounding ``function``. To
|
||||
avoid confusion, declare all variables at the top of a function.
|
||||
|
||||
Javascript ``for (i in myArray)``
|
||||
---------------------------------
|
||||
|
||||
Don't use it:
|
||||
`[1] <http://stackoverflow.com/questions/500504/javascript-for-in-with-arrays>`__,
|
||||
`[2] <http://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml#for-in_loop>`__,
|
||||
`[3] <http://www.jslint.com/lint.html#forin>`__
|
||||
|
||||
jQuery global state
|
||||
-------------------
|
||||
|
||||
Don't mess with jQuery global state once the app has loaded. Code like
|
||||
this is very dangerous::
|
||||
|
||||
$.ajaxSetup({ async: false });
|
||||
$.get(...);
|
||||
$.ajaxSetup({ async: true });
|
||||
|
||||
jQuery and the browser are free to run other code while the request is
|
||||
pending, which could perform other Ajax requests with the altered
|
||||
settings.
|
||||
|
||||
Instead, switch to the more general |ajax|_ function, which can take options
|
||||
like ``async``.
|
||||
|
||||
.. |ajax| replace:: ``$.ajax``
|
||||
.. _ajax: http://api.jquery.com/jQuery.ajax
|
||||
|
||||
State and logs files
|
||||
--------------------
|
||||
|
||||
Do not write state and logs files inside the current working directory
|
||||
in the production environment. This will not how you expect, because the
|
||||
current working directory for the app changes every time we do a deploy.
|
||||
Instead, hardcode a path in settings.py -- see SERVER\_LOG\_PATH in
|
||||
settings.py for an example.
|
||||
|
||||
JS array/object manipulation
|
||||
============================
|
||||
|
||||
For generic functions that operate on arrays or JavaScript objects, you
|
||||
should generally use `Underscore <http://underscorejs.org/>`__. We used
|
||||
to use jQuery's utility functions, but the Underscore equivalents are
|
||||
more consistent, better-behaved and offer more choices.
|
||||
|
||||
A quick conversion table::
|
||||
|
||||
$.each → _.each (parameters to the callback reversed)
|
||||
$.inArray → _.indexOf (parameters reversed)
|
||||
$.grep → _.filter
|
||||
$.map → _.map
|
||||
$.extend → _.extend
|
||||
|
||||
There's a subtle difference in the case of ``_.extend``; it will replace
|
||||
attributes with undefined, whereas jQuery won't::
|
||||
|
||||
$.extend({foo: 2}, {foo: undefined}); // yields {foo: 2}, BUT...
|
||||
_.extend({foo: 2}, {foo: undefined}); // yields {foo: undefined}!
|
||||
|
||||
Also, ``_.each`` does not let you break out of the iteration early by
|
||||
returning false, the way jQuery's version does. If you're doing this,
|
||||
you probably want ``_.find``, ``_.every``, or ``_.any``, rather than
|
||||
'each'.
|
||||
|
||||
Some Underscore functions have multiple names. You should always use the
|
||||
canonical name (given in large print in the Underscore documentation),
|
||||
with the exception of ``_.any``, which we prefer over the less clear
|
||||
'some'.
|
||||
|
||||
More arbitrary style things
|
||||
===========================
|
||||
|
||||
General
|
||||
-------
|
||||
|
||||
Indentation is four space characters for Python, JS, CSS, and shell
|
||||
scripts. Indentation is two space characters for HTML templates.
|
||||
|
||||
We never use tabs anywhere in source code we write, but we have some
|
||||
third-party files which contain tabs.
|
||||
|
||||
Keep third-party static files under the directory
|
||||
``zephyr/static/third/``, with one subdirectory per third-party project.
|
||||
|
||||
We don't have an absolute hard limit on line length, but we should avoid
|
||||
extremely long lines. A general guideline is: refactor stuff to get it
|
||||
under 85 characters, unless that makes the code a lot uglier, in which
|
||||
case it's fine to go up to 120 or so.
|
||||
|
||||
Whitespace guidelines:
|
||||
|
||||
- Put one space (or more for alignment) around binary arithmetic and
|
||||
equality operators.
|
||||
- Put one space around each part of the ternary operator.
|
||||
- Put one space between keywords like ``if`` and ``while`` and their
|
||||
associated open paren.
|
||||
- Put one space between the closing paren for ``if`` and ``while``-like
|
||||
constructs and the opening curly brace. Put the curly brace on the
|
||||
same line unless doing otherwise improves readability.
|
||||
- Put no space before or after the open paren for function calls and no
|
||||
space before the close paren for function calls.
|
||||
- For the comma operator and colon operator in languages where it is
|
||||
used for inline dictionaries, put no space before it and at least one
|
||||
space after. Only use more than one space for alignment.
|
||||
|
||||
Javascript
|
||||
----------
|
||||
|
||||
Don't use ``==`` and ``!=`` because these operators perform type
|
||||
coercions, which can mask bugs. Always use ``===`` and ``!==``.
|
||||
|
||||
End every statement with a semicolon.
|
||||
|
||||
``if`` statements with no braces are allowed, if the body is simple and
|
||||
its extent is abundantly clear from context and formatting.
|
||||
|
||||
Anonymous functions should have spaces before and after the argument
|
||||
list::
|
||||
|
||||
var x = function (foo, bar) { // ...
|
||||
|
||||
When calling a function with an anonymous function as an argument, use
|
||||
this style::
|
||||
|
||||
$.get('foo', function (data) {
|
||||
var x = ...;
|
||||
// ...
|
||||
});
|
||||
|
||||
The inner function body is indented one level from the outer function
|
||||
call. The closing brace for the inner function and the closing
|
||||
parenthesis for the outer call are together on the same line. This style
|
||||
isn't necessarily appropriate for calls with multiple anonymous
|
||||
functions or other arguments following them.
|
||||
|
||||
Use
|
||||
|
||||
::
|
||||
|
||||
$(function () { ...
|
||||
|
||||
rather than
|
||||
|
||||
::
|
||||
|
||||
$(document).ready(function () { ...
|
||||
|
||||
and combine adjacent on-ready functions, if they are logically related.
|
||||
|
||||
The best way to build complicated DOM elements is a Mustache template
|
||||
like ``zephyr/static/templates/message.handlebars``. For simpler things
|
||||
you can use jQuery DOM building APIs like so::
|
||||
|
||||
var new_tr = $('<tr />').attr('id', zephyr.id);
|
||||
|
||||
Passing a HTML string to jQuery is fine for simple hardcoded things::
|
||||
|
||||
foo.append('<p id="selected">foo</p>');
|
||||
|
||||
but avoid programmatically building complicated strings.
|
||||
|
||||
We used to favor attaching behaviors in templates like so::
|
||||
|
||||
<p onclick="select_zephyr({{id}})">
|
||||
|
||||
but there are some reasons to prefer attaching events using jQuery code:
|
||||
|
||||
- Potential huge performance gains by using delegated events where
|
||||
possible
|
||||
- When calling a function from an ``onclick`` attribute, ``this`` is
|
||||
not bound to the element like you might think
|
||||
- jQuery does event normalization
|
||||
|
||||
Either way, avoid complicated JavaScript code inside HTML attributes;
|
||||
call a helper function instead.
|
||||
|
||||
HTML / CSS
|
||||
----------
|
||||
|
||||
Don't use the ``style=`` attribute. Instead, define logical classes and
|
||||
put your styles in ``zulip.css``.
|
||||
|
||||
Don't use the tag name in a selector unless you have to. In other words,
|
||||
use ``.foo`` instead of ``span.foo``. We shouldn't have to care if the
|
||||
tag type changes in the future.
|
||||
|
||||
Don't use inline event handlers (``onclick=``, etc. attributes).
|
||||
Instead, attach a jQuery event handler
|
||||
(``$('#foo').on('click', function () {...})``) when the DOM is ready
|
||||
(inside a ``$(function () {...})`` block).
|
||||
|
||||
Use this format when you have the same block applying to multiple CSS
|
||||
styles (separate lines for each selector)::
|
||||
|
||||
selector1,
|
||||
selector2 {
|
||||
};
|
||||
|
||||
Python
|
||||
------
|
||||
|
||||
- Scripts should start with ``#!/usr/bin/env python2.7`` and not
|
||||
``#!/usr/bin/env python2.7``. See commit ``437d4aee`` for an explanation of
|
||||
why. Don't put such a line on a Python file unless it's meaningful to
|
||||
run it as a script. (Some libraries can also be run as scripts, e.g.
|
||||
to run a test suite.)
|
||||
- The first import in a file should be
|
||||
``from __future__ import absolute_import``, per `PEP
|
||||
328 <http://docs.python.org/2/whatsnew/2.5.html#pep-328-absolute-and-relative-imports>`__
|
||||
- Put all imports together at the top of the file, absent a compelling
|
||||
reason to do otherwise.
|
||||
- Unpacking sequences doesn't require list brackets::
|
||||
|
||||
[x, y] = xs # unnecessary
|
||||
x, y = xs # better
|
||||
|
||||
- For string formatting, use ``x % (y,)`` rather than ``x % y``, to
|
||||
avoid ambiguity if ``y`` happens to be a tuple.
|
||||
- When selecting by id, don't use ``foo.pk`` when you mean ``foo.id``.
|
||||
E.g.
|
||||
|
||||
::
|
||||
|
||||
recipient = Recipient(type_id=huddle.pk, type=Recipient.HUDDLE)
|
||||
|
||||
should be written as
|
||||
|
||||
::
|
||||
|
||||
recipient = Recipient(type_id=huddle.id, type=Recipient.HUDDLE)
|
||||
|
||||
in case we ever change the primary keys.
|
||||
|
||||
Version Control
|
||||
===============
|
||||
|
||||
Commit Discipline
|
||||
-----------------
|
||||
|
||||
We follow the Git project's own commit discipline practice of "Each
|
||||
commit is a minimal coherent idea".
|
||||
|
||||
Coherency requirements for any commit:
|
||||
|
||||
- It should pass tests (so test updates needed by a change should be in
|
||||
the same commit as the original change, not a separate "fix the tests
|
||||
that were broken by the last commit" commit).
|
||||
- It should be safe to deploy individually, or comment in detail in the
|
||||
commit message as to why it isn't (maybe with a [manual] tag). So
|
||||
implementing a new API endpoint in one commit and then adding the
|
||||
security checks in a future commit should be avoided -- the security
|
||||
checks should be there from the beginning.
|
||||
- Error handling should generally be included along with the code that
|
||||
might trigger the error.
|
||||
- TODO comments should be in the commit that introduces the
|
||||
issue or functionality with further work required.
|
||||
|
||||
When you should be minimal:
|
||||
|
||||
- Significant refactorings should be done in a separate commit from
|
||||
functional changes.
|
||||
- Moving code from one file to another should be done in a separate
|
||||
commits from functional changes or even refactoring within a file.
|
||||
- 2 different refactorings should be done in different commits.
|
||||
- 2 different features should be done in different commits.
|
||||
- If you find yourself writing a commit message that reads like a list
|
||||
of somewhat dissimilar things that you did, you probably should have
|
||||
just done 2 commits.
|
||||
|
||||
When not to be overly minimal:
|
||||
|
||||
- For completely new features, you don't necessarily need to split out
|
||||
new commits for each little subfeature of the new feature. E.g. if
|
||||
you're writing a new tool from scratch, it's fine to have the initial
|
||||
tool have plenty of options/features without doing separate commits
|
||||
for each one. That said, reviewing a 2000-line giant blob of new
|
||||
code isn't fun, so please be thoughtful about submitting things in
|
||||
reviewable units.
|
||||
- Don't bother to split back end commits from front end commits, even
|
||||
though the backend can often be coherent on its own.
|
||||
|
||||
Other considerations:
|
||||
|
||||
- Overly fine commits are easily squashed, but not vice versa, so err
|
||||
toward small commits, and the code reviewer can advise on squashing.
|
||||
|
||||
It can take some practice to get used to writing your commits this
|
||||
way. For example, often you'll start adding a feature, and discover
|
||||
you need to a refactoring partway through writing the feature. When
|
||||
that happens, we recommend stashing your partial feature, do the
|
||||
refactoring, commit it, and then finish implementing your feature.
|
||||
|
||||
Commit Messages
|
||||
---------------
|
||||
|
||||
- The first line of commit messages should be written in the imperative
|
||||
and be kept relatively short while concisely explaining what the
|
||||
commit does. For example:
|
||||
|
||||
Bad::
|
||||
|
||||
bugfix
|
||||
gather_subscriptions was broken
|
||||
|
||||
Good::
|
||||
|
||||
Prevent gather_subscriptions from throwing an exception when given bad input.
|
||||
|
||||
- Please use a complete sentence, ending with a period.
|
||||
|
||||
- The rest of the commit message should be written in full prose and
|
||||
explain why and how the change was made. If the commit makes
|
||||
performance improvements, you should generally include some rough
|
||||
benchmarks showing that it actually improves the performance.
|
||||
|
||||
- In your commit message, you should describe any manual testing you
|
||||
did in addition to running the automated tests, and any aspects of
|
||||
the commit that you think are questionable and you'd like special
|
||||
attention applied to.
|
||||
|
||||
Tests
|
||||
-----
|
||||
|
||||
All significant new features should come with tests.
|
||||
|
||||
Third party code
|
||||
----------------
|
||||
|
||||
When adding new third-party packages to our codebase, please include
|
||||
"[third]" at the beginning of the commit message. You don't necessarily
|
||||
need to do this when patching third-party code that's already in tree.
|
||||
295
docs/conf.py
295
docs/conf.py
@@ -1,295 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# zulip-contributor-docs documentation build configuration file, created by
|
||||
# sphinx-quickstart on Mon Aug 17 16:24:04 2015.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys
|
||||
import os
|
||||
import shlex
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = []
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
# source_suffix = ['.rst', '.md']
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
#source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'Zulip'
|
||||
copyright = u'2015, The Zulip Team'
|
||||
author = u'The Zulip Team'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '0.1'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '0.1'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = ['_build']
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all
|
||||
# documents.
|
||||
#default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
#add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
#add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
#show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
#modindex_common_prefix = []
|
||||
|
||||
# If true, keep warnings as "system message" paragraphs in the built documents.
|
||||
#keep_warnings = False
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = False
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
|
||||
# Read The Docs can't import sphinx_rtd_theme, so don't import it there.
|
||||
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
|
||||
|
||||
if not on_rtd:
|
||||
import sphinx_rtd_theme
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
#html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
#html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#html_logo = None
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# Add any extra paths that contain custom files (such as robots.txt or
|
||||
# .htaccess) here, relative to this directory. These files are copied
|
||||
# directly to the root of the documentation.
|
||||
#html_extra_path = []
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
#html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
#html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
#html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
#html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
#html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
#html_file_suffix = None
|
||||
|
||||
# Language to be used for generating the HTML full-text search index.
|
||||
# Sphinx supports the following languages:
|
||||
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
|
||||
# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
|
||||
#html_search_language = 'en'
|
||||
|
||||
# A dictionary with options for the search language support, empty by default.
|
||||
# Now only 'ja' uses this config value
|
||||
#html_search_options = {'type': 'default'}
|
||||
|
||||
# The name of a javascript file (relative to the configuration directory) that
|
||||
# implements a search results scorer. If empty, the default will be used.
|
||||
#html_search_scorer = 'scorer.js'
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'zulip-contributor-docsdoc'
|
||||
|
||||
def setup(app):
|
||||
# overrides for wide tables in RTD theme
|
||||
app.add_stylesheet('theme_overrides.css') # path relative to _static
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'zulip-contributor-docs.tex', u'Zulip Documentation',
|
||||
u'The Zulip Team', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
#latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'zulip-contributor-docs', u'Zulip Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'zulip-contributor-docs', u'Zulip Documentation',
|
||||
author, 'zulip-contributor-docs', 'Documentation for contributing to Zulip.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
#texinfo_show_urls = 'footnote'
|
||||
|
||||
# If true, do not generate a @detailmenu in the "Top" node's menu.
|
||||
#texinfo_no_detailmenu = False
|
||||
@@ -1,95 +0,0 @@
|
||||
===================
|
||||
Directory structure
|
||||
===================
|
||||
|
||||
This page documents the Zulip directory structure and how to decide where to
|
||||
put a file.
|
||||
|
||||
Scripts
|
||||
=======
|
||||
|
||||
+--------------------+-----------------------------------------------------------------------------------+
|
||||
| ``scripts/`` | Scripts that production deployments might run manually (e.g. ``restart-server``) |
|
||||
+--------------------+-----------------------------------------------------------------------------------+
|
||||
| ``bin/`` | Scripts that are needed on production deployments but humans should never run |
|
||||
+--------------------+-----------------------------------------------------------------------------------+
|
||||
| ``scripts/setup/`` | Tools that production deployments will only run once, during installation |
|
||||
+--------------------+-----------------------------------------------------------------------------------+
|
||||
| ``tools/`` | Development tools |
|
||||
+--------------------+-----------------------------------------------------------------------------------+
|
||||
|
||||
Bots
|
||||
====
|
||||
|
||||
+------------------------+----------------------------------------------------------------------+
|
||||
| ``api/integrations`` | Bots distributed as part of the Zulip API bundle. |
|
||||
+------------------------+----------------------------------------------------------------------+
|
||||
| ``bots/`` | Previously Zulip internal bots. These usually need a bit of work. |
|
||||
+------------------------+----------------------------------------------------------------------+
|
||||
|
||||
Management commands
|
||||
===================
|
||||
|
||||
+-------------------------------------+------------------------------------------------------------------------------------------------------------------------------------+
|
||||
| ``zerver/management/commands/`` | Management commands one might run at a production deployment site (e.g. scripts to change a value or deactivate a user properly) |
|
||||
+-------------------------------------+------------------------------------------------------------------------------------------------------------------------------------+
|
||||
|
||||
Views
|
||||
=====
|
||||
|
||||
+--------------------------------+-----------------------------------------+
|
||||
| ``zerver/tornadoviews.py`` | Tornado views |
|
||||
+--------------------------------+-----------------------------------------+
|
||||
| ``zerver/views/webhooks.py`` | Webhook views |
|
||||
+--------------------------------+-----------------------------------------+
|
||||
| ``zerver/views/messages.py`` | message-related views |
|
||||
+--------------------------------+-----------------------------------------+
|
||||
| ``zerver/views/__init__.py`` | other Django views |
|
||||
+--------------------------------+-----------------------------------------+
|
||||
|
||||
Static assets
|
||||
=============
|
||||
|
||||
+---------------+---------------------------------------------------------------------------------------------------------------+
|
||||
| ``assets/`` | For assets not to be served to the web (e.g. the system to generate our favicons) |
|
||||
+---------------+---------------------------------------------------------------------------------------------------------------+
|
||||
| ``static/`` | For things we do want to both serve to the web and distribute to production deployments (e.g. the webpages) |
|
||||
+---------------+---------------------------------------------------------------------------------------------------------------+
|
||||
|
||||
Puppet
|
||||
======
|
||||
|
||||
+--------------------+----------------------------------------------------------------------------------+
|
||||
| ``puppet/zulip`` | For configuration for production deployments |
|
||||
+--------------------+----------------------------------------------------------------------------------+
|
||||
|
||||
Templates
|
||||
=========
|
||||
|
||||
+--------------------------+--------------------------------------------------------+
|
||||
| ``templates/zerver`` | For templates related to zerver views |
|
||||
+--------------------------+--------------------------------------------------------+
|
||||
| ``static/templates`` | Handlebars templates for the frontend |
|
||||
+--------------------------+--------------------------------------------------------+
|
||||
|
||||
Tests
|
||||
=====
|
||||
|
||||
+------------------------+-----------------------------------+
|
||||
| ``zerver/test*.py`` | Backend tests |
|
||||
+------------------------+-----------------------------------+
|
||||
| ``frontend_tests/node`` | Node Frontend unit tests |
|
||||
+------------------------+-----------------------------------+
|
||||
| ``frontend_tests/tests`` | Casper frontend tests |
|
||||
+------------------------+-----------------------------------+
|
||||
|
||||
Documentation
|
||||
=============
|
||||
|
||||
+-------------+-----------------------------------------------+
|
||||
| ``docs/`` | Source for this documentation |
|
||||
+-------------+-----------------------------------------------+
|
||||
|
||||
You can consult the repository's .gitattributes file to see exactly
|
||||
which components are excluded from production releases (release
|
||||
tarballs are generated using tools/build-release-tarball).
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/usr/bin/env python2.7
|
||||
|
||||
# Remove HTML entity escaping left over from MediaWiki->rST conversion.
|
||||
|
||||
import html
|
||||
import sys
|
||||
|
||||
for line in sys.stdin:
|
||||
print(html.unescape(line), end='')
|
||||
@@ -1,25 +0,0 @@
|
||||
.. zulip documentation master file, created by
|
||||
sphinx-quickstart on Mon Aug 17 16:24:04 2015.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Welcome to Zulip documentation!
|
||||
===============================
|
||||
|
||||
Contents:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
new-feature-tutorial
|
||||
code-style
|
||||
directory-structure
|
||||
testing
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
||||
@@ -1,216 +0,0 @@
|
||||
====================
|
||||
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.
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
# Empty requirements.txt to avoid readthedocs installing all our dependencies.
|
||||
323
docs/testing.rst
323
docs/testing.rst
@@ -1,323 +0,0 @@
|
||||
=======
|
||||
Testing
|
||||
=======
|
||||
|
||||
Running tests
|
||||
=============
|
||||
|
||||
To run everything, just use ``./tools/test-all``. This runs lint checks,
|
||||
web frontend / whole-system blackbox tests, and backend Django tests.
|
||||
|
||||
If you want to run individual parts, see the various commands inside
|
||||
that script.
|
||||
|
||||
Schema and initial data changes
|
||||
-------------------------------
|
||||
|
||||
If you change the database schema or change the initial test data, you
|
||||
have have to regenerate the pristine test database by running
|
||||
``tools/do-destroy-rebuild-test-database``.
|
||||
|
||||
Wiping the test databases
|
||||
-------------------------
|
||||
|
||||
You should first try running: ``tools/do-destroy-rebuild-test-database``
|
||||
|
||||
If that fails you should try to do:
|
||||
|
||||
::
|
||||
|
||||
sudo -u postgres psql
|
||||
> DROP DATABASE zulip_test;
|
||||
> DROP DATABASE zulip_test_template;
|
||||
|
||||
and then run ``tools/do-destroy-rebuild-test-database``
|
||||
|
||||
Recreating the postgres cluster
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. warning::
|
||||
|
||||
**This is irreversible, so do it with care, and never do this anywhere
|
||||
in production.**
|
||||
|
||||
If your postgres cluster (collection of databases) gets totally trashed
|
||||
permissions-wise, and you can't otherwise repair it, you can recreate
|
||||
it. On Ubuntu:
|
||||
|
||||
::
|
||||
|
||||
sudo pg_dropcluster --stop 9.1 main
|
||||
sudo pg_createcluster --locale=en_US.utf8 --start 9.1 main
|
||||
|
||||
Backend Django tests
|
||||
--------------------
|
||||
|
||||
These live in ``zerver/tests.py`` and ``zerver/test_*.py``. Run them
|
||||
with ``tools/test-backend``.
|
||||
|
||||
Web frontend black-box casperjs tests
|
||||
-------------------------------------
|
||||
|
||||
These live in ``frontend_tests/casper_tests/``. This is a "black box"
|
||||
test; we load the frontend in a real (headless) browser, from a real dev
|
||||
server, and simulate UI interactions like sending messages, narrowing,
|
||||
etc.
|
||||
|
||||
Since this is interacting with a real dev server, it can catch backend
|
||||
bugs as well.
|
||||
|
||||
You can run this with ``./tools/test-js-with-casper`` or as
|
||||
``./tools/test-js-with-casper 05-settings.js`` to run a single test
|
||||
file from ``frontend_tests/casper_tests/``.
|
||||
|
||||
Writing Casper tests
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Probably the easiest way to learn how to write Casper tests is to
|
||||
study some of the existing test files. There are a few tips that can
|
||||
be useful for writing Casper tests in addition to the debugging notes
|
||||
below:
|
||||
|
||||
- Run just the file containing your new tests as described above to
|
||||
have a fast debugging cycle.
|
||||
- With frontend tests in general, it's very important to write your
|
||||
code to wait for the right events. Before essentially every action
|
||||
you take on the page, you'll want to use ``waitForSelector``,
|
||||
``waitUntilVisible``, or a similar function to make sure the page or
|
||||
elemant is ready before you interact with it. For instance, if you
|
||||
want to click a button that you can select via ``#btn-submit``, and
|
||||
then check that it causes ``success-elt`` to appear, you'll want to
|
||||
write something like:
|
||||
|
||||
::
|
||||
|
||||
casper.waitForSelector("#btn-submit", function () {
|
||||
casper.click('#btn-submit')
|
||||
casper.test.assertExists("#success-elt");
|
||||
});
|
||||
|
||||
This will ensure that the element is present before the interaction
|
||||
is attempted. The various wait functions supported in Casper are
|
||||
documented in the Casper here:
|
||||
http://docs.casperjs.org/en/latest/modules/casper.html#waitforselector
|
||||
and the various assert statements available are documented here:
|
||||
http://docs.casperjs.org/en/latest/modules/tester.html#the-tester-prototype
|
||||
- Casper uses CSS3 selectors; you can often save time by testing and
|
||||
debugigng your selectors on the relevant page of the Zulip
|
||||
development app in the Chrome javascript console by using
|
||||
e.g. ``$$("#settings-dropdown")``.
|
||||
- The test suite uses a smaller set of default user accounts and other
|
||||
data initialized in the database than the development environment;
|
||||
to see what differs check out the section related to
|
||||
``options["test_suite"]`` in
|
||||
``zilencer/management/commands/populate_db.py``.
|
||||
- Casper effectively runs your test file in two phases -- first it
|
||||
runs the code in the test file, which for most test files will just
|
||||
collect a series of steps (each being a ``casper.then`` or
|
||||
``casper.wait...`` call). Then, usually at the end of the test
|
||||
file, you'll have a ``casper.run`` call which actually runs that
|
||||
series of steps. This means that if you If you write code in your
|
||||
test file outside a ``casper.then`` or ``casper.wait...`` method, it
|
||||
will actually run before all the Casper test steps that are declared
|
||||
in the file, which can lead to confusing failures where the new code
|
||||
you write in between two ``casper.then`` blocks actually runs before
|
||||
either of them. See this for more details about how Casper works:
|
||||
http://docs.casperjs.org/en/latest/faq.html#how-does-then-and-the-step-stack-work
|
||||
|
||||
Debugging Casper.JS
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Casper.js (via PhantomJS) has support for remote debugging. However, it
|
||||
is not perfect. Here are some steps for using it and gotchas you might
|
||||
want to know.
|
||||
|
||||
To turn on remote debugging, pass ``--remote-debug`` to the
|
||||
``./frontend_tests/tests/run`` script. This will run the tests with
|
||||
port ``7777`` open for remote debugging. You can now connect to
|
||||
``localhost:7777`` in a Webkit browser. Somewhat recent versions of
|
||||
Chrome or Safari might be required.
|
||||
|
||||
- When connecting to the remote debugger, you will see a list of pages,
|
||||
probably 2. One page called ``about:blank`` is the headless page in
|
||||
which the CasperJS test itself is actually running in. This is where
|
||||
your test code is.
|
||||
- The other page, probably ``localhost:9981``, is the Zulip page that
|
||||
the test is testing---that is, the page running our app that our test
|
||||
is exercising.
|
||||
|
||||
Since the tests are now running, you can open the ``about:blank`` page,
|
||||
switch to the Scripts tab, and open the running ``0x-foo.js`` test. If
|
||||
you set a breakpoint and it is hit, the inspector will pause and you can
|
||||
do your normal JS debugging. You can also put breakpoints in the Zulip
|
||||
webpage itself if you wish to inspect the state of the Zulip frontend.
|
||||
|
||||
If you need to use print debugging in casper, you can do using
|
||||
``casper.log``; see http://docs.casperjs.org/en/latest/logging.html
|
||||
for details.
|
||||
|
||||
An additional debugging technique is to enable verbose mode in the
|
||||
Casper tests; you can do this by adding to the top of the relevant
|
||||
test file the following:
|
||||
|
||||
::
|
||||
|
||||
var casper = require('casper').create({
|
||||
verbose: true,
|
||||
logLevel: "debug"
|
||||
});
|
||||
|
||||
This can sometimes give insight into exactly what's happening.
|
||||
|
||||
Web frontend unit tests
|
||||
-----------------------
|
||||
|
||||
As an alternative to the black-box whole-app testing, you can unit test
|
||||
individual JavaScript files that use the module pattern. For example, to
|
||||
test the ``foobar.js`` file, you would first add the following to the
|
||||
bottom of ``foobar.js``:
|
||||
|
||||
::
|
||||
|
||||
if (typeof module !== 'undefined') {
|
||||
module.exports = foobar;
|
||||
}
|
||||
|
||||
This makes ``foobar.js`` follow the CommonJS module pattern, so it can
|
||||
be required in Node.js, which runs our tests.
|
||||
|
||||
Now create ``frontend_tests/node_tests/foobar.js``. At the top, require
|
||||
the `Node.js assert module <http://nodejs.org/api/assert.html>`__, and
|
||||
the module you're testing, like so:
|
||||
|
||||
::
|
||||
|
||||
var assert = require('assert');
|
||||
var foobar = require('js/foobar.js');
|
||||
|
||||
(If the module you're testing depends on other modules, or modifies
|
||||
global state, you need to also read `the next section`__.)
|
||||
|
||||
__ handling-dependencies_
|
||||
|
||||
Define and call some tests using the `assert
|
||||
module <http://nodejs.org/api/assert.html>`__. Note that for "equal"
|
||||
asserts, the *actual* value comes first, the *expected* value second.
|
||||
|
||||
::
|
||||
|
||||
(function test_somefeature() {
|
||||
assert.strictEqual(foobar.somefeature('baz'), 'quux');
|
||||
assert.throws(foobar.somefeature('Invalid Input'));
|
||||
}());
|
||||
|
||||
The test runner (index.js) automatically runs all .js files in the
|
||||
frontend_tests/node directory.
|
||||
|
||||
.. _handling-dependencies:
|
||||
|
||||
Handling dependencies in unit tests
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The following scheme helps avoid tests leaking globals between each
|
||||
other.
|
||||
|
||||
First, if you can avoid globals, do it, and the code that is directly
|
||||
under test can simply be handled like this:
|
||||
|
||||
::
|
||||
|
||||
var search = require('js/search_suggestion.js');
|
||||
|
||||
For deeper dependencies, you want to categorize each module as follows:
|
||||
|
||||
- Exercise the module's real code for deeper, more realistic testing?
|
||||
- Stub out the module's interface for more control, speed, and
|
||||
isolation?
|
||||
- Do some combination of the above?
|
||||
|
||||
For all the modules where you want to run actual code, add a statement
|
||||
like the following to the top of your test file:
|
||||
|
||||
::
|
||||
|
||||
add_dependencies({
|
||||
_: 'third/underscore/underscore.js',
|
||||
util: 'js/util.js',
|
||||
Dict: 'js/dict.js',
|
||||
Handlebars: 'handlebars',
|
||||
Filter: 'js/filter.js',
|
||||
typeahead_helper: 'js/typeahead_helper.js',
|
||||
stream_data: 'js/stream_data.js',
|
||||
narrow: 'js/narrow.js'
|
||||
});
|
||||
|
||||
For modules that you want to completely stub out, please use a pattern
|
||||
like this:
|
||||
|
||||
::
|
||||
|
||||
set_global('page_params', {
|
||||
email: 'bob@zulip.com'
|
||||
});
|
||||
|
||||
// then maybe further down
|
||||
global.page_params.email = 'alice@zulip.com';
|
||||
|
||||
Finally, there's the hybrid situation, where you want to borrow some of
|
||||
a module's real functionality but stub out other pieces. Obviously, this
|
||||
is a pretty strong smell that the other module might be lacking in
|
||||
cohesion, but that code might be outside your jurisdiction. The pattern
|
||||
here is this:
|
||||
|
||||
::
|
||||
|
||||
// Use real versions of parse/unparse
|
||||
var narrow = require('js/narrow.js');
|
||||
set_global('narrow', {
|
||||
parse: narrow.parse,
|
||||
unparse: narrow.unparse
|
||||
});
|
||||
|
||||
// But later, I want to stub the stream without having to call super-expensive
|
||||
// real code like narrow.activate().
|
||||
global.narrow.stream = function () {
|
||||
return 'office';
|
||||
};
|
||||
|
||||
Coverage reports
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
You can automatically generate coverage reports for the JavaScript unit
|
||||
tests. To do so, install istanbul:
|
||||
|
||||
::
|
||||
|
||||
sudo npm install -g istanbul
|
||||
|
||||
And run test-js-with-node with the 'cover' parameter:
|
||||
|
||||
::
|
||||
|
||||
tools/test-js-with-node cover
|
||||
|
||||
Then open ``coverage/lcov-report/js/index.html`` in your browser.
|
||||
Modules we don't test *at all* aren't listed in the report, so this
|
||||
tends to overstate how good our overall coverage is, but it's accurate
|
||||
for individual files. You can also click a filename to see the specific
|
||||
statements and branches not tested. 100% branch coverage isn't
|
||||
necessarily possible, but getting to at least 80% branch coverage is a
|
||||
good goal.
|
||||
|
||||
Manual testing (local app + web browser)
|
||||
========================================
|
||||
|
||||
Setting up the manual testing database
|
||||
--------------------------------------
|
||||
|
||||
::
|
||||
|
||||
./tools/do-destroy-rebuild-database
|
||||
|
||||
Will populate your local database with all the usual accounts plus some
|
||||
test messages involving Shakespeare characters.
|
||||
2
frontend_tests/casper_lib/.gitignore
vendored
2
frontend_tests/casper_lib/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
/server.log
|
||||
/test_credentials.js
|
||||
@@ -1,46 +0,0 @@
|
||||
var common = require('../casper_lib/common.js').common;
|
||||
|
||||
common.start_and_log_in();
|
||||
|
||||
casper.then(function () {
|
||||
casper.test.info('Subscriptions page');
|
||||
casper.click('a[href^="#subscriptions"]');
|
||||
casper.test.assertUrlMatch(/^http:\/\/[^\/]+\/#subscriptions/, 'URL suggests we are on subscriptions page');
|
||||
casper.test.assertExists('#subscriptions.tab-pane.active', 'Subscriptions page is active');
|
||||
// subscriptions need to load; if they have *any* subs,
|
||||
// the word "Unsubscribe" will appear
|
||||
});
|
||||
casper.waitForText('Subscribed', function () {
|
||||
casper.test.assertTextExists('Subscribed', 'Initial subscriptions loaded');
|
||||
casper.fill('form#add_new_subscription', {stream_name: 'Waseemio'});
|
||||
casper.click('form#add_new_subscription input.btn');
|
||||
});
|
||||
casper.waitForText('Waseemio', function () {
|
||||
casper.test.assertTextExists('Create stream Waseemio', 'Modal for specifying new stream users');
|
||||
casper.click('form#stream_creation_form button.btn.btn-primary');
|
||||
});
|
||||
casper.waitFor(function () {
|
||||
return casper.evaluate(function () {
|
||||
return $('.subscription_name').is(':contains("Waseemio")');
|
||||
});
|
||||
});
|
||||
|
||||
casper.then(function () {
|
||||
casper.test.assertSelectorHasText('.subscription_name', 'Waseemio', 'Subscribing to a stream');
|
||||
casper.fill('form#add_new_subscription', {stream_name: 'WASeemio'});
|
||||
casper.click('form#add_new_subscription input.btn');
|
||||
});
|
||||
casper.waitForText('Already subscribed', function () {
|
||||
casper.test.assertTextExists('Already subscribed', "Can't subscribe twice to a stream");
|
||||
casper.fill('form#add_new_subscription', {stream_name: ' '});
|
||||
casper.click('form#add_new_subscription input.btn');
|
||||
});
|
||||
casper.waitForText('Error adding subscription', function () {
|
||||
casper.test.assertTextExists('Error adding subscription', "Can't subscribe to an empty stream name");
|
||||
});
|
||||
|
||||
common.then_log_out();
|
||||
|
||||
casper.run(function () {
|
||||
casper.test.done();
|
||||
});
|
||||
@@ -1,77 +0,0 @@
|
||||
var common = require('../casper_lib/common.js').common;
|
||||
var test_credentials = require('../casper_lib/test_credentials.js').test_credentials;
|
||||
|
||||
common.start_and_log_in();
|
||||
|
||||
casper.then(function () {
|
||||
casper.test.info('Administration page');
|
||||
casper.click('a[href^="#administration"]');
|
||||
casper.test.assertUrlMatch(/^http:\/\/[^\/]+\/#administration/, 'URL suggests we are on administration page');
|
||||
casper.test.assertExists('#administration.tab-pane.active', 'Administration page is active');
|
||||
});
|
||||
|
||||
// Test user deactivation and reactivation
|
||||
casper.waitForSelector('.user_row[id="user_cordelia@zulip.com"]', function () {
|
||||
casper.test.assertSelectorHasText('.user_row[id="user_cordelia@zulip.com"]', 'Deactivate');
|
||||
casper.click('.user_row[id="user_cordelia@zulip.com"] .deactivate');
|
||||
casper.test.assertTextExists('Deactivate cordelia@zulip.com', 'Deactivate modal has right user');
|
||||
casper.test.assertTextExists('Deactivate now', 'Deactivate now button available');
|
||||
casper.click('#do_deactivate_user_button');
|
||||
});
|
||||
|
||||
casper.waitForSelector('.user_row[id="user_cordelia@zulip.com"].deactivated_user', function () {
|
||||
casper.test.assertSelectorHasText('.user_row[id="user_cordelia@zulip.com"]', 'Reactivate');
|
||||
casper.click('.user_row[id="user_cordelia@zulip.com"] .reactivate');
|
||||
});
|
||||
|
||||
casper.waitForSelector('.user_row[id="user_cordelia@zulip.com"]:not(.deactivated_user)', function () {
|
||||
casper.test.assertSelectorHasText('.user_row[id="user_cordelia@zulip.com"]', 'Deactivate');
|
||||
});
|
||||
|
||||
// Test Deactivated users section of admin page
|
||||
casper.waitForSelector('.user_row[id="user_cordelia@zulip.com"]', function () {
|
||||
casper.test.assertSelectorHasText('.user_row[id="user_cordelia@zulip.com"]', 'Deactivate');
|
||||
casper.click('.user_row[id="user_cordelia@zulip.com"] .deactivate');
|
||||
casper.test.assertTextExists('Deactivate cordelia@zulip.com', 'Deactivate modal has right user');
|
||||
casper.test.assertTextExists('Deactivate now', 'Deactivate now button available');
|
||||
casper.click('#do_deactivate_user_button');
|
||||
});
|
||||
|
||||
casper.then(function () {
|
||||
// Leave the page and return
|
||||
casper.click('#settings-dropdown');
|
||||
casper.click('a[href^="#subscriptions"]');
|
||||
casper.click('#settings-dropdown');
|
||||
casper.click('a[href^="#administration"]');
|
||||
|
||||
casper.waitForSelector('.user_row[id="user_cordelia@zulip.com"]', function () {
|
||||
casper.test.assertSelectorHasText('#admin_deactivated_users_table .user_row[id="user_cordelia@zulip.com"]', 'Reactivate');
|
||||
casper.click('#admin_deactivated_users_table .user_row[id="user_cordelia@zulip.com"] .reactivate');
|
||||
});
|
||||
|
||||
casper.waitForSelector('#admin_deactivated_users_table .user_row[id="user_cordelia@zulip.com"]:not(.deactivated_user)', function () {
|
||||
casper.test.assertSelectorHasText('#admin_deactivated_users_table .user_row[id="user_cordelia@zulip.com"]', 'Deactivate');
|
||||
});
|
||||
});
|
||||
|
||||
// Test bot deactivation and reactivation
|
||||
casper.waitForSelector('.user_row[id="user_new-user-bot@zulip.com"]', function () {
|
||||
casper.test.assertSelectorHasText('.user_row[id="user_new-user-bot@zulip.com"]', 'Deactivate');
|
||||
casper.click('.user_row[id="user_new-user-bot@zulip.com"] .deactivate');
|
||||
});
|
||||
|
||||
casper.waitForSelector('.user_row[id="user_new-user-bot@zulip.com"].deactivated_user', function () {
|
||||
casper.test.assertSelectorHasText('.user_row[id="user_new-user-bot@zulip.com"]', 'Reactivate');
|
||||
casper.click('.user_row[id="user_new-user-bot@zulip.com"] .reactivate');
|
||||
});
|
||||
casper.waitForSelector('.user_row[id="user_new-user-bot@zulip.com"]:not(.deactivated_user)', function () {
|
||||
casper.test.assertSelectorHasText('.user_row[id="user_new-user-bot@zulip.com"]', 'Deactivate');
|
||||
});
|
||||
|
||||
// TODO: Test stream deletion
|
||||
|
||||
common.then_log_out();
|
||||
|
||||
casper.run(function () {
|
||||
casper.test.done();
|
||||
});
|
||||
@@ -1,108 +0,0 @@
|
||||
var _ = global._;
|
||||
|
||||
set_global('$', function (f) {
|
||||
if (f) {
|
||||
return f();
|
||||
}
|
||||
return {trigger: function () {}};
|
||||
});
|
||||
set_global('document', null);
|
||||
|
||||
var page_params = {
|
||||
bot_list: [{email: 'bot0@zulip.com', full_name: 'Bot 0'}],
|
||||
is_admin: false,
|
||||
email: 'owner@zulip.com'
|
||||
};
|
||||
set_global('page_params', page_params);
|
||||
|
||||
var patched_underscore = _.clone(_);
|
||||
patched_underscore.debounce = function (f) { return(f); };
|
||||
global.patch_builtin('_', patched_underscore);
|
||||
|
||||
|
||||
var bot_data = require('js/bot_data.js');
|
||||
|
||||
// Our startup logic should have added Bot 0 from page_params.
|
||||
assert.equal(bot_data.get('bot0@zulip.com').full_name, 'Bot 0');
|
||||
|
||||
(function () {
|
||||
var test_bot = {
|
||||
email: 'bot1@zulip.com',
|
||||
avatar_url: '',
|
||||
default_all_public_streams: '',
|
||||
default_events_register_stream: '',
|
||||
default_sending_stream: '',
|
||||
full_name: 'Bot 1',
|
||||
extra: 'Not in data'
|
||||
};
|
||||
|
||||
(function test_add() {
|
||||
bot_data.add(test_bot);
|
||||
|
||||
var bot = bot_data.get('bot1@zulip.com');
|
||||
assert.equal('Bot 1', bot.full_name);
|
||||
assert.equal(undefined, bot.extra);
|
||||
}());
|
||||
|
||||
(function test_update() {
|
||||
var bot;
|
||||
|
||||
bot_data.add(test_bot);
|
||||
|
||||
bot = bot_data.get('bot1@zulip.com');
|
||||
assert.equal('Bot 1', bot.full_name);
|
||||
bot_data.update('bot1@zulip.com', {full_name: 'New Bot 1'});
|
||||
bot = bot_data.get('bot1@zulip.com');
|
||||
assert.equal('New Bot 1', bot.full_name);
|
||||
}());
|
||||
|
||||
(function test_remove() {
|
||||
var bot;
|
||||
|
||||
bot_data.add(test_bot);
|
||||
|
||||
bot = bot_data.get('bot1@zulip.com');
|
||||
assert.equal('Bot 1', bot.full_name);
|
||||
bot_data.remove('bot1@zulip.com');
|
||||
bot = bot_data.get('bot1@zulip.com');
|
||||
assert.equal(undefined, bot);
|
||||
}());
|
||||
|
||||
(function test_owner_can_admin() {
|
||||
var bot;
|
||||
|
||||
bot_data.add(_.extend({owner: 'owner@zulip.com'}, test_bot));
|
||||
|
||||
bot = bot_data.get('bot1@zulip.com');
|
||||
assert(bot.can_admin);
|
||||
|
||||
bot_data.add(_.extend({owner: 'notowner@zulip.com'}, test_bot));
|
||||
|
||||
bot = bot_data.get('bot1@zulip.com');
|
||||
assert.equal(false, bot.can_admin);
|
||||
}());
|
||||
|
||||
(function test_admin_can_admin() {
|
||||
var bot;
|
||||
page_params.is_admin = true;
|
||||
|
||||
bot_data.add(test_bot);
|
||||
|
||||
bot = bot_data.get('bot1@zulip.com');
|
||||
assert(bot.can_admin);
|
||||
|
||||
page_params.is_admin = false;
|
||||
}());
|
||||
|
||||
(function test_get_editable() {
|
||||
var can_admin;
|
||||
|
||||
bot_data.add(_.extend({}, test_bot, {owner: 'owner@zulip.com'}));
|
||||
bot_data.add(_.extend({}, test_bot, {email: 'bot2@zulip.com'}));
|
||||
|
||||
can_admin = _.pluck(bot_data.get_editable(), 'email');
|
||||
assert.deepEqual(['bot1@zulip.com'], can_admin);
|
||||
}());
|
||||
|
||||
|
||||
}());
|
||||
@@ -1,20 +0,0 @@
|
||||
var hashchange = require('js/hashchange.js');
|
||||
|
||||
(function test_basics() {
|
||||
var operators;
|
||||
var hash;
|
||||
|
||||
operators = [
|
||||
{operator: 'stream', operand: 'devel'},
|
||||
{operator: 'topic', operand: 'algol'}
|
||||
];
|
||||
hash = hashchange.operators_to_hash(operators);
|
||||
assert.equal(hash, '#narrow/stream/devel/topic/algol');
|
||||
|
||||
operators = [
|
||||
{operator: 'stream', operand: 'devel'},
|
||||
{operator: 'topic', operand: 'visual c++', negated: true}
|
||||
];
|
||||
hash = hashchange.operators_to_hash(operators);
|
||||
assert.equal(hash, '#narrow/stream/devel/-topic/visual.20c.2B.2B');
|
||||
}());
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user