Compare commits
5 commits
Author | SHA1 | Date | |
---|---|---|---|
088c87e209 | |||
3edef77e81 | |||
b010bc90f5 | |||
8e82e7440a | |||
79e6a46b94 |
9
.gitignore
vendored
|
@ -1,8 +1 @@
|
|||
*~
|
||||
*.pyc
|
||||
.coverage
|
||||
.tox/
|
||||
dist/
|
||||
docs/_build/
|
||||
htmlcov/
|
||||
Tailbone.egg-info/
|
||||
rattail.pyramid.egg-info
|
||||
|
|
621
CHANGELOG.md
|
@ -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
|
@ -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.
|
141
COPYING.txt
|
@ -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>.
|
||||
|
|
20
MANIFEST.in
|
@ -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
|
||||
|
|
|
@ -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
|
@ -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.
|
177
docs/Makefile
|
@ -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."
|
7539
docs/OLDCHANGES.rst
0
docs/_static/.dummy
vendored
|
@ -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
|
|
@ -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
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
``tailbone.db``
|
||||
===============
|
||||
|
||||
.. automodule:: tailbone.db
|
||||
:members:
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
``tailbone.diffs``
|
||||
==================
|
||||
|
||||
.. automodule:: tailbone.diffs
|
||||
:members:
|
|
@ -1,9 +0,0 @@
|
|||
|
||||
``tailbone.forms``
|
||||
==================
|
||||
|
||||
.. automodule:: tailbone.forms
|
||||
:members:
|
||||
|
||||
.. autoclass:: tailbone.forms.Form
|
||||
:members:
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
``tailbone.forms.widgets``
|
||||
==========================
|
||||
|
||||
.. automodule:: tailbone.forms.widgets
|
||||
:members:
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
``tailbone.grids.core``
|
||||
=======================
|
||||
|
||||
.. automodule:: tailbone.grids.core
|
||||
:members:
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
``tailbone.grids``
|
||||
==================
|
||||
|
||||
.. automodule:: tailbone.grids
|
||||
:members:
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
``tailbone.progress``
|
||||
=====================
|
||||
|
||||
.. automodule:: tailbone.progress
|
||||
:members:
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
``tailbone.subscribers``
|
||||
========================
|
||||
|
||||
.. automodule:: tailbone.subscribers
|
||||
:members:
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
``tailbone.util``
|
||||
=================
|
||||
|
||||
.. automodule:: tailbone.util
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
|
||||
``tailbone.views.batch``
|
||||
========================
|
||||
|
||||
.. automodule:: tailbone.views.batch
|
|
@ -1,10 +0,0 @@
|
|||
|
||||
``tailbone.views.batch.vendorcatalog``
|
||||
======================================
|
||||
|
||||
.. automodule:: tailbone.views.batch.vendorcatalog
|
||||
|
||||
.. autoclass:: VendorCatalogsView
|
||||
:members:
|
||||
|
||||
.. autofunction:: includeme
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
``tailbone.views.core``
|
||||
=======================
|
||||
|
||||
.. automodule:: tailbone.views.core
|
||||
:members:
|
|
@ -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
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
``tailbone.views.members``
|
||||
==========================
|
||||
|
||||
.. automodule:: tailbone.views.members
|
||||
:members:
|
|
@ -1,9 +0,0 @@
|
|||
|
||||
``tailbone.views.purchasing.batch``
|
||||
===================================
|
||||
|
||||
.. automodule:: tailbone.views.purchasing.batch
|
||||
|
||||
.. autoclass:: PurchasingBatchView
|
||||
|
||||
.. automethod:: save_edit_row_form
|
|
@ -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
|
|
@ -1,8 +0,0 @@
|
|||
|
||||
Changelog Archive
|
||||
=================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
OLDCHANGES
|
|
@ -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.
|
|
@ -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.
|
|
@ -1,7 +0,0 @@
|
|||
|
||||
Console Commands
|
||||
================
|
||||
|
||||
.. contents:: :local:
|
||||
|
||||
TODO
|
|
@ -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`.
|
52
docs/conf.py
|
@ -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'
|
|
@ -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
|
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 7.6 KiB |
|
@ -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`
|
||||
|
242
docs/make.bat
|
@ -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
|
|
@ -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>
|
|
@ -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',)
|
|
@ -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
|
|
@ -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
|
103
pyproject.toml
|
@ -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
|
@ -0,0 +1 @@
|
|||
__import__('pkg_resources').declare_namespace(__name__)
|
68
rattail/pyramid/__init__.py
Normal 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')
|
1
rattail/pyramid/_version.py
Normal file
|
@ -0,0 +1 @@
|
|||
__version__ = '0.4a1'
|
52
rattail/pyramid/auth.py
Normal 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
|
31
rattail/pyramid/forms/__init__.py
Normal 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 *
|
111
rattail/pyramid/forms/core.py
Normal 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)))
|
32
rattail/pyramid/forms/formalchemy/__init__.py
Normal 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 *
|
108
rattail/pyramid/forms/formalchemy/core.py
Normal 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)
|
75
rattail/pyramid/forms/formalchemy/fields.py
Normal 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)
|
72
rattail/pyramid/forms/formalchemy/fieldset.py
Normal 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)
|
188
rattail/pyramid/forms/formalchemy/renderers.py
Normal 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 ($ %0.2f / %u)' % (
|
||||
price.price, price.multiple,
|
||||
price.pack_price, price.pack_multiple))
|
||||
return literal('$ %0.2f ($ %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 += ' (%s)' % pretty_datetime(price.ends, from_='utc')
|
||||
return res
|
66
rattail/pyramid/forms/simpleform.py
Normal 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)
|
32
rattail/pyramid/grids/__init__.py
Normal 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
|
124
rattail/pyramid/grids/alchemy.py
Normal 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
|
138
rattail/pyramid/grids/core.py
Normal 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
|
274
rattail/pyramid/grids/search.py
Normal 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
|
138
rattail/pyramid/grids/util.py
Normal 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)())
|
33
rattail/pyramid/helpers.py
Normal 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 *
|
78
rattail/pyramid/progress.py
Normal 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
|
|
@ -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"> </td>
|
||||
</tr>
|
||||
% endfor
|
|
@ -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: ${dept.name} (${dept.number})</th>
|
||||
<th class="department" colspan="19">Department: ${dept.name} (${dept.number})</th>
|
||||
</tr>
|
||||
% for subdept in sorted(costs[dept], key=lambda x: x.name):
|
||||
<tr>
|
||||
<th class="subdepartment" colspan="21">Subdepartment: ${subdept.name} (${subdept.number})</th>
|
||||
<th class="subdepartment" colspan="19">Subdepartment: ${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
|
24
rattail/pyramid/static/css/autocomplete.css
Normal 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;
|
||||
}
|
98
rattail/pyramid/static/css/base.css
Normal 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;
|
||||
}
|
24
rattail/pyramid/static/css/filters.css
Normal 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;
|
||||
}
|
97
rattail/pyramid/static/css/forms.css
Normal 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;
|
||||
}
|
178
rattail/pyramid/static/css/grids.css
Normal 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;
|
||||
}
|
75
rattail/pyramid/static/css/layout.css
Normal 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;
|
||||
}
|
29
rattail/pyramid/static/css/login.css
Normal 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;
|
||||
}
|
33
rattail/pyramid/static/css/perms.css
Normal 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;
|
||||
}
|
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 180 B |
After Width: | Height: | Size: 178 B |
After Width: | Height: | Size: 120 B |
After Width: | Height: | Size: 105 B |
After Width: | Height: | Size: 111 B |
After Width: | Height: | Size: 110 B |
After Width: | Height: | Size: 119 B |
After Width: | Height: | Size: 101 B |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 4.3 KiB |
489
rattail/pyramid/static/css/smoothness/jquery-ui-1.8.2.custom.css
vendored
Normal 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%; }
|
Before Width: | Height: | Size: 641 B After Width: | Height: | Size: 641 B |
Before Width: | Height: | Size: 533 B After Width: | Height: | Size: 533 B |
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
Before Width: | Height: | Size: 158 B After Width: | Height: | Size: 158 B |
Before Width: | Height: | Size: 169 B After Width: | Height: | Size: 169 B |
392
rattail/pyramid/static/js/jquery.autocomplete.js
Normal 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
13
rattail/pyramid/static/js/jquery.loading.js
Normal 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);
|
1012
rattail/pyramid/static/js/jquery.ui.js
Normal file
380
rattail/pyramid/static/js/rattail.js
Normal 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');
|
||||
});
|
||||
|
||||
});
|
103
rattail/pyramid/subscribers.py
Normal 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')
|
29
rattail/pyramid/templates/autocomplete.mako
Normal 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>
|