diff --git a/.envrc b/.envrc
new file mode 100644
index 0000000..0a722d7
--- /dev/null
+++ b/.envrc
@@ -0,0 +1 @@
+use nix;
diff --git a/.gcloudignore b/.gcloudignore
new file mode 100644
index 0000000..f0f21f0
--- /dev/null
+++ b/.gcloudignore
@@ -0,0 +1,6 @@
+.git
+.DS_Store
+*.pyc
+*.nix
+.envrc
+#!include:.gitignore
diff --git a/.gitignore b/.gitignore
index 0d20b64..0b27afb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,3 @@
*.pyc
+.direnv
+__pycache__
diff --git a/Makefile b/Makefile
index fc5af25..1fcbacf 100644
--- a/Makefile
+++ b/Makefile
@@ -1,13 +1,19 @@
.PHONY: serve test_deps test check appcfg-update deploy
serve:
- dev_appserver.py --log_level=debug . --host=0.0.0.0
+ honcho start
test_deps:
pip install -r requirements-dev.txt
-test check:
- python -m unittest discover -p '*_test.py'
+test:
+ pytest
+
+typecheck:
+ pyright
appcfg-update deploy:
- gcloud app deploy --project "${APP}"
+ gcloud app deploy
+
+create-indexes:
+ gcloud datastore indexes create index.yaml
diff --git a/Procfile b/Procfile
new file mode 100644
index 0000000..f3350d9
--- /dev/null
+++ b/Procfile
@@ -0,0 +1,2 @@
+datastore: gcloud beta emulators datastore start
+web: $(gcloud beta emulators datastore env-init); python main.py
diff --git a/app.yaml b/app.yaml
index c4cdfdb..218c01e 100644
--- a/app.yaml
+++ b/app.yaml
@@ -1,7 +1,15 @@
-runtime: python27
-threadsafe: yes
-api_version: 1
+runtime: python310
default_expiration: "365d"
+app_engine_apis: true
+
+# Slack has a very short timeout on API callbacks, so we need to keep at least 1
+# process running for it
+automatic_scaling:
+ min_instances: 1
+
+# min_instances requires warmup requests to work
+inbound_services:
+ - warmup
handlers:
- url: /static
@@ -13,23 +21,13 @@ handlers:
upload: static/favicon.ico
- url: /admin/.*
- script: snippets.application
+ script: auto
login: admin
+ secure: always
-- url: .*
- script: snippets.application
-
-skip_files:
-- .git
-- .DS_Store
-- .*.pyc
+- url: /.*
+ script: auto
+ secure: always
builtins:
- remote_api: on
-
-libraries:
-- name: jinja2
- version: "2.6"
-# This also brings in webapp2_extras:
-- name: webapp2
- version: "2.5.1"
diff --git a/conftest.py b/conftest.py
new file mode 100644
index 0000000..15c726f
--- /dev/null
+++ b/conftest.py
@@ -0,0 +1,35 @@
+"""conftest - loaded automatically by the pytest runner"""
+
+from unittest.mock import MagicMock
+
+from google.cloud import ndb
+from google.cloud.ndb import _datastore_api
+from InMemoryCloudDatastoreStub.datastore_stub import LocalDatastoreStub
+import pytest
+from _pytest.monkeypatch import MonkeyPatch
+
+
+@pytest.fixture(autouse=True)
+def ndb_stub(monkeypatch: MonkeyPatch) -> LocalDatastoreStub:
+ stub = LocalDatastoreStub()
+ monkeypatch.setattr(_datastore_api, "stub", MagicMock(return_value=stub))
+ return stub
+
+
+@pytest.fixture(autouse=True)
+def ndb_context(init_ndb_env_vars):
+ client = ndb.Client()
+ with client.context() as context:
+ yield context
+
+
+@pytest.fixture(autouse=True)
+def init_ndb_env_vars(monkeypatch: MonkeyPatch) -> None:
+ """Set environment variables for the test ndb client.
+
+ Initializing an ndb Client in a test env requires some environment variables
+ to be set. For now, these are just garbage values intended to give the
+ library _something_ (we don't expect them to actually work yet)
+ """
+ monkeypatch.setenv("DATASTORE_EMULATOR_HOST", "localhost")
+ monkeypatch.setenv("DATASTORE_DATASET", "datastore-stub-test")
diff --git a/cron.yaml b/cron.yaml
index f20cfb1..c526358 100644
--- a/cron.yaml
+++ b/cron.yaml
@@ -2,7 +2,7 @@ cron:
- description: snippets chat -- early reminder to write snippets
url: /admin/send_friday_reminder_chat
- schedule: every friday 16:00
+ schedule: every friday 13:00
timezone: US/Pacific
- description: snippets email -- reminder to write snippets
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..65537a1
--- /dev/null
+++ b/main.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+
+import snippets
+
+import google.appengine.api
+import google.cloud.ndb
+
+
+class NDBMiddleware:
+ """WSGI middleware to wrap the app in Google Cloud NDB context"""
+ def __init__(self, app):
+ self.app = app
+ self.client = google.cloud.ndb.Client()
+
+ def __call__(self, environ, start_response):
+ with self.client.context():
+ return self.app(environ, start_response)
+
+app = snippets.app
+app.wsgi_app = google.appengine.api.wrap_wsgi_app(app.wsgi_app)
+app.wsgi_app = NDBMiddleware(app.wsgi_app)
+
+
+if __name__ == '__main__':
+ # This is used when running locally only. When deploying to Google App
+ # Engine, a webserver process such as Gunicorn will serve the app. You
+ # can configure startup instructions by adding `entrypoint` to app.yaml.
+ #
+ # To control listening IP and port, set SERVER_NAME in the environment.
+ # e.g. SERVER_NAME=127.0.0.1:8080
+ # Default is to listen on 127.0.0.1:5000
+ app.run(debug=True)
diff --git a/models.py b/models.py
index c6f0f10..427ec33 100644
--- a/models.py
+++ b/models.py
@@ -2,7 +2,7 @@
import hashlib
import os
-from google.appengine.ext import db
+from google.cloud import ndb
from google.appengine.api import users
@@ -21,54 +21,55 @@
# support that later.
-class User(db.Model):
+class User(ndb.Model):
"""User preferences."""
- created = db.DateTimeProperty()
- last_modified = db.DateTimeProperty(auto_now=True)
- email = db.StringProperty(required=True) # The key to this record
- is_hidden = db.BooleanProperty(default=False) # hide 'empty' snippets
- category = db.StringProperty(default=NULL_CATEGORY) # groups snippets
- uses_markdown = db.BooleanProperty(default=True) # interpret snippet text
- private_snippets = db.BooleanProperty(default=False) # private by default?
- wants_email = db.BooleanProperty(default=True) # get nag emails?
+ created = ndb.DateTimeProperty(auto_now_add=True)
+ last_modified = ndb.DateTimeProperty(auto_now=True)
+ email = ndb.StringProperty(required=True) # The key to this record
+ is_hidden = ndb.BooleanProperty(default=False) # hide 'empty' snippets
+ category = ndb.StringProperty(default=NULL_CATEGORY) # groups snippets
+ uses_markdown = ndb.BooleanProperty(default=True) # interpret snippet text
+ private_snippets = ndb.BooleanProperty(default=False) # private by default?
+ wants_email = ndb.BooleanProperty(default=True) # get nag emails?
# TODO(csilvers): make a ListProperty instead.
- wants_to_view = db.TextProperty(default='all') # comma-separated list
- display_name = db.TextProperty(default='') # display name of the user
+ wants_to_view = ndb.TextProperty(default='all') # comma-separated list
+ display_name = ndb.TextProperty(default='') # Display name of the user
+ slack_id = ndb.StringProperty(default='') # Slack member ID (not nickname!)
-class Snippet(db.Model):
+class Snippet(ndb.Model):
"""Every snippet is identified by the monday of the week it goes with."""
- created = db.DateTimeProperty()
- last_modified = db.DateTimeProperty(auto_now=True)
- display_name = db.StringProperty() # display name of the user
- email = db.StringProperty(required=True) # week+email: key to this record
- week = db.DateProperty(required=True) # the monday of the week
- text = db.TextProperty()
- private = db.BooleanProperty(default=False) # snippet is private?
- is_markdown = db.BooleanProperty(default=False) # text is markdown?
+ created = ndb.DateTimeProperty(auto_now_add=True)
+ last_modified = ndb.DateTimeProperty(auto_now=True)
+ display_name = ndb.StringProperty() # display name of the user
+ email = ndb.StringProperty(required=True) # week+email: key to this record
+ week = ndb.DateProperty(required=True) # the monday of the week
+ text = ndb.TextProperty()
+ private = ndb.BooleanProperty(default=False) # snippet is private?
+ is_markdown = ndb.BooleanProperty(default=False) # text is markdown?
@property
def email_md5_hash(self):
m = hashlib.md5()
- m.update(self.email)
+ m.update(self.email.encode('utf-8'))
return m.hexdigest()
-class AppSettings(db.Model):
+class AppSettings(ndb.Model):
"""Application-wide preferences."""
- created = db.DateTimeProperty()
- last_modified = db.DateTimeProperty(auto_now=True)
+ created = ndb.DateTimeProperty(auto_now_add=True)
+ last_modified = ndb.DateTimeProperty(auto_now=True)
# Application settings
- domains = db.StringListProperty(required=True)
- hostname = db.StringProperty(required=True) # used for emails
- default_private = db.BooleanProperty(default=False) # new-user default
- default_markdown = db.BooleanProperty(default=True) # new-user default
- default_email = db.BooleanProperty(default=True) # new-user default
+ domains = ndb.StringProperty(repeated=True)
+ hostname = ndb.StringProperty(required=True) # used for emails
+ default_private = ndb.BooleanProperty(default=False) # new-user default
+ default_markdown = ndb.BooleanProperty(default=True) # new-user default
+ default_email = ndb.BooleanProperty(default=True) # new-user default
# Chat and email settings
- email_from = db.StringProperty(default='')
- slack_channel = db.StringProperty(default='')
- slack_token = db.StringProperty(default='')
- slack_slash_token = db.StringProperty(default='')
+ email_from = ndb.StringProperty(default='')
+ slack_channel = ndb.StringProperty(default='')
+ slack_token = ndb.StringProperty(default='')
+ slack_slash_token = ndb.StringProperty(default='')
@staticmethod
def get(create_if_missing=False, domains=None):
@@ -79,12 +80,12 @@ def get(create_if_missing=False, domains=None):
are initialized with the given value for 'domains'. The new
entity is *not* put to the datastore.
"""
- retval = AppSettings.get_by_key_name('global_settings')
+ retval = AppSettings.get_by_id('global_settings')
if retval:
return retval
elif create_if_missing:
# We default to sending email, and having it look like it's
- # comint from the current user. We add a '+snippets' in there
+ # coming from the current user. We add a '+snippets' in there
# to allow for filtering
email_address = users.get_current_user().email()
email_address = email_address.replace('@', '+snippets@')
@@ -93,7 +94,7 @@ def get(create_if_missing=False, domains=None):
# you accessed the site on here.
hostname = '%s://%s' % (os.environ.get('wsgi.url_scheme', 'http'),
os.environ['HTTP_HOST'])
- return AppSettings(key_name='global_settings',
+ return AppSettings(id='global_settings',
created=datetime.datetime.now(),
domains=domains,
hostname=hostname,
diff --git a/nix/sources.json b/nix/sources.json
new file mode 100644
index 0000000..70e5847
--- /dev/null
+++ b/nix/sources.json
@@ -0,0 +1,14 @@
+{
+ "nixpkgs": {
+ "branch": "nixos-22.11",
+ "description": "Nix Packages collection",
+ "homepage": null,
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "eef86b8a942913a828b9ef13722835f359deef29",
+ "sha256": "1ig0mc7f2n1pxzd5y9m4vz38zp515vn5s576kwhkhj62zdw99p2v",
+ "type": "tarball",
+ "url": "https://github.com/NixOS/nixpkgs/archive/eef86b8a942913a828b9ef13722835f359deef29.tar.gz",
+ "url_template": "https://github.com///archive/.tar.gz"
+ }
+}
diff --git a/nix/sources.nix b/nix/sources.nix
new file mode 100644
index 0000000..fe3dadf
--- /dev/null
+++ b/nix/sources.nix
@@ -0,0 +1,198 @@
+# This file has been generated by Niv.
+
+let
+
+ #
+ # The fetchers. fetch_ fetches specs of type .
+ #
+
+ fetch_file = pkgs: name: spec:
+ let
+ name' = sanitizeName name + "-src";
+ in
+ if spec.builtin or true then
+ builtins_fetchurl { inherit (spec) url sha256; name = name'; }
+ else
+ pkgs.fetchurl { inherit (spec) url sha256; name = name'; };
+
+ fetch_tarball = pkgs: name: spec:
+ let
+ name' = sanitizeName name + "-src";
+ in
+ if spec.builtin or true then
+ builtins_fetchTarball { name = name'; inherit (spec) url sha256; }
+ else
+ pkgs.fetchzip { name = name'; inherit (spec) url sha256; };
+
+ fetch_git = name: spec:
+ let
+ ref =
+ spec.ref or (
+ if spec ? branch then "refs/heads/${spec.branch}" else
+ if spec ? tag then "refs/tags/${spec.tag}" else
+ abort "In git source '${name}': Please specify `ref`, `tag` or `branch`!"
+ );
+ submodules = spec.submodules or false;
+ submoduleArg =
+ let
+ nixSupportsSubmodules = builtins.compareVersions builtins.nixVersion "2.4" >= 0;
+ emptyArgWithWarning =
+ if submodules
+ then
+ builtins.trace
+ (
+ "The niv input \"${name}\" uses submodules "
+ + "but your nix's (${builtins.nixVersion}) builtins.fetchGit "
+ + "does not support them"
+ )
+ { }
+ else { };
+ in
+ if nixSupportsSubmodules
+ then { inherit submodules; }
+ else emptyArgWithWarning;
+ in
+ builtins.fetchGit
+ ({ url = spec.repo; inherit (spec) rev; inherit ref; } // submoduleArg);
+
+ fetch_local = spec: spec.path;
+
+ fetch_builtin-tarball = name: throw
+ ''[${name}] The niv type "builtin-tarball" is deprecated. You should instead use `builtin = true`.
+ $ niv modify ${name} -a type=tarball -a builtin=true'';
+
+ fetch_builtin-url = name: throw
+ ''[${name}] The niv type "builtin-url" will soon be deprecated. You should instead use `builtin = true`.
+ $ niv modify ${name} -a type=file -a builtin=true'';
+
+ #
+ # Various helpers
+ #
+
+ # https://github.com/NixOS/nixpkgs/pull/83241/files#diff-c6f540a4f3bfa4b0e8b6bafd4cd54e8bR695
+ sanitizeName = name:
+ (
+ concatMapStrings (s: if builtins.isList s then "-" else s)
+ (
+ builtins.split "[^[:alnum:]+._?=-]+"
+ ((x: builtins.elemAt (builtins.match "\\.*(.*)" x) 0) name)
+ )
+ );
+
+ # The set of packages used when specs are fetched using non-builtins.
+ mkPkgs = sources: system:
+ let
+ sourcesNixpkgs =
+ import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) { inherit system; };
+ hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath;
+ hasThisAsNixpkgsPath = == ./.;
+ in
+ if builtins.hasAttr "nixpkgs" sources
+ then sourcesNixpkgs
+ else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then
+ import { }
+ else
+ abort
+ ''
+ Please specify either (through -I or NIX_PATH=nixpkgs=...) or
+ add a package called "nixpkgs" to your sources.json.
+ '';
+
+ # The actual fetching function.
+ fetch = pkgs: name: spec:
+
+ if ! builtins.hasAttr "type" spec then
+ abort "ERROR: niv spec ${name} does not have a 'type' attribute"
+ else if spec.type == "file" then fetch_file pkgs name spec
+ else if spec.type == "tarball" then fetch_tarball pkgs name spec
+ else if spec.type == "git" then fetch_git name spec
+ else if spec.type == "local" then fetch_local spec
+ else if spec.type == "builtin-tarball" then fetch_builtin-tarball name
+ else if spec.type == "builtin-url" then fetch_builtin-url name
+ else
+ abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}";
+
+ # If the environment variable NIV_OVERRIDE_${name} is set, then use
+ # the path directly as opposed to the fetched source.
+ replace = name: drv:
+ let
+ saneName = stringAsChars (c: if (builtins.match "[a-zA-Z0-9]" c) == null then "_" else c) name;
+ ersatz = builtins.getEnv "NIV_OVERRIDE_${saneName}";
+ in
+ if ersatz == "" then drv else
+ # this turns the string into an actual Nix path (for both absolute and
+ # relative paths)
+ if builtins.substring 0 1 ersatz == "/" then /. + ersatz else /. + builtins.getEnv "PWD" + "/${ersatz}";
+
+ # Ports of functions for older nix versions
+
+ # a Nix version of mapAttrs if the built-in doesn't exist
+ mapAttrs = builtins.mapAttrs or (
+ f: set: with builtins;
+ listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set))
+ );
+
+ # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/lists.nix#L295
+ range = first: last: if first > last then [ ] else builtins.genList (n: first + n) (last - first + 1);
+
+ # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L257
+ stringToCharacters = s: map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1));
+
+ # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L269
+ stringAsChars = f: s: concatStrings (map f (stringToCharacters s));
+ concatMapStrings = f: list: concatStrings (map f list);
+ concatStrings = builtins.concatStringsSep "";
+
+ # https://github.com/NixOS/nixpkgs/blob/8a9f58a375c401b96da862d969f66429def1d118/lib/attrsets.nix#L331
+ optionalAttrs = cond: as: if cond then as else { };
+
+ # fetchTarball version that is compatible between all the versions of Nix
+ builtins_fetchTarball = { url, name ? null, sha256 }@attrs:
+ let
+ inherit (builtins) lessThan nixVersion fetchTarball;
+ in
+ if lessThan nixVersion "1.12" then
+ fetchTarball ({ inherit url; } // (optionalAttrs (name != null) { inherit name; }))
+ else
+ fetchTarball attrs;
+
+ # fetchurl version that is compatible between all the versions of Nix
+ builtins_fetchurl = { url, name ? null, sha256 }@attrs:
+ let
+ inherit (builtins) lessThan nixVersion fetchurl;
+ in
+ if lessThan nixVersion "1.12" then
+ fetchurl ({ inherit url; } // (optionalAttrs (name != null) { inherit name; }))
+ else
+ fetchurl attrs;
+
+ # Create the final "sources" from the config
+ mkSources = config:
+ mapAttrs
+ (
+ name: spec:
+ if builtins.hasAttr "outPath" spec
+ then
+ abort
+ "The values in sources.json should not have an 'outPath' attribute"
+ else
+ spec // { outPath = replace name (fetch config.pkgs name spec); }
+ )
+ config.sources;
+
+ # The "config" used by the fetchers
+ mkConfig =
+ { sourcesFile ? if builtins.pathExists ./sources.json then ./sources.json else null
+ , sources ? if sourcesFile == null then { } else builtins.fromJSON (builtins.readFile sourcesFile)
+ , system ? builtins.currentSystem
+ , pkgs ? mkPkgs sources system
+ }: rec {
+ # The sources, i.e. the attribute set of spec name to spec
+ inherit sources;
+
+ # The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers
+ inherit pkgs;
+ };
+
+in
+mkSources (mkConfig { }) // { __functor = _: settings: mkSources (mkConfig settings); }
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 7829dbe..abf84ca 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1 +1,4 @@
webtest>=2.0.16
+pytest
+InMemoryCloudDatastoreStub
+time-machine>=2.8
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..a7f2e5b
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,5 @@
+appengine-python-standard>=1.0.0
+Flask>=2.2.2
+Jinja2>=3.1.2
+google-cloud-ndb>=1.11.1
+google-cloud-logging>=3.2.5
diff --git a/shell.nix b/shell.nix
new file mode 100644
index 0000000..7a670c1
--- /dev/null
+++ b/shell.nix
@@ -0,0 +1,105 @@
+{ sources ? import ./nix/sources.nix }:
+
+let
+ pkgs = import sources.nixpkgs {};
+
+ python = pkgs.python310.override {
+ packageOverrides = self: super: {
+
+ google-cloud-datastore-stub = super.buildPythonPackage rec {
+ pname = "InMemoryCloudDatastoreStub";
+ version = "0.0.15";
+ src = super.fetchPypi {
+ inherit pname version;
+ sha256 = "sha256-nuJNhnlXSbAg6CdWW8u7rGDDd07FvLNRtsTfzUBh7bE=";
+ };
+ propagatedBuildInputs = [
+ self.google-cloud-ndb
+ ];
+ buildInputs = [
+ super.pytest
+ ];
+ };
+
+ google-cloud-ndb = super.buildPythonPackage rec {
+ pname = "google-cloud-ndb";
+ version = "1.11.1";
+
+ src = super.fetchPypi {
+ inherit pname version;
+ sha256 = "sha256-ooEt7uTNEumYeN5EbmpVSqNKj/60OCVM4HHiUT7FaYY=";
+ };
+ propagatedBuildInputs = [
+ super.setuptools
+ super.google-cloud-core
+ self.google-cloud-datastore
+ super.pymemcache
+ super.redis
+ super.pytz
+ self.protobuf
+ ];
+ doCheck = false;
+ };
+
+ google-cloud-datastore = super.buildPythonPackage rec {
+ pname = "google-cloud-datastore";
+ version = "1.15.5";
+ src = super.fetchPypi {
+ inherit pname version;
+ sha256 = "sha256-2tyhYxCIt6c38xHfh1dKQX8lTOcdapgIDzbzW9yWsw8=";
+ };
+ propagatedBuildInputs = [
+ self.protobuf
+ super.google-cloud-core
+ ];
+ doCheck = false;
+ };
+
+ protobuf = super.protobuf.override { protobuf = pkgs.protobuf3_20; };
+
+ appengine-python-standard = super.buildPythonPackage rec {
+ pname = "appengine-python-standard";
+ version = "1.0.0";
+ src = super.fetchPypi {
+ inherit pname version;
+ sha256 = "sha256-h/HLQC6Ez4oaFkW2Kkkwb0e+QSmUQVC0wrJD4igkkrk=";
+ };
+ propagatedBuildInputs = [
+ super.requests
+ self.protobuf
+ super.attrs
+ super.google-auth
+ super.pillow
+ super.pytz
+ super.frozendict
+ super.ruamel-yaml
+ super.mock
+ ];
+ };
+ };
+
+ };
+
+ pythonEnv = python.withPackages (ps: [
+ ps.google-cloud-logging
+ ps.google-cloud-ndb
+ ps.appengine-python-standard
+ ps.flask
+ ps.pytest
+ ps.google-cloud-datastore-stub
+ ps.time-machine
+ ]);
+
+
+in pkgs.mkShell {
+ buildInputs = with pkgs; [
+ nodePackages.pyright # Type checker
+ honcho # Procfile runner
+ pythonEnv
+ (google-cloud-sdk.withExtraComponents ([
+ google-cloud-sdk.components.app-engine-python
+ google-cloud-sdk.components.app-engine-python-extras
+ ]))
+ jre # gcloud datastore emulator needs jre
+ ];
+}
diff --git a/slacklib.py b/slacklib.py
index 10a4b4d..eaaf6a0 100644
--- a/slacklib.py
+++ b/slacklib.py
@@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
"""Snippets server -> Slack integration.
This provides Slack integration with the snippet server, for
@@ -21,14 +19,12 @@
import json
import logging
import re
-import os
import textwrap
-import urllib
-import urllib2
-import webapp2
+import urllib.error
+import urllib.parse
+import urllib.request
-from google.appengine.ext import db
-from google.appengine.api import memcache
+import flask
import models
import util
@@ -40,11 +36,7 @@
_REQUIRE_SLASH_TOKEN = True
-# This allows mocking in a different day, for testing.
-_TODAY_FN = datetime.datetime.now
-
-# The web URL we point people to as the base for web operations
-_WEB_URL = 'http://' + os.environ.get('SERVER_NAME', 'localhost')
+app_blueprint = flask.Blueprint("slack", __name__, url_prefix="/slack")
def _web_api(api_method, payload):
@@ -59,7 +51,8 @@ def _web_api(api_method, payload):
app_settings = models.AppSettings.get()
payload.setdefault('token', app_settings.slack_token)
uri = 'https://slack.com/api/' + api_method
- r = urllib2.urlopen(uri, urllib.urlencode(payload))
+ r = urllib.request.urlopen(uri,
+ urllib.parse.urlencode(payload).encode('utf-8'))
# check return code for server errors
if r.getcode() != 200:
@@ -72,40 +65,6 @@ def _web_api(api_method, payload):
return reply
-def _get_user_email(uid):
- """Retrieve the email address for a specific userid from the Slack Web API.
-
- Raises ValueError if could not be retrieved.
- """
- reply = _web_api('users.info', {'user': uid}) # possible ValueError
- email = reply.get('user', {}).get('profile', {}).get('email', None)
- if email is None:
- raise ValueError('Slack user profile did not have email')
- return email
-
-
-def _get_user_email_cached(uid, force_refresh=False):
- """Retrieve the email address for a specific user id, with a cache.
-
- Results are stored in memcache for up to a day.
-
- If force_refresh parameter is specified, cached data will be refreshed.
-
- Raises ValueError if could not be retrieved.
- """
- key = 'slack_profile_email_' + uid
- cached_data = memcache.get(key)
- if (cached_data is None) or force_refresh:
- logging.debug("cache miss/refresh for slack email lookup %s", uid)
- email = _get_user_email(uid) # possible ValueError
- if not memcache.set(key=key, value=email, time=86400):
- logging.error('memcache set failed!')
- return email
- else:
- logging.debug("cache hit for slack email lookup %s", uid)
- return cached_data
-
-
def send_to_slack_channel(channel, msg):
"""Send a plaintext message to a Slack channel."""
try:
@@ -116,7 +75,7 @@ def send_to_slack_channel(channel, msg):
'icon_emoji': ':pencil:',
'unfurl_links': False, # no link previews, please
})
- except ValueError, why:
+ except ValueError as why:
logging.error('Failed sending message to slack: %s', why)
@@ -138,20 +97,25 @@ def command_usage():
def command_help():
"""Return the help string for slash commands."""
+ app_settings = models.AppSettings.get()
return (
"I can help you manage your "
- "<{}|weekly snippets>! :pencil:".format(_WEB_URL) +
+ "<{}|weekly snippets>! :pencil:".format(app_settings.hostname) +
command_usage()
)
-def _no_user_error(user_email):
- return (
- "You don't appear to have a snippets account yet!\n"
- "To create one, go to {}\n"
- "We looked for your Slack email address: {}"
- .format(_WEB_URL, user_email)
- )
+def _no_user_error(user_id):
+ try:
+ app_settings = models.AppSettings.get()
+ return (f"You don't seem to be logged in! Please configure your slack "
+ f"user ID (which is `{user_id}`) in the snippets server's "
+ f"settings page: {app_settings.hostname}/settings")
+ except ValueError:
+ return (
+ "You don't seem to be logged in, and this snippets server is "
+ "unconfigured! Please ask your snippets admin to set it up "
+ "before trying again.")
def _user_snippet(user_email, weeks_back=0):
@@ -175,7 +139,8 @@ def _user_snippet(user_email, weeks_back=0):
)
filled_snips = util.fill_in_missing_snippets(user_snips, account,
- user_email, _TODAY_FN())
+ user_email,
+ datetime.datetime.now())
logging.debug(
'User %s snippets *filled* to: %s', user_email, len(filled_snips)
)
@@ -217,7 +182,7 @@ def _format_snippet_items(items):
return "\n".join(fi)
-def command_list(user_email):
+def command_list(user_email: str) -> str:
"""Return the users current snippets for the week in pretty format."""
try:
items = _snippet_items(_user_snippet(user_email))
@@ -305,12 +270,11 @@ def command_add(user_email, new_item):
snippet.is_markdown = True
# TODO(mroth): we should abstract out DB writes to a library wrapper
- db.put(snippet)
- db.get(snippet.key()) # ensure db consistency for HRD
+ snippet.put()
return "Added *{}* to your weekly snippets.".format(new_item)
-def command_del(user_email, args):
+def command_del(user_email, args) -> str:
"""Delete an item at an index from the users current snippets.
The `args` parameter should be the args passed to the command. We
@@ -354,8 +318,7 @@ def command_del(user_email, args):
snippet.text = _markdown_list(items)
snippet.is_markdown = True
- db.put(snippet)
- db.get(snippet.key()) # ensure db consistency for HRD
+ snippet.put()
return "Removed *{}* from your weekly snippets.".format(removed_item)
@@ -368,95 +331,85 @@ def command_dump(user_email):
return "```{}```".format(snippet.text or 'No snippet yet for this week')
-class SlashCommand(webapp2.RequestHandler):
- def post(self):
- """Process an incoming slash command from Slack.
-
- Incoming request POST looks like the following (example taken from
- https://api.slack.com/slash-commands):
- token=gIkuvaNzQIHg97ATvDxqgjtO
- team_id=T0001
- team_domain=example
- channel_id=C2147483705
- channel_name=test
- user_id=U2147483697
- user_name=Steve
- command=/weather
- text=94070
- """
- req, res = self.request, self.response
-
- expected_token = models.AppSettings.get().slack_slash_token
-
- if not expected_token:
- res.write('Slack slash commands disabled. An admin '
- 'can enable them at /admin/settings')
- return
-
- # verify slash API post token for security
- if _REQUIRE_SLASH_TOKEN:
- token = req.get('token')
- if token != expected_token:
- logging.error("POST MADE WITH INVALID TOKEN")
- res.write("OH NO YOU DIDNT! Security issue plz contact admin.")
- return
-
- user_name = req.get('user_name')
- user_id = req.get('user_id')
- text = req.get('text')
-
- try:
- user_email = _get_user_email_cached(user_id)
- except ValueError:
- logging.error("Failed getting %s email from Slack API", user_name)
- res.write(
- "Error getting your email address from the Slack API! "
- "Please contact an admin and report the time of this error."
- )
- return
+def get_user_by_slack_id(slack_id: str) -> models.User:
+ return models.User.query(models.User.slack_id == slack_id).get()
+
- words = text.strip().split()
- if not words:
- logging.info('null (list) command from user %s', user_name)
- res.write(command_list(user_email))
+# This route is actually /slack because app_blueprint sets url_prefix
+@app_blueprint.route("/", methods=["POST"])
+def slash_command_handler() -> str:
+ """Process an incoming slash command from Slack.
+
+ Incoming request POST looks like the following (example taken from
+ https://api.slack.com/slash-commands):
+ token=gIkuvaNzQIHg97ATvDxqgjtO
+ team_id=T0001
+ team_domain=example
+ channel_id=C2147483705
+ channel_name=test
+ user_id=U2147483697
+ user_name=Steve
+ command=/weather
+ text=94070
+ """
+ req = flask.request
+
+ app_settings = models.AppSettings.get()
+ expected_token = app_settings.slack_slash_token
+
+ if not expected_token:
+ return ('Slack slash commands disabled. An admin '
+ 'can enable them at /admin/settings')
+
+ # verify slash API post token for security
+ if _REQUIRE_SLASH_TOKEN:
+ token = req.form.get('token')
+ if token != expected_token:
+ logging.error("POST MADE WITH INVALID TOKEN")
+ return "OH NO YOU DIDNT! Security issue plz contact admin."
+
+ user_name = req.form.get('user_name')
+ user_id = req.form.get('user_id')
+ text = req.form.get('text')
+
+ user = get_user_by_slack_id(user_id)
+ if user is None:
+ logging.info("Slack command from unrecognized user_id: %s", user_id)
+ return _no_user_error(user_id)
+
+ words = text.strip().split()
+ if not words:
+ logging.info('null (list) command from user %s', user_name)
+ return command_list(user.email)
+ else:
+ cmd, args = words[0], words[1:]
+ if cmd == 'help':
+ logging.info('help command from user %s', user_name)
+ return command_help()
+ elif cmd == 'whoami':
+ # undocumented command to echo user email back
+ logging.info('whoami command from user %s', user_name)
+ return str(user.email)
+ elif cmd == 'list':
+ # this is the same as the null command, but support for UX
+ logging.info('list command from user %s', user_name)
+ return command_list(user.email)
+ elif cmd == 'last':
+ logging.info('last command from user %s', user_name)
+ return command_last(user.email)
+ elif cmd == 'add':
+ logging.info('add command from user %s', user_name)
+ return command_add(user.email, " ".join(args))
+ elif cmd == 'del':
+ logging.info('del command from user %s', user_name)
+ return command_del(user.email, args)
+ elif cmd == 'dump':
+ logging.info('dump command from user %s', user_name)
+ return command_dump(user.email)
else:
- cmd, args = words[0], words[1:]
- if cmd == 'help':
- logging.info('help command from user %s', user_name)
- res.write(command_help())
- elif cmd == 'whoami':
- # undocumented command to echo user email back
- logging.info('whoami command from user %s', user_name)
- res.write(user_email)
- elif cmd == 'whoami!':
- # whoami! forces a refresh of cache, for debugging
- logging.info('whoami! command from user %s', user_name)
- logging.info('whoami! potential cached email for %s: %s',
- user_name, user_email)
- refreshed = _get_user_email_cached(user_id, force_refresh=True)
- logging.info('whoami! refreshed email for %s: %s',
- user_name, refreshed)
- res.write(refreshed)
- elif cmd == 'list':
- # this is the same as the null command, but support for UX
- logging.info('list command from user %s', user_name)
- res.write(command_list(user_email))
- elif cmd == 'last':
- logging.info('last command from user %s', user_name)
- res.write(command_last(user_email))
- elif cmd == 'add':
- logging.info('add command from user %s', user_name)
- res.write(command_add(user_email, " ".join(args)))
- elif cmd == 'del':
- logging.info('del command from user %s', user_name)
- res.write(command_del(user_email, args))
- elif cmd == 'dump':
- logging.info('dump command from user %s', user_name)
- res.write(command_dump(user_email))
- else:
- logging.info('unknown command %s from user %s', cmd, user_name)
- res.write(
- "I don't understand what you said! "
- "Perhaps you meant one of these?\n```%s```\n"
- % command_usage()
- )
+ logging.info('unknown command %s from user %s', cmd, user_name)
+ return (
+ "I don't understand what you said! "
+ "Perhaps you meant one of these?\n```%s```\n"
+ % command_usage()
+ )
diff --git a/slacklib_test.py b/slacklib_test.py
index 11cc2c7..5bc2905 100644
--- a/slacklib_test.py
+++ b/slacklib_test.py
@@ -1,39 +1,45 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
import datetime
import textwrap
import unittest
+from zoneinfo import ZoneInfo
-# Update sys.path so it can find these. We just need to add
-# 'google_appengine', but we add all of $PATH to be easy. This
-# assumes the google_appengine directory is on the path.
-import os
-import sys
-sys.path.extend(os.environ['PATH'].split(':'))
-import dev_appserver
-dev_appserver.fix_sys_path()
-from google.appengine.ext import db
from google.appengine.ext import testbed
+import time_machine
import models
import slacklib
-
+# The fictional date for these tests is Wednesday, July 29, 2015
+@time_machine.travel(datetime.datetime(2015, 7, 29, tzinfo=ZoneInfo("UTC")))
class SlashCommandTest(unittest.TestCase):
- def _mock_data(self):
- # The fictional day for these tests Wednesday, July 29, 2015
- slacklib._TODAY_FN = lambda: datetime.datetime(2015, 7, 29)
+ def setUp(self):
+ self.testbed = testbed.Testbed()
+ self.testbed.activate()
+ self.testbed.init_user_stub()
+
+ self.testbed.setup_env(
+ user_email="bob@example.com",
+ user_id="bob@example.com",
+ user_is_admin='0',
+ overwrite=True
+ )
+ appsettings = models.AppSettings.get(
+ create_if_missing=True,
+ domains=["khanacademy.org"],
+ )
+ appsettings.put()
# Stuart created his account, but has never once filled out a snippet
- db.put(models.User(email='stuart@khanacademy.org'))
+ models.User(email='stuart@khanacademy.org').put()
# Fleetwood has two recent snippets, and always uses markdown lists,
# but sometimes uses different list indicators or indention.
- db.put(models.User(email='fleetwood@khanacademy.org'))
- db.put(models.Snippet(
+ models.User(email='fleetwood@khanacademy.org').put()
+ models.Snippet(
email='fleetwood@khanacademy.org',
week=datetime.date(2015, 7, 27),
text=textwrap.dedent("""
@@ -41,21 +47,21 @@ def _mock_data(self):
* sniffed some things
* hoping to sniff more things! #yolo
""")
- ))
- db.put(models.Snippet(
+ ).put()
+ models.Snippet(
email='fleetwood@khanacademy.org',
week=datetime.date(2015, 7, 20),
text=textwrap.dedent("""
- lots of walks this week
- not enough sniffing, hope to remedy next week!
""")
- ))
+ ).put()
# Toby has filled out two snippets, but missed a week in-between while
# on vacation. When he got back from vacation he was still jetlagged so
# he wrote a longform paragraph instead of a list.
- db.put(models.User(email='toby@khanacademy.org'))
- db.put(models.Snippet(
+ models.User(email='toby@khanacademy.org').put()
+ models.Snippet(
email='toby@khanacademy.org',
week=datetime.date(2015, 7, 13),
text=textwrap.dedent("""
@@ -63,8 +69,8 @@ def _mock_data(self):
""")
- ))
- db.put(models.Snippet(
+ ).put()
+ models.Snippet(
email='toby@khanacademy.org',
week=datetime.date(2015, 7, 27),
text=textwrap.dedent("""
@@ -76,37 +82,30 @@ def _mock_data(self):
LUNCHTIME SUCKERS!
""")
- ))
+ ).put()
# Fozzie tried hard to create an entry manually in the previous week,
# but didn't understand markdown list syntax and got discouraged (so
# has no entry this week, and a malformed one last week).
- db.put(models.User(email='fozzie@khanacademy.org'))
- db.put(models.Snippet(
+ models.User(email='fozzie@khanacademy.org').put()
+ models.Snippet(
email='fozzie@khanacademy.org',
week=datetime.date(2015, 7, 20),
text=textwrap.dedent("""
-is this how I list?
-why is it not formatting??!?
""")
- ))
-
- def _most_recent_snippet(self, user_email):
- snippets_q = models.Snippet.all()
- snippets_q.filter('email = ', user_email)
- snippets_q.order('-week') # newest snippet first
- return snippets_q.fetch(1)[0]
-
- def setUp(self):
- self.testbed = testbed.Testbed()
- self.testbed.activate()
- self.testbed.init_datastore_v3_stub()
- self.testbed.init_memcache_stub()
- self._mock_data()
+ ).put()
def tearDown(self):
self.testbed.deactivate()
+ def _most_recent_snippet(self, user_email):
+ snippets_q = models.Snippet.query(
+ models.Snippet.email == user_email
+ ).order('-week') # newest snippet first
+ return snippets_q.fetch(1)[0]
+
def testDumpCommand_empty(self):
# user without a recent snippet should just see null text
response = slacklib.command_dump('stuart@khanacademy.org')
@@ -120,8 +119,8 @@ def testDumpCommand_formatting(self):
def testDumpCommand_noAccount(self):
# user without an account should get a helpful error message
response = slacklib.command_dump('bob@bob.com')
- self.assertIn("You don't appear to have a snippets account", response)
- self.assertIn("Slack email address: bob@bob.com", response)
+ self.assertIn("You don't seem to be logged in!", response)
+ self.assertIn("bob@bob.com", response)
def testListCommand_empty(self):
# user without a recent snippet should get a helpful message
@@ -142,7 +141,7 @@ def testListCommand_formatting(self):
def testListCommand_noAccount(self):
# user without an account should get a helpful error message
response = slacklib.command_list('bob@bob.com')
- self.assertIn("You don't appear to have a snippets account", response)
+ self.assertIn("You don't seem to be logged in!", response)
def testLastCommand_empty(self):
# user without a snippet last week should get a helpful message
@@ -160,7 +159,7 @@ def testLastCommand_formatting(self):
def testLastCommand_noAccount(self):
# user without an account should get a helpful error message
response = slacklib.command_last('bob@bob.com')
- self.assertIn("You don't appear to have a snippets account", response)
+ self.assertIn("You don't seem to be logged in!", response)
def testBadMarkdown_listCommand(self):
toby_recent = slacklib.command_list('toby@khanacademy.org')
@@ -175,8 +174,8 @@ def testAddCommand_blank(self):
r = slacklib.command_add('stuart@khanacademy.org', 'went to the park')
t = self._most_recent_snippet('stuart@khanacademy.org')
self.assertIn("Added *went to the park* to your weekly snippets", r)
- self.assertEquals('- went to the park', t.text)
- self.assertEquals(True, t.is_markdown)
+ self.assertEqual('- went to the park', t.text)
+ self.assertEqual(True, t.is_markdown)
def testAddCommand_existing(self):
# on this one, the user markdown formatting gets altered/standardized
@@ -189,7 +188,7 @@ def testAddCommand_existing(self):
- went to the park
""").strip()
self.assertEqual(expected, t.text)
- self.assertEquals(True, t.is_markdown)
+ self.assertEqual(True, t.is_markdown)
def testAddCommand_existingIsMalformed(self):
# we should be told we cannot to add to a snippet that is malformed!
@@ -200,7 +199,7 @@ def testAddCommand_existingIsMalformed(self):
t = self._most_recent_snippet(toby_email)
self.assertNotIn("went to the park", t.text)
self.assertIn("LUNCHTIME SUCKERS!", t.text)
- self.assertEquals(False, t.is_markdown)
+ self.assertEqual(False, t.is_markdown)
def testAddCommand_noArgs(self):
# we need to handle when they try to add nothing!
@@ -210,7 +209,7 @@ def testAddCommand_noArgs(self):
def testAddCommand_noAccount(self):
# dont crash horribly if user doesnt exist
r = slacklib.command_add('bob@bob.com', 'how is account formed?')
- self.assertIn("You don't appear to have a snippets account", r)
+ self.assertIn("You don't seem to be logged in!", r)
def testAddCommand_markupUsernames(self):
# usernames should be marked up properly so they get syntax highlighted
@@ -220,7 +219,7 @@ def testAddCommand_markupUsernames(self):
self.assertIn("- ate w/ <@toby>, yay", t.text)
def testAddCommand_unicode(self):
- r = slacklib.command_add('stuart@khanacademy.org', u'i “like” food')
+ r = slacklib.command_add('stuart@khanacademy.org', 'i “like” food')
t = self._most_recent_snippet('stuart@khanacademy.org')
self.assertIn('i “like” food', r)
self.assertIn('i “like” food', t.text)
@@ -233,7 +232,7 @@ def testDelCommand_noArgs(self):
def testDelCommand_noAccount(self):
# dont crash horribly if user doesnt exist
r = slacklib.command_del('bob@bob.com', ['1'])
- self.assertIn("You don't appear to have a snippets account", r)
+ self.assertIn("You don't seem to be logged in!", r)
def testDelCommand_normalCase(self):
r = slacklib.command_del('fleetwood@khanacademy.org', ['1'])
@@ -245,7 +244,7 @@ def testDelCommand_normalCase(self):
- hoping to sniff more things! #yolo
""").strip()
self.assertEqual(expected, t.text)
- self.assertEquals(True, t.is_markdown)
+ self.assertEqual(True, t.is_markdown)
def testDelCommand_nonexistentIndex(self):
r1 = slacklib.command_del('stuart@khanacademy.org', ['0'])
@@ -265,7 +264,4 @@ def testDelCommand_existingIsMalformed(self):
# ...and the existing snippets should not have been touched
t = self._most_recent_snippet('toby@khanacademy.org')
self.assertIn("I had fun", t.text)
- self.assertEquals(False, t.is_markdown)
-
-if __name__ == '__main__':
- unittest.main()
+ self.assertEqual(False, t.is_markdown)
diff --git a/snippets.py b/snippets.py
index 9d6f286..8d49679 100644
--- a/snippets.py
+++ b/snippets.py
@@ -13,38 +13,47 @@
import os
import re
import time
-import urllib
+import urllib.parse
from google.appengine.api import mail
from google.appengine.api import users
-from google.appengine.ext import db
-import webapp2
-from webapp2_extras import jinja2
+from google.cloud import ndb
+import google.cloud.logging
+import flask
import models
import slacklib
import util
+if os.getenv("GAE_ENV", "").startswith("standard"):
+ # We're running in the real appengine environment, hopefully.
+ # Set up cloud logging
+ logging_client = google.cloud.logging.Client()
+ logging_client.setup_logging(log_level=logging.INFO)
-# This allows mocking in a different day, for testing.
-_TODAY_FN = datetime.datetime.now
+app = flask.Flask(
+ __name__,
+ # Drop /static prefix for things like /favicon.ico
+ static_url_path="",
+ static_folder="static",
+ template_folder="templates")
+app.register_blueprint(slacklib.app_blueprint)
-jinja2.default_config['template_path'] = os.path.join(
- os.path.dirname(__file__),
- "templates"
-)
-jinja2.default_config['filters'] = {
- 'readable_date': (
- lambda value: value.strftime('%B %d, %Y').replace(' 0', ' ')),
- 'iso_date': (
- lambda value: value.strftime('%m-%d-%Y')),
-}
+@app.template_filter("readable_date")
+def _readable_date_filter(value: datetime.date):
+ return value.strftime('%B %d, %Y').replace(' 0', ' ')
-def _login_page(request, redirector):
+
+@app.template_filter("iso_date")
+def _iso_date_filter(value: datetime.date):
+ return value.strftime("%m-%d-%Y")
+
+
+def _login_page(request):
"""Redirect the user to a page where they can log in."""
- redirector.redirect(users.create_login_url(request.uri))
+ return flask.redirect(users.create_login_url(request.url))
def _current_user_email():
@@ -70,19 +79,21 @@ def _get_or_create_user(email, put_new_user=True):
# TODO(csilvers): move this get/update/put atomic into a txn
user.is_hidden = False
user.put()
+ return user
elif not _logged_in_user_has_permission_for(email):
# TODO(csilvers): turn this into a 403 somewhere
raise IndexError('User "%s" not found; did you specify'
' the full email address?' % email)
else:
- # You can only create a new user under one of the app-listed domains.
try:
app_settings = models.AppSettings.get()
except ValueError:
# TODO(csilvers): do this instead:
# /admin/settings?redirect_to=user_setting
- return None
+ # return None
+ raise # TODO(benley) implement Redirect exception
+ # You can only create a new user under one of the app-listed domains.
domain = email.split('@')[-1]
allowed_domains = app_settings.domains
if domain not in allowed_domains:
@@ -93,15 +104,14 @@ def _get_or_create_user(email, put_new_user=True):
% (' or '.join(allowed_domains), domain))
# Set the user defaults based on the global app defaults.
- user = models.User(created=_TODAY_FN(),
+ user = models.User(created=datetime.datetime.now(),
email=email,
uses_markdown=app_settings.default_markdown,
private_snippets=app_settings.default_private,
wants_email=app_settings.default_email)
if put_new_user:
- db.put(user)
- db.get(user.key()) # ensure db consistency for HRD
- return user
+ user.put()
+ return user
def _logged_in_user_has_permission_for(email):
@@ -146,527 +156,530 @@ def _send_to_chat(msg, url_path):
slacklib.send_to_slack_channel(slack_channel, msg)
-class BaseHandler(webapp2.RequestHandler):
- """Set up as per the jinja2.py docstring."""
- @webapp2.cached_property
- def jinja2(self):
- return jinja2.get_jinja2()
+@app.route("/")
+def user_page_handler():
+ """Show all the snippets for a single user."""
- def render_response(self, template_filename, context):
- html = self.jinja2.render_template(template_filename, **context)
- self.response.write(html)
+ if not users.get_current_user():
+ return _login_page(flask.request)
+ user_email = flask.request.args.get('u', _current_user_email())
+ user = util.get_user(user_email)
-class UserPage(BaseHandler):
- """Show all the snippets for a single user."""
+ now = datetime.datetime.now()
- def get(self):
- if not users.get_current_user():
- return _login_page(self.request, self)
-
- user_email = self.request.get('u', _current_user_email())
- user = util.get_user(user_email)
-
- if not user:
- # If there are no app settings, set those up before setting
- # up the user settings.
- if users.is_current_user_admin():
- try:
- models.AppSettings.get()
- except ValueError:
- self.redirect("/admin/settings?redirect_to=user_setting"
- "&msg=Welcome+to+the+snippet+server!+"
- "Please+take+a+moment+to+configure+it.")
- return
-
- template_values = {
- 'new_user': True,
- 'login_url': users.create_login_url(self.request.uri),
- 'logout_url': users.create_logout_url('/'),
- 'username': user_email,
- }
- self.render_response('new_user.html', template_values)
- return
-
- snippets = util.snippets_for_user(user_email)
-
- if not _can_view_private_snippets(_current_user_email(), user_email):
- snippets = [snippet for snippet in snippets if not snippet.private]
- snippets = util.fill_in_missing_snippets(snippets, user,
- user_email, _TODAY_FN())
- snippets.reverse() # get to newest snippet first
+ if not user:
+ # If there are no app settings, set those up before setting
+ # up the user settings.
+ if users.is_current_user_admin():
+ try:
+ models.AppSettings.get()
+ except ValueError:
+ return flask.redirect(
+ "/admin/settings?redirect_to=user_setting"
+ "&msg=Welcome+to+the+snippet+server!+"
+ "Please+take+a+moment+to+configure+it.")
template_values = {
+ 'new_user': True,
+ 'login_url': users.create_login_url(flask.request.url),
'logout_url': users.create_logout_url('/'),
- 'message': self.request.get('msg'),
'username': user_email,
- 'is_admin': users.is_current_user_admin(),
- 'domain': user_email.split('@')[-1],
- 'view_week': util.existingsnippet_monday(_TODAY_FN()),
- # Snippets for the week of are due today.
- 'one_week_ago': _TODAY_FN().date() - datetime.timedelta(days=7),
- 'eight_days_ago': _TODAY_FN().date() - datetime.timedelta(days=8),
- 'editable': (_logged_in_user_has_permission_for(user_email) and
- self.request.get('edit', '1') == '1'),
- 'user': user,
- 'snippets': snippets,
- 'null_category': models.NULL_CATEGORY,
}
- self.render_response('user_snippets.html', template_values)
+ return flask.render_template('new_user.html', **template_values)
+
+ snippets = util.snippets_for_user(user_email)
+
+ if not _can_view_private_snippets(_current_user_email(), user_email):
+ snippets = [snippet for snippet in snippets if not snippet.private]
+ snippets = util.fill_in_missing_snippets(snippets, user, user_email, now)
+ snippets.reverse() # get to newest snippet first
+
+ template_values = {
+ 'logout_url': users.create_logout_url('/'),
+ 'message': flask.request.args.get('msg'),
+ 'username': user_email,
+ 'is_admin': users.is_current_user_admin(),
+ 'domain': user_email.split('@')[-1],
+ 'view_week': util.existingsnippet_monday(now),
+ # Snippets for the week of are due today.
+ 'one_week_ago': now.date() - datetime.timedelta(days=7),
+ 'eight_days_ago': now.date() - datetime.timedelta(days=8),
+ 'editable': (_logged_in_user_has_permission_for(user_email) and
+ flask.request.args.get('edit', '1') == '1'),
+ 'user': user,
+ 'snippets': snippets,
+ 'null_category': models.NULL_CATEGORY,
+ }
+ return flask.render_template('user_snippets.html', **template_values)
def _title_case(s):
"""Like string.title(), but does not uppercase 'and'."""
- # Smarter would be to use 'pip install titlecase'.
- SMALL = 'a|an|and|as|at|but|by|en|for|if|in|of|on|or|the|to|v\.?|via|vs\.?'
+ # TODO(benley): use titlecase from pypi?
+ SMALL = r'a|an|and|as|at|but|by|en|for|if|in|of|on|or|the|to|v\.?|via|vs\.?'
# We purposefully don't match small words at the beginning of a string.
SMALL_RE = re.compile(r' (%s)\b' % SMALL, re.I)
return SMALL_RE.sub(lambda m: ' ' + m.group(1).lower(), s.title().strip())
-class SummaryPage(BaseHandler):
+@app.route("/weekly")
+def summary_page_handler():
"""Show all the snippets for a single week."""
- def get(self):
- if not users.get_current_user():
- return _login_page(self.request, self)
+ if not users.get_current_user():
+ return _login_page(flask.request)
- week_string = self.request.get('week')
- if week_string:
- week = datetime.datetime.strptime(week_string, '%m-%d-%Y').date()
+ week_string = flask.request.args.get('week')
+ if week_string:
+ week = datetime.datetime.strptime(week_string, '%m-%d-%Y').date()
+ else:
+ week = util.existingsnippet_monday(datetime.datetime.now())
+
+ snippets_q = models.Snippet.query(
+ models.Snippet.week == week
+ )
+ snippets = snippets_q.fetch(1000) # good for many users...
+ # TODO(csilvers): filter based on wants_to_view
+
+ # Get all the user records so we can categorize snippets.
+ user_q = models.User.query()
+ results = user_q.fetch(1000)
+ email_to_category = {}
+ email_to_user = {}
+ for result in results:
+ # People aren't very good about capitalizing their
+ # categories consistently, so we enforce title-case,
+ # with exceptions for 'and'.
+ email_to_category[result.email] = _title_case(result.category)
+ email_to_user[result.email] = result
+
+ # Collect the snippets and users by category. As we see each email,
+ # delete it from email_to_category. At the end of this,
+ # email_to_category will hold people who did not give
+ # snippets this week.
+ snippets_and_users_by_category = {}
+ for snippet in snippets:
+ # Ignore this snippet if we don't have permission to view it.
+ if (snippet.private and
+ not _can_view_private_snippets(_current_user_email(),
+ snippet.email)):
+ continue
+ category = email_to_category.get(
+ snippet.email, models.NULL_CATEGORY
+ )
+ if snippet.email in email_to_user:
+ snippets_and_users_by_category.setdefault(category, []).append(
+ (snippet, email_to_user[snippet.email])
+ )
else:
- week = util.existingsnippet_monday(_TODAY_FN())
-
- snippets_q = models.Snippet.all()
- snippets_q.filter('week = ', week)
- snippets = snippets_q.fetch(1000) # good for many users...
- # TODO(csilvers): filter based on wants_to_view
-
- # Get all the user records so we can categorize snippets.
- user_q = models.User.all()
- results = user_q.fetch(1000)
- email_to_category = {}
- email_to_user = {}
- for result in results:
- # People aren't very good about capitalizing their
- # categories consistently, so we enforce title-case,
- # with exceptions for 'and'.
- email_to_category[result.email] = _title_case(result.category)
- email_to_user[result.email] = result
-
- # Collect the snippets and users by category. As we see each email,
- # delete it from email_to_category. At the end of this,
- # email_to_category will hold people who did not give
- # snippets this week.
- snippets_and_users_by_category = {}
- for snippet in snippets:
- # Ignore this snippet if we don't have permission to view it.
- if (snippet.private and
- not _can_view_private_snippets(_current_user_email(),
- snippet.email)):
- continue
- category = email_to_category.get(
- snippet.email, models.NULL_CATEGORY
+ snippets_and_users_by_category.setdefault(category, []).append(
+ (snippet, models.User(email=snippet.email))
)
- if snippet.email in email_to_user:
- snippets_and_users_by_category.setdefault(category, []).append(
- (snippet, email_to_user[snippet.email])
- )
- else:
- snippets_and_users_by_category.setdefault(category, []).append(
- (snippet, models.User(email=snippet.email))
- )
-
- if snippet.email in email_to_category:
- del email_to_category[snippet.email]
-
- # Add in empty snippets for the people who didn't have any --
- # unless a user is marked 'hidden'. (That's what 'hidden'
- # means: pretend they don't exist until they have a non-empty
- # snippet again.)
- for (email, category) in email_to_category.iteritems():
- if not email_to_user[email].is_hidden:
- snippet = models.Snippet(email=email, week=week)
- snippets_and_users_by_category.setdefault(category, []).append(
- (snippet, email_to_user[snippet.email])
- )
-
- # Now get a sorted list, categories in alphabetical order and
- # each snippet-author within the category in alphabetical
- # order.
- # The data structure is ((category, ((snippet, user), ...)), ...)
- categories_and_snippets = []
- for (category,
- snippets_and_users) in snippets_and_users_by_category.iteritems():
- snippets_and_users.sort(key=lambda (snippet, user): snippet.email)
- categories_and_snippets.append((category, snippets_and_users))
- categories_and_snippets.sort()
- template_values = {
- 'logout_url': users.create_logout_url('/'),
- 'message': self.request.get('msg'),
- # Used only to switch to 'username' mode and to modify settings.
- 'username': _current_user_email(),
- 'is_admin': users.is_current_user_admin(),
- 'prev_week': week - datetime.timedelta(7),
- 'view_week': week,
- 'next_week': week + datetime.timedelta(7),
- 'categories_and_snippets': categories_and_snippets,
- }
- self.render_response('weekly_snippets.html', template_values)
+ if snippet.email in email_to_category:
+ del email_to_category[snippet.email]
+
+ # Add in empty snippets for the people who didn't have any --
+ # unless a user is marked 'hidden'. (That's what 'hidden'
+ # means: pretend they don't exist until they have a non-empty
+ # snippet again.)
+ for (email, category) in email_to_category.items():
+ if not email_to_user[email].is_hidden:
+ snippet = models.Snippet(email=email, week=week)
+ snippets_and_users_by_category.setdefault(category, []).append(
+ (snippet, email_to_user[snippet.email])
+ )
+
+ # Now get a sorted list, categories in alphabetical order and
+ # each snippet-author within the category in alphabetical
+ # order.
+ # The data structure is ((category, ((snippet, user), ...)), ...)
+ categories_and_snippets = []
+ for category, snippets_and_users in snippets_and_users_by_category.items():
+ # This looks stupid but python no longer allows lambda (snippet, user): snippet.email
+ snippets_and_users.sort(key=lambda snippet_user: snippet_user[0].email)
+ categories_and_snippets.append((category, snippets_and_users))
+ categories_and_snippets.sort()
+
+ template_values = {
+ 'logout_url': users.create_logout_url('/'),
+ 'message': flask.request.args.get('msg'),
+ # Used only to switch to 'username' mode and to modify settings.
+ 'username': _current_user_email(),
+ 'is_admin': users.is_current_user_admin(),
+ 'prev_week': week - datetime.timedelta(7),
+ 'view_week': week,
+ 'next_week': week + datetime.timedelta(7),
+ 'categories_and_snippets': categories_and_snippets,
+ }
+ return flask.render_template('weekly_snippets.html', **template_values)
+
+
+def update_snippet(email: str,
+ week: datetime.date,
+ text: str,
+ private: bool,
+ is_markdown: bool):
+ assert week.weekday() == 0, 'passed-in date must be a Monday'
+
+ # TODO(csilvers): make this get-update-put atomic.
+ # (maybe make the snippet id be email + week).
+ q = models.Snippet.query(
+ models.Snippet.email == email,
+ models.Snippet.week == week
+ )
+ snippet = q.get()
+
+ # When adding a snippet, make sure we create a user record for
+ # that email as well, if it doesn't already exist.
+ user = _get_or_create_user(email)
+
+ # Store user's display_name in snippet so that if a user is later
+ # deleted, we could still show his / her display_name.
+ if snippet:
+ snippet.text = text # just update the snippet text
+ snippet.display_name = user.display_name
+ snippet.private = private
+ snippet.is_markdown = is_markdown
+ else:
+ # add the snippet to the db
+ snippet = models.Snippet(created=datetime.datetime.now(),
+ display_name=user.display_name,
+ email=email, week=week,
+ text=text, private=private,
+ is_markdown=is_markdown)
+ snippet.put()
-class UpdateSnippet(BaseHandler):
- def update_snippet(self, email):
- week_string = self.request.get('week')
+@app.route("/update_snippet", methods=["POST"])
+def update_snippet_handler_post():
+ """Handle ajax updates via POST.
+
+ In particular, return status via json rather than redirects and hard
+ exceptions. This isn't actually RESTy, it's just status codes and json.
+ """
+
+ data = flask.request.form
+ week_string = data.get('week', '')
+ text = data.get('snippet', '')
+ private = data.get('private') == 'True'
+ is_markdown = data.get('is_markdown') == 'True'
+
+ # TODO(marcos): consider using PUT?
+
+ if not users.get_current_user():
+ return flask.make_response(
+ {"status": 401, "message": "not logged in"}, 401)
+
+ email = data.get('u', _current_user_email())
+ if not _logged_in_user_has_permission_for(email):
+ # TODO(marcos): present these messages to the ajax client
+ error = ('You do not have permissions to update user'
+ ' snippets for %s' % email)
+ return flask.make_response({"status": 403, "message": error}, 403)
+
+ try:
week = datetime.datetime.strptime(week_string, '%m-%d-%Y').date()
- assert week.weekday() == 0, 'passed-in date must be a Monday'
-
- text = self.request.get('snippet')
-
- private = self.request.get('private') == 'True'
- is_markdown = self.request.get('is_markdown') == 'True'
-
- # TODO(csilvers): make this get-update-put atomic.
- # (maybe make the snippet id be email + week).
- q = models.Snippet.all()
- q.filter('email = ', email)
- q.filter('week = ', week)
- snippet = q.get()
-
- # When adding a snippet, make sure we create a user record for
- # that email as well, if it doesn't already exist.
- user = _get_or_create_user(email)
-
- # Store user's display_name in snippet so that if a user is later
- # deleted, we could still show his / her display_name.
- if snippet:
- snippet.text = text # just update the snippet text
- snippet.display_name = user.display_name
- snippet.private = private
- snippet.is_markdown = is_markdown
- else:
- # add the snippet to the db
- snippet = models.Snippet(created=_TODAY_FN(),
- display_name=user.display_name,
- email=email, week=week,
- text=text, private=private,
- is_markdown=is_markdown)
- db.put(snippet)
- db.get(snippet.key()) # ensure db consistency for HRD
-
- self.response.set_status(200)
-
- def post(self):
- """handle ajax updates via POST
-
- in particular, return status via json rather than redirects and
- hard exceptions. This isn't actually RESTy, it's just status
- codes and json.
- """
- # TODO(marcos): consider using PUT?
-
- self.response.headers['Content-Type'] = 'application/json'
-
- if not users.get_current_user():
- # 403s are the catch-all 'please log in error' here
- self.response.set_status(403)
- self.response.out.write('{"status": 403, '
- '"message": "not logged in"}')
- return
-
- email = self.request.get('u', _current_user_email())
-
- if not _logged_in_user_has_permission_for(email):
- # TODO(marcos): present these messages to the ajax client
- self.response.set_status(403)
- error = ('You do not have permissions to update user'
- ' snippets for %s' % email)
- self.response.out.write('{"status": 403, '
- '"message": "%s"}' % error)
- return
-
- self.update_snippet(email)
- self.response.out.write('{"status": 200, "message": "ok"}')
-
- def get(self):
- if not users.get_current_user():
- return _login_page(self.request, self)
-
- email = self.request.get('u', _current_user_email())
- if not _logged_in_user_has_permission_for(email):
- # TODO(csilvers): return a 403 here instead.
- raise RuntimeError('You do not have permissions to update user'
- ' snippets for %s' % email)
-
- self.update_snippet(email)
-
- email = self.request.get('u', _current_user_email())
- self.redirect("/?msg=Snippet+saved&u=%s" % urllib.quote(email))
-
-
-class Settings(BaseHandler):
- """Page to display a user's settings (from class User) for modification."""
+ except (ValueError, TypeError):
+ return flask.make_response(
+ {"status": 400,
+ "message": "Invalid week. Expected format: MM-DD-YYYY"},
+ 400)
- def get(self):
- if not users.get_current_user():
- return _login_page(self.request, self)
-
- user_email = self.request.get('u', _current_user_email())
- if not _logged_in_user_has_permission_for(user_email):
- # TODO(csilvers): return a 403 here instead.
- raise RuntimeError('You do not have permissions to view user'
- ' settings for %s' % user_email)
- # We won't put() the new user until the settings are saved.
- user = _get_or_create_user(user_email, put_new_user=False)
- try:
- user.key()
- is_new_user = False
- except db.NotSavedError:
- is_new_user = True
+ try:
+ update_snippet(email, week, text, private, is_markdown)
+ except AssertionError as err:
+ return flask.make_response({"status": 400, "message": err}, 400)
- template_values = {
- 'logout_url': users.create_logout_url('/'),
- 'message': self.request.get('msg'),
- 'username': user.email,
- 'is_admin': users.is_current_user_admin(),
- 'view_week': util.existingsnippet_monday(_TODAY_FN()),
- 'user': user,
- 'is_new_user': is_new_user,
- 'redirect_to': self.request.get('redirect_to', ''),
- # We could get this from user, but we want to replace
- # commas with newlines for printing.
- 'wants_to_view': user.wants_to_view.replace(',', '\n'),
- }
- self.render_response('settings.html', template_values)
+ return flask.make_response({"status": 200, "message": "ok"})
-class UpdateSettings(BaseHandler):
- """Updates the db with modifications from the Settings page."""
+@app.route("/update_snippet", methods=["GET"])
+def update_snippet_handler_get():
+ data = flask.request.args
+ week_string = data.get('week', '')
+ text = data.get('snippet', '')
+ private = data.get('private') == 'True'
+ is_markdown = data.get('is_markdown') == 'True'
- def get(self):
- if not users.get_current_user():
- return _login_page(self.request, self)
-
- user_email = self.request.get('u', _current_user_email())
- if not _logged_in_user_has_permission_for(user_email):
- # TODO(csilvers): return a 403 here instead.
- raise RuntimeError('You do not have permissions to modify user'
- ' settings for %s' % user_email)
- # TODO(csilvers): make this get/update/put atomic (put in a txn)
- user = _get_or_create_user(user_email)
-
- # First, check if the user clicked on 'delete' or 'hide'
- # rather than 'save'.
- if self.request.get('hide'):
- user.is_hidden = True
- user.put()
- time.sleep(0.1) # some time for eventual consistency
- self.redirect('/weekly?msg=You+are+now+hidden.+Have+a+nice+day!')
- return
- elif self.request.get('delete'):
- db.delete(user)
- self.redirect('/weekly?msg=Your+account+has+been+deleted.+'
- '(Note+your+existing+snippets+have+NOT+been+'
- 'deleted.)+Have+a+nice+day!')
- return
-
- display_name = self.request.get('display_name')
- category = self.request.get('category')
- uses_markdown = self.request.get('markdown') == 'yes'
- private_snippets = self.request.get('private') == 'yes'
- wants_email = self.request.get('reminder_email') == 'yes'
-
- # We want this list to be comma-separated, but people are
- # likely to use whitespace to separate as well. Convert here.
- wants_to_view = self.request.get('to_view')
- wants_to_view = re.sub(r'\s+', ',', wants_to_view)
- wants_to_view = wants_to_view.split(',')
- wants_to_view = [w for w in wants_to_view if w] # deal with ',,'
- wants_to_view = ','.join(wants_to_view) # TODO(csilvers): keep as list
-
- # Changing their settings is the kind of activity that unhides
- # someone who was hidden, unless they specifically ask to be
- # hidden.
- is_hidden = self.request.get('is_hidden', 'no') == 'yes'
-
- user.is_hidden = is_hidden
- user.display_name = display_name
- user.category = category or models.NULL_CATEGORY
- user.uses_markdown = uses_markdown
- user.private_snippets = private_snippets
- user.wants_email = wants_email
- user.wants_to_view = wants_to_view
- db.put(user)
- db.get(user.key()) # ensure db consistency for HRD
-
- redirect_to = self.request.get('redirect_to')
- if redirect_to == 'snippet_entry': # true for new_user.html
- self.redirect('/?u=%s' % urllib.quote(user_email))
- else:
- self.redirect("/settings?msg=Changes+saved&u=%s"
- % urllib.quote(user_email))
+ if not users.get_current_user():
+ return _login_page(flask.request)
+ email = data.get('u', _current_user_email())
+ if not _logged_in_user_has_permission_for(email):
+ # TODO(benley): Add a friendlier error page template, maybe?
+ return flask.make_response("You do not have permission to update"
+ " user snippets for %s" % email, 403)
-class AppSettings(BaseHandler):
- """Page to display settings for the whole app, for modification.
+ try:
+ week = datetime.datetime.strptime(week_string, '%m-%d-%Y').date()
+ except (ValueError, TypeError):
+ return flask.make_response(
+ "Invalid week. Expected format: MM-DD-YYYY", 400)
- This page should be restricted to admin users via app.yaml.
- """
+ try:
+ update_snippet(email, week, text, private, is_markdown)
+ except AssertionError as err:
+ return flask.make_response("Failed to save snippet: %s" % err, 400)
- def get(self):
- my_domain = _current_user_email().split('@')[-1]
- app_settings = models.AppSettings.get(create_if_missing=True,
- domains=[my_domain])
+ return flask.redirect(
+ "/?msg=Snippet+saved&u=%s" % urllib.parse.quote(email))
- template_values = {
- 'logout_url': users.create_logout_url('/'),
- 'message': self.request.get('msg'),
- 'username': _current_user_email(),
- 'is_admin': users.is_current_user_admin(),
- 'view_week': util.existingsnippet_monday(_TODAY_FN()),
- 'redirect_to': self.request.get('redirect_to', ''),
- 'settings': app_settings,
- 'slack_slash_commands': (
- slacklib.command_usage().strip())
- }
- self.render_response('app_settings.html', template_values)
+@app.route("/settings")
+def settings_handler():
+ """Page to display a user's settings (from class User) for modification."""
-class UpdateAppSettings(BaseHandler):
- """Updates the db with modifications from the App-Settings page.
+ if not users.get_current_user():
+ return _login_page(flask.request)
+
+ user_email = flask.request.args.get('u', _current_user_email())
+ if not _logged_in_user_has_permission_for(user_email):
+ return flask.make_response('You do not have permissions to view user'
+ ' settings for %s' % user_email, 403)
+ # We won't put() the new user until the settings are saved.
+ user = _get_or_create_user(user_email, put_new_user=False)
+
+ # TODO(benley): https://stackoverflow.com/questions/12083254/is-it-possible-to-determine-with-ndb-if-model-is-persistent-in-the-datastore-or/12096066#12096066
+ # NOTE: this will break if you explicitly set the key when creating the
+ # user entity!
+ # See https://groups.google.com/g/google-appengine/c/Tm8NDWIvc70
+ is_new_user = bool(user.key and user.key.id())
+
+ template_values = {
+ 'logout_url': users.create_logout_url('/'),
+ 'message': flask.request.args.get('msg'),
+ 'username': user.email,
+ 'is_admin': users.is_current_user_admin(),
+ 'view_week': util.existingsnippet_monday(datetime.datetime.now()),
+ 'user': user,
+ 'is_new_user': is_new_user,
+ 'redirect_to': flask.request.args.get('redirect_to', ''),
+ # We could get this from user, but we want to replace
+ # commas with newlines for printing.
+ 'wants_to_view': user.wants_to_view.replace(',', '\n'),
+ }
+ return flask.render_template('settings.html', **template_values)
+
+
+@app.route("/update_settings")
+def update_settings_handler():
+ """Updates the db with modifications from the Settings page."""
+
+ if not users.get_current_user():
+ return _login_page(flask.request)
+
+ user_email = flask.request.args.get('u', _current_user_email())
+ if not _logged_in_user_has_permission_for(user_email):
+ return flask.make_response('You do not have permissions to modify user'
+ ' settings for %s' % user_email, 403)
+ # TODO(csilvers): make this get/update/put atomic (put in a txn)
+ user = _get_or_create_user(user_email)
+
+ # First, check if the user clicked on 'delete' or 'hide'
+ # rather than 'save'.
+ if flask.request.args.get('hide'):
+ user.is_hidden = True
+ user.put()
+ return flask.redirect('/weekly?msg=You+are+now+hidden.+Have+a+nice+day!')
+ elif flask.request.args.get('delete'):
+ user.key.delete()
+ return flask.redirect('/weekly?msg=Your+account+has+been+deleted.+'
+ '(Note+your+existing+snippets+have+NOT+been+'
+ 'deleted.)+Have+a+nice+day!')
+
+ display_name = flask.request.args.get('display_name')
+ category = flask.request.args.get('category')
+ slack_id = flask.request.args.get('slack_id')
+ uses_markdown = flask.request.args.get('markdown') == 'yes'
+ private_snippets = flask.request.args.get('private') == 'yes'
+ wants_email = flask.request.args.get('reminder_email') == 'yes'
+
+ # We want this list to be comma-separated, but people are
+ # likely to use whitespace to separate as well. Convert here.
+ wants_to_view = flask.request.args.get('to_view', '')
+ wants_to_view = re.sub(r'\s+', ',', wants_to_view)
+ wants_to_view = wants_to_view.split(',')
+ wants_to_view = [w for w in wants_to_view if w] # deal with ',,'
+ wants_to_view = ','.join(wants_to_view) # TODO(csilvers): keep as list
+
+ # Changing their settings is the kind of activity that unhides
+ # someone who was hidden, unless they specifically ask to be
+ # hidden.
+ is_hidden = flask.request.args.get('is_hidden', 'no') == 'yes'
+
+ user.is_hidden = is_hidden
+ user.display_name = display_name
+ user.category = category or models.NULL_CATEGORY
+ user.slack_id = slack_id
+ user.uses_markdown = uses_markdown
+ user.private_snippets = private_snippets
+ user.wants_email = wants_email
+ user.wants_to_view = wants_to_view
+ user.put()
+
+ redirect_to = flask.request.args.get('redirect_to')
+ if redirect_to == 'snippet_entry': # true for new_user.html
+ return flask.redirect('/?u=%s' % urllib.parse.quote(user_email))
+ else:
+ return flask.redirect("/settings?msg=Changes+saved&u=%s"
+ % urllib.parse.quote(user_email))
+
+
+@app.route("/admin/settings")
+def admin_settings_handler():
+ """Page to display settings for the whole app, for modification.
This page should be restricted to admin users via app.yaml.
"""
+ my_domain = _current_user_email().split('@')[-1]
+ app_settings = models.AppSettings.get(create_if_missing=True,
+ domains=[my_domain])
+
+ template_values = {
+ 'logout_url': users.create_logout_url('/'),
+ 'message': flask.request.args.get('msg'),
+ 'username': _current_user_email(),
+ 'is_admin': users.is_current_user_admin(),
+ 'view_week': util.existingsnippet_monday(datetime.datetime.now()),
+ 'redirect_to': flask.request.args.get('redirect_to', ''),
+ 'settings': app_settings,
+ 'slack_slash_commands': slacklib.command_usage().strip()
+ }
+ return flask.render_template('app_settings.html', **template_values)
+
+
+@app.route("/admin/update_settings", methods=["POST"])
+def admin_update_settings_handler():
+ """Updates the db with modifications from the App-Settings page.
- def get(self):
- _get_or_create_user(_current_user_email())
-
- domains = self.request.get('domains')
- default_private = self.request.get('private') == 'yes'
- default_markdown = self.request.get('markdown') == 'yes'
- default_email = self.request.get('reminder_email') == 'yes'
- email_from = self.request.get('email_from')
- slack_channel = self.request.get('slack_channel')
- slack_token = self.request.get('slack_token')
- slack_slash_token = self.request.get('slack_slash_token')
-
- # Turn domains into a list. Allow whitespace or comma to separate.
- domains = re.sub(r'\s+', ',', domains)
- domains = [d for d in domains.split(',') if d]
-
- @db.transactional
- def update_settings():
- app_settings = models.AppSettings.get(create_if_missing=True,
- domains=domains)
- app_settings.domains = domains
- app_settings.default_private = default_private
- app_settings.default_markdown = default_markdown
- app_settings.default_email = default_email
- app_settings.email_from = email_from
- app_settings.slack_channel = slack_channel
- app_settings.slack_token = slack_token
- app_settings.slack_slash_token = slack_slash_token
- app_settings.put()
-
- update_settings()
-
- redirect_to = self.request.get('redirect_to')
- if redirect_to == 'user_setting': # true for new_user.html
- self.redirect('/settings?redirect_to=snippet_entry'
- '&msg=Now+enter+your+personal+user+settings.')
- else:
- self.redirect("/admin/settings?msg=Changes+saved")
+ This page should be restricted to admin users via app.yaml.
+ """
+ domains = flask.request.form.get('domains')
+ default_private = flask.request.form.get('private') == 'yes'
+ default_markdown = flask.request.form.get('markdown') == 'yes'
+ default_email = flask.request.form.get('reminder_email') == 'yes'
+ email_from = flask.request.form.get('email_from')
+ hipchat_room = flask.request.form.get('hipchat_room')
+ hipchat_token = flask.request.form.get('hipchat_token')
+ hostname = flask.request.form.get('hostname')
+ slack_channel = flask.request.form.get('slack_channel')
+ slack_token = flask.request.form.get('slack_token')
+ slack_slash_token = flask.request.form.get('slack_slash_token')
+
+ # Turn domains into a list. Allow whitespace or comma to separate.
+ domains = re.sub(r'\s+', ',', domains)
+ domains = [d for d in domains.split(',') if d]
+
+ @ndb.transactional()
+ def update_settings():
+ app_settings = models.AppSettings.get(create_if_missing=True,
+ domains=domains)
+ app_settings.domains = domains
+ app_settings.default_private = default_private
+ app_settings.default_markdown = default_markdown
+ app_settings.default_email = default_email
+ app_settings.email_from = email_from
+ app_settings.hostname = hostname
+ app_settings.slack_channel = slack_channel
+ app_settings.slack_token = slack_token
+ app_settings.slack_slash_token = slack_slash_token
+ app_settings.put()
+
+ update_settings()
+
+ _get_or_create_user(_current_user_email())
+
+ redirect_to = flask.request.form.get('redirect_to')
+ if redirect_to == 'user_setting': # true for new_user.html
+ return flask.redirect('/settings?redirect_to=snippet_entry'
+ '&msg=Now+enter+your+personal+user+settings.')
+ else:
+ return flask.redirect("/admin/settings?msg=Changes+saved")
-class ManageUsers(BaseHandler):
+@app.route("/admin/manage_users", methods=["GET", "POST"])
+def admin_manage_users_handler():
"""Lets admins delete and otherwise manage users."""
-
- def get(self):
- # options are 'email', 'creation_time', 'last_snippet_time'
- sort_by = self.request.get('sort_by', 'creation_time')
-
- # First, check if the user had clicked on a button.
- for (name, value) in self.request.params.iteritems():
- if name.startswith('hide '):
- email_of_user_to_hide = name[len('hide '):]
- # TODO(csilvers): move this get/update/put atomic into a txn
- user = util.get_user_or_die(email_of_user_to_hide)
- user.is_hidden = True
- user.put()
- time.sleep(0.1) # encourage eventual consistency
- self.redirect('/admin/manage_users?sort_by=%s&msg=%s+hidden'
- % (sort_by, email_of_user_to_hide))
- return
- if name.startswith('unhide '):
- email_of_user_to_unhide = name[len('unhide '):]
- # TODO(csilvers): move this get/update/put atomic into a txn
- user = util.get_user_or_die(email_of_user_to_unhide)
- user.is_hidden = False
- user.put()
- time.sleep(0.1) # encourage eventual consistency
- self.redirect('/admin/manage_users?sort_by=%s&msg=%s+unhidden'
- % (sort_by, email_of_user_to_unhide))
- return
- if name.startswith('delete '):
- email_of_user_to_delete = name[len('delete '):]
- user = util.get_user_or_die(email_of_user_to_delete)
- db.delete(user)
- time.sleep(0.1) # encourage eventual consistency
- self.redirect('/admin/manage_users?sort_by=%s&msg=%s+deleted'
- % (sort_by, email_of_user_to_delete))
- return
-
- user_q = models.User.all()
- results = user_q.fetch(1000)
-
- # Tuple: (email, is-hidden, creation-time, days since last snippet)
- user_data = []
- for user in results:
- # Get the last snippet for that user.
- last_snippet = util.most_recent_snippet_for_user(user.email)
- if last_snippet:
- seconds_since_snippet = (
- (_TODAY_FN().date() - last_snippet.week).total_seconds())
- weeks_since_snippet = int(
- seconds_since_snippet /
- datetime.timedelta(days=7).total_seconds())
- else:
- weeks_since_snippet = None
- user_data.append((user.email, user.is_hidden,
- user.created, weeks_since_snippet))
-
- # We have to use 'cmp' here since we want ascending in the
- # primary key and descending in the secondary key, sometimes.
- if sort_by == 'email':
- user_data.sort(lambda x, y: cmp(x[0], y[0]))
- elif sort_by == 'creation_time':
- user_data.sort(lambda x, y: (-cmp(x[2] or datetime.datetime.min,
- y[2] or datetime.datetime.min)
- or cmp(x[0], y[0])))
- elif sort_by == 'last_snippet_time':
- user_data.sort(lambda x, y: (-cmp(1000 if x[3] is None else x[3],
- 1000 if y[3] is None else y[3])
- or cmp(x[0], y[0])))
+ # options are 'email', 'creation_time', 'last_snippet_time'
+ sort_by = flask.request.args.get('sort_by', 'creation_time')
+
+ # First, check if the user had clicked on a button.
+ if flask.request.form.get("action") == "hide":
+ email_of_user_to_hide = flask.request.form.get("email")
+ # TODO(csilvers): move this get/update/put atomic into a txn
+ user = util.get_user_or_die(email_of_user_to_hide)
+ user.is_hidden = True
+ user.put()
+ logging.info("Hide user: %s", user.email)
+ return flask.redirect('/admin/manage_users?sort_by=%s&msg=%s+hidden'
+ % (sort_by, user.email))
+ elif flask.request.form.get("action") == "unhide":
+ email_of_user_to_unhide = flask.request.form.get("email")
+ # TODO(csilvers): move this get/update/put atomic into a txn
+ user = util.get_user_or_die(email_of_user_to_unhide)
+ user.is_hidden = False
+ user.put()
+ logging.info("Unhide user: %s", user.email)
+ return flask.redirect('/admin/manage_users?sort_by=%s&msg=%s+unhidden'
+ % (sort_by, user.email))
+ elif flask.request.form.get("action") == "delete":
+ email_of_user_to_delete = flask.request.form.get("email")
+ user = util.get_user_or_die(email_of_user_to_delete)
+ user.key.delete()
+ logging.info("Delete user: %s", user.email)
+ return flask.redirect('/admin/manage_users?sort_by=%s&msg=%s+deleted'
+ % (sort_by, user.email))
+
+ user_q = models.User.query()
+ results = user_q.fetch(1000)
+
+ now = datetime.datetime.now()
+
+ # Tuple: (email, is-hidden, creation-time, days since last snippet)
+ user_data = []
+ for user in results:
+ # Get the last snippet for that user.
+ last_snippet = util.most_recent_snippet_for_user(user.email)
+ if last_snippet:
+ seconds_since_snippet = (now.date() -
+ last_snippet.week).total_seconds()
+ weeks_since_snippet = int(
+ seconds_since_snippet /
+ datetime.timedelta(days=7).total_seconds())
else:
- raise ValueError('Invalid sort_by value "%s"' % sort_by)
+ weeks_since_snippet = None
+ user_data.append((user.email, user.is_hidden,
+ user.created, weeks_since_snippet))
+
+ if sort_by == 'email':
+ user_data.sort(key=lambda x: x[0])
+ elif sort_by == 'creation_time':
+ user_data.sort(key=lambda x: x[0])
+ user_data.sort(key=lambda x: x[2] or datetime.datetime.min, reverse=True)
+ elif sort_by == 'last_snippet_time':
+ user_data.sort(key=lambda x: x[0])
+ user_data.sort(key=lambda x: 1000 if x[3] is None else x[3], reverse=True)
+ else:
+ raise ValueError('Invalid sort_by value "%s"' % sort_by)
- template_values = {
- 'logout_url': users.create_logout_url('/'),
- 'message': self.request.get('msg'),
- 'username': _current_user_email(),
- 'is_admin': users.is_current_user_admin(),
- 'view_week': util.existingsnippet_monday(_TODAY_FN()),
- 'user_data': user_data,
- 'sort_by': sort_by,
- }
- self.render_response('manage_users.html', template_values)
+ template_values = {
+ 'logout_url': users.create_logout_url('/'),
+ 'message': flask.request.args.get('msg'),
+ 'username': _current_user_email(),
+ 'is_admin': users.is_current_user_admin(),
+ 'view_week': util.existingsnippet_monday(now),
+ 'user_data': user_data,
+ 'sort_by': sort_by,
+ }
+ return flask.render_template('manage_users.html', **template_values)
# The following two classes are called by cron.
-def _get_email_to_current_snippet_map(today):
+def _get_email_to_current_snippet_map(today: datetime.datetime) -> dict[str, bool]:
"""Return a map from email to True if they've written snippets this week.
Goes through all users registered on the system, and checks if
@@ -686,7 +699,7 @@ def _get_email_to_current_snippet_map(today):
a map from email (user.email for each user) to True or False,
depending on if they've written snippets for this week or not.
"""
- user_q = models.User.all()
+ user_q = models.User.query()
users = user_q.fetch(1000)
retval = {}
for user in users:
@@ -695,8 +708,9 @@ def _get_email_to_current_snippet_map(today):
retval[user.email] = False # assume the worst, for now
week = util.existingsnippet_monday(today)
- snippets_q = models.Snippet.all()
- snippets_q.filter('week = ', week)
+ snippets_q = models.Snippet.query(
+ models.Snippet.week == week
+ )
snippets = snippets_q.fetch(1000)
for snippet in snippets:
if snippet.email in retval: # don't introduce new keys here
@@ -716,12 +730,11 @@ def _maybe_send_snippets_mail(to, subject, template_path, template_values):
template_values.setdefault('hostname', app_settings.hostname)
- jinja2_instance = jinja2.get_jinja2()
mail.send_mail(sender=app_settings.email_from,
to=to,
subject=subject,
- body=jinja2_instance.render_template(template_path,
- **template_values))
+ body=flask.render_template(template_path,
+ **template_values))
# Appengine has a quota of 32 emails per minute:
# https://developers.google.com/appengine/docs/quotas#Mail
# We pause 2 seconds between each email to make sure we
@@ -729,66 +742,62 @@ def _maybe_send_snippets_mail(to, subject, template_path, template_values):
time.sleep(2)
-class SendFridayReminderChat(BaseHandler):
+@app.route("/admin/send_friday_reminder_chat")
+def admin_send_friday_reminder_chat_handler() -> flask.Response:
"""Send a chat message to the configured chat room(s)."""
-
- def get(self):
- msg = 'Reminder: Weekly snippets due Monday at 5pm.'
- _send_to_chat(msg, "/")
+ msg = 'Reminder: Weekly snippets due Monday at 5pm.'
+ _send_to_chat(msg, "/")
+ return flask.make_response({"status": 200, "message": "Sent friday reminder chat"})
-class SendReminderEmail(BaseHandler):
+@app.route("/admin/send_reminder_email")
+def admin_send_reminder_email_handler() -> flask.Response:
"""Send an email to everyone who doesn't have a snippet for this week."""
- def _send_mail(self, email):
+ def _send_mail(email):
template_values = {}
_maybe_send_snippets_mail(email, 'Weekly snippets due today at 5pm',
'reminder_email.txt', template_values)
- def get(self):
- email_to_has_snippet = _get_email_to_current_snippet_map(_TODAY_FN())
- for (user_email, has_snippet) in email_to_has_snippet.iteritems():
- if not has_snippet:
- self._send_mail(user_email)
- logging.debug('sent reminder email to %s' % user_email)
- else:
- logging.debug('did not send reminder email to %s: '
- 'has a snippet already' % user_email)
+ email_to_has_snippet = _get_email_to_current_snippet_map(
+ datetime.datetime.now())
+ for user_email, has_snippet in email_to_has_snippet.items():
+ if not has_snippet:
+ _send_mail(user_email)
+ logging.debug('sent reminder email to %s', user_email)
+ else:
+ logging.debug('did not send reminder email to %s: '
+ 'has a snippet already', user_email)
- msg = 'Reminder: Weekly snippets due today at 5pm.'
- _send_to_chat(msg, "/")
+ msg = 'Reminder: Weekly snippets due today at 5pm.'
+ _send_to_chat(msg, "/")
+ return flask.make_response({"status": 200, "message": "Sent reminder emails"})
-class SendViewEmail(BaseHandler):
+@app.route("/admin/send_view_email")
+def admin_send_view_email_handler() -> flask.Response:
"""Send an email to everyone to look at the week's snippets."""
- def _send_mail(self, email, has_snippets):
+ def _send_mail(email, has_snippets):
template_values = {'has_snippets': has_snippets}
_maybe_send_snippets_mail(email, 'Weekly snippets are ready!',
'view_email.txt', template_values)
- def get(self):
- email_to_has_snippet = _get_email_to_current_snippet_map(_TODAY_FN())
- for (user_email, has_snippet) in email_to_has_snippet.iteritems():
- self._send_mail(user_email, has_snippet)
- logging.debug('sent "view" email to %s' % user_email)
-
- msg = 'Weekly snippets are ready!'
- _send_to_chat(msg, "/weekly")
-
-
-application = webapp2.WSGIApplication([
- ('/', UserPage),
- ('/weekly', SummaryPage),
- ('/update_snippet', UpdateSnippet),
- ('/settings', Settings),
- ('/update_settings', UpdateSettings),
- ('/admin/settings', AppSettings),
- ('/admin/update_settings', UpdateAppSettings),
- ('/admin/manage_users', ManageUsers),
- ('/admin/send_friday_reminder_chat', SendFridayReminderChat),
- ('/admin/send_reminder_email', SendReminderEmail),
- ('/admin/send_view_email', SendViewEmail),
- ('/slack', slacklib.SlashCommand),
- ],
- debug=True)
+ email_to_has_snippet = _get_email_to_current_snippet_map(
+ datetime.datetime.now())
+ for user_email, has_snippet in email_to_has_snippet.items():
+ _send_mail(user_email, has_snippet)
+ logging.debug('sent "view" email to %s', user_email)
+
+ msg = 'Weekly snippets are ready!'
+ _send_to_chat(msg, "/weekly")
+ return flask.make_response({"status": 200, "message": "Sent 'snippets are ready' emails"})
+
+
+@app.route('/_ah/warmup')
+def warmup():
+ """App engine warmup requests handler
+
+ See https://cloud.google.com/appengine/docs/standard/configuring-warmup-requests?tab=python
+ """
+ return "OK", 200, {}
diff --git a/snippets_test.py b/snippets_test.py
index 19d9e05..9ad9d4e 100755
--- a/snippets_test.py
+++ b/snippets_test.py
@@ -1,57 +1,44 @@
#!/usr/bin/env python
+# -*- coding: utf-8 -*-
"""Tests for the snippets server.
This tests the functionality found at weekly-snippets.appspot.com.
c.f. http://code.google.com/appengine/docs/python/tools/localunittesting.html
-c.f. http://webtest.pythonpaste.org/en/latest/index.html
+ https://flask.palletsprojects.com/en/2.2.x/testing/
+ https://docs.pytest.org/en/7.1.x/how-to/unittest.html
+ https://github.com/adamchainz/time-machine
"""
__author__ = 'Craig Silverstein '
-
import datetime
-import os
import re
-import sys
import time
-try: # Work under either python2.5 or python2.7
- import unittest2 as unittest
-except ImportError:
- import unittest
-
-# Update sys.path so it can find these. We just need to add
-# 'google_appengine', but we add all of $PATH to be easy. This
-# assumes the google_appengine directory is on the path.
-sys.path.extend(os.environ['PATH'].split(':'))
-import dev_appserver
-dev_appserver.fix_sys_path()
-
-from google.appengine.datastore import datastore_stub_util
-from google.appengine.ext import db
+import unittest
+from typing import AnyStr, Pattern
+import warnings
+from zoneinfo import ZoneInfo
+
+from google.cloud import ndb
from google.appengine.ext import testbed
-import webtest # may need to do 'pip install webtest'
+import time_machine
import models
import slacklib
import snippets
-_TEST_TODAY = datetime.datetime(2012, 2, 23)
-
-
+# Today's pretend date is Thursday, Feb 23, 2012
+@time_machine.travel(datetime.datetime(2012, 2, 23, tzinfo=ZoneInfo("UTC")))
class SnippetsTestBase(unittest.TestCase):
def setUp(self):
- # We're not interested in testing consistency stuff in these tests.
self.testbed = testbed.Testbed()
self.testbed.activate()
- policy = datastore_stub_util.PseudoRandomHRConsistencyPolicy(
- probability=1)
- self.testbed.init_datastore_v3_stub(consistency_policy=policy)
self.testbed.init_user_stub()
- self.request_fetcher = webtest.TestApp(snippets.application)
- snippets._TODAY_FN = lambda: _TEST_TODAY
+
+ self.request_fetcher = snippets.app.test_client()
# Make sure we never accidentally send messages to chat.
self.old_send_to_slack_channel = slacklib.send_to_slack_channel
@@ -64,9 +51,12 @@ def tearDown(self):
slacklib.send_to_slack_channel = self.old_send_to_slack_channel
def login(self, email):
- self.testbed.setup_env(user_email=email, overwrite=True)
- self.testbed.setup_env(user_id=email, overwrite=True)
- self.testbed.setup_env(user_is_admin='0', overwrite=True)
+ self.testbed.setup_env(
+ user_email=email,
+ user_id=email,
+ user_is_admin='0',
+ overwrite=True
+ )
# Now make sure there are global settings.
settings = models.AppSettings.get(
create_if_missing=True,
@@ -78,6 +68,18 @@ def login(self, email):
def set_is_admin(self):
self.testbed.setup_env(user_is_admin='1', overwrite=True)
+ def assertDictContainsSubset(self, subset, dictionary, msg=None):
+ """Checks whether dictionary is a superset of subset.
+
+ Suppresses the deprecation warning from upstream.
+ """
+ with warnings.catch_warnings():
+ warnings.filterwarnings("ignore", category=DeprecationWarning)
+ # If python upstream removes this function entirely, you can substitute
+ # this in a pinch:
+ # self.assertLessEqual(subset.items(), dictionary.items(), msg)
+ super().assertDictContainsSubset(subset, dictionary, msg)
+
def assertNumSnippets(self, body, expected_count):
"""Assert the page 'body' has exactly expected_count snippets in it."""
# We annotate the div at the beginning of each snippet with
@@ -118,26 +120,42 @@ def assertNotInSnippet(self, text, body, snippet_number):
"""For snippet-page 'body', assert 'text' is not in the ith snippet."""
self.assertNotIn(text, self._ith_snippet(body, snippet_number))
+ def assertSnippetRegex(self, regex: AnyStr | Pattern[AnyStr], body, snippet_number):
+ """For snippet-page 'body', assert 'regex' matches the i-th snippet.
+
+ This works for both user-pages and weekly-pages -- we figure out the
+ boundaries of the snippets, and check whether the regex matches the
+ given snippet.
+
+ Arguments:
+ regex: regular expression to match against the snippet
+ body: the full html page
+ snippet_number: which snippet on the page to examine.
+ Index starts at 0.
+ """
+ self.assertRegex(self._ith_snippet(body, snippet_number), regex)
+
class UserTestBase(SnippetsTestBase):
"""The most common base: someone who is logged in as user@example.com."""
def setUp(self):
- super(UserTestBase, self).setUp()
+ super().setUp()
self.login('user@example.com')
class PostTestCase(SnippetsTestBase):
"""test the correct output from the server when POSTing"""
- def testPostSnippet(self):
+ def testPostSnippetNormal(self):
self.login('user@example.com')
url = '/update_snippet'
params = {
'week': '02-20-2012',
'snippet': 'my inspired snippet'
}
- response = self.request_fetcher.post(url, params, status=200)
- self.assertIn('{"status": 200, "message": "ok"}', response)
+ response = self.request_fetcher.post(url, data=params)
+ self.assertEqual(response.status_code, 200)
+ self.assertDictContainsSubset({"status": 200, "message": "ok"}, response.json)
def testPostSnippetAsOtherPerson(self):
self.login('user@example.com')
@@ -147,9 +165,10 @@ def testPostSnippetAsOtherPerson(self):
'snippet': 'my fallacious snippet',
'u': 'joeuser@example.com'
}
- response = self.request_fetcher.post(url, params, status=403)
- self.assertIn('"status": 403', response)
- self.assertIn('joeuser@example.com', response)
+ response = self.request_fetcher.post(url, data=params)
+ self.assertEqual(response.status_code, 403)
+ self.assertDictContainsSubset({"status": 403}, response.json)
+ self.assertIn('joeuser@example.com', response.text)
def testPostSnippetNotLoggedIn(self):
url = '/update_snippet'
@@ -158,9 +177,10 @@ def testPostSnippetNotLoggedIn(self):
'snippet': 'my fallacious snippet',
'u': 'user@example.com'
}
- response = self.request_fetcher.post(url, params, status=403)
- self.assertIn('"status": 403', response)
- self.assertIn('"message": "not logged in"', response)
+ response = self.request_fetcher.post(url, data=params)
+ self.assertEqual(response.status_code, 401)
+ self.assertDictContainsSubset({"status": 401}, response.json)
+ self.assertDictContainsSubset({"message": "not logged in"}, response.json)
def testPostSnippetIsolation(self):
# updating a single snippet via POST should not affect other snippets
@@ -180,14 +200,15 @@ def testPostSnippetIsolation(self):
'week': '02-27-2012',
'snippet': 'updated second snippet'
}
- self.request_fetcher.post(url, params, status=200)
+ response = self.request_fetcher.post(url, data=params)
+ self.assertEqual(response.status_code, 200)
# make sure only the second snippet changed
response = self.request_fetcher.get('/')
- self.assertInSnippet('>my third snippet<', response.body, 0)
- self.assertInSnippet('>updated second snippet<', response.body, 1)
- self.assertNotInSnippet('>my second snippet<', response.body, 1)
- self.assertInSnippet('>my snippet<', response.body, 2)
+ self.assertInSnippet('>my third snippet<', response.text, 0)
+ self.assertInSnippet('>updated second snippet<', response.text, 1)
+ self.assertNotInSnippet('>my second snippet<', response.text, 1)
+ self.assertInSnippet('>my snippet<', response.text, 2)
class LoginRequiredTestCase(SnippetsTestBase):
@@ -261,16 +282,19 @@ def testCanViewWeeklyPage(self):
def testCannotEditOtherSnippets(self):
url = ('/update_snippet?week=02-20-2012&snippet=my+snippet'
'&u=notuser@example.com')
- # Raises an error if we don't get a 500 response (meaning no perm).
- self.request_fetcher.get(url, status=500)
+ # Raises an error if we don't get a 403 response (meaning no perm).
+ response = self.request_fetcher.get(url)
+ self.assertEqual(response.status_code, 403)
def testCannotViewOtherSettings(self):
url = '/settings?u=notuser@example.com'
- self.request_fetcher.get(url, status=500)
+ response = self.request_fetcher.get(url)
+ self.assertEqual(response.status_code, 403)
def testCannotEditOtherSettings(self):
url = '/update_settings?u=notuser@example.com'
- self.request_fetcher.get(url, status=500)
+ response = self.request_fetcher.get(url)
+ self.assertEqual(response.status_code, 403)
def testCanEditOwnSnippets(self):
url = ('/update_snippet?week=02-20-2012&snippet=my+snippet'
@@ -307,65 +331,64 @@ class NewUserTestCase(UserTestBase):
def testNewUserLogin(self):
response = self.request_fetcher.get('/')
- self.assertIn('New user', response.body)
+ self.assertIn('New user', response.text)
def testNewUserContinueUrl(self):
"""After verifying settings, we should go back to snippet-entry."""
response = self.request_fetcher.get('/')
- m = re.search(r'', response.body)
+ m = re.search(r'', response.text)
continue_url = m.group(1)
settings_response = self.request_fetcher.get(continue_url)
self.assertIn('name="redirect_to" value="snippet_entry"',
- settings_response.body)
+ settings_response.text)
# Now kinda-simulate clicking on the submit button.
done_response = self.request_fetcher.get(
- '/update_settings?u=user@example.com&redirect_to=snippet_entry')
- if done_response.status_int in (301, 302, 303, 304):
- done_response = done_response.follow()
- self.assertIn('Snippets for user@example.com', done_response.body)
+ '/update_settings?u=user@example.com&redirect_to=snippet_entry',
+ follow_redirects=True
+ )
+ self.assertIn('Snippets for user@example.com', done_response.text)
def testNewAdminWithNoAppSettings(self):
"""The first time someone logs in, we should go to app settings."""
self.set_is_admin()
app_settings = models.AppSettings.get()
- app_settings.delete()
+ app_settings.key.delete()
- response = self.request_fetcher.get('/')
- if response.status_int in (301, 302, 303, 304):
- response = response.follow()
- self.assertIn('Application settings', response.body)
+ response = self.request_fetcher.get('/', follow_redirects=True)
+ self.assertIn('Application settings', response.text)
def testNewAdminContinueUrls(self):
"""We should go from app settings to user settings to snippet."""
self.set_is_admin()
app_settings = models.AppSettings.get()
- app_settings.delete()
+ app_settings.key.delete()
- response = self.request_fetcher.get('/')
- if response.status_int in (301, 302, 303, 304):
- response = response.follow()
- m = re.search(r'name="redirect_to" value="([^"]*)"', response.body)
+ response = self.request_fetcher.get('/', follow_redirects=True)
+ self.assertEqual(response.status_code, 200)
+ m = re.search(r'name="redirect_to" value="([^"]*)"', response.text)
continue_url = m.group(1)
# Now kinda-simulate clicking on the submit button.
- done_response = self.request_fetcher.get(
- '/admin/update_settings?domains=example.com&redirect_to=%s'
- % continue_url)
- if done_response.status_int in (301, 302, 303, 304):
- done_response = done_response.follow()
- self.assertIn('User settings', done_response.body)
+ done_response = self.request_fetcher.post(
+ '/admin/update_settings',
+ data={"domains": "example.com",
+ "hostname": "snippets.example.com",
+ "redirect_to": continue_url},
+ follow_redirects=True
+ )
+ self.assertIn('User settings', done_response.text)
self.assertIn('name="redirect_to" value="snippet_entry"',
- done_response.body)
+ done_response.text)
def testNewUserWithNoAppSettings(self):
"""For non-admins, we should not offer the app-settings page."""
app_settings = models.AppSettings.get()
- app_settings.delete()
+ app_settings.key.delete()
response = self.request_fetcher.get('/')
- self.assertIn('New user', response.body)
+ self.assertIn('New user', response.text)
def testNewUserInheritsAppDefaults(self):
app_settings = models.AppSettings.get()
@@ -374,14 +397,14 @@ def testNewUserInheritsAppDefaults(self):
app_settings.put()
response = self.request_fetcher.get('/')
- m = re.search(r'', response.body)
+ m = re.search(r'', response.text)
continue_url = m.group(1)
settings_response = self.request_fetcher.get(continue_url)
- self.assertRegexpMatches(settings_response.body,
- r'name="markdown"\s+value="yes"\s+checked')
- self.assertRegexpMatches(settings_response.body,
- r'name="private"\s+value="yes"\s+checked')
+ self.assertRegex(settings_response.text,
+ r'name="markdown"\s+value="yes"\s+checked')
+ self.assertRegex(settings_response.text,
+ r'name="private"\s+value="yes"\s+checked')
# Now change the app-defaults and make sure this is reflected in
# the new-user setup page.
@@ -390,50 +413,55 @@ def testNewUserInheritsAppDefaults(self):
app_settings.put()
response = self.request_fetcher.get('/')
- m = re.search(r'', response.body)
+ m = re.search(r'', response.text)
continue_url = m.group(1)
settings_response = self.request_fetcher.get(continue_url)
- self.assertRegexpMatches(settings_response.body,
- r'name="markdown"\s+value="no"\s+checked')
- self.assertRegexpMatches(settings_response.body,
- r'name="private"\s+value="no"\s+checked')
+ self.assertRegex(settings_response.text,
+ r'name="markdown"\s+value="no"\s+checked')
+ self.assertRegex(settings_response.text,
+ r'name="private"\s+value="no"\s+checked')
def testNewUserInValidDomain(self):
self.login('newuser@example.com')
response = self.request_fetcher.get('/settings')
- self.assertIn('User settings', response.body)
+ self.assertIn('User settings', response.text)
def testNewUserInInvalidDomain(self):
"""Test you can not register as a new user from a random domain."""
self.login('newuser@notallowed.com')
# TODO(csilvers): give a nice error page instead of a 500.
- self.request_fetcher.get('/settings', status=500)
+ response = self.request_fetcher.get('/settings')
+ self.assertEqual(response.status_code, 500)
def testSettingsPageDoesNotCreateANewUser(self):
"""Only *saving* the settings should create a new user."""
response = self.request_fetcher.get('/')
- self.assertIn('New user', response.body)
- m = re.search(r'', response.body)
+ self.assertIn('New user', response.text)
+ m = re.search(r'', response.text)
continue_url = m.group(1)
self.request_fetcher.get(continue_url) # visit /settings
# Now if we go to / again, we should get the new-user page again
# because the settings were never saved.
response = self.request_fetcher.get('/')
- self.assertIn('New user', response.body)
+ self.assertIn('New user', response.text)
class AppSettingsTestCase(UserTestBase):
"""Test the app-settings page."""
def setUp(self):
- super(AppSettingsTestCase, self).setUp()
+ super().setUp()
self.set_is_admin()
def testDomainsParsing(self):
- self.request_fetcher.get(
- '/admin/update_settings?domains=a.com,b.com++c.com%0Bd.com,%0B')
+ self.request_fetcher.post(
+ '/admin/update_settings',
+ data={
+ "domains": "a.com,b.com\nc.com d.com,",
+ "hostname": "example.com",
+ })
app_settings = models.AppSettings.get()
self.assertEqual(['a.com', 'b.com', 'c.com', 'd.com'],
app_settings.domains)
@@ -443,12 +471,12 @@ class UserSettingsTestCase(UserTestBase):
"""Test setting and using user settings."""
def assertInputIsChecked(self, name, body, snippet_number):
- self.assertRegexpMatches(body, r'name="%s" value="True"\s+checked\s*>'
- % name, snippet_number)
+ self.assertRegex(body, r'name="%s" value="True"\s+checked\s*>' % name,
+ snippet_number)
def assertInputIsNotChecked(self, name, body, snippet_number):
- self.assertRegexpMatches(body, r'name="%s" value="True"\s*>' % name,
- snippet_number)
+ self.assertRegex(body, r'name="%s" value="True"\s*>' % name,
+ snippet_number)
def testDefaultUserSettings(self):
# Make sure the rest of the tests are actually testing
@@ -457,61 +485,61 @@ def testDefaultUserSettings(self):
response = self.request_fetcher.get('/')
# Neither private nor 'is-markdown' are checked by default.
- self.assertInputIsNotChecked('private', response.body, 0)
- self.assertInputIsNotChecked('is_markdown', response.body, 0)
+ self.assertInputIsNotChecked('private', response.text, 0)
+ self.assertInputIsNotChecked('is_markdown', response.text, 0)
def testPrivateUser(self):
self.request_fetcher.get(
'/update_settings?u=user@example.com&private=yes')
response = self.request_fetcher.get('/')
- self.assertInputIsChecked('private', response.body, 0)
- self.assertInputIsNotChecked('is_markdown', response.body, 0)
+ self.assertInputIsChecked('private', response.text, 0)
+ self.assertInputIsNotChecked('is_markdown', response.text, 0)
def testMarkdownUser(self):
self.request_fetcher.get(
'/update_settings?u=user@example.com&markdown=yes')
response = self.request_fetcher.get('/')
- self.assertInputIsNotChecked('private', response.body, 0)
- self.assertInputIsChecked('is_markdown', response.body, 0)
+ self.assertInputIsNotChecked('private', response.text, 0)
+ self.assertInputIsChecked('is_markdown', response.text, 0)
def testSettingsForFilledInSnippets(self):
url = '/update_snippet?week=02-21-2011&snippet=old+snippet'
self.request_fetcher.get(url)
response = self.request_fetcher.get('/')
- self.assertNumSnippets(response.body, 53)
- self.assertInputIsNotChecked('private', response.body, 9)
- self.assertInputIsNotChecked('is_markdown', response.body, 9)
- self.assertInSnippet('old snippet', response.body, 52)
- self.assertInputIsNotChecked('private', response.body, 52)
- self.assertInputIsNotChecked('is_markdown', response.body, 52)
+ self.assertNumSnippets(response.text, 53)
+ self.assertInputIsNotChecked('private', response.text, 9)
+ self.assertInputIsNotChecked('is_markdown', response.text, 9)
+ self.assertInSnippet('old snippet', response.text, 52)
+ self.assertInputIsNotChecked('private', response.text, 52)
+ self.assertInputIsNotChecked('is_markdown', response.text, 52)
self.request_fetcher.get(
'/update_settings?u=user@example.com&markdown=yes')
response = self.request_fetcher.get('/')
- self.assertNumSnippets(response.body, 53)
- self.assertInputIsNotChecked('private', response.body, 9)
- self.assertInputIsChecked('is_markdown', response.body, 9)
+ self.assertNumSnippets(response.text, 53)
+ self.assertInputIsNotChecked('private', response.text, 9)
+ self.assertInputIsChecked('is_markdown', response.text, 9)
# But the existing snippet is unaffected.
- self.assertInSnippet('old snippet', response.body, 52)
- self.assertInputIsNotChecked('private', response.body, 52)
- self.assertInputIsNotChecked('is_markdown', response.body, 52)
+ self.assertInSnippet('old snippet', response.text, 52)
+ self.assertInputIsNotChecked('private', response.text, 52)
+ self.assertInputIsNotChecked('is_markdown', response.text, 52)
self.request_fetcher.get(
'/update_settings?u=user@example.com&private=yes')
response = self.request_fetcher.get('/')
- self.assertNumSnippets(response.body, 53)
- self.assertInputIsChecked('private', response.body, 9)
- self.assertInputIsNotChecked('is_markdown', response.body, 9)
- self.assertInSnippet('old snippet', response.body, 52)
- self.assertInputIsNotChecked('private', response.body, 52)
- self.assertInputIsNotChecked('is_markdown', response.body, 52)
+ self.assertNumSnippets(response.text, 53)
+ self.assertInputIsChecked('private', response.text, 9)
+ self.assertInputIsNotChecked('is_markdown', response.text, 9)
+ self.assertInSnippet('old snippet', response.text, 52)
+ self.assertInputIsNotChecked('private', response.text, 52)
+ self.assertInputIsNotChecked('is_markdown', response.text, 52)
def testCategoryUnset(self):
self.request_fetcher.get('/update_settings?u=user@example.com')
response = self.request_fetcher.get('/')
self.assertInSnippet(
'WARNING: Snippet will go in the "(unknown)"',
- response.body, 0
+ response.text, 0
)
def testCategorySet(self):
@@ -521,7 +549,7 @@ def testCategorySet(self):
response = self.request_fetcher.get('/')
self.assertNotInSnippet(
'WARNING: Snippet will go in the "(unknown)"',
- response.body, 0
+ response.text, 0
)
def testCategoryUnsetButSnippetHasContent(self):
@@ -531,22 +559,22 @@ def testCategoryUnsetButSnippetHasContent(self):
response = self.request_fetcher.get('/')
self.assertInSnippet(
'WARNING: Snippet will go in the "(unknown)"',
- response.body, 0
+ response.text, 0
)
def testCategoryCheckForFilledInSnippets(self):
url = '/update_snippet?week=02-21-2011&snippet=old+snippet'
self.request_fetcher.get(url)
response = self.request_fetcher.get('/')
- self.assertNumSnippets(response.body, 53)
+ self.assertNumSnippets(response.text, 53)
self.assertInSnippet(
'WARNING: Snippet will go in the "(unknown)"',
- response.body, 9
+ response.text, 9
)
- self.assertInSnippet('old snippet', response.body, 52)
+ self.assertInSnippet('old snippet', response.text, 52)
self.assertInSnippet(
'WARNING: Snippet will go in the "(unknown)"',
- response.body, 52
+ response.text, 52
)
def testHiddenUser(self):
@@ -554,20 +582,20 @@ def testHiddenUser(self):
self.request_fetcher.get(url)
response = self.request_fetcher.get('/')
- self.assertNumSnippets(response.body, 2)
+ self.assertNumSnippets(response.text, 2)
response = self.request_fetcher.get('/weekly?week=02-27-2012')
- self.assertNumSnippets(response.body, 1) # "no snippet this week"
+ self.assertNumSnippets(response.text, 1) # "no snippet this week"
url = '/update_settings?u=user@example.com&is_hidden=yes'
self.request_fetcher.get(url)
response = self.request_fetcher.get('/')
# Hiding doesn't affect the user-snippets page, just the weekly one.
- self.assertNumSnippets(response.body, 2)
+ self.assertNumSnippets(response.text, 2)
response = self.request_fetcher.get('/weekly?week=02-27-2012')
- self.assertNumSnippets(response.body, 0)
+ self.assertNumSnippets(response.text, 0)
# And it doesn't affect existing snippets, just empty ones.
response = self.request_fetcher.get('/weekly?week=02-20-2012')
- self.assertNumSnippets(response.body, 1)
+ self.assertNumSnippets(response.text, 1)
def testNewSnippetUnhides(self):
url = '/update_snippet?week=02-13-2012&snippet=my+snippet'
@@ -576,15 +604,15 @@ def testNewSnippetUnhides(self):
url = '/update_settings?u=user@example.com&is_hidden=yes'
self.request_fetcher.get(url)
response = self.request_fetcher.get('/weekly?week=02-20-2012')
- self.assertNumSnippets(response.body, 0)
+ self.assertNumSnippets(response.text, 0)
url = '/update_snippet?week=02-27-2012&snippet=new+snippet'
self.request_fetcher.get(url)
response = self.request_fetcher.get('/weekly?week=02-20-2012')
- self.assertNumSnippets(response.body, 1)
+ self.assertNumSnippets(response.text, 1)
response = self.request_fetcher.get('/weekly?week=02-27-2012')
- self.assertNumSnippets(response.body, 1)
+ self.assertNumSnippets(response.text, 1)
def testChangingSettingsUnhides(self):
url = '/update_snippet?week=02-20-2012&snippet=my+snippet'
@@ -593,12 +621,12 @@ def testChangingSettingsUnhides(self):
url = '/update_settings?u=user@example.com&is_hidden=yes'
self.request_fetcher.get(url)
response = self.request_fetcher.get('/weekly?week=02-27-2012')
- self.assertNumSnippets(response.body, 0)
+ self.assertNumSnippets(response.text, 0)
url = '/update_settings?u=user@example.com'
self.request_fetcher.get(url)
response = self.request_fetcher.get('/weekly?week=02-27-2012')
- self.assertNumSnippets(response.body, 1)
+ self.assertNumSnippets(response.text, 1)
def testHideButton(self):
# First, register the user.
@@ -609,14 +637,14 @@ def testHideButton(self):
self.request_fetcher.get(url)
response = self.request_fetcher.get('/weekly?week=02-20-2012')
- self.assertNumSnippets(response.body, 1)
+ self.assertNumSnippets(response.text, 1)
# Now hide using the hide button.
url = '/update_settings?u=user@example.com&hide=Hide'
self.request_fetcher.get(url)
response = self.request_fetcher.get('/weekly?week=02-20-2012')
- self.assertNumSnippets(response.body, 0)
+ self.assertNumSnippets(response.text, 0)
def testDeleteButton(self):
# First, register the user.
@@ -624,13 +652,13 @@ def testDeleteButton(self):
self.request_fetcher.get(url)
response = self.request_fetcher.get('/')
- self.assertNotIn('New user', response.body)
+ self.assertNotIn('New user', response.text)
url = '/update_settings?u=user@example.com&delete=Delete'
self.request_fetcher.get(url)
response = self.request_fetcher.get('/')
- self.assertIn('New user', response.body)
+ self.assertIn('New user', response.text)
class SetAndViewSnippetsTestCase(UserTestBase):
@@ -640,22 +668,22 @@ def testSetAndViewInUserMode(self):
url = '/update_snippet?week=02-20-2012&snippet=my+snippet'
self.request_fetcher.get(url)
response = self.request_fetcher.get('/')
- self.assertNumSnippets(response.body, 2)
- self.assertInSnippet('>my snippet<', response.body, 0)
+ self.assertNumSnippets(response.text, 2)
+ self.assertInSnippet('>my snippet<', response.text, 0)
def testSetAndViewInWeeklyMode(self):
url = '/update_snippet?week=02-20-2012&snippet=my+snippet'
self.request_fetcher.get(url)
response = self.request_fetcher.get('/weekly?week=02-20-2012')
- self.assertNumSnippets(response.body, 1)
- self.assertInSnippet('>my snippet<', response.body, 0)
+ self.assertNumSnippets(response.text, 1)
+ self.assertInSnippet('>my snippet<', response.text, 0)
def testCannotSeeInOtherWeek(self):
url = '/update_snippet?week=02-20-2012&snippet=my+snippet'
self.request_fetcher.get(url)
response = self.request_fetcher.get('/weekly?week=02-13-2012')
- self.assertNumSnippets(response.body, 1)
- self.assertNotIn('>my snippet<', response.body)
+ self.assertNumSnippets(response.text, 1)
+ self.assertNotIn('>my snippet<', response.text)
def testViewSnippetsForTwoUsers(self):
url = '/update_snippet?week=02-20-2012&snippet=my+snippet'
@@ -666,32 +694,32 @@ def testViewSnippetsForTwoUsers(self):
# This is done as other
response = self.request_fetcher.get('/')
- self.assertIn('other@example.com', response.body)
- self.assertNumSnippets(response.body, 2)
- self.assertInSnippet('>other snippet<', response.body, 0)
- self.assertNotIn('user@example.com', response.body)
- self.assertNotIn('>my snippet<', response.body)
+ self.assertIn('other@example.com', response.text)
+ self.assertNumSnippets(response.text, 2)
+ self.assertInSnippet('>other snippet<', response.text, 0)
+ self.assertNotIn('user@example.com', response.text)
+ self.assertNotIn('>my snippet<', response.text)
response = self.request_fetcher.get('/weekly?week=02-20-2012')
- self.assertNumSnippets(response.body, 2)
- self.assertInSnippet('other@example.com', response.body, 0)
- self.assertInSnippet('>other snippet<', response.body, 0)
- self.assertInSnippet('user@example.com', response.body, 1)
- self.assertInSnippet('>my snippet<', response.body, 1)
+ self.assertNumSnippets(response.text, 2)
+ self.assertInSnippet('other@example.com', response.text, 0)
+ self.assertInSnippet('>other snippet<', response.text, 0)
+ self.assertInSnippet('user@example.com', response.text, 1)
+ self.assertInSnippet('>my snippet<', response.text, 1)
# This is done as user
self.login('user@example.com')
response = self.request_fetcher.get('/')
- self.assertIn('user@example.com', response.body)
- self.assertNumSnippets(response.body, 2)
- self.assertInSnippet('>my snippet<', response.body, 0)
- self.assertNotIn('other@example.com', response.body)
- self.assertNotIn('>other snippet<', response.body)
+ self.assertIn('user@example.com', response.text)
+ self.assertNumSnippets(response.text, 2)
+ self.assertInSnippet('>my snippet<', response.text, 0)
+ self.assertNotIn('other@example.com', response.text)
+ self.assertNotIn('>other snippet<', response.text)
response = self.request_fetcher.get('/weekly?week=02-20-2012')
- self.assertNumSnippets(response.body, 2)
- self.assertInSnippet('other@example.com', response.body, 0)
- self.assertInSnippet('>other snippet<', response.body, 0)
- self.assertInSnippet('user@example.com', response.body, 1)
- self.assertInSnippet('>my snippet<', response.body, 1)
+ self.assertNumSnippets(response.text, 2)
+ self.assertInSnippet('other@example.com', response.text, 0)
+ self.assertInSnippet('>other snippet<', response.text, 0)
+ self.assertInSnippet('user@example.com', response.text, 1)
+ self.assertInSnippet('>my snippet<', response.text, 1)
def testViewSnippetsForTwoWeeks(self):
url = '/update_snippet?week=02-20-2012&snippet=my+snippet'
@@ -700,27 +728,27 @@ def testViewSnippetsForTwoWeeks(self):
self.request_fetcher.get(url)
response = self.request_fetcher.get('/')
- self.assertNumSnippets(response.body, 3)
+ self.assertNumSnippets(response.text, 3)
# Snippets go in reverse chronological order (i.e. newest first)
- self.assertInSnippet('>my second snippet<', response.body, 0)
- self.assertInSnippet('>my snippet<', response.body, 1)
+ self.assertInSnippet('>my second snippet<', response.text, 0)
+ self.assertInSnippet('>my snippet<', response.text, 1)
response = self.request_fetcher.get('/weekly?week=02-20-2012')
- self.assertNumSnippets(response.body, 1)
- self.assertInSnippet('user@example.com', response.body, 0)
- self.assertInSnippet('>my snippet<', response.body, 0)
- self.assertNotIn('>my second snippet<', response.body)
+ self.assertNumSnippets(response.text, 1)
+ self.assertInSnippet('user@example.com', response.text, 0)
+ self.assertInSnippet('>my snippet<', response.text, 0)
+ self.assertNotIn('>my second snippet<', response.text)
response = self.request_fetcher.get('/weekly?week=02-27-2012')
- self.assertNumSnippets(response.body, 1)
- self.assertInSnippet('user@example.com', response.body, 0)
- self.assertInSnippet('>my second snippet<', response.body, 0)
- self.assertNotIn('>my snippet<', response.body)
+ self.assertNumSnippets(response.text, 1)
+ self.assertInSnippet('user@example.com', response.text, 0)
+ self.assertInSnippet('>my second snippet<', response.text, 0)
+ self.assertNotIn('>my snippet<', response.text)
response = self.request_fetcher.get('/weekly?week=02-13-2012')
- self.assertNumSnippets(response.body, 1)
- self.assertNotIn('>my snippet<', response.body)
- self.assertNotIn('>my second snippet<', response.body)
+ self.assertNumSnippets(response.text, 1)
+ self.assertNotIn('>my snippet<', response.text)
+ self.assertNotIn('>my second snippet<', response.text)
def testViewEmptySnippetsInWeekMode(self):
url = '/update_snippet?week=02-20-2012&snippet=my+snippet'
@@ -730,17 +758,17 @@ def testViewEmptySnippetsInWeekMode(self):
self.request_fetcher.get(url)
response = self.request_fetcher.get('/weekly?week=02-20-2012')
- self.assertNumSnippets(response.body, 2)
+ self.assertNumSnippets(response.text, 2)
# Other-user comes first alphabetically.
- self.assertInSnippet('other@example.com', response.body, 0)
- self.assertInSnippet('user@example.com', response.body, 1)
- self.assertInSnippet('>my snippet<', response.body, 1)
+ self.assertInSnippet('other@example.com', response.text, 0)
+ self.assertInSnippet('user@example.com', response.text, 1)
+ self.assertInSnippet('>my snippet<', response.text, 1)
response = self.request_fetcher.get('/weekly?week=02-27-2012')
- self.assertNumSnippets(response.body, 2)
- self.assertInSnippet('other@example.com', response.body, 0)
- self.assertInSnippet('>other snippet<', response.body, 0)
- self.assertInSnippet('user@example.com', response.body, 1)
+ self.assertNumSnippets(response.text, 2)
+ self.assertInSnippet('other@example.com', response.text, 0)
+ self.assertInSnippet('>other snippet<', response.text, 0)
+ self.assertInSnippet('user@example.com', response.text, 1)
def testViewEmptySnippetsInUserMode(self):
"""Occurs when there's a gap between two snippets."""
@@ -750,9 +778,9 @@ def testViewEmptySnippetsInUserMode(self):
self.request_fetcher.get(url)
response = self.request_fetcher.get('/')
- self.assertNumSnippets(response.body, 3)
- self.assertInSnippet('>my snippet<', response.body, 0)
- self.assertInSnippet('>my old snippet<', response.body, 2)
+ self.assertNumSnippets(response.text, 3)
+ self.assertInSnippet('>my snippet<', response.text, 0)
+ self.assertInSnippet('>my old snippet<', response.text, 2)
def testCategorizeSnippets(self):
"""Weekly view should sort based on user categories."""
@@ -782,11 +810,11 @@ def testCategorizeSnippets(self):
# Order should be 4 ((unknown) category), 2 and 3 (a 1st) and
# then 1 (b 2nd).
response = self.request_fetcher.get('/weekly?week=02-20-2012')
- self.assertNumSnippets(response.body, 4)
- self.assertInSnippet('4@example.com', response.body, 0)
- self.assertInSnippet('2@example.com', response.body, 1)
- self.assertInSnippet('3@example.com', response.body, 2)
- self.assertInSnippet('1@example.com', response.body, 3)
+ self.assertNumSnippets(response.text, 4)
+ self.assertInSnippet('4@example.com', response.text, 0)
+ self.assertInSnippet('2@example.com', response.text, 1)
+ self.assertInSnippet('3@example.com', response.text, 2)
+ self.assertInSnippet('1@example.com', response.text, 3)
def testViewSnippetAfterAUserIsDeleted(self):
"""When a user is deleted, their snippet should still show up."""
@@ -797,146 +825,146 @@ def testViewSnippetAfterAUserIsDeleted(self):
self.request_fetcher.get(url)
# Now delete user 2
- u = models.User.all().filter('email =', '2@example.com').get()
- u.delete()
+ u = models.User.query(models.User.email == '2@example.com').get()
+ u.key.delete()
response = self.request_fetcher.get('/weekly?week=02-20-2012')
- self.assertNumSnippets(response.body, 1)
- self.assertInSnippet('2@example.com', response.body, 0)
- self.assertTrue('(unknown)' in response.body)
+ self.assertNumSnippets(response.text, 1)
+ self.assertInSnippet('2@example.com', response.text, 0)
+ self.assertTrue('(unknown)' in response.text)
def testWarningsWhenDue(self):
url = '/update_snippet?week=02-06-2012&snippet=old+snippet'
self.request_fetcher.get(url)
- snippets._TODAY_FN = lambda: datetime.datetime(2012, 2, 19)
- response = self.request_fetcher.get('/')
- self.assertNotIn('Due today', response.body)
- self.assertNotIn('OVERDUE', response.body)
+ with time_machine.travel(datetime.datetime(2012, 2, 19)):
+ response = self.request_fetcher.get('/')
+ self.assertNotIn('Due today', response.text)
+ self.assertNotIn('OVERDUE', response.text)
- snippets._TODAY_FN = lambda: datetime.datetime(2012, 2, 20)
- response = self.request_fetcher.get('/')
- self.assertIn('Due today', response.body)
- self.assertNotIn('OVERDUE', response.body)
+ with time_machine.travel(datetime.datetime(2012, 2, 20)):
+ response = self.request_fetcher.get('/')
+ self.assertIn('Due today', response.text)
+ self.assertNotIn('OVERDUE', response.text)
- snippets._TODAY_FN = lambda: datetime.datetime(2012, 2, 21)
- response = self.request_fetcher.get('/')
- self.assertNotIn('Due today', response.body)
- self.assertIn('OVERDUE', response.body)
+ with time_machine.travel(datetime.datetime(2012, 2, 21)):
+ response = self.request_fetcher.get('/')
+ self.assertNotIn('Due today', response.text)
+ self.assertIn('OVERDUE', response.text)
- snippets._TODAY_FN = lambda: datetime.datetime(2012, 2, 22)
- response = self.request_fetcher.get('/')
- self.assertNotIn('Due today', response.body)
- self.assertIn('OVERDUE', response.body)
+ with time_machine.travel(datetime.datetime(2012, 2, 22)):
+ response = self.request_fetcher.get('/')
+ self.assertNotIn('Due today', response.text)
+ self.assertIn('OVERDUE', response.text)
def testWarningsWhenNotDue(self):
url = '/update_snippet?week=02-13-2012&snippet=my+snippet'
self.request_fetcher.get(url)
for date in (19, 20, 21, 22):
- snippets._TODAY_FN = lambda: datetime.datetime(2012, 2, date)
- response = self.request_fetcher.get('/')
- self.assertNotIn('Due today', response.body)
- self.assertNotIn('OVERDUE', response.body)
+ with time_machine.travel(datetime.datetime(2012, 2, date)):
+ response = self.request_fetcher.get('/')
+ self.assertNotIn('Due today', response.text)
+ self.assertNotIn('OVERDUE', response.text)
def testPrettyDateFormatting(self):
# Just so we're not a new user.
url = '/update_snippet?week=02-06-2012&snippet=my+snippet'
self.request_fetcher.get(url)
- snippets._TODAY_FN = lambda: datetime.datetime(2012, 2, 6)
- response = self.request_fetcher.get('/')
- self.assertIn('February 6, 2012', response.body)
+ with time_machine.travel(datetime.datetime(2012, 2, 6)):
+ response = self.request_fetcher.get('/')
+ self.assertIn('February 6, 2012', response.text)
def testUrlize(self):
url = '/update_snippet?week=02-20-2012&snippet=visit+http://foo.com'
self.request_fetcher.get(url)
response = self.request_fetcher.get('/weekly?week=02-20-2012')
- self.assertNumSnippets(response.body, 1)
- self.assertInSnippet(
- '>visit http://foo.com<',
- response.body, 0)
+ self.assertNumSnippets(response.text, 1)
+ self.assertSnippetRegex(
+ r'>visit http://foo.com<',
+ response.text, 0)
# Also make sure we urlize on the user page.
self.login('2@example.com')
response = self.request_fetcher.get('/?u=user@example.com')
- self.assertNumSnippets(response.body, 2)
- self.assertInSnippet(
- '>visit http://foo.com<',
- response.body, 0)
+ self.assertNumSnippets(response.text, 2)
+ self.assertSnippetRegex(
+ r'>visit http://foo.com<',
+ response.text, 0)
def testEditMode(self):
url = '/update_snippet?week=02-20-2012&snippet=hello'
self.request_fetcher.get(url)
response = self.request_fetcher.get('/?u=user@example.com')
- self.assertIn('Make snippet private', response.body)
+ self.assertIn('Make snippet private', response.text)
response = self.request_fetcher.get('/?u=user@example.com&edit=1')
- self.assertIn('Make snippet private', response.body)
+ self.assertIn('Make snippet private', response.text)
response = self.request_fetcher.get('/?u=user@example.com&edit=0')
- self.assertNotIn('Make snippet private', response.body)
+ self.assertNotIn('Make snippet private', response.text)
class ShowCorrectWeekTestCase(UserTestBase):
"""Test we show the right snippets for edit/view based on day of week."""
def setUp(self):
- super(ShowCorrectWeekTestCase, self).setUp()
+ super().setUp()
# Register the user so snippet-fetching works.
url = '/update_settings?category=dummy'
self.request_fetcher.get(url)
def testMonday(self):
- snippets._TODAY_FN = lambda: datetime.datetime(2012, 2, 20)
- response = self.request_fetcher.get('/')
- self.assertInSnippet('February 20, 2012', response.body, 0)
+ with time_machine.travel(datetime.datetime(2012, 2, 20)):
+ response = self.request_fetcher.get('/')
+ self.assertInSnippet('February 20, 2012', response.text, 0)
# For *viewing*'s snippets, we always show last week's snippets.
response = self.request_fetcher.get('/weekly')
- self.assertIn('February 13, 2012', response.body)
+ self.assertIn('February 13, 2012', response.text)
def testTuesday(self):
- snippets._TODAY_FN = lambda: datetime.datetime(2012, 2, 21)
- response = self.request_fetcher.get('/')
- self.assertInSnippet('February 20, 2012', response.body, 0)
+ with time_machine.travel(datetime.datetime(2012, 2, 21)):
+ response = self.request_fetcher.get('/')
+ self.assertInSnippet('February 20, 2012', response.text, 0)
response = self.request_fetcher.get('/weekly')
- self.assertIn('February 13, 2012', response.body)
+ self.assertIn('February 13, 2012', response.text)
def testWednesday(self):
- snippets._TODAY_FN = lambda: datetime.datetime(2012, 2, 22)
- response = self.request_fetcher.get('/')
- self.assertInSnippet('February 20, 2012', response.body, 0)
+ with time_machine.travel(datetime.datetime(2012, 2, 22)):
+ response = self.request_fetcher.get('/')
+ self.assertInSnippet('February 20, 2012', response.text, 0)
response = self.request_fetcher.get('/weekly')
- self.assertIn('February 13, 2012', response.body)
+ self.assertIn('February 13, 2012', response.text)
def testThursday(self):
- snippets._TODAY_FN = lambda: datetime.datetime(2012, 2, 23)
- response = self.request_fetcher.get('/')
- self.assertInSnippet('February 20, 2012', response.body, 0)
+ with time_machine.travel(datetime.datetime(2012, 2, 23)):
+ response = self.request_fetcher.get('/')
+ self.assertInSnippet('February 20, 2012', response.text, 0)
response = self.request_fetcher.get('/weekly')
- self.assertIn('February 13, 2012', response.body)
+ self.assertIn('February 13, 2012', response.text)
def testFriday(self):
- snippets._TODAY_FN = lambda: datetime.datetime(2012, 2, 24)
- response = self.request_fetcher.get('/')
- self.assertInSnippet('February 20, 2012', response.body, 0)
+ with time_machine.travel(datetime.datetime(2012, 2, 24)):
+ response = self.request_fetcher.get('/')
+ self.assertInSnippet('February 20, 2012', response.text, 0)
response = self.request_fetcher.get('/weekly')
- self.assertIn('February 13, 2012', response.body)
+ self.assertIn('February 13, 2012', response.text)
def testSaturday(self):
- snippets._TODAY_FN = lambda: datetime.datetime(2012, 2, 25)
- response = self.request_fetcher.get('/')
- self.assertInSnippet('February 20, 2012', response.body, 0)
+ with time_machine.travel(datetime.datetime(2012, 2, 25)):
+ response = self.request_fetcher.get('/')
+ self.assertInSnippet('February 20, 2012', response.text, 0)
response = self.request_fetcher.get('/weekly')
- self.assertIn('February 13, 2012', response.body)
+ self.assertIn('February 13, 2012', response.text)
def testSunday(self):
- snippets._TODAY_FN = lambda: datetime.datetime(2012, 2, 26)
- response = self.request_fetcher.get('/')
- self.assertInSnippet('February 20, 2012', response.body, 0)
+ with time_machine.travel(datetime.datetime(2012, 2, 26)):
+ response = self.request_fetcher.get('/')
+ self.assertInSnippet('February 20, 2012', response.text, 0)
response = self.request_fetcher.get('/weekly')
- self.assertIn('February 13, 2012', response.body)
+ self.assertIn('February 13, 2012', response.text)
class NosnippetGapFillingTestCase(UserTestBase):
@@ -945,23 +973,23 @@ class NosnippetGapFillingTestCase(UserTestBase):
def testNoSnippets(self):
# If nobody is registered, the user-db will be empty.
response = self.request_fetcher.get('/weekly')
- self.assertNumSnippets(response.body, 0)
+ self.assertNumSnippets(response.text, 0)
url = '/update_settings?category=dummy' # register the user
self.request_fetcher.get(url)
response = self.request_fetcher.get('/')
- self.assertNumSnippets(response.body, 2)
+ self.assertNumSnippets(response.text, 2)
def testOneSnippetInDistantPast(self):
url = '/update_snippet?week=02-21-2011&snippet=old+snippet'
self.request_fetcher.get(url)
response = self.request_fetcher.get('/')
- self.assertNumSnippets(response.body, 53)
- self.assertInSnippet('old snippet', response.body, 52)
+ self.assertNumSnippets(response.text, 53)
+ self.assertInSnippet('old snippet', response.text, 52)
response = self.request_fetcher.get('/weekly')
- self.assertNumSnippets(response.body, 1)
+ self.assertNumSnippets(response.text, 1)
def testTwoSnippetsInDistantPast(self):
url = '/update_snippet?week=08-22-2011&snippet=oldish+snippet'
@@ -969,22 +997,22 @@ def testTwoSnippetsInDistantPast(self):
url = '/update_snippet?week=02-21-2011&snippet=old+snippet'
self.request_fetcher.get(url)
response = self.request_fetcher.get('/')
- self.assertNumSnippets(response.body, 53)
- self.assertInSnippet('oldish snippet', response.body, 26)
- self.assertInSnippet('old snippet', response.body, 52)
+ self.assertNumSnippets(response.text, 53)
+ self.assertInSnippet('oldish snippet', response.text, 26)
+ self.assertInSnippet('old snippet', response.text, 52)
response = self.request_fetcher.get('/weekly')
- self.assertNumSnippets(response.body, 1)
+ self.assertNumSnippets(response.text, 1)
def testSnippetInTheFuture(self):
url = '/update_snippet?week=02-18-2013&snippet=future+snippet'
self.request_fetcher.get(url)
response = self.request_fetcher.get('/')
- self.assertNumSnippets(response.body, 54)
- self.assertInSnippet('future snippet', response.body, 0)
+ self.assertNumSnippets(response.text, 54)
+ self.assertInSnippet('future snippet', response.text, 0)
response = self.request_fetcher.get('/weekly')
- self.assertNumSnippets(response.body, 1)
+ self.assertNumSnippets(response.text, 1)
def testSnippetInThePastAndFuture(self):
url = '/update_snippet?week=02-21-2011&snippet=old+snippet'
@@ -992,19 +1020,19 @@ def testSnippetInThePastAndFuture(self):
url = '/update_snippet?week=02-18-2013&snippet=future+snippet'
self.request_fetcher.get(url)
response = self.request_fetcher.get('/')
- self.assertNumSnippets(response.body, 105)
- self.assertInSnippet('future snippet', response.body, 0)
- self.assertInSnippet('old snippet', response.body, 104)
+ self.assertNumSnippets(response.text, 105)
+ self.assertInSnippet('future snippet', response.text, 0)
+ self.assertInSnippet('old snippet', response.text, 104)
response = self.request_fetcher.get('/weekly')
- self.assertNumSnippets(response.body, 1)
+ self.assertNumSnippets(response.text, 1)
class PrivateSnippetTestCase(UserTestBase):
"""Tests that we properly restrict viewing of private snippets."""
def setUp(self):
- super(PrivateSnippetTestCase, self).setUp()
+ super().setUp()
# Set up a user with some private and some not-private snippets,
# another user with only private, and another with only public.
self.login('private@example.com')
@@ -1036,51 +1064,51 @@ def setUp(self):
def testAdminCanSeeAllSnippets(self):
self.set_is_admin()
response = self.request_fetcher.get('/weekly?week=02-13-2012')
- self.assertNumSnippets(response.body, 4)
+ self.assertNumSnippets(response.text, 4)
response = self.request_fetcher.get('/weekly?week=02-20-2012')
- self.assertNumSnippets(response.body, 4)
+ self.assertNumSnippets(response.text, 4)
def testUserCanSeeAllSnippetsFromTheirDomain(self):
# As user@example.com, we can see all but some_other_domain.com
response = self.request_fetcher.get('/weekly?week=02-13-2012')
# For private@some_other_domain.com, we should see 'no snippet found'.
- self.assertNumSnippets(response.body, 4)
- self.assertInSnippet('private@some_other_domain.com', response.body, 2)
- self.assertNotInSnippet('foreign', response.body, 2)
+ self.assertNumSnippets(response.text, 4)
+ self.assertInSnippet('private@some_other_domain.com', response.text, 2)
+ self.assertNotInSnippet('foreign', response.text, 2)
# We *should* see stuff from our domain, but marked private.
- self.assertInSnippet('private@example.com', response.body, 1)
- self.assertInSnippet('snippet-tag-private', response.body, 1)
- self.assertInSnippet('no see um', response.body, 1)
+ self.assertInSnippet('private@example.com', response.text, 1)
+ self.assertInSnippet('snippet-tag-private', response.text, 1)
+ self.assertInSnippet('no see um', response.text, 1)
# And we should see public snippets, not marked private.
- self.assertInSnippet('public@example.com', response.body, 3)
- self.assertNotInSnippet('snippet-tag-private', response.body, 3)
- self.assertInSnippet('see me', response.body, 3)
+ self.assertInSnippet('public@example.com', response.text, 3)
+ self.assertNotInSnippet('snippet-tag-private', response.text, 3)
+ self.assertInSnippet('see me', response.text, 3)
self.login('random@some_other_domain.com')
response = self.request_fetcher.get('/weekly?week=02-13-2012')
- self.assertNumSnippets(response.body, 4)
- self.assertInSnippet('private@some_other_domain.com', response.body, 2)
- self.assertInSnippet('foreign', response.body, 2)
- self.assertInSnippet('snippet-tag-private', response.body, 2)
+ self.assertNumSnippets(response.text, 4)
+ self.assertInSnippet('private@some_other_domain.com', response.text, 2)
+ self.assertInSnippet('foreign', response.text, 2)
+ self.assertInSnippet('snippet-tag-private', response.text, 2)
# Now we shouldn't see stuff from example.com
- self.assertInSnippet('private@example.com', response.body, 1)
- self.assertNotInSnippet('no see um', response.body, 1)
+ self.assertInSnippet('private@example.com', response.text, 1)
+ self.assertNotInSnippet('no see um', response.text, 1)
# And we should also see public snippets, not in gray.
- self.assertInSnippet('public@example.com', response.body, 3)
- self.assertNotInSnippet('snippet-tag-private', response.body, 3)
- self.assertInSnippet('see me', response.body, 3)
+ self.assertInSnippet('public@example.com', response.text, 3)
+ self.assertNotInSnippet('snippet-tag-private', response.text, 3)
+ self.assertInSnippet('see me', response.text, 3)
def testPrivacyIsPerSnippet(self):
self.login('random@some_other_domain.com')
response = self.request_fetcher.get('/weekly?week=02-13-2012')
- self.assertNumSnippets(response.body, 4)
- self.assertInSnippet('mixed@example.com', response.body, 0)
- self.assertNotInSnippet('cautious', response.body, 0)
+ self.assertNumSnippets(response.text, 4)
+ self.assertInSnippet('mixed@example.com', response.text, 0)
+ self.assertNotInSnippet('cautious', response.text, 0)
response = self.request_fetcher.get('/weekly?week=02-20-2012')
- self.assertNumSnippets(response.body, 4)
- self.assertInSnippet('mixed@example.com', response.body, 0)
- self.assertInSnippet('not cautious', response.body, 0)
+ self.assertNumSnippets(response.text, 4)
+ self.assertInSnippet('mixed@example.com', response.text, 0)
+ self.assertInSnippet('not cautious', response.text, 0)
def testDomainMatching(self):
# Let's make it legal for all these domains to log in.
@@ -1107,10 +1135,10 @@ def testDomainMatching(self):
self.login('user@example.com')
response = self.request_fetcher.get('/weekly?week=02-13-2012')
- self.assertNumSnippets(response.body, 8)
+ self.assertNumSnippets(response.text, 8)
for i in (0, 1, 2, 3): # the 4 close@ snippets should sort first
- self.assertInSnippet('close@', response.body, i)
- self.assertNotInSnippet('whoa', response.body, i)
+ self.assertInSnippet('close@', response.text, i)
+ self.assertNotInSnippet('whoa', response.text, i)
class MarkdownSnippetTestCase(UserTestBase):
@@ -1121,7 +1149,7 @@ class MarkdownSnippetTestCase(UserTestBase):
class.
"""
def setUp(self):
- super(MarkdownSnippetTestCase, self).setUp()
+ super().setUp()
# Set up some snippets as markdown, and some not.
url = ('/update_snippet?week=02-13-2012&snippet=*+item+1%0A*+item+2'
@@ -1132,35 +1160,35 @@ def setUp(self):
def testMarkdownRendering(self):
response = self.request_fetcher.get('/weekly?week=02-13-2012')
- self.assertInSnippet('class="snippet-text-markdown', response.body, 0)
+ self.assertInSnippet('class="snippet-text-markdown', response.text, 0)
def testTextRendering(self):
response = self.request_fetcher.get('/weekly?week=02-20-2012')
- self.assertInSnippet('class="snippet-text', response.body, 0)
+ self.assertInSnippet('class="snippet-text', response.text, 0)
class ManageUsersTestCase(UserTestBase):
"""Test we can delete users properly."""
def setUp(self):
- super(ManageUsersTestCase, self).setUp()
+ super().setUp()
# Have users with various snippet characteristics.
- snippets._TODAY_FN = lambda: datetime.datetime(2012, 2, 20, 12, 0, 0)
- self.login('has_one_snippet@example.com')
- self.request_fetcher.get('/update_snippet?week=02-13-2012&snippet=s1')
+ with time_machine.travel(datetime.datetime(2012, 2, 20, 12, 0, 0)):
+ self.login('has_one_snippet@example.com')
+ self.request_fetcher.get('/update_snippet?week=02-13-2012&snippet=s1')
- snippets._TODAY_FN = lambda: datetime.datetime(2012, 2, 20, 12, 0, 1)
- self.login('has_many_snippets@example.com')
- self.request_fetcher.get('/update_snippet?week=01-30-2012&snippet=s2')
- self.request_fetcher.get('/update_snippet?week=02-13-2012&snippet=s3')
+ with time_machine.travel(datetime.datetime(2012, 2, 20, 12, 0, 1)):
+ self.login('has_many_snippets@example.com')
+ self.request_fetcher.get('/update_snippet?week=01-30-2012&snippet=s2')
+ self.request_fetcher.get('/update_snippet?week=02-13-2012&snippet=s3')
- snippets._TODAY_FN = lambda: datetime.datetime(2012, 2, 20, 12, 0, 2)
- self.login('has_old_snippet@example.com')
- self.request_fetcher.get('/update_snippet?week=02-14-2011&snippet=s4')
+ with time_machine.travel(datetime.datetime(2012, 2, 20, 12, 0, 2)):
+ self.login('has_old_snippet@example.com')
+ self.request_fetcher.get('/update_snippet?week=02-14-2011&snippet=s4')
- snippets._TODAY_FN = lambda: datetime.datetime(2012, 2, 20, 12, 0, 3)
- self.login('has_no_snippets@example.com')
- self.request_fetcher.get('/update_settings')
+ with time_machine.travel(datetime.datetime(2012, 2, 20, 12, 0, 3)):
+ self.login('has_no_snippets@example.com')
+ self.request_fetcher.get('/update_settings')
def get_user_list(self, body):
"""Returns the email usernames of the user-list, in order."""
@@ -1175,8 +1203,8 @@ def testSortByEmail(self):
'?sort_by=email')
expected = ['has_many_snippets', 'has_no_snippets',
'has_old_snippet', 'has_one_snippet']
- self.assertEqual(expected, self.get_user_list(response.body))
- self.assertNotIn('@example.com deleted', response.body) # we didn't
+ self.assertEqual(expected, self.get_user_list(response.text))
+ self.assertNotIn('@example.com deleted', response.text) # we didn't
def testSortByCreation(self):
response = self.request_fetcher.get('/admin/manage_users'
@@ -1184,8 +1212,8 @@ def testSortByCreation(self):
# Reverse order from when we created them above.
expected = ['has_no_snippets', 'has_old_snippet',
'has_many_snippets', 'has_one_snippet']
- self.assertEqual(expected, self.get_user_list(response.body))
- self.assertNotIn('@example.com deleted', response.body) # we didn't
+ self.assertEqual(expected, self.get_user_list(response.text))
+ self.assertNotIn('@example.com deleted', response.text) # we didn't
def testSortByLastSnippet(self):
response = self.request_fetcher.get('/admin/manage_users'
@@ -1193,78 +1221,114 @@ def testSortByLastSnippet(self):
# 'many' and 'one' are tied; the tiebreak is email.
expected = ['has_no_snippets', 'has_old_snippet',
'has_many_snippets', 'has_one_snippet']
- self.assertEqual(expected, self.get_user_list(response.body))
- self.assertNotIn('@example.com deleted', response.body) # we didn't
+ self.assertEqual(expected, self.get_user_list(response.text))
+ self.assertNotIn('@example.com deleted', response.text) # we didn't
def testBadSortBy(self):
# status=500 means we expect to get back a 500 error for this.
- self.request_fetcher.get('/admin/manage_users?sort_by=unknown',
- status=500)
+ response = self.request_fetcher.get('/admin/manage_users?sort_by=unknown')
+ self.assertEqual(response.status_code, 500)
def testDelete(self):
- response = self.request_fetcher.get(
- '/admin/manage_users?delete+has_old_snippet@example.com=Delete')
- if response.status_int in (301, 302, 303, 304):
- response = response.follow()
+ response = self.request_fetcher.post(
+ '/admin/manage_users',
+ data={
+ "action": "delete",
+ "email": "has_old_snippet@example.com",
+ },
+ follow_redirects=True)
expected = ['has_no_snippets', 'has_many_snippets', 'has_one_snippet']
- self.assertEqual(expected, self.get_user_list(response.body))
- self.assertIn('has_old_snippet@example.com deleted', response.body)
+ self.assertEqual(expected, self.get_user_list(response.text))
+ self.assertIn('has_old_snippet@example.com deleted', response.text)
def testHide(self):
- response = self.request_fetcher.get(
- '/admin/manage_users?hide+has_old_snippet@example.com=Hide')
- if response.status_int in (301, 302, 303, 304):
- response = response.follow()
+ response = self.request_fetcher.post(
+ '/admin/manage_users',
+ data={
+ "action": "hide",
+ "email": "has_old_snippet@example.com",
+ },
+ follow_redirects=True)
expected = ['has_no_snippets', 'has_old_snippet',
'has_many_snippets', 'has_one_snippet']
- self.assertEqual(expected, self.get_user_list(response.body))
- self.assertIn('has_old_snippet@example.com hidden', response.body)
- self.assertIn('value="Unhide"', response.body)
+ self.assertEqual(expected, self.get_user_list(response.text))
+ self.assertIn('has_old_snippet@example.com hidden', response.text)
+ self.assertIn('value="Unhide"', response.text)
def testPreserveSortBy(self):
response = self.request_fetcher.get(
'/admin/manage_users?hide+has_old_snippet@example.com=Hide'
- '&sort_by=last_snippet_time')
- if response.status_int in (301, 302, 303, 304):
- response = response.follow()
+ '&sort_by=last_snippet_time',
+ follow_redirects=True)
# "Last snippet" shouldn't have a link letting you sort by
# last snippet, because it should already be doing so!
- self.assertIn('| Last snippet | ', response.body)
+ self.assertIn('Last snippet | ', response.text)
def testUnhide(self):
- self.request_fetcher.get(
- '/admin/manage_users?hide+has_old_snippet@example.com=Hide')
- response = self.request_fetcher.get(
- '/admin/manage_users?unhide+has_old_snippet@example.com=Hide')
- if response.status_int in (301, 302, 303, 304):
- response = response.follow()
+ self.request_fetcher.post(
+ '/admin/manage_users',
+ data={
+ "action": "hide",
+ "email": "has_old_snippet@example.com",
+ })
+ response = self.request_fetcher.post(
+ '/admin/manage_users',
+ data={
+ "action": "unhide",
+ "email": "has_old_snippet@example.com",
+ },
+ follow_redirects=True)
expected = ['has_no_snippets', 'has_old_snippet',
'has_many_snippets', 'has_one_snippet']
- self.assertEqual(expected, self.get_user_list(response.body))
- self.assertIn('has_old_snippet@example.com unhidden', response.body)
- self.assertNotIn('value="Unhide"', response.body)
+ self.assertEqual(expected, self.get_user_list(response.text))
+ self.assertIn('has_old_snippet@example.com unhidden', response.text)
+ self.assertNotIn('value="Unhide"', response.text)
def testInvalidButton(self):
- self.request_fetcher.get('/admin/manage_users'
- '?delete+has_old_snippet=Delete',
- status=500)
- self.request_fetcher.get('/admin/manage_users'
- '?hide+has_old_snippet=Hide',
- status=500)
- self.request_fetcher.get('/admin/manage_users'
- '?unhide+has_old_snippet=Unhide',
- status=500)
+ self.assertEqual(
+ self.request_fetcher.post(
+ '/admin/manage_users',
+ data={
+ "action": "delete",
+ "email": "has_old_snippet",
+ }
+ ).status_code,
+ 500
+ )
+ self.assertEqual(
+ self.request_fetcher.post(
+ '/admin/manage_users',
+ data={
+ "action": "hide",
+ "email": "has_old_snippet",
+ }
+ ).status_code,
+ 500
+ )
+ self.assertEqual(
+ self.request_fetcher.post(
+ '/admin/manage_users',
+ data={
+ "action": "unhide",
+ "email": "has_old_snippet",
+ }
+ ).status_code,
+ 500
+ )
+# We send out mail on Sunday nights and Monday mornings, so we'll set 'today' to
+# be Sunday right around midnight.
+@time_machine.travel(datetime.datetime(2012, 2, 19, 23, 50, 0, tzinfo=ZoneInfo("UTC")))
class SendingEmailTestCase(UserTestBase):
"""Test we correctly send cron emails."""
def setUp(self):
- super(SendingEmailTestCase, self).setUp()
+ super().setUp()
self.testbed.init_mail_stub()
self.mail_stub = self.testbed.get_stub(testbed.MAIL_SERVICE_NAME)
# The email-senders sleep 2 seconds between sends for quota
@@ -1273,10 +1337,6 @@ def setUp(self):
self.sleep_fn = time.sleep
time.sleep = lambda sec: sec
- # We send out mail on Sunday nights and Monday mornings, so
- # we'll set 'today' to be Sunday right around midnight.
- snippets._TODAY_FN = lambda: datetime.datetime(2012, 2, 19, 23, 50, 0)
-
# For our mail tests, we set up a db with a few users, some of
# whom have snippets for this week ('this week' being 13 Feb
# 2012), some of whom don't.
@@ -1296,8 +1356,8 @@ def setUp(self):
self.login('user@example.com') # back to the normal user
def tearDown(self):
- UserTestBase.tearDown(self)
time.sleep = self.sleep_fn
+ super().tearDown()
def assertEmailSentTo(self, email):
r = self.mail_stub.get_sent_messages(to=email)
@@ -1323,7 +1383,7 @@ def testMustBeAdminToSendMail(self):
def testDoNotSendMailWithoutSetting(self):
app_settings = models.AppSettings.get()
- app_settings.delete()
+ app_settings.key.delete()
self.request_fetcher.get('/admin/send_reminder_email')
self.assertEmailNotSentTo('does_not_have_snippet@example.com')
@@ -1411,8 +1471,8 @@ def count_calls_per_minute(sleep_seconds):
# We'll do 500 users. Rather than go through the request
# API, we modify the db directly; it's much faster.
users = [models.User(email='snippets%d@example.com' % i)
- for i in xrange(500)]
- db.put(users)
+ for i in range(500)]
+ ndb.put_multi(users)
self.request_fetcher.get('/admin/send_view_email')
# https://developers.google.com/appengine/docs/quotas#Mail
@@ -1428,7 +1488,7 @@ class SendingChatTestCase(UserTestBase):
"""Test we correctly send to Slack."""
def setUp(self):
# (The superclass sets up slack_sends for us.)
- super(SendingChatTestCase, self).setUp()
+ super().setUp()
# Let's set up default chat configs.
app_settings = models.AppSettings.get()
app_settings.slack_channel = '#slack_chann3l'
@@ -1474,15 +1534,15 @@ def testTrimTrailingSpaces(self):
class DisplayNameTestCase(UserTestBase):
"""Manipulate a user's display name and check it in weekly page."""
def setUp(self):
- super(UserTestBase, self).setUp()
+ super().setUp()
self.login('user@example.com')
def testUserHasEmptyDisplayName(self):
url = '/update_snippet?week=02-20-2012&snippet=my+snippet'
self.request_fetcher.get(url)
response = self.request_fetcher.get('/weekly?week=02-20-2012')
- self.assertNumSnippets(response.body, 1)
- self.assertInSnippet('user@example.com:
', response.body, 0)
+ self.assertNumSnippets(response.text, 1)
+ self.assertInSnippet('user@example.com:
', response.text, 0)
def testUserHasDisplayName(self):
self.request_fetcher.get(
@@ -1490,9 +1550,9 @@ def testUserHasDisplayName(self):
url = '/update_snippet?week=02-20-2012&snippet=my+snippet'
self.request_fetcher.get(url)
response = self.request_fetcher.get('/weekly?week=02-20-2012')
- self.assertNumSnippets(response.body, 1)
+ self.assertNumSnippets(response.text, 1)
self.assertInSnippet('test name (user@example.com):
',
- response.body, 0)
+ response.text, 0)
def testUserChangesDisplayName(self):
self.request_fetcher.get(
@@ -1500,16 +1560,16 @@ def testUserChangesDisplayName(self):
url = '/update_snippet?week=02-20-2012&snippet=my+snippet'
self.request_fetcher.get(url)
response = self.request_fetcher.get('/weekly?week=02-20-2012')
- self.assertNumSnippets(response.body, 1)
+ self.assertNumSnippets(response.text, 1)
self.assertInSnippet('test name (user@example.com):
',
- response.body, 0)
+ response.text, 0)
self.request_fetcher.get(
'/update_settings?u=user@example.com&display_name=fancy+name')
response = self.request_fetcher.get('/weekly?week=02-20-2012')
- self.assertNumSnippets(response.body, 1)
+ self.assertNumSnippets(response.text, 1)
self.assertInSnippet('fancy name (user@example.com):
',
- response.body, 0)
+ response.text, 0)
def testSnippetHasDisplayName(self):
self.request_fetcher.get(
@@ -1519,29 +1579,29 @@ def testSnippetHasDisplayName(self):
self.request_fetcher.get(
'/update_settings?u=user@example.com&display_name=')
response = self.request_fetcher.get('/weekly?week=02-20-2012')
- self.assertNumSnippets(response.body, 1)
+ self.assertNumSnippets(response.text, 1)
self.assertInSnippet('test name (user@example.com):
',
- response.body, 0)
+ response.text, 0)
def testSnippetFromDeletedUser(self):
url = '/update_snippet?week=02-20-2012&snippet=my+snippet'
self.request_fetcher.get(url)
response = self.request_fetcher.get('/weekly?week=02-20-2012')
- self.assertNumSnippets(response.body, 1)
- self.assertInSnippet('user@example.com:
', response.body, 0)
+ self.assertNumSnippets(response.text, 1)
+ self.assertInSnippet('user@example.com:
', response.text, 0)
self.request_fetcher.get(
'/update_settings?u=user@example.com&display_name=test+name')
response = self.request_fetcher.get('/weekly?week=02-20-2012')
- self.assertNumSnippets(response.body, 1)
+ self.assertNumSnippets(response.text, 1)
self.assertInSnippet('test name (user@example.com):
',
- response.body, 0)
+ response.text, 0)
url = '/update_settings?u=user@example.com&delete=Delete'
self.request_fetcher.get(url)
response = self.request_fetcher.get('/weekly?week=02-20-2012')
- self.assertNumSnippets(response.body, 1)
- self.assertInSnippet('user@example.com:
', response.body, 0)
+ self.assertNumSnippets(response.text, 1)
+ self.assertInSnippet('user@example.com:
', response.text, 0)
def testSnippetFromDeletedUser2(self):
self.request_fetcher.get(
@@ -1549,16 +1609,13 @@ def testSnippetFromDeletedUser2(self):
url = '/update_snippet?week=02-20-2012&snippet=my+snippet'
self.request_fetcher.get(url)
response = self.request_fetcher.get('/weekly?week=02-20-2012')
- self.assertNumSnippets(response.body, 1)
+ self.assertNumSnippets(response.text, 1)
self.assertInSnippet('test name (user@example.com):
',
- response.body, 0)
+ response.text, 0)
url = '/update_settings?u=user@example.com&delete=Delete'
self.request_fetcher.get(url)
response = self.request_fetcher.get('/weekly?week=02-20-2012')
- self.assertNumSnippets(response.body, 1)
+ self.assertNumSnippets(response.text, 1)
self.assertInSnippet('test name (user@example.com):
',
- response.body, 0)
-
-if __name__ == '__main__':
- unittest.main()
+ response.text, 0)
diff --git a/static/manage_users.js b/static/manage_users.js
index 540d8a8..338168f 100644
--- a/static/manage_users.js
+++ b/static/manage_users.js
@@ -8,7 +8,7 @@ $(function() {
if ($inputButton.value === "Hide") {
$inputButton.value = "Hiding...";
$inputButton.disabled = true;
- $.ajax("/admin/manage_users?hide%20" + email)
+ $.post("/admin/manage_users", {action: "hide", email: email})
.then(function() {
$inputButton.name = "unhide " + email;
$inputButton.value = "Unhide";
@@ -20,7 +20,7 @@ $(function() {
} else {
$inputButton.value = "Unhiding...";
$inputButton.disabled = true;
- $.ajax("/admin/manage_users?unhide%20" + email)
+ $.post("/admin/manage_users", {action: "unhide", email: email})
.then(function() {
$inputButton.name = "hide " + email;
$inputButton.value = "Hide";
@@ -37,7 +37,7 @@ $(function() {
var email = $($inputButton).attr("data-email");
$inputButton.value = "Deleting...";
$inputButton.disabled = true;
- $.ajax("/admin/manage_users?delete%20" + email)
+ $.post("/admin/manage_users", {action: "delete", email: email})
.then(function() {
$inputButton.value = "Deleted";
// TODO(csilvers): change the whole row to indicate deleted
diff --git a/templates/app_settings.html b/templates/app_settings.html
index 67bbbc4..ef5075c 100644
--- a/templates/app_settings.html
+++ b/templates/app_settings.html
@@ -9,7 +9,7 @@ Manage Users
hide users
-