.
11 |
$page.body
12 |
--------------------------------------------------------------------------------
/scripts/run_python_linters.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # Used for GitHub Actions
3 | set -e -v
4 |
5 | # Run linters and formatters
6 | black --skip-string-normalization .
7 | codespell \
8 | --exclude-file=infogami/core/files/js/repetition/repetition-model.js
9 | ruff check . # See pyproject.toml for args
10 | mypy --install-types --non-interactive .
11 |
--------------------------------------------------------------------------------
/infogami/core/i18n/account/register/strings.en:
--------------------------------------------------------------------------------
1 | confirm_password = 'Confirm Password'
2 | username = 'Username'
3 | display_name = 'Display Name'
4 | register = 'Register'
5 | username_already_exists = 'Username already exists'
6 | create_new_account = 'Create A New Account'
7 | passwords_did_not_match = 'Passwords did not match'
8 | password = 'Password'
9 | email = 'E-Mail'
--------------------------------------------------------------------------------
/infogami/core/pages/__root__.page:
--------------------------------------------------------------------------------
1 | {
2 | 'key': '/',
3 | 'type': {'key': '/type/page'},
4 | 'title': 'Welcome',
5 | 'body': 'Infogami is a free, easy-to-use, structured wiki.\n'
6 | 'Visit [Infogami Developer Site](http://infogami.org/dev/) for more details.\n\n'
7 | '* [[pagelist]]\n'
8 | '* [[recentchanges]]\n'
9 | }
10 |
--------------------------------------------------------------------------------
/scripts/run_python_tests.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # Used for GitHub Actions
3 | set -e
4 |
5 | # web.py needs to find the database on a host named postgres
6 | echo "127.0.0.1 postgres" | sudo tee -a /etc/hosts
7 | # MUST have a --host=
8 | psql --host=postgres --command='create database infobase_test;'
9 |
10 | # Run tests
11 | pytest infogami -s
12 | pytest tests
13 | pytest test
14 |
--------------------------------------------------------------------------------
/infogami/core/i18n/mode/edit/strings.en:
--------------------------------------------------------------------------------
1 | save = 'Save'
2 | edit_title = 'Edit %s'
3 | add_property = 'Add %s'
4 | view = 'View'
5 | history = 'History'
6 | move_down = 'Move up'
7 | page_type = 'Page Type'
8 | edit_summary = '
Edit Summary (briefly describe the changes you have made):'
9 | move_up = 'Move down'
10 | preview = 'Preview'
11 | change = 'Change'
12 | delete = 'Delete'
--------------------------------------------------------------------------------
/infogami/core/templates/default_repr.html:
--------------------------------------------------------------------------------
1 | $def with (thing, property=None)
2 | $if thing.is_primitive: $thing.value
3 | $elif thing.type.kind == 'embeddable' or thing.key is None:
4 |
5 | $for k in thing:
6 |
7 | $k
8 | $:thingrepr(thing[k])
9 |
10 |
11 | $else:
$thing.key
12 |
13 |
--------------------------------------------------------------------------------
/infogami/core/templates/preferences.html:
--------------------------------------------------------------------------------
1 | $def with (preferences)
2 |
3 | $ _ = i18n.get_namespace('/account/preferences')
4 |
5 | $var title: $_.preferences
6 |
7 |
10 |
11 |
12 | $for title, href in preferences:
13 | $title
14 |
15 |
--------------------------------------------------------------------------------
/infogami/core/templates/permission.html:
--------------------------------------------------------------------------------
1 | $def with (p)
2 |
3 | $var title: Permission of $p.key
4 |
5 |
10 |
--------------------------------------------------------------------------------
/infogami/plugins/i18n/db.py:
--------------------------------------------------------------------------------
1 | import web
2 |
3 |
4 | def get_all_strings(site):
5 | t = site.get('/type/i18n')
6 | if t is None:
7 | return []
8 | else:
9 | q = {'type': '/type/i18n', 'limit': 1000}
10 | return site.get_many(site.things(q))
11 |
12 |
13 | def get_all_sites():
14 | if web.ctx.site.exists():
15 | return [web.ctx.site]
16 | else:
17 | return []
18 |
--------------------------------------------------------------------------------
/test/test_doctests.py:
--------------------------------------------------------------------------------
1 | """Run all doctests in infogami.
2 | """
3 |
4 | from test import webtest
5 |
6 |
7 | def suite():
8 | modules = [
9 | "infogami.infobase.common",
10 | "infogami.infobase.readquery",
11 | "infogami.infobase.writequery",
12 | "infogami.infobase.dbstore",
13 | ]
14 | return webtest.doctest_suite(modules)
15 |
16 |
17 | if __name__ == "__main__":
18 | webtest.main()
19 |
--------------------------------------------------------------------------------
/infogami/core/files/js/repetition/repetition-model-msie_init.js:
--------------------------------------------------------------------------------
1 | //Loaded dynamically in MSIE by script[defer] tag which emulated DOMContentLoaded event. See
2 | if(!window.RepetitionElement)
3 | throw Error("Repetition Model error: You must include the file 'repetition-model.js' to enable the functionality. The file you included is loaded dynamically for MSIE.");
4 | RepetitionElement._init_document();
--------------------------------------------------------------------------------
/infogami/core/templates/login.html:
--------------------------------------------------------------------------------
1 | $def with (f)
2 |
3 | $ _ = i18n.get_namespace('/account/login')
4 |
5 | $var title: $_.login
6 |
7 |
$_.login_stmt
8 |
9 |
13 |
14 |
($_.forgot_password )
15 |
16 |
$:_.create_new_account(homepath() + "/account/register")
17 |
--------------------------------------------------------------------------------
/infogami/core/i18n/account/preferences/strings.en:
--------------------------------------------------------------------------------
1 | confirm_password = 'Confirm Password'
2 | login_preferences = 'Login Preferences'
3 | new_password = 'New Password'
4 | template_root = 'Template Root'
5 | current_password = 'Current Password'
6 | template_preferences = 'Template Preferences'
7 | users_preferences = "%s's Preferences"
8 | passwords_did_not_match = 'Passwords did not match'
9 | save = 'Save'
10 | incorrect_password = 'Incorrect password'
11 | preferences = ' Preferences'
--------------------------------------------------------------------------------
/infogami/core/macros/PageList.html:
--------------------------------------------------------------------------------
1 | $def with (path="", limit=50)
2 |
3 | $ page = safeint(query_param("page", "0"))
4 | $ pages = list_pages(path, limit=limit, offset=page * limit)
5 |
6 |
7 | $for p in pages:
8 | $p.key
9 |
10 |
11 |
12 | $if page != 0:
13 | ←
Prev
14 |
15 | $if len(pages) == limit:
16 | ...
Next →
17 |
18 |
--------------------------------------------------------------------------------
/infogami/core/types/page.type:
--------------------------------------------------------------------------------
1 | {
2 | 'key': '/type/page',
3 | 'type': {'key': '/type/type'},
4 | 'name': 'Page',
5 | 'kind': 'regular',
6 | 'properties': [{
7 | 'name': 'title',
8 | 'type': {'key': '/type/property'},
9 | 'expected_type': {'key': '/type/string'},
10 | 'unique': True
11 | }, {
12 | 'name': 'body',
13 | 'type': {'key': '/type/property'},
14 | 'expected_type': {'key': '/type/text'},
15 | 'unique': True
16 | }]
17 | }
18 |
--------------------------------------------------------------------------------
/infogami/core/templates/editpage.html:
--------------------------------------------------------------------------------
1 | $def with (page, preview=False)
2 |
3 | $ _ = i18n.get_namespace('/mode/edit')
4 |
5 |
9 |
10 | $if preview:
11 |
12 | $:thingview(page)
13 |
14 |
15 |
16 |
17 | $ edit = thingedit(page)
18 |
19 | $var title: $edit.title
20 |
21 | $:edit
22 |
23 |
24 |
--------------------------------------------------------------------------------
/infogami/plugins/wikitemplates/types/macro.type:
--------------------------------------------------------------------------------
1 | {
2 | 'key': '/type/macro',
3 | 'type': {'key': '/type/type'},
4 | 'name': 'Macro',
5 | 'kind': 'regular',
6 | 'properties': [{
7 | 'name': 'macro',
8 | 'type': {'key': '/type/property'},
9 | 'expected_type': {'key': '/type/text'},
10 | 'unique': True
11 | }, {
12 | 'name': 'description',
13 | 'type': {'key': '/type/property'},
14 | 'expected_type': {'key': '/type/string'},
15 | 'unique': True
16 | }]
17 | }
18 |
19 |
--------------------------------------------------------------------------------
/infogami/plugins/wikitemplates/types/template.type:
--------------------------------------------------------------------------------
1 | {
2 | 'key': '/type/template',
3 | 'type': {'key': '/type/type'},
4 | 'name': 'Template',
5 | 'kind': 'regular',
6 | 'properties': [{
7 | 'name': 'title',
8 | 'type': {'key': '/type/property'},
9 | 'expected_type': {'key': '/type/string'},
10 | 'unique': True
11 | }, {
12 | 'name': 'body',
13 | 'type': {'key': '/type/property'},
14 | 'expected_type': {'key': '/type/text'},
15 | 'unique': True
16 | }]
17 | }
18 |
19 |
--------------------------------------------------------------------------------
/sample_infobase.yml:
--------------------------------------------------------------------------------
1 | ## infobase configuration
2 |
3 | db_parameters:
4 | host: localhost
5 | name: infobase
6 | user: joe
7 | password: secret
8 |
9 | ## secret_key used in encrypting user passwords
10 | # secret_key: my-secret-key
11 |
12 | ## query timeout in milli sec.
13 | query_timeout: 60000
14 |
15 | cache_size: 1000
16 |
17 | ## Additional python path. will be added to python sys.path
18 | # python_path:
19 | # - /addition/path1
20 |
21 | ## plugins
22 | # plugins:
23 | # - plugin1
24 |
25 | bind_address:
26 | port: 8080
27 | fastcgi: false
28 |
--------------------------------------------------------------------------------
/infogami/core/templates/type/boolean/input.html:
--------------------------------------------------------------------------------
1 | $def with (value, property)
2 |
3 | $# when checkbox is not selected, browser doesn't send any value to the server.
4 | $# Adding a hidden field sends false value when checkbox is not selected
5 | $# and 2 values when checkbox is selected and the server takes only the latter value.
6 |
7 |
8 | $if value.value:
9 |
10 | $else:
11 |
12 |
--------------------------------------------------------------------------------
/infogami/utils/types.py:
--------------------------------------------------------------------------------
1 | """Maintains a registry of path pattern vs type names to guess type from path when a page is newly created.
2 | """
3 |
4 | import re
5 |
6 | from infogami.utils import storage
7 |
8 | default_type = '/type/page'
9 | type_patterns = storage.OrderedDict()
10 |
11 |
12 | def register_type(pattern, typename):
13 | type_patterns[pattern] = typename
14 |
15 |
16 | def guess_type(path):
17 | for pattern, typename in type_patterns.items():
18 | if re.search(pattern, path):
19 | return typename
20 |
21 | return default_type
22 |
--------------------------------------------------------------------------------
/infogami/core/templates/viewpage.html:
--------------------------------------------------------------------------------
1 | $def with (page)
2 |
3 | $ _ = i18n.get_namespace('/mode/view')
4 |
5 | $ view = thingview(page)
6 |
7 | $var title: $view.title
8 |
9 | $if 'content_type' in view:
10 | $var content_type = view.content_type
11 |
12 | $if 'rawtext' in view:
13 | $var rawtext = view.rawtext
14 |
15 | $:view
16 |
17 |
22 |
23 |
--------------------------------------------------------------------------------
/infogami/core/templates/password_mailer.html:
--------------------------------------------------------------------------------
1 | $def with (site, username, code)
2 |
3 | $var subject: Forgot password
4 | $var title: Forgot password
5 | Hello,
6 |
7 | You (or someone claiming to be you) have requested us to reset your password.
8 |
9 | In case you forgot your username, it is $:username.
10 |
11 | If you want to reset your password, click the following link.
12 |
13 | $:url(site + "/account/reset_password", username=username, code=code)
14 |
15 | If you have not requested this, please ignore this mail. Nothing is changed in your account.
16 |
17 | Thanks
18 | Infogami
19 |
--------------------------------------------------------------------------------
/infogami/core/templates/type/text/diff.html:
--------------------------------------------------------------------------------
1 | $def with (a, b, name)
2 |
3 | $ diffresults = better_diff(a.value.splitlines(), b.value.splitlines())
4 |
5 |
6 |
7 |
8 |
9 | $for key, ai, a, bi, b in diffresults:
10 |
11 |
12 | $ai
13 | $:spacesafe(a)
14 | $bi
15 | $:spacesafe(b)
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/infogami/core/templates/type/backreference/input.html:
--------------------------------------------------------------------------------
1 | $def with (value, p)
2 |
3 |
4 |
5 | Name
6 | $:thinginput(value.name, expected_type="/type/string", name=p.name + ".name")
7 |
8 |
9 |
10 | Expected Type
11 | $:thinginput(value.expected_type, expected_type="/type/type", name=p.name + ".expected_type")
12 |
13 |
14 | Property Name
15 | $:thinginput(value.property_name, expected_type="/type/string", name=p.name + ".property_name")
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/infogami/plugins/review/templates/changes.html:
--------------------------------------------------------------------------------
1 | $def with (root, changes)
2 |
3 | $var title: $_.UNREVIEWED_CHANGES
4 |
5 |
6 |
7 | $_.PATH
8 | $_.REVIEWED
9 | $_.LATEST
10 |
11 |
12 |
13 | $for c in changes:
14 |
15 | $c.path
16 | $(c.reviewed_revision or "-")
17 | $c.revision
18 |
19 | $_.REVIEW
20 |
21 |
22 | $else:
23 | $_.NO_NEW_CHANGES_TO_REVIEW
24 |
25 |
26 |
--------------------------------------------------------------------------------
/scripts/pytests_failing_on_py2_and_py3.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | FAILING_FILES=(
4 | infogami/core/dbupgrade.py
5 | infogami/infobase/_json.py
6 | infogami/infobase/bulkupload.py
7 | infogami/plugins/i18n/code.py
8 | infogami/plugins/links/db.py
9 | infogami/plugins/pages/code.py
10 | infogami/plugins/review/code.py
11 | infogami/plugins/review/db.py
12 | infogami/plugins/wikitemplates/code.py
13 | migration/migrate-0.4-0.5.py
14 | test/bug_239238.py
15 | )
16 |
17 | for FILEPATH in "${FAILING_FILES[@]}"; do
18 | echo "<<< $FILEPATH >>>"
19 | pytest "$FILEPATH"
20 | pytest --doctest-modules "$FILEPATH"
21 | done
22 |
--------------------------------------------------------------------------------
/infogami/core/i18n/utils/date/strings.en:
--------------------------------------------------------------------------------
1 | ago = "ago"
2 | from_now = "from now"
3 |
4 | microsecond = "microsecond"
5 | millisecond = "millisecond"
6 | second = "second"
7 | minute = "minute"
8 | hour = "hour"
9 | day = "day"
10 |
11 | microseconds = "microseconds"
12 | milliseconds = "milliseconds"
13 | seconds = "seconds"
14 | minutes = "minutes"
15 | hours = "hours"
16 | days = "days"
17 |
18 | january = "January"
19 | february = "February"
20 | march = "March"
21 | april = "April"
22 | may = "May"
23 | june = "June"
24 | july = "July"
25 | august = "August"
26 | septemeber = "September"
27 | october = "October"
28 | november = "November"
29 | december = "December"
30 |
31 |
--------------------------------------------------------------------------------
/infogami/plugins/wikitemplates/forms.py:
--------------------------------------------------------------------------------
1 | from web.form import Button, Form, Textbox, net
2 |
3 | from infogami.utils import i18n
4 |
5 |
6 | class BetterButton(Button):
7 | def render(self):
8 | label = self.attrs.get('label', self.name)
9 | safename = net.websafe(self.name)
10 | x = f'
{label} '
11 | return x
12 |
13 |
14 | _ = i18n.strings.get_namespace('/account/preferences')
15 |
16 | template_preferences = Form(
17 | Textbox("path", description=_.template_root), BetterButton('save', label=_.save)
18 | )
19 |
20 | if __name__ == "__main__":
21 | print(template_preferences().render())
22 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | from setuptools import find_packages, setup
4 |
5 | setup(
6 | name='infogami',
7 | version="0.5dev",
8 | description='Infogami: A new kind of wiki',
9 | author='Anand Chitipothu',
10 | author_email='anandology@gmail.com',
11 | url=' http://infogami.org/',
12 | packages=find_packages(exclude=["ez_setup"]),
13 | classifiers=[
14 | 'Programming Language :: Python',
15 | 'Programming Language :: Python :: 3',
16 | 'Programming Language :: Python :: 3.12',
17 | 'Programming Language :: Python :: Implementation :: CPython',
18 | ],
19 | license="AGPLv3",
20 | platforms=["any"],
21 | )
22 |
--------------------------------------------------------------------------------
/infogami/core/types/rawtext.type:
--------------------------------------------------------------------------------
1 | {
2 | 'key': '/type/rawtext',
3 | 'type': {'key': '/type/type'},
4 | 'name': 'Raw Text',
5 | 'kind': 'regular',
6 | 'properties': [{
7 | 'name': 'title',
8 | 'type': {'key': '/type/property'},
9 | 'expected_type': {'key': '/type/string'},
10 | 'unique': True
11 | }, {
12 | 'name': 'content_type',
13 | 'type': {'key': '/type/property'},
14 | 'expected_type': {'key': '/type/string'},
15 | 'unique': True
16 | }, {
17 | 'name': 'body',
18 | 'type': {'key': '/type/property'},
19 | 'expected_type': {'key': '/type/text'},
20 | 'unique': True
21 | }]
22 | }
23 |
--------------------------------------------------------------------------------
/infogami/plugins/links/code.py:
--------------------------------------------------------------------------------
1 | """
2 | links: allow interwiki links
3 |
4 | Adds a markdown preprocessor to catch `[[foo]]` style links.
5 | Creates a new set of database tables to keep track of them.
6 | Creates a new `m=backlinks` to display the results.
7 | """
8 |
9 | from infogami.core import db
10 | from infogami.utils import delegate
11 | from infogami.utils.template import render
12 |
13 |
14 | class backlinks(delegate.mode):
15 | def GET(self, site, path):
16 | # @@ fix later
17 | return []
18 | # TODO: (cclauss) unreachable code...
19 | links = db.Things(type=db.get_type(site, 'type/page'), parent=site, links=path)
20 | return render.backlinks(links)
21 |
--------------------------------------------------------------------------------
/infogami/core/files/jquery.autocomplete.css:
--------------------------------------------------------------------------------
1 | .ac_results {
2 | padding: 0px;
3 | border: 1px solid WindowFrame;
4 | background-color: Window;
5 | overflow: hidden;
6 | }
7 |
8 | .ac_results ul {
9 | width: 100%;
10 | list-style-position: outside;
11 | list-style: none;
12 | padding: 0;
13 | margin: 0;
14 | }
15 |
16 | .ac_results li {
17 | margin: 0px;
18 | padding: 2px 5px;
19 | cursor: default;
20 | display: block;
21 | width: 100%;
22 | font: menu;
23 | font-size: 12px;
24 | overflow: hidden;
25 | }
26 |
27 | .ac_loading {
28 | background : Window url('indicator.gif') right center no-repeat;
29 | }
30 |
31 | .ac_over {
32 | background-color: Highlight;
33 | color: HighlightText;
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/infogami/infobase/tests/test_seq.py:
--------------------------------------------------------------------------------
1 | from infogami.infobase._dbstore.sequence import SequenceImpl
2 | from infogami.infobase.tests import utils
3 |
4 |
5 | def setup_module(mod):
6 | global db
7 | utils.setup_db(mod)
8 | mod.seq = SequenceImpl(db)
9 |
10 |
11 | def teardown_module(mod):
12 | utils.teardown_db(mod)
13 | mod.seq = None
14 |
15 |
16 | class TestSeq:
17 | def setup_method(self, method):
18 | global db
19 | db.delete("seq", where="1=1")
20 |
21 | def test_seq(self):
22 | global seq
23 | seq.get_value("foo") == 0
24 | seq.next_value("foo") == 1
25 | seq.get_value("foo") == 1
26 |
27 | seq.next_value("foo") == 2
28 | seq.next_value("foo") == 3
29 |
--------------------------------------------------------------------------------
/.git-blame-ignore-revs:
--------------------------------------------------------------------------------
1 | # This file is used to ignore specific commit revision for `git blame`. It
2 | # contains a list of commits that are not likely what you are looking for in a
3 | # blame, such as mass reformatting or renaming. It can be used by any of the
4 | # following ways:
5 | #
6 | # - Include the file in the command:
7 | # $ git blame --ignore-revs-file .git-blame-ignore-revs
8 | #
9 | # - Through gitconfig (needs to be set per-project):
10 | # $ git config blame.ignoreRevsFile .git-blame-ignore-revs
11 | #
12 | # Each commit has to be the full 40 character hash and comments are allowed.
13 | # Required git version: 2.23
14 |
15 | # Format the entire codebase through black (PR #162)
16 | 42acd0ae22c27659959788fdbc82d89a4c35afd4
17 |
--------------------------------------------------------------------------------
/infogami/core/templates/default_diff.html:
--------------------------------------------------------------------------------
1 | $def with (a, b, name)
2 |
3 | $if a.is_primitive:
4 | $ diffresults = diff(a.value, b.value)
5 | $else:
6 | $ diffresults = diff(a.key, b.key)
7 |
8 |
9 | $for r in diffresults:
10 | $if r.tag == "equal": $r.left \
11 | $if r.tag == "delete": $r.left \
12 | $if r.tag == "insert": $r.left \
13 | $if r.tag == "replace": $r.left \
14 |
15 | $for r in diffresults:
16 | $if r.tag == "equal": $r.right \
17 | $if r.tag == "delete": $r.right \
18 | $if r.tag == "insert": $r.right \
19 | $if r.tag == "replace": $r.right \
20 |
21 |
--------------------------------------------------------------------------------
/infogami/plugins/wikitemplates/db.py:
--------------------------------------------------------------------------------
1 | import web
2 |
3 |
4 | def get_all_templates(site):
5 | t = site.get('/type/template')
6 | if t is None:
7 | return []
8 | q = {'type': '/type/template', 'limit': 1000}
9 | # return [site.get(key) for key in site.things(q)]
10 | return site.get_many([key for key in site.things(q)])
11 |
12 |
13 | def get_all_macros(site):
14 | t = site.get('/type/macro')
15 | if t is None:
16 | return []
17 | q = {'type': '/type/macro', 'limit': 1000}
18 | # return [site.get(key) for key in site.things(q)]
19 | return site.get_many([key for key in site.things(q)])
20 |
21 |
22 | def get_all_sites():
23 | if web.ctx.site.exists():
24 | return [web.ctx.site]
25 | else:
26 | return []
27 |
--------------------------------------------------------------------------------
/infogami/core/templates/type/property/input.html:
--------------------------------------------------------------------------------
1 | $def with (value, p)
2 |
3 |
4 |
5 | Name
6 | $:thinginput(value.name, expected_type="/type/string", name=p.name + ".name")
7 |
8 |
9 |
10 | Expected Type
11 | $:thinginput(value.expected_type, expected_type="/type/type", name=p.name + ".expected_type")
12 |
13 |
14 | Unique
15 | $:thinginput(value.unique, expected_type="/type/boolean", name=p.name + ".unique")
16 |
17 |
18 | Description
19 | $:thinginput(value.description, expected_type="/type/string", name=p.name + ".description")
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/scripts/server:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Script to run infogami server.
3 |
4 | USAGE:
5 |
6 | * Run Infogami http server at port 8080.
7 |
8 | $ ./scripts/server infogami.yml startserver 8080
9 |
10 | * Run Infobase as fastcgi server at port 7070
11 |
12 | $ ./scripts/server infogami.yml startserver fastcgi 8080
13 | """
14 | import sys
15 |
16 | import _init_path # noqa: F401 Imported for its side effect of setting PYTHONPATH
17 |
18 | import infogami
19 |
20 |
21 | def main(args):
22 | if len(args) < 1 or sys.argv[0] in ("-h", "--help"):
23 | print(f"USAGE: {sys.argv[0]} configfile [subcommand] [arguments]", file=sys.stderr)
24 | sys.exit(1)
25 |
26 | infogami.main(*args)
27 |
28 |
29 | if __name__ == "__main__":
30 | main(sys.argv[1:])
31 |
--------------------------------------------------------------------------------
/infogami/core/templates/default_input.html:
--------------------------------------------------------------------------------
1 | $def with (thing, property)
2 |
3 | $ kind = property.expected_type.kind
4 |
5 | $if kind == "primitive" or kind == "basic":
6 | $if property.get('options'):
7 | $:Dropdown(property.name, property.options, value=thing.value).render()
8 | $else:
9 |
10 | $elif kind == 'embeddable':
11 |
12 | $for p in property.expected_type.properties:
13 |
14 | $p.name
15 | $:thinginput(thing[p.name], expected_type=p.expected_type, name=property.name + '.' + p.name)
16 |
17 |
18 | $else:
19 | $:macros.ThingReference(thing.type, property.name, thing)
20 |
21 |
--------------------------------------------------------------------------------
/scripts/infobase_server:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Script to run infobase.
3 |
4 | USAGE:
5 |
6 | * Run Infobase http server at port 7070.
7 |
8 | $ python ./script/infobase_server infobase.yaml 7070
9 |
10 | * Run Infobase as fastcgi server at port 7070
11 |
12 | $ python ./script/infobase_server infobase.yaml fastcgi 7070
13 | """
14 | import sys
15 |
16 | import _init_path # noqa: F401 Imported for its side effect of setting PYTHONPATH
17 |
18 | from infogami.infobase import server
19 |
20 |
21 | def main(args):
22 | if len(args) < 1 or args[0] in ('-h', '--help'):
23 | print(f"USAGE: {sys.argv[0]} configfile [port]", file=sys.stderr)
24 | sys.exit(1)
25 |
26 | server.start(*args)
27 |
28 | if __name__ == "__main__":
29 | main(sys.argv[1:])
30 |
--------------------------------------------------------------------------------
/infogami/core/macros/TypeChanger.html:
--------------------------------------------------------------------------------
1 | $def with (type, usetable=0)
2 |
3 | $ _ = i18n.get_namespace('/mode/edit')
4 |
5 |
11 |
12 | $if usetable:
13 |
14 | $_.page_type
15 |
16 | $:thinginput(type, name="type.key", expected_type="/type/type", kind="regular")
17 |
18 |
19 |
20 | $else:
21 | $_.page_type $:thinginput(type, name="type.key", expected_type="/type/type", kind="regular")
22 |
23 |
24 |
--------------------------------------------------------------------------------
/infogami/core/templates/type/page/edit.html:
--------------------------------------------------------------------------------
1 | $def with (page)
2 |
3 | $ _e = i18n.get_namespace('/mode/edit')
4 | $ _t = i18n.get_namespace('/type/page')
5 |
6 | $var title: $_e.edit_title(page.title)
7 |
8 |
9 |
26 |
--------------------------------------------------------------------------------
/infogami/plugins/links/db.py:
--------------------------------------------------------------------------------
1 | from infogami import tdb
2 | from infogami.core import db
3 |
4 |
5 | def get_links_type():
6 | linkstype = db.get_type('links') or db.new_type('links')
7 | linkstype.save()
8 | return linkstype
9 |
10 |
11 | def new_links(page, links):
12 | # for links thing: parent=page, type=linkstype
13 | site = page.parent
14 | path = page.name
15 | d = {'site': site, 'path': path, 'links': list(links)}
16 |
17 | try:
18 | backlinks = tdb.withName("links", page)
19 | backlinks.setdata(d)
20 | backlinks.save()
21 | except tdb.NotFound:
22 | backlinks = tdb.new("links", page, get_links_type(), d)
23 | backlinks.save()
24 |
25 |
26 | def get_links(site, path):
27 | return tdb.Things(type=get_links_type(), site=site, links=path)
28 |
--------------------------------------------------------------------------------
/infogami/plugins/wikitemplates/templates/type/template/edit.html:
--------------------------------------------------------------------------------
1 | $def with (page)
2 |
3 | $ _e = i18n.get_namespace('/mode/edit')
4 | $ _t = i18n.get_namespace('/type/template')
5 |
6 | $var title: $_e.edit_title(page.name)
7 |
8 |
9 |
10 |
11 | $:macros.TypeChanger(page.type)
12 |
13 | $_t.title
14 | $page.body
15 |
16 | $:_e.edit_summary
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/infogami/utils/context.py:
--------------------------------------------------------------------------------
1 | """
2 | Threaded context for infogami.
3 | """
4 |
5 | import web
6 |
7 | # Placeholder for keeping context defaults. This is populated by
8 | # the app on startup.
9 | defaults = web.storage()
10 |
11 |
12 | class InfogamiContext(web.ThreadedDict):
13 | """
14 | Threaded context for infogami.
15 | Uses web.ctx for providing a thread-specific context for infogami.
16 | """
17 |
18 | def load(self):
19 | self.update(defaults)
20 |
21 | def __getattr__(self, name):
22 | # In some error conditions, context is not initialzied.
23 | # Using the default as fallback.
24 | try:
25 | return web.ThreadedDict.__getattr__(self, name)
26 | except AttributeError:
27 | return getattr(defaults, name)
28 |
29 |
30 | context = InfogamiContext()
31 |
--------------------------------------------------------------------------------
/infogami/core/templates/default_view.html:
--------------------------------------------------------------------------------
1 | $def with (page)
2 |
3 | $var title: $page.name
4 |
5 |
6 |
7 |
8 | $for p in page.type.properties:
9 | $ label = i18n.get(page.type.key, p.name)
10 | $if p.unique:
11 |
12 | $label
13 | $:thingrepr(page[p.name], p.expected_type)
14 |
15 | $else:
16 | $for x in page[p.name]:
17 |
18 | $label
19 | $:thingrepr(x, p.expected_type)
20 |
21 |
22 | $for p in page.type.backreferences:
23 | $ label = i18n.get(page.type.key, p.name)
24 | $for x in page[p.name]:
25 |
26 | $label
27 | $:thingrepr(x, p.expected_type)
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/infogami/config.py:
--------------------------------------------------------------------------------
1 | """
2 | Infogami configuration.
3 | """
4 |
5 |
6 | def get(name, default=None):
7 | return globals().get(name, default)
8 |
9 |
10 | middleware = [] # type: ignore
11 |
12 | cache_templates = True
13 | db_printing = False
14 | db_kind = 'SQL'
15 |
16 | db_parameters: dict[str, str] | None = None
17 | infobase_host = None
18 | site = "infogami.org"
19 |
20 | plugins = ['links']
21 | plugin_modules = [] # type: ignore
22 |
23 | plugin_path = ['infogami.plugins']
24 |
25 | # key for encrypting password
26 | encryption_key = "ofu889e4i5kfem"
27 |
28 | # salt added to password before encrypting
29 | password_salt = "zxps#2s4g@z"
30 |
31 | from_address = "noreply@infogami.org"
32 | smtp_server = "localhost"
33 |
34 | login_cookie_name = "infogami_session"
35 |
36 | infobase_parameters = dict(type='local')
37 | bugfixer = None
38 |
39 | admin_password = "admin123"
40 |
--------------------------------------------------------------------------------
/.github/workflows/openlibrary_tests.yml:
--------------------------------------------------------------------------------
1 | name: openlibrary_tests
2 | on:
3 | push:
4 | branches: [ master ]
5 | pull_request:
6 | branches: [ master ]
7 | workflow_dispatch:
8 | jobs:
9 | openlibrary_tests:
10 | strategy:
11 | fail-fast: false
12 | matrix:
13 | python-version: ["3.12"]
14 | runs-on: ubuntu-latest
15 | steps:
16 | # - if: "contains(matrix.python-version, '-dev')"
17 | # run: sudo apt-get install -y libxml2 libxslt-dev
18 | - name: Checkout Open Library
19 | uses: actions/checkout@v3
20 | with:
21 | repository: internetarchive/openlibrary
22 | - name: Checkout current Infogami
23 | uses: actions/checkout@v3
24 | with:
25 | path: vendor/infogami
26 | - uses: actions/setup-python@v4
27 | with:
28 | python-version: ${{ matrix.python-version }}
29 | - run: pip install -r requirements_test.txt
30 | - run: make test-py
31 |
--------------------------------------------------------------------------------
/tests/test_doctests.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from web.test import doctest_suite
4 |
5 |
6 | def add_test(test):
7 | if isinstance(test, unittest.TestSuite):
8 | for t in test._tests:
9 | add_test(t)
10 | elif isinstance(test, unittest.TestCase):
11 | test_method = getattr(test, test._testMethodName)
12 |
13 | def do_test(test_method=test_method):
14 | test_method()
15 |
16 | name = "test_" + test.id().replace(".", "_")
17 | globals()[name] = do_test
18 |
19 |
20 | modules = [
21 | "infogami.core.code",
22 | "infogami.core.helpers",
23 | "infogami.utils.app",
24 | "infogami.utils.i18n",
25 | "infogami.utils.storage",
26 | "infogami.infobase.common",
27 | "infogami.infobase.client",
28 | "infogami.infobase.dbstore",
29 | "infogami.infobase.lru",
30 | "infogami.infobase.readquery",
31 | "infogami.infobase.utils",
32 | "infogami.infobase.writequery",
33 | ]
34 |
35 | suite = doctest_suite(modules)
36 |
37 | add_test(suite)
38 |
--------------------------------------------------------------------------------
/tests/test_infogami/test_account.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import web
3 |
4 | from infogami.utils.delegate import app
5 |
6 | b = app.browser()
7 |
8 |
9 | @pytest.mark.skip(
10 | reason="Browser test not currently functioning, requires BeautifulSoup and ClientForm, and site is still set to None"
11 | )
12 | def test_login():
13 | # try with bad account
14 | b.open('/account/login')
15 | b.select_form(name='login')
16 | b['username'] = 'joe'
17 | b['password'] = 'secret'
18 |
19 | try:
20 | b.submit()
21 | except web.BrowserError as e:
22 | assert str(e) == 'Invalid username or password'
23 | else:
24 | assert False, 'Expected exception'
25 |
26 | # follow register link
27 | b.follow_link(text='create a new account')
28 | assert b.path == '/account/register'
29 |
30 | b.select_form('register')
31 | b['username'] = 'joe'
32 | b['displayname'] = 'Joe'
33 | b['password'] = 'secret'
34 | b['password2'] = 'secret'
35 | b['email'] = 'joe@example.com'
36 | b.submit()
37 | assert b.path == '/'
38 |
--------------------------------------------------------------------------------
/infogami/core/macros/ThingReference.html:
--------------------------------------------------------------------------------
1 | $def with (type, name, value, property="key", limit=10)
2 |
3 | $add_javascript('/static/js/jquery/jquery.js')
4 | $add_javascript('/static/js/jquery/jquery.bgiframe.min.js')
5 | $add_javascript('/static/js/jquery/jquery.dimensions.pack.js')
6 | $add_javascript('/static/js/jquery/jquery.autocomplete.js')
7 | $add_javascript('/static/js/autocomplete.js')
8 |
9 | $add_stylesheet('/static/jquery.autocomplete.css')
10 |
11 | $if property == "key":
12 |
13 | $else:
14 |
15 |
16 |
17 |
24 |
--------------------------------------------------------------------------------
/infogami/infobase/tests/test_doctests.py:
--------------------------------------------------------------------------------
1 | import doctest
2 |
3 | import pytest
4 |
5 | modules = [
6 | "infogami.infobase.account",
7 | "infogami.infobase.bootstrap",
8 | "infogami.infobase.cache",
9 | "infogami.infobase.client",
10 | "infogami.infobase.common",
11 | "infogami.infobase.core",
12 | "infogami.infobase.dbstore",
13 | "infogami.infobase.infobase",
14 | "infogami.infobase.logger",
15 | "infogami.infobase.logreader",
16 | "infogami.infobase.lru",
17 | "infogami.infobase.readquery",
18 | "infogami.infobase.tests.pytest_wildcard",
19 | "infogami.infobase.utils",
20 | "infogami.infobase.writequery",
21 | ]
22 |
23 |
24 | @pytest.mark.parametrize('module', modules)
25 | def test_doctest(module):
26 | mod = __import__(module, None, None, ['x'])
27 | finder = doctest.DocTestFinder()
28 | tests = finder.find(mod, mod.__name__)
29 | for test in tests:
30 | runner = doctest.DocTestRunner(verbose=True)
31 | failures, tries = runner.run(test)
32 | if failures:
33 | pytest.fail("doctest failed: " + test.name)
34 |
--------------------------------------------------------------------------------
/scripts/pytests_failing_on_py3_only.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | FAILING_FILES=(
4 | infogami/core/code.py
5 | infogami/infobase/_dbstore/indexer.py
6 | infogami/infobase/common.py
7 | infogami/infobase/tests/__init__.py
8 | infogami/infobase/tests/test_account.py
9 | infogami/infobase/tests/test_client.py
10 | infogami/infobase/tests/test_doctests.py
11 | infogami/infobase/tests/test_infobase.py
12 | infogami/infobase/tests/test_read.py
13 | infogami/infobase/tests/test_save.py
14 | infogami/infobase/tests/test_seq.py
15 | infogami/infobase/tests/test_store.py
16 | infogami/infobase/tests/test_writequery.py
17 | infogami/utils/app.py
18 | infogami/utils/view.py
19 | test/test_dbstore.py
20 | test/test_doctests.py
21 | tests/__init__.py
22 | tests/test_doctests.py
23 | )
24 |
25 | for FILEPATH in "${FAILING_FILES[@]}"; do
26 | echo "<<< $FILEPATH >>>"
27 | pytest "$FILEPATH"
28 | # See TODO in test/test_dbstore.py
29 | if [ "$FILEPATH" != "test/test_dbstore.py" ]; then
30 | pytest --doctest-modules "$FILEPATH";
31 | fi
32 | done
33 |
--------------------------------------------------------------------------------
/infogami/infobase/config.py:
--------------------------------------------------------------------------------
1 | """Infobase configuration."""
2 |
3 | # IP address of machines which can be trusted for doing admin tasks
4 | trusted_machines = ["127.0.0.1"]
5 |
6 | # default size of cache
7 | cache_size = 1000
8 |
9 | secret_key = "bzuim9ws8u"
10 |
11 | # set this to log dir to enable logging
12 | logroot = None
13 | compress_log = False
14 |
15 | # query_timeout in milli seconds.
16 | query_timeout = "60000"
17 |
18 | user_root = "/user/"
19 |
20 | # @@ Hack to execute some code when infobase is created.
21 | # @@ This will be replaced with a better method soon.
22 | startup_hook = None
23 |
24 | bind_address = None
25 | port = 5964
26 | fastcgi = False
27 |
28 | # earlier there used to be a machine_comment column in version table.
29 | # Set this flag to True to continue to use that field in earlier installations.
30 | use_machine_comment = False
31 |
32 | # bot column is added transaction table to mark edits by bot. Flag to enable/disable this feature.
33 | use_bot_column = True
34 |
35 | verify_user_email = False
36 |
37 |
38 | def get(key, default=None):
39 | return globals().get(key, default)
40 |
--------------------------------------------------------------------------------
/infogami/infobase/tests/pytest_wildcard.py:
--------------------------------------------------------------------------------
1 | """py.test wildcard plugin.
2 | """
3 |
4 | import pytest
5 |
6 |
7 | class Wildcard:
8 | """Wildcard object is equal to anything.
9 |
10 | Useful to compare datastructures which contain some random numbers or db sequences.
11 |
12 | >>> import random
13 | >>> assert [random.random(), 1, 2] == [Wildcard(), 1, 2]
14 | """
15 |
16 | def __eq__(self, other):
17 | return True
18 |
19 | def __ne__(self, other):
20 | return False
21 |
22 | def __repr__(self):
23 | return '>'
24 |
25 |
26 | def test_wildcard():
27 | wildcard = Wildcard()
28 | assert wildcard == 1
29 | assert wildcard == [1, 2, 3]
30 | assert 1 == wildcard
31 | assert ["foo", 1, 2] == [wildcard, 1, 2]
32 |
33 |
34 | @pytest.fixture()
35 | def wildcard(request):
36 | """Returns the wildcard object.
37 |
38 | Wildcard object is equal to anything. It is useful in testing datastuctures with some random parts.
39 |
40 | >>> import random
41 | >>> assert [random.random(), 1, 2] == [Wildcard(), 1, 2]
42 | """
43 | return Wildcard()
44 |
--------------------------------------------------------------------------------
/infogami/plugins/i18n/templates/i18n.html:
--------------------------------------------------------------------------------
1 | $def with (namespace, lang, page)
2 |
3 |
14 |
15 |
16 | Language: $:Dropdown('lang', _.get_languages(), onchange="changelang();", value=lang).render()
17 |
18 |
19 |
20 |
21 |
22 |
23 | $for ns in _.get_namespaces():
24 | $(ns or "root") $(_.get_count(ns, lang))/$_.get_count(ns)
25 |
26 |
27 | $if page:
28 | $page.key
29 |
30 | $:thingview(page)
31 | $else:
32 | $ name = homepath() + "/i18n/" + namespace + "/strings." + lang
33 | $:name
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/infogami/utils/tests/test_app.py:
--------------------------------------------------------------------------------
1 | from infogami.utils import app
2 |
3 |
4 | def test_parse_accept():
5 | # testing examples from http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
6 | assert app.parse_accept("audio/*; q=0.2, audio/basic") == [
7 | {"media_type": "audio/basic"},
8 | {"media_type": "audio/*", "q": 0.2},
9 | ]
10 |
11 | assert app.parse_accept(
12 | "text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"
13 | ) == [
14 | {"media_type": "text/html"},
15 | {"media_type": "text/x-c"},
16 | {"media_type": "text/x-dvi", "q": 0.8},
17 | {"media_type": "text/plain", "q": 0.5},
18 | ]
19 |
20 | # try empty
21 | assert app.parse_accept("") == [{'media_type': ''}]
22 | assert app.parse_accept(" ") == [{'media_type': ''}]
23 | assert app.parse_accept(",") == [{'media_type': ''}, {'media_type': ''}]
24 |
25 | # try some bad ones
26 | assert app.parse_accept("hc/url;*/*") == [{"media_type": "hc/url"}]
27 | assert app.parse_accept("text/plain;q=bad") == [{"media_type": "text/plain"}]
28 |
29 | assert app.parse_accept(";q=1") == [{"media_type": "", "q": 1.0}]
30 |
--------------------------------------------------------------------------------
/scripts/run_doctests.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # USER=openlibrary@example.com pytest --doctest-modules || true
4 |
5 | USER=openlibrary@example.com pytest --doctest-modules \
6 | --ignore=infogami/core/dbupgrade.py \
7 | --ignore=infogami/infobase/_json.py \
8 | --ignore=infogami/infobase/bulkupload.py \
9 | --ignore=infogami/infobase/tests/test_account.py \
10 | --ignore=infogami/infobase/tests/test_client.py \
11 | --ignore=infogami/infobase/tests/test_infobase.py \
12 | --ignore=infogami/infobase/tests/test_read.py \
13 | --ignore=infogami/infobase/tests/test_save.py \
14 | --ignore=infogami/infobase/tests/test_seq.py \
15 | --ignore=infogami/infobase/tests/test_store.py \
16 | --ignore=infogami/infobase/tests/test_writequery.py \
17 | --ignore=infogami/plugins/i18n/code.py \
18 | --ignore=infogami/plugins/links/db.py \
19 | --ignore=infogami/plugins/pages/code.py \
20 | --ignore=infogami/plugins/review/code.py \
21 | --ignore=infogami/plugins/review/db.py \
22 | --ignore=infogami/plugins/wikitemplates/code.py \
23 | --ignore=migration/migrate-0.4-0.5.py \
24 | --ignore=test/bug_239238.py \
25 | --ignore=test/test_dbstore.py
26 |
--------------------------------------------------------------------------------
/sample_run.py:
--------------------------------------------------------------------------------
1 | """
2 | Sample run.py
3 | """
4 |
5 | import infogami
6 |
7 | # your db parameters
8 | infogami.config.db_parameters = dict(
9 | dbn='postgres', db="infogami", user='yourname', pw=''
10 | )
11 |
12 | # site name
13 | infogami.config.site = 'infogami.org'
14 | infogami.config.admin_password = "admin123"
15 |
16 | # add additional plugins and plugin path
17 | # infogami.config.plugin_path += ['plugins']
18 | # infogami.config.plugins += ['search']
19 |
20 |
21 | def createsite():
22 | import web
23 |
24 | from infogami.infobase import config, dbstore, infobase, server
25 |
26 | web.config.db_parameters = infogami.config.db_parameters
27 | web.config.db_printing = True
28 | web.ctx.ip = '127.0.0.1'
29 |
30 | server.app.request('/')
31 | schema = dbstore.Schema()
32 | store = dbstore.DBStore(schema)
33 | ib = infobase.Infobase(store, config.secret_key)
34 | ib.create(infogami.config.site)
35 |
36 |
37 | if __name__ == "__main__":
38 | import sys
39 |
40 | if '--schema' in sys.argv:
41 | from infogami.infobase.dbstore import Schema
42 |
43 | print(Schema().sql())
44 | elif '--createsite' in sys.argv:
45 | createsite()
46 | else:
47 | infogami.run()
48 |
--------------------------------------------------------------------------------
/infogami/core/files/js/autocomplete.js:
--------------------------------------------------------------------------------
1 |
2 | function setup_autocomplete(e) {
3 | var e = $(e);
4 |
5 | var name = e.attr('ac_name');
6 | var type = e.attr('ac_type');
7 | var property = e.attr('ac_property');
8 | var limit = e.attr("ac_limit");
9 |
10 | e.autocomplete("/getthings", {
11 | extraParams: {
12 | type: type,
13 | property: property
14 | },
15 | matchCase: true,
16 | max: limit,
17 | formatItem: function (row) {
18 | if (property == "key")
19 | return row[0];
20 | else
21 | return "" + row[0] + "
" + row[1] + "
";
22 | }
23 | })
24 | .result(function(event, data, formatted) {
25 | var name = $(this).attr('ac_name');
26 | if ($(this).attr("ac_property") != "key")
27 | $(document.getElementById('result_' + name)).val(data[1])
28 | })
29 | .change(function() {
30 | // When user selects empty string, set the result to empty
31 | var name = $(this).attr('ac_name');
32 | if ($(this).attr("ac_property") != "key")
33 | $(document.getElementById("result_" + name)).val("");
34 | });
35 | }
36 |
--------------------------------------------------------------------------------
/infogami/core/macros/RecentChanges.html:
--------------------------------------------------------------------------------
1 | $def with (author=None, type=None, limit=50)
2 |
3 | $ page = safeint(query_param('page', '0'))
4 | $ ip = query_param('ip', None)
5 | $ type = type or query_param('type', None)
6 | $ changes = get_recent_changes(author=author, ip=ip, type=type, limit=limit, offset=page * limit)
7 |
8 |
9 |
10 | $var title: $_.RECENT_CHANGES
11 |
12 |
13 |
14 | $_.WHEN
15 | $_.PATH
16 | $_.WHO
17 | $_.WHAT
18 | $_.ACTIONS
19 |
20 |
21 | $for v in changes:
22 |
23 | $datestr(v.created)
24 | $v.key
25 | $if v.author:
26 | $v.author.displayname
27 | $else:
28 | $v.ip
29 | $v.comment
30 |
31 | $_.VIEW
32 | $_.EDIT
33 | $_.DIFF
34 |
35 |
36 |
37 |
38 |
39 | $if page != 0:
40 |
Newer
41 |
42 | $if len(changes) == limit:
43 |
Older
44 |
45 |
46 |
--------------------------------------------------------------------------------
/infogami/infobase/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Infobase.
3 | """
4 |
5 | import sys
6 |
7 | import web
8 |
9 | from infogami.infobase import infobase, server
10 |
11 | commands = {}
12 |
13 |
14 | def command(f):
15 | commands[f.__name__] = f
16 | return f
17 |
18 |
19 | @command
20 | def help():
21 | """Prints this help."""
22 | print("Infobase help\n\nCommands:\n")
23 | for name, c in commands.items():
24 | print("%-20s %s" % (name, c.__doc__))
25 |
26 |
27 | @command
28 | def createsite(sitename, admin_password):
29 | """Creates a new site. Takes 2 arguments sitename and admin_password."""
30 | web.load()
31 | infobase.Infobase().create_site(sitename, admin_password)
32 |
33 |
34 | @command
35 | def startserver(*args):
36 | """Starts the infobase server at port 8080. An optional port argument can be specified to run the server at a different port."""
37 | sys.argv = [sys.argv[0]] + list(args)
38 | server.run()
39 |
40 |
41 | def run():
42 | action = sys.argv[1] if len(sys.argv) > 1 else 'startserver'
43 | return commands[action](*sys.argv[2:])
44 |
45 |
46 | if __name__ == "__main__":
47 | import os
48 |
49 | dbname = os.environ.get('INFOBASE_DB', 'infobase')
50 | web.config.db_printing = True
51 | web.config.db_parameters = dict(dbn='postgres', db=dbname)
52 | run()
53 |
--------------------------------------------------------------------------------
/infogami/core/templates/feed.html:
--------------------------------------------------------------------------------
1 | $def with (site, changes)
2 |
3 |
4 |
5 |
6 | Infogami - Recent changes
7 | $site
8 |
9 | Track the most recent changes to $site in this feed.
10 | $changes[0].created
11 | infogami
12 | $for v in changes:
13 | -
14 |
$v.key
15 | $site$v.key?m=diff&b=$v.revision
16 | $site$v.key?m=diff&b=$v.revision
17 |
18 | Path: $v.key <br/>
19 | Comment: $v.comment <br/>
20 | $if v.author:
21 | Author: $v.author.displayname
22 | $else:
23 | Author: $v.ip
24 | <br/>
25 | $v.diff
26 |
27 | $v.created
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/infogami/utils/flash.py:
--------------------------------------------------------------------------------
1 | """Utility to display flash messages.
2 |
3 | To add a flash message:
4 |
5 | add_flash_message('info', 'Login successful!')
6 |
7 | To display flash messages in a template:
8 |
9 | $ for flash in get_flash_messages():
10 |
$flash.message
11 | """
12 |
13 | import json
14 |
15 | import web
16 |
17 |
18 | def get_flash_messages():
19 | flash = web.ctx.get('flash', [])
20 | web.ctx.flash = []
21 | return flash
22 |
23 |
24 | def add_flash_message(type, message):
25 | flash = web.ctx.setdefault('flash', [])
26 | flash.append(web.storage(type=type, message=message))
27 |
28 |
29 | def flash_processor(handler):
30 | flash = web.cookies(flash="[]").flash
31 | try:
32 | flash = [
33 | web.storage(d)
34 | for d in json.loads(flash)
35 | if isinstance(d, dict) and 'type' in d and 'message' in d
36 | ]
37 | except ValueError:
38 | flash = []
39 |
40 | web.ctx.flash = list(flash)
41 |
42 | try:
43 | return handler()
44 | finally:
45 | # Flash changed. Need to save it.
46 | if flash != web.ctx.flash:
47 | if web.ctx.flash:
48 | web.setcookie('flash', json.dumps(web.ctx.flash))
49 | else:
50 | web.setcookie('flash', '', expires=-1)
51 |
--------------------------------------------------------------------------------
/infogami/core/files/js/jquery/jquery.bgiframe.min.js:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2006 Brandon Aaron (http://brandonaaron.net)
2 | * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
3 | * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
4 | *
5 | * $LastChangedDate: 2007-03-19 16:02:41 +0100 (Mo, 19 Mrz 2007) $
6 | * $Rev: 1546 $
7 | */
8 | (function($){$.fn.bgIframe=jQuery.fn.bgiframe=function(s){if(!($.browser.msie&&typeof XMLHttpRequest=='function'))return this;s=$.extend({top:'auto',left:'auto',width:'auto',height:'auto',opacity:true,src:'javascript:false;'},s||{});var prop=function(n){return n&&n.constructor==Number?n+'px':n;},html='
';return this.each(function(){if(!$('iframe.bgiframe',this)[0])this.insertBefore(document.createElement(html),this.firstChild);});};})(jQuery);
--------------------------------------------------------------------------------
/infogami/core/templates/diff.html:
--------------------------------------------------------------------------------
1 | $def with (a, b)
2 |
3 | $ _t = i18n.get_namespace(a.type.name)
4 | $ _ = i18n.get_namespace('/mode/diff')
5 |
6 |
7 | $add_stylesheet('/static/diff.css')
8 |
9 | $var title: $_.diff_title(a.name)
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | $for p in set(a.keys() + b.keys()):
26 | $ label = _t[p]
27 | $:thingdiff(get_expected_type(a, p), label, a[p], b[p])
28 |
29 |
30 |
31 |
32 |
33 |
$_.legend:
34 |
35 | $_.unmodified
36 | $_.added
37 | $_.removed
38 | $_.modified
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/infogami/plugins/i18n/templates/type/i18n/edit.html:
--------------------------------------------------------------------------------
1 | $def with (page)
2 |
3 | $var title: $_.get('/mode/edit', 'edit_title')(page.key)
4 |
5 |
6 |
7 | $:macros.TypeChanger(page.type)
8 |
9 |
10 |
16 |
17 |
29 |
30 |
31 | $_.get('/type/i18n', 'add_new_key')
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/test/bug_239238.py:
--------------------------------------------------------------------------------
1 | """Bug#239238
2 |
3 | https://bugs.launchpad.net/infogami/+bug/239238
4 |
5 | Change author of a book from a1 to a2.
6 | That book is not listed in a2.books.
7 | """
8 |
9 | from infogami.infobase import client
10 |
11 | from . import webtest
12 | from .test_infobase import InfobaseTestCase
13 |
14 |
15 | class Test(InfobaseTestCase):
16 | def create_site(self, name='test'):
17 | conn = client.connect(type='local')
18 | return client.Site(conn, 'test')
19 |
20 | def testBug(self):
21 | self.create_book_author_types()
22 |
23 | self.new('/a/a1', '/type/author')
24 | self.new('/a/a2', '/type/author')
25 | self.new('/b/b1', '/type/book', author='/a/a1')
26 |
27 | site = self.create_site()
28 | a1 = site.get('/a/a1')
29 | a2 = site.get('/a/a2')
30 |
31 | def keys(things):
32 | return [t.key for t in things]
33 |
34 | assert keys(a1.books) == ['/b/b1']
35 | assert keys(a2.books) == []
36 |
37 | site.write(
38 | {
39 | 'key': '/b/b1',
40 | 'author': {
41 | 'connect': 'update',
42 | 'key': '/a/a2',
43 | },
44 | }
45 | )
46 |
47 | site = self.create_site()
48 | a1 = site.get('/a/a1')
49 | a2 = site.get('/a/a2')
50 |
51 | assert keys(a1.books) == []
52 | assert keys(a2.books) == ['/b/b1']
53 |
54 |
55 | if __name__ == "__main__":
56 | webtest.main()
57 |
--------------------------------------------------------------------------------
/infogami/utils/stats.py:
--------------------------------------------------------------------------------
1 | """Library to collect count and timings of various part of code.
2 |
3 | Here is an example usage:
4 |
5 | stats.begin("memcache", method="get", key="foo")
6 | memcache_client.get("foo")
7 | stats.end()
8 |
9 | Currently this doesn't support nesting.
10 | """
11 |
12 | import time
13 |
14 | import web
15 |
16 | from infogami.utils.context import context
17 |
18 |
19 | def _get_stats():
20 | if "stats" not in web.ctx:
21 | context.stats = web.ctx.stats = []
22 | return web.ctx.stats
23 |
24 |
25 | def begin(name, **kw):
26 | stats = _get_stats()
27 | stats.append(web.storage(name=name, data=kw, t_start=time.time(), time=0.0))
28 |
29 |
30 | def end(**kw):
31 | stats = _get_stats()
32 | s = stats[-1]
33 |
34 | s.data.update(kw)
35 | s.t_end = time.time()
36 | s.time = s.t_end - s.t_start
37 |
38 |
39 | def stats_summary():
40 | d = web.storage()
41 |
42 | if not web.ctx.get("stats"):
43 | return d
44 |
45 | total_measured = 0.0
46 |
47 | for s in web.ctx.stats:
48 | if s.name not in d:
49 | d[s.name] = web.storage(count=0, time=0.0)
50 | d[s.name].count += 1
51 | d[s.name].time += s.time
52 | total_measured += s.time
53 |
54 | # consider the start time of first stat as start of the request
55 | total_time = time.time() - web.ctx.stats[0].t_start
56 | d['total'] = web.storage(
57 | count=0, time=total_time, unaccounted=total_time - total_measured
58 | )
59 |
60 | return d
61 |
--------------------------------------------------------------------------------
/infogami/core/files/js/repetition/repetition-model-wrapper.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Wrapper for Web Forms 2.0 Repetition Model Cross-browser Implementation
3 | * Copyright: 2007, Weston Ruter
4 | * License: http://creativecommons.org/licenses/LGPL/2.1/
5 | *
6 | * The comments contained in this code are largely quotations from the
7 | * WebForms 2.0 specification:
8 | *
9 | * Usage:
10 | */
11 |
12 | if(!window.RepetitionElement || (
13 | document.implementation && document.implementation.hasFeature &&
14 | !document.implementation.hasFeature("WebForms", "2.0")
15 | )){
16 | //get path to source directory
17 | var scripts = document.getElementsByTagName('head')[0].getElementsByTagName('script'), match, dirname = '';
18 | for(var i = 0; i < scripts.length; i++){
19 | if(match = scripts[i].src.match(/^(.*)repetition-model-wrapper\.js$/))
20 | dirname = match[1];
21 | }
22 |
23 | //load script
24 | if(document.write)
25 | document.write("");
26 | else {
27 | var script = document.createElement('script');
28 | script.setAttribute('type', 'text/javascript');
29 | script.setAttribute('src', dirname + 'repetition-model-p.js');
30 | script.setAttribute('language', 'JavaScript');
31 | document.getElementsByTagName('head')[0].appendChild(script);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/infogami/core/templates/history.html:
--------------------------------------------------------------------------------
1 | $def with (page, h)
2 |
3 | $ _ = i18n.get_namespace('/mode/history')
4 |
5 | $var title: $_.history
6 |
7 |
11 |
12 |
13 |
38 |
39 |
40 |
41 |
42 |
56 |
57 |
--------------------------------------------------------------------------------
/infogami/infobase/_json.py:
--------------------------------------------------------------------------------
1 | r"""
2 | Wrapper to simplejson to fix unicode/utf-8 issues in python 2.4.
3 |
4 | See Bug#231831 for details.
5 |
6 |
7 | >>> loads(dumps(u'\u1234'))
8 | u'\u1234'
9 | >>> loads(dumps(u'\u1234'.encode('utf-8')))
10 | u'\u1234'
11 | >>> loads(dumps({u'x': u'\u1234'.encode('utf-8')}))
12 | {u'x': u'\u1234'}
13 | """
14 |
15 | import datetime
16 |
17 | import simplejson
18 |
19 |
20 | def unicodify(d):
21 | """Converts all utf-8 encoded strings to unicode recursively."""
22 | if isinstance(d, dict):
23 | return {k: unicodify(v) for k, v in d.items()}
24 | elif isinstance(d, list):
25 | return [unicodify(x) for x in d]
26 | elif isinstance(d, bytes):
27 | return d.decode('utf-8')
28 | elif isinstance(d, datetime.datetime):
29 | return d.isoformat()
30 | else:
31 | return d
32 |
33 |
34 | class JSONEncoder(simplejson.JSONEncoder):
35 | def default(self, o):
36 | if hasattr(o, '__json__'):
37 | return simplejson.loads(o.__json__())
38 | else:
39 | return simplejson.JSONEncoder.default(self, o)
40 |
41 |
42 | def dumps(obj, **kw):
43 | """
44 | >>> class Foo:
45 | ... def __json__(self): return 'foo'
46 | ...
47 | >>> a = [Foo(), Foo()]
48 | >>> dumps(a)
49 | '[foo, foo]'
50 | """
51 | return simplejson.dumps(unicodify(obj), cls=JSONEncoder, **kw)
52 |
53 |
54 | def loads(s, **kw):
55 | return simplejson.loads(s, **kw)
56 |
57 |
58 | if __name__ == "__main__":
59 | import doctest
60 |
61 | doctest.testmod()
62 |
--------------------------------------------------------------------------------
/tests/test_infogami/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import web
4 |
5 | import infogami
6 | from infogami.infobase import server
7 | from infogami.utils.delegate import app
8 |
9 | # overwrite _cleanup to stop clearing thread state between requests
10 | app._cleanup = lambda *a: None
11 |
12 | db_parameters = dict(dbn="postgres", db="infogami_test", user=os.environ["USER"], pw="")
13 |
14 |
15 | def setup_module(module):
16 | monkey_patch_browser()
17 |
18 | infogami.config.site = "infogami.org"
19 | infogami.config.db_parameters = web.config.db_parameters = db_parameters
20 |
21 | server.get_site("infogami.org") # to initialize db
22 |
23 | module.db = server._infobase.store.db
24 | module.db.printing = False
25 | module.t = module.db.transaction()
26 |
27 | infogami._setup()
28 |
29 |
30 | def teardown_module(module):
31 | module.t.rollback()
32 |
33 |
34 | def monkey_patch_browser():
35 | def check_errors(self):
36 | errors = [
37 | self.get_text(e)
38 | for e in self.get_soup().findAll(attrs={'id': 'error'})
39 | + self.get_soup().findAll(attrs={'class': 'wrong'})
40 | ]
41 | if errors:
42 | raise web.BrowserError(errors[0])
43 |
44 | _do_request = web.AppBrowser.do_request
45 |
46 | def do_request(self, req):
47 | response = _do_request(self, req)
48 | if self.status != 200:
49 | raise web.BrowserError(str(self.status))
50 | self.check_errors()
51 | return response
52 |
53 | web.AppBrowser.do_request = do_request
54 | web.AppBrowser.check_errors = check_errors
55 |
--------------------------------------------------------------------------------
/infogami/core/diff.py:
--------------------------------------------------------------------------------
1 | from difflib import SequenceMatcher
2 |
3 | import web
4 |
5 |
6 | def better_diff(a, b):
7 | labels = dict(equal="", insert='add', replace='mod', delete='rem')
8 |
9 | map = []
10 | for tag, i1, i2, j1, j2 in SequenceMatcher(a=a, b=b).get_opcodes():
11 | n = (j2 - j1) - (i2 - i1)
12 |
13 | x = a[i1:i2]
14 | xn = list(range(i1, i2))
15 | y = b[j1:j2]
16 | yn = list(range(j1, j2))
17 |
18 | if tag == 'insert':
19 | x += [''] * n
20 | xn += [''] * n
21 | elif tag == 'delete':
22 | y += [''] * -n
23 | yn += [''] * -n
24 | elif tag == 'equal':
25 | if i2 - i1 > 5:
26 | x = y = [a[i1], '', a[i2 - 1]]
27 | xn = yn = [i1, '...', i2 - 1]
28 | elif tag == 'replace':
29 | isize = i2 - i1
30 | jsize = j2 - j1
31 |
32 | if isize < jsize:
33 | x += [''] * (jsize - isize)
34 | xn += [''] * (jsize - isize)
35 | else:
36 | y += [''] * (isize - jsize)
37 | yn += [''] * (isize - jsize)
38 |
39 | map += zip([labels[tag]] * len(x), xn, x, yn, y)
40 |
41 | return map
42 |
43 |
44 | def simple_diff(a, b):
45 | a = a or ''
46 | b = b or ''
47 | if a is None:
48 | a = ''
49 | if b is None:
50 | b = ''
51 | a = web.safestr(a).split(' ')
52 | b = web.safestr(b).split(' ')
53 | out = []
54 | for tag, i1, i2, j1, j2 in SequenceMatcher(a=a, b=b).get_opcodes():
55 | out.append(
56 | web.storage(tag=tag, left=' '.join(a[i1:i2]), right=' '.join(b[j1:j2]))
57 | )
58 | return out
59 |
--------------------------------------------------------------------------------
/.github/workflows/python_tests.yml:
--------------------------------------------------------------------------------
1 | # https://docs.github.com/en/actions/guides/creating-postgresql-service-containers
2 | name: python_tests
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 | workflow_dispatch:
9 | jobs:
10 | python_tests:
11 | strategy:
12 | fail-fast: false
13 | matrix:
14 | python-version: ["3.12"]
15 | runs-on: ubuntu-latest
16 | services:
17 | # Label does not set the Postgres host name which will be localhost
18 | postgres:
19 | # Docker Hub image
20 | image: postgres
21 | # Provide the password for postgres
22 | env:
23 | POSTGRES_USER: runner
24 | POSTGRES_HOST_AUTH_METHOD: trust
25 | # Set health checks to wait until postgres has started
26 | options: >-
27 | --health-cmd pg_isready
28 | --health-interval 10s
29 | --health-timeout 5s
30 | --health-retries 5
31 | ports:
32 | # Map TCP port 5432 on service container to the host
33 | - 5432:5432
34 | steps:
35 | - if: "contains(matrix.python-version, '-dev')"
36 | run: sudo apt-get install -y libxml2 libxslt-dev
37 | - uses: actions/checkout@v3
38 | - uses: actions/setup-python@v4
39 | with:
40 | python-version: ${{ matrix.python-version }}
41 | - uses: actions/cache@v3
42 | with:
43 | path: ${{ env.pythonLocation }}
44 | key: ${{ runner.os }}-venv-${{ env.pythonLocation }}-${{ hashFiles('requirements*.txt') }}
45 | - name: Install dependencies
46 | run: |
47 | pip install --upgrade pip
48 | pip install -r requirements_test.txt
49 | - run: scripts/run_python_linters.sh
50 | - run: scripts/run_python_tests.sh
51 | - run: scripts/run_doctests.sh
52 |
--------------------------------------------------------------------------------
/infogami/infobase/_dbstore/sequence.py:
--------------------------------------------------------------------------------
1 | """High-level sequence API.
2 | """
3 |
4 |
5 | class SequenceImpl:
6 | def __init__(self, db):
7 | self.db = db
8 | self.listener = None
9 |
10 | def set_listener(self, f):
11 | self.listener = f
12 |
13 | def fire_event(self, event_name, name, value):
14 | self.listener and self.listener("seq.set", {"name": name, "value": value})
15 |
16 | def get_value(self, name):
17 | try:
18 | return self.db.query("SELECT * FROM seq WHERE name=$name", vars=locals())[
19 | 0
20 | ].value
21 | except IndexError:
22 | return 0
23 |
24 | def next_value(self, name, increment=1):
25 | try:
26 | tx = self.db.transaction()
27 | d = self.db.query(
28 | "SELECT * FROM seq WHERE name=$name FOR UPDATE", vars=locals()
29 | )
30 | if d:
31 | value = d[0].value + 1
32 | self.db.update("seq", value=value, where="name=$name", vars=locals())
33 | else:
34 | value = 1
35 | self.db.insert("seq", name=name, value=value)
36 | except Exception:
37 | tx.rollback()
38 | raise
39 | else:
40 | tx.commit()
41 | return value
42 |
43 | def set_value(self, name, value):
44 | try:
45 | tx = self.db.transaction()
46 | d = self.db.query(
47 | "SELECT * FROM seq WHERE name=$name FOR UPDATE", vars=locals()
48 | )
49 | if d:
50 | self.db.update("seq", value=value, where="name=$name", vars=locals())
51 | else:
52 | self.db.insert("seq", name=name, value=value)
53 | except Exception:
54 | tx.rollback()
55 | raise
56 | else:
57 | tx.commit()
58 | return value
59 |
--------------------------------------------------------------------------------
/infogami/core/templates/site.html:
--------------------------------------------------------------------------------
1 | $def with (page)
2 |
3 |
4 |
5 |
6 | $page.title ($_.site_title)
7 |
8 |
9 | $for s in ctx.stylesheets:
10 |
11 |
12 | $for jsurl in ctx.javascripts:
13 |
14 |
15 |
16 |
17 |
18 |
41 |
42 |
43 |
44 | $for flash in get_flash_messages():
45 |
$flash.message
46 |
47 |
48 |
$page.title
49 |
50 | $:page
51 |
52 |
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/scripts/test:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import os
4 | import os.path
5 | import sys
6 |
7 | sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
8 |
9 | import subcommand
10 | import web.test
11 |
12 |
13 | @subcommand.subcommand()
14 | def doctests(options=None):
15 | """Run all doctests."""
16 | print("\nRunning doctests\n", file=sys.stderr)
17 |
18 | modules = [
19 | "infogami.infobase.common",
20 | "infogami.infobase.dbstore",
21 | "infogami.infobase.lru",
22 | "infogami.infobase.readquery",
23 | "infogami.infobase.writequery",
24 | "infogami.utils.delegate", # required for monkey-patching
25 | "infogami.utils.i18n",
26 | "infogami.utils.storage",
27 | ]
28 | suite = web.test.doctest_suite(modules)
29 | web.test.runTests(suite)
30 |
31 |
32 | @subcommand.subcommand()
33 | def infobase(options=None, *args):
34 | """Test Infobase.
35 |
36 | Options:
37 | -d [--database] database : database name (default: test_infobase)
38 | -u [--url] url : url of infobase to test; tested locally if not provided.
39 | """
40 | print("\nRunning infobase tests\n", file=sys.stderr)
41 |
42 | options = options or web.storage(database='test_infobase')
43 | db = options.database or 'test_infobase'
44 | web.config.test_url = options.__dict__.get('url')
45 | web.config.debug = False
46 |
47 | user = os.environ['USER']
48 | web.config.db_parameters = dict(dbn='postgres', db=db, user=user, pw='')
49 |
50 | from test import test_infobase
51 |
52 | suite = web.test.module_suite(test_infobase, args or None)
53 | web.test.runTests(suite)
54 |
55 |
56 | @subcommand.subcommand()
57 | def alltests(options=None):
58 | """Run all tests."""
59 | doctests([])
60 | infobase([])
61 |
62 |
63 | @subcommand.subcommand()
64 | def schema(options=None):
65 | """Print db schema."""
66 | from infogami.infobase import dbstore
67 |
68 | print(dbstore.Schema().sql())
69 |
70 |
71 | if __name__ == "__main__":
72 | subcommand.main()
73 |
--------------------------------------------------------------------------------
/infogami/plugins/review/db.py:
--------------------------------------------------------------------------------
1 | import web
2 |
3 | from infogami import core
4 |
5 |
6 | class SQL:
7 | def get_modified_pages(self, url, user_id):
8 | site_id = core.db.get_site_id(url)
9 |
10 | # @@ improve later
11 | d = web.query(
12 | """
13 | SELECT
14 | page.id as id,
15 | page.path as path,
16 | MAX(version.revision) as revision,
17 | MAX(review.revision) as reviewed_revision
18 | FROM page
19 | JOIN version ON page.id = version.page_id
20 | LEFT OUTER JOIN review
21 | ON page.id = review.page_id
22 | AND review.user_id=$user_id
23 | GROUP BY page.id, page.path
24 | """,
25 | vars=locals(),
26 | )
27 |
28 | d = [
29 | p for p in d if not p.reviewed_revision or p.revision > p.reviewed_revision
30 | ]
31 | return d
32 |
33 | def approve(self, url, user_id, path, revision):
34 | site_id = core.db.get_site_id(url)
35 | page_id = core.db.get_page_id(url, path)
36 |
37 | # @@ is there any better way?
38 | web.transact()
39 | try:
40 | web.delete(
41 | "review",
42 | where="site_id=$site_id AND page_id=$page_id AND user_id=$user_id",
43 | vars=locals(),
44 | )
45 | web.insert(
46 | "review",
47 | site_id=site_id,
48 | page_id=page_id,
49 | user_id=user_id,
50 | revision=revision,
51 | )
52 | except Exception:
53 | web.rollback()
54 | raise
55 | else:
56 | web.commit()
57 |
58 | def revert(self, url, path, author_id, revision):
59 | """Reverts a page to an older version."""
60 | data = core.db.get_version(url, path, revision).data
61 | core.db.new_version(url, path, author_id, data)
62 |
63 |
64 | from infogami.utils.delegate import pickdb # type: ignore
65 |
66 | pickdb(globals())
67 |
--------------------------------------------------------------------------------
/infogami/core/files/style.css:
--------------------------------------------------------------------------------
1 | body
2 | {
3 | font-size: 0.8em;
4 | font-family: Verdana;
5 | padding: 0;
6 | margin: 0;
7 | }
8 |
9 |
10 | #wrapper {
11 | }
12 |
13 | /* header */
14 |
15 | #header {
16 | margin: 0;
17 | padding: 10px;
18 | background: gray;
19 | background-color: #B39565;
20 | }
21 |
22 | #header #menu {
23 | clear: both;
24 | text-align: right;
25 | margin: 0px;
26 | color: white;
27 | }
28 |
29 | #header #menu a {
30 | color: white;
31 | }
32 |
33 | #header #title a {
34 | color: white;
35 | text-decoration:none
36 | }
37 |
38 | #header #title {
39 | font-size: 2em;
40 | color: white;
41 | text-align: left;
42 | padding: 0px 0px 10px 0px;
43 | margin-top: -5px;
44 | }
45 |
46 | #header #subtitle {
47 | font-size: 1em;
48 | color: #dddddd;
49 | text-align: left;
50 | }
51 |
52 | #content {
53 | margin: 0px 20px 0px 20px;
54 | }
55 |
56 | #content a {
57 | color: #937545;
58 | }
59 |
60 | #footer
61 | {
62 | margin: 10px;
63 | padding: 10px;
64 | font-size: 0.8em;
65 | text-align: center;
66 | clear: both;
67 | border-top: 1px solid #ccc;
68 | }
69 |
70 | #footer a
71 | {
72 | color: gray;
73 | text-decoration: none;
74 | }
75 |
76 | #footer a:hover
77 | {
78 | text-decoration: underline;
79 | }
80 |
81 | #dateline {
82 | color:gray;
83 | font-family:verdana,arial,helvetica,sans-serif;
84 | font-size:0.9em;
85 | margin-top:13px;
86 | margin-bottom: 10px;
87 | padding-top:2px;
88 | text-align:left;
89 | }
90 |
91 | #dateline a {
92 | background:#EEEEEE none repeat scroll 0% 0%;
93 | color:#555555;
94 | padding:2px;
95 | }
96 |
97 | #dateline a:hover {
98 | background:#555555 none repeat scroll 0% 0%;
99 | color:white;
100 | }
101 |
102 | .wrong {
103 | color: red;
104 | }
105 |
106 | div.info {
107 | border: 1px solid #FCEFA1;
108 | background: #FBF9EE;
109 | margin: 5px;
110 | padding: 5px;
111 | }
112 |
113 | div.error {
114 | border:1px solid #CD0A0A;
115 | background: #FEF1EC;
116 | color: #CD0A0A;
117 | margin: 5px;
118 | padding: 5px;
119 | }
120 |
--------------------------------------------------------------------------------
/infogami/core/files/diff.css:
--------------------------------------------------------------------------------
1 |
2 | .diff .sidebyside { clear: both; margin: 0; padding: 0; margin-bottom: 20px;}
3 | .diff table.sidebyside colgroup.content { width: 50%; }
4 | .diff table.sidebyside tbody.mod td.l { background: #fe9 }
5 | .diff table.sidebyside tbody.mod td.r { background: #fd8 }
6 | .diff table.sidebyside tbody.add td.l { background: #dfd }
7 | .diff table.sidebyside tbody.add td.r { background: #cfc }
8 | .diff table.sidebyside tbody.rem td.l { background: #f88 }
9 | .diff table.sidebyside tbody.rem td.r { background: #faa }
10 | .diff table.sidebyside tbody.mod del, .diff table.sidebyside tbody.mod ins {
11 | background: #fc0;
12 | }
13 |
14 | .diff table.sidebyside {
15 | float: left;
16 | width: 100%;
17 | }
18 |
19 | .diff #legend .mod { background: #fd8 }
20 | .diff #legend .rem { background: #f88 }
21 | .diff #legend .add { background: #bfb }
22 |
23 |
24 | .diff #legend {
25 | float: left;
26 | font-size: 9px;
27 | line-height: 1em;
28 | margin: 1em 0;
29 | padding: .5em;
30 | }
31 |
32 |
33 | /* .diff #legend h3 { display: none; } */
34 | .diff #legend dt {
35 | background: #fff;
36 | border: 1px solid #999;
37 | float: left;
38 | margin: .1em .5em .1em 2em;
39 | overflow: hidden;
40 | width: .8em; height: .8em;
41 | }
42 | .diff #legend dl, .diff #legend dd {
43 | float: left;
44 | padding: 0;
45 | margin: 0;
46 | margin-right: .5em;
47 | }
48 |
49 | /* ADDED BY WC (webchick?) */
50 |
51 | .diff-header-top { font-weight: bold; border-bottom: 1px solid #cccccc; background-color: #eeeeee; padding: 4px 4px 4px 10px; border-top: 1px solid #cccccc; white-space: nowrap; }
52 | .diff-header-side { font-weight: bold; background-color: #cccccc; padding: 10px 4px 4px 4px; border-left: 1px solid #cccccc; text-align: right; }
53 | .diff-body { border-bottom: 1px solid #cccccc; padding: 12px; border-right: 1px solid #cccccc; width: 50% }
54 |
55 | .diff-border {margin: 30px;}
56 |
57 | .diff-number { border-bottom: 1px solid #cccccc; padding: 4px; text-align: center; font-weight: bold; background-color: #cccccc; vertical-align: top; }
58 |
59 | td.l {border-bottom: 1px solid #cccccc; padding-left: 12px; }
60 | td.r {border-bottom: 1px solid #cccccc; padding-left: 12px; }
61 |
62 | ins { font-weight: bold; color: green;}
63 | del { font-weight: bold; color: red; }
64 |
--------------------------------------------------------------------------------
/tests/test_infogami/test_pages.py:
--------------------------------------------------------------------------------
1 | import json
2 | from urllib.parse import urlencode
3 |
4 | import pytest
5 | import web
6 |
7 | from infogami.utils.delegate import app
8 |
9 | b = app.browser()
10 |
11 |
12 | @pytest.mark.skip(reason="Site is None")
13 | def test_home():
14 | b.open('/')
15 | b.status == 200
16 |
17 |
18 | @pytest.mark.skip(reason="Site is None")
19 | def test_write():
20 | b.open('/sandbox/test?m=edit')
21 | b.select_form(name="edit")
22 | b['title'] = 'Foo'
23 | b['body'] = 'Bar'
24 | b.submit()
25 | assert b.path == '/sandbox/test'
26 |
27 | b.open('/sandbox/test')
28 | assert 'Foo' in b.data
29 | assert 'Bar' in b.data
30 |
31 |
32 | @pytest.mark.skip(reason="Site is None")
33 | def test_delete():
34 | b.open('/sandbox/delete?m=edit')
35 | b.select_form(name="edit")
36 | b['title'] = 'Foo'
37 | b['body'] = 'Bar'
38 | b.submit()
39 | assert b.path == '/sandbox/delete'
40 |
41 | b.open('/sandbox/delete?m=edit')
42 | b.select_form(name="edit")
43 | try:
44 | b.submit(name="_delete")
45 | except web.BrowserError as e:
46 | pass
47 | else:
48 | assert False, "expected 404"
49 |
50 |
51 | @pytest.mark.skip(reason="Site is None")
52 | def test_notfound():
53 | try:
54 | b.open('/notthere')
55 | except web.BrowserError:
56 | assert b.status == 404
57 |
58 |
59 | @pytest.mark.skip(reason="Site is None")
60 | def test_recent_changes():
61 | b.open('/recentchanges')
62 |
63 |
64 | def save(key, **data):
65 | b.open(key + '?m=edit')
66 | b.select_form(name="edit")
67 |
68 | if "type" in data:
69 | data['type.key'] = [data.pop('type')]
70 |
71 | for k, v in data.items():
72 | b[k] = v
73 | b.submit()
74 |
75 |
76 | def query(**kw):
77 | url = '/query.json?' + urlencode(kw)
78 | return [d['key'] for d in json.loads(b.open(url).read())]
79 |
80 |
81 | @pytest.mark.skip(reason="Site is None")
82 | def test_query():
83 | save('/test_query_1', title="title 1", body="body 1", type="/type/page")
84 | assert query(type='/type/page', title='title 1') == ['/test_query_1']
85 |
86 | save('/test_query_1', title="title 2", body="body 1", type="/type/page")
87 | assert query(type='/type/page', title='title 1') == []
88 |
--------------------------------------------------------------------------------
/infogami/utils/features.py:
--------------------------------------------------------------------------------
1 | """Feature flags support for Infogami.
2 | """
3 |
4 | import web
5 |
6 | from infogami.utils.context import context
7 |
8 | feature_flags = {}
9 |
10 |
11 | def set_feature_flags(flags):
12 | global feature_flags
13 |
14 | # sanity check
15 | if isinstance(flags, dict):
16 | feature_flags = flags
17 |
18 |
19 | filters = {}
20 |
21 |
22 | def register_filter(name, method):
23 | filters[name] = method
24 |
25 |
26 | def call_filter(spec):
27 | if isinstance(spec, list):
28 | return any(call_filter(x) for x in spec)
29 | elif isinstance(spec, dict):
30 | spec = spec.copy()
31 | filter_name = spec.pop('filter', None)
32 | kwargs = spec
33 | else:
34 | filter_name = spec
35 | kwargs = {}
36 |
37 | if filter_name in filters:
38 | return filters[filter_name](**kwargs)
39 | else:
40 | return False
41 |
42 |
43 | def find_enabled_features():
44 | return {f for f, spec in feature_flags.items() if call_filter(spec)}
45 |
46 |
47 | def loadhook():
48 | features = find_enabled_features()
49 | web.ctx.features = features
50 | context.features = features
51 |
52 |
53 | def is_enabled(flag):
54 | """Tests whether the given feature flag is enabled for this request."""
55 | return flag in context.features
56 |
57 |
58 | def filter_disabled():
59 | return False
60 |
61 |
62 | def filter_enabled():
63 | return True
64 |
65 |
66 | def filter_loggedin():
67 | return context.user is not None
68 |
69 |
70 | def filter_admin():
71 | return filter_usergroup("/usergroup/admin")
72 |
73 |
74 | def filter_usergroup(usergroup):
75 | """Returns true if the current user is member of the given usergroup."""
76 |
77 | def get_members():
78 | return [m.key for m in web.ctx.site.get(usergroup).members]
79 |
80 | return context.user and context.user.key in get_members()
81 |
82 |
83 | def filter_queryparam(name, value):
84 | """Returns true if the current request has a queryparam with given name and value."""
85 | i = web.input(_method="GET")
86 | return i.get(name) == value
87 |
88 |
89 | register_filter("disabled", filter_disabled)
90 | register_filter("enabled", filter_enabled)
91 | register_filter("admin", filter_admin)
92 | register_filter("loggedin", filter_loggedin)
93 | register_filter("usergroup", filter_usergroup)
94 | register_filter("queryparam", filter_queryparam)
95 |
--------------------------------------------------------------------------------
/infogami/infobase/tests/utils.py:
--------------------------------------------------------------------------------
1 | import contextlib
2 | import os
3 |
4 | import web
5 |
6 | from infogami.infobase import client, dbstore, server
7 |
8 | db_parameters = dict(
9 | host='postgres',
10 | dbn='postgres',
11 | db='infobase_test',
12 | user=os.getenv('USER'),
13 | pw='',
14 | pooling=False,
15 | )
16 |
17 |
18 | @web.memoize
19 | def recreate_database():
20 | """drop and create infobase_test database.
21 |
22 | This function is memoized to recreate the db only once per test session.
23 | """
24 | assert os.system('dropdb --host=postgres infobase_test') == 0
25 | assert os.system('createdb --host=postgres infobase_test') == 0
26 |
27 | db = web.database(**db_parameters)
28 |
29 | schema = dbstore.default_schema or dbstore.Schema()
30 | sql = str(schema.sql())
31 | db.query(sql)
32 |
33 |
34 | def setup_db(mod):
35 | recreate_database()
36 |
37 | mod.db_parameters = db_parameters.copy()
38 | web.config.db_parameters = db_parameters.copy()
39 | mod.db = web.database(**db_parameters)
40 |
41 | mod._create_database = dbstore.create_database
42 | dbstore.create_database = lambda *a, **kw: mod.db
43 |
44 | mod._tx = mod.db.transaction()
45 |
46 |
47 | def teardown_db(mod):
48 | dbstore.create_database = mod._create_database
49 |
50 | mod._tx.rollback()
51 |
52 | mod.db.ctx.clear()
53 | with contextlib.suppress(Exception):
54 | del mod.db
55 |
56 |
57 | def setup_conn(mod):
58 | setup_db(mod)
59 | web.config.db_parameters = mod.db_parameters
60 | web.config.debug = False
61 | mod.conn = client.LocalConnection()
62 |
63 |
64 | def teardown_conn(mod):
65 | teardown_db(mod)
66 | with contextlib.suppress(Exception):
67 | del mod.conn
68 |
69 |
70 | def setup_server(mod):
71 | # clear unwanted state
72 | web.ctx.clear()
73 |
74 | server._infobase = None # clear earlier reference, if any.
75 | server.get_site("test") # initialize server._infobase
76 | mod.site = server._infobase.create("test") # create a new site
77 |
78 |
79 | def teardown_server(mod):
80 | server._infobase = None
81 |
82 | with contextlib.suppress(Exception):
83 | del mod.site
84 |
85 |
86 | def setup_site(mod):
87 | web.config.db_parameters = db_parameters.copy()
88 | setup_db(mod)
89 | setup_server(mod)
90 |
91 |
92 | def teardown_site(mod):
93 | teardown_server(mod)
94 | teardown_db(mod)
95 |
--------------------------------------------------------------------------------
/infogami/plugins/review/view.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | import web
4 |
5 | from infogami.plugins.links.view import keyencode
6 | from infogami.utils import view
7 |
8 |
9 | def get_links(text):
10 | """Returns all distinct links in the text."""
11 | doc = view.get_doc(text)
12 |
13 | def is_link(e):
14 | return (
15 | e.type == 'element'
16 | and e.nodeName == 'a'
17 | and e.attribute_values.get('class', '') == 'internal'
18 | )
19 |
20 | links = set()
21 | for a in doc.find(is_link):
22 | links.add(keyencode(a.attribute_values['href']))
23 |
24 | return links
25 |
26 |
27 | link_re = web.re_compile(r'(?>> indexer = Indexer()
10 | >>> sorted(indexer.compute_index({"key": "/books/foo", "title": "The Foo Book", "authors": [{"key": "/authors/a1"}, {"key": "/authors/a2"}]}))
11 | [('ref', 'authors', '/authors/a1'), ('ref', 'authors', '/authors/a2'), ('str', 'title', 'The Foo Book')]
12 | """
13 |
14 | def compute_index(self, doc):
15 | """Returns an iterator with (datatype, key, value) for each value be indexed."""
16 | index = common.flatten_dict(doc)
17 |
18 | # skip special values and /type/text
19 | skip = [
20 | "id",
21 | "key",
22 | "type.key",
23 | "revision",
24 | "latest_revison",
25 | "last_modified",
26 | "created",
27 | ]
28 | index = {
29 | (k, v)
30 | for k, v in index
31 | if k not in skip and not k.endswith(".value") and not k.endswith(".type")
32 | }
33 |
34 | for k, v in index:
35 | if k.endswith(".key"):
36 | yield 'ref', web.rstrips(k, ".key"), v
37 | elif isinstance(v, str):
38 | yield 'str', k, v
39 | elif isinstance(v, int):
40 | yield 'int', k, v
41 |
42 | def diff_index(self, old_doc, new_doc):
43 | """Compute the difference between the index of old doc and new doc.
44 | Returns the indexes to be deleted and indexes to be inserted.
45 |
46 | >>> i = Indexer()
47 | >>> r1 = {"key": "/books/foo", "title": "The Foo Book", "authors": [{"key": "/authors/a1"}, {"key": "/authors/a2"}]}
48 | >>> r2 = {"key": "/books/foo", "title": "The Bar Book", "authors": [{"key": "/authors/a2"}]}
49 | >>> deletes, inserts = i.diff_index(r1, r2)
50 | >>> sorted(deletes)
51 | [('ref', 'authors', '/authors/a1'), ('str', 'title', 'The Foo Book')]
52 | >>> list(inserts)
53 | [('str', 'title', 'The Bar Book')]
54 | """
55 |
56 | def get_type(doc):
57 | return doc.get('type', {}).get('key', None)
58 |
59 | new_index = set(self.compute_index(new_doc))
60 |
61 | # nothing to delete when the old doc is not specified
62 | if not old_doc:
63 | return [], new_index
64 |
65 | old_index = set(self.compute_index(old_doc))
66 | if get_type(old_doc) != get_type(new_doc):
67 | return old_index, new_index
68 | else:
69 | return old_index.difference(new_index), new_index.difference(old_index)
70 |
--------------------------------------------------------------------------------
/infogami/plugins/review/code.py:
--------------------------------------------------------------------------------
1 | """
2 | review: allow user reviews
3 |
4 | Creates a new set of database tables to keep track of user reviews.
5 | Creates '/changes' page for displaying modifications since last review.
6 | """
7 |
8 | import web
9 |
10 | from infogami import core
11 | from infogami.plugins.review import db
12 | from infogami.utils import delegate, view
13 | from infogami.utils.template import render
14 | from infogami.utils.view import require_login
15 |
16 |
17 | class changes(delegate.page):
18 | @require_login
19 | def GET(self, site):
20 | user = core.auth.get_user()
21 | d = db.get_modified_pages(site, user.id)
22 | return render.changes(web.ctx.homepath, d)
23 |
24 |
25 | def input():
26 | i = web.input("a", "b", "c")
27 | i.a = i.a and int(i.a) or 0
28 | i.b = int(i.b)
29 | i.c = int(i.c)
30 | return i
31 |
32 |
33 | class review(delegate.mode):
34 | @require_login
35 | def GET(self, site, path):
36 | user = core.auth.get_user()
37 | i = input()
38 |
39 | if i.a == 0:
40 | alines = []
41 | xa = web.storage(created="", revision=0)
42 | else:
43 | xa = core.db.get_version(site, path, revision=i.a)
44 | alines = xa.data.body.splitlines()
45 |
46 | xb = core.db.get_version(site, path, revision=i.b)
47 | blines = xb.data.body.splitlines()
48 | map = core.diff.better_diff(alines, blines)
49 |
50 | view.add_stylesheet('core', 'diff.css')
51 | diff = render.diff(map, xa, xb)
52 |
53 | return render.review(path, diff, i.a, i.b, i.c)
54 |
55 |
56 | class approve(delegate.mode):
57 | @require_login
58 | def POST(self, site, path):
59 | i = input()
60 |
61 | if i.c != core.db.get_version(site, path).revision:
62 | return render.parallel_modification()
63 |
64 | user = core.auth.get_user()
65 |
66 | if i.b != i.c: # user requested for some reverts before approving this
67 | db.revert(site, path, user.id, i.b)
68 | revision = i.c + 1 # one new version has been added by revert
69 | else:
70 | revision = i.b
71 |
72 | db.approve(site, user.id, path, revision)
73 | web.seeother(web.changequery(m=None, a=None, b=None, c=None))
74 |
75 |
76 | class revert(delegate.mode):
77 | @require_login
78 | def POST(self, site, path):
79 | i = input()
80 |
81 | if i.c != core.db.get_version(site, path).revision:
82 | return render.parallel_modification()
83 |
84 | if i.a == i.b:
85 | return approve().POST(site, path)
86 | else:
87 | web.seeother(web.changequery(m='review', b=i.b - 1))
88 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.mypy]
2 | ignore_missing_imports = true
3 | pretty = true
4 | show_error_codes = true
5 | show_error_context = true
6 |
7 | [tool.ruff]
8 | select = [
9 | "AIR", # Airflow
10 | "ASYNC", # flake8-async
11 | "C90", # McCabe cyclomatic complexity
12 | "CPY", # flake8-copyright
13 | "DJ", # flake8-django
14 | "E", # pycodestyle errors
15 | "F", # Pyflakes
16 | "FA", # flake8-future-annotations
17 | "FLY", # flynt
18 | "I", # isort
19 | "ICN", # flake8-import-conventions
20 | "INT", # flake8-gettext
21 | "NPY", # NumPy-specific rules
22 | "PD", # pandas-vet
23 | "PIE", # flake8-pie
24 | "PL", # Pylint
25 | "PYI", # flake8-pyi
26 | "RSE", # flake8-raise
27 | "SLOT", # flake8-slots
28 | "T10", # flake8-debugger
29 | "TCH", # flake8-type-checking
30 | "TID", # flake8-tidy-imports
31 | "UP", # pyupgrade
32 | "W", # pycodestyle warnings
33 | "YTT", # flake8-2020
34 | # "A", # flake8-builtins
35 | # "ANN", # flake8-annotations
36 | # "ARG", # flake8-unused-arguments
37 | # "B", # flake8-bugbear
38 | # "BLE", # flake8-blind-except
39 | # "C4", # flake8-comprehensions
40 | # "COM", # flake8-commas
41 | # "D", # pydocstyle
42 | # "DTZ", # flake8-datetimez
43 | # "EM", # flake8-errmsg
44 | # "ERA", # eradicate
45 | # "EXE", # flake8-executable
46 | # "FBT", # flake8-boolean-trap
47 | # "FIX", # flake8-fixme
48 | # "G", # flake8-logging-format
49 | # "INP", # flake8-no-pep420
50 | # "ISC", # flake8-implicit-str-concat
51 | # "N", # pep8-naming
52 | # "PERF", # Perflint
53 | # "PGH", # pygrep-hooks
54 | # "PT", # flake8-pytest-style
55 | # "PTH", # flake8-use-pathlib
56 | # "Q", # flake8-quotes
57 | # "RET", # flake8-return
58 | # "RUF", # Ruff-specific rules
59 | # "S", # flake8-bandit
60 | # "SIM", # flake8-simplify
61 | # "SLF", # flake8-self
62 | # "T20", # flake8-print
63 | # "TD", # flake8-todos
64 | # "TRY", # tryceratops
65 | ]
66 | ignore = [
67 | "E402",
68 | "E731",
69 | "F811",
70 | "F841",
71 | "PLE1205",
72 | "PLR1714",
73 | "PLR5501",
74 | "PLW0120",
75 | "PLW0602",
76 | "PLW0603",
77 | "PLW2901",
78 | "UP03",
79 | ]
80 | line-length = 146
81 | target-version = "py311"
82 |
83 | [tool.ruff.mccabe]
84 | max-complexity = 15 # default is 10
85 |
86 | [tool.ruff.pylint]
87 | allow-magic-value-types = ["int", "str"]
88 | max-args = 10 # default is 5
89 | max-branches = 16 # default is 12
90 | max-returns = 6 # default is 6
91 | max-statements = 50 # default is 50
92 |
--------------------------------------------------------------------------------
/infogami/plugins/i18n/code.py:
--------------------------------------------------------------------------------
1 | """
2 | i18n: allow keeping i18n strings in wiki
3 | """
4 |
5 | import web
6 |
7 | import infogami
8 | from infogami.infobase import client
9 | from infogami.plugins.i18n import db
10 | from infogami.utils import delegate, i18n
11 |
12 | re_i18n = web.re_compile(r'^/i18n(/.*)?/strings\.([^/]*)$')
13 |
14 |
15 | class hook(client.hook):
16 | def on_new_version(self, page):
17 | """Update i18n strings when a i18n wiki page is changed."""
18 | if page.type.key == '/type/i18n':
19 | data = page._getdata()
20 | load(page.key, data)
21 |
22 |
23 | def load_strings(site):
24 | """Load strings from wiki."""
25 | pages = db.get_all_strings(site)
26 | for page in pages:
27 | load(page.key, page._getdata())
28 |
29 |
30 | def load(key, data):
31 | if result := re_i18n.match(key):
32 | namespace, lang = result.groups()
33 | namespace = namespace or '/'
34 | i18n.strings._set_strings(namespace, lang, unstringify(data))
35 |
36 |
37 | def setup():
38 | delegate.fakeload()
39 | from infogami.utils import types
40 |
41 | types.register_type('/i18n(/.*)?/strings.[^/]*', '/type/i18n')
42 |
43 | for site in db.get_all_sites():
44 | load_strings(site)
45 |
46 |
47 | def stringify(d):
48 | """Prefix string_ for every key in a dictionary.
49 |
50 | >>> stringify({'a': 1, 'b': 2})
51 | {'string_a': 1, 'string_b': 2}
52 | """
53 | return {'string_' + k: v for k, v in d.items()}
54 |
55 |
56 | def unstringify(d):
57 | """Removes string_ prefix from every key in a dictionary.
58 |
59 | >>> unstringify({'string_a': 1, 'string_b': 2})
60 | {'a': 1, 'b': 2}
61 | """
62 | return {
63 | web.lstrips(k, 'string_'): v for k, v in d.items() if k.startswith('string_')
64 | }
65 |
66 |
67 | def pathjoin(a, *p):
68 | """Join two or more pathname components, inserting '/' as needed.
69 |
70 | >>> pathjoin('/i18n', '/type/type', 'strings.en')
71 | '/i18n/type/type/strings.en'
72 | """
73 | path = a
74 | for b in p:
75 | if b.startswith('/'):
76 | b = b[1:] # strip /
77 | if path == '' or path.endswith('/'):
78 | path += b
79 | else:
80 | path += '/' + b
81 | return path
82 |
83 |
84 | @infogami.install_hook
85 | @infogami.action
86 | def movestrings():
87 | """Moves i18n strings to wiki."""
88 | query = []
89 | for (namespace, lang), d in i18n.strings._data.items():
90 | q = stringify(d)
91 | q['create'] = 'unless_exists'
92 | q['key'] = pathjoin('/i18n', namespace, '/strings.' + lang)
93 | q['type'] = '/type/i18n'
94 | query.append(q)
95 | web.ctx.site.write(query)
96 |
97 |
98 | setup()
99 |
--------------------------------------------------------------------------------
/test/webtest.py:
--------------------------------------------------------------------------------
1 | """webtest: test utilities.
2 | """
3 |
4 | import os
5 | import sys
6 | import unittest
7 |
8 | # adding current directory to path to make sure local copy of web module is used.
9 | sys.path.insert(0, '.')
10 |
11 | import web
12 | from web.browser import Browser
13 |
14 | import infogami
15 | from infogami.utils import delegate
16 |
17 | web.config.debug = False
18 | infogami.config.site = 'infogami.org'
19 |
20 |
21 | class TestCase(unittest.TestCase):
22 | def setUp(self):
23 | from infogami.infobase import server
24 |
25 | db = server.get_site('infogami.org').store.db
26 |
27 | self._t = db.transaction()
28 |
29 | def tearDown(self):
30 | self._t.rollback()
31 |
32 | def browser(self):
33 | return Browser(delegate.app)
34 |
35 |
36 | def runTests(suite):
37 | runner = unittest.TextTestRunner()
38 | return runner.run(suite)
39 |
40 |
41 | def main(suite=None):
42 | user = os.getenv('USER')
43 | web.config.db_parameters = dict(
44 | host='postgres', dbn='postgres', db='infogami_test', user=user, pw=''
45 | )
46 | web.load()
47 |
48 | delegate.app.request('/')
49 | delegate._load()
50 |
51 | if not suite:
52 | main_module = __import__('__main__')
53 | suite = module_suite(main_module, sys.argv[1:] or None)
54 |
55 | result = runTests(suite)
56 | sys.exit(not result.wasSuccessful())
57 |
58 |
59 | def suite(module_names):
60 | """Creates a suite from multiple modules."""
61 | suite = unittest.TestSuite()
62 | for mod in load_modules(module_names):
63 | suite.addTest(module_suite(mod))
64 | return suite
65 |
66 |
67 | def doctest_suite(module_names):
68 | """Makes a test suite from doctests."""
69 | import doctest
70 |
71 | suite = unittest.TestSuite()
72 | for mod in load_modules(module_names):
73 | suite.addTest(doctest.DocTestSuite(mod))
74 | return suite
75 |
76 |
77 | def load_modules(names):
78 | return [__import__(name, None, None, "x") for name in names]
79 |
80 |
81 | def module_suite(module, classnames=None):
82 | """Makes a suite from a module."""
83 | if hasattr(module, 'suite'):
84 | return module.suite()
85 | elif classnames:
86 | return unittest.TestLoader().loadTestsFromNames(classnames, module)
87 | else:
88 | return unittest.TestLoader().loadTestsFromModule(module)
89 |
90 |
91 | def with_debug(f):
92 | """Decorator to enable debug prints."""
93 |
94 | def g(*a, **kw):
95 | db_printing = web.config.get('db_printing')
96 | web.config.db_printing = True
97 |
98 | try:
99 | return f(*a, **kw)
100 | finally:
101 | web.config.db_printing = db_printing
102 |
103 | return g
104 |
--------------------------------------------------------------------------------
/infogami/infobase/multiple_insert.py:
--------------------------------------------------------------------------------
1 | """Support for multiple inserts"""
2 |
3 | import web
4 |
5 |
6 | def join(items, sep):
7 | q = web.SQLQuery('')
8 | for i, item in enumerate(items):
9 | if i:
10 | q += sep
11 | q += item
12 | return q
13 |
14 |
15 | _pg_version = None
16 |
17 |
18 | def get_postgres_version():
19 | global _pg_version
20 | if _pg_version is None:
21 | version = web.query('SELECT version();')[0].version
22 | # convert "PostgreSQL 8.2.4 on ..." in to (8, 2, 4)
23 | tokens = version.split()[1].split('.')
24 | _pg_version = tuple(int(t) for t in tokens)
25 | return _pg_version
26 |
27 |
28 | def multiple_insert(tablename, values, seqname=None, _test=False):
29 | # multiple inserts are supported only in version 8.2+
30 | if get_postgres_version() < (8, 2):
31 | result = [web.insert(tablename, seqname=seqname, **v) for v in values]
32 | if seqname:
33 | return result
34 | else:
35 | return None
36 |
37 | if not values:
38 | return []
39 |
40 | keys = list(values[0])
41 |
42 | # @@ make sure all keys are valid
43 |
44 | # make sure all rows have same keys.
45 | for v in values:
46 | if list(v) != keys:
47 | raise Exception('Bad data')
48 |
49 | q = web.SQLQuery('INSERT INTO {} ({}) VALUES '.format(tablename, ', '.join(keys)))
50 |
51 | data = []
52 |
53 | for row in values:
54 | d = join([web.SQLQuery(web.aparam(), [row[k]]) for k in keys], ', ')
55 | data.append('(' + d + ')')
56 |
57 | q += join(data, ',')
58 |
59 | if seqname is not False:
60 | if seqname is None:
61 | seqname = tablename + "_id_seq"
62 | q += "; SELECT currval('%s')" % seqname
63 |
64 | if _test:
65 | return q
66 |
67 | db_cursor = web.ctx.db_cursor()
68 | web.ctx.db_execute(db_cursor, q)
69 |
70 | try:
71 | out = db_cursor.fetchone()[0]
72 | out = list(range(out - len(values) + 1, out + 1))
73 | except Exception:
74 | out = None
75 |
76 | if not web.ctx.db_transaction:
77 | web.ctx.db.commit()
78 | return out
79 |
80 |
81 | if __name__ == "__main__":
82 | web.config.db_parameters = dict(
83 | dbn='postgres', db='coverthing_test', user='anand', pw=''
84 | )
85 | web.config.db_printing = True
86 | web.load()
87 |
88 | def data(id):
89 | return [
90 | dict(thing_id=id, key='isbn', value=1, datatype=1),
91 | dict(thing_id=id, key='source', value='amazon', datatype=1),
92 | dict(thing_id=id, key='image', value='foo', datatype=10),
93 | ]
94 |
95 | ids = multiple_insert('thing', [dict(dummy=1)] * 10)
96 | values = []
97 | for id in ids:
98 | values += data(id)
99 | multiple_insert('datum', values, seqname=False)
100 |
--------------------------------------------------------------------------------
/infogami/core/db.py:
--------------------------------------------------------------------------------
1 | import web
2 |
3 | from infogami.utils.view import public
4 |
5 |
6 | def get_version(path, revision=None):
7 | return web.ctx.site.get(path, revision)
8 |
9 |
10 | @public
11 | def get_type(path):
12 | return get_version(path)
13 |
14 |
15 | @public
16 | def get_expected_type(page, property_name):
17 | """Returns the expected type of a property."""
18 | defaults = {
19 | "key": "/type/key",
20 | "type": "/type/type",
21 | "permission": "/type/permission",
22 | "child_permission": "/type/permission",
23 | }
24 |
25 | if property_name in defaults:
26 | return defaults[property_name]
27 |
28 | for p in page.type.properties:
29 | if p.name == property_name:
30 | return p.expected_type
31 |
32 | return "/type/string"
33 |
34 |
35 | def new_version(path, type):
36 | if isinstance(type, str):
37 | type = get_type(type)
38 |
39 | assert type is not None
40 | return web.ctx.site.new(path, {'key': path, 'type': type})
41 |
42 |
43 | @public
44 | def get_i18n_page(page):
45 | key = page.key
46 | if key == '/':
47 | key = '/index'
48 |
49 | def get(lang):
50 | return lang and get_version(key + '.' + lang)
51 |
52 | return get(web.ctx.lang) or get('en') or None
53 |
54 |
55 | class ValidationException(Exception):
56 | pass
57 |
58 |
59 | def get_user_preferences(user):
60 | return get_version(user.key + '/preferences')
61 |
62 |
63 | @public
64 | def get_recent_changes(
65 | key=None, author=None, ip=None, type=None, bot=None, limit=None, offset=None
66 | ):
67 | q = {'sort': '-created'}
68 | if key is not None:
69 | q['key'] = key
70 |
71 | if author:
72 | q['author'] = author.key
73 |
74 | if type:
75 | q['type'] = type
76 |
77 | if ip:
78 | q['ip'] = ip
79 |
80 | if bot is not None:
81 | q['bot'] = bot
82 |
83 | q['limit'] = limit or 100
84 | q['offset'] = offset or 0
85 | result = web.ctx.site.versions(q)
86 | for r in result:
87 | r.thing = web.ctx.site.get(r.key, r.revision, lazy=True)
88 | return result
89 |
90 |
91 | @public
92 | def list_pages(path, limit=100, offset=0):
93 | """Lists all pages with name path/*"""
94 | return _list_pages(path, limit=limit, offset=offset)
95 |
96 |
97 | def _list_pages(path, limit, offset):
98 | q = {}
99 | if path != '/':
100 | q['key~'] = path + '/*'
101 |
102 | # don't show /type/delete and /type/redirect
103 | q['a:type!='] = '/type/delete'
104 | q['b:type!='] = '/type/redirect'
105 |
106 | q['sort'] = 'key'
107 | q['limit'] = limit
108 | q['offset'] = offset
109 | # queries are very slow with != conditions
110 | # q['type'] != '/type/delete'
111 | return [web.ctx.site.get(key, lazy=True) for key in web.ctx.site.things(q)]
112 |
113 |
114 | def get_things(typename, prefix, limit):
115 | """Lists all things whose names start with typename"""
116 | q = {'key~': prefix + '*', 'type': typename, 'sort': 'key', 'limit': limit}
117 | return [web.ctx.site.get(key, lazy=True) for key in web.ctx.site.things(q)]
118 |
--------------------------------------------------------------------------------
/infogami/core/forms.py:
--------------------------------------------------------------------------------
1 | from web.form import * # noqa: F401,F403 TODO (cclauss): Remove wildcard imports
2 | from web.form import (
3 | Button,
4 | Checkbox,
5 | Form,
6 | Hidden,
7 | Password,
8 | Textbox,
9 | Validator,
10 | net,
11 | notnull,
12 | regexp,
13 | )
14 |
15 | from infogami.core import db
16 | from infogami.utils import i18n
17 | from infogami.utils.context import context
18 |
19 |
20 | class BetterButton(Button):
21 | def render(self):
22 | label = self.attrs.get('label', self.name)
23 | safename = net.websafe(self.name)
24 | x = f'{label} '
25 | return x
26 |
27 |
28 | _ = i18n.strings.get_namespace('/account/login')
29 |
30 | login = Form(
31 | Hidden('redirect'),
32 | Textbox('username', notnull, description=_.username),
33 | Password('password', notnull, description=_.password),
34 | Checkbox('remember', description=_.remember_me),
35 | )
36 |
37 | vlogin = regexp(
38 | r"^[A-Za-z0-9-_]{3,20}$", 'must be between 3 and 20 letters and numbers'
39 | )
40 | vpass = regexp(r".{3,20}", 'must be between 3 and 20 characters')
41 | vemail = regexp(r".*@.*", "must be a valid email address")
42 | not_already_used = Validator(
43 | 'This email is already used',
44 | lambda email: db.get_user_by_email(context.site, email) is None, # type: ignore
45 | )
46 |
47 | _ = i18n.strings.get_namespace('/account/register')
48 |
49 | register = Form(
50 | Textbox('username', vlogin, description=_.username),
51 | Textbox('displayname', notnull, description=_.display_name),
52 | Textbox('email', notnull, vemail, description=_.email),
53 | Password('password', notnull, vpass, description=_.password),
54 | Password('password2', notnull, description=_.confirm_password),
55 | validators=[
56 | Validator(_.passwords_did_not_match, lambda i: i.password == i.password2)
57 | ],
58 | )
59 |
60 | _ = i18n.strings.get_namespace('/account/preferences')
61 |
62 | login_preferences = Form(
63 | Password("oldpassword", notnull, description=_.current_password),
64 | Password("password", notnull, vpass, description=_.new_password),
65 | Password("password2", notnull, description=_.confirm_password),
66 | BetterButton("save", label=_.save),
67 | validators=[
68 | Validator(_.passwords_did_not_match, lambda i: i.password == i.password2)
69 | ],
70 | )
71 |
72 | _ = i18n.strings.get_namespace('/account/forgot_password')
73 |
74 | validemail = Validator(
75 | _.email_not_registered,
76 | lambda email: db.get_user_by_email(context.site, email), # type: ignore
77 | )
78 | forgot_password = Form(
79 | Textbox('email', notnull, vemail, description=_.email),
80 | )
81 |
82 | _register = i18n.strings.get_namespace('/account/register')
83 | _preferences = i18n.strings.get_namespace('/account/preferences')
84 |
85 | reset_password = Form(
86 | Password('password', notnull, vpass, description=_register.password),
87 | Password('password2', notnull, description=_register.confirm_password),
88 | BetterButton("save", label=_preferences.save),
89 | validators=[
90 | Validator(
91 | _register.passwords_did_not_match, lambda i: i.password == i.password2
92 | )
93 | ],
94 | )
95 |
--------------------------------------------------------------------------------
/infogami/infobase/_dbstore/schema.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import web
4 |
5 | # https://stackoverflow.com/questions/58518448
6 | web.template.ALLOWED_AST_NODES.append('Constant')
7 |
8 | INDEXED_DATATYPES = ["str", "int", "ref"]
9 |
10 |
11 | class Schema:
12 | """Schema to map to database table.
13 |
14 | >>> schema = Schema()
15 | >>> schema.add_entry('page_str', '/type/page', 'str', None)
16 | >>> schema.find_table('/type/page', 'str', 'title')
17 | 'page_str'
18 | >>> schema.find_table('/type/article', 'str', 'title')
19 | 'datum_str'
20 | """
21 |
22 | def __init__(self, multisite=False):
23 | self.entries = []
24 | self.sequences = {}
25 | self.prefixes = set()
26 | self.multisite = multisite
27 | self._table_cache = {}
28 |
29 | def add_entry(self, table, type, datatype, name):
30 | entry = web.storage(table=table, type=type, datatype=datatype, name=name)
31 | self.entries.append(entry)
32 |
33 | def add_seq(self, type, pattern='/%d'):
34 | self.sequences[type] = pattern
35 |
36 | def get_seq(self, type):
37 | if type in self.sequences:
38 | # name is 'type_page_seq' for type='/type/page'
39 | name = type[1:].replace('/', '_') + '_seq'
40 | return web.storage(type=type, pattern=self.sequences[type], name=name)
41 |
42 | def add_table_group(self, prefix, type, datatypes=None):
43 | datatypes = datatypes or INDEXED_DATATYPES
44 | for d in datatypes:
45 | self.add_entry(prefix + "_" + d, type, d, None)
46 |
47 | self.prefixes.add(prefix)
48 |
49 | def find_table(self, type, datatype, name):
50 | if datatype not in INDEXED_DATATYPES:
51 | return None
52 |
53 | def f():
54 | def match(a, b):
55 | return a is None or a == b
56 |
57 | for e in self.entries:
58 | if (
59 | match(e.type, type)
60 | and match(e.datatype, datatype)
61 | and match(e.name, name)
62 | ):
63 | return e.table
64 | return 'datum_' + datatype
65 |
66 | key = type, datatype, name
67 | if key not in self._table_cache:
68 | self._table_cache[key] = f()
69 | return self._table_cache[key]
70 |
71 | def find_tables(self, type):
72 | return [self.find_table(type, d, None) for d in INDEXED_DATATYPES]
73 |
74 | def sql(self):
75 | prefixes = sorted(list(self.prefixes) + ['datum'])
76 | sequences = [self.get_seq(type).name for type in self.sequences]
77 |
78 | path = os.path.join(os.path.dirname(__file__), 'schema.sql')
79 | t = web.template.frender(path)
80 |
81 | self.add_table_group("datum", None)
82 |
83 | tables = sorted({(e.table, e.datatype) for e in self.entries})
84 | web.template.Template.globals['dict'] = dict
85 | web.template.Template.globals['enumerate'] = enumerate
86 | return t(tables, sequences, self.multisite)
87 |
88 | def list_tables(self):
89 | self.add_table_group("datum", None)
90 | tables = sorted({e.table for e in self.entries})
91 | return tables
92 |
93 | def __str__(self):
94 | lines = [f"{e.table}\t{e.type}\t{e.datatype}\t{e.name}" for e in self.entries]
95 | return "\n".join(lines)
96 |
--------------------------------------------------------------------------------
/infogami/core/templates/sitepreferences.html:
--------------------------------------------------------------------------------
1 | $def with (permissions)
2 |
3 | $var title: Site Preferences
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | New Path
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
102 |
103 |
104 |
105 |
--------------------------------------------------------------------------------
/infogami/utils/storage.py:
--------------------------------------------------------------------------------
1 | """
2 | Useful datastructures.
3 | """
4 |
5 | from collections import OrderedDict, defaultdict
6 | from collections.abc import Mapping
7 |
8 | import web
9 |
10 | storage: dict[str, dict] = defaultdict(OrderedDict)
11 |
12 |
13 | class SiteLocalDict:
14 | """
15 | Takes a dictionary that maps sites to objects.
16 | When somebody tries to get or set an attribute or item
17 | of the SiteLocalDict, it passes it on to the object
18 | for the active site in dictionary.
19 | Active site is found from `context.site`.
20 | see infogami.utils.context.context
21 | """
22 |
23 | def __init__(self):
24 | self.__dict__['_SiteLocalDict__d'] = {}
25 |
26 | def __getattr__(self, name):
27 | return getattr(self._getd(), name)
28 |
29 | def __setattr__(self, name, value):
30 | setattr(self._getd(), name, value)
31 |
32 | def __delattr__(self, name):
33 | delattr(self._getd(), name)
34 |
35 | def _getd(self):
36 | site = web.ctx.get('site')
37 | key = site and site.name
38 | if key not in self.__d:
39 | self.__d[key] = web.storage()
40 | return self.__d[key]
41 |
42 |
43 | class ReadOnlyDict:
44 | """Dictionary wrapper to provide read-only access to a dictionary."""
45 |
46 | def __init__(self, d):
47 | self._d = d
48 |
49 | def __getitem__(self, key):
50 | return self._d[key]
51 |
52 | def __getattr__(self, key):
53 | try:
54 | return self._d[key]
55 | except KeyError:
56 | raise AttributeError(key)
57 |
58 |
59 | class DictPile(Mapping):
60 | """Pile of dictionaries.
61 | A key in top dictionary covers the key with the same name in the bottom dictionary.
62 |
63 | >>> a = {'x': 1, 'y': 2}
64 | >>> b = {'y': 5, 'z': 6}
65 | >>> d = DictPile([a, b])
66 | >>> len(d)
67 | 3
68 | >>> list(iter(d)) == list(d.keys())
69 | True
70 | >>> list(iter(d)) == list(d)
71 | True
72 | >>> d['x'], d['y'], d['z']
73 | (1, 5, 6)
74 | >>> b['x'] = 4
75 | >>> d['x'], d['y'], d['z']
76 | (4, 5, 6)
77 | >>> c = {'x':0, 'y':1}
78 | >>> d.add_dict(c)
79 | >>> d['x'], d['y'], d['z']
80 | (0, 1, 6)
81 | >>> d.add_dict({'new': 99})
82 | >>> len(d)
83 | 4
84 | >>> list(iter(d)) == list(d.keys())
85 | True
86 | >>> list(iter(d)) == list(d)
87 | True
88 | >>> 'new' in d
89 | True
90 | >>> 'nope' in d
91 | False
92 | """
93 |
94 | def __init__(self, dicts=[]):
95 | self.dicts = dicts[:]
96 |
97 | def add_dict(self, d):
98 | """Adds d to the pile of dicts at the top."""
99 | self.dicts.append(d)
100 |
101 | def __getitem__(self, key):
102 | for d in self.dicts[::-1]:
103 | if key in d:
104 | return d[key]
105 | else:
106 | raise KeyError(key)
107 |
108 | def __iter__(self):
109 | yield from self.keys()
110 |
111 | def __len__(self):
112 | return len(self.keys())
113 |
114 | def keys(self):
115 | keys = set()
116 | for d in self.dicts:
117 | keys.update(d.keys())
118 | return list(keys)
119 |
120 |
121 | if __name__ == "__main__":
122 | import doctest
123 |
124 | doctest.testmod()
125 |
--------------------------------------------------------------------------------
/infogami/core/dbupgrade.py:
--------------------------------------------------------------------------------
1 | """
2 | module for doing database upgrades when code changes.
3 | """
4 |
5 | import web
6 |
7 | import infogami
8 | from infogami import tdb
9 | from infogami.core import db
10 | from infogami.utils.context import context as ctx
11 |
12 |
13 | def get_db_version():
14 | return tdb.root.d.get('__version__', 0)
15 |
16 |
17 | upgrades = []
18 |
19 |
20 | def upgrade(f):
21 | upgrades.append(f)
22 | return f
23 |
24 |
25 | def apply_upgrades():
26 | from infogami import tdb
27 |
28 | tdb.transact()
29 | try:
30 | v = get_db_version()
31 | for u in upgrades[v:]:
32 | print('applying upgrade:', u.__name__, file=web.debug)
33 | u()
34 |
35 | mark_upgrades()
36 | tdb.commit()
37 | print('upgrade successful.', file=web.debug)
38 | except Exception:
39 | print('upgrade failed', file=web.debug)
40 | import traceback
41 |
42 | traceback.print_exc()
43 | tdb.rollback()
44 |
45 |
46 | @infogami.action
47 | def dbupgrade():
48 | apply_upgrades()
49 |
50 |
51 | def mark_upgrades():
52 | tdb.root.__version__ = len(upgrades)
53 | tdb.root.save()
54 |
55 |
56 | @upgrade
57 | def hash_passwords():
58 | from infogami.core import auth
59 |
60 | tuser = db.get_type(ctx.site, 'type/user')
61 | users = tdb.Things(parent=ctx.site, type=tuser).list()
62 |
63 | for u in users:
64 | try:
65 | preferences = u._c('preferences')
66 | except Exception:
67 | # setup preferences for broken accounts, so that they can use forgot password.
68 | preferences = db.new_version(
69 | u, 'preferences', db.get_type(ctx.site, 'type/thing'), dict(password='')
70 | )
71 | preferences.save()
72 |
73 | if preferences.password:
74 | auth.set_password(u, preferences.password)
75 |
76 |
77 | @upgrade
78 | def upgrade_types():
79 | from infogami.core.db import _create_type, tdbsetup
80 |
81 | tdbsetup()
82 | type = db.get_type(ctx.site, "type/type")
83 | types = tdb.Things(parent=ctx.site, type=type)
84 | types = [t for t in types if 'properties' not in t.d and 'is_primitive' not in t.d]
85 | primitives = dict(
86 | int='type/int', integer='type/int', string='type/string', text='type/text'
87 | )
88 |
89 | newtypes = {}
90 | for t in types:
91 | properties = []
92 | backreferences = []
93 | print(t, t.d, file=web.debug)
94 | if t.name == 'type/site':
95 | continue
96 | for name, value in t.d.items():
97 | p = web.storage(name=name)
98 | typename = web.lstrips(value, "thing ")
99 |
100 | if typename.startswith('#'):
101 | typename, property_name = typename.lstrip('#').split('.')
102 | p.type = db.get_type(ctx.site, typename)
103 | p.property_name = property_name
104 | backreferences.append(p)
105 | continue
106 |
107 | if typename.endswith('*'):
108 | typename = typename[:-1]
109 | p.unique = False
110 | else:
111 | p.unique = True
112 | typename = primitives.get(typename, typename)
113 | p.type = db.get_type(ctx.site, typename)
114 | properties.append(p)
115 | _create_type(ctx.site, t.name, properties, backreferences)
116 |
--------------------------------------------------------------------------------
/infogami/infobase/tests/test_logreader.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 | from infogami.infobase import logreader
4 |
5 |
6 | def test_nextday():
7 | assert logreader.nextday(datetime.date(2010, 10, 20)) == datetime.date(2010, 10, 21)
8 | assert logreader.nextday(datetime.date(2010, 10, 31)) == datetime.date(2010, 11, 1)
9 |
10 |
11 | def test_daterange():
12 | def f(begin, end):
13 | return list(logreader.daterange(begin, end))
14 |
15 | oct10 = datetime.date(2010, 10, 10)
16 | oct11 = datetime.date(2010, 10, 11)
17 | assert f(oct10, oct10) == [oct10]
18 | assert f(oct10, oct11) == [oct10, oct11]
19 | assert f(oct11, oct10) == []
20 |
21 |
22 | def test_to_timestamp():
23 | assert logreader.to_timestamp('2010-01-02T03:04:05.678900') == datetime.datetime(
24 | 2010, 1, 2, 3, 4, 5, 678900
25 | )
26 |
27 |
28 | class TestLogFile:
29 | def test_file2date(self):
30 | logfile = logreader.LogFile("foo")
31 | assert logfile.file2date("foo/2010/10/20.log") == datetime.date(2010, 10, 20)
32 |
33 | def test_date2file(self):
34 | logfile = logreader.LogFile("foo")
35 | assert logfile.date2file(datetime.date(2010, 10, 20)) == "foo/2010/10/20.log"
36 |
37 | def test_tell(self, tmpdir):
38 | root = tmpdir.mkdir("log")
39 | logfile = logreader.LogFile(root.strpath)
40 |
41 | # when there are no files, it must tell the epoch time
42 | assert logfile.tell() == datetime.date.fromtimestamp(0).isoformat() + ":0"
43 |
44 | def test_find_filelist(self, tmpdir):
45 | root = tmpdir.mkdir("log")
46 | logfile = logreader.LogFile(root.strpath)
47 |
48 | # when there are no files, it should return empty list.
49 | assert logfile.find_filelist() == []
50 | assert logfile.find_filelist(from_date=datetime.date(2010, 10, 10)) == []
51 |
52 | # create empty log file and check if it returns them
53 | d = root.mkdir("2010").mkdir("10")
54 | f1 = d.join("01.log")
55 | f1.write("")
56 | f2 = d.join("02.log")
57 | f2.write("")
58 | assert logfile.find_filelist() == [f1.strpath, f2.strpath]
59 | assert logfile.find_filelist(from_date=datetime.date(2010, 10, 2)) == [
60 | f2.strpath
61 | ]
62 |
63 | # create a bad file and make it behaves correctly
64 | d.join("foo.log").write("")
65 | assert logfile.find_filelist() == [f1.strpath, f2.strpath]
66 |
67 | def test_readline(self, tmpdir):
68 | root = tmpdir.mkdir("log")
69 | logfile = logreader.LogFile(root.strpath)
70 | assert logfile.readline() == ''
71 |
72 | root.mkdir("2010").mkdir("10")
73 | f = root.join("2010/10/01.log")
74 | f.write("helloworld\n")
75 | assert logfile.readline() == 'helloworld\n'
76 |
77 | f.write("hello 1\n", mode='a')
78 | f.write("hello 2\n", mode='a')
79 | assert logfile.readline() == 'hello 1\n'
80 | assert logfile.readline() == 'hello 2\n'
81 | assert logfile.readline() == ''
82 |
83 | def test_seek(self, tmpdir):
84 | root = tmpdir.mkdir("log")
85 | logfile = logreader.LogFile(root.strpath)
86 |
87 | # seek should not have any effect when there are no log files.
88 | pos = logfile.tell()
89 | logfile.seek("2010-10-10:0")
90 | pos2 = logfile.tell()
91 | assert pos == pos2
92 |
93 | # when the requested file is not found, offset should go to the next available file.
94 | root.mkdir("2010").mkdir("10")
95 | f = root.join("2010/10/20.log")
96 | f.write("")
97 |
98 | logfile.seek("2010-10-10:0")
99 | assert logfile.tell() == "2010-10-20:0"
100 |
--------------------------------------------------------------------------------
/infogami/infobase/logger.py:
--------------------------------------------------------------------------------
1 | """
2 | Infobase Logger module.
3 |
4 | Infogami log file is a stream of events where each event is a dictionary represented in
5 | JSON format having keys [`action`, `site`, `data`].
6 |
7 | * action: Name of action being logged. Possible values are write, new_account and update_object.
8 | * site: Name of site
9 | * data: data associated with the event. This data is used for replaying that action.
10 |
11 | Log files are circulated on daily basis. Default log file format is $logroot/yyyy/mm/dd.log.
12 | """
13 |
14 | import datetime
15 | import json
16 | import os
17 | import threading
18 |
19 |
20 | def synchronize(f):
21 | """decorator to synchronize a method."""
22 |
23 | def g(self, *a, **kw):
24 | if not getattr(self, '_lock'):
25 | self._lock = threading.Lock()
26 |
27 | self._lock.acquire()
28 | try:
29 | return f(self, *a, **kw)
30 | finally:
31 | self._lock.release()
32 |
33 | return f
34 |
35 |
36 | def to_timestamp(iso_date_string):
37 | """
38 | >>> t = '2008-01-01T01:01:01.010101'
39 | >>> to_timestamp(t).isoformat()
40 | '2008-01-01T01:01:01.010101'
41 | """
42 | # @@ python datetime module is ugly.
43 | # @@ It takes so much of work to create datetime from isoformat.
44 | date, time = iso_date_string.split('T', 1)
45 | y, m, d = date.split('-')
46 | H, M, S = time.split(':')
47 | S, ms = S.split('.')
48 | return datetime.datetime(*map(int, [y, m, d, H, M, S, ms]))
49 |
50 |
51 | class DummyLogger:
52 | def __init__(self, *a, **kw):
53 | pass
54 |
55 | def on_write(self, *a, **kw):
56 | pass
57 |
58 | def on_new_account(self, *a, **kw):
59 | pass
60 |
61 | def on_update_account(self, *a, **kw):
62 | pass
63 |
64 | def __call__(self, event):
65 | pass
66 |
67 |
68 | class Logger:
69 | def __init__(self, root, compress=False):
70 | self.root = root
71 | if compress:
72 | import gzip
73 |
74 | self.extn = ".log.gz"
75 | self._open = gzip.open
76 | else:
77 | self.extn = ".log"
78 | self._open = open
79 |
80 | def get_path(self, timestamp=None):
81 | timestamp = timestamp or datetime.datetime.utcnow()
82 | date = timestamp.date()
83 | return (
84 | os.path.join(
85 | self.root, "%02d" % date.year, "%02d" % date.month, "%02d" % date.day
86 | )
87 | + self.extn
88 | )
89 |
90 | def __call__(self, event):
91 | data = event.data.copy()
92 | event.timestamp = event.timestamp or datetime.datetime.utcnow()
93 | if event.name in ['write', 'save', 'save_many']:
94 | name = event.name
95 | data['ip'] = event.ip
96 | data['author'] = event.username
97 | elif event.name == 'register':
98 | # data will already contain username, password, email and displayname
99 | name = "new_account"
100 | data['ip'] = event.ip
101 | elif event.name == 'update_user':
102 | name = "update_account"
103 | data['ip'] = event.ip
104 | elif event.name.startswith("store."):
105 | name = event.name
106 | else:
107 | return
108 |
109 | self.write(name, event.sitename, event.timestamp, data)
110 |
111 | @synchronize
112 | def write(self, action, sitename, timestamp, data):
113 | path = self.get_path(timestamp)
114 | dir = os.path.dirname(path)
115 | if not os.path.exists(dir):
116 | os.makedirs(dir)
117 | f = self._open(path, 'a')
118 | f.write(
119 | json.dumps(
120 | dict(
121 | action=action,
122 | site=sitename,
123 | timestamp=timestamp.isoformat(),
124 | data=data,
125 | )
126 | )
127 | )
128 | f.write('\n')
129 | f.flush()
130 | # @@ optimize: call fsync after all modifications are written instead of calling for every modification
131 | os.fsync(f.fileno())
132 | f.close()
133 |
134 |
135 | if __name__ == '__main__':
136 | import doctest
137 |
138 | doctest.testmod()
139 |
--------------------------------------------------------------------------------
/infogami/infobase/utils.py:
--------------------------------------------------------------------------------
1 | """Generic utilities.
2 | """
3 |
4 | import datetime
5 | import re
6 |
7 | import web
8 |
9 |
10 | def parse_datetime(value):
11 | """Creates datetime object from isoformat.
12 |
13 | >>> t = '2008-01-01T01:01:01.010101'
14 | >>> parse_datetime(t).isoformat()
15 | '2008-01-01T01:01:01.010101'
16 | """
17 | if isinstance(value, datetime.datetime):
18 | return value
19 | else:
20 | tokens = re.split(r'-|T|:|\.| ', value)
21 | return datetime.datetime(*map(int, tokens))
22 |
23 |
24 | def parse_boolean(value):
25 | return web.safeunicode(value).lower() in ["1", "true"]
26 |
27 |
28 | def dict_diff(d1, d2):
29 | """Compares 2 dictionaries and returns the following.
30 |
31 | * all keys in d1 whose values are changed in d2
32 | * all keys in d1 which have same values in d2
33 | * all keys in d2 whose values are changed in d1
34 |
35 | >>> a, b, c = dict_diff({'x': 1, 'y': 2, 'z': 3}, {'x': 11, 'z': 3, 'w': 23})
36 | >>> sorted(a), sorted(b), sorted(c)
37 | (['x', 'y'], ['z'], ['w', 'x'])
38 | """
39 | same = {k for k in d1 if d1[k] == d2.get(k)}
40 | left = set(d1.keys()).difference(same)
41 | right = set(d2.keys()).difference(same)
42 | return left, same, right
43 |
44 |
45 | def pprint(obj):
46 | """Pretty prints given object.
47 | >>> pprint(1)
48 | 1
49 | >>> pprint("hello")
50 | 'hello'
51 | >>> pprint([1, 2, 3])
52 | [1, 2, 3]
53 | >>> pprint({'x': 1, 'y': 2})
54 | {
55 | 'x': 1,
56 | 'y': 2
57 | }
58 | >>> pprint([dict(x=1, y=2), dict(c=1, a=2)])
59 | [{
60 | 'x': 1,
61 | 'y': 2
62 | }, {
63 | 'a': 2,
64 | 'c': 1
65 | }]
66 | >>> pprint({'x': 1, 'y': {'a': 1, 'b': 2}, 'z': 3})
67 | {
68 | 'x': 1,
69 | 'y': {
70 | 'a': 1,
71 | 'b': 2
72 | },
73 | 'z': 3
74 | }
75 | >>> pprint({})
76 | {
77 | }
78 | """
79 | print(prepr(obj))
80 |
81 |
82 | def prepr(obj, indent=""):
83 | """Pretty representation."""
84 | if isinstance(obj, list):
85 | return "[" + ", ".join(prepr(x, indent) for x in obj) + "]"
86 | elif isinstance(obj, tuple):
87 | return "(" + ", ".join(prepr(x, indent) for x in obj) + ")"
88 | elif isinstance(obj, dict):
89 | if hasattr(obj, '__prepr__'):
90 | return obj.__prepr__()
91 | else:
92 | indent = indent + " "
93 | items = [
94 | "\n" + indent + prepr(k) + ": " + prepr(obj[k], indent)
95 | for k in sorted(obj.keys())
96 | ]
97 | return '{' + ",".join(items) + "\n" + indent[4:] + "}"
98 | else:
99 | return repr(obj)
100 |
101 |
102 | def flatten(nested_list, result=None):
103 | """Flattens a nested list.::
104 |
105 | >>> flatten([1, [2, 3], [4, [5, 6]]])
106 | [1, 2, 3, 4, 5, 6]
107 | """
108 | if result is None:
109 | result = []
110 |
111 | for x in nested_list:
112 | if isinstance(x, list):
113 | flatten(x, result)
114 | else:
115 | result.append(x)
116 | return result
117 |
118 |
119 | def flatten_dict(d):
120 | """Flattens a dictionary.::
121 |
122 | >>> flatten_dict({"type": {"key": "/type/book"}, "key": "/books/foo", "authors": [{"key": "/authors/a1"}, {"key": "/authors/a2"}]})
123 | [('type.key', '/type/book'), ('key', '/books/foo'), ('authors.key', '/authors/a1'), ('authors.key', '/authors/a2')]
124 | """
125 |
126 | def f(key, value):
127 | if isinstance(value, dict):
128 | for k, v in value.items():
129 | f(key + "." + k, v)
130 | elif isinstance(value, list):
131 | for v in value:
132 | f(key, v)
133 | else:
134 | key = web.lstrips(key, ".")
135 | items.append((key, value))
136 |
137 | items = []
138 | f("", d)
139 | return items
140 |
141 |
142 | def safeint(value, default):
143 | """Converts a string to integer. Returns the specified default value on error.::
144 |
145 | >>> safeint("1", 0)
146 | 1
147 | >>> safeint("foo", 0)
148 | 0
149 | >>> safeint(None, 0)
150 | 0
151 | """
152 | try:
153 | return int(value)
154 | except (ValueError, TypeError):
155 | return default
156 |
157 |
158 | if __name__ == "__main__":
159 | import doctest
160 |
161 | doctest.testmod()
162 |
--------------------------------------------------------------------------------
/infogami/core/helpers.py:
--------------------------------------------------------------------------------
1 | """
2 | Generic Utilities.
3 | """
4 |
5 |
6 | class xdict:
7 | """Dictionary wrapper to give sorted repr.
8 | Used for doctest.
9 | """
10 |
11 | def __init__(self, d):
12 | self.d = d
13 |
14 | def __repr__(self):
15 | def f(d):
16 | if isinstance(d, dict):
17 | return xdict(d)
18 | else:
19 | return d
20 |
21 | return (
22 | '{' + ", ".join([f"'{k}': {f(v)}" for k, v in sorted(self.d.items())]) + '}'
23 | )
24 |
25 |
26 | def flatten(d):
27 | """Make a dictionary flat.
28 |
29 | >>> d = {'a': 1, 'b': [2, 3], 'c': {'x': 4, 'y': 5}}
30 | >>> xdict(flatten(d))
31 | {'a': 1, 'b#0': 2, 'b#1': 3, 'c.x': 4, 'c.y': 5}
32 | """
33 |
34 | def traverse(d, prefix, delim, visit):
35 | for k, v in d.items():
36 | k = str(k)
37 | if isinstance(v, dict):
38 | traverse(v, prefix + delim + k, '.', visit)
39 | elif isinstance(v, list):
40 | traverse(betterlist(v), prefix + delim + k, '#', visit)
41 | else:
42 | visit(prefix + delim + k, v)
43 |
44 | def visit(k, v):
45 | d2[k] = v
46 |
47 | d2 = {}
48 | traverse(d, "", "", visit)
49 | return d2
50 |
51 |
52 | def unflatten(d):
53 | """Inverse of flatten.
54 |
55 | >>> xdict(unflatten({'a': 1, 'b#0': 2, 'b#1': 3, 'c.x': 4, 'c.y': 5}))
56 | {'a': 1, 'b': [2, 3], 'c': {'x': 4, 'y': 5}}
57 | >>> unflatten({'a#1#2.b': 1})
58 | {'a': [None, [None, None, {'b': 1}]]}
59 | """
60 |
61 | def setdefault(d, k, v):
62 | # error check: This can happen when d has both foo.x and foo as keys
63 | if not isinstance(d, (dict, betterlist)):
64 | return
65 |
66 | if '.' in k:
67 | a, b = k.split('.', 1)
68 | return setdefault(setdefault(d, a, {}), b, v)
69 | elif '#' in k:
70 | a, b = k.split('#', 1)
71 | return setdefault(setdefault(d, a, betterlist()), b, v)
72 | else:
73 | return d.setdefault(k, v)
74 |
75 | d2 = {}
76 | for k, v in d.items():
77 | setdefault(d2, k, v)
78 | return d2
79 |
80 |
81 | class betterlist(list):
82 | """List with dict like setdefault method."""
83 |
84 | def fill(self, size):
85 | while len(self) < size:
86 | self.append(None)
87 |
88 | def setdefault(self, index, value):
89 | index = int(index)
90 | self.fill(index + 1)
91 | if self[index] is None:
92 | self[index] = value
93 | return self[index]
94 |
95 | def iteritems(self):
96 | return enumerate(self)
97 |
98 | def items(self):
99 | return list(self.iteritems()) # Works on both Python 2 and 3
100 |
101 |
102 | def trim(x):
103 | """Remove empty elements from a list or dictionary.
104 |
105 | >>> trim([2, 3, None, None, '', 42])
106 | [2, 3, 42]
107 | >>> trim([{'x': 1}, {'x': ''}, {'x': 3}])
108 | [{'x': 1}, {'x': 3}]
109 | >>> trim({'x': 1, 'y': '', 'z': ['a', '', 'b']})
110 | {'x': 1, 'z': ['a', 'b']}
111 | >>> trim(unflatten({'a#1#2.b': 1}))
112 | {'a': [[{'b': 1}]]}
113 | >>> trim(flatten(unflatten({'a#1#2.b': 1})))
114 | {'a#1#2.b': 1}
115 | """
116 |
117 | def trimlist(x):
118 | y = []
119 | for v in x:
120 | if isinstance(v, list):
121 | v = trimlist(v)
122 | elif isinstance(v, dict):
123 | v = trimdict(v)
124 | if v:
125 | y.append(v)
126 | return y
127 |
128 | def trimdict(x):
129 | y = {}
130 | for k, v in x.items():
131 | if isinstance(v, list):
132 | v = trimlist(v)
133 | elif isinstance(v, dict):
134 | v = trimdict(v)
135 | if v:
136 | y[k] = v
137 | return y
138 |
139 | if isinstance(x, list):
140 | return trimlist(x)
141 | elif isinstance(x, dict):
142 | return trimdict(x)
143 | else:
144 | return x
145 |
146 |
147 | def subdict(d, keys):
148 | """Subset like operation on dictionary.
149 |
150 | >>> subdict({'a': 1, 'b': 2, 'c': 3}, ['a', 'c'])
151 | {'a': 1, 'c': 3}
152 | >>> subdict({'a': 1, 'b': 2, 'c': 3}, ['a', 'c', 'd'])
153 | {'a': 1, 'c': 3}
154 | """
155 | return {k: d[k] for k in keys if k in d}
156 |
157 |
158 | if __name__ == "__main__":
159 | import doctest
160 |
161 | doctest.testmod()
162 |
--------------------------------------------------------------------------------
/infogami/infobase/_dbstore/schema.sql:
--------------------------------------------------------------------------------
1 | $def with (tables, sequences, multisite=False)
2 |
3 | BEGIN;
4 |
5 | -- changelog:
6 | -- 10: added active and bot columns to account and created meta table to track the schema version.
7 |
8 | create table meta (
9 | version int
10 | );
11 | insert into meta (version) values (10);
12 |
13 | $if multisite:
14 | create table site (
15 | id serial primary key,
16 | name text UNIQUE,
17 | created timestamp default(current_timestamp at time zone 'utc')
18 | );
19 |
20 | create table thing (
21 | id serial primary key,
22 | $if multisite:
23 | site_id int references site,
24 | key text,
25 | type int references thing,
26 | latest_revision int default 1,
27 | created timestamp default(current_timestamp at time zone 'utc'),
28 | last_modified timestamp default(current_timestamp at time zone 'utc')
29 | );
30 | $for name in ['type', 'latest_revision', 'last_modified', 'created']:
31 | create index thing_${name}_idx ON thing($name);
32 |
33 | create unique index thing_key_idx ON thing(key);
34 |
35 | $if multisite:
36 | create index thing_site_id_idx ON thing(site_id);
37 |
38 | create table transaction (
39 | id serial primary key,
40 | action varchar(256),
41 | author_id int references thing,
42 | ip inet,
43 | comment text,
44 | bot boolean default 'f', -- true if the change is made by a bot
45 | created timestamp default (current_timestamp at time zone 'utc'),
46 | changes text,
47 | data text
48 | );
49 |
50 | $for name in ['author_id', 'ip', 'created']:
51 | create index transaction_${name}_idx ON transaction($name);
52 |
53 | create table transaction_index (
54 | tx_id int references transaction,
55 | key text,
56 | value text
57 | );
58 |
59 | create index transaction_index_key_value_idx ON transaction_index(key, value);
60 | create index transaction_index_tx_id_idx ON transaction_index(tx_id);
61 |
62 | create table version (
63 | id serial primary key,
64 | thing_id int references thing,
65 | revision int,
66 | transaction_id int references transaction,
67 | UNIQUE (thing_id, revision)
68 | );
69 |
70 | create table property (
71 | id serial primary key,
72 | type int references thing,
73 | name text,
74 | UNIQUE (type, name)
75 | );
76 |
77 | CREATE FUNCTION get_property_name(integer, integer)
78 | RETURNS text AS
79 | 'select property.name FROM property, thing WHERE thing.type = property.type AND thing.id=$$1 AND property.id=$$2;'
80 | LANGUAGE SQL;
81 |
82 | create table account (
83 | $if multisite:
84 | site_id int references site,
85 | thing_id int references thing,
86 | email text,
87 | password text,
88 | active boolean default 't',
89 | bot boolean default 'f',
90 | verified boolean default 'f',
91 |
92 | $if multisite:
93 | UNIQUE(site_id, email)
94 | $else:
95 | UNIQUE(email)
96 | );
97 |
98 | create index account_thing_id_idx ON account(thing_id);
99 | create index account_thing_email_idx ON account(email);
100 | create index account_thing_active_idx ON account(active);
101 | create index account_thing_bot_idx ON account(bot);
102 |
103 | create table data (
104 | thing_id int references thing,
105 | revision int,
106 | data text
107 | );
108 | create unique index data_thing_id_revision_idx ON data(thing_id, revision);
109 |
110 | $ sqltypes = dict(int="int", float="float", boolean="boolean", str="varchar(2048)", datetime="timestamp", ref="int references thing")
111 |
112 | $for table, datatype in tables:
113 | create table $table (
114 | thing_id int references thing,
115 | key_id int references property,
116 | value $sqltypes[datatype],
117 | ordering int default NULL
118 | );
119 | create index ${table}_idx ON ${table}(key_id, value);
120 | create index ${table}_thing_id_idx ON ${table}(thing_id);
121 |
122 | -- sequences --
123 | $for seq in sequences:
124 | CREATE SEQUENCE $seq;
125 |
126 | create table store (
127 | id serial primary key,
128 | key text unique,
129 | json text
130 | );
131 |
132 | create table store_index (
133 | id serial primary key,
134 | store_id int references store,
135 | type text,
136 | name text,
137 | value text
138 | );
139 |
140 | create index store_index_store_id_idx ON store_index (store_id);
141 | create index store_idx ON store_index(type, name, value);
142 |
143 | create table seq (
144 | id serial primary key,
145 | name text unique,
146 | value int default 0
147 | );
148 |
149 | COMMIT;
150 |
151 |
--------------------------------------------------------------------------------
/infogami/infobase/tests/test_account.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from infogami.infobase import common
4 | from infogami.infobase.tests import utils
5 |
6 |
7 | def setup_module(mod):
8 | utils.setup_site(mod)
9 |
10 |
11 | def teardown_module(mod):
12 | site.cache.clear()
13 | utils.teardown_site(mod)
14 |
15 |
16 | class TestAccount:
17 | global site
18 |
19 | def setup_method(self, method):
20 | global db
21 | self.tx = db.transaction()
22 |
23 | def teardown_method(self, method):
24 | self.tx.rollback()
25 | site.cache.clear()
26 |
27 | def test_register(self):
28 | a = site.account_manager
29 | a.register(username="joe", email="joe@example.com", password="secret", data={})
30 |
31 | # login should fail before activation
32 | assert a.login('joe', 'secret') == "account_not_verified"
33 | assert a.login('joe', 'wrong-password') == "account_bad_password"
34 |
35 | a.activate(username="joe")
36 |
37 | assert a.login('joe', 'secret') == "ok"
38 | assert a.login('joe', 'wrong-password') == "account_bad_password"
39 |
40 | def test_register_failures(self, _activate=True):
41 | a = site.account_manager
42 | a.register(
43 | username="joe",
44 | email="joe@example.com",
45 | password="secret",
46 | data={},
47 | _activate=_activate,
48 | )
49 |
50 | try:
51 | a.register(
52 | username="joe", email="joe2@example.com", password="secret", data={}
53 | )
54 | assert False
55 | except common.BadData as e:
56 | assert e.d['message'] == "User already exists: joe"
57 |
58 | try:
59 | a.register(
60 | username="joe2", email="joe@example.com", password="secret", data={}
61 | )
62 | assert False
63 | except common.BadData as e:
64 | assert e.d['message'] == "Email is already used: joe@example.com"
65 |
66 | def test_register_failures2(self):
67 | # test registration without activation + registration with same username/email
68 | self.test_register_failures(_activate=False)
69 |
70 | def encrypt(self, password):
71 | """Generates encrypted password from raw password."""
72 | a = site.account_manager
73 | return a._generate_salted_hash(a.secret_key, password)
74 |
75 | def test_login_account(self):
76 | f = site.account_manager._verify_login
77 | enc_password = self.encrypt("secret")
78 |
79 | assert f(dict(enc_password=enc_password, status="active"), "secret") == "ok"
80 | assert (
81 | f(dict(enc_password=enc_password, status="active"), "bad-password")
82 | == "account_bad_password"
83 | )
84 |
85 | # pending accounts should return "account_not_verified" if the password is correct
86 | assert (
87 | f(dict(enc_password=enc_password, status="pending"), "secret")
88 | == "account_not_verified"
89 | )
90 | assert (
91 | f(dict(enc_password=enc_password, status="pending"), "bad-password")
92 | == "account_bad_password"
93 | )
94 |
95 | def test_update(self):
96 | a = site.account_manager
97 | a.register(username="foo", email="foo@example.com", password="secret", data={})
98 | a.activate("foo")
99 | assert a.login("foo", "secret") == "ok"
100 |
101 | # test update password
102 | assert a.update("foo", password="more-secret") == "ok"
103 | assert a.login("foo", "secret") == "account_bad_password"
104 | assert a.login("foo", "more-secret") == "ok"
105 |
106 | # test update email
107 |
108 | # registering with the same email should fail.
109 | assert pytest.raises(
110 | common.BadData,
111 | a.register,
112 | username="bar",
113 | email="foo@example.com",
114 | password="secret",
115 | data={},
116 | )
117 |
118 | assert a.update("foo", email="foo2@example.com") == "ok"
119 |
120 | # someone else should be able to register with the old email
121 | a.register(username="bar", email="foo@example.com", password="secret", data={})
122 |
123 | # and no one should be allowed to register with new email
124 | assert pytest.raises(
125 | common.BadData,
126 | a.register,
127 | username="bar",
128 | email="foo2@example.com",
129 | password="secret",
130 | data={},
131 | )
132 |
--------------------------------------------------------------------------------
/infogami/infobase/cache.py:
--------------------------------------------------------------------------------
1 | """
2 | Infobase cache.
3 |
4 | Infobase cache contains multiple layers.
5 |
6 | new_objects (thread-local)
7 | special_cache
8 | local_cache (thread-local)
9 | global_cache
10 |
11 | new_objects is a thread-local dictionary containing objects created in the
12 | current request. It is stored at web.ctx.new_objects. new_objects are added
13 | to the global cache at the end of every request. It is the responsibility of
14 | the DBStore to populate this on write and it should also make sure that this
15 | is cleared on write failures.
16 |
17 | special_cache is an optional cache provided to cache most frequently accessed
18 | objects (like types and properties) and the application is responsible to keep
19 | it in sync.
20 |
21 | local_cache is a thread-local cache maintained to avoid repeated requests to
22 | global cache. This is stored at web.ctx.local_cache.
23 |
24 | global_cache is typically expensive to access, so its access is minimized.
25 | Typical examples of global_cache are LRU cache and memcached cache.
26 |
27 | Any elements added to the infobase cache during a request are cached locally until the end
28 | of that request and then they are added to the global cache.
29 | """
30 |
31 | import logging
32 |
33 | import web
34 |
35 | from infogami.infobase import lru
36 |
37 | logger = logging.getLogger("infobase.cache")
38 |
39 |
40 | class NoneDict:
41 | def __getitem__(self, key):
42 | raise KeyError(key)
43 |
44 | def __setitem__(self, key, value):
45 | pass
46 |
47 | def update(self, d):
48 | pass
49 |
50 |
51 | class MemcachedDict:
52 | def __init__(self, memcache_client=None, servers=[]):
53 | if memcache_client is None:
54 | import memcache
55 |
56 | memcache_client = memcache.Client(servers)
57 | self.memcache_client = memcache_client
58 |
59 | def __getitem__(self, key):
60 | key = web.safestr(key)
61 | value = self.memcache_client.get(key)
62 | if value is None:
63 | raise KeyError(key)
64 | return value
65 |
66 | def __setitem__(self, key, value):
67 | key = web.safestr(key)
68 | logger.debug("MemcachedDict.set: %s", key)
69 | self.memcache_client.set(key, value)
70 |
71 | def update(self, d):
72 | d = {web.safestr(k): v for k, v in d.items()}
73 | logger.debug("MemcachedDict.update: %s", list(d))
74 | self.memcache_client.set_multi(d)
75 |
76 | def clear(self):
77 | self.memcache_client.flush_all()
78 |
79 |
80 | _cache_classes = {}
81 |
82 |
83 | def register_cache(type, klass):
84 | _cache_classes[type] = klass
85 |
86 |
87 | register_cache('lru', lru.LRU)
88 | register_cache('memcache', MemcachedDict)
89 |
90 |
91 | def create_cache(type, **kw):
92 | klass = _cache_classes.get(type) or NoneDict
93 | return klass(**kw)
94 |
95 |
96 | special_cache: dict = {}
97 | global_cache = lru.LRU(200)
98 |
99 |
100 | def loadhook():
101 | web.ctx.new_objects = {}
102 | web.ctx.local_cache = {}
103 | web.ctx.locally_added = {}
104 |
105 |
106 | def unloadhook():
107 | """Called at the end of every request."""
108 | d = {}
109 | d.update(web.ctx.locally_added)
110 | d.update(web.ctx.new_objects)
111 |
112 | if d:
113 | global_cache.update(d)
114 |
115 |
116 | class Cache:
117 | def __getitem__(self, key):
118 | ctx = web.ctx
119 | obj = (
120 | ctx.new_objects.get(key)
121 | or special_cache.get(key)
122 | or ctx.local_cache.get(key)
123 | )
124 | if not obj:
125 | obj = global_cache[key]
126 | ctx.local_cache[key] = obj
127 |
128 | return obj
129 |
130 | def get(self, key, default=None):
131 | try:
132 | return self[key]
133 | except KeyError:
134 | return default
135 |
136 | def __contains__(self, key):
137 | """Tests whether an element is present in the cache.
138 | This function call is expensive. Provided for the sake of completeness.
139 |
140 | Use:
141 | obj = cache.get(key)
142 | if obj is None:
143 | do_something()
144 |
145 | instead of:
146 | if key in cache:
147 | obj = cache[key]
148 | else:
149 | do_something()
150 | """
151 | try:
152 | self[key]
153 | return True
154 | except KeyError:
155 | return False
156 |
157 | def __setitem__(self, key, value):
158 | web.ctx.local_cache[key] = value
159 | web.ctx.locally_added[key] = value
160 |
161 | def clear(self, local=False):
162 | """Clears the cache.
163 | When local=True, only the local cache is cleared.
164 | """
165 | web.ctx.locally_added.clear()
166 | web.ctx.local_cache.clear()
167 | web.ctx.new_objects.clear()
168 | if not local:
169 | global_cache.clear()
170 |
--------------------------------------------------------------------------------
/infogami/core/files/js/jquery/jquery.dimensions.pack.js:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2007 Paul Bakaus (paul.bakaus@googlemail.com) and Brandon Aaron (brandon.aaron@gmail.com || http://brandonaaron.net)
2 | * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
3 | * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
4 | *
5 | * $LastChangedDate: 2007-08-12 22:47:23 -0500 (Sun, 12 Aug 2007) $
6 | * $Rev: 2669 $
7 | *
8 | * Version: 1.1
9 | *
10 | * Requires: jQuery 1.1.3+
11 | */
12 | eval(function(p,a,c,k,e,r){e=function(c){return(c35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('(8($){k e=$.1u.B,p=$.1u.p;$.1u.J({B:8(){3(!1[0])f();3(1[0]==o)3(($.9.15||$.9.O)&&$(5).p()>m.18)6 m.16-i();s 6 m.16||$.K&&5.11.1I||5.n.1I;3(1[0]==5)6 1F.1E(5.n.1W,5.n.1b);6 e.1x(1,1Q)},p:8(){3(!1[0])f();3(1[0]==o)3(($.9.15||$.9.O)&&$(5).B()>m.16)6 m.18-i();s 6 m.18||$.K&&5.11.1P||5.n.1P;3(1[0]==5)3($.9.15){k a=m.1t;m.19(26,m.1s);k b=m.1t;m.19(a,m.1s);6 5.n.17+b}s 6 1F.1E(5.n.25,5.n.17);6 p.1x(1,1Q)},16:8(){3(!1[0])f();6 1[0]==o||1[0]==5?1.B():1.V(\':L\')?1[0].1b-h(1,\'t\')-h(1,\'1M\'):1.B()+h(1,\'1m\')+h(1,\'1L\')},18:8(){3(!1[0])f();6 1[0]==o||1[0]==5?1.p():1.V(\':L\')?1[0].17-h(1,\'r\')-h(1,\'1H\'):1.p()+h(1,\'1i\')+h(1,\'1G\')},23:8(a){3(!1[0])f();a=$.J({w:u},a||{});6 1[0]==o||1[0]==5?1.B():1.V(\':L\')?1[0].1b+(a.w?(h(1,\'H\')+h(1,\'1D\')):0):1.B()+h(1,\'t\')+h(1,\'1M\')+h(1,\'1m\')+h(1,\'1L\')+(a.w?(h(1,\'H\')+h(1,\'1D\')):0)},1V:8(a){3(!1[0])f();a=$.J({w:u},a||{});6 1[0]==o||1[0]==5?1.p():1.V(\':L\')?1[0].17+(a.w?(h(1,\'I\')+h(1,\'1y\')):0):1.p()+h(1,\'r\')+h(1,\'1H\')+h(1,\'1i\')+h(1,\'1G\')+(a.w?(h(1,\'I\')+h(1,\'1y\')):0)},l:8(a){3(!1[0])f();3(a!=1B)6 1.1w(8(){3(1==o||1==5)o.19(a,$(o).q());s 1.l=a});3(1[0]==o||1[0]==5)6 m.1t||$.K&&5.11.l||5.n.l;6 1[0].l},q:8(a){3(!1[0])f();3(a!=1B)6 1.1w(8(){3(1==o||1==5)o.19($(o).l(),a);s 1.q=a});3(1[0]==o||1[0]==5)6 m.1s||$.K&&5.11.q||5.n.q;6 1[0].q},Y:8(a){6 1.1N({w:u,C:u,v:1.z()},a)},1N:8(b,c){3(!1[0])f();k x=0,y=0,G=0,F=0,7=1[0],4=1[0],N,X,W=$.A(7,\'Y\'),E=$.9.15,M=$.9.24,14=$.9.O,1r=$.9.13,P=$.9.13&&12($.9.1q)>1p,1o=u,1n=u,b=$.J({w:Q,10:u,1e:u,C:Q,1K:u,v:5.n},b||{});3(b.1K)6 1.1J(b,c);3(b.v.1k)b.v=b.v[0];3(7.D==\'U\'){x=7.S;y=7.R;3(E){x+=h(7,\'I\')+(h(7,\'r\')*2);y+=h(7,\'H\')+(h(7,\'t\')*2)}s 3(14){x+=h(7,\'I\');y+=h(7,\'H\')}s 3((M&&1g.K)){x+=h(7,\'r\');y+=h(7,\'t\')}s 3(P){x+=h(7,\'I\')+h(7,\'r\');y+=h(7,\'H\')+h(7,\'t\')}}s{Z{X=$.A(4,\'Y\');x+=4.S;y+=4.R;3(E||M||P){x+=h(4,\'r\');y+=h(4,\'t\');3(E&&X==\'1f\')1o=Q;3(M&&X==\'22\')1n=Q}N=4.z||5.n;3(b.C||E){Z{3(b.C){G+=4.l;F+=4.q}3(14&&($.A(4,\'21\')||\'\').20(/1Z-1Y|1X/)){G=G-((4.l==4.S)?4.l:0);F=F-((4.q==4.R)?4.q:0)}3(E&&4!=7&&$.A(4,\'1d\')!=\'L\'){x+=h(4,\'r\');y+=h(4,\'t\')}4=4.1C}T(4!=N)}4=N;3(4==b.v&&!(4.D==\'U\'||4.D==\'1c\')){3(E&&4!=7&&$.A(4,\'1d\')!=\'L\'){x+=h(4,\'r\');y+=h(4,\'t\')}3(((1r&&!P)||14)&&X!=\'1l\'){x-=h(N,\'r\');y-=h(N,\'t\')}1A}3(4.D==\'U\'||4.D==\'1c\'){3(((1r&&!P)||(M&&$.K))&&W!=\'1f\'&&W!=\'1z\'){x+=h(4,\'I\');y+=h(4,\'H\')}3(P||(E&&!1o&&W!=\'1z\')||(M&&W==\'1l\'&&!1n)){x+=h(4,\'r\');y+=h(4,\'t\')}1A}}T(4)}k a=j(7,b,x,y,G,F);3(c){$.J(c,a);6 1}s{6 a}},1J:8(b,c){3(!1[0])f();k x=0,y=0,G=0,F=0,4=1[0],z,b=$.J({w:Q,10:u,1e:u,C:Q,v:5.n},b||{});3(b.v.1k)b.v=b.v[0];Z{x+=4.S;y+=4.R;z=4.z||5.n;3(b.C){Z{G+=4.l;F+=4.q;4=4.1C}T(4!=z)}4=z}T(4&&4.D!=\'U\'&&4.D!=\'1c\'&&4!=b.v);k a=j(1[0],b,x,y,G,F);3(c){$.J(c,a);6 1}s{6 a}},z:8(){3(!1[0])f();k a=1[0].z;T(a&&(a.D!=\'U\'&&$.A(a,\'Y\')==\'1l\'))a=a.z;6 $(a)}});k f=8(){1U"1T: 1g 1S V 1R";};k h=8(a,b){6 12($.A(a.1k?a[0]:a,b))||0};k j=8(a,b,x,y,d,c){3(!b.w){x-=h(a,\'I\');y-=h(a,\'H\')}3(b.10&&(($.9.13&&12($.9.1q)<1p)||$.9.O)){x+=h(a,\'r\');y+=h(a,\'t\')}s 3(!b.10&&!(($.9.13&&12($.9.1q)<1p)||$.9.O)){x-=h(a,\'r\');y-=h(a,\'t\')}3(b.1e){x+=h(a,\'1i\');y+=h(a,\'1m\')}3(b.C&&(!$.9.O||a.S!=a.l&&a.R!=a.l)){d-=a.l;c-=a.q}6 b.C?{1h:y-c,1j:x-d,q:c,l:d}:{1h:y,1j:x}};k g=0;k i=8(){3(!g){k a=$(\'<1v>\').A({p:1a,B:1a,1d:\'2c\',Y:\'1f\',1h:-1O,1j:-1O}).2b(\'n\');g=1a-a.2a(\'<1v>\').29(\'1v\').A({p:\'1a%\',B:28}).p();a.27()}6 g}})(1g);',62,137,'|this||if|parent|document|return|elem|function|browser|||||||||||var|scrollLeft|self|body|window|width|scrollTop|borderLeftWidth|else|borderTopWidth|false|relativeTo|margin|||offsetParent|css|height|scroll|tagName|mo|st|sl|marginTop|marginLeft|extend|boxModel|visible|ie|op|opera|sf3|true|offsetTop|offsetLeft|while|BODY|is|elemPos|parPos|position|do|border|documentElement|parseInt|safari|oa|mozilla|innerHeight|offsetWidth|innerWidth|scrollTo|100|offsetHeight|HTML|overflow|padding|absolute|jQuery|top|paddingLeft|left|jquery|static|paddingTop|relparent|absparent|520|version|sf|pageYOffset|pageXOffset|fn|div|each|apply|marginRight|fixed|break|undefined|parentNode|marginBottom|max|Math|paddingRight|borderRightWidth|clientHeight|offsetLite|lite|paddingBottom|borderBottomWidth|offset|1000|clientWidth|arguments|empty|collection|Dimensions|throw|outerWidth|scrollHeight|inline|row|table|match|display|relative|outerHeight|msie|scrollWidth|99999999|remove|200|find|append|appendTo|auto'.split('|'),0,{}))
--------------------------------------------------------------------------------
/infogami/core/templates/default_edit.html:
--------------------------------------------------------------------------------
1 | $def with (page)
2 |
3 | $ _t = i18n.get_namespace(page.type.key)
4 | $ _ = i18n.get_namespace('/mode/edit')
5 |
6 | $var title: $_.edit_title(page.name)
7 |
8 | $add_javascript("/static/js/repetition/repetition-model.js")
9 | $add_javascript("/static/js/jquery/jquery.js")
10 |
11 |
64 |
65 | $def display_multiple_row(property, value, index):
66 | $if index is None:
67 | $ attrs = 'id="row_%s" repeat="template"' % property.name
68 | $ prefix = '%s#[row_%s]' % (property.name, property.name)
69 | $else:
70 | $ attrs = 'repeat="%d""' % index
71 | $ prefix = '%s#%d' % (property.name, index)
72 |
73 | $if property.expected_type.kind == "embeddable":
74 | $for p in property.expected_type.properties:
75 | $:thinginput(value and value[p.name], name=prefix + "." + p.name, expected_type=p.expected_type)
76 | $else:
77 | $:thinginput(value, name=prefix, expected_type=property.expected_type)
78 | $:move_buttons(property.name)
79 |
80 | $def display_header(property):
81 | $if property.expected_type.kind == "embeddable":
82 |
83 | $for p in property.expected_type.properties:
84 | $i18n.get(p.expected_type.key, p.name)
85 |
86 |
87 | $def display_multiple(property, value):
88 |
89 |
90 | $:display_header(property)
91 |
92 |
93 | $for i, v in enumerate(value):
94 | $:display_multiple_row(property, v, i)
95 | $:display_multiple_row(property, None, None)
96 | $:add_button(property)
97 |
98 |
99 |
100 | $def display_regular(p, value):
101 | $:thinginput(value, p)
102 |
103 | $def display(property, value):
104 | $if property.unique:
105 | $:display_regular(property, value)
106 | $else:
107 | $:display_multiple(property, value)
108 |
109 | $def move_buttons(key):
110 |
111 | -
112 | ↑
113 | ↓
114 |
115 |
116 | $def add_button(property):
117 |
118 | $if property.expected_type.kind == "embeddable":
119 | $for p in property.expected_type.properties:
120 |
121 | $else:
122 |
123 | +
124 |
125 |
126 |
127 |
128 |
129 | $:macros.TypeChanger(page.type, usetable=True)
130 |
131 |
132 |
133 |
134 |
135 |
136 | $for p in page.type.properties:
137 | $ label = _t[p.name]
138 | $ value = page[p.name]
139 |
140 |
141 | $label
142 | $:display(p, value)
143 |
144 |
145 |
146 |
147 | $:_.edit_summary
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
--------------------------------------------------------------------------------
/infogami/infobase/tests/test_writequery.py:
--------------------------------------------------------------------------------
1 | import web
2 |
3 | from infogami.infobase import common, writequery
4 | from infogami.infobase.tests import utils
5 |
6 |
7 | def setup_module(mod):
8 | utils.setup_site(mod)
9 |
10 | type_book = {
11 | "key": "/type/book",
12 | "kind": "regular",
13 | "type": {"key": "/type/type"},
14 | "properties": [
15 | {"name": "title", "expected_type": {"key": "/type/string"}, "unique": True},
16 | {
17 | "name": "authors",
18 | "expected_type": {"key": "/type/author"},
19 | "unique": False,
20 | },
21 | {
22 | "name": "publish_year",
23 | "expected_type": {"key": "/type/int"},
24 | "unique": True,
25 | },
26 | {"name": "links", "expected_type": {"key": "/type/link"}, "unique": False},
27 | ],
28 | }
29 | type_author = {
30 | "key": "/type/author",
31 | "kind": "regular",
32 | "type": {"key": "/type/type"},
33 | "properties": [
34 | {"name": "name", "expected_type": {"key": "/type/string"}, "unique": True}
35 | ],
36 | }
37 | type_link = {
38 | "key": "/type/link",
39 | "kind": "embeddable",
40 | "type": {"key": "/type/type"},
41 | "properties": [
42 | {"name": "title", "expected_type": {"key": "/type/string"}, "unique": True},
43 | {"name": "url", "expected_type": {"key": "/type/string"}, "unique": True},
44 | ],
45 | }
46 | mod.site.save_many([type_book, type_author, type_link])
47 |
48 |
49 | def teardown_module(mod):
50 | utils.teardown_site(mod)
51 |
52 |
53 | class DBTest:
54 | def setup_method(self, method):
55 | global db
56 | self.tx = db.transaction()
57 |
58 | def teardown_method(self, method):
59 | self.tx.rollback()
60 |
61 |
62 | class TestSaveProcessor(DBTest):
63 | global site
64 |
65 | def test_errors(self):
66 | def save_many(query):
67 | try:
68 | site.save_many(query)
69 | except common.InfobaseException as e:
70 | return e.dict()
71 |
72 | q = {
73 | "key": "/authors/1",
74 | }
75 | assert save_many([q]) == {
76 | 'error': 'bad_data',
77 | 'message': 'missing type',
78 | 'at': {'key': '/authors/1'},
79 | }
80 |
81 | q = {"key": "/authors/1", "type": "/type/author", "name": ["a", "b"]}
82 | assert save_many([q]) == {
83 | 'error': 'bad_data',
84 | 'message': 'expected atom, found list',
85 | 'at': {'key': '/authors/1', 'property': 'name'},
86 | 'value': ['a', 'b'],
87 | }
88 |
89 | q = {"key": "/authors/1", "type": "/type/author", "name": 123}
90 | assert save_many([q]) == {
91 | 'error': 'bad_data',
92 | 'message': 'expected /type/string, found /type/int',
93 | 'at': {'key': '/authors/1', 'property': 'name'},
94 | "value": 123,
95 | }
96 |
97 | q = {
98 | "key": "/books/1",
99 | "type": "/type/book",
100 | "authors": [{"key": "/authors/1"}],
101 | }
102 | assert save_many([q]) == {
103 | 'error': 'notfound',
104 | 'key': '/authors/1',
105 | 'at': {'key': '/books/1', 'property': 'authors'},
106 | }
107 |
108 | q = {"key": "/books/1", "type": "/type/book", "publish_year": "not-int"}
109 | assert save_many([q]) == {
110 | 'error': 'bad_data',
111 | 'message': "invalid literal for int() with base 10: 'not-int'",
112 | 'at': {'key': '/books/1', 'property': 'publish_year'},
113 | "value": "not-int",
114 | }
115 |
116 | q = {"key": "/books/1", "type": "/type/book", "links": ["foo"]}
117 | assert save_many([q]) == {
118 | 'error': 'bad_data',
119 | 'message': 'expected /type/link, found /type/string',
120 | 'at': {'key': '/books/1', 'property': 'links'},
121 | 'value': 'foo',
122 | }
123 |
124 | q = {"key": "/books/1", "type": "/type/book", "links": [{"title": 1}]}
125 | assert save_many([q]) == {
126 | 'error': 'bad_data',
127 | 'message': 'expected /type/string, found /type/int',
128 | 'at': {'key': '/books/1', 'property': 'links.title'},
129 | 'value': 1,
130 | }
131 |
132 | def test_process_value(self):
133 | def property(expected_type, unique=True, name='foo'):
134 | return web.storage(
135 | expected_type=web.storage(key=expected_type, kind='regular'),
136 | unique=unique,
137 | name=name,
138 | )
139 |
140 | p = writequery.SaveProcessor(site.store, None)
141 | assert p.process_value(1, property('/type/int')) == 1
142 | assert p.process_value('1', property('/type/int')) == 1
143 | assert p.process_value(['1', '2'], property('/type/int', unique=False)) == [
144 | 1,
145 | 2,
146 | ]
147 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # infogami
2 | [](https://travis-ci.org/internetarchive/infogami)
3 |
4 | The Open Library interface is powered by infogami -- a cleaner, simpler alternative to other wiki applications. But unlike other wikis, Infogami has the flexibility to handle different classes of data. Most wikis let you store unstructured pages -- big blocks of text. Infogami lets you store structured data.
5 |
6 | In addition to this, infogami facilitates the creation of dynamic HTML templates and macros. This flexible environment enables users to create, share and build collaborative interfaces. With Open Library in particular, we are focused on building a productive and vital community focused on the discovery of books.
7 |
8 | Applications are written by extending Infogami through two layers: plugins and templates. Plugins are Python modules that get loaded into Infogami through a special API. (See [an overview of Infogami plugins][6].) They are invoked by submitting HTTP requests to the application, either HTML form posts or direct GET requests. Plugins can use any library or application code that they wish, and they create Python objects to represent results, that then get expanded to HTML by templates. Templates are a mixture of HTML text and user-written code, in the spirit of PHP templates. The user-written code is in a special-purpose scripting language that is approximately a Python subset, which runs in a hopefully-secure server-side interpreter embedded in the Python app that has limited access to system functions and resources.
9 |
10 | In this document, you'll learn how to develop for infogami, including building new templates for displaying your own data, running your own copy, and developing new features and plugins.
11 |
12 | ## Summary (Audience Statement)
13 |
14 | This document describes the internal workings of the Open Library software, from a developers' point of view. You should read it if you are:
15 |
16 | 1) a software developer wanting to add new features to Open Library. To do this, you will also have to be a good Python programmer experienced in writing web server applications (not necessarily in Python). The document will explain the Open Library's software architecture and its internal interfaces, and will explain how to write extensions (plugins), but the sections about plugin writing will assume that you are familiar with Python and web programming in general.
17 |
18 | If you do not yet know Python, you should first study the Python documentation or the free book Dive into Python. Python is an easy language to learn, but the OL codebase is probably not understandable by complete beginners.
19 | For web server principles, the somewhat dated Philip and Alex's Guide to Web Publishing is still an informative read, though maybe someone can suggest something newer. You should also understand the principles of software security -- see David A Wheeler's page for many documents.
20 |
21 | 2) A user or web designer wanting to improve or customize the Open Library's user interface, either for yourself or for our whole community. You will mainly want to study the section about template programming. You will need to know how to write HTML and it will help if you've done some server-side template programming (such as in PHP). It will also help if you've had some exposure to Python, but the programming skills you'll need for template writing are generally less intense than they'd be for extension writing.
22 |
23 | 3) A general user just wanting to know how the software works behind the scenes. You might not understand all the details, but reading the doc should give you a general impression of how sites like this are put together.
24 |
25 | 4) A librarian or metadata maintainer wanting to process large volumes of metadata for import into the Open Library. If you only want to import a few books, it's probably easiest to use the web interface (or the Z39.50 interface once we have one). To import bulk data, you'll have to process it into a format that Open Library can understand, which may require programming, but you can use your own choices of languages and platforms for that purpose since you only have to create uploadable files, rather than use language-specific interfaces. You'll mainly want to look at the section about data formats and schemas.
26 |
27 | If you just want to be an OL user accessing or editing book data, you do NOT need to read this doc. The doc is about how to customize and extend the software, not how to use it. As developers and designers, our goal is to make the site self-explanatory for users and not need much separate documentation, but we do have some user docs at /help.
28 |
29 | ## Introduction
30 |
31 | Infogami is a wiki application framework built on web.py. Actual applications (like Open Library) are written by extending Infogami through two layers: plugins and templates. Plugins are Python modules that get loaded into Infogami through a special API. They are invoked by submitting HTTP requests to the application, either HTML form posts or direct GET requests. Plugins can use any library or application code that they wish, and they create Python objects to represent results, that then get expanded to HTML by templates. Templates are a mixture of HTML text and user-written code, approximately in the spirit of PHP templates. The user-written code is in a special-purpose scripting language that is approximately a Python subset, which runs in a hopefully-secure server-side interpreter (embedded in the Python app) that has limited access to system functions and resources.
32 |
33 | ## Continued
34 |
35 | See docs @ https://openlibrary.org/dev/docs/infogami
36 |
37 |
--------------------------------------------------------------------------------
/test/test_dbstore.py:
--------------------------------------------------------------------------------
1 | import os
2 | import unittest
3 |
4 | import pytest
5 | import web
6 |
7 | from infogami.infobase import common, dbstore, infobase
8 |
9 |
10 | class InfobaseTestCase(unittest.TestCase):
11 | def get_site_store(self):
12 | return self.ib.get('test')
13 |
14 |
15 | @pytest.fixture(scope="session")
16 | def site():
17 | # TODO: this does not clear data between tests. Make this work in scope=class
18 | user = os.getenv('USER')
19 | web.config.db_parameters = dict(
20 | host='postgres', dbn='postgres', db='infobase_test', user=user, pw=''
21 | )
22 | store = dbstore.DBStore(dbstore.Schema())
23 | store.db.printing = False
24 | ib = infobase.Infobase(store, 'secret')
25 | return ib.create('test')
26 |
27 |
28 | class DBStoreTest(InfobaseTestCase):
29 | @pytest.mark.skip(reason="is already skipped with an underscore")
30 | def _test_save(self):
31 | store = self.get_site_store()
32 |
33 | d = dict(
34 | key='/x', type={'key': '/type/type'}, title='foo', x={'x': 1, 'y': 'foo'}
35 | )
36 | store.save('/x', d)
37 |
38 | d = store.get('/x')._get_data()
39 |
40 | del d['title']
41 | d['body'] = 'bar'
42 | store.save('/x', d)
43 |
44 |
45 | class TestSaveTest:
46 | def testSave(self, site):
47 | d = dict(key='/foo', type='/type/object')
48 | assert site.save('/foo', d) == {'key': '/foo', 'revision': 1}
49 |
50 | d = dict(key='/foo', type='/type/object', x=1)
51 | assert site.save('/foo', d) == {'key': '/foo', 'revision': 2}
52 |
53 | def new(self, site, error=None, **d):
54 | try:
55 | key = d['key']
56 | assert site.save(key, d) == {'key': key, 'revision': 1}
57 | except common.InfobaseException as e:
58 | assert str(e) == error, (str(e), error)
59 |
60 | def test_type(self, site):
61 | self.new(site, key='/a', type='/type/object')
62 | self.new(site, key='/b', type={'key': '/type/object'})
63 | self.new(
64 | site,
65 | key='/c',
66 | type='/type/noobject',
67 | error='{"error": "notfound", "key": "/type/noobject", "at": {"key": "/c", "property": "type"}}',
68 | )
69 |
70 | def test_expected_type(self, site):
71 | def p(name, expected_type, unique=True):
72 | return locals()
73 |
74 | self.new(
75 | site,
76 | key='/type/test',
77 | type='/type/type',
78 | properties=[
79 | p('i', '/type/int'),
80 | p('s', '/type/string'),
81 | p('f', '/type/float'),
82 | p('t', '/type/type'),
83 | ],
84 | )
85 |
86 | self.new(site, key='/aa', type='/type/test', i='1', f='1.2', t='/type/test')
87 | self.new(
88 | site,
89 | key='/bb',
90 | type='/type/test',
91 | i={'type': '/type/int', 'value': '1'},
92 | f='1.2',
93 | t={'key': '/type/test'},
94 | )
95 | self.new(
96 | site,
97 | key='/e1',
98 | type='/type/test',
99 | i='bad integer',
100 | error='{"error": "bad_data", "message": "invalid literal for int() with base 10: \'bad integer\'", "at": {"key": "/e1", "property": "i"}, "value": "bad integer"}', # noqa: E501
101 | )
102 |
103 | @pytest.mark.skip(
104 | reason="d is expected to be json (DBSiteStore.get()), but is actually a string from DBStore.get()"
105 | )
106 | def test_embeddable_types(self, site):
107 | def test(site, key, type):
108 | self.new(
109 | site,
110 | key=key,
111 | type=type,
112 | link=dict(title='foo', link='http://infogami.org'),
113 | )
114 | d = site.get(key)._get_data()
115 | assert d['link']['title'] == 'foo'
116 | assert d['link']['link'] == 'http://infogami.org'
117 |
118 | def p(name, expected_type, unique=True, **d):
119 | return locals()
120 |
121 | self.new(
122 | site,
123 | key='/type/link',
124 | type='/type/type',
125 | properties=[p('title', '/type/string'), p('link', '/type/string')],
126 | kind='embeddable',
127 | )
128 | self.new(
129 | site,
130 | key='/type/book',
131 | type='/type/type',
132 | properties=[p('link', '/type/link')],
133 | )
134 |
135 | test(site, '/aaa', '/type/object')
136 | test(site, '/bbb', '/type/book')
137 |
138 | @pytest.mark.skip(
139 | reason="site.things(query) always returns [], suspect these tests are old and superseded by those in infogami/infobase/tests"
140 | )
141 | def test_things_with_embeddable_types(self, site):
142 | def link(title, url):
143 | return dict(title=title, url='http://example.com/' + url)
144 |
145 | self.new(
146 | site, key='/x', type='/type/object', links=[link('a', 'a'), link('b', 'b')]
147 | )
148 | self.new(
149 | site, key='/y', type='/type/object', links=[link('a', 'b'), link('b', 'a')]
150 | )
151 |
152 | def things(site, query, result):
153 | x = site.things(query)
154 | assert sorted(x) == sorted(result)
155 |
156 | things(
157 | site,
158 | {
159 | 'type': '/type/object',
160 | 'links': {'title': 'a', 'url': 'http://example.com/a'},
161 | },
162 | ['/x'],
163 | )
164 | things(
165 | site,
166 | {
167 | 'type': '/type/object',
168 | 'links': {'title': 'a', 'url': 'http://example.com/b'},
169 | },
170 | ['/y'],
171 | )
172 | things(site, {'type': '/type/object', 'links': {'title': 'a'}}, ['/x', '/y'])
173 | things(
174 | site,
175 | {'type': '/type/object', 'links': {'url': 'http://example.com/a'}},
176 | ['/x', '/y'],
177 | )
178 |
--------------------------------------------------------------------------------
/infogami/__init__.py:
--------------------------------------------------------------------------------
1 | """Infogami: Structured Wiki (http://infogami.org)"""
2 |
3 | __version__ = "0.5dev"
4 |
5 | import sys
6 |
7 | import web
8 |
9 | from infogami import config
10 |
11 | usage = """
12 | Infogami
13 |
14 | list of commands:
15 |
16 | run start the webserver
17 | dbupgrade upgrade the database
18 | help show this
19 | """
20 |
21 | _actions = []
22 |
23 |
24 | def action(f):
25 | """Decorator to register an infogami action."""
26 | _actions.append(f)
27 | return f
28 |
29 |
30 | _install_hooks = []
31 |
32 |
33 | def install_hook(f):
34 | """Decorator to register install hook."""
35 | _install_hooks.append(f)
36 | return f
37 |
38 |
39 | def find_action(name):
40 | for a in _actions:
41 | if a.__name__ == name:
42 | return a
43 |
44 |
45 | def _setup():
46 | # if config.db_parameters is None:
47 | # raise Exception('infogami.config.db_parameters is not specified')
48 |
49 | if config.site is None:
50 | raise Exception('infogami.config.site is not specified')
51 |
52 | if config.bugfixer:
53 | web.webapi.internalerror = web.emailerrors(config.bugfixer, web.debugerror)
54 | web.internalerror = web.webapi.internalerror
55 | web.config.db_parameters = config.db_parameters
56 | web.config.db_printing = config.db_printing
57 |
58 | if config.get("debug", None) is not None:
59 | web.config.debug = config.debug
60 |
61 | from infogami.utils import delegate
62 |
63 | delegate._load()
64 |
65 | # setup context etc.
66 | delegate.fakeload()
67 |
68 |
69 | @action
70 | def startserver(*args):
71 | """Start webserver."""
72 | from infogami.utils import delegate
73 |
74 | sys.argv = [sys.argv[0]] + list(args)
75 | web.ctx.clear()
76 | delegate.app.run(*config.middleware)
77 |
78 |
79 | @action
80 | def help(name=None):
81 | """Show this help."""
82 |
83 | a = name and find_action(name)
84 |
85 | print("Infogami Help")
86 | print("")
87 |
88 | if a:
89 | print(f" {a.__name__}\t{a.__doc__}")
90 | else:
91 | print("Available actions")
92 | for a in _actions:
93 | print(f" {a.__name__}\t{a.__doc__}")
94 |
95 |
96 | @action
97 | def install():
98 | """Setup everything."""
99 |
100 | # set debug=False to avoid reload magic.
101 | web.config.debug = False
102 |
103 | from infogami.utils import delegate
104 |
105 | delegate.fakeload()
106 | if not web.ctx.site.exists():
107 | web.ctx.site.create()
108 |
109 | delegate.admin_login()
110 | for a in _install_hooks:
111 | print(a.__name__, file=web.debug)
112 | a()
113 |
114 |
115 | @action
116 | def shell(*args):
117 | """Interactive Shell"""
118 | if "--ipython" in args:
119 | """IPython Interactive Shell - IPython must be installed to use."""
120 | # remove an argument that confuses ipython
121 | sys.argv.pop(sys.argv.index("--ipython"))
122 | from IPython.Shell import IPShellEmbed
123 |
124 | import infogami # noqa: F401
125 | from infogami.core import db # noqa: F401
126 | from infogami.utils import delegate
127 | from infogami.utils.context import context as ctx # noqa: F401
128 |
129 | delegate.fakeload()
130 | ipshell = IPShellEmbed()
131 | ipshell()
132 | else:
133 | from code import InteractiveConsole
134 |
135 | console = InteractiveConsole()
136 | console.push("import infogami")
137 | console.push("from infogami.utils import delegate")
138 | console.push("from infogami.core import db")
139 | console.push("from infogami.utils.context import context as ctx")
140 | console.push("delegate.fakeload()")
141 | console.interact()
142 |
143 |
144 | @action
145 | def runscript(filename, *args):
146 | """Executes given script after setting up the plugins."""
147 | sys.argv = [filename] + list(args)
148 | g = {"__name__": "__main__"}
149 | with open(filename) as in_file:
150 | exec(in_file.read(), g, g)
151 |
152 |
153 | def run_action(name, args=[]):
154 | if a := find_action(name):
155 | a(*args)
156 | else:
157 | print('unknown command', name, file=sys.stderr)
158 | help()
159 |
160 |
161 | def run(args=None):
162 | if args is None:
163 | args = sys.argv[1:]
164 |
165 | _setup()
166 | if len(args) == 0:
167 | run_action("startserver")
168 | else:
169 | run_action(args[0], args[1:])
170 |
171 |
172 | def load_config(config_file):
173 | import yaml
174 |
175 | from infogami.infobase import config as infobase_config
176 | from infogami.infobase import lru
177 | from infogami.infobase import server as infobase_server
178 |
179 | def storify(d):
180 | if isinstance(d, dict):
181 | return web.storage((k, storify(v)) for k, v in d.items())
182 | elif isinstance(d, list):
183 | return [storify(x) for x in d]
184 | else:
185 | return d
186 |
187 | # load config
188 | with open(config_file) as in_file:
189 | runtime_config = yaml.safe_load(in_file)
190 |
191 | # update config
192 | for k, v in runtime_config.items():
193 | setattr(config, k, storify(v))
194 |
195 | for k, v in runtime_config.get('infobase', {}).items():
196 | setattr(infobase_config, k, storify(v))
197 |
198 | # setup python path
199 | sys.path += config.get('python_path', [])
200 |
201 | config.db_parameters = infobase_server.parse_db_parameters(config.db_parameters)
202 | web.config.db_parameters = config.db_parameters
203 |
204 | # setup infobase
205 | if config.get('cache_size'):
206 | from infogami.infobase import cache
207 |
208 | cache.global_cache = lru.LRU(config.cache_size)
209 |
210 | if config.get('secret_key'):
211 | infobase_config.secret_key = config.secret_key
212 |
213 | # setup smtp_server
214 | if config.get('smtp_server'):
215 | web.config.smtp_server = config.smtp_server
216 |
217 |
218 | def main(config_file, *args):
219 | """Start Infogami using config file."""
220 | load_config(config_file)
221 | run(args)
222 |
--------------------------------------------------------------------------------