diff --git a/.gitignore b/.gitignore index cda94d0f21..4a9ea0c43e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ /cache/* /compiled/* /databases/* +/docs/build/* /errors/* /indices/* /models/000_config.py diff --git a/README.md b/README.md index 93d06b36a8..44be78ad2a 100644 --- a/README.md +++ b/README.md @@ -8,3 +8,8 @@ Platform. # Who is Eden ASP for? Eden ASP is for application developers and service providers. + +# What about documentation? + +The developer handbook is included in the repository, or you can +read it on [ReadTheDocs](https://eden-asp.readthedocs.io). diff --git a/controllers/admin.py b/controllers/admin.py index b0998e3e66..0e5e9e83a6 100755 --- a/controllers/admin.py +++ b/controllers/admin.py @@ -35,24 +35,11 @@ def role(): def prep(r): if r.representation not in ("html", "aadata", "csv", "json"): return False - - # Configure REST methods - methods = ("read", - "list", - "create", - "update", - "delete", - "users", - "copy", - "datatable", - "datalist", - "import", - ) - r.set_handler(methods, s3base.S3RoleManager) + r.custom_action = s3base.S3RoleManager return True s3.prep = prep - return s3_rest_controller("auth", "group") + return crud_controller("auth", "group") # ----------------------------------------------------------------------------- def user(): @@ -186,19 +173,19 @@ def link_user(r, **args): # Custom Methods set_method = s3db.set_method - set_method("auth", "user", + set_method("auth_user", method = "roles", action = s3base.S3RoleManager) - set_method("auth", "user", + set_method("auth_user", method = "disable", action = disable_user) - set_method("auth", "user", + set_method("auth_user", method = "approve", action = approve_user) - set_method("auth", "user", + set_method("auth_user", method = "link", action = link_user) @@ -460,12 +447,11 @@ def postp(r, output): s3.import_prep = auth.s3_import_prep - output = s3_rest_controller("auth", "user", - csv_stylesheet = ("auth", "user.xsl"), - csv_template = ("auth", "user"), - rheader = rheader, - ) - return output + return crud_controller("auth", "user", + csv_stylesheet = ("auth", "user.xsl"), + csv_template = ("auth", "user"), + rheader = rheader, + ) # ============================================================================= def group(): @@ -498,7 +484,7 @@ def group(): ) s3db.configure(tablename, main="role") - return s3_rest_controller("auth", "group") + return crud_controller("auth", "group") # ----------------------------------------------------------------------------- @auth.s3_requires_membership(1) @@ -525,7 +511,7 @@ def organisation(): msg_list_empty = T("No Organization Domains currently registered") ) - return s3_rest_controller("auth", "organisation") + return crud_controller("auth", "organisation") # ----------------------------------------------------------------------------- def user_create_onvalidation (form): @@ -550,7 +536,7 @@ def audit(): # Represent the user_id column db.s3_audit.user_id.represent = s3db.auth_UserRepresent() - return s3_rest_controller("s3", "audit") + return crud_controller("s3", "audit") # ============================================================================= # Consent Tracking @@ -559,10 +545,10 @@ def audit(): def processing_type(): """ Types of Data Processing: RESTful CRUD Controller """ - return s3_rest_controller("auth", "processing_type", - csv_template = ("auth", "processing_type"), - csv_stylesheet = ("auth", "processing_type.xsl"), - ) + return crud_controller("auth", "processing_type", + csv_template = ("auth", "processing_type"), + csv_stylesheet = ("auth", "processing_type.xsl"), + ) # ----------------------------------------------------------------------------- @auth.s3_requires_membership(1) @@ -630,10 +616,10 @@ def postp(r, output): return output s3.postp = postp - return s3_rest_controller("auth", "consent_option", - csv_template = ("auth", "consent_option"), - csv_stylesheet = ("auth", "consent_option.xsl"), - ) + return crud_controller("auth", "consent_option", + csv_template = ("auth", "consent_option"), + csv_stylesheet = ("auth", "consent_option.xsl"), + ) # ----------------------------------------------------------------------------- @auth.s3_requires_membership(1) @@ -734,8 +720,7 @@ def acl(): next = request.vars._next s3db.configure(tablename, delete_next=next) - output = s3_rest_controller("s3", "permission") - return output + return crud_controller("s3", "permission") # ----------------------------------------------------------------------------- def acl_represent(acl, options): @@ -1319,8 +1304,7 @@ def postp(r, output): return output s3.postp = postp - output = s3_rest_controller("translate", "language") - return output + return crud_controller("translate", "language") # ============================================================================= @auth.s3_requires_membership(1) @@ -1352,9 +1336,9 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller("scheduler", "task", - rheader = s3db.s3_scheduler_rheader, - ) + return crud_controller("scheduler", "task", + rheader = s3db.s3_scheduler_rheader, + ) # ============================================================================= def result(): @@ -1424,6 +1408,6 @@ def dashboard(): Dashboard Configurations """ - return s3_rest_controller("s3", "dashboard") + return crud_controller("s3", "dashboard") # END ========================================================================= diff --git a/controllers/assess.py b/controllers/assess.py index 050f678d63..7a155b3a94 100644 --- a/controllers/assess.py +++ b/controllers/assess.py @@ -31,7 +31,7 @@ def ifrc24h(): # Keep UX simple settings.pr.lookup_duplicates = False - return s3_rest_controller("assess", "24h") + return crud_controller("assess", "24h") # ----------------------------------------------------------------------------- def building_marker_fn(record): @@ -105,19 +105,19 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller(rheader = s3db.assess_building_rheader) + return crud_controller(rheader=s3db.assess_building_rheader) # ----------------------------------------------------------------------------- def canvass(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def need(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def needs(): @@ -190,6 +190,6 @@ def needs(): crud_form = crud_form, ) - return s3_rest_controller() + return crud_controller() # END ========================================================================= diff --git a/controllers/assess2.py b/controllers/assess2.py index 67fc937023..1a1d0627b4 100644 --- a/controllers/assess2.py +++ b/controllers/assess2.py @@ -1792,7 +1792,7 @@ def assess_rat_summary(r, **attr): raise HTTP(405, ERROR.BAD_METHOD) - s3db.set_method("assess", "rat", + s3db.set_method("assess_rat", method="summary", action=assess_rat_summary) @@ -1943,8 +1943,7 @@ def population(): """ RESTful controller """ - output = s3_rest_controller() - return output + return crud_controller() # ============================================================================= # Rapid Assessments @@ -2074,8 +2073,8 @@ def postp(r, output): rheader = lambda r: rat_rheader(r, tabs) - output = s3_rest_controller(rheader=rheader, - s3ocr_config={"tabs": tabs}) + output = curd_controller(rheader=rheader, + s3ocr_config={"tabs": tabs}) response.s3.stylesheets.append( "S3/rat.css" ) return output @@ -2192,7 +2191,7 @@ def prep(r): rheader = lambda r: assess_rheader(r, tabs) - return s3_rest_controller(rheader=rheader) + return crud_controller(rheader=rheader) # ----------------------------------------------------------------------------- def impact_type(): @@ -2204,7 +2203,7 @@ def impact_type(): module = "impact" resourcename = "type" - return s3_rest_controller(module, resourcename) + return crud_controller(module, resourcename) # ----------------------------------------------------------------------------- def baseline_type(): @@ -2213,7 +2212,7 @@ def baseline_type(): # Load Models assess_tables() - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def baseline(): @@ -2222,7 +2221,7 @@ def baseline(): # Load Models assess_tables() - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def summary(): @@ -2231,7 +2230,7 @@ def summary(): # Load Models assess_tables() - return s3_rest_controller() + return crud_controller() # ============================================================================= def basic_assess(): @@ -2534,12 +2533,12 @@ def custom_assess(custom_assess_fields, location_id=None): def type(): """ RESTful CRUD controller """ - return s3_rest_controller("impact", "type") + return crud_controller("impact", "type") # ============================================================================= def impact(): """ RESTful CRUD controller """ - return s3_rest_controller("impact", "impact") + return crud_controller("impact", "impact") # END ========================================================================= diff --git a/controllers/asset.py b/controllers/asset.py index 8c9f20e15c..485c874042 100644 --- a/controllers/asset.py +++ b/controllers/asset.py @@ -50,15 +50,15 @@ def asset(): def brand(): """ RESTful CRUD controller """ - return s3_rest_controller("supply", "brand") + return crud_controller("supply", "brand") # ----------------------------------------------------------------------------- def catalog(): """ RESTful CRUD controller """ - return s3_rest_controller("supply", "catalog", - rheader = s3db.supply_catalog_rheader, - ) + return crud_controller("supply", "catalog", + rheader = s3db.supply_catalog_rheader, + ) # ----------------------------------------------------------------------------- def item(): @@ -97,10 +97,10 @@ def catalog_item(): - used for Imports """ - return s3_rest_controller("supply", "catalog_item", - csv_template = ("supply", "catalog_item"), - csv_stylesheet = ("supply", "catalog_item.xsl"), - ) + return crud_controller("supply", "catalog_item", + csv_template = ("supply", "catalog_item"), + csv_stylesheet = ("supply", "catalog_item.xsl"), + ) # ----------------------------------------------------------------------------- def item_category(): @@ -116,7 +116,7 @@ def item_category(): field.readable = field.writable = False field.default = True - return s3_rest_controller("supply", "item_category") + return crud_controller("supply", "item_category") # ----------------------------------------------------------------------------- def supplier(): diff --git a/controllers/br.py b/controllers/br.py index 21c25f7ce4..8fec0bfc5d 100644 --- a/controllers/br.py +++ b/controllers/br.py @@ -36,7 +36,7 @@ def person(): s3db.br_case_default_status() # Set contacts-method for tab - s3db.set_method("pr", "person", + s3db.set_method("pr_person", method = "contacts", action = s3db.pr_Contacts, ) @@ -123,7 +123,7 @@ def prep(r): # Configure Anonymizer from core import S3Anonymize - s3db.set_method("pr", "person", + s3db.set_method("pr_person", method = "anonymize", action = S3Anonymize, ) @@ -319,7 +319,7 @@ def prep(r): resource.configure(filter_widgets = filter_widgets) # Autocomplete search-method - s3db.set_method("pr", "person", + s3db.set_method("pr_person", method = "search_ac", action = s3db.pr_PersonSearchAutocomplete(name_fields), ) @@ -479,10 +479,9 @@ def postp(r, output): return output s3.postp = postp - output = s3_rest_controller("pr", "person", - rheader = s3db.br_rheader, - ) - return output + return crud_controller("pr", "person", + rheader = s3db.br_rheader, + ) # ----------------------------------------------------------------------------- def person_search(): @@ -508,14 +507,14 @@ def prep(r): # Autocomplete search-method including pe_label search_fields = tuple(name_fields) + ("pe_label",) - s3db.set_method("pr", "person", + s3db.set_method("pr_person", method = "search_ac", action = s3db.pr_PersonSearchAutocomplete(search_fields), ) return True s3.prep = prep - return s3_rest_controller("pr", "person") + return crud_controller("pr", "person") # ----------------------------------------------------------------------------- def group_membership(): @@ -539,7 +538,7 @@ def prep(r): if vtablename == "pr_person": # Set contacts-method to retain the tab - s3db.set_method("pr", "person", + s3db.set_method("pr_person", method = "contacts", action = s3db.pr_Contacts, ) @@ -667,9 +666,9 @@ def prep(r): settings.pr.request_home_phone = False settings.hrm.email_required = False - return s3_rest_controller("pr", "group_membership", - rheader = s3db.br_rheader, - ) + return crud_controller("pr", "group_membership", + rheader = s3db.br_rheader, + ) # ----------------------------------------------------------------------------- def document(): @@ -703,7 +702,7 @@ def prep(r): r.unauthorised() # Set contacts-method to retain the tab - s3db.set_method("pr", "person", + s3db.set_method("pr_person", method = "contacts", action = s3db.pr_Contacts, ) @@ -828,9 +827,9 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller("doc", "document", - rheader = s3db.br_rheader, - ) + return crud_controller("doc", "document", + rheader = s3db.br_rheader, + ) # ============================================================================= # Case Activities @@ -1019,7 +1018,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # ============================================================================= def activities(): @@ -1127,7 +1126,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller("br", "case_activity") + return crud_controller("br", "case_activity") # ============================================================================= # Assistance @@ -1135,25 +1134,25 @@ def prep(r): def assistance_status(): """ Assistance Statuses: RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def assistance_theme(): """ Assistance Themes: RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def assistance_type(): """ Types of Assistance: RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def assistance_offer(): """ Offers of Assistance: RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def offers(): @@ -1176,7 +1175,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller("br", "assistance_offer") + return crud_controller("br", "assistance_offer") # ----------------------------------------------------------------------------- def assistance_measure(): @@ -1312,7 +1311,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # ============================================================================= # Look-up Tables @@ -1329,36 +1328,36 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def case_activity_status(): """ Activity Statuses: RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def case_activity_update_type(): """ Activity Update Types: RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def need(): """ Needs: RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def note_type(): """ Note Types: RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def service_contact_type(): """ Service Contact Types: RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # END ========================================================================= diff --git a/controllers/budget.py b/controllers/budget.py index 112e8da370..d7337520b2 100755 --- a/controllers/budget.py +++ b/controllers/budget.py @@ -40,7 +40,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller(rheader = s3db.budget_rheader) + return crud_controller(rheader=s3db.budget_rheader) # ============================================================================= def allocation(): @@ -50,7 +50,7 @@ def allocation(): @status: experimental, not for production use """ - return s3_rest_controller() + return crud_controller() # ============================================================================= def location(): @@ -60,7 +60,7 @@ def location(): # @todo: link to gis_location - return s3_rest_controller(main = "code") + return crud_controller(main="code") # ============================================================================= def item(): @@ -70,7 +70,7 @@ def item(): #s3.formats.pdf = URL(f="item_export_pdf") - return s3_rest_controller() + return crud_controller() # ============================================================================= def kit(): @@ -83,9 +83,9 @@ def kit(): s3db.configure("budget_kit", update_next=URL(f="kit_item", args=request.args[1])) - return s3_rest_controller(main = "code", - rheader = s3db.budget_rheader, - ) + return crud_controller(main = "code", + rheader = s3db.budget_rheader, + ) # ============================================================================= def bundle(): @@ -95,7 +95,7 @@ def bundle(): s3db.configure("budget_bundle", update_next=URL(f="bundle_kit_item", args=request.args[1])) - return s3_rest_controller(rheader = s3db.budget_rheader) + return crud_controller(rheader=s3db.budget_rheader) # ============================================================================= def staff(): @@ -105,7 +105,7 @@ def staff(): # @todo: link to hrm_job_title (?) - return s3_rest_controller() + return crud_controller() # ============================================================================= def budget_staff(): @@ -114,7 +114,7 @@ def budget_staff(): """ s3.prep = lambda r: r.representation == "s3json" - return s3_rest_controller() + return crud_controller() # ============================================================================= def budget_bundle(): @@ -123,7 +123,7 @@ def budget_bundle(): """ s3.prep = lambda r: r.representation == "s3json" - return s3_rest_controller() + return crud_controller() # ============================================================================= def bundle_kit(): @@ -132,7 +132,7 @@ def bundle_kit(): """ s3.prep = lambda r: r.representation == "s3json" - return s3_rest_controller() + return crud_controller() # ============================================================================= def bundle_item(): @@ -141,7 +141,7 @@ def bundle_item(): """ s3.prep = lambda r: r.representation == "s3json" - return s3_rest_controller() + return crud_controller() # ============================================================================= def kit_item(): @@ -150,7 +150,7 @@ def kit_item(): """ s3.prep = lambda r: r.representation == "s3json" - return s3_rest_controller() + return crud_controller() # ============================================================================= def project(): @@ -164,11 +164,9 @@ def project(): ] rheader = lambda r: s3db.project_rheader(r, tabs=tabs) - output = s3_rest_controller("project", "project", - rheader = rheader, - ) - - return output + return crud_controller("project", "project", + rheader = rheader, + ) # ============================================================================= def parameter(): @@ -188,7 +186,7 @@ def postp(r, output): return output s3.postp = postp - r = s3_request(args=[str(record_id)]) + r = crud_request(args=[str(record_id)]) return r() # ============================================================================= diff --git a/controllers/building.py b/controllers/building.py index 2d0f6666ed..506ceee96d 100644 --- a/controllers/building.py +++ b/controllers/building.py @@ -595,8 +595,7 @@ def nzseel1(): rheader = nzseel1_rheader - output = s3_rest_controller(rheader=rheader) - return output + return crud_controller(rheader=rheader) # ----------------------------------------------------------------------------- def nzseel1_rheader(r, tabs=[]): @@ -675,8 +674,7 @@ def nzseel2(): rheader = nzseel2_rheader - output = s3_rest_controller(rheader=rheader) - return output + return crud_controller(rheader=rheader) # ----------------------------------------------------------------------------- def nzseel2_rheader(r, tabs=[]): diff --git a/controllers/cap.py b/controllers/cap.py index be77ed9262..2d605f6a15 100644 --- a/controllers/cap.py +++ b/controllers/cap.py @@ -22,7 +22,7 @@ def alerting_authority(): Alerting Authorities: RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def alert_history(): @@ -30,7 +30,7 @@ def alert_history(): Alert History: RESTful CRUD controller """ - return s3_rest_controller(rheader=s3db.cap_history_rheader) + return crud_controller(rheader=s3db.cap_history_rheader) # ----------------------------------------------------------------------------- def alert_ack(): @@ -38,7 +38,7 @@ def alert_ack(): Alert Acknowledgements: RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def info_prep(r): @@ -1018,10 +1018,9 @@ def postp(r, output): return output s3.postp = postp - output = s3_rest_controller("cap", "alert", - rheader = s3db.cap_rheader, - ) - return output + return crud_controller("cap", "alert", + rheader = s3db.cap_rheader, + ) # ----------------------------------------------------------------------------- def info(): @@ -1085,7 +1084,7 @@ def postp(r, output): return output s3.postp = postp - return s3_rest_controller(rheader=s3db.cap_rheader) + return crud_controller(rheader=s3db.cap_rheader) # ----------------------------------------------------------------------------- def info_parameter(): @@ -1113,7 +1112,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def template(): @@ -1297,7 +1296,7 @@ def postp(r, output): return output s3.postp = postp - return s3_rest_controller("cap", "alert", rheader=s3db.cap_rheader) + return crud_controller("cap", "alert", rheader=s3db.cap_rheader) # ----------------------------------------------------------------------------- def area(): @@ -1369,7 +1368,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller("cap", "area", rheader=s3db.cap_rheader) + return crud_controller("cap", "area", rheader=s3db.cap_rheader) # ----------------------------------------------------------------------------- def warning_priority(): @@ -1397,7 +1396,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def notify_approver(): diff --git a/controllers/cms.py b/controllers/cms.py index fd8980a432..b384fe58cd 100644 --- a/controllers/cms.py +++ b/controllers/cms.py @@ -64,7 +64,7 @@ def prep(r): ctable.body.represent = lambda body: XML(body) ctable.body.widget = s3_richtext_widget else: - ctable.body.represent = lambda body: XML(s3_URLise(body)) + ctable.body.represent = lambda body: XML(s3base.s3_URLise(body)) ctable.body.widget = None # Special-purpose series @@ -102,19 +102,19 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller(rheader=s3db.cms_rheader) + return crud_controller(rheader=s3db.cms_rheader) # ----------------------------------------------------------------------------- def status(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def tag(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def blog(): @@ -138,8 +138,7 @@ def postp(r, output): return output s3.postp = postp - output = s3_rest_controller("cms", "series") - return output + return crud_controller("cms", "series") # ----------------------------------------------------------------------------- def post(): @@ -151,7 +150,7 @@ def post(): #s3.filter = (table.series_id == None) # Custom Method to add Comments - s3db.set_method("cms", "post", + s3db.set_method("cms_post", method = "discuss", action = discuss) @@ -295,9 +294,7 @@ def prep(r): return True s3.prep = prep - output = s3_rest_controller(rheader = s3db.cms_rheader, - ) - return output + return crud_controller(rheader=s3db.cms_rheader) # ----------------------------------------------------------------------------- def page(): @@ -362,8 +359,7 @@ def postp(r, output): return output s3.postp = postp - output = s3_rest_controller("cms", "post") - return output + return crud_controller("cms", "post") # ----------------------------------------------------------------------------- def cms_post_age(row): @@ -770,8 +766,7 @@ def postp(r, output): return output s3.postp = postp - output = s3_rest_controller("cms", "post") - return output + return crud_controller("cms", "post") # ============================================================================= # Comments @@ -779,7 +774,7 @@ def postp(r, output): def comment(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def discuss(r, **attr): diff --git a/controllers/cr.py b/controllers/cr.py index a28ca8b5ee..e98aec43cd 100755 --- a/controllers/cr.py +++ b/controllers/cr.py @@ -33,8 +33,7 @@ def shelter_type(): School, Hospital -- see Agasti opt_camp_type.) """ - output = s3_rest_controller() - return output + return crud_controller() # ----------------------------------------------------------------------------- def shelter_service(): @@ -43,8 +42,7 @@ def shelter_service(): List / add shelter services (e.g. medical, housing, food, ...) """ - output = s3_rest_controller() - return output + return crud_controller() # ----------------------------------------------------------------------------- def shelter_unit(): @@ -78,7 +76,7 @@ def prep(r): s3.prep = prep - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def shelter_registration(): @@ -98,8 +96,7 @@ def shelter_registration(): msg_list_empty = T("No people currently registered in shelters") ) - output = s3_rest_controller() - return output + return crud_controller() # ============================================================================= def shelter(): @@ -232,7 +229,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller(rheader = s3db.cr_shelter_rheader) + return crud_controller(rheader=s3db.cr_shelter_rheader) # ----------------------------------------------------------------------------- def shelter_flag(): @@ -274,7 +271,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def shelter_inspection(): @@ -282,7 +279,7 @@ def shelter_inspection(): Shelter Inspections - RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def shelter_inspection_flag(): @@ -290,7 +287,7 @@ def shelter_inspection_flag(): Shelter Inspection Flags - RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ============================================================================= def incoming(): diff --git a/controllers/custom.py b/controllers/custom.py index d772f305d5..34335c6320 100644 --- a/controllers/custom.py +++ b/controllers/custom.py @@ -51,6 +51,6 @@ def rest(): prefix, name = c, f # Run REST controller - return s3_rest_controller(prefix, name) + return crud_controller(prefix, name) # END ========================================================================= diff --git a/controllers/dc.py b/controllers/dc.py index 76f0eba71e..99014438b3 100644 --- a/controllers/dc.py +++ b/controllers/dc.py @@ -69,7 +69,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller(rheader = s3db.dc_rheader) + return crud_controller(rheader=s3db.dc_rheader) # ----------------------------------------------------------------------------- def question(): @@ -78,8 +78,7 @@ def question(): - used for imports & to manage translations """ - return s3_rest_controller(rheader = s3db.dc_rheader, - ) + return crud_controller(rheader=s3db.dc_rheader) # ----------------------------------------------------------------------------- def question_l10n(): @@ -88,8 +87,8 @@ def question_l10n(): - used for imports """ - return s3_rest_controller(#rheader = s3db.dc_rheader, - ) + return crud_controller(#rheader = s3db.dc_rheader, + ) # ----------------------------------------------------------------------------- def target(): @@ -122,16 +121,12 @@ def prep(r): ) elif r.id and not r.component and r.representation == "xls": # Custom XLS Exporter to include all Responses. - r.set_handler("read", s3db.dc_TargetXLS(), - http = ("GET", "POST"), - representation = "xls" - ) + r.custom_action = s3db.dc_TargetXLS return True s3.prep = prep - return s3_rest_controller(rheader = s3db.dc_rheader, - ) + return crud_controller(rheader=s3db.dc_rheader) # ----------------------------------------------------------------------------- def respnse(): # Cannot call this 'response' or it will clobber the global @@ -191,8 +186,8 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller("dc", "response", - rheader = s3db.dc_rheader, - ) + return crud_controller("dc", "response", + rheader = s3db.dc_rheader, + ) # END ========================================================================= diff --git a/controllers/default.py b/controllers/default.py index f0c22894ca..e2340a806f 100755 --- a/controllers/default.py +++ b/controllers/default.py @@ -402,7 +402,7 @@ def audit(): - used e.g. for Site Activity """ - return s3_rest_controller("s3", "audit") + return crud_controller("s3", "audit") # ----------------------------------------------------------------------------- #def call(): @@ -449,8 +449,7 @@ def prep(r): return True s3.prep = prep - output = s3_rest_controller(prefix, resourcename) - return output + return crud_controller(prefix, resourcename) templates = settings.get_template() if templates != "default": @@ -578,7 +577,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller("pr", "group") + return crud_controller("pr", "group") # ----------------------------------------------------------------------------- def help(): @@ -888,12 +887,12 @@ def auth_profile_method(r, **attr): "form": form, } - set_method("pr", "person", + set_method("pr_person", method = "user_profile", action = auth_profile_method) # Custom Method for Contacts - set_method("pr", "person", + set_method("pr_person", method = "contacts", action = s3db.pr_Contacts) @@ -1104,9 +1103,9 @@ def postp(r, output): (T("My Maps"), "config"), ] - return s3_rest_controller("pr", "person", - rheader = lambda r, tabs=tabs: \ - s3db.pr_rheader(r, tabs=tabs)) + return crud_controller("pr", "person", + rheader = lambda r, t=tabs: s3db.pr_rheader(r, tabs=t), + ) # ----------------------------------------------------------------------------- def privacy(): @@ -1181,7 +1180,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller("hrm", "skill") + return crud_controller("hrm", "skill") # ----------------------------------------------------------------------------- def tables(): @@ -1189,11 +1188,11 @@ def tables(): RESTful CRUD Controller for Dynamic Table Models """ - return s3_rest_controller("s3", "table", - rheader = s3db.s3_table_rheader, - csv_template = ("s3", "table"), - csv_stylesheet = ("s3", "table.xsl"), - ) + return crud_controller("s3", "table", + rheader = s3db.s3_table_rheader, + csv_template = ("s3", "table"), + csv_stylesheet = ("s3", "table.xsl"), + ) # ----------------------------------------------------------------------------- def table(): @@ -1207,7 +1206,7 @@ def table(): args = request.args if len(args): - return s3_rest_controller(dynamic = args[0].rsplit(".", 1)[0]) + return crud_controller(dynamic = args[0].rsplit(".", 1)[0]) else: raise HTTP(400, "No resource specified") @@ -1327,7 +1326,7 @@ def user(): elif arg == "options.s3json": # Used when adding organisations from registration form - return s3_rest_controller(prefix="auth", resourcename="user") + return crud_controller(prefix="auth", resourcename="user") else: # logout or verify_email diff --git a/controllers/deploy.py b/controllers/deploy.py index d86ae79155..3ef9526df2 100644 --- a/controllers/deploy.py +++ b/controllers/deploy.py @@ -122,12 +122,12 @@ def postp(r, output): return output s3.postp = postp - return s3_rest_controller(# Remove the title if we have a component - # (rheader includes the title) - notitle = lambda r: {"title": ""} \ - if r.component else None, - rheader = s3db.deploy_rheader, - ) + return crud_controller(# Remove the title if we have a component + # (rheader includes the title) + notitle = lambda r: {"title": ""} \ + if r.component else None, + rheader = s3db.deploy_rheader, + ) # ----------------------------------------------------------------------------- def response_message(): @@ -136,9 +136,9 @@ def response_message(): - can't be called 'response' as this clobbbers web2py global! """ - return s3_rest_controller("deploy", "response", - custom_crud_buttons = {"list_btn": None}, - ) + return crud_controller("deploy", "response", + custom_crud_buttons = {"list_btn": None}, + ) # ----------------------------------------------------------------------------- def human_resource(): @@ -272,7 +272,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller("pr", "group") + return crud_controller("pr", "group") # ----------------------------------------------------------------------------- def application(): @@ -296,11 +296,12 @@ def prep(r): s3.prep = prep if "delete" in request.args or \ - request.env.request_method == "POST" and auth.permission.format=="s3json": - return s3_rest_controller() + request.env.request_method == "POST" and \ + auth.permission.format == "s3json": + return crud_controller() else: #return s3db.hrm_human_resource_controller() - return s3_rest_controller("hrm", "human_resource") + return crud_controller("hrm", "human_resource") # ----------------------------------------------------------------------------- def assignment(): @@ -393,7 +394,7 @@ def postp(r, output): return output s3.postp = postp - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def competency(): @@ -417,13 +418,13 @@ def experience(): def event_type(): """ RESTful CRUD Controller """ - return s3_rest_controller("event", "event_type") + return crud_controller("event", "event_type") # ----------------------------------------------------------------------------- def job_title(): """ RESTful CRUD Controller """ - return s3_rest_controller("hrm", "job_title") + return crud_controller("hrm", "job_title") # ----------------------------------------------------------------------------- def training(): @@ -444,7 +445,7 @@ def hr_search(): s3.prep = lambda r: r.method == "search_ac" - return s3_rest_controller("hrm", "human_resource") + return crud_controller("hrm", "human_resource") # ----------------------------------------------------------------------------- def person_search(): @@ -459,7 +460,7 @@ def person_search(): s3.prep = lambda r: r.method == "search_ac" - return s3_rest_controller("pr", "person") + return crud_controller("pr", "person") # ----------------------------------------------------------------------------- def alert_create_script(): @@ -638,12 +639,12 @@ def postp(r, output): return output s3.postp = postp - return s3_rest_controller(rheader = s3db.deploy_rheader, - # Show filter only on recipient tab - hide_filter = {"recipient": False, - "_default": True, - } - ) + return crud_controller(rheader = s3db.deploy_rheader, + # Show filter only on recipient tab + hide_filter = {"recipient": False, + "_default": True, + } + ) # ----------------------------------------------------------------------------- def alert_response(): @@ -678,7 +679,7 @@ def alert_response(): # # Block # pass - return s3_rest_controller("deploy", "response") + return crud_controller("deploy", "response") # ----------------------------------------------------------------------------- def email_inbox(): @@ -782,7 +783,7 @@ def postp(r, output): return output s3.postp = postp - return s3_rest_controller("msg", "email") + return crud_controller("msg", "email") # ----------------------------------------------------------------------------- def email_channel(): @@ -868,7 +869,7 @@ def postp(r, output): return output s3.postp = postp - return s3_rest_controller("msg") + return crud_controller("msg") # ----------------------------------------------------------------------------- def twitter_channel(): @@ -972,10 +973,10 @@ def postp(r, output): return output s3.postp = postp - return s3_rest_controller("msg", - deduplicate = "", - list_btn = "", - ) + return crud_controller("msg", + deduplicate = "", + list_btn = "", + ) # ----------------------------------------------------------------------------- def alert_recipient(): @@ -986,7 +987,7 @@ def alert_recipient(): s3.prep = lambda r: r.method == "options" and r.representation == "s3json" - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- # Messaging diff --git a/controllers/disease.py b/controllers/disease.py index 71b9bcdc29..f099fc4a21 100644 --- a/controllers/disease.py +++ b/controllers/disease.py @@ -22,7 +22,7 @@ def index(): def disease(): """ Disease Information Controller """ - return s3_rest_controller(rheader = s3db.disease_rheader) + return crud_controller(rheader=s3db.disease_rheader) # ----------------------------------------------------------------------------- def case(): @@ -81,7 +81,7 @@ def postp(r, output): return output s3.postp = postp - return s3_rest_controller(rheader = s3db.disease_rheader) + return crud_controller(rheader=s3db.disease_rheader) # ----------------------------------------------------------------------------- def person(): @@ -114,10 +114,10 @@ def prep(r): r.error(404, current.ERROR.BAD_RECORD) # Update the request - request = s3base.S3Request("pr", "person", - args = [str(row.person_id)], - vars = {}, - ) + request = s3base.CRUDRequest("pr", "person", + args = [str(row.person_id)], + vars = {}, + ) r.resource = resource = request.resource r.record = request.record r.id = request.id @@ -173,9 +173,9 @@ def postp(r, output): return output s3.postp = postp - return s3_rest_controller("pr", "person", - rheader = s3db.disease_rheader, - ) + return crud_controller("pr", "person", + rheader = s3db.disease_rheader, + ) # ----------------------------------------------------------------------------- def tracing(): @@ -197,19 +197,19 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller(rheader = s3db.disease_rheader) + return crud_controller(rheader=s3db.disease_rheader) # ----------------------------------------------------------------------------- def demographic(): """ Disease Demographic: RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def testing_report(): """ Testing Site Daily Summary Report: RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def testing_demographic(): @@ -224,36 +224,36 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def testing_device(): """ Testing Device Registry: RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def case_diagnostics(): """ Diagnostic Tests: RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def statistic(): """ RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def stats_data(): """ RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def stats_aggregate(): """ RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # END ========================================================================= diff --git a/controllers/doc.py b/controllers/doc.py index 602a377752..39a2a9ade9 100644 --- a/controllers/doc.py +++ b/controllers/doc.py @@ -35,9 +35,7 @@ def prep(r): return True s3.prep = prep - output = s3_rest_controller(rheader = document_rheader, - ) - return output + return crud_controller(rheader=document_rheader) # ----------------------------------------------------------------------------- def document_rheader(r): @@ -131,8 +129,7 @@ def prep(r): return True s3.prep = prep - output = s3_rest_controller() - return output + return crud_controller() # ============================================================================= def bulk_upload(): @@ -145,7 +142,7 @@ def bulk_upload(): """ s3.stylesheets.append("plugins/fileuploader.css") - return dict() + return {} def upload_bulk(): """ @@ -318,6 +315,6 @@ def ck_delete(): # ----------------------------------------------------------------------------- def card_config(): - return s3_rest_controller() + return crud_controller() # END ========================================================================= diff --git a/controllers/dvi.py b/controllers/dvi.py index 3ed9e3bc10..7681983409 100755 --- a/controllers/dvi.py +++ b/controllers/dvi.py @@ -92,8 +92,7 @@ def prep(r): return True s3.prep = prep - output = s3_rest_controller() - return output + return crud_controller() # ----------------------------------------------------------------------------- def morgue(): @@ -117,8 +116,7 @@ def prep(r): return True s3.prep = prep - output = s3_rest_controller(rheader=rheader) - return output + return crud_controller(rheader=rheader) # ----------------------------------------------------------------------------- def body(): @@ -146,7 +144,7 @@ def body(): ], tabs=dvi_tabs) - return s3_rest_controller(rheader=rheader) + return crud_controller(rheader=rheader) # ----------------------------------------------------------------------------- def person(): @@ -221,12 +219,11 @@ def prep(r): rheader = lambda r: s3db.pr_rheader(r, tabs=mpr_tabs) - output = s3_rest_controller("pr", "person", - main = "first_name", - extra = "last_name", - rheader = rheader, - ) - return output + return crud_controller("pr", "person", + main = "first_name", + extra = "last_name", + rheader = rheader, + ) # ------------------------------------------------------------------------- def dvi_match_query(body_id): diff --git a/controllers/dvr.py b/controllers/dvr.py index ecd8360f92..8d5d7e8f13 100644 --- a/controllers/dvr.py +++ b/controllers/dvr.py @@ -385,7 +385,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller("pr", "person", rheader = s3db.dvr_rheader) + return crud_controller("pr", "person", rheader=s3db.dvr_rheader) # ----------------------------------------------------------------------------- def person_search(): @@ -405,7 +405,7 @@ def prep(r): s3.prep = prep - return s3_rest_controller("pr", "person") + return crud_controller("pr", "person") # ----------------------------------------------------------------------------- def document(): @@ -536,9 +536,9 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller("doc", "document", - rheader = s3db.dvr_rheader, - ) + return crud_controller("doc", "document", + rheader = s3db.dvr_rheader, + ) # ----------------------------------------------------------------------------- def group_membership(): @@ -634,9 +634,9 @@ def prep(r): settings.pr.request_home_phone = False settings.hrm.email_required = False - return s3_rest_controller("pr", "group_membership", - rheader = s3db.dvr_rheader, - ) + return crud_controller("pr", "group_membership", + rheader = s3db.dvr_rheader, + ) # ============================================================================= # Activities @@ -644,26 +644,25 @@ def prep(r): def activity(): """ Activities: RESTful CRUD Controller """ - return s3_rest_controller(rheader = s3db.dvr_rheader, - ) + return crud_controller(rheader=s3db.dvr_rheader) # ----------------------------------------------------------------------------- def activity_age_group(): """ Activity Age Groups: RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def activity_group_type(): """ Activity Group Types: RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def activity_focus(): """ Activity Focuses: RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ============================================================================= # Cases @@ -673,25 +672,25 @@ def case(): s3db.dvr_case_default_status() - return s3_rest_controller(rheader = s3db.dvr_rheader) + return crud_controller(rheader=s3db.dvr_rheader) # ----------------------------------------------------------------------------- def case_flag(): """ Case Flags: RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def case_status(): """ Case Statuses: RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def case_type(): """ Case Types: RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ============================================================================= # Case Activities @@ -753,7 +752,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def due_followups(): @@ -826,25 +825,25 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller("dvr", "case_activity") + return crud_controller("dvr", "case_activity") # ----------------------------------------------------------------------------- def activity_funding(): """ Activity Funding Proposals: RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def provider_type(): """ Provider Types for Case Activities: RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def referral_type(): """ Referral Types: RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ============================================================================= # Responses @@ -852,7 +851,7 @@ def referral_type(): def response_theme(): """ Response Themes: RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def response_type(): @@ -866,13 +865,13 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def response_status(): """ Response Statuses: RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def response_action(): @@ -964,8 +963,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller(rheader = s3db.dvr_rheader, - ) + return crud_controller(rheader=s3db.dvr_rheader) # ----------------------------------------------------------------------------- def termination_type(): @@ -988,7 +986,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def vulnerability_type(): @@ -1002,19 +1000,19 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def case_activity_update_type(): """ Case Activity Update Types: RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def case_activity_status(): """ Case Activity Statuses: RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ============================================================================= # Allowance @@ -1091,11 +1089,11 @@ def prep(r): table = s3db.dvr_allowance - return s3_rest_controller(csv_extra_fields=[{"label": "Date", - "field": table.date, - }, - ], - ) + return crud_controller(csv_extra_fields = [{"label": "Date", + "field": table.date, + }, + ], + ) # ============================================================================= # Appointments @@ -1127,23 +1125,23 @@ def prep(r): table = s3db.dvr_case_appointment - return s3_rest_controller(csv_extra_fields=[{"label": "Appointment Type", - "field": table.type_id, - }, - {"label": "Appointment Date", - "field": table.date, - }, - {"label": "Appointment Status", - "field": table.status, - }, - ], - ) + return crud_controller(csv_extra_fields = [{"label": "Appointment Type", + "field": table.type_id, + }, + {"label": "Appointment Date", + "field": table.date, + }, + {"label": "Appointment Status", + "field": table.status, + }, + ], + ) # ----------------------------------------------------------------------------- def case_appointment_type(): """ Appointment Type: RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ============================================================================= # Case Events @@ -1165,13 +1163,13 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def case_event_type(): """ Case Event Types: RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ============================================================================= # Needs @@ -1197,7 +1195,7 @@ def need(): orderby="%s.name" % tablename, )) - return s3_rest_controller() + return crud_controller() # ============================================================================= # Notes @@ -1212,12 +1210,12 @@ def note(): field.default = person_id field.readable = field.writable = False - return s3_rest_controller() + return crud_controller() def note_type(): """ Note Types: RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ============================================================================= # Household @@ -1225,13 +1223,13 @@ def note_type(): def beneficiary_type(): """ Beneficiary Types: RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def beneficiary_data(): """ Beneficiary Data: RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ============================================================================= # Economy @@ -1242,19 +1240,19 @@ def housing(): s3.prep = lambda r: r.method == "options" and \ r.representation == "s3json" - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def housing_type(): """ Housing Types: RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def income_source(): """ Income Sources: RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ============================================================================= # Legal Status @@ -1262,13 +1260,13 @@ def income_source(): def residence_status_type(): """ Residence Status Types: RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def residence_permit_type(): """ Residence Permit Types: RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ============================================================================= # Service Contacts @@ -1276,7 +1274,7 @@ def residence_permit_type(): def service_contact_type(): """ Service Contact Types: RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ============================================================================= # Evaluations @@ -1335,19 +1333,19 @@ def evaluation(): #subheadings = subheadings, ) - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def evaluation_question(): """ RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def evaluation_data(): """ RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ============================================================================= # Site Activities (in connection with CR module) @@ -1355,6 +1353,6 @@ def evaluation_data(): def site_activity(): """ Site Activity Reports: RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # END ========================================================================= diff --git a/controllers/event.py b/controllers/event.py index 3c4abfea55..5d40fc459f 100644 --- a/controllers/event.py +++ b/controllers/event.py @@ -86,7 +86,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller(rheader = s3db.event_rheader) + return crud_controller(rheader=s3db.event_rheader) # ----------------------------------------------------------------------------- def event_location(): @@ -94,7 +94,7 @@ def event_location(): RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def event_type(): @@ -102,7 +102,7 @@ def event_type(): RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def incident_type(): @@ -110,7 +110,7 @@ def incident_type(): RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def incident(): @@ -242,8 +242,7 @@ def postp(r, output): return output s3.postp = postp - output = s3_rest_controller(rheader = s3db.event_rheader) - return output + return crud_controller(rheader=s3db.event_rheader) # ----------------------------------------------------------------------------- def incident_report(): @@ -281,7 +280,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def job_title(): @@ -327,7 +326,7 @@ def prep(r): if not auth.s3_has_role("ADMIN"): s3.filter &= auth.filter_by_root_org(table) - return s3_rest_controller("hrm") + return crud_controller("hrm") # ----------------------------------------------------------------------------- def scenario(): @@ -335,7 +334,7 @@ def scenario(): RESTful CRUD controller """ - return s3_rest_controller(rheader = s3db.event_rheader) + return crud_controller(rheader=s3db.event_rheader) # ----------------------------------------------------------------------------- def sitrep(): @@ -396,7 +395,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller(rheader = s3db.event_rheader) + return crud_controller(rheader=s3db.event_rheader) # ----------------------------------------------------------------------------- def template(): @@ -407,9 +406,7 @@ def template(): s3db.dc_template.master.default = "event_sitrep" - return s3_rest_controller("dc", "template", - rheader = s3db.dc_rheader, - ) + return crud_controller("dc", "template", rheader=s3db.dc_rheader) # ----------------------------------------------------------------------------- def resource(): @@ -441,7 +438,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def person(): @@ -456,7 +453,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller("pr", "person") + return crud_controller("pr", "person") # ----------------------------------------------------------------------------- def group(): @@ -493,31 +490,31 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller("pr", "group") + return crud_controller("pr", "group") # ----------------------------------------------------------------------------- def team(): """ Events <> Teams """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def team_status(): """ Team statuses """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def human_resource(): """ Events <> Human Resources """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def organisation(): """ Events <> Organisations """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def compose(): diff --git a/controllers/fin.py b/controllers/fin.py index 65df46e2bf..4c172a672b 100644 --- a/controllers/fin.py +++ b/controllers/fin.py @@ -20,7 +20,7 @@ def index(): def expense(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def voucher_program(): @@ -52,19 +52,19 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller(rheader = s3db.fin_rheader) + return crud_controller(rheader=s3db.fin_rheader) # ----------------------------------------------------------------------------- def voucher(): """ Vouchers: RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def voucher_debit(): """ Voucher Debits: RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def voucher_claim(): @@ -91,7 +91,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller(rheader = s3db.fin_rheader) + return crud_controller(rheader=s3db.fin_rheader) # ----------------------------------------------------------------------------- def voucher_invoice(): @@ -115,6 +115,6 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller(rheader = s3db.fin_rheader) + return crud_controller(rheader=s3db.fin_rheader) # END ========================================================================= diff --git a/controllers/fire.py b/controllers/fire.py index 67e211c8a3..b4c4ee2ef9 100644 --- a/controllers/fire.py +++ b/controllers/fire.py @@ -58,13 +58,13 @@ def index(): def zone(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def zone_type(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def station(): @@ -101,9 +101,9 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller(rheader = fire_rheader, - csv_extra_fields = csv_extra_fields, - ) + return crud_controller(rheader = fire_rheader, + csv_extra_fields = csv_extra_fields, + ) # ----------------------------------------------------------------------------- def station_vehicle(): @@ -111,31 +111,31 @@ def station_vehicle(): s3.prep = lambda r: r.method == "import" - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def water_source(): """ Water Sources """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def hazard_point(): """ Hazard Points """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def person(): """ Person Controller for Ajax Requests """ - return s3_rest_controller("pr", "person") + return crud_controller("pr", "person") # ----------------------------------------------------------------------------- def ireport_vehicle(): """ REST controller """ - return s3_rest_controller("irs", "ireport_vehicle") + return crud_controller("irs", "ireport_vehicle") # ----------------------------------------------------------------------------- def fire_rheader(r, tabs=[]): diff --git a/controllers/gis.py b/controllers/gis.py index 635e9becec..b297bb0cfd 100644 --- a/controllers/gis.py +++ b/controllers/gis.py @@ -296,13 +296,13 @@ def location(): # Custom Methods set_method = s3db.set_method from core import S3ExportPOI, S3ImportPOI - set_method("gis", "location", + set_method("gis_location", method = "export_poi", action = S3ExportPOI()) - set_method("gis", "location", + set_method("gis_location", method = "import_poi", action = S3ImportPOI()) - set_method("gis", "location", + set_method("gis_location", method = "parents", action = s3_gis_location_parents) @@ -650,13 +650,13 @@ def prep(r, prep_vars): represent = lambda code: \ gis.get_country(code, key_type="code") or UNKNOWN_OPT) - output = s3_rest_controller(# CSV column headers, so no T() - csv_extra_fields = [{"label": "Country", - "field": country(), - } - ], - rheader = s3db.gis_rheader, - ) + output = crud_controller(# CSV column headers, so no T() + csv_extra_fields = [{"label": "Country", + "field": country(), + } + ], + rheader = s3db.gis_rheader, + ) _map = prep_vars.get("_map") if _map and isinstance(output, dict): @@ -1095,17 +1095,17 @@ def config(): # Custom Methods to set as default set_method = s3db.set_method - set_method(module, resourcename, + set_method("gis_config", method = "default", action = config_default) # Custom Methods to enable/disable layers - set_method(module, resourcename, - component_name = "layer_entity", + set_method("gis_config", + component = "layer_entity", method = "enable", action = enable_layer) - set_method(module, resourcename, - component_name = "layer_entity", + set_method("gis_config", + component = "layer_entity", method = "disable", action = disable_layer) @@ -1376,9 +1376,7 @@ def postp(r, output): return output s3.postp = postp - output = s3_rest_controller(rheader = s3db.gis_rheader, - ) - return output + return crud_controller(rheader=s3db.gis_rheader) # ----------------------------------------------------------------------------- def enable_layer(r, **attr): @@ -1424,19 +1422,19 @@ def hierarchy(): s3db.gis_hierarchy_form_setup() - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def location_tag(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def menu(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def marker(): @@ -1452,7 +1450,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller(rheader=s3db.gis_rheader) + return crud_controller(rheader=s3db.gis_rheader) # ----------------------------------------------------------------------------- def projection(): @@ -1461,7 +1459,7 @@ def projection(): if settings.get_security_map() and not auth.s3_has_role("MAP_ADMIN"): auth.permission.fail() - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def style(): @@ -1474,13 +1472,13 @@ def style(): field.requires = IS_ONE_OF(db, "gis_layer_entity.layer_id", represent) - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def waypoint(): """ RESTful CRUD controller for GPS Waypoints """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def waypoint_upload(): @@ -1495,13 +1493,13 @@ def waypoint_upload(): def trackpoint(): """ RESTful CRUD controller for GPS Track points """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def track(): """ RESTful CRUD controller for GPS Tracks (uploaded as files) """ - return s3_rest_controller() + return crud_controller() # ============================================================================= def inject_enable(output): @@ -1549,8 +1547,7 @@ def layer_config(): # Cannot import without a specific layer type csv_stylesheet = None - output = s3_rest_controller(csv_stylesheet = csv_stylesheet) - return output + return crud_controller(csv_stylesheet=csv_stylesheet) # ----------------------------------------------------------------------------- def layer_entity(): @@ -1560,7 +1557,7 @@ def layer_entity(): auth.permission.fail() # Custom Method - s3db.set_method(module, resourcename, + s3db.set_method("gis_layer_entity", method = "disable", action = disable_layer) @@ -1613,15 +1610,14 @@ def prep(r): return True s3.prep = prep - output = s3_rest_controller(rheader = s3db.gis_rheader) - return output + return crud_controller(rheader=s3db.gis_rheader) # ----------------------------------------------------------------------------- def layer_feature(): """ RESTful CRUD controller """ # Custom Method - s3db.set_method(module, resourcename, + s3db.set_method("gis_layer_feature", method = "disable", action = disable_layer) @@ -1657,8 +1653,7 @@ def postp(r, output): return output s3.postp = postp - output = s3_rest_controller(rheader = s3db.gis_rheader) - return output + return crud_controller(rheader=s3db.gis_rheader) # ----------------------------------------------------------------------------- def layer_openstreetmap(): @@ -1715,9 +1710,7 @@ def postp(r, output): return output s3.postp = postp - output = s3_rest_controller(rheader = s3db.gis_rheader) - - return output + return crud_controller(rheader=s3db.gis_rheader) # ----------------------------------------------------------------------------- def layer_bing(): @@ -1772,9 +1765,7 @@ def postp(r, output): return output s3.postp = postp - output = s3_rest_controller(rheader = s3db.gis_rheader) - - return output + return crud_controller(rheader=s3db.gis_rheader) # ----------------------------------------------------------------------------- def layer_empty(): @@ -1820,9 +1811,7 @@ def prep(r): return True s3.prep = prep - output = s3_rest_controller(rheader = s3db.gis_rheader) - - return output + return crud_controller(rheader=s3db.gis_rheader) # ----------------------------------------------------------------------------- def layer_google(): @@ -1876,9 +1865,7 @@ def postp(r, output): return output s3.postp = postp - output = s3_rest_controller(rheader = s3db.gis_rheader) - - return output + return crud_controller(rheader=s3db.gis_rheader) # ----------------------------------------------------------------------------- def layer_mgrs(): @@ -1931,9 +1918,7 @@ def prep(r): return True s3.prep = prep - output = s3_rest_controller(rheader = s3db.gis_rheader) - - return output + return crud_controller(rheader=s3db.gis_rheader) # ----------------------------------------------------------------------------- def layer_arcrest(): @@ -1962,7 +1947,7 @@ def layer_arcrest(): msg_list_empty = NO_LAYERS) # Custom Method - s3db.set_method(module, resourcename, + s3db.set_method("gis_layer_arcrest", method = "enable", action = enable_layer) @@ -1998,9 +1983,7 @@ def postp(r, output): return output s3.postp = postp - output = s3_rest_controller(rheader = s3db.gis_rheader) - - return output + return crud_controller(rheader=s3db.gis_rheader) # ----------------------------------------------------------------------------- def layer_geojson(): @@ -2064,9 +2047,7 @@ def postp(r, output): return output s3.postp = postp - output = s3_rest_controller(rheader = s3db.gis_rheader) - - return output + return crud_controller(rheader=s3db.gis_rheader) # ----------------------------------------------------------------------------- def layer_georss(): @@ -2095,7 +2076,7 @@ def layer_georss(): msg_list_empty = NO_LAYERS) # Custom Method - s3db.set_method(module, resourcename, + s3db.set_method("gis_layer_georss", method = "enable", action = enable_layer) @@ -2135,9 +2116,7 @@ def postp(r, output): return output s3.postp = postp - output = s3_rest_controller(rheader = s3db.gis_rheader) - - return output + return crud_controller(rheader=s3db.gis_rheader) # ----------------------------------------------------------------------------- def layer_gpx(): @@ -2199,9 +2178,7 @@ def postp(r, output): return output s3.postp = postp - output = s3_rest_controller(rheader = s3db.gis_rheader) - - return output + return crud_controller(rheader=s3db.gis_rheader) # ----------------------------------------------------------------------------- def layer_kml(): @@ -2230,7 +2207,7 @@ def layer_kml(): msg_list_empty = NO_LAYERS) # Custom Method - #s3db.set_method(module, resourcename, + #s3db.set_method("gis_layer_kml", # method = "enable", # action = enable_layer) @@ -2266,9 +2243,7 @@ def postp(r, output): return output s3.postp = postp - output = s3_rest_controller(rheader = s3db.gis_rheader) - - return output + return crud_controller(rheader=s3db.gis_rheader) # ----------------------------------------------------------------------------- def layer_openweathermap(): @@ -2297,7 +2272,7 @@ def layer_openweathermap(): msg_list_empty = NO_LAYERS) # Custom Method - s3db.set_method(module, resourcename, + s3db.set_method("gis_layer_openweathermap", method = "enable", action = enable_layer) @@ -2337,8 +2312,7 @@ def postp(r, output): return output s3.postp = postp - output = s3_rest_controller(rheader = s3db.gis_rheader) - return output + return crud_controller(rheader=s3db.gis_rheader) # ----------------------------------------------------------------------------- def layer_shapefile(): @@ -2367,7 +2341,7 @@ def layer_shapefile(): msg_list_empty = NO_LAYERS) # Custom Method - s3db.set_method(module, resourcename, + s3db.set_method("gis_layer_shapefile", method = "enable", action = enable_layer) @@ -2445,8 +2419,7 @@ def postp(r, output): return output s3.postp = postp - output = s3_rest_controller(rheader = s3db.gis_rheader) - return output + return crud_controller(rheader=s3db.gis_rheader) # ----------------------------------------------------------------------------- def layer_theme(): @@ -2513,12 +2486,12 @@ def postp(r, output): if "import" in request.args: # Import to 'layer_config' resource instead - output = s3_rest_controller("gis", "layer_config", - csv_template="layer_theme", - csv_stylesheet="layer_theme.xsl", - ) + output = crud_controller("gis", "layer_config", + csv_template="layer_theme", + csv_stylesheet="layer_theme.xsl", + ) else: - output = s3_rest_controller(rheader = s3db.gis_rheader) + output = crud_controller(rheader=s3db.gis_rheader) return output @@ -2528,13 +2501,12 @@ def theme_data(): field = s3db.gis_layer_theme_id() field.requires = IS_EMPTY_OR(field.requires) - output = s3_rest_controller(csv_extra_fields = [# CSV column headers, so no T() - {"label": "Layer", - "field": field, - }], - ) - - return output + return crud_controller(csv_extra_fields = [# CSV column headers, so no T() + {"label": "Layer", + "field": field, + }, + ], + ) # ----------------------------------------------------------------------------- def layer_tms(): @@ -2563,7 +2535,7 @@ def layer_tms(): msg_list_empty = NO_LAYERS) # Custom Method - s3db.set_method(module, resourcename, + s3db.set_method("gis_layer_tms", method = "enable", action = enable_layer) @@ -2599,9 +2571,7 @@ def postp(r, output): return output s3.postp = postp - output = s3_rest_controller(rheader = s3db.gis_rheader) - - return output + return crud_controller(rheader=s3db.gis_rheader) # ----------------------------------------------------------------------------- def layer_wfs(): @@ -2661,9 +2631,7 @@ def postp(r, output): return output s3.postp = postp - output = s3_rest_controller(rheader = s3db.gis_rheader) - - return output + return crud_controller(rheader=s3db.gis_rheader) # ----------------------------------------------------------------------------- def layer_wms(): @@ -2692,7 +2660,7 @@ def layer_wms(): msg_list_empty = NO_LAYERS) # Custom Method - s3db.set_method(module, resourcename, + s3db.set_method("gis_layer_wms", method = "enable", action = enable_layer) @@ -2727,9 +2695,7 @@ def postp(r, output): return output s3.postp = postp - output = s3_rest_controller(rheader = s3db.gis_rheader) - - return output + return crud_controller(rheader=s3db.gis_rheader) # ----------------------------------------------------------------------------- def layer_xyz(): @@ -2758,7 +2724,7 @@ def layer_xyz(): msg_list_empty = NO_LAYERS) # Custom Method - s3db.set_method(module, resourcename, + s3db.set_method("gis_layer_xyz", method = "enable", action = enable_layer) @@ -2794,9 +2760,7 @@ def postp(r, output): return output s3.postp = postp - output = s3_rest_controller(rheader = s3db.gis_rheader) - - return output + return crud_controller(rheader=s3db.gis_rheader) # ----------------------------------------------------------------------------- def layer_js(): @@ -2857,9 +2821,7 @@ def postp(r, output): return output s3.postp = postp - output = s3_rest_controller(rheader = s3db.gis_rheader) - - return output + return crud_controller(rheader=s3db.gis_rheader) # ============================================================================= def cache_feed(): @@ -2900,8 +2862,7 @@ def cache_feed(): # Unzip & Follow Network Links #download_kml.delay(url) - output = s3_rest_controller("gis", "cache") - return output + return crud_controller("gis", "cache") # ============================================================================= def feature_query(): @@ -2938,7 +2899,7 @@ def feature_query(): s3.filter = (table.lat != None) & (table.lon != None) # Parse the Request - r = s3_request() + r = crud_request() if r.representation != "geojson": session.error = ERROR.BAD_FORMAT @@ -2957,7 +2918,7 @@ def poi_type(): RESTful CRUD controller for PoI Types """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def poi(): @@ -3047,7 +3008,7 @@ def postp(r, output): dt_bulk_actions = [(T("Delete"), "delete")] - return s3_rest_controller(dtargs = {"dt_bulk_actions": dt_bulk_actions}) + return crud_controller(dtargs = {"dt_bulk_actions": dt_bulk_actions}) # ============================================================================= def display_feature(): @@ -3437,7 +3398,7 @@ def get_location_info(): ], ) - output = s3_rest_controller("gis", "location") + output = crud_controller("gis", "location") _map = vars.get("_map", None) if _map and isinstance(output, dict): diff --git a/controllers/hms.py b/controllers/hms.py index f553d88a35..c78d5e454b 100755 --- a/controllers/hms.py +++ b/controllers/hms.py @@ -254,8 +254,7 @@ def prep(r): return True s3.prep = prep - output = s3_rest_controller(rheader = s3db.hms_hospital_rheader) - return output + return crud_controller(rheader=s3db.hms_hospital_rheader) # ----------------------------------------------------------------------------- def incoming(): diff --git a/controllers/hrm.py b/controllers/hrm.py index a25a9367a2..f2a4a61e55 100644 --- a/controllers/hrm.py +++ b/controllers/hrm.py @@ -184,7 +184,7 @@ def postp(r, output): return output s3.postp = postp - return s3_rest_controller("hrm", "human_resource") + return crud_controller("hrm", "human_resource") # ----------------------------------------------------------------------------- def person(): @@ -224,7 +224,7 @@ def profile(): request.args = [str(auth.s3_logged_in_person())] # Custom Method for Contacts - s3db.set_method("pr", "profile", + s3db.set_method("pr_profile", method = "contacts", action = s3db.pr_Contacts) @@ -284,9 +284,7 @@ def prep(r): return False s3.prep = prep - return s3_rest_controller("pr", "person", - rheader = s3db.hrm_rheader, - ) + return crud_controller("pr", "person", rheader=s3db.hrm_rheader) # ----------------------------------------------------------------------------- def hr_search(): @@ -305,7 +303,7 @@ def hr_search(): s3.prep = lambda r: r.method == "search_ac" - return s3_rest_controller("hrm", "human_resource") + return crud_controller("hrm", "human_resource") # ----------------------------------------------------------------------------- def person_search(): @@ -324,7 +322,7 @@ def person_search(): s3.prep = lambda r: r.method == "search_ac" - return s3_rest_controller("pr", "person") + return crud_controller("pr", "person") # ============================================================================= # Teams @@ -369,10 +367,10 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller("pr", "group_membership", - csv_stylesheet = ("hrm", "group_membership.xsl"), - csv_template = "group_membership", - ) + return crud_controller("pr", "group_membership", + csv_stylesheet = ("hrm", "group_membership.xsl"), + csv_template = "group_membership", + ) # ============================================================================= # Jobs @@ -383,7 +381,7 @@ def department(): if not auth.s3_has_role("ADMIN"): s3.filter = auth.filter_by_root_org(s3db.hrm_department) - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def job_title(): @@ -421,7 +419,7 @@ def prep(r): if not auth.s3_has_role("ADMIN"): s3.filter &= auth.filter_by_root_org(s3db.hrm_job_title) - return s3_rest_controller() + return crud_controller() # ============================================================================= # Skills @@ -429,25 +427,25 @@ def prep(r): def skill(): """ Skills Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def skill_type(): """ Skill Types Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def competency_rating(): """ Competency Rating for Skill Types Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def skill_provision(): """ Skill Provisions Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def course(): @@ -462,14 +460,13 @@ def prep(r): if not auth.s3_has_role("ADMIN") and not s3.filter: s3.filter = auth.filter_by_root_org(s3db.hrm_course) - return s3_rest_controller(rheader = s3db.hrm_rheader, - ) + return crud_controller(rheader=s3db.hrm_rheader) # ----------------------------------------------------------------------------- def course_certificate(): """ Courses to Certificates Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def certificate(): @@ -479,8 +476,7 @@ def certificate(): not auth.s3_has_role("ADMIN"): s3.filter = auth.filter_by_root_org(s3db.hrm_certificate) - return s3_rest_controller(rheader = s3db.hrm_rheader, - ) + return crud_controller(rheader=s3db.hrm_rheader) # ----------------------------------------------------------------------------- def certification(): @@ -503,20 +499,19 @@ def certification(): not auth.s3_has_role("ADMIN"): s3.filter = auth.filter_by_root_org(s3db.hrm_certificate) - return s3_rest_controller(rheader = s3db.hrm_rheader, - ) + return crud_controller(rheader=s3db.hrm_rheader) # ----------------------------------------------------------------------------- def certificate_skill(): """ Certificates to Skills Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def event_type(): """ Event Types Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def facility(): @@ -723,18 +718,18 @@ def staff_for_site(): def programme(): """ Programmes Controller """ - return s3_rest_controller() + return crud_controller() def programme_hours(): """ Programme Hours Controller """ - return s3_rest_controller() + return crud_controller() # ============================================================================= def strategy(): """ Strategies Controller """ - return s3_rest_controller("project") + return crud_controller("project") # ============================================================================= # Salaries @@ -742,13 +737,13 @@ def strategy(): def staff_level(): """ Staff Levels Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def salary_grade(): """ Salary Grade Controller """ - return s3_rest_controller() + return crud_controller() # ============================================================================= # Insurance Information @@ -756,7 +751,7 @@ def salary_grade(): def insurance(): """ Insurance Information Controller """ - return s3_rest_controller() + return crud_controller() # ============================================================================= # Awards @@ -764,13 +759,13 @@ def insurance(): def award_type(): """ Award Type Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def award(): """ Awards Controller """ - return s3_rest_controller() + return crud_controller() # ============================================================================= # Disciplinary Record @@ -778,13 +773,13 @@ def award(): def disciplinary_type(): """ Disciplinary Type Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def disciplinary_action(): """ Disciplinary Action Controller """ - return s3_rest_controller() + return crud_controller() # ============================================================================= # Shifts @@ -826,12 +821,12 @@ def prep(r): s3.prep = prep - return s3_rest_controller() + return crud_controller() def shift_template(): """ Shift Templates Controller """ - return s3_rest_controller() + return crud_controller() # ============================================================================= # Delegations @@ -863,7 +858,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # ============================================================================= # Messaging diff --git a/controllers/inv.py b/controllers/inv.py index 0302e096a5..4874b21a2e 100644 --- a/controllers/inv.py +++ b/controllers/inv.py @@ -211,8 +211,7 @@ def index2(): pdf_groupby = "site_id", ) s3.filter = filter - r = s3_request("inv", "inv_item", - vars={"orderby" : orderby}) + r = crud_request("inv", "inv_item", vars={"orderby" : orderby}) r.resource = resource output = r(pdf_groupby = "site_id", dt_group = 1, @@ -262,7 +261,7 @@ def index2(): dt_action_col = 1, ) return supply_items - r = s3_request(prefix = "inv", name = "inv_item") + r = crud_request(prefix = "inv", name = "inv_item") return {"module_name": module_name, "warehouses": warehouses, "inventory": inventory, @@ -410,21 +409,20 @@ def postp(r, output): native = False from s3db.inv import inv_rheader - output = s3_rest_controller(module, resourcename, - #hide_filter = {"inv_item": False, - # "_default": True, - # }, - # Extra fields for CSV uploads: - #csv_extra_fields = [ - # dict(label="Organisation", - # field=s3db.org_organisation_id(comment=None)) - #] - csv_stylesheet = csv_stylesheet, - csv_template = resourcename, - native = native, - rheader = inv_rheader, - ) - return output + return crud_controller(module, resourcename, + #hide_filter = {"inv_item": False, + # "_default": True, + # }, + ## Extra fields for CSV uploads: + #csv_extra_fields = [{"label": "Organisation", + # "field": s3db.org_organisation_id(comment=None)), + # }, + # ], + csv_stylesheet = csv_stylesheet, + csv_template = resourcename, + native = native, + rheader = inv_rheader, + ) # ----------------------------------------------------------------------------- def warehouse_type(): @@ -432,7 +430,7 @@ def warehouse_type(): RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def supplier(): @@ -549,54 +547,52 @@ def prep(r): s3.prep = prep # Import pre-process - def import_prep(data): + def import_prep(tree): """ Deletes all Stock records of the organisation/branch before processing a new data import """ - if s3.import_replace: - resource, tree = data - if tree is not None: - xml = current.xml - tag = xml.TAG - att = xml.ATTRIBUTE - - root = tree.getroot() - expr = "/%s/%s[@%s='org_organisation']/%s[@%s='name']" % \ - (tag.root, tag.resource, att.name, tag.data, att.field) - orgs = root.xpath(expr) - otable = s3db.org_organisation - stable = s3db.org_site - itable = s3db.inv_inv_item - for org in orgs: - org_name = org.get("value", None) or org.text - if org_name: - try: - org_name = json.loads(xml.xml_decode(org_name)) - except: - pass - if org_name: - query = (otable.name == org_name) & \ - (stable.organisation_id == otable.id) & \ - (itable.site_id == stable.id) - resource = s3db.resource("inv_inv_item", filter=query) - # Use cascade=True so that the deletion gets - # rolled back if the import fails: - resource.delete(format="xml", cascade=True) - resource.skip_import = True + if s3.import_replace and tree is not None: + xml = current.xml + tag = xml.TAG + att = xml.ATTRIBUTE + + root = tree.getroot() + expr = "/%s/%s[@%s='org_organisation']/%s[@%s='name']" % \ + (tag.root, tag.resource, att.name, tag.data, att.field) + orgs = root.xpath(expr) + otable = s3db.org_organisation + stable = s3db.org_site + itable = s3db.inv_inv_item + for org in orgs: + org_name = org.get("value", None) or org.text + if org_name: + try: + org_name = json.loads(xml.xml_decode(org_name)) + except: + pass + if org_name: + query = (otable.name == org_name) & \ + (stable.organisation_id == otable.id) & \ + (itable.site_id == stable.id) + resource = s3db.resource("inv_inv_item", filter=query) + # Use cascade=True so that the deletion gets + # rolled back if the import fails: + resource.delete(format="xml", cascade=True) + s3.import_prep = import_prep - output = s3_rest_controller(#csv_extra_fields = [{"label": "Organisation", - # "field": s3db.org_organisation_id(comment = None) - # }, - # ], - pdf_orientation = "Landscape", - pdf_table_autogrow = "B", - pdf_groupby = "site_id, item_id", - pdf_orderby = "expiry_date, supply_org_id", - replace_option = T("Remove existing data before import"), - rheader = s3db.inv_rheader, - ) + output = crud_controller(#csv_extra_fields = [{"label": "Organisation", + # "field": s3db.org_organisation_id(comment = None) + # }, + # ], + pdf_orientation = "Landscape", + pdf_table_autogrow = "B", + pdf_groupby = "site_id, item_id", + pdf_orderby = "expiry_date, supply_org_id", + replace_option = T("Remove existing data before import"), + rheader = s3db.inv_rheader, + ) if not settings.get_inv_direct_stock_edits() and \ isinstance(output, dict) and \ @@ -629,9 +625,9 @@ def prep(r): return True s3.prep = prep - output = s3_rest_controller("inv", "track_item", - rheader = s3db.inv_rheader, - ) + output = crud_controller("inv", "track_item", + rheader = s3db.inv_rheader, + ) if isinstance(output, dict) and \ "add_btn" in output: del output["add_btn"] @@ -1132,9 +1128,7 @@ def prep(r): ) from s3db.inv import inv_recv_rheader - output = s3_rest_controller(rheader = inv_recv_rheader, - ) - return output + return crud_controller(rheader=inv_recv_rheader) # ----------------------------------------------------------------------------- def req_items_for_inv(site_id, quantity_type): @@ -1285,7 +1279,7 @@ def recv_process(): # Customise the inv_track_item so templates can customise the onaccept tracktable = db.inv_track_item - r = s3_request("inv", "track_item", args=[], vars={}) + r = crud_request("inv", "track_item", args=[], vars={}) r.customise_resource("inv_track_item") # Lookup the send_id from a track item of this recv @@ -1531,9 +1525,7 @@ def track_item(): s3.filter = (FS("expiry_date") != None) from s3db.inv import inv_rheader - output = s3_rest_controller(rheader = inv_rheader, - ) - return output + return crud_controller(rheader=inv_rheader) # ============================================================================= def adj(): @@ -1634,9 +1626,7 @@ def postp(r, output): ) from s3db.inv import inv_adj_rheader - output = s3_rest_controller(rheader = inv_adj_rheader, - ) - return output + return crud_controller(rheader=inv_adj_rheader) # ----------------------------------------------------------------------------- def adj_close(): @@ -1802,8 +1792,7 @@ def send_item_json(): def kitting(): from s3db.inv import inv_rheader - return s3_rest_controller(rheader = inv_rheader, - ) + return crud_controller(rheader=inv_rheader) # ----------------------------------------------------------------------------- def facility(): @@ -1817,7 +1806,7 @@ def facility(): # ----------------------------------------------------------------------------- def facility_type(): - return s3_rest_controller("org") + return crud_controller("org") # ----------------------------------------------------------------------------- def project(): @@ -1846,7 +1835,7 @@ def project(): list_fields = list_fields, ) - return s3_rest_controller("project") + return crud_controller("project") # ----------------------------------------------------------------------------- def incoming(): diff --git a/controllers/irs.py b/controllers/irs.py index 1f9ce32d5f..cf1f9f3701 100644 --- a/controllers/irs.py +++ b/controllers/irs.py @@ -27,8 +27,7 @@ def icategory(): The full list of hard-coded categories are visible to admins & should remain unchanged for sync """ - output = s3_rest_controller() - return output + return crud_controller() # ----------------------------------------------------------------------------- def ireport(): @@ -140,7 +139,7 @@ def postp(r, output): return output response.s3.postp = postp - output = s3_rest_controller(rheader=s3db.irs_rheader) + output = crud_controller(rheader=s3db.irs_rheader) # @ToDo: Add 'Dispatch' button to send OpenGeoSMS #try: diff --git a/controllers/member.py b/controllers/member.py index 7d568387d6..e55157fa6a 100644 --- a/controllers/member.py +++ b/controllers/member.py @@ -34,8 +34,7 @@ def membership_type(): if not auth.s3_has_role("ADMIN"): s3.filter = auth.filter_by_root_org(s3db.member_membership_type) - output = s3_rest_controller() - return output + return crud_controller() # ============================================================================= def membership(): @@ -78,7 +77,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller(rheader = s3db.member_rheader) + return crud_controller(rheader=s3db.member_rheader) # ============================================================================= def person(): @@ -104,49 +103,47 @@ def person(): # Custom Method for Contacts set_method = s3db.set_method - set_method("pr", resourcename, + set_method("pr_person", method = "contacts", action = s3db.pr_Contacts) # Custom Method for CV - set_method("pr", "person", + set_method("pr_person", method = "cv", # @ToDo: Allow Members to have a CV without enabling HRM? action = s3db.hrm_CV) # Import pre-process - def import_prep(data): + def import_prep(tree): """ Deletes all Member records of the organisation/branch before processing a new data import """ - if s3.import_replace: - resource, tree = data - if tree is not None: - xml = current.xml - tag = xml.TAG - att = xml.ATTRIBUTE - - root = tree.getroot() - expr = "/%s/%s[@%s='org_organisation']/%s[@%s='name']" % \ - (tag.root, tag.resource, att.name, tag.data, att.field) - orgs = root.xpath(expr) - for org in orgs: - org_name = org.get("value", None) or org.text - if org_name: - try: - org_name = json.loads(xml.xml_decode(org_name)) - except: - pass - if org_name: - mtable = s3db.member_membership - otable = s3db.org_organisation - query = (otable.name == org_name) & \ - (mtable.organisation_id == otable.id) - resource = s3db.resource("member_membership", filter=query) - # Use cascade=True so that the deletion gets - # rolled back if the import fails: - resource.delete(format="xml", cascade=True) + if s3.import_replace and tree is not None: + xml = current.xml + tag = xml.TAG + att = xml.ATTRIBUTE + + root = tree.getroot() + expr = "/%s/%s[@%s='org_organisation']/%s[@%s='name']" % \ + (tag.root, tag.resource, att.name, tag.data, att.field) + orgs = root.xpath(expr) + for org in orgs: + org_name = org.get("value", None) or org.text + if org_name: + try: + org_name = json.loads(xml.xml_decode(org_name)) + except: + pass + if org_name: + mtable = s3db.member_membership + otable = s3db.org_organisation + query = (otable.name == org_name) & \ + (mtable.organisation_id == otable.id) + resource = s3db.resource("member_membership", filter=query) + # Use cascade=True so that the deletion gets + # rolled back if the import fails: + resource.delete(format="xml", cascade=True) s3.import_prep = import_prep @@ -192,11 +189,11 @@ def prep(r): def postp(r, output): if r.interactive: - if not r.component and "buttons" in output: + if not r.component and isinstance(output, dict) and "buttons" in output: # Provide correct list-button (non-native controller) buttons = output["buttons"] if "list_btn" in buttons: - crud_button = r.resource.crud.crud_button + crud_button = s3base.S3CRUD.crud_button buttons["list_btn"] = crud_button(None, tablename="member_membership", name="label_list_button", @@ -205,10 +202,9 @@ def postp(r, output): return output s3.postp = postp - output = s3_rest_controller("pr", resourcename, - replace_option = T("Remove existing data before import"), - rheader = s3db.member_rheader, - ) - return output + return crud_controller("pr", resourcename, + replace_option = T("Remove existing data before import"), + rheader = s3db.member_rheader, + ) # END ========================================================================= diff --git a/controllers/mpr.py b/controllers/mpr.py index aad4bb806c..19e6d811b3 100644 --- a/controllers/mpr.py +++ b/controllers/mpr.py @@ -133,7 +133,7 @@ def postp(r, output): linkto = URL(f="person", args=("[id]", "note")) else: label = UPDATE - linkto = r.resource.crud._linkto(r)("[id]") + linkto = s3base.S3CRUD._linkto(r)("[id]") s3.actions = [action(label, linkto)] if not r.component: label = str(T("Found")) @@ -169,6 +169,6 @@ def postp(r, output): ] rheader = lambda r: s3db.pr_rheader(r, tabs=mpr_tabs) - return s3_rest_controller("pr", "person", rheader=rheader) + return crud_controller("pr", "person", rheader=rheader) # END ========================================================================= diff --git a/controllers/msg.py b/controllers/msg.py index 88326c013f..5ae38409d9 100644 --- a/controllers/msg.py +++ b/controllers/msg.py @@ -23,7 +23,7 @@ def index(): def basestation(): """ RESTful CRUD controller for Base Stations """ - return s3_rest_controller() + return crud_controller() # ============================================================================= def compose(): @@ -73,7 +73,7 @@ def postp(r, output): insertable = False, ) - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def contact(): @@ -87,7 +87,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # ============================================================================= def mark_sender(): @@ -169,7 +169,7 @@ def postp(r, output): insertable = False, ) - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def email_outbox(): @@ -222,7 +222,7 @@ def postp(r, output): ], ) - return s3_rest_controller(module, "email") + return crud_controller(module, "email") # ----------------------------------------------------------------------------- def facebook_outbox(): @@ -273,7 +273,7 @@ def facebook_outbox(): ], ) - return s3_rest_controller(module, "facebook") + return crud_controller(module, "facebook") # ----------------------------------------------------------------------------- def sms_outbox(): @@ -325,7 +325,7 @@ def postp(r, output): ], ) - return s3_rest_controller(module, "sms") + return crud_controller(module, "sms") # ----------------------------------------------------------------------------- def twitter_outbox(): @@ -377,7 +377,7 @@ def postp(r, output): ], ) - return s3_rest_controller(module, "twitter") + return crud_controller(module, "twitter") # ============================================================================= def inbox(): @@ -418,7 +418,7 @@ def inbox(): ], ) - return s3_rest_controller(module, "message") + return crud_controller(module, "message") # ----------------------------------------------------------------------------- def email_inbox(): @@ -480,7 +480,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller(module, "email") + return crud_controller(module, "email") # ============================================================================= def rss(): @@ -519,7 +519,7 @@ def rss(): ], ) - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def sms_inbox(): @@ -559,7 +559,7 @@ def sms_inbox(): ], ) - return s3_rest_controller(module, "sms") + return crud_controller(module, "sms") # ----------------------------------------------------------------------------- def twitter(): @@ -580,7 +580,7 @@ def twitter(): ], ) - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def twitter_inbox(): @@ -618,7 +618,7 @@ def twitter_inbox(): ], ) - return s3_rest_controller(module, "twitter") + return crud_controller(module, "twitter") # ============================================================================= def tropo(): @@ -701,7 +701,7 @@ def sms_outbound_gateway(): msg_record_deleted = T("SMS Outbound Gateway deleted"), msg_list_empty = T("No SMS Outbound Gateways currently registered")) - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def channel(): @@ -710,7 +710,7 @@ def channel(): - unused """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def email_channel(): @@ -787,7 +787,7 @@ def postp(r, output): return output s3.postp = postp - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def facebook_channel(): @@ -850,7 +850,7 @@ def postp(r, output): return output s3.postp = postp - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def mcommons_channel(): @@ -930,7 +930,7 @@ def postp(r, output): return output s3.postp = postp - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def gcm_channel(): @@ -1000,7 +1000,7 @@ def postp(r, output): return output s3.postp = postp - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def rss_channel(): @@ -1082,7 +1082,7 @@ def postp(r, output): return output s3.postp = postp - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def twilio_channel(): @@ -1157,7 +1157,7 @@ def postp(r, output): return output s3.postp = postp - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- @auth.s3_requires_membership(1) @@ -1202,7 +1202,7 @@ def sms_modem_channel(): msg_record_deleted = T("Modem Channel deleted"), msg_list_empty = T("No Modem Channels currently defined")) - return s3_rest_controller() + return crud_controller() #------------------------------------------------------------------------------ @auth.s3_requires_membership(1) @@ -1244,7 +1244,7 @@ def sms_smtp_channel(): s3db.configure(tablename, update_next = URL(args=[1, "update"])) - return s3_rest_controller() + return crud_controller() #------------------------------------------------------------------------------ @auth.s3_requires_membership(1) @@ -1298,7 +1298,7 @@ def sms_webapi_channel(): msg_record_deleted=T("Web API Channel deleted"), msg_list_empty=T("No Web API Channels currently registered")) - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- @auth.s3_requires_membership(1) @@ -1330,7 +1330,7 @@ def tropo_channel(): msg_record_deleted=T("Tropo Channel deleted"), msg_list_empty=T("No Tropo Channels currently registered")) - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- @auth.s3_requires_membership(1) @@ -1439,7 +1439,7 @@ def postp(r, output): return output s3.postp = postp - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def inject_search_after_save(output): @@ -1622,7 +1622,7 @@ def postp(r, output): return output s3.postp = postp - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def twitter_result(): @@ -1715,7 +1715,7 @@ def postp(r, output): s3.postp = postp - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def sender(): @@ -1751,13 +1751,13 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def keyword(): """ REST Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def parser(): @@ -1851,7 +1851,7 @@ def postp(r, output): return output s3.postp = postp - return s3_rest_controller() + return crud_controller() # ============================================================================= # The following functions hook into the pr functions: @@ -1875,9 +1875,9 @@ def group(): # Do not show system groups s3.filter = (table.system == False) - return s3_rest_controller(module, resourcename, - rheader = s3db.pr_rheader, - ) + return crud_controller(module, resourcename, + rheader = s3db.pr_rheader, + ) # ----------------------------------------------------------------------------- def group_membership(): @@ -1895,7 +1895,7 @@ def group_membership(): table.comments.readable = table.comments.writable = False table.group_head.readable = table.group_head.writable = False - return s3_rest_controller("pr", resourcename) + return crud_controller("pr", resourcename) # ----------------------------------------------------------------------------- def contacts(): @@ -1943,7 +1943,7 @@ def prep(r): response.menu_options = [] - return s3_rest_controller("pr", "contact") + return crud_controller("pr", "contact") # ----------------------------------------------------------------------------- def search(): @@ -2055,7 +2055,7 @@ def person_search(value, type=None): def subscription(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- # Send Outbound Messages (was for being called via cron, now useful for debugging) @@ -2246,7 +2246,7 @@ def twitter_post(): def tag(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ============================================================================= # Enabled only for testing: diff --git a/controllers/org.py b/controllers/org.py index 6dd139b282..f6e2f7a133 100644 --- a/controllers/org.py +++ b/controllers/org.py @@ -77,7 +77,7 @@ def capacity_assessment(): #subheadings = subheadings, ) - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def capacity_assessment_data(): @@ -86,13 +86,13 @@ def capacity_assessment_data(): - just used for the custom_report method """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def capacity_indicator(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def group(): @@ -104,7 +104,7 @@ def group(): URL(c="hrm", f="group", args=[record_id]), ) - return s3_rest_controller(rheader = s3db.org_rheader) + return crud_controller(rheader=s3db.org_rheader) # ----------------------------------------------------------------------------- def group_membership(): @@ -120,13 +120,13 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def group_membership_status(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def group_person(): @@ -134,13 +134,13 @@ def group_person(): s3.prep = lambda r: r.representation == "s3json" and r.method == "options" - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def group_person_status(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def facility(): @@ -152,7 +152,7 @@ def facility(): def facility_type(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def office(): @@ -165,7 +165,7 @@ def office(): def office_type(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def organisation(): @@ -178,7 +178,7 @@ def organisation(): def organisation_type(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def organisation_organisation_type(): @@ -186,7 +186,7 @@ def organisation_organisation_type(): s3.prep = lambda r: r.representation == "s3json" and r.method == "options" - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def org_search(): @@ -198,7 +198,7 @@ def org_search(): s3.prep = lambda r: r.method == "search_ac" - return s3_rest_controller(module, "organisation") + return crud_controller(module, "organisation") # ----------------------------------------------------------------------------- def organisation_list_represent(l): @@ -234,7 +234,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def sector(): @@ -247,13 +247,13 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def subsector(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def site(): @@ -276,7 +276,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def sites_for_org(): @@ -331,7 +331,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller("pr", "person") + return crud_controller("pr", "person") # ----------------------------------------------------------------------------- def room(): @@ -352,7 +352,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def mailing_list(): @@ -391,9 +391,7 @@ def mailing_list(): rheader = lambda r: _rheader(r, tabs = _tabs) - return s3_rest_controller("pr", "group", - rheader = rheader, - ) + return crud_controller("pr", "group", rheader=rheader) # ----------------------------------------------------------------------------- def donor(): @@ -419,14 +417,13 @@ def donor(): listadd = False, ) - output = s3_rest_controller() - return output + return crud_controller() # ----------------------------------------------------------------------------- #def organisation_location(): # """ RESTful CRUD controller """ -# return s3_rest_controller() +# return crud_controller() # ----------------------------------------------------------------------------- def resource(): @@ -451,43 +448,43 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def resource_type(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def service(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def service_location(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def service_mode(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def booking_mode(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def site_location(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def req_match(): diff --git a/controllers/patient.py b/controllers/patient.py index 3b582e3855..a29d1e1167 100644 --- a/controllers/patient.py +++ b/controllers/patient.py @@ -30,7 +30,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller("pr", "person") + return crud_controller("pr", "person") # ----------------------------------------------------------------------------- def patient(): @@ -61,10 +61,9 @@ def postp(r, output): tabs = [(T("Basic Details"), None), (T("Accompanying Relative"), "relative"), (T("Home"), "home")] - rheader = lambda r: patient_rheader(r, tabs=tabs) - output = s3_rest_controller(rheader = rheader) - return output + rheader = lambda r: patient_rheader(r, tabs=tabs) + return crud_controller(rheader=rheader) # ----------------------------------------------------------------------------- def patient_rheader(r, tabs=[]): diff --git a/controllers/pr.py b/controllers/pr.py index eb558a731c..a82d80f814 100755 --- a/controllers/pr.py +++ b/controllers/pr.py @@ -94,27 +94,26 @@ def prep(r): # Contacts Tabs contacts_tabs = [] - resourcename = request.function set_method = s3db.set_method setting = settings.get_pr_contacts_tabs() if "all" in setting: - s3db.set_method(module, resourcename, - method = "contacts", - action = s3db.pr_Contacts) + set_method("pr_person", + method = "contacts", + action = s3db.pr_Contacts) contacts_tabs.append((settings.get_pr_contacts_tab_label("all"), "contacts", )) if "public" in setting: - s3db.set_method(module, resourcename, - method = "public_contacts", - action = s3db.pr_Contacts) + set_method("pr_person", + method = "public_contacts", + action = s3db.pr_Contacts) contacts_tabs.append((settings.get_pr_contacts_tab_label("public_contacts"), "public_contacts", )) if "private" in setting and auth.is_logged_in(): - s3db.set_method(module, resourcename, - method = "private_contacts", - action = s3db.pr_Contacts) + set_method("pr_person", + method = "private_contacts", + action = s3db.pr_Contacts) contacts_tabs.append((settings.get_pr_contacts_tab_label("private_contacts"), "private_contacts", )) @@ -141,12 +140,10 @@ def prep(r): listadd = False, ) - output = s3_rest_controller(main = "first_name", - extra = "last_name", - rheader = lambda r: \ - s3db.pr_rheader(r, tabs=tabs)) - - return output + return crud_controller(main = "first_name", + extra = "last_name", + rheader = lambda r: s3db.pr_rheader(r, tabs=tabs), + ) # ----------------------------------------------------------------------------- def address(): @@ -201,8 +198,7 @@ def prep(r): return True s3.prep = prep - output = s3_rest_controller() - return output + return crud_controller() # ----------------------------------------------------------------------------- def contact(): @@ -271,7 +267,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def contact_emergency(): @@ -321,8 +317,7 @@ def prep(r): return True s3.prep = prep - output = s3_rest_controller() - return output + return crud_controller() # ----------------------------------------------------------------------------- def person_search(): @@ -333,7 +328,7 @@ def person_search(): """ s3.prep = lambda r: r.method == "search_ac" - return s3_rest_controller(module, "person") + return crud_controller(module, "person") # ----------------------------------------------------------------------------- def forum(): @@ -373,17 +368,14 @@ def prep(r): return True s3.prep = prep - output = s3_rest_controller(rheader = s3db.pr_rheader) - return output + return crud_controller(rheader = s3db.pr_rheader) # ----------------------------------------------------------------------------- #def forum_membership(): # """ RESTful CRUD controller """ # -# output = s3_rest_controller() +# return crud_controller() # -# return output - # ----------------------------------------------------------------------------- def group(): """ RESTful CRUD controller """ @@ -408,27 +400,25 @@ def group(): (T("Members"), "group_membership") ]) - output = s3_rest_controller(rheader = rheader) - - return output + return crud_controller(rheader = rheader) # ----------------------------------------------------------------------------- def group_member_role(): """ Group Member Roles: RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def group_status(): """ Group Statuses: RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def image(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def education(): @@ -446,13 +436,13 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def education_level(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def language(): @@ -470,19 +460,19 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def occupation_type(): """ Occupation Types: RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def religion(): """ Religions: RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- #def contact(): @@ -494,7 +484,7 @@ def religion(): # table.pe_id.readable = True # table.pe_id.writable = True # -# return s3_rest_controller() +# return crud_controller() # ----------------------------------------------------------------------------- def presence(): @@ -516,7 +506,7 @@ def presence(): table.presence_condition.readable = False # @ToDo: Add Skills - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def pentity(): @@ -526,37 +516,37 @@ def pentity(): """ s3.prep = lambda r: r.method == "search_ac" - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def affiliation(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def role(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def slot(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def date_formula(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def time_formula(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def tooltip(): @@ -591,8 +581,7 @@ def postp(r, output): return output s3.postp = postp - output = s3_rest_controller() - return output + return crud_controller() # ============================================================================= def subscription(): @@ -601,8 +590,7 @@ def subscription(): - to allow Admins to control subscriptions for people """ - output = s3_rest_controller() - return output + return crud_controller() # ============================================================================= def human_resource(): @@ -622,7 +610,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller("hrm", "human_resource") + return crud_controller("hrm", "human_resource") # ============================================================================= # Messaging diff --git a/controllers/proc.py b/controllers/proc.py index dc3e714ff7..1276c0ed55 100644 --- a/controllers/proc.py +++ b/controllers/proc.py @@ -30,28 +30,28 @@ def index(): def order(): """ RESTful CRUD controller """ - return s3_rest_controller(rheader = s3db.proc_rheader, - hide_filter = True, - ) + return crud_controller(rheader = s3db.proc_rheader, + hide_filter = True, + ) # ----------------------------------------------------------------------------- #def order_item(): # """ RESTful CRUD controller """ -# return s3_rest_controller() +# return crud_controller() # ----------------------------------------------------------------------------- def plan(): """ RESTful CRUD controller """ - return s3_rest_controller(rheader = s3db.proc_rheader, - hide_filter = True, - ) + return crud_controller(rheader = s3db.proc_rheader, + hide_filter = True, + ) # ----------------------------------------------------------------------------- def supplier(): """ RESTful CRUD controller """ - return s3_rest_controller("org", "organisation") + return crud_controller("org", "organisation") # END ========================================================================= diff --git a/controllers/project.py b/controllers/project.py index b7568190a8..2396b8c2c7 100644 --- a/controllers/project.py +++ b/controllers/project.py @@ -491,14 +491,14 @@ def postp(r, output): return output s3.postp = postp - return s3_rest_controller(module, "project", - csv_template = "project", - hide_filter = {None: False, - #"indicator_data": False, - "_default": True, - }, - rheader = s3db.project_rheader, - ) + return crud_controller(module, "project", + csv_template = "project", + hide_filter = {None: False, + #"indicator_data": False, + "_default": True, + }, + rheader = s3db.project_rheader, + ) # ----------------------------------------------------------------------------- def open_tasks_for_project(): @@ -529,9 +529,7 @@ def postp(r, output): return output s3.postp = postp - return s3_rest_controller(module, "project", - hide_filter = False, - ) + return crud_controller(module, "project", hide_filter=False) # ----------------------------------------------------------------------------- def set_theme_requires(sector_ids): @@ -593,19 +591,19 @@ def set_activity_type_requires(tablename, sector_ids): def sector(): """ RESTful CRUD controller """ - return s3_rest_controller("org", "sector") + return crud_controller("org", "sector") # ----------------------------------------------------------------------------- def status(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def theme(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def theme_project(): @@ -614,7 +612,7 @@ def theme_project(): - not normally exposed to users via a menu """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def theme_sector(): @@ -630,21 +628,21 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def hazard(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def framework(): """ RESTful CRUD controller """ - return s3_rest_controller(dtargs = {"dt_text_maximum_len": 160}, - hide_filter = True, - ) + return crud_controller(dtargs = {"dt_text_maximum_len": 160}, + hide_filter = True, + ) # ============================================================================= def organisation(): @@ -663,8 +661,8 @@ def organisation(): # args="report", vars=get_vars), # _class="action-btn") - return s3_rest_controller(#list_btn=list_btn, - ) + return crud_controller(#list_btn=list_btn, + ) else: # e.g. DRRPP @@ -673,15 +671,13 @@ def organisation(): (T("Contacts"), "human_resource"), ] rheader = lambda r: s3db.org_rheader(r, tabs) - return s3_rest_controller("org", resourcename, - rheader = rheader, - ) + return crud_controller("org", resourcename, rheader=rheader) # ============================================================================= def beneficiary_type(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def beneficiary(): @@ -715,14 +711,13 @@ def beneficiary(): # return True #s3.prep = prep - return s3_rest_controller(hide_filter = False, - ) + return crud_controller(hide_filter=False) # ============================================================================= def activity_type(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def activity_type_sector(): @@ -738,7 +733,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def activity_organisation(): @@ -754,7 +749,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller(module, "activity_organisation") + return crud_controller(module, "activity_organisation") # ----------------------------------------------------------------------------- def activity(): @@ -796,11 +791,11 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller("project", "activity", - csv_template = "activity", - #hide_filter = False, - rheader = s3db.project_rheader, - ) + return crud_controller("project", "activity", + csv_template = "activity", + #hide_filter = False, + rheader = s3db.project_rheader, + ) # ----------------------------------------------------------------------------- def distribution(): @@ -922,17 +917,17 @@ def postp(r, output): return output s3.postp = postp - return s3_rest_controller(interactive_report = True, - csv_template = "location", - hide_filter = False, - rheader = s3db.project_rheader, - ) + return crud_controller(interactive_report = True, + csv_template = "location", + hide_filter = False, + rheader = s3db.project_rheader, + ) # ----------------------------------------------------------------------------- def demographic(): """ RESTful CRUD controller """ - return s3_rest_controller("stats", "demographic") + return crud_controller("stats", "demographic") # ----------------------------------------------------------------------------- def demographic_data(): @@ -944,8 +939,7 @@ def demographic_data(): def location_contact(): """ RESTful CRUD controller for Community Contacts """ - return s3_rest_controller(hide_filter = False, - ) + return crud_controller(hide_filter=False) # ----------------------------------------------------------------------------- def report(): @@ -955,7 +949,7 @@ def report(): @ToDo: Why is this needed? To have no rheader? """ - return s3_rest_controller(module, "activity") + return crud_controller(module, "activity") # ----------------------------------------------------------------------------- def partners(): @@ -1009,7 +1003,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # ============================================================================= def task_activity(): @@ -1025,7 +1019,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # ============================================================================= def task_milestone(): @@ -1041,7 +1035,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # ============================================================================= def task_tag(): @@ -1054,19 +1048,19 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # ============================================================================= def role(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ============================================================================= def member(): """ RESTful CRUD Controller """ - return s3_rest_controller() + return crud_controller() # ============================================================================= def milestone(): @@ -1078,13 +1072,13 @@ def milestone(): field.writable = False field.comment = None - return s3_rest_controller() + return crud_controller() # ============================================================================= def tag(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ============================================================================= def time(): @@ -1146,8 +1140,7 @@ def time(): delta = month * months s3.filter = (table.date > (now - delta)) - return s3_rest_controller(hide_filter = hide_filter, - ) + return crud_controller(hide_filter=hide_filter) # ============================================================================= # Programmes @@ -1155,20 +1148,20 @@ def time(): def programme(): """ RESTful controller for Programmes """ - return s3_rest_controller() + return crud_controller() def programme_project(): """ RESTful controller for Programmes <> Projects """ s3.prep = lambda r: r.method == "options" and r.representation == "s3json" - return s3_rest_controller() + return crud_controller() # ============================================================================= def strategy(): """ RESTful controller for Strategies """ - return s3_rest_controller() + return crud_controller() # ============================================================================= # Planning @@ -1176,17 +1169,17 @@ def strategy(): def goal(): """ RESTful controller for Goals """ - return s3_rest_controller() + return crud_controller() def outcome(): """ RESTful controller for Outcomes """ - return s3_rest_controller() + return crud_controller() def output(): """ RESTful controller for Outputs """ - return s3_rest_controller() + return crud_controller() def indicator(): """ RESTful CRUD controller """ @@ -1264,12 +1257,12 @@ def dt_row_actions(component): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() def indicator_data(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() def person(): """ RESTful controller for Community Volunteers """ @@ -1290,7 +1283,7 @@ def volunteer(): def window(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ============================================================================= # Comments @@ -1378,15 +1371,15 @@ def comments(): field.default = task_id field.writable = field.readable = False - # Create S3Request for S3SQLForm - r = s3_request(prefix = "project", - name = "comment", - # Override task_id - args = [], - vars = None, - # Override .loads - extension = "html", - ) + # Create CRUDRequest for S3SQLForm + r = crud_request(prefix = "project", + name = "comment", + # Override task_id + args = [], + vars = None, + # Override .loads + extension = "html", + ) # Customise resource r.customise_resource() @@ -1436,7 +1429,7 @@ def comments(): def comment(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ============================================================================= # Campaigns @@ -1444,31 +1437,31 @@ def comment(): def campaign(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def campaign_keyword(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def campaign_message(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def campaign_response(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def campaign_response_summary(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def human_resource_project(): @@ -1486,6 +1479,6 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # END ========================================================================= diff --git a/controllers/req.py b/controllers/req.py index ee3dadb86a..aab06315e3 100644 --- a/controllers/req.py +++ b/controllers/req.py @@ -941,9 +941,7 @@ def postp(r, output): return output s3.postp = postp - return s3_rest_controller("req", "req", - rheader = s3db.req_rheader, - ) + return crud_controller("req", "req", rheader=s3db.req_rheader) # ============================================================================= def req_item(): @@ -977,7 +975,7 @@ def order_item(r, **attr): args = [req_id, "req_item"] )) - s3db.set_method("req", "req_item", + s3db.set_method("req_req_item", method = "order", action = order_item ) @@ -1008,7 +1006,7 @@ def prep(r): return True s3.prep = prep - output = s3_rest_controller("req", "req_item") + output = crud_controller("req", "req_item") if settings.get_req_prompt_match(): req_item_inv_item_btn = {"label": s3_str(T("Request from Facility")), @@ -1128,7 +1126,7 @@ def req_item_inv_item(): # Tweak CRUD String for this context s3.crud_strings["inv_inv_item"].msg_list_empty = T("No Inventories currently have this item in stock") - inv_items = s3_rest_controller("inv", "inv_item") + inv_items = crud_controller("inv", "inv_item") output["items"] = inv_items["items"] if settings.get_supply_use_alt_name(): @@ -1141,7 +1139,7 @@ def req_item_inv_item(): if alt_item_ids: s3.filter = (itable.item_id.belongs(alt_item_ids)) - inv_items_alt = s3_rest_controller("inv", "inv_item") + inv_items_alt = crud_controller("inv", "inv_item") output["items_alt"] = inv_items_alt["items"] else: output["items_alt"] = T("No Inventories currently have suitable alternative items in stock") @@ -1220,7 +1218,7 @@ def postp(r, output): return output s3.postp = postp - return s3_rest_controller("req", "req_skill") + return crud_controller("req", "req_skill") # ============================================================================= def skills_filter(req_id): @@ -1462,7 +1460,7 @@ def postp(r, output): return output s3.postp = postp - return s3_rest_controller(rheader = commit_rheader) + return crud_controller(rheader=commit_rheader) # ----------------------------------------------------------------------------- def commit_rheader(r): @@ -1614,7 +1612,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # ============================================================================= def commit_req(): @@ -1958,7 +1956,7 @@ def approver(): #if not auth.s3_has_role("ADMIN"): # s3.filter = auth.filter_by_root_org(s3db.req_approver) - return s3_rest_controller() + return crud_controller() # ============================================================================= def fema(): @@ -2016,7 +2014,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller(rheader = s3db.req_rheader) + return crud_controller(rheader=s3db.req_rheader) # ----------------------------------------------------------------------------- def need_line(): @@ -2024,7 +2022,7 @@ def need_line(): RESTful CRUD Controller for Need Lines """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def need_response(): @@ -2032,7 +2030,7 @@ def need_response(): RESTful CRUD Controller for Need Responses (i.e. Activity Groups) """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def need_response_line(): @@ -2040,7 +2038,7 @@ def need_response_line(): RESTful CRUD Controller for Need Response Lines (i.e. Activities) """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def order_item(): @@ -2048,7 +2046,7 @@ def order_item(): RESTful CRUD Controller for Order Items """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def facility(): @@ -2075,6 +2073,6 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # END ========================================================================= diff --git a/controllers/security.py b/controllers/security.py index 28e7e2751a..b6442c7907 100644 --- a/controllers/security.py +++ b/controllers/security.py @@ -19,19 +19,19 @@ def index(): def level(): """ Security Level Assessments: RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def zone(): """ Security Zones: RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def zone_type(): """ Security Zone Types: RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def person(): @@ -40,7 +40,7 @@ def person(): authorisation and configuration """ - return s3_rest_controller("pr") + return crud_controller("pr") # ----------------------------------------------------------------------------- def person_search(): @@ -49,19 +49,19 @@ def person_search(): """ s3.prep = lambda r: r.method == "search_ac" - return s3_rest_controller("pr", "person") + return crud_controller("pr", "person") # ----------------------------------------------------------------------------- def staff(): """ Security Staff Assignments: RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def staff_type(): """ Security Staff Types (roles): RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def essential(): @@ -70,24 +70,24 @@ def essential(): table = s3db.hrm_human_resource s3.filter = (table.essential == True) - return s3_rest_controller("hrm", "human_resource") + return crud_controller("hrm", "human_resource") # ----------------------------------------------------------------------------- def seized_item_type(): """ Seized item types: RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def seized_item_depository(): """ Seized item depositories: RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def seized_item(): """ Seized items: RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # END ========================================================================= diff --git a/controllers/setup.py b/controllers/setup.py index d4e1e8c4cc..a0c6a68f38 100644 --- a/controllers/setup.py +++ b/controllers/setup.py @@ -69,38 +69,38 @@ def index(): # ----------------------------------------------------------------------------- def aws_cloud(): - return s3_rest_controller(#rheader = s3db.setup_rheader, - ) + return crud_controller(#rheader = s3db.setup_rheader, + ) # ----------------------------------------------------------------------------- def openstack_cloud(): - return s3_rest_controller(#rheader = s3db.setup_rheader, - ) + return crud_controller(#rheader = s3db.setup_rheader, + ) # ----------------------------------------------------------------------------- def gandi_dns(): - return s3_rest_controller(#rheader = s3db.setup_rheader, - ) + return crud_controller(#rheader = s3db.setup_rheader, + ) # ----------------------------------------------------------------------------- def godaddy_dns(): - return s3_rest_controller(#rheader = s3db.setup_rheader, - ) + return crud_controller(#rheader = s3db.setup_rheader, + ) # ----------------------------------------------------------------------------- def smtp(): - return s3_rest_controller(#rheader = s3db.setup_rheader, - ) + return crud_controller(#rheader = s3db.setup_rheader, + ) # ----------------------------------------------------------------------------- def google_email(): - return s3_rest_controller(#rheader = s3db.setup_rheader, - ) + return crud_controller(#rheader = s3db.setup_rheader, + ) # ----------------------------------------------------------------------------- def deployment(): @@ -542,8 +542,7 @@ def postp(r, output): return output s3.postp = postp - return s3_rest_controller(rheader = s3db.setup_rheader, - ) + return crud_controller(rheader=s3db.setup_rheader) # ----------------------------------------------------------------------------- def server(): @@ -653,20 +652,19 @@ def postp(r, output): return output s3.postp = postp - return s3_rest_controller(rheader = s3db.setup_rheader, - ) + return crud_controller(rheader=s3db.setup_rheader) # ----------------------------------------------------------------------------- #def instance(): -# return s3_rest_controller(#rheader = s3db.setup_rheader, -# ) +# return crud_controller(#rheader = s3db.setup_rheader, +# ) # ----------------------------------------------------------------------------- #def setting(): -# return s3_rest_controller(#rheader = s3db.setup_rheader, -# ) +# return crud_controller(#rheader = s3db.setup_rheader, +# ) # ----------------------------------------------------------------------------- def monitor_check(): @@ -699,8 +697,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller(rheader = s3db.setup_rheader, - ) + return crud_controller(rheader=s3db.setup_rheader) # ----------------------------------------------------------------------------- def monitor_run(): @@ -708,7 +705,7 @@ def monitor_run(): Logs """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def monitor_task(): @@ -768,7 +765,6 @@ def postp(r, output): return output s3.postp = postp - return s3_rest_controller(rheader = s3db.setup_rheader, - ) + return crud_controller(rheader=s3db.setup_rheader) # END ========================================================================= diff --git a/controllers/stats.py b/controllers/stats.py index 0c316ec333..1cc429dd83 100644 --- a/controllers/stats.py +++ b/controllers/stats.py @@ -29,31 +29,31 @@ def index_alt(): def parameter(): """ REST Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def data(): """ REST Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def source(): """ REST Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def demographic(): """ REST Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def demographic_data(): """ REST Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def demographic_aggregate(): @@ -68,41 +68,40 @@ def clear_aggregates(r, **attr): args="", )) - s3db.set_method("stats", "demographic_aggregate", + s3db.set_method("stats_demographic_aggregate", method="clear", action=clear_aggregates) - output = s3_rest_controller() - return output + return crud_controller() # ----------------------------------------------------------------------------- def people_type(): """ REST Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def people(): """ REST Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def trained_type(): """ REST Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def trained(): """ REST Controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def impact_type(): """ REST Controller for impact types """ - return s3_rest_controller() + return crud_controller() # END ========================================================================= diff --git a/controllers/supply.py b/controllers/supply.py index 34efb15d0c..3b543df84b 100644 --- a/controllers/supply.py +++ b/controllers/supply.py @@ -26,19 +26,19 @@ def index(): def brand(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def catalog(): """ RESTful CRUD controller """ - return s3_rest_controller(rheader=s3db.supply_catalog_rheader) + return crud_controller(rheader=s3db.supply_catalog_rheader) # ----------------------------------------------------------------------------- def catalog_item(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def distribution_rheader(r): @@ -90,7 +90,7 @@ def distribution(): # return True #s3.prep = prep - return s3_rest_controller(rheader = distribution_rheader) + return crud_controller(rheader=distribution_rheader) # ----------------------------------------------------------------------------- def distribution_report(): @@ -104,13 +104,13 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller("supply", "distribution") + return crud_controller("supply", "distribution") # ----------------------------------------------------------------------------- def distribution_item(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def item(): @@ -145,7 +145,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def item_entity(): @@ -162,24 +162,24 @@ def item_pack(): listadd = False, ) - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def kit_item(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def person_item(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def person_item_status(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # END ========================================================================= diff --git a/controllers/survey.py b/controllers/survey.py index 70854f86b9..e7e9a824f3 100644 --- a/controllers/survey.py +++ b/controllers/survey.py @@ -149,8 +149,7 @@ def postp(r, output): #deletable=False, ) - output = s3_rest_controller(rheader=s3db.survey_template_rheader) - return output + return crud_controller(rheader=s3db.survey_template_rheader) # ----------------------------------------------------------------------------- def template_read(): @@ -181,7 +180,7 @@ def postp(r, output): deletable=False, ) - r = s3_request("survey", "template", args=[template_id]) + r = crud_request("survey", "template", args=[template_id]) output = r(method="read", rheader=s3db.survey_template_rheader) return output @@ -217,10 +216,10 @@ def postp(r, output): deletable=False, ) - output = s3_rest_controller("survey", "template", - method = "list", - rheader=s3db.survey_template_rheader, - ) + output = crud_controller("survey", "template", + method = "list", + rheader=s3db.survey_template_rheader, + ) s3.actions = None return output @@ -290,8 +289,7 @@ def postp(r, output): deletable = False, ) - output = s3_rest_controller(rheader=s3db.survey_series_rheader) - return output + return crud_controller(rheader=s3db.survey_series_rheader) # ----------------------------------------------------------------------------- def series_export_formatted(): @@ -303,9 +301,9 @@ def series_export_formatted(): try: series_id = request.args[0] except: - output = s3_rest_controller(module, "series", - rheader = s3db.survey_series_rheader) - return output + return crud_controller(module, "series", + rheader = s3db.survey_series_rheader, + ) # Load Model table = s3db.survey_series @@ -372,9 +370,9 @@ def series_export_formatted(): content_type = ".rtf" else: - output = s3_rest_controller(module, "series", - rheader = s3db.survey_series_rheader) - return output + return crud_controller(module, "series", + rheader = s3db.survey_series_rheader, + ) from gluon.contenttype import contenttype @@ -508,9 +506,9 @@ def series_export_spreadsheet(matrix, matrix_answers, logo): import xlwt except ImportError: response.error = T("xlwt not installed, so cannot export as a Spreadsheet") - output = s3_rest_controller(module, "survey_series", - rheader=s3db.survey_series_rheader) - return output + return crud_controller(module, "survey_series", + rheader = s3db.survey_series_rheader, + ) import math from io import BytesIO @@ -863,10 +861,9 @@ def postp(r, output): return output #s3.postp = postp - output = s3_rest_controller(# Undefined - #rheader=s3db.survey_section_rheader - ) - return output + return crud_controller(# Undefined + #rheader=s3db.survey_section_rheader + ) # ----------------------------------------------------------------------------- def question(): @@ -879,31 +876,27 @@ def prep(r): return True s3.prep = prep - output = s3_rest_controller(# Undefined - #rheader=s3db.survey_section_rheader - ) - return output + return crud_controller(# Undefined + #rheader=s3db.survey_section_rheader + ) # ----------------------------------------------------------------------------- def question_list(): """ RESTful CRUD controller """ - output = s3_rest_controller() - return output + return crud_controller() # ----------------------------------------------------------------------------- def formatter(): """ RESTful CRUD controller """ - output = s3_rest_controller() - return output + return crud_controller() # ----------------------------------------------------------------------------- def question_metadata(): """ RESTful CRUD controller """ - output = s3_rest_controller() - return output + return crud_controller() # ----------------------------------------------------------------------------- def new_assessment(): @@ -978,11 +971,10 @@ def postp(r, output): return output s3.postp = postp - output = s3_rest_controller(module, "complete", - method = "create", - rheader = s3db.survey_series_rheader - ) - return output + return crud_controller(module, "complete", + method = "create", + rheader = s3db.survey_series_rheader + ) # ----------------------------------------------------------------------------- def complete(): @@ -1119,15 +1111,13 @@ def import_xls(uploadFile): s3.xls_parser = import_xls - output = s3_rest_controller(csv_extra_fields = csv_extra_fields) - return output + return crud_controller(csv_extra_fields=csv_extra_fields) # ----------------------------------------------------------------------------- def answer(): """ RESTful CRUD controller """ - output = s3_rest_controller() - return output + return crud_controller() # ----------------------------------------------------------------------------- def analysis(): @@ -1142,8 +1132,7 @@ def analysis(): listadd = False, ) - output = s3_rest_controller(module, "complete") - return output + return crud_controller(module, "complete") # ----------------------------------------------------------------------------- def admin(): diff --git a/controllers/sync.py b/controllers/sync.py index c873479441..71b7c99690 100755 --- a/controllers/sync.py +++ b/controllers/sync.py @@ -31,14 +31,14 @@ def postp(r, output): s3.postp = postp # Can't do anything else than update here - r = s3_request(args=[str(record_id), "update"], extension="html") + r = crud_request(args=[str(record_id), "update"], extension="html") return r() # ----------------------------------------------------------------------------- def repository(): """ Repository Management Controller """ - s3db.set_method("sync", "repository", + s3db.set_method("sync_repository", method = "register", action = current.sync, ) @@ -203,7 +203,7 @@ def postp(r, output): return output s3.postp = postp - return s3_rest_controller("sync", "repository", rheader=s3db.sync_rheader) + return crud_controller("sync", "repository", rheader=s3db.sync_rheader) # ----------------------------------------------------------------------------- def dataset(): @@ -268,7 +268,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller(rheader = s3db.sync_rheader) + return crud_controller(rheader=s3db.sync_rheader) # ----------------------------------------------------------------------------- def sync(): @@ -296,11 +296,11 @@ def sync(): # Request prefix, name = tablename.split("_", 1) - r = s3_request(prefix = prefix, - name = name, - args = ["sync"], - get_vars = get_vars_new, - ) + r = crud_request(prefix = prefix, + name = name, + args = ["sync"], + get_vars = get_vars_new, + ) # Response return r(mixed=mixed) @@ -328,11 +328,11 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller("sync", "log", - subtitle=None, - rheader=s3base.S3SyncLog.rheader, - list_btn=list_btn, - ) + return crud_controller("sync", "log", + subtitle = None, + rheader = s3base.S3SyncLog.rheader, + list_btn = list_btn, + ) # ----------------------------------------------------------------------------- def task(): @@ -351,7 +351,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def resource_filter(): @@ -370,6 +370,6 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller() + return crud_controller() # END ========================================================================= diff --git a/controllers/transport.py b/controllers/transport.py index 12dedeb418..67bf47d777 100644 --- a/controllers/transport.py +++ b/controllers/transport.py @@ -42,13 +42,13 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller(rheader=s3db.transport_rheader) + return crud_controller(rheader=s3db.transport_rheader) # ----------------------------------------------------------------------------- def border_crossing(): """ RESTful CRUD controller """ - return s3_rest_controller(rheader=s3db.transport_rheader) + return crud_controller(rheader=s3db.transport_rheader) # ----------------------------------------------------------------------------- def border_control_point(): @@ -77,7 +77,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller(rheader=s3db.transport_rheader) + return crud_controller(rheader=s3db.transport_rheader) # ----------------------------------------------------------------------------- def heliport(): @@ -106,7 +106,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller(rheader=s3db.transport_rheader) + return crud_controller(rheader=s3db.transport_rheader) # ----------------------------------------------------------------------------- def seaport(): @@ -135,7 +135,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller(rheader=s3db.transport_rheader) + return crud_controller(rheader=s3db.transport_rheader) # ----------------------------------------------------------------------------- def incoming(): diff --git a/controllers/vehicle.py b/controllers/vehicle.py index d61ba57247..da4210f3fb 100644 --- a/controllers/vehicle.py +++ b/controllers/vehicle.py @@ -46,13 +46,13 @@ def vehicle(): set_method = s3db.set_method - set_method("asset", "asset", method="assign", + set_method("asset_asset", method="assign", action = s3db.hrm_AssignMethod(component="human_resource")) - set_method("asset", "asset", method="check-in", + set_method("asset_asset", method="check-in", action = s3base.S3CheckInMethod()) - set_method("asset", "asset", method="check-out", + set_method("asset_asset", method="check-out", action = s3base.S3CheckOutMethod()) # Type is Vehicle @@ -120,7 +120,7 @@ def vehicle(): def vehicle_type(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ============================================================================= def item(): @@ -186,6 +186,6 @@ def item_category(): field.readable = field.writable = False field.default = True - return s3_rest_controller("supply", "item_category") + return crud_controller("supply", "item_category") # END ========================================================================= diff --git a/controllers/vol.py b/controllers/vol.py index 4d6559609f..b487963cb0 100644 --- a/controllers/vol.py +++ b/controllers/vol.py @@ -38,7 +38,7 @@ def human_resource(): """ # Custom method for Service Record - s3db.set_method("hrm", "human_resource", + s3db.set_method("hrm_human_resource", method = "form", action = s3db.vol_service_record, ) @@ -70,7 +70,7 @@ def hr_search(): # Only allow use in the search_ac method s3.prep = lambda r: r.method == "search_ac" - return s3_rest_controller("hrm", "human_resource") + return crud_controller("hrm", "human_resource") # ----------------------------------------------------------------------------- def person_search(): @@ -86,7 +86,7 @@ def person_search(): # Only allow use in the search_ac method s3.prep = lambda r: r.method == "search_ac" - return s3_rest_controller("pr", "person") + return crud_controller("pr", "person") # ============================================================================= # Teams @@ -144,10 +144,10 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller("pr", "group_membership", - csv_stylesheet = ("hrm", "group_membership.xsl"), - csv_template = ("hrm", "group_membership"), - ) + return crud_controller("pr", "group_membership", + csv_stylesheet = ("hrm", "group_membership.xsl"), + csv_template = ("hrm", "group_membership"), + ) # ============================================================================= # Jobs @@ -158,7 +158,7 @@ def department(): if not auth.s3_has_role("ADMIN"): s3.filter = auth.filter_by_root_org(s3db.hrm_department) - return s3_rest_controller("hrm", resourcename) + return crud_controller("hrm", resourcename) # ----------------------------------------------------------------------------- def job_title(): @@ -186,10 +186,10 @@ def prep(r): if not auth.s3_has_role("ADMIN"): s3.filter &= auth.filter_by_root_org(s3db.hrm_job_title) - return s3_rest_controller("hrm", resourcename, - csv_stylesheet = ("hrm", "job_title.xsl"), - csv_template = ("hrm", "job_title"), - ) + return crud_controller("hrm", resourcename, + csv_stylesheet = ("hrm", "job_title.xsl"), + csv_template = ("hrm", "job_title"), + ) # ============================================================================= # Skills @@ -197,31 +197,31 @@ def prep(r): def skill(): """ Skills Controller """ - return s3_rest_controller("hrm", resourcename, - csv_stylesheet = ("hrm", "skill.xsl"), - csv_template = ("hrm", "skill"), - ) + return crud_controller("hrm", resourcename, + csv_stylesheet = ("hrm", "skill.xsl"), + csv_template = ("hrm", "skill"), + ) # ----------------------------------------------------------------------------- def skill_type(): """ Skill Types Controller """ - return s3_rest_controller("hrm", resourcename) + return crud_controller("hrm", resourcename) # ----------------------------------------------------------------------------- def competency_rating(): """ Competency Rating for Skill Types Controller """ - return s3_rest_controller("hrm", resourcename, - csv_stylesheet = ("hrm", "competency_rating.xsl"), - csv_template = ("hrm", "competency_rating"), - ) + return crud_controller("hrm", resourcename, + csv_stylesheet = ("hrm", "competency_rating.xsl"), + csv_template = ("hrm", "competency_rating"), + ) # ----------------------------------------------------------------------------- def skill_provision(): """ Skill Provisions Controller """ - return s3_rest_controller("hrm", resourcename) + return crud_controller("hrm", resourcename) # ----------------------------------------------------------------------------- def course(): @@ -230,17 +230,17 @@ def course(): if not auth.s3_has_role("ADMIN"): s3.filter = auth.filter_by_root_org(s3db.hrm_course) - return s3_rest_controller("hrm", resourcename, - csv_stylesheet = ("hrm", "course.xsl"), - csv_template = ("hrm", "course"), - rheader = s3db.hrm_rheader, - ) + return crud_controller("hrm", resourcename, + csv_stylesheet = ("hrm", "course.xsl"), + csv_template = ("hrm", "course"), + rheader = s3db.hrm_rheader, + ) # ----------------------------------------------------------------------------- def course_certificate(): """ Courses to Certificates Controller """ - return s3_rest_controller("hrm", resourcename) + return crud_controller("hrm", resourcename) # ----------------------------------------------------------------------------- def certificate(): @@ -250,17 +250,17 @@ def certificate(): not auth.s3_has_role("ADMIN"): s3.filter = auth.filter_by_root_org(s3db.hrm_certificate) - return s3_rest_controller("hrm", resourcename, - csv_stylesheet = ("hrm", "certificate.xsl"), - csv_template = ("hrm", "certificate"), - rheader = s3db.hrm_rheader, - ) + return crud_controller("hrm", resourcename, + csv_stylesheet = ("hrm", "certificate.xsl"), + csv_template = ("hrm", "certificate"), + rheader = s3db.hrm_rheader, + ) # ----------------------------------------------------------------------------- def certificate_skill(): """ Certificates to Skills Controller """ - return s3_rest_controller("hrm", resourcename) + return crud_controller("hrm", resourcename) # ----------------------------------------------------------------------------- def training(): @@ -353,7 +353,7 @@ def staff_org_site_json(): # ============================================================================= def activity_type(): - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def activity(): @@ -395,8 +395,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller(rheader = s3db.hrm_rheader, - ) + return crud_controller(rheader=s3db.hrm_rheader) # ----------------------------------------------------------------------------- def activity_hours(): @@ -405,7 +404,7 @@ def activity_hours(): - used for Imports & Reports """ - return s3_rest_controller() + return crud_controller() # ============================================================================= def facility(): @@ -441,11 +440,11 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller("hrm", resourcename, - csv_stylesheet = ("hrm", "programme.xsl"), - csv_template = ("hrm", "programme"), - rheader = s3db.hrm_rheader, - ) + return crud_controller("hrm", resourcename, + csv_stylesheet = ("hrm", "programme.xsl"), + csv_template = ("hrm", "programme"), + rheader = s3db.hrm_rheader, + ) # ----------------------------------------------------------------------------- def programme_hours(): @@ -454,16 +453,16 @@ def programme_hours(): - used for Imports & Reports """ - return s3_rest_controller("hrm", resourcename, - csv_stylesheet = ("hrm", "programme_hours.xsl"), - csv_template = ("hrm", "programme_hours") - ) + return crud_controller("hrm", resourcename, + csv_stylesheet = ("hrm", "programme_hours.xsl"), + csv_template = ("hrm", "programme_hours") + ) # ============================================================================= def award(): """ Volunteer Awards controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def volunteer_award(): @@ -483,31 +482,31 @@ def volunteer_award(): # return True #s3.prep = prep - return s3_rest_controller() + return crud_controller() # ============================================================================= def cluster_type(): """ Volunteer Cluster Types controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def cluster(): """ Volunteer Clusters controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def cluster_position(): """ Volunteer Group Positions controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def volunteer_cluster(): """ ONLY FOR RETURNING options to the S3PopupLink PopUp """ - return s3_rest_controller() + return crud_controller() # ============================================================================= def task(): @@ -542,7 +541,7 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller("hrm", "delegation") + return crud_controller("hrm", "delegation") # ============================================================================= # Messaging diff --git a/controllers/water.py b/controllers/water.py index 8b400e8462..24d1cf2d42 100644 --- a/controllers/water.py +++ b/controllers/water.py @@ -20,7 +20,7 @@ def index(): def debris_basin(): """ Debris Basins, RESTful controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def gauge(): @@ -53,26 +53,24 @@ def postp(r, output): return output s3.postp = postp - output = s3_rest_controller() - - return output + return crud_controller() # ----------------------------------------------------------------------------- def river(): """ Rivers, RESTful controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def zone(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # ----------------------------------------------------------------------------- def zone_type(): """ RESTful CRUD controller """ - return s3_rest_controller() + return crud_controller() # END ========================================================================= diff --git a/controllers/xforms.py b/controllers/xforms.py index 4f34987847..c8d736277a 100755 --- a/controllers/xforms.py +++ b/controllers/xforms.py @@ -31,11 +31,10 @@ def forms(): method = ["xform.%s" % extension] if len(args) > 1: method.insert(0, args[1]) - r = s3_request(prefix, name, - args = method, - extension = None, - ) - r.set_handler("xform", S3XForms) + r = crud_request(prefix, name, + args = method, + extension = None, + ) output = r() else: # Form list diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000000..d0c3cbf102 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/VM.txt b/docs/VM.txt deleted file mode 100644 index a55bc34cdf..0000000000 --- a/docs/VM.txt +++ /dev/null @@ -1,22 +0,0 @@ -Welcome to the Sahana Eden Developer environment - -To launch Sahana, double-click on the Eclipse icon & start the debugger. - -Firefox will open on the local site & also has other Bookmarks - -You should Register in Sahana - 1st user will get Admin rights. - -To view the Filesystem in Eclipse, click on the 'PyDev' button in the upper-right of the screen. - -For the ticket viewer: -Admin password: eden - -To open an interactive Python shell in the Sahana environment, open the terminal & type: -w2p - -To update this virtual machine, open the terminal & type: -update - -Password for the Sahana user: eden - -Happy coding! diff --git a/docs/epydoc.conf b/docs/epydoc.conf deleted file mode 100755 index 1a27bceb8f..0000000000 --- a/docs/epydoc.conf +++ /dev/null @@ -1,21 +0,0 @@ -[epydoc] # Epydoc section marker (required by ConfigParser) - -# Information about the project. -name: Sahana-Eden -url: http://eden.sahanafoundation.org/ - -# The list of modules to document. Modules can be named using -# dotted names, module filenames, or package directory names. -# This option may be repeated. -#modules: modules/sahana.py, modules/s3_test.py, modules/t2.py, modules/validators.py, models/__db.py, models/__pr.py, models/_gis.py, models/cr.py, models/or.py, controllers/default.py, controllers/appadmin.py, controllers/gis.py, controllers/or.py, controllers/cr.py, controllers/pr.py -#modules: modules/*.py, models/*.py, controllers/*.py -modules: modules/*.py - -# Write html output to a directory -output: html -target: docs/html/ - -# Include all automatically generated graphs. These graphs are -# generated using Graphviz dot. -#graph: all -#dotpath: /usr/local/bin/dot diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000000..6fcf05b4b7 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 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.https://www.sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000000..2ddcbab770 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,54 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +sys.path.insert(0, os.path.abspath('../../../web2py')) +sys.path.insert(0, os.path.abspath('../../modules')) + + +# -- Project information ----------------------------------------------------- + +project = 'Eden ASP' +copyright = '2021, Eden ASP Team' +author = 'Eden ASP Team' + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['sphinx.ext.autodoc'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +autoclass_content = "both" diff --git a/docs/source/core/aaa.rst b/docs/source/core/aaa.rst new file mode 100644 index 0000000000..9f6dd18e9d --- /dev/null +++ b/docs/source/core/aaa.rst @@ -0,0 +1,14 @@ +Authentication, Authorization and Accounting +============================================ + +.. Topics to cover + - Authentication + - Interactive login - auth/user controller + - HTTP Basic Auth + - OAuth2 + - Authorization + - Policies + - Realms + - auth_roles.csv + - Role Manager + diff --git a/docs/source/core/crud.rst b/docs/source/core/crud.rst new file mode 100644 index 0000000000..7f0f0de3a1 --- /dev/null +++ b/docs/source/core/crud.rst @@ -0,0 +1,3 @@ +CRUD (create-read-update-delete) +================================ + diff --git a/docs/source/core/index.rst b/docs/source/core/index.rst new file mode 100644 index 0000000000..9fd69e4ed7 --- /dev/null +++ b/docs/source/core/index.rst @@ -0,0 +1,22 @@ +The Core Libraries +================== + +The **core** libraries extend the *gluon* and *PyDAL* framework libraries, +implementing classes and functions to build RESTful CRUD controllers. + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + Controllers and Resources + CRUD Methods + Data Export and Import + User Interface + Authentication, Authorization and Accounting + Tools + +.. Topics to add later + - GIS and Maps + - Messaging + - Synchronization + diff --git a/docs/source/core/io.rst b/docs/source/core/io.rst new file mode 100644 index 0000000000..c73ad180d9 --- /dev/null +++ b/docs/source/core/io.rst @@ -0,0 +1,11 @@ +Data Export and Import +====================== + +.. Topics to cover + - S3XML + - Spreadsheet Importer + - Export Formats + - XLS + - PDF + - PDF Cards + - Prepop diff --git a/docs/source/core/methods/crud.rst b/docs/source/core/methods/crud.rst new file mode 100644 index 0000000000..90c4b12eb5 --- /dev/null +++ b/docs/source/core/methods/crud.rst @@ -0,0 +1,5 @@ +Standard Interactive CRUD +========================= + +*to be written* + diff --git a/docs/source/core/methods/index.rst b/docs/source/core/methods/index.rst new file mode 100644 index 0000000000..618914b99f --- /dev/null +++ b/docs/source/core/methods/index.rst @@ -0,0 +1,14 @@ +Built-in CRUD Methods +===================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + REST + CRUD + Pivottable Reports + Timeplot Reports + Spreadsheet Importer + + diff --git a/docs/source/core/methods/report.rst b/docs/source/core/methods/report.rst new file mode 100644 index 0000000000..cf5aa4bdae --- /dev/null +++ b/docs/source/core/methods/report.rst @@ -0,0 +1,5 @@ +Pivottable Reports +================== + +*to be written* + diff --git a/docs/source/core/methods/rest.rst b/docs/source/core/methods/rest.rst new file mode 100644 index 0000000000..fddb6db4af --- /dev/null +++ b/docs/source/core/methods/rest.rst @@ -0,0 +1,5 @@ +REST API +======== + +*to be written* + diff --git a/docs/source/core/methods/ssi.rst b/docs/source/core/methods/ssi.rst new file mode 100644 index 0000000000..dba3f1ac2d --- /dev/null +++ b/docs/source/core/methods/ssi.rst @@ -0,0 +1,5 @@ +Spreadsheet Importer +==================== + +*to be written* + diff --git a/docs/source/core/methods/timeplot.rst b/docs/source/core/methods/timeplot.rst new file mode 100644 index 0000000000..fa5e58f714 --- /dev/null +++ b/docs/source/core/methods/timeplot.rst @@ -0,0 +1,5 @@ +Timeplot Reports +================ + +*to be written* + diff --git a/docs/source/core/tools.rst b/docs/source/core/tools.rst new file mode 100644 index 0000000000..3d124665d2 --- /dev/null +++ b/docs/source/core/tools.rst @@ -0,0 +1,3 @@ +Tools +===== + diff --git a/docs/source/core/ui.rst b/docs/source/core/ui.rst new file mode 100644 index 0000000000..bc089c096c --- /dev/null +++ b/docs/source/core/ui.rst @@ -0,0 +1,15 @@ +User Interface Elements +======================= + +.. Topics to cover + - Filters + - URL queries, $filter, $search + - Widgets + - Client-side Scripts + - UI widgets + - s3.scripts and s3.jquery_ready + - Forms + - Widgets + - Inline-Components etc. + - Default Form, Custom Form + - Data Tables, Card Lists diff --git a/docs/source/deploy/index.rst b/docs/source/deploy/index.rst new file mode 100644 index 0000000000..3099a42be0 --- /dev/null +++ b/docs/source/deploy/index.rst @@ -0,0 +1,10 @@ +How to deploy Eden ASP applications +=================================== + +Eden ASP is normally deployed behind a separate front-end web server (e.g. nginx) +using WSGI/uWSGI to plugin web2py. This section describes how to setup a production +instance of an Eden ASP application on a Debian server. + +.. toctree:: + :maxdepth: 2 + :caption: Contents: diff --git a/docs/source/dev/advanced.rst b/docs/source/dev/advanced.rst new file mode 100644 index 0000000000..b4e2ead8d1 --- /dev/null +++ b/docs/source/dev/advanced.rst @@ -0,0 +1,18 @@ +Advanced Topics +=============== + +Themes +------ +.. Layouts + Views + Navigation + Styles + Including styles per page (s3.styles) + css.cfg + Build + +Models in templates +------------------- + +Re-routing controllers +---------------------- diff --git a/docs/source/dev/controller.rst b/docs/source/dev/controller.rst new file mode 100644 index 0000000000..dff498e594 --- /dev/null +++ b/docs/source/dev/controller.rst @@ -0,0 +1,220 @@ +About Controllers +================= + +Controllers are functions defined inside Python scripts in the *controllers* +directory, which handle HTTP requests and produce a response. + +Basic Request Routing +--------------------- + +Web2py maps the first three elements of the URL path to controllers as follows:: + + https:// server.domain.tld / application / controller / function + +The *application* refers to the subdirectory in web2py's application directory, +which in the case of Eden ASP is normally **eden** (it is possible to name it +differently, however). + +The **controller** refers to a Python script in the *controllers* directory inside +the application, which is executed. + +For instance:: + + https:// server.domain.tld / eden / my / page + +executes the script:: + + controllers / my.py + +The **function** refers to a *parameter-less* function defined in the controller +script, which is subsequently called. In the example above, that would mean this +function: + +.. code-block:: python + :caption: In controllers/my.py + + def page(): + ... + return output + +If the output format is HTML, the output of the controller function is further +passed to the view compiler to render the HTML which is then returned to the +client in the HTTP response. + +Every controller having its own URL also means that every *page* in the web +GUI has its own controller - and Eden ASP (like any web2py application) is a +*multi-page application* (MPA). Therefore, in the context of the web GUI, the +terms "controller function" and "page" are often used synonymously. + +That said, not every controller function actually produces a web page. Some +controllers exclusively serve non-interactive requests. + +CRUD Controllers +---------------- + +The basic database functions **create**, **read**, **update** and **delete** +(short: *CRUD*) are implemented in Eden ASP as one generic function: + +.. code-block:: python + :caption: In controllers/my.py + + def page(): + + return crud_controller() + +This single function call automatically generates web forms to create and +update records, displays filterable tables, generates pivot table reports +and more - including a generic RESTful API for non-interactive clients. + +If called without parameters, *crud_controller* will interpret *controller* +and *function* of the page URL as prefix and name of the database table which +to provide the functionality for, i.e. in the above example, CRUD functions +would be provided for the table *my_page*. + +It is possible to override the default table, by passing prefix and name +explicitly to *crud_controller*, e.g.: + +.. code-block:: python + :caption: In controllers/my.py + + def page(): + + return crud_controller("org", "organisation") + +...will provide CRUD functions for the *org_organisation* table instead. + +.. _resources_and_components: + +Resources and Components +------------------------ + +As explained above, a *crud_controller* is a database end-point that maps to +a certain table or - depending on the request - certain records in that table. + +This *context data set* (consisting of a table and a query) is referred to +as the **resource** addressed by the HTTP request and served by the controller. + +Apart from the data set in the primary table (called *master*), a resource +can also include data in related tables that reference the master (e.g. via +foreign keys or link tables) and which have been *declared* (usually in the +data model) as **components** in the context of the master table. + +An example for this would be addresses (*component*) of a person (*master*). + +CRUD URLs and Methods +--------------------- + +The *crud_controller* extends web2py's URL schema with two additional path elements:: + + https:// server.domain.tld / a / c / f / record / method + +Here, the **record** is the primary key (*id*) of a record in the table served +by the crud_controller function - while the **method** specifies how to access +that record, e.g. *read* or *update*. + +For instance, the following URL:: + + https:// server.domain.tld / eden / org / organisation / 4 / update + +...accesses the workflow to update the record #4 in the org_organisation table +(with HTTP GET to retrieve the update-form, and POST to submit it and perform +the update). + +Without a *record* key, the URL accesses the table itself - as some methods, like +*create*, only make sense in the table context:: + + https:// server.domain.tld / eden / org / organisation / create + +The *crud_controller* comes pre-configured with a number of standard methods, +including: + +=============================================== ======== =========================================================== +Method Target Description +=============================================== ======== =========================================================== +:doc:`create <../reference/methods/crud>` *Table* Create a new record (form) +:doc:`read <../reference/methods/crud>` *Record* View a record (read-only representation) +:doc:`update <../reference/methods/crud>` *Record* Update a record (form) +:doc:`delete <../reference/methods/crud>` *Record* Delete a record +:doc:`list <../reference/methods/datatable>` *Table* A tabular view of records +:doc:`report <../reference/methods/report>` *Table* Pivot table report with charts +:doc:`timeplot <../reference/methods/timeplot>` *Table* Statistics over a time axis +:doc:`map <../reference/methods/map>` *Table* Show location context of records on a map +:doc:`summary <../reference/methods/summary>` *Table* Meta-method with list, report, map on the same page (tabs) +:doc:`import <../reference/methods/import>` *Table* Import records from spreadsheets +:doc:`organize <../reference/methods/organize>` *Table* Calendar-based manipulation of records +=============================================== ======== =========================================================== + +.. note:: + + Both *models* and *templates* can extend the *crud_controller* by adding + further methods, or overriding the standard methods with specific + implementations. + +.. _restapi: + +Default REST API +---------------- + +If no *method* is specified in the URL, then the *crud_controller* will treat +the request as **RESTful** - i.e. the HTTP verb (GET, PUT, POST or DELETE) +determines the access method, e.g.:: + + GET https:// server.domain.tld / eden / org / organisation / 3.xml + +...produces a XML representation of the record #3 in the org_organisation table. +A *POST* request to the same URL, with XML data in the request body, will update +the record. + +This **REST API** is a simpler, lower-level interface that is primarily used by +certain client-side scripts, e.g. the map viewer. It does not implement complete +CRUD workflows, but rather each function individually (stateless). + +.. note:: + + A data format extension in the URL is required for the REST API, as it can + produce and process multiple data formats (extensible). Without extension, + HTML format will be assumed and one of the interactive *read*, *update*, + *delete* or *list* methods will be chosen to handle the request instead. + +The default REST API *could* be used to integrate Eden ASP with other +applications, but normally such integrations require process-specific end +points (rather than just database end points) - which would be implemented +as explicit methods instead. + +Component URLs +-------------- + +URLs served by a *crud_controller* can also directly address a :ref:`component `. +For that, the *record* parameter would be extended like:: + + https:// server.domain.tld / a / c / f / record / component / method + +Here, the **component** is the *declared* name (*alias*) of the component in +the context of the master table - usually the name of the component table +without prefix, e.g.:: + + https:// server.domain.tld / eden / pr / person / 16 / address + +...would produce a list of all addresses (*pr_address* table) that are related +to the *pr_person* record #16. Similar, replacing *list* with *create* would +access the workflow to create new addresses in the context of that person record. + +.. note:: + + The `/list` method can be omitted here - if the end-point is a table rather + than a single record, then the *crud_controller* will automatically apply + the *list* method for interactive data formats. + +To access a particular record in a component, the primary key (id) of the +component record can be appended, as in:: + + https:// server.domain.tld / eden / pr / person / 16 / address / 2 / read + +...to read the *pr_address* record #2 in the context of the *pr_person* +record #16 (if the specified component record does not reference that master +record, the request will result in a HTTP 404 status). + +.. note:: + + The :ref:`default REST API ` *always* serves the master table, even if the URL + addresses a component (however, the XML/JSON will include the component). diff --git a/docs/source/dev/index.rst b/docs/source/dev/index.rst new file mode 100644 index 0000000000..f94b5b24d0 --- /dev/null +++ b/docs/source/dev/index.rst @@ -0,0 +1,12 @@ +Building Applications +===================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + Setting up for Development + About Templates + About Controllers + Implementing Templates + Advanced Topics diff --git a/docs/source/dev/settings.rst b/docs/source/dev/settings.rst new file mode 100644 index 0000000000..a451d55e91 --- /dev/null +++ b/docs/source/dev/settings.rst @@ -0,0 +1,148 @@ +About Templates +=============== + +Global Config +------------- + +Many features and behaviors of Eden ASP can be controlled by settings. + +These settings are stored in a global *S3Config* instance - which is accessible +through :doc:`current ` as *current.deployment_settings*. + +.. code-block:: python + + from gluon import current + + settings = current.deployment_settings + +.. note:: + In the models and controllers context, *current.deployment_settings* is + accessible simply as *settings*. + +Deployment Settings +------------------- + +*S3Config* comes with meaningful defaults where possible. + +However, some settings will need to be adjusted to configure the application +for a particular system environment - or to enable, disable, configure, +customize or extend features in the specific context of the deployment. + +This configuration happens in a machine-specific configuration file: + + **models/000_config.py** + +.. note:: + + *models/000_config.py is not part of the code base, and must be created + before the application can be started. An annotated example can be found + in the *modules/templates* directory. + +The configuration file is a Python script that is executed for every request cycle: + +.. code-block:: python + :caption: models/000_config.py (partial example) + :emphasize-lines: 11,36 + + # -*- coding: utf-8 -*- + + """ + Machine-specific settings + """ + + # Remove this line when this file is ready for 1st run + FINISHED_EDITING_CONFIG_FILE = True + + # Select the Template + settings.base.template = "MYAPP" + + # Database settings + settings.database.db_type = "postgres" + #settings.database.host = "localhost" + #settings.database.port = 3306 + settings.database.database = "myapp" + #settings.database.username = "eden" + #settings.database.password = "password" + + # Do we have a spatial DB available? + settings.gis.spatialdb = True + + settings.base.migrate = True + #settings.base.fake_migrate = True + + settings.base.debug = True + #settings.log.level = "WARNING" + #settings.log.console = False + #settings.log.logfile = None + #settings.log.caller_info = True + + # ============================================================================= + # Import the settings from the Template + # + settings.import_template() + + # ============================================================================= + # Over-rides to the Template may be done here + # + # After 1st_run, set this for Production + #settings.base.prepopulate = 0 + + # ============================================================================= + VERSION = 1 + + # END ========================================================================= + +Templates +--------- + +Deployment configurations use configuration **templates**, which provide +pre-configured settings, customizations and extensions suitable for a concrete +deployment scenario. The example above highlights how these templates are applied. + +.. important:: + Implementing configuration **templates** is the primary strategy to build + applications with Eden ASP. + +Templates are Python packages located in the *modules/templates* directory: + +.. image:: template_location.png + :align: center + +Each template package must contain a module *config.py* which defines +a *config*-function : + +.. code-block:: python + :caption: modules/templates/MYAPP/config.py + + def config(settings): + + T = current.T + + settings.base.system_name = T("My Application") + settings.base.system_name_short = T("MyApp") + + ... + +This *config* function is called from *models/000_config.py* (i.e. for every +request cycle) with the *current.deployment_settings* instance as parameter, +so that it can modify the global settings as needed. + +.. note:: + The template directory must also contain an *__init__.py* file (which can + be empty) in order to become a Python package! + +Cascading Templates +------------------- + +It is possible for a deployment configuration to apply multiple templates +in a cascade, so that they complement each other: + +.. code-block:: python + :caption: Cascading templates (in models/000_config.py) + + # Select the Template + settings.base.template = ("locations.DE", "MYAPP") + +This is useful to separate e.g. locale-specific settings from use-case +configurations, so that both can be reused independently across multiple +deployments. diff --git a/docs/source/dev/setup.rst b/docs/source/dev/setup.rst new file mode 100644 index 0000000000..1ee10fbca6 --- /dev/null +++ b/docs/source/dev/setup.rst @@ -0,0 +1,229 @@ +Setting up for Development +========================== + +This page describes how you can set up a local Eden ASP instance for +application development on your computer. + +.. note:: + + This guide assumes that you are working in a Linux environment (shell commands + are for *bash*). + + If you are working with another operating system, you can still take this as a + general guideline, but commands may be different, and additional installation + steps could be required. + +.. note:: + + This guide further assumes that you have *Python* (version 3.6 or later) + installed, which comes bundled with the *pip* package installer - and that + you are familiar with the Python programming language. + + Additionally, you will need to have `git `_ + installed. + +Prerequisites +------------- + +Eden ASP requires a couple of Python libraries, which can be installed +with the *pip* installer. + +As a minimum, *lxml* and *python-dateutil* must be installed: + +.. code-block:: bash + + sudo pip install lxml python-dateutil + +The following are also required for normal operation: + +.. code-block:: bash + + sudo pip install pyparsing requests xlrd xlwt openpyxl reportlab shapely geopy + +Some specialist functionality may require additional libraries, e.g.: + +.. code-block:: bash + + sudo pip install qrcode docx-mailmerge + +.. tip:: + + The above commands use `sudo pip` to install the libraries globally. + If you want to install them only in your home directory, you can + omit `sudo`. + +Installing web2py +----------------- + +To install web2py, clone it directly from GitHub: + +.. code-block:: bash + + git clone --recursive https://github.com/web2py/web2py.git ~/web2py + +.. tip:: + You can of course choose any other target location than *~/web2py* for + the clone - just remember to use the correct path in subsequent commands. + +Change into the *web2py* directory, and reset the repository (including +all submodules) to the supported stable version (currently 2.21.2): + +.. code-block:: bash + + cd ~/web2py + git reset --hard 3190585 + git submodule update --recursive + +Installing Eden ASP +------------------- + +To install Eden ASP, clone it directly from GitHub: + +.. code-block:: bash + + git clone --recursive https://github.com/aqmaster/eden-asp.git ~/eden + +.. tip:: + You can of course choose any other target location than *~/eden* for + the clone - just remember to use the correct path in subsequent commands. + +Configure Eden ASP as a web2py application by adding a symbolic link +to the *eden* directory under *web2py/applications*: + +.. code-block:: bash + + cd ~/web2py/applications + ln -s ~/eden eden + +The name of this symbolic link (*eden*) becomes the web2py application name, +and will later be used in URLs to access the application. + +.. tip:: + You can also clone Eden ASP into the *~/web2py/applications/eden* + directory - then you will not need the symbolic link. + +Configuring Eden ASP +-------------------- + +Before running Eden ASP the first time, you need to create a configuration +file. To do so, copy the *000_config.py* template into Eden ASP's *models* folder: + +.. code-block:: bash + + cd ~/eden + cp modules/templates/000_config.py models + +Open the *~/eden/models/000_config.py* file in an editor and adjust any +settings as needed. + +For development, you do not normally need to change anything, except +setting the following to *True* (or removing the line altogether): + +.. code-block:: python + :caption: Editing models/000_config.py + + FINISHED_EDITING_CONFIG_FILE = True + +That said, it normally makes sense to also turn on *debug* mode for +development: + +.. code-block:: python + :caption: Editing models/000_config.py + + settings.base.debug = True + +First run +--------- + +The first start of Eden ASP will set up the database, creating all tables +and populating them with some data. + +This is normally done by running the *noop.py* script in the web2py shell: + +.. code-block:: bash + + cd ~/web2py + python web2py.py -S eden -M -R applications/eden/static/scripts/tools/noop.py + +This will give a console output similar to this: + +.. code-block:: bash + :caption: Console output during first run + + WARNING: S3Msg unresolved dependency: pyserial required for Serial port modem usage + WARNING: Setup unresolved dependency: ansible required for Setup Module + WARNING: Error when loading optional dependency: google-api-python-client + WARNING: Error when loading optional dependency: translate-toolkit + + *** FIRST RUN - SETTING UP DATABASE *** + + Setting Up System Roles... + Setting Up Scheduler Tasks... + Creating Database Tables (this can take a minute)... + Database Tables Created. (3.74 sec) + + Please be patient whilst the database is populated... + + Importing default/base... + Imports for default/base complete (1.99 sec) + + Importing default... + Imports for default complete (5.20 sec) + + Importing default/users... + Imports for default/users complete (0.04 sec) + + Updating database... + Location Tree update completed (0.63 sec) + Demographic Data aggregation completed (0.01 sec) + + Pre-populate complete (7.90 sec) + + Creating indexes... + + *** FIRST RUN COMPLETE *** + +You can ignore the *WARNING* messages here about unresolved, optional dependencies. + +Starting the server +------------------- + +In a development environment, we normally use the built-in HTTP server (*Rocket*) +of web2py, which can be launched with: + +.. code-block:: bash + + cd ~/web2py + python web2py.py --no_gui -a [password] + +Replace *[password]* here with a password of your choosing - this password is +needed to access web2py's application manager (e.g. to view error tickets). + +Once the server is running, it will give you a localhost URL to access it: + +.. code-block:: bash + :caption: Console output of web2py after launch + + web2py Web Framework + Created by Massimo Di Pierro, Copyright 2007-2021 + Version 2.21.1-stable+timestamp.2020.11.27.18.21.43 + Database drivers available: sqlite3, MySQLdb, psycopg2, imaplib, pymysql, pyodbc + + please visit: + http://127.0.0.1:8000/ + use "kill -SIGTERM 8455" to shutdown the web2py server + +Append the application name *eden* to the URL (http://127.0.0.1:8000/eden), +and open that address in your web browser to access Eden ASP. + +The first run will have installed two demo user accounts, namely: + + - `admin@example.com` (a user with the system administrator role) + - `normaluser@example.com` (an unprivileged user account) + +...each with the password `testing`. So you can login and explore the functionality. + +Using PostgreSQL +---------------- + +*to be written* diff --git a/docs/source/dev/template_location.png b/docs/source/dev/template_location.png new file mode 100644 index 0000000000..37e993985e Binary files /dev/null and b/docs/source/dev/template_location.png differ diff --git a/docs/source/dev/templates.rst b/docs/source/dev/templates.rst new file mode 100644 index 0000000000..62ce98ce7d --- /dev/null +++ b/docs/source/dev/templates.rst @@ -0,0 +1,21 @@ +Implementing Templates +====================== + +Settings +-------- + +Customising resources +--------------------- + +Customising controllers +----------------------- + +Pre-populating data +------------------- + +Menus +----- + +Configuring Auth +---------------- + diff --git a/docs/source/extend/controllers/basics.rst b/docs/source/extend/controllers/basics.rst new file mode 100644 index 0000000000..022f51d71a --- /dev/null +++ b/docs/source/extend/controllers/basics.rst @@ -0,0 +1,6 @@ +Basic Concepts +============== + +CRUDRequest +----------- + diff --git a/docs/source/extend/controllers/crud.rst b/docs/source/extend/controllers/crud.rst new file mode 100644 index 0000000000..b4dd68efa2 --- /dev/null +++ b/docs/source/extend/controllers/crud.rst @@ -0,0 +1,11 @@ +Implementing CRUD Controllers +============================= + +crud_controller +--------------- + +prep +---- + +postp +----- diff --git a/docs/source/extend/controllers/index.rst b/docs/source/extend/controllers/index.rst new file mode 100644 index 0000000000..3070d48855 --- /dev/null +++ b/docs/source/extend/controllers/index.rst @@ -0,0 +1,10 @@ +Implementing Controllers +======================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + Basic Concepts + CRUD Controllers + diff --git a/docs/source/extend/index.rst b/docs/source/extend/index.rst new file mode 100644 index 0000000000..403532f616 --- /dev/null +++ b/docs/source/extend/index.rst @@ -0,0 +1,11 @@ +Extending Eden ASP +================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + Implementing Controllers + Implementing Data Models + Implementing Themes + Adding Form Widgets diff --git a/docs/source/extend/models/basics.rst b/docs/source/extend/models/basics.rst new file mode 100644 index 0000000000..e889c4e85d --- /dev/null +++ b/docs/source/extend/models/basics.rst @@ -0,0 +1,18 @@ +Basic Concepts +============== + +Model Loader *s3db* +------------------- + +Resources +--------- + +Components +---------- + +Super-Entities +-------------- + +Field Selectors and Resource Queries +------------------------------------ + diff --git a/docs/source/extend/models/configure.rst b/docs/source/extend/models/configure.rst new file mode 100644 index 0000000000..ddea9259e5 --- /dev/null +++ b/docs/source/extend/models/configure.rst @@ -0,0 +1,17 @@ +Table Configuration +=================== + +CRUD Hooks +---------- + +Linking to Super-Entities +------------------------- + +CRUD Strings +------------ + +Adding Components +----------------- + +Adding Methods +-------------- diff --git a/docs/source/extend/models/index.rst b/docs/source/extend/models/index.rst new file mode 100644 index 0000000000..c87ccb3848 --- /dev/null +++ b/docs/source/extend/models/index.rst @@ -0,0 +1,13 @@ +Implementing Data Models +======================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + Basic Concepts + Defining Tables + Table Configuration + Reusable Fields + Representation Methods + diff --git a/docs/source/extend/models/represent.rst b/docs/source/extend/models/represent.rst new file mode 100644 index 0000000000..1461df0657 --- /dev/null +++ b/docs/source/extend/models/represent.rst @@ -0,0 +1,8 @@ +Field Representation +==================== + +Common Representation Functions +------------------------------- + +Foreign Key Bulk Representation (S3Represent) +--------------------------------------------- diff --git a/docs/source/extend/models/reusablefield.rst b/docs/source/extend/models/reusablefield.rst new file mode 100644 index 0000000000..8ee090cde2 --- /dev/null +++ b/docs/source/extend/models/reusablefield.rst @@ -0,0 +1,11 @@ +Reusable Fields +=============== + +Common Field Functions +---------------------- + +Meta-Fields +----------- + +Implementing Reusable Fields +---------------------------- diff --git a/docs/source/extend/models/tables.rst b/docs/source/extend/models/tables.rst new file mode 100644 index 0000000000..49756bacac --- /dev/null +++ b/docs/source/extend/models/tables.rst @@ -0,0 +1,20 @@ +Defining Tables +=============== + +Subclassing DataModel +--------------------- + +model() +------- + +defaults() +---------- + +mandatory() +----------- + +Exposing names +-------------- + +Defining Tables +--------------- diff --git a/docs/source/extend/themes/index.rst b/docs/source/extend/themes/index.rst new file mode 100644 index 0000000000..1a971dcc3c --- /dev/null +++ b/docs/source/extend/themes/index.rst @@ -0,0 +1,2 @@ +Adding new themes +================= diff --git a/docs/source/extend/widgets/index.rst b/docs/source/extend/widgets/index.rst new file mode 100644 index 0000000000..db52ed8a01 --- /dev/null +++ b/docs/source/extend/widgets/index.rst @@ -0,0 +1,2 @@ +Adding new Form Widgets +======================= diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000000..9e0cc10615 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,29 @@ +Eden ASP +======== + +Eden ASP is a rapid application development (RAD) kit for web-based, database-driven +humanitarian and emergency management applications, originally derived from the +*Sahana Eden Humanitarian Management Platform*. + +Eden ASP builds on the **web2py** web application framework, and is written in the +**Python** programming language (version 3.6+). It also uses *HTML5*, *JavaScript*, +and *SCSS* to generate web contents, as well as *XSLT* to handle certain data formats. + +This documentation is aimed at application developers, and included in the source code. + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + Introduction + Building Applications + Reference Guide + Deploying Applications + Extending Eden ASP + Core Libraries + +Indices and tables +================== + +* :ref:`genindex` + diff --git a/docs/source/intro/basics.rst b/docs/source/intro/basics.rst new file mode 100644 index 0000000000..fcc6cdd709 --- /dev/null +++ b/docs/source/intro/basics.rst @@ -0,0 +1,126 @@ +Basic Concepts +============== + +This page explains the basic concepts, structure and operations of Eden ASP, and +introduces the fundamental terminology used throughout this documentation. + +Client and Server +----------------- + +Eden ASP is a **web application**, which means it is run as a **server** program +and is accessed remotely by **client** programs connected over the network. + +.. image:: client_server.png + :align: center + +Most of the time, the client program will be a **web browser** - but it could +also be a mobile app, or another type of program accessing web services. Many +clients can be connected to the server at the same time. + +Client and server communicate using the **HTTP** protocol, in which the client +sends a **request** to the server, the server processes the request and +produces a **response** (e.g. a HTML page) that is sent back to the client, +and then the client processes the response (e.g. by rendering the HTML page +on the screen). + +.. note:: + + Responding to HTTP requests is Eden ASP's fundamental mode of operation. + +Web2Py and PyDAL +---------------- + +Eden ASP builds on the **web2py** web application framework, which consists +of three basic components: a *HTTP server*, the *application runner* and various +libraries, and a *database abstraction layer*. + +.. image:: web2py_stack.png + :align: center + +The **HTTP server** (also commonly called "web server") manages client connections. +Web2py comes with a built-in HTTP server (*Rocket*), but production environments +typically deploy a separate front-end HTTP server (e.g. *nginx*) that connects +to web2py through a WSGI plugin or service (e.g. *uWSGI*). + +.. image:: web2py_stack_prod.png + :align: center + +The **application runner** (*gluon*) decodes the HTTP request, then calls certain +Python functions in the Eden ASP application with the request data as input, and +from their output renders the HTTP response. Additionally, *gluon* provides a number +of libraries to generate interactive web contents and process user input. + +The **database abstraction layer** (*PyDAL*) provides a generic interface to +the database, as well as a mapping between Python objects and the tables +and records in the database (*ORM, object-relational mapping*). For production +environments, the preferred database back-end is PostgreSQL with the +PostGIS extension, but SQLite and MariaDB/MySQL are also supported. + +Application Structure +--------------------- + +Web2py applications like Eden ASP implement the MVC (model-view-controller) +application model, meaning that the application code is separated in: + + - **models** defining the data(base) structure, + - **views** implementing the user interface, + - **controllers** implementing the logic connecting models and views + +This is somewhat reflected by the directory layout of Eden ASP: + +.. image:: directory_layout.png + :align: center + +.. note:: + + This directory layout can be somewhat misleading about where certain + functionality can be found in the code: + + The *controllers* directory contains Python scripts implementing the logic + of the application. In Eden ASP, these controllers delegate much of that + logic to **core** modules. + + The *models* directory contains Python scripts to configure the application + and define the database structure. In Eden ASP, the former is largely delegated + to configuration **templates**, and the latter is reduced to the instantiation + of a model loader, which then loads the actual data models from **s3db** modules + if and when they are actually needed. + +The Request Cycle +----------------- + +Eden ASP runs in cycles triggered by incoming HTTP requests. + +.. image:: request_cycle.png + :align: center + +When an HTTP request is received, web2py parses and translates it into a global +**request** object. + + For instance, the request URI is translated like:: + + https://www.example.com/[application]/[controller]/[function]/[args]?[vars] + + ...and its elements stored as properties of the *request* object + (e.g. *request.controller* and *request.function*). These values determine + which function of the application is to be executed. + +Web2py also generates a global **response** object, which can be written to +in order to set parameters for the eventual HTTP response. + +Web2py then runs the Eden ASP application: + + 1. executes all scripts in the *models/* directory in lexical (ASCII) order. + + 2. executes the script in the *controllers/* directory that corresponds + to *request.controller*, and then calls the function defined by that + script that corresponds to *request.function*. + + E.g. if *request.controller* is "dvr" and *request.function* is "person", + then the *controllers/dvr.py* script will be executed, and then the + *person()* function defined in that script will be invoked. + + 3. takes the output of the function call to compile the view template + configured as *response.view*. + +These three steps are commonly referred to as the *request cycle*. diff --git a/docs/source/intro/client_server.png b/docs/source/intro/client_server.png new file mode 100644 index 0000000000..3a5ac3c7e8 Binary files /dev/null and b/docs/source/intro/client_server.png differ diff --git a/docs/source/intro/directory_layout.png b/docs/source/intro/directory_layout.png new file mode 100644 index 0000000000..fadbd69ea6 Binary files /dev/null and b/docs/source/intro/directory_layout.png differ diff --git a/docs/source/intro/index.rst b/docs/source/intro/index.rst new file mode 100644 index 0000000000..bc43ca6226 --- /dev/null +++ b/docs/source/intro/index.rst @@ -0,0 +1,8 @@ +Introduction into Eden ASP +========================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + basics diff --git a/docs/source/intro/request_cycle.png b/docs/source/intro/request_cycle.png new file mode 100644 index 0000000000..34b22fe022 Binary files /dev/null and b/docs/source/intro/request_cycle.png differ diff --git a/docs/source/intro/web2py_stack.png b/docs/source/intro/web2py_stack.png new file mode 100644 index 0000000000..7e3f1b62f5 Binary files /dev/null and b/docs/source/intro/web2py_stack.png differ diff --git a/docs/source/intro/web2py_stack_prod.png b/docs/source/intro/web2py_stack_prod.png new file mode 100644 index 0000000000..7b42fc6085 Binary files /dev/null and b/docs/source/intro/web2py_stack_prod.png differ diff --git a/docs/source/reference/current.rst b/docs/source/reference/current.rst new file mode 100644 index 0000000000..f5a8c5d98e --- /dev/null +++ b/docs/source/reference/current.rst @@ -0,0 +1,29 @@ +The *current* Object +==================== + +The *current* object holds thread-local global variables. It can be imported into any context: + +.. code-block:: python + + from gluon import current + +.. table:: Objects accessible through current + :widths: auto + + =================================== ================= ============================================ + Attribute Type Explanation + =================================== ================= ============================================ + current.db DAL the database + :doc:`current.s3db ` DataModel the model loader + current.deployment_settings S3Config deployment settings + :doc:`current.auth ` AuthS3 global authentication/authorisation service + :doc:`current.gis ` GIS global GIS service + :doc:`current.msg ` S3Msg global messaging service + :doc:`current.xml ` S3XML global XML decoder/encoder service + current.request Request web2py's global request object + current.response Response web2py's global response object + current.T TranslatorFactory String Translator (for i18n) + current.messages Messages Common labels (internationalised) + current.ERROR Messages Common error messages (internationalised) + =================================== ================= ============================================ + diff --git a/docs/source/reference/index.rst b/docs/source/reference/index.rst new file mode 100644 index 0000000000..99cdd4a569 --- /dev/null +++ b/docs/source/reference/index.rst @@ -0,0 +1,12 @@ +Reference Guide +=============== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + current + services/index + settings/index + models/index + methods/index diff --git a/docs/source/reference/methods/create.png b/docs/source/reference/methods/create.png new file mode 100644 index 0000000000..dbb4f8b77a Binary files /dev/null and b/docs/source/reference/methods/create.png differ diff --git a/docs/source/reference/methods/crud.rst b/docs/source/reference/methods/crud.rst new file mode 100644 index 0000000000..54138f8dee --- /dev/null +++ b/docs/source/reference/methods/crud.rst @@ -0,0 +1,36 @@ +Form-based CRUD +=============== + +Simple, form-based Create, Read, Update and Delete functions. + +Create +------ + +End-point: */create* + +.. figure:: create.png + + Create-form + +Read +---- + +End-point: *[id]/read* + +.. figure:: read_tabs.png + + Read view with component tabs + +Update +------ + +End-point: *[id]/update* + +.. figure:: update_tabs.png + + Update-form on tab + +Delete +------ + +End-point: [id]/delete diff --git a/docs/source/reference/methods/datatable.rst b/docs/source/reference/methods/datatable.rst new file mode 100644 index 0000000000..04103080a2 --- /dev/null +++ b/docs/source/reference/methods/datatable.rst @@ -0,0 +1,8 @@ +Data Tables +=========== + +Tabular view of records (end-point: */list*, and default for table end-point without method and interactive data format). + +.. figure:: list_filter.png + + Data Table View with Filter Form diff --git a/docs/source/reference/methods/import.rst b/docs/source/reference/methods/import.rst new file mode 100644 index 0000000000..a4bc8ceeac --- /dev/null +++ b/docs/source/reference/methods/import.rst @@ -0,0 +1,12 @@ +Spreadsheet Importer +==================== + +Interactive Spreadsheet (CSV/XLS) Importer with review and record selection (end-point: */import*). + +.. figure:: import_upload.png + + Spreadsheet Importer, Upload Dialog + +.. figure:: import_commit.png + + Spreadsheet Importer, Review and Record Selection diff --git a/docs/source/reference/methods/import_commit.png b/docs/source/reference/methods/import_commit.png new file mode 100644 index 0000000000..578d5b46ad Binary files /dev/null and b/docs/source/reference/methods/import_commit.png differ diff --git a/docs/source/reference/methods/import_upload.png b/docs/source/reference/methods/import_upload.png new file mode 100644 index 0000000000..dd9ce712e5 Binary files /dev/null and b/docs/source/reference/methods/import_upload.png differ diff --git a/docs/source/reference/methods/index.rst b/docs/source/reference/methods/index.rst new file mode 100644 index 0000000000..53ec3387d8 --- /dev/null +++ b/docs/source/reference/methods/index.rst @@ -0,0 +1,15 @@ +Standard CRUD Methods +===================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + Data Table + Form-based CRUD + Map + Pivottable Report + Timeplot + Summary + Organizer + Spreadsheet Import diff --git a/docs/source/reference/methods/list_filter.png b/docs/source/reference/methods/list_filter.png new file mode 100644 index 0000000000..faf62faf66 Binary files /dev/null and b/docs/source/reference/methods/list_filter.png differ diff --git a/docs/source/reference/methods/map.rst b/docs/source/reference/methods/map.rst new file mode 100644 index 0000000000..f0a9405062 --- /dev/null +++ b/docs/source/reference/methods/map.rst @@ -0,0 +1,8 @@ +Map +=== + +Filterable Map (end-point: */map*). + +.. figure:: map_filter.png + + Map with filter form diff --git a/docs/source/reference/methods/map_filter.png b/docs/source/reference/methods/map_filter.png new file mode 100644 index 0000000000..b39f24c829 Binary files /dev/null and b/docs/source/reference/methods/map_filter.png differ diff --git a/docs/source/reference/methods/organize.png b/docs/source/reference/methods/organize.png new file mode 100644 index 0000000000..3a5329f567 Binary files /dev/null and b/docs/source/reference/methods/organize.png differ diff --git a/docs/source/reference/methods/organize.rst b/docs/source/reference/methods/organize.rst new file mode 100644 index 0000000000..12ab6ee9ca --- /dev/null +++ b/docs/source/reference/methods/organize.rst @@ -0,0 +1,13 @@ +Organizer +========= + +Calendar-based view and manipulation of records (end-point: */organize*) + +.. figure:: organize.png + + Organizer (Weekly Agenda View) + +.. note:: + + This method requires configuration of start and end date fields, as + well as of popup contents. diff --git a/docs/source/reference/methods/read_tabs.png b/docs/source/reference/methods/read_tabs.png new file mode 100644 index 0000000000..8f83228d8a Binary files /dev/null and b/docs/source/reference/methods/read_tabs.png differ diff --git a/docs/source/reference/methods/report.png b/docs/source/reference/methods/report.png new file mode 100644 index 0000000000..048ca56b22 Binary files /dev/null and b/docs/source/reference/methods/report.png differ diff --git a/docs/source/reference/methods/report.rst b/docs/source/reference/methods/report.rst new file mode 100644 index 0000000000..aa265000b7 --- /dev/null +++ b/docs/source/reference/methods/report.rst @@ -0,0 +1,16 @@ +Pivottable Reports +================== + +User-definable pivot tables with chart option (end-point: */report*). + +.. figure:: report.png + + Pivot Table Report + +.. figure:: report_charts.png + + Pivot Table with Chart + +.. note:: + + This method requires configuration. diff --git a/docs/source/reference/methods/report_charts.png b/docs/source/reference/methods/report_charts.png new file mode 100644 index 0000000000..700e6a887a Binary files /dev/null and b/docs/source/reference/methods/report_charts.png differ diff --git a/docs/source/reference/methods/summary.png b/docs/source/reference/methods/summary.png new file mode 100644 index 0000000000..17b1be4fe8 Binary files /dev/null and b/docs/source/reference/methods/summary.png differ diff --git a/docs/source/reference/methods/summary.rst b/docs/source/reference/methods/summary.rst new file mode 100644 index 0000000000..40204004d5 --- /dev/null +++ b/docs/source/reference/methods/summary.rst @@ -0,0 +1,13 @@ +Summary +======= + +Meta-method with multiple other methods on the same page (on tabs), +and a common filter form (end-point: */summary*). + +.. figure:: summary.png + + Summary view with table, report and map tabs, and common filter form. + +.. note:: + + This method requires configuration. diff --git a/docs/source/reference/methods/timeplot.png b/docs/source/reference/methods/timeplot.png new file mode 100644 index 0000000000..fe73328c44 Binary files /dev/null and b/docs/source/reference/methods/timeplot.png differ diff --git a/docs/source/reference/methods/timeplot.rst b/docs/source/reference/methods/timeplot.rst new file mode 100644 index 0000000000..b1bcb4ab56 --- /dev/null +++ b/docs/source/reference/methods/timeplot.rst @@ -0,0 +1,12 @@ +Timeplot +======== + +Visualisation of the development of a numeric fact over a time axis (endpoint: */timeplot*). + +.. figure:: timeplot.png + + Timeplot + +.. note:: + + This method requires configuration. diff --git a/docs/source/reference/methods/update_tabs.png b/docs/source/reference/methods/update_tabs.png new file mode 100644 index 0000000000..6da9316462 Binary files /dev/null and b/docs/source/reference/methods/update_tabs.png differ diff --git a/docs/source/reference/models/business/disease.rst b/docs/source/reference/models/business/disease.rst new file mode 100644 index 0000000000..342657a22c --- /dev/null +++ b/docs/source/reference/models/business/disease.rst @@ -0,0 +1,11 @@ +Disease Tracking +================ + +This module implements data elements to track disease outbreaks, both +on the individual case level, and in mass testing. It was originally +developed for Ebola Virus Disease outbreaks, and has later been +re-used during the COVID-19 pandemic. + +.. image:: er_disease.png + :align: center + diff --git a/docs/source/reference/models/business/er_disease.png b/docs/source/reference/models/business/er_disease.png new file mode 100644 index 0000000000..96553cd645 Binary files /dev/null and b/docs/source/reference/models/business/er_disease.png differ diff --git a/docs/source/reference/models/business/index.rst b/docs/source/reference/models/business/index.rst new file mode 100644 index 0000000000..4d9f82cd1c --- /dev/null +++ b/docs/source/reference/models/business/index.rst @@ -0,0 +1,20 @@ +Business Data Models +==================== + +The models implement data structures for specific business cases. Typically, they +have been developed for actual deployments, and then (often only partially) +generalized. + +.. note:: + + Some of these models may be under active development, and thus this + documentation not always fully up-to-date - please study the current + code before planning your project. + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + Disease Tracking + Project Management + Training Courses and Events diff --git a/docs/source/reference/models/business/project.rst b/docs/source/reference/models/business/project.rst new file mode 100644 index 0000000000..436942f2ec --- /dev/null +++ b/docs/source/reference/models/business/project.rst @@ -0,0 +1,4 @@ +Project Management +================== + +*to be written* diff --git a/docs/source/reference/models/business/training.rst b/docs/source/reference/models/business/training.rst new file mode 100644 index 0000000000..18f746d242 --- /dev/null +++ b/docs/source/reference/models/business/training.rst @@ -0,0 +1,5 @@ +Training Courses and Events +=========================== + +*to be written* + diff --git a/docs/source/reference/models/core/auth.rst b/docs/source/reference/models/core/auth.rst new file mode 100644 index 0000000000..8071a45c36 --- /dev/null +++ b/docs/source/reference/models/core/auth.rst @@ -0,0 +1,4 @@ +User Accounts and Roles - *auth* +================================ + +*to be written* diff --git a/docs/source/reference/models/core/doc.rst b/docs/source/reference/models/core/doc.rst new file mode 100644 index 0000000000..eff4a23dae --- /dev/null +++ b/docs/source/reference/models/core/doc.rst @@ -0,0 +1,5 @@ +Document Management - *doc* +=========================== + +*to be written* + diff --git a/docs/source/reference/models/core/er_core.png b/docs/source/reference/models/core/er_core.png new file mode 100644 index 0000000000..80864db42c Binary files /dev/null and b/docs/source/reference/models/core/er_core.png differ diff --git a/docs/source/reference/models/core/gis.rst b/docs/source/reference/models/core/gis.rst new file mode 100644 index 0000000000..89245fdcfe --- /dev/null +++ b/docs/source/reference/models/core/gis.rst @@ -0,0 +1,6 @@ +Geospatial Information and Maps - *gis* +======================================= + +*to be written* + + diff --git a/docs/source/reference/models/core/hrm.rst b/docs/source/reference/models/core/hrm.rst new file mode 100644 index 0000000000..c335c0c89f --- /dev/null +++ b/docs/source/reference/models/core/hrm.rst @@ -0,0 +1,4 @@ +Human Resources +=============== + +*to be written* diff --git a/docs/source/reference/models/core/index.rst b/docs/source/reference/models/core/index.rst new file mode 100644 index 0000000000..5a84fbaf43 --- /dev/null +++ b/docs/source/reference/models/core/index.rst @@ -0,0 +1,23 @@ +Core Models +=========== + +Core models form the basis of the Eden ASP database, defining base +entities *Persons*, *Organisations* and *Locations* that represent +the fundamental elements of the user world. + +These models are required for essential system functionality, and +therefore cannot be disabled. + +.. image:: er_core.png + :align: center + +.. toctree:: + :maxdepth: 1 + :caption: In Depth: + + Persons and Groups + Organisations and Sites + Human Resources + Users, Roles and Permissions + Geospatial Information and Maps + Document Management diff --git a/docs/source/reference/models/core/org.rst b/docs/source/reference/models/core/org.rst new file mode 100644 index 0000000000..2ddd7289fd --- /dev/null +++ b/docs/source/reference/models/core/org.rst @@ -0,0 +1,6 @@ +Organisations and Sites - *org* +=============================== + +*to be written* + + diff --git a/docs/source/reference/models/core/pr.png b/docs/source/reference/models/core/pr.png new file mode 100644 index 0000000000..57f1d7738a Binary files /dev/null and b/docs/source/reference/models/core/pr.png differ diff --git a/docs/source/reference/models/core/pr.rst b/docs/source/reference/models/core/pr.rst new file mode 100644 index 0000000000..46c21c4e02 --- /dev/null +++ b/docs/source/reference/models/core/pr.rst @@ -0,0 +1,32 @@ +Persons and Groups - *pr* +========================= + +This data model describes individual persons and groups of persons. + +Database Structure +------------------ + +.. image:: pr.png + :align: center + +Description +----------- + +======================= =========================== ========================================= +Table Type Description +======================= =========================== ========================================= +pr_address Object Component Addresses +pr_contact Object Component Contact information (Email, Phone, ...) +**pr_group** Main Entity Groups of persons +pr_group_member_role Taxonomy Role of the group member within the group +**pr_group_membership** Relationship Group membership +pr_group_status Taxonomy Status of the group +pr_group_tag Key-Value Tags for groups +pr_identity Component A person's identities (ID documents) +pr_image Object Component Images (e.g. Photos) +pr_pentity Object Table (Super-Entity) All entities representing persons +**pr_person** Main Entity Individual persons +pr_person_details Subtable Additional fields for pr_person +pr_person_tag Key-Value Tags for persons +pr_person_user Link Table Link between a person and a user account +======================= =========================== ========================================= diff --git a/docs/source/reference/models/ext/cms.rst b/docs/source/reference/models/ext/cms.rst new file mode 100644 index 0000000000..8c5596b426 --- /dev/null +++ b/docs/source/reference/models/ext/cms.rst @@ -0,0 +1,22 @@ +Content Management +================== + +The Content Management System (*cms*) is a place to store all kinds of +user-editable contents. Its main entity is the **Post** (=content item), +which can be linked to various :doc:`core entities <../core/index>`. +Posts are also *DocEntities*, i.e. can have attachments. + +.. image:: er_cms.png + +The CMS was originally designed for news and discussion feeds, but is +more commonly used for informative page contents including, but not +limited to: + + - page intros + - legal, contact and privacy information pages + - guidance on forms or form elements + - group announcements + - online user guides + +...as well as for notification templates. + diff --git a/docs/source/reference/models/ext/er_cms.png b/docs/source/reference/models/ext/er_cms.png new file mode 100644 index 0000000000..27d5e84e08 Binary files /dev/null and b/docs/source/reference/models/ext/er_cms.png differ diff --git a/docs/source/reference/models/ext/index.rst b/docs/source/reference/models/ext/index.rst new file mode 100644 index 0000000000..1342c72519 --- /dev/null +++ b/docs/source/reference/models/ext/index.rst @@ -0,0 +1,11 @@ +Extensions +========== + +Extension models implement data elements for non-essential system +functionality. + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + Content Management diff --git a/docs/source/reference/models/index.rst b/docs/source/reference/models/index.rst new file mode 100644 index 0000000000..26a7b5483d --- /dev/null +++ b/docs/source/reference/models/index.rst @@ -0,0 +1,10 @@ +Built-in Data Models +==================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + Core Models + Extensions + Business Data Models diff --git a/docs/source/reference/services/auth.rst b/docs/source/reference/services/auth.rst new file mode 100644 index 0000000000..e8d9442997 --- /dev/null +++ b/docs/source/reference/services/auth.rst @@ -0,0 +1,62 @@ +Authentication and Authorisation *auth* +======================================= + +Global authentication/authorisation service, accessible through **current.auth**. + +.. code-block:: python + + from gluon import current + + auth = current.auth + +User Status and Roles +--------------------- + +.. function:: auth.s3_logged_in() + + Check whether the user is logged in; attempts a HTTP Basic Auth login if not. + + :returns bool: whether the user is logged in or not + +.. function:: auth.s3_has_role(role, for_pe=None, include_admin=True) + + Check whether the user has a certain role. + + :param str|int role: the UID/ID of the role + :param int for_pe: the *pe_id* of a realm entity + :param bool include_admin: return True for ADMIN even if role is not explicitly assigned + + :returns bool: whether the user has the role (for the realm) + +.. TODO explain for_pe options + +Access Permissions +------------------ + +Access methods: + +=========== ========================== +Method Name Meaning +=========== ========================== +create create new records +read read records +update update existing records +delete delete records +review review unapproved records +approve approve records +=========== ========================== + +.. function:: auth.s3_has_permission(method, table, record_id=None, c=None, f=None): + + Check whether the current user has permission to perform an action + in the given context. + + :param str method: the access method + :param str|Table table: the table + :param int record_id: the record ID + :param str c: the controller name (if not specified, current.request.controller will be used) + :param str f: the function name (if not specified, current.request.function will be used) + + :returns bool: whether the intended action is permitted + +.. TODO auth.s3_accessible_query diff --git a/docs/source/reference/services/gis.rst b/docs/source/reference/services/gis.rst new file mode 100644 index 0000000000..bbd711bbeb --- /dev/null +++ b/docs/source/reference/services/gis.rst @@ -0,0 +1,3 @@ +Geospatial Information and Maps *gis* +===================================== + diff --git a/docs/source/reference/services/index.rst b/docs/source/reference/services/index.rst new file mode 100644 index 0000000000..f580b6c8d0 --- /dev/null +++ b/docs/source/reference/services/index.rst @@ -0,0 +1,25 @@ +Services +======== + +Services are thread-local global singleton objects, instantiated during +the *models* run. + +They can be accessed through :doc:`current ` , e.g.: + +.. code-block:: python + + from gluon import current + + s3db = current.s3db + +This section describes the services, and their most relevant functions. + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + Model Loader (s3db) + Authorization (auth) + Mapping (gis) + Messaging (msg) + XML Codec (xml) diff --git a/docs/source/reference/services/msg.rst b/docs/source/reference/services/msg.rst new file mode 100644 index 0000000000..e3acd4923b --- /dev/null +++ b/docs/source/reference/services/msg.rst @@ -0,0 +1,3 @@ +Messaging *msg* +=============== + diff --git a/docs/source/reference/services/s3db.rst b/docs/source/reference/services/s3db.rst new file mode 100644 index 0000000000..bafebe47aa --- /dev/null +++ b/docs/source/reference/services/s3db.rst @@ -0,0 +1,192 @@ +Model Loader *s3db* +=================== + +The **s3db** model loader provides access to database tables and +other named objects defined in dynamically loaded models. + +The model loader can be accessed through :doc:`current `: + +.. code-block:: python + :emphasize-lines: 3 + + from gluon import current + + s3db = current.s3db + +Accessing Tables and Objects +---------------------------- + +A table or other object defined in a dynamically loaded data model +can be accessed by name either as attribute or as key of *current.s3db*: + +.. code-block:: python + :caption: Example: accessing the org_organisation table using attribute-pattern + + table = s3db.org_organisation + +.. code-block:: python + :caption: Example: accessing the org_organisation table using key-pattern + + tablename = "org_organisation" + table = s3db[tablename] + +Either pattern will raise an *AttributeError* if the table or object is +not defined, e.g. when the module is disabled. + +Both access methods build on the lower-level *table()* method: + +.. function:: s3db.table(tablename, default=None, db_only=False) + + Access a named object (usually a Table instance) defined in a + dynamically loaded model. + + :param str tablename: the name of the table (or object) + :param default: the default to return if the table (or object) is not defined + :param bool db_only: return only Table instances, not other objects with the + given name + +.. note:: + If an *Exception* instance is passed as default, it will be raised + rather than returned. + +Table Settings +-------------- + +Table settings are used to configure entity-specific behaviors, e.g. forms, +list fields, CRUD callbacks and access rules. The following functions can be +used to manage table settings: + +.. function:: s3db.configure(tablename, **attr) + + Add or modify table settings. + + :param str tablename: the name of the table + :param attr: table settings as key-value pairs + +.. code-block:: python + :caption: Example: configuring table settings + + s3db.configure("org_organisation", + insertable = False, + list_fields = ["name", "acronym", "website"], + ) + +.. function:: s3db.get_config(tablename, key, default=None) + + Inspect table settings. + + :param str tablename: the name of the table + :param str key: the settings-key + :param default: the default value if setting is not defined for the table + :returns: the current value of the setting, or default + +.. code-block:: python + :caption: Example: inspecting table settings + + if s3db.get_config("org_organisation", "insertable", True): + # ... + else: + # ... + +.. function:: s3db.clear_config(tablename, *keys) + + Remove table settings. + + :param str tablename: the name of the table + :param keys: the keys for the settings to remove + +.. code-block:: python + :caption: Example: removing table settings + + s3db.clear_config("org_organisation", "list_fields") + +.. warning:: + + If *clear_config* is called without keys, **all** settings for the table + will be removed! + +Declaring Components +-------------------- + +The *add_components* method can be used to declare :doc:`components `. + +.. function:: s3db.add_components(tablename, **links) + + Declare components for a table. + + :param str tablename: the name of the table + :param links: component links + +.. code-block:: python + :caption: Example: declaring components + + s3db.add_components("org_organisation", + + # A 1:n component with foreign key + org_office = "organisation_id", + + # A 1:n component with foreign key, single entry + org_facility = {"joinby": "organisation_id", + "multiple": False, + }, + + # A m:n component with link table + project_project = {"link": "project_organisation", + "joinby": "organisation_id", + "key": "project_id", + }, + ) + +URL Method Handlers +------------------- + +.. function:: s3db.set_method(tablename, component=None, method=None, action=None) + + Configure a URL method for a table, or a component in the context of the table + + :param str tablename: the name of the table + :param str component: component alias + :param str method: name of the method (to use in URLs) + :param action: function or other callable to invoke for this method, + receives the CRUDRequest instance and controller keyword + parameters as arguments + +.. code-block:: python + :caption: Example: defining and configuring a handler for a URL method for a table + :emphasize-lines: 11 + + def check_in_func(r, **attr): + """ Handler for check_in method """ + + # Produce some output... + + # Return output to view + return {} + + # Configure check_in_func as handler for the "check_in" method + # (i.e. for URLs like /eden/pr/person/5/check_in): + s3db.set_method("pr_person", method="check_in", action=check_in_func) + +.. tip:: + + If a S3Method class is specified as action, it will be instantiated + when the method is called (lazy instantiation). + +.. function:: s3db.get_method(tablename, component=None, method=None) + + Get the handler for a URL method for a table, or a component in the context + of the table + + :param str tablename: the name of the table + :param str component: component alias + :param str method: name of the method + :returns: the handler configured for the method (or None) + +CRUD Callbacks +-------------- + +.. Topics to cover: + - onvalidation + - onaccept + +*to be written* diff --git a/docs/source/reference/services/xml.rst b/docs/source/reference/services/xml.rst new file mode 100644 index 0000000000..85aa47b4bb --- /dev/null +++ b/docs/source/reference/services/xml.rst @@ -0,0 +1,3 @@ +XML Encoder/Decoder *xml* +========================= + diff --git a/docs/source/reference/settings/index.rst b/docs/source/reference/settings/index.rst new file mode 100644 index 0000000000..e2334da01f --- /dev/null +++ b/docs/source/reference/settings/index.rst @@ -0,0 +1,6 @@ +Settings +======== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: diff --git a/models/000_1st_run.py b/models/000_1st_run.py index 73ab3424a1..37eb8e3a54 100644 --- a/models/000_1st_run.py +++ b/models/000_1st_run.py @@ -100,7 +100,6 @@ # Import S3Config import s3cfg -settings = s3cfg.S3Config() -current.deployment_settings = deployment_settings = settings +current.deployment_settings = deployment_settings = settings = s3cfg.S3Config() # END ========================================================================= diff --git a/models/00_db.py b/models/00_db.py index 40db551fbe..74ecd9361b 100644 --- a/models/00_db.py +++ b/models/00_db.py @@ -126,8 +126,8 @@ # GIS Module gis = current.gis = s3base.GIS() -# s3_request -s3_request = s3base.s3_request +# crud_request +crud_request = s3base.crud_request # Field Selectors FS = s3base.FS diff --git a/models/00_tables.py b/models/00_tables.py index cada7a7002..59d9338f73 100644 --- a/models/00_tables.py +++ b/models/00_tables.py @@ -54,8 +54,8 @@ import s3db.water # Set up current.s3db -from core import S3Model -s3db = current.s3db = S3Model() +from core import DataModel +s3db = current.s3db = DataModel() # ============================================================================= # Configure the auth models diff --git a/models/00_utils.py b/models/00_utils.py index 180e5fddef..a069e8b797 100644 --- a/models/00_utils.py +++ b/models/00_utils.py @@ -127,206 +127,8 @@ # @todo: replace by current.menu.override s3_menu_dict = {} -# ----------------------------------------------------------------------------- -def s3_rest_controller(prefix=None, resourcename=None, **attr): - """ - Helper function to apply the S3Resource REST interface - - @param prefix: the application prefix - @param resourcename: the resource name (without prefix) - @param attr: additional keyword parameters - - Any keyword parameters will be copied into the output dict (provided - that the output is a dict). If a keyword parameter is callable, then - it will be invoked, and its return value will be added to the output - dict instead. The callable receives the S3Request as its first and - only parameter. - - CRUD can be configured per table using: - - s3db.configure(tablename, **attr) - - *** Redirection: - - create_next URL to redirect to after a record has been created - update_next URL to redirect to after a record has been updated - delete_next URL to redirect to after a record has been deleted - - *** Form configuration: - - list_fields list of names of fields to include into list views - subheadings Sub-headings (see separate documentation) - listadd Enable/Disable add-form in list views - - *** CRUD configuration: - - editable Allow/Deny record updates in this table - deletable Allow/Deny record deletions in this table - insertable Allow/Deny record insertions into this table - copyable Allow/Deny record copying within this table - - *** Callbacks: - - create_onvalidation Function for additional record validation on create - create_onaccept Function after successful record insertion - - update_onvalidation Function for additional record validation on update - update_onaccept Function after successful record update - - onvalidation Fallback for both create_onvalidation and update_onvalidation - onaccept Fallback for both create_onaccept and update_onaccept - ondelete Function after record deletion - """ - - # Parse the request - dynamic = attr.get("dynamic") - if dynamic: - # Dynamic table controller - c = request.controller - f = request.function - attr = settings.customise_controller("%s_%s" % (c, f), **attr) - from core import DYNAMIC_PREFIX, s3_get_extension - r = s3_request(DYNAMIC_PREFIX, - dynamic, - f = "%s/%s" % (f, dynamic), - args = request.args[1:], - extension = s3_get_extension(request), - ) - else: - # Customise Controller from Template - attr = settings.customise_controller( - "%s_%s" % (prefix or request.controller, - resourcename or request.function, - ), - **attr) - r = s3_request(prefix, resourcename) - - # Customize target resource(s) from Template - r.customise_resource() - - # Configure standard method handlers - set_handler = r.set_handler - from s3db.cms import S3CMS - set_handler("cms", S3CMS) - set_handler("compose", s3base.S3Compose) - # @ToDo: Make work in Component Tabs: - set_handler("copy", lambda r, **attr: \ - redirect(URL(args = "create", - vars = {"from_record":r.id}))) - set_handler("deduplicate", s3base.S3Merge) - set_handler("filter", s3base.S3Filter) - set_handler("grouped", s3base.S3GroupedItemsReport) - set_handler("hierarchy", s3base.S3HierarchyCRUD) - set_handler("import", s3base.SpreadsheetImporter) - set_handler("map", s3base.S3Map) - set_handler("mform", s3base.S3MobileCRUD, representation="json") - set_handler("organize", s3base.S3Organizer) - set_handler("profile", s3base.S3Profile) - set_handler("report", s3base.S3Report) # For HTML, JSON - set_handler("report", s3base.S3Report, transform=True) # For GeoJSON - set_handler("search_ac", s3base.search_ac) - set_handler("summary", s3base.S3Summary) - set_handler("timeplot", s3base.S3TimePlot) - set_handler("xform", s3base.S3XForms) - - # List of methods rendering datatables with default action buttons - dt_methods = (None, "datatable", "datatable_f", "summary") - - # List of methods rendering datatables with custom action buttons, - # => for these, s3.actions must not be touched, see below - # (defining here allows postp to add a custom method to the list) - s3.action_methods = ("import", - "review", - "approve", - "reject", - "deduplicate", - ) - - # Execute the request - output = r(**attr) - - method = r.method - if isinstance(output, dict) and method in dt_methods: - - if s3.actions is None: - - # Add default action buttons - prefix, name, table, tablename = r.target() - authorised = auth.s3_has_permission("update", tablename) - - # If a component has components itself, then action buttons - # can be forwarded to the native controller by setting native=True - if r.component and s3db.has_components(table): - native = output.get("native", False) - else: - native = False - - # Get table config - get_config = s3db.get_config - listadd = get_config(tablename, "listadd", True) - - # Which is the standard open-action? - if settings.get_ui_open_read_first(): - # Always read, irrespective permissions - editable = False - else: - editable = get_config(tablename, "editable", True) - if editable and \ - auth.permission.ownership_required("update", table): - # User cannot edit all records in the table - if settings.get_ui_auto_open_update(): - # Decide automatically per-record (implicit method) - editable = "auto" - else: - # Always open read first (explicit read) - editable = False - - deletable = get_config(tablename, "deletable", True) - copyable = get_config(tablename, "copyable", False) - - # URL to open the resource - open_url = r.resource.crud._linkto(r, - authorised = authorised, - update = editable, - native = native)("[id]") - - # Add action buttons for Open/Delete/Copy as appropriate - s3_action_buttons(r, - deletable = deletable, - copyable = copyable, - editable = editable, - read_url = open_url, - update_url = open_url - # To use modals - #update_url = "%s.popup?refresh=list" % open_url - ) - - # Override Add-button, link to native controller and put - # the primary key into get_vars for automatic linking - if native and not listadd and \ - auth.s3_has_permission("create", tablename): - label = s3base.S3CRUD.crud_string(tablename, - "label_create") - component = r.resource.components[name] - fkey = "%s.%s" % (name, component.fkey) - get_vars_copy = get_vars.copy() - get_vars_copy.update({fkey: r.record[component.fkey]}) - url = URL(prefix, name, - args = ["create"], - vars = get_vars_copy, - ) - add_btn = A(label, - _href = url, - _class = "action-btn", - ) - output.update(add_btn = add_btn) - - elif method not in s3.action_methods: - s3.actions = None - - return output - # Enable access to this function from modules -current.rest_controller = s3_rest_controller +from core import crud_controller +current.crud_controller = crud_controller # END ========================================================================= diff --git a/modules/core/__init__.py b/modules/core/__init__.py index 1d54890dcd..da4c7bbb0d 100755 --- a/modules/core/__init__.py +++ b/modules/core/__init__.py @@ -41,10 +41,10 @@ from .errors import * from .filters import * from .gis import * -from .io import * from .methods import * from .model import * from .msg import * +from .resource import * from .sync import * from .tools import * from .ui import * diff --git a/modules/core/auth/base.py b/modules/core/auth/base.py index 9bc48cd9a3..4098f8ac57 100644 --- a/modules/core/auth/base.py +++ b/modules/core/auth/base.py @@ -48,7 +48,7 @@ from s3dal import Row, Rows, Query, Field, original_tablename -from ..controller import S3Request +from ..controller import CRUDRequest from ..model import S3MetaFields, s3_comments from ..tools import S3Represent, S3Tracker, s3_addrow, s3_mark_required, s3_str, IS_ISO639_2_LANGUAGE_CODE @@ -2152,7 +2152,7 @@ def configure_user_fields(self, pe_ids=None): )) # ------------------------------------------------------------------------- - def s3_import_prep(self, data): + def s3_import_prep(self, tree): """ Called when users are imported from CSV @@ -2168,8 +2168,6 @@ def s3_import_prep(self, data): otable = s3db.org_organisation btable = s3db.org_organisation_branch - tree = data[1] - ORG_ADMIN = not self.s3_has_role("ADMIN") TRANSLATE = current.deployment_settings.get_L10n_translate_org_organisation() if TRANSLATE: @@ -3449,10 +3447,10 @@ def customise(hr_id): """ Customise hrm_human_resource """ customise = settings.customise_resource(htablename) if customise: - request = S3Request("hrm", "human_resource", - current.request, - args = [str(hr_id)] if hr_id else [], - ) + request = CRUDRequest("hrm", "human_resource", + current.request, + args = [str(hr_id)] if hr_id else [], + ) customise(request, htablename) # Determine the site ID @@ -3600,9 +3598,9 @@ def s3_link_to_member(self, # Customise the resource customise = current.deployment_settings.customise_resource(mtablename) if customise: - request = S3Request("member", "membership", - current.request, - args = [str(member_id)]) + request = CRUDRequest("member", "membership", + current.request, + args = [str(member_id)]) customise(request, mtablename) self.s3_set_record_owner(mtable, member_id) diff --git a/modules/core/controller.py b/modules/core/controller.py index 092c248d86..cbaa7d9ab8 100644 --- a/modules/core/controller.py +++ b/modules/core/controller.py @@ -27,31 +27,29 @@ OTHER DEALINGS IN THE SOFTWARE. """ -__all__ = ("S3Request", - "s3_request", +__all__ = ("CRUDRequest", + "crud_request", + "crud_controller", ) import json import os -import re import sys from io import StringIO -from urllib.request import urlopen -from gluon import current, redirect, HTTP, URL +from gluon import current, redirect, A, HTTP, URL from gluon.storage import Storage -from .model import S3Resource -from .tools import s3_parse_datetime, s3_get_extension, s3_keep_messages, s3_remove_last_record_id, s3_store_last_record_id, s3_str +from .resource import S3Resource +from .tools import s3_get_extension, s3_keep_messages, s3_store_last_record_id, s3_str -REGEX_FILTER = re.compile(r".+\..+|.*\(.+\).*") HTTP_METHODS = ("GET", "PUT", "POST", "DELETE") # ============================================================================= -class S3Request(object): +class CRUDRequest(object): """ - Class to handle RESTful requests + Class to handle CRUD requests """ INTERACTIVE_FORMATS = ("html", "iframe", "popup", "dl") @@ -69,76 +67,79 @@ def __init__(self, extension = None, get_vars = None, post_vars = None, - http = None): + http = None, + ): """ Constructor - @param prefix: the table name prefix - @param name: the table name - @param c: the controller prefix - @param f: the controller function - @param args: list of request arguments - @param vars: dict of request variables - @param extension: the format extension (representation) - @param get_vars: the URL query variables (overrides vars) - @param post_vars: the POST variables (overrides vars) - @param http: the HTTP method (GET, PUT, POST, or DELETE) - - @note: all parameters fall back to the attributes of the - current web2py request object + :param prefix: the table name prefix + :param name: the table name + :param c: the controller prefix + :param f: the controller function + :param args: list of request arguments + :param vars: dict of request variables + :param extension: the format extension (representation) + :param get_vars: the URL query variables (overrides vars) + :param post_vars: the POST variables (overrides vars) + :param http: the HTTP method (GET, PUT, POST, or DELETE) + + .. note:: all parameters fall back to the attributes of the + current web2py request object """ - auth = current.auth - - # Common settings - # XSLT Paths self.XSLT_PATH = "static/formats" self.XSLT_EXTENSION = "xsl" - # Attached files - self.files = Storage() - # Allow override of controller/function self.controller = c or self.controller self.function = f or self.function + + # Format extension + request = current.request if "." in self.function: self.function, ext = self.function.split(".", 1) if extension is None: extension = ext + self.extension = extension or request.extension + + # Check permission + auth = current.auth if c or f: if not auth.permission.has_permission("read", - c=self.controller, - f=self.function): + c = self.controller, + f = self.function): auth.permission.fail() + + # HTTP method + self.http = http or request.env.request_method + + # Attached files + self.files = Storage() + # Allow override of request args/vars if args is not None: - if isinstance(args, (list, tuple)): - self.args = args - else: - self.args = [args] - if get_vars is not None: - self.get_vars = get_vars - self.vars = get_vars.copy() + self.args = args if isinstance(args, (list, tuple)) else [args] + + if get_vars is not None or post_vars is not None: + + self.vars = request_vars = Storage() + + if get_vars is not None: + self.get_vars = Storage(get_vars) + request_vars.update(self.get_vars) + if post_vars is not None: - self.vars.update(post_vars) - else: - self.vars.update(self.post_vars) - if post_vars is not None: - self.post_vars = post_vars - if get_vars is None: - self.vars = post_vars.copy() - self.vars.update(self.get_vars) - if get_vars is None and post_vars is None and vars is not None: - self.vars = vars - self.get_vars = vars - self.post_vars = Storage() + self.post_vars = Storage(post_vars) + request_vars.update(self.post_vars) - self.extension = extension or current.request.extension - self.http = http or current.request.env.request_method + elif vars is not None: - # Main resource attributes + self.vars = self.get_vars = Storage(vars) + self.post_vars = Storage() + + # Target table prefix/name if r is not None: if not prefix: prefix = r.prefix @@ -150,47 +151,50 @@ def __init__(self, # Parse the request self.__parse() self.custom_action = None - get_vars = Storage(self.get_vars) # Interactive representation format? self.interactive = self.representation in self.INTERACTIVE_FORMATS + get_vars = self.get_vars + # Show information on deleted records? include_deleted = False if self.representation == "xml" and "include_deleted" in get_vars: include_deleted = True - if "components" in get_vars: - cnames = get_vars["components"] - if isinstance(cnames, list): - cnames = ",".join(cnames) - cnames = cnames.split(",") - if len(cnames) == 1 and cnames[0].lower() == "none": - cnames = [] + + # Which components to load? + component_name = self.component_name + if not component_name: + if "components" in get_vars: + cnames = get_vars["components"] + if isinstance(cnames, list): + cnames = ",".join(cnames) + cnames = cnames.split(",") + if len(cnames) == 1 and cnames[0].lower() == "none": + cnames = [] + components = cnames + else: + components = None else: - cnames = None + components = component_name # Append component ID to the URL query - component_name = self.component_name + url_query = dict(get_vars) component_id = self.component_id if component_name and component_id: varname = "%s.id" % component_name if varname in get_vars: - var = get_vars[varname] + var = url_query[varname] if not isinstance(var, (list, tuple)): var = [var] var.append(component_id) - get_vars[varname] = var + url_query[varname] = var else: - get_vars[varname] = component_id - - # Define the target resource - _filter = current.response.s3.filter - components = component_name - if components is None: - components = cnames + url_query[varname] = component_id tablename = "%s_%s" % (self.prefix, self.name) + # Handle approval settings if not current.deployment_settings.get_auth_record_approval(): # Record Approval is off approved, unapproved = True, False @@ -204,10 +208,11 @@ def __init__(self, else: approved, unapproved = True, False + # Instantiate resource self.resource = S3Resource(tablename, id = self.id, - filter = _filter, - vars = get_vars, + filter = current.response.s3.filter, + vars = url_query, components = components, approved = approved, unapproved = unapproved, @@ -216,228 +221,50 @@ def __init__(self, filter_component = component_name, ) - self.tablename = self.resource.tablename - table = self.table = self.resource.table + resource = self.resource + self.tablename = resource.tablename + table = self.table = resource.table # Try to load the master record self.record = None uid = self.vars.get("%s.uid" % self.name) if self.id or uid and not isinstance(uid, (list, tuple)): # Single record expected - self.resource.load() - if len(self.resource) == 1: - self.record = self.resource.records().first() - _id = table._id.name - self.id = self.record[_id] + resource.load() + if len(resource) == 1: + self.record = resource.records().first() + self.id = self.record[table._id.name] s3_store_last_record_id(self.tablename, self.id) else: raise KeyError(current.ERROR.BAD_RECORD) # Identify the component - self.component = None - if self.component_name: - c = self.resource.components.get(self.component_name) + self.component = component = None + if component_name: + c = resource.components.get(component_name) if c: - self.component = c + self.component = component = c else: - error = "%s not a component of %s" % (self.component_name, - self.resource.tablename) + error = "%s not a component of %s" % \ + (self.component_name, self.tablename) raise AttributeError(error) # Identify link table and link ID - self.link = None - self.link_id = None - - if self.component is not None: - self.link = self.component.link - if self.link and self.id and self.component_id: - self.link_id = self.link.link_id(self.id, self.component_id) - if self.link_id is None: + link = link_id = None + if component is not None: + link = component.link + if link and self.id and self.component_id: + link_id = link.link_id(self.id, self.component_id) + if link_id is None: raise KeyError(current.ERROR.BAD_RECORD) + self.link = link + self.link_id = link_id - # Store method handlers - self._handlers = {} - set_handler = self.set_handler - - set_handler("export_tree", self.get_tree, - http=("GET",), transform=True) - set_handler("import_tree", self.put_tree, - http=("GET", "PUT", "POST"), transform=True) - set_handler("fields", self.get_fields, - http=("GET",), transform=True) - set_handler("options", self.get_options, - http=("GET",), - representation = ("__transform__", "json"), - ) - - sync = current.sync - set_handler("sync", sync, - http=("GET", "PUT", "POST",), transform=True) - set_handler("sync_log", sync.log, - http=("GET",), transform=True) - set_handler("sync_log", sync.log, - http=("GET",), transform=False) - - # Initialize CRUD - self.resource.crud(self, method="_init") - if self.component is not None: - self.component.crud(self, method="_init") - - # ------------------------------------------------------------------------- - # Method handler configuration - # ------------------------------------------------------------------------- - def set_handler(self, method, handler, - http = None, - representation = None, - transform = False): - """ - Set a method handler for this request - - @param method: the method name - @param handler: the handler function - @type handler: handler(S3Request, **attr) - @param http: restrict to these HTTP methods, list|tuple - @param representation: register handler for non-transformable data - formats - @param transform: register handler for transformable data formats - (overrides representation) - """ - - if http is None: - http = HTTP_METHODS - else: - if not isinstance(http, (tuple, list)): - http = (http,) - - if transform: - representation = ("__transform__",) - elif not representation: - representation = (self.DEFAULT_REPRESENTATION,) - else: - if not isinstance(representation, (tuple, list)): - representation = (representation,) - - if not isinstance(method, (tuple, list)): - method = (method,) - - handlers = self._handlers - for h in http: - if h not in HTTP_METHODS: - continue - format_hooks = handlers.get(h) - if format_hooks is None: - format_hooks = handlers[h] = {} - for r in representation: - method_hooks = format_hooks.get(r) - if method_hooks is None: - method_hooks = format_hooks[r] = {} - for m in method: - method_hooks[m] = handler - - # ------------------------------------------------------------------------- - def get_handler(self, method, transform=False): - """ - Get a method handler for this request - - @param method: the method name - @param transform: get handler for transformable data format - - @return: the method handler - """ - - handlers = self._handlers - - http_hooks = handlers.get(self.http) - if not http_hooks: - return None - - DEFAULT_REPRESENTATION = self.DEFAULT_REPRESENTATION - hooks = http_hooks.get(DEFAULT_REPRESENTATION) - if hooks: - method_hooks = dict(hooks) - else: - method_hooks = {} - - representation = "__transform__" if transform else self.representation - if representation and representation != DEFAULT_REPRESENTATION: - hooks = http_hooks.get(representation) - if hooks: - method_hooks.update(hooks) - - if not method: - methods = (None,) - else: - methods = (method, None) - for m in methods: - handler = method_hooks.get(m) - if handler is not None: - break - - if isinstance(handler, type): - return handler() - else: - return handler - - # ------------------------------------------------------------------------- - def get_widget_handler(self, method): - """ - Get the widget handler for a method - - @param r: the S3Request - @param method: the widget method - """ - - if self.component: - resource = self.component - if resource.link: - resource = resource.link - else: - resource = self.resource - prefix, name = self.prefix, self.name - component_name = self.component_name - - custom_action = current.s3db.get_method(prefix, - name, - component_name=component_name, - method=method) - - http = self.http - handler = None - - if method and custom_action: - handler = custom_action - - if http == "GET": - if not method: - if resource.count() == 1: - method = "read" - else: - method = "list" - transform = self.transformable() - handler = self.get_handler(method, transform=transform) - - elif http == "PUT": - transform = self.transformable(method="import") - handler = self.get_handler(method, transform=transform) - - elif http == "POST": - transform = self.transformable(method="import") - return self.get_handler(method, transform=transform) + # Initialize default methods + self._default_methods = None - elif http == "DELETE": - if method: - return self.get_handler(method) - else: - return self.get_handler("delete") - - else: - return None - - if handler is None: - handler = resource.crud - if isinstance(handler, type): - handler = handler() - return handler + # Initialize next-URL + self.next = None # ------------------------------------------------------------------------- # Request Parser @@ -512,7 +339,7 @@ def __search(self): in POST vars (if multipart), or from JSON request body (if not multipart or $search=ajax). - NB: overrides S3Request method as GET (r.http) to trigger + NB: overrides CRUDRequest method as GET (r.http) to trigger the correct method handlers, but will not change current.request.env.request_method """ @@ -536,7 +363,6 @@ def __search(self): body = self.body body.seek(0) # Decode request body (=bytes stream) into a str - # - json.load/loads do not accept bytes in Py3 before 3.6 # - minor performance advantage by avoiding the need for # json.loads to detect the encoding s = body.read().decode("utf-8") @@ -587,13 +413,101 @@ def __search(self): self.vars.update(self.post_vars) # ------------------------------------------------------------------------- - # REST Interface + # Method handlers + # ------------------------------------------------------------------------- + @property + def default_methods(self): + """ + Default method handlers as dict {method: handler} + """ + + methods = self._default_methods + + if not methods: + from .methods import RESTful, S3Filter, S3GroupedItemsReport, \ + S3HierarchyCRUD, S3Map, S3Merge, S3MobileCRUD, \ + S3Organizer, S3Profile, S3Report, S3Summary, \ + S3TimePlot, S3XForms, SpreadsheetImporter + + methods = {"deduplicate": S3Merge, + "fields": RESTful, + "filter": S3Filter, + "grouped": S3GroupedItemsReport, + "hierarchy": S3HierarchyCRUD, + "import": SpreadsheetImporter, + "map": S3Map, + "mform": S3MobileCRUD, + "options": RESTful, + "organize": S3Organizer, + "profile": S3Profile, + "report": S3Report, + "summary": S3Summary, + "sync": current.sync, + "timeplot": S3TimePlot, + "xform": S3XForms, + } + + methods["copy"] = lambda r, **attr: redirect(URL(args = "create", + vars = {"from_record": r.id}, + )) + + from .msg import S3Compose + methods["compose"] = S3Compose + + from .ui import search_ac + methods["search_ac"] = search_ac + + try: + from s3db.cms import S3CMS + except ImportError: + current.log.error("S3CMS default method not found") + else: + methods["cms"] = S3CMS + + self._default_methods = methods + + return methods + + # ------------------------------------------------------------------------- + def get_widget_handler(self, method): + """ + Get the widget handler for a method + + :param r: the CRUDRequest + :param method: the widget method + """ + + handler = None + + if method: + handler = current.s3db.get_method(self.tablename, + component = self.component_name, + method = method, + ) + if handler is None: + if self.http == "GET" and not method: + component = self.component + if component: + resource = component.link if component.link else component + else: + resource = self.resource + method = "read" if resource.count() == 1 else "list" + handler = self.default_methods.get(method) + + if handler is None: + from .methods import S3CRUD + handler = S3CRUD() + + return handler() if isinstance(handler, type) else handler + + # ------------------------------------------------------------------------- + # Controller # ------------------------------------------------------------------------- def __call__(self, **attr): """ Execute this request - @param attr: Parameters for the method handler + :param attr: Controller parameters """ response = current.response @@ -626,23 +540,20 @@ def __call__(self, **attr): pre = preprocess(self) # Re-read representation after preprocess: representation = self.representation - if pre and isinstance(pre, dict): + if not pre: + self.error(400, current.ERROR.BAD_REQUEST) + elif isinstance(pre, dict): bypass = pre.get("bypass", False) is True output = pre.get("output") - if not bypass: - success = pre.get("success", True) - if not success: - if representation == "html" and output: - if isinstance(output, dict): - output["r"] = self - return output - else: - status = pre.get("status", 400) - message = pre.get("message", - current.ERROR.BAD_REQUEST) - self.error(status, message) - elif not pre: - self.error(400, current.ERROR.BAD_REQUEST) + success = pre.get("success", True) + if not bypass and not success: + if representation == "html" and output: + if isinstance(output, dict): + output["r"] = self + return output + status = pre.get("status", 400) + message = pre.get("message", current.ERROR.BAD_REQUEST) + self.error(status, message) # Default view if representation not in ("html", "popup"): @@ -653,39 +564,36 @@ def __call__(self, **attr): "text/html") # Custom action? - if not self.custom_action: - action = current.s3db.get_method(self.prefix, - self.name, - component_name = self.component_name, - method = self.method) - if isinstance(action, type): - self.custom_action = action() - else: - self.custom_action = action + custom_action = self.custom_action + if not custom_action: + custom_action = current.s3db.get_method(self.tablename, + component = self.component_name, + method = self.method, + ) + self.custom_action = custom_action # Method handling - http = self.http + http, method = self.http, self.method handler = None if not bypass: - # Find the method handler - if self.method and self.custom_action: - handler = self.custom_action - elif http == "GET": - handler = self.__GET() - elif http == "PUT": - handler = self.__PUT() - elif http == "POST": - handler = self.__POST() - elif http == "DELETE": - handler = self.__DELETE() - else: - self.error(405, current.ERROR.BAD_METHOD) - # Invoke the method handler - if handler is not None: - output = handler(self, **attr) + if custom_action: + handler = custom_action else: - # Fall back to CRUD - output = self.resource.crud(self, **attr) + handler = self.default_methods.get(method) + if not handler: + m = "import" if http in ("PUT", "POST") else None + if not method and \ + (http not in ("GET", "POST") or self.transformable(method=m)): + from .methods import RESTful + handler = RESTful + elif http in HTTP_METHODS: + from .methods import S3CRUD + handler = S3CRUD + else: + self.error(405, current.ERROR.BAD_METHOD) + if isinstance(handler, type): + handler = handler() + output = handler(self, **attr) # Post-process if s3 is not None: @@ -717,535 +625,6 @@ def __call__(self, **attr): return output - # ------------------------------------------------------------------------- - def __GET(self, resource=None): - """ - Get the GET method handler - """ - - method = self.method - transform = False - if method is None or method in ("read", "display", "update"): - if self.transformable(): - method = "export_tree" - transform = True - elif self.component: - resource = self.resource - if self.interactive and resource.count() == 1: - # Load the record - if not resource._rows: - resource.load(start=0, limit=1) - if resource._rows: - self.record = resource._rows[0] - self.id = resource.get_id() - self.uid = resource.get_uid() - if self.component.multiple and not self.component_id: - method = "list" - else: - method = "read" - elif self.id or method in ("read", "display", "update"): - # Enforce single record - resource = self.resource - if not resource._rows: - resource.load(start=0, limit=1) - if resource._rows: - self.record = resource._rows[0] - self.id = resource.get_id() - self.uid = resource.get_uid() - else: - # Record not found => go to list - self.error(404, current.ERROR.BAD_RECORD, - next = self.url(id="", method=""), - ) - method = "read" - else: - method = "list" - - elif method in ("create", "update"): - if self.transformable(method="import"): - method = "import_tree" - transform = True - - elif method == "delete": - return self.__DELETE() - - elif method == "clear" and not self.component: - s3_remove_last_record_id(self.tablename) - self.next = URL(r=self, f=self.name) - return lambda r, **attr: None - - elif self.transformable(): - transform = True - - return self.get_handler(method, transform=transform) - - # ------------------------------------------------------------------------- - def __PUT(self): - """ - Get the PUT method handler - """ - - transform = self.transformable(method="import") - - method = self.method - if not method and transform: - method = "import_tree" - - return self.get_handler(method, transform=transform) - - # ------------------------------------------------------------------------- - def __POST(self): - """ - Get the POST method handler - """ - - if self.method == "delete": - return self.__DELETE() - else: - if self.transformable(method="import"): - return self.__PUT() - else: - post_vars = self.post_vars - table = self.target()[2] - if "deleted" in table and "id" not in post_vars: # and "uuid" not in post_vars: - original = S3Resource.original(table, post_vars) - if original and original.deleted: - self.post_vars["id"] = original.id - self.vars["id"] = original.id - return self.__GET() - - # ------------------------------------------------------------------------- - def __DELETE(self): - """ - Get the DELETE method handler - """ - - if self.method: - return self.get_handler(self.method) - else: - return self.get_handler("delete") - - # ------------------------------------------------------------------------- - # Built-in method handlers - # ------------------------------------------------------------------------- - @staticmethod - def get_tree(r, **attr): - """ - XML Element tree export method - - @param r: the S3Request instance - @param attr: controller attributes - """ - - get_vars = r.get_vars - args = Storage() - - # Slicing - start = get_vars.get("start") - if start is not None: - try: - start = int(start) - except ValueError: - start = None - limit = get_vars.get("limit") - if limit is not None: - try: - limit = int(limit) - except ValueError: - limit = None - - # msince - msince = get_vars.get("msince") - if msince is not None: - msince = s3_parse_datetime(msince) - - # Show IDs (default: False) - if "show_ids" in get_vars: - if get_vars["show_ids"].lower() == "true": - current.xml.show_ids = True - - # Show URLs (default: True) - if "show_urls" in get_vars: - if get_vars["show_urls"].lower() == "false": - current.xml.show_urls = False - - # Mobile data export (default: False) - mdata = get_vars.get("mdata") == "1" - - # Maxbounds (default: False) - maxbounds = False - if "maxbounds" in get_vars: - if get_vars["maxbounds"].lower() == "true": - maxbounds = True - if r.representation in ("gpx", "osm"): - maxbounds = True - - # Components of the master resource (tablenames) - if "mcomponents" in get_vars: - mcomponents = get_vars["mcomponents"] - if str(mcomponents).lower() == "none": - mcomponents = None - elif not isinstance(mcomponents, list): - mcomponents = mcomponents.split(",") - else: - mcomponents = [] # all - - # Components of referenced resources (tablenames) - if "rcomponents" in get_vars: - rcomponents = get_vars["rcomponents"] - if str(rcomponents).lower() == "none": - rcomponents = None - elif not isinstance(rcomponents, list): - rcomponents = rcomponents.split(",") - else: - rcomponents = None - - # Maximum reference resolution depth - if "maxdepth" in get_vars: - try: - args["maxdepth"] = int(get_vars["maxdepth"]) - except ValueError: - pass - - # References to resolve (field names) - if "references" in get_vars: - references = get_vars["references"] - if str(references).lower() == "none": - references = [] - elif not isinstance(references, list): - references = references.split(",") - else: - references = None # all - - # Export field selection - if "fields" in get_vars: - fields = get_vars["fields"] - if str(fields).lower() == "none": - fields = [] - elif not isinstance(fields, list): - fields = fields.split(",") - else: - fields = None # all - - # Find XSLT stylesheet - stylesheet = r.stylesheet() - - # Add stylesheet parameters - if stylesheet is not None: - if r.component: - args["id"] = r.id - args["component"] = r.component.tablename - if r.component.alias: - args["alias"] = r.component.alias - mode = get_vars.get("xsltmode") - if mode is not None: - args["mode"] = mode - - # Set response headers - response = current.response - s3 = response.s3 - headers = response.headers - representation = r.representation - if representation in s3.json_formats: - as_json = True - default = "application/json" - else: - as_json = False - default = "text/xml" - headers["Content-Type"] = s3.content_type.get(representation, - default) - - # Export the resource - resource = r.resource - target = r.target()[3] - if target == resource.tablename: - # Master resource targetted - target = None - output = resource.export_xml(start = start, - limit = limit, - msince = msince, - fields = fields, - dereference = True, - # maxdepth in args - references = references, - mdata = mdata, - mcomponents = mcomponents, - rcomponents = rcomponents, - stylesheet = stylesheet, - as_json = as_json, - maxbounds = maxbounds, - target = target, - **args) - # Transformation error? - if not output: - r.error(400, "XSLT Transformation Error: %s " % current.xml.error) - - return output - - # ------------------------------------------------------------------------- - @staticmethod - def put_tree(r, **attr): - """ - XML Element tree import method - - @param r: the S3Request method - @param attr: controller attributes - """ - - get_vars = r.get_vars - - # Skip invalid records? - if "ignore_errors" in get_vars: - ignore_errors = True - else: - ignore_errors = False - - # Find all source names in the URL vars - def findnames(get_vars, name): - nlist = [] - if name in get_vars: - names = get_vars[name] - if isinstance(names, (list, tuple)): - names = ",".join(names) - names = names.split(",") - for n in names: - if n[0] == "(" and ")" in n[1:]: - nlist.append(n[1:].split(")", 1)) - else: - nlist.append([None, n]) - return nlist - filenames = findnames(get_vars, "filename") - fetchurls = findnames(get_vars, "fetchurl") - source_url = None - - # Get the source(s) - s3 = current.response.s3 - json_formats = s3.json_formats - csv_formats = s3.csv_formats - source = [] - representation = r.representation - if representation in json_formats or representation in csv_formats: - if filenames: - try: - for f in filenames: - source.append((f[0], open(f[1], "rb"))) - except: - source = [] - elif fetchurls: - try: - for u in fetchurls: - source.append((u[0], urlopen(u[1]))) - except: - source = [] - elif r.http != "GET": - source = r.read_body() - else: - if filenames: - source = filenames - elif fetchurls: - source = fetchurls - # Assume only 1 URL for GeoRSS feed caching - source_url = fetchurls[0][1] - elif r.http != "GET": - source = r.read_body() - if not source: - if filenames or fetchurls: - # Error: source not found - r.error(400, "Invalid source") - else: - # No source specified => return resource structure - return r.get_struct(r, **attr) - - # Find XSLT stylesheet - stylesheet = r.stylesheet(method="import") - # Target IDs - if r.method == "create": - _id = None - else: - _id = r.id - - # Transformation mode? - if "xsltmode" in get_vars: - args = {"xsltmode": get_vars["xsltmode"]} - else: - args = {} - # These 3 options are called by gis.show_map() & read by the - # GeoRSS Import stylesheet to populate the gis_cache table - # Source URL: For GeoRSS/KML Feed caching - if source_url: - args["source_url"] = source_url - # Data Field: For GeoRSS/KML Feed popups - if "data_field" in get_vars: - args["data_field"] = get_vars["data_field"] - # Image Field: For GeoRSS/KML Feed popups - if "image_field" in get_vars: - args["image_field"] = get_vars["image_field"] - - # Format type? - if representation in json_formats: - representation = "json" - elif representation in csv_formats: - representation = "csv" - else: - representation = "xml" - - try: - output = r.resource.import_xml(source, - id=_id, - format=representation, - files=r.files, - stylesheet=stylesheet, - ignore_errors=ignore_errors, - **args) - except IOError: - current.auth.permission.fail() - except SyntaxError: - e = sys.exc_info()[1] - if hasattr(e, "message"): - e = e.message - r.error(400, e) - - return output - - # ------------------------------------------------------------------------- - @staticmethod - def get_struct(r, **attr): - """ - Resource structure introspection method - - @param r: the S3Request instance - @param attr: controller attributes - """ - - response = current.response - json_formats = response.s3.json_formats - if r.representation in json_formats: - as_json = True - content_type = "application/json" - else: - as_json = False - content_type = "text/xml" - get_vars = r.get_vars - meta = str(get_vars.get("meta", False)).lower() == "true" - opts = str(get_vars.get("options", False)).lower() == "true" - refs = str(get_vars.get("references", False)).lower() == "true" - stylesheet = r.stylesheet() - output = r.resource.export_struct(meta=meta, - options=opts, - references=refs, - stylesheet=stylesheet, - as_json=as_json) - if output is None: - # Transformation error - r.error(400, current.xml.error) - response.headers["Content-Type"] = content_type - return output - - # ------------------------------------------------------------------------- - @staticmethod - def get_fields(r, **attr): - """ - Resource structure introspection method (single table) - - @param r: the S3Request instance - @param attr: controller attributes - """ - - representation = r.representation - if representation == "xml": - output = r.resource.export_fields(component=r.component_name) - content_type = "text/xml" - elif representation == "s3json": - output = r.resource.export_fields(component=r.component_name, - as_json=True) - content_type = "application/json" - else: - r.error(415, current.ERROR.BAD_FORMAT) - response = current.response - response.headers["Content-Type"] = content_type - return output - - # ------------------------------------------------------------------------- - @staticmethod - def get_options(r, **attr): - """ - Field options introspection method (single table) - - @param r: the S3Request instance - @param attr: controller attributes - """ - - get_vars = r.get_vars - - items = get_vars.get("field") - if items: - if not isinstance(items, (list, tuple)): - items = [items] - fields = [] - add_fields = fields.extend - for item in items: - f = item.split(",") - if f: - add_fields(f) - else: - fields = None - - if "hierarchy" in get_vars: - hierarchy = get_vars["hierarchy"].lower() not in ("false", "0") - else: - hierarchy = False - - if "only_last" in get_vars: - only_last = get_vars["only_last"].lower() not in ("false", "0") - else: - only_last = False - - if "show_uids" in get_vars: - show_uids = get_vars["show_uids"].lower() not in ("false", "0") - else: - show_uids = False - - representation = r.representation - flat = False - if representation == "xml": - only_last = False - as_json = False - content_type = "text/xml" - elif representation == "s3json": - show_uids = False - as_json = True - content_type = "application/json" - elif representation == "json" and fields and len(fields) == 1: - # JSON option supported for flat data structures only - # e.g. for use by jquery.jeditable - flat = True - show_uids = False - as_json = True - content_type = "application/json" - else: - r.error(415, current.ERROR.BAD_FORMAT) - - component = r.component_name - output = r.resource.export_options(component=component, - fields=fields, - show_uids=show_uids, - only_last=only_last, - hierarchy=hierarchy, - as_json=as_json, - ) - - if flat: - s3json = json.loads(output) - output = {} - options = s3json.get("option") - if options: - for item in options: - output[item.get("@value")] = item.get("$", "") - output = json.dumps(output) - - current.response.headers["Content-Type"] = content_type - return output - # ------------------------------------------------------------------------- # Tools # ------------------------------------------------------------------------- @@ -1253,19 +632,19 @@ def factory(self, **args): """ Generate a new request for the same resource - @param args: arguments for request constructor + :param args: arguments for request constructor """ - return s3_request(r=self, **args) + return crud_request(r=self, **args) # ------------------------------------------------------------------------- def __getattr__(self, key): """ - Called upon S3Request. - looks up the value for the + Called upon CRUDRequest. - looks up the value for the attribute. Falls back to current.request if the attribute is - not defined in this S3Request. + not defined in this CRUDRequest. - @param key: the key to lookup + :param key: the key to lookup """ if key in self.__dict__: @@ -1282,7 +661,7 @@ def transformable(self, method=None): """ Check the request for a transformable format - @param method: "import" for import methods, else None + :param method: "import" for import methods, else None """ if self.representation in ("html", "aadata", "popup", "iframe"): @@ -1300,7 +679,7 @@ def actuate_link(self, component_id=None): """ Determine whether to actuate a link or not - @param component_id: the component_id (if not self.component_id) + :param component_id: the component_id (if not self.component_id) """ if not component_id: @@ -1347,9 +726,9 @@ def error(self, status, message, tree=None, next=None): """ Action upon error - @param status: HTTP status code - @param message: the error message - @param tree: the tree causing the error + :param status: HTTP status code + :param message: the error message + :param tree: the tree causing the error """ if self.representation == "html": @@ -1371,14 +750,15 @@ def error(self, status, message, tree=None, next=None): # ------------------------------------------------------------------------- def url(self, - id=None, - component=None, - component_id=None, - target=None, - method=None, - representation=None, - vars=None, - host=None): + id = None, + component = None, + component_id = None, + target = None, + method = None, + representation = None, + vars = None, + host = None, + ): """ Returns the URL of this request, use parameters to override current requests attributes: @@ -1387,14 +767,14 @@ def url(self, - 0 or "" to set attribute to NONE - value to use explicit value - @param id: the master record ID - @param component: the component name - @param component_id: the component ID - @param target: the target record ID (choose automatically) - @param method: the URL method - @param representation: the representation for the URL - @param vars: the URL query variables - @param host: string to force absolute URL with host (True means http_host) + :param id: the master record ID + :param component: the component name + :param component_id: the component ID + :param target: the target record ID (choose automatically) + :param method: the URL method + :param representation: the representation for the URL + :param vars: the URL query variables + :param host: string to force absolute URL with host (True means http_host) Particular behavior: - changing the master record ID resets the component ID @@ -1504,10 +884,10 @@ def target(self): """ Get the target table of the current request - @return: a tuple of (prefix, name, table, tablename) of the target + :return: a tuple of (prefix, name, table, tablename) of the target resource of this request - @todo: update for link table support + TODO update for link table support """ component = self.component @@ -1535,7 +915,7 @@ def viewing(self): Parse the "viewing" URL parameter, frequently used for perspective discrimination and processing in prep - @returns: tuple (tablename, record_id) if "viewing" is set, + :returns: tuple (tablename, record_id) if "viewing" is set, None otherwise """ @@ -1558,8 +938,8 @@ def stylesheet(self, method=None, skip_error=False): """ Find the XSLT stylesheet for this request - @param method: "import" for data imports, else None - @param skip_error: do not raise an HTTP error status + :param method: "import" for data imports, else None + :param skip_error: do not raise an HTTP error status if the stylesheet cannot be found """ @@ -1654,7 +1034,7 @@ def customise_resource(self, tablename=None): """ Invoke the customization callback for a resource. - @param tablename: the tablename of the resource; if called + :param tablename: the tablename of the resource; if called without tablename it will invoke the callbacks for the target resources of this request: - master @@ -1673,10 +1053,10 @@ def customise_resource_my_table(r, tablename): settings.customise_resource_my_table = \ customise_resource_my_table - @note: the hook itself can call r.customise_resource in order - to cascade customizations as necessary - @note: if a table is customised that is not currently loaded, - then it will be loaded for this process + .. note:: the hook itself can call r.customise_resource in order + to cascade customizations as necessary + .. note:: if a table is customised that is not currently loaded, + then it will be loaded for this process """ if tablename is None: @@ -1700,16 +1080,14 @@ def customise_resource_my_table(r, tablename): customise(self, tablename) # ============================================================================= -# Global functions -# -def s3_request(*args, **kwargs): +def crud_request(*args, **kwargs): """ - Helper function to generate S3Request instances + Helper function to generate CRUDRequest instances - @param args: arguments for the S3Request - @param kwargs: keyword arguments for the S3Request + :param args: arguments for the CRUDRequest + :param kwargs: keyword arguments for the CRUDRequest - @keyword catch_errors: if set to False, errors will be raised + :keyword catch_errors: if set to False, errors will be raised instead of returned to the client, useful for optional sub-requests, or if the caller implements fallbacks @@ -1719,7 +1097,7 @@ def s3_request(*args, **kwargs): error = None try: - r = S3Request(*args, **kwargs) + r = CRUDRequest(*args, **kwargs) except (AttributeError, SyntaxError): if catch_errors is False: raise @@ -1739,12 +1117,194 @@ def s3_request(*args, **kwargs): headers = {"Content-Type":"application/json"} current.log.error(message) raise HTTP(error, - body=current.xml.json_message(success=False, - statuscode=error, - message=message, - ), - web2py_error=message, + body = current.xml.json_message(success = False, + statuscode = error, + message = message, + ), + web2py_error = message, **headers) return r +# ----------------------------------------------------------------------------- +def crud_controller(prefix=None, resourcename=None, **attr): + """ + Helper function to apply the S3Resource REST interface + + :param prefix: the application prefix + :param resourcename: the resource name (without prefix) + :param attr: additional keyword parameters + + Any keyword parameters will be copied into the output dict (provided + that the output is a dict). If a keyword parameter is callable, then + it will be invoked, and its return value will be added to the output + dict instead. The callable receives the CRUDRequest as its first and + only parameter. + + CRUD can be configured per table using: + + s3db.configure(tablename, **attr) + + *** Redirection: + + create_next URL to redirect to after a record has been created + update_next URL to redirect to after a record has been updated + delete_next URL to redirect to after a record has been deleted + + *** Form configuration: + + list_fields list of names of fields to include into list views + subheadings Sub-headings (see separate documentation) + listadd Enable/Disable add-form in list views + + *** CRUD configuration: + + editable Allow/Deny record updates in this table + deletable Allow/Deny record deletions in this table + insertable Allow/Deny record insertions into this table + copyable Allow/Deny record copying within this table + + *** Callbacks: + + create_onvalidation Function for additional record validation on create + create_onaccept Function after successful record insertion + + update_onvalidation Function for additional record validation on update + update_onaccept Function after successful record update + + onvalidation Fallback for both create_onvalidation and update_onvalidation + onaccept Fallback for both create_onaccept and update_onaccept + ondelete Function after record deletion + """ + + auth = current.auth + s3db = current.s3db + + request = current.request + response = current.response + s3 = response.s3 + settings = current.deployment_settings + + # Parse the request + dynamic = attr.get("dynamic") + if dynamic: + # Dynamic table controller + c = request.controller + f = request.function + attr = settings.customise_controller("%s_%s" % (c, f), **attr) + from core import DYNAMIC_PREFIX, s3_get_extension + r = crud_request(DYNAMIC_PREFIX, + dynamic, + f = "%s/%s" % (f, dynamic), + args = request.args[1:], + extension = s3_get_extension(request), + ) + else: + # Customise Controller from Template + attr = settings.customise_controller( + "%s_%s" % (prefix or request.controller, + resourcename or request.function, + ), + **attr) + r = crud_request(prefix, resourcename) + + # Customize target resource(s) from Template + r.customise_resource() + + # List of methods rendering datatables with default action buttons + dt_methods = (None, "datatable", "datatable_f", "summary", "list") + + # List of methods rendering datatables with custom action buttons, + # => for these, s3.actions must not be touched, see below + # (defining here allows postp to add a custom method to the list) + s3.action_methods = ("import", + "review", + "approve", + "reject", + "deduplicate", + ) + + # Execute the request + output = r(**attr) + + method = r.method + if isinstance(output, dict) and method in dt_methods: + + if s3.actions is None: + + # Add default action buttons + prefix, name, table, tablename = r.target() + authorised = auth.s3_has_permission("update", tablename) + + # If a component has components itself, then action buttons + # can be forwarded to the native controller by setting native=True + if r.component and s3db.has_components(table): + native = output.get("native", False) + else: + native = False + + # Get table config + get_config = s3db.get_config + listadd = get_config(tablename, "listadd", True) + + # Which is the standard open-action? + if settings.get_ui_open_read_first(): + # Always read, irrespective permissions + editable = False + else: + editable = get_config(tablename, "editable", True) + if editable and \ + auth.permission.ownership_required("update", table): + # User cannot edit all records in the table + if settings.get_ui_auto_open_update(): + # Decide automatically per-record (implicit method) + editable = "auto" + else: + # Always open read first (explicit read) + editable = False + + deletable = get_config(tablename, "deletable", True) + copyable = get_config(tablename, "copyable", False) + + # URL to open the resource + from .methods import S3CRUD + open_url = S3CRUD._linkto(r, + authorised = authorised, + update = editable, + native = native)("[id]") + + # Add action buttons for Open/Delete/Copy as appropriate + S3CRUD.action_buttons(r, + deletable = deletable, + copyable = copyable, + editable = editable, + read_url = open_url, + update_url = open_url + # To use modals + #update_url = "%s.popup?refresh=list" % open_url + ) + + # Override Add-button, link to native controller and put + # the primary key into get_vars for automatic linking + if native and not listadd and \ + auth.s3_has_permission("create", tablename): + label = S3CRUD.crud_string(tablename, "label_create") + component = r.resource.components[name] + fkey = "%s.%s" % (name, component.fkey) + get_vars_copy = request.get_vars.copy() + get_vars_copy.update({fkey: r.record[component.fkey]}) + url = URL(prefix, name, + args = ["create"], + vars = get_vars_copy, + ) + add_btn = A(label, + _href = url, + _class = "action-btn", + ) + output.update(add_btn = add_btn) + + elif method not in s3.action_methods: + s3.actions = None + + return output + # END ========================================================================= diff --git a/modules/core/filters/__init__.py b/modules/core/filters/__init__.py index 1328a151d3..a4e85aaf0e 100644 --- a/modules/core/filters/__init__.py +++ b/modules/core/filters/__init__.py @@ -1,4 +1,3 @@ # -*- coding: utf-8 -*- from .base import * -from .query import * diff --git a/modules/core/filters/base.py b/modules/core/filters/base.py index 9f503bd588..5721082ff8 100644 --- a/modules/core/filters/base.py +++ b/modules/core/filters/base.py @@ -64,7 +64,7 @@ S3GroupedOptionsWidget, S3HierarchyWidget, \ S3MultiSelectWidget -from .query import FS, S3ResourceField, S3ResourceQuery, S3URLQuery +from ..resource import FS, S3ResourceField, S3ResourceQuery, S3URLQuery # Compact JSON encoding SEPARATORS = (",", ":") @@ -1784,7 +1784,7 @@ def get_lx_ancestors(levels, resource, selector=None, location_ids=None, path=Fa rfield = resource.resolve_selector(selector) # Get the joins for the selector - from .query import S3Joins + from ..resource import S3Joins joins = S3Joins(resource.tablename) joins.extend(rfield._joins) join = joins.as_list() diff --git a/modules/core/io/__init__.py b/modules/core/io/__init__.py deleted file mode 100644 index 6456107ff0..0000000000 --- a/modules/core/io/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# -*- coding: utf-8 -*- - -from .codec import * -from .exporter import * -from .importer import * -from .rtb import * -from .xml import * diff --git a/modules/core/methods/__init__.py b/modules/core/methods/__init__.py index a6edd58bdf..fa73e28497 100644 --- a/modules/core/methods/__init__.py +++ b/modules/core/methods/__init__.py @@ -15,8 +15,10 @@ from .pois import * from .profile import * from .report import * +from .rest import * from .rolemgr import * from .ssi import SpreadsheetImporter from .summary import * from .timeplot import * from .xforms import * + diff --git a/modules/core/methods/anonymize.py b/modules/core/methods/anonymize.py index d9ab76713e..2b8c6acd57 100644 --- a/modules/core/methods/anonymize.py +++ b/modules/core/methods/anonymize.py @@ -55,7 +55,7 @@ def apply_method(self, r, **attr): """ Entry point for REST API - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller parameters @return: output data (JSON) @@ -90,7 +90,7 @@ def anonymize(cls, r, table, record_id): """ Handle POST (anonymize-request), i.e. anonymize the target record - @param r: the S3Request + @param r: the CRUDRequest @param table: the target Table @param record_id: the target record ID @@ -221,7 +221,7 @@ def cascade(cls, table, record_ids, rules): the transaction if an exception is raised """ - from ..filters import FS, S3Joins + from ..resource import FS, S3Joins s3db = current.s3db @@ -377,10 +377,10 @@ def widget(cls, ): """ Render an action item (link or button) to anonymize the - target record of an S3Request, which can be embedded in + target record of an CRUDRequest, which can be embedded in the record view - @param r: the S3Request + @param r: the CRUDRequest @param label: The label for the action item @param ajaxURL: The URL for the AJAX request @param _class: HTML class for the action item @@ -609,7 +609,7 @@ def apply_method(self, r, **attr): """ Entry point for REST API - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller parameters @return: output data (JSON) @@ -666,7 +666,7 @@ def anonymize(cls, r, table, record_ids): """ Handle POST (anonymize-request), i.e. anonymize the target record - @param r: the S3Request + @param r: the CRUDRequest @param table: the target Table @param record_ids: the target record IDs @@ -757,7 +757,7 @@ def widget(cls, Render an action item (link or button) to anonymize the provided records - @param r: the S3Request + @param r: the CRUDRequest @param record_ids: The list of record_ids to act on @param _class: HTML class for the action item diff --git a/modules/core/methods/base.py b/modules/core/methods/base.py index 4a8d528a9d..ee7b2b6e1e 100644 --- a/modules/core/methods/base.py +++ b/modules/core/methods/base.py @@ -36,8 +36,6 @@ from gluon import current from gluon.storage import Storage -REGEX_FILTER = re.compile(r".+\..+|.*\(.+\).*") - # ============================================================================= class S3Method(object): """ @@ -76,7 +74,7 @@ def __call__(self, r, method=None, widget_id=None, **attr): """ Entry point for the REST interface - @param r: the S3Request + @param r: the CRUDRequest @param method: the method established by the REST interface @param widget_id: widget ID @param attr: dict of parameters for the method handler @@ -189,7 +187,7 @@ def apply_method(self, r, **attr): Stub, to be implemented in subclass. This method is used to get the results as a standalone page. - @param r: the S3Request + @param r: the CRUDRequest @param attr: dictionary of parameters for the method handler @return: output object to send to the view @@ -232,7 +230,7 @@ def widget(self, r, method=None, widget_id=None, visible=True, **attr): parameter if delayed loading of the data layer is not all([possible, useful, supported]). - @param r: the S3Request + @param r: the CRUDRequest @param method: the URL method @param widget_id: the widget ID @param visible: whether the widget is initially visible @@ -293,9 +291,9 @@ def _permitted(self, method=None): @staticmethod def _record_id(r): """ - Get the ID of the target record of a S3Request + Get the ID of the target record of a CRUDRequest - @param r: the S3Request + @param r: the CRUDRequest """ master_id = r.id @@ -344,7 +342,7 @@ def _view(r, default): """ Get the path to the view template - @param r: the S3Request + @param r: the CRUDRequest @param default: name of the default view template """ @@ -410,7 +408,7 @@ def _extend_view(output, r, **attr): Add additional view variables (invokes all callables) @param output: the output dict - @param r: the S3Request + @param r: the CRUDRequest @param attr: the view variables (e.g. 'rheader') @note: overload this method in subclasses if you don't want @@ -451,8 +449,10 @@ def _remove_filters(get_vars): @param get_vars: the URL vars as dict """ + regex_filter = re.compile(r".+\..+|.*\(.+\).*") + return Storage((k, v) for k, v in get_vars.items() - if not REGEX_FILTER.match(k)) + if not regex_filter.match(k)) # ------------------------------------------------------------------------- @staticmethod diff --git a/modules/core/methods/cico.py b/modules/core/methods/cico.py index 2c0ef9b6e1..2427857d8c 100644 --- a/modules/core/methods/cico.py +++ b/modules/core/methods/cico.py @@ -50,7 +50,7 @@ def apply_method(r, **attr): """ Apply method. - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller options for this request """ @@ -168,7 +168,7 @@ def apply_method(r, **attr): """ Apply method. - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller options for this request """ diff --git a/modules/core/methods/crud.py b/modules/core/methods/crud.py index 9b4062e634..d6061bfc87 100644 --- a/modules/core/methods/crud.py +++ b/modules/core/methods/crud.py @@ -36,12 +36,7 @@ import json -try: - from lxml import etree -except ImportError: - import sys - sys.stderr.write("ERROR: lxml module needed for XML handling\n") - raise +from lxml import etree from gluon import current, redirect, HTTP, URL, \ A, DIV, FORM, INPUT, TABLE, TD, TR, XML @@ -50,7 +45,7 @@ from gluon.storage import Storage from gluon.tools import callback -from ..io import S3Exporter +from ..resource import S3Exporter from ..tools import S3DateTime, s3_decode_iso_datetime, s3_str, s3_validate, s3_represent_value, s3_set_extension from ..ui import S3EmbeddedComponentWidget, S3Selector, ICON, S3SQLDefaultForm @@ -70,7 +65,7 @@ def apply_method(self, r, **attr): """ Apply CRUD methods - @param r: the S3Request + @param r: the CRUDRequest @param attr: dictionary of parameters for the method handler @return: output object to send to the view @@ -98,10 +93,14 @@ def apply_method(self, r, **attr): if r.http == "DELETE" or self.method == "delete": output = self.delete(r, **attr) + elif method == "create": + output = self.create(r, **attr) + elif method == "read": output = self.read(r, **attr) + elif method == "update": output = self.update(r, **attr) @@ -117,6 +116,7 @@ def apply_method(self, r, **attr): if method == "datatable_f": self.hide_filter = False output = self.select(r, **_attr) + elif method in ("datalist", "datalist_f"): _attr = Storage(attr) _attr["list_type"] = "datalist" @@ -126,6 +126,7 @@ def apply_method(self, r, **attr): elif method == "validate": output = self.validate(r, **attr) + elif method == "review": if r.record: output = self.review(r, **attr) @@ -142,7 +143,7 @@ def widget(self, r, method=None, widget_id=None, visible=True, **attr): Entry point for other method handlers to embed this method as widget - @param r: the S3Request + @param r: the CRUDRequest @param method: the widget method @param widget_id: the widget ID @param visible: whether the widget is initially visible @@ -177,7 +178,7 @@ def create(self, r, **attr): """ Create new records - @param r: the S3Request + @param r: the CRUDRequest @param attr: dictionary of parameters for the method handler """ @@ -346,9 +347,16 @@ def create(self, r, **attr): # Success message message = crud_string(self.tablename, "msg_record_created") - # Copy formkey if un-deleting a duplicate - if "id" in request.post_vars: - post_vars = request.post_vars + # Re-instate a deleted duplicate + post_vars = r.post_vars + if r.http == "POST": + if "deleted" in table and "id" not in post_vars: + existing = resource.original(table, post_vars) + if existing and existing.deleted: + r.vars["id"] = post_vars["id"] = existing.id + + # Copy formkey if re-instating a deleted duplicate + if "id" in post_vars: original = str(post_vars.id) if original: formkey = session.get("_formkey[%s/None]" % tablename) @@ -480,10 +488,6 @@ def create(self, r, **attr): else: session.confirmation = current.T("Data uploaded") - elif representation == "url": - results = self.import_url(r) - return results - else: r.error(415, current.ERROR.BAD_FORMAT) @@ -494,7 +498,7 @@ def _widget_create(self, r, **attr): """ Create-buttons/form in summary views, both GET and POST - @param r: the S3Request + @param r: the CRUDRequest @param attr: dictionary of parameters for the method handler """ @@ -582,7 +586,7 @@ def read(self, r, **attr): """ Read a single record - @param r: the S3Request + @param r: the CRUDRequest @param attr: dictionary of parameters for the method handler """ @@ -848,7 +852,7 @@ def update(self, r, **attr): """ Update a record - @param r: the S3Request + @param r: the CRUDRequest @param attr: dictionary of parameters for the method handler """ @@ -1026,9 +1030,6 @@ def update(self, r, **attr): else: self.next = update_next - elif representation == "url": - return self.import_url(r) - else: r.error(415, current.ERROR.BAD_FORMAT) @@ -1039,7 +1040,7 @@ def delete(self, r, **attr): """ Delete record(s) - @param r: the S3Request + @param r: the CRUDRequest @param attr: dictionary of parameters for the method handler @todo: update for link table components @@ -1156,7 +1157,7 @@ def select(self, r, **attr): """ Filterable datatable/datalist - @param r: the S3Request + @param r: the CRUDRequest @param attr: dictionary of parameters for the method handler """ @@ -1439,7 +1440,7 @@ def _datatable(self, r, **attr): """ Get a data table - @param r: the S3Request + @param r: the CRUDRequest @param attr: parameters for the method handler """ @@ -1625,7 +1626,7 @@ def _datalist(self, r, **attr): """ Get a data list - @param r: the S3Request + @param r: the CRUDRequest @param attr: parameters for the method handler """ @@ -1693,7 +1694,7 @@ def _datalist(self, r, **attr): record_id = get_vars.get("record", None) if record_id is not None: # Ajax-reload of a single record - from ..filters import FS + from ..resource import FS resource.add_filter(FS("id") == record_id) start = 0 limit = 1 @@ -1798,7 +1799,7 @@ def unapproved(self, r, **attr): """ Get a list of unapproved records in this resource - @param r: the S3Request + @param r: the CRUDRequest @param attr: dictionary of parameters for the method handler """ @@ -1983,7 +1984,7 @@ def review(self, r, **attr): """ Review/approve/reject an unapproved record. - @param r: the S3Request + @param r: the CRUDRequest @param attr: dictionary of parameters for the method handler """ @@ -2099,7 +2100,7 @@ def validate(self, r, **attr): and returns a JSON object with either the validation errors or the text representations of the data. - @param r: the S3Request + @param r: the CRUDRequest @param attr: dictionary of parameters for the method handler Input JSON format: @@ -2133,7 +2134,7 @@ def validate(self, r, **attr): if r.representation != "json": r.error(415, current.ERROR.BAD_FORMAT) - resource = self.resource + resource = r.resource get_vars = r.get_vars if "component" in get_vars: @@ -2476,7 +2477,7 @@ def render_buttons(self, r, buttons, record_id=None, **attr): """ Render CRUD buttons - @param r: the S3Request + @param r: the CRUDRequest @param buttons: list of button names, any of: "add", "edit", "delete", "list", "summary" @param record_id: the record ID @@ -2642,7 +2643,7 @@ def action_buttons(cls, that would be inserted by CRUD/select via linkto. The resource id should be represented by "[id]". - @param r: the S3Request + @param r: the CRUDRequest @param deletable: records can be deleted @param editable: records can be modified @param copyable: record data can be copied into new record @@ -2782,7 +2783,7 @@ def _default_cancel_button(self, r): Individual controllers can override this by setting response.s3.cancel = False. - @param r: the S3Request + @param r: the CRUDRequest """ if r.representation != "html": @@ -2851,102 +2852,6 @@ def import_csv(self, stream, table=None): db.import_from_csv_file(stream) db.commit() - # ------------------------------------------------------------------------- - @staticmethod - def import_url(r): - """ - Import data from vars in URL query - - @param r: the S3Request - @note: can only update single records (no mass-update) - - @todo: update for link table components - @todo: re-integrate into S3Importer - """ - - xml = current.xml - - table = r.target()[2] - - record = r.record - resource = r.resource - - # Handle components - if record and r.component: - resource = resource.components[r.component_name] - resource.load() - if len(resource) == 1: - record = resource.records()[0] - else: - record = None - r.vars.update({resource.fkey: r.record[resource.pkey]}) - elif not record and r.component: - item = xml.json_message(False, 400, "Invalid Request!") - return {"item": item} - - # Check for update - if record and xml.UID in table.fields: - r.vars.update({xml.UID: xml.export_uid(record[xml.UID])}) - - # Build tree - element = etree.Element(xml.TAG.resource) - element.set(xml.ATTRIBUTE.name, resource.tablename) - for var in r.vars: - if var.find(".") != -1: - continue - elif var in table.fields: - field = table[var] - value = s3_str(r.vars[var]) - if var in xml.FIELDS_TO_ATTRIBUTES: - element.set(var, value) - else: - data = etree.Element(xml.TAG.data) - data.set(xml.ATTRIBUTE.field, var) - if field.type == "upload": - data.set(xml.ATTRIBUTE.filename, value) - else: - data.text = value - element.append(data) - tree = xml.tree([element], domain=xml.domain) - - # Import data - result = Storage(committed=False) - def log(item): - result["item"] = item - resource.configure(oncommit_import_item = log) - try: - success = resource.import_xml(tree) - except SyntaxError: - pass - - # Check result - if result.item: - result = result.item - - # Build response - if success and result.committed: - r.id = result.id - method = result.method - if method == result.METHOD.CREATE: - item = xml.json_message(True, 201, "Created as %s?%s.id=%s" % - (str(r.url(method="", - representation="html", - vars={}, - ) - ), - r.name, result.id) - ) - else: - item = xml.json_message(True, 200, "Record updated") - else: - item = xml.json_message(False, 403, - "Could not create/update record: %s" % - resource.error or xml.error, - tree=xml.tree2json(tree)) - - return {"item": item} - - # ------------------------------------------------------------------------- def _embed_component(self, resource, record=None): """ @@ -3084,11 +2989,12 @@ def _postprocess_embedded(self, form, component=None, key=None): return # ------------------------------------------------------------------------- - def _linkto(self, r, authorised=None, update=None, native=False): + @classmethod + def _linkto(cls, r, authorised=None, update=None, native=False): """ Returns a linker function for the record ID column in list views - @param r: the S3Request + @param r: the CRUDRequest @param authorised: user authorised for update (override internal check) @param update: provide link to update rather than to read @@ -3135,7 +3041,7 @@ def list_linkto(record_id, r=r, c=c, f=f, except TypeError: url = linkto % record_id else: - get_vars = self._linkto_vars(r) + get_vars = cls._linkto_vars(r) if r.component: if r.link and not r.actuate_link(): @@ -3184,7 +3090,7 @@ def _linkto_vars(r): """ Retain certain GET vars of the request in action links - @param r: the S3Request + @param r: the CRUDRequest @return: Storage with GET vars """ diff --git a/modules/core/methods/filtermgr.py b/modules/core/methods/filtermgr.py index 3d2cf09b39..6d11cea463 100644 --- a/modules/core/methods/filtermgr.py +++ b/modules/core/methods/filtermgr.py @@ -50,7 +50,7 @@ def apply_method(self, r, **attr): """ Entry point for REST interface - @param r: the S3Request + @param r: the CRUDRequest @param attr: additional controller parameters """ @@ -90,7 +90,7 @@ def _form(self, r, **attr): GET filter.html - @param r: the S3Request + @param r: the CRUDRequest @param attr: additional controller parameters """ @@ -108,7 +108,7 @@ def _options(self, r, **attr): GET filter.options - @param r: the S3Request + @param r: the CRUDRequest @param attr: additional controller parameters (ignored currently) """ @@ -138,7 +138,7 @@ def _delete(r, **attr): """ Delete a filter, responds to POST filter.json?delete= - @param r: the S3Request + @param r: the CRUDRequest @param attr: additional controller parameters """ @@ -191,7 +191,7 @@ def _save(self, r, **attr): """ Save a filter, responds to POST filter.json - @param r: the S3Request + @param r: the CRUDRequest @param attr: additional controller parameters """ @@ -293,7 +293,7 @@ def _load(self, r, **attr): GET filter.json or GET filter.json?load= - @param r: the S3Request + @param r: the CRUDRequest @param attr: additional controller parameters """ diff --git a/modules/core/methods/grouped.py b/modules/core/methods/grouped.py index 8fd5bb1b77..e8eedf5a2f 100644 --- a/modules/core/methods/grouped.py +++ b/modules/core/methods/grouped.py @@ -60,7 +60,7 @@ def apply_method(self, r, **attr): """ Page-render entry point for REST interface. - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes """ @@ -76,7 +76,7 @@ def widget(self, r, method=None, widget_id=None, visible=True, **attr): """ Summary widget method - @param r: the S3Request + @param r: the CRUDRequest @param method: the widget method @param widget_id: the widget ID @param visible: whether the widget is initially visible @@ -95,7 +95,7 @@ def report(self, r, **attr): """ Report generator - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes """ @@ -474,7 +474,7 @@ def export_links(r): """ Render export links for the report - @param r: the S3Request + @param r: the CRUDRequest """ T = current.T @@ -613,7 +613,7 @@ def pdf(self, r, filename=None): """ Produce a PDF representation of the grouped table - @param r: the S3Request + @param r: the CRUDRequest @return: the PDF document """ @@ -638,7 +638,7 @@ def pdf(self, r, filename=None): pdf_footer = self.pdf_footer - from ..io import S3Exporter + from ..resource import S3Exporter exporter = S3Exporter().pdf return exporter(self.resource, request = r, @@ -658,7 +658,7 @@ def xls(self, r, filename=None): """ Produce an XLS sheet of the grouped table - @param r: the S3Request + @param r: the CRUDRequest @return: the XLS document """ @@ -702,7 +702,7 @@ def xls(self, r, filename=None): } # Export as XLS - from ..io import S3Exporter + from ..resource import S3Exporter exporter = S3Exporter().xls return exporter(xlsdata, title = self.title, diff --git a/modules/core/methods/hcrud.py b/modules/core/methods/hcrud.py index 01c779ffac..0d70bd57f2 100644 --- a/modules/core/methods/hcrud.py +++ b/modules/core/methods/hcrud.py @@ -49,7 +49,7 @@ def apply_method(self, r, **attr): """ Entry point for REST interface - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller attributes """ @@ -72,7 +72,7 @@ def tree(self, r, **attr): """ Page load - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller attributes """ @@ -171,7 +171,7 @@ def node_json(self, r, **attr): """ Return a single node as JSON (id, parent and label) - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller attributes """ @@ -372,7 +372,7 @@ def export_xls(self, r, **attr): all_nodes = h.findall(roots, inclusive=True) # ...and extract their data from a clone of the resource - from ..filters import FS + from ..resource import FS query = FS(h.pkey.name).belongs(all_nodes) clone = current.s3db.resource(resource, filter=query) data = clone.select(selectors, represent=True, raw_data=True) @@ -412,7 +412,7 @@ def export_xls(self, r, **attr): output.extend(rows) # Encode in XLS format - from ..io import S3Codec + from ..resource import S3Codec codec = S3Codec.get_codec("xls") result = codec.encode(output, title = resource.name, diff --git a/modules/core/methods/mapview.py b/modules/core/methods/mapview.py index c9961fe60f..4fed7fbc86 100644 --- a/modules/core/methods/mapview.py +++ b/modules/core/methods/mapview.py @@ -44,10 +44,10 @@ class S3Map(S3Method): # ------------------------------------------------------------------------- def apply_method(self, r, **attr): """ - Entry point to apply map method to S3Requests + Entry point to apply map method to CRUDRequests - produces a full page with S3FilterWidgets above a Map - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes for the request @return: output object to send to the view @@ -69,7 +69,7 @@ def page(self, r, **attr): """ Map page - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes for the request """ @@ -144,7 +144,7 @@ def widget(self, Render a Map widget suitable for use in an S3Filter-based page such as S3Summary - @param r: the S3Request + @param r: the CRUDRequest @param method: the widget method @param widget_id: the widget ID @param visible: whether the widget is initially visible diff --git a/modules/core/methods/merge.py b/modules/core/methods/merge.py index 626e7e3a3d..68c6654b20 100644 --- a/modules/core/methods/merge.py +++ b/modules/core/methods/merge.py @@ -36,7 +36,7 @@ from s3dal import Field -from ..filters import FS +from ..resource import FS from ..tools import s3_get_foreign_key, s3_represent_value, s3_str, IS_ONE_OF from ..ui import S3DataTable, S3AddPersonWidget, S3LocationSelector, S3LocationAutocompleteWidget @@ -57,7 +57,7 @@ def apply_method(self, r, **attr): """ Apply Merge methods - @param r: the S3Request + @param r: the CRUDRequest @param attr: dictionary of parameters for the method handler @return: output object to send to the view @@ -105,7 +105,7 @@ def mark(self, r, **attr): """ Bookmark the current record for de-duplication - @param r: the S3Request + @param r: the CRUDRequest @param attr: the controller parameters for the request """ @@ -137,7 +137,7 @@ def unmark(self, r, **attr): """ Remove a record from the deduplicate list - @param r: the S3Request + @param r: the CRUDRequest @param attr: the controller parameters for the request """ @@ -174,7 +174,7 @@ def bookmark(cls, r, tablename, record_id): view, also renders a link to the duplicate bookmark list to initiate the merge process from - @param r: the S3Request + @param r: the CRUDRequest @param tablename: the table name @param record_id: the record ID """ @@ -233,7 +233,7 @@ def duplicates(self, r, **attr): records in this resource, with option to select two and initiate the merge process from here - @param r: the S3Request + @param r: the CRUDRequest @param attr: the controller attributes for the request """ @@ -387,7 +387,7 @@ def merge(self, r, **attr): """ Merge form for two records - @param r: the S3Request + @param r: the CRUDRequest @param **attr: the controller attributes for the request @note: this method can always only be POSTed, and requires diff --git a/modules/core/methods/mobile.py b/modules/core/methods/mobile.py index 1ad5f3fdc5..328b6d2e7a 100644 --- a/modules/core/methods/mobile.py +++ b/modules/core/methods/mobile.py @@ -39,7 +39,7 @@ from gluon import IS_EMPTY_OR, IS_IN_SET, current -from ..io import S3ResourceTree +from ..resource import S3ResourceTree from ..tools import s3_get_foreign_key, s3_str, SEPARATORS, s3_parse_datetime, \ S3Represent from ..ui import S3SQLCustomForm, S3SQLDummyField, S3SQLField, \ @@ -1260,7 +1260,7 @@ def apply_method(self, r, **attr): """ Entry point for REST interface. - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes """ @@ -1288,7 +1288,7 @@ def mform(self, r, **attr): """ Get the mobile form for the target resource - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes @returns: a JSON string diff --git a/modules/core/methods/organizer.py b/modules/core/methods/organizer.py index f205ddf6bf..909437e338 100644 --- a/modules/core/methods/organizer.py +++ b/modules/core/methods/organizer.py @@ -60,7 +60,7 @@ def apply_method(self, r, **attr): """ Page-render entry point for REST interface. - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes """ @@ -87,7 +87,7 @@ def organizer(self, r, **attr): """ Render the organizer view (HTML method) - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes @returns: dict of values for the view @@ -254,7 +254,7 @@ def get_json_data(self, r, **attr): """ Extract the resource data and return them as JSON (Ajax method) - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes TODO correct documentation! @@ -312,7 +312,7 @@ def get_json_data(self, r, **attr): # Add date filter start, end = self.parse_interval(r.get_vars.get("$interval")) if start and end: - from ..filters import FS + from ..resource import FS start_fs = FS(start_rfield.selector) if not end_rfield: query = (start_fs >= start) & (start_fs < end) @@ -414,7 +414,7 @@ def update_json(self, r, **attr): """ Update or delete calendar items (Ajax method) - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes """ diff --git a/modules/core/methods/pois.py b/modules/core/methods/pois.py index c0cb851727..7f9cb853a3 100644 --- a/modules/core/methods/pois.py +++ b/modules/core/methods/pois.py @@ -38,7 +38,7 @@ from s3dal import Field from ..gis import GIS -from ..io import S3ResourceTree +from ..resource import S3ResourceTree from ..tools import s3_format_datetime, s3_parse_datetime from .base import S3Method @@ -52,7 +52,7 @@ def apply_method(self, r, **attr): """ Apply method. - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller options for this request """ @@ -88,7 +88,7 @@ def export(self, r, **attr): (other formats can be requested, but may give unexpected results) - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller options for this request """ @@ -243,7 +243,7 @@ def _add_lx_filter(resource, lx): @param resource: the resource """ - from ..filters import FS + from ..resource import FS query = (FS("location_id$path").contains("/%s/" % lx)) | \ (FS("location_id$path").like("%s/%%" % lx)) resource.add_filter(query) @@ -260,7 +260,7 @@ def apply_method(r, **attr): """ Apply method. - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller options for this request """ @@ -422,18 +422,21 @@ def apply_method(r, **attr): # Module disabled continue resource = define_resource(tablename) - s3xml = xml.transform(tree, stylesheet_path=stylesheet, - name=resource.name) + s3xml = xml.transform(tree, + stylesheet_path = stylesheet, + name = resource.name, + ) try: - resource.import_xml(s3xml, - ignore_errors=ignore_errors) - import_count += resource.import_count + result = resource.import_xml(s3xml, + ignore_errors = ignore_errors, + ) except Exception: response.error += str(sys.exc_info()[1]) + else: + import_count += result.count if import_count: response.confirmation = "%s %s" % \ - (import_count, - T("PoIs successfully imported.")) + (import_count, T("PoIs successfully imported.")) else: response.information = T("No PoIs available.") diff --git a/modules/core/methods/profile.py b/modules/core/methods/profile.py index 37b21efc8a..e38b0dd29f 100644 --- a/modules/core/methods/profile.py +++ b/modules/core/methods/profile.py @@ -36,7 +36,7 @@ from gluon.html import * from gluon.storage import Storage -from ..filters import FS +from ..resource import FS from ..tools import s3_str from ..ui import ICON @@ -65,7 +65,7 @@ def apply_method(self, r, **attr): """ API entry point - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes for the request """ @@ -97,7 +97,7 @@ def profile(self, r, **attr): """ Generate a Profile page - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes for the request """ @@ -326,7 +326,7 @@ def _comments(self, r, widget, **attr): """ Generate a Comments widget - @param r: the S3Request instance + @param r: the CRUDRequest instance @param widget: the widget definition as dict @param attr: controller attributes for the request @@ -364,7 +364,7 @@ def _custom(self, r, widget, **attr): """ Generate a Custom widget - @param r: the S3Request instance + @param r: the CRUDRequest instance @param widget: the widget definition as dict @param attr: controller attributes for the request """ @@ -400,7 +400,7 @@ def _datalist(self, r, widget, **attr): """ Generate a data list - @param r: the S3Request instance + @param r: the CRUDRequest instance @param widget: the widget definition as dict @param attr: controller attributes for the request """ @@ -563,7 +563,7 @@ def _datatable(self, r, widget, **attr): """ Generate a data table. - @param r: the S3Request instance + @param r: the CRUDRequest instance @param widget: the widget definition as dict @param attr: controller attributes for the request @@ -800,7 +800,7 @@ def _form(self, r, widget, **attr): """ Generate a Form widget - @param r: the S3Request instance + @param r: the CRUDRequest instance @param widget: the widget definition as dict @param attr: controller attributes for the request """ @@ -885,7 +885,7 @@ def _map(self, r, widget, widgets, **attr): """ Generate a Map widget - @param r: the S3Request instance + @param r: the CRUDRequest instance @param widget: the widget as a tuple: (label, type, icon) @param attr: controller attributes for the request """ @@ -1053,7 +1053,7 @@ def _report(self, r, widget, **attr): """ Generate a Report widget - @param r: the S3Request instance + @param r: the CRUDRequest instance @param widget: the widget as a tuple: (label, type, icon) @param attr: controller attributes for the request """ @@ -1109,7 +1109,7 @@ def _organizer(self, r, widget, **attr): """ Generate an Organizer widget - @param r: the S3Request instance + @param r: the CRUDRequest instance @param widget: the widget configuration (a dict) @param attr: controller attributes for the request """ @@ -1247,7 +1247,7 @@ def _lookup_class(r, widget): """ Provide the column-width class for the widgets - @param r: the S3Request + @param r: the CRUDRequest @param widget: the widget config (dict) """ @@ -1272,7 +1272,7 @@ def _create_popup(r, widget, list_id, resource, context, numrows): Render an action link for a create-popup (used in data lists and data tables). - @param r: the S3Request instance + @param r: the CRUDRequest instance @param widget: the widget definition as dict @param list_id: the list ID @param resource: the target resource diff --git a/modules/core/methods/report.py b/modules/core/methods/report.py index 4722e9da1e..109f2a4303 100644 --- a/modules/core/methods/report.py +++ b/modules/core/methods/report.py @@ -40,7 +40,7 @@ import re import sys -from itertools import product +from itertools import product, chain from gluon import current from gluon.contenttype import contenttype @@ -50,8 +50,7 @@ from gluon.storage import Storage from gluon.validators import IS_IN_SET, IS_EMPTY_OR -from ..filters import FS -from ..io import S3XMLFormat +from ..resource import FS, S3XMLFormat, S3Joins from ..tools import s3_flatlist, s3_has_foreign_key, s3_str, S3MarkupStripper, s3_represent_value, JSONERRORS, IS_NUMBER from .base import S3Method @@ -73,7 +72,7 @@ def apply_method(self, r, **attr): """ Page-render entry point for REST interface. - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes for the request """ @@ -98,7 +97,7 @@ def report(self, r, **attr): """ Pivot table report page - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes for the request """ @@ -280,7 +279,7 @@ def geojson(self, r, **attr): Render the pivot table data as a dict ready to be exported as GeoJSON for display on a Map. - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes for the request """ @@ -467,7 +466,7 @@ def widget(self, r, method=None, widget_id=None, visible=True, **attr): """ Pivot table report widget - @param r: the S3Request + @param r: the CRUDRequest @param method: the widget method @param widget_id: the widget ID @param visible: whether the widget is initially visible @@ -580,7 +579,7 @@ def explore(self, r, **attr): - called with a body JSON containing the record IDs to represent, and the URL params for the pivot table (rows, cols, fact) - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes for the request """ @@ -1856,7 +1855,7 @@ def __init__(self, resource, rows, cols, facts, strict=True, precision=None): axes = (rfield for rfield in (rfields[rows], rfields[cols]) if rfield != None) - axisfilter = resource.axisfilter(axes) + axisfilter = self.axisfilter(resource, axes) else: axisfilter = None @@ -2397,7 +2396,7 @@ def xls(self, title): @returns: the XLS file as stream """ - from ..io import S3Codec + from ..resource import S3Codec exporter = S3Codec.get_codec("xls") return exporter.encode_pt(self, title) @@ -2905,4 +2904,252 @@ def _expand(self, row, axisfilter=None): result = [dict(i) for i in product(*pairs)] return result + # ------------------------------------------------------------------------- + @staticmethod + def axisfilter(resource, axes): + """ + Get all values for the given S3ResourceFields (axes) which + match the resource query, used in pivot tables to filter out + additional values where dimensions can have multiple values + per record + + @param axes: the axis fields as list/tuple of S3ResourceFields + + @return: a dict with values per axis, only containes those + axes which are affected by the resource filter + """ + + axisfilter = {} + + qdict = resource.get_query().as_dict(flat=True) + + for rfield in axes: + field = rfield.field + + if field is None: + # virtual field or unresolvable selector + continue + + left_joins = S3Joins(resource.tablename) + left_joins.extend(rfield.left) + + tablenames = list(left_joins.joins.keys()) + tablenames.append(resource.tablename) + af = S3AxisFilter(qdict, tablenames) + + if af.op is not None: + query = af.query() + left = left_joins.as_list() + + # @todo: this does not work with virtual fields: need + # to retrieve all extra_fields for the dimension table + # and can't groupby (=must deduplicate afterwards) + rows = current.db(query).select(field, + left=left, + groupby=field) + colname = rfield.colname + if rfield.ftype[:5] == "list:": + values = [] + vappend = values.append + for row in rows: + v = row[colname] + vappend(v if v else [None]) + values = set(chain.from_iterable(values)) + + include, exclude = af.values(rfield) + fdict = {} + if include: + for v in values: + vstr = s3_str(v) if v is not None else v + if vstr in include and vstr not in exclude: + fdict[v] = None + else: + fdict = dict((v, None) for v in values) + + axisfilter[colname] = fdict + + else: + axisfilter[colname] = dict((row[colname], None) + for row in rows) + + return axisfilter + +# ============================================================================= +class S3AxisFilter(object): + """ + Helper to extract filter values for pivot table axis fields + """ + + # ------------------------------------------------------------------------- + def __init__(self, qdict, tablenames): + """ + Constructor, recursively introspect the query dict and extract + all relevant subqueries. + + @param qdict: the query dict (from Query.as_dict(flat=True)) + @param tablenames: the names of the relevant tables + """ + + self.l = None + self.r = None + self.op = None + + self.tablename = None + self.fieldname = None + + if not qdict: + return + + l = qdict["first"] + if "second" in qdict: + r = qdict["second"] + else: + r = None + + op = qdict["op"] + if op: + # Convert operator name to standard uppercase name + # without underscore prefix + op = op.upper().strip("_") + + if "tablename" in l: + if l["tablename"] in tablenames: + self.tablename = l["tablename"] + self.fieldname = l["fieldname"] + if isinstance(r, dict): + self.op = None + else: + self.op = op + self.r = r + + elif op == "AND": + self.l = S3AxisFilter(l, tablenames) + self.r = S3AxisFilter(r, tablenames) + if self.l.op or self.r.op: + self.op = op + + elif op == "OR": + self.l = S3AxisFilter(l, tablenames) + self.r = S3AxisFilter(r, tablenames) + if self.l.op and self.r.op: + self.op = op + + elif op == "NOT": + self.l = S3AxisFilter(l, tablenames) + self.op = op + + else: + self.l = S3AxisFilter(l, tablenames) + if self.l.op: + self.op = op + + # ------------------------------------------------------------------------- + def query(self): + """ Reconstruct the query from this filter """ + + op = self.op + if op is None: + return None + + if self.tablename and self.fieldname: + l = current.s3db[self.tablename][self.fieldname] + elif self.l: + l = self.l.query() + else: + l = None + + r = self.r + if op in ("AND", "OR", "NOT"): + r = r.query() if r else True + + if op == "AND": + if l is not None and r is not None: + return l & r + elif r is not None: + return r + else: + return l + elif op == "OR": + if l is not None and r is not None: + return l | r + else: + return None + elif op == "NOT": + if l is not None: + return ~l + else: + return None + elif l is None: + return None + + if isinstance(r, S3AxisFilter): + r = r.query() + if r is None: + return None + + if op == "LOWER": + return l.lower() + elif op == "UPPER": + return l.upper() + elif op == "EQ": + return l == r + elif op == "NE": + return l != r + elif op == "LT": + return l < r + elif op == "LE": + return l <= r + elif op == "GE": + return l >= r + elif op == "GT": + return l > r + elif op == "BELONGS": + return l.belongs(r) + elif op == "CONTAINS": + return l.contains(r) + else: + return None + + # ------------------------------------------------------------------------- + def values(self, rfield): + """ + Helper method to filter list:type axis values + + @param rfield: the axis field + + @return: pair of value lists [include], [exclude] + """ + + op = self.op + tablename = self.tablename + fieldname = self.fieldname + + if tablename == rfield.tname and \ + fieldname == rfield.fname: + value = self.r + if isinstance(value, (list, tuple)): + value = [s3_str(v) for v in value] + if not value: + value = [None] + else: + value = [s3_str(value)] + if op == "CONTAINS": + return value, [] + elif op == "EQ": + return value, [] + elif op == "NE": + return [], value + elif op == "AND": + li, le = self.l.values(rfield) + ri, re = self.r.values(rfield) + return [v for v in li + ri if v not in le + re], [] + elif op == "OR": + li, le = self.l.values(rfield) + ri, re = self.r.values(rfield) + return [v for v in li + ri], [] + if op == "NOT": + li, le = self.l.values(rfield) + return [], li + return [], [] + # END ========================================================================= diff --git a/modules/core/methods/rest.py b/modules/core/methods/rest.py new file mode 100644 index 0000000000..881f393862 --- /dev/null +++ b/modules/core/methods/rest.py @@ -0,0 +1,515 @@ +# -*- coding: utf-8 -*- + +""" REST API + + @copyright: 2009-2021 (c) Sahana Software Foundation + @license: MIT + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +""" + +__all__ = ("RESTful",) + +import json +import sys + +from urllib.request import urlopen + +from gluon import current +from gluon.storage import Storage + +from ..tools import s3_parse_datetime + +from .base import S3Method + +# ============================================================================= +class RESTful(S3Method): + """ REST API """ + + # ------------------------------------------------------------------------- + def apply_method(self, r, **attr): + """ + Apply methods + + :param r: the CRUDRequest + :param attr: controller parameters + """ + + http, method = r.http, r.method + if not method: + if http == "GET": + output = self.get_tree(r, **attr) + elif r.http in ("PUT", "POST"): + output = self.put_tree(r, **attr) + # TODO support DELETE + #elif r.http == "DELETE" + # output = self.delete(r, **attr) + else: + r.error(405, current.ERROR.BAD_METHOD) + + elif method == "fields": + if http == "GET": + output = self.get_fields(r, **attr) + else: + r.error(405, current.ERROR.BAD_METHOD) + + elif method == "options": + if http == "GET": + output = self.get_options(r, **attr) + else: + r.error(405, current.ERROR.BAD_METHOD) + + else: + r.error(404, current.ERROR.BAD_ENDPOINT) + + return output + + # ------------------------------------------------------------------------- + # Built-in method handlers + # ------------------------------------------------------------------------- + @staticmethod + def get_tree(r, **attr): + """ + XML Element tree export method + + :param r: the CRUDRequest instance + :param attr: controller attributes + """ + + get_vars = r.get_vars + args = Storage() + + # Slicing + start = get_vars.get("start") + if start is not None: + try: + start = int(start) + except ValueError: + start = None + limit = get_vars.get("limit") + if limit is not None: + try: + limit = int(limit) + except ValueError: + limit = None + + # msince + msince = get_vars.get("msince") + if msince is not None: + msince = s3_parse_datetime(msince) + + # Show IDs (default: False) + if "show_ids" in get_vars: + if get_vars["show_ids"].lower() == "true": + current.xml.show_ids = True + + # Show URLs (default: True) + if "show_urls" in get_vars: + if get_vars["show_urls"].lower() == "false": + current.xml.show_urls = False + + # Mobile data export (default: False) + mdata = get_vars.get("mdata") == "1" + + # Maxbounds (default: False) + maxbounds = False + if "maxbounds" in get_vars: + if get_vars["maxbounds"].lower() == "true": + maxbounds = True + if r.representation in ("gpx", "osm"): + maxbounds = True + + # Components of the master resource (tablenames) + if "mcomponents" in get_vars: + mcomponents = get_vars["mcomponents"] + if str(mcomponents).lower() == "none": + mcomponents = None + elif not isinstance(mcomponents, list): + mcomponents = mcomponents.split(",") + else: + mcomponents = [] # all + + # Components of referenced resources (tablenames) + if "rcomponents" in get_vars: + rcomponents = get_vars["rcomponents"] + if str(rcomponents).lower() == "none": + rcomponents = None + elif not isinstance(rcomponents, list): + rcomponents = rcomponents.split(",") + else: + rcomponents = None + + # Maximum reference resolution depth + if "maxdepth" in get_vars: + try: + args["maxdepth"] = int(get_vars["maxdepth"]) + except ValueError: + pass + + # References to resolve (field names) + if "references" in get_vars: + references = get_vars["references"] + if str(references).lower() == "none": + references = [] + elif not isinstance(references, list): + references = references.split(",") + else: + references = None # all + + # Export field selection + if "fields" in get_vars: + fields = get_vars["fields"] + if str(fields).lower() == "none": + fields = [] + elif not isinstance(fields, list): + fields = fields.split(",") + else: + fields = None # all + + # Find XSLT stylesheet + stylesheet = r.stylesheet() + + # Add stylesheet parameters + if stylesheet is not None: + if r.component: + args["id"] = r.id + args["component"] = r.component.tablename + if r.component.alias: + args["alias"] = r.component.alias + mode = get_vars.get("xsltmode") + if mode is not None: + args["mode"] = mode + + # Set response headers + response = current.response + s3 = response.s3 + headers = response.headers + representation = r.representation + if representation in s3.json_formats: + as_json = True + default = "application/json" + else: + as_json = False + default = "text/xml" + headers["Content-Type"] = s3.content_type.get(representation, + default) + + # Export the resource + resource = r.resource + target = r.target()[3] + if target == resource.tablename: + # Master resource targetted + target = None + output = resource.export_xml(start = start, + limit = limit, + msince = msince, + fields = fields, + dereference = True, + # maxdepth in args + references = references, + mdata = mdata, + mcomponents = mcomponents, + rcomponents = rcomponents, + stylesheet = stylesheet, + as_json = as_json, + maxbounds = maxbounds, + target = target, + **args) + # Transformation error? + if not output: + r.error(400, "XSLT Transformation Error: %s " % current.xml.error) + + return output + + # ------------------------------------------------------------------------- + @staticmethod + def put_tree(r, **attr): + """ + XML Element tree import method + + :param r: the CRUDRequest method + :param attr: controller attributes + """ + + get_vars = r.get_vars + + # Skip invalid records? + ignore_errors = "ignore_errors" in get_vars + + # Find all source names in the URL vars + def findnames(get_vars, name): + nlist = [] + if name in get_vars: + names = get_vars[name] + if isinstance(names, (list, tuple)): + names = ",".join(names) + names = names.split(",") + for n in names: + if n[0] == "(" and ")" in n[1:]: + nlist.append(n[1:].split(")", 1)) + else: + nlist.append([None, n]) + return nlist + filenames = findnames(get_vars, "filename") + fetchurls = findnames(get_vars, "fetchurl") + source_url = None + + # Get the source(s) + s3 = current.response.s3 + json_formats = s3.json_formats + csv_formats = s3.csv_formats + source = [] + representation = r.representation + if representation in json_formats or representation in csv_formats: + if filenames: + try: + for f in filenames: + source.append((f[0], open(f[1], "rb"))) + except: + source = [] + elif fetchurls: + try: + for u in fetchurls: + source.append((u[0], urlopen(u[1]))) + except: + source = [] + elif r.http != "GET": + source = r.read_body() + else: + if filenames: + source = filenames + elif fetchurls: + source = fetchurls + # Assume only 1 URL for GeoRSS feed caching + source_url = fetchurls[0][1] + elif r.http != "GET": + source = r.read_body() + if not source: + if filenames or fetchurls: + # Error: source not found + r.error(400, "Invalid source") + else: + # No source specified => return resource structure + return r.get_struct(r, **attr) + + # Find XSLT stylesheet + stylesheet = r.stylesheet(method="import") + # Target IDs + if r.method == "create": + record_id = None + else: + record_id = r.id + + # Transformation mode? + if "xsltmode" in get_vars: + args = {"xsltmode": get_vars["xsltmode"]} + else: + args = {} + + # These 3 options are called by gis.show_map() & read by the + # GeoRSS Import stylesheet to populate the gis_cache table + # Source URL: For GeoRSS/KML Feed caching + if source_url: + args["source_url"] = source_url + # Data Field: For GeoRSS/KML Feed popups + if "data_field" in get_vars: + args["data_field"] = get_vars["data_field"] + # Image Field: For GeoRSS/KML Feed popups + if "image_field" in get_vars: + args["image_field"] = get_vars["image_field"] + + # Format type? + if representation in json_formats: + representation = "json" + elif representation in csv_formats: + representation = "csv" + else: + representation = "xml" + + try: + result = r.resource.import_xml(source, + record_id = record_id, + source_type = representation, + files = r.files, + stylesheet = stylesheet, + ignore_errors = ignore_errors, + **args) + except IOError: + current.auth.permission.fail() + except SyntaxError: + e = sys.exc_info()[1] + if hasattr(e, "message"): + e = e.message + r.error(400, e) + + if representation == "json": + current.response.headers["Content-Type"] = "application/json" + return result.json_message() + + # ------------------------------------------------------------------------- + @staticmethod + def get_struct(r, **attr): + """ + Resource structure introspection method + + :param r: the CRUDRequest instance + :param attr: controller attributes + """ + + response = current.response + + json_formats = response.s3.json_formats + if r.representation in json_formats: + as_json = True + content_type = "application/json" + else: + as_json = False + content_type = "text/xml" + + get_vars = r.get_vars + meta = str(get_vars.get("meta", False)).lower() == "true" + opts = str(get_vars.get("options", False)).lower() == "true" + refs = str(get_vars.get("references", False)).lower() == "true" + + stylesheet = r.stylesheet() + output = r.resource.export_struct(meta = meta, + options = opts, + references = refs, + stylesheet = stylesheet, + as_json = as_json, + ) + if output is None: + # Transformation error + r.error(400, current.xml.error) + + response.headers["Content-Type"] = content_type + + return output + + # ------------------------------------------------------------------------- + @staticmethod + def get_fields(r, **attr): + """ + Resource structure introspection method (single table) + + :param r: the CRUDRequest instance + :param attr: controller attributes + """ + + representation = r.representation + if representation == "xml": + output = r.resource.export_fields(component=r.component_name) + content_type = "text/xml" + elif representation == "s3json": + output = r.resource.export_fields(component=r.component_name, + as_json=True) + content_type = "application/json" + else: + r.error(415, current.ERROR.BAD_FORMAT) + + response = current.response + response.headers["Content-Type"] = content_type + + return output + + # ------------------------------------------------------------------------- + @staticmethod + def get_options(r, **attr): + """ + Export field options for the table + + :param r: the CRUDRequest instance + :param attr: controller attributes + """ + + get_vars = r.get_vars + + items = get_vars.get("field") + if items: + if not isinstance(items, (list, tuple)): + items = [items] + fields = [] + add_fields = fields.extend + for item in items: + f = item.split(",") + if f: + add_fields(f) + else: + fields = None + + if "hierarchy" in get_vars: + hierarchy = get_vars["hierarchy"].lower() not in ("false", "0") + else: + hierarchy = False + + if "only_last" in get_vars: + only_last = get_vars["only_last"].lower() not in ("false", "0") + else: + only_last = False + + if "show_uids" in get_vars: + show_uids = get_vars["show_uids"].lower() not in ("false", "0") + else: + show_uids = False + + representation = r.representation + flat = False + if representation == "xml": + only_last = False + as_json = False + content_type = "text/xml" + elif representation == "s3json": + show_uids = False + as_json = True + content_type = "application/json" + elif representation == "json" and fields and len(fields) == 1: + # JSON option supported for flat data structures only + # e.g. for use by jquery.jeditable + flat = True + show_uids = False + as_json = True + content_type = "application/json" + else: + r.error(415, current.ERROR.BAD_FORMAT) + + output = r.resource.export_options(component = r.component_name, + fields = fields, + show_uids = show_uids, + only_last = only_last, + hierarchy = hierarchy, + as_json = as_json, + ) + + if flat: + s3json = json.loads(output) + output = {} + options = s3json.get("option") + if options: + for item in options: + output[item.get("@value")] = item.get("$", "") + output = json.dumps(output) + + current.response.headers["Content-Type"] = content_type + + return output + +# END ========================================================================= diff --git a/modules/core/methods/rolemgr.py b/modules/core/methods/rolemgr.py index 714f692c35..97925e3d6c 100644 --- a/modules/core/methods/rolemgr.py +++ b/modules/core/methods/rolemgr.py @@ -40,8 +40,7 @@ from s3dal import Field -from ..filters import FS -from ..io import SEPARATORS +from ..resource import FS, SEPARATORS from ..tools import s3_str, s3_mark_required, JSONERRORS from ..ui import s3_comments_widget @@ -57,7 +56,7 @@ def apply_method(self, r, **attr): """ Entry point for REST interface. - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes """ @@ -118,7 +117,7 @@ def role_list(self, r, **attr): """ List or export roles - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes NB this function must be restricted to ADMINs (in apply_method) @@ -268,7 +267,7 @@ def role_list_actions(self, r): """ Configure action buttons for role list - @param r: the S3Request + @param r: the CRUDRequest """ T = current.T diff --git a/modules/core/methods/ssi.py b/modules/core/methods/ssi.py index d2b85db4ba..4a95f58a6f 100644 --- a/modules/core/methods/ssi.py +++ b/modules/core/methods/ssi.py @@ -41,7 +41,6 @@ from s3dal import Field -from ..io import S3ImportJob from ..tools import s3_mark_required, s3_str, s3_addrow from .base import S3Method @@ -63,7 +62,7 @@ def apply_method(self, r, **attr): """ Full-page method - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller parameters (see below) @keyword csv_extra_fields: add values to each row in the CSV, @@ -124,7 +123,7 @@ def upload(self, r, **attr): """ Request/submit upload form - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller parameters """ @@ -170,7 +169,7 @@ def upload(self, r, **attr): fmt = fmt, stylesheet = stylesheet, extra_data = extra_data, - #commit_job = False, + #commit = False, **args, ) except ValueError: @@ -201,7 +200,7 @@ def select_items(self, job_id, r, **attr): - submitting the selection goes to commit() @param job_id: the import job UUID (or None to read from request vars) - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller parameters """ @@ -212,9 +211,10 @@ def select_items(self, job_id, r, **attr): if not job_id: r.error(400, T("No import job specified")) + s3db = current.s3db s3 = current.response.s3 - itable = S3ImportJob.define_item_table() + itable = s3db.s3_import_item field = itable.element field.represent = self.element_represent @@ -222,10 +222,10 @@ def select_items(self, job_id, r, **attr): # Target resource tablename ttablename = r.resource.tablename - from ..filters import FS + from ..resource import FS query = (FS("job_id") == job_id) & \ (FS("tablename") == ttablename) - iresource = current.s3db.resource(itable, filter=query) + iresource = s3db.resource(itable, filter=query) # Get a list of the records that have an error of None query = (itable.job_id == job_id) & \ @@ -355,15 +355,14 @@ def commit(self, r, **attr): """ Commit the selected items (coming from select_items()) - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller parameters """ T = current.T - post_vars = r.post_vars - # Get the import job ID + post_vars = r.post_vars job_id = post_vars.get("job_id") if not job_id: r.error(400, T("Missing import job ID")) @@ -375,7 +374,8 @@ def commit(self, r, **attr): r.unauthorised() # Check that the job exists - jtable = S3ImportJob.define_job_table() + s3db = current.s3db + jtable = s3db.s3_import_job query = (jtable.job_id == job_id) job = current.db(query).select(jtable.id, limitby = (0, 1), @@ -393,7 +393,7 @@ def commit(self, r, **attr): if mode == "Inclusive": select_items = selected elif mode == "Exclusive": - itable = S3ImportJob.define_item_table() + itable = s3db.s3_import_item query = (itable.job_id == job_id) & \ (itable.tablename == tablename) if selected: @@ -431,7 +431,7 @@ def upload_form(self, r, **attr): """ Construct the upload form - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller parameters @returns: FORM @@ -514,7 +514,7 @@ def get_template_url(r, **attr): """ Get a download URL for the CSV template - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller parameters @returns: URL (or None if no CSV template can be downloaded) @@ -622,7 +622,7 @@ def get_stylesheet(r, **attr): """ Get the XSLT transformation stylesheet - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller parameters @returns: the path to the XSLT stylesheet @@ -657,7 +657,7 @@ def import_from_source(cls, fmt = "csv", stylesheet = None, extra_data = None, - commit_job = False, + commit = False, **args, ): """ @@ -667,25 +667,25 @@ def import_from_source(cls, @param source: the source (file-like object) @param fmt: the source file format (in connection with source) @param extra_data: extra data to add to source rows (in connection with source) - @param commit_job: whether to commit the import immediately (in connection with source) + @param commit: whether to commit the import immediately (in connection with source) @param args: additional stylesheet args @returns: import job UUID """ - resource.import_xml(source, - format = fmt, - extra_data = extra_data, - commit_job = commit_job, - ignore_errors = True, - stylesheet = stylesheet, - **args) + result = resource.import_xml(source, + source_type = fmt, + extra_data = extra_data, + commit = commit, + ignore_errors = True, + stylesheet = stylesheet, + **args) - job = resource.job - if not job and resource.error: - raise ValueError(resource.error) + job_id = result.job_id + if not job_id and result.error: + raise ValueError(result.error) - return job.job_id if job else None + return job_id # ------------------------------------------------------------------------- def element_represent(self, value): @@ -698,15 +698,13 @@ def element_represent(self, value): @returns: DIV containing a representation of the element """ - s3db = current.s3db - try: element = etree.fromstring(value) except (etree.ParseError, etree.XMLSyntaxError): return DIV(value) - tablename = element.get("name") - table = s3db[tablename] + s3db = current.s3db + table = s3db[element.get("name")] output = DIV() details = TABLE(_class="import-item-details") @@ -718,7 +716,6 @@ def element_represent(self, value): # Add component details, if present components = element.findall("resource") - s3db = current.s3db for component in components: ctablename = component.get("name") ctable = s3db.table(ctablename) @@ -826,7 +823,7 @@ def commit_import_job(cls, db = current.db # Count matching top-level items in job - itable = S3ImportJob.define_item_table() + itable = current.s3db.s3_import_item query = (itable.job_id == job_id) & \ (itable.tablename == resource.tablename) & \ (itable.parent == None) @@ -837,16 +834,16 @@ def commit_import_job(cls, selected = db(query).count() # Commit the job - resource.import_xml(None, - job_id = job_id, - select_items = select_items, - ignore_errors = True, - ) + result = resource.import_xml(None, + job_id = job_id, + select_items = select_items, + ignore_errors = True, + ) return {"total": total, - "imported": resource.import_count, + "imported": result.count, "skipped": max(total - selected, 0), - "errors": resource.import_errors, + "errors": result.failed, } # END ========================================================================= diff --git a/modules/core/methods/summary.py b/modules/core/methods/summary.py index 154ad248d8..d3ec9740a0 100644 --- a/modules/core/methods/summary.py +++ b/modules/core/methods/summary.py @@ -34,6 +34,7 @@ from ..filters import S3FilterForm from .base import S3Method +from .crud import S3CRUD # ============================================================================= class S3Summary(S3Method): @@ -44,7 +45,7 @@ def apply_method(self, r, **attr): """ Entry point for REST interface - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller attributes """ @@ -60,7 +61,7 @@ def summary(self, r, **attr): """ Render the summary page - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller attributes """ @@ -157,7 +158,7 @@ def summary(self, r, **attr): handler = r.get_widget_handler(method) if handler is None: # Fall back to CRUD - handler = resource.crud + handler = S3CRUD() if handler is not None: if method == "datatable": # Assume that we have a FilterForm, so disable Quick Search @@ -297,7 +298,7 @@ def ajax(self, r, **attr): """ Render a specific widget for pulling-in via AJAX - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller attributes """ diff --git a/modules/core/methods/timeplot.py b/modules/core/methods/timeplot.py index d6e516237f..b4395ed6ae 100644 --- a/modules/core/methods/timeplot.py +++ b/modules/core/methods/timeplot.py @@ -50,13 +50,13 @@ from gluon.validators import IS_IN_SET from gluon.sqlhtml import OptionsWidget -from ..filters import FS +from ..resource import FS from ..tools import s3_decode_iso_datetime, s3_utc, s3_flatlist, s3_represent_value, s3_str, S3MarkupStripper from .base import S3Method from .report import S3Report, S3ReportForm -tp_datetime = lambda *t: datetime.datetime(tzinfo=dateutil.tz.tzutc(), *t) +tp_datetime = lambda year, *t: datetime.datetime(year, *t, tzinfo=dateutil.tz.tzutc()) tp_tzsafe = lambda dt: dt.replace(tzinfo=dateutil.tz.tzutc()) \ if dt and dt.tzinfo is None else dt @@ -87,7 +87,7 @@ def apply_method(self, r, **attr): """ Page-render entry point for REST interface. - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes for the request """ @@ -102,7 +102,7 @@ def widget(self, r, method=None, widget_id=None, visible=True, **attr): """ Widget-render entry point for S3Summary. - @param r: the S3Request + @param r: the CRUDRequest @param method: the widget method @param widget_id: the widget ID @param visible: whether the widget is initially visible @@ -196,7 +196,7 @@ def timeplot(self, r, **attr): """ Time plot report page - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes for the request """ @@ -336,7 +336,7 @@ def get_target(self, r): """ Identify the target resource - @param r: the S3Request + @param r: the CRUDRequest """ # Fallback @@ -359,7 +359,7 @@ def get_options(r, resource): """ Read the relevant GET vars for the timeplot - @param r: the S3Request + @param r: the CRUDRequest @param resource: the target S3Resource """ diff --git a/modules/core/methods/xforms.py b/modules/core/methods/xforms.py index 76de4ec1f9..e183e2dbc6 100644 --- a/modules/core/methods/xforms.py +++ b/modules/core/methods/xforms.py @@ -46,7 +46,7 @@ def apply_method(self, r, **attr): """ Apply CRUD methods - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller parameters for the request @return: output object to send to the view @@ -62,7 +62,7 @@ def form(self, r, **attr): """ Generate an XForms form for the current resource - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller parameters for the request """ diff --git a/modules/core/model/__init__.py b/modules/core/model/__init__.py index b6e75a328c..dd3f52fd61 100644 --- a/modules/core/model/__init__.py +++ b/modules/core/model/__init__.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from .base import DEFAULT, S3Model -from .delete import * +from .datamodel import DEFAULT, DataModel from .dynamic import DYNAMIC_PREFIX, SERIALIZABLE_OPTS from .fields import * -from .resource import * diff --git a/modules/core/model/base.py b/modules/core/model/datamodel.py similarity index 90% rename from modules/core/model/base.py rename to modules/core/model/datamodel.py index 992c1293f5..d08d1ef61e 100644 --- a/modules/core/model/base.py +++ b/modules/core/model/datamodel.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -""" S3 Data Model Extensions +""" Data Models @copyright: 2009-2021 (c) Sahana Software Foundation @license: MIT @@ -27,7 +27,7 @@ OTHER DEALINGS IN THE SOFTWARE. """ -__all__ = ("S3Model", +__all__ = ("DataModel", ) import sys @@ -41,20 +41,19 @@ from ..tools import IS_ONE_OF from ..ui import S3ScriptItem -from .dynamic import S3DynamicModel, DYNAMIC_PREFIX -from .resource import S3Resource +from .dynamic import DynamicTableModel, DYNAMIC_PREFIX DEFAULT = lambda: None MODULE_TYPE = type(sys) # ============================================================================= -class S3Model(object): - """ Base class for S3 models """ +class DataModel(object): + """ Base class for data models """ - _s3model = True + _edenmodel = True - LOCK = "s3_model_lock" - LOAD = "s3_model_load" + LOCK = "eden_model_lock" + LOAD = "eden_model_load" DELETED = "deleted" def __init__(self, module=None): @@ -225,7 +224,7 @@ def module_map(self): try: p = __import__(package, fromlist=("DEFAULT",)) except ImportError: - current.log.error("S3Model cannot import package %s" % package) + current.log.error("DataModel cannot import package %s" % package) continue for k, v in p.__dict__.items(): @@ -240,14 +239,13 @@ def module_map(self): @classmethod def table(cls, tablename, default=None, db_only=False): """ - Helper function to load a table or other named object from - models + Helper function to load a table or other named object from models - @param tablename: the table name (or name of the object) - @param default: the default value to return if not found, + :param tablename: the table name (or name of the object) + :param default: the default value to return if not found, - if default is an exception instance, it will be raised instead of returned - @param db_only: find only tables, not other objects + :param db_only: find only tables, not other objects """ s3 = current.response.s3 @@ -275,7 +273,7 @@ def table(cls, tablename, default=None, db_only=False): prefix = tablename.split("_", 1)[0] if prefix == DYNAMIC_PREFIX: try: - found = S3DynamicModel(tablename).table + found = DynamicTableModel(tablename).table except AttributeError: pass else: @@ -292,12 +290,12 @@ def table(cls, tablename, default=None, db_only=False): s3db.classes[tablename] = module found = s3models[tablename] else: - # A name defined in an S3Model + # A name defined in a DataModel generic = [] loaded = False for n in names: model = s3models[n] - if hasattr(model, "_s3model"): + if hasattr(model, "_edenmodel"): if hasattr(model, "names"): if tablename in model.names: model(prefix) @@ -328,9 +326,9 @@ def table(cls, tablename, default=None, db_only=False): @classmethod def load(cls, prefix): """ - Helper function to load all S3Models in a module + Helper function to load all DataModels in a module - @param prefix: the module prefix + :param prefix: the module prefix """ s3 = current.response.s3 @@ -345,7 +343,7 @@ def load(cls, prefix): for n in module.__all__: model = module.__dict__[n] if type(model).__name__ == "type" and \ - issubclass(model, S3Model): + issubclass(model, DataModel): model(prefix) elif n.startswith("%s_" % prefix): s3[n] = model @@ -367,11 +365,6 @@ def load_all_models(cls): for prefix in current.s3db.module_map: cls.load(prefix) - # Define importer tables - from ..io import S3ImportJob - S3ImportJob.define_job_table() - S3ImportJob.define_item_table() - # Define Scheduler tables # - already done during Scheduler().init() run during S3Task().init() in models/tasks.py #settings = current.deployment_settings @@ -419,10 +412,10 @@ def get_aliased(table, alias): re-instantiation of an already existing alias for the same table (which can otherwise lead to name collisions in PyDAL). - @param table: the original table - @param alias: the alias + :param table: the original table + :param alias: the alias - @return: the aliased Table instance + :returns: the aliased Table instance """ db = current.db @@ -449,6 +442,7 @@ def resource(tablename, *args, **kwargs): the global s3db.resource() method """ + from ..resource import S3Resource return S3Resource(tablename, *args, **kwargs) # ------------------------------------------------------------------------- @@ -457,8 +451,8 @@ def configure(cls, tablename, **attr): """ Update the extra configuration of a table - @param tablename: the name of the table - @param attr: dict of attributes to update + :param tablename: the name of the table + :param attr: dict of attributes to update """ config = current.model["config"] @@ -475,8 +469,8 @@ def get_config(cls, tablename, key, default=None): """ Reads a configuration attribute of a resource - @param tablename: the name of the resource DB table - @param key: the key (name) of the attribute + :param tablename: the name of the resource DB table + :param key: the key (name) of the attribute """ config = current.model["config"] @@ -493,8 +487,8 @@ def clear_config(cls, tablename, *keys): """ Removes configuration attributes of a resource - @param table: the resource DB table - @param keys: keys of attributes to remove (maybe multiple) + :param table: the resource DB table + :param keys: keys of attributes to remove (maybe multiple) """ config = current.model["config"] @@ -516,12 +510,12 @@ def add_custom_callback(cls, tablename, hook, cb, method=None): callback to the originally configured callback chain, for use in customise_* in templates - @param tablename: the table name - @param hook: the main hook ("onvalidation"|"onaccept") - @param cb: the custom callback function - @param method: the sub-hook ("create"|"update"|None) + :param tablename: the table name + :param hook: the main hook ("onvalidation"|"onaccept") + :param cb: the custom callback function + :param method: the sub-hook ("create"|"update"|None) - @example: + Example: # Add a create-onvalidation callback for the pr_person # table, while retaining any existing onvalidation: s3db.add_custom_callback("pr_person", @@ -590,9 +584,8 @@ def virtual_reference(cls, field): }, ) - @param field: the Field - - @returns: the name of the referenced table + :param field: the Field + :returns: the name of the referenced table """ if str(field.type) == "integer": @@ -622,9 +615,9 @@ def onaccept(cls, table, record, method="create"): """ Helper to run the onvalidation routine for a record - @param table: the Table - @param record: the FORM or the Row to validate - @param method: the method + :param table: the Table + :param record: the FORM or the Row to validate + :param method: the method """ if hasattr(table, "_tablename"): @@ -647,9 +640,9 @@ def onvalidation(cls, table, record, method="create"): """ Helper to run the onvalidation routine for a record - @param table: the Table - @param record: the FORM or the Row to validate - @param method: the method + :param table: the Table + :param record: the FORM or the Row to validate + :param method: the method """ if hasattr(table, "_tablename"): @@ -673,8 +666,8 @@ def add_components(cls, master, **links): """ Configure component links for a master table. - @param master: the name of the master table - @param links: component link configurations + :param master: the name of the master table + :param links: component link configurations """ components = current.model["components"] @@ -798,8 +791,8 @@ def add_dynamic_components(cls, tablename, exclude=None): for a table; called by get_components if dynamic_components is configured for the table - @param tablename: the table name - @param exclude: names to exclude (static components) + :param tablename: the table name + :param exclude: names to exclude (static components) """ mtable = cls.table(tablename) @@ -876,10 +869,10 @@ def get_component(cls, table, alias): """ Get a component description for a component alias - @param table: the master table - @param alias: the component alias + :param table: the master table + :param alias: the component alias - @returns: the component description (Storage) + :returns: the component description (Storage) """ return cls.parse_hook(table, alias) @@ -889,11 +882,11 @@ def get_components(cls, table, names=None): """ Finds components of a table - @param table: the table or table name - @param names: a list of components names to limit the search to, + :param table: the table or table name + :param names: a list of components names to limit the search to, None for all available components - @returns: the component descriptions (Storage {alias: description}) + :returns: the component descriptions (Storage {alias: description}) """ table, hooks = cls.get_hooks(table, names=names) @@ -915,11 +908,11 @@ def parse_hook(cls, table, alias, hook=None): Parse a component configuration, loading all necessary table models and applying defaults - @param table: the master table - @param alias: the component alias - @param hook: the component configuration (if already known) + :param table: the master table + :param alias: the component alias + :param hook: the component configuration (if already known) - @returns: the component description (Storage {key: value}) + :returns: the component description (Storage {key: value}) """ load = cls.table @@ -1007,11 +1000,11 @@ def get_hooks(cls, table, names=None): """ Find applicable component configurations (hooks) for a table - @param table: the master table (or table name) - @param names: component aliases to find (default: all configured + :param table: the master table (or table name) + :param names: component aliases to find (default: all configured components for the master table) - @returns: tuple (table, {alias: hook, ...}) + :returns: tuple (table, {alias: hook, ...}) """ components = current.model["components"] @@ -1093,12 +1086,12 @@ def __filter_hooks(cls, components, hooks, names=None, supertable=None): """ DRY Helper method to filter component hooks - @param components: components already found, dict {alias: component} - @param hooks: component hooks to filter, dict {alias: hook} - @param names: the names (=aliases) to include - @param supertable: the super-table name to set for the component + :param components: components already found, dict {alias: component} + :param hooks: component hooks to filter, dict {alias: hook} + :param names: the names (=aliases) to include + :param supertable: the super-table name to set for the component - @returns: set of names that could not be found, + :returns: set of names that could not be found, or None if names was None """ @@ -1118,7 +1111,7 @@ def has_components(cls, table): """ Checks whether there are components defined for a table - @param table: the table or table name + :param table: the table or table name """ components = current.model["components"] @@ -1172,8 +1165,8 @@ def get_alias(cls, tablename, link): """ Find a component alias from the link table alias. - @param tablename: the name of the master table - @param link: the alias of the link table + :param tablename: the name of the master table + :param link: the alias of the link table """ components = current.model["components"] @@ -1226,9 +1219,9 @@ def hierarchy_link(cls, tablename): Get the alias of the component that represents the parent node in a hierarchy (for link-table based hierarchies) - @param tablename: the table name + :param tablename: the table name - @returns: the alias of the hierarchy parent component + :returns: the alias of the hierarchy parent component """ if not cls.table(tablename, db_only=True): @@ -1249,19 +1242,19 @@ def hierarchy_link(cls, tablename): # Resource Methods # ------------------------------------------------------------------------- @classmethod - def set_method(cls, prefix, name, - component_name = None, + def set_method(cls, tablename, + component = None, method = None, action = None, ): """ - Adds a custom method for a resource or component + Configure a URL method for a table, or a component in the context + of the table - @param prefix: prefix of the resource name (=module name) - @param name: name of the resource (=without prefix) - @param component_name: name of the component - @param method: name of the method - @param action: function to invoke for this method + :param str tablename: the name of the table + :param str component: component alias + :param str method: name of the method + :param action: function to invoke for this method """ methods = current.model["methods"] @@ -1270,31 +1263,29 @@ def set_method(cls, prefix, name, if not method: raise SyntaxError("No method specified") - tablename = "%s_%s" % (prefix, name) - - if not component_name: + if not component: if method not in methods: methods[method] = {} methods[method][tablename] = action else: if method not in cmethods: cmethods[method] = {} - if component_name not in cmethods[method]: - cmethods[method][component_name] = {} - cmethods[method][component_name][tablename] = action + if component not in cmethods[method]: + cmethods[method][component] = {} + cmethods[method][component][tablename] = action # ------------------------------------------------------------------------- @classmethod - def get_method(cls, prefix, name, - component_name=None, - method=None): + def get_method(cls, tablename, component=None, method=None): """ - Retrieves a custom method for a resource or component + Get the handler for a URL method for a table, or a component + in the context of the table - @param prefix: prefix of the resource name (=module name) - @param name: name of the resource (=without prefix) - @param component_name: name of the component - @param method: name of the method + :param tablename: the name of the table + :param component: component alias + :param method: name of the method + + :returns: the method handler """ methods = current.model["methods"] @@ -1303,18 +1294,16 @@ def get_method(cls, prefix, name, if not method: return None - tablename = "%s_%s" % (prefix, name) - - if not component_name: + if not component: if method in methods and tablename in methods[method]: return methods[method][tablename] else: return None else: if method in cmethods and \ - component_name in cmethods[method] and \ - tablename in cmethods[method][component_name]: - return cmethods[method][component_name][tablename] + component in cmethods[method] and \ + tablename in cmethods[method][component]: + return cmethods[method][component][tablename] else: return None @@ -1326,11 +1315,11 @@ def super_entity(cls, tablename, key, types, *fields, **args): """ Define a super-entity table - @param tablename: the tablename - @param key: name of the primary key - @param types: a dictionary of instance types - @param fields: any shared fields - @param args: table arguments (e.g. migrate) + :param tablename: the tablename + :param key: name of the primary key + :param types: a dictionary of instance types + :param fields: any shared fields + :param args: table arguments (e.g. migrate) """ db = current.db @@ -1367,7 +1356,7 @@ def super_key(cls, supertable, default=None): """ Get the name of the key for a super-entity - @param supertable: the super-entity table + :param supertable: the super-entity table """ if supertable is None and default: @@ -1409,11 +1398,11 @@ def super_link(cls, """ Get a foreign key field for a super-entity - @param supertable: the super-entity table - @param label: label for the field - @param comment: comment for the field - @param readable: set the field readable - @param represent: set a representation function for the field + :param supertable: the super-entity table + :param label: label for the field + :param comment: comment for the field + :param readable: set the field readable + :param represent: set a representation function for the field """ if isinstance(supertable, str): @@ -1482,8 +1471,8 @@ def update_super(cls, table, record): """ Updates the super-entity links of an instance record - @param table: the instance table - @param record: the instance record + :param table: the instance table + :param record: the instance record """ get_config = cls.get_config @@ -1612,11 +1601,11 @@ def delete_super(cls, table, record): """ Removes the super-entity links of an instance record - @param table: the instance table - @param record: the instance record + :param table: the instance table + :param record: the instance record - @return: True if successful, otherwise False (caller must - roll back the transaction if False is returned!) + :returns: True if successful, otherwise False (caller must + roll back the transaction if False is returned!) """ # Must have a record ID @@ -1685,8 +1674,8 @@ def get_super_keys(cls, table): """ Get the super-keys in an instance table - @param table: the instance table - @returns: list of field names + :param table: the instance table + :returns: list of field names """ tablename = original_tablename(table) @@ -1716,9 +1705,9 @@ def get_instance(cls, supertable, superid): """ Get prefix, name and ID of an instance record - @param supertable: the super-entity table - @param superid: the super-entity record ID - @return: a tuple (prefix, name, ID) of the instance + :param supertable: the super-entity table + :param superid: the super-entity record ID + :returns: a tuple (prefix, name, ID) of the instance record (if it exists) """ diff --git a/modules/core/model/dynamic.py b/modules/core/model/dynamic.py index 9497ae5d78..15458e3d61 100644 --- a/modules/core/model/dynamic.py +++ b/modules/core/model/dynamic.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -""" S3 Data Model Extensions +""" Dynamic Table Models @copyright: 2009-2021 (c) Sahana Software Foundation @license: MIT @@ -27,7 +27,7 @@ OTHER DEALINGS IN THE SOFTWARE. """ -__all__ = ("S3DynamicModel", +__all__ = ("DynamicTableModel", "DYNAMIC_PREFIX", "SERIALIZABLE_OPTS", ) @@ -59,7 +59,7 @@ DEFAULT = lambda: None # ============================================================================= -class S3DynamicModel(object): +class DynamicTableModel(object): """ Class representing a dynamic table model """ diff --git a/modules/core/model/resource.py b/modules/core/model/resource.py deleted file mode 100644 index d76ab27c46..0000000000 --- a/modules/core/model/resource.py +++ /dev/null @@ -1,5927 +0,0 @@ -# -*- coding: utf-8 -*- - -""" S3 Resources - - @copyright: 2009-2021 (c) Sahana Software Foundation - @license: MIT - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. - - @group Resource API: S3Resource, - @group Filter API: S3ResourceFilter - @group Helper Classes: S3AxisFilter, S3ResourceData -""" - -__all__ = ("S3AxisFilter", - "S3Resource", - "S3ResourceFilter", - "MAXDEPTH", - ) - -import json -import sys - -from functools import reduce -from io import StringIO -from itertools import chain - -try: - from lxml import etree -except ImportError: - sys.stderr.write("ERROR: lxml module needed for XML handling\n") - raise - -from gluon import current -from gluon.html import A, TAG -from gluon.validators import IS_EMPTY_OR -from gluon.storage import Storage -from gluon.tools import callback - -from s3dal import Expression, Field, Row, Rows, Table, S3DAL, VirtualCommand -from ..filters import FS, S3ResourceField, S3ResourceQuery, S3Joins, S3URLQuery -from ..tools import s3_format_datetime, s3_get_last_record_id, s3_has_foreign_key, s3_remove_last_record_id, s3_str, IS_ONE_OF -from ..ui import S3DataTable, S3DataList - -from .fields import s3_all_meta_field_names - -osetattr = object.__setattr__ -ogetattr = object.__getattribute__ - -MAXDEPTH = 10 -DEFAULT = lambda: None - -# Compact JSON encoding -#SEPARATORS = (",", ":") - -# ============================================================================= -class S3Resource(object): - """ - API for resources. - - A "resource" is a set of records in a database table including their - references in certain related resources (components). A resource can - be defined like: - - resource = S3Resource(table) - - A resource defined like this would include all records in the table. - Further parameters for the resource constructor as well as methods - of the resource instance can be used to filter for particular subsets. - - This API provides extended standard methods to access and manipulate - data in resources while respecting current authorization and other - S3 framework rules. - """ - - def __init__(self, tablename, - id = None, - prefix = None, - uid = None, - filter = None, - vars = None, - parent = None, - linked = None, - linktable = None, - alias = None, - components = None, - filter_component = None, - include_deleted = False, - approved = True, - unapproved = False, - context = False, - extra_filters = None - ): - """ - Constructor - - @param tablename: tablename, Table, or an S3Resource instance - @param prefix: prefix to use for the tablename - - @param id: record ID (or list of record IDs) - @param uid: record UID (or list of record UIDs) - - @param filter: filter query - @param vars: dictionary of URL query variables - - @param components: list of component aliases - to load for this resource - @param filter_component: alias of the component the URL filters - apply for (filters for this component - must be handled separately) - - @param alias: the alias for this resource (internal use only) - @param parent: the parent resource (internal use only) - @param linked: the linked resource (internal use only) - @param linktable: the link table (internal use only) - - @param include_deleted: include deleted records (used for - synchronization) - - @param approved: include approved records - @param unapproved: include unapproved records - @param context: apply context filters - @param extra_filters: extra filters (to be applied on - pre-filtered subsets), as list of - tuples (method, expression) - """ - - s3db = current.s3db - auth = current.auth - - # Names --------------------------------------------------------------- - - table = None - table_alias = None - - if prefix is None: - if not isinstance(tablename, str): - if isinstance(tablename, Table): - table = tablename - table_alias = table._tablename - tablename = table_alias - - #self.table = tablename - #self._alias = self.table._tablename - #tablename = self._alias - elif isinstance(tablename, S3Resource): - table = tablename.table - table_alias = table._tablename - tablename = tablename.tablename - - #self.table = tablename.table - #self._alias = self.table._tablename - #tablename = tablename.tablename - else: - error = "%s is not a valid type for a tablename" % tablename - raise SyntaxError(error) - if "_" in tablename: - prefix, name = tablename.split("_", 1) - else: - raise SyntaxError("invalid tablename: %s" % tablename) - else: - name = tablename - tablename = "%s_%s" % (prefix, name) - - self.tablename = tablename - - # Module prefix and resource name - self.prefix = prefix - self.name = name - - # Resource alias defaults to tablename without module prefix - if not alias: - alias = name - self.alias = alias - - # Table --------------------------------------------------------------- - - if table is None: - table = s3db[tablename] - - # Set default approver - auth.permission.set_default_approver(table) - - if parent is not None: - if parent.tablename == self.tablename: - # Component table same as parent table => must use table alias - table_alias = "%s_%s_%s" % (prefix, alias, name) - table = s3db.get_aliased(table, table_alias) - - self.table = table - self._alias = table_alias or tablename - - self.fields = table.fields - self._id = table._id - - self.defaults = None - - # Hooks --------------------------------------------------------------- - - # Authorization hooks - self.accessible_query = auth.s3_accessible_query - - # Filter -------------------------------------------------------------- - - # Default query options - self.include_deleted = include_deleted - self._approved = approved - self._unapproved = unapproved - - # Component Filter - self.filter = None - - # Resource Filter - self.rfilter = None - - # Rows ---------------------------------------------------------------- - - self._rows = None - self._rowindex = None - self.rfields = None - self.dfields = None - self._ids = [] - self._uids = [] - self._length = None - - # Request attributes -------------------------------------------------- - - self.vars = None # set during build_query - self.lastid = None - self.files = Storage() - - # Components ---------------------------------------------------------- - - # Initialize component properties (will be set during _attach) - self.link = None - self.linktable = None - self.actuate = None - self.lkey = None - self.rkey = None - self.pkey = None - self.fkey = None - self.multiple = True - - self.parent = parent # the parent resource - self.linked = linked # the linked resource - - self.components = S3Components(self, components) - self.links = self.components.links - - if parent is None: - # Build query - self.build_query(id = id, - uid = uid, - filter = filter, - vars = vars, - extra_filters = extra_filters, - filter_component = filter_component, - ) - if context: - self.add_filter(s3db.context) - - # Component - attach link table - elif linktable is not None: - # This is a link-table component - attach the link table - link_alias = "%s__link" % self.alias - self.link = S3Resource(linktable, - alias = link_alias, - parent = self.parent, - linked = self, - include_deleted = self.include_deleted, - approved = self._approved, - unapproved = self._unapproved, - ) - - # Export and Import --------------------------------------------------- - - # Pending Imports - self.skip_import = False - self.job = None - self.mtime = None - self.error = None - self.error_tree = None - self.import_count = 0 - self.import_errors = 0 - self.import_created = [] - self.import_updated = [] - self.import_deleted = [] - - # Export meta data - self.muntil = None # latest mtime of the exported records - self.results = None # number of exported records - - # Standard methods ---------------------------------------------------- - - # CRUD - from ..methods import S3CRUD - self.crud = S3CRUD() - self.crud.resource = self - - # ------------------------------------------------------------------------- - # Query handling - # ------------------------------------------------------------------------- - def build_query(self, - id = None, - uid = None, - filter = None, - vars = None, - extra_filters = None, - filter_component = None, - ): - """ - Query builder - - @param id: record ID or list of record IDs to include - @param uid: record UID or list of record UIDs to include - @param filter: filtering query (DAL only) - @param vars: dict of URL query variables - @param extra_filters: extra filters (to be applied on - pre-filtered subsets), as list of - tuples (method, expression) - @param filter_component: the alias of the component the URL - filters apply for (filters for this - component must be handled separately) - """ - - # Reset the rows counter - self._length = None - - self.rfilter = S3ResourceFilter(self, - id = id, - uid = uid, - filter = filter, - vars = vars, - extra_filters = extra_filters, - filter_component = filter_component, - ) - return self.rfilter - - # ------------------------------------------------------------------------- - def add_filter(self, f=None, c=None): - """ - Extend the current resource filter - - @param f: a Query or a S3ResourceQuery instance - @param c: alias of the component this filter concerns, - automatically adds the respective component join - (not needed for S3ResourceQuery instances) - """ - - if f is None: - return - - self.clear() - - if self.rfilter is None: - self.rfilter = S3ResourceFilter(self) - - self.rfilter.add_filter(f, component=c) - - # ------------------------------------------------------------------------- - def add_component_filter(self, alias, f=None): - """ - Extend the resource filter of a particular component, does - not affect the master resource filter (as opposed to add_filter) - - @param alias: the alias of the component - @param f: a Query or a S3ResourceQuery instance - """ - - if f is None: - return - - if self.rfilter is None: - self.rfilter = S3ResourceFilter(self) - - self.rfilter.add_filter(f, component=alias, master=False) - - # ------------------------------------------------------------------------- - def add_extra_filter(self, method, expression): - """ - And an extra filter (to be applied on pre-filtered subsets) - - @param method: a name of a known filter method, or a - callable filter method - @param expression: the filter expression (string) - """ - - self.clear() - - if self.rfilter is None: - self.rfilter = S3ResourceFilter(self) - - self.rfilter.add_extra_filter(method, expression) - - # ------------------------------------------------------------------------- - def set_extra_filters(self, filters): - """ - Replace the current extra filters - - @param filters: list of tuples (method, expression), or None - to remove all extra filters - """ - - self.clear() - - if self.rfilter is None: - self.rfilter = S3ResourceFilter(self) - - self.rfilter.set_extra_filters(filters) - - # ------------------------------------------------------------------------- - def get_query(self): - """ - Get the effective query - - @return: Query - """ - - if self.rfilter is None: - self.build_query() - - return self.rfilter.get_query() - - # ------------------------------------------------------------------------- - def get_filter(self): - """ - Get the effective virtual filter - - @return: S3ResourceQuery - """ - - if self.rfilter is None: - self.build_query() - - return self.rfilter.get_filter() - - # ------------------------------------------------------------------------- - def clear_query(self): - """ - Remove the current query (does not remove the set!) - """ - - self.rfilter = None - - for component in self.components.loaded.values(): - component.clear_query() - - # ------------------------------------------------------------------------- - # Data access (new API) - # ------------------------------------------------------------------------- - def count(self, left=None, distinct=False): - """ - Get the total number of available records in this resource - - @param left: left outer joins, if required - @param distinct: only count distinct rows - """ - - if self.rfilter is None: - self.build_query() - if self._length is None: - self._length = self.rfilter.count(left = left, - distinct = distinct) - return self._length - - # ------------------------------------------------------------------------- - def select(self, - fields, - start = 0, - limit = None, - left = None, - orderby = None, - groupby = None, - distinct = False, - virtual = True, - count = False, - getids = False, - as_rows = False, - represent = False, - show_links = True, - raw_data = False, - ): - """ - Extract data from this resource - - @param fields: the fields to extract (selector strings) - @param start: index of the first record - @param limit: maximum number of records - @param left: additional left joins required for filters - @param orderby: orderby-expression for DAL - @param groupby: fields to group by (overrides fields!) - @param distinct: select distinct rows - @param virtual: include mandatory virtual fields - @param count: include the total number of matching records - @param getids: include the IDs of all matching records - @param as_rows: return the rows (don't extract) - @param represent: render field value representations - @param raw_data: include raw data in the result - """ - - data = S3ResourceData(self, - fields, - start = start, - limit = limit, - left = left, - orderby = orderby, - groupby = groupby, - distinct = distinct, - virtual = virtual, - count = count, - getids = getids, - as_rows = as_rows, - represent = represent, - show_links = show_links, - raw_data = raw_data, - ) - if as_rows: - return data.rows - else: - return data - - # ------------------------------------------------------------------------- - def insert(self, **fields): - """ - Insert a record into this resource - - @param fields: dict of field/value pairs to insert - """ - - table = self.table - tablename = self.tablename - - # Check permission - authorised = current.auth.s3_has_permission("create", tablename) - if not authorised: - from ..errors import S3PermissionError - raise S3PermissionError("Operation not permitted: INSERT INTO %s" % - tablename) - - # Insert new record - record_id = self.table.insert(**fields) - - # Post-process create - if record_id: - - # Audit - current.audit("create", self.prefix, self.name, record=record_id) - - record = Storage(fields) - record.id = record_id - - # Update super - s3db = current.s3db - s3db.update_super(table, record) - - # Record owner - auth = current.auth - auth.s3_set_record_owner(table, record_id) - auth.s3_make_session_owner(table, record_id) - - # Execute onaccept - s3db.onaccept(tablename, record, method="create") - - return record_id - - # ------------------------------------------------------------------------- - def update(self): - """ - Bulk updater, @todo - """ - - raise NotImplementedError - - # ------------------------------------------------------------------------- - def delete(self, - format = None, - cascade = False, - replaced_by = None, - log_errors = False, - ): - """ - Delete all records in this resource - - @param format: the representation format of the request (optional) - @param cascade: this is a cascade delete (prevents commits) - @param replaced_by: used by record merger - @param log_errors: log errors even when cascade=True - - @return: number of records deleted - - NB skipping undeletable rows is no longer the default behavior, - process will now fail immediately for any error; use S3Delete - directly if skipping of undeletable rows is desired - """ - - from .delete import S3Delete - - delete = S3Delete(self, representation=format) - result = delete(cascade = cascade, - replaced_by = replaced_by, - #skip_undeletable = False, - ) - - if log_errors and cascade: - # Call log_errors explicitly if suppressed by cascade - delete.log_errors() - - return result - - # ------------------------------------------------------------------------- - def approve(self, components=(), approve=True, approved_by=None): - """ - Approve all records in this resource - - @param components: list of component aliases to include, None - for no components, empty list or tuple to - approve all components (default) - @param approve: set to approved (False to reset to unapproved) - @param approved_by: set approver explicitly, a valid auth_user.id - or 0 for approval by system authority - """ - - if "approved_by" not in self.fields: - # No approved_by field => treat as approved by default - return True - - auth = current.auth - if approve: - if approved_by is None: - user = auth.user - if user: - user_id = user.id - else: - return False - else: - user_id = approved_by - else: - # Reset to unapproved - user_id = None - - db = current.db - table = self._table - - # Get all record_ids in the resource - pkey = self._id.name - rows = self.select([pkey], limit=None, as_rows=True) - if not rows: - # No records to approve => exit early - return True - - # Collect record_ids and clear cached permissions - record_ids = set() - add = record_ids.add - forget_permissions = auth.permission.forget - for record in rows: - record_id = record[pkey] - forget_permissions(table, record_id) - add(record_id) - - # Set approved_by for each record in the set - dbset = db(table._id.belongs(record_ids)) - try: - success = dbset.update(approved_by = user_id) - except: - # DB error => raise in debug mode to produce a proper ticket - if current.response.s3.debug: - raise - success = False - if not success: - db.rollback() - return False - - # Invoke onapprove-callback for each updated record - onapprove = self.get_config("onapprove", None) - if onapprove: - rows = dbset.select(limitby=(0, len(record_ids))) - for row in rows: - callback(onapprove, row, tablename=self.tablename) - - # Return early if no components to approve - if components is None: - return True - - # Determine which components to approve - # NB: Components are pre-filtered with the master filter, too - if components: - # FIXME this is probably wrong => should load - # the components which are to be approved - cdict = self.components.exposed - components = [cdict[k] for k in cdict if k in components] - else: - # Approve all currently attached components - # FIXME use exposed.values() - components = self.components.values() - - for component in components: - success = component.approve(components = None, - approve = approve, - approved_by = approved_by, - ) - if not success: - return False - - return True - - # ------------------------------------------------------------------------- - def reject(self, cascade=False): - """ Reject (delete) all records in this resource """ - - db = current.db - s3db = current.s3db - - define_resource = s3db.resource - DELETED = current.xml.DELETED - - INTEGRITY_ERROR = current.ERROR.INTEGRITY_ERROR - tablename = self.tablename - table = self.table - pkey = table._id.name - - # Get hooks configuration - get_config = s3db.get_config - ondelete = get_config(tablename, "ondelete") - onreject = get_config(tablename, "onreject") - ondelete_cascade = get_config(tablename, "ondelete_cascade") - - # Get all rows - if "uuid" in table.fields: - rows = self.select([table._id.name, "uuid"], as_rows=True) - else: - rows = self.select([table._id.name], as_rows=True) - if not rows: - return True - - delete_super = s3db.delete_super - - if DELETED in table: - - references = table._referenced_by - - for row in rows: - - error = self.error - self.error = None - - # On-delete-cascade - if ondelete_cascade: - callback(ondelete_cascade, row, tablename=tablename) - - # Automatic cascade - for ref in references: - tn, fn = ref.tablename, ref.name - rtable = db[tn] - rfield = rtable[fn] - query = (rfield == row[pkey]) - # Ignore RESTRICTs => reject anyway - if rfield.ondelete in ("CASCADE", "RESTRICT"): - rresource = define_resource(tn, filter=query, unapproved=True) - rresource.reject(cascade=True) - if rresource.error: - break - elif rfield.ondelete == "SET NULL": - try: - db(query).update(**{fn:None}) - except: - self.error = INTEGRITY_ERROR - break - elif rfield.ondelete == "SET DEFAULT": - try: - db(query).update(**{fn:rfield.default}) - except: - self.error = INTEGRITY_ERROR - break - - if not self.error and not delete_super(table, row): - self.error = INTEGRITY_ERROR - - if self.error: - db.rollback() - raise RuntimeError("Reject failed for %s.%s" % - (tablename, row[table._id])) - else: - # Pull back prior error status - self.error = error - error = None - - # On-reject hook - if onreject: - callback(onreject, row, tablename=tablename) - - # Park foreign keys - fields = {"deleted": True} - if "deleted_fk" in table: - record = table[row[pkey]] - fk = {} - for f in table.fields: - if record[f] is not None and \ - s3_has_foreign_key(table[f]): - fk[f] = record[f] - fields[f] = None - else: - continue - if fk: - fields.update(deleted_fk=json.dumps(fk)) - - # Update the row, finally - db(table._id == row[pkey]).update(**fields) - - # Clear session - if s3_get_last_record_id(tablename) == row[pkey]: - s3_remove_last_record_id(tablename) - - # On-delete hook - if ondelete: - callback(ondelete, row, tablename=tablename) - - else: - # Hard delete - for row in rows: - - # On-delete-cascade - if ondelete_cascade: - callback(ondelete_cascade, row, tablename=tablename) - - # On-reject - if onreject: - callback(onreject, row, tablename=tablename) - - try: - del table[row[pkey]] - except: - # Row is not deletable - self.error = INTEGRITY_ERROR - db.rollback() - raise - else: - # Clear session - if s3_get_last_record_id(tablename) == row[pkey]: - s3_remove_last_record_id(tablename) - - # Delete super-entity - delete_super(table, row) - - # On-delete - if ondelete: - callback(ondelete, row, tablename=tablename) - - return True - - # ------------------------------------------------------------------------- - def merge(self, - original_id, - duplicate_id, - replace = None, - update = None, - main = True): - """ Merge two records, see also S3RecordMerger.merge """ - - from ..methods import S3RecordMerger - return S3RecordMerger(self).merge(original_id, - duplicate_id, - replace = replace, - update = update, - main = main) - - # ------------------------------------------------------------------------- - # Exports - # ------------------------------------------------------------------------- - def datatable(self, - fields = None, - start = 0, - limit = None, - left = None, - orderby = None, - distinct = False, - ): - """ - Generate a data table of this resource - - @param fields: list of fields to include (field selector strings) - @param start: index of the first record to include - @param limit: maximum number of records to include - @param left: additional left joins for DB query - @param orderby: orderby for DB query - @param distinct: distinct-flag for DB query - - @return: tuple (S3DataTable, numrows), where numrows represents - the total number of rows in the table that match the query - """ - - # Choose fields - if fields is None: - fields = [f.name for f in self.readable_fields()] - selectors = list(fields) - - table = self.table - - # Automatically include the record ID - table_id = table._id - pkey = table_id.name - if pkey not in selectors: - fields.insert(0, pkey) - selectors.insert(0, pkey) - - # Skip representation of IDs in data tables - id_repr = table_id.represent - table_id.represent = None - - # Extract the data - data = self.select(selectors, - start = start, - limit = limit, - orderby = orderby, - left = left, - distinct = distinct, - count = True, - getids = False, - represent = True, - ) - - rows = data.rows - - # Restore ID representation - table_id.represent = id_repr - - # Empty table - or just no match? - empty = False - if not rows: - DELETED = current.xml.DELETED - if DELETED in table: - query = (table[DELETED] == False) - else: - query = (table_id > 0) - row = current.db(query).select(table_id, limitby=(0, 1)).first() - if not row: - empty = True - - # Generate the data table - rfields = data.rfields - dt = S3DataTable(rfields, rows, orderby=orderby, empty=empty) - - return dt, data.numrows - - # ------------------------------------------------------------------------- - def datalist(self, - fields = None, - start = 0, - limit = None, - left = None, - orderby = None, - distinct = False, - list_id = None, - layout = None): - """ - Generate a data list of this resource - - @param fields: list of fields to include (field selector strings) - @param start: index of the first record to include - @param limit: maximum number of records to include - @param left: additional left joins for DB query - @param orderby: orderby for DB query - @param distinct: distinct-flag for DB query - @param list_id: the list identifier - @param layout: custom renderer function (see S3DataList.render) - - @return: tuple (S3DataList, numrows, ids), where numrows represents - the total number of rows in the table that match the query - """ - - # Choose fields - if fields is None: - fields = [f.name for f in self.readable_fields()] - selectors = list(fields) - - table = self.table - - # Automatically include the record ID - pkey = table._id.name - if pkey not in selectors: - fields.insert(0, pkey) - selectors.insert(0, pkey) - - # Extract the data - data = self.select(selectors, - start = start, - limit = limit, - orderby = orderby, - left = left, - distinct = distinct, - count = True, - getids = False, - raw_data = True, - represent = True, - ) - - # Generate the data list - numrows = data.numrows - dl = S3DataList(self, - fields, - data.rows, - list_id = list_id, - start = start, - limit = limit, - total = numrows, - layout = layout, - ) - - return dl, numrows - - # ------------------------------------------------------------------------- - def json(self, - fields=None, - start=0, - limit=None, - left=None, - distinct=False, - orderby=None): - """ - Export a JSON representation of the resource. - - @param fields: list of field selector strings - @param start: index of the first record - @param limit: maximum number of records - @param left: list of (additional) left joins - @param distinct: select only distinct rows - @param orderby: Orderby-expression for the query - - @return: the JSON (as string), representing a list of - dicts with {"tablename.fieldname":"value"} - """ - - data = self.select(fields=fields, - start=start, - limit=limit, - orderby=orderby, - left=left, - distinct=distinct)["rows"] - - return json.dumps(data) - - # ------------------------------------------------------------------------- - # Data Object API - # ------------------------------------------------------------------------- - def load(self, - fields = None, - skip = None, - start = None, - limit = None, - orderby = None, - virtual = True, - cacheable = False): - """ - Loads records from the resource, applying the current filters, - and stores them in the instance. - - @param fields: list of field names to include - @param skip: list of field names to skip - @param start: the index of the first record to load - @param limit: the maximum number of records to load - @param orderby: orderby-expression for the query - @param virtual: whether to load virtual fields or not - @param cacheable: don't define Row actions like update_record - or delete_record (faster, and the record can - be cached) - - @return: the records as list of Rows - """ - - - table = self.table - tablename = self.tablename - - UID = current.xml.UID - load_uids = hasattr(table, UID) - - if not skip: - skip = () - - if fields or skip: - s3 = current.response.s3 - if "all_meta_fields" in s3: - meta_fields = s3.all_meta_fields - else: - meta_fields = s3.all_meta_fields = s3_all_meta_field_names() - s3db = current.s3db - superkeys = s3db.get_super_keys(table) - else: - meta_fields = superkeys = None - - # Field selection - qfields = ([table._id.name, UID]) - append = qfields.append - for f in table.fields: - - if f in ("wkt", "the_geom"): - if tablename == "gis_location": - if f == "the_geom": - # Filter out bulky Polygons - continue - else: - fmt = current.auth.permission.format - if fmt == "cap": - # Include WKT - pass - elif fmt == "xml" and current.deployment_settings.get_gis_xml_wkt(): - # Include WKT - pass - else: - # Filter out bulky Polygons - continue - elif tablename.startswith("gis_layer_shapefile_"): - # Filter out bulky Polygons - continue - - if fields or skip: - - # Must include all meta-fields - if f in meta_fields: - append(f) - continue - - # Must include the fkey if component - if self.parent and not self.link and f == self.fkey: - append(f) - continue - - # Must include all super-keys - if f in superkeys: - append(f) - continue - - if f in skip: - continue - if not fields or f in fields: - qfields.append(f) - - fields = list(set(fn for fn in qfields if hasattr(table, fn))) - - if self._rows is not None: - self.clear() - - pagination = limit is not None or start - - rfilter = self.rfilter - multiple = rfilter.multiple if rfilter is not None else True - if not multiple and self.parent and self.parent.count() == 1: - start = 0 - limit = 1 - - rows = self.select(fields, - start=start, - limit=limit, - orderby=orderby, - virtual=virtual, - as_rows=True) - - ids = self._ids = [] - new_id = ids.append - - self._uids = [] - self._rows = [] - - if rows: - new_uid = self._uids.append - new_row = self._rows.append - pkey = table._id.name - for row in rows: - if hasattr(row, tablename): - _row = ogetattr(row, tablename) - if type(_row) is Row: - row = _row - record_id = ogetattr(row, pkey) - if record_id not in ids: - new_id(record_id) - new_row(row) - if load_uids: - new_uid(ogetattr(row, UID)) - - # If this is an unlimited load, or the first page with no - # rows, then the result length is equal to the total number - # of matching records => store length for subsequent count()s - length = len(self._rows) - if not pagination or not start and not length: - self._length = length - - return self._rows - - # ------------------------------------------------------------------------- - def clear(self): - """ Removes the records currently stored in this instance """ - - self._rows = None - self._rowindex = None - self._length = None - self._ids = None - self._uids = None - self.files = Storage() - - for component in self.components.loaded.values(): - component.clear() - - # ------------------------------------------------------------------------- - def records(self, fields=None): - """ - Get the current set as Rows instance - - @param fields: the fields to include (list of Fields) - """ - - if fields is None: - if self.tablename == "gis_location": - fields = [f for f in self.table - if f.name not in ("wkt", "the_geom")] - else: - fields = [f for f in self.table] - - if self._rows is None: - return Rows(current.db) - else: - colnames = [str(f) for f in fields] - return Rows(current.db, self._rows, colnames=colnames) - - # ------------------------------------------------------------------------- - def __getitem__(self, key): - """ - Find a record currently stored in this instance by its record ID - - @param key: the record ID - @return: a Row - - @raises: IndexError if the record is not currently loaded - """ - - index = self._rowindex - if index is None: - _id = self._id.name - rows = self._rows - if rows: - index = Storage([(str(row[_id]), row) for row in rows]) - else: - index = Storage() - self._rowindex = index - key = str(key) - if key in index: - return index[key] - raise IndexError - - # ------------------------------------------------------------------------- - def __iter__(self): - """ - Iterate over the records currently stored in this instance - """ - - if self._rows is None: - self.load() - rows = self._rows - for i in range(len(rows)): - yield rows[i] - return - - # ------------------------------------------------------------------------- - def get(self, key, component=None, link=None): - """ - Get component records for a record currently stored in this - instance. - - @param key: the record ID - @param component: the name of the component - @param link: the name of the link table - - @return: a Row (if component is None) or a list of rows - """ - - if not key: - raise KeyError("Record not found") - if self._rows is None: - self.load() - try: - master = self[key] - except IndexError: - raise KeyError("Record not found") - - if not component and not link: - return master - elif link: - if link in self.links: - c = self.links[link] - else: - calias = current.s3db.get_alias(self.tablename, link) - if calias: - c = self.components[calias].link - else: - raise AttributeError("Undefined link %s" % link) - else: - try: - c = self.components[component] - except KeyError: - raise AttributeError("Undefined component %s" % component) - - rows = c._rows - if rows is None: - rows = c.load() - if not rows: - return [] - pkey, fkey = c.pkey, c.fkey - if pkey in master: - master_id = master[pkey] - if c.link: - lkey, rkey = c.lkey, c.rkey - lids = [r[rkey] for r in c.link if master_id == r[lkey]] - rows = [record for record in rows if record[fkey] in lids] - else: - try: - rows = [record for record in rows if master_id == record[fkey]] - except AttributeError: - # Most likely need to tweak static/formats/geoson/export.xsl - raise AttributeError("Component %s records are missing fkey %s" % (component, fkey)) - else: - rows = [] - return rows - - # ------------------------------------------------------------------------- - def get_id(self): - """ Get the IDs of all records currently stored in this instance """ - - if self._ids is None: - self.__load_ids() - - if not self._ids: - return None - elif len(self._ids) == 1: - return self._ids[0] - else: - return self._ids - - # ------------------------------------------------------------------------- - def get_uid(self): - """ Get the UUIDs of all records currently stored in this instance """ - - if current.xml.UID not in self.table.fields: - return None - if self._ids is None: - self.__load_ids() - - if not self._uids: - return None - elif len(self._uids) == 1: - return self._uids[0] - else: - return self._uids - - # ------------------------------------------------------------------------- - def __len__(self): - """ - The number of currently loaded rows - """ - - if self._rows is not None: - return len(self._rows) - else: - return 0 - - # ------------------------------------------------------------------------- - def __load_ids(self): - """ Loads the IDs/UIDs of all records matching the current filter """ - - table = self.table - UID = current.xml.UID - - pkey = table._id.name - - if UID in table.fields: - has_uid = True - fields = (pkey, UID) - else: - has_uid = False - fields = (pkey, ) - - rfilter = self.rfilter - multiple = rfilter.multiple if rfilter is not None else True - if not multiple and self.parent and self.parent.count() == 1: - start = 0 - limit = 1 - else: - start = limit = None - - rows = self.select(fields, - start=start, - limit=limit)["rows"] - - if rows: - ID = str(table._id) - self._ids = [row[ID] for row in rows] - if has_uid: - uid = str(table[UID]) - self._uids = [row[uid] for row in rows] - else: - self._ids = [] - - return - - # ------------------------------------------------------------------------- - # Representation - # ------------------------------------------------------------------------- - def __repr__(self): - """ - String representation of this resource - """ - - pkey = self.table._id.name - - if self._rows: - ids = [r[pkey] for r in self] - return "" % (self.tablename, ids) - else: - return "" % self.tablename - - # ------------------------------------------------------------------------- - def __contains__(self, item): - """ - Tests whether this resource contains a (real) field. - - @param item: the field selector or Field instance - """ - - fn = str(item) - if "." in fn: - tn, fn = fn.split(".", 1) - if tn == self.tablename: - item = fn - try: - rf = self.resolve_selector(str(item)) - except (SyntaxError, AttributeError): - return 0 - if rf.field is not None: - return 1 - else: - return 0 - - # ------------------------------------------------------------------------- - def __bool__(self): - """ Boolean test of this resource """ - - return self is not None - - def __nonzero__(self): - """ Python-2.7 backwards-compatibility """ - - return self is not None - - # ------------------------------------------------------------------------- - # XML Export - # ------------------------------------------------------------------------- - def export_xml(self, - start=None, - limit=None, - msince=None, - fields=None, - dereference=True, - maxdepth=MAXDEPTH, - mcomponents=DEFAULT, - rcomponents=None, - references=None, - mdata=False, - stylesheet=None, - as_tree=False, - as_json=False, - maxbounds=False, - filters=None, - pretty_print=False, - location_data=None, - map_data=None, - target=None, - **args): - """ - Export this resource as S3XML - - @param start: index of the first record to export (slicing) - @param limit: maximum number of records to export (slicing) - - @param msince: export only records which have been modified - after this datetime - - @param fields: data fields to include (default: all) - - @param dereference: include referenced resources - @param maxdepth: maximum depth for reference exports - - @param mcomponents: components of the master resource to - include (list of aliases), empty list - for all available components - @param rcomponents: components of referenced resources to - include (list of "tablename:alias") - - @param references: foreign keys to include (default: all) - @param mdata: mobile data export - (=>reduced field set, lookup-only option) - @param stylesheet: path to the XSLT stylesheet (if required) - @param as_tree: return the ElementTree (do not convert into string) - @param as_json: represent the XML tree as JSON - @param maxbounds: include lat/lon boundaries in the top - level element (off by default) - @param filters: additional URL filters (Sync), as dict - {tablename: {url_var: string}} - @param pretty_print: insert newlines/indentation in the output - @param location_data: dictionary of location data which has been - looked-up in bulk ready for xml.gis_encode() - @param map_data: dictionary of options which can be read by the map - @param target: alias of component targetted (or None to target master resource) - @param args: dict of arguments to pass to the XSLT stylesheet - """ - - - xml = current.xml - - output = None - args = Storage(args) - - from ..io import S3XMLFormat - xmlformat = S3XMLFormat(stylesheet) if stylesheet else None - - if mcomponents is DEFAULT: - mcomponents = [] - - # Export as element tree - from ..io import S3ResourceTree - rtree = S3ResourceTree(self, - location_data = location_data, - map_data = map_data, - ) - - tree = rtree.build(start = start, - limit = limit, - msince = msince, - fields = fields, - dereference = dereference, - maxdepth = maxdepth, - mcomponents = mcomponents, - rcomponents = rcomponents, - references = references, - sync_filters = filters, - mdata = mdata, - maxbounds = maxbounds, - xmlformat = xmlformat, - target = target, - ) - - # XSLT transformation - if tree and xmlformat is not None: - import uuid - args.update(domain = xml.domain, - base_url = current.response.s3.base_url, - prefix = self.prefix, - name = self.name, - utcnow = s3_format_datetime(), - msguid = uuid.uuid4().urn, - ) - tree = xmlformat.transform(tree, **args) - - # Convert into the requested format - # NB Content-Type headers are to be set by caller - if tree: - if as_tree: - output = tree - elif as_json: - output = xml.tree2json(tree, pretty_print=pretty_print) - else: - output = xml.tostring(tree, pretty_print=pretty_print) - - return output - - # ------------------------------------------------------------------------- - # XML Import - # ------------------------------------------------------------------------- - def import_xml(self, source, - files = None, - id = None, - format = "xml", - stylesheet = None, - extra_data = None, - ignore_errors = False, - job_id = None, - select_items = None, - commit_job = True, - delete_job = False, - strategy = None, - update_policy = None, - conflict_policy = None, - last_sync = None, - onconflict = None, - **args): - """ - XML Importer - - @param source: the data source, accepts source=xxx, source=[xxx, yyy, zzz] or - source=[(resourcename1, xxx), (resourcename2, yyy)], where the - xxx has to be either an ElementTree or a file-like object - @param files: attached files (None to read in the HTTP request) - @param id: ID (or list of IDs) of the record(s) to update (performs only update) - @param format: type of source = "xml", "json" or "csv" - @param stylesheet: stylesheet to use for transformation - @param extra_data: for CSV imports, dict of extra cols to add to each row - @param ignore_errors: skip invalid records silently - @param job_id: resume from previous import job_id - @param commit_job: commit the job to the database - @param delete_job: delete the import job from the queue - @param strategy: tuple of allowed import methods (create/update/delete) - @param update_policy: policy for updates (sync) - @param conflict_policy: policy for conflict resolution (sync) - @param last_sync: last synchronization datetime (sync) - @param onconflict: callback hook for conflict resolution (sync) - @param args: parameters to pass to the transformation stylesheet - """ - - # Check permission for the resource - has_permission = current.auth.s3_has_permission - authorised = has_permission("create", self.table) and \ - has_permission("update", self.table) - if not authorised: - raise IOError("Insufficient permissions") - - xml = current.xml - tree = None - self.job = None - - if not job_id: - - # Additional stylesheet parameters - args.update(domain = xml.domain, - base_url = current.response.s3.base_url, - prefix = self.prefix, - name = self.name, - utcnow = s3_format_datetime()) - - # Build import tree - if not isinstance(source, (list, tuple)): - source = [source] - for item in source: - if isinstance(item, (list, tuple)): - resourcename, s = item[:2] - else: - resourcename, s = None, item - if isinstance(s, etree._ElementTree): - t = s - elif format == "json": - if isinstance(s, str): - source = StringIO(s) - t = xml.json2tree(source) - else: - t = xml.json2tree(s) - elif format == "csv": - t = xml.csv2tree(s, - resourcename = resourcename, - extra_data = extra_data) - elif format == "xls": - t = xml.xls2tree(s, - resourcename = resourcename, - extra_data = extra_data) - elif format == "xlsx": - t = xml.xlsx2tree(s, - resourcename = resourcename, - extra_data = extra_data) - else: - t = xml.parse(s) - if not t: - if xml.error: - raise SyntaxError(xml.error) - else: - raise SyntaxError("Invalid source") - - if stylesheet is not None: - t = xml.transform(t, stylesheet, **args) - if not t: - raise SyntaxError(xml.error) - # Use this to debug the source tree if needed: - #if s.name[-16:] == "organisation.csv": - #sys.stderr.write(xml.tostring(t, pretty_print=True).decode("utf-8")) - - if not tree: - tree = t.getroot() - else: - tree.extend(list(t.getroot())) - - if files is not None and isinstance(files, dict): - self.files = Storage(files) - - else: - # job ID given - pass - - response = current.response - # Flag to let onvalidation/onaccept know this is coming from a Bulk Import - response.s3.bulk = True - success = self.import_tree(id, tree, - ignore_errors = ignore_errors, - job_id = job_id, - select_items = select_items if job_id else None, - commit_job = commit_job, - delete_job = delete_job, - strategy = strategy, - update_policy = update_policy, - conflict_policy = conflict_policy, - last_sync = last_sync, - onconflict = onconflict) - response.s3.bulk = False - - self.files = Storage() - - # Response message - if format == "json": - # Whilst all Responses are JSON, it's easier to debug by having the - # response appear in the browser than launching a text editor - response.headers["Content-Type"] = "application/json" - if self.error_tree is not None: - tree = xml.tree2json(self.error_tree) - else: - tree = None - - # Import Summary Info - import_info = {"records": self.import_count} - created = list(set(self.import_created)) - if created: - import_info["created"] = created - updated = list(set(self.import_updated)) - if updated: - import_info["updated"] = updated - deleted = list(set(self.import_deleted)) - if deleted: - import_info["deleted"] = deleted - - if success is True: - # 2nd phase of 2-phase import - # Execute postimport if-defined - postimport = self.get_config("postimport") - if postimport: - #try: - callback(postimport, import_info, tablename=self.tablename) - #except: - # error = "postimport failed: %s" % postimport - # current.log.error(error) - # raise RuntimeError - - return xml.json_message(message=self.error, tree=tree, - **import_info) - - elif success and hasattr(success, "job_id"): - # 1st phase of 2-phase import - # NB import_info is meaningless here as IDs have been rolled-back - self.job = success - return xml.json_message(message=self.error, tree=tree, - **import_info) - - # Failure - return xml.json_message(False, 400, - message=self.error, tree=tree) - - # ------------------------------------------------------------------------- - def import_tree(self, record_id, tree, - job_id = None, - select_items = None, - ignore_errors = False, - delete_job = False, - commit_job = True, - strategy = None, - update_policy = None, - conflict_policy = None, - last_sync = None, - onconflict = None): - """ - Import data from an S3XML element tree. - - @param record_id: record ID or list of record IDs to update - @param tree: the element tree - @param ignore_errors: continue at errors (=skip invalid elements) - - @param job_id: restore a job from the job table (ID or UID) - @param delete_job: delete the import job from the job table - @param commit_job: commit the job (default) - - @todo: update for link table support - """ - - from ..io import S3ImportJob - - db = current.db - tablename = self.tablename - - if job_id is not None: - - # Restore a job from the job table - self.error = None - self.error_tree = None - try: - import_job = S3ImportJob(self.table, - job_id = job_id, - strategy = strategy, - update_policy = update_policy, - conflict_policy = conflict_policy, - last_sync = last_sync, - onconflict = onconflict) - except: - self.error = current.ERROR.BAD_SOURCE - return False - - # Delete the job? - if delete_job: - import_job.delete() - return True - - # Load items - job_id = import_job.job_id - item_table = import_job.item_table - - query = (item_table.job_id == job_id) - if select_items: - # Limit to selected items for the resource table - query &= (item_table.tablename != self.tablename) | \ - (item_table.id.belongs(select_items)) - items = db(query).select() - - load_item = import_job.load_item - for item in items: - success = load_item(item) - if not success: - self.error = import_job.error - self.error_tree = import_job.error_tree - import_job.restore_references() - - if commit_job: - if self.error and not ignore_errors: - return False - else: - return import_job - - # Call the import pre-processor to prepare tables - # and cleanup the tree as necessary - # NB For 2-phase imports this gets called twice! - # can't use commit_job to differentiate since we need it to run on the trial import - import_prep = current.response.s3.import_prep - if import_prep: - tree = import_job.get_tree() - callback(import_prep, - # takes tuple (resource, tree) as argument - (self, tree), - tablename=tablename) - # Skip import? - if self.skip_import: - current.log.debug("Skipping import to %s" % tablename) - self.skip_import = False - return True - - else: - # Create a new job from an element tree - # Do not import into tables without "id" field - table = self.table - if "id" not in table.fields: - self.error = current.ERROR.BAD_RESOURCE - return False - - xml = current.xml - - # Reset error and error tree - self.error = None - self.error_tree = None - - # Call the import pre-processor to prepare tables - # and cleanup the tree as necessary - # NB For 2-phase imports this gets called twice! - # can't use commit_job to differentiate since we need it to run on the trial import - import_prep = current.response.s3.import_prep - if import_prep: - if not isinstance(tree, etree._ElementTree): - tree = etree.ElementTree(tree) - callback(import_prep, - # takes tuple (resource, tree) as argument - (self, tree), - tablename=tablename) - # Skip import? - if self.skip_import: - current.log.debug("Skipping import to %s" % tablename) - self.skip_import = False - return True - - # Select the elements for this table - elements = xml.select_resources(tree, tablename) - if not elements: - # nothing to import => still ok - return True - - # Find matching elements, if a target record ID is given - UID = xml.UID - if record_id and UID in table: - if not isinstance(record_id, (tuple, list)): - query = (table._id == record_id) - else: - query = (table._id.belongs(record_id)) - originals = db(query).select(table[UID]) - uids = [row[UID] for row in originals] - matches = [] - import_uid = xml.import_uid - append = matches.append - for element in elements: - element_uid = import_uid(element.get(UID, None)) - if not element_uid: - continue - if element_uid in uids: - append(element) - if not matches: - first = elements[0] - if len(elements) and not first.get(UID, None): - first.set(UID, uids[0]) - matches = [first] - if not matches: - self.error = current.ERROR.NO_MATCH - return False - else: - elements = matches - - # Import all matching elements - import_job = S3ImportJob(table, - tree = tree, - files = self.files, - strategy = strategy, - update_policy = update_policy, - conflict_policy = conflict_policy, - last_sync = last_sync, - onconflict = onconflict) - add_item = import_job.add_item - exposed_aliases = self.components.exposed_aliases - for element in elements: - success = add_item(element = element, - components = exposed_aliases, - ) - if not success: - self.error = import_job.error - self.error_tree = import_job.error_tree - if self.error and not ignore_errors: - return False - - # Commit the import job - auth = current.auth - auth.rollback = not commit_job - success = import_job.commit(ignore_errors=ignore_errors, - log_items = self.get_config("oncommit_import_item")) - auth.rollback = False - self.error = import_job.error - self.import_count += import_job.count - self.import_errors += import_job.errors - self.import_created += import_job.created - self.import_updated += import_job.updated - self.import_deleted += import_job.deleted - job_mtime = import_job.mtime - if self.mtime is None or \ - job_mtime and job_mtime > self.mtime: - self.mtime = job_mtime - if self.error: - if ignore_errors: - self.error = "%s - invalid items ignored" % self.error - self.error_tree = import_job.error_tree - elif not success: - # Oops - how could this happen? We can have an error - # without failure, but not a failure without error! - # If we ever get here, then there's a bug without a - # chance to recover - hence let it crash: - raise RuntimeError("Import failed without error message") - if not success or not commit_job: - db.rollback() - if not commit_job: - import_job.store() - return import_job - else: - # Remove the job when committed - if job_id is not None: - import_job.delete() - - return self.error is None or ignore_errors - - # ------------------------------------------------------------------------- - # XML introspection - # ------------------------------------------------------------------------- - def export_options(self, - component = None, - fields = None, - only_last = False, - show_uids = False, - hierarchy = False, - as_json = False): - """ - Export field options of this resource as element tree - - @param component: name of the component which the options are - requested of, None for the primary table - @param fields: list of names of fields for which the options - are requested, None for all fields (which have - options) - @param as_json: convert the output into JSON - @param only_last: obtain only the latest record - """ - - if component is not None: - c = self.components.get(component) - if c: - tree = c.export_options(fields = fields, - only_last = only_last, - show_uids = show_uids, - hierarchy = hierarchy, - as_json = as_json) - return tree - else: - # If we get here, we've been called from the back-end, - # otherwise the request would have failed during parse. - # So it's safe to raise an exception: - raise AttributeError - else: - if as_json and only_last and len(fields) == 1: - # Identify the field - default = {"option":[]} - try: - field = self.table[fields[0]] - except AttributeError: - # Can't raise an exception here as this goes - # directly to the client - return json.dumps(default) - - # Check that the validator has a lookup table - requires = field.requires - if not isinstance(requires, (list, tuple)): - requires = [requires] - requires = requires[0] - if isinstance(requires, IS_EMPTY_OR): - requires = requires.other - from ..tools import IS_LOCATION - if not isinstance(requires, (IS_ONE_OF, IS_LOCATION)): - # Can't raise an exception here as this goes - # directly to the client - return json.dumps(default) - - # Identify the lookup table - db = current.db - lookuptable = requires.ktable - lookupfield = db[lookuptable][requires.kfield] - - # Fields to extract - fields = [lookupfield] - h = None - if hierarchy: - from ..tools import S3Hierarchy - h = S3Hierarchy(lookuptable) - if not h.config: - h = None - elif h.pkey.name != lookupfield.name: - # Also extract the node key for the hierarchy - fields.append(h.pkey) - - # Get the latest record - # NB: this assumes that the lookupfield is auto-incremented - row = db().select(orderby = ~lookupfield, - limitby = (0, 1), - *fields).first() - - # Represent the value and generate the output JSON - if row: - value = row[lookupfield] - widget = field.widget - if hasattr(widget, "represent") and widget.represent: - # Prefer the widget's represent as options.json - # is usually called to Ajax-update the widget - represent = widget.represent(value) - elif field.represent: - represent = field.represent(value) - else: - represent = s3_str(value) - if isinstance(represent, A): - represent = represent.components[0] - - item = {"@value": value, "$": represent} - if h: - parent = h.parent(row[h.pkey]) - if parent: - item["@parent"] = str(parent) - result = [item] - else: - result = [] - return json.dumps({'option': result}) - - xml = current.xml - tree = xml.get_options(self.table, - fields = fields, - show_uids = show_uids, - hierarchy = hierarchy) - - if as_json: - return xml.tree2json(tree, pretty_print=False, - native=True) - else: - return xml.tostring(tree, pretty_print=False) - - # ------------------------------------------------------------------------- - def export_fields(self, component=None, as_json=False): - """ - Export a list of fields in the resource as element tree - - @param component: name of the component to lookup the fields - (None for primary table) - @param as_json: convert the output XML into JSON - """ - - if component is not None: - try: - c = self.components[component] - except KeyError: - raise AttributeError("Undefined component %s" % component) - return c.export_fields(as_json=as_json) - else: - xml = current.xml - tree = xml.get_fields(self.prefix, self.name) - if as_json: - return xml.tree2json(tree, pretty_print=True) - else: - return xml.tostring(tree, pretty_print=True) - - # ------------------------------------------------------------------------- - def export_struct(self, - meta = False, - options = False, - references = False, - stylesheet = None, - as_json = False, - as_tree = False): - """ - Get the structure of the resource - - @param options: include option lists in option fields - @param references: include option lists even for reference fields - @param stylesheet: the stylesheet to use for transformation - @param as_json: convert into JSON after transformation - """ - - xml = current.xml - - # Get the structure of the main resource - root = etree.Element(xml.TAG.root) - main = xml.get_struct(self.prefix, self.name, - alias = self.alias, - parent = root, - meta = meta, - options = options, - references = references, - ) - - # Include the exposed components - for component in self.components.exposed.values(): - prefix = component.prefix - name = component.name - xml.get_struct(prefix, name, - alias = component.alias, - parent = main, - meta = meta, - options = options, - references = references, - ) - - # Transformation - tree = etree.ElementTree(root) - if stylesheet is not None: - args = {"domain": xml.domain, - "base_url": current.response.s3.base_url, - "prefix": self.prefix, - "name": self.name, - "utcnow": s3_format_datetime(), - } - - tree = xml.transform(tree, stylesheet, **args) - if tree is None: - return None - - # Return tree if requested - if as_tree: - return tree - - # Otherwise string-ify it - if as_json: - return xml.tree2json(tree, pretty_print=True) - else: - return xml.tostring(tree, pretty_print=True) - - # ------------------------------------------------------------------------- - # Data Model Helpers - # ------------------------------------------------------------------------- - @classmethod - def original(cls, table, record, mandatory=None): - """ - Find the original record for a possible duplicate: - - if the record contains a UUID, then only that UUID is used - to match the record with an existing DB record - - otherwise, if the record contains some values for unique - fields, all of them must match the same existing DB record - - @param table: the table - @param record: the record as dict or S3XML Element - """ - - db = current.db - xml = current.xml - xml_decode = xml.xml_decode - - VALUE = xml.ATTRIBUTE["value"] - UID = xml.UID - ATTRIBUTES_TO_FIELDS = xml.ATTRIBUTES_TO_FIELDS - - # Get primary keys - pkeys = [f for f in table.fields if table[f].unique] - pvalues = Storage() - - # Get the values from record - get = record.get - if type(record) is etree._Element: #isinstance(record, etree._Element): - xpath = record.xpath - xexpr = "%s[@%s='%%s']" % (xml.TAG["data"], - xml.ATTRIBUTE["field"]) - for f in pkeys: - v = None - if f == UID or f in ATTRIBUTES_TO_FIELDS: - v = get(f, None) - else: - child = xpath(xexpr % f) - if child: - child = child[0] - v = child.get(VALUE, xml_decode(child.text)) - if v: - pvalues[f] = v - elif isinstance(record, dict): - for f in pkeys: - v = get(f, None) - if v: - pvalues[f] = v - else: - raise TypeError - - # Build match query - query = None - for f in pvalues: - if f == UID: - continue - _query = (table[f] == pvalues[f]) - if query is not None: - query = query | _query - else: - query = _query - - fields = cls.import_fields(table, pvalues, mandatory=mandatory) - - # Try to find exactly one match by non-UID unique keys - if query is not None: - original = db(query).select(limitby=(0, 2), *fields) - if len(original) == 1: - return original.first() - - # If no match, then try to find a UID-match - if UID in pvalues: - uid = xml.import_uid(pvalues[UID]) - query = (table[UID] == uid) - original = db(query).select(limitby=(0, 1), *fields).first() - if original: - return original - - # No match or multiple matches - return None - - # ------------------------------------------------------------------------- - @staticmethod - def import_fields(table, data, mandatory=None): - - fnames = set(s3_all_meta_field_names()) - fnames.add(table._id.name) - if mandatory: - fnames |= set(mandatory) - for fn in data: - fnames.add(fn) - return [table[fn] for fn in fnames if fn in table.fields] - - # ------------------------------------------------------------------------- - def readable_fields(self, subset=None): - """ - Get a list of all readable fields in the resource table - - @param subset: list of fieldnames to limit the selection to - """ - - fkey = None - table = self.table - - parent = self.parent - linked = self.linked - - if parent and linked is None: - component = parent.components.get(self.alias) - if component: - fkey = component.fkey - elif linked is not None: - component = linked - if component: - fkey = component.lkey - - if subset: - return [ogetattr(table, f) for f in subset - if f in table.fields and \ - ogetattr(table, f).readable and f != fkey] - else: - return [ogetattr(table, f) for f in table.fields - if ogetattr(table, f).readable and f != fkey] - - # ------------------------------------------------------------------------- - def resolve_selectors(self, selectors, - skip_components=False, - extra_fields=True, - show=True): - """ - Resolve a list of field selectors against this resource - - @param selectors: the field selectors - @param skip_components: skip fields in components - @param extra_fields: automatically add extra_fields of all virtual - fields in this table - @param show: default for S3ResourceField.show - - @return: tuple of (fields, joins, left, distinct) - """ - - prefix = lambda s: "~.%s" % s \ - if "." not in s.split("$", 1)[0] else s - - display_fields = set() - add = display_fields.add - - # Store field selectors - for item in selectors: - if not item: - continue - elif type(item) is tuple: - item = item[-1] - if isinstance(item, str): - selector = item - elif isinstance(item, S3ResourceField): - selector = item.selector - elif isinstance(item, FS): - selector = item.name - else: - continue - add(prefix(selector)) - - slist = list(selectors) - - # Collect extra fields from virtual tables - if extra_fields: - extra = self.get_config("extra_fields") - if extra: - append = slist.append - for selector in extra: - s = prefix(selector) - if s not in display_fields: - append(s) - - joins = {} - left = {} - - distinct = False - - columns = set() - add_column = columns.add - - rfields = [] - append = rfields.append - - for s in slist: - - # Allow to override the field label - if type(s) is tuple: - label, selector = s - else: - label, selector = None, s - - # Resolve the selector - if isinstance(selector, str): - selector = prefix(selector) - try: - rfield = S3ResourceField(self, selector, label=label) - except (AttributeError, SyntaxError): - continue - elif isinstance(selector, FS): - try: - rfield = selector.resolve(self) - except (AttributeError, SyntaxError): - continue - elif isinstance(selector, S3ResourceField): - rfield = selector - else: - continue - - # Unresolvable selector? - if rfield.field is None and not rfield.virtual: - continue - - # De-duplicate columns - colname = rfield.colname - if colname in columns: - continue - else: - add_column(colname) - - # Replace default label - if label is not None: - rfield.label = label - - # Skip components - if skip_components: - head = rfield.selector.split("$", 1)[0] - if "." in head and head.split(".")[0] not in ("~", self.alias): - continue - - # Resolve the joins - if rfield.distinct: - left.update(rfield._joins) - distinct = True - elif rfield.join: - joins.update(rfield._joins) - - rfield.show = show and rfield.selector in display_fields - append(rfield) - - return (rfields, joins, left, distinct) - - # ------------------------------------------------------------------------- - def resolve_selector(self, selector): - """ - Wrapper for S3ResourceField, retained for backward compatibility - """ - - return S3ResourceField(self, selector) - - # ------------------------------------------------------------------------- - def split_fields(self, skip=DEFAULT, data=None, references=None): - """ - Split the readable fields in the resource table into - reference and non-reference fields. - - @param skip: list of field names to skip - @param data: data fields to include (None for all) - @param references: foreign key fields to include (None for all) - """ - - if skip is DEFAULT: - skip = [] - - rfields = self.rfields - dfields = self.dfields - - if rfields is None or dfields is None: - if self.tablename == "gis_location": - settings = current.deployment_settings - if "wkt" not in skip: - fmt = current.auth.permission.format - if fmt == "cap": - # Include WKT - pass - elif fmt == "xml" and settings.get_gis_xml_wkt(): - # Include WKT - pass - else: - # Skip bulky WKT fields - skip.append("wkt") - if "the_geom" not in skip and settings.get_gis_spatialdb(): - skip.append("the_geom") - - xml = current.xml - UID = xml.UID - IGNORE_FIELDS = xml.IGNORE_FIELDS - FIELDS_TO_ATTRIBUTES = xml.FIELDS_TO_ATTRIBUTES - - show_ids = current.xml.show_ids - rfields = [] - dfields = [] - table = self.table - pkey = table._id.name - for f in table.fields: - - if f == UID or f in skip or f in IGNORE_FIELDS: - # Skip (show_ids=True overrides this for pkey) - if f != pkey or not show_ids: - continue - - # Meta-field? => always include (in dfields) - meta = f in FIELDS_TO_ATTRIBUTES - - if s3_has_foreign_key(table[f]) and not meta: - # Foreign key => add to rfields unless excluded - if references is None or f in references: - rfields.append(f) - - elif data is None or f in data or meta: - # Data field => add to dfields - dfields.append(f) - - self.rfields = rfields - self.dfields = dfields - - return (rfields, dfields) - - # ------------------------------------------------------------------------- - # Utility functions - # ------------------------------------------------------------------------- - def configure(self, **settings): - """ - Update configuration settings for this resource - - @param settings: configuration settings for this resource - as keyword arguments - """ - - current.s3db.configure(self.tablename, **settings) - - # ------------------------------------------------------------------------- - def get_config(self, key, default=None): - """ - Get a configuration setting for the current resource - - @param key: the setting key - @param default: the default value to return if the setting - is not configured for this resource - """ - - return current.s3db.get_config(self.tablename, key, default=default) - - # ------------------------------------------------------------------------- - def clear_config(self, *keys): - """ - Clear configuration settings for this resource - - @param keys: keys to remove (can be multiple) - - @note: no keys specified removes all settings for this resource - """ - - current.s3db.clear_config(self.tablename, *keys) - - # ------------------------------------------------------------------------- - @staticmethod - def limitby(start=0, limit=0): - """ - Convert start+limit parameters into a limitby tuple - - limit without start => start = 0 - - start without limit => limit = ROWSPERPAGE - - limit 0 (or less) => limit = 1 - - start less than 0 => start = 0 - - @param start: index of the first record to select - @param limit: maximum number of records to select - """ - - if limit is None: - return None - - if start is None: - start = 0 - if limit == 0: - limit = current.response.s3.ROWSPERPAGE - - if limit <= 0: - limit = 1 - if start < 0: - start = 0 - - return (start, start + limit) - - # ------------------------------------------------------------------------- - def _join(self, implicit=False, reverse=False): - """ - Get a join for this component - - @param implicit: return a subquery with an implicit join rather - than an explicit join - @param reverse: get the reverse join (joining master to component) - - @return: a Query if implicit=True, otherwise a list of joins - """ - - if self.parent is None: - # This isn't a component - return None - else: - ltable = self.parent.table - - rtable = self.table - pkey = self.pkey - fkey = self.fkey - - DELETED = current.xml.DELETED - - if self.linked: - return self.linked._join(implicit=implicit, reverse=reverse) - - elif self.linktable: - linktable = self.linktable - lkey = self.lkey - rkey = self.rkey - lquery = (ltable[pkey] == linktable[lkey]) - if DELETED in linktable: - lquery &= (linktable[DELETED] == False) - if self.filter is not None and not reverse: - rquery = (linktable[rkey] == rtable[fkey]) & self.filter - else: - rquery = (linktable[rkey] == rtable[fkey]) - if reverse: - join = [linktable.on(rquery), ltable.on(lquery)] - else: - join = [linktable.on(lquery), rtable.on(rquery)] - - else: - lquery = (ltable[pkey] == rtable[fkey]) - if DELETED in rtable and not reverse: - lquery &= (rtable[DELETED] == False) - if self.filter is not None: - lquery &= self.filter - if reverse: - join = [ltable.on(lquery)] - else: - join = [rtable.on(lquery)] - - if implicit: - query = None - for expression in join: - if query is None: - query = expression.second - else: - query &= expression.second - return query - else: - return join - - # ------------------------------------------------------------------------- - def get_join(self): - """ Get join for this component """ - - return self._join(implicit=True) - - # ------------------------------------------------------------------------- - def get_left_join(self): - """ Get a left join for this component """ - - return self._join() - - # ------------------------------------------------------------------------- - def link_id(self, master_id, component_id): - """ - Helper method to find the link table entry ID for - a pair of linked records. - - @param master_id: the ID of the master record - @param component_id: the ID of the component record - """ - - if self.parent is None or self.linked is None: - return None - - join = self.get_join() - ltable = self.table - mtable = self.parent.table - ctable = self.linked.table - query = join & \ - (mtable._id == master_id) & \ - (ctable._id == component_id) - row = current.db(query).select(ltable._id, limitby=(0, 1)).first() - if row: - return row[ltable._id.name] - else: - return None - - # ------------------------------------------------------------------------- - def component_id(self, master_id, link_id): - """ - Helper method to find the component record ID for - a particular link of a particular master record - - @param link: the link (S3Resource) - @param master_id: the ID of the master record - @param link_id: the ID of the link table entry - """ - - if self.parent is None or self.linked is None: - return None - - join = self.get_join() - ltable = self.table - mtable = self.parent.table - ctable = self.linked.table - query = join & (ltable._id == link_id) - if master_id is not None: - # master ID is redundant, but can be used to check negatives - query &= (mtable._id == master_id) - row = current.db(query).select(ctable._id, limitby=(0, 1)).first() - if row: - return row[ctable._id.name] - else: - return None - - # ------------------------------------------------------------------------- - def update_link(self, master, record): - """ - Create a new link in a link table if it doesn't yet exist. - This function is meant to also update links in "embed" - actuation mode once this gets implemented, therefore the - method name "update_link". - - @param master: the master record - @param record: the new component record to be linked - """ - - if self.parent is None or self.linked is None: - return None - - # Find the keys - resource = self.linked - pkey = resource.pkey - lkey = resource.lkey - rkey = resource.rkey - fkey = resource.fkey - if pkey not in master: - return None - _lkey = master[pkey] - if fkey not in record: - return None - _rkey = record[fkey] - if not _lkey or not _rkey: - return None - - ltable = self.table - ltn = ltable._tablename - - # Create the link if it does not already exist - query = ((ltable[lkey] == _lkey) & - (ltable[rkey] == _rkey)) - row = current.db(query).select(ltable._id, limitby=(0, 1)).first() - if not row: - s3db = current.s3db - onaccept = s3db.get_config(ltn, "create_onaccept") - if onaccept is None: - onaccept = s3db.get_config(ltn, "onaccept") - data = {lkey:_lkey, rkey:_rkey} - link_id = ltable.insert(**data) - data[ltable._id.name] = link_id - s3db.update_super(ltable, data) - current.auth.s3_set_record_owner(ltable, data) - if link_id and onaccept: - callback(onaccept, Storage(vars=Storage(data))) - else: - link_id = row[ltable._id.name] - return link_id - - # ------------------------------------------------------------------------- - def datatable_filter(self, fields, get_vars): - """ - Parse datatable search/sort vars into a tuple of - query, orderby and left joins - - @param fields: list of field selectors representing - the order of fields in the datatable (list_fields) - @param get_vars: the datatable GET vars - - @return: tuple of (query, orderby, left joins) - """ - - db = current.db - get_aliased = current.s3db.get_aliased - - left_joins = S3Joins(self.tablename) - - sSearch = "sSearch" - iColumns = "iColumns" - iSortingCols = "iSortingCols" - - parent = self.parent - fkey = self.fkey - - # Skip joins for linked tables - if self.linked is not None: - skip = self.linked.tablename - else: - skip = None - - # Resolve the list fields - rfields = self.resolve_selectors(fields)[0] - - # FILTER -------------------------------------------------------------- - - searchq = None - if sSearch in get_vars and iColumns in get_vars: - - # Build filter - text = get_vars[sSearch] - words = [w for w in text.lower().split()] - - if words: - try: - numcols = int(get_vars[iColumns]) - except ValueError: - numcols = 0 - - flist = [] - for i in range(numcols): - try: - rfield = rfields[i] - field = rfield.field - except (KeyError, IndexError): - continue - if field is None: - # Virtual - if hasattr(rfield, "search_field"): - field = db[rfield.tname][rfield.search_field] - else: - # Cannot search - continue - ftype = str(field.type) - - # Add left joins - left_joins.extend(rfield.left) - - if ftype[:9] == "reference" and \ - hasattr(field, "sortby") and field.sortby: - # For foreign keys, we search through their sortby - - # Get the lookup table - tn = ftype[10:] - if parent is not None and \ - parent.tablename == tn and field.name != fkey: - alias = "%s_%s_%s" % (parent.prefix, - "linked", - parent.name) - ktable = get_aliased(db[tn], alias) - ktable._id = ktable[ktable._id.name] - tn = alias - elif tn == field.tablename: - prefix, name = field.tablename.split("_", 1) - alias = "%s_%s_%s" % (prefix, field.name, name) - ktable = get_aliased(db[tn], alias) - ktable._id = ktable[ktable._id.name] - tn = alias - else: - ktable = db[tn] - - # Add left join for lookup table - if tn != skip: - left_joins.add(ktable.on(field == ktable._id)) - - if isinstance(field.sortby, (list, tuple)): - flist.extend([ktable[f] for f in field.sortby - if f in ktable.fields]) - else: - if field.sortby in ktable.fields: - flist.append(ktable[field.sortby]) - - else: - # Otherwise, we search through the field itself - flist.append(field) - - # Build search query - # @todo: migrate this to S3ResourceQuery? - opts = Storage() - queries = [] - for w in words: - - wqueries = [] - for field in flist: - ftype = str(field.type) - options = None - fname = str(field) - if fname in opts: - options = opts[fname] - elif ftype[:7] in ("integer", - "list:in", - "list:st", - "referen", - "list:re", - "string"): - requires = field.requires - if not isinstance(requires, (list, tuple)): - requires = [requires] - if requires: - r = requires[0] - if isinstance(r, IS_EMPTY_OR): - r = r.other - if hasattr(r, "options"): - try: - options = r.options() - except: - options = [] - if options is None and ftype in ("string", "text"): - wqueries.append(field.lower().like("%%%s%%" % w)) - elif options is not None: - opts[fname] = options - vlist = [v for v, t in options - if s3_str(t).lower().find(s3_str(w)) != -1] - if vlist: - wqueries.append(field.belongs(vlist)) - if len(wqueries): - queries.append(reduce(lambda x, y: x | y \ - if x is not None else y, - wqueries)) - if len(queries): - searchq = reduce(lambda x, y: x & y \ - if x is not None else y, queries) - - # ORDERBY ------------------------------------------------------------- - - orderby = [] - if iSortingCols in get_vars: - - # Sorting direction - def direction(i): - sort_dir = get_vars["sSortDir_%s" % str(i)] - return " %s" % sort_dir if sort_dir else "" - - # Get the fields to order by - try: - numcols = int(get_vars[iSortingCols]) - except: - numcols = 0 - - columns = [] - pkey = str(self._id) - for i in range(numcols): - try: - iSortCol = int(get_vars["iSortCol_%s" % i]) - except (AttributeError, KeyError): - # iSortCol_x not present in get_vars => ignore - columns.append(Storage(field=None)) - continue - - # Map sortable-column index to the real list_fields - # index: for every non-id non-sortable column to the - # left of sortable column subtract 1 - for j in range(iSortCol): - if get_vars.get("bSortable_%s" % j, "true") == "false": - try: - if rfields[j].colname != pkey: - iSortCol -= 1 - except KeyError: - break - - try: - rfield = rfields[iSortCol] - except IndexError: - # iSortCol specifies a non-existent column, i.e. - # iSortCol_x>=numcols => ignore - columns.append(Storage(field=None)) - else: - columns.append(rfield) - - # Process the orderby-fields - for i in range(len(columns)): - rfield = columns[i] - field = rfield.field - if field is None: - continue - ftype = str(field.type) - - represent = field.represent - if ftype == "json": - # Can't sort by JSON fields - # => try using corresponding id column to maintain some - # fake yet consistent sort order: - tn = field.tablename - try: - orderby.append("%s%s" % (db[tn]._id, direction(i))) - except AttributeError: - continue - elif not hasattr(represent, "skip_dt_orderby") and \ - hasattr(represent, "dt_orderby"): - # Custom orderby logic in field.represent - field.represent.dt_orderby(field, - direction(i), - orderby, - left_joins) - - elif ftype[:9] == "reference" and \ - hasattr(field, "sortby") and field.sortby: - # Foreign keys with sortby will be sorted by sortby - - # Get the lookup table - tn = ftype[10:] - if parent is not None and \ - parent.tablename == tn and field.name != fkey: - alias = "%s_%s_%s" % (parent.prefix, "linked", parent.name) - ktable = get_aliased(db[tn], alias) - ktable._id = ktable[ktable._id.name] - tn = alias - elif tn == field.tablename: - prefix, name = field.tablename.split("_", 1) - alias = "%s_%s_%s" % (prefix, field.name, name) - ktable = get_aliased(db[tn], alias) - ktable._id = ktable[ktable._id.name] - tn = alias - else: - ktable = db[tn] - - # Add left joins for lookup table - if tn != skip: - left_joins.extend(rfield.left) - left_joins.add(ktable.on(field == ktable._id)) - - # Construct orderby from sortby - if not isinstance(field.sortby, (list, tuple)): - orderby.append("%s.%s%s" % (tn, field.sortby, direction(i))) - else: - orderby.append(", ".join(["%s.%s%s" % - (tn, fn, direction(i)) - for fn in field.sortby])) - - else: - # Otherwise, we sort by the field itself - orderby.append("%s%s" % (field, direction(i))) - - if orderby: - orderby = ", ".join(orderby) - else: - orderby = None - - left_joins = left_joins.as_list(tablenames=list(left_joins.joins.keys())) - return (searchq, orderby, left_joins) - - # ------------------------------------------------------------------------- - def axisfilter(self, axes): - """ - Get all values for the given S3ResourceFields (axes) which - match the resource query, used in pivot tables to filter out - additional values where dimensions can have multiple values - per record - - @param axes: the axis fields as list/tuple of S3ResourceFields - - @return: a dict with values per axis, only containes those - axes which are affected by the resource filter - """ - - axisfilter = {} - - qdict = self.get_query().as_dict(flat=True) - - for rfield in axes: - field = rfield.field - - if field is None: - # virtual field or unresolvable selector - continue - - left_joins = S3Joins(self.tablename) - left_joins.extend(rfield.left) - - tablenames = list(left_joins.joins.keys()) - tablenames.append(self.tablename) - af = S3AxisFilter(qdict, tablenames) - - if af.op is not None: - query = af.query() - left = left_joins.as_list() - - # @todo: this does not work with virtual fields: need - # to retrieve all extra_fields for the dimension table - # and can't groupby (=must deduplicate afterwards) - rows = current.db(query).select(field, - left=left, - groupby=field) - colname = rfield.colname - if rfield.ftype[:5] == "list:": - values = [] - vappend = values.append - for row in rows: - v = row[colname] - vappend(v if v else [None]) - values = set(chain.from_iterable(values)) - - include, exclude = af.values(rfield) - fdict = {} - if include: - for v in values: - vstr = s3_str(v) if v is not None else v - if vstr in include and vstr not in exclude: - fdict[v] = None - else: - fdict = dict((v, None) for v in values) - - axisfilter[colname] = fdict - - else: - axisfilter[colname] = dict((row[colname], None) - for row in rows) - - return axisfilter - - # ------------------------------------------------------------------------- - def prefix_selector(self, selector): - """ - Helper method to ensure consistent prefixing of field selectors - - @param selector: the selector - """ - - head = selector.split("$", 1)[0] - if "." in head: - prefix = head.split(".", 1)[0] - if prefix == self.alias: - return selector.replace("%s." % prefix, "~.") - else: - return selector - else: - return "~.%s" % selector - - # ------------------------------------------------------------------------- - def list_fields(self, key="list_fields", id_column=0): - """ - Get the list_fields for this resource - - @param key: alternative key for the table configuration - @param id_column: - False to exclude the record ID - - True to include it if it is configured - - 0 to make it the first column regardless - whether it is configured or not - """ - - list_fields = self.get_config(key, None) - - if not list_fields and key != "list_fields": - list_fields = self.get_config("list_fields", None) - if not list_fields: - list_fields = [f.name for f in self.readable_fields()] - - id_field = pkey = self._id.name - - # Do not include the parent key for components - if self.parent and not self.link and \ - not current.response.s3.component_show_key: - fkey = self.fkey - else: - fkey = None - - fields = [] - append = fields.append - selectors = set() - seen = selectors.add - for f in list_fields: - selector = f[1] if type(f) is tuple else f - if fkey and selector == fkey: - continue - if selector == pkey and not id_column: - id_field = f - elif selector not in selectors: - seen(selector) - append(f) - - if id_column == 0: - fields.insert(0, id_field) - - return fields - - # ------------------------------------------------------------------------- - def get_defaults(self, master, defaults=None, data=None): - """ - Get implicit defaults for new component records - - @param master: the master record - @param defaults: any explicit defaults - @param data: any actual values for the new record - - @return: a dict of {fieldname: values} with the defaults - """ - - values = {} - - parent = self.parent - if not parent: - # Not a component - return values - - # Implicit defaults from component filters - hook = current.s3db.get_component(parent.tablename, self.alias) - filterby = hook.get("filterby") - if filterby: - for (k, v) in filterby.items(): - if not isinstance(v, (tuple, list)): - values[k] = v - - # Explicit defaults from component hook - if self.defaults: - values.update(self.defaults) - - # Explicit defaults from caller - if defaults: - values.update(defaults) - - # Actual record values - if data: - values.update(data) - - # Check for values to look up from master record - lookup = {} - for (k, v) in list(values.items()): - # Skip nonexistent fields - if k not in self.fields: - del values[k] - continue - # Resolve any field selectors - if isinstance(v, FS): - try: - rfield = v.resolve(parent) - except (AttributeError, SyntaxError): - continue - field = rfield.field - if not field or field.table != parent.table: - continue - if field.name in master: - values[k] = master[field.name] - else: - del values[k] - lookup[field.name] = k - - # Do we need to reload the master record to look up values? - if lookup: - row = None - parent_id = parent._id - record_id = master.get(parent_id.name) - if record_id: - fields = [parent.table[f] for f in lookup] - row = current.db(parent_id == record_id).select(limitby = (0, 1), - *fields).first() - if row: - for (k, v) in lookup.items(): - if k in row: - values[v] = row[k] - - return values - - # ------------------------------------------------------------------------- - @property - def _table(self): - """ - Get the original Table object (without SQL Alias), this - is required for SQL update (DAL doesn't detect the alias - and uses the wrong tablename). - """ - - if self.tablename != self._alias: - return current.s3db[self.tablename] - else: - return self.table - -# ============================================================================= -class S3Components(object): - """ - Lazy component loader - """ - - def __init__(self, master, expose=None): - """ - Constructor - - @param master: the master resource (S3Resource) - @param expose: aliases of components to expose, defaults to - all configured components - """ - - self.master = master - - if expose is None: - hooks = current.s3db.get_hooks(master.tablename)[1] - if hooks: - self.exposed_aliases = set(hooks.keys()) - else: - self.exposed_aliases = set() - else: - self.exposed_aliases = set(expose) - - self._components = {} - self._exposed = {} - - self.links = {} - - # ------------------------------------------------------------------------- - def get(self, alias, default=None): - """ - Access a component resource by its alias; will load the - component if not loaded yet - - @param alias: the component alias - @param default: default to return if the alias is not defined - - @return: the component resource (S3Resource) - """ - - components = self._components - - component = components.get(alias) - if not component: - self.__load((alias,)) - return components.get(alias, default) - else: - db = current.db - table_alias = component._alias - if not getattr(db, table_alias, None): - setattr(db._aliased_tables, table_alias, component.table) - return component - - # ------------------------------------------------------------------------- - def __getitem__(self, alias): - """ - Access a component by its alias in key notation; will load the - component if not loaded yet - - @param alias: the component alias - - @return: the component resource (S3Resource) - - @raises: KeyError if the component is not defined - """ - - component = self.get(alias) - if component is None: - raise KeyError - else: - return component - - # ------------------------------------------------------------------------- - def __contains__(self, alias): - """ - Check if a component is defined for this resource - - @param alias: the alias to check - - @return: True|False whether the component is defined - """ - - if self.get(alias): - return True - else: - return False - - # ------------------------------------------------------------------------- - @property - def loaded(self): - """ - Get all currently loaded components - - @return: dict {alias: resource} with loaded components - """ - return self._components - - # ------------------------------------------------------------------------- - @property - def exposed(self): - """ - Get all exposed components (=> will thus load them all) - - @return: dict {alias: resource} with exposed components - """ - - loaded = self._components - exposed = self._exposed - - missing = set() - for alias in self.exposed_aliases: - if alias not in exposed: - if alias in loaded: - exposed[alias] = loaded[alias] - else: - missing.add(alias) - - if missing: - self.__load(missing) - - return exposed - - # ------------------------------------------------------------------------- - # Methods kept for backwards-compatibility - # - to be deprecated - # - use-cases should explicitly address either .loaded or .exposed - # - def keys(self): - """ - Get the aliases of all exposed components ([alias]) - """ - return list(self.exposed.keys()) - - def values(self): - """ - Get all exposed components ([resource]) - """ - return list(self.exposed.values()) - - def items(self): - """ - Get all exposed components ([(alias, resource)]) - """ - return list(self.exposed.items()) - - # ------------------------------------------------------------------------- - def __load(self, aliases, force=False): - """ - Instantiate component resources - - @param aliases: iterable of aliases of components to instantiate - @param force: forced reload of components - - @return: dict of loaded components {alias: resource} - """ - - s3db = current.s3db - - master = self.master - - components = self._components - exposed = self._exposed - exposed_aliases = self.exposed_aliases - - links = self.links - - if aliases: - if force: - # Forced reload - new = aliases - else: - new = [alias for alias in aliases if alias not in components] - else: - new = None - - hooks = s3db.get_components(master.table, names=new) - if not hooks: - return {} - - for alias, hook in hooks.items(): - - filterby = hook.filterby - if alias is not None and filterby is not None: - table_alias = "%s_%s_%s" % (hook.prefix, - hook.alias, - hook.name, - ) - table = s3db.get_aliased(hook.table, table_alias) - hook.table = table - else: - table_alias = None - table = hook.table - - # Instantiate component resource - component = S3Resource(table, - parent = master, - alias = alias, - linktable = hook.linktable, - include_deleted = master.include_deleted, - approved = master._approved, - unapproved = master._unapproved, - ) - - if table_alias: - component.tablename = hook.tablename - component._alias = table_alias - - # Copy hook properties to the component resource - component.pkey = hook.pkey - component.fkey = hook.fkey - - component.linktable = hook.linktable - component.lkey = hook.lkey - component.rkey = hook.rkey - component.actuate = hook.actuate - component.autodelete = hook.autodelete - component.autocomplete = hook.autocomplete - - #component.alias = alias - component.multiple = hook.multiple - component.defaults = hook.defaults - - # Component filter - if not filterby: - # Can use filterby=False to enforce table aliasing yet - # suppress component filtering, useful e.g. if the same - # table is declared as component more than once for the - # same master table (using different foreign keys) - component.filter = None - - else: - if callable(filterby): - # Callable to construct complex join filter - # => pass the (potentially aliased) component table - query = filterby(table) - elif isinstance(filterby, dict): - # Filter by multiple criteria - query = None - for k, v in filterby.items(): - if isinstance(v, FS): - # Match a field in the master table - # => identify the field - try: - rfield = v.resolve(master) - except (AttributeError, SyntaxError): - if current.response.s3.debug: - raise - else: - current.log.error(sys.exc_info()[1]) - continue - # => must be a real field in the master table - field = rfield.field - if not field or field.table != master.table: - current.log.error("Component filter for %s<=%s: " - "invalid lookup field '%s'" % - (master.tablename, alias, v.name)) - continue - subquery = (table[k] == field) - else: - is_list = isinstance(v, (tuple, list)) - if is_list and len(v) == 1: - filterfor = v[0] - is_list = False - else: - filterfor = v - if not is_list: - subquery = (table[k] == filterfor) - elif filterfor: - subquery = (table[k].belongs(set(filterfor))) - else: - continue - if subquery: - if query is None: - query = subquery - else: - query &= subquery - if query: - component.filter = query - - # Copy component properties to the link resource - link = component.link - if link is not None: - - link.pkey = component.pkey - link.fkey = component.lkey - - link.multiple = component.multiple - - link.actuate = component.actuate - link.autodelete = component.autodelete - - # Register the link table - links[link.name] = links[link.alias] = link - - # Register the component - components[alias] = component - - if alias in exposed_aliases: - exposed[alias] = component - - return components - - # ------------------------------------------------------------------------- - def reset(self, aliases=None, expose=DEFAULT): - """ - Detach currently loaded components, e.g. to force a reload - - @param aliases: aliases to remove, None for all - @param expose: aliases of components to expose (default: - keep previously exposed aliases), None for - all configured components - """ - - if expose is not DEFAULT: - if expose is None: - hooks = current.s3db.get_hooks(self.master.tablename)[1] - if hooks: - self.exposed_aliases = set(hooks.keys()) - else: - self.exposed_aliases = set() - else: - self.exposed_aliases = set(expose) - - if aliases: - - loaded = self._components - links = self.links - exposed = self._exposed - - for alias in aliases: - component = loaded.pop(alias, None) - if component: - link = component.link - for k, v in list(links.items()): - if v is link: - links.pop(k) - exposed.pop(alias, None) - else: - self._components = {} - self._exposed = {} - - self.links.clear() - -# ============================================================================= -class S3AxisFilter(object): - """ - Experimental: helper class to extract filter values for pivot - table axis fields - """ - - # ------------------------------------------------------------------------- - def __init__(self, qdict, tablenames): - """ - Constructor, recursively introspect the query dict and extract - all relevant subqueries. - - @param qdict: the query dict (from Query.as_dict(flat=True)) - @param tablenames: the names of the relevant tables - """ - - self.l = None - self.r = None - self.op = None - - self.tablename = None - self.fieldname = None - - if not qdict: - return - - l = qdict["first"] - if "second" in qdict: - r = qdict["second"] - else: - r = None - - op = qdict["op"] - if op: - # Convert operator name to standard uppercase name - # without underscore prefix - op = op.upper().strip("_") - - if "tablename" in l: - if l["tablename"] in tablenames: - self.tablename = l["tablename"] - self.fieldname = l["fieldname"] - if isinstance(r, dict): - self.op = None - else: - self.op = op - self.r = r - - elif op == "AND": - self.l = S3AxisFilter(l, tablenames) - self.r = S3AxisFilter(r, tablenames) - if self.l.op or self.r.op: - self.op = op - - elif op == "OR": - self.l = S3AxisFilter(l, tablenames) - self.r = S3AxisFilter(r, tablenames) - if self.l.op and self.r.op: - self.op = op - - elif op == "NOT": - self.l = S3AxisFilter(l, tablenames) - self.op = op - - else: - self.l = S3AxisFilter(l, tablenames) - if self.l.op: - self.op = op - - # ------------------------------------------------------------------------- - def query(self): - """ Reconstruct the query from this filter """ - - op = self.op - if op is None: - return None - - if self.tablename and self.fieldname: - l = current.s3db[self.tablename][self.fieldname] - elif self.l: - l = self.l.query() - else: - l = None - - r = self.r - if op in ("AND", "OR", "NOT"): - r = r.query() if r else True - - if op == "AND": - if l is not None and r is not None: - return l & r - elif r is not None: - return r - else: - return l - elif op == "OR": - if l is not None and r is not None: - return l | r - else: - return None - elif op == "NOT": - if l is not None: - return ~l - else: - return None - elif l is None: - return None - - if isinstance(r, S3AxisFilter): - r = r.query() - if r is None: - return None - - if op == "LOWER": - return l.lower() - elif op == "UPPER": - return l.upper() - elif op == "EQ": - return l == r - elif op == "NE": - return l != r - elif op == "LT": - return l < r - elif op == "LE": - return l <= r - elif op == "GE": - return l >= r - elif op == "GT": - return l > r - elif op == "BELONGS": - return l.belongs(r) - elif op == "CONTAINS": - return l.contains(r) - else: - return None - - # ------------------------------------------------------------------------- - def values(self, rfield): - """ - Helper method to filter list:type axis values - - @param rfield: the axis field - - @return: pair of value lists [include], [exclude] - """ - - op = self.op - tablename = self.tablename - fieldname = self.fieldname - - if tablename == rfield.tname and \ - fieldname == rfield.fname: - value = self.r - if isinstance(value, (list, tuple)): - value = [s3_str(v) for v in value] - if not value: - value = [None] - else: - value = [s3_str(value)] - if op == "CONTAINS": - return value, [] - elif op == "EQ": - return value, [] - elif op == "NE": - return [], value - elif op == "AND": - li, le = self.l.values(rfield) - ri, re = self.r.values(rfield) - return [v for v in li + ri if v not in le + re], [] - elif op == "OR": - li, le = self.l.values(rfield) - ri, re = self.r.values(rfield) - return [v for v in li + ri], [] - if op == "NOT": - li, le = self.l.values(rfield) - return [], li - return [], [] - -# ============================================================================= -class S3ResourceFilter(object): - """ Class representing a resource filter """ - - def __init__(self, - resource, - id = None, - uid = None, - filter = None, - vars = None, - extra_filters = None, - filter_component = None): - """ - Constructor - - @param resource: the S3Resource - @param id: the record ID (or list of record IDs) - @param uid: the record UID (or list of record UIDs) - @param filter: a filter query (S3ResourceQuery or Query) - @param vars: the dict of GET vars (URL filters) - @param extra_filters: extra filters (to be applied on - pre-filtered subsets), as list of - tuples (method, expression) - @param filter_component: the alias of the component the URL - filters apply for (filters for this - component must be handled separately) - """ - - self.resource = resource - - self.queries = [] - self.filters = [] - self.cqueries = {} - self.cfilters = {} - - # Extra filters - self._extra_filter_methods = None - if extra_filters: - self.set_extra_filters(extra_filters) - else: - self.efilters = [] - - self.query = None - self.rfltr = None - self.vfltr = None - - self.transformed = None - - self.multiple = True - self.distinct = False - - # Joins - self.ijoins = {} - self.ljoins = {} - - table = resource.table - - # Accessible/available query - if resource.accessible_query is not None: - method = [] - if resource._approved: - method.append("read") - if resource._unapproved: - method.append("review") - mquery = resource.accessible_query(method, table) - else: - mquery = (table._id > 0) - - # ID query - if id is not None: - if not isinstance(id, (list, tuple)): - self.multiple = False - mquery = (table._id == id) & mquery - else: - mquery = (table._id.belongs(id)) & mquery - - # UID query - UID = current.xml.UID - if uid is not None and UID in table: - if not isinstance(uid, (list, tuple)): - self.multiple = False - mquery = (table[UID] == uid) & mquery - else: - mquery = (table[UID].belongs(uid)) & mquery - - # Deletion status - DELETED = current.xml.DELETED - if DELETED in table.fields and not resource.include_deleted: - remaining = (table[DELETED] == False) - mquery &= remaining - - parent = resource.parent - if not parent: - # Standard master query - self.mquery = mquery - - # URL queries - if vars: - resource.vars = Storage(vars) - - if not vars.get("track"): - # Apply BBox Filter unless using S3Track to geolocate - bbox, joins = self.parse_bbox_query(resource, vars) - if bbox is not None: - self.queries.append(bbox) - if joins: - self.ljoins.update(joins) - - # Filters - add_filter = self.add_filter - - # Current concept: - # Interpret all URL filters in the context of master - queries = S3URLQuery.parse(resource, vars) - - # @todo: Alternative concept (inconsistent?): - # Interpret all URL filters in the context of filter_component: - #if filter_component: - # context = resource.components.get(filter_component) - # if not context: - # context = resource - #queries = S3URLQuery.parse(context, vars) - - for alias in queries: - if filter_component == alias: - for q in queries[alias]: - add_filter(q, component=alias, master=False) - else: - for q in queries[alias]: - add_filter(q) - self.cfilters = queries - else: - # Parent filter - pf = parent.rfilter - if not pf: - pf = parent.build_query() - - # Extended master query - self.mquery = mquery & pf.get_query() - - # Join the master - self.ijoins[parent._alias] = resource._join(reverse=True) - - # Component/link-table specific filters - add_filter = self.add_filter - aliases = [resource.alias] - if resource.link is not None: - aliases.append(resource.link.alias) - elif resource.linked is not None: - aliases.append(resource.linked.alias) - for alias in aliases: - for filter_set in (pf.cqueries, pf.cfilters): - if alias in filter_set: - for q in filter_set[alias]: - add_filter(q) - - # Additional filters - if filter is not None: - self.add_filter(filter) - - # ------------------------------------------------------------------------- - # Properties - # ------------------------------------------------------------------------- - @property - def extra_filter_methods(self): - """ - Getter for extra filter methods, lazy property so methods - are only imported/initialized when needed - - @todo: document the expected signature of filter methods - - @return: dict {name: callable} of known named filter methods - """ - - methods = self._extra_filter_methods - if methods is None: - - # @todo: implement hooks - methods = {} - - self._extra_filter_methods = methods - - return methods - - # ------------------------------------------------------------------------- - # Manipulation - # ------------------------------------------------------------------------- - def add_filter(self, query, component=None, master=True): - """ - Extend this filter - - @param query: a Query or S3ResourceQuery object - @param component: alias of the component the filter shall be - added to (None for master) - @param master: False to filter only component - """ - - alias = None - if not master: - if not component: - return - if component != self.resource.alias: - alias = component - - if isinstance(query, S3ResourceQuery): - self.transformed = None - filters = self.filters - cfilters = self.cfilters - self.distinct |= query._joins(self.resource)[1] - - else: - # DAL Query - filters = self.queries - cfilters = self.cqueries - - self.query = None - if alias: - if alias in self.cfilters: - cfilters[alias].append(query) - else: - cfilters[alias] = [query] - else: - filters.append(query) - return - - # ------------------------------------------------------------------------- - def add_extra_filter(self, method, expression): - """ - Add an extra filter - - @param method: a name of a known filter method, or a - callable filter method - @param expression: the filter expression (string) - """ - - efilters = self.efilters - efilters.append((method, expression)) - - return efilters - - # ------------------------------------------------------------------------- - def set_extra_filters(self, filters): - """ - Replace the current extra filters - - @param filters: list of tuples (method, expression), or None - to remove all extra filters - """ - - self.efilters = [] - if filters: - add = self.add_extra_filter - for method, expression in filters: - add(method, expression) - - return self.efilters - - # ------------------------------------------------------------------------- - # Getters - # ------------------------------------------------------------------------- - def get_query(self): - """ Get the effective DAL query """ - - if self.query is not None: - return self.query - - resource = self.resource - - query = reduce(lambda x, y: x & y, self.queries, self.mquery) - if self.filters: - if self.transformed is None: - - # Combine all filters - filters = reduce(lambda x, y: x & y, self.filters) - - # Transform with external search engine - transformed = filters.transform(resource) - self.transformed = transformed - - # Split DAL and virtual filters - self.rfltr, self.vfltr = transformed.split(resource) - - # Add to query - rfltr = self.rfltr - if isinstance(rfltr, S3ResourceQuery): - - # Resolve query against the resource - rq = rfltr.query(resource) - - # False indicates that the subquery shall be ignored - # (e.g. if not supported by platform) - if rq is not False: - query &= rq - - elif rfltr is not None: - - # Combination of virtual field filter and web2py Query - query &= rfltr - - self.query = query - return query - - # ------------------------------------------------------------------------- - def get_filter(self): - """ Get the effective virtual filter """ - - if self.query is None: - self.get_query() - return self.vfltr - - # ------------------------------------------------------------------------- - def get_extra_filters(self): - """ - Get the list of extra filters - - @return: list of tuples (method, expression) - """ - - return list(self.efilters) - - # ------------------------------------------------------------------------- - def get_joins(self, left=False, as_list=True): - """ - Get the joins required for this filter - - @param left: get the left joins - @param as_list: return a flat list rather than a nested dict - """ - - if self.query is None: - self.get_query() - - joins = dict(self.ljoins if left else self.ijoins) - - resource = self.resource - for q in self.filters: - subjoins = q._joins(resource, left=left)[0] - joins.update(subjoins) - - # Cross-component left joins - parent = resource.parent - if parent: - pf = parent.rfilter - if pf is None: - pf = parent.build_query() - - parent_left = pf.get_joins(left=True, as_list=False) - if parent_left: - tablename = resource._alias - if left: - for tn in parent_left: - if tn not in joins and tn != tablename: - joins[tn] = parent_left[tn] - joins[parent._alias] = resource._join(reverse=True) - else: - joins.pop(parent._alias, None) - - if as_list: - return [j for tablename in joins for j in joins[tablename]] - else: - return joins - - # ------------------------------------------------------------------------- - def get_fields(self): - """ Get all field selectors in this filter """ - - if self.query is None: - self.get_query() - - if self.vfltr: - return self.vfltr.fields() - else: - return [] - - # ------------------------------------------------------------------------- - # Filtering - # ------------------------------------------------------------------------- - def __call__(self, rows, start=None, limit=None): - """ - Filter a set of rows by the effective virtual filter - - @param rows: a Rows object - @param start: index of the first matching record to select - @param limit: maximum number of records to select - """ - - vfltr = self.get_filter() - - if rows is None or vfltr is None: - return rows - resource = self.resource - if start is None: - start = 0 - first = start - if limit is not None: - last = start + limit - if last < first: - first, last = last, first - if first < 0: - first = 0 - if last < 0: - last = 0 - else: - last = None - i = 0 - result = [] - append = result.append - for row in rows: - if last is not None and i >= last: - break - success = vfltr(resource, row, virtual=True) - if success or success is None: - if i >= first: - append(row) - i += 1 - return Rows(rows.db, - result, - colnames = rows.colnames, - compact = False) - - # ------------------------------------------------------------------------- - def apply_extra_filters(self, ids, start=None, limit=None): - """ - Apply all extra filters on a list of record ids - - @param ids: the pre-filtered set of record IDs - @param limit: the maximum number of matching IDs to establish, - None to find all matching IDs - - @return: a sequence of matching IDs - """ - - # Get the resource - resource = self.resource - - # Get extra filters - efilters = self.efilters - - # Resolve filter methods - methods = self.extra_filter_methods - filters = [] - append = filters.append - for method, expression in efilters: - if callable(method): - append((method, expression)) - else: - method = methods.get(method) - if method: - append((method, expression)) - else: - current.log.warning("Unknown filter method: %s" % method) - if not filters: - # No applicable filters - return ids - - # Clear extra filters so that apply_extra_filters is not - # called from inside a filter method (e.g. if the method - # uses resource.select) - self.efilters = [] - - # Initialize subset - subset = set() - tail = ids - limit_ = limit - - while tail: - - if limit: - head, tail = tail[:limit_], tail[limit_:] - else: - head, tail = tail, None - - match = head - for method, expression in filters: - # Apply filter - match = method(resource, match, expression) - if not match: - break - - if match: - subset |= set(match) - - found = len(subset) - - if limit: - if found < limit: - # Need more - limit_ = limit - found - else: - # Found all - tail = None - - # Restore order - subset = [item for item in ids if item in subset] - - # Select start - if start: - subset = subset[start:] - - # Restore extra filters - self.efilters = efilters - - return subset - - # ------------------------------------------------------------------------- - def count(self, left=None, distinct=False): - """ - Get the total number of matching records - - @param left: left outer joins - @param distinct: count only distinct rows - """ - - distinct |= self.distinct - - resource = self.resource - if resource is None: - return 0 - - table = resource.table - - vfltr = self.get_filter() - - if vfltr is None and not distinct: - - tablename = table._tablename - - ijoins = S3Joins(tablename, self.get_joins(left=False)) - ljoins = S3Joins(tablename, self.get_joins(left=True)) - ljoins.add(left) - - join = ijoins.as_list(prefer=ljoins) - left = ljoins.as_list() - - cnt = table._id.count() - row = current.db(self.query).select(cnt, - join=join, - left=left).first() - if row: - return row[cnt] - else: - return 0 - - else: - data = resource.select([table._id.name], - # We don't really want to retrieve - # any rows but just count, hence: - limit=1, - count=True) - return data["numrows"] - - # ------------------------------------------------------------------------- - # Utility Methods - # ------------------------------------------------------------------------- - def __repr__(self): - """ String representation of the instance """ - - resource = self.resource - - inner_joins = self.get_joins(left=False) - if inner_joins: - inner = S3Joins(resource.tablename, inner_joins) - ijoins = ", ".join([str(j) for j in inner.as_list()]) - else: - ijoins = None - - left_joins = self.get_joins(left=True) - if left_joins: - left = S3Joins(resource.tablename, left_joins) - ljoins = ", ".join([str(j) for j in left.as_list()]) - else: - ljoins = None - - vfltr = self.get_filter() - if vfltr: - vfltr = vfltr.represent(resource) - else: - vfltr = None - - represent = "" % (resource.tablename, - self.get_query(), - ijoins, - ljoins, - self.distinct, - vfltr, - ) - - return represent - - # ------------------------------------------------------------------------- - @staticmethod - def parse_bbox_query(resource, get_vars): - """ - Generate a Query from a URL boundary box query; supports multiple - bboxes, but optimised for the usual case of just 1 - - @param resource: the resource - @param get_vars: the URL GET vars - """ - - tablenames = ("gis_location", - "gis_feature_query", - "gis_layer_shapefile", - ) - - POLYGON = "POLYGON((%s %s, %s %s, %s %s, %s %s, %s %s))" - - query = None - joins = {} - - if get_vars: - - table = resource.table - tablename = resource.tablename - fields = table.fields - - introspect = tablename not in tablenames - for k, v in get_vars.items(): - - if k[:4] == "bbox": - - if type(v) is list: - v = v[-1] - try: - minLon, minLat, maxLon, maxLat = v.split(",") - except ValueError: - # Badly-formed bbox - ignore - continue - - # Identify the location reference - field = None - rfield = None - alias = False - - if k.find(".") != -1: - - # Field specified in query - fname = k.split(".")[1] - if fname not in fields: - # Field not found - ignore - continue - field = table[fname] - if query is not None or "bbox" in get_vars: - # Need alias - alias = True - - elif introspect: - - # Location context? - context = resource.get_config("context") - if context and "location" in context: - try: - rfield = resource.resolve_selector("(location)$lat") - except (SyntaxError, AttributeError): - rfield = None - else: - if not rfield.field or rfield.tname != "gis_location": - # Invalid location context - rfield = None - - # Fall back to location_id (or site_id as last resort) - if rfield is None: - fname = None - for f in fields: - ftype = str(table[f].type) - if ftype[:22] == "reference gis_location": - fname = f - break - elif not fname and \ - ftype[:18] == "reference org_site": - fname = f - field = table[fname] if fname else None - - if not rfield and not field: - # No location reference could be identified => skip - continue - - # Construct the join to gis_location - gtable = current.s3db.gis_location - if rfield: - joins.update(rfield.left) - - elif field: - fname = field.name - gtable = current.s3db.gis_location - if alias: - gtable = gtable.with_alias("gis_%s_location" % fname) - tname = str(gtable) - ftype = str(field.type) - if ftype == "reference gis_location": - joins[tname] = [gtable.on(gtable.id == field)] - elif ftype == "reference org_site": - stable = current.s3db.org_site - if alias: - stable = stable.with_alias("org_%s_site" % fname) - joins[tname] = [stable.on(stable.site_id == field), - gtable.on(gtable.id == stable.location_id)] - elif introspect: - # => not a location or site reference - continue - - elif tablename in ("gis_location", "gis_feature_query"): - gtable = table - - elif tablename == "gis_layer_shapefile": - # Find the layer_shapefile_%(layer_id)s component - # (added dynamically in gis/layer_shapefile controller) - gtable = None - hooks = current.s3db.get_hooks("gis_layer_shapefile")[1] - for alias in hooks: - if alias[:19] == "gis_layer_shapefile": - component = resource.components.get(alias) - if component: - gtable = component.table - break - # Join by layer_id - if gtable: - joins[str(gtable)] = \ - [gtable.on(gtable.layer_id == table._id)] - else: - continue - - # Construct the bbox filter - bbox_filter = None - if current.deployment_settings.get_gis_spatialdb(): - # Use the Spatial Database - minLon = float(minLon) - maxLon = float(maxLon) - minLat = float(minLat) - maxLat = float(maxLat) - bbox = POLYGON % (minLon, minLat, - minLon, maxLat, - maxLon, maxLat, - maxLon, minLat, - minLon, minLat) - try: - # Spatial DAL & Database - bbox_filter = gtable.the_geom \ - .st_intersects(bbox) - except: - # Old DAL or non-spatial database - pass - - if bbox_filter is None: - # Standard Query - bbox_filter = (gtable.lon > float(minLon)) & \ - (gtable.lon < float(maxLon)) & \ - (gtable.lat > float(minLat)) & \ - (gtable.lat < float(maxLat)) - - # Add bbox filter to query - if query is None: - query = bbox_filter - else: - # Merge with the previous BBOX - query = query & bbox_filter - - return query, joins - - # ------------------------------------------------------------------------- - def serialize_url(self): - """ - Serialize this filter as URL query - - @return: a Storage of URL GET variables - """ - - resource = self.resource - url_vars = Storage() - for f in self.filters: - sub = f.serialize_url(resource=resource) - url_vars.update(sub) - return url_vars - -# ============================================================================= -class S3ResourceData(object): - """ Class representing data in a resource """ - - def __init__(self, - resource, - fields, - start = 0, - limit = None, - left = None, - orderby = None, - groupby = None, - distinct = False, - virtual = True, - count = False, - getids = False, - as_rows = False, - represent = False, - show_links = True, - raw_data = False - ): - """ - Constructor, extracts (and represents) data from a resource - - @param resource: the resource - @param fields: the fields to extract (selector strings) - @param start: index of the first record - @param limit: maximum number of records - @param left: additional left joins required for custom filters - @param orderby: orderby-expression for DAL - @param groupby: fields to group by (overrides fields!) - @param distinct: select distinct rows - @param virtual: include mandatory virtual fields - @param count: include the total number of matching records - @param getids: include the IDs of all matching records - @param as_rows: return the rows (don't extract/represent) - @param represent: render field value representations - @param raw_data: include raw data in the result - - @note: as_rows / groupby prevent automatic splitting of - large multi-table joins, so use with care! - @note: with groupby, only the groupby fields will be returned - (i.e. fields will be ignored), because aggregates are - not supported (yet) - """ - - db = current.db - - # Suppress instantiation of LazySets in rows where we don't need them - if not as_rows and not groupby: - rname = db._referee_name - db._referee_name = None - else: - rname = None - - # The resource - self.resource = resource - self.table = table = resource.table - - # If postprocessing is required, always include raw data - postprocess = resource.get_config("postprocess_select") - if postprocess: - raw_data = True - - # Dict to collect accessible queries for differential - # field authorization (each joined table is authorized - # separately) - self.aqueries = aqueries = {} - - # Retain the current accessible-context of the parent - # resource in reverse component joins: - parent = resource.parent - if parent and parent.accessible_query is not None: - method = [] - if parent._approved: - method.append("read") - if parent._unapproved: - method.append("review") - aqueries[parent.tablename] = parent.accessible_query(method, - parent.table, - ) - - # Joins (inner/left) - tablename = table._tablename - self.ijoins = ijoins = S3Joins(tablename) - self.ljoins = ljoins = S3Joins(tablename) - - # The query - master_query = query = resource.get_query() - - # Joins from filters - # @note: in components, rfilter is None until after get_query! - rfilter = resource.rfilter - filter_tables = set(ijoins.add(rfilter.get_joins(left=False))) - filter_tables.update(ljoins.add(rfilter.get_joins(left=True))) - - # Left joins from caller - master_tables = set(ljoins.add(left)) - filter_tables.update(master_tables) - - resolve = resource.resolve_selectors - - # Virtual fields and extra fields required by filter - virtual_fields = rfilter.get_fields() - vfields, vijoins, vljoins, d = resolve(virtual_fields, show=False) - extra_tables = set(ijoins.extend(vijoins)) - extra_tables.update(ljoins.extend(vljoins)) - distinct |= d - - # Display fields (fields to include in the result) - if fields is None: - fields = [f.name for f in resource.readable_fields()] - dfields, dijoins, dljoins, d = resolve(fields, extra_fields=False) - ijoins.extend(dijoins) - ljoins.extend(dljoins) - distinct |= d - - # Primary key - pkey = str(table._id) - - # Initialize field data and effort estimates - if not groupby or as_rows: - self.init_field_data(dfields) - else: - self.field_data = self.effort = None - - # Resolve ORDERBY - orderby, orderby_aggr, orderby_fields, tables = self.resolve_orderby(orderby) - if tables: - filter_tables.update(tables) - - # Joins for filter query - filter_ijoins = ijoins.as_list(tablenames = filter_tables, - aqueries = aqueries, - prefer = ljoins, - ) - filter_ljoins = ljoins.as_list(tablenames = filter_tables, - aqueries = aqueries, - ) - - # Virtual fields filter - vfilter = resource.get_filter() - - # Extra filters - efilter = rfilter.get_extra_filters() - - # Is this a paginated request? - pagination = limit is not None or start - - # Subselect? - subselect = bool(ljoins or ijoins or efilter or vfilter and pagination) - - # Do we need a filter query? - fq = count_only = False - if not groupby: - end_count = (vfilter or efilter) and not pagination - if count and not end_count: - fq = True - count_only = True - if subselect or \ - getids and pagination or \ - extra_tables and extra_tables != filter_tables: - fq = True - count_only = False - - # Shall we use scalability-optimized strategies? - bigtable = current.deployment_settings.get_base_bigtable() - - # Filter Query: - # If we need to determine the number and/or ids of all matching - # records, but not to extract all records, then we run a - # separate query here to extract just this information: - ids = page = totalrows = None - if fq: - # Execute the filter query - if bigtable and not vfilter: - limitby = resource.limitby(start=start, limit=limit) - else: - limitby = None - totalrows, ids = self.filter_query(query, - join = filter_ijoins, - left = filter_ljoins, - getids = not count_only, - orderby = orderby_aggr, - limitby = limitby, - ) - - # Simplify the master query if possible - empty = False - limitby = None - orderby_on_limitby = True - - # If we know all possible record IDs from the filter query, - # then we can simplify the master query so it doesn't need - # complex joins - if ids is not None: - if not ids: - # No records matching the filter query, so we - # can skip the master query too - empty = True - else: - # Which records do we need to extract? - if pagination and (efilter or vfilter): - master_ids = ids - else: - if bigtable: - master_ids = page = ids - else: - limitby = resource.limitby(start=start, limit=limit) - if limitby: - page = ids[limitby[0]:limitby[1]] - else: - page = ids - master_ids = page - - # Simplify master query - if page is not None and not page: - # Empty page, skip the master query - empty = True - master_query = None - elif len(master_ids) == 1: - # Single record, don't use belongs (faster) - master_query = table._id == master_ids[0] - else: - master_query = table._id.belongs(set(master_ids)) - - orderby = None - if not ljoins or ijoins: - # Without joins, there can only be one row per id, - # so we can limit the master query (faster) - limitby = (0, len(master_ids)) - # Prevent automatic ordering - orderby_on_limitby = False - else: - # With joins, there could be more than one row per id, - # so we can not limit the master query - limitby = None - - elif pagination and not (efilter or vfilter or count or getids): - - limitby = resource.limitby(start=start, limit=limit) - - if not empty: - # If we don't use a simplified master_query, we must include - # all necessary joins for filter and orderby (=filter_tables) in - # the master query - if ids is None and (filter_ijoins or filter_ljoins): - master_tables = filter_tables - - # Determine fields in master query - if not groupby: - master_tables.update(extra_tables) - tables, qfields, mfields, groupby = self.master_fields(dfields, - vfields, - master_tables, - as_rows = as_rows, - groupby = groupby, - ) - # Additional tables to join? - if tables: - master_tables.update(tables) - - # ORDERBY settings - if groupby: - distinct = False - orderby = orderby_aggr - has_id = pkey in qfields - else: - if distinct and orderby: - # With DISTINCT, ORDERBY-fields must appear in SELECT - # (required by postgresql?) - for orderby_field in orderby_fields: - fn = str(orderby_field) - if fn not in qfields: - qfields[fn] = orderby_field - - # Make sure we have the primary key in SELECT - if pkey not in qfields: - qfields[pkey] = resource._id - has_id = True - - # Execute master query - db = current.db - - master_fields = list(qfields.keys()) - if not groupby and not pagination and \ - has_id and ids and len(master_fields) == 1: - # We already have the ids, and master query doesn't select - # anything else => skip the master query, construct Rows from - # ids instead - master_id = table._id.name - rows = Rows(db, - [Row({master_id: record_id}) for record_id in ids], - colnames = [pkey], - compact = False, - ) - # Add field methods (some do work from bare ids) - try: - fields_lazy = [(f.name, f) for f in table._virtual_methods] - except (AttributeError, TypeError): - # Incompatible PyDAL version - pass - else: - if fields_lazy: - for row in rows: - for f, v in fields_lazy: - try: - row[f] = (v.handler or VirtualCommand)(v.f, row) - except (AttributeError, KeyError): - pass - else: - # Joins for master query - master_ijoins = ijoins.as_list(tablenames = master_tables, - aqueries = aqueries, - prefer = ljoins, - ) - master_ljoins = ljoins.as_list(tablenames = master_tables, - aqueries = aqueries, - ) - - # Suspend (mandatory) virtual fields if so requested - if not virtual: - vf = table.virtualfields - osetattr(table, "virtualfields", []) - - rows = db(master_query).select(join = master_ijoins, - left = master_ljoins, - distinct = distinct, - groupby = groupby, - orderby = orderby, - limitby = limitby, - orderby_on_limitby = orderby_on_limitby, - cacheable = not as_rows, - *list(qfields.values())) - - # Restore virtual fields - if not virtual: - osetattr(table, "virtualfields", vf) - - else: - rows = Rows(current.db) - - # Apply any virtual/extra filters, determine the subset - if not len(rows) and not ids: - - # Empty set => empty subset (no point to filter/count) - page = [] - ids = [] - totalrows = 0 - - elif not groupby: - if efilter or vfilter: - - # Filter by virtual fields - shortcut = False - if vfilter: - if pagination and not any((getids, count, efilter)): - # Don't need ids or totalrows - rows = rfilter(rows, start=start, limit=limit) - page = self.getids(rows, pkey) - shortcut = True - else: - rows = rfilter(rows) - - # Extra filter - if efilter: - if vfilter or not ids: - ids = self.getids(rows, pkey) - if pagination and not (getids or count): - limit_ = start + limit - else: - limit_ = None - ids = rfilter.apply_extra_filters(ids, limit = limit_) - rows = self.getrows(rows, ids, pkey) - - if pagination: - # Subset selection with vfilter/efilter - # (=post-filter pagination) - if not shortcut: - if not efilter: - ids = self.getids(rows, pkey) - totalrows = len(ids) - rows, page = self.subset(rows, ids, - start = start, - limit = limit, - has_id = has_id, - ) - else: - # Unlimited select with vfilter/efilter - if not efilter: - ids = self.getids(rows, pkey) - page = ids - totalrows = len(ids) - - elif pagination: - - if page is None: - if limitby: - # Limited master query without count/getids - # (=rows is the subset, only need page IDs) - page = self.getids(rows, pkey) - else: - # Limited select with unlimited master query - # (=getids/count without filter query, need subset) - if not ids: - ids = self.getids(rows, pkey) - # Build the subset - rows, page = self.subset(rows, ids, - start = start, - limit = limit, - has_id = has_id, - ) - totalrows = len(ids) - - elif not ids: - # Unlimited select without vfilter/efilter - page = ids = self.getids(rows, pkey) - totalrows = len(ids) - - # Build the result - self.rfields = dfields - self.numrows = 0 if totalrows is None else totalrows - self.ids = ids - - if groupby or as_rows: - # Just store the rows, no further queries or extraction - self.rows = rows - - elif not rows: - # No rows found => empty list - self.rows = [] - - else: - # Extract the data from the master rows - records = self.extract(rows, - pkey, - list(mfields), - join = hasattr(rows[0], tablename), - represent = represent, - ) - - # Extract the page record IDs if we don't have them yet - if page is None: - if ids is None: - self.ids = ids = self.getids(rows, pkey) - page = ids - - - # Execute any joined queries - joined_fields = self.joined_fields(dfields, qfields) - joined_query = table._id.belongs(page) - - for jtablename, jfields in joined_fields.items(): - records = self.joined_query(jtablename, - joined_query, - jfields, - records, - represent = represent, - ) - - # Re-combine and represent the records - results = {} - - field_data = self.field_data - NONE = current.messages["NONE"] - - render = self.render - for dfield in dfields: - - if represent: - # results = {RecordID: {ColumnName: Representation}} - results = render(dfield, - results, - none = NONE, - raw_data = raw_data, - show_links = show_links, - ) - - else: - # results = {RecordID: {ColumnName: Value}} - colname = dfield.colname - - fdata = field_data[colname] - frecords = fdata[1] - list_type = fdata[3] - - for record_id in records: - if record_id not in results: - result = results[record_id] = Storage() - else: - result = results[record_id] - - data = list(frecords[record_id].keys()) - if len(data) == 1 and not list_type: - data = data[0] - result[colname] = data - - self.rows = [results[record_id] for record_id in page] - - if rname: - # Restore referee name - db._referee_name = rname - - # Postprocess data (postprocess_select hook of the resource): - # Allow the callback to modify the selected data before - # returning them to the caller, callback receives: - # - a dict with the data {record_id: row} - # - the list of resource fields - # - the represent-flag to indicate represented data - # - the as_rows-flag to indicate bare Rows in the data dict - # NB the callback must not remove fields from the rows - if postprocess: - postprocess(dict(zip(page, self.rows)), - rfields = dfields, - represent = represent, - as_rows = bool(as_rows or groupby), - ) - - # ------------------------------------------------------------------------- - def init_field_data(self, rfields): - """ - Initialize field data and effort estimates for representation - - Field data: allow representation per unique value (rather than - record by record), together with bulk-represent this - can reduce the total lookup effort per field to a - single query - - Effort estimates: if no bulk-represent is available for a - list:reference, then a lookup per unique value - is only faster if the number of unique values - is significantly lower than the number of - extracted rows (and the number of values per - row), otherwise a per-row lookup is more - efficient. - - E.g. 5 rows with 2 values each, - 10 unique values in total - => row-by-row lookup more efficient - (5 queries vs 10 queries) - but: 5 rows with 2 values each, - 2 unique values in total - => value-by-value lookup is faster - (5 queries vs 2 queries) - - However: 15 rows with 15 values each, - 20 unique values in total - => value-by-value lookup faster - (15 queries á 15 values vs. - 20 queries á 1 value)! - - The required effort is estimated - during the data extraction, and then used to - determine the lookup strategy for the - representation. - - @param rfields: the fields to extract ([S3ResourceField]) - """ - - table = self.resource.table - tablename = table._tablename - pkey = str(table._id) - - field_data = {pkey: ({}, {}, False, False, False, False)} - effort = {pkey: 0} - for dfield in rfields: - colname = dfield.colname - effort[colname] = 0 - ftype = dfield.ftype[:4] - field_data[colname] = ({}, {}, - dfield.tname != tablename, - ftype == "list", - dfield.virtual, - ftype == "json", - ) - - self.field_data = field_data - self.effort = effort - - return - - # ------------------------------------------------------------------------- - def resolve_orderby(self, orderby): - """ - Resolve the ORDERBY expression. - - @param orderby: the orderby expression from the caller - @return: tuple (expr, aggr, fields, tables): - expr: the orderby expression (resolved into Fields) - aggr: the orderby expression with aggregations - fields: the fields in the orderby - tables: the tables required for the orderby - - @note: for GROUPBY id (e.g. filter query), all ORDERBY fields - must appear in aggregation functions, otherwise ORDERBY - can be ambiguous => use aggr instead of expr - """ - - table = self.resource.table - tablename = table._tablename - pkey = str(table._id) - - ljoins = self.ljoins - ijoins = self.ijoins - - tables = set() - adapter = S3DAL() - - if orderby: - - db = current.db - items = self.resolve_expression(orderby) - - expr = [] - aggr = [] - fields = [] - - for item in items: - - expression = None - - if type(item) is Expression: - f = item.first - op = item.op - if op == adapter.AGGREGATE: - # Already an aggregation - expression = item - elif isinstance(f, Field) and op == adapter.INVERT: - direction = "desc" - else: - # Other expression - not supported - continue - elif isinstance(item, Field): - direction = "asc" - f = item - elif isinstance(item, str): - fn, direction = (item.strip().split() + ["asc"])[:2] - tn, fn = ([tablename] + fn.split(".", 1))[-2:] - try: - f = db[tn][fn] - except (AttributeError, KeyError): - continue - else: - continue - - fname = str(f) - tname = fname.split(".", 1)[0] - - if tname != tablename: - if tname in ljoins or tname in ijoins: - tables.add(tname) - else: - # No join found for this field => skip - continue - - fields.append(f) - if expression is None: - expression = f if direction == "asc" else ~f - expr.append(expression) - direction = direction.strip().lower()[:3] - if fname != pkey: - expression = f.min() if direction == "asc" else ~(f.max()) - else: - expr.append(expression) - aggr.append(expression) - - else: - expr = None - aggr = None - fields = None - - return expr, aggr, fields, tables - - # ------------------------------------------------------------------------- - def filter_query(self, - query, - join = None, - left = None, - getids = False, - limitby = None, - orderby = None, - ): - """ - Execute a query to determine the number/record IDs of all - matching rows - - @param query: the filter query - @param join: the inner joins for the query - @param left: the left joins for the query - @param getids: extract the IDs of matching records - @param limitby: tuple of indices (start, end) to extract only - a limited set of IDs - @param orderby: ORDERBY expression for the query - - @return: tuple of (TotalNumberOfRecords, RecordIDs) - """ - - db = current.db - - table = self.table - - # Temporarily deactivate virtual fields - vf = table.virtualfields - osetattr(table, "virtualfields", []) - - if getids and limitby: - # Large result sets expected on average (settings.base.bigtable) - # => effort almost independent of result size, much faster - # for large and very large filter results - start = limitby[0] - limit = limitby[1] - start - - # Don't penalize the smallest filter results (=effective filtering) - if limit: - maxids = max(limit, 200) - limitby_ = (start, start + maxids) - else: - limitby_ = None - - # Extract record IDs - field = table._id - rows = db(query).select(field, - join = join, - left = left, - limitby = limitby_, - orderby = orderby, - groupby = field, - cacheable = True, - ) - pkey = str(field) - results = rows[:limit] if limit else rows - ids = [row[pkey] for row in results] - - totalids = len(rows) - if limit and totalids >= maxids or start != 0 and not totalids: - # Count all matching records - cnt = table._id.count(distinct=True) - row = db(query).select(cnt, - join = join, - left = left, - cacheable = True, - ).first() - totalrows = row[cnt] - else: - # We already know how many there are - totalrows = start + totalids - - elif getids: - # Extract all matching IDs, then count them in Python - # => effort proportional to result size, slightly faster - # than counting separately for small filter results - field = table._id - rows = db(query).select(field, - join=join, - left=left, - orderby = orderby, - groupby = field, - cacheable = True, - ) - pkey = str(field) - ids = [row[pkey] for row in rows] - totalrows = len(ids) - - else: - # Only count, do not extract any IDs (constant effort) - field = table._id.count(distinct=True) - rows = db(query).select(field, - join = join, - left = left, - cacheable = True, - ) - ids = None - totalrows = rows.first()[field] - - # Restore the virtual fields - osetattr(table, "virtualfields", vf) - - return totalrows, ids - - # ------------------------------------------------------------------------- - def master_fields(self, - dfields, - vfields, - joined_tables, - as_rows = False, - groupby = None - ): - """ - Find all tables and fields to retrieve in the master query - - @param dfields: the requested fields (S3ResourceFields) - @param vfields: the virtual filter fields - @param joined_tables: the tables joined in the master query - @param as_rows: whether to produce web2py Rows - @param groupby: the GROUPBY expression from the caller - - @return: tuple (tables, fields, extract, groupby): - tables: the tables required to join - fields: the fields to retrieve - extract: the fields to extract from the result - groupby: the GROUPBY expression (resolved into Fields) - """ - - db = current.db - tablename = self.resource.table._tablename - - # Names of additional tables to join - tables = set() - - # Fields to retrieve in the master query, as dict {ColumnName: Field} - fields = {} - - # Column names of fields to extract from the master rows - extract = set() - - if groupby: - # Resolve the groupby into Fields - items = self.resolve_expression(groupby) - - groupby = [] - groupby_append = groupby.append - for item in items: - - # Identify the field - tname = None - if isinstance(item, Field): - f = item - elif isinstance(item, str): - fn = item.strip() - tname, fn = ([tablename] + fn.split(".", 1))[-2:] - try: - f = db[tname][fn] - except (AttributeError, KeyError): - continue - else: - continue - groupby_append(f) - - # Add to fields - fname = str(f) - if not tname: - tname = f.tablename - fields[fname] = f - - # Do we need to join additional tables? - if tname == tablename: - # no join required - continue - else: - # Get joins from dfields - tnames = None - for dfield in dfields: - if dfield.colname == fname: - tnames = self.rfield_tables(dfield) - break - if tnames: - tables |= tnames - else: - # Join at least the table that holds the fields - tables.add(tname) - - # Only extract GROUPBY fields (as we don't support aggregates) - extract = set(fields.keys()) - - else: - rfields = dfields + vfields - for rfield in rfields: - - # Is the field in a joined table? - tname = rfield.tname - joined = tname == tablename or tname in joined_tables - - if as_rows or joined: - colname = rfield.colname - if rfield.show: - # If show => add to extract - extract.add(colname) - if rfield.field: - # If real field => add to fields - fields[colname] = rfield.field - if not joined: - # Not joined yet? => add all required tables - tables |= self.rfield_tables(rfield) - - return tables, fields, extract, groupby - - # ------------------------------------------------------------------------- - def joined_fields(self, all_fields, master_fields): - """ - Determine which fields in joined tables haven't been - retrieved in the master query - - @param all_fields: all requested fields (list of S3ResourceFields) - @param master_fields: all fields in the master query, a dict - {ColumnName: Field} - - @return: a nested dict {TableName: {ColumnName: Field}}, - additionally required left joins are stored per - table in the inner dict as "_left" - """ - - resource = self.resource - table = resource.table - tablename = table._tablename - - fields = {} - for rfield in all_fields: - - colname = rfield.colname - if colname in master_fields or rfield.tname == tablename: - continue - tname = rfield.tname - - if tname not in fields: - sfields = fields[tname] = {} - left = rfield.left - joins = S3Joins(table) - for tn in left: - joins.add(left[tn]) - sfields["_left"] = joins - else: - sfields = fields[tname] - - if colname not in sfields: - sfields[colname] = rfield.field - - return fields - - # ------------------------------------------------------------------------- - def joined_query(self, tablename, query, fields, records, represent=False): - """ - Extract additional fields from a joined table: if there are - fields in joined tables which haven't been extracted in the - master query, then we perform a separate query for each joined - table (this is faster than building a multi-table-join) - - @param tablename: name of the joined table - @param query: the Query - @param fields: the fields to extract - @param records: the output dict to update, structure: - {RecordID: {ColumnName: RawValues}} - @param represent: store extracted data (self.field_data) for - fast representation, and estimate lookup - efforts (self.effort) - - @return: the output dict - """ - - s3db = current.s3db - - ljoins = self.ljoins - table = self.resource.table - pkey = str(table._id) - - # Get the extra fields for subtable - sresource = s3db.resource(tablename) - efields, ejoins, l, d = sresource.resolve_selectors([]) - - # Get all left joins for subtable - tnames = ljoins.extend(l) + list(fields["_left"].tables) - sjoins = ljoins.as_list(tablenames = tnames, - aqueries = self.aqueries, - ) - if not sjoins: - return records - del fields["_left"] - - # Get all fields for subtable query - extract = list(fields.keys()) - for efield in efields: - fields[efield.colname] = efield.field - sfields = [f for f in fields.values() if f] - if not sfields: - sfields.append(sresource._id) - sfields.insert(0, table._id) - - # Retrieve the subtable rows - # - can't use distinct with native JSON fields - distinct = not any(f.type == "json" for f in sfields) - rows = current.db(query).select(left = sjoins, - distinct = distinct, - cacheable = True, - *sfields) - - # Extract and merge the data - records = self.extract(rows, - pkey, - extract, - records = records, - join = True, - represent = represent, - ) - - return records - - # ------------------------------------------------------------------------- - def extract(self, - rows, - pkey, - columns, - join = True, - records = None, - represent = False - ): - """ - Extract the data from rows and store them in self.field_data - - @param rows: the rows - @param pkey: the primary key - @param columns: the columns to extract - @param join: the rows are the result of a join query - @param records: the records dict to merge the data into - @param represent: collect unique values per field and estimate - representation efforts for list:types - """ - - field_data = self.field_data - effort = self.effort - - if records is None: - records = {} - - def get(key): - t, f = key.split(".", 1) - if join: - def getter(row): - return ogetattr(ogetattr(row, t), f) - else: - def getter(row): - return ogetattr(row, f) - return getter - - getkey = get(pkey) - getval = [get(c) for c in columns] - - from itertools import groupby - for k, g in groupby(rows, key=getkey): - group = list(g) - record = records.get(k, {}) - for idx, col in enumerate(columns): - fvalues, frecords, joined, list_type, virtual, json_type = field_data[col] - values = record.get(col, {}) - lazy = False - for row in group: - try: - value = getval[idx](row) - except AttributeError: - current.log.warning("Warning S3Resource.extract: column %s not in row" % col) - value = None - if lazy or callable(value): - # Lazy virtual field - value = value() - lazy = True - if virtual and not list_type and type(value) is list: - # Virtual field that returns a list - list_type = True - if list_type and value is not None: - if represent and value: - effort[col] += 30 + len(value) - for v in value: - if v not in values: - values[v] = None - if represent and v not in fvalues: - fvalues[v] = None - elif json_type: - # Returns unhashable types - value = json.dumps(value) - if value not in values: - values[value] = None - if represent and value not in fvalues: - fvalues[value] = None - else: - if value not in values: - values[value] = None - if represent and value not in fvalues: - fvalues[value] = None - record[col] = values - if k not in frecords: - frecords[k] = record[col] - records[k] = record - - return records - - # ------------------------------------------------------------------------- - def render(self, - rfield, - results, - none = "-", - raw_data = False, - show_links = True - ): - """ - Render the representations of the values for rfield in - all records in the result - - @param rfield: the field (S3ResourceField) - @param results: the output dict to update with the representations, - structure: {RecordID: {ColumnName: Representation}}, - the raw data will be a special item "_row" in the - inner dict holding a Storage of the raw field values - @param none: default representation of None - @param raw_data: retain the raw data in the output dict - @param show_links: allow representation functions to render - links as HTML - """ - - colname = rfield.colname - - fvalues, frecords, joined, list_type = self.field_data[colname][:4] - - # Get the renderer - renderer = rfield.represent - if not callable(renderer): - # @ToDo: Don't convert unformatted numbers to strings - renderer = lambda v: s3_str(v) if v is not None else none - - # Deactivate linkto if so requested - if not show_links and hasattr(renderer, "show_link"): - show_link = renderer.show_link - renderer.show_link = False - else: - show_link = None - - per_row_lookup = list_type and \ - self.effort[colname] < len(fvalues) * 30 - - # Treat even single values as lists? - # - can be set as class attribute of custom S3Represents - always_list = hasattr(renderer, "always_list") and renderer.always_list - - # Render all unique values - if hasattr(renderer, "bulk") and not list_type: - per_row_lookup = False - fvalues = renderer.bulk(list(fvalues.keys()), list_type=False) - elif not per_row_lookup: - for value in fvalues: - try: - text = renderer(value) - except: - raise - text = s3_str(value) - fvalues[value] = text - - # Write representations into result - for record_id in frecords: - - if record_id not in results: - results[record_id] = Storage() \ - if not raw_data \ - else Storage(_row=Storage()) - - record = frecords[record_id] - result = results[record_id] - - # List type with per-row lookup? - if per_row_lookup: - value = list(record.keys()) - if None in value and len(value) > 1: - value = [v for v in value if v is not None] - try: - text = renderer(value) - except: - text = s3_str(value) - result[colname] = text - if raw_data: - result["_row"][colname] = value - - # Single value (master record) - elif len(record) == 1 and not always_list or \ - not joined and not list_type: - value = list(record.keys())[0] - result[colname] = fvalues[value] \ - if value in fvalues else none - if raw_data: - result["_row"][colname] = value - continue - - # Multiple values (joined or list-type, or explicit always_list) - else: - if hasattr(renderer, "render_list"): - # Prefer S3Represent's render_list (so it can be customized) - data = renderer.render_list(list(record.keys()), - fvalues, - show_link = show_links, - ) - else: - # Build comma-separated list of values - vlist = [] - for value in record: - if value is None and not list_type: - continue - value = fvalues[value] \ - if value in fvalues else none - vlist.append(value) - - if any([hasattr(v, "xml") for v in vlist]): - data = TAG[""]( - list( - chain.from_iterable( - [(v, ", ") for v in vlist]) - )[:-1] - ) - else: - data = ", ".join([s3_str(v) for v in vlist]) - - result[colname] = data - if raw_data: - result["_row"][colname] = list(record.keys()) - - # Restore linkto - if show_link is not None: - renderer.show_link = show_link - - return results - - # ------------------------------------------------------------------------- - def __getitem__(self, key): - """ - Helper method to access the results as dict items, for - backwards-compatibility - - @param key: the key - - @todo: migrate use-cases to . notation, then deprecate - """ - - if key in ("rfields", "numrows", "ids", "rows"): - return getattr(self, key) - else: - raise AttributeError - - # ------------------------------------------------------------------------- - @staticmethod - def getids(rows, pkey): - """ - Extract all unique record IDs from rows, preserving the - order by first match - - @param rows: the Rows - @param pkey: the primary key - - @return: list of unique record IDs - """ - - x = set() - seen = x.add - - result = [] - append = result.append - for row in rows: - row_id = row[pkey] - if row_id not in x: - seen(row_id) - append(row_id) - return result - - # ------------------------------------------------------------------------- - @staticmethod - def getrows(rows, ids, pkey): - """ - Select a subset of rows by their record IDs - - @param rows: the Rows - @param ids: the record IDs - @param pkey: the primary key - - @return: the subset (Rows) - """ - - if ids: - ids = set(ids) - subset = lambda row: row[pkey] in ids - else: - subset = lambda row: False - return rows.find(subset) - - # ------------------------------------------------------------------------- - @staticmethod - def subset(rows, ids, start=None, limit=None, has_id=True): - """ - Build a subset [start:limit] from rows and ids - - @param rows: the Rows - @param ids: all matching record IDs - @param start: start index of the page - @param limit: maximum length of the page - @param has_id: whether the Rows contain the primary key - - @return: tuple (rows, page), with: - rows = the Rows in the subset, in order - page = the record IDs in the subset, in order - """ - - if limit and start is None: - start = 0 - - if start is not None and limit is not None: - rows = rows[start:start+limit] - page = ids[start:start+limit] - - elif start is not None: - rows = rows[start:] - page = ids[start:] - - else: - page = ids - - return rows, page - - # ------------------------------------------------------------------------- - @staticmethod - def rfield_tables(rfield): - """ - Get the names of all tables that need to be joined for a field - - @param rfield: the field (S3ResourceField) - - @return: a set of tablenames - """ - - left = rfield.left - if left: - # => add all left joins required for that table - tablenames = set(j.first._tablename - for tn in left for j in left[tn]) - else: - # => we don't know any further left joins, - # but as a minimum we need to add this table - tablenames = {rfield.tname} - - return tablenames - - # ------------------------------------------------------------------------- - @staticmethod - def resolve_expression(expr): - """ - Resolve an orderby or groupby expression into its items - - @param expr: the orderby/groupby expression - """ - - if isinstance(expr, str): - items = expr.split(",") - elif not isinstance(expr, (list, tuple)): - items = [expr] - else: - items = expr - return items - -# END ========================================================================= diff --git a/modules/core/msg/base.py b/modules/core/msg/base.py index 42191fcb7e..f8f5c62603 100644 --- a/modules/core/msg/base.py +++ b/modules/core/msg/base.py @@ -46,17 +46,12 @@ import sys from io import StringIO +from lxml import etree from urllib import request as urllib2 from urllib.error import HTTPError, URLError from urllib.request import urlopen from urllib.parse import urlencode -try: - from lxml import etree -except ImportError: - sys.stderr.write("ERROR: lxml module needed for XML handling\n") - raise - from gluon import current, redirect, IS_IN_SET from gluon.html import * @@ -2639,7 +2634,7 @@ def apply_method(self, r, **attr): """ API entry point - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes for the request """ @@ -2654,7 +2649,7 @@ def compose(self, r, **attr): """ Generate a form to send a message - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes for the request """ diff --git a/modules/core/msg/notify.py b/modules/core/msg/notify.py index a4c8466d29..02348634f8 100644 --- a/modules/core/msg/notify.py +++ b/modules/core/msg/notify.py @@ -237,7 +237,7 @@ def send(cls, r, resource): notification message and send it - responds to POST?format=msg requests to the respective resource. - @param r: the S3Request + @param r: the CRUDRequest @param resource: the S3Resource """ diff --git a/modules/core/resource/__init__.py b/modules/core/resource/__init__.py new file mode 100644 index 0000000000..23f8f72cbf --- /dev/null +++ b/modules/core/resource/__init__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- + +from .bi import * +from .codec import * +#from .components import * +from .delete import * +from .exporter import * +from .importer import * +from .query import * +from .resource import * +#from .rfilter import * +#from .select import * +from .rtb import * +from .xml import * diff --git a/modules/core/resource/bi.py b/modules/core/resource/bi.py new file mode 100644 index 0000000000..114ef95b8d --- /dev/null +++ b/modules/core/resource/bi.py @@ -0,0 +1,1003 @@ +# -*- coding: utf-8 -*- + +""" Bulk Importer Tool + + @copyright: 2011-2021 (c) Sahana Software Foundation + @license: MIT + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +""" + +__all__ = ("S3BulkImporter", + ) + +import datetime +import json +import os + +from io import StringIO, BytesIO +from urllib import request as urllib2 +from urllib.error import HTTPError, URLError +from urllib.request import urlopen + +from gluon import current, SQLFORM +from gluon.storage import Storage +from gluon.tools import callback, fetch + +from ..tools import IS_JSONS3 + +# ============================================================================= +class S3BulkImporter(object): + """ + Import CSV files of data to pre-populate the database. + Suitable for use in Testing, Demos & Simulations + + http://eden.sahanafoundation.org/wiki/DeveloperGuidelines/PrePopulate + """ + + def __init__(self): + + import csv + from xml.sax.saxutils import unescape + + self.csv = csv + self.unescape = unescape + self.tasks = [] + # Some functions refer to a different resource + self.alternateTables = { + "hrm_group_membership": {"tablename": "pr_group_membership", + "prefix": "pr", + "name": "group_membership"}, + "hrm_person": {"tablename": "pr_person", + "prefix": "pr", + "name": "person"}, + "member_person": {"tablename": "pr_person", + "prefix": "pr", + "name": "person"}, + } + # Keep track of which resources have been customised so we don't do this twice + self.customised = [] + self.errorList = [] + self.resultList = [] + + # ------------------------------------------------------------------------- + def load_descriptor(self, path): + """ + Load the descriptor file and then all the import tasks in that file + into the task property. + The descriptor file is the file called tasks.cfg in path. + The file consists of a comma separated list of: + module, resource name, csv filename, xsl filename. + """ + + source = open(os.path.join(path, "tasks.cfg"), "r") + values = self.csv.reader(source) + for details in values: + if details == []: + continue + prefix = details[0][0].strip('" ') + if prefix == "#": # comment + continue + if prefix == "*": # specialist function + self.extract_other_import_line(path, details) + else: # standard CSV importer + self.extract_csv_import_line(path, details) + + # ------------------------------------------------------------------------- + def extract_csv_import_line(self, path, details): + """ + Extract the details for a CSV Import Task + """ + + num_args = len(details) + if num_args == 4 or num_args == 5: + # Remove any spaces and enclosing double quote + mod = details[0].strip('" ') + res = details[1].strip('" ') + folder = current.request.folder + + csv_filename = details[2].strip('" ') + if csv_filename[:7] == "http://": + csv = csv_filename + else: + (csv_path, csv_file) = os.path.split(csv_filename) + if csv_path != "": + path = os.path.join(folder, + "modules", + "templates", + csv_path) + # @todo: deprecate this block once migration completed + if not os.path.exists(path): + # Non-standard location (legacy template)? + path = os.path.join(folder, + "private", + "templates", + csv_path) + csv = os.path.join(path, csv_file) + + xslt_filename = details[3].strip('" ') + xslt_path = os.path.join(folder, + "static", + "formats", + "s3csv") + # Try the module directory in the templates directory first + xsl = os.path.join(xslt_path, mod, xslt_filename) + if os.path.exists(xsl) == False: + # Now try the templates directory + xsl = os.path.join(xslt_path, xslt_filename) + if os.path.exists(xsl) == False: + # Use the same directory as the csv file + xsl = os.path.join(path, xslt_filename) + if os.path.exists(xsl) == False: + self.errorList.append( + "Failed to find a transform file %s, Giving up." % xslt_filename) + return + + if num_args == 5: + extra_data = details[4] + else: + extra_data = None + self.tasks.append([1, mod, res, csv, xsl, extra_data]) + else: + self.errorList.append( + "prepopulate error: job not of length 4, ignored: %s" % str(details)) + + # ------------------------------------------------------------------------- + def extract_other_import_line(self, path, details): + """ + Store a single import job into the tasks property + *,function,filename,*extra_args + """ + + function = details[1].strip('" ') + filepath = None + if len(details) >= 3: + filename = details[2].strip('" ') + if filename != "": + (subfolder, filename) = os.path.split(filename) + if subfolder != "": + path = os.path.join(current.request.folder, + "modules", + "templates", + subfolder) + # @todo: deprecate this block once migration completed + if not os.path.exists(path): + # Non-standard location (legacy template)? + path = os.path.join(current.request.folder, + "private", + "templates", + subfolder) + filepath = os.path.join(path, filename) + + if len(details) >= 4: + extra_args = details[3:] + else: + extra_args = None + + self.tasks.append((2, function, filepath, extra_args)) + + # ------------------------------------------------------------------------- + def execute_import_task(self, task): + """ + Execute each import job, in order + """ + + # Disable min_length for password during prepop + current.auth.ignore_min_password_length() + + start = datetime.datetime.now() + if task[0] == 1: + s3db = current.s3db + response = current.response + error_string = "prepopulate error: file %s missing" + # Store the view + view = response.view + + #current.log.debug("Running job %s %s (filename=%s transform=%s)" % (task[1], + # task[2], + # task[3], + # task[4], + # )) + + prefix = task[1] + name = task[2] + tablename = "%s_%s" % (prefix, name) + if tablename in self.alternateTables: + details = self.alternateTables[tablename] + if "tablename" in details: + tablename = details["tablename"] + s3db.table(tablename) + if "loader" in details: + loader = details["loader"] + if loader is not None: + loader() + if "prefix" in details: + prefix = details["prefix"] + if "name" in details: + name = details["name"] + + try: + resource = s3db.resource(tablename) + except AttributeError: + # Table cannot be loaded + self.errorList.append("WARNING: Unable to find table %s import job skipped" % tablename) + return + + # Check if the source file is accessible + filename = task[3] + if filename[:7] == "http://": + req = urllib2.Request(url=filename) + try: + f = urlopen(req) + except HTTPError as e: + self.errorList.append("Could not access %s: %s" % (filename, e.read())) + return + except: + self.errorList.append(error_string % filename) + return + else: + csv = f + else: + try: + csv = open(filename, "rb") + except IOError: + self.errorList.append(error_string % filename) + return + + # Check if the stylesheet is accessible + try: + stylesheet = open(task[4], "r") + except IOError: + self.errorList.append(error_string % task[4]) + return + else: + stylesheet.close() + + if tablename not in self.customised: + # Customise the resource + customise = current.deployment_settings.customise_resource(tablename) + if customise: + from ..controller import CRUDRequest + request = CRUDRequest(prefix, name, current.request) + customise(request, tablename) + self.customised.append(tablename) + + extra_data = None + if task[5]: + try: + extradata = self.unescape(task[5], {"'": '"'}) + extradata = json.loads(extradata) + extra_data = extradata + except: + self.errorList.append("WARNING:5th parameter invalid, parameter %s ignored" % task[5]) + auth = current.auth + auth.rollback = True + try: + # @todo: add extra_data and file attachments + result = resource.import_xml(csv, + source_type = "csv", + stylesheet = task[4], + extra_data = extra_data, + ) + except SyntaxError as e: + self.errorList.append("WARNING: import error - %s (file: %s, stylesheet: %s)" % + (e, filename, task[4])) + auth.rollback = False + return + + error = result.error + if error: + # Must roll back if there was an error! + self.errorList.append("%s - %s: %s" % ( + task[3], resource.tablename, error)) + errors = current.xml.collect_errors(result.error_tree) + if errors: + self.errorList.extend(errors) + current.db.rollback() + else: + current.db.commit() + + auth.rollback = False + + # Restore the view + response.view = view + end = datetime.datetime.now() + duration = end - start + csv_name = task[3][task[3].rfind("/") + 1:] + duration = '{:.2f}'.format(duration.total_seconds()) + msg = "%s imported (%s sec)" % (csv_name, duration) + self.resultList.append(msg) + current.log.debug(msg) + + # ------------------------------------------------------------------------- + def execute_special_task(self, task): + """ + Execute import tasks which require a custom function, + such as import_role + """ + + start = datetime.datetime.now() + s3 = current.response.s3 + if task[0] == 2: + fun = task[1] + filepath = task[2] + extra_args = task[3] + if filepath is None: + if extra_args is None: + error = s3[fun]() + else: + error = s3[fun](*extra_args) + elif extra_args is None: + error = s3[fun](filepath) + else: + error = s3[fun](filepath, *extra_args) + if error: + self.errorList.append(error) + end = datetime.datetime.now() + duration = end - start + duration = '{:.2f}'.format(duration.total_seconds()) + msg = "%s completed (%s sec)" % (fun, duration) + self.resultList.append(msg) + current.log.debug(msg) + + # ------------------------------------------------------------------------- + @staticmethod + def _lookup_pe(entity): + """ + Convert an Entity to a pe_id + - helper for import_role + - assumes org_organisation.name unless specified + - entity needs to exist already + """ + + if "=" in entity: + pe_type, value = entity.split("=") + else: + pe_type = "org_organisation.name" + value = entity + pe_tablename, pe_field = pe_type.split(".") + + table = current.s3db.table(pe_tablename) + record = current.db(table[pe_field] == value).select(table.pe_id, + limitby = (0, 1) + ).first() + try: + pe_id = record.pe_id + except AttributeError: + current.log.warning("import_role cannot find pe_id for %s" % entity) + pe_id = None + + return pe_id + + # ------------------------------------------------------------------------- + def import_role(self, filename): + """ + Import Roles from CSV + """ + + # Check if the source file is accessible + try: + open_file = open(filename, "r") + except IOError: + return "Unable to open file %s" % filename + + auth = current.auth + acl = auth.permission + create_role = auth.s3_create_role + + def parse_acl(acl_str): + permissions = acl_str.split("|") + acl_value = 0 + for permission in permissions: + if permission == "READ": + acl_value |= acl.READ + if permission == "CREATE": + acl_value |= acl.CREATE + if permission == "UPDATE": + acl_value |= acl.UPDATE + if permission == "DELETE": + acl_value |= acl.DELETE + if permission == "REVIEW": + acl_value |= acl.REVIEW + if permission == "APPROVE": + acl_value |= acl.APPROVE + if permission == "PUBLISH": + acl_value |= acl.PUBLISH + if permission == "ALL": + acl_value |= acl.ALL + return acl_value + + reader = self.csv.DictReader(open_file) + roles = {} + acls = {} + args = {} + for row in reader: + if row != None: + row_get = row.get + role = row_get("role") + desc = row_get("description", "") + rules = {} + extra_param = {} + controller = row_get("controller") + if controller: + rules["c"] = controller + fn = row_get("function") + if fn: + rules["f"] = fn + table = row_get("table") + if table: + rules["t"] = table + oacl = row_get("oacl") + if oacl: + rules["oacl"] = parse_acl(oacl) + uacl = row_get("uacl") + if uacl: + rules["uacl"] = parse_acl(uacl) + #org = row_get("org") + #if org: + # rules["organisation"] = org + #facility = row_get("facility") + #if facility: + # rules["facility"] = facility + entity = row_get("entity") + if entity: + if entity == "any": + # Pass through as-is + pass + else: + # NB Entity here is *not* hierarchical! + try: + entity = int(entity) + except ValueError: + entity = self._lookup_pe(entity) + rules["entity"] = entity + flag = lambda s: bool(s) and s.lower() in ("1", "true", "yes") + hidden = row_get("hidden") + if hidden: + extra_param["hidden"] = flag(hidden) + system = row_get("system") + if system: + extra_param["system"] = flag(system) + protected = row_get("protected") + if protected: + extra_param["protected"] = flag(protected) + uid = row_get("uid") + if uid: + extra_param["uid"] = uid + if role in roles: + acls[role].append(rules) + else: + roles[role] = [role, desc] + acls[role] = [rules] + if len(extra_param) > 0 and role not in args: + args[role] = extra_param + for rulelist in roles.values(): + if rulelist[0] in args: + create_role(rulelist[0], + rulelist[1], + *acls[rulelist[0]], + **args[rulelist[0]]) + else: + create_role(rulelist[0], + rulelist[1], + *acls[rulelist[0]]) + + # ------------------------------------------------------------------------- + def import_user(self, filename): + """ + Import Users from CSV with an import Prep + """ + + current.response.s3.import_prep = current.auth.s3_import_prep + + current.s3db.add_components("auth_user", + auth_masterkey = "user_id", + ) + + user_task = [1, + "auth", + "user", + filename, + os.path.join(current.request.folder, + "static", + "formats", + "s3csv", + "auth", + "user.xsl" + ), + None + ] + self.execute_import_task(user_task) + + # ------------------------------------------------------------------------- + def import_feed(self, filename): + """ + Import RSS Feeds from CSV with an import Prep + """ + + stylesheet = os.path.join(current.request.folder, + "static", + "formats", + "s3csv", + "msg", + "rss_channel.xsl" + ) + + # 1st import any Contacts + current.response.s3.import_prep = current.s3db.pr_import_prep + user_task = [1, + "pr", + "contact", + filename, + stylesheet, + None + ] + self.execute_import_task(user_task) + + # Then import the Channels + user_task = [1, + "msg", + "rss_channel", + filename, + stylesheet, + None + ] + self.execute_import_task(user_task) + + # ------------------------------------------------------------------------- + def import_image(self, + filename, + tablename, + idfield, + imagefield + ): + """ + Import images, such as a logo or person image + + filename a CSV list of records and filenames + tablename the name of the table + idfield the field used to identify the record + imagefield the field to where the image will be added + + Example: + bi.import_image ("org_logos.csv", "org_organisation", "name", "logo") + and the file org_logos.csv may look as follows + id file + Sahana Software Foundation sahanalogo.jpg + American Red Cross icrc.gif + """ + + # Check if the source file is accessible + try: + open_file = open(filename, "r", encoding="utf-8") + except IOError: + return "Unable to open file %s" % filename + + prefix, name = tablename.split("_", 1) + + reader = self.csv.DictReader(open_file) + + db = current.db + s3db = current.s3db + audit = current.audit + table = s3db[tablename] + idfield = table[idfield] + base_query = (table.deleted == False) + fieldnames = [table._id.name, + imagefield + ] + # https://github.com/web2py/web2py/blob/master/gluon/sqlhtml.py#L1947 + for field in table: + if field.name not in fieldnames and field.writable is False \ + and field.update is None and field.compute is None: + fieldnames.append(field.name) + fields = [table[f] for f in fieldnames] + + # Get callbacks + get_config = s3db.get_config + onvalidation = get_config(tablename, "update_onvalidation") or \ + get_config(tablename, "onvalidation") + onaccept = get_config(tablename, "update_onaccept") or \ + get_config(tablename, "onaccept") + update_realm = get_config(tablename, "update_realm") + if update_realm: + set_realm_entity = current.auth.set_realm_entity + update_super = s3db.update_super + + for row in reader: + if row != None: + # Open the file + image = row["file"] + try: + # Extract the path to the CSV file, image should be in + # this directory, or relative to it + path = os.path.split(filename)[0] + imagepath = os.path.join(path, image) + open_file = open(imagepath, "rb") + except IOError: + current.log.error("Unable to open image file %s" % image) + continue + image_source = BytesIO(open_file.read()) + # Get the id of the resource + query = base_query & (idfield == row["id"]) + record = db(query).select(limitby = (0, 1), + *fields).first() + try: + record_id = record.id + except AttributeError: + current.log.error("Unable to get record %s of the resource %s to attach the image file to" % (row["id"], tablename)) + continue + # Create and accept the form + form = SQLFORM(table, record, fields=["id", imagefield]) + form_vars = Storage() + form_vars._formname = "%s/%s" % (tablename, record_id) + form_vars.id = record_id + source = Storage() + source.filename = imagepath + source.file = image_source + form_vars[imagefield] = source + if form.accepts(form_vars, onvalidation=onvalidation): + # Audit + audit("update", prefix, name, form=form, + record=record_id, representation="csv") + + # Update super entity links + update_super(table, form_vars) + + # Update realm + if update_realm: + set_realm_entity(table, form_vars, force_update=True) + + # Execute onaccept + callback(onaccept, form, tablename=tablename) + else: + for (key, error) in form.errors.items(): + current.log.error("error importing logo %s: %s %s" % (image, key, error)) + + return None # no error + + # ------------------------------------------------------------------------- + @staticmethod + def import_font(url): + """ + Install a Font + """ + + if url == "unifont": + #url = "http://unifoundry.com/pub/unifont-7.0.06/font-builds/unifont-7.0.06.ttf" + #url = "http://unifoundry.com/pub/unifont-10.0.07/font-builds/unifont-10.0.07.ttf" + url = "http://unifoundry.com/pub/unifont/unifont-13.0.01/font-builds/unifont-13.0.01.ttf" + # Rename to make version upgrades be transparent + filename = "unifont.ttf" + extension = "ttf" + else: + filename = url.split("/")[-1] + filename, extension = filename.rsplit(".", 1) + + if extension not in ("ttf", "gz", "zip"): + current.log.warning("Unsupported font extension: %s" % extension) + return + + filename = "%s.ttf" % filename + + font_path = os.path.join(current.request.folder, "static", "fonts") + if os.path.exists(os.path.join(font_path, filename)): + current.log.warning("Using cached copy of %s" % filename) + return + + # Download as we have no cached copy + + # Copy the current working directory to revert back to later + cwd = os.getcwd() + + # Set the current working directory + os.chdir(font_path) + try: + _file = fetch(url) + except URLError as exception: + current.log.error(exception) + # Revert back to the working directory as before. + os.chdir(cwd) + return + + if extension == "gz": + import tarfile + tf = tarfile.open(fileobj = StringIO(_file)) + tf.extractall() + + elif extension == "zip": + import zipfile + zf = zipfile.ZipFile(StringIO(_file)) + zf.extractall() + + else: + f = open(filename, "wb") + f.write(_file) + f.close() + + # Revert back to the working directory as before. + os.chdir(cwd) + + # ------------------------------------------------------------------------- + def import_remote_csv(self, url, prefix, resource, stylesheet): + """ Import CSV files from remote servers """ + + extension = url.split(".")[-1] + if extension not in ("csv", "zip"): + current.log.error("error importing remote file %s: invalid extension" % (url)) + return + + # Copy the current working directory to revert back to later + cwd = os.getcwd() + + # Shortcut + os_path = os.path + os_path_exists = os_path.exists + os_path_join = os_path.join + + # Create the working directory + TEMP = os_path_join(cwd, "temp") + if not os_path_exists(TEMP): # use web2py/temp/remote_csv as a cache + import tempfile + TEMP = tempfile.gettempdir() + temp_path = os_path_join(TEMP, "remote_csv") + if not os_path_exists(temp_path): + try: + os.mkdir(temp_path) + except OSError: + current.log.error("Unable to create temp folder %s!" % temp_path) + return + + filename = url.split("/")[-1] + if extension == "zip": + filename = filename.replace(".zip", ".csv") + if os_path_exists(os_path_join(temp_path, filename)): + current.log.warning("Using cached copy of %s" % filename) + else: + # Download if we have no cached copy + # Set the current working directory + os.chdir(temp_path) + try: + _file = fetch(url) + except URLError as exception: + current.log.error(exception) + # Revert back to the working directory as before. + os.chdir(cwd) + return + + if extension == "zip": + # Need to unzip + import zipfile + try: + myfile = zipfile.ZipFile(StringIO(_file)) + except zipfile.BadZipfile as exception: + # e.g. trying to download through a captive portal + current.log.error(exception) + # Revert back to the working directory as before. + os.chdir(cwd) + return + files = myfile.infolist() + for f in files: + filename = f.filename + extension = filename.split(".")[-1] + if extension == "csv": + _file = myfile.read(filename) + _f = open(filename, "w") + _f.write(_file) + _f.close() + break + myfile.close() + else: + f = open(filename, "w") + f.write(_file) + f.close() + + # Revert back to the working directory as before. + os.chdir(cwd) + + task = [1, prefix, resource, + os_path_join(temp_path, filename), + os_path_join(current.request.folder, + "static", + "formats", + "s3csv", + prefix, + stylesheet + ), + None + ] + self.execute_import_task(task) + + # ------------------------------------------------------------------------- + @staticmethod + def import_script(filename): + """ + Run a custom Import Script + + @ToDo: Report Errors during Script run to console better + """ + + from gluon.cfs import getcfs + from gluon.compileapp import build_environment + from gluon.restricted import restricted + + environment = build_environment(current.request, current.response, current.session) + environment["current"] = current + environment["auth"] = current.auth + environment["db"] = current.db + environment["gis"] = current.gis + environment["s3db"] = current.s3db + environment["settings"] = current.deployment_settings + + code = getcfs(filename, filename, None) + restricted(code, environment, layer=filename) + + # ------------------------------------------------------------------------- + def import_task(self, + task_name, + args_json = None, + vars_json = None + ): + """ + Import a Scheduled Task + """ + + # Store current value of Bulk + bulk = current.response.s3.bulk + # Set Bulk to true for this parse + current.response.s3.bulk = True + validator = IS_JSONS3() + if args_json: + task_args, error = validator(args_json) + if error: + self.errorList.append(error) + return + else: + task_args = [] + if vars_json: + all_vars, error = validator(vars_json) + if error: + self.errorList.append(error) + return + else: + all_vars = {} + # Restore bulk setting + current.response.s3.bulk = bulk + + kwargs = {} + task_vars = {} + options = ("function_name", + "start_time", + "next_run_time", + "stop_time", + "repeats", + "period", # seconds + "timeout", # seconds + "enabled", # None = Enabled + "group_name", + "ignore_duplicate", + "sync_output", + ) + for var in all_vars: + if var in options: + kwargs[var] = all_vars[var] + else: + task_vars[var] = all_vars[var] + + current.s3task.schedule_task(task_name.split(os.path.sep)[-1], # Strip the path + args = task_args, + vars = task_vars, + **kwargs + ) + + # ------------------------------------------------------------------------- + def import_xml(self, + filepath, + prefix, + resourcename, + dataformat, + source_type = None, + ): + """ + Import XML data using an XSLT: static/formats//import.xsl + Setting the source_type is possible + """ + + # Remove any spaces and enclosing double quote + prefix = prefix.strip('" ') + resourcename = resourcename.strip('" ') + + try: + source = open(filepath, "rb") + except IOError: + error_string = "prepopulate error: file %s missing" + self.errorList.append(error_string % filepath) + return + + stylesheet = os.path.join(current.request.folder, + "static", + "formats", + dataformat, + "import.xsl") + try: + xslt_file = open(stylesheet, "r") + except IOError: + error_string = "prepopulate error: file %s missing" + self.errorList.append(error_string % stylesheet) + return + else: + xslt_file.close() + + tablename = "%s_%s" % (prefix, resourcename) + resource = current.s3db.resource(tablename) + + if tablename not in self.customised: + # Customise the resource + customise = current.deployment_settings.customise_resource(tablename) + if customise: + from ..controller import CRUDRequest + request = CRUDRequest(prefix, resourcename, current.request) + customise(request, tablename) + self.customised.append(tablename) + + auth = current.auth + auth.rollback = True + try: + resource.import_xml(source, + stylesheet = stylesheet, + source_type = source_type, + ) + except SyntaxError as e: + self.errorList.append("WARNING: import error - %s (file: %s, stylesheet: %s/import.xsl)" % + (e, filepath, dataformat)) + auth.rollback = False + return + + if not resource.error: + current.db.commit() + else: + # Must roll back if there was an error! + error = resource.error + self.errorList.append("%s - %s: %s" % ( + filepath, tablename, error)) + errors = current.xml.collect_errors(resource) + if errors: + self.errorList.extend(errors) + current.db.rollback() + + auth.rollback = False + + # ------------------------------------------------------------------------- + def perform_tasks(self, path): + """ + Load and then execute the import jobs that are listed in the + descriptor file (tasks.cfg) + """ + + self.load_descriptor(path) + for task in self.tasks: + if task[0] == 1: + self.execute_import_task(task) + elif task[0] == 2: + self.execute_special_task(task) + +# END ========================================================================= diff --git a/modules/core/io/codec.py b/modules/core/resource/codec.py similarity index 99% rename from modules/core/io/codec.py rename to modules/core/resource/codec.py index 2f15950f2a..de5d282664 100644 --- a/modules/core/io/codec.py +++ b/modules/core/resource/codec.py @@ -67,7 +67,7 @@ def get_codec(cls, fmt): name = cls.CODECS.get(fmt) if name: - package = "core.io.codecs.%s" % fmt + package = "core.resource.codecs.%s" % fmt try: codec = getattr(__import__(package, fromlist=[name]), name) except (ImportError, AttributeError): diff --git a/modules/core/io/codecs/__init__.py b/modules/core/resource/codecs/__init__.py similarity index 100% rename from modules/core/io/codecs/__init__.py rename to modules/core/resource/codecs/__init__.py diff --git a/modules/core/io/codecs/card.py b/modules/core/resource/codecs/card.py similarity index 99% rename from modules/core/io/codecs/card.py rename to modules/core/resource/codecs/card.py index 42691fea1f..de4cfbb90e 100644 --- a/modules/core/io/codecs/card.py +++ b/modules/core/resource/codecs/card.py @@ -49,7 +49,7 @@ from gluon import current, HTTP -from ...model import S3Resource +from ...resource import S3Resource from ...tools import s3_str from ..codec import S3Codec diff --git a/modules/core/io/codecs/pdf.py b/modules/core/resource/codecs/pdf.py similarity index 99% rename from modules/core/io/codecs/pdf.py rename to modules/core/resource/codecs/pdf.py index 2f67186630..f8307bc3ee 100644 --- a/modules/core/io/codecs/pdf.py +++ b/modules/core/resource/codecs/pdf.py @@ -178,10 +178,10 @@ def encode(self, resource, **attr): Export data as a PDF document @param resource: the resource - @param attr: dictionary of keyword arguments, in s3_rest_controller + @param attr: dictionary of keyword arguments, in crud_controller passed through from the calling controller - @keyword request: the S3Request + @keyword request: the CRUDRequest @keyword method: "read" to not include a list view when no component is specified @keyword list_fields: fields to include in lists @@ -362,7 +362,7 @@ def get_html_flowable(self, rules, printable_width, styles=None): @param rules: the HTML (web2py helper class) or a callback to produce it. The callback receives the - S3Request as parameter. + CRUDRequest as parameter. @param printable_width: the printable width @param styles: styles for HTML=>PDF conversion """ diff --git a/modules/core/io/codecs/shp.py b/modules/core/resource/codecs/shp.py similarity index 100% rename from modules/core/io/codecs/shp.py rename to modules/core/resource/codecs/shp.py diff --git a/modules/core/io/codecs/svg.py b/modules/core/resource/codecs/svg.py similarity index 100% rename from modules/core/io/codecs/svg.py rename to modules/core/resource/codecs/svg.py diff --git a/modules/core/io/codecs/xls.py b/modules/core/resource/codecs/xls.py similarity index 100% rename from modules/core/io/codecs/xls.py rename to modules/core/resource/codecs/xls.py diff --git a/modules/core/resource/components.py b/modules/core/resource/components.py new file mode 100644 index 0000000000..3f49c5a0b8 --- /dev/null +++ b/modules/core/resource/components.py @@ -0,0 +1,385 @@ +# -*- coding: utf-8 -*- + +""" Lazy Components Loader + + @copyright: 2009-2021 (c) Sahana Software Foundation + @license: MIT + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +""" + +__all__ = ("S3Components", + ) + +import sys + +from gluon import current + +from .query import FS + +DEFAULT = lambda: None + +# ============================================================================= +class S3Components(object): + """ + Lazy component loader + """ + + def __init__(self, master, expose=None): + """ + Constructor + + @param master: the master resource (S3Resource) + @param expose: aliases of components to expose, defaults to + all configured components + """ + + self.master = master + + if expose is None: + hooks = current.s3db.get_hooks(master.tablename)[1] + if hooks: + self.exposed_aliases = set(hooks.keys()) + else: + self.exposed_aliases = set() + else: + self.exposed_aliases = set(expose) + + self._components = {} + self._exposed = {} + + self.links = {} + + # ------------------------------------------------------------------------- + def get(self, alias, default=None): + """ + Access a component resource by its alias; will load the + component if not loaded yet + + @param alias: the component alias + @param default: default to return if the alias is not defined + + @return: the component resource (S3Resource) + """ + + components = self._components + + component = components.get(alias) + if not component: + self.__load((alias,)) + return components.get(alias, default) + else: + db = current.db + table_alias = component._alias + if not getattr(db, table_alias, None): + setattr(db._aliased_tables, table_alias, component.table) + return component + + # ------------------------------------------------------------------------- + def __getitem__(self, alias): + """ + Access a component by its alias in key notation; will load the + component if not loaded yet + + @param alias: the component alias + + @return: the component resource (S3Resource) + + @raises: KeyError if the component is not defined + """ + + component = self.get(alias) + if component is None: + raise KeyError + else: + return component + + # ------------------------------------------------------------------------- + def __contains__(self, alias): + """ + Check if a component is defined for this resource + + @param alias: the alias to check + + @return: True|False whether the component is defined + """ + + return bool(self.get(alias)) + + # ------------------------------------------------------------------------- + @property + def loaded(self): + """ + Get all currently loaded components + + @return: dict {alias: resource} with loaded components + """ + return self._components + + # ------------------------------------------------------------------------- + @property + def exposed(self): + """ + Get all exposed components (=> will thus load them all) + + @return: dict {alias: resource} with exposed components + """ + + loaded = self._components + exposed = self._exposed + + missing = set() + for alias in self.exposed_aliases: + if alias not in exposed: + if alias in loaded: + exposed[alias] = loaded[alias] + else: + missing.add(alias) + + if missing: + self.__load(missing) + + return exposed + + # ------------------------------------------------------------------------- + # Methods kept for backwards-compatibility + # - to be deprecated + # - use-cases should explicitly address either .loaded or .exposed + # + def keys(self): + """ + Get the aliases of all exposed components ([alias]) + """ + return list(self.exposed.keys()) + + def values(self): + """ + Get all exposed components ([resource]) + """ + return list(self.exposed.values()) + + def items(self): + """ + Get all exposed components ([(alias, resource)]) + """ + return list(self.exposed.items()) + + # ------------------------------------------------------------------------- + def __load(self, aliases, force=False): + """ + Instantiate component resources + + @param aliases: iterable of aliases of components to instantiate + @param force: forced reload of components + + @return: dict of loaded components {alias: resource} + """ + + s3db = current.s3db + + master = self.master + + components = self._components + exposed = self._exposed + exposed_aliases = self.exposed_aliases + + links = self.links + + if aliases: + if force: + # Forced reload + new = aliases + else: + new = [alias for alias in aliases if alias not in components] + else: + new = None + + hooks = s3db.get_components(master.table, names=new) + if not hooks: + return {} + + for alias, hook in hooks.items(): + + filterby = hook.filterby + if alias is not None and filterby is not None: + table_alias = "%s_%s_%s" % (hook.prefix, + hook.alias, + hook.name, + ) + table = s3db.get_aliased(hook.table, table_alias) + hook.table = table + else: + table_alias = None + table = hook.table + + # Instantiate component resource + from .resource import S3Resource + component = S3Resource(table, + parent = master, + alias = alias, + linktable = hook.linktable, + include_deleted = master.include_deleted, + approved = master._approved, + unapproved = master._unapproved, + ) + + if table_alias: + component.tablename = hook.tablename + component._alias = table_alias + + # Copy hook properties to the component resource + component.pkey = hook.pkey + component.fkey = hook.fkey + + component.linktable = hook.linktable + component.lkey = hook.lkey + component.rkey = hook.rkey + component.actuate = hook.actuate + component.autodelete = hook.autodelete + component.autocomplete = hook.autocomplete + + #component.alias = alias + component.multiple = hook.multiple + component.defaults = hook.defaults + + # Component filter + if not filterby: + # Can use filterby=False to enforce table aliasing yet + # suppress component filtering, useful e.g. if the same + # table is declared as component more than once for the + # same master table (using different foreign keys) + component.filter = None + + else: + if callable(filterby): + # Callable to construct complex join filter + # => pass the (potentially aliased) component table + query = filterby(table) + elif isinstance(filterby, dict): + # Filter by multiple criteria + query = None + for k, v in filterby.items(): + if isinstance(v, FS): + # Match a field in the master table + # => identify the field + try: + rfield = v.resolve(master) + except (AttributeError, SyntaxError): + if current.response.s3.debug: + raise + else: + current.log.error(sys.exc_info()[1]) + continue + # => must be a real field in the master table + field = rfield.field + if not field or field.table != master.table: + current.log.error("Component filter for %s<=%s: " + "invalid lookup field '%s'" % + (master.tablename, alias, v.name)) + continue + subquery = (table[k] == field) + else: + is_list = isinstance(v, (tuple, list)) + if is_list and len(v) == 1: + filterfor = v[0] + is_list = False + else: + filterfor = v + if not is_list: + subquery = (table[k] == filterfor) + elif filterfor: + subquery = (table[k].belongs(set(filterfor))) + else: + continue + if subquery: + if query is None: + query = subquery + else: + query &= subquery + if query: + component.filter = query + + # Copy component properties to the link resource + link = component.link + if link is not None: + + link.pkey = component.pkey + link.fkey = component.lkey + + link.multiple = component.multiple + + link.actuate = component.actuate + link.autodelete = component.autodelete + + # Register the link table + links[link.name] = links[link.alias] = link + + # Register the component + components[alias] = component + + if alias in exposed_aliases: + exposed[alias] = component + + return components + + # ------------------------------------------------------------------------- + def reset(self, aliases=None, expose=DEFAULT): + """ + Detach currently loaded components, e.g. to force a reload + + @param aliases: aliases to remove, None for all + @param expose: aliases of components to expose (default: + keep previously exposed aliases), None for + all configured components + """ + + if expose is not DEFAULT: + if expose is None: + hooks = current.s3db.get_hooks(self.master.tablename)[1] + if hooks: + self.exposed_aliases = set(hooks.keys()) + else: + self.exposed_aliases = set() + else: + self.exposed_aliases = set(expose) + + if aliases: + + loaded = self._components + links = self.links + exposed = self._exposed + + for alias in aliases: + component = loaded.pop(alias, None) + if component: + link = component.link + for k, v in list(links.items()): + if v is link: + links.pop(k) + exposed.pop(alias, None) + else: + self._components = {} + self._exposed = {} + + self.links.clear() + +# END ========================================================================= diff --git a/modules/core/model/delete.py b/modules/core/resource/delete.py similarity index 93% rename from modules/core/model/delete.py rename to modules/core/resource/delete.py index 84f3873f9b..9165e60751 100644 --- a/modules/core/model/delete.py +++ b/modules/core/resource/delete.py @@ -2,8 +2,8 @@ """ S3 Record Deletion - @copyright: 2018-2021 (c) Sahana Software Foundation - @license: MIT + :copyright: 2018-2021 (c) Sahana Software Foundation + :license: MIT Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation @@ -52,10 +52,10 @@ def __init__(self, resource, archive=None, representation=None): """ Constructor - @param resource: the S3Resource to delete records from - @param archive: True|False to override global - security.archive_not_delete setting - @param representation: the request format (for audit, optional) + :param S3Resource resource: the resource to delete records from + :param bool archive: True|False to override global + security.archive_not_delete setting + :param str representation: the request format (for audit, optional) """ self.resource = resource @@ -97,13 +97,13 @@ def __call__(self, cascade=False, replaced_by=None, skip_undeletable=False): Main deletion process, deletes/archives all records in the resource - @param cascade: this is called as a cascade-action from another + :param cascade: this is called as a cascade-action from another process (e.g. another delete) - @param skip_undeletable: delete whatever is possible, skip - undeletable rows - @param replaced_by: dict of {replaced_id: replacement_id}, + :param replaced_by: dict of {replaced_id: replacement_id}, used by record merger to log which record has replaced which + :param skip_undeletable: delete whatever is possible, skip + undeletable rows """ # Must not re-use instance @@ -262,7 +262,9 @@ def extract(self): """ Extract the rows to be deleted - @returns: a Rows instance + :returns: a Rows instance + + :meta private: """ table = self.table @@ -286,13 +288,14 @@ def check_deletable(self, rows, check_all=False): """ Check which rows in the set are deletable, collect all errors - @param rows: the Rows to be deleted - @param check_all: find all restrictions for each record + :param rows: the Rows to be deleted + :param check_all: find all restrictions for each record rather than from just one table (not standard because of performance cost) - - @returns: array of Rows found to be deletable + :returns: array of Rows found to be deletable NB those can still fail further down the cascade + + :meta private: """ db = current.db @@ -360,8 +363,9 @@ def cascade(self, row, check_all=False): Run the automatic deletion cascade: remove or update records referencing this row with ondelete!="RESTRICT" - @param row: the Row to delete - @param check_all: process the entire cascade to reveal all + :meta private: + :param row: the Row to delete + :param check_all: process the entire cascade to reveal all errors (rather than breaking out of it after the first error) """ @@ -440,6 +444,8 @@ def auto_delete_linked(self, row): Auto-delete linked records if row was the last link @param row: the Row about to get deleted + + :meta private: """ resource = self.resource @@ -485,12 +491,13 @@ def archive_record(self, row, replaced_by=None): """ Archive ("soft-delete") a record - @param row: the Row to delete - @param replaced_by: dict of {replaced_id: replacement_id}, - used by record merger to log which record - has replaced which + :param row: the Row to delete + :param replaced_by: dict of {replaced_id: replacement_id}, used \ + by record merger to log which record has replaced which - @returns: True for success, False on error + :returns: True for success, False on error + + :meta private: """ table = self.table @@ -539,6 +546,8 @@ def delete_record(self, row): @param row: the Row to delete @returns: True for success, False on error + + :meta private: """ table = self.table @@ -567,6 +576,8 @@ def super_keys(self): List of super-keys (instance links) in this resource @returns: a list of field names + + :meta private: """ super_keys = self._super_keys @@ -603,6 +614,8 @@ def foreign_keys(self): List of foreign key fields in this resource @returns: a list of field names + + :meta private: """ # Produce a list of foreign key Fields in self.table @@ -620,10 +633,12 @@ def foreign_keys(self): @property def references(self): """ - A list of foreign keys referencing this resource, - lazy property + A list of foreign keys referencing this resource, + lazy property - @returns: a list of Fields + @returns: a list of Fields + + :meta private: """ references = self._references @@ -642,6 +657,8 @@ def restrictions(self): ondelete="RESTRICT", lazy property @returns: a list of Fields + + :meta private: """ restrictions = self._restrictions @@ -656,6 +673,8 @@ def restrictions(self): def introspect(self): """ Introspect the resource to set process properties + + :meta private: """ # Must load all models to detect dependencies @@ -687,6 +706,8 @@ def add_error(self, record_id, msg): @param record_id: the record ID @param msg: the error message + + :meta private: """ key = (self.tablename, record_id) @@ -703,6 +724,8 @@ def add_error(self, record_id, msg): def set_resource_error(self): """ Set the resource.error + + :meta private: """ if not self.errors: @@ -718,6 +741,8 @@ def set_resource_error(self): def log_errors(self): """ Log all errors of this process instance + + :meta private: """ if not self.errors: @@ -736,6 +761,8 @@ def _log(cls, master, reference, errors): @param master: the master log message @param reference: the prefix for the sub-message @param errors: the errors + + :meta private: """ log = current.log.error diff --git a/modules/core/io/exporter.py b/modules/core/resource/exporter.py similarity index 100% rename from modules/core/io/exporter.py rename to modules/core/resource/exporter.py diff --git a/modules/core/io/importer.py b/modules/core/resource/importer.py similarity index 65% rename from modules/core/io/importer.py rename to modules/core/resource/importer.py index 6197a795de..c0eb0e6484 100644 --- a/modules/core/io/importer.py +++ b/modules/core/resource/importer.py @@ -27,2216 +27,2537 @@ OTHER DEALINGS IN THE SOFTWARE. """ -__all__ = ("S3ImportJob", - "S3ImportItem", +__all__ = ("XMLImporter", + "ImportJob", + "ImportItem", + "SyncPolicy", "S3Duplicate", - "S3BulkImporter", ) import datetime import json import pickle -import os import sys import uuid from copy import deepcopy -from io import StringIO, BytesIO +from io import StringIO from lxml import etree -from urllib import request as urllib2 -from urllib.error import HTTPError, URLError -from urllib.request import urlopen -from gluon import current, IS_EMPTY_OR, SQLFORM +from gluon import current, IS_EMPTY_OR from gluon.storage import Storage -from gluon.tools import callback, fetch +from gluon.tools import callback from s3dal import Field -from ..controller import S3Request -from ..model import S3Resource -from ..tools import s3_utc, s3_get_foreign_key, s3_has_foreign_key, \ - s3_str, IS_JSONS3 +from ..tools import s3_utc, s3_get_foreign_key, s3_has_foreign_key, s3_str, s3_format_datetime # ============================================================================= -class S3ImportItem(object): - """ Class representing an import item (=a single record) """ +class XMLImporter(object): + """ S3XML Importer Utility """ - METHOD = Storage( - CREATE = "create", - UPDATE = "update", - DELETE = "delete", - MERGE = "merge" - ) + # ------------------------------------------------------------------------- + @classmethod + def parse_source(cls, + tablename, + source, + source_type = "xml", + stylesheet = None, + extra_data = None, + **args): + """ + Parse a data source for import, and convert it into a S3XML + element tree. + + :param tablename: the name of the target table + :param source: the data source; accepts a single source, a list of + sources or a list of tuples (name, source); each + source must be either an ElementTree or a file-like + object + :param str source_type: the source type (xml|json|csv|xls|xlsx) + :param stylesheet: the transformation stylesheet + :param extra_data: for CSV imports, dict of extra columns to add + to each row + :param args: parameters to pass to the transformation stylesheet + """ - POLICY = Storage( - THIS = "THIS", # keep local instance - OTHER = "OTHER", # update unconditionally - NEWER = "NEWER", # update if import is newer - MASTER = "MASTER" # update if import is master - ) + xml = current.xml + tree = None + + if not isinstance(source, (list, tuple)): + source = [source] + + for item in source: + + if isinstance(item, (list, tuple)): + name, s = item[:2] + else: + name, s = None, item + + if isinstance(s, etree._ElementTree): + t = s + elif source_type == "json": + if isinstance(s, str): + t = xml.json2tree(StringIO(s)) + else: + t = xml.json2tree(s) + elif source_type == "csv": + t = xml.csv2tree(s, resourcename=name, extra_data=extra_data) + elif source_type == "xls": + t = xml.xls2tree(s, resourcename=name, extra_data=extra_data) + elif source_type == "xlsx": + t = xml.xlsx2tree(s, resourcename=name, extra_data=extra_data) + else: + t = xml.parse(s) + + if not t: + if xml.error: + raise SyntaxError(xml.error) + else: + raise SyntaxError("Invalid source") + + if stylesheet is not None: + prefix, name = tablename.split("_", 1) + args.update(domain = xml.domain, + base_url = current.response.s3.base_url, + prefix = prefix, + name = name, + utcnow = s3_format_datetime(), + ) + t = xml.transform(t, stylesheet, **args) + if not t: + raise SyntaxError(xml.error) + + if not tree: + tree = t.getroot() + else: + tree.extend(list(t.getroot())) + + return tree # ------------------------------------------------------------------------- - def __init__(self, job): + @classmethod + def import_tree(cls, + tablename, + tree, + files = None, + record_id = None, + components = None, + commit = True, + ignore_errors = False, + job_id = None, + select_items = None, + strategy = None, + sync_policy = None, + ): """ - Constructor + Import data from an S3XML element tree. - @param job: the import job this item belongs to - """ + :param str tablename: the name of the target table + :param ElementTree tree: the S3XML element tree + :param dict files: file attachments referenced by the tree + :param record_id: the target record ID + :param list components: list of importable components + :param commit: commit the import job, if False, the import job + will be rolled back and stored for committing at + a later time + :param ignore_errors: ignore any errors, import what is possible + :param job_id: the job UID, to restore and commit a previously + stored import job + :param list select_items: only restore these items from the job + (list of import item record IDs) - self.job = job + :param strategy: list of allowed import methods + :param SyncPolicy sync_policy: the synchronization policy + """ - # Locking and error handling - self.lock = False - self.error = None + db = current.db + s3db = current.s3db - # Identification - self.item_id = uuid.uuid4() # unique ID for this item - self.id = None - self.uid = None + s3 = current.response.s3 - # Data elements - self.table = None - self.tablename = None - self.element = None - self.data = None - self.original = None - self.components = [] - self.references = [] - self.load_components = [] - self.load_references = [] - self.parent = None - self.skip = False + table = s3db.table(tablename) + if not table or "id" not in table.fields: + return ImportResult(False, current.ERROR.BAD_RESOURCE) - # Conflict handling - self.mci = 2 - self.mtime = datetime.datetime.utcnow() - self.modified = True - self.conflict = False + if tree is not None: + # Run import_prep callback + import_prep = s3.import_prep + if import_prep: + if not isinstance(tree, etree._ElementTree): + tree = etree.ElementTree(tree) + callback(import_prep, tree, tablename=tablename) + + # Select matching elements from tree + elements = cls.matching_elements(tree, tablename, record_id=record_id) + if not elements: + # Nothing to import + # - this is only an error if an update of a specific record + # was expected + error = current.ERROR.NO_MATCH if record_id else None + return ImportResult(not record_id, error) + + # Create import job + import_job = ImportJob(table, + tree = tree, + files = files, + strategy = strategy, + sync_policy = sync_policy, + ) - # Allowed import methods - self.strategy = job.strategy - # Update and conflict resolution policies - self.update_policy = job.update_policy - self.conflict_policy = job.conflict_policy + # Add import items for matching elements + error = None + s3.bulk = True + add_item = import_job.add_item + for element in elements: + success = add_item(element = element, + components = components, + ) + if not success: + error = import_job.error + if error and not ignore_errors: + s3.bulk = False + return ImportResult(False, error, job=import_job) - # Actual import method - self.method = None + elif not commit: + raise ValueError("Element tree required for trial import") - self.onvalidation = None - self.onaccept = None + elif job_id is not None: - # Item import status flags - self.accepted = None - self.permitted = False - self.committed = False + # Re-instate the stored import job + try: + import_job = ImportJob(table, + job_id = job_id, + strategy = strategy, + sync_policy = sync_policy, + ) + except SyntaxError: + return ImportResult(False, current.ERROR.BAD_SOURCE) + + # Select items for target table + item_table = s3db.s3_import_item + query = (item_table.job_id == job_id) + if select_items: + # Limit to selected items for the resource table + query &= (item_table.tablename != tablename) | \ + (item_table.id.belongs(select_items)) + items = db(query).select() + + # Restore the items and references + s3.bulk = True + load_item = import_job.load_item + error = None + for item in items: + success = load_item(item) + if not success: + error = import_job.error + import_job.restore_references() + if error and not ignore_errors: + s3.bulk = False + return ImportResult(False, error) + + # Run import_prep callback + import_prep = s3.import_prep + if import_prep: + tree = import_job.get_tree() + callback(import_prep, tree, tablename=tablename) - # Writeback hook for circular references: - # Items which need a second write to update references - self.update = [] + else: + raise ValueError("Element tree or job ID required") - # ------------------------------------------------------------------------- - def __repr__(self): - """ Helper method for debugging """ + # Commit the import job + s3.bulk = True + auth = current.auth + auth.rollback = not commit + success = import_job.commit(ignore_errors=ignore_errors) + auth.rollback = False + s3.bulk = False + + # Rollback on failure or if so requested + if not success or not commit: + db.rollback() + + # Prepare result + error = import_job.error + if error: + if ignore_errors: + error = "%s - invalid items ignored" % import_job.error + elif not success: + raise RuntimeError("Import failed without error message") + result = ImportResult(error is None or ignore_errors, + error = error, + job = import_job, + ) + + if not commit: + # Save the job + import_job.store() + else: + # Delete the import job when committed + import_job.delete() + result.job_id = None - _str = "" % \ - (self.table, self.item_id, self.uid, self.id, self.error, self.data) - return _str + return result # ------------------------------------------------------------------------- - def parse(self, - element, - original = None, - table = None, - tree = None, - files = None - ): + @staticmethod + def matching_elements(tree, tablename, record_id=None): """ - Read data from a element + Find elements in the source tree that belong to the target + record, or the target table if no record is specified. - @param element: the element - @param table: the DB table - @param tree: the import tree - @param files: uploaded files + :param ElementTree tree: the source tree + :param str tablename: the name of the target table + :param int record_id: the target record ID - @return: True if successful, False if not (sets self.error) + :returns list: list of matching elements, or None """ - s3db = current.s3db xml = current.xml - ERROR = xml.ATTRIBUTE["error"] - - self.element = element - if table is None: - tablename = element.get(xml.ATTRIBUTE["name"]) - table = s3db.table(tablename) - if table is None: - self.error = current.ERROR.BAD_RESOURCE - element.set(ERROR, s3_str(self.error)) - return False - else: - tablename = table._tablename + db = current.db - self.table = table - self.tablename = tablename + # Select the elements for this table + elements = xml.select_resources(tree, tablename) + if not elements: + return None + # Find matching elements, if a target record ID is given UID = xml.UID + table = current.s3db[tablename] + if record_id and UID in table: - if original is None: - original = S3Resource.original(table, element, - mandatory = self._mandatory_fields()) - elif isinstance(original, str) and UID in table.fields: - # Single-component update in add-item => load the original now - query = (table[UID] == original) - pkeys = set(fname for fname in table.fields if table[fname].unique) - fields = S3Resource.import_fields(table, pkeys, - mandatory = self._mandatory_fields()) - original = current.db(query).select(limitby=(0, 1), *fields).first() - else: - original = None - - postprocess = s3db.get_config(tablename, "xml_post_parse") - data = xml.record(table, element, - files = files, - original = original, - postprocess = postprocess) - - if data is None: - self.error = current.ERROR.VALIDATION_ERROR - self.accepted = False - if not element.get(ERROR, False): - element.set(ERROR, s3_str(self.error)) - return False - - self.data = data + if not isinstance(record_id, (tuple, list)): + query = (table._id == record_id) + else: + query = (table._id.belongs(record_id)) + originals = db(query).select(table[UID]) - MCI = xml.MCI - MTIME = xml.MTIME + uids = [row[UID] for row in originals] - self.uid = data.get(UID) - if original is not None: + matches = [] + import_uid = xml.import_uid + append = matches.append + for element in elements: + element_uid = import_uid(element.get(UID, None)) + if not element_uid: + continue + if element_uid in uids: + append(element) + if not matches: + first = elements[0] + if len(elements) and not first.get(UID, None): + first.set(UID, uids[0]) + matches = [first] + elements = matches - self.original = original - self.id = original[table._id.name] + return elements if elements else None - if not current.response.s3.synchronise_uuids and UID in original: - self.uid = self.data[UID] = original[UID] +# ============================================================================= +class ImportResult(object): + """ + Result of an ImportJob + """ - if MTIME in data: - self.mtime = data[MTIME] - if MCI in data: - self.mci = data[MCI] + def __init__(self, success, error=None, job=None): + """ + :param bool success: whether the job was successful + :param str error: error message + :param ImportJob job: the ImportJob + """ - #current.log.debug("New item: %s" % self) - return True + self.success = success + self.error = error + if job: + self.job_id = job.job_id + self.count = job.count + self.failed = job.errors + self.created = job.created + self.updated = job.updated + self.deleted = job.deleted + self.mtime = job.mtime + self.error_tree = job.error_tree + else: + self.job_id = None + self.count = 0 + self.failed = 0 + self.created = [] + self.updated = [] + self.deleted = [] + self.mtime = None + self.error_tree = None # ------------------------------------------------------------------------- - def deduplicate(self): + def json_message(self): """ - Detect whether this is an update or a new record + Generate a JSON message from this result + + :returns str: the JSON message """ - table = self.table - if table is None or self.id: - return + xml = current.xml - METHOD = self.METHOD - CREATE = METHOD["CREATE"] - UPDATE = METHOD["UPDATE"] - DELETE = METHOD["DELETE"] - MERGE = METHOD["MERGE"] + if self.error_tree is not None: + tree = xml.tree2json(self.error_tree) + else: + tree = None + + # Import Summary Info + info = {"records": self.count, + } + if self.created: + info["created"] = list(set(self.created)) + if self.updated: + info["updated"] = list(set(self.updated)) + if self.deleted: + info["deleted"] = list(set(self.deleted)) + + if self.success: + msg = xml.json_message(message = self.error, + tree = tree, + **info) + else: + msg = xml.json_message(False, 400, + message = self.error, + tree = tree, + ) + return msg - xml = current.xml - UID = xml.UID +# ============================================================================= +class ImportJob(): + """ + Class to import an element tree into the database + """ - data = self.data - if self.job.second_pass and UID in table.fields: - uid = data.get(UID) - if uid and not self.element.get(UID) and not self.original: - # Previously identified original does no longer exist - del data[UID] + def __init__(self, + table, + tree = None, + files = None, + job_id = None, + strategy = None, + sync_policy = None, + ): + """ + :param tree: the element tree to import + :param files: files attached to the import (for upload fields) + :param job_id: restore job from database (record ID or job_id) + :param strategy: the import strategy + :param sync_policy: the synchronization policy + """ - mandatory = self._mandatory_fields() + self.error = None # the last error + self.error_tree = etree.Element(current.xml.TAG.root) - if self.original is not None: - original = self.original - elif self.data: - original = S3Resource.original(table, - self.data, - mandatory=mandatory, - ) - else: - original = None + self.table = table + self.tree = tree + self.files = files + self.directory = Storage() - synchronise_uuids = current.response.s3.synchronise_uuids + self._uidmap = None - deleted = data[xml.DELETED] - if deleted: - if data[xml.REPLACEDBY]: - self.method = MERGE - else: - self.method = DELETE + # Mandatory fields + self.mandatory_fields = Storage() - self.uid = data.get(UID) + self.elements = Storage() + self.items = Storage() + self.references = [] - if original is not None: + self.count = 0 # total number of records imported + self.errors = 0 # total number of records in error + self.created = [] # IDs of created records + self.updated = [] # IDs of updated records + self.deleted = [] # IDs of deleted records - # The original record could be identified by a unique-key-match, - # so this must be an update - self.id = original[table._id.name] + self.log = None - if not deleted: - self.method = UPDATE + # Import strategy + if strategy is None: + METHOD = ImportItem.METHOD + strategy = [METHOD.CREATE, + METHOD.UPDATE, + METHOD.DELETE, + METHOD.MERGE, + ] + if not isinstance(strategy, (tuple, list)): + strategy = [strategy] + self.strategy = strategy + # Synchronization settings + if sync_policy: + self.update_policy = sync_policy.update_policy or SyncPolicy.OTHER + self.conflict_policy = sync_policy.conflict_policy or SyncPolicy.MASTER + self.last_sync = sync_policy.last_sync + self.onconflict = sync_policy.onconflict else: + self.update_policy = SyncPolicy.OTHER + self.conflict_policy = SyncPolicy.MASTER + self.last_sync = None + self.onconflict = None - if UID in data and not synchronise_uuids: - # The import item has a UUID but there is no match - # in the database, so this must be a new record - self.id = None - if not deleted: - self.method = CREATE - else: - # Nonexistent record to be deleted => skip - self.method = DELETE - self.skip = True + self.mtime = None + if job_id: + s3db = current.s3db + jobtable = s3db.s3_import_job + if str(job_id).isdigit(): + query = (jobtable.id == job_id) else: - # Use the resource's deduplicator to identify the original - resolve = current.s3db.get_config(self.tablename, "deduplicate") - if data and resolve: - resolve(self) - - if self.id and self.method in (UPDATE, DELETE, MERGE): - # Retrieve the original - fields = S3Resource.import_fields(table, - data, - mandatory=mandatory, - ) - original = current.db(table._id == self.id) \ - .select(limitby=(0, 1), *fields).first() - - # Retain the original UUID (except in synchronise_uuids mode) - if original and not synchronise_uuids and UID in original: - self.uid = data[UID] = original[UID] - - self.original = original + query = (jobtable.job_id == job_id) + row = current.db(query).select(jobtable.job_id, + jobtable.tablename, + limitby=(0, 1)).first() + if not row: + raise SyntaxError("Job record not found") + self.job_id = row.job_id + self.second_pass = True + if not self.table: + tablename = row.tablename + try: + table = s3db[tablename] + except AttributeError: + pass + else: + self.job_id = uuid.uuid4() # unique ID for this job + self.second_pass = False # ------------------------------------------------------------------------- - def authorize(self): + @property + def uidmap(self): """ - Authorize the import of this item, sets self.permitted + Map uuid/tuid => element, for faster reference lookups """ - if not self.table: - return False - - auth = current.auth - tablename = self.tablename + uidmap = self._uidmap + tree = self.tree - # Check whether self.table is protected - if not auth.override and tablename.split("_", 1)[0] in auth.PROTECTED: - return False + if uidmap is None and tree is not None: - # Determine the method - METHOD = self.METHOD - if self.data.deleted is True: - if self.data.deleted_rb: - self.method = METHOD["MERGE"] - else: - self.method = METHOD["DELETE"] - self.accepted = True if self.id else False - elif self.id: - if not self.original: - fields = S3Resource.import_fields(self.table, self.data, - mandatory=self._mandatory_fields()) - query = (self.table.id == self.id) - self.original = current.db(query).select(limitby=(0, 1), - *fields).first() - if self.original: - self.method = METHOD["UPDATE"] - else: - self.method = METHOD["CREATE"] - else: - self.method = METHOD["CREATE"] + root = tree if isinstance(tree, etree._Element) else tree.getroot() - # Set self.id - if self.method == METHOD["CREATE"]: - self.id = 0 + xml = current.xml + UUID = xml.UID + TUID = xml.ATTRIBUTE.tuid + NAME = xml.ATTRIBUTE.name - # Authorization - authorize = current.auth.s3_has_permission - if authorize: - self.permitted = authorize(self.method, - tablename, - record_id=self.id) - else: - self.permitted = True + elements = root.xpath(".//%s" % xml.TAG.resource) + self._uidmap = uidmap = {UUID: {}, + TUID: {}, + } + uuidmap = uidmap[UUID] + tuidmap = uidmap[TUID] + for element in elements: + name = element.get(NAME) + r_uuid = element.get(UUID) + if r_uuid and r_uuid not in uuidmap: + uuidmap[(name, r_uuid)] = element + r_tuid = element.get(TUID) + if r_tuid and r_tuid not in tuidmap: + tuidmap[(name, r_tuid)] = element - return self.permitted + return uidmap # ------------------------------------------------------------------------- - def validate(self): + def add_item(self, + element = None, + original = None, + components = None, + parent = None, + joinby = None): """ - Validate this item (=record onvalidation), sets self.accepted + Parse and validate an XML element and add it as new item + to the job. + + :param element: the element + :param original: the original DB record (if already available, + will otherwise be looked-up by this function) + :param components: a dictionary of components (as in S3Resource) + to include in the job (defaults to all + defined components) + :param parent: the parent item (if this is a component) + :param joinby: the component join key(s) (if this is a component) + + :returns: a unique identifier for the new item, or None if there + was an error. self.error contains the last error, and + self.error_tree an element tree with all failing elements + including error attributes. """ - data = self.data + if element in self.elements: + # element has already been added to this job + return self.elements[element] - if self.accepted is not None: - return self.accepted - if data is None or not self.table: - self.accepted = False - return False + # Parse the main element + item = ImportItem(self) - xml = current.xml - ERROR = xml.ATTRIBUTE["error"] + # Update lookup lists + item_id = item.item_id + self.items[item_id] = item + if element is not None: + self.elements[element] = item_id - METHOD = self.METHOD - DELETE = METHOD.DELETE - MERGE = METHOD.MERGE + if not item.parse(element, + original = original, + files = self.files): + self.error = item.error + item.accepted = False + if parent is None: + self.error_tree.append(deepcopy(item.element)) - # Detect update - if not self.id: - self.deduplicate() - if self.accepted is False: - # Item rejected by deduplicator (e.g. due to ambiguity) - return False + else: + # Now parse the components + table = item.table - # Don't need to validate skipped or deleted records - if self.skip or self.method in (DELETE, MERGE): - self.accepted = True if self.id else False - return True + s3db = current.s3db + components = s3db.get_components(table, names=components) + super_keys = s3db.get_super_keys(table) - # Set dynamic defaults for new records - if not self.id: - self._dynamic_defaults(data) + cnames = Storage() + cinfos = Storage() + for alias in components: - # Check for mandatory fields - required_fields = self._mandatory_fields() + component = components[alias] - all_fields = list(data.keys()) + ctable = component.table + if ctable._id != "id" and "instance_type" in ctable.fields: + # Super-entities cannot be imported to directly => skip + continue - failed_references = [] - items = self.job.items - for reference in self.references: - resolvable = resolved = True - entry = reference.entry - if entry and not entry.id: - if entry.item_id: - item = items[entry.item_id] - if item.error: - relement = reference.element - if relement is not None: - # Repeat the errors from the referenced record - # in the element (better reasoning) - msg = "; ".join(xml.collect_errors(entry.element)) - relement.set(ERROR, msg) - else: - resolvable = False - resolved = False - else: - resolvable = resolved = False - field = reference.field - if isinstance(field, (tuple, list)): - field = field[1] - if resolved: - all_fields.append(field) - elif resolvable: - # Both reference and referenced record are in the XML, - # => treat foreign key as mandatory, and mark as failed - if field not in required_fields: - required_fields.append(field) - if field not in failed_references: - failed_references.append(field) + # Determine the keys + pkey = component.pkey - missing = [fname for fname in required_fields - if fname not in all_fields] + if pkey != table._id.name and pkey not in super_keys: + # Pseudo-component cannot be imported => skip + continue - original = self.original - if missing: - if original: - missing = [fname for fname in missing - if fname not in original] - if missing: - fields = [f for f in missing - if f not in failed_references] - if fields: - errors = ["%s: value(s) required" % ", ".join(fields)] + if component.linktable: + ctable = component.linktable + fkey = component.lkey else: - errors = [] - if failed_references: - fields = ", ".join(failed_references) - errors.append("%s: reference import(s) failed" % - ", ".join(failed_references)) - self.error = "; ".join(errors) - self.element.set(ERROR, self.error) - self.accepted = False - return False + fkey = component.fkey - # Run onvalidation - form = Storage(method = self.method, - vars = data, - request_vars = data, - # Useless since always incomplete: - #record = original, - ) - if self.id: - form.vars.id = self.id - - form.errors = Storage() - tablename = self.tablename - key = "%s_onvalidation" % self.method - get_config = current.s3db.get_config - onvalidation = get_config(tablename, key, - get_config(tablename, "onvalidation")) - if onvalidation: - try: - callback(onvalidation, form, tablename=tablename) - except: - from traceback import format_exc - current.log.error("S3Import %s onvalidation exception:" % tablename) - current.log.debug(format_exc(10)) - accepted = True - if form.errors: - element = self.element - for k in form.errors: - e = element.findall("data[@field='%s']" % k) - if not e: - e = element.findall("reference[@field='%s']" % k) - if not e: - e = element - form.errors[k] = "[%s] %s" % (k, form.errors[k]) + ctablename = ctable._tablename + if ctablename in cnames: + cnames[ctablename].append(alias) else: - e = e[0] - e.set(ERROR, s3_str(form.errors[k])) - self.error = current.ERROR.VALIDATION_ERROR - accepted = False + cnames[ctablename] = [alias] - self.accepted = accepted - return accepted + cinfos[(ctablename, alias)] = Storage(component = component, + ctable = ctable, + pkey = pkey, + fkey = fkey, + first = True, + ) + add_item = self.add_item + xml = current.xml + UID = xml.UID + for celement in xml.components(element, names=list(cnames.keys())): - # ------------------------------------------------------------------------- - def commit(self, ignore_errors=False): - """ - Commit this item to the database + # Get the component tablename + ctablename = celement.get(xml.ATTRIBUTE.name, None) + if not ctablename or ctablename not in cnames: + continue - @param ignore_errors: skip invalid components - (still reports errors) - """ + # Get the component alias (for disambiguation) + calias = celement.get(xml.ATTRIBUTE.alias, None) + if calias is None: + aliases = cnames[ctablename] + if len(aliases) == 1: + calias = aliases[0] + else: + calias = ctablename.split("_", 1)[1] - if self.committed: - # already committed - return True + if (ctablename, calias) not in cinfos: + continue + else: + cinfo = cinfos[(ctablename, calias)] - # If the parent item gets skipped, then skip this item as well - if self.parent is not None and self.parent.skip: - return True + component = cinfo.component + ctable = cinfo.ctable - # Globals - db = current.db - s3db = current.s3db + pkey = cinfo.pkey + fkey = cinfo.fkey - xml = current.xml - ATTRIBUTE = xml.ATTRIBUTE + original = None - # Methods - METHOD = self.METHOD - CREATE = METHOD.CREATE - UPDATE = METHOD.UPDATE - DELETE = METHOD.DELETE - MERGE = METHOD.MERGE + if not component.multiple: + # Single-component: skip all subsequent items after + # the first under the same master record + if not cinfo.first: + continue + cinfo.first = False - # Policies - POLICY = self.POLICY - THIS = POLICY["THIS"] - NEWER = POLICY["NEWER"] - MASTER = POLICY["MASTER"] + # Single component = the first component record + # under the master record is always the original, + # only relevant if the master record exists in + # the db and hence item.id is not None + if item.id: + db = current.db + query = (table.id == item.id) & \ + (table[pkey] == ctable[fkey]) + if UID in ctable.fields: + # Load only the UUID now, parse will load any + # required data later + row = db(query).select(ctable[UID], + limitby = (0, 1) + ).first() + if row: + original = row[UID] + else: + # Not nice, but a rare edge-case + original = db(query).select(ctable.ALL, + limitby = (0, 1) + ).first() - # Constants - UID = xml.UID - MCI = xml.MCI - MTIME = xml.MTIME - VALIDATION_ERROR = current.ERROR.VALIDATION_ERROR + # Recurse + item_id = add_item(element = celement, + original = original, + parent = item, + joinby = (pkey, fkey)) + if item_id is None: + item.error = self.error + self.error_tree.append(deepcopy(item.element)) + else: + citem = self.items[item_id] + citem.parent = item + item.components.append(citem) - # Make item mtime TZ-aware - self.mtime = s3_utc(self.mtime) + lookahead = self.lookahead + directory = self.directory - # Resolve references - self._resolve_references() + # Handle references + table = item.table + data = item.data + tree = self.tree - # Deduplicate and validate - if not self.validate(): - self.skip = True + def schedule(reference): + """ Schedule a referenced item for implicit import """ + entry = reference.entry + if entry and entry.element is not None and not entry.item_id: + item_id = add_item(element=entry.element) + if item_id: + entry.item_id = item_id - # Notify the error in the parent to have reported in the - # interactive (2-phase) importer - # Note that the parent item is already written at this point, - # so this notification can NOT prevent/rollback the import of - # the parent item if ignore_errors is True (forced commit), or - # if the user deliberately chose to import it despite error. - parent = self.parent + # Foreign key fields in table + if tree is not None: + fields = [table[f] for f in table.fields] + rfields = [f for f in fields if s3_has_foreign_key(f)] + item.references = lookahead(element, + table = table, + fields = rfields, + tree = tree, + directory = directory, + ) + for reference in item.references: + schedule(reference) + + references = item.references + rappend = references.append + + # Parent reference if parent is not None: - parent.error = VALIDATION_ERROR - element = parent.element - if not element.get(ATTRIBUTE.error, False): - element.set(ATTRIBUTE.error, s3_str(parent.error)) + entry = Storage(item_id = parent.item_id, + element = parent.element, + tablename = parent.tablename, + ) + rappend(Storage(field = joinby, + entry = entry, + )) - return ignore_errors + # References in JSON field data + json_references = s3db.get_config(table, "json_references") + if json_references: + if json_references is True: + # Discover references in any JSON fields + fields = table.fields + else: + # Discover references in fields specified by setting + fields = json_references + if not isinstance(fields, (tuple, list)): + fields = [fields] + for fieldname in fields: + value = data.get(fieldname) + field = table[fieldname] + if value and field.type == "json": + objref = ObjectReferences(value) + for ref in objref.refs: + rl = lookahead(None, + tree = tree, + directory = directory, + lookup = ref, + ) + if rl: + reference = rl[0] + schedule(reference) + rappend(Storage(field = fieldname, + objref = objref, + refkey = ref, + entry = reference.entry, + )) - elif self.method not in (MERGE, DELETE) and self.components: - for component in self.components: - if component.accepted is False or \ - component.data is None: - component.skip = True - # Skip this item on any component validation errors - self.skip = True - self.error = VALIDATION_ERROR - return ignore_errors + # Replacement reference + deleted = data.get(xml.DELETED, False) + if deleted: + fieldname = xml.REPLACEDBY + replaced_by = data.get(fieldname) + if replaced_by: + rl = lookahead(element, + tree = tree, + directory = directory, + lookup = (table, replaced_by), + ) + if rl: + reference = rl[0] + schedule(reference) + rappend(Storage(field = fieldname, + entry = reference.entry, + )) - elif self.method in (MERGE, DELETE) and not self.accepted: - self.skip = True - # Deletion of non-existent record: ignore silently - return True + return item.item_id - # Authorize item - if not self.authorize(): - self.error = "%s: %s, %s, %s" % (current.ERROR.NOT_PERMITTED, - self.method, - self.tablename, - self.id) - self.skip = True - return ignore_errors - - # Update the method - method = self.method - - # Check if import method is allowed in strategy - strategy = self.strategy - if not isinstance(strategy, (list, tuple)): - strategy = [strategy] - if method not in strategy: - self.error = current.ERROR.NOT_PERMITTED - self.skip = True - return True - - # Check mtime and mci - table = self.table - original = self.original - original_mtime = None - original_mci = 0 - if original: - if hasattr(table, MTIME): - original_mtime = s3_utc(original[MTIME]) - if hasattr(table, MCI): - original_mci = original[MCI] - original_deleted = "deleted" in original and original.deleted - else: - original_deleted = False - - # Detect conflicts - job = self.job - original_modified = True - self.modified = True - self.conflict = False - last_sync = s3_utc(job.last_sync) - if last_sync: - if original_mtime and original_mtime < last_sync: - original_modified = False - if self.mtime and self.mtime < last_sync: - self.modified = False - if self.modified and original_modified: - self.conflict = True - if self.conflict and method in (UPDATE, DELETE, MERGE): - if job.onconflict: - job.onconflict(self) - - if self.data is not None: - data = table._filter_fields(self.data, id=True) - else: - data = Storage() - - # Update policy - if isinstance(self.update_policy, dict): - def update_policy(f): - setting = self.update_policy - p = setting.get(f, - setting.get("__default__", THIS)) - if p not in POLICY: - return THIS - return p - else: - def update_policy(f): - p = self.update_policy - if p not in POLICY: - return THIS - return p - - # Log this item - if callable(job.log): - job.log(self) - - tablename = self.tablename - enforce_realm_update = False - - # Update existing record - if method == UPDATE: + # ------------------------------------------------------------------------- + def lookahead(self, + element, + table = None, + fields = None, + tree = None, + directory = None, + lookup = None): + """ + Find referenced elements in the tree - if original: - if original_deleted: - policy = update_policy(None) - if policy == NEWER and \ - original_mtime and original_mtime > self.mtime or \ - policy == MASTER and \ - (original_mci == 0 or self.mci != 1): - self.skip = True - return True + :param element: the element + :param table: the DB table + :param fields: the FK fields in the table + :param tree: the import tree + :param directory: a dictionary to lookup elements in the tree + (will be filled in by this function) + """ - for f in list(data.keys()): - if f in original: - # Check if unchanged - if type(original[f]) is datetime.datetime: - if s3_utc(data[f]) == s3_utc(original[f]): - del data[f] - continue - else: - if data[f] == original[f]: - del data[f] - continue - remove = False - policy = update_policy(f) - if policy == THIS: - remove = True - elif policy == NEWER: - if original_mtime and original_mtime > self.mtime: - remove = True - elif policy == MASTER: - if original_mci == 0 or self.mci != 1: - remove = True - if remove: - del data[f] + db = current.db + s3db = current.s3db - if original_deleted: - # Undelete re-imported records - data["deleted"] = False - if hasattr(table, "deleted_fk"): - data["deleted_fk"] = "" + xml = current.xml + import_uid = xml.import_uid - # Set new author stamp - if hasattr(table, "created_by"): - data["created_by"] = table.created_by.default - if hasattr(table, "modified_by"): - data["modified_by"] = table.modified_by.default + ATTRIBUTE = xml.ATTRIBUTE + TAG = xml.TAG + UID = xml.UID - # Restore defaults for foreign keys - for fieldname in table.fields: - field = table[fieldname] - default = field.default - if str(field.type)[:9] == "reference" and \ - fieldname not in data and \ - default is not None: - data[fieldname] = default + reference_list = [] + rlappend = reference_list.append - # Enforce update of realm entity - enforce_realm_update = True + root = None + if tree is not None: + root = tree if isinstance(tree, etree._Element) else tree.getroot() + uidmap = self.uidmap - if not self.skip and not self.conflict and \ - (len(data) or self.components or self.references): - if self.uid and hasattr(table, UID): - data[UID] = self.uid - if MTIME in table: - data[MTIME] = self.mtime - if MCI in data: - # retain local MCI on updates - del data[MCI] - query = (table._id == self.id) - try: - db(query).update(**dict(data)) - except: - self.error = sys.exc_info()[1] - self.skip = True - return ignore_errors + references = [lookup] if lookup else element.findall("reference") + for reference in references: + if lookup: + field = None + if element is None: + tablename, attr, uid = reference + ktable = s3db.table(tablename) + if ktable is None: + continue + uids = [import_uid(uid)] if attr == "uuid" else [uid] else: - self.committed = True + tablename = element.get(ATTRIBUTE.name, None) + ktable, uid = reference + attr = UID + uids = [import_uid(uid)] else: - # Nothing to update - self.committed = True - - # Create new record - elif method == CREATE: - - # Do not apply field policy to UID and MCI - if UID in data: - del data[UID] - if MCI in data: - del data[MCI] - - for f in data: - if update_policy(f) == MASTER and self.mci != 1: - del data[f] - - if self.skip: - return True - - elif len(data) or self.components or self.references: + field = reference.get(ATTRIBUTE.field, None) - # Restore UID and MCI - if self.uid and UID in table.fields: - data[UID] = self.uid - if MCI in table.fields: - data[MCI] = self.mci + # Ignore references without valid field-attribute + if not field or field not in fields or field not in table: + continue - # Insert the new record + # Find the key table + ktablename, _, multiple = s3_get_foreign_key(table[field]) + if not ktablename: + continue try: - success = table.insert(**dict(data)) - except: - self.error = sys.exc_info()[1] - self.skip = True - return ignore_errors - if success: - self.id = success - self.committed = True - - else: - # Nothing to create - self.skip = True - return True - - # Delete local record - elif method == DELETE: - - if original: - if original_deleted: - self.skip = True - policy = update_policy(None) - if policy == THIS: - self.skip = True - elif policy == NEWER and \ - (original_mtime and original_mtime > self.mtime): - self.skip = True - elif policy == MASTER and \ - (original_mci == 0 or self.mci != 1): - self.skip = True - else: - self.skip = True - - if not self.skip and not self.conflict: - - resource = s3db.resource(tablename, id=self.id) - # Use cascade=True so that the deletion can be - # rolled back (e.g. trial phase, subsequent failure) - success = resource.delete(cascade=True) - if resource.error: - self.error = resource.error - self.skip = True - return ignore_errors - - return True - - # Merge records - elif method == MERGE: + ktable = s3db[ktablename] + except AttributeError: + continue - if UID not in table.fields: - self.skip = True - elif original: - if original_deleted: - self.skip = True - policy = update_policy(None) - if policy == THIS: - self.skip = True - elif policy == NEWER and \ - (original_mtime and original_mtime > self.mtime): - self.skip = True - elif policy == MASTER and \ - (original_mci == 0 or self.mci != 1): - self.skip = True - else: - self.skip = True + tablename = reference.get(ATTRIBUTE.resource, None) + # Ignore references to tables without UID field: + if UID not in ktable.fields: + continue + # Fall back to key table name if tablename is not specified: + if not tablename: + tablename = ktablename + # Super-entity references must use the super-key: + if tablename != ktablename: + field = (ktable._id.name, field) + # Ignore direct references to super-entities: + if tablename == ktablename and ktable._id.name != "id": + continue + # Get the foreign key + uids = reference.get(UID, None) + attr = UID + if not uids: + uids = reference.get(ATTRIBUTE.tuid, None) + attr = ATTRIBUTE.tuid + if uids and multiple: + uids = json.loads(uids) + elif uids: + uids = [uids] - if not self.skip and not self.conflict: + # Find the elements and map to DB records + relements = [] - row = db(table[UID] == data[xml.REPLACEDBY]) \ - .select(table._id, limitby=(0, 1)) \ - .first() - if row: - original_id = row[table._id] - resource = s3db.resource(tablename, - id = [original_id, self.id], - ) - try: - success = resource.merge(original_id, self.id) - except: - self.error = sys.exc_info()[1] - self.skip = True - return ignore_errors - if success: - self.committed = True + # Create a UID<->ID map + id_map = {} + if attr == UID and uids: + if len(uids) == 1: + uid = import_uid(uids[0]) + query = (ktable[UID] == uid) + record = db(query).select(ktable.id, + cacheable = True, + limitby = (0, 1), + ).first() + if record: + id_map[uid] = record.id else: - self.skip = True + uids_ = [import_uid(uid) for uid in uids] + query = (ktable[UID].belongs(uids_)) + records = db(query).select(ktable.id, + ktable[UID], + limitby = (0, len(uids_)), + ) + for r in records: + id_map[r[UID]] = r.id - return True + if not uids: + # Anonymous reference: inside the element + expr = './/%s[@%s="%s"]' % (TAG.resource, + ATTRIBUTE.name, + tablename, + ) + relements = reference.xpath(expr) + if relements and not multiple: + relements = relements[:1] - else: - raise RuntimeError("unknown import method: %s" % method) + elif root is not None: - # Audit + onaccept on successful commits - if self.committed: + for uid in uids: - # Create a pseudo-form for callbacks - form = Storage() - form.method = method - form.table = table - form.vars = self.data - prefix, name = tablename.split("_", 1) - if self.id: - form.vars.id = self.id + entry = None - # Audit - current.audit(method, prefix, name, - form = form, - record = self.id, - representation = "xml", - ) + # Entry already in directory? + if directory is not None: + entry = directory.get((tablename, attr, uid)) - # Prevent that record post-processing breaks time-delayed - # synchronization by implicitly updating "modified_on" - if MTIME in table.fields: - modified_on = table[MTIME] - modified_on_update = modified_on.update - modified_on.update = None - else: - modified_on_update = None + if not entry: + e = uidmap[attr].get((tablename, uid)) if uidmap else None + if e is not None: + # Element in the source => append to relements + relements.append(e) + else: + # No element found, see if original record exists + _uid = import_uid(uid) + if _uid and _uid in id_map: + _id = id_map[_uid] + entry = Storage(tablename = tablename, + element = None, + uid = uid, + id = _id, + item_id = None, + ) + rlappend(Storage(field = field, + element = reference, + entry = entry, + )) + else: + continue + else: + rlappend(Storage(field = field, + element = reference, + entry = entry, + )) - # Update super entity links - s3db.update_super(table, form.vars) - if method == CREATE: - # Set record owner - current.auth.s3_set_record_owner(table, self.id) - elif method == UPDATE: - # Update realm - update_realm = enforce_realm_update or \ - s3db.get_config(table, "update_realm") - if update_realm: - current.auth.set_realm_entity(table, self.id, - force_update = True, - ) - # Onaccept - key = "%s_onaccept" % method - onaccept = current.deployment_settings.get_import_callback(tablename, key) - if onaccept: - callback(onaccept, form, tablename=tablename) + # Create entries for all newly found elements + for relement in relements: + uid = relement.get(attr, None) + if attr == UID: + _uid = import_uid(uid) + _id = _uid and id_map and id_map.get(_uid, None) or None + else: + _uid = None + _id = None + entry = Storage(tablename = tablename, + element = relement, + uid = uid, + id = _id, + item_id = None, + ) + # Add entry to directory + if uid and directory is not None: + directory[(tablename, attr, uid)] = entry + # Append the entry to the reference list + rlappend(Storage(field = field, + element = reference, + entry = entry, + )) - # Restore modified_on.update - if modified_on_update is not None: - modified_on.update = modified_on_update + return reference_list - # Update referencing items - if self.update and self.id: - for u in self.update: + # ------------------------------------------------------------------------- + def load_item(self, row): + """ + Load an item from the item table (counterpart to add_item + when restoring a job from the database) + """ - # The other import item that shall be updated - item = u.get("item") - if not item: - continue + item = ImportItem(self) + if not item.restore(row): + self.error = item.error + if item.load_parent is None: + self.error_tree.append(deepcopy(item.element)) + # Update lookup lists + item_id = item.item_id + self.items[item_id] = item + return item_id - # The field in the other item that shall be updated - field = u.get("field") - if isinstance(field, (list, tuple)): - # The field references something else than the - # primary key of this table => look it up - pkey, fkey = field - query = (table.id == self.id) - row = db(query).select(table[pkey], limitby=(0, 1)).first() - ref_id = row[pkey] - else: - # The field references the primary key of this table - pkey, fkey = None, field - ref_id = self.id + # ------------------------------------------------------------------------- + def resolve(self, item_id, import_list): + """ + Resolve the reference list of an item - if "refkey" in u: - # Target field is a JSON object - item._update_objref(fkey, u["refkey"], ref_id) - else: - # Target field is a reference or list:reference - item._update_reference(fkey, ref_id) + :param item_id: the import item UID + :param import_list: the ordered list of items (UIDs) to import + """ + item = self.items[item_id] + if item.lock or item.accepted is False: + return False + references = [] + for reference in item.references: + ritem_id = reference.entry.item_id + if ritem_id and ritem_id not in import_list: + references.append(ritem_id) + for ritem_id in references: + item.lock = True + if self.resolve(ritem_id, import_list): + import_list.append(ritem_id) + item.lock = False return True # ------------------------------------------------------------------------- - def _dynamic_defaults(self, data): + def commit(self, ignore_errors=False, log_items=None): """ - Applies dynamic defaults from any keys in data that start with - an underscore, used only for new records and only if the respective - field is not populated yet. + Commit the import job to the DB - @param data: the data dict + :param ignore_errors: skip any items with errors + (does still report the errors) + :param log_items: callback function to log import items + before committing them """ - for k, v in list(data.items()): - if k[0] == "_": - fn = k[1:] - if fn in self.table.fields and fn not in data: - data[fn] = v + ATTRIBUTE = current.xml.ATTRIBUTE + METHOD = ImportItem.METHOD + + # Resolve references + import_list = [] + for item_id in self.items: + self.resolve(item_id, import_list) + if item_id not in import_list: + import_list.append(item_id) + # Commit the items + items = self.items + count = 0 + errors = 0 + mtime = None + created = [] + cappend = created.append + updated = [] + deleted = [] + tablename = self.table._tablename + + self.log = log_items + failed = False + for item_id in import_list: + item = items[item_id] + error = None + + if item.accepted is not False: + logged = False + success = item.commit(ignore_errors=ignore_errors) + else: + # Field validation failed + logged = True + success = ignore_errors + + if not success: + failed = True + + error = item.error + if error: + current.log.error(error) + self.error = error + element = item.element + if element is not None: + if not element.get(ATTRIBUTE.error, False): + element.set(ATTRIBUTE.error, s3_str(error)) + if not logged: + self.error_tree.append(deepcopy(element)) + if item.tablename == tablename: + errors += 1 + + elif item.tablename == tablename: + count += 1 + if mtime is None or item.mtime > mtime: + mtime = item.mtime + if item.id: + if item.method == METHOD.CREATE: + cappend(item.id) + elif item.method == METHOD.UPDATE: + updated.append(item.id) + elif item.method in (METHOD.MERGE, METHOD.DELETE): + deleted.append(item.id) + + if failed: + return False + + self.count = count + self.errors = errors + self.mtime = mtime + self.created = created + self.updated = updated + self.deleted = deleted + return True # ------------------------------------------------------------------------- - def _mandatory_fields(self): + def store(self): + """ + Store this job and all its items in the job table + """ - job = self.job + db = current.db + s3db = current.s3db - mandatory = None - tablename = self.tablename + jobtable = s3db.s3_import_job + query = (jobtable.job_id == self.job_id) + row = db(query).select(jobtable.id, limitby=(0, 1)).first() + if row: + record_id = row.id + else: + record_id = None + record = Storage(job_id=self.job_id) + try: + tablename = self.table._tablename + except AttributeError: + pass + else: + record.update(tablename=tablename) + for item in self.items.values(): + item.store(item_table=s3db.s3_import_item) + if record_id: + db(jobtable.id == record_id).update(**record) + else: + record_id = jobtable.insert(**record) - mfields = job.mandatory_fields - if tablename in mfields: - mandatory = mfields[tablename] + return record_id - if mandatory is None: - mandatory = [] - for field in self.table: - if field.default is not None: - continue - requires = field.requires - if requires: - if not isinstance(requires, (list, tuple)): - requires = [requires] - if isinstance(requires[0], IS_EMPTY_OR): - continue - error = field.validate("")[1] - if error: - mandatory.append(field.name) - mfields[tablename] = mandatory + # ------------------------------------------------------------------------- + def get_tree(self): + """ + Reconstruct the element tree of this job + """ - return mandatory + if self.tree is not None: + return self.tree + else: + xml = current.xml + ATTRIBUTE = xml.ATTRIBUTE + UID = xml.UID + root = etree.Element(xml.TAG.root) + for item in self.items.values(): + element = item.element + if element is not None and not item.parent: + if item.tablename == self.table._tablename or \ + element.get(UID, None) or \ + element.get(ATTRIBUTE.tuid, None): + root.append(deepcopy(element)) + return etree.ElementTree(root) # ------------------------------------------------------------------------- - def _resolve_references(self): + def delete(self): """ - Resolve the references of this item (=look up all foreign - keys from other items of the same job). If a foreign key - is not yet available, it will be scheduled for later update. + Delete this job and all its items from the job table """ - table = self.table - if not table: - return - db = current.db - items = self.job.items - for reference in self.references: - - entry = reference.entry - if not entry: - continue - - field = reference.field - - # Resolve key tuples - if isinstance(field, (list, tuple)): - pkey, fkey = field - else: - pkey, fkey = ("id", field) - - f = table[fkey] - if f.type == "json": - is_json = True - objref = reference.objref - if not objref: - objref = S3ObjectReferences(self.data.get(fkey)) - refkey = reference.refkey - if not refkey: - continue - else: - is_json = False - refkey = objref = None - ktablename, _, multiple = s3_get_foreign_key(f) - if not ktablename: - continue + s3db = current.s3db - # Get the lookup table - if entry.tablename: - ktablename = entry.tablename - try: - ktable = current.s3db[ktablename] - except AttributeError: - continue + job_id = self.job_id - # Resolve the foreign key (value) - item = None - fk = entry.id - if entry.item_id: - item = items[entry.item_id] - if item: - if item.original and \ - item.original.get("deleted") and \ - not item.committed: - # Original is deleted and has not been updated - fk = None - else: - fk = item.id - if fk and pkey != "id": - row = db(ktable._id == fk).select(ktable[pkey], - limitby=(0, 1)).first() - if not row: - fk = None - continue - else: - fk = row[pkey] + item_table = s3db.s3_import_item + db(item_table.job_id == job_id).delete() - # Update record data - if fk: - if is_json: - objref.resolve(refkey[0], refkey[1], refkey[2], fk) - elif multiple: - val = self.data.get(fkey, []) - if fk not in val: - val.append(fk) - self.data[fkey] = val - else: - self.data[fkey] = fk - else: - if fkey in self.data and not multiple and not is_json: - del self.data[fkey] - if item: - update = {"item": self, "field": fkey} - if is_json: - update["refkey"] = refkey - item.update.append(update) + job_table = s3db.s3_import_job + db(job_table.job_id == job_id).delete() # ------------------------------------------------------------------------- - def _update_reference(self, field, value): + def restore_references(self): """ - Helper method to update a foreign key in an already written - record. Will be called by the referenced item after (and only - if) it has been committed. This is only needed if the reference - could not be resolved before commit due to circular references. - - @param field: the field name of the foreign key - @param value: the value of the foreign key + Restore the job's reference structure after loading items + from the item table """ - table = self.table - record_id = self.id - - if not value or not table or not record_id or not self.permitted: - return - db = current.db - update = None + UID = current.xml.UID - fieldtype = str(table[field].type) - if fieldtype.startswith("list:reference"): - query = (table._id == record_id) - record = db(query).select(table[field], - limitby = (0, 1), - ).first() - if record: - values = record[field] - if value not in values: - values.append(value) - update = {field: values} - else: - update = {field: value} + for item in self.items.values(): + for citem_id in item.load_components: + if citem_id in self.items: + item.components.append(self.items[citem_id]) + item.load_components = [] + for ritem in item.load_references: + field = ritem["field"] + if "item_id" in ritem: + item_id = ritem["item_id"] + if item_id in self.items: + _item = self.items[item_id] + entry = Storage(tablename=_item.tablename, + element=_item.element, + uid=_item.uid, + id=_item.id, + item_id=item_id) + item.references.append(Storage(field=field, + entry=entry)) + else: + _id = None + uid = ritem.get("uid", None) + tablename = ritem.get("tablename", None) + if tablename and uid: + try: + table = current.s3db[tablename] + except AttributeError: + continue + if UID not in table.fields: + continue + query = table[UID] == uid + row = db(query).select(table._id, + limitby=(0, 1)).first() + if row: + _id = row[table._id.name] + else: + continue + entry = Storage(tablename = ritem["tablename"], + element=None, + uid = ritem["uid"], + id = _id, + item_id = None) + item.references.append(Storage(field=field, + entry=entry)) + item.load_references = [] + if item.load_parent is not None: + parent = self.items[item.load_parent] + if parent is None: + # Parent has been removed + item.skip = True + else: + item.parent = parent + item.load_parent = None - if update: - if "modified_on" in table.fields: - update["modified_on"] = table.modified_on - if "modified_by" in table.fields: - update["modified_by"] = table.modified_by - db(table._id == record_id).update(**update) +# ============================================================================= +class ImportItem(object): + """ Class representing an import item (=a single record) """ + + METHOD = Storage( + CREATE = "create", + UPDATE = "update", + DELETE = "delete", + MERGE = "merge" + ) - # ------------------------------------------------------------------------- - def _update_objref(self, field, refkey, value): + def __init__(self, job): """ - Update object references in a JSON field - - @param fieldname: the name of the JSON field - @param refkey: the reference key, a tuple (tablename, uidtype, uid) - @param value: the foreign key value + :param job: the import job this item belongs to """ + self.job = job - table = self.table - record_id = self.id - - if not value or not table or not record_id or not self.permitted: - return - - db = current.db - query = (table._id == record_id) - record = db(query).select(table._id, - table[field], - limitby = (0, 1), - ).first() - if record: - obj = record[field] + # Locking and error handling + self.lock = False + self.error = None - tn, uidtype, uid = refkey - S3ObjectReferences(obj).resolve(tn, uidtype, uid, value) + # Identification + self.item_id = uuid.uuid4() # unique ID for this item + self.id = None + self.uid = None - update = {field: obj} - if "modified_on" in table.fields: - update["modified_on"] = table.modified_on - if "modified_by" in table.fields: - update["modified_by"] = table.modified_by - record.update_record(**update) + # Data elements + self.table = None + self.tablename = None + self.element = None + self.data = None + self.original = None + self.components = [] + self.references = [] + self.load_components = [] + self.load_references = [] + self.parent = None + self.skip = False - # ------------------------------------------------------------------------- - def store(self, item_table=None): - """ - Store this item in the DB - """ + # Conflict handling + self.mci = 2 + self.mtime = datetime.datetime.utcnow() + self.modified = True + self.conflict = False - if item_table is None: - return None + # Allowed import methods + self.strategy = job.strategy + # Update and conflict resolution policies + self.update_policy = job.update_policy + self.conflict_policy = job.conflict_policy - item_id = self.item_id - db = current.db - row = db(item_table.item_id == item_id).select(item_table.id, - limitby=(0, 1) - ).first() - if row: - record_id = row.id - else: - record_id = None + # Actual import method + self.method = None - record = Storage(job_id = self.job.job_id, - item_id = item_id, - tablename = self.tablename, - record_uid = self.uid, - skip = self.skip, - error = self.error or "", - ) + self.onvalidation = None + self.onaccept = None - if self.element is not None: - element_str = current.xml.tostring(self.element, - xml_declaration=False) - record.update(element=element_str) + # Item import status flags + self.accepted = None + self.permitted = False + self.committed = False - self_data = self.data - if self_data is not None: - table = self.table - fields = table.fields - data = Storage() - for f in self_data.keys(): - if f not in fields: - continue - field = table[f] - field_type = str(field.type) - if field_type == "id" or s3_has_foreign_key(field): - continue - data_ = self_data[f] - if isinstance(data_, Field): - # Not picklable - # This is likely to be a modified_on to avoid updating this field, which skipping does just fine too - continue - data.update({f: data_}) - record["data"] = pickle.dumps(data) + # Writeback hook for circular references: + # Items which need a second write to update references + self.update = [] - ritems = [] - for reference in self.references: - field = reference.field - entry = reference.entry - store_entry = None - if entry: - if entry.item_id is not None: - store_entry = {"field": field, - "item_id": str(entry.item_id), - } - elif entry.uid is not None: - store_entry = {"field": field, - "tablename": entry.tablename, - "uid": str(entry.uid), - } - if store_entry is not None: - ritems.append(json.dumps(store_entry)) - if ritems: - record.update(ritems=ritems) - citems = [c.item_id for c in self.components] - if citems: - record.update(citems=citems) - if self.parent: - record.update(parent=self.parent.item_id) - if record_id: - db(item_table.id == record_id).update(**record) - else: - record_id = item_table.insert(**record) + # ------------------------------------------------------------------------- + def __repr__(self): + """ Helper method for debugging """ - return record_id + _str = "" % \ + (self.table, self.item_id, self.uid, self.id, self.error, self.data) + return _str # ------------------------------------------------------------------------- - def restore(self, row): + def parse(self, + element, + original = None, + table = None, + tree = None, + files = None + ): """ - Restore an item from a item table row. This does not restore - the references (since this can not be done before all items - are restored), must call job.restore_references() to do that + Read data from a element + + :param element: the element + :param table: the DB table + :param tree: the import tree + :param files: uploaded files - @param row: the item table row + :returns: True if successful, False if not (sets self.error) """ + s3db = current.s3db xml = current.xml - self.item_id = row.item_id - self.accepted = None - self.permitted = False - self.committed = False - tablename = row.tablename - self.id = None - self.uid = row.record_uid - self.skip = row.skip - if row.data is not None: - self.data = pickle.loads(row.data) + ERROR = xml.ATTRIBUTE["error"] + + self.element = element + if table is None: + tablename = element.get(xml.ATTRIBUTE["name"]) + table = s3db.table(tablename) + if table is None: + self.error = current.ERROR.BAD_RESOURCE + element.set(ERROR, s3_str(self.error)) + return False else: - self.data = Storage() - data = self.data - if xml.MTIME in data: - self.mtime = data[xml.MTIME] - if xml.MCI in data: - self.mci = data[xml.MCI] + tablename = table._tablename + + self.table = table + self.tablename = tablename + UID = xml.UID - if UID in data: - self.uid = data[UID] - self.element = etree.fromstring(row.element) - if row.citems: - self.load_components = row.citems - if row.ritems: - self.load_references = [json.loads(ritem) for ritem in row.ritems] - self.load_parent = row.parent - s3db = current.s3db - try: - table = s3db[tablename] - except AttributeError: - self.error = current.ERROR.BAD_RESOURCE - return False + + from ..resource import S3Resource + if original is None: + original = S3Resource.original(table, element, + mandatory = self._mandatory_fields()) + elif isinstance(original, str) and UID in table.fields: + # Single-component update in add-item => load the original now + query = (table[UID] == original) + pkeys = set(fname for fname in table.fields if table[fname].unique) + fields = S3Resource.import_fields(table, pkeys, + mandatory = self._mandatory_fields()) + original = current.db(query).select(limitby=(0, 1), *fields).first() else: - self.table = table - self.tablename = tablename - original = S3Resource.original(table, self.data, - mandatory=self._mandatory_fields()) + original = None + + postprocess = s3db.get_config(tablename, "xml_post_parse") + data = xml.record(table, element, + files = files, + original = original, + postprocess = postprocess) + + if data is None: + self.error = current.ERROR.VALIDATION_ERROR + self.accepted = False + if not element.get(ERROR, False): + element.set(ERROR, s3_str(self.error)) + return False + + self.data = data + + MCI = xml.MCI + MTIME = xml.MTIME + + self.uid = data.get(UID) if original is not None: + self.original = original self.id = original[table._id.name] - if not current.response.s3.synchronise_uuids and UID in original: - self.uid = self.data[UID] = original[UID] - self.error = row.error - postprocess = s3db.get_config(self.tablename, "xml_post_parse") - if postprocess: - postprocess(self.element, self.data) - if self.error and not self.data: - # Validation error - return False - return True -# ============================================================================= -class S3ImportJob(): - """ - Class to import an element tree into the database - """ + if not current.response.s3.synchronise_uuids and UID in original: + self.uid = self.data[UID] = original[UID] + + if MTIME in data: + self.mtime = data[MTIME] + if MCI in data: + self.mci = data[MCI] - JOB_TABLE_NAME = "s3_import_job" - ITEM_TABLE_NAME = "s3_import_item" + #current.log.debug("New item: %s" % self) + return True # ------------------------------------------------------------------------- - def __init__(self, table, - tree=None, - files=None, - job_id=None, - strategy=None, - update_policy=None, - conflict_policy=None, - last_sync=None, - onconflict=None): - """ - Constructor - - @param tree: the element tree to import - @param files: files attached to the import (for upload fields) - @param job_id: restore job from database (record ID or job_id) - @param strategy: the import strategy - @param update_policy: the update policy - @param conflict_policy: the conflict resolution policy - @param last_sync: the last synchronization time stamp (datetime) - @param onconflict: custom conflict resolver function + def deduplicate(self): + """ + Detect whether this is an update or a new record """ - self.error = None # the last error - self.error_tree = etree.Element(current.xml.TAG.root) + table = self.table + if table is None or self.id: + return - self.table = table - self.tree = tree - self.files = files - self.directory = Storage() + from ..resource import S3Resource - self._uidmap = None + METHOD = self.METHOD + CREATE = METHOD["CREATE"] + UPDATE = METHOD["UPDATE"] + DELETE = METHOD["DELETE"] + MERGE = METHOD["MERGE"] - # Mandatory fields - self.mandatory_fields = Storage() + xml = current.xml + UID = xml.UID - self.elements = Storage() - self.items = Storage() - self.references = [] + data = self.data + if self.job.second_pass and UID in table.fields: + uid = data.get(UID) + if uid and not self.element.get(UID) and not self.original: + # Previously identified original does no longer exist + del data[UID] - self.job_table = None - self.item_table = None + mandatory = self._mandatory_fields() - self.count = 0 # total number of records imported - self.errors = 0 # total number of records in error - self.created = [] # IDs of created records - self.updated = [] # IDs of updated records - self.deleted = [] # IDs of deleted records + if self.original is not None: + original = self.original + elif self.data: + original = S3Resource.original(table, + self.data, + mandatory=mandatory, + ) + else: + original = None - self.log = None + synchronise_uuids = current.response.s3.synchronise_uuids - # Import strategy - if strategy is None: - METHOD = S3ImportItem.METHOD - strategy = [METHOD.CREATE, - METHOD.UPDATE, - METHOD.DELETE, - METHOD.MERGE, - ] - if not isinstance(strategy, (tuple, list)): - strategy = [strategy] - self.strategy = strategy + deleted = data[xml.DELETED] + if deleted: + if data[xml.REPLACEDBY]: + self.method = MERGE + else: + self.method = DELETE + + self.uid = data.get(UID) + + if original is not None: + + # The original record could be identified by a unique-key-match, + # so this must be an update + self.id = original[table._id.name] + + if not deleted: + self.method = UPDATE - # Update policy (default=always update) - if update_policy: - self.update_policy = update_policy - else: - self.update_policy = S3ImportItem.POLICY.OTHER - # Conflict resolution policy (default=always update) - if conflict_policy: - self.conflict_policy = conflict_policy else: - self.conflict_policy = S3ImportItem.POLICY.OTHER - # Synchronization settings - self.mtime = None - self.last_sync = last_sync - self.onconflict = onconflict + if UID in data and not synchronise_uuids: + # The import item has a UUID but there is no match + # in the database, so this must be a new record + self.id = None + if not deleted: + self.method = CREATE + else: + # Nonexistent record to be deleted => skip + self.method = DELETE + self.skip = True + else: + # Use the resource's deduplicator to identify the original + resolve = current.s3db.get_config(self.tablename, "deduplicate") + if data and resolve: + resolve(self) - if job_id: - self.__define_tables() - jobtable = self.job_table - if str(job_id).isdigit(): - query = (jobtable.id == job_id) + if self.id and self.method in (UPDATE, DELETE, MERGE): + # Retrieve the original + fields = S3Resource.import_fields(table, + data, + mandatory=mandatory, + ) + original = current.db(table._id == self.id) \ + .select(limitby=(0, 1), *fields).first() + + # Retain the original UUID (except in synchronise_uuids mode) + if original and not synchronise_uuids and UID in original: + self.uid = data[UID] = original[UID] + + self.original = original + + # ------------------------------------------------------------------------- + def authorize(self): + """ + Authorize the import of this item, sets self.permitted + """ + + if not self.table: + return False + + auth = current.auth + tablename = self.tablename + + # Check whether self.table is protected + if not auth.override and tablename.split("_", 1)[0] in auth.PROTECTED: + return False + + # Determine the method + METHOD = self.METHOD + if self.data.deleted is True: + if self.data.deleted_rb: + self.method = METHOD["MERGE"] else: - query = (jobtable.job_id == job_id) - row = current.db(query).select(jobtable.job_id, - jobtable.tablename, - limitby=(0, 1)).first() - if not row: - raise SyntaxError("Job record not found") - self.job_id = row.job_id - self.second_pass = True - if not self.table: - tablename = row.tablename - try: - table = current.s3db[tablename] - except AttributeError: - pass + self.method = METHOD["DELETE"] + self.accepted = True if self.id else False + elif self.id: + if not self.original: + from ..resource import S3Resource + fields = S3Resource.import_fields(self.table, self.data, + mandatory=self._mandatory_fields()) + query = (self.table.id == self.id) + self.original = current.db(query).select(limitby=(0, 1), + *fields).first() + if self.original: + self.method = METHOD["UPDATE"] + else: + self.method = METHOD["CREATE"] else: - self.job_id = uuid.uuid4() # unique ID for this job - self.second_pass = False + self.method = METHOD["CREATE"] + + # Set self.id + if self.method == METHOD["CREATE"]: + self.id = 0 + + # Authorization + authorize = current.auth.s3_has_permission + if authorize: + self.permitted = authorize(self.method, + tablename, + record_id=self.id) + else: + self.permitted = True + + return self.permitted # ------------------------------------------------------------------------- - @property - def uidmap(self): + def validate(self): """ - Map uuid/tuid => element, for faster reference lookups + Validate this item (=record onvalidation), sets self.accepted """ - uidmap = self._uidmap - tree = self.tree + data = self.data + + if self.accepted is not None: + return self.accepted + if data is None or not self.table: + self.accepted = False + return False + + xml = current.xml + ERROR = xml.ATTRIBUTE["error"] + + METHOD = self.METHOD + DELETE = METHOD.DELETE + MERGE = METHOD.MERGE + + # Detect update + if not self.id: + self.deduplicate() + if self.accepted is False: + # Item rejected by deduplicator (e.g. due to ambiguity) + return False + + # Don't need to validate skipped or deleted records + if self.skip or self.method in (DELETE, MERGE): + self.accepted = True if self.id else False + return True + + # Set dynamic defaults for new records + if not self.id: + self._dynamic_defaults(data) + + # Check for mandatory fields + required_fields = self._mandatory_fields() + + all_fields = list(data.keys()) + + failed_references = [] + items = self.job.items + for reference in self.references: + resolvable = resolved = True + entry = reference.entry + if entry and not entry.id: + if entry.item_id: + item = items[entry.item_id] + if item.error: + relement = reference.element + if relement is not None: + # Repeat the errors from the referenced record + # in the element (better reasoning) + msg = "; ".join(xml.collect_errors(entry.element)) + relement.set(ERROR, msg) + else: + resolvable = False + resolved = False + else: + resolvable = resolved = False + field = reference.field + if isinstance(field, (tuple, list)): + field = field[1] + if resolved: + all_fields.append(field) + elif resolvable: + # Both reference and referenced record are in the XML, + # => treat foreign key as mandatory, and mark as failed + if field not in required_fields: + required_fields.append(field) + if field not in failed_references: + failed_references.append(field) - if uidmap is None and tree is not None: + missing = [fname for fname in required_fields + if fname not in all_fields] - root = tree if isinstance(tree, etree._Element) else tree.getroot() + original = self.original + if missing: + if original: + missing = [fname for fname in missing + if fname not in original] + if missing: + fields = [f for f in missing + if f not in failed_references] + if fields: + errors = ["%s: value(s) required" % ", ".join(fields)] + else: + errors = [] + if failed_references: + fields = ", ".join(failed_references) + errors.append("%s: reference import(s) failed" % + ", ".join(failed_references)) + self.error = "; ".join(errors) + self.element.set(ERROR, self.error) + self.accepted = False + return False - xml = current.xml - UUID = xml.UID - TUID = xml.ATTRIBUTE.tuid - NAME = xml.ATTRIBUTE.name + # Run onvalidation + form = Storage(method = self.method, + vars = data, + request_vars = data, + # Useless since always incomplete: + #record = original, + ) + if self.id: + form.vars.id = self.id - elements = root.xpath(".//%s" % xml.TAG.resource) - self._uidmap = uidmap = {UUID: {}, - TUID: {}, - } - uuidmap = uidmap[UUID] - tuidmap = uidmap[TUID] - for element in elements: - name = element.get(NAME) - r_uuid = element.get(UUID) - if r_uuid and r_uuid not in uuidmap: - uuidmap[(name, r_uuid)] = element - r_tuid = element.get(TUID) - if r_tuid and r_tuid not in tuidmap: - tuidmap[(name, r_tuid)] = element + form.errors = Storage() + tablename = self.tablename + key = "%s_onvalidation" % self.method + get_config = current.s3db.get_config + onvalidation = get_config(tablename, key, + get_config(tablename, "onvalidation")) + if onvalidation: + try: + callback(onvalidation, form, tablename=tablename) + except: + from traceback import format_exc + current.log.error("S3Import %s onvalidation exception:" % tablename) + current.log.debug(format_exc(10)) + accepted = True + if form.errors: + element = self.element + for k in form.errors: + e = element.findall("data[@field='%s']" % k) + if not e: + e = element.findall("reference[@field='%s']" % k) + if not e: + e = element + form.errors[k] = "[%s] %s" % (k, form.errors[k]) + else: + e = e[0] + e.set(ERROR, s3_str(form.errors[k])) + self.error = current.ERROR.VALIDATION_ERROR + accepted = False - return uidmap + self.accepted = accepted + return accepted # ------------------------------------------------------------------------- - def add_item(self, - element = None, - original = None, - components = None, - parent = None, - joinby = None): + def commit(self, ignore_errors=False): """ - Parse and validate an XML element and add it as new item - to the job. - - @param element: the element - @param original: the original DB record (if already available, - will otherwise be looked-up by this function) - @param components: a dictionary of components (as in S3Resource) - to include in the job (defaults to all - defined components) - @param parent: the parent item (if this is a component) - @param joinby: the component join key(s) (if this is a component) + Commit this item to the database - @return: a unique identifier for the new item, or None if there - was an error. self.error contains the last error, and - self.error_tree an element tree with all failing elements - including error attributes. + :param ignore_errors: skip invalid components + (still reports errors) """ - if element in self.elements: - # element has already been added to this job - return self.elements[element] + if self.committed: + # already committed + return True - # Parse the main element - item = S3ImportItem(self) + # If the parent item gets skipped, then skip this item as well + if self.parent is not None and self.parent.skip: + return True - # Update lookup lists - item_id = item.item_id - self.items[item_id] = item - if element is not None: - self.elements[element] = item_id + # Globals + db = current.db + s3db = current.s3db - if not item.parse(element, - original = original, - files = self.files): - self.error = item.error - item.accepted = False - if parent is None: - self.error_tree.append(deepcopy(item.element)) + xml = current.xml + ATTRIBUTE = xml.ATTRIBUTE - else: - # Now parse the components - table = item.table + # Methods + METHOD = self.METHOD + CREATE = METHOD.CREATE + UPDATE = METHOD.UPDATE + DELETE = METHOD.DELETE + MERGE = METHOD.MERGE - s3db = current.s3db - components = s3db.get_components(table, names=components) - super_keys = s3db.get_super_keys(table) + # Policies + THIS = SyncPolicy.THIS + OTHER = SyncPolicy.OTHER + NEWER = SyncPolicy.NEWER + MASTER = SyncPolicy.MASTER + POLICY = {THIS, OTHER, NEWER, MASTER} - cnames = Storage() - cinfos = Storage() - for alias in components: + # Constants + UID = xml.UID + MCI = xml.MCI + MTIME = xml.MTIME + VALIDATION_ERROR = current.ERROR.VALIDATION_ERROR - component = components[alias] + # Make item mtime TZ-aware + self.mtime = s3_utc(self.mtime) - ctable = component.table - if ctable._id != "id" and "instance_type" in ctable.fields: - # Super-entities cannot be imported to directly => skip - continue + # Resolve references + self._resolve_references() - # Determine the keys - pkey = component.pkey + # Deduplicate and validate + if not self.validate(): + self.skip = True - if pkey != table._id.name and pkey not in super_keys: - # Pseudo-component cannot be imported => skip - continue + # Notify the error in the parent to have reported in the + # interactive (2-phase) importer + # Note that the parent item is already written at this point, + # so this notification can NOT prevent/rollback the import of + # the parent item if ignore_errors is True (forced commit), or + # if the user deliberately chose to import it despite error. + parent = self.parent + if parent is not None: + parent.error = VALIDATION_ERROR + element = parent.element + if not element.get(ATTRIBUTE.error, False): + element.set(ATTRIBUTE.error, s3_str(parent.error)) - if component.linktable: - ctable = component.linktable - fkey = component.lkey - else: - fkey = component.fkey + return ignore_errors - ctablename = ctable._tablename - if ctablename in cnames: - cnames[ctablename].append(alias) - else: - cnames[ctablename] = [alias] + elif self.method not in (MERGE, DELETE) and self.components: + for component in self.components: + if component.accepted is False or \ + component.data is None: + component.skip = True + # Skip this item on any component validation errors + self.skip = True + self.error = VALIDATION_ERROR + return ignore_errors - cinfos[(ctablename, alias)] = Storage(component = component, - ctable = ctable, - pkey = pkey, - fkey = fkey, - first = True, - ) - add_item = self.add_item - xml = current.xml - UID = xml.UID - for celement in xml.components(element, names=list(cnames.keys())): + elif self.method in (MERGE, DELETE) and not self.accepted: + self.skip = True + # Deletion of non-existent record: ignore silently + return True - # Get the component tablename - ctablename = celement.get(xml.ATTRIBUTE.name, None) - if not ctablename or ctablename not in cnames: - continue + # Authorize item + if not self.authorize(): + self.error = "%s: %s, %s, %s" % (current.ERROR.NOT_PERMITTED, + self.method, + self.tablename, + self.id) + self.skip = True + return ignore_errors - # Get the component alias (for disambiguation) - calias = celement.get(xml.ATTRIBUTE.alias, None) - if calias is None: - aliases = cnames[ctablename] - if len(aliases) == 1: - calias = aliases[0] - else: - calias = ctablename.split("_", 1)[1] + # Update the method + method = self.method - if (ctablename, calias) not in cinfos: - continue - else: - cinfo = cinfos[(ctablename, calias)] + # Check if import method is allowed in strategy + strategy = self.strategy + if not isinstance(strategy, (list, tuple)): + strategy = [strategy] + if method not in strategy: + self.error = current.ERROR.NOT_PERMITTED + self.skip = True + return True + + # Check mtime and mci + table = self.table + original = self.original + original_mtime = None + original_mci = 0 + if original: + if hasattr(table, MTIME): + original_mtime = s3_utc(original[MTIME]) + if hasattr(table, MCI): + original_mci = original[MCI] + original_deleted = "deleted" in original and original.deleted + else: + original_deleted = False - component = cinfo.component - ctable = cinfo.ctable + # Detect conflicts + job = self.job + original_modified = True + self.modified = True + self.conflict = False + last_sync = s3_utc(job.last_sync) + if last_sync: + if original_mtime and original_mtime < last_sync: + original_modified = False + if self.mtime and self.mtime < last_sync: + self.modified = False + if self.modified and original_modified: + self.conflict = True + if self.conflict and method in (UPDATE, DELETE, MERGE): + if job.onconflict: + job.onconflict(self) - pkey = cinfo.pkey - fkey = cinfo.fkey + if self.data is not None: + data = table._filter_fields(self.data, id=True) + else: + data = Storage() - original = None + # Update policy + if isinstance(self.update_policy, dict): + def update_policy(f): + setting = self.update_policy + p = setting.get(f, + setting.get("__default__", THIS)) + if p not in POLICY: + return THIS + return p + else: + def update_policy(f): + p = self.update_policy + if p not in POLICY: + return THIS + return p - if not component.multiple: - # Single-component: skip all subsequent items after - # the first under the same master record - if not cinfo.first: - continue - cinfo.first = False + # Log this item + if callable(job.log): + job.log(self) - # Single component = the first component record - # under the master record is always the original, - # only relevant if the master record exists in - # the db and hence item.id is not None - if item.id: - db = current.db - query = (table.id == item.id) & \ - (table[pkey] == ctable[fkey]) - if UID in ctable.fields: - # Load only the UUID now, parse will load any - # required data later - row = db(query).select(ctable[UID], - limitby = (0, 1) - ).first() - if row: - original = row[UID] - else: - # Not nice, but a rare edge-case - original = db(query).select(ctable.ALL, - limitby = (0, 1) - ).first() + tablename = self.tablename + enforce_realm_update = False - # Recurse - item_id = add_item(element = celement, - original = original, - parent = item, - joinby = (pkey, fkey)) - if item_id is None: - item.error = self.error - self.error_tree.append(deepcopy(item.element)) - else: - citem = self.items[item_id] - citem.parent = item - item.components.append(citem) + # Update existing record + if method == UPDATE: - lookahead = self.lookahead - directory = self.directory + if original: + if original_deleted: + policy = update_policy(None) + if policy == NEWER and \ + original_mtime and original_mtime > self.mtime or \ + policy == MASTER and \ + (original_mci == 0 or self.mci != 1): + self.skip = True + return True - # Handle references - table = item.table - data = item.data - tree = self.tree + for f in list(data.keys()): + if f in original: + # Check if unchanged + if type(original[f]) is datetime.datetime: + if s3_utc(data[f]) == s3_utc(original[f]): + del data[f] + continue + else: + if data[f] == original[f]: + del data[f] + continue + remove = False + policy = update_policy(f) + if policy == THIS: + remove = True + elif policy == NEWER: + if original_mtime and original_mtime > self.mtime: + remove = True + elif policy == MASTER: + if original_mci == 0 or self.mci != 1: + remove = True + if remove: + del data[f] - def schedule(reference): - """ Schedule a referenced item for implicit import """ - entry = reference.entry - if entry and entry.element is not None and not entry.item_id: - item_id = add_item(element=entry.element) - if item_id: - entry.item_id = item_id + if original_deleted: + # Undelete re-imported records + data["deleted"] = False + if hasattr(table, "deleted_fk"): + data["deleted_fk"] = "" - # Foreign key fields in table - if tree is not None: - fields = [table[f] for f in table.fields] - rfields = [f for f in fields if s3_has_foreign_key(f)] - item.references = lookahead(element, - table = table, - fields = rfields, - tree = tree, - directory = directory, - ) - for reference in item.references: - schedule(reference) + # Set new author stamp + if hasattr(table, "created_by"): + data["created_by"] = table.created_by.default + if hasattr(table, "modified_by"): + data["modified_by"] = table.modified_by.default - references = item.references - rappend = references.append + # Restore defaults for foreign keys + for fieldname in table.fields: + field = table[fieldname] + default = field.default + if str(field.type)[:9] == "reference" and \ + fieldname not in data and \ + default is not None: + data[fieldname] = default - # Parent reference - if parent is not None: - entry = Storage(item_id = parent.item_id, - element = parent.element, - tablename = parent.tablename, - ) - rappend(Storage(field = joinby, - entry = entry, - )) + # Enforce update of realm entity + enforce_realm_update = True - # References in JSON field data - json_references = s3db.get_config(table, "json_references") - if json_references: - if json_references is True: - # Discover references in any JSON fields - fields = table.fields + if not self.skip and not self.conflict and \ + (len(data) or self.components or self.references): + if self.uid and hasattr(table, UID): + data[UID] = self.uid + if MTIME in table: + data[MTIME] = self.mtime + if MCI in data: + # retain local MCI on updates + del data[MCI] + query = (table._id == self.id) + try: + db(query).update(**dict(data)) + except: + self.error = sys.exc_info()[1] + self.skip = True + return ignore_errors else: - # Discover references in fields specified by setting - fields = json_references - if not isinstance(fields, (tuple, list)): - fields = [fields] - for fieldname in fields: - value = data.get(fieldname) - field = table[fieldname] - if value and field.type == "json": - objref = S3ObjectReferences(value) - for ref in objref.refs: - rl = lookahead(None, - tree = tree, - directory = directory, - lookup = ref, - ) - if rl: - reference = rl[0] - schedule(reference) - rappend(Storage(field = fieldname, - objref = objref, - refkey = ref, - entry = reference.entry, - )) + self.committed = True + else: + # Nothing to update + self.committed = True - # Replacement reference - deleted = data.get(xml.DELETED, False) - if deleted: - fieldname = xml.REPLACEDBY - replaced_by = data.get(fieldname) - if replaced_by: - rl = lookahead(element, - tree = tree, - directory = directory, - lookup = (table, replaced_by), - ) - if rl: - reference = rl[0] - schedule(reference) - rappend(Storage(field = fieldname, - entry = reference.entry, - )) + # Create new record + elif method == CREATE: - return item.item_id + # Do not apply field policy to UID and MCI + if UID in data: + del data[UID] + if MCI in data: + del data[MCI] - # ------------------------------------------------------------------------- - def lookahead(self, - element, - table = None, - fields = None, - tree = None, - directory = None, - lookup = None): - """ - Find referenced elements in the tree + for f in data: + if update_policy(f) == MASTER and self.mci != 1: + del data[f] - @param element: the element - @param table: the DB table - @param fields: the FK fields in the table - @param tree: the import tree - @param directory: a dictionary to lookup elements in the tree - (will be filled in by this function) - """ + if self.skip: + return True - db = current.db - s3db = current.s3db + elif len(data) or self.components or self.references: - xml = current.xml - import_uid = xml.import_uid + # Restore UID and MCI + if self.uid and UID in table.fields: + data[UID] = self.uid + if MCI in table.fields: + data[MCI] = self.mci - ATTRIBUTE = xml.ATTRIBUTE - TAG = xml.TAG - UID = xml.UID + # Insert the new record + try: + success = table.insert(**dict(data)) + except: + self.error = sys.exc_info()[1] + self.skip = True + return ignore_errors + if success: + self.id = success + self.committed = True - reference_list = [] - rlappend = reference_list.append + else: + # Nothing to create + self.skip = True + return True - root = None - if tree is not None: - root = tree if isinstance(tree, etree._Element) else tree.getroot() - uidmap = self.uidmap + # Delete local record + elif method == DELETE: - references = [lookup] if lookup else element.findall("reference") - for reference in references: - if lookup: - field = None - if element is None: - tablename, attr, uid = reference - ktable = s3db.table(tablename) - if ktable is None: - continue - uids = [import_uid(uid)] if attr == "uuid" else [uid] - else: - tablename = element.get(ATTRIBUTE.name, None) - ktable, uid = reference - attr = UID - uids = [import_uid(uid)] + if original: + if original_deleted: + self.skip = True + policy = update_policy(None) + if policy == THIS: + self.skip = True + elif policy == NEWER and \ + (original_mtime and original_mtime > self.mtime): + self.skip = True + elif policy == MASTER and \ + (original_mci == 0 or self.mci != 1): + self.skip = True else: - field = reference.get(ATTRIBUTE.field, None) + self.skip = True - # Ignore references without valid field-attribute - if not field or field not in fields or field not in table: - continue + if not self.skip and not self.conflict: - # Find the key table - ktablename, _, multiple = s3_get_foreign_key(table[field]) - if not ktablename: - continue - try: - ktable = s3db[ktablename] - except AttributeError: - continue + resource = s3db.resource(tablename, id=self.id) + # Use cascade=True so that the deletion can be + # rolled back (e.g. trial phase, subsequent failure) + success = resource.delete(cascade=True) + if resource.error: + self.error = resource.error + self.skip = True + return ignore_errors - tablename = reference.get(ATTRIBUTE.resource, None) - # Ignore references to tables without UID field: - if UID not in ktable.fields: - continue - # Fall back to key table name if tablename is not specified: - if not tablename: - tablename = ktablename - # Super-entity references must use the super-key: - if tablename != ktablename: - field = (ktable._id.name, field) - # Ignore direct references to super-entities: - if tablename == ktablename and ktable._id.name != "id": - continue - # Get the foreign key - uids = reference.get(UID, None) - attr = UID - if not uids: - uids = reference.get(ATTRIBUTE.tuid, None) - attr = ATTRIBUTE.tuid - if uids and multiple: - uids = json.loads(uids) - elif uids: - uids = [uids] + return True - # Find the elements and map to DB records - relements = [] + # Merge records + elif method == MERGE: - # Create a UID<->ID map - id_map = {} - if attr == UID and uids: - if len(uids) == 1: - uid = import_uid(uids[0]) - query = (ktable[UID] == uid) - record = db(query).select(ktable.id, - cacheable = True, - limitby = (0, 1), - ).first() - if record: - id_map[uid] = record.id + if UID not in table.fields: + self.skip = True + elif original: + if original_deleted: + self.skip = True + policy = update_policy(None) + if policy == THIS: + self.skip = True + elif policy == NEWER and \ + (original_mtime and original_mtime > self.mtime): + self.skip = True + elif policy == MASTER and \ + (original_mci == 0 or self.mci != 1): + self.skip = True + else: + self.skip = True + + if not self.skip and not self.conflict: + + row = db(table[UID] == data[xml.REPLACEDBY]) \ + .select(table._id, limitby=(0, 1)) \ + .first() + if row: + original_id = row[table._id] + resource = s3db.resource(tablename, + id = [original_id, self.id], + ) + try: + success = resource.merge(original_id, self.id) + except: + self.error = sys.exc_info()[1] + self.skip = True + return ignore_errors + if success: + self.committed = True else: - uids_ = [import_uid(uid) for uid in uids] - query = (ktable[UID].belongs(uids_)) - records = db(query).select(ktable.id, - ktable[UID], - limitby = (0, len(uids_)), - ) - for r in records: - id_map[r[UID]] = r.id + self.skip = True - if not uids: - # Anonymous reference: inside the element - expr = './/%s[@%s="%s"]' % (TAG.resource, - ATTRIBUTE.name, - tablename, - ) - relements = reference.xpath(expr) - if relements and not multiple: - relements = relements[:1] + return True - elif root is not None: + else: + raise RuntimeError("unknown import method: %s" % method) - for uid in uids: + # Audit + onaccept on successful commits + if self.committed: - entry = None + # Create a pseudo-form for callbacks + form = Storage() + form.method = method + form.table = table + form.vars = self.data + prefix, name = tablename.split("_", 1) + if self.id: + form.vars.id = self.id - # Entry already in directory? - if directory is not None: - entry = directory.get((tablename, attr, uid)) + # Audit + current.audit(method, prefix, name, + form = form, + record = self.id, + representation = "xml", + ) + + # Prevent that record post-processing breaks time-delayed + # synchronization by implicitly updating "modified_on" + if MTIME in table.fields: + modified_on = table[MTIME] + modified_on_update = modified_on.update + modified_on.update = None + else: + modified_on_update = None + + # Update super entity links + s3db.update_super(table, form.vars) + if method == CREATE: + # Set record owner + current.auth.s3_set_record_owner(table, self.id) + elif method == UPDATE: + # Update realm + update_realm = enforce_realm_update or \ + s3db.get_config(table, "update_realm") + if update_realm: + current.auth.set_realm_entity(table, self.id, + force_update = True, + ) + # Onaccept + key = "%s_onaccept" % method + onaccept = current.deployment_settings.get_import_callback(tablename, key) + if onaccept: + callback(onaccept, form, tablename=tablename) + + # Restore modified_on.update + if modified_on_update is not None: + modified_on.update = modified_on_update + + # Update referencing items + if self.update and self.id: + for u in self.update: - if not entry: - e = uidmap[attr].get((tablename, uid)) if uidmap else None - if e is not None: - # Element in the source => append to relements - relements.append(e) - else: - # No element found, see if original record exists - _uid = import_uid(uid) - if _uid and _uid in id_map: - _id = id_map[_uid] - entry = Storage(tablename = tablename, - element = None, - uid = uid, - id = _id, - item_id = None, - ) - rlappend(Storage(field = field, - element = reference, - entry = entry, - )) - else: - continue - else: - rlappend(Storage(field = field, - element = reference, - entry = entry, - )) + # The other import item that shall be updated + item = u.get("item") + if not item: + continue - # Create entries for all newly found elements - for relement in relements: - uid = relement.get(attr, None) - if attr == UID: - _uid = import_uid(uid) - _id = _uid and id_map and id_map.get(_uid, None) or None + # The field in the other item that shall be updated + field = u.get("field") + if isinstance(field, (list, tuple)): + # The field references something else than the + # primary key of this table => look it up + pkey, fkey = field + query = (table.id == self.id) + row = db(query).select(table[pkey], limitby=(0, 1)).first() + ref_id = row[pkey] else: - _uid = None - _id = None - entry = Storage(tablename = tablename, - element = relement, - uid = uid, - id = _id, - item_id = None, - ) - # Add entry to directory - if uid and directory is not None: - directory[(tablename, attr, uid)] = entry - # Append the entry to the reference list - rlappend(Storage(field = field, - element = reference, - entry = entry, - )) + # The field references the primary key of this table + pkey, fkey = None, field + ref_id = self.id - return reference_list + if "refkey" in u: + # Target field is a JSON object + item._update_objref(fkey, u["refkey"], ref_id) + else: + # Target field is a reference or list:reference + item._update_reference(fkey, ref_id) + + return True # ------------------------------------------------------------------------- - def load_item(self, row): + def _dynamic_defaults(self, data): """ - Load an item from the item table (counterpart to add_item - when restoring a job from the database) + Applies dynamic defaults from any keys in data that start with + an underscore, used only for new records and only if the respective + field is not populated yet. + + :param data: the data dict """ - item = S3ImportItem(self) - if not item.restore(row): - self.error = item.error - if item.load_parent is None: - self.error_tree.append(deepcopy(item.element)) - # Update lookup lists - item_id = item.item_id - self.items[item_id] = item - return item_id + for k, v in list(data.items()): + if k[0] == "_": + fn = k[1:] + if fn in self.table.fields and fn not in data: + data[fn] = v # ------------------------------------------------------------------------- - def resolve(self, item_id, import_list): - """ - Resolve the reference list of an item + def _mandatory_fields(self): - @param item_id: the import item UID - @param import_list: the ordered list of items (UIDs) to import - """ + job = self.job - item = self.items[item_id] - if item.lock or item.accepted is False: - return False - references = [] - for reference in item.references: - ritem_id = reference.entry.item_id - if ritem_id and ritem_id not in import_list: - references.append(ritem_id) - for ritem_id in references: - item.lock = True - if self.resolve(ritem_id, import_list): - import_list.append(ritem_id) - item.lock = False - return True + mandatory = None + tablename = self.tablename + + mfields = job.mandatory_fields + if tablename in mfields: + mandatory = mfields[tablename] + + if mandatory is None: + mandatory = [] + for field in self.table: + if field.default is not None: + continue + requires = field.requires + if requires: + if not isinstance(requires, (list, tuple)): + requires = [requires] + if isinstance(requires[0], IS_EMPTY_OR): + continue + error = field.validate("")[1] + if error: + mandatory.append(field.name) + mfields[tablename] = mandatory + + return mandatory # ------------------------------------------------------------------------- - def commit(self, ignore_errors=False, log_items=None): + def _resolve_references(self): """ - Commit the import job to the DB - - @param ignore_errors: skip any items with errors - (does still report the errors) - @param log_items: callback function to log import items - before committing them + Resolve the references of this item (=look up all foreign + keys from other items of the same job). If a foreign key + is not yet available, it will be scheduled for later update. """ - ATTRIBUTE = current.xml.ATTRIBUTE - METHOD = S3ImportItem.METHOD + table = self.table + if not table: + return - # Resolve references - import_list = [] - for item_id in self.items: - self.resolve(item_id, import_list) - if item_id not in import_list: - import_list.append(item_id) - # Commit the items - items = self.items - count = 0 - errors = 0 - mtime = None - created = [] - cappend = created.append - updated = [] - deleted = [] - tablename = self.table._tablename + db = current.db + items = self.job.items + for reference in self.references: - self.log = log_items - failed = False - for item_id in import_list: - item = items[item_id] - error = None + entry = reference.entry + if not entry: + continue - if item.accepted is not False: - logged = False - success = item.commit(ignore_errors=ignore_errors) - else: - # Field validation failed - logged = True - success = ignore_errors + field = reference.field - if not success: - failed = True + # Resolve key tuples + if isinstance(field, (list, tuple)): + pkey, fkey = field + else: + pkey, fkey = ("id", field) - error = item.error - if error: - current.log.error(error) - self.error = error - element = item.element - if element is not None: - if not element.get(ATTRIBUTE.error, False): - element.set(ATTRIBUTE.error, s3_str(error)) - if not logged: - self.error_tree.append(deepcopy(element)) - if item.tablename == tablename: - errors += 1 + f = table[fkey] + if f.type == "json": + is_json = True + objref = reference.objref + if not objref: + objref = ObjectReferences(self.data.get(fkey)) + refkey = reference.refkey + if not refkey: + continue + else: + is_json = False + refkey = objref = None + ktablename, _, multiple = s3_get_foreign_key(f) + if not ktablename: + continue - elif item.tablename == tablename: - count += 1 - if mtime is None or item.mtime > mtime: - mtime = item.mtime - if item.id: - if item.method == METHOD.CREATE: - cappend(item.id) - elif item.method == METHOD.UPDATE: - updated.append(item.id) - elif item.method in (METHOD.MERGE, METHOD.DELETE): - deleted.append(item.id) + # Get the lookup table + if entry.tablename: + ktablename = entry.tablename + try: + ktable = current.s3db[ktablename] + except AttributeError: + continue - if failed: - return False + # Resolve the foreign key (value) + item = None + fk = entry.id + if entry.item_id: + item = items[entry.item_id] + if item: + if item.original and \ + item.original.get("deleted") and \ + not item.committed: + # Original is deleted and has not been updated + fk = None + else: + fk = item.id + if fk and pkey != "id": + row = db(ktable._id == fk).select(ktable[pkey], + limitby=(0, 1)).first() + if not row: + fk = None + continue + else: + fk = row[pkey] - self.count = count - self.errors = errors - self.mtime = mtime - self.created = created - self.updated = updated - self.deleted = deleted - return True + # Update record data + if fk: + if is_json: + objref.resolve(refkey[0], refkey[1], refkey[2], fk) + elif multiple: + val = self.data.get(fkey, []) + if fk not in val: + val.append(fk) + self.data[fkey] = val + else: + self.data[fkey] = fk + else: + if fkey in self.data and not multiple and not is_json: + del self.data[fkey] + if item: + update = {"item": self, "field": fkey} + if is_json: + update["refkey"] = refkey + item.update.append(update) # ------------------------------------------------------------------------- - def __define_tables(self): + def _update_reference(self, field, value): """ - Define the database tables for jobs and items + Helper method to update a foreign key in an already written + record. Will be called by the referenced item after (and only + if) it has been committed. This is only needed if the reference + could not be resolved before commit due to circular references. + + :param field: the field name of the foreign key + :param value: the value of the foreign key """ - self.job_table = self.define_job_table() - self.item_table = self.define_item_table() + table = self.table + record_id = self.id - # ------------------------------------------------------------------------- - @classmethod - def define_job_table(cls): + if not value or not table or not record_id or not self.permitted: + return db = current.db - if cls.JOB_TABLE_NAME not in db: - db.define_table(cls.JOB_TABLE_NAME, - Field("job_id", length=128, - unique=True, - notnull=True), - Field("tablename"), - Field("timestmp", "datetime", - default = datetime.datetime.utcnow() - ) - ) + update = None + + fieldtype = str(table[field].type) + if fieldtype.startswith("list:reference"): + query = (table._id == record_id) + record = db(query).select(table[field], + limitby = (0, 1), + ).first() + if record: + values = record[field] + if value not in values: + values.append(value) + update = {field: values} + else: + update = {field: value} - return db[cls.JOB_TABLE_NAME] + if update: + if "modified_on" in table.fields: + update["modified_on"] = table.modified_on + if "modified_by" in table.fields: + update["modified_by"] = table.modified_by + db(table._id == record_id).update(**update) # ------------------------------------------------------------------------- - @classmethod - def define_item_table(cls): + def _update_objref(self, field, refkey, value): + """ + Update object references in a JSON field + + :param fieldname: the name of the JSON field + :param refkey: the reference key, a tuple (tablename, uidtype, uid) + :param value: the foreign key value + """ + + + table = self.table + record_id = self.id + + if not value or not table or not record_id or not self.permitted: + return db = current.db - if cls.ITEM_TABLE_NAME not in db: - db.define_table(cls.ITEM_TABLE_NAME, - Field("item_id", length=128, - unique=True, - notnull=True), - Field("job_id", length=128), - Field("tablename", length=128), - #Field("record_id", "integer"), - Field("record_uid"), - Field("skip", "boolean"), - Field("error", "text"), - Field("data", "blob"), - Field("element", "text"), - Field("ritems", "list:string"), - Field("citems", "list:string"), - Field("parent", length=128) - ) + query = (table._id == record_id) + record = db(query).select(table._id, + table[field], + limitby = (0, 1), + ).first() + if record: + obj = record[field] + + tn, uidtype, uid = refkey + ObjectReferences(obj).resolve(tn, uidtype, uid, value) - return db[cls.ITEM_TABLE_NAME] + update = {field: obj} + if "modified_on" in table.fields: + update["modified_on"] = table.modified_on + if "modified_by" in table.fields: + update["modified_by"] = table.modified_by + record.update_record(**update) # ------------------------------------------------------------------------- - def store(self): + def store(self, item_table=None): """ - Store this job and all its items in the job table + Store this item in the DB """ - db = current.db + if item_table is None: + return None - self.__define_tables() - jobtable = self.job_table - query = jobtable.job_id == self.job_id - row = db(query).select(jobtable.id, limitby=(0, 1)).first() + item_id = self.item_id + db = current.db + row = db(item_table.item_id == item_id).select(item_table.id, + limitby=(0, 1) + ).first() if row: record_id = row.id else: record_id = None - record = Storage(job_id=self.job_id) - try: - tablename = self.table._tablename - except AttributeError: - pass - else: - record.update(tablename=tablename) - for item in self.items.values(): - item.store(item_table=self.item_table) + + record = Storage(job_id = self.job.job_id, + item_id = item_id, + tablename = self.tablename, + record_uid = self.uid, + skip = self.skip, + error = self.error or "", + ) + + if self.element is not None: + element_str = current.xml.tostring(self.element, + xml_declaration=False) + record.update(element=element_str) + + self_data = self.data + if self_data is not None: + table = self.table + fields = table.fields + data = Storage() + for f in self_data.keys(): + if f not in fields: + continue + field = table[f] + field_type = str(field.type) + if field_type == "id" or s3_has_foreign_key(field): + continue + data_ = self_data[f] + if isinstance(data_, Field): + # Not picklable + # This is likely to be a modified_on to avoid updating this field, which skipping does just fine too + continue + data.update({f: data_}) + record["data"] = pickle.dumps(data) + + ritems = [] + for reference in self.references: + field = reference.field + entry = reference.entry + store_entry = None + if entry: + if entry.item_id is not None: + store_entry = {"field": field, + "item_id": str(entry.item_id), + } + elif entry.uid is not None: + store_entry = {"field": field, + "tablename": entry.tablename, + "uid": str(entry.uid), + } + if store_entry is not None: + ritems.append(json.dumps(store_entry)) + if ritems: + record.update(ritems=ritems) + citems = [c.item_id for c in self.components] + if citems: + record.update(citems=citems) + if self.parent: + record.update(parent=self.parent.item_id) if record_id: - db(jobtable.id == record_id).update(**record) + db(item_table.id == record_id).update(**record) else: - record_id = jobtable.insert(**record) + record_id = item_table.insert(**record) return record_id # ------------------------------------------------------------------------- - def get_tree(self): - """ - Reconstruct the element tree of this job + def restore(self, row): """ + Restore an item from a item table row. This does not restore + the references (since this can not be done before all items + are restored), must call job.restore_references() to do that - if self.tree is not None: - return self.tree - else: - xml = current.xml - ATTRIBUTE = xml.ATTRIBUTE - UID = xml.UID - root = etree.Element(xml.TAG.root) - for item in self.items.values(): - element = item.element - if element is not None and not item.parent: - if item.tablename == self.table._tablename or \ - element.get(UID, None) or \ - element.get(ATTRIBUTE.tuid, None): - root.append(deepcopy(element)) - return etree.ElementTree(root) - - # ------------------------------------------------------------------------- - def delete(self): - """ - Delete this job and all its items from the job table + :param row: the item table row """ - db = current.db + xml = current.xml + + self.item_id = row.item_id + self.accepted = None + self.permitted = False + self.committed = False + tablename = row.tablename + self.id = None + self.uid = row.record_uid + self.skip = row.skip + if row.data is not None: + self.data = pickle.loads(row.data) + else: + self.data = Storage() + data = self.data + if xml.MTIME in data: + self.mtime = data[xml.MTIME] + if xml.MCI in data: + self.mci = data[xml.MCI] + UID = xml.UID + if UID in data: + self.uid = data[UID] + self.element = etree.fromstring(row.element) + if row.citems: + self.load_components = row.citems + if row.ritems: + self.load_references = [json.loads(ritem) for ritem in row.ritems] + self.load_parent = row.parent + s3db = current.s3db + try: + table = s3db[tablename] + except AttributeError: + self.error = current.ERROR.BAD_RESOURCE + return False + else: + self.table = table + self.tablename = tablename + from ..resource import S3Resource + original = S3Resource.original(table, self.data, + mandatory=self._mandatory_fields()) + if original is not None: + self.original = original + self.id = original[table._id.name] + if not current.response.s3.synchronise_uuids and UID in original: + self.uid = self.data[UID] = original[UID] + self.error = row.error + postprocess = s3db.get_config(self.tablename, "xml_post_parse") + if postprocess: + postprocess(self.element, self.data) + if self.error and not self.data: + # Validation error + return False + return True - #current.log.debug("Deleting job ID=%s" % self.job_id) +# ============================================================================= +class SyncPolicy(object): + """ Synchronization Policy """ - self.__define_tables() - item_table = self.item_table - query = item_table.job_id == self.job_id - db(query).delete() - job_table = self.job_table - query = job_table.job_id == self.job_id - db(query).delete() + THIS = "THIS" # never update + OTHER = "OTHER" # always update + NEWER = "NEWER" # keep the newer record + MASTER = "MASTER" # keep the record with the lower MCI - # ------------------------------------------------------------------------- - def restore_references(self): + def __init__(self, + onupdate = None, + onconflict = None, + resolve = None, + last_sync = None, + ): """ - Restore the job's reference structure after loading items - from the item table + :param str onupdate: update policy + :param str onconflict: conflict policy + :param function resolve: callback to resolve conflicts, receives + the import item as parameter + :param datetime last_sync: date and time of the last sync run + with the remote repository """ - db = current.db - UID = current.xml.UID - - for item in self.items.values(): - for citem_id in item.load_components: - if citem_id in self.items: - item.components.append(self.items[citem_id]) - item.load_components = [] - for ritem in item.load_references: - field = ritem["field"] - if "item_id" in ritem: - item_id = ritem["item_id"] - if item_id in self.items: - _item = self.items[item_id] - entry = Storage(tablename=_item.tablename, - element=_item.element, - uid=_item.uid, - id=_item.id, - item_id=item_id) - item.references.append(Storage(field=field, - entry=entry)) - else: - _id = None - uid = ritem.get("uid", None) - tablename = ritem.get("tablename", None) - if tablename and uid: - try: - table = current.s3db[tablename] - except AttributeError: - continue - if UID not in table.fields: - continue - query = table[UID] == uid - row = db(query).select(table._id, - limitby=(0, 1)).first() - if row: - _id = row[table._id.name] - else: - continue - entry = Storage(tablename = ritem["tablename"], - element=None, - uid = ritem["uid"], - id = _id, - item_id = None) - item.references.append(Storage(field=field, - entry=entry)) - item.load_references = [] - if item.load_parent is not None: - parent = self.items[item.load_parent] - if parent is None: - # Parent has been removed - item.skip = True - else: - item.parent = parent - item.load_parent = None + self.update_policy = onupdate + self.conflict_policy = onconflict + self.conflict_resolver = resolve + self.last_sync = last_sync # ============================================================================= -class S3ObjectReferences(object): +class ObjectReferences(object): """ Utility to discover and resolve references in a JSON object; handles both uuid- and tuid-based references @@ -2250,12 +2571,12 @@ class S3ObjectReferences(object): - resolve() replaces them with: "": - @example: + .. example:: # Get a list of all references in obj - refs = S3ObjectReferences(obj).refs - @example + refs = ObjectReferences(obj).refs + .. example:: # Resolve a reference in obj - S3ObjectReferences(obj).resolve("req_req", "uuid", "REQ1", 57) + ObjectReferences(obj).resolve("req_req", "uuid", "REQ1", 57) """ TABLENAME_KEYS = ("@resource", "r") @@ -2264,9 +2585,7 @@ class S3ObjectReferences(object): def __init__(self, obj): """ - Constructor - - @param obj: the object to inspect (parsed) + :param obj: the object to inspect (parsed) """ self.obj = obj @@ -2280,7 +2599,7 @@ def refs(self): """ List of references discovered in the object (lazy property) - @returns: a list of tuples (tablename, uidtype, uid) + :returns: a list of tuples (tablename, uidtype, uid) """ if self._refs is None: @@ -2295,7 +2614,7 @@ def objs(self): """ A dict with pointers to the references inside the object - @returns: a dict {(tablename, uidtype, uid): (obj, key)} + :returns: a dict {(tablename, uidtype, uid): (obj, key)} """ if self._objs is None: @@ -2310,7 +2629,7 @@ def _traverse(self, obj): Traverse a (possibly nested) object and find all references, populates self.refs and self.objs - @param obj: the object to inspect + :param obj: the object to inspect """ refs = self._refs @@ -2362,10 +2681,10 @@ def resolve(self, tablename, uidtype, uid, value): Resolve a reference in self.obj with the given value; will resolve all occurences of the reference - @param tablename: the referenced table - @param uidtype: the type of uid (uuid or tuid) - @param uid: the uuid or tuid - @param value: the value to resolve the reference + :param tablename: the referenced table + :param uidtype: the type of uid (uuid or tuid) + :param uid: the uuid or tuid + :param value: the value to resolve the reference """ items = self.objs.get((tablename, uidtype, uid)) @@ -2387,19 +2706,15 @@ def __init__(self, noupdate = False, ): """ - Constructor - - @param primary: list or tuple of primary fields to find a + :param primary: list or tuple of primary fields to find a match, must always match (mandatory, defaults to "name" field) - @param secondary: list or tuple of secondary fields to + :param secondary: list or tuple of secondary fields to find a match, must match if values are present in the import item - @param ignore_case: ignore case for string/text fields - @param ignore_deleted: do not match deleted records - @param noupdate: match, but do not update - - @ToDo: Fuzzy option to do a LIKE search + :param ignore_case: ignore case for string/text fields + :param ignore_deleted: do not match deleted records + :param noupdate: match, but do not update """ if not primary: @@ -2420,12 +2735,12 @@ def __call__(self, item): """ Entry point for importer - @param item: the import item + :param item: the import item - @return: the duplicate Row if match found, otherwise None + :returns: the duplicate Row if match found, otherwise None - @raise SyntaxError: if any of the query fields doesn't exist - in the item table + :raises SyntaxError: if any of the query fields doesn't exist + in the item table """ data = item.data @@ -2484,10 +2799,10 @@ def match(self, field, value): """ Helper function to generate a match-query - @param field: the Field - @param value: the value + :param field: the Field + :param value: the value - @return: a Query + :returns: a Query """ ftype = str(field.type) @@ -2508,956 +2823,4 @@ def match(self, field, value): return query -# ============================================================================= -class S3BulkImporter(object): - """ - Import CSV files of data to pre-populate the database. - Suitable for use in Testing, Demos & Simulations - - http://eden.sahanafoundation.org/wiki/DeveloperGuidelines/PrePopulate - """ - - def __init__(self): - """ Constructor """ - - import csv - from xml.sax.saxutils import unescape - - self.csv = csv - self.unescape = unescape - self.tasks = [] - # Some functions refer to a different resource - self.alternateTables = { - "hrm_group_membership": {"tablename": "pr_group_membership", - "prefix": "pr", - "name": "group_membership"}, - "hrm_person": {"tablename": "pr_person", - "prefix": "pr", - "name": "person"}, - "member_person": {"tablename": "pr_person", - "prefix": "pr", - "name": "person"}, - } - # Keep track of which resources have been customised so we don't do this twice - self.customised = [] - self.errorList = [] - self.resultList = [] - - # ------------------------------------------------------------------------- - def load_descriptor(self, path): - """ - Load the descriptor file and then all the import tasks in that file - into the task property. - The descriptor file is the file called tasks.cfg in path. - The file consists of a comma separated list of: - module, resource name, csv filename, xsl filename. - """ - - source = open(os.path.join(path, "tasks.cfg"), "r") - values = self.csv.reader(source) - for details in values: - if details == []: - continue - prefix = details[0][0].strip('" ') - if prefix == "#": # comment - continue - if prefix == "*": # specialist function - self.extract_other_import_line(path, details) - else: # standard CSV importer - self.extract_csv_import_line(path, details) - - # ------------------------------------------------------------------------- - def extract_csv_import_line(self, path, details): - """ - Extract the details for a CSV Import Task - """ - - argCnt = len(details) - if argCnt == 4 or argCnt == 5: - # Remove any spaces and enclosing double quote - mod = details[0].strip('" ') - res = details[1].strip('" ') - folder = current.request.folder - - csvFileName = details[2].strip('" ') - if csvFileName[:7] == "http://": - csv = csvFileName - else: - (csvPath, csvFile) = os.path.split(csvFileName) - if csvPath != "": - path = os.path.join(folder, - "modules", - "templates", - csvPath) - # @todo: deprecate this block once migration completed - if not os.path.exists(path): - # Non-standard location (legacy template)? - path = os.path.join(folder, - "private", - "templates", - csvPath) - csv = os.path.join(path, csvFile) - - xslFileName = details[3].strip('" ') - templateDir = os.path.join(folder, - "static", - "formats", - "s3csv") - # Try the module directory in the templates directory first - xsl = os.path.join(templateDir, mod, xslFileName) - if os.path.exists(xsl) == False: - # Now try the templates directory - xsl = os.path.join(templateDir, xslFileName) - if os.path.exists(xsl) == False: - # Use the same directory as the csv file - xsl = os.path.join(path, xslFileName) - if os.path.exists(xsl) == False: - self.errorList.append( - "Failed to find a transform file %s, Giving up." % xslFileName) - return - - if argCnt == 5: - extra_data = details[4] - else: - extra_data = None - self.tasks.append([1, mod, res, csv, xsl, extra_data]) - else: - self.errorList.append( - "prepopulate error: job not of length 4, ignored: %s" % str(details)) - - # ------------------------------------------------------------------------- - def extract_other_import_line(self, path, details): - """ - Store a single import job into the tasks property - *,function,filename,*extraArgs - """ - - function = details[1].strip('" ') - filepath = None - if len(details) >= 3: - filename = details[2].strip('" ') - if filename != "": - (subfolder, filename) = os.path.split(filename) - if subfolder != "": - path = os.path.join(current.request.folder, - "modules", - "templates", - subfolder) - # @todo: deprecate this block once migration completed - if not os.path.exists(path): - # Non-standard location (legacy template)? - path = os.path.join(current.request.folder, - "private", - "templates", - subfolder) - filepath = os.path.join(path, filename) - - if len(details) >= 4: - extraArgs = details[3:] - else: - extraArgs = None - - self.tasks.append((2, function, filepath, extraArgs)) - - # ------------------------------------------------------------------------- - def execute_import_task(self, task): - """ - Execute each import job, in order - """ - - # Disable min_length for password during prepop - current.auth.ignore_min_password_length() - - start = datetime.datetime.now() - if task[0] == 1: - s3db = current.s3db - response = current.response - error_string = "prepopulate error: file %s missing" - # Store the view - view = response.view - - #current.log.debug("Running job %s %s (filename=%s transform=%s)" % (task[1], - # task[2], - # task[3], - # task[4], - # )) - - prefix = task[1] - name = task[2] - tablename = "%s_%s" % (prefix, name) - if tablename in self.alternateTables: - details = self.alternateTables[tablename] - if "tablename" in details: - tablename = details["tablename"] - s3db.table(tablename) - if "loader" in details: - loader = details["loader"] - if loader is not None: - loader() - if "prefix" in details: - prefix = details["prefix"] - if "name" in details: - name = details["name"] - - try: - resource = s3db.resource(tablename) - except AttributeError: - # Table cannot be loaded - self.errorList.append("WARNING: Unable to find table %s import job skipped" % tablename) - return - - # Check if the source file is accessible - filename = task[3] - if filename[:7] == "http://": - req = urllib2.Request(url=filename) - try: - f = urlopen(req) - except HTTPError as e: - self.errorList.append("Could not access %s: %s" % (filename, e.read())) - return - except: - self.errorList.append(error_string % filename) - return - else: - csv = f - else: - try: - csv = open(filename, "rb") - except IOError: - self.errorList.append(error_string % filename) - return - - # Check if the stylesheet is accessible - try: - S = open(task[4], "r") - except IOError: - self.errorList.append(error_string % task[4]) - return - else: - S.close() - - if tablename not in self.customised: - # Customise the resource - customise = current.deployment_settings.customise_resource(tablename) - if customise: - request = S3Request(prefix, name, current.request) - customise(request, tablename) - self.customised.append(tablename) - - extra_data = None - if task[5]: - try: - extradata = self.unescape(task[5], {"'": '"'}) - extradata = json.loads(extradata) - extra_data = extradata - except: - self.errorList.append("WARNING:5th parameter invalid, parameter %s ignored" % task[5]) - auth = current.auth - auth.rollback = True - try: - # @todo: add extra_data and file attachments - resource.import_xml(csv, - format = "csv", - stylesheet = task[4], - extra_data = extra_data, - ) - except SyntaxError as e: - self.errorList.append("WARNING: import error - %s (file: %s, stylesheet: %s)" % - (e, filename, task[4])) - auth.rollback = False - return - - if not resource.error: - current.db.commit() - else: - # Must roll back if there was an error! - error = resource.error - self.errorList.append("%s - %s: %s" % ( - task[3], resource.tablename, error)) - errors = current.xml.collect_errors(resource) - if errors: - self.errorList.extend(errors) - current.db.rollback() - - auth.rollback = False - - # Restore the view - response.view = view - end = datetime.datetime.now() - duration = end - start - csvName = task[3][task[3].rfind("/") + 1:] - duration = '{:.2f}'.format(duration.total_seconds()) - msg = "%s imported (%s sec)" % (csvName, duration) - self.resultList.append(msg) - current.log.debug(msg) - - # ------------------------------------------------------------------------- - def execute_special_task(self, task): - """ - Execute import tasks which require a custom function, - such as import_role - """ - - start = datetime.datetime.now() - s3 = current.response.s3 - if task[0] == 2: - fun = task[1] - filepath = task[2] - extraArgs = task[3] - if filepath is None: - if extraArgs is None: - error = s3[fun]() - else: - error = s3[fun](*extraArgs) - elif extraArgs is None: - error = s3[fun](filepath) - else: - error = s3[fun](filepath, *extraArgs) - if error: - self.errorList.append(error) - end = datetime.datetime.now() - duration = end - start - duration = '{:.2f}'.format(duration.total_seconds()) - msg = "%s completed (%s sec)" % (fun, duration) - self.resultList.append(msg) - current.log.debug(msg) - - # ------------------------------------------------------------------------- - @staticmethod - def _lookup_pe(entity): - """ - Convert an Entity to a pe_id - - helper for import_role - - assumes org_organisation.name unless specified - - entity needs to exist already - """ - - if "=" in entity: - pe_type, value = entity.split("=") - else: - pe_type = "org_organisation.name" - value = entity - pe_tablename, pe_field = pe_type.split(".") - - table = current.s3db.table(pe_tablename) - record = current.db(table[pe_field] == value).select(table.pe_id, - limitby = (0, 1) - ).first() - try: - pe_id = record.pe_id - except AttributeError: - current.log.warning("import_role cannot find pe_id for %s" % entity) - pe_id = None - - return pe_id - - # ------------------------------------------------------------------------- - def import_role(self, filename): - """ - Import Roles from CSV - """ - - # Check if the source file is accessible - try: - openFile = open(filename, "r") - except IOError: - return "Unable to open file %s" % filename - - auth = current.auth - acl = auth.permission - create_role = auth.s3_create_role - - def parseACL(_acl): - permissions = _acl.split("|") - acl_value = 0 - for permission in permissions: - if permission == "READ": - acl_value |= acl.READ - if permission == "CREATE": - acl_value |= acl.CREATE - if permission == "UPDATE": - acl_value |= acl.UPDATE - if permission == "DELETE": - acl_value |= acl.DELETE - if permission == "REVIEW": - acl_value |= acl.REVIEW - if permission == "APPROVE": - acl_value |= acl.APPROVE - if permission == "PUBLISH": - acl_value |= acl.PUBLISH - if permission == "ALL": - acl_value |= acl.ALL - return acl_value - - reader = self.csv.DictReader(openFile) - roles = {} - acls = {} - args = {} - for row in reader: - if row != None: - row_get = row.get - role = row_get("role") - desc = row_get("description", "") - rules = {} - extra_param = {} - controller = row_get("controller") - if controller: - rules["c"] = controller - fn = row_get("function") - if fn: - rules["f"] = fn - table = row_get("table") - if table: - rules["t"] = table - oacl = row_get("oacl") - if oacl: - rules["oacl"] = parseACL(oacl) - uacl = row_get("uacl") - if uacl: - rules["uacl"] = parseACL(uacl) - #org = row_get("org") - #if org: - # rules["organisation"] = org - #facility = row_get("facility") - #if facility: - # rules["facility"] = facility - entity = row_get("entity") - if entity: - if entity == "any": - # Pass through as-is - pass - else: - # NB Entity here is *not* hierarchical! - try: - entity = int(entity) - except ValueError: - entity = self._lookup_pe(entity) - rules["entity"] = entity - flag = lambda s: bool(s) and s.lower() in ("1", "true", "yes") - hidden = row_get("hidden") - if hidden: - extra_param["hidden"] = flag(hidden) - system = row_get("system") - if system: - extra_param["system"] = flag(system) - protected = row_get("protected") - if protected: - extra_param["protected"] = flag(protected) - uid = row_get("uid") - if uid: - extra_param["uid"] = uid - if role in roles: - acls[role].append(rules) - else: - roles[role] = [role, desc] - acls[role] = [rules] - if len(extra_param) > 0 and role not in args: - args[role] = extra_param - for rulelist in roles.values(): - if rulelist[0] in args: - create_role(rulelist[0], - rulelist[1], - *acls[rulelist[0]], - **args[rulelist[0]]) - else: - create_role(rulelist[0], - rulelist[1], - *acls[rulelist[0]]) - - # ------------------------------------------------------------------------- - def import_user(self, filename): - """ - Import Users from CSV with an import Prep - """ - - current.response.s3.import_prep = current.auth.s3_import_prep - - current.s3db.add_components("auth_user", - auth_masterkey = "user_id", - ) - - user_task = [1, - "auth", - "user", - filename, - os.path.join(current.request.folder, - "static", - "formats", - "s3csv", - "auth", - "user.xsl" - ), - None - ] - self.execute_import_task(user_task) - - # ------------------------------------------------------------------------- - def import_feed(self, filename): - """ - Import RSS Feeds from CSV with an import Prep - """ - - stylesheet = os.path.join(current.request.folder, - "static", - "formats", - "s3csv", - "msg", - "rss_channel.xsl" - ) - - # 1st import any Contacts - current.response.s3.import_prep = current.s3db.pr_import_prep - user_task = [1, - "pr", - "contact", - filename, - stylesheet, - None - ] - self.execute_import_task(user_task) - - # Then import the Channels - user_task = [1, - "msg", - "rss_channel", - filename, - stylesheet, - None - ] - self.execute_import_task(user_task) - - # ------------------------------------------------------------------------- - def import_image(self, - filename, - tablename, - idfield, - imagefield - ): - """ - Import images, such as a logo or person image - - filename a CSV list of records and filenames - tablename the name of the table - idfield the field used to identify the record - imagefield the field to where the image will be added - - Example: - bi.import_image ("org_logos.csv", "org_organisation", "name", "logo") - and the file org_logos.csv may look as follows - id file - Sahana Software Foundation sahanalogo.jpg - American Red Cross icrc.gif - """ - - # Check if the source file is accessible - try: - openFile = open(filename, "r", encoding="utf-8") - except IOError: - return "Unable to open file %s" % filename - - prefix, name = tablename.split("_", 1) - - reader = self.csv.DictReader(openFile) - - db = current.db - s3db = current.s3db - audit = current.audit - table = s3db[tablename] - idfield = table[idfield] - base_query = (table.deleted == False) - fieldnames = [table._id.name, - imagefield - ] - # https://github.com/web2py/web2py/blob/master/gluon/sqlhtml.py#L1947 - for field in table: - if field.name not in fieldnames and field.writable is False \ - and field.update is None and field.compute is None: - fieldnames.append(field.name) - fields = [table[f] for f in fieldnames] - - # Get callbacks - get_config = s3db.get_config - onvalidation = get_config(tablename, "update_onvalidation") or \ - get_config(tablename, "onvalidation") - onaccept = get_config(tablename, "update_onaccept") or \ - get_config(tablename, "onaccept") - update_realm = get_config(tablename, "update_realm") - if update_realm: - set_realm_entity = current.auth.set_realm_entity - update_super = s3db.update_super - - for row in reader: - if row != None: - # Open the file - image = row["file"] - try: - # Extract the path to the CSV file, image should be in - # this directory, or relative to it - path = os.path.split(filename)[0] - imagepath = os.path.join(path, image) - openFile = open(imagepath, "rb") - except IOError: - current.log.error("Unable to open image file %s" % image) - continue - image_source = BytesIO(openFile.read()) - # Get the id of the resource - query = base_query & (idfield == row["id"]) - record = db(query).select(limitby = (0, 1), - *fields).first() - try: - record_id = record.id - except AttributeError: - current.log.error("Unable to get record %s of the resource %s to attach the image file to" % (row["id"], tablename)) - continue - # Create and accept the form - form = SQLFORM(table, record, fields=["id", imagefield]) - form_vars = Storage() - form_vars._formname = "%s/%s" % (tablename, record_id) - form_vars.id = record_id - source = Storage() - source.filename = imagepath - source.file = image_source - form_vars[imagefield] = source - if form.accepts(form_vars, onvalidation=onvalidation): - # Audit - audit("update", prefix, name, form=form, - record=record_id, representation="csv") - - # Update super entity links - update_super(table, form_vars) - - # Update realm - if update_realm: - set_realm_entity(table, form_vars, force_update=True) - - # Execute onaccept - callback(onaccept, form, tablename=tablename) - else: - for (key, error) in form.errors.items(): - current.log.error("error importing logo %s: %s %s" % (image, key, error)) - - # ------------------------------------------------------------------------- - @staticmethod - def import_font(url): - """ - Install a Font - """ - - if url == "unifont": - #url = "http://unifoundry.com/pub/unifont-7.0.06/font-builds/unifont-7.0.06.ttf" - #url = "http://unifoundry.com/pub/unifont-10.0.07/font-builds/unifont-10.0.07.ttf" - url = "http://unifoundry.com/pub/unifont/unifont-13.0.01/font-builds/unifont-13.0.01.ttf" - # Rename to make version upgrades be transparent - filename = "unifont.ttf" - extension = "ttf" - else: - filename = url.split("/")[-1] - filename, extension = filename.rsplit(".", 1) - - if extension not in ("ttf", "gz", "zip"): - current.log.warning("Unsupported font extension: %s" % extension) - return - - filename = "%s.ttf" % filename - - fontPath = os.path.join(current.request.folder, "static", "fonts") - if os.path.exists(os.path.join(fontPath, filename)): - current.log.warning("Using cached copy of %s" % filename) - return - - # Download as we have no cached copy - - # Copy the current working directory to revert back to later - cwd = os.getcwd() - - # Set the current working directory - os.chdir(fontPath) - try: - _file = fetch(url) - except URLError as exception: - current.log.error(exception) - # Revert back to the working directory as before. - os.chdir(cwd) - return - - if extension == "gz": - import tarfile - tf = tarfile.open(fileobj = StringIO(_file)) - tf.extractall() - - elif extension == "zip": - import zipfile - zf = zipfile.ZipFile(StringIO(_file)) - zf.extractall() - - else: - f = open(filename, "wb") - f.write(_file) - f.close() - - # Revert back to the working directory as before. - os.chdir(cwd) - - # ------------------------------------------------------------------------- - def import_remote_csv(self, url, prefix, resource, stylesheet): - """ Import CSV files from remote servers """ - - extension = url.split(".")[-1] - if extension not in ("csv", "zip"): - current.log.error("error importing remote file %s: invalid extension" % (url)) - return - - # Copy the current working directory to revert back to later - cwd = os.getcwd() - - # Shortcut - os_path = os.path - os_path_exists = os_path.exists - os_path_join = os_path.join - - # Create the working directory - TEMP = os_path_join(cwd, "temp") - if not os_path_exists(TEMP): # use web2py/temp/remote_csv as a cache - import tempfile - TEMP = tempfile.gettempdir() - tempPath = os_path_join(TEMP, "remote_csv") - if not os_path_exists(tempPath): - try: - os.mkdir(tempPath) - except OSError: - current.log.error("Unable to create temp folder %s!" % tempPath) - return - - filename = url.split("/")[-1] - if extension == "zip": - filename = filename.replace(".zip", ".csv") - if os_path_exists(os_path_join(tempPath, filename)): - current.log.warning("Using cached copy of %s" % filename) - else: - # Download if we have no cached copy - # Set the current working directory - os.chdir(tempPath) - try: - _file = fetch(url) - except URLError as exception: - current.log.error(exception) - # Revert back to the working directory as before. - os.chdir(cwd) - return - - if extension == "zip": - # Need to unzip - import zipfile - try: - myfile = zipfile.ZipFile(StringIO(_file)) - except zipfile.BadZipfile as exception: - # e.g. trying to download through a captive portal - current.log.error(exception) - # Revert back to the working directory as before. - os.chdir(cwd) - return - files = myfile.infolist() - for f in files: - filename = f.filename - extension = filename.split(".")[-1] - if extension == "csv": - _file = myfile.read(filename) - _f = open(filename, "w") - _f.write(_file) - _f.close() - break - myfile.close() - else: - f = open(filename, "w") - f.write(_file) - f.close() - - # Revert back to the working directory as before. - os.chdir(cwd) - - task = [1, prefix, resource, - os_path_join(tempPath, filename), - os_path_join(current.request.folder, - "static", - "formats", - "s3csv", - prefix, - stylesheet - ), - None - ] - self.execute_import_task(task) - - # ------------------------------------------------------------------------- - @staticmethod - def import_script(filename): - """ - Run a custom Import Script - - @ToDo: Report Errors during Script run to console better - """ - - from gluon.cfs import getcfs - from gluon.compileapp import build_environment - from gluon.restricted import restricted - - environment = build_environment(current.request, current.response, current.session) - environment["current"] = current - environment["auth"] = current.auth - environment["db"] = current.db - environment["gis"] = current.gis - environment["s3db"] = current.s3db - environment["settings"] = current.deployment_settings - - code = getcfs(filename, filename, None) - restricted(code, environment, layer=filename) - - # ------------------------------------------------------------------------- - def import_task(self, - task_name, - args_json = None, - vars_json = None - ): - """ - Import a Scheduled Task - """ - - # Store current value of Bulk - bulk = current.response.s3.bulk - # Set Bulk to true for this parse - current.response.s3.bulk = True - validator = IS_JSONS3() - if args_json: - task_args, error = validator(args_json) - if error: - self.errorList.append(error) - return - else: - task_args = [] - if vars_json: - all_vars, error = validator(vars_json) - if error: - self.errorList.append(error) - return - else: - all_vars = {} - # Restore bulk setting - current.response.s3.bulk = bulk - - kwargs = {} - task_vars = {} - options = ("function_name", - "start_time", - "next_run_time", - "stop_time", - "repeats", - "period", # seconds - "timeout", # seconds - "enabled", # None = Enabled - "group_name", - "ignore_duplicate", - "sync_output", - ) - for var in all_vars: - if var in options: - kwargs[var] = all_vars[var] - else: - task_vars[var] = all_vars[var] - - current.s3task.schedule_task(task_name.split(os.path.sep)[-1], # Strip the path - args = task_args, - vars = task_vars, - **kwargs - ) - - # ------------------------------------------------------------------------- - def import_xml(self, - filepath, - prefix, - resourcename, - dataformat, - source_type = None, - ): - """ - Import XML data using an XSLT: static/formats//import.xsl - Setting the source_type is possible - """ - - # Remove any spaces and enclosing double quote - prefix = prefix.strip('" ') - resourcename = resourcename.strip('" ') - - try: - source = open(filepath, "rb") - except IOError: - error_string = "prepopulate error: file %s missing" - self.errorList.append(error_string % filepath) - return - - stylesheet = os.path.join(current.request.folder, - "static", - "formats", - dataformat, - "import.xsl") - try: - xslt_file = open(stylesheet, "r") - except IOError: - error_string = "prepopulate error: file %s missing" - self.errorList.append(error_string % stylesheet) - return - else: - xslt_file.close() - - tablename = "%s_%s" % (prefix, resourcename) - resource = current.s3db.resource(tablename) - - if tablename not in self.customised: - # Customise the resource - customise = current.deployment_settings.customise_resource(tablename) - if customise: - request = S3Request(prefix, resourcename, current.request) - customise(request, tablename) - self.customised.append(tablename) - - auth = current.auth - auth.rollback = True - try: - resource.import_xml(source, - stylesheet = stylesheet, - source_type = source_type, - ) - except SyntaxError as e: - self.errorList.append("WARNING: import error - %s (file: %s, stylesheet: %s/import.xsl)" % - (e, filepath, dataformat)) - auth.rollback = False - return - - if not resource.error: - current.db.commit() - else: - # Must roll back if there was an error! - error = resource.error - self.errorList.append("%s - %s: %s" % ( - filepath, tablename, error)) - errors = current.xml.collect_errors(resource) - if errors: - self.errorList.extend(errors) - current.db.rollback() - - auth.rollback = False - - # ------------------------------------------------------------------------- - def perform_tasks(self, path): - """ - Load and then execute the import jobs that are listed in the - descriptor file (tasks.cfg) - """ - - self.load_descriptor(path) - for task in self.tasks: - if task[0] == 1: - self.execute_import_task(task) - elif task[0] == 2: - self.execute_special_task(task) - # END ========================================================================= diff --git a/modules/core/filters/query.py b/modules/core/resource/query.py similarity index 100% rename from modules/core/filters/query.py rename to modules/core/resource/query.py diff --git a/modules/core/resource/resource.py b/modules/core/resource/resource.py new file mode 100644 index 0000000000..31043b3717 --- /dev/null +++ b/modules/core/resource/resource.py @@ -0,0 +1,2831 @@ +# -*- coding: utf-8 -*- + +""" CRUD Resource + + @copyright: 2009-2021 (c) Sahana Software Foundation + @license: MIT + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +""" + +__all__ = ("S3Resource", + "MAXDEPTH", + ) + +import json + +from functools import reduce +from lxml import etree + +from gluon import current +from gluon.html import A +from gluon.validators import IS_EMPTY_OR +from gluon.storage import Storage +from gluon.tools import callback + +from s3dal import Row, Rows, Table +from ..tools import s3_format_datetime, s3_get_last_record_id, s3_has_foreign_key, s3_remove_last_record_id, s3_str, IS_ONE_OF +from ..ui import S3DataTable, S3DataList +from ..model import s3_all_meta_field_names + +from .components import S3Components +from .query import FS, S3ResourceField, S3Joins +from .rfilter import S3ResourceFilter +from .select import S3ResourceData + +#osetattr = object.__setattr__ +ogetattr = object.__getattribute__ + +MAXDEPTH = 10 +DEFAULT = lambda: None + +# ============================================================================= +class S3Resource(object): + """ + API for resources. + + A "resource" is a set of records in a database table including their + references in certain related resources (components). A resource can + be defined like: + + resource = S3Resource(table) + + A resource defined like this would include all records in the table. + Further parameters for the resource constructor as well as methods + of the resource instance can be used to filter for particular subsets. + + This API provides extended standard methods to access and manipulate + data in resources while respecting current authorization and other + S3 framework rules. + """ + + def __init__(self, tablename, + id = None, + prefix = None, + uid = None, + filter = None, + vars = None, + parent = None, + linked = None, + linktable = None, + alias = None, + components = None, + filter_component = None, + include_deleted = False, + approved = True, + unapproved = False, + context = False, + extra_filters = None + ): + """ + :param tablename: tablename, Table, or an S3Resource instance + :param prefix: prefix to use for the tablename + + :param id: record ID (or list of record IDs) + :param uid: record UID (or list of record UIDs) + + :param filter: filter query + :param vars: dictionary of URL query variables + + :param components: list of component aliases + to load for this resource + :param filter_component: alias of the component the URL filters + apply for (filters for this component + must be handled separately) + + :param alias: the alias for this resource (internal use only) + :param parent: the parent resource (internal use only) + :param linked: the linked resource (internal use only) + :param linktable: the link table (internal use only) + + :param include_deleted: include deleted records (used for + synchronization) + + :param approved: include approved records + :param unapproved: include unapproved records + :param context: apply context filters + :param extra_filters: extra filters (to be applied on + pre-filtered subsets), as list of + tuples (method, expression) + """ + + s3db = current.s3db + auth = current.auth + + # Names --------------------------------------------------------------- + + table = None + table_alias = None + + if prefix is None: + if not isinstance(tablename, str): + if isinstance(tablename, Table): + table = tablename + table_alias = table._tablename + tablename = table_alias + elif isinstance(tablename, S3Resource): + table = tablename.table + table_alias = table._tablename + tablename = tablename.tablename + else: + error = "%s is not a valid type for a tablename" % tablename + raise SyntaxError(error) + if "_" in tablename: + prefix, name = tablename.split("_", 1) + else: + raise SyntaxError("invalid tablename: %s" % tablename) + else: + name = tablename + tablename = "%s_%s" % (prefix, name) + + self.tablename = tablename + + # Module prefix and resource name + self.prefix = prefix + self.name = name + + # Resource alias defaults to tablename without module prefix + if not alias: + alias = name + self.alias = alias + + # Table --------------------------------------------------------------- + + if table is None: + table = s3db[tablename] + + # Set default approver + auth.permission.set_default_approver(table) + + if parent is not None: + if parent.tablename == self.tablename: + # Component table same as parent table => must use table alias + table_alias = "%s_%s_%s" % (prefix, alias, name) + table = s3db.get_aliased(table, table_alias) + + self.table = table + self._alias = table_alias or tablename + + self.fields = table.fields + self._id = table._id + + self.defaults = None + + # Hooks --------------------------------------------------------------- + + # Authorization hooks + self.accessible_query = auth.s3_accessible_query + + # Filter -------------------------------------------------------------- + + # Default query options + self.include_deleted = include_deleted + self._approved = approved + self._unapproved = unapproved + + # Component Filter + self.filter = None + + # Resource Filter + self.rfilter = None + + # Rows ---------------------------------------------------------------- + + self._rows = None + self._rowindex = None + self.rfields = None + self.dfields = None + self._ids = [] + self._uids = [] + self._length = None + + # Request attributes -------------------------------------------------- + + self.vars = None # set during build_query + self.lastid = None + self.files = Storage() + + # Components ---------------------------------------------------------- + + # Initialize component properties (will be set during _attach) + self.link = None + self.linktable = None + self.actuate = None + self.lkey = None + self.rkey = None + self.pkey = None + self.fkey = None + self.multiple = True + + self.parent = parent # the parent resource + self.linked = linked # the linked resource + + self.components = S3Components(self, components) + self.links = self.components.links + + if parent is None: + # Build query + self.build_query(id = id, + uid = uid, + filter = filter, + vars = vars, + extra_filters = extra_filters, + filter_component = filter_component, + ) + if context: + self.add_filter(s3db.context) + + # Component - attach link table + elif linktable is not None: + # This is a link-table component - attach the link table + link_alias = "%s__link" % self.alias + self.link = S3Resource(linktable, + alias = link_alias, + parent = self.parent, + linked = self, + include_deleted = self.include_deleted, + approved = self._approved, + unapproved = self._unapproved, + ) + + # Export meta data ---------------------------------------------------- + + self.muntil = None # latest mtime of the exported records + self.results = None # number of exported records + + # Errors -------------------------------------------------------------- + + self.error = None + + # ------------------------------------------------------------------------- + # Query handling + # ------------------------------------------------------------------------- + def build_query(self, + id = None, + uid = None, + filter = None, + vars = None, + extra_filters = None, + filter_component = None, + ): + """ + Query builder + + :param id: record ID or list of record IDs to include + :param uid: record UID or list of record UIDs to include + :param filter: filtering query (DAL only) + :param vars: dict of URL query variables + :param extra_filters: extra filters (to be applied on + pre-filtered subsets), as list of + tuples (method, expression) + :param filter_component: the alias of the component the URL + filters apply for (filters for this + component must be handled separately) + """ + + # Reset the rows counter + self._length = None + + self.rfilter = S3ResourceFilter(self, + id = id, + uid = uid, + filter = filter, + vars = vars, + extra_filters = extra_filters, + filter_component = filter_component, + ) + return self.rfilter + + # ------------------------------------------------------------------------- + def add_filter(self, f=None, c=None): + """ + Extend the current resource filter + + :param f: a Query or a S3ResourceQuery instance + :param c: alias of the component this filter concerns, + automatically adds the respective component join + (not needed for S3ResourceQuery instances) + """ + + if f is None: + return + + self.clear() + + if self.rfilter is None: + self.rfilter = S3ResourceFilter(self) + + self.rfilter.add_filter(f, component=c) + + # ------------------------------------------------------------------------- + def add_component_filter(self, alias, f=None): + """ + Extend the resource filter of a particular component, does + not affect the master resource filter (as opposed to add_filter) + + :param alias: the alias of the component + :param f: a Query or a S3ResourceQuery instance + """ + + if f is None: + return + + if self.rfilter is None: + self.rfilter = S3ResourceFilter(self) + + self.rfilter.add_filter(f, component=alias, master=False) + + # ------------------------------------------------------------------------- + def add_extra_filter(self, method, expression): + """ + And an extra filter (to be applied on pre-filtered subsets) + + :param method: a name of a known filter method, or a + callable filter method + :param expression: the filter expression (string) + """ + + self.clear() + + if self.rfilter is None: + self.rfilter = S3ResourceFilter(self) + + self.rfilter.add_extra_filter(method, expression) + + # ------------------------------------------------------------------------- + def set_extra_filters(self, filters): + """ + Replace the current extra filters + + :param filters: list of tuples (method, expression), or None + to remove all extra filters + """ + + self.clear() + + if self.rfilter is None: + self.rfilter = S3ResourceFilter(self) + + self.rfilter.set_extra_filters(filters) + + # ------------------------------------------------------------------------- + def get_query(self): + """ + Get the effective query + + :returns: Query + """ + + if self.rfilter is None: + self.build_query() + + return self.rfilter.get_query() + + # ------------------------------------------------------------------------- + def get_filter(self): + """ + Get the effective virtual filter + + :returns: S3ResourceQuery + """ + + if self.rfilter is None: + self.build_query() + + return self.rfilter.get_filter() + + # ------------------------------------------------------------------------- + def clear_query(self): + """ + Remove the current query (does not remove the set!) + """ + + self.rfilter = None + + for component in self.components.loaded.values(): + component.clear_query() + + # ------------------------------------------------------------------------- + # Data access (new API) + # ------------------------------------------------------------------------- + def count(self, left=None, distinct=False): + """ + Get the total number of available records in this resource + + :param left: left outer joins, if required + :param distinct: only count distinct rows + """ + + if self.rfilter is None: + self.build_query() + if self._length is None: + self._length = self.rfilter.count(left = left, + distinct = distinct) + return self._length + + # ------------------------------------------------------------------------- + def select(self, + fields, + start = 0, + limit = None, + left = None, + orderby = None, + groupby = None, + distinct = False, + virtual = True, + count = False, + getids = False, + as_rows = False, + represent = False, + show_links = True, + raw_data = False, + ): + """ + Extract data from this resource + + :param fields: the fields to extract (selector strings) + :param start: index of the first record + :param limit: maximum number of records + :param left: additional left joins required for filters + :param orderby: orderby-expression for DAL + :param groupby: fields to group by (overrides fields!) + :param distinct: select distinct rows + :param virtual: include mandatory virtual fields + :param count: include the total number of matching records + :param getids: include the IDs of all matching records + :param as_rows: return the rows (don't extract) + :param represent: render field value representations + :param raw_data: include raw data in the result + """ + + data = S3ResourceData(self, + fields, + start = start, + limit = limit, + left = left, + orderby = orderby, + groupby = groupby, + distinct = distinct, + virtual = virtual, + count = count, + getids = getids, + as_rows = as_rows, + represent = represent, + show_links = show_links, + raw_data = raw_data, + ) + if as_rows: + return data.rows + else: + return data + + # ------------------------------------------------------------------------- + def insert(self, **fields): + """ + Insert a record into this resource + + :param fields: dict of field/value pairs to insert + """ + + table = self.table + tablename = self.tablename + + # Check permission + authorised = current.auth.s3_has_permission("create", tablename) + if not authorised: + from ..errors import S3PermissionError + raise S3PermissionError("Operation not permitted: INSERT INTO %s" % + tablename) + + # Insert new record + record_id = self.table.insert(**fields) + + # Post-process create + if record_id: + + # Audit + current.audit("create", self.prefix, self.name, record=record_id) + + record = Storage(fields) + record.id = record_id + + # Update super + s3db = current.s3db + s3db.update_super(table, record) + + # Record owner + auth = current.auth + auth.s3_set_record_owner(table, record_id) + auth.s3_make_session_owner(table, record_id) + + # Execute onaccept + s3db.onaccept(tablename, record, method="create") + + return record_id + + # ------------------------------------------------------------------------- + def update(self): + """ + TODO Bulk updater + """ + + raise NotImplementedError + + # ------------------------------------------------------------------------- + def delete(self, + format = None, + cascade = False, + replaced_by = None, + log_errors = False, + ): + """ + Delete all records in this resource + + :param format: the representation format of the request (optional) + :param cascade: this is a cascade delete (prevents commits) + :param replaced_by: used by record merger + :param log_errors: log errors even when cascade=True + + :returns: number of records deleted + + NB skipping undeletable rows is no longer the default behavior, + process will now fail immediately for any error; use S3Delete + directly if skipping of undeletable rows is desired + """ + + from .delete import S3Delete + + delete = S3Delete(self, representation=format) + result = delete(cascade = cascade, + replaced_by = replaced_by, + #skip_undeletable = False, + ) + + if log_errors and cascade: + # Call log_errors explicitly if suppressed by cascade + delete.log_errors() + + return result + + # ------------------------------------------------------------------------- + def approve(self, components=(), approve=True, approved_by=None): + """ + Approve all records in this resource + + :param components: list of component aliases to include, None + for no components, empty list or tuple to + approve all components (default) + :param approve: set to approved (False to reset to unapproved) + :param approved_by: set approver explicitly, a valid auth_user.id + or 0 for approval by system authority + """ + + if "approved_by" not in self.fields: + # No approved_by field => treat as approved by default + return True + + auth = current.auth + if approve: + if approved_by is None: + user = auth.user + if user: + user_id = user.id + else: + return False + else: + user_id = approved_by + else: + # Reset to unapproved + user_id = None + + db = current.db + table = self._table + + # Get all record_ids in the resource + pkey = self._id.name + rows = self.select([pkey], limit=None, as_rows=True) + if not rows: + # No records to approve => exit early + return True + + # Collect record_ids and clear cached permissions + record_ids = set() + add = record_ids.add + forget_permissions = auth.permission.forget + for record in rows: + record_id = record[pkey] + forget_permissions(table, record_id) + add(record_id) + + # Set approved_by for each record in the set + dbset = db(table._id.belongs(record_ids)) + try: + success = dbset.update(approved_by = user_id) + except: + # DB error => raise in debug mode to produce a proper ticket + if current.response.s3.debug: + raise + success = False + if not success: + db.rollback() + return False + + # Invoke onapprove-callback for each updated record + onapprove = self.get_config("onapprove", None) + if onapprove: + rows = dbset.select(limitby=(0, len(record_ids))) + for row in rows: + callback(onapprove, row, tablename=self.tablename) + + # Return early if no components to approve + if components is None: + return True + + # Determine which components to approve + # NB: Components are pre-filtered with the master filter, too + if components: + # FIXME this is probably wrong => should load + # the components which are to be approved + cdict = self.components.exposed + components = [cdict[k] for k in cdict if k in components] + else: + # Approve all currently attached components + # FIXME use exposed.values() + components = self.components.values() + + for component in components: + success = component.approve(components = None, + approve = approve, + approved_by = approved_by, + ) + if not success: + return False + + return True + + # ------------------------------------------------------------------------- + def reject(self, cascade=False): + """ Reject (delete) all records in this resource """ + + db = current.db + s3db = current.s3db + + define_resource = s3db.resource + DELETED = current.xml.DELETED + + INTEGRITY_ERROR = current.ERROR.INTEGRITY_ERROR + tablename = self.tablename + table = self.table + pkey = table._id.name + + # Get hooks configuration + get_config = s3db.get_config + ondelete = get_config(tablename, "ondelete") + onreject = get_config(tablename, "onreject") + ondelete_cascade = get_config(tablename, "ondelete_cascade") + + # Get all rows + if "uuid" in table.fields: + rows = self.select([table._id.name, "uuid"], as_rows=True) + else: + rows = self.select([table._id.name], as_rows=True) + if not rows: + return True + + delete_super = s3db.delete_super + + if DELETED in table: + + references = table._referenced_by + + for row in rows: + + error = self.error + self.error = None + + # On-delete-cascade + if ondelete_cascade: + callback(ondelete_cascade, row, tablename=tablename) + + # Automatic cascade + for ref in references: + tn, fn = ref.tablename, ref.name + rtable = db[tn] + rfield = rtable[fn] + query = (rfield == row[pkey]) + # Ignore RESTRICTs => reject anyway + if rfield.ondelete in ("CASCADE", "RESTRICT"): + rresource = define_resource(tn, filter=query, unapproved=True) + rresource.reject(cascade=True) + if rresource.error: + break + elif rfield.ondelete == "SET NULL": + try: + db(query).update(**{fn:None}) + except: + self.error = INTEGRITY_ERROR + break + elif rfield.ondelete == "SET DEFAULT": + try: + db(query).update(**{fn:rfield.default}) + except: + self.error = INTEGRITY_ERROR + break + + if not self.error and not delete_super(table, row): + self.error = INTEGRITY_ERROR + + if self.error: + db.rollback() + raise RuntimeError("Reject failed for %s.%s" % + (tablename, row[table._id])) + else: + # Pull back prior error status + self.error = error + error = None + + # On-reject hook + if onreject: + callback(onreject, row, tablename=tablename) + + # Park foreign keys + fields = {"deleted": True} + if "deleted_fk" in table: + record = table[row[pkey]] + fk = {} + for f in table.fields: + if record[f] is not None and \ + s3_has_foreign_key(table[f]): + fk[f] = record[f] + fields[f] = None + else: + continue + if fk: + fields.update(deleted_fk=json.dumps(fk)) + + # Update the row, finally + db(table._id == row[pkey]).update(**fields) + + # Clear session + if s3_get_last_record_id(tablename) == row[pkey]: + s3_remove_last_record_id(tablename) + + # On-delete hook + if ondelete: + callback(ondelete, row, tablename=tablename) + + else: + # Hard delete + for row in rows: + + # On-delete-cascade + if ondelete_cascade: + callback(ondelete_cascade, row, tablename=tablename) + + # On-reject + if onreject: + callback(onreject, row, tablename=tablename) + + try: + del table[row[pkey]] + except: + # Row is not deletable + self.error = INTEGRITY_ERROR + db.rollback() + raise + else: + # Clear session + if s3_get_last_record_id(tablename) == row[pkey]: + s3_remove_last_record_id(tablename) + + # Delete super-entity + delete_super(table, row) + + # On-delete + if ondelete: + callback(ondelete, row, tablename=tablename) + + return True + + # ------------------------------------------------------------------------- + def merge(self, + original_id, + duplicate_id, + replace = None, + update = None, + main = True): + """ Merge two records, see also S3RecordMerger.merge """ + + from ..methods import S3RecordMerger + return S3RecordMerger(self).merge(original_id, + duplicate_id, + replace = replace, + update = update, + main = main) + + # ------------------------------------------------------------------------- + # Exports + # ------------------------------------------------------------------------- + def datatable(self, + fields = None, + start = 0, + limit = None, + left = None, + orderby = None, + distinct = False, + ): + """ + Generate a data table of this resource + + :param fields: list of fields to include (field selector strings) + :param start: index of the first record to include + :param limit: maximum number of records to include + :param left: additional left joins for DB query + :param orderby: orderby for DB query + :param distinct: distinct-flag for DB query + + :returns: tuple (S3DataTable, numrows), where numrows represents + the total number of rows in the table that match the query + """ + + # Choose fields + if fields is None: + fields = [f.name for f in self.readable_fields()] + selectors = list(fields) + + table = self.table + + # Automatically include the record ID + table_id = table._id + pkey = table_id.name + if pkey not in selectors: + fields.insert(0, pkey) + selectors.insert(0, pkey) + + # Skip representation of IDs in data tables + id_repr = table_id.represent + table_id.represent = None + + # Extract the data + data = self.select(selectors, + start = start, + limit = limit, + orderby = orderby, + left = left, + distinct = distinct, + count = True, + getids = False, + represent = True, + ) + + rows = data.rows + + # Restore ID representation + table_id.represent = id_repr + + # Empty table - or just no match? + empty = False + if not rows: + DELETED = current.xml.DELETED + if DELETED in table: + query = (table[DELETED] == False) + else: + query = (table_id > 0) + row = current.db(query).select(table_id, limitby=(0, 1)).first() + if not row: + empty = True + + # Generate the data table + rfields = data.rfields + dt = S3DataTable(rfields, rows, orderby=orderby, empty=empty) + + return dt, data.numrows + + # ------------------------------------------------------------------------- + def datalist(self, + fields = None, + start = 0, + limit = None, + left = None, + orderby = None, + distinct = False, + list_id = None, + layout = None): + """ + Generate a data list of this resource + + :param fields: list of fields to include (field selector strings) + :param start: index of the first record to include + :param limit: maximum number of records to include + :param left: additional left joins for DB query + :param orderby: orderby for DB query + :param distinct: distinct-flag for DB query + :param list_id: the list identifier + :param layout: custom renderer function (see S3DataList.render) + + :returns: tuple (S3DataList, numrows, ids), where numrows represents + the total number of rows in the table that match the query + """ + + # Choose fields + if fields is None: + fields = [f.name for f in self.readable_fields()] + selectors = list(fields) + + table = self.table + + # Automatically include the record ID + pkey = table._id.name + if pkey not in selectors: + fields.insert(0, pkey) + selectors.insert(0, pkey) + + # Extract the data + data = self.select(selectors, + start = start, + limit = limit, + orderby = orderby, + left = left, + distinct = distinct, + count = True, + getids = False, + raw_data = True, + represent = True, + ) + + # Generate the data list + numrows = data.numrows + dl = S3DataList(self, + fields, + data.rows, + list_id = list_id, + start = start, + limit = limit, + total = numrows, + layout = layout, + ) + + return dl, numrows + + # ------------------------------------------------------------------------- + def json(self, + fields=None, + start=0, + limit=None, + left=None, + distinct=False, + orderby=None): + """ + Export a JSON representation of the resource. + + :param fields: list of field selector strings + :param start: index of the first record + :param limit: maximum number of records + :param left: list of (additional) left joins + :param distinct: select only distinct rows + :param orderby: Orderby-expression for the query + + :returns: the JSON (as string), representing a list of + dicts with {"tablename.fieldname":"value"} + """ + + data = self.select(fields=fields, + start=start, + limit=limit, + orderby=orderby, + left=left, + distinct=distinct)["rows"] + + return json.dumps(data) + + # ------------------------------------------------------------------------- + # Data Object API + # ------------------------------------------------------------------------- + def load(self, + fields = None, + skip = None, + start = None, + limit = None, + orderby = None, + virtual = True, + cacheable = False): + """ + Loads records from the resource, applying the current filters, + and stores them in the instance. + + :param fields: list of field names to include + :param skip: list of field names to skip + :param start: the index of the first record to load + :param limit: the maximum number of records to load + :param orderby: orderby-expression for the query + :param virtual: whether to load virtual fields or not + :param cacheable: don't define Row actions like update_record + or delete_record (faster, and the record can + be cached) + + :returns: the records as list of Rows + """ + + + table = self.table + tablename = self.tablename + + UID = current.xml.UID + load_uids = hasattr(table, UID) + + if not skip: + skip = () + + if fields or skip: + s3 = current.response.s3 + if "all_meta_fields" in s3: + meta_fields = s3.all_meta_fields + else: + meta_fields = s3.all_meta_fields = s3_all_meta_field_names() + s3db = current.s3db + superkeys = s3db.get_super_keys(table) + else: + meta_fields = superkeys = None + + # Field selection + qfields = ([table._id.name, UID]) + append = qfields.append + for f in table.fields: + + if f in ("wkt", "the_geom"): + if tablename == "gis_location": + if f == "the_geom": + # Filter out bulky Polygons + continue + else: + fmt = current.auth.permission.format + if fmt == "cap": + # Include WKT + pass + elif fmt == "xml" and current.deployment_settings.get_gis_xml_wkt(): + # Include WKT + pass + else: + # Filter out bulky Polygons + continue + elif tablename.startswith("gis_layer_shapefile_"): + # Filter out bulky Polygons + continue + + if fields or skip: + + # Must include all meta-fields + if f in meta_fields: + append(f) + continue + + # Must include the fkey if component + if self.parent and not self.link and f == self.fkey: + append(f) + continue + + # Must include all super-keys + if f in superkeys: + append(f) + continue + + if f in skip: + continue + if not fields or f in fields: + qfields.append(f) + + fields = list(set(fn for fn in qfields if hasattr(table, fn))) + + if self._rows is not None: + self.clear() + + pagination = limit is not None or start + + rfilter = self.rfilter + multiple = rfilter.multiple if rfilter is not None else True + if not multiple and self.parent and self.parent.count() == 1: + start = 0 + limit = 1 + + rows = self.select(fields, + start=start, + limit=limit, + orderby=orderby, + virtual=virtual, + as_rows=True) + + ids = self._ids = [] + new_id = ids.append + + self._uids = [] + self._rows = [] + + if rows: + new_uid = self._uids.append + new_row = self._rows.append + pkey = table._id.name + for row in rows: + if hasattr(row, tablename): + _row = ogetattr(row, tablename) + if type(_row) is Row: + row = _row + record_id = ogetattr(row, pkey) + if record_id not in ids: + new_id(record_id) + new_row(row) + if load_uids: + new_uid(ogetattr(row, UID)) + + # If this is an unlimited load, or the first page with no + # rows, then the result length is equal to the total number + # of matching records => store length for subsequent count()s + length = len(self._rows) + if not pagination or not start and not length: + self._length = length + + return self._rows + + # ------------------------------------------------------------------------- + def clear(self): + """ Removes the records currently stored in this instance """ + + self._rows = None + self._rowindex = None + self._length = None + self._ids = None + self._uids = None + self.files = Storage() + + for component in self.components.loaded.values(): + component.clear() + + # ------------------------------------------------------------------------- + def records(self, fields=None): + """ + Get the current set as Rows instance + + :param fields: the fields to include (list of Fields) + """ + + if fields is None: + if self.tablename == "gis_location": + fields = [f for f in self.table + if f.name not in ("wkt", "the_geom")] + else: + fields = [f for f in self.table] + + if self._rows is None: + return Rows(current.db) + else: + colnames = [str(f) for f in fields] + return Rows(current.db, self._rows, colnames=colnames) + + # ------------------------------------------------------------------------- + def __getitem__(self, key): + """ + Find a record currently stored in this instance by its record ID + + :param key: the record ID + :returns: a Row + + :raises IndexError: if the record is not currently loaded + """ + + index = self._rowindex + if index is None: + _id = self._id.name + rows = self._rows + if rows: + index = Storage([(str(row[_id]), row) for row in rows]) + else: + index = Storage() + self._rowindex = index + key = str(key) + if key in index: + return index[key] + raise IndexError + + # ------------------------------------------------------------------------- + def __iter__(self): + """ + Iterate over the records currently stored in this instance + """ + + if self._rows is None: + self.load() + rows = self._rows + for i in range(len(rows)): + yield rows[i] + return + + # ------------------------------------------------------------------------- + def get(self, key, component=None, link=None): + """ + Get component records for a record currently stored in this + instance. + + :param key: the record ID + :param component: the name of the component + :param link: the name of the link table + + :returns: a Row (if component is None) or a list of rows + """ + + if not key: + raise KeyError("Record not found") + if self._rows is None: + self.load() + try: + master = self[key] + except IndexError: + raise KeyError("Record not found") + + if not component and not link: + return master + elif link: + if link in self.links: + c = self.links[link] + else: + calias = current.s3db.get_alias(self.tablename, link) + if calias: + c = self.components[calias].link + else: + raise AttributeError("Undefined link %s" % link) + else: + try: + c = self.components[component] + except KeyError: + raise AttributeError("Undefined component %s" % component) + + rows = c._rows + if rows is None: + rows = c.load() + if not rows: + return [] + pkey, fkey = c.pkey, c.fkey + if pkey in master: + master_id = master[pkey] + if c.link: + lkey, rkey = c.lkey, c.rkey + lids = [r[rkey] for r in c.link if master_id == r[lkey]] + rows = [record for record in rows if record[fkey] in lids] + else: + try: + rows = [record for record in rows if master_id == record[fkey]] + except AttributeError: + # Most likely need to tweak static/formats/geoson/export.xsl + raise AttributeError("Component %s records are missing fkey %s" % (component, fkey)) + else: + rows = [] + return rows + + # ------------------------------------------------------------------------- + def get_id(self): + """ Get the IDs of all records currently stored in this instance """ + + if self._ids is None: + self.__load_ids() + + if not self._ids: + return None + elif len(self._ids) == 1: + return self._ids[0] + else: + return self._ids + + # ------------------------------------------------------------------------- + def get_uid(self): + """ Get the UUIDs of all records currently stored in this instance """ + + if current.xml.UID not in self.table.fields: + return None + if self._ids is None: + self.__load_ids() + + if not self._uids: + return None + elif len(self._uids) == 1: + return self._uids[0] + else: + return self._uids + + # ------------------------------------------------------------------------- + def __len__(self): + """ + The number of currently loaded rows + """ + + if self._rows is not None: + return len(self._rows) + else: + return 0 + + # ------------------------------------------------------------------------- + def __load_ids(self): + """ Loads the IDs/UIDs of all records matching the current filter """ + + table = self.table + UID = current.xml.UID + + pkey = table._id.name + + if UID in table.fields: + has_uid = True + fields = (pkey, UID) + else: + has_uid = False + fields = (pkey, ) + + rfilter = self.rfilter + multiple = rfilter.multiple if rfilter is not None else True + if not multiple and self.parent and self.parent.count() == 1: + start = 0 + limit = 1 + else: + start = limit = None + + rows = self.select(fields, + start=start, + limit=limit)["rows"] + + if rows: + ID = str(table._id) + self._ids = [row[ID] for row in rows] + if has_uid: + uid = str(table[UID]) + self._uids = [row[uid] for row in rows] + else: + self._ids = [] + + return + + # ------------------------------------------------------------------------- + # Representation + # ------------------------------------------------------------------------- + def __repr__(self): + """ + String representation of this resource + """ + + pkey = self.table._id.name + + if self._rows: + ids = [r[pkey] for r in self] + return "" % (self.tablename, ids) + else: + return "" % self.tablename + + # ------------------------------------------------------------------------- + def __contains__(self, item): + """ + Tests whether this resource contains a (real) field. + + :param item: the field selector or Field instance + """ + + fn = str(item) + if "." in fn: + tn, fn = fn.split(".", 1) + if tn == self.tablename: + item = fn + try: + rf = self.resolve_selector(str(item)) + except (SyntaxError, AttributeError): + return 0 + if rf.field is not None: + return 1 + else: + return 0 + + # ------------------------------------------------------------------------- + def __bool__(self): + """ Boolean test of this resource """ + + return self is not None + + def __nonzero__(self): + """ Python-2.7 backwards-compatibility """ + + return self is not None + + # ------------------------------------------------------------------------- + # XML Export + # ------------------------------------------------------------------------- + def export_xml(self, + start=None, + limit=None, + msince=None, + fields=None, + dereference=True, + maxdepth=MAXDEPTH, + mcomponents=DEFAULT, + rcomponents=None, + references=None, + mdata=False, + stylesheet=None, + as_tree=False, + as_json=False, + maxbounds=False, + filters=None, + pretty_print=False, + location_data=None, + map_data=None, + target=None, + **args): + """ + Export this resource as S3XML + + :param start: index of the first record to export (slicing) + :param limit: maximum number of records to export (slicing) + + :param msince: export only records which have been modified + after this datetime + + :param fields: data fields to include (default: all) + + :param dereference: include referenced resources + :param maxdepth: maximum depth for reference exports + + :param mcomponents: components of the master resource to + include (list of aliases), empty list + for all available components + :param rcomponents: components of referenced resources to + include (list of "tablename:alias") + + :param references: foreign keys to include (default: all) + :param mdata: mobile data export + (=>reduced field set, lookup-only option) + :param stylesheet: path to the XSLT stylesheet (if required) + :param as_tree: return the ElementTree (do not convert into string) + :param as_json: represent the XML tree as JSON + :param maxbounds: include lat/lon boundaries in the top + level element (off by default) + :param filters: additional URL filters (Sync), as dict + {tablename: {url_var: string}} + :param pretty_print: insert newlines/indentation in the output + :param location_data: dictionary of location data which has been + looked-up in bulk ready for xml.gis_encode() + :param map_data: dictionary of options which can be read by the map + :param target: alias of component targetted (or None to target master resource) + :param args: dict of arguments to pass to the XSLT stylesheet + """ + + + xml = current.xml + + output = None + args = Storage(args) + + from .xml import S3XMLFormat + xmlformat = S3XMLFormat(stylesheet) if stylesheet else None + + if mcomponents is DEFAULT: + mcomponents = [] + + # Export as element tree + from .rtb import S3ResourceTree + rtree = S3ResourceTree(self, + location_data = location_data, + map_data = map_data, + ) + + tree = rtree.build(start = start, + limit = limit, + msince = msince, + fields = fields, + dereference = dereference, + maxdepth = maxdepth, + mcomponents = mcomponents, + rcomponents = rcomponents, + references = references, + sync_filters = filters, + mdata = mdata, + maxbounds = maxbounds, + xmlformat = xmlformat, + target = target, + ) + + # XSLT transformation + if tree and xmlformat is not None: + import uuid + args.update(domain = xml.domain, + base_url = current.response.s3.base_url, + prefix = self.prefix, + name = self.name, + utcnow = s3_format_datetime(), + msguid = uuid.uuid4().urn, + ) + tree = xmlformat.transform(tree, **args) + + # Convert into the requested format + # NB Content-Type headers are to be set by caller + if tree: + if as_tree: + output = tree + elif as_json: + output = xml.tree2json(tree, pretty_print=pretty_print) + else: + output = xml.tostring(tree, pretty_print=pretty_print) + + return output + + # ------------------------------------------------------------------------- + # XML Import + # ------------------------------------------------------------------------- + def import_xml(self, + source, + source_type = "xml", + stylesheet = None, + extra_data = None, + files = None, + record_id = None, + commit = True, + ignore_errors = False, + job_id = None, + select_items = None, + strategy = None, + sync_policy = None, + **args): + """ + Import data + + :param source: the data source + :param str source_type: the source type (xml|json|csv|xls|xlsx) + :param stylesheet: transformation stylesheet + :param extra_data: extra columns to add to spreadsheet rows + :param files: attached files + :param record_id: target record ID + :param commit: commit the import, if False, the import will be + rolled back and the job stored for later commit + :param ignore_errors: ignore any errors, import whatever is valid + :param job_id: a previous import job to restore and commit + :param select_items: items of the previous import job to select + :param strategy: allowed import methods + :param SyncPolicy sync_policy: the synchronization policy + :param args: arguments for the transformation stylesheet + """ + + # Check permission + has_permission = current.auth.s3_has_permission + authorised = has_permission("create", self.table) and \ + has_permission("update", self.table) + if not authorised: + raise IOError("Insufficient permissions") + + self.job_id = None + tablename = self.tablename + + from .importer import XMLImporter + tree = None + if source: + tree = XMLImporter.parse_source(tablename, + source, + source_type = source_type, + stylesheet = stylesheet, + extra_data = extra_data, + **args) + elif not commit: + raise ValueError("Source required for trial import") + elif not job_id: + raise ValueError("Source or Job ID required") + + return XMLImporter.import_tree(tablename, + tree, + files = files, + record_id = record_id, + components = self.components.exposed_aliases, + commit = commit, + ignore_errors = ignore_errors, + job_id = job_id if tree is None else None, + select_items = select_items, + strategy = strategy, + sync_policy = sync_policy, + ) + + # ------------------------------------------------------------------------- + # XML introspection + # ------------------------------------------------------------------------- + def export_options(self, + component = None, + fields = None, + only_last = False, + show_uids = False, + hierarchy = False, + as_json = False): + """ + Export field options of this resource as element tree + + :param component: name of the component which the options are + requested of, None for the primary table + :param fields: list of names of fields for which the options + are requested, None for all fields (which have + options) + :param as_json: convert the output into JSON + :param only_last: obtain only the latest record + """ + + if component is not None: + c = self.components.get(component) + if c: + tree = c.export_options(fields = fields, + only_last = only_last, + show_uids = show_uids, + hierarchy = hierarchy, + as_json = as_json) + return tree + else: + # If we get here, we've been called from the back-end, + # otherwise the request would have failed during parse. + # So it's safe to raise an exception: + raise AttributeError + else: + if as_json and only_last and len(fields) == 1: + # Identify the field + default = {"option":[]} + try: + field = self.table[fields[0]] + except AttributeError: + # Can't raise an exception here as this goes + # directly to the client + return json.dumps(default) + + # Check that the validator has a lookup table + requires = field.requires + if not isinstance(requires, (list, tuple)): + requires = [requires] + requires = requires[0] + if isinstance(requires, IS_EMPTY_OR): + requires = requires.other + from ..tools import IS_LOCATION + if not isinstance(requires, (IS_ONE_OF, IS_LOCATION)): + # Can't raise an exception here as this goes + # directly to the client + return json.dumps(default) + + # Identify the lookup table + db = current.db + lookuptable = requires.ktable + lookupfield = db[lookuptable][requires.kfield] + + # Fields to extract + fields = [lookupfield] + h = None + if hierarchy: + from ..tools import S3Hierarchy + h = S3Hierarchy(lookuptable) + if not h.config: + h = None + elif h.pkey.name != lookupfield.name: + # Also extract the node key for the hierarchy + fields.append(h.pkey) + + # Get the latest record + # NB: this assumes that the lookupfield is auto-incremented + row = db().select(orderby = ~lookupfield, + limitby = (0, 1), + *fields).first() + + # Represent the value and generate the output JSON + if row: + value = row[lookupfield] + widget = field.widget + if hasattr(widget, "represent") and widget.represent: + # Prefer the widget's represent as options.json + # is usually called to Ajax-update the widget + represent = widget.represent(value) + elif field.represent: + represent = field.represent(value) + else: + represent = s3_str(value) + if isinstance(represent, A): + represent = represent.components[0] + + item = {"@value": value, "$": represent} + if h: + parent = h.parent(row[h.pkey]) + if parent: + item["@parent"] = str(parent) + result = [item] + else: + result = [] + return json.dumps({'option': result}) + + xml = current.xml + tree = xml.get_options(self.table, + fields = fields, + show_uids = show_uids, + hierarchy = hierarchy) + + if as_json: + return xml.tree2json(tree, pretty_print=False, + native=True) + else: + return xml.tostring(tree, pretty_print=False) + + # ------------------------------------------------------------------------- + def export_fields(self, component=None, as_json=False): + """ + Export a list of fields in the resource as element tree + + :param component: name of the component to lookup the fields + (None for primary table) + :param as_json: convert the output XML into JSON + """ + + if component is not None: + try: + c = self.components[component] + except KeyError: + raise AttributeError("Undefined component %s" % component) + return c.export_fields(as_json=as_json) + else: + xml = current.xml + tree = xml.get_fields(self.prefix, self.name) + if as_json: + return xml.tree2json(tree, pretty_print=True) + else: + return xml.tostring(tree, pretty_print=True) + + # ------------------------------------------------------------------------- + def export_struct(self, + meta = False, + options = False, + references = False, + stylesheet = None, + as_json = False, + as_tree = False): + """ + Get the structure of the resource + + :param options: include option lists in option fields + :param references: include option lists even for reference fields + :param stylesheet: the stylesheet to use for transformation + :param as_json: convert into JSON after transformation + """ + + xml = current.xml + + # Get the structure of the main resource + root = etree.Element(xml.TAG.root) + main = xml.get_struct(self.prefix, self.name, + alias = self.alias, + parent = root, + meta = meta, + options = options, + references = references, + ) + + # Include the exposed components + for component in self.components.exposed.values(): + prefix = component.prefix + name = component.name + xml.get_struct(prefix, name, + alias = component.alias, + parent = main, + meta = meta, + options = options, + references = references, + ) + + # Transformation + tree = etree.ElementTree(root) + if stylesheet is not None: + args = {"domain": xml.domain, + "base_url": current.response.s3.base_url, + "prefix": self.prefix, + "name": self.name, + "utcnow": s3_format_datetime(), + } + + tree = xml.transform(tree, stylesheet, **args) + if tree is None: + return None + + # Return tree if requested + if as_tree: + return tree + + # Otherwise string-ify it + if as_json: + return xml.tree2json(tree, pretty_print=True) + else: + return xml.tostring(tree, pretty_print=True) + + # ------------------------------------------------------------------------- + # Data Model Helpers + # ------------------------------------------------------------------------- + @classmethod + def original(cls, table, record, mandatory=None): + """ + Find the original record for a possible duplicate: + - if the record contains a UUID, then only that UUID is used + to match the record with an existing DB record + - otherwise, if the record contains some values for unique + fields, all of them must match the same existing DB record + + :param table: the table + :param record: the record as dict or S3XML Element + """ + + db = current.db + xml = current.xml + xml_decode = xml.xml_decode + + VALUE = xml.ATTRIBUTE["value"] + UID = xml.UID + ATTRIBUTES_TO_FIELDS = xml.ATTRIBUTES_TO_FIELDS + + # Get primary keys + pkeys = [f for f in table.fields if table[f].unique] + pvalues = Storage() + + # Get the values from record + get = record.get + if type(record) is etree._Element: #isinstance(record, etree._Element): + xpath = record.xpath + xexpr = "%s[@%s='%%s']" % (xml.TAG["data"], + xml.ATTRIBUTE["field"]) + for f in pkeys: + v = None + if f == UID or f in ATTRIBUTES_TO_FIELDS: + v = get(f, None) + else: + child = xpath(xexpr % f) + if child: + child = child[0] + v = child.get(VALUE, xml_decode(child.text)) + if v: + pvalues[f] = v + elif isinstance(record, dict): + for f in pkeys: + v = get(f, None) + if v: + pvalues[f] = v + else: + raise TypeError + + # Build match query + query = None + for f in pvalues: + if f == UID: + continue + _query = (table[f] == pvalues[f]) + if query is not None: + query = query | _query + else: + query = _query + + fields = cls.import_fields(table, pvalues, mandatory=mandatory) + + # Try to find exactly one match by non-UID unique keys + if query is not None: + original = db(query).select(limitby=(0, 2), *fields) + if len(original) == 1: + return original.first() + + # If no match, then try to find a UID-match + if UID in pvalues: + uid = xml.import_uid(pvalues[UID]) + query = (table[UID] == uid) + original = db(query).select(limitby=(0, 1), *fields).first() + if original: + return original + + # No match or multiple matches + return None + + # ------------------------------------------------------------------------- + @staticmethod + def import_fields(table, data, mandatory=None): + + fnames = set(s3_all_meta_field_names()) + fnames.add(table._id.name) + if mandatory: + fnames |= set(mandatory) + for fn in data: + fnames.add(fn) + return [table[fn] for fn in fnames if fn in table.fields] + + # ------------------------------------------------------------------------- + def readable_fields(self, subset=None): + """ + Get a list of all readable fields in the resource table + + :param subset: list of fieldnames to limit the selection to + """ + + fkey = None + table = self.table + + parent = self.parent + linked = self.linked + + if parent and linked is None: + component = parent.components.get(self.alias) + if component: + fkey = component.fkey + elif linked is not None: + component = linked + if component: + fkey = component.lkey + + if subset: + return [ogetattr(table, f) for f in subset + if f in table.fields and \ + ogetattr(table, f).readable and f != fkey] + else: + return [ogetattr(table, f) for f in table.fields + if ogetattr(table, f).readable and f != fkey] + + # ------------------------------------------------------------------------- + def resolve_selectors(self, selectors, + skip_components=False, + extra_fields=True, + show=True): + """ + Resolve a list of field selectors against this resource + + :param selectors: the field selectors + :param skip_components: skip fields in components + :param extra_fields: automatically add extra_fields of all virtual + fields in this table + :param show: default for S3ResourceField.show + + :returns: tuple of (fields, joins, left, distinct) + """ + + prefix = lambda s: "~.%s" % s \ + if "." not in s.split("$", 1)[0] else s + + display_fields = set() + add = display_fields.add + + # Store field selectors + for item in selectors: + if not item: + continue + elif type(item) is tuple: + item = item[-1] + if isinstance(item, str): + selector = item + elif isinstance(item, S3ResourceField): + selector = item.selector + elif isinstance(item, FS): + selector = item.name + else: + continue + add(prefix(selector)) + + slist = list(selectors) + + # Collect extra fields from virtual tables + if extra_fields: + extra = self.get_config("extra_fields") + if extra: + append = slist.append + for selector in extra: + s = prefix(selector) + if s not in display_fields: + append(s) + + joins = {} + left = {} + + distinct = False + + columns = set() + add_column = columns.add + + rfields = [] + append = rfields.append + + for s in slist: + + # Allow to override the field label + if type(s) is tuple: + label, selector = s + else: + label, selector = None, s + + # Resolve the selector + if isinstance(selector, str): + selector = prefix(selector) + try: + rfield = S3ResourceField(self, selector, label=label) + except (AttributeError, SyntaxError): + continue + elif isinstance(selector, FS): + try: + rfield = selector.resolve(self) + except (AttributeError, SyntaxError): + continue + elif isinstance(selector, S3ResourceField): + rfield = selector + else: + continue + + # Unresolvable selector? + if rfield.field is None and not rfield.virtual: + continue + + # De-duplicate columns + colname = rfield.colname + if colname in columns: + continue + else: + add_column(colname) + + # Replace default label + if label is not None: + rfield.label = label + + # Skip components + if skip_components: + head = rfield.selector.split("$", 1)[0] + if "." in head and head.split(".")[0] not in ("~", self.alias): + continue + + # Resolve the joins + if rfield.distinct: + left.update(rfield._joins) + distinct = True + elif rfield.join: + joins.update(rfield._joins) + + rfield.show = show and rfield.selector in display_fields + append(rfield) + + return (rfields, joins, left, distinct) + + # ------------------------------------------------------------------------- + def resolve_selector(self, selector): + """ + Wrapper for S3ResourceField, retained for backward compatibility + """ + + return S3ResourceField(self, selector) + + # ------------------------------------------------------------------------- + def split_fields(self, skip=DEFAULT, data=None, references=None): + """ + Split the readable fields in the resource table into + reference and non-reference fields. + + :param skip: list of field names to skip + :param data: data fields to include (None for all) + :param references: foreign key fields to include (None for all) + """ + + if skip is DEFAULT: + skip = [] + + rfields = self.rfields + dfields = self.dfields + + if rfields is None or dfields is None: + if self.tablename == "gis_location": + settings = current.deployment_settings + if "wkt" not in skip: + fmt = current.auth.permission.format + if fmt == "cap": + # Include WKT + pass + elif fmt == "xml" and settings.get_gis_xml_wkt(): + # Include WKT + pass + else: + # Skip bulky WKT fields + skip.append("wkt") + if "the_geom" not in skip and settings.get_gis_spatialdb(): + skip.append("the_geom") + + xml = current.xml + UID = xml.UID + IGNORE_FIELDS = xml.IGNORE_FIELDS + FIELDS_TO_ATTRIBUTES = xml.FIELDS_TO_ATTRIBUTES + + show_ids = current.xml.show_ids + rfields = [] + dfields = [] + table = self.table + pkey = table._id.name + for f in table.fields: + + if f == UID or f in skip or f in IGNORE_FIELDS: + # Skip (show_ids=True overrides this for pkey) + if f != pkey or not show_ids: + continue + + # Meta-field? => always include (in dfields) + meta = f in FIELDS_TO_ATTRIBUTES + + if s3_has_foreign_key(table[f]) and not meta: + # Foreign key => add to rfields unless excluded + if references is None or f in references: + rfields.append(f) + + elif data is None or f in data or meta: + # Data field => add to dfields + dfields.append(f) + + self.rfields = rfields + self.dfields = dfields + + return (rfields, dfields) + + # ------------------------------------------------------------------------- + # Utility functions + # ------------------------------------------------------------------------- + def configure(self, **settings): + """ + Update configuration settings for this resource + + :param settings: configuration settings for this resource + as keyword arguments + """ + + current.s3db.configure(self.tablename, **settings) + + # ------------------------------------------------------------------------- + def get_config(self, key, default=None): + """ + Get a configuration setting for the current resource + + :param key: the setting key + :param default: the default value to return if the setting + is not configured for this resource + """ + + return current.s3db.get_config(self.tablename, key, default=default) + + # ------------------------------------------------------------------------- + def clear_config(self, *keys): + """ + Clear configuration settings for this resource + + :param keys: keys to remove (can be multiple) + + .. note:: no keys specified removes all settings for this resource + """ + + current.s3db.clear_config(self.tablename, *keys) + + # ------------------------------------------------------------------------- + @staticmethod + def limitby(start=0, limit=0): + """ + Convert start+limit parameters into a limitby tuple + - limit without start => start = 0 + - start without limit => limit = ROWSPERPAGE + - limit 0 (or less) => limit = 1 + - start less than 0 => start = 0 + + :param start: index of the first record to select + :param limit: maximum number of records to select + """ + + if limit is None: + return None + + if start is None: + start = 0 + if limit == 0: + limit = current.response.s3.ROWSPERPAGE + + if limit <= 0: + limit = 1 + if start < 0: + start = 0 + + return (start, start + limit) + + # ------------------------------------------------------------------------- + def _join(self, implicit=False, reverse=False): + """ + Get a join for this component + + :param implicit: return a subquery with an implicit join rather + than an explicit join + :param reverse: get the reverse join (joining master to component) + + :returns: a Query if implicit=True, otherwise a list of joins + """ + + if self.parent is None: + # This isn't a component + return None + else: + ltable = self.parent.table + + rtable = self.table + pkey = self.pkey + fkey = self.fkey + + DELETED = current.xml.DELETED + + if self.linked: + return self.linked._join(implicit=implicit, reverse=reverse) + + elif self.linktable: + linktable = self.linktable + lkey = self.lkey + rkey = self.rkey + lquery = (ltable[pkey] == linktable[lkey]) + if DELETED in linktable: + lquery &= (linktable[DELETED] == False) + if self.filter is not None and not reverse: + rquery = (linktable[rkey] == rtable[fkey]) & self.filter + else: + rquery = (linktable[rkey] == rtable[fkey]) + if reverse: + join = [linktable.on(rquery), ltable.on(lquery)] + else: + join = [linktable.on(lquery), rtable.on(rquery)] + + else: + lquery = (ltable[pkey] == rtable[fkey]) + if DELETED in rtable and not reverse: + lquery &= (rtable[DELETED] == False) + if self.filter is not None: + lquery &= self.filter + if reverse: + join = [ltable.on(lquery)] + else: + join = [rtable.on(lquery)] + + if implicit: + query = None + for expression in join: + if query is None: + query = expression.second + else: + query &= expression.second + return query + else: + return join + + # ------------------------------------------------------------------------- + def get_join(self): + """ Get join for this component """ + + return self._join(implicit=True) + + # ------------------------------------------------------------------------- + def get_left_join(self): + """ Get a left join for this component """ + + return self._join() + + # ------------------------------------------------------------------------- + def link_id(self, master_id, component_id): + """ + Helper method to find the link table entry ID for + a pair of linked records. + + :param master_id: the ID of the master record + :param component_id: the ID of the component record + """ + + if self.parent is None or self.linked is None: + return None + + join = self.get_join() + ltable = self.table + mtable = self.parent.table + ctable = self.linked.table + query = join & \ + (mtable._id == master_id) & \ + (ctable._id == component_id) + row = current.db(query).select(ltable._id, limitby=(0, 1)).first() + if row: + return row[ltable._id.name] + else: + return None + + # ------------------------------------------------------------------------- + def component_id(self, master_id, link_id): + """ + Helper method to find the component record ID for + a particular link of a particular master record + + :param link: the link (S3Resource) + :param master_id: the ID of the master record + :param link_id: the ID of the link table entry + """ + + if self.parent is None or self.linked is None: + return None + + join = self.get_join() + ltable = self.table + mtable = self.parent.table + ctable = self.linked.table + query = join & (ltable._id == link_id) + if master_id is not None: + # master ID is redundant, but can be used to check negatives + query &= (mtable._id == master_id) + row = current.db(query).select(ctable._id, limitby=(0, 1)).first() + if row: + return row[ctable._id.name] + else: + return None + + # ------------------------------------------------------------------------- + def update_link(self, master, record): + """ + Create a new link in a link table if it doesn't yet exist. + This function is meant to also update links in "embed" + actuation mode once this gets implemented, therefore the + method name "update_link". + + :param master: the master record + :param record: the new component record to be linked + """ + + if self.parent is None or self.linked is None: + return None + + # Find the keys + resource = self.linked + pkey = resource.pkey + lkey = resource.lkey + rkey = resource.rkey + fkey = resource.fkey + if pkey not in master: + return None + _lkey = master[pkey] + if fkey not in record: + return None + _rkey = record[fkey] + if not _lkey or not _rkey: + return None + + ltable = self.table + ltn = ltable._tablename + + # Create the link if it does not already exist + query = ((ltable[lkey] == _lkey) & + (ltable[rkey] == _rkey)) + row = current.db(query).select(ltable._id, limitby=(0, 1)).first() + if not row: + s3db = current.s3db + onaccept = s3db.get_config(ltn, "create_onaccept") + if onaccept is None: + onaccept = s3db.get_config(ltn, "onaccept") + data = {lkey:_lkey, rkey:_rkey} + link_id = ltable.insert(**data) + data[ltable._id.name] = link_id + s3db.update_super(ltable, data) + current.auth.s3_set_record_owner(ltable, data) + if link_id and onaccept: + callback(onaccept, Storage(vars=Storage(data))) + else: + link_id = row[ltable._id.name] + return link_id + + # ------------------------------------------------------------------------- + def datatable_filter(self, fields, get_vars): + """ + Parse datatable search/sort vars into a tuple of + query, orderby and left joins + + :param fields: list of field selectors representing + the order of fields in the datatable (list_fields) + :param get_vars: the datatable GET vars + + :returns: tuple of (query, orderby, left joins) + """ + + db = current.db + get_aliased = current.s3db.get_aliased + + left_joins = S3Joins(self.tablename) + + sSearch = "sSearch" + iColumns = "iColumns" + iSortingCols = "iSortingCols" + + parent = self.parent + fkey = self.fkey + + # Skip joins for linked tables + if self.linked is not None: + skip = self.linked.tablename + else: + skip = None + + # Resolve the list fields + rfields = self.resolve_selectors(fields)[0] + + # FILTER -------------------------------------------------------------- + + searchq = None + if sSearch in get_vars and iColumns in get_vars: + + # Build filter + text = get_vars[sSearch] + words = [w for w in text.lower().split()] + + if words: + try: + numcols = int(get_vars[iColumns]) + except ValueError: + numcols = 0 + + flist = [] + for i in range(numcols): + try: + rfield = rfields[i] + field = rfield.field + except (KeyError, IndexError): + continue + if field is None: + # Virtual + if hasattr(rfield, "search_field"): + field = db[rfield.tname][rfield.search_field] + else: + # Cannot search + continue + ftype = str(field.type) + + # Add left joins + left_joins.extend(rfield.left) + + if ftype[:9] == "reference" and \ + hasattr(field, "sortby") and field.sortby: + # For foreign keys, we search through their sortby + + # Get the lookup table + tn = ftype[10:] + if parent is not None and \ + parent.tablename == tn and field.name != fkey: + alias = "%s_%s_%s" % (parent.prefix, + "linked", + parent.name) + ktable = get_aliased(db[tn], alias) + ktable._id = ktable[ktable._id.name] + tn = alias + elif tn == field.tablename: + prefix, name = field.tablename.split("_", 1) + alias = "%s_%s_%s" % (prefix, field.name, name) + ktable = get_aliased(db[tn], alias) + ktable._id = ktable[ktable._id.name] + tn = alias + else: + ktable = db[tn] + + # Add left join for lookup table + if tn != skip: + left_joins.add(ktable.on(field == ktable._id)) + + if isinstance(field.sortby, (list, tuple)): + flist.extend([ktable[f] for f in field.sortby + if f in ktable.fields]) + else: + if field.sortby in ktable.fields: + flist.append(ktable[field.sortby]) + + else: + # Otherwise, we search through the field itself + flist.append(field) + + # Build search query + opts = Storage() + queries = [] + for w in words: + + wqueries = [] + for field in flist: + ftype = str(field.type) + options = None + fname = str(field) + if fname in opts: + options = opts[fname] + elif ftype[:7] in ("integer", + "list:in", + "list:st", + "referen", + "list:re", + "string"): + requires = field.requires + if not isinstance(requires, (list, tuple)): + requires = [requires] + if requires: + r = requires[0] + if isinstance(r, IS_EMPTY_OR): + r = r.other + if hasattr(r, "options"): + try: + options = r.options() + except: + options = [] + if options is None and ftype in ("string", "text"): + wqueries.append(field.lower().like("%%%s%%" % w)) + elif options is not None: + opts[fname] = options + vlist = [v for v, t in options + if s3_str(t).lower().find(s3_str(w)) != -1] + if vlist: + wqueries.append(field.belongs(vlist)) + if len(wqueries): + queries.append(reduce(lambda x, y: x | y \ + if x is not None else y, + wqueries)) + if len(queries): + searchq = reduce(lambda x, y: x & y \ + if x is not None else y, queries) + + # ORDERBY ------------------------------------------------------------- + + orderby = [] + if iSortingCols in get_vars: + + # Sorting direction + def direction(i): + sort_dir = get_vars["sSortDir_%s" % str(i)] + return " %s" % sort_dir if sort_dir else "" + + # Get the fields to order by + try: + numcols = int(get_vars[iSortingCols]) + except: + numcols = 0 + + columns = [] + pkey = str(self._id) + for i in range(numcols): + try: + iSortCol = int(get_vars["iSortCol_%s" % i]) + except (AttributeError, KeyError): + # iSortCol_x not present in get_vars => ignore + columns.append(Storage(field=None)) + continue + + # Map sortable-column index to the real list_fields + # index: for every non-id non-sortable column to the + # left of sortable column subtract 1 + for j in range(iSortCol): + if get_vars.get("bSortable_%s" % j, "true") == "false": + try: + if rfields[j].colname != pkey: + iSortCol -= 1 + except KeyError: + break + + try: + rfield = rfields[iSortCol] + except IndexError: + # iSortCol specifies a non-existent column, i.e. + # iSortCol_x>=numcols => ignore + columns.append(Storage(field=None)) + else: + columns.append(rfield) + + # Process the orderby-fields + for i in range(len(columns)): + rfield = columns[i] + field = rfield.field + if field is None: + continue + ftype = str(field.type) + + represent = field.represent + if ftype == "json": + # Can't sort by JSON fields + # => try using corresponding id column to maintain some + # fake yet consistent sort order: + tn = field.tablename + try: + orderby.append("%s%s" % (db[tn]._id, direction(i))) + except AttributeError: + continue + elif not hasattr(represent, "skip_dt_orderby") and \ + hasattr(represent, "dt_orderby"): + # Custom orderby logic in field.represent + field.represent.dt_orderby(field, + direction(i), + orderby, + left_joins) + + elif ftype[:9] == "reference" and \ + hasattr(field, "sortby") and field.sortby: + # Foreign keys with sortby will be sorted by sortby + + # Get the lookup table + tn = ftype[10:] + if parent is not None and \ + parent.tablename == tn and field.name != fkey: + alias = "%s_%s_%s" % (parent.prefix, "linked", parent.name) + ktable = get_aliased(db[tn], alias) + ktable._id = ktable[ktable._id.name] + tn = alias + elif tn == field.tablename: + prefix, name = field.tablename.split("_", 1) + alias = "%s_%s_%s" % (prefix, field.name, name) + ktable = get_aliased(db[tn], alias) + ktable._id = ktable[ktable._id.name] + tn = alias + else: + ktable = db[tn] + + # Add left joins for lookup table + if tn != skip: + left_joins.extend(rfield.left) + left_joins.add(ktable.on(field == ktable._id)) + + # Construct orderby from sortby + if not isinstance(field.sortby, (list, tuple)): + orderby.append("%s.%s%s" % (tn, field.sortby, direction(i))) + else: + orderby.append(", ".join(["%s.%s%s" % + (tn, fn, direction(i)) + for fn in field.sortby])) + + else: + # Otherwise, we sort by the field itself + orderby.append("%s%s" % (field, direction(i))) + + if orderby: + orderby = ", ".join(orderby) + else: + orderby = None + + left_joins = left_joins.as_list(tablenames=list(left_joins.joins.keys())) + return (searchq, orderby, left_joins) + + # ------------------------------------------------------------------------- + def prefix_selector(self, selector): + """ + Helper method to ensure consistent prefixing of field selectors + + :param selector: the selector + """ + + head = selector.split("$", 1)[0] + if "." in head: + prefix = head.split(".", 1)[0] + if prefix == self.alias: + return selector.replace("%s." % prefix, "~.") + else: + return selector + else: + return "~.%s" % selector + + # ------------------------------------------------------------------------- + def list_fields(self, key="list_fields", id_column=0): + """ + Get the list_fields for this resource + + :param key: alternative key for the table configuration + :param id_column: - False to exclude the record ID + - True to include it if it is configured + - 0 to make it the first column regardless + whether it is configured or not + """ + + list_fields = self.get_config(key, None) + + if not list_fields and key != "list_fields": + list_fields = self.get_config("list_fields", None) + if not list_fields: + list_fields = [f.name for f in self.readable_fields()] + + id_field = pkey = self._id.name + + # Do not include the parent key for components + if self.parent and not self.link and \ + not current.response.s3.component_show_key: + fkey = self.fkey + else: + fkey = None + + fields = [] + append = fields.append + selectors = set() + seen = selectors.add + for f in list_fields: + selector = f[1] if type(f) is tuple else f + if fkey and selector == fkey: + continue + if selector == pkey and not id_column: + id_field = f + elif selector not in selectors: + seen(selector) + append(f) + + if id_column == 0: + fields.insert(0, id_field) + + return fields + + # ------------------------------------------------------------------------- + def get_defaults(self, master, defaults=None, data=None): + """ + Get implicit defaults for new component records + + :param master: the master record + :param defaults: any explicit defaults + :param data: any actual values for the new record + + :returns: a dict of {fieldname: values} with the defaults + """ + + values = {} + + parent = self.parent + if not parent: + # Not a component + return values + + # Implicit defaults from component filters + hook = current.s3db.get_component(parent.tablename, self.alias) + filterby = hook.get("filterby") + if filterby: + for (k, v) in filterby.items(): + if not isinstance(v, (tuple, list)): + values[k] = v + + # Explicit defaults from component hook + if self.defaults: + values.update(self.defaults) + + # Explicit defaults from caller + if defaults: + values.update(defaults) + + # Actual record values + if data: + values.update(data) + + # Check for values to look up from master record + lookup = {} + for (k, v) in list(values.items()): + # Skip nonexistent fields + if k not in self.fields: + del values[k] + continue + # Resolve any field selectors + if isinstance(v, FS): + try: + rfield = v.resolve(parent) + except (AttributeError, SyntaxError): + continue + field = rfield.field + if not field or field.table != parent.table: + continue + if field.name in master: + values[k] = master[field.name] + else: + del values[k] + lookup[field.name] = k + + # Do we need to reload the master record to look up values? + if lookup: + row = None + parent_id = parent._id + record_id = master.get(parent_id.name) + if record_id: + fields = [parent.table[f] for f in lookup] + row = current.db(parent_id == record_id).select(limitby = (0, 1), + *fields).first() + if row: + for (k, v) in lookup.items(): + if k in row: + values[v] = row[k] + + return values + + # ------------------------------------------------------------------------- + @property + def _table(self): + """ + Get the original Table object (without SQL Alias), this + is required for SQL update (DAL doesn't detect the alias + and uses the wrong tablename). + """ + + if self.tablename != self._alias: + return current.s3db[self.tablename] + else: + return self.table + +# END ========================================================================= diff --git a/modules/core/resource/rfilter.py b/modules/core/resource/rfilter.py new file mode 100644 index 0000000000..a8ad585e5c --- /dev/null +++ b/modules/core/resource/rfilter.py @@ -0,0 +1,821 @@ +# -*- coding: utf-8 -*- + +""" Resource Filter + + @copyright: 2009-2021 (c) Sahana Software Foundation + @license: MIT + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +""" + +__all__ = ("S3ResourceFilter", + ) + +from functools import reduce + +from gluon import current +from gluon.storage import Storage + +from s3dal import Rows + +from .query import S3ResourceQuery, S3Joins, S3URLQuery + +# ============================================================================= +class S3ResourceFilter(object): + """ Class representing a resource filter """ + + def __init__(self, + resource, + id = None, + uid = None, + filter = None, + vars = None, + extra_filters = None, + filter_component = None): + """ + Constructor + + @param resource: the S3Resource + @param id: the record ID (or list of record IDs) + @param uid: the record UID (or list of record UIDs) + @param filter: a filter query (S3ResourceQuery or Query) + @param vars: the dict of GET vars (URL filters) + @param extra_filters: extra filters (to be applied on + pre-filtered subsets), as list of + tuples (method, expression) + @param filter_component: the alias of the component the URL + filters apply for (filters for this + component must be handled separately) + """ + + self.resource = resource + + self.queries = [] + self.filters = [] + self.cqueries = {} + self.cfilters = {} + + # Extra filters + self._extra_filter_methods = None + if extra_filters: + self.set_extra_filters(extra_filters) + else: + self.efilters = [] + + self.query = None + self.rfltr = None + self.vfltr = None + + self.transformed = None + + self.multiple = True + self.distinct = False + + # Joins + self.ijoins = {} + self.ljoins = {} + + table = resource.table + + # Accessible/available query + if resource.accessible_query is not None: + method = [] + if resource._approved: + method.append("read") + if resource._unapproved: + method.append("review") + mquery = resource.accessible_query(method, table) + else: + mquery = (table._id > 0) + + # ID query + if id is not None: + if not isinstance(id, (list, tuple)): + self.multiple = False + mquery = (table._id == id) & mquery + else: + mquery = (table._id.belongs(id)) & mquery + + # UID query + UID = current.xml.UID + if uid is not None and UID in table: + if not isinstance(uid, (list, tuple)): + self.multiple = False + mquery = (table[UID] == uid) & mquery + else: + mquery = (table[UID].belongs(uid)) & mquery + + # Deletion status + DELETED = current.xml.DELETED + if DELETED in table.fields and not resource.include_deleted: + remaining = (table[DELETED] == False) + mquery &= remaining + + parent = resource.parent + if not parent: + # Standard master query + self.mquery = mquery + + # URL queries + if vars: + resource.vars = Storage(vars) + + if not vars.get("track"): + # Apply BBox Filter unless using S3Track to geolocate + bbox, joins = self.parse_bbox_query(resource, vars) + if bbox is not None: + self.queries.append(bbox) + if joins: + self.ljoins.update(joins) + + # Filters + add_filter = self.add_filter + + # Current concept: + # Interpret all URL filters in the context of master + queries = S3URLQuery.parse(resource, vars) + + # @todo: Alternative concept (inconsistent?): + # Interpret all URL filters in the context of filter_component: + #if filter_component: + # context = resource.components.get(filter_component) + # if not context: + # context = resource + #queries = S3URLQuery.parse(context, vars) + + for alias in queries: + if filter_component == alias: + for q in queries[alias]: + add_filter(q, component=alias, master=False) + else: + for q in queries[alias]: + add_filter(q) + self.cfilters = queries + else: + # Parent filter + pf = parent.rfilter + if not pf: + pf = parent.build_query() + + # Extended master query + self.mquery = mquery & pf.get_query() + + # Join the master + self.ijoins[parent._alias] = resource._join(reverse=True) + + # Component/link-table specific filters + add_filter = self.add_filter + aliases = [resource.alias] + if resource.link is not None: + aliases.append(resource.link.alias) + elif resource.linked is not None: + aliases.append(resource.linked.alias) + for alias in aliases: + for filter_set in (pf.cqueries, pf.cfilters): + if alias in filter_set: + for q in filter_set[alias]: + add_filter(q) + + # Additional filters + if filter is not None: + self.add_filter(filter) + + # ------------------------------------------------------------------------- + # Properties + # ------------------------------------------------------------------------- + @property + def extra_filter_methods(self): + """ + Getter for extra filter methods, lazy property so methods + are only imported/initialized when needed + + @todo: document the expected signature of filter methods + + @return: dict {name: callable} of known named filter methods + """ + + methods = self._extra_filter_methods + if methods is None: + + # @todo: implement hooks + methods = {} + + self._extra_filter_methods = methods + + return methods + + # ------------------------------------------------------------------------- + # Manipulation + # ------------------------------------------------------------------------- + def add_filter(self, query, component=None, master=True): + """ + Extend this filter + + @param query: a Query or S3ResourceQuery object + @param component: alias of the component the filter shall be + added to (None for master) + @param master: False to filter only component + """ + + alias = None + if not master: + if not component: + return + if component != self.resource.alias: + alias = component + + if isinstance(query, S3ResourceQuery): + self.transformed = None + filters = self.filters + cfilters = self.cfilters + self.distinct |= query._joins(self.resource)[1] + + else: + # DAL Query + filters = self.queries + cfilters = self.cqueries + + self.query = None + if alias: + if alias in self.cfilters: + cfilters[alias].append(query) + else: + cfilters[alias] = [query] + else: + filters.append(query) + return + + # ------------------------------------------------------------------------- + def add_extra_filter(self, method, expression): + """ + Add an extra filter + + @param method: a name of a known filter method, or a + callable filter method + @param expression: the filter expression (string) + """ + + efilters = self.efilters + efilters.append((method, expression)) + + return efilters + + # ------------------------------------------------------------------------- + def set_extra_filters(self, filters): + """ + Replace the current extra filters + + @param filters: list of tuples (method, expression), or None + to remove all extra filters + """ + + self.efilters = [] + if filters: + add = self.add_extra_filter + for method, expression in filters: + add(method, expression) + + return self.efilters + + # ------------------------------------------------------------------------- + # Getters + # ------------------------------------------------------------------------- + def get_query(self): + """ Get the effective DAL query """ + + if self.query is not None: + return self.query + + resource = self.resource + + query = reduce(lambda x, y: x & y, self.queries, self.mquery) + if self.filters: + if self.transformed is None: + + # Combine all filters + filters = reduce(lambda x, y: x & y, self.filters) + + # Transform with external search engine + transformed = filters.transform(resource) + self.transformed = transformed + + # Split DAL and virtual filters + self.rfltr, self.vfltr = transformed.split(resource) + + # Add to query + rfltr = self.rfltr + if isinstance(rfltr, S3ResourceQuery): + + # Resolve query against the resource + rq = rfltr.query(resource) + + # False indicates that the subquery shall be ignored + # (e.g. if not supported by platform) + if rq is not False: + query &= rq + + elif rfltr is not None: + + # Combination of virtual field filter and web2py Query + query &= rfltr + + self.query = query + return query + + # ------------------------------------------------------------------------- + def get_filter(self): + """ Get the effective virtual filter """ + + if self.query is None: + self.get_query() + return self.vfltr + + # ------------------------------------------------------------------------- + def get_extra_filters(self): + """ + Get the list of extra filters + + @return: list of tuples (method, expression) + """ + + return list(self.efilters) + + # ------------------------------------------------------------------------- + def get_joins(self, left=False, as_list=True): + """ + Get the joins required for this filter + + @param left: get the left joins + @param as_list: return a flat list rather than a nested dict + """ + + if self.query is None: + self.get_query() + + joins = dict(self.ljoins if left else self.ijoins) + + resource = self.resource + for q in self.filters: + subjoins = q._joins(resource, left=left)[0] + joins.update(subjoins) + + # Cross-component left joins + parent = resource.parent + if parent: + pf = parent.rfilter + if pf is None: + pf = parent.build_query() + + parent_left = pf.get_joins(left=True, as_list=False) + if parent_left: + tablename = resource._alias + if left: + for tn in parent_left: + if tn not in joins and tn != tablename: + joins[tn] = parent_left[tn] + joins[parent._alias] = resource._join(reverse=True) + else: + joins.pop(parent._alias, None) + + if as_list: + return [j for tablename in joins for j in joins[tablename]] + else: + return joins + + # ------------------------------------------------------------------------- + def get_fields(self): + """ Get all field selectors in this filter """ + + if self.query is None: + self.get_query() + + if self.vfltr: + return self.vfltr.fields() + else: + return [] + + # ------------------------------------------------------------------------- + # Filtering + # ------------------------------------------------------------------------- + def __call__(self, rows, start=None, limit=None): + """ + Filter a set of rows by the effective virtual filter + + @param rows: a Rows object + @param start: index of the first matching record to select + @param limit: maximum number of records to select + """ + + vfltr = self.get_filter() + + if rows is None or vfltr is None: + return rows + resource = self.resource + if start is None: + start = 0 + first = start + if limit is not None: + last = start + limit + if last < first: + first, last = last, first + if first < 0: + first = 0 + if last < 0: + last = 0 + else: + last = None + i = 0 + result = [] + append = result.append + for row in rows: + if last is not None and i >= last: + break + success = vfltr(resource, row, virtual=True) + if success or success is None: + if i >= first: + append(row) + i += 1 + return Rows(rows.db, + result, + colnames = rows.colnames, + compact = False) + + # ------------------------------------------------------------------------- + def apply_extra_filters(self, ids, start=None, limit=None): + """ + Apply all extra filters on a list of record ids + + @param ids: the pre-filtered set of record IDs + @param limit: the maximum number of matching IDs to establish, + None to find all matching IDs + + @return: a sequence of matching IDs + """ + + # Get the resource + resource = self.resource + + # Get extra filters + efilters = self.efilters + + # Resolve filter methods + methods = self.extra_filter_methods + filters = [] + append = filters.append + for method, expression in efilters: + if callable(method): + append((method, expression)) + else: + method = methods.get(method) + if method: + append((method, expression)) + else: + current.log.warning("Unknown filter method: %s" % method) + if not filters: + # No applicable filters + return ids + + # Clear extra filters so that apply_extra_filters is not + # called from inside a filter method (e.g. if the method + # uses resource.select) + self.efilters = [] + + # Initialize subset + subset = set() + tail = ids + limit_ = limit + + while tail: + + if limit: + head, tail = tail[:limit_], tail[limit_:] + else: + head, tail = tail, None + + match = head + for method, expression in filters: + # Apply filter + match = method(resource, match, expression) + if not match: + break + + if match: + subset |= set(match) + + found = len(subset) + + if limit: + if found < limit: + # Need more + limit_ = limit - found + else: + # Found all + tail = None + + # Restore order + subset = [item for item in ids if item in subset] + + # Select start + if start: + subset = subset[start:] + + # Restore extra filters + self.efilters = efilters + + return subset + + # ------------------------------------------------------------------------- + def count(self, left=None, distinct=False): + """ + Get the total number of matching records + + @param left: left outer joins + @param distinct: count only distinct rows + """ + + distinct |= self.distinct + + resource = self.resource + if resource is None: + return 0 + + table = resource.table + + vfltr = self.get_filter() + + if vfltr is None and not distinct: + + tablename = table._tablename + + ijoins = S3Joins(tablename, self.get_joins(left=False)) + ljoins = S3Joins(tablename, self.get_joins(left=True)) + ljoins.add(left) + + join = ijoins.as_list(prefer=ljoins) + left = ljoins.as_list() + + cnt = table._id.count() + row = current.db(self.query).select(cnt, + join=join, + left=left).first() + if row: + return row[cnt] + else: + return 0 + + else: + data = resource.select([table._id.name], + # We don't really want to retrieve + # any rows but just count, hence: + limit=1, + count=True) + return data["numrows"] + + # ------------------------------------------------------------------------- + # Utility Methods + # ------------------------------------------------------------------------- + def __repr__(self): + """ String representation of the instance """ + + resource = self.resource + + inner_joins = self.get_joins(left=False) + if inner_joins: + inner = S3Joins(resource.tablename, inner_joins) + ijoins = ", ".join([str(j) for j in inner.as_list()]) + else: + ijoins = None + + left_joins = self.get_joins(left=True) + if left_joins: + left = S3Joins(resource.tablename, left_joins) + ljoins = ", ".join([str(j) for j in left.as_list()]) + else: + ljoins = None + + vfltr = self.get_filter() + if vfltr: + vfltr = vfltr.represent(resource) + else: + vfltr = None + + represent = "" % (resource.tablename, + self.get_query(), + ijoins, + ljoins, + self.distinct, + vfltr, + ) + + return represent + + # ------------------------------------------------------------------------- + @staticmethod + def parse_bbox_query(resource, get_vars): + """ + Generate a Query from a URL boundary box query; supports multiple + bboxes, but optimised for the usual case of just 1 + + @param resource: the resource + @param get_vars: the URL GET vars + """ + + tablenames = ("gis_location", + "gis_feature_query", + "gis_layer_shapefile", + ) + + POLYGON = "POLYGON((%s %s, %s %s, %s %s, %s %s, %s %s))" + + query = None + joins = {} + + if get_vars: + + table = resource.table + tablename = resource.tablename + fields = table.fields + + introspect = tablename not in tablenames + for k, v in get_vars.items(): + + if k[:4] == "bbox": + + if type(v) is list: + v = v[-1] + try: + minLon, minLat, maxLon, maxLat = v.split(",") + except ValueError: + # Badly-formed bbox - ignore + continue + + # Identify the location reference + field = None + rfield = None + alias = False + + if k.find(".") != -1: + + # Field specified in query + fname = k.split(".")[1] + if fname not in fields: + # Field not found - ignore + continue + field = table[fname] + if query is not None or "bbox" in get_vars: + # Need alias + alias = True + + elif introspect: + + # Location context? + context = resource.get_config("context") + if context and "location" in context: + try: + rfield = resource.resolve_selector("(location)$lat") + except (SyntaxError, AttributeError): + rfield = None + else: + if not rfield.field or rfield.tname != "gis_location": + # Invalid location context + rfield = None + + # Fall back to location_id (or site_id as last resort) + if rfield is None: + fname = None + for f in fields: + ftype = str(table[f].type) + if ftype[:22] == "reference gis_location": + fname = f + break + elif not fname and \ + ftype[:18] == "reference org_site": + fname = f + field = table[fname] if fname else None + + if not rfield and not field: + # No location reference could be identified => skip + continue + + # Construct the join to gis_location + gtable = current.s3db.gis_location + if rfield: + joins.update(rfield.left) + + elif field: + fname = field.name + gtable = current.s3db.gis_location + if alias: + gtable = gtable.with_alias("gis_%s_location" % fname) + tname = str(gtable) + ftype = str(field.type) + if ftype == "reference gis_location": + joins[tname] = [gtable.on(gtable.id == field)] + elif ftype == "reference org_site": + stable = current.s3db.org_site + if alias: + stable = stable.with_alias("org_%s_site" % fname) + joins[tname] = [stable.on(stable.site_id == field), + gtable.on(gtable.id == stable.location_id)] + elif introspect: + # => not a location or site reference + continue + + elif tablename in ("gis_location", "gis_feature_query"): + gtable = table + + elif tablename == "gis_layer_shapefile": + # Find the layer_shapefile_%(layer_id)s component + # (added dynamically in gis/layer_shapefile controller) + gtable = None + hooks = current.s3db.get_hooks("gis_layer_shapefile")[1] + for alias in hooks: + if alias[:19] == "gis_layer_shapefile": + component = resource.components.get(alias) + if component: + gtable = component.table + break + # Join by layer_id + if gtable: + joins[str(gtable)] = \ + [gtable.on(gtable.layer_id == table._id)] + else: + continue + + # Construct the bbox filter + bbox_filter = None + if current.deployment_settings.get_gis_spatialdb(): + # Use the Spatial Database + minLon = float(minLon) + maxLon = float(maxLon) + minLat = float(minLat) + maxLat = float(maxLat) + bbox = POLYGON % (minLon, minLat, + minLon, maxLat, + maxLon, maxLat, + maxLon, minLat, + minLon, minLat) + try: + # Spatial DAL & Database + bbox_filter = gtable.the_geom \ + .st_intersects(bbox) + except: + # Old DAL or non-spatial database + pass + + if bbox_filter is None: + # Standard Query + bbox_filter = (gtable.lon > float(minLon)) & \ + (gtable.lon < float(maxLon)) & \ + (gtable.lat > float(minLat)) & \ + (gtable.lat < float(maxLat)) + + # Add bbox filter to query + if query is None: + query = bbox_filter + else: + # Merge with the previous BBOX + query = query & bbox_filter + + return query, joins + + # ------------------------------------------------------------------------- + def serialize_url(self): + """ + Serialize this filter as URL query + + @return: a Storage of URL GET variables + """ + + resource = self.resource + url_vars = Storage() + for f in self.filters: + sub = f.serialize_url(resource=resource) + url_vars.update(sub) + return url_vars + +# END ========================================================================= diff --git a/modules/core/io/rtb.py b/modules/core/resource/rtb.py similarity index 99% rename from modules/core/io/rtb.py rename to modules/core/resource/rtb.py index bdd70a3979..e25c7a3650 100644 --- a/modules/core/io/rtb.py +++ b/modules/core/resource/rtb.py @@ -38,10 +38,11 @@ from s3dal import original_tablename -from ..model import DEFAULT, MAXDEPTH -from ..filters import FS, S3URLQuery from ..tools import s3_get_foreign_key, s3_str, S3Represent, S3RepresentLazy +from .query import FS, S3URLQuery +from .resource import DEFAULT, MAXDEPTH + # ============================================================================= class S3ResourceTree(object): """ Resource Tree Builder """ diff --git a/modules/core/resource/select.py b/modules/core/resource/select.py new file mode 100644 index 0000000000..b6255baf48 --- /dev/null +++ b/modules/core/resource/select.py @@ -0,0 +1,1395 @@ +# -*- coding: utf-8 -*- + +""" Resource Data Reader + + @copyright: 2009-2021 (c) Sahana Software Foundation + @license: MIT + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. +""" + +import json + +from itertools import chain + +from gluon import current +from gluon.html import TAG +from gluon.storage import Storage + +from s3dal import Expression, Field, Row, Rows, S3DAL, VirtualCommand +from ..tools import s3_str + +from .query import S3Joins + +osetattr = object.__setattr__ +ogetattr = object.__getattribute__ + +# ============================================================================= +class S3ResourceData(object): + """ Class representing data in a resource """ + + def __init__(self, + resource, + fields, + start = 0, + limit = None, + left = None, + orderby = None, + groupby = None, + distinct = False, + virtual = True, + count = False, + getids = False, + as_rows = False, + represent = False, + show_links = True, + raw_data = False + ): + """ + Constructor, extracts (and represents) data from a resource + + @param resource: the resource + @param fields: the fields to extract (selector strings) + @param start: index of the first record + @param limit: maximum number of records + @param left: additional left joins required for custom filters + @param orderby: orderby-expression for DAL + @param groupby: fields to group by (overrides fields!) + @param distinct: select distinct rows + @param virtual: include mandatory virtual fields + @param count: include the total number of matching records + @param getids: include the IDs of all matching records + @param as_rows: return the rows (don't extract/represent) + @param represent: render field value representations + @param raw_data: include raw data in the result + + @note: as_rows / groupby prevent automatic splitting of + large multi-table joins, so use with care! + @note: with groupby, only the groupby fields will be returned + (i.e. fields will be ignored), because aggregates are + not supported (yet) + """ + + db = current.db + + # Suppress instantiation of LazySets in rows where we don't need them + if not as_rows and not groupby: + rname = db._referee_name + db._referee_name = None + else: + rname = None + + # The resource + self.resource = resource + self.table = table = resource.table + + # If postprocessing is required, always include raw data + postprocess = resource.get_config("postprocess_select") + if postprocess: + raw_data = True + + # Dict to collect accessible queries for differential + # field authorization (each joined table is authorized + # separately) + self.aqueries = aqueries = {} + + # Retain the current accessible-context of the parent + # resource in reverse component joins: + parent = resource.parent + if parent and parent.accessible_query is not None: + method = [] + if parent._approved: + method.append("read") + if parent._unapproved: + method.append("review") + aqueries[parent.tablename] = parent.accessible_query(method, + parent.table, + ) + + # Joins (inner/left) + tablename = table._tablename + self.ijoins = ijoins = S3Joins(tablename) + self.ljoins = ljoins = S3Joins(tablename) + + # The query + master_query = query = resource.get_query() + + # Joins from filters + # @note: in components, rfilter is None until after get_query! + rfilter = resource.rfilter + filter_tables = set(ijoins.add(rfilter.get_joins(left=False))) + filter_tables.update(ljoins.add(rfilter.get_joins(left=True))) + + # Left joins from caller + master_tables = set(ljoins.add(left)) + filter_tables.update(master_tables) + + resolve = resource.resolve_selectors + + # Virtual fields and extra fields required by filter + virtual_fields = rfilter.get_fields() + vfields, vijoins, vljoins, d = resolve(virtual_fields, show=False) + extra_tables = set(ijoins.extend(vijoins)) + extra_tables.update(ljoins.extend(vljoins)) + distinct |= d + + # Display fields (fields to include in the result) + if fields is None: + fields = [f.name for f in resource.readable_fields()] + dfields, dijoins, dljoins, d = resolve(fields, extra_fields=False) + ijoins.extend(dijoins) + ljoins.extend(dljoins) + distinct |= d + + # Primary key + pkey = str(table._id) + + # Initialize field data and effort estimates + if not groupby or as_rows: + self.init_field_data(dfields) + else: + self.field_data = self.effort = None + + # Resolve ORDERBY + orderby, orderby_aggr, orderby_fields, tables = self.resolve_orderby(orderby) + if tables: + filter_tables.update(tables) + + # Joins for filter query + filter_ijoins = ijoins.as_list(tablenames = filter_tables, + aqueries = aqueries, + prefer = ljoins, + ) + filter_ljoins = ljoins.as_list(tablenames = filter_tables, + aqueries = aqueries, + ) + + # Virtual fields filter + vfilter = resource.get_filter() + + # Extra filters + efilter = rfilter.get_extra_filters() + + # Is this a paginated request? + pagination = limit is not None or start + + # Subselect? + subselect = bool(ljoins or ijoins or efilter or vfilter and pagination) + + # Do we need a filter query? + fq = count_only = False + if not groupby: + end_count = (vfilter or efilter) and not pagination + if count and not end_count: + fq = True + count_only = True + if subselect or \ + getids and pagination or \ + extra_tables and extra_tables != filter_tables: + fq = True + count_only = False + + # Shall we use scalability-optimized strategies? + bigtable = current.deployment_settings.get_base_bigtable() + + # Filter Query: + # If we need to determine the number and/or ids of all matching + # records, but not to extract all records, then we run a + # separate query here to extract just this information: + ids = page = totalrows = None + if fq: + # Execute the filter query + if bigtable and not vfilter: + limitby = resource.limitby(start=start, limit=limit) + else: + limitby = None + totalrows, ids = self.filter_query(query, + join = filter_ijoins, + left = filter_ljoins, + getids = not count_only, + orderby = orderby_aggr, + limitby = limitby, + ) + + # Simplify the master query if possible + empty = False + limitby = None + orderby_on_limitby = True + + # If we know all possible record IDs from the filter query, + # then we can simplify the master query so it doesn't need + # complex joins + if ids is not None: + if not ids: + # No records matching the filter query, so we + # can skip the master query too + empty = True + else: + # Which records do we need to extract? + if pagination and (efilter or vfilter): + master_ids = ids + else: + if bigtable: + master_ids = page = ids + else: + limitby = resource.limitby(start=start, limit=limit) + if limitby: + page = ids[limitby[0]:limitby[1]] + else: + page = ids + master_ids = page + + # Simplify master query + if page is not None and not page: + # Empty page, skip the master query + empty = True + master_query = None + elif len(master_ids) == 1: + # Single record, don't use belongs (faster) + master_query = table._id == master_ids[0] + else: + master_query = table._id.belongs(set(master_ids)) + + orderby = None + if not ljoins or ijoins: + # Without joins, there can only be one row per id, + # so we can limit the master query (faster) + limitby = (0, len(master_ids)) + # Prevent automatic ordering + orderby_on_limitby = False + else: + # With joins, there could be more than one row per id, + # so we can not limit the master query + limitby = None + + elif pagination and not (efilter or vfilter or count or getids): + + limitby = resource.limitby(start=start, limit=limit) + + if not empty: + # If we don't use a simplified master_query, we must include + # all necessary joins for filter and orderby (=filter_tables) in + # the master query + if ids is None and (filter_ijoins or filter_ljoins): + master_tables = filter_tables + + # Determine fields in master query + if not groupby: + master_tables.update(extra_tables) + tables, qfields, mfields, groupby = self.master_fields(dfields, + vfields, + master_tables, + as_rows = as_rows, + groupby = groupby, + ) + # Additional tables to join? + if tables: + master_tables.update(tables) + + # ORDERBY settings + if groupby: + distinct = False + orderby = orderby_aggr + has_id = pkey in qfields + else: + if distinct and orderby: + # With DISTINCT, ORDERBY-fields must appear in SELECT + # (required by postgresql?) + for orderby_field in orderby_fields: + fn = str(orderby_field) + if fn not in qfields: + qfields[fn] = orderby_field + + # Make sure we have the primary key in SELECT + if pkey not in qfields: + qfields[pkey] = resource._id + has_id = True + + # Execute master query + db = current.db + + master_fields = list(qfields.keys()) + if not groupby and not pagination and \ + has_id and ids and len(master_fields) == 1: + # We already have the ids, and master query doesn't select + # anything else => skip the master query, construct Rows from + # ids instead + master_id = table._id.name + rows = Rows(db, + [Row({master_id: record_id}) for record_id in ids], + colnames = [pkey], + compact = False, + ) + # Add field methods (some do work from bare ids) + try: + fields_lazy = [(f.name, f) for f in table._virtual_methods] + except (AttributeError, TypeError): + # Incompatible PyDAL version + pass + else: + if fields_lazy: + for row in rows: + for f, v in fields_lazy: + try: + row[f] = (v.handler or VirtualCommand)(v.f, row) + except (AttributeError, KeyError): + pass + else: + # Joins for master query + master_ijoins = ijoins.as_list(tablenames = master_tables, + aqueries = aqueries, + prefer = ljoins, + ) + master_ljoins = ljoins.as_list(tablenames = master_tables, + aqueries = aqueries, + ) + + # Suspend (mandatory) virtual fields if so requested + if not virtual: + vf = table.virtualfields + osetattr(table, "virtualfields", []) + + rows = db(master_query).select(join = master_ijoins, + left = master_ljoins, + distinct = distinct, + groupby = groupby, + orderby = orderby, + limitby = limitby, + orderby_on_limitby = orderby_on_limitby, + cacheable = not as_rows, + *list(qfields.values())) + + # Restore virtual fields + if not virtual: + osetattr(table, "virtualfields", vf) + + else: + rows = Rows(current.db) + + # Apply any virtual/extra filters, determine the subset + if not len(rows) and not ids: + + # Empty set => empty subset (no point to filter/count) + page = [] + ids = [] + totalrows = 0 + + elif not groupby: + if efilter or vfilter: + + # Filter by virtual fields + shortcut = False + if vfilter: + if pagination and not any((getids, count, efilter)): + # Don't need ids or totalrows + rows = rfilter(rows, start=start, limit=limit) + page = self.getids(rows, pkey) + shortcut = True + else: + rows = rfilter(rows) + + # Extra filter + if efilter: + if vfilter or not ids: + ids = self.getids(rows, pkey) + if pagination and not (getids or count): + limit_ = start + limit + else: + limit_ = None + ids = rfilter.apply_extra_filters(ids, limit = limit_) + rows = self.getrows(rows, ids, pkey) + + if pagination: + # Subset selection with vfilter/efilter + # (=post-filter pagination) + if not shortcut: + if not efilter: + ids = self.getids(rows, pkey) + totalrows = len(ids) + rows, page = self.subset(rows, ids, + start = start, + limit = limit, + has_id = has_id, + ) + else: + # Unlimited select with vfilter/efilter + if not efilter: + ids = self.getids(rows, pkey) + page = ids + totalrows = len(ids) + + elif pagination: + + if page is None: + if limitby: + # Limited master query without count/getids + # (=rows is the subset, only need page IDs) + page = self.getids(rows, pkey) + else: + # Limited select with unlimited master query + # (=getids/count without filter query, need subset) + if not ids: + ids = self.getids(rows, pkey) + # Build the subset + rows, page = self.subset(rows, ids, + start = start, + limit = limit, + has_id = has_id, + ) + totalrows = len(ids) + + elif not ids: + # Unlimited select without vfilter/efilter + page = ids = self.getids(rows, pkey) + totalrows = len(ids) + + # Build the result + self.rfields = dfields + self.numrows = 0 if totalrows is None else totalrows + self.ids = ids + + if groupby or as_rows: + # Just store the rows, no further queries or extraction + self.rows = rows + + elif not rows: + # No rows found => empty list + self.rows = [] + + else: + # Extract the data from the master rows + records = self.extract(rows, + pkey, + list(mfields), + join = hasattr(rows[0], tablename), + represent = represent, + ) + + # Extract the page record IDs if we don't have them yet + if page is None: + if ids is None: + self.ids = ids = self.getids(rows, pkey) + page = ids + + + # Execute any joined queries + joined_fields = self.joined_fields(dfields, qfields) + joined_query = table._id.belongs(page) + + for jtablename, jfields in joined_fields.items(): + records = self.joined_query(jtablename, + joined_query, + jfields, + records, + represent = represent, + ) + + # Re-combine and represent the records + results = {} + + field_data = self.field_data + NONE = current.messages["NONE"] + + render = self.render + for dfield in dfields: + + if represent: + # results = {RecordID: {ColumnName: Representation}} + results = render(dfield, + results, + none = NONE, + raw_data = raw_data, + show_links = show_links, + ) + + else: + # results = {RecordID: {ColumnName: Value}} + colname = dfield.colname + + fdata = field_data[colname] + frecords = fdata[1] + list_type = fdata[3] + + for record_id in records: + if record_id not in results: + result = results[record_id] = Storage() + else: + result = results[record_id] + + data = list(frecords[record_id].keys()) + if len(data) == 1 and not list_type: + data = data[0] + result[colname] = data + + self.rows = [results[record_id] for record_id in page] + + if rname: + # Restore referee name + db._referee_name = rname + + # Postprocess data (postprocess_select hook of the resource): + # Allow the callback to modify the selected data before + # returning them to the caller, callback receives: + # - a dict with the data {record_id: row} + # - the list of resource fields + # - the represent-flag to indicate represented data + # - the as_rows-flag to indicate bare Rows in the data dict + # NB the callback must not remove fields from the rows + if postprocess: + postprocess(dict(zip(page, self.rows)), + rfields = dfields, + represent = represent, + as_rows = bool(as_rows or groupby), + ) + + # ------------------------------------------------------------------------- + def init_field_data(self, rfields): + """ + Initialize field data and effort estimates for representation + + Field data: allow representation per unique value (rather than + record by record), together with bulk-represent this + can reduce the total lookup effort per field to a + single query + + Effort estimates: if no bulk-represent is available for a + list:reference, then a lookup per unique value + is only faster if the number of unique values + is significantly lower than the number of + extracted rows (and the number of values per + row), otherwise a per-row lookup is more + efficient. + + E.g. 5 rows with 2 values each, + 10 unique values in total + => row-by-row lookup more efficient + (5 queries vs 10 queries) + but: 5 rows with 2 values each, + 2 unique values in total + => value-by-value lookup is faster + (5 queries vs 2 queries) + + However: 15 rows with 15 values each, + 20 unique values in total + => value-by-value lookup faster + (15 queries á 15 values vs. + 20 queries á 1 value)! + + The required effort is estimated + during the data extraction, and then used to + determine the lookup strategy for the + representation. + + @param rfields: the fields to extract ([S3ResourceField]) + """ + + table = self.resource.table + tablename = table._tablename + pkey = str(table._id) + + field_data = {pkey: ({}, {}, False, False, False, False)} + effort = {pkey: 0} + for dfield in rfields: + colname = dfield.colname + effort[colname] = 0 + ftype = dfield.ftype[:4] + field_data[colname] = ({}, {}, + dfield.tname != tablename, + ftype == "list", + dfield.virtual, + ftype == "json", + ) + + self.field_data = field_data + self.effort = effort + + return + + # ------------------------------------------------------------------------- + def resolve_orderby(self, orderby): + """ + Resolve the ORDERBY expression. + + @param orderby: the orderby expression from the caller + @return: tuple (expr, aggr, fields, tables): + expr: the orderby expression (resolved into Fields) + aggr: the orderby expression with aggregations + fields: the fields in the orderby + tables: the tables required for the orderby + + @note: for GROUPBY id (e.g. filter query), all ORDERBY fields + must appear in aggregation functions, otherwise ORDERBY + can be ambiguous => use aggr instead of expr + """ + + table = self.resource.table + tablename = table._tablename + pkey = str(table._id) + + ljoins = self.ljoins + ijoins = self.ijoins + + tables = set() + adapter = S3DAL() + + if orderby: + + db = current.db + items = self.resolve_expression(orderby) + + expr = [] + aggr = [] + fields = [] + + for item in items: + + expression = None + + if type(item) is Expression: + f = item.first + op = item.op + if op == adapter.AGGREGATE: + # Already an aggregation + expression = item + elif isinstance(f, Field) and op == adapter.INVERT: + direction = "desc" + else: + # Other expression - not supported + continue + elif isinstance(item, Field): + direction = "asc" + f = item + elif isinstance(item, str): + fn, direction = (item.strip().split() + ["asc"])[:2] + tn, fn = ([tablename] + fn.split(".", 1))[-2:] + try: + f = db[tn][fn] + except (AttributeError, KeyError): + continue + else: + continue + + fname = str(f) + tname = fname.split(".", 1)[0] + + if tname != tablename: + if tname in ljoins or tname in ijoins: + tables.add(tname) + else: + # No join found for this field => skip + continue + + fields.append(f) + if expression is None: + expression = f if direction == "asc" else ~f + expr.append(expression) + direction = direction.strip().lower()[:3] + if fname != pkey: + expression = f.min() if direction == "asc" else ~(f.max()) + else: + expr.append(expression) + aggr.append(expression) + + else: + expr = None + aggr = None + fields = None + + return expr, aggr, fields, tables + + # ------------------------------------------------------------------------- + def filter_query(self, + query, + join = None, + left = None, + getids = False, + limitby = None, + orderby = None, + ): + """ + Execute a query to determine the number/record IDs of all + matching rows + + @param query: the filter query + @param join: the inner joins for the query + @param left: the left joins for the query + @param getids: extract the IDs of matching records + @param limitby: tuple of indices (start, end) to extract only + a limited set of IDs + @param orderby: ORDERBY expression for the query + + @return: tuple of (TotalNumberOfRecords, RecordIDs) + """ + + db = current.db + + table = self.table + + # Temporarily deactivate virtual fields + vf = table.virtualfields + osetattr(table, "virtualfields", []) + + if getids and limitby: + # Large result sets expected on average (settings.base.bigtable) + # => effort almost independent of result size, much faster + # for large and very large filter results + start = limitby[0] + limit = limitby[1] - start + + # Don't penalize the smallest filter results (=effective filtering) + if limit: + maxids = max(limit, 200) + limitby_ = (start, start + maxids) + else: + limitby_ = None + + # Extract record IDs + field = table._id + rows = db(query).select(field, + join = join, + left = left, + limitby = limitby_, + orderby = orderby, + groupby = field, + cacheable = True, + ) + pkey = str(field) + results = rows[:limit] if limit else rows + ids = [row[pkey] for row in results] + + totalids = len(rows) + if limit and totalids >= maxids or start != 0 and not totalids: + # Count all matching records + cnt = table._id.count(distinct=True) + row = db(query).select(cnt, + join = join, + left = left, + cacheable = True, + ).first() + totalrows = row[cnt] + else: + # We already know how many there are + totalrows = start + totalids + + elif getids: + # Extract all matching IDs, then count them in Python + # => effort proportional to result size, slightly faster + # than counting separately for small filter results + field = table._id + rows = db(query).select(field, + join=join, + left=left, + orderby = orderby, + groupby = field, + cacheable = True, + ) + pkey = str(field) + ids = [row[pkey] for row in rows] + totalrows = len(ids) + + else: + # Only count, do not extract any IDs (constant effort) + field = table._id.count(distinct=True) + rows = db(query).select(field, + join = join, + left = left, + cacheable = True, + ) + ids = None + totalrows = rows.first()[field] + + # Restore the virtual fields + osetattr(table, "virtualfields", vf) + + return totalrows, ids + + # ------------------------------------------------------------------------- + def master_fields(self, + dfields, + vfields, + joined_tables, + as_rows = False, + groupby = None + ): + """ + Find all tables and fields to retrieve in the master query + + @param dfields: the requested fields (S3ResourceFields) + @param vfields: the virtual filter fields + @param joined_tables: the tables joined in the master query + @param as_rows: whether to produce web2py Rows + @param groupby: the GROUPBY expression from the caller + + @return: tuple (tables, fields, extract, groupby): + tables: the tables required to join + fields: the fields to retrieve + extract: the fields to extract from the result + groupby: the GROUPBY expression (resolved into Fields) + """ + + db = current.db + tablename = self.resource.table._tablename + + # Names of additional tables to join + tables = set() + + # Fields to retrieve in the master query, as dict {ColumnName: Field} + fields = {} + + # Column names of fields to extract from the master rows + extract = set() + + if groupby: + # Resolve the groupby into Fields + items = self.resolve_expression(groupby) + + groupby = [] + groupby_append = groupby.append + for item in items: + + # Identify the field + tname = None + if isinstance(item, Field): + f = item + elif isinstance(item, str): + fn = item.strip() + tname, fn = ([tablename] + fn.split(".", 1))[-2:] + try: + f = db[tname][fn] + except (AttributeError, KeyError): + continue + else: + continue + groupby_append(f) + + # Add to fields + fname = str(f) + if not tname: + tname = f.tablename + fields[fname] = f + + # Do we need to join additional tables? + if tname == tablename: + # no join required + continue + else: + # Get joins from dfields + tnames = None + for dfield in dfields: + if dfield.colname == fname: + tnames = self.rfield_tables(dfield) + break + if tnames: + tables |= tnames + else: + # Join at least the table that holds the fields + tables.add(tname) + + # Only extract GROUPBY fields (as we don't support aggregates) + extract = set(fields.keys()) + + else: + rfields = dfields + vfields + for rfield in rfields: + + # Is the field in a joined table? + tname = rfield.tname + joined = tname == tablename or tname in joined_tables + + if as_rows or joined: + colname = rfield.colname + if rfield.show: + # If show => add to extract + extract.add(colname) + if rfield.field: + # If real field => add to fields + fields[colname] = rfield.field + if not joined: + # Not joined yet? => add all required tables + tables |= self.rfield_tables(rfield) + + return tables, fields, extract, groupby + + # ------------------------------------------------------------------------- + def joined_fields(self, all_fields, master_fields): + """ + Determine which fields in joined tables haven't been + retrieved in the master query + + @param all_fields: all requested fields (list of S3ResourceFields) + @param master_fields: all fields in the master query, a dict + {ColumnName: Field} + + @return: a nested dict {TableName: {ColumnName: Field}}, + additionally required left joins are stored per + table in the inner dict as "_left" + """ + + resource = self.resource + table = resource.table + tablename = table._tablename + + fields = {} + for rfield in all_fields: + + colname = rfield.colname + if colname in master_fields or rfield.tname == tablename: + continue + tname = rfield.tname + + if tname not in fields: + sfields = fields[tname] = {} + left = rfield.left + joins = S3Joins(table) + for tn in left: + joins.add(left[tn]) + sfields["_left"] = joins + else: + sfields = fields[tname] + + if colname not in sfields: + sfields[colname] = rfield.field + + return fields + + # ------------------------------------------------------------------------- + def joined_query(self, tablename, query, fields, records, represent=False): + """ + Extract additional fields from a joined table: if there are + fields in joined tables which haven't been extracted in the + master query, then we perform a separate query for each joined + table (this is faster than building a multi-table-join) + + @param tablename: name of the joined table + @param query: the Query + @param fields: the fields to extract + @param records: the output dict to update, structure: + {RecordID: {ColumnName: RawValues}} + @param represent: store extracted data (self.field_data) for + fast representation, and estimate lookup + efforts (self.effort) + + @return: the output dict + """ + + s3db = current.s3db + + ljoins = self.ljoins + table = self.resource.table + pkey = str(table._id) + + # Get the extra fields for subtable + sresource = s3db.resource(tablename) + efields, ejoins, l, d = sresource.resolve_selectors([]) + + # Get all left joins for subtable + tnames = ljoins.extend(l) + list(fields["_left"].tables) + sjoins = ljoins.as_list(tablenames = tnames, + aqueries = self.aqueries, + ) + if not sjoins: + return records + del fields["_left"] + + # Get all fields for subtable query + extract = list(fields.keys()) + for efield in efields: + fields[efield.colname] = efield.field + sfields = [f for f in fields.values() if f] + if not sfields: + sfields.append(sresource._id) + sfields.insert(0, table._id) + + # Retrieve the subtable rows + # - can't use distinct with native JSON fields + distinct = not any(f.type == "json" for f in sfields) + rows = current.db(query).select(left = sjoins, + distinct = distinct, + cacheable = True, + *sfields) + + # Extract and merge the data + records = self.extract(rows, + pkey, + extract, + records = records, + join = True, + represent = represent, + ) + + return records + + # ------------------------------------------------------------------------- + def extract(self, + rows, + pkey, + columns, + join = True, + records = None, + represent = False + ): + """ + Extract the data from rows and store them in self.field_data + + @param rows: the rows + @param pkey: the primary key + @param columns: the columns to extract + @param join: the rows are the result of a join query + @param records: the records dict to merge the data into + @param represent: collect unique values per field and estimate + representation efforts for list:types + """ + + field_data = self.field_data + effort = self.effort + + if records is None: + records = {} + + def get(key): + t, f = key.split(".", 1) + if join: + def getter(row): + return ogetattr(ogetattr(row, t), f) + else: + def getter(row): + return ogetattr(row, f) + return getter + + getkey = get(pkey) + getval = [get(c) for c in columns] + + from itertools import groupby + for k, g in groupby(rows, key=getkey): + group = list(g) + record = records.get(k, {}) + for idx, col in enumerate(columns): + fvalues, frecords, joined, list_type, virtual, json_type = field_data[col] + values = record.get(col, {}) + lazy = False + for row in group: + try: + value = getval[idx](row) + except AttributeError: + current.log.warning("Warning S3Resource.extract: column %s not in row" % col) + value = None + if lazy or callable(value): + # Lazy virtual field + value = value() + lazy = True + if virtual and not list_type and type(value) is list: + # Virtual field that returns a list + list_type = True + if list_type and value is not None: + if represent and value: + effort[col] += 30 + len(value) + for v in value: + if v not in values: + values[v] = None + if represent and v not in fvalues: + fvalues[v] = None + elif json_type: + # Returns unhashable types + value = json.dumps(value) + if value not in values: + values[value] = None + if represent and value not in fvalues: + fvalues[value] = None + else: + if value not in values: + values[value] = None + if represent and value not in fvalues: + fvalues[value] = None + record[col] = values + if k not in frecords: + frecords[k] = record[col] + records[k] = record + + return records + + # ------------------------------------------------------------------------- + def render(self, + rfield, + results, + none = "-", + raw_data = False, + show_links = True + ): + """ + Render the representations of the values for rfield in + all records in the result + + @param rfield: the field (S3ResourceField) + @param results: the output dict to update with the representations, + structure: {RecordID: {ColumnName: Representation}}, + the raw data will be a special item "_row" in the + inner dict holding a Storage of the raw field values + @param none: default representation of None + @param raw_data: retain the raw data in the output dict + @param show_links: allow representation functions to render + links as HTML + """ + + colname = rfield.colname + + fvalues, frecords, joined, list_type = self.field_data[colname][:4] + + # Get the renderer + renderer = rfield.represent + if not callable(renderer): + # @ToDo: Don't convert unformatted numbers to strings + renderer = lambda v: s3_str(v) if v is not None else none + + # Deactivate linkto if so requested + if not show_links and hasattr(renderer, "show_link"): + show_link = renderer.show_link + renderer.show_link = False + else: + show_link = None + + per_row_lookup = list_type and \ + self.effort[colname] < len(fvalues) * 30 + + # Treat even single values as lists? + # - can be set as class attribute of custom S3Represents + always_list = hasattr(renderer, "always_list") and renderer.always_list + + # Render all unique values + if hasattr(renderer, "bulk") and not list_type: + per_row_lookup = False + fvalues = renderer.bulk(list(fvalues.keys()), list_type=False) + elif not per_row_lookup: + for value in fvalues: + try: + text = renderer(value) + except: + #raise + text = s3_str(value) + fvalues[value] = text + + # Write representations into result + for record_id in frecords: + + if record_id not in results: + results[record_id] = Storage() \ + if not raw_data \ + else Storage(_row=Storage()) + + record = frecords[record_id] + result = results[record_id] + + # List type with per-row lookup? + if per_row_lookup: + value = list(record.keys()) + if None in value and len(value) > 1: + value = [v for v in value if v is not None] + try: + text = renderer(value) + except: + text = s3_str(value) + result[colname] = text + if raw_data: + result["_row"][colname] = value + + # Single value (master record) + elif len(record) == 1 and not always_list or \ + not joined and not list_type: + value = list(record.keys())[0] + result[colname] = fvalues[value] \ + if value in fvalues else none + if raw_data: + result["_row"][colname] = value + continue + + # Multiple values (joined or list-type, or explicit always_list) + else: + if hasattr(renderer, "render_list"): + # Prefer S3Represent's render_list (so it can be customized) + data = renderer.render_list(list(record.keys()), + fvalues, + show_link = show_links, + ) + else: + # Build comma-separated list of values + vlist = [] + for value in record: + if value is None and not list_type: + continue + value = fvalues[value] \ + if value in fvalues else none + vlist.append(value) + + if any([hasattr(v, "xml") for v in vlist]): + data = TAG[""]( + list( + chain.from_iterable( + [(v, ", ") for v in vlist]) + )[:-1] + ) + else: + data = ", ".join([s3_str(v) for v in vlist]) + + result[colname] = data + if raw_data: + result["_row"][colname] = list(record.keys()) + + # Restore linkto + if show_link is not None: + renderer.show_link = show_link + + return results + + # ------------------------------------------------------------------------- + def __getitem__(self, key): + """ + Helper method to access the results as dict items, for + backwards-compatibility + + @param key: the key + + @todo: migrate use-cases to . notation, then deprecate + """ + + if key in ("rfields", "numrows", "ids", "rows"): + return getattr(self, key) + else: + raise AttributeError + + # ------------------------------------------------------------------------- + @staticmethod + def getids(rows, pkey): + """ + Extract all unique record IDs from rows, preserving the + order by first match + + @param rows: the Rows + @param pkey: the primary key + + @return: list of unique record IDs + """ + + x = set() + seen = x.add + + result = [] + append = result.append + for row in rows: + row_id = row[pkey] + if row_id not in x: + seen(row_id) + append(row_id) + return result + + # ------------------------------------------------------------------------- + @staticmethod + def getrows(rows, ids, pkey): + """ + Select a subset of rows by their record IDs + + @param rows: the Rows + @param ids: the record IDs + @param pkey: the primary key + + @return: the subset (Rows) + """ + + if ids: + ids = set(ids) + subset = lambda row: row[pkey] in ids + else: + subset = lambda row: False + return rows.find(subset) + + # ------------------------------------------------------------------------- + @staticmethod + def subset(rows, ids, start=None, limit=None, has_id=True): + """ + Build a subset [start:limit] from rows and ids + + @param rows: the Rows + @param ids: all matching record IDs + @param start: start index of the page + @param limit: maximum length of the page + @param has_id: whether the Rows contain the primary key + + @return: tuple (rows, page), with: + rows = the Rows in the subset, in order + page = the record IDs in the subset, in order + """ + + if limit and start is None: + start = 0 + + if start is not None and limit is not None: + rows = rows[start:start+limit] + page = ids[start:start+limit] + + elif start is not None: + rows = rows[start:] + page = ids[start:] + + else: + page = ids + + return rows, page + + # ------------------------------------------------------------------------- + @staticmethod + def rfield_tables(rfield): + """ + Get the names of all tables that need to be joined for a field + + @param rfield: the field (S3ResourceField) + + @return: a set of tablenames + """ + + left = rfield.left + if left: + # => add all left joins required for that table + tablenames = set(j.first._tablename + for tn in left for j in left[tn]) + else: + # => we don't know any further left joins, + # but as a minimum we need to add this table + tablenames = {rfield.tname} + + return tablenames + + # ------------------------------------------------------------------------- + @staticmethod + def resolve_expression(expr): + """ + Resolve an orderby or groupby expression into its items + + @param expr: the orderby/groupby expression + """ + + if isinstance(expr, str): + items = expr.split(",") + elif not isinstance(expr, (list, tuple)): + items = [expr] + else: + items = expr + return items + +# END ========================================================================= diff --git a/modules/core/io/xml.py b/modules/core/resource/xml.py similarity index 99% rename from modules/core/io/xml.py rename to modules/core/resource/xml.py index 137afc6d1d..81a4ec2668 100644 --- a/modules/core/io/xml.py +++ b/modules/core/resource/xml.py @@ -37,15 +37,10 @@ import re import sys +from lxml import etree from urllib import parse as urlparse from urllib.request import urlopen -try: - from lxml import etree -except ImportError: - sys.stderr.write("ERROR: lxml module needed for XML handling\n") - raise - from gluon import current, HTTP, URL, IS_EMPTY_OR from gluon.storage import Storage diff --git a/modules/core/sync/adapters/data.py b/modules/core/sync/adapters/data.py index 4e286fc721..00972dc043 100644 --- a/modules/core/sync/adapters/data.py +++ b/modules/core/sync/adapters/data.py @@ -31,15 +31,10 @@ import json import sys +from lxml import etree from urllib.error import HTTPError, URLError from urllib.parse import quote as urllib_quote -try: - from lxml import etree -except ImportError: - sys.stderr.write("ERROR: lxml module needed for XML handling\n") - raise - from gluon import current from ...tools import s3_encode_iso_datetime, JSONERRORS @@ -318,10 +313,10 @@ def _import(self, update): response = update["response"] try: - resource.import_xml(response, - ignore_errors = True, - strategy = strategy, - ) + import_result = resource.import_xml(response, + ignore_errors = True, + strategy = strategy, + ) except IOError: result = log.FATAL error = "%s" % sys.exc_info()[1] @@ -333,13 +328,13 @@ def _import(self, update): traceback.format_exc() else: - if resource.error: + if import_result.error: result = log.ERROR - error = resource.error + error = import_result.error else: result = log.SUCCESS - update["count"] = resource.import_count - update["mtime"] = resource.mtime + update["count"] = import_result.count + update["mtime"] = import_result.mtime update["result"] = result diff --git a/modules/core/sync/adapters/eden.py b/modules/core/sync/adapters/eden.py index 815af4070a..e94724c01e 100644 --- a/modules/core/sync/adapters/eden.py +++ b/modules/core/sync/adapters/eden.py @@ -32,18 +32,14 @@ import sys import traceback +from lxml import etree from urllib import request as urllib2 from urllib.error import HTTPError, URLError from urllib.parse import quote as urllib_quote -try: - from lxml import etree -except ImportError: - sys.stderr.write("ERROR: lxml module needed for XML handling\n") - raise - from gluon import current +from ...resource import SyncPolicy from ...tools import s3_encode_iso_datetime, JSONERRORS from ..base import S3SyncBaseAdapter, S3SyncDataArchive @@ -220,7 +216,7 @@ def pull(self, task, onconflict=None): # Construct the URL url = "%s/sync/sync.xml?resource=%s&repository=%s" % \ (repository.url, resource_name, config.uuid) - if last_pull and task.update_policy not in ("THIS", "OTHER"): + if last_pull and task.update_policy not in (SyncPolicy.THIS, SyncPolicy.OTHER): url += "&msince=%s" % s3_encode_iso_datetime(last_pull) if task.components is False: # Allow None to remain the old default of 'Include Components' url += "&mcomponents=None" @@ -295,17 +291,10 @@ def pull(self, task, onconflict=None): # Process the response mtime = None if response: - - # Get import strategy and update policy - strategy = task.strategy - update_policy = task.update_policy - conflict_policy = task.conflict_policy - - success = True message = "" action = "import" - # Import the data + # Sync Policy if onconflict: onconflict_callback = lambda item: onconflict(item, repository, @@ -313,17 +302,21 @@ def pull(self, task, onconflict=None): ) else: onconflict_callback = None + sync_policy = SyncPolicy(onupdate = task.update_policy, + onconflict = task.conflict_policy, + resolve = onconflict_callback, + last_sync = last_pull, + ) + + # Import the data count = 0 try: - success = resource.import_xml(response, - ignore_errors = True, - strategy = strategy, - update_policy = update_policy, - conflict_policy = conflict_policy, - last_sync = last_pull, - onconflict = onconflict_callback, - ) - count = resource.import_count + import_result = resource.import_xml(response, + ignore_errors = True, + strategy = task.strategy, + sync_policy = sync_policy, + ) + count = import_result.count except IOError as e: result = log.FATAL @@ -341,13 +334,13 @@ def pull(self, task, onconflict=None): traceback.format_exc() output = xml.json_message(False, 500, sys.exc_info()[1]) - mtime = resource.mtime + mtime = import_result.mtime # Log all validation errors - if resource.error_tree is not None: + if import_result.error_tree is not None: result = log.WARNING - message = "%s" % resource.error - for element in resource.error_tree.findall("resource"): + message = "%s" % import_result.error + for element in import_result.error_tree.findall("resource"): for field in element.findall("data[@error]"): error_msg = field.get("error", None) if error_msg: @@ -360,10 +353,10 @@ def pull(self, task, onconflict=None): message = "%s, %s" % (message, msg) # Check for failure - if not success: + if not import_result.success: result = log.FATAL if not message: - message = "%s" % resource.error + message = "%s" % import_result.error output = xml.json_message(False, 400, message) mtime = None @@ -433,7 +426,7 @@ def push(self, task): if conflict_policy: url += "&conflict_policy=%s" % conflict_policy last_push = task.last_push - if last_push and update_policy not in ("THIS", "OTHER"): + if last_push and update_policy not in (SyncPolicy.THIS, SyncPolicy.OTHER): url += "&msince=%s" % s3_encode_iso_datetime(last_push) else: last_push = None @@ -527,12 +520,13 @@ def push(self, task): # ------------------------------------------------------------------------- def send(self, resource, - start=None, - limit=None, - msince=None, - filters=None, - mixed=False, - pretty_print=False): + start = None, + limit = None, + msince = None, + filters = None, + mixed = False, + pretty_print = False, + ): """ Respond to an incoming pull from the peer repository @@ -586,12 +580,13 @@ def send(self, def receive(self, source, resource, - strategy=None, - update_policy=None, - conflict_policy=None, - onconflict=None, - last_sync=None, - mixed=False): + strategy = None, + update_policy = None, + conflict_policy = None, + onconflict = None, + last_sync = None, + mixed = False, + ): """ Respond to an incoming push from the peer repository @@ -625,6 +620,7 @@ def receive(self, # - have a repository setting to enforce strict validation? ignore_errors = True + # Sync Policy if onconflict: onconflict_callback = lambda item: onconflict(item, repository, @@ -632,28 +628,30 @@ def receive(self, ) else: onconflict_callback = None + sync_policy = SyncPolicy(onupdate = update_policy, + onconflict = conflict_policy, + resolve = onconflict_callback, + last_sync = last_sync, + ) - output = resource.import_xml(source, - format = "xml", - ignore_errors = ignore_errors, - strategy = strategy, - update_policy = update_policy, - conflict_policy = conflict_policy, - last_sync = last_sync, - onconflict = onconflict_callback, - ) + import_result = resource.import_xml(source, + source_type = "xml", + ignore_errors = ignore_errors, + strategy = strategy, + sync_policy = sync_policy, + ) log = self.log - if resource.error_tree is not None: + if import_result.error_tree is not None: # Validation error (log in any case) if ignore_errors: result = log.WARNING else: result = log.FATAL remote = True - message = "%s" % resource.error - for element in resource.error_tree.findall("resource"): + message = "%s" % import_result.error + for element in import_result.error_tree.findall("resource"): error_msg = element.get("error", "unknown error") @@ -689,7 +687,7 @@ def receive(self, return {"status": result, "remote": remote, "message": message, - "response": output, + "response": import_result.json_message(), } # ------------------------------------------------------------------------- @@ -941,8 +939,9 @@ def _http_opener(self, url, headers=None, auth=True): # required (i.e. no 401 triggered), but we want to login in # any case: import base64 - base64string = base64.encodestring('%s:%s' % (username, password))[:-1] - addheaders.append(("Authorization", "Basic %s" % base64string)) + credentials = "%s:%s" % (username, password) + encoded = base64.b64encode(credentials.encode("utf-8")) + addheaders.append(("Authorization", "Basic %s" % encoded.decode("utf-8"))) if addheaders: opener.addheaders = addheaders diff --git a/modules/core/sync/adapters/filesync.py b/modules/core/sync/adapters/filesync.py index 4fe6df8a65..9b746c3a6d 100644 --- a/modules/core/sync/adapters/filesync.py +++ b/modules/core/sync/adapters/filesync.py @@ -134,15 +134,15 @@ def pull(self, task, onconflict=None): return (error, None) # Set strategy and policies - from ...methods import S3ImportItem + from ...resource import SyncPolicy strategy = task.strategy conflict_policy = task.conflict_policy if not conflict_policy: - conflict_policy = S3ImportItem.POLICY.MASTER + conflict_policy = SyncPolicy.MASTER update_policy = task.update_policy if not update_policy: - update_policy = S3ImportItem.POLICY.NEWER - if update_policy not in ("THIS", "OTHER"): + update_policy = SyncPolicy.NEWER + if update_policy not in (SyncPolicy.THIS, SyncPolicy.OTHER): last_sync = task.last_pull else: last_sync = None @@ -277,11 +277,11 @@ def push(self, task): return (error, None) # Update policy and msince - from ...methods import S3ImportItem + from ...resource import SyncPolicy update_policy = task.update_policy if not update_policy: - update_policy = S3ImportItem.POLICY.NEWER - if update_policy not in ("THIS", "OTHER"): + update_policy = SyncPolicy.NEWER + if update_policy not in (SyncPolicy.THIS, SyncPolicy.OTHER): msince = task.last_push else: msince = None diff --git a/modules/core/sync/adapters/ftp.py b/modules/core/sync/adapters/ftp.py index de9946ca70..29c7a2aeac 100644 --- a/modules/core/sync/adapters/ftp.py +++ b/modules/core/sync/adapters/ftp.py @@ -33,9 +33,8 @@ from gluon import * -from ...controller import S3Request -from ...filters import S3URLQuery, FS -from ...io import S3Exporter +from ...controller import CRUDRequest +from ...resource import S3URLQuery, FS, S3Exporter from ..base import S3SyncBaseAdapter @@ -267,10 +266,10 @@ def push(self, task): def _get_data(self, resource, representation): """ Returns the representation data for the resource """ - request = S3Request(prefix = resource.prefix, - name = resource.name, - extension = representation, - ) + request = CRUDRequest(prefix = resource.prefix, + name = resource.name, + extension = representation, + ) if request.transformable(): return resource.export_xml(stylesheet = request.stylesheet(), diff --git a/modules/core/sync/base.py b/modules/core/sync/base.py index bb8ca69e62..fb89fa4793 100644 --- a/modules/core/sync/base.py +++ b/modules/core/sync/base.py @@ -36,8 +36,8 @@ from gluon import current, URL, DIV from gluon.storage import Storage -from ..filters import S3URLQuery -from ..methods import S3Method +from ..resource import S3URLQuery, SyncPolicy +from ..methods import S3Method, S3CRUD from ..tools import s3_parse_datetime, s3_utc, s3_str # ============================================================================= @@ -45,7 +45,6 @@ class S3Sync(S3Method): """ Synchronization Handler """ def __init__(self): - """ Constructor """ S3Method.__init__(self) @@ -61,9 +60,9 @@ def apply_method(self, r, **attr): - POST sync/repository/register.json - remote registration NB incoming pull/push reponse normally by local sync/sync - controller as resource proxy => back-end generated S3Request + controller as resource proxy => back-end generated CRUDRequest - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller parameters for the request """ @@ -107,7 +106,7 @@ def __register(self, r, **attr): """ Respond to an incoming registration request - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller parameters for the request """ @@ -206,7 +205,7 @@ def __send(self, r, **attr): """ Respond to an incoming pull - @param r: the S3Request + @param r: the CRUDRequest @param attr: the controller attributes """ @@ -309,11 +308,11 @@ def __receive(self, r, **attr): """ Respond to an incoming push - @param r: the S3Request + @param r: the CRUDRequest @param attr: the controller attributes """ - from ..methods import S3ImportItem + from ..resource import ImportItem mixed = attr.get("mixed", False) get_vars = r.get_vars @@ -343,8 +342,8 @@ def __receive(self, r, **attr): )) # Get strategy and policy - default_update_policy = S3ImportItem.POLICY.NEWER - default_conflict_policy = S3ImportItem.POLICY.MASTER + default_update_policy = SyncPolicy.NEWER + default_conflict_policy = SyncPolicy.MASTER # Identify the synchronization task ttable = s3db.sync_task @@ -361,32 +360,27 @@ def __receive(self, r, **attr): strategy = task.strategy update_policy = task.update_policy or default_update_policy conflict_policy = task.conflict_policy or default_conflict_policy - if update_policy not in ("THIS", "OTHER"): + if update_policy not in (SyncPolicy.THIS, SyncPolicy.OTHER): last_sync = task.last_pull else: - policies = S3ImportItem.POLICY + policies = {SyncPolicy.THIS: SyncPolicy.OTHER, + SyncPolicy.OTHER: SyncPolicy.THIS, + SyncPolicy.NEWER: SyncPolicy.NEWER, + SyncPolicy.MASTER: SyncPolicy.MASTER, + } p = get_vars.get("update_policy", None) - values = {"THIS": "OTHER", "OTHER": "THIS"} - switch = lambda p: p in values and values[p] or p - if p and p in policies: - p = switch(p) - update_policy = policies[p] - else: - update_policy = default_update_policy + update_policy = policies.get(p) if p else default_update_policy p = get_vars.get("conflict_policy", None) - if p and p in policies: - p = switch(p) - conflict_policy = policies[p] - else: - conflict_policy = default_conflict_policy + conflict_policy = policies.get(p) if p else default_conflict_policy + msince = get_vars.get("msince", None) if msince is not None: last_sync = s3_parse_datetime(msince) s = get_vars.get("strategy", None) if s: s = str(s).split(",") - methods = S3ImportItem.METHOD + methods = ImportItem.METHOD strategy = [method for method in methods.values() if method in s] else: @@ -553,8 +547,6 @@ def onconflict(cls, item, repository, resource): @param resource: the resource the item shall be imported to """ - from ..methods import S3ImportItem - s3db = current.s3db debug = current.log.debug @@ -576,7 +568,6 @@ def onconflict(cls, item, repository, resource): else: debug("Applying default rule") ttable = s3db.sync_task - policies = S3ImportItem.POLICY query = (ttable.repository_id == repository.id) & \ (ttable.resource_name == tablename) & \ (ttable.deleted == False) @@ -584,11 +575,11 @@ def onconflict(cls, item, repository, resource): if task and item.original: original = item.original conflict_policy = task.conflict_policy - if conflict_policy == policies.OTHER: + if conflict_policy == SyncPolicy.OTHER: # Always accept debug("Accept by default") item.conflict = False - elif conflict_policy == policies.NEWER: + elif conflict_policy == SyncPolicy.NEWER: # Accept if newer xml = current.xml if xml.MTIME in original and \ @@ -597,7 +588,7 @@ def onconflict(cls, item, repository, resource): item.conflict = False else: debug("Do not accept") - elif conflict_policy == policies.MASTER: + elif conflict_policy == SyncPolicy.MASTER: # Accept if master if current.xml.MCI in original and \ original.mci == 0 or item.mci == 1: @@ -840,7 +831,7 @@ def apply_method(self, r, **attr): """ RESTful method handler - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes for the request """ @@ -848,7 +839,7 @@ def apply_method(self, r, **attr): resource = r.resource if resource.tablename == self.TABLENAME: - return resource.crud.select(r, **attr) + return S3CRUD().select(r, **attr) elif resource.tablename == "sync_repository": # READ for sync log for this repository (currently not needed) @@ -950,9 +941,7 @@ class S3SyncRepository(object): def __init__(self, repository): """ - Constructor - - @param repository: the repository record (Row) + :param Row repository: the repository record """ # Logger and Config @@ -1044,9 +1033,7 @@ class S3SyncBaseAdapter(object): def __init__(self, repository): """ - Constructor - - @param repository: the repository (S3Repository instance) + :param S3Repository repository: the repository """ self.repository = repository diff --git a/modules/core/tools/hierarchy.py b/modules/core/tools/hierarchy.py index bde1dff85f..bbfc57dfd8 100644 --- a/modules/core/tools/hierarchy.py +++ b/modules/core/tools/hierarchy.py @@ -630,7 +630,7 @@ def delete(self, node_ids, cascade=False): total += result # Delete node - from ..filters import FS + from ..resource import FS query = (FS(self.pkey.name) == node_id) resource = current.s3db.resource(tablename, filter=query) success = resource.delete(cascade=True) diff --git a/modules/core/tools/translate.py b/modules/core/tools/translate.py index bd77d942f6..b5dd59cfad 100644 --- a/modules/core/tools/translate.py +++ b/modules/core/tools/translate.py @@ -825,7 +825,7 @@ def get_database_strings(all_template_flag): which are to be considered for translation. """ - from ..methods import S3BulkImporter + from ..resource import S3BulkImporter diff --git a/modules/core/tools/utils.py b/modules/core/tools/utils.py index cb690fe06b..f4673a43cf 100644 --- a/modules/core/tools/utils.py +++ b/modules/core/tools/utils.py @@ -695,7 +695,7 @@ def s3_get_extension(request=None): """ Get the file extension in the path of the request - @param request: the request object (web2py request or S3Request), + @param request: the request object (web2py request or CRUDRequest), defaults to current.request """ diff --git a/modules/core/ui/dashboard.py b/modules/core/ui/dashboard.py index 11cd270471..79e4af6e28 100644 --- a/modules/core/ui/dashboard.py +++ b/modules/core/ui/dashboard.py @@ -352,7 +352,7 @@ def save(self, context, update=None): configs.append(widget) # Generate a new version key - version = uuid4().get_hex() + version = uuid4().hex config_id = self.config_id if not config_id: diff --git a/modules/core/ui/datalist.py b/modules/core/ui/datalist.py index 37647a5a17..3dca3e58ad 100644 --- a/modules/core/ui/datalist.py +++ b/modules/core/ui/datalist.py @@ -150,8 +150,8 @@ def html(self, ] if empty is None: - empty = resource.crud.crud_string(resource.tablename, - "msg_no_match") + from ..methods import S3Method + empty = S3Method.crud_string(resource.tablename, "msg_no_match") empty = DIV(empty, _class="dl-empty") if self.total > 0: empty.update(_style="display:none") diff --git a/modules/core/ui/forms.py b/modules/core/ui/forms.py index c813f0be3d..239ca2cc65 100644 --- a/modules/core/ui/forms.py +++ b/modules/core/ui/forms.py @@ -141,7 +141,7 @@ def __call__(self, """ Render/process the form. To be implemented in subclass. - @param request: the S3Request + @param request: the CRUDRequest @param resource: the target S3Resource @param record_id: the record ID @param readonly: render the form read-only @@ -454,7 +454,7 @@ def __call__(self, """ Render/process the form. - @param request: the S3Request + @param request: the CRUDRequest @param resource: the target S3Resource @param record_id: the record ID @param readonly: render the form read-only @@ -836,7 +836,7 @@ def __call__(self, """ Render/process the form. - @param request: the S3Request + @param request: the CRUDRequest @param resource: the target S3Resource @param record_id: the record ID @param readonly: render the form read-only @@ -896,15 +896,15 @@ def __call__(self, # Customise subtables if subtables: if not request: - # Create dummy S3Request - from ..controller import S3Request - r = S3Request(resource.prefix, - resource.name, - # Current request args/vars could be in a different - # resource context, so must override them here: - args = [], - get_vars = {}, - ) + # Create dummy CRUDRequest + from ..controller import CRUDRequest + r = CRUDRequest(resource.prefix, + resource.name, + # Current request args/vars could be in a different + # resource context, so must override them here: + args = [], + get_vars = {}, + ) else: r = request @@ -1761,7 +1761,7 @@ def resolve(self, resource): """ # Import S3ResourceField only here, to avoid circular dependency - from ..filters import S3ResourceField + from ..resource import S3ResourceField rfield = S3ResourceField(resource, self.selector) @@ -3896,14 +3896,14 @@ def extract(self, resource, record_id): component, link = self.get_link() # Customise resources - from ..controller import S3Request - r = S3Request(resource.prefix, - resource.name, - # Current request args/vars could be in a different - # resource context, so must override them here: - args = [], - get_vars = {}, - ) + from ..controller import CRUDRequest + r = CRUDRequest(resource.prefix, + resource.name, + # Current request args/vars could be in a different + # resource context, so must override them here: + args = [], + get_vars = {}, + ) customise_resource = current.deployment_settings.customise_resource for tablename in (component.tablename, link.tablename): customise = customise_resource(tablename) @@ -4093,7 +4093,7 @@ def accept(self, form, master_id=None, format=None): @todo: implement audit """ - from ..filters import FS + from ..resource import FS s3db = current.s3db @@ -4232,7 +4232,7 @@ def get_options(self): @return: dict {value: representation} of options """ - from ..filters import FS + from ..resource import FS resource = self.resource component, link = self.get_link() diff --git a/modules/core/ui/navigation.py b/modules/core/ui/navigation.py index e04e471bab..8bbd6906bf 100644 --- a/modules/core/ui/navigation.py +++ b/modules/core/ui/navigation.py @@ -1345,7 +1345,7 @@ def s3_rheader_resource(r): """ Identify the tablename and record ID for the rheader - @param r: the current S3Request + @param r: the current CRUDRequest """ @@ -1402,7 +1402,7 @@ def render(self, r): """ Render the tabs row - @param r: the S3Request + @param r: the CRUDRequest """ rheader_tabs = [] @@ -1594,7 +1594,7 @@ def __init__(self, tab): """ # @todo: use component hook label/plural as fallback for title - # (see S3Model.add_components) + # (see DataModel.add_components) title, component = tab[:2] # 'component' can be method self.title = title @@ -1634,13 +1634,12 @@ def active(self, r): """ Check whether the this tab is active - @param r: the S3Request + @param r: the CRUDRequest """ s3db = current.s3db get_components = s3db.get_components - get_method = s3db.get_method get_vars = r.get_vars tablename = None if "viewing" in get_vars: @@ -1653,6 +1652,7 @@ def active(self, r): component = self.component function = self.function if component: + # Check if component alias clist = get_components(resource.table, names=[component]) is_component = False if component in clist: @@ -1663,15 +1663,14 @@ def active(self, r): is_component = True if is_component: return self.authorised(clist[component]) - handler = get_method(resource.prefix, - resource.name, - method=component) + + # Check if URL method + get_method = s3db.get_method + handler = get_method(resource.tablename, method=component) if handler is None and tablename: - prefix, name = tablename.split("_", 1) - handler = get_method(prefix, name, - method=component) + handler = get_method(tablename, method=component) if handler is None: - handler = r.get_handler(component) + handler = r.default_methods.get(component) if handler is None: return component in ("create", "read", "update", "delete") @@ -1709,7 +1708,7 @@ def vars_match(self, r): Check whether the request GET vars match the GET vars in the URL of this tab - @param r: the S3Request + @param r: the CRUDRequest """ if self.vars is None: @@ -1820,7 +1819,7 @@ def __call__(self, r, tabs=None, table=None, record=None, actions=None, as_div=T """ Return the HTML representation of this rheader - @param r: the S3Request instance to render the header for + @param r: the CRUDRequest instance to render the header for @param tabs: the tabs (overrides the original tabs definition) @param table: override r.table @param record: override r.record diff --git a/modules/core/ui/widgets.py b/modules/core/ui/widgets.py index 0112713782..cb5c04f1c8 100644 --- a/modules/core/ui/widgets.py +++ b/modules/core/ui/widgets.py @@ -3533,7 +3533,7 @@ def link_filter_query(table, expression): if "deleted" in linktable: fq &= (linktable.deleted == False) linked = current.db(fq).select(table._id) - from ..filters import FS + from ..resource import FS pkey = FS("id") exclude = (~(pkey.belongs([r[table._id.name] for r in linked]))) return exclude @@ -9015,7 +9015,7 @@ def search_ac(r, **attr): """ JSON search method for S3AutocompleteWidget - @param r: the S3Request + @param r: the CRUDRequest @param attr: request attributes """ @@ -9038,7 +9038,7 @@ def search_ac(r, **attr): limit = int(_vars.limit or 0) - from ..filters import FS + from ..resource import FS field = FS(fieldname) # Default fields to return diff --git a/modules/s3db/assess.py b/modules/s3db/assess.py index 6f41c2a38b..19d5531377 100644 --- a/modules/s3db/assess.py +++ b/modules/s3db/assess.py @@ -49,7 +49,7 @@ } # ============================================================================= -class S3Assess24HModel(S3Model): +class S3Assess24HModel(DataModel): """ IFRC 24H Assessment form """ @@ -127,10 +127,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class S3AssessBuildingModel(S3Model): +class S3AssessBuildingModel(DataModel): """ Building Damage Assessment form """ @@ -785,7 +785,7 @@ def model(self): ) # Generate Work Order - self.set_method("assess", "building", + self.set_method("assess_building", method="form", action=self.assess_building_form) @@ -1006,7 +1006,7 @@ def assess_building_form(r, **attr): ) # ============================================================================= -class S3AssessCanvassModel(S3Model): +class S3AssessCanvassModel(DataModel): """ Building Canvassing form """ @@ -1088,10 +1088,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class S3AssessNeedsModel(S3Model): +class S3AssessNeedsModel(DataModel): """ Needs Assessment form - based on Iraqi Red Crescent requirements @@ -1231,7 +1231,7 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= def assess_multi_type_represent(ids, opts): diff --git a/modules/s3db/asset.py b/modules/s3db/asset.py index 3f09e91eda..3e644386a7 100644 --- a/modules/s3db/asset.py +++ b/modules/s3db/asset.py @@ -80,7 +80,7 @@ } # ============================================================================= -class S3AssetModel(S3Model): +class S3AssetModel(DataModel): """ Asset Management """ @@ -832,7 +832,7 @@ def asset_log_onaccept(form): db(atable.id == asset_id).update(cond = form_vars.cond) # ============================================================================= -class S3AssetHRModel(S3Model): +class S3AssetHRModel(DataModel): """ Optionally link Assets to Human Resources - useful for staffing a vehicle @@ -859,10 +859,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class S3AssetTeamModel(S3Model): +class S3AssetTeamModel(DataModel): """ Optionally link Assets to Teams """ @@ -888,10 +888,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class S3AssetTelephoneModel(S3Model): +class S3AssetTelephoneModel(DataModel): """ Extend the Assset Module for Telephones: Usage Costs @@ -953,7 +953,7 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= def asset_get_current_log(asset_id): @@ -1249,7 +1249,7 @@ def prep(r): s3.prep = prep # Import pre-process - def import_prep(data): + def import_prep(tree): """ Flag that this is an Import (to distinguish from Sync) @ToDo: Find Person records from their email addresses @@ -1261,7 +1261,6 @@ def import_prep(data): ctable = s3db.pr_contact ptable = s3db.pr_person - resource, tree = data elements = tree.getroot().xpath("/s3xml//resource[@name='pr_person']/data[@field='first_name']") persons = {} for element in elements: @@ -1305,10 +1304,9 @@ def postp(r, output): return output s3.postp = postp - output = current.rest_controller("asset", "asset", - rheader = asset_rheader, - ) - return output + return current.crud_controller("asset", "asset", + rheader = asset_rheader, + ) # ============================================================================= class asset_AssetRepresent(S3Represent): diff --git a/modules/s3db/auth.py b/modules/s3db/auth.py index a0e3f9b143..6b2dfd34b0 100644 --- a/modules/s3db/auth.py +++ b/modules/s3db/auth.py @@ -47,7 +47,7 @@ from ..s3layouts import S3PopupLink # ============================================================================= -class AuthDomainApproverModel(S3Model): +class AuthDomainApproverModel(DataModel): names = ("auth_organisation",) @@ -106,11 +106,11 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class AuthUserOptionsModel(S3Model): +class AuthUserOptionsModel(DataModel): """ Model to store per-user configuration options """ names = ("auth_user_options",) @@ -150,10 +150,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class AuthConsentModel(S3Model): +class AuthConsentModel(DataModel): """ Model to track consent, e.g. to legitimise processing of personal data under GDPR rules. @@ -495,7 +495,7 @@ def consent_onaccept(form): db(query).update(expires_on = today) # ============================================================================= -class AuthMasterKeyModel(S3Model): +class AuthMasterKeyModel(DataModel): """ Model to store Master Keys - used for Authentication from Mobile App to e.g. Surveys @@ -1318,7 +1318,7 @@ def auth_user_options_get_osm(pe_id): return None # ============================================================================= -class AuthUserTempModel(S3Model): +class AuthUserTempModel(DataModel): """ Model to store complementary data for pending user accounts after self-registration @@ -1356,7 +1356,7 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= diff --git a/modules/s3db/br.py b/modules/s3db/br.py index 55cbd8d3a1..4a0da276a7 100644 --- a/modules/s3db/br.py +++ b/modules/s3db/br.py @@ -72,7 +72,7 @@ CASE_GROUP = 7 # ============================================================================= -class BRCaseModel(S3Model): +class BRCaseModel(DataModel): """ Model to register cases ("case registry") and track their processing status; uses pr_person for beneficiary person data @@ -300,14 +300,14 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ------------------------------------------------------------------------- @staticmethod def defaults(): """ Safe defaults for names in case the module is disabled """ - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -468,7 +468,7 @@ def case_onaccept(form, create=False): # ============================================================================= # Process Models # ============================================================================= -class BRCaseActivityModel(S3Model): +class BRCaseActivityModel(DataModel): """ Model for problem/needs-oriented case processing: activities taking place in response to individual needs of the beneficiary @@ -994,7 +994,7 @@ def case_activity_onaccept(form): data["end_date"] = None # ============================================================================= -class BRAppointmentModel(S3Model): +class BRAppointmentModel(DataModel): """ Model for workflow-oriented case processing: cases must pass a number of predefined processing steps (=appointments) in order to achieve a @@ -1139,19 +1139,19 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ------------------------------------------------------------------------- @staticmethod def defaults(): """ Safe defaults for names in case the module is disabled """ - return {} + return None # ============================================================================= # Category Models # ============================================================================= -class BRNeedsModel(S3Model): +class BRNeedsModel(DataModel): """ Model for Need Categories """ names = ("br_need", @@ -1275,7 +1275,7 @@ def defaults(): # ============================================================================= # Assistance Models # ============================================================================= -class BRAssistanceModel(S3Model): +class BRAssistanceModel(DataModel): """ Generic model to track individual measures of assistance """ @@ -2093,7 +2093,7 @@ def assistance_inline_component(): ) # ============================================================================= -class BRAssistanceOfferModel(S3Model): +class BRAssistanceOfferModel(DataModel): """ Generic model to track individual measures of assistance """ @@ -2461,14 +2461,14 @@ def direct_offer_create_onvalidation(form): form.errors[error_field] = error # ============================================================================= -class BRDistributionModel(S3Model): +class BRDistributionModel(DataModel): """ Model to process+track relief item distributions to beneficiaries """ pass # ============================================================================= -class BRPaymentModel(S3Model): +class BRPaymentModel(DataModel): """ Model to process+track benefits payments to beneficiaries """ @@ -2477,7 +2477,7 @@ class BRPaymentModel(S3Model): # ============================================================================= # Tracking Models # ============================================================================= -class BRCaseEventModel(S3Model): +class BRCaseEventModel(DataModel): """ Model for checkpoint-style tracking of beneficiaries """ @@ -2486,7 +2486,7 @@ class BRCaseEventModel(S3Model): # ============================================================================= # Case Documentation Models # ============================================================================= -class BRLanguageModel(S3Model): +class BRLanguageModel(DataModel): """ Model to document language options for communication with a beneficiary @@ -2537,21 +2537,21 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ------------------------------------------------------------------------- @staticmethod def defaults(): """ Safe defaults for names in case the module is disabled """ - return {} + return None # ============================================================================= -class BRLegalStatusModel(S3Model): +class BRLegalStatusModel(DataModel): pass # ============================================================================= -class BRServiceContactModel(S3Model): +class BRServiceContactModel(DataModel): """ Model to track external service contacts of beneficiaries """ names = ("br_service_contact", @@ -2680,17 +2680,17 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ------------------------------------------------------------------------- @staticmethod def defaults(): """ Safe defaults for names in case the module is disabled """ - return {} + return None # ============================================================================= -class BRNotesModel(S3Model): +class BRNotesModel(DataModel): """ Simple Journal for Case Files """ names = ("br_note", @@ -2810,14 +2810,14 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class BRReferralModel(S3Model): +class BRReferralModel(DataModel): pass # ============================================================================= -class BRVulnerabilityModel(S3Model): +class BRVulnerabilityModel(DataModel): pass # ============================================================================= @@ -3989,7 +3989,7 @@ def br_group_membership_onaccept(membership, group, group_id, person_id): row = db(query).select(ctable.id, limitby=(0, 1)).first() if not row: # Customise case resource - r = S3Request("br", "case", current.request) + r = CRUDRequest("br", "case", current.request) r.customise_resource("br_case") # Get the default case status from database diff --git a/modules/s3db/budget.py b/modules/s3db/budget.py index 8ef4f86024..43e3926f02 100644 --- a/modules/s3db/budget.py +++ b/modules/s3db/budget.py @@ -44,7 +44,7 @@ from s3layouts import S3PopupLink # ============================================================================= -class S3BudgetModel(S3Model): +class S3BudgetModel(DataModel): names = ("budget_entity", "budget_budget", @@ -553,7 +553,7 @@ def budget_budget_staff_ondelete(row): return # ============================================================================= -class S3BudgetKitModel(S3Model): +class S3BudgetKitModel(DataModel): names = ("budget_kit", "budget_item", @@ -922,7 +922,7 @@ def budget_kit_item_ondelete(row): return # ============================================================================= -class S3BudgetBundleModel(S3Model): +class S3BudgetBundleModel(DataModel): """ Model for Budget Bundles """ names = ("budget_bundle", @@ -1308,7 +1308,7 @@ def budget_budget_bundle_ondelete(row): return # ============================================================================= -class S3BudgetAllocationModel(S3Model): +class S3BudgetAllocationModel(DataModel): """ Model for Budget Allocation """ @@ -1402,7 +1402,7 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ------------------------------------------------------------------------- def defaults(self): @@ -1410,7 +1410,7 @@ def defaults(self): Safe defaults for model-global names in case module is disabled """ - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -1446,7 +1446,7 @@ def budget_allocation_duplicate(item): return # ============================================================================= -class S3BudgetMonitoringModel(S3Model): +class S3BudgetMonitoringModel(DataModel): """ Budget Monitoring Model @@ -1528,7 +1528,7 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ------------------------------------------------------------------------- @staticmethod diff --git a/modules/s3db/cap.py b/modules/s3db/cap.py index c1c430c8ba..a9b656c42a 100644 --- a/modules/s3db/cap.py +++ b/modules/s3db/cap.py @@ -310,7 +310,7 @@ def get_cap_options(): return cap_options # ============================================================================= -class CAPAlertModel(S3Model): +class CAPAlertModel(DataModel): """ Model for the cap:alert container object """ names = ("cap_alert", @@ -329,8 +329,6 @@ def model(self): crud_strings = current.response.s3.crud_strings define_table = self.define_table - set_method = self.set_method - cap_options = get_cap_options() incident_types = cap_options["incident_types"] @@ -588,17 +586,18 @@ def model(self): ) # Resource-specific REST Methods - set_method("cap", "alert", + set_method = self.set_method + set_method("cap_alert", method = "import_feed", action = cap_ImportAlert, ) - set_method("cap", "alert", + set_method("cap_alert", method = "assign", action = cap_AssignArea, ) - set_method("cap", "alert", + set_method("cap_alert", method = "clone", action = cap_CloneAlert, ) @@ -1009,7 +1008,7 @@ def template_represent(alert_id, row=None): return repr_str # ============================================================================= -class CAPInfoModel(S3Model): +class CAPInfoModel(DataModel): names = ("cap_info", "cap_info_id", @@ -1416,7 +1415,7 @@ def defaults(): Return safe defaults in case the model has been deactivated. """ - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -1596,7 +1595,7 @@ def info_duplicate(item): """ Duplicate detection for info-segments - @param item: the S3ImportItem + @param item: the ImportItem """ data = item.data @@ -1707,7 +1706,7 @@ def sanitize_json(string): return "[%s]" % sanitized # ============================================================================= -class CAPAreaModel(S3Model): +class CAPAreaModel(DataModel): names = ("cap_area", "cap_area_represent", @@ -2131,7 +2130,7 @@ def area_duplicate(item): """ Detect an area duplicate - @param item: the S3ImportItem + @param item: the ImportItem """ data = item.data @@ -2302,7 +2301,7 @@ def area_tag_onaccept(form): #s3db.onaccept(ltable, link) # ============================================================================= -class CAPResourceModel(S3Model): +class CAPResourceModel(DataModel): names = ("cap_resource", ) @@ -2482,7 +2481,7 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -2491,7 +2490,7 @@ def defaults(): Return safe defaults in case the model has been deactivated. """ - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -2562,7 +2561,7 @@ def resource_onaccept(form): db(db.cap_resource.id == form_vars.id).update(alert_id=alert_id) # ============================================================================= -class CAPWarningPriorityModel(S3Model): +class CAPWarningPriorityModel(DataModel): names = ("cap_warning_priority", "cap_warning_priority_id", @@ -2827,7 +2826,7 @@ def color_code_represent(color_code): return output # ============================================================================= -class CAPHistoryModel(S3Model): +class CAPHistoryModel(DataModel): """ TODO docstring (what is this used for?) """ names = ("cap_alert_history", @@ -3707,7 +3706,7 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -3716,10 +3715,10 @@ def defaults(): Return safe defaults in case the model has been deactivated. """ - return {} + return None # ============================================================================= -class CAPAlertingAuthorityModel(S3Model): +class CAPAlertingAuthorityModel(DataModel): """ Model for known Alerting Authorities - see http://alerting.worldweather.org @@ -3878,7 +3877,7 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -3887,10 +3886,10 @@ def defaults(): Return safe defaults in case the model has been deactivated. """ - return {} + return None # ============================================================================= -class CAPMessageModel(S3Model): +class CAPMessageModel(DataModel): """ Link Alerts to Messages """ names = ("cap_alert_message", @@ -3914,7 +3913,7 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -3923,7 +3922,7 @@ def defaults(): Return safe defaults in case the model has been deactivated. """ - return {} + return None # ============================================================================= def list_string_represent(value, fmt=lambda v: v): @@ -4600,7 +4599,7 @@ def apply_method(self, r, **attr): """ Apply method. - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller options for this request """ @@ -4791,22 +4790,22 @@ def import_cap(tree, version=None, resource=None, url=None, ignore_errors=False) if resource is None: resource = s3db.resource("cap_alert") try: - resource.import_xml(tree, - stylesheet = stylesheet, - ignore_errors = ignore_errors, - ) + result = resource.import_xml(tree, + stylesheet = stylesheet, + ignore_errors = ignore_errors, + ) except (IOError, SyntaxError): import sys error = "CAP import error: %s" % sys.exc_info()[1] else: - if resource.error: + if result.error: # Import validation error - errors = current.xml.collect_errors(resource.error_tree) - error = "%s\n%s" % (resource.error, "\n".join(errors)) + errors = current.xml.collect_errors(result.error_tree) + error = "%s\n%s" % (result.error, "\n".join(errors)) else: error = None - if resource.import_count == 0: + if result.count == 0: if not error: # No error, but nothing imported either error = "No CAP alerts found in source" @@ -4814,8 +4813,7 @@ def import_cap(tree, version=None, resource=None, url=None, ignore_errors=False) # Success error = None msg = "%s new CAP alerts imported, %s alerts updated" % ( - len(resource.import_created), - len(resource.import_updated)) + len(result.created), len(result.updated)) return error, msg @@ -4927,8 +4925,9 @@ def opener(url, # Pre-emptive basic auth if preemptive_auth and username and password: import base64 - base64string = base64.encodestring('%s:%s' % (username, password))[:-1] - addheaders.append(("Authorization", "Basic %s" % base64string)) + credentials = "%s:%s" % (username, password) + encoded = base64.b64encode(credentials.encode("utf-8")) + addheaders.append(("Authorization", "Basic %s" % encoded.decode("utf-8"))) if addheaders: opener.addheaders = addheaders @@ -4944,7 +4943,7 @@ class cap_AssignArea(S3Method): def apply_method(self, r, **attr): """ Apply method. - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller options for this request """ @@ -5108,7 +5107,7 @@ def apply_method(self, r, **attr): if filter_widgets: # Where to retrieve filtered data from: - get_vars = aresource.crud._remove_filters(r.get_vars) + get_vars = S3Method._remove_filters(r.get_vars) filter_submit_url = r.url(vars=get_vars) # Where to retrieve updated filter options from: @@ -5273,7 +5272,7 @@ def apply_method(self, r, **attr): """ Apply method - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller options for this request """ @@ -5292,7 +5291,7 @@ def clone(r, record=None, **attr): """ Clone the cap_alert - @param r: the S3Request instance + @param r: the CRUDRequest instance @param record: the record row @param attr: controller attributes """ diff --git a/modules/s3db/cms.py b/modules/s3db/cms.py index d29c7d1018..6623b5316e 100644 --- a/modules/s3db/cms.py +++ b/modules/s3db/cms.py @@ -60,7 +60,7 @@ SEPARATORS = (",", ":") # ============================================================================= -class CMSContentModel(S3Model): +class CMSContentModel(DataModel): """ Content Management System """ @@ -502,31 +502,31 @@ def body_represent(body): ) # Custom Methods - set_method("cms", "post", + set_method("cms_post", method = "add_bookmark", action = self.cms_add_bookmark) - set_method("cms", "post", + set_method("cms_post", method = "remove_bookmark", action = self.cms_remove_bookmark) - set_method("cms", "post", + set_method("cms_post", method = "add_tag", action = self.cms_add_tag) - set_method("cms", "post", + set_method("cms_post", method = "remove_tag", action = self.cms_remove_tag) - set_method("cms", "post", + set_method("cms_post", method = "share", action = self.cms_share) - set_method("cms", "post", + set_method("cms_post", method = "unshare", action = self.cms_unshare) - set_method("cms", "post", + set_method("cms_post", method = "calendar", action = cms_Calendar) @@ -603,7 +603,7 @@ def body_represent(body): ) # Custom Methods - set_method("cms", "tag", + set_method("cms_tag", method = "tag_list", action = cms_TagList) @@ -1060,7 +1060,7 @@ def cms_unshare(r, **attr): return output # ============================================================================= -class CMSContentForumModel(S3Model): +class CMSContentForumModel(DataModel): """ Link Posts to Forums to allow Users to Share posts """ @@ -1083,10 +1083,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class CMSContentMapModel(S3Model): +class CMSContentMapModel(DataModel): """ Use of the CMS to provide extra data about Map Layers """ @@ -1107,10 +1107,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class CMSContentOrgModel(S3Model): +class CMSContentOrgModel(DataModel): """ Link Posts to Organisations """ @@ -1135,10 +1135,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class CMSContentOrgGroupModel(S3Model): +class CMSContentOrgGroupModel(DataModel): """ Link Posts to Organisation Groups (Coalitions/Networks) """ @@ -1159,10 +1159,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class CMSContentTeamModel(S3Model): +class CMSContentTeamModel(DataModel): """ Link Posts to Teams """ @@ -1187,10 +1187,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class CMSContentUserModel(S3Model): +class CMSContentUserModel(DataModel): """ Link Posts to Users to allow Users to Bookmark posts """ @@ -1211,10 +1211,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class CMSContentRoleModel(S3Model): +class CMSContentRoleModel(DataModel): """ Link CMS posts to user roles - for role-specific announcements @@ -1250,7 +1250,7 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= def cms_rheader(r, tabs=None): @@ -1417,7 +1417,7 @@ def cms_documentation(r, default_page, default_url): """ Render an online documentation page, to be called from prep - @param r: the S3Request + @param r: the CRUDRequest @param default_page: the default page name @param default_url: the default URL if no contents found """ @@ -1507,10 +1507,10 @@ class S3CMS(S3Method): # ------------------------------------------------------------------------- def apply_method(self, r, **attr): """ - Entry point to apply cms method to S3Requests + Entry point to apply cms method to CRUDRequests - produces a full page with a Richtext widget - @param r: the S3Request + @param r: the CRUDRequest @param attr: dictionary of parameters for the method handler @return: output object to send to the view @@ -1526,7 +1526,7 @@ def widget(self, r, method="cms", widget_id=None, **attr): S3Summary @param method: the widget method - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller attributes @ToDo: Support comments @@ -2189,7 +2189,7 @@ def apply_method(self, r, **attr): """ Entry point for REST API - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller arguments """ @@ -2359,7 +2359,7 @@ def apply_method(self, r, **attr): """ Entry point for REST API - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller arguments """ diff --git a/modules/s3db/cr.py b/modules/s3db/cr.py index 3e130aaae4..c7f2f7f7d4 100644 --- a/modules/s3db/cr.py +++ b/modules/s3db/cr.py @@ -51,7 +51,7 @@ DAY_AND_NIGHT = 2 # ============================================================================= -class CRShelterModel(S3Model): +class CRShelterModel(DataModel): names = ("cr_shelter_type", "cr_shelter", @@ -516,25 +516,25 @@ def model(self): ) # Custom method to assign HRs - set_method("cr", "shelter", + set_method("cr_shelter", method = "assign", action = self.hrm_AssignMethod(component="human_resource_site"), ) # Check-in method - set_method("cr", "shelter", + set_method("cr_shelter", method="check-in", action = self.org_SiteCheckInMethod, ) # Notification-dispatch method - set_method("cr", "shelter", + set_method("cr_shelter", method = "dispatch", action = cr_notification_dispatcher, ) # Shelter Inspection method - set_method("cr", "shelter", + set_method("cr_shelter", method = "inspection", action = CRShelterInspection, ) @@ -1030,7 +1030,7 @@ def cr_shelter_unit_status(row): return current.messages["NONE"] # ============================================================================= -class CRShelterServiceModel(S3Model): +class CRShelterServiceModel(DataModel): """ Model for Shelter Services """ names = ("cr_shelter_service", @@ -1131,7 +1131,7 @@ def model(self): # } # ============================================================================= -class CRShelterInspectionModel(S3Model): +class CRShelterInspectionModel(DataModel): """ Model for Shelter / Housing Unit Flags """ names = ("cr_shelter_flag", @@ -1599,7 +1599,7 @@ def shelter_inspection_task_ondelete_cascade(row, tablename=None): link.update_record(task_id = None) # ============================================================================= -class CRShelterRegistrationModel(S3Model): +class CRShelterRegistrationModel(DataModel): names = ("cr_shelter_allocation", "cr_shelter_registration", @@ -1755,7 +1755,7 @@ def model(self): ) # Custom Methods - self.set_method("cr", "shelter_registration", + self.set_method("cr_shelter_registration", method = "assign", action = cr_AssignUnit()) @@ -2499,7 +2499,7 @@ def apply_method(self, r, **attr): """ Entry point for REST API - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller arguments """ @@ -2715,7 +2715,7 @@ def apply_method(self, r, **attr): """ Main entry point for REST interface. - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller parameters """ @@ -2756,7 +2756,7 @@ def inspection_form(self, r, **attr): """ Generate the form - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller parameters """ @@ -2852,7 +2852,7 @@ def inspection_ajax(self, r, **attr): """ Ajax-registration of shelter inspection - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller parameters """ diff --git a/modules/s3db/dc.py b/modules/s3db/dc.py index 61cfc04dbe..dedf481880 100644 --- a/modules/s3db/dc.py +++ b/modules/s3db/dc.py @@ -48,7 +48,7 @@ SEPARATORS = (",", ":") # ============================================================================= -class DataCollectionTemplateModel(S3Model): +class DataCollectionTemplateModel(DataModel): """ Templates to use for Assessments / Surveys - uses the Dynamic Tables back-end to store Questions @@ -977,7 +977,7 @@ def dc_question_l10n_onaccept(form): write_dict(w2pfilename, translations) # ============================================================================= -class DataCollectionModel(S3Model): +class DataCollectionModel(DataModel): """ Results of Assessments / Surveys - uses the Dynamic Tables back-end to store Answers @@ -1114,7 +1114,7 @@ def model(self): msg_record_deleted = T("Data Collection Target deleted"), msg_list_empty = T("No Data Collection Targets currently registered")) - self.set_method("dc", "target", + self.set_method("dc_target", method = "results", action = dc_TargetReport()) @@ -1543,7 +1543,7 @@ def apply_method(self, r, **attr): """ Entry point for REST API - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller arguments """ @@ -1837,7 +1837,7 @@ def pdf(self, r, title, **attr): (original is project_SummaryReport) """ - from core.io.codecs.pdf import EdenDocTemplate, S3RL_PDF + from core.resource.codecs.pdf import EdenDocTemplate, S3RL_PDF T = current.T table = r.table @@ -1888,7 +1888,7 @@ class dc_TargetXLS(S3Method): def apply_method(self, r, **attr): - from core.io.codecs.xls import S3XLS + from core.resource.codecs.xls import S3XLS try: import xlwt diff --git a/modules/s3db/deploy.py b/modules/s3db/deploy.py index 629385ab84..538387dbf0 100644 --- a/modules/s3db/deploy.py +++ b/modules/s3db/deploy.py @@ -46,7 +46,7 @@ from s3layouts import S3PopupLink # ============================================================================= -class S3DeploymentOrganisationModel(S3Model): +class S3DeploymentOrganisationModel(DataModel): """ Split into separate model to avoid circular deadlock in HRModel """ @@ -69,10 +69,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class S3DeploymentModel(S3Model): +class S3DeploymentModel(DataModel): names = ("deploy_mission", "deploy_mission_id", @@ -842,7 +842,7 @@ def deploy_assignment_appraisal_ondelete_cascade(row, tablename=None): s3db.resource("hrm_appraisal", id=link.appraisal_id).delete() # ============================================================================= -class S3DeploymentAlertModel(S3Model): +class S3DeploymentAlertModel(DataModel): names = ("deploy_alert", "deploy_alert_recipient", @@ -990,7 +990,7 @@ def model(self): ) # Custom method to send alerts - self.set_method("deploy", "alert", + self.set_method("deploy_alert", method = "send", action = self.deploy_alert_send, ) @@ -1080,7 +1080,7 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -1358,7 +1358,7 @@ def deploy_availability_filter(r): - called from prep of the respective controller - adds resource filter for r.resource - @param r: the S3Request + @param r: the CRUDRequest """ get_vars = r.get_vars @@ -1680,7 +1680,7 @@ def apply_method(self, r, **attr): Custom method for email inbox, provides a datatable with bulk-delete option - @param r: the S3Request + @param r: the CRUDRequest @param attr: the controller attributes """ @@ -2067,7 +2067,7 @@ def deploy_apply(r, **attr): if filter_widgets: # Where to retrieve filtered data from: - submit_url_vars = resource.crud._remove_filters(r.get_vars) + submit_url_vars = S3Method._remove_filters(r.get_vars) filter_submit_url = r.url(vars=submit_url_vars) # Where to retrieve updated filter options from: @@ -2294,7 +2294,7 @@ def deploy_alert_select_recipients(r, **attr): if filter_widgets: # Where to retrieve filtered data from: - _vars = resource.crud._remove_filters(r.get_vars) + _vars = S3Method._remove_filters(r.get_vars) filter_submit_url = r.url(vars=_vars) # Where to retrieve updated filter options from: @@ -2514,7 +2514,7 @@ def deploy_response_select_mission(r, **attr): if filter_widgets: # Where to retrieve filtered data from: - submit_url_vars = resource.crud._remove_filters(get_vars) + submit_url_vars = S3Method._remove_filters(get_vars) filter_submit_url = r.url(vars=submit_url_vars) # Where to retrieve updated filter options from: diff --git a/modules/s3db/disease.py b/modules/s3db/disease.py index 17041c4e25..d76872d89c 100644 --- a/modules/s3db/disease.py +++ b/modules/s3db/disease.py @@ -65,7 +65,7 @@ } # ============================================================================= -class DiseaseDataModel(S3Model): +class DiseaseDataModel(DataModel): names = ("disease_disease", "disease_disease_id", @@ -357,7 +357,7 @@ def testing_device_onaccept(form): record.update_record(available=False) # ============================================================================= -class DiseaseMonitoringModel(S3Model): +class DiseaseMonitoringModel(DataModel): """ Data Model for Disease Monitoring """ names = ("disease_demographic", @@ -823,7 +823,7 @@ def testing_demographic_ondelete(cls, row): cls.update_report_from_demographics(report_id) # ============================================================================= -class DiseaseCertificateModel(S3Model): +class DiseaseCertificateModel(DataModel): """ Model to manage disease-related health certificates """ @@ -890,7 +890,7 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -899,10 +899,10 @@ def defaults(): #dummy = S3ReusableField.dummy - return {} + return None # ============================================================================= -class CaseTrackingModel(S3Model): +class CaseTrackingModel(DataModel): names = ("disease_case", "disease_case_id", @@ -1703,7 +1703,7 @@ def represent_row(self, row): return full_name # ============================================================================= -class ContactTracingModel(S3Model): +class ContactTracingModel(DataModel): names = ("disease_tracing", "disease_exposure", @@ -1890,13 +1890,13 @@ def model(self): msg_list_empty = T("No Exposure Information currently registered")) # Pass names back to global scope (s3.*) - return {} + return None # ------------------------------------------------------------------------- @staticmethod def defaults(): - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -2066,7 +2066,7 @@ def disease_upgrade_monitoring(case_id, level, case=None): case.update_record(monitoring_level = level) # ============================================================================= -class DiseaseStatsModel(S3Model): +class DiseaseStatsModel(DataModel): """ Disease Statistics: Cases: diff --git a/modules/s3db/doc.py b/modules/s3db/doc.py index 23061dc671..06a21e997a 100644 --- a/modules/s3db/doc.py +++ b/modules/s3db/doc.py @@ -46,7 +46,7 @@ from ..core import * # ============================================================================= -class S3DocumentLibrary(S3Model): +class S3DocumentLibrary(DataModel): names = ("doc_entity", "doc_document", @@ -529,7 +529,7 @@ def document_ondelete(row): ) # ============================================================================= -class S3DocumentTagModel(S3Model): +class S3DocumentTagModel(DataModel): """ Document Tags """ @@ -569,7 +569,7 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= def doc_image_represent(filename): @@ -742,7 +742,7 @@ def link(self, k, v, row=None): return v # ============================================================================= -class S3CKEditorModel(S3Model): +class S3CKEditorModel(DataModel): """ Storage for Images used by CKEditor - and hence the s3_richtext_widget @@ -816,7 +816,7 @@ def doc_filetype(filename): return ftype # ============================================================================= -class S3DataCardModel(S3Model): +class S3DataCardModel(DataModel): """ Model to manage context-specific features of printable data cards (S3PDFCard) diff --git a/modules/s3db/dvi.py b/modules/s3db/dvi.py index 76b198fcb9..7353a79226 100644 --- a/modules/s3db/dvi.py +++ b/modules/s3db/dvi.py @@ -35,7 +35,7 @@ from s3layouts import S3PopupLink # ============================================================================= -class S3DVIModel(S3Model): +class S3DVIModel(DataModel): names = ("dvi_recreq", "dvi_body", diff --git a/modules/s3db/dvr.py b/modules/s3db/dvr.py index d4a0e76086..c4b38f495b 100644 --- a/modules/s3db/dvr.py +++ b/modules/s3db/dvr.py @@ -79,7 +79,7 @@ from s3layouts import S3PopupLink # ============================================================================= -class DVRCaseModel(S3Model): +class DVRCaseModel(DataModel): """ Model for DVR Cases @@ -870,7 +870,7 @@ def case_onaccept(form, create=False): dvr_case_household_size(row.id) # ============================================================================= -class DVRCaseFlagModel(S3Model): +class DVRCaseFlagModel(DataModel): """ Model for Case Flags """ names = ("dvr_case_flag", @@ -1076,7 +1076,7 @@ def defaults(): } # ============================================================================= -class DVRNeedsModel(S3Model): +class DVRNeedsModel(DataModel): """ Model for Needs """ names = ("dvr_need", @@ -1216,7 +1216,7 @@ def defaults(): } # ============================================================================= -class DVRNotesModel(S3Model): +class DVRNotesModel(DataModel): """ Model for Notes """ @@ -1320,10 +1320,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class DVRReferralModel(S3Model): +class DVRReferralModel(DataModel): """ Data model for case referrals (both incoming and outgoing) """ @@ -1399,7 +1399,7 @@ def defaults(): } # ============================================================================= -class DVRResponseModel(S3Model): +class DVRResponseModel(DataModel): """ Model representing responses to case needs """ names = ("dvr_response_action", @@ -1910,14 +1910,14 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ------------------------------------------------------------------------- @staticmethod def defaults(): """ Safe defaults for names in case the module is disabled """ - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -2360,7 +2360,7 @@ def response_action_theme_ondelete(row): action.update_record(response_theme_ids = theme_ids) # ============================================================================= -class DVRCaseActivityModel(S3Model): +class DVRCaseActivityModel(DataModel): """ Model for Case Activities """ names = ("dvr_activity", @@ -3502,7 +3502,7 @@ def case_activity_onaccept(cls, form): activity.update_record(end_date = None) # ============================================================================= -class DVRCaseEffortModel(S3Model): +class DVRCaseEffortModel(DataModel): """ Effort Log for Case / Case Activities """ names = ("dvr_case_effort", @@ -3573,14 +3573,14 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ------------------------------------------------------------------------- @staticmethod def defaults(): """ Safe defaults for names in case the module is disabled """ - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -3630,7 +3630,7 @@ def case_effort_onaccept(form): effort.update_record(person_id = activity.person_id) # ============================================================================= -class DVRCaseAppointmentModel(S3Model): +class DVRCaseAppointmentModel(DataModel): """ Model for Case Appointments """ names = ("dvr_case_appointment", @@ -3814,7 +3814,7 @@ def model(self): ) # Custom methods - self.set_method("dvr", "case_appointment", + self.set_method("dvr_case_appointment", method = "manage", action = DVRManageAppointments, ) @@ -3976,11 +3976,11 @@ def case_appointment_onaccept(form): for case in cases: if has_permission("update", ctable, record_id=case.id): # Customise case resource - r = S3Request("dvr", "case", - current.request, - args = [], - get_vars = {}, - ) + r = CRUDRequest("dvr", "case", + current.request, + args = [], + get_vars = {}, + ) r.customise_resource("dvr_case") # Update case status + run onaccept case.update_record(status_id = status_id) @@ -4018,7 +4018,7 @@ def case_appointment_ondelete(row): dvr_update_last_seen(person_id) # ============================================================================= -class DVRHouseholdModel(S3Model): +class DVRHouseholdModel(DataModel): """ Model to document the household situation of a case - used by STL (DRK use pr_group_membership, SCPHIMS use DVRHouseholdMemberModel) @@ -4228,7 +4228,7 @@ def defaults(): # ============================================================================= -class DVRHouseholdMembersModel(S3Model): +class DVRHouseholdMembersModel(DataModel): """ Model to document the household situation of a case - used by SCPHIMS (DRK use pr_group_membership, STL use DVRHouseholdModel) @@ -4279,10 +4279,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class DVRCaseEconomyInformationModel(S3Model): +class DVRCaseEconomyInformationModel(DataModel): """ Model for Household Economy Information """ names = ("dvr_economy", @@ -4497,17 +4497,17 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ------------------------------------------------------------------------- @staticmethod def defaults(): """ Safe defaults for names in case the module is disabled """ - return {} + return None # ============================================================================= -class DVRLegalStatusModel(S3Model): +class DVRLegalStatusModel(DataModel): """ Models to document the legal status of a beneficiary """ names = ("dvr_residence_status_type", @@ -4661,17 +4661,17 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ------------------------------------------------------------------------- @staticmethod def defaults(): """ Safe defaults for names in case the module is disabled """ - return {} + return None # ============================================================================= -class DVRCaseAllowanceModel(S3Model): +class DVRCaseAllowanceModel(DataModel): """ Model for Allowance Management """ names = ("dvr_allowance", @@ -4780,11 +4780,11 @@ def model(self): onvalidation = self.allowance_onvalidation, ) - set_method("dvr", "allowance", + set_method("dvr_allowance", method = "register", action = DVRRegisterPayment, ) - set_method("dvr", "allowance", + set_method("dvr_allowance", method = "manage", action = DVRManageAllowance, ) @@ -4888,7 +4888,7 @@ def allowance_ondelete(row): dvr_update_last_seen(person_id) # ============================================================================= -class DVRCaseEventModel(S3Model): +class DVRCaseEventModel(DataModel): """ Model representing monitoring events for cases """ names = ("dvr_case_event_type", @@ -5175,7 +5175,7 @@ def model(self): ) # Custom method for event registration - self.set_method("dvr", "case_event", + self.set_method("dvr_case_event", method = "register", action = DVRRegisterCaseEvent, ) @@ -5183,14 +5183,14 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ------------------------------------------------------------------------- @staticmethod def defaults(): """ Safe defaults for names in case the module is disabled """ - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -5368,11 +5368,11 @@ def case_event_create_onaccept(form): ) if permitted: # Customise appointment resource - r = S3Request("dvr", "case_appointment", - current.request, - args = [], - get_vars = {}, - ) + r = CRUDRequest("dvr", "case_appointment", + current.request, + args = [], + get_vars = {}, + ) r.customise_resource("dvr_case_appointment") # Update appointment success = update.update_record(**data) @@ -5414,7 +5414,7 @@ def case_event_ondelete(row): dvr_update_last_seen(person_id) # ============================================================================= -class DVRCaseEvaluationModel(S3Model): +class DVRCaseEvaluationModel(DataModel): """ Evaluation of Cases - Flexible Questions (Dynamic Data Model) @@ -5525,16 +5525,16 @@ def model(self): ) # Custom Report Method - #self.set_method("org", "capacity_assessment_data", + #self.set_method("org_capacity_assessment_data", # method = "custom_report", # action = org_CapacityReport()) # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class DVRVulnerabilityModel(S3Model): +class DVRVulnerabilityModel(DataModel): """ Targeted vulnerabilities for activities """ names = ("dvr_vulnerability_type", @@ -5668,10 +5668,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class DVRActivityFundingModel(S3Model): +class DVRActivityFundingModel(DataModel): """ Model to manage funding needs for cases """ names = ("dvr_activity_funding", @@ -5729,10 +5729,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class DVRServiceContactModel(S3Model): +class DVRServiceContactModel(DataModel): """ Model to track external service contacts of beneficiaries """ names = ("dvr_service_contact", @@ -5856,17 +5856,17 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ------------------------------------------------------------------------- @staticmethod def defaults(): """ Safe defaults for names in case the module is disabled """ - return {} + return None # ============================================================================= -class DVRSiteActivityModel(S3Model): +class DVRSiteActivityModel(DataModel): """ Model to record the activity of a site over time """ names = ("dvr_site_activity", @@ -5971,14 +5971,14 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ------------------------------------------------------------------------- @staticmethod def defaults(): """ Safe defaults for names in case the module is disabled """ - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -6255,10 +6255,10 @@ def dvr_due_followups(human_resource_id=None): """ # Generate a request for case activities and customise it - r = S3Request("dvr", "case_activity", - args = ["count_due_followups"], - get_vars = {}, - ) + r = CRUDRequest("dvr", "case_activity", + args = ["count_due_followups"], + get_vars = {}, + ) r.customise_resource() resource = r.resource @@ -7197,7 +7197,7 @@ def apply_method(self, r, **attr): if filter_widgets: # Where to retrieve filtered data from: - _vars = resource.crud._remove_filters(r.get_vars) + _vars = S3Method._remove_filters(r.get_vars) filter_submit_url = r.url(vars=_vars) # Where to retrieve updated filter options from: @@ -7268,7 +7268,7 @@ def apply_method(self, r, **attr): """ Main entry point for REST interface. - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller parameters """ @@ -7292,7 +7292,7 @@ def bulk_update_status(self, r, **attr): """ Method to bulk-update status of allowance payments - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller parameters """ @@ -7548,7 +7548,7 @@ def apply_method(self, r, **attr): """ Main entry point for REST interface. - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller parameters """ @@ -7580,7 +7580,7 @@ def registration_form(self, r, **attr): """ Render and process the registration form - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller parameters """ @@ -7889,7 +7889,7 @@ def accept(self, r, form, event_type=None): """ Helper function to process the form - @param r: the S3Request + @param r: the CRUDRequest @param form: the FORM @param event_type: the event_type (Row) """ @@ -7930,7 +7930,7 @@ def registration_ajax(self, r, **attr): t: the event type code } - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller parameters @return: JSON response, structure: @@ -8198,11 +8198,11 @@ def register_event(person_id, type_id): case_id = None # Customise event resource - r = S3Request("dvr", "case_event", - current.request, - args = [], - get_vars = {}, - ) + r = CRUDRequest("dvr", "case_event", + current.request, + args = [], + get_vars = {}, + ) r.customise_resource("dvr_case_event") data = {"person_id": person_id, @@ -8851,7 +8851,7 @@ def accept(self, r, form, event_type=None): """ Helper function to process the form - @param r: the S3Request + @param r: the CRUDRequest @param form: the FORM @param event_type: the event_type (Row) """ @@ -8904,7 +8904,7 @@ def registration_ajax(self, r, **attr): d: the payment data (raw data, which payments to update) } - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller parameters @return: JSON response, structure: @@ -9199,11 +9199,11 @@ def register_payments(person_id, payments, date=None, comments=None): failed = 0 # Customise allowance resource - r = S3Request("dvr", "allowance", - current.request, - args = [], - get_vars = {}, - ) + r = CRUDRequest("dvr", "allowance", + current.request, + args = [], + get_vars = {}, + ) r.customise_resource("dvr_allowance") onaccept = current.s3db.onaccept @@ -9271,7 +9271,7 @@ def apply_method(self, r, **attr): """ Apply method. - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller options for this request """ @@ -9422,7 +9422,7 @@ def apply_method(self, r, **attr): if filter_widgets: # Where to retrieve filtered data from: - _vars = resource.crud._remove_filters(r.get_vars) + _vars = S3Method._remove_filters(r.get_vars) filter_submit_url = r.url(vars=_vars) # Default Filters (before selecting data!) diff --git a/modules/s3db/event.py b/modules/s3db/event.py index 384f576b77..80f28e9e47 100644 --- a/modules/s3db/event.py +++ b/modules/s3db/event.py @@ -86,7 +86,7 @@ from s3layouts import S3PopupLink # ============================================================================= -class S3EventModel(S3Model): +class S3EventModel(DataModel): """ Event Model @@ -566,36 +566,36 @@ def model(self): ) # Custom Methods - set_method("event", "event", + set_method("event_event", method = "dispatch", action = event_notification_dispatcher) - set_method("event", "event", + set_method("event_event", method = "add_bookmark", action = self.event_add_bookmark) - set_method("event", "event", + set_method("event_event", method = "remove_bookmark", action = self.event_remove_bookmark) - set_method("event", "event", + set_method("event_event", method = "add_tag", action = self.event_add_tag) - set_method("event", "event", + set_method("event_event", method = "remove_tag", action = self.event_remove_tag) - set_method("event", "event", + set_method("event_event", method = "share", action = self.event_share) - set_method("event", "event", + set_method("event_event", method = "unshare", action = self.event_unshare) # Custom Method to Assign HRs - set_method("event", "event", + set_method("event_event", method = "assign", action = self.pr_AssignMethod(component="human_resource")) @@ -938,7 +938,7 @@ def event_update_onaccept(form): db(table.id == row.post_id).update(expired=True) # ============================================================================= -class S3EventLocationModel(S3Model): +class S3EventLocationModel(DataModel): """ Event Locations model - locations for Events @@ -981,10 +981,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class S3EventNameModel(S3Model): +class S3EventNameModel(DataModel): """ Event Names model - local names for Events @@ -1020,10 +1020,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class S3EventTagModel(S3Model): +class S3EventTagModel(DataModel): """ Event Tags model - tags for Events @@ -1069,10 +1069,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class S3IncidentModel(S3Model): +class S3IncidentModel(DataModel): """ Incidents - the primary unit at which things are managed: @@ -1440,47 +1440,47 @@ def model(self): ) # Custom Methods - set_method("event", "incident", + set_method("event_incident", method = "add_bookmark", action = self.incident_add_bookmark) - set_method("event", "incident", + set_method("event_incident", method = "remove_bookmark", action = self.incident_remove_bookmark) - set_method("event", "incident", + set_method("event_incident", method = "add_tag", action = self.incident_add_tag) - set_method("event", "incident", + set_method("event_incident", method = "remove_tag", action = self.incident_remove_tag) - set_method("event", "incident", + set_method("event_incident", method = "share", action = self.incident_share) - set_method("event", "incident", + set_method("event_incident", method = "unshare", action = self.incident_unshare) - set_method("event", "incident", + set_method("event_incident", method = "plan", action = event_ActionPlan) - set_method("event", "incident", + set_method("event_incident", method = "scenario", action = event_ApplyScenario) - set_method("event", "incident", + set_method("event_incident", method = "assign", action = self.pr_AssignMethod(component="human_resource")) - set_method("event", "incident", + set_method("event_incident", method = "event", action = event_EventAssignMethod()) - set_method("event", "incident", + set_method("event_incident", method = "dispatch", action = event_notification_dispatcher) @@ -2061,7 +2061,7 @@ def incident_unshare(r, **attr): return output # ============================================================================= -class S3IncidentReportModel(S3Model): +class S3IncidentReportModel(DataModel): """ Incident Reports - reports about incidents @@ -2217,7 +2217,7 @@ def model(self): ) # Custom Methods - self.set_method("event", "incident_report", + self.set_method("event_incident_report", method = "assign", action = event_IncidentAssignMethod(component = "incident_report_incident", next_tab = "incident_report")) @@ -2263,10 +2263,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class S3EventActivityModel(S3Model): +class S3EventActivityModel(DataModel): """ Link Project Activities to Events """ @@ -2299,10 +2299,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class S3EventRequestModel(S3Model): +class S3EventRequestModel(DataModel): """ Link Requests to Incidents &/or Events """ @@ -2338,10 +2338,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class S3EventResourceModel(S3Model): +class S3EventResourceModel(DataModel): """ Resources Assigned to Events/Incidents - depends on Stats module @@ -2360,7 +2360,7 @@ def model(self): if not current.deployment_settings.has_module("stats"): current.log.warning("Event Resource Model needs Stats module enabling") - return {} + return None T = current.T super_link = self.super_link @@ -2449,7 +2449,7 @@ def model(self): msg_list_empty=T("No Resources assigned to Incident")) # Custom Methods - #self.set_method("event", "resource", + #self.set_method("event_resource", # method = "check-in", # action = S3CheckInMethod()) @@ -2517,10 +2517,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class S3IncidentReportOrganisationGroupModel(S3Model): +class S3IncidentReportOrganisationGroupModel(DataModel): """ Links between Incident Reports & Organisation Groups """ @@ -2555,10 +2555,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class S3IncidentLogModel(S3Model): +class S3IncidentLogModel(DataModel): """ Incident Logs - record of all changes @@ -2628,7 +2628,7 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -2661,7 +2661,7 @@ def event_incident_log_create_onaccept(form): current.msg.send_by_pe_id(pe_id, subject, message, contact_method="SMS") # ============================================================================= -class S3IncidentTypeModel(S3Model): +class S3IncidentTypeModel(DataModel): """ Incident Types """ @@ -2786,7 +2786,7 @@ def defaults(): } # ============================================================================= -class S3IncidentTypeTagModel(S3Model): +class S3IncidentTypeTagModel(DataModel): """ Incident Type Tags - Key-Value extensions @@ -2822,10 +2822,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class S3EventAlertModel(S3Model): +class S3EventAlertModel(DataModel): """ Alerts for Events/Incidents @@ -2887,7 +2887,7 @@ def model(self): msg_list_empty = T("No Alerts currently defined")) # Custom method to send alerts - #self.set_method("event", "alert", + #self.set_method("event_alert", # method = "send", # action = self.event_alert_send) @@ -2926,10 +2926,10 @@ def model(self): msg_list_empty = T("No Recipients currently defined")) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class S3EventAssetModel(S3Model): +class S3EventAssetModel(DataModel): """ Link Assets to Incidents """ @@ -2943,7 +2943,7 @@ def model(self): if not settings.has_module("supply"): # Don't crash #return self.defaults() - return {} + return None T = current.T @@ -3075,7 +3075,7 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # --------------------------------------------------------------------- @staticmethod @@ -3154,7 +3154,7 @@ def event_asset_onaccept(form, create=True): ) # ============================================================================= -class S3EventBookmarkModel(S3Model): +class S3EventBookmarkModel(DataModel): """ Bookmarks for Events &/or Incidents - the Incident bookmarks do NOT populate the Event's @@ -3200,10 +3200,10 @@ def model(self): # msg_list_empty = T("No Incidents currently bookmarked")) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class S3EventCMSModel(S3Model): +class S3EventCMSModel(DataModel): """ Link CMS Posts to Events &/or Incidents """ @@ -3279,10 +3279,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class S3EventCMSTagModel(S3Model): +class S3EventCMSTagModel(DataModel): """ Link (CMS) Tags to Events or Incidents (used in WACOP) - the Incident tags do NOT populate the Event's @@ -3321,10 +3321,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class S3EventDCModel(S3Model): +class S3EventDCModel(DataModel): """ Link Data Collections to Events &/or Incidents """ @@ -3391,10 +3391,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class S3EventExpenseModel(S3Model): +class S3EventExpenseModel(DataModel): """ Link Expenses to Incidents &/or Events - normally linked at the Incident level & just visible at the Event level @@ -3440,10 +3440,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class S3EventForumModel(S3Model): +class S3EventForumModel(DataModel): """ Shares for Events &/or Incidents - the Incident shares do NOT populate the Event's @@ -3488,10 +3488,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class S3EventHRModel(S3Model): +class S3EventHRModel(DataModel): """ Link Human Resources to Events/Incidents """ @@ -3664,7 +3664,7 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # --------------------------------------------------------------------- @staticmethod @@ -3743,7 +3743,7 @@ def event_human_resource_onaccept(form, create=True): ) # ============================================================================= -class S3EventTeamModel(S3Model): +class S3EventTeamModel(DataModel): """ Link Teams to Events &/or Incidents """ names = ("event_team_status", @@ -3855,10 +3855,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class S3EventImpactModel(S3Model): +class S3EventImpactModel(DataModel): """ Link Events &/or Incidents with Impacts """ @@ -3870,7 +3870,7 @@ def model(self): if not current.deployment_settings.has_module("stats"): current.log.warning("Event Impact Model needs Stats module enabling") - return {} + return None #T = current.T @@ -3916,10 +3916,10 @@ def model(self): # msg_list_empty = T("No Impacts currently registered in this Event")) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class S3EventMapModel(S3Model): +class S3EventMapModel(DataModel): """ Link Map Configs to Incidents """ @@ -3963,10 +3963,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class S3EventNeedModel(S3Model): +class S3EventNeedModel(DataModel): """ Link Events &/or Incidents with Needs """ @@ -4021,10 +4021,10 @@ def model(self): # msg_list_empty = T("No Needs currently registered in this Event")) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class S3EventNeedResponseModel(S3Model): +class S3EventNeedResponseModel(DataModel): """ Link Events &/or Incidents with Need Responses (Activity Groups) """ @@ -4079,10 +4079,10 @@ def model(self): # msg_list_empty = T("No Activity Groups currently registered in this Event")) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class S3EventOrganisationModel(S3Model): +class S3EventOrganisationModel(DataModel): """ Link Organisations to Events &/or Incidents """ @@ -4152,10 +4152,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class S3EventProjectModel(S3Model): +class S3EventProjectModel(DataModel): """ Link Projects to Events """ @@ -4188,10 +4188,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class S3EventScenarioModel(S3Model): +class S3EventScenarioModel(DataModel): """ Scenario Model @@ -4313,7 +4313,7 @@ def model(self): super_entity = "doc_entity", ) - self.set_method("event", "scenario", + self.set_method("event_scenario", method = "plan", action = event_ScenarioActionPlan) @@ -4322,7 +4322,7 @@ def model(self): } # ============================================================================= -class S3EventScenarioAssetModel(S3Model): +class S3EventScenarioAssetModel(DataModel): """ Link Scenarios to Assets """ @@ -4336,7 +4336,7 @@ def model(self): if not settings.has_module("supply"): # Don't crash #return self.defaults() - return {} + return None T = current.T @@ -4419,10 +4419,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class S3EventScenarioHRModel(S3Model): +class S3EventScenarioHRModel(DataModel): """ Link Scenarios to Human Resources """ @@ -4518,10 +4518,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class S3EventScenarioOrganisationModel(S3Model): +class S3EventScenarioOrganisationModel(DataModel): """ Link Scenarios to Organisations """ @@ -4569,10 +4569,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class S3EventScenarioTaskModel(S3Model): +class S3EventScenarioTaskModel(DataModel): """ Link Scenarios to Tasks @@ -4623,10 +4623,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class S3EventSiteModel(S3Model): +class S3EventSiteModel(DataModel): """ Link Sites (Facilities) to Incidents """ @@ -4738,10 +4738,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class S3EventShelterModel(S3Model): +class S3EventShelterModel(DataModel): """ Link Shelters to Events """ @@ -4810,10 +4810,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class S3EventSitRepModel(S3Model): +class S3EventSitRepModel(DataModel): """ Situation Reports - can be simple text/rich text @@ -5176,7 +5176,7 @@ def event_sitrep_create_onaccept(form): db(db.event_sitrep.id == sitrep_id).update(table_id=table_id) # ============================================================================= -class S3EventTaskModel(S3Model): +class S3EventTaskModel(DataModel): """ Link Tasks to Incidents &/or Events - normally linked at the Incident level & just visible at the Event level @@ -5238,7 +5238,7 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= def set_event_from_incident(form, tablename): @@ -5322,7 +5322,7 @@ def apply_method(self, r, **attr): """ Entry point for REST API - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller arguments """ @@ -5577,7 +5577,7 @@ def apply_method(self, r, **attr): """ Entry point for REST API - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller arguments """ @@ -5806,7 +5806,7 @@ def apply_method(self, r, **attr): """ Entry point for REST API - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller arguments """ @@ -5936,7 +5936,7 @@ def apply_method(self, r, **attr): """ Apply method. - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller options for this request """ @@ -6210,7 +6210,7 @@ def apply_method(self, r, **attr): if filter_widgets: # Where to retrieve filtered data from: - submit_url_vars = resource.crud._remove_filters(r.get_vars) + submit_url_vars = S3Method._remove_filters(r.get_vars) filter_submit_url = r.url(vars=submit_url_vars) # Default Filters (before selecting data!) @@ -6332,7 +6332,7 @@ def apply_method(self, r, **attr): """ Apply method. - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller options for this request """ @@ -6595,7 +6595,7 @@ def apply_method(self, r, **attr): if filter_widgets: # Where to retrieve filtered data from: - submit_url_vars = resource.crud._remove_filters(r.get_vars) + submit_url_vars = S3Method._remove_filters(r.get_vars) filter_submit_url = r.url(vars=submit_url_vars) # Default Filters (before selecting data!) diff --git a/modules/s3db/fin.py b/modules/s3db/fin.py index d0178beb3f..c5e514704e 100644 --- a/modules/s3db/fin.py +++ b/modules/s3db/fin.py @@ -44,7 +44,7 @@ from ..core import * # ============================================================================= -class FinExpensesModel(S3Model): +class FinExpensesModel(DataModel): """ Model for Expenses """ names = ("fin_expense", @@ -152,7 +152,7 @@ def defaults(): } # ============================================================================= -class FinVoucherModel(S3Model): +class FinVoucherModel(DataModel): """ Model for Voucher Programs """ names = ("fin_voucher_program", @@ -1080,7 +1080,7 @@ def model(self): create_onaccept = self.debit_create_onaccept, ) - self.set_method("fin", "voucher_debit", + self.set_method("fin_voucher_debit", method = "cancel", action = fin_VoucherCancelDebit, ) @@ -3003,8 +3003,8 @@ def generate_claims(self): ctable = s3db.fin_voucher_claim # Customise claim resource - from core import S3Request - r = S3Request("fin", "voucher_claim", args=[], get_vars={}) + from core import CRUDRequest + r = CRUDRequest("fin", "voucher_claim", args=[], get_vars={}) r.customise_resource("fin_voucher_claim") # Base query @@ -3158,8 +3158,8 @@ def generate_invoice(cls, claim_id): invoice_no = "B%s%02dC%04d" % (bprefix, claim.billing_id, claim.id) # Customise invoice resource - from core import S3Request - r = S3Request("fin", "voucher_invoice", args=[], get_vars={}) + from core import CRUDRequest + r = CRUDRequest("fin", "voucher_invoice", args=[], get_vars={}) r.customise_resource("fin_voucher_invoice") # Generate invoice @@ -3349,8 +3349,8 @@ def settle_invoice(cls, invoice_id, ptoken): ) # Customise invoice resource - from core import S3Request - r = S3Request("fin", "voucher_invoice", args=[], get_vars={}) + from core import CRUDRequest + r = CRUDRequest("fin", "voucher_invoice", args=[], get_vars={}) r.customise_resource("fin_voucher_invoice") # Trigger onsettled-callback for invoice @@ -3556,7 +3556,7 @@ def apply_method(self, r, **attr): """ Entry point for REST API - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes """ @@ -3581,7 +3581,7 @@ def cancel(self, r, **attr): """ Cancel a voucher debit - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes """ diff --git a/modules/s3db/fire.py b/modules/s3db/fire.py index b86f3c5eca..a20f38715c 100644 --- a/modules/s3db/fire.py +++ b/modules/s3db/fire.py @@ -38,7 +38,7 @@ from s3layouts import S3PopupLink # ============================================================================= -class S3FireModel(S3Model): +class S3FireModel(DataModel): """ Fire Zones: Burn Perimeter, Burnt zone, Evacuation Zone, etc """ @@ -137,10 +137,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class S3FireStationModel(S3Model): +class S3FireStationModel(DataModel): """ A Model to manage Fire Stations: http://eden.sahanafoundation.org/wiki/Deployments/Bombeiros @@ -364,7 +364,7 @@ def model(self): msg_no_match = T("No Vehicles could be found"), msg_list_empty = T("No Vehicles currently registered")) - self.set_method("fire", "station", + self.set_method("fire_station", method = "vehicle_report", action = self.vehicle_report, ) @@ -603,7 +603,6 @@ def vehicle_report(r, **attr): fact = "sum(minutes)", ), ) - req.set_handler("report", S3Report()) req.resource.add_filter(query) return req(rheader=rheader) diff --git a/modules/s3db/gis.py b/modules/s3db/gis.py index fa71651637..1cd6550cc5 100644 --- a/modules/s3db/gis.py +++ b/modules/s3db/gis.py @@ -65,7 +65,7 @@ SEPARATORS = (",", ":") # ============================================================================= -class S3LocationModel(S3Model): +class S3LocationModel(DataModel): """ Locations model """ @@ -376,7 +376,7 @@ def model(self): ) # Custom Method for S3LocationAutocompleteWidget - self.set_method("gis", "location", + self.set_method("gis_location", method = "search_ac", action = self.gis_search_ac) @@ -723,7 +723,7 @@ def gis_location_duplicate(item): This callback will be called when importing location records it will look to see if the record being imported is a duplicate. - @param item: An S3ImportItem object which includes all the details + @param item: An ImportItem object which includes all the details of the record being imported If the record is a duplicate then it will set the item method to update @@ -932,7 +932,7 @@ def gis_search_ac(r, **attr): JSON search method for S3LocationAutocompleteWidget - adds hierarchy support - @param r: the S3Request + @param r: the CRUDRequest @param attr: request attributes """ @@ -1232,7 +1232,7 @@ def gis_search_ac(r, **attr): return output # ============================================================================= -class S3LocationNameModel(S3Model): +class S3LocationNameModel(DataModel): """ Location Names model - local/alternate names for Locations @@ -1300,10 +1300,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class S3LocationTagModel(S3Model): +class S3LocationTagModel(DataModel): """ Location Tags model - flexible Key-Value component attributes to Locations @@ -1383,7 +1383,7 @@ def gis_country_opts(countries): return od # ============================================================================= -class S3LocationGroupModel(S3Model): +class S3LocationGroupModel(DataModel): """ Location Groups model - currently unused @@ -1436,10 +1436,10 @@ def model(self): *s3_meta_fields()) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class S3LocationHierarchyModel(S3Model): +class S3LocationHierarchyModel(DataModel): """ Location Hierarchy model """ @@ -1615,7 +1615,7 @@ def gis_hierarchy_onvalidation(form): form.errors[gap] = hierarchy_gap # ============================================================================= -class S3GISConfigModel(S3Model): +class S3GISConfigModel(DataModel): """ GIS Config model: Web Map Context - Site config @@ -2474,7 +2474,7 @@ def represent_row(self, row): return represent # ============================================================================== -class S3LayerEntityModel(S3Model): +class S3LayerEntityModel(DataModel): """ Model for Layer SuperEntity - used to provide a common link table for: @@ -2860,7 +2860,7 @@ def rgb2hex(r, g, b): ) # ============================================================================= -class S3FeatureLayerModel(S3Model): +class S3FeatureLayerModel(DataModel): """ Model for Feature Layers - used to select a set of Features for either Display on a Map @@ -3014,7 +3014,7 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -3069,7 +3069,7 @@ def gis_layer_feature_deduplicate(item): item.method = item.METHOD.UPDATE # ============================================================================= -class S3MapModel(S3Model): +class S3MapModel(DataModel): """ Models for Maps """ names = ("gis_cache", @@ -4067,7 +4067,7 @@ def model(self): *s3_meta_fields()) # Pass names back to global scope (s3.*) - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -4434,7 +4434,7 @@ def gis_layer_shapefile_onaccept_update(form): S3MapModel.gis_layer_shapefile_onaccept(form) # ============================================================================= -class S3GISThemeModel(S3Model): +class S3GISThemeModel(DataModel): """ Thematic Mapping model @@ -4501,7 +4501,7 @@ def model(self): ) # Custom Method to generate a style - self.set_method("gis", "layer_theme", + self.set_method("gis_layer_theme", method = "style", action = self.gis_theme_style) @@ -4594,7 +4594,7 @@ def gis_theme_style(r, **attr): return json.dumps(style) # ============================================================================= -class S3PoIModel(S3Model): +class S3PoIModel(DataModel): """ Data Model for PoIs (Points of Interest) """ @@ -4897,7 +4897,7 @@ def gis_poi_type_onaccept(form): current.log.warning("Unable to update GIS PoI Style as there are multiple possible") # ============================================================================= -class S3PoIOrganisationGroupModel(S3Model): +class S3PoIOrganisationGroupModel(DataModel): """ PoI Organisation Group Model @@ -4932,10 +4932,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class S3PoIFeedModel(S3Model): +class S3PoIFeedModel(DataModel): """ Data Model for PoI feeds """ names = ("gis_poi_feed",) @@ -4953,7 +4953,7 @@ def model(self): *s3_meta_fields()) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= def name_field(): diff --git a/modules/s3db/hms.py b/modules/s3db/hms.py index 5cf64b6c6f..fc953f66fb 100644 --- a/modules/s3db/hms.py +++ b/modules/s3db/hms.py @@ -41,7 +41,7 @@ from s3layouts import S3PopupLink # ============================================================================= -class HospitalDataModel(S3Model): +class HospitalDataModel(DataModel): names = ("hms_hospital", "hms_contact", @@ -412,7 +412,7 @@ def model(self): ) # Custom Method to Assign HRs - self.set_method("hms", "hospital", + self.set_method("hms_hospital", method = "assign", action = self.hrm_AssignMethod(component="human_resource_site")) @@ -985,7 +985,7 @@ def hms_bed_capacity_onaccept(form): available_beds=a_beds) # ============================================================================= -class CholeraTreatmentCapabilityModel(S3Model): +class CholeraTreatmentCapabilityModel(DataModel): names = ("hms_ctc",) @@ -1124,15 +1124,15 @@ def model(self): # --------------------------------------------------------------------- # Return global names to s3db # - return {} + return None # ------------------------------------------------------------------------- def defaults(self): - return {} + return None # ============================================================================= -class HospitalActivityReportModel(S3Model): +class HospitalActivityReportModel(DataModel): names = ("hms_activity",) @@ -1216,12 +1216,12 @@ def model(self): # --------------------------------------------------------------------- # Return global names to s3db # - return {} + return None # ------------------------------------------------------------------------- def defaults(self): - return {} + return None # ------------------------------------------------------------------------- @staticmethod diff --git a/modules/s3db/hrm.py b/modules/s3db/hrm.py index f6d9f36186..3639cea7a9 100644 --- a/modules/s3db/hrm.py +++ b/modules/s3db/hrm.py @@ -88,7 +88,7 @@ SEPARATORS = (",", ":") # ============================================================================= -class HRModel(S3Model): +class HRModel(DataModel): names = ("hrm_department", "hrm_department_id", @@ -621,11 +621,11 @@ def model(self): # Custom Method for S3HumanResourceAutocompleteWidget and S3AddPersonWidget set_method = self.set_method - set_method("hrm", "human_resource", + set_method("hrm_human_resource", method = "search_ac", action = self.hrm_search_ac) - set_method("hrm", "human_resource", + set_method("hrm_human_resource", method = "lookup", action = self.hrm_lookup) @@ -1053,7 +1053,7 @@ def hrm_job_title_duplicate(item): """ Update detection for hrm_job_title - @param item: the S3ImportItem + @param item: the ImportItem """ data = item.data @@ -1387,7 +1387,7 @@ def hrm_human_resource_ondelete(row): current.s3db.pr_update_affiliations(htable, row) # ============================================================================= -class HRSiteModel(S3Model): +class HRSiteModel(DataModel): names = ("hrm_human_resource_site",) @@ -1439,7 +1439,7 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -1509,7 +1509,7 @@ def hrm_human_resource_site_ondelete(row): ) # ============================================================================= -class HRSalaryModel(S3Model): +class HRSalaryModel(DataModel): """ Data Model to track salaries of staff """ names = ("hrm_staff_level", @@ -1654,7 +1654,7 @@ def model(self): # # @todo: implement - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -1741,7 +1741,7 @@ def represent_row(self, row): return name # ============================================================================= -class HRInsuranceModel(S3Model): +class HRInsuranceModel(DataModel): """ Data Model to track insurance information of staff members """ names = ("hrm_insurance", @@ -1807,10 +1807,10 @@ def model(self): ), ) - return {} + return None # ============================================================================= -class HRContractModel(S3Model): +class HRContractModel(DataModel): """ Data model to track employment contract details of staff members """ names = ("hrm_contract", @@ -1860,10 +1860,10 @@ def model(self): deduplicate = S3Duplicate(primary = ("human_resource_id",)), ) - return {} + return None # ============================================================================= -class HRJobModel(S3Model): +class HRJobModel(DataModel): """ Unused @ToDo: If bringing back into use then Availability better as Person component not HR @@ -2034,7 +2034,7 @@ def model(self): } # ============================================================================= -class HRSkillModel(S3Model): +class HRSkillModel(DataModel): names = ("hrm_skill_type", "hrm_skill", @@ -3876,7 +3876,7 @@ def hrm_competency_rating_duplicate(item): This callback will be called when importing records it will look to see if the record being imported is a duplicate. - @param item: An S3ImportItem object which includes all the details + @param item: An ImportItem object which includes all the details of the record being imported If the record is a duplicate then it will set the item method to update @@ -4182,7 +4182,7 @@ def hrm_training_onaccept(form): hrm_certification_onaccept(form) # ============================================================================= -class HREventStrategyModel(S3Model): +class HREventStrategyModel(DataModel): """ (Training) Events <> Strategies Link Table """ @@ -4208,10 +4208,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class HREventProgrammeModel(S3Model): +class HREventProgrammeModel(DataModel): """ (Training) Events <> Programmes Link Table """ @@ -4237,10 +4237,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class HREventProjectModel(S3Model): +class HREventProjectModel(DataModel): """ (Training) Events <> Projects Link Table """ @@ -4266,10 +4266,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class HREventAssessmentModel(S3Model): +class HREventAssessmentModel(DataModel): """ (Training) Events <> Data Collection Assessments Link Table Can be used for: @@ -4313,10 +4313,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class HRAppraisalModel(S3Model): +class HRAppraisalModel(DataModel): """ Appraisal for an HR - can be for a specific Mission or routine annual appraisal @@ -4444,7 +4444,7 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -4501,7 +4501,7 @@ def hrm_appraisal_document_onaccept(form): db(db.doc_document.id == document_id).update(doc_id = doc_id) # ============================================================================= -class HRExperienceModel(S3Model): +class HRExperienceModel(DataModel): """ Record a person's work experience """ @@ -4678,10 +4678,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class HRAwardModel(S3Model): +class HRAwardModel(DataModel): """ Data model for staff awards """ names = ("hrm_award_type", @@ -4755,10 +4755,10 @@ def model(self): msg_list_empty = T("Currently no awards registered")) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class HRDisciplinaryActionModel(S3Model): +class HRDisciplinaryActionModel(DataModel): """ Data model for staff disciplinary record """ names = ("hrm_disciplinary_type", @@ -4818,10 +4818,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class HRTagModel(S3Model): +class HRTagModel(DataModel): """ Arbitrary Key:Value Tags for Human Resources """ names = ("hrm_human_resource_tag", @@ -4859,10 +4859,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class HRProgrammeModel(S3Model): +class HRProgrammeModel(DataModel): """ Programmes - record Volunteer Hours @@ -5109,7 +5109,7 @@ def model(self): } # ============================================================================= -class HRShiftModel(S3Model): +class HRShiftModel(DataModel): """ Shifts """ @@ -5288,7 +5288,7 @@ def model(self): (T("Skills"), "person_id$competency.skill_id"), ] - set_method("hrm", "shift", + set_method("hrm_shift", method = "assign", action = self.hrm_AssignMethod(component = "human_resource_shift", next_tab = "facility", @@ -5317,7 +5317,7 @@ def facility_redirect(r, **attr): args = [facility.id, "shift"], )) - set_method("hrm", "shift", + set_method("hrm_shift", method = "facility", action = facility_redirect) @@ -5365,7 +5365,7 @@ def defaults(): } # ============================================================================= -class HRDelegationModel(S3Model): +class HRDelegationModel(DataModel): """ Model to manage delegations of staff/volunteers to other organisations. @@ -5744,7 +5744,7 @@ def apply_method(self, r, **attr): """ Apply method. - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller options for this request """ @@ -6004,7 +6004,7 @@ def apply_method(self, r, **attr): if filter_widgets: # Where to retrieve filtered data from: - submit_url_vars = resource.crud._remove_filters(r.get_vars) + submit_url_vars = S3Method._remove_filters(r.get_vars) filter_submit_url = r.url(vars = submit_url_vars) # Default Filters (before selecting data!) @@ -7849,7 +7849,7 @@ def postp(r, output): return output s3.postp = postp - return current.rest_controller("hrm", "competency", + return current.crud_controller("hrm", "competency", # @ToDo: Create these if-required #csv_stylesheet = ("hrm", "competency.xsl"), #csv_template = ("hrm", "competency"), @@ -7880,7 +7880,7 @@ def prep(r): return True s3.prep = prep - return current.rest_controller("hrm", "credential", + return current.crud_controller("hrm", "credential", # @ToDo: Create these if-required #csv_stylesheet = ("hrm", "credential.xsl"), #csv_template = ("hrm", "credential"), @@ -7910,7 +7910,7 @@ def prep(r): return True current.response.s3.prep = prep - return current.rest_controller("hrm", "experience", + return current.crud_controller("hrm", "experience", # @ToDo: Create these if-required #csv_stylesheet = ("hrm", "experience.xsl"), #csv_template = ("hrm", "experience"), @@ -8104,7 +8104,7 @@ def postp(r, output): (T("Documents"), "document"), ] - return current.rest_controller("pr", "group", + return current.crud_controller("pr", "group", csv_stylesheet = ("hrm", "group.xsl"), csv_template = "group", rheader = lambda r: \ @@ -8593,7 +8593,7 @@ def postp(r, output): return output s3.postp = postp - return current.rest_controller("hrm", "human_resource") + return current.crud_controller("hrm", "human_resource") # ============================================================================= def hrm_person_controller(**attr): @@ -8619,30 +8619,30 @@ def hrm_person_controller(**attr): # Custom Method(s) for Contacts contacts_tabs = settings.get_pr_contacts_tabs() if "all" in contacts_tabs: - set_method("pr", "person", + set_method("pr_person", method = "contacts", action = s3db.pr_Contacts) if "public" in contacts_tabs: - set_method("pr", "person", + set_method("pr_person", method = "public_contacts", action = s3db.pr_Contacts) if "private" in contacts_tabs: - set_method("pr", "person", + set_method("pr_person", method = "private_contacts", action = s3db.pr_Contacts) # Custom Method for CV - set_method("pr", "person", + set_method("pr_person", method = "cv", action = hrm_CV) # Custom Method for Medical - set_method("pr", "person", + set_method("pr_person", method = "medical", action = hrm_Medical) # Custom Method for HR Record - set_method("pr", "person", + set_method("pr_person", method = "record", action = hrm_Record) @@ -8748,46 +8748,44 @@ def hrm_person_controller(**attr): ) # Import pre-process - def import_prep(data, group=group): + def import_prep(tree, group=group): """ Deletes all HR records (of the given group) of the organisation/branch before processing a new data import """ - if s3.import_replace: - resource, tree = data - if tree is not None: - xml = current.xml - tag = xml.TAG - att = xml.ATTRIBUTE - - if group == "staff": - group = 1 - elif group == "volunteer": - group = 2 - else: - return # don't delete if no group specified - - root = tree.getroot() - expr = "/%s/%s[@%s='org_organisation']/%s[@%s='name']" % \ - (tag.root, tag.resource, att.name, tag.data, att.field) - orgs = root.xpath(expr) - for org in orgs: - org_name = org.get("value", None) or org.text - if org_name: - try: - org_name = json.loads(xml.xml_decode(org_name)) - except: - pass - if org_name: - htable = s3db.hrm_human_resource - otable = s3db.org_organisation - query = (otable.name == org_name) & \ - (htable.organisation_id == otable.id) & \ - (htable.type == group) - resource = s3db.resource("hrm_human_resource", filter=query) - # Use cascade=True so that the deletion gets - # rolled back if the import fails: - resource.delete(format="xml", cascade=True) + if s3.import_replace and tree is not None: + xml = current.xml + tag = xml.TAG + att = xml.ATTRIBUTE + + if group == "staff": + group = 1 + elif group == "volunteer": + group = 2 + else: + return # don't delete if no group specified + + root = tree.getroot() + expr = "/%s/%s[@%s='org_organisation']/%s[@%s='name']" % \ + (tag.root, tag.resource, att.name, tag.data, att.field) + orgs = root.xpath(expr) + for org in orgs: + org_name = org.get("value", None) or org.text + if org_name: + try: + org_name = json.loads(xml.xml_decode(org_name)) + except: + pass + if org_name: + htable = s3db.hrm_human_resource + otable = s3db.org_organisation + query = (otable.name == org_name) & \ + (htable.organisation_id == otable.id) & \ + (htable.type == group) + resource = s3db.resource("hrm_human_resource", filter=query) + # Use cascade=True so that the deletion gets + # rolled back if the import fails: + resource.delete(format="xml", cascade=True) s3.import_prep = import_prep @@ -9022,7 +9020,7 @@ def postp(r, output): } _attr.update(attr) - return current.rest_controller("pr", "person", **_attr) + return current.crud_controller("pr", "person", **_attr) # ============================================================================= def hrm_training_controller(): @@ -9084,7 +9082,7 @@ def prep(r): return True current.response.s3.prep = prep - return current.rest_controller("hrm", "training", + return current.crud_controller("hrm", "training", csv_stylesheet = ("hrm", "training.xsl"), csv_template = ("hrm", "training"), csv_extra_fields = [{"label": "Training Event", @@ -9228,7 +9226,7 @@ def prep(r): # return output #s3.postp = postp - return current.rest_controller("hrm", "training_event", + return current.crud_controller("hrm", "training_event", rheader = hrm_rheader, ) @@ -9373,7 +9371,7 @@ def apply_method(self, r, **attr): """ Entry point for REST API - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller arguments """ @@ -9669,7 +9667,7 @@ def apply_method(self, r, **attr): """ Entry point for REST API - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller arguments """ @@ -9791,7 +9789,7 @@ def apply_method(self, r, **attr): """ Entry point for REST API - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller arguments """ @@ -10088,7 +10086,7 @@ def hrm_configure_salary(r): """ Configure the salary tab - @param r: the S3Request + @param r: the CRUDRequest """ hr_id = None diff --git a/modules/s3db/inv.py b/modules/s3db/inv.py index 465590f135..c689fccee8 100644 --- a/modules/s3db/inv.py +++ b/modules/s3db/inv.py @@ -119,7 +119,7 @@ def inv_itn_label(): return current.T("CTN") # ============================================================================= -class InvWarehouseModel(S3Model): +class InvWarehouseModel(DataModel): names = ("inv_warehouse", "inv_warehouse_type", @@ -395,7 +395,7 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -407,7 +407,7 @@ def inv_warehouse_onaccept(form): current.s3db.org_update_affiliations("inv_warehouse", form.vars) # ============================================================================= -class InventoryModel(S3Model): +class InventoryModel(DataModel): """ Inventory Management @@ -890,7 +890,7 @@ def inv_item_duplicate(item): """ Update detection for inv_inv_item - @param item: the S3ImportItem + @param item: the ImportItem """ table = item.table @@ -930,7 +930,7 @@ def inv_item_duplicate(item): item.data.quantity = duplicate.quantity # ============================================================================= -class InventoryTrackingModel(S3Model): +class InventoryTrackingModel(DataModel): """ A module to manage the shipment of inventory items - Sent Items @@ -1251,11 +1251,11 @@ def model(self): # Custom methods # Generate Consignment Note - set_method("inv", "send", + set_method("inv_send", method = "form", action = self.inv_send_form) - set_method("inv", "send", + set_method("inv_send", method = "timeline", action = self.inv_timeline) @@ -1566,15 +1566,15 @@ def model(self): # Custom methods # Print Forms - set_method("inv", "recv", + set_method("inv_recv", method = "form", action = self.inv_recv_form) - set_method("inv", "recv", + set_method("inv_recv", method = "cert", action = self.inv_recv_donation_cert) - set_method("inv", "recv", + set_method("inv_recv", method = "timeline", action = self.inv_timeline) @@ -4060,7 +4060,7 @@ def inv_recv_pdf_footer(r): return None # ============================================================================= -class InventoryAdjustModel(S3Model): +class InventoryAdjustModel(DataModel): """ A module to manage the shipment of inventory items - Sent Items @@ -5141,10 +5141,10 @@ def prep(r): ) s3.prep = prep - output = current.rest_controller("inv", "send", - rheader = inv_send_rheader, - ) - return output + + return current.crud_controller("inv", "send", + rheader = inv_send_rheader, + ) # ============================================================================= def inv_send_process(): diff --git a/modules/s3db/irs.py b/modules/s3db/irs.py index 9d3cc9be8e..2345e34b39 100644 --- a/modules/s3db/irs.py +++ b/modules/s3db/irs.py @@ -34,8 +34,6 @@ import json -from collections import OrderedDict - from gluon import * from gluon.storage import Storage @@ -46,7 +44,7 @@ SEPARATORS = (",", ":") # ============================================================================= -class S3IRSModel(S3Model): +class S3IRSModel(DataModel): names = ("irs_icategory", "irs_ireport", @@ -479,15 +477,15 @@ def model(self): ) # Custom Methods - set_method("irs", "ireport", + set_method("irs_ireport", method = "dispatch", action=self.irs_dispatch) - set_method("irs", "ireport", + set_method("irs_ireport", method = "timeline", action = self.irs_timeline) - set_method("irs", "ireport", + set_method("irs_ireport", method = "ushahidi", action = self.irs_ushahidi_import) @@ -968,23 +966,23 @@ def irs_ushahidi_import(r, **attr): ignore_errors = formvars.get("ignore_errors") resource = r.resource try: - success = resource.import_xml(ushahidi_url, - stylesheet = stylesheet, - ignore_errors = ignore_errors, - ) + result = resource.import_xml(ushahidi_url, + stylesheet = stylesheet, + ignore_errors = ignore_errors, + ) except: import sys response.error = sys.exc_info()[1] else: - if success: - count = resource.import_count + if result.success: + count = result.count if count: response.confirmation = "%(number)s reports successfully imported." % \ {"number": count} else: response.information = T("No reports available.") else: - response.error = resource.error + response.error = result.error response.view = "create.html" @@ -994,7 +992,7 @@ def irs_ushahidi_import(r, **attr): r.error(405, current.ERROR.BAD_METHOD) # ============================================================================= -class S3IRSResponseModel(S3Model): +class S3IRSResponseModel(DataModel): """ Tables used when responding to Incident Reports - with HRMs &/or Vehicles @@ -1080,7 +1078,7 @@ def response_represent(opt): ]) if not settings.has_module("vehicle"): - return {} + return None # --------------------------------------------------------------------- # Vehicles assigned to an Incident @@ -1157,7 +1155,7 @@ def response_represent(opt): # --------------------------------------------------------------------- # Return model-global names to s3db.* # - return {} + return None # ------------------------------------------------------------------------- @staticmethod diff --git a/modules/s3db/member.py b/modules/s3db/member.py index c43877d8f9..2ba96d5731 100644 --- a/modules/s3db/member.py +++ b/modules/s3db/member.py @@ -39,7 +39,7 @@ from s3layouts import S3PopupLink # ============================================================================= -class S3MembersModel(S3Model): +class S3MembersModel(DataModel): """ """ @@ -572,7 +572,7 @@ def member_onaccept(form): record.update_record(**data) # ============================================================================= -class S3MemberProgrammeModel(S3Model): +class S3MemberProgrammeModel(DataModel): """ Member Programmes Model """ names = ("member_membership_programme", @@ -592,7 +592,7 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= def member_rheader(r, tabs=None): diff --git a/modules/s3db/msg.py b/modules/s3db/msg.py index 2f7e9a14be..c813bdbfca 100644 --- a/modules/s3db/msg.py +++ b/modules/s3db/msg.py @@ -56,7 +56,7 @@ SEPARATORS = (",", ":") # ============================================================================= -class S3ChannelModel(S3Model): +class S3ChannelModel(DataModel): """ Messaging Channels - all Inbound & Outbound channels for messages are instances of this @@ -356,7 +356,7 @@ def channel_poll(r, **attr): redirect(URL(f=fn)) # ============================================================================= -class S3MessageModel(S3Model): +class S3MessageModel(DataModel): """ Messages """ @@ -537,7 +537,7 @@ def defaults(): } # ============================================================================= -class S3MessageAttachmentModel(S3Model): +class S3MessageAttachmentModel(DataModel): """ Message Attachments - link table between msg_message & doc_document @@ -559,10 +559,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class S3MessageContactModel(S3Model): +class S3MessageContactModel(DataModel): """ Contact Form """ @@ -655,10 +655,10 @@ def model(self): msg_list_empty=T("No Contacts currently registered")) # --------------------------------------------------------------------- - return {} + return None # ============================================================================= -class S3MessageTagModel(S3Model): +class S3MessageTagModel(DataModel): """ Message Tags """ @@ -699,7 +699,7 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= class S3EmailModel(S3ChannelModel): @@ -764,15 +764,15 @@ def model(self): super_entity = "msg_channel", ) - set_method("msg", "email_channel", + set_method("msg_email_channel", method = "enable", action = self.msg_channel_enable_interactive) - set_method("msg", "email_channel", + set_method("msg_email_channel", method = "disable", action = self.msg_channel_disable_interactive) - set_method("msg", "email_channel", + set_method("msg_email_channel", method = "poll", action = self.msg_channel_poll) @@ -833,7 +833,7 @@ def model(self): ) # --------------------------------------------------------------------- - return {} + return None # ============================================================================= class S3FacebookModel(S3ChannelModel): @@ -900,15 +900,15 @@ def model(self): super_entity = "msg_channel", ) - set_method("msg", "facebook_channel", + set_method("msg_facebook_channel", method = "enable", action = self.msg_channel_enable_interactive) - set_method("msg", "facebook_channel", + set_method("msg_facebook_channel", method = "disable", action = self.msg_channel_disable_interactive) - #set_method("msg", "facebook_channel", + #set_method("msg_facebook_channel", # method = "poll", # action = self.msg_channel_poll) @@ -1037,20 +1037,20 @@ def model(self): super_entity = "msg_channel", ) - set_method("msg", "mcommons_channel", + set_method("msg_mcommons_channel", method = "enable", action = self.msg_channel_enable_interactive) - set_method("msg", "mcommons_channel", + set_method("msg_mcommons_channel", method = "disable", action = self.msg_channel_disable_interactive) - set_method("msg", "mcommons_channel", + set_method("msg_mcommons_channel", method = "poll", action = self.msg_channel_poll) # --------------------------------------------------------------------- - return {} + return None # ============================================================================= class S3GCMModel(S3ChannelModel): @@ -1099,20 +1099,20 @@ def model(self): super_entity = "msg_channel", ) - set_method("msg", "gcm_channel", + set_method("msg_gcm_channel", method = "enable", action = self.msg_channel_enable_interactive) - set_method("msg", "gcm_channel", + set_method("msg_gcm_channel", method = "disable", action = self.msg_channel_disable_interactive) - #set_method("msg", "gcm_channel", + #set_method("msg_gcm_channel", # method = "poll", # action = self.msg_channel_poll) # --------------------------------------------------------------------- - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -1126,7 +1126,7 @@ def msg_gcm_channel_onaccept(form): S3ChannelModel.channel_onaccept(form) # ============================================================================= -class S3ParsingModel(S3Model): +class S3ParsingModel(DataModel): """ Message Parsing Model """ @@ -1174,15 +1174,15 @@ def model(self): onaccept = self.msg_parser_onaccept, ) - set_method("msg", "parser", + set_method("msg_parser", method = "enable", action = self.parser_enable_interactive) - set_method("msg", "parser", + set_method("msg_parser", method = "disable", action = self.parser_disable_interactive) - set_method("msg", "parser", + set_method("msg_parser", method = "parse", action = self.parser_parse) @@ -1518,15 +1518,15 @@ def model(self): super_entity = "msg_channel", ) - set_method("msg", "rss_channel", + set_method("msg_rss_channel", method = "enable", action = self.msg_channel_enable_interactive) - set_method("msg", "rss_channel", + set_method("msg_rss_channel", method = "disable", action = self.msg_channel_disable_interactive) - set_method("msg", "rss_channel", + set_method("msg_rss_channel", method = "poll", action = self.msg_channel_poll) @@ -1616,10 +1616,10 @@ def model(self): ) # --------------------------------------------------------------------- - return {} + return None # ============================================================================= -class S3SMSModel(S3Model): +class S3SMSModel(DataModel): """ SMS: Short Message Service @@ -1681,10 +1681,10 @@ def model(self): ) # --------------------------------------------------------------------- - return {} + return None # ============================================================================= -class S3SMSOutboundModel(S3Model): +class S3SMSOutboundModel(DataModel): """ SMS: Short Message Service - Outbound Channels @@ -1856,10 +1856,10 @@ def model(self): ) # --------------------------------------------------------------------- - return {} + return None # ============================================================================= -class S3TropoModel(S3Model): +class S3TropoModel(DataModel): """ Tropo can be used to send & receive SMS, Twitter & XMPP @@ -1898,15 +1898,15 @@ def model(self): super_entity = "msg_channel", ) - set_method("msg", "tropo_channel", + set_method("msg_tropo_channel", method = "enable", action = self.msg_channel_enable_interactive) - set_method("msg", "tropo_channel", + set_method("msg_tropo_channel", method = "disable", action = self.msg_channel_disable_interactive) - set_method("msg", "tropo_channel", + set_method("msg_tropo_channel", method = "poll", action = self.msg_channel_poll) @@ -1923,7 +1923,7 @@ def model(self): ) # --------------------------------------------------------------------- - return {} + return None # ============================================================================= class S3TwilioModel(S3ChannelModel): @@ -1981,15 +1981,15 @@ def model(self): super_entity = "msg_channel", ) - set_method("msg", "twilio_channel", + set_method("msg_twilio_channel", method = "enable", action = self.msg_channel_enable_interactive) - set_method("msg", "twilio_channel", + set_method("msg_twilio_channel", method = "disable", action = self.msg_channel_disable_interactive) - set_method("msg", "twilio_channel", + set_method("msg_twilio_channel", method = "poll", action = self.msg_channel_poll) @@ -2005,10 +2005,10 @@ def model(self): *s3_meta_fields()) # --------------------------------------------------------------------- - return {} + return None # ============================================================================= -class S3TwitterModel(S3Model): +class S3TwitterModel(DataModel): names = ("msg_twitter_channel", "msg_twitter", @@ -2081,15 +2081,15 @@ def model(self): super_entity = "msg_channel", ) - set_method("msg", "twitter_channel", + set_method("msg_twitter_channel", method = "enable", action = self.msg_channel_enable_interactive) - set_method("msg", "twitter_channel", + set_method("msg_twitter_channel", method = "disable", action = self.msg_channel_disable_interactive) - set_method("msg", "twitter_channel", + set_method("msg_twitter_channel", method = "poll", action = self.msg_channel_poll) @@ -2144,7 +2144,7 @@ def model(self): ) # --------------------------------------------------------------------- - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -2310,15 +2310,15 @@ def model(self): ), ) - set_method("msg", "twitter_search", + set_method("msg_twitter_search", method = "poll", action = self.twitter_search_poll) - set_method("msg", "twitter_search", + set_method("msg_twitter_search", method = "keygraph", action = self.twitter_keygraph) - set_method("msg", "twitter_result", + set_method("msg_twitter_result", method = "timeline", action = self.twitter_timeline) @@ -2376,7 +2376,7 @@ def model(self): ) # --------------------------------------------------------------------- - return {} + return None # ----------------------------------------------------------------------------- @staticmethod @@ -2507,7 +2507,7 @@ def twitter_timeline(r, **attr): else: r.error(405, current.ERROR.BAD_METHOD) # ============================================================================= -class S3XFormsModel(S3Model): +class S3XFormsModel(DataModel): """ XForms are used by the ODK Collect mobile client @@ -2532,10 +2532,10 @@ def model(self): ) # --------------------------------------------------------------------- - return {} + return None # ============================================================================= -class S3BaseStationModel(S3Model): +class S3BaseStationModel(DataModel): """ Base Stations (Cell Towers) are a type of Site @@ -2608,6 +2608,6 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # END ========================================================================= diff --git a/modules/s3db/org.py b/modules/s3db/org.py index b35e1a8e2b..de4fa161ad 100644 --- a/modules/s3db/org.py +++ b/modules/s3db/org.py @@ -98,7 +98,7 @@ SEPARATORS = (",", ":") # ============================================================================= -class OrgOrganisationModel(S3Model): +class OrgOrganisationModel(DataModel): """ Organisations """ @@ -662,7 +662,7 @@ def model(self): ) # Custom Method for S3OrganisationAutocompleteWidget - self.set_method("org", "organisation", + self.set_method("org_organisation", method = "search_ac", action = self.org_search_ac) @@ -1008,7 +1008,7 @@ def org_search_ac(r, **attr): JSON search method for S3OrganisationAutocompleteWidget - searches name & acronym for both this organisation & the parent of branches - @param r: the S3Request + @param r: the CRUDRequest @param attr: request attributes """ @@ -1130,7 +1130,7 @@ def org_search_ac(r, **attr): return json.dumps(output, separators=SEPARATORS) # ============================================================================= -class OrgOrganisationNameModel(S3Model): +class OrgOrganisationNameModel(DataModel): """ Organsiation Names model - local names/acronyms for Organisations @@ -1169,10 +1169,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class OrgOrganisationBranchModel(S3Model): +class OrgOrganisationBranchModel(DataModel): """ Organisation Branches """ @@ -1377,7 +1377,7 @@ def org_branch_ondelete(row): org_update_affiliations("org_organisation_branch", record) # ============================================================================= -class OrgOrganisationCapacityModel(S3Model): +class OrgOrganisationCapacityModel(DataModel): """ (Branch) Organisational Capacity Assessment - Flexible Questions (Dynamic Data Model) @@ -1467,16 +1467,16 @@ def model(self): ) # Custom Report Method - self.set_method("org", "capacity_assessment_data", + self.set_method("org_capacity_assessment_data", method = "custom_report", action = org_CapacityReport()) # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class OrgOrganisationGroupModel(S3Model): +class OrgOrganisationGroupModel(DataModel): """ Organisation Group Model - 'Coalitions' or 'Networks' @@ -1615,7 +1615,7 @@ def model(self): ) # Custom Method to Assign Orgs - self.set_method("org", "group", + self.set_method("org_group", method = "assign", action = org_AssignMethod(component="membership")) @@ -1728,7 +1728,7 @@ def group_membership_onaccept(form): org_update_affiliations("org_group_membership", record) # ============================================================================= -class OrgOrganisationGroupPersonModel(S3Model): +class OrgOrganisationGroupPersonModel(DataModel): """ Link table between Organisation Groups & Persons """ @@ -1809,10 +1809,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class OrgOrganisationGroupTeamModel(S3Model): +class OrgOrganisationGroupTeamModel(DataModel): """ Link table between Organisation Groups & Teams """ @@ -1846,7 +1846,7 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -1882,7 +1882,7 @@ def org_group_team_onaccept(form): ) # ============================================================================= -class OrgOrganisationLocationModel(S3Model): +class OrgOrganisationLocationModel(DataModel): """ Organisation Location Model - Locations served by an Organisation @@ -1930,10 +1930,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class OrgOrganisationOrganisationModel(S3Model): +class OrgOrganisationOrganisationModel(DataModel): """ Link table between Organisations & Organisations - can be used to provide non-hierarchical relationships @@ -1978,7 +1978,7 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -2004,7 +2004,7 @@ def org_organisation_organisation_realm_entity(table, row): return None # ============================================================================= -class OrgOrganisationResourceModel(S3Model): +class OrgOrganisationResourceModel(DataModel): """ Organisation Resource Model - depends on Stats module @@ -2019,7 +2019,7 @@ def model(self): #settings = current.deployment_settings if not current.deployment_settings.has_module("stats"): current.log.warning("Organisation Resource Model needs Stats module enabling") - return {} + return None T = current.T #auth = current.auth @@ -2161,10 +2161,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class OrgOrganisationSectorModel(S3Model): +class OrgOrganisationSectorModel(DataModel): """ Organisation Sector Model """ @@ -2489,7 +2489,7 @@ def org_sector_onaccept(form): # return current.messages.UNKNOWN_OPT # ============================================================================= -class OrgServiceModel(S3Model): +class OrgServiceModel(DataModel): """ Organisation Service Model """ @@ -3062,7 +3062,7 @@ def descendants(ids): return new_root # ============================================================================= -class OrgOrganisationTagModel(S3Model): +class OrgOrganisationTagModel(DataModel): """ Organisation Tags """ @@ -3104,10 +3104,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class OrgOrganisationTeamModel(S3Model): +class OrgOrganisationTeamModel(DataModel): """ Link table between Organisations & Teams """ @@ -3139,7 +3139,7 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -3170,7 +3170,7 @@ def organisation_team_ondelete(row): org_update_affiliations("org_organisation_team", row.id) # ============================================================================= -class OrgOrganisationTypeTagModel(S3Model): +class OrgOrganisationTypeTagModel(DataModel): """ Organisation Type Tags """ @@ -3209,10 +3209,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class OrgSiteModel(S3Model): +class OrgSiteModel(DataModel): """ Site Super-Entity """ @@ -3303,19 +3303,19 @@ def model(self): ) # Custom Method for S3SiteAutocompleteWidget - set_method("org", "site", + set_method("org_site", method = "search_ac", action = self.site_search_ac) # Custom Method for S3AddPersonWidget # @ToDo: One for HRMs - set_method("org", "site", + set_method("org_site", method = "site_contact_person", action = self.site_contact_person) # Custom Method to Assign HRs # - done in instances - #set_method("org", "site", + #set_method("org_site", # method = "assign", # action = self.hrm_AssignMethod(component="human_resource_site")) @@ -3699,7 +3699,7 @@ def site_search_ac(r, **attr): """ JSON search method for S3SiteAutocompleteWidget - @param r: the S3Request + @param r: the CRUDRequest @param attr: request attributes """ @@ -3796,7 +3796,7 @@ def site_search_ac(r, **attr): return json.dumps(output, separators=SEPARATORS) # ============================================================================= -class OrgSiteDetailsModel(S3Model): +class OrgSiteDetailsModel(DataModel): """ Extra optional details for Sites """ names = ("org_site_status", @@ -3904,7 +3904,7 @@ def model(self): } # ============================================================================= -class OrgSiteEventModel(S3Model): +class OrgSiteEventModel(DataModel): """ Events for Sites - Check-In/Check-Out @@ -3966,10 +3966,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class OrgSiteGroupModel(S3Model): +class OrgSiteGroupModel(DataModel): """ Link Sites to Org Groups """ names = ("org_site_org_group", @@ -3990,10 +3990,10 @@ def model(self): *s3_meta_fields()) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class OrgSiteNameModel(S3Model): +class OrgSiteNameModel(DataModel): """ Site Names model - local names/acronyms for Sites/Facilities @@ -4028,10 +4028,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class OrgSiteShiftModel(S3Model): +class OrgSiteShiftModel(DataModel): """ Site Shifts model """ @@ -4070,10 +4070,10 @@ def model(self): # ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class OrgSiteTagModel(S3Model): +class OrgSiteTagModel(DataModel): """ Site Tags """ @@ -4118,10 +4118,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class OrgSiteLocationModel(S3Model): +class OrgSiteLocationModel(DataModel): """ Site Location Model - Locations served by a Site/Facility @@ -4183,10 +4183,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class OrgFacilityModel(S3Model): +class OrgFacilityModel(DataModel): """ Generic Site """ @@ -4614,7 +4614,7 @@ def model(self): ) # Custom Method to Assign HRs - self.set_method("org", "facility", + self.set_method("org_facility", method = "assign", action = self.hrm_AssignMethod(component="human_resource_site")) @@ -4852,7 +4852,7 @@ def org_facility_rheader(r, tabs=None): return rheader # ============================================================================= -class OrgRoomModel(S3Model): +class OrgRoomModel(DataModel): """ Rooms are a location within a Site - used by Asset module @@ -4935,7 +4935,7 @@ def model(self): } # ============================================================================= -class OrgOfficeModel(S3Model): +class OrgOfficeModel(DataModel): names = ("org_office", "org_office_type", @@ -5267,7 +5267,7 @@ def org_office_onaccept(form): org_update_affiliations("org_office", form.vars) # ============================================================================= -class OrgOfficeTypeTagModel(S3Model): +class OrgOfficeTypeTagModel(DataModel): """ Office Type Tags """ @@ -5308,7 +5308,7 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= def org_organisation_address(row): @@ -5996,7 +5996,7 @@ def apply_method(self, r, **attr): """ Entry point for the REST API - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller parameters """ @@ -6023,7 +6023,7 @@ def check_in_form(self, r, **attr): """ Render the check-in page - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller parameters """ @@ -6189,7 +6189,7 @@ def submit_ajax(self, r, **attr): l: the PE label } - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller parameters """ @@ -6348,7 +6348,7 @@ def status(r, person): invokes the check_in_status hook for the site resource to obtain current status information. - @param r: the S3Request + @param r: the CRUDRequest @param site_id: the site ID @param person: the person record @@ -6447,7 +6447,7 @@ def check_in(r, person): Check-in the person at this site, invokes the site_check_in hook for the site resource - @param r: the S3Request + @param r: the CRUDRequest @param person: the person record """ @@ -6482,7 +6482,7 @@ def check_out(r, person): Check-out the person from this site, invokes the site_check_out hook for the site resource - @param r: the S3Request + @param r: the CRUDRequest @param person: the person record """ @@ -7236,19 +7236,14 @@ def postp(r, output): return output s3.postp = postp - output = current.rest_controller("org", "organisation", - # Need to be explicit since can also come from HRM or Project controllers - csv_stylesheet = ("org", "organisation.xsl"), - csv_template = ("org", "organisation"), - # Don't allow components with components (such as document) to breakout from tabs - native = False, - rheader = org_rheader, - ) - return output - - - - + return current.crud_controller("org", "organisation", + # Need to be explicit since can also come from HRM or Project controllers + csv_stylesheet = ("org", "organisation.xsl"), + csv_template = ("org", "organisation"), + # Don't allow components with components (such as document) to breakout from tabs + native = False, + rheader = org_rheader, + ) # ----------------------------------------------------------------------------- def org_organisation_organisation_onaccept(form): @@ -7549,12 +7544,11 @@ def postp(r, output): return output s3.postp = postp - output = current.rest_controller("org", "office", - # Don't allow components with components (such as document) to breakout from tabs - native = False, - rheader = org_rheader, - ) - return output + return current.crud_controller("org", "office", + # Don't allow components with components (such as document) to breakout from tabs + native = False, + rheader = org_rheader, + ) # ============================================================================= def org_facility_controller(): @@ -7812,10 +7806,7 @@ def postp(r, output): return output s3.postp = postp - output = current.rest_controller("org", "facility", - rheader = org_rheader, - ) - return output + return current.crud_controller("org", "facility", rheader=org_rheader) # ============================================================================= # Hierarchy Manipulation @@ -8217,7 +8208,7 @@ def duplicate(cls, item): """ Main method, to be set for the "deduplicate" hook - @param item: the S3ImportItem + @param item: the ImportItem """ try: @@ -8434,7 +8425,7 @@ def apply_method(self, r, **attr): """ Apply method. - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller options for this request """ @@ -8594,7 +8585,7 @@ def apply_method(self, r, **attr): if filter_widgets: # Where to retrieve filtered data from: - _vars = resource.crud._remove_filters(r.get_vars) + _vars = S3Method._remove_filters(r.get_vars) filter_submit_url = r.url(vars=_vars) # Where to retrieve updated filter options from: @@ -8662,7 +8653,7 @@ def apply_method(self, r, **attr): """ Apply method. - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller options for this request """ @@ -8748,7 +8739,7 @@ def _extract(r): """ Method to read the data - @param r: the S3Request + @param r: the CRUDRequest """ # Read all the permitted data @@ -8826,8 +8817,8 @@ def _xls(data): try: import xlwt except ImportError: - from core.io.codecs.xls import S3XLS - if current.auth.permission.format in S3Request.INTERACTIVE_FORMATS: + from core.resource.codecs.xls import S3XLS + if current.auth.permission.format in CRUDRequest.INTERACTIVE_FORMATS: current.session.error = S3XLS.ERROR.XLWT_ERROR redirect(URL(extension="")) else: diff --git a/modules/s3db/patient.py b/modules/s3db/patient.py index 6db422ad72..bddefb8cad 100644 --- a/modules/s3db/patient.py +++ b/modules/s3db/patient.py @@ -35,7 +35,7 @@ from ..core import * # ============================================================================= -class S3PatientModel(S3Model): +class S3PatientModel(DataModel): """ """ @@ -230,7 +230,7 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= class patient_PatientRepresent(S3Represent): diff --git a/modules/s3db/pr.py b/modules/s3db/pr.py index 1a1547d648..3bed7e50c1 100644 --- a/modules/s3db/pr.py +++ b/modules/s3db/pr.py @@ -146,7 +146,7 @@ SEPARATORS = (",", ":") # ============================================================================= -class PRPersonEntityModel(S3Model): +class PRPersonEntityModel(DataModel): """ Person Super-Entity """ names = ("pr_pentity", @@ -326,7 +326,7 @@ def model(self): ) # Custom Method for S3AutocompleteWidget - self.set_method("pr", "pentity", + self.set_method("pr_pentity", method = "search_ac", action = self.pe_search_ac) @@ -473,7 +473,7 @@ def pe_search_ac(r, **attr): """ JSON search method for S3AutocompleteWidget - @param r: the S3Request + @param r: the CRUDRequest @param attr: request attributes """ @@ -740,7 +740,7 @@ def pr_affiliation_ondelete(row): return # ============================================================================= -class PRPersonModel(S3Model): +class PRPersonModel(DataModel): """ Persons and Groups """ names = ("pr_person", @@ -1014,25 +1014,25 @@ def model(self): # Custom Methods for S3PersonAutocompleteWidget and S3AddPersonWidget set_method = self.set_method - set_method("pr", "person", + set_method("pr_person", method = "search_ac", action = self.pr_search_ac, ) - set_method("pr", "person", + set_method("pr_person", method = "lookup", action = self.pr_person_lookup) - set_method("pr", "person", + set_method("pr_person", method = "check_duplicates", action = self.pr_person_check_duplicates) # Enable in templates as-required - #set_method("pr", "person", + #set_method("pr_person", # method = "templates", # action = pr_Templates()) - #set_method("pr", "person", + #set_method("pr_person", # method = "template", # action = pr_Template()) @@ -1854,7 +1854,7 @@ def pr_person_lookup(r, **attr): r.error(400, "No Record ID provided") # NB Requirement to identify a single record also (indirectly) - # requires read permission to that record (=>S3Request). + # requires read permission to that record (=>CRUDRequest). # # However: that does not imply that the user also has # permission to see person details or contact information, @@ -2387,7 +2387,7 @@ def pr_person_check_duplicates(r, **attr): return output # ============================================================================= -class PRPersonRelationModel(S3Model): +class PRPersonRelationModel(DataModel): """ Link table between Persons & Persons - can be used to provide non-hierarchical relationships @@ -2425,10 +2425,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class PRGroupModel(S3Model): +class PRGroupModel(DataModel): """ Groups """ names = ("pr_group_status", @@ -3149,7 +3149,7 @@ def group_membership_onaccept(form): row = db(query).select(ctable.id, limitby=(0, 1)).first() if not row: # Customise case resource - r = S3Request("dvr", "case", current.request) + r = CRUDRequest("dvr", "case", current.request) r.customise_resource("dvr_case") # Get the default case status from database @@ -3191,7 +3191,7 @@ def group_membership_realm_entity(table, row): return None # ============================================================================= -class PRGroupCompetencyModel(S3Model): +class PRGroupCompetencyModel(DataModel): """ Group Competency Model - Skills available in a Group @@ -3236,10 +3236,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class PRGroupLocationModel(S3Model): +class PRGroupLocationModel(DataModel): """ Group Location Model - Locations served by a Group @@ -3287,10 +3287,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class PRGroupTagModel(S3Model): +class PRGroupTagModel(DataModel): """ Group Tags """ @@ -3321,10 +3321,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class PRForumModel(S3Model): +class PRForumModel(DataModel): """ Forums - similar to Groups, they are collections of People, however these are restricted to those with User Accounts @@ -3432,19 +3432,19 @@ def model(self): ) # Custom Methods - set_method("pr", "forum", + set_method("pr_forum", method = "assign", action = pr_AssignMethod(component = "forum_membership")) - set_method("pr", "forum", + set_method("pr_forum", method = "join", action = self.pr_forum_join) - set_method("pr", "forum", + set_method("pr_forum", method = "leave", action = self.pr_forum_leave) - set_method("pr", "forum", + set_method("pr_forum", method = "request", action = self.pr_forum_request) @@ -3695,7 +3695,7 @@ def pr_forum_request(r, **attr): redirect(URL(args=None)) # ============================================================================= -class PRRealmModel(S3Model): +class PRRealmModel(DataModel): """ Realms - used to be able to share data across multiple realms @@ -3731,10 +3731,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class PRAddressModel(S3Model): +class PRAddressModel(DataModel): """ Addresses for Person Entities: Persons and Organisations """ names = ("pr_address", @@ -3996,7 +3996,7 @@ def pr_address_onaccept(form): db(mtable.id == member.id).update(location_id=location_id) # ============================================================================= -class PRContactModel(S3Model): +class PRContactModel(DataModel): """ Person Entity Contacts - for Persons, Groups, Organisations and Organisation Groups @@ -4269,7 +4269,7 @@ def pr_contact_onvalidation(form): return # ============================================================================= -class PRImageModel(S3Model): +class PRImageModel(DataModel): """ Images for Persons """ names = ("pr_image",) @@ -4407,7 +4407,7 @@ def cb(): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -4514,7 +4514,7 @@ def pr_image_ondelete(row): current.s3db.pr_image_delete_all(row.image) # ============================================================================= -class PRPresenceModel(S3Model): +class PRPresenceModel(DataModel): """ Presence Log for Persons @@ -4864,7 +4864,7 @@ def presence_onaccept(form): db(db.pr_person.pe_id == pe_id).update(missing = False) # ============================================================================= -class PRAvailabilityModel(S3Model): +class PRAvailabilityModel(DataModel): """ Availability for Persons, Sites, Services, Assets, etc - will allow for automated rostering/matching @@ -5199,7 +5199,7 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -5305,7 +5305,7 @@ def availability_onaccept(form): db(table.id == record_id).update(**data) # ============================================================================= -class PRUnavailabilityModel(S3Model): +class PRUnavailabilityModel(DataModel): """ Allow people to mark times when they are unavailable - this is generally easier for longer-term volunteers than marking times @@ -5360,10 +5360,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class PRDescriptionModel(S3Model): +class PRDescriptionModel(DataModel): """ Additional tables used mostly for DVI/MPR """ @@ -5797,7 +5797,7 @@ def note_onaccept(form): return # ============================================================================= -class PREducationModel(S3Model): +class PREducationModel(DataModel): """ Education details for Persons """ names = ("pr_education_level", @@ -5981,10 +5981,10 @@ def model(self): # --------------------------------------------------------------------- # Return model-global names to response.s3 # - return {} + return None # ============================================================================= -class PRIdentityModel(S3Model): +class PRIdentityModel(DataModel): """ Identities for Persons """ names = ("pr_identity",) @@ -6107,10 +6107,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class PRLanguageModel(S3Model): +class PRLanguageModel(DataModel): """ Languages for Persons - alternate model to Skills for alternate UX @@ -6174,10 +6174,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class PROccupationModel(S3Model): +class PROccupationModel(DataModel): """ Model for a person's current occupations, catalog-based alternative to the free-text pr_person_details.occupation @@ -6266,10 +6266,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class PRPersonDetailsModel(S3Model): +class PRPersonDetailsModel(DataModel): """ Extra optional details for People """ names = ("pr_person_details", @@ -6496,7 +6496,7 @@ def model(self): } # ============================================================================= -class PRPersonLocationModel(S3Model): +class PRPersonLocationModel(DataModel): """ Person Location Model - Locations served by a Person @@ -6544,10 +6544,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class PRPersonTagModel(S3Model): +class PRPersonTagModel(DataModel): """ Person Tags """ @@ -6578,10 +6578,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class PRReligionModel(S3Model): +class PRReligionModel(DataModel): """ Model for religions - alternative for the simple religion field for when a full hiearchy is @@ -6699,10 +6699,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class S3ImageLibraryModel(S3Model): +class S3ImageLibraryModel(DataModel): """ Image Model @@ -6787,7 +6787,7 @@ def pr_image_delete_all(original_image_name): dbset.delete() # ============================================================================= -class S3SavedFilterModel(S3Model): +class S3SavedFilterModel(DataModel): """ Saved Filters """ names = ("pr_filter", @@ -6864,7 +6864,7 @@ def pr_filter_onvalidation(form): form.vars.query = query # ============================================================================= -class S3SubscriptionModel(S3Model): +class S3SubscriptionModel(DataModel): """ Model for Subscriptions & hence Notifications http://eden.sahanafoundation.org/wiki/S3/Notifications @@ -8093,7 +8093,7 @@ def apply_method(self, r, **attr): """ Apply method. - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller options for this request """ @@ -8313,7 +8313,7 @@ def apply_method(self, r, **attr): if filter_widgets: # Where to retrieve filtered data from: - submit_url_vars = resource.crud._remove_filters(r.get_vars) + submit_url_vars = S3Method._remove_filters(r.get_vars) filter_submit_url = r.url(vars = submit_url_vars) # Default Filters (before selecting data!) @@ -8498,7 +8498,7 @@ def apply_method(self, r, **attr): """ Entry point for REST API - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller parameters for the request """ @@ -8609,7 +8609,7 @@ def contacts(self, r, pe_id, allow_create=False, method="contacts"): """ Contact Information Subform - @param r: the S3Request + @param r: the CRUDRequest @param pe_id: the pe_id @param allow_create: allow adding of new contacts @param method: the request method ("contacts", "private_contacts" @@ -8864,7 +8864,7 @@ def apply_method(self, r, **attr): """ Apply method. - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller options for this request """ @@ -8924,7 +8924,7 @@ def apply_method(self, r, **attr): """ Apply method. - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller options for this request """ @@ -10285,7 +10285,7 @@ def pr_availability_filter(r): - called from prep of the respective controller - adds resource filter for r.resource - @param r: the S3Request + @param r: the CRUDRequest """ get_vars = r.get_vars @@ -10328,7 +10328,7 @@ def pr_availability_filter(r): r.resource.add_filter(~(FS("id").belongs(person_ids))) # ============================================================================= -def pr_import_prep(data): +def pr_import_prep(tree): """ Called when contacts are imported from CSV @@ -10346,8 +10346,6 @@ def pr_import_prep(data): update_super = s3db.update_super table = s3db.org_organisation - tree = data[1] - # Memberships elements = tree.getroot().xpath("/s3xml//resource[@name='pr_contact']/data[@field='pe_id']") looked_up = {} @@ -10895,7 +10893,7 @@ def apply_method(self, r, **attr): """ Entry point for REST controller - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller parameters for the request """ diff --git a/modules/s3db/proc.py b/modules/s3db/proc.py index 0888c2dfa5..fe5c206bc4 100644 --- a/modules/s3db/proc.py +++ b/modules/s3db/proc.py @@ -48,7 +48,7 @@ from ..core import * # ============================================================================= -class S3ProcurementPlansModel(S3Model): +class S3ProcurementPlansModel(DataModel): """ Procurement Plans @@ -244,7 +244,7 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -272,7 +272,7 @@ def proc_plan_represent(plan_id, row=None): return current.messages.UNKNOWN_OPT # ============================================================================= -class S3PurchaseOrdersModel(S3Model): +class S3PurchaseOrdersModel(DataModel): """ Purchase Orders (PO) diff --git a/modules/s3db/project.py b/modules/s3db/project.py index 71677fae4c..aaa029d70a 100644 --- a/modules/s3db/project.py +++ b/modules/s3db/project.py @@ -101,7 +101,7 @@ SEPARATORS = (",", ":") # ============================================================================= -class ProjectModel(S3Model): +class ProjectModel(DataModel): """ Project Model @@ -535,39 +535,39 @@ def model(self): ) # Custom Methods - set_method("project", "project", + set_method("project_project", method = "assign", action = self.hrm_AssignMethod(component="human_resource")) - set_method("project", "project", + set_method("project_project", method = "details", action = project_Details) - set_method("project", "project", + set_method("project_project", method = "map", action = self.project_map) - set_method("project", "project", + set_method("project_project", method = "timeline", action = self.project_timeline) - set_method("project", "project", + set_method("project_project", method = "summary_report", action = project_SummaryReport) - set_method("project", "project", + set_method("project_project", method = "indicator_summary_report", action = project_IndicatorSummaryReport) - set_method("project", "project", + set_method("project_project", method = "project_progress_report", action = project_ProgressReport) - #set_method("project", "project", + #set_method("project_project", # method = "budget_progress_report", # action = project_BudgetProgressReport) - #set_method("project", "project", + #set_method("project_project", # method = "indicator_progress_report", # action = project_IndicatorProgressReport) @@ -1091,7 +1091,7 @@ def project_timeline(r, **attr): r.error(405, current.ERROR.BAD_METHOD) # ============================================================================= -class ProjectActivityModel(S3Model): +class ProjectActivityModel(DataModel): """ Project Activity Model @@ -1401,7 +1401,7 @@ def model(self): # This component no longer has a case_id in it #if settings.has_module("dvr"): # # Custom Method to Assign Cases - # self.set_method("project", "activity", + # self.set_method("project_activity", # method = "assign", # action = self.dvr_AssignMethod(component="case_activity"), # ) @@ -1699,7 +1699,7 @@ def project_activity_realm_entity(table, record): return None # ============================================================================= -class ProjectActivityTypeModel(S3Model): +class ProjectActivityTypeModel(DataModel): """ Project Activity Type Model @@ -1858,7 +1858,7 @@ def model(self): } # ============================================================================= -class ProjectActivityPersonModel(S3Model): +class ProjectActivityPersonModel(DataModel): """ Project Activity Person Model @@ -1920,10 +1920,10 @@ def model(self): *s3_meta_fields()) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class ProjectActivityOrganisationModel(S3Model): +class ProjectActivityOrganisationModel(DataModel): """ Project Activity Organisation Model @@ -1987,10 +1987,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class ProjectActivityOrganisationGroupModel(S3Model): +class ProjectActivityOrganisationGroupModel(DataModel): """ Project Activity Organisation Group Model @@ -2027,10 +2027,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class ProjectActivityDemographicsModel(S3Model): +class ProjectActivityDemographicsModel(DataModel): """ Project Activity Demographics Model @@ -2093,10 +2093,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class ProjectActivityItemModel(S3Model): +class ProjectActivityItemModel(DataModel): """ Project Activity Item Model @@ -2156,10 +2156,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class ProjectActivitySectorModel(S3Model): +class ProjectActivitySectorModel(DataModel): """ Project Activity Sector Model @@ -2194,10 +2194,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class ProjectActivityTagModel(S3Model): +class ProjectActivityTagModel(DataModel): """ Activity Tags """ @@ -2237,10 +2237,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class ProjectAnnualBudgetModel(S3Model): +class ProjectAnnualBudgetModel(DataModel): """ Project Budget Model @@ -2313,10 +2313,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class ProjectBeneficiaryModel(S3Model): +class ProjectBeneficiaryModel(DataModel): """ Project Beneficiary Model - depends on Stats module @@ -2333,7 +2333,7 @@ def model(self): if not current.deployment_settings.has_module("stats"): current.log.warning("Project Beneficiary Model needs Stats module enabling") #return self.defaults() - return {} + return None T = current.T db = current.db @@ -2683,7 +2683,7 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -2738,7 +2738,7 @@ def project_beneficiary_onaccept(form): ) # ============================================================================= -class ProjectCampaignModel(S3Model): +class ProjectCampaignModel(DataModel): """ Project Campaign Model - used for TERA integration: @@ -2758,7 +2758,7 @@ def model(self): if not current.deployment_settings.has_module("stats"): # Campaigns Model needs Stats module enabling #return self.defaults() - return {} + return None T = current.T db = current.db @@ -3016,10 +3016,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class ProjectFrameworkModel(S3Model): +class ProjectFrameworkModel(DataModel): """ Project Framework Model """ @@ -3160,10 +3160,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class ProjectHazardModel(S3Model): +class ProjectHazardModel(DataModel): """ Project Hazard Model """ @@ -3261,7 +3261,7 @@ def model(self): } # ============================================================================= -class ProjectHRModel(S3Model): +class ProjectHRModel(DataModel): """ Optionally link Projects <> Human Resources """ @@ -3333,7 +3333,7 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -3358,7 +3358,7 @@ def project_human_resource_onvalidation(form): form.errors.human_resource_id = current.T("Record already exists") # ============================================================================= -class ProjectIndicatorModel(S3Model): +class ProjectIndicatorModel(DataModel): """ Project Indicator Model - depends on Stats module @@ -3374,7 +3374,7 @@ def model(self): if not current.deployment_settings.has_module("stats"): current.log.warning("Project Indicator Model needs Stats module enabling") #return self.defaults() - return {} + return None T = current.T db = current.db @@ -3622,10 +3622,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class ProjectL10nModel(S3Model): +class ProjectL10nModel(DataModel): """ Project L10n Model @@ -3648,10 +3648,10 @@ def model(self): *s3_meta_fields()) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class ProjectLocationModel(S3Model): +class ProjectLocationModel(DataModel): """ Project Location Model - these can simply be ways to display a Project on the Map @@ -4076,7 +4076,7 @@ def project_location_contact_onaccept(form): person.update_record(realm_entity = realm_entity) # ============================================================================= -class ProjectMasterKeyModel(S3Model): +class ProjectMasterKeyModel(DataModel): """ Link Projects to Master Keys for Mobile Data Entry """ @@ -4100,10 +4100,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class ProjectOrganisationModel(S3Model): +class ProjectOrganisationModel(DataModel): """ Project Organisation Model """ @@ -4218,7 +4218,7 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -4336,7 +4336,7 @@ def project_organisation_realm_entity(table, record): return None # ============================================================================= -class ProjectPlanningModel(S3Model): +class ProjectPlanningModel(DataModel): """ Project Planning Model: Goals (Objectives) @@ -7130,7 +7130,7 @@ def apply_method(self, r, **attr): """ Entry point for REST API - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller arguments """ @@ -8336,7 +8336,7 @@ def pdf(self, r, **attr): -the actual report """ - from core.io.codecs.pdf import EdenDocTemplate, S3RL_PDF + from core.resource.codecs.pdf import EdenDocTemplate, S3RL_PDF T = current.T db = current.db @@ -8696,7 +8696,7 @@ def apply_method(self, r, **attr): """ Entry point for REST API - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller arguments """ @@ -9089,7 +9089,7 @@ def xls(self, r, **attr): XLS Representation """ - from core.io.codecs.xls import S3XLS + from core.resource.codecs.xls import S3XLS try: import xlwt @@ -9827,7 +9827,7 @@ def project_ProgressReport(r, **attr): # r.error(405, current.ERROR.BAD_METHOD) # ============================================================================= -class ProjectProgrammeModel(S3Model): +class ProjectProgrammeModel(DataModel): """ Programmes Model """ @@ -9942,7 +9942,7 @@ def defaults(): } # ============================================================================= -class ProjectProgrammeProjectModel(S3Model): +class ProjectProgrammeProjectModel(DataModel): """ Project Programme<>Project Model """ @@ -9964,10 +9964,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class ProjectSectorModel(S3Model): +class ProjectSectorModel(DataModel): """ Project Sector Model """ @@ -10007,10 +10007,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class ProjectStatusModel(S3Model): +class ProjectStatusModel(DataModel): """ Project Status Model - used by both Projects & Activities @@ -10085,7 +10085,7 @@ def defaults(self): } # ============================================================================= -class ProjectStrategyModel(S3Model): +class ProjectStrategyModel(DataModel): """ Project Strategy Model - currently just used by IFRC to hold AoF/SFI (& then only for (Training) Events for Bangkok CCST) @@ -10161,7 +10161,7 @@ def defaults(): } # ============================================================================= -class ProjectTagModel(S3Model): +class ProjectTagModel(DataModel): """ Project Tags """ @@ -10201,10 +10201,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class ProjectThemeModel(S3Model): +class ProjectThemeModel(DataModel): """ Project Theme Model """ @@ -10502,7 +10502,7 @@ def project_theme_project_onaccept(form): percentage = percentages[theme_id]) # ============================================================================= -class ProjectDRRModel(S3Model): +class ProjectDRRModel(DataModel): """ Models for DRR (Disaster Risk Reduction) extensions """ @@ -10534,7 +10534,7 @@ def model(self): *s3_meta_fields()) # Pass names back to global scope (s3.*) - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -10555,7 +10555,7 @@ def hfa_opts_represent(opt): return ", ".join(vals) # ============================================================================= -class ProjectDRRPPModel(S3Model): +class ProjectDRRPPModel(DataModel): """ Models for DRR Project Portal extensions - injected into custom Project CRUD forms @@ -10709,7 +10709,7 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -10776,7 +10776,7 @@ def opts_represent(opt, prefix): return current.messages["NONE"] # ============================================================================= -class ProjectTargetModel(S3Model): +class ProjectTargetModel(DataModel): """ Project Target Model """ @@ -10816,10 +10816,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class ProjectTaskModel(S3Model): +class ProjectTaskModel(DataModel): """ Project Task Model @@ -11294,15 +11294,15 @@ def model(self): project_task_represent_w_project = project_TaskRepresent(show_project=True) # Custom Methods - set_method("project", "task", + set_method("project_task", method = "share", action = self.project_task_share) - set_method("project", "task", + set_method("project_task", method = "unshare", action = self.project_task_unshare) - set_method("project", "task", + set_method("project_task", method = "dispatch", action = self.project_task_dispatch) @@ -12186,7 +12186,7 @@ def project_time_onaccept(form): db(query).update(time_actual = hours) # ============================================================================= -class ProjectTaskForumModel(S3Model): +class ProjectTaskForumModel(DataModel): """ Shares for Tasks """ @@ -12223,10 +12223,10 @@ def model(self): # msg_list_empty = T("No Tasks currently shared")) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class ProjectTaskHRMModel(S3Model): +class ProjectTaskHRMModel(DataModel): """ Project Task HRM Model @@ -12272,10 +12272,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class ProjectTaskTagModel(S3Model): +class ProjectTaskTagModel(DataModel): """ Task Tags """ @@ -12314,10 +12314,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class ProjectWindowModel(S3Model): +class ProjectWindowModel(DataModel): """ Project Window Model @@ -12349,7 +12349,7 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= def multi_theme_percentage_represent(record_id): @@ -13499,7 +13499,7 @@ def postp(r, output): else: hide_filter = None - return current.rest_controller("project", "task", + return current.crud_controller("project", "task", hide_filter = hide_filter, rheader = s3db.project_rheader, ) @@ -14127,7 +14127,7 @@ def apply_method(self, r, **attr): """ Entry point for REST API - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller arguments """ diff --git a/modules/s3db/req.py b/modules/s3db/req.py index 0d8f525629..81c64a6a9c 100644 --- a/modules/s3db/req.py +++ b/modules/s3db/req.py @@ -169,7 +169,7 @@ def req_timeframe(): ) # ============================================================================= -class RequestModel(S3Model): +class RequestModel(DataModel): """ Model for Requests """ @@ -684,28 +684,28 @@ def model(self): ) # Custom Methods - set_method("req", "req", + set_method("req_req", method = "check", action = req_CheckMethod()) - set_method("req", "req", + set_method("req_req", method = "commit_all", action = self.req_commit_all) - set_method("req", "req", + set_method("req_req", method = "copy_all", action = self.req_copy_all) - set_method("req", "req", + set_method("req_req", method = "submit", action = self.req_submit) - set_method("req", "req", + set_method("req_req", method = "approve_req", # Don't clash with core approve method action = self.req_approve) # Print Forms - set_method("req", "req", + set_method("req_req", method = "form", action = self.req_form) @@ -1499,7 +1499,7 @@ def req_onaccept(form): req_status = record.req_status if req_status is not None: status_requires = table.req_status.requires - if status_requires.hasattr("other"): + if hasattr(status_requires, "other"): status_requires = status_requires.other opts = [opt[0] for opt in status_requires.options()] if str(REQ_STATUS_CANCEL) in opts: @@ -1662,7 +1662,7 @@ def req_req_ondelete(row): db(query).delete() # ============================================================================= -class RequestApproverModel(S3Model): +class RequestApproverModel(DataModel): """ Model for request approvers """ @@ -1746,10 +1746,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class RequestItemModel(S3Model): +class RequestItemModel(DataModel): """ Model for requested items """ @@ -2100,7 +2100,7 @@ def req_item_duplicate(item): This callback will be called when importing records. It will look to see if the record being imported is a duplicate. - @param item: An S3ImportItem object which includes all the details + @param item: An ImportItem object which includes all the details of the record being imported If the record is a duplicate then it will set the item method to update @@ -2146,7 +2146,7 @@ def req_item_duplicate(item): item.method = item.METHOD.UPDATE # ============================================================================= -class RequestSkillModel(S3Model): +class RequestSkillModel(DataModel): """ Modell for requested skills """ @@ -2433,7 +2433,7 @@ def req_skill_represent(record_id): return current.messages.UNKNOWN_OPT # ============================================================================= -class RequestRecurringModel(S3Model): +class RequestRecurringModel(DataModel): """ Adjuvant model to support request generation by scheduler """ @@ -2469,14 +2469,14 @@ def model(self): msg_no_match = T("No jobs configured")) # Custom Methods - self.set_method("req", "req", - component_name = "job", + self.set_method("req_req", + component = "job", method = "reset", action = req_job_reset, ) - self.set_method("req", "req", - component_name = "job", + self.set_method("req_req", + component = "job", method = "run", action = req_job_run, ) @@ -2484,10 +2484,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class RequestNeedsModel(S3Model): +class RequestNeedsModel(DataModel): """ Simple Requests Management System - Starts as Simple free text Needs @@ -2607,7 +2607,7 @@ def model(self): ) # Custom Methods - self.set_method("req", "need", + self.set_method("req_need", method = "assign", action = self.pr_AssignMethod(component="need_person")) @@ -2644,7 +2644,7 @@ def defaults(self): } # ============================================================================= -class RequestNeedsActivityModel(S3Model): +class RequestNeedsActivityModel(DataModel): """ Simple Requests Management System - optional link to Activities (Activity created to respond to Need) @@ -2676,10 +2676,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class RequestNeedsContactModel(S3Model): +class RequestNeedsContactModel(DataModel): """ Simple Requests Management System - optional link to Contacts (People) @@ -2715,10 +2715,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class RequestNeedsDemographicsModel(S3Model): +class RequestNeedsDemographicsModel(DataModel): """ Simple Requests Management System - optional link to Demographics @@ -2812,10 +2812,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class RequestNeedsItemsModel(S3Model): +class RequestNeedsItemsModel(DataModel): """ Simple Requests Management System - optional extension to support Items, but still not using Inventory-linked Requests @@ -2910,10 +2910,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class RequestNeedsSkillsModel(S3Model): +class RequestNeedsSkillsModel(DataModel): """ Simple Requests Management System - optional extension to support Skills, but still not using normal Requests @@ -2983,10 +2983,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class RequestNeedsLineModel(S3Model): +class RequestNeedsLineModel(DataModel): """ Simple Requests Management System - optional extension to support Demographics & Items within a single Line @@ -3168,7 +3168,7 @@ def model(self): # ============================================================================= -class RequestNeedsOrganisationModel(S3Model): +class RequestNeedsOrganisationModel(DataModel): """ Simple Requests Management System - optional link to Organisations @@ -3210,10 +3210,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class RequestNeedsPersonModel(S3Model): +class RequestNeedsPersonModel(DataModel): """ Simple Requests Management System - optional link to People (used for assignments to Skills) @@ -3278,10 +3278,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class RequestNeedsSectorModel(S3Model): +class RequestNeedsSectorModel(DataModel): """ Simple Requests Management System - optional link to Sectors @@ -3313,10 +3313,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class RequestNeedsSiteModel(S3Model): +class RequestNeedsSiteModel(DataModel): """ Simple Requests Management System - optional link to Sites @@ -3356,10 +3356,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class RequestNeedsTagModel(S3Model): +class RequestNeedsTagModel(DataModel): """ Needs Tags """ @@ -3406,10 +3406,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class RequestNeedsResponseModel(S3Model): +class RequestNeedsResponseModel(DataModel): """ A Response to a Need - a group of Activities @@ -3519,7 +3519,7 @@ def defaults(self): } # ============================================================================= -class RequestNeedsResponseLineModel(S3Model): +class RequestNeedsResponseLineModel(DataModel): """ A Line within a Response to a Need - an Activity @@ -3630,10 +3630,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class RequestNeedsResponseOrganisationModel(S3Model): +class RequestNeedsResponseOrganisationModel(DataModel): """ Organisations involved in Activity Groups """ @@ -3686,10 +3686,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class RequestTagModel(S3Model): +class RequestTagModel(DataModel): """ Request Tags """ @@ -3729,10 +3729,10 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= -class RequestOrderItemModel(S3Model): +class RequestOrderItemModel(DataModel): """ Simple Item Ordering for Requests - for when Procurement model isn't being used @@ -3800,10 +3800,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class RequestProjectModel(S3Model): +class RequestProjectModel(DataModel): """ Link Requests to Projects """ @@ -3832,10 +3832,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class RequestTaskModel(S3Model): +class RequestTaskModel(DataModel): """ Link Requests for Skills to Tasks """ @@ -3866,10 +3866,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class RequestRequesterCategoryModel(S3Model): +class RequestRequesterCategoryModel(DataModel): """ Model to control which types of requester can request which items - used by RLPPTM @@ -3905,10 +3905,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class CommitModel(S3Model): +class CommitModel(DataModel): """ Model for commits (pledges) """ @@ -4129,7 +4129,7 @@ def model(self): ) # Custom Method to Assign HRs - self.set_method("req", "commit", + self.set_method("req_commit", method = "assign", action = self.hrm_AssignMethod(component="commit_person", next_tab="commit_person", @@ -4225,7 +4225,7 @@ def commit_ondelete(row): req_update_commit_quantities_and_status(req) # ============================================================================= -class CommitItemModel(S3Model): +class CommitItemModel(DataModel): """ Model for committed (pledged) items """ @@ -4352,7 +4352,7 @@ def commit_item_ondelete(row): req_update_commit_quantities_and_status(req) # ============================================================================= -class CommitPersonModel(S3Model): +class CommitPersonModel(DataModel): """ Commit a named individual to a Request @@ -4408,7 +4408,7 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -4446,7 +4446,7 @@ def commit_person_onaccept(form): #req_skill_onaccept(None) # ============================================================================= -class CommitSkillModel(S3Model): +class CommitSkillModel(DataModel): """ Commit anonymous people to a Request @@ -4493,7 +4493,7 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -4604,7 +4604,7 @@ def req_tabs(r, match=True): """ Add a set of rheader tabs for a site's request management - @param r: the S3Request (for permission checking) + @param r: the CRUDRequest (for permission checking) @param match: request matching is applicable for this type of site @return: list of rheader tab definitions @@ -5346,10 +5346,7 @@ def postp(r, output): return output s3.postp = postp - output = current.rest_controller("req", "req", - rheader = rheader, - ) - return output + return current.crud_controller("req", "req", rheader=rheader) # ============================================================================= def req_send_commit(): @@ -5459,7 +5456,7 @@ def apply_method(self, r, **attr): """ Apply method. - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller options for this request """ diff --git a/modules/s3db/s3.py b/modules/s3db/s3.py index 499474425f..fc8303eb5e 100644 --- a/modules/s3db/s3.py +++ b/modules/s3db/s3.py @@ -29,6 +29,7 @@ __all__ = ("S3HierarchyModel", "S3DashboardModel", + "S3ImportJobModel", "S3DynamicTablesModel", "s3_table_rheader", "s3_scheduler_rheader", @@ -40,7 +41,7 @@ from ..core import * # ============================================================================= -class S3HierarchyModel(S3Model): +class S3HierarchyModel(DataModel): """ Model for stored object hierarchies """ names = ("s3_hierarchy", @@ -63,16 +64,16 @@ def model(self): # --------------------------------------------------------------------- # Return global names to s3.* # - return {} + return None # ------------------------------------------------------------------------- def defaults(self): """ Safe defaults if module is disabled """ - return {} + return None # ============================================================================= -class S3DashboardModel(S3Model): +class S3DashboardModel(DataModel): """ Model for stored dashboard configurations """ names = ("s3_dashboard", @@ -125,13 +126,13 @@ def model(self): # --------------------------------------------------------------------- # Return global names to s3.* # - return {} + return None # ------------------------------------------------------------------------- def defaults(self): """ Safe defaults if module is disabled """ - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -171,7 +172,44 @@ def dashboard_onaccept(form): db(query).update(active = False) # ============================================================================= -class S3DynamicTablesModel(S3Model): +class S3ImportJobModel(DataModel): + """ Tables to store pending import jobs """ + + names = ("s3_import_job", + "s3_import_item", + ) + + def model(self): + + # --------------------------------------------------------------------- + tablename = "s3_import_job" + self.define_table(tablename, + Field("job_id", length=128, unique=True, notnull=True), + Field("tablename"), + s3_datetime("timestmp", default="now"), + ) + + # --------------------------------------------------------------------- + tablename = "s3_import_item" + self.define_table(tablename, + Field("item_id", length=128, unique=True, notnull=True), + Field("job_id", length=128), + Field("tablename", length=128), + Field("record_uid"), + Field("skip", "boolean"), + Field("error", "text"), + Field("data", "blob"), + Field("element", "text"), + Field("ritems", "list:string"), + Field("citems", "list:string"), + Field("parent", length=128), + ) + + # --------------------------------------------------------------------- + return None + +# ============================================================================= +class S3DynamicTablesModel(DataModel): """ Model for dynamic tables """ names = ("s3_table", diff --git a/modules/s3db/security.py b/modules/s3db/security.py index c8c9bfbd75..3ced3b2aeb 100644 --- a/modules/s3db/security.py +++ b/modules/s3db/security.py @@ -37,7 +37,7 @@ from s3layouts import S3PopupLink # ============================================================================= -class SecurityZonesModel(S3Model): +class SecurityZonesModel(DataModel): """ Model for security zones """ names = ("security_level", @@ -298,7 +298,7 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ----------------------------------------------------------------------------- @staticmethod @@ -337,7 +337,7 @@ def security_staff_type_multirepresent(opt): return vals # ============================================================================= -class SecuritySeizedItemsModel(S3Model): +class SecuritySeizedItemsModel(DataModel): """ Model for the tracking of seized items (e.g. in connection with security procedures at shelters, borders or transport diff --git a/modules/s3db/setup.py b/modules/s3db/setup.py index af536aa707..50c8c329c0 100644 --- a/modules/s3db/setup.py +++ b/modules/s3db/setup.py @@ -89,7 +89,7 @@ } # ============================================================================= -class S3DNSModel(S3Model): +class S3DNSModel(DataModel): """ Domain Name System (DNS) Providers - super-entity @@ -202,7 +202,7 @@ def model(self): ) # --------------------------------------------------------------------- - return {} + return None # ============================================================================= class S3GoDaddyDNSModel(S3DNSModel): @@ -251,10 +251,10 @@ def model(self): ) # --------------------------------------------------------------------- - return {} + return None # ============================================================================= -class S3CloudModel(S3Model): +class S3CloudModel(DataModel): """ Clouds - super-entity @@ -418,7 +418,7 @@ def model(self): ) # --------------------------------------------------------------------- - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -605,7 +605,7 @@ def model(self): ) # --------------------------------------------------------------------- - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -679,7 +679,7 @@ def setup_openstack_server_ondelete(row): ) # ============================================================================= -class S3EmailProviderModel(S3Model): +class S3EmailProviderModel(DataModel): """ Email Providers (we just use Groups currently) - super-entity @@ -829,7 +829,7 @@ def model(self): ) # --------------------------------------------------------------------- - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -883,7 +883,7 @@ def setup_google_instance_ondelete(row): os.unlink(creds_path) # ============================================================================= -class S3SMTPModel(S3Model): +class S3SMTPModel(DataModel): """ SMTP Smart Hosts - tested with: @@ -961,7 +961,7 @@ def model(self): } # ============================================================================= -class S3SetupDeploymentModel(S3Model): +class S3SetupDeploymentModel(DataModel): names = ("setup_deployment", "setup_deployment_id", @@ -1122,7 +1122,7 @@ def model(self): setup_setting = "deployment_id", ) - set_method("setup", "deployment", + set_method("setup_deployment", method = "wizard", action = self.setup_server_wizard) @@ -1263,15 +1263,15 @@ def model(self): sortby = "name", ) - set_method("setup", "server", + set_method("setup_server", method = "enable", action = setup_monitor_server_enable_interactive) - set_method("setup", "server", + set_method("setup_server", method = "disable", action = setup_monitor_server_disable_interactive) - set_method("setup", "server", + set_method("setup_server", method = "check", action = setup_monitor_server_check) @@ -1399,38 +1399,38 @@ def model(self): update_onaccept = self.setup_instance_update_onaccept, ) - set_method("setup", "deployment", - component_name = "instance", + set_method("setup_deployment", + component = "instance", method = "deploy", action = self.setup_instance_deploy, ) - set_method("setup", "deployment", - component_name = "instance", + set_method("setup_deployment", + component = "instance", method = "settings", action = self.setup_instance_settings, ) - set_method("setup", "deployment", - component_name = "instance", + set_method("setup_deployment", + component = "instance", method = "start", action = self.setup_instance_start, ) - set_method("setup", "deployment", - component_name = "instance", + set_method("setup_deployment", + component = "instance", method = "stop", action = self.setup_instance_stop, ) - set_method("setup", "deployment", - component_name = "instance", + set_method("setup_deployment", + component = "instance", method = "clean", action = self.setup_instance_clean, ) - set_method("setup", "deployment", - component_name = "instance", + set_method("setup_deployment", + component = "instance", method = "wizard", action = self.setup_instance_wizard, ) @@ -1484,8 +1484,8 @@ def model(self): msg_record_deleted = T("Setting deleted"), msg_list_empty = T("No Settings currently registered")) - set_method("setup", "deployment", - component_name = "setting", + set_method("setup_deployment", + component = "setting", method = "apply", action = self.setup_setting_apply_interactive, ) @@ -2281,7 +2281,7 @@ def setup_setting_apply_interactive(r, **attr): ) # ============================================================================= -class S3SetupMonitorModel(S3Model): +class S3SetupMonitorModel(DataModel): names = ("setup_monitor_server", "setup_monitor_check", @@ -2513,15 +2513,15 @@ def model(self): setup_monitor_run = "task_id", ) - set_method("setup", "monitor_task", + set_method("setup_monitor_task", method = "enable", action = setup_monitor_task_enable_interactive) - set_method("setup", "monitor_task", + set_method("setup_monitor_task", method = "disable", action = setup_monitor_task_disable_interactive) - set_method("setup", "monitor_task", + set_method("setup_monitor_task", method = "check", action = setup_monitor_task_run) @@ -2592,7 +2592,7 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ------------------------------------------------------------------------- @staticmethod diff --git a/modules/s3db/sit.py b/modules/s3db/sit.py index 22d2b99049..c76141b4a5 100644 --- a/modules/s3db/sit.py +++ b/modules/s3db/sit.py @@ -36,7 +36,7 @@ #from s3layouts import S3PopupLink # ============================================================================= -class S3SituationModel(S3Model): +class S3SituationModel(DataModel): """ Situation Super Entity & Presence tables for Trackable resources """ diff --git a/modules/s3db/skeleton.py b/modules/s3db/skeleton.py index 10d0d9df18..0b5a5cc01d 100644 --- a/modules/s3db/skeleton.py +++ b/modules/s3db/skeleton.py @@ -20,7 +20,7 @@ # mandatory __all__ statement: # # - all classes in the name list will be initialized with the -# module prefix as only parameter. Subclasses of S3Model +# module prefix as only parameter. Subclasses of DataModel # support this automatically, and run the model() method # if the module is enabled in deployment_settings, otherwise # the default() method. @@ -42,14 +42,14 @@ from s3layouts import S3PopupLink # ============================================================================= -# Define a new class as subclass of S3Model +# Define a new class as subclass of DataModel # => you can define multiple of these classes within the same module, each # of them will be initialized only when one of the declared names gets # requested from s3db # => remember to list all model classes in __all__, otherwise they won't ever # be loaded. # -class SkeletonDataModel(S3Model): +class SkeletonDataModel(DataModel): # Declare all the names this model can auto-load, i.e. all tablenames # and all response.s3 names which are defined here. If you omit the "names" diff --git a/modules/s3db/stats.py b/modules/s3db/stats.py index bbcb8f7e07..2e4aa161fd 100644 --- a/modules/s3db/stats.py +++ b/modules/s3db/stats.py @@ -50,7 +50,7 @@ from s3layouts import S3PopupLink # ============================================================================= -class S3StatsModel(S3Model): +class S3StatsModel(DataModel): """ Statistics Data """ @@ -231,7 +231,7 @@ def defaults(self): } # ============================================================================= -class S3StatsDemographicModel(S3Model): +class S3StatsDemographicModel(DataModel): """ Baseline Demographics @@ -1173,8 +1173,7 @@ def stats_demographic_data_controller(): request = current.request if "options.s3json" in request.args: # options.s3json lookups for AddResourceLink - output = current.rest_controller("stats", "demographic_data") - return output + return current.crud_controller("stats", "demographic_data") # Only viewing is valid get_vars = request.get_vars @@ -1215,14 +1214,12 @@ def postp(r, output): else: rheader = None - output = current.rest_controller("stats", "demographic_data", - rheader = rheader, - ) - - return output + return current.crud_controller("stats", "demographic_data", + rheader = rheader, + ) # ============================================================================= -class S3StatsImpactModel(S3Model): +class S3StatsImpactModel(DataModel): """ Used to record Impacts of Events &/or Incidents - links to Needs (Requests module) @@ -1354,7 +1351,7 @@ def model(self): } # ============================================================================= -class S3StatsPeopleModel(S3Model): +class S3StatsPeopleModel(DataModel): """ Used to record people in the CRMT (Community Resilience Mapping Tool) template @@ -1510,7 +1507,7 @@ def model(self): *s3_meta_fields()) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= def stats_quantile(data, q): diff --git a/modules/s3db/supply.py b/modules/s3db/supply.py index dfa72d2804..2dbd2b9668 100644 --- a/modules/s3db/supply.py +++ b/modules/s3db/supply.py @@ -65,7 +65,7 @@ ) # ============================================================================= -class S3SupplyModel(S3Model): +class S3SupplyModel(DataModel): """ Generic Supply functionality such as catalogs and items that is used across multiple modules. @@ -1053,7 +1053,7 @@ def supply_item_duplicate(item): Callback function used to look for duplicates during the import process - @param item: the S3ImportItem to check + @param item: the ImportItem to check """ data = item.data @@ -1100,7 +1100,7 @@ def supply_item_category_duplicate(item): Callback function used to look for duplicates during the import process - @param item: the S3ImportItem to check + @param item: the ImportItem to check """ data = item.data @@ -1131,7 +1131,7 @@ def supply_catalog_item_duplicate(item): Callback function used to look for duplicates during the import process - @param item: the S3ImportItem to check + @param item: the ImportItem to check """ data = item.data @@ -1159,7 +1159,7 @@ def supply_item_pack_duplicate(item): Callback function used to look for duplicates during the import process - @param item: the S3ImportItem to check + @param item: the ImportItem to check """ data = item.data @@ -1264,7 +1264,7 @@ def supply_item_onaccept(form): ) # ============================================================================= -class S3SupplyDistributionModel(S3Model): +class S3SupplyDistributionModel(DataModel): """ Supply Distribution Model - depends on Stats module @@ -1754,7 +1754,7 @@ def supply_distribution_year(row): return list(range(date.year, end_date.year + 1)) # ============================================================================= -class S3SupplyDistributionDVRActivityModel(S3Model): +class S3SupplyDistributionDVRActivityModel(DataModel): """ Model to link distributions to DVR activities / case activities """ @@ -1781,10 +1781,10 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ============================================================================= -class S3SupplyPersonModel(S3Model): +class S3SupplyPersonModel(DataModel): """ Link table between People & Items - e.g. Donations @@ -1883,7 +1883,7 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= class supply_ItemRepresent(S3Represent): @@ -2737,7 +2737,7 @@ def prep(r): return True s3.prep = prep - return current.rest_controller("supply", "item", + return current.crud_controller("supply", "item", rheader = supply_item_rheader, ) @@ -2991,10 +2991,7 @@ def postp(r, output): return output s3.postp = postp - output = current.rest_controller("supply", "item_entity", - hide_filter = True, - ) - return output + return current.crud_controller("supply", "item_entity", hide_filter=True) # ----------------------------------------------------------------------------- def supply_get_shipping_code(doctype, site_id, field): diff --git a/modules/s3db/survey.py b/modules/s3db/survey.py index 72582a12ce..c1e178f96e 100644 --- a/modules/s3db/survey.py +++ b/modules/s3db/survey.py @@ -102,7 +102,7 @@ def _debug(msg): _debug = lambda m: None # ============================================================================= -class S3SurveyTemplateModel(S3Model): +class S3SurveyTemplateModel(DataModel): """ Template model @@ -224,8 +224,8 @@ def model(self): survey_translate = "template_id", ) - self.set_method("survey", "template", - component_name = "translate", + self.set_method("survey_template", + component = "translate", method = "translate_download", action = survey_TranslateDownload, ) @@ -765,7 +765,7 @@ def survey_build_template_summary(template_id): return form # ============================================================================= -class S3SurveyQuestionModel(S3Model): +class S3SurveyQuestionModel(DataModel): """ Question Model """ @@ -1231,7 +1231,7 @@ def survey_updateMetaData(record, qtype, metadata): widget_obj.insertChildren(record, metadata_list) # ============================================================================= -class S3SurveyFormatterModel(S3Model): +class S3SurveyFormatterModel(DataModel): """ The survey_formatter table defines the order in which the questions will be laid out when a formatted presentation is used. @@ -1307,7 +1307,7 @@ def model(self): ) # --------------------------------------------------------------------- - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -1402,7 +1402,7 @@ def survey_getQstnLayoutRules(template_id, section_id, method = 1): return row_list # ============================================================================= -class S3SurveySeriesModel(S3Model): +class S3SurveySeriesModel(DataModel): """ Series Model @@ -1540,15 +1540,15 @@ def model(self): ) # Custom Methods - set_method("survey", "series", method="summary", # NB This conflicts with the global summary method! + set_method("survey_series", method="summary", # NB This conflicts with the global summary method! action = self.seriesSummary) - set_method("survey", "series", method="graph", + set_method("survey_series", method="graph", action = self.seriesGraph) - set_method("survey", "series", method="map", # NB This conflicts with the global map method! + set_method("survey_series", method="map", # NB This conflicts with the global map method! action = self.seriesMap) - set_method("survey", "series", method="series_chart_download", + set_method("survey_series", method="series_chart_download", action = self.seriesChartDownload) - set_method("survey", "series", method="export_responses", + set_method("survey_series", method="export_responses", action = survey_ExportResponses) # --------------------------------------------------------------------- @@ -1624,7 +1624,7 @@ def seriesSummary(r, **attr): question_ids.append(str(question.question_id)) items = buildCompletedList(series_id, question_ids) if r.representation == "xls": - from core.io.codecs.xls import S3XLS + from core.resource.codecs.xls import S3XLS exporter = S3XLS() return exporter.encode(items, title=crud_strings.title_selected, @@ -2402,7 +2402,7 @@ def buildSeriesSummary(series_id, posn_offset): return form # ============================================================================= -class S3SurveyCompleteModel(S3Model): +class S3SurveyCompleteModel(DataModel): """ Completed Surveys Model """ @@ -2648,7 +2648,7 @@ def importAnswers(complete_id, question_list): "survey", "answer.xsl") resource = current.s3db.resource("survey_answer") - resource.import_xml(bio, stylesheet=xsl, format="csv") + resource.import_xml(bio, stylesheet=xsl, source_type="csv") # ------------------------------------------------------------------------- @staticmethod @@ -2707,7 +2707,7 @@ def importLocations(location_dict): "gis", "location.xsl") resource = current.s3db.resource("gis_location") - resource.import_xml(bio, stylesheet = xsl, format="csv") + resource.import_xml(bio, stylesheet = xsl, source_type="csv") # ------------------------------------------------------------------------- @staticmethod @@ -3081,7 +3081,7 @@ def getLocationList(series_id): return response_locations # ============================================================================= -class S3SurveyTranslateModel(S3Model): +class S3SurveyTranslateModel(DataModel): """ Translations Model """ @@ -3133,7 +3133,7 @@ def model(self): onaccept = self.translate_onaccept, ) # --------------------------------------------------------------------- - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -3238,7 +3238,7 @@ def apply_method(self, r, **attr): """ Entry point for REST API - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller arguments """ @@ -3341,7 +3341,7 @@ def apply_method(self, r, **attr): """ Entry point for REST API - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller arguments """ diff --git a/modules/s3db/sync.py b/modules/s3db/sync.py index de6a2e0abd..525d9aa9a1 100644 --- a/modules/s3db/sync.py +++ b/modules/s3db/sync.py @@ -47,7 +47,7 @@ from s3layouts import S3PopupLink # ============================================================================= -class SyncConfigModel(S3Model): +class SyncConfigModel(DataModel): """ Model to store local sync configuration """ names = ("sync_config", @@ -110,10 +110,10 @@ def model(self): # --------------------------------------------------------------------- # Return global names to s3.* # - return {} + return None # ============================================================================= -class SyncStatusModel(S3Model): +class SyncStatusModel(DataModel): """ Model to store the current sync module status """ names = ("sync_status", @@ -145,10 +145,10 @@ def model(self): # --------------------------------------------------------------------- # Return global names to s3.* # - return {} + return None # ============================================================================= -class SyncRepositoryModel(S3Model): +class SyncRepositoryModel(DataModel): """ Model representing a peer repository """ names = ("sync_repository", @@ -380,13 +380,13 @@ def model(self): ) # REST Methods - set_method("sync", "repository", + set_method("sync_repository", method = "now", action = sync_now, ) - set_method("sync", "repository", - component_name = "job", + set_method("sync_repository", + component = "job", method = "reset", action = sync_job_reset, ) @@ -555,7 +555,7 @@ def sync_repository_create_next(r): return URL(c="sync", f="repository", args=["[id]", create_next]) # ============================================================================= -class SyncDatasetModel(S3Model): +class SyncDatasetModel(DataModel): """ Model representing a public data set """ names = ("sync_dataset", @@ -635,7 +635,7 @@ def model(self): ) # REST Methods - self.set_method("sync", "dataset", + self.set_method("sync_dataset", method = "archive", action = sync_CreateArchive, ) @@ -850,7 +850,7 @@ def archive_file_represent(filename): return current.messages["NONE"] # ============================================================================= -class SyncLogModel(S3Model): +class SyncLogModel(DataModel): """ Model for the Sync log """ names = ("sync_log", @@ -911,10 +911,10 @@ def model(self): # --------------------------------------------------------------------- # Return global names to s3.* # - return {} + return None # ============================================================================= -class SyncTaskModel(S3Model): +class SyncTaskModel(DataModel): names = ("sync_task", "sync_resource_filter", @@ -950,7 +950,7 @@ def model(self): } # Strategy (allowed import methods) - sync_strategy = S3ImportItem.METHOD + sync_strategy = ImportItem.METHOD all_strategies = list(sync_strategy.values()) sync_strategy_represent = lambda opt: ", ".join(o for o in sync_strategy.values() if o in opt) \ @@ -963,13 +963,12 @@ def model(self): } # Update/conflict resolution policy - sync_policies = S3ImportItem.POLICY - sync_policy = { - sync_policies.OTHER: T("always update"), - sync_policies.NEWER: T("update if newer"), - sync_policies.MASTER: T("update if master"), - sync_policies.THIS: T("never update") - } + from core import SyncPolicy + sync_policy = {SyncPolicy.OTHER: T("always update"), + SyncPolicy.NEWER: T("update if newer"), + SyncPolicy.MASTER: T("update if master"), + SyncPolicy.THIS: T("never update") + } sync_policy_represent = lambda opt: \ opt and sync_policy.get(opt, UNKNOWN_OPT) or NONE @@ -1117,10 +1116,10 @@ def model(self): ), ), Field("update_policy", - default = sync_policies.NEWER, + default = SyncPolicy.NEWER, label = T("Update Policy"), represent = sync_policy_represent, - requires = IS_IN_SET(sync_policies, + requires = IS_IN_SET(sync_policy, zero = None, ), comment = DIV(_class = "tooltip", @@ -1131,10 +1130,10 @@ def model(self): ), ), Field("conflict_policy", - default = sync_policies.NEWER, + default = SyncPolicy.NEWER, label = T("Conflict Policy"), represent = sync_policy_represent, - requires = IS_IN_SET(sync_policies, + requires = IS_IN_SET(sync_policy, zero = None, ), comment = DIV(_class = "tooltip", @@ -1213,7 +1212,7 @@ def model(self): # --------------------------------------------------------------------- # Return global names to s3.* # - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -1312,7 +1311,7 @@ def sync_resource_filter_onaccept(form): db(ttable.id == task_id).update(last_push=None) # ============================================================================= -class SyncScheduleModel(S3Model): +class SyncScheduleModel(DataModel): """ Model for automatic synchronization schedule """ names = ("sync_job", @@ -1352,7 +1351,7 @@ def model(self): # --------------------------------------------------------------------- # Return global names to s3.* # - return {} + return None # ============================================================================= def sync_rheader(r, tabs=None): @@ -1461,7 +1460,7 @@ def sync_now(r, **attr): """ Manual synchronization of a repository - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller options for the request """ @@ -1537,7 +1536,7 @@ def apply_method(self, r, **attr): """ Entry point for REST controller - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller parameters @todo: perform archive creation async? @@ -1578,7 +1577,7 @@ def form(r, row): """ Simple UI form to trigger POST method - @param r: the S3Request embedding the form + @param r: the CRUDRequest embedding the form @param row: the data set Row @todo: if archive is currently being built (async), diff --git a/modules/s3db/translate.py b/modules/s3db/translate.py index 61a81aef38..f0faf92b47 100644 --- a/modules/s3db/translate.py +++ b/modules/s3db/translate.py @@ -35,7 +35,7 @@ from ..core import * # ============================================================================= -class S3TranslateModel(S3Model): +class S3TranslateModel(DataModel): names = ("translate_language", "translate_percentage", @@ -91,7 +91,7 @@ def model(self): #---------------------------------------------------------------------- # Pass names back to global scope (s3.*) - return {} + return None # ------------------------------------------------------------------------- @staticmethod diff --git a/modules/s3db/transport.py b/modules/s3db/transport.py index 573d320fda..7252d401e9 100644 --- a/modules/s3db/transport.py +++ b/modules/s3db/transport.py @@ -38,7 +38,7 @@ from ..s3layouts import S3PopupLink # ============================================================================= -class S3TransportModel(S3Model): +class S3TransportModel(DataModel): """ http://eden.sahanafoundation.org/wiki/BluePrint/Transport """ @@ -771,7 +771,7 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # ------------------------------------------------------------------------- @staticmethod diff --git a/modules/s3db/vehicle.py b/modules/s3db/vehicle.py index 86ce47d195..150a65a45c 100644 --- a/modules/s3db/vehicle.py +++ b/modules/s3db/vehicle.py @@ -35,7 +35,7 @@ from ..core import * # ============================================================================= -class S3VehicleModel(S3Model): +class S3VehicleModel(DataModel): """ Vehicle Management Functionality diff --git a/modules/s3db/vol.py b/modules/s3db/vol.py index 461afbd31a..473346a617 100644 --- a/modules/s3db/vol.py +++ b/modules/s3db/vol.py @@ -51,7 +51,7 @@ SEPARATORS = (",", ":") # ============================================================================= -class VolunteerModel(S3Model): +class VolunteerModel(DataModel): names = ("vol_details",) @@ -102,7 +102,7 @@ def vol_active_represent(opt): return output # ============================================================================= -class VolunteerActivityModel(S3Model): +class VolunteerActivityModel(DataModel): """ Currently used by CRMADA """ @@ -499,7 +499,7 @@ def model(self): *s3_meta_fields()) # Pass names back to global scope (s3.*) - return {} + return None # ============================================================================= def vol_activity_hours_month(row): @@ -586,7 +586,7 @@ def vol_activity_hours_onaccept(form): active = active) # ============================================================================= -class VolunteerAwardModel(S3Model): +class VolunteerAwardModel(DataModel): names = ("vol_award", "vol_volunteer_award", @@ -711,7 +711,7 @@ def model(self): ) # Pass names back to global scope (s3.*) - return {} + return None # ------------------------------------------------------------------------- @staticmethod @@ -738,7 +738,7 @@ def vol_award_file_represent(filename): return current.messages["NONE"] # ============================================================================= -class VolunteerClusterModel(S3Model): +class VolunteerClusterModel(DataModel): """ Fucntionality to support the Philippines Red Cross """ @@ -1479,7 +1479,7 @@ def postp(r, output): return output s3.postp = postp - return current.rest_controller("hrm", "human_resource") + return current.crud_controller("hrm", "human_resource") # ----------------------------------------------------------------------------- def vol_person_controller(): @@ -1497,23 +1497,22 @@ def vol_person_controller(): s3 = response.s3 session = current.session settings = current.deployment_settings - resourcename = "person" configure = s3db.configure set_method = s3db.set_method # Custom Method for Contacts - set_method("pr", resourcename, + set_method("pr_person", method = "contacts", action = s3db.pr_Contacts) # Custom Method for CV - set_method("pr", resourcename, + set_method("pr_person", method = "cv", action = s3db.hrm_CV) # Custom Method for HR Record - set_method("pr", resourcename, + set_method("pr_person", method = "record", action = s3db.hrm_Record) @@ -1597,45 +1596,44 @@ def vol_person_controller(): ) # Import pre-process - def import_prep(data, group=group): + def import_prep(tree, group=group): """ Deletes all HR records (of the given group) of the organisation before processing a new data import, used for the import_prep hook in response.s3 """ - if s3.import_replace: - resource, tree = data - if tree is not None: - xml = current.xml - tag = xml.TAG - att = xml.ATTRIBUTE - - if group == "staff": - group = 1 - elif group == "volunteer": - group = 2 - else: - return # don't delete if no group specified - - root = tree.getroot() - expr = "/%s/%s[@%s='org_organisation']/%s[@%s='name']" % \ - (tag.root, tag.resource, att.name, tag.data, att.field) - orgs = root.xpath(expr) - for org in orgs: - org_name = org.get("value", None) or org.text - if org_name: - try: - org_name = json.loads(xml.xml_decode(org_name)) - except: - pass - if org_name: - htable = s3db.hrm_human_resource - otable = s3db.org_organisation - query = (otable.name == org_name) & \ - (htable.organisation_id == otable.id) & \ - (htable.type == group) - resource = s3db.resource("hrm_human_resource", filter=query) - resource.delete(format="xml", cascade=True) + if s3.import_replace and tree is not None: + xml = current.xml + tag = xml.TAG + att = xml.ATTRIBUTE + + if group == "staff": + group = 1 + elif group == "volunteer": + group = 2 + else: + return # don't delete if no group specified + + root = tree.getroot() + expr = "/%s/%s[@%s='org_organisation']/%s[@%s='name']" % \ + (tag.root, tag.resource, att.name, tag.data, att.field) + orgs = root.xpath(expr) + for org in orgs: + org_name = org.get("value", None) or org.text + if org_name: + try: + org_name = json.loads(xml.xml_decode(org_name)) + except: + pass + if org_name: + htable = s3db.hrm_human_resource + otable = s3db.org_organisation + query = (otable.name == org_name) & \ + (htable.organisation_id == otable.id) & \ + (htable.type == group) + resource = s3db.resource("hrm_human_resource", filter=query) + resource.delete(format="xml", cascade=True) + s3.import_prep = import_prep # CRUD pre-process @@ -1860,7 +1858,7 @@ def postp(r, output): # REST Interface #orgname = session.s3.hrm.orgname - return current.rest_controller("pr", resourcename, + return current.crud_controller("pr", "person", csv_template = ("hrm", "volunteer"), csv_stylesheet = ("hrm", "person.xsl"), csv_extra_fields = [ diff --git a/modules/s3db/water.py b/modules/s3db/water.py index 43f2244fd1..62a534a927 100644 --- a/modules/s3db/water.py +++ b/modules/s3db/water.py @@ -35,7 +35,7 @@ from s3layouts import S3PopupLink # ============================================================================= -class S3WaterModel(S3Model): +class S3WaterModel(DataModel): """ Water Sources """ @@ -283,6 +283,6 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return {} + return None # END ========================================================================= diff --git a/modules/templates/BRCMS/RLP/config.py b/modules/templates/BRCMS/RLP/config.py index 71d100994a..f10d1aff76 100644 --- a/modules/templates/BRCMS/RLP/config.py +++ b/modules/templates/BRCMS/RLP/config.py @@ -2034,7 +2034,7 @@ def prep(r): # Configure Anonymizer from core import S3Anonymize - s3db.set_method("pr", "person", + s3db.set_method("pr_person", method = "anonymize", action = S3Anonymize, ) diff --git a/modules/templates/BRCMS/RLP/controllers.py b/modules/templates/BRCMS/RLP/controllers.py index c037bb209d..5f5e2c77f0 100644 --- a/modules/templates/BRCMS/RLP/controllers.py +++ b/modules/templates/BRCMS/RLP/controllers.py @@ -15,7 +15,7 @@ from core import FS, ICON, IS_PHONE_NUMBER_MULTI, IS_PHONE_NUMBER_SINGLE, \ JSONERRORS, S3CRUD, S3CustomController, S3LocationSelector, \ - S3Represent, S3Report, S3Request, S3WithIntro, \ + S3Represent, S3Report, CRUDRequest, S3WithIntro, \ s3_comments_widget, s3_get_extension, s3_mark_required, \ s3_str, s3_text_represent, s3_truncate @@ -2059,7 +2059,7 @@ def __call__(self): s3 = response.s3 representation = s3_get_extension(request) or \ - S3Request.DEFAULT_REPRESENTATION + CRUDRequest.DEFAULT_REPRESENTATION # Pagination get_vars = request.get_vars @@ -2076,7 +2076,7 @@ def __call__(self): distinct = False dtargs = {} - if representation in S3Request.INTERACTIVE_FORMATS: + if representation in CRUDRequest.INTERACTIVE_FORMATS: # How many records per page? if s3.dataTable_pageLength: @@ -2192,7 +2192,7 @@ def __call__(self): '"draw":%s,' \ '"data":[]}' % (totalrows, list_id, draw) else: - S3Request("auth", "user").error(415, current.ERROR.BAD_FORMAT) + CRUDRequest("auth", "user").error(415, current.ERROR.BAD_FORMAT) return output diff --git a/modules/templates/BRCMS/RLP/helpers.py b/modules/templates/BRCMS/RLP/helpers.py index d1caa1ef0b..fed0470a02 100644 --- a/modules/templates/BRCMS/RLP/helpers.py +++ b/modules/templates/BRCMS/RLP/helpers.py @@ -765,7 +765,7 @@ def restrict_data_formats(r): """ Restrict data exports (prevent S3XML/S3JSON of records) - @param r: the S3Request + @param r: the CRUDRequest """ settings = current.deployment_settings diff --git a/modules/templates/BRCMS/idcards.py b/modules/templates/BRCMS/idcards.py index b4ed7d32a2..bd4ae2e455 100644 --- a/modules/templates/BRCMS/idcards.py +++ b/modules/templates/BRCMS/idcards.py @@ -9,7 +9,7 @@ from gluon import current -from core.io.codecs.card import S3PDFCardLayout +from core.resource.codecs.card import S3PDFCardLayout from core import s3_format_fullname, s3_str # Fonts we use in this layout diff --git a/modules/templates/DRKCM/config.py b/modules/templates/DRKCM/config.py index cb2ca29d29..2ce6f1d42f 100644 --- a/modules/templates/DRKCM/config.py +++ b/modules/templates/DRKCM/config.py @@ -493,7 +493,7 @@ def customise_doc_document_controller(**attr): attr["rheader"] = drk_dvr_rheader # Set contacts-method to retain the tab - s3db.set_method("pr", "person", + s3db.set_method("pr_person", method = "contacts", action = s3db.pr_Contacts, ) @@ -704,7 +704,7 @@ def customise_pr_person_resource(r, tablename): # Configure anonymize-method # TODO make standard via setting from core import S3Anonymize - s3db.set_method("pr", "person", + s3db.set_method("pr_person", method = "anonymize", action = S3Anonymize, ) @@ -717,11 +717,11 @@ def customise_pr_person_resource(r, tablename): if current.auth.s3_has_role("CASE_MANAGEMENT"): # Allow use of Document Templates - s3db.set_method("pr", "person", + s3db.set_method("pr_person", method = "templates", action = s3db.pr_Templates(), ) - s3db.set_method("pr", "person", + s3db.set_method("pr_person", method = "template", action = s3db.pr_Template(), ) @@ -804,7 +804,7 @@ def custom_prep(r): configure = resource.configure # Set contacts-method for tab - s3db.set_method("pr", "person", + s3db.set_method("pr_person", method = "contacts", action = s3db.pr_Contacts, ) @@ -822,7 +822,7 @@ def custom_prep(r): else: # Add-Person-Widget (family members) search_fields = ("first_name", "last_name") - s3db.set_method("pr", "person", + s3db.set_method("pr_person", method = "search_ac", action = s3db.pr_PersonSearchAutocomplete(search_fields), ) @@ -1437,7 +1437,7 @@ def custom_prep(r): if r.controller == "dvr": # Set contacts-method to retain the tab - s3db.set_method("pr", "person", + s3db.set_method("pr_person", method = "contacts", action = s3db.pr_Contacts, ) @@ -3232,7 +3232,7 @@ def customise_dvr_response_action_controller(**attr): if "viewing" in current.request.get_vars: # Set contacts-method to retain the tab - s3db.set_method("pr", "person", + s3db.set_method("pr_person", method = "contacts", action = s3db.pr_Contacts, ) @@ -3251,7 +3251,7 @@ def custom_prep(r): if not r.id: from .stats import PerformanceIndicatorExport pitype = get_ui_options().get("response_performance_indicators") - s3db.set_method("dvr", "response_action", + s3db.set_method("dvr_response_action", method = "indicators", action = PerformanceIndicatorExport(pitype), ) diff --git a/modules/templates/DRKCM/controllers.py b/modules/templates/DRKCM/controllers.py index 775a842826..1ca4facf1b 100644 --- a/modules/templates/DRKCM/controllers.py +++ b/modules/templates/DRKCM/controllers.py @@ -8,7 +8,7 @@ from gluon.html import * from gluon.storage import Storage -from core import FS, S3CustomController +from core import FS, S3CRUD, S3CustomController from s3theme import formstyle_foundation_inline THEME = "DRK" @@ -266,18 +266,18 @@ def __call__(self): if not auth.s3_has_role("ORG_GROUP_ADMIN"): auth.permission.fail() - from core import S3CRUD, s3_get_extension, s3_request + from core import S3CRUD, s3_get_extension, crud_request request = current.request args = request.args - # Create an S3Request - r = s3_request("org", "organisation", - c = "default", - f = "index/%s" % args[0], - args = args[1:], - extension = s3_get_extension(request), - ) + # Create an CRUDRequest + r = crud_request("org", "organisation", + c = "default", + f = "index/%s" % args[0], + args = args[1:], + extension = s3_get_extension(request), + ) # Filter to root organisations resource = r.resource @@ -340,7 +340,7 @@ def __call__(self): output["title"] = T("User Statistics") # URL to open the resource - open_url = resource.crud._linkto(r, update=False)("[id]") + open_url = S3CRUD._linkto(r, update=False)("[id]") # Add action button for open action_buttons = S3CRUD.action_buttons @@ -358,7 +358,7 @@ def rheader(self, r): """ Show the current date in the output - @param r: the S3Request + @param r: the CRUDRequest @returns: the page header (rheader) """ diff --git a/modules/templates/DRKCM/stats.py b/modules/templates/DRKCM/stats.py index 47a3c26df8..d69e206831 100644 --- a/modules/templates/DRKCM/stats.py +++ b/modules/templates/DRKCM/stats.py @@ -5,7 +5,7 @@ from gluon import current, HTTP from core import S3Method, s3_decode_iso_datetime, s3_str -from core.io.codecs.xls import S3XLS +from core.resource.codecs.xls import S3XLS # ============================================================================= class PerformanceIndicators(object): @@ -374,7 +374,7 @@ def apply_method(self, r, **attr): """ Page-render entry point for REST interface. - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes """ @@ -395,7 +395,7 @@ def xls(self, r, **attr): """ Export the performance indicators as XLS data sheet - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes """ diff --git a/modules/templates/Disease/Ebola/ebola.cases.update.py b/modules/templates/Disease/Ebola/ebola.cases.update.py index a91c084fd7..5aa8830a8c 100644 --- a/modules/templates/Disease/Ebola/ebola.cases.update.py +++ b/modules/templates/Disease/Ebola/ebola.cases.update.py @@ -301,7 +301,7 @@ def write_details_to_csv(csv_file, statistic, data): stylesheet = os.path.join(request.folder, "static", "formats", "s3csv", "disease", "stats_data.xsl") resource = s3db.resource("disease_stats_data") File = open(OUTPUT_CSV, "rb") -resource.import_xml(File, format="csv", stylesheet=stylesheet) +resource.import_xml(File, source_type="csv", stylesheet=stylesheet) db.commit() if len(rejected_loc) or len(rejected_data) or len(suspect_data) or len(new_org): diff --git a/modules/templates/RLP/config.py b/modules/templates/RLP/config.py index 3e784aee13..1786635d89 100644 --- a/modules/templates/RLP/config.py +++ b/modules/templates/RLP/config.py @@ -1076,7 +1076,7 @@ def customise_volunteer_availability_fields(r): """ Customise availability fields in volunteer form - @param r: the current S3Request + @param r: the current CRUDRequest """ from core import S3WeeklyHoursWidget, S3WithIntro, s3_text_represent @@ -1123,7 +1123,7 @@ def volunteer_list_fields(r, coordinator=False, name_fields=None): """ Determine fields for volunteer list - @param r: the current S3Request + @param r: the current CRUDRequest @param coordinator: user is COORDINATOR @param name_fields: name fields in order @@ -1426,7 +1426,7 @@ def custom_prep(r): # Configure anonymize-method from core import S3Anonymize - s3db.set_method("pr", "person", + s3db.set_method("pr_person", method = "anonymize", action = S3Anonymize, ) @@ -1713,7 +1713,7 @@ def custom_prep(r): # Configure anonymize-method from core import S3Anonymize - s3db.set_method("pr", "person", + s3db.set_method("pr_person", method = "anonymize", action = S3Anonymize, ) @@ -2598,7 +2598,7 @@ def custom_prep(r): # Set method for Ajax-lookup of notification data from .notifications import InlineNotificationsData - s3db.set_method("hrm", "delegation", + s3db.set_method("hrm_delegation", method = "notifications", action = InlineNotificationsData, ) diff --git a/modules/templates/RLP/controllers.py b/modules/templates/RLP/controllers.py index ee0bdc97fb..02ad6de046 100644 --- a/modules/templates/RLP/controllers.py +++ b/modules/templates/RLP/controllers.py @@ -807,8 +807,8 @@ def register_onaccept(cls, user_id): return # Customise resources - from core import S3Request - r = S3Request("auth", "user", args=[], get_vars={}) + from core import CRUDRequest + r = CRUDRequest("auth", "user", args=[], get_vars={}) customise_resource = current.deployment_settings.customise_resource for tablename in ("pr_person", "pr_group", "pr_group_membership"): customise = customise_resource(tablename) diff --git a/modules/templates/RLP/notifications.py b/modules/templates/RLP/notifications.py index b754c26b4f..f2db943a0b 100644 --- a/modules/templates/RLP/notifications.py +++ b/modules/templates/RLP/notifications.py @@ -533,7 +533,7 @@ def apply_method(self, r, **attr): """ Entry point for REST interface. - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes """ diff --git a/modules/templates/RLP/tools/pooladjust.py b/modules/templates/RLP/tools/pooladjust.py index 6ec98c13f9..fae36a442b 100644 --- a/modules/templates/RLP/tools/pooladjust.py +++ b/modules/templates/RLP/tools/pooladjust.py @@ -62,8 +62,8 @@ def adjust_pools(rules): s3db = current.s3db # Customise Resources (doesn't happen automatically when running from CLI) - from core import s3_request - r = s3_request("pr", "person") + from core import crud_request + r = crud_request("pr", "person") r.customise_resource("pr_person") r.customise_resource("pr_group_membership") diff --git a/modules/templates/RLPPTM/config.py b/modules/templates/RLPPTM/config.py index 1f01864db7..9d373d6ec7 100644 --- a/modules/templates/RLPPTM/config.py +++ b/modules/templates/RLPPTM/config.py @@ -14,7 +14,7 @@ from gluon.storage import Storage -from core import FS, IS_FLOAT_AMOUNT, ICON, IS_ONE_OF, IS_UTC_DATE, S3Represent, s3_str +from core import FS, IS_FLOAT_AMOUNT, ICON, IS_ONE_OF, IS_UTC_DATE, S3CRUD, S3Represent, s3_str from s3dal import original_tablename from .rlpgeonames import rlp_GeoNames @@ -875,15 +875,15 @@ def customise_disease_case_diagnostics_resource(r, tablename): # Custom REST methods from .cwa import TestResultRegistration - s3db.set_method("disease", "case_diagnostics", + s3db.set_method("disease_case_diagnostics", method = "register", action = TestResultRegistration, ) - s3db.set_method("disease", "case_diagnostics", + s3db.set_method("disease_case_diagnostics", method = "certify", action = TestResultRegistration, ) - s3db.set_method("disease", "case_diagnostics", + s3db.set_method("disease_case_diagnostics", method = "cwaretry", action = TestResultRegistration, ) @@ -942,10 +942,9 @@ def custom_postp(r, output): elif record and method in (None, "read"): key, label = "list_btn", T("Register another test result") if key: - crud = r.resource.crud - regbtn = crud.crud_button(label = label, - _href = r.url(id="", method="register"), - ) + regbtn = S3CRUD.crud_button(label = label, + _href = r.url(id="", method="register"), + ) output["buttons"] = {key: regbtn} return output @@ -995,14 +994,28 @@ def customise_disease_testing_report_resource(r, tablename): table.tests_total.readable = False table.tests_positive.readable = False - # If there is only one selectable site, set as default + make r/o + # Order testing sites selector by obsolete-flag field = table.site_id - requires = field.requires - if hasattr(requires, "options"): - selectable = [o[0] for o in field.requires.options() if o[0]] - if len(selectable) == 1: - field.default = selectable[0] - field.writable = False + stable = current.s3db.org_site + field.requires = IS_ONE_OF(db, "org_site.site_id", + field.represent, + instance_types = ["org_facility"], + orderby = (stable.obsolete, stable.name), + sort = False, + ) + # Check how many sites are selectable + selectable = [o[0] for o in field.requires.options() if o[0]] + if len(selectable) == 1: + # If only one selectable site, set as default + make r/o + field.default = selectable[0] + field.writable = False + else: + # If one active site, set it as default, but leave selectable + query = (stable.site_id.belongs(selectable)) & \ + (stable.obsolete == False) + active = db(query).select(stable.site_id, limitby = (0, 2)) + if len(active) == 1: + field.default = active.first().site_id # Allow daily reports up to 3 months back in time (1st of month) field = table.date @@ -1865,7 +1878,7 @@ def customise_fin_voucher_claim_resource(r, tablename): # PDF export method from .helpers import ClaimPDF - s3db.set_method("fin", "voucher_claim", + s3db.set_method("fin_voucher_claim", method = "record", action = ClaimPDF, ) @@ -2145,7 +2158,7 @@ def hr_filter_opts(): # PDF export method from .helpers import InvoicePDF - s3db.set_method("fin", "voucher_invoice", + s3db.set_method("fin_voucher_invoice", method = "record", action = InvoicePDF, ) @@ -2452,7 +2465,7 @@ def prep(r): # Add invite-method for ORG_GROUP_ADMIN role from .helpers import InviteUserOrg - s3db.set_method("org", "organisation", + s3db.set_method("org_organisation", method = "invite", action = InviteUserOrg, ) @@ -3185,7 +3198,7 @@ def customise_org_facility_resource(r, tablename): # Custom method to produce KV report from .helpers import TestFacilityInfo - s3db.set_method("org", "facility", + s3db.set_method("org_facility", method = "info", action = TestFacilityInfo, ) @@ -3329,7 +3342,6 @@ def postp(r, output): # Override list-button to go to summary buttons = output.get("buttons") if isinstance(buttons, dict) and "list_btn" in buttons: - from core import S3CRUD summary = r.url(method="summary", id="", component="") buttons["list_btn"] = S3CRUD.crud_button(label = T("List Facilities"), _href = summary, @@ -4405,7 +4417,7 @@ def customise_req_req_resource(r, tablename): if auth.s3_has_role("SUPPLY_COORDINATOR"): # Custom method to register a shipment from .requests import RegisterShipment - s3db.set_method("req", "req", + s3db.set_method("req_req", method = "ship", action = RegisterShipment, ) @@ -4714,7 +4726,6 @@ def postp(r, output): stable = s3db.org_site # Default action buttons (except delete) - from core import S3CRUD S3CRUD.action_buttons(r, deletable =False) if has_role("SUPPLY_COORDINATOR"): diff --git a/modules/templates/RLPPTM/controllers.py b/modules/templates/RLPPTM/controllers.py index 763a358833..ad70951906 100644 --- a/modules/templates/RLPPTM/controllers.py +++ b/modules/templates/RLPPTM/controllers.py @@ -11,7 +11,7 @@ from gluon.storage import Storage from core import FS, ICON, IS_PHONE_NUMBER_MULTI, JSONERRORS, S3CRUD, S3CustomController, \ - S3GroupedOptionsWidget, S3LocationSelector, S3Represent, S3Request, \ + S3GroupedOptionsWidget, S3LocationSelector, S3Represent, CRUDRequest, \ S3WithIntro, s3_comments_widget, s3_get_extension, s3_mark_required, \ s3_str, s3_text_represent, s3_truncate @@ -832,7 +832,7 @@ def __call__(self): s3 = response.s3 representation = s3_get_extension(request) or \ - S3Request.DEFAULT_REPRESENTATION + CRUDRequest.DEFAULT_REPRESENTATION # Pagination get_vars = request.get_vars @@ -849,7 +849,7 @@ def __call__(self): distinct = False dtargs = {} - if representation in S3Request.INTERACTIVE_FORMATS: + if representation in CRUDRequest.INTERACTIVE_FORMATS: # How many records per page? if s3.dataTable_pageLength: @@ -965,7 +965,7 @@ def __call__(self): '"draw":%s,' \ '"data":[]}' % (totalrows, list_id, draw) else: - S3Request("auth", "user").error(415, current.ERROR.BAD_FORMAT) + CRUDRequest("auth", "user").error(415, current.ERROR.BAD_FORMAT) return output diff --git a/modules/templates/RLPPTM/cwa.py b/modules/templates/RLPPTM/cwa.py index 22127e65f9..55ec8ba0a0 100644 --- a/modules/templates/RLPPTM/cwa.py +++ b/modules/templates/RLPPTM/cwa.py @@ -38,7 +38,7 @@ def apply_method(self, r, **attr): """ Page-render entry point for REST interface. - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes """ @@ -58,7 +58,7 @@ def register(self, r, **attr): """ Register a test result - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes """ @@ -410,7 +410,7 @@ def certify(r, **attr): """ Generate a test certificate (PDF) for download - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes """ @@ -507,7 +507,7 @@ def cwaretry(r, **attr): """ Retry sending test result to CWA result server - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes """ diff --git a/modules/templates/RLPPTM/helpers.py b/modules/templates/RLPPTM/helpers.py index 5e16c66318..9630d40506 100644 --- a/modules/templates/RLPPTM/helpers.py +++ b/modules/templates/RLPPTM/helpers.py @@ -272,7 +272,7 @@ def restrict_data_formats(r): """ Restrict data exports (prevent S3XML/S3JSON of records) - @param r: the S3Request + @param r: the CRUDRequest """ settings = current.deployment_settings @@ -1585,7 +1585,7 @@ def apply_method(self, r, **attr): """ Page-render entry point for REST interface. - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes """ @@ -1608,7 +1608,7 @@ def invite(self, r, **attr): """ Prepare and process invitation form - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes """ @@ -1827,7 +1827,7 @@ def apply_method(self, r, **attr): """ Generate a PDF of an Invoice - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes """ @@ -1864,7 +1864,7 @@ def invoice_header(cls, r): """ Generate the invoice header - @param r: the S3Request + @param r: the CRUDRequest """ T = current.T @@ -1909,7 +1909,7 @@ def invoice(cls, r): """ Generate the invoice body - @param r: the S3Request + @param r: the CRUDRequest """ T = current.T @@ -1975,7 +1975,7 @@ def invoice_footer(r): """ Generate the invoice footer - @param r: the S3Request + @param r: the CRUDRequest """ T = current.T @@ -2144,7 +2144,7 @@ def apply_method(self, r, **attr): """ Generate a PDF of a Claim - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes """ @@ -2199,7 +2199,7 @@ def claim_header(cls, r): """ Generate the claim header - @param r: the S3Request + @param r: the CRUDRequest """ T = current.T @@ -2248,7 +2248,7 @@ def claim(cls, r): """ Generate the claim body - @param r: the S3Request + @param r: the CRUDRequest """ T = current.T @@ -2314,7 +2314,7 @@ def claim_footer(r): """ Generate the claim footer - @param r: the S3Request + @param r: the CRUDRequest """ T = current.T @@ -2494,7 +2494,7 @@ def apply_method(self, r, **attr): """ Report test facility information - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes """ diff --git a/modules/templates/RLPPTM/maintenance.py b/modules/templates/RLPPTM/maintenance.py index a9a6cf961a..a2204aafc8 100644 --- a/modules/templates/RLPPTM/maintenance.py +++ b/modules/templates/RLPPTM/maintenance.py @@ -193,6 +193,7 @@ def cleanup_public_registry(): from core import s3_str errors = [] + update_super = s3db.update_super for row in rows: organisation = row.org_organisation @@ -201,6 +202,7 @@ def cleanup_public_registry(): # Mark facility as obsolete facility.update_record(obsolete = True) + update_super(ftable, facility) # Prepare data for notification template place = location.L4 if location.L4 else location.L3 diff --git a/modules/templates/RLPPTM/requests.py b/modules/templates/RLPPTM/requests.py index 9012cdcf0d..465f4b1eb8 100644 --- a/modules/templates/RLPPTM/requests.py +++ b/modules/templates/RLPPTM/requests.py @@ -359,7 +359,7 @@ def apply_method(self, r, **attr): """ Entry point for REST interface. - @param r: the S3Request instance + @param r: the CRUDRequest instance @param attr: controller attributes """ diff --git a/modules/templates/RLPPTM/vouchers.py b/modules/templates/RLPPTM/vouchers.py index 69b55400a8..60763e4c74 100644 --- a/modules/templates/RLPPTM/vouchers.py +++ b/modules/templates/RLPPTM/vouchers.py @@ -10,7 +10,7 @@ from gluon import current -from core.io.codecs.card import S3PDFCardLayout +from core.resource.codecs.card import S3PDFCardLayout from core import s3_str # Fonts we use in this layout diff --git a/modules/templates/SHARE/config.py b/modules/templates/SHARE/config.py index 4ae09f29db..6e724bccc5 100644 --- a/modules/templates/SHARE/config.py +++ b/modules/templates/SHARE/config.py @@ -1584,7 +1584,7 @@ def customise_req_need_controller(**attr): vars = {})) # Custom commit method to create an Activity Group from a Need - current.s3db.set_method("req", "need", + current.s3db.set_method("req_need", method = "commit", action = req_need_commit) @@ -1711,7 +1711,7 @@ def customise_req_need_line_resource(r, tablename): f.label = T("GN") # Custom method to (manually) update homepage statistics - s3db.set_method("req", "need_line", + s3db.set_method("req_need_line", method = "update_stats", action = req_need_line_update_stats, ) @@ -1866,7 +1866,7 @@ def customise_req_need_line_controller(**attr): ) # Custom commit method to create an Activity from a Need Line - s3db.set_method("req", "need_line", + s3db.set_method("req_need_line", method = "commit", action = req_need_line_commit) diff --git a/modules/templates/UCCE/config.py b/modules/templates/UCCE/config.py index e14254dec7..e3cea27223 100644 --- a/modules/templates/UCCE/config.py +++ b/modules/templates/UCCE/config.py @@ -490,16 +490,16 @@ def customise_dc_question_controller(**attr): from templates.UCCE.controllers import dc_QuestionSave set_method = current.s3db.set_method - set_method("dc", "question", + set_method("dc_question", method = "create_json", action = dc_QuestionCreate()) - set_method("dc", "question", + set_method("dc_question", method = "image_delete", action = dc_QuestionImageDelete()) - set_method("dc", "question", + set_method("dc_question", method = "image_upload", action = dc_QuestionImageUpload()) - set_method("dc", "question", + set_method("dc_question", method = "update_json", action = dc_QuestionSave()) @@ -675,28 +675,28 @@ def customise_dc_target_controller(**attr): from templates.UCCE.controllers import dc_TargetReportFilters set_method = current.s3db.set_method - set_method("dc", "target", + set_method("dc_target", method = "activate", action = dc_TargetActivate()) - set_method("dc", "target", + set_method("dc_target", method = "deactivate", action = dc_TargetDeactivate()) - set_method("dc", "target", + set_method("dc_target", method = "delete_confirm", action = dc_TargetDelete()) - set_method("dc", "target", + set_method("dc_target", method = "edit_confirm", action = dc_TargetEdit()) - set_method("dc", "target", + set_method("dc_target", method = "name", action = dc_TargetName()) - set_method("dc", "target", + set_method("dc_target", method = "l10n", action = dc_TargetL10n()) - set_method("dc", "target", + set_method("dc_target", method = "report_custom", action = dc_TargetReport()) - set_method("dc", "target", + set_method("dc_target", method = "report_filters", action = dc_TargetReportFilters()) @@ -786,19 +786,19 @@ def customise_dc_template_controller(**attr): from templates.UCCE.controllers import dc_TemplateSave set_method = s3db.set_method - set_method("dc", "template", + set_method("dc_template", method = "editor", action = dc_TemplateEditor()) - set_method("dc", "template", + set_method("dc_template", method = "export_l10n", action = dc_TemplateExportL10n()) - set_method("dc", "template", + set_method("dc_template", method = "upload_l10n", action = dc_TemplateImportL10n()) - set_method("dc", "template", + set_method("dc_template", method = "update_json", action = dc_TemplateSave()) @@ -1167,7 +1167,7 @@ def customise_project_project_controller(**attr): from templates.UCCE.controllers import dc_ProjectDelete s3db = current.s3db - s3db.set_method("project", "project", + s3db.set_method("project_project", method = "delete_confirm", action = dc_ProjectDelete()) @@ -1184,10 +1184,7 @@ def prep(r): if r.id and not r.component and r.representation == "xls": # Custom XLS Exporter to include all Responses. - r.set_handler("read", s3db.dc_TargetXLS(), - http = ("GET", "POST"), - representation = "xls" - ) + r.custom_action = s3db.dc_TargetXLS elif r.component_name == "target": ltable = s3db.project_l10n l10n = current.db(ltable.project_id == r.id).select(ltable.language, diff --git a/modules/templates/UCCE/controllers.py b/modules/templates/UCCE/controllers.py index 76c6dd8f5d..6865820055 100644 --- a/modules/templates/UCCE/controllers.py +++ b/modules/templates/UCCE/controllers.py @@ -446,7 +446,7 @@ def apply_method(self, r, **attr): """ Entry point for REST API - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller arguments """ @@ -495,7 +495,7 @@ def apply_method(self, r, **attr): """ Entry point for REST API - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller arguments """ @@ -539,7 +539,7 @@ def apply_method(self, r, **attr): """ Entry point for REST API - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller arguments """ @@ -589,7 +589,7 @@ def apply_method(self, r, **attr): """ Entry point for REST API - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller arguments """ @@ -679,7 +679,7 @@ def apply_method(self, r, **attr): """ Entry point for REST API - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller arguments """ @@ -893,7 +893,7 @@ def apply_method(self, r, **attr): """ Entry point for REST API - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller arguments """ @@ -943,7 +943,7 @@ def apply_method(self, r, **attr): """ Entry point for REST API - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller arguments """ @@ -1080,7 +1080,7 @@ def apply_method(self, r, **attr): """ Entry point for REST API - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller arguments """ @@ -1191,7 +1191,7 @@ def apply_method(self, r, **attr): """ Entry point for REST API - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller arguments """ @@ -1250,7 +1250,7 @@ def apply_method(self, r, **attr): """ Entry point for REST API - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller arguments """ @@ -1329,7 +1329,7 @@ def apply_method(self, r, **attr): """ Entry point for REST API - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller arguments """ @@ -2075,7 +2075,7 @@ def pdf(data): raise NotImplementedError - from core.io.codecs.pdf import EdenDocTemplate, S3RL_PDF + from core.resource.codecs.pdf import EdenDocTemplate, S3RL_PDF # etc, etc @@ -2107,7 +2107,7 @@ def apply_method(self, r, **attr): """ Entry point for REST API - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller arguments """ @@ -2293,7 +2293,7 @@ def apply_method(self, r, **attr): """ Entry point for REST API - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller arguments """ @@ -2690,7 +2690,7 @@ def apply_method(self, r, **attr): """ Entry point for REST API - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller arguments """ @@ -2700,7 +2700,7 @@ def apply_method(self, r, **attr): # No need to check for 'read' permission within single-record methods, as that has already been checked - from core.io.codecs.xls import S3XLS + from core.resource.codecs.xls import S3XLS try: import xlwt @@ -2961,7 +2961,7 @@ def apply_method(self, r, **attr): """ Entry point for REST API - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller arguments """ @@ -3165,7 +3165,7 @@ def apply_method(self, r, **attr): """ Entry point for REST API - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller arguments """ @@ -3206,7 +3206,7 @@ def apply_method(self, r, **attr): """ Entry point for REST API - @param r: the S3Request + @param r: the CRUDRequest @param attr: controller arguments """ diff --git a/modules/unit_tests/core/benchmark.py b/modules/unit_tests/core/benchmark.py index d0a072dfb0..38a156debb 100644 --- a/modules/unit_tests/core/benchmark.py +++ b/modules/unit_tests/core/benchmark.py @@ -19,15 +19,15 @@ # When running these tests in order to optimize the environment, you can # use these benchmarks as a rough guideline for what you can expect: # -# S3Model.configure = 2.90979003906 µs -# S3Model.get_config = 2.2715420723 µs -# S3Model.table(non-table) = 2.13528013229 µs -# S3Model.get(non-table) = 2.0619969368 µs -# S3Model.__getattr__(non-table) = 4.12402009964 µs -# S3Model.__getitem__(non-table) = 4.24327015877 µs -# S3Model.table = 2.91769790649 µs -# S3Model.__getattr__ = 4.959856987 µs -# S3Model.__getitem__ = 5.19830703735 µs +# DataModel.configure = 2.90979003906 µs +# DataModel.get_config = 2.2715420723 µs +# DataModel.table(non-table) = 2.13528013229 µs +# DataModel.get(non-table) = 2.0619969368 µs +# DataModel.__getattr__(non-table) = 4.12402009964 µs +# DataModel.__getitem__(non-table) = 4.24327015877 µs +# DataModel.table = 2.91769790649 µs +# DataModel.__getattr__ = 4.959856987 µs +# DataModel.__getitem__ = 5.19830703735 µs # S3Resource.import_xml = 12.0009431839 ms (=83 rec/sec) # S3Resource.export (incl. DB extraction) = 3.75156188011 ms (=266 rec/sec) # S3Resource.export (w/o DB extraction) = 1.7192029953 ms (=581 rec/sec) @@ -84,7 +84,7 @@ def testDBSelect(self): mlt = timeit.Timer(x).timeit(number = 10) * (100/n) info("db.select = %s ms/query (=%s q/sec)" % (mlt, int(1000/mlt))) - def testS3ModelTable(self): + def testDataModelTable(self): s3db = current.s3db @@ -93,20 +93,20 @@ def testS3ModelTable(self): if table is not None: x = lambda: s3db.table("pr_person") mlt = timeit.Timer(x).timeit() - info("S3Model.table = %s µs" % mlt) + info("DataModel.table = %s µs" % mlt) self.assertTrue(mlt<10) x = lambda: s3db.pr_person mlt = timeit.Timer(x).timeit() - info("S3Model.__getattr__ = %s µs" % mlt) + info("DataModel.__getattr__ = %s µs" % mlt) self.assertTrue(mlt<10) x = lambda: s3db["pr_person"] mlt = timeit.Timer(x).timeit() - info("S3Model.__getitem__ = %s µs" % mlt) + info("DataModel.__getitem__ = %s µs" % mlt) self.assertTrue(mlt<10) - def testS3ModelName(self): + def testDataModelName(self): s3db = current.s3db @@ -115,25 +115,25 @@ def testS3ModelName(self): if func is not None: x = lambda: s3db.table("pr_person_represent") mlt = timeit.Timer(x).timeit() - info("S3Model.table(non-table) = %s µs" % mlt) + info("DataModel.table(non-table) = %s µs" % mlt) self.assertTrue(mlt<10) x = lambda: s3db.get("pr_person_represent") mlt = timeit.Timer(x).timeit() - info("S3Model.get(non-table) = %s µs" % mlt) + info("DataModel.get(non-table) = %s µs" % mlt) self.assertTrue(mlt<10) x = lambda: s3db.pr_person_represent mlt = timeit.Timer(x).timeit() - info("S3Model.__getattr__(non-table) = %s µs" % mlt) + info("DataModel.__getattr__(non-table) = %s µs" % mlt) self.assertTrue(mlt<10) x = lambda: s3db["pr_person_represent"] mlt = timeit.Timer(x).timeit() - info("S3Model.__getitem__(non-table) = %s µs" % mlt) + info("DataModel.__getitem__(non-table) = %s µs" % mlt) self.assertTrue(mlt<10) - def testS3ModelConfigure(self): + def testDataModelConfigure(self): s3db = current.s3db @@ -141,13 +141,13 @@ def testS3ModelConfigure(self): configure = s3db.configure x = lambda: configure("pr_person", testconfig = "Test") mlt = timeit.Timer(x).timeit() - info("S3Model.configure = %s µs" % mlt) + info("DataModel.configure = %s µs" % mlt) self.assertTrue(mlt<10) get_config = s3db.get_config x = lambda: get_config("pr_person", "testconfig") mlt = timeit.Timer(x).timeit() - info("S3Model.get_config = %s µs" % mlt) + info("DataModel.get_config = %s µs" % mlt) self.assertTrue(mlt<10) def testS3ResourceInit(self): diff --git a/modules/unit_tests/core/s3cfg.py b/modules/unit_tests/core/s3cfg.py index 9159a52e70..e56d9b0d13 100644 --- a/modules/unit_tests/core/s3cfg.py +++ b/modules/unit_tests/core/s3cfg.py @@ -56,7 +56,7 @@ def testSetOrgDependentFields(self): tree = etree.ElementTree(etree.fromstring(xmlstr)) resource = s3db.resource("org_organisation") - msg = resource.import_xml(tree) + resource.import_xml(tree) resource = s3db.resource("org_organisation", uid="ExampleRootOrg") org = resource.select(None, as_rows=True)[0] diff --git a/modules/unit_tests/core/s3crud.py b/modules/unit_tests/core/s3crud.py index 1692ebce18..f96533fc6f 100644 --- a/modules/unit_tests/core/s3crud.py +++ b/modules/unit_tests/core/s3crud.py @@ -15,6 +15,8 @@ from unit_tests import run_suite +from core import S3CRUD + # ============================================================================= class ValidateTests(unittest.TestCase): """ Test S3CRUD/validate """ @@ -41,7 +43,7 @@ def testValidateMainTable(self): """ Test successful main table validation """ request = self.request - crud = self.resource.crud + crud = S3CRUD() jsonstr = """{"name":"TestOrganisation", "acronym":"TO"}""" request.body = StringIO(jsonstr) @@ -72,7 +74,7 @@ def testValidateMainTableError(self): """ Test error in main table validation """ request = self.request - crud = self.resource.crud + crud = S3CRUD() jsonstr = """{"name":"", "acronym":"TO"}""" request.body = StringIO(jsonstr) @@ -102,7 +104,7 @@ def testValidateComponentTable(self): """ Test successful component validation """ request = self.request - crud = self.resource.crud + crud = S3CRUD() jsonstr = """{"name":"TestOffice"}""" request.body = StringIO(jsonstr) @@ -127,7 +129,7 @@ def testValidateComponentTableFailure(self): """ Test error in component validation """ request = self.request - crud = self.resource.crud + crud = S3CRUD() jsonstr = """{"name":"", "acronym":"test"}""" request.body = StringIO(jsonstr) @@ -172,7 +174,7 @@ def testTypeConversionFeature(self): representation="json", http="GET") - crud = resource.crud + crud = S3CRUD() jsonstr = """{"organisation_id":"1", "role":"1"}""" request.body = StringIO(jsonstr) diff --git a/modules/unit_tests/core/s3hierarchy.py b/modules/unit_tests/core/s3hierarchy.py index 3700be1286..7578e7e120 100644 --- a/modules/unit_tests/core/s3hierarchy.py +++ b/modules/unit_tests/core/s3hierarchy.py @@ -172,7 +172,7 @@ def testHierarchyConstruction(self): def testPreprocessCreateNode(self): """ Test preprocessing of a create-node request """ - r = s3_request("test", "hierarchy", http="POST") + r = crud_request("test", "hierarchy", http="POST") parent_node = self.rows["HIERARCHY1"] parent_id = parent_node.id @@ -875,7 +875,7 @@ def testHierarchyConstruction(self): def testPreprocessCreateNode(self): """ Test preprocessing of a create-node request """ - r = s3_request("test", "lhierarchy", http="POST") + r = crud_request("test", "lhierarchy", http="POST") parent_node = self.rows["LHIERARCHY1"] h = S3Hierarchy("test_lhierarchy") @@ -892,7 +892,7 @@ def testPreprocessCreateNode(self): def testPostprocessCreateNode(self): """ Test postprocessing of a create-node request """ - r = s3_request("test", "lhierarchy", http="POST") + r = crud_request("test", "lhierarchy", http="POST") parent_node = self.rows["LHIERARCHY1"] h = S3Hierarchy("test_lhierarchy") diff --git a/modules/unit_tests/core/s3import.py b/modules/unit_tests/core/s3import.py index f1e0f57ecc..c7c5288fec 100644 --- a/modules/unit_tests/core/s3import.py +++ b/modules/unit_tests/core/s3import.py @@ -13,8 +13,8 @@ from gluon.storage import Storage from lxml import etree -from core import S3Duplicate, S3ImportItem, S3ImportJob, s3_meta_fields -from core.io.importer import S3ObjectReferences +from core import S3Duplicate, ImportItem, ImportJob, s3_meta_fields +from core.resource.importer import ObjectReferences from unit_tests import run_suite @@ -163,7 +163,7 @@ def testOrganisationBranchImport(self): current.auth.override = True resource = s3db.resource("org_organisation") - msg = resource.import_xml(self.branch_tree) + resource.import_xml(self.branch_tree) table = resource.table @@ -190,7 +190,7 @@ def testParentImport(self): current.auth.override = True resource = s3db.resource("org_organisation") - msg = resource.import_xml(self.parent_tree) + resource.import_xml(self.parent_tree) table = resource.table @@ -312,7 +312,7 @@ def testFailedReferenceExplicit(self): resource = current.s3db.resource("org_office") result = resource.import_xml(tree) - msg = json.loads(result) + msg = json.loads(result.json_message()) self.assertEqual(msg["status"], "failed") error_resources = list(msg["tree"].keys()) @@ -359,7 +359,7 @@ def testFailedReferenceInline(self): resource = current.s3db.resource("org_office") result = resource.import_xml(tree) - msg = json.loads(result) + msg = json.loads(result.json_message()) self.assertEqual(msg["status"], "failed") error_resources = list(msg["tree"].keys()) @@ -418,7 +418,7 @@ def tearDownClass(cls): def setUp(self): # Create a dummy import job - self.job = S3ImportJob(current.db.dedup_test) + self.job = ImportJob(current.db.dedup_test) db = current.db table = db.dedup_test @@ -450,7 +450,7 @@ def testMatch(self): ) # Dummy item for testing - item = S3ImportItem(self.job) + item = ImportItem(self.job) item.table = current.db.dedup_test ids = self.ids @@ -500,7 +500,7 @@ def testDefaults(self): deduplicate = S3Duplicate() # Dummy item for testing - item = S3ImportItem(self.job) + item = ImportItem(self.job) item.table = current.db.dedup_test ids = self.ids @@ -540,7 +540,7 @@ def testExceptions(self): # Dummy item for testing - item = S3ImportItem(self.job) + item = ImportItem(self.job) item.table = current.db.dedup_test # Test invalid primary @@ -609,7 +609,7 @@ def testMtimeImport(self): # Import the data resource = s3db.resource("org_facility") - result = resource.import_xml(tree) + resource.import_xml(tree) # Verify outer resource resource = s3db.resource("org_facility", uid="MTFAC") @@ -623,7 +623,7 @@ def testMtimeImport(self): # ============================================================================= class ObjectReferencesTests(unittest.TestCase): - """ Tests for S3ObjectReferences """ + """ Tests for ObjectReferences """ # ------------------------------------------------------------------------- def testDiscoverFromObject(self): @@ -639,7 +639,7 @@ def testDiscoverFromObject(self): "key3": "value_3", } - refs = S3ObjectReferences(obj).refs + refs = ObjectReferences(obj).refs assertTrue(isinstance(refs, list)) assertEqual(len(refs), 1) @@ -666,7 +666,7 @@ def testDiscoverFromList(self): None, ] - refs = S3ObjectReferences(obj).refs + refs = ObjectReferences(obj).refs assertTrue(isinstance(refs, list)) assertEqual(len(refs), 1) @@ -699,7 +699,7 @@ def testDiscoverFromNested(self): None, ] - refs = S3ObjectReferences(obj).refs + refs = ObjectReferences(obj).refs assertTrue(isinstance(refs, list)) assertEqual(len(refs), 1) @@ -736,7 +736,7 @@ def testDiscoverMultiple(self): None, ] - refs = S3ObjectReferences(obj).refs + refs = ObjectReferences(obj).refs assertTrue(isinstance(refs, list)) assertEqual(len(refs), 2) @@ -776,7 +776,7 @@ def testDiscoverInvalid(self): None, ] - refs = S3ObjectReferences(obj).refs + refs = ObjectReferences(obj).refs assertTrue(isinstance(refs, list)) assertEqual(len(refs), 1) @@ -798,7 +798,7 @@ def testResolveObject(self): "key3": "value_3", } - S3ObjectReferences(obj).resolve("org_organisation", "tuid", "ORG1", 57) + ObjectReferences(obj).resolve("org_organisation", "tuid", "ORG1", 57) target = obj self.assertNotIn("$k_key_2", target) @@ -818,7 +818,7 @@ def testResolveList(self): None, ] - S3ObjectReferences(obj).resolve("org_organisation", "uuid", "ORG1", 57) + ObjectReferences(obj).resolve("org_organisation", "uuid", "ORG1", 57) target = obj[1] self.assertNotIn("$k_key_2", target) @@ -844,7 +844,7 @@ def testResolveNested(self): None, ] - S3ObjectReferences(obj).resolve("pr_person", "tuid", "PR2", 3283) + ObjectReferences(obj).resolve("pr_person", "tuid", "PR2", 3283) target = obj[1]["complex"][1] self.assertNotIn("$k_key_2", target) @@ -880,7 +880,7 @@ def testResolveMultiple(self): }, ] - refs = S3ObjectReferences(obj) + refs = ObjectReferences(obj) refs.resolve("pr_person", "tuid", "PR2", 3283) refs.resolve("org_organisation", "uuid", "ORG1", 14) @@ -921,7 +921,7 @@ def testResolveInvalid(self): None, ] - S3ObjectReferences(obj).resolve("req_req", "uuid", "REQ0928", 3) + ObjectReferences(obj).resolve("req_req", "uuid", "REQ0928", 3) target = obj[1]["$k_key_3"][1] self.assertNotIn("$k_key_2", target) @@ -989,7 +989,7 @@ def testImpliedImport(self): # Create an import job tree = etree.fromstring(xmlstr) - job = S3ImportJob(current.db.ort_master, tree) + job = ImportJob(current.db.ort_master, tree) # Add the ort_master element to it element = tree.findall('resource[@name="ort_master"][1]')[0] diff --git a/modules/unit_tests/core/s3model.py b/modules/unit_tests/core/s3model.py index 038d1267cf..7cec321677 100644 --- a/modules/unit_tests/core/s3model.py +++ b/modules/unit_tests/core/s3model.py @@ -13,17 +13,17 @@ from gluon.storage import Storage from core import s3_meta_fields, DYNAMIC_PREFIX, IS_NOT_ONE_OF, IS_ONE_OF, IS_UTC_DATE, IS_UTC_DATETIME -from core.model.dynamic import S3DynamicModel +from core.model.dynamic import DynamicTableModel from unit_tests import run_suite # ============================================================================= -class S3ModelTests(unittest.TestCase): +class DataModelTests(unittest.TestCase): pass # ============================================================================= -class S3SuperEntityTests(unittest.TestCase): +class SuperEntityTests(unittest.TestCase): # ------------------------------------------------------------------------- @classmethod @@ -234,7 +234,7 @@ def testDeleteSuperRestrict(self): self.assertFalse(super_record.deleted) # ============================================================================= -class S3DynamicModelTests(unittest.TestCase): +class DynamicTableModelTests(unittest.TestCase): """ Dynamic Model Tests """ TABLENAME = "%s_test" % DYNAMIC_PREFIX @@ -277,6 +277,8 @@ def setUpClass(cls): record["id"] = record_id s3db.onaccept(ftable, record) + current.db.commit() + # ------------------------------------------------------------------------- @classmethod def tearDownClass(cls): @@ -287,6 +289,7 @@ def tearDownClass(cls): ttable = s3db.s3_table query = (ttable.name == cls.TABLENAME) current.db(query).delete() + current.db.commit() # ------------------------------------------------------------------------- def testDynamicTableInstantiationFailure(self): @@ -304,7 +307,7 @@ def testDynamicTableInstantiationFailure(self): # ------------------------------------------------------------------------- def testDynamicTableInstantiation(self): - """ Test instantiation of dynamic tables with S3Model """ + """ Test instantiation of dynamic tables with DataModel """ assertEqual = self.assertEqual assertNotEqual = self.assertNotEqual @@ -353,7 +356,7 @@ def testStringFieldConstruction(self): assertTrue = self.assertTrue assertFalse = self.assertFalse - dm = S3DynamicModel(self.TABLENAME) + dm = DynamicTableModel(self.TABLENAME) define_field = dm._field # String-field, not unique and empty allowed @@ -421,7 +424,7 @@ def testReferenceFieldConstruction(self): assertTrue = self.assertTrue assertFalse = self.assertFalse - dm = S3DynamicModel(self.TABLENAME) + dm = DynamicTableModel(self.TABLENAME) define_field = dm._field # Reference-field, empty allowed @@ -466,7 +469,7 @@ def testBooleanFieldConstruction(self): assertTrue = self.assertTrue assertFalse = self.assertFalse - dm = S3DynamicModel(self.TABLENAME) + dm = DynamicTableModel(self.TABLENAME) define_field = dm._field from core import s3_yes_no_represent @@ -504,7 +507,7 @@ def testIntegerFieldConstruction(self): assertTrue = self.assertTrue assertFalse = self.assertFalse - dm = S3DynamicModel(self.TABLENAME) + dm = DynamicTableModel(self.TABLENAME) define_field = dm._field # Integer-field @@ -579,7 +582,7 @@ def testDoubleFieldConstruction(self): assertTrue = self.assertTrue assertFalse = self.assertFalse - dm = S3DynamicModel(self.TABLENAME) + dm = DynamicTableModel(self.TABLENAME) define_field = dm._field # Double-field @@ -634,7 +637,7 @@ def testDateFieldConstruction(self): assertTrue = self.assertTrue assertFalse = self.assertFalse - dm = S3DynamicModel(self.TABLENAME) + dm = DynamicTableModel(self.TABLENAME) define_field = dm._field # Date-field @@ -751,7 +754,7 @@ def testDateTimeFieldConstruction(self): assertTrue = self.assertTrue assertFalse = self.assertFalse - dm = S3DynamicModel(self.TABLENAME) + dm = DynamicTableModel(self.TABLENAME) define_field = dm._field # Datetime-field @@ -876,7 +879,7 @@ def testOptionFieldConstruction(self): T = current.T T.force("en") # Options sort order depends on language - dm = S3DynamicModel(self.TABLENAME) + dm = DynamicTableModel(self.TABLENAME) define_field = dm._field # Options-field @@ -968,7 +971,7 @@ def testOptionFieldConstruction(self): ]) # ============================================================================= -class S3DynamicComponentTests(unittest.TestCase): +class DynamicComponentTests(unittest.TestCase): """ Dynamic Component Tests """ TABLENAME = "%s_test_component" % DYNAMIC_PREFIX @@ -1018,6 +1021,8 @@ def setUpClass(cls): record["id"] = record_id s3db.onaccept(ftable, record) + current.db.commit() + # ------------------------------------------------------------------------- @classmethod def tearDownClass(cls): @@ -1028,6 +1033,7 @@ def tearDownClass(cls): ttable = s3db.s3_table query = (ttable.name == cls.TABLENAME) current.db(query).delete() + current.db.commit() # ------------------------------------------------------------------------- def setUp(self): @@ -1090,10 +1096,10 @@ def testFieldSelectorResolution(self): if __name__ == "__main__": run_suite( - #S3ModelTests, - S3SuperEntityTests, - S3DynamicModelTests, - S3DynamicComponentTests, + #DataModelTests, + SuperEntityTests, + DynamicTableModelTests, + DynamicComponentTests, ) # END ======================================================================== diff --git a/modules/unit_tests/core/s3msg.py b/modules/unit_tests/core/s3msg.py index 3c20808cc5..1b8975aa6a 100644 --- a/modules/unit_tests/core/s3msg.py +++ b/modules/unit_tests/core/s3msg.py @@ -67,19 +67,19 @@ def setUpClass(cls): # Import the test entities resource = s3db.resource("pr_person") - resource.import_xml(xmltree) - if resource.error is not None: - raise AssertionError("Test data import failed: %s" % resource.error) + result = resource.import_xml(xmltree) + if result.error is not None: + raise AssertionError("Test data import failed: %s" % result.error) resource = s3db.resource("org_organisation") - resource.import_xml(xmltree) - if resource.error is not None: - raise AssertionError("Test data import failed: %s" % resource.error) + result = resource.import_xml(xmltree) + if result.error is not None: + raise AssertionError("Test data import failed: %s" % result.error) resource = s3db.resource("pr_group") - resource.import_xml(xmltree) - if resource.error is not None: - raise AssertionError("Test data import failed: %s" % resource.error) + result = resource.import_xml(xmltree) + if result.error is not None: + raise AssertionError("Test data import failed: %s" % result.error) # ------------------------------------------------------------------------- @classmethod diff --git a/modules/unit_tests/core/s3resource.py b/modules/unit_tests/core/s3resource.py index 842116307d..4fcee33f50 100644 --- a/modules/unit_tests/core/s3resource.py +++ b/modules/unit_tests/core/s3resource.py @@ -162,6 +162,8 @@ class ResourceAxisFilterTests(unittest.TestCase): def testListTypeFilter(self): """ Test list:type axis value filtering """ + from core.methods.report import S3AxisFilter + assertTrue = self.assertTrue assertFalse = self.assertFalse @@ -682,9 +684,9 @@ def testImportXML(self): xmltree = etree.ElementTree(etree.fromstring(xmlstr)) resource = current.s3db.resource("pr_person") - msg = resource.import_xml(xmltree) + result = resource.import_xml(xmltree) - msg = json.loads(msg) + msg = json.loads(result.json_message()) assertEqual(msg["status"], "success") assertEqual(msg["statuscode"], "200") @@ -713,8 +715,8 @@ def testImportXMLWithMTime(self): xmltree = etree.ElementTree(etree.fromstring(xmlstr)) resource = current.s3db.resource("importer_test") - resource.import_xml(xmltree) - self.assertEqual(s3_utc(resource.mtime), + result = resource.import_xml(xmltree) + self.assertEqual(s3_utc(result.mtime), s3_utc(datetime.datetime(2012, 4, 21, 0, 0, 0))) # ------------------------------------------------------------------------- @@ -731,10 +733,10 @@ def testImportXMLWithoutMTime(self): xmltree = etree.ElementTree(etree.fromstring(xmlstr)) resource = current.s3db.resource("importer_test") - resource.import_xml(xmltree) + result = resource.import_xml(xmltree) # Can't compare with exactly utcnow as these would be milliseconds apart, # assume equal dates are sufficient for this test - self.assertEqual(s3_utc(resource.mtime).date(), + self.assertEqual(s3_utc(result.mtime).date(), s3_utc(datetime.datetime.utcnow()).date()) # ------------------------------------------------------------------------- @@ -754,8 +756,8 @@ def testImportXMLWithPartialMTime(self): xmltree = etree.ElementTree(etree.fromstring(xmlstr)) resource = current.s3db.resource("importer_test") - resource.import_xml(xmltree) - self.assertEqual(s3_utc(resource.mtime).date(), + result = resource.import_xml(xmltree) + self.assertEqual(s3_utc(result.mtime).date(), s3_utc(datetime.datetime.utcnow()).date()) # ============================================================================= diff --git a/modules/unit_tests/core/s3rest.py b/modules/unit_tests/core/s3rest.py index ef9f66e3a5..bde977911b 100644 --- a/modules/unit_tests/core/s3rest.py +++ b/modules/unit_tests/core/s3rest.py @@ -12,7 +12,7 @@ from gluon import * from gluon.storage import Storage -from core import S3Request +from core import CRUDRequest from unit_tests import run_suite @@ -46,17 +46,17 @@ def testPOSTFilter(self): request.env.content_type = "multipart/form-data" # Test with valid filter expression JSON - r = S3Request(prefix = "org", - name = "organisation", - http = "POST", - get_vars = {"$search": "form", "test": "retained"}, - post_vars = {"~.name|~.comments__like": '''["first","second"]''', - "~.other_field__lt": '''"1"''', - "multi.nonstr__belongs": '''[1,2,3]''', - "service_organisation.service_id__belongs": "1", - "other": "testing", - }, - ) + r = CRUDRequest(prefix = "org", + name = "organisation", + http = "POST", + get_vars = {"$search": "form", "test": "retained"}, + post_vars = {"~.name|~.comments__like": '''["first","second"]''', + "~.other_field__lt": '''"1"''', + "multi.nonstr__belongs": '''[1,2,3]''', + "service_organisation.service_id__belongs": "1", + "other": "testing", + }, + ) # Method changed to GET: assertEqual(r.http, "GET") @@ -86,14 +86,14 @@ def testPOSTFilter(self): assertEqual(get_vars.get("test"), "retained") # Test without $search - r = S3Request(prefix = "org", - name = "organisation", - http = "POST", - get_vars = {"test": "retained"}, - post_vars = {"service_organisation.service_id__belongs": "1", - "other": "testing", - }, - ) + r = CRUDRequest(prefix = "org", + name = "organisation", + http = "POST", + get_vars = {"test": "retained"}, + post_vars = {"service_organisation.service_id__belongs": "1", + "other": "testing", + }, + ) # Method should still be POST: assertEqual(r.http, "POST") @@ -124,11 +124,11 @@ def testPOSTFilterAjax(self): # Test with valid filter expression JSON jsonstr = '''{"service_organisation.service_id__belongs":"1","~.example__lt":1,"~.other__like":[1,2],"~.name__like":"*Liquiçá*"}''' request._body = BytesIO(jsonstr.encode("utf-8")) - r = S3Request(prefix = "org", - name = "organisation", - http = "POST", - get_vars = {"$search": "ajax", "test": "retained"}, - ) + r = CRUDRequest(prefix = "org", + name = "organisation", + http = "POST", + get_vars = {"$search": "ajax", "test": "retained"}, + ) # Method changed to GET: assertEqual(r.http, "GET") @@ -154,11 +154,11 @@ def testPOSTFilterAjax(self): # Test without $search request._body = BytesIO(b'{"service_organisation.service_id__belongs":"1"}') - r = S3Request(prefix = "org", - name = "organisation", - http = "POST", - get_vars = {"test": "retained"}, - ) + r = CRUDRequest(prefix = "org", + name = "organisation", + http = "POST", + get_vars = {"test": "retained"}, + ) # Method should still be POST: assertEqual(r.http, "POST") @@ -175,11 +175,11 @@ def testPOSTFilterAjax(self): # Test with valid JSON but invalid filter expression request._body = BytesIO(b'[1,2,3]') - r = S3Request(prefix = "org", - name = "organisation", - http = "POST", - get_vars = {"$search": "ajax", "test": "retained"}, - ) + r = CRUDRequest(prefix = "org", + name = "organisation", + http = "POST", + get_vars = {"$search": "ajax", "test": "retained"}, + ) # Method changed to GET: assertEqual(r.http, "GET") @@ -196,11 +196,11 @@ def testPOSTFilterAjax(self): # Test with empty body request._body = BytesIO(b'') - r = S3Request(prefix = "org", - name = "organisation", - http = "POST", - get_vars = {"$search": "ajax", "test": "retained"}, - ) + r = CRUDRequest(prefix = "org", + name = "organisation", + http = "POST", + get_vars = {"$search": "ajax", "test": "retained"}, + ) # Method changed to GET: assertEqual(r.http, "GET") @@ -235,12 +235,12 @@ def setUp(self): self.a = current.request.application self.p = str(record[ptable.id]) self.c = str(record[ctable.id]) - self.r = S3Request(prefix="pr", - name="person", - c="pr", - f="person", - args=[self.p, "contact", self.c, "method"], - vars=Storage(format="xml", test="test")) + self.r = CRUDRequest(prefix="pr", + name="person", + c="pr", + f="person", + args=[self.p, "contact", self.c, "method"], + vars=Storage(format="xml", test="test")) # ------------------------------------------------------------------------- def tearDown(self): @@ -272,12 +272,12 @@ def testURLMethodOverride(self): "/%s/pr/person/%s/contact/%s/read.xml?test=test" % (a, p, c)) # Test without component - r = S3Request(prefix="pr", - name="person", - c="pr", - f="person", - args=[self.p, "method"], - vars=Storage(format="xml", test="test")) + r = CRUDRequest(prefix="pr", + name="person", + c="pr", + f="person", + args=[self.p, "method"], + vars=Storage(format="xml", test="test")) # No change self.assertEqual(r.url(method=None), @@ -378,12 +378,12 @@ def testURLComponentIDOverride(self): def testURLTargetOverrideMaster(self): (a, p, c, r) = (self.a, self.p, self.c, self.r) - r = S3Request(prefix="pr", - name="person", - c="pr", - f="person", - args=[self.p, "method"], - vars=Storage(format="xml", test="test")) + r = CRUDRequest(prefix="pr", + name="person", + c="pr", + f="person", + args=[self.p, "method"], + vars=Storage(format="xml", test="test")) # No change self.assertEqual(r.url(target=None), @@ -453,12 +453,12 @@ def testURLCombinations(self): "/%s/pr/person/%s/contact/deduplicate.xml" % (a, p)) # Test request with component (without component ID) - r = S3Request(prefix="pr", - name="person", - c="pr", - f="person", - args=[self.p, "contact", "method"], - vars=Storage(format="xml", test="test")) + r = CRUDRequest(prefix="pr", + name="person", + c="pr", + f="person", + args=[self.p, "contact", "method"], + vars=Storage(format="xml", test="test")) self.assertEqual(r.url(method="", id=5), "/%s/pr/person/5/contact.xml?test=test" % a) @@ -470,12 +470,12 @@ def testURLCombinations(self): "/%s/pr/person/%s/contact/deduplicate.xml" % (a, p)) # Test request without component - r = S3Request(prefix="pr", - name="person", - c="pr", - f="person", - args=[self.p, "method"], - vars=Storage(format="xml", test="test")) + r = CRUDRequest(prefix="pr", + name="person", + c="pr", + f="person", + args=[self.p, "method"], + vars=Storage(format="xml", test="test")) self.assertEqual(r.url(method="", id=5), "/%s/pr/person/5.xml?test=test" % a) diff --git a/modules/unit_tests/core/s3sync.py b/modules/unit_tests/core/s3sync.py index 2bca68cbe6..e2004a0358 100644 --- a/modules/unit_tests/core/s3sync.py +++ b/modules/unit_tests/core/s3sync.py @@ -159,8 +159,8 @@ def setUp(self): xmltree = etree.ElementTree(etree.fromstring(xmlstr)) resource = current.s3db.resource("org_organisation") - resource.import_xml(xmltree) - self.assertEqual(resource.error, None) + result = resource.import_xml(xmltree) + self.assertEqual(result.error, None) def testImportMerge(self): @@ -197,8 +197,8 @@ def testImportMerge(self): xmltree = etree.ElementTree(etree.fromstring(xmlstr)) resource = current.s3db.resource("org_organisation") - msg = resource.import_xml(xmltree) - self.assertEqual(resource.error, None) + result = resource.import_xml(xmltree) + self.assertEqual(result.error, None) # Check the result resource = s3db.resource("org_organisation", @@ -240,8 +240,8 @@ def setUp(self): xmltree = etree.ElementTree(etree.fromstring(xmlstr)) resource = current.s3db.resource("org_organisation") - resource.import_xml(xmltree) - self.assertEqual(resource.error, None) + result = resource.import_xml(xmltree) + self.assertEqual(result.error, None) def testImportMerge(self): @@ -274,8 +274,8 @@ def testImportMerge(self): xmltree = etree.ElementTree(etree.fromstring(xmlstr)) resource = current.s3db.resource("org_organisation") - msg = resource.import_xml(xmltree) - self.assertEqual(resource.error, None) + result = resource.import_xml(xmltree) + self.assertEqual(result.error, None) # Check the result: the duplicate should never be imported # Note that no components of the deleted duplicate would ever @@ -315,8 +315,8 @@ def setUp(self): xmltree = etree.ElementTree(etree.fromstring(xmlstr)) resource = current.s3db.resource("org_organisation") - resource.import_xml(xmltree) - self.assertEqual(resource.error, None) + result = resource.import_xml(xmltree) + self.assertEqual(result.error, None) def testImportMerge(self): @@ -350,8 +350,8 @@ def testImportMerge(self): xmltree = etree.ElementTree(etree.fromstring(xmlstr)) resource = current.s3db.resource("org_organisation") - msg = resource.import_xml(xmltree) - self.assertEqual(resource.error, None) + result = resource.import_xml(xmltree) + self.assertEqual(result.error, None) # Check the result: new record gets imported, duplicate merged into it resource = s3db.resource("org_organisation", @@ -412,8 +412,8 @@ def testImportMerge(self): xmltree = etree.ElementTree(etree.fromstring(xmlstr)) resource = current.s3db.resource("org_organisation") - msg = resource.import_xml(xmltree) - self.assertEqual(resource.error, None) + result = resource.import_xml(xmltree) + self.assertEqual(result.error, None) # Check the result: only the final record gets imported # Note that no components of the deleted duplicate would ever diff --git a/modules/unit_tests/s3db/org.py b/modules/unit_tests/s3db/org.py index 1907a5c49a..a6f8a05582 100644 --- a/modules/unit_tests/s3db/org.py +++ b/modules/unit_tests/s3db/org.py @@ -684,10 +684,9 @@ def testMultipleNameMatchesWithParentItemAmbiguous(self): result = resource.import_xml(xmltree, ignore_errors=True) # Verify that we have an error reported for the import item - result = json.loads(result) - msg = result["message"] + msg = json.loads(result.json_message())["message"] - error_tree = resource.error_tree + error_tree = result.error_tree assertNotEqual(error_tree, None) elements = error_tree.xpath("resource[data[@field='name']/text()='DeDupBranch3']") @@ -793,10 +792,9 @@ def testMultipleNameMatchesWithoutParentItemAmbiguous(self): result = resource.import_xml(xmltree, ignore_errors=True) # Verify that we have an error reported for the import item - result = json.loads(result) - msg = result["message"] + msg = json.loads(result.json_message())["message"] - error_tree = resource.error_tree + error_tree = result.error_tree assertNotEqual(error_tree, None) elements = error_tree.xpath("resource[data[@field='name']/text()='DeDupBranch2']") diff --git a/modules/unit_tests/s3db/pr.py b/modules/unit_tests/s3db/pr.py index 8dbeecb0f6..7c9993a3f1 100644 --- a/modules/unit_tests/s3db/pr.py +++ b/modules/unit_tests/s3db/pr.py @@ -199,7 +199,7 @@ def testHook(self): def testMatchNames(self): s3db = current.s3db - from core import S3ImportItem + from core import ImportItem deduplicate = s3db.get_config("pr_person", "deduplicate") @@ -210,7 +210,7 @@ def testMatchNames(self): item = self.import_item(person) deduplicate(item) self.assertEqual(item.id, self.person1_id) - self.assertEqual(item.method, S3ImportItem.METHOD.UPDATE) + self.assertEqual(item.method, ImportItem.METHOD.UPDATE) # Test Mismatch: # Different first name, same last name @@ -225,7 +225,7 @@ def testMatchNames(self): def testMatchEmail(self): s3db = current.s3db - from core import S3ImportItem + from core import ImportItem deduplicate = s3db.get_config("pr_person", "deduplicate") @@ -239,7 +239,7 @@ def testMatchEmail(self): item = self.import_item(person, email="testuser@example.com") deduplicate(item) self.assertEqual(item.id, self.person1_id) - self.assertEqual(item.method, S3ImportItem.METHOD.UPDATE) + self.assertEqual(item.method, ImportItem.METHOD.UPDATE) # Test Mismatch # Different first name, same last name, @@ -270,7 +270,7 @@ def testMatchEmail(self): item = self.import_item(person, email="testuser@example.com") deduplicate(item) self.assertEqual(item.id, self.person1_id) - self.assertEqual(item.method, S3ImportItem.METHOD.UPDATE) + self.assertEqual(item.method, ImportItem.METHOD.UPDATE) # Test Mismatch - same names, no email in import item person = Storage(first_name = "Test", @@ -287,7 +287,7 @@ def testMatchEmail(self): item = self.import_item(person) deduplicate(item) self.assertEqual(item.id, self.person1_id) - self.assertEqual(item.method, S3ImportItem.METHOD.UPDATE) + self.assertEqual(item.method, ImportItem.METHOD.UPDATE) # Test Mismatch - same names, different email person = Storage(first_name = "Test", @@ -304,7 +304,7 @@ def testMatchEmail(self): item = self.import_item(person, email="otheremail@example.com") deduplicate(item) self.assertEqual(item.id, self.person1_id) - self.assertEqual(item.method, S3ImportItem.METHOD.UPDATE) + self.assertEqual(item.method, ImportItem.METHOD.UPDATE) # Test Match - same names, same email, but other record person = Storage(first_name = "Test", @@ -312,7 +312,7 @@ def testMatchEmail(self): item = self.import_item(person, email="otheruser@example.org") deduplicate(item) self.assertEqual(item.id, self.person2_id) - self.assertEqual(item.method, S3ImportItem.METHOD.UPDATE) + self.assertEqual(item.method, ImportItem.METHOD.UPDATE) # Test Mismatch - First names different person = Storage(first_name = "Other", @@ -326,7 +326,7 @@ def testMatchEmail(self): def testMatchInitials(self): s3db = current.s3db - from core import S3ImportItem + from core import ImportItem deduplicate = s3db.get_config("pr_person", "deduplicate") @@ -346,7 +346,7 @@ def testMatchInitials(self): item = self.import_item(person) deduplicate(item) self.assertEqual(item.id, self.person1_id) - self.assertEqual(item.method, S3ImportItem.METHOD.UPDATE) + self.assertEqual(item.method, ImportItem.METHOD.UPDATE) # Test Match - same names, different initials person = Storage(first_name="Test", @@ -355,7 +355,7 @@ def testMatchInitials(self): item = self.import_item(person) deduplicate(item) self.assertEqual(item.id, self.person2_id) - self.assertEqual(item.method, S3ImportItem.METHOD.UPDATE) + self.assertEqual(item.method, ImportItem.METHOD.UPDATE) # Test Match - same names, different initials, and email person = Storage(first_name="Test", @@ -364,21 +364,21 @@ def testMatchInitials(self): item = self.import_item(person, email="testuser@example.org") deduplicate(item) self.assertEqual(item.id, self.person2_id) - self.assertEqual(item.method, S3ImportItem.METHOD.UPDATE) + self.assertEqual(item.method, ImportItem.METHOD.UPDATE) # Test Match - same initials person = Storage(initials="OU") item = self.import_item(person) deduplicate(item) self.assertEqual(item.id, self.person2_id) - self.assertEqual(item.method, S3ImportItem.METHOD.UPDATE) + self.assertEqual(item.method, ImportItem.METHOD.UPDATE) # Test Match - same initials, same email person = Storage(initials="TU") item = self.import_item(person, email="testuser@example.com") deduplicate(item) self.assertEqual(item.id, self.person1_id) - self.assertEqual(item.method, S3ImportItem.METHOD.UPDATE) + self.assertEqual(item.method, ImportItem.METHOD.UPDATE) # ------------------------------------------------------------------------- def testMatchDOB(self): @@ -417,14 +417,14 @@ def testMatchDOB(self): def import_item(self, person, email=None, sms=None): """ Construct a fake import item """ - from core import S3ImportItem + from core import ImportItem def item(tablename, data): return Storage(id = None, method = None, tablename = tablename, data = data, components = [], - METHOD = S3ImportItem.METHOD) + METHOD = ImportItem.METHOD) import_item = item("pr_person", person) if email: import_item.components.append(item("pr_contact", @@ -551,7 +551,7 @@ def testMobilePhoneNumberImportValidationStandard(self): xmltree = etree.ElementTree(etree.fromstring(xmlstr)) resource = s3db.resource("pr_person") - result = resource.import_xml(xmltree, ignore_errors=True) + resource.import_xml(xmltree, ignore_errors=True) resource = s3db.resource("pr_contact", uid="VALIDATORTESTCONTACT1") self.assertEqual(resource.count(), 1) @@ -590,7 +590,7 @@ def testMobilePhoneNumberImportValidationInternational(self): xmltree = etree.ElementTree(etree.fromstring(xmlstr)) resource = s3db.resource("pr_person") - result = resource.import_xml(xmltree, ignore_errors=True) + resource.import_xml(xmltree, ignore_errors=True) resource = s3db.resource("pr_contact", uid="VALIDATORTESTCONTACT1") self.assertEqual(resource.count(), 0) @@ -650,7 +650,7 @@ def setUpClass(cls): xmltree = etree.ElementTree(etree.fromstring(xmlstr)) resource = s3db.resource("pr_person") - result = resource.import_xml(xmltree, ignore_errors=True) + resource.import_xml(xmltree, ignore_errors=True) @classmethod def tearDownClass(cls): diff --git a/static/scripts/tools/import_supply_item_ifrc_standard.py b/static/scripts/tools/import_supply_item_ifrc_standard.py index 5b30532109..3ace846739 100644 --- a/static/scripts/tools/import_supply_item_ifrc_standard.py +++ b/static/scripts/tools/import_supply_item_ifrc_standard.py @@ -31,7 +31,7 @@ resource = s3db.resource("supply_item_category") File = open(import_file, "r") resource.import_xml(File, - format="csv", + source_type="csv", stylesheet=stylesheet) File.close() @@ -49,7 +49,7 @@ resource = s3db.resource("supply_item") File = open(import_file, "r") resource.import_xml(File, - format="csv", + source_type="csv", stylesheet=stylesheet) File.close() diff --git a/static/scripts/tools/people.py b/static/scripts/tools/people.py index 28f432ab81..55e2d04ae3 100644 --- a/static/scripts/tools/people.py +++ b/static/scripts/tools/people.py @@ -9,7 +9,7 @@ # stylesheet = os.path.join(request.folder, "static", "formats", "s3csv", "hrm", "person.xsl") # filename = "people.csv" # File = open(filename, "r") -# resource.import_xml(File, format="csv", stylesheet=stylesheet) +# resource.import_xml(File, source_type="csv", stylesheet=stylesheet) # db.commit() # # @ToDo: Email Addresses, Job Titles diff --git a/static/scripts/tools/sync_setup.py b/static/scripts/tools/sync_setup.py index b47c54e695..dccbf35d54 100644 --- a/static/scripts/tools/sync_setup.py +++ b/static/scripts/tools/sync_setup.py @@ -105,7 +105,7 @@ # sys.stderr.write("Could not auto-register repository, please register manually\n") # Resources - sync_policies = s3base.S3ImportItem.POLICY + sync_policies = s3base.ImportItem.POLICY sync_task = db.sync_task for resource_name in resources: task = Storage(resource_name=resource_name, diff --git a/static/themes/default/eden.min.css b/static/themes/default/eden.min.css index 5c89c8bc8e..d138cb112c 100644 --- a/static/themes/default/eden.min.css +++ b/static/themes/default/eden.min.css @@ -1 +1 @@ -@charset "UTF-8";.swidth{width:640px}.colmask{position:relative;clear:both;float:left;width:100%;overflow:hidden;z-index:0;margin-top:42px}.col3left{float:left;width:33%;position:relative}.col3mid,.col3right{float:right;width:33%;position:relative}.col2left{float:left;width:49%;position:relative}.col2right{float:right;width:49%;position:relative}.col1,.col2,.col3{float:left;position:relative;padding:0 0 3px 0;overflow:hidden}.fullpage{padding-top:1px;overflow:visible}.fullpage .col1{width:99%;left:0.5%;min-width:800px}.aside{float:left;width:200px}.rightside{margin-left:200px}.ext-el-mask{background-color:#ccc}.ext-el-mask-msg{border-color:#999;background-color:#ddd;background-image:url(../../scripts/ext/resources/images/gray/panel/white-top-bottom.gif);background-position:0 -1px}.ext-el-mask-msg div{background-color:#eee;border-color:#d0d0d0;color:#222;font:normal 11px tahoma,arial,helvetica,sans-serif}.x-mask-loading div{background-color:#fbfbfb;background-image:url(../../scripts/ext/resources/images/default/grid/loading.gif)}.x-item-disabled{color:gray}.x-item-disabled *{color:gray !important}.x-splitbar-proxy{background-color:#aaa}.x-color-palette a{border-color:#fff}.x-color-palette a:hover,.x-color-palette a.x-color-palette-sel{border-color:#CFCFCF;background-color:#eaeaea}.x-color-palette em{border-color:#aca899}.x-ie-shadow{background-color:#777}.x-shadow .xsmc{background-image:url(../../scripts/ext/resources/images/default/shadow-c.png)}.x-shadow .xsml,.x-shadow .xsmr{background-image:url(../../scripts/ext/resources/images/default/shadow-lr.png)}.x-shadow .xstl,.x-shadow .xstc,.x-shadow .xstr,.x-shadow .xsbl,.x-shadow .xsbc,.x-shadow .xsbr{background-image:url(../../scripts/ext/resources/images/default/shadow.png)}.loading-indicator{font-size:11px;background-image:url(../../scripts/ext/resources/images/default/grid/loading.gif)}.x-spotlight{background-color:#ccc}.x-tab-panel-header,.x-tab-panel-footer{background-color:#eaeaea;border-color:#d0d0d0;overflow:hidden;zoom:1}.x-tab-panel-header,.x-tab-panel-footer{border-color:#d0d0d0}ul.x-tab-strip-top{background-color:#dbdbdb;background-image:url(../../scripts/ext/resources/images/gray/tabs/tab-strip-bg.gif);border-bottom-color:#d0d0d0}ul.x-tab-strip-bottom{background-color:#dbdbdb;background-image:url(../../scripts/ext/resources/images/gray/tabs/tab-strip-btm-bg.gif);border-top-color:#d0d0d0}.x-tab-panel-header-plain .x-tab-strip-spacer,.x-tab-panel-footer-plain .x-tab-strip-spacer{border-color:#d0d0d0;background-color:#eaeaea}.x-tab-strip span.x-tab-strip-text{font:normal 11px tahoma,arial,helvetica;color:#333}.x-tab-strip-over span.x-tab-strip-text{color:#111}.x-tab-strip-active span.x-tab-strip-text{color:#333;font-weight:bold}.x-tab-strip-disabled .x-tabs-text{color:#aaaaaa}.x-tab-strip-top .x-tab-right,.x-tab-strip-top .x-tab-left,.x-tab-strip-top .x-tab-strip-inner{background-image:url(../../scripts/ext/resources/images/gray/tabs/tabs-sprite.gif)}.x-tab-strip-bottom .x-tab-right{background-image:url(../../scripts/ext/resources/images/gray/tabs/tab-btm-inactive-right-bg.gif)}.x-tab-strip-bottom .x-tab-left{background-image:url(../../scripts/ext/resources/images/gray/tabs/tab-btm-inactive-left-bg.gif)}.x-tab-strip-bottom .x-tab-strip-over .x-tab-left{background-image:url(../../scripts/ext/resources/images/gray/tabs/tab-btm-over-left-bg.gif)}.x-tab-strip-bottom .x-tab-strip-over .x-tab-right{background-image:url(../../scripts/ext/resources/images/gray/tabs/tab-btm-over-right-bg.gif)}.x-tab-strip-bottom .x-tab-strip-active .x-tab-right{background-image:url(../../scripts/ext/resources/images/gray/tabs/tab-btm-right-bg.gif)}.x-tab-strip-bottom .x-tab-strip-active .x-tab-left{background-image:url(../../scripts/ext/resources/images/gray/tabs/tab-btm-left-bg.gif)}.x-tab-strip .x-tab-strip-closable a.x-tab-strip-close{background-image:url(../../scripts/ext/resources/images/gray/tabs/tab-close.gif)}.x-tab-strip .x-tab-strip-closable a.x-tab-strip-close:hover{background-image:url(../../scripts/ext/resources/images/gray/tabs/tab-close.gif)}.x-tab-panel-body{border-color:#d0d0d0;background-color:#fff}.x-tab-panel-body-top{border-top:0 none}.x-tab-panel-body-bottom{border-bottom:0 none}.x-tab-scroller-left{background-image:url(../../scripts/ext/resources/images/gray/tabs/scroll-left.gif);border-bottom-color:#d0d0d0}.x-tab-scroller-left-over{background-position:0 0}.x-tab-scroller-left-disabled{background-position:-18px 0;opacity:.5;-moz-opacity:.5;filter:alpha(opacity=50);cursor:default}.x-tab-scroller-right{background-image:url(../../scripts/ext/resources/images/gray/tabs/scroll-right.gif);border-bottom-color:#d0d0d0}.x-tab-panel-bbar .x-toolbar,.x-tab-panel-tbar .x-toolbar{border-color:#d0d0d0}.x-form-field{font:normal 12px tahoma,arial,helvetica,sans-serif}.x-form-text,textarea.x-form-field{background-color:#fff;background-image:url(../../scripts/ext/resources/images/default/form/text-bg.gif);border-color:#C1C1C1}.x-form-select-one{background-color:#fff;border-color:#C1C1C1}.x-form-check-group-label{border-bottom:1px solid #d0d0d0;color:#333}.x-editor .x-form-check-wrap{background-color:#fff}.x-form-field-wrap .x-form-trigger{background-image:url(../../scripts/ext/resources/images/gray/form/trigger.gif);border-bottom-color:#b5b8c8}.x-form-field-wrap .x-form-date-trigger{background-image:url(../../scripts/ext/resources/images/gray/form/date-trigger.gif)}.x-form-field-wrap .x-form-clear-trigger{background-image:url(../../scripts/ext/resources/images/gray/form/clear-trigger.gif)}.x-form-field-wrap .x-form-search-trigger{background-image:url(../../scripts/ext/resources/images/gray/form/search-trigger.gif)}.x-trigger-wrap-focus .x-form-trigger{border-bottom-color:#777777}.x-item-disabled .x-form-trigger-over{border-bottom-color:#b5b8c8}.x-item-disabled .x-form-trigger-click{border-bottom-color:#b5b8c8}.x-form-focus,textarea.x-form-focus{border-color:#777777}.x-form-invalid,textarea.x-form-invalid{background-color:#fff;background-image:url(../../scripts/ext/resources/images/default/grid/invalid_line.gif);border-color:#c30}.ext-webkit .x-form-invalid{background-color:#fee;border-color:#ff7870}.x-form-inner-invalid,textarea.x-form-inner-invalid{background-color:#fff;background-image:url(../../scripts/ext/resources/images/default/grid/invalid_line.gif)}.x-form-grow-sizer{font:normal 12px tahoma,arial,helvetica,sans-serif}.x-form-item{font:normal 12px tahoma,arial,helvetica,sans-serif}.x-form-invalid-msg{color:#c0272b;font:normal 11px tahoma,arial,helvetica,sans-serif;background-image:url(../../scripts/ext/resources/images/default/shared/warning.gif)}.x-form-empty-field{color:gray}.x-small-editor .x-form-field{font:normal 11px arial,tahoma,helvetica,sans-serif}.ext-webkit .x-small-editor .x-form-field{font:normal 12px arial,tahoma,helvetica,sans-serif}.x-form-invalid-icon{background-image:url(../../scripts/ext/resources/images/default/form/exclamation.gif)}.x-fieldset{border-color:#CCCCCC}.x-fieldset legend{font:bold 11px tahoma,arial,helvetica,sans-serif;color:#777777}.x-btn{font:normal 11px tahoma,verdana,helvetica}.x-btn button{font:normal 11px arial,tahoma,verdana,helvetica;color:#333}.x-btn em{font-style:normal;font-weight:normal}.x-btn-tl,.x-btn-tr,.x-btn-tc,.x-btn-ml,.x-btn-mr,.x-btn-mc,.x-btn-bl,.x-btn-br,.x-btn-bc{background-image:url(../../scripts/ext/resources/images/gray/button/btn.gif)}.x-btn-click .x-btn-text,.x-btn-menu-active .x-btn-text,.x-btn-pressed .x-btn-text{color:#000}.x-btn-disabled *{color:gray !important}.x-btn-mc em.x-btn-arrow{background-image:url(../../scripts/ext/resources/images/default/button/arrow.gif)}.x-btn-mc em.x-btn-split{background-image:url(../../scripts/ext/resources/images/default/button/s-arrow.gif)}.x-btn-over .x-btn-mc em.x-btn-split,.x-btn-click .x-btn-mc em.x-btn-split,.x-btn-menu-active .x-btn-mc em.x-btn-split,.x-btn-pressed .x-btn-mc em.x-btn-split{background-image:url(../../scripts/ext/resources/images/gray/button/s-arrow-o.gif)}.x-btn-mc em.x-btn-arrow-bottom{background-image:url(../../scripts/ext/resources/images/default/button/s-arrow-b-noline.gif)}.x-btn-mc em.x-btn-split-bottom{background-image:url(../../scripts/ext/resources/images/default/button/s-arrow-b.gif)}.x-btn-over .x-btn-mc em.x-btn-split-bottom,.x-btn-click .x-btn-mc em.x-btn-split-bottom,.x-btn-menu-active .x-btn-mc em.x-btn-split-bottom,.x-btn-pressed .x-btn-mc em.x-btn-split-bottom{background-image:url(../../scripts/ext/resources/images/gray/button/s-arrow-bo.gif)}.x-btn-group-header{color:#666}.x-btn-group-tc{background-image:url(../../scripts/ext/resources/images/gray/button/group-tb.gif)}.x-btn-group-tl{background-image:url(../../scripts/ext/resources/images/gray/button/group-cs.gif)}.x-btn-group-tr{background-image:url(../../scripts/ext/resources/images/gray/button/group-cs.gif)}.x-btn-group-bc{background-image:url(../../scripts/ext/resources/images/gray/button/group-tb.gif)}.x-btn-group-bl{background-image:url(../../scripts/ext/resources/images/gray/button/group-cs.gif)}.x-btn-group-br{background-image:url(../../scripts/ext/resources/images/gray/button/group-cs.gif)}.x-btn-group-ml{background-image:url(../../scripts/ext/resources/images/gray/button/group-lr.gif)}.x-btn-group-mr{background-image:url(../../scripts/ext/resources/images/gray/button/group-lr.gif)}.x-btn-group-notitle .x-btn-group-tc{background-image:url(../../scripts/ext/resources/images/gray/button/group-tb.gif)}.x-toolbar{border-color:#d0d0d0;background-color:#f0f0f0;background-image:url(../../scripts/ext/resources/images/gray/toolbar/bg.gif)}.x-toolbar td,.x-toolbar span,.x-toolbar input,.x-toolbar div,.x-toolbar select,.x-toolbar label{font:normal 11px arial,tahoma,helvetica,sans-serif}.x-toolbar .x-item-disabled{color:gray}.x-toolbar .x-item-disabled *{color:gray}.x-toolbar .x-btn-mc em.x-btn-split{background-image:url(../../scripts/ext/resources/images/default/button/s-arrow-noline.gif)}.x-toolbar .x-btn-over .x-btn-mc em.x-btn-split,.x-toolbar .x-btn-click .x-btn-mc em.x-btn-split,.x-toolbar .x-btn-menu-active .x-btn-mc em.x-btn-split,.x-toolbar .x-btn-pressed .x-btn-mc em.x-btn-split{background-image:url(../../scripts/ext/resources/images/gray/button/s-arrow-o.gif)}.x-toolbar .x-btn-mc em.x-btn-split-bottom{background-image:url(../../scripts/ext/resources/images/default/button/s-arrow-b-noline.gif)}.x-toolbar .x-btn-over .x-btn-mc em.x-btn-split-bottom,.x-toolbar .x-btn-click .x-btn-mc em.x-btn-split-bottom,.x-toolbar .x-btn-menu-active .x-btn-mc em.x-btn-split-bottom,.x-toolbar .x-btn-pressed .x-btn-mc em.x-btn-split-bottom{background-image:url(../../scripts/ext/resources/images/gray/button/s-arrow-bo.gif)}.x-toolbar .xtb-sep{background-image:url(../../scripts/ext/resources/images/default/grid/grid-split.gif)}.x-tbar-page-first{background-image:url(../../scripts/ext/resources/images/gray/grid/page-first.gif) !important}.x-tbar-loading{background-image:url(../../scripts/ext/resources/images/gray/grid/refresh.gif) !important}.x-tbar-page-last{background-image:url(../../scripts/ext/resources/images/gray/grid/page-last.gif) !important}.x-tbar-page-next{background-image:url(../../scripts/ext/resources/images/gray/grid/page-next.gif) !important}.x-tbar-page-prev{background-image:url(../../scripts/ext/resources/images/gray/grid/page-prev.gif) !important}.x-item-disabled .x-tbar-loading{background-image:url(../../scripts/ext/resources/images/default/grid/loading.gif) !important}.x-item-disabled .x-tbar-page-first{background-image:url(../../scripts/ext/resources/images/default/grid/page-first-disabled.gif) !important}.x-item-disabled .x-tbar-page-last{background-image:url(../../scripts/ext/resources/images/default/grid/page-last-disabled.gif) !important}.x-item-disabled .x-tbar-page-next{background-image:url(../../scripts/ext/resources/images/default/grid/page-next-disabled.gif) !important}.x-item-disabled .x-tbar-page-prev{background-image:url(../../scripts/ext/resources/images/default/grid/page-prev-disabled.gif) !important}.x-paging-info{color:#444}.x-toolbar-more-icon{background-image:url(../../scripts/ext/resources/images/gray/toolbar/more.gif) !important}.x-resizable-handle{background-color:#fff}.x-resizable-over .x-resizable-handle-east,.x-resizable-pinned .x-resizable-handle-east,.x-resizable-over .x-resizable-handle-west,.x-resizable-pinned .x-resizable-handle-west{background-image:url(../../scripts/ext/resources/images/gray/sizer/e-handle.gif)}.x-resizable-over .x-resizable-handle-south,.x-resizable-pinned .x-resizable-handle-south,.x-resizable-over .x-resizable-handle-north,.x-resizable-pinned .x-resizable-handle-north{background-image:url(../../scripts/ext/resources/images/gray/sizer/s-handle.gif)}.x-resizable-over .x-resizable-handle-north,.x-resizable-pinned .x-resizable-handle-north{background-image:url(../../scripts/ext/resources/images/gray/sizer/s-handle.gif)}.x-resizable-over .x-resizable-handle-southeast,.x-resizable-pinned .x-resizable-handle-southeast{background-image:url(../../scripts/ext/resources/images/gray/sizer/se-handle.gif)}.x-resizable-over .x-resizable-handle-northwest,.x-resizable-pinned .x-resizable-handle-northwest{background-image:url(../../scripts/ext/resources/images/gray/sizer/nw-handle.gif)}.x-resizable-over .x-resizable-handle-northeast,.x-resizable-pinned .x-resizable-handle-northeast{background-image:url(../../scripts/ext/resources/images/gray/sizer/ne-handle.gif)}.x-resizable-over .x-resizable-handle-southwest,.x-resizable-pinned .x-resizable-handle-southwest{background-image:url(../../scripts/ext/resources/images/gray/sizer/sw-handle.gif)}.x-resizable-proxy{border-color:#565656}.x-resizable-overlay{background-color:#fff}.x-grid3{background-color:#fff}.x-grid-panel .x-panel-mc .x-panel-body{border-color:#d0d0d0}.x-grid3-row td,.x-grid3-summary-row td{font:normal 11px/13px arial,tahoma,helvetica,sans-serif}.x-grid3-hd-row td{font:normal 11px/15px arial,tahoma,helvetica,sans-serif}.x-grid3-hd-row td{border-left-color:#eee;border-right-color:#d0d0d0}.x-grid-row-loading{background-color:#fff;background-image:url(../../scripts/ext/resources/images/default/shared/loading-balls.gif)}.x-grid3-row{border-color:#ededed;border-top-color:#fff}.x-grid3-row-alt{background-color:#fafafa}.x-grid3-row-over{border-color:#ddd;background-color:#efefef;background-image:url(../../scripts/ext/resources/images/default/grid/row-over.gif)}.x-grid3-resize-proxy{background-color:#777}.x-grid3-resize-marker{background-color:#777}.x-grid3-header{background-color:#f9f9f9;background-image:url(../../scripts/ext/resources/images/gray/grid/grid3-hrow2.gif)}.x-grid3-header-pop{border-left-color:#d0d0d0}.x-grid3-header-pop-inner{border-left-color:#eee;background-image:url(../../scripts/ext/resources/images/default/grid/hd-pop.gif)}td.x-grid3-hd-over,td.sort-desc,td.sort-asc,td.x-grid3-hd-menu-open{border-left-color:#ACACAC;border-right-color:#ACACAC}td.x-grid3-hd-over .x-grid3-hd-inner,td.sort-desc .x-grid3-hd-inner,td.sort-asc .x-grid3-hd-inner,td.x-grid3-hd-menu-open .x-grid3-hd-inner{background-color:#f9f9f9;background-image:url(../../scripts/ext/resources/images/gray/grid/grid3-hrow-over2.gif)}.sort-asc .x-grid3-sort-icon{background-image:url(../../scripts/ext/resources/images/gray/grid/sort_asc.gif)}.sort-desc .x-grid3-sort-icon{background-image:url(../../scripts/ext/resources/images/gray/grid/sort_desc.gif)}.x-grid3-cell-text,.x-grid3-hd-text{color:#000}.x-grid3-split{background-image:url(../../scripts/ext/resources/images/default/grid/grid-split.gif)}.x-grid3-hd-text{color:#333}.x-dd-drag-proxy .x-grid3-hd-inner{background-color:#f9f9f9;background-image:url(../../scripts/ext/resources/images/gray/grid/grid3-hrow-over2.gif);border-color:#ACACAC}.col-move-top{background-image:url(../../scripts/ext/resources/images/gray/grid/col-move-top.gif)}.col-move-bottom{background-image:url(../../scripts/ext/resources/images/gray/grid/col-move-bottom.gif)}.x-grid3-row-selected{background-color:#CCCCCC !important;background-image:none;border-color:#ACACAC}.x-grid3-cell-selected{background-color:#CBCBCB !important;color:#000}.x-grid3-cell-selected span{color:#000 !important}.x-grid3-cell-selected .x-grid3-cell-text{color:#000}.x-grid3-locked td.x-grid3-row-marker,.x-grid3-locked .x-grid3-row-selected td.x-grid3-row-marker{background-color:#ebeadb !important;background-image:url(../../scripts/ext/resources/images/default/grid/grid-hrow.gif) !important;color:#000;border-top-color:#fff;border-right-color:#6fa0df !important}.x-grid3-locked td.x-grid3-row-marker div,.x-grid3-locked .x-grid3-row-selected td.x-grid3-row-marker div{color:#333 !important}.x-grid3-dirty-cell{background-image:url(../../scripts/ext/resources/images/default/grid/dirty.gif)}.x-grid3-topbar,.x-grid3-bottombar{font:normal 11px arial,tahoma,helvetica,sans-serif}.x-grid3-bottombar .x-toolbar{border-top-color:#a9bfd3}.x-props-grid .x-grid3-td-name .x-grid3-cell-inner{background-image:url(../../scripts/ext/resources/images/default/grid/grid3-special-col-bg.gif) !important;color:#000 !important}.x-props-grid .x-grid3-body .x-grid3-td-name{background-color:#fff !important;border-right-color:#eee}.xg-hmenu-sort-asc .x-menu-item-icon{background-image:url(../../scripts/ext/resources/images/default/grid/hmenu-asc.gif)}.xg-hmenu-sort-desc .x-menu-item-icon{background-image:url(../../scripts/ext/resources/images/default/grid/hmenu-desc.gif)}.xg-hmenu-lock .x-menu-item-icon{background-image:url(../../scripts/ext/resources/images/default/grid/hmenu-lock.gif)}.xg-hmenu-unlock .x-menu-item-icon{background-image:url(../../scripts/ext/resources/images/default/grid/hmenu-unlock.gif)}.x-grid3-hd-btn{background-color:#f9f9f9;background-image:url(../../scripts/ext/resources/images/gray/grid/grid3-hd-btn.gif)}.x-grid3-body .x-grid3-td-expander{background-image:url(../../scripts/ext/resources/images/default/grid/grid3-special-col-bg.gif)}.x-grid3-row-expander{background-image:url(../../scripts/ext/resources/images/gray/grid/row-expand-sprite.gif)}.x-grid3-body .x-grid3-td-checker{background-image:url(../../scripts/ext/resources/images/default/grid/grid3-special-col-bg.gif)}.x-grid3-row-checker,.x-grid3-hd-checker{background-image:url(../../scripts/ext/resources/images/default/grid/row-check-sprite.gif)}.x-grid3-body .x-grid3-td-numberer{background-image:url(../../scripts/ext/resources/images/default/grid/grid3-special-col-bg.gif)}.x-grid3-body .x-grid3-td-numberer .x-grid3-cell-inner{color:#444}.x-grid3-body .x-grid3-td-row-icon{background-image:url(../../scripts/ext/resources/images/default/grid/grid3-special-col-bg.gif)}.x-grid3-body .x-grid3-row-selected .x-grid3-td-numberer,.x-grid3-body .x-grid3-row-selected .x-grid3-td-checker,.x-grid3-body .x-grid3-row-selected .x-grid3-td-expander{background-image:url(../../scripts/ext/resources/images/gray/grid/grid3-special-col-sel-bg.gif)}.x-grid3-check-col{background-image:url(../../scripts/ext/resources/images/default/menu/unchecked.gif)}.x-grid3-check-col-on{background-image:url(../../scripts/ext/resources/images/default/menu/checked.gif)}.x-grid-group,.x-grid-group-body,.x-grid-group-hd{zoom:1}.x-grid-group-hd{border-bottom-color:#d0d0d0}.x-grid-group-hd div.x-grid-group-title{background-image:url(../../scripts/ext/resources/images/gray/grid/group-collapse.gif);color:#5F5F5F;font:bold 11px tahoma,arial,helvetica,sans-serif}.x-grid-group-collapsed .x-grid-group-hd div.x-grid-group-title{background-image:url(../../scripts/ext/resources/images/gray/grid/group-expand.gif)}.x-group-by-icon{background-image:url(../../scripts/ext/resources/images/default/grid/group-by.gif)}.x-cols-icon{background-image:url(../../scripts/ext/resources/images/default/grid/columns.gif)}.x-show-groups-icon{background-image:url(../../scripts/ext/resources/images/default/grid/group-by.gif)}.x-grid-empty{color:gray;font:normal 11px tahoma,arial,helvetica,sans-serif}.x-grid-with-col-lines .x-grid3-row td.x-grid3-cell{border-right-color:#ededed}.x-grid-with-col-lines .x-grid3-row{border-top-color:#ededed}.x-grid-with-col-lines .x-grid3-row-selected{border-top-color:#B9B9B9}.x-pivotgrid .x-grid3-header-offset table td{background:url(../../scripts/ext/resources/images/gray/grid/grid3-hrow2.gif) repeat-x 50% 100%;border-left:1px solid;border-right:1px solid;border-left-color:#D0D0D0;border-right-color:#D0D0D0}.x-pivotgrid .x-grid3-row-headers{background-color:#f9f9f9}.x-pivotgrid .x-grid3-row-headers table td{background:#EEE url(../../scripts/ext/resources/images/default/grid/grid3-rowheader.gif) repeat-x left top;border-left:1px solid;border-right:1px solid;border-left-color:#EEE;border-right-color:#D0D0D0;border-bottom:1px solid;border-bottom-color:#D0D0D0;height:18px}.x-dd-drag-ghost{color:#000;font:normal 11px arial,helvetica,sans-serif;border-color:#ddd #bbb #bbb #ddd;background-color:#fff}.x-dd-drop-nodrop .x-dd-drop-icon{background-image:url(../../scripts/ext/resources/images/default/dd/drop-no.gif)}.x-dd-drop-ok .x-dd-drop-icon{background-image:url(../../scripts/ext/resources/images/default/dd/drop-yes.gif)}.x-dd-drop-ok-add .x-dd-drop-icon{background-image:url(../../scripts/ext/resources/images/default/dd/drop-add.gif)}.x-view-selector{background-color:#D6D6D6;border-color:#888888}.x-tree-node-expanded .x-tree-node-icon{background-image:url(../../scripts/ext/resources/images/default/tree/folder-open.gif)}.x-tree-node-leaf .x-tree-node-icon{background-image:url(../../scripts/ext/resources/images/default/tree/leaf.gif)}.x-tree-node-collapsed .x-tree-node-icon{background-image:url(../../scripts/ext/resources/images/default/tree/folder.gif)}.x-tree-node-loading .x-tree-node-icon{background-image:url(../../scripts/ext/resources/images/default/tree/loading.gif) !important}.x-tree-node .x-tree-node-inline-icon{background-image:none}.x-tree-node-loading a span{font-style:italic;color:#444444}.ext-ie .x-tree-node-el input{width:15px;height:15px}.x-tree-lines .x-tree-elbow{background-image:url(../../scripts/ext/resources/images/default/tree/elbow.gif)}.x-tree-lines .x-tree-elbow-plus{background-image:url(../../scripts/ext/resources/images/default/tree/elbow-plus.gif)}.x-tree-lines .x-tree-elbow-minus{background-image:url(../../scripts/ext/resources/images/default/tree/elbow-minus.gif)}.x-tree-lines .x-tree-elbow-end{background-image:url(../../scripts/ext/resources/images/default/tree/elbow-end.gif)}.x-tree-lines .x-tree-elbow-end-plus{background-image:url(../../scripts/ext/resources/images/gray/tree/elbow-end-plus.gif)}.x-tree-lines .x-tree-elbow-end-minus{background-image:url(../../scripts/ext/resources/images/gray/tree/elbow-end-minus.gif)}.x-tree-lines .x-tree-elbow-line{background-image:url(../../scripts/ext/resources/images/default/tree/elbow-line.gif)}.x-tree-no-lines .x-tree-elbow-plus{background-image:url(../../scripts/ext/resources/images/default/tree/elbow-plus-nl.gif)}.x-tree-no-lines .x-tree-elbow-minus{background-image:url(../../scripts/ext/resources/images/default/tree/elbow-minus-nl.gif)}.x-tree-no-lines .x-tree-elbow-end-plus{background-image:url(../../scripts/ext/resources/images/gray/tree/elbow-end-plus-nl.gif)}.x-tree-no-lines .x-tree-elbow-end-minus{background-image:url(../../scripts/ext/resources/images/gray/tree/elbow-end-minus-nl.gif)}.x-tree-arrows .x-tree-elbow-plus{background-image:url(../../scripts/ext/resources/images/gray/tree/arrows.gif)}.x-tree-arrows .x-tree-elbow-minus{background-image:url(../../scripts/ext/resources/images/gray/tree/arrows.gif)}.x-tree-arrows .x-tree-elbow-end-plus{background-image:url(../../scripts/ext/resources/images/gray/tree/arrows.gif)}.x-tree-arrows .x-tree-elbow-end-minus{background-image:url(../../scripts/ext/resources/images/gray/tree/arrows.gif)}.x-tree-node{color:#000;font:normal 11px arial,tahoma,helvetica,sans-serif}.x-tree-node a,.x-dd-drag-ghost a{color:#000}.x-tree-node a span,.x-dd-drag-ghost a span{color:#000}.x-tree-node .x-tree-node-disabled a span{color:gray !important}.x-tree-node div.x-tree-drag-insert-below{border-bottom-color:#36c}.x-tree-node div.x-tree-drag-insert-above{border-top-color:#36c}.x-tree-dd-underline .x-tree-node div.x-tree-drag-insert-below a{border-bottom-color:#36c}.x-tree-dd-underline .x-tree-node div.x-tree-drag-insert-above a{border-top-color:#36c}.x-tree-node .x-tree-drag-append a span{background-color:#ddd;border-color:gray}.x-tree-node .x-tree-node-over{background-color:#eee}.x-tree-node .x-tree-selected{background-color:#ddd}.x-tree-drop-ok-append .x-dd-drop-icon{background-image:url(../../scripts/ext/resources/images/default/tree/drop-add.gif)}.x-tree-drop-ok-above .x-dd-drop-icon{background-image:url(../../scripts/ext/resources/images/default/tree/drop-over.gif)}.x-tree-drop-ok-below .x-dd-drop-icon{background-image:url(../../scripts/ext/resources/images/default/tree/drop-under.gif)}.x-tree-drop-ok-between .x-dd-drop-icon{background-image:url(../../scripts/ext/resources/images/default/tree/drop-between.gif)}.x-date-picker{border-color:#585858;background-color:#fff}.x-date-middle,.x-date-left,.x-date-right{background-image:url(../../scripts/ext/resources/images/gray/shared/hd-sprite.gif);color:#fff;font:bold 11px "sans serif",tahoma,verdana,helvetica}.x-date-middle .x-btn .x-btn-text{color:#fff}.x-date-middle .x-btn-mc em.x-btn-arrow{background-image:url(../../scripts/ext/resources/images/gray/toolbar/btn-arrow-light.gif)}.x-date-right a{background-image:url(../../scripts/ext/resources/images/gray/shared/right-btn.gif)}.x-date-left a{background-image:url(../../scripts/ext/resources/images/gray/shared/left-btn.gif)}.x-date-inner th{background-color:#D8D8D8;background-image:url(../../scripts/ext/resources/images/gray/panel/white-top-bottom.gif);border-bottom-color:#AFAFAF;font:normal 10px arial,helvetica,tahoma,sans-serif;color:#595959}.x-date-inner td{border-color:#fff}.x-date-inner a{font:normal 11px arial,helvetica,tahoma,sans-serif;color:#000}.x-date-inner .x-date-active{color:#000}.x-date-inner .x-date-selected a{background-image:none;background-color:#D8D8D8;border-color:#DCDCDC}.x-date-inner .x-date-today a{border-color:darkred}.x-date-inner .x-date-selected span{font-weight:bold}.x-date-inner .x-date-prevday a,.x-date-inner .x-date-nextday a{color:#aaa}.x-date-bottom{border-top-color:#AFAFAF;background-color:#D8D8D8;background:#D8D8D8 url(../../scripts/ext/resources/images/gray/panel/white-top-bottom.gif) 0 -2px}.x-date-inner a:hover,.x-date-inner .x-date-disabled a:hover{color:#000;background-color:#D8D8D8}.x-date-inner .x-date-disabled a{background-color:#eee;color:#bbb}.x-date-mmenu{background-color:#eee !important}.x-date-mmenu .x-menu-item{font-size:10px;color:#000}.x-date-mp{background-color:#fff}.x-date-mp td{font:normal 11px arial,helvetica,tahoma,sans-serif}.x-date-mp-btns button{background-color:#4E565F;color:#fff;border-color:#C0C0C0 #434343 #434343 #C0C0C0;font:normal 11px arial,helvetica,tahoma,sans-serif}.x-date-mp-btns{background-color:#D8D8D8;background:#D8D8D8 url(../../scripts/ext/resources/images/gray/panel/white-top-bottom.gif) 0 -2px}.x-date-mp-btns td{border-top-color:#AFAFAF}td.x-date-mp-month a,td.x-date-mp-year a{color:#333}td.x-date-mp-month a:hover,td.x-date-mp-year a:hover{color:#333;background-color:#FDFDFD}td.x-date-mp-sel a{background-color:#D8D8D8;background:#D8D8D8 url(../../scripts/ext/resources/images/gray/panel/white-top-bottom.gif) 0 -2px;border-color:#DCDCDC}.x-date-mp-ybtn a{background-image:url(../../scripts/ext/resources/images/gray/panel/tool-sprites.gif)}td.x-date-mp-sep{border-right-color:#D7D7D7}.x-tip .x-tip-close{background-image:url(../../scripts/ext/resources/images/gray/qtip/close.gif)}.x-tip .x-tip-tc,.x-tip .x-tip-tl,.x-tip .x-tip-tr,.x-tip .x-tip-bc,.x-tip .x-tip-bl,.x-tip .x-tip-br,.x-tip .x-tip-ml,.x-tip .x-tip-mr{background-image:url(../../scripts/ext/resources/images/gray/qtip/tip-sprite.gif)}.x-tip .x-tip-mc{font:normal 11px tahoma,arial,helvetica,sans-serif}.x-tip .x-tip-ml{background-color:#fff}.x-tip .x-tip-header-text{font:bold 11px tahoma,arial,helvetica,sans-serif;color:#444}.x-tip .x-tip-body{font:normal 11px tahoma,arial,helvetica,sans-serif;color:#444}.x-form-invalid-tip .x-tip-tc,.x-form-invalid-tip .x-tip-tl,.x-form-invalid-tip .x-tip-tr,.x-form-invalid-tip .x-tip-bc,.x-form-invalid-tip .x-tip-bl,.x-form-invalid-tip .x-tip-br,.x-form-invalid-tip .x-tip-ml,.x-form-invalid-tip .x-tip-mr{background-image:url(../../scripts/ext/resources/images/default/form/error-tip-corners.gif)}.x-form-invalid-tip .x-tip-body{background-image:url(../../scripts/ext/resources/images/default/form/exclamation.gif)}.x-tip-anchor{background-image:url(../../scripts/ext/resources/images/gray/qtip/tip-anchor-sprite.gif)}.x-menu{background-color:#f0f0f0;background-image:url(../../scripts/ext/resources/images/default/menu/menu.gif)}.x-menu-floating{border-color:#7D7D7D}.x-menu-nosep{background-image:none}.x-menu-list-item{font:normal 11px arial,tahoma,sans-serif}.x-menu-item-arrow{background-image:url(../../scripts/ext/resources/images/gray/menu/menu-parent.gif)}.x-menu-sep{background-color:#e0e0e0;border-bottom-color:#fff}a.x-menu-item{color:#222}.x-menu-item-active{background-image:url(../../scripts/ext/resources/images/gray/menu/item-over.gif);background-color:#f1f1f1;border-color:#ACACAC}.x-menu-item-active a.x-menu-item{border-color:#ACACAC}.x-menu-check-item .x-menu-item-icon{background-image:url(../../scripts/ext/resources/images/default/menu/unchecked.gif)}.x-menu-item-checked .x-menu-item-icon{background-image:url(../../scripts/ext/resources/images/default/menu/checked.gif)}.x-menu-item-checked .x-menu-group-item .x-menu-item-icon{background-image:url(../../scripts/ext/resources/images/gray/menu/group-checked.gif)}.x-menu-group-item .x-menu-item-icon{background-image:none}.x-menu-plain{background-color:#fff !important}.x-menu .x-date-picker{border-color:#AFAFAF}.x-cycle-menu .x-menu-item-checked{border-color:#B9B9B9 !important;background-color:#F1F1F1}.x-menu-scroller-top{background-image:url(../../scripts/ext/resources/images/default/layout/mini-top.gif)}.x-menu-scroller-bottom{background-image:url(../../scripts/ext/resources/images/default/layout/mini-bottom.gif)}.x-box-tl{background-image:url(../../scripts/ext/resources/images/default/box/corners.gif)}.x-box-tc{background-image:url(../../scripts/ext/resources/images/default/box/tb.gif)}.x-box-tr{background-image:url(../../scripts/ext/resources/images/default/box/corners.gif)}.x-box-ml{background-image:url(../../scripts/ext/resources/images/default/box/l.gif)}.x-box-mc{background-color:#eee;background-image:url(../../scripts/ext/resources/images/default/box/tb.gif);font-family:"Myriad Pro","Myriad Web","Tahoma","Helvetica","Arial",sans-serif;color:#393939;font-size:12px}.x-box-mc h3{font-size:14px;font-weight:bold}.x-box-mr{background-image:url(../../scripts/ext/resources/images/default/box/r.gif)}.x-box-bl{background-image:url(../../scripts/ext/resources/images/default/box/corners.gif)}.x-box-bc{background-image:url(../../scripts/ext/resources/images/default/box/tb.gif)}.x-box-br{background-image:url(../../scripts/ext/resources/images/default/box/corners.gif)}.x-box-blue .x-box-bl,.x-box-blue .x-box-br,.x-box-blue .x-box-tl,.x-box-blue .x-box-tr{background-image:url(../../scripts/ext/resources/images/default/box/corners-blue.gif)}.x-box-blue .x-box-bc,.x-box-blue .x-box-mc,.x-box-blue .x-box-tc{background-image:url(../../scripts/ext/resources/images/default/box/tb-blue.gif)}.x-box-blue .x-box-mc{background-color:#c3daf9}.x-box-blue .x-box-mc h3{color:#17385b}.x-box-blue .x-box-ml{background-image:url(../../scripts/ext/resources/images/default/box/l-blue.gif)}.x-box-blue .x-box-mr{background-image:url(../../scripts/ext/resources/images/default/box/r-blue.gif)}.x-combo-list{border-color:#ccc;background-color:#ddd;font:normal 12px tahoma,arial,helvetica,sans-serif}.x-combo-list-inner{background-color:#fff}.x-combo-list-hd{font:bold 11px tahoma,arial,helvetica,sans-serif;color:#333;background-image:url(../../scripts/ext/resources/images/default/layout/panel-title-light-bg.gif);border-bottom-color:#BCBCBC}.x-resizable-pinned .x-combo-list-inner{border-bottom-color:#BEBEBE}.x-combo-list-item{border-color:#fff}.x-combo-list .x-combo-selected{border-color:#777 !important;background-color:#f0f0f0}.x-combo-list .x-toolbar{border-top-color:#BCBCBC}.x-combo-list-small{font:normal 11px tahoma,arial,helvetica,sans-serif}.x-panel{border-color:#d0d0d0}.x-panel-header{color:#333;font-weight:bold;font-size:11px;font-family:tahoma,arial,verdana,sans-serif;border-color:#d0d0d0;background-image:url(../../scripts/ext/resources/images/gray/panel/white-top-bottom.gif)}.x-panel-body{border-color:#d0d0d0;background-color:#fff}.x-panel-bbar .x-toolbar,.x-panel-tbar .x-toolbar{border-color:#d0d0d0}.x-panel-tbar-noheader .x-toolbar,.x-panel-mc .x-panel-tbar .x-toolbar{border-top-color:#d0d0d0}.x-panel-body-noheader,.x-panel-mc .x-panel-body{border-top-color:#d0d0d0}.x-panel-tl .x-panel-header{color:#333;font:bold 11px tahoma,arial,verdana,sans-serif}.x-panel-tc{background-image:url(../../scripts/ext/resources/images/gray/panel/top-bottom.gif)}.x-panel-tl,.x-panel-tr,.x-panel-bl,.x-panel-br{background-image:url(../../scripts/ext/resources/images/gray/panel/corners-sprite.gif);border-bottom-color:#d0d0d0}.x-panel-bc{background-image:url(../../scripts/ext/resources/images/gray/panel/top-bottom.gif)}.x-panel-mc{font:normal 11px tahoma,arial,helvetica,sans-serif;background-color:#f1f1f1}.x-panel-ml{background-color:#fff;background-image:url(../../scripts/ext/resources/images/gray/panel/left-right.gif)}.x-panel-mr{background-image:url(../../scripts/ext/resources/images/gray/panel/left-right.gif)}.x-tool{background-image:url(../../scripts/ext/resources/images/gray/panel/tool-sprites.gif)}.x-panel-ghost{background-color:#f2f2f2}.x-panel-ghost ul{border-color:#d0d0d0}.x-panel-dd-spacer{border-color:#d0d0d0}.x-panel-fbar td,.x-panel-fbar span,.x-panel-fbar input,.x-panel-fbar div,.x-panel-fbar select,.x-panel-fbar label{font:normal 11px arial,tahoma,helvetica,sans-serif}.x-window-proxy{background-color:#fcfcfc;border-color:#d0d0d0}.x-window-tl .x-window-header{color:#555;font:bold 11px tahoma,arial,verdana,sans-serif}.x-window-tc{background-image:url(../../scripts/ext/resources/images/gray/window/top-bottom.png)}.x-window-tl{background-image:url(../../scripts/ext/resources/images/gray/window/left-corners.png)}.x-window-tr{background-image:url(../../scripts/ext/resources/images/gray/window/right-corners.png)}.x-window-bc{background-image:url(../../scripts/ext/resources/images/gray/window/top-bottom.png)}.x-window-bl{background-image:url(../../scripts/ext/resources/images/gray/window/left-corners.png)}.x-window-br{background-image:url(../../scripts/ext/resources/images/gray/window/right-corners.png)}.x-window-mc{border-color:#d0d0d0;font:normal 11px tahoma,arial,helvetica,sans-serif;background-color:#e8e8e8}.x-window-ml{background-image:url(../../scripts/ext/resources/images/gray/window/left-right.png)}.x-window-mr{background-image:url(../../scripts/ext/resources/images/gray/window/left-right.png)}.x-window-maximized .x-window-tc{background-color:#fff}.x-window-bbar .x-toolbar{border-top-color:#d0d0d0}.x-panel-ghost .x-window-tl{border-bottom-color:#d0d0d0}.x-panel-collapsed .x-window-tl{border-bottom-color:#d0d0d0}.x-dlg-mask{background-color:#ccc}.x-window-plain .x-window-mc{background-color:#E8E8E8;border-color:#D0D0D0 #EEEEEE #EEEEEE #D0D0D0}.x-window-plain .x-window-body{border-color:#EEEEEE #D0D0D0 #D0D0D0 #EEEEEE}body.x-body-masked .x-window-plain .x-window-mc{background-color:#E4E4E4}.x-html-editor-wrap{border-color:#BCBCBC;background-color:#fff}.x-html-editor-tb .x-btn-text{background-image:url(../../scripts/ext/resources/images/default/editor/tb-sprite.gif)}.x-panel-noborder .x-panel-header-noborder{border-bottom-color:#d0d0d0}.x-panel-noborder .x-panel-tbar-noborder .x-toolbar{border-bottom-color:#d0d0d0}.x-panel-noborder .x-panel-bbar-noborder .x-toolbar{border-top-color:#d0d0d0}.x-tab-panel-bbar-noborder .x-toolbar{border-top-color:#d0d0d0}.x-tab-panel-tbar-noborder .x-toolbar{border-bottom-color:#d0d0d0}.x-border-layout-ct{background-color:#f0f0f0}.x-border-layout-ct{background-color:#f0f0f0}.x-accordion-hd{color:#222;font-weight:normal;background-image:url(../../scripts/ext/resources/images/gray/panel/light-hd.gif)}.x-layout-collapsed{background-color:#dfdfdf;border-color:#d0d0d0}.x-layout-collapsed-over{background-color:#e7e7e7}.x-layout-split-west .x-layout-mini{background-image:url(../../scripts/ext/resources/images/default/layout/mini-left.gif)}.x-layout-split-east .x-layout-mini{background-image:url(../../scripts/ext/resources/images/default/layout/mini-right.gif)}.x-layout-split-north .x-layout-mini{background-image:url(../../scripts/ext/resources/images/default/layout/mini-top.gif)}.x-layout-split-south .x-layout-mini{background-image:url(../../scripts/ext/resources/images/default/layout/mini-bottom.gif)}.x-layout-cmini-west .x-layout-mini{background-image:url(../../scripts/ext/resources/images/default/layout/mini-right.gif)}.x-layout-cmini-east .x-layout-mini{background-image:url(../../scripts/ext/resources/images/default/layout/mini-left.gif)}.x-layout-cmini-north .x-layout-mini{background-image:url(../../scripts/ext/resources/images/default/layout/mini-bottom.gif)}.x-layout-cmini-south .x-layout-mini{background-image:url(../../scripts/ext/resources/images/default/layout/mini-top.gif)}.x-progress-wrap{border-color:#8E8E8E}.x-progress-inner{background-color:#E7E7E7;background-image:url(../../scripts/ext/resources/images/gray/qtip/bg.gif)}.x-progress-bar{background-color:#BCBCBC;background-image:url(../../scripts/ext/resources/images/gray/progress/progress-bg.gif);border-top-color:#E2E2E2;border-bottom-color:#A4A4A4;border-right-color:#A4A4A4}.x-progress-text{font-size:11px;font-weight:bold;color:#fff}.x-progress-text-back{color:#5F5F5F}.x-list-header{background-color:#f9f9f9;background-image:url(../../scripts/ext/resources/images/gray/grid/grid3-hrow2.gif)}.x-list-header-inner div em{border-left-color:#ddd;font:normal 11px arial,tahoma,helvetica,sans-serif}.x-list-body dt em{font:normal 11px arial,tahoma,helvetica,sans-serif}.x-list-over{background-color:#eee}.x-list-selected{background-color:#f0f0f0}.x-list-resizer{border-left-color:#555;border-right-color:#555}.x-list-header-inner em.sort-asc,.x-list-header-inner em.sort-desc{background-image:url(../../scripts/ext/resources/images/gray/grid/sort-hd.gif);border-color:#d0d0d0}.x-slider-horz,.x-slider-horz .x-slider-end,.x-slider-horz .x-slider-inner{background-image:url(../../scripts/ext/resources/images/default/slider/slider-bg.png)}.x-slider-horz .x-slider-thumb{background-image:url(../../scripts/ext/resources/images/gray/slider/slider-thumb.png)}.x-slider-vert,.x-slider-vert .x-slider-end,.x-slider-vert .x-slider-inner{background-image:url(../../scripts/ext/resources/images/default/slider/slider-v-bg.png)}.x-slider-vert .x-slider-thumb{background-image:url(../../scripts/ext/resources/images/gray/slider/slider-v-thumb.png)}.x-window-dlg .ext-mb-text,.x-window-dlg .x-window-header-text{font-size:12px}.x-window-dlg .ext-mb-textarea{font:normal 12px tahoma,arial,helvetica,sans-serif}.x-window-dlg .x-msg-box-wait{background-image:url(../../scripts/ext/resources/images/default/grid/loading.gif)}.x-window-dlg .ext-mb-info{background-image:url(../../scripts/ext/resources/images/gray/window/icon-info.gif)}.x-window-dlg .ext-mb-warning{background-image:url(../../scripts/ext/resources/images/gray/window/icon-warning.gif)}.x-window-dlg .ext-mb-question{background-image:url(../../scripts/ext/resources/images/gray/window/icon-question.gif)}.x-window-dlg .ext-mb-error{background-image:url(../../scripts/ext/resources/images/gray/window/icon-error.gif)}@charset "UTF-8";#footer{margin:0 auto;clear:both;float:left;width:100%;text-align:center;border-top:#fff 1px solid}#socialmedia_share{float:left;margin-top:7px;margin-left:20px;style:block}.socialmedia_element{float:left;margin-right:8px}#poweredby{float:right;margin-right:20px}#poweredby a{color:#2A485D;text-decoration:none}#twttrHubFrame{left:-9999em}@charset "UTF-8";body.ltr{direction:ltr}body.rtl div{direction:rtl}form label{cursor:pointer}p.legend{margin-bottom:1em}p.legend em{color:#c00;font-style:normal}.form-container{width:100%;overflow:auto;margin-top:5px;margin-bottom:15px}.form-container form{padding:5px;background-color:#fff;border:#eee 1px solid;background-color:#fbfbfb}.form-container p{margin:0.5em 0 0 0}.form-container form p{margin:0}.form-container form p.note{font-style:italic;margin-left:18em;font-size:80%;color:#666}.form-container form input,.form-container form button,.form-container form select,.form-container form textarea{padding:2px;margin:2px 0 2px 0}.form-container form input.string,.form-container form textarea{width:500px}.form-container form input.date{width:auto}#login_form form table,#register_form form table{width:95%}#login_form input.string,#register_form input.string{width:95%}.form-container form input[type="checkbox"],.form-container form input[type="radio"]{margin:2px 5px}.form-container form fieldset{margin:0 0 10px 0;padding:10px;border:#ddd 1px solid;background-color:#fff}.form-container form legend{font-weight:bold;color:#666}.form-container form td.w2p_fl,.item-container form td.w2p_fl{font-weight:bold}.form-container form tr td,.item-container form tr td{padding:3px 0 0 3px}.form-container .controlset label,.form-container .controlset input{display:inline;float:none}.form-container .controlset div{margin-left:15em}.form-container .buttonrow{margin-left:180px}div.hint{position:relative;}label.over{color:#ccc;font-style:italic;position:absolute; left:5px}table.embeddedComponent{border:1px solid #b3b3b3}form table.embeddedComponent td{padding:0 5px;border:solid #b3b3b3;border-width:0 0 1px 0;text-align:left}table.embeddedComponent tr.label-row td{color:#b3b3b3}.form-container form .embeddedComponent input.string,.form-container form .embeddedComponent textarea{width:auto}table.embeddedComponent input.integer{max-width:10rem}.inline-throbber{background-image:url(../../img/indicator.gif);background-repeat:no-repeat;background-position:center;height:16px;width:16px}.inline-add,.inline-dsc,.inline-cnc,.inline-edt,.inline-rdy,.inline-rmv{cursor:pointer;background-repeat:no-repeat;background-position:center;height:23px;width:23px}.inline-add{background-image:url(../../img/crud/add.png)}.inline-dsc{background-image:url(../../img/crud/cancel.png)}.inline-cnc{background-image:url(../../img/crud/cancel.png)}.inline-edt{background-image:url(../../img/crud/edit.png)}.inline-rdy{background-image:url(../../img/crud/apply.png)}.inline-rmv{background-image:url(../../img/crud/remove.png)}.s3_inline_add_resource_link a{margin-left:2px;padding-left:2px}#filter-form{margin:0}#summary-tabs{visibility:hidden}#summary-filter-form{margin:0}#summary-sections #map{margin-top:0;padding:0}.ui-tabs .ui-tabs-panel{padding:2px 5px}textarea.comments{height:50px}textarea.richtext{height:100px}#list-btn-add,.list-btn-add{margin-bottom:10px}#list-add{display:none}#table-container{display:block;width:100%; margin-top:-1px}#table-container .empty{margin-left:10px}.dataTable thead th{ border:1px solid #ccc;border-bottom:1px solid black}.dataTable th,.fixedHeader th{text-align:center;border:1px solid #ccc}.dataTable tr.even td,.dataTable tr.odd td{border:1px solid #ccc;padding:4px 10px}.dt-export-options{float:right;padding-top:5px}.list_formats div{padding:1px;cursor:pointer;height:16px;width:16px;float:right;background-repeat:no-repeat}.export_cap_large{background-image:url(../../img/icon-cap.jpg);height:36px;width:99px}.export_cap{background-image:url(../../img/cap_16.png)}.export_have{background-image:url(../../img/have_16.png)}.export_kml{background-image:url(../../img/kml_icon.png)}.export_map{background-image:url(../../img/map_icon.png)}.export_pdf{background-image:url(../../img/pdficon_small.gif)}.export_rss{background-image:url(../../img/RSS_16.png)}.export_xls{background-image:url(../../img/icon-xls.png)}.export_xml{background-image:url(../../img/icon-xml.png)}.empty{margin-top:30px}div .dataTable_table{overflow:auto;clear:both}.dataTable{width:100%}.dataTable tr td{vertical-align:top}.dataTable.group{background-color:#ddd;border:1px solid #aaa}.dataTable tr.level_1{background-color:#999;color:#def}.dataTable tr.level_1 a{color:#def}.dataTable tr.activeRow.level_1{background-color:#1d70cf}.dataTable tr.level_2{background-color:#ddd;color:#248}.dataTable tr.level_2 a{color:#248}.dataTable tr.activeRow.level_2{background-color:#528dd1}.dataTables_filter{width:auto;float:left !important;margin-bottom:4px}.dataTables_processing{float:left;margin-left:10px}.dataTables_info{width:auto;float:right !important;clear:none !important;margin:7px 0 4px 10px}.dataTables_length{float:right !important;margin-bottom:4px}.dataTables_paginate{float:left;margin:4px 0 4px 0}.paging_full_numbers{width:auto}a.paginate_button,a.paginate_active{text-decoration:none}.sorting_disabled{background:no-repeat scroll right center transparent}.dataTable tr.dtalert .action-btn,.dataTable tr.dtalert .delete-btn{background-color:#d0d004;color:#444420}.dataTable tr.dtwarning .action-btn,.dataTable tr.dtwarning .delete-btn{background-color:#d07060;color:#431}.dataTable tr.dtdisable{text-shadow:#ccc 1px 1px 1px;color:#888}.dataTable-btn{background-color:#ddd;border:1px solid #aaa;border-radius:5px;padding:2px 5px;margin:0 3px;cursor:pointer;*cursor:hand}.dataTable-btn:hover{background-color:#efefef}table.import-item-details{display:none}.pivot-table-contents{overflow:auto}#dl-container{clear:left}div.dl{border-bottom:1px solid #aaa}.dl-header{float:right;padding:3px}.dl-row{clear:both;padding:0;border-top:1px solid #aaa}.dl-item{float:left;padding:3px 5px 3px 5px;width:98.7%}.dl-row.even,.dl-row.even .dl-item{background-color:white}.dl-row.odd,.dl-row.odd .dl-item{background-color:#e2e4ff}.dl-1-cols{width:98%}.dl-2-cols{width:48%}.dl-3-cols{width:31%}.dl-4-cols{width:22%}.dl-field{clear:left}.dl-field-label{margin-right:10px;font-weight:bold}.dl-field-value{}.infscr-loading{float:left;clear:left}.card_1_line,.card_manylines{font-size:12px;padding-top:4px;color:#666;padding-bottom:2px}.card_1_line{height:16px;line-height:normal;margin-bottom:0;text-overflow:none;overflow:hidden}.card_1_line i{margin-right:5px}.item-container{width:100%;overflow:auto;margin:5px 0 5px 0}.default-text{color:#a1a1a1;font-style:italic}ul.ui-autocomplete{z-index:9999 !important}#map{width:100%;overflow:auto}.error,.expired,.req,.req_key{color:red;font-weight:bold}.mapError{border:solid 1px red}.tooltip,.tooltipbody,.stickytip,.htmltip,.ajaxtip{position:static;text-transform:uppercase;height:20px;width:50px;background:none;background:url(../../img/help_off.gif) no-repeat}.tooltip span,.tooltipbody span,.stickytip span,.htmltip .htmltip-content,.ajaxtip span{display:none}.tooltip:hover,.tooltipbody:hover,.stickytip:hover,.htmltip:hover,.ajaxtip:hover{background-color:transparent;height:20px;width:50px;background:url(../../img/help_on.gif) no-repeat}body.popup{background-color:#fbfbfb;min-width:auto;height:auto}#popup{max-width:750px;width:100%;display:none}.loading{background:url(../../img/ajax-loader.gif) center no-repeat !important}#popup .form-container{overflow:inherit}#popup .control-group{padding-right:20px}.alert-success{color:#070;font-weight:bold;text-align:center;border:#070 1px solid;background:url(../../img/dialog-confirmation.png) #e5ffe5 no-repeat 5px 5px;margin-top:0.0em;margin-bottom:0.5em;padding-left:30px;padding-right:20px;padding-top:1.0em;padding-bottom:1.0em;cursor:pointer;clear:left}.alert-success p em{color:#070}.alert-error{color:#c00;font-weight:bold;text-align:center;border:#c00 1px solid;background:url(../../img/dialog-error.png) #ffe5e5 no-repeat 5px 5px;margin-top:0.0em;margin-bottom:0.5em;padding-left:30px;padding-right:20px;padding-top:1.0em;padding-bottom:1.0em;cursor:pointer;clear:left}.alert-error p em{color:#c00}.alert-info{color:#748d8e;font-weight:bold;text-align:center;border:#9ed8d7 1px solid;background:url(../../img/dialog-information.png) #ecfdff no-repeat 5px 5px;margin-top:0.0em;margin-bottom:0.5em;padding-left:30px;padding-right:20px;padding-top:1.0em;padding-bottom:1.0em;cursor:pointer;clear:left}.alert-info p em{color:#748d8e}.alert-warning{color:#c00;font-weight:bold;text-align:center;border:#fc6 1px solid;background:url(../../img/dialog-warning.png) #ffc no-repeat 5px 5px;margin-top:0.0em;margin-bottom:0.5em;padding-left:30px;padding-right:20px;padding-top:1.0em;padding-bottom:1.0em;cursor:pointer;clear:left}.alert-warning p em{color:#c00}.alert button.close{background:none repeat scroll 0 0 rgba(0,0,0,0);border:0 none;cursor:pointer;padding:0;display:inline;margin:0 0.3rem}.throbber,.layer_throbber,.s3-twitter-throbber,.map_loader{background-image:url(../../img/ajax-loader.gif);background-repeat:no-repeat;height:32px;width:32px}.throbber{margin-bottom:-16px;padding:0 0 0 10px}.input_throbber{background-size:60% !important;height:24px;width:24px;display:inline-block;margin:0 0 -11px -24px}.s3-twitter-throbber{height:0px;margin:66px 0 0 42px;padding:20px;width:0px}#rheader{margin-bottom:0.75em}#rheader th,#rheader td{text-align:left;padding:0.1rem 0.5rem 0.05rem 0;white-space:pre-line}#rheader th{font-weight:bold}#rheader td{padding-right:1.25rem}div.tabs{width:100%;clear:left;height:1.5em;padding:8px 0 2px 0;margin:5px 0 0 0;text-align:left;border-bottom:1px solid #3286e2}div.tabs span{float:left;border-radius:3px 3px 0 0}span.tab_last,span.tab_other{background:#3286e2;border-color:#3286e2;border-width:2px 1px 0 3px;border-style:solid;margin-right:3px;padding-right:3px}div.tabs span a{color:#fff;text-decoration:none}div.tabs span.tab_here{display:inline;position:relative;bottom:0;background:#f0f3f4;border-width:2px 2px 0 3px;border-style:solid;border-color:#69c;padding:1px 6px 0 5px;margin-right:5px;font-weight:bold}form div.tabs span.tab_here{background:#fff;border-bottom:2px solid #fff}div.tabs span.tab_here a{color:#069}span.tab_last a:hover,span.tab_other a:hover{color:#fff;background:transparent}span.tab_last:hover,span.tab_other:hover{background:#164b8b;border-color:#164b8b}span.tab_prev_active{border:1px solid #69c;color:#069;border-bottom:0}span.tab_next_active{border:1px solid #69c;color:#069;border-bottom:0}span.tab_prev_inactive{border:1px solid #bbb;color:#bbb;border-bottom:0}span.tab_next_inactive{border:1px solid #bbb;color:#bbb;border-bottom:0}span.tab_prev_active a,span.tab_next_active a{color:#069;text-decoration:None}span.tab_prev_inactive a,span.tab_next_inactive a{color:#bbb;text-decoration:None}#component{float:left;width:100%}#rfooter{padding:15px 0;clear:both}#last_update{text-align:right;font-style:italic;font-size:80%;color:#666;float:right;clear:right}.authorinfo{font-style:italic;font-size:80%;color:#666}.action-btn,.delete-btn-ajax,.delete-btn{cursor:pointer;line-height:1.5;text-decoration:none;color:#fff;background-color:#3286e2;border:1px solid #4c95e6;border-bottom:2px solid #164b8b;border-top:1px solid #5f9eeb;padding:2px 4px 2px 4px;margin:2px;z-index:500;white-space:nowrap;border-radius:2px}.action-btn:hover,.delete-btn-ajax:hover,.delete-btn:hover,.action-btn:focus,.delete-btn-ajax:focus,.delete-btn:focus{text-decoration:none;color:#fff;background-color:#164b8b}#delete-btn{margin-bottom:8px}#markDuplicate{float:right;clear:right;padding-bottom:8px}.cancel-btn{padding-left:10px}.action-lnk{font-size:85%;margin-left:15px;cursor:pointer}.action-lnk:first-child{margin-left:0}.form-toggle,.form-toggle:hover{text-decoration:none}.form-toggle i{margin-left:3px}.sublabels{font-size:85%}.plus{position:static;height:16px;width:16px;background-image:url(../../img/icon_blue_plus15px.png)}.minus{position:static;height:16px;width:16px;background-image:url(../../img/icon_blue_minus15px.png)}.expand{height:16px;width:16px;float:left;background-image:url(../../img/jquery-ui/ui-icons_222222_256x240.png);background-position:-64px -16px;white-space:nowrap}.expanded{height:16px;width:16px;float:left;background-image:url(../../img/jquery-ui/ui-icons_222222_256x240.png);background-position:-32px -16px;white-space:nowrap}#select_from_registry_row td{padding:8px}.box_top,.box_top_inner{border-top:#bbb 1px solid}.box_top{padding-top:8px}.box_top_inner{padding-top:0.2rem;padding-bottom:0.5rem}form table td.box_top_td{padding-top:8px}.box_top label,.box_top_inner label{display:inline-block}.box_bottom{border-bottom:#bbb 1px solid;padding-bottom:8px}form table tr.box_bottom td{border-bottom:#bbb 1px solid;padding-bottom:8px}.form-horizontal .control-group.box_top{margin:15px 0 0;max-width:680px}.form-horizontal .control-group.box_bottom{margin-bottom:8px;max-width:680px}.add_person_edit_bar{cursor:pointer;display:inline-block;padding-left:1.2rem}.add_person_edit_bar a{text-decoration:none}td.subheading{padding-top:10px !important;padding-bottom:5px;border-bottom:thin solid #bbb;font-weight:bold}tr.after_subheading td{padding-top:10px !important}select[disabled='disabled'],input[disabled='disabled']{background:#eee;color:#333;cursor:default}li input + a{text-decoration:none}.rfilter{float:left;padding:10px 20px 10px 10px}#comments{margin:0;padding:0;list-style:none outside none}#comments ul,ol{padding-left:20px;list-style:none outside none}#comments li{padding:10px 0 0}#comments li a.jcollapsible:hover{background:none}#comments div.comment-text ul{list-style:disc outside none}#comments div.comment-text ol{list-style:decimal outside none}#comments div.comment-text li{padding:0}#comments div.comment-body{white-space:pre-line}#comments div.comment-body p{margin-left:25px}#comments em{font-style:italic}#comments strong{font-weight:bold}#comment-form{width:390px;border:1px #9c9c9c dashed;padding:5px;margin-top:5px}.avatar{background:none repeat scroll 0 0 #fff;border-bottom:1px solid #d7d7d7;border-left:1px solid #f2f2f2;border-right:1px solid #f2f2f2;float:left;height:55px;padding:4px;width:55px}.rheader-avatar{float:left;clear:right;padding-bottom:5px;padding-right:10px}.comment-box{overflow:hidden;padding:15px 0;background:none repeat scroll 0 0 #fff;display:block;overflow:hidden;padding:10px;margin-left:15px}.comment-text{padding:0 0 0 20px;float:left}.comment-text div{white-space:pre-line}.comment-header{margin:0 0 10px 0}.comment-footer{clear:left}.comment-date{font-size:11px;margin:0 0 10px 0}.showall{display:none;position:absolute;border-style:solid;background-color:#ffc;padding:5px;margin:0 20px 0 -50px}#template_sections{margin-right:10px}#template_sections li,#master_sections li{list-style:none}.ui-droppable{padding-bottom:25px}li.ui-draggable:hover,li.ui-draggable-dragging{cursor:pointer;list-style:none;padding:3px;border:solid 1px #bbb;background:none repeat scroll 0 0 #cfdde7}.imagecrop-drag{font-weight:bold;text-align:center;padding:3em 0;margin:1em 0;color:#555;border:2px dashed #555;border-radius:7px;cursor:default}.imagecrop-drag.hover{border-style:solid;background-color:#F7F8F9}.imagecrop-btn{display:none;cursor:pointer}#show-dialog-btn{border:1px solid #efefef;margin:10px;padding:10px}.req_status_none{color:red;font-weight:bold}.req_status_partial{color:darkorange;font-weight:bold}.req_status_complete{color:green;font-weight:bold}.contacts-wrapper{width:500px}.contacts-wrapper p{margin-bottom:0.8em}.contacts-wrapper div.margin{margin-bottom:10px}.contacts-wrapper .contact.saving .editBtn,.contacts-wrapper .contact.edit .editBtn{display:none}ul.x-tab-strip,ul.x-tree-node-ct,ul.x-tree-root-ct{list-style:none outside none}.geocode_success{color:#0a0}.geocode_fail{color:#f00}.s3-grouped-checkboxes-widget-label,.s3-groupedopts-label{margin:10px 0 0 7px;padding-left:20px;height:16px;background:url(../../img/icon_blue_plus15px.png) no-repeat;cursor:pointer}.s3-grouped-checkboxes-widget-label.expanded,.s3-groupedopts-label.expanded{height:16px;width:16px;background:url(../../img/icon_blue_minus15px.png) no-repeat}.s3-grouped-checkboxes-widget .s3-checkboxes-widget,.s3-groupedopts-widget table{margin-left:2em}.form-container form fieldset .s3-checkboxes-widget label,.form-container form fieldset .s3-groupedopts-widget label{white-space:nowrap;text-align:left}.s3-groupedopts-widget label{display:inline;margin-left:5px}.s3-groupedopts-widget tr > td{padding-top:5px;padding-right:10px}.no-options-available{color:#aaa;font-style:italic}.checkboxes-widget-s3 input,.s3-checkboxes-widget input,.s3-groupedopts-widget input{display:inline-block;vertical-align:middle}.s3-checkboxes-widget-filter input{vertical-align:middle}.range-filter-label{font-size:85%}.range-filter-field{display:inline-block;margin-right:0.7rem}.age-filter-widget{display:inline-block}.age-filter-label{display:inline-block;margin-right:0.5rem}.age-filter-unit{display:inline-block;vertical-align:text-bottom;line-height:normal}.filter-form td,#filter_options td{border-top:1px solid #d9d9d9}.filter-form tr:first-child > td,#filter_options tr:first-child > td{border-top:0}.filter-form table.s3-checkboxes-widget td,.filter-form table.s3-groupedopts-widget td,#filter_options table.s3-checkboxes-widget td,#filter_options table.s3searchminmaxwidget td{border-top:0}.filter-form .ui-multiselect.ui-widget.ui-state-default.ui-corner-all,.form-container .ui-multiselect.ui-widget.ui-state-default.ui-corner-all{display:block;min-width:220px}.filter-form .ui-multiselect.ui-widget.ui-state-default.ui-corner-all:first-of-type,.form-container .ui-multiselect.ui-widget.ui-state-default.ui-corner-all:first-of-type{clear:none}.ui-selectmenu-button,.ui-multiselect-menu{min-width:220px}select.multiselect-filter-widget{display:none}.filter-advanced{text-decoration:none;cursor:pointer}.filter-advanced-label{padding-right:4px}.s3-options-filter-anyall label{display:inline;margin-right:0.7rem;font-size:0.7rem}.widget-org-hierarchy-menu{overflow:auto;height:10em;width:36em;position:relative}.widget-org-hierarchy-menu .ui-menu{position:absolute;top:0;bottom:0;overflow:auto;width:32em}.widget-org-hierarchy-menu .ui-menu a{cursor:pointer}.widget-org-hierarchy-crumbs{list-style:none}.widget-org-hierarchy-crumbs li{display:inline;cursor:pointer}.widget-org-hierarchy-crumbs li:after{content:" > "}.widget-org-hierarchy-crumbs li a{text-decoration:none}.widget-org-hierarchy-crumbs li.selected a{text-decoration:none;border-bottom:1px dashed black}.ui-datepicker-trigger{background-image:url(../../img/calendar.gif);background-repeat:no-repeat;height:15px;width:16px;margin-left:3px;border:0;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}.form-container form button.ui-datepicker-trigger{margin-left:3px}td .icon{margin-top:0}option.missing{background-color:yellow}.dms-label{padding:4px;font-weight:bold}.dms-input.invalidinput{border:solid thin red}.resizable-textarea .grippie{ background:url(../../img/grippie.png) no-repeat scroll center 2px #eee;border-color:#ddd;border-right:1px solid #ddd;border-style:solid;border-width:0 1px 1px;cursor:s-resize;height:9px;overflow:hidden}.translation_module_table{width:55%}textarea#project_task_description{height:200px}select#sub_defaulttime_defaulttime_person_id_edit_none{width:150px}input#sub_defaulttime_defaulttime_hours_edit_none{width:60px}.filter-manager-widget{float:left}.fm-load,.fm-save,.fm-delete,.fm-create,.fm-accept,.fm-cancel{float:left;margin-left:5px}div.fm-load,div.fm-save,div.fm-delete,div.fm-create,div.fm-accept,div.fm-cancel{margin-top:7px;width:16px;height:16px}div.fm-load{background:url(../../img/filter/load.png) no-repeat}div.fm-save{background:url(../../img/filter/save.png) no-repeat}div.fm-delete{background:url(../../img/filter/delete.png) no-repeat}div.fm-create{background:url(../../img/filter/create.png) no-repeat}div.fm-accept{background:url(../../img/crud/apply.png) no-repeat}div.fm-cancel{background:url(../../img/crud/cancel.png) no-repeat}.cms-edit{display:table}.datetimepicker{width:110px}.datetimepicker.hide-time{width:75px}.ui-dialog .ui-dialog-content{padding:0 !important}.ui-dialog{padding:0;width:750px !important}body.ltr label.ui-corner-all span{left:10px}body.rtl label.ui-corner-all span{right:10px}.s3-hierarchy-tree.jstree,.s3-hierarchy-header{border:1px solid #ccc;padding:2px 3px 4px 3px;background:white;}.jstree-contextmenu{z-index:9999}.s3-hierarchy-wrapper{z-index:9998}.s3-hierarchy-header{font-size:0.8rem;display:none}.s3-hierarchy-action-node,.s3-hierarchy-none{font-style:italic}form.jeditable-input input{max-width:400px !important}.pt-form legend{font-size:14px;margin-bottom:0;border:0 !important}button.toggle-text{font-size:10px !important;margin-left:12px !important;line-height:1.0}.action-bar{color:#8a8989;font-size:14px;position:relative;top:4px}.action-bar.fleft{margin-right:8px}.action-bar a:hover,.action-bar a:visited:hover{color:#ffa500;text-decoration:none}.maxLength{background-color:#ffcdcd;border:3px solid #d55b5b}.ui-selectmenu-menu .ui-menu.customicons{height:400px}.ui-selectmenu-menu .ui-menu.customicons .ui-menu-item{padding:1em 0 1em 4em}.ui-selectmenu-menu .ui-menu.customicons .ui-menu-item .ui-icon{background-repeat:no-repeat !important;background-position:left top;top:0.1em}.card > .fleft{margin-right:10px}.media-object{display:block}.ajax_more{float:right;width:16px;height:16px;margin:0 2px 2px 0}.ajax_more.collapsed{background:url(../../img/icon_blue_plus15px.png) no-repeat left top}.ajax_more.expanded{background:url(../../img/icon_blue_minus15px.png) no-repeat left top}.s3-timeline{height:400px;border:1px solid #aaa;font-family:Trebuchet MS,sans-serif;font-size:85%}#video-toc{clear:left}.video-header{padding-top:50px}.s3-unmask{margin-left:10px;cursor:pointer}.s3-password-widget{display:inline-block}.s3-twitter-container{width:350px;height:130px}.db-config{padding-top:0.2rem;float:right}.db-config-on,.db-config-off{cursor:pointer;padding:0.125rem;font-size:1rem!important}.db-config-on:hover,.db-config-off:hover{background-color:#7f7f7f}.db-config-on{color:#7f7f7f}.db-config-on:hover{color:white}.db-config-off{color:green}.db-config-off:hover{color:lightgreen}.db-configbar{background-color:#7f7f7f;padding:0.125rem;display:none}.db-configbar-right{float:right}.db-configbar i{color:white;padding:0.125rem;cursor:pointer}.db-configbar i:hover{background-color:silver}.db-config-active{padding:0.125rem;border:1px solid #7f7f7f}.db-has-dialog .db-configbar{background-color:#af4f4f}body.rtl .db-config,body.rtl .db-configbar-right{float:left}@charset "UTF-8";.ir{display:block;text-indent:-999em;overflow:hidden;background-repeat:no-repeat}.hide{display:none !important}.visuallyhidden{position:absolute !important;clip:rect(1px 1px 1px 1px);clip:rect(1px,1px,1px,1px)}.invisible{visibility:hidden}.mini{font-size:80%}.wide{width:100%}.fleft{float:left !important}.fright{float:right !important}.tacenter{text-align:center !important}.taleft{text-align:left !important}.taright{text-align:right !important}.cf:before,.cf:after{content:"\0020";display:block;height:0;visibility:hidden}.cf:after{clear:both}.cf{zoom:1}* html .cf{height:1%}.ltr{direction:ltr}.rtl{direction:rtl}@charset "UTF-8";#login_box{width:100% !important;background:transparent;border:0}#site-title{text-align:center;padding-top:2rem;margin-bottom:3rem}#site-title h2,#site-title h3{font-weight:normal!important;background:none!important;padding:0.35rem 0!important;margin:0!important}#site-title h2{font-size:2.3125rem!important}#login_form h3{font-size:1.6875rem!important;padding:0.35rem 0!important}.row.home-top{min-height:30rem}@font-face{font-family:'FontAwesome';src:url('../../fonts/fontawesome-webfont.eot?v=4.7.0');src:url('../../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url('../../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('../../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'),url('../../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('../../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:0.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul > li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:0.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid 0.08em #eeeeee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)} 100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)} 100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0,mirror=1)";-webkit-transform:scale(-1,1);-ms-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2,mirror=1)";-webkit-transform:scale(1,-1);-ms-transform:scale(1,-1);transform:scale(1,-1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#ffffff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}div.ui-cluetip{font-size:1em}.ui-cluetip-header,.ui-cluetip-content{padding:12px}.ui-cluetip-header{font-size:1em;margin:0;overflow:hidden}.cluetip-title .cluetip-close{float:right;position:relative}.cluetip-close img{border:0}#cluetip-waitimage{width:43px;height:11px;position:absolute;background-image:url(../../img/jquery.cluetip/wait.gif)}.cluetip-arrows{display:none;position:absolute;top:0;left:-11px;width:11px;height:22px;background-repeat:no-repeat;background-position:0 0;border-width:0}.cluetip-extra{display:none}.cluetip-default,.cluetip-default .cluetip-outer{background-color:#d9d9c2}.cluetip-default .ui-cluetip-header{background-color:#87876a}div.cluetip-default .cluetip-arrows{border-width:0;background:transparent none}div.clue-right-default .cluetip-arrows{background-image:url(../../img/jquery.cluetip/darrowleft.gif)}div.clue-left-default .cluetip-arrows{background-image:url(../../img/jquery.cluetip/darrowright.gif);left:100%;margin-right:-11px}div.clue-top-default .cluetip-arrows{background-image:url(../../img/jquery.cluetip/darrowdown.gif);top:100%;left:50%;margin-left:-11px;width:22px;height:11px}div.clue-bottom-default .cluetip-arrows{background-image:url(../../img/jquery.cluetip/darrowup.gif);top:-11px;left:50%;margin-left:-11px;width:22px;height:11px}.cluetip-jtip{background-color:#fff}.cluetip-jtip .cluetip-outer{border:2px solid #ccc;position:relative;background-color:#fff}.cluetip-jtip .cluetip-inner{padding:5px;display:inline-block}.cluetip-jtip div.cluetip-close{text-align:right;margin:0 5px 0;color:#900}.cluetip-jtip .ui-cluetip-header{background-color:#ccc;padding:6px}div.cluetip-jtip .cluetip-arrows{border-width:0;background:transparent none}div.clue-right-jtip .cluetip-arrows{background-image:url(../../img/jquery.cluetip/arrowleft.gif)}div.clue-left-jtip .cluetip-arrows{background-image:url(../../img/jquery.cluetip/arrowright.gif);left:100%;margin-right:-11px}div.clue-top-jtip .cluetip-arrows{background-image:url(../../img/jquery.cluetip/arrowdown.gif);top:100%;left:50%;width:22px;height:11px;margin-left:-11px}div.clue-bottom-jtip .cluetip-arrows{background-image:url(../../img/jquery.cluetip/arrowup.gif);top:-11px;left:50%;width:22px;height:11px;margin-left:-11px}.cluetip-rounded{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;background-color:#fff;border:1px solid #ccc}.cluetip-rounded .cluetip-outer{background-color:#fff}.cluetip-rounded .cluetip-arrows{border-color:#ccc}div.cluetip-rounded .cluetip-arrows{font-size:0;line-height:0%;width:0;height:0;border-style:solid;background:transparent none}div.clue-right-rounded .cluetip-arrows{border-width:11px 11px 11px 0;border-top-color:transparent;border-bottom-color:transparent;border-left-color:transparent}div.clue-left-rounded .cluetip-arrows{left:100%;margin-right:-11px;border-width:11px 0 11px 11px;border-top-color:transparent;border-right-color:transparent;border-bottom-color:transparent}div.clue-top-rounded .cluetip-arrows{top:100%;left:50%;border-width:11px 11px 0 11px;border-right-color:transparent;border-bottom-color:transparent;border-left-color:transparent}.clue-bottom-rounded .cluetip-arrows{top:-11px;left:50%;border-width:0 11px 11px 11px;border-top-color:transparent;border-right-color:transparent;border-left-color:transparent}.cluetip-rounded .cluetip-title,.cluetip-rounded .cluetip-inner{zoom:1}table.dataTable{margin:0 auto;clear:both;width:100%}table.dataTable thead th{padding:3px 18px 3px 10px;border-bottom:1px solid black;font-weight:bold;cursor:pointer;*cursor:hand}table.dataTable tfoot th{padding:3px 18px 3px 10px;border-top:1px solid black;font-weight:bold}table.dataTable td{padding:3px 10px}table.dataTable td.center,table.dataTable td.dataTables_empty{text-align:center}table.dataTable tr.odd{background-color:#E2E4FF}table.dataTable tr.even{background-color:white}table.dataTable tr.odd td.sorting_1{background-color:#D3D6FF}table.dataTable tr.odd td.sorting_2{background-color:#DADCFF}table.dataTable tr.odd td.sorting_3{background-color:#E0E2FF}table.dataTable tr.even td.sorting_1{background-color:#EAEBFF}table.dataTable tr.even td.sorting_2{background-color:#F2F3FF}table.dataTable tr.even td.sorting_3{background-color:#F9F9FF}.dataTables_wrapper{position:relative;clear:both;*zoom:1}.dataTables_length{float:left}.dataTables_filter{float:right;text-align:right}.dataTables_info{clear:both;float:left}.dataTables_paginate{float:right;text-align:right}.paginate_disabled_previous,.paginate_enabled_previous,.paginate_disabled_next,.paginate_enabled_next{height:19px;float:left;cursor:pointer;*cursor:hand;color:#111 !important}.paginate_disabled_previous:hover,.paginate_enabled_previous:hover,.paginate_disabled_next:hover,.paginate_enabled_next:hover{text-decoration:none !important}.paginate_disabled_previous:active,.paginate_enabled_previous:active,.paginate_disabled_next:active,.paginate_enabled_next:active{outline:none}.paginate_disabled_previous,.paginate_disabled_next{color:#666 !important}.paginate_disabled_previous,.paginate_enabled_previous{padding-left:23px}.paginate_disabled_next,.paginate_enabled_next{padding-right:23px;margin-left:10px}.paginate_enabled_previous{background:url('../../img/jquery.dataTables/back_enabled.png') no-repeat top left}.paginate_enabled_previous:hover{background:url('../../img/jquery.dataTables/back_enabled_hover.png') no-repeat top left}.paginate_disabled_previous{background:url('../../img/jquery.dataTables/back_disabled.png') no-repeat top left}.paginate_enabled_next{background:url('../../img/jquery.dataTables/forward_enabled.png') no-repeat top right}.paginate_enabled_next:hover{background:url('../../img/jquery.dataTables/forward_enabled_hover.png') no-repeat top right}.paginate_disabled_next{background:url('../../img/jquery.dataTables/forward_disabled.png') no-repeat top right}.paging_full_numbers{height:22px;line-height:22px}.paging_full_numbers a:active{outline:none}.paging_full_numbers a:hover{text-decoration:none}.paging_full_numbers a.paginate_button,.paging_full_numbers a.paginate_active{border:1px solid #aaa;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;padding:2px 5px;margin:0 3px;cursor:pointer;*cursor:hand;color:#333 !important}.paginate_button_disabled{opacity:0.5;cursor:not-allowed !important}.paging_full_numbers a.paginate_button{background-color:#ddd}.paging_full_numbers a.paginate_button:hover{background-color:#ccc;text-decoration:none !important}.paging_full_numbers a.paginate_active{background-color:#99B3FF}.dataTables_processing{position:absolute;top:50%;left:50%;width:250px;height:30px;margin-left:-125px;margin-top:-15px;padding:14px 0 2px 0;border:1px solid #ddd;text-align:center;color:#999;font-size:14px;background-color:white}.sorting{background:url('../../img/jquery.dataTables/sort_both.png') no-repeat center right}.sorting_asc{background:url('../../img/jquery.dataTables/sort_asc.png') no-repeat center right}.sorting_desc{background:url('../../img/jquery.dataTables/sort_desc.png') no-repeat center right}.sorting_asc_disabled{background:url('../../img/jquery.dataTables/sort_asc_disabled.png') no-repeat center right}.sorting_desc_disabled{background:url('../../img/jquery.dataTables/sort_desc_disabled.png') no-repeat center right}table.dataTable thead th:active,table.dataTable thead td:active{outline:none}.dataTables_scroll{clear:both}.dataTables_scrollBody{*margin-top:-1px;-webkit-overflow-scrolling:touch}table.dataTable.dtr-inline.collapsed tbody td:first-child,table.dataTable.dtr-inline.collapsed tbody th:first-child{position:relative;padding-left:30px;cursor:pointer}table.dataTable.dtr-inline.collapsed tbody td:first-child:before,table.dataTable.dtr-inline.collapsed tbody th:first-child:before{top:8px;left:4px;height:16px;width:16px;display:block;position:absolute;color:white;border:2px solid white;border-radius:16px;text-align:center;line-height:14px;box-shadow:0 0 3px #444;box-sizing:content-box;content:'+';background-color:#31b131}table.dataTable.dtr-inline.collapsed tbody td:first-child.dataTables_empty:before,table.dataTable.dtr-inline.collapsed tbody th:first-child.dataTables_empty:before{display:none}table.dataTable.dtr-inline.collapsed tbody tr.parent td:first-child:before,table.dataTable.dtr-inline.collapsed tbody tr.parent th:first-child:before{content:'-';background-color:#d33333}table.dataTable.dtr-inline.collapsed tbody tr.child td:before{display:none}table.dataTable.dtr-column tbody td.control,table.dataTable.dtr-column tbody th.control{position:relative;cursor:pointer}table.dataTable.dtr-column tbody td.control:before,table.dataTable.dtr-column tbody th.control:before{top:50%;left:50%;height:16px;width:16px;margin-top:-10px;margin-left:-10px;display:block;position:absolute;color:white;border:2px solid white;border-radius:16px;text-align:center;line-height:14px;box-shadow:0 0 3px #444;box-sizing:content-box;content:'+';background-color:#31b131}table.dataTable.dtr-column tbody tr.parent td.control:before,table.dataTable.dtr-column tbody tr.parent th.control:before{content:'-';background-color:#d33333}table.dataTable tr.child{padding:0.5em 1em}table.dataTable tr.child:hover{background:transparent !important}table.dataTable tr.child ul{display:inline-block;list-style-type:none;margin:0;padding:0}table.dataTable tr.child ul li{border-bottom:1px solid #efefef;padding:0.5em 0}table.dataTable tr.child ul li:first-child{padding-top:0}table.dataTable tr.child ul li:last-child{border-bottom:none}table.dataTable tr.child span.dtr-title{display:inline-block;min-width:75px;font-weight:bold}ul.tagit{padding:1px 5px;overflow:auto;margin-left:inherit;margin-right:inherit}ul.tagit li{display:block;float:left;margin:2px 5px 2px 0}ul.tagit li.tagit-choice{position:relative;line-height:inherit}input.tagit-hidden-field{display:none}ul.tagit li.tagit-choice-read-only{padding:.2em .5em .2em .5em} ul.tagit li.tagit-choice-editable{padding:.2em 18px .2em .5em} ul.tagit li.tagit-new{padding:.25em 4px .25em 0}ul.tagit li.tagit-choice a.tagit-label{cursor:pointer;text-decoration:none}ul.tagit li.tagit-choice .tagit-close{cursor:pointer;position:absolute;right:.1em;top:50%;margin-top:-8px;line-height:17px}ul.tagit li.tagit-choice .tagit-close .text-icon{display:none}ul.tagit li.tagit-choice input{display:block;float:left;margin:2px 5px 2px 0}ul.tagit input[type="text"]{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;-moz-box-shadow:none;-webkit-box-shadow:none;box-shadow:none;border:none;margin:0;padding:0;width:inherit;background-color:inherit;outline:none}.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.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:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default !important;pointer-events:none}.ui-icon{display:inline-block;vertical-align:middle;margin-top:-.25em;position:relative;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-icon-block{left:50%;margin-left:-8px;display:block}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-button{padding:.4em 1em;display:inline-block;position:relative;line-height:normal;margin-right:.1em;cursor:pointer;vertical-align:middle;text-align:center;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;overflow:visible}.ui-button,.ui-button:link,.ui-button:visited,.ui-button:hover,.ui-button:active{text-decoration:none}.ui-button-icon-only{width:2em;box-sizing:border-box;text-indent:-9999px;white-space:nowrap}input.ui-button.ui-button-icon-only{text-indent:0}.ui-button-icon-only .ui-icon{position:absolute;top:50%;left:50%;margin-top:-8px;margin-left:-8px}.ui-button.ui-icon-notext .ui-icon{padding:0;width:2.1em;height:2.1em;text-indent:-9999px;white-space:nowrap}input.ui-button.ui-icon-notext .ui-icon{width:auto;height:auto;text-indent:0;white-space:normal;padding:.4em 1em}input.ui-button::-moz-focus-inner,button.ui-button::-moz-focus-inner{border:0;padding:0}.ui-datepicker{min-width:17em;padding:.2em .2em 0;display:none}.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,.ui-datepicker select.ui-datepicker-year{width:45%}.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}.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,.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%;font-size:0}.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,.ui-datepicker-rtl .ui-datepicker-group{float:right}.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-datepicker .ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat;left:.5em;top:.3em}.ui-dialog{position:absolute;top:0;left:0;padding:.2em;outline:0}.ui-dialog .ui-dialog-titlebar{padding:.4em 1em;position:relative}.ui-dialog .ui-dialog-title{float:left;margin:.1em 0;white-space:nowrap;width:90%;overflow:hidden;text-overflow:ellipsis}.ui-dialog .ui-dialog-titlebar-close{position:absolute;right:.3em;top:50%;width:20px;margin:-10px 0 0 0;padding:1px;height:20px}.ui-dialog .ui-dialog-content{position:relative;border:0;padding:.5em 1em;background:none;overflow:auto}.ui-dialog .ui-dialog-buttonpane{text-align:left;border-width:1px 0 0 0;background-image:none;margin-top:.5em;padding:.3em 1em .5em .4em}.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset{float:right}.ui-dialog .ui-dialog-buttonpane button{margin:.5em .4em .5em 0;cursor:pointer}.ui-dialog .ui-resizable-n{height:2px;top:0}.ui-dialog .ui-resizable-e{width:2px;right:0}.ui-dialog .ui-resizable-s{height:2px;bottom:0}.ui-dialog .ui-resizable-w{width:2px;left:0}.ui-dialog .ui-resizable-se,.ui-dialog .ui-resizable-sw,.ui-dialog .ui-resizable-ne,.ui-dialog .ui-resizable-nw{width:7px;height:7px}.ui-dialog .ui-resizable-se{right:0;bottom:0}.ui-dialog .ui-resizable-sw{left:0;bottom:0}.ui-dialog .ui-resizable-ne{right:0;top:0}.ui-dialog .ui-resizable-nw{left:0;top:0}.ui-draggable .ui-dialog-titlebar{cursor:move}.ui-draggable-handle{-ms-touch-action:none;touch-action:none}.ui-menu{list-style:none;padding:0;margin:0;display:block;outline:0}.ui-menu .ui-menu{position:absolute}.ui-menu .ui-menu-item{margin:0;cursor:pointer;list-style-image:url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")}.ui-menu .ui-menu-item-wrapper{position:relative;padding:3px 1em 3px .4em}.ui-menu .ui-menu-divider{margin:5px 0;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-state-focus,.ui-menu .ui-state-active{margin:-1px}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item-wrapper{padding-left:2em}.ui-menu .ui-icon{position:absolute;top:0;bottom:0;left:.2em;margin:auto 0}.ui-menu .ui-menu-icon{left:auto;right:0}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block;-ms-touch-action:none;touch-action:none}.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}.ui-selectmenu-menu{padding:0;margin:0;position:absolute;top:0;left:0;display:none}.ui-selectmenu-menu .ui-menu{overflow:auto;overflow-x:hidden;padding-bottom:1px}.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup{font-size:1em;font-weight:bold;line-height:1.5;padding:2px 0.4em;margin:0.5em 0 0 0;height:auto;border:0}.ui-selectmenu-open{display:block}.ui-selectmenu-text{display:block;margin-right:20px;overflow:hidden;text-overflow:ellipsis}.ui-selectmenu-button.ui-button{text-align:left;white-space:nowrap;width:14em}.ui-selectmenu-icon.ui-icon{float:right;margin-top:0}.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;-ms-touch-action:none;touch-action:none}.ui-slider .ui-slider-range{position:absolute;z-index:1;font-size:.7em;display:block;border:0;background-position:0 0}.ui-slider.ui-state-disabled .ui-slider-handle,.ui-slider.ui-state-disabled .ui-slider-range{filter:inherit}.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}.ui-tabs{position:relative;padding:.2em}.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:0;margin:1px .2em 0 0;border-bottom-width:0;padding:0;white-space:nowrap}.ui-tabs .ui-tabs-nav .ui-tabs-anchor{float:left;padding:.5em 1em;text-decoration:none}.ui-tabs .ui-tabs-nav li.ui-tabs-active{margin-bottom:-1px;padding-bottom:1px}.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor{cursor:text}.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor{cursor:pointer}.ui-tabs .ui-tabs-panel{display:block;border-width:0;padding:1em 1.4em;background:none}.ui-timepicker-inline{display:inline}#ui-timepicker-div{padding:0.2em}.ui-timepicker-table{display:inline-table;width:0}.ui-timepicker-table table{margin:0.15em 0 0 0;border-collapse:collapse}.ui-timepicker-hours,.ui-timepicker-minutes{padding:0.2em}.ui-timepicker-table .ui-timepicker-title{line-height:1.8em;text-align:center}.ui-timepicker-table td{padding:0.1em;width:2.2em}.ui-timepicker-table th.periods{padding:0.1em;width:2.2em}.ui-timepicker-table td span{display:block;padding:0.2em 1.5em 0.2em 0.5em;width:1.2em;text-align:right;text-decoration:none}.ui-timepicker-table td a{display:block;padding:0.2em 0.3em 0.2em 0.5em;cursor:pointer;text-align:right;text-decoration:none}.ui-timepicker .ui-timepicker-buttonpane{background-image:none;margin:.7em 0 0 0;padding:0 .2em;border-left:0;border-right:0;border-bottom:0}.ui-timepicker .ui-timepicker-buttonpane button{margin:.5em .2em .4em;cursor:pointer;padding:.2em .6em .3em .6em;width:auto;overflow:visible}.ui-timepicker .ui-timepicker-close{float:right }.ui-timepicker .ui-timepicker-now{float:left}.ui-timepicker .ui-timepicker-deselect{float:left}body.ltr .ui-multiselect{padding:2px 0 2px 4px;text-align:left }body.ltr .ui-multiselect span.ui-icon{float:right }.ui-multiselect-single .ui-multiselect-checkboxes input{position:absolute !important;top:auto !important}body.ltr .ui-multiselect-single .ui-multiselect-checkboxes input{left:-9999px}.ui-multiselect-single .ui-multiselect-checkboxes label{padding:5px !important }.ui-multiselect-header{margin-bottom:3px;padding:3px 0 3px 4px }.ui-multiselect-header ul{font-size:0.9em }.ui-multiselect-header ul li{float:left;padding:0 10px 0 0 }.ui-multiselect-header a{text-decoration:none }.ui-multiselect-header a:hover{text-decoration:underline }.ui-multiselect-header span.ui-icon{float:left }.ui-multiselect-header li.ui-multiselect-close{float:right;text-align:right;padding-right:0 }.ui-multiselect-menu{display:none;padding:3px;position:absolute;z-index:999999}.ui-multiselect-checkboxes{position:relative ;overflow-y:scroll }.ui-multiselect-checkboxes label{cursor:default;display:block;border:1px solid transparent;padding:3px 1px }.ui-multiselect-checkboxes label input{position:relative;top:1px }.ui-multiselect-checkboxes li{clear:both;font-size:0.9em;padding-right:3px }.ui-multiselect-checkboxes li.ui-multiselect-optgroup-label{text-align:center;font-weight:bold;border-bottom:1px solid }.ui-multiselect-checkboxes li.ui-multiselect-optgroup-label a{display:block;padding:3px;margin:1px 0;text-decoration:none }.ui-multiselect-hasfilter ul{position:relative;top:2px }.ui-multiselect-filter{float:left;margin-right:10px;font-size:11px }.ui-multiselect-filter input{width:100px;font-size:10px;margin-left:5px;height:15px;padding:2px;border:1px solid #292929;-webkit-appearance:textfield;-webkit-box-sizing:content-box}body.rtl .ui-multiselect{direction:rtl;text-align:right;padding:2px 4px 2px 0px}body.rtl .ui-multiselect span.ui-icon{float:left}body.rtl .ui-multiselect-checkboxes{direction:rtl;text-align:right}body.rtl .ui-multiselect-single .ui-multiselect-checkboxes input{right:-9999px}body.rtl .ui-multiselect-menu .ui-state-hover{font-weight:normal}body.rtl button.ui-state-default{font-weight:normal}body.rtl .ui-multiselect-filter{direction:ltr}.ui-timepicker-div .ui-widget-header{margin-bottom:8px}.ui-timepicker-div dl{text-align:left}.ui-timepicker-div dl dt{float:left;clear:left;padding:0 0 0 5px}.ui-timepicker-div dl dd{margin:0 10px 10px 40%}.ui-timepicker-div td{font-size:90%}.ui-tpicker-grid-label{background:none;border:none;margin:0;padding:0}.ui-timepicker-rtl{direction:rtl}.ui-timepicker-rtl dl{text-align:right;padding:0 5px 0 0}.ui-timepicker-rtl dl dt{float:right;clear:right}.ui-timepicker-rtl dl dd{margin:0 40% 10px 10px}.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.ui-widget-content{border:1px solid #d3d3d3}.ui-widget-content{border:1px solid #aaaaaa;background:#ffffff;color:#222222}.ui-widget-content a{color:#222222}.ui-widget-header{border:1px solid #aaaaaa;background:#cccccc url("../../img/jquery.ui/ui-bg_highlight-soft_75_cccccc_1x100.png") 50% 50% repeat-x;color:#222222;font-weight:bold}.ui-widget-header a{color:#222222}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default,.ui-button,html .ui-button.ui-state-disabled:hover,html .ui-button.ui-state-disabled:active{border:1px solid #d3d3d3;background:#e6e6e6 url("../../img/jquery.ui/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,a.ui-button,a:link.ui-button,a:visited.ui-button,.ui-button{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,.ui-button:hover,.ui-button:focus{border:1px solid #999999;background:#dadada url("../../img/jquery.ui/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,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited,a.ui-button:hover,a.ui-button:focus{color:#212121;text-decoration:none}.ui-visual-focus{box-shadow:0 0 3px 1px rgb(94,158,214)}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active,a.ui-button:active,.ui-button:active,.ui-button.ui-state-active:hover{border:1px solid #aaaaaa;background:#ffffff url("../../img/jquery.ui/ui-bg_glass_65_ffffff_1x400.png") 50% 50% repeat-x;font-weight:normal;color:#212121}.ui-icon-background,.ui-state-active .ui-icon-background{border:#aaaaaa;background-color:#212121}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#212121;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #fcefa1;background:#fbf9ee url("../../img/jquery.ui/ui-bg_glass_55_fbf9ee_1x400.png") 50% 50% repeat-x;color:#363636}.ui-state-checked{border:1px solid #fcefa1;background:#fbf9ee}.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("../../img/jquery.ui/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}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url("../../img/jquery.ui/ui-icons_222222_256x240.png")}.ui-widget-header .ui-icon{background-image:url("../../img/jquery.ui/ui-icons_222222_256x240.png")}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon,.ui-button:hover .ui-icon,.ui-button:focus .ui-icon{background-image:url("../../img/jquery.ui/ui-icons_454545_256x240.png")}.ui-state-active .ui-icon,.ui-button:active .ui-icon{background-image:url("../../img/jquery.ui/ui-icons_454545_256x240.png")}.ui-state-highlight .ui-icon,.ui-button .ui-state-highlight.ui-icon{background-image:url("../../img/jquery.ui/ui-icons_2e83ff_256x240.png")}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url("../../img/jquery.ui/ui-icons_cd0a0a_256x240.png")}.ui-button .ui-icon{background-image:url("../../img/jquery.ui/ui-icons_888888_256x240.png")}.ui-icon-blank{background-position:16px 16px}.ui-icon-caret-1-n{background-position:0 0}.ui-icon-caret-1-ne{background-position:-16px 0}.ui-icon-caret-1-e{background-position:-32px 0}.ui-icon-caret-1-se{background-position:-48px 0}.ui-icon-caret-1-s{background-position:-65px 0}.ui-icon-caret-1-sw{background-position:-80px 0}.ui-icon-caret-1-w{background-position:-96px 0}.ui-icon-caret-1-nw{background-position:-112px 0}.ui-icon-caret-2-n-s{background-position:-128px 0}.ui-icon-caret-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:-65px -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:-65px -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:1px -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-on{background-position:-96px -144px}.ui-icon-radio-off{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{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}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:4px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:4px}.ui-widget-overlay{background:#aaaaaa;opacity:.3;filter:Alpha(Opacity=30)}.ui-widget-shadow{-webkit-box-shadow:-8px -8px 8px #aaaaaa;box-shadow:-8px -8px 8px #aaaaaa}div.olMap{z-index:0;padding:0 !important;margin:0 !important;cursor:default}div.olMapViewport{text-align:left}div.olLayerDiv{-moz-user-select:none;-khtml-user-select:none}.olLayerGoogleCopyright{left:2px;bottom:2px}.olLayerGoogleV3.olLayerGoogleCopyright{right:auto !important}.olLayerGooglePoweredBy{left:2px;bottom:15px}.olLayerGoogleV3.olLayerGooglePoweredBy{bottom:15px !important}.olForeignContainer{opacity:1 !important}.olControlAttribution{font-size:smaller;right:3px;bottom:4.5em;position:absolute;display:block}.olControlScale{right:3px;bottom:3em;display:block;position:absolute;font-size:smaller}.olControlScaleLine{display:block;position:absolute;left:10px;bottom:15px;font-size:xx-small}.olControlScaleLineBottom{border:solid 2px black;border-bottom:none;margin-top:-2px;text-align:center}.olControlScaleLineTop{border:solid 2px black;border-top:none;text-align:center}.olControlPermalink{right:3px;bottom:1.5em;display:block;position:absolute;font-size:smaller}div.olControlMousePosition{bottom:0;right:3px;display:block;position:absolute;font-family:Arial;font-size:smaller}.olControlOverviewMapContainer{position:absolute;bottom:0;right:0}.olControlOverviewMapElement{padding:10px 18px 10px 10px;background-color:#00008B;-moz-border-radius:1em 0 0 0}.olControlOverviewMapMinimizeButton,.olControlOverviewMapMaximizeButton{height:18px;width:18px;right:0;bottom:80px;cursor:pointer}.olControlOverviewMapExtentRectangle{overflow:hidden;background-image:url(../../img/gis/openlayers/theme_default/blank.gif);cursor:move;border:2px dotted red}.olControlOverviewMapRectReplacement{overflow:hidden;cursor:move;background-image:url(../../img/gis/openlayers/theme_default/overview_replacement.gif);background-repeat:no-repeat;background-position:center}.olLayerGeoRSSDescription{float:left;width:100%;overflow:auto;font-size:1.0em}.olLayerGeoRSSClose{float:right;color:gray;font-size:1.2em;margin-right:6px;font-family:sans-serif}.olLayerGeoRSSTitle{float:left;font-size:1.2em}.olPopupContent{padding:5px;overflow:auto}.olControlNavigationHistory{background-image:url(../../img/gis/openlayers/theme_default/navigation_history.png);background-repeat:no-repeat;width:24px;height:24px}.olControlNavigationHistoryPreviousItemActive{background-position:0 0}.olControlNavigationHistoryPreviousItemInactive{background-position:0 -24px}.olControlNavigationHistoryNextItemActive{background-position:-24px 0}.olControlNavigationHistoryNextItemInactive{background-position:-24px -24px}div.olControlSaveFeaturesItemActive{background-image:url(../../img/gis/openlayers/theme_default/save_features_on.png);background-repeat:no-repeat;background-position:0 1px}div.olControlSaveFeaturesItemInactive{background-image:url(../../img/gis/openlayers/theme_default/save_features_off.png);background-repeat:no-repeat;background-position:0 1px}.olHandlerBoxZoomBox{border:2px solid red;position:absolute;background-color:white;opacity:0.50;font-size:1px;filter:alpha(opacity=50)}.olHandlerBoxSelectFeature{border:2px solid blue;position:absolute;background-color:white;opacity:0.50;font-size:1px;filter:alpha(opacity=50)}.olControlPanPanel{top:10px;left:5px}.olControlPanPanel div{background-image:url(../../img/gis/openlayers/theme_default/pan-panel.png);height:18px;width:18px;cursor:pointer;position:absolute}.olControlPanPanel .olControlPanNorthItemInactive{top:0;left:9px;background-position:0 0}.olControlPanPanel .olControlPanSouthItemInactive{top:36px;left:9px;background-position:18px 0}.olControlPanPanel .olControlPanWestItemInactive{position:absolute;top:18px;left:0;background-position:0 18px}.olControlPanPanel .olControlPanEastItemInactive{top:18px;left:18px;background-position:18px 18px}.olControlZoomPanel{top:71px;left:14px}.olControlZoomPanel div{background-image:url(../../img/gis/openlayers/theme_default/zoom-panel.png);position:absolute;height:18px;width:18px;cursor:pointer}.olControlZoomPanel .olControlZoomInItemInactive{top:0;left:0;background-position:0 0}.olControlZoomPanel .olControlZoomToMaxExtentItemInactive{top:18px;left:0;background-position:0 -18px}.olControlZoomPanel .olControlZoomOutItemInactive{top:36px;left:0;background-position:0 18px}.olControlPanZoomBar div{font-size:1px}.olPopupCloseBox{background:url(../../img/gis/openlayers/theme_default/close.gif) no-repeat;cursor:pointer}.olFramedCloudPopupContent{padding:5px;overflow:auto}.olControlNoSelect{-moz-user-select:none;-khtml-user-select:none}.olImageLoadError{background-color:pink;opacity:0.5;filter:alpha(opacity=50)}.olCursorWait{cursor:wait}.olDragDown{cursor:move}.olDrawBox{cursor:crosshair}.olControlDragFeatureOver{cursor:move}.olControlDragFeatureActive.olControlDragFeatureOver.olDragDown{cursor:-moz-grabbing}.olControlLayerSwitcher{position:absolute;top:25px;right:0;width:20em;font-family:sans-serif;font-weight:bold;margin-top:3px;margin-left:3px;margin-bottom:3px;font-size:smaller;color:white;background-color:transparent}.olControlLayerSwitcher .layersDiv{padding-top:5px;padding-left:10px;padding-bottom:5px;padding-right:10px;background-color:darkblue}.olControlLayerSwitcher .layersDiv .baseLbl,.olControlLayerSwitcher .layersDiv .dataLbl{margin-top:3px;margin-left:3px;margin-bottom:3px}.olControlLayerSwitcher .layersDiv .baseLayersDiv,.olControlLayerSwitcher .layersDiv .dataLayersDiv{padding-left:10px}.olControlLayerSwitcher .maximizeDiv,.olControlLayerSwitcher .minimizeDiv{width:18px;height:18px;top:5px;right:0;cursor:pointer}.olBingAttribution{color:#DDD}.olBingAttribution.road{color:#333}.olGoogleAttribution.hybrid,.olGoogleAttribution.satellite{color:#EEE}.olGoogleAttribution{color:#333}span.olGoogleAttribution a{color:#77C}span.olGoogleAttribution.hybrid a,span.olGoogleAttribution.satellite a{color:#EEE}.olControlNavToolbar ,.olControlEditingToolbar{margin:5px 5px 0 0}.olControlNavToolbar div,.olControlEditingToolbar div{background-image:url(../../img/gis/openlayers/theme_default/editing_tool_bar.png);background-repeat:no-repeat;margin:0 0 5px 5px;width:24px;height:22px;cursor:pointer}.olControlEditingToolbar{right:0;top:0}.olControlNavToolbar{top:295px;left:9px}.olControlEditingToolbar div{float:right}.olControlNavToolbar .olControlNavigationItemInactive,.olControlEditingToolbar .olControlNavigationItemInactive{background-position:-103px -1px}.olControlNavToolbar .olControlNavigationItemActive ,.olControlEditingToolbar .olControlNavigationItemActive{background-position:-103px -24px}.olControlNavToolbar .olControlZoomBoxItemInactive{background-position:-128px -1px}.olControlNavToolbar .olControlZoomBoxItemActive{background-position:-128px -24px}.olControlEditingToolbar .olControlDrawFeaturePointItemInactive{background-position:-77px -1px}.olControlEditingToolbar .olControlDrawFeaturePointItemActive{background-position:-77px -24px}.olControlEditingToolbar .olControlDrawFeaturePathItemInactive{background-position:-51px -1px}.olControlEditingToolbar .olControlDrawFeaturePathItemActive{background-position:-51px -24px}.olControlEditingToolbar .olControlDrawFeaturePolygonItemInactive{background-position:-26px -1px}.olControlEditingToolbar .olControlDrawFeaturePolygonItemActive{background-position:-26px -24px}div.olControlZoom{position:absolute;top:8px;left:8px;background:rgba(255,255,255,0.4);border-radius:4px;padding:2px}div.olControlZoom a{display:block;margin:1px;padding:0;color:white;font-size:18px;font-family:'Lucida Grande',Verdana,Geneva,Lucida,Arial,Helvetica,sans-serif;font-weight:bold;text-decoration:none;text-align:center;height:22px;width:22px;line-height:19px;background:#130085;background:rgba(0,60,136,0.5);filter:alpha(opacity=80)}div.olControlZoom a:hover{background:#130085;background:rgba(0,60,136,0.7);filter:alpha(opacity=100)}@media only screen and (max-width:600px){div.olControlZoom a:hover{background:rgba(0,60,136,0.5)}}a.olControlZoomIn{border-radius:4px 4px 0 0}a.olControlZoomOut{border-radius:0 0 4px 4px}.olLayerGrid .olTileImage{-webkit-transition:opacity 0.2s linear;-moz-transition:opacity 0.2s linear;-o-transition:opacity 0.2s linear;transition:opacity 0.2s linear}.olTileImage{-webkit-transform:translateZ(0);-moz-transform:translateZ(0);-o-transform:translateZ(0);-ms-transform:translateZ(0);transform:translateZ(0);-webkit-backface-visibility:hidden;-moz-backface-visibility:hidden;-ms-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000;-moz-perspective:1000;-ms-perspective:1000;perspective:1000}.olTileReplacing{display:none}img.olTileImage{max-width:none}@font-face{font-family:'zocial';font-style:normal;font-weight:normal;src:url('../../fonts/zocial-regular-webfont.eot');src:url('../../fonts/zocial-regular-webfont.eot?#iefix') format('embedded-opentype'),url('../../fonts/zocial-regular-webfont.woff') format('woff'),url('../../fonts/zocial-regular-webfont.ttf') format('truetype'),url('../../fonts/zocial-regular-webfont.svg#ZocialRegular') format('svg')}.zocial{border-bottom-color:rgba(0,0,0,0.4);border:1px solid rgba(0,0,0,0.2);color:#fff !important;-moz-box-shadow:inset 0 0.08em 0 rgba(255,255,255,0.5),inset 0 0 0.1em rgba(255,255,255,0.9);-webkit-box-shadow:inset 0 0.08em 0 rgba(255,255,255,0.5),inset 0 0 0.1em rgba(255,255,255,0.9);box-shadow:inset 0 0.08em 0 rgba(255,255,255,0.5),inset 0 0 0.1em rgba(255,255,255,0.9);cursor:pointer;display:inline-block;font-family:"Lucida Grande",Tahoma,sans-serif;font-style:normal !important;font-weight:bold !important;letter-spacing:0;padding:0;position:relative;text-align:center;text-decoration:none !important;text-shadow:0 1px 0 rgba(0,0,0,0.5);-moz-user-select:none !important;-webkit-user-select:none !important;user-select:none !important}.zocial > span:before{border-right:0.075em solid rgba(0,0,0,0.1);-moz-box-shadow:0.075em 0 0 rgba(255,255,255,0.25);-webkit-box-shadow:0.075em 0 0 rgba(255,255,255,0.25);box-shadow:0.075em 0 0 rgba(255,255,255,0.25);content:"";display:block;float:left;font-family:"zocial" !important;font-size:125% !important;line-height:1.65;font-style:normal !important;font-weight:normal !important;margin:0.1em 0.5em 0 0;padding:0 0.5em;text-align:center !important;text-decoration:none !important;text-transform:none !important}.zocial > span{display:block;font-size:80% !important;line-height:2.1;font-weight:bold;padding:0em 1em 0 0;white-space:nowrap}.zocial,.zocial > span{-moz-border-radius:0.2em;-webkit-border-radius:0.2em;border-radius:0.2em;position:relative;z-index:100}.zocial:active{outline:none}.zocial.icon{overflow:hidden;width:1.85em;height:1.85em}.zocial.icon > span:before{padding:0;width:1.85em;height:1.85em}.zocial > span{background:-moz-linear-gradient(top,rgba(255,255,255,0.1),rgba(255,255,255,0.05) 49%,rgba(0,0,0,0.05) 51%,rgba(0,0,0,0.1));background:-webkit-gradient(linear,left top,left bottom,from(rgba(255,255,255,0.1)),color-stop(49%,rgba(255,255,255,0.05)),color-stop(51%,rgba(0,0,0,0.05)),to(rgba(0,0,0,0.1)));background:-webkit-linear-gradient(top,rgba(255,255,255,0.1),rgba(255,255,255,0.05) 49%,rgba(0,0,0,0.05) 51%,rgba(0,0,0,0.1));background:-o-linear-gradient(top,rgba(255,255,255,0.1),rgba(255,255,255,0.05) 49%,rgba(0,0,0,0.05) 51%,rgba(0,0,0,0.1));background:-ms-linear-gradient(top,rgba(255,255,255,0.1),rgba(255,255,255,0.05) 49%,rgba(0,0,0,0.05) 51%,rgba(0,0,0,0.1));background:linear-gradient(top,rgba(255,255,255,0.1),rgba(255,255,255,0.05) 49%,rgba(0,0,0,0.05) 51%,rgba(0,0,0,0.1))}.zocial:hover > span,.zocial:focus > span{background:-moz-linear-gradient(top,rgba(255,255,255,0.15),rgba(255,255,255,0.15) 49%,rgba(0,0,0,0.1) 51%,rgba(0,0,0,0.15));background:-webkit-gradient(linear,left top,left bottom,from(rgba(255,255,255,0.15)),color-stop(49%,rgba(255,255,255,0.15)),color-stop(51%,rgba(0,0,0,0.1)),to(rgba(0,0,0,0.15)));background:-webkit-linear-gradient(top,rgba(255,255,255,0.15),rgba(255,255,255,0.15) 49%,rgba(0,0,0,0.1) 51%,rgba(0,0,0,0.15));background:-o-linear-gradient(top,rgba(255,255,255,0.15),rgba(255,255,255,0.15) 49%,rgba(0,0,0,0.1) 51%,rgba(0,0,0,0.15));background:-ms-linear-gradient(top,rgba(255,255,255,0.15),rgba(255,255,255,0.15) 49%,rgba(0,0,0,0.1) 51%,rgba(0,0,0,0.15));background:linear-gradient(top,rgba(255,255,255,0.15),rgba(255,255,255,0.15) 49%,rgba(0,0,0,0.1) 51%,rgba(0,0,0,0.15))}.zocial:active > span{background:-moz-linear-gradient(bottom,rgba(255,255,255,0.1),rgba(255,255,255,0) 30%,rgba(0,0,0,0) 50%,rgba(0,0,0,0.1));background:-webkit-gradient(linear,left top,left bottom,from(rgba(255,255,255,0.1)),color-stop(30%,rgba(255,255,255,0)),color-stop(50%,rgba(0,0,0,0)),to(rgba(0,0,0,0.1)));background:-webkit-linear-gradient(bottom,rgba(255,255,255,0.1),rgba(255,255,255,0) 30%,rgba(0,0,0,0) 50%,rgba(0,0,0,0.1));background:-o-linear-gradient(bottom,rgba(255,255,255,0.1),rgba(255,255,255,0) 30%,rgba(0,0,0,0) 50%,rgba(0,0,0,0.1));background:-ms-linear-gradient(bottom,rgba(255,255,255,0.1),rgba(255,255,255,0) 30%,rgba(0,0,0,0) 50%,rgba(0,0,0,0.1));background:linear-gradient(bottom,rgba(255,255,255,0.1),rgba(255,255,255,0) 30%,rgba(0,0,0,0) 50%,rgba(0,0,0,0.1))}.zocial.bitcoin,.zocial.cloudapp,.zocial.dropbox,.zocial.email,.zocial.github,.zocial.gmail,.zocial.instapaper,.zocial.itunes,.zocial.ninetyninedesigns,.zocial.openid,.zocial.plancast,.zocial.posterous,.zocial.secondary,.zocial.viadeo,.zocial.weibo,.zocial.wikipedia{border:1px solid rgba(0,0,0,0.3);border-bottom-color:rgba(0,0,0,0.5);-moz-box-shadow:inset 0 0.08em 0 rgba(255,255,255,0.7),inset 0 0 0.08em rgba(255,255,255,0.5);-webkit-box-shadow:inset 0 0.08em 0 rgba(255,255,255,0.7),inset 0 0 0.08em rgba(255,255,255,0.5);box-shadow:inset 0 0.08em 0 rgba(255,255,255,0.7),inset 0 0 0.08em rgba(255,255,255,0.5);text-shadow:0 1px 0 rgba(255,255,255,0.8)}.zocial.bitcoin:focus > span,.zocial.bitcoin:hover > span,.zocial.dropbox:focus > span,.zocial.dropbox:hover > span,.zocial.email:focus > span,.zocial.email:hover > span,.zocial.github:focus > span,.zocial.github:hover > span,.zocial.gmail:focus > span,.zocial.gmail:hover > span,.zocial.instapaper:focus > span,.zocial.instapaper:hover > span,.zocial.itunes:focus > span,.zocial.itunes:hover > span,.zocial.ninetyninedesigns:focus > span,.zocial.ninetyninedesigns:hover > span,.zocial.openid:focus > span,.zocial.openid:hover > span,.zocial.plancast:focus > span,.zocial.plancast:hover > span,.zocial.posterous:focus > span,.zocial.posterous:hover > span,.zocial.secondary:focus > span,.zocial.secondary:hover > span,.zocial.twitter:focus > span,.zocial.viadeo:focus > span,.zocial.viadeo:hover > span,.zocial.weibo:focus > span,.zocial.weibo:hover > span,.zocial.wikipedia:focus > span,.zocial.wikipedia:hover > span{background:-webkit-gradient(linear,left top,left bottom,from(rgba(255,255,255,0.5)),color-stop(49%,rgba(255,255,255,0.2)),color-stop(51%,rgba(0,0,0,0.05)),to(rgba(0,0,0,0.15)));background:-moz-linear-gradient(top,rgba(255,255,255,0.5),rgba(255,255,255,0.2) 49%,rgba(0,0,0,0.05) 51%,rgba(0,0,0,0.15));background:-webkit-linear-gradient(top,rgba(255,255,255,0.5),rgba(255,255,255,0.2) 49%,rgba(0,0,0,0.05) 51%,rgba(0,0,0,0.15));background:-o-linear-gradient(top,rgba(255,255,255,0.5),rgba(255,255,255,0.2) 49%,rgba(0,0,0,0.05) 51%,rgba(0,0,0,0.15));background:-ms-linear-gradient(top,rgba(255,255,255,0.5),rgba(255,255,255,0.2) 49%,rgba(0,0,0,0.05) 51%,rgba(0,0,0,0.15));background:linear-gradient(top,rgba(255,255,255,0.5),rgba(255,255,255,0.2) 49%,rgba(0,0,0,0.05) 51%,rgba(0,0,0,0.15))}.zocial.bitcoin:active > span,.zocial.dropbox:active > span,.zocial.email:active > span,.zocial.github:active > span,.zocial.gmail:active > span,.zocial.instapaper:active > span,.zocial.itunes:active > span,.zocial.ninetyninedesigns:active > span,.zocial.openid:active > span,.zocial.plancast:active > span,.zocial.posterous:active > span,.zocial.secondary:active > span,.zocial.viadeo:active > span,.zocial.weibo:active > span,.zocial.wikipedia:active > span{background:-webkit-gradient(linear,left top,left bottom,from(rgba(255,255,255,0)),color-stop(30%,rgba(255,255,255,0)),color-stop(50%,rgba(0,0,0,0)),to(rgba(0,0,0,0.1)));background:-moz-linear-gradient(bottom,rgba(255,255,255,0),rgba(255,255,255,0) 30%,rgba(0,0,0,0) 50%,rgba(0,0,0,0.1));background:-webkit-linear-gradient(bottom,rgba(255,255,255,0),rgba(255,255,255,0) 30%,rgba(0,0,0,0) 50%,rgba(0,0,0,0.1));background:-o-linear-gradient(bottom,rgba(255,255,255,0),rgba(255,255,255,0) 30%,rgba(0,0,0,0) 50%,rgba(0,0,0,0.1));background:-ms-linear-gradient(bottom,rgba(255,255,255,0),rgba(255,255,255,0) 30%,rgba(0,0,0,0) 50%,rgba(0,0,0,0.1));background:linear-gradient(bottom,rgba(255,255,255,0),rgba(255,255,255,0) 30%,rgba(0,0,0,0) 50%,rgba(0,0,0,0.1))}.zocial.amazon > span:before{content:"a"}.zocial.android > span:before{content:"&"}.zocial.aol > span:before{content:"\""}.zocial.appstore > span:before{content:"A"}.zocial.bitcoin > span:before{content:"2";color:#f7931a !important}.zocial.blogger > span:before{content:"B"}.zocial.call > span:before{content:"7"}.zocial.chrome > span:before{content:"["}.zocial.cloudapp > span:before{content:"c"}.zocial.creativecommons > span:before{content:"C"}.zocial.disqus > span:before{content:"Q"}.zocial.dribbble > span:before{content:"D"}.zocial.dropbox > span:before{content:"d";color:#1f75cc !important}.zocial.email > span:before{content:"]";color:#312c2a !important}.zocial.eventasaurus > span:before{content:"v"}.zocial.eventbrite > span:before{content:"|"}.zocial.evernote > span:before{content:"E"}.zocial.facebook > span:before{content:"f"}.zocial.fivehundredpx > span:before{content:"0";color:#29b6ff !important}.zocial.flattr > span:before{content:"%"}.zocial.forrst > span:before{content:":";color:#50894f !important}.zocial.foursquare > span:before{content:"4"}.zocial.github > span:before{content:"g"}.zocial.gmail > span:before{content:"m";color:#f00 !important}.zocial.google > span:before{content:"G"}.zocial.googleplus > span:before{content:"+"}.zocial.gowalla > span:before{content:"@"}.zocial.grooveshark > span:before{content:"K"}.zocial.guest > span:before{content:"?"}.zocial.html5 > span:before{content:"5"}.zocial.ie > span:before{content:"6"}.zocial.instapaper > span:before{content:"I"}.zocial.intensedebate > span:before{content:"{"}.zocial.itunes > span:before{content:"i";color:#1a6dd2 !important}.zocial.lastfm > span:before{content:"l"}.zocial.linkedin > span:before{content:"L"}.zocial.macstore > span:before{content:"^"}.zocial.meetup > span:before{content:"M"}.zocial.myspace > span:before{content:"_"}.zocial.ninetyninedesigns > span:before{content:"9";color:#f50 !important}.zocial.openid > span:before{content:"o";color:#ff921d !important}.zocial.paypal > span:before{content:"$"}.zocial.pinboard > span:before{content:"n"}.zocial.pinterest > span:before{content:"1"}.zocial.plancast > span:before{content:"P"}.zocial.plurk > span:before{content:"j"}.zocial.podcast > span:before{content:"`"}.zocial.posterous > span:before{content:"~"}.zocial.quora > span:before{content:"q"}.zocial.rss > span:before{content:"R"}.zocial.scribd > span:before{content:"}";color:#00d5ea !important}.zocial.skype > span:before{content:"S"}.zocial.smashing > span:before{content:"*"}.zocial.songkick > span:before{content:"k"}.zocial.soundcloud > span:before{content:"s"}.zocial.spotify > span:before{content:"="}.zocial.stumbleupon > span:before{content:"/"}.zocial.tumblr > span:before{content:"t"}.zocial.twitter > span:before{content:"T"}.zocial.viadeo > span:before{content:"H";color:#f59b20 !important}.zocial.vimeo > span:before{content:"V"}.zocial.weibo > span:before{content:"J";color:#e6162d !important}.zocial.wikipedia > span:before{content:","}.zocial.windows > span:before{content:"W"}.zocial.wordpress > span:before{content:"w"}.zocial.yahoo > span:before{content:"Y"}.zocial.yelp > span:before{content:"y"}.zocial.youtube > span:before{content:"U"}.zocial.amazon{background:#ffad1d;color:#030037 !important;text-shadow:0 1px 0 rgba(255,255,255,0.5)}.zocial.android{background:#a4c639}.zocial.aol{background:#f00}.zocial.appstore{background:#000}.zocial.bitcoin{background:#efefef;color:#4d4d4d !important}.zocial.blogger{background:#ee5a22}.zocial.call{background:#008000}.zocial.chrome{background:#006cd4}.zocial.cloudapp{background:#fff;color:#312c2a !important}.zocial.creativecommons{background:#000}.zocial.disqus{background:#5d8aad}.zocial.dribbble{background:#ea4c89}.zocial.dropbox{background:#fff;color:#312c2a !important}.zocial.email{background:#f0f0eb;color:#312c2a !important}.zocial.eventasaurus{background:#8ccc33}.zocial.eventbrite{background:#ff5616}.zocial.evernote{background:#6bb130;color:#fff !important}.zocial.facebook{background:#4863ae}.zocial.fivehundredpx{background:#333}.zocial.flattr{background:#8aba42}.zocial.forrst{background:#1e360d}.zocial.foursquare{background:#44a8e0}.zocial.github{background:#fbfbfb;color:#050505 !important}.zocial.gmail{background:#efefef;color:#222 !important}.zocial.google{background:#4e6cf7}.zocial.googleplus{background:#dd4b39}.zocial.gowalla{background:#ff720a}.zocial.grooveshark{background:#111;color:#eee !important}.zocial.guest{background:#1b4d6d}.zocial.html5{background:#ff3617}.zocial.ie{background:#00a1d9}.zocial.instapaper{background:#eee;color:#222 !important}.zocial.intensedebate{background:#0099e1}.zocial.itunes{background:#efefeb;color:#312c2a !important}.zocial.lastfm{background:#dc1a23}.zocial.linkedin{background:#0083a8}.zocial.macstore{background:#007dcb}.zocial.meetup{background:#ff0026}.zocial.myspace{background:#000}.zocial.ninetyninedesigns{background:#fff;color:#072243 !important}.zocial.openid{background:#f5f5f5;color:#333 !important}.zocial.paypal{background:#ff921d;color:#032751 !important;text-shadow:0 1px 0 rgba(255,255,255,0.5)}.zocial.pinboard{background:blue}.zocial.pinterest{background:#c91618}.zocial.plancast{background:#e7ebed;color:#333 !important}.zocial.plurk{background:#cf682f}.zocial.podcast{background:#9365ce}.zocial.posterous{background:#ffd959;color:#bc7134 !important}.zocial.quora{background:#a82400}.zocial.rss{background:#ff7f25}.zocial.scribd{background:#231c1a}.zocial.skype{background:#00a2ed}.zocial.smashing{background:#ff4f27}.zocial.songkick{background:#ff0050}.zocial.soundcloud{background:#ff4500}.zocial.spotify{background:#60af00}.zocial.stumbleupon{background:#eb4924}.zocial.tumblr{background:#374a61}.zocial.twitter{background:#46c0fb}.zocial.viadeo{background:#fff;color:#000 !important}.zocial.vimeo{background:#00a2cd}.zocial.weibo{background:#faf6f1;color:#000 !important}.zocial.wikipedia{background:#fff;color:#000 !important}.zocial.windows{background:#0052a4;color:#FFF !important}.zocial.wordpress{background:#464646}.zocial.yahoo{background:#a200c2}.zocial.yelp{background:#e60010}.zocial.youtube{background:#f00}.zocial.primary > span,.zocial.secondary > span{margin:0.1em 0;padding:0 1em}.zocial.primary > span:before,.zocial.secondary > span:before{display:none}.zocial.primary{background:#333}.zocial.secondary{background:#f0f0eb;color:#222 !important;text-shadow:0 1px 0 rgba(255,255,255,0.8)}.zocial.humanitarianid > span:before{content:url(../../img/humanitarianid.png);height:24px;padding-top:3px}.zocial.humanitarianid{background:##e2e2e2;color:#2a5d81 !important}.zocial.humanitarianid{border:1px solid rgba(0,0,0,0.3);border-bottom-color:rgba(0,0,0,0.5);-moz-box-shadow:inset 0 0.08em 0 rgba(255,255,255,0.7),inset 0 0 0.08em rgba(255,255,255,0.5);-webkit-box-shadow:inset 0 0.08em 0 rgba(255,255,255,0.7),inset 0 0 0.08em rgba(255,255,255,0.5);box-shadow:inset 0 0.08em 0 rgba(255,255,255,0.7),inset 0 0 0.08em rgba(255,255,255,0.5);text-shadow:0 1px 0 rgba(255,255,255,0.8)}.zocial.humanitarianid:hover > span,.zocial.humanitarianid:focus > span{background:-webkit-gradient(linear,left top,left bottom,from(rgba(255,255,255,0.5)),color-stop(49%,rgba(255,255,255,0.2)),color-stop(51%,rgba(0,0,0,0.05)),to(rgba(0,0,0,0.15)));background:-moz-linear-gradient(top,rgba(255,255,255,0.5),rgba(255,255,255,0.2) 49%,rgba(0,0,0,0.05) 51%,rgba(0,0,0,0.15));background:-webkit-linear-gradient(top,rgba(255,255,255,0.5),rgba(255,255,255,0.2) 49%,rgba(0,0,0,0.05) 51%,rgba(0,0,0,0.15));background:-o-linear-gradient(top,rgba(255,255,255,0.5),rgba(255,255,255,0.2) 49%,rgba(0,0,0,0.05) 51%,rgba(0,0,0,0.15));background:-ms-linear-gradient(top,rgba(255,255,255,0.5),rgba(255,255,255,0.2) 49%,rgba(0,0,0,0.05) 51%,rgba(0,0,0,0.15));background:linear-gradient(top,rgba(255,255,255,0.5),rgba(255,255,255,0.2) 49%,rgba(0,0,0,0.05) 51%,rgba(0,0,0,0.15))}.zocial.humanitarianid:active > span{background:-webkit-gradient(linear,left top,left bottom,from(rgba(255,255,255,0)),color-stop(30%,rgba(255,255,255,0)),color-stop(50%,rgba(0,0,0,0)),to(rgba(0,0,0,0.1)));background:-moz-linear-gradient(bottom,rgba(255,255,255,0),rgba(255,255,255,0) 30%,rgba(0,0,0,0) 50%,rgba(0,0,0,0.1));background:-webkit-linear-gradient(bottom,rgba(255,255,255,0),rgba(255,255,255,0) 30%,rgba(0,0,0,0) 50%,rgba(0,0,0,0.1));background:-o-linear-gradient(bottom,rgba(255,255,255,0),rgba(255,255,255,0) 30%,rgba(0,0,0,0) 50%,rgba(0,0,0,0.1));background:-ms-linear-gradient(bottom,rgba(255,255,255,0),rgba(255,255,255,0) 30%,rgba(0,0,0,0) 50%,rgba(0,0,0,0.1));background:linear-gradient(bottom,rgba(255,255,255,0),rgba(255,255,255,0) 30%,rgba(0,0,0,0) 50%,rgba(0,0,0,0.1))}button::-moz-focus-inner{border:0;padding:0}.embeddedComponent .map_wrapper{min-width:350px}.map_wrapper{position:relative;overflow:hidden;clear:right}.map_wrapper.fullscreen{overflow:visible}.gis_west .x-panel-body{overflow-y:auto}.map_loader{margin-top:50px;margin-left:auto;margin-right:auto}form .map_loader{margin-top:0}.x-form-item-label{margin:0 0 0 4px}body.x-window-maximized-ct{width:100%}.form-container form tr.x-toolbar-left-row td{padding:0}.map_home{margin:0 0 0 -12px}.map_home .gis_fullscreen_map-btn{font-weight:bold;padding-left:8px}body.rtl .map_home .gis_fullscreen_map-btn{float:left}.gis_print_map-btn{font-weight:bold;float:right;padding-right:8px}.notitle .ui-dialog-titlebar{background-image:none !important;border:0;padding:0}.notitle .ui-dialog-title{margin:0;padding:0}.notitle .ui-dialog-titlebar-close{margin-top:-10px}.gis-map-window.x-resizable-pinned .x-window-tl{height:0}.form-container form button.gis_loc_select_btn{padding:4px}.form-container form button.gis_loc_select_btn i.icon-map{padding-right:2px}.gis-display-feature i.icon-map-marker{cursor:pointer;cursor:hand;padding-left:5px}.embeddedComponent .map_wrapper{width:100%}.gis_coord_wrap .decimal{width:174px}.gis_coord_wrap .gis_coord_dms input{width:37px}.gis_coord_wrap .gis_coord_dms input.seconds{width:70px}.gis_coord_wrap div{padding-top:8px}.x-tree-elbow,.x-tree-elbow-end,.x-tree-node-icon{display:none}.x-tree-node-anchor{padding-left:5px;padding-right:5px}.x-tree-node{font-size:12px}.x-tree-node-leaf{margin-left:10px}.x-tree-root-ct,.x-tree-node-ct{margin:0}.map_legend_div{position:absolute;bottom:0;right:0}.map_wrapper.fullscreen .map_legend_div{z-index:9100}.map_wrapper.a4 .map_legend_div{margin-bottom:-440px}.map_wrapper.a3 .map_legend_div{margin-bottom:-687px;right:-140px}.map_wrapper.a2 .map_legend_div{margin-bottom:-1036px;right:-530px}.map_wrapper.a1 .map_legend_div{margin-bottom:-1529px;right:-2130px}.map_wrapper.a0 .map_legend_div{margin-bottom:-2229px;right:-3125px}.map_legend_panel{background-color:#fbfbfb;border:solid;border-radius:5px 0 0 5px;padding:1px;margin:0 0 20px;width:auto;max-width:800px;max-height:500px;overflow-y:auto;border-right:none}.map_legend_panel .x-panel-header-noborder{border:none}.map_legend_tab{background-color:#fbfbfb;border:solid;border-width:2px;margin-top:5px;font-family:FontAwesome;font-weight:normal;font-style:normal;text-decoration:inherit;-webkit-font-smoothing:antialiased;float:left;border-radius:3px 0 3px 3px;border-right:none;border-right-width:0;margin-right:-6px;margin-left:-12px;padding-left:2px;padding-right:3px}.map_legend_tab:before{text-decoration:inherit;display:inline-block;speak:none}.map_legend_tab.left:before{content:"\f100"}.map_legend_tab.right:before{content:"\f101"}.map_wrapper.print .map_legend_tab{height:0}.map_wrapper.print .map_legend_tab.right:before,.map_wrapper.print .map_legend_tab.left:before{content:normal}.gis_legend_title{font-weight:bold;margin-top:10px}.gis_legend_desc{max-width:200px}.layer_throbber.float{position:absolute;top:10px;right:10px}.layer_throbber.save{top:65px}.map_polygon_panel{background-color:#fbfbfb;position:absolute;top:10px;height:125px;width:350px;margin-left:-175px;left:50%;border:solid;border-radius:2px;border-width:1px;font-size:small;text-align:center;padding:12px}.map_polygon_buttons{font-size:0.75rem;margin-top:12px;text-transform:uppercase}.button.map_polygon_finish{margin-right:12px}.map_save_panel{background-color:#fbfbfb;position:absolute;top:0;right:0;margin:10px 0;padding:5px;border:solid;border-radius:5px 0 0 5px;width:auto}.map_wrapper.fullscreen .map_save_panel{z-index:9100;margin-top:-100px}.map_wrapper.print .map_save_button{height:0;width:0;visibility:hidden}.map_save_panel.off{visibility:hidden}.map_save_button{background-color:#fbfbfb;padding:5px;border:solid;border-radius:5px 5px 5px 5px;width:auto;cursor:pointer;visibility:visible;float:right}.map_save_name{font-weight:bold;text-align:center;padding:5px;margin-top:2px}.map_save_panel input{width:150px;margin-top:1px;margin-right:5px}.map_save_panel input.checkbox{width:10px;margin:0 5px 0 0}.map_save_panel .new_map{font-size:small}.map_save_panel .saved{float:left}.map_save_panel p{float:left;padding:5px;color:green;margin:0}#config-gis_config_pe_id-options-filter__row .s3-groupedopts-option{display:none}#config-gis_config_pe_id-options-filter__row .s3-groupedopts-widget td:first-child{border-right:solid 1px;padding-right:10px}.olControlMousePosition{font-size:10px;background-color:white}.crosshair{cursor:crosshair}.olLayerGoogleCopyright{right:3px;bottom:2px;left:auto}.olLayerGooglePoweredBy{left:2px;bottom:2px}.olForeignContainer div.olControlMousePosition{bottom:28px}.gis_tooltip{opacity:0.7 !important}.gis_tooltip_content{overflow:hidden;padding:3px;margin:10px}.olPopup #plain{max-width:450px}.olPopupCloseBox{margin-right:15px;margin-top:-8px}.olFramedCloudPopupContent label{padding-right:5px}.gis-map-window .olFramedCloudPopupContent td{padding:2px}.gis_popup_row{display:table-row}.gis_popup_label{display:table-cell;font-weight:bold;text-align:right;padding-right:2px}.gis_popup_val{display:table-cell}#georsspopup h2,#kmlpopup h2{margin:0}.gx-popup-anc{background:transparent url(../../img/gis/geoext/anchor.png) no-repeat 0 0;position:relative;top:-1px;left:5px;z-index:2;height:16px;width:31px}.gx-ruledrag-insert-below{border-bottom:1px dotted}.gx-ruledrag-insert-above{border-top:1px dotted}.mappnlcntr .zoomfull{background-image:url(../../img/gis/mapfish/icon_zoomfull.png) !important;height:20px !important;width:20px !important}.mappnlcntr .zoomin{background-image:url(../../img/gis/mapfish/icon_zoomin.png) !important;height:20px !important;width:20px !important}.mappnlcntr .zoomout{background-image:url(../../img/gis/mapfish/icon_zoomout.png) !important;height:20px !important;width:20px !important}.mappnlcntr .pan-off{background-image:url(../../img/gis/mapfish/icon_pan.png) !important;height:20px !important;width:20px !important}.mappnlcntr .measure-off{background-image:url(../../img/gis/measuring-stick-off.png) !important;height:20px !important;width:20px !important}.mappnlcntr .measure-area{background-image:url(../../img/gis/measure-area-off.png) !important;height:20px !important;width:20px !important}.mappnlcntr .modifyfeature{background-image:url(../../img/gis/mapfish/move_vertex_off.png) !important;height:20px !important;width:20px !important}.mappnlcntr .drawpoint-off{background-image:url(../../img/gis/add_point_off.png) !important;height:20px !important;width:20px !important}.mappnlcntr .drawline-off{background-image:url(../../img/gis/mapfish/draw_line_off.png) !important;height:20px !important;width:20px !important}.mappnlcntr .drawpolygon-off{background-image:url(../../img/gis/mapfish/draw_polygon_off.png) !important;height:20px !important;width:20px !important}.mappnlcntr .drawpolygonclear-off{background-image:url(../../img/gis/mapfish/draw_polygon_clear_off.png) !important;height:20px !important;width:20px !important}.mappnlcntr .drawcircle-on{background-image:url(../../img/draw_circle_on.png) !important;height:20px !important;width:20px !important}.mappnlcntr .info{background-image:url(../../img/gis/mapfish/info.png) !important;height:20px !important;width:20px !important}.mappnlcntr .searchclick{background-image:url(../../img/ext/information.png) !important;height:20px !important;width:20px !important}.mappnlcntr .searchbox{background-image:url(../../img/ext/information-box.png) !important;height:20px !important;width:20px !important}.mappnlcntr .back{background-image:url(../../img/gis/mapfish/resultset_previous.png) !important;height:20px !important;width:20px !important}.mappnlcntr .next{background-image:url(../../img/gis/mapfish/resultset_next.png) !important;height:20px !important;width:20px !important}.mappnlcntr .print{background-image:url(../../img/silk/printer.png) !important;height:20px !important;width:20px !important}.mappnlcntr .save{background-image:url(../../img/ext/save.gif) !important;height:20px !important;width:20px !important}.x-btn-text.geolocation{background-image:url(../../img/gis/geolocation.png) !important;height:20px !important;width:20px !important}.x-btn-text.potlatch{background-image:url(../../img/gis/openstreetmap.png) !important;height:20px !important;width:20px !important}.x-btn-text.streetview{background-image:url(../../img/gis/streetview.png) !important;height:20px !important;width:20px !important}.gxp-icon-addlayers{background-image:url(../../img/silk/add.png) !important}.gxp-icon-addserver{background-image:url(../../img/silk/server_add.png) !important}.gxp-icon-getfeatureinfo{background-image:url(../../img/silk/information.png) !important}.gxp-icon-removelayers{background-image:url(../../img/silk/delete.png) !important}.gxp-icon-layerproperties{background-image:url(../../img/silk/wrench.png) !important}.icon-clearlayers{background-image:url(../../img/silk/eye.png)}.mappnlcntr .movefeature{background-image:url(../../img/gis/arrow_refresh.png) !important;height:20px !important;width:20px !important}.mappnlcntr .removefeature{background-image:url(../../img/gis/remove_point_off.png) !important;height:20px !important;width:20px !important}.mappnlcntr .resizefeature{background-image:url(../../img/gis/resize.png) !important;height:20px !important;width:20px !important}.mappnlcntr .rotatefeature{background-image:url(../../img/gis/arrow_rotate_clockwise.png) !important;height:20px !important;width:20px !important}.gis-map-window table,.map_wrapper table{background:none;border:none;margin-bottom:0}.gis-map-window table tr:nth-of-type(2n),.map_wrapper table tr:nth-of-type(2n){background-color:inherit}.gis-map-window table tr th,.gis-map-window table tr td,.map_wrapper table tr th,.map_wrapper table tr td{color:inherit;font-size:inherit;padding:0}.gis-map-window input[type="text"],.gis-map-window input[type="checkbox"],.gis-map-window input[type="radio"],.map_wrapper input[type="text"],.map_wrapper input[type="checkbox"],.map_wrapper input[type="radio"]{margin:0;padding:0}.x-form-element input[type="text"]{display:inline;font-size:inherit;margin:0;padding:0}.map_legend_tab.right{float:left !important}#contents .map_wrapper a:not(.action-btn):not(.delete-btn){text-decoration:none}@charset "UTF-8";@media all and (orientation:portrait){}@media all and (orientation:landscape){}@media screen and (max-device-width:480px){}@media handheld{*{float:none;font-size:80%;background:#fff;color:#000}}@charset "UTF-8";@media print{body{ background:transparent;color:black;font-family:"Georgia",Times New Roman,Serif;font-size:12pt} #menu_modules,#menu_options,#footer,#rheader_tabs,#searchCombo{display:none} #content{background-color:transparent;width:100%;float:none !important;border:0;border-radius:0;margin:0;padding:0} #content h1,#content h2{background:white;color:black;font-size:16pt;border:0;border-radius:0;margin:0} #content h3{background:white;color:black;font-size:14pt;margin:0} a{color:black;background:transparent;text-decoration:underline} #comments{page-break-before:always} *{background:transparent !important;color:#444 !important;text-shadow:none !important} a,a:visited{color:#444 !important;text-decoration:underline} a:after{content:" (" attr(href) ")"} abbr:after{content:" (" attr(title) ")"} .ir a:after{content:""} pre,blockquote{border:1px solid #999;page-break-inside:avoid} thead{display:table-header-group} tr,img{page-break-inside:avoid} @page{margin:0.5cm} p,h2,h3{orphans:3;widows:3} h2,h3{page-break-after:avoid}}.nvd3 .nv-axis{pointer-events:none;opacity:1}.nvd3 .nv-axis path{fill:none;stroke:#000;stroke-opacity:.75;shape-rendering:crispEdges}.nvd3 .nv-axis path.domain{stroke-opacity:.75}.nvd3 .nv-axis.nv-x path.domain{stroke-opacity:0}.nvd3 .nv-axis line{fill:none;stroke:#e5e5e5;shape-rendering:crispEdges}.nvd3 .nv-axis .zero line,.nvd3 .nv-axis line.zero{stroke-opacity:.75}.nvd3 .nv-axis .nv-axisMaxMin text{font-weight:bold}.nvd3 .x .nv-axis .nv-axisMaxMin text,.nvd3 .x2 .nv-axis .nv-axisMaxMin text,.nvd3 .x3 .nv-axis .nv-axisMaxMin text{text-anchor:middle}.nvd3 .nv-axis.nv-disabled{opacity:0}.nvd3 .nv-bars rect{fill-opacity:.75;transition:fill-opacity 250ms linear}.nvd3 .nv-bars rect.hover{fill-opacity:1}.nvd3 .nv-bars .hover rect{fill:lightblue}.nvd3 .nv-bars text{fill:rgba(0,0,0,0)}.nvd3 .nv-bars .hover text{fill:rgba(0,0,0,1)}.nvd3 .nv-multibar .nv-groups rect,.nvd3 .nv-multibarHorizontal .nv-groups rect,.nvd3 .nv-discretebar .nv-groups rect{stroke-opacity:0;transition:fill-opacity 250ms linear}.nvd3 .nv-multibar .nv-groups rect:hover,.nvd3 .nv-multibarHorizontal .nv-groups rect:hover,.nvd3 .nv-candlestickBar .nv-ticks rect:hover,.nvd3 .nv-discretebar .nv-groups rect:hover{fill-opacity:1}.nvd3 .nv-discretebar .nv-groups text,.nvd3 .nv-multibarHorizontal .nv-groups text{font-weight:bold;fill:rgba(0,0,0,1);stroke:rgba(0,0,0,0)}.nvd3 .nv-boxplot circle{fill-opacity:0.5}.nvd3 .nv-boxplot circle:hover{fill-opacity:1}.nvd3 .nv-boxplot rect:hover{fill-opacity:1}.nvd3 line.nv-boxplot-median{stroke:black}.nv-boxplot-tick:hover{stroke-width:2.5px}.nvd3.nv-bullet{font:10px sans-serif}.nvd3.nv-bullet .nv-measure{fill-opacity:.8}.nvd3.nv-bullet .nv-measure:hover{fill-opacity:1}.nvd3.nv-bullet .nv-marker{stroke:#000;stroke-width:2px}.nvd3.nv-bullet .nv-markerTriangle{stroke:#000;fill:#fff;stroke-width:1.5px}.nvd3.nv-bullet .nv-markerLine{stroke:#000;stroke-width:1.5px}.nvd3.nv-bullet .nv-tick line{stroke:#666;stroke-width:.5px}.nvd3.nv-bullet .nv-range.nv-s0{fill:#eee}.nvd3.nv-bullet .nv-range.nv-s1{fill:#ddd}.nvd3.nv-bullet .nv-range.nv-s2{fill:#ccc}.nvd3.nv-bullet .nv-title{font-size:14px;font-weight:bold}.nvd3.nv-bullet .nv-subtitle{fill:#999}.nvd3.nv-bullet .nv-range{fill:#bababa;fill-opacity:.4}.nvd3.nv-bullet .nv-range:hover{fill-opacity:.7}.nvd3.nv-candlestickBar .nv-ticks .nv-tick{stroke-width:1px}.nvd3.nv-candlestickBar .nv-ticks .nv-tick.hover{stroke-width:2px}.nvd3.nv-candlestickBar .nv-ticks .nv-tick.positive rect{stroke:#2ca02c;fill:#2ca02c}.nvd3.nv-candlestickBar .nv-ticks .nv-tick.negative rect{stroke:#d62728;fill:#d62728}.with-transitions .nv-candlestickBar .nv-ticks .nv-tick{transition:stroke-width 250ms linear,stroke-opacity 250ms linear}.nvd3.nv-candlestickBar .nv-ticks line{stroke:#333}.nv-force-node{stroke:#fff;stroke-width:1.5px}.nv-force-link{stroke:#999;stroke-opacity:.6}.nv-force-node text{stroke-width:0px}.nvd3 .nv-legend .nv-disabled rect{}.nvd3 .nv-check-box .nv-box{fill-opacity:0;stroke-width:2}.nvd3 .nv-check-box .nv-check{fill-opacity:0;stroke-width:4}.nvd3 .nv-series.nv-disabled .nv-check-box .nv-check{fill-opacity:0;stroke-opacity:0}.nvd3 .nv-controlsWrap .nv-legend .nv-check-box .nv-check{opacity:0}.nvd3.nv-linePlusBar .nv-bar rect{fill-opacity:.75}.nvd3.nv-linePlusBar .nv-bar rect:hover{fill-opacity:1}.nvd3 .nv-groups path.nv-line{fill:none}.nvd3 .nv-groups path.nv-area{stroke:none}.nvd3.nv-line .nvd3.nv-scatter .nv-groups .nv-point{fill-opacity:0;stroke-opacity:0}.nvd3.nv-scatter.nv-single-point .nv-groups .nv-point{fill-opacity:.5 !important;stroke-opacity:.5 !important}.with-transitions .nvd3 .nv-groups .nv-point{transition:stroke-width 250ms linear,stroke-opacity 250ms linear}.nvd3.nv-scatter .nv-groups .nv-point.hover,.nvd3 .nv-groups .nv-point.hover{stroke-width:7px;fill-opacity:.95 !important;stroke-opacity:.95 !important}.nvd3 .nv-point-paths path{stroke:#aaa;stroke-opacity:0;fill:#eee;fill-opacity:0}.nvd3 .nv-indexLine{cursor:ew-resize}svg.nvd3-svg{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;display:block;width:100%;height:100%}.nvtooltip.with-3d-shadow,.with-3d-shadow .nvtooltip{box-shadow:0 5px 10px rgba(0,0,0,.2);border-radius:5px}.nvd3 text{font:normal 12px Arial,sans-serif}.nvd3 .title{font:bold 14px Arial,sans-serif}.nvd3 .nv-background{fill:white;fill-opacity:0}.nvd3.nv-noData{font-size:18px;font-weight:bold}.nv-brush .extent{fill-opacity:.125;shape-rendering:crispEdges}.nv-brush .resize path{fill:#eee;stroke:#666}.nvd3 .nv-legend .nv-series{cursor:pointer}.nvd3 .nv-legend .nv-disabled circle{fill-opacity:0}.nvd3 .nv-brush .extent{fill-opacity:0 !important}.nvd3 .nv-brushBackground rect{stroke:#000;stroke-width:.4;fill:#fff;fill-opacity:.7}@media print{.nvd3 text{stroke-width:0;fill-opacity:1}}.nvd3.nv-ohlcBar .nv-ticks .nv-tick{stroke-width:1px}.nvd3.nv-ohlcBar .nv-ticks .nv-tick.hover{stroke-width:2px}.nvd3.nv-ohlcBar .nv-ticks .nv-tick.positive{stroke:#2ca02c}.nvd3.nv-ohlcBar .nv-ticks .nv-tick.negative{stroke:#d62728}.nvd3 .background path{fill:none;stroke:#EEE;stroke-opacity:.4;shape-rendering:crispEdges}.nvd3 .foreground path{fill:none;stroke-opacity:.7}.nvd3 .nv-parallelCoordinates-brush .extent{fill:#fff;fill-opacity:.6;stroke:gray;shape-rendering:crispEdges}.nvd3 .nv-parallelCoordinates .hover{fill-opacity:1;stroke-width:3px}.nvd3 .missingValuesline line{fill:none;stroke:black;stroke-width:1;stroke-opacity:1;stroke-dasharray:5,5}.nvd3.nv-pie path{stroke-opacity:0;transition:fill-opacity 250ms linear,stroke-width 250ms linear,stroke-opacity 250ms linear}.nvd3.nv-pie .nv-pie-title{font-size:24px;fill:rgba(19,196,249,0.59)}.nvd3.nv-pie .nv-slice text{stroke:#000;stroke-width:0}.nvd3.nv-pie path{stroke:#fff;stroke-width:1px;stroke-opacity:1}.nvd3.nv-pie path{fill-opacity:.7}.nvd3.nv-pie .hover path{fill-opacity:1}.nvd3.nv-pie .nv-label{pointer-events:none}.nvd3.nv-pie .nv-label rect{fill-opacity:0;stroke-opacity:0}.nvd3 .nv-groups .nv-point.hover{stroke-width:20px;stroke-opacity:.5}.nvd3 .nv-scatter .nv-point.hover{fill-opacity:1}.nv-noninteractive{pointer-events:none}.nv-distx,.nv-disty{pointer-events:none}.nvd3.nv-sparkline path{fill:none}.nvd3.nv-sparklineplus g.nv-hoverValue{pointer-events:none}.nvd3.nv-sparklineplus .nv-hoverValue line{stroke:#333;stroke-width:1.5px}.nvd3.nv-sparklineplus,.nvd3.nv-sparklineplus g{pointer-events:all}.nvd3 .nv-hoverArea{fill-opacity:0;stroke-opacity:0}.nvd3.nv-sparklineplus .nv-xValue,.nvd3.nv-sparklineplus .nv-yValue{stroke-width:0;font-size:.9em;font-weight:normal}.nvd3.nv-sparklineplus .nv-yValue{stroke:#f66}.nvd3.nv-sparklineplus .nv-maxValue{stroke:#2ca02c;fill:#2ca02c}.nvd3.nv-sparklineplus .nv-minValue{stroke:#d62728;fill:#d62728}.nvd3.nv-sparklineplus .nv-currentValue{font-weight:bold;font-size:1.1em}.nvd3.nv-stackedarea path.nv-area{fill-opacity:.7;stroke-opacity:0;transition:fill-opacity 250ms linear,stroke-opacity 250ms linear}.nvd3.nv-stackedarea path.nv-area.hover{fill-opacity:.9}.nvd3.nv-stackedarea .nv-groups .nv-point{stroke-opacity:0;fill-opacity:0}.nvtooltip{position:absolute;background-color:rgba(255,255,255,1.0);color:rgba(0,0,0,1.0);padding:1px;border:1px solid rgba(0,0,0,.2);z-index:10000;display:block;font-family:Arial,sans-serif;font-size:13px;text-align:left;pointer-events:none;white-space:nowrap;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.nvtooltip{background:rgba(255,255,255,0.8);border:1px solid rgba(0,0,0,0.5);border-radius:4px}.nvtooltip.with-transitions,.with-transitions .nvtooltip{transition:opacity 50ms linear;transition-delay:200ms}.nvtooltip.x-nvtooltip,.nvtooltip.y-nvtooltip{padding:8px}.nvtooltip h3{margin:0;padding:4px 14px;line-height:18px;font-weight:normal;background-color:rgba(247,247,247,0.75);color:rgba(0,0,0,1.0);text-align:center;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.nvtooltip p{margin:0;padding:5px 14px;text-align:center}.nvtooltip span{display:inline-block;margin:2px 0}.nvtooltip table{margin:6px;border-spacing:0}.nvtooltip table td{padding:2px 9px 2px 0;vertical-align:middle}.nvtooltip table td.key{font-weight:normal}.nvtooltip table td.key.total{font-weight:bold}.nvtooltip table td.value{text-align:right;font-weight:bold}.nvtooltip table td.percent{color:darkgray}.nvtooltip table tr.highlight td{padding:1px 9px 1px 0;border-bottom-style:solid;border-bottom-width:1px;border-top-style:solid;border-top-width:1px}.nvtooltip table td.legend-color-guide div{width:8px;height:8px;vertical-align:middle}.nvtooltip table td.legend-color-guide div{width:12px;height:12px;border:1px solid #999}.nvtooltip .footer{padding:3px;text-align:center}.nvtooltip-pending-removal{pointer-events:none;display:none}.nvd3 .nv-interactiveGuideLine{pointer-events:none}.nvd3 line.nv-guideline{stroke:#ccc}.tp-form fieldset legend button:first-child,.pt-form fieldset legend button:first-child{display:none}.pt-form .pt-rows{margin-right:0.5rem}.pt-form .pt-cols{margin-left:0.5rem}.pt-form .pt-axis-options select,.pt-form .pt-axis-options label{display:inline-block;width:auto}.tp-chart-tops,.pt-chart-opts{height:1.0rem}.tp-chart-icon,.pt-chart-icon{width:16px;height:16px;float:left;margin-right:5px;cursor:pointer}.tp-chart-label,.pt-chart-label{font-size:0.7rem;float:left;margin-right:8px}.tp-lchart{background:url(../../img/report/lchart.png) center no-repeat}.tp-bchart{background:url(../../img/report/vchart.png) center no-repeat}.pt-pchart{background:url(../../img/report/pchart.png) center no-repeat}.pt-vchart{background:url(../../img/report/vchart.png) center no-repeat}.pt-hchart{background:url(../../img/report/hchart.png) center no-repeat}.pt-schart{background:url(../../img/report/pchart.png) center no-repeat}.tp-chart-contents,.pt-chart-contents{background-color:#fffdf6;padding:8px;border:1px solid #999;margin-top:5px;margin-bottom:5px;position:relative}.pt-chart-contents .pt-chart-title{position:absolute;left:8;top:0}.pt-chart-contents .pt-chart-title h4{font-size:1.0rem;font-weight:normal}.pt-chart-contents .pt-hide-chart{cursor:pointer;min-height:16px;min-width:16px;background:url(../../img/cross.png) right top no-repeat}.pt-chart-contents .pt-chart{margin-top:20px}.pt-chart-contents .pt-spectrum-pie{height:140px}.pt-chart-contents .pt-spectrum-pie svg.nv{float:left;width:auto}.pt-chart-contents .pt-spectrum-bar{height:280px;clear:left}.pt-chart-contents .pt-spectrum-form{float:left}.pt-chart-contents .pt-spectrum-form label{font-weight:bold;margin-right:8px;display:block}.pt-chart-contents .pt-spectrum-form select{font-size:0.875rem;max-width:360px}.pt-tooltip{font-family:Arial;font-size:13px;text-align:center;padding:4px}.pt-tooltip .pt-tooltip-label{font-weight:bold}.pt-tooltip .pt-tooltip-label,.pt-tooltip .pt-tooltip-text{max-width:175px;white-space:normal}.pt-table-contents{min-height:16px}.pt-table-contents .pt-table{overflow:auto}.pt-table-contents .pt-table th{border-bottom:0}.pt-table-contents .pt-table tr.pt-totals-row th.pt-totals-header{border-bottom:1px solid #ccc;border-top:2px solid #ccc}.pt-table-contents .pt-table .pt-totals-header,.pt-table-contents .pt-table .pt-total,.pt-table-contents .pt-table .pt-row-total,.pt-table-contents .pt-table .pt-col-total{font-weight:bold}.pt-table-contents .pt-table .pt-total,.pt-table-contents .pt-table .pt-col-total{border-top:2px solid #ccc}.pt-table-contents .pt-table .pt-total,.pt-table-contents .pt-table .pt-row-total{border-left:2px solid #ccc}.pt-table-contents .pt-table .pt-cell-value{display:inline}.pt-table-contents .pt-table .pt-cell-value li{font-size:0.8rem}.pt-table-contents .pt-table .pt-cell-records{clear:left}.pt-table-contents .pt-table td{min-width:60px;padding-right:16px;white-space:nowrap}.pt-table-contents .pt-table td .pt-cell-zoom{width:16px;height:16px;cursor:pointer;display:inline-block;vertical-align:text-top;margin-left:5px;background:url(../../img/silk/magnifier_zoom_in.png) no-repeat top right}.pt-table-contents .pt-table td .pt-cell-zoom.opened{background-image:url(../../img/silk/magnifier_zoom_out.png)}.pt-table-contents .pt-table tfoot{font-style:italic}.tp-throbber,.pt-throbber{float:right;padding:5px;z-index:999}.gi-empty,.tp-empty,.pt-empty,.pt-no-data{font-style:italic;font-size:0.8rem}.tp-chart-controls,.tp-chart.contents,.pt-toggle-table,.pt-chart-controls,.pt-chart-contents,.pt-table-controls,.pt-table-contents{clear:left}.pt-toggle-table{display:inline-block}.pt-show-table,.pt-hide-table{font-size:10px;cursor:pointer;color:#039;text-decoration:underline;margin-bottom:3px}.pt-show-table{display:none}.pt-table-controls{position:relative;padding:0.4rem 0}.pt-export-table{text-align:right;margin:0.3rem;display:inline-block;position:absolute;right:0}.pt-export-opt{background-repeat:no-repeat;background-position:center;width:16px;height:16px;display:inline-block;cursor:pointer}.pt-export-xls{background-image:url('../../img/icon-xls.png')}.gi-table table{border-collapse:separate}.gi-table table thead td,.gi-table table tfoot td{background-color:#333;color:#fff}.gi-group-header.gi-level-1 td{background-color:#999;color:#fff}.gi-group-header.gi-level-1 td a{color:#fff}.gi-group-header.gi-level-2 td{background-color:#eee;border-top:1px solid #999}.gi-group-footer.gi-level-1 td{background-color:#999;color:#fff;border-bottom:1px dotted #666}.gi-group-footer.gi-level-1 td a{color:#fff}.gi-group-footer.gi-level-2 td{background-color:#eee;border-bottom:1px solid #999}.gi-group-footer-inline-label{display:inline-block;font-size:11px;line-height:1;position:relative;top:-0.1em;margin:0 0.25em 0 1em;padding:0.25em 0.75em;text-transform:uppercase;background-color:#ccc;color:#666}.gi-export-formats{display:inline}.gi-export-formats .gi-export{height:16px;width:16px;display:inline-block;padding-right:1.2rem;padding-bottom:1.5rem;background-repeat:no-repeat;cursor:pointer}@charset "UTF-8";tr.survey_section th{color:#003399;font-size:150%;text-align:center}tr.survey_question th{color:#112038;font-size:90%;font-weight:bold;vertical-align:top}div.survey_map-legend td{vertical-align:top}div.survey_scrollable{width:900px;overflow:scroll}@font-face{font-family:'DRMP';src:url('../../fonts/DRMP.eot');src:url('../../fonts/DRMP.eot?#iefix') format('embedded-opentype'),url('../../fonts/DRMP.woff') format('woff'),url('../../fonts/DRMP.ttf') format('truetype'),url('../../fonts/DRMP.svg#DRMP') format('svg');font-weight:normal;font-style:normal}.icon-activity,.icon-alert,.icon-assessment,.icon-event,.icon-incident,.icon-map,.icon-news,.icon-plan,.icon-project,.icon-report,.icon-training_material{font-family:'DRMP';speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased}.icon-activity5:before,.icon-activity:before{content:"\5b"}.icon-alert:before{content:"\70"}.icon-assessment:before{content:"\6f"}.icon-event:before{content:"\69"}.icon-incident:before{content:"\75"}.icon-map:before{content:"\79"}.icon-news:before{content:"\71"}.icon-plan:before{content:"\74"}.icon-project:before{content:"\72"}.icon-report:before{content:"\65"}.icon-training_material:before{content:"\77"}.latest-updates,.list_formats{margin-top:0.5rem}.latest-updates .action-btn.s3_modal{float:right;font-weight:normal}body.rtl .latest-updates .action-btn.s3_modal{float:left}.side-filter{margin-top:10px}.side-filter .form-row{margin-left:0.375rem;margin-right:0.375rem;padding:0}.side-filter input[type="text"]{width:100% !important}.dl-header{font-size:0.825rem}.dl-exports{margin:0}.card-holder-home .dl-1-cols{width:100%}.card-holder-home .dl-item{padding:0;font-size:0.8rem;margin-bottom:0.5rem;box-shadow:0 1px 1px #aaaaa7}.card-holder-home .dl-row.even,.card-holder-home .dl-row.odd,.card-holder-home .dl-row.even .dl-item,.card-holder-home .dl-row.odd .dl-item{background-color:white}.card-header{background:linear-gradient(to bottom,rgba(255,255,255,1) 0%,rgba(246,246,246,1) 47%,rgba(237,237,237,1) 100%) repeat scroll 0 0 rgba(0,0,0,0);font-size:0.8rem;padding:0 0.175rem 0 0.325rem;border-bottom:1px solid #bdbdbb}.card-header span{padding:0 5px 0 5px;border-right:1px solid #7f7f7f}.card-header .edit-bar a{color:#7f7f7f;text-decoration:none;padding-left:0.175rem}.card-header .location-title{font-weight:bold}.card-header .date-title{color:#7f7f7f}.card-footer{margin-top:0.2rem;padding:0}.card-footer a.action-btn:first-child,.card-footer a.delete-btn:first-child{margin-left:0}.dl-item .pull-right{float:right}body.rtl .dl-item .pull-right{float:left}.dl-item .pull-left{float:left;margin-right:0.8rem}body.rtl .dl-item .pull-left{float:right;margin-left:0.8rem}body.rtl .fright{float:left !important}.dl-item .media{font-size:0.8rem;padding:0.375rem}.dl-item .card-subtitle{font-weight:bold;font-size:0.9rem}.dl-item .date-title,.dl-item .card-person{font-size:0.7rem}.dl-inline-data{margin-top:0.5rem}.dl-inline-label,.dl-inline-value{color:#999;font-size:0.7rem;display:inline-block}.dl-inline-label{font-weight:normal;padding-right:0.2rem}.dl-inline-label::after{content:":"}.dl-inline-value{font-weight:bold;padding-right:0.8rem}.dl-item ul.s3-tags{font-size:0.8rem;margin:0 0.375rem !important;list-style:none}.dl-item ul.s3-tags li.tagit-new{padding:0}meta.foundation-version{font-family:"/{{VERSION}}/"}meta.foundation-mq-small{font-family:"/only screen/";width:0}meta.foundation-mq-small-only{font-family:"/only screen and (max-width:40em)/";width:0}meta.foundation-mq-medium{font-family:"/only screen and (min-width:40.0625em)/";width:40.0625em}meta.foundation-mq-medium-only{font-family:"/only screen and (min-width:40.0625em) and (max-width:64em)/";width:40.0625em}meta.foundation-mq-large{font-family:"/only screen and (min-width:64.0625em)/";width:64.0625em}meta.foundation-mq-large-only{font-family:"/only screen and (min-width:64.0625em) and (max-width:90em)/";width:64.0625em}meta.foundation-mq-xlarge{font-family:"/only screen and (min-width:90.0625em)/";width:90.0625em}meta.foundation-mq-xlarge-only{font-family:"/only screen and (min-width:90.0625em) and (max-width:120em)/";width:90.0625em}meta.foundation-mq-xxlarge{font-family:"/only screen and (min-width:120.0625em)/";width:120.0625em}meta.foundation-data-attribute-namespace{font-family:false}.inline-throbber{background-image:url(../../img/sunflower_fade_indicator.gif)}.throbber,.layer_throbber,.map_loader{background-image:url(../../img/sunflower_spin_throbber.gif)}.loading{background:url(../../img/sunflower_spin_throbber.gif) center no-repeat !important}.sahana-logo{background:url(../../img/S3menu_logo.png) left top no-repeat;text-shadow:none;padding:0;margin-left:5px;margin-top:10px;width:35px;height:28px;display:inline-block}.alert.alert-error,.alert.alert-info,.alert.alert-warning,.alert.alert-success{border:1px solid #b2b2b2;box-shadow:0 1px 1px #aaaaa7;margin-left:auto;margin-right:auto;margin-top:10px;padding:8px 35px 8px 14px;position:relative;width:auto;z-index:98;border-radius:4px}.error,.expired,.req,.req_key{color:#c60f13;font-weight:bold}.req_key{font-size:0.75rem}.username{color:#666666;padding:0.5rem 0;padding-right:0.5rem;font-size:0.7rem}.username i.icon,.username i.fa{padding-left:0.2rem;padding-right:0}body.rtl .username{padding-left:0.5rem;padding-right:0;float:left}body.rtl .username i.icon,body.rtl .username i.fa{padding-left:0;padding-right:0.2rem}.item-container form,.form-container form,#datalist-filter-form,#datatable-filter-form,#summary-filter-form,#summary-sections,.thumbnail{border:1px solid #E0E0E0}.widget-container #list-btn-add{margin-bottom:0;position:relative;top:1.0rem}#component{float:none}.map_home{margin:0;margin-top:0.5rem}#content a.help:link{color:#fff;text-decoration:none;margin-right:10px}#content a.help:hover{background-color:#336699;text-decoration:underline}#content a.help:visited{font-weight:normal;color:#666}#content h1,#content h2{font-size:1.3em;font-weight:bolder;background-color:#F7F8F9;padding:0.35rem}#content h2{margin-top:10px;font-size:1.0rem;padding-left:0.7rem}#content h3{font-size:1.1em;padding-bottom:5px}#footer{background:transparent;padding-top:20px;border-top:1px solid #F0F0F0;margin-top:20px;margin-bottom:1rem}#poweredby{margin-right:0.5rem;text-align:right;color:#999;font-size:0.8rem}#poweredby a{color:#999;text-decoration:none;font-size:0.8rem;margin-left:0.2rem}body.rtl #poweredby{text-align:left}.sub-nav.about-menu{color:#999;text-align:left;margin-left:0;margin-right:0}.sub-nav.about-menu a{color:#999}body.rtl .about-menu{text-align:right}form{font-size:0.8rem}form .form-row{padding:0 0.3rem;margin-top:0.75rem !important}form .form-row .button{margin-right:0.5rem}form .form-row .button i.fa{margin-right:0.3rem}form .form-row > .columns:first-child{overflow:hidden}form .form-row > .columns > label{font-weight:bold}form .form-row > a{margin-left:0.625rem}form .form-row select{min-width:4rem}form .form-row .controls span.postfix{height:1.75rem;line-height:1.5rem;font-size:0.8rem}form .form-row .controls span.postfix .fa{line-height:inherit}form .form-row .s3-hierarchy-widget,form .form-row .calendar-widget-container{display:inline-block;vertical-align:middle}form .form-row .inline-component{overflow:auto}form .jstree-anchor{font-size:0.9rem}form label.inline{padding:0}form .gis_loc_select_btn{font-size:0.8em}form .gis_loc_select_btn i{font-size:1.0em;margin-right:4px}form .error{display:block;padding:0.2rem 0 0 0.2rem;margin-top:0;margin-bottom:0.2rem;font-size:0.75rem;font-style:italic;background:transparent;color:#c60f13}form .error_top .error{margin-top:1.4rem;padding:0}form .invalidinput{border:1px solid #c60f13}form .date-clear-btn{font-size:0.75rem;margin-left:0.15rem !important}form .action-lnk{font-weight:normal;font-size:0.75rem}form table.embeddedComponent{margin-top:0.125rem;margin-bottom:0.125rem;border:1px solid #dddddd;border-collapse:separate}form table.embeddedComponent td{border:none;padding-bottom:0.05rem;padding-top:0.15rem;vertical-align:top}form table.embeddedComponent.subform-vertical .add-row td,form table.embeddedComponent.subform-vertical .edit-row td{border-top:1px solid #dddddd;padding:0 0.4rem 0.4rem 0.4rem}form table.embeddedComponent.subform-vertical .add-row td.subform-action,form table.embeddedComponent.subform-vertical .edit-row td.subform-action{vertical-align:bottom}form table.embeddedComponent .label-row td{border-bottom:1px solid #dddddd}form table.embeddedComponent .label-row td:empty{padding:0}form table.embeddedComponent .label-row label{color:black}form table.embeddedComponent tr.inline-form input,form table.embeddedComponent tr.inline-form .btn.date-clear-btn,form table.embeddedComponent tr.inline-form .postfix.calendar-clear-btn{margin-top:0}form table.embeddedComponent tr.inline-form input[type="text"],form table.embeddedComponent tr.inline-form .s3-upload-widget{font-size:0.8rem;max-width:14rem}form table.embeddedComponent tr.inline-form textarea{font-size:0.8rem}form table.embeddedComponent tr.inline-form.single td:only-child{padding:0}form table.embeddedComponent tr.inline-form.single td:only-child div.form-row{padding:0 0.2rem 0.2rem}form table.embeddedComponent tr.inline-form select{max-width:18rem}form table.embeddedComponent tr.inline-form .s3_inline_add_resource_link{padding:0.1rem}form table.embeddedComponent tr.inline-form .zoom img,form table.embeddedComponent tr.read-row .zoom img{max-height:8rem}form table.embeddedComponent tr.inline-form td,form table.embeddedComponent tr.read-row td{font-size:0.8rem;max-width:21rem;white-space:pre-line}form .inline-open-add{display:inline-block;margin-bottom:1.25rem;margin-left:0}form .subheading{background-color:#FFEDCB;font-size:0.9rem;font-weight:bold;border-top:solid 1px #FFD7A3;border-left:solid 1px #FFD7A3;margin:1.5rem 0 0.75rem;padding:0.5rem 0.25rem 0.25rem 0.75rem;max-width:80%}form .subheading:first-of-type{margin-top:0}form.auth_login #submit_record__row{white-space:pre}ul.s3-inline-link{font-size:inherit}input[type="text"],input[type="password"],input[type="date"],input[type="datetime"],input[type="datetime-local"],input[type="month"],input[type="week"],input[type="email"],input[type="number"],input[type="search"],input[type="tel"],input[type="time"],input[type="url"],textarea,select{height:1.75rem;padding:0.25rem !important;display:inline-block;font-weight:normal}input[type="text"]:focus,input[type="password"]:focus,input[type="date"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="month"]:focus,input[type="week"]:focus,input[type="email"]:focus,input[type="number"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="time"]:focus,input[type="url"]:focus,textarea:focus,select:focus{background-color:#fffbe0}input[type="text"],input[type="password"],input[type="date"],input[type="datetime"],input[type="datetime-local"],input[type="month"],input[type="week"],input[type="email"],input[type="number"],input[type="search"],input[type="tel"],input[type="time"],input[type="url"],input[type="file"],select{width:auto !important;max-width:100%}textarea{resize:both;max-width:100%;vertical-align:top;font-size:0.8rem}input.datetimepicker{max-width:8rem}select{padding:0 1rem 0 0.25rem !important}.form-info{min-height:1.2rem}#last_update,#markDuplicate{float:right;clear:right;text-align:right}#markDuplicate{margin-bottom:0;margin-top:-0.5rem}body.rtl #last_update,body.rtl #markDuplicate{float:left;clear:left;text-align:left}.form-container,.item-container{width:auto;overflow:inherit}.form-container form,.item-container form{background:#fefefe;padding:5px 10px}.form-container form tr td,.item-container form tr td{padding:0.1875rem}.form-container form .embeddedComponent td,.item-container form .embeddedComponent td{padding-right:0.5625rem}.form-container .controls,.item-container .controls{display:inline-block;min-height:1.7rem;padding:0 0.1rem}.form-container .controls:not(.columns),.item-container .controls:not(.columns){max-width:100%}.form-container .no-options-available,.item-container .no-options-available{color:#aaa;font-style:italic;padding-left:0.7rem}.form-container{margin-top:0.125rem;margin-bottom:0.875rem}.form-container form select,.form-container form input.string:not(.date),.form-container form textarea{margin:0}.form-container form .ui-multiselect{max-width:100%;display:inline-block !important;font-size:0.8rem;line-height:1.5;margin-right:0.3rem}.form-container form:not(.filter-form):not(.auth_login):not(.auth_register):not(.rm-form) input[type="text"]:not(.integer):not(.double):not(.datetimepicker):not(.date):not(.hours):not(.dms-input){width:20rem !important}.item-container .controls{background:#fafafa;padding:0.3rem;min-height:1.7rem}.filter-form table tr,.filter-form .ui-multiselect,.report-options table tr,.report-options .ui-multiselect{max-width:60%}.filter-form table.s3-groupedopts-widget,.report-options table.s3-groupedopts-widget{display:inline-block}.filter-form label.inline,.report-options label.inline{line-height:normal;padding-top:0.2rem}#login_form,#register_form,#login_box{clear:none !important}.inline-tooltip{display:inline-block;vertical-align:top;padding-left:0.75rem}.inline-tooltip .s3_add_resource_link{display:inline-block;margin:-3px 0 -3px 0.7rem;padding-right:0.3rem}.tooltip,.tooltipbody,.stickytip,.ajaxtip,.htmltip{display:inline-block;position:static;padding:0;text-transform:uppercase;height:20px;background:none}.tooltip.inline-tooltip,.tooltipbody.inline-tooltip,.stickytip.inline-tooltip,.ajaxtip.inline-tooltip,.htmltip.inline-tooltip{margin-left:1rem}.tooltip:before,.tooltipbody:before,.stickytip:before,.ajaxtip:before,.htmltip:before{content:"\f29c";color:#AAA;font:normal normal normal 14px/1 FontAwesome;font-size:1.2rem;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.tooltip:hover,.tooltipbody:hover,.stickytip:hover,.ajaxtip:hover,.htmltip:hover{background:none;cursor:help}#rheader{width:100%}#rheader a.th{margin-right:10px}#rheader a.th img{height:60px}#rheader .rheader-avatar{clear:none;display:inline-block;float:none;padding:0;vertical-align:top}#rheader img.rheader-avatar{margin-right:0.8rem}#rheader .rheader-content{display:inline-block}#rheader .rheader-title{color:#333;font-weight:bold;font-size:1.1rem;line-height:1.0}#rheader table{display:inline;margin-bottom:10px;border:none}#rheader table tr{background:none}div.tabs{display:block;clear:none;height:1.9rem;width:100%;margin:0.5rem 0;padding:0;line-height:1.3rem;text-align:left;border-bottom:1px solid #166068}div.tabs span{float:left;display:inline;position:relative;margin:0.2rem 0.2rem 0 0;padding:0.1rem 0.3rem 0.1rem;border-radius:3px 3px 0 0}div.tabs span a{color:#ffffff !important;text-decoration:none !important;background:transparent !important}div.tabs span a:hover{background:transparent !important}div.tabs span.tab_here{font-size:1rem;font-weight:bold;margin:0.2rem 0.35rem 0 0;padding:0.1rem 0.4rem 0.12rem;background:#ffffff;border-color:#166068;border-style:solid;border-width:0.125rem 2px 0.0625rem 3px;border-bottom-style:solid;border-bottom-color:#ffffff}div.tabs span.tab_here:hover{background-color:#f1edec}div.tabs span.tab_here a{color:#666666 !important}div.tabs span.tab_here a:hover{color:#666666 !important}div.tabs span.tab_last,div.tabs span.tab_other{font-size:0.9rem;margin:0.35rem 0.2rem 0 0;padding:0.1rem 0.3rem 0;background-color:#166068;border-color:#166068;border-width:0.0625rem 1px 0.0625rem 2px;border-style:solid}div.tabs span.tab_last:hover,div.tabs span.tab_other:hover{background-color:#124d53;border-color:#124d53}div.tabs span.tab_last a,div.tabs span.tab_other a{color:#ffffff !important}div.tabs span.tab_last a:hover,div.tabs span.tab_other a:hover{color:#ffffff !important}.action-btn,.delete-btn-ajax,.delete-btn,.selected-action{font-size:0.6875rem;border:0;line-height:1;margin-bottom:inherit;padding:0.25rem 0.5rem 0.375rem;cursor:pointer;text-decoration:none !important;display:inline-block}.action-btn[disabled],.action-btn[disabled]:hover,.delete-btn-ajax[disabled],.delete-btn-ajax[disabled]:hover,.delete-btn[disabled],.delete-btn[disabled]:hover,.selected-action[disabled],.selected-action[disabled]:hover{color:white;background-color:rgba(192,192,192,0.25)}.action-btn,.selected-action{background-color:#166068;color:white !important}.action-btn:hover,.selected-action:hover{background-color:#124d53;color:white !important}.delete-btn-ajax,.delete-btn{background-color:#c60f13;color:white !important}.delete-btn-ajax:hover,.delete-btn:hover{background-color:#9e0c0f !important;color:white !important}.cancel-form-btn{padding:0.9375rem}.cancel-form-btn:hover{color:white;background-color:#166068}.map_home .gis_fullscreen_map-btn{font-weight:normal;font-size:0.8rem;padding:0.2rem}.dataTable .action-btn,.dataTable .selected-action{color:white}.dataTable .delete-btn,.dataTable .delete-btn-ajax{color:white}.dataTable td.actions{white-space:nowrap}.pr-contacts-editable button{font-size:0.6875rem;border:0;line-height:1;margin-bottom:inherit;padding:0.25rem 0.5rem 0.375rem;cursor:pointer;text-decoration:none !important;display:inline-block}#footer button.btn{font-size:0.6875rem;border:0;line-height:1;margin-bottom:inherit;padding:0.25rem 0.5rem 0.375rem;cursor:pointer;text-decoration:none !important;display:inline-block;margin-left:2px;margin-right:2px;color:white;background:#dddddd}#footer button.btn:hover{color:white;background:#a0a0a0}.action-lnk{margin-left:0.6rem}.action-lnk:first-child{margin-left:0}body.rtl .action-lnk{margin-left:0;margin-right:0.6rem}body.rtl .action-lnk:first-child{margin-right:0}.caret{display:inline-block;width:0;height:0;vertical-align:top;border-top:4px solid #000000;border-right:4px solid transparent;border-left:4px solid transparent;content:""}.top-bar{z-index:1000}.top-bar.expanded{width:100%}.top-bar .logo{color:white;display:inline-block;font-size:1.1rem;font-weight:bold;height:35px;padding:0.625rem 0.5rem 0 0.7rem;text-transform:uppercase}.top-bar .menu-toggle input{margin-right:5px}.top-bar li.name{padding-top:0.2rem}.top-bar .home-link{font-size:1.375rem;color:white;padding:0.5rem}.top-bar-section li.menu-home a{font-weight:bold !important;font-size:1.4em !important;text-transform:capitalize !important}.sidebar{background:none repeat scroll 0 0 #d7d8d9;margin-top:10px;padding-top:0.25rem;padding-bottom:0.25rem}.side-nav li{list-style-type:none;list-style-position:inside;line-height:1.2;padding:0}.side-nav li.heading{margin-top:0.125rem}.side-nav li.heading:not(:first-child){border-top:1px solid #c0c1c2}.side-nav li.active > a:first-child:not(.button){font-weight:bold;background-color:#e0e1e2}.side-nav li.active > a:first-child:not(.button):hover{color:#FFFFFF;background-color:#2ba6cb}.main-title .org-logo{vertical-align:top;display:inline-block;padding:0.4rem 0.8rem 0.3rem 0}.main-title .system-title{display:inline-block}.main-title .system-title .system-name{color:#333333}.main-title .system-title .org-name{color:#999999;line-height:1rem}.main-title .system-title h5:first-child,.main-title .system-title h6:first-child{margin:0.3rem 0 0.125rem}.main-title .personal-menu-area{text-align:right}.main-title .personal-menu{float:right;clear:right;padding-top:0;margin-bottom:0.125rem}.main-title .personal-menu li a{padding:0 0.375rem}.main-title .language-selector{float:right;display:block;margin-top:0.625rem;margin-bottom:0.125rem}body.rtl .main-title .personal-menu{float:left;clear:left}body.rtl .main-title .language-selector{float:left}#table-container{margin-bottom:1.5rem}table.dataTable thead th,table.dataTable th,table.dataTable td{border:1px solid #cccccc;padding:0.2em 1.5em 0.2em 0.5em}table.dataTable thead th,table.dataTable thead td{background-color:#ffffff}table.dataTable tbody tr.even{background-color:#ffffff}table.dataTable tbody tr.even td.sorting_1{background-color:#fafafa}table.dataTable tbody tr.odd{background-color:#f7f8f9}table.dataTable tbody tr.odd td.sorting_1{background-color:#f0f1f2}table.dataTable tbody tr.odd td{border-color:#cccccc}table.dataTable tbody tr.row_selected.odd{background-color:#40fa8d}table.dataTable tbody tr.row_selected.odd td.sorting_1{background-color:#20f0ad}table.dataTable tbody tr.row_selected.even{background-color:#60f6ad}table.dataTable tbody tr.row_selected.even td.sorting_1{background-color:#40fa8d}table.dataTable tbody tr.dtalert.odd{background-color:#ffffc0}table.dataTable tbody tr.dtalert.odd td.sorting_1{background-color:#ffffb0}table.dataTable tbody tr.dtalert.even{background-color:#ffffa0}table.dataTable tbody tr.dtalert.even td.sorting_1{background-color:#fffff0}table.dataTable tbody tr.dtwarning.odd{background-color:#ffd9d9}table.dataTable tbody tr.dtwarning.odd td.sorting_1{background-color:#ffb6b6}table.dataTable tbody tr.dtwarning.even{background-color:#ffa6a6}table.dataTable tbody tr.dtwarning.even td.sorting_1{background-color:#ff8383}table.dataTable tfoot th,table.dataTable tfoot td{background-color:#f7f8f9;border-top:2px solid #cccccc;padding:0.5em}table.dataTable .selected-action{margin:5px 0 5px}table.dataTable .bulk-select-options{font-size:0.7rem;font-weight:normal}table.dataTable .bulk-select-options input[type=checkbox]{margin-bottom:0.2rem;margin-right:0.5rem;vertical-align:middle}table.dataTable input.bulkcheckbox[type=checkbox]{margin-top:0.2rem}table.dataTable .group span.ui-icon{display:inline-block}table.dataTable .group .group-indent{width:10px}table.dataTable .group .group-opened,table.dataTable .group .group-closed{padding:0.2rem}table.dataTable .group .group-collapse,table.dataTable .group .group-expand{cursor:pointer;float:right}table.dataTable.dtr-inline.collapsed tbody td:first-child::before,table.dataTable.dtr-inline.collapsed tbody th:first-child::before{top:6px;background-color:#166068}table.dataTable.dtr-inline.collapsed tbody tr.parent td:first-child::before,table.dataTable.dtr-inline.collapsed tbody tr.parent th:first-child::before{top:6px;background-color:#c60f13}table.dataTable table.import-item-details{display:none}.dataTables_length{float:left !important}.dataTables_length label{font-size:0.75rem;white-space:nowrap;margin-right:10em;margin-bottom:0.3em}.dataTables_length select{height:auto;padding:0.2rem 1.1rem 0.2rem 0 !important;font-size:0.75rem}.dataTables_processing{padding:14px 0 28px}.dataTables_filter{text-align:left;font-size:0.75rem;margin-right:3rem}.dataTables_filter input[type="search"]{margin-left:0.2rem}.dt-export-options{float:right}.dt-export-options .list_formats{padding-top:0;margin:0 0.2rem}.dt-export-options .dt-export{margin:0 0.1rem}.dt-export-options .dt-export.fa{font-size:14px;padding:0;padding-top:2px}body.rtl .dt-export-options,body.rtl .list_formats div{float:left}body.rtl .dataTable .group .group-collapse,body.rtl .dataTable .group .group-expand{float:left}.empty{font-style:italic;font-size:0.825rem}.fc table,.fc table tr{background:transparent}.dl .dl-1-cols{width:100%}.dl .dl-header{float:none;text-align:right}.dl .dl-row{border:0}.dl .dl-item .dropdown .caret{margin-left:0.2rem;margin-top:0.4rem}.dl .dl-item .attachments{margin-right:0.2rem}body.rtl .dl .dl-header{text-align:left}body.rtl .dl .dl-item .dropdown .caret{margin-left:0;margin-right:0.2rem}body.rtl .dl .dl-item .attachments{margin-right:0;margin-left:0.2rem}.dl-empty{font-style:italic;font-size:0.825rem}.dl-exports.list_formats{padding:0}.card_1_line,.card_manylines{color:#666;font-size:0.7rem;padding:0.05rem}.card_1_line{height:auto;margin-bottom:0;overflow:hidden}h4.profile-sub-header{background-color:#efefef;padding:0.1rem 0.3rem;font-size:1.2rem}.profile-widget .icon-fullscreen{float:right;position:relative;right:9px;top:17px}.profile .dl-header{display:none}.empty_card-holder{text-align:center}.ui-widget-header{background:none;border:1px solid #ADA6A0}.ui-widget-header a{color:#222222 !important;border-color:#C7C7C7;margin-bottom:0px;font-weight:bold;text-decoration:none !important;font-size:0.7em}.ui-widget-header.ui-slider-range{background:#cccccc}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active{border-color:#ADA6A0}.ui-widget-content .ui-state-active.ui-slider-handle{background:#007fff;border:1px solid #003eff}.ui-multiselect-header span.ui-icon{margin-top:4px}.ui-multiselect-checkboxes li label input{margin-left:0.2rem;margin-right:0.3rem}.ui-widget{font-family:"Helvetica Neue",Helvetica,Roboto,Arial,sans-serif}#list-btn-add{float:right;margin-right:0.35rem}.ui-tabs .ui-tabs-panel{padding:0.5em}#summary-tabs{margin-top:10px}#summary-tabs.ui-widget-content{background:none;border:none}#summary-tab-headers .ui-tabs-nav{border:none;margin:0 10px}#summary-tab-headers a{border-color:#CCC}#summary-tab-headers .ui-tabs-active a{border-bottom:1px solid white}#summary-tab-headers li{margin-right:5px;box-shadow:none}#summary-sections .x-panel-body{border:none;border-radius:4px}input.date.hasDatepicker{display:inline-block}.ui-datepicker-trigger{background-color:transparent;background-image:url("../../img/bootstrap/calendar.gif");margin-left:10px}.ui-datepicker.ui-widget{z-index:1000 !important}.range-filter-label label{font-size:1.0em}.range-filter-widget input.date-filter-input{width:auto;margin-right:2px}.range-filter-widget button.date-clear-btn{font-size:0.85em;padding:0.325em;margin-top:0.5em}.range-filter-widget .postfix.calendar-clear-btn{margin-left:-2px}.btn.date-clear-btn{font-size:0.85em;padding:0.325em;margin-top:0.5em}.calendar-widget-container{white-space:nowrap}.postfix.calendar-clear-btn{cursor:pointer;display:inline-block;margin-top:2px;width:1.2rem;vertical-align:top}.postfix.calendar-clear-btn .fa{color:#B0B0B0}.s3-groupedopts-label{float:none;margin-bottom:0.3rem}.s3-groupedopts-widget table{margin-left:0;margin-bottom:0.5rem}table.s3-groupedopts-widget{margin-bottom:0}table.s3-groupedopts-widget td{white-space:nowrap}table.s3-groupedopts-widget td input.s3-groupedopts-option{vertical-align:text-top;margin:0 0.3rem 0 0}table.s3-groupedopts-widget td label{white-space:pre-wrap;margin:0 0.5rem}div.s3-groupedopts-widget .s3-groupedopts-item{display:inline-block;padding-top:0.15rem}div.s3-groupedopts-widget input.s3-groupedopts-option{vertical-align:text-top;margin:0 0.3rem 0 0}.imagecrop-btn{font-size:1em;margin-left:0.2em;padding:0.3em}.s3-widget-intro{max-width:42rem;padding:0.5rem 0;font-size:0.8rem}.pr-contacts-wrapper h3{margin-top:0.5rem;padding-bottom:0 !important}.pr-contacts-editable input,.pr-contacts-editable select{min-height:1.2rem;margin:2px 0}.pr-contacts-editable button{font-size:0.6875rem;border:0;line-height:1;margin-bottom:inherit;padding:0.25rem 0.5rem 0.375rem;cursor:pointer;text-decoration:none !important;display:inline-block;margin:2px}.pr-contact-actions{margin-bottom:0.5rem}.pr-contact,.pr-emergency-contact{padding:0.5rem}.pr-contact-priority,.pr-contact-details{display:inline-block}.pr-contact-subtitle{font-size:0.8rem;font-style:italic}.pr-contact-priority{border:thin solid #2ba6cb;border-radius:2px;color:#2ba6cb;font-size:0.8rem;line-height:1rem;margin:0.25rem 1rem 0 0;padding:0 0.25rem 0.125rem;vertical-align:top}.pr-contact-priority input[type=submit]{color:black;line-height:1.2rem;font-size:0.8rem}.controls .checkboxes-widget-s3{display:inline-flex}.comment-box{background:none repeat scroll 0 0 #f5f5f5}.text-body{white-space:pre-wrap}#close-iframe-map{padding:7px;margin-top:5px}.s3-truncate-more,.s3-truncate-less{font-style:italic}.s3-truncate-more:before,.s3-truncate-less:before{font-style:italic;content:"..."}.box_bottom{clear:left}.ui-menu-item .pe-label{font-size:0.7rem;color:#A0A0A0;vertical-align:super}.qrinput{display:inline-block}.qrinput .qrscan-btn{padding:0.4rem 1.2rem;margin:3px 0.5rem;vertical-align:top}.qrinput-success{text-align:center;font-size:4rem;padding:1rem;color:darkgreen}form.anonymize-form,.anonymize-success{padding:1rem}.anonymize-select,.anonymize-confirm{margin-left:1rem}.anonymize-buttons{margin-top:1rem}.anonymize-btn{background-color:#c60f13}.anonymize-btn:hover{background-color:#9e0c0f}.s3-anonymize{display:inline-block;margin:0.2rem}.rm-form .controls{width:100%}.rm-form .rm-assign .action-lnk,.rm-form .rm-rules .action-lnk{display:inline-block;color:#166068;font-style:normal}.rm-form .rm-toggle-all{margin-top:0.2rem;margin-bottom:0.5rem}.rm-form .rm-assign{width:80%;border-collapse:separate;border-spacing:0}.rm-form .rm-assign th{min-width:10rem}.rm-form .rm-assign th:first-child{width:25%}.rm-form .rm-assign td{padding:0.5rem}.rm-form .rm-assign tfoot td{border-top:1px solid #C0C0C0}.rm-form .rm-assign .rm-item-name,.rm-form .rm-assign .rm-item-title{display:block}.rm-form .rm-assign .rm-item-title{font-size:0.8rem;font-style:italic;color:#A0A0A0}.rm-form .rm-assign .rm-duplicate td{border-top:1px solid red;border-bottom:1px solid red}.rm-form .rm-assign .rm-duplicate td:first-child{border-left:1px solid red}.rm-form .rm-assign .rm-duplicate td:last-child{border-right:1px solid red}.rm-form .rm-module-rules{width:100%}.rm-form .rm-module-rules .rm-module-header{cursor:pointer;background-color:#F0F0F0}.rm-form .rm-module-rules .rm-module-header div{margin-right:0.3rem;display:inline-block}.rm-form .rm-module-rules .rm-module-header .rm-module-toggle{width:0.5rem}.rm-form .rm-module-rules .rm-module-header .rm-module-prefix{width:6.5rem}.rm-form .rm-module-rules .rm-module-header .rm-module-numrules{font-weight:normal}.rm-form .rm-module-rules.hasrules .rm-module-header{background-color:#D0D0D0}.rm-form .rm-module-rules th,.rm-form .rm-module-rules td{padding:0.5rem}.rm-form .rm-module-rules .rm-default-rule td,.rm-form .rm-module-rules .rm-default-permissions td{font-style:italic;color:#a0a0a0}.rm-form .rm-module-rules .rm-rule-target{width:20%}.rm-form input.rm-invalid{background-color:#FFA0A0}.rm-fixed,.rm-hint{display:block;font-style:italic;font-size:0.8rem;color:#AAAAAA}.s3-organizer-popup label{font-size:0.7rem;font-weight:bold}.s3-organizer-popup p{font-size:0.8rem;font-weight:normal;margin-bottom:0.3rem}.s3-organizer-popup .action-btn{margin-left:0;margin-right:0.3rem}.s3-organizer-create .action-btn{display:block}.consent-widget{border:1px solid #E0E0E0;padding:0.5rem 1rem}.consent-widget .consent-option{margin-top:0.3rem}.consent-widget .consent-explanation{white-space:pre-wrap;padding:0.3rem 2rem;font-size:0.8rem;font-style:italic;color:#7F7F7F}.consent-widget .consent-title{font-weight:bold}.consent-widget .consent-checkbox{vertical-align:bottom;margin-right:0.5rem}.consent-widget .req{padding:0 0.2rem}.consent-widget .req_key{font-size:0.7rem;font-weight:normal;font-style:italic;margin:0 0.5rem}.prio{padding:0 4px;border:1px solid black;text-align:center;white-space:pre;font-weight:normal}.prio-red{color:white;background-color:#d10000}.prio-blue{color:white;background-color:#10427b}.prio-lightblue{background-color:#b7ddff}.prio-grey{background-color:silver}.wh-intro{max-width:42rem;padding:0.5rem 0;font-size:0.8rem}.wh-raster{margin-top:0.3rem}.wh-raster tr{border-bottom:1px solid #ccc}.wh-raster td{border-left:1px solid #ccc}.wh-raster td.wh-tick{border-left-width:2px}.wh-raster thead td{min-width:1.8rem;padding-right:0.6rem !important;font-size:0.6rem;font-weight:normal}.wh-raster tbody td.wh-hour{text-align:center;font-weight:normal}.wh-raster tbody td.wh-hour.wh-on{color:darkgreen}.wh-raster tbody td.wh-day{text-align:left;font-size:0.7rem;padding:0.2rem 0.4rem}.wh-raster .wh-hour{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.wh-raster .wh-hour.wh-on{background-color:lightgreen}.wh-raster .wh-hour.wh-off{background-color:transparent}.wh-schedule{list-style:none;white-space:pre;margin:0;font-size:0.7rem}.wh-schedule .wh-dayname{width:1.5rem;display:inline-block}.wh-schedule .wh-dayname::after{content:":"}.announcements-title{font-variant:petite-caps;font-weight:normal}.announcements{margin:0;padding:0 0 0 1rem;border-left:1px solid #166068;list-style:none outside none}.announcements ul,.announcements ol{padding-left:20px;list-style:none outside none}.announcements li{padding:10px 0 0}.announcements .announcement-box{display:block;background:#fff7d9;border:1px solid #aaa;overflow:hidden;padding:0.7rem;margin-bottom:1.5rem}.announcements .announcement-box.announcement-important .announcement-icon{color:#0088ca}.announcements .announcement-box.announcement-critical{border:1px solid #ca0000;background-color:#ffebea}.announcements .announcement-box.announcement-critical .announcement-icon{color:#ca0000;animation:blinker 0.7s step-start 4}.announcements .announcement-box .announcement-header .announcement-icon{display:inline-block;font-size:1.3rem;padding:0 0.7rem 0 0}.announcements .announcement-box .announcement-header h4{display:inline-block}.announcements .announcement-box .announcement-text{font-size:0.9rem;line-height:1.3}.announcements .announcement-box .announcement-text h4{font-size:1.2rem}.announcements .announcement-box .announcement-text ul{list-style:disc outside none}.announcements .announcement-box .announcement-text ol{list-style:decimal outside none}.announcements .announcement-box .announcement-text li{padding:0}.announcements .announcement-box .announcement-text div{white-space:pre-line}.announcements .announcement-box .announcement-text .announcement-header{margin:0 0 0.8rem 0}.announcements .announcement-box .announcement-text announcement-body{white-space:pre-line}.announcements .announcement-box .announcement-text announcement-body p{margin-left:1rem}.announcements .announcement-box .announcement-date{font-size:0.7rem;margin-top:0.4rem}body.rtl .ui-multiselect-header .ui-multiselect-filter{direction:rtl;float:right;margin-right:3px;margin-left:10px}body.rtl .ui-multiselect-header ul li{float:right;padding:0 3px 0 10px}body.rtl .ui-multiselect-header li.ui-multiselect-close{float:left;text-align:left;padding-left:0}body.rtl .range-filter-widget input.date-filter-input{margin-right:0;margin-left:2px}body.rtl #list-btn-add{float:left;margin-right:0;margin-left:0.35rem}body.rtl .rm-form .rm-module-rules .rm-module-header div{margin-right:0;margin-left:0.3rem}body.rtl .rm-form .rm-assign .rm-duplicate td:first-child{border-left:0;border-right:1px solid red}body.rtl .rm-form .rm-assign .rm-duplicate td:last-child{border-right:0;border-left:1px solid red} \ No newline at end of file +@charset "UTF-8";.swidth{width:640px}.colmask{position:relative;clear:both;float:left;width:100%;overflow:hidden;z-index:0;margin-top:42px}.col3left{float:left;width:33%;position:relative}.col3mid,.col3right{float:right;width:33%;position:relative}.col2left{float:left;width:49%;position:relative}.col2right{float:right;width:49%;position:relative}.col1,.col2,.col3{float:left;position:relative;padding:0 0 3px 0;overflow:hidden}.fullpage{padding-top:1px;overflow:visible}.fullpage .col1{width:99%;left:0.5%;min-width:800px}.aside{float:left;width:200px}.rightside{margin-left:200px}.ext-el-mask{background-color:#ccc}.ext-el-mask-msg{border-color:#999;background-color:#ddd;background-image:url(../../scripts/ext/resources/images/gray/panel/white-top-bottom.gif);background-position:0 -1px}.ext-el-mask-msg div{background-color:#eee;border-color:#d0d0d0;color:#222;font:normal 11px tahoma,arial,helvetica,sans-serif}.x-mask-loading div{background-color:#fbfbfb;background-image:url(../../scripts/ext/resources/images/default/grid/loading.gif)}.x-item-disabled{color:gray}.x-item-disabled *{color:gray !important}.x-splitbar-proxy{background-color:#aaa}.x-color-palette a{border-color:#fff}.x-color-palette a:hover,.x-color-palette a.x-color-palette-sel{border-color:#CFCFCF;background-color:#eaeaea}.x-color-palette em{border-color:#aca899}.x-ie-shadow{background-color:#777}.x-shadow .xsmc{background-image:url(../../scripts/ext/resources/images/default/shadow-c.png)}.x-shadow .xsml,.x-shadow .xsmr{background-image:url(../../scripts/ext/resources/images/default/shadow-lr.png)}.x-shadow .xstl,.x-shadow .xstc,.x-shadow .xstr,.x-shadow .xsbl,.x-shadow .xsbc,.x-shadow .xsbr{background-image:url(../../scripts/ext/resources/images/default/shadow.png)}.loading-indicator{font-size:11px;background-image:url(../../scripts/ext/resources/images/default/grid/loading.gif)}.x-spotlight{background-color:#ccc}.x-tab-panel-header,.x-tab-panel-footer{background-color:#eaeaea;border-color:#d0d0d0;overflow:hidden;zoom:1}.x-tab-panel-header,.x-tab-panel-footer{border-color:#d0d0d0}ul.x-tab-strip-top{background-color:#dbdbdb;background-image:url(../../scripts/ext/resources/images/gray/tabs/tab-strip-bg.gif);border-bottom-color:#d0d0d0}ul.x-tab-strip-bottom{background-color:#dbdbdb;background-image:url(../../scripts/ext/resources/images/gray/tabs/tab-strip-btm-bg.gif);border-top-color:#d0d0d0}.x-tab-panel-header-plain .x-tab-strip-spacer,.x-tab-panel-footer-plain .x-tab-strip-spacer{border-color:#d0d0d0;background-color:#eaeaea}.x-tab-strip span.x-tab-strip-text{font:normal 11px tahoma,arial,helvetica;color:#333}.x-tab-strip-over span.x-tab-strip-text{color:#111}.x-tab-strip-active span.x-tab-strip-text{color:#333;font-weight:bold}.x-tab-strip-disabled .x-tabs-text{color:#aaaaaa}.x-tab-strip-top .x-tab-right,.x-tab-strip-top .x-tab-left,.x-tab-strip-top .x-tab-strip-inner{background-image:url(../../scripts/ext/resources/images/gray/tabs/tabs-sprite.gif)}.x-tab-strip-bottom .x-tab-right{background-image:url(../../scripts/ext/resources/images/gray/tabs/tab-btm-inactive-right-bg.gif)}.x-tab-strip-bottom .x-tab-left{background-image:url(../../scripts/ext/resources/images/gray/tabs/tab-btm-inactive-left-bg.gif)}.x-tab-strip-bottom .x-tab-strip-over .x-tab-left{background-image:url(../../scripts/ext/resources/images/gray/tabs/tab-btm-over-left-bg.gif)}.x-tab-strip-bottom .x-tab-strip-over .x-tab-right{background-image:url(../../scripts/ext/resources/images/gray/tabs/tab-btm-over-right-bg.gif)}.x-tab-strip-bottom .x-tab-strip-active .x-tab-right{background-image:url(../../scripts/ext/resources/images/gray/tabs/tab-btm-right-bg.gif)}.x-tab-strip-bottom .x-tab-strip-active .x-tab-left{background-image:url(../../scripts/ext/resources/images/gray/tabs/tab-btm-left-bg.gif)}.x-tab-strip .x-tab-strip-closable a.x-tab-strip-close{background-image:url(../../scripts/ext/resources/images/gray/tabs/tab-close.gif)}.x-tab-strip .x-tab-strip-closable a.x-tab-strip-close:hover{background-image:url(../../scripts/ext/resources/images/gray/tabs/tab-close.gif)}.x-tab-panel-body{border-color:#d0d0d0;background-color:#fff}.x-tab-panel-body-top{border-top:0 none}.x-tab-panel-body-bottom{border-bottom:0 none}.x-tab-scroller-left{background-image:url(../../scripts/ext/resources/images/gray/tabs/scroll-left.gif);border-bottom-color:#d0d0d0}.x-tab-scroller-left-over{background-position:0 0}.x-tab-scroller-left-disabled{background-position:-18px 0;opacity:.5;-moz-opacity:.5;filter:alpha(opacity=50);cursor:default}.x-tab-scroller-right{background-image:url(../../scripts/ext/resources/images/gray/tabs/scroll-right.gif);border-bottom-color:#d0d0d0}.x-tab-panel-bbar .x-toolbar,.x-tab-panel-tbar .x-toolbar{border-color:#d0d0d0}.x-form-field{font:normal 12px tahoma,arial,helvetica,sans-serif}.x-form-text,textarea.x-form-field{background-color:#fff;background-image:url(../../scripts/ext/resources/images/default/form/text-bg.gif);border-color:#C1C1C1}.x-form-select-one{background-color:#fff;border-color:#C1C1C1}.x-form-check-group-label{border-bottom:1px solid #d0d0d0;color:#333}.x-editor .x-form-check-wrap{background-color:#fff}.x-form-field-wrap .x-form-trigger{background-image:url(../../scripts/ext/resources/images/gray/form/trigger.gif);border-bottom-color:#b5b8c8}.x-form-field-wrap .x-form-date-trigger{background-image:url(../../scripts/ext/resources/images/gray/form/date-trigger.gif)}.x-form-field-wrap .x-form-clear-trigger{background-image:url(../../scripts/ext/resources/images/gray/form/clear-trigger.gif)}.x-form-field-wrap .x-form-search-trigger{background-image:url(../../scripts/ext/resources/images/gray/form/search-trigger.gif)}.x-trigger-wrap-focus .x-form-trigger{border-bottom-color:#777777}.x-item-disabled .x-form-trigger-over{border-bottom-color:#b5b8c8}.x-item-disabled .x-form-trigger-click{border-bottom-color:#b5b8c8}.x-form-focus,textarea.x-form-focus{border-color:#777777}.x-form-invalid,textarea.x-form-invalid{background-color:#fff;background-image:url(../../scripts/ext/resources/images/default/grid/invalid_line.gif);border-color:#c30}.ext-webkit .x-form-invalid{background-color:#fee;border-color:#ff7870}.x-form-inner-invalid,textarea.x-form-inner-invalid{background-color:#fff;background-image:url(../../scripts/ext/resources/images/default/grid/invalid_line.gif)}.x-form-grow-sizer{font:normal 12px tahoma,arial,helvetica,sans-serif}.x-form-item{font:normal 12px tahoma,arial,helvetica,sans-serif}.x-form-invalid-msg{color:#c0272b;font:normal 11px tahoma,arial,helvetica,sans-serif;background-image:url(../../scripts/ext/resources/images/default/shared/warning.gif)}.x-form-empty-field{color:gray}.x-small-editor .x-form-field{font:normal 11px arial,tahoma,helvetica,sans-serif}.ext-webkit .x-small-editor .x-form-field{font:normal 12px arial,tahoma,helvetica,sans-serif}.x-form-invalid-icon{background-image:url(../../scripts/ext/resources/images/default/form/exclamation.gif)}.x-fieldset{border-color:#CCCCCC}.x-fieldset legend{font:bold 11px tahoma,arial,helvetica,sans-serif;color:#777777}.x-btn{font:normal 11px tahoma,verdana,helvetica}.x-btn button{font:normal 11px arial,tahoma,verdana,helvetica;color:#333}.x-btn em{font-style:normal;font-weight:normal}.x-btn-tl,.x-btn-tr,.x-btn-tc,.x-btn-ml,.x-btn-mr,.x-btn-mc,.x-btn-bl,.x-btn-br,.x-btn-bc{background-image:url(../../scripts/ext/resources/images/gray/button/btn.gif)}.x-btn-click .x-btn-text,.x-btn-menu-active .x-btn-text,.x-btn-pressed .x-btn-text{color:#000}.x-btn-disabled *{color:gray !important}.x-btn-mc em.x-btn-arrow{background-image:url(../../scripts/ext/resources/images/default/button/arrow.gif)}.x-btn-mc em.x-btn-split{background-image:url(../../scripts/ext/resources/images/default/button/s-arrow.gif)}.x-btn-over .x-btn-mc em.x-btn-split,.x-btn-click .x-btn-mc em.x-btn-split,.x-btn-menu-active .x-btn-mc em.x-btn-split,.x-btn-pressed .x-btn-mc em.x-btn-split{background-image:url(../../scripts/ext/resources/images/gray/button/s-arrow-o.gif)}.x-btn-mc em.x-btn-arrow-bottom{background-image:url(../../scripts/ext/resources/images/default/button/s-arrow-b-noline.gif)}.x-btn-mc em.x-btn-split-bottom{background-image:url(../../scripts/ext/resources/images/default/button/s-arrow-b.gif)}.x-btn-over .x-btn-mc em.x-btn-split-bottom,.x-btn-click .x-btn-mc em.x-btn-split-bottom,.x-btn-menu-active .x-btn-mc em.x-btn-split-bottom,.x-btn-pressed .x-btn-mc em.x-btn-split-bottom{background-image:url(../../scripts/ext/resources/images/gray/button/s-arrow-bo.gif)}.x-btn-group-header{color:#666}.x-btn-group-tc{background-image:url(../../scripts/ext/resources/images/gray/button/group-tb.gif)}.x-btn-group-tl{background-image:url(../../scripts/ext/resources/images/gray/button/group-cs.gif)}.x-btn-group-tr{background-image:url(../../scripts/ext/resources/images/gray/button/group-cs.gif)}.x-btn-group-bc{background-image:url(../../scripts/ext/resources/images/gray/button/group-tb.gif)}.x-btn-group-bl{background-image:url(../../scripts/ext/resources/images/gray/button/group-cs.gif)}.x-btn-group-br{background-image:url(../../scripts/ext/resources/images/gray/button/group-cs.gif)}.x-btn-group-ml{background-image:url(../../scripts/ext/resources/images/gray/button/group-lr.gif)}.x-btn-group-mr{background-image:url(../../scripts/ext/resources/images/gray/button/group-lr.gif)}.x-btn-group-notitle .x-btn-group-tc{background-image:url(../../scripts/ext/resources/images/gray/button/group-tb.gif)}.x-toolbar{border-color:#d0d0d0;background-color:#f0f0f0;background-image:url(../../scripts/ext/resources/images/gray/toolbar/bg.gif)}.x-toolbar td,.x-toolbar span,.x-toolbar input,.x-toolbar div,.x-toolbar select,.x-toolbar label{font:normal 11px arial,tahoma,helvetica,sans-serif}.x-toolbar .x-item-disabled{color:gray}.x-toolbar .x-item-disabled *{color:gray}.x-toolbar .x-btn-mc em.x-btn-split{background-image:url(../../scripts/ext/resources/images/default/button/s-arrow-noline.gif)}.x-toolbar .x-btn-over .x-btn-mc em.x-btn-split,.x-toolbar .x-btn-click .x-btn-mc em.x-btn-split,.x-toolbar .x-btn-menu-active .x-btn-mc em.x-btn-split,.x-toolbar .x-btn-pressed .x-btn-mc em.x-btn-split{background-image:url(../../scripts/ext/resources/images/gray/button/s-arrow-o.gif)}.x-toolbar .x-btn-mc em.x-btn-split-bottom{background-image:url(../../scripts/ext/resources/images/default/button/s-arrow-b-noline.gif)}.x-toolbar .x-btn-over .x-btn-mc em.x-btn-split-bottom,.x-toolbar .x-btn-click .x-btn-mc em.x-btn-split-bottom,.x-toolbar .x-btn-menu-active .x-btn-mc em.x-btn-split-bottom,.x-toolbar .x-btn-pressed .x-btn-mc em.x-btn-split-bottom{background-image:url(../../scripts/ext/resources/images/gray/button/s-arrow-bo.gif)}.x-toolbar .xtb-sep{background-image:url(../../scripts/ext/resources/images/default/grid/grid-split.gif)}.x-tbar-page-first{background-image:url(../../scripts/ext/resources/images/gray/grid/page-first.gif) !important}.x-tbar-loading{background-image:url(../../scripts/ext/resources/images/gray/grid/refresh.gif) !important}.x-tbar-page-last{background-image:url(../../scripts/ext/resources/images/gray/grid/page-last.gif) !important}.x-tbar-page-next{background-image:url(../../scripts/ext/resources/images/gray/grid/page-next.gif) !important}.x-tbar-page-prev{background-image:url(../../scripts/ext/resources/images/gray/grid/page-prev.gif) !important}.x-item-disabled .x-tbar-loading{background-image:url(../../scripts/ext/resources/images/default/grid/loading.gif) !important}.x-item-disabled .x-tbar-page-first{background-image:url(../../scripts/ext/resources/images/default/grid/page-first-disabled.gif) !important}.x-item-disabled .x-tbar-page-last{background-image:url(../../scripts/ext/resources/images/default/grid/page-last-disabled.gif) !important}.x-item-disabled .x-tbar-page-next{background-image:url(../../scripts/ext/resources/images/default/grid/page-next-disabled.gif) !important}.x-item-disabled .x-tbar-page-prev{background-image:url(../../scripts/ext/resources/images/default/grid/page-prev-disabled.gif) !important}.x-paging-info{color:#444}.x-toolbar-more-icon{background-image:url(../../scripts/ext/resources/images/gray/toolbar/more.gif) !important}.x-resizable-handle{background-color:#fff}.x-resizable-over .x-resizable-handle-east,.x-resizable-pinned .x-resizable-handle-east,.x-resizable-over .x-resizable-handle-west,.x-resizable-pinned .x-resizable-handle-west{background-image:url(../../scripts/ext/resources/images/gray/sizer/e-handle.gif)}.x-resizable-over .x-resizable-handle-south,.x-resizable-pinned .x-resizable-handle-south,.x-resizable-over .x-resizable-handle-north,.x-resizable-pinned .x-resizable-handle-north{background-image:url(../../scripts/ext/resources/images/gray/sizer/s-handle.gif)}.x-resizable-over .x-resizable-handle-north,.x-resizable-pinned .x-resizable-handle-north{background-image:url(../../scripts/ext/resources/images/gray/sizer/s-handle.gif)}.x-resizable-over .x-resizable-handle-southeast,.x-resizable-pinned .x-resizable-handle-southeast{background-image:url(../../scripts/ext/resources/images/gray/sizer/se-handle.gif)}.x-resizable-over .x-resizable-handle-northwest,.x-resizable-pinned .x-resizable-handle-northwest{background-image:url(../../scripts/ext/resources/images/gray/sizer/nw-handle.gif)}.x-resizable-over .x-resizable-handle-northeast,.x-resizable-pinned .x-resizable-handle-northeast{background-image:url(../../scripts/ext/resources/images/gray/sizer/ne-handle.gif)}.x-resizable-over .x-resizable-handle-southwest,.x-resizable-pinned .x-resizable-handle-southwest{background-image:url(../../scripts/ext/resources/images/gray/sizer/sw-handle.gif)}.x-resizable-proxy{border-color:#565656}.x-resizable-overlay{background-color:#fff}.x-grid3{background-color:#fff}.x-grid-panel .x-panel-mc .x-panel-body{border-color:#d0d0d0}.x-grid3-row td,.x-grid3-summary-row td{font:normal 11px/13px arial,tahoma,helvetica,sans-serif}.x-grid3-hd-row td{font:normal 11px/15px arial,tahoma,helvetica,sans-serif}.x-grid3-hd-row td{border-left-color:#eee;border-right-color:#d0d0d0}.x-grid-row-loading{background-color:#fff;background-image:url(../../scripts/ext/resources/images/default/shared/loading-balls.gif)}.x-grid3-row{border-color:#ededed;border-top-color:#fff}.x-grid3-row-alt{background-color:#fafafa}.x-grid3-row-over{border-color:#ddd;background-color:#efefef;background-image:url(../../scripts/ext/resources/images/default/grid/row-over.gif)}.x-grid3-resize-proxy{background-color:#777}.x-grid3-resize-marker{background-color:#777}.x-grid3-header{background-color:#f9f9f9;background-image:url(../../scripts/ext/resources/images/gray/grid/grid3-hrow2.gif)}.x-grid3-header-pop{border-left-color:#d0d0d0}.x-grid3-header-pop-inner{border-left-color:#eee;background-image:url(../../scripts/ext/resources/images/default/grid/hd-pop.gif)}td.x-grid3-hd-over,td.sort-desc,td.sort-asc,td.x-grid3-hd-menu-open{border-left-color:#ACACAC;border-right-color:#ACACAC}td.x-grid3-hd-over .x-grid3-hd-inner,td.sort-desc .x-grid3-hd-inner,td.sort-asc .x-grid3-hd-inner,td.x-grid3-hd-menu-open .x-grid3-hd-inner{background-color:#f9f9f9;background-image:url(../../scripts/ext/resources/images/gray/grid/grid3-hrow-over2.gif)}.sort-asc .x-grid3-sort-icon{background-image:url(../../scripts/ext/resources/images/gray/grid/sort_asc.gif)}.sort-desc .x-grid3-sort-icon{background-image:url(../../scripts/ext/resources/images/gray/grid/sort_desc.gif)}.x-grid3-cell-text,.x-grid3-hd-text{color:#000}.x-grid3-split{background-image:url(../../scripts/ext/resources/images/default/grid/grid-split.gif)}.x-grid3-hd-text{color:#333}.x-dd-drag-proxy .x-grid3-hd-inner{background-color:#f9f9f9;background-image:url(../../scripts/ext/resources/images/gray/grid/grid3-hrow-over2.gif);border-color:#ACACAC}.col-move-top{background-image:url(../../scripts/ext/resources/images/gray/grid/col-move-top.gif)}.col-move-bottom{background-image:url(../../scripts/ext/resources/images/gray/grid/col-move-bottom.gif)}.x-grid3-row-selected{background-color:#CCCCCC !important;background-image:none;border-color:#ACACAC}.x-grid3-cell-selected{background-color:#CBCBCB !important;color:#000}.x-grid3-cell-selected span{color:#000 !important}.x-grid3-cell-selected .x-grid3-cell-text{color:#000}.x-grid3-locked td.x-grid3-row-marker,.x-grid3-locked .x-grid3-row-selected td.x-grid3-row-marker{background-color:#ebeadb !important;background-image:url(../../scripts/ext/resources/images/default/grid/grid-hrow.gif) !important;color:#000;border-top-color:#fff;border-right-color:#6fa0df !important}.x-grid3-locked td.x-grid3-row-marker div,.x-grid3-locked .x-grid3-row-selected td.x-grid3-row-marker div{color:#333 !important}.x-grid3-dirty-cell{background-image:url(../../scripts/ext/resources/images/default/grid/dirty.gif)}.x-grid3-topbar,.x-grid3-bottombar{font:normal 11px arial,tahoma,helvetica,sans-serif}.x-grid3-bottombar .x-toolbar{border-top-color:#a9bfd3}.x-props-grid .x-grid3-td-name .x-grid3-cell-inner{background-image:url(../../scripts/ext/resources/images/default/grid/grid3-special-col-bg.gif) !important;color:#000 !important}.x-props-grid .x-grid3-body .x-grid3-td-name{background-color:#fff !important;border-right-color:#eee}.xg-hmenu-sort-asc .x-menu-item-icon{background-image:url(../../scripts/ext/resources/images/default/grid/hmenu-asc.gif)}.xg-hmenu-sort-desc .x-menu-item-icon{background-image:url(../../scripts/ext/resources/images/default/grid/hmenu-desc.gif)}.xg-hmenu-lock .x-menu-item-icon{background-image:url(../../scripts/ext/resources/images/default/grid/hmenu-lock.gif)}.xg-hmenu-unlock .x-menu-item-icon{background-image:url(../../scripts/ext/resources/images/default/grid/hmenu-unlock.gif)}.x-grid3-hd-btn{background-color:#f9f9f9;background-image:url(../../scripts/ext/resources/images/gray/grid/grid3-hd-btn.gif)}.x-grid3-body .x-grid3-td-expander{background-image:url(../../scripts/ext/resources/images/default/grid/grid3-special-col-bg.gif)}.x-grid3-row-expander{background-image:url(../../scripts/ext/resources/images/gray/grid/row-expand-sprite.gif)}.x-grid3-body .x-grid3-td-checker{background-image:url(../../scripts/ext/resources/images/default/grid/grid3-special-col-bg.gif)}.x-grid3-row-checker,.x-grid3-hd-checker{background-image:url(../../scripts/ext/resources/images/default/grid/row-check-sprite.gif)}.x-grid3-body .x-grid3-td-numberer{background-image:url(../../scripts/ext/resources/images/default/grid/grid3-special-col-bg.gif)}.x-grid3-body .x-grid3-td-numberer .x-grid3-cell-inner{color:#444}.x-grid3-body .x-grid3-td-row-icon{background-image:url(../../scripts/ext/resources/images/default/grid/grid3-special-col-bg.gif)}.x-grid3-body .x-grid3-row-selected .x-grid3-td-numberer,.x-grid3-body .x-grid3-row-selected .x-grid3-td-checker,.x-grid3-body .x-grid3-row-selected .x-grid3-td-expander{background-image:url(../../scripts/ext/resources/images/gray/grid/grid3-special-col-sel-bg.gif)}.x-grid3-check-col{background-image:url(../../scripts/ext/resources/images/default/menu/unchecked.gif)}.x-grid3-check-col-on{background-image:url(../../scripts/ext/resources/images/default/menu/checked.gif)}.x-grid-group,.x-grid-group-body,.x-grid-group-hd{zoom:1}.x-grid-group-hd{border-bottom-color:#d0d0d0}.x-grid-group-hd div.x-grid-group-title{background-image:url(../../scripts/ext/resources/images/gray/grid/group-collapse.gif);color:#5F5F5F;font:bold 11px tahoma,arial,helvetica,sans-serif}.x-grid-group-collapsed .x-grid-group-hd div.x-grid-group-title{background-image:url(../../scripts/ext/resources/images/gray/grid/group-expand.gif)}.x-group-by-icon{background-image:url(../../scripts/ext/resources/images/default/grid/group-by.gif)}.x-cols-icon{background-image:url(../../scripts/ext/resources/images/default/grid/columns.gif)}.x-show-groups-icon{background-image:url(../../scripts/ext/resources/images/default/grid/group-by.gif)}.x-grid-empty{color:gray;font:normal 11px tahoma,arial,helvetica,sans-serif}.x-grid-with-col-lines .x-grid3-row td.x-grid3-cell{border-right-color:#ededed}.x-grid-with-col-lines .x-grid3-row{border-top-color:#ededed}.x-grid-with-col-lines .x-grid3-row-selected{border-top-color:#B9B9B9}.x-pivotgrid .x-grid3-header-offset table td{background:url(../../scripts/ext/resources/images/gray/grid/grid3-hrow2.gif) repeat-x 50% 100%;border-left:1px solid;border-right:1px solid;border-left-color:#D0D0D0;border-right-color:#D0D0D0}.x-pivotgrid .x-grid3-row-headers{background-color:#f9f9f9}.x-pivotgrid .x-grid3-row-headers table td{background:#EEE url(../../scripts/ext/resources/images/default/grid/grid3-rowheader.gif) repeat-x left top;border-left:1px solid;border-right:1px solid;border-left-color:#EEE;border-right-color:#D0D0D0;border-bottom:1px solid;border-bottom-color:#D0D0D0;height:18px}.x-dd-drag-ghost{color:#000;font:normal 11px arial,helvetica,sans-serif;border-color:#ddd #bbb #bbb #ddd;background-color:#fff}.x-dd-drop-nodrop .x-dd-drop-icon{background-image:url(../../scripts/ext/resources/images/default/dd/drop-no.gif)}.x-dd-drop-ok .x-dd-drop-icon{background-image:url(../../scripts/ext/resources/images/default/dd/drop-yes.gif)}.x-dd-drop-ok-add .x-dd-drop-icon{background-image:url(../../scripts/ext/resources/images/default/dd/drop-add.gif)}.x-view-selector{background-color:#D6D6D6;border-color:#888888}.x-tree-node-expanded .x-tree-node-icon{background-image:url(../../scripts/ext/resources/images/default/tree/folder-open.gif)}.x-tree-node-leaf .x-tree-node-icon{background-image:url(../../scripts/ext/resources/images/default/tree/leaf.gif)}.x-tree-node-collapsed .x-tree-node-icon{background-image:url(../../scripts/ext/resources/images/default/tree/folder.gif)}.x-tree-node-loading .x-tree-node-icon{background-image:url(../../scripts/ext/resources/images/default/tree/loading.gif) !important}.x-tree-node .x-tree-node-inline-icon{background-image:none}.x-tree-node-loading a span{font-style:italic;color:#444444}.ext-ie .x-tree-node-el input{width:15px;height:15px}.x-tree-lines .x-tree-elbow{background-image:url(../../scripts/ext/resources/images/default/tree/elbow.gif)}.x-tree-lines .x-tree-elbow-plus{background-image:url(../../scripts/ext/resources/images/default/tree/elbow-plus.gif)}.x-tree-lines .x-tree-elbow-minus{background-image:url(../../scripts/ext/resources/images/default/tree/elbow-minus.gif)}.x-tree-lines .x-tree-elbow-end{background-image:url(../../scripts/ext/resources/images/default/tree/elbow-end.gif)}.x-tree-lines .x-tree-elbow-end-plus{background-image:url(../../scripts/ext/resources/images/gray/tree/elbow-end-plus.gif)}.x-tree-lines .x-tree-elbow-end-minus{background-image:url(../../scripts/ext/resources/images/gray/tree/elbow-end-minus.gif)}.x-tree-lines .x-tree-elbow-line{background-image:url(../../scripts/ext/resources/images/default/tree/elbow-line.gif)}.x-tree-no-lines .x-tree-elbow-plus{background-image:url(../../scripts/ext/resources/images/default/tree/elbow-plus-nl.gif)}.x-tree-no-lines .x-tree-elbow-minus{background-image:url(../../scripts/ext/resources/images/default/tree/elbow-minus-nl.gif)}.x-tree-no-lines .x-tree-elbow-end-plus{background-image:url(../../scripts/ext/resources/images/gray/tree/elbow-end-plus-nl.gif)}.x-tree-no-lines .x-tree-elbow-end-minus{background-image:url(../../scripts/ext/resources/images/gray/tree/elbow-end-minus-nl.gif)}.x-tree-arrows .x-tree-elbow-plus{background-image:url(../../scripts/ext/resources/images/gray/tree/arrows.gif)}.x-tree-arrows .x-tree-elbow-minus{background-image:url(../../scripts/ext/resources/images/gray/tree/arrows.gif)}.x-tree-arrows .x-tree-elbow-end-plus{background-image:url(../../scripts/ext/resources/images/gray/tree/arrows.gif)}.x-tree-arrows .x-tree-elbow-end-minus{background-image:url(../../scripts/ext/resources/images/gray/tree/arrows.gif)}.x-tree-node{color:#000;font:normal 11px arial,tahoma,helvetica,sans-serif}.x-tree-node a,.x-dd-drag-ghost a{color:#000}.x-tree-node a span,.x-dd-drag-ghost a span{color:#000}.x-tree-node .x-tree-node-disabled a span{color:gray !important}.x-tree-node div.x-tree-drag-insert-below{border-bottom-color:#36c}.x-tree-node div.x-tree-drag-insert-above{border-top-color:#36c}.x-tree-dd-underline .x-tree-node div.x-tree-drag-insert-below a{border-bottom-color:#36c}.x-tree-dd-underline .x-tree-node div.x-tree-drag-insert-above a{border-top-color:#36c}.x-tree-node .x-tree-drag-append a span{background-color:#ddd;border-color:gray}.x-tree-node .x-tree-node-over{background-color:#eee}.x-tree-node .x-tree-selected{background-color:#ddd}.x-tree-drop-ok-append .x-dd-drop-icon{background-image:url(../../scripts/ext/resources/images/default/tree/drop-add.gif)}.x-tree-drop-ok-above .x-dd-drop-icon{background-image:url(../../scripts/ext/resources/images/default/tree/drop-over.gif)}.x-tree-drop-ok-below .x-dd-drop-icon{background-image:url(../../scripts/ext/resources/images/default/tree/drop-under.gif)}.x-tree-drop-ok-between .x-dd-drop-icon{background-image:url(../../scripts/ext/resources/images/default/tree/drop-between.gif)}.x-date-picker{border-color:#585858;background-color:#fff}.x-date-middle,.x-date-left,.x-date-right{background-image:url(../../scripts/ext/resources/images/gray/shared/hd-sprite.gif);color:#fff;font:bold 11px "sans serif",tahoma,verdana,helvetica}.x-date-middle .x-btn .x-btn-text{color:#fff}.x-date-middle .x-btn-mc em.x-btn-arrow{background-image:url(../../scripts/ext/resources/images/gray/toolbar/btn-arrow-light.gif)}.x-date-right a{background-image:url(../../scripts/ext/resources/images/gray/shared/right-btn.gif)}.x-date-left a{background-image:url(../../scripts/ext/resources/images/gray/shared/left-btn.gif)}.x-date-inner th{background-color:#D8D8D8;background-image:url(../../scripts/ext/resources/images/gray/panel/white-top-bottom.gif);border-bottom-color:#AFAFAF;font:normal 10px arial,helvetica,tahoma,sans-serif;color:#595959}.x-date-inner td{border-color:#fff}.x-date-inner a{font:normal 11px arial,helvetica,tahoma,sans-serif;color:#000}.x-date-inner .x-date-active{color:#000}.x-date-inner .x-date-selected a{background-image:none;background-color:#D8D8D8;border-color:#DCDCDC}.x-date-inner .x-date-today a{border-color:darkred}.x-date-inner .x-date-selected span{font-weight:bold}.x-date-inner .x-date-prevday a,.x-date-inner .x-date-nextday a{color:#aaa}.x-date-bottom{border-top-color:#AFAFAF;background-color:#D8D8D8;background:#D8D8D8 url(../../scripts/ext/resources/images/gray/panel/white-top-bottom.gif) 0 -2px}.x-date-inner a:hover,.x-date-inner .x-date-disabled a:hover{color:#000;background-color:#D8D8D8}.x-date-inner .x-date-disabled a{background-color:#eee;color:#bbb}.x-date-mmenu{background-color:#eee !important}.x-date-mmenu .x-menu-item{font-size:10px;color:#000}.x-date-mp{background-color:#fff}.x-date-mp td{font:normal 11px arial,helvetica,tahoma,sans-serif}.x-date-mp-btns button{background-color:#4E565F;color:#fff;border-color:#C0C0C0 #434343 #434343 #C0C0C0;font:normal 11px arial,helvetica,tahoma,sans-serif}.x-date-mp-btns{background-color:#D8D8D8;background:#D8D8D8 url(../../scripts/ext/resources/images/gray/panel/white-top-bottom.gif) 0 -2px}.x-date-mp-btns td{border-top-color:#AFAFAF}td.x-date-mp-month a,td.x-date-mp-year a{color:#333}td.x-date-mp-month a:hover,td.x-date-mp-year a:hover{color:#333;background-color:#FDFDFD}td.x-date-mp-sel a{background-color:#D8D8D8;background:#D8D8D8 url(../../scripts/ext/resources/images/gray/panel/white-top-bottom.gif) 0 -2px;border-color:#DCDCDC}.x-date-mp-ybtn a{background-image:url(../../scripts/ext/resources/images/gray/panel/tool-sprites.gif)}td.x-date-mp-sep{border-right-color:#D7D7D7}.x-tip .x-tip-close{background-image:url(../../scripts/ext/resources/images/gray/qtip/close.gif)}.x-tip .x-tip-tc,.x-tip .x-tip-tl,.x-tip .x-tip-tr,.x-tip .x-tip-bc,.x-tip .x-tip-bl,.x-tip .x-tip-br,.x-tip .x-tip-ml,.x-tip .x-tip-mr{background-image:url(../../scripts/ext/resources/images/gray/qtip/tip-sprite.gif)}.x-tip .x-tip-mc{font:normal 11px tahoma,arial,helvetica,sans-serif}.x-tip .x-tip-ml{background-color:#fff}.x-tip .x-tip-header-text{font:bold 11px tahoma,arial,helvetica,sans-serif;color:#444}.x-tip .x-tip-body{font:normal 11px tahoma,arial,helvetica,sans-serif;color:#444}.x-form-invalid-tip .x-tip-tc,.x-form-invalid-tip .x-tip-tl,.x-form-invalid-tip .x-tip-tr,.x-form-invalid-tip .x-tip-bc,.x-form-invalid-tip .x-tip-bl,.x-form-invalid-tip .x-tip-br,.x-form-invalid-tip .x-tip-ml,.x-form-invalid-tip .x-tip-mr{background-image:url(../../scripts/ext/resources/images/default/form/error-tip-corners.gif)}.x-form-invalid-tip .x-tip-body{background-image:url(../../scripts/ext/resources/images/default/form/exclamation.gif)}.x-tip-anchor{background-image:url(../../scripts/ext/resources/images/gray/qtip/tip-anchor-sprite.gif)}.x-menu{background-color:#f0f0f0;background-image:url(../../scripts/ext/resources/images/default/menu/menu.gif)}.x-menu-floating{border-color:#7D7D7D}.x-menu-nosep{background-image:none}.x-menu-list-item{font:normal 11px arial,tahoma,sans-serif}.x-menu-item-arrow{background-image:url(../../scripts/ext/resources/images/gray/menu/menu-parent.gif)}.x-menu-sep{background-color:#e0e0e0;border-bottom-color:#fff}a.x-menu-item{color:#222}.x-menu-item-active{background-image:url(../../scripts/ext/resources/images/gray/menu/item-over.gif);background-color:#f1f1f1;border-color:#ACACAC}.x-menu-item-active a.x-menu-item{border-color:#ACACAC}.x-menu-check-item .x-menu-item-icon{background-image:url(../../scripts/ext/resources/images/default/menu/unchecked.gif)}.x-menu-item-checked .x-menu-item-icon{background-image:url(../../scripts/ext/resources/images/default/menu/checked.gif)}.x-menu-item-checked .x-menu-group-item .x-menu-item-icon{background-image:url(../../scripts/ext/resources/images/gray/menu/group-checked.gif)}.x-menu-group-item .x-menu-item-icon{background-image:none}.x-menu-plain{background-color:#fff !important}.x-menu .x-date-picker{border-color:#AFAFAF}.x-cycle-menu .x-menu-item-checked{border-color:#B9B9B9 !important;background-color:#F1F1F1}.x-menu-scroller-top{background-image:url(../../scripts/ext/resources/images/default/layout/mini-top.gif)}.x-menu-scroller-bottom{background-image:url(../../scripts/ext/resources/images/default/layout/mini-bottom.gif)}.x-box-tl{background-image:url(../../scripts/ext/resources/images/default/box/corners.gif)}.x-box-tc{background-image:url(../../scripts/ext/resources/images/default/box/tb.gif)}.x-box-tr{background-image:url(../../scripts/ext/resources/images/default/box/corners.gif)}.x-box-ml{background-image:url(../../scripts/ext/resources/images/default/box/l.gif)}.x-box-mc{background-color:#eee;background-image:url(../../scripts/ext/resources/images/default/box/tb.gif);font-family:"Myriad Pro","Myriad Web","Tahoma","Helvetica","Arial",sans-serif;color:#393939;font-size:12px}.x-box-mc h3{font-size:14px;font-weight:bold}.x-box-mr{background-image:url(../../scripts/ext/resources/images/default/box/r.gif)}.x-box-bl{background-image:url(../../scripts/ext/resources/images/default/box/corners.gif)}.x-box-bc{background-image:url(../../scripts/ext/resources/images/default/box/tb.gif)}.x-box-br{background-image:url(../../scripts/ext/resources/images/default/box/corners.gif)}.x-box-blue .x-box-bl,.x-box-blue .x-box-br,.x-box-blue .x-box-tl,.x-box-blue .x-box-tr{background-image:url(../../scripts/ext/resources/images/default/box/corners-blue.gif)}.x-box-blue .x-box-bc,.x-box-blue .x-box-mc,.x-box-blue .x-box-tc{background-image:url(../../scripts/ext/resources/images/default/box/tb-blue.gif)}.x-box-blue .x-box-mc{background-color:#c3daf9}.x-box-blue .x-box-mc h3{color:#17385b}.x-box-blue .x-box-ml{background-image:url(../../scripts/ext/resources/images/default/box/l-blue.gif)}.x-box-blue .x-box-mr{background-image:url(../../scripts/ext/resources/images/default/box/r-blue.gif)}.x-combo-list{border-color:#ccc;background-color:#ddd;font:normal 12px tahoma,arial,helvetica,sans-serif}.x-combo-list-inner{background-color:#fff}.x-combo-list-hd{font:bold 11px tahoma,arial,helvetica,sans-serif;color:#333;background-image:url(../../scripts/ext/resources/images/default/layout/panel-title-light-bg.gif);border-bottom-color:#BCBCBC}.x-resizable-pinned .x-combo-list-inner{border-bottom-color:#BEBEBE}.x-combo-list-item{border-color:#fff}.x-combo-list .x-combo-selected{border-color:#777 !important;background-color:#f0f0f0}.x-combo-list .x-toolbar{border-top-color:#BCBCBC}.x-combo-list-small{font:normal 11px tahoma,arial,helvetica,sans-serif}.x-panel{border-color:#d0d0d0}.x-panel-header{color:#333;font-weight:bold;font-size:11px;font-family:tahoma,arial,verdana,sans-serif;border-color:#d0d0d0;background-image:url(../../scripts/ext/resources/images/gray/panel/white-top-bottom.gif)}.x-panel-body{border-color:#d0d0d0;background-color:#fff}.x-panel-bbar .x-toolbar,.x-panel-tbar .x-toolbar{border-color:#d0d0d0}.x-panel-tbar-noheader .x-toolbar,.x-panel-mc .x-panel-tbar .x-toolbar{border-top-color:#d0d0d0}.x-panel-body-noheader,.x-panel-mc .x-panel-body{border-top-color:#d0d0d0}.x-panel-tl .x-panel-header{color:#333;font:bold 11px tahoma,arial,verdana,sans-serif}.x-panel-tc{background-image:url(../../scripts/ext/resources/images/gray/panel/top-bottom.gif)}.x-panel-tl,.x-panel-tr,.x-panel-bl,.x-panel-br{background-image:url(../../scripts/ext/resources/images/gray/panel/corners-sprite.gif);border-bottom-color:#d0d0d0}.x-panel-bc{background-image:url(../../scripts/ext/resources/images/gray/panel/top-bottom.gif)}.x-panel-mc{font:normal 11px tahoma,arial,helvetica,sans-serif;background-color:#f1f1f1}.x-panel-ml{background-color:#fff;background-image:url(../../scripts/ext/resources/images/gray/panel/left-right.gif)}.x-panel-mr{background-image:url(../../scripts/ext/resources/images/gray/panel/left-right.gif)}.x-tool{background-image:url(../../scripts/ext/resources/images/gray/panel/tool-sprites.gif)}.x-panel-ghost{background-color:#f2f2f2}.x-panel-ghost ul{border-color:#d0d0d0}.x-panel-dd-spacer{border-color:#d0d0d0}.x-panel-fbar td,.x-panel-fbar span,.x-panel-fbar input,.x-panel-fbar div,.x-panel-fbar select,.x-panel-fbar label{font:normal 11px arial,tahoma,helvetica,sans-serif}.x-window-proxy{background-color:#fcfcfc;border-color:#d0d0d0}.x-window-tl .x-window-header{color:#555;font:bold 11px tahoma,arial,verdana,sans-serif}.x-window-tc{background-image:url(../../scripts/ext/resources/images/gray/window/top-bottom.png)}.x-window-tl{background-image:url(../../scripts/ext/resources/images/gray/window/left-corners.png)}.x-window-tr{background-image:url(../../scripts/ext/resources/images/gray/window/right-corners.png)}.x-window-bc{background-image:url(../../scripts/ext/resources/images/gray/window/top-bottom.png)}.x-window-bl{background-image:url(../../scripts/ext/resources/images/gray/window/left-corners.png)}.x-window-br{background-image:url(../../scripts/ext/resources/images/gray/window/right-corners.png)}.x-window-mc{border-color:#d0d0d0;font:normal 11px tahoma,arial,helvetica,sans-serif;background-color:#e8e8e8}.x-window-ml{background-image:url(../../scripts/ext/resources/images/gray/window/left-right.png)}.x-window-mr{background-image:url(../../scripts/ext/resources/images/gray/window/left-right.png)}.x-window-maximized .x-window-tc{background-color:#fff}.x-window-bbar .x-toolbar{border-top-color:#d0d0d0}.x-panel-ghost .x-window-tl{border-bottom-color:#d0d0d0}.x-panel-collapsed .x-window-tl{border-bottom-color:#d0d0d0}.x-dlg-mask{background-color:#ccc}.x-window-plain .x-window-mc{background-color:#E8E8E8;border-color:#D0D0D0 #EEEEEE #EEEEEE #D0D0D0}.x-window-plain .x-window-body{border-color:#EEEEEE #D0D0D0 #D0D0D0 #EEEEEE}body.x-body-masked .x-window-plain .x-window-mc{background-color:#E4E4E4}.x-html-editor-wrap{border-color:#BCBCBC;background-color:#fff}.x-html-editor-tb .x-btn-text{background-image:url(../../scripts/ext/resources/images/default/editor/tb-sprite.gif)}.x-panel-noborder .x-panel-header-noborder{border-bottom-color:#d0d0d0}.x-panel-noborder .x-panel-tbar-noborder .x-toolbar{border-bottom-color:#d0d0d0}.x-panel-noborder .x-panel-bbar-noborder .x-toolbar{border-top-color:#d0d0d0}.x-tab-panel-bbar-noborder .x-toolbar{border-top-color:#d0d0d0}.x-tab-panel-tbar-noborder .x-toolbar{border-bottom-color:#d0d0d0}.x-border-layout-ct{background-color:#f0f0f0}.x-border-layout-ct{background-color:#f0f0f0}.x-accordion-hd{color:#222;font-weight:normal;background-image:url(../../scripts/ext/resources/images/gray/panel/light-hd.gif)}.x-layout-collapsed{background-color:#dfdfdf;border-color:#d0d0d0}.x-layout-collapsed-over{background-color:#e7e7e7}.x-layout-split-west .x-layout-mini{background-image:url(../../scripts/ext/resources/images/default/layout/mini-left.gif)}.x-layout-split-east .x-layout-mini{background-image:url(../../scripts/ext/resources/images/default/layout/mini-right.gif)}.x-layout-split-north .x-layout-mini{background-image:url(../../scripts/ext/resources/images/default/layout/mini-top.gif)}.x-layout-split-south .x-layout-mini{background-image:url(../../scripts/ext/resources/images/default/layout/mini-bottom.gif)}.x-layout-cmini-west .x-layout-mini{background-image:url(../../scripts/ext/resources/images/default/layout/mini-right.gif)}.x-layout-cmini-east .x-layout-mini{background-image:url(../../scripts/ext/resources/images/default/layout/mini-left.gif)}.x-layout-cmini-north .x-layout-mini{background-image:url(../../scripts/ext/resources/images/default/layout/mini-bottom.gif)}.x-layout-cmini-south .x-layout-mini{background-image:url(../../scripts/ext/resources/images/default/layout/mini-top.gif)}.x-progress-wrap{border-color:#8E8E8E}.x-progress-inner{background-color:#E7E7E7;background-image:url(../../scripts/ext/resources/images/gray/qtip/bg.gif)}.x-progress-bar{background-color:#BCBCBC;background-image:url(../../scripts/ext/resources/images/gray/progress/progress-bg.gif);border-top-color:#E2E2E2;border-bottom-color:#A4A4A4;border-right-color:#A4A4A4}.x-progress-text{font-size:11px;font-weight:bold;color:#fff}.x-progress-text-back{color:#5F5F5F}.x-list-header{background-color:#f9f9f9;background-image:url(../../scripts/ext/resources/images/gray/grid/grid3-hrow2.gif)}.x-list-header-inner div em{border-left-color:#ddd;font:normal 11px arial,tahoma,helvetica,sans-serif}.x-list-body dt em{font:normal 11px arial,tahoma,helvetica,sans-serif}.x-list-over{background-color:#eee}.x-list-selected{background-color:#f0f0f0}.x-list-resizer{border-left-color:#555;border-right-color:#555}.x-list-header-inner em.sort-asc,.x-list-header-inner em.sort-desc{background-image:url(../../scripts/ext/resources/images/gray/grid/sort-hd.gif);border-color:#d0d0d0}.x-slider-horz,.x-slider-horz .x-slider-end,.x-slider-horz .x-slider-inner{background-image:url(../../scripts/ext/resources/images/default/slider/slider-bg.png)}.x-slider-horz .x-slider-thumb{background-image:url(../../scripts/ext/resources/images/gray/slider/slider-thumb.png)}.x-slider-vert,.x-slider-vert .x-slider-end,.x-slider-vert .x-slider-inner{background-image:url(../../scripts/ext/resources/images/default/slider/slider-v-bg.png)}.x-slider-vert .x-slider-thumb{background-image:url(../../scripts/ext/resources/images/gray/slider/slider-v-thumb.png)}.x-window-dlg .ext-mb-text,.x-window-dlg .x-window-header-text{font-size:12px}.x-window-dlg .ext-mb-textarea{font:normal 12px tahoma,arial,helvetica,sans-serif}.x-window-dlg .x-msg-box-wait{background-image:url(../../scripts/ext/resources/images/default/grid/loading.gif)}.x-window-dlg .ext-mb-info{background-image:url(../../scripts/ext/resources/images/gray/window/icon-info.gif)}.x-window-dlg .ext-mb-warning{background-image:url(../../scripts/ext/resources/images/gray/window/icon-warning.gif)}.x-window-dlg .ext-mb-question{background-image:url(../../scripts/ext/resources/images/gray/window/icon-question.gif)}.x-window-dlg .ext-mb-error{background-image:url(../../scripts/ext/resources/images/gray/window/icon-error.gif)}@charset "UTF-8";#footer{margin:0 auto;clear:both;float:left;width:100%;text-align:center;border-top:#fff 1px solid}#socialmedia_share{float:left;margin-top:7px;margin-left:20px;style:block}.socialmedia_element{float:left;margin-right:8px}#poweredby{float:right;margin-right:20px}#poweredby a{color:#2A485D;text-decoration:none}#twttrHubFrame{left:-9999em}@charset "UTF-8";body.ltr{direction:ltr}body.rtl div{direction:rtl}form label{cursor:pointer}p.legend{margin-bottom:1em}p.legend em{color:#c00;font-style:normal}.form-container{width:100%;overflow:auto;margin-top:5px;margin-bottom:15px}.form-container form{padding:5px;background-color:#fff;border:#eee 1px solid;background-color:#fbfbfb}.form-container p{margin:0.5em 0 0 0}.form-container form p{margin:0}.form-container form p.note{font-style:italic;margin-left:18em;font-size:80%;color:#666}.form-container form input,.form-container form button,.form-container form select,.form-container form textarea{padding:2px;margin:2px 0 2px 0}.form-container form input.string,.form-container form textarea{width:500px}.form-container form input.date{width:auto}#login_form form table,#register_form form table{width:95%}#login_form input.string,#register_form input.string{width:95%}.form-container form input[type="checkbox"],.form-container form input[type="radio"]{margin:2px 5px}.form-container form fieldset{margin:0 0 10px 0;padding:10px;border:#ddd 1px solid;background-color:#fff}.form-container form legend{font-weight:bold;color:#666}.form-container form td.w2p_fl,.item-container form td.w2p_fl{font-weight:bold}.form-container form tr td,.item-container form tr td{padding:3px 0 0 3px}.form-container .controlset label,.form-container .controlset input{display:inline;float:none}.form-container .controlset div{margin-left:15em}.form-container .buttonrow{margin-left:180px}div.hint{position:relative;}label.over{color:#ccc;font-style:italic;position:absolute; left:5px}table.embeddedComponent{border:1px solid #b3b3b3}form table.embeddedComponent td{padding:0 5px;border:solid #b3b3b3;border-width:0 0 1px 0;text-align:left}table.embeddedComponent tr.label-row td{color:#b3b3b3}.form-container form .embeddedComponent input.string,.form-container form .embeddedComponent textarea{width:auto}table.embeddedComponent input.integer{max-width:10rem}.inline-throbber{background-image:url(../../img/indicator.gif);background-repeat:no-repeat;background-position:center;height:16px;width:16px}.inline-add,.inline-dsc,.inline-cnc,.inline-edt,.inline-rdy,.inline-rmv{cursor:pointer;background-repeat:no-repeat;background-position:center;height:23px;width:23px}.inline-add{background-image:url(../../img/crud/add.png)}.inline-dsc{background-image:url(../../img/crud/cancel.png)}.inline-cnc{background-image:url(../../img/crud/cancel.png)}.inline-edt{background-image:url(../../img/crud/edit.png)}.inline-rdy{background-image:url(../../img/crud/apply.png)}.inline-rmv{background-image:url(../../img/crud/remove.png)}.s3_inline_add_resource_link a{margin-left:2px;padding-left:2px}#filter-form{margin:0}#summary-tabs{visibility:hidden}#summary-filter-form{margin:0}#summary-sections #map{margin-top:0;padding:0}.ui-tabs .ui-tabs-panel{padding:2px 5px}textarea.comments{height:50px}textarea.richtext{height:100px}#list-btn-add,.list-btn-add{margin-bottom:10px}#list-add{display:none}#table-container{display:block;width:100%; margin-top:-1px}#table-container .empty{margin-left:10px}.dataTable thead th{ border:1px solid #ccc;border-bottom:1px solid black}.dataTable th,.fixedHeader th{text-align:center;border:1px solid #ccc}.dataTable tr.even td,.dataTable tr.odd td{border:1px solid #ccc;padding:4px 10px}.dt-export-options{float:right;padding-top:5px}.list_formats div{padding:1px;cursor:pointer;height:16px;width:16px;float:right;background-repeat:no-repeat}.export_cap_large{background-image:url(../../img/icon-cap.jpg);height:36px;width:99px}.export_cap{background-image:url(../../img/cap_16.png)}.export_have{background-image:url(../../img/have_16.png)}.export_kml{background-image:url(../../img/kml_icon.png)}.export_map{background-image:url(../../img/map_icon.png)}.export_pdf{background-image:url(../../img/pdficon_small.gif)}.export_rss{background-image:url(../../img/RSS_16.png)}.export_xls{background-image:url(../../img/icon-xls.png)}.export_xml{background-image:url(../../img/icon-xml.png)}.empty{margin-top:30px}div .dataTable_table{overflow:auto;clear:both}.dataTable{width:100%}.dataTable tr td{vertical-align:top}.dataTable.group{background-color:#ddd;border:1px solid #aaa}.dataTable tr.level_1{background-color:#999;color:#def}.dataTable tr.level_1 a{color:#def}.dataTable tr.activeRow.level_1{background-color:#1d70cf}.dataTable tr.level_2{background-color:#ddd;color:#248}.dataTable tr.level_2 a{color:#248}.dataTable tr.activeRow.level_2{background-color:#528dd1}.dataTables_filter{width:auto;float:left !important;margin-bottom:4px}.dataTables_processing{float:left;margin-left:10px}.dataTables_info{width:auto;float:right !important;clear:none !important;margin:7px 0 4px 10px}.dataTables_length{float:right !important;margin-bottom:4px}.dataTables_paginate{float:left;margin:4px 0 4px 0}.paging_full_numbers{width:auto}a.paginate_button,a.paginate_active{text-decoration:none}.sorting_disabled{background:no-repeat scroll right center transparent}.dataTable tr.dtalert .action-btn,.dataTable tr.dtalert .delete-btn{background-color:#d0d004;color:#444420}.dataTable tr.dtwarning .action-btn,.dataTable tr.dtwarning .delete-btn{background-color:#d07060;color:#431}.dataTable tr.dtdisable{text-shadow:#ccc 1px 1px 1px;color:#888}.dataTable-btn{background-color:#ddd;border:1px solid #aaa;border-radius:5px;padding:2px 5px;margin:0 3px;cursor:pointer;*cursor:hand}.dataTable-btn:hover{background-color:#efefef}table.import-item-details{display:none}.pivot-table-contents{overflow:auto}#dl-container{clear:left}div.dl{border-bottom:1px solid #aaa}.dl-header{float:right;padding:3px}.dl-row{clear:both;padding:0;border-top:1px solid #aaa}.dl-item{float:left;padding:3px 5px 3px 5px;width:98.7%}.dl-row.even,.dl-row.even .dl-item{background-color:white}.dl-row.odd,.dl-row.odd .dl-item{background-color:#e2e4ff}.dl-1-cols{width:98%}.dl-2-cols{width:48%}.dl-3-cols{width:31%}.dl-4-cols{width:22%}.dl-field{clear:left}.dl-field-label{margin-right:10px;font-weight:bold}.dl-field-value{}.infscr-loading{float:left;clear:left}.card_1_line,.card_manylines{font-size:12px;padding-top:4px;color:#666;padding-bottom:2px}.card_1_line{height:16px;line-height:normal;margin-bottom:0;text-overflow:none;overflow:hidden}.card_1_line i{margin-right:5px}.item-container{width:100%;overflow:auto;margin:5px 0 5px 0}.default-text{color:#a1a1a1;font-style:italic}ul.ui-autocomplete{z-index:9999 !important}#map{width:100%;overflow:auto}.error,.expired,.req,.req_key{color:red;font-weight:bold}.mapError{border:solid 1px red}.tooltip,.tooltipbody,.stickytip,.htmltip,.ajaxtip{position:static;text-transform:uppercase;height:20px;width:50px;background:none;background:url(../../img/help_off.gif) no-repeat}.tooltip span,.tooltipbody span,.stickytip span,.htmltip .htmltip-content,.ajaxtip span{display:none}.tooltip:hover,.tooltipbody:hover,.stickytip:hover,.htmltip:hover,.ajaxtip:hover{background-color:transparent;height:20px;width:50px;background:url(../../img/help_on.gif) no-repeat}body.popup{background-color:#fbfbfb;min-width:auto;height:auto}#popup{max-width:750px;width:100%;display:none}.loading{background:url(../../img/ajax-loader.gif) center no-repeat !important}#popup .form-container{overflow:inherit}#popup .control-group{padding-right:20px}.alert-success{color:#070;font-weight:bold;text-align:center;border:#070 1px solid;background:url(../../img/dialog-confirmation.png) #e5ffe5 no-repeat 5px 5px;margin-top:0.0em;margin-bottom:0.5em;padding-left:30px;padding-right:20px;padding-top:1.0em;padding-bottom:1.0em;cursor:pointer;clear:left}.alert-success p em{color:#070}.alert-error{color:#c00;font-weight:bold;text-align:center;border:#c00 1px solid;background:url(../../img/dialog-error.png) #ffe5e5 no-repeat 5px 5px;margin-top:0.0em;margin-bottom:0.5em;padding-left:30px;padding-right:20px;padding-top:1.0em;padding-bottom:1.0em;cursor:pointer;clear:left}.alert-error p em{color:#c00}.alert-info{color:#748d8e;font-weight:bold;text-align:center;border:#9ed8d7 1px solid;background:url(../../img/dialog-information.png) #ecfdff no-repeat 5px 5px;margin-top:0.0em;margin-bottom:0.5em;padding-left:30px;padding-right:20px;padding-top:1.0em;padding-bottom:1.0em;cursor:pointer;clear:left}.alert-info p em{color:#748d8e}.alert-warning{color:#c00;font-weight:bold;text-align:center;border:#fc6 1px solid;background:url(../../img/dialog-warning.png) #ffc no-repeat 5px 5px;margin-top:0.0em;margin-bottom:0.5em;padding-left:30px;padding-right:20px;padding-top:1.0em;padding-bottom:1.0em;cursor:pointer;clear:left}.alert-warning p em{color:#c00}.alert button.close{background:none repeat scroll 0 0 rgba(0,0,0,0);border:0 none;cursor:pointer;padding:0;display:inline;margin:0 0.3rem}.throbber,.layer_throbber,.s3-twitter-throbber,.map_loader{background-image:url(../../img/ajax-loader.gif);background-repeat:no-repeat;height:32px;width:32px}.throbber{margin-bottom:-16px;padding:0 0 0 10px}.input_throbber{background-size:60% !important;height:24px;width:24px;display:inline-block;margin:0 0 -11px -24px}.s3-twitter-throbber{height:0px;margin:66px 0 0 42px;padding:20px;width:0px}#rheader{margin-bottom:0.75em}#rheader th,#rheader td{text-align:left;padding:0.1rem 0.5rem 0.05rem 0;white-space:pre-line}#rheader th{font-weight:bold}#rheader td{padding-right:1.25rem}div.tabs{width:100%;clear:left;height:1.5em;padding:8px 0 2px 0;margin:5px 0 0 0;text-align:left;border-bottom:1px solid #3286e2}div.tabs span{float:left;border-radius:3px 3px 0 0}span.tab_last,span.tab_other{background:#3286e2;border-color:#3286e2;border-width:2px 1px 0 3px;border-style:solid;margin-right:3px;padding-right:3px}div.tabs span a{color:#fff;text-decoration:none}div.tabs span.tab_here{display:inline;position:relative;bottom:0;background:#f0f3f4;border-width:2px 2px 0 3px;border-style:solid;border-color:#69c;padding:1px 6px 0 5px;margin-right:5px;font-weight:bold}form div.tabs span.tab_here{background:#fff;border-bottom:2px solid #fff}div.tabs span.tab_here a{color:#069}span.tab_last a:hover,span.tab_other a:hover{color:#fff;background:transparent}span.tab_last:hover,span.tab_other:hover{background:#164b8b;border-color:#164b8b}span.tab_prev_active{border:1px solid #69c;color:#069;border-bottom:0}span.tab_next_active{border:1px solid #69c;color:#069;border-bottom:0}span.tab_prev_inactive{border:1px solid #bbb;color:#bbb;border-bottom:0}span.tab_next_inactive{border:1px solid #bbb;color:#bbb;border-bottom:0}span.tab_prev_active a,span.tab_next_active a{color:#069;text-decoration:None}span.tab_prev_inactive a,span.tab_next_inactive a{color:#bbb;text-decoration:None}#component{float:left;width:100%}#rfooter{padding:15px 0;clear:both}#last_update{text-align:right;font-style:italic;font-size:80%;color:#666;float:right;clear:right}.authorinfo{font-style:italic;font-size:80%;color:#666}.action-btn,.delete-btn-ajax,.delete-btn{cursor:pointer;line-height:1.5;text-decoration:none;color:#fff;background-color:#3286e2;border:1px solid #4c95e6;border-bottom:2px solid #164b8b;border-top:1px solid #5f9eeb;padding:2px 4px 2px 4px;margin:2px;z-index:500;white-space:nowrap;border-radius:2px}.action-btn:hover,.delete-btn-ajax:hover,.delete-btn:hover,.action-btn:focus,.delete-btn-ajax:focus,.delete-btn:focus{text-decoration:none;color:#fff;background-color:#164b8b}#delete-btn{margin-bottom:8px}#markDuplicate{float:right;clear:right;padding-bottom:8px}.cancel-btn{padding-left:10px}.action-lnk{font-size:85%;margin-left:15px;cursor:pointer}.action-lnk:first-child{margin-left:0}.form-toggle,.form-toggle:hover{text-decoration:none}.form-toggle i{margin-left:3px}.sublabels{font-size:85%}.plus{position:static;height:16px;width:16px;background-image:url(../../img/icon_blue_plus15px.png)}.minus{position:static;height:16px;width:16px;background-image:url(../../img/icon_blue_minus15px.png)}.expand{height:16px;width:16px;float:left;background-image:url(../../img/jquery-ui/ui-icons_222222_256x240.png);background-position:-64px -16px;white-space:nowrap}.expanded{height:16px;width:16px;float:left;background-image:url(../../img/jquery-ui/ui-icons_222222_256x240.png);background-position:-32px -16px;white-space:nowrap}#select_from_registry_row td{padding:8px}.box_top,.box_top_inner{border-top:#bbb 1px solid}.box_top{padding-top:8px}.box_top_inner{padding-top:0.2rem;padding-bottom:0.5rem}form table td.box_top_td{padding-top:8px}.box_top label,.box_top_inner label{display:inline-block}.box_bottom{border-bottom:#bbb 1px solid;padding-bottom:8px}form table tr.box_bottom td{border-bottom:#bbb 1px solid;padding-bottom:8px}.form-horizontal .control-group.box_top{margin:15px 0 0;max-width:680px}.form-horizontal .control-group.box_bottom{margin-bottom:8px;max-width:680px}.add_person_edit_bar{cursor:pointer;display:inline-block;padding-left:1.2rem}.add_person_edit_bar a{text-decoration:none}td.subheading{padding-top:10px !important;padding-bottom:5px;border-bottom:thin solid #bbb;font-weight:bold}tr.after_subheading td{padding-top:10px !important}select[disabled='disabled'],input[disabled='disabled']{background:#eee;color:#333;cursor:default}li input + a{text-decoration:none}.rfilter{float:left;padding:10px 20px 10px 10px}#comments{margin:0;padding:0;list-style:none outside none}#comments ul,ol{padding-left:20px;list-style:none outside none}#comments li{padding:10px 0 0}#comments li a.jcollapsible:hover{background:none}#comments div.comment-text ul{list-style:disc outside none}#comments div.comment-text ol{list-style:decimal outside none}#comments div.comment-text li{padding:0}#comments div.comment-body{white-space:pre-line}#comments div.comment-body p{margin-left:25px}#comments em{font-style:italic}#comments strong{font-weight:bold}#comment-form{width:390px;border:1px #9c9c9c dashed;padding:5px;margin-top:5px}.avatar{background:none repeat scroll 0 0 #fff;border-bottom:1px solid #d7d7d7;border-left:1px solid #f2f2f2;border-right:1px solid #f2f2f2;float:left;height:55px;padding:4px;width:55px}.rheader-avatar{float:left;clear:right;padding-bottom:5px;padding-right:10px}.comment-box{overflow:hidden;padding:15px 0;background:none repeat scroll 0 0 #fff;display:block;overflow:hidden;padding:10px;margin-left:15px}.comment-text{padding:0 0 0 20px;float:left}.comment-text div{white-space:pre-line}.comment-header{margin:0 0 10px 0}.comment-footer{clear:left}.comment-date{font-size:11px;margin:0 0 10px 0}.showall{display:none;position:absolute;border-style:solid;background-color:#ffc;padding:5px;margin:0 20px 0 -50px}#template_sections{margin-right:10px}#template_sections li,#master_sections li{list-style:none}.ui-droppable{padding-bottom:25px}li.ui-draggable:hover,li.ui-draggable-dragging{cursor:pointer;list-style:none;padding:3px;border:solid 1px #bbb;background:none repeat scroll 0 0 #cfdde7}.imagecrop-drag{font-weight:bold;text-align:center;padding:3em 0;margin:1em 0;color:#555;border:2px dashed #555;border-radius:7px;cursor:default}.imagecrop-drag.hover{border-style:solid;background-color:#F7F8F9}.imagecrop-btn{display:none;cursor:pointer}#show-dialog-btn{border:1px solid #efefef;margin:10px;padding:10px}.req_status_none{color:red;font-weight:bold}.req_status_partial{color:darkorange;font-weight:bold}.req_status_complete{color:green;font-weight:bold}.contacts-wrapper{width:500px}.contacts-wrapper p{margin-bottom:0.8em}.contacts-wrapper div.margin{margin-bottom:10px}.contacts-wrapper .contact.saving .editBtn,.contacts-wrapper .contact.edit .editBtn{display:none}ul.x-tab-strip,ul.x-tree-node-ct,ul.x-tree-root-ct{list-style:none outside none}.geocode_success{color:#0a0}.geocode_fail{color:#f00}.s3-grouped-checkboxes-widget-label,.s3-groupedopts-label{margin:10px 0 0 7px;padding-left:20px;height:16px;background:url(../../img/icon_blue_plus15px.png) no-repeat;cursor:pointer}.s3-grouped-checkboxes-widget-label.expanded,.s3-groupedopts-label.expanded{height:16px;width:16px;background:url(../../img/icon_blue_minus15px.png) no-repeat}.s3-grouped-checkboxes-widget .s3-checkboxes-widget,.s3-groupedopts-widget table{margin-left:2em}.form-container form fieldset .s3-checkboxes-widget label,.form-container form fieldset .s3-groupedopts-widget label{white-space:nowrap;text-align:left}.s3-groupedopts-widget label{display:inline;margin-left:5px}.s3-groupedopts-widget tr > td{padding-top:5px;padding-right:10px}.no-options-available{color:#aaa;font-style:italic}.checkboxes-widget-s3 input,.s3-checkboxes-widget input,.s3-groupedopts-widget input{display:inline-block;vertical-align:middle}.s3-checkboxes-widget-filter input{vertical-align:middle}.range-filter-label{font-size:85%}.range-filter-field{display:inline-block;margin-right:0.7rem}.age-filter-widget{display:inline-block}.age-filter-label{display:inline-block;margin-right:0.5rem}.age-filter-unit{display:inline-block;vertical-align:text-bottom;line-height:normal}.filter-form td,#filter_options td{border-top:1px solid #d9d9d9}.filter-form tr:first-child > td,#filter_options tr:first-child > td{border-top:0}.filter-form table.s3-checkboxes-widget td,.filter-form table.s3-groupedopts-widget td,#filter_options table.s3-checkboxes-widget td,#filter_options table.s3searchminmaxwidget td{border-top:0}.filter-form .ui-multiselect.ui-widget.ui-state-default.ui-corner-all,.form-container .ui-multiselect.ui-widget.ui-state-default.ui-corner-all{display:block;min-width:220px}.filter-form .ui-multiselect.ui-widget.ui-state-default.ui-corner-all:first-of-type,.form-container .ui-multiselect.ui-widget.ui-state-default.ui-corner-all:first-of-type{clear:none}.ui-selectmenu-button,.ui-multiselect-menu{min-width:220px}select.multiselect-filter-widget{display:none}.filter-advanced{text-decoration:none;cursor:pointer}.filter-advanced-label{padding-right:4px}.s3-options-filter-anyall label{display:inline;margin-right:0.7rem;font-size:0.7rem}.widget-org-hierarchy-menu{overflow:auto;height:10em;width:36em;position:relative}.widget-org-hierarchy-menu .ui-menu{position:absolute;top:0;bottom:0;overflow:auto;width:32em}.widget-org-hierarchy-menu .ui-menu a{cursor:pointer}.widget-org-hierarchy-crumbs{list-style:none}.widget-org-hierarchy-crumbs li{display:inline;cursor:pointer}.widget-org-hierarchy-crumbs li:after{content:" > "}.widget-org-hierarchy-crumbs li a{text-decoration:none}.widget-org-hierarchy-crumbs li.selected a{text-decoration:none;border-bottom:1px dashed black}.ui-datepicker-trigger{background-image:url(../../img/calendar.gif);background-repeat:no-repeat;height:15px;width:16px;margin-left:3px;border:0;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}.form-container form button.ui-datepicker-trigger{margin-left:3px}td .icon{margin-top:0}option.missing{background-color:yellow}.dms-label{padding:4px;font-weight:bold}.dms-input.invalidinput{border:solid thin red}.resizable-textarea .grippie{ background:url(../../img/grippie.png) no-repeat scroll center 2px #eee;border-color:#ddd;border-right:1px solid #ddd;border-style:solid;border-width:0 1px 1px;cursor:s-resize;height:9px;overflow:hidden}.translation_module_table{width:55%}textarea#project_task_description{height:200px}select#sub_defaulttime_defaulttime_person_id_edit_none{width:150px}input#sub_defaulttime_defaulttime_hours_edit_none{width:60px}.filter-manager-widget{float:left}.fm-load,.fm-save,.fm-delete,.fm-create,.fm-accept,.fm-cancel{float:left;margin-left:5px}div.fm-load,div.fm-save,div.fm-delete,div.fm-create,div.fm-accept,div.fm-cancel{margin-top:7px;width:16px;height:16px}div.fm-load{background:url(../../img/filter/load.png) no-repeat}div.fm-save{background:url(../../img/filter/save.png) no-repeat}div.fm-delete{background:url(../../img/filter/delete.png) no-repeat}div.fm-create{background:url(../../img/filter/create.png) no-repeat}div.fm-accept{background:url(../../img/crud/apply.png) no-repeat}div.fm-cancel{background:url(../../img/crud/cancel.png) no-repeat}.cms-edit{display:table}.datetimepicker{width:110px}.datetimepicker.hide-time{width:75px}.ui-dialog .ui-dialog-content{padding:0 !important}.ui-dialog{padding:0;width:750px !important}body.ltr label.ui-corner-all span{left:10px}body.rtl label.ui-corner-all span{right:10px}.s3-hierarchy-tree.jstree,.s3-hierarchy-header{border:1px solid #ccc;padding:2px 3px 4px 3px;background:white;}.jstree-contextmenu{z-index:9999}.s3-hierarchy-wrapper{z-index:9998}.s3-hierarchy-header{font-size:0.8rem;display:none}.s3-hierarchy-action-node,.s3-hierarchy-none{font-style:italic}form.jeditable-input input{max-width:400px !important}.pt-form legend{font-size:14px;margin-bottom:0;border:0 !important}button.toggle-text{font-size:10px !important;margin-left:12px !important;line-height:1.0}.action-bar{color:#8a8989;font-size:14px;position:relative;top:4px}.action-bar.fleft{margin-right:8px}.action-bar a:hover,.action-bar a:visited:hover{color:#ffa500;text-decoration:none}.maxLength{background-color:#ffcdcd;border:3px solid #d55b5b}.ui-selectmenu-menu .ui-menu.customicons{height:400px}.ui-selectmenu-menu .ui-menu.customicons .ui-menu-item{padding:1em 0 1em 4em}.ui-selectmenu-menu .ui-menu.customicons .ui-menu-item .ui-icon{background-repeat:no-repeat !important;background-position:left top;top:0.1em}.card > .fleft{margin-right:10px}.media-object{display:block}.ajax_more{float:right;width:16px;height:16px;margin:0 2px 2px 0}.ajax_more.collapsed{background:url(../../img/icon_blue_plus15px.png) no-repeat left top}.ajax_more.expanded{background:url(../../img/icon_blue_minus15px.png) no-repeat left top}.s3-timeline{height:400px;border:1px solid #aaa;font-family:Trebuchet MS,sans-serif;font-size:85%}#video-toc{clear:left}.video-header{padding-top:50px}.s3-unmask{margin-left:10px;cursor:pointer}.s3-password-widget{display:inline-block}.s3-twitter-container{width:350px;height:130px}.db-config{padding-top:0.2rem;float:right}.db-config-on,.db-config-off{cursor:pointer;padding:0.125rem;font-size:1rem!important}.db-config-on:hover,.db-config-off:hover{background-color:#7f7f7f}.db-config-on{color:#7f7f7f}.db-config-on:hover{color:white}.db-config-off{color:green}.db-config-off:hover{color:lightgreen}.db-configbar{background-color:#7f7f7f;padding:0.125rem;display:none}.db-configbar-right{float:right}.db-configbar i{color:white;padding:0.125rem;cursor:pointer}.db-configbar i:hover{background-color:silver}.db-config-active{padding:0.125rem;border:1px solid #7f7f7f}.db-has-dialog .db-configbar{background-color:#af4f4f}body.rtl .db-config,body.rtl .db-configbar-right{float:left}@charset "UTF-8";.ir{display:block;text-indent:-999em;overflow:hidden;background-repeat:no-repeat}.hide{display:none !important}.visuallyhidden{position:absolute !important;clip:rect(1px 1px 1px 1px);clip:rect(1px,1px,1px,1px)}.invisible{visibility:hidden}.mini{font-size:80%}.wide{width:100%}.fleft{float:left !important}.fright{float:right !important}.tacenter{text-align:center !important}.taleft{text-align:left !important}.taright{text-align:right !important}.cf:before,.cf:after{content:"\0020";display:block;height:0;visibility:hidden}.cf:after{clear:both}.cf{zoom:1}* html .cf{height:1%}.ltr{direction:ltr}.rtl{direction:rtl}@charset "UTF-8";#login_box{width:100% !important;background:transparent;border:0}#site-title{text-align:center;padding-top:2rem;margin-bottom:3rem}#site-title h2,#site-title h3{font-weight:normal!important;background:none!important;padding:0.35rem 0!important;margin:0!important}#site-title h2{font-size:2.3125rem!important}#login_form h3{font-size:1.6875rem!important;padding:0.35rem 0!important}.row.home-top{min-height:30rem}@font-face{font-family:'FontAwesome';src:url('../../fonts/fontawesome-webfont.eot?v=4.7.0');src:url('../../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url('../../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('../../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'),url('../../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('../../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:0.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul > li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:0.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid 0.08em #eeeeee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)} 100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)} 100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0,mirror=1)";-webkit-transform:scale(-1,1);-ms-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2,mirror=1)";-webkit-transform:scale(1,-1);-ms-transform:scale(1,-1);transform:scale(1,-1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#ffffff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}div.ui-cluetip{font-size:1em}.ui-cluetip-header,.ui-cluetip-content{padding:12px}.ui-cluetip-header{font-size:1em;margin:0;overflow:hidden}.cluetip-title .cluetip-close{float:right;position:relative}.cluetip-close img{border:0}#cluetip-waitimage{width:43px;height:11px;position:absolute;background-image:url(../../img/jquery.cluetip/wait.gif)}.cluetip-arrows{display:none;position:absolute;top:0;left:-11px;width:11px;height:22px;background-repeat:no-repeat;background-position:0 0;border-width:0}.cluetip-extra{display:none}.cluetip-default,.cluetip-default .cluetip-outer{background-color:#d9d9c2}.cluetip-default .ui-cluetip-header{background-color:#87876a}div.cluetip-default .cluetip-arrows{border-width:0;background:transparent none}div.clue-right-default .cluetip-arrows{background-image:url(../../img/jquery.cluetip/darrowleft.gif)}div.clue-left-default .cluetip-arrows{background-image:url(../../img/jquery.cluetip/darrowright.gif);left:100%;margin-right:-11px}div.clue-top-default .cluetip-arrows{background-image:url(../../img/jquery.cluetip/darrowdown.gif);top:100%;left:50%;margin-left:-11px;width:22px;height:11px}div.clue-bottom-default .cluetip-arrows{background-image:url(../../img/jquery.cluetip/darrowup.gif);top:-11px;left:50%;margin-left:-11px;width:22px;height:11px}.cluetip-jtip{background-color:#fff}.cluetip-jtip .cluetip-outer{border:2px solid #ccc;position:relative;background-color:#fff}.cluetip-jtip .cluetip-inner{padding:5px;display:inline-block}.cluetip-jtip div.cluetip-close{text-align:right;margin:0 5px 0;color:#900}.cluetip-jtip .ui-cluetip-header{background-color:#ccc;padding:6px}div.cluetip-jtip .cluetip-arrows{border-width:0;background:transparent none}div.clue-right-jtip .cluetip-arrows{background-image:url(../../img/jquery.cluetip/arrowleft.gif)}div.clue-left-jtip .cluetip-arrows{background-image:url(../../img/jquery.cluetip/arrowright.gif);left:100%;margin-right:-11px}div.clue-top-jtip .cluetip-arrows{background-image:url(../../img/jquery.cluetip/arrowdown.gif);top:100%;left:50%;width:22px;height:11px;margin-left:-11px}div.clue-bottom-jtip .cluetip-arrows{background-image:url(../../img/jquery.cluetip/arrowup.gif);top:-11px;left:50%;width:22px;height:11px;margin-left:-11px}.cluetip-rounded{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;background-color:#fff;border:1px solid #ccc}.cluetip-rounded .cluetip-outer{background-color:#fff}.cluetip-rounded .cluetip-arrows{border-color:#ccc}div.cluetip-rounded .cluetip-arrows{font-size:0;line-height:0%;width:0;height:0;border-style:solid;background:transparent none}div.clue-right-rounded .cluetip-arrows{border-width:11px 11px 11px 0;border-top-color:transparent;border-bottom-color:transparent;border-left-color:transparent}div.clue-left-rounded .cluetip-arrows{left:100%;margin-right:-11px;border-width:11px 0 11px 11px;border-top-color:transparent;border-right-color:transparent;border-bottom-color:transparent}div.clue-top-rounded .cluetip-arrows{top:100%;left:50%;border-width:11px 11px 0 11px;border-right-color:transparent;border-bottom-color:transparent;border-left-color:transparent}.clue-bottom-rounded .cluetip-arrows{top:-11px;left:50%;border-width:0 11px 11px 11px;border-top-color:transparent;border-right-color:transparent;border-left-color:transparent}.cluetip-rounded .cluetip-title,.cluetip-rounded .cluetip-inner{zoom:1}table.dataTable{margin:0 auto;clear:both;width:100%}table.dataTable thead th{padding:3px 18px 3px 10px;border-bottom:1px solid black;font-weight:bold;cursor:pointer;*cursor:hand}table.dataTable tfoot th{padding:3px 18px 3px 10px;border-top:1px solid black;font-weight:bold}table.dataTable td{padding:3px 10px}table.dataTable td.center,table.dataTable td.dataTables_empty{text-align:center}table.dataTable tr.odd{background-color:#E2E4FF}table.dataTable tr.even{background-color:white}table.dataTable tr.odd td.sorting_1{background-color:#D3D6FF}table.dataTable tr.odd td.sorting_2{background-color:#DADCFF}table.dataTable tr.odd td.sorting_3{background-color:#E0E2FF}table.dataTable tr.even td.sorting_1{background-color:#EAEBFF}table.dataTable tr.even td.sorting_2{background-color:#F2F3FF}table.dataTable tr.even td.sorting_3{background-color:#F9F9FF}.dataTables_wrapper{position:relative;clear:both;*zoom:1}.dataTables_length{float:left}.dataTables_filter{float:right;text-align:right}.dataTables_info{clear:both;float:left}.dataTables_paginate{float:right;text-align:right}.paginate_disabled_previous,.paginate_enabled_previous,.paginate_disabled_next,.paginate_enabled_next{height:19px;float:left;cursor:pointer;*cursor:hand;color:#111 !important}.paginate_disabled_previous:hover,.paginate_enabled_previous:hover,.paginate_disabled_next:hover,.paginate_enabled_next:hover{text-decoration:none !important}.paginate_disabled_previous:active,.paginate_enabled_previous:active,.paginate_disabled_next:active,.paginate_enabled_next:active{outline:none}.paginate_disabled_previous,.paginate_disabled_next{color:#666 !important}.paginate_disabled_previous,.paginate_enabled_previous{padding-left:23px}.paginate_disabled_next,.paginate_enabled_next{padding-right:23px;margin-left:10px}.paginate_enabled_previous{background:url('../../img/jquery.dataTables/back_enabled.png') no-repeat top left}.paginate_enabled_previous:hover{background:url('../../img/jquery.dataTables/back_enabled_hover.png') no-repeat top left}.paginate_disabled_previous{background:url('../../img/jquery.dataTables/back_disabled.png') no-repeat top left}.paginate_enabled_next{background:url('../../img/jquery.dataTables/forward_enabled.png') no-repeat top right}.paginate_enabled_next:hover{background:url('../../img/jquery.dataTables/forward_enabled_hover.png') no-repeat top right}.paginate_disabled_next{background:url('../../img/jquery.dataTables/forward_disabled.png') no-repeat top right}.paging_full_numbers{height:22px;line-height:22px}.paging_full_numbers a:active{outline:none}.paging_full_numbers a:hover{text-decoration:none}.paging_full_numbers a.paginate_button,.paging_full_numbers a.paginate_active{border:1px solid #aaa;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;padding:2px 5px;margin:0 3px;cursor:pointer;*cursor:hand;color:#333 !important}.paginate_button_disabled{opacity:0.5;cursor:not-allowed !important}.paging_full_numbers a.paginate_button{background-color:#ddd}.paging_full_numbers a.paginate_button:hover{background-color:#ccc;text-decoration:none !important}.paging_full_numbers a.paginate_active{background-color:#99B3FF}.dataTables_processing{position:absolute;top:50%;left:50%;width:250px;height:30px;margin-left:-125px;margin-top:-15px;padding:14px 0 2px 0;border:1px solid #ddd;text-align:center;color:#999;font-size:14px;background-color:white}.sorting{background:url('../../img/jquery.dataTables/sort_both.png') no-repeat center right}.sorting_asc{background:url('../../img/jquery.dataTables/sort_asc.png') no-repeat center right}.sorting_desc{background:url('../../img/jquery.dataTables/sort_desc.png') no-repeat center right}.sorting_asc_disabled{background:url('../../img/jquery.dataTables/sort_asc_disabled.png') no-repeat center right}.sorting_desc_disabled{background:url('../../img/jquery.dataTables/sort_desc_disabled.png') no-repeat center right}table.dataTable thead th:active,table.dataTable thead td:active{outline:none}.dataTables_scroll{clear:both}.dataTables_scrollBody{*margin-top:-1px;-webkit-overflow-scrolling:touch}table.dataTable.dtr-inline.collapsed tbody td:first-child,table.dataTable.dtr-inline.collapsed tbody th:first-child{position:relative;padding-left:30px;cursor:pointer}table.dataTable.dtr-inline.collapsed tbody td:first-child:before,table.dataTable.dtr-inline.collapsed tbody th:first-child:before{top:8px;left:4px;height:16px;width:16px;display:block;position:absolute;color:white;border:2px solid white;border-radius:16px;text-align:center;line-height:14px;box-shadow:0 0 3px #444;box-sizing:content-box;content:'+';background-color:#31b131}table.dataTable.dtr-inline.collapsed tbody td:first-child.dataTables_empty:before,table.dataTable.dtr-inline.collapsed tbody th:first-child.dataTables_empty:before{display:none}table.dataTable.dtr-inline.collapsed tbody tr.parent td:first-child:before,table.dataTable.dtr-inline.collapsed tbody tr.parent th:first-child:before{content:'-';background-color:#d33333}table.dataTable.dtr-inline.collapsed tbody tr.child td:before{display:none}table.dataTable.dtr-column tbody td.control,table.dataTable.dtr-column tbody th.control{position:relative;cursor:pointer}table.dataTable.dtr-column tbody td.control:before,table.dataTable.dtr-column tbody th.control:before{top:50%;left:50%;height:16px;width:16px;margin-top:-10px;margin-left:-10px;display:block;position:absolute;color:white;border:2px solid white;border-radius:16px;text-align:center;line-height:14px;box-shadow:0 0 3px #444;box-sizing:content-box;content:'+';background-color:#31b131}table.dataTable.dtr-column tbody tr.parent td.control:before,table.dataTable.dtr-column tbody tr.parent th.control:before{content:'-';background-color:#d33333}table.dataTable tr.child{padding:0.5em 1em}table.dataTable tr.child:hover{background:transparent !important}table.dataTable tr.child ul{display:inline-block;list-style-type:none;margin:0;padding:0}table.dataTable tr.child ul li{border-bottom:1px solid #efefef;padding:0.5em 0}table.dataTable tr.child ul li:first-child{padding-top:0}table.dataTable tr.child ul li:last-child{border-bottom:none}table.dataTable tr.child span.dtr-title{display:inline-block;min-width:75px;font-weight:bold}ul.tagit{padding:1px 5px;overflow:auto;margin-left:inherit;margin-right:inherit}ul.tagit li{display:block;float:left;margin:2px 5px 2px 0}ul.tagit li.tagit-choice{position:relative;line-height:inherit}input.tagit-hidden-field{display:none}ul.tagit li.tagit-choice-read-only{padding:.2em .5em .2em .5em} ul.tagit li.tagit-choice-editable{padding:.2em 18px .2em .5em} ul.tagit li.tagit-new{padding:.25em 4px .25em 0}ul.tagit li.tagit-choice a.tagit-label{cursor:pointer;text-decoration:none}ul.tagit li.tagit-choice .tagit-close{cursor:pointer;position:absolute;right:.1em;top:50%;margin-top:-8px;line-height:17px}ul.tagit li.tagit-choice .tagit-close .text-icon{display:none}ul.tagit li.tagit-choice input{display:block;float:left;margin:2px 5px 2px 0}ul.tagit input[type="text"]{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;-moz-box-shadow:none;-webkit-box-shadow:none;box-shadow:none;border:none;margin:0;padding:0;width:inherit;background-color:inherit;outline:none}.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.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:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default !important;pointer-events:none}.ui-icon{display:inline-block;vertical-align:middle;margin-top:-.25em;position:relative;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-icon-block{left:50%;margin-left:-8px;display:block}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-button{padding:.4em 1em;display:inline-block;position:relative;line-height:normal;margin-right:.1em;cursor:pointer;vertical-align:middle;text-align:center;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;overflow:visible}.ui-button,.ui-button:link,.ui-button:visited,.ui-button:hover,.ui-button:active{text-decoration:none}.ui-button-icon-only{width:2em;box-sizing:border-box;text-indent:-9999px;white-space:nowrap}input.ui-button.ui-button-icon-only{text-indent:0}.ui-button-icon-only .ui-icon{position:absolute;top:50%;left:50%;margin-top:-8px;margin-left:-8px}.ui-button.ui-icon-notext .ui-icon{padding:0;width:2.1em;height:2.1em;text-indent:-9999px;white-space:nowrap}input.ui-button.ui-icon-notext .ui-icon{width:auto;height:auto;text-indent:0;white-space:normal;padding:.4em 1em}input.ui-button::-moz-focus-inner,button.ui-button::-moz-focus-inner{border:0;padding:0}.ui-datepicker{min-width:17em;padding:.2em .2em 0;display:none}.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,.ui-datepicker select.ui-datepicker-year{width:45%}.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}.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,.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%;font-size:0}.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,.ui-datepicker-rtl .ui-datepicker-group{float:right}.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-datepicker .ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat;left:.5em;top:.3em}.ui-dialog{position:absolute;top:0;left:0;padding:.2em;outline:0}.ui-dialog .ui-dialog-titlebar{padding:.4em 1em;position:relative}.ui-dialog .ui-dialog-title{float:left;margin:.1em 0;white-space:nowrap;width:90%;overflow:hidden;text-overflow:ellipsis}.ui-dialog .ui-dialog-titlebar-close{position:absolute;right:.3em;top:50%;width:20px;margin:-10px 0 0 0;padding:1px;height:20px}.ui-dialog .ui-dialog-content{position:relative;border:0;padding:.5em 1em;background:none;overflow:auto}.ui-dialog .ui-dialog-buttonpane{text-align:left;border-width:1px 0 0 0;background-image:none;margin-top:.5em;padding:.3em 1em .5em .4em}.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset{float:right}.ui-dialog .ui-dialog-buttonpane button{margin:.5em .4em .5em 0;cursor:pointer}.ui-dialog .ui-resizable-n{height:2px;top:0}.ui-dialog .ui-resizable-e{width:2px;right:0}.ui-dialog .ui-resizable-s{height:2px;bottom:0}.ui-dialog .ui-resizable-w{width:2px;left:0}.ui-dialog .ui-resizable-se,.ui-dialog .ui-resizable-sw,.ui-dialog .ui-resizable-ne,.ui-dialog .ui-resizable-nw{width:7px;height:7px}.ui-dialog .ui-resizable-se{right:0;bottom:0}.ui-dialog .ui-resizable-sw{left:0;bottom:0}.ui-dialog .ui-resizable-ne{right:0;top:0}.ui-dialog .ui-resizable-nw{left:0;top:0}.ui-draggable .ui-dialog-titlebar{cursor:move}.ui-draggable-handle{-ms-touch-action:none;touch-action:none}.ui-menu{list-style:none;padding:0;margin:0;display:block;outline:0}.ui-menu .ui-menu{position:absolute}.ui-menu .ui-menu-item{margin:0;cursor:pointer;list-style-image:url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")}.ui-menu .ui-menu-item-wrapper{position:relative;padding:3px 1em 3px .4em}.ui-menu .ui-menu-divider{margin:5px 0;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-state-focus,.ui-menu .ui-state-active{margin:-1px}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item-wrapper{padding-left:2em}.ui-menu .ui-icon{position:absolute;top:0;bottom:0;left:.2em;margin:auto 0}.ui-menu .ui-menu-icon{left:auto;right:0}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block;-ms-touch-action:none;touch-action:none}.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}.ui-selectmenu-menu{padding:0;margin:0;position:absolute;top:0;left:0;display:none}.ui-selectmenu-menu .ui-menu{overflow:auto;overflow-x:hidden;padding-bottom:1px}.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup{font-size:1em;font-weight:bold;line-height:1.5;padding:2px 0.4em;margin:0.5em 0 0 0;height:auto;border:0}.ui-selectmenu-open{display:block}.ui-selectmenu-text{display:block;margin-right:20px;overflow:hidden;text-overflow:ellipsis}.ui-selectmenu-button.ui-button{text-align:left;white-space:nowrap;width:14em}.ui-selectmenu-icon.ui-icon{float:right;margin-top:0}.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;-ms-touch-action:none;touch-action:none}.ui-slider .ui-slider-range{position:absolute;z-index:1;font-size:.7em;display:block;border:0;background-position:0 0}.ui-slider.ui-state-disabled .ui-slider-handle,.ui-slider.ui-state-disabled .ui-slider-range{filter:inherit}.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}.ui-tabs{position:relative;padding:.2em}.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:0;margin:1px .2em 0 0;border-bottom-width:0;padding:0;white-space:nowrap}.ui-tabs .ui-tabs-nav .ui-tabs-anchor{float:left;padding:.5em 1em;text-decoration:none}.ui-tabs .ui-tabs-nav li.ui-tabs-active{margin-bottom:-1px;padding-bottom:1px}.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor{cursor:text}.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor{cursor:pointer}.ui-tabs .ui-tabs-panel{display:block;border-width:0;padding:1em 1.4em;background:none}.ui-timepicker-inline{display:inline}#ui-timepicker-div{padding:0.2em}.ui-timepicker-table{display:inline-table;width:0}.ui-timepicker-table table{margin:0.15em 0 0 0;border-collapse:collapse}.ui-timepicker-hours,.ui-timepicker-minutes{padding:0.2em}.ui-timepicker-table .ui-timepicker-title{line-height:1.8em;text-align:center}.ui-timepicker-table td{padding:0.1em;width:2.2em}.ui-timepicker-table th.periods{padding:0.1em;width:2.2em}.ui-timepicker-table td span{display:block;padding:0.2em 1.5em 0.2em 0.5em;width:1.2em;text-align:right;text-decoration:none}.ui-timepicker-table td a{display:block;padding:0.2em 0.3em 0.2em 0.5em;cursor:pointer;text-align:right;text-decoration:none}.ui-timepicker .ui-timepicker-buttonpane{background-image:none;margin:.7em 0 0 0;padding:0 .2em;border-left:0;border-right:0;border-bottom:0}.ui-timepicker .ui-timepicker-buttonpane button{margin:.5em .2em .4em;cursor:pointer;padding:.2em .6em .3em .6em;width:auto;overflow:visible}.ui-timepicker .ui-timepicker-close{float:right }.ui-timepicker .ui-timepicker-now{float:left}.ui-timepicker .ui-timepicker-deselect{float:left}body.ltr .ui-multiselect{padding:2px 0 2px 4px;text-align:left }body.ltr .ui-multiselect span.ui-icon{float:right }.ui-multiselect-single .ui-multiselect-checkboxes input{position:absolute !important;top:auto !important}body.ltr .ui-multiselect-single .ui-multiselect-checkboxes input{left:-9999px}.ui-multiselect-single .ui-multiselect-checkboxes label{padding:5px !important }.ui-multiselect-header{margin-bottom:3px;padding:3px 0 3px 4px }.ui-multiselect-header ul{font-size:0.9em }.ui-multiselect-header ul li{float:left;padding:0 10px 0 0 }.ui-multiselect-header a{text-decoration:none }.ui-multiselect-header a:hover{text-decoration:underline }.ui-multiselect-header span.ui-icon{float:left }.ui-multiselect-header li.ui-multiselect-close{float:right;text-align:right;padding-right:0 }.ui-multiselect-menu{display:none;padding:3px;position:absolute;z-index:999999}.ui-multiselect-checkboxes{position:relative ;overflow-y:scroll }.ui-multiselect-checkboxes label{cursor:default;display:block;border:1px solid transparent;padding:3px 1px }.ui-multiselect-checkboxes label input{position:relative;top:1px }.ui-multiselect-checkboxes li{clear:both;font-size:0.9em;padding-right:3px }.ui-multiselect-checkboxes li.ui-multiselect-optgroup-label{text-align:center;font-weight:bold;border-bottom:1px solid }.ui-multiselect-checkboxes li.ui-multiselect-optgroup-label a{display:block;padding:3px;margin:1px 0;text-decoration:none }.ui-multiselect-hasfilter ul{position:relative;top:2px }.ui-multiselect-filter{float:left;margin-right:10px;font-size:11px }.ui-multiselect-filter input{width:100px;font-size:10px;margin-left:5px;height:15px;padding:2px;border:1px solid #292929;-webkit-appearance:textfield;-webkit-box-sizing:content-box}body.rtl .ui-multiselect{direction:rtl;text-align:right;padding:2px 4px 2px 0px}body.rtl .ui-multiselect span.ui-icon{float:left}body.rtl .ui-multiselect-checkboxes{direction:rtl;text-align:right}body.rtl .ui-multiselect-single .ui-multiselect-checkboxes input{right:-9999px}body.rtl .ui-multiselect-menu .ui-state-hover{font-weight:normal}body.rtl button.ui-state-default{font-weight:normal}body.rtl .ui-multiselect-filter{direction:ltr}.ui-timepicker-div .ui-widget-header{margin-bottom:8px}.ui-timepicker-div dl{text-align:left}.ui-timepicker-div dl dt{float:left;clear:left;padding:0 0 0 5px}.ui-timepicker-div dl dd{margin:0 10px 10px 40%}.ui-timepicker-div td{font-size:90%}.ui-tpicker-grid-label{background:none;border:none;margin:0;padding:0}.ui-timepicker-rtl{direction:rtl}.ui-timepicker-rtl dl{text-align:right;padding:0 5px 0 0}.ui-timepicker-rtl dl dt{float:right;clear:right}.ui-timepicker-rtl dl dd{margin:0 40% 10px 10px}.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.ui-widget-content{border:1px solid #d3d3d3}.ui-widget-content{border:1px solid #aaaaaa;background:#ffffff;color:#222222}.ui-widget-content a{color:#222222}.ui-widget-header{border:1px solid #aaaaaa;background:#cccccc url("../../img/jquery.ui/ui-bg_highlight-soft_75_cccccc_1x100.png") 50% 50% repeat-x;color:#222222;font-weight:bold}.ui-widget-header a{color:#222222}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default,.ui-button,html .ui-button.ui-state-disabled:hover,html .ui-button.ui-state-disabled:active{border:1px solid #d3d3d3;background:#e6e6e6 url("../../img/jquery.ui/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,a.ui-button,a:link.ui-button,a:visited.ui-button,.ui-button{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,.ui-button:hover,.ui-button:focus{border:1px solid #999999;background:#dadada url("../../img/jquery.ui/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,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited,a.ui-button:hover,a.ui-button:focus{color:#212121;text-decoration:none}.ui-visual-focus{box-shadow:0 0 3px 1px rgb(94,158,214)}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active,a.ui-button:active,.ui-button:active,.ui-button.ui-state-active:hover{border:1px solid #aaaaaa;background:#ffffff url("../../img/jquery.ui/ui-bg_glass_65_ffffff_1x400.png") 50% 50% repeat-x;font-weight:normal;color:#212121}.ui-icon-background,.ui-state-active .ui-icon-background{border:#aaaaaa;background-color:#212121}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#212121;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #fcefa1;background:#fbf9ee url("../../img/jquery.ui/ui-bg_glass_55_fbf9ee_1x400.png") 50% 50% repeat-x;color:#363636}.ui-state-checked{border:1px solid #fcefa1;background:#fbf9ee}.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("../../img/jquery.ui/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}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url("../../img/jquery.ui/ui-icons_222222_256x240.png")}.ui-widget-header .ui-icon{background-image:url("../../img/jquery.ui/ui-icons_222222_256x240.png")}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon,.ui-button:hover .ui-icon,.ui-button:focus .ui-icon{background-image:url("../../img/jquery.ui/ui-icons_454545_256x240.png")}.ui-state-active .ui-icon,.ui-button:active .ui-icon{background-image:url("../../img/jquery.ui/ui-icons_454545_256x240.png")}.ui-state-highlight .ui-icon,.ui-button .ui-state-highlight.ui-icon{background-image:url("../../img/jquery.ui/ui-icons_2e83ff_256x240.png")}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url("../../img/jquery.ui/ui-icons_cd0a0a_256x240.png")}.ui-button .ui-icon{background-image:url("../../img/jquery.ui/ui-icons_888888_256x240.png")}.ui-icon-blank{background-position:16px 16px}.ui-icon-caret-1-n{background-position:0 0}.ui-icon-caret-1-ne{background-position:-16px 0}.ui-icon-caret-1-e{background-position:-32px 0}.ui-icon-caret-1-se{background-position:-48px 0}.ui-icon-caret-1-s{background-position:-65px 0}.ui-icon-caret-1-sw{background-position:-80px 0}.ui-icon-caret-1-w{background-position:-96px 0}.ui-icon-caret-1-nw{background-position:-112px 0}.ui-icon-caret-2-n-s{background-position:-128px 0}.ui-icon-caret-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:-65px -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:-65px -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:1px -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-on{background-position:-96px -144px}.ui-icon-radio-off{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{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}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:4px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:4px}.ui-widget-overlay{background:#aaaaaa;opacity:.3;filter:Alpha(Opacity=30)}.ui-widget-shadow{-webkit-box-shadow:-8px -8px 8px #aaaaaa;box-shadow:-8px -8px 8px #aaaaaa}div.olMap{z-index:0;padding:0 !important;margin:0 !important;cursor:default}div.olMapViewport{text-align:left}div.olLayerDiv{-moz-user-select:none;-khtml-user-select:none}.olLayerGoogleCopyright{left:2px;bottom:2px}.olLayerGoogleV3.olLayerGoogleCopyright{right:auto !important}.olLayerGooglePoweredBy{left:2px;bottom:15px}.olLayerGoogleV3.olLayerGooglePoweredBy{bottom:15px !important}.olForeignContainer{opacity:1 !important}.olControlAttribution{font-size:smaller;right:3px;bottom:4.5em;position:absolute;display:block}.olControlScale{right:3px;bottom:3em;display:block;position:absolute;font-size:smaller}.olControlScaleLine{display:block;position:absolute;left:10px;bottom:15px;font-size:xx-small}.olControlScaleLineBottom{border:solid 2px black;border-bottom:none;margin-top:-2px;text-align:center}.olControlScaleLineTop{border:solid 2px black;border-top:none;text-align:center}.olControlPermalink{right:3px;bottom:1.5em;display:block;position:absolute;font-size:smaller}div.olControlMousePosition{bottom:0;right:3px;display:block;position:absolute;font-family:Arial;font-size:smaller}.olControlOverviewMapContainer{position:absolute;bottom:0;right:0}.olControlOverviewMapElement{padding:10px 18px 10px 10px;background-color:#00008B;-moz-border-radius:1em 0 0 0}.olControlOverviewMapMinimizeButton,.olControlOverviewMapMaximizeButton{height:18px;width:18px;right:0;bottom:80px;cursor:pointer}.olControlOverviewMapExtentRectangle{overflow:hidden;background-image:url(../../img/gis/openlayers/theme_default/blank.gif);cursor:move;border:2px dotted red}.olControlOverviewMapRectReplacement{overflow:hidden;cursor:move;background-image:url(../../img/gis/openlayers/theme_default/overview_replacement.gif);background-repeat:no-repeat;background-position:center}.olLayerGeoRSSDescription{float:left;width:100%;overflow:auto;font-size:1.0em}.olLayerGeoRSSClose{float:right;color:gray;font-size:1.2em;margin-right:6px;font-family:sans-serif}.olLayerGeoRSSTitle{float:left;font-size:1.2em}.olPopupContent{padding:5px;overflow:auto}.olControlNavigationHistory{background-image:url(../../img/gis/openlayers/theme_default/navigation_history.png);background-repeat:no-repeat;width:24px;height:24px}.olControlNavigationHistoryPreviousItemActive{background-position:0 0}.olControlNavigationHistoryPreviousItemInactive{background-position:0 -24px}.olControlNavigationHistoryNextItemActive{background-position:-24px 0}.olControlNavigationHistoryNextItemInactive{background-position:-24px -24px}div.olControlSaveFeaturesItemActive{background-image:url(../../img/gis/openlayers/theme_default/save_features_on.png);background-repeat:no-repeat;background-position:0 1px}div.olControlSaveFeaturesItemInactive{background-image:url(../../img/gis/openlayers/theme_default/save_features_off.png);background-repeat:no-repeat;background-position:0 1px}.olHandlerBoxZoomBox{border:2px solid red;position:absolute;background-color:white;opacity:0.50;font-size:1px;filter:alpha(opacity=50)}.olHandlerBoxSelectFeature{border:2px solid blue;position:absolute;background-color:white;opacity:0.50;font-size:1px;filter:alpha(opacity=50)}.olControlPanPanel{top:10px;left:5px}.olControlPanPanel div{background-image:url(../../img/gis/openlayers/theme_default/pan-panel.png);height:18px;width:18px;cursor:pointer;position:absolute}.olControlPanPanel .olControlPanNorthItemInactive{top:0;left:9px;background-position:0 0}.olControlPanPanel .olControlPanSouthItemInactive{top:36px;left:9px;background-position:18px 0}.olControlPanPanel .olControlPanWestItemInactive{position:absolute;top:18px;left:0;background-position:0 18px}.olControlPanPanel .olControlPanEastItemInactive{top:18px;left:18px;background-position:18px 18px}.olControlZoomPanel{top:71px;left:14px}.olControlZoomPanel div{background-image:url(../../img/gis/openlayers/theme_default/zoom-panel.png);position:absolute;height:18px;width:18px;cursor:pointer}.olControlZoomPanel .olControlZoomInItemInactive{top:0;left:0;background-position:0 0}.olControlZoomPanel .olControlZoomToMaxExtentItemInactive{top:18px;left:0;background-position:0 -18px}.olControlZoomPanel .olControlZoomOutItemInactive{top:36px;left:0;background-position:0 18px}.olControlPanZoomBar div{font-size:1px}.olPopupCloseBox{background:url(../../img/gis/openlayers/theme_default/close.gif) no-repeat;cursor:pointer}.olFramedCloudPopupContent{padding:5px;overflow:auto}.olControlNoSelect{-moz-user-select:none;-khtml-user-select:none}.olImageLoadError{background-color:pink;opacity:0.5;filter:alpha(opacity=50)}.olCursorWait{cursor:wait}.olDragDown{cursor:move}.olDrawBox{cursor:crosshair}.olControlDragFeatureOver{cursor:move}.olControlDragFeatureActive.olControlDragFeatureOver.olDragDown{cursor:-moz-grabbing}.olControlLayerSwitcher{position:absolute;top:25px;right:0;width:20em;font-family:sans-serif;font-weight:bold;margin-top:3px;margin-left:3px;margin-bottom:3px;font-size:smaller;color:white;background-color:transparent}.olControlLayerSwitcher .layersDiv{padding-top:5px;padding-left:10px;padding-bottom:5px;padding-right:10px;background-color:darkblue}.olControlLayerSwitcher .layersDiv .baseLbl,.olControlLayerSwitcher .layersDiv .dataLbl{margin-top:3px;margin-left:3px;margin-bottom:3px}.olControlLayerSwitcher .layersDiv .baseLayersDiv,.olControlLayerSwitcher .layersDiv .dataLayersDiv{padding-left:10px}.olControlLayerSwitcher .maximizeDiv,.olControlLayerSwitcher .minimizeDiv{width:18px;height:18px;top:5px;right:0;cursor:pointer}.olBingAttribution{color:#DDD}.olBingAttribution.road{color:#333}.olGoogleAttribution.hybrid,.olGoogleAttribution.satellite{color:#EEE}.olGoogleAttribution{color:#333}span.olGoogleAttribution a{color:#77C}span.olGoogleAttribution.hybrid a,span.olGoogleAttribution.satellite a{color:#EEE}.olControlNavToolbar ,.olControlEditingToolbar{margin:5px 5px 0 0}.olControlNavToolbar div,.olControlEditingToolbar div{background-image:url(../../img/gis/openlayers/theme_default/editing_tool_bar.png);background-repeat:no-repeat;margin:0 0 5px 5px;width:24px;height:22px;cursor:pointer}.olControlEditingToolbar{right:0;top:0}.olControlNavToolbar{top:295px;left:9px}.olControlEditingToolbar div{float:right}.olControlNavToolbar .olControlNavigationItemInactive,.olControlEditingToolbar .olControlNavigationItemInactive{background-position:-103px -1px}.olControlNavToolbar .olControlNavigationItemActive ,.olControlEditingToolbar .olControlNavigationItemActive{background-position:-103px -24px}.olControlNavToolbar .olControlZoomBoxItemInactive{background-position:-128px -1px}.olControlNavToolbar .olControlZoomBoxItemActive{background-position:-128px -24px}.olControlEditingToolbar .olControlDrawFeaturePointItemInactive{background-position:-77px -1px}.olControlEditingToolbar .olControlDrawFeaturePointItemActive{background-position:-77px -24px}.olControlEditingToolbar .olControlDrawFeaturePathItemInactive{background-position:-51px -1px}.olControlEditingToolbar .olControlDrawFeaturePathItemActive{background-position:-51px -24px}.olControlEditingToolbar .olControlDrawFeaturePolygonItemInactive{background-position:-26px -1px}.olControlEditingToolbar .olControlDrawFeaturePolygonItemActive{background-position:-26px -24px}div.olControlZoom{position:absolute;top:8px;left:8px;background:rgba(255,255,255,0.4);border-radius:4px;padding:2px}div.olControlZoom a{display:block;margin:1px;padding:0;color:white;font-size:18px;font-family:'Lucida Grande',Verdana,Geneva,Lucida,Arial,Helvetica,sans-serif;font-weight:bold;text-decoration:none;text-align:center;height:22px;width:22px;line-height:19px;background:#130085;background:rgba(0,60,136,0.5);filter:alpha(opacity=80)}div.olControlZoom a:hover{background:#130085;background:rgba(0,60,136,0.7);filter:alpha(opacity=100)}@media only screen and (max-width:600px){div.olControlZoom a:hover{background:rgba(0,60,136,0.5)}}a.olControlZoomIn{border-radius:4px 4px 0 0}a.olControlZoomOut{border-radius:0 0 4px 4px}.olLayerGrid .olTileImage{-webkit-transition:opacity 0.2s linear;-moz-transition:opacity 0.2s linear;-o-transition:opacity 0.2s linear;transition:opacity 0.2s linear}.olTileImage{-webkit-transform:translateZ(0);-moz-transform:translateZ(0);-o-transform:translateZ(0);-ms-transform:translateZ(0);transform:translateZ(0);-webkit-backface-visibility:hidden;-moz-backface-visibility:hidden;-ms-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000;-moz-perspective:1000;-ms-perspective:1000;perspective:1000}.olTileReplacing{display:none}img.olTileImage{max-width:none}@font-face{font-family:'zocial';font-style:normal;font-weight:normal;src:url('../../fonts/zocial-regular-webfont.eot');src:url('../../fonts/zocial-regular-webfont.eot?#iefix') format('embedded-opentype'),url('../../fonts/zocial-regular-webfont.woff') format('woff'),url('../../fonts/zocial-regular-webfont.ttf') format('truetype'),url('../../fonts/zocial-regular-webfont.svg#ZocialRegular') format('svg')}.zocial{border-bottom-color:rgba(0,0,0,0.4);border:1px solid rgba(0,0,0,0.2);color:#fff !important;-moz-box-shadow:inset 0 0.08em 0 rgba(255,255,255,0.5),inset 0 0 0.1em rgba(255,255,255,0.9);-webkit-box-shadow:inset 0 0.08em 0 rgba(255,255,255,0.5),inset 0 0 0.1em rgba(255,255,255,0.9);box-shadow:inset 0 0.08em 0 rgba(255,255,255,0.5),inset 0 0 0.1em rgba(255,255,255,0.9);cursor:pointer;display:inline-block;font-family:"Lucida Grande",Tahoma,sans-serif;font-style:normal !important;font-weight:bold !important;letter-spacing:0;padding:0;position:relative;text-align:center;text-decoration:none !important;text-shadow:0 1px 0 rgba(0,0,0,0.5);-moz-user-select:none !important;-webkit-user-select:none !important;user-select:none !important}.zocial > span:before{border-right:0.075em solid rgba(0,0,0,0.1);-moz-box-shadow:0.075em 0 0 rgba(255,255,255,0.25);-webkit-box-shadow:0.075em 0 0 rgba(255,255,255,0.25);box-shadow:0.075em 0 0 rgba(255,255,255,0.25);content:"";display:block;float:left;font-family:"zocial" !important;font-size:125% !important;line-height:1.65;font-style:normal !important;font-weight:normal !important;margin:0.1em 0.5em 0 0;padding:0 0.5em;text-align:center !important;text-decoration:none !important;text-transform:none !important}.zocial > span{display:block;font-size:80% !important;line-height:2.1;font-weight:bold;padding:0em 1em 0 0;white-space:nowrap}.zocial,.zocial > span{-moz-border-radius:0.2em;-webkit-border-radius:0.2em;border-radius:0.2em;position:relative;z-index:100}.zocial:active{outline:none}.zocial.icon{overflow:hidden;width:1.85em;height:1.85em}.zocial.icon > span:before{padding:0;width:1.85em;height:1.85em}.zocial > span{background:-moz-linear-gradient(top,rgba(255,255,255,0.1),rgba(255,255,255,0.05) 49%,rgba(0,0,0,0.05) 51%,rgba(0,0,0,0.1));background:-webkit-gradient(linear,left top,left bottom,from(rgba(255,255,255,0.1)),color-stop(49%,rgba(255,255,255,0.05)),color-stop(51%,rgba(0,0,0,0.05)),to(rgba(0,0,0,0.1)));background:-webkit-linear-gradient(top,rgba(255,255,255,0.1),rgba(255,255,255,0.05) 49%,rgba(0,0,0,0.05) 51%,rgba(0,0,0,0.1));background:-o-linear-gradient(top,rgba(255,255,255,0.1),rgba(255,255,255,0.05) 49%,rgba(0,0,0,0.05) 51%,rgba(0,0,0,0.1));background:-ms-linear-gradient(top,rgba(255,255,255,0.1),rgba(255,255,255,0.05) 49%,rgba(0,0,0,0.05) 51%,rgba(0,0,0,0.1));background:linear-gradient(top,rgba(255,255,255,0.1),rgba(255,255,255,0.05) 49%,rgba(0,0,0,0.05) 51%,rgba(0,0,0,0.1))}.zocial:hover > span,.zocial:focus > span{background:-moz-linear-gradient(top,rgba(255,255,255,0.15),rgba(255,255,255,0.15) 49%,rgba(0,0,0,0.1) 51%,rgba(0,0,0,0.15));background:-webkit-gradient(linear,left top,left bottom,from(rgba(255,255,255,0.15)),color-stop(49%,rgba(255,255,255,0.15)),color-stop(51%,rgba(0,0,0,0.1)),to(rgba(0,0,0,0.15)));background:-webkit-linear-gradient(top,rgba(255,255,255,0.15),rgba(255,255,255,0.15) 49%,rgba(0,0,0,0.1) 51%,rgba(0,0,0,0.15));background:-o-linear-gradient(top,rgba(255,255,255,0.15),rgba(255,255,255,0.15) 49%,rgba(0,0,0,0.1) 51%,rgba(0,0,0,0.15));background:-ms-linear-gradient(top,rgba(255,255,255,0.15),rgba(255,255,255,0.15) 49%,rgba(0,0,0,0.1) 51%,rgba(0,0,0,0.15));background:linear-gradient(top,rgba(255,255,255,0.15),rgba(255,255,255,0.15) 49%,rgba(0,0,0,0.1) 51%,rgba(0,0,0,0.15))}.zocial:active > span{background:-moz-linear-gradient(bottom,rgba(255,255,255,0.1),rgba(255,255,255,0) 30%,rgba(0,0,0,0) 50%,rgba(0,0,0,0.1));background:-webkit-gradient(linear,left top,left bottom,from(rgba(255,255,255,0.1)),color-stop(30%,rgba(255,255,255,0)),color-stop(50%,rgba(0,0,0,0)),to(rgba(0,0,0,0.1)));background:-webkit-linear-gradient(bottom,rgba(255,255,255,0.1),rgba(255,255,255,0) 30%,rgba(0,0,0,0) 50%,rgba(0,0,0,0.1));background:-o-linear-gradient(bottom,rgba(255,255,255,0.1),rgba(255,255,255,0) 30%,rgba(0,0,0,0) 50%,rgba(0,0,0,0.1));background:-ms-linear-gradient(bottom,rgba(255,255,255,0.1),rgba(255,255,255,0) 30%,rgba(0,0,0,0) 50%,rgba(0,0,0,0.1));background:linear-gradient(bottom,rgba(255,255,255,0.1),rgba(255,255,255,0) 30%,rgba(0,0,0,0) 50%,rgba(0,0,0,0.1))}.zocial.bitcoin,.zocial.cloudapp,.zocial.dropbox,.zocial.email,.zocial.github,.zocial.gmail,.zocial.instapaper,.zocial.itunes,.zocial.ninetyninedesigns,.zocial.openid,.zocial.plancast,.zocial.posterous,.zocial.secondary,.zocial.viadeo,.zocial.weibo,.zocial.wikipedia{border:1px solid rgba(0,0,0,0.3);border-bottom-color:rgba(0,0,0,0.5);-moz-box-shadow:inset 0 0.08em 0 rgba(255,255,255,0.7),inset 0 0 0.08em rgba(255,255,255,0.5);-webkit-box-shadow:inset 0 0.08em 0 rgba(255,255,255,0.7),inset 0 0 0.08em rgba(255,255,255,0.5);box-shadow:inset 0 0.08em 0 rgba(255,255,255,0.7),inset 0 0 0.08em rgba(255,255,255,0.5);text-shadow:0 1px 0 rgba(255,255,255,0.8)}.zocial.bitcoin:focus > span,.zocial.bitcoin:hover > span,.zocial.dropbox:focus > span,.zocial.dropbox:hover > span,.zocial.email:focus > span,.zocial.email:hover > span,.zocial.github:focus > span,.zocial.github:hover > span,.zocial.gmail:focus > span,.zocial.gmail:hover > span,.zocial.instapaper:focus > span,.zocial.instapaper:hover > span,.zocial.itunes:focus > span,.zocial.itunes:hover > span,.zocial.ninetyninedesigns:focus > span,.zocial.ninetyninedesigns:hover > span,.zocial.openid:focus > span,.zocial.openid:hover > span,.zocial.plancast:focus > span,.zocial.plancast:hover > span,.zocial.posterous:focus > span,.zocial.posterous:hover > span,.zocial.secondary:focus > span,.zocial.secondary:hover > span,.zocial.twitter:focus > span,.zocial.viadeo:focus > span,.zocial.viadeo:hover > span,.zocial.weibo:focus > span,.zocial.weibo:hover > span,.zocial.wikipedia:focus > span,.zocial.wikipedia:hover > span{background:-webkit-gradient(linear,left top,left bottom,from(rgba(255,255,255,0.5)),color-stop(49%,rgba(255,255,255,0.2)),color-stop(51%,rgba(0,0,0,0.05)),to(rgba(0,0,0,0.15)));background:-moz-linear-gradient(top,rgba(255,255,255,0.5),rgba(255,255,255,0.2) 49%,rgba(0,0,0,0.05) 51%,rgba(0,0,0,0.15));background:-webkit-linear-gradient(top,rgba(255,255,255,0.5),rgba(255,255,255,0.2) 49%,rgba(0,0,0,0.05) 51%,rgba(0,0,0,0.15));background:-o-linear-gradient(top,rgba(255,255,255,0.5),rgba(255,255,255,0.2) 49%,rgba(0,0,0,0.05) 51%,rgba(0,0,0,0.15));background:-ms-linear-gradient(top,rgba(255,255,255,0.5),rgba(255,255,255,0.2) 49%,rgba(0,0,0,0.05) 51%,rgba(0,0,0,0.15));background:linear-gradient(top,rgba(255,255,255,0.5),rgba(255,255,255,0.2) 49%,rgba(0,0,0,0.05) 51%,rgba(0,0,0,0.15))}.zocial.bitcoin:active > span,.zocial.dropbox:active > span,.zocial.email:active > span,.zocial.github:active > span,.zocial.gmail:active > span,.zocial.instapaper:active > span,.zocial.itunes:active > span,.zocial.ninetyninedesigns:active > span,.zocial.openid:active > span,.zocial.plancast:active > span,.zocial.posterous:active > span,.zocial.secondary:active > span,.zocial.viadeo:active > span,.zocial.weibo:active > span,.zocial.wikipedia:active > span{background:-webkit-gradient(linear,left top,left bottom,from(rgba(255,255,255,0)),color-stop(30%,rgba(255,255,255,0)),color-stop(50%,rgba(0,0,0,0)),to(rgba(0,0,0,0.1)));background:-moz-linear-gradient(bottom,rgba(255,255,255,0),rgba(255,255,255,0) 30%,rgba(0,0,0,0) 50%,rgba(0,0,0,0.1));background:-webkit-linear-gradient(bottom,rgba(255,255,255,0),rgba(255,255,255,0) 30%,rgba(0,0,0,0) 50%,rgba(0,0,0,0.1));background:-o-linear-gradient(bottom,rgba(255,255,255,0),rgba(255,255,255,0) 30%,rgba(0,0,0,0) 50%,rgba(0,0,0,0.1));background:-ms-linear-gradient(bottom,rgba(255,255,255,0),rgba(255,255,255,0) 30%,rgba(0,0,0,0) 50%,rgba(0,0,0,0.1));background:linear-gradient(bottom,rgba(255,255,255,0),rgba(255,255,255,0) 30%,rgba(0,0,0,0) 50%,rgba(0,0,0,0.1))}.zocial.amazon > span:before{content:"a"}.zocial.android > span:before{content:"&"}.zocial.aol > span:before{content:"\""}.zocial.appstore > span:before{content:"A"}.zocial.bitcoin > span:before{content:"2";color:#f7931a !important}.zocial.blogger > span:before{content:"B"}.zocial.call > span:before{content:"7"}.zocial.chrome > span:before{content:"["}.zocial.cloudapp > span:before{content:"c"}.zocial.creativecommons > span:before{content:"C"}.zocial.disqus > span:before{content:"Q"}.zocial.dribbble > span:before{content:"D"}.zocial.dropbox > span:before{content:"d";color:#1f75cc !important}.zocial.email > span:before{content:"]";color:#312c2a !important}.zocial.eventasaurus > span:before{content:"v"}.zocial.eventbrite > span:before{content:"|"}.zocial.evernote > span:before{content:"E"}.zocial.facebook > span:before{content:"f"}.zocial.fivehundredpx > span:before{content:"0";color:#29b6ff !important}.zocial.flattr > span:before{content:"%"}.zocial.forrst > span:before{content:":";color:#50894f !important}.zocial.foursquare > span:before{content:"4"}.zocial.github > span:before{content:"g"}.zocial.gmail > span:before{content:"m";color:#f00 !important}.zocial.google > span:before{content:"G"}.zocial.googleplus > span:before{content:"+"}.zocial.gowalla > span:before{content:"@"}.zocial.grooveshark > span:before{content:"K"}.zocial.guest > span:before{content:"?"}.zocial.html5 > span:before{content:"5"}.zocial.ie > span:before{content:"6"}.zocial.instapaper > span:before{content:"I"}.zocial.intensedebate > span:before{content:"{"}.zocial.itunes > span:before{content:"i";color:#1a6dd2 !important}.zocial.lastfm > span:before{content:"l"}.zocial.linkedin > span:before{content:"L"}.zocial.macstore > span:before{content:"^"}.zocial.meetup > span:before{content:"M"}.zocial.myspace > span:before{content:"_"}.zocial.ninetyninedesigns > span:before{content:"9";color:#f50 !important}.zocial.openid > span:before{content:"o";color:#ff921d !important}.zocial.paypal > span:before{content:"$"}.zocial.pinboard > span:before{content:"n"}.zocial.pinterest > span:before{content:"1"}.zocial.plancast > span:before{content:"P"}.zocial.plurk > span:before{content:"j"}.zocial.podcast > span:before{content:"`"}.zocial.posterous > span:before{content:"~"}.zocial.quora > span:before{content:"q"}.zocial.rss > span:before{content:"R"}.zocial.scribd > span:before{content:"}";color:#00d5ea !important}.zocial.skype > span:before{content:"S"}.zocial.smashing > span:before{content:"*"}.zocial.songkick > span:before{content:"k"}.zocial.soundcloud > span:before{content:"s"}.zocial.spotify > span:before{content:"="}.zocial.stumbleupon > span:before{content:"/"}.zocial.tumblr > span:before{content:"t"}.zocial.twitter > span:before{content:"T"}.zocial.viadeo > span:before{content:"H";color:#f59b20 !important}.zocial.vimeo > span:before{content:"V"}.zocial.weibo > span:before{content:"J";color:#e6162d !important}.zocial.wikipedia > span:before{content:","}.zocial.windows > span:before{content:"W"}.zocial.wordpress > span:before{content:"w"}.zocial.yahoo > span:before{content:"Y"}.zocial.yelp > span:before{content:"y"}.zocial.youtube > span:before{content:"U"}.zocial.amazon{background:#ffad1d;color:#030037 !important;text-shadow:0 1px 0 rgba(255,255,255,0.5)}.zocial.android{background:#a4c639}.zocial.aol{background:#f00}.zocial.appstore{background:#000}.zocial.bitcoin{background:#efefef;color:#4d4d4d !important}.zocial.blogger{background:#ee5a22}.zocial.call{background:#008000}.zocial.chrome{background:#006cd4}.zocial.cloudapp{background:#fff;color:#312c2a !important}.zocial.creativecommons{background:#000}.zocial.disqus{background:#5d8aad}.zocial.dribbble{background:#ea4c89}.zocial.dropbox{background:#fff;color:#312c2a !important}.zocial.email{background:#f0f0eb;color:#312c2a !important}.zocial.eventasaurus{background:#8ccc33}.zocial.eventbrite{background:#ff5616}.zocial.evernote{background:#6bb130;color:#fff !important}.zocial.facebook{background:#4863ae}.zocial.fivehundredpx{background:#333}.zocial.flattr{background:#8aba42}.zocial.forrst{background:#1e360d}.zocial.foursquare{background:#44a8e0}.zocial.github{background:#fbfbfb;color:#050505 !important}.zocial.gmail{background:#efefef;color:#222 !important}.zocial.google{background:#4e6cf7}.zocial.googleplus{background:#dd4b39}.zocial.gowalla{background:#ff720a}.zocial.grooveshark{background:#111;color:#eee !important}.zocial.guest{background:#1b4d6d}.zocial.html5{background:#ff3617}.zocial.ie{background:#00a1d9}.zocial.instapaper{background:#eee;color:#222 !important}.zocial.intensedebate{background:#0099e1}.zocial.itunes{background:#efefeb;color:#312c2a !important}.zocial.lastfm{background:#dc1a23}.zocial.linkedin{background:#0083a8}.zocial.macstore{background:#007dcb}.zocial.meetup{background:#ff0026}.zocial.myspace{background:#000}.zocial.ninetyninedesigns{background:#fff;color:#072243 !important}.zocial.openid{background:#f5f5f5;color:#333 !important}.zocial.paypal{background:#ff921d;color:#032751 !important;text-shadow:0 1px 0 rgba(255,255,255,0.5)}.zocial.pinboard{background:blue}.zocial.pinterest{background:#c91618}.zocial.plancast{background:#e7ebed;color:#333 !important}.zocial.plurk{background:#cf682f}.zocial.podcast{background:#9365ce}.zocial.posterous{background:#ffd959;color:#bc7134 !important}.zocial.quora{background:#a82400}.zocial.rss{background:#ff7f25}.zocial.scribd{background:#231c1a}.zocial.skype{background:#00a2ed}.zocial.smashing{background:#ff4f27}.zocial.songkick{background:#ff0050}.zocial.soundcloud{background:#ff4500}.zocial.spotify{background:#60af00}.zocial.stumbleupon{background:#eb4924}.zocial.tumblr{background:#374a61}.zocial.twitter{background:#46c0fb}.zocial.viadeo{background:#fff;color:#000 !important}.zocial.vimeo{background:#00a2cd}.zocial.weibo{background:#faf6f1;color:#000 !important}.zocial.wikipedia{background:#fff;color:#000 !important}.zocial.windows{background:#0052a4;color:#FFF !important}.zocial.wordpress{background:#464646}.zocial.yahoo{background:#a200c2}.zocial.yelp{background:#e60010}.zocial.youtube{background:#f00}.zocial.primary > span,.zocial.secondary > span{margin:0.1em 0;padding:0 1em}.zocial.primary > span:before,.zocial.secondary > span:before{display:none}.zocial.primary{background:#333}.zocial.secondary{background:#f0f0eb;color:#222 !important;text-shadow:0 1px 0 rgba(255,255,255,0.8)}.zocial.humanitarianid > span:before{content:url(../../img/humanitarianid.png);height:24px;padding-top:3px}.zocial.humanitarianid{background:##e2e2e2;color:#2a5d81 !important}.zocial.humanitarianid{border:1px solid rgba(0,0,0,0.3);border-bottom-color:rgba(0,0,0,0.5);-moz-box-shadow:inset 0 0.08em 0 rgba(255,255,255,0.7),inset 0 0 0.08em rgba(255,255,255,0.5);-webkit-box-shadow:inset 0 0.08em 0 rgba(255,255,255,0.7),inset 0 0 0.08em rgba(255,255,255,0.5);box-shadow:inset 0 0.08em 0 rgba(255,255,255,0.7),inset 0 0 0.08em rgba(255,255,255,0.5);text-shadow:0 1px 0 rgba(255,255,255,0.8)}.zocial.humanitarianid:hover > span,.zocial.humanitarianid:focus > span{background:-webkit-gradient(linear,left top,left bottom,from(rgba(255,255,255,0.5)),color-stop(49%,rgba(255,255,255,0.2)),color-stop(51%,rgba(0,0,0,0.05)),to(rgba(0,0,0,0.15)));background:-moz-linear-gradient(top,rgba(255,255,255,0.5),rgba(255,255,255,0.2) 49%,rgba(0,0,0,0.05) 51%,rgba(0,0,0,0.15));background:-webkit-linear-gradient(top,rgba(255,255,255,0.5),rgba(255,255,255,0.2) 49%,rgba(0,0,0,0.05) 51%,rgba(0,0,0,0.15));background:-o-linear-gradient(top,rgba(255,255,255,0.5),rgba(255,255,255,0.2) 49%,rgba(0,0,0,0.05) 51%,rgba(0,0,0,0.15));background:-ms-linear-gradient(top,rgba(255,255,255,0.5),rgba(255,255,255,0.2) 49%,rgba(0,0,0,0.05) 51%,rgba(0,0,0,0.15));background:linear-gradient(top,rgba(255,255,255,0.5),rgba(255,255,255,0.2) 49%,rgba(0,0,0,0.05) 51%,rgba(0,0,0,0.15))}.zocial.humanitarianid:active > span{background:-webkit-gradient(linear,left top,left bottom,from(rgba(255,255,255,0)),color-stop(30%,rgba(255,255,255,0)),color-stop(50%,rgba(0,0,0,0)),to(rgba(0,0,0,0.1)));background:-moz-linear-gradient(bottom,rgba(255,255,255,0),rgba(255,255,255,0) 30%,rgba(0,0,0,0) 50%,rgba(0,0,0,0.1));background:-webkit-linear-gradient(bottom,rgba(255,255,255,0),rgba(255,255,255,0) 30%,rgba(0,0,0,0) 50%,rgba(0,0,0,0.1));background:-o-linear-gradient(bottom,rgba(255,255,255,0),rgba(255,255,255,0) 30%,rgba(0,0,0,0) 50%,rgba(0,0,0,0.1));background:-ms-linear-gradient(bottom,rgba(255,255,255,0),rgba(255,255,255,0) 30%,rgba(0,0,0,0) 50%,rgba(0,0,0,0.1));background:linear-gradient(bottom,rgba(255,255,255,0),rgba(255,255,255,0) 30%,rgba(0,0,0,0) 50%,rgba(0,0,0,0.1))}button::-moz-focus-inner{border:0;padding:0}.embeddedComponent .map_wrapper{min-width:350px}.map_wrapper{position:relative;overflow:hidden;clear:right}.map_wrapper.fullscreen{overflow:visible}.gis_west .x-panel-body{overflow-y:auto}.map_loader{margin-top:50px;margin-left:auto;margin-right:auto}form .map_loader{margin-top:0}.x-form-item-label{margin:0 0 0 4px}body.x-window-maximized-ct{width:100%}.form-container form tr.x-toolbar-left-row td{padding:0}.map_home{margin:0 0 0 -12px}.map_home .gis_fullscreen_map-btn{font-weight:bold;padding-left:8px}body.rtl .map_home .gis_fullscreen_map-btn{float:left}.gis_print_map-btn{font-weight:bold;float:right;padding-right:8px}.notitle .ui-dialog-titlebar{background-image:none !important;border:0;padding:0}.notitle .ui-dialog-title{margin:0;padding:0}.notitle .ui-dialog-titlebar-close{margin-top:-10px}.gis-map-window.x-resizable-pinned .x-window-tl{height:0}.form-container form button.gis_loc_select_btn{padding:4px}.form-container form button.gis_loc_select_btn i.icon-map{padding-right:2px}.gis-display-feature i.icon-map-marker{cursor:pointer;cursor:hand;padding-left:5px}.embeddedComponent .map_wrapper{width:100%}.gis_coord_wrap .decimal{width:174px}.gis_coord_wrap .gis_coord_dms input{width:37px}.gis_coord_wrap .gis_coord_dms input.seconds{width:70px}.gis_coord_wrap div{padding-top:8px}.x-tree-elbow,.x-tree-elbow-end,.x-tree-node-icon{display:none}.x-tree-node-anchor{padding-left:5px;padding-right:5px}.x-tree-node{font-size:12px}.x-tree-node-leaf{margin-left:10px}.x-tree-root-ct,.x-tree-node-ct{margin:0}.map_legend_div{position:absolute;bottom:0;right:0}.map_wrapper.fullscreen .map_legend_div{z-index:9100}.map_wrapper.a4 .map_legend_div{margin-bottom:-440px}.map_wrapper.a3 .map_legend_div{margin-bottom:-687px;right:-140px}.map_wrapper.a2 .map_legend_div{margin-bottom:-1036px;right:-530px}.map_wrapper.a1 .map_legend_div{margin-bottom:-1529px;right:-2130px}.map_wrapper.a0 .map_legend_div{margin-bottom:-2229px;right:-3125px}.map_legend_panel{background-color:#fbfbfb;border:solid;border-radius:5px 0 0 5px;padding:1px;margin:0 0 20px;width:auto;max-width:800px;max-height:500px;overflow-y:auto;border-right:none}.map_legend_panel .x-panel-header-noborder{border:none}.map_legend_tab{background-color:#fbfbfb;border:solid;border-width:2px;margin-top:5px;font-family:FontAwesome;font-weight:normal;font-style:normal;text-decoration:inherit;-webkit-font-smoothing:antialiased;float:left;border-radius:3px 0 3px 3px;border-right:none;border-right-width:0;margin-right:-6px;margin-left:-12px;padding-left:2px;padding-right:3px}.map_legend_tab:before{text-decoration:inherit;display:inline-block;speak:none}.map_legend_tab.left:before{content:"\f100"}.map_legend_tab.right:before{content:"\f101"}.map_wrapper.print .map_legend_tab{height:0}.map_wrapper.print .map_legend_tab.right:before,.map_wrapper.print .map_legend_tab.left:before{content:normal}.gis_legend_title{font-weight:bold;margin-top:10px}.gis_legend_desc{max-width:200px}.layer_throbber.float{position:absolute;top:10px;right:10px}.layer_throbber.save{top:65px}.map_polygon_panel{background-color:#fbfbfb;position:absolute;top:10px;height:125px;width:350px;margin-left:-175px;left:50%;border:solid;border-radius:2px;border-width:1px;font-size:small;text-align:center;padding:12px}.map_polygon_buttons{font-size:0.75rem;margin-top:12px;text-transform:uppercase}.button.map_polygon_finish{margin-right:12px}.map_save_panel{background-color:#fbfbfb;position:absolute;top:0;right:0;margin:10px 0;padding:5px;border:solid;border-radius:5px 0 0 5px;width:auto}.map_wrapper.fullscreen .map_save_panel{z-index:9100;margin-top:-100px}.map_wrapper.print .map_save_button{height:0;width:0;visibility:hidden}.map_save_panel.off{visibility:hidden}.map_save_button{background-color:#fbfbfb;padding:5px;border:solid;border-radius:5px 5px 5px 5px;width:auto;cursor:pointer;visibility:visible;float:right}.map_save_name{font-weight:bold;text-align:center;padding:5px;margin-top:2px}.map_save_panel input{width:150px;margin-top:1px;margin-right:5px}.map_save_panel input.checkbox{width:10px;margin:0 5px 0 0}.map_save_panel .new_map{font-size:small}.map_save_panel .saved{float:left}.map_save_panel p{float:left;padding:5px;color:green;margin:0}#config-gis_config_pe_id-options-filter__row .s3-groupedopts-option{display:none}#config-gis_config_pe_id-options-filter__row .s3-groupedopts-widget td:first-child{border-right:solid 1px;padding-right:10px}.olControlMousePosition{font-size:10px;background-color:white}.crosshair{cursor:crosshair}.olLayerGoogleCopyright{right:3px;bottom:2px;left:auto}.olLayerGooglePoweredBy{left:2px;bottom:2px}.olForeignContainer div.olControlMousePosition{bottom:28px}.gis_tooltip{opacity:0.7 !important}.gis_tooltip_content{overflow:hidden;padding:3px;margin:10px}.olPopup #plain{max-width:450px}.olPopupCloseBox{margin-right:15px;margin-top:-8px}.olFramedCloudPopupContent label{padding-right:5px}.gis-map-window .olFramedCloudPopupContent td{padding:2px}.gis_popup_row{display:table-row}.gis_popup_label{display:table-cell;font-weight:bold;text-align:right;padding-right:2px}.gis_popup_val{display:table-cell}#georsspopup h2,#kmlpopup h2{margin:0}.gx-popup-anc{background:transparent url(../../img/gis/geoext/anchor.png) no-repeat 0 0;position:relative;top:-1px;left:5px;z-index:2;height:16px;width:31px}.gx-ruledrag-insert-below{border-bottom:1px dotted}.gx-ruledrag-insert-above{border-top:1px dotted}.mappnlcntr .zoomfull{background-image:url(../../img/gis/mapfish/icon_zoomfull.png) !important;height:20px !important;width:20px !important}.mappnlcntr .zoomin{background-image:url(../../img/gis/mapfish/icon_zoomin.png) !important;height:20px !important;width:20px !important}.mappnlcntr .zoomout{background-image:url(../../img/gis/mapfish/icon_zoomout.png) !important;height:20px !important;width:20px !important}.mappnlcntr .pan-off{background-image:url(../../img/gis/mapfish/icon_pan.png) !important;height:20px !important;width:20px !important}.mappnlcntr .measure-off{background-image:url(../../img/gis/measuring-stick-off.png) !important;height:20px !important;width:20px !important}.mappnlcntr .measure-area{background-image:url(../../img/gis/measure-area-off.png) !important;height:20px !important;width:20px !important}.mappnlcntr .modifyfeature{background-image:url(../../img/gis/mapfish/move_vertex_off.png) !important;height:20px !important;width:20px !important}.mappnlcntr .drawpoint-off{background-image:url(../../img/gis/add_point_off.png) !important;height:20px !important;width:20px !important}.mappnlcntr .drawline-off{background-image:url(../../img/gis/mapfish/draw_line_off.png) !important;height:20px !important;width:20px !important}.mappnlcntr .drawpolygon-off{background-image:url(../../img/gis/mapfish/draw_polygon_off.png) !important;height:20px !important;width:20px !important}.mappnlcntr .drawpolygonclear-off{background-image:url(../../img/gis/mapfish/draw_polygon_clear_off.png) !important;height:20px !important;width:20px !important}.mappnlcntr .drawcircle-on{background-image:url(../../img/draw_circle_on.png) !important;height:20px !important;width:20px !important}.mappnlcntr .info{background-image:url(../../img/gis/mapfish/info.png) !important;height:20px !important;width:20px !important}.mappnlcntr .searchclick{background-image:url(../../img/ext/information.png) !important;height:20px !important;width:20px !important}.mappnlcntr .searchbox{background-image:url(../../img/ext/information-box.png) !important;height:20px !important;width:20px !important}.mappnlcntr .back{background-image:url(../../img/gis/mapfish/resultset_previous.png) !important;height:20px !important;width:20px !important}.mappnlcntr .next{background-image:url(../../img/gis/mapfish/resultset_next.png) !important;height:20px !important;width:20px !important}.mappnlcntr .print{background-image:url(../../img/silk/printer.png) !important;height:20px !important;width:20px !important}.mappnlcntr .save{background-image:url(../../img/ext/save.gif) !important;height:20px !important;width:20px !important}.x-btn-text.geolocation{background-image:url(../../img/gis/geolocation.png) !important;height:20px !important;width:20px !important}.x-btn-text.potlatch{background-image:url(../../img/gis/openstreetmap.png) !important;height:20px !important;width:20px !important}.x-btn-text.streetview{background-image:url(../../img/gis/streetview.png) !important;height:20px !important;width:20px !important}.gxp-icon-addlayers{background-image:url(../../img/silk/add.png) !important}.gxp-icon-addserver{background-image:url(../../img/silk/server_add.png) !important}.gxp-icon-getfeatureinfo{background-image:url(../../img/silk/information.png) !important}.gxp-icon-removelayers{background-image:url(../../img/silk/delete.png) !important}.gxp-icon-layerproperties{background-image:url(../../img/silk/wrench.png) !important}.icon-clearlayers{background-image:url(../../img/silk/eye.png)}.mappnlcntr .movefeature{background-image:url(../../img/gis/arrow_refresh.png) !important;height:20px !important;width:20px !important}.mappnlcntr .removefeature{background-image:url(../../img/gis/remove_point_off.png) !important;height:20px !important;width:20px !important}.mappnlcntr .resizefeature{background-image:url(../../img/gis/resize.png) !important;height:20px !important;width:20px !important}.mappnlcntr .rotatefeature{background-image:url(../../img/gis/arrow_rotate_clockwise.png) !important;height:20px !important;width:20px !important}.gis-map-window table,.map_wrapper table{background:none;border:none;margin-bottom:0}.gis-map-window table tr:nth-of-type(2n),.map_wrapper table tr:nth-of-type(2n){background-color:inherit}.gis-map-window table tr th,.gis-map-window table tr td,.map_wrapper table tr th,.map_wrapper table tr td{color:inherit;font-size:inherit;padding:0}.gis-map-window input[type="text"],.gis-map-window input[type="checkbox"],.gis-map-window input[type="radio"],.map_wrapper input[type="text"],.map_wrapper input[type="checkbox"],.map_wrapper input[type="radio"]{margin:0;padding:0}.x-form-element input[type="text"]{display:inline;font-size:inherit;margin:0;padding:0}.map_legend_tab.right{float:left !important}#contents .map_wrapper a:not(.action-btn):not(.delete-btn){text-decoration:none}@charset "UTF-8";@media all and (orientation:portrait){}@media all and (orientation:landscape){}@media screen and (max-device-width:480px){}@media handheld{*{float:none;font-size:80%;background:#fff;color:#000}}@charset "UTF-8";@media print{body{ background:transparent;color:black;font-family:"Georgia",Times New Roman,Serif;font-size:12pt} #menu_modules,#menu_options,#footer,#rheader_tabs,#searchCombo{display:none} #content{background-color:transparent;width:100%;float:none !important;border:0;border-radius:0;margin:0;padding:0} #content h1,#content h2{background:white;color:black;font-size:16pt;border:0;border-radius:0;margin:0} #content h3{background:white;color:black;font-size:14pt;margin:0} a{color:black;background:transparent;text-decoration:underline} #comments{page-break-before:always} *{background:transparent !important;color:#444 !important;text-shadow:none !important} a,a:visited{color:#444 !important;text-decoration:underline} a:after{content:" (" attr(href) ")"} abbr:after{content:" (" attr(title) ")"} .ir a:after{content:""} pre,blockquote{border:1px solid #999;page-break-inside:avoid} thead{display:table-header-group} tr,img{page-break-inside:avoid} @page{margin:0.5cm} p,h2,h3{orphans:3;widows:3} h2,h3{page-break-after:avoid}}.nvd3 .nv-axis{pointer-events:none;opacity:1}.nvd3 .nv-axis path{fill:none;stroke:#000;stroke-opacity:.75;shape-rendering:crispEdges}.nvd3 .nv-axis path.domain{stroke-opacity:.75}.nvd3 .nv-axis.nv-x path.domain{stroke-opacity:0}.nvd3 .nv-axis line{fill:none;stroke:#e5e5e5;shape-rendering:crispEdges}.nvd3 .nv-axis .zero line,.nvd3 .nv-axis line.zero{stroke-opacity:.75}.nvd3 .nv-axis .nv-axisMaxMin text{font-weight:bold}.nvd3 .x .nv-axis .nv-axisMaxMin text,.nvd3 .x2 .nv-axis .nv-axisMaxMin text,.nvd3 .x3 .nv-axis .nv-axisMaxMin text{text-anchor:middle}.nvd3 .nv-axis.nv-disabled{opacity:0}.nvd3 .nv-bars rect{fill-opacity:.75;transition:fill-opacity 250ms linear}.nvd3 .nv-bars rect.hover{fill-opacity:1}.nvd3 .nv-bars .hover rect{fill:lightblue}.nvd3 .nv-bars text{fill:rgba(0,0,0,0)}.nvd3 .nv-bars .hover text{fill:rgba(0,0,0,1)}.nvd3 .nv-multibar .nv-groups rect,.nvd3 .nv-multibarHorizontal .nv-groups rect,.nvd3 .nv-discretebar .nv-groups rect{stroke-opacity:0;transition:fill-opacity 250ms linear}.nvd3 .nv-multibar .nv-groups rect:hover,.nvd3 .nv-multibarHorizontal .nv-groups rect:hover,.nvd3 .nv-candlestickBar .nv-ticks rect:hover,.nvd3 .nv-discretebar .nv-groups rect:hover{fill-opacity:1}.nvd3 .nv-discretebar .nv-groups text,.nvd3 .nv-multibarHorizontal .nv-groups text{font-weight:bold;fill:rgba(0,0,0,1);stroke:rgba(0,0,0,0)}.nvd3 .nv-boxplot circle{fill-opacity:0.5}.nvd3 .nv-boxplot circle:hover{fill-opacity:1}.nvd3 .nv-boxplot rect:hover{fill-opacity:1}.nvd3 line.nv-boxplot-median{stroke:black}.nv-boxplot-tick:hover{stroke-width:2.5px}.nvd3.nv-bullet{font:10px sans-serif}.nvd3.nv-bullet .nv-measure{fill-opacity:.8}.nvd3.nv-bullet .nv-measure:hover{fill-opacity:1}.nvd3.nv-bullet .nv-marker{stroke:#000;stroke-width:2px}.nvd3.nv-bullet .nv-markerTriangle{stroke:#000;fill:#fff;stroke-width:1.5px}.nvd3.nv-bullet .nv-markerLine{stroke:#000;stroke-width:1.5px}.nvd3.nv-bullet .nv-tick line{stroke:#666;stroke-width:.5px}.nvd3.nv-bullet .nv-range.nv-s0{fill:#eee}.nvd3.nv-bullet .nv-range.nv-s1{fill:#ddd}.nvd3.nv-bullet .nv-range.nv-s2{fill:#ccc}.nvd3.nv-bullet .nv-title{font-size:14px;font-weight:bold}.nvd3.nv-bullet .nv-subtitle{fill:#999}.nvd3.nv-bullet .nv-range{fill:#bababa;fill-opacity:.4}.nvd3.nv-bullet .nv-range:hover{fill-opacity:.7}.nvd3.nv-candlestickBar .nv-ticks .nv-tick{stroke-width:1px}.nvd3.nv-candlestickBar .nv-ticks .nv-tick.hover{stroke-width:2px}.nvd3.nv-candlestickBar .nv-ticks .nv-tick.positive rect{stroke:#2ca02c;fill:#2ca02c}.nvd3.nv-candlestickBar .nv-ticks .nv-tick.negative rect{stroke:#d62728;fill:#d62728}.with-transitions .nv-candlestickBar .nv-ticks .nv-tick{transition:stroke-width 250ms linear,stroke-opacity 250ms linear}.nvd3.nv-candlestickBar .nv-ticks line{stroke:#333}.nv-force-node{stroke:#fff;stroke-width:1.5px}.nv-force-link{stroke:#999;stroke-opacity:.6}.nv-force-node text{stroke-width:0px}.nvd3 .nv-legend .nv-disabled rect{}.nvd3 .nv-check-box .nv-box{fill-opacity:0;stroke-width:2}.nvd3 .nv-check-box .nv-check{fill-opacity:0;stroke-width:4}.nvd3 .nv-series.nv-disabled .nv-check-box .nv-check{fill-opacity:0;stroke-opacity:0}.nvd3 .nv-controlsWrap .nv-legend .nv-check-box .nv-check{opacity:0}.nvd3.nv-linePlusBar .nv-bar rect{fill-opacity:.75}.nvd3.nv-linePlusBar .nv-bar rect:hover{fill-opacity:1}.nvd3 .nv-groups path.nv-line{fill:none}.nvd3 .nv-groups path.nv-area{stroke:none}.nvd3.nv-line .nvd3.nv-scatter .nv-groups .nv-point{fill-opacity:0;stroke-opacity:0}.nvd3.nv-scatter.nv-single-point .nv-groups .nv-point{fill-opacity:.5 !important;stroke-opacity:.5 !important}.with-transitions .nvd3 .nv-groups .nv-point{transition:stroke-width 250ms linear,stroke-opacity 250ms linear}.nvd3.nv-scatter .nv-groups .nv-point.hover,.nvd3 .nv-groups .nv-point.hover{stroke-width:7px;fill-opacity:.95 !important;stroke-opacity:.95 !important}.nvd3 .nv-point-paths path{stroke:#aaa;stroke-opacity:0;fill:#eee;fill-opacity:0}.nvd3 .nv-indexLine{cursor:ew-resize}svg.nvd3-svg{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;display:block;width:100%;height:100%}.nvtooltip.with-3d-shadow,.with-3d-shadow .nvtooltip{box-shadow:0 5px 10px rgba(0,0,0,.2);border-radius:5px}.nvd3 text{font:normal 12px Arial,sans-serif}.nvd3 .title{font:bold 14px Arial,sans-serif}.nvd3 .nv-background{fill:white;fill-opacity:0}.nvd3.nv-noData{font-size:18px;font-weight:bold}.nv-brush .extent{fill-opacity:.125;shape-rendering:crispEdges}.nv-brush .resize path{fill:#eee;stroke:#666}.nvd3 .nv-legend .nv-series{cursor:pointer}.nvd3 .nv-legend .nv-disabled circle{fill-opacity:0}.nvd3 .nv-brush .extent{fill-opacity:0 !important}.nvd3 .nv-brushBackground rect{stroke:#000;stroke-width:.4;fill:#fff;fill-opacity:.7}@media print{.nvd3 text{stroke-width:0;fill-opacity:1}}.nvd3.nv-ohlcBar .nv-ticks .nv-tick{stroke-width:1px}.nvd3.nv-ohlcBar .nv-ticks .nv-tick.hover{stroke-width:2px}.nvd3.nv-ohlcBar .nv-ticks .nv-tick.positive{stroke:#2ca02c}.nvd3.nv-ohlcBar .nv-ticks .nv-tick.negative{stroke:#d62728}.nvd3 .background path{fill:none;stroke:#EEE;stroke-opacity:.4;shape-rendering:crispEdges}.nvd3 .foreground path{fill:none;stroke-opacity:.7}.nvd3 .nv-parallelCoordinates-brush .extent{fill:#fff;fill-opacity:.6;stroke:gray;shape-rendering:crispEdges}.nvd3 .nv-parallelCoordinates .hover{fill-opacity:1;stroke-width:3px}.nvd3 .missingValuesline line{fill:none;stroke:black;stroke-width:1;stroke-opacity:1;stroke-dasharray:5,5}.nvd3.nv-pie path{stroke-opacity:0;transition:fill-opacity 250ms linear,stroke-width 250ms linear,stroke-opacity 250ms linear}.nvd3.nv-pie .nv-pie-title{font-size:24px;fill:rgba(19,196,249,0.59)}.nvd3.nv-pie .nv-slice text{stroke:#000;stroke-width:0}.nvd3.nv-pie path{stroke:#fff;stroke-width:1px;stroke-opacity:1}.nvd3.nv-pie path{fill-opacity:.7}.nvd3.nv-pie .hover path{fill-opacity:1}.nvd3.nv-pie .nv-label{pointer-events:none}.nvd3.nv-pie .nv-label rect{fill-opacity:0;stroke-opacity:0}.nvd3 .nv-groups .nv-point.hover{stroke-width:20px;stroke-opacity:.5}.nvd3 .nv-scatter .nv-point.hover{fill-opacity:1}.nv-noninteractive{pointer-events:none}.nv-distx,.nv-disty{pointer-events:none}.nvd3.nv-sparkline path{fill:none}.nvd3.nv-sparklineplus g.nv-hoverValue{pointer-events:none}.nvd3.nv-sparklineplus .nv-hoverValue line{stroke:#333;stroke-width:1.5px}.nvd3.nv-sparklineplus,.nvd3.nv-sparklineplus g{pointer-events:all}.nvd3 .nv-hoverArea{fill-opacity:0;stroke-opacity:0}.nvd3.nv-sparklineplus .nv-xValue,.nvd3.nv-sparklineplus .nv-yValue{stroke-width:0;font-size:.9em;font-weight:normal}.nvd3.nv-sparklineplus .nv-yValue{stroke:#f66}.nvd3.nv-sparklineplus .nv-maxValue{stroke:#2ca02c;fill:#2ca02c}.nvd3.nv-sparklineplus .nv-minValue{stroke:#d62728;fill:#d62728}.nvd3.nv-sparklineplus .nv-currentValue{font-weight:bold;font-size:1.1em}.nvd3.nv-stackedarea path.nv-area{fill-opacity:.7;stroke-opacity:0;transition:fill-opacity 250ms linear,stroke-opacity 250ms linear}.nvd3.nv-stackedarea path.nv-area.hover{fill-opacity:.9}.nvd3.nv-stackedarea .nv-groups .nv-point{stroke-opacity:0;fill-opacity:0}.nvtooltip{position:absolute;background-color:rgba(255,255,255,1.0);color:rgba(0,0,0,1.0);padding:1px;border:1px solid rgba(0,0,0,.2);z-index:10000;display:block;font-family:Arial,sans-serif;font-size:13px;text-align:left;pointer-events:none;white-space:nowrap;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.nvtooltip{background:rgba(255,255,255,0.8);border:1px solid rgba(0,0,0,0.5);border-radius:4px}.nvtooltip.with-transitions,.with-transitions .nvtooltip{transition:opacity 50ms linear;transition-delay:200ms}.nvtooltip.x-nvtooltip,.nvtooltip.y-nvtooltip{padding:8px}.nvtooltip h3{margin:0;padding:4px 14px;line-height:18px;font-weight:normal;background-color:rgba(247,247,247,0.75);color:rgba(0,0,0,1.0);text-align:center;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.nvtooltip p{margin:0;padding:5px 14px;text-align:center}.nvtooltip span{display:inline-block;margin:2px 0}.nvtooltip table{margin:6px;border-spacing:0}.nvtooltip table td{padding:2px 9px 2px 0;vertical-align:middle}.nvtooltip table td.key{font-weight:normal}.nvtooltip table td.key.total{font-weight:bold}.nvtooltip table td.value{text-align:right;font-weight:bold}.nvtooltip table td.percent{color:darkgray}.nvtooltip table tr.highlight td{padding:1px 9px 1px 0;border-bottom-style:solid;border-bottom-width:1px;border-top-style:solid;border-top-width:1px}.nvtooltip table td.legend-color-guide div{width:8px;height:8px;vertical-align:middle}.nvtooltip table td.legend-color-guide div{width:12px;height:12px;border:1px solid #999}.nvtooltip .footer{padding:3px;text-align:center}.nvtooltip-pending-removal{pointer-events:none;display:none}.nvd3 .nv-interactiveGuideLine{pointer-events:none}.nvd3 line.nv-guideline{stroke:#ccc}.tp-form fieldset legend button:first-child,.pt-form fieldset legend button:first-child{display:none}.pt-form .pt-rows{margin-right:0.5rem}.pt-form .pt-cols{margin-left:0.5rem}.pt-form .pt-axis-options select,.pt-form .pt-axis-options label{display:inline-block;width:auto}.tp-chart-tops,.pt-chart-opts{height:1.0rem}.tp-chart-icon,.pt-chart-icon{width:16px;height:16px;float:left;margin-right:5px;cursor:pointer}.tp-chart-label,.pt-chart-label{font-size:0.7rem;float:left;margin-right:8px}.tp-lchart{background:url(../../img/report/lchart.png) center no-repeat}.tp-bchart{background:url(../../img/report/vchart.png) center no-repeat}.pt-pchart{background:url(../../img/report/pchart.png) center no-repeat}.pt-vchart{background:url(../../img/report/vchart.png) center no-repeat}.pt-hchart{background:url(../../img/report/hchart.png) center no-repeat}.pt-schart{background:url(../../img/report/pchart.png) center no-repeat}.tp-chart-contents,.pt-chart-contents{background-color:#fffdf6;padding:8px;border:1px solid #999;margin-top:5px;margin-bottom:5px;position:relative}.pt-chart-contents .pt-chart-title{position:absolute;left:8;top:0}.pt-chart-contents .pt-chart-title h4{font-size:1.0rem;font-weight:normal}.pt-chart-contents .pt-hide-chart{cursor:pointer;min-height:16px;min-width:16px;background:url(../../img/cross.png) right top no-repeat}.pt-chart-contents .pt-chart{margin-top:20px}.pt-chart-contents .pt-spectrum-pie{height:140px}.pt-chart-contents .pt-spectrum-pie svg.nv{float:left;width:auto}.pt-chart-contents .pt-spectrum-bar{height:280px;clear:left}.pt-chart-contents .pt-spectrum-form{float:left}.pt-chart-contents .pt-spectrum-form label{font-weight:bold;margin-right:8px;display:block}.pt-chart-contents .pt-spectrum-form select{font-size:0.875rem;max-width:360px}.pt-tooltip{font-family:Arial;font-size:13px;text-align:center;padding:4px}.pt-tooltip .pt-tooltip-label{font-weight:bold}.pt-tooltip .pt-tooltip-label,.pt-tooltip .pt-tooltip-text{max-width:175px;white-space:normal}.pt-table-contents{min-height:16px}.pt-table-contents .pt-table{overflow:auto}.pt-table-contents .pt-table th{border-bottom:0}.pt-table-contents .pt-table tr.pt-totals-row th.pt-totals-header{border-bottom:1px solid #ccc;border-top:2px solid #ccc}.pt-table-contents .pt-table .pt-totals-header,.pt-table-contents .pt-table .pt-total,.pt-table-contents .pt-table .pt-row-total,.pt-table-contents .pt-table .pt-col-total{font-weight:bold}.pt-table-contents .pt-table .pt-total,.pt-table-contents .pt-table .pt-col-total{border-top:2px solid #ccc}.pt-table-contents .pt-table .pt-total,.pt-table-contents .pt-table .pt-row-total{border-left:2px solid #ccc}.pt-table-contents .pt-table .pt-cell-value{display:inline}.pt-table-contents .pt-table .pt-cell-value li{font-size:0.8rem}.pt-table-contents .pt-table .pt-cell-records{clear:left}.pt-table-contents .pt-table td{min-width:60px;padding-right:16px;white-space:nowrap}.pt-table-contents .pt-table td .pt-cell-zoom{width:16px;height:16px;cursor:pointer;display:inline-block;vertical-align:text-top;margin-left:5px;background:url(../../img/silk/magnifier_zoom_in.png) no-repeat top right}.pt-table-contents .pt-table td .pt-cell-zoom.opened{background-image:url(../../img/silk/magnifier_zoom_out.png)}.pt-table-contents .pt-table tfoot{font-style:italic}.tp-throbber,.pt-throbber{float:right;padding:5px;z-index:999}.gi-empty,.tp-empty,.pt-empty,.pt-no-data{font-style:italic;font-size:0.8rem}.tp-chart-controls,.tp-chart.contents,.pt-toggle-table,.pt-chart-controls,.pt-chart-contents,.pt-table-controls,.pt-table-contents{clear:left}.pt-toggle-table{display:inline-block}.pt-show-table,.pt-hide-table{font-size:10px;cursor:pointer;color:#039;text-decoration:underline;margin-bottom:3px}.pt-show-table{display:none}.pt-table-controls{position:relative;padding:0.4rem 0}.pt-export-table{text-align:right;margin:0.3rem;display:inline-block;position:absolute;right:0}.pt-export-opt{background-repeat:no-repeat;background-position:center;width:16px;height:16px;display:inline-block;cursor:pointer}.pt-export-xls{background-image:url('../../img/icon-xls.png')}.gi-table table{border-collapse:separate}.gi-table table thead td,.gi-table table tfoot td{background-color:#333;color:#fff}.gi-group-header.gi-level-1 td{background-color:#999;color:#fff}.gi-group-header.gi-level-1 td a{color:#fff}.gi-group-header.gi-level-2 td{background-color:#eee;border-top:1px solid #999}.gi-group-footer.gi-level-1 td{background-color:#999;color:#fff;border-bottom:1px dotted #666}.gi-group-footer.gi-level-1 td a{color:#fff}.gi-group-footer.gi-level-2 td{background-color:#eee;border-bottom:1px solid #999}.gi-group-footer-inline-label{display:inline-block;font-size:11px;line-height:1;position:relative;top:-0.1em;margin:0 0.25em 0 1em;padding:0.25em 0.75em;text-transform:uppercase;background-color:#ccc;color:#666}.gi-export-formats{display:inline}.gi-export-formats .gi-export{height:16px;width:16px;display:inline-block;padding-right:1.2rem;padding-bottom:1.5rem;background-repeat:no-repeat;cursor:pointer}@charset "UTF-8";tr.survey_section th{color:#003399;font-size:150%;text-align:center}tr.survey_question th{color:#112038;font-size:90%;font-weight:bold;vertical-align:top}div.survey_map-legend td{vertical-align:top}div.survey_scrollable{width:900px;overflow:scroll}@font-face{font-family:'DRMP';src:url('../../fonts/DRMP.eot');src:url('../../fonts/DRMP.eot?#iefix') format('embedded-opentype'),url('../../fonts/DRMP.woff') format('woff'),url('../../fonts/DRMP.ttf') format('truetype'),url('../../fonts/DRMP.svg#DRMP') format('svg');font-weight:normal;font-style:normal}.icon-activity,.icon-alert,.icon-assessment,.icon-event,.icon-incident,.icon-map,.icon-news,.icon-plan,.icon-project,.icon-report,.icon-training_material{font-family:'DRMP';speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased}.icon-activity5:before,.icon-activity:before{content:"\5b"}.icon-alert:before{content:"\70"}.icon-assessment:before{content:"\6f"}.icon-event:before{content:"\69"}.icon-incident:before{content:"\75"}.icon-map:before{content:"\79"}.icon-news:before{content:"\71"}.icon-plan:before{content:"\74"}.icon-project:before{content:"\72"}.icon-report:before{content:"\65"}.icon-training_material:before{content:"\77"}.latest-updates,.list_formats{margin-top:0.5rem}.latest-updates .action-btn.s3_modal{float:right;font-weight:normal}body.rtl .latest-updates .action-btn.s3_modal{float:left}.side-filter{margin-top:10px}.side-filter .form-row{margin-left:0.375rem;margin-right:0.375rem;padding:0}.side-filter input[type="text"]{width:100% !important}.dl-header{font-size:0.825rem}.dl-exports{margin:0}.card-holder-home .dl-1-cols{width:100%}.card-holder-home .dl-item{padding:0;font-size:0.8rem;margin-bottom:0.5rem;box-shadow:0 1px 1px #aaaaa7}.card-holder-home .dl-row.even,.card-holder-home .dl-row.odd,.card-holder-home .dl-row.even .dl-item,.card-holder-home .dl-row.odd .dl-item{background-color:white}.card-header{background:linear-gradient(to bottom,rgba(255,255,255,1) 0%,rgba(246,246,246,1) 47%,rgba(237,237,237,1) 100%) repeat scroll 0 0 rgba(0,0,0,0);font-size:0.8rem;padding:0 0.175rem 0 0.325rem;border-bottom:1px solid #bdbdbb}.card-header span{padding:0 5px 0 5px;border-right:1px solid #7f7f7f}.card-header .edit-bar a{color:#7f7f7f;text-decoration:none;padding-left:0.175rem}.card-header .location-title{font-weight:bold}.card-header .date-title{color:#7f7f7f}.card-footer{margin-top:0.2rem;padding:0}.card-footer a.action-btn:first-child,.card-footer a.delete-btn:first-child{margin-left:0}.dl-item .pull-right{float:right}body.rtl .dl-item .pull-right{float:left}.dl-item .pull-left{float:left;margin-right:0.8rem}body.rtl .dl-item .pull-left{float:right;margin-left:0.8rem}body.rtl .fright{float:left !important}.dl-item .media{font-size:0.8rem;padding:0.375rem}.dl-item .card-subtitle{font-weight:bold;font-size:0.9rem}.dl-item .date-title,.dl-item .card-person{font-size:0.7rem}.dl-inline-data{margin-top:0.5rem}.dl-inline-label,.dl-inline-value{color:#999;font-size:0.7rem;display:inline-block}.dl-inline-label{font-weight:normal;padding-right:0.2rem}.dl-inline-label::after{content:":"}.dl-inline-value{font-weight:bold;padding-right:0.8rem}.dl-item ul.s3-tags{font-size:0.8rem;margin:0 0.375rem !important;list-style:none}.dl-item ul.s3-tags li.tagit-new{padding:0}meta.foundation-version{font-family:"/{{VERSION}}/"}meta.foundation-mq-small{font-family:"/only screen/";width:0}meta.foundation-mq-small-only{font-family:"/only screen and (max-width:40em)/";width:0}meta.foundation-mq-medium{font-family:"/only screen and (min-width:40.0625em)/";width:40.0625em}meta.foundation-mq-medium-only{font-family:"/only screen and (min-width:40.0625em) and (max-width:64em)/";width:40.0625em}meta.foundation-mq-large{font-family:"/only screen and (min-width:64.0625em)/";width:64.0625em}meta.foundation-mq-large-only{font-family:"/only screen and (min-width:64.0625em) and (max-width:90em)/";width:64.0625em}meta.foundation-mq-xlarge{font-family:"/only screen and (min-width:90.0625em)/";width:90.0625em}meta.foundation-mq-xlarge-only{font-family:"/only screen and (min-width:90.0625em) and (max-width:120em)/";width:90.0625em}meta.foundation-mq-xxlarge{font-family:"/only screen and (min-width:120.0625em)/";width:120.0625em}meta.foundation-data-attribute-namespace{font-family:false}.inline-throbber{background-image:url(../../img/sunflower_fade_indicator.gif)}.throbber,.layer_throbber,.map_loader{background-image:url(../../img/sunflower_spin_throbber.gif)}.loading{background:url(../../img/sunflower_spin_throbber.gif) center no-repeat !important}.sahana-logo{background:url(../../img/S3menu_logo.png) left top no-repeat;text-shadow:none;padding:0;margin-left:5px;margin-top:10px;width:35px;height:28px;display:inline-block}.alert.alert-error,.alert.alert-info,.alert.alert-warning,.alert.alert-success{border:1px solid #b2b2b2;box-shadow:0 1px 1px #aaaaa7;margin-left:auto;margin-right:auto;margin-top:10px;padding:8px 35px 8px 14px;position:relative;width:auto;z-index:98;border-radius:4px}.error,.expired,.req,.req_key{color:#c60f13;font-weight:bold}.req_key{font-size:0.75rem}.username{color:#666666;padding:0.5rem 0;padding-right:0.5rem;font-size:0.7rem}.username i.icon,.username i.fa{padding-left:0.2rem;padding-right:0}body.rtl .username{padding-left:0.5rem;padding-right:0;float:left}body.rtl .username i.icon,body.rtl .username i.fa{padding-left:0;padding-right:0.2rem}.item-container form,.form-container form,#datalist-filter-form,#datatable-filter-form,#summary-filter-form,#summary-sections,.thumbnail{border:1px solid #E0E0E0}.widget-container #list-btn-add{margin-bottom:0;position:relative;top:1.0rem}#component{float:none}.map_home{margin:0;margin-top:0.5rem}#content a.help:link{color:#fff;text-decoration:none;margin-right:10px}#content a.help:hover{background-color:#336699;text-decoration:underline}#content a.help:visited{font-weight:normal;color:#666}#content h1,#content h2{font-size:1.3em;font-weight:bolder;background-color:#F7F8F9;padding:0.35rem}#content h2{margin-top:10px;font-size:1.0rem;padding-left:0.7rem}#content h3{font-size:1.1em;padding-bottom:5px}#footer{background:transparent;padding-top:20px;border-top:1px solid #F0F0F0;margin-top:20px;margin-bottom:1rem}#poweredby{margin-right:0.5rem;text-align:right;color:#999;font-size:0.8rem}#poweredby a{color:#999;text-decoration:none;font-size:0.8rem;margin-left:0.2rem}body.rtl #poweredby{text-align:left}.sub-nav.about-menu{color:#999;text-align:left;margin-left:0;margin-right:0}.sub-nav.about-menu a{color:#999}body.rtl .about-menu{text-align:right}form{font-size:0.8rem}form .form-row{padding:0 0.3rem;margin-top:0.75rem !important}form .form-row .button{margin-right:0.5rem}form .form-row .button i.fa{margin-right:0.3rem}form .form-row > .columns:first-child{overflow:hidden}form .form-row > .columns > label{font-weight:bold}form .form-row > a{margin-left:0.625rem}form .form-row select{min-width:4rem}form .form-row .controls span.postfix{height:1.75rem;line-height:1.5rem;font-size:0.8rem}form .form-row .controls span.postfix .fa{line-height:inherit}form .form-row .s3-hierarchy-widget,form .form-row .calendar-widget-container{display:inline-block;vertical-align:middle}form .form-row .inline-component{overflow:auto}form .jstree-anchor{font-size:0.9rem}form label.inline{padding:0}form .gis_loc_select_btn{font-size:0.8em}form .gis_loc_select_btn i{font-size:1.0em;margin-right:4px}form .error{display:block;padding:0.2rem 0 0 0.2rem;margin-top:0;margin-bottom:0.2rem;font-size:0.75rem;font-style:italic;background:transparent;color:#c60f13}form .error_top .error{margin-top:1.4rem;padding:0}form .invalidinput{border:1px solid #c60f13}form .date-clear-btn{font-size:0.75rem;margin-left:0.15rem !important}form .action-lnk{font-weight:normal;font-size:0.75rem}form table.embeddedComponent{margin-top:0.125rem;margin-bottom:0.125rem;border:1px solid #dddddd;border-collapse:separate}form table.embeddedComponent td{border:none;padding-bottom:0.05rem;padding-top:0.15rem;vertical-align:top}form table.embeddedComponent.subform-vertical .add-row td,form table.embeddedComponent.subform-vertical .edit-row td{border-top:1px solid #dddddd;padding:0 0.4rem 0.4rem 0.4rem}form table.embeddedComponent.subform-vertical .add-row td.subform-action,form table.embeddedComponent.subform-vertical .edit-row td.subform-action{vertical-align:bottom}form table.embeddedComponent .label-row td{border-bottom:1px solid #dddddd}form table.embeddedComponent .label-row td:empty{padding:0}form table.embeddedComponent .label-row label{color:black}form table.embeddedComponent tr.inline-form input,form table.embeddedComponent tr.inline-form .btn.date-clear-btn,form table.embeddedComponent tr.inline-form .postfix.calendar-clear-btn{margin-top:0}form table.embeddedComponent tr.inline-form input[type="text"],form table.embeddedComponent tr.inline-form .s3-upload-widget{font-size:0.8rem;max-width:14rem}form table.embeddedComponent tr.inline-form textarea{font-size:0.8rem}form table.embeddedComponent tr.inline-form.single td:only-child{padding:0}form table.embeddedComponent tr.inline-form.single td:only-child div.form-row{padding:0 0.2rem 0.2rem}form table.embeddedComponent tr.inline-form select{max-width:18rem}form table.embeddedComponent tr.inline-form .s3_inline_add_resource_link{padding:0.1rem}form table.embeddedComponent tr.inline-form .zoom img,form table.embeddedComponent tr.read-row .zoom img{max-height:8rem}form table.embeddedComponent tr.inline-form td,form table.embeddedComponent tr.read-row td{font-size:0.8rem;max-width:21rem;white-space:pre-line}form .inline-open-add{display:inline-block;margin-bottom:1.25rem;margin-left:0}form .subheading{background-color:#FFEDCB;font-size:0.9rem;font-weight:bold;border-top:solid 1px #FFD7A3;border-left:solid 1px #FFD7A3;margin:1.5rem 0 0.75rem;padding:0.5rem 0.25rem 0.25rem 0.75rem;max-width:80%}form .subheading:first-of-type{margin-top:0}form.auth_login #submit_record__row{white-space:pre}ul.s3-inline-link{font-size:inherit}input[type="text"],input[type="password"],input[type="date"],input[type="datetime"],input[type="datetime-local"],input[type="month"],input[type="week"],input[type="email"],input[type="number"],input[type="search"],input[type="tel"],input[type="time"],input[type="url"],textarea,select{height:1.75rem;padding:0.25rem !important;display:inline-block;font-weight:normal}input[type="text"]:focus,input[type="password"]:focus,input[type="date"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="month"]:focus,input[type="week"]:focus,input[type="email"]:focus,input[type="number"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="time"]:focus,input[type="url"]:focus,textarea:focus,select:focus{background-color:#fffbe0}input[type="text"],input[type="password"],input[type="date"],input[type="datetime"],input[type="datetime-local"],input[type="month"],input[type="week"],input[type="email"],input[type="number"],input[type="search"],input[type="tel"],input[type="time"],input[type="url"],input[type="file"],select{width:auto !important;max-width:100%}textarea{resize:both;max-width:100%;vertical-align:top;font-size:0.8rem}input.datetimepicker{max-width:8rem}select{padding:0 1rem 0 0.25rem !important}.form-info{min-height:1.2rem}#last_update,#markDuplicate{float:right;clear:right;text-align:right}#markDuplicate{margin-bottom:0;margin-top:-0.5rem}body.rtl #last_update,body.rtl #markDuplicate{float:left;clear:left;text-align:left}.form-container,.item-container{width:auto;overflow:inherit}.form-container form,.item-container form{background:#fefefe;padding:5px 10px}.form-container form tr td,.item-container form tr td{padding:0.1875rem}.form-container form .embeddedComponent td,.item-container form .embeddedComponent td{padding-right:0.5625rem}.form-container .controls,.item-container .controls{display:inline-block;min-height:1.7rem;padding:0 0.1rem}.form-container .controls:not(.columns),.item-container .controls:not(.columns){max-width:100%}.form-container .no-options-available,.item-container .no-options-available{color:#aaa;font-style:italic;padding-left:0.7rem}.form-container{margin-top:0.125rem;margin-bottom:0.875rem}.form-container form select,.form-container form input.string:not(.date),.form-container form textarea{margin:0}.form-container form .ui-multiselect{max-width:100%;display:inline-block !important;font-size:0.8rem;line-height:1.5;margin-right:0.3rem}.form-container form:not(.filter-form):not(.auth_login):not(.auth_register):not(.rm-form) input[type="text"]:not(.integer):not(.double):not(.datetimepicker):not(.date):not(.hours):not(.dms-input){width:20rem !important}.item-container .controls{background:#fafafa;padding:0.3rem;min-height:1.7rem}.filter-form table tr,.filter-form .ui-multiselect,.report-options table tr,.report-options .ui-multiselect{max-width:60%}.filter-form table.s3-groupedopts-widget,.report-options table.s3-groupedopts-widget{display:inline-block}.filter-form label.inline,.report-options label.inline{line-height:normal;padding-top:0.2rem}#login_form,#register_form,#login_box{clear:none !important}.inline-tooltip{display:inline-block;vertical-align:top;padding-left:0.75rem}.inline-tooltip .s3_add_resource_link{display:inline-block;margin:-3px 0 -3px 0.7rem;padding-right:0.3rem}.tooltip,.tooltipbody,.stickytip,.ajaxtip,.htmltip{display:inline-block;position:static;padding:0;text-transform:uppercase;height:20px;background:none}.tooltip.inline-tooltip,.tooltipbody.inline-tooltip,.stickytip.inline-tooltip,.ajaxtip.inline-tooltip,.htmltip.inline-tooltip{margin-left:1rem}.tooltip:before,.tooltipbody:before,.stickytip:before,.ajaxtip:before,.htmltip:before{content:"\f29c";color:#AAA;font:normal normal normal 14px/1 FontAwesome;font-size:1.2rem;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.tooltip:hover,.tooltipbody:hover,.stickytip:hover,.ajaxtip:hover,.htmltip:hover{background:none;cursor:help}#rheader{width:100%}#rheader a.th{margin-right:10px}#rheader a.th img{height:60px}#rheader .rheader-avatar{clear:none;display:inline-block;float:none;padding:0;vertical-align:top}#rheader img.rheader-avatar{margin-right:0.8rem}#rheader .rheader-content{display:inline-block}#rheader .rheader-title{color:#333;font-weight:bold;font-size:1.1rem;line-height:1.0}#rheader table{display:inline;margin-bottom:10px;border:none}#rheader table tr{background:none}div.tabs{display:block;clear:none;height:1.9rem;width:100%;margin:0.5rem 0;padding:0;line-height:1.3rem;text-align:left;border-bottom:1px solid #166068}div.tabs span{float:left;display:inline;position:relative;margin:0.2rem 0.2rem 0 0;padding:0.1rem 0.3rem 0.1rem;border-radius:3px 3px 0 0}div.tabs span a{color:#ffffff !important;text-decoration:none !important;background:transparent !important}div.tabs span a:hover{background:transparent !important}div.tabs span.tab_here{font-size:1rem;font-weight:bold;margin:0.2rem 0.35rem 0 0;padding:0.1rem 0.4rem 0.12rem;background:#ffffff;border-color:#166068;border-style:solid;border-width:0.125rem 2px 0.0625rem 3px;border-bottom-style:solid;border-bottom-color:#ffffff}div.tabs span.tab_here:hover{background-color:#f1edec}div.tabs span.tab_here a{color:#666666 !important}div.tabs span.tab_here a:hover{color:#666666 !important}div.tabs span.tab_last,div.tabs span.tab_other{font-size:0.9rem;margin:0.35rem 0.2rem 0 0;padding:0.1rem 0.3rem 0;background-color:#166068;border-color:#166068;border-width:0.0625rem 1px 0.0625rem 2px;border-style:solid}div.tabs span.tab_last:hover,div.tabs span.tab_other:hover{background-color:#124d53;border-color:#124d53}div.tabs span.tab_last a,div.tabs span.tab_other a{color:#ffffff !important}div.tabs span.tab_last a:hover,div.tabs span.tab_other a:hover{color:#ffffff !important}.action-btn,.delete-btn-ajax,.delete-btn,.selected-action{font-size:0.6875rem;border:0;line-height:1;margin-bottom:inherit;padding:0.25rem 0.5rem 0.375rem;cursor:pointer;text-decoration:none !important;display:inline-block}.action-btn[disabled],.action-btn[disabled]:hover,.delete-btn-ajax[disabled],.delete-btn-ajax[disabled]:hover,.delete-btn[disabled],.delete-btn[disabled]:hover,.selected-action[disabled],.selected-action[disabled]:hover{color:white;background-color:rgba(192,192,192,0.25)}.action-btn,.selected-action{background-color:#166068;color:white !important}.action-btn:hover,.selected-action:hover{background-color:#124d53;color:white !important}.delete-btn-ajax,.delete-btn{background-color:#c60f13;color:white !important}.delete-btn-ajax:hover,.delete-btn:hover{background-color:#9e0c0f !important;color:white !important}.cancel-form-btn{padding:0.9375rem}.cancel-form-btn:hover{color:white;background-color:#166068}.map_home .gis_fullscreen_map-btn{font-weight:normal;font-size:0.8rem;padding:0.2rem}.dataTable .action-btn,.dataTable .selected-action{color:white}.dataTable .delete-btn,.dataTable .delete-btn-ajax{color:white}.dataTable td.actions{white-space:nowrap}.pr-contacts-editable button{font-size:0.6875rem;border:0;line-height:1;margin-bottom:inherit;padding:0.25rem 0.5rem 0.375rem;cursor:pointer;text-decoration:none !important;display:inline-block}#footer button.btn{font-size:0.6875rem;border:0;line-height:1;margin-bottom:inherit;padding:0.25rem 0.5rem 0.375rem;cursor:pointer;text-decoration:none !important;display:inline-block;margin-left:2px;margin-right:2px;color:white;background:#dddddd}#footer button.btn:hover{color:white;background:#a0a0a0}.action-lnk{margin-left:0.6rem}.action-lnk:first-child{margin-left:0}body.rtl .action-lnk{margin-left:0;margin-right:0.6rem}body.rtl .action-lnk:first-child{margin-right:0}.caret{display:inline-block;width:0;height:0;vertical-align:top;border-top:4px solid #000000;border-right:4px solid transparent;border-left:4px solid transparent;content:""}.top-bar{z-index:1000}.top-bar.expanded{width:100%}.top-bar .logo{color:white;display:inline-block;font-size:1.1rem;font-weight:bold;height:35px;padding:0.625rem 0.5rem 0 0.7rem;text-transform:uppercase}.top-bar .menu-toggle input{margin-right:5px}.top-bar li.name{padding-top:0.2rem}.top-bar .home-link{font-size:1.375rem;color:white;padding:0.5rem}.top-bar-section li.menu-home a{font-weight:bold !important;font-size:1.4em !important;text-transform:capitalize !important}.sidebar{background:none repeat scroll 0 0 #d7d8d9;margin-top:10px;padding-top:0.25rem;padding-bottom:0.25rem}.side-nav li{list-style-type:none;list-style-position:inside;line-height:1.2;padding:0}.side-nav li.heading{margin-top:0.125rem}.side-nav li.heading:not(:first-child){border-top:1px solid #c0c1c2}.side-nav li.active > a:first-child:not(.button){font-weight:bold;background-color:#e0e1e2}.side-nav li.active > a:first-child:not(.button):hover{color:#FFFFFF;background-color:#2ba6cb}.main-title .org-logo{vertical-align:top;display:inline-block;padding:0.4rem 0.8rem 0.3rem 0}.main-title .system-title{display:inline-block}.main-title .system-title .system-name{color:#333333}.main-title .system-title .org-name{color:#999999;line-height:1rem}.main-title .system-title h5:first-child,.main-title .system-title h6:first-child{margin:0.3rem 0 0.125rem}.main-title .personal-menu-area{text-align:right}.main-title .personal-menu{float:right;clear:right;padding-top:0;margin-bottom:0.125rem}.main-title .personal-menu li a{padding:0 0.375rem}.main-title .language-selector{float:right;display:block;margin-top:0.625rem;margin-bottom:0.125rem}body.rtl .main-title .personal-menu{float:left;clear:left}body.rtl .main-title .language-selector{float:left}#table-container{margin-bottom:1.5rem}table.dataTable thead th,table.dataTable th,table.dataTable td{border:1px solid #cccccc;padding:0.2em 1.5em 0.2em 0.5em}table.dataTable thead th,table.dataTable thead td{background-color:#ffffff}table.dataTable tbody tr.even{background-color:#ffffff}table.dataTable tbody tr.even td.sorting_1{background-color:#fafafa}table.dataTable tbody tr.odd{background-color:#f7f8f9}table.dataTable tbody tr.odd td.sorting_1{background-color:#f0f1f2}table.dataTable tbody tr.odd td{border-color:#cccccc}table.dataTable tbody tr.row_selected.odd{background-color:#40fa8d}table.dataTable tbody tr.row_selected.odd td.sorting_1{background-color:#20f0ad}table.dataTable tbody tr.row_selected.even{background-color:#60f6ad}table.dataTable tbody tr.row_selected.even td.sorting_1{background-color:#40fa8d}table.dataTable tbody tr.dtalert.odd{background-color:#ffffc0}table.dataTable tbody tr.dtalert.odd td.sorting_1{background-color:#ffffb0}table.dataTable tbody tr.dtalert.even{background-color:#ffffa0}table.dataTable tbody tr.dtalert.even td.sorting_1{background-color:#fffff0}table.dataTable tbody tr.dtwarning.odd{background-color:#ffd9d9}table.dataTable tbody tr.dtwarning.odd td.sorting_1{background-color:#ffb6b6}table.dataTable tbody tr.dtwarning.even{background-color:#ffa6a6}table.dataTable tbody tr.dtwarning.even td.sorting_1{background-color:#ff8383}table.dataTable tfoot th,table.dataTable tfoot td{background-color:#f7f8f9;border-top:2px solid #cccccc;padding:0.5em}table.dataTable .selected-action{margin:5px 0 5px}table.dataTable .bulk-select-options{font-size:0.7rem;font-weight:normal}table.dataTable .bulk-select-options input[type=checkbox]{margin-bottom:0.2rem;margin-right:0.5rem;vertical-align:middle}table.dataTable input.bulkcheckbox[type=checkbox]{margin-top:0.2rem}table.dataTable .group span.ui-icon{display:inline-block}table.dataTable .group .group-indent{width:10px}table.dataTable .group .group-opened,table.dataTable .group .group-closed{padding:0.2rem}table.dataTable .group .group-collapse,table.dataTable .group .group-expand{cursor:pointer;float:right}table.dataTable.dtr-inline.collapsed tbody td:first-child::before,table.dataTable.dtr-inline.collapsed tbody th:first-child::before{top:6px;background-color:#166068}table.dataTable.dtr-inline.collapsed tbody tr.parent td:first-child::before,table.dataTable.dtr-inline.collapsed tbody tr.parent th:first-child::before{top:6px;background-color:#c60f13}table.dataTable table.import-item-details{display:none}.dataTables_length{float:left !important}.dataTables_length label{font-size:0.75rem;white-space:nowrap;margin-right:10em;margin-bottom:0.3em}.dataTables_length select{height:auto;padding:0.2rem 1.1rem 0.2rem 0 !important;font-size:0.75rem}.dataTables_processing{padding:14px 0 28px}.dataTables_filter{text-align:left;font-size:0.75rem;margin-right:3rem}.dataTables_filter input[type="search"]{margin-left:0.2rem}.dt-export-options{float:right}.dt-export-options .list_formats{padding-top:0;margin:0 0.2rem}.dt-export-options .dt-export{margin:0 0.1rem}.dt-export-options .dt-export.fa{font-size:14px;padding:0;padding-top:2px}body.rtl .dt-export-options,body.rtl .list_formats div{float:left}body.rtl .dataTable .group .group-collapse,body.rtl .dataTable .group .group-expand{float:left}.empty{font-style:italic;font-size:0.825rem}.fc table,.fc table tr{background:transparent}.dl .dl-1-cols{width:100%}.dl .dl-header{float:none;text-align:right}.dl .dl-row{border:0}.dl .dl-item .dropdown .caret{margin-left:0.2rem;margin-top:0.4rem}.dl .dl-item .attachments{margin-right:0.2rem}body.rtl .dl .dl-header{text-align:left}body.rtl .dl .dl-item .dropdown .caret{margin-left:0;margin-right:0.2rem}body.rtl .dl .dl-item .attachments{margin-right:0;margin-left:0.2rem}.dl-empty{font-style:italic;font-size:0.825rem}.dl-exports.list_formats{padding:0}.card_1_line,.card_manylines{color:#666;font-size:0.7rem;padding:0.05rem}.card_1_line{height:auto;margin-bottom:0;overflow:hidden}h4.profile-sub-header{background-color:#efefef;padding:0.1rem 0.3rem;font-size:1.2rem}.profile-widget .icon-fullscreen{float:right;position:relative;right:9px;top:17px}.profile .dl-header{display:none}.empty_card-holder{text-align:center}@keyframes blinker{50%{opacity:0}}.ui-widget-header{background:none;border:1px solid #ADA6A0}.ui-widget-header a{color:#222222 !important;border-color:#C7C7C7;margin-bottom:0px;font-weight:bold;text-decoration:none !important;font-size:0.7em}.ui-widget-header.ui-slider-range{background:#cccccc}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active{border-color:#ADA6A0}.ui-widget-content .ui-state-active.ui-slider-handle{background:#007fff;border:1px solid #003eff}.ui-multiselect-header span.ui-icon{margin-top:4px}.ui-multiselect-checkboxes li label input{margin-left:0.2rem;margin-right:0.3rem}.ui-widget{font-family:"Helvetica Neue",Helvetica,Roboto,Arial,sans-serif}#list-btn-add{float:right;margin-right:0.35rem}.ui-tabs .ui-tabs-panel{padding:0.5em}#summary-tabs{margin-top:10px}#summary-tabs.ui-widget-content{background:none;border:none}#summary-tab-headers .ui-tabs-nav{border:none;margin:0 10px}#summary-tab-headers a{border-color:#CCC}#summary-tab-headers .ui-tabs-active a{border-bottom:1px solid white}#summary-tab-headers li{margin-right:5px;box-shadow:none}#summary-sections .x-panel-body{border:none;border-radius:4px}input.date.hasDatepicker{display:inline-block}.ui-datepicker-trigger{background-color:transparent;background-image:url("../../img/bootstrap/calendar.gif");margin-left:10px}.ui-datepicker.ui-widget{z-index:1000 !important}.range-filter-label label{font-size:1.0em}.range-filter-widget input.date-filter-input{width:auto;margin-right:2px}.range-filter-widget button.date-clear-btn{font-size:0.85em;padding:0.325em;margin-top:0.5em}.range-filter-widget .postfix.calendar-clear-btn{margin-left:-2px}.btn.date-clear-btn{font-size:0.85em;padding:0.325em;margin-top:0.5em}.calendar-widget-container{white-space:nowrap}.postfix.calendar-clear-btn{cursor:pointer;display:inline-block;margin-top:2px;width:1.2rem;vertical-align:top}.postfix.calendar-clear-btn .fa{color:#B0B0B0}.s3-groupedopts-label{float:none;margin-bottom:0.3rem}.s3-groupedopts-widget table{margin-left:0;margin-bottom:0.5rem}table.s3-groupedopts-widget{margin-bottom:0}table.s3-groupedopts-widget td{white-space:nowrap}table.s3-groupedopts-widget td input.s3-groupedopts-option{vertical-align:text-top;margin:0 0.3rem 0 0}table.s3-groupedopts-widget td label{white-space:pre-wrap;margin:0 0.5rem}div.s3-groupedopts-widget .s3-groupedopts-item{display:inline-block;padding-top:0.15rem}div.s3-groupedopts-widget input.s3-groupedopts-option{vertical-align:text-top;margin:0 0.3rem 0 0}.imagecrop-btn{font-size:1em;margin-left:0.2em;padding:0.3em}.s3-widget-intro{max-width:42rem;padding:0.5rem 0;font-size:0.8rem}.pr-contacts-wrapper h3{margin-top:0.5rem;padding-bottom:0 !important}.pr-contacts-editable input,.pr-contacts-editable select{min-height:1.2rem;margin:2px 0}.pr-contacts-editable button{font-size:0.6875rem;border:0;line-height:1;margin-bottom:inherit;padding:0.25rem 0.5rem 0.375rem;cursor:pointer;text-decoration:none !important;display:inline-block;margin:2px}.pr-contact-actions{margin-bottom:0.5rem}.pr-contact,.pr-emergency-contact{padding:0.5rem}.pr-contact-priority,.pr-contact-details{display:inline-block}.pr-contact-subtitle{font-size:0.8rem;font-style:italic}.pr-contact-priority{border:thin solid #2ba6cb;border-radius:2px;color:#2ba6cb;font-size:0.8rem;line-height:1rem;margin:0.25rem 1rem 0 0;padding:0 0.25rem 0.125rem;vertical-align:top}.pr-contact-priority input[type=submit]{color:black;line-height:1.2rem;font-size:0.8rem}.controls .checkboxes-widget-s3{display:inline-flex}.comment-box{background:none repeat scroll 0 0 #f5f5f5}.text-body{white-space:pre-wrap}#close-iframe-map{padding:7px;margin-top:5px}.s3-truncate-more,.s3-truncate-less{font-style:italic}.s3-truncate-more:before,.s3-truncate-less:before{font-style:italic;content:"..."}.box_bottom{clear:left}.ui-menu-item .pe-label{font-size:0.7rem;color:#A0A0A0;vertical-align:super}.qrinput{display:inline-block}.qrinput .qrscan-btn{padding:0.4rem 1.2rem;margin:3px 0.5rem;vertical-align:top}.qrinput-success{text-align:center;font-size:4rem;padding:1rem;color:darkgreen}form.anonymize-form,.anonymize-success{padding:1rem}.anonymize-select,.anonymize-confirm{margin-left:1rem}.anonymize-buttons{margin-top:1rem}.anonymize-btn{background-color:#c60f13}.anonymize-btn:hover{background-color:#9e0c0f}.s3-anonymize{display:inline-block;margin:0.2rem}.rm-form .controls{width:100%}.rm-form .rm-assign .action-lnk,.rm-form .rm-rules .action-lnk{display:inline-block;color:#166068;font-style:normal}.rm-form .rm-toggle-all{margin-top:0.2rem;margin-bottom:0.5rem}.rm-form .rm-assign{width:80%;border-collapse:separate;border-spacing:0}.rm-form .rm-assign th{min-width:10rem}.rm-form .rm-assign th:first-child{width:25%}.rm-form .rm-assign td{padding:0.5rem}.rm-form .rm-assign tfoot td{border-top:1px solid #C0C0C0}.rm-form .rm-assign .rm-item-name,.rm-form .rm-assign .rm-item-title{display:block}.rm-form .rm-assign .rm-item-title{font-size:0.8rem;font-style:italic;color:#A0A0A0}.rm-form .rm-assign .rm-duplicate td{border-top:1px solid red;border-bottom:1px solid red}.rm-form .rm-assign .rm-duplicate td:first-child{border-left:1px solid red}.rm-form .rm-assign .rm-duplicate td:last-child{border-right:1px solid red}.rm-form .rm-module-rules{width:100%}.rm-form .rm-module-rules .rm-module-header{cursor:pointer;background-color:#F0F0F0}.rm-form .rm-module-rules .rm-module-header div{margin-right:0.3rem;display:inline-block}.rm-form .rm-module-rules .rm-module-header .rm-module-toggle{width:0.5rem}.rm-form .rm-module-rules .rm-module-header .rm-module-prefix{width:6.5rem}.rm-form .rm-module-rules .rm-module-header .rm-module-numrules{font-weight:normal}.rm-form .rm-module-rules.hasrules .rm-module-header{background-color:#D0D0D0}.rm-form .rm-module-rules th,.rm-form .rm-module-rules td{padding:0.5rem}.rm-form .rm-module-rules .rm-default-rule td,.rm-form .rm-module-rules .rm-default-permissions td{font-style:italic;color:#a0a0a0}.rm-form .rm-module-rules .rm-rule-target{width:20%}.rm-form input.rm-invalid{background-color:#FFA0A0}.rm-fixed,.rm-hint{display:block;font-style:italic;font-size:0.8rem;color:#AAAAAA}.s3-organizer-popup label{font-size:0.7rem;font-weight:bold}.s3-organizer-popup p{font-size:0.8rem;font-weight:normal;margin-bottom:0.3rem}.s3-organizer-popup .action-btn{margin-left:0;margin-right:0.3rem}.s3-organizer-create .action-btn{display:block}.consent-widget{border:1px solid #E0E0E0;padding:0.5rem 1rem}.consent-widget .consent-option{margin-top:0.3rem}.consent-widget .consent-explanation{white-space:pre-wrap;padding:0.3rem 2rem;font-size:0.8rem;font-style:italic;color:#7F7F7F}.consent-widget .consent-title{font-weight:bold}.consent-widget .consent-checkbox{vertical-align:bottom;margin-right:0.5rem}.consent-widget .req{padding:0 0.2rem}.consent-widget .req_key{font-size:0.7rem;font-weight:normal;font-style:italic;margin:0 0.5rem}.prio{padding:0 4px;border:1px solid black;text-align:center;white-space:pre;font-weight:normal}.prio-red{color:white;background-color:#d10000}.prio-blue{color:white;background-color:#10427b}.prio-lightblue{background-color:#b7ddff}.prio-grey{background-color:silver}.wh-intro{max-width:42rem;padding:0.5rem 0;font-size:0.8rem}.wh-raster{margin-top:0.3rem}.wh-raster tr{border-bottom:1px solid #ccc}.wh-raster td{border-left:1px solid #ccc}.wh-raster td.wh-tick{border-left-width:2px}.wh-raster thead td{min-width:1.8rem;padding-right:0.6rem !important;font-size:0.6rem;font-weight:normal}.wh-raster tbody td.wh-hour{text-align:center;font-weight:normal}.wh-raster tbody td.wh-hour.wh-on{color:darkgreen}.wh-raster tbody td.wh-day{text-align:left;font-size:0.7rem;padding:0.2rem 0.4rem}.wh-raster .wh-hour{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.wh-raster .wh-hour.wh-on{background-color:lightgreen}.wh-raster .wh-hour.wh-off{background-color:transparent}.wh-schedule{list-style:none;white-space:pre;margin:0;font-size:0.7rem}.wh-schedule .wh-dayname{width:1.5rem;display:inline-block}.wh-schedule .wh-dayname::after{content:":"}.announcements-title{font-variant:petite-caps;font-weight:normal}.announcements{margin:0;padding:0 0 0 1rem;border-left:1px solid #166068;list-style:none outside none}.announcements ul,.announcements ol{padding-left:20px;list-style:none outside none}.announcements li{padding:10px 0 0}.announcements .announcement-box{display:block;background:#fff7d9;border:1px solid #aaa;overflow:hidden;padding:0.7rem;margin-bottom:1.5rem}.announcements .announcement-box.announcement-important .announcement-icon{color:#0088ca}.announcements .announcement-box.announcement-critical{border:1px solid #ca0000;background-color:#ffebea}.announcements .announcement-box.announcement-critical .announcement-icon{color:#ca0000;animation:blinker 0.7s step-start 4}.announcements .announcement-box .announcement-header .announcement-icon{display:inline-block;font-size:1.3rem;padding:0 0.7rem 0 0}.announcements .announcement-box .announcement-header h4{display:inline-block}.announcements .announcement-box .announcement-text{font-size:0.9rem;line-height:1.3}.announcements .announcement-box .announcement-text h4{font-size:1.2rem}.announcements .announcement-box .announcement-text ul{list-style:disc outside none}.announcements .announcement-box .announcement-text ol{list-style:decimal outside none}.announcements .announcement-box .announcement-text li{padding:0}.announcements .announcement-box .announcement-text div{white-space:pre-line}.announcements .announcement-box .announcement-text .announcement-header{margin:0 0 0.8rem 0}.announcements .announcement-box .announcement-text announcement-body{white-space:pre-line}.announcements .announcement-box .announcement-text announcement-body p{margin-left:1rem}.announcements .announcement-box .announcement-date{font-size:0.7rem;margin-top:0.4rem}body.rtl .ui-multiselect-header .ui-multiselect-filter{direction:rtl;float:right;margin-right:3px;margin-left:10px}body.rtl .ui-multiselect-header ul li{float:right;padding:0 3px 0 10px}body.rtl .ui-multiselect-header li.ui-multiselect-close{float:left;text-align:left;padding-left:0}body.rtl .range-filter-widget input.date-filter-input{margin-right:0;margin-left:2px}body.rtl #list-btn-add{float:left;margin-right:0;margin-left:0.35rem}body.rtl .rm-form .rm-module-rules .rm-module-header div{margin-right:0;margin-left:0.3rem}body.rtl .rm-form .rm-assign .rm-duplicate td:first-child{border-left:0;border-right:1px solid red}body.rtl .rm-form .rm-assign .rm-duplicate td:last-child{border-right:0;border-left:1px solid red} \ No newline at end of file diff --git a/static/themes/default/scss/theme/_widgets.scss b/static/themes/default/scss/theme/_widgets.scss index f56d6cfbe9..f76c341330 100644 --- a/static/themes/default/scss/theme/_widgets.scss +++ b/static/themes/default/scss/theme/_widgets.scss @@ -18,6 +18,13 @@ $anonymize_button_bgcolor_hover: $alert-button-bg-hover !default; // ============================================================================ // STYLES +/* Blinker configuration */ +@keyframes blinker { + 50% { + opacity: 0; + } +} + /* jQuery UI widgets */ .ui-widget-header { background: none; diff --git a/static/themes/default/theme.css b/static/themes/default/theme.css index 4e23cdf1e6..53eb0885cb 100644 --- a/static/themes/default/theme.css +++ b/static/themes/default/theme.css @@ -1475,13 +1475,19 @@ h4.profile-sub-header { text-align: center; } +/* Blinker configuration */ +@keyframes blinker { + 50% { + opacity: 0; + } +} /* jQuery UI widgets */ -/* line 22, ../scss/theme/_widgets.scss */ +/* line 29, ../scss/theme/_widgets.scss */ .ui-widget-header { background: none; border: 1px solid #ADA6A0; } -/* line 25, ../scss/theme/_widgets.scss */ +/* line 32, ../scss/theme/_widgets.scss */ .ui-widget-header a { color: #222222 !important; border-color: #C7C7C7; @@ -1491,141 +1497,141 @@ h4.profile-sub-header { font-size: 0.7em; } -/* line 35, ../scss/theme/_widgets.scss */ +/* line 42, ../scss/theme/_widgets.scss */ .ui-widget-header.ui-slider-range { background: #cccccc; } -/* line 38, ../scss/theme/_widgets.scss */ +/* line 45, ../scss/theme/_widgets.scss */ .ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { border-color: #ADA6A0; } -/* line 43, ../scss/theme/_widgets.scss */ +/* line 50, ../scss/theme/_widgets.scss */ .ui-widget-content .ui-state-active.ui-slider-handle { background: #007fff; border: 1px solid #003eff; } -/* line 48, ../scss/theme/_widgets.scss */ +/* line 55, ../scss/theme/_widgets.scss */ .ui-multiselect-header span.ui-icon { margin-top: 4px; } -/* line 52, ../scss/theme/_widgets.scss */ +/* line 59, ../scss/theme/_widgets.scss */ .ui-multiselect-checkboxes li label input { margin-left: 0.2rem; margin-right: 0.3rem; } -/* line 56, ../scss/theme/_widgets.scss */ +/* line 63, ../scss/theme/_widgets.scss */ .ui-widget { font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; } /* List-Add Button */ -/* line 61, ../scss/theme/_widgets.scss */ +/* line 68, ../scss/theme/_widgets.scss */ #list-btn-add { float: right; margin-right: 0.35rem; } /* Summary tabs */ -/* line 67, ../scss/theme/_widgets.scss */ +/* line 74, ../scss/theme/_widgets.scss */ .ui-tabs .ui-tabs-panel { padding: 0.5em; } -/* line 70, ../scss/theme/_widgets.scss */ +/* line 77, ../scss/theme/_widgets.scss */ #summary-tabs { margin-top: 10px; } -/* line 72, ../scss/theme/_widgets.scss */ +/* line 79, ../scss/theme/_widgets.scss */ #summary-tabs.ui-widget-content { background: none; border: none; } -/* line 78, ../scss/theme/_widgets.scss */ +/* line 85, ../scss/theme/_widgets.scss */ #summary-tab-headers .ui-tabs-nav { border: none; margin: 0 10px; } -/* line 82, ../scss/theme/_widgets.scss */ +/* line 89, ../scss/theme/_widgets.scss */ #summary-tab-headers a { border-color: #CCC; } -/* line 85, ../scss/theme/_widgets.scss */ +/* line 92, ../scss/theme/_widgets.scss */ #summary-tab-headers .ui-tabs-active a { border-bottom: 1px solid white; } -/* line 88, ../scss/theme/_widgets.scss */ +/* line 95, ../scss/theme/_widgets.scss */ #summary-tab-headers li { margin-right: 5px; box-shadow: none; } -/* line 95, ../scss/theme/_widgets.scss */ +/* line 102, ../scss/theme/_widgets.scss */ #summary-sections .x-panel-body { border: none; border-radius: 4px; } /* Date picker */ -/* line 102, ../scss/theme/_widgets.scss */ +/* line 109, ../scss/theme/_widgets.scss */ input.date.hasDatepicker { display: inline-block; } -/* line 105, ../scss/theme/_widgets.scss */ +/* line 112, ../scss/theme/_widgets.scss */ .ui-datepicker-trigger { background-color: transparent; background-image: url("../../img/bootstrap/calendar.gif"); margin-left: 10px; } -/* line 110, ../scss/theme/_widgets.scss */ +/* line 117, ../scss/theme/_widgets.scss */ .ui-datepicker.ui-widget { z-index: 1000 !important; } /* Range filter widgets */ -/* line 115, ../scss/theme/_widgets.scss */ +/* line 122, ../scss/theme/_widgets.scss */ .range-filter-label label { font-size: 1.0em; } -/* line 119, ../scss/theme/_widgets.scss */ +/* line 126, ../scss/theme/_widgets.scss */ .range-filter-widget input.date-filter-input { width: auto; margin-right: 2px; } -/* line 123, ../scss/theme/_widgets.scss */ +/* line 130, ../scss/theme/_widgets.scss */ .range-filter-widget button.date-clear-btn { font-size: 0.85em; padding: 0.325em; margin-top: 0.5em; } -/* line 128, ../scss/theme/_widgets.scss */ +/* line 135, ../scss/theme/_widgets.scss */ .range-filter-widget .postfix.calendar-clear-btn { margin-left: -2px; } -/* line 132, ../scss/theme/_widgets.scss */ +/* line 139, ../scss/theme/_widgets.scss */ .btn.date-clear-btn { font-size: 0.85em; padding: 0.325em; margin-top: 0.5em; } -/* line 137, ../scss/theme/_widgets.scss */ +/* line 144, ../scss/theme/_widgets.scss */ .calendar-widget-container { white-space: nowrap; } -/* line 140, ../scss/theme/_widgets.scss */ +/* line 147, ../scss/theme/_widgets.scss */ .postfix.calendar-clear-btn { cursor: pointer; display: inline-block; @@ -1633,57 +1639,57 @@ input.date.hasDatepicker { width: 1.2rem; vertical-align: top; } -/* line 142, ../scss/theme/_widgets.scss */ +/* line 149, ../scss/theme/_widgets.scss */ .postfix.calendar-clear-btn .fa { color: #B0B0B0; } /* GroupedOptions widget */ -/* line 152, ../scss/theme/_widgets.scss */ +/* line 159, ../scss/theme/_widgets.scss */ .s3-groupedopts-label { float: none; margin-bottom: 0.3rem; } -/* line 156, ../scss/theme/_widgets.scss */ +/* line 163, ../scss/theme/_widgets.scss */ .s3-groupedopts-widget table { margin-left: 0; margin-bottom: 0.5rem; } -/* line 160, ../scss/theme/_widgets.scss */ +/* line 167, ../scss/theme/_widgets.scss */ table.s3-groupedopts-widget { /* border: 0; TODO make configurable */ margin-bottom: 0; } -/* line 161, ../scss/theme/_widgets.scss */ +/* line 168, ../scss/theme/_widgets.scss */ table.s3-groupedopts-widget td { white-space: nowrap; } -/* line 163, ../scss/theme/_widgets.scss */ +/* line 170, ../scss/theme/_widgets.scss */ table.s3-groupedopts-widget td input.s3-groupedopts-option { vertical-align: text-top; margin: 0 0.3rem 0 0; } -/* line 167, ../scss/theme/_widgets.scss */ +/* line 174, ../scss/theme/_widgets.scss */ table.s3-groupedopts-widget td label { white-space: pre-wrap; margin: 0 0.5rem; } -/* line 176, ../scss/theme/_widgets.scss */ +/* line 183, ../scss/theme/_widgets.scss */ div.s3-groupedopts-widget .s3-groupedopts-item { display: inline-block; padding-top: 0.15rem; } -/* line 180, ../scss/theme/_widgets.scss */ +/* line 187, ../scss/theme/_widgets.scss */ div.s3-groupedopts-widget input.s3-groupedopts-option { vertical-align: text-top; margin: 0 0.3rem 0 0; } /* ImageCropWidget */ -/* line 187, ../scss/theme/_widgets.scss */ +/* line 194, ../scss/theme/_widgets.scss */ .imagecrop-btn { font-size: 1em; margin-left: 0.2em; @@ -1691,7 +1697,7 @@ div.s3-groupedopts-widget input.s3-groupedopts-option { } /* Widget Intro-Wrapper */ -/* line 194, ../scss/theme/_widgets.scss */ +/* line 201, ../scss/theme/_widgets.scss */ .s3-widget-intro { max-width: 42rem; padding: 0.5rem 0; @@ -1699,18 +1705,18 @@ div.s3-groupedopts-widget input.s3-groupedopts-option { } /* pr_Contacts widgets */ -/* line 201, ../scss/theme/_widgets.scss */ +/* line 208, ../scss/theme/_widgets.scss */ .pr-contacts-wrapper h3 { margin-top: 0.5rem; padding-bottom: 0 !important; } -/* line 206, ../scss/theme/_widgets.scss */ +/* line 213, ../scss/theme/_widgets.scss */ .pr-contacts-editable input, .pr-contacts-editable select { min-height: 1.2rem; margin: 2px 0; } -/* line 210, ../scss/theme/_widgets.scss */ +/* line 217, ../scss/theme/_widgets.scss */ .pr-contacts-editable button { font-size: 0.6875rem; border: 0; @@ -1723,30 +1729,30 @@ div.s3-groupedopts-widget input.s3-groupedopts-option { margin: 2px; } -/* line 215, ../scss/theme/_widgets.scss */ +/* line 222, ../scss/theme/_widgets.scss */ .pr-contact-actions { margin-bottom: 0.5rem; } -/* line 218, ../scss/theme/_widgets.scss */ +/* line 225, ../scss/theme/_widgets.scss */ .pr-contact, .pr-emergency-contact { padding: 0.5rem; } -/* line 222, ../scss/theme/_widgets.scss */ +/* line 229, ../scss/theme/_widgets.scss */ .pr-contact-priority, .pr-contact-details { display: inline-block; } -/* line 226, ../scss/theme/_widgets.scss */ +/* line 233, ../scss/theme/_widgets.scss */ .pr-contact-subtitle { font-size: 0.8rem; font-style: italic; } -/* line 230, ../scss/theme/_widgets.scss */ +/* line 237, ../scss/theme/_widgets.scss */ .pr-contact-priority { border: thin solid #2ba6cb; border-radius: 2px; @@ -1757,7 +1763,7 @@ div.s3-groupedopts-widget input.s3-groupedopts-option { padding: 0 0.25rem 0.125rem; vertical-align: top; } -/* line 239, ../scss/theme/_widgets.scss */ +/* line 246, ../scss/theme/_widgets.scss */ .pr-contact-priority input[type=submit] { color: black; line-height: 1.2rem; @@ -1765,38 +1771,38 @@ div.s3-groupedopts-widget input.s3-groupedopts-option { } /* Checkboxes Widget */ -/* line 247, ../scss/theme/_widgets.scss */ +/* line 254, ../scss/theme/_widgets.scss */ .controls .checkboxes-widget-s3 { display: inline-flex; } /* Comment feeds */ -/* line 252, ../scss/theme/_widgets.scss */ +/* line 259, ../scss/theme/_widgets.scss */ .comment-box { background: none repeat scroll 0 0 #f5f5f5; } /* Text representation */ -/* line 257, ../scss/theme/_widgets.scss */ +/* line 264, ../scss/theme/_widgets.scss */ .text-body { white-space: pre-wrap; } /* Iframe Map (Location Represent links) TODO move into page or forms? */ -/* line 262, ../scss/theme/_widgets.scss */ +/* line 269, ../scss/theme/_widgets.scss */ #close-iframe-map { padding: 7px; margin-top: 5px; } /* Truncated Texts */ -/* line 268, ../scss/theme/_widgets.scss */ +/* line 275, ../scss/theme/_widgets.scss */ .s3-truncate-more, .s3-truncate-less { font-style: italic; } -/* line 272, ../scss/theme/_widgets.scss */ +/* line 279, ../scss/theme/_widgets.scss */ .s3-truncate-more:before, .s3-truncate-less:before { font-style: italic; @@ -1804,12 +1810,12 @@ div.s3-groupedopts-widget input.s3-groupedopts-option { } /* AddPersonWidget */ -/* line 279, ../scss/theme/_widgets.scss */ +/* line 286, ../scss/theme/_widgets.scss */ .box_bottom { clear: left; } -/* line 282, ../scss/theme/_widgets.scss */ +/* line 289, ../scss/theme/_widgets.scss */ .ui-menu-item .pe-label { font-size: 0.7rem; color: #A0A0A0; @@ -1817,18 +1823,18 @@ div.s3-groupedopts-widget input.s3-groupedopts-option { } /* QR Input */ -/* line 289, ../scss/theme/_widgets.scss */ +/* line 296, ../scss/theme/_widgets.scss */ .qrinput { display: inline-block; } -/* line 291, ../scss/theme/_widgets.scss */ +/* line 298, ../scss/theme/_widgets.scss */ .qrinput .qrscan-btn { padding: 0.4rem 1.2rem; margin: 3px 0.5rem; vertical-align: top; } -/* line 297, ../scss/theme/_widgets.scss */ +/* line 304, ../scss/theme/_widgets.scss */ .qrinput-success { text-align: center; font-size: 4rem; @@ -1837,151 +1843,151 @@ div.s3-groupedopts-widget input.s3-groupedopts-option { } /* Anonymize-Widget */ -/* line 305, ../scss/theme/_widgets.scss */ +/* line 312, ../scss/theme/_widgets.scss */ form.anonymize-form, .anonymize-success { padding: 1rem; } -/* line 309, ../scss/theme/_widgets.scss */ +/* line 316, ../scss/theme/_widgets.scss */ .anonymize-select, .anonymize-confirm { margin-left: 1rem; } -/* line 313, ../scss/theme/_widgets.scss */ +/* line 320, ../scss/theme/_widgets.scss */ .anonymize-buttons { margin-top: 1rem; } -/* line 316, ../scss/theme/_widgets.scss */ +/* line 323, ../scss/theme/_widgets.scss */ .anonymize-btn { background-color: #c60f13; } -/* line 318, ../scss/theme/_widgets.scss */ +/* line 325, ../scss/theme/_widgets.scss */ .anonymize-btn:hover { background-color: #9e0c0f; } -/* line 322, ../scss/theme/_widgets.scss */ +/* line 329, ../scss/theme/_widgets.scss */ .s3-anonymize { display: inline-block; margin: 0.2rem; } /* Role Manager */ -/* line 329, ../scss/theme/_widgets.scss */ +/* line 336, ../scss/theme/_widgets.scss */ .rm-form .controls { width: 100%; } -/* line 332, ../scss/theme/_widgets.scss */ +/* line 339, ../scss/theme/_widgets.scss */ .rm-form .rm-assign .action-lnk, .rm-form .rm-rules .action-lnk { display: inline-block; color: #166068; font-style: normal; } -/* line 338, ../scss/theme/_widgets.scss */ +/* line 345, ../scss/theme/_widgets.scss */ .rm-form .rm-toggle-all { margin-top: 0.2rem; margin-bottom: 0.5rem; } -/* line 342, ../scss/theme/_widgets.scss */ +/* line 349, ../scss/theme/_widgets.scss */ .rm-form .rm-assign { width: 80%; border-collapse: separate; border-spacing: 0; } -/* line 346, ../scss/theme/_widgets.scss */ +/* line 353, ../scss/theme/_widgets.scss */ .rm-form .rm-assign th { min-width: 10rem; } -/* line 349, ../scss/theme/_widgets.scss */ +/* line 356, ../scss/theme/_widgets.scss */ .rm-form .rm-assign th:first-child { width: 25%; } -/* line 352, ../scss/theme/_widgets.scss */ +/* line 359, ../scss/theme/_widgets.scss */ .rm-form .rm-assign td { padding: 0.5rem; } -/* line 355, ../scss/theme/_widgets.scss */ +/* line 362, ../scss/theme/_widgets.scss */ .rm-form .rm-assign tfoot td { border-top: 1px solid #C0C0C0; } -/* line 358, ../scss/theme/_widgets.scss */ +/* line 365, ../scss/theme/_widgets.scss */ .rm-form .rm-assign .rm-item-name, .rm-form .rm-assign .rm-item-title { display: block; } -/* line 362, ../scss/theme/_widgets.scss */ +/* line 369, ../scss/theme/_widgets.scss */ .rm-form .rm-assign .rm-item-title { font-size: 0.8rem; font-style: italic; color: #A0A0A0; } -/* line 368, ../scss/theme/_widgets.scss */ +/* line 375, ../scss/theme/_widgets.scss */ .rm-form .rm-assign .rm-duplicate td { border-top: 1px solid red; border-bottom: 1px solid red; } -/* line 371, ../scss/theme/_widgets.scss */ +/* line 378, ../scss/theme/_widgets.scss */ .rm-form .rm-assign .rm-duplicate td:first-child { border-left: 1px solid red; } -/* line 374, ../scss/theme/_widgets.scss */ +/* line 381, ../scss/theme/_widgets.scss */ .rm-form .rm-assign .rm-duplicate td:last-child { border-right: 1px solid red; } -/* line 380, ../scss/theme/_widgets.scss */ +/* line 387, ../scss/theme/_widgets.scss */ .rm-form .rm-module-rules { width: 100%; } -/* line 382, ../scss/theme/_widgets.scss */ +/* line 389, ../scss/theme/_widgets.scss */ .rm-form .rm-module-rules .rm-module-header { cursor: pointer; background-color: #F0F0F0; } -/* line 385, ../scss/theme/_widgets.scss */ +/* line 392, ../scss/theme/_widgets.scss */ .rm-form .rm-module-rules .rm-module-header div { margin-right: 0.3rem; display: inline-block; } -/* line 389, ../scss/theme/_widgets.scss */ +/* line 396, ../scss/theme/_widgets.scss */ .rm-form .rm-module-rules .rm-module-header .rm-module-toggle { width: 0.5rem; } -/* line 392, ../scss/theme/_widgets.scss */ +/* line 399, ../scss/theme/_widgets.scss */ .rm-form .rm-module-rules .rm-module-header .rm-module-prefix { width: 6.5rem; } -/* line 395, ../scss/theme/_widgets.scss */ +/* line 402, ../scss/theme/_widgets.scss */ .rm-form .rm-module-rules .rm-module-header .rm-module-numrules { font-weight: normal; } -/* line 399, ../scss/theme/_widgets.scss */ +/* line 406, ../scss/theme/_widgets.scss */ .rm-form .rm-module-rules.hasrules .rm-module-header { background-color: #D0D0D0; } -/* line 403, ../scss/theme/_widgets.scss */ +/* line 410, ../scss/theme/_widgets.scss */ .rm-form .rm-module-rules th, .rm-form .rm-module-rules td { padding: 0.5rem; } -/* line 407, ../scss/theme/_widgets.scss */ +/* line 414, ../scss/theme/_widgets.scss */ .rm-form .rm-module-rules .rm-default-rule td, .rm-form .rm-module-rules .rm-default-permissions td { font-style: italic; color: #a0a0a0; } -/* line 412, ../scss/theme/_widgets.scss */ +/* line 419, ../scss/theme/_widgets.scss */ .rm-form .rm-module-rules .rm-rule-target { width: 20%; } -/* line 416, ../scss/theme/_widgets.scss */ +/* line 423, ../scss/theme/_widgets.scss */ .rm-form input.rm-invalid { background-color: #FFA0A0; } -/* line 420, ../scss/theme/_widgets.scss */ +/* line 427, ../scss/theme/_widgets.scss */ .rm-fixed, .rm-hint { display: block; @@ -1991,39 +1997,39 @@ form.anonymize-form, } /* Organizer */ -/* line 430, ../scss/theme/_widgets.scss */ +/* line 437, ../scss/theme/_widgets.scss */ .s3-organizer-popup label { font-size: 0.7rem; font-weight: bold; } -/* line 434, ../scss/theme/_widgets.scss */ +/* line 441, ../scss/theme/_widgets.scss */ .s3-organizer-popup p { font-size: 0.8rem; font-weight: normal; margin-bottom: 0.3rem; } -/* line 439, ../scss/theme/_widgets.scss */ +/* line 446, ../scss/theme/_widgets.scss */ .s3-organizer-popup .action-btn { margin-left: 0; margin-right: 0.3rem; } -/* line 445, ../scss/theme/_widgets.scss */ +/* line 452, ../scss/theme/_widgets.scss */ .s3-organizer-create .action-btn { display: block; } /* Consent Widget */ -/* line 451, ../scss/theme/_widgets.scss */ +/* line 458, ../scss/theme/_widgets.scss */ .consent-widget { border: 1px solid #E0E0E0; padding: 0.5rem 1rem; } -/* line 454, ../scss/theme/_widgets.scss */ +/* line 461, ../scss/theme/_widgets.scss */ .consent-widget .consent-option { margin-top: 0.3rem; } -/* line 457, ../scss/theme/_widgets.scss */ +/* line 464, ../scss/theme/_widgets.scss */ .consent-widget .consent-explanation { white-space: pre-wrap; padding: 0.3rem 2rem; @@ -2031,20 +2037,20 @@ form.anonymize-form, font-style: italic; color: #7F7F7F; } -/* line 464, ../scss/theme/_widgets.scss */ +/* line 471, ../scss/theme/_widgets.scss */ .consent-widget .consent-title { font-weight: bold; } -/* line 467, ../scss/theme/_widgets.scss */ +/* line 474, ../scss/theme/_widgets.scss */ .consent-widget .consent-checkbox { vertical-align: bottom; margin-right: 0.5rem; } -/* line 471, ../scss/theme/_widgets.scss */ +/* line 478, ../scss/theme/_widgets.scss */ .consent-widget .req { padding: 0 0.2rem; } -/* line 474, ../scss/theme/_widgets.scss */ +/* line 481, ../scss/theme/_widgets.scss */ .consent-widget .req_key { font-size: 0.7rem; font-weight: normal; @@ -2053,7 +2059,7 @@ form.anonymize-form, } /* Priority Representation */ -/* line 483, ../scss/theme/_widgets.scss */ +/* line 490, ../scss/theme/_widgets.scss */ .prio { padding: 0 4px; border: 1px solid black; @@ -2062,75 +2068,75 @@ form.anonymize-form, font-weight: normal; } -/* line 490, ../scss/theme/_widgets.scss */ +/* line 497, ../scss/theme/_widgets.scss */ .prio-red { color: white; background-color: #d10000; } -/* line 494, ../scss/theme/_widgets.scss */ +/* line 501, ../scss/theme/_widgets.scss */ .prio-blue { color: white; background-color: #10427b; } -/* line 498, ../scss/theme/_widgets.scss */ +/* line 505, ../scss/theme/_widgets.scss */ .prio-lightblue { background-color: #b7ddff; } -/* line 501, ../scss/theme/_widgets.scss */ +/* line 508, ../scss/theme/_widgets.scss */ .prio-grey { background-color: silver; } /* Weekly Hours Widget */ -/* line 506, ../scss/theme/_widgets.scss */ +/* line 513, ../scss/theme/_widgets.scss */ .wh-intro { max-width: 42rem; padding: 0.5rem 0; font-size: 0.8rem; } -/* line 511, ../scss/theme/_widgets.scss */ +/* line 518, ../scss/theme/_widgets.scss */ .wh-raster { margin-top: 0.3rem; } -/* line 513, ../scss/theme/_widgets.scss */ +/* line 520, ../scss/theme/_widgets.scss */ .wh-raster tr { border-bottom: 1px solid #ccc; } -/* line 516, ../scss/theme/_widgets.scss */ +/* line 523, ../scss/theme/_widgets.scss */ .wh-raster td { border-left: 1px solid #ccc; } -/* line 518, ../scss/theme/_widgets.scss */ +/* line 525, ../scss/theme/_widgets.scss */ .wh-raster td.wh-tick { border-left-width: 2px; } -/* line 523, ../scss/theme/_widgets.scss */ +/* line 530, ../scss/theme/_widgets.scss */ .wh-raster thead td { min-width: 1.8rem; padding-right: 0.6rem !important; font-size: 0.6rem; font-weight: normal; } -/* line 532, ../scss/theme/_widgets.scss */ +/* line 539, ../scss/theme/_widgets.scss */ .wh-raster tbody td.wh-hour { text-align: center; font-weight: normal; } -/* line 535, ../scss/theme/_widgets.scss */ +/* line 542, ../scss/theme/_widgets.scss */ .wh-raster tbody td.wh-hour.wh-on { color: darkgreen; } -/* line 539, ../scss/theme/_widgets.scss */ +/* line 546, ../scss/theme/_widgets.scss */ .wh-raster tbody td.wh-day { text-align: left; font-size: 0.7rem; padding: 0.2rem 0.4rem; } -/* line 546, ../scss/theme/_widgets.scss */ +/* line 553, ../scss/theme/_widgets.scss */ .wh-raster .wh-hour { -webkit-touch-callout: none; -webkit-user-select: none; @@ -2139,56 +2145,56 @@ form.anonymize-form, -ms-user-select: none; user-select: none; } -/* line 554, ../scss/theme/_widgets.scss */ +/* line 561, ../scss/theme/_widgets.scss */ .wh-raster .wh-hour.wh-on { background-color: lightgreen; } -/* line 557, ../scss/theme/_widgets.scss */ +/* line 564, ../scss/theme/_widgets.scss */ .wh-raster .wh-hour.wh-off { background-color: transparent; } /* Weekly Hours Representation */ -/* line 564, ../scss/theme/_widgets.scss */ +/* line 571, ../scss/theme/_widgets.scss */ .wh-schedule { list-style: none; white-space: pre; margin: 0; font-size: 0.7rem; } -/* line 569, ../scss/theme/_widgets.scss */ +/* line 576, ../scss/theme/_widgets.scss */ .wh-schedule .wh-dayname { width: 1.5rem; display: inline-block; } -/* line 573, ../scss/theme/_widgets.scss */ +/* line 580, ../scss/theme/_widgets.scss */ .wh-schedule .wh-dayname::after { content: ":"; } -/* line 578, ../scss/theme/_widgets.scss */ +/* line 585, ../scss/theme/_widgets.scss */ .announcements-title { font-variant: petite-caps; font-weight: normal; } -/* line 582, ../scss/theme/_widgets.scss */ +/* line 589, ../scss/theme/_widgets.scss */ .announcements { margin: 0; padding: 0 0 0 1rem; border-left: 1px solid #166068; list-style: none outside none; } -/* line 587, ../scss/theme/_widgets.scss */ +/* line 594, ../scss/theme/_widgets.scss */ .announcements ul, .announcements ol { padding-left: 20px; list-style: none outside none; } -/* line 591, ../scss/theme/_widgets.scss */ +/* line 598, ../scss/theme/_widgets.scss */ .announcements li { padding: 10px 0 0; } -/* line 594, ../scss/theme/_widgets.scss */ +/* line 601, ../scss/theme/_widgets.scss */ .announcements .announcement-box { display: block; background: #fff7d9; @@ -2197,113 +2203,113 @@ form.anonymize-form, padding: 0.7rem; margin-bottom: 1.5rem; } -/* line 602, ../scss/theme/_widgets.scss */ +/* line 609, ../scss/theme/_widgets.scss */ .announcements .announcement-box.announcement-important .announcement-icon { color: #0088ca; } -/* line 606, ../scss/theme/_widgets.scss */ +/* line 613, ../scss/theme/_widgets.scss */ .announcements .announcement-box.announcement-critical { border: 1px solid #ca0000; background-color: #ffebea; } -/* line 609, ../scss/theme/_widgets.scss */ +/* line 616, ../scss/theme/_widgets.scss */ .announcements .announcement-box.announcement-critical .announcement-icon { color: #ca0000; animation: blinker 0.7s step-start 4; } -/* line 615, ../scss/theme/_widgets.scss */ +/* line 622, ../scss/theme/_widgets.scss */ .announcements .announcement-box .announcement-header .announcement-icon { display: inline-block; font-size: 1.3rem; padding: 0 0.7rem 0 0; } -/* line 620, ../scss/theme/_widgets.scss */ +/* line 627, ../scss/theme/_widgets.scss */ .announcements .announcement-box .announcement-header h4 { display: inline-block; } -/* line 624, ../scss/theme/_widgets.scss */ +/* line 631, ../scss/theme/_widgets.scss */ .announcements .announcement-box .announcement-text { font-size: 0.9rem; line-height: 1.3; } -/* line 627, ../scss/theme/_widgets.scss */ +/* line 634, ../scss/theme/_widgets.scss */ .announcements .announcement-box .announcement-text h4 { font-size: 1.2rem; } -/* line 630, ../scss/theme/_widgets.scss */ +/* line 637, ../scss/theme/_widgets.scss */ .announcements .announcement-box .announcement-text ul { list-style: disc outside none; } -/* line 633, ../scss/theme/_widgets.scss */ +/* line 640, ../scss/theme/_widgets.scss */ .announcements .announcement-box .announcement-text ol { list-style: decimal outside none; } -/* line 636, ../scss/theme/_widgets.scss */ +/* line 643, ../scss/theme/_widgets.scss */ .announcements .announcement-box .announcement-text li { padding: 0; } -/* line 639, ../scss/theme/_widgets.scss */ +/* line 646, ../scss/theme/_widgets.scss */ .announcements .announcement-box .announcement-text div { white-space: pre-line; } -/* line 642, ../scss/theme/_widgets.scss */ +/* line 649, ../scss/theme/_widgets.scss */ .announcements .announcement-box .announcement-text .announcement-header { margin: 0 0 0.8rem 0; } -/* line 645, ../scss/theme/_widgets.scss */ +/* line 652, ../scss/theme/_widgets.scss */ .announcements .announcement-box .announcement-text announcement-body { white-space: pre-line; } -/* line 647, ../scss/theme/_widgets.scss */ +/* line 654, ../scss/theme/_widgets.scss */ .announcements .announcement-box .announcement-text announcement-body p { margin-left: 1rem; } -/* line 652, ../scss/theme/_widgets.scss */ +/* line 659, ../scss/theme/_widgets.scss */ .announcements .announcement-box .announcement-date { font-size: 0.7rem; margin-top: 0.4rem; } -/* line 662, ../scss/theme/_widgets.scss */ +/* line 669, ../scss/theme/_widgets.scss */ body.rtl .ui-multiselect-header .ui-multiselect-filter { direction: rtl; float: right; margin-right: 3px; margin-left: 10px; } -/* line 668, ../scss/theme/_widgets.scss */ +/* line 675, ../scss/theme/_widgets.scss */ body.rtl .ui-multiselect-header ul li { float: right; padding: 0 3px 0 10px; } -/* line 672, ../scss/theme/_widgets.scss */ +/* line 679, ../scss/theme/_widgets.scss */ body.rtl .ui-multiselect-header li.ui-multiselect-close { float: left; text-align: left; padding-left: 0; } -/* line 679, ../scss/theme/_widgets.scss */ +/* line 686, ../scss/theme/_widgets.scss */ body.rtl .range-filter-widget input.date-filter-input { margin-right: 0; margin-left: 2px; } -/* line 684, ../scss/theme/_widgets.scss */ +/* line 691, ../scss/theme/_widgets.scss */ body.rtl #list-btn-add { float: left; margin-right: 0; margin-left: 0.35rem; } -/* line 690, ../scss/theme/_widgets.scss */ +/* line 697, ../scss/theme/_widgets.scss */ body.rtl .rm-form .rm-module-rules .rm-module-header div { margin-right: 0; margin-left: 0.3rem; } -/* line 696, ../scss/theme/_widgets.scss */ +/* line 703, ../scss/theme/_widgets.scss */ body.rtl .rm-form .rm-assign .rm-duplicate td:first-child { border-left: 0; border-right: 1px solid red; } -/* line 700, ../scss/theme/_widgets.scss */ +/* line 707, ../scss/theme/_widgets.scss */ body.rtl .rm-form .rm-assign .rm-duplicate td:last-child { border-right: 0; border-left: 1px solid red; diff --git a/tests/travis/install_web2py.sh b/tests/travis/install_web2py.sh index 9ca536b2c3..2302ce7b5a 100644 --- a/tests/travis/install_web2py.sh +++ b/tests/travis/install_web2py.sh @@ -14,8 +14,8 @@ echo "=================================" # Handle for the checkout-directory (different paths for different repos) BRANCH_HOME=`pwd` -# Use web2py-2.21.1-stable -WEB2PY_COMMIT=6da8479 +# Use web2py-2.21.2-stable +WEB2PY_COMMIT=3190585 # Clone web2py under build home (usually /home/travis/build) cd ../.. diff --git a/views/layout_popup.html b/views/layout_popup.html index 76c8b67efc..9d06896405 100644 --- a/views/layout_popup.html +++ b/views/layout_popup.html @@ -1,3 +1,3 @@ {{# Form has been submitted successfully: Don't load unnecessary JS, Refresh the Main form & Close the pop up}} -{{s3.scripts=[]}}{{s3.js_global=[]}}{{s3.jquery_ready=['''s3_popup_refresh_caller(%s)''' % (s3.popup_data if isinstance(s3.get("popup_data"), basestring) else "")]}} +{{s3.scripts=[]}}{{s3.js_global=[]}}{{s3.jquery_ready=['''s3_popup_refresh_caller(%s)''' % (s3.popup_data if isinstance(s3.get("popup_data"), str) else "")]}}