Compare commits

..

5 commits
master ... v0.4

Author SHA1 Message Date
Lance Edgar 088c87e209 Merge branch 'master' of ssh://edbob.org/srv/git/rattail.pyramid 2012-12-26 10:07:21 -08:00
Lance Edgar 3edef77e81 convenience commit 2012-12-26 10:07:04 -08:00
Lance Edgar b010bc90f5 Merge branch 'master', remote-tracking branch 'origin' 2012-12-18 12:18:10 -08:00
Lance Edgar 8e82e7440a remove edbob dependency 2012-12-15 09:56:05 -08:00
Lance Edgar 79e6a46b94 initial rattail v0.4 port (savepoint) 2012-12-14 09:26:15 -08:00
622 changed files with 10185 additions and 98027 deletions

9
.gitignore vendored
View file

@ -1,8 +1 @@
*~
*.pyc
.coverage
.tox/
dist/
docs/_build/
htmlcov/
Tailbone.egg-info/
rattail.pyramid.egg-info

View file

@ -1,621 +0,0 @@
# Changelog
All notable changes to Tailbone will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## v0.21.11 (2024-10-03)
### Fix
- custom method for adding grid action
- become/stop root should redirect to previous url
## v0.21.10 (2024-09-15)
### Fix
- update project repo links, kallithea -> forgejo
- use better icon for submit button on login page
- wrap notes text for batch view
- expose datasync consumer batch size via configure page
## v0.21.9 (2024-08-28)
### Fix
- render custom attrs in form component tag
## v0.21.8 (2024-08-28)
### Fix
- ignore session kwarg for `MasterView.make_row_grid()`
## v0.21.7 (2024-08-28)
### Fix
- avoid error when form value cannot be obtained
## v0.21.6 (2024-08-28)
### Fix
- avoid error when grid value cannot be obtained
## v0.21.5 (2024-08-28)
### Fix
- set empty string for "-new-" file configure option
## v0.21.4 (2024-08-26)
### Fix
- handle differing email profile keys for appinfo/configure
## v0.21.3 (2024-08-26)
### Fix
- show non-standard config values for app info configure email
## v0.21.2 (2024-08-26)
### Fix
- refactor waterpark base template to use wutta feedback component
- fix input/output file upload feature for configure pages, per oruga
- tweak how grid data translates to Vue template context
- merge filters into main grid template
- add basic wutta view for users
- some fixes for wutta people view
- various fixes for waterpark theme
- avoid deprecated `component` form kwarg
## v0.21.1 (2024-08-22)
### Fix
- misc. bugfixes per recent changes
## v0.21.0 (2024-08-22)
### Feat
- move "most" filtering logic for grid class to wuttaweb
- inherit from wuttaweb templates for home, login pages
- inherit from wuttaweb for AppInfoView, appinfo/configure template
- add "has output file templates" config option for master view
### Fix
- change grid reset-view param name to match wuttaweb
- move "searchable columns" grid feature to wuttaweb
- use wuttaweb to get/render csrf token
- inherit from wuttaweb for appinfo/index template
- prefer wuttaweb config for "home redirect to login" feature
- fix master/index template rendering for waterpark theme
- fix spacing for navbar logo/title in waterpark theme
## v0.20.1 (2024-08-20)
### Fix
- fix default filter verbs logic for workorder status
## v0.20.0 (2024-08-20)
### Feat
- add new 'waterpark' theme, based on wuttaweb w/ vue2 + buefy
- refactor templates to simplify base/page/form structure
### Fix
- avoid deprecated reference to app db engine
## v0.19.3 (2024-08-19)
### Fix
- add pager stats to all grid vue data (fixes view history)
## v0.19.2 (2024-08-19)
### Fix
- sort on frontend for appinfo package listing grid
- prefer attr over key lookup when getting model values
- replace all occurrences of `component_studly` => `vue_component`
## v0.19.1 (2024-08-19)
### Fix
- fix broken user auth for web API app
## v0.19.0 (2024-08-18)
### Feat
- move multi-column grid sorting logic to wuttaweb
- move single-column grid sorting logic to wuttaweb
### Fix
- fix misc. errors in grid template per wuttaweb
- fix broken permission directives in web api startup
## v0.18.0 (2024-08-16)
### Feat
- move "basic" grid pagination logic to wuttaweb
- inherit from wutta base class for Grid
- inherit most logic from wuttaweb, for GridAction
### Fix
- avoid route error in user view, when using wutta people view
- fix some more wutta compat for base template
## v0.17.0 (2024-08-15)
### Feat
- use wuttaweb for `get_liburl()` logic
## v0.16.1 (2024-08-15)
### Fix
- improve wutta People view a bit
- update references to `get_class_hierarchy()`
- tweak template for `people/view_profile` per wutta compat
## v0.16.0 (2024-08-15)
### Feat
- add first wutta-based master, for PersonView
- refactor forms/grids/views/templates per wuttaweb compat
## v0.15.6 (2024-08-13)
### Fix
- avoid `before_render` subscriber hook for web API
- simplify verbiage for batch execution panel
## v0.15.5 (2024-08-09)
### Fix
- assign convenience attrs for all views (config, app, enum, model)
## v0.15.4 (2024-08-09)
### Fix
- avoid bug when checking current theme
## v0.15.3 (2024-08-08)
### Fix
- fix timepicker `parseTime()` when value is null
## v0.15.2 (2024-08-06)
### Fix
- use auth handler, avoid legacy calls for role/perm checks
## v0.15.1 (2024-08-05)
### Fix
- move magic `b` template context var to wuttaweb
## v0.15.0 (2024-08-05)
### Feat
- move more subscriber logic to wuttaweb
### Fix
- use wuttaweb logic for `util.get_form_data()`
## v0.14.5 (2024-08-03)
### Fix
- use auth handler instead of deprecated auth functions
- avoid duplicate `partial` param when grid reloads data
## v0.14.4 (2024-07-18)
### Fix
- fix more settings persistence bug(s) for datasync/configure
- fix modals for luigi tasks page, per oruga
## v0.14.3 (2024-07-17)
### Fix
- fix auto-collapse title for viewing trainwreck txn
- allow auto-collapse of header when viewing trainwreck txn
## v0.14.2 (2024-07-15)
### Fix
- add null menu handler, for use with API apps
## v0.14.1 (2024-07-14)
### Fix
- update usage of auth handler, per rattail changes
- fix model reference in menu handler
- fix bug when making "integration" menus
## v0.14.0 (2024-07-14)
### Feat
- move core menu logic to wuttaweb
## v0.13.2 (2024-07-13)
### Fix
- fix logic bug for datasync/config settings save
## v0.13.1 (2024-07-13)
### Fix
- fix settings persistence bug(s) for datasync/configure page
## v0.13.0 (2024-07-12)
### Feat
- begin integrating WuttaWeb as upstream dependency
### Fix
- cast enum as list to satisfy deform widget
## v0.12.1 (2024-07-11)
### Fix
- refactor `config.get_model()` => `app.model`
## v0.12.0 (2024-07-09)
### Feat
- drop python 3.6 support, use pyproject.toml (again)
## v0.11.10 (2024-07-05)
### Fix
- make the Members tab optional, for profile view
## v0.11.9 (2024-07-05)
### Fix
- do not show flash message when changing app theme
- improve collapse panels for butterball theme
- expand input for butterball theme
- add xref button to customer profile, for trainwreck txn view
- add optional Transactions tab for profile view
## v0.11.8 (2024-07-04)
### Fix
- fix grid action icons for datasync/configure, per oruga
- allow view supplements to add extra links for profile employee tab
- leverage import handler method to determine command/subcommand
- add tool to make user account from profile view
## v0.11.7 (2024-07-04)
### Fix
- add stacklevel to deprecation warnings
- require zope.sqlalchemy >= 1.5
- include edit profile email/phone dialogs only if user has perms
- allow view supplements to add to profile member context
- cast enum as list to satisfy deform widget
- expand POD image URL setting input
## v0.11.6 (2024-07-01)
### Fix
- set explicit referrer when changing dbkey
- remove references, dependency for `six` package
## v0.11.5 (2024-06-30)
### Fix
- allow comma in numeric filter input
- add custom url prefix if needed, for fanstatic
- use vue 3.4.31 and oruga 0.8.12 by default
## v0.11.4 (2024-06-30)
### Fix
- start/stop being root should submit POST instead of GET
- require vendor when making new ordering batch via api
- don't escape each address for email attempts grid
## v0.11.3 (2024-06-28)
### Fix
- add link to "resolved by" user for pending products
- handle error when merging 2 records fails
## v0.11.2 (2024-06-18)
### Fix
- hide certain custorder settings if not applicable
- use different logic for buefy/oruga for product lookup keydown
- product records should be touchable
- show flash error message if resolve pending product fails
## v0.11.1 (2024-06-14)
### Fix
- revert back to setup.py + setup.cfg
## v0.11.0 (2024-06-10)
### Feat
- switch from setup.cfg to pyproject.toml + hatchling
## v0.10.16 (2024-06-10)
### Feat
- standardize how app, package versions are determined
### Fix
- avoid deprecated config methods for app/node title
## v0.10.15 (2024-06-07)
### Fix
- do *not* Use `pkg_resources` to determine package versions
## v0.10.14 (2024-06-06)
### Fix
- use `pkg_resources` to determine package versions
## v0.10.13 (2024-06-06)
### Feat
- remove old/unused scaffold for use with `pcreate`
- add 'fanstatic' support for sake of libcache assets
## v0.10.12 (2024-06-04)
### Feat
- require pyramid 2.x; remove 1.x-style auth policies
- remove version cap for deform
- set explicit referrer when changing app theme
- add `<b-tooltip>` component shim
- include extra styles from `base_meta` template for butterball
- include butterball theme by default for new apps
### Fix
- fix product lookup component, per butterball
## v0.10.11 (2024-06-03)
### Feat
- fix vue3 refresh bugs for various views
- fix grid bug for tempmon appliance view, per oruga
- fix ordering worksheet generator, per butterball
- fix inventory worksheet generator, per butterball
## v0.10.10 (2024-06-03)
### Feat
- more butterball fixes for "view profile" template
### Fix
- fix focus for `<b-select>` shim component
## v0.10.9 (2024-06-03)
### Feat
- let master view control context menu items for page
- fix the "new custorder" page for butterball
### Fix
- fix panel style for PO vs. Invoice breakdown in receiving batch
## v0.10.8 (2024-06-02)
### Feat
- add styling for checked grid rows, per oruga/butterball
- fix product view template for oruga/butterball
- allow per-user custom styles for butterball
- use oruga 0.8.9 by default
## v0.10.7 (2024-06-01)
### Feat
- add setting to allow decimal quantities for receiving
- log error if registry has no rattail config
- add column filters for import/export main grid
- escape all unsafe html for grid data
- add speedbumps for delete, set preferred email/phone in profile view
- fix file upload widget for oruga
### Fix
- fix overflow when instance header title is too long (butterball)
## v0.10.6 (2024-05-29)
### Feat
- add way to flag organic products within lookup dialog
- expose db picker for butterball theme
- expose quickie lookup for butterball theme
- fix basic problems with people profile view, per butterball
## v0.10.5 (2024-05-29)
### Feat
- add `<tailbone-timepicker>` component for oruga
## v0.10.4 (2024-05-12)
### Fix
- fix styles for grid actions, per butterball
## v0.10.3 (2024-05-10)
### Fix
- fix bug with grid date filters
## v0.10.2 (2024-05-08)
### Feat
- remove version restriction for pyramid_beaker dependency
- rename some attrs etc. for buefy components used with oruga
- fix "tools" helper for receiving batch view, per oruga
- more data type fixes for ``<tailbone-datepicker>``
- fix "view receiving row" page, per oruga
- tweak styles for grid action links, per butterball
### Fix
- fix employees grid when viewing department (per oruga)
- fix login "enter" key behavior, per oruga
- fix button text for autocomplete
## v0.10.1 (2024-04-28)
### Feat
- sort list of available themes
- update various icon names for oruga compatibility
- show "View This" button when cloning a record
- stop including 'falafel' as available theme
### Fix
- fix vertical alignment in main menu bar, for butterball
- fix upgrade execution logic/UI per oruga
## v0.10.0 (2024-04-28)
This version bump is to reflect adding support for Vue 3 + Oruga via
the 'butterball' theme. There is likely more work to be done for that
yet, but it mostly works at this point.
### Feat
- misc. template and view logic tweaks (applicable to all themes) for
better patterns, consistency etc.
- add initial support for Vue 3 + Oruga, via "butterball" theme
## Older Releases
Please see `docs/OLDCHANGES.rst` for older release notes.

124
CHANGES.txt Normal file
View file

@ -0,0 +1,124 @@
0.3a14
------
- [feature] Added some more CRUD. Mostly this was for departments,
subdepartments, brands and products. This was rather ad-hoc and still is
probably far from complete.
- [general] Changed main batch route.
- [bug] Fixed label profile templates so they properly handle a missing or
invalid printer spec.
0.3a13
------
- [bug] Fixed bug which prevented UPC search from working on products screen.
0.3a12
------
- [general] Fixed namespace packages, per ``setuptools`` documentation.
- [feature] Added support for ``LabelProfile.visible``. This field may now be
edited, and it is honored when displaying the list of available profiles to
be used for printing from the products page.
- [bug] Fixed bug where non-numeric data entered in the UPC search field on the
products page was raising an error.
0.3a11
------
- [bug] Fixed product label printing to handle any uncaught exception, and
report the error message to the end user.
0.3a10
------
- [general] Updated category views and templates. These were sorely out of
date.
0.3a9
-----
- Add brands autocomplete view.
- Add departments autocomplete view.
- Add ID filter to vendors grid.
0.3a8
-----
- Tweak batch progress indicators.
- Add "Executed" column, filter to batch grid.
0.3a7
-----
- Add ability to restrict batch providers via config.
0.3a6
-----
- Add Vendor CRUD.
- Add Brand views.
0.3a5
-----
- Added support for GPC data type.
- Added eager import of ``rattail.sil`` in ``before_render`` hook.
- Removed ``rattail.pyramid.util`` module.
- Added initial batch support: views, templates, creation from Product grid.
- Added support for ``rattail.LabelProfile`` class.
- Improved Product grid to include filter/sort on Vendor.
- Cleaned up dependencies.
- Added ``rattail.pyramid.includeme()``.
- Added ``CustomerGroup`` CRUD view (read only).
- Added hot links to ``Customer`` CRUD view.
- Added ``Store`` index, CRUD views.
- Updated ``rattail.pyramid.views.includeme()``.
- Added ``email_preference`` to ``Customer`` CRUD.
0.3a4
-----
- Update grid and CRUD views per changes in ``edbob``.
0.3a3
-----
- Add price field renderers.
- Add/tweak lots of views for database models.
- Add label printing to product list view.
- Add (some of) ``Product`` CRUD.
0.3a2
-----
- Refactor category views.
0.3a1
-----
- Initial port to Rattail v0.3.

View file

@ -1,5 +1,5 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
@ -7,17 +7,15 @@
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
@ -72,7 +60,7 @@ modification follow.
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
GNU Affero General Public License for more details.
You should have received a copy of the GNU General Public License
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
For more information on this, and how to apply and follow the GNU AGPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

View file

@ -1,18 +1,2 @@
include *.txt
include *.rst
include *.py
include tailbone/static/robots.txt
recursive-include tailbone/static *.js
recursive-include tailbone/static *.css
recursive-include tailbone/static *.png
recursive-include tailbone/static *.jpg
recursive-include tailbone/static *.gif
recursive-include tailbone/static *.ico
recursive-include tailbone/static/files *
recursive-include tailbone/templates *.mako
recursive-include tailbone/templates *.pt
recursive-include tailbone/reports *.mako
include *.txt *.ini *.cfg *.rst
recursive-include rattail/pyramid *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml

View file

@ -1,8 +0,0 @@
# Tailbone
Tailbone is an extensible web application based on Rattail. It provides a
"back-office network environment" (BONE) for use in managing retail data.
Please see Rattail's [home page](http://rattailproject.org/) for more
information.

11
README.txt Normal file
View file

@ -0,0 +1,11 @@
rattail.pyramid
===============
Rattail is a retail software framework based on `edbob <http://edbob.org/>`_,
and released under the GNU Affero General Public License.
This package contains Pyramid views, etc., for managing a Rattail system.
Please see Rattail's `home page <http://rattail.edbob.org/>`_ for more
information.

View file

@ -1,177 +0,0 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
endif
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Tailbone.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Tailbone.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/Tailbone"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Tailbone"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."

File diff suppressed because it is too large Load diff

0
docs/_static/.dummy vendored
View file

View file

@ -1,15 +0,0 @@
``tailbone.api.batch.core``
===========================
.. automodule:: tailbone.api.batch.core
.. autoclass:: APIBatchMixin
.. autoclass:: APIBatchView
.. autoclass:: APIBatchRowView
.. autoattribute:: editable
.. autoattribute:: supports_quick_entry

View file

@ -1,41 +0,0 @@
``tailbone.api.batch.ordering``
===============================
.. automodule:: tailbone.api.batch.ordering
.. autoclass:: OrderingBatchViews
.. autoattribute:: collection_url_prefix
.. autoattribute:: object_url_prefix
.. autoattribute:: model_class
.. autoattribute:: route_prefix
.. autoattribute:: permission_prefix
.. autoattribute:: default_handler_spec
.. automethod:: base_query
.. automethod:: create_object
.. autoclass:: OrderingBatchRowViews
.. autoattribute:: collection_url_prefix
.. autoattribute:: object_url_prefix
.. autoattribute:: model_class
.. autoattribute:: route_prefix
.. autoattribute:: permission_prefix
.. autoattribute:: default_handler_spec
.. autoattribute:: supports_quick_entry
.. automethod:: update_object

View file

@ -1,6 +0,0 @@
``tailbone.db``
===============
.. automodule:: tailbone.db
:members:

View file

@ -1,6 +0,0 @@
``tailbone.diffs``
==================
.. automodule:: tailbone.diffs
:members:

View file

@ -1,9 +0,0 @@
``tailbone.forms``
==================
.. automodule:: tailbone.forms
:members:
.. autoclass:: tailbone.forms.Form
:members:

View file

@ -1,6 +0,0 @@
``tailbone.forms.widgets``
==========================
.. automodule:: tailbone.forms.widgets
:members:

View file

@ -1,6 +0,0 @@
``tailbone.grids.core``
=======================
.. automodule:: tailbone.grids.core
:members:

View file

@ -1,6 +0,0 @@
``tailbone.grids``
==================
.. automodule:: tailbone.grids
:members:

View file

@ -1,6 +0,0 @@
``tailbone.progress``
=====================
.. automodule:: tailbone.progress
:members:

View file

@ -1,6 +0,0 @@
``tailbone.subscribers``
========================
.. automodule:: tailbone.subscribers
:members:

View file

@ -1,6 +0,0 @@
``tailbone.util``
=================
.. automodule:: tailbone.util
:members:

View file

@ -1,5 +0,0 @@
``tailbone.views.batch``
========================
.. automodule:: tailbone.views.batch

View file

@ -1,10 +0,0 @@
``tailbone.views.batch.vendorcatalog``
======================================
.. automodule:: tailbone.views.batch.vendorcatalog
.. autoclass:: VendorCatalogsView
:members:
.. autofunction:: includeme

View file

@ -1,6 +0,0 @@
``tailbone.views.core``
=======================
.. automodule:: tailbone.views.core
:members:

View file

@ -1,126 +0,0 @@
``tailbone.views.master``
=========================
.. module:: tailbone.views.master
Model Master View
------------------
This module contains the "model master" view class. This is a convenience
abstraction which provides some patterns/consistency for the typical set of
views needed to expose a table's data for viewing/editing/etc. Usually this
means providing something like the following view methods for a model:
* index (list/filter)
* create
* view
* edit
* delete
The actual list of provided view methods will depend on usage. Generally
speaking, each view method which is provided by the master class may be
configured in some way by the subclass (e.g. add extra filters to a grid).
.. autoclass:: MasterView
.. automethod:: index
.. automethod:: create
.. automethod:: view
.. automethod:: edit
.. automethod:: delete
Attributes to Override
----------------------
The following is a list of attributes which you can (and in some cases must)
override when defining your subclass.
.. attribute:: MasterView.model_class
All master view subclasses *must* define this attribute. Its value must
be a data model class which has been mapped via SQLAlchemy, e.g.
``rattail.db.model.Product``.
.. attribute:: MasterView.normalized_model_name
Name of the model class which has been "normalized" for the sake of usage
as a key (for grid settings etc.). If not defined by the subclass, the
default will be the lower-cased model class name, e.g. 'product'.
.. attribute:: grid_key
Unique value to be used as a key for the grid settings, etc. If not
defined by the subclass, the normalized model name will be used.
.. attribute:: MasterView.route_prefix
Value with which all routes provided by the view class will be prefixed.
If not defined by the subclass, a default will be constructed by simply
adding an 's' to the end of the normalized model name, e.g. 'products'.
.. attribute:: MasterView.grid_factory
Factory callable to be used when creating new grid instances; defaults to
:class:`tailbone.grids.Grid`.
.. attribute:: MasterView.results_downloadable_csv
Flag indicating whether the view should allow CSV download of grid data,
i.e. primary search results.
.. attribute:: MasterView.help_url
If set, this defines the "default" help URL for all views provided by the
master. Default value for this is simply ``None`` which would mean the
Help button is not shown at all. Note that the master may choose to
override this for certain views, if so that should be done within
:meth:`get_help_url()`.
.. attribute:: MasterView.version_diff_factory
Optional factory to use for version diff objects. By default
this is *not set* but a subclass is free to set it. See also
:meth:`get_version_diff_factory()`.
Methods to Override
-------------------
The following is a list of methods which you can override when defining your
subclass.
.. automethod:: MasterView.editable_instance
.. .. automethod:: MasterView.get_settings
.. automethod:: MasterView.get_csv_fields
.. automethod:: MasterView.get_csv_row
.. automethod:: MasterView.get_help_url
.. automethod:: MasterView.get_model_key
.. automethod:: MasterView.get_version_diff_enums
.. automethod:: MasterView.get_version_diff_factory
.. automethod:: MasterView.make_version_diff
.. automethod:: MasterView.title_for_version
Support Methods
---------------
The following is a list of methods you should (probably) not need to
override, but may find useful:
.. automethod:: MasterView.default_edit_url
.. automethod:: MasterView.get_action_route_kwargs

View file

@ -1,6 +0,0 @@
``tailbone.views.members``
==========================
.. automodule:: tailbone.views.members
:members:

View file

@ -1,9 +0,0 @@
``tailbone.views.purchasing.batch``
===================================
.. automodule:: tailbone.views.purchasing.batch
.. autoclass:: PurchasingBatchView
.. automethod:: save_edit_row_form

View file

@ -1,15 +0,0 @@
``tailbone.views.purchasing.ordering``
======================================
.. automodule:: tailbone.views.purchasing.ordering
.. autoclass:: OrderingBatchView
.. autoattribute:: model_class
.. autoattribute:: default_handler_spec
.. automethod:: configure_row_form
.. automethod:: worksheet_update

View file

@ -1,8 +0,0 @@
Changelog Archive
=================
.. toctree::
:maxdepth: 1
OLDCHANGES

View file

@ -1,65 +0,0 @@
Data Batches
============
.. contents:: :local:
Data "batches" are one of the most powerful features of Rattail / Tailbone.
However each "batch type" is different, and they usually require custom
development. In all cases they require a Rattail-based app database, for
storage.
General Overview
----------------
You can think of data batches as a sort of "temporary spreadsheet" feature.
When a batch is created, it is usually populated with rows, from some data
source. The user(s) may then manipulate the batch data as needed, with the
final goal being to "execute" the batch. What execution specifically means
will depend on context, e.g. type of batch, but generally it will "commit" the
"pending changes" which are represented by the batch.
Note that when a batch is executed, it becomes read-only ("frozen in time") and
at that point may be considered part of an audit trail of sorts. The utility
of this may vary depending on the nature of the batch data.
Beyond that it's difficult to describe batches very well at this level,
precisely because they're all different.
..
This graphic tries to show how batches are created and executed over time.
Note that each batch type is free to target a different system(s) upon
execution.
TODO: need graphic
Batch Tables
------------
In most cases the table(s) underlying a particular batch type, have a "static"
schema and must be defined as ORM classes, e.g. within the ``poser.db.model``
package.
In some rare cases the batch data (row) table may be dynamic; however the batch
header table must still be defined.
Batch Handlers
--------------
Once the batch table(s) are present, the next puzzle piece is the batch
handler. Again there is generally (at least) one handler defined for each
batch type.
The batch "handler" is considered part of the data layer and provides logic for
populating the batch, executing it etc.
Batch Views
-----------
This discussion would not be complete without mentioning the web views for the
batch. Again each batch type will require a custom view(s) although these
"usually" are simple wrappers as most logic is provided by the base view.

View file

@ -1,115 +0,0 @@
Configuration
=============
.. contents:: :local:
Configuration for an app can come from two sources: configuration file(s), and
the Settings table in the database.
Config File Inheritance
-----------------------
An important thing to understand regarding Rattail config files, is that one
file may "include" another file(s), which in turn may "include" others etc.
Invocation of the app will often require only a single config file to be
specified, since that file may include others as needed.
For example ``web.conf`` will typically include ``rattail.conf`` but the web
app need only be invoked with ``web.conf`` - config from both files will inform
the app's behavior.
Typical Config Files
--------------------
A typical Poser (Rattail-based) app will have at the very least, one file named
``rattail.conf`` - this is considered the most fundamental config file. It
will usually define database connections, logging config, and any other "core"
things which would be required for any invocation of the app, regardless of the
environment (e.g. console vs. web).
Note that even ``rattail.conf`` is free to include other files. This may be
useful for instance, if you have a single site-wide config file which is shared
among all Rattail apps.
There is no *strict* requirement for having a ``rattail.conf`` file, but these
docs will assume its presence. Here are some other typical files, which the
docs also may reference occasionally:
**web.conf** - This is the "core" config file for the web app, although it
still includes the ``rattail.conf`` file. In production (running on Apache
etc.) it is specified within the WSGI module which is responsible for
instantiating the web app. When running the development server, it is
specified via command line.
**quiet.conf** - This is a slight wrapper around ``rattail.conf`` for the sake
of a "quieter" console, when running app commands via console. It may be used
in place of ``rattail.conf`` - i.e. you would specify ``-c quiet.conf`` when
running the command. The only function of this wrapper is to set the level to
INFO for the console logging handler. In practice this hides DEBUG logging
messages which are shown by default when using ``rattail.conf`` as the app
config file.
**cron.conf** - Another wrapper around ``rattail.conf`` which suppresses
logging even further. The idea is that this config file would be used by cron
jobs; that way the only actual output is warnings and errors, hence cron would
not send email unless something actually went wrong. It may be used in place
of ``rattail.conf`` - i.e. you would specify ``-c cron.conf`` when running the
command. The only function of this wrapper is to set the level to WARNING for
the console logging handler.
**ignore-changes.conf** - This file is only relevant if your ``rattail.conf``
says to "record changes" when write activity occurs in the database(s). Note
that this file does *not* include ``rattail.conf`` because it is meant to be
supplemental only. For instance on the command line, you would need to specify
two config files, first ``rattail.conf`` or a suitable alternative, but then
``ignore-changes.conf`` also. If specified, this file will cause changes to be
ignored, i.e. **not recorded** when write activity occurs.
**without-versioning.conf** - This file is only relevant if your
``rattail.conf`` says to enable "data versioning" when write activity occurs in
the database(s). Note that this file does *not* include ``rattail.conf``
because it is meant to be supplemental only. For instance on the command line,
you would need to specify two config files, first ``rattail.conf`` or a
suitable alternative, but then ``without-versioning.conf`` also. If specified,
this file will disable the data versioning system entirely. Note that if
versioning is undesirable for a given app run, this is the only way to
effectively disable it; once loaded that feature cannot be disabled.
Settings from Database
----------------------
The other (often more convenient) source of app configuration is the Settings
table within the app database. Whether or not this table is a valid source for
app configuration, ultimately depends on what the config file(s) has to say
about it.
Assuming the config file(s) defines a database connection and declares it a
valid source for config values, then the Settings table may contribute to the
running app config. The nice thing about this is that these settings are
checked in real-time. So whereas changing a config file will require an app
restart, any edits to the settings table should take effect immediately.
Usually the settings table will *override* values found in the config file.
This behavior also is configurable to some extent, and in some cases a config
value may *only* come from a config file and never the settings table.
An example may help here. If the config file contained the following value:
.. code-block:: ini
[poser]
foo = bar
Then you could create a new Setting in the database with the following fields:
* **name** = poser.foo
* **value** = baz
Assuming typical setup, i.e. where settings table may override config file, the
app would consider 'baz' to be the config value. So basically the setting name
must correspond to a combination of the config file "section" name, then a dot,
then the "option" name.

View file

@ -1,7 +0,0 @@
Console Commands
================
.. contents:: :local:
TODO

View file

@ -1,45 +0,0 @@
Database Schema
===============
.. contents:: :local:
Rattail provides a "core" schema which is assumed to be the foundation of any
Poser app database.
Core Tables
-----------
All tables which are considered part of the Rattail "core" schema, are defined
as ORM classes within the ``rattail.db.model`` package.
.. note::
The Rattail project has its roots in retail grocery-type stores, and its
schema reflects that to a large degree. In practice however the software
may be used to support a wide variety of apps. The next section describes
that a bit more.
Customizing the Schema
----------------------
Almost certainly a custom app will need some of the core tables, but just as
certainly, it will *not* need others. And to make things even more
interesting, it may need some tables but also need to "supplement" them
somehow, to track additional data for each record etc.
Any table in the core schema which is *not* needed, may simply be ignored,
i.e. hidden from the app UI etc.
Any table which is "missing" from core schema, from the custom app's
perspective, should be added as a custom table.
Also, any table which is "present but missing columns" from the app's
perspective, will require a custom table. In this case each record in the
custom table will "tie back to" the core table record. The custom record will
then supply any additional data for the core record.
Defining custom tables, and associated tasks, are documented in
:doc:`../schemachange`.

View file

@ -1,52 +0,0 @@
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
from importlib.metadata import version as get_version
project = 'Tailbone'
copyright = '2010 - 2024, Lance Edgar'
author = 'Lance Edgar'
release = get_version('Tailbone')
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.todo',
'sphinx.ext.intersphinx',
'sphinx.ext.viewcode',
]
templates_path = ['_templates']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
intersphinx_mapping = {
'rattail': ('https://rattailproject.org/docs/rattail/', None),
'webhelpers2': ('https://webhelpers2.readthedocs.io/en/latest/', None),
'wuttaweb': ('https://rattailproject.org/docs/wuttaweb/', None),
'wuttjamaican': ('https://rattailproject.org/docs/wuttjamaican/', None),
}
# allow todo entries to show up
todo_include_todos = True
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = 'furo'
html_static_path = ['_static']
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
#html_logo = 'images/rattail_avatar.png'
# Output file base name for HTML help builder.
#htmlhelp_basename = 'Tailbonedoc'

View file

@ -1,78 +0,0 @@
Development Environment
=======================
.. contents:: :local:
Base System
-----------
Development for Tailbone in particular is assumed to occur on a Linux machine.
This is because it's assumed that the web app would run on Linux. It should be
possible (presumably) to do either on Windows or Mac but that is not officially
supported.
Furthermore it is assumed the Linux flavor in use is either Debian or Ubuntu,
or a similar alternative. Presumably any Linux would work although some
details may differ from what's shown here.
Prerequisites
-------------
Python
^^^^^^
The only supported Python is 2.7. Of course that should already be present on
Linux.
It usually is required at some point to compile C code for certain Python
extension modules. In practice this means you probably want the Python header
files as well:
.. code-block:: sh
sudo apt-get install python-dev
pip
^^^
The only supported Python package manager is ``pip``. This can be installed a
few ways, one of which is:
.. code-block:: sh
sudo apt-get install python-pip
virtualenvwrapper
^^^^^^^^^^^^^^^^^
While not technically required, it is recommended to use ``virtualenvwrapper``
as well. There is more than one way to set this up, e.g.:
.. code-block:: sh
sudo apt-get install python-virtualenvwrapper
The main variable as concerns these docs, is where your virtual environment(s)
will live. If you install virtualenvwrapper via the above command, then most
likely your ``$WORKON_HOME`` environment variable will be set to
``~/.virtualenvs`` - however these docs will assume ``/srv/envs`` instead.
Please adjust any commands as needed.
PostgreSQL
^^^^^^^^^^
The other primary requirement is PostgreSQL. Technically that may be installed
on a separate machine, which allows connection from the development machine.
But of course it will usually just be installed on the dev machine:
.. code-block:: sh
sudo apt-get install postgresql
Regardless of where your PG server lives, you will probably need some extras in
order to compile extensions for the ``psycopg2`` package:
.. code-block:: sh
sudo apt-get install libpq-dev

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

View file

@ -1,85 +0,0 @@
Tailbone
========
Welcome to Tailbone, part of the Rattail project. While the core Rattail
package provides the data layer, the Tailbone package provides the (default,
back-end) web application layer.
Some additional information is available on the `website`_. Certainly not
everything is documented yet, but here you can see what has received some
attention thus far.
.. _website: https://rattailproject.org/
Quick Start for Custom Apps:
.. toctree::
:maxdepth: 1
structure
devenv
newproject
schemachange
Concept Guide:
.. toctree::
concepts/config
concepts/console
concepts/schema
concepts/batches
Narrative Documentation:
.. toctree::
narr/batches
Package API:
.. toctree::
:maxdepth: 1
api/api/batch/core
api/api/batch/ordering
api/db
api/diffs
api/forms
api/forms.widgets
api/grids
api/grids.core
api/progress
api/subscribers
api/util
api/views/batch
api/views/batch.vendorcatalog
api/views/core
api/views/master
api/views/members
api/views/purchasing.batch
api/views/purchasing.ordering
Changelog:
.. toctree::
:maxdepth: 1
changelog
Documentation To-Do
===================
.. todolist::
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View file

@ -1,242 +0,0 @@
@ECHO OFF
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set BUILDDIR=_build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
set I18NSPHINXOPTS=%SPHINXOPTS% .
if NOT "%PAPER%" == "" (
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
)
if "%1" == "" goto help
if "%1" == "help" (
:help
echo.Please use `make ^<target^>` where ^<target^> is one of
echo. html to make standalone HTML files
echo. dirhtml to make HTML files named index.html in directories
echo. singlehtml to make a single large HTML file
echo. pickle to make pickle files
echo. json to make JSON files
echo. htmlhelp to make HTML files and a HTML help project
echo. qthelp to make HTML files and a qthelp project
echo. devhelp to make HTML files and a Devhelp project
echo. epub to make an epub
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
echo. text to make text files
echo. man to make manual pages
echo. texinfo to make Texinfo files
echo. gettext to make PO message catalogs
echo. changes to make an overview over all changed/added/deprecated items
echo. xml to make Docutils-native XML files
echo. pseudoxml to make pseudoxml-XML files for display purposes
echo. linkcheck to check all external links for integrity
echo. doctest to run all doctests embedded in the documentation if enabled
goto end
)
if "%1" == "clean" (
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
del /q /s %BUILDDIR%\*
goto end
)
%SPHINXBUILD% 2> nul
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
if "%1" == "html" (
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
goto end
)
if "%1" == "dirhtml" (
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
goto end
)
if "%1" == "singlehtml" (
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
goto end
)
if "%1" == "pickle" (
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the pickle files.
goto end
)
if "%1" == "json" (
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the JSON files.
goto end
)
if "%1" == "htmlhelp" (
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
goto end
)
if "%1" == "qthelp" (
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run "qcollectiongenerator" with the ^
.qhcp project file in %BUILDDIR%/qthelp, like this:
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Tailbone.qhcp
echo.To view the help file:
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Tailbone.ghc
goto end
)
if "%1" == "devhelp" (
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished.
goto end
)
if "%1" == "epub" (
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The epub file is in %BUILDDIR%/epub.
goto end
)
if "%1" == "latex" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
if errorlevel 1 exit /b 1
echo.
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "latexpdf" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
cd %BUILDDIR%/latex
make all-pdf
cd %BUILDDIR%/..
echo.
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "latexpdfja" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
cd %BUILDDIR%/latex
make all-pdf-ja
cd %BUILDDIR%/..
echo.
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "text" (
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The text files are in %BUILDDIR%/text.
goto end
)
if "%1" == "man" (
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The manual pages are in %BUILDDIR%/man.
goto end
)
if "%1" == "texinfo" (
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
goto end
)
if "%1" == "gettext" (
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
goto end
)
if "%1" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
if errorlevel 1 exit /b 1
echo.
echo.The overview file is in %BUILDDIR%/changes.
goto end
)
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
if errorlevel 1 exit /b 1
echo.
echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
goto end
)
if "%1" == "doctest" (
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
if errorlevel 1 exit /b 1
echo.
echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
goto end
)
if "%1" == "xml" (
%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The XML files are in %BUILDDIR%/xml.
goto end
)
if "%1" == "pseudoxml" (
%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
goto end
)
:end

View file

@ -1,74 +0,0 @@
.. -*- coding: utf-8 -*-
Data Batches
============
This document briefly outlines what comprises a batch in terms of the Tailbone
user interface etc.
Batch Views
-----------
Adding support for a new batch type is mostly a matter of providing some custom
views for the batch and its rows. In fact you must define four different view
classes, inheriting from each of the following:
* :class:`tailbone.views.batch.BatchGrid`
* :class:`tailbone.views.batch.BatchCrud`
* :class:`tailbone.views.batch.BatchRowGrid`
* :class:`tailbone.views.batch.BatchRowCrud`
It would sure be nice to only require two view classes instead of four, hopefully
that can happen "soon". In the meantime that's what it takes. Note that as with
batch data models, there are some more specialized parent classes which you may
want to inherit from instead of the core classes mentioned above:
* :class:`tailbone.views.batch.FileBatchGrid`
* :class:`tailbone.views.batch.FileBatchCrud`
* :class:`tailbone.views.batch.ProductBatchRowGrid`
Here are the vendor catalog views as examples:
* :class:`tailbone.views.vendors.catalogs.VendorCatalogGrid`
* :class:`tailbone.views.vendors.catalogs.VendorCatalogCrud`
* :class:`tailbone.views.vendors.catalogs.VendorCatalogRowGrid`
* :class:`tailbone.views.vendors.catalogs.VendorCatalogRowCrud`
Pyramid Config
--------------
In addition to defining the batch views, the Pyramid Configurator object must be
told of the views and their routes. This also could probably stand to be simpler
somehow, but for now the easiest thing is to apply default configuration with:
* :func:`tailbone.views.batch.defaults()`
See the source behind the vendor catalog for an example:
* :func:`tailbone.views.vendors.catalogs.includeme()`
Note of course that your view config must be included by the core/upstream
config process of your application's startup to take effect. At this point
your views should be accessible by navigating to the URLs directly, e.g. for
the vendor catalog views:
* List Uploaded Catalogs - http://example.com/vendors/catalogs/
* Upload New Catlaog - http://example.com/vendors/catalogs/new
Menu and Templates
------------------
Providing access to the batch views is (I think) the last step. You must add
links to the views, wherever that makes sense for your app. In case it's
helpful, here's a Mako template snippet which would show some links to the main
vendor catalog views:
.. code-block:: mako
<ul>
<li>${h.link_to("Vendor Catalogs", url('vendors.catalogs'))}</li>
<li>${h.link_to("Upload new Vendor Catalog", url('vendors.catalogs.create'))}</li>
</ul>

View file

@ -1,154 +0,0 @@
Creating a New Project
======================
.. contents:: :local:
.. highlight:: bash
This describes the process of creating a new app project based on
Rattail/Tailbone. It assumes you are working from a supported :doc:`devenv`.
Per convention, this doc uses "Poser" (and ``poser``) to represent the custom
app. Please adjust commands etc. accordingly. See also :doc:`structure`.
Create the Virtual Environment
------------------------------
First step is simple enough::
mkvirtualenv poser
Then with your new environment activated, install the Tailbone package::
pip install Tailbone
Create the Project
------------------
Now with your environment still activated, ``cd`` to wherever you like
(e.g. ``~/src``) and create a new project skeleton like so::
mkdir -p ~/src
cd ~/src
pcreate -s rattail poser
This will have created a new project at ``~/src/poser`` which you can then edit
as you wish. At some point you will need to "install" this project to the
environment like so (again with environment active)::
cd ~/src/poser
pip install -e .
Setup the App Environment
-------------------------
Any project based on Rattail will effectively be its own "app" (usually), but
Rattail itself provides some app functionality as well. However all such apps
require config files, usually. If running a web app then you may also need to
have configured a folder for session storage, etc. To hopefully simplify all
this, there are a few commands you should now run, with your virtual
environment still active::
rattail make-appdir
cdvirtualenv app
rattail make-config -T rattail
rattail make-config -T quiet
rattail make-config -T web
This will have created a new 'app' folder in your environment (e.g. at
``/srv/envs/poser/app``) and then created ``rattail.conf`` and ``web.conf``
files within that app dir. Note that there will be other folders inside the
app dir as well; these are referenced by the config files.
But you're not done yet... You should likely edit the config files, at the
very least edit ``rattail.conf`` and change the ``default.url`` value (under
``[rattail.db]`` section) which defines the Rattail database connection.
Create the Database
-------------------
If applicable, it's time for that. First you must literally create the user
and database on your PostgreSQL server, e.g.::
sudo -u postgres createuser --no-createdb --no-createrole --no-superuser poser
sudo -u postgres psql -c "alter user poser password 'mypassword'"
sudo -u postgres createdb --owner poser poser
Then you can install the schema; with your virtual environment activated::
cdvirtualenv
alembic -c app/rattail.conf upgrade heads
At this point your 'poser' database should have some empty tables. To confirm,
on your PG server do::
sudo -u postgres psql -c '\d' poser
Create Admin User
-----------------
If your intention is to have a web app, or at least to test one, you'll
probably want to create the initial admin user. With your env active::
cdvirtualenv
rattail -c app/quiet.conf make-user --admin myusername
This should prompt you for a password, then create a single user and assign it
to the Administrator role.
Install Sample Data
-------------------
If desired, you can install a bit of sample data to your fresh Rattail
database. With your env active do::
cdvirtualenv
rattail -c app/quiet.conf -P import-sample
Run Dev Web Server
------------------
With all the above in place, you may now run the web server in dev mode::
cdvirtualenv
pserve --reload app/web.conf
And finally..you may browse your new project dev site at http://localhost:9080/
(unless you changed the port etc.)
Schema Migrations
-----------------
Often a new project will require custom schema additions to track/manage data
unique to the project. Rattail uses `Alembic`_ for handling schema migrations.
General usage of that is documented elsewhere, but a little should be said here
regarding new projects.
.. _Alembic: https://pypi.python.org/pypi/alembic
The new project template includes most of an Alembic "repo" for schema
migrations. However there is one step required to really bootstrap it, i.e. to
the point where normal Alembic usage will work: you must create the initial
version script. Before you do this, you should be reasonably happy with any
ORM classes you've defined, as the initial version script will be used to
create that schema. Once you're ready for the script, this command should do
it::
cdvirtualenv
bin/alembic -c app/rattail.conf revision --autogenerate --version-path ~/src/poser/poser/db/alembic/versions/ -m 'initial Poser tables'
You should of course look over and edit the generated script as needed. One
change in particular you should make is to add a branch label, e.g.:
.. code-block:: python
branch_labels = ('poser',)

View file

@ -1,63 +0,0 @@
Migrating the Schema
====================
.. contents:: :local:
As development progresses for your custom app, you may need to migrate the
database schema from time to time.
See also this general discussion of the :doc:`concepts/schema`.
.. note::
The only "safe" migrations are those which add or modify (or remove)
"custom" tables, i.e. those *not* provided by the ``rattail.db.model``
package. This doc assumes you are aware of this and are only attempting a
safe migration.
Modify ORM Classes
------------------
First step is to modify the ORM classes defined by your app, so they reflect
the "desired" schema. Typically this will mean editing files under the
``poser.db.model`` package within your source. In particular when adding new
tables, you must be sure to include them within ``poser/db/model/__init__.py``.
As noted above, only those classes *not* provided by ``rattail.db.model``
should be modified here, to be safe. If you wish to "extend" an existing
table, you must create a secondary table which ties back to the first via
one-to-one foreign key relationship.
Create Migration Script
-----------------------
Next you will create the Alembic script which is responsible for performing the
schema migration against a database. This is typically done like so:
.. code-block:: sh
workon poser
cdvirtualenv
bin/alembic -c app/rattail.conf revision --autogenerate --head poser@head -m "describe migration here"
This will create a new file under
e.g. ``~/src/poser/poser/db/alembic/versions/``. You should edit this file as
needed to ensure it performs all steps required for the migration. Technically
it should support downgrade as well as upgrade, although in practice that isn't
always required.
Upgrade Database Schema
-----------------------
Once you're happy with the new script, you can apply it against your dev
database with something like:
.. code-block:: sh
workon poser
cdvirtualenv
bin/alembic -c app/rattail.conf upgrade heads

View file

@ -1,130 +0,0 @@
App Organization & Structure
============================
.. contents:: :local:
Tailbone doesn't try to be an "app" proper. But it does try to provide just
about everything you'd need to make one. These docs assume you are making a
custom app, and will refer to the app as "Poser" to be consistent. In practice
you would give your app a unique name which is meaningful to you. Please
mentally replace "Poser" with your app name as you read.
.. note::
Technically it *is possible* to use Tailbone directly as the app. You may
do so for basic testing of the concepts, but you'd be stuck with Tailbone
logic, with far fewer customization options. All docs will assume a custom
"Poser" app which wraps and (as necessary) overrides Tailbone and Rattail.
Architecture
------------
In terms of how the Poser app hangs together, here is a conceptual diagram.
Note that all systems on the right-hand side are *external* to Poser, i.e. they
are not "plugins" although Poser may use plugin-like logic for the sake of
integrating with these systems.
.. image:: images/poser-architecture.png
Data Layer vs. Web Layer
^^^^^^^^^^^^^^^^^^^^^^^^
While the above graphic doesn't do a great job highlighting the difference, it
will (presumably) help to understand the difference in purpose and function of
Tailbone vs. Rattail packages.
**Rattail** is the data layer, and is responsible for database connectivity,
table schema information, and some business rules logic (among other things).
**Tailbone** is the web app layer, and is responsible for presentation and
management of data objects which are made available by Rattail (and others).
**Poser** is a custom layer which can make use of both data and web app layers,
supplementing each as necessary. In practice the lines may get blurry within
Poser.
The reason for this distinction between layers, is to allow creation of custom
apps which use only the data layer but not the web app layer. This can be
useful for console-based apps; a traditional GUI app would also be possible
although none is yet planned.
File Layout
-----------
Below is an example file layout for a Poser app project. This tries to be
"complete" and show most kinds of files a typical project may need. In
practice you can usually ignore anything which doesn't apply to your app,
i.e. relatively few of the files shown here are actually required. Of course
some apps may need many more files than this to achieve their goals.
Note that all files in the root ``poser`` package namespace would correspond to
the "data layer" mentioned above, whereas everything under ``poser.web`` would
of course supply the web app layer.
.. code-block:: none
~/src/poser/
├── CHANGELOG.md
├── docs/
├── fabfile.py
├── MANIFEST.in
├── poser/
│   ├── __init__.py
│   ├── batch/
│   │   ├── __init__.py
│   │   └── foobatch.py
│   ├── commands.py
│   ├── config.py
│   ├── datasync/
│   ├── db/
│   │   ├── __init__.py
│   │   ├── alembic/
│   │   └── model/
│   │   ├── __init__.py
│   │   ├── batch/
│   │   │   ├── __init__.py
│   │   │   └── foobatch.py
│   │   └── customers.py
│   ├── emails.py
│   ├── enum.py
│   ├── importing/
│   │   ├── __init__.py
│   │   ├── model.py
│   │   ├── poser.py
│   │   └── versions.py
│   ├── problems.py
│   ├── templates/
│   │   └── mail/
│   │   └── warn_about_foo.html.mako
│   ├── _version.py
│   └── web/
│   ├── __init__.py
│   ├── app.py
│   ├── static/
│   │   ├── __init__.py
│   │   ├── css/
│   │   ├── favicon.ico
│   │   ├── img/
│   │   └── js/
│   ├── subscribers.py
│   ├── templates/
│   │   ├── base.mako
│   │   ├── batch/
│   │   │   └── foobatch/
│   │   ├── customers/
│   │   ├── menu.mako
│   │   └── products/
│   └── views/
│   ├── __init__.py
│   ├── batch/
│   │   ├── __init__.py
│   │   └── foobatch.py
│   ├── common.py
│   ├── customers.py
│   └── products.py
├── README.rst
└── setup.py

View file

@ -1,103 +0,0 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "Tailbone"
version = "0.21.11"
description = "Backoffice Web Application for Rattail"
readme = "README.md"
authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]
license = {text = "GNU GPL v3+"}
classifiers = [
"Development Status :: 4 - Beta",
"Environment :: Web Environment",
"Framework :: Pyramid",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
"Natural Language :: English",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Topic :: Internet :: WWW/HTTP",
"Topic :: Office/Business",
"Topic :: Software Development :: Libraries :: Python Modules",
]
requires-python = ">= 3.8"
dependencies = [
"asgiref",
"colander",
"ColanderAlchemy",
"cornice",
"cornice-swagger",
"deform",
"humanize",
"Mako",
"markdown",
"openpyxl",
"paginate",
"paginate_sqlalchemy",
"passlib",
"Pillow",
"pyramid>=2",
"pyramid_beaker",
"pyramid_deform",
"pyramid_exclog",
"pyramid_fanstatic",
"pyramid_mako",
"pyramid_retry",
"pyramid_tm",
"rattail[db,bouncer]>=0.18.5",
"sa-filters",
"simplejson",
"transaction",
"waitress",
"WebHelpers2",
"WuttaWeb>=0.14.0",
"zope.sqlalchemy>=1.5",
]
[project.optional-dependencies]
docs = ["Sphinx", "furo"]
tests = ["coverage", "mock", "pytest", "pytest-cov"]
[project.entry-points."paste.app_factory"]
main = "tailbone.app:main"
webapi = "tailbone.webapi:main"
[project.entry-points."rattail.cleaners"]
beaker = "tailbone.cleanup:BeakerCleaner"
[project.entry-points."rattail.config.extensions"]
tailbone = "tailbone.config:ConfigExtension"
[project.urls]
Homepage = "https://rattailproject.org"
Repository = "https://forgejo.wuttaproject.org/rattail/tailbone"
Issues = "https://forgejo.wuttaproject.org/rattail/tailbone/issues"
Changelog = "https://forgejo.wuttaproject.org/rattail/tailbone/src/branch/master/CHANGELOG.md"
[tool.commitizen]
version_provider = "pep621"
tag_format = "v$version"
update_changelog_on_bump = true
[tool.nosetests]
nocapture = 1
cover-package = "tailbone"
cover-erase = 1
cover-html = 1
cover-html-dir = "htmlcov"

1
rattail/__init__.py Normal file
View file

@ -0,0 +1 @@
__import__('pkg_resources').declare_namespace(__name__)

View file

@ -0,0 +1,68 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
``rattail.pyramid`` -- Rattail's Pyramid Framework
"""
from rattail.pyramid._version import __version__
from sqlalchemy.orm import sessionmaker, scoped_session
Session = scoped_session(sessionmaker())
def includeme(config):
import rattail
import rattail.db
from zope.sqlalchemy import ZopeTransactionExtension
# Configure Beaker session.
config.include('pyramid_beaker')
# Bring in transaction manager.
config.include('pyramid_tm')
# Configure SQLAlchemy session.
rattail.init_modules(['rattail.db'])
Session.configure(bind=rattail.db.engine)
Session.configure(extension=ZopeTransactionExtension())
# Configure user authentication / authorization.
from pyramid.authentication import SessionAuthenticationPolicy
config.set_authentication_policy(SessionAuthenticationPolicy())
from rattail.pyramid.auth import RattailAuthorizationPolicy
config.set_authorization_policy(RattailAuthorizationPolicy())
# Add forbidden view.
config.add_forbidden_view('rattail.pyramid.views.core:forbidden')
# Add static views.
config.add_static_view('rattail', 'rattail.pyramid:static')
# Add subscriber hooks.
config.include('rattail.pyramid.subscribers')
# config.include('rattail.pyramid.views')

View file

@ -0,0 +1 @@
__version__ = '0.4a1'

52
rattail/pyramid/auth.py Normal file
View file

@ -0,0 +1,52 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
``rattail.pyramid.auth`` -- Authorization Policy
"""
from zope.interface import implementer
from pyramid.interfaces import IAuthorizationPolicy
from pyramid.security import Everyone, Authenticated
from rattail.pyramid import Session
from rattail.db.auth import has_permission
from rattail.db.model import User
@implementer(IAuthorizationPolicy)
class RattailAuthorizationPolicy(object):
def permits(self, context, principals, permission):
for userid in principals:
if userid not in (Everyone, Authenticated):
user = Session.query(User).get(userid)
assert user
return has_permission(Session(), user, permission)
if Everyone in principals:
return has_permission(Session(), None, permission)
return False
# def principals_allowed_by_permission(self, context, permission):
# raise NotImplementedError

View file

@ -0,0 +1,31 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
``rattail.pyramid.forms`` -- Forms
"""
from rattail.pyramid.forms.core import *
from rattail.pyramid.forms.formalchemy import *
from rattail.pyramid.forms.simpleform import *

View file

@ -0,0 +1,111 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
``rattail.pyramid.forms.core`` -- Core Forms
"""
import datetime
from sqlalchemy.util import OrderedDict
from webhelpers.html import literal, tags
import rattail
from rattail.time import localize
__all__ = ['Form']
class Form(rattail.Object):
"""
Generic form class.
This class exists primarily so that rendering calls may mimic those used by
FormAlchemy.
"""
readonly = False
successive = False
action_url = None
home_route = None
home_url = None
# template = None
render_fields = OrderedDict()
errors = {}
# def __init__(self, request=None, action_url=None, home_url=None, template=None, **kwargs):
def __init__(self, request=None, action_url=None, home_url=None, **kwargs):
super(Form, self).__init__(**kwargs)
self.request = request
if action_url:
self.action_url = action_url
if request and not self.action_url:
self.action_url = request.current_route_url()
if home_url:
self.home_url = home_url
if request and not self.home_url:
home = self.home_route if self.home_route else 'home'
self.home_url = request.route_url(home)
# if template:
# self.template = template
# if not self.template:
# self.template = '%s.mako' % self.action_url
@property
def action_url(self):
return self.request.current_route_url()
def standard_buttons(self, submit="Save"):
return literal(tags.submit('submit', submit) + ' ' + self.cancel_button())
def cancel_button(self):
return literal('<button type="button" class="cancel">Cancel</button>')
def render(self, **kwargs):
"""
Renders the form as HTML. All keyword arguments are passed on to the
template context.
"""
return ''
def pretty_datetime(value, from_='local', to='local'):
"""
Formats a ``datetime.datetime`` instance and returns a "pretty"
human-readable string from it, e.g. "42 minutes ago". ``value`` is
rendered directly as a string if no date/time can be parsed from it.
"""
if not isinstance(value, datetime.datetime):
return str(value) if value else ''
if not value.tzinfo:
value = localize(value, from_=from_, to=to)
return literal('<span title="%s">%s</span>' % (
value.strftime('%Y-%m-%d %H:%M:%S %Z%z'),
pretty.date(value)))

View file

@ -0,0 +1,32 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
``rattail.pyramid.forms.formalchemy`` -- FormAlchemy Forms
"""
from rattail.pyramid.forms.formalchemy.core import *
from rattail.pyramid.forms.formalchemy.fieldset import *
from rattail.pyramid.forms.formalchemy.fields import *
from rattail.pyramid.forms.formalchemy.renderers import *

View file

@ -0,0 +1,108 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
``rattail.pyramid.forms.formalchemy.core`` -- FormAlchemy Core
"""
from __future__ import absolute_import
from pyramid.renderers import render
import formalchemy
from formalchemy.validators import accepts_none
import rattail
from rattail.pyramid import Session
__all__ = ['AlchemyForm', 'required']
class TemplateEngine(formalchemy.templates.TemplateEngine):
"""
Mako template engine for FormAlchemy.
"""
def render(self, template, prefix='/forms/', suffix='.mako', **kwargs):
template = ''.join((prefix, template, suffix))
return render(template, kwargs)
# Make our TemplateEngine the default.
engine = TemplateEngine()
formalchemy.config.engine = engine
class AlchemyForm(rattail.Object):
"""
Form to contain a :class:`formalchemy.FieldSet` instance.
"""
create_label = "Create"
update_label = "Update"
allow_successive_creates = False
def __init__(self, request, fieldset, **kwargs):
super(AlchemyForm, self).__init__(**kwargs)
self.request = request
self.fieldset = fieldset
def _get_readonly(self):
return self.fieldset.readonly
def _set_readonly(self, val):
self.fieldset.readonly = val
readonly = property(_get_readonly, _set_readonly)
@property
def successive_create_label(self):
return "%s and continue" % self.create_label
def render(self, **kwargs):
kwargs['form'] = self
if self.readonly:
template = '/forms/form_readonly.mako'
else:
template = '/forms/form.mako'
return render(template, kwargs)
def save(self):
self.fieldset.sync()
Session.flush()
def validate(self):
self.fieldset.rebind(data=self.request.params)
return self.fieldset.validate()
@accepts_none
def required(value, field=None):
if value is None or value == '':
msg = "Please provide a value"
if field:
msg = "You must provide a value for %s" % field.label()
raise formalchemy.ValidationError(msg)

View file

@ -0,0 +1,75 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
``rattail.pyramid.forms.formalchemy.fields`` -- Field Classes
"""
import formalchemy
__all__ = ['AssociationProxyField', 'ChildGridField', 'PropertyField']
def AssociationProxyField(name, **kwargs):
"""
Returns a :class:`Field` class which is aware of SQLAlchemy association
proxies.
"""
class ProxyField(formalchemy.Field):
def sync(self):
if not self.is_readonly():
setattr(self.parent.model, self.name,
self.renderer.deserialize())
kwargs.setdefault('value', lambda x: getattr(x, name))
return ProxyField(name, **kwargs)
class ChildGridField(formalchemy.Field):
"""
Convenience class for including a child grid within a fieldset as a
read-only field.
"""
def __init__(self, name, value, *args, **kwargs):
super(ChildGridField, self).__init__(name, *args, **kwargs)
self.set(value=value)
self.set(readonly=True)
class PropertyField(formalchemy.Field):
"""
Convenience class for fields which simply involve a read-only property
value.
"""
def __init__(self, name, attr=None, *args, **kwargs):
super(PropertyField, self).__init__(name, *args, **kwargs)
if not attr:
attr = name
self.set(value=lambda x: getattr(x, attr))
self.set(readonly=True)

View file

@ -0,0 +1,72 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
``rattail.pyramid.forms.formalchemy.fieldset`` -- FormAlchemy FieldSet
"""
import formalchemy
from rattail.pyramid import Session
from rattail.util import prettify
__all__ = ['FieldSet', 'make_fieldset']
class FieldSet(formalchemy.FieldSet):
"""
Adds a little magic to the :class:`formalchemy.FieldSet` class.
"""
prettify = staticmethod(prettify)
def __init__(self, model, class_name=None, crud_title=None, url=None,
route_name=None, action_url='', home_url=None, **kwargs):
super(FieldSet, self).__init__(model, **kwargs)
self.class_name = class_name or self._original_cls.__name__.lower()
self.crud_title = crud_title or prettify(self.class_name)
self.edit = isinstance(model, self._original_cls)
self.route_name = route_name or (self.class_name + 's')
self.action_url = action_url
self.home_url = home_url
self.allow_continue = kwargs.pop('allow_continue', False)
def get_display_text(self):
return unicode(self.model)
def render(self, **kwargs):
kwargs.setdefault('class_', self.class_name)
return super(FieldSet, self).render(**kwargs)
def make_fieldset(model, **kwargs):
"""
Returns a :class:`FieldSet` equipped with the current scoped
:class:`rattail.db.Session` instance (unless ``session`` is provided as a
keyword argument).
"""
kwargs.setdefault('session', Session())
return FieldSet(model, **kwargs)

View file

@ -0,0 +1,188 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
``rattail.pyramid.forms.formalchemy.renderers`` -- Field Renderers
"""
import datetime
import formalchemy
from webhelpers.html import literal
from pyramid.renderers import render
from rattail.barcodes import GPC
from rattail.pyramid.forms.core import pretty_datetime
from rattail.pyramid.forms.formalchemy.fieldset import FieldSet
from rattail.time import local_time
__all__ = ['AutocompleteFieldRenderer', 'EnumFieldRenderer',
'StrippingFieldRenderer', 'YesNoFieldRenderer',
'DateTimeFieldRenderer']
def AutocompleteFieldRenderer(service_url, field_value=None, field_display=None, width='300px'):
"""
Autocomplete renderer.
"""
class AutocompleteFieldRenderer(formalchemy.fields.FieldRenderer):
@property
def focus_name(self):
return self.name + '-textbox'
@property
def needs_focus(self):
return not bool(self.value or field_value)
def render(self, **kwargs):
kwargs.setdefault('field_name', self.name)
kwargs.setdefault('field_value', self.value or field_value)
kwargs.setdefault('field_display', self.raw_value or field_display)
kwargs.setdefault('service_url', service_url)
kwargs.setdefault('width', width)
return render('/forms/field_autocomplete.mako', kwargs)
return AutocompleteFieldRenderer
def EnumFieldRenderer(enum):
"""
Adds support for enumeration fields.
"""
class Renderer(formalchemy.fields.SelectFieldRenderer):
def render_readonly(self, **kwargs):
value = self.raw_value
if value is None:
return ''
if value in enum:
return enum[value]
return str(value)
def render(self, **kwargs):
opts = [(enum[x], x) for x in sorted(enum)]
return formalchemy.fields.SelectFieldRenderer.render(self, opts, **kwargs)
return Renderer
class StrippingFieldRenderer(formalchemy.TextFieldRenderer):
"""
Standard text field renderer, which strips whitespace from either end of
the input value on deserialization.
"""
def deserialize(self):
value = super(StrippingFieldRenderer, self).deserialize()
return value.strip()
class YesNoFieldRenderer(formalchemy.fields.CheckBoxFieldRenderer):
def render_readonly(self, **kwargs):
value = self.raw_value
if value is None:
return ''
return 'Yes' if value else 'No'
class DateTimeFieldRenderer(formalchemy.fields.DateTimeFieldRenderer):
"""
Leverages Rattail time system to coerce timestamp to local time zone before
displaying it.
"""
def render_readonly(self, **kwargs):
value = self.raw_value
if isinstance(value, datetime.datetime):
value = local_time(value)
return value.strftime(self.format)
return ''
FieldSet.default_renderers[formalchemy.types.DateTime] = DateTimeFieldRenderer
def PrettyDateTimeFieldRenderer(from_='local', to='local'):
class PrettyDateTimeFieldRenderer(formalchemy.fields.DateTimeFieldRenderer):
def render_readonly(self, **kwargs):
return pretty_datetime(self.raw_value, from_=from_, to=to)
return PrettyDateTimeFieldRenderer
class GPCFieldRenderer(formalchemy.TextFieldRenderer):
"""
Renderer for :class:`rattail.GPC` fields.
"""
@property
def length(self):
# Hm, should maybe consider hard-coding this...?
return len(str(GPC(0)))
class PriceFieldRenderer(formalchemy.TextFieldRenderer):
"""
Renderer for fields which reference a :class:`ProductPrice` instance.
"""
def render_readonly(self, **kwargs):
price = self.field.raw_value
if price:
if price.price is not None and price.pack_price is not None:
if price.multiple > 1:
return literal('$ %0.2f / %u&nbsp; ($ %0.2f / %u)' % (
price.price, price.multiple,
price.pack_price, price.pack_multiple))
return literal('$ %0.2f&nbsp; ($ %0.2f / %u)' % (
price.price, price.pack_price, price.pack_multiple))
if price.price is not None:
if price.multiple > 1:
return '$ %0.2f / %u' % (price.price, price.multiple)
return '$ %0.2f' % price.price
if price.pack_price is not None:
return '$ %0.2f / %u' % (price.pack_price, price.pack_multiple)
return ''
class PriceWithExpirationFieldRenderer(PriceFieldRenderer):
"""
Price field renderer which also displays the expiration date, if present.
"""
def render_readonly(self, **kwargs):
res = super(PriceWithExpirationFieldRenderer, self).render_readonly(**kwargs)
if res:
price = self.field.raw_value
if price.ends:
res += '&nbsp; (%s)' % pretty_datetime(price.ends, from_='utc')
return res

View file

@ -0,0 +1,66 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
``rattail.pyramid.forms.simpleform`` -- pyramid_simpleform Forms
"""
from pyramid.renderers import render
import formencode
import pyramid_simpleform
from pyramid_simpleform.renderers import FormRenderer
from rattail.pyramid.forms.core import Form
__all__ = ['Schema', 'SimpleForm']
class Schema(formencode.Schema):
"""
Subclass of ``formencode.Schema``, which exists only to ignore extra
fields. These normally would cause a schema instance to be deemed invalid,
and pretty much *every* form has a submit button which would be considered
an extra field.
"""
allow_extra_fields = True
filter_extra_fields = True
class SimpleForm(Form):
template = None
def __init__(self, request, **kwargs):
super(SimpleForm, self).__init__(request, **kwargs)
self.form = pyramid_simpleform.Form(request)
def render(self, **kwargs):
kw = {
'form': self,
}
kw.update(kwargs)
return render('/forms/form.mako', kw)

View file

@ -0,0 +1,32 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
``rattail.pyramid.grids`` -- Grids
"""
from rattail.pyramid.grids.core import *
from rattail.pyramid.grids.alchemy import *
from rattail.pyramid.grids import util
from rattail.pyramid.grids import search

View file

@ -0,0 +1,124 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
``rattail.pyramid.grids.alchemy`` -- FormAlchemy Grid
"""
from webhelpers.html import tags
from webhelpers.html import HTML
import formalchemy
import rattail
from rattail.pyramid import Session
from rattail.pyramid.grids.core import Grid
from rattail.util import prettify
__all__ = ['AlchemyGrid']
class AlchemyGrid(Grid):
sort_map = {}
pager = None
pager_format = '$link_first $link_previous ~1~ $link_next $link_last'
def __init__(self, request, cls, instances, **kwargs):
super(AlchemyGrid, self).__init__(request, **kwargs)
self._formalchemy_grid = formalchemy.Grid(
cls, instances, session=Session(), request=request)
self._formalchemy_grid.prettify = prettify
self.noclick_fields = []
def __delattr__(self, attr):
delattr(self._formalchemy_grid, attr)
def __getattr__(self, attr):
return getattr(self._formalchemy_grid, attr)
def cell_class(self, field):
classes = [field.name]
if field.name in self.noclick_fields:
classes.append('noclick')
return ' '.join(classes)
def checkbox(self, row):
return tags.checkbox('check-'+row.uuid)
def click_route_kwargs(self, row):
return {'uuid': row.uuid}
def column_header(self, field):
class_ = None
label = field.label()
if field.key in self.sort_map:
class_ = 'sortable'
if field.key == self.config['sort']:
class_ += ' sorted ' + self.config['dir']
label = tags.link_to(label, '#')
return HTML.tag('th', class_=class_, field=field.key,
title=self.column_titles.get(field.key), c=label)
def edit_route_kwargs(self, row):
return {'uuid': row.uuid}
def delete_route_kwargs(self, row):
return {'uuid': row.uuid}
def iter_fields(self):
return self._formalchemy_grid.render_fields.itervalues()
def iter_rows(self):
for row in self._formalchemy_grid.rows:
self._formalchemy_grid._set_active(row)
yield row
def page_count_options(self):
options = rattail.config.get('rattail.pyramid', 'grid.page_count_options')
if options:
options = options.split(',')
options = [int(x.strip()) for x in options]
else:
options = [5, 10, 20, 50, 100]
return options
def page_links(self):
return self.pager.pager(self.pager_format,
symbol_next='next',
symbol_previous='prev',
onclick="grid_navigate_page(this, '$partial_url'); return false;")
def render_field(self, field):
if self._formalchemy_grid.readonly:
return field.render_readonly()
return field.render()
def row_attrs(self, row, i):
attrs = super(AlchemyGrid, self).row_attrs(row, i)
if hasattr(row, 'uuid'):
attrs['uuid'] = row.uuid
return attrs

View file

@ -0,0 +1,138 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
``rattail.pyramid.grids.core`` -- Core Grid Classes
"""
try:
from collections import OrderedDict
except ImportError:
from ordereddict import OrderedDict
from webhelpers.html import HTML
from webhelpers.html.builder import format_attrs
from pyramid.renderers import render
from rattail import Object
__all__ = ['Grid']
class Grid(Object):
full = False
hoverable = True
clickable = False
checkboxes = False
editable = False
deletable = False
partial_only = False
click_route_name = None
click_route_kwargs = None
edit_route_name = None
edit_route_kwargs = None
delete_route_name = None
delete_route_kwargs = None
def __init__(self, request, **kwargs):
kwargs.setdefault('fields', OrderedDict())
kwargs.setdefault('column_titles', {})
kwargs.setdefault('extra_columns', [])
super(Grid, self).__init__(**kwargs)
self.request = request
def add_column(self, name, label, callback):
self.extra_columns.append(
Object(name=name, label=label, callback=callback))
def column_header(self, field):
return HTML.tag('th', field=field.name,
title=self.column_titles.get(field.name),
c=field.label)
def div_attrs(self):
classes = ['grid']
if self.full:
classes.append('full')
if self.clickable:
classes.append('clickable')
if self.hoverable:
classes.append('hoverable')
return format_attrs(
class_=' '.join(classes),
url=self.request.current_route_url())
def get_delete_url(self, row):
kwargs = {}
if self.delete_route_kwargs:
if callable(self.delete_route_kwargs):
kwargs = self.delete_route_kwargs(row)
else:
kwargs = self.delete_route_kwargs
return self.request.route_url(self.delete_route_name, **kwargs)
def get_edit_url(self, row):
kwargs = {}
if self.edit_route_kwargs:
if callable(self.edit_route_kwargs):
kwargs = self.edit_route_kwargs(row)
else:
kwargs = self.edit_route_kwargs
return self.request.route_url(self.edit_route_name, **kwargs)
def get_row_attrs(self, row, i):
attrs = self.row_attrs(row, i)
if self.clickable:
kwargs = {}
if self.click_route_kwargs:
if callable(self.click_route_kwargs):
kwargs = self.click_route_kwargs(row)
else:
kwargs = self.click_route_kwargs
attrs['url'] = self.request.route_url(self.click_route_name, **kwargs)
return format_attrs(**attrs)
def iter_fields(self):
return self.fields.itervalues()
def iter_rows(self):
raise NotImplementedError
def render(self, template='/grids/grid.mako', **kwargs):
kwargs.setdefault('grid', self)
return render(template, kwargs)
def render_field(self, field):
raise NotImplementedError
def row_attrs(self, row, i):
attrs = {'class_': 'odd' if i % 2 else 'even'}
return attrs

View file

@ -0,0 +1,274 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
``rattail.pyramid.grids.search`` -- Grid Search Filters
"""
from sqlalchemy import or_
from webhelpers.html import tags
from webhelpers.html import literal
from pyramid.renderers import render
from pyramid_simpleform import Form
from pyramid_simpleform.renderers import FormRenderer
from rattail.util import Object, prettify
class SearchFilter(Object):
"""
Base class and default implementation for search filters.
"""
def __init__(self, name, label=None, **kwargs):
Object.__init__(self, **kwargs)
self.name = name
self.label = label or prettify(name)
def types_select(self):
types = [
('is', "is"),
('nt', "is not"),
('lk', "contains"),
('nl', "doesn't contain"),
]
options = []
filter_map = self.search.filter_map[self.name]
for value, label in types:
if value in filter_map:
options.append((value, label))
return tags.select('filter_type_'+self.name,
self.search.config.get('filter_type_'+self.name),
options, class_='filter-type')
def value_control(self):
return tags.text(self.name, self.search.config.get(self.name))
class BooleanSearchFilter(SearchFilter):
"""
Boolean search filter.
"""
def value_control(self):
return tags.select(self.name, self.search.config.get(self.name),
["True", "False"])
class SearchForm(Form):
"""
Generic form class which aggregates :class:`SearchFilter` instances.
"""
def __init__(self, request, filter_map, config, *args, **kwargs):
super(SearchForm, self).__init__(request, *args, **kwargs)
self.filter_map = filter_map
self.config = config
self.filters = {}
def add_filter(self, filter_):
filter_.search = self
self.filters[filter_.name] = filter_
class SearchFormRenderer(FormRenderer):
"""
Renderer for :class:`SearchForm` instances.
"""
def __init__(self, form, *args, **kwargs):
super(SearchFormRenderer, self).__init__(form, *args, **kwargs)
self.request = form.request
self.filters = form.filters
self.config = form.config
def add_filter(self, visible):
options = ['add a filter']
for f in self.sorted_filters():
options.append((f.name, f.label))
return self.select('add-filter', options,
style='display: none;' if len(visible) == len(self.filters) else None)
def checkbox(self, name, checked=None, **kwargs):
if name.startswith('include_filter_'):
if checked is None:
checked = self.config[name]
return tags.checkbox(name, checked=checked, **kwargs)
if checked is None:
checked = False
return super(SearchFormRenderer, self).checkbox(name, checked=checked, **kwargs)
def render(self, **kwargs):
kwargs['search'] = self
return literal(render('/grids/search.mako', kwargs))
def sorted_filters(self):
return sorted(self.filters.values(), key=lambda x: x.label)
def text(self, name, **kwargs):
return tags.text(name, value=self.config.get(name), **kwargs)
def filter_exact(field):
"""
Convenience function which returns a filter map entry, with typical logic
built in for "exact match" queries applied to ``field``.
"""
return {
'is':
lambda q, v: q.filter(field == v) if v else q,
'nt':
lambda q, v: q.filter(field != v) if v else q,
}
def filter_ilike(field):
"""
Convenience function which returns a filter map entry, with typical logic
built in for "ILIKE" queries applied to ``field``.
"""
def ilike(query, value):
if value:
query = query.filter(field.ilike('%%%s%%' % value))
return query
def not_ilike(query, value):
if value:
query = query.filter(or_(
field == None,
~field.ilike('%%%s%%' % value),
))
return query
return {'lk': ilike, 'nl': not_ilike}
def get_filter_config(prefix, request, filter_map, **kwargs):
"""
Returns a configuration dictionary for a search form.
"""
config = {}
def update_config(dict_, prefix='', exclude_by_default=False):
"""
Updates the ``config`` dictionary based on the contents of ``dict_``.
"""
for field in filter_map:
if prefix+'include_filter_'+field in dict_:
include = dict_[prefix+'include_filter_'+field]
include = bool(include) and include != '0'
config['include_filter_'+field] = include
elif exclude_by_default:
config['include_filter_'+field] = False
if prefix+'filter_type_'+field in dict_:
config['filter_type_'+field] = dict_[prefix+'filter_type_'+field]
if prefix+field in dict_:
config[field] = dict_[prefix+field]
# Update config to exclude all filters by default.
for field in filter_map:
config['include_filter_'+field] = False
# Update config to honor default settings.
config.update(kwargs)
# Update config with data cached in session.
update_config(request.session, prefix=prefix+'.')
# Update config with data from GET/POST request.
if request.params.get('filters') == 'true':
update_config(request.params, exclude_by_default=True)
# Cache filter data in session.
for key in config:
if (not key.startswith('filter_factory_')
and not key.startswith('filter_label_')):
request.session[prefix+'.'+key] = config[key]
return config
def get_filter_map(cls, exact=[], ilike=[], **kwargs):
"""
Convenience function which returns a "filter map" for ``cls``.
``exact``, if provided, should be a list of field names for which "exact"
filtering is to be allowed.
``ilike``, if provided, should be a list of field names for which "ILIKE"
filtering is to be allowed.
Any remaining ``kwargs`` are assumed to be filter map entries themselves,
and are added directly to the map.
"""
fmap = {}
for name in exact:
fmap[name] = filter_exact(getattr(cls, name))
for name in ilike:
fmap[name] = filter_ilike(getattr(cls, name))
fmap.update(kwargs)
return fmap
def get_search_form(request, filter_map, config):
"""
Returns a :class:`SearchForm` instance with a :class:`SearchFilter` for
each filter in ``filter_map``, using configuration from ``config``.
"""
search = SearchForm(request, filter_map, config)
for field in filter_map:
factory = config.get('filter_factory_%s' % field, SearchFilter)
label = config.get('filter_label_%s' % field)
search.add_filter(factory(field, label=label))
return search
def filter_query(query, config, filter_map, join_map):
"""
Filters ``query`` according to ``config`` and ``filter_map``. ``join_map``
is used, if necessary, to join additional tables to the base query. The
filtered query is returned.
"""
joins = config.setdefault('joins', [])
for key in config:
if key.startswith('include_filter_') and config[key]:
field = key[15:]
if field in join_map and field not in joins:
query = join_map[field](query)
joins.append(field)
value = config.get(field)
if value:
fmap = filter_map[field]
filt = fmap[config['filter_type_'+field]]
query = filt(query, value)
return query

View file

@ -0,0 +1,138 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
``rattail.pyramid.grids.util`` -- Grid Utilities
"""
from sqlalchemy.orm.attributes import InstrumentedAttribute
from webhelpers.html import literal
from pyramid.response import Response
from rattail.pyramid.grids.search import SearchFormRenderer
def get_sort_config(name, request, **kwargs):
"""
Returns a configuration dictionary for grid sorting.
"""
# Initial config uses some default values.
config = {
'dir': 'asc',
'per_page': 20,
'page': 1,
}
# Override with defaults provided by caller.
config.update(kwargs)
# Override with values from GET/POST request and/or session.
for key in config:
full_key = name+'_'+key
if request.params.get(key):
value = request.params[key]
config[key] = value
request.session[full_key] = value
elif request.session.get(full_key):
value = request.session[full_key]
config[key] = value
return config
def get_sort_map(cls, names=None, **kwargs):
"""
Convenience function which returns a sort map for ``cls``.
If ``names`` is not specified, the map will include all "standard" fields
present on the mapped class. Otherwise, the map will be limited to only
the fields which are named.
All remaining ``kwargs`` are assumed to be sort map entries, and will be
added to the map directly.
"""
smap = {}
if names is None:
names = []
for attr in cls.__dict__:
obj = getattr(cls, attr)
if isinstance(obj, InstrumentedAttribute):
if obj.key != 'uuid':
names.append(obj.key)
for name in names:
smap[name] = sorter(getattr(cls, name))
smap.update(kwargs)
return smap
def render_grid(grid, search_form=None, **kwargs):
"""
Convenience function to render ``grid`` (which should be a
:class:`rattail.pyramid.grids.Grid` instance).
This "usually" will return a dictionary to be used as context for rendering
the final view template.
However, if a partial grid is requested (or mandated), then the grid body
will be rendered and a :class:`pyramid.response.Response` object will be
returned instead.
"""
if grid.partial_only or grid.request.params.get('partial'):
return Response(body=grid.render(), content_type='text/html')
kwargs['grid'] = literal(grid.render())
if search_form:
kwargs['search'] = SearchFormRenderer(search_form)
return kwargs
def sort_query(query, config, sort_map, join_map={}):
"""
Sorts ``query`` according to ``config`` and ``sort_map``. ``join_map`` is
used, if necessary, to join additional tables to the base query. The
sorted query is returned.
"""
field = config.get('sort')
if not field:
return query
joins = config.setdefault('joins', [])
if field in join_map and field not in joins:
query = join_map[field](query)
joins.append(field)
sort = sort_map[field]
return sort(query, config['dir'])
def sorter(field):
"""
Returns a function suitable for a sort map callable, with typical logic
built in for sorting applied to ``field``.
"""
return lambda q, d: q.order_by(getattr(field, d)())

View file

@ -0,0 +1,33 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
``rattail.pyramid.helpers`` -- Template Context Helpers
"""
import datetime
from decimal import Decimal
from webhelpers.html import *
from webhelpers.html.tags import *

View file

@ -0,0 +1,78 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
``rattail.pyramid.progress`` -- Progress Indicator
"""
from beaker.session import Session
def get_progress_session(session, key):
request = session.request
id = '%s.progress.%s' % (session.id, key)
session = Session(request, id)
return session
class SessionProgress(object):
"""
Provides a session-based progress bar mechanism.
This class is only responsible for keeping the progress *data* current. It
is the responsibility of some client-side AJAX (etc.) to consume the data
for display to the user.
"""
def __init__(self, session, key):
self.session = get_progress_session(session, key)
self.canceled = False
self.clear()
def __call__(self, message, maximum):
self.clear()
self.session['message'] = message
self.session['maximum'] = maximum
self.session['value'] = 0
self.session.save()
return self
def clear(self):
self.session.clear()
self.session['complete'] = False
self.session['error'] = False
self.session['canceled'] = False
self.session.save()
def update(self, value):
self.session.load()
if self.session.get('canceled'):
self.canceled = True
else:
self.session['value'] = value
self.session.save()
return not self.canceled
def destroy(self):
pass

View file

@ -1,4 +1,3 @@
## -*- coding: utf-8 -*-
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html style="direction: ltr;" xmlns="http://www.w3.org/1999/xhtml" lang="en-us">
<head>
@ -83,7 +82,7 @@
<tr>
<td class="upc">${get_upc(product)}</td>
<td class="brand">${product.brand or ''}</td>
<td class="description">${product.description} ${product.size}</td>
<td class="description">${product.description}</td>
<td class="count">&nbsp;</td>
</tr>
% endfor

View file

@ -1,4 +1,3 @@
## -*- coding: utf-8 -*-
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html style="direction: ltr;" xmlns="http://www.w3.org/1999/xhtml" lang="en-us">
<head>
@ -6,10 +5,6 @@
<title>Ordering Worksheet : ${vendor.name}</title>
<style type="text/css">
body {
font-family: sans-serif;
}
h1 {
font-size: 24px;
margin: 10px 0px;
@ -62,11 +57,6 @@
text-align: center;
}
td.code {
font-family: monospace;
font-size: 120%;
}
td.scratch_pad {
width: 20px;
}
@ -89,29 +79,25 @@
<table>
% for dept in sorted(costs, key=lambda x: x.name):
<tr>
<th class="department" colspan="21">Department:&nbsp; ${dept.name} (${dept.number})</th>
<th class="department" colspan="19">Department:&nbsp; ${dept.name} (${dept.number})</th>
</tr>
% for subdept in sorted(costs[dept], key=lambda x: x.name):
<tr>
<th class="subdepartment" colspan="21">Subdepartment:&nbsp; ${subdept.name} (${subdept.number})</th>
<th class="subdepartment" colspan="19">Subdepartment:&nbsp; ${subdept.name} (${subdept.number})</th>
</tr>
<tr>
<th>UPC</th>
<th>Brand</th>
<th>Description</th>
<th>Size</th>
<th>Case</th>
<th>Case Qty.</th>
<th>Vend. Code</th>
<th>Pref.</th>
<th>Preferred</th>
<th colspan="14">Order Scratch Pad</th>
</tr>
% for cost in sorted(costs[dept][subdept], key=cost_sort_key):
% for cost in sorted(costs[dept][subdept], key=lambda x: x.product.description):
<tr>
<td class="upc">${get_upc(cost.product)}</td>
<td class="brand">${cost.product.brand or ''}</td>
<td class="desc">${cost.product.description}</td>
<td class="size">${cost.product.size or ''}</td>
<td class="case-qty">${app.render_quantity(cost.case_size)} ${"LB" if cost.product.weighed else "EA"}</td>
<td class="case-qty">${cost.case_size} ${rattail.UNIT_OF_MEASURE.get(cost.product.unit_of_measure, '')}</td>
<td class="code">${cost.code or ''}</td>
<td class="preferred">${'X' if cost.preference == 1 else ''}</td>
% for i in range(14):
@ -120,7 +106,7 @@
</tr>
% endfor
<tr>
<td class="spacer" colspan="21">
<td class="spacer" colspan="19">
</tr>
% endfor
% endfor

View file

@ -0,0 +1,24 @@
/******************************
* Autocomplete
******************************/
div.autocomplete {
border: 1px solid #000000;
margin-top: 5px;
}
div.autocomplete div {
background-color: #dddddd;
margin: 0px;
padding: 2px 5px;
}
div.autocomplete strong {
margin: 0px 1px;
}
div.autocomplete .selected {
cursor: pointer;
background-color: #aaaaaa;
}

View file

@ -0,0 +1,98 @@
/******************************
* General
******************************/
* {
margin: 0px;
}
html, body {
font-family: sans-serif;
font-size: 11pt;
}
a {
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
h1 {
margin-bottom: 15px;
}
h2 {
font-size: 12pt;
margin: 20px auto 10px auto;
}
li {
line-height: 2em;
}
p {
margin-bottom: 5px;
}
.left {
float: left;
text-align: left;
}
.right {
float: right;
text-align: right;
}
.wrapper {
overflow: auto;
}
div.buttons {
clear: both;
margin-top: 10px;
}
/* div.controls { */
/* font-weight: bold; */
/* margin: 10px auto; */
/* } */
/* div.controls div { */
/* margin: 5px; */
/* } */
/* div.controls label { */
/* display: block; */
/* float: left; */
/* width: 120px; */
/* } */
div.dialog {
display: none;
}
div.flash-message {
background-color: #dddddd;
margin-bottom: 8px;
padding: 3px;
}
div.error {
color: #dd6666;
font-weight: bold;
margin-bottom: 10px;
}
ul.error {
color: #dd6666;
font-weight: bold;
padding: 0px;
}
ul.error li {
list-style-type: none;
}

View file

@ -0,0 +1,24 @@
/******************************
* Filters
******************************/
div.filters div.filter {
margin-bottom: 10px;
}
div.filters div.filter label {
margin-right: 8px;
}
div.filters div.filter select.filter-type {
margin-right: 8px;
}
div.filters div.filter div.value {
display: inline;
}
div.filters div.buttons * {
margin-right: 8px;
}

View file

@ -0,0 +1,97 @@
/******************************
* Form Wrapper
******************************/
div.form-wrapper {
overflow: auto;
}
/******************************
* Context Menu
******************************/
div.form-wrapper ul.context-menu {
float: right;
list-style-type: none;
margin: 0px;
text-align: right;
}
/******************************
* Forms
******************************/
div.form,
div.fieldset-form,
div.fieldset {
float: left;
margin-top: 10px;
}
/******************************
* Fieldsets
******************************/
div.field-wrapper {
clear: both;
min-height: 30px;
overflow: auto;
padding: 5px;
}
div.field-wrapper.error {
background-color: #ddcccc;
border: 2px solid #dd6666;
}
div.field-wrapper label {
color: #000000;
display: block;
float: left;
width: 140px;
font-weight: bold;
margin-top: 2px;
white-space: nowrap;
}
div.field-wrapper div.field-error {
/* clear: both; */
color: #dd6666;
font-weight: bold;
}
div.field-wrapper div.field {
display: block;
float: left;
margin-bottom: 5px;
line-height: 25px;
}
div.field-wrapper div.field input[type=text],
div.field-wrapper div.field input[type=password],
div.field-wrapper div.field select,
div.field-wrapper div.field textarea {
width: 320px;
}
label input[type=checkbox] {
margin-right: 8px;
}
/******************************
* Buttons
******************************/
div.buttons {
clear: both;
margin-top: 10px;
}
div.buttons * {
margin-right: 8px;
}

View file

@ -0,0 +1,178 @@
/******************************
* Grid Header
******************************/
table.grid-header {
padding-bottom: 5px;
width: 100%;
}
/******************************
* Form (Filters etc.)
******************************/
table.grid-header td.form {
vertical-align: bottom;
}
/******************************
* Context Menu
******************************/
table.grid-header td.context-menu {
vertical-align: top;
}
table.grid-header td.context-menu ul {
list-style-type: none;
margin: 0px;
text-align: right;
}
/******************************
* Tools
******************************/
table.grid-header td.tools {
text-align: right;
vertical-align: bottom;
}
/******************************
* Grid
******************************/
div.grid {
clear: both;
}
div.grid table {
border-top: 1px solid black;
border-left: 1px solid black;
border-collapse: collapse;
font-size: 9pt;
line-height: normal;
white-space: nowrap;
}
div.grid.full table {
width: 100%;
}
div.grid table th,
div.grid table td {
border-right: 1px solid black;
border-bottom: 1px solid black;
padding: 2px 3px;
}
div.grid table th.sortable a {
display: block;
padding-right: 18px;
}
div.grid table th.sorted {
background-position: right center;
background-repeat: no-repeat;
}
div.grid table th.sorted.asc {
background-image: url(../img/sort_arrow_up.png);
}
div.grid table th.sorted.desc {
background-image: url(../img/sort_arrow_down.png);
}
div.grid table tbody td {
text-align: left;
}
div.grid table tbody td.center {
text-align: center;
}
div.grid table tbody td.right {
float: none;
text-align: right;
}
div.grid table tr.odd {
background-color: #e0e0e0;
}
/* div.grid table thead th.checkbox, */
/* div.grid table tbody td.checkbox { */
/* text-align: center; */
/* vertical-align: middle; */
/* width: 15px; */
/* } */
div.grid table tbody tr.hovering {
background-color: #bbbbbb;
}
div.grid table.hoverable tbody tr {
cursor: default;
}
div.grid.clickable table tbody tr,
div.grid table.selectable tbody tr,
div.grid table.checkable tbody tr {
cursor: pointer;
}
div.grid.clickable table tbody tr td.noclick {
cursor: default;
text-align: center;
width: 18px;
}
div.grid table tbody tr td.noclick.edit,
div.grid table tbody tr td.noclick.delete {
background-repeat: no-repeat;
background-position: center;
cursor: pointer;
min-width: 18px;
}
div.grid table tbody tr td.noclick.edit {
background-image: url(../img/edit.png);
}
div.grid table tbody tr td.noclick.delete {
background-image: url(../img/delete.png);
}
/* div.grid table.selectable tbody tr.selected, */
/* div.grid table.checkable tbody tr.selected { */
/* background-color: #666666; */
/* color: white; */
/* } */
div.pager {
margin-bottom: 20px;
margin-top: 5px;
}
div.pager p {
font-size: 10pt;
margin: 0px;
}
div.pager p.showing {
float: left;
}
div.pager #grid-page-count {
font-size: 8pt;
}
div.pager p.page-links {
float: right;
}

View file

@ -0,0 +1,75 @@
/******************************
* Main Layout
******************************/
html, body, #container {
height: 100%;
}
body > #container {
height: auto;
min-height: 100%;
}
#container {
margin: 0 auto;
width: 1000px;
}
#header {
border-bottom: 1px solid #000000;
overflow: auto;
}
#body {
padding-top: 15px;
padding-bottom: 5em;
}
#footer {
clear: both;
margin-top: -4em;
text-align: center;
}
/******************************
* Header
******************************/
#header h1 {
margin: 0px 5px 10px 5px;
}
#login {
margin: 8px 20px auto auto;
}
#login a.username {
font-weight: bold;
}
#user-menu {
float: left;
}
#home-link {
font-weight: bold;
}
#header-links {
float: right;
text-align: right;
}
#main-menu {
border-top: 1px solid black;
clear: both;
font-weight: bold;
}
#main-menu li {
display: inline;
margin-right: 15px;
}

View file

@ -0,0 +1,29 @@
/******************************
* login.css
******************************/
div.form {
margin: auto;
float: none;
text-align: center;
}
div.field-wrapper {
margin: 10px auto;
width: 300px;
}
div.field-wrapper label {
margin: 0px;
text-align: right;
width: 100px;
}
div.field-wrapper input {
width: 150px;
}
div.buttons input {
margin: auto 5px;
}

View file

@ -0,0 +1,33 @@
/******************************
* Permission Lists
******************************/
div.field-wrapper.permissions div.field div.group {
margin-bottom: 10px;
}
div.field-wrapper.permissions div.field div.group p {
font-weight: bold;
}
div.field-wrapper.permissions div.field label {
float: none;
font-weight: normal;
}
div.field-wrapper.permissions div.field label input {
margin-left: 15px;
margin-right: 10px;
}
div.field-wrapper.permissions div.field div.group p.perm {
font-weight: normal;
margin-left: 15px;
}
div.field-wrapper.permissions div.field div.group p.perm span {
font-family: monospace;
/* font-weight: bold; */
margin-right: 10px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View file

@ -0,0 +1,489 @@
/*
* jQuery UI CSS Framework
* Copyright (c) 2010 AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT (MIT-LICENSE.txt) and GPL (GPL-LICENSE.txt) licenses.
*/
/* Layout helpers
----------------------------------*/
.ui-helper-hidden { display: none; }
.ui-helper-hidden-accessible { position: absolute; left: -99999999px; }
.ui-helper-reset { margin: 0; padding: 0; border: 0; outline: 0; line-height: 1.3; text-decoration: none; font-size: 100%; list-style: none; }
.ui-helper-clearfix:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; }
.ui-helper-clearfix { display: inline-block; }
/* required comment for clearfix to work in Opera \*/
* html .ui-helper-clearfix { height:1%; }
.ui-helper-clearfix { display:block; }
/* end clearfix */
.ui-helper-zfix { width: 100%; height: 100%; top: 0; left: 0; position: absolute; opacity: 0; filter:Alpha(Opacity=0); }
/* Interaction Cues
----------------------------------*/
.ui-state-disabled { cursor: default !important; }
/* Icons
----------------------------------*/
/* states and images */
.ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; }
/* Misc visuals
----------------------------------*/
/* Overlays */
.ui-widget-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
/*
* jQuery UI CSS Framework
* Copyright (c) 2010 AUTHORS.txt (http://jqueryui.com/about)
* Dual licensed under the MIT (MIT-LICENSE.txt) and GPL (GPL-LICENSE.txt) licenses.
* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana,Arial,sans-serif&fwDefault=normal&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=cccccc&bgTextureHeader=03_highlight_soft.png&bgImgOpacityHeader=75&borderColorHeader=aaaaaa&fcHeader=222222&iconColorHeader=222222&bgColorContent=ffffff&bgTextureContent=01_flat.png&bgImgOpacityContent=75&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=222222&bgColorDefault=e6e6e6&bgTextureDefault=02_glass.png&bgImgOpacityDefault=75&borderColorDefault=d3d3d3&fcDefault=555555&iconColorDefault=888888&bgColorHover=dadada&bgTextureHover=02_glass.png&bgImgOpacityHover=75&borderColorHover=999999&fcHover=212121&iconColorHover=454545&bgColorActive=ffffff&bgTextureActive=02_glass.png&bgImgOpacityActive=65&borderColorActive=aaaaaa&fcActive=212121&iconColorActive=454545&bgColorHighlight=fbf9ee&bgTextureHighlight=02_glass.png&bgImgOpacityHighlight=55&borderColorHighlight=fcefa1&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=02_glass.png&bgImgOpacityError=95&borderColorError=cd0a0a&fcError=cd0a0a&iconColorError=cd0a0a&bgColorOverlay=aaaaaa&bgTextureOverlay=01_flat.png&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=01_flat.png&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px
*/
/* Component containers
----------------------------------*/
.ui-widget { font-family: Verdana,Arial,sans-serif; font-size: 1.1em; }
.ui-widget .ui-widget { font-size: 1em; }
.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Verdana,Arial,sans-serif; font-size: 1em; }
.ui-widget-content { border: 1px solid #aaaaaa; background: #ffffff url(images/ui-bg_flat_75_ffffff_40x100.png) 50% 50% repeat-x; color: #222222; }
.ui-widget-content a { color: #222222; }
.ui-widget-header { border: 1px solid #aaaaaa; background: #cccccc url(images/ui-bg_highlight-soft_75_cccccc_1x100.png) 50% 50% repeat-x; color: #222222; font-weight: bold; }
.ui-widget-header a { color: #222222; }
/* Interaction states
----------------------------------*/
.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #d3d3d3; background: #e6e6e6 url(images/ui-bg_glass_75_e6e6e6_1x400.png) 50% 50% repeat-x; font-weight: normal; color: #555555; }
.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #555555; text-decoration: none; }
.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { border: 1px solid #999999; background: #dadada url(images/ui-bg_glass_75_dadada_1x400.png) 50% 50% repeat-x; font-weight: normal; color: #212121; }
.ui-state-hover a, .ui-state-hover a:hover { color: #212121; text-decoration: none; }
.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { border: 1px solid #aaaaaa; background: #ffffff url(images/ui-bg_glass_65_ffffff_1x400.png) 50% 50% repeat-x; font-weight: normal; color: #212121; }
.ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #212121; text-decoration: none; }
.ui-widget :active { outline: none; }
/* Interaction Cues
----------------------------------*/
.ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight {border: 1px solid #fcefa1; background: #fbf9ee url(images/ui-bg_glass_55_fbf9ee_1x400.png) 50% 50% repeat-x; color: #363636; }
.ui-state-highlight a, .ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a { color: #363636; }
.ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error {border: 1px solid #cd0a0a; background: #fef1ec url(images/ui-bg_glass_95_fef1ec_1x400.png) 50% 50% repeat-x; color: #cd0a0a; }
.ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { color: #cd0a0a; }
.ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { color: #cd0a0a; }
.ui-priority-primary, .ui-widget-content .ui-priority-primary, .ui-widget-header .ui-priority-primary { font-weight: bold; }
.ui-priority-secondary, .ui-widget-content .ui-priority-secondary, .ui-widget-header .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; }
.ui-state-disabled, .ui-widget-content .ui-state-disabled, .ui-widget-header .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; }
/* Icons
----------------------------------*/
/* states and images */
.ui-icon { width: 16px; height: 16px; background-image: url(images/ui-icons_222222_256x240.png); }
.ui-widget-content .ui-icon {background-image: url(images/ui-icons_222222_256x240.png); }
.ui-widget-header .ui-icon {background-image: url(images/ui-icons_222222_256x240.png); }
.ui-state-default .ui-icon { background-image: url(images/ui-icons_888888_256x240.png); }
.ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: url(images/ui-icons_454545_256x240.png); }
.ui-state-active .ui-icon {background-image: url(images/ui-icons_454545_256x240.png); }
.ui-state-highlight .ui-icon {background-image: url(images/ui-icons_2e83ff_256x240.png); }
.ui-state-error .ui-icon, .ui-state-error-text .ui-icon {background-image: url(images/ui-icons_cd0a0a_256x240.png); }
/* positioning */
.ui-icon-carat-1-n { background-position: 0 0; }
.ui-icon-carat-1-ne { background-position: -16px 0; }
.ui-icon-carat-1-e { background-position: -32px 0; }
.ui-icon-carat-1-se { background-position: -48px 0; }
.ui-icon-carat-1-s { background-position: -64px 0; }
.ui-icon-carat-1-sw { background-position: -80px 0; }
.ui-icon-carat-1-w { background-position: -96px 0; }
.ui-icon-carat-1-nw { background-position: -112px 0; }
.ui-icon-carat-2-n-s { background-position: -128px 0; }
.ui-icon-carat-2-e-w { background-position: -144px 0; }
.ui-icon-triangle-1-n { background-position: 0 -16px; }
.ui-icon-triangle-1-ne { background-position: -16px -16px; }
.ui-icon-triangle-1-e { background-position: -32px -16px; }
.ui-icon-triangle-1-se { background-position: -48px -16px; }
.ui-icon-triangle-1-s { background-position: -64px -16px; }
.ui-icon-triangle-1-sw { background-position: -80px -16px; }
.ui-icon-triangle-1-w { background-position: -96px -16px; }
.ui-icon-triangle-1-nw { background-position: -112px -16px; }
.ui-icon-triangle-2-n-s { background-position: -128px -16px; }
.ui-icon-triangle-2-e-w { background-position: -144px -16px; }
.ui-icon-arrow-1-n { background-position: 0 -32px; }
.ui-icon-arrow-1-ne { background-position: -16px -32px; }
.ui-icon-arrow-1-e { background-position: -32px -32px; }
.ui-icon-arrow-1-se { background-position: -48px -32px; }
.ui-icon-arrow-1-s { background-position: -64px -32px; }
.ui-icon-arrow-1-sw { background-position: -80px -32px; }
.ui-icon-arrow-1-w { background-position: -96px -32px; }
.ui-icon-arrow-1-nw { background-position: -112px -32px; }
.ui-icon-arrow-2-n-s { background-position: -128px -32px; }
.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
.ui-icon-arrow-2-e-w { background-position: -160px -32px; }
.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
.ui-icon-arrowstop-1-n { background-position: -192px -32px; }
.ui-icon-arrowstop-1-e { background-position: -208px -32px; }
.ui-icon-arrowstop-1-s { background-position: -224px -32px; }
.ui-icon-arrowstop-1-w { background-position: -240px -32px; }
.ui-icon-arrowthick-1-n { background-position: 0 -48px; }
.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
.ui-icon-arrowthick-1-e { background-position: -32px -48px; }
.ui-icon-arrowthick-1-se { background-position: -48px -48px; }
.ui-icon-arrowthick-1-s { background-position: -64px -48px; }
.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
.ui-icon-arrowthick-1-w { background-position: -96px -48px; }
.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
.ui-icon-arrow-4 { background-position: 0 -80px; }
.ui-icon-arrow-4-diag { background-position: -16px -80px; }
.ui-icon-extlink { background-position: -32px -80px; }
.ui-icon-newwin { background-position: -48px -80px; }
.ui-icon-refresh { background-position: -64px -80px; }
.ui-icon-shuffle { background-position: -80px -80px; }
.ui-icon-transfer-e-w { background-position: -96px -80px; }
.ui-icon-transferthick-e-w { background-position: -112px -80px; }
.ui-icon-folder-collapsed { background-position: 0 -96px; }
.ui-icon-folder-open { background-position: -16px -96px; }
.ui-icon-document { background-position: -32px -96px; }
.ui-icon-document-b { background-position: -48px -96px; }
.ui-icon-note { background-position: -64px -96px; }
.ui-icon-mail-closed { background-position: -80px -96px; }
.ui-icon-mail-open { background-position: -96px -96px; }
.ui-icon-suitcase { background-position: -112px -96px; }
.ui-icon-comment { background-position: -128px -96px; }
.ui-icon-person { background-position: -144px -96px; }
.ui-icon-print { background-position: -160px -96px; }
.ui-icon-trash { background-position: -176px -96px; }
.ui-icon-locked { background-position: -192px -96px; }
.ui-icon-unlocked { background-position: -208px -96px; }
.ui-icon-bookmark { background-position: -224px -96px; }
.ui-icon-tag { background-position: -240px -96px; }
.ui-icon-home { background-position: 0 -112px; }
.ui-icon-flag { background-position: -16px -112px; }
.ui-icon-calendar { background-position: -32px -112px; }
.ui-icon-cart { background-position: -48px -112px; }
.ui-icon-pencil { background-position: -64px -112px; }
.ui-icon-clock { background-position: -80px -112px; }
.ui-icon-disk { background-position: -96px -112px; }
.ui-icon-calculator { background-position: -112px -112px; }
.ui-icon-zoomin { background-position: -128px -112px; }
.ui-icon-zoomout { background-position: -144px -112px; }
.ui-icon-search { background-position: -160px -112px; }
.ui-icon-wrench { background-position: -176px -112px; }
.ui-icon-gear { background-position: -192px -112px; }
.ui-icon-heart { background-position: -208px -112px; }
.ui-icon-star { background-position: -224px -112px; }
.ui-icon-link { background-position: -240px -112px; }
.ui-icon-cancel { background-position: 0 -128px; }
.ui-icon-plus { background-position: -16px -128px; }
.ui-icon-plusthick { background-position: -32px -128px; }
.ui-icon-minus { background-position: -48px -128px; }
.ui-icon-minusthick { background-position: -64px -128px; }
.ui-icon-close { background-position: -80px -128px; }
.ui-icon-closethick { background-position: -96px -128px; }
.ui-icon-key { background-position: -112px -128px; }
.ui-icon-lightbulb { background-position: -128px -128px; }
.ui-icon-scissors { background-position: -144px -128px; }
.ui-icon-clipboard { background-position: -160px -128px; }
.ui-icon-copy { background-position: -176px -128px; }
.ui-icon-contact { background-position: -192px -128px; }
.ui-icon-image { background-position: -208px -128px; }
.ui-icon-video { background-position: -224px -128px; }
.ui-icon-script { background-position: -240px -128px; }
.ui-icon-alert { background-position: 0 -144px; }
.ui-icon-info { background-position: -16px -144px; }
.ui-icon-notice { background-position: -32px -144px; }
.ui-icon-help { background-position: -48px -144px; }
.ui-icon-check { background-position: -64px -144px; }
.ui-icon-bullet { background-position: -80px -144px; }
.ui-icon-radio-off { background-position: -96px -144px; }
.ui-icon-radio-on { background-position: -112px -144px; }
.ui-icon-pin-w { background-position: -128px -144px; }
.ui-icon-pin-s { background-position: -144px -144px; }
.ui-icon-play { background-position: 0 -160px; }
.ui-icon-pause { background-position: -16px -160px; }
.ui-icon-seek-next { background-position: -32px -160px; }
.ui-icon-seek-prev { background-position: -48px -160px; }
.ui-icon-seek-end { background-position: -64px -160px; }
.ui-icon-seek-start { background-position: -80px -160px; }
/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */
.ui-icon-seek-first { background-position: -80px -160px; }
.ui-icon-stop { background-position: -96px -160px; }
.ui-icon-eject { background-position: -112px -160px; }
.ui-icon-volume-off { background-position: -128px -160px; }
.ui-icon-volume-on { background-position: -144px -160px; }
.ui-icon-power { background-position: 0 -176px; }
.ui-icon-signal-diag { background-position: -16px -176px; }
.ui-icon-signal { background-position: -32px -176px; }
.ui-icon-battery-0 { background-position: -48px -176px; }
.ui-icon-battery-1 { background-position: -64px -176px; }
.ui-icon-battery-2 { background-position: -80px -176px; }
.ui-icon-battery-3 { background-position: -96px -176px; }
.ui-icon-circle-plus { background-position: 0 -192px; }
.ui-icon-circle-minus { background-position: -16px -192px; }
.ui-icon-circle-close { background-position: -32px -192px; }
.ui-icon-circle-triangle-e { background-position: -48px -192px; }
.ui-icon-circle-triangle-s { background-position: -64px -192px; }
.ui-icon-circle-triangle-w { background-position: -80px -192px; }
.ui-icon-circle-triangle-n { background-position: -96px -192px; }
.ui-icon-circle-arrow-e { background-position: -112px -192px; }
.ui-icon-circle-arrow-s { background-position: -128px -192px; }
.ui-icon-circle-arrow-w { background-position: -144px -192px; }
.ui-icon-circle-arrow-n { background-position: -160px -192px; }
.ui-icon-circle-zoomin { background-position: -176px -192px; }
.ui-icon-circle-zoomout { background-position: -192px -192px; }
.ui-icon-circle-check { background-position: -208px -192px; }
.ui-icon-circlesmall-plus { background-position: 0 -208px; }
.ui-icon-circlesmall-minus { background-position: -16px -208px; }
.ui-icon-circlesmall-close { background-position: -32px -208px; }
.ui-icon-squaresmall-plus { background-position: -48px -208px; }
.ui-icon-squaresmall-minus { background-position: -64px -208px; }
.ui-icon-squaresmall-close { background-position: -80px -208px; }
.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
.ui-icon-grip-solid-vertical { background-position: -32px -224px; }
.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
.ui-icon-grip-diagonal-se { background-position: -80px -224px; }
/* Misc visuals
----------------------------------*/
/* Corner radius */
.ui-corner-tl { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; border-top-left-radius: 4px; }
.ui-corner-tr { -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; border-top-right-radius: 4px; }
.ui-corner-bl { -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; border-bottom-left-radius: 4px; }
.ui-corner-br { -moz-border-radius-bottomright: 4px; -webkit-border-bottom-right-radius: 4px; border-bottom-right-radius: 4px; }
.ui-corner-top { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; border-top-left-radius: 4px; -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; border-top-right-radius: 4px; }
.ui-corner-bottom { -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; border-bottom-left-radius: 4px; -moz-border-radius-bottomright: 4px; -webkit-border-bottom-right-radius: 4px; border-bottom-right-radius: 4px; }
.ui-corner-right { -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; border-top-right-radius: 4px; -moz-border-radius-bottomright: 4px; -webkit-border-bottom-right-radius: 4px; border-bottom-right-radius: 4px; }
.ui-corner-left { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; border-top-left-radius: 4px; -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; border-bottom-left-radius: 4px; }
.ui-corner-all { -moz-border-radius: 4px; -webkit-border-radius: 4px; border-radius: 4px; }
/* Overlays */
.ui-widget-overlay { background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x; opacity: .30;filter:Alpha(Opacity=30); }
.ui-widget-shadow { margin: -8px 0 0 -8px; padding: 8px; background: #aaaaaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x; opacity: .30;filter:Alpha(Opacity=30); -moz-border-radius: 8px; -webkit-border-radius: 8px; border-radius: 8px; }/* Resizable
----------------------------------*/
.ui-resizable { position: relative;}
.ui-resizable-handle { position: absolute;font-size: 0.1px;z-index: 99999; display: block;}
.ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { display: none; }
.ui-resizable-n { cursor: n-resize; height: 7px; width: 100%; top: -5px; left: 0; }
.ui-resizable-s { cursor: s-resize; height: 7px; width: 100%; bottom: -5px; left: 0; }
.ui-resizable-e { cursor: e-resize; width: 7px; right: -5px; top: 0; height: 100%; }
.ui-resizable-w { cursor: w-resize; width: 7px; left: -5px; top: 0; height: 100%; }
.ui-resizable-se { cursor: se-resize; width: 12px; height: 12px; right: 1px; bottom: 1px; }
.ui-resizable-sw { cursor: sw-resize; width: 9px; height: 9px; left: -5px; bottom: -5px; }
.ui-resizable-nw { cursor: nw-resize; width: 9px; height: 9px; left: -5px; top: -5px; }
.ui-resizable-ne { cursor: ne-resize; width: 9px; height: 9px; right: -5px; top: -5px;}/* Selectable
----------------------------------*/
.ui-selectable-helper { border:1px dotted black }
/* Accordion
----------------------------------*/
.ui-accordion .ui-accordion-header { cursor: pointer; position: relative; margin-top: 1px; zoom: 1; }
.ui-accordion .ui-accordion-li-fix { display: inline; }
.ui-accordion .ui-accordion-header-active { border-bottom: 0 !important; }
.ui-accordion .ui-accordion-header a { display: block; font-size: 1em; padding: .5em .5em .5em .7em; }
/* IE7-/Win - Fix extra vertical space in lists */
.ui-accordion a { zoom: 1; }
.ui-accordion-icons .ui-accordion-header a { padding-left: 2.2em; }
.ui-accordion .ui-accordion-header .ui-icon { position: absolute; left: .5em; top: 50%; margin-top: -8px; }
.ui-accordion .ui-accordion-content { padding: 1em 2.2em; border-top: 0; margin-top: -2px; position: relative; top: 1px; margin-bottom: 2px; overflow: auto; display: none; zoom: 1; }
.ui-accordion .ui-accordion-content-active { display: block; }/* Autocomplete
----------------------------------*/
.ui-autocomplete { position: absolute; cursor: default; }
.ui-autocomplete-loading { background: white url('images/ui-anim_basic_16x16.gif') right center no-repeat; }
/* workarounds */
* html .ui-autocomplete { width:1px; } /* without this, the menu expands to 100% in IE6 */
/* Menu
----------------------------------*/
.ui-menu {
list-style:none;
padding: 2px;
margin: 0;
display:block;
}
.ui-menu .ui-menu {
margin-top: -3px;
}
.ui-menu .ui-menu-item {
margin:0;
padding: 0;
zoom: 1;
float: left;
clear: left;
width: 100%;
}
.ui-menu .ui-menu-item a {
text-decoration:none;
display:block;
padding:.2em .4em;
line-height:1.5;
zoom:1;
}
.ui-menu .ui-menu-item a.ui-state-hover,
.ui-menu .ui-menu-item a.ui-state-active {
font-weight: normal;
margin: -1px;
}
/* Button
----------------------------------*/
.ui-button { display: inline-block; position: relative; padding: 0; margin-right: .1em; text-decoration: none !important; cursor: pointer; text-align: center; zoom: 1; overflow: visible; } /* the overflow property removes extra width in IE */
.ui-button-icon-only { width: 2.2em; } /* to make room for the icon, a width needs to be set here */
button.ui-button-icon-only { width: 2.4em; } /* button elements seem to need a little more width */
.ui-button-icons-only { width: 3.4em; }
button.ui-button-icons-only { width: 3.7em; }
/*button text element */
.ui-button .ui-button-text { display: block; line-height: 1.4; }
.ui-button-text-only .ui-button-text { padding: .4em 1em; }
.ui-button-icon-only .ui-button-text, .ui-button-icons-only .ui-button-text { padding: .4em; text-indent: -9999999px; }
.ui-button-text-icon .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 1em .4em 2.1em; }
.ui-button-text-icons .ui-button-text { padding-left: 2.1em; padding-right: 2.1em; }
/* no icon support for input elements, provide padding by default */
input.ui-button { padding: .4em 1em; }
/*button icon element(s) */
.ui-button-icon-only .ui-icon, .ui-button-text-icon .ui-icon, .ui-button-text-icons .ui-icon, .ui-button-icons-only .ui-icon { position: absolute; top: 50%; margin-top: -8px; }
.ui-button-icon-only .ui-icon { left: 50%; margin-left: -8px; }
.ui-button-text-icon .ui-button-icon-primary, .ui-button-text-icons .ui-button-icon-primary, .ui-button-icons-only .ui-button-icon-primary { left: .5em; }
.ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; }
/*button sets*/
.ui-buttonset { margin-right: 7px; }
.ui-buttonset .ui-button { margin-left: 0; margin-right: -.3em; }
/* workarounds */
button.ui-button::-moz-focus-inner { border: 0; padding: 0; } /* reset extra padding in Firefox */
/* Dialog
----------------------------------*/
.ui-dialog { position: absolute; padding: .2em; width: 300px; overflow: hidden; }
.ui-dialog .ui-dialog-titlebar { padding: .5em 1em .3em; position: relative; }
.ui-dialog .ui-dialog-title { float: left; margin: .1em 16px .2em 0; }
.ui-dialog .ui-dialog-titlebar-close { position: absolute; right: .3em; top: 50%; width: 19px; margin: -10px 0 0 0; padding: 1px; height: 18px; }
.ui-dialog .ui-dialog-titlebar-close span { display: block; margin: 1px; }
.ui-dialog .ui-dialog-titlebar-close:hover, .ui-dialog .ui-dialog-titlebar-close:focus { padding: 0; }
.ui-dialog .ui-dialog-content { border: 0; padding: .5em 1em; background: none; overflow: auto; zoom: 1; }
.ui-dialog .ui-dialog-buttonpane { text-align: left; border-width: 1px 0 0 0; background-image: none; margin: .5em 0 0 0; padding: .3em 1em .5em .4em; }
.ui-dialog .ui-dialog-buttonpane button { float: right; margin: .5em .4em .5em 0; cursor: pointer; padding: .2em .6em .3em .6em; line-height: 1.4em; width:auto; overflow:visible; }
.ui-dialog .ui-resizable-se { width: 14px; height: 14px; right: 3px; bottom: 3px; }
.ui-draggable .ui-dialog-titlebar { cursor: move; }
/* Slider
----------------------------------*/
.ui-slider { position: relative; text-align: left; }
.ui-slider .ui-slider-handle { position: absolute; z-index: 2; width: 1.2em; height: 1.2em; cursor: default; }
.ui-slider .ui-slider-range { position: absolute; z-index: 1; font-size: .7em; display: block; border: 0; background-position: 0 0; }
.ui-slider-horizontal { height: .8em; }
.ui-slider-horizontal .ui-slider-handle { top: -.3em; margin-left: -.6em; }
.ui-slider-horizontal .ui-slider-range { top: 0; height: 100%; }
.ui-slider-horizontal .ui-slider-range-min { left: 0; }
.ui-slider-horizontal .ui-slider-range-max { right: 0; }
.ui-slider-vertical { width: .8em; height: 100px; }
.ui-slider-vertical .ui-slider-handle { left: -.3em; margin-left: 0; margin-bottom: -.6em; }
.ui-slider-vertical .ui-slider-range { left: 0; width: 100%; }
.ui-slider-vertical .ui-slider-range-min { bottom: 0; }
.ui-slider-vertical .ui-slider-range-max { top: 0; }/* Tabs
----------------------------------*/
.ui-tabs { position: relative; padding: .2em; zoom: 1; } /* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */
.ui-tabs .ui-tabs-nav { margin: 0; padding: .2em .2em 0; }
.ui-tabs .ui-tabs-nav li { list-style: none; float: left; position: relative; top: 1px; margin: 0 .2em 1px 0; border-bottom: 0 !important; padding: 0; white-space: nowrap; }
.ui-tabs .ui-tabs-nav li a { float: left; padding: .5em 1em; text-decoration: none; }
.ui-tabs .ui-tabs-nav li.ui-tabs-selected { margin-bottom: 0; padding-bottom: 1px; }
.ui-tabs .ui-tabs-nav li.ui-tabs-selected a, .ui-tabs .ui-tabs-nav li.ui-state-disabled a, .ui-tabs .ui-tabs-nav li.ui-state-processing a { cursor: text; }
.ui-tabs .ui-tabs-nav li a, .ui-tabs.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-selected a { cursor: pointer; } /* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */
.ui-tabs .ui-tabs-panel { display: block; border-width: 0; padding: 1em 1.4em; background: none; }
.ui-tabs .ui-tabs-hide { display: none !important; }
/* Datepicker
----------------------------------*/
.ui-datepicker { width: 17em; padding: .2em .2em 0; }
.ui-datepicker .ui-datepicker-header { position:relative; padding:.2em 0; }
.ui-datepicker .ui-datepicker-prev, .ui-datepicker .ui-datepicker-next { position:absolute; top: 2px; width: 1.8em; height: 1.8em; }
.ui-datepicker .ui-datepicker-prev-hover, .ui-datepicker .ui-datepicker-next-hover { top: 1px; }
.ui-datepicker .ui-datepicker-prev { left:2px; }
.ui-datepicker .ui-datepicker-next { right:2px; }
.ui-datepicker .ui-datepicker-prev-hover { left:1px; }
.ui-datepicker .ui-datepicker-next-hover { right:1px; }
.ui-datepicker .ui-datepicker-prev span, .ui-datepicker .ui-datepicker-next span { display: block; position: absolute; left: 50%; margin-left: -8px; top: 50%; margin-top: -8px; }
.ui-datepicker .ui-datepicker-title { margin: 0 2.3em; line-height: 1.8em; text-align: center; }
.ui-datepicker .ui-datepicker-title select { font-size:1em; margin:1px 0; }
.ui-datepicker select.ui-datepicker-month-year {width: 100%;}
.ui-datepicker select.ui-datepicker-month,
.ui-datepicker select.ui-datepicker-year { width: 49%;}
.ui-datepicker table {width: 100%; font-size: .9em; border-collapse: collapse; margin:0 0 .4em; }
.ui-datepicker th { padding: .7em .3em; text-align: center; font-weight: bold; border: 0; }
.ui-datepicker td { border: 0; padding: 1px; }
.ui-datepicker td span, .ui-datepicker td a { display: block; padding: .2em; text-align: right; text-decoration: none; }
.ui-datepicker .ui-datepicker-buttonpane { background-image: none; margin: .7em 0 0 0; padding:0 .2em; border-left: 0; border-right: 0; border-bottom: 0; }
.ui-datepicker .ui-datepicker-buttonpane button { float: right; margin: .5em .2em .4em; cursor: pointer; padding: .2em .6em .3em .6em; width:auto; overflow:visible; }
.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { float:left; }
/* with multiple calendars */
.ui-datepicker.ui-datepicker-multi { width:auto; }
.ui-datepicker-multi .ui-datepicker-group { float:left; }
.ui-datepicker-multi .ui-datepicker-group table { width:95%; margin:0 auto .4em; }
.ui-datepicker-multi-2 .ui-datepicker-group { width:50%; }
.ui-datepicker-multi-3 .ui-datepicker-group { width:33.3%; }
.ui-datepicker-multi-4 .ui-datepicker-group { width:25%; }
.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header { border-left-width:0; }
.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { border-left-width:0; }
.ui-datepicker-multi .ui-datepicker-buttonpane { clear:left; }
.ui-datepicker-row-break { clear:both; width:100%; }
/* RTL support */
.ui-datepicker-rtl { direction: rtl; }
.ui-datepicker-rtl .ui-datepicker-prev { right: 2px; left: auto; }
.ui-datepicker-rtl .ui-datepicker-next { left: 2px; right: auto; }
.ui-datepicker-rtl .ui-datepicker-prev:hover { right: 1px; left: auto; }
.ui-datepicker-rtl .ui-datepicker-next:hover { left: 1px; right: auto; }
.ui-datepicker-rtl .ui-datepicker-buttonpane { clear:right; }
.ui-datepicker-rtl .ui-datepicker-buttonpane button { float: left; }
.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current { float:right; }
.ui-datepicker-rtl .ui-datepicker-group { float:right; }
.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header { border-right-width:0; border-left-width:1px; }
.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { border-right-width:0; border-left-width:1px; }
/* IE6 IFRAME FIX (taken from datepicker 1.5.3 */
.ui-datepicker-cover {
display: none; /*sorry for IE5*/
display/**/: block; /*sorry for IE5*/
position: absolute; /*must have*/
z-index: -1; /*must have*/
filter: mask(); /*must have*/
top: -4px; /*must have*/
left: -4px; /*must have*/
width: 200px; /*must have*/
height: 200px; /*must have*/
}/* Progressbar
----------------------------------*/
.ui-progressbar { height:2em; text-align: left; }
.ui-progressbar .ui-progressbar-value {margin: -1px; height:100%; }

View file

Before

Width:  |  Height:  |  Size: 641 B

After

Width:  |  Height:  |  Size: 641 B

View file

Before

Width:  |  Height:  |  Size: 533 B

After

Width:  |  Height:  |  Size: 533 B

View file

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

View file

Before

Width:  |  Height:  |  Size: 158 B

After

Width:  |  Height:  |  Size: 158 B

View file

Before

Width:  |  Height:  |  Size: 169 B

After

Width:  |  Height:  |  Size: 169 B

View file

@ -0,0 +1,392 @@
/**
* Ajax Autocomplete for jQuery, version 1.1.3
* (c) 2010 Tomas Kirda
*
* Ajax Autocomplete for jQuery is freely distributable under the terms of an MIT-style license.
* For details, see the web site: http://www.devbridge.com/projects/autocomplete/jquery/
*
* Last Review: 04/19/2010
*/
/*jslint onevar: true, evil: true, nomen: true, eqeqeq: true, bitwise: true, regexp: true, newcap: true, immed: true */
/*global window: true, document: true, clearInterval: true, setInterval: true, jQuery: true */
(function($) {
var reEscape = new RegExp('(\\' + ['/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\'].join('|\\') + ')', 'g');
function fnFormatResult(value, data, currentValue) {
var pattern = '(' + currentValue.replace(reEscape, '\\$1') + ')';
return value.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
}
function Autocomplete(el, options) {
this.el = $(el);
this.el.attr('autocomplete', 'off');
this.suggestions = [];
this.data = [];
this.badQueries = [];
this.selectedIndex = -1;
this.currentValue = this.el.val();
this.intervalId = 0;
this.cachedResponse = [];
this.onChangeInterval = null;
this.ignoreValueChange = false;
this.serviceUrl = options.serviceUrl;
this.isLocal = false;
this.options = {
autoSubmit: false,
minChars: 1,
maxHeight: 300,
deferRequestBy: 0,
width: 0,
highlight: true,
params: {},
fnFormatResult: fnFormatResult,
delimiter: null,
zIndex: 9999
};
this.initialize();
this.setOptions(options);
}
$.fn.autocomplete = function(options) {
return new Autocomplete(this.get(0)||$('<input />'), options);
};
Autocomplete.prototype = {
killerFn: null,
initialize: function() {
var me, uid, autocompleteElId;
me = this;
uid = Math.floor(Math.random()*0x100000).toString(16);
autocompleteElId = 'Autocomplete_' + uid;
this.killerFn = function(e) {
if ($(e.target).parents('.autocomplete').size() === 0) {
me.killSuggestions();
me.disableKillerFn();
}
};
if (!this.options.width) { this.options.width = this.el.width(); }
this.mainContainerId = 'AutocompleteContainter_' + uid;
$('<div id="' + this.mainContainerId + '" style="position:absolute;z-index:9999;"><div class="autocomplete-w1"><div class="autocomplete" id="' + autocompleteElId + '" style="display:none; width:300px;"></div></div></div>').appendTo('body');
this.container = $('#' + autocompleteElId);
this.fixPosition();
if (window.opera) {
this.el.keypress(function(e) { me.onKeyPress(e); });
} else {
this.el.keydown(function(e) { me.onKeyPress(e); });
}
this.el.keyup(function(e) { me.onKeyUp(e); });
this.el.blur(function() { me.enableKillerFn(); });
this.el.focus(function() { me.fixPosition(); });
},
setOptions: function(options){
var o = this.options;
$.extend(o, options);
if(o.lookup){
this.isLocal = true;
if($.isArray(o.lookup)){ o.lookup = { suggestions:o.lookup, data:[] }; }
}
$('#'+this.mainContainerId).css({ zIndex:o.zIndex });
this.container.css({ maxHeight: o.maxHeight + 'px', width:o.width });
},
clearCache: function(){
this.cachedResponse = [];
this.badQueries = [];
},
disable: function(){
this.disabled = true;
},
enable: function(){
this.disabled = false;
},
fixPosition: function() {
var offset = this.el.offset();
$('#' + this.mainContainerId).css({ top: (offset.top + this.el.innerHeight()) + 'px', left: offset.left + 'px' });
},
enableKillerFn: function() {
var me = this;
$(document).bind('click', me.killerFn);
},
disableKillerFn: function() {
var me = this;
$(document).unbind('click', me.killerFn);
},
killSuggestions: function() {
var me = this;
this.stopKillSuggestions();
this.intervalId = window.setInterval(function() { me.hide(); me.stopKillSuggestions(); }, 300);
},
stopKillSuggestions: function() {
window.clearInterval(this.intervalId);
},
onKeyPress: function(e) {
if (this.disabled || !this.enabled) { return; }
// return will exit the function
// and event will not be prevented
switch (e.keyCode) {
case 27: //KEY_ESC:
this.el.val(this.currentValue);
this.hide();
break;
case 9: //KEY_TAB:
case 13: //KEY_RETURN:
if (this.selectedIndex === -1) {
this.hide();
return;
}
this.select(this.selectedIndex);
if(e.keyCode === 9){ return; }
break;
case 38: //KEY_UP:
this.moveUp();
break;
case 40: //KEY_DOWN:
this.moveDown();
break;
default:
return;
}
e.stopImmediatePropagation();
e.preventDefault();
},
onKeyUp: function(e) {
if(this.disabled){ return; }
switch (e.keyCode) {
case 38: //KEY_UP:
case 40: //KEY_DOWN:
return;
}
clearInterval(this.onChangeInterval);
if (this.currentValue !== this.el.val()) {
if (this.options.deferRequestBy > 0) {
// Defer lookup in case when value changes very quickly:
var me = this;
this.onChangeInterval = setInterval(function() { me.onValueChange(); }, this.options.deferRequestBy);
} else {
this.onValueChange();
}
}
},
onValueChange: function() {
clearInterval(this.onChangeInterval);
this.currentValue = this.el.val();
var q = this.getQuery(this.currentValue);
this.selectedIndex = -1;
if (this.ignoreValueChange) {
this.ignoreValueChange = false;
return;
}
if (q === '' || q.length < this.options.minChars) {
this.hide();
} else {
this.getSuggestions(q);
}
},
getQuery: function(val) {
var d, arr;
d = this.options.delimiter;
if (!d) { return $.trim(val); }
arr = val.split(d);
return $.trim(arr[arr.length - 1]);
},
getSuggestionsLocal: function(q) {
var ret, arr, len, val, i;
arr = this.options.lookup;
len = arr.suggestions.length;
ret = { suggestions:[], data:[] };
q = q.toLowerCase();
for(i=0; i< len; i++){
val = arr.suggestions[i];
if(val.toLowerCase().indexOf(q) === 0){
ret.suggestions.push(val);
ret.data.push(arr.data[i]);
}
}
return ret;
},
getSuggestions: function(q) {
var cr, me;
cr = this.isLocal ? this.getSuggestionsLocal(q) : this.cachedResponse[q];
if (cr && $.isArray(cr.suggestions)) {
this.suggestions = cr.suggestions;
this.data = cr.data;
this.suggest();
} else if (!this.isBadQuery(q)) {
me = this;
me.options.params.query = q;
$.get(this.serviceUrl, me.options.params, function(txt) { me.processResponse(txt); }, 'text');
}
},
isBadQuery: function(q) {
var i = this.badQueries.length;
while (i--) {
if (q.indexOf(this.badQueries[i]) === 0) { return true; }
}
return false;
},
hide: function() {
this.enabled = false;
this.selectedIndex = -1;
this.container.hide();
},
suggest: function() {
if (this.suggestions.length === 0) {
this.hide();
return;
}
var me, len, div, f, v, i, s, mOver, mClick;
me = this;
len = this.suggestions.length;
f = this.options.fnFormatResult;
v = this.getQuery(this.currentValue);
mOver = function(xi) { return function() { me.activate(xi); }; };
mClick = function(xi) { return function() { me.select(xi); }; };
this.container.hide().empty();
for (i = 0; i < len; i++) {
s = this.suggestions[i];
div = $((me.selectedIndex === i ? '<div class="selected"' : '<div') + ' title="' + s + '">' + f(s, this.data[i], v) + '</div>');
div.mouseover(mOver(i));
div.click(mClick(i));
this.container.append(div);
}
this.enabled = true;
this.container.show();
if (len) {
this.adjustScroll(0);
}
},
processResponse: function(text) {
var response;
try {
response = eval('(' + text + ')');
} catch (err) { return; }
if (!$.isArray(response.data)) { response.data = []; }
if(!this.options.noCache){
this.cachedResponse[response.query] = response;
if (response.suggestions.length === 0) { this.badQueries.push(response.query); }
}
if (response.query === this.getQuery(this.currentValue)) {
this.suggestions = response.suggestions;
this.data = response.data;
this.suggest();
}
},
activate: function(index) {
var divs, activeItem;
divs = this.container.children();
// Clear previous selection:
if (this.selectedIndex !== -1 && divs.length > this.selectedIndex) {
$(divs.get(this.selectedIndex)).removeClass();
}
this.selectedIndex = index;
if (this.selectedIndex !== -1 && divs.length > this.selectedIndex) {
activeItem = divs.get(this.selectedIndex);
$(activeItem).addClass('selected');
}
return activeItem;
},
deactivate: function(div, index) {
div.className = '';
if (this.selectedIndex === index) { this.selectedIndex = -1; }
},
select: function(i) {
var selectedValue, f;
selectedValue = this.suggestions[i];
if (selectedValue) {
this.el.val(selectedValue);
if (this.options.autoSubmit) {
f = this.el.parents('form');
if (f.length > 0) { f.get(0).submit(); }
}
this.ignoreValueChange = true;
this.hide();
this.onSelect(i);
}
},
moveUp: function() {
if (this.selectedIndex === -1) { return; }
if (this.selectedIndex === 0) {
this.container.children().get(0).className = '';
this.selectedIndex = -1;
this.el.val(this.currentValue);
return;
}
this.adjustScroll(this.selectedIndex - 1);
},
moveDown: function() {
if (this.selectedIndex === (this.suggestions.length - 1)) { return; }
this.adjustScroll(this.selectedIndex + 1);
},
adjustScroll: function(i) {
var activeItem, offsetTop, upperBound, lowerBound;
activeItem = this.activate(i);
offsetTop = activeItem.offsetTop;
upperBound = this.container.scrollTop();
lowerBound = upperBound + this.options.maxHeight - 25;
if (offsetTop < upperBound) {
this.container.scrollTop(offsetTop);
} else if (offsetTop > lowerBound) {
this.container.scrollTop(offsetTop - this.options.maxHeight + 25);
}
},
onSelect: function(i) {
var me, fn, s, d;
me = this;
fn = me.options.onSelect;
s = me.suggestions[i];
d = me.data[i];
me.el.val(me.getValue(s));
if ($.isFunction(fn)) { fn(s, d, me.el); }
},
getValue: function(value){
var del, currVal, arr, me;
me = this;
del = me.options.delimiter;
if (!del) { return value; }
currVal = me.currentValue;
arr = currVal.split(del);
if (arr.length === 1) { return value; }
return currVal.substr(0, currVal.length - arr[arr.length - 1].length) + value;
}
};
}(jQuery));

4
rattail/pyramid/static/js/jquery.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,13 @@
;(function($){var L=$.loading=function(show,opts){return $('body').loading(show,opts,true);};$.fn.loading=function(show,opts,page){opts=toOpts(show,opts);var base=page?$.extend(true,{},L,L.pageOptions):L;return this.each(function(){var $el=$(this),l=$.extend(true,{},base,$.metadata?$el.metadata():null,opts);if(typeof l.onAjax=="boolean"){L.setAjax.call($el,l);}else{L.toggle.call($el,l);}});};var fixed={position:$.browser.msie?'absolute':'fixed'};$.extend(L,{version:"1.6.4",align:'top-left',pulse:'working error',mask:false,img:null,element:null,text:'Loading...',onAjax:undefined,delay:0,max:0,classname:'loading',imgClass:'loading-img',elementClass:'loading-element',maskClass:'loading-mask',css:{position:'absolute',whiteSpace:'nowrap',zIndex:1001},maskCss:{position:'absolute',opacity:.15,background:'#333',zIndex:101,display:'block',cursor:'wait'},cloneEvents:true,pageOptions:{page:true,align:'top-center',css:fixed,maskCss:fixed},html:'<div></div>',maskHtml:'<div></div>',maskedClass:'loading-masked',maskEvents:'mousedown mouseup keydown keypress',resizeEvents:'resize',working:{time:10000,text:'Still working...',run:function(l){var w=l.working,self=this;w.timeout=setTimeout(function(){self.height('auto').width('auto').text(l.text=w.text);l.place.call(self,l);},w.time);}},error:{time:100000,text:'Task may have failed...',classname:'loading-error',run:function(l){var e=l.error,self=this;e.timeout=setTimeout(function(){self.height('auto').width('auto').text(l.text=e.text).addClass(e.classname);l.place.call(self,l);},e.time);}},fade:{time:800,speed:'slow',run:function(l){var f=l.fade,s=f.speed,self=this;f.interval=setInterval(function(){self.fadeOut(s).fadeIn(s);},f.time);}},ellipsis:{time:300,run:function(l){var e=l.ellipsis,self=this;e.interval=setInterval(function(){var et=self.text(),t=l.text,i=dotIndex(t);self.text((et.length-i)<3?et+'.':t.substring(0,i));},e.time);function dotIndex(t){var x=t.indexOf('.');return x<0?t.length:x;}}},type:{time:100,run:function(l){var t=l.type,self=this;t.interval=setInterval(function(){var e=self.text(),el=e.length,txt=l.text;self.text(el==txt.length?txt.charAt(0):txt.substring(0,el+1));},t.time);}},toggle:function(l){var old=this.data('loading');if(old){if(l.show!==true)old.off.call(this,old,l);}else{if(l.show!==false)l.on.call(this,l);}},setAjax:function(l){if(l.onAjax){var self=this,count=0,A=l.ajax={start:function(){if(!count++)l.on.call(self,l);},stop:function(){if(!--count)l.off.call(self,l,l);}};this.bind('ajaxStart.loading',A.start).bind('ajaxStop.loading',A.stop);}else{this.unbind('ajaxStart.loading ajaxStop.loading');}},on:function(l,force){var p=l.parent=this.data('loading',l);if(l.max)l.maxout=setTimeout(function(){l.off.call(p,l,l);},l.max);if(l.delay&&!force){return l.timeout=setTimeout(function(){delete l.timeout;l.on.call(p,l,true);},l.delay);}
if(l.mask)l.mask=l.createMask.call(p,l);l.display=l.create.call(p,l);if(l.img){l.initImg.call(p,l);}else if(l.element){l.initElement.call(p,l);}else{l.init.call(p,l);}
p.trigger('loadingStart',[l]);},initImg:function(l){var self=this;l.imgElement=$('<img src="'+l.img+'"/>').bind('load',function(){l.init.call(self,l);});l.display.addClass(l.imgClass).append(l.imgElement);},initElement:function(l){l.element=$(l.element).clone(l.cloneEvents).show();l.display.addClass(l.elementClass).append(l.element);l.init.call(this,l);},init:function(l){l.place.call(l.display,l);if(l.pulse)l.initPulse.call(this,l);},initPulse:function(l){$.each(l.pulse.split(' '),function(){l[this].run.call(l.display,l);});},create:function(l){var el=$(l.html).addClass(l.classname).css(l.css).appendTo(this);if(l.text&&!l.img&&!l.element)el.text(l.originalText=l.text);$(window).bind(l.resizeEvents,l.resizer=function(){l.resize(l);});return el;},resize:function(l){l.parent.box=null;if(l.mask)l.mask.hide();l.place.call(l.display.hide(),l);if(l.mask)l.mask.show().css(l.parent.box);},createMask:function(l){var box=l.measure.call(this.addClass(l.maskedClass),l);l.handler=function(e){return l.maskHandler(e,l);};$(document).bind(l.maskEvents,l.handler);return $(l.maskHtml).addClass(l.maskClass).css(box).css(l.maskCss).appendTo(this);},maskHandler:function(e,l){var $els=$(e.target).parents().andSelf();if($els.filter('.'+l.classname).length!=0)return true;return!l.page&&$els.filter('.'+l.maskedClass).length==0;},place:function(l){var box=l.align,v='top',h='left';if(typeof box=="object"){box=$.extend(l.calc.call(this,v,h,l),box);}else{if(box!='top-left'){var s=box.split('-');if(s.length==1){v=h=s[0];}else{v=s[0];h=s[1];}}
if(!this.hasClass(v))this.addClass(v);if(!this.hasClass(h))this.addClass(h);box=l.calc.call(this,v,h,l);}
this.show().css(l.box=box);},calc:function(v,h,l){var box=$.extend({},l.measure.call(l.parent,l)),H=$.boxModel?this.height():this.innerHeight(),W=$.boxModel?this.width():this.innerWidth();if(v!='top'){var d=box.height-H;if(v=='center'){d/=2;}else if(v!='bottom'){d=0;}else if($.boxModel){d-=css(this,'paddingTop')+css(this,'paddingBottom');}
box.top+=d;}
if(h!='left'){var d=box.width-W;if(h=='center'){d/=2;}else if(h!='right'){d=0;}else if($.boxModel){d-=css(this,'paddingLeft')+css(this,'paddingRight');}
box.left+=d;}
box.height=H;box.width=W;return box;},measure:function(l){return this.box||(this.box=l.page?l.pageBox(l):l.elementBox(this,l));},elementBox:function(e,l){if(e.css('position')=='absolute'){var box={top:0,left:0};}else{var box=e.position();box.top+=css(e,'marginTop');box.left+=css(e,'marginLeft');}
box.height=e.outerHeight();box.width=e.outerWidth();return box;},pageBox:function(l){var full=$.boxModel&&l.css.position!='fixed';return{top:0,left:0,height:get(full,'Height'),width:get(full,'Width')};function get(full,side){var doc=document;if(full){var s=side.toLowerCase(),d=$(doc)[s](),w=$(window)[s]();return d-css($(doc.body),'marginTop')>w?d:w;}
var c='client'+side;return Math.max(doc.documentElement[c],doc.body[c]);}},off:function(old,l){this.data('loading',null);if(old.maxout)clearTimeout(old.maxout);if(old.timeout)return clearTimeout(old.timeout);if(old.pulse)old.stopPulse.call(this,old,l);if(old.originalText)old.text=old.originalText;if(old.mask)old.stopMask.call(this,old,l);$(window).unbind(old.resizeEvents,old.resizer);if(old.display)old.display.remove();if(old.parent)old.parent.trigger('loadingEnd',[old]);},stopPulse:function(old,l){$.each(old.pulse.split(' '),function(){var p=old[this];if(p.end)p.end.call(l.display,old,l);if(p.interval)clearInterval(p.interval);if(p.timeout)clearTimeout(p.timeout);});},stopMask:function(old,l){this.removeClass(l.maskedClass);$(document).unbind(old.maskEvents,old.handler);old.mask.remove();}});function toOpts(s,l){if(l===undefined){l=(typeof s=="boolean")?{show:s}:s;}else{l.show=s;}
if(l&&(l.img||l.element)&&!l.pulse)l.pulse=false;if(l&&l.onAjax!==undefined&&l.show===undefined)l.show=false;return l;}
function css(el,prop){var val=el.css(prop);return val=='auto'?0:parseFloat(val,10);}})(jQuery);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,380 @@
/************************************************************
*
* rattail.js
*
* This library contains all of Javascript functionality
* provided directly by Rattail.
*
* It also attaches some jQuery event handlers for certain
* design patterns.
*
************************************************************/
var filters_to_disable = [];
function disable_button(button, text) {
if (text) {
$(button).html(text + ", please wait...");
}
$(button).attr('disabled', 'disabled');
}
function disable_filter_options() {
for (var i = 0; i <= filters_to_disable.length; ++i) {
var filter = filters_to_disable.pop();
var option = $('#add-filter option[value='+filter+']').attr('disabled', true);
}
}
/*
* get_dialog(id, callback)
*
* Returns a <DIV> element suitable for use as a jQuery dialog.
*
* ``id`` is used to construct a proper ID for the element and allows the
* dialog to be resused if possible.
*
* ``callback``, if specified, should be a callback function for the dialog.
* This function will be called whenever the dialog has been closed
* "successfully" (i.e. data submitted) by the user, and should accept a single
* ``data`` object which is the JSON response returned by the server.
*/
function get_dialog(id, callback) {
var dialog = $('#'+id+'-dialog');
if (! dialog.length) {
dialog = $('<div class="dialog" id="'+id+'-dialog"></div>');
}
if (callback) {
dialog.attr('callback', callback);
}
return dialog;
}
/*
* get_lookup_dialog(id, callback, textcol)
*
* TODO: Document this.
*/
function get_lookup_dialog(id, callback, textcol) {
var dialog = get_dialog('lookup-'+id, callback);
dialog.addClass('lookup');
dialog.attr('textcol', textcol || 0);
return dialog;
}
/*
* get_uuid(obj)
*
* Returns the UUID associated with ``obj``, if any can be found. The object
* itself is checked, as well its most immediate <TR> parent.
*/
function get_uuid(obj) {
obj = $(obj);
if (obj.attr('uuid')) {
return obj.attr('uuid');
}
var tr = obj.parents('tr:first');
if (tr.attr('uuid')) {
return tr.attr('uuid');
}
return undefined;
}
/*
* json_success(data)
*
* Returns a boolean indicating whether ``data`` represents a successful
* response from the server, or not.
*/
function json_success(data) {
return typeof(data) == 'object' && data.ok == 'success';
}
/*
* loading(element)
*
* Used to indicate that data is being retrieved from the server. ``element``
* is typically a <div class="grid"> element, though it can be anything.
*/
function loading(element) {
element.loading(true, {mask: true, text: ''});
}
/*
* Navigates to another page of results within a grid.
*/
function grid_navigate_page(link, url) {
var div = $(link).parents('div.grid:first');
loading(div);
div.load(url);
}
/*
* reload_grid_div(div)
*
* Reloads a grid's contents. ``div``, if provied, is assumed to be an element
* of type <div class="grid">, or else contain such an element.
*/
function reload_grid_div(div) {
if (! div) {
div = $('div.grid');
} else if (! div.hasClass('grid')) {
div = div.find('div.grid');
}
if (! div.length) {
alert('assert: div should have length');
return;
}
loading(div);
div.load(div.attr('url'));
}
$(function() {
$('div.filter label').live('click', function() {
var checkbox = $(this).prev();
if (checkbox.attr('checked')) {
checkbox.attr('checked', false);
return false;
}
checkbox.attr('checked', true);
return true;
});
$('#add-filter').live('change', function() {
var div = $(this).parents('div.filters:first');
var filter = div.find('#filter-'+$(this).val());
filter.find(':first-child').attr('checked', true);
filter.show();
var field = filter.find(':last-child');
field.select();
field.focus();
$(this).find('option:selected').attr('disabled', true);
$(this).val('add a filter');
if ($(this).find('option[disabled=false]').length == 1) {
$(this).hide();
}
div.find('input[type=submit]').show();
div.find('button[type=reset]').show();
});
$('div.filters form').live('submit', function() {
var div = $('div.grid:first');
var data = $(this).serialize() + '&partial=true';
loading(div);
$.post(div.attr('url'), data, function(data) {
div.replaceWith(data);
});
return false;
});
$('div.filters form div.buttons button[type=reset]').click(function() {
var filters = $(this).parents('div.filters:first');
filters.find('div.filter').each(function() {
$(this).find('div.value input').val('');
});
var url = filters.attr('url');
var grid = $('div.grid[url='+url+']');
loading(grid);
var form = filters.find('form');
var data = form.serialize() + '&partial=true';
$.post(url, data, function(data) {
grid.replaceWith(data);
});
return false;
});
$('div.grid table th.sortable a').live('click', function() {
var div = $(this).parents('div.grid:first');
var th = $(this).parents('th:first');
var dir = 'asc';
if (th.hasClass('sorted') && th.hasClass('asc')) {
dir = 'desc';
}
loading(div);
var url = div.attr('url');
url += url.match(/\?/) ? '&' : '?';
url += 'sort=' + th.attr('field') + '&dir=' + dir;
url += '&page=1';
url += '&partial=true';
div.load(url);
return false;
});
$('div.grid.hoverable table tbody tr').live('mouseenter', function() {
$(this).addClass('hovering');
});
$('div.grid.hoverable table tbody tr').live('mouseleave', function() {
$(this).removeClass('hovering');
});
$('div.grid.clickable table tbody tr').live('mouseenter', function() {
$(this).addClass('hovering');
});
$('div.grid.clickable table tbody tr').live('mouseleave', function() {
$(this).removeClass('hovering');
});
$('div.grid.selectable table tbody tr').live('mouseenter', function() {
$(this).addClass('hovering');
});
$('div.grid.selectable table tbody tr').live('mouseleave', function() {
$(this).removeClass('hovering');
});
$('div.grid.checkable table tbody tr').live('mouseenter', function() {
$(this).addClass('hovering');
});
$('div.grid.checkable table tbody tr').live('mouseleave', function() {
$(this).removeClass('hovering');
});
$('div.grid.clickable table tbody td').live('click', function() {
if (! $(this).hasClass('noclick')) {
var row = $(this).parents('tr:first');
var url = row.attr('url');
if (url) {
location.href = url;
}
}
});
$('div.grid table thead th.checkbox input[type=checkbox]').live('click', function() {
var checked = $(this).is(':checked');
var table = $(this).parents('table:first');
table.find('tbody tr').each(function() {
$(this).find('td.checkbox input[type=checkbox]').attr('checked', checked);
// if (checked) {
// $(this).addClass('selected');
// } else {
// $(this).removeClass('selected');
// }
});
});
$('div.grid.selectable table tbody tr').live('click', function() {
var table = $(this).parents('table:first');
if (! table.hasClass('multiple')) {
table.find('tbody tr').removeClass('selected');
}
$(this).addClass('selected');
});
$('div.grid.checkable table tbody tr').live('click', function() {
var checkbox = $(this).find('td:first input[type=checkbox]');
checkbox.attr('checked', !checkbox.is(':checked'));
$(this).toggleClass('selected');
});
$('div.grid table tbody td.edit').live('click', function() {
var url = $(this).attr('url');
if (url) {
location.href = url;
}
});
$('div.grid table tbody td.delete').live('click', function() {
var url = $(this).attr('url');
if (url) {
if (confirm("Do you really wish to delete this object?")) {
location.href = url;
}
}
});
$('#grid-page-count').live('change', function() {
var div = $(this).parents('div.grid:first');
loading(div);
url = div.attr('url');
url += url.match(/\?/) ? '&' : '?';
url += 'per_page=' + $(this).val();
url += '&partial=true';
div.load(url);
});
$('div.autocomplete-container div.autocomplete-display button.autocomplete-change').live('click', function() {
var container = $(this).parents('div.autocomplete-container');
var display = $(this).parents('div.autocomplete-display');
var textbox = container.find('input.autocomplete-textbox');
var hidden = container.find('input[type=hidden]');
display.hide();
hidden.val('');
textbox.show();
textbox.select();
textbox.focus();
});
$('div.dialog form').live('submit', function() {
var form = $(this);
var dialog = form.parents('div.dialog:first');
$.ajax({
type: 'POST',
url: form.attr('action'),
data: form.serialize(),
success: function(data) {
if (json_success(data)) {
if (dialog.attr('callback')) {
eval(dialog.attr('callback'))(data);
}
dialog.dialog('close');
} else if (typeof(data) == 'object') {
alert(data.message);
} else {
dialog.html(data);
}
},
error: function() {
alert("Sorry, something went wrong...try again?");
},
});
return false;
});
$('div.dialog button.close').live('click', function() {
var dialog = $(this).parents('div.dialog:first');
dialog.dialog('close');
});
$('div.dialog button.cancel').live('click', function() {
var dialog = $(this).parents('div.dialog:first');
dialog.dialog('close');
});
$('div.dialog.lookup button.ok').live('click', function() {
var dialog = $(this).parents('div.dialog.lookup:first');
var tr = dialog.find('div.grid table tbody tr.selected');
if (! tr.length) {
alert("You haven't selected anything.");
return false;
}
var uuid = get_uuid(tr);
var col = parseInt(dialog.attr('textcol'));
var text = tr.find('td:eq('+col+')').html();
eval(dialog.attr('callback'))(uuid, text);
dialog.dialog('close');
});
});

View file

@ -0,0 +1,103 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
``rattail.pyramid.subscribers`` -- Event Subscribers
"""
from pyramid import threadlocal
from pyramid.security import authenticated_userid
import rattail
from rattail.pyramid import helpers
from rattail.pyramid import Session
from rattail.db.model import User
from rattail.db.auth import has_permission
def before_render(event):
"""
Adds goodies to the global template renderer context.
"""
request = event.get('request') or threadlocal.get_current_request()
renderer_globals = event
renderer_globals['h'] = helpers
renderer_globals['url'] = request.route_url
renderer_globals['rattail'] = rattail
renderer_globals['Session'] = Session
def context_found(event):
"""
This hook attaches various attributes and methods to the ``request``
object. Specifically:
The :class:`rattail.db.model.User` instance currently logged-in (if indeed
there is one) is attached as ``request.user``.
A ``request.has_perm()`` method is attached, which is a shortcut for
:func:`rattail.db.auth.has_permission()`.
A ``request.get_referrer()`` method is attached, which contains some
convenient logic for determining the referring URL.
"""
request = event.request
request.user = None
uuid = authenticated_userid(request)
if uuid:
request.user = Session.query(User).get(uuid)
def has_perm(perm):
return has_permission(Session(), request.user, perm)
request.has_perm = has_perm
def has_any_perm(perms):
for perm in perms:
if has_permission(Session(), request.user, perm):
return True
return False
request.has_any_perm = has_any_perm
def get_referrer(default=None):
if request.params.get('referrer'):
return request.params['referrer']
if request.session.get('referrer'):
return request.session.pop('referrer')
referrer = request.referrer
if not referrer or referrer == request.current_route_url():
if default:
referrer = default
else:
referrer = request.route_url('home')
return referrer
request.get_referrer = get_referrer
def includeme(config):
config.add_subscriber(before_render, 'pyramid.events.BeforeRender')
config.add_subscriber(context_found, 'pyramid.events.ContextFound')

View file

@ -0,0 +1,29 @@
<%def name="autocomplete(field_name, service_url, field_value=None, field_display=None, width='300px', callback=None)">
<div id="${field_name}-container" class="autocomplete-container">
${h.hidden(field_name, id=field_name, value=field_value)}
${h.text(field_name+'-textbox', id=field_name+'-textbox', value=field_display,
class_='autocomplete-textbox', style='display: none;' if field_value else '')}
<div id="${field_name}-display" class="autocomplete-display"${'' if field_value else ' style="display: none;"'|n}>
<span>${field_display or ''}</span>
<button type="button" id="${field_name}-change" class="autocomplete-change">Change</button>
</div>
</div>
<script language="javascript" type="text/javascript">
$(function() {
var autocompleter_${field_name.replace('-', '_')} = $('#${field_name}-textbox').autocomplete({
serviceUrl: '${service_url}',
width: '${width}',
onSelect: function(value, data) {
$('#${field_name}').val(data);
$('#${field_name}-display span').text(value);
$('#${field_name}-textbox').hide();
$('#${field_name}-display').show();
$('#${field_name}-change').focus();
% if callback:
${callback}(data, value);
% endif
},
});
});
</script>
</%def>

Some files were not shown because too many files have changed in this diff Show more