mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 05:53:43 +00:00 
			
		
		
		
	Compare commits
	
		
			13 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					6bad5b6616 | ||
| 
						 | 
					8813c7b542 | ||
| 
						 | 
					ba528f9345 | ||
| 
						 | 
					4192276aa3 | ||
| 
						 | 
					f055a7d133 | ||
| 
						 | 
					2e4ae9c5dc | ||
| 
						 | 
					139fb8c2ee | ||
| 
						 | 
					93ffaa73bd | ||
| 
						 | 
					960d736e55 | ||
| 
						 | 
					28a3dcf787 | ||
| 
						 | 
					4eb958b6d8 | ||
| 
						 | 
					d35d5953c7 | ||
| 
						 | 
					c256c5e91c | 
@@ -1,143 +0,0 @@
 | 
			
		||||
# See CircleCI upstream's docs on this config format:
 | 
			
		||||
#   https://circleci.com/docs/2.0/language-python/
 | 
			
		||||
#
 | 
			
		||||
version: 2
 | 
			
		||||
jobs:
 | 
			
		||||
  "trusty-python-3.4":
 | 
			
		||||
    docker:
 | 
			
		||||
      # This is built from tools/circleci/images/trusty/Dockerfile .
 | 
			
		||||
      - image: gregprice/circleci:trusty-python-4.test
 | 
			
		||||
 | 
			
		||||
    working_directory: ~/zulip
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
      - checkout
 | 
			
		||||
 | 
			
		||||
      - run:
 | 
			
		||||
          name: create cache directories
 | 
			
		||||
          command: |
 | 
			
		||||
              dirs=(/srv/zulip-{npm,venv}-cache)
 | 
			
		||||
              sudo mkdir -p "${dirs[@]}"
 | 
			
		||||
              sudo chown -R circleci "${dirs[@]}"
 | 
			
		||||
      - restore_cache:
 | 
			
		||||
          keys:
 | 
			
		||||
          - v1-npm-base.trusty.1
 | 
			
		||||
      - restore_cache:
 | 
			
		||||
          keys:
 | 
			
		||||
          - v1-venv-base.trusty.1
 | 
			
		||||
 | 
			
		||||
      - run:
 | 
			
		||||
          name: install dependencies
 | 
			
		||||
          command: |
 | 
			
		||||
            # Install moreutils so we can use `ts` and `mispipe` in the following.
 | 
			
		||||
            sudo apt-get install -y moreutils
 | 
			
		||||
 | 
			
		||||
            # CircleCI sets the following in Git config at clone time:
 | 
			
		||||
            #   url.ssh://git@github.com.insteadOf https://github.com
 | 
			
		||||
            # This breaks the Git clones in the NVM `install.sh` we run
 | 
			
		||||
            # in `install-node`.
 | 
			
		||||
            # TODO: figure out why that breaks, and whether we want it.
 | 
			
		||||
            #   (Is it an optimization?)
 | 
			
		||||
            rm -f /home/circleci/.gitconfig
 | 
			
		||||
 | 
			
		||||
            # This is the main setup job for the test suite
 | 
			
		||||
            mispipe "tools/travis/setup-backend" ts
 | 
			
		||||
 | 
			
		||||
            # Cleaning caches is mostly unnecessary in Circle, because
 | 
			
		||||
            # most builds don't get to write to the cache.
 | 
			
		||||
            # mispipe "scripts/lib/clean-unused-caches --verbose --threshold 0" ts
 | 
			
		||||
 | 
			
		||||
      - save_cache:
 | 
			
		||||
          paths:
 | 
			
		||||
            - /srv/zulip-npm-cache
 | 
			
		||||
          key: v1-npm-base.trusty.1
 | 
			
		||||
      - save_cache:
 | 
			
		||||
          paths:
 | 
			
		||||
            - /srv/zulip-venv-cache
 | 
			
		||||
          key: v1-venv-base.trusty.1
 | 
			
		||||
      # TODO: in Travis we also cache ~/zulip-emoji-cache, ~/node, ~/misc
 | 
			
		||||
 | 
			
		||||
      # The moment of truth!  Run the tests.
 | 
			
		||||
 | 
			
		||||
      - run:
 | 
			
		||||
          name: run backend tests
 | 
			
		||||
          command: |
 | 
			
		||||
            . /srv/zulip-py3-venv/bin/activate
 | 
			
		||||
            mispipe ./tools/travis/backend ts
 | 
			
		||||
 | 
			
		||||
      - run:
 | 
			
		||||
          name: run frontend tests
 | 
			
		||||
          command: |
 | 
			
		||||
            . /srv/zulip-py3-venv/bin/activate
 | 
			
		||||
            mispipe ./tools/travis/frontend ts
 | 
			
		||||
 | 
			
		||||
      -  run:
 | 
			
		||||
          name: upload coverage report
 | 
			
		||||
          command: |
 | 
			
		||||
            . /srv/zulip-py3-venv/bin/activate
 | 
			
		||||
            pip install codecov && codecov \
 | 
			
		||||
              || echo "Error in uploading coverage reports to codecov.io."
 | 
			
		||||
 | 
			
		||||
      # - store_artifacts:  # TODO
 | 
			
		||||
      #     path: var/casper/
 | 
			
		||||
      #     # also /tmp/zulip-test-event-log/
 | 
			
		||||
      #     destination: test-reports
 | 
			
		||||
 | 
			
		||||
  "xenial-python-3.5":
 | 
			
		||||
    docker:
 | 
			
		||||
      # This is built from tools/circleci/images/xenial/Dockerfile .
 | 
			
		||||
      - image: gregprice/circleci:xenial-python-3.test
 | 
			
		||||
 | 
			
		||||
    working_directory: ~/zulip
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
      - checkout
 | 
			
		||||
 | 
			
		||||
      - run:
 | 
			
		||||
          name: create cache directories
 | 
			
		||||
          command: |
 | 
			
		||||
              dirs=(/srv/zulip-{npm,venv}-cache)
 | 
			
		||||
              sudo mkdir -p "${dirs[@]}"
 | 
			
		||||
              sudo chown -R circleci "${dirs[@]}"
 | 
			
		||||
      - restore_cache:
 | 
			
		||||
          keys:
 | 
			
		||||
          - v1-npm-base.xenial.1
 | 
			
		||||
      - restore_cache:
 | 
			
		||||
          keys:
 | 
			
		||||
          - v1-venv-base.xenial.1
 | 
			
		||||
 | 
			
		||||
      - run:
 | 
			
		||||
          name: install dependencies
 | 
			
		||||
          command: |
 | 
			
		||||
            sudo apt-get install -y moreutils
 | 
			
		||||
            rm -f /home/circleci/.gitconfig
 | 
			
		||||
            mispipe "tools/travis/setup-backend" ts
 | 
			
		||||
 | 
			
		||||
      - save_cache:
 | 
			
		||||
          paths:
 | 
			
		||||
            - /srv/zulip-npm-cache
 | 
			
		||||
          key: v1-npm-base.xenial.1
 | 
			
		||||
      - save_cache:
 | 
			
		||||
          paths:
 | 
			
		||||
            - /srv/zulip-venv-cache
 | 
			
		||||
          key: v1-venv-base.xenial.1
 | 
			
		||||
 | 
			
		||||
      - run:
 | 
			
		||||
          name: run backend tests
 | 
			
		||||
          command: |
 | 
			
		||||
            . /srv/zulip-py3-venv/bin/activate
 | 
			
		||||
            mispipe ./tools/travis/backend ts
 | 
			
		||||
 | 
			
		||||
      -  run:
 | 
			
		||||
          name: upload coverage report
 | 
			
		||||
          command: |
 | 
			
		||||
            . /srv/zulip-py3-venv/bin/activate
 | 
			
		||||
            pip install codecov && codecov \
 | 
			
		||||
              || echo "Error in uploading coverage reports to codecov.io."
 | 
			
		||||
 | 
			
		||||
workflows:
 | 
			
		||||
  version: 2
 | 
			
		||||
  build:
 | 
			
		||||
    jobs:
 | 
			
		||||
      - "trusty-python-3.4"
 | 
			
		||||
      - "xenial-python-3.5"
 | 
			
		||||
@@ -5,6 +5,6 @@ coverage:
 | 
			
		||||
    project:
 | 
			
		||||
      default:
 | 
			
		||||
        target: auto
 | 
			
		||||
        threshold: 0.50
 | 
			
		||||
        threshold: 0.03
 | 
			
		||||
        base: auto
 | 
			
		||||
    patch: off
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ charset = utf-8
 | 
			
		||||
trim_trailing_whitespace = true
 | 
			
		||||
insert_final_newline = true
 | 
			
		||||
 | 
			
		||||
[*.{sh,py,pyi,js,json,yml,xml,css,md,markdown,handlebars,html}]
 | 
			
		||||
[*.{sh,py,js,json,yml,xml,css,md,markdown,handlebars,html}]
 | 
			
		||||
indent_style = space
 | 
			
		||||
indent_size = 4
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,6 @@
 | 
			
		||||
        "Dropbox": false,
 | 
			
		||||
        "SockJS": false,
 | 
			
		||||
        "marked": false,
 | 
			
		||||
        "md5": false,
 | 
			
		||||
        "moment": false,
 | 
			
		||||
        "i18n": false,
 | 
			
		||||
        "DynamicText": false,
 | 
			
		||||
@@ -32,21 +31,13 @@
 | 
			
		||||
        "popovers": false,
 | 
			
		||||
        "server_events": false,
 | 
			
		||||
        "server_events_dispatch": false,
 | 
			
		||||
        "message_scroll": false,
 | 
			
		||||
        "info_overlay": false,
 | 
			
		||||
        "ui": false,
 | 
			
		||||
        "ui_report": false,
 | 
			
		||||
        "night_mode": false,
 | 
			
		||||
        "ui_util": false,
 | 
			
		||||
        "lightbox": false,
 | 
			
		||||
        "input_pill": false,
 | 
			
		||||
        "user_pill": false,
 | 
			
		||||
        "compose_pm_pill": false,
 | 
			
		||||
        "stream_color": false,
 | 
			
		||||
        "people": false,
 | 
			
		||||
        "user_groups": false,
 | 
			
		||||
        "navigate": false,
 | 
			
		||||
        "toMarkdown": false,
 | 
			
		||||
        "settings_account": false,
 | 
			
		||||
        "settings_display": false,
 | 
			
		||||
        "settings_notifications": false,
 | 
			
		||||
@@ -56,13 +47,9 @@
 | 
			
		||||
        "settings_sections": false,
 | 
			
		||||
        "settings_emoji": false,
 | 
			
		||||
        "settings_org": false,
 | 
			
		||||
        "settings_ui": false,
 | 
			
		||||
        "settings_users": false,
 | 
			
		||||
        "settings_streams": false,
 | 
			
		||||
        "settings_filters": false,
 | 
			
		||||
        "settings_invites": false,
 | 
			
		||||
        "settings_user_groups": false,
 | 
			
		||||
        "settings_profile_fields": false,
 | 
			
		||||
        "settings": false,
 | 
			
		||||
        "resize": false,
 | 
			
		||||
        "loading": false,
 | 
			
		||||
@@ -71,7 +58,6 @@
 | 
			
		||||
        "typing_data": false,
 | 
			
		||||
        "typing_status": false,
 | 
			
		||||
        "sent_messages": false,
 | 
			
		||||
        "transmit": false,
 | 
			
		||||
        "compose": false,
 | 
			
		||||
        "compose_actions": false,
 | 
			
		||||
        "compose_state": false,
 | 
			
		||||
@@ -93,10 +79,8 @@
 | 
			
		||||
        "gear_menu": false,
 | 
			
		||||
        "hashchange": false,
 | 
			
		||||
        "hash_util": false,
 | 
			
		||||
        "FetchStatus": false,
 | 
			
		||||
        "message_list": false,
 | 
			
		||||
        "Filter": false,
 | 
			
		||||
        "flatpickr": false,
 | 
			
		||||
        "pointer": false,
 | 
			
		||||
        "util": false,
 | 
			
		||||
        "MessageListView": false,
 | 
			
		||||
@@ -161,7 +145,6 @@
 | 
			
		||||
        "recent_senders": false,
 | 
			
		||||
        "unread_ui": false,
 | 
			
		||||
        "unread_ops": false,
 | 
			
		||||
        "upload": false,
 | 
			
		||||
        "user_events": false,
 | 
			
		||||
        "Plotly": false,
 | 
			
		||||
        "emoji_codes": false,
 | 
			
		||||
@@ -172,12 +155,8 @@
 | 
			
		||||
        "hotspots": false,
 | 
			
		||||
        "compose_ui": false,
 | 
			
		||||
        "common": false,
 | 
			
		||||
        "panels": false,
 | 
			
		||||
        "PerfectScrollbar": false
 | 
			
		||||
        "desktop_notifications_panel": false
 | 
			
		||||
    },
 | 
			
		||||
    "plugins": [
 | 
			
		||||
        "eslint-plugin-empty-returns"
 | 
			
		||||
    ],
 | 
			
		||||
    "rules": {
 | 
			
		||||
        "array-callback-return": "error",
 | 
			
		||||
        "array-bracket-spacing": "error",
 | 
			
		||||
@@ -197,7 +176,6 @@
 | 
			
		||||
        "complexity": [ 0, 4 ],
 | 
			
		||||
        "curly": 2,
 | 
			
		||||
        "dot-notation": [ "error", { "allowKeywords": true } ],
 | 
			
		||||
        "empty-returns/main": "error",
 | 
			
		||||
        "eol-last": [ "error", "always" ],
 | 
			
		||||
        "eqeqeq": 2,
 | 
			
		||||
        "func-style": [ "off", "expression" ],
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
								
							@@ -1,7 +1,6 @@
 | 
			
		||||
*   text=auto eol=lf
 | 
			
		||||
*.gif binary
 | 
			
		||||
*.jpg binary
 | 
			
		||||
*.jpeg binary
 | 
			
		||||
*.eot binary
 | 
			
		||||
*.woff binary
 | 
			
		||||
*.woff2 binary
 | 
			
		||||
@@ -10,4 +9,3 @@
 | 
			
		||||
*.png binary
 | 
			
		||||
*.otf binary
 | 
			
		||||
*.tif binary
 | 
			
		||||
yarn.lock binary
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										14
									
								
								.github/pull_request_template.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								.github/pull_request_template.md
									
									
									
									
										vendored
									
									
								
							@@ -1,14 +0,0 @@
 | 
			
		||||
<!-- What's this PR for?  (Just a link to an issue is fine.) -->
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
**Testing Plan:** <!-- How have you tested? -->
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
**GIFs or Screenshots:** <!-- If a UI change.  See:
 | 
			
		||||
  https://zulip.readthedocs.io/en/latest/tutorials/screenshot-and-gif-software.html
 | 
			
		||||
  -->
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
<!-- Also be sure to make clear, coherent commits:
 | 
			
		||||
  https://zulip.readthedocs.io/en/latest/contributing/version-control.html
 | 
			
		||||
  -->
 | 
			
		||||
							
								
								
									
										17
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										17
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -12,26 +12,19 @@
 | 
			
		||||
#  * Subdirectories with several internal things to ignore get their own
 | 
			
		||||
#    `.gitignore` files.
 | 
			
		||||
#
 | 
			
		||||
#  * Comments must be on their own line.  (Otherwise they don't work.)
 | 
			
		||||
#
 | 
			
		||||
# See `git help ignore` for details on the format.
 | 
			
		||||
 | 
			
		||||
## Config files for the dev environment
 | 
			
		||||
/zproject/dev-secrets.conf
 | 
			
		||||
/tools/conf.ini
 | 
			
		||||
/tools/custom_provision
 | 
			
		||||
/tools/droplets/conf.ini
 | 
			
		||||
 | 
			
		||||
## Byproducts of setting up and using the dev environment
 | 
			
		||||
*.pyc
 | 
			
		||||
package-lock.json
 | 
			
		||||
 | 
			
		||||
/.vagrant
 | 
			
		||||
/var
 | 
			
		||||
 | 
			
		||||
# Dockerfiles generated for CircleCI
 | 
			
		||||
/tools/circleci/images
 | 
			
		||||
 | 
			
		||||
# Static build
 | 
			
		||||
*.mo
 | 
			
		||||
npm-debug.log
 | 
			
		||||
@@ -44,11 +37,6 @@ npm-debug.log
 | 
			
		||||
# Test / analysis tools
 | 
			
		||||
.coverage
 | 
			
		||||
 | 
			
		||||
## Files (or really symlinks) created in a prod deployment
 | 
			
		||||
/zproject/prod_settings.py
 | 
			
		||||
/zulip-current-venv
 | 
			
		||||
/zulip-py3-venv
 | 
			
		||||
 | 
			
		||||
## Files left by various editors and local environments
 | 
			
		||||
# (Ideally these should be in everyone's respective personal gitignore files.)
 | 
			
		||||
*~
 | 
			
		||||
@@ -63,11 +51,6 @@ zulip.kdev4
 | 
			
		||||
*.sublime-workspace
 | 
			
		||||
.vscode/
 | 
			
		||||
*.DS_Store
 | 
			
		||||
# .cache/ is generated by VSCode's test runner
 | 
			
		||||
.cache/
 | 
			
		||||
.eslintcache
 | 
			
		||||
 | 
			
		||||
## Miscellaneous
 | 
			
		||||
# (Ideally this section is empty.)
 | 
			
		||||
zthumbor/thumbor_local_settings.py
 | 
			
		||||
.transifexrc
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										6
									
								
								.gitlint
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								.gitlint
									
									
									
									
									
								
							@@ -1,10 +1,10 @@
 | 
			
		||||
[general]
 | 
			
		||||
ignore=title-trailing-punctuation, body-min-length, body-is-missing, title-imperative-mood
 | 
			
		||||
ignore=title-trailing-punctuation, body-min-length, body-is-missing
 | 
			
		||||
 | 
			
		||||
extra-path=tools/lib/gitlint-rules.py
 | 
			
		||||
 | 
			
		||||
[title-match-regex-allow-exception]
 | 
			
		||||
regex=^(.+:\ )?[A-Z].+\.$
 | 
			
		||||
[title-match-regex]
 | 
			
		||||
regex=^.+\.$
 | 
			
		||||
 | 
			
		||||
[title-max-length]
 | 
			
		||||
line-length=76
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +0,0 @@
 | 
			
		||||
[settings]
 | 
			
		||||
line_length = 79
 | 
			
		||||
multi_line_output = 2
 | 
			
		||||
balanced_wrapping = true
 | 
			
		||||
known_third_party = django, ujson, sqlalchemy
 | 
			
		||||
known_first_party = zerver, zproject, version, confirmation, zilencer, analytics, frontend_tests, scripts, corporate
 | 
			
		||||
sections = FUTURE, STDLIB, THIRDPARTY, FIRSTPARTY, LOCALFOLDER
 | 
			
		||||
lines_after_imports = 1
 | 
			
		||||
							
								
								
									
										22
									
								
								.travis.yml
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								.travis.yml
									
									
									
									
									
								
							@@ -1,11 +1,9 @@
 | 
			
		||||
# See https://zulip.readthedocs.io/en/latest/testing/travis.html for
 | 
			
		||||
# See https://zulip.readthedocs.io/en/latest/travis.html for
 | 
			
		||||
# high-level documentation on our Travis CI setup.
 | 
			
		||||
dist: trusty
 | 
			
		||||
group: deprecated-2017Q4
 | 
			
		||||
install:
 | 
			
		||||
  # Disable sometimes-broken sources.list in Travis base images
 | 
			
		||||
  - sudo rm -vf /etc/apt/sources.list.d/*
 | 
			
		||||
  - sudo apt-get update
 | 
			
		||||
  # Disable broken riak sources.list in Travis base image 2017-10-18
 | 
			
		||||
  - rm -vf "/etc/apt/sources.list.d/*riak*"
 | 
			
		||||
 | 
			
		||||
  # Disable Travis CI's built-in NVM installation
 | 
			
		||||
  - mispipe "mv ~/.nvm ~/.travis-nvm-disabled" ts
 | 
			
		||||
@@ -35,7 +33,6 @@ cache:
 | 
			
		||||
    - $HOME/zulip-npm-cache
 | 
			
		||||
    - $HOME/zulip-emoji-cache
 | 
			
		||||
    - $HOME/node
 | 
			
		||||
    - $HOME/misc
 | 
			
		||||
env:
 | 
			
		||||
  global:
 | 
			
		||||
    - BOTO_CONFIG=/tmp/nowhere
 | 
			
		||||
@@ -48,9 +45,14 @@ matrix:
 | 
			
		||||
    # that doesn't seem to be documented, but it's what we see empirically.
 | 
			
		||||
    # We only get 4 jobs running at a time, so we try to make the first few
 | 
			
		||||
    # the most likely to break.
 | 
			
		||||
    - python: "3.4"
 | 
			
		||||
      env: TEST_SUITE=frontend
 | 
			
		||||
    - python: "3.4"
 | 
			
		||||
      env: TEST_SUITE=backend
 | 
			
		||||
    - python: "3.4"
 | 
			
		||||
      env: TEST_SUITE=production
 | 
			
		||||
    # Other suites moved to CircleCI -- see .circleci/.
 | 
			
		||||
    - python: "3.5"
 | 
			
		||||
      env: TEST_SUITE=backend
 | 
			
		||||
sudo: required
 | 
			
		||||
addons:
 | 
			
		||||
  artifacts:
 | 
			
		||||
@@ -65,3 +67,9 @@ addons:
 | 
			
		||||
      - moreutils
 | 
			
		||||
after_success:
 | 
			
		||||
  - codecov
 | 
			
		||||
notifications:
 | 
			
		||||
  webhooks:
 | 
			
		||||
    urls:
 | 
			
		||||
      - https://zulip.org/zulipbot/travis
 | 
			
		||||
    on_success: always
 | 
			
		||||
    on_failure: always
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								.tx/config
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								.tx/config
									
									
									
									
									
								
							@@ -13,21 +13,3 @@ source_file = static/locale/en/translations.json
 | 
			
		||||
source_lang = en
 | 
			
		||||
type = KEYVALUEJSON
 | 
			
		||||
file_filter = static/locale/<lang>/translations.json
 | 
			
		||||
 | 
			
		||||
[zulip.messages]
 | 
			
		||||
source_file = static/locale/en/mobile.json
 | 
			
		||||
source_lang = en
 | 
			
		||||
type = KEYVALUEJSON
 | 
			
		||||
file_filter = static/locale/<lang>/mobile.json
 | 
			
		||||
 | 
			
		||||
[zulip-test.djangopo]
 | 
			
		||||
source_file = static/locale/en/LC_MESSAGES/django.po
 | 
			
		||||
source_lang = en
 | 
			
		||||
type = PO
 | 
			
		||||
file_filter = static/locale/<lang>/LC_MESSAGES/django.po
 | 
			
		||||
 | 
			
		||||
[zulip-test.translationsjson]
 | 
			
		||||
source_file = static/locale/en/translations.json
 | 
			
		||||
source_lang = en
 | 
			
		||||
type = KEYVALUEJSON
 | 
			
		||||
file_filter = static/locale/<lang>/translations.json
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										333
									
								
								CONTRIBUTING.md
									
									
									
									
									
								
							
							
						
						
									
										333
									
								
								CONTRIBUTING.md
									
									
									
									
									
								
							@@ -1,333 +0,0 @@
 | 
			
		||||
# Contributing to Zulip
 | 
			
		||||
 | 
			
		||||
Welcome to the Zulip community!
 | 
			
		||||
 | 
			
		||||
## Community
 | 
			
		||||
 | 
			
		||||
The
 | 
			
		||||
[Zulip community server](https://zulip.readthedocs.io/en/latest/contributing/chat-zulip-org.html)
 | 
			
		||||
is the primary communication forum for the Zulip community. It is a good
 | 
			
		||||
place to start whether you have a question, are a new contributor, are a new
 | 
			
		||||
user, or anything else. Make sure to read the
 | 
			
		||||
[community norms](https://zulip.readthedocs.io/en/latest/contributing/chat-zulip-org.html#community-norms)
 | 
			
		||||
before posting. The Zulip community is also governed by a
 | 
			
		||||
[code of conduct](https://zulip.readthedocs.io/en/latest/code-of-conduct.html).
 | 
			
		||||
 | 
			
		||||
You can subscribe to zulip-devel@googlegroups.com for a lower traffic (~1
 | 
			
		||||
email/month) way to hear about things like mentorship opportunities with Google
 | 
			
		||||
Code-in, in-person sprints at conferences, and other opportunities to
 | 
			
		||||
contribute.
 | 
			
		||||
 | 
			
		||||
## Ways to contribute
 | 
			
		||||
 | 
			
		||||
To make a code or documentation contribution, read our
 | 
			
		||||
[step-by-step guide](#your-first-codebase-contribution) to getting
 | 
			
		||||
started with the Zulip codebase. A small sample of the type of work that
 | 
			
		||||
needs doing:
 | 
			
		||||
* Bug squashing and feature development on our Python/Django
 | 
			
		||||
  [backend](https://github.com/zulip/zulip), web
 | 
			
		||||
  [frontend](https://github.com/zulip/zulip), React Native
 | 
			
		||||
  [mobile app](https://github.com/zulip/zulip-mobile), or Electron
 | 
			
		||||
  [desktop app](https://github.com/zulip/zulip-electron).
 | 
			
		||||
* Building out our
 | 
			
		||||
  [Python API and bots](https://github.com/zulip/python-zulip-api) framework.
 | 
			
		||||
* [Writing an integration](https://zulipchat.com/api/integration-guide).
 | 
			
		||||
* Improving our [user](https://zulipchat.com/help/) or
 | 
			
		||||
  [developer](https://zulip.readthedocs.io/en/latest/) documentation.
 | 
			
		||||
* [Reviewing code](https://zulip.readthedocs.io/en/latest/contributing/code-reviewing.html)
 | 
			
		||||
  and manually testing pull requests.
 | 
			
		||||
 | 
			
		||||
**Non-code contributions**: Some of the most valuable ways to contribute
 | 
			
		||||
don't require touching the codebase at all. We list a few of them below:
 | 
			
		||||
 | 
			
		||||
* [Reporting issues](#reporting-issues), including both feature requests and
 | 
			
		||||
  bug reports.
 | 
			
		||||
* [Giving feedback](#user-feedback) if you are evaluating or using Zulip.
 | 
			
		||||
* [Translating](https://zulip.readthedocs.io/en/latest/translating/translating.html)
 | 
			
		||||
  Zulip.
 | 
			
		||||
* [Outreach](#zulip-outreach): Star us on GitHub, upvote us
 | 
			
		||||
  on product comparison sites, or write for [the Zulip blog](http://blog.zulip.org/).
 | 
			
		||||
 | 
			
		||||
## Your first (codebase) contribution
 | 
			
		||||
 | 
			
		||||
This section has a step by step guide to starting as a Zulip codebase
 | 
			
		||||
contributor. It's long, but don't worry about doing all the steps perfectly;
 | 
			
		||||
no one gets it right the first time, and there are a lot of people available
 | 
			
		||||
to help.
 | 
			
		||||
* First, make an account on the
 | 
			
		||||
  [Zulip community server](https://zulip.readthedocs.io/en/latest/contributing/chat-zulip-org.html),
 | 
			
		||||
  paying special attention to the community norms. If you'd like, introduce
 | 
			
		||||
  yourself in
 | 
			
		||||
  [#new members](https://chat.zulip.org/#narrow/stream/new.20members), using
 | 
			
		||||
  your name as the topic. Bonus: tell us about your first impressions of
 | 
			
		||||
  Zulip, and anything that felt confusing/broken as you started using the
 | 
			
		||||
  product.
 | 
			
		||||
* Read [What makes a great Zulip contributor](#what-makes-a-great-zulip-contributor).
 | 
			
		||||
* [Install the development environment](https://zulip.readthedocs.io/en/latest/development/overview.html),
 | 
			
		||||
  getting help in
 | 
			
		||||
  [#development help](https://chat.zulip.org/#narrow/stream/development.20help)
 | 
			
		||||
  if you run into any troubles.
 | 
			
		||||
* Read the
 | 
			
		||||
  [Zulip guide to Git](https://zulip.readthedocs.io/en/latest/git/index.html)
 | 
			
		||||
  and do the Git tutorial (coming soon) if you are unfamiliar with Git,
 | 
			
		||||
  getting help in
 | 
			
		||||
  [#git help](https://chat.zulip.org/#narrow/stream/git.20help) if you run
 | 
			
		||||
  into any troubles.
 | 
			
		||||
* Sign the
 | 
			
		||||
  [Dropbox Contributor License Agreement](https://opensource.dropbox.com/cla/).
 | 
			
		||||
 | 
			
		||||
### Picking an issue
 | 
			
		||||
 | 
			
		||||
Now, you're ready to pick your first issue! There are hundreds of open issues
 | 
			
		||||
in the main codebase alone. This section will help you find an issue to work
 | 
			
		||||
on.
 | 
			
		||||
 | 
			
		||||
* If you're interested in
 | 
			
		||||
  [mobile](https://github.com/zulip/zulip-mobile/issues?q=is%3Aopen+is%3Aissue),
 | 
			
		||||
  [desktop](https://github.com/zulip/zulip-electron/issues?q=is%3Aopen+is%3Aissue),
 | 
			
		||||
  or
 | 
			
		||||
  [bots](https://github.com/zulip/python-zulip-api/issues?q=is%3Aopen+is%3Aissue)
 | 
			
		||||
  development, check the respective links for open issues, or post in
 | 
			
		||||
  [#mobile](https://chat.zulip.org/#narrow/stream/mobile),
 | 
			
		||||
  [#electron](https://chat.zulip.org/#narrow/stream/electron), or
 | 
			
		||||
  [#bots](https://chat.zulip.org/#narrow/stream/bots).
 | 
			
		||||
* For the main server and web repository, start by looking through issues
 | 
			
		||||
  with the label
 | 
			
		||||
  [good first issue](https://github.com/zulip/zulip/issues?q=is%3Aopen+is%3Aissue+label%3A"good+first+issue").
 | 
			
		||||
  These are smaller projects particularly suitable for a first contribution.
 | 
			
		||||
* We also partition all of our issues in the main repo into areas like
 | 
			
		||||
  admin, compose, emoji, hotkeys, i18n, onboarding, search, etc. Look
 | 
			
		||||
  through our [list of labels](https://github.com/zulip/zulip/labels), and
 | 
			
		||||
  click on some of the `area:` labels to see all the issues related to your
 | 
			
		||||
  areas of interest.
 | 
			
		||||
* If the lists of issues are overwhelming, post in
 | 
			
		||||
  [#new members](https://chat.zulip.org/#narrow/stream/new.20members) with a
 | 
			
		||||
  bit about your background and interests, and we'll help you out. The most
 | 
			
		||||
  important thing to say is whether you're looking for a backend (Python),
 | 
			
		||||
  frontend (JavaScript), mobile (React Native), desktop (Electron),
 | 
			
		||||
  documentation (English) or visual design (JavaScript + CSS) issue, and a
 | 
			
		||||
  bit about your programming experience and available time.
 | 
			
		||||
 | 
			
		||||
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. If you
 | 
			
		||||
have a new feature you'd like to add, we recommend you start by posting in
 | 
			
		||||
[#new members](https://chat.zulip.org/#narrow/stream/new.20members) with the
 | 
			
		||||
feature idea and the problem that you're hoping to solve.
 | 
			
		||||
 | 
			
		||||
Other notes:
 | 
			
		||||
* For a first pull request, it's better to aim for a smaller contribution
 | 
			
		||||
  than a bigger one. Many first contributions have fewer than 10 lines of
 | 
			
		||||
  changes (not counting changes to tests).
 | 
			
		||||
* The full list of issues looking for a contributor can be found with the
 | 
			
		||||
  [help wanted](https://github.com/zulip/zulip/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22)
 | 
			
		||||
  label.
 | 
			
		||||
* For most new contributors, there's a lot to learn while making your first
 | 
			
		||||
  pull request. It's OK if it takes you a while; that's normal! You'll be
 | 
			
		||||
  able to work a lot faster as you build experience.
 | 
			
		||||
 | 
			
		||||
### Working on an issue
 | 
			
		||||
 | 
			
		||||
To work on an issue, claim it by adding a comment with `@zulipbot claim` to
 | 
			
		||||
the issue thread. [Zulipbot](https://github.com/zulip/zulipbot) is a GitHub
 | 
			
		||||
workflow bot; it will assign you to the issue and label the issue as "in
 | 
			
		||||
progress". Some additional notes:
 | 
			
		||||
 | 
			
		||||
* 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. You can ask questions on
 | 
			
		||||
  chat.zulip.org, or on the GitHub issue or pull request.
 | 
			
		||||
* We encourage early pull requests for work in progress. Prefix the title of
 | 
			
		||||
  work in progress pull requests with `[WIP]`, and remove the prefix when
 | 
			
		||||
  you think it might be mergeable and want it to be reviewed.
 | 
			
		||||
* After updating a PR, add a comment to the GitHub thread mentioning that it
 | 
			
		||||
  is ready for another review. GitHub only notifies maintainers of the
 | 
			
		||||
  changes when you post a comment, so if you don't, your PR will likely be
 | 
			
		||||
  neglected by accident!
 | 
			
		||||
 | 
			
		||||
### And beyond
 | 
			
		||||
 | 
			
		||||
A great place to look for a second issue is to look for issues with the same
 | 
			
		||||
`area:` label as the last issue you resolved. You'll be able to reuse the
 | 
			
		||||
work you did learning how that part of the codebase works. Also, the path to
 | 
			
		||||
becoming a core developer often involves taking ownership of one of these area
 | 
			
		||||
labels.
 | 
			
		||||
 | 
			
		||||
## What makes a great Zulip contributor?
 | 
			
		||||
 | 
			
		||||
Zulip runs a lot of [internship programs](#internship-programs), so we have
 | 
			
		||||
a lot of experience with new contributors. In our experience, these are the
 | 
			
		||||
best predictors of success:
 | 
			
		||||
 | 
			
		||||
* Posting good questions. This generally means explaining your current
 | 
			
		||||
  understanding, saying what you've done or tried so far, and including
 | 
			
		||||
  tracebacks or other error messages if appropriate.
 | 
			
		||||
* Learning and practicing
 | 
			
		||||
  [Git commit discipline](https://zulip.readthedocs.io/en/latest/contributing/version-control.html#commit-discipline).
 | 
			
		||||
* Submitting carefully tested code. This generally means checking your work
 | 
			
		||||
  through a combination of automated tests and manually clicking around the
 | 
			
		||||
  UI trying to find bugs in your work. See
 | 
			
		||||
  [things to look for](https://zulip.readthedocs.io/en/latest/contributing/code-reviewing.html#things-to-look-for)
 | 
			
		||||
  for additional ideas.
 | 
			
		||||
* Posting
 | 
			
		||||
  [screenshots or GIFs](https://zulip.readthedocs.io/en/latest/tutorials/screenshot-and-gif-software.html)
 | 
			
		||||
  for frontend changes.
 | 
			
		||||
* Being responsive to feedback on pull requests. This means incorporating or
 | 
			
		||||
  responding to all suggested changes, and leaving a note if you won't be
 | 
			
		||||
  able to address things within a few days.
 | 
			
		||||
* Being helpful and friendly on chat.zulip.org.
 | 
			
		||||
 | 
			
		||||
These are also the main criteria we use to select interns for all of our
 | 
			
		||||
internship programs.
 | 
			
		||||
 | 
			
		||||
## Reporting issues
 | 
			
		||||
 | 
			
		||||
If you find an easily reproducible bug and/or are experienced in reporting
 | 
			
		||||
bugs, feel free to just open an issue on the relevant project on GitHub.
 | 
			
		||||
 | 
			
		||||
If you have a feature request or are not yet sure what the underlying bug
 | 
			
		||||
is, the best place to post issues is
 | 
			
		||||
[#issues](https://chat.zulip.org/#narrow/stream/issues) (or
 | 
			
		||||
[#mobile](https://chat.zulip.org/#narrow/stream/mobile) or
 | 
			
		||||
[#electron](https://chat.zulip.org/#narrow/stream/electron)) on the
 | 
			
		||||
[Zulip community server](https://zulip.readthedocs.io/en/latest/contributing/chat-zulip-org.html).
 | 
			
		||||
This allows us to interactively figure out what is going on, let you know if
 | 
			
		||||
a similar issue has already been opened, and collect any other information
 | 
			
		||||
we need. Choose a 2-4 word topic that describes the issue, explain the issue
 | 
			
		||||
and how to reproduce it if known, your browser/OS if relevant, and a
 | 
			
		||||
[screenshot or screenGIF](https://zulip.readthedocs.io/en/latest/tutorials/screenshot-and-gif-software.html)
 | 
			
		||||
if appropriate.
 | 
			
		||||
 | 
			
		||||
**Reporting security issues**. Please do not report security issues
 | 
			
		||||
  publicly, including on public streams on chat.zulip.org. You can email
 | 
			
		||||
  zulip-security@googlegroups.com. We create a CVE for every security issue.
 | 
			
		||||
 | 
			
		||||
## User feedback
 | 
			
		||||
 | 
			
		||||
Nearly every feature we develop starts with a user request. If you are part
 | 
			
		||||
of a group that is either using or considering using Zulip, we would love to
 | 
			
		||||
hear about your experience with the product. If you're not sure what to
 | 
			
		||||
write, here are some questions we're always very curious to know the answer
 | 
			
		||||
to:
 | 
			
		||||
 | 
			
		||||
* Evaluation: What is the process by which your organization chose or will
 | 
			
		||||
  choose a group chat product?
 | 
			
		||||
* Pros and cons: What are the pros and cons of Zulip for your organization,
 | 
			
		||||
  and the pros and cons of other products you are evaluating?
 | 
			
		||||
* Features: What are the features that are most important for your
 | 
			
		||||
  organization? In the best case scenario, what would your chat solution do
 | 
			
		||||
  for you?
 | 
			
		||||
* Onboarding: If you remember it, what was your impression during your first
 | 
			
		||||
  few minutes of using Zulip? What did you notice, and how did you feel? Was
 | 
			
		||||
  there anything that stood out to you as confusing, or broken, or great?
 | 
			
		||||
* Organization: What does your organization do? How big is the organization?
 | 
			
		||||
  A link to your organization's website?
 | 
			
		||||
 | 
			
		||||
## Internship programs
 | 
			
		||||
 | 
			
		||||
Zulip runs internship programs with
 | 
			
		||||
[Outreachy](https://www.outreachy.org/),
 | 
			
		||||
[Google Summer of Code (GSoC)](https://developers.google.com/open-source/gsoc/)
 | 
			
		||||
[1], and the
 | 
			
		||||
[MIT Externship program](https://alum.mit.edu/students/NetworkwithAlumni/ExternshipProgram),
 | 
			
		||||
and has in the past taken summer interns from Harvard, MIT, and
 | 
			
		||||
Stanford.
 | 
			
		||||
 | 
			
		||||
While each third-party program has its own rules and requirements, the
 | 
			
		||||
Zulip community's approaches all of these programs with these ideas in
 | 
			
		||||
mind:
 | 
			
		||||
* We try to make the application process as valuable for the applicant as
 | 
			
		||||
  possible. Expect high quality code reviews, a supportive community, and
 | 
			
		||||
  publicly viewable patches you can link to from your resume, regardless of
 | 
			
		||||
  whether you are selected.
 | 
			
		||||
* To apply, you'll have to submit at least one pull request to a Zulip
 | 
			
		||||
  repository.  Most students accepted to one of our programs have
 | 
			
		||||
  several merged pull requests (including at least one larger PR) by
 | 
			
		||||
  the time of the application deadline.
 | 
			
		||||
* The main criteria we use is quality of your best contributions, and
 | 
			
		||||
  the bullets listed at
 | 
			
		||||
  [What makes a great Zulip contributor](#what-makes-a-great-zulip-contributor).
 | 
			
		||||
  Because we focus on evaluating your best work, it doesn't hurt your
 | 
			
		||||
  application to makes mistakes in your first few PRs as long as your
 | 
			
		||||
  work improves.
 | 
			
		||||
 | 
			
		||||
Zulip also participates in
 | 
			
		||||
[Google Code-In](https://developers.google.com/open-source/gci/). Our
 | 
			
		||||
selection criteria for Finalists and Grand Prize Winners is the same as our
 | 
			
		||||
selection criteria for interns above.
 | 
			
		||||
 | 
			
		||||
Most of our interns end up sticking around the project long-term, and many
 | 
			
		||||
quickly become core team members. We hope you apply!
 | 
			
		||||
 | 
			
		||||
### Google Summer of Code
 | 
			
		||||
 | 
			
		||||
GSoC is by far the largest of our internship programs (we had 14 GSoC
 | 
			
		||||
students in summer 2017).  While we don't control how many slots
 | 
			
		||||
Google allocates to Zulip, we hope to mentor a similar number of
 | 
			
		||||
students in 2018.
 | 
			
		||||
 | 
			
		||||
If you're reading this well before the application deadline and want
 | 
			
		||||
to make your application strong, we recommend getting involved in the
 | 
			
		||||
community and fixing issues in Zulip now. Having good contributions
 | 
			
		||||
and building a reputation for doing good work is best way to have a
 | 
			
		||||
strong application.  About half of Zulip's GSoC students for Summer
 | 
			
		||||
2017 had made significant contributions to the project by February
 | 
			
		||||
2017, and about half had not.  Our
 | 
			
		||||
[GSoC project ideas page][gsoc-guide] has lots more details on how
 | 
			
		||||
Zulip does GSoC, as well as project ideas (though the project idea
 | 
			
		||||
list is maintained only during the GSoC application period, so if
 | 
			
		||||
you're looking at some other time of year, the project list is likely
 | 
			
		||||
out-of-date).
 | 
			
		||||
 | 
			
		||||
We also have in some past years run a Zulip Summer of Code (ZSoC)
 | 
			
		||||
program for students who we didn't have enough slots to accept for
 | 
			
		||||
GSoC but were able to find funding for.  Student expectations are the
 | 
			
		||||
same as with GSoC, and it has no separate application process; your
 | 
			
		||||
GSoC application is your ZSoC application.  If we'd like to select you
 | 
			
		||||
for ZSoC, we'll contact you when the GSoC results are announced.
 | 
			
		||||
 | 
			
		||||
[gsoc-guide]: https://zulip.readthedocs.io/en/latest/overview/gsoc-ideas.html
 | 
			
		||||
[gsoc-faq]: https://developers.google.com/open-source/gsoc/faq
 | 
			
		||||
 | 
			
		||||
[1] Formally, [GSoC isn't an internship][gsoc-faq], but it is similar
 | 
			
		||||
enough that we're treating it as such for the purposes of this
 | 
			
		||||
documentation.
 | 
			
		||||
 | 
			
		||||
## Zulip Outreach
 | 
			
		||||
 | 
			
		||||
**Upvoting Zulip**. Upvotes and reviews make a big difference in the public
 | 
			
		||||
perception of projects like Zulip. We've collected a few sites below
 | 
			
		||||
where we know Zulip has been discussed. Doing everything in the following
 | 
			
		||||
list typically takes about 15 minutes.
 | 
			
		||||
* Star us on GitHub. There are four main repositories:
 | 
			
		||||
  [server/web](https://github.com/zulip/zulip),
 | 
			
		||||
  [mobile](https://github.com/zulip/zulip-mobile),
 | 
			
		||||
  [desktop](https://github.com/zulip/zulip-electron), and
 | 
			
		||||
  [Python API](https://github.com/zulip/python-zulip-api).
 | 
			
		||||
* [Follow us](https://twitter.com/zulip) on Twitter.
 | 
			
		||||
 | 
			
		||||
For both of the following, you'll need to make an account on the site if you
 | 
			
		||||
don't already have one.
 | 
			
		||||
 | 
			
		||||
* [Like Zulip](https://alternativeto.net/software/zulip-chat-server/) on
 | 
			
		||||
  AlternativeTo. We recommend upvoting a couple of other products you like
 | 
			
		||||
  as well, both to give back to their community, and since single-upvote
 | 
			
		||||
  accounts are generally given less weight. You can also
 | 
			
		||||
  [upvote Zulip](https://alternativeto.net/software/slack/) on their page
 | 
			
		||||
  for Slack.
 | 
			
		||||
* [Add Zulip to your stack](https://stackshare.io/zulip) on StackShare, star
 | 
			
		||||
  it, and upvote the reasons why people like Zulip that you find most
 | 
			
		||||
  compelling. Again, we recommend adding a few other products that you like
 | 
			
		||||
  as well.
 | 
			
		||||
 | 
			
		||||
We have a doc with more detailed instructions and a few other sites, if you
 | 
			
		||||
have been using Zulip for a while and want to contribute more.
 | 
			
		||||
 | 
			
		||||
**Blog posts**. Writing a blog post about your experiences with Zulip, or
 | 
			
		||||
about a technical aspect of Zulip can be a great way to spread the word
 | 
			
		||||
about Zulip.
 | 
			
		||||
 | 
			
		||||
We also occasionally [publish](http://blog.zulip.org/) longer form
 | 
			
		||||
articles related to Zulip. Our posts typically get tens of thousands
 | 
			
		||||
of views, and we always have good ideas for blog posts that we can
 | 
			
		||||
outline but don't have time to write. If you are an experienced writer
 | 
			
		||||
or copyeditor, send us a portfolio; we'd love to talk!
 | 
			
		||||
							
								
								
									
										46
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						
									
										46
									
								
								LICENSE
									
									
									
									
									
								
							@@ -1,24 +1,3 @@
 | 
			
		||||
Copyright 2011-2017 Dropbox, Inc., Kandra Labs, Inc., and contributors
 | 
			
		||||
 | 
			
		||||
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 ``docs/THIRDPARTY`` file included with this distribution.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
--------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
                                 Apache License
 | 
			
		||||
                           Version 2.0, January 2004
 | 
			
		||||
@@ -196,3 +175,28 @@ see the ``docs/THIRDPARTY`` file included with this distribution.
 | 
			
		||||
      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.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										348
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										348
									
								
								README.md
									
									
									
									
									
								
							@@ -1,83 +1,301 @@
 | 
			
		||||
**[Zulip overview](#zulip-overview)** |
 | 
			
		||||
**[Community](#community)** |
 | 
			
		||||
**[Installing for dev](#installing-the-zulip-development-environment)** |
 | 
			
		||||
**[Installing for production](#running-zulip-in-production)** |
 | 
			
		||||
**[Ways to contribute](#ways-to-contribute)** |
 | 
			
		||||
**[How to get involved](#how-to-get-involved-with-contributing-to-zulip)** |
 | 
			
		||||
**[License](#license)**
 | 
			
		||||
 | 
			
		||||
# Zulip overview
 | 
			
		||||
 | 
			
		||||
Zulip is a powerful, open source group chat application that combines the
 | 
			
		||||
immediacy of real-time chat with the productivity benefits of threaded
 | 
			
		||||
conversations. Zulip is used by open source projects, Fortune 500 companies,
 | 
			
		||||
large standards bodies, and others who need a real-time chat system that
 | 
			
		||||
allows users to easily process hundreds or thousands of messages a day. With
 | 
			
		||||
over 300 contributors merging over 500 commits a month, Zulip is also the
 | 
			
		||||
largest and fastest growing open source group chat project.
 | 
			
		||||
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.
 | 
			
		||||
 | 
			
		||||
[](https://circleci.com/gh/zulip/zulip)
 | 
			
		||||
[](https://travis-ci.org/zulip/zulip)
 | 
			
		||||
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>.
 | 
			
		||||
 | 
			
		||||
[](https://travis-ci.org/zulip/zulip)
 | 
			
		||||
[](https://codecov.io/gh/zulip/zulip)
 | 
			
		||||
[][mypy-coverage]
 | 
			
		||||
[](https://zulip.readthedocs.io/en/latest/)
 | 
			
		||||
[](http://blog.zulip.org/2016/10/13/static-types-in-python-oh-mypy/)
 | 
			
		||||
[](http://zulip.readthedocs.io/en/latest/)
 | 
			
		||||
[](https://chat.zulip.org)
 | 
			
		||||
[](https://twitter.com/zulip)
 | 
			
		||||
[](http://twitter.com/zulip)
 | 
			
		||||
 | 
			
		||||
[mypy-coverage]: https://blog.zulip.org/2016/10/13/static-types-in-python-oh-mypy/
 | 
			
		||||
## Community
 | 
			
		||||
 | 
			
		||||
## Getting started
 | 
			
		||||
There are several places online where folks discuss Zulip.
 | 
			
		||||
 | 
			
		||||
Click on the appropriate link below. If nothing seems to apply,
 | 
			
		||||
join us on the
 | 
			
		||||
[Zulip community server](https://zulip.readthedocs.io/en/latest/contributing/chat-zulip-org.html)
 | 
			
		||||
and tell us what's up!
 | 
			
		||||
* The primary place is the
 | 
			
		||||
  [Zulip development community Zulip server][czo-doc] at
 | 
			
		||||
  chat.zulip.org.
 | 
			
		||||
 | 
			
		||||
You might be interested in:
 | 
			
		||||
* For Google Summer of Code students and applicants, we have
 | 
			
		||||
[a mailing list](https://groups.google.com/forum/#!forum/zulip-gsoc)
 | 
			
		||||
for help, questions, and announcements.  But it's often simpler to
 | 
			
		||||
[visit chat.zulip.org][czo-doc] instead.
 | 
			
		||||
 | 
			
		||||
* **Contributing code**. Check out our
 | 
			
		||||
  [guide for new contributors](https://zulip.readthedocs.io/en/latest/overview/contributing.html)
 | 
			
		||||
  to get started. Zulip prides itself on maintaining a clean and
 | 
			
		||||
  well-tested codebase, and a stock of hundreds of
 | 
			
		||||
  [beginner-friendly issues][beginner-friendly].
 | 
			
		||||
* We have a [public development discussion mailing list][zulip-devel],
 | 
			
		||||
zulip-devel, which is currently pretty low traffic because most
 | 
			
		||||
discussions happen in our public Zulip instance.  We use it to
 | 
			
		||||
announce Zulip developer community gatherings and ask for feedback on
 | 
			
		||||
major technical or design decisions.  It has several hundred
 | 
			
		||||
subscribers, so you can use it to ask questions about features or
 | 
			
		||||
possible bugs, but please don't use it ask for generic help getting
 | 
			
		||||
started as a contributor (e.g. because you want to do Google Summer of
 | 
			
		||||
Code).  The rest of this page covers how to get involved in the Zulip
 | 
			
		||||
project in detail.
 | 
			
		||||
 | 
			
		||||
* **Contributing non-code**.
 | 
			
		||||
  [Report an issue](https://zulip.readthedocs.io/en/latest/overview/contributing.html#reporting-issue),
 | 
			
		||||
  [translate](https://zulip.readthedocs.io/en/latest/translating/translating.html) Zulip
 | 
			
		||||
  into your language,
 | 
			
		||||
  [write](https://zulip.readthedocs.io/en/latest/overview/contributing.html#zulip-outreach)
 | 
			
		||||
  for the Zulip blog, or
 | 
			
		||||
  [give us feedback](https://zulip.readthedocs.io/en/latest/overview/contributing.html#user-feedback). We
 | 
			
		||||
  would love to hear from you, even if you're just trying the product out.
 | 
			
		||||
* Zulip also has a [blog](https://blog.zulip.org/) and
 | 
			
		||||
  [twitter account](https://twitter.com/zulip).
 | 
			
		||||
 | 
			
		||||
* **Supporting Zulip**. Advocate for your organization to use Zulip, write a
 | 
			
		||||
  review in the mobile app stores, or
 | 
			
		||||
  [upvote Zulip](https://zulip.readthedocs.io/en/latest/overview/contributing.html#zulip-outreach) on
 | 
			
		||||
  product comparison sites.
 | 
			
		||||
* Last but not least, we use [GitHub](https://github.com/zulip/zulip)
 | 
			
		||||
to track Zulip-related issues (and store our code, of course).
 | 
			
		||||
Anybody with a GitHub account should be able to create Issues there
 | 
			
		||||
pertaining to bugs or enhancement requests.  We also use Pull Requests
 | 
			
		||||
as our primary mechanism to receive code contributions.
 | 
			
		||||
 | 
			
		||||
* **Checking Zulip out**. The best way to see Zulip in action is to drop by
 | 
			
		||||
  the
 | 
			
		||||
  [Zulip community server](https://zulip.readthedocs.io/en/latest/contributing/chat-zulip-org.html). We
 | 
			
		||||
  also recommend reading Zulip for
 | 
			
		||||
  [open source](https://zulipchat.com/for/open-source/), Zulip for
 | 
			
		||||
  [companies](https://zulipchat.com/for/companies/), or Zulip for
 | 
			
		||||
  [working groups and part time communities](https://zulipchat.com/for/working-groups-and-communities/).
 | 
			
		||||
The Zulip community has a [Code of Conduct][code-of-conduct].
 | 
			
		||||
 | 
			
		||||
* **Running a Zulip server**. Setting up a server takes just a couple of
 | 
			
		||||
  minutes. Zulip runs on Ubuntu 16.04 Xenial and Ubuntu 14.04 Trusty. The
 | 
			
		||||
  installation process is
 | 
			
		||||
  [documented here](https://zulip.readthedocs.io/en/1.7.1/prod.html).
 | 
			
		||||
  Commercial support is available; see <https://zulipchat.com/plans> for
 | 
			
		||||
  details.
 | 
			
		||||
[zulip-devel]: https://groups.google.com/forum/#!forum/zulip-devel
 | 
			
		||||
 | 
			
		||||
* **Using Zulip without setting up a server**. <https://zulipchat.com> offers
 | 
			
		||||
  free and commercial hosting.
 | 
			
		||||
## Installing the Zulip Development environment
 | 
			
		||||
 | 
			
		||||
* **Applying for a Zulip internship**. Zulip runs internship programs with
 | 
			
		||||
  [Outreachy](https://www.outreachy.org/),
 | 
			
		||||
  [Google Summer of Code](https://developers.google.com/open-source/gsoc/),
 | 
			
		||||
  and the
 | 
			
		||||
  [MIT Externship program](https://alum.mit.edu/students/NetworkwithAlumni/ExternshipProgram). Zulip
 | 
			
		||||
  also participates in
 | 
			
		||||
  [Google Code-In](https://developers.google.com/open-source/gci/). More
 | 
			
		||||
  information is available
 | 
			
		||||
  [here](https://zulip.readthedocs.io/en/latest/overview/contributing.html#internship-programs).
 | 
			
		||||
The Zulip development environment is the recommended option for folks
 | 
			
		||||
interested in trying out Zulip, since it is very easy to install.
 | 
			
		||||
This is documented in [the developer installation guide][dev-install].
 | 
			
		||||
 | 
			
		||||
You may also be interested in reading our [blog](http://blog.zulip.org/) or
 | 
			
		||||
following us on [twitter](https://twitter.com/zulip).
 | 
			
		||||
Zulip is distributed under the
 | 
			
		||||
[Apache 2.0](https://github.com/zulip/zulip/blob/master/LICENSE) license.
 | 
			
		||||
## Running Zulip in production
 | 
			
		||||
 | 
			
		||||
[beginner-friendly]: https://github.com/zulip/zulip/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22
 | 
			
		||||
Zulip in production supports Ubuntu 16.04 Xenial and Ubuntu 14.04
 | 
			
		||||
Trusty. We're happy to support work to enable Zulip to run on
 | 
			
		||||
additional platforms. The installation process is
 | 
			
		||||
[documented here](https://zulip.readthedocs.io/en/latest/prod.html).
 | 
			
		||||
 | 
			
		||||
## Ways to contribute
 | 
			
		||||
 | 
			
		||||
Zulip welcomes all forms of contributions!  This page documents the
 | 
			
		||||
Zulip development process.
 | 
			
		||||
 | 
			
		||||
* **Pull requests**. Before a pull request can be merged, you need to
 | 
			
		||||
sign the [Dropbox Contributor License Agreement][cla].  Also,
 | 
			
		||||
please skim our [commit message style guidelines][doc-commit-style].
 | 
			
		||||
We encourage early pull requests for work in progress. Prefix the title
 | 
			
		||||
of your pull request with `[WIP]` and reference it when asking for
 | 
			
		||||
community feedback. When you are ready for final review, remove
 | 
			
		||||
the `[WIP]`.
 | 
			
		||||
 | 
			
		||||
* **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
 | 
			
		||||
docs][doc-test]. You can also try out [our new desktop
 | 
			
		||||
client][electron], which is in alpha; we'd appreciate testing and
 | 
			
		||||
[feedback](https://github.com/zulip/zulip-electron/issues/new).
 | 
			
		||||
 | 
			
		||||
* **Developer Documentation**.  Zulip has a growing collection of
 | 
			
		||||
developer documentation on [Read The Docs][doc].  Recommended reading
 | 
			
		||||
for new contributors includes the [directory structure][doc-dirstruct]
 | 
			
		||||
and [new feature tutorial][doc-newfeat]. You can also improve
 | 
			
		||||
[Zulip.org][z-org].
 | 
			
		||||
 | 
			
		||||
* **Mailing lists and bug tracker**. Zulip has a [development
 | 
			
		||||
discussion mailing list](#community) and uses [GitHub issues
 | 
			
		||||
][gh-issues].  There are also lists for the [Android][email-android]
 | 
			
		||||
and [iOS][email-ios] apps.  Feel free to send any questions or
 | 
			
		||||
suggestions of areas where you'd love to see more documentation to the
 | 
			
		||||
relevant list! Check out our [bug report guidelines][bug-report]
 | 
			
		||||
before submitting. Please report any security issues you discover to
 | 
			
		||||
zulip-security@googlegroups.com.
 | 
			
		||||
 | 
			
		||||
* **App codebases**. This repository is for the Zulip server and web
 | 
			
		||||
app (including most integrations).  The
 | 
			
		||||
[beta React Native mobile app][mobile], [Java Android app][Android]
 | 
			
		||||
(see [our mobile strategy][mobile-strategy]),
 | 
			
		||||
[new Electron desktop app][electron], and
 | 
			
		||||
[legacy Qt-based desktop app][desktop] are all separate repositories.
 | 
			
		||||
 | 
			
		||||
* **Glue code**. We maintain a [Hubot adapter][hubot-adapter] and several
 | 
			
		||||
integrations ([Phabricator][phab], [Jenkins][], [Puppet][], [Redmine][],
 | 
			
		||||
and [Trello][]), plus [node.js API bindings][node], an [isomorphic
 | 
			
		||||
 JavaScript library][zulip-js], and a [full-text search PostgreSQL
 | 
			
		||||
 extension][tsearch], as separate repos.
 | 
			
		||||
 | 
			
		||||
* **Translations**.  Zulip is in the process of being translated into
 | 
			
		||||
10+ languages, and we love contributions to our translations.  See our
 | 
			
		||||
[translating documentation][transifex] if you're interested in
 | 
			
		||||
contributing!
 | 
			
		||||
 | 
			
		||||
* **Code Reviews**. Zulip is all about community and helping each
 | 
			
		||||
other out.  Check out [#code review][code-review] on
 | 
			
		||||
[chat.zulip.org][czo-doc] to help review PRs and give comments on
 | 
			
		||||
other people's work. Everyone is welcome to participate, even those
 | 
			
		||||
new to Zulip! Even just checking out the code, manually testing it,
 | 
			
		||||
and posting on whether or not it worked is valuable.
 | 
			
		||||
 | 
			
		||||
[cla]: https://opensource.dropbox.com/cla/
 | 
			
		||||
[code-of-conduct]: https://zulip.readthedocs.io/en/latest/code-of-conduct.html
 | 
			
		||||
[dev-install]: https://zulip.readthedocs.io/en/latest/dev-overview.html
 | 
			
		||||
[doc]: https://zulip.readthedocs.io/
 | 
			
		||||
[doc-commit-style]: http://zulip.readthedocs.io/en/latest/version-control.html#commit-messages
 | 
			
		||||
[doc-dirstruct]: http://zulip.readthedocs.io/en/latest/directory-structure.html
 | 
			
		||||
[doc-newfeat]: http://zulip.readthedocs.io/en/latest/new-feature-tutorial.html
 | 
			
		||||
[doc-test]: http://zulip.readthedocs.io/en/latest/testing.html
 | 
			
		||||
[electron]: https://github.com/zulip/zulip-electron
 | 
			
		||||
[gh-issues]: https://github.com/zulip/zulip/issues
 | 
			
		||||
[desktop]: https://github.com/zulip/zulip-desktop
 | 
			
		||||
[android]: https://github.com/zulip/zulip-android
 | 
			
		||||
[mobile]: https://github.com/zulip/zulip-mobile
 | 
			
		||||
[mobile-strategy]: https://github.com/zulip/zulip-android/blob/master/android-strategy.md
 | 
			
		||||
[email-android]: https://groups.google.com/forum/#!forum/zulip-android
 | 
			
		||||
[email-ios]: https://groups.google.com/forum/#!forum/zulip-ios
 | 
			
		||||
[hubot-adapter]: https://github.com/zulip/hubot-zulip
 | 
			
		||||
[jenkins]: https://github.com/zulip/zulip-jenkins-plugin
 | 
			
		||||
[node]: https://github.com/zulip/zulip-node
 | 
			
		||||
[zulip-js]: https://github.com/zulip/zulip-js
 | 
			
		||||
[phab]: https://github.com/zulip/phabricator-to-zulip
 | 
			
		||||
[puppet]: https://github.com/matthewbarr/puppet-zulip
 | 
			
		||||
[redmine]: https://github.com/zulip/zulip-redmine-plugin
 | 
			
		||||
[trello]: https://github.com/zulip/trello-to-zulip
 | 
			
		||||
[tsearch]: https://github.com/zulip/tsearch_extras
 | 
			
		||||
[transifex]: https://zulip.readthedocs.io/en/latest/translating.html#testing-translations
 | 
			
		||||
[z-org]: https://github.com/zulip/zulip.github.io
 | 
			
		||||
[code-review]: https://chat.zulip.org/#narrow/stream/code.20review
 | 
			
		||||
[bug-report]: http://zulip.readthedocs.io/en/latest/bug-reports.html
 | 
			
		||||
 | 
			
		||||
## Google Summer of Code
 | 
			
		||||
 | 
			
		||||
We participated in
 | 
			
		||||
[GSoC](https://developers.google.com/open-source/gsoc/) in 2016 (with
 | 
			
		||||
[great results](https://blog.zulip.org/2016/10/13/static-types-in-python-oh-mypy/))
 | 
			
		||||
and [are participating](https://github.com/zulip/zulip.github.io/blob/master/gsoc-ideas.md)
 | 
			
		||||
in 2017 as well.
 | 
			
		||||
 | 
			
		||||
## How to get involved with contributing to Zulip
 | 
			
		||||
 | 
			
		||||
First, subscribe to the Zulip [development discussion mailing
 | 
			
		||||
list](#community).
 | 
			
		||||
 | 
			
		||||
The Zulip project uses a system of labels in our [issue
 | 
			
		||||
tracker][gh-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:
 | 
			
		||||
 | 
			
		||||
* [Integrations](https://github.com/zulip/zulip/labels/area%3A%20integrations).
 | 
			
		||||
  Integrate Zulip with another piece of software and contribute it
 | 
			
		||||
  back to the community!  Writing an integration can be a great first
 | 
			
		||||
  contribution.  There's detailed documentation on how to write
 | 
			
		||||
  integrations in [the Zulip integration writing
 | 
			
		||||
  guide](https://zulip.readthedocs.io/en/latest/integration-guide.html).
 | 
			
		||||
 | 
			
		||||
* [Good first issue](https://github.com/zulip/zulip/labels/good%20first%20issue):
 | 
			
		||||
  Smaller projects that might be a great first contribution.
 | 
			
		||||
 | 
			
		||||
* [Documentation](https://github.com/zulip/zulip/labels/area%3A%20documentation):
 | 
			
		||||
  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.
 | 
			
		||||
 | 
			
		||||
* [2016 roadmap milestone](http://zulip.readthedocs.io/en/latest/roadmap.html):
 | 
			
		||||
  The projects that are
 | 
			
		||||
  [priorities for the Zulip project](https://zulip.readthedocs.io/en/latest/roadmap.html).
 | 
			
		||||
  These are great projects if you're looking to make an impact.
 | 
			
		||||
 | 
			
		||||
Another way to find issues in Zulip is to take advantage of our
 | 
			
		||||
`area:<foo>` convention in separating out issues.  We partition all of
 | 
			
		||||
our issues into areas like admin, compose, emoji, hotkeys, i18n,
 | 
			
		||||
onboarding, search, etc.  Look through our
 | 
			
		||||
[list of labels](https://github.com/zulip/zulip/labels), and click on
 | 
			
		||||
some of the `area:` labels to see all the tickets related to your
 | 
			
		||||
areas of interest.
 | 
			
		||||
 | 
			
		||||
If you're excited about helping with an open issue, make sure to claim
 | 
			
		||||
the issue by commenting the following in the comment section:
 | 
			
		||||
"**@zulipbot** claim". **@zulipbot** will assign you to the issue and
 | 
			
		||||
label the issue as **in progress**. For more details, check out
 | 
			
		||||
[**@zulipbot**](https://github.com/zulip/zulipbot).
 | 
			
		||||
 | 
			
		||||
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. It's great to ask
 | 
			
		||||
questions in comments on GitHub issues and pull requests, or
 | 
			
		||||
[on chat.zulip.org][czo-doc].  We'll direct longer discussions to
 | 
			
		||||
Zulip chat, but please post a summary of what you learned from the
 | 
			
		||||
chat, or link to the conversation, in a comment on the GitHub issue.
 | 
			
		||||
 | 
			
		||||
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 the
 | 
			
		||||
[#design](https://chat.zulip.org/#narrow/stream/design) stream on
 | 
			
		||||
[chat.zulip.org][czo-doc] 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](http://zulip.readthedocs.io/en/latest/new-feature-tutorial.html)
 | 
			
		||||
and [coding style guidelines](http://zulip.readthedocs.io/en/latest/code-style.html)
 | 
			
		||||
on ReadTheDocs.
 | 
			
		||||
 | 
			
		||||
Feedback on how to make this development process more efficient, fun,
 | 
			
		||||
and friendly to new contributors is very welcome!  Just send an email
 | 
			
		||||
to the [zulip-devel](#community) list with your thoughts.
 | 
			
		||||
 | 
			
		||||
When you feel like you have completed your work on an issue, post your
 | 
			
		||||
PR to the
 | 
			
		||||
[#code review](https://chat.zulip.org/#narrow/stream/code.20review)
 | 
			
		||||
stream on [chat.zulip.org][czo-doc].  This is our lightweight process
 | 
			
		||||
that gives other developers the opportunity to give you comments and
 | 
			
		||||
suggestions on your work.
 | 
			
		||||
 | 
			
		||||
## License
 | 
			
		||||
 | 
			
		||||
Copyright 2011-2017 Dropbox, Inc., Kandra Labs, Inc., and contributors
 | 
			
		||||
 | 
			
		||||
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 ``docs/THIRDPARTY`` file included with this distribution.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
[czo-doc]: https://zulip.readthedocs.io/en/latest/chat-zulip-org.html
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										103
									
								
								Vagrantfile
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										103
									
								
								Vagrantfile
									
									
									
									
										vendored
									
									
								
							@@ -10,7 +10,7 @@ end
 | 
			
		||||
if Vagrant::VERSION == "1.8.7" then
 | 
			
		||||
    path = `which curl`
 | 
			
		||||
    if path.include?('/opt/vagrant/embedded/bin/curl') then
 | 
			
		||||
        puts "In Vagrant 1.8.7, curl is broken. Please use Vagrant 2.0.2 "\
 | 
			
		||||
        puts "In Vagrant 1.8.7, curl is broken. Please use Vagrant 1.8.6 "\
 | 
			
		||||
             "or run 'sudo rm -f /opt/vagrant/embedded/bin/curl' to fix the "\
 | 
			
		||||
             "issue before provisioning. See "\
 | 
			
		||||
             "https://github.com/mitchellh/vagrant/issues/7997 "\
 | 
			
		||||
@@ -19,53 +19,6 @@ if Vagrant::VERSION == "1.8.7" then
 | 
			
		||||
    end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
# Workaround: the lxc-config in vagrant-lxc is incompatible with changes in
 | 
			
		||||
# LXC 2.1.0, found in Ubuntu 17.10 artful.  LXC 2.1.1 (in 18.04 LTS bionic)
 | 
			
		||||
# ignores the old config key, so this will only be needed for artful.
 | 
			
		||||
#
 | 
			
		||||
# vagrant-lxc upstream has an attempted fix:
 | 
			
		||||
#   https://github.com/fgrehm/vagrant-lxc/issues/445
 | 
			
		||||
# but it didn't work in our testing.  This is a temporary issue, so we just
 | 
			
		||||
# hack in a fix: we patch the skeleton `lxc-config` file right in the
 | 
			
		||||
# distribution of the vagrant-lxc "box" we use.  If the user doesn't yet
 | 
			
		||||
# have the box (e.g. on first setup), Vagrant would download it but too
 | 
			
		||||
# late for us to patch it like this; so we prompt them to explicitly add it
 | 
			
		||||
# first and then rerun.
 | 
			
		||||
if ['up', 'provision'].include? ARGV[0]
 | 
			
		||||
  if command? "lxc-ls"
 | 
			
		||||
    LXC_VERSION = `lxc-ls --version`.strip unless defined? LXC_VERSION
 | 
			
		||||
    if LXC_VERSION == "2.1.0"
 | 
			
		||||
      lxc_config_file = ENV['HOME'] + "/.vagrant.d/boxes/fgrehm-VAGRANTSLASH-trusty64-lxc/1.2.0/lxc/lxc-config"
 | 
			
		||||
      if File.file?(lxc_config_file)
 | 
			
		||||
        lines = File.readlines(lxc_config_file)
 | 
			
		||||
        deprecated_line = "lxc.pivotdir = lxc_putold\n"
 | 
			
		||||
        if lines[1] == deprecated_line
 | 
			
		||||
          lines[1] = "# #{deprecated_line}"
 | 
			
		||||
          File.open(lxc_config_file, 'w') do |f|
 | 
			
		||||
            f.puts(lines)
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
      else
 | 
			
		||||
        puts 'You are running LXC 2.1.0, and fgrehm/trusty64-lxc box is incompatible '\
 | 
			
		||||
            "with it by default. First add the box by doing:\n"\
 | 
			
		||||
            "  vagrant box add  https://vagrantcloud.com/fgrehm/trusty64-lxc\n"\
 | 
			
		||||
            'Once this command succeeds, do "vagrant up" again.'
 | 
			
		||||
        exit
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
# Workaround: Vagrant removed the atlas.hashicorp.com to
 | 
			
		||||
# vagrantcloud.com redirect in February 2018. The value of
 | 
			
		||||
# DEFAULT_SERVER_URL in Vagrant versions less than 1.9.3 is
 | 
			
		||||
# atlas.hashicorp.com, which means that removal broke the fetching and
 | 
			
		||||
# updating of boxes (since the old URL doesn't work).  See
 | 
			
		||||
# https://github.com/hashicorp/vagrant/issues/9442
 | 
			
		||||
if Vagrant::DEFAULT_SERVER_URL == "atlas.hashicorp.com"
 | 
			
		||||
  Vagrant::DEFAULT_SERVER_URL.replace('https://vagrantcloud.com')
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
 | 
			
		||||
 | 
			
		||||
  # For LXC. VirtualBox hosts use a different box, described below.
 | 
			
		||||
@@ -77,13 +30,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
 | 
			
		||||
  host_ip_addr = "127.0.0.1"
 | 
			
		||||
 | 
			
		||||
  config.vm.synced_folder ".", "/vagrant", disabled: true
 | 
			
		||||
  if (/darwin/ =~ RUBY_PLATFORM) != nil
 | 
			
		||||
    config.vm.synced_folder ".", "/srv/zulip", type: "nfs",
 | 
			
		||||
        linux__nfs_options: ['rw']
 | 
			
		||||
    config.vm.network "private_network", type: "dhcp"
 | 
			
		||||
  else
 | 
			
		||||
    config.vm.synced_folder ".", "/srv/zulip"
 | 
			
		||||
  end
 | 
			
		||||
  config.vm.synced_folder ".", "/srv/zulip"
 | 
			
		||||
 | 
			
		||||
  vagrant_config_file = ENV['HOME'] + "/.zulip-vagrant-config"
 | 
			
		||||
  if File.file?(vagrant_config_file)
 | 
			
		||||
@@ -153,29 +100,10 @@ $provision_script = <<SCRIPT
 | 
			
		||||
set -x
 | 
			
		||||
set -e
 | 
			
		||||
set -o pipefail
 | 
			
		||||
 | 
			
		||||
# Code should go here, rather than tools/provision, only if it is
 | 
			
		||||
# something that we don't want to happen when running provision in a
 | 
			
		||||
# development environment not using Vagrant.
 | 
			
		||||
 | 
			
		||||
# Set the MOTD on the system to have Zulip instructions
 | 
			
		||||
sudo rm -f /etc/update-motd.d/*
 | 
			
		||||
sudo bash -c 'cat << EndOfMessage > /etc/motd
 | 
			
		||||
Welcome to the Zulip development environment!  Popular commands:
 | 
			
		||||
* tools/provision - Update the development environment
 | 
			
		||||
* tools/run-dev.py - Run the development server
 | 
			
		||||
* tools/lint - Run the linter (quick and catches many problmes)
 | 
			
		||||
* tools/test-* - Run tests (use --help to learn about options)
 | 
			
		||||
 | 
			
		||||
Read https://zulip.readthedocs.io/en/latest/testing.html to learn
 | 
			
		||||
how to run individual test suites so that you can get a fast debug cycle.
 | 
			
		||||
 | 
			
		||||
EndOfMessage'
 | 
			
		||||
 | 
			
		||||
# If the host is running SELinux remount the /sys/fs/selinux directory as read only,
 | 
			
		||||
# needed for apt-get to work.
 | 
			
		||||
if [ -d "/sys/fs/selinux" ]; then
 | 
			
		||||
    sudo mount -o remount,ro /sys/fs/selinux
 | 
			
		||||
  sudo mount -o remount,ro /sys/fs/selinux
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
# Set default locale, this prevents errors if the user has another locale set.
 | 
			
		||||
@@ -183,35 +111,14 @@ if ! grep -q 'LC_ALL=en_US.UTF-8' /etc/default/locale; then
 | 
			
		||||
    echo "LC_ALL=en_US.UTF-8" | sudo tee -a /etc/default/locale
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
# Set an environment variable, so that we won't print the virtualenv
 | 
			
		||||
# shell warning (it'll be wrong, since the shell is dying anyway)
 | 
			
		||||
export SKIP_VENV_SHELL_WARNING=1
 | 
			
		||||
 | 
			
		||||
# End `set -x`, so that the end of provision doesn't look like an error
 | 
			
		||||
# message after a successful run.
 | 
			
		||||
set +x
 | 
			
		||||
 | 
			
		||||
# Check if the zulip directory is writable
 | 
			
		||||
if [ ! -w /srv/zulip ]; then
 | 
			
		||||
    echo "The vagrant user is unable to write to the zulip directory."
 | 
			
		||||
    echo "To fix this, run the following commands on the host machine:"
 | 
			
		||||
    # sudo is required since our uid is not 1000
 | 
			
		||||
    echo '    vagrant halt -f'
 | 
			
		||||
    echo '    rm -rf /PATH/TO/ZULIP/CLONE/.vagrant'
 | 
			
		||||
    echo '    sudo chown -R 1000:$(whoami) /PATH/TO/ZULIP/CLONE'
 | 
			
		||||
    echo "Replace /PATH/TO/ZULIP/CLONE with the path to where zulip code is cloned."
 | 
			
		||||
    echo "You can resume setting up your vagrant environment by running:"
 | 
			
		||||
    echo "    vagrant up"
 | 
			
		||||
    exit 1
 | 
			
		||||
fi
 | 
			
		||||
# Provision the development environment
 | 
			
		||||
ln -nsf /srv/zulip ~/zulip
 | 
			
		||||
/srv/zulip/tools/provision
 | 
			
		||||
 | 
			
		||||
# Run any custom provision hooks the user has configured
 | 
			
		||||
if [ -f /srv/zulip/tools/custom_provision ]; then
 | 
			
		||||
    chmod +x /srv/zulip/tools/custom_provision
 | 
			
		||||
    /srv/zulip/tools/custom_provision
 | 
			
		||||
  chmod +x /srv/zulip/tools/custom_provision
 | 
			
		||||
  /srv/zulip/tools/custom_provision
 | 
			
		||||
fi
 | 
			
		||||
SCRIPT
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,40 +1,38 @@
 | 
			
		||||
import time
 | 
			
		||||
from collections import OrderedDict, defaultdict
 | 
			
		||||
from datetime import datetime, timedelta
 | 
			
		||||
import logging
 | 
			
		||||
from typing import Any, Callable, Dict, List, \
 | 
			
		||||
    Optional, Text, Tuple, Type, Union
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db import connection, models
 | 
			
		||||
from django.db.models import F
 | 
			
		||||
 | 
			
		||||
from analytics.models import Anomaly, BaseCount, \
 | 
			
		||||
    FillState, InstallationCount, RealmCount, StreamCount, \
 | 
			
		||||
    UserCount, installation_epoch, last_successful_fill
 | 
			
		||||
from zerver.lib.logging_util import log_to_file
 | 
			
		||||
from zerver.lib.timestamp import ceiling_to_day, \
 | 
			
		||||
    ceiling_to_hour, floor_to_hour, verify_UTC
 | 
			
		||||
from zerver.models import Message, Realm, RealmAuditLog, \
 | 
			
		||||
    Stream, UserActivityInterval, UserProfile, models
 | 
			
		||||
from analytics.models import InstallationCount, RealmCount, \
 | 
			
		||||
    UserCount, StreamCount, BaseCount, FillState, Anomaly, installation_epoch, \
 | 
			
		||||
    last_successful_fill
 | 
			
		||||
from zerver.models import Realm, UserProfile, Message, Stream, \
 | 
			
		||||
    UserActivityInterval, RealmAuditLog, models
 | 
			
		||||
from zerver.lib.timestamp import floor_to_day, floor_to_hour, ceiling_to_day, \
 | 
			
		||||
    ceiling_to_hour, verify_UTC
 | 
			
		||||
 | 
			
		||||
from typing import Any, Callable, Dict, List, Optional, Text, Tuple, Type, Union
 | 
			
		||||
 | 
			
		||||
from collections import defaultdict, OrderedDict
 | 
			
		||||
from datetime import timedelta, datetime
 | 
			
		||||
from zerver.lib.logging_util import create_logger
 | 
			
		||||
import time
 | 
			
		||||
 | 
			
		||||
## Logging setup ##
 | 
			
		||||
 | 
			
		||||
logger = logging.getLogger('zulip.management')
 | 
			
		||||
log_to_file(logger, settings.ANALYTICS_LOG_PATH)
 | 
			
		||||
logger = create_logger('zulip.management', settings.ANALYTICS_LOG_PATH, 'INFO')
 | 
			
		||||
 | 
			
		||||
# You can't subtract timedelta.max from a datetime, so use this instead
 | 
			
		||||
TIMEDELTA_MAX = timedelta(days=365*1000)
 | 
			
		||||
 | 
			
		||||
## Class definitions ##
 | 
			
		||||
 | 
			
		||||
class CountStat:
 | 
			
		||||
class CountStat(object):
 | 
			
		||||
    HOUR = 'hour'
 | 
			
		||||
    DAY = 'day'
 | 
			
		||||
    FREQUENCIES = frozenset([HOUR, DAY])
 | 
			
		||||
 | 
			
		||||
    def __init__(self, property: str, data_collector: 'DataCollector', frequency: str,
 | 
			
		||||
                 interval: Optional[timedelta]=None) -> None:
 | 
			
		||||
    def __init__(self, property, data_collector, frequency, interval=None):
 | 
			
		||||
        # type: (str, DataCollector, str, Optional[timedelta]) -> None
 | 
			
		||||
        self.property = property
 | 
			
		||||
        self.data_collector = data_collector
 | 
			
		||||
        # might have to do something different for bitfields
 | 
			
		||||
@@ -48,28 +46,31 @@ class CountStat:
 | 
			
		||||
        else:  # frequency == CountStat.DAY
 | 
			
		||||
            self.interval = timedelta(days=1)
 | 
			
		||||
 | 
			
		||||
    def __str__(self) -> Text:
 | 
			
		||||
        return "<CountStat: %s>" % (self.property,)
 | 
			
		||||
    def __unicode__(self):
 | 
			
		||||
        # type: () -> Text
 | 
			
		||||
        return u"<CountStat: %s>" % (self.property,)
 | 
			
		||||
 | 
			
		||||
class LoggingCountStat(CountStat):
 | 
			
		||||
    def __init__(self, property: str, output_table: Type[BaseCount], frequency: str) -> None:
 | 
			
		||||
    def __init__(self, property, output_table, frequency):
 | 
			
		||||
        # type: (str, Type[BaseCount], str) -> None
 | 
			
		||||
        CountStat.__init__(self, property, DataCollector(output_table, None), frequency)
 | 
			
		||||
 | 
			
		||||
class DependentCountStat(CountStat):
 | 
			
		||||
    def __init__(self, property: str, data_collector: 'DataCollector', frequency: str,
 | 
			
		||||
                 interval: Optional[timedelta]=None, dependencies: List[str]=[]) -> None:
 | 
			
		||||
    def __init__(self, property, data_collector, frequency, interval=None, dependencies=[]):
 | 
			
		||||
        # type: (str, DataCollector, str, Optional[timedelta], List[str]) -> None
 | 
			
		||||
        CountStat.__init__(self, property, data_collector, frequency, interval=interval)
 | 
			
		||||
        self.dependencies = dependencies
 | 
			
		||||
 | 
			
		||||
class DataCollector:
 | 
			
		||||
    def __init__(self, output_table: Type[BaseCount],
 | 
			
		||||
                 pull_function: Optional[Callable[[str, datetime, datetime], int]]) -> None:
 | 
			
		||||
class DataCollector(object):
 | 
			
		||||
    def __init__(self, output_table, pull_function):
 | 
			
		||||
        # type: (Type[BaseCount], Optional[Callable[[str, datetime, datetime], int]]) -> None
 | 
			
		||||
        self.output_table = output_table
 | 
			
		||||
        self.pull_function = pull_function
 | 
			
		||||
 | 
			
		||||
## CountStat-level operations ##
 | 
			
		||||
 | 
			
		||||
def process_count_stat(stat: CountStat, fill_to_time: datetime) -> None:
 | 
			
		||||
def process_count_stat(stat, fill_to_time):
 | 
			
		||||
    # type: (CountStat, datetime) -> None
 | 
			
		||||
    if stat.frequency == CountStat.HOUR:
 | 
			
		||||
        time_increment = timedelta(hours=1)
 | 
			
		||||
    elif stat.frequency == CountStat.DAY:
 | 
			
		||||
@@ -119,14 +120,16 @@ def process_count_stat(stat: CountStat, fill_to_time: datetime) -> None:
 | 
			
		||||
        currently_filled = currently_filled + time_increment
 | 
			
		||||
        logger.info("DONE %s (%dms)" % (stat.property, (end-start)*1000))
 | 
			
		||||
 | 
			
		||||
def do_update_fill_state(fill_state: FillState, end_time: datetime, state: int) -> None:
 | 
			
		||||
def do_update_fill_state(fill_state, end_time, state):
 | 
			
		||||
    # type: (FillState, datetime, int) -> None
 | 
			
		||||
    fill_state.end_time = end_time
 | 
			
		||||
    fill_state.state = state
 | 
			
		||||
    fill_state.save()
 | 
			
		||||
 | 
			
		||||
# We assume end_time is valid (e.g. is on a day or hour boundary as appropriate)
 | 
			
		||||
# and is timezone aware. It is the caller's responsibility to enforce this!
 | 
			
		||||
def do_fill_count_stat_at_hour(stat: CountStat, end_time: datetime) -> None:
 | 
			
		||||
def do_fill_count_stat_at_hour(stat, end_time):
 | 
			
		||||
    # type: (CountStat, datetime) -> None
 | 
			
		||||
    start_time = end_time - stat.interval
 | 
			
		||||
    if not isinstance(stat, LoggingCountStat):
 | 
			
		||||
        timer = time.time()
 | 
			
		||||
@@ -136,7 +139,8 @@ def do_fill_count_stat_at_hour(stat: CountStat, end_time: datetime) -> None:
 | 
			
		||||
                    (stat.property, (time.time()-timer)*1000, rows_added))
 | 
			
		||||
    do_aggregate_to_summary_table(stat, end_time)
 | 
			
		||||
 | 
			
		||||
def do_delete_counts_at_hour(stat: CountStat, end_time: datetime) -> None:
 | 
			
		||||
def do_delete_counts_at_hour(stat, end_time):
 | 
			
		||||
    # type: (CountStat, datetime) -> None
 | 
			
		||||
    if isinstance(stat, LoggingCountStat):
 | 
			
		||||
        InstallationCount.objects.filter(property=stat.property, end_time=end_time).delete()
 | 
			
		||||
        if stat.data_collector.output_table in [UserCount, StreamCount]:
 | 
			
		||||
@@ -147,7 +151,8 @@ def do_delete_counts_at_hour(stat: CountStat, end_time: datetime) -> None:
 | 
			
		||||
        RealmCount.objects.filter(property=stat.property, end_time=end_time).delete()
 | 
			
		||||
        InstallationCount.objects.filter(property=stat.property, end_time=end_time).delete()
 | 
			
		||||
 | 
			
		||||
def do_aggregate_to_summary_table(stat: CountStat, end_time: datetime) -> None:
 | 
			
		||||
def do_aggregate_to_summary_table(stat, end_time):
 | 
			
		||||
    # type: (CountStat, datetime) -> None
 | 
			
		||||
    cursor = connection.cursor()
 | 
			
		||||
 | 
			
		||||
    # Aggregate into RealmCount
 | 
			
		||||
@@ -172,8 +177,7 @@ def do_aggregate_to_summary_table(stat: CountStat, end_time: datetime) -> None:
 | 
			
		||||
        start = time.time()
 | 
			
		||||
        cursor.execute(realmcount_query, {'end_time': end_time})
 | 
			
		||||
        end = time.time()
 | 
			
		||||
        logger.info("%s RealmCount aggregation (%dms/%sr)" % (
 | 
			
		||||
            stat.property, (end - start) * 1000, cursor.rowcount))
 | 
			
		||||
        logger.info("%s RealmCount aggregation (%dms/%sr)" % (stat.property, (end-start)*1000, cursor.rowcount))
 | 
			
		||||
 | 
			
		||||
    # Aggregate into InstallationCount
 | 
			
		||||
    installationcount_query = """
 | 
			
		||||
@@ -190,16 +194,14 @@ def do_aggregate_to_summary_table(stat: CountStat, end_time: datetime) -> None:
 | 
			
		||||
    start = time.time()
 | 
			
		||||
    cursor.execute(installationcount_query, {'end_time': end_time})
 | 
			
		||||
    end = time.time()
 | 
			
		||||
    logger.info("%s InstallationCount aggregation (%dms/%sr)" % (
 | 
			
		||||
        stat.property, (end - start) * 1000, cursor.rowcount))
 | 
			
		||||
    logger.info("%s InstallationCount aggregation (%dms/%sr)" % (stat.property, (end-start)*1000, cursor.rowcount))
 | 
			
		||||
    cursor.close()
 | 
			
		||||
 | 
			
		||||
## Utility functions called from outside counts.py ##
 | 
			
		||||
 | 
			
		||||
# called from zerver/lib/actions.py; should not throw any errors
 | 
			
		||||
def do_increment_logging_stat(zerver_object: Union[Realm, UserProfile, Stream], stat: CountStat,
 | 
			
		||||
                              subgroup: Optional[Union[str, int, bool]], event_time: datetime,
 | 
			
		||||
                              increment: int=1) -> None:
 | 
			
		||||
def do_increment_logging_stat(zerver_object, stat, subgroup, event_time, increment=1):
 | 
			
		||||
    # type: (Union[Realm, UserProfile, Stream], CountStat, Optional[Union[str, int, bool]], datetime, int) -> None
 | 
			
		||||
    table = stat.data_collector.output_table
 | 
			
		||||
    if table == RealmCount:
 | 
			
		||||
        id_args = {'realm': zerver_object}
 | 
			
		||||
@@ -220,7 +222,8 @@ def do_increment_logging_stat(zerver_object: Union[Realm, UserProfile, Stream],
 | 
			
		||||
        row.value = F('value') + increment
 | 
			
		||||
        row.save(update_fields=['value'])
 | 
			
		||||
 | 
			
		||||
def do_drop_all_analytics_tables() -> None:
 | 
			
		||||
def do_drop_all_analytics_tables():
 | 
			
		||||
    # type: () -> None
 | 
			
		||||
    UserCount.objects.all().delete()
 | 
			
		||||
    StreamCount.objects.all().delete()
 | 
			
		||||
    RealmCount.objects.all().delete()
 | 
			
		||||
@@ -228,7 +231,8 @@ def do_drop_all_analytics_tables() -> None:
 | 
			
		||||
    FillState.objects.all().delete()
 | 
			
		||||
    Anomaly.objects.all().delete()
 | 
			
		||||
 | 
			
		||||
def do_drop_single_stat(property: str) -> None:
 | 
			
		||||
def do_drop_single_stat(property):
 | 
			
		||||
    # type: (str) -> None
 | 
			
		||||
    UserCount.objects.filter(property=property).delete()
 | 
			
		||||
    StreamCount.objects.filter(property=property).delete()
 | 
			
		||||
    RealmCount.objects.filter(property=property).delete()
 | 
			
		||||
@@ -237,8 +241,8 @@ def do_drop_single_stat(property: str) -> None:
 | 
			
		||||
 | 
			
		||||
## DataCollector-level operations ##
 | 
			
		||||
 | 
			
		||||
def do_pull_by_sql_query(property: str, start_time: datetime, end_time: datetime, query: str,
 | 
			
		||||
                         group_by: Optional[Tuple[models.Model, str]]) -> int:
 | 
			
		||||
def do_pull_by_sql_query(property, start_time, end_time, query, group_by):
 | 
			
		||||
    # type: (str, datetime, datetime, str, Optional[Tuple[models.Model, str]]) -> int
 | 
			
		||||
    if group_by is None:
 | 
			
		||||
        subgroup = 'NULL'
 | 
			
		||||
        group_by_clause  = ''
 | 
			
		||||
@@ -258,13 +262,15 @@ def do_pull_by_sql_query(property: str, start_time: datetime, end_time: datetime
 | 
			
		||||
    cursor.close()
 | 
			
		||||
    return rowcount
 | 
			
		||||
 | 
			
		||||
def sql_data_collector(output_table: Type[BaseCount], query: str,
 | 
			
		||||
                       group_by: Optional[Tuple[models.Model, str]]) -> DataCollector:
 | 
			
		||||
    def pull_function(property: str, start_time: datetime, end_time: datetime) -> int:
 | 
			
		||||
def sql_data_collector(output_table, query, group_by):
 | 
			
		||||
    # type: (Type[BaseCount], str, Optional[Tuple[models.Model, str]]) -> DataCollector
 | 
			
		||||
    def pull_function(property, start_time, end_time):
 | 
			
		||||
        # type: (str, datetime, datetime) -> int
 | 
			
		||||
        return do_pull_by_sql_query(property, start_time, end_time, query, group_by)
 | 
			
		||||
    return DataCollector(output_table, pull_function)
 | 
			
		||||
 | 
			
		||||
def do_pull_minutes_active(property: str, start_time: datetime, end_time: datetime) -> int:
 | 
			
		||||
def do_pull_minutes_active(property, start_time, end_time):
 | 
			
		||||
    # type: (str, datetime, datetime) -> int
 | 
			
		||||
    user_activity_intervals = UserActivityInterval.objects.filter(
 | 
			
		||||
        end__gt=start_time, start__lt=end_time
 | 
			
		||||
    ).select_related(
 | 
			
		||||
@@ -288,8 +294,7 @@ count_message_by_user_query = """
 | 
			
		||||
    INSERT INTO analytics_usercount
 | 
			
		||||
        (user_id, realm_id, value, property, subgroup, end_time)
 | 
			
		||||
    SELECT
 | 
			
		||||
        zerver_userprofile.id, zerver_userprofile.realm_id, count(*),
 | 
			
		||||
        '%(property)s', %(subgroup)s, %%(time_end)s
 | 
			
		||||
        zerver_userprofile.id, zerver_userprofile.realm_id, count(*), '%(property)s', %(subgroup)s, %%(time_end)s
 | 
			
		||||
    FROM zerver_userprofile
 | 
			
		||||
    JOIN zerver_message
 | 
			
		||||
    ON
 | 
			
		||||
@@ -331,9 +336,7 @@ count_message_type_by_user_query = """
 | 
			
		||||
        LEFT JOIN zerver_stream
 | 
			
		||||
        ON
 | 
			
		||||
            zerver_recipient.type_id = zerver_stream.id
 | 
			
		||||
        GROUP BY
 | 
			
		||||
            zerver_userprofile.realm_id, zerver_userprofile.id,
 | 
			
		||||
            zerver_recipient.type, zerver_stream.invite_only
 | 
			
		||||
        GROUP BY zerver_userprofile.realm_id, zerver_userprofile.id, zerver_recipient.type, zerver_stream.invite_only
 | 
			
		||||
    ) AS subquery
 | 
			
		||||
    GROUP BY realm_id, id, message_type
 | 
			
		||||
"""
 | 
			
		||||
@@ -518,11 +521,6 @@ count_stats_ = [
 | 
			
		||||
              CountStat.DAY, interval=timedelta(days=15)-UserActivityInterval.MIN_INTERVAL_LENGTH),
 | 
			
		||||
    CountStat('minutes_active::day', DataCollector(UserCount, do_pull_minutes_active), CountStat.DAY),
 | 
			
		||||
 | 
			
		||||
    # Rate limiting stats
 | 
			
		||||
 | 
			
		||||
    # Used to limit the number of invitation emails sent by a realm
 | 
			
		||||
    LoggingCountStat('invites_sent::day', RealmCount, CountStat.DAY),
 | 
			
		||||
 | 
			
		||||
    # Dependent stats
 | 
			
		||||
    # Must come after their dependencies.
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,19 @@
 | 
			
		||||
from zerver.models import Realm, UserProfile, Stream, Message
 | 
			
		||||
from analytics.models import InstallationCount, RealmCount, UserCount, StreamCount
 | 
			
		||||
from analytics.lib.counts import CountStat
 | 
			
		||||
from analytics.lib.time_utils import time_range
 | 
			
		||||
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
from math import sqrt
 | 
			
		||||
from random import gauss, random, seed
 | 
			
		||||
from typing import List
 | 
			
		||||
 | 
			
		||||
from analytics.lib.counts import CountStat
 | 
			
		||||
from six.moves import zip
 | 
			
		||||
 | 
			
		||||
def generate_time_series_data(days: int=100, business_hours_base: float=10,
 | 
			
		||||
                              non_business_hours_base: float=10, growth: float=1,
 | 
			
		||||
                              autocorrelation: float=0, spikiness: float=1,
 | 
			
		||||
                              holiday_rate: float=0, frequency: str=CountStat.DAY,
 | 
			
		||||
                              partial_sum: bool=False, random_seed: int=26) -> List[int]:
 | 
			
		||||
def generate_time_series_data(days=100, business_hours_base=10, non_business_hours_base=10,
 | 
			
		||||
                              growth=1, autocorrelation=0, spikiness=1, holiday_rate=0,
 | 
			
		||||
                              frequency=CountStat.DAY, partial_sum=False, random_seed=26):
 | 
			
		||||
    # type: (int, float, float, float, float, float, float, str, bool, int) -> List[int]
 | 
			
		||||
    """
 | 
			
		||||
    Generate semi-realistic looking time series data for testing analytics graphs.
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,16 @@
 | 
			
		||||
from zerver.lib.timestamp import floor_to_hour, floor_to_day, \
 | 
			
		||||
    timestamp_to_datetime, verify_UTC
 | 
			
		||||
from analytics.lib.counts import CountStat
 | 
			
		||||
 | 
			
		||||
from datetime import datetime, timedelta
 | 
			
		||||
from typing import List, Optional
 | 
			
		||||
 | 
			
		||||
from analytics.lib.counts import CountStat
 | 
			
		||||
from zerver.lib.timestamp import floor_to_day, floor_to_hour, verify_UTC
 | 
			
		||||
 | 
			
		||||
# If min_length is None, returns end_times from ceiling(start) to floor(end), inclusive.
 | 
			
		||||
# If min_length is greater than 0, pads the list to the left.
 | 
			
		||||
# So informally, time_range(Sep 20, Sep 22, day, None) returns [Sep 20, Sep 21, Sep 22],
 | 
			
		||||
# and time_range(Sep 20, Sep 22, day, 5) returns [Sep 18, Sep 19, Sep 20, Sep 21, Sep 22]
 | 
			
		||||
def time_range(start: datetime, end: datetime, frequency: str,
 | 
			
		||||
               min_length: Optional[int]) -> List[datetime]:
 | 
			
		||||
def time_range(start, end, frequency, min_length):
 | 
			
		||||
    # type: (datetime, datetime, str, Optional[int]) -> List[datetime]
 | 
			
		||||
    verify_UTC(start)
 | 
			
		||||
    verify_UTC(end)
 | 
			
		||||
    if frequency == CountStat.HOUR:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,14 @@
 | 
			
		||||
import datetime
 | 
			
		||||
import logging
 | 
			
		||||
import time
 | 
			
		||||
from typing import Any, Dict
 | 
			
		||||
 | 
			
		||||
from django.core.management.base import BaseCommand, CommandParser
 | 
			
		||||
 | 
			
		||||
from zerver.models import Recipient, Message
 | 
			
		||||
from zerver.lib.timestamp import timestamp_to_datetime
 | 
			
		||||
from zerver.models import Message, Recipient
 | 
			
		||||
import datetime
 | 
			
		||||
import time
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
def compute_stats(log_level: int) -> None:
 | 
			
		||||
def compute_stats(log_level):
 | 
			
		||||
    # type: (int) -> None
 | 
			
		||||
    logger = logging.getLogger()
 | 
			
		||||
    logger.setLevel(log_level)
 | 
			
		||||
 | 
			
		||||
@@ -71,10 +71,12 @@ def compute_stats(log_level: int) -> None:
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "Compute statistics on MIT Zephyr usage."
 | 
			
		||||
 | 
			
		||||
    def add_arguments(self, parser: CommandParser) -> None:
 | 
			
		||||
    def add_arguments(self, parser):
 | 
			
		||||
        # type: (CommandParser) -> None
 | 
			
		||||
        parser.add_argument('--verbose', default=False, action='store_true')
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args: Any, **options: Any) -> None:
 | 
			
		||||
    def handle(self, *args, **options):
 | 
			
		||||
        # type: (*Any, **Any) -> None
 | 
			
		||||
        level = logging.INFO
 | 
			
		||||
        if options["verbose"]:
 | 
			
		||||
            level = logging.DEBUG
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,14 @@
 | 
			
		||||
import datetime
 | 
			
		||||
from typing import Any, Dict
 | 
			
		||||
 | 
			
		||||
from zerver.lib.statistics import seconds_usage_between
 | 
			
		||||
 | 
			
		||||
from django.core.management.base import BaseCommand, CommandParser
 | 
			
		||||
from zerver.models import UserProfile
 | 
			
		||||
import datetime
 | 
			
		||||
from django.utils.timezone import utc
 | 
			
		||||
 | 
			
		||||
from zerver.lib.statistics import seconds_usage_between
 | 
			
		||||
from zerver.models import UserProfile
 | 
			
		||||
 | 
			
		||||
def analyze_activity(options: Dict[str, Any]) -> None:
 | 
			
		||||
def analyze_activity(options):
 | 
			
		||||
    # type: (Dict[str, Any]) -> None
 | 
			
		||||
    day_start = datetime.datetime.strptime(options["date"], "%Y-%m-%d").replace(tzinfo=utc)
 | 
			
		||||
    day_end = day_start + datetime.timedelta(days=options["duration"])
 | 
			
		||||
 | 
			
		||||
@@ -46,11 +47,13 @@ Usage: ./manage.py analyze_user_activity [--realm=zulip] [--date=2013-09-10] [--
 | 
			
		||||
By default, if no date is selected 2013-09-10 is used. If no realm is provided, information
 | 
			
		||||
is shown for all realms"""
 | 
			
		||||
 | 
			
		||||
    def add_arguments(self, parser: CommandParser) -> None:
 | 
			
		||||
    def add_arguments(self, parser):
 | 
			
		||||
        # type: (CommandParser) -> None
 | 
			
		||||
        parser.add_argument('--realm', action='store')
 | 
			
		||||
        parser.add_argument('--date', action='store', default="2013-09-06")
 | 
			
		||||
        parser.add_argument('--duration', action='store', default=1, type=int,
 | 
			
		||||
                            help="How many days to show usage information for")
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args: Any, **options: Any) -> None:
 | 
			
		||||
    def handle(self, *args, **options):
 | 
			
		||||
        # type: (*Any, **Any) -> None
 | 
			
		||||
        analyze_activity(options)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,85 +0,0 @@
 | 
			
		||||
from argparse import ArgumentParser
 | 
			
		||||
from datetime import timedelta
 | 
			
		||||
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
from django.utils.timezone import now as timezone_now
 | 
			
		||||
 | 
			
		||||
from analytics.models import InstallationCount, installation_epoch, \
 | 
			
		||||
    last_successful_fill
 | 
			
		||||
from analytics.lib.counts import COUNT_STATS, CountStat
 | 
			
		||||
from zerver.lib.timestamp import floor_to_hour, floor_to_day, verify_UTC, \
 | 
			
		||||
    TimezoneNotUTCException
 | 
			
		||||
from zerver.models import Realm
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
import subprocess
 | 
			
		||||
import sys
 | 
			
		||||
import time
 | 
			
		||||
from typing import Any, Dict
 | 
			
		||||
 | 
			
		||||
states = {
 | 
			
		||||
    0: "OK",
 | 
			
		||||
    1: "WARNING",
 | 
			
		||||
    2: "CRITICAL",
 | 
			
		||||
    3: "UNKNOWN"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = """Checks FillState table.
 | 
			
		||||
 | 
			
		||||
    Run as a cron job that runs every hour."""
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args: Any, **options: Any) -> None:
 | 
			
		||||
        fill_state = self.get_fill_state()
 | 
			
		||||
        status = fill_state['status']
 | 
			
		||||
        message = fill_state['message']
 | 
			
		||||
 | 
			
		||||
        state_file_path = "/var/lib/nagios_state/check-analytics-state"
 | 
			
		||||
        state_file_tmp = state_file_path + "-tmp"
 | 
			
		||||
 | 
			
		||||
        with open(state_file_tmp, "w") as f:
 | 
			
		||||
            f.write("%s|%s|%s|%s\n" % (
 | 
			
		||||
                int(time.time()), status, states[status], message))
 | 
			
		||||
        subprocess.check_call(["mv", state_file_tmp, state_file_path])
 | 
			
		||||
 | 
			
		||||
    def get_fill_state(self) -> Dict[str, Any]:
 | 
			
		||||
        if not Realm.objects.exists():
 | 
			
		||||
            return {'status': 0, 'message': 'No realms exist, so not checking FillState.'}
 | 
			
		||||
 | 
			
		||||
        warning_unfilled_properties = []
 | 
			
		||||
        critical_unfilled_properties = []
 | 
			
		||||
        for property, stat in COUNT_STATS.items():
 | 
			
		||||
            last_fill = last_successful_fill(property)
 | 
			
		||||
            if last_fill is None:
 | 
			
		||||
                last_fill = installation_epoch()
 | 
			
		||||
            try:
 | 
			
		||||
                verify_UTC(last_fill)
 | 
			
		||||
            except TimezoneNotUTCException:
 | 
			
		||||
                return {'status': 2, 'message': 'FillState not in UTC for %s' % (property,)}
 | 
			
		||||
 | 
			
		||||
            if stat.frequency == CountStat.DAY:
 | 
			
		||||
                floor_function = floor_to_day
 | 
			
		||||
                warning_threshold = timedelta(hours=26)
 | 
			
		||||
                critical_threshold = timedelta(hours=50)
 | 
			
		||||
            else:  # CountStat.HOUR
 | 
			
		||||
                floor_function = floor_to_hour
 | 
			
		||||
                warning_threshold = timedelta(minutes=90)
 | 
			
		||||
                critical_threshold = timedelta(minutes=150)
 | 
			
		||||
 | 
			
		||||
            if floor_function(last_fill) != last_fill:
 | 
			
		||||
                return {'status': 2, 'message': 'FillState not on %s boundary for %s' %
 | 
			
		||||
                        (stat.frequency, property)}
 | 
			
		||||
 | 
			
		||||
            time_to_last_fill = timezone_now() - last_fill
 | 
			
		||||
            if time_to_last_fill > critical_threshold:
 | 
			
		||||
                critical_unfilled_properties.append(property)
 | 
			
		||||
            elif time_to_last_fill > warning_threshold:
 | 
			
		||||
                warning_unfilled_properties.append(property)
 | 
			
		||||
 | 
			
		||||
        if len(critical_unfilled_properties) == 0 and len(warning_unfilled_properties) == 0:
 | 
			
		||||
            return {'status': 0, 'message': 'FillState looks fine.'}
 | 
			
		||||
        if len(critical_unfilled_properties) == 0:
 | 
			
		||||
            return {'status': 1, 'message': 'Missed filling %s once.' %
 | 
			
		||||
                    (', '.join(warning_unfilled_properties),)}
 | 
			
		||||
        return {'status': 2, 'message': 'Missed filling %s once. Missed filling %s at least twice.' %
 | 
			
		||||
                (', '.join(warning_unfilled_properties), ', '.join(critical_unfilled_properties))}
 | 
			
		||||
@@ -1,20 +1,24 @@
 | 
			
		||||
import sys
 | 
			
		||||
from argparse import ArgumentParser
 | 
			
		||||
from typing import Any
 | 
			
		||||
 | 
			
		||||
from argparse import ArgumentParser
 | 
			
		||||
from django.db import connection
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from analytics.lib.counts import do_drop_all_analytics_tables
 | 
			
		||||
 | 
			
		||||
from typing import Any
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = """Clear analytics tables."""
 | 
			
		||||
 | 
			
		||||
    def add_arguments(self, parser: ArgumentParser) -> None:
 | 
			
		||||
    def add_arguments(self, parser):
 | 
			
		||||
        # type: (ArgumentParser) -> None
 | 
			
		||||
        parser.add_argument('--force',
 | 
			
		||||
                            action='store_true',
 | 
			
		||||
                            help="Clear analytics tables.")
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args: Any, **options: Any) -> None:
 | 
			
		||||
    def handle(self, *args, **options):
 | 
			
		||||
        # type: (*Any, **Any) -> None
 | 
			
		||||
        if options['force']:
 | 
			
		||||
            do_drop_all_analytics_tables()
 | 
			
		||||
        else:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,18 @@
 | 
			
		||||
import sys
 | 
			
		||||
from argparse import ArgumentParser
 | 
			
		||||
from typing import Any
 | 
			
		||||
 | 
			
		||||
from argparse import ArgumentParser
 | 
			
		||||
from django.db import connection
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
from analytics.lib.counts import COUNT_STATS, do_drop_single_stat
 | 
			
		||||
from analytics.lib.counts import do_drop_single_stat, COUNT_STATS
 | 
			
		||||
 | 
			
		||||
from typing import Any
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = """Clear analytics tables."""
 | 
			
		||||
 | 
			
		||||
    def add_arguments(self, parser: ArgumentParser) -> None:
 | 
			
		||||
    def add_arguments(self, parser):
 | 
			
		||||
        # type: (ArgumentParser) -> None
 | 
			
		||||
        parser.add_argument('--force',
 | 
			
		||||
                            action='store_true',
 | 
			
		||||
                            help="Actually do it.")
 | 
			
		||||
@@ -17,7 +20,8 @@ class Command(BaseCommand):
 | 
			
		||||
                            type=str,
 | 
			
		||||
                            help="The property of the stat to be cleared.")
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args: Any, **options: Any) -> None:
 | 
			
		||||
    def handle(self, *args, **options):
 | 
			
		||||
        # type: (*Any, **Any) -> None
 | 
			
		||||
        property = options['property']
 | 
			
		||||
        if property not in COUNT_STATS:
 | 
			
		||||
            print("Invalid property: %s" % (property,))
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,14 @@
 | 
			
		||||
import datetime
 | 
			
		||||
from argparse import ArgumentParser
 | 
			
		||||
from typing import Any
 | 
			
		||||
 | 
			
		||||
from argparse import ArgumentParser
 | 
			
		||||
from django.db.models import Count, QuerySet
 | 
			
		||||
from django.utils.timezone import now as timezone_now
 | 
			
		||||
 | 
			
		||||
from zerver.lib.management import ZulipBaseCommand
 | 
			
		||||
from zerver.models import UserActivity
 | 
			
		||||
 | 
			
		||||
import datetime
 | 
			
		||||
 | 
			
		||||
class Command(ZulipBaseCommand):
 | 
			
		||||
    help = """Report rough client activity globally, for a realm, or for a user
 | 
			
		||||
 | 
			
		||||
@@ -17,16 +18,18 @@ Usage examples:
 | 
			
		||||
./manage.py client_activity --target realm --realm zulip
 | 
			
		||||
./manage.py client_activity --target user --user hamlet@zulip.com --realm zulip"""
 | 
			
		||||
 | 
			
		||||
    def add_arguments(self, parser: ArgumentParser) -> None:
 | 
			
		||||
    def add_arguments(self, parser):
 | 
			
		||||
        # type: (ArgumentParser) -> None
 | 
			
		||||
        parser.add_argument('--target', dest='target', required=True, type=str,
 | 
			
		||||
                            help="'server' will calculate client activity of the entire server. "
 | 
			
		||||
                                 "'realm' will calculate client activity of realm. "
 | 
			
		||||
                                 "'user' will calculate client activity of the user.")
 | 
			
		||||
        parser.add_argument('--user', dest='user', type=str,
 | 
			
		||||
                            help="The email address of the user you want to calculate activity.")
 | 
			
		||||
                            help="The email adress of the user you want to calculate activity.")
 | 
			
		||||
        self.add_realm_args(parser)
 | 
			
		||||
 | 
			
		||||
    def compute_activity(self, user_activity_objects: QuerySet) -> None:
 | 
			
		||||
    def compute_activity(self, user_activity_objects):
 | 
			
		||||
        # type: (QuerySet) -> None
 | 
			
		||||
        # Report data from the past week.
 | 
			
		||||
        #
 | 
			
		||||
        # This is a rough report of client activity because we inconsistently
 | 
			
		||||
@@ -56,7 +59,8 @@ Usage examples:
 | 
			
		||||
            print("%25s %15d" % (count[1], count[0]))
 | 
			
		||||
        print("Total:", total)
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args: Any, **options: str) -> None:
 | 
			
		||||
    def handle(self, *args, **options):
 | 
			
		||||
        # type: (*Any, **str) -> None
 | 
			
		||||
        realm = self.get_realm(options)
 | 
			
		||||
        if options["user"] is None:
 | 
			
		||||
            if options["target"] == "server" and realm is None:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,22 @@
 | 
			
		||||
 | 
			
		||||
from datetime import datetime, timedelta
 | 
			
		||||
from typing import Any, Dict, List, Mapping, Optional, Text, Type, Union
 | 
			
		||||
from argparse import ArgumentParser
 | 
			
		||||
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
from django.utils.timezone import now as timezone_now
 | 
			
		||||
 | 
			
		||||
from analytics.lib.counts import COUNT_STATS, \
 | 
			
		||||
    CountStat, do_drop_all_analytics_tables
 | 
			
		||||
from analytics.lib.counts import COUNT_STATS, CountStat, do_drop_all_analytics_tables
 | 
			
		||||
from analytics.lib.fixtures import generate_time_series_data
 | 
			
		||||
from analytics.lib.time_utils import time_range
 | 
			
		||||
from analytics.models import BaseCount, FillState, RealmCount, UserCount, StreamCount
 | 
			
		||||
from analytics.models import BaseCount, InstallationCount, RealmCount, \
 | 
			
		||||
    UserCount, StreamCount, FillState
 | 
			
		||||
from zerver.lib.timestamp import floor_to_day
 | 
			
		||||
from zerver.models import Realm, UserProfile, Stream, Message, Client, \
 | 
			
		||||
    RealmAuditLog, Recipient
 | 
			
		||||
    RealmAuditLog
 | 
			
		||||
 | 
			
		||||
from datetime import datetime, timedelta
 | 
			
		||||
 | 
			
		||||
from six.moves import zip
 | 
			
		||||
from typing import Any, Dict, List, Optional, Text, Type, Union, Mapping
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = """Populates analytics tables with randomly generated data."""
 | 
			
		||||
@@ -20,11 +24,8 @@ class Command(BaseCommand):
 | 
			
		||||
    DAYS_OF_DATA = 100
 | 
			
		||||
    random_seed = 26
 | 
			
		||||
 | 
			
		||||
    def create_user(self, email: Text,
 | 
			
		||||
                    full_name: Text,
 | 
			
		||||
                    is_staff: bool,
 | 
			
		||||
                    date_joined: datetime,
 | 
			
		||||
                    realm: Realm) -> UserProfile:
 | 
			
		||||
    def create_user(self, email, full_name, is_staff, date_joined, realm):
 | 
			
		||||
        # type: (Text, Text, Text, bool, datetime, Realm) -> UserProfile
 | 
			
		||||
        user = UserProfile.objects.create(
 | 
			
		||||
            email=email, full_name=full_name, is_staff=is_staff,
 | 
			
		||||
            realm=realm, short_name=full_name, pointer=-1, last_pointer_updater='none',
 | 
			
		||||
@@ -34,10 +35,10 @@ class Command(BaseCommand):
 | 
			
		||||
            event_time=user.date_joined)
 | 
			
		||||
        return user
 | 
			
		||||
 | 
			
		||||
    def generate_fixture_data(self, stat: CountStat, business_hours_base: float,
 | 
			
		||||
                              non_business_hours_base: float, growth: float,
 | 
			
		||||
                              autocorrelation: float, spikiness: float,
 | 
			
		||||
                              holiday_rate: float=0, partial_sum: bool=False) -> List[int]:
 | 
			
		||||
    def generate_fixture_data(self, stat, business_hours_base, non_business_hours_base,
 | 
			
		||||
                              growth, autocorrelation, spikiness, holiday_rate=0,
 | 
			
		||||
                              partial_sum=False):
 | 
			
		||||
        # type: (CountStat, float, float, float, float, float, float, bool) -> List[int]
 | 
			
		||||
        self.random_seed += 1
 | 
			
		||||
        return generate_time_series_data(
 | 
			
		||||
            days=self.DAYS_OF_DATA, business_hours_base=business_hours_base,
 | 
			
		||||
@@ -45,7 +46,8 @@ class Command(BaseCommand):
 | 
			
		||||
            autocorrelation=autocorrelation, spikiness=spikiness, holiday_rate=holiday_rate,
 | 
			
		||||
            frequency=stat.frequency, partial_sum=partial_sum, random_seed=self.random_seed)
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args: Any, **options: Any) -> None:
 | 
			
		||||
    def handle(self, *args, **options):
 | 
			
		||||
        # type: (*Any, **Any) -> None
 | 
			
		||||
        do_drop_all_analytics_tables()
 | 
			
		||||
        # I believe this also deletes any objects with this realm as a foreign key
 | 
			
		||||
        Realm.objects.filter(string_id='analytics').delete()
 | 
			
		||||
@@ -55,22 +57,15 @@ class Command(BaseCommand):
 | 
			
		||||
        realm = Realm.objects.create(
 | 
			
		||||
            string_id='analytics', name='Analytics', date_created=installation_time)
 | 
			
		||||
        shylock = self.create_user('shylock@analytics.ds', 'Shylock', True, installation_time, realm)
 | 
			
		||||
        stream = Stream.objects.create(
 | 
			
		||||
            name='all', realm=realm, date_created=installation_time)
 | 
			
		||||
        Recipient.objects.create(type_id=stream.id, type=Recipient.STREAM)
 | 
			
		||||
 | 
			
		||||
        def insert_fixture_data(stat: CountStat,
 | 
			
		||||
                                fixture_data: Mapping[Optional[str], List[int]],
 | 
			
		||||
                                table: Type[BaseCount]) -> None:
 | 
			
		||||
        def insert_fixture_data(stat, fixture_data, table):
 | 
			
		||||
            # type: (CountStat, Mapping[Optional[str], List[int]], Type[BaseCount]) -> None
 | 
			
		||||
            end_times = time_range(last_end_time, last_end_time, stat.frequency,
 | 
			
		||||
                                   len(list(fixture_data.values())[0]))
 | 
			
		||||
            if table == RealmCount:
 | 
			
		||||
                id_args = {'realm': realm}
 | 
			
		||||
            if table == UserCount:
 | 
			
		||||
                id_args = {'realm': realm, 'user': shylock}
 | 
			
		||||
            if table == StreamCount:
 | 
			
		||||
                id_args = {'stream': stream, 'realm': realm}
 | 
			
		||||
 | 
			
		||||
            for subgroup, values in fixture_data.items():
 | 
			
		||||
                table.objects.bulk_create([
 | 
			
		||||
                    table(property=stat.property, subgroup=subgroup, end_time=end_time,
 | 
			
		||||
@@ -139,12 +134,4 @@ class Command(BaseCommand):
 | 
			
		||||
        FillState.objects.create(property=stat.property, end_time=last_end_time,
 | 
			
		||||
                                 state=FillState.DONE)
 | 
			
		||||
 | 
			
		||||
        stat = COUNT_STATS['messages_in_stream:is_bot:day']
 | 
			
		||||
        realm_data = {'false': self.generate_fixture_data(stat, 30, 5, 6, .6, 4),
 | 
			
		||||
                      'true': self.generate_fixture_data(stat, 20, 2, 3, .2, 3)}
 | 
			
		||||
        insert_fixture_data(stat, realm_data, RealmCount)
 | 
			
		||||
        stream_data = {'false': self.generate_fixture_data(stat, 10, 7, 5, .6, 4),
 | 
			
		||||
                       'true': self.generate_fixture_data(stat, 5, 3, 2, .4, 2)}  # type: Mapping[Optional[str], List[int]]
 | 
			
		||||
        insert_fixture_data(stat, stream_data, StreamCount)
 | 
			
		||||
        FillState.objects.create(property=stat.property, end_time=last_end_time,
 | 
			
		||||
                                 state=FillState.DONE)
 | 
			
		||||
        # TODO: messages_sent_to_stream:is_bot
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,15 @@
 | 
			
		||||
import datetime
 | 
			
		||||
from argparse import ArgumentParser
 | 
			
		||||
from typing import Any, List
 | 
			
		||||
 | 
			
		||||
from argparse import ArgumentParser
 | 
			
		||||
import datetime
 | 
			
		||||
import pytz
 | 
			
		||||
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
from django.db.models import Count
 | 
			
		||||
from django.utils.timezone import now as timezone_now
 | 
			
		||||
 | 
			
		||||
from zerver.models import Message, Realm, Recipient, Stream, \
 | 
			
		||||
    Subscription, UserActivity, UserMessage, UserProfile, get_realm
 | 
			
		||||
from zerver.models import UserProfile, Realm, Stream, Message, Recipient, UserActivity, \
 | 
			
		||||
    Subscription, UserMessage, get_realm
 | 
			
		||||
 | 
			
		||||
MOBILE_CLIENT_LIST = ["Android", "ios"]
 | 
			
		||||
HUMAN_CLIENT_LIST = MOBILE_CLIENT_LIST + ["website"]
 | 
			
		||||
@@ -18,11 +19,13 @@ 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: ArgumentParser) -> None:
 | 
			
		||||
    def add_arguments(self, parser):
 | 
			
		||||
        # type: (ArgumentParser) -> None
 | 
			
		||||
        parser.add_argument('realms', metavar='<realm>', type=str, nargs='*',
 | 
			
		||||
                            help="realm to generate statistics for")
 | 
			
		||||
 | 
			
		||||
    def active_users(self, realm: Realm) -> List[UserProfile]:
 | 
			
		||||
    def active_users(self, realm):
 | 
			
		||||
        # type: (Realm) -> List[UserProfile]
 | 
			
		||||
        # Has been active (on the website, for now) in the last 7 days.
 | 
			
		||||
        activity_cutoff = timezone_now() - datetime.timedelta(days=7)
 | 
			
		||||
        return [activity.user_profile for activity in (
 | 
			
		||||
@@ -32,44 +35,53 @@ class Command(BaseCommand):
 | 
			
		||||
                                        query="/json/users/me/pointer",
 | 
			
		||||
                                        client__name="website"))]
 | 
			
		||||
 | 
			
		||||
    def messages_sent_by(self, user: UserProfile, days_ago: int) -> int:
 | 
			
		||||
    def messages_sent_by(self, user, days_ago):
 | 
			
		||||
        # type: (UserProfile, int) -> int
 | 
			
		||||
        sent_time_cutoff = timezone_now() - datetime.timedelta(days=days_ago)
 | 
			
		||||
        return human_messages.filter(sender=user, pub_date__gt=sent_time_cutoff).count()
 | 
			
		||||
 | 
			
		||||
    def total_messages(self, realm: Realm, days_ago: int) -> int:
 | 
			
		||||
    def total_messages(self, realm, days_ago):
 | 
			
		||||
        # type: (Realm, int) -> int
 | 
			
		||||
        sent_time_cutoff = timezone_now() - datetime.timedelta(days=days_ago)
 | 
			
		||||
        return Message.objects.filter(sender__realm=realm, pub_date__gt=sent_time_cutoff).count()
 | 
			
		||||
 | 
			
		||||
    def human_messages(self, realm: Realm, days_ago: int) -> int:
 | 
			
		||||
    def human_messages(self, realm, days_ago):
 | 
			
		||||
        # type: (Realm, int) -> int
 | 
			
		||||
        sent_time_cutoff = timezone_now() - datetime.timedelta(days=days_ago)
 | 
			
		||||
        return human_messages.filter(sender__realm=realm, pub_date__gt=sent_time_cutoff).count()
 | 
			
		||||
 | 
			
		||||
    def api_messages(self, realm: Realm, days_ago: int) -> int:
 | 
			
		||||
    def api_messages(self, realm, days_ago):
 | 
			
		||||
        # type: (Realm, int) -> int
 | 
			
		||||
        return (self.total_messages(realm, days_ago) - self.human_messages(realm, days_ago))
 | 
			
		||||
 | 
			
		||||
    def stream_messages(self, realm: Realm, days_ago: int) -> int:
 | 
			
		||||
    def stream_messages(self, realm, days_ago):
 | 
			
		||||
        # type: (Realm, int) -> int
 | 
			
		||||
        sent_time_cutoff = timezone_now() - datetime.timedelta(days=days_ago)
 | 
			
		||||
        return human_messages.filter(sender__realm=realm, pub_date__gt=sent_time_cutoff,
 | 
			
		||||
                                     recipient__type=Recipient.STREAM).count()
 | 
			
		||||
 | 
			
		||||
    def private_messages(self, realm: Realm, days_ago: int) -> int:
 | 
			
		||||
    def private_messages(self, realm, days_ago):
 | 
			
		||||
        # type: (Realm, int) -> int
 | 
			
		||||
        sent_time_cutoff = timezone_now() - datetime.timedelta(days=days_ago)
 | 
			
		||||
        return human_messages.filter(sender__realm=realm, pub_date__gt=sent_time_cutoff).exclude(
 | 
			
		||||
            recipient__type=Recipient.STREAM).exclude(recipient__type=Recipient.HUDDLE).count()
 | 
			
		||||
 | 
			
		||||
    def group_private_messages(self, realm: Realm, days_ago: int) -> int:
 | 
			
		||||
    def group_private_messages(self, realm, days_ago):
 | 
			
		||||
        # type: (Realm, int) -> int
 | 
			
		||||
        sent_time_cutoff = timezone_now() - datetime.timedelta(days=days_ago)
 | 
			
		||||
        return human_messages.filter(sender__realm=realm, pub_date__gt=sent_time_cutoff).exclude(
 | 
			
		||||
            recipient__type=Recipient.STREAM).exclude(recipient__type=Recipient.PERSONAL).count()
 | 
			
		||||
 | 
			
		||||
    def report_percentage(self, numerator: float, denominator: float, text: str) -> None:
 | 
			
		||||
    def report_percentage(self, numerator, denominator, text):
 | 
			
		||||
        # type: (float, float, str) -> None
 | 
			
		||||
        if not denominator:
 | 
			
		||||
            fraction = 0.0
 | 
			
		||||
        else:
 | 
			
		||||
            fraction = numerator / float(denominator)
 | 
			
		||||
        print("%.2f%% of" % (fraction * 100,), text)
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args: Any, **options: Any) -> None:
 | 
			
		||||
    def handle(self, *args, **options):
 | 
			
		||||
        # type: (*Any, **Any) -> None
 | 
			
		||||
        if options['realms']:
 | 
			
		||||
            try:
 | 
			
		||||
                realms = [get_realm(string_id) for string_id in options['realms']]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,20 +1,20 @@
 | 
			
		||||
from argparse import ArgumentParser
 | 
			
		||||
from typing import Any
 | 
			
		||||
 | 
			
		||||
from argparse import ArgumentParser
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
from django.db.models import Q
 | 
			
		||||
 | 
			
		||||
from zerver.models import Message, Realm, \
 | 
			
		||||
    Recipient, Stream, Subscription, get_realm
 | 
			
		||||
from zerver.models import Realm, Stream, Message, Subscription, Recipient, get_realm
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "Generate statistics on the streams for a realm."
 | 
			
		||||
 | 
			
		||||
    def add_arguments(self, parser: ArgumentParser) -> None:
 | 
			
		||||
    def add_arguments(self, parser):
 | 
			
		||||
        # type: (ArgumentParser) -> None
 | 
			
		||||
        parser.add_argument('realms', metavar='<realm>', type=str, nargs='*',
 | 
			
		||||
                            help="realm to generate statistics for")
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args: Any, **options: str) -> None:
 | 
			
		||||
    def handle(self, *args, **options):
 | 
			
		||||
        # type: (*Any, **str) -> None
 | 
			
		||||
        if options['realms']:
 | 
			
		||||
            try:
 | 
			
		||||
                realms = [get_realm(string_id) for string_id in options['realms']]
 | 
			
		||||
@@ -36,8 +36,7 @@ class Command(BaseCommand):
 | 
			
		||||
                    continue
 | 
			
		||||
                print("%25s" % (stream.name,), end=' ')
 | 
			
		||||
                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)),), end=' ')
 | 
			
		||||
                num_messages = len(Message.objects.filter(recipient=recipient))
 | 
			
		||||
                print("%12d" % (num_messages,))
 | 
			
		||||
            print("%d invite-only streams" % (invite_only_count,))
 | 
			
		||||
 
 | 
			
		||||
@@ -1,29 +1,34 @@
 | 
			
		||||
import os
 | 
			
		||||
import time
 | 
			
		||||
from argparse import ArgumentParser
 | 
			
		||||
from typing import Any, Dict
 | 
			
		||||
import sys
 | 
			
		||||
from scripts.lib.zulip_tools import ENDC, WARNING
 | 
			
		||||
 | 
			
		||||
from argparse import ArgumentParser
 | 
			
		||||
from datetime import timedelta
 | 
			
		||||
import time
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
from django.utils.dateparse import parse_datetime
 | 
			
		||||
from django.utils.timezone import now as timezone_now
 | 
			
		||||
from django.utils.timezone import utc as timezone_utc
 | 
			
		||||
from django.utils.dateparse import parse_datetime
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
 | 
			
		||||
from analytics.models import RealmCount, UserCount
 | 
			
		||||
from analytics.lib.counts import COUNT_STATS, logger, process_count_stat
 | 
			
		||||
from scripts.lib.zulip_tools import ENDC, WARNING
 | 
			
		||||
from zerver.lib.timestamp import floor_to_hour
 | 
			
		||||
from zerver.models import Realm
 | 
			
		||||
from zerver.models import UserProfile, Message, Realm
 | 
			
		||||
 | 
			
		||||
from typing import Any, Dict
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = """Fills Analytics tables.
 | 
			
		||||
 | 
			
		||||
    Run as a cron job that runs every hour."""
 | 
			
		||||
 | 
			
		||||
    def add_arguments(self, parser: ArgumentParser) -> None:
 | 
			
		||||
    def add_arguments(self, parser):
 | 
			
		||||
        # type: (ArgumentParser) -> None
 | 
			
		||||
        parser.add_argument('--time', '-t',
 | 
			
		||||
                            type=str,
 | 
			
		||||
                            help='Update stat tables from current state to'
 | 
			
		||||
                                 '--time. Defaults to the current time.',
 | 
			
		||||
                            help='Update stat tables from current state to --time. Defaults to the current time.',
 | 
			
		||||
                            default=timezone_now().isoformat())
 | 
			
		||||
        parser.add_argument('--utc',
 | 
			
		||||
                            action='store_true',
 | 
			
		||||
@@ -37,7 +42,8 @@ class Command(BaseCommand):
 | 
			
		||||
                            help="Print timing information to stdout.",
 | 
			
		||||
                            default=False)
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args: Any, **options: Any) -> None:
 | 
			
		||||
    def handle(self, *args, **options):
 | 
			
		||||
        # type: (*Any, **Any) -> None
 | 
			
		||||
        try:
 | 
			
		||||
            os.mkdir(settings.ANALYTICS_LOCK_DIR)
 | 
			
		||||
        except OSError:
 | 
			
		||||
@@ -49,7 +55,8 @@ class Command(BaseCommand):
 | 
			
		||||
        finally:
 | 
			
		||||
            os.rmdir(settings.ANALYTICS_LOCK_DIR)
 | 
			
		||||
 | 
			
		||||
    def run_update_analytics_counts(self, options: Dict[str, Any]) -> None:
 | 
			
		||||
    def run_update_analytics_counts(self, options):
 | 
			
		||||
        # type: (Dict[str, Any]) -> None
 | 
			
		||||
        # installation_epoch relies on there being at least one realm; we
 | 
			
		||||
        # shouldn't run the analytics code if that condition isn't satisfied
 | 
			
		||||
        if not Realm.objects.exists():
 | 
			
		||||
 
 | 
			
		||||
@@ -1,25 +1,29 @@
 | 
			
		||||
import datetime
 | 
			
		||||
from argparse import ArgumentParser
 | 
			
		||||
import datetime
 | 
			
		||||
import pytz
 | 
			
		||||
from typing import Any
 | 
			
		||||
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
from django.utils.timezone import now as timezone_now
 | 
			
		||||
 | 
			
		||||
from zerver.models import Message, Realm, Stream, UserProfile, get_realm
 | 
			
		||||
from zerver.models import UserProfile, Realm, Stream, Message, get_realm
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "Generate statistics on user activity."
 | 
			
		||||
 | 
			
		||||
    def add_arguments(self, parser: ArgumentParser) -> None:
 | 
			
		||||
    def add_arguments(self, parser):
 | 
			
		||||
        # type: (ArgumentParser) -> None
 | 
			
		||||
        parser.add_argument('realms', metavar='<realm>', type=str, nargs='*',
 | 
			
		||||
                            help="realm to generate statistics for")
 | 
			
		||||
 | 
			
		||||
    def messages_sent_by(self, user: UserProfile, week: int) -> int:
 | 
			
		||||
    def messages_sent_by(self, user, week):
 | 
			
		||||
        # type: (UserProfile, int) -> int
 | 
			
		||||
        start = timezone_now() - datetime.timedelta(days=(week + 1)*7)
 | 
			
		||||
        end = timezone_now() - datetime.timedelta(days=week*7)
 | 
			
		||||
        return Message.objects.filter(sender=user, pub_date__gt=start, pub_date__lte=end).count()
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args: Any, **options: Any) -> None:
 | 
			
		||||
    def handle(self, *args, **options):
 | 
			
		||||
        # type: (*Any, **Any) -> None
 | 
			
		||||
        if options['realms']:
 | 
			
		||||
            try:
 | 
			
		||||
                realms = [get_realm(string_id) for string_id in options['realms']]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,10 @@
 | 
			
		||||
# -*- coding: utf-8 -*-
 | 
			
		||||
from django.db import models, migrations
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
import zerver.lib.str_utils
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
# -*- coding: utf-8 -*-
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
# -*- coding: utf-8 -*-
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
import zerver.lib.str_utils
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
# -*- coding: utf-8 -*-
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
# -*- coding: utf-8 -*-
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
# -*- coding: utf-8 -*-
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@
 | 
			
		||||
# Generated by Django 1.10.5 on 2017-02-01 22:28
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,11 @@
 | 
			
		||||
# -*- coding: utf-8 -*-
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
from django.db.backends.postgresql_psycopg2.schema import DatabaseSchemaEditor
 | 
			
		||||
from django.db.migrations.state import StateApps
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
 | 
			
		||||
def delete_messages_sent_to_stream_stat(apps: StateApps, schema_editor: DatabaseSchemaEditor) -> None:
 | 
			
		||||
 | 
			
		||||
def delete_messages_sent_to_stream_stat(apps, schema_editor):
 | 
			
		||||
    # type: (StateApps, DatabaseSchemaEditor) -> None
 | 
			
		||||
    UserCount = apps.get_model('analytics', 'UserCount')
 | 
			
		||||
    StreamCount = apps.get_model('analytics', 'StreamCount')
 | 
			
		||||
    RealmCount = apps.get_model('analytics', 'RealmCount')
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,10 @@
 | 
			
		||||
# -*- coding: utf-8 -*-
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
from django.db.backends.postgresql_psycopg2.schema import DatabaseSchemaEditor
 | 
			
		||||
from django.db.migrations.state import StateApps
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
 | 
			
		||||
def clear_message_sent_by_message_type_values(apps: StateApps, schema_editor: DatabaseSchemaEditor) -> None:
 | 
			
		||||
def clear_message_sent_by_message_type_values(apps, schema_editor):
 | 
			
		||||
    # type: (StateApps, DatabaseSchemaEditor) -> None
 | 
			
		||||
    UserCount = apps.get_model('analytics', 'UserCount')
 | 
			
		||||
    StreamCount = apps.get_model('analytics', 'StreamCount')
 | 
			
		||||
    RealmCount = apps.get_model('analytics', 'RealmCount')
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,11 @@
 | 
			
		||||
# -*- coding: utf-8 -*-
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
from django.db.backends.postgresql_psycopg2.schema import DatabaseSchemaEditor
 | 
			
		||||
from django.db.migrations.state import StateApps
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
 | 
			
		||||
def clear_analytics_tables(apps: StateApps, schema_editor: DatabaseSchemaEditor) -> None:
 | 
			
		||||
 | 
			
		||||
def clear_analytics_tables(apps, schema_editor):
 | 
			
		||||
    # type: (StateApps, DatabaseSchemaEditor) -> None
 | 
			
		||||
    UserCount = apps.get_model('analytics', 'UserCount')
 | 
			
		||||
    StreamCount = apps.get_model('analytics', 'StreamCount')
 | 
			
		||||
    RealmCount = apps.get_model('analytics', 'RealmCount')
 | 
			
		||||
 
 | 
			
		||||
@@ -1,36 +0,0 @@
 | 
			
		||||
# -*- coding: utf-8 -*-
 | 
			
		||||
# Generated by Django 1.11.6 on 2018-01-29 08:14
 | 
			
		||||
from __future__ import unicode_literals
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('analytics', '0011_clear_analytics_tables'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='installationcount',
 | 
			
		||||
            name='anomaly',
 | 
			
		||||
            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='analytics.Anomaly'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='realmcount',
 | 
			
		||||
            name='anomaly',
 | 
			
		||||
            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='analytics.Anomaly'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='streamcount',
 | 
			
		||||
            name='anomaly',
 | 
			
		||||
            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='analytics.Anomaly'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='usercount',
 | 
			
		||||
            name='anomaly',
 | 
			
		||||
            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='analytics.Anomaly'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -1,12 +1,14 @@
 | 
			
		||||
import datetime
 | 
			
		||||
from typing import Any, Dict, Optional, Text, Tuple, Union
 | 
			
		||||
 | 
			
		||||
from django.db import models
 | 
			
		||||
 | 
			
		||||
from zerver.models import Realm, UserProfile, Stream, Recipient
 | 
			
		||||
from zerver.lib.str_utils import ModelReprMixin
 | 
			
		||||
from zerver.lib.timestamp import floor_to_day
 | 
			
		||||
from zerver.models import Realm, Recipient, Stream, UserProfile
 | 
			
		||||
 | 
			
		||||
class FillState(models.Model):
 | 
			
		||||
import datetime
 | 
			
		||||
 | 
			
		||||
from typing import Optional, Tuple, Union, Dict, Any, Text
 | 
			
		||||
 | 
			
		||||
class FillState(ModelReprMixin, models.Model):
 | 
			
		||||
    property = models.CharField(max_length=40, unique=True)  # type: Text
 | 
			
		||||
    end_time = models.DateTimeField()  # type: datetime.datetime
 | 
			
		||||
 | 
			
		||||
@@ -17,16 +19,19 @@ class FillState(models.Model):
 | 
			
		||||
 | 
			
		||||
    last_modified = models.DateTimeField(auto_now=True)  # type: datetime.datetime
 | 
			
		||||
 | 
			
		||||
    def __str__(self) -> Text:
 | 
			
		||||
        return "<FillState: %s %s %s>" % (self.property, self.end_time, self.state)
 | 
			
		||||
    def __unicode__(self):
 | 
			
		||||
        # type: () -> Text
 | 
			
		||||
        return u"<FillState: %s %s %s>" % (self.property, self.end_time, self.state)
 | 
			
		||||
 | 
			
		||||
# The earliest/starting end_time in FillState
 | 
			
		||||
# We assume there is at least one realm
 | 
			
		||||
def installation_epoch() -> datetime.datetime:
 | 
			
		||||
def installation_epoch():
 | 
			
		||||
    # type: () -> datetime.datetime
 | 
			
		||||
    earliest_realm_creation = Realm.objects.aggregate(models.Min('date_created'))['date_created__min']
 | 
			
		||||
    return floor_to_day(earliest_realm_creation)
 | 
			
		||||
 | 
			
		||||
def last_successful_fill(property: str) -> Optional[datetime.datetime]:
 | 
			
		||||
def last_successful_fill(property):
 | 
			
		||||
    # type: (str) -> Optional[datetime.datetime]
 | 
			
		||||
    fillstate = FillState.objects.filter(property=property).first()
 | 
			
		||||
    if fillstate is None:
 | 
			
		||||
        return None
 | 
			
		||||
@@ -35,13 +40,14 @@ def last_successful_fill(property: str) -> Optional[datetime.datetime]:
 | 
			
		||||
    return fillstate.end_time - datetime.timedelta(hours=1)
 | 
			
		||||
 | 
			
		||||
# would only ever make entries here by hand
 | 
			
		||||
class Anomaly(models.Model):
 | 
			
		||||
class Anomaly(ModelReprMixin, models.Model):
 | 
			
		||||
    info = models.CharField(max_length=1000)  # type: Text
 | 
			
		||||
 | 
			
		||||
    def __str__(self) -> Text:
 | 
			
		||||
        return "<Anomaly: %s... %s>" % (self.info, self.id)
 | 
			
		||||
    def __unicode__(self):
 | 
			
		||||
        # type: () -> Text
 | 
			
		||||
        return u"<Anomaly: %s... %s>" % (self.info, self.id)
 | 
			
		||||
 | 
			
		||||
class BaseCount(models.Model):
 | 
			
		||||
class BaseCount(ModelReprMixin, models.Model):
 | 
			
		||||
    # Note: When inheriting from BaseCount, you may want to rearrange
 | 
			
		||||
    # the order of the columns in the migration to make sure they
 | 
			
		||||
    # match how you'd like the table to be arranged.
 | 
			
		||||
@@ -49,52 +55,55 @@ class BaseCount(models.Model):
 | 
			
		||||
    subgroup = models.CharField(max_length=16, null=True)  # type: Optional[Text]
 | 
			
		||||
    end_time = models.DateTimeField()  # type: datetime.datetime
 | 
			
		||||
    value = models.BigIntegerField()  # type: int
 | 
			
		||||
    anomaly = models.ForeignKey(Anomaly, on_delete=models.SET_NULL, null=True)  # type: Optional[Anomaly]
 | 
			
		||||
    anomaly = models.ForeignKey(Anomaly, null=True)  # type: Optional[Anomaly]
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
    class Meta(object):
 | 
			
		||||
        abstract = True
 | 
			
		||||
 | 
			
		||||
class InstallationCount(BaseCount):
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
    class Meta(object):
 | 
			
		||||
        unique_together = ("property", "subgroup", "end_time")
 | 
			
		||||
 | 
			
		||||
    def __str__(self) -> Text:
 | 
			
		||||
        return "<InstallationCount: %s %s %s>" % (self.property, self.subgroup, self.value)
 | 
			
		||||
    def __unicode__(self):
 | 
			
		||||
        # type: () -> Text
 | 
			
		||||
        return u"<InstallationCount: %s %s %s>" % (self.property, self.subgroup, self.value)
 | 
			
		||||
 | 
			
		||||
class RealmCount(BaseCount):
 | 
			
		||||
    realm = models.ForeignKey(Realm, on_delete=models.CASCADE)
 | 
			
		||||
    realm = models.ForeignKey(Realm)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
    class Meta(object):
 | 
			
		||||
        unique_together = ("realm", "property", "subgroup", "end_time")
 | 
			
		||||
        index_together = ["property", "end_time"]
 | 
			
		||||
 | 
			
		||||
    def __str__(self) -> Text:
 | 
			
		||||
        return "<RealmCount: %s %s %s %s>" % (self.realm, self.property, self.subgroup, self.value)
 | 
			
		||||
    def __unicode__(self):
 | 
			
		||||
        # type: () -> Text
 | 
			
		||||
        return u"<RealmCount: %s %s %s %s>" % (self.realm, self.property, self.subgroup, self.value)
 | 
			
		||||
 | 
			
		||||
class UserCount(BaseCount):
 | 
			
		||||
    user = models.ForeignKey(UserProfile, on_delete=models.CASCADE)
 | 
			
		||||
    realm = models.ForeignKey(Realm, on_delete=models.CASCADE)
 | 
			
		||||
    user = models.ForeignKey(UserProfile)
 | 
			
		||||
    realm = models.ForeignKey(Realm)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
    class Meta(object):
 | 
			
		||||
        unique_together = ("user", "property", "subgroup", "end_time")
 | 
			
		||||
        # This index dramatically improves the performance of
 | 
			
		||||
        # aggregating from users to realms
 | 
			
		||||
        index_together = ["property", "realm", "end_time"]
 | 
			
		||||
 | 
			
		||||
    def __str__(self) -> Text:
 | 
			
		||||
        return "<UserCount: %s %s %s %s>" % (self.user, self.property, self.subgroup, self.value)
 | 
			
		||||
    def __unicode__(self):
 | 
			
		||||
        # type: () -> Text
 | 
			
		||||
        return u"<UserCount: %s %s %s %s>" % (self.user, self.property, self.subgroup, self.value)
 | 
			
		||||
 | 
			
		||||
class StreamCount(BaseCount):
 | 
			
		||||
    stream = models.ForeignKey(Stream, on_delete=models.CASCADE)
 | 
			
		||||
    realm = models.ForeignKey(Realm, on_delete=models.CASCADE)
 | 
			
		||||
    stream = models.ForeignKey(Stream)
 | 
			
		||||
    realm = models.ForeignKey(Realm)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
    class Meta(object):
 | 
			
		||||
        unique_together = ("stream", "property", "subgroup", "end_time")
 | 
			
		||||
        # This index dramatically improves the performance of
 | 
			
		||||
        # aggregating from streams to realms
 | 
			
		||||
        index_together = ["property", "realm", "end_time"]
 | 
			
		||||
 | 
			
		||||
    def __str__(self) -> Text:
 | 
			
		||||
        return "<StreamCount: %s %s %s %s %s>" % (
 | 
			
		||||
            self.stream, self.property, self.subgroup, self.value, self.id)
 | 
			
		||||
    def __unicode__(self):
 | 
			
		||||
        # type: () -> Text
 | 
			
		||||
        return u"<StreamCount: %s %s %s %s %s>" % (self.stream, self.property, self.subgroup, self.value, self.id)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,4 @@
 | 
			
		||||
 | 
			
		||||
from datetime import datetime, timedelta
 | 
			
		||||
from typing import Any, Dict, List, Optional, Text, Tuple, Type, Union
 | 
			
		||||
 | 
			
		||||
import ujson
 | 
			
		||||
from django.apps import apps
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models import Sum
 | 
			
		||||
@@ -10,22 +6,23 @@ from django.test import TestCase
 | 
			
		||||
from django.utils.timezone import now as timezone_now
 | 
			
		||||
from django.utils.timezone import utc as timezone_utc
 | 
			
		||||
 | 
			
		||||
from analytics.lib.counts import COUNT_STATS, CountStat, DataCollector, \
 | 
			
		||||
    DependentCountStat, LoggingCountStat, do_aggregate_to_summary_table, \
 | 
			
		||||
    do_drop_all_analytics_tables, do_drop_single_stat, \
 | 
			
		||||
    do_fill_count_stat_at_hour, do_increment_logging_stat, \
 | 
			
		||||
    process_count_stat, sql_data_collector
 | 
			
		||||
from analytics.models import Anomaly, BaseCount, \
 | 
			
		||||
    FillState, InstallationCount, RealmCount, StreamCount, \
 | 
			
		||||
    UserCount, installation_epoch, last_successful_fill
 | 
			
		||||
from zerver.lib.actions import do_activate_user, do_create_user, \
 | 
			
		||||
    do_deactivate_user, do_reactivate_user, update_user_activity_interval, \
 | 
			
		||||
    do_invite_users, do_revoke_user_invite, do_resend_user_invite_email, \
 | 
			
		||||
    InvitationError
 | 
			
		||||
from zerver.lib.timestamp import TimezoneNotUTCException, floor_to_day
 | 
			
		||||
from zerver.models import Client, Huddle, Message, Realm, \
 | 
			
		||||
    RealmAuditLog, Recipient, Stream, UserActivityInterval, \
 | 
			
		||||
    UserProfile, get_client, get_user, PreregistrationUser
 | 
			
		||||
from analytics.lib.counts import CountStat, COUNT_STATS, process_count_stat, \
 | 
			
		||||
    do_fill_count_stat_at_hour, do_increment_logging_stat, DataCollector, \
 | 
			
		||||
    sql_data_collector, LoggingCountStat, do_aggregate_to_summary_table, \
 | 
			
		||||
    do_drop_all_analytics_tables, do_drop_single_stat, DependentCountStat
 | 
			
		||||
from analytics.models import BaseCount, InstallationCount, RealmCount, \
 | 
			
		||||
    UserCount, StreamCount, FillState, Anomaly, installation_epoch, \
 | 
			
		||||
    last_successful_fill
 | 
			
		||||
from zerver.lib.actions import do_create_user, do_deactivate_user, \
 | 
			
		||||
    do_activate_user, do_reactivate_user, update_user_activity_interval
 | 
			
		||||
from zerver.lib.timestamp import floor_to_day, TimezoneNotUTCException
 | 
			
		||||
from zerver.models import Realm, UserProfile, Message, Stream, Recipient, \
 | 
			
		||||
    Huddle, Client, UserActivityInterval, RealmAuditLog, get_client, get_user
 | 
			
		||||
 | 
			
		||||
from datetime import datetime, timedelta
 | 
			
		||||
import ujson
 | 
			
		||||
 | 
			
		||||
from typing import Any, Dict, List, Optional, Text, Tuple, Type, Union
 | 
			
		||||
 | 
			
		||||
class AnalyticsTestCase(TestCase):
 | 
			
		||||
    MINUTE = timedelta(seconds = 60)
 | 
			
		||||
@@ -34,7 +31,8 @@ class AnalyticsTestCase(TestCase):
 | 
			
		||||
    TIME_ZERO = datetime(1988, 3, 14).replace(tzinfo=timezone_utc)
 | 
			
		||||
    TIME_LAST_HOUR = TIME_ZERO - HOUR
 | 
			
		||||
 | 
			
		||||
    def setUp(self) -> None:
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        self.default_realm = Realm.objects.create(
 | 
			
		||||
            string_id='realmtest', name='Realm Test', date_created=self.TIME_ZERO - 2*self.DAY)
 | 
			
		||||
        # used to generate unique names in self.create_*
 | 
			
		||||
@@ -43,7 +41,8 @@ class AnalyticsTestCase(TestCase):
 | 
			
		||||
        self.current_property = None  # type: Optional[str]
 | 
			
		||||
 | 
			
		||||
    # Lightweight creation of users, streams, and messages
 | 
			
		||||
    def create_user(self, **kwargs: Any) -> UserProfile:
 | 
			
		||||
    def create_user(self, **kwargs):
 | 
			
		||||
        # type: (**Any) -> UserProfile
 | 
			
		||||
        self.name_counter += 1
 | 
			
		||||
        defaults = {
 | 
			
		||||
            'email': 'user%s@domain.tld' % (self.name_counter,),
 | 
			
		||||
@@ -58,7 +57,8 @@ class AnalyticsTestCase(TestCase):
 | 
			
		||||
            kwargs[key] = kwargs.get(key, value)
 | 
			
		||||
        return UserProfile.objects.create(**kwargs)
 | 
			
		||||
 | 
			
		||||
    def create_stream_with_recipient(self, **kwargs: Any) -> Tuple[Stream, Recipient]:
 | 
			
		||||
    def create_stream_with_recipient(self, **kwargs):
 | 
			
		||||
        # type: (**Any) -> Tuple[Stream, Recipient]
 | 
			
		||||
        self.name_counter += 1
 | 
			
		||||
        defaults = {'name': 'stream name %s' % (self.name_counter,),
 | 
			
		||||
                    'realm': self.default_realm,
 | 
			
		||||
@@ -69,7 +69,8 @@ class AnalyticsTestCase(TestCase):
 | 
			
		||||
        recipient = Recipient.objects.create(type_id=stream.id, type=Recipient.STREAM)
 | 
			
		||||
        return stream, recipient
 | 
			
		||||
 | 
			
		||||
    def create_huddle_with_recipient(self, **kwargs: Any) -> Tuple[Huddle, Recipient]:
 | 
			
		||||
    def create_huddle_with_recipient(self, **kwargs):
 | 
			
		||||
        # type: (**Any) -> Tuple[Huddle, Recipient]
 | 
			
		||||
        self.name_counter += 1
 | 
			
		||||
        defaults = {'huddle_hash': 'hash%s' % (self.name_counter,)}
 | 
			
		||||
        for key, value in defaults.items():
 | 
			
		||||
@@ -78,7 +79,8 @@ class AnalyticsTestCase(TestCase):
 | 
			
		||||
        recipient = Recipient.objects.create(type_id=huddle.id, type=Recipient.HUDDLE)
 | 
			
		||||
        return huddle, recipient
 | 
			
		||||
 | 
			
		||||
    def create_message(self, sender: UserProfile, recipient: Recipient, **kwargs: Any) -> Message:
 | 
			
		||||
    def create_message(self, sender, recipient, **kwargs):
 | 
			
		||||
        # type: (UserProfile, Recipient, **Any) -> Message
 | 
			
		||||
        defaults = {
 | 
			
		||||
            'sender': sender,
 | 
			
		||||
            'recipient': recipient,
 | 
			
		||||
@@ -91,9 +93,9 @@ class AnalyticsTestCase(TestCase):
 | 
			
		||||
        return Message.objects.create(**kwargs)
 | 
			
		||||
 | 
			
		||||
    # kwargs should only ever be a UserProfile or Stream.
 | 
			
		||||
    def assertCountEquals(self, table: Type[BaseCount], value: int, property: Optional[Text]=None,
 | 
			
		||||
                          subgroup: Optional[Text]=None, end_time: datetime=TIME_ZERO,
 | 
			
		||||
                          realm: Optional[Realm]=None, **kwargs: models.Model) -> None:
 | 
			
		||||
    def assertCountEquals(self, table, value, property=None, subgroup=None,
 | 
			
		||||
                          end_time=TIME_ZERO, realm=None, **kwargs):
 | 
			
		||||
        # type: (Type[BaseCount], int, Optional[Text], Optional[Text], datetime, Optional[Realm], **models.Model) -> None
 | 
			
		||||
        if property is None:
 | 
			
		||||
            property = self.current_property
 | 
			
		||||
        queryset = table.objects.filter(property=property, end_time=end_time).filter(**kwargs)
 | 
			
		||||
@@ -105,8 +107,8 @@ class AnalyticsTestCase(TestCase):
 | 
			
		||||
            queryset = queryset.filter(subgroup=subgroup)
 | 
			
		||||
        self.assertEqual(queryset.values_list('value', flat=True)[0], value)
 | 
			
		||||
 | 
			
		||||
    def assertTableState(self, table: Type[BaseCount], arg_keys: List[str],
 | 
			
		||||
                         arg_values: List[List[object]]) -> None:
 | 
			
		||||
    def assertTableState(self, table, arg_keys, arg_values):
 | 
			
		||||
        # type: (Type[BaseCount], List[str], List[List[Union[int, str, bool, datetime, Realm, UserProfile, Stream]]]) -> None
 | 
			
		||||
        """Assert that the state of a *Count table is what it should be.
 | 
			
		||||
 | 
			
		||||
        Example usage:
 | 
			
		||||
@@ -151,18 +153,20 @@ class AnalyticsTestCase(TestCase):
 | 
			
		||||
        self.assertEqual(table.objects.count(), len(arg_values))
 | 
			
		||||
 | 
			
		||||
class TestProcessCountStat(AnalyticsTestCase):
 | 
			
		||||
    def make_dummy_count_stat(self, property: str) -> CountStat:
 | 
			
		||||
    def make_dummy_count_stat(self, property):
 | 
			
		||||
        # type: (str) -> CountStat
 | 
			
		||||
        query = """INSERT INTO analytics_realmcount (realm_id, value, property, end_time)
 | 
			
		||||
                   VALUES (%s, 1, '%s', %%%%(time_end)s)""" % (self.default_realm.id, property)
 | 
			
		||||
        return CountStat(property, sql_data_collector(RealmCount, query, None), CountStat.HOUR)
 | 
			
		||||
 | 
			
		||||
    def assertFillStateEquals(self, stat: CountStat, end_time: datetime,
 | 
			
		||||
                              state: int=FillState.DONE) -> None:
 | 
			
		||||
    def assertFillStateEquals(self, stat, end_time, state=FillState.DONE):
 | 
			
		||||
        # type: (CountStat, datetime, int) -> None
 | 
			
		||||
        fill_state = FillState.objects.filter(property=stat.property).first()
 | 
			
		||||
        self.assertEqual(fill_state.end_time, end_time)
 | 
			
		||||
        self.assertEqual(fill_state.state, state)
 | 
			
		||||
 | 
			
		||||
    def test_process_stat(self) -> None:
 | 
			
		||||
    def test_process_stat(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        # process new stat
 | 
			
		||||
        current_time = installation_epoch() + self.HOUR
 | 
			
		||||
        stat = self.make_dummy_count_stat('test stat')
 | 
			
		||||
@@ -188,7 +192,8 @@ class TestProcessCountStat(AnalyticsTestCase):
 | 
			
		||||
        self.assertFillStateEquals(stat, current_time)
 | 
			
		||||
        self.assertEqual(InstallationCount.objects.filter(property=stat.property).count(), 2)
 | 
			
		||||
 | 
			
		||||
    def test_bad_fill_to_time(self) -> None:
 | 
			
		||||
    def test_bad_fill_to_time(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        stat = self.make_dummy_count_stat('test stat')
 | 
			
		||||
        with self.assertRaises(ValueError):
 | 
			
		||||
            process_count_stat(stat, installation_epoch() + 65*self.MINUTE)
 | 
			
		||||
@@ -198,7 +203,8 @@ class TestProcessCountStat(AnalyticsTestCase):
 | 
			
		||||
    # This tests the LoggingCountStat branch of the code in do_delete_counts_at_hour.
 | 
			
		||||
    # It is important that do_delete_counts_at_hour not delete any of the collected
 | 
			
		||||
    # logging data!
 | 
			
		||||
    def test_process_logging_stat(self) -> None:
 | 
			
		||||
    def test_process_logging_stat(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        end_time = self.TIME_ZERO
 | 
			
		||||
 | 
			
		||||
        user_stat = LoggingCountStat('user stat', UserCount, CountStat.DAY)
 | 
			
		||||
@@ -220,13 +226,9 @@ class TestProcessCountStat(AnalyticsTestCase):
 | 
			
		||||
        self.assertTableState(UserCount, ['property', 'value'], [[user_stat.property, 5]])
 | 
			
		||||
        self.assertTableState(StreamCount, ['property', 'value'], [[stream_stat.property, 5]])
 | 
			
		||||
        self.assertTableState(RealmCount, ['property', 'value'],
 | 
			
		||||
                              [[user_stat.property, 5],
 | 
			
		||||
                               [stream_stat.property, 5],
 | 
			
		||||
                               [realm_stat.property, 5]])
 | 
			
		||||
                              [[user_stat.property, 5], [stream_stat.property, 5], [realm_stat.property, 5]])
 | 
			
		||||
        self.assertTableState(InstallationCount, ['property', 'value'],
 | 
			
		||||
                              [[user_stat.property, 5],
 | 
			
		||||
                               [stream_stat.property, 5],
 | 
			
		||||
                               [realm_stat.property, 5]])
 | 
			
		||||
                              [[user_stat.property, 5], [stream_stat.property, 5], [realm_stat.property, 5]])
 | 
			
		||||
 | 
			
		||||
        # Change the logged data and mark FillState as dirty
 | 
			
		||||
        UserCount.objects.update(value=6)
 | 
			
		||||
@@ -240,21 +242,17 @@ class TestProcessCountStat(AnalyticsTestCase):
 | 
			
		||||
        self.assertTableState(UserCount, ['property', 'value'], [[user_stat.property, 6]])
 | 
			
		||||
        self.assertTableState(StreamCount, ['property', 'value'], [[stream_stat.property, 6]])
 | 
			
		||||
        self.assertTableState(RealmCount, ['property', 'value'],
 | 
			
		||||
                              [[user_stat.property, 6],
 | 
			
		||||
                               [stream_stat.property, 6],
 | 
			
		||||
                               [realm_stat.property, 6]])
 | 
			
		||||
                              [[user_stat.property, 6], [stream_stat.property, 6], [realm_stat.property, 6]])
 | 
			
		||||
        self.assertTableState(InstallationCount, ['property', 'value'],
 | 
			
		||||
                              [[user_stat.property, 6],
 | 
			
		||||
                               [stream_stat.property, 6],
 | 
			
		||||
                               [realm_stat.property, 6]])
 | 
			
		||||
                              [[user_stat.property, 6], [stream_stat.property, 6], [realm_stat.property, 6]])
 | 
			
		||||
 | 
			
		||||
    def test_process_dependent_stat(self) -> None:
 | 
			
		||||
    def test_process_dependent_stat(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        stat1 = self.make_dummy_count_stat('stat1')
 | 
			
		||||
        stat2 = self.make_dummy_count_stat('stat2')
 | 
			
		||||
        query = """INSERT INTO analytics_realmcount (realm_id, value, property, end_time)
 | 
			
		||||
                   VALUES (%s, 1, '%s', %%%%(time_end)s)""" % (self.default_realm.id, 'stat3')
 | 
			
		||||
        stat3 = DependentCountStat('stat3', sql_data_collector(RealmCount, query, None),
 | 
			
		||||
                                   CountStat.HOUR,
 | 
			
		||||
        stat3 = DependentCountStat('stat3', sql_data_collector(RealmCount, query, None), CountStat.HOUR,
 | 
			
		||||
                                   dependencies=['stat1', 'stat2'])
 | 
			
		||||
        hour = [installation_epoch() + i*self.HOUR for i in range(5)]
 | 
			
		||||
 | 
			
		||||
@@ -287,8 +285,7 @@ class TestProcessCountStat(AnalyticsTestCase):
 | 
			
		||||
        # test daily dependent stat with hourly dependencies
 | 
			
		||||
        query = """INSERT INTO analytics_realmcount (realm_id, value, property, end_time)
 | 
			
		||||
                   VALUES (%s, 1, '%s', %%%%(time_end)s)""" % (self.default_realm.id, 'stat4')
 | 
			
		||||
        stat4 = DependentCountStat('stat4', sql_data_collector(RealmCount, query, None),
 | 
			
		||||
                                   CountStat.DAY,
 | 
			
		||||
        stat4 = DependentCountStat('stat4', sql_data_collector(RealmCount, query, None), CountStat.DAY,
 | 
			
		||||
                                   dependencies=['stat1', 'stat2'])
 | 
			
		||||
        hour24 = installation_epoch() + 24*self.HOUR
 | 
			
		||||
        hour25 = installation_epoch() + 25*self.HOUR
 | 
			
		||||
@@ -299,8 +296,9 @@ class TestProcessCountStat(AnalyticsTestCase):
 | 
			
		||||
        self.assertFillStateEquals(stat4, hour24)
 | 
			
		||||
 | 
			
		||||
class TestCountStats(AnalyticsTestCase):
 | 
			
		||||
    def setUp(self) -> None:
 | 
			
		||||
        super().setUp()
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        super(TestCountStats, self).setUp()
 | 
			
		||||
        # This tests two things for each of the queries/CountStats: Handling
 | 
			
		||||
        # more than 1 realm, and the time bounds (time_start and time_end in
 | 
			
		||||
        # the queries).
 | 
			
		||||
@@ -328,7 +326,8 @@ class TestCountStats(AnalyticsTestCase):
 | 
			
		||||
        # This huddle should not show up anywhere
 | 
			
		||||
        self.create_huddle_with_recipient()
 | 
			
		||||
 | 
			
		||||
    def test_active_users_by_is_bot(self) -> None:
 | 
			
		||||
    def test_active_users_by_is_bot(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        stat = COUNT_STATS['active_users:is_bot:day']
 | 
			
		||||
        self.current_property = stat.property
 | 
			
		||||
 | 
			
		||||
@@ -346,21 +345,19 @@ class TestCountStats(AnalyticsTestCase):
 | 
			
		||||
                              [[2, 'true'], [1, 'false'],
 | 
			
		||||
                               [3, 'false', self.second_realm],
 | 
			
		||||
                               [1, 'false', self.no_message_realm]])
 | 
			
		||||
        self.assertTableState(InstallationCount,
 | 
			
		||||
                              ['value', 'subgroup'],
 | 
			
		||||
                              [[2, 'true'], [5, 'false']])
 | 
			
		||||
        self.assertTableState(InstallationCount, ['value', 'subgroup'], [[2, 'true'], [5, 'false']])
 | 
			
		||||
        self.assertTableState(UserCount, [], [])
 | 
			
		||||
        self.assertTableState(StreamCount, [], [])
 | 
			
		||||
 | 
			
		||||
    def test_messages_sent_by_is_bot(self) -> None:
 | 
			
		||||
    def test_messages_sent_by_is_bot(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        stat = COUNT_STATS['messages_sent:is_bot:hour']
 | 
			
		||||
        self.current_property = stat.property
 | 
			
		||||
 | 
			
		||||
        bot = self.create_user(is_bot=True)
 | 
			
		||||
        human1 = self.create_user()
 | 
			
		||||
        human2 = self.create_user()
 | 
			
		||||
        recipient_human1 = Recipient.objects.create(type_id=human1.id,
 | 
			
		||||
                                                    type=Recipient.PERSONAL)
 | 
			
		||||
        recipient_human1 = Recipient.objects.create(type_id=human1.id, type=Recipient.PERSONAL)
 | 
			
		||||
 | 
			
		||||
        recipient_stream = self.create_stream_with_recipient()[1]
 | 
			
		||||
        recipient_huddle = self.create_huddle_with_recipient()[1]
 | 
			
		||||
@@ -381,7 +378,8 @@ class TestCountStats(AnalyticsTestCase):
 | 
			
		||||
        self.assertTableState(InstallationCount, ['value', 'subgroup'], [[3, 'false'], [3, 'true']])
 | 
			
		||||
        self.assertTableState(StreamCount, [], [])
 | 
			
		||||
 | 
			
		||||
    def test_messages_sent_by_message_type(self) -> None:
 | 
			
		||||
    def test_messages_sent_by_message_type(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        stat = COUNT_STATS['messages_sent:message_type:day']
 | 
			
		||||
        self.current_property = stat.property
 | 
			
		||||
 | 
			
		||||
@@ -443,7 +441,8 @@ class TestCountStats(AnalyticsTestCase):
 | 
			
		||||
                               [2, 'huddle_message']])
 | 
			
		||||
        self.assertTableState(StreamCount, [], [])
 | 
			
		||||
 | 
			
		||||
    def test_messages_sent_to_recipients_with_same_id(self) -> None:
 | 
			
		||||
    def test_messages_sent_to_recipients_with_same_id(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        stat = COUNT_STATS['messages_sent:message_type:day']
 | 
			
		||||
        self.current_property = stat.property
 | 
			
		||||
 | 
			
		||||
@@ -462,7 +461,8 @@ class TestCountStats(AnalyticsTestCase):
 | 
			
		||||
        self.assertCountEquals(UserCount, 1, subgroup='huddle_message')
 | 
			
		||||
        self.assertCountEquals(UserCount, 1, subgroup='public_stream')
 | 
			
		||||
 | 
			
		||||
    def test_messages_sent_by_client(self) -> None:
 | 
			
		||||
    def test_messages_sent_by_client(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        stat = COUNT_STATS['messages_sent:client:day']
 | 
			
		||||
        self.current_property = stat.property
 | 
			
		||||
 | 
			
		||||
@@ -497,7 +497,8 @@ class TestCountStats(AnalyticsTestCase):
 | 
			
		||||
                              [[4, website_client_id], [3, client2_id]])
 | 
			
		||||
        self.assertTableState(StreamCount, [], [])
 | 
			
		||||
 | 
			
		||||
    def test_messages_sent_to_stream_by_is_bot(self) -> None:
 | 
			
		||||
    def test_messages_sent_to_stream_by_is_bot(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        stat = COUNT_STATS['messages_in_stream:is_bot:day']
 | 
			
		||||
        self.current_property = stat.property
 | 
			
		||||
 | 
			
		||||
@@ -534,13 +535,14 @@ class TestCountStats(AnalyticsTestCase):
 | 
			
		||||
        self.assertTableState(InstallationCount, ['value', 'subgroup'], [[5, 'false'], [2, 'true']])
 | 
			
		||||
        self.assertTableState(UserCount, [], [])
 | 
			
		||||
 | 
			
		||||
    def create_interval(self, user: UserProfile, start_offset: timedelta,
 | 
			
		||||
                        end_offset: timedelta) -> None:
 | 
			
		||||
    def create_interval(self, user, start_offset, end_offset):
 | 
			
		||||
        # type: (UserProfile, timedelta, timedelta) -> None
 | 
			
		||||
        UserActivityInterval.objects.create(
 | 
			
		||||
            user_profile=user, start=self.TIME_ZERO-start_offset,
 | 
			
		||||
            end=self.TIME_ZERO-end_offset)
 | 
			
		||||
 | 
			
		||||
    def test_15day_actives(self) -> None:
 | 
			
		||||
    def test_15day_actives(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        stat = COUNT_STATS['15day_actives::day']
 | 
			
		||||
        self.current_property = stat.property
 | 
			
		||||
 | 
			
		||||
@@ -583,7 +585,8 @@ class TestCountStats(AnalyticsTestCase):
 | 
			
		||||
        self.assertTableState(InstallationCount, ['value'], [[6]])
 | 
			
		||||
        self.assertTableState(StreamCount, [], [])
 | 
			
		||||
 | 
			
		||||
    def test_minutes_active(self) -> None:
 | 
			
		||||
    def test_minutes_active(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        stat = COUNT_STATS['minutes_active::day']
 | 
			
		||||
        self.current_property = stat.property
 | 
			
		||||
 | 
			
		||||
@@ -631,14 +634,16 @@ class TestDoAggregateToSummaryTable(AnalyticsTestCase):
 | 
			
		||||
    # feature important for keeping the size of the analytics tables small,
 | 
			
		||||
    # which is that if there is no relevant data in the table being
 | 
			
		||||
    # aggregated, the aggregation table doesn't get a row with value 0.
 | 
			
		||||
    def test_no_aggregated_zeros(self) -> None:
 | 
			
		||||
    def test_no_aggregated_zeros(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        stat = LoggingCountStat('test stat', UserCount, CountStat.HOUR)
 | 
			
		||||
        do_aggregate_to_summary_table(stat, self.TIME_ZERO)
 | 
			
		||||
        self.assertFalse(RealmCount.objects.exists())
 | 
			
		||||
        self.assertFalse(InstallationCount.objects.exists())
 | 
			
		||||
 | 
			
		||||
class TestDoIncrementLoggingStat(AnalyticsTestCase):
 | 
			
		||||
    def test_table_and_id_args(self) -> None:
 | 
			
		||||
    def test_table_and_id_args(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        # For realms, streams, and users, tests that the new rows are going to
 | 
			
		||||
        # the appropriate *Count table, and that using a different zerver_object
 | 
			
		||||
        # results in a new row being created
 | 
			
		||||
@@ -663,7 +668,8 @@ class TestDoIncrementLoggingStat(AnalyticsTestCase):
 | 
			
		||||
        do_increment_logging_stat(stream2, stat, None, self.TIME_ZERO)
 | 
			
		||||
        self.assertTableState(StreamCount, ['stream'], [[stream1], [stream2]])
 | 
			
		||||
 | 
			
		||||
    def test_frequency(self) -> None:
 | 
			
		||||
    def test_frequency(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        times = [self.TIME_ZERO - self.MINUTE*i for i in [0, 1, 61, 24*60+1]]
 | 
			
		||||
 | 
			
		||||
        stat = LoggingCountStat('day test', RealmCount, CountStat.DAY)
 | 
			
		||||
@@ -680,7 +686,8 @@ class TestDoIncrementLoggingStat(AnalyticsTestCase):
 | 
			
		||||
                               [1, 'hour test', self.TIME_LAST_HOUR],
 | 
			
		||||
                               [1, 'hour test', self.TIME_ZERO - self.DAY]])
 | 
			
		||||
 | 
			
		||||
    def test_get_or_create(self) -> None:
 | 
			
		||||
    def test_get_or_create(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        stat = LoggingCountStat('test', RealmCount, CountStat.HOUR)
 | 
			
		||||
        # All these should trigger the create part of get_or_create.
 | 
			
		||||
        # property is tested in test_frequency, and id_args are tested in test_id_args,
 | 
			
		||||
@@ -698,7 +705,8 @@ class TestDoIncrementLoggingStat(AnalyticsTestCase):
 | 
			
		||||
                              [[2, 'subgroup1', self.TIME_ZERO], [1, 'subgroup2', self.TIME_ZERO],
 | 
			
		||||
                              [1, 'subgroup1', self.TIME_LAST_HOUR]])
 | 
			
		||||
 | 
			
		||||
    def test_increment(self) -> None:
 | 
			
		||||
    def test_increment(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        stat = LoggingCountStat('test', RealmCount, CountStat.DAY)
 | 
			
		||||
        self.current_property = 'test'
 | 
			
		||||
        do_increment_logging_stat(self.default_realm, stat, None, self.TIME_ZERO, increment=-1)
 | 
			
		||||
@@ -709,7 +717,8 @@ class TestDoIncrementLoggingStat(AnalyticsTestCase):
 | 
			
		||||
        self.assertTableState(RealmCount, ['value'], [[3]])
 | 
			
		||||
 | 
			
		||||
class TestLoggingCountStats(AnalyticsTestCase):
 | 
			
		||||
    def test_aggregation(self) -> None:
 | 
			
		||||
    def test_aggregation(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        stat = LoggingCountStat('realm test', RealmCount, CountStat.DAY)
 | 
			
		||||
        do_increment_logging_stat(self.default_realm, stat, None, self.TIME_ZERO)
 | 
			
		||||
        process_count_stat(stat, self.TIME_ZERO)
 | 
			
		||||
@@ -731,7 +740,8 @@ class TestLoggingCountStats(AnalyticsTestCase):
 | 
			
		||||
        self.assertTableState(UserCount, ['property', 'value'], [['user test', 1]])
 | 
			
		||||
        self.assertTableState(StreamCount, ['property', 'value'], [['stream test', 1]])
 | 
			
		||||
 | 
			
		||||
    def test_active_users_log_by_is_bot(self) -> None:
 | 
			
		||||
    def test_active_users_log_by_is_bot(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        property = 'active_users_log:is_bot:day'
 | 
			
		||||
        user = do_create_user('email', 'password', self.default_realm, 'full_name', 'short_name')
 | 
			
		||||
        self.assertEqual(1, RealmCount.objects.filter(property=property, subgroup=False)
 | 
			
		||||
@@ -749,47 +759,9 @@ class TestLoggingCountStats(AnalyticsTestCase):
 | 
			
		||||
        self.assertEqual(1, RealmCount.objects.filter(property=property, subgroup=False)
 | 
			
		||||
                         .aggregate(Sum('value'))['value__sum'])
 | 
			
		||||
 | 
			
		||||
    def test_invites_sent(self) -> None:
 | 
			
		||||
        property = 'invites_sent::day'
 | 
			
		||||
 | 
			
		||||
        def assertInviteCountEquals(count: int) -> None:
 | 
			
		||||
            self.assertEqual(count, RealmCount.objects.filter(property=property, subgroup=None)
 | 
			
		||||
                             .aggregate(Sum('value'))['value__sum'])
 | 
			
		||||
 | 
			
		||||
        user = self.create_user(email='first@domain.tld')
 | 
			
		||||
        stream, _ = self.create_stream_with_recipient()
 | 
			
		||||
        do_invite_users(user, ['user1@domain.tld', 'user2@domain.tld'], [stream])
 | 
			
		||||
        assertInviteCountEquals(2)
 | 
			
		||||
 | 
			
		||||
        # We currently send emails when re-inviting users that haven't
 | 
			
		||||
        # turned into accounts, so count them towards the total
 | 
			
		||||
        do_invite_users(user, ['user1@domain.tld', 'user2@domain.tld'], [stream])
 | 
			
		||||
        assertInviteCountEquals(4)
 | 
			
		||||
 | 
			
		||||
        # Test mix of good and malformed invite emails
 | 
			
		||||
        try:
 | 
			
		||||
            do_invite_users(user, ['user3@domain.tld', 'malformed'], [stream])
 | 
			
		||||
        except InvitationError:
 | 
			
		||||
            pass
 | 
			
		||||
        assertInviteCountEquals(4)
 | 
			
		||||
 | 
			
		||||
        # Test inviting existing users
 | 
			
		||||
        try:
 | 
			
		||||
            do_invite_users(user, ['first@domain.tld', 'user4@domain.tld'], [stream])
 | 
			
		||||
        except InvitationError:
 | 
			
		||||
            pass
 | 
			
		||||
        assertInviteCountEquals(5)
 | 
			
		||||
 | 
			
		||||
        # Revoking invite should not give you credit
 | 
			
		||||
        do_revoke_user_invite(PreregistrationUser.objects.filter(realm=user.realm).first())
 | 
			
		||||
        assertInviteCountEquals(5)
 | 
			
		||||
 | 
			
		||||
        # Resending invite should cost you
 | 
			
		||||
        do_resend_user_invite_email(PreregistrationUser.objects.first())
 | 
			
		||||
        assertInviteCountEquals(6)
 | 
			
		||||
 | 
			
		||||
class TestDeleteStats(AnalyticsTestCase):
 | 
			
		||||
    def test_do_drop_all_analytics_tables(self) -> None:
 | 
			
		||||
    def test_do_drop_all_analytics_tables(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        user = self.create_user()
 | 
			
		||||
        stream = self.create_stream_with_recipient()[0]
 | 
			
		||||
        count_args = {'property': 'test', 'end_time': self.TIME_ZERO, 'value': 10}
 | 
			
		||||
@@ -809,7 +781,8 @@ class TestDeleteStats(AnalyticsTestCase):
 | 
			
		||||
        for table in list(analytics.models.values()):
 | 
			
		||||
            self.assertFalse(table.objects.exists())
 | 
			
		||||
 | 
			
		||||
    def test_do_drop_single_stat(self) -> None:
 | 
			
		||||
    def test_do_drop_single_stat(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        user = self.create_user()
 | 
			
		||||
        stream = self.create_stream_with_recipient()[0]
 | 
			
		||||
        count_args_to_delete = {'property': 'to_delete', 'end_time': self.TIME_ZERO, 'value': 10}
 | 
			
		||||
@@ -837,14 +810,15 @@ class TestDeleteStats(AnalyticsTestCase):
 | 
			
		||||
                self.assertTrue(table.objects.filter(property='to_save').exists())
 | 
			
		||||
 | 
			
		||||
class TestActiveUsersAudit(AnalyticsTestCase):
 | 
			
		||||
    def setUp(self) -> None:
 | 
			
		||||
        super().setUp()
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        super(TestActiveUsersAudit, self).setUp()
 | 
			
		||||
        self.user = self.create_user()
 | 
			
		||||
        self.stat = COUNT_STATS['active_users_audit:is_bot:day']
 | 
			
		||||
        self.current_property = self.stat.property
 | 
			
		||||
 | 
			
		||||
    def add_event(self, event_type: str, days_offset: float,
 | 
			
		||||
                  user: Optional[UserProfile]=None) -> None:
 | 
			
		||||
    def add_event(self, event_type, days_offset, user=None):
 | 
			
		||||
        # type: (str, float, Optional[UserProfile]) -> None
 | 
			
		||||
        hours_offset = int(24*days_offset)
 | 
			
		||||
        if user is None:
 | 
			
		||||
            user = self.user
 | 
			
		||||
@@ -852,25 +826,29 @@ class TestActiveUsersAudit(AnalyticsTestCase):
 | 
			
		||||
            realm=user.realm, modified_user=user, event_type=event_type,
 | 
			
		||||
            event_time=self.TIME_ZERO - hours_offset*self.HOUR)
 | 
			
		||||
 | 
			
		||||
    def test_user_deactivated_in_future(self) -> None:
 | 
			
		||||
    def test_user_deactivated_in_future(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        self.add_event('user_created', 1)
 | 
			
		||||
        self.add_event('user_deactivated', 0)
 | 
			
		||||
        do_fill_count_stat_at_hour(self.stat, self.TIME_ZERO)
 | 
			
		||||
        self.assertTableState(UserCount, ['subgroup'], [['false']])
 | 
			
		||||
 | 
			
		||||
    def test_user_reactivated_in_future(self) -> None:
 | 
			
		||||
    def test_user_reactivated_in_future(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        self.add_event('user_deactivated', 1)
 | 
			
		||||
        self.add_event('user_reactivated', 0)
 | 
			
		||||
        do_fill_count_stat_at_hour(self.stat, self.TIME_ZERO)
 | 
			
		||||
        self.assertTableState(UserCount, [], [])
 | 
			
		||||
 | 
			
		||||
    def test_user_active_then_deactivated_same_day(self) -> None:
 | 
			
		||||
    def test_user_active_then_deactivated_same_day(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        self.add_event('user_created', 1)
 | 
			
		||||
        self.add_event('user_deactivated', .5)
 | 
			
		||||
        do_fill_count_stat_at_hour(self.stat, self.TIME_ZERO)
 | 
			
		||||
        self.assertTableState(UserCount, [], [])
 | 
			
		||||
 | 
			
		||||
    def test_user_unactive_then_activated_same_day(self) -> None:
 | 
			
		||||
    def test_user_unactive_then_activated_same_day(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        self.add_event('user_deactivated', 1)
 | 
			
		||||
        self.add_event('user_reactivated', .5)
 | 
			
		||||
        do_fill_count_stat_at_hour(self.stat, self.TIME_ZERO)
 | 
			
		||||
@@ -878,20 +856,23 @@ class TestActiveUsersAudit(AnalyticsTestCase):
 | 
			
		||||
 | 
			
		||||
    # Arguably these next two tests are duplicates of the _in_future tests, but are
 | 
			
		||||
    # a guard against future refactorings where they may no longer be duplicates
 | 
			
		||||
    def test_user_active_then_deactivated_with_day_gap(self) -> None:
 | 
			
		||||
    def test_user_active_then_deactivated_with_day_gap(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        self.add_event('user_created', 2)
 | 
			
		||||
        self.add_event('user_deactivated', 1)
 | 
			
		||||
        process_count_stat(self.stat, self.TIME_ZERO)
 | 
			
		||||
        self.assertTableState(UserCount, ['subgroup', 'end_time'],
 | 
			
		||||
                              [['false', self.TIME_ZERO - self.DAY]])
 | 
			
		||||
 | 
			
		||||
    def test_user_deactivated_then_reactivated_with_day_gap(self) -> None:
 | 
			
		||||
    def test_user_deactivated_then_reactivated_with_day_gap(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        self.add_event('user_deactivated', 2)
 | 
			
		||||
        self.add_event('user_reactivated', 1)
 | 
			
		||||
        process_count_stat(self.stat, self.TIME_ZERO)
 | 
			
		||||
        self.assertTableState(UserCount, ['subgroup'], [['false']])
 | 
			
		||||
 | 
			
		||||
    def test_event_types(self) -> None:
 | 
			
		||||
    def test_event_types(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        self.add_event('user_created', 4)
 | 
			
		||||
        self.add_event('user_deactivated', 3)
 | 
			
		||||
        self.add_event('user_activated', 2)
 | 
			
		||||
@@ -903,7 +884,8 @@ class TestActiveUsersAudit(AnalyticsTestCase):
 | 
			
		||||
 | 
			
		||||
    # Also tests that aggregation to RealmCount and InstallationCount is
 | 
			
		||||
    # being done, and that we're storing the user correctly in UserCount
 | 
			
		||||
    def test_multiple_users_realms_and_bots(self) -> None:
 | 
			
		||||
    def test_multiple_users_realms_and_bots(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        user1 = self.create_user()
 | 
			
		||||
        user2 = self.create_user()
 | 
			
		||||
        second_realm = Realm.objects.create(string_id='moo', name='moo')
 | 
			
		||||
@@ -927,7 +909,8 @@ class TestActiveUsersAudit(AnalyticsTestCase):
 | 
			
		||||
    # do_fill_count_stat_at_hour. E.g. if one changes self.stat.frequency to
 | 
			
		||||
    # CountStat.HOUR from CountStat.DAY, this will fail, while many of the
 | 
			
		||||
    # tests above will not.
 | 
			
		||||
    def test_update_from_two_days_ago(self) -> None:
 | 
			
		||||
    def test_update_from_two_days_ago(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        self.add_event('user_created', 2)
 | 
			
		||||
        process_count_stat(self.stat, self.TIME_ZERO)
 | 
			
		||||
        self.assertTableState(UserCount, ['subgroup', 'end_time'],
 | 
			
		||||
@@ -936,27 +919,31 @@ class TestActiveUsersAudit(AnalyticsTestCase):
 | 
			
		||||
    # User with no relevant activity could happen e.g. for a system bot that
 | 
			
		||||
    # doesn't go through do_create_user. Mainly just want to make sure that
 | 
			
		||||
    # that situation doesn't throw an error.
 | 
			
		||||
    def test_empty_realm_or_user_with_no_relevant_activity(self) -> None:
 | 
			
		||||
    def test_empty_realm_or_user_with_no_relevant_activity(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        self.add_event('unrelated', 1)
 | 
			
		||||
        self.create_user()  # also test a user with no RealmAuditLog entries
 | 
			
		||||
        Realm.objects.create(string_id='moo', name='moo')
 | 
			
		||||
        do_fill_count_stat_at_hour(self.stat, self.TIME_ZERO)
 | 
			
		||||
        self.assertTableState(UserCount, [], [])
 | 
			
		||||
 | 
			
		||||
    def test_max_audit_entry_is_unrelated(self) -> None:
 | 
			
		||||
    def test_max_audit_entry_is_unrelated(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        self.add_event('user_created', 1)
 | 
			
		||||
        self.add_event('unrelated', .5)
 | 
			
		||||
        do_fill_count_stat_at_hour(self.stat, self.TIME_ZERO)
 | 
			
		||||
        self.assertTableState(UserCount, ['subgroup'], [['false']])
 | 
			
		||||
 | 
			
		||||
    # Simultaneous related audit entries should not be allowed, and so not testing for that.
 | 
			
		||||
    def test_simultaneous_unrelated_audit_entry(self) -> None:
 | 
			
		||||
    def test_simultaneous_unrelated_audit_entry(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        self.add_event('user_created', 1)
 | 
			
		||||
        self.add_event('unrelated', 1)
 | 
			
		||||
        do_fill_count_stat_at_hour(self.stat, self.TIME_ZERO)
 | 
			
		||||
        self.assertTableState(UserCount, ['subgroup'], [['false']])
 | 
			
		||||
 | 
			
		||||
    def test_simultaneous_max_audit_entries_of_different_users(self) -> None:
 | 
			
		||||
    def test_simultaneous_max_audit_entries_of_different_users(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        user1 = self.create_user()
 | 
			
		||||
        user2 = self.create_user()
 | 
			
		||||
        user3 = self.create_user()
 | 
			
		||||
@@ -968,7 +955,8 @@ class TestActiveUsersAudit(AnalyticsTestCase):
 | 
			
		||||
        self.assertTableState(UserCount, ['user', 'subgroup'],
 | 
			
		||||
                              [[user1, 'false'], [user2, 'false']])
 | 
			
		||||
 | 
			
		||||
    def test_end_to_end_with_actions_dot_py(self) -> None:
 | 
			
		||||
    def test_end_to_end_with_actions_dot_py(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        user1 = do_create_user('email1', 'password', self.default_realm, 'full_name', 'short_name')
 | 
			
		||||
        user2 = do_create_user('email2', 'password', self.default_realm, 'full_name', 'short_name')
 | 
			
		||||
        user3 = do_create_user('email3', 'password', self.default_realm, 'full_name', 'short_name')
 | 
			
		||||
@@ -985,26 +973,30 @@ class TestActiveUsersAudit(AnalyticsTestCase):
 | 
			
		||||
        self.assertFalse(UserCount.objects.filter(user=user2).exists())
 | 
			
		||||
 | 
			
		||||
class TestRealmActiveHumans(AnalyticsTestCase):
 | 
			
		||||
    def setUp(self) -> None:
 | 
			
		||||
        super().setUp()
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        super(TestRealmActiveHumans, self).setUp()
 | 
			
		||||
        self.stat = COUNT_STATS['realm_active_humans::day']
 | 
			
		||||
        self.current_property = self.stat.property
 | 
			
		||||
 | 
			
		||||
    def mark_audit_active(self, user: UserProfile, end_time: Optional[datetime]=None) -> None:
 | 
			
		||||
    def mark_audit_active(self, user, end_time=None):
 | 
			
		||||
        # type: (UserProfile, Optional[datetime]) -> None
 | 
			
		||||
        if end_time is None:
 | 
			
		||||
            end_time = self.TIME_ZERO
 | 
			
		||||
        UserCount.objects.create(
 | 
			
		||||
            user=user, realm=user.realm, property='active_users_audit:is_bot:day',
 | 
			
		||||
            subgroup=ujson.dumps(user.is_bot), end_time=end_time, value=1)
 | 
			
		||||
 | 
			
		||||
    def mark_15day_active(self, user: UserProfile, end_time: Optional[datetime]=None) -> None:
 | 
			
		||||
    def mark_15day_active(self, user, end_time=None):
 | 
			
		||||
        # type: (UserProfile, Optional[datetime]) -> None
 | 
			
		||||
        if end_time is None:
 | 
			
		||||
            end_time = self.TIME_ZERO
 | 
			
		||||
        UserCount.objects.create(
 | 
			
		||||
            user=user, realm=user.realm, property='15day_actives::day',
 | 
			
		||||
            end_time=end_time, value=1)
 | 
			
		||||
 | 
			
		||||
    def test_basic_boolean_logic(self) -> None:
 | 
			
		||||
    def test_basic_boolean_logic(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        user = self.create_user()
 | 
			
		||||
        self.mark_audit_active(user, end_time=self.TIME_ZERO - self.DAY)
 | 
			
		||||
        self.mark_15day_active(user, end_time=self.TIME_ZERO)
 | 
			
		||||
@@ -1015,14 +1007,16 @@ class TestRealmActiveHumans(AnalyticsTestCase):
 | 
			
		||||
            do_fill_count_stat_at_hour(self.stat, self.TIME_ZERO + i*self.DAY)
 | 
			
		||||
        self.assertTableState(RealmCount, ['value', 'end_time'], [[1, self.TIME_ZERO + self.DAY]])
 | 
			
		||||
 | 
			
		||||
    def test_bots_not_counted(self) -> None:
 | 
			
		||||
    def test_bots_not_counted(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        bot = self.create_user(is_bot=True)
 | 
			
		||||
        self.mark_audit_active(bot)
 | 
			
		||||
        self.mark_15day_active(bot)
 | 
			
		||||
        do_fill_count_stat_at_hour(self.stat, self.TIME_ZERO)
 | 
			
		||||
        self.assertTableState(RealmCount, [], [])
 | 
			
		||||
 | 
			
		||||
    def test_multiple_users_realms_and_times(self) -> None:
 | 
			
		||||
    def test_multiple_users_realms_and_times(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        user1 = self.create_user()
 | 
			
		||||
        user2 = self.create_user()
 | 
			
		||||
        second_realm = Realm.objects.create(string_id='second', name='second')
 | 
			
		||||
@@ -1062,7 +1056,8 @@ class TestRealmActiveHumans(AnalyticsTestCase):
 | 
			
		||||
                               [1, self.default_realm, self.TIME_ZERO - self.DAY],
 | 
			
		||||
                               [2, second_realm, self.TIME_ZERO - self.DAY]])
 | 
			
		||||
 | 
			
		||||
    def test_end_to_end(self) -> None:
 | 
			
		||||
    def test_end_to_end(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        user1 = do_create_user('email1', 'password', self.default_realm, 'full_name', 'short_name')
 | 
			
		||||
        user2 = do_create_user('email2', 'password', self.default_realm, 'full_name', 'short_name')
 | 
			
		||||
        do_create_user('email3', 'password', self.default_realm, 'full_name', 'short_name')
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,12 @@
 | 
			
		||||
from zerver.lib.test_classes import ZulipTestCase
 | 
			
		||||
 | 
			
		||||
from analytics.lib.counts import CountStat
 | 
			
		||||
from analytics.lib.fixtures import generate_time_series_data
 | 
			
		||||
from zerver.lib.test_classes import ZulipTestCase
 | 
			
		||||
 | 
			
		||||
# A very light test suite; the code being tested is not run in production.
 | 
			
		||||
class TestFixtures(ZulipTestCase):
 | 
			
		||||
    def test_deterministic_settings(self) -> None:
 | 
			
		||||
    def test_deterministic_settings(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        # test basic business_hour / non_business_hour calculation
 | 
			
		||||
        # test we get an array of the right length with frequency=CountStat.DAY
 | 
			
		||||
        data = generate_time_series_data(
 | 
			
		||||
 
 | 
			
		||||
@@ -1,22 +1,25 @@
 | 
			
		||||
from datetime import datetime, timedelta
 | 
			
		||||
from typing import Dict, List, Optional
 | 
			
		||||
 | 
			
		||||
import mock
 | 
			
		||||
from django.utils.timezone import utc
 | 
			
		||||
 | 
			
		||||
from analytics.lib.counts import COUNT_STATS, CountStat
 | 
			
		||||
from analytics.lib.time_utils import time_range
 | 
			
		||||
from analytics.models import FillState, \
 | 
			
		||||
    RealmCount, UserCount, last_successful_fill
 | 
			
		||||
from analytics.views import get_chart_data, rewrite_client_arrays, \
 | 
			
		||||
    sort_by_totals, sort_client_labels, stats
 | 
			
		||||
from django.utils.timezone import get_fixed_timezone, utc
 | 
			
		||||
from zerver.lib.test_classes import ZulipTestCase
 | 
			
		||||
from zerver.lib.timestamp import ceiling_to_day, \
 | 
			
		||||
    ceiling_to_hour, datetime_to_timestamp
 | 
			
		||||
from zerver.models import Client, get_realm
 | 
			
		||||
from zerver.lib.timestamp import ceiling_to_hour, ceiling_to_day, \
 | 
			
		||||
    datetime_to_timestamp
 | 
			
		||||
from zerver.models import Realm, UserProfile, Client, get_realm
 | 
			
		||||
 | 
			
		||||
from analytics.lib.counts import CountStat, COUNT_STATS
 | 
			
		||||
from analytics.lib.time_utils import time_range
 | 
			
		||||
from analytics.models import RealmCount, UserCount, BaseCount, \
 | 
			
		||||
    FillState, last_successful_fill
 | 
			
		||||
from analytics.views import stats, get_chart_data, sort_by_totals, \
 | 
			
		||||
    sort_client_labels, rewrite_client_arrays
 | 
			
		||||
 | 
			
		||||
from datetime import datetime, timedelta
 | 
			
		||||
import mock
 | 
			
		||||
import ujson
 | 
			
		||||
 | 
			
		||||
from typing import List, Dict, Optional
 | 
			
		||||
 | 
			
		||||
class TestStatsEndpoint(ZulipTestCase):
 | 
			
		||||
    def test_stats(self) -> None:
 | 
			
		||||
    def test_stats(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        self.user = self.example_user('hamlet')
 | 
			
		||||
        self.login(self.user.email)
 | 
			
		||||
        result = self.client_get('/stats')
 | 
			
		||||
@@ -25,7 +28,8 @@ class TestStatsEndpoint(ZulipTestCase):
 | 
			
		||||
        self.assert_in_response("Zulip analytics for", result)
 | 
			
		||||
 | 
			
		||||
class TestGetChartData(ZulipTestCase):
 | 
			
		||||
    def setUp(self) -> None:
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        self.realm = get_realm('zulip')
 | 
			
		||||
        self.user = self.example_user('hamlet')
 | 
			
		||||
        self.login(self.user.email)
 | 
			
		||||
@@ -34,11 +38,12 @@ class TestGetChartData(ZulipTestCase):
 | 
			
		||||
        self.end_times_day = [ceiling_to_day(self.realm.date_created) + timedelta(days=i)
 | 
			
		||||
                              for i in range(4)]
 | 
			
		||||
 | 
			
		||||
    def data(self, i: int) -> List[int]:
 | 
			
		||||
    def data(self, i):
 | 
			
		||||
        # type: (int) -> List[int]
 | 
			
		||||
        return [0, 0, i, 0]
 | 
			
		||||
 | 
			
		||||
    def insert_data(self, stat: CountStat, realm_subgroups: List[Optional[str]],
 | 
			
		||||
                    user_subgroups: List[str]) -> None:
 | 
			
		||||
    def insert_data(self, stat, realm_subgroups, user_subgroups):
 | 
			
		||||
        # type: (CountStat, List[Optional[str]], List[str]) -> None
 | 
			
		||||
        if stat.frequency == CountStat.HOUR:
 | 
			
		||||
            insert_time = self.end_times_hour[2]
 | 
			
		||||
            fill_time = self.end_times_hour[-1]
 | 
			
		||||
@@ -56,7 +61,8 @@ class TestGetChartData(ZulipTestCase):
 | 
			
		||||
            for i, subgroup in enumerate(user_subgroups)])
 | 
			
		||||
        FillState.objects.create(property=stat.property, end_time=fill_time, state=FillState.DONE)
 | 
			
		||||
 | 
			
		||||
    def test_number_of_humans(self) -> None:
 | 
			
		||||
    def test_number_of_humans(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        stat = COUNT_STATS['realm_active_humans::day']
 | 
			
		||||
        self.insert_data(stat, [None], [])
 | 
			
		||||
        result = self.client_get('/json/analytics/chart_data',
 | 
			
		||||
@@ -72,7 +78,8 @@ class TestGetChartData(ZulipTestCase):
 | 
			
		||||
            'result': 'success',
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    def test_messages_sent_over_time(self) -> None:
 | 
			
		||||
    def test_messages_sent_over_time(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        stat = COUNT_STATS['messages_sent:is_bot:hour']
 | 
			
		||||
        self.insert_data(stat, ['true', 'false'], ['false'])
 | 
			
		||||
        result = self.client_get('/json/analytics/chart_data',
 | 
			
		||||
@@ -89,7 +96,8 @@ class TestGetChartData(ZulipTestCase):
 | 
			
		||||
            'result': 'success',
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    def test_messages_sent_by_message_type(self) -> None:
 | 
			
		||||
    def test_messages_sent_by_message_type(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        stat = COUNT_STATS['messages_sent:message_type:day']
 | 
			
		||||
        self.insert_data(stat, ['public_stream', 'private_message'],
 | 
			
		||||
                         ['public_stream', 'private_stream'])
 | 
			
		||||
@@ -109,7 +117,8 @@ class TestGetChartData(ZulipTestCase):
 | 
			
		||||
            'result': 'success',
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    def test_messages_sent_by_client(self) -> None:
 | 
			
		||||
    def test_messages_sent_by_client(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        stat = COUNT_STATS['messages_sent:client:day']
 | 
			
		||||
        client1 = Client.objects.create(name='client 1')
 | 
			
		||||
        client2 = Client.objects.create(name='client 2')
 | 
			
		||||
@@ -132,10 +141,10 @@ class TestGetChartData(ZulipTestCase):
 | 
			
		||||
            'result': 'success',
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    def test_include_empty_subgroups(self) -> None:
 | 
			
		||||
    def test_include_empty_subgroups(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        FillState.objects.create(
 | 
			
		||||
            property='realm_active_humans::day', end_time=self.end_times_day[0],
 | 
			
		||||
            state=FillState.DONE)
 | 
			
		||||
            property='realm_active_humans::day', end_time=self.end_times_day[0], state=FillState.DONE)
 | 
			
		||||
        result = self.client_get('/json/analytics/chart_data',
 | 
			
		||||
                                 {'chart_name': 'number_of_humans'})
 | 
			
		||||
        self.assert_json_success(result)
 | 
			
		||||
@@ -144,8 +153,7 @@ class TestGetChartData(ZulipTestCase):
 | 
			
		||||
        self.assertFalse('user' in data)
 | 
			
		||||
 | 
			
		||||
        FillState.objects.create(
 | 
			
		||||
            property='messages_sent:is_bot:hour', end_time=self.end_times_hour[0],
 | 
			
		||||
            state=FillState.DONE)
 | 
			
		||||
            property='messages_sent:is_bot:hour', end_time=self.end_times_hour[0], state=FillState.DONE)
 | 
			
		||||
        result = self.client_get('/json/analytics/chart_data',
 | 
			
		||||
                                 {'chart_name': 'messages_sent_over_time'})
 | 
			
		||||
        self.assert_json_success(result)
 | 
			
		||||
@@ -154,22 +162,18 @@ class TestGetChartData(ZulipTestCase):
 | 
			
		||||
        self.assertEqual(data['user'], {'human': [0], 'bot': [0]})
 | 
			
		||||
 | 
			
		||||
        FillState.objects.create(
 | 
			
		||||
            property='messages_sent:message_type:day', end_time=self.end_times_day[0],
 | 
			
		||||
            state=FillState.DONE)
 | 
			
		||||
            property='messages_sent:message_type:day', end_time=self.end_times_day[0], state=FillState.DONE)
 | 
			
		||||
        result = self.client_get('/json/analytics/chart_data',
 | 
			
		||||
                                 {'chart_name': 'messages_sent_by_message_type'})
 | 
			
		||||
        self.assert_json_success(result)
 | 
			
		||||
        data = result.json()
 | 
			
		||||
        self.assertEqual(data['realm'], {
 | 
			
		||||
            'Public streams': [0], 'Private streams': [0],
 | 
			
		||||
            'Private messages': [0], 'Group private messages': [0]})
 | 
			
		||||
            'Public streams': [0], 'Private streams': [0], 'Private messages': [0], 'Group private messages': [0]})
 | 
			
		||||
        self.assertEqual(data['user'], {
 | 
			
		||||
            'Public streams': [0], 'Private streams': [0],
 | 
			
		||||
            'Private messages': [0], 'Group private messages': [0]})
 | 
			
		||||
            'Public streams': [0], 'Private streams': [0], 'Private messages': [0], 'Group private messages': [0]})
 | 
			
		||||
 | 
			
		||||
        FillState.objects.create(
 | 
			
		||||
            property='messages_sent:client:day', end_time=self.end_times_day[0],
 | 
			
		||||
            state=FillState.DONE)
 | 
			
		||||
            property='messages_sent:client:day', end_time=self.end_times_day[0], state=FillState.DONE)
 | 
			
		||||
        result = self.client_get('/json/analytics/chart_data',
 | 
			
		||||
                                 {'chart_name': 'messages_sent_by_client'})
 | 
			
		||||
        self.assert_json_success(result)
 | 
			
		||||
@@ -177,7 +181,8 @@ class TestGetChartData(ZulipTestCase):
 | 
			
		||||
        self.assertEqual(data['realm'], {})
 | 
			
		||||
        self.assertEqual(data['user'], {})
 | 
			
		||||
 | 
			
		||||
    def test_start_and_end(self) -> None:
 | 
			
		||||
    def test_start_and_end(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        stat = COUNT_STATS['realm_active_humans::day']
 | 
			
		||||
        self.insert_data(stat, [None], [])
 | 
			
		||||
        end_time_timestamps = [datetime_to_timestamp(dt) for dt in self.end_times_day]
 | 
			
		||||
@@ -199,7 +204,8 @@ class TestGetChartData(ZulipTestCase):
 | 
			
		||||
                                  'end': end_time_timestamps[1]})
 | 
			
		||||
        self.assert_json_error_contains(result, 'Start time is later than')
 | 
			
		||||
 | 
			
		||||
    def test_min_length(self) -> None:
 | 
			
		||||
    def test_min_length(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        stat = COUNT_STATS['realm_active_humans::day']
 | 
			
		||||
        self.insert_data(stat, [None], [])
 | 
			
		||||
        # test min_length is too short to change anything
 | 
			
		||||
@@ -220,12 +226,14 @@ class TestGetChartData(ZulipTestCase):
 | 
			
		||||
        self.assertEqual(data['end_times'], [datetime_to_timestamp(dt) for dt in end_times])
 | 
			
		||||
        self.assertEqual(data['realm'], {'human': [0]+self.data(100)})
 | 
			
		||||
 | 
			
		||||
    def test_non_existent_chart(self) -> None:
 | 
			
		||||
    def test_non_existent_chart(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        result = self.client_get('/json/analytics/chart_data',
 | 
			
		||||
                                 {'chart_name': 'does_not_exist'})
 | 
			
		||||
        self.assert_json_error_contains(result, 'Unknown chart name')
 | 
			
		||||
 | 
			
		||||
    def test_analytics_not_running(self) -> None:
 | 
			
		||||
    def test_analytics_not_running(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        # try to get data for a valid chart, but before we've put anything in the database
 | 
			
		||||
        # (e.g. before update_analytics_counts has been run)
 | 
			
		||||
        with mock.patch('logging.warning'):
 | 
			
		||||
@@ -236,7 +244,8 @@ class TestGetChartData(ZulipTestCase):
 | 
			
		||||
class TestGetChartDataHelpers(ZulipTestCase):
 | 
			
		||||
    # last_successful_fill is in analytics/models.py, but get_chart_data is
 | 
			
		||||
    # the only function that uses it at the moment
 | 
			
		||||
    def test_last_successful_fill(self) -> None:
 | 
			
		||||
    def test_last_successful_fill(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        self.assertIsNone(last_successful_fill('non-existant'))
 | 
			
		||||
        a_time = datetime(2016, 3, 14, 19).replace(tzinfo=utc)
 | 
			
		||||
        one_hour_before = datetime(2016, 3, 14, 18).replace(tzinfo=utc)
 | 
			
		||||
@@ -247,18 +256,21 @@ class TestGetChartDataHelpers(ZulipTestCase):
 | 
			
		||||
        fillstate.save()
 | 
			
		||||
        self.assertEqual(last_successful_fill('property'), one_hour_before)
 | 
			
		||||
 | 
			
		||||
    def test_sort_by_totals(self) -> None:
 | 
			
		||||
    def test_sort_by_totals(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        empty = []  # type: List[int]
 | 
			
		||||
        value_arrays = {'c': [0, 1], 'a': [9], 'b': [1, 1, 1], 'd': empty}
 | 
			
		||||
        self.assertEqual(sort_by_totals(value_arrays), ['a', 'b', 'c', 'd'])
 | 
			
		||||
 | 
			
		||||
    def test_sort_client_labels(self) -> None:
 | 
			
		||||
    def test_sort_client_labels(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        data = {'realm': {'a': [16], 'c': [15], 'b': [14], 'e': [13], 'd': [12], 'h': [11]},
 | 
			
		||||
                'user': {'a': [6], 'b': [5], 'd': [4], 'e': [3], 'f': [2], 'g': [1]}}
 | 
			
		||||
        self.assertEqual(sort_client_labels(data), ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'])
 | 
			
		||||
 | 
			
		||||
class TestTimeRange(ZulipTestCase):
 | 
			
		||||
    def test_time_range(self) -> None:
 | 
			
		||||
    def test_time_range(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        HOUR = timedelta(hours=1)
 | 
			
		||||
        DAY = timedelta(days=1)
 | 
			
		||||
 | 
			
		||||
@@ -284,7 +296,8 @@ class TestTimeRange(ZulipTestCase):
 | 
			
		||||
                         [floor_day-2*DAY, floor_day-DAY, floor_day, floor_day+DAY])
 | 
			
		||||
 | 
			
		||||
class TestMapArrays(ZulipTestCase):
 | 
			
		||||
    def test_map_arrays(self) -> None:
 | 
			
		||||
    def test_map_arrays(self):
 | 
			
		||||
        # type: () -> None
 | 
			
		||||
        a = {'desktop app 1.0': [1, 2, 3],
 | 
			
		||||
             'desktop app 2.0': [10, 12, 13],
 | 
			
		||||
             'desktop app 3.0': [21, 22, 23],
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
from django.conf.urls import include, url
 | 
			
		||||
from django.conf.urls import url, include
 | 
			
		||||
from zerver.lib.rest import rest_dispatch
 | 
			
		||||
 | 
			
		||||
import analytics.views
 | 
			
		||||
from zerver.lib.rest import rest_dispatch
 | 
			
		||||
 | 
			
		||||
i18n_urlpatterns = [
 | 
			
		||||
    # Server admin (user_profile.is_staff) visible stats pages
 | 
			
		||||
 
 | 
			
		||||
@@ -1,52 +1,56 @@
 | 
			
		||||
 | 
			
		||||
import itertools
 | 
			
		||||
import json
 | 
			
		||||
import logging
 | 
			
		||||
import re
 | 
			
		||||
import time
 | 
			
		||||
from collections import defaultdict
 | 
			
		||||
from datetime import datetime, timedelta
 | 
			
		||||
from typing import Any, Callable, Dict, List, \
 | 
			
		||||
    Optional, Set, Text, Tuple, Type, Union
 | 
			
		||||
 | 
			
		||||
import pytz
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
from django.core import urlresolvers
 | 
			
		||||
from django.db import connection
 | 
			
		||||
from django.db.models import Sum
 | 
			
		||||
from django.db.models.query import QuerySet
 | 
			
		||||
from django.http import HttpRequest, HttpResponse, HttpResponseNotFound
 | 
			
		||||
from django.shortcuts import render
 | 
			
		||||
from django.http import HttpResponseNotFound, HttpRequest, HttpResponse
 | 
			
		||||
from django.template import RequestContext, loader
 | 
			
		||||
from django.utils.timezone import now as timezone_now, utc as timezone_utc
 | 
			
		||||
from django.utils.timezone import now as timezone_now
 | 
			
		||||
from django.utils.translation import ugettext as _
 | 
			
		||||
from django.shortcuts import render
 | 
			
		||||
from jinja2 import Markup as mark_safe
 | 
			
		||||
 | 
			
		||||
from analytics.lib.counts import COUNT_STATS, CountStat, process_count_stat
 | 
			
		||||
from analytics.lib.counts import CountStat, process_count_stat, COUNT_STATS
 | 
			
		||||
from analytics.lib.time_utils import time_range
 | 
			
		||||
from analytics.models import BaseCount, InstallationCount, \
 | 
			
		||||
    RealmCount, StreamCount, UserCount, last_successful_fill
 | 
			
		||||
from zerver.decorator import require_server_admin, \
 | 
			
		||||
    to_non_negative_int, to_utc_datetime, zulip_login_required
 | 
			
		||||
from zerver.lib.exceptions import JsonableError
 | 
			
		||||
from zerver.lib.request import REQ, has_request_variables
 | 
			
		||||
from analytics.models import BaseCount, InstallationCount, RealmCount, \
 | 
			
		||||
    UserCount, StreamCount, last_successful_fill
 | 
			
		||||
 | 
			
		||||
from zerver.decorator import has_request_variables, REQ, require_server_admin, \
 | 
			
		||||
    zulip_login_required, to_non_negative_int, to_utc_datetime
 | 
			
		||||
from zerver.lib.request import JsonableError
 | 
			
		||||
from zerver.lib.response import json_success
 | 
			
		||||
from zerver.lib.timestamp import ceiling_to_day, \
 | 
			
		||||
    ceiling_to_hour, convert_to_UTC, timestamp_to_datetime
 | 
			
		||||
from zerver.models import Client, get_realm, Realm, \
 | 
			
		||||
    UserActivity, UserActivityInterval, UserProfile
 | 
			
		||||
from zerver.lib.timestamp import ceiling_to_hour, ceiling_to_day, \
 | 
			
		||||
    timestamp_to_datetime, convert_to_UTC
 | 
			
		||||
from zerver.models import Realm, UserProfile, UserActivity, \
 | 
			
		||||
    UserActivityInterval, Client
 | 
			
		||||
 | 
			
		||||
from collections import defaultdict
 | 
			
		||||
from datetime import datetime, timedelta
 | 
			
		||||
import itertools
 | 
			
		||||
import json
 | 
			
		||||
import logging
 | 
			
		||||
import pytz
 | 
			
		||||
import re
 | 
			
		||||
import time
 | 
			
		||||
 | 
			
		||||
from six.moves import filter, map, range, zip
 | 
			
		||||
from typing import Any, Callable, Dict, List, Optional, Set, Text, \
 | 
			
		||||
    Tuple, Type, Union
 | 
			
		||||
 | 
			
		||||
@zulip_login_required
 | 
			
		||||
def stats(request: HttpRequest) -> HttpResponse:
 | 
			
		||||
def stats(request):
 | 
			
		||||
    # type: (HttpRequest) -> HttpResponse
 | 
			
		||||
    return render(request,
 | 
			
		||||
                  'analytics/stats.html',
 | 
			
		||||
                  context=dict(realm_name = request.user.realm.name))
 | 
			
		||||
 | 
			
		||||
@has_request_variables
 | 
			
		||||
def get_chart_data(request: HttpRequest, user_profile: UserProfile, chart_name: Text=REQ(),
 | 
			
		||||
                   min_length: Optional[int]=REQ(converter=to_non_negative_int, default=None),
 | 
			
		||||
                   start: Optional[datetime]=REQ(converter=to_utc_datetime, default=None),
 | 
			
		||||
                   end: Optional[datetime]=REQ(converter=to_utc_datetime, default=None)) -> HttpResponse:
 | 
			
		||||
def get_chart_data(request, user_profile, chart_name=REQ(),
 | 
			
		||||
                   min_length=REQ(converter=to_non_negative_int, default=None),
 | 
			
		||||
                   start=REQ(converter=to_utc_datetime, default=None),
 | 
			
		||||
                   end=REQ(converter=to_utc_datetime, default=None)):
 | 
			
		||||
    # type: (HttpRequest, UserProfile, Text, Optional[int], Optional[datetime], Optional[datetime]) -> HttpResponse
 | 
			
		||||
    if chart_name == 'number_of_humans':
 | 
			
		||||
        stat = COUNT_STATS['realm_active_humans::day']
 | 
			
		||||
        tables = [RealmCount]
 | 
			
		||||
@@ -115,7 +119,8 @@ def get_chart_data(request: HttpRequest, user_profile: UserProfile, chart_name:
 | 
			
		||||
        data['display_order'] = None
 | 
			
		||||
    return json_success(data=data)
 | 
			
		||||
 | 
			
		||||
def sort_by_totals(value_arrays: Dict[str, List[int]]) -> List[str]:
 | 
			
		||||
def sort_by_totals(value_arrays):
 | 
			
		||||
    # type: (Dict[str, List[int]]) -> List[str]
 | 
			
		||||
    totals = [(sum(values), label) for label, values in value_arrays.items()]
 | 
			
		||||
    totals.sort(reverse=True)
 | 
			
		||||
    return [label for total, label in totals]
 | 
			
		||||
@@ -126,7 +131,8 @@ def sort_by_totals(value_arrays: Dict[str, List[int]]) -> List[str]:
 | 
			
		||||
# understanding the realm's traffic and the user's traffic. This function
 | 
			
		||||
# tries to rank the clients so that taking the first N elements of the
 | 
			
		||||
# sorted list has a reasonable chance of doing so.
 | 
			
		||||
def sort_client_labels(data: Dict[str, Dict[str, List[int]]]) -> List[str]:
 | 
			
		||||
def sort_client_labels(data):
 | 
			
		||||
    # type: (Dict[str, Dict[str, List[int]]]) -> List[str]
 | 
			
		||||
    realm_order = sort_by_totals(data['realm'])
 | 
			
		||||
    user_order = sort_by_totals(data['user'])
 | 
			
		||||
    label_sort_values = {}  # type: Dict[str, float]
 | 
			
		||||
@@ -137,7 +143,8 @@ def sort_client_labels(data: Dict[str, Dict[str, List[int]]]) -> List[str]:
 | 
			
		||||
    return [label for label, sort_value in sorted(label_sort_values.items(),
 | 
			
		||||
                                                  key=lambda x: x[1])]
 | 
			
		||||
 | 
			
		||||
def table_filtered_to_id(table: Type[BaseCount], key_id: int) -> QuerySet:
 | 
			
		||||
def table_filtered_to_id(table, key_id):
 | 
			
		||||
    # type: (Type[BaseCount], int) -> QuerySet
 | 
			
		||||
    if table == RealmCount:
 | 
			
		||||
        return RealmCount.objects.filter(realm_id=key_id)
 | 
			
		||||
    elif table == UserCount:
 | 
			
		||||
@@ -149,7 +156,8 @@ def table_filtered_to_id(table: Type[BaseCount], key_id: int) -> QuerySet:
 | 
			
		||||
    else:
 | 
			
		||||
        raise AssertionError("Unknown table: %s" % (table,))
 | 
			
		||||
 | 
			
		||||
def client_label_map(name: str) -> str:
 | 
			
		||||
def client_label_map(name):
 | 
			
		||||
    # type: (str) -> str
 | 
			
		||||
    if name == "website":
 | 
			
		||||
        return "Website"
 | 
			
		||||
    if name.startswith("desktop app"):
 | 
			
		||||
@@ -168,7 +176,8 @@ def client_label_map(name: str) -> str:
 | 
			
		||||
        return name[len("Zulip"):-len("Webhook")] + " webhook"
 | 
			
		||||
    return name
 | 
			
		||||
 | 
			
		||||
def rewrite_client_arrays(value_arrays: Dict[str, List[int]]) -> Dict[str, List[int]]:
 | 
			
		||||
def rewrite_client_arrays(value_arrays):
 | 
			
		||||
    # type: (Dict[str, List[int]]) -> Dict[str, List[int]]
 | 
			
		||||
    mapped_arrays = {}  # type: Dict[str, List[int]]
 | 
			
		||||
    for label, array in value_arrays.items():
 | 
			
		||||
        mapped_label = client_label_map(label)
 | 
			
		||||
@@ -179,12 +188,8 @@ def rewrite_client_arrays(value_arrays: Dict[str, List[int]]) -> Dict[str, List[
 | 
			
		||||
            mapped_arrays[mapped_label] = [value_arrays[label][i] for i in range(0, len(array))]
 | 
			
		||||
    return mapped_arrays
 | 
			
		||||
 | 
			
		||||
def get_time_series_by_subgroup(stat: CountStat,
 | 
			
		||||
                                table: Type[BaseCount],
 | 
			
		||||
                                key_id: int,
 | 
			
		||||
                                end_times: List[datetime],
 | 
			
		||||
                                subgroup_to_label: Dict[Optional[str], str],
 | 
			
		||||
                                include_empty_subgroups: bool) -> Dict[str, List[int]]:
 | 
			
		||||
def get_time_series_by_subgroup(stat, table, key_id, end_times, subgroup_to_label, include_empty_subgroups):
 | 
			
		||||
    # type: (CountStat, Type[BaseCount], int, List[datetime], Dict[Optional[str], str], bool) -> Dict[str, List[int]]
 | 
			
		||||
    queryset = table_filtered_to_id(table, key_id).filter(property=stat.property) \
 | 
			
		||||
                                                  .values_list('subgroup', 'end_time', 'value')
 | 
			
		||||
    value_dicts = defaultdict(lambda: defaultdict(int))  # type: Dict[Optional[str], Dict[datetime, int]]
 | 
			
		||||
@@ -205,10 +210,12 @@ def get_time_series_by_subgroup(stat: CountStat,
 | 
			
		||||
 | 
			
		||||
eastern_tz = pytz.timezone('US/Eastern')
 | 
			
		||||
 | 
			
		||||
def make_table(title: str, cols: List[str], rows: List[Any], has_row_class: bool=False) -> str:
 | 
			
		||||
def make_table(title, cols, rows, has_row_class=False):
 | 
			
		||||
    # type: (str, List[str], List[Any], bool) -> str
 | 
			
		||||
 | 
			
		||||
    if not has_row_class:
 | 
			
		||||
        def fix_row(row: Any) -> Dict[str, Any]:
 | 
			
		||||
        def fix_row(row):
 | 
			
		||||
            # type: (Any) -> Dict[str, Any]
 | 
			
		||||
            return dict(cells=row, row_class=None)
 | 
			
		||||
        rows = list(map(fix_row, rows))
 | 
			
		||||
 | 
			
		||||
@@ -221,7 +228,8 @@ def make_table(title: str, cols: List[str], rows: List[Any], has_row_class: bool
 | 
			
		||||
 | 
			
		||||
    return content
 | 
			
		||||
 | 
			
		||||
def dictfetchall(cursor: connection.cursor) -> List[Dict[str, Any]]:
 | 
			
		||||
def dictfetchall(cursor):
 | 
			
		||||
    # type: (connection.cursor) -> List[Dict[str, Any]]
 | 
			
		||||
    "Returns all rows from a cursor as a dict"
 | 
			
		||||
    desc = cursor.description
 | 
			
		||||
    return [
 | 
			
		||||
@@ -230,7 +238,8 @@ def dictfetchall(cursor: connection.cursor) -> List[Dict[str, Any]]:
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_realm_day_counts() -> Dict[str, Dict[str, str]]:
 | 
			
		||||
def get_realm_day_counts():
 | 
			
		||||
    # type: () -> Dict[str, Dict[str, str]]
 | 
			
		||||
    query = '''
 | 
			
		||||
        select
 | 
			
		||||
            r.string_id,
 | 
			
		||||
@@ -265,13 +274,12 @@ def get_realm_day_counts() -> Dict[str, Dict[str, str]]:
 | 
			
		||||
    result = {}
 | 
			
		||||
    for string_id in counts:
 | 
			
		||||
        raw_cnts = [counts[string_id].get(age, 0) for age in range(8)]
 | 
			
		||||
        min_cnt = min(raw_cnts[1:])
 | 
			
		||||
        max_cnt = max(raw_cnts[1:])
 | 
			
		||||
        min_cnt = min(raw_cnts)
 | 
			
		||||
        max_cnt = max(raw_cnts)
 | 
			
		||||
 | 
			
		||||
        def format_count(cnt: int, style: Optional[str]=None) -> str:
 | 
			
		||||
            if style is not None:
 | 
			
		||||
                good_bad = style
 | 
			
		||||
            elif cnt == min_cnt:
 | 
			
		||||
        def format_count(cnt):
 | 
			
		||||
            # type: (int) -> str
 | 
			
		||||
            if cnt == min_cnt:
 | 
			
		||||
                good_bad = 'bad'
 | 
			
		||||
            elif cnt == max_cnt:
 | 
			
		||||
                good_bad = 'good'
 | 
			
		||||
@@ -280,21 +288,18 @@ def get_realm_day_counts() -> Dict[str, Dict[str, str]]:
 | 
			
		||||
 | 
			
		||||
            return '<td class="number %s">%s</td>' % (good_bad, cnt)
 | 
			
		||||
 | 
			
		||||
        cnts = (format_count(raw_cnts[0], 'neutral')
 | 
			
		||||
                + ''.join(map(format_count, raw_cnts[1:])))
 | 
			
		||||
        cnts = ''.join(map(format_count, raw_cnts))
 | 
			
		||||
        result[string_id] = dict(cnts=cnts)
 | 
			
		||||
 | 
			
		||||
    return result
 | 
			
		||||
 | 
			
		||||
def realm_summary_table(realm_minutes: Dict[str, float]) -> str:
 | 
			
		||||
    now = timezone_now()
 | 
			
		||||
 | 
			
		||||
def realm_summary_table(realm_minutes):
 | 
			
		||||
    # type: (Dict[str, float]) -> str
 | 
			
		||||
    query = '''
 | 
			
		||||
        SELECT
 | 
			
		||||
            realm.string_id,
 | 
			
		||||
            realm.date_created,
 | 
			
		||||
            coalesce(user_counts.dau_count, 0) dau_count,
 | 
			
		||||
            coalesce(wau_counts.wau_count, 0) wau_count,
 | 
			
		||||
            coalesce(user_counts.active_user_count, 0) active_user_count,
 | 
			
		||||
            coalesce(at_risk_counts.at_risk_count, 0) at_risk_count,
 | 
			
		||||
            (
 | 
			
		||||
                SELECT
 | 
			
		||||
                    count(*)
 | 
			
		||||
@@ -316,24 +321,22 @@ def realm_summary_table(realm_minutes: Dict[str, float]) -> str:
 | 
			
		||||
            (
 | 
			
		||||
                SELECT
 | 
			
		||||
                    up.realm_id realm_id,
 | 
			
		||||
                    count(distinct(ua.user_profile_id)) dau_count
 | 
			
		||||
                    count(distinct(ua.user_profile_id)) active_user_count
 | 
			
		||||
                FROM zerver_useractivity ua
 | 
			
		||||
                JOIN zerver_userprofile up
 | 
			
		||||
                    ON up.id = ua.user_profile_id
 | 
			
		||||
                WHERE
 | 
			
		||||
                    up.is_active
 | 
			
		||||
                AND (not up.is_bot)
 | 
			
		||||
                AND
 | 
			
		||||
                    query in (
 | 
			
		||||
                        '/json/send_message',
 | 
			
		||||
                        'send_message_backend',
 | 
			
		||||
                        '/api/v1/send_message',
 | 
			
		||||
                        '/json/update_pointer',
 | 
			
		||||
                        '/json/users/me/pointer',
 | 
			
		||||
                        'update_pointer_backend'
 | 
			
		||||
                        '/json/users/me/pointer'
 | 
			
		||||
                    )
 | 
			
		||||
                AND
 | 
			
		||||
                    last_visit > now() - interval '1 day'
 | 
			
		||||
                AND
 | 
			
		||||
                    not is_bot
 | 
			
		||||
                GROUP BY realm_id
 | 
			
		||||
            ) user_counts
 | 
			
		||||
            ON user_counts.realm_id = realm.id
 | 
			
		||||
@@ -341,7 +344,7 @@ def realm_summary_table(realm_minutes: Dict[str, float]) -> str:
 | 
			
		||||
            (
 | 
			
		||||
                SELECT
 | 
			
		||||
                    realm_id,
 | 
			
		||||
                    count(*) wau_count
 | 
			
		||||
                    count(*) at_risk_count
 | 
			
		||||
                FROM (
 | 
			
		||||
                    SELECT
 | 
			
		||||
                        realm.id as realm_id,
 | 
			
		||||
@@ -359,37 +362,35 @@ def realm_summary_table(realm_minutes: Dict[str, float]) -> str:
 | 
			
		||||
                            'send_message_backend',
 | 
			
		||||
                            '/api/v1/send_message',
 | 
			
		||||
                            '/json/update_pointer',
 | 
			
		||||
                            '/json/users/me/pointer',
 | 
			
		||||
                            'update_pointer_backend'
 | 
			
		||||
                            '/json/users/me/pointer'
 | 
			
		||||
                        )
 | 
			
		||||
                    GROUP by realm.id, up.email
 | 
			
		||||
                    HAVING max(last_visit) > now() - interval '7 day'
 | 
			
		||||
                ) as wau_users
 | 
			
		||||
                    HAVING max(last_visit) between
 | 
			
		||||
                        now() - interval '7 day' and
 | 
			
		||||
                        now() - interval '1 day'
 | 
			
		||||
                ) as at_risk_users
 | 
			
		||||
                GROUP BY realm_id
 | 
			
		||||
            ) wau_counts
 | 
			
		||||
            ON wau_counts.realm_id = realm.id
 | 
			
		||||
            ) at_risk_counts
 | 
			
		||||
            ON at_risk_counts.realm_id = realm.id
 | 
			
		||||
        WHERE EXISTS (
 | 
			
		||||
                SELECT *
 | 
			
		||||
                FROM zerver_useractivity ua
 | 
			
		||||
                JOIN zerver_userprofile up
 | 
			
		||||
                    ON up.id = ua.user_profile_id
 | 
			
		||||
                WHERE
 | 
			
		||||
                    up.realm_id = realm.id
 | 
			
		||||
                AND up.is_active
 | 
			
		||||
                AND (not up.is_bot)
 | 
			
		||||
                AND
 | 
			
		||||
                    query in (
 | 
			
		||||
                        '/json/send_message',
 | 
			
		||||
                        '/api/v1/send_message',
 | 
			
		||||
                        'send_message_backend',
 | 
			
		||||
                        '/json/update_pointer',
 | 
			
		||||
                        '/json/users/me/pointer',
 | 
			
		||||
                        'update_pointer_backend'
 | 
			
		||||
                        '/json/users/me/pointer'
 | 
			
		||||
                    )
 | 
			
		||||
                AND
 | 
			
		||||
                    up.realm_id = realm.id
 | 
			
		||||
                AND
 | 
			
		||||
                    last_visit > now() - interval '2 week'
 | 
			
		||||
        )
 | 
			
		||||
        ORDER BY dau_count DESC, string_id ASC
 | 
			
		||||
        ORDER BY active_user_count DESC, string_id ASC
 | 
			
		||||
        '''
 | 
			
		||||
 | 
			
		||||
    cursor = connection.cursor()
 | 
			
		||||
@@ -397,21 +398,6 @@ def realm_summary_table(realm_minutes: Dict[str, float]) -> str:
 | 
			
		||||
    rows = dictfetchall(cursor)
 | 
			
		||||
    cursor.close()
 | 
			
		||||
 | 
			
		||||
    # Fetch all the realm administrator users
 | 
			
		||||
    realm_admins = defaultdict(list)  # type: Dict[str, List[str]]
 | 
			
		||||
    for up in UserProfile.objects.select_related("realm").filter(
 | 
			
		||||
        is_realm_admin=True,
 | 
			
		||||
        is_active=True
 | 
			
		||||
    ):
 | 
			
		||||
        realm_admins[up.realm.string_id].append(up.email)
 | 
			
		||||
 | 
			
		||||
    for row in rows:
 | 
			
		||||
        row['date_created_day'] = row['date_created'].strftime('%Y-%m-%d')
 | 
			
		||||
        row['age_days'] = int((now - row['date_created']).total_seconds()
 | 
			
		||||
                              / 86400)
 | 
			
		||||
        row['is_new'] = row['age_days'] < 12 * 7
 | 
			
		||||
        row['realm_admin_email'] = ', '.join(realm_admins[row['string_id']])
 | 
			
		||||
 | 
			
		||||
    # get messages sent per day
 | 
			
		||||
    counts = get_realm_day_counts()
 | 
			
		||||
    for row in rows:
 | 
			
		||||
@@ -429,7 +415,7 @@ def realm_summary_table(realm_minutes: Dict[str, float]) -> str:
 | 
			
		||||
        total_hours += hours
 | 
			
		||||
        row['hours'] = str(int(hours))
 | 
			
		||||
        try:
 | 
			
		||||
            row['hours_per_user'] = '%.1f' % (hours / row['dau_count'],)
 | 
			
		||||
            row['hours_per_user'] = '%.1f' % (hours / row['active_user_count'],)
 | 
			
		||||
        except Exception:
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
@@ -438,42 +424,41 @@ def realm_summary_table(realm_minutes: Dict[str, float]) -> str:
 | 
			
		||||
        row['string_id'] = realm_activity_link(row['string_id'])
 | 
			
		||||
 | 
			
		||||
    # Count active sites
 | 
			
		||||
    def meets_goal(row: Dict[str, int]) -> bool:
 | 
			
		||||
        return row['dau_count'] >= 5
 | 
			
		||||
    def meets_goal(row):
 | 
			
		||||
        # type: (Dict[str, int]) -> bool
 | 
			
		||||
        return row['active_user_count'] >= 5
 | 
			
		||||
 | 
			
		||||
    num_active_sites = len(list(filter(meets_goal, rows)))
 | 
			
		||||
 | 
			
		||||
    # create totals
 | 
			
		||||
    total_dau_count = 0
 | 
			
		||||
    total_active_user_count = 0
 | 
			
		||||
    total_user_profile_count = 0
 | 
			
		||||
    total_bot_count = 0
 | 
			
		||||
    total_wau_count = 0
 | 
			
		||||
    total_at_risk_count = 0
 | 
			
		||||
    for row in rows:
 | 
			
		||||
        total_dau_count += int(row['dau_count'])
 | 
			
		||||
        total_active_user_count += int(row['active_user_count'])
 | 
			
		||||
        total_user_profile_count += int(row['user_profile_count'])
 | 
			
		||||
        total_bot_count += int(row['bot_count'])
 | 
			
		||||
        total_wau_count += int(row['wau_count'])
 | 
			
		||||
        total_at_risk_count += int(row['at_risk_count'])
 | 
			
		||||
 | 
			
		||||
    rows.append(dict(
 | 
			
		||||
        string_id='Total',
 | 
			
		||||
        date_created_day='',
 | 
			
		||||
        realm_admin_email='',
 | 
			
		||||
        dau_count=total_dau_count,
 | 
			
		||||
        active_user_count=total_active_user_count,
 | 
			
		||||
        user_profile_count=total_user_profile_count,
 | 
			
		||||
        bot_count=total_bot_count,
 | 
			
		||||
        hours=int(total_hours),
 | 
			
		||||
        wau_count=total_wau_count,
 | 
			
		||||
        at_risk_count=total_at_risk_count,
 | 
			
		||||
    ))
 | 
			
		||||
 | 
			
		||||
    content = loader.render_to_string(
 | 
			
		||||
        'analytics/realm_summary_table.html',
 | 
			
		||||
        dict(rows=rows, num_active_sites=num_active_sites,
 | 
			
		||||
             now=now.strftime('%Y-%m-%dT%H:%M:%SZ'))
 | 
			
		||||
        dict(rows=rows, num_active_sites=num_active_sites)
 | 
			
		||||
    )
 | 
			
		||||
    return content
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def user_activity_intervals() -> Tuple[mark_safe, Dict[str, float]]:
 | 
			
		||||
def user_activity_intervals():
 | 
			
		||||
    # type: () -> Tuple[mark_safe, Dict[str, float]]
 | 
			
		||||
    day_end = timestamp_to_datetime(time.time())
 | 
			
		||||
    day_start = day_end - timedelta(hours=24)
 | 
			
		||||
 | 
			
		||||
@@ -523,7 +508,8 @@ def user_activity_intervals() -> Tuple[mark_safe, Dict[str, float]]:
 | 
			
		||||
    content = mark_safe('<pre>' + output + '</pre>')
 | 
			
		||||
    return content, realm_minutes
 | 
			
		||||
 | 
			
		||||
def sent_messages_report(realm: str) -> str:
 | 
			
		||||
def sent_messages_report(realm):
 | 
			
		||||
    # type: (str) -> str
 | 
			
		||||
    title = 'Recently sent messages for ' + realm
 | 
			
		||||
 | 
			
		||||
    cols = [
 | 
			
		||||
@@ -590,16 +576,18 @@ def sent_messages_report(realm: str) -> str:
 | 
			
		||||
 | 
			
		||||
    return make_table(title, cols, rows)
 | 
			
		||||
 | 
			
		||||
def ad_hoc_queries() -> List[Dict[str, str]]:
 | 
			
		||||
    def get_page(query: str, cols: List[str], title: str) -> Dict[str, str]:
 | 
			
		||||
def ad_hoc_queries():
 | 
			
		||||
    # type: () -> List[Dict[str, str]]
 | 
			
		||||
    def get_page(query, cols, title):
 | 
			
		||||
        # type: (str, List[str], str) -> Dict[str, str]
 | 
			
		||||
        cursor = connection.cursor()
 | 
			
		||||
        cursor.execute(query)
 | 
			
		||||
        rows = cursor.fetchall()
 | 
			
		||||
        rows = list(map(list, rows))
 | 
			
		||||
        cursor.close()
 | 
			
		||||
 | 
			
		||||
        def fix_rows(i: int,
 | 
			
		||||
                     fixup_func: Union[Callable[[Realm], mark_safe], Callable[[datetime], str]]) -> None:
 | 
			
		||||
        def fix_rows(i, fixup_func):
 | 
			
		||||
            # type: (int, Union[Callable[[Realm], mark_safe], Callable[[datetime], str]]) -> None
 | 
			
		||||
            for row in rows:
 | 
			
		||||
                row[i] = fixup_func(row[i])
 | 
			
		||||
 | 
			
		||||
@@ -761,7 +749,8 @@ def ad_hoc_queries() -> List[Dict[str, str]]:
 | 
			
		||||
 | 
			
		||||
@require_server_admin
 | 
			
		||||
@has_request_variables
 | 
			
		||||
def get_activity(request: HttpRequest) -> HttpResponse:
 | 
			
		||||
def get_activity(request):
 | 
			
		||||
    # type: (HttpRequest) -> HttpResponse
 | 
			
		||||
    duration_content, realm_minutes = user_activity_intervals()  # type: Tuple[mark_safe, Dict[str, float]]
 | 
			
		||||
    counts_content = realm_summary_table(realm_minutes)  # type: str
 | 
			
		||||
    data = [
 | 
			
		||||
@@ -779,7 +768,8 @@ def get_activity(request: HttpRequest) -> HttpResponse:
 | 
			
		||||
        context=dict(data=data, title=title, is_home=True),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
def get_user_activity_records_for_realm(realm: str, is_bot: bool) -> QuerySet:
 | 
			
		||||
def get_user_activity_records_for_realm(realm, is_bot):
 | 
			
		||||
    # type: (str, bool) -> QuerySet
 | 
			
		||||
    fields = [
 | 
			
		||||
        'user_profile__full_name',
 | 
			
		||||
        'user_profile__email',
 | 
			
		||||
@@ -798,7 +788,8 @@ def get_user_activity_records_for_realm(realm: str, is_bot: bool) -> QuerySet:
 | 
			
		||||
    records = records.select_related('user_profile', 'client').only(*fields)
 | 
			
		||||
    return records
 | 
			
		||||
 | 
			
		||||
def get_user_activity_records_for_email(email: str) -> List[QuerySet]:
 | 
			
		||||
def get_user_activity_records_for_email(email):
 | 
			
		||||
    # type: (str) -> List[QuerySet]
 | 
			
		||||
    fields = [
 | 
			
		||||
        'user_profile__full_name',
 | 
			
		||||
        'query',
 | 
			
		||||
@@ -814,7 +805,8 @@ def get_user_activity_records_for_email(email: str) -> List[QuerySet]:
 | 
			
		||||
    records = records.select_related('user_profile', 'client').only(*fields)
 | 
			
		||||
    return records
 | 
			
		||||
 | 
			
		||||
def raw_user_activity_table(records: List[QuerySet]) -> str:
 | 
			
		||||
def raw_user_activity_table(records):
 | 
			
		||||
    # type: (List[QuerySet]) -> str
 | 
			
		||||
    cols = [
 | 
			
		||||
        'query',
 | 
			
		||||
        'client',
 | 
			
		||||
@@ -822,7 +814,8 @@ def raw_user_activity_table(records: List[QuerySet]) -> str:
 | 
			
		||||
        'last_visit'
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    def row(record: QuerySet) -> List[Any]:
 | 
			
		||||
    def row(record):
 | 
			
		||||
        # type: (QuerySet) -> List[Any]
 | 
			
		||||
        return [
 | 
			
		||||
            record.query,
 | 
			
		||||
            record.client.name,
 | 
			
		||||
@@ -834,7 +827,8 @@ def raw_user_activity_table(records: List[QuerySet]) -> str:
 | 
			
		||||
    title = 'Raw Data'
 | 
			
		||||
    return make_table(title, cols, rows)
 | 
			
		||||
 | 
			
		||||
def get_user_activity_summary(records: List[QuerySet]) -> Dict[str, Dict[str, Any]]:
 | 
			
		||||
def get_user_activity_summary(records):
 | 
			
		||||
    # type: (List[QuerySet]) -> Dict[str, Dict[str, Any]]
 | 
			
		||||
    #: `Any` used above should be `Union(int, datetime)`.
 | 
			
		||||
    #: However current version of `Union` does not work inside other function.
 | 
			
		||||
    #: We could use something like:
 | 
			
		||||
@@ -842,7 +836,8 @@ def get_user_activity_summary(records: List[QuerySet]) -> Dict[str, Dict[str, An
 | 
			
		||||
    #: but that would require this long `Union` to carry on throughout inner functions.
 | 
			
		||||
    summary = {}  # type: Dict[str, Dict[str, Any]]
 | 
			
		||||
 | 
			
		||||
    def update(action: str, record: QuerySet) -> None:
 | 
			
		||||
    def update(action, record):
 | 
			
		||||
        # type: (str, QuerySet) -> None
 | 
			
		||||
        if action not in summary:
 | 
			
		||||
            summary[action] = dict(
 | 
			
		||||
                count=record.count,
 | 
			
		||||
@@ -876,32 +871,35 @@ def get_user_activity_summary(records: List[QuerySet]) -> Dict[str, Dict[str, An
 | 
			
		||||
            update('website', record)
 | 
			
		||||
        if ('send_message' in query) or re.search('/api/.*/external/.*', query):
 | 
			
		||||
            update('send', record)
 | 
			
		||||
        if query in ['/json/update_pointer', '/json/users/me/pointer', '/api/v1/update_pointer',
 | 
			
		||||
                     'update_pointer_backend']:
 | 
			
		||||
        if query in ['/json/update_pointer', '/json/users/me/pointer', '/api/v1/update_pointer']:
 | 
			
		||||
            update('pointer', record)
 | 
			
		||||
        update(client, record)
 | 
			
		||||
 | 
			
		||||
    return summary
 | 
			
		||||
 | 
			
		||||
def format_date_for_activity_reports(date: Optional[datetime]) -> str:
 | 
			
		||||
def format_date_for_activity_reports(date):
 | 
			
		||||
    # type: (Optional[datetime]) -> str
 | 
			
		||||
    if date:
 | 
			
		||||
        return date.astimezone(eastern_tz).strftime('%Y-%m-%d %H:%M')
 | 
			
		||||
    else:
 | 
			
		||||
        return ''
 | 
			
		||||
 | 
			
		||||
def user_activity_link(email: str) -> mark_safe:
 | 
			
		||||
def user_activity_link(email):
 | 
			
		||||
    # type: (str) -> mark_safe
 | 
			
		||||
    url_name = 'analytics.views.get_user_activity'
 | 
			
		||||
    url = reverse(url_name, kwargs=dict(email=email))
 | 
			
		||||
    url = urlresolvers.reverse(url_name, kwargs=dict(email=email))
 | 
			
		||||
    email_link = '<a href="%s">%s</a>' % (url, email)
 | 
			
		||||
    return mark_safe(email_link)
 | 
			
		||||
 | 
			
		||||
def realm_activity_link(realm_str: str) -> mark_safe:
 | 
			
		||||
def realm_activity_link(realm_str):
 | 
			
		||||
    # type: (str) -> mark_safe
 | 
			
		||||
    url_name = 'analytics.views.get_realm_activity'
 | 
			
		||||
    url = reverse(url_name, kwargs=dict(realm_str=realm_str))
 | 
			
		||||
    url = urlresolvers.reverse(url_name, kwargs=dict(realm_str=realm_str))
 | 
			
		||||
    realm_link = '<a href="%s">%s</a>' % (url, realm_str)
 | 
			
		||||
    return mark_safe(realm_link)
 | 
			
		||||
 | 
			
		||||
def realm_client_table(user_summaries: Dict[str, Dict[str, Dict[str, Any]]]) -> str:
 | 
			
		||||
def realm_client_table(user_summaries):
 | 
			
		||||
    # type: (Dict[str, Dict[str, Dict[str, Any]]]) -> str
 | 
			
		||||
    exclude_keys = [
 | 
			
		||||
        'internal',
 | 
			
		||||
        'name',
 | 
			
		||||
@@ -945,7 +943,8 @@ def realm_client_table(user_summaries: Dict[str, Dict[str, Dict[str, Any]]]) ->
 | 
			
		||||
 | 
			
		||||
    return make_table(title, cols, rows)
 | 
			
		||||
 | 
			
		||||
def user_activity_summary_table(user_summary: Dict[str, Dict[str, Any]]) -> str:
 | 
			
		||||
def user_activity_summary_table(user_summary):
 | 
			
		||||
    # type: (Dict[str, Dict[str, Any]]) -> str
 | 
			
		||||
    rows = []
 | 
			
		||||
    for k, v in user_summary.items():
 | 
			
		||||
        if k == 'name':
 | 
			
		||||
@@ -971,29 +970,33 @@ def user_activity_summary_table(user_summary: Dict[str, Dict[str, Any]]) -> str:
 | 
			
		||||
    title = 'User Activity'
 | 
			
		||||
    return make_table(title, cols, rows)
 | 
			
		||||
 | 
			
		||||
def realm_user_summary_table(all_records: List[QuerySet],
 | 
			
		||||
                             admin_emails: Set[Text]) -> Tuple[Dict[str, Dict[str, Any]], str]:
 | 
			
		||||
def realm_user_summary_table(all_records, admin_emails):
 | 
			
		||||
    # type: (List[QuerySet], Set[Text]) -> Tuple[Dict[str, Dict[str, Any]], str]
 | 
			
		||||
    user_records = {}
 | 
			
		||||
 | 
			
		||||
    def by_email(record: QuerySet) -> str:
 | 
			
		||||
    def by_email(record):
 | 
			
		||||
        # type: (QuerySet) -> str
 | 
			
		||||
        return record.user_profile.email
 | 
			
		||||
 | 
			
		||||
    for email, records in itertools.groupby(all_records, by_email):
 | 
			
		||||
        user_records[email] = get_user_activity_summary(list(records))
 | 
			
		||||
 | 
			
		||||
    def get_last_visit(user_summary: Dict[str, Dict[str, datetime]], k: str) -> Optional[datetime]:
 | 
			
		||||
    def get_last_visit(user_summary, k):
 | 
			
		||||
        # type: (Dict[str, Dict[str, datetime]], str) -> Optional[datetime]
 | 
			
		||||
        if k in user_summary:
 | 
			
		||||
            return user_summary[k]['last_visit']
 | 
			
		||||
        else:
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
    def get_count(user_summary: Dict[str, Dict[str, str]], k: str) -> str:
 | 
			
		||||
    def get_count(user_summary, k):
 | 
			
		||||
        # type: (Dict[str, Dict[str, str]], str) -> str
 | 
			
		||||
        if k in user_summary:
 | 
			
		||||
            return user_summary[k]['count']
 | 
			
		||||
        else:
 | 
			
		||||
            return ''
 | 
			
		||||
 | 
			
		||||
    def is_recent(val: Optional[datetime]) -> bool:
 | 
			
		||||
    def is_recent(val):
 | 
			
		||||
        # type: (Optional[datetime]) -> bool
 | 
			
		||||
        age = timezone_now() - val
 | 
			
		||||
        return age.total_seconds() < 5 * 60
 | 
			
		||||
 | 
			
		||||
@@ -1015,7 +1018,8 @@ def realm_user_summary_table(all_records: List[QuerySet],
 | 
			
		||||
        row = dict(cells=cells, row_class=row_class)
 | 
			
		||||
        rows.append(row)
 | 
			
		||||
 | 
			
		||||
    def by_used_time(row: Dict[str, Any]) -> str:
 | 
			
		||||
    def by_used_time(row):
 | 
			
		||||
        # type: (Dict[str, Any]) -> str
 | 
			
		||||
        return row['cells'][3]
 | 
			
		||||
 | 
			
		||||
    rows = sorted(rows, key=by_used_time, reverse=True)
 | 
			
		||||
@@ -1038,7 +1042,8 @@ def realm_user_summary_table(all_records: List[QuerySet],
 | 
			
		||||
    return user_records, content
 | 
			
		||||
 | 
			
		||||
@require_server_admin
 | 
			
		||||
def get_realm_activity(request: HttpRequest, realm_str: str) -> HttpResponse:
 | 
			
		||||
def get_realm_activity(request, realm_str):
 | 
			
		||||
    # type: (HttpRequest, str) -> HttpResponse
 | 
			
		||||
    data = []  # type: List[Tuple[str, str]]
 | 
			
		||||
    all_user_records = {}  # type: Dict[str, Any]
 | 
			
		||||
 | 
			
		||||
@@ -1073,7 +1078,8 @@ def get_realm_activity(request: HttpRequest, realm_str: str) -> HttpResponse:
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
@require_server_admin
 | 
			
		||||
def get_user_activity(request: HttpRequest, email: str) -> HttpResponse:
 | 
			
		||||
def get_user_activity(request, email):
 | 
			
		||||
    # type: (HttpRequest, str) -> HttpResponse
 | 
			
		||||
    records = get_user_activity_records_for_email(email)
 | 
			
		||||
 | 
			
		||||
    data = []  # type: List[Tuple[str, str]]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,22 +0,0 @@
 | 
			
		||||
# -*- coding: utf-8 -*-
 | 
			
		||||
# Generated by Django 1.11.6 on 2017-11-30 00:13
 | 
			
		||||
from __future__ import unicode_literals
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('zerver', '0124_stream_enable_notifications'),
 | 
			
		||||
        ('confirmation', '0004_remove_confirmationmanager'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='confirmation',
 | 
			
		||||
            name='realm',
 | 
			
		||||
            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='zerver.Realm'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -1,20 +0,0 @@
 | 
			
		||||
# -*- coding: utf-8 -*-
 | 
			
		||||
# Generated by Django 1.11.6 on 2018-01-29 18:39
 | 
			
		||||
from __future__ import unicode_literals
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('confirmation', '0005_confirmation_realm'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='realmcreationkey',
 | 
			
		||||
            name='presume_email_valid',
 | 
			
		||||
            field=models.BooleanField(default=False),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -7,9 +7,9 @@ __revision__ = '$Id: models.py 28 2009-10-22 15:03:02Z jarek.zgoda $'
 | 
			
		||||
import datetime
 | 
			
		||||
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models import CASCADE
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
from django.core.urlresolvers import reverse
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.contrib.sites.models import Site
 | 
			
		||||
from django.contrib.contenttypes.models import ContentType
 | 
			
		||||
from django.contrib.contenttypes.fields import GenericForeignKey
 | 
			
		||||
from django.http import HttpRequest, HttpResponse
 | 
			
		||||
@@ -18,8 +18,7 @@ from django.utils.timezone import now as timezone_now
 | 
			
		||||
 | 
			
		||||
from zerver.lib.send_email import send_email
 | 
			
		||||
from zerver.lib.utils import generate_random_token
 | 
			
		||||
from zerver.models import PreregistrationUser, EmailChangeStatus, MultiuseInvite, \
 | 
			
		||||
    UserProfile, Realm
 | 
			
		||||
from zerver.models import PreregistrationUser, EmailChangeStatus, MultiuseInvite
 | 
			
		||||
from random import SystemRandom
 | 
			
		||||
import string
 | 
			
		||||
from typing import Any, Dict, Optional, Text, Union
 | 
			
		||||
@@ -29,31 +28,32 @@ class ConfirmationKeyException(Exception):
 | 
			
		||||
    EXPIRED = 2
 | 
			
		||||
    DOES_NOT_EXIST = 3
 | 
			
		||||
 | 
			
		||||
    def __init__(self, error_type: int) -> None:
 | 
			
		||||
        super().__init__()
 | 
			
		||||
    def __init__(self, error_type):
 | 
			
		||||
        # type: (int) -> None
 | 
			
		||||
        super(ConfirmationKeyException, self).__init__()
 | 
			
		||||
        self.error_type = error_type
 | 
			
		||||
 | 
			
		||||
def render_confirmation_key_error(request: HttpRequest, exception: ConfirmationKeyException) -> HttpResponse:
 | 
			
		||||
def render_confirmation_key_error(request, exception):
 | 
			
		||||
    # type: (HttpRequest, ConfirmationKeyException) -> HttpResponse
 | 
			
		||||
    if exception.error_type == ConfirmationKeyException.WRONG_LENGTH:
 | 
			
		||||
        return render(request, 'confirmation/link_malformed.html')
 | 
			
		||||
    if exception.error_type == ConfirmationKeyException.EXPIRED:
 | 
			
		||||
        return render(request, 'confirmation/link_expired.html')
 | 
			
		||||
    return render(request, 'confirmation/link_does_not_exist.html')
 | 
			
		||||
 | 
			
		||||
def generate_key() -> str:
 | 
			
		||||
def generate_key():
 | 
			
		||||
    # type: () -> str
 | 
			
		||||
    generator = SystemRandom()
 | 
			
		||||
    # 24 characters * 5 bits of entropy/character = 120 bits of entropy
 | 
			
		||||
    return ''.join(generator.choice(string.ascii_lowercase + string.digits) for _ in range(24))
 | 
			
		||||
 | 
			
		||||
ConfirmationObjT = Union[MultiuseInvite, PreregistrationUser, EmailChangeStatus]
 | 
			
		||||
def get_object_from_key(confirmation_key: str,
 | 
			
		||||
                        confirmation_type: int) -> ConfirmationObjT:
 | 
			
		||||
def get_object_from_key(confirmation_key):
 | 
			
		||||
    # type: (str) -> Union[MultiuseInvite, PreregistrationUser, EmailChangeStatus]
 | 
			
		||||
    # Confirmation keys used to be 40 characters
 | 
			
		||||
    if len(confirmation_key) not in (24, 40):
 | 
			
		||||
        raise ConfirmationKeyException(ConfirmationKeyException.WRONG_LENGTH)
 | 
			
		||||
    try:
 | 
			
		||||
        confirmation = Confirmation.objects.get(confirmation_key=confirmation_key,
 | 
			
		||||
                                                type=confirmation_type)
 | 
			
		||||
        confirmation = Confirmation.objects.get(confirmation_key=confirmation_key)
 | 
			
		||||
    except Confirmation.DoesNotExist:
 | 
			
		||||
        raise ConfirmationKeyException(ConfirmationKeyException.DOES_NOT_EXIST)
 | 
			
		||||
 | 
			
		||||
@@ -67,17 +67,15 @@ def get_object_from_key(confirmation_key: str,
 | 
			
		||||
        obj.save(update_fields=['status'])
 | 
			
		||||
    return obj
 | 
			
		||||
 | 
			
		||||
def create_confirmation_link(obj: ContentType, host: str,
 | 
			
		||||
                             confirmation_type: int,
 | 
			
		||||
                             url_args: Optional[Dict[str, str]]=None) -> str:
 | 
			
		||||
def create_confirmation_link(obj, host, confirmation_type, url_args=None):
 | 
			
		||||
    # type: (Union[ContentType, int], str, int, Optional[Dict[str, str]]) -> str
 | 
			
		||||
    key = generate_key()
 | 
			
		||||
    Confirmation.objects.create(content_object=obj, date_sent=timezone_now(), confirmation_key=key,
 | 
			
		||||
                                realm=obj.realm, type=confirmation_type)
 | 
			
		||||
                                type=confirmation_type)
 | 
			
		||||
    return confirmation_url(key, host, confirmation_type, url_args)
 | 
			
		||||
 | 
			
		||||
def confirmation_url(confirmation_key: str, host: str,
 | 
			
		||||
                     confirmation_type: int,
 | 
			
		||||
                     url_args: Optional[Dict[str, str]]=None) -> str:
 | 
			
		||||
def confirmation_url(confirmation_key, host, confirmation_type, url_args=None):
 | 
			
		||||
    # type: (str, str, int, Optional[Dict[str, str]]) -> str
 | 
			
		||||
    if url_args is None:
 | 
			
		||||
        url_args = {}
 | 
			
		||||
    url_args['confirmation_key'] = confirmation_key
 | 
			
		||||
@@ -85,12 +83,11 @@ def confirmation_url(confirmation_key: str, host: str,
 | 
			
		||||
                       reverse(_properties[confirmation_type].url_name, kwargs=url_args))
 | 
			
		||||
 | 
			
		||||
class Confirmation(models.Model):
 | 
			
		||||
    content_type = models.ForeignKey(ContentType, on_delete=CASCADE)
 | 
			
		||||
    content_type = models.ForeignKey(ContentType)
 | 
			
		||||
    object_id = models.PositiveIntegerField()  # type: int
 | 
			
		||||
    content_object = GenericForeignKey('content_type', 'object_id')
 | 
			
		||||
    date_sent = models.DateTimeField()  # type: datetime.datetime
 | 
			
		||||
    confirmation_key = models.CharField(max_length=40)  # type: str
 | 
			
		||||
    realm = models.ForeignKey(Realm, null=True, on_delete=CASCADE)  # type: Optional[Realm]
 | 
			
		||||
 | 
			
		||||
    # The following list is the set of valid types
 | 
			
		||||
    USER_REGISTRATION = 1
 | 
			
		||||
@@ -99,69 +96,51 @@ class Confirmation(models.Model):
 | 
			
		||||
    UNSUBSCRIBE = 4
 | 
			
		||||
    SERVER_REGISTRATION = 5
 | 
			
		||||
    MULTIUSE_INVITE = 6
 | 
			
		||||
    REALM_CREATION = 7
 | 
			
		||||
    type = models.PositiveSmallIntegerField()  # type: int
 | 
			
		||||
 | 
			
		||||
    def __str__(self) -> Text:
 | 
			
		||||
    def __unicode__(self):
 | 
			
		||||
        # type: () -> Text
 | 
			
		||||
        return '<Confirmation: %s>' % (self.content_object,)
 | 
			
		||||
 | 
			
		||||
class ConfirmationType:
 | 
			
		||||
    def __init__(self, url_name: str,
 | 
			
		||||
                 validity_in_days: int=settings.CONFIRMATION_LINK_DEFAULT_VALIDITY_DAYS) -> None:
 | 
			
		||||
class ConfirmationType(object):
 | 
			
		||||
    def __init__(self, url_name, validity_in_days=settings.CONFIRMATION_LINK_DEFAULT_VALIDITY_DAYS):
 | 
			
		||||
        # type: (str, int) -> None
 | 
			
		||||
        self.url_name = url_name
 | 
			
		||||
        self.validity_in_days = validity_in_days
 | 
			
		||||
 | 
			
		||||
_properties = {
 | 
			
		||||
    Confirmation.USER_REGISTRATION: ConfirmationType('check_prereg_key_and_redirect'),
 | 
			
		||||
    Confirmation.INVITATION: ConfirmationType('check_prereg_key_and_redirect',
 | 
			
		||||
    Confirmation.USER_REGISTRATION: ConfirmationType('confirmation.views.confirm'),
 | 
			
		||||
    Confirmation.INVITATION: ConfirmationType('confirmation.views.confirm',
 | 
			
		||||
                                              validity_in_days=settings.INVITATION_LINK_VALIDITY_DAYS),
 | 
			
		||||
    Confirmation.EMAIL_CHANGE: ConfirmationType('zerver.views.user_settings.confirm_email_change'),
 | 
			
		||||
    Confirmation.UNSUBSCRIBE: ConfirmationType('zerver.views.unsubscribe.email_unsubscribe',
 | 
			
		||||
                                               validity_in_days=1000000),  # should never expire
 | 
			
		||||
    Confirmation.MULTIUSE_INVITE: ConfirmationType(
 | 
			
		||||
        'zerver.views.registration.accounts_home_from_multiuse_invite',
 | 
			
		||||
        validity_in_days=settings.INVITATION_LINK_VALIDITY_DAYS),
 | 
			
		||||
    Confirmation.REALM_CREATION: ConfirmationType('check_prereg_key_and_redirect'),
 | 
			
		||||
    Confirmation.MULTIUSE_INVITE: ConfirmationType('zerver.views.registration.accounts_home_from_multiuse_invite',
 | 
			
		||||
                                                   validity_in_days=settings.INVITATION_LINK_VALIDITY_DAYS)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# Functions related to links generated by the generate_realm_creation_link.py
 | 
			
		||||
# management command.
 | 
			
		||||
# Note that being validated here will just allow the user to access the create_realm
 | 
			
		||||
# form, where they will enter their email and go through the regular
 | 
			
		||||
# Confirmation.REALM_CREATION pathway.
 | 
			
		||||
# Arguably RealmCreationKey should just be another ConfirmationObjT and we should
 | 
			
		||||
# add another Confirmation.type for this; it's this way for historical reasons.
 | 
			
		||||
# Conirmation pathways for which there is no content_object that we need to
 | 
			
		||||
# keep track of.
 | 
			
		||||
 | 
			
		||||
def validate_key(creation_key: Optional[str]) -> Optional['RealmCreationKey']:
 | 
			
		||||
    """Get the record for this key, raising InvalidCreationKey if non-None but invalid."""
 | 
			
		||||
    if creation_key is None:
 | 
			
		||||
        return None
 | 
			
		||||
    try:
 | 
			
		||||
        key_record = RealmCreationKey.objects.get(creation_key=creation_key)
 | 
			
		||||
    except RealmCreationKey.DoesNotExist:
 | 
			
		||||
        raise RealmCreationKey.Invalid()
 | 
			
		||||
    time_elapsed = timezone_now() - key_record.date_created
 | 
			
		||||
    if time_elapsed.total_seconds() > settings.REALM_CREATION_LINK_VALIDITY_DAYS * 24 * 3600:
 | 
			
		||||
        raise RealmCreationKey.Invalid()
 | 
			
		||||
    return key_record
 | 
			
		||||
def check_key_is_valid(creation_key):
 | 
			
		||||
    # type: (Text) -> bool
 | 
			
		||||
    if not RealmCreationKey.objects.filter(creation_key=creation_key).exists():
 | 
			
		||||
        return False
 | 
			
		||||
    days_sofar = (timezone_now() - RealmCreationKey.objects.get(creation_key=creation_key).date_created).days
 | 
			
		||||
    # Realm creation link expires after settings.REALM_CREATION_LINK_VALIDITY_DAYS
 | 
			
		||||
    if days_sofar <= settings.REALM_CREATION_LINK_VALIDITY_DAYS:
 | 
			
		||||
        return True
 | 
			
		||||
    return False
 | 
			
		||||
 | 
			
		||||
def generate_realm_creation_url(by_admin: bool=False) -> Text:
 | 
			
		||||
def generate_realm_creation_url():
 | 
			
		||||
    # type: () -> Text
 | 
			
		||||
    key = generate_key()
 | 
			
		||||
    RealmCreationKey.objects.create(creation_key=key,
 | 
			
		||||
                                    date_created=timezone_now(),
 | 
			
		||||
                                    presume_email_valid=by_admin)
 | 
			
		||||
    return '%s%s%s' % (settings.EXTERNAL_URI_SCHEME,
 | 
			
		||||
                       settings.EXTERNAL_HOST,
 | 
			
		||||
                       reverse('zerver.views.create_realm',
 | 
			
		||||
                               kwargs={'creation_key': key}))
 | 
			
		||||
    RealmCreationKey.objects.create(creation_key=key, date_created=timezone_now())
 | 
			
		||||
    return u'%s%s%s' % (settings.EXTERNAL_URI_SCHEME,
 | 
			
		||||
                        settings.EXTERNAL_HOST,
 | 
			
		||||
                        reverse('zerver.views.create_realm',
 | 
			
		||||
                                kwargs={'creation_key': key}))
 | 
			
		||||
 | 
			
		||||
class RealmCreationKey(models.Model):
 | 
			
		||||
    creation_key = models.CharField('activation key', max_length=40)
 | 
			
		||||
    date_created = models.DateTimeField('created', default=timezone_now)
 | 
			
		||||
 | 
			
		||||
    # True just if we should presume the email address the user enters
 | 
			
		||||
    # is theirs, and skip sending mail to it to confirm that.
 | 
			
		||||
    presume_email_valid = models.BooleanField(default=False)  # type: bool
 | 
			
		||||
 | 
			
		||||
    class Invalid(Exception):
 | 
			
		||||
        pass
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										31
									
								
								confirmation/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								confirmation/views.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
			
		||||
# -*- coding: utf-8 -*-
 | 
			
		||||
 | 
			
		||||
# Copyright: (c) 2008, Jarek Zgoda <jarek.zgoda@gmail.com>
 | 
			
		||||
 | 
			
		||||
__revision__ = '$Id: views.py 21 2008-12-05 09:21:03Z jarek.zgoda $'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
from django.shortcuts import render
 | 
			
		||||
from django.template import RequestContext
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.http import HttpRequest, HttpResponse
 | 
			
		||||
 | 
			
		||||
from confirmation.models import Confirmation, get_object_from_key, ConfirmationKeyException, \
 | 
			
		||||
    render_confirmation_key_error
 | 
			
		||||
from zerver.models import PreregistrationUser
 | 
			
		||||
 | 
			
		||||
from typing import Any, Dict
 | 
			
		||||
 | 
			
		||||
# This is currently only used for confirming PreregistrationUser.
 | 
			
		||||
# Do not add other confirmation paths here.
 | 
			
		||||
def confirm(request, confirmation_key):
 | 
			
		||||
    # type: (HttpRequest, str) -> HttpResponse
 | 
			
		||||
    try:
 | 
			
		||||
        get_object_from_key(confirmation_key)
 | 
			
		||||
    except ConfirmationKeyException as exception:
 | 
			
		||||
        return render_confirmation_key_error(request, exception)
 | 
			
		||||
 | 
			
		||||
    return render(request, 'confirmation/confirm_preregistrationuser.html',
 | 
			
		||||
                  context={
 | 
			
		||||
                      'key': confirmation_key,
 | 
			
		||||
                      'full_name': request.GET.get("full_name", None)})
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
from django.conf.urls import url
 | 
			
		||||
from django.views.generic import TemplateView
 | 
			
		||||
from django.views.generic import TemplateView, RedirectView
 | 
			
		||||
 | 
			
		||||
i18n_urlpatterns = [
 | 
			
		||||
    # Zephyr/MIT
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
#
 | 
			
		||||
 | 
			
		||||
# You can set these variables from the command line.
 | 
			
		||||
SPHINXOPTS    = -j8
 | 
			
		||||
SPHINXOPTS    =
 | 
			
		||||
SPHINXBUILD   = sphinx-build
 | 
			
		||||
PAPER         =
 | 
			
		||||
BUILDDIR      = _build
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
# Documentation systems
 | 
			
		||||
# Documentation
 | 
			
		||||
 | 
			
		||||
Zulip has three major documentation systems:
 | 
			
		||||
 | 
			
		||||
@@ -58,8 +58,7 @@ Markdown, though that won't be as faithful as the `make html`
 | 
			
		||||
approach.
 | 
			
		||||
 | 
			
		||||
When editing dependencies for the Zulip documentation, you should edit
 | 
			
		||||
`requirements/docs.in` and then run `tools/update-locked-requirements`
 | 
			
		||||
which updates docs.txt file (which is used by ReadTheDocs to build the
 | 
			
		||||
`requirements/docs.txt` (which is used by ReadTheDocs to build the
 | 
			
		||||
Zulip developer documentation, without installing all of Zulip's
 | 
			
		||||
dependencies).
 | 
			
		||||
 | 
			
		||||
@@ -37,7 +37,7 @@ Copyright: 2011, Krzysztof Wilczynski
 | 
			
		||||
 2011, Puppet Labs Inc
 | 
			
		||||
License: Apache-2.0
 | 
			
		||||
 | 
			
		||||
File: puppet/zulip_ops/files/mediawiki/Auth_remoteuser.php
 | 
			
		||||
File: puppet/zulip_internal/files/mediawiki/Auth_remoteuser.php
 | 
			
		||||
Copyright: 2006 Otheus Shelling
 | 
			
		||||
 2007 Rusty Burchfield
 | 
			
		||||
 2009 James Kinsman
 | 
			
		||||
@@ -59,24 +59,15 @@ Files: puppet/zulip/files/nagios_plugins/zulip_nagios_server/check_website_respo
 | 
			
		||||
Copyright: 2011 Chris Freeman
 | 
			
		||||
License: GPL-2.0
 | 
			
		||||
 | 
			
		||||
Files: puppet/zulip_ops/files/trac/cgi-bin/
 | 
			
		||||
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_ops/files/zulip-ec2-configure-interfaces
 | 
			
		||||
Files: puppet/zulip_internal/files/zulip-ec2-configure-interfaces
 | 
			
		||||
Copyright: 2013-2017, Dropbox, Inc., Kandra Labs, Inc., and contributors
 | 
			
		||||
License: Expat
 | 
			
		||||
 | 
			
		||||
Files: scripts/setup/generate-self-signed-cert
 | 
			
		||||
Copyright: 2003-2006 Thom May
 | 
			
		||||
           2006 Fabio M. Di Nitto
 | 
			
		||||
           2006 Adam Conrad
 | 
			
		||||
           2006-2008 Tollef Fog Heen
 | 
			
		||||
           2008-2015 Stefan Fritsch
 | 
			
		||||
           2018 Kandra Labs, Inc., and contributors
 | 
			
		||||
License: BSD-3-Clause
 | 
			
		||||
 | 
			
		||||
Files: static/audio/zulip.*
 | 
			
		||||
Copyright: 2011 Vidsyn
 | 
			
		||||
License: CC-0-1.0
 | 
			
		||||
@@ -191,10 +182,6 @@ Files: zerver/lib/decorator.py zerver/management/commands/runtornado.py scripts/
 | 
			
		||||
Copyright: Django Software Foundation and individual contributors
 | 
			
		||||
License: BSD-3-Clause
 | 
			
		||||
 | 
			
		||||
Files: zerver/lib/json_encoder_for_html.py zerver/tests/test_json_encoder_for_html.py
 | 
			
		||||
Copyright: 2006 Bob Ippolito
 | 
			
		||||
License: MIT or Academic Free License v. 2.1
 | 
			
		||||
 | 
			
		||||
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.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								docs/_static/zulip-create-realm.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/_static/zulip-create-realm.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 38 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								docs/_static/zulip-create-user-and-org.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/_static/zulip-create-user-and-org.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 40 KiB  | 
@@ -6,10 +6,11 @@ Key Codebases
 | 
			
		||||
 | 
			
		||||
The core Zulip application is at
 | 
			
		||||
[<https://github.com/zulip/zulip>](https://github.com/zulip/zulip) and
 | 
			
		||||
is a web application written in Python 3.x and using the Django framework. That
 | 
			
		||||
codebase includes server-side code and the web client, as well as Python API
 | 
			
		||||
bindings and most of our integrations with other services and applications (see
 | 
			
		||||
[the directory structure guide](../overview/directory-structure.html)).
 | 
			
		||||
is a web application written in Python 2.7 (soon to also support
 | 
			
		||||
Python 3) and using the Django framework. That codebase includes
 | 
			
		||||
server-side code and the web client, as well as Python API bindings
 | 
			
		||||
and most of our integrations with other services and applications (see
 | 
			
		||||
[the directory structure guide](directory-structure.html)).
 | 
			
		||||
 | 
			
		||||
[Zulip Mobile](https://github.com/zulip/zulip-mobile) is the official
 | 
			
		||||
mobile Zulip client supporting both iOS and Android, written in
 | 
			
		||||
@@ -56,12 +57,12 @@ choose whether to allow anyone to register an account and join, or
 | 
			
		||||
only allow people who have been invited, or restrict registrations to
 | 
			
		||||
members of particular groups (using email domain names or corporate
 | 
			
		||||
single-sign-on login for verification). For more on security
 | 
			
		||||
considerations, see [the security model section](../production/security-model.html).
 | 
			
		||||
considerations, see [the security model section](security-model.html).
 | 
			
		||||
 | 
			
		||||
The Zulip "All messages" screen is like a chronologically ordered inbox;
 | 
			
		||||
The default Zulip home screen is like a chronologically ordered inbox;
 | 
			
		||||
it displays messages, starting at the oldest message that the user
 | 
			
		||||
hasn't viewed yet (for more on that logic, see [the guide to the
 | 
			
		||||
pointer and unread counts](../subsystems/pointer.html)). The "All messages" screen displays
 | 
			
		||||
pointer and unread counts](pointer.html)). The home screen displays
 | 
			
		||||
the most recent messages in all the streams a user has joined (except
 | 
			
		||||
for the streams they've muted), as well as private messages from other
 | 
			
		||||
users, in strict chronological order. A user can *narrow* to view only
 | 
			
		||||
@@ -80,7 +81,7 @@ real-time notifications they find irrelevant.
 | 
			
		||||
Components
 | 
			
		||||
----------
 | 
			
		||||
 | 
			
		||||
  
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
### Django and Tornado
 | 
			
		||||
 | 
			
		||||
@@ -112,7 +113,7 @@ exception to this is that Zulip uses websockets through Tornado to
 | 
			
		||||
minimize latency on the code path for **sending** messages.
 | 
			
		||||
 | 
			
		||||
There is detailed documentation on the
 | 
			
		||||
[real-time push and event queue system](../subsystems/events-system.html); most of
 | 
			
		||||
[real-time push and event queue system](events-system.html); most of
 | 
			
		||||
the code is in `zerver/tornado`.
 | 
			
		||||
 | 
			
		||||
#### HTML templates, JavaScript, etc.
 | 
			
		||||
@@ -125,10 +126,10 @@ live-rendering HTML from JavaScript for things like the main message
 | 
			
		||||
feed.
 | 
			
		||||
 | 
			
		||||
For more details on the frontend, see our documentation on
 | 
			
		||||
[translation](../translating/translating.html),
 | 
			
		||||
[templates](../subsystems/html-templates.html),
 | 
			
		||||
[directory structure](../overview/directory-structure.html), and
 | 
			
		||||
[the static asset pipeline](../subsystems/front-end-build-process.html).
 | 
			
		||||
[translation](translating.html),
 | 
			
		||||
[templates](html-templates.html),
 | 
			
		||||
[directory structure](directory-structure.html), and
 | 
			
		||||
[the static asset pipeline](front-end-build-process.html).
 | 
			
		||||
 | 
			
		||||
[Jinja2]: http://jinja.pocoo.org/
 | 
			
		||||
[Handlebars]: http://handlebarsjs.com/
 | 
			
		||||
@@ -179,7 +180,7 @@ processes that process event queues. We use event queues for the kinds
 | 
			
		||||
of tasks that are best run in the background because they are
 | 
			
		||||
expensive (in terms of performance) and don't have to be synchronous
 | 
			
		||||
--- e.g., sending emails or updating analytics. Also see [the queuing
 | 
			
		||||
guide](../subsystems/queuing.html).
 | 
			
		||||
guide](queuing.html).
 | 
			
		||||
 | 
			
		||||
### memcached
 | 
			
		||||
 | 
			
		||||
@@ -226,7 +227,7 @@ processes started by Supervisor are queue processors that continually
 | 
			
		||||
pull things out of a RabbitMQ queue and handle them; they are defined
 | 
			
		||||
in `zerver/worker/queue_processors.py`.
 | 
			
		||||
 | 
			
		||||
Also see [the queuing guide](../subsystems/queuing.html).
 | 
			
		||||
Also see [the queuing guide](queuing.html).
 | 
			
		||||
 | 
			
		||||
### PostgreSQL
 | 
			
		||||
 | 
			
		||||
@@ -294,8 +295,8 @@ are welcome!
 | 
			
		||||
 | 
			
		||||
* **star**: Zulip allows a user to mark any message they can see,
 | 
			
		||||
    public or private, as "starred". A user can easily access messages
 | 
			
		||||
    they've starred through the "Starred messages" link in the
 | 
			
		||||
    left sidebar, or use "is:starred" as a narrow or a search
 | 
			
		||||
    they've starred through the "Starred messages" link in the menu
 | 
			
		||||
    near "Home", or use "is:starred" as a narrow or a search
 | 
			
		||||
    constraint. Whether a user has or has not starred a particular
 | 
			
		||||
    message is private; other users and realm admins don't know
 | 
			
		||||
    whether a message has been starred, or by whom.
 | 
			
		||||
							
								
								
									
										119
									
								
								docs/brief-install-vagrant-dev.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								docs/brief-install-vagrant-dev.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,119 @@
 | 
			
		||||
# Vagrant environment setup (in brief)
 | 
			
		||||
 | 
			
		||||
Start by cloning this repository: `git clone https://github.com/zulip/zulip.git`
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
  ```
 | 
			
		||||
  You may want to [configure sudo to be passwordless when using Vagrant LXC][avoiding-sudo].
 | 
			
		||||
 | 
			
		||||
* If your host is Ubuntu 14.04, you will need to [download a newer
 | 
			
		||||
  version of Vagrant][vagrant-dl], 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
 | 
			
		||||
  ```
 | 
			
		||||
  You may want to [configure sudo to be passwordless when using Vagrant LXC][avoiding-sudo].
 | 
			
		||||
 | 
			
		||||
* For other Linux hosts with a kernel above 3.12, [follow the Vagrant
 | 
			
		||||
  LXC installation instructions][vagrant-lxc] to get Vagrant with LXC
 | 
			
		||||
  for your platform.
 | 
			
		||||
 | 
			
		||||
* If your host is macOS or older Linux, [download Vagrant][vagrant-dl]
 | 
			
		||||
  and [VirtualBox][vbox-dl].  Or, instead of Virtualbox you can use
 | 
			
		||||
  [VMWare Fusion][vmware-fusion-dl] with the [VMWare vagrant
 | 
			
		||||
  provider][vagrant-vmware-fusion-dl] for a nonfree option with better
 | 
			
		||||
  performance.
 | 
			
		||||
 | 
			
		||||
* 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).
 | 
			
		||||
 | 
			
		||||
[vagrant-dl]: https://www.vagrantup.com/downloads.html
 | 
			
		||||
[vagrant-lxc]: https://github.com/fgrehm/vagrant-lxc
 | 
			
		||||
[vbox-dl]: https://www.virtualbox.org/wiki/Downloads
 | 
			
		||||
[vmware-fusion-dl]: http://www.vmware.com/products/fusion.html
 | 
			
		||||
[vagrant-vmware-fusion-dl]: https://www.vagrantup.com/vmware/
 | 
			
		||||
[avoiding-sudo]: https://github.com/fgrehm/vagrant-lxc#avoiding-sudo-passwords
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
# Now inside the container
 | 
			
		||||
/srv/zulip/tools/run-dev.py
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
To get shell access to the virtual machine running the server to run
 | 
			
		||||
lint, management commands, etc., use `vagrant ssh`.
 | 
			
		||||
 | 
			
		||||
At this point you should [read about using the development
 | 
			
		||||
environment][using-dev].
 | 
			
		||||
 | 
			
		||||
[using-dev]: using-dev-environment.html
 | 
			
		||||
 | 
			
		||||
### Specifying a proxy
 | 
			
		||||
 | 
			
		||||
If you need to use a proxy server to access the Internet, you will
 | 
			
		||||
need to specify the proxy settings before running `Vagrant up`.
 | 
			
		||||
First, install the Vagrant plugin `vagrant-proxyconf`:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
vagrant plugin install vagrant-proxyconf.
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Then create `~/.zulip-vagrant-config` and add the following lines to
 | 
			
		||||
it (with the appropriate values in it for your proxy):
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
HTTP_PROXY http://proxy_host:port
 | 
			
		||||
HTTPS_PROXY http://proxy_host:port
 | 
			
		||||
NO_PROXY localhost,127.0.0.1,.example.com
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Now run `vagrant up` in your terminal to install the development
 | 
			
		||||
server. If you ran `vagrant up` before and failed, you'll need to run
 | 
			
		||||
`vagrant destroy` first to clean up the failed installation.
 | 
			
		||||
 | 
			
		||||
You can also change the port on the host machine that Vagrant uses by
 | 
			
		||||
adding to your `~/.zulip-vagrant-config` file.  E.g. if you set:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
HOST_PORT 9971
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
(and halt and restart the Vagrant guest), then you would visit
 | 
			
		||||
http://localhost:9971/ to connect to your development server.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
If you'd like to be able to connect to your development environment from other
 | 
			
		||||
machines than the VM host, you can manually set the host IP address in the
 | 
			
		||||
'~/.zulip-vagrant-config' file as well. For example, if you set:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
HOST_IP_ADDR 0.0.0.0
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
(and restart the Vagrant guest), your host IP would be 0.0.0.0, a special value
 | 
			
		||||
for the IP address that means any IP address can connect to your development server.
 | 
			
		||||
@@ -7,159 +7,21 @@ All notable changes to the Zulip server are documented in this file.
 | 
			
		||||
This section lists notable unreleased changes; it is generally updated
 | 
			
		||||
in bursts.
 | 
			
		||||
 | 
			
		||||
**Highlights:**
 | 
			
		||||
### 1.7.2 -- 2018-04-12
 | 
			
		||||
 | 
			
		||||
- Added a user setting to choose the emoji set used in Zulip: Google,
 | 
			
		||||
  Twitter, Apple, or Emoji One.
 | 
			
		||||
- Added a video call integration powered by Jitsi.
 | 
			
		||||
- Dramatically simplified the server installation process; it's now possible
 | 
			
		||||
  to install Zulip without first setting up outgoing email.
 | 
			
		||||
- Added certbot support to the installer for getting certificates.
 | 
			
		||||
- Added support for mentioning groups of users.
 | 
			
		||||
- Added a new "night mode" theme for dark environments.
 | 
			
		||||
- Added experimental support for importing an organization from Slack.
 | 
			
		||||
- Overhauled our settings system to eliminate the ugly "save changes"
 | 
			
		||||
  button system.
 | 
			
		||||
- Rewrote our API documentation to be much more friendly and
 | 
			
		||||
  expansive; it now covers most important endpoints, with nice examples.
 | 
			
		||||
- The security model for private streams has changed.  Now
 | 
			
		||||
  organization administrators can remove users, edit descriptions, and
 | 
			
		||||
  rename private streams they are not subscribed to.  See Zulip's
 | 
			
		||||
  security model documentation for details.
 | 
			
		||||
- Lots of visual polish improvements.
 | 
			
		||||
This is a security release, with a handful of cherry-picked changes
 | 
			
		||||
since 1.7.1.  All Zulip server admins are encouraged to upgrade
 | 
			
		||||
promptly.
 | 
			
		||||
 | 
			
		||||
**Full feature changelog:**
 | 
			
		||||
- CVE-2018-9986: Fix XSS issues with frontend markdown processor.
 | 
			
		||||
- CVE-2018-9987: Fix XSS issue with muting notifications.
 | 
			
		||||
- CVE-2018-9990: Fix XSS issue with stream names in topic typeahead.
 | 
			
		||||
- CVE-2018-9999: Fix XSS issue with user uploads.  The fix for this
 | 
			
		||||
  adds a Content-Security-Policy for the `LOCAL_UPLOADS_DIR` storage
 | 
			
		||||
  backend for user-uploaded files.
 | 
			
		||||
 | 
			
		||||
- New integrations: ErrBot, GoCD, Google Code-In, Opbeat, Groove, Raygun,
 | 
			
		||||
  Insping, Dropbox, Front, Intercom, Statuspage.io, Flock and Beeminder.
 | 
			
		||||
- The local uploads backend now does the same security checks that the
 | 
			
		||||
  S3 backend did before serving files to users.
 | 
			
		||||
- Added support for users in multiple realms having the same email.
 | 
			
		||||
- Added support for embedded interactive bots.
 | 
			
		||||
- Added inline preview + player for Vimeo videos.
 | 
			
		||||
- Added a setting to allow users to delete their messages.
 | 
			
		||||
- Added an organization setting to limit creation of bots.
 | 
			
		||||
- Added support for uploading files in the message-edit UI.
 | 
			
		||||
- Added new event types to several webhook integrations.
 | 
			
		||||
- Added a display for whether the user is logged-in in logged-out
 | 
			
		||||
  pages.
 | 
			
		||||
- Added support for hosting multiple domains, not all as subdomains of
 | 
			
		||||
  the same base domain.
 | 
			
		||||
- Added a new /team/ page explaining the team, with a nice
 | 
			
		||||
  visualization of our contributors.
 | 
			
		||||
- Added support for default bots to receive messages when they're
 | 
			
		||||
mentioned, even if they are not subscribed.
 | 
			
		||||
- Added support for inviting a new user as an administrator.
 | 
			
		||||
- Added a new organization settings page for managing invites.
 | 
			
		||||
- Added a user setting to control whether the organization's name is
 | 
			
		||||
  included in email subject lines.
 | 
			
		||||
- Added support for clicking on a mention to see a user's profile.
 | 
			
		||||
- Added new compose features for pasting HTML.
 | 
			
		||||
- Redesigned the compose are for private messages to use pretty pills
 | 
			
		||||
  rather than raw email addresses to display recipients.
 | 
			
		||||
- Added new ctrl+B, ctrl+I, ctrl+L compose shortcuts for inserting
 | 
			
		||||
  common syntax.
 | 
			
		||||
- Added warning when linking to a private stream via typeahead.
 | 
			
		||||
- Added rate-limiting on inviting users to join a realm (prevents spam).
 | 
			
		||||
- Added support for automatically-numbered markdown lists.
 | 
			
		||||
- Added a big warning when posting to #announce.
 | 
			
		||||
- Added a user setting to control whether email notifications include
 | 
			
		||||
  message content (or just the fact that there are new messages).
 | 
			
		||||
- Added a notification when drafts are saved, to make them more
 | 
			
		||||
discoverable.
 | 
			
		||||
- Added a fast local echo to emoji reactions.
 | 
			
		||||
- Added new "basics" section to keyboard shortcuts documentation.
 | 
			
		||||
- Added a new ">" keyboard shortcut for quote-and-reply.
 | 
			
		||||
- Added a new "p" keyboard shortcut to just to next unread PM thread.
 | 
			
		||||
- Added support for overriding the topic is all incoming webhook integrations.
 | 
			
		||||
- Added a new nagios check for the Zulip analytics state.
 | 
			
		||||
- Added a menu item to mark all messages as read.
 | 
			
		||||
- Added support for logging into the mobile apps with RemoteUserBackend.
 | 
			
		||||
- Added an organization setting to disable welcome emails to new users.
 | 
			
		||||
- Added traffic statistics (messages/week) to the "Manage streams" UI.
 | 
			
		||||
- Added a display setting to translate emoticons/smileys to emoji.
 | 
			
		||||
- Added an organization setting to ban disposable email addresses
 | 
			
		||||
  (I.e.. those from sites like mailinator.com).
 | 
			
		||||
- Added a server setting to control whether digest emails are sent.
 | 
			
		||||
- Links to logged-in content in Zulip now take the user to the
 | 
			
		||||
  appropriate upload or view after a user logs in.
 | 
			
		||||
- Incoming webhooks now send a private message to the bot owner for
 | 
			
		||||
  more convenient testing.
 | 
			
		||||
- Rewrote documentation for many integrations to use a cleaner
 | 
			
		||||
  numbered-list format.
 | 
			
		||||
- Renamed "Home" to "All messages", to avoid users clicking on it too
 | 
			
		||||
  early in using Zulip.
 | 
			
		||||
- Messages containing just a link to an image (or an uploaded image)
 | 
			
		||||
  now don't clutter the feed with the URL: we just display the image.
 | 
			
		||||
- Refactored the authentication backends codebase to be much easier to
 | 
			
		||||
  verify.
 | 
			
		||||
- Expanded setting for displaying emoji as text to cover all display
 | 
			
		||||
  settings (previously only affected reactions).
 | 
			
		||||
- Redesigned the API for emoji reactions to support the full range of
 | 
			
		||||
  how emoji reactions are used.
 | 
			
		||||
- Migrated the codebase to use the nice Python 3 typing syntax.
 | 
			
		||||
- Optimized how user avatar URLs are transmitted over the wire.
 | 
			
		||||
- Optimized message sending performance a bit more.
 | 
			
		||||
- Split the Notifications Stream setting in two settings, one for new
 | 
			
		||||
  users, the other for new streams.
 | 
			
		||||
- Fixed numerous issues in the "stream settings" UI.
 | 
			
		||||
- Fixed most of the known (mostly obscure) bugs in how messages are
 | 
			
		||||
  formatted in Zulip.
 | 
			
		||||
- Fixed "more topics" to correctly display all historical topics for
 | 
			
		||||
  public streams, even though from before a user subscribed.
 | 
			
		||||
- Fixed several bugs around interacting with deactivated users.
 | 
			
		||||
- Fixed image upload file pickers offering non-image files.
 | 
			
		||||
- Fixed some subtle bugs with full-text search and unicode.
 | 
			
		||||
- Fixed bugs in the "edit history" HTML rendering process.
 | 
			
		||||
- Fixed several hotkeys scope bugs.
 | 
			
		||||
- Fixed popovers being closed when new messages come in.
 | 
			
		||||
- Fixed unexpected code blocks when using the email mirror.
 | 
			
		||||
- Fixed clicking on links to a narrow opening a new window.
 | 
			
		||||
- Fixed several subtle bugs with the email gateway system.
 | 
			
		||||
- Fixed layering issues with mobile Safari.
 | 
			
		||||
- Fixed several obscure real-time synchronization bugs.
 | 
			
		||||
- Fixed handling of messages with a very large HTML rendering.
 | 
			
		||||
- Fixed buggy APNs logic that could cause extra exception emails.
 | 
			
		||||
- Fixed interaction bugs with unread counts and deleting messages.
 | 
			
		||||
- Fixed support for replacing deactivated custom emoji.
 | 
			
		||||
- Fixed a missing dependency for the localhost_sso auth backend.
 | 
			
		||||
- Fixed uploading user avatars encoded using the CMYK mode.
 | 
			
		||||
- Fixed scrolling downwards in narrows.
 | 
			
		||||
- Fixed numerous subtle bugs with the stream creation UI.
 | 
			
		||||
- Dramatically improved organization of developer docs.
 | 
			
		||||
- Statistics on translation percentages now include mobile apps.
 | 
			
		||||
- Improved typeahead's handling of editing an already-completed mention.
 | 
			
		||||
- Improved syntax for inline LaTeX to be more convenient.
 | 
			
		||||
- Improve keyboard navigation of left and right sidebars with arrow keys.
 | 
			
		||||
- Changes the URL scheme for stream narrows to encode the stream ID,
 | 
			
		||||
  so that they can be robust to streams being renamed.  The change is
 | 
			
		||||
  backwards-compatible; existing narrow URLs still work.
 | 
			
		||||
- APIs for fetching messages now provide more metadata to help clients.
 | 
			
		||||
- Clarified instructions for server settings (especially LDAP auth).
 | 
			
		||||
- Redesigned the "uploaded files" UI.
 | 
			
		||||
- Redesigned the "account settings" UI.
 | 
			
		||||
- Redesigned error pages for the various email confirmation flows.
 | 
			
		||||
- Added missing information on requesting user in many exception emails.
 | 
			
		||||
- Our emoji now display at full resolution on retina displays.
 | 
			
		||||
- Improved placement of text when inserting emoji via picker.
 | 
			
		||||
- Improved the password reset flow to be less confusing if you don't
 | 
			
		||||
  have an account.
 | 
			
		||||
- Improved behavior of copy-pasting a large number of messages.
 | 
			
		||||
- Improved Tornado retry logic for connecting to RabbitMQ.
 | 
			
		||||
- Improved the descriptions and UI for many settings.
 | 
			
		||||
- Improved handling of browser undo in compose.
 | 
			
		||||
- Improved mobile notifications to support narrowing when one click a
 | 
			
		||||
  mobile push notification.
 | 
			
		||||
- Improved visual design of the help center (/help/).
 | 
			
		||||
- Improved saved drafts system to garbage-collect old drafts and sort
 | 
			
		||||
  by last modification, not creation.
 | 
			
		||||
- Removed the legacy "Zulip labs" autoscroll_forever setting.  It was
 | 
			
		||||
  enabled mostly by accident.
 | 
			
		||||
- Removed some long-deprecated markdown syntax for mentions.
 | 
			
		||||
- Statistics on the fraction of strings that are translated now
 | 
			
		||||
  include strings in the mobile apps as well.
 | 
			
		||||
- Backend test coverage is now 95%.
 | 
			
		||||
- Countless other little bug fixes both in the backend and the UI.
 | 
			
		||||
Thanks to Suhas Sunil Gaikwad for reporting CVE-2018-9987 and w2w for
 | 
			
		||||
reporting CVE-2018-9986 and CVE-2018-9990.
 | 
			
		||||
 | 
			
		||||
### 1.7.1 -- 2017-11-21
 | 
			
		||||
 | 
			
		||||
@@ -236,9 +98,9 @@ Backend and scaling
 | 
			
		||||
  minimizes disruption by running these first, before beginning the
 | 
			
		||||
  user-facing downtime.  However, if you'd like to watch the downtime
 | 
			
		||||
  phase of the upgrade closely, we recommend
 | 
			
		||||
  [running them first manually](../production/expensive-migrations.html) and as well
 | 
			
		||||
  [running them first manually](expensive-migrations.html) and as well
 | 
			
		||||
  as the usual trick of
 | 
			
		||||
  [doing an apt upgrade first](../production/maintain-secure-upgrade.html#applying-ubuntu-system-updates).
 | 
			
		||||
  [doing an apt upgrade first](prod-maintain-secure-upgrade.html#applying-ubuntu-system-updates).
 | 
			
		||||
 | 
			
		||||
* We've removed support for an uncommon legacy deployment model where
 | 
			
		||||
  a Zulip server served multiple organizations on the same domain.
 | 
			
		||||
@@ -248,7 +110,7 @@ Backend and scaling
 | 
			
		||||
  This change should have no effect for the vast majority of Zulip
 | 
			
		||||
  servers that only have one organization.  If you manage a server
 | 
			
		||||
  that hosts multiple organizations, you'll want to read [our guide on
 | 
			
		||||
  multiple organizations](../production/multiple-organizations.html).
 | 
			
		||||
  multiple organizations](prod-multiple-organizations.html).
 | 
			
		||||
 | 
			
		||||
* We simplified the configuration for our password strength checker to
 | 
			
		||||
  be much more intuitive.  If you were using the
 | 
			
		||||
@@ -396,7 +258,7 @@ Zulip apps.
 | 
			
		||||
  Hungarian, Polish, Dutch, Russian, Bulgarian, Portuguese,
 | 
			
		||||
  Serbian, Malayalam, Korean, and Italian).
 | 
			
		||||
 | 
			
		||||
[mobile-push]: ../production/mobile-push-notifications.html
 | 
			
		||||
[mobile-push]: https://zulip.readthedocs.io/en/latest/prod-mobile-push-notifications.html
 | 
			
		||||
[electron-app]: https://github.com/zulip/zulip-electron/releases
 | 
			
		||||
[ios-app]: https://itunes.apple.com/us/app/zulip/id1203036395
 | 
			
		||||
 | 
			
		||||
@@ -1,20 +1,32 @@
 | 
			
		||||
# The chat.zulip.org community
 | 
			
		||||
 | 
			
		||||
[https://chat.zulip.org](https://chat.zulip.org/) is the primary communication
 | 
			
		||||
forum for the Zulip community.
 | 
			
		||||
[chat.zulip.org](https://chat.zulip.org/) is the primary communication
 | 
			
		||||
forum for the Zulip community.  It is a Zulip server that you can
 | 
			
		||||
connect to from any modern web browser.
 | 
			
		||||
 | 
			
		||||
You can go through the simple signup process at that link, and then
 | 
			
		||||
you will soon be talking to core Zulip developers and other users.  To
 | 
			
		||||
get help in real time, you will have the best luck finding core
 | 
			
		||||
developers roughly between 17:00 UTC and 6:00 UTC, but the sun never
 | 
			
		||||
sets on the Zulip community.  Most questions get a reply within
 | 
			
		||||
developers roughly between 17:00 UTC and 2:00 UTC or during [office
 | 
			
		||||
hours and sprints](#office-hours-and-sprints), but the sun never
 | 
			
		||||
sleeps on the Zulip community.  Most questions get a reply within
 | 
			
		||||
minutes to a few hours, depending on the time of day.
 | 
			
		||||
 | 
			
		||||
## Community norms
 | 
			
		||||
## This is a bleeding edge development server
 | 
			
		||||
 | 
			
		||||
* Send test messages to
 | 
			
		||||
The chat.zulip.org server is frequently deployed off of `master` from
 | 
			
		||||
the Zulip Git repository, so please point out anything you notice that
 | 
			
		||||
seems wrong!  We catch many bugs that escape code review this way.
 | 
			
		||||
 | 
			
		||||
The chat.zulip.org server is a development and testing server, not a
 | 
			
		||||
production service, so don't use it for anything mission-critical,
 | 
			
		||||
secret/embarrassing, etc.
 | 
			
		||||
 | 
			
		||||
## Community conventions
 | 
			
		||||
 | 
			
		||||
* Send any test messages to
 | 
			
		||||
  [#test here](https://chat.zulip.org/#narrow/stream/test.20here) or
 | 
			
		||||
  as a PM to yourself to avoid disturbing others.
 | 
			
		||||
  as a PM to yourself to avoid disrupting others.
 | 
			
		||||
* When asking for help, provide the details needed for others to help
 | 
			
		||||
  you.  E.g. include the full traceback in a code block (not a
 | 
			
		||||
  screenshot), a link to the code or a WIP PR you're having trouble
 | 
			
		||||
@@ -28,11 +40,8 @@ minutes to a few hours, depending on the time of day.
 | 
			
		||||
  Mentioning other users is great for timely questions or making sure
 | 
			
		||||
  someone who is not online sees your message.
 | 
			
		||||
* Converse informally; there's no need to use titles like "Sir" or "Madam".
 | 
			
		||||
* Use
 | 
			
		||||
  [gender-neutral language](https://en.wikipedia.org/wiki/Gender-neutral_language).
 | 
			
		||||
  For example, avoid using a pronoun like her or his in sentences like
 | 
			
		||||
  "Every developer should clean [their] keyboard at least once a week."
 | 
			
		||||
* Follow the community [code of conduct](../code-of-conduct.html).
 | 
			
		||||
* Use gender-neutral language.
 | 
			
		||||
* Follow the [community code of conduct](code-of-conduct.html).
 | 
			
		||||
* Participate!  Zulip is a friendly and welcoming community, and we
 | 
			
		||||
  love meeting new people, hearing about what brought them to Zulip,
 | 
			
		||||
  and getting their feedback.  If you're not sure where to start,
 | 
			
		||||
@@ -52,16 +61,6 @@ To make the best use of your time, we highly recommend that you
 | 
			
		||||
unsubscribe from streams that you aren't interested in, and mute
 | 
			
		||||
streams that are only of occasional interest.
 | 
			
		||||
 | 
			
		||||
## This is a bleeding edge development server
 | 
			
		||||
 | 
			
		||||
The chat.zulip.org server is frequently deployed off of `master` from
 | 
			
		||||
the Zulip Git repository, so please point out anything you notice that
 | 
			
		||||
seems wrong!  We catch many bugs that escape code review this way.
 | 
			
		||||
 | 
			
		||||
The chat.zulip.org server is a development and testing server, not a
 | 
			
		||||
production service, so don't use it for anything mission-critical,
 | 
			
		||||
secret/embarrassing, etc.
 | 
			
		||||
 | 
			
		||||
## Streams
 | 
			
		||||
 | 
			
		||||
There are a few streams worth highlighting that are relevant for
 | 
			
		||||
@@ -73,7 +72,7 @@ everyone, even non-developers:
 | 
			
		||||
* [#feedback](https://chat.zulip.org/#narrow/stream/feedback) is for
 | 
			
		||||
  posting feedback on Zulip.
 | 
			
		||||
* [#design](https://chat.zulip.org/#narrow/stream/design) is where we
 | 
			
		||||
  discuss UI and feature design and collect feedback on potential design
 | 
			
		||||
  discuss the UI design and collect feedback on potential design
 | 
			
		||||
  changes.  We love feedback, so don't hesitate to speak up!
 | 
			
		||||
* [#user community](https://chat.zulip.org/#narrow/stream/user.20community) is
 | 
			
		||||
  for Zulip users to discuss their experiences using and adopting Zulip.
 | 
			
		||||
@@ -124,16 +123,45 @@ meetings, and they're a great time to stop by and introduce yourself
 | 
			
		||||
if you'd like to get involved (though really, any time is, so).
 | 
			
		||||
 | 
			
		||||
Here are the regular meetings that exist today along with their usual
 | 
			
		||||
times:
 | 
			
		||||
times (actual times are listed in the linked agenda documents):
 | 
			
		||||
 | 
			
		||||
* Mobile team on
 | 
			
		||||
[#mobile](https://chat.zulip.org/#narrow/stream/mobile), generally
 | 
			
		||||
Wednesdays at 10AM Pacific time.
 | 
			
		||||
Wednesdays at 10AM Pacific time.  [Agendas][mobile-agendas].
 | 
			
		||||
 | 
			
		||||
* Backend/infrastructure team on
 | 
			
		||||
[#backend](https://chat.zulip.org/#narrow/stream/backend), generally
 | 
			
		||||
Fridays at 10AM Pacific time.
 | 
			
		||||
Fridays at 10AM Pacific time.  [Agendas][infra-agendas].
 | 
			
		||||
 | 
			
		||||
* Bots and integrations team on
 | 
			
		||||
[#integrations](https://chat.zulip.org/#narrow/stream/integrations),
 | 
			
		||||
generally Fridays at 9AM Pacific time.
 | 
			
		||||
generally Fridays at 9AM Pacific time.  [Agendas][bots-agendas].
 | 
			
		||||
 | 
			
		||||
[mobile-agendas]: https://paper.dropbox.com/doc/Zulip-mobile-agendas-nVdb9I7SDiom9hY8Zw8Ge
 | 
			
		||||
[infra-agendas]: https://paper.dropbox.com/doc/Zulip-infrastructure-team-agendas-kGyCvF2u2kLcZ1Hzyd9iD
 | 
			
		||||
[bots-agendas]: https://paper.dropbox.com/doc/Zulip-bots-and-integrations-agendas-3MR8NAL3fg4tIEpfb5jyx
 | 
			
		||||
 | 
			
		||||
### Office hours and sprints
 | 
			
		||||
 | 
			
		||||
We also do project-wide ad-hoc "office hours" and remote sprints
 | 
			
		||||
irregularly, about once a month.
 | 
			
		||||
 | 
			
		||||
Anyone can schedule one: announce it in
 | 
			
		||||
[#announce](https://chat.zulip.org/#narrow/stream/announce) and on
 | 
			
		||||
[the zulip-devel mailing list](https://groups.google.com/forum/#!forum/zulip-devel)
 | 
			
		||||
a few days ahead of time, and ideally, tell
 | 
			
		||||
[Sumana](https://chat.zulip.org/#narrow/sender/18-sh) so she can put
 | 
			
		||||
it on [the public Zulip meetings calendar][meetings-calendar].
 | 
			
		||||
 | 
			
		||||
*Office hours* are simply times for us to informally discuss current
 | 
			
		||||
global project priorities, find out what questions people have, and so
 | 
			
		||||
on. We set them up so people know there'll be more people around at a
 | 
			
		||||
particular time to chat. You don't need to RSVP and you don't need to
 | 
			
		||||
show up on time or stop conversations when the "hour" stops. They
 | 
			
		||||
start in [#general](https://chat.zulip.org/#narrow/stream/general) and
 | 
			
		||||
conversations move into other streams and topics as they come up.
 | 
			
		||||
 | 
			
		||||
*Sprints* are times when Zulip developers get together in chat, and
 | 
			
		||||
sometimes in person, to work on related issues at the same time.
 | 
			
		||||
 | 
			
		||||
[meetings-calendar]: https://calendar.google.com/calendar/embed?src=ktiduof4eoh47lmgcl2qunnc0o@group.calendar.google.com
 | 
			
		||||
@@ -72,7 +72,7 @@ group. However, "讨论组" has one more Chinese character than "频道
 | 
			
		||||
* Invite-Only/Public Stream - **私有/公开频道**
 | 
			
		||||
 | 
			
		||||
"Invite-Only Stream" requires users must be invited explicitly to
 | 
			
		||||
subscribe, which assures a high privacy. Other users cannot perceive
 | 
			
		||||
subscribe, which assures a high privacy. Other users can not perceive
 | 
			
		||||
the presence of such streams. Since literal translation is hard to
 | 
			
		||||
read, it is translated sense to sense as "私有频道(Private Stream)"。
 | 
			
		||||
 | 
			
		||||
@@ -35,4 +35,4 @@ object as `request.client`.
 | 
			
		||||
In most integrations, `request.client` is then passed to
 | 
			
		||||
`check_send_stream_message`, where it is used to keep track of which client
 | 
			
		||||
sent the message (which in turn is used by analytics). For more
 | 
			
		||||
information, see [the webhook walkthrough](https://zulipchat.com/api/webhook-walkthrough).
 | 
			
		||||
information, see [the webhook walkthrough](webhook-walkthrough.html).
 | 
			
		||||
@@ -103,7 +103,7 @@ this?". Good choices include
 | 
			
		||||
  change being made.  Tests that exclude whole classes of potential
 | 
			
		||||
  bugs are preferred when possible (e.g., the common test suite
 | 
			
		||||
  `test_bugdown.py` between the Zulip server's [frontend and backend
 | 
			
		||||
  Markdown processors](../subsystems/markdown.html), or the `GetEventsTest` test for
 | 
			
		||||
  Markdown processors](markdown.html), or the `GetEventsTest` test for
 | 
			
		||||
  buggy race condition handling).
 | 
			
		||||
 | 
			
		||||
* *Translation.* Make sure that the strings are marked for
 | 
			
		||||
@@ -192,11 +192,11 @@ We also strongly recommend reviewers to go through the following resources.
 | 
			
		||||
* [Code Review - A consolidation of advice and stuff from the
 | 
			
		||||
   sinternet](https://gist.github.com/porterjamesj/002fb27dd70df003646df46f15e898de)
 | 
			
		||||
  article by James J. Porter
 | 
			
		||||
* [Zulip Code of Conduct](../code-of-conduct.html)
 | 
			
		||||
* [Zulip Code of Conduct](https://zulip.readthedocs.io/en/latest/code-of-conduct.html)
 | 
			
		||||
 | 
			
		||||
[code-style]: ../contributing/code-style.html
 | 
			
		||||
[commit-messages]: ../contributing/version-control.html#commit-messages
 | 
			
		||||
[test-writing]: ../testing/testing.html
 | 
			
		||||
[mypy]: ../contributing/mypy.html
 | 
			
		||||
[git tool]: ../git/zulip-tools.html#fetch-a-pull-request-and-rebase
 | 
			
		||||
[translation]: ../translating/translating.html
 | 
			
		||||
[code-style]: code-style.html
 | 
			
		||||
[commit-messages]: version-control.html#commit-messages
 | 
			
		||||
[test-writing]: testing.html
 | 
			
		||||
[mypy]: mypy.html
 | 
			
		||||
[git tool]: git-guide.html#fetch-a-pull-request-and-rebase
 | 
			
		||||
[translation]: translating.html
 | 
			
		||||
@@ -163,7 +163,7 @@ Don't use it:
 | 
			
		||||
### Translation tags
 | 
			
		||||
 | 
			
		||||
Remember to
 | 
			
		||||
[tag all user-facing strings for translation](../translating/translating.html), whether
 | 
			
		||||
[tag all user-facing strings for translation](translating.html), whether
 | 
			
		||||
they are in HTML templates or JavaScript editing the HTML (e.g. error
 | 
			
		||||
messages).
 | 
			
		||||
 | 
			
		||||
@@ -217,7 +217,7 @@ code a lot uglier, in which case it's fine to go up to 120 or so.
 | 
			
		||||
When calling a function with an anonymous function as an argument, use
 | 
			
		||||
this style:
 | 
			
		||||
 | 
			
		||||
    my_function('foo', function (data) {
 | 
			
		||||
    $.get('foo', function (data) {
 | 
			
		||||
        var x = ...;
 | 
			
		||||
        // ...
 | 
			
		||||
    });
 | 
			
		||||
@@ -292,5 +292,5 @@ All significant new features should come with tests. See testing.
 | 
			
		||||
 | 
			
		||||
### Third party code
 | 
			
		||||
 | 
			
		||||
See [our docs on dependencies](../subsystems/dependencies.html) for discussion of
 | 
			
		||||
See [our docs on dependencies](dependencies.html) for discussion of
 | 
			
		||||
rules about integrating third-party projects.
 | 
			
		||||
							
								
								
									
										64
									
								
								docs/conf.py
									
									
									
									
									
								
							
							
						
						
									
										64
									
								
								docs/conf.py
									
									
									
									
									
								
							@@ -15,8 +15,8 @@
 | 
			
		||||
import sys
 | 
			
		||||
import os
 | 
			
		||||
import shlex
 | 
			
		||||
 | 
			
		||||
from typing import Any, Dict, List, Optional
 | 
			
		||||
if False:
 | 
			
		||||
    from typing import Any, Dict, List, Optional
 | 
			
		||||
 | 
			
		||||
# 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
 | 
			
		||||
@@ -43,23 +43,18 @@ templates_path = ['_templates']
 | 
			
		||||
master_doc = 'index'
 | 
			
		||||
 | 
			
		||||
# General information about the project.
 | 
			
		||||
project = 'Zulip'
 | 
			
		||||
copyright = '2015-2017, The Zulip Team'
 | 
			
		||||
author = 'The Zulip Team'
 | 
			
		||||
project = u'Zulip'
 | 
			
		||||
copyright = u'2015-2017, 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 = '1.7+git'
 | 
			
		||||
version = '1.7'
 | 
			
		||||
# The full version, including alpha/beta/rc tags.
 | 
			
		||||
release = '1.7.1+git'
 | 
			
		||||
 | 
			
		||||
# This allows us to insert a warning that appears only on an unreleased
 | 
			
		||||
# version, e.g. to say that something is likely to have changed.
 | 
			
		||||
if release.endswith('+git'):
 | 
			
		||||
    tags.add('unreleased')
 | 
			
		||||
release = '1.7.2'
 | 
			
		||||
 | 
			
		||||
# The language for content autogenerated by Sphinx. Refer to documentation
 | 
			
		||||
# for a list of supported languages.
 | 
			
		||||
@@ -118,18 +113,11 @@ if not on_rtd:
 | 
			
		||||
    import sphinx_rtd_theme
 | 
			
		||||
    html_theme = 'sphinx_rtd_theme'
 | 
			
		||||
    html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
 | 
			
		||||
else:
 | 
			
		||||
    html_theme = 'sphinx_rtd_theme'
 | 
			
		||||
    html_style = None
 | 
			
		||||
    html_theme_options = {'collapse_navigation': False}
 | 
			
		||||
    using_rtd_theme = True
 | 
			
		||||
 | 
			
		||||
# 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 = {
 | 
			
		||||
    'collapse_navigation': False,
 | 
			
		||||
}
 | 
			
		||||
#html_theme_options = {}
 | 
			
		||||
 | 
			
		||||
# Add any paths that contain custom themes here, relative to this directory.
 | 
			
		||||
#html_theme_path = []
 | 
			
		||||
@@ -218,6 +206,12 @@ html_static_path = ['_static']
 | 
			
		||||
# Output file base name for HTML help builder.
 | 
			
		||||
htmlhelp_basename = 'zulip-contributor-docsdoc'
 | 
			
		||||
 | 
			
		||||
def setup(app):
 | 
			
		||||
    # type: (Any) -> None
 | 
			
		||||
 | 
			
		||||
    # overrides for wide tables in RTD theme
 | 
			
		||||
    app.add_stylesheet('theme_overrides.css')  # path relative to _static
 | 
			
		||||
 | 
			
		||||
# -- Options for LaTeX output ---------------------------------------------
 | 
			
		||||
 | 
			
		||||
latex_elements = {
 | 
			
		||||
@@ -238,8 +232,8 @@ latex_elements = {
 | 
			
		||||
# (source start file, target name, title,
 | 
			
		||||
#  author, documentclass [howto, manual, or own class]).
 | 
			
		||||
latex_documents = [
 | 
			
		||||
    (master_doc, 'zulip-contributor-docs.tex', 'Zulip Documentation',
 | 
			
		||||
     'The Zulip Team', 'manual'),
 | 
			
		||||
    (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
 | 
			
		||||
@@ -268,7 +262,7 @@ latex_documents = [
 | 
			
		||||
# One entry per manual page. List of tuples
 | 
			
		||||
# (source start file, name, description, authors, manual section).
 | 
			
		||||
man_pages = [
 | 
			
		||||
    (master_doc, 'zulip-contributor-docs', 'Zulip Documentation',
 | 
			
		||||
    (master_doc, 'zulip-contributor-docs', u'Zulip Documentation',
 | 
			
		||||
     [author], 1)
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
@@ -282,7 +276,7 @@ man_pages = [
 | 
			
		||||
# (source start file, target name, title, author,
 | 
			
		||||
#  dir menu entry, description, category)
 | 
			
		||||
texinfo_documents = [
 | 
			
		||||
    (master_doc, 'zulip-contributor-docs', 'Zulip Documentation',
 | 
			
		||||
    (master_doc, 'zulip-contributor-docs', u'Zulip Documentation',
 | 
			
		||||
     author, 'zulip-contributor-docs', 'Documentation for contributing to Zulip.',
 | 
			
		||||
     'Miscellaneous'),
 | 
			
		||||
]
 | 
			
		||||
@@ -300,7 +294,6 @@ texinfo_documents = [
 | 
			
		||||
#texinfo_no_detailmenu = False
 | 
			
		||||
 | 
			
		||||
from recommonmark.parser import CommonMarkParser
 | 
			
		||||
from recommonmark.transform import AutoStructify
 | 
			
		||||
 | 
			
		||||
source_parsers = {
 | 
			
		||||
    '.md': CommonMarkParser,
 | 
			
		||||
@@ -309,24 +302,3 @@ source_parsers = {
 | 
			
		||||
# The suffix(es) of source filenames.
 | 
			
		||||
# You can specify multiple suffix as a list of string:
 | 
			
		||||
source_suffix = ['.rst', '.md']
 | 
			
		||||
 | 
			
		||||
def setup(app: Any) -> None:
 | 
			
		||||
 | 
			
		||||
    app.add_config_value('recommonmark_config', {
 | 
			
		||||
        'enable_eval_rst': True,
 | 
			
		||||
        # Turn off recommonmark features we aren't using.
 | 
			
		||||
        'enable_auto_doc_ref': False,
 | 
			
		||||
        'auto_toc_tree_section': None,
 | 
			
		||||
        'enable_auto_toc_tree': False,
 | 
			
		||||
        'enable_math': False,
 | 
			
		||||
        'enable_inline_math': False,
 | 
			
		||||
        'url_resolver': lambda x: x,
 | 
			
		||||
    }, True)
 | 
			
		||||
 | 
			
		||||
    # Enable `eval_rst`, and any other features enabled in recommonmark_config.
 | 
			
		||||
    # Docs: http://recommonmark.readthedocs.io/en/latest/auto_structify.html
 | 
			
		||||
    # (But NB those docs are for master, not latest release.)
 | 
			
		||||
    app.add_transform(AutoStructify)
 | 
			
		||||
 | 
			
		||||
    # overrides for wide tables in RTD theme
 | 
			
		||||
    app.add_stylesheet('theme_overrides.css')  # path relative to _static
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +0,0 @@
 | 
			
		||||
#######################
 | 
			
		||||
Code Contribution Guide
 | 
			
		||||
#######################
 | 
			
		||||
 | 
			
		||||
.. toctree::
 | 
			
		||||
   :maxdepth: 3
 | 
			
		||||
 | 
			
		||||
   version-control
 | 
			
		||||
   code-style
 | 
			
		||||
   mypy
 | 
			
		||||
   code-reviewing
 | 
			
		||||
   chat-zulip-org
 | 
			
		||||
   zulipbot-usage
 | 
			
		||||
   accessibility
 | 
			
		||||
   bug-reports
 | 
			
		||||
   ../code-of-conduct
 | 
			
		||||
   summer-with-zulip
 | 
			
		||||
@@ -1,285 +0,0 @@
 | 
			
		||||
# How to have an amazing summer with Zulip
 | 
			
		||||
 | 
			
		||||
The purpose of this doc is to provide advice to GSoC/ZSoC mentors and students
 | 
			
		||||
on how to make the summer as successful as possible. It's mandatory reading, in
 | 
			
		||||
addition to [Google's
 | 
			
		||||
materials](https://developers.google.com/open-source/gsoc/resources/manual).
 | 
			
		||||
 | 
			
		||||
- Don't focus too much on doing precisely what's in the project proposal or
 | 
			
		||||
  following precisely that schedule. The goals are for students to learn and to
 | 
			
		||||
  advance Zulip, not to do in July what we guessed would be the right plan in
 | 
			
		||||
  March with limited information.
 | 
			
		||||
 | 
			
		||||
  - We probably will want to create a Dropbox Paper document for each student to
 | 
			
		||||
    keep track of the current version of their project plan, but make sure to
 | 
			
		||||
    keep GitHub up to date with what issues you're working on.
 | 
			
		||||
 | 
			
		||||
  - Claim issues using zulipbot only when you actually start work on them. And
 | 
			
		||||
    if someone else fixes an issue you were planning to fix, don't worry about
 | 
			
		||||
    it! It's great for Zulip that the project was finished, and there's plenty
 | 
			
		||||
    of issues to work on :D. You can help review their work to build
 | 
			
		||||
    your expertise in the subsystem you're working on.
 | 
			
		||||
 | 
			
		||||
  - Look for, claim, and fix bugs to help keep Zulip polished. Bugs and polish
 | 
			
		||||
    are usually more important to users than new features.
 | 
			
		||||
 | 
			
		||||
  - Help test new features! It's fun, and one of the most valuable
 | 
			
		||||
    ways one can contribute to any software project is finding bugs in
 | 
			
		||||
    it before they reach a lot of users :).
 | 
			
		||||
 | 
			
		||||
  - Participate and be helpful in the community! Helping a new Zulip server
 | 
			
		||||
    administrator debug their installation problem or playing with the mobile
 | 
			
		||||
    app until you can get something to break are great ways to contribute.
 | 
			
		||||
 | 
			
		||||
- Mentors and students should stay in close contact, both with each other and
 | 
			
		||||
  the rest of the Zulip community. We recommend the following:
 | 
			
		||||
 | 
			
		||||
  - Daily checkins on #checkins on chat.zulip.org; ideally at some time of day
 | 
			
		||||
    you can both be online, but when not possible, async is better than nothing!
 | 
			
		||||
 | 
			
		||||
    - We prefer checkins in public streams, since it makes easier for
 | 
			
		||||
      other contributors to keep track of what everyone else is
 | 
			
		||||
      working on and share ideas (and helps organization leadership
 | 
			
		||||
      keep track of progress). Though, of course, feel free to have
 | 
			
		||||
      much more involved/detailed discussions privately as well.
 | 
			
		||||
 | 
			
		||||
    - If a mentor will be traveling or otherwise offline, mentors should make
 | 
			
		||||
      sure another mentor is paying attention in the meantime.
 | 
			
		||||
 | 
			
		||||
  - Video calls are great! Mentors should do 1-2 video calls with their students
 | 
			
		||||
    calls per week, depending on length, schedules, and what's happening.
 | 
			
		||||
 | 
			
		||||
  - Make sure to talk about not just the current project, but also meta-issues
 | 
			
		||||
    like your development process, where things are getting stuck, skills you
 | 
			
		||||
    need help learning, and time-saving tricks.
 | 
			
		||||
 | 
			
		||||
  - If you need feedback from the community / decisions made, ask in the
 | 
			
		||||
    appropriate public stream on [chat.zulip.org](http://chat.zulip.org). Often
 | 
			
		||||
    someone can provide important context that you need to succeed in your
 | 
			
		||||
    project.
 | 
			
		||||
 | 
			
		||||
  - Communicate clearly, especially in public places! You'll get much more
 | 
			
		||||
    useful feedback to a well-written Zulip message or GitHub issue comment than
 | 
			
		||||
    one that is unclear.
 | 
			
		||||
 | 
			
		||||
    - Be sure to mention any concerns you have with your own work!
 | 
			
		||||
 | 
			
		||||
    - Talk with your mentor about the status of your various projects and where
 | 
			
		||||
      they're stuck.
 | 
			
		||||
 | 
			
		||||
    - And when you update your PR having addressed a set of review feedback, be
 | 
			
		||||
      clear about which issues you've resolved (and how!) and
 | 
			
		||||
      especially any that you haven't yet (this helps code reviewers
 | 
			
		||||
      use their time well).
 | 
			
		||||
 | 
			
		||||
    - Post screenshots and/or brief videos of UI changes; a picture can be worth
 | 
			
		||||
      1000 words, especially for verifying whether a design change is
 | 
			
		||||
      working as intended.
 | 
			
		||||
 | 
			
		||||
    - Use #design and similar forums to get feedback on issues where we need
 | 
			
		||||
      community consensus on what something should look like or how it
 | 
			
		||||
      should work.
 | 
			
		||||
 | 
			
		||||
  - Bring up problems early, whether technical or otherwise. If you
 | 
			
		||||
    find you're stressed about something, mention it your mentor
 | 
			
		||||
    immediately, so they can help you solve the problem. If you're
 | 
			
		||||
    stressed about something involving your mentor, bring it up with
 | 
			
		||||
    an organization admin.
 | 
			
		||||
 | 
			
		||||
  - Join Zulip's GitHub teams that relate to your projects and/or interests, so
 | 
			
		||||
    that you see new issues and PRs coming in that are relevant to your work.
 | 
			
		||||
    You can browse the area teams here:
 | 
			
		||||
    https://github.com/orgs/zulip/teams (You need to be a member of
 | 
			
		||||
    the Zulip organization to see them; ask Tim for an invite if needed).
 | 
			
		||||
 | 
			
		||||
- Everyone's goal is to avoid students ending up blocked and feeling stuck.
 | 
			
		||||
  There are lots of things that students can do (and mentors can help them to)
 | 
			
		||||
  to avoid this:
 | 
			
		||||
 | 
			
		||||
  - Get really good at using `git rebase -i` to produce a really clean
 | 
			
		||||
    commit history that's fast to review. We occasionally do workshops
 | 
			
		||||
    on how to do relatively complex rebases.
 | 
			
		||||
 | 
			
		||||
  - Work on multiple parallelizable projects (or parts of projects) at a time.
 | 
			
		||||
    This can help avoid being stuck while waiting for something to be reviewed.
 | 
			
		||||
 | 
			
		||||
    - It can help to plan a bit in advance; if your next project requires some
 | 
			
		||||
      UX decisions to be made with the community, start the conversation a few
 | 
			
		||||
      days before you need an answer. Or do some preparatory refactoring that
 | 
			
		||||
      will make the feature easier to complete and can be merged without making
 | 
			
		||||
      all the decisions.
 | 
			
		||||
 | 
			
		||||
    - Think about how to test your changes.
 | 
			
		||||
 | 
			
		||||
  - Among your various projects, prioritize as follows:
 | 
			
		||||
 | 
			
		||||
    - (1) Fixing regressions you introduced with recently merged work (and other
 | 
			
		||||
      bugs you notice).
 | 
			
		||||
 | 
			
		||||
    - (2) Responding to code review feedback and fixing your in-flight branches
 | 
			
		||||
      over starting new work. Unmerged PRs develop painful merge conflicts
 | 
			
		||||
      pretty quickly, so you'll do much less total work per feature if you're
 | 
			
		||||
      responsive and try to make it easy for maintainers to merge your commits.
 | 
			
		||||
 | 
			
		||||
    - (3) Do any relevant follow-ups to larger projects you've completed, to
 | 
			
		||||
      make sure that you've left things better than how you found them.
 | 
			
		||||
 | 
			
		||||
    - (4) Starting on the next project.
 | 
			
		||||
 | 
			
		||||
  - Figure out a QA/testing process that works for you, and be sure to explain
 | 
			
		||||
    in your PRs how you've tested your changes. Most of the time, in a large
 | 
			
		||||
    open source project, is spent looking for and fixing regressions, and it
 | 
			
		||||
    saves everyone time when bugs can be fixed before the code is reviewed, or
 | 
			
		||||
    barring that, before it's merged.
 | 
			
		||||
 | 
			
		||||
  - Plan (and if when planning fails, rebase) your branches until they are easy
 | 
			
		||||
    to merge partially (i.e. merging just the first commit will not make Zulip
 | 
			
		||||
    worse or break the tests). Ideally, when reviewing a branch of yours, the
 | 
			
		||||
    maintainer should be able to merge the first few commits and leave comments
 | 
			
		||||
    on the rest. This is by far the most efficient way to do collaborative
 | 
			
		||||
    development, since one is constantly making progress, we keep branches
 | 
			
		||||
    small, and developers don't end up reviewing the easily merged parts of a PR
 | 
			
		||||
    repeatedly.
 | 
			
		||||
 | 
			
		||||
    - Look at Steve Howell's closed PRs to get a feel for how to do this well
 | 
			
		||||
      for even complex changes.
 | 
			
		||||
 | 
			
		||||
    - Or Eklavya Sharma's (from GSoC 2016) to see a fellow GSoC student doing
 | 
			
		||||
      this well. (`git log -p` `--``author=Eklavya` is a fast way to skim).
 | 
			
		||||
 | 
			
		||||
  - Team up with other developers close to or in your time zone who are working
 | 
			
		||||
    on similar areas to trade timely initial code reviews. 75% of the feedback
 | 
			
		||||
    that the expert maintainers give is bugs/UI problems from clicking around,
 | 
			
		||||
    lack of tests, or code clarity issues that anyone else in the project should
 | 
			
		||||
    be able to point out. Doing this well can save a lot of round-trips.
 | 
			
		||||
 | 
			
		||||
- Help with code review! Reviewing others' changes is one of the best ways to
 | 
			
		||||
  learn to be a better developer, since you'll both see how others solve
 | 
			
		||||
  problems and also practice the art of catching bugs in unfamiliar code.
 | 
			
		||||
 | 
			
		||||
  - It's best to start with areas where you know the surrounding code
 | 
			
		||||
    and expertise, but don't be afraid to open up the code in your
 | 
			
		||||
    development environment and read it rather than trying to
 | 
			
		||||
    understand everything from the context GitHub will give you. Even
 | 
			
		||||
    Tim reads surrounding code much of the time when reviewing things,
 | 
			
		||||
    and so should you :).
 | 
			
		||||
 | 
			
		||||
  - It's OK to review something that's already been reviewed or just post a
 | 
			
		||||
    comment on one thing you noticed in a quick look!
 | 
			
		||||
 | 
			
		||||
  - Even posting a comment that you tried a PR and it worked in your development
 | 
			
		||||
    environment is valuable; you'll save the next reviewer a bit of time
 | 
			
		||||
    verifying that.
 | 
			
		||||
 | 
			
		||||
  - If you're confused by some code, usually that's because the code is
 | 
			
		||||
    confusing, not because you're not smart enough. So speak up when you notice
 | 
			
		||||
    this! Very frequently, this is a sign that we need to write more
 | 
			
		||||
    docs/comments or (better, if possible!) to make the code more
 | 
			
		||||
    self-explanatory.
 | 
			
		||||
 | 
			
		||||
- Plan your approach to larger projects. Usually, when tackling something big,
 | 
			
		||||
  there's a few phases you want to go through:
 | 
			
		||||
 | 
			
		||||
  - Studying the subsystem, reading its docs, etc., to get a feel for how things
 | 
			
		||||
    work. Often a good approach is to fix some small bugs in the area to warm
 | 
			
		||||
    your knowledge up.
 | 
			
		||||
 | 
			
		||||
  - Figure out how you'll test your work feature, both manually and via
 | 
			
		||||
    automated tests. For some projects, can save a lot of hours by doing a bit
 | 
			
		||||
    of pre-work on test infrastructure or `populate_db` initial data
 | 
			
		||||
    to make it easy for both you and code reviewers to get the state
 | 
			
		||||
    necessary to test a feature.
 | 
			
		||||
 | 
			
		||||
  - Make a plan for how to create a series of small (<100LOC) commits that are
 | 
			
		||||
    each safely mergable and move you towards your goal. Often this ends up
 | 
			
		||||
    happening through first doing a hacky attempt to hooking together the
 | 
			
		||||
    feature, with reading and print statements as part of the effort, to
 | 
			
		||||
    identify any refactoring needed or tests you want to write to help make sure
 | 
			
		||||
    your changes won't break anything important as you work. Work out a fast and
 | 
			
		||||
    consistent test procedure for how to make sure the feature is working as
 | 
			
		||||
    planned.
 | 
			
		||||
 | 
			
		||||
  - Do the prerequisite test/refactoring/etc. work, and get those changes
 | 
			
		||||
    merged.
 | 
			
		||||
 | 
			
		||||
  - Build a mergeable version of the feature on top of those refactorings.
 | 
			
		||||
    Whenever possible, find chunks of complexity that you can separate from the
 | 
			
		||||
    rest of the project.
 | 
			
		||||
 | 
			
		||||
- Spend time every week thinking about what could make contributing to Zulip
 | 
			
		||||
  easier for both yourself and the next generation of Zulip developers. And then
 | 
			
		||||
  make those ideas reality!
 | 
			
		||||
 | 
			
		||||
- Have fun! Spending your summer coding on open source is an amazing life
 | 
			
		||||
  opportunity, and we hope you'll have a blast. With some luck and hard work,
 | 
			
		||||
  your contributions to the open source world this summer will be something you
 | 
			
		||||
  can be proud of for the rest of your life.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## What makes a successful summer
 | 
			
		||||
 | 
			
		||||
Success for the student means a few things, in order of importance:
 | 
			
		||||
 | 
			
		||||
- Mastery of the skills needed to be a self-sufficient and effective open source
 | 
			
		||||
  developer. Ideally, by the end of the summer, most of the student's PRs should
 | 
			
		||||
  go through only a couple rounds of code review before being merged, both in
 | 
			
		||||
  Zulip and in any future open source projects they choose to join.
 | 
			
		||||
  Our most successful students end up as the maintainer for one or
 | 
			
		||||
  more areas within Zulip.
 | 
			
		||||
 | 
			
		||||
- The student has become a valued member of the Zulip community, and has made
 | 
			
		||||
  the Zulip community a better place through their efforts. Reviewing PRs,
 | 
			
		||||
  helping others debug, providing feedback, and finding bugs are all essential
 | 
			
		||||
  ways to contribute beyond the code in your own project.
 | 
			
		||||
 | 
			
		||||
- Zulip becoming significantly better in the areas the student focused on. The
 | 
			
		||||
  area should feel more polished, and have several new major features the
 | 
			
		||||
  student has implemented. That section of code should be more readable,
 | 
			
		||||
  better-tested, and have clearer documentation.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## Extra notes for mentors
 | 
			
		||||
 | 
			
		||||
- You're personally accountable for your student having a successful summer. If
 | 
			
		||||
  you get swamped and find you don't have enough time, tell the org admins so
 | 
			
		||||
  that we can make sure someone is covering for you. Yes, it sucks when you
 | 
			
		||||
  can't do what you signed up for, but even worse is to not tell anyone and thus
 | 
			
		||||
  prevent the project from finding a replacement.
 | 
			
		||||
 | 
			
		||||
- Mentors are expected to provide on the mentors stream a **brief report
 | 
			
		||||
  weekly** on (1) how your students' projects are going, (2) what (if anything)
 | 
			
		||||
  you're worried about, and (3) what new things you'd like to try this week to
 | 
			
		||||
  help your student. A great time to do this is after a weekly scheduled call
 | 
			
		||||
  with your student, while your recollection of the state is fresh.
 | 
			
		||||
 | 
			
		||||
- Timely feedback is more important than complete feedback, so get a fast
 | 
			
		||||
  feedback cadence going with your student. It's amazing how useful just 5
 | 
			
		||||
  minutes of feedback can be. Pay attention to the relative timezones; if you
 | 
			
		||||
  plan it, you can get several round trips in per day even with big timezone
 | 
			
		||||
  differences like USA + India.
 | 
			
		||||
 | 
			
		||||
-  What exactly you focus on in your mentorship will vary from week to week and
 | 
			
		||||
   depend somewhat on what the student needs. It might be any combination of
 | 
			
		||||
   these things:
 | 
			
		||||
 | 
			
		||||
  - Helping the student plan, chunk, and prioritize their work.
 | 
			
		||||
 | 
			
		||||
  - Manually testing UI changes and helping find bugs.
 | 
			
		||||
 | 
			
		||||
  - Doing code review of your student's work
 | 
			
		||||
 | 
			
		||||
  - Providing early feedback on visual and technical design questions.
 | 
			
		||||
 | 
			
		||||
  - Helping the student figure out how to test their changes.
 | 
			
		||||
 | 
			
		||||
  - Helping the student break their PRs into reviewing chunks.
 | 
			
		||||
 | 
			
		||||
  - Making sure busy maintainers like Tim Abbott provide any necessary feedback
 | 
			
		||||
    so that the student's project doesn't get stuck.
 | 
			
		||||
 | 
			
		||||
  - Helping with the technical design of projects and making sure they're aware
 | 
			
		||||
    of useful and relevant reference materials.
 | 
			
		||||
 | 
			
		||||
  - Pair programming with the student to help make sure you share useful tricks.
 | 
			
		||||
 | 
			
		||||
  - Emotional support when things feel like they aren't going well.
 | 
			
		||||
@@ -33,7 +33,7 @@ document:
 | 
			
		||||
 | 
			
		||||
This document focuses almost entirely on the **export** piece.  Issues
 | 
			
		||||
with getting Zulip itself running are out of scope here; see [the
 | 
			
		||||
production installation instructions](../index.html#zulip-in-production).
 | 
			
		||||
production installation instructions](index.html#zulip-in-production).
 | 
			
		||||
As for the import side of things, we only touch on it implicitly.  (My
 | 
			
		||||
reasoning was that we *had* to get the export piece right in a timely
 | 
			
		||||
fashion, even if it meant we would have to sort out some straggling
 | 
			
		||||
@@ -116,9 +116,9 @@ process the data, which isn't surprising for a top-down approach.)
 | 
			
		||||
 | 
			
		||||
The next section of the document talks about risk factors.
 | 
			
		||||
 | 
			
		||||
## Risk Mitigation
 | 
			
		||||
# Risk Mitigation
 | 
			
		||||
 | 
			
		||||
### Generic considerations
 | 
			
		||||
## Generic considerations
 | 
			
		||||
 | 
			
		||||
We have two major mechanisms for getting data:
 | 
			
		||||
 | 
			
		||||
@@ -144,9 +144,9 @@ duplicating some work, particularly on the message side of things.
 | 
			
		||||
We have not yet integrated the approved-transfer model, which tells us
 | 
			
		||||
which users can be moved.
 | 
			
		||||
 | 
			
		||||
### Risk factors broken out by data categories
 | 
			
		||||
## Risk factors broken out by data categories
 | 
			
		||||
 | 
			
		||||
#### Message Data
 | 
			
		||||
### Message Data
 | 
			
		||||
 | 
			
		||||
- models: `Message`/`UserMessage`.
 | 
			
		||||
- assets: `messages-*.json`, subprocesses, partial files
 | 
			
		||||
@@ -165,7 +165,7 @@ We currently have these measures in place for top-down processing:
 | 
			
		||||
- messages are filtered by both sender and recipient
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#### File Related Data
 | 
			
		||||
### File Related Data
 | 
			
		||||
 | 
			
		||||
- models: `Attachment`
 | 
			
		||||
- assets: S3, `attachment.json`, `uploads-temp/`, image files in
 | 
			
		||||
@@ -185,7 +185,7 @@ parts**:
 | 
			
		||||
- At import time we have to populate the `m2m` table (but fortunately,
 | 
			
		||||
  this is pretty low risk in terms of breaking anything.)
 | 
			
		||||
 | 
			
		||||
#### Recipient Data
 | 
			
		||||
### Recipient Data
 | 
			
		||||
- models: `Recipient/Stream/Subscription/Huddle`
 | 
			
		||||
- assets: `realm.json`, `(user,stream,huddle)_(recipient,subscription)`
 | 
			
		||||
 | 
			
		||||
@@ -219,7 +219,7 @@ Recommendation: We probably want to get a backup of all this data that
 | 
			
		||||
is very simply bulk-exported from the entire DB, and we should
 | 
			
		||||
obviously put it in a secure place.
 | 
			
		||||
 | 
			
		||||
#### Cross Realm Data
 | 
			
		||||
### Cross Realm Data
 | 
			
		||||
- models: `Client`
 | 
			
		||||
- assets: `realm.json`, three bots (`notification`/`email`/`welcome`),
 | 
			
		||||
  `id_maps`
 | 
			
		||||
@@ -245,7 +245,7 @@ example.  As for possibly missing messages that the welcome bot and
 | 
			
		||||
friends have sent in the past, I am not sure what our risk profile is
 | 
			
		||||
there, but I imagine it is relatively low.
 | 
			
		||||
 | 
			
		||||
#### Disjoint User Data
 | 
			
		||||
### Disjoint User Data
 | 
			
		||||
- models: `UserProfile/UserActivity/UserActivityInterval/UserPresence`
 | 
			
		||||
- assets: `realm.json`, `password`, `api_key`, `avatar salt`,
 | 
			
		||||
  `id_maps`
 | 
			
		||||
@@ -259,7 +259,7 @@ We have code in place to exclude `password` and `api_key` from
 | 
			
		||||
`UserProfile` rows.  The import process calls
 | 
			
		||||
`set_unusable_password()`.
 | 
			
		||||
 | 
			
		||||
#### Public Realm Data
 | 
			
		||||
### Public Realm Data
 | 
			
		||||
 | 
			
		||||
- models: `Realm/RealmDomain/RealmEmoji/RealmFilter/DefaultStream`
 | 
			
		||||
- asserts: `realm.json`
 | 
			
		||||
@@ -177,7 +177,7 @@ you basically have to solve these problems:
 | 
			
		||||
Zulip actually supports a bunch of integrations out-of-the-box that
 | 
			
		||||
perform as **World Readers**.
 | 
			
		||||
 | 
			
		||||
The [three different integration models](https://zulipchat.com/api/integration-guide#types-of-integrations)
 | 
			
		||||
The [three different integration models](integration-guide.html#types-of-integrations)
 | 
			
		||||
basically differ in where they perform the main functions of a
 | 
			
		||||
**World Reader**.
 | 
			
		||||
 | 
			
		||||
@@ -74,12 +74,12 @@ the backend, but does in JavaScript.
 | 
			
		||||
 | 
			
		||||
For the third-party services like Postgres, Redis, Nginx, and RabbitMQ
 | 
			
		||||
that are documented in the
 | 
			
		||||
[architecture overview](../overview/architecture-overview.html), we rely on the
 | 
			
		||||
[architecture overview](architecture-overview.html), we rely on the
 | 
			
		||||
versions of those packages provided alongside the Linux distribution
 | 
			
		||||
on which Zulip is deployed.  Because Zulip
 | 
			
		||||
[only supports Ubuntu in production](../production/requirements.html), this
 | 
			
		||||
[only supports Ubuntu in production](prod-requirements.html), this
 | 
			
		||||
usually means `apt`, though we do support
 | 
			
		||||
[other platforms in development](../development/setup-advanced.html).  Since
 | 
			
		||||
[other platforms in development](dev-setup-non-vagrant.html).  Since
 | 
			
		||||
we don't control the versions of these dependencies, we avoid relying
 | 
			
		||||
on specific versions of these packages wherever possible.
 | 
			
		||||
 | 
			
		||||
@@ -117,8 +117,8 @@ highlighting.  The system is largely managed by the code in
 | 
			
		||||
  versions in a `requirements.txt` file to declare what we're using.
 | 
			
		||||
  Since we have a few different installation targets, we maintain
 | 
			
		||||
  several `requirements.txt` format files in the `requirements/`
 | 
			
		||||
  directory (e.g. `dev.in` for development, `prod.in` for
 | 
			
		||||
  production, `docs.in` for ReadTheDocs, `common.in` for the vast
 | 
			
		||||
  directory (e.g. `dev.txt` for development, `prod.txt` for
 | 
			
		||||
  production, `docs.txt` for ReadTheDocs, `common.txt` for the vast
 | 
			
		||||
  majority of packages common to prod and development, etc.).  We use
 | 
			
		||||
  `pip install --no-deps` to ensure we only install the packages we
 | 
			
		||||
  explicitly declare as dependencies.
 | 
			
		||||
@@ -133,23 +133,6 @@ highlighting.  The system is largely managed by the code in
 | 
			
		||||
  effect is that it's easy to debug problems caused by dependency
 | 
			
		||||
  upgrades, since we're always doing those upgrades with an explicit
 | 
			
		||||
  commit updating the `requirements/` directory.
 | 
			
		||||
* **Pinning versions of indirect dependencies**.  We "pin" or "lock"
 | 
			
		||||
  the versions of our indirect dependencies files with
 | 
			
		||||
  `tools/update-locked-requirements` (powered by `pip-compile`).  What
 | 
			
		||||
  this means is that we have some "source" requirements files, like
 | 
			
		||||
  `requirements/common.in`, that declare the packages that Zulip
 | 
			
		||||
  depends on directly.  Those packages have their own recursive
 | 
			
		||||
  dependencies.  When adding or removing a dependency from Zulip, one
 | 
			
		||||
  simply edits the appropriate "source" requirements files, and then
 | 
			
		||||
  runs `tools/update-locked-requirements`.  That tool will use `pip
 | 
			
		||||
  compile` to generate the locked requirements files like `prod.txt`,
 | 
			
		||||
  `dev.txt` etc files that explicitly declare versions of all of
 | 
			
		||||
  Zulip's recursive dependencies.  For indirect dependencies (i.e.
 | 
			
		||||
  dependencies not explicitly declared in the source requirements files),
 | 
			
		||||
  it provides helpful comments explaining which direct dependency (or
 | 
			
		||||
  dependencies) needed that indirect dependency.  The process for
 | 
			
		||||
  using this system is documented in more detail in
 | 
			
		||||
  `requirements/README.md`.
 | 
			
		||||
* **Caching of virtualenvs and packages**.  To make updating the
 | 
			
		||||
  dependencies of a Zulip installation efficient, we maintain a cache
 | 
			
		||||
  of virtualenvs named by the hash of the relevant `requirements.txt`
 | 
			
		||||
@@ -170,6 +153,23 @@ highlighting.  The system is largely managed by the code in
 | 
			
		||||
  production deployment directory under `/home/zulip/deployments/`.
 | 
			
		||||
  This helps ensure that a Zulip installation doesn't leak large
 | 
			
		||||
  amounts of disk over time.
 | 
			
		||||
* **Pinning versions of indirect dependencies**.  We "pin" or "lock"
 | 
			
		||||
  the versions of our indirect dependencies files with
 | 
			
		||||
  `tools/update-locked-requirements` (powered by `pip-compile`).  What
 | 
			
		||||
  this means is that we have some "source" requirements files, like
 | 
			
		||||
  `requirements/common.txt`, that declare the packages that Zulip
 | 
			
		||||
  depends on directly.  Those packages have their own recursive
 | 
			
		||||
  dependencies.  When adding or removing a dependency from Zulip, one
 | 
			
		||||
  simply edits the appropriate "source" requirements files, and then
 | 
			
		||||
  runs `tools/update-locked-requirements`.  That tool will use `pip
 | 
			
		||||
  compile` to generate the `prod_lock.txt` and `dev_lock.txt` files
 | 
			
		||||
  that explicitly declare versions of all of Zulip's recursive
 | 
			
		||||
  dependencies.  For indirect dependencies (i.e. dependencies not
 | 
			
		||||
  explicitly declared in the source requirements files), it provides
 | 
			
		||||
  helpful comments explaining which direct dependency (or
 | 
			
		||||
  dependencies) needed that indirect dependency.  The process for
 | 
			
		||||
  using this system is documented in more detail in
 | 
			
		||||
  `requirements/README.md`.
 | 
			
		||||
* **Scripts**.  Often, we want a script running in production to use
 | 
			
		||||
  the Zulip virtualenv.  To make that work without a lot of duplicated
 | 
			
		||||
  code, we have a helpful library,
 | 
			
		||||
@@ -197,7 +197,7 @@ reasoning here.
 | 
			
		||||
  dependencies in the `yarn.lock` file; `yarn upgrade` updates the
 | 
			
		||||
  `yarn.lock` files.
 | 
			
		||||
* `tools/update-prod-static`.  This process is discussed in detail in
 | 
			
		||||
  the [static asset pipeline](../subsystems/front-end-build-process.html) article,
 | 
			
		||||
  the [static asset pipeline](front-end-build-process.html) article,
 | 
			
		||||
  but we don't use the `node_modules` directories directly in
 | 
			
		||||
  production.  Instead, static assets are compiled using our static
 | 
			
		||||
  asset pipeline and it is the compiled assets that are served
 | 
			
		||||
@@ -241,7 +241,7 @@ Zulip uses the [iamcal emoji data package][iamcal] for its emoji data
 | 
			
		||||
and sprite sheets.  We download this dependency using `npm`, and then
 | 
			
		||||
have a tool, `tools/setup/build_emoji`, which reformats the emoji data
 | 
			
		||||
into the files under `static/generated/emoji`.  Those files are in
 | 
			
		||||
turn used by our [markdown processor](../subsystems/markdown.html) and
 | 
			
		||||
turn used by our [markdown processor](markdown.html) and
 | 
			
		||||
`tools/update-prod-static` to make Zulip's emoji work in the various
 | 
			
		||||
environments where they need to be displayed.
 | 
			
		||||
 | 
			
		||||
@@ -256,7 +256,7 @@ files and a few large ones.  There is a more extended article on our
 | 
			
		||||
 | 
			
		||||
### Translations data
 | 
			
		||||
 | 
			
		||||
Zulip's [translations infrastructure](../translating/translating.html) generates
 | 
			
		||||
Zulip's [translations infrastructure](translating.html) generates
 | 
			
		||||
several files from the source data, which we manage similar to our
 | 
			
		||||
emoji, but without the caching (and thus without the
 | 
			
		||||
garbage-collection).  New translations data is downloaded from
 | 
			
		||||
@@ -288,7 +288,7 @@ usually one needs to think about making changes in 3 places:
 | 
			
		||||
 | 
			
		||||
* `tools/lib/provision.py`.  This is the main provisioning script,
 | 
			
		||||
  used by most developers to maintain their development environment.
 | 
			
		||||
* `docs/development/dev-setup-non-vagrant.md`.  This is our "manual installation"
 | 
			
		||||
* `docs/dev-setup-non-vagrant.md`.  This is our "manual installation"
 | 
			
		||||
  documentation.  Strategically, we'd like to move the support for more
 | 
			
		||||
  versions of Linux from here into `tools/lib/provision.py`.
 | 
			
		||||
* Production.  Our tools for compiling/generating static assets need
 | 
			
		||||
@@ -24,7 +24,7 @@ environment,** check
 | 
			
		||||
[Troubleshooting and Common Errors](#troubleshooting-and-common-errors). If
 | 
			
		||||
that doesn't help, please visit
 | 
			
		||||
[#provision help](https://chat.zulip.org/#narrow/stream/provision.20help)
 | 
			
		||||
in the [Zulip development community server](../contributing/chat-zulip-org.html) for
 | 
			
		||||
in the [Zulip development community server](chat-zulip-org.html) for
 | 
			
		||||
real-time help, send a note to the
 | 
			
		||||
[Zulip-devel Google group](https://groups.google.com/forum/#!forum/zulip-devel)
 | 
			
		||||
or [file an issue](https://github.com/zulip/zulip/issues).
 | 
			
		||||
@@ -46,9 +46,9 @@ proxy](#specifying-a-proxy) if you need a proxy to access the internet.)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
- **All**: 2GB available RAM, Active broadband internet connection, [GitHub account][set-up-git].
 | 
			
		||||
- **macOS**: macOS (10.11 El Capitan or newer recommended), Git,
 | 
			
		||||
  VirtualBox (version [5.2.6][vbox-dl-macos] recommended -- we find
 | 
			
		||||
  it's more stable than more recent versions),
 | 
			
		||||
- **macOS**: macOS (10.11 El Capitan or 10.12 Sierra recommended),
 | 
			
		||||
  Git, VirtualBox (version [5.1.8][vbox-dl-macos]
 | 
			
		||||
  recommended -- we find it's more stable than more recent versions),
 | 
			
		||||
  [Vagrant][vagrant-dl-macos].
 | 
			
		||||
- **Ubuntu**: 14.04 64-bit or 16.04 64-bit, Git, [Vagrant][vagrant-dl-deb], lxc.
 | 
			
		||||
  - or **Debian**: 9.0 "stretch" 64-bit
 | 
			
		||||
@@ -82,12 +82,8 @@ Jump to:
 | 
			
		||||
 | 
			
		||||
#### macOS
 | 
			
		||||
 | 
			
		||||
0. If you are running MacOS High Sierra, make sure you are not running
 | 
			
		||||
   a version with a
 | 
			
		||||
   [buggy NFS implementation](#importerror-no-module-named-on-macos-during-vagrant-provisioning).
 | 
			
		||||
   Versions 10.13.2 and above have the bug fixed.
 | 
			
		||||
1. Install [Vagrant][vagrant-dl-macos] (2.0.2).
 | 
			
		||||
2. Install [VirtualBox][vbox-dl-macos] (5.2.6).
 | 
			
		||||
1. Install [Vagrant][vagrant-dl-macos] (1.8.4-1.8.6, do not use 1.8.7).
 | 
			
		||||
2. Install [VirtualBox][vbox-dl-macos] (5.1.8).
 | 
			
		||||
 | 
			
		||||
(For a non-free option, but better performance, you can also use [VMWare
 | 
			
		||||
Fusion][vmware-fusion-dl] with the [VMWare Fusion Vagrant
 | 
			
		||||
@@ -104,7 +100,7 @@ after which you can jump to [Step 2: Get Zulip Code](#step-2-get-zulip-code):
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
sudo apt-get -y purge vagrant && \
 | 
			
		||||
wget https://releases.hashicorp.com/vagrant/2.0.2/vagrant_2.0.2_x86_64.deb && \
 | 
			
		||||
wget https://releases.hashicorp.com/vagrant/1.8.6/vagrant_1.8.6_x86_64.deb && \
 | 
			
		||||
sudo dpkg -i vagrant*.deb && \
 | 
			
		||||
sudo apt-get -y install build-essential git ruby lxc lxc-templates cgroup-lite redir && \
 | 
			
		||||
vagrant plugin install vagrant-lxc && \
 | 
			
		||||
@@ -126,11 +122,11 @@ christie@ubuntu-desktop:~
 | 
			
		||||
$ sudo apt-get purge vagrant
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Now download and install the .deb package for [Vagrant 2.0.2][vagrant-dl-deb]:
 | 
			
		||||
Now download and install the .deb package for [Vagrant 1.8.6][vagrant-dl-deb]:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
christie@ubuntu-desktop:~
 | 
			
		||||
$ wget https://releases.hashicorp.com/vagrant/2.0.2/vagrant_2.0.2_x86_64.deb
 | 
			
		||||
$ wget https://releases.hashicorp.com/vagrant/1.8.6/vagrant_1.8.6_x86_64.deb
 | 
			
		||||
 | 
			
		||||
christie@ubuntu-desktop:~
 | 
			
		||||
$ sudo dpkg -i vagrant*.deb
 | 
			
		||||
@@ -174,36 +170,35 @@ Now you are ready for [Step 2: Get Zulip Code.](#step-2-get-zulip-code)
 | 
			
		||||
 | 
			
		||||
#### Debian
 | 
			
		||||
 | 
			
		||||
The setup for Debian 9.0 "stretch" is very similar to that
 | 
			
		||||
[for Ubuntu 16.04 above](#ubuntu).  Follow those instructions,
 | 
			
		||||
except with the following differences:
 | 
			
		||||
The setup for Debian 9.0 "stretch" is just like [for Ubuntu 16.04](#ubuntu),
 | 
			
		||||
with one difference.
 | 
			
		||||
 | 
			
		||||
**Apt package list**.  In "2. Install remaining dependencies", the
 | 
			
		||||
command to install the dependencies is a bit shorter:
 | 
			
		||||
If you're in a hurry, you can copy and paste the following into your terminal
 | 
			
		||||
after which you can jump to [Step 2: Get Zulip Code](#step-2-get-zulip-code):
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
christie@ubuntu-desktop:~
 | 
			
		||||
$ sudo apt-get install build-essential git ruby lxc redir
 | 
			
		||||
sudo apt-get -y purge vagrant && \
 | 
			
		||||
wget https://releases.hashicorp.com/vagrant/1.8.6/vagrant_1.8.6_x86_64.deb && \
 | 
			
		||||
sudo dpkg -i vagrant*.deb && \
 | 
			
		||||
sudo apt-get -y install build-essential git ruby lxc redir && \
 | 
			
		||||
vagrant plugin install vagrant-lxc && \
 | 
			
		||||
vagrant lxc sudoers
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
**Set up LXC networking**.  After completing "2. Install remaining
 | 
			
		||||
dependencies", you will have to set up networking for LXC containers,
 | 
			
		||||
because Debian's packaging for LXC does not ship any default
 | 
			
		||||
network setup for them.  You can do this by
 | 
			
		||||
[following the steps][lxc-networking-quickstart] outlined in
 | 
			
		||||
[Debian's LXC docs](https://wiki.debian.org/LXC#network_setup).
 | 
			
		||||
For a step-by-step explanation, follow the [Ubuntu instructions above](#ubuntu),
 | 
			
		||||
with the following difference: in "2. Install remaining dependencies", the
 | 
			
		||||
command is
 | 
			
		||||
 | 
			
		||||
[lxc-networking-quickstart]: https://wiki.debian.org/LXC#Minimal_changes_to_set_up_networking_for_LXC_for_Debian_.2BIBw-stretch.2BIB0_.28testing.29
 | 
			
		||||
```
 | 
			
		||||
sudo apt-get install build-essential git ruby lxc redir
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Then return to the next step in the Ubuntu instructions above.  After
 | 
			
		||||
finishing those steps, you will be ready for
 | 
			
		||||
[Step 2: Get Zulip Code](#step-2-get-zulip-code).
 | 
			
		||||
 | 
			
		||||
#### Windows 10
 | 
			
		||||
 | 
			
		||||
1. Install [Git for Windows][git-bash], which installs *Git BASH*.
 | 
			
		||||
2. Install [VirtualBox][vbox-dl] (version == 5.2.6).
 | 
			
		||||
3. Install [Vagrant][vagrant-dl-win] (version 2.0.2, do not use 1.8.7).
 | 
			
		||||
2. Install [VirtualBox][vbox-dl] (version >= 5.1.6).
 | 
			
		||||
3. Install [Vagrant][vagrant-dl-win] (version 1.8.4-1.8.6, do not use 1.8.7).
 | 
			
		||||
 | 
			
		||||
(Note: While *Git BASH* is recommended, you may also use [Cygwin][cygwin-dl].
 | 
			
		||||
If you do, make sure to **install default required packages** along with
 | 
			
		||||
@@ -275,13 +270,9 @@ Now you are ready for [Step 2: Get Zulip Code.](#step-2-get-zulip-code)
 | 
			
		||||
   do this.
 | 
			
		||||
2. Open Terminal (macOS/Ubuntu) or Git BASH (Windows; must
 | 
			
		||||
   **run as an Administrator**).
 | 
			
		||||
3. In Terminal/Git BASH,
 | 
			
		||||
   [clone your fork of the Zulip repository](../git/cloning.html#step-1b-clone-to-your-machine)
 | 
			
		||||
   and [connect the Zulip upstream repository](../git/cloning.html#step-1c-connect-your-fork-to-zulip-upstream):
 | 
			
		||||
 | 
			
		||||
3. In Terminal/Git BASH, clone your fork:
 | 
			
		||||
```
 | 
			
		||||
git clone --config pull.rebase git@github.com:YOURUSERNAME/zulip.git
 | 
			
		||||
git remote add -f upstream https://github.com/zulip/zulip.git
 | 
			
		||||
git clone git@github.com:YOURUSERNAME/zulip.git
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
This will create a 'zulip' directory and download the Zulip code into it.
 | 
			
		||||
@@ -291,7 +282,7 @@ something like:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
christie@win10 ~
 | 
			
		||||
$ git clone --config pull.rebase git@github.com:YOURUSERNAME/zulip.git
 | 
			
		||||
$ git clone git@github.com:YOURUSERNAME/zulip.git
 | 
			
		||||
Cloning into 'zulip'...
 | 
			
		||||
remote: Counting objects: 73571, done.
 | 
			
		||||
remote: Compressing objects: 100% (2/2), done.
 | 
			
		||||
@@ -330,7 +321,7 @@ does the following:
 | 
			
		||||
  downloads all required dependencies, sets up the python environment for
 | 
			
		||||
  the Zulip development server, and initializes a default test
 | 
			
		||||
  database.  We call this process "provisioning", and it is documented
 | 
			
		||||
  in some detail in our [dependencies documentation](../subsystems/dependencies.html).
 | 
			
		||||
  in some detail in our [dependencies documentation](dependencies.html).
 | 
			
		||||
 | 
			
		||||
You will need an active internet connection during the entire
 | 
			
		||||
process. (See [Specifying a proxy](#specifying-a-proxy) if you need a
 | 
			
		||||
@@ -342,7 +333,7 @@ documented in the
 | 
			
		||||
[Troubleshooting and Common Errors](#troubleshooting-and-common-errors)
 | 
			
		||||
section.  If that doesn't help, please visit
 | 
			
		||||
[#provision help](https://chat.zulip.org/#narrow/stream/provision.20help)
 | 
			
		||||
in the [Zulip development community server](../contributing/chat-zulip-org.html) for
 | 
			
		||||
in the [Zulip development community server](chat-zulip-org.html) for
 | 
			
		||||
real-time help.
 | 
			
		||||
 | 
			
		||||
On Windows, you will see `The system cannot find the path specified.` message
 | 
			
		||||
@@ -436,7 +427,7 @@ navigating to <http://localhost:9991/> in the browser on your main machine.
 | 
			
		||||
 | 
			
		||||
You should see something like this:
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
The Zulip server will continue to run and send output to the terminal window.
 | 
			
		||||
When you navigate to Zulip in your browser, check your terminal and you
 | 
			
		||||
@@ -479,7 +470,7 @@ It's good to have the terminal running `run-dev.py` up as you work since error
 | 
			
		||||
messages including tracebacks along with every backend request will be printed
 | 
			
		||||
there.
 | 
			
		||||
 | 
			
		||||
See [Logging](../subsystems/logging.html) for further details on the run-dev.py console
 | 
			
		||||
See [Logging](logging.html) for further details on the run-dev.py console
 | 
			
		||||
output.
 | 
			
		||||
 | 
			
		||||
#### Committing and pushing changes with git
 | 
			
		||||
@@ -508,7 +499,7 @@ After provisioning, you'll want to
 | 
			
		||||
 | 
			
		||||
If you run into any trouble, the
 | 
			
		||||
[#provision help](https://chat.zulip.org/#narrow/stream/provision.20help)
 | 
			
		||||
in the [Zulip development community server](../contributing/chat-zulip-org.html) for
 | 
			
		||||
in the [Zulip development community server](chat-zulip-org.html) for
 | 
			
		||||
is a great place to ask for help.
 | 
			
		||||
 | 
			
		||||
#### Rebuilding the development environment
 | 
			
		||||
@@ -604,7 +595,7 @@ If these solutions aren't working for you or you encounter an issue not
 | 
			
		||||
documented below, there are a few ways to get further help:
 | 
			
		||||
 | 
			
		||||
* Ask in [#provision help](https://chat.zulip.org/#narrow/stream/provision.20help)
 | 
			
		||||
  in the [Zulip development community server](../contributing/chat-zulip-org.html),
 | 
			
		||||
  in the [Zulip development community server](chat-zulip-org.html),
 | 
			
		||||
* send a note to the [Zulip-devel Google
 | 
			
		||||
  group](https://groups.google.com/forum/#!forum/zulip-devel), or
 | 
			
		||||
* [File an issue](https://github.com/zulip/zulip/issues).
 | 
			
		||||
@@ -662,8 +653,8 @@ macOS.
 | 
			
		||||
On **macOS** this error is most likely to occur with Vagrant version 1.8.7 and
 | 
			
		||||
is a [known issue](https://github.com/mitchellh/vagrant/issues/7997).
 | 
			
		||||
 | 
			
		||||
The solution is to downgrade Vagrant to version 2.0.2 ([available
 | 
			
		||||
here](https://releases.hashicorp.com/vagrant/2.0.2/)), or to use your system's
 | 
			
		||||
The solution is to downgrade Vagrant to version 1.8.6 ([available
 | 
			
		||||
here](https://releases.hashicorp.com/vagrant/1.8.6/)), or to use your system's
 | 
			
		||||
version of `curl` instead of the one that ships with Vagrant:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
@@ -722,33 +713,6 @@ This is equivalent of running a halt followed by an up (aka rebooting
 | 
			
		||||
the guest).  After this, you can do `vagrant provision` and `vagrant
 | 
			
		||||
ssh`.
 | 
			
		||||
 | 
			
		||||
#### ssl read error
 | 
			
		||||
 | 
			
		||||
If you receive the following error while running `vagrant up`:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
SSL read: error:00000000:lib(0):func(0):reason(0), errno 104
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
It means that either your network connection is unstable and/or very
 | 
			
		||||
slow. To resolve it, run `vagrant up` until it works (possibly on a
 | 
			
		||||
better network connection).
 | 
			
		||||
 | 
			
		||||
#### Unmet dependencies error
 | 
			
		||||
 | 
			
		||||
When running `vagrant up` or `provision`, if you see the following error:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
==> default: E:unmet dependencies. Try 'apt-get -f install' with no packages (or specify a solution).
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
It means that your local apt repository has been corrupted, which can
 | 
			
		||||
usually be resolved by executing the command:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
apt-get -f install
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### ssh connection closed by remote host
 | 
			
		||||
 | 
			
		||||
On running `vagrant ssh`, if you see the following error:
 | 
			
		||||
@@ -914,7 +878,7 @@ Likely causes are:
 | 
			
		||||
1. Networking issues
 | 
			
		||||
2. Insufficient RAM. Check whether you've allotted at least two
 | 
			
		||||
gigabytes of RAM, which is the minimum Zulip
 | 
			
		||||
[requires](../development/setup-vagrant.html#requirements). If
 | 
			
		||||
[requires](dev-env-first-time-contributors.html#requirements). If
 | 
			
		||||
not, go to your VM settings and increase the RAM, then restart
 | 
			
		||||
the VM.
 | 
			
		||||
 | 
			
		||||
@@ -992,58 +956,10 @@ christie@xenial:~
 | 
			
		||||
$ sudo patch --directory /usr/lib/ruby/vendor_ruby/vagrant < vagrant-plugin.patch
 | 
			
		||||
patching file bundler.rb
 | 
			
		||||
```
 | 
			
		||||
#### VT-X unavailability error
 | 
			
		||||
 | 
			
		||||
Users who are unable to do "vagrant up" due to a VT-X unavailability error need to disable "Hyper-V" to get it to work.
 | 
			
		||||
 | 
			
		||||
#### Permissions errors when running the test suite in LXC
 | 
			
		||||
 | 
			
		||||
See ["Possible testing issues"](../testing/testing.html#possible-testing-issues).
 | 
			
		||||
 | 
			
		||||
#### ImportError: No module named '...' on MacOS during Vagrant provisioning
 | 
			
		||||
 | 
			
		||||
If you see following error (or similar) when you try to provision
 | 
			
		||||
Vagrant environment by `vagrant provision` (or during first run
 | 
			
		||||
`vagrant up`):
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
    default: ImportError: No module named 'zerver.lib.emoji'
 | 
			
		||||
    default: Error running a subcommand of ./lib/provision.py: tools/do-destroy-rebuild-database
 | 
			
		||||
    default: Actual error output for the subcommand is just above this.
 | 
			
		||||
    default: Traceback (most recent call last):
 | 
			
		||||
    default:   File "./lib/provision.py", line 413, in <module>
 | 
			
		||||
    default:     sys.exit(main(options))
 | 
			
		||||
    default:   File "./lib/provision.py", line 349, in main
 | 
			
		||||
    default:     run(["tools/do-destroy-rebuild-database"])
 | 
			
		||||
    default:   File "/srv/zulip/scripts/lib/zulip_tools.py", line 163, in run
 | 
			
		||||
    default:     subprocess.check_call(args, **kwargs)
 | 
			
		||||
    default:   File "/usr/lib/python3.4/subprocess.py", line 561, in check_call
 | 
			
		||||
    default:     raise CalledProcessError(retcode, cmd)
 | 
			
		||||
    default: subprocess.CalledProcessError: Command '['tools/do-destroy-rebuild-database']' returned non-zero exit status 1
 | 
			
		||||
    default:
 | 
			
		||||
    default: Provisioning failed!
 | 
			
		||||
    default: * Look at the traceback(s) above to find more about the errors.
 | 
			
		||||
    default: * Resolve the errors or get help on chat.
 | 
			
		||||
    default: * If you can fix this yourself, you can re-run tools/provision at any time.
 | 
			
		||||
    default: * Logs are here: zulip/var/log/provision.log
 | 
			
		||||
    default:
 | 
			
		||||
The SSH command responded with a non-zero exit status. Vagrant
 | 
			
		||||
assumes that this means the command failed. The output for this command
 | 
			
		||||
should be in the log above. Please read the output to determine what
 | 
			
		||||
went wrong.
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
This error is caused by a bug in the MacOS NFS file syncing
 | 
			
		||||
implementation (Zulip uses Vagrant's NFS feature for syncing files on
 | 
			
		||||
MacOS).  In early versions of MacOS High Sierra, files present in the
 | 
			
		||||
directory on the host machine would appear to not be present in the
 | 
			
		||||
Vagrant guest (e.g. in the exception above, `zerver/lib/emoji.py` is
 | 
			
		||||
missing).  This bug is fixed in MacOS High Sierra 10.13.2 and above,
 | 
			
		||||
so the fix is to upgrade to a version of MacOS with a working NFS
 | 
			
		||||
implementation.
 | 
			
		||||
 | 
			
		||||
You can read more about this
 | 
			
		||||
[here](https://github.com/hashicorp/vagrant/issues/8788).
 | 
			
		||||
See ["Possible testing issues"](testing.html#possible-testing-issues).
 | 
			
		||||
 | 
			
		||||
### Specifying a proxy
 | 
			
		||||
 | 
			
		||||
@@ -1096,22 +1012,22 @@ for the IP address that means any IP address can connect to your development ser
 | 
			
		||||
 | 
			
		||||
[cygwin-dl]: http://cygwin.com/
 | 
			
		||||
[vagrant-dl]: https://www.vagrantup.com/downloads.html
 | 
			
		||||
[vagrant-dl-win]: https://releases.hashicorp.com/vagrant/2.0.2/vagrant_2.0.2_x86_64.msi
 | 
			
		||||
[vagrant-dl-macos]: https://releases.hashicorp.com/vagrant/2.0.2/vagrant_2.0.2_x86_64.dmg
 | 
			
		||||
[vagrant-dl-deb]: https://releases.hashicorp.com/vagrant/2.0.2/vagrant_2.0.2_x86_64.deb
 | 
			
		||||
[vagrant-dl-win]: https://releases.hashicorp.com/vagrant/1.8.6/vagrant_1.8.6.msi
 | 
			
		||||
[vagrant-dl-macos]: https://releases.hashicorp.com/vagrant/1.8.6/vagrant_1.8.6.dmg
 | 
			
		||||
[vagrant-dl-deb]: https://releases.hashicorp.com/vagrant/1.8.6/vagrant_1.8.6_x86_64.deb
 | 
			
		||||
[vagrant-lxc]: https://github.com/fgrehm/vagrant-lxc
 | 
			
		||||
[vbox-dl]: https://www.virtualbox.org/wiki/Downloads
 | 
			
		||||
[vbox-dl-macos]: https://download.virtualbox.org/virtualbox/5.2.6/VirtualBox-5.2.6-120293-OSX.dmg
 | 
			
		||||
[vbox-dl-macos]: http://download.virtualbox.org/virtualbox/5.1.8/VirtualBox-5.1.8-111374-OSX.dmg
 | 
			
		||||
[vmware-fusion-dl]: http://www.vmware.com/products/fusion.html
 | 
			
		||||
[vagrant-vmware-fusion-dl]: https://www.vagrantup.com/vmware/
 | 
			
		||||
[avoiding-sudo]: https://github.com/fgrehm/vagrant-lxc#avoiding-sudo-passwords
 | 
			
		||||
[install-advanced]: ../development/setup-advanced.html
 | 
			
		||||
[install-advanced]: dev-setup-non-vagrant.html
 | 
			
		||||
[lxc-sf]: https://github.com/fgrehm/vagrant-lxc/wiki/FAQ#help-my-shared-folders-have-the-wrong-owner
 | 
			
		||||
[rtd-git-guide]: ../git/index.html
 | 
			
		||||
[rtd-testing]: ../testing/testing.html
 | 
			
		||||
[rtd-using-dev-env]: using.html
 | 
			
		||||
[rtd-dev-remote]: remote.html
 | 
			
		||||
[rtd-git-guide]: git-guide.html
 | 
			
		||||
[rtd-testing]: testing.html
 | 
			
		||||
[rtd-using-dev-env]: using-dev-environment.html
 | 
			
		||||
[rtd-dev-remote]: dev-remote.html
 | 
			
		||||
[git-bash]: https://git-for-windows.github.io/
 | 
			
		||||
[bash-admin-setup]: https://superuser.com/questions/1002262/run-applications-as-administrator-by-default-in-windows-10
 | 
			
		||||
[set-up-git]: ../git/setup.html
 | 
			
		||||
[travis-ci]: ../git/cloning.html#step-3-configure-travis-ci-continuous-integration
 | 
			
		||||
[set-up-git]: git-guide.html#set-up-git
 | 
			
		||||
[travis-ci]: git-guide.html#step-3-configure-travis-ci-continuous-integration
 | 
			
		||||
@@ -82,14 +82,14 @@ And if you've setup the Zulip development environment on a remote
 | 
			
		||||
machine, take a look at our tips for
 | 
			
		||||
[developing remotely][dev-remote].
 | 
			
		||||
 | 
			
		||||
[dev-remote]: remote.html
 | 
			
		||||
[install-direct]: ../development/setup-advanced.html#installing-directly-on-ubuntu
 | 
			
		||||
[install-docker]: ../development/setup-advanced.html#using-docker-experimental
 | 
			
		||||
[install-generic]: ../development/setup-advanced.html#installing-manually-on-linux
 | 
			
		||||
[install-vagrant]: ../development/setup-vagrant.html
 | 
			
		||||
[dev-remote]: dev-remote.html
 | 
			
		||||
[install-direct]: dev-setup-non-vagrant.html#installing-directly-on-ubuntu
 | 
			
		||||
[install-docker]: dev-setup-non-vagrant.html#using-docker-experimental
 | 
			
		||||
[install-generic]: dev-setup-non-vagrant.html#installing-manually-on-linux
 | 
			
		||||
[install-vagrant]: dev-env-first-time-contributors.html
 | 
			
		||||
[self-install-remote]: #installing-remotely
 | 
			
		||||
[self-slow-internet]: #slow-internet-connections
 | 
			
		||||
[configure-proxy]: ../development/setup-vagrant.html#specifying-a-proxy
 | 
			
		||||
[using-dev-env]: using.html
 | 
			
		||||
[testing]: ../testing/testing.html
 | 
			
		||||
[travis-ci]: ../git/cloning.html#step-3-configure-travis-ci-continuous-integration
 | 
			
		||||
[configure-proxy]: dev-env-first-time-contributors.html#specifying-a-proxy
 | 
			
		||||
[using-dev-env]: using-dev-environment.html
 | 
			
		||||
[testing]: testing.html
 | 
			
		||||
[travis-ci]: git-guide.html#step-3-configure-travis-ci-continuous-integration
 | 
			
		||||
@@ -41,19 +41,10 @@ the remote virtual machine, we recommend installing
 | 
			
		||||
[Vagrant][install-vagrant] method so you can easily uninstall if you
 | 
			
		||||
need to.
 | 
			
		||||
 | 
			
		||||
The main difference from the standard instructions is that for a
 | 
			
		||||
remote development environment, you'll need to run `export
 | 
			
		||||
EXTERNAL_HOST=<REMOTE_IP>:9991` in a shell before running `run-dev.py`
 | 
			
		||||
(and see also the `--interface=''` option documented below).  If your
 | 
			
		||||
server has a static IP address, we recommend putting this command in
 | 
			
		||||
`~/.bashrc`, so you don't need to remember to run it every time. This
 | 
			
		||||
allows you to access Zulip running in your development environment
 | 
			
		||||
using a browser on another host.
 | 
			
		||||
 | 
			
		||||
## Running the development server
 | 
			
		||||
 | 
			
		||||
Once you have set up the development environment, you can start up the
 | 
			
		||||
development server with the following command in the directory where
 | 
			
		||||
development instance of Zulip with the following command in the directory where
 | 
			
		||||
you cloned Zulip:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
@@ -65,12 +56,7 @@ navigate to `http://<REMOTE_IP>:9991` and you should see something like
 | 
			
		||||
this screenshot of the Zulip development environment:
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
The `--interface=''` command makes the Zulip development environment
 | 
			
		||||
accessible from any IP address (in contrast with the more secure
 | 
			
		||||
default of only being accessible from localhost, which is great for
 | 
			
		||||
developing on your laptop).
 | 
			
		||||
environment](images/zulip-dev.png)
 | 
			
		||||
 | 
			
		||||
You can [port
 | 
			
		||||
forward](https://help.ubuntu.com/community/SSH/OpenSSH/PortForwarding) using
 | 
			
		||||
@@ -101,7 +87,7 @@ don't have a favorite, here are some suggestions:
 | 
			
		||||
* [spacemacs](https://github.com/syl20bnr/spacemacs)
 | 
			
		||||
* [sublime](https://www.sublimetext.com/)
 | 
			
		||||
 | 
			
		||||
Next, follow our [Git and GitHub Guide](../git/index.html) to clone and configure
 | 
			
		||||
Next, follow our [Git and GitHub Guide](git-guide.html) to clone and configure
 | 
			
		||||
your fork of zulip on your local computer.
 | 
			
		||||
 | 
			
		||||
Once you have cloned your code locally, you can get to work.
 | 
			
		||||
@@ -186,13 +172,13 @@ Next, read the following to learn more about developing for Zulip:
 | 
			
		||||
* [Using the Development Environment][rtd-using-dev-env]
 | 
			
		||||
* [Testing][rtd-testing]
 | 
			
		||||
 | 
			
		||||
[install-direct]: ../development/setup-advanced.html#installing-directly-on-ubuntu
 | 
			
		||||
[install-generic]: ../development/setup-advanced.html#installing-manually-on-linux
 | 
			
		||||
[install-vagrant]: ../development/setup-vagrant.html
 | 
			
		||||
[rtd-git-guide]: ../git/index.html
 | 
			
		||||
[rtd-using-dev-env]: using.html
 | 
			
		||||
[rtd-testing]: ../testing/testing.html
 | 
			
		||||
[install-direct]: dev-setup-non-vagrant.html#installing-directly-on-ubuntu
 | 
			
		||||
[install-generic]: dev-setup-non-vagrant.html#installing-manually-on-linux
 | 
			
		||||
[install-vagrant]: dev-env-first-time-contributors.html
 | 
			
		||||
[rtd-git-guide]: git-guide.html
 | 
			
		||||
[rtd-using-dev-env]: using-dev-environment.html
 | 
			
		||||
[rtd-testing]: testing.html
 | 
			
		||||
[git-bash]: https://git-for-windows.github.io/
 | 
			
		||||
[codeanywhere]: https://codeanywhere.com/
 | 
			
		||||
[img-ca-settings]: ../images/codeanywhere-settings.png
 | 
			
		||||
[img-ca-workspace]: ../images/codeanywhere-workspace.png
 | 
			
		||||
[img-ca-settings]: images/codeanywhere-settings.png
 | 
			
		||||
[img-ca-workspace]: images/codeanywhere-workspace.png
 | 
			
		||||
@@ -1,21 +1,15 @@
 | 
			
		||||
# Advanced Setup (non-Vagrant)
 | 
			
		||||
# Zulip development environment setup without Vagrant
 | 
			
		||||
 | 
			
		||||
Contents:
 | 
			
		||||
 | 
			
		||||
* [Installing directly on Ubuntu](#installing-directly-on-ubuntu)
 | 
			
		||||
* [Installing manually on Linux](#installing-manually-on-linux)
 | 
			
		||||
* [Installing directly on cloud9](#installing-on-cloud9)
 | 
			
		||||
* [Using Docker (experimental)](#using-docker-experimental)
 | 
			
		||||
 | 
			
		||||
## Installing directly on Ubuntu
 | 
			
		||||
 | 
			
		||||
Start by [cloning your fork of the Zulip repository][zulip-rtd-git-cloning]
 | 
			
		||||
and [connecting the Zulip upstream repository][zulip-rtd-git-connect]:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
git clone --config pull.rebase https://github.com/YOURUSERNAME/zulip.git
 | 
			
		||||
git remote add -f upstream https://github.com/zulip/zulip.git
 | 
			
		||||
```
 | 
			
		||||
Start by cloning this repository: `git clone
 | 
			
		||||
https://github.com/zulip/zulip.git`
 | 
			
		||||
 | 
			
		||||
If you'd like to install a Zulip development environment on a computer
 | 
			
		||||
that's already running Ubuntu 16.04 Xenial or Ubuntu 14.04 Trusty, you
 | 
			
		||||
@@ -34,7 +28,7 @@ development environment).
 | 
			
		||||
 | 
			
		||||
Once you've done the above setup, you can pick up the [documentation
 | 
			
		||||
on using the Zulip development
 | 
			
		||||
environment](../development/setup-vagrant.html#step-4-developing),
 | 
			
		||||
environment](dev-env-first-time-contributors.html#step-4-developing),
 | 
			
		||||
ignoring the parts about `vagrant` (since you're not using it).
 | 
			
		||||
 | 
			
		||||
## Installing manually on Linux
 | 
			
		||||
@@ -67,13 +61,8 @@ Install the following non-Python dependencies:
 | 
			
		||||
 | 
			
		||||
#### Using the official Ubuntu repositories, PGroonga PPA and `tsearch-extras` deb package:
 | 
			
		||||
 | 
			
		||||
Start by [cloning your fork of the Zulip repository][zulip-rtd-git-cloning]
 | 
			
		||||
and [connecting the Zulip upstream repository][zulip-rtd-git-connect]:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
git clone --config pull.rebase https://github.com/YOURUSERNAME/zulip.git
 | 
			
		||||
git remote add -f upstream https://github.com/zulip/zulip.git
 | 
			
		||||
```
 | 
			
		||||
Start by cloning this repository: `git clone
 | 
			
		||||
https://github.com/zulip/zulip.git`
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
sudo apt-get install closure-compiler libfreetype6-dev libffi-dev \
 | 
			
		||||
@@ -89,8 +78,6 @@ sudo apt-get update
 | 
			
		||||
sudo apt-get install postgresql-9.3-pgroonga
 | 
			
		||||
# On 16.04
 | 
			
		||||
sudo apt-get install postgresql-9.5-pgroonga
 | 
			
		||||
# On 17.04 or 17.10
 | 
			
		||||
sudo apt-get install postgresql-9.6-pgroonga
 | 
			
		||||
 | 
			
		||||
# If using Debian, follow the instructions here: http://pgroonga.github.io/install/debian.html
 | 
			
		||||
 | 
			
		||||
@@ -120,7 +107,7 @@ sudo dpkg -i postgresql-9.4-tsearch-extras_0.1_amd64.deb
 | 
			
		||||
 | 
			
		||||
# If on 16.04 or stretch
 | 
			
		||||
wget https://launchpad.net/~tabbott/+archive/ubuntu/zulip/+files/postgresql-9.5-tsearch-extras_0.2_amd64.deb
 | 
			
		||||
sudo dpkg -i postgresql-9.5-tsearch-extras_0.3_amd64.deb
 | 
			
		||||
sudo dpkg -i postgresql-9.5-tsearch-extras_0.2_amd64.deb
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Alternatively, you can always build the package from [tsearch-extras
 | 
			
		||||
@@ -132,13 +119,8 @@ Now continue with the [All Systems](#all-systems) instructions below.
 | 
			
		||||
 | 
			
		||||
[zulip-ppa]: https://launchpad.net/~tabbott/+archive/ubuntu/zulip/+packages
 | 
			
		||||
 | 
			
		||||
Start by [cloning your fork of the Zulip repository][zulip-rtd-git-cloning]
 | 
			
		||||
and [connecting the Zulip upstream repository][zulip-rtd-git-connect]:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
git clone --config pull.rebase https://github.com/YOURUSERNAME/zulip.git
 | 
			
		||||
git remote add -f upstream https://github.com/zulip/zulip.git
 | 
			
		||||
```
 | 
			
		||||
Start by cloning this repository: `git clone
 | 
			
		||||
https://github.com/zulip/zulip.git`
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
sudo add-apt-repository ppa:tabbott/zulip
 | 
			
		||||
@@ -157,13 +139,8 @@ Now continue with the [All Systems](#all-systems) instructions below.
 | 
			
		||||
These instructions are experimental and may have bugs; patches
 | 
			
		||||
welcome!
 | 
			
		||||
 | 
			
		||||
Start by [cloning your fork of the Zulip repository][zulip-rtd-git-cloning]
 | 
			
		||||
and [connecting the Zulip upstream repository][zulip-rtd-git-connect]:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
git clone --config pull.rebase https://github.com/YOURUSERNAME/zulip.git
 | 
			
		||||
git remote add -f upstream https://github.com/zulip/zulip.git
 | 
			
		||||
```
 | 
			
		||||
Start by cloning this repository: `git clone
 | 
			
		||||
https://github.com/zulip/zulip.git`
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
sudo dnf install libffi-devel memcached rabbitmq-server \
 | 
			
		||||
@@ -179,13 +156,8 @@ Now continue with the [Common to Fedora/CentOS](#common-to-fedora-centos-instruc
 | 
			
		||||
These instructions are experimental and may have bugs; patches
 | 
			
		||||
welcome!
 | 
			
		||||
 | 
			
		||||
Start by [cloning your fork of the Zulip repository][zulip-rtd-git-cloning]
 | 
			
		||||
and [connecting the Zulip upstream repository][zulip-rtd-git-connect]:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
git clone --config pull.rebase https://github.com/YOURUSERNAME/zulip.git
 | 
			
		||||
git remote add -f upstream https://github.com/zulip/zulip.git
 | 
			
		||||
```
 | 
			
		||||
Start by cloning this repository: `git clone
 | 
			
		||||
https://github.com/zulip/zulip.git`
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
# Add user zulip to the system (not necessary if you configured zulip
 | 
			
		||||
@@ -218,7 +190,7 @@ sudo yum install libffi-devel memcached rabbitmq-server openldap-devel \
 | 
			
		||||
sudo yum groupinstall "Development Tools"
 | 
			
		||||
 | 
			
		||||
# clone Zulip's git repo and cd into it
 | 
			
		||||
cd && git clone --config pull.rebase https://github.com/zulip/zulip && cd zulip/
 | 
			
		||||
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
 | 
			
		||||
@@ -242,13 +214,8 @@ Now continue with the [Common to Fedora/CentOS](#common-to-fedora-centos-instruc
 | 
			
		||||
These instructions are experimental and may have bugs; patches
 | 
			
		||||
welcome!
 | 
			
		||||
 | 
			
		||||
Start by [cloning your fork of the Zulip repository][zulip-rtd-git-cloning]
 | 
			
		||||
and [connecting the Zulip upstream repository][zulip-rtd-git-connect]:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
git clone --config pull.rebase https://github.com/YOURUSERNAME/zulip.git
 | 
			
		||||
git remote add -f upstream https://github.com/zulip/zulip.git
 | 
			
		||||
```
 | 
			
		||||
Start by cloning this repository: `git clone
 | 
			
		||||
https://github.com/zulip/zulip.git`
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
doas pkg_add sudo bash gcc postgresql-server redis rabbitmq \
 | 
			
		||||
@@ -281,13 +248,8 @@ Finally continue with the [All Systems](#all-systems) instructions below.
 | 
			
		||||
 | 
			
		||||
### Common to Fedora/CentOS instructions
 | 
			
		||||
 | 
			
		||||
Start by [cloning your fork of the Zulip repository][zulip-rtd-git-cloning]
 | 
			
		||||
and [connecting the Zulip upstream repository][zulip-rtd-git-connect]:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
git clone --config pull.rebase https://github.com/YOURUSERNAME/zulip.git
 | 
			
		||||
git remote add -f upstream https://github.com/zulip/zulip.git
 | 
			
		||||
```
 | 
			
		||||
Start by cloning this repository: `git clone
 | 
			
		||||
https://github.com/zulip/zulip.git`
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
# Build and install postgres tsearch-extras module
 | 
			
		||||
@@ -349,7 +311,7 @@ sudo virtualenv /srv/zulip-py3-venv -p python3 # Create a python3 virtualenv
 | 
			
		||||
sudo chown -R `whoami`:`whoami` /srv/zulip-py3-venv
 | 
			
		||||
source /srv/zulip-py3-venv/bin/activate # Activate python3 virtualenv
 | 
			
		||||
pip install --upgrade pip # upgrade pip itself because older versions have known issues
 | 
			
		||||
pip install --no-deps -r requirements/dev.txt # install python packages required for development
 | 
			
		||||
pip install --no-deps -r requirements/dev_lock.txt # install python packages required for development
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Now run these commands:
 | 
			
		||||
@@ -361,7 +323,6 @@ sudo mkdir /srv/zulip-emoji-cache
 | 
			
		||||
sudo chown -R `whoami`:`whoami` /srv/zulip-emoji-cache
 | 
			
		||||
./tools/setup/emoji/build_emoji
 | 
			
		||||
./tools/inline-email-css
 | 
			
		||||
./tools/generate-custom-icon-webfont
 | 
			
		||||
./tools/setup/build_pygments_data
 | 
			
		||||
./tools/setup/generate_zulip_bots_static_files
 | 
			
		||||
./scripts/setup/generate_secrets.py --development
 | 
			
		||||
@@ -404,63 +365,10 @@ proxy in the environment as follows:
 | 
			
		||||
 yarn config set https-proxy http://proxy_host:port
 | 
			
		||||
 ```
 | 
			
		||||
 | 
			
		||||
## Installing on cloud9
 | 
			
		||||
 | 
			
		||||
AWS Cloud9 is a cloud-based integrated development environment (IDE)
 | 
			
		||||
that lets you write, run, and debug your code with just a browser. It
 | 
			
		||||
includes a code editor, debugger, and terminal.
 | 
			
		||||
 | 
			
		||||
This section documents how to setup the Zulip development environment
 | 
			
		||||
in a cloud9 workspace.  If you don't have an existing cloud9 account,
 | 
			
		||||
you can sign up [here](https://aws.amazon.com/cloud9/).
 | 
			
		||||
 | 
			
		||||
* Create a Workspace, and select the blank template.
 | 
			
		||||
* Resize the workspace to be 1GB of memory and 4GB of disk
 | 
			
		||||
  space. (This is under free limit for both the old Cloud9 and the AWS
 | 
			
		||||
  Free Tier).
 | 
			
		||||
* Clone the zulip repo: `git clone --config pull.rebase
 | 
			
		||||
  https://github.com/<your-username>/zulip.git`
 | 
			
		||||
* Restart rabbitmq-server since its broken on cloud9: `sudo service
 | 
			
		||||
  rabbitmq-server restart`.
 | 
			
		||||
* And run provision `cd zulip && ./tools/provision`, once this is done.
 | 
			
		||||
* Activate the zulip virtual environment by `source
 | 
			
		||||
  /srv/zulip-py3-venv/bin/activate` or by opening a new terminal.
 | 
			
		||||
 | 
			
		||||
#### Install zulip-cloud9
 | 
			
		||||
 | 
			
		||||
There's an NPM package, `zulip-cloud9`, that provides a wrapper around
 | 
			
		||||
the Zulip development server for use in the Cloud9 environment.
 | 
			
		||||
 | 
			
		||||
Note: `npm i -g zulip-cloud9` does not work in zulip's virtual
 | 
			
		||||
environment.  Although by default, any packages installed in workspace
 | 
			
		||||
folder (i.e. the top level folder) are added to `$PATH`.
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
cd .. # switch to workspace folder if you are in zulip directory
 | 
			
		||||
npm i zulip-cloud9
 | 
			
		||||
zulip-dev start # to start the development server
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
If you get error of the form `bash: cannot find command zulip-dev`,
 | 
			
		||||
you need to start a new terminal.
 | 
			
		||||
 | 
			
		||||
Your development server would be running at
 | 
			
		||||
`https://<workspace-name>-<username>.c9users.io` on port 8080.  You
 | 
			
		||||
dont need to add `:8080` to your url, since the cloud9 proxy should
 | 
			
		||||
automatically forward the connection. You might want to visit
 | 
			
		||||
[zulip-cloud9 repo](https://github.com/cPhost/zulip-cloud9) and it's
 | 
			
		||||
[wiki](https://github.com/cPhost/zulip-cloud9/wiki) for more info on
 | 
			
		||||
how to use zulip-cloud9 package.
 | 
			
		||||
 | 
			
		||||
## Using Docker (experimental)
 | 
			
		||||
 | 
			
		||||
Start by [cloning your fork of the Zulip repository][zulip-rtd-git-cloning]
 | 
			
		||||
and [connecting the Zulip upstream repository][zulip-rtd-git-connect]:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
git clone --config pull.rebase https://github.com/YOURUSERNAME/zulip.git
 | 
			
		||||
git remote add -f upstream https://github.com/zulip/zulip.git
 | 
			
		||||
```
 | 
			
		||||
Start by cloning this repository: `git clone
 | 
			
		||||
https://github.com/zulip/zulip.git`
 | 
			
		||||
 | 
			
		||||
The docker instructions for development are experimental, so they may
 | 
			
		||||
have bugs.  If you try them and run into any issues, please report
 | 
			
		||||
@@ -471,8 +379,8 @@ First, you need to install Docker in your development machine
 | 
			
		||||
following the [instructions][docker-install].  Some other interesting
 | 
			
		||||
links for somebody new in Docker are:
 | 
			
		||||
 | 
			
		||||
* [Get Started](https://docs.docker.com/get-started/)
 | 
			
		||||
* [Understand the architecture](https://docs.docker.com/engine/docker-overview/)
 | 
			
		||||
* [Get Started](https://docs.docker.com/engine/installation/linux/)
 | 
			
		||||
* [Understand the architecture](https://docs.docker.com/engine/understanding-docker/)
 | 
			
		||||
* [Docker run reference](https://docs.docker.com/engine/reference/run/)
 | 
			
		||||
* [Dockerfile reference](https://docs.docker.com/engine/reference/builder/)
 | 
			
		||||
 | 
			
		||||
@@ -503,7 +411,7 @@ docker run -itv $(pwd):/srv/zulip -p 9991:9991 user/zulipdev:v2 \
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
You'll want to
 | 
			
		||||
[read the guide for Zulip development](../development/setup-vagrant.html#step-4-developing)
 | 
			
		||||
[read the guide for Zulip development](dev-env-first-time-contributors.html#step-4-developing)
 | 
			
		||||
to understand how to use the Zulip development.  Note that
 | 
			
		||||
`start-dockers` automatically runs `tools/run-dev.py` inside the
 | 
			
		||||
container; you can then visit http://localhost:9991 to connect to your
 | 
			
		||||
@@ -555,6 +463,3 @@ the results in your browser.
 | 
			
		||||
Currently, the Docker workflow is substantially less convenient than
 | 
			
		||||
the Vagrant workflow and less documented; please contribute to this
 | 
			
		||||
guide and the Docker tooling if you are using Docker to develop Zulip!
 | 
			
		||||
 | 
			
		||||
[zulip-rtd-git-cloning]: ../git/cloning.html#step-1b-clone-to-your-machine
 | 
			
		||||
[zulip-rtd-git-connect]: ../git/cloning.html#step-1c-connect-your-fork-to-zulip-upstream
 | 
			
		||||
@@ -1,12 +0,0 @@
 | 
			
		||||
#######################
 | 
			
		||||
Development Environment
 | 
			
		||||
#######################
 | 
			
		||||
 | 
			
		||||
.. toctree::
 | 
			
		||||
   :maxdepth: 3
 | 
			
		||||
 | 
			
		||||
   Development environment installation <overview>
 | 
			
		||||
   Recommended setup (Vagrant) <setup-vagrant>
 | 
			
		||||
   Advanced Setup (non-Vagrant) <setup-advanced>
 | 
			
		||||
   Using the development environment <using>
 | 
			
		||||
   Developing remotely <remote>
 | 
			
		||||
@@ -1,83 +0,0 @@
 | 
			
		||||
```eval_rst
 | 
			
		||||
:orphan:
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
# How to request a remote Zulip development instance
 | 
			
		||||
 | 
			
		||||
Under specific circumstances, typically during sprints, hackathons, and
 | 
			
		||||
Google Code-in, Zulip can provide you with a virtual machine with the
 | 
			
		||||
development environment already set up.
 | 
			
		||||
 | 
			
		||||
The machines (droplets) are being generously provided by
 | 
			
		||||
[Digital Ocean](https://www.digitalocean.com/). Thank you Digital Ocean!
 | 
			
		||||
 | 
			
		||||
## Step 1: Join GitHub and create SSH Keys
 | 
			
		||||
 | 
			
		||||
To contribute to Zulip and to use a remote Zulip developer instance, you'll
 | 
			
		||||
need a GitHub account. If you don't already have one, sign up
 | 
			
		||||
[here][github-join].
 | 
			
		||||
 | 
			
		||||
You'll also need to [create SSH keys and add them to your GitHub
 | 
			
		||||
account][github-help-add-ssh-key].
 | 
			
		||||
 | 
			
		||||
## Step 2: Create a fork of zulip/zulip
 | 
			
		||||
 | 
			
		||||
Zulip uses a **forked-repo** and **[rebase][gitbook-rebase]-oriented
 | 
			
		||||
workflow**. This means that all contributors create a fork of the [Zulip
 | 
			
		||||
repository][github-zulip-zulip] they want to contribute to and then submit pull
 | 
			
		||||
requests to the upstream repository to have their contributions reviewed and
 | 
			
		||||
accepted.
 | 
			
		||||
 | 
			
		||||
When we create your Zulip dev instance, we'll connect it to your fork of Zulip,
 | 
			
		||||
so that needs to exist before you make your request.
 | 
			
		||||
 | 
			
		||||
While you're logged in to GitHub, navigate to [zulip/zulip][github-zulip-zulip]
 | 
			
		||||
and click the **Fork** button. (See [GitHub's help article][github-help-fork]
 | 
			
		||||
for further details).
 | 
			
		||||
 | 
			
		||||
## Step 3: Make request via chat.zulip.org
 | 
			
		||||
 | 
			
		||||
Now that you have a GitHub account, have added your SSH keys, and forked
 | 
			
		||||
zulip/zulip, you are ready to request your Zulip developer instance.
 | 
			
		||||
 | 
			
		||||
If you haven't already, create an account on https://chat.zulip.org/.
 | 
			
		||||
 | 
			
		||||
Next, join the [development
 | 
			
		||||
help](https://chat.zulip.org/#narrow/stream/development.20help) stream. Create a
 | 
			
		||||
new **stream message** with your GitHub username as the **topic** and request
 | 
			
		||||
your remote dev instance. **Please make sure you have completed steps 1 and 2
 | 
			
		||||
before doing so**. A core developer should reply letting you know they're
 | 
			
		||||
working on creating it as soon as they are available to help.
 | 
			
		||||
 | 
			
		||||
Once requested, it will only take a few minutes to create your instance. You
 | 
			
		||||
will be contacted when it is complete and available.
 | 
			
		||||
 | 
			
		||||
## Next steps
 | 
			
		||||
 | 
			
		||||
Once your remote dev instance is ready:
 | 
			
		||||
 | 
			
		||||
- Connect to your server by running
 | 
			
		||||
  `ssh zulipdev@<username>.zulipdev.org` on the command line
 | 
			
		||||
  (Terminal for macOS and Linux, Bash for Git on Windows).
 | 
			
		||||
- There is no password; your account is configured to use your SSH keys.
 | 
			
		||||
- Once you log in, you should see `(zulip-py3-venv) ~$`.
 | 
			
		||||
- To start the dev server, `cd zulip` and then run `./tools/run-dev.py`.
 | 
			
		||||
- While the dev server is running, you can see the Zulip server in your browser
 | 
			
		||||
  at http://username.zulipdev.org:9991.
 | 
			
		||||
 | 
			
		||||
Once you've confirmed you can connect to your remote server, take a look at:
 | 
			
		||||
 | 
			
		||||
* [developing remotely](../development/remote.html) for tips on using the remote dev
 | 
			
		||||
  instance, and
 | 
			
		||||
* our [Git & GitHub Guide](../git/index.html) to learn how to use Git with Zulip.
 | 
			
		||||
 | 
			
		||||
Next, read the following to learn more about developing for Zulip:
 | 
			
		||||
 | 
			
		||||
* [Using the Development Environment](../development/using.html)
 | 
			
		||||
* [Testing](../testing/testing.html)
 | 
			
		||||
 | 
			
		||||
[github-join]: https://github.com/join
 | 
			
		||||
[github-help-add-ssh-key]: https://help.github.com/articles/adding-a-new-ssh-key-to-your-github-account/
 | 
			
		||||
[github-zulip-zulip]: https://github.com/zulip/zulip/
 | 
			
		||||
[github-help-fork]: https://help.github.com/articles/fork-a-repo/
 | 
			
		||||
[gitbook-rebase]: https://git-scm.com/book/en/v2/Git-Branching-Rebasing
 | 
			
		||||
@@ -4,7 +4,7 @@ This page documents the Zulip directory structure, where to find
 | 
			
		||||
things, and how to decide where to put a file.
 | 
			
		||||
 | 
			
		||||
You may also find the [new application feature
 | 
			
		||||
tutorial](../tutorials/new-feature-tutorial.html) helpful for understanding the
 | 
			
		||||
tutorial](new-feature-tutorial.html) helpful for understanding the
 | 
			
		||||
flow through these files.
 | 
			
		||||
 | 
			
		||||
### Core Python files
 | 
			
		||||
@@ -25,16 +25,15 @@ paths will be familiar to Django developers.
 | 
			
		||||
 | 
			
		||||
* `zerver/views/*.py` Most [Django views](https://docs.djangoproject.com/en/1.8/topics/http/views/).
 | 
			
		||||
 | 
			
		||||
* `zerver/webhooks/` Webhook views and tests for [Zulip webhook integrations](
 | 
			
		||||
  https://zulipchat.com/api/integration-guide).
 | 
			
		||||
* `zerver/webhooks/` Webhook views and tests for [Zulip webhook integrations](integration-guide.html).
 | 
			
		||||
 | 
			
		||||
* `zerver/tornado/views.py` Tornado views.
 | 
			
		||||
 | 
			
		||||
* `zerver/worker/queue_processors.py` [Queue workers](../subsystems/queuing.html).
 | 
			
		||||
* `zerver/worker/queue_processors.py` [Queue workers](queuing.html).
 | 
			
		||||
 | 
			
		||||
* `zerver/lib/*.py` Most library code.
 | 
			
		||||
 | 
			
		||||
* `zerver/lib/bugdown/` [Backend Markdown processor](../subsystems/markdown.html).
 | 
			
		||||
* `zerver/lib/bugdown/` [Backend Markdown processor](markdown.html).
 | 
			
		||||
 | 
			
		||||
* `zproject/backends.py` [Authentication backends](https://docs.djangoproject.com/en/1.8/topics/auth/customizing/).
 | 
			
		||||
 | 
			
		||||
@@ -42,7 +41,7 @@ paths will be familiar to Django developers.
 | 
			
		||||
 | 
			
		||||
### HTML Templates
 | 
			
		||||
 | 
			
		||||
See [our docs](../subsystems/html-templates.html) for details on Zulip's
 | 
			
		||||
See [our docs](html-templates.html) for details on Zulip's
 | 
			
		||||
templating systems.
 | 
			
		||||
 | 
			
		||||
* `templates/zerver/` For [Jinja2](http://jinja.pocoo.org/) templates
 | 
			
		||||
@@ -87,7 +86,7 @@ These are distinguished from scripts, below, by needing to run a
 | 
			
		||||
Django context (i.e. with database access).
 | 
			
		||||
 | 
			
		||||
* `zerver/management/commands/`
 | 
			
		||||
  [Management commands](../subsystems/management-commands.html) one might run at a
 | 
			
		||||
  [Management commands](management-commands.html) one might run at a
 | 
			
		||||
  production deployment site (e.g. scripts to change a value or
 | 
			
		||||
  deactivate a user properly).
 | 
			
		||||
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
 | 
			
		||||
This page has developer documentation on the Zulip email system. If you're
 | 
			
		||||
trying to configure your server to send email, you might be looking for our
 | 
			
		||||
guide to [sending outgoing email](../production/email.html). If you're trying to
 | 
			
		||||
guide to [sending outgoing email](prod-email.html). If you're trying to
 | 
			
		||||
configure an email integration to receive incoming email (e.g. so that users
 | 
			
		||||
can reply to missed message emails via email), you might be interested in
 | 
			
		||||
our instructions for
 | 
			
		||||
@@ -40,7 +40,7 @@ Email takes about a quarter second per email to process and send. Generally
 | 
			
		||||
speaking, if you're sending just one email, doing it in the current process
 | 
			
		||||
is fine. If you're sending emails in a loop, you probably want to send it
 | 
			
		||||
from a queue. Documentation on our queueing system is available
 | 
			
		||||
[here](../subsystems/queuing.html).
 | 
			
		||||
[here](queuing.html).
 | 
			
		||||
 | 
			
		||||
## Development and testing
 | 
			
		||||
 | 
			
		||||
@@ -56,23 +56,6 @@ our custom backend, `EmailLogBackEnd`.  It does the following:
 | 
			
		||||
* Print a friendly message on console advertising `/emails` to make
 | 
			
		||||
  this nice and discoverable.
 | 
			
		||||
 | 
			
		||||
You can also forward all the emails sent in the development environment
 | 
			
		||||
to an email id of your choice by clicking on **Forward emails to a mail
 | 
			
		||||
account** in `/emails` page. This feature can be used for testing how
 | 
			
		||||
emails gets rendered by different email clients. Before enabling this
 | 
			
		||||
you have to first configure the following SMTP settings.
 | 
			
		||||
 | 
			
		||||
* The hostname `EMAIL_HOST` in `zproject/dev_settings.py`
 | 
			
		||||
* The username `EMAIL_HOST_USER` in `zproject/dev_settings.py`.
 | 
			
		||||
* The password `email_password` in `zproject/dev-secrets.conf`.
 | 
			
		||||
 | 
			
		||||
See [this](../production/email.html#free-outgoing-email-services)
 | 
			
		||||
section for instructions on obtaining SMTP details.
 | 
			
		||||
 | 
			
		||||
**Note: The base_image_uri of the images in forwarded emails would be replaced
 | 
			
		||||
with `https://chat.zulip.org/static/images/emails` inorder for the email clients
 | 
			
		||||
to render the images. See `zproject/email_backends.py` for more details.**
 | 
			
		||||
 | 
			
		||||
While running the backend test suite, we use
 | 
			
		||||
`django.core.mail.backends.locmem.EmailBackend` as the email
 | 
			
		||||
backend. The `locmem` backend stores messages in a special attribute
 | 
			
		||||
							
								
								
									
										92
									
								
								docs/emoji.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								docs/emoji.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,92 @@
 | 
			
		||||
# Emoji
 | 
			
		||||
 | 
			
		||||
Emoji seem like a simple idea, but there's actually a ton of
 | 
			
		||||
complexity that goes into an effective emoji implementation.  This
 | 
			
		||||
document discusses a number of these issues.
 | 
			
		||||
 | 
			
		||||
Currently, Zulip uses the Noto (Android) emoji set, but we are close
 | 
			
		||||
to being able to support the user choosing which emoji set they want
 | 
			
		||||
to use.
 | 
			
		||||
 | 
			
		||||
## Emoji codes
 | 
			
		||||
 | 
			
		||||
The Unicode standard has various ranges of characters set aside for
 | 
			
		||||
emoji.  So you can put emoji in your terminal using actual unicode
 | 
			
		||||
characters like 😀  and 👍.  If you paste those into Zulip, Zulip will
 | 
			
		||||
render them as the corresponding emoji image.
 | 
			
		||||
 | 
			
		||||
However, the Unicode committee did not standardize on a set of
 | 
			
		||||
human-readable names for emoji.  So, for example, when using the
 | 
			
		||||
popular `:` based style for entering emoji from the keyboard, we have
 | 
			
		||||
to decide whether to use `:angry:` or `:angry_face:` to represent an
 | 
			
		||||
angry face.  Different products use different approaches, but for
 | 
			
		||||
purposes like emoji pickers or autocomplete, you definitely want to
 | 
			
		||||
pick exactly one of these names, since otherwise users will always be
 | 
			
		||||
seeing duplicates of a given emoji next to each other.
 | 
			
		||||
 | 
			
		||||
Picking which emoji name to use is surprisingly complicated!  Zulip
 | 
			
		||||
has a nice library, `tools/setup/emoji/emoji_setup_utils.py`, which we
 | 
			
		||||
use to make these decisions systematically, with a relatively small
 | 
			
		||||
list of hand-coded exceptions.
 | 
			
		||||
 | 
			
		||||
### Custom emoji
 | 
			
		||||
 | 
			
		||||
Zulip supports custom user-uploaded emoji.  We manage those by having
 | 
			
		||||
the name of the emoji be its "emoji code", and using an emoji_type
 | 
			
		||||
field to keep track of it.  We are in the progress of migrating Zulip
 | 
			
		||||
to refer to these emoji only by ID, which is a requirement for being
 | 
			
		||||
able to support deprecating old realm emoji in a sensible way.
 | 
			
		||||
 | 
			
		||||
## Tooling
 | 
			
		||||
 | 
			
		||||
We use the [iamcal emoji data package][iamcal] to provide sprite
 | 
			
		||||
sheets and individual images for our emoji, as well as a data set of
 | 
			
		||||
emoji categories, code points, names, etc.  The sprite sheets are used
 | 
			
		||||
by the Zulip webapp to display emoji in messages, emoji reactions,
 | 
			
		||||
etc.  However, we can't use the sprite sheets in some contexts, such
 | 
			
		||||
as missed-message and digestemails, that need to have self-contained
 | 
			
		||||
assets.  For those, we use individual emoji files under
 | 
			
		||||
`static/generated/emoji`.  The structure of that repository contains
 | 
			
		||||
both files named after the unicode representation of emoji (as actual
 | 
			
		||||
image files) as well as symlinks pointing to those emoji.
 | 
			
		||||
 | 
			
		||||
We need to maintain those both for the names used in the iamcal emoji
 | 
			
		||||
data set as well as our old emoji data set (`emoji_map.json`).  Zulip
 | 
			
		||||
has a tool, `tools/setup/emoji/build_emoji`, that combines the
 | 
			
		||||
`emoji.json` file from iamcal with the old `emoji-map.json` data set
 | 
			
		||||
to construct the various symlink farms and output files described
 | 
			
		||||
below that support our emoji experience.
 | 
			
		||||
 | 
			
		||||
The `build_emoji` tool generates the set of files under
 | 
			
		||||
`static/generated/emoji` (or really, it generates the
 | 
			
		||||
`/srv/zulip-emoji-cache/<sha1>/emoji` tree, and
 | 
			
		||||
`static/generated/emoji` is a symlink to that tree; we do this in
 | 
			
		||||
order to cache old versions to make provisioning and production
 | 
			
		||||
deployments super fast in the common case that we haven't changed the
 | 
			
		||||
emoji tooling).  See [our dependencies document](dependencies.html)
 | 
			
		||||
for more details on this strategy.
 | 
			
		||||
 | 
			
		||||
The emoji tree generated by this process contains several import elements:
 | 
			
		||||
* `emoji_codes.js`: A set of mappings used by the Zulip frontend to
 | 
			
		||||
  understand what unicode emoji exist and what their shortnames are,
 | 
			
		||||
  used for autocomplete, emoji pickers, etc.  This has been
 | 
			
		||||
  deduplicated using the logic in
 | 
			
		||||
  `tools/setup/emoji/emoji_setup_utils.py` to generally only have
 | 
			
		||||
  `:angry:` and not also `:angry_face:`, since having both is ugly and
 | 
			
		||||
  pointless for purposes like autocomplete and emoji pickers.
 | 
			
		||||
* `images/emoji/unicode/*.png`: A farm of emoji
 | 
			
		||||
* `images/emoji/*.png`: A farm of symlinks from emoji names to the
 | 
			
		||||
  `images/emoji/unicode/` tree.  This is used to serve individual emoji
 | 
			
		||||
  images, as well as for the
 | 
			
		||||
  [backend markdown processor](markdown.html) to know which emoji
 | 
			
		||||
  names exist and what unicode emoji / images they map to.  In this
 | 
			
		||||
  tree, we currently include all of the emoji in `emoji-map.json`;
 | 
			
		||||
  this means that if you send `:angry_face:`, it won't autocomplete,
 | 
			
		||||
  but will still work (but not in previews).
 | 
			
		||||
* Some CSS and PNGs for the emoji spritesheets, used in Zulip for
 | 
			
		||||
  emoji pickers where we would otherwise need to download over 1000 of
 | 
			
		||||
  individual emoji images (which would cause a browser performance
 | 
			
		||||
  problem).  We have multiple spritesheets: one for each emoji
 | 
			
		||||
  provider that we support (Google, Twitter, EmojiOne, etc.).
 | 
			
		||||
 | 
			
		||||
[iamcal]: https://github.com/iamcal/emoji-data
 | 
			
		||||
@@ -4,7 +4,7 @@ Zulip's "events system" is the server-to-client push system that
 | 
			
		||||
powers our real-time sync.  This document explains how it works; to
 | 
			
		||||
read an example of how a complete feature using this system works,
 | 
			
		||||
check out the
 | 
			
		||||
[new application feature tutorial](../tutorials/new-feature-tutorial.html).
 | 
			
		||||
[new application feature tutorial](new-feature-tutorial.html).
 | 
			
		||||
 | 
			
		||||
Any single-page web application like Zulip needs a story for how
 | 
			
		||||
changes made by one client are synced to other clients, though having
 | 
			
		||||
@@ -85,7 +85,7 @@ wide range of possible clients, and make it easy for developers.
 | 
			
		||||
Zulip's event delivery (real-time push) system is based on Tornado,
 | 
			
		||||
which is ideal for handling a large number of open requests.  Details
 | 
			
		||||
on Tornado are available in the
 | 
			
		||||
[architecture overview](../overview/architecture-overview.html), but in short it
 | 
			
		||||
[architecture overview](architecture-overview.html), but in short it
 | 
			
		||||
is good at holding open a large number of connections for a long time.
 | 
			
		||||
The complete system is about 1500 lines of code in `zerver/tornado/`,
 | 
			
		||||
primarily `zerver/tornado/event_queue.py`.
 | 
			
		||||
@@ -125,7 +125,7 @@ parameters, updating `last_event_id` each time to acknowledge any
 | 
			
		||||
events it has received (see `call_on_each_event` in the
 | 
			
		||||
[Zulip Python API bindings][api-bindings-code] for a complete example
 | 
			
		||||
implementation).  When handling each `GET /json/events` request, the
 | 
			
		||||
queue server can safely delete any events that have an event ID less
 | 
			
		||||
queue server can safely delete any events have have an event ID less
 | 
			
		||||
than or equal to the client's `last_event_id` (event IDs are just a
 | 
			
		||||
counter for the events a given queue has received.)
 | 
			
		||||
 | 
			
		||||
@@ -140,7 +140,7 @@ those events could be lost).
 | 
			
		||||
[api-bindings-code]: https://github.com/zulip/python-zulip-api/blob/master/zulip/zulip/__init__.py
 | 
			
		||||
 | 
			
		||||
The queue servers are a very high-traffic system, processing at a
 | 
			
		||||
minimum one request for every message delivered to every Zulip client.
 | 
			
		||||
minimum a request for every message delivered to every Zulip client.
 | 
			
		||||
Additionally, as a workaround for low-quality NAT servers that kill
 | 
			
		||||
HTTP connections that are open without activity for more than 60s, the
 | 
			
		||||
queue servers also send a heartbeat event to each queue at least once
 | 
			
		||||
@@ -1,7 +1,3 @@
 | 
			
		||||
```eval_rst
 | 
			
		||||
:orphan:
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
# Running expensive migrations early
 | 
			
		||||
 | 
			
		||||
Zulip 1.7 contains some significant database migrations that can take
 | 
			
		||||
@@ -3,9 +3,9 @@
 | 
			
		||||
This page documents additional information that may be useful when
 | 
			
		||||
developing new features for Zulip that require front-end changes,
 | 
			
		||||
especially those that involve adding new files. For a more general
 | 
			
		||||
overview, see the [new feature tutorial](../tutorials/new-feature-tutorial.html).
 | 
			
		||||
overview, see the [new feature tutorial](new-feature-tutorial.html).
 | 
			
		||||
 | 
			
		||||
Our [dependencies documentation](../subsystems/dependencies.html) has useful
 | 
			
		||||
Our [dependencies documentation](dependencies.html) has useful
 | 
			
		||||
relevant background as well.
 | 
			
		||||
 | 
			
		||||
## Primary build process
 | 
			
		||||
@@ -199,7 +199,7 @@ with Zulip could think of this as streams that are online.
 | 
			
		||||
 | 
			
		||||
* Bot - **Bot**
 | 
			
		||||
 | 
			
		||||
Not only is "bot" a short and easily memorable term, it is also widely used
 | 
			
		||||
Not only is "bot" a short and easily rememberable term, it is also widely used
 | 
			
		||||
in German technology magazines, forums, etc.
 | 
			
		||||
 | 
			
		||||
*"Bot" (Transifex, Heise, Die Zeit)*
 | 
			
		||||
							
								
								
									
										54
									
								
								docs/git-cheat-sheet-detailed.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								docs/git-cheat-sheet-detailed.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,54 @@
 | 
			
		||||
# Git Cheat Sheet (Detailed)
 | 
			
		||||
 | 
			
		||||
See also
 | 
			
		||||
[fixing commits][fix-commit]
 | 
			
		||||
 | 
			
		||||
Commands:
 | 
			
		||||
 | 
			
		||||
- add
 | 
			
		||||
    - `git add foo.py`: add `foo.py` to the staging area
 | 
			
		||||
    - `git add foo.py bar.py`: add `foo.py` AND `bar.py` to the staging area
 | 
			
		||||
- checkout
 | 
			
		||||
    - `git checkout -b new-branch-name`: create branch `new-branch-name` and switch/checkout to that new branch
 | 
			
		||||
    - `git checkout master`: switch to your `master` branch
 | 
			
		||||
    - `git checkout old-branch-name`: switch to an existing branch `old-branch-name`
 | 
			
		||||
- commit
 | 
			
		||||
    - `git commit --amend`: changing the last commit message. Read more [here][fix-commit]
 | 
			
		||||
- config
 | 
			
		||||
    - `git config --global core.editor nano`: set core editor to `nano` (you can set this to `vim` or others)
 | 
			
		||||
    - `git config --global core.symlinks true`: allow symbolic links
 | 
			
		||||
- diff
 | 
			
		||||
    - `git diff`: display the changes you have made to all files
 | 
			
		||||
    - `git diff --cached`: display the changes you have made to staged files
 | 
			
		||||
    - `git diff HEAD~2..`: display the 2 most recent changes you have made to files
 | 
			
		||||
- fetch
 | 
			
		||||
    - `git fetch origin`: fetch origin repository
 | 
			
		||||
    - `git fetch upstream`: fetch upstream repository
 | 
			
		||||
- grep
 | 
			
		||||
    - `git grep update_unread_counts -- '*.js'`: search all files (ending in `.js`) for `update_unread_counts`
 | 
			
		||||
- log
 | 
			
		||||
    - `git log`: show commit logs
 | 
			
		||||
- pull
 | 
			
		||||
    - **do not use for Zulip**
 | 
			
		||||
- push
 | 
			
		||||
    - `git push origin +branch-name`: push your commits to your origin repository
 | 
			
		||||
- rebase
 | 
			
		||||
    - `git rebase -i HEAD~3`: interactive rebasing current branch with first three items on HEAD
 | 
			
		||||
    - `git rebase -i master`: interactive rebasing current branch with master branch
 | 
			
		||||
    - `git rebase upstream/master`: rebasing current branch with master branch from upstream repository
 | 
			
		||||
- reflog
 | 
			
		||||
    - `git reflog | head -10`: manage reference logs for the past 10 commits
 | 
			
		||||
- remote
 | 
			
		||||
    - `git remote -v`: display your origin and upstream repositories
 | 
			
		||||
- reset
 | 
			
		||||
    - `git reset HEAD~2`: reset two most recent commits
 | 
			
		||||
- rm
 | 
			
		||||
    - `git rm oops.txt`: remove `oops.txt`
 | 
			
		||||
- show
 | 
			
		||||
    - `git show HEAD`: display most recent commit
 | 
			
		||||
    - `git show HEAD~~~`: display third most recent commit
 | 
			
		||||
    - `git show master`: display most recent commit on `master`
 | 
			
		||||
- status
 | 
			
		||||
    - `git status`: show the working tree status, unstaged and staged files
 | 
			
		||||
 | 
			
		||||
[fix-commit]: fixing-commits.html
 | 
			
		||||
							
								
								
									
										52
									
								
								docs/git-cheat-sheet.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								docs/git-cheat-sheet.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
			
		||||
# Git Cheat Sheet
 | 
			
		||||
 | 
			
		||||
See also [fixing commits][fix-commit]
 | 
			
		||||
 | 
			
		||||
Commands:
 | 
			
		||||
 | 
			
		||||
- add
 | 
			
		||||
    - `git add foo.py`
 | 
			
		||||
- checkout
 | 
			
		||||
    - `git checkout -b new-branch-name`
 | 
			
		||||
    - `git checkout master`
 | 
			
		||||
    - `git checkout old-branch-name`
 | 
			
		||||
- commit
 | 
			
		||||
    - `git commit --amend`
 | 
			
		||||
- config
 | 
			
		||||
    - `git config --global core.editor nano`
 | 
			
		||||
    - `git config --global core.symlinks true`
 | 
			
		||||
- diff
 | 
			
		||||
    - `git diff`
 | 
			
		||||
    - `git diff --cached`
 | 
			
		||||
    - `git diff HEAD~2..`
 | 
			
		||||
- fetch
 | 
			
		||||
    - `git fetch origin`
 | 
			
		||||
    - `git fetch upstream`
 | 
			
		||||
- grep
 | 
			
		||||
    - `git grep update_unread_counts -- '*.js'`
 | 
			
		||||
- log
 | 
			
		||||
    - `git log`
 | 
			
		||||
- pull
 | 
			
		||||
    - **do not use for Zulip**
 | 
			
		||||
- push
 | 
			
		||||
    - `git push origin +branch-name`
 | 
			
		||||
- rebase
 | 
			
		||||
    - `git rebase -i HEAD~3`
 | 
			
		||||
    - `git rebase -i master`
 | 
			
		||||
    - `git rebase upstream/master`
 | 
			
		||||
- reflog
 | 
			
		||||
    - `git reflog | head -10`
 | 
			
		||||
- remote
 | 
			
		||||
    - `git remote -v`
 | 
			
		||||
- reset
 | 
			
		||||
    - `git reset HEAD~2`
 | 
			
		||||
- rm
 | 
			
		||||
    - `git rm oops.txt`
 | 
			
		||||
- show
 | 
			
		||||
    - `git show HEAD`
 | 
			
		||||
    - `git show HEAD~~~`
 | 
			
		||||
    - `git show master`
 | 
			
		||||
- status
 | 
			
		||||
    - `git status`
 | 
			
		||||
 | 
			
		||||
[fix-commit]: fixing-commits.html
 | 
			
		||||
							
								
								
									
										1569
									
								
								docs/git-guide.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1569
									
								
								docs/git-guide.md
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,116 +0,0 @@
 | 
			
		||||
# Git Cheat Sheet
 | 
			
		||||
 | 
			
		||||
See also [fixing commits][fix-commit]
 | 
			
		||||
 | 
			
		||||
## Common Commands
 | 
			
		||||
 | 
			
		||||
- add
 | 
			
		||||
    - `git add foo.py`
 | 
			
		||||
- checkout
 | 
			
		||||
    - `git checkout -b new-branch-name`
 | 
			
		||||
    - `git checkout master`
 | 
			
		||||
    - `git checkout old-branch-name`
 | 
			
		||||
- commit
 | 
			
		||||
    - `git commit -m "topic: Commit message title."`
 | 
			
		||||
    - `git commit --amend`: Modify the previous commit.
 | 
			
		||||
- config
 | 
			
		||||
    - `git config --global core.editor nano`
 | 
			
		||||
    - `git config --global core.symlinks true`
 | 
			
		||||
- diff
 | 
			
		||||
    - `git diff`
 | 
			
		||||
    - `git diff --cached`
 | 
			
		||||
    - `git diff HEAD~2..`
 | 
			
		||||
- fetch
 | 
			
		||||
    - `git fetch origin`
 | 
			
		||||
    - `git fetch upstream`
 | 
			
		||||
- grep
 | 
			
		||||
    - `git grep update_unread_counts -- '*.js'`
 | 
			
		||||
- log
 | 
			
		||||
    - `git log`
 | 
			
		||||
- pull
 | 
			
		||||
    - `git pull --rebase`: **Use this**. Zulip uses a [rebase oriented workflow][git-overview].
 | 
			
		||||
    - `git pull` (with no options): Will either create a merge commit
 | 
			
		||||
      (which you don't want) or do the same thing as `git pull --rebase`,
 | 
			
		||||
      depending on [whether you're configured Git properly][git-clone-config]
 | 
			
		||||
- push
 | 
			
		||||
    - `git push origin +branch-name`
 | 
			
		||||
- rebase
 | 
			
		||||
    - `git rebase -i HEAD~3`
 | 
			
		||||
    - `git rebase -i master`
 | 
			
		||||
    - `git rebase upstream/master`
 | 
			
		||||
- reflog
 | 
			
		||||
    - `git reflog | head -10`
 | 
			
		||||
- remote
 | 
			
		||||
    - `git remote -v`
 | 
			
		||||
- reset
 | 
			
		||||
    - `git reset HEAD~2`
 | 
			
		||||
- rm
 | 
			
		||||
    - `git rm oops.txt`
 | 
			
		||||
- show
 | 
			
		||||
    - `git show HEAD`
 | 
			
		||||
    - `git show HEAD~~~`
 | 
			
		||||
    - `git show master`
 | 
			
		||||
- status
 | 
			
		||||
    - `git status`
 | 
			
		||||
 | 
			
		||||
## Detailed Cheat Sheet
 | 
			
		||||
 | 
			
		||||
- add
 | 
			
		||||
    - `git add foo.py`: add `foo.py` to the staging area
 | 
			
		||||
    - `git add foo.py bar.py`: add `foo.py` AND `bar.py` to the staging area
 | 
			
		||||
    - `git add -u`: Adds all tracked files to the staging area.
 | 
			
		||||
- checkout
 | 
			
		||||
    - `git checkout -b new-branch-name`: create branch `new-branch-name` and switch/checkout to that new branch
 | 
			
		||||
    - `git checkout master`: switch to your `master` branch
 | 
			
		||||
    - `git checkout old-branch-name`: switch to an existing branch `old-branch-name`
 | 
			
		||||
- commit
 | 
			
		||||
    - `git commit -m "commit message"`: It is recommended to type a
 | 
			
		||||
       multiline commit message, however.
 | 
			
		||||
    - `git commit`: Opens your default text editor to write a commit message.
 | 
			
		||||
    - `git commit --amend`: changing the last commit message. Read more [here][fix-commit]
 | 
			
		||||
- config
 | 
			
		||||
    - `git config --global core.editor nano`: set core editor to `nano` (you can set this to `vim` or others)
 | 
			
		||||
    - `git config --global core.symlinks true`: allow symbolic links
 | 
			
		||||
- diff
 | 
			
		||||
    - `git diff`: display the changes you have made to all files
 | 
			
		||||
    - `git diff --cached`: display the changes you have made to staged files
 | 
			
		||||
    - `git diff HEAD~2..`: display the 2 most recent changes you have made to files
 | 
			
		||||
- fetch
 | 
			
		||||
    - `git fetch origin`: fetch origin repository
 | 
			
		||||
    - `git fetch upstream`: fetch upstream repository
 | 
			
		||||
- grep
 | 
			
		||||
    - `git grep update_unread_counts -- '*.js'`: search all files (ending in `.js`) for `update_unread_counts`
 | 
			
		||||
- log
 | 
			
		||||
    - `git log`: show commit logs
 | 
			
		||||
    - `git log --oneline | head`: To quickly see the latest ten commits on a branch.
 | 
			
		||||
- pull
 | 
			
		||||
    - `git pull --rebase`: rebase your changes on top of master.
 | 
			
		||||
    - `git pull` (with no options): Will either create a merge commit
 | 
			
		||||
      (which you don't want) or do the same thing as `git pull --rebase`,
 | 
			
		||||
      depending on [whether you're configured Git properly][git-clone-config]
 | 
			
		||||
- push
 | 
			
		||||
    - `git push origin branch-name`: push you commits to the origin repository *only if* there are no conflicts.
 | 
			
		||||
      Use this when collaborating with others to prevent overwriting their work.
 | 
			
		||||
    - `git push origin +branch-name`: force push your commits to your origin repository.
 | 
			
		||||
- rebase
 | 
			
		||||
    - `git rebase -i HEAD~3`: interactive rebasing current branch with first three items on HEAD
 | 
			
		||||
    - `git rebase -i master`: interactive rebasing current branch with master branch
 | 
			
		||||
    - `git rebase upstream/master`: rebasing current branch with master branch from upstream repository
 | 
			
		||||
- reflog
 | 
			
		||||
    - `git reflog | head -10`: manage reference logs for the past 10 commits
 | 
			
		||||
- remote
 | 
			
		||||
    - `git remote -v`: display your origin and upstream repositories
 | 
			
		||||
- reset
 | 
			
		||||
    - `git reset HEAD~2`: reset two most recent commits
 | 
			
		||||
- rm
 | 
			
		||||
    - `git rm oops.txt`: remove `oops.txt`
 | 
			
		||||
- show
 | 
			
		||||
    - `git show HEAD`: display most recent commit
 | 
			
		||||
    - `git show HEAD~~~`: display third most recent commit
 | 
			
		||||
    - `git show master`: display most recent commit on `master`
 | 
			
		||||
- status
 | 
			
		||||
    - `git status`: show the working tree status, unstaged and staged files
 | 
			
		||||
 | 
			
		||||
[fix-commit]: fixing-commits.html
 | 
			
		||||
[git-config-clone]: cloning.html#step-1b-clone-to-your-machine
 | 
			
		||||
[git-overview]: overview.html
 | 
			
		||||
@@ -1,141 +0,0 @@
 | 
			
		||||
# Get Zulip code
 | 
			
		||||
 | 
			
		||||
Zulip uses a **forked-repo** and **[rebase][gitbook-rebase]-oriented
 | 
			
		||||
workflow.**. This means that all contributors create a fork of the [Zulip
 | 
			
		||||
repository][github-zulip] they want to contribute to and then submit pull
 | 
			
		||||
requests to the upstream repository to have their contributions reviewed and
 | 
			
		||||
accepted. We also recommend you work on feature branches.
 | 
			
		||||
 | 
			
		||||
## Step 1a: Create your fork
 | 
			
		||||
 | 
			
		||||
The following steps you'll only need to do the first time you setup a machine
 | 
			
		||||
for contributing to a given Zulip project. You'll need to repeat the steps for
 | 
			
		||||
any additional Zulip projects ([list][github-zulip]) that you work on.
 | 
			
		||||
 | 
			
		||||
The first thing you'll want to do to contribute to Zulip is fork ([see
 | 
			
		||||
how][github-help-fork]) the appropriate [Zulip repository][github-zulip]. For
 | 
			
		||||
the main server app, this is [zulip/zulip][github-zulip-zulip].
 | 
			
		||||
 | 
			
		||||
## Step 1b: Clone to your machine
 | 
			
		||||
 | 
			
		||||
Next, clone your fork to your local machine:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
$ git clone --config pull.rebase git@github.com:christi3k/zulip.git
 | 
			
		||||
Cloning into 'zulip'
 | 
			
		||||
remote: Counting objects: 86768, done.
 | 
			
		||||
remote: Compressing objects: 100% (15/15), done.
 | 
			
		||||
remote: Total 86768 (delta 5), reused 1 (delta 1), pack-reused 86752
 | 
			
		||||
Receiving objects: 100% (86768/86768), 112.96 MiB | 523.00 KiB/s, done.
 | 
			
		||||
Resolving deltas: 100% (61106/61106), done.
 | 
			
		||||
Checking connectivity... done.
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
(The `--config pull.rebase` option configures Git so that `git pull`
 | 
			
		||||
will behave like `git pull --rebase` by default.  Using `git pull
 | 
			
		||||
--rebase` to update your changes to resolve merge conflicts is
 | 
			
		||||
expected by essentially all of open source projects, including Zulip.
 | 
			
		||||
You can also set that option after cloning using `git config --add
 | 
			
		||||
pull.rebase true`, or just be careful to always run `git pull
 | 
			
		||||
--rebase`, never `git pull`).
 | 
			
		||||
 | 
			
		||||
Note: If you receive an error while cloning, you may not have [added your ssh
 | 
			
		||||
key to GitHub][github-help-add-ssh-key].
 | 
			
		||||
 | 
			
		||||
Once the repository is cloned, we recommend running
 | 
			
		||||
[setup-git-repo][zulip-rtd-tools-setup] to install Zulip's pre-commit
 | 
			
		||||
hook which runs the Zulip linters on the changed files when you
 | 
			
		||||
commit.
 | 
			
		||||
 | 
			
		||||
## Step 1c: Connect your fork to Zulip upstream
 | 
			
		||||
 | 
			
		||||
Next you'll want to [configure an upstream remote
 | 
			
		||||
repository][github-help-conf-remote] for your fork of Zulip. This will allow
 | 
			
		||||
you to [sync changes][github-help-sync-fork] from the main project back into
 | 
			
		||||
your fork.
 | 
			
		||||
 | 
			
		||||
First, show the currently configured remote repository:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
$ git remote -v
 | 
			
		||||
origin  git@github.com:YOUR_USERNAME/zulip.git (fetch)
 | 
			
		||||
origin  git@github.com:YOUR_USERNAME/zulip.git (push)
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Note: If you've cloned the repository using a graphical client, you may already
 | 
			
		||||
have the upstream remote repository configured. For example, when you clone
 | 
			
		||||
[zulip/zulip][github-zulip-zulip] with the GitHub desktop client it configures
 | 
			
		||||
the remote repository `zulip` and you see the following output from `git remote
 | 
			
		||||
-v`:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
origin  git@github.com:YOUR_USERNAME/zulip.git (fetch)
 | 
			
		||||
origin  git@github.com:YOUR_USERNAME/zulip.git (push)
 | 
			
		||||
zulip    https://github.com/zulip/zulip.git (fetch)
 | 
			
		||||
zulip    https://github.com/zulip/zulip.git (push)
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
If your client hasn't automatically configured a remote for zulip/zulip, you'll
 | 
			
		||||
need to with:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
$ git remote add -f upstream https://github.com/zulip/zulip.git
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Finally, confirm that the new remote repository, upstream, has been configured:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
$ git remote -v
 | 
			
		||||
origin  git@github.com:YOUR_USERNAME/zulip.git (fetch)
 | 
			
		||||
origin  git@github.com:YOUR_USERNAME/zulip.git (push)
 | 
			
		||||
upstream https://github.com/zulip/zulip.git (fetch)
 | 
			
		||||
upstream https://github.com/zulip/zulip.git (push)
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Step 2: Set up the Zulip development environment
 | 
			
		||||
 | 
			
		||||
If you haven't already, now is a good time to install the Zulip development environment
 | 
			
		||||
([overview][zulip-rtd-dev-overview]). If you're new to working on Zulip or open
 | 
			
		||||
source projects in general, we recommend following our [detailed guide for
 | 
			
		||||
first-time contributors][zulip-rtd-dev-first-time].
 | 
			
		||||
 | 
			
		||||
## Step 3: Configure Travis CI (continuous integration)
 | 
			
		||||
 | 
			
		||||
This step is optional, but recommended.
 | 
			
		||||
 | 
			
		||||
The Zulip Server project is configured to use [Travis CI][travis-ci]
 | 
			
		||||
to test and create builds upon each new commit and pull
 | 
			
		||||
request. Travis CI is free for open source projects and it's easy to
 | 
			
		||||
configure for your own fork of Zulip. After doing so, Travis CI will
 | 
			
		||||
run tests for new refs you push to GitHub and email you the outcome
 | 
			
		||||
(you can also view the results in the web interface).
 | 
			
		||||
 | 
			
		||||
Running Travis CI against your fork can help save both you and the
 | 
			
		||||
Zulip maintainers time by making it easy to test a change fully before
 | 
			
		||||
submitting a pull request.  We generally recommend a worfklow where as
 | 
			
		||||
you make changes, you use a fast edit-refresh cycle running individual
 | 
			
		||||
tests locally until your changes work.  But then once you've gotten
 | 
			
		||||
the tests you'd expect to be relevant to your changes working, push a
 | 
			
		||||
branch to Travis CI to run the full test suite before you create a
 | 
			
		||||
pull request.  While you wait for Travis CI to run, you can start
 | 
			
		||||
working on your next task.  When the tests finish, you can create a
 | 
			
		||||
pull request that you already know passes the tests.
 | 
			
		||||
 | 
			
		||||
First, sign in to [Travis CI][travis-ci] with your GitHub account and authorize
 | 
			
		||||
Travis CI to access your GitHub account and repositories. Once you've done
 | 
			
		||||
this, Travis CI will fetch your repository information and display it on your
 | 
			
		||||
[profile page][travis-ci-profile]. From there you can enable integration with
 | 
			
		||||
Zulip. ([See screen cast](../_static/zulip-travisci.gif).)
 | 
			
		||||
 | 
			
		||||
[gitbook-rebase]: https://git-scm.com/book/en/v2/Git-Branching-Rebasing
 | 
			
		||||
[github-help-add-ssh-key]: https://help.github.com/articles/adding-a-new-ssh-key-to-your-github-account/
 | 
			
		||||
[github-help-conf-remote]: https://help.github.com/articles/configuring-a-remote-for-a-fork/
 | 
			
		||||
[github-help-fork]: https://help.github.com/articles/fork-a-repo/
 | 
			
		||||
[github-help-sync-fork]: https://help.github.com/articles/syncing-a-fork/
 | 
			
		||||
[github-zulip]: https://github.com/zulip/
 | 
			
		||||
[github-zulip-zulip]: https://github.com/zulip/zulip/
 | 
			
		||||
[travis-ci]: https://travis-ci.org/
 | 
			
		||||
[travis-ci-profile]: https://travis-ci.org/profile
 | 
			
		||||
[zulip-rtd-dev-first-time]: ../development/setup-vagrant.html
 | 
			
		||||
[zulip-rtd-dev-overview]: ../development/overview.html
 | 
			
		||||
[zulip-rtd-tools-setup]: ../git/zulip-tools.html#set-up-git-repo-script
 | 
			
		||||
@@ -1,57 +0,0 @@
 | 
			
		||||
# Collaborate
 | 
			
		||||
 | 
			
		||||
## Fetch another contributor's branch
 | 
			
		||||
 | 
			
		||||
What happens when you would like to collaborate with another contributor and
 | 
			
		||||
they have work-in-progress on their own fork of Zulip? No problem! Just add
 | 
			
		||||
their fork as a remote and pull their changes.
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
$ git remote add <username> https://github.com/<username>/zulip.git
 | 
			
		||||
$ git fetch <username>
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Now you can checkout their branch just like you would any other. You can name
 | 
			
		||||
the branch anything you want, but using both the username and branch name will
 | 
			
		||||
help you keep things organized.
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
$ git checkout -b <username>/<branchname>
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
You can choose to rename the branch if you prefer:
 | 
			
		||||
```
 | 
			
		||||
git checkout -b <custombranchname> <username>/<branchname>
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Checkout a pull request locally
 | 
			
		||||
 | 
			
		||||
Just as you can checkout any user's branch locally, you can also checkout any
 | 
			
		||||
pull request locally. GitHub provides a special syntax
 | 
			
		||||
([details][github-help-co-pr-locally]) for this since pull requests are
 | 
			
		||||
specific to GitHub rather than Git.
 | 
			
		||||
 | 
			
		||||
First, fetch and create a branch for the pull request, replacing *ID* and
 | 
			
		||||
*BRANCHNAME* with the ID of the pull request and your desired branch name:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
$ git fetch upstream pull/ID/head:BRANCHNAME
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Now switch to the branch:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
$ git checkout BRANCHNAME
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Now you work on this branch as you would any other.
 | 
			
		||||
 | 
			
		||||
Note: you can use the scripts provided in the tools/ directory to fetch pull
 | 
			
		||||
requests. You can read more about what they do [here][tools-PR].
 | 
			
		||||
```
 | 
			
		||||
tools/fetch-rebase-pull-request <PR-number>
 | 
			
		||||
tools/fetch-pull-request <PR-number>
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
[github-help-co-pr-locally]: https://help.github.com/articles/checking-out-pull-requests-locally/
 | 
			
		||||
[tools-PR]: ../git/zulip-tools.html#fetch-a-pull-request-and-rebase
 | 
			
		||||
@@ -1,21 +0,0 @@
 | 
			
		||||
#########
 | 
			
		||||
Git Guide
 | 
			
		||||
#########
 | 
			
		||||
 | 
			
		||||
.. toctree::
 | 
			
		||||
   :maxdepth: 3
 | 
			
		||||
 | 
			
		||||
   Quick Start <overview>
 | 
			
		||||
   Set up Git <setup>
 | 
			
		||||
   How Git is different <the-git-difference>
 | 
			
		||||
   Important Git terms <terminology>
 | 
			
		||||
   Get Zulip code <cloning>
 | 
			
		||||
   Working copies <working-copies>
 | 
			
		||||
   Using Git as you work <using>
 | 
			
		||||
   Pull Requests <pull-requests>
 | 
			
		||||
   Collaborate <collaborate>
 | 
			
		||||
   Fixing commits <fixing-commits>
 | 
			
		||||
   Reviewing changes <reviewing>
 | 
			
		||||
   Get and stay out of trouble <troubleshooting>
 | 
			
		||||
   Zulip-specific-tools <zulip-tools>
 | 
			
		||||
   Git Cheat Sheet <cheat-sheet>
 | 
			
		||||
@@ -1,71 +0,0 @@
 | 
			
		||||
# Quick start: How Zulip uses Git and GitHub
 | 
			
		||||
 | 
			
		||||
This quick start provides a brief overview of how Zulip uses Git and GitHub.
 | 
			
		||||
 | 
			
		||||
Those who are familiar with Git and GitHub should be able to start contributing
 | 
			
		||||
with these details in mind:
 | 
			
		||||
 | 
			
		||||
- We use **GitHub for source control and code review.** To contribute, fork
 | 
			
		||||
  [zulip/zulip][github-zulip-zulip] (or the appropriate
 | 
			
		||||
  [repository][github-zulip], if you are working on something else besides
 | 
			
		||||
  Zulip server) to your own account and then create feature/issue branches.
 | 
			
		||||
  When you're ready to get feedback, submit a work-in-progress (WIP) pull
 | 
			
		||||
  request. *We encourage you to submit WIP pull requests early and often.*
 | 
			
		||||
 | 
			
		||||
- We use a **[rebase][gitbook-rebase]-oriented workflow.** We do not use merge
 | 
			
		||||
  commits. This means you should use `git fetch` followed by `git rebase`
 | 
			
		||||
  rather than `git pull` (or you can use `git pull --rebase`). Also, to prevent
 | 
			
		||||
  pull requests from becoming out of date with the main line of development,
 | 
			
		||||
  you should rebase your feature branch prior to submitting a pull request, and
 | 
			
		||||
  as needed thereafter. If you're unfamiliar with how to rebase a pull request,
 | 
			
		||||
  [read this excellent guide][github-rebase-pr].
 | 
			
		||||
 | 
			
		||||
  We use this strategy in order to avoid the extra commits that appear
 | 
			
		||||
  when another branch is merged, that clutter the commit history (it's
 | 
			
		||||
  popular with other large projects such as Django).  This makes
 | 
			
		||||
  Zulip's commit history more readable, but a side effect is that many
 | 
			
		||||
  pull requests we merge will be reported by GitHub's UI as *closed*
 | 
			
		||||
  instead of *merged*, since GitHub has poor support for
 | 
			
		||||
  rebase-oriented workflows.
 | 
			
		||||
 | 
			
		||||
- We have a **[code style guide][zulip-rtd-code-style]**, a **[commit message
 | 
			
		||||
  guide][zulip-rtd-commit-messages]**, and strive for each commit to be *a
 | 
			
		||||
  minimal coherent idea* (see **[commit
 | 
			
		||||
  discipline][zulip-rtd-commit-discipline]** for details).
 | 
			
		||||
 | 
			
		||||
- We provide **many tools to help you submit quality code.** These include
 | 
			
		||||
  [linters][zulip-rtd-lint-tools], [tests][zulip-rtd-testing], continuous
 | 
			
		||||
  integration with [Travis CI][travis-ci], and [mypy][zulip-rtd-mypy].
 | 
			
		||||
 | 
			
		||||
- We use [zulipbot][zulip-rtd-zulipbot-usage] to manage our issues and
 | 
			
		||||
  pull requests to create a better GitHub workflow for contributors.
 | 
			
		||||
 | 
			
		||||
- We provide some handy **[Zulip-specific Git scripts][zulip-rtd-zulip-tools]**
 | 
			
		||||
  for developers to easily do tasks like fetching and rebasing a pull
 | 
			
		||||
  request, cleaning unimportant branches, etc. These reduce the common
 | 
			
		||||
  tasks of testing other contributors' pull requests to single commands.
 | 
			
		||||
 | 
			
		||||
Finally, install the [Zulip developer environment][zulip-rtd-dev-overview], and then
 | 
			
		||||
[configure your fork for use with Travis CI][zulip-git-guide-travisci].
 | 
			
		||||
 | 
			
		||||
***
 | 
			
		||||
 | 
			
		||||
The following sections will help you be awesome with Zulip and Git/GitHub in a
 | 
			
		||||
rebased-based workflow. Read through it if you're new to git, to a rebase-based
 | 
			
		||||
git workflow, or if you'd like a git refresher.
 | 
			
		||||
 | 
			
		||||
[gitbook-rebase]: https://git-scm.com/book/en/v2/Git-Branching-Rebasing
 | 
			
		||||
[github-rebase-pr]: https://github.com/edx/edx-platform/wiki/How-to-Rebase-a-Pull-Request
 | 
			
		||||
[github-zulip]: https://github.com/zulip/
 | 
			
		||||
[github-zulip-zulip]: https://github.com/zulip/zulip/
 | 
			
		||||
[travis-ci]: https://travis-ci.org/
 | 
			
		||||
[zulip-git-guide-travisci]: ../git/cloning.html#step-3-configure-travis-ci-continuous-integration
 | 
			
		||||
[zulip-rtd-code-style]: ../contributing/code-style.html
 | 
			
		||||
[zulip-rtd-commit-discipline]: ../contributing/version-control.html#commit-discipline
 | 
			
		||||
[zulip-rtd-commit-messages]: ../contributing/version-control.html#commit-messages
 | 
			
		||||
[zulip-rtd-dev-overview]: ../development/overview.html
 | 
			
		||||
[zulip-rtd-lint-tools]: ../contributing/code-style.html#lint-tools
 | 
			
		||||
[zulip-rtd-mypy]: ../contributing/mypy.html
 | 
			
		||||
[zulip-rtd-testing]: ../testing/testing.html
 | 
			
		||||
[zulip-rtd-zulip-tools]: ../git/zulip-tools.html
 | 
			
		||||
[zulip-rtd-zulipbot-usage]: ../contributing/zulipbot-usage.html
 | 
			
		||||
@@ -1,152 +0,0 @@
 | 
			
		||||
# Create a pull request
 | 
			
		||||
 | 
			
		||||
When you're ready for feedback, submit a pull request. Pull requests
 | 
			
		||||
are a feature specific to GitHub. They provide a simple, web-based way
 | 
			
		||||
to submit your work (often called "patches") to a project. It's called
 | 
			
		||||
a *pull request* because you're asking the project to *pull changes*
 | 
			
		||||
from your fork.
 | 
			
		||||
 | 
			
		||||
If you're unfamiliar with how to create a pull request, you can check
 | 
			
		||||
out GitHub's documentation on
 | 
			
		||||
[creating a pull request from a fork][github-help-create-pr-fork]. You
 | 
			
		||||
might also find GitHub's article
 | 
			
		||||
[about pull requests][github-help-about-pr] helpful. That all said,
 | 
			
		||||
the tutorial below will walk you through the process.
 | 
			
		||||
 | 
			
		||||
## Work in progress pull requests
 | 
			
		||||
 | 
			
		||||
In the Zulip project, we encourage submitting work-in-progress pull
 | 
			
		||||
requests early and often. This allows you to share your code to make
 | 
			
		||||
it easier to get feedback and help with your changes. Prefix the
 | 
			
		||||
titles of work-in-progress pull requests with **[WIP]**, which in our
 | 
			
		||||
project means that you don't think your pull request is ready to be
 | 
			
		||||
merged (e.g. it might not work or pass tests).  This sets expectations
 | 
			
		||||
correctly for any feedback from other developers, and prevents your
 | 
			
		||||
work from being merged before you're confident in it.
 | 
			
		||||
 | 
			
		||||
## Create a pull request
 | 
			
		||||
 | 
			
		||||
### Step 1: Update your branch with git rebase
 | 
			
		||||
 | 
			
		||||
The best way to update your branch is with `git fetch` and `git rebase`. Do not
 | 
			
		||||
use `git pull` or `git merge` as this will create merge commits. See [keep your
 | 
			
		||||
fork up to date][keep-up-to-date] for details.
 | 
			
		||||
 | 
			
		||||
Here's an example (you would replace *issue-123* with the name of your feature branch):
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
$ git checkout issue-123
 | 
			
		||||
Switched to branch 'issue-123'
 | 
			
		||||
 | 
			
		||||
$ git fetch upstream
 | 
			
		||||
remote: Counting objects: 69, done.
 | 
			
		||||
remote: Compressing objects: 100% (23/23), done.
 | 
			
		||||
remote: Total 69 (delta 49), reused 39 (delta 39), pack-reused 7
 | 
			
		||||
Unpacking objects: 100% (69/69), done.
 | 
			
		||||
From https://github.com/zulip/zulip
 | 
			
		||||
   69fa600..43e21f6  master     -> upstream/master
 | 
			
		||||
 | 
			
		||||
$ git rebase upstream/master
 | 
			
		||||
 | 
			
		||||
First, rewinding head to replay your work on top of it...
 | 
			
		||||
Applying: troubleshooting tip about provisioning
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Step 2: Push your updated branch to your remote fork
 | 
			
		||||
 | 
			
		||||
Once you've updated your local feature branch, push the changes to GitHub:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
$ git push origin issue-123
 | 
			
		||||
Counting objects: 6, done.
 | 
			
		||||
Delta compression using up to 4 threads.
 | 
			
		||||
Compressing objects: 100% (4/4), done.
 | 
			
		||||
Writing objects: 100% (6/6), 658 bytes | 0 bytes/s, done.
 | 
			
		||||
Total 6 (delta 3), reused 0 (delta 0)
 | 
			
		||||
remote: Resolving deltas: 100% (3/3), completed with 1 local objects.
 | 
			
		||||
To git@github.com:christi3k/zulip.git
 | 
			
		||||
 + 2d49e2d...bfb2433 issue-123 -> issue-123
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
If your push is rejected with error **failed to push some refs** then you need
 | 
			
		||||
to prefix the name of your branch with a `+`:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
$ git push origin +issue-123
 | 
			
		||||
Counting objects: 6, done.
 | 
			
		||||
Delta compression using up to 4 threads.
 | 
			
		||||
Compressing objects: 100% (4/4), done.
 | 
			
		||||
Writing objects: 100% (6/6), 658 bytes | 0 bytes/s, done.
 | 
			
		||||
Total 6 (delta 3), reused 0 (delta 0)
 | 
			
		||||
remote: Resolving deltas: 100% (3/3), completed with 1 local objects.
 | 
			
		||||
To git@github.com:christi3k/zulip.git
 | 
			
		||||
 + 2d49e2d...bfb2433 issue-123 -> issue-123 (forced update)
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
This is perfectly okay to do on your own feature branches, especially if you're
 | 
			
		||||
the only one making changes to the branch. If others are working along with
 | 
			
		||||
you, they might run into complications when they retrieve your changes because
 | 
			
		||||
anyone who has based their changes off a branch you rebase will have to do a
 | 
			
		||||
complicated rebase.
 | 
			
		||||
 | 
			
		||||
### Step 3: Open the pull request
 | 
			
		||||
 | 
			
		||||
If you've never created a pull request or need a refresher, take a look at
 | 
			
		||||
GitHub's article [creating a pull request from a
 | 
			
		||||
fork][github-help-create-pr-fork]. We'll briefly review the process here.
 | 
			
		||||
 | 
			
		||||
The first step in creating a pull request is to use your web browser to
 | 
			
		||||
navigate to your fork of Zulip. Sign in to GitHub if you haven't already.
 | 
			
		||||
 | 
			
		||||
Next, navigate to the branch you've been working on. Do this by clicking on the
 | 
			
		||||
**Branch** button and selecting the relevant branch. Finally, click the **New
 | 
			
		||||
pull request** button.
 | 
			
		||||
 | 
			
		||||
Alternatively, if you've recently pushed to your fork, you will see a green
 | 
			
		||||
**Compare & pull request** button.
 | 
			
		||||
 | 
			
		||||
You'll see the *Open a pull request* page:
 | 
			
		||||
 | 
			
		||||
![images-create-pr]
 | 
			
		||||
 | 
			
		||||
Provide a **title** and first comment for your pull request. Remember to prefix
 | 
			
		||||
your pull request title with [WIP] if it is a [work-in-progress][wip-prs].
 | 
			
		||||
 | 
			
		||||
If your pull request has an effect on the visuals of a component, you might want
 | 
			
		||||
to include a screenshot of this change or a GIF of the interaction in your first
 | 
			
		||||
comment. This will allow reviewers to comment on your changes without having to
 | 
			
		||||
checkout your branch; you can find a list of tools you can use for this over
 | 
			
		||||
[here][screenshots-gifs].
 | 
			
		||||
 | 
			
		||||
When ready, click the green **Create pull request** to submit the pull request.
 | 
			
		||||
 | 
			
		||||
Note: **Pull request titles are different from commit messages.** Commit
 | 
			
		||||
messages can be edited with `git commit --amend`, `git rebase -i`, etc., while
 | 
			
		||||
the title of a pull request can only be edited via GitHub.
 | 
			
		||||
 | 
			
		||||
## Update a pull request
 | 
			
		||||
 | 
			
		||||
As you get make progress on your feature or bugfix, your pull request, once
 | 
			
		||||
submitted, will be updated each time you [push commits][self-push-commits] to
 | 
			
		||||
your remote branch. This means you can keep your pull request open as long as
 | 
			
		||||
you need, rather than closing and opening new ones for the same feature or
 | 
			
		||||
bugfix.
 | 
			
		||||
 | 
			
		||||
It's a good idea to keep your pull request mergeable with Zulip upstream by
 | 
			
		||||
frequently fetching, rebasing, and pushing changes. See [keep your fork up to
 | 
			
		||||
date][keep-up-to-date] for details. You might also find this excellent
 | 
			
		||||
article [How to Rebase a Pull Request][edx-howto-rebase-pr] helpful.
 | 
			
		||||
 | 
			
		||||
And, as you address review comments others have made, we recommend posting a
 | 
			
		||||
follow-up comment in which you: a) ask for any clarifications you need, b)
 | 
			
		||||
explain to the reviewer how you solved any problems they mentioned, and c) ask
 | 
			
		||||
for another review.
 | 
			
		||||
 | 
			
		||||
[edx-howto-rebase-pr]: https://github.com/edx/edx-platform/wiki/How-to-Rebase-a-Pull-Request
 | 
			
		||||
[github-help-about-pr]: https://help.github.com/articles/about-pull-requests/
 | 
			
		||||
[github-help-create-pr-fork]: https://help.github.com/articles/creating-a-pull-request-from-a-fork/
 | 
			
		||||
[images-create-pr]: ../images/zulip-open-pr.png
 | 
			
		||||
[keep-up-to-date]: ../git/using.html#keep-your-fork-up-to-date
 | 
			
		||||
[push-commits]: ../git/using.html#push-your-commits-to-github
 | 
			
		||||
[screenshots-gifs]: ../tutorials/screenshot-and-gif-software.html
 | 
			
		||||
[wip-prs]: #work-in-progress-pull-requests
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user