From 1885b9e18e550ff2a8aeaaf4370eef8e285d2ef5 Mon Sep 17 00:00:00 2001 From: Disassembler Date: Tue, 14 Apr 2020 14:17:21 +0200 Subject: [PATCH] Add SaFiRe + ShaRe, closes #410 --- lxc-apps/safire/app | 30 + lxc-apps/safire/install.sh | 52 + lxc-apps/safire/install/createdb.sql | 8 + .../safire/install/postgres_data/pg_hba.conf | 3 + .../install/postgres_data/postgresql.conf | 750 +++++ .../safire/install/sahana_conf/000_config.py | 263 ++ .../safire/install/sahana_conf/00_settings.py | 318 ++ .../install/sahana_data/SAFIRE/config.py | 1213 ++++++++ .../install/sahana_data/masterUsers.csv | 2 + lxc-apps/safire/install/update-conf.sh | 14 + lxc-apps/safire/uninstall.sh | 8 + lxc-apps/share/app | 30 + lxc-apps/share/install.sh | 52 + lxc-apps/share/install/createdb.sql | 8 + .../share/install/postgres_data/pg_hba.conf | 3 + .../install/postgres_data/postgresql.conf | 750 +++++ .../share/install/sahana_conf/000_config.py | 263 ++ .../share/install/sahana_conf/00_settings.py | 318 ++ .../share/install/sahana_data/SHARE/config.py | 2551 +++++++++++++++++ .../share/install/sahana_data/masterUsers.csv | 2 + lxc-apps/share/install/update-conf.sh | 14 + lxc-apps/share/uninstall.sh | 8 + 22 files changed, 6660 insertions(+) create mode 100644 lxc-apps/safire/app create mode 100755 lxc-apps/safire/install.sh create mode 100644 lxc-apps/safire/install/createdb.sql create mode 100644 lxc-apps/safire/install/postgres_data/pg_hba.conf create mode 100644 lxc-apps/safire/install/postgres_data/postgresql.conf create mode 100644 lxc-apps/safire/install/sahana_conf/000_config.py create mode 100644 lxc-apps/safire/install/sahana_conf/00_settings.py create mode 100644 lxc-apps/safire/install/sahana_data/SAFIRE/config.py create mode 100644 lxc-apps/safire/install/sahana_data/masterUsers.csv create mode 100755 lxc-apps/safire/install/update-conf.sh create mode 100755 lxc-apps/safire/uninstall.sh create mode 100644 lxc-apps/share/app create mode 100755 lxc-apps/share/install.sh create mode 100644 lxc-apps/share/install/createdb.sql create mode 100644 lxc-apps/share/install/postgres_data/pg_hba.conf create mode 100644 lxc-apps/share/install/postgres_data/postgresql.conf create mode 100644 lxc-apps/share/install/sahana_conf/000_config.py create mode 100644 lxc-apps/share/install/sahana_conf/00_settings.py create mode 100644 lxc-apps/share/install/sahana_data/SHARE/config.py create mode 100644 lxc-apps/share/install/sahana_data/masterUsers.csv create mode 100755 lxc-apps/share/install/update-conf.sh create mode 100755 lxc-apps/share/uninstall.sh diff --git a/lxc-apps/safire/app b/lxc-apps/safire/app new file mode 100644 index 0000000..3b720f3 --- /dev/null +++ b/lxc-apps/safire/app @@ -0,0 +1,30 @@ +{ + "version": "0.0.1-200403", + "meta": { + "title": "Sahana Eden - SAFIRE", + "desc-cs": "Řízení humanítární činnosti - Řešení nouzových událostí", + "desc-en": "Management of humanitarian activities - First response", + "license": "GPL" + }, + "containers": { + "safire": { + "image": "sahana_0.0.1-200403", + "depends": [ + "safire-postgres" + ], + "mounts": { + "safire/sahana_conf": "srv/web2py/applications/eden/models", + "safire/sahana_data/SAFIRE": "srv/web2py/applications/eden/modules/templates/SAFIRE", + "safire/sahana_data/databases": "srv/web2py/applications/eden/databases", + "safire/sahana_data/uploads": "srv/web2py/applications/eden/uploads", + "safire/sahana_data/masterUsers.csv": "srv/web2py/applications/eden/modules/templates/default/users/masterUsers.csv:file" + } + }, + "safire-postgres": { + "image": "postgis_3.0.0-200403", + "mounts": { + "safire/postgres_data": "var/lib/postgresql" + } + } + } +} diff --git a/lxc-apps/safire/install.sh b/lxc-apps/safire/install.sh new file mode 100755 index 0000000..41bbb49 --- /dev/null +++ b/lxc-apps/safire/install.sh @@ -0,0 +1,52 @@ +#!/bin/sh +set -ev + +# Volumes +POSTGRES_DATA="${VOLUMES_DIR}/safire/postgres_data" +SAHANA_DATA="${VOLUMES_DIR}/safire/sahana_data" +SAHANA_CONF="${VOLUMES_DIR}/safire/sahana_conf" +SAHANA_LAYER="${LAYERS_DIR}/sahana_0.0.1-200403" + +# Create Postgres instance +install -o 105432 -g 105432 -m 700 -d ${POSTGRES_DATA} +spoc-container exec safire-postgres -- initdb -D /var/lib/postgresql + +# Configure Postgres +install -o 105432 -g 105432 -m 600 postgres_data/postgresql.conf ${POSTGRES_DATA}/postgresql.conf +install -o 105432 -g 105432 -m 600 postgres_data/pg_hba.conf ${POSTGRES_DATA}/pg_hba.conf + +# Create PostgreSQL user and database +export SAFIRE_PWD=$(head -c 18 /dev/urandom | base64 | tr -d '+/=') +spoc-container start safire-postgres +envsubst 0 logs only + # statements running at least this number + # of milliseconds + +#log_transaction_sample_rate = 0.0 # Fraction of transactions whose statements + # are logged regardless of their duration. 1.0 logs all + # statements from all transactions, 0.0 never logs. + +# - What to Log - + +#debug_print_parse = off +#debug_print_rewritten = off +#debug_print_plan = off +#debug_pretty_print = on +#log_checkpoints = off +#log_connections = off +#log_disconnections = off +#log_duration = off +#log_error_verbosity = default # terse, default, or verbose messages +#log_hostname = off +log_line_prefix = '%m [%p] %q%u@%d ' # special values: + # %a = application name + # %u = user name + # %d = database name + # %r = remote host and port + # %h = remote host + # %p = process ID + # %t = timestamp without milliseconds + # %m = timestamp with milliseconds + # %n = timestamp with milliseconds (as a Unix epoch) + # %i = command tag + # %e = SQL state + # %c = session ID + # %l = session line number + # %s = session start timestamp + # %v = virtual transaction ID + # %x = transaction ID (0 if none) + # %q = stop here in non-session + # processes + # %% = '%' + # e.g. '<%u%%%d> ' +#log_lock_waits = off # log lock waits >= deadlock_timeout +#log_statement = 'none' # none, ddl, mod, all +#log_replication_commands = off +#log_temp_files = -1 # log temporary files equal or larger + # than the specified size in kilobytes; + # -1 disables, 0 logs all temp files +log_timezone = 'Europe/Prague' + +#------------------------------------------------------------------------------ +# PROCESS TITLE +#------------------------------------------------------------------------------ + +#cluster_name = '' # added to process titles if nonempty + # (change requires restart) +#update_process_title = on + + +#------------------------------------------------------------------------------ +# STATISTICS +#------------------------------------------------------------------------------ + +# - Query and Index Statistics Collector - + +#track_activities = on +#track_counts = on +#track_io_timing = off +#track_functions = none # none, pl, all +#track_activity_query_size = 1024 # (change requires restart) +#stats_temp_directory = 'pg_stat_tmp' + + +# - Monitoring - + +#log_parser_stats = off +#log_planner_stats = off +#log_executor_stats = off +#log_statement_stats = off + + +#------------------------------------------------------------------------------ +# AUTOVACUUM +#------------------------------------------------------------------------------ + +#autovacuum = on # Enable autovacuum subprocess? 'on' + # requires track_counts to also be on. +#log_autovacuum_min_duration = -1 # -1 disables, 0 logs all actions and + # their durations, > 0 logs only + # actions running at least this number + # of milliseconds. +#autovacuum_max_workers = 3 # max number of autovacuum subprocesses + # (change requires restart) +#autovacuum_naptime = 1min # time between autovacuum runs +#autovacuum_vacuum_threshold = 50 # min number of row updates before + # vacuum +#autovacuum_analyze_threshold = 50 # min number of row updates before + # analyze +#autovacuum_vacuum_scale_factor = 0.2 # fraction of table size before vacuum +#autovacuum_analyze_scale_factor = 0.1 # fraction of table size before analyze +#autovacuum_freeze_max_age = 200000000 # maximum XID age before forced vacuum + # (change requires restart) +#autovacuum_multixact_freeze_max_age = 400000000 # maximum multixact age + # before forced vacuum + # (change requires restart) +#autovacuum_vacuum_cost_delay = 2ms # default vacuum cost delay for + # autovacuum, in milliseconds; + # -1 means use vacuum_cost_delay +#autovacuum_vacuum_cost_limit = -1 # default vacuum cost limit for + # autovacuum, -1 means use + # vacuum_cost_limit + + +#------------------------------------------------------------------------------ +# CLIENT CONNECTION DEFAULTS +#------------------------------------------------------------------------------ + +# - Statement Behavior - + +#client_min_messages = notice # values in order of decreasing detail: + # debug5 + # debug4 + # debug3 + # debug2 + # debug1 + # log + # notice + # warning + # error +#search_path = '"$user", public' # schema names +#row_security = on +#default_tablespace = '' # a tablespace name, '' uses the default +#temp_tablespaces = '' # a list of tablespace names, '' uses + # only default tablespace +#default_table_access_method = 'heap' +#check_function_bodies = on +#default_transaction_isolation = 'read committed' +#default_transaction_read_only = off +#default_transaction_deferrable = off +#session_replication_role = 'origin' +#statement_timeout = 0 # in milliseconds, 0 is disabled +#lock_timeout = 0 # in milliseconds, 0 is disabled +#idle_in_transaction_session_timeout = 0 # in milliseconds, 0 is disabled +#vacuum_freeze_min_age = 50000000 +#vacuum_freeze_table_age = 150000000 +#vacuum_multixact_freeze_min_age = 5000000 +#vacuum_multixact_freeze_table_age = 150000000 +#vacuum_cleanup_index_scale_factor = 0.1 # fraction of total number of tuples + # before index cleanup, 0 always performs + # index cleanup +#bytea_output = 'hex' # hex, escape +#xmlbinary = 'base64' +#xmloption = 'content' +#gin_fuzzy_search_limit = 0 +#gin_pending_list_limit = 4MB + +# - Locale and Formatting - + +datestyle = 'iso, mdy' +#intervalstyle = 'postgres' +timezone = 'Europe/Prague' +#timezone_abbreviations = 'Default' # Select the set of available time zone + # abbreviations. Currently, there are + # Default + # Australia (historical usage) + # India + # You can create your own file in + # share/timezonesets/. +#extra_float_digits = 1 # min -15, max 3; any value >0 actually + # selects precise output mode +#client_encoding = sql_ascii # actually, defaults to database + # encoding + +# These settings are initialized by initdb, but they can be changed. +lc_messages = 'C' # locale for system error message + # strings +lc_monetary = 'C' # locale for monetary formatting +lc_numeric = 'C' # locale for number formatting +lc_time = 'C' # locale for time formatting + +# default configuration for text search +default_text_search_config = 'pg_catalog.english' + +# - Shared Library Preloading - + +#shared_preload_libraries = '' # (change requires restart) +#local_preload_libraries = '' +#session_preload_libraries = '' +#jit_provider = 'llvmjit' # JIT library to use + +# - Other Defaults - + +#dynamic_library_path = '$libdir' + + +#------------------------------------------------------------------------------ +# LOCK MANAGEMENT +#------------------------------------------------------------------------------ + +#deadlock_timeout = 1s +#max_locks_per_transaction = 64 # min 10 + # (change requires restart) +#max_pred_locks_per_transaction = 64 # min 10 + # (change requires restart) +#max_pred_locks_per_relation = -2 # negative values mean + # (max_pred_locks_per_transaction + # / -max_pred_locks_per_relation) - 1 +#max_pred_locks_per_page = 2 # min 0 + + +#------------------------------------------------------------------------------ +# VERSION AND PLATFORM COMPATIBILITY +#------------------------------------------------------------------------------ + +# - Previous PostgreSQL Versions - + +#array_nulls = on +#backslash_quote = safe_encoding # on, off, or safe_encoding +#escape_string_warning = on +#lo_compat_privileges = off +#operator_precedence_warning = off +#quote_all_identifiers = off +#standard_conforming_strings = on +#synchronize_seqscans = on + +# - Other Platforms and Clients - + +#transform_null_equals = off + + +#------------------------------------------------------------------------------ +# ERROR HANDLING +#------------------------------------------------------------------------------ + +#exit_on_error = off # terminate session on any error? +#restart_after_crash = on # reinitialize after backend crash? +#data_sync_retry = off # retry or panic on failure to fsync + # data? + # (change requires restart) + + +#------------------------------------------------------------------------------ +# CONFIG FILE INCLUDES +#------------------------------------------------------------------------------ + +# These options allow settings to be loaded from files other than the +# default postgresql.conf. Note that these are directives, not variable +# assignments, so they can usefully be given more than once. + +#include_dir = '...' # include files ending in '.conf' from + # a directory, e.g., 'conf.d' +#include_if_exists = '...' # include file only if it exists +#include = '...' # include file + + +#------------------------------------------------------------------------------ +# CUSTOMIZED OPTIONS +#------------------------------------------------------------------------------ + +# Add settings for extensions here diff --git a/lxc-apps/safire/install/sahana_conf/000_config.py b/lxc-apps/safire/install/sahana_conf/000_config.py new file mode 100644 index 0000000..a200cc8 --- /dev/null +++ b/lxc-apps/safire/install/sahana_conf/000_config.py @@ -0,0 +1,263 @@ +# -*- coding: utf-8 -*- + +""" + Machine-specific settings + All settings which are typically edited for a specific machine should be done here + + Deployers should ideally not need to edit any other files outside of their template folder + + Note for Developers: + /models/000_config.py is NOT in the Git repository, to avoid leaking of + sensitive or irrelevant information into the repository. + For changes to be committed, please also edit: + modules/templates/000_config.py +""" + +# Remove this line when you have edited this file sufficiently to proceed to the web interface +FINISHED_EDITING_CONFIG_FILE = True + +# Select the Template +# - which Modules are enabled +# - PrePopulate data +# - Security Policy +# - Workflows +# - Theme +# - note that you should restart your web2py after changing this setting +settings.base.template = "SAFIRE" + +# Database settings +# Uncomment to use a different database, other than sqlite +settings.database.db_type = "postgres" +#settings.database.db_type = "mysql" +# Uncomment to use a different host +settings.database.host = "safire-postgres" +# Uncomment to use a different port +#settings.database.port = 3306 +#settings.database.port = 5432 +# Uncomment to select a different name for your database +settings.database.database = "safire" +# Uncomment to select a different username for your database +settings.database.username = "safire" +# Uncomment to set the password +# NB Web2Py doesn't like passwords with an @ in them +settings.database.password = "${SAFIRE_PWD}" +# Uncomment to use a different pool size +#settings.database.pool_size = 30 +# Do we have a spatial DB available? (currently supports PostGIS. Spatialite to come.) +settings.gis.spatialdb = True + +# Base settings +settings.base.system_name = T("Sahana First Response") +settings.base.system_name_short = T("SAFIRE") +# Set this to the Public URL of the instance +settings.base.public_url = "https://safire.spotter.vm" + +# Switch to "False" in Production for a Performance gain +# (need to set to "True" again when Table definitions are changed) +settings.base.migrate = True +# To just create the .table files (also requires migrate=True): +#settings.base.fake_migrate = True + +# Set this to True to switch to Debug mode +# Debug mode means that uncompressed CSS/JS files are loaded +# JS Debug messages are also available in the Console +# can also load an individual page in debug mode by appending URL with +# ?debug=1 +settings.base.debug = True + +# Uncomment this to prevent automated test runs from remote +# settings.base.allow_testing = False + +# Configure the log level ("DEBUG", "INFO", "WARNING", "ERROR" or "CRITICAL"), None = turn off logging (default) +#settings.log.level = "ERROR" # DEBUG set automatically when base.debug is True +# Uncomment to prevent writing log messages to the console (sys.stderr) +#settings.log.console = False +# Configure a log file (file name) +#settings.log.logfile = None +# Uncomment to get detailed caller information +#settings.log.caller_info = True + +# Uncomment to use Content Delivery Networks to speed up Internet-facing sites +#settings.base.cdn = True + +# Allow language files to be updated automatically +#settings.L10n.languages_readonly = False + +# This setting should be changed _before_ registering the 1st user +# - should happen automatically if installing using supported scripts +settings.auth.hmac_key = "${SAFIRE_HMAC}" + +# If using Masterkey Authentication, then set this to a deployment-specific 32 char string: +#settings.auth.masterkey_app_key = "randomstringrandomstringrandomstring" + +# Minimum Password Length +#settings.auth.password_min_length = 8 + +# Email settings +# Outbound server +settings.mail.server = "host:25" +#settings.mail.tls = True +# Useful for Windows Laptops: +# https://www.google.com/settings/security/lesssecureapps +#settings.mail.server = "smtp.gmail.com:587" +#settings.mail.tls = True +#settings.mail.login = "username:password" +# From Address - until this is set, no mails can be sent +settings.mail.sender = "${SAFIRE_ADMIN_USER}" +# Default email address to which requests to approve new user accounts gets sent +# This can be overridden for specific domains/organisations via the auth_domain table +settings.mail.approver = "${SAFIRE_ADMIN_USER}" +# Daily Limit on Sending of emails +#settings.mail.limit = 1000 + +# Frontpage settings +# RSS feeds +settings.frontpage.rss = [ + {"title": "Eden", + # Trac timeline + "url": "http://eden.sahanafoundation.org/timeline?ticket=on&changeset=on&milestone=on&wiki=on&max=50&daysback=90&format=rss" + }, + {"title": "Twitter", + # @SahanaFOSS + #"url": "https://search.twitter.com/search.rss?q=from%3ASahanaFOSS" # API v1 deprecated, so doesn't work, need to use 3rd-party service, like: + "url": "http://www.rssitfor.me/getrss?name=@SahanaFOSS" + # Hashtag + #url: "http://search.twitter.com/search.atom?q=%23eqnz" # API v1 deprecated, so doesn't work, need to use 3rd-party service, like: + #url: "http://api2.socialmention.com/search?q=%23eqnz&t=all&f=rss" + } +] + +# Uncomment to restrict to specific country/countries +#settings.gis.countries= ("LK",) + +# Uncomment to enable a guided tour +#settings.base.guided_tour = True + +# Bing API Key (for Map layers) +# http://www.microsoft.com/maps/create-a-bing-maps-key.aspx +#settings.gis.api_bing = "" +# Google API Key (for Google Maps Layers) +settings.gis.api_google = "" +# Yahoo API Key (for Geocoder) +#settings.gis.api_yahoo = "" + +# GeoNames username +#settings.gis.geonames_username = "" + +# Fill this in to get Google Analytics for your site +#settings.base.google_analytics_tracking_id = "" + +# Chat server, see: http://eden.sahanafoundation.org/wiki/InstallationGuidelines/Chat +#settings.base.chat_server = { +# "ip": "127.0.0.1", +# "port": 7070, +# "name": "servername", +# # Default group everyone is added to +# "groupname" : "everyone", +# "server_db" : "openfire", +# # These settings fallback to main DB settings if not specified +# # Only mysql/postgres supported +# #"server_db_type" : "mysql", +# #"server_db_username" : "", +# #"server_db_password": "", +# #"server_db_port" : 3306, +# #"server_db_ip" : "127.0.0.1", +# } + +# GeoServer (Currently used by GeoExplorer. Will allow REST control of GeoServer.) +# NB Needs to be publically-accessible URL for querying via client JS +#settings.gis.geoserver_url = "http://localhost/geoserver" +#settings.gis.geoserver_username = "admin" +#settings.gis.geoserver_password = "" +# Print Service URL: http://eden.sahanafoundation.org/wiki/BluePrintGISPrinting +#settings.gis.print_service = "/geoserver/pdf/" + +# Google OAuth (to allow users to login using Google) +# https://code.google.com/apis/console/ +#settings.auth.google_id = "" +#settings.auth.google_secret = "" + +# Pootle server +# settings.L10n.pootle_url = "http://pootle.sahanafoundation.org/" +# settings.L10n.pootle_username = "username" +# settings.L10n.pootle_password = "*****" + +# SOLR server for Full-Text Search +#settings.base.solr_url = "http://127.0.0.1:8983/solr/" + +# Memcache server to allow sharing of sessions across instances +#settings.base.session_memcache = '127.0.0.1:11211' + +settings.base.session_db = True + +# UI options +# Should user be prompted to save before navigating away? +#settings.ui.navigate_away_confirm = False +# Should user be prompted to confirm actions? +#settings.ui.confirm = False +# Should potentially large dropdowns be turned into autocompletes? +# (unused currently) +#settings.ui.autocomplete = True +#settings.ui.read_label = "Details" +#settings.ui.update_label = "Edit" + +# Audit settings +# - can be a callable for custom hooks (return True to also perform normal logging, or False otherwise) +# NB Auditing (especially Reads) slows system down & consumes diskspace +#settings.security.audit_write = False +#settings.security.audit_read = False + +# Performance Options +# Maximum number of search results for an Autocomplete Widget +#settings.search.max_results = 200 +# Maximum number of features for a Map Layer +#settings.gis.max_features = 1000 + +# CAP Settings +# Change for different authority and organisations +# See http://alerting.worldweather.org/ for oid +# Country root oid. The oid for the organisation includes this base +#settings.cap.identifier_oid = "2.49.0.0.608.0" +# Set the period (in days) after which alert info segments expire (default=2) +#settings.cap.info_effective_period = 2 + +# ============================================================================= +# Import the settings from the Template +# - note: invalid settings are ignored +# +settings.import_template() + +# ============================================================================= +# Over-rides to the Template may be done here +# + +# e.g. +#settings.base.system_name = T("Sahana TEST") +#settings.base.prepopulate = ("MY_TEMPLATE_ONLY") +#settings.base.prepopulate += ("default", "default/users") +#settings.base.theme = "default" +settings.L10n.default_language = "cs" +#settings.security.policy = 7 # Organisation-ACLs +# Enable Additional Module(s) +#from gluon.storage import Storage +#settings.modules["delphi"] = Storage( +# name_nice = T("Delphi Decision Maker"), +# restricted = False, +# module_type = 10, +# ) +# Disable a module which is normally used by the template +# - NB Only templates with adaptive menus will work nicely with this! +#del settings.modules["irs"] + +# Production instances should set this before prepopulate is run +#settings.base.prepopulate_demo = 0 + +# After 1st_run, set this for Production to save 1x DAL hit/request +#settings.base.prepopulate = 0 + +# ============================================================================= +# A version number to tell update_check if there is a need to refresh the +# running copy of this file +VERSION = 1 + +# END ========================================================================= diff --git a/lxc-apps/safire/install/sahana_conf/00_settings.py b/lxc-apps/safire/install/sahana_conf/00_settings.py new file mode 100644 index 0000000..5610291 --- /dev/null +++ b/lxc-apps/safire/install/sahana_conf/00_settings.py @@ -0,0 +1,318 @@ +# -*- coding: utf-8 -*- + +""" + Global settings: + + Those which are typically edited during a deployment are in + 000_config.py & their results parsed into here. Deployers + shouldn't typically need to edit any settings here. +""" + +# Keep all our configuration options off the main global variables + +# Use response.s3 for one-off variables which are visible in views without explicit passing +s3.formats = Storage() + +# Workaround for this Bug in Selenium with FF4: +# http://code.google.com/p/selenium/issues/detail?id=1604 +s3.interactive = settings.get_ui_confirm() + +s3.base_url = "%s/%s" % (settings.get_base_public_url(), + appname) +s3.download_url = "%s/default/download" % s3.base_url + +# ----------------------------------------------------------------------------- +# Client tests + +# Check whether browser is Mobile & store result in session +# - commented-out until we make use of it +#if session.s3.mobile is None: +# session.s3.mobile = s3base.s3_is_mobile_client(request) +#if session.s3.browser is None: +# session.s3.browser = s3base.s3_populate_browser_compatibility(request) + +# ----------------------------------------------------------------------------- +# Global variables + +# Strings to i18n +# Common Labels +#messages["BREADCRUMB"] = ">> " +messages["UNKNOWN_OPT"] = "" +messages["NONE"] = "" +messages["OBSOLETE"] = "Obsolete" +messages["READ"] = settings.get_ui_label_read() +messages["UPDATE"] = settings.get_ui_label_update() +messages["DELETE"] = "Delete" +messages["COPY"] = "Copy" +messages["NOT_APPLICABLE"] = "N/A" +messages["ADD_PERSON"] = "Create a Person" +messages["ADD_LOCATION"] = "Create Location" +messages["SELECT_LOCATION"] = "Select a location" +messages["COUNTRY"] = "Country" +messages["ORGANISATION"] = "Organization" +messages["AUTOCOMPLETE_HELP"] = "Enter some characters to bring up a list of possible matches" + +for u in messages: + if isinstance(messages[u], str): + globals()[u] = T(messages[u]) + +# CRUD Labels +s3.crud_labels = Storage(READ=READ, + UPDATE=UPDATE, + DELETE=DELETE, + COPY=COPY, + NONE=NONE, + ) + +# Error Messages +ERROR["BAD_RECORD"] = "Record not found!" +ERROR["BAD_METHOD"] = "Unsupported method!" +ERROR["BAD_FORMAT"] = "Unsupported data format!" +ERROR["BAD_REQUEST"] = "Invalid request" +ERROR["BAD_SOURCE"] = "Invalid source" +ERROR["BAD_TEMPLATE"] = "XSLT stylesheet not found" +ERROR["BAD_RESOURCE"] = "Nonexistent or invalid resource" +ERROR["DATA_IMPORT_ERROR"] = "Data import error" +ERROR["INTEGRITY_ERROR"] = "Integrity error: record can not be deleted while it is referenced by other records" +ERROR["METHOD_DISABLED"] = "Method disabled" +ERROR["NO_MATCH"] = "No matching element found in the data source" +ERROR["NOT_IMPLEMENTED"] = "Not implemented" +ERROR["NOT_PERMITTED"] = "Operation not permitted" +ERROR["PARSE_ERROR"] = "XML parse error" +ERROR["TRANSFORMATION_ERROR"] = "XSLT transformation error" +ERROR["UNAUTHORISED"] = "Not Authorized" +ERROR["VALIDATION_ERROR"] = "Validation error" + +# To get included in +s3.stylesheets = [] +s3.external_stylesheets = [] +# To get included at the end of +s3.scripts = [] +s3.scripts_modules = [] +s3.js_global = [] +s3.jquery_ready = [] + +# ----------------------------------------------------------------------------- +# Languages + +s3.l10n_languages = settings.get_L10n_languages() + +# Default strings are in US English +T.current_languages = ("en", "en-us") +# Check if user has selected a specific language +if get_vars._language: + language = get_vars._language + session.s3.language = language +elif session.s3.language: + # Use the last-selected language + language = session.s3.language +elif auth.is_logged_in(): + # Use user preference + language = auth.user.language +else: + # Use system default + language = settings.get_L10n_default_language() +#else: +# # Use what browser requests (default web2py behaviour) +# T.force(T.http_accept_language) + +# IE doesn't set request.env.http_accept_language +#if language != "en": +T.force(language) + +# Store for views (e.g. Ext) +if language.find("-") == -1: + # Ext peculiarities + if language == "vi": + s3.language = "vn" + elif language == "el": + s3.language = "el_GR" + else: + s3.language = language +else: + lang_parts = language.split("-") + s3.language = "%s_%s" % (lang_parts[0], lang_parts[1].upper()) + +# List of Languages which use a Right-to-Left script (Arabic, Hebrew, Farsi, Urdu) +if language in ("ar", "prs", "ps", "ur"): + s3.rtl = True +else: + s3.rtl = False + +# ----------------------------------------------------------------------------- +# Auth + +_settings = auth.settings +_settings.lock_keys = False + +_settings.expiration = 28800 # seconds + +if settings.get_auth_openid(): + # Requires http://pypi.python.org/pypi/python-openid/ + try: + from gluon.contrib.login_methods.openid_auth import OpenIDAuth + openid_login_form = OpenIDAuth(auth) + from gluon.contrib.login_methods.extended_login_form import ExtendedLoginForm + _settings.login_form = ExtendedLoginForm(auth, openid_login_form, + signals=["oid", "janrain_nonce"]) + except ImportError: + session.warning = "Library support not available for OpenID" + +# Allow use of LDAP accounts for login +# NB Currently this means that change password should be disabled: +#_settings.actions_disabled.append("change_password") +# (NB These are not automatically added to PR or to Authenticated role since they enter via the login() method not register()) +#from gluon.contrib.login_methods.ldap_auth import ldap_auth +# Require even alternate login methods to register users 1st +#_settings.alternate_requires_registration = True +# Active Directory +#_settings.login_methods.append(ldap_auth(mode="ad", server="dc.domain.org", base_dn="ou=Users,dc=domain,dc=org")) +# or if not wanting local users at all (no passwords saved within DB): +#_settings.login_methods = [ldap_auth(mode="ad", server="dc.domain.org", base_dn="ou=Users,dc=domain,dc=org")] +# Domino +#_settings.login_methods.append(ldap_auth(mode="domino", server="domino.domain.org")) +# OpenLDAP +#_settings.login_methods.append(ldap_auth(server="directory.sahanafoundation.org", base_dn="ou=users,dc=sahanafoundation,dc=org")) +# Allow use of Email accounts for login +#_settings.login_methods.append(email_auth("smtp.gmail.com:587", "@gmail.com")) + +# Require captcha verification for registration +#auth.settings.captcha = RECAPTCHA(request, public_key="PUBLIC_KEY", private_key="PRIVATE_KEY") +# Require Email Verification +_settings.registration_requires_verification = settings.get_auth_registration_requires_verification() +_settings.on_failed_authorization = URL(c="default", f="user", + args="not_authorized") +_settings.reset_password_requires_verification = True +_settings.verify_email_next = URL(c="default", f="index") + +# Require Admin approval for self-registered users +_settings.registration_requires_approval = settings.get_auth_registration_requires_approval() + +# We don't wish to clutter the groups list with 1 per user. +_settings.create_user_groups = False +# We need to allow basic logins for Webservices +_settings.allow_basic_login = True + +_settings.logout_onlogout = s3_auth_on_logout +_settings.login_onaccept = s3_auth_on_login +# Now read in auth.login() to avoid setting unneccesarily in every request +#_settings.login_next = settings.get_auth_login_next() +if settings.has_module("vol") and \ + settings.get_auth_registration_volunteer(): + _settings.register_next = URL(c="vol", f="person") + +# Languages available in User Profiles +#if len(s3.l10n_languages) > 1: +# _settings.table_user.language.requires = s3base.IS_ISO639_2_LANGUAGE_CODE(sort = True, +# translate = True, +# zero = None, +# ) +#else: +# field = _settings.table_user.language +# field.default = s3.l10n_languages.keys()[0] +# field.readable = False +# field.writable = False + +_settings.lock_keys = True + +# ----------------------------------------------------------------------------- +# Mail + +# These settings could be made configurable as part of the Messaging Module +# - however also need to be used by Auth (order issues) +sender = settings.get_mail_sender() +if sender: + mail.settings.sender = sender + mail.settings.server = settings.get_mail_server() + mail.settings.tls = settings.get_mail_server_tls() + mail_server_login = settings.get_mail_server_login() + if mail_server_login: + mail.settings.login = mail_server_login + # Email settings for registration verification and approval + _settings.mailer = mail + +# ----------------------------------------------------------------------------- +# Session + +# Custom Notifications +response.error = session.error +response.confirmation = session.confirmation +response.information = session.information +response.warning = session.warning +session.error = [] +session.confirmation = [] +session.information = [] +session.warning = [] + +# Shortcuts for system role IDs, see modules/s3aaa.py/AuthS3 +#system_roles = auth.get_system_roles() +#ADMIN = system_roles.ADMIN +#AUTHENTICATED = system_roles.AUTHENTICATED +#ANONYMOUS = system_roles.ANONYMOUS +#EDITOR = system_roles.EDITOR +#MAP_ADMIN = system_roles.MAP_ADMIN +#ORG_ADMIN = system_roles.ORG_ADMIN +#ORG_GROUP_ADMIN = system_roles.ORG_GROUP_ADMIN + +if s3.debug: + # Add the developer toolbar from modules/s3/s3utils.py + s3.toolbar = s3base.s3_dev_toolbar + +# ----------------------------------------------------------------------------- +# CRUD + +s3_formstyle = settings.get_ui_formstyle() +s3_formstyle_read = settings.get_ui_formstyle_read() +s3_formstyle_mobile = s3_formstyle +submit_button = T("Save") +s3_crud = s3.crud +s3_crud.formstyle = s3_formstyle +s3_crud.formstyle_read = s3_formstyle_read +s3_crud.submit_button = submit_button +# Optional class for Submit buttons +#s3_crud.submit_style = "submit-button" +s3_crud.confirm_delete = T("Do you really want to delete these records?") +s3_crud.archive_not_delete = settings.get_security_archive_not_delete() +s3_crud.navigate_away_confirm = settings.get_ui_navigate_away_confirm() + +# Content Type Headers, default is application/xml for XML formats +# and text/x-json for JSON formats, other content types must be +# specified here: +s3.content_type = Storage( + tc = "application/atom+xml", # TableCast feeds + rss = "application/rss+xml", # RSS + georss = "application/rss+xml", # GeoRSS + kml = "application/vnd.google-earth.kml+xml", # KML +) + +# JSON Formats +s3.json_formats = ["geojson", "s3json"] + +# CSV Formats +s3.csv_formats = ["hrf", "s3csv"] + +# Datatables default number of rows per page +s3.ROWSPERPAGE = 20 + +# Valid Extensions for Image Upload fields +s3.IMAGE_EXTENSIONS = ["png", "PNG", "jpg", "JPG", "jpeg", "JPEG"] + +# Default CRUD strings +s3.crud_strings = Storage( + label_create = T("Add Record"), + title_display = T("Record Details"), + title_list = T("Records"), + title_update = T("Edit Record"), + title_map = T("Map"), + title_report = T("Report"), + label_list_button = T("List Records"), + label_delete_button = T("Delete Record"), + msg_record_created = T("Record added"), + msg_record_modified = T("Record updated"), + msg_record_deleted = T("Record deleted"), + msg_list_empty = T("No Records currently available"), + msg_match = T("Matching Records"), + msg_no_match = T("No Matching Records"), + ) + +# END ========================================================================= diff --git a/lxc-apps/safire/install/sahana_data/SAFIRE/config.py b/lxc-apps/safire/install/sahana_data/SAFIRE/config.py new file mode 100644 index 0000000..89200ae --- /dev/null +++ b/lxc-apps/safire/install/sahana_data/SAFIRE/config.py @@ -0,0 +1,1213 @@ +# -*- coding: utf-8 -*- + +from collections import OrderedDict + +from gluon import current, URL +from gluon.storage import Storage + +def config(settings): + """ + Template settings for SaFiRe: Sahana First Response + + http://eden.sahanafoundation.org/wiki/BluePrint/SAFIRE + """ + + T = current.T + + settings.base.system_name = T("Sahana First Response") + settings.base.system_name_short = T("SAFIRE") + + # PrePopulate data + settings.base.prepopulate.append("SAFIRE") + settings.base.prepopulate_demo.append("SAFIRE/Demo") + + # Theme (folder to use for views/layout.html) + #settings.base.theme = "SAFIRE" + + # Authentication settings + # Should users be allowed to register themselves? + #settings.security.self_registration = False + # Do new users need to verify their email address? + #settings.auth.registration_requires_verification = True + # Do new users need to be approved by an administrator prior to being able to login? + #settings.auth.registration_requires_approval = True + settings.auth.registration_requests_organisation = True + + # Approval emails get sent to all admins + settings.mail.approver = "ADMIN" + + settings.auth.registration_link_user_to = {"staff": T("Staff"), + } + + settings.auth.registration_link_user_to_default = ["staff"] + + # Uncomment to display the Map Legend as a floating DIV + settings.gis.legend = "float" + # Uncomment to Disable the Postcode selector in the LocationSelector + #settings.gis.postcode_selector = False # @ToDo: Vary by country (include in the gis_config!) + # Uncomment to show the Print control: + # http://eden.sahanafoundation.org/wiki/UserGuidelines/Admin/MapPrinting + #settings.gis.print_button = True + + # GeoNames username + settings.gis.geonames_username = "trendspotter" + settings.gis.simplify_tolerance = 0 + + # L10n settings + # Number formats (defaults to ISO 31-0) + # Decimal separator for numbers (defaults to ,) + settings.L10n.decimal_separator = "." + # Thousands separator for numbers (defaults to space) + settings.L10n.thousands_separator = "," + + # Security Policy + # http://eden.sahanafoundation.org/wiki/S3AAA#System-widePolicy + # 1: Simple (default): Global as Reader, Authenticated as Editor + # 2: Editor role required for Update/Delete, unless record owned by session + # 3: Apply Controller ACLs + # 4: Apply both Controller & Function ACLs + # 5: Apply Controller, Function & Table ACLs + # 6: Apply Controller, Function, Table ACLs and Entity Realm + # 7: Apply Controller, Function, Table ACLs and Entity Realm + Hierarchy + # 8: Apply Controller, Function, Table ACLs, Entity Realm + Hierarchy and Delegations + + settings.security.policy = 5 # Controller, Function & Table ACLs + + # ------------------------------------------------------------------------- + # Comment/uncomment modules here to disable/enable them + # Modules menu is defined in modules/eden/menu.py + settings.modules = OrderedDict([ + # Core modules which shouldn't be disabled + ("default", Storage( + name_nice = "Home", + restricted = False, # Use ACLs to control access to this module + #access = None, # All Users (inc Anonymous) can see this module in the default menu & access the controller + module_type = None # This item is not shown in the menu + )), + ("admin", Storage( + name_nice = "Administration", + #description = "Site Administration", + access = "|1|", # Only Administrators can see this module in the default menu & access the controller + module_type = None # This item is handled separately for the menu + )), + ("appadmin", Storage( + name_nice = "Administration", + #description = "Site Administration", + module_type = None # No Menu + )), + ("errors", Storage( + name_nice = "Ticket Viewer", + #description = "Needed for Breadcrumbs", + restricted = False, + module_type = None # No Menu + )), + ("sync", Storage( + name_nice = "Synchronization", + #description = "Synchronization", + access = "|1|", # Only Administrators can see this module in the default menu & access the controller + module_type = None # This item is handled separately for the menu + )), + #("tour", Storage( + # name_nice = T("Guided Tour Functionality"), + # module_type = None, + #)), + #("translate", Storage( + # name_nice = T("Translation Functionality"), + # #description = "Selective translation of strings based on module.", + # module_type = None, + #)), + ("gis", Storage( + name_nice = "Map", + #description = "Situation Awareness & Geospatial Analysis", + module_type = 6, # 6th item in the menu + )), + ("pr", Storage( + name_nice = "Person Registry", + #description = "Central point to record details on People", + access = "|1|", # Only Administrators can see this module in the default menu (access to controller is possible to all still) + module_type = 10 + )), + ("org", Storage( + name_nice = "Organizations", + #description = 'Lists "who is doing what & where". Allows relief agencies to coordinate their activities', + module_type = 1 + )), + ("hrm", Storage( + name_nice = "Staff", + #description = "Human Resources Management", + module_type = 2, + )), + ("vol", Storage( + name_nice = T("Volunteers"), + #description = "Human Resources Management", + module_type = 2, + )), + ("cms", Storage( + name_nice = "Content Management", + #description = "Content Management System", + module_type = 10, + )), + ("doc", Storage( + name_nice = "Documents", + #description = "A library of digital resources, such as photos, documents and reports", + module_type = 10, + )), + ("msg", Storage( + name_nice = "Messaging", + #description = "Sends & Receives Alerts via Email & SMS", + # The user-visible functionality of this module isn't normally required. Rather it's main purpose is to be accessed from other modules. + module_type = None, + )), + ("supply", Storage( + name_nice = "Supply Chain Management", + #description = "Used within Inventory Management, Request Management and Asset Management", + module_type = None, # Not displayed + )), + ("inv", Storage( + name_nice = T("Warehouses"), + #description = "Receiving and Sending Items", + module_type = 4 + )), + ("asset", Storage( + name_nice = "Assets", + #description = "Recording and Assigning Assets", + module_type = 5, + )), + # Vehicle depends on Assets + ("vehicle", Storage( + name_nice = "Vehicles", + #description = "Manage Vehicles", + module_type = 10, + )), + #("budget", Storage( + # name_nice = T("Budgets"), + # #description = "Tracks the location, capacity and breakdown of victims in Shelters", + # module_type = 10 + #)), + ("fin", Storage( + name_nice = T("Finance"), + module_type = 10 + )), + ("cr", Storage( + name_nice = T("Shelters"), + #description = "Tracks the location, capacity and breakdown of victims in Shelters", + module_type = 10 + )), + ("project", Storage( + name_nice = "Tasks", + #description = "Tracking of Projects, Activities and Tasks", + module_type = 2 + )), + ("req", Storage( + name_nice = "Requests", + #description = "Manage requests for supplies, assets, staff or other resources. Matches against Inventories where supplies are requested.", + module_type = 10, + )), + ("hms", Storage( + name_nice = T("Hospitals"), + #description = "Helps to monitor status of hospitals", + module_type = 10 + )), + #("dvr", Storage( + # name_nice = T("Disaster Victim Registry"), + # #description = "Allow affected individuals & households to register to receive compensation and distributions", + # module_type = 10, + #)), + ("event", Storage( + name_nice = "Events", + #description = "Activate Events (e.g. from Scenario templates) for allocation of appropriate Resources (Human, Assets & Facilities).", + module_type = 10, + )), + #("transport", Storage( + # name_nice = T("Transport"), + # module_type = 10, + #)), + #("stats", Storage( + # name_nice = T("Statistics"), + # #description = "Manages statistics", + # module_type = None, + #)), + ]) + + # ------------------------------------------------------------------------- + # CMS + # ------------------------------------------------------------------------- + settings.cms.richtext = True + + # ------------------------------------------------------------------------- + # Organisations + # ------------------------------------------------------------------------- + settings.org.documents_tab = True + settings.org.projects_tab = False + + # ------------------------------------------------------------------------- + # Shelters + # ------------------------------------------------------------------------- + settings.cr.people_registration = False + + # ------------------------------------------------------------------------- + def customise_cr_shelter_resource(r, tablename): + + #table = current.s3db.cr_shelter + f = current.s3db.cr_shelter.shelter_service_id + f.readable = f.writable = False + + settings.customise_cr_shelter_resource = customise_cr_shelter_resource + + # ------------------------------------------------------------------------- + # Events + # ------------------------------------------------------------------------- + def event_rheader(r): + rheader = None + + record = r.record + if record and r.representation == "html": + + from gluon import A, DIV, TABLE, TR, TH + from s3 import s3_rheader_tabs + + name = r.name + if name == "incident": + if settings.get_incident_label(): # == "Ticket" + label = T("Ticket Details") + else: + label = T("Incident Details") + tabs = [(label, None), + #(T("Tasks"), "task"), + #(T("Human Resources"), "human_resource"), + #(T("Equipment"), "asset"), + (T("Action Plan"), "plan"), + (T("Incident Reports"), "incident_report"), + (T("Logs"), "log"), + (T("Expenses"), "expense"), + (T("Situation Reports"), "sitrep"), + ] + + rheader_tabs = s3_rheader_tabs(r, tabs) + + record_id = r.id + incident_type_id = record.incident_type_id + + editable = current.auth.s3_has_permission("UPDATE", "event_incident", record_id) + + if editable: + # Dropdown of Scenarios to select + stable = current.s3db.event_scenario + query = (stable.incident_type_id == incident_type_id) & \ + (stable.deleted == False) + scenarios = current.db(query).select(stable.id, + stable.name, + ) + if len(scenarios) and r.method != "event": + from gluon import SELECT, OPTION + dropdown = SELECT(_id="scenarios") + dropdown["_data-incident_id"] = record_id + dappend = dropdown.append + dappend(OPTION(T("Select Scenario"))) + for s in scenarios: + dappend(OPTION(s.name, _value=s.id)) + scenarios = TR(TH("%s: " % T("Scenario")), + dropdown, + ) + s3 = current.response.s3 + script = "/%s/static/themes/SAFIRE/js/incident_profile.js" % r.application + if script not in s3.scripts: + s3.scripts.append(script) + s3.js_global.append('''i18n.scenarioConfirm="%s"''' % T("Populate Incident with Tasks, Organizations, Positions and Equipment from the Scenario?")) + else: + scenarios = "" + else: + scenarios = "" + + if record.exercise: + exercise = TH(T("EXERCISE")) + else: + exercise = TH() + if record.closed: + closed = TH(T("CLOSED")) + else: + closed = TH() + + if record.event_id or r.method == "event" or not editable: + event = "" + else: + if settings.get_event_label(): # == "Disaster" + label = T("Assign to Disaster") + else: + label = T("Assign to Event") + event = A(label, + _href = URL(c = "event", + f = "incident", + args = [record_id, "event"], + ), + _class = "action-btn" + ) + + table = r.table + rheader = DIV(TABLE(TR(exercise), + TR(TH("%s: " % table.name.label), + record.name, + ), + TR(TH("%s: " % table.incident_type_id.label), + table.incident_type_id.represent(incident_type_id), + ), + TR(TH("%s: " % table.location_id.label), + table.location_id.represent(record.location_id), + ), + # @ToDo: Add Zone + TR(TH("%s: " % table.severity.label), + table.severity.represent(record.severity), + ), + TR(TH("%s: " % table.level.label), + table.level.represent(record.level), + ), + TR(TH("%s: " % table.organisation_id.label), + table.organisation_id.represent(record.organisation_id), + ), + TR(TH("%s: " % table.person_id.label), + table.person_id.represent(record.person_id), + ), + scenarios, + TR(TH("%s: " % table.comments.label), + record.comments, + ), + TR(TH("%s: " % table.date.label), + table.date.represent(record.date), + ), + TR(closed), + event, + ), rheader_tabs) + + elif name == "incident_report": + record_id = r.id + ltable = current.s3db.event_incident_report_incident + query = (ltable.incident_report_id == record_id) + link = current.db(query).select(ltable.incident_id, + limitby = (0, 1) + ).first() + if link: + from s3 import S3Represent + represent = S3Represent(lookup="event_incident", show_link=True) + rheader = DIV(TABLE(TR(TH("%s: " % ltable.incident_id.label), + represent(link.incident_id), + ), + )) + else: + if settings.get_incident_label(): # == "Ticket" + label = T("Assign to Ticket") + else: + label = T("Assign to Incident") + rheader = DIV(A(label, + _href = URL(c = "event", + f = "incident_report", + args = [record_id, "assign"], + ), + _class = "action-btn" + )) + + elif name == "event": + if settings.get_event_label(): # == "Disaster" + label = T("Disaster Details") + else: + label = T("Event Details") + if settings.get_incident_label(): # == "Ticket" + INCIDENTS = T("Tickets") + else: + INCIDENTS = T("Incidents") + tabs = [(label, None), + (INCIDENTS, "incident"), + (T("Documents"), "document"), + (T("Photos"), "image"), + ] + + rheader_tabs = s3_rheader_tabs(r, tabs) + + table = r.table + rheader = DIV(TABLE(TR(TH("%s: " % table.event_type_id.label), + table.event_type_id.represent(record.event_type_id), + ), + TR(TH("%s: " % table.name.label), + record.name, + ), + TR(TH("%s: " % table.start_date.label), + table.start_date.represent(record.start_date), + ), + TR(TH("%s: " % table.comments.label), + record.comments, + ), + ), rheader_tabs) + + elif name == "scenario": + tabs = [(T("Scenario Details"), None), + #(T("Tasks"), "task"), + #(T("Human Resources"), "human_resource"), + #(T("Equipment"), "asset"), + (T("Action Plan"), "plan"), + (T("Incident Reports"), "incident_report"), + ] + + rheader_tabs = s3_rheader_tabs(r, tabs) + + table = r.table + rheader = DIV(TABLE(TR(TH("%s: " % table.incident_type_id.label), + table.incident_type_id.represent(record.incident_type_id), + ), + TR(TH("%s: " % table.organisation_id.label), + table.organisation_id.represent(record.organisation_id), + ), + TR(TH("%s: " % table.location_id.label), + table.location_id.represent(record.location_id), + ), + TR(TH("%s: " % table.name.label), + record.name, + ), + TR(TH("%s: " % table.comments.label), + record.comments, + ), + ), rheader_tabs) + + return rheader + + # ------------------------------------------------------------------------- + def customise_event_event_controller(**attr): + + #s3 = current.response.s3 + + # No sidebar menu + #current.menu.options = None + attr["rheader"] = event_rheader + + return attr + + settings.customise_event_event_controller = customise_event_event_controller + + # ------------------------------------------------------------------------- + def customise_event_incident_report_resource(r, tablename): + + current.response.s3.crud_strings[tablename] = Storage( + label_create = T("Log Call"), + title_display = T("Call Log Details"), + title_list = T("Call Logs"), + title_update = T("Edit Call Log"), + label_list_button = T("List Call Logs"), + label_delete_button = T("Delete Call Log"), + msg_record_created = T("Call Log added"), + msg_record_modified = T("Call Log updated"), + msg_record_deleted = T("Call Log removed"), + msg_list_empty = T("No Calls currently logged"), + ) + + settings.customise_event_incident_report_resource = customise_event_incident_report_resource + + # ------------------------------------------------------------------------- + def customise_event_incident_report_controller(**attr): + + from gluon import A + + s3 = current.response.s3 + + # Custom prep + standard_prep = s3.prep + def custom_prep(r): + # Call standard postp + if callable(standard_prep): + result = standard_prep(r) + if not result: + return False + + method = r.method + if method in (None, "create"): + current.s3db.gis_location.addr_street.label = T("Street Address or Location Details") + from s3 import S3SQLCustomForm + crud_form = S3SQLCustomForm((T("What is it?"), "name"), + "incident_type_id", + (T("Who am I speaking with?"), "reported_by"), + (T("How can we contact you?"), "contact"), + (T("Where did this Incident take place?"), "location_id"), + (T("Explain the Situation?"), "description"), + (T("What are your immediate needs?"), "needs"), + ) + r.resource.configure(create_next = URL(args=["[id]", "assign"]), + crud_form = crud_form, + ) + + return True + s3.prep = custom_prep + + # No sidebar menu + current.menu.options = None + req_args = current.request.args + if len(req_args) > 1 and req_args[1] == "assign": + if settings.get_incident_label(): # == "Ticket" + label = T("New Ticket") + else: + label = T("New Incident") + attr["rheader"] = A(label, + _class = "action-btn", + _href = URL(c="event", f="incident", + args = ["create"], + vars = {"incident_report_id": req_args[0]}, + ), + ) + else: + attr["rheader"] = event_rheader + + return attr + + settings.customise_event_incident_report_controller = customise_event_incident_report_controller + + # ------------------------------------------------------------------------- + def event_incident_create_onaccept(form): + """ + Automate Level based on Type, Zone (intersect from Location) & Severity + @ToDo: Move this to SAFIRE/SC + """ + + db = current.db + s3db = current.s3db + + form_vars_get = form.vars.get + incident_id = form_vars_get("id") + + # If Incident Type is Chemical then level must be > 2 + level = form_vars_get("level") + if level and int(level) < 3: + incident_type_id = form_vars_get("incident_type_id") + ittable = s3db.event_incident_type + incident_type = db(ittable.id == incident_type_id).select(ittable.name, + limitby = (0,1) + ).first().name + if incident_type == "Chemical Hazard": + itable = s3db.event_incident + db(itable.id == incident_id).update(level = 3) + current.response.warning = T("Chemical Hazard Incident so Level raised to 3") + + # Alert Lead Agency + organisation_id = form_vars_get("organisation_id") + if organisation_id: + otable = s3db.org_organisation_tag + query = (otable.organisation_id == organisation_id) & \ + (otable.tag == "duty") + duty = db(query).select(otable.value, + limitby = (0, 1) + ).first() + if duty: + current.msg.send_sms_via_api(duty.value, + "You have been assigned an Incident: %s%s" % (settings.get_base_public_url(), + URL(c="event", f= "incident", + args = incident_id), + )) + + # ------------------------------------------------------------------------- + def customise_event_incident_resource(r, tablename): + + from s3 import S3LocationSelector + + s3db = current.s3db + + table = s3db.event_incident + f = table.severity + f.readable = f.writable = True + f = table.level + f.readable = f.writable = True + table.location_id.widget = S3LocationSelector(polygons = True, + show_address = True, + ) + f = table.organisation_id + f.readable = f.writable = True + f.label = T("Lead Response Organization") + if r.method == "plan": + table.action_plan.label = T("Event Action Plan") + else: + f = table.action_plan + f.readable = f.writable = False + + if r.interactive: + s3db.add_custom_callback(tablename, + "create_onaccept", + event_incident_create_onaccept, + ) + + settings.customise_event_incident_resource = customise_event_incident_resource + + # ------------------------------------------------------------------------- + def customise_event_incident_controller(**attr): + + s3db = current.s3db + s3 = current.response.s3 + + # Custom prep + standard_prep = s3.prep + def custom_prep(r): + # Call standard postp + if callable(standard_prep): + result = standard_prep(r) + if not result: + return False + + resource = r.resource + + # Redirect to action plan after create + resource.configure(create_next = URL(c="event", f="incident", + args = ["[id]", "plan"]), + ) + + method = r.method + if method == "create": + incident_report_id = r.get_vars.get("incident_report_id") + if incident_report_id: + # Got here from incident report assign => "New Incident" + # - prepopulate incident name from report title + # - copy incident type and location from report + # - onaccept: link the incident report to the incident + if r.http == "GET": + from s3 import s3_truncate + rtable = s3db.event_incident_report + incident_report = current.db(rtable.id == incident_report_id).select(rtable.name, + rtable.incident_type_id, + rtable.location_id, + limitby = (0, 1), + ).first() + table = r.table + table.name.default = s3_truncate(incident_report.name, 64) + table.incident_type_id.default = incident_report.incident_type_id + table.location_id.default = incident_report.location_id + + elif r.http == "POST": + def create_onaccept(form): + s3db.event_incident_report_incident.insert(incident_id = form.vars.id, + incident_report_id = incident_report_id, + ) + + s3db.add_custom_callback("event_incident", + "create_onaccept", + create_onaccept, + ) + + elif method == "plan" and settings.get_incident_label(): # == "Ticket" + s3db.event_task + s3db.event_organisation + crud_strings = s3.crud_strings + crud_strings.event_task.msg_list_empty = T("No Tasks currently registered for this ticket") + crud_strings.event_organisation.msg_list_empty = T("No Organizations currently registered in this ticket") + + return True + s3.prep = custom_prep + + # No sidebar menu + current.menu.options = None + attr["rheader"] = event_rheader + + return attr + + settings.customise_event_incident_controller = customise_event_incident_controller + + # ------------------------------------------------------------------------- + def customise_event_asset_resource(r, tablename): + + table = current.s3db.event_asset + table.item_id.label = T("Item Type") + table.asset_id.label = T("Specific Item") + # DateTime + from gluon import IS_EMPTY_OR + from s3 import IS_UTC_DATETIME, S3CalendarWidget, S3DateTime + for f in (table.start_date, table.end_date): + f.requires = IS_EMPTY_OR(IS_UTC_DATETIME()) + f.represent = lambda dt: S3DateTime.datetime_represent(dt, utc=True) + f.widget = S3CalendarWidget(timepicker = True) + + if settings.get_incident_label(): # == "Ticket" + current.response.s3.crud_strings[tablename] = Storage( + label_create = T("Add Equipment"), + title_display = T("Equipment Details"), + title_list = T("Equipment"), + title_update = T("Edit Equipment"), + label_list_button = T("List Equipment"), + label_delete_button = T("Remove Equipment from this ticket"), + msg_record_created = T("Equipment added"), + msg_record_modified = T("Equipment updated"), + msg_record_deleted = T("Equipment removed"), + msg_list_empty = T("No Equipment currently registered for this ticket")) + else: + current.response.s3.crud_strings[tablename] = Storage( + label_create = T("Add Equipment"), + title_display = T("Equipment Details"), + title_list = T("Equipment"), + title_update = T("Edit Equipment"), + label_list_button = T("List Equipment"), + label_delete_button = T("Remove Equipment from this incident"), + msg_record_created = T("Equipment added"), + msg_record_modified = T("Equipment updated"), + msg_record_deleted = T("Equipment removed"), + msg_list_empty = T("No Equipment currently registered for this incident")) + + settings.customise_event_asset_resource = customise_event_asset_resource + + # ------------------------------------------------------------------------- + def event_human_resource_onaccept(form, create=True): + """ + When a Position is assigned to an Incident: + - set_event_from_incident + - add Log Entry + - send Notification + """ + + db = current.db + s3db = current.s3db + + s3db.event_set_event_from_incident(form, "event_human_resource") + + table = s3db.event_human_resource + + form_vars = form.vars + form_vars_get = form_vars.get + link_id = form_vars_get("id") + incident_id = form_vars_get("incident_id") + if not incident_id: + link = db(table.id == link_id).select(table.incident_id, + limitby = (0, 1) + ).first() + incident_id = link.incident_id + + pe_id = None + if create: + person_id = form_vars_get("person_id") + if person_id: + ptable = s3db.pr_person + person = db(ptable.id == person_id).select(ptable.pe_id, + limitby = (0, 1) + ).first() + pe_id = person.pe_id + + job_title_id = form_vars_get("job_title_id") + if job_title_id: + s3db.event_incident_log.insert(incident_id = incident_id, + name = "Person Requested", + comments = s3db.event_human_resource.job_title_id.represent(job_title_id), + ) + else: + # Update + record = form.record + if record: # Not True for a record merger + from s3dal import Field + changed = {} + for var in form_vars: + vvar = form_vars[var] + if isinstance(vvar, Field): + # modified_by/modified_on + continue + rvar = record.get(var, "NOT_PRESENT") + if rvar != "NOT_PRESENT" and vvar != rvar: + f = table[var] + if var == "pe_id": + pe_id = vvar + type_ = f.type + if type_ == "integer" or \ + type_.startswith("reference"): + if vvar: + vvar = int(vvar) + if vvar == rvar: + continue + represent = table[var].represent + if represent: + if hasattr(represent, "show_link"): + represent.show_link = False + else: + represent = lambda o: o + if rvar: + changed[var] = "%s changed from %s to %s" % \ + (f.label, represent(rvar), represent(vvar)) + else: + changed[var] = "%s changed to %s" % \ + (f.label, represent(vvar)) + + if changed: + table = s3db.event_incident_log + text = [] + for var in changed: + text.append(changed[var]) + text = "\n".join(text) + table.insert(incident_id = incident_id, + #name = "Person Assigned", + name = "Person Request Updated", + comments = text, + ) + + if pe_id: + # Notify Assignee + if settings.get_incident_label(): # == "Ticket" + label = T("Ticket") + else: + label = T("Incident") + current.msg.send_by_pe_id(pe_id, + subject = "", + message = "You have been assigned to an %s: %s%s" % \ + (label, + settings.get_base_public_url(), + URL(c="event", f= "incident", + args = [incident_id, "human_resource", link_id]), + ), + contact_method = "SMS") + + # ------------------------------------------------------------------------- + def customise_event_human_resource_resource(r, tablename): + + s3db = current.s3db + table = s3db.event_human_resource + # DateTime + from gluon import IS_EMPTY_OR + from s3 import IS_UTC_DATETIME, S3CalendarWidget, S3DateTime + for f in (table.start_date, table.end_date): + f.requires = IS_EMPTY_OR(IS_UTC_DATETIME()) + f.represent = lambda dt: S3DateTime.datetime_represent(dt, utc=True) + f.widget = S3CalendarWidget(timepicker = True) + + if settings.get_incident_label(): # == "Ticket" + current.response.s3.crud_strings[tablename] = Storage( + label_create = T("Add Person"), + title_display = T("Person Details"), + title_list = T("Personnel"), + title_update = T("Edit Person"), + label_list_button = T("List Personnel"), + label_delete_button = T("Remove Person from this ticket"), + msg_record_created = T("Person added"), + msg_record_modified = T("Person updated"), + msg_record_deleted = T("Person removed"), + msg_list_empty = T("No Persons currently registered for this ticket")) + else: + current.response.s3.crud_strings[tablename] = Storage( + label_create = T("Add Person"), + title_display = T("Person Details"), + title_list = T("Personnel"), + title_update = T("Edit Person"), + label_list_button = T("List Personnel"), + label_delete_button = T("Remove Person from this incident"), + msg_record_created = T("Person added"), + msg_record_modified = T("Person updated"), + msg_record_deleted = T("Person removed"), + msg_list_empty = T("No Persons currently registered for this incident")) + + s3db.configure(tablename, + # Deliberately over-rides + create_onaccept = event_human_resource_onaccept, + update_onaccept = lambda form: + event_human_resource_onaccept(form, create=False), + ) + + settings.customise_event_human_resource_resource = customise_event_human_resource_resource + + # ------------------------------------------------------------------------- + def customise_event_scenario_controller(**attr): + + s3 = current.response.s3 + + # Custom prep + standard_prep = s3.prep + def custom_prep(r): + # Call standard postp + if callable(standard_prep): + result = standard_prep(r) + if not result: + return False + + if r.method != "plan": + f = r.table.action_plan + f.readable = f.writable = False + + if r.method == "create"and r.http == "POST": + r.resource.configure(create_next = URL(c="event", f="scenario", + args = ["[id]", "plan"]), + ) + + return True + s3.prep = custom_prep + + # No sidebar menu + current.menu.options = None + attr["rheader"] = event_rheader + + return attr + + settings.customise_event_scenario_controller = customise_event_scenario_controller + + # ------------------------------------------------------------------------- + def customise_event_scenario_asset_resource(r, tablename): + + table = current.s3db.event_scenario_asset + table.item_id.label = T("Item Type") + table.asset_id.label = T("Specific Item") + + if settings.get_incident_label(): # == "Ticket" + current.response.s3.crud_strings[tablename] = Storage( + label_create = T("Add Equipment"), + title_display = T("Equipment Details"), + title_list = T("Equipment"), + title_update = T("Edit Equipment"), + label_list_button = T("List Equipment"), + label_delete_button = T("Remove Equipment from this ticket"), + msg_record_created = T("Equipment added"), + msg_record_modified = T("Equipment updated"), + msg_record_deleted = T("Equipment removed"), + msg_list_empty = T("No Equipment currently registered for this ticket")) + else: + current.response.s3.crud_strings[tablename] = Storage( + label_create = T("Add Equipment"), + title_display = T("Equipment Details"), + title_list = T("Equipment"), + title_update = T("Edit Equipment"), + label_list_button = T("List Equipment"), + label_delete_button = T("Remove Equipment from this incident"), + msg_record_created = T("Equipment added"), + msg_record_modified = T("Equipment updated"), + msg_record_deleted = T("Equipment removed"), + msg_list_empty = T("No Equipment currently registered for this incident")) + + settings.customise_event_scenario_asset_resource = customise_event_scenario_asset_resource + + # ------------------------------------------------------------------------- + def customise_event_scenario_human_resource_resource(r, tablename): + + if settings.get_incident_label(): # == "Ticket" + current.response.s3.crud_strings[tablename] = Storage( + label_create = T("Add Person"), + title_display = T("Person Details"), + title_list = T("Personnel"), + title_update = T("Edit Person"), + label_list_button = T("List Personnel"), + label_delete_button = T("Remove Person from this ticket"), + msg_record_created = T("Person added"), + msg_record_modified = T("Person updated"), + msg_record_deleted = T("Person removed"), + msg_list_empty = T("No Persons currently registered for this ticket")) + else: + current.response.s3.crud_strings[tablename] = Storage( + label_create = T("Add Person"), + title_display = T("Person Details"), + title_list = T("Personnel"), + title_update = T("Edit Person"), + label_list_button = T("List Personnel"), + label_delete_button = T("Remove Person from this incident"), + msg_record_created = T("Person added"), + msg_record_modified = T("Person updated"), + msg_record_deleted = T("Person removed"), + msg_list_empty = T("No Persons currently registered for this incident")) + + settings.customise_event_scenario_human_resource_resource = customise_event_scenario_human_resource_resource + + # ------------------------------------------------------------------------- + # HRM + # ------------------------------------------------------------------------- + settings.hrm.job_title_deploy = True + settings.hrm.org_dependent_job_titles = True + + # ------------------------------------------------------------------------- + # Organisations + # ------------------------------------------------------------------------- + + # ------------------------------------------------------------------------- + def customise_org_organisation_resource(r, tablename): + + s3db = current.s3db + + # Custom Components + s3db.add_components(tablename, + org_organisation_tag = (# On-call Duty Number + {"name": "duty", + "joinby": "organisation_id", + "filterby": {"tag": "duty", + }, + "multiple": False, + }, + ), + ) + + from s3 import S3SQLCustomForm, S3SQLInlineComponent, S3SQLInlineLink, \ + IS_EMPTY_OR, IS_PHONE_NUMBER_MULTI, S3PhoneWidget, s3_phone_represent + + # Individual settings for specific tag components + components_get = s3db.resource(tablename).components.get + + duty = components_get("duty") + f = duty.table.value + f.represent = s3_phone_represent, + f.requires = IS_EMPTY_OR(IS_PHONE_NUMBER_MULTI()) + f.widget = S3PhoneWidget() + + crud_form = S3SQLCustomForm("name", + "acronym", + S3SQLInlineLink("organisation_type", + field = "organisation_type_id", + # Default 10 options just triggers which adds unnecessary complexity to a commonly-used form & commonly an early one (create Org when registering) + search = False, + label = T("Type"), + multiple = False, + widget = "multiselect", + ), + "country", + (T("Reception Phone #"), "phone"), + S3SQLInlineComponent("duty", + label = T("On-call Duty Number"), + fields = [("", "value")], + multiple = False, + ), + "website", + "logo", + "comments", + ) + + s3db.configure(tablename, + crud_form = crud_form, + ) + + settings.customise_org_organisation_resource = customise_org_organisation_resource + + # ------------------------------------------------------------------------- + # Projects + # ------------------------------------------------------------------------- + + # ------------------------------------------------------------------------- + def project_task_onaccept(form, create=True): + """ + Send Person a Notification when they are assigned to a Task + Log changes in Incident Log + """ + + if current.request.function == "scenario": + # Must be a Scenario + # - don't Log + # - don't send Notification + return + + db = current.db + s3db = current.s3db + ltable = s3db.event_task + + form_vars = form.vars + form_vars_get = form_vars.get + task_id = form_vars_get("id") + link = db(ltable.task_id == task_id).select(ltable.incident_id, + limitby = (0, 1) + ).first() + if not link: + # Not attached to an Incident + # - don't Log + # - don't send Notification + return + + incident_id = link.incident_id + + if create: + pe_id = form_vars_get("pe_id") + # Log + name = form_vars_get("name") + if name: + s3db.event_incident_log.insert(incident_id = incident_id, + name = "Task Created", + comments = name, + ) + + else: + # Update + pe_id = None + record = form.record + if record: # Not True for a record merger + from s3dal import Field + table = s3db.project_task + changed = {} + for var in form_vars: + vvar = form_vars[var] + if isinstance(vvar, Field): + # modified_by/modified_on + continue + if var == "pe_id": + pe_id = vvar + rvar = record.get(var, "NOT_PRESENT") + if rvar != "NOT_PRESENT" and vvar != rvar: + f = table[var] + type_ = f.type + if type_ == "integer" or \ + type_.startswith("reference"): + if vvar: + vvar = int(vvar) + if vvar == rvar: + continue + represent = table[var].represent + if represent: + if hasattr(represent, "show_link"): + represent.show_link = False + else: + represent = lambda o: o + if rvar: + changed[var] = "%s changed from %s to %s" % \ + (f.label, represent(rvar), represent(vvar)) + else: + changed[var] = "%s changed to %s" % \ + (f.label, represent(vvar)) + + if changed: + table = s3db.event_incident_log + text = [] + for var in changed: + text.append(changed[var]) + text = "\n".join(text) + table.insert(incident_id = incident_id, + name = "Task Updated", + comments = text, + ) + + if pe_id: + # Notify Assignee + message = "You have been assigned a Task: %s%s" % \ + (settings.get_base_public_url(), + URL(c="event", f= "incident", + args = [incident_id, "task", task_id]), + ) + instance_type = s3db.pr_instance_type(pe_id) + if instance_type == "org_organisation": + # Notify the Duty Number for the Organisation, not everyone in the Organisation! + otable = s3db.org_organisation + ottable = s3db.org_organisation_tag + query = (otable.pe_id == pe_id) & \ + (ottable.organisation_id == otable.id) & \ + (ottable.tag == "duty") + duty = db(query).select(ottable.value, + limitby = (0, 1) + ).first() + if duty: + current.msg.send_sms_via_api(duty.value, + message) + else: + task_notification = settings.get_event_task_notification() + if task_notification: + current.msg.send_by_pe_id(pe_id, + subject = "%s: Task assigned to you" % settings.get_system_name_short(), + message = message, + contact_method = task_notification) + + # ------------------------------------------------------------------------- + def customise_project_task_resource(r, tablename): + + s3db = current.s3db + + f = s3db.project_task.source + f.readable = f.writable = False + + s3db.configure(tablename, + # No need to see time log: KISS + crud_form = None, + # NB We deliberatly over-ride the default one + create_onaccept = project_task_onaccept, + # In event_ActionPlan() + #list_fields = ["priority", + # "name", + # "pe_id", + # "status_id", + # "date_due", + # ], + update_onaccept = lambda form: + project_task_onaccept(form, create=False), + ) + + settings.customise_project_task_resource = customise_project_task_resource + +# END ========================================================================= diff --git a/lxc-apps/safire/install/sahana_data/masterUsers.csv b/lxc-apps/safire/install/sahana_data/masterUsers.csv new file mode 100644 index 0000000..89fcbca --- /dev/null +++ b/lxc-apps/safire/install/sahana_data/masterUsers.csv @@ -0,0 +1,2 @@ +First Name,Last Name,Email,Password,Role,Organisation +Admin,User,${SAFIRE_ADMIN_USER},${SAFIRE_ADMIN_PWD},ADMIN, diff --git a/lxc-apps/safire/install/update-conf.sh b/lxc-apps/safire/install/update-conf.sh new file mode 100755 index 0000000..43348a9 --- /dev/null +++ b/lxc-apps/safire/install/update-conf.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +# Volumes +SAHANA_CONF="${VOLUMES_DIR}/safire/sahana_conf" + +# Variables +HTTP_HOST="${HOST}" +[ "${PORT}" != "443" ] && HTTP_HOST="${HTTP_HOST}:${PORT}" + +# Replacements +sed -i "s|\(^settings\.base\.public_url = \).*|\1\"https://${HTTP_HOST}\"|" ${SAHANA_CONF}/000_config.py +sed -i "s|\(^settings\.mail\.sender = \).*|\1\"${EMAIL}\"|" ${SAHANA_CONF}/000_config.py +sed -i "s|\(^settings\.mail\.approver = \).*|\1\"${EMAIL}\"|" ${SAHANA_CONF}/000_config.py +sed -i "s|\(^settings\.gis\.api_google = \).*|\1\"${GMAPS_API_KEY}\"|" ${SAHANA_CONF}/000_config.py diff --git a/lxc-apps/safire/uninstall.sh b/lxc-apps/safire/uninstall.sh new file mode 100755 index 0000000..1a984a5 --- /dev/null +++ b/lxc-apps/safire/uninstall.sh @@ -0,0 +1,8 @@ +#!/bin/sh +set -ev + +# Remove persistent data +rm -rf "${VOLUMES_DIR}/safire" + +# Unregister application +vmmgr unregister-app safire diff --git a/lxc-apps/share/app b/lxc-apps/share/app new file mode 100644 index 0000000..e11800c --- /dev/null +++ b/lxc-apps/share/app @@ -0,0 +1,30 @@ +{ + "version": "0.0.1-200403", + "meta": { + "title": "Sahana Eden - SHARE", + "desc-cs": "Řízení humanítární činnosti - Úleva a rehabilitace", + "desc-en": "Management of humanitarian activities - Relief and rehabilitation", + "license": "GPL" + }, + "containers": { + "share": { + "image": "sahana_0.0.1-200403", + "depends": [ + "share-postgres" + ], + "mounts": { + "share/sahana_conf": "srv/web2py/applications/eden/models", + "share/sahana_data/SHARE": "srv/web2py/applications/eden/modules/templates/SHARE", + "share/sahana_data/databases": "srv/web2py/applications/eden/databases", + "share/sahana_data/uploads": "srv/web2py/applications/eden/uploads", + "share/sahana_data/masterUsers.csv": "srv/web2py/applications/eden/modules/templates/default/users/masterUsers.csv:file" + } + }, + "share-postgres": { + "image": "postgis_3.0.0-200403", + "mounts": { + "share/postgres_data": "var/lib/postgresql" + } + } + } +} diff --git a/lxc-apps/share/install.sh b/lxc-apps/share/install.sh new file mode 100755 index 0000000..6a7116f --- /dev/null +++ b/lxc-apps/share/install.sh @@ -0,0 +1,52 @@ +#!/bin/sh +set -ev + +# Volumes +POSTGRES_DATA="${VOLUMES_DIR}/share/postgres_data" +SAHANA_DATA="${VOLUMES_DIR}/share/sahana_data" +SAHANA_CONF="${VOLUMES_DIR}/share/sahana_conf" +SAHANA_LAYER="${LAYERS_DIR}/sahana_0.0.1-200403" + +# Create Postgres instance +install -o 105432 -g 105432 -m 700 -d ${POSTGRES_DATA} +spoc-container exec share-postgres -- initdb -D /var/lib/postgresql + +# Configure Postgres +install -o 105432 -g 105432 -m 600 postgres_data/postgresql.conf ${POSTGRES_DATA}/postgresql.conf +install -o 105432 -g 105432 -m 600 postgres_data/pg_hba.conf ${POSTGRES_DATA}/pg_hba.conf + +# Create PostgreSQL user and database +export SHARE_PWD=$(head -c 18 /dev/urandom | base64 | tr -d '+/=') +spoc-container start share-postgres +envsubst 0 logs only + # statements running at least this number + # of milliseconds + +#log_transaction_sample_rate = 0.0 # Fraction of transactions whose statements + # are logged regardless of their duration. 1.0 logs all + # statements from all transactions, 0.0 never logs. + +# - What to Log - + +#debug_print_parse = off +#debug_print_rewritten = off +#debug_print_plan = off +#debug_pretty_print = on +#log_checkpoints = off +#log_connections = off +#log_disconnections = off +#log_duration = off +#log_error_verbosity = default # terse, default, or verbose messages +#log_hostname = off +log_line_prefix = '%m [%p] %q%u@%d ' # special values: + # %a = application name + # %u = user name + # %d = database name + # %r = remote host and port + # %h = remote host + # %p = process ID + # %t = timestamp without milliseconds + # %m = timestamp with milliseconds + # %n = timestamp with milliseconds (as a Unix epoch) + # %i = command tag + # %e = SQL state + # %c = session ID + # %l = session line number + # %s = session start timestamp + # %v = virtual transaction ID + # %x = transaction ID (0 if none) + # %q = stop here in non-session + # processes + # %% = '%' + # e.g. '<%u%%%d> ' +#log_lock_waits = off # log lock waits >= deadlock_timeout +#log_statement = 'none' # none, ddl, mod, all +#log_replication_commands = off +#log_temp_files = -1 # log temporary files equal or larger + # than the specified size in kilobytes; + # -1 disables, 0 logs all temp files +log_timezone = 'Europe/Prague' + +#------------------------------------------------------------------------------ +# PROCESS TITLE +#------------------------------------------------------------------------------ + +#cluster_name = '' # added to process titles if nonempty + # (change requires restart) +#update_process_title = on + + +#------------------------------------------------------------------------------ +# STATISTICS +#------------------------------------------------------------------------------ + +# - Query and Index Statistics Collector - + +#track_activities = on +#track_counts = on +#track_io_timing = off +#track_functions = none # none, pl, all +#track_activity_query_size = 1024 # (change requires restart) +#stats_temp_directory = 'pg_stat_tmp' + + +# - Monitoring - + +#log_parser_stats = off +#log_planner_stats = off +#log_executor_stats = off +#log_statement_stats = off + + +#------------------------------------------------------------------------------ +# AUTOVACUUM +#------------------------------------------------------------------------------ + +#autovacuum = on # Enable autovacuum subprocess? 'on' + # requires track_counts to also be on. +#log_autovacuum_min_duration = -1 # -1 disables, 0 logs all actions and + # their durations, > 0 logs only + # actions running at least this number + # of milliseconds. +#autovacuum_max_workers = 3 # max number of autovacuum subprocesses + # (change requires restart) +#autovacuum_naptime = 1min # time between autovacuum runs +#autovacuum_vacuum_threshold = 50 # min number of row updates before + # vacuum +#autovacuum_analyze_threshold = 50 # min number of row updates before + # analyze +#autovacuum_vacuum_scale_factor = 0.2 # fraction of table size before vacuum +#autovacuum_analyze_scale_factor = 0.1 # fraction of table size before analyze +#autovacuum_freeze_max_age = 200000000 # maximum XID age before forced vacuum + # (change requires restart) +#autovacuum_multixact_freeze_max_age = 400000000 # maximum multixact age + # before forced vacuum + # (change requires restart) +#autovacuum_vacuum_cost_delay = 2ms # default vacuum cost delay for + # autovacuum, in milliseconds; + # -1 means use vacuum_cost_delay +#autovacuum_vacuum_cost_limit = -1 # default vacuum cost limit for + # autovacuum, -1 means use + # vacuum_cost_limit + + +#------------------------------------------------------------------------------ +# CLIENT CONNECTION DEFAULTS +#------------------------------------------------------------------------------ + +# - Statement Behavior - + +#client_min_messages = notice # values in order of decreasing detail: + # debug5 + # debug4 + # debug3 + # debug2 + # debug1 + # log + # notice + # warning + # error +#search_path = '"$user", public' # schema names +#row_security = on +#default_tablespace = '' # a tablespace name, '' uses the default +#temp_tablespaces = '' # a list of tablespace names, '' uses + # only default tablespace +#default_table_access_method = 'heap' +#check_function_bodies = on +#default_transaction_isolation = 'read committed' +#default_transaction_read_only = off +#default_transaction_deferrable = off +#session_replication_role = 'origin' +#statement_timeout = 0 # in milliseconds, 0 is disabled +#lock_timeout = 0 # in milliseconds, 0 is disabled +#idle_in_transaction_session_timeout = 0 # in milliseconds, 0 is disabled +#vacuum_freeze_min_age = 50000000 +#vacuum_freeze_table_age = 150000000 +#vacuum_multixact_freeze_min_age = 5000000 +#vacuum_multixact_freeze_table_age = 150000000 +#vacuum_cleanup_index_scale_factor = 0.1 # fraction of total number of tuples + # before index cleanup, 0 always performs + # index cleanup +#bytea_output = 'hex' # hex, escape +#xmlbinary = 'base64' +#xmloption = 'content' +#gin_fuzzy_search_limit = 0 +#gin_pending_list_limit = 4MB + +# - Locale and Formatting - + +datestyle = 'iso, mdy' +#intervalstyle = 'postgres' +timezone = 'Europe/Prague' +#timezone_abbreviations = 'Default' # Select the set of available time zone + # abbreviations. Currently, there are + # Default + # Australia (historical usage) + # India + # You can create your own file in + # share/timezonesets/. +#extra_float_digits = 1 # min -15, max 3; any value >0 actually + # selects precise output mode +#client_encoding = sql_ascii # actually, defaults to database + # encoding + +# These settings are initialized by initdb, but they can be changed. +lc_messages = 'C' # locale for system error message + # strings +lc_monetary = 'C' # locale for monetary formatting +lc_numeric = 'C' # locale for number formatting +lc_time = 'C' # locale for time formatting + +# default configuration for text search +default_text_search_config = 'pg_catalog.english' + +# - Shared Library Preloading - + +#shared_preload_libraries = '' # (change requires restart) +#local_preload_libraries = '' +#session_preload_libraries = '' +#jit_provider = 'llvmjit' # JIT library to use + +# - Other Defaults - + +#dynamic_library_path = '$libdir' + + +#------------------------------------------------------------------------------ +# LOCK MANAGEMENT +#------------------------------------------------------------------------------ + +#deadlock_timeout = 1s +#max_locks_per_transaction = 64 # min 10 + # (change requires restart) +#max_pred_locks_per_transaction = 64 # min 10 + # (change requires restart) +#max_pred_locks_per_relation = -2 # negative values mean + # (max_pred_locks_per_transaction + # / -max_pred_locks_per_relation) - 1 +#max_pred_locks_per_page = 2 # min 0 + + +#------------------------------------------------------------------------------ +# VERSION AND PLATFORM COMPATIBILITY +#------------------------------------------------------------------------------ + +# - Previous PostgreSQL Versions - + +#array_nulls = on +#backslash_quote = safe_encoding # on, off, or safe_encoding +#escape_string_warning = on +#lo_compat_privileges = off +#operator_precedence_warning = off +#quote_all_identifiers = off +#standard_conforming_strings = on +#synchronize_seqscans = on + +# - Other Platforms and Clients - + +#transform_null_equals = off + + +#------------------------------------------------------------------------------ +# ERROR HANDLING +#------------------------------------------------------------------------------ + +#exit_on_error = off # terminate session on any error? +#restart_after_crash = on # reinitialize after backend crash? +#data_sync_retry = off # retry or panic on failure to fsync + # data? + # (change requires restart) + + +#------------------------------------------------------------------------------ +# CONFIG FILE INCLUDES +#------------------------------------------------------------------------------ + +# These options allow settings to be loaded from files other than the +# default postgresql.conf. Note that these are directives, not variable +# assignments, so they can usefully be given more than once. + +#include_dir = '...' # include files ending in '.conf' from + # a directory, e.g., 'conf.d' +#include_if_exists = '...' # include file only if it exists +#include = '...' # include file + + +#------------------------------------------------------------------------------ +# CUSTOMIZED OPTIONS +#------------------------------------------------------------------------------ + +# Add settings for extensions here diff --git a/lxc-apps/share/install/sahana_conf/000_config.py b/lxc-apps/share/install/sahana_conf/000_config.py new file mode 100644 index 0000000..f61d4aa --- /dev/null +++ b/lxc-apps/share/install/sahana_conf/000_config.py @@ -0,0 +1,263 @@ +# -*- coding: utf-8 -*- + +""" + Machine-specific settings + All settings which are typically edited for a specific machine should be done here + + Deployers should ideally not need to edit any other files outside of their template folder + + Note for Developers: + /models/000_config.py is NOT in the Git repository, to avoid leaking of + sensitive or irrelevant information into the repository. + For changes to be committed, please also edit: + modules/templates/000_config.py +""" + +# Remove this line when you have edited this file sufficiently to proceed to the web interface +FINISHED_EDITING_CONFIG_FILE = True + +# Select the Template +# - which Modules are enabled +# - PrePopulate data +# - Security Policy +# - Workflows +# - Theme +# - note that you should restart your web2py after changing this setting +settings.base.template = "SHARE" + +# Database settings +# Uncomment to use a different database, other than sqlite +settings.database.db_type = "postgres" +#settings.database.db_type = "mysql" +# Uncomment to use a different host +settings.database.host = "share-postgres" +# Uncomment to use a different port +#settings.database.port = 3306 +#settings.database.port = 5432 +# Uncomment to select a different name for your database +settings.database.database = "share" +# Uncomment to select a different username for your database +settings.database.username = "share" +# Uncomment to set the password +# NB Web2Py doesn't like passwords with an @ in them +settings.database.password = "${SHARE_PWD}" +# Uncomment to use a different pool size +#settings.database.pool_size = 30 +# Do we have a spatial DB available? (currently supports PostGIS. Spatialite to come.) +settings.gis.spatialdb = True + +# Base settings +settings.base.system_name = T("Sahana Relief and Rehabilitation ") +settings.base.system_name_short = T("SHARE") +# Set this to the Public URL of the instance +settings.base.public_url = "https://share.spotter.vm" + +# Switch to "False" in Production for a Performance gain +# (need to set to "True" again when Table definitions are changed) +settings.base.migrate = True +# To just create the .table files (also requires migrate=True): +#settings.base.fake_migrate = True + +# Set this to True to switch to Debug mode +# Debug mode means that uncompressed CSS/JS files are loaded +# JS Debug messages are also available in the Console +# can also load an individual page in debug mode by appending URL with +# ?debug=1 +settings.base.debug = True + +# Uncomment this to prevent automated test runs from remote +# settings.base.allow_testing = False + +# Configure the log level ("DEBUG", "INFO", "WARNING", "ERROR" or "CRITICAL"), None = turn off logging (default) +#settings.log.level = "ERROR" # DEBUG set automatically when base.debug is True +# Uncomment to prevent writing log messages to the console (sys.stderr) +#settings.log.console = False +# Configure a log file (file name) +#settings.log.logfile = None +# Uncomment to get detailed caller information +#settings.log.caller_info = True + +# Uncomment to use Content Delivery Networks to speed up Internet-facing sites +#settings.base.cdn = True + +# Allow language files to be updated automatically +#settings.L10n.languages_readonly = False + +# This setting should be changed _before_ registering the 1st user +# - should happen automatically if installing using supported scripts +settings.auth.hmac_key = "${SHARE_HMAC}" + +# If using Masterkey Authentication, then set this to a deployment-specific 32 char string: +#settings.auth.masterkey_app_key = "randomstringrandomstringrandomstring" + +# Minimum Password Length +#settings.auth.password_min_length = 8 + +# Email settings +# Outbound server +settings.mail.server = "host:25" +#settings.mail.tls = True +# Useful for Windows Laptops: +# https://www.google.com/settings/security/lesssecureapps +#settings.mail.server = "smtp.gmail.com:587" +#settings.mail.tls = True +#settings.mail.login = "username:password" +# From Address - until this is set, no mails can be sent +settings.mail.sender = "${SHARE_ADMIN_USER}" +# Default email address to which requests to approve new user accounts gets sent +# This can be overridden for specific domains/organisations via the auth_domain table +settings.mail.approver = "${SHARE_ADMIN_USER}" +# Daily Limit on Sending of emails +#settings.mail.limit = 1000 + +# Frontpage settings +# RSS feeds +settings.frontpage.rss = [ + {"title": "Eden", + # Trac timeline + "url": "http://eden.sahanafoundation.org/timeline?ticket=on&changeset=on&milestone=on&wiki=on&max=50&daysback=90&format=rss" + }, + {"title": "Twitter", + # @SahanaFOSS + #"url": "https://search.twitter.com/search.rss?q=from%3ASahanaFOSS" # API v1 deprecated, so doesn't work, need to use 3rd-party service, like: + "url": "http://www.rssitfor.me/getrss?name=@SahanaFOSS" + # Hashtag + #url: "http://search.twitter.com/search.atom?q=%23eqnz" # API v1 deprecated, so doesn't work, need to use 3rd-party service, like: + #url: "http://api2.socialmention.com/search?q=%23eqnz&t=all&f=rss" + } +] + +# Uncomment to restrict to specific country/countries +#settings.gis.countries= ("LK",) + +# Uncomment to enable a guided tour +#settings.base.guided_tour = True + +# Bing API Key (for Map layers) +# http://www.microsoft.com/maps/create-a-bing-maps-key.aspx +#settings.gis.api_bing = "" +# Google API Key (for Google Maps Layers) +settings.gis.api_google = "" +# Yahoo API Key (for Geocoder) +#settings.gis.api_yahoo = "" + +# GeoNames username +#settings.gis.geonames_username = "" + +# Fill this in to get Google Analytics for your site +#settings.base.google_analytics_tracking_id = "" + +# Chat server, see: http://eden.sahanafoundation.org/wiki/InstallationGuidelines/Chat +#settings.base.chat_server = { +# "ip": "127.0.0.1", +# "port": 7070, +# "name": "servername", +# # Default group everyone is added to +# "groupname" : "everyone", +# "server_db" : "openfire", +# # These settings fallback to main DB settings if not specified +# # Only mysql/postgres supported +# #"server_db_type" : "mysql", +# #"server_db_username" : "", +# #"server_db_password": "", +# #"server_db_port" : 3306, +# #"server_db_ip" : "127.0.0.1", +# } + +# GeoServer (Currently used by GeoExplorer. Will allow REST control of GeoServer.) +# NB Needs to be publically-accessible URL for querying via client JS +#settings.gis.geoserver_url = "http://localhost/geoserver" +#settings.gis.geoserver_username = "admin" +#settings.gis.geoserver_password = "" +# Print Service URL: http://eden.sahanafoundation.org/wiki/BluePrintGISPrinting +#settings.gis.print_service = "/geoserver/pdf/" + +# Google OAuth (to allow users to login using Google) +# https://code.google.com/apis/console/ +#settings.auth.google_id = "" +#settings.auth.google_secret = "" + +# Pootle server +# settings.L10n.pootle_url = "http://pootle.sahanafoundation.org/" +# settings.L10n.pootle_username = "username" +# settings.L10n.pootle_password = "*****" + +# SOLR server for Full-Text Search +#settings.base.solr_url = "http://127.0.0.1:8983/solr/" + +# Memcache server to allow sharing of sessions across instances +#settings.base.session_memcache = '127.0.0.1:11211' + +settings.base.session_db = True + +# UI options +# Should user be prompted to save before navigating away? +#settings.ui.navigate_away_confirm = False +# Should user be prompted to confirm actions? +#settings.ui.confirm = False +# Should potentially large dropdowns be turned into autocompletes? +# (unused currently) +#settings.ui.autocomplete = True +#settings.ui.read_label = "Details" +#settings.ui.update_label = "Edit" + +# Audit settings +# - can be a callable for custom hooks (return True to also perform normal logging, or False otherwise) +# NB Auditing (especially Reads) slows system down & consumes diskspace +#settings.security.audit_write = False +#settings.security.audit_read = False + +# Performance Options +# Maximum number of search results for an Autocomplete Widget +#settings.search.max_results = 200 +# Maximum number of features for a Map Layer +#settings.gis.max_features = 1000 + +# CAP Settings +# Change for different authority and organisations +# See http://alerting.worldweather.org/ for oid +# Country root oid. The oid for the organisation includes this base +#settings.cap.identifier_oid = "2.49.0.0.608.0" +# Set the period (in days) after which alert info segments expire (default=2) +#settings.cap.info_effective_period = 2 + +# ============================================================================= +# Import the settings from the Template +# - note: invalid settings are ignored +# +settings.import_template() + +# ============================================================================= +# Over-rides to the Template may be done here +# + +# e.g. +#settings.base.system_name = T("Sahana TEST") +#settings.base.prepopulate = ("MY_TEMPLATE_ONLY") +#settings.base.prepopulate += ("default", "default/users") +#settings.base.theme = "default" +settings.L10n.default_language = "cs" +#settings.security.policy = 7 # Organisation-ACLs +# Enable Additional Module(s) +#from gluon.storage import Storage +#settings.modules["delphi"] = Storage( +# name_nice = T("Delphi Decision Maker"), +# restricted = False, +# module_type = 10, +# ) +# Disable a module which is normally used by the template +# - NB Only templates with adaptive menus will work nicely with this! +#del settings.modules["irs"] + +# Production instances should set this before prepopulate is run +#settings.base.prepopulate_demo = 0 + +# After 1st_run, set this for Production to save 1x DAL hit/request +#settings.base.prepopulate = 0 + +# ============================================================================= +# A version number to tell update_check if there is a need to refresh the +# running copy of this file +VERSION = 1 + +# END ========================================================================= diff --git a/lxc-apps/share/install/sahana_conf/00_settings.py b/lxc-apps/share/install/sahana_conf/00_settings.py new file mode 100644 index 0000000..5610291 --- /dev/null +++ b/lxc-apps/share/install/sahana_conf/00_settings.py @@ -0,0 +1,318 @@ +# -*- coding: utf-8 -*- + +""" + Global settings: + + Those which are typically edited during a deployment are in + 000_config.py & their results parsed into here. Deployers + shouldn't typically need to edit any settings here. +""" + +# Keep all our configuration options off the main global variables + +# Use response.s3 for one-off variables which are visible in views without explicit passing +s3.formats = Storage() + +# Workaround for this Bug in Selenium with FF4: +# http://code.google.com/p/selenium/issues/detail?id=1604 +s3.interactive = settings.get_ui_confirm() + +s3.base_url = "%s/%s" % (settings.get_base_public_url(), + appname) +s3.download_url = "%s/default/download" % s3.base_url + +# ----------------------------------------------------------------------------- +# Client tests + +# Check whether browser is Mobile & store result in session +# - commented-out until we make use of it +#if session.s3.mobile is None: +# session.s3.mobile = s3base.s3_is_mobile_client(request) +#if session.s3.browser is None: +# session.s3.browser = s3base.s3_populate_browser_compatibility(request) + +# ----------------------------------------------------------------------------- +# Global variables + +# Strings to i18n +# Common Labels +#messages["BREADCRUMB"] = ">> " +messages["UNKNOWN_OPT"] = "" +messages["NONE"] = "" +messages["OBSOLETE"] = "Obsolete" +messages["READ"] = settings.get_ui_label_read() +messages["UPDATE"] = settings.get_ui_label_update() +messages["DELETE"] = "Delete" +messages["COPY"] = "Copy" +messages["NOT_APPLICABLE"] = "N/A" +messages["ADD_PERSON"] = "Create a Person" +messages["ADD_LOCATION"] = "Create Location" +messages["SELECT_LOCATION"] = "Select a location" +messages["COUNTRY"] = "Country" +messages["ORGANISATION"] = "Organization" +messages["AUTOCOMPLETE_HELP"] = "Enter some characters to bring up a list of possible matches" + +for u in messages: + if isinstance(messages[u], str): + globals()[u] = T(messages[u]) + +# CRUD Labels +s3.crud_labels = Storage(READ=READ, + UPDATE=UPDATE, + DELETE=DELETE, + COPY=COPY, + NONE=NONE, + ) + +# Error Messages +ERROR["BAD_RECORD"] = "Record not found!" +ERROR["BAD_METHOD"] = "Unsupported method!" +ERROR["BAD_FORMAT"] = "Unsupported data format!" +ERROR["BAD_REQUEST"] = "Invalid request" +ERROR["BAD_SOURCE"] = "Invalid source" +ERROR["BAD_TEMPLATE"] = "XSLT stylesheet not found" +ERROR["BAD_RESOURCE"] = "Nonexistent or invalid resource" +ERROR["DATA_IMPORT_ERROR"] = "Data import error" +ERROR["INTEGRITY_ERROR"] = "Integrity error: record can not be deleted while it is referenced by other records" +ERROR["METHOD_DISABLED"] = "Method disabled" +ERROR["NO_MATCH"] = "No matching element found in the data source" +ERROR["NOT_IMPLEMENTED"] = "Not implemented" +ERROR["NOT_PERMITTED"] = "Operation not permitted" +ERROR["PARSE_ERROR"] = "XML parse error" +ERROR["TRANSFORMATION_ERROR"] = "XSLT transformation error" +ERROR["UNAUTHORISED"] = "Not Authorized" +ERROR["VALIDATION_ERROR"] = "Validation error" + +# To get included in +s3.stylesheets = [] +s3.external_stylesheets = [] +# To get included at the end of +s3.scripts = [] +s3.scripts_modules = [] +s3.js_global = [] +s3.jquery_ready = [] + +# ----------------------------------------------------------------------------- +# Languages + +s3.l10n_languages = settings.get_L10n_languages() + +# Default strings are in US English +T.current_languages = ("en", "en-us") +# Check if user has selected a specific language +if get_vars._language: + language = get_vars._language + session.s3.language = language +elif session.s3.language: + # Use the last-selected language + language = session.s3.language +elif auth.is_logged_in(): + # Use user preference + language = auth.user.language +else: + # Use system default + language = settings.get_L10n_default_language() +#else: +# # Use what browser requests (default web2py behaviour) +# T.force(T.http_accept_language) + +# IE doesn't set request.env.http_accept_language +#if language != "en": +T.force(language) + +# Store for views (e.g. Ext) +if language.find("-") == -1: + # Ext peculiarities + if language == "vi": + s3.language = "vn" + elif language == "el": + s3.language = "el_GR" + else: + s3.language = language +else: + lang_parts = language.split("-") + s3.language = "%s_%s" % (lang_parts[0], lang_parts[1].upper()) + +# List of Languages which use a Right-to-Left script (Arabic, Hebrew, Farsi, Urdu) +if language in ("ar", "prs", "ps", "ur"): + s3.rtl = True +else: + s3.rtl = False + +# ----------------------------------------------------------------------------- +# Auth + +_settings = auth.settings +_settings.lock_keys = False + +_settings.expiration = 28800 # seconds + +if settings.get_auth_openid(): + # Requires http://pypi.python.org/pypi/python-openid/ + try: + from gluon.contrib.login_methods.openid_auth import OpenIDAuth + openid_login_form = OpenIDAuth(auth) + from gluon.contrib.login_methods.extended_login_form import ExtendedLoginForm + _settings.login_form = ExtendedLoginForm(auth, openid_login_form, + signals=["oid", "janrain_nonce"]) + except ImportError: + session.warning = "Library support not available for OpenID" + +# Allow use of LDAP accounts for login +# NB Currently this means that change password should be disabled: +#_settings.actions_disabled.append("change_password") +# (NB These are not automatically added to PR or to Authenticated role since they enter via the login() method not register()) +#from gluon.contrib.login_methods.ldap_auth import ldap_auth +# Require even alternate login methods to register users 1st +#_settings.alternate_requires_registration = True +# Active Directory +#_settings.login_methods.append(ldap_auth(mode="ad", server="dc.domain.org", base_dn="ou=Users,dc=domain,dc=org")) +# or if not wanting local users at all (no passwords saved within DB): +#_settings.login_methods = [ldap_auth(mode="ad", server="dc.domain.org", base_dn="ou=Users,dc=domain,dc=org")] +# Domino +#_settings.login_methods.append(ldap_auth(mode="domino", server="domino.domain.org")) +# OpenLDAP +#_settings.login_methods.append(ldap_auth(server="directory.sahanafoundation.org", base_dn="ou=users,dc=sahanafoundation,dc=org")) +# Allow use of Email accounts for login +#_settings.login_methods.append(email_auth("smtp.gmail.com:587", "@gmail.com")) + +# Require captcha verification for registration +#auth.settings.captcha = RECAPTCHA(request, public_key="PUBLIC_KEY", private_key="PRIVATE_KEY") +# Require Email Verification +_settings.registration_requires_verification = settings.get_auth_registration_requires_verification() +_settings.on_failed_authorization = URL(c="default", f="user", + args="not_authorized") +_settings.reset_password_requires_verification = True +_settings.verify_email_next = URL(c="default", f="index") + +# Require Admin approval for self-registered users +_settings.registration_requires_approval = settings.get_auth_registration_requires_approval() + +# We don't wish to clutter the groups list with 1 per user. +_settings.create_user_groups = False +# We need to allow basic logins for Webservices +_settings.allow_basic_login = True + +_settings.logout_onlogout = s3_auth_on_logout +_settings.login_onaccept = s3_auth_on_login +# Now read in auth.login() to avoid setting unneccesarily in every request +#_settings.login_next = settings.get_auth_login_next() +if settings.has_module("vol") and \ + settings.get_auth_registration_volunteer(): + _settings.register_next = URL(c="vol", f="person") + +# Languages available in User Profiles +#if len(s3.l10n_languages) > 1: +# _settings.table_user.language.requires = s3base.IS_ISO639_2_LANGUAGE_CODE(sort = True, +# translate = True, +# zero = None, +# ) +#else: +# field = _settings.table_user.language +# field.default = s3.l10n_languages.keys()[0] +# field.readable = False +# field.writable = False + +_settings.lock_keys = True + +# ----------------------------------------------------------------------------- +# Mail + +# These settings could be made configurable as part of the Messaging Module +# - however also need to be used by Auth (order issues) +sender = settings.get_mail_sender() +if sender: + mail.settings.sender = sender + mail.settings.server = settings.get_mail_server() + mail.settings.tls = settings.get_mail_server_tls() + mail_server_login = settings.get_mail_server_login() + if mail_server_login: + mail.settings.login = mail_server_login + # Email settings for registration verification and approval + _settings.mailer = mail + +# ----------------------------------------------------------------------------- +# Session + +# Custom Notifications +response.error = session.error +response.confirmation = session.confirmation +response.information = session.information +response.warning = session.warning +session.error = [] +session.confirmation = [] +session.information = [] +session.warning = [] + +# Shortcuts for system role IDs, see modules/s3aaa.py/AuthS3 +#system_roles = auth.get_system_roles() +#ADMIN = system_roles.ADMIN +#AUTHENTICATED = system_roles.AUTHENTICATED +#ANONYMOUS = system_roles.ANONYMOUS +#EDITOR = system_roles.EDITOR +#MAP_ADMIN = system_roles.MAP_ADMIN +#ORG_ADMIN = system_roles.ORG_ADMIN +#ORG_GROUP_ADMIN = system_roles.ORG_GROUP_ADMIN + +if s3.debug: + # Add the developer toolbar from modules/s3/s3utils.py + s3.toolbar = s3base.s3_dev_toolbar + +# ----------------------------------------------------------------------------- +# CRUD + +s3_formstyle = settings.get_ui_formstyle() +s3_formstyle_read = settings.get_ui_formstyle_read() +s3_formstyle_mobile = s3_formstyle +submit_button = T("Save") +s3_crud = s3.crud +s3_crud.formstyle = s3_formstyle +s3_crud.formstyle_read = s3_formstyle_read +s3_crud.submit_button = submit_button +# Optional class for Submit buttons +#s3_crud.submit_style = "submit-button" +s3_crud.confirm_delete = T("Do you really want to delete these records?") +s3_crud.archive_not_delete = settings.get_security_archive_not_delete() +s3_crud.navigate_away_confirm = settings.get_ui_navigate_away_confirm() + +# Content Type Headers, default is application/xml for XML formats +# and text/x-json for JSON formats, other content types must be +# specified here: +s3.content_type = Storage( + tc = "application/atom+xml", # TableCast feeds + rss = "application/rss+xml", # RSS + georss = "application/rss+xml", # GeoRSS + kml = "application/vnd.google-earth.kml+xml", # KML +) + +# JSON Formats +s3.json_formats = ["geojson", "s3json"] + +# CSV Formats +s3.csv_formats = ["hrf", "s3csv"] + +# Datatables default number of rows per page +s3.ROWSPERPAGE = 20 + +# Valid Extensions for Image Upload fields +s3.IMAGE_EXTENSIONS = ["png", "PNG", "jpg", "JPG", "jpeg", "JPEG"] + +# Default CRUD strings +s3.crud_strings = Storage( + label_create = T("Add Record"), + title_display = T("Record Details"), + title_list = T("Records"), + title_update = T("Edit Record"), + title_map = T("Map"), + title_report = T("Report"), + label_list_button = T("List Records"), + label_delete_button = T("Delete Record"), + msg_record_created = T("Record added"), + msg_record_modified = T("Record updated"), + msg_record_deleted = T("Record deleted"), + msg_list_empty = T("No Records currently available"), + msg_match = T("Matching Records"), + msg_no_match = T("No Matching Records"), + ) + +# END ========================================================================= diff --git a/lxc-apps/share/install/sahana_data/SHARE/config.py b/lxc-apps/share/install/sahana_data/SHARE/config.py new file mode 100644 index 0000000..54365f3 --- /dev/null +++ b/lxc-apps/share/install/sahana_data/SHARE/config.py @@ -0,0 +1,2551 @@ +# -*- coding: utf-8 -*- + +from collections import OrderedDict + +from gluon import current, URL +from gluon.storage import Storage + +from s3 import S3ReportRepresent + +def config(settings): + """ + Settings for the SHARE Template + + Migration Issues: + req_need.name is now length=64 + (SHARE can use req_need.description instead if the notnull=True removed) + """ + + T = current.T + + settings.base.system_name = T("Humanitarian Country Team (HCT) Relief and Rehabilitation System") + settings.base.system_name_short = T("SHARE") + + # UI Settings + settings.ui.menu_logo = URL(c = "static", + f = "themes", + args = ["SHARE", "img", "sharemenulogo.png"], + ) + + # PrePopulate data + settings.base.prepopulate.append("SHARE") + settings.base.prepopulate_demo.append("SHARE/Demo") + + # Theme (folder to use for views/layout.html) + settings.base.theme = "SHARE" + + # Authentication settings + # Should users be allowed to register themselves? + #settings.security.self_registration = False + # Do new users need to verify their email address? + #settings.auth.registration_requires_verification = True + # Do new users need to be approved by an administrator prior to being able to login? + #settings.auth.registration_requires_approval = True + settings.auth.registration_requests_organisation = True + #settings.auth.registration_organisation_required = True + #settings.auth.registration_requests_site = True + + settings.auth.registration_link_user_to = {"staff": T("Staff"), + "volunteer": T("Volunteer"), + #"member": T("Member") + } + + def registration_organisation_default(default): + auth = current.auth + has_role = auth.s3_has_role + if has_role("ORG_ADMIN") and not has_role("ADMIN"): + return auth.user.organisation_id + else: + return default + + settings.auth.registration_organisation_default = registration_organisation_default + + # Approval emails get sent to all admins + settings.mail.approver = "ADMIN" + + # Restrict the Location Selector to just certain countries + # NB This can also be over-ridden for specific contexts later + # e.g. Activities filtered to those of parent Project + #settings.gis.countries = ("US",) + # Uncomment to display the Map Legend as a floating DIV + settings.gis.legend = "float" + # Uncomment to Disable the Postcode selector in the LocationSelector + #settings.gis.postcode_selector = False # @ToDo: Vary by country (include in the gis_config!) + # Uncomment to show the Print control: + # http://eden.sahanafoundation.org/wiki/UserGuidelines/Admin/MapPrinting + #settings.gis.print_button = True + + # GeoNames username + settings.gis.geonames_username = "trendspotter" + settings.gis.simplify_tolerance = 0 + + # L10n settings + # Number formats (defaults to ISO 31-0) + # Decimal separator for numbers (defaults to ,) + settings.L10n.decimal_separator = "." + # Thousands separator for numbers (defaults to space) + settings.L10n.thousands_separator = "," + + # Security Policy + # http://eden.sahanafoundation.org/wiki/S3AAA#System-widePolicy + # 1: Simple (default): Global as Reader, Authenticated as Editor + # 2: Editor role required for Update/Delete, unless record owned by session + # 3: Apply Controller ACLs + # 4: Apply both Controller & Function ACLs + # 5: Apply Controller, Function & Table ACLs + # 6: Apply Controller, Function, Table ACLs and Entity Realm + # 7: Apply Controller, Function, Table ACLs and Entity Realm + Hierarchy + # 8: Apply Controller, Function, Table ACLs, Entity Realm + Hierarchy and Delegations + + settings.security.policy = 6 # Controller, Function, Table ACLs and Entity Realm + + # Don't show version info on About page + settings.security.version_info = False + + # UI Settings + settings.ui.datatables_responsive = False + settings.ui.datatables_double_scroll = True + + # Disable permalink + settings.ui.label_permalink = None + + # Default summary pages: + settings.ui.summary = ({"common": True, + "name": "add", + "widgets": [{"method": "create"}], + }, + {"name": "table", + "label": "Table", + "widgets": [{"method": "datatable"}], + }, + ) + + # ------------------------------------------------------------------------- + # CMS Content Management + # + settings.cms.bookmarks = True + settings.cms.richtext = True + settings.cms.show_tags = True + + # ------------------------------------------------------------------------- + # Events + settings.event.label = "Disaster" + # Uncomment to not use Incidents under Events + settings.event.incident = False + + # ------------------------------------------------------------------------- + # Messaging + settings.msg.parser = "SAMBRO" # for parse_tweet + + # ------------------------------------------------------------------------- + # Organisations + settings.org.sector = True + # Show Organisation Types in the rheader + settings.org.organisation_type_rheader = True + + # ------------------------------------------------------------------------- + # Projects + # Don't use Beneficiaries + settings.project.activity_beneficiaries = False + # Don't use Item Catalog for Distributions + settings.project.activity_items = False + settings.project.activity_sectors = True + # Links to Filtered Components for Donors & Partners + settings.project.organisation_roles = { + 1: T("Organization"), + 2: T("Implementing Partner"), + 3: T("Donor"), + } + + # ------------------------------------------------------------------------- + # Supply + # Disable the use of Multiple Item Catalogs + settings.supply.catalog_multi = False + + # ------------------------------------------------------------------------- + # Comment/uncomment modules here to disable/enable them + # Modules menu is defined in modules/eden/menu.py + settings.modules = OrderedDict([ + # Core modules which shouldn't be disabled + ("default", Storage( + name_nice = "Home", + restricted = False, # Use ACLs to control access to this module + access = None, # All Users (inc Anonymous) can see this module in the default menu & access the controller + module_type = None # This item is not shown in the menu + )), + ("admin", Storage( + name_nice = "Administration", + #description = "Site Administration", + restricted = True, + access = "|1|", # Only Administrators can see this module in the default menu & access the controller + module_type = None # This item is handled separately for the menu + )), + ("appadmin", Storage( + name_nice = "Administration", + #description = "Site Administration", + restricted = True, + module_type = None # No Menu + )), + ("errors", Storage( + name_nice = "Ticket Viewer", + #description = "Needed for Breadcrumbs", + restricted = False, + module_type = None # No Menu + )), + ("setup", Storage( + name_nice = T("Setup"), + #description = "WebSetup", + restricted = True, + access = "|1|", # Only Administrators can see this module in the default menu & access the controller + module_type = None # No Menu + )), + ("sync", Storage( + name_nice = "Synchronization", + #description = "Synchronization", + restricted = True, + access = "|1|", # Only Administrators can see this module in the default menu & access the controller + module_type = None # This item is handled separately for the menu + )), + #("tour", Storage( + # name_nice = T("Guided Tour Functionality"), + # module_type = None, + #)), + ("translate", Storage( + name_nice = T("Translation Functionality"), + #description = "Selective translation of strings based on module.", + module_type = None, + )), + ("gis", Storage( + name_nice = "Map", + #description = "Situation Awareness & Geospatial Analysis", + restricted = True, + module_type = 6, # 6th item in the menu + )), + ("pr", Storage( + name_nice = "Person Registry", + #description = "Central point to record details on People", + restricted = True, + access = "|1|", # Only Administrators can see this module in the default menu (access to controller is possible to all still) + module_type = 10 + )), + ("org", Storage( + name_nice = "Organizations", + #description = 'Lists "who is doing what & where". Allows relief agencies to coordinate their activities', + restricted = True, + module_type = 1 + )), + ("hrm", Storage( + name_nice = "Staff", + #description = "Human Resources Management", + restricted = True, + module_type = 2, + )), + ("vol", Storage( + name_nice = T("Volunteers"), + #description = "Human Resources Management", + restricted = True, + module_type = 2, + )), + ("cms", Storage( + name_nice = "Content Management", + #description = "Content Management System", + restricted = True, + module_type = 10, + )), + ("doc", Storage( + name_nice = "Documents", + #description = "A library of digital resources, such as photos, documents and reports", + restricted = True, + module_type = 10, + )), + ("msg", Storage( + name_nice = "Messaging", + #description = "Sends & Receives Alerts via Email & SMS", + restricted = True, + # The user-visible functionality of this module isn't normally required. Rather it's main purpose is to be accessed from other modules. + module_type = None, + )), + ("supply", Storage( + name_nice = "Supply Chain Management", + #description = "Used within Inventory Management, Request Management and Asset Management", + restricted = True, + module_type = None, # Not displayed + )), + ("inv", Storage( + name_nice = T("Warehouses"), + #description = "Receiving and Sending Items", + restricted = True, + module_type = 4 + )), + ("asset", Storage( + name_nice = "Assets", + #description = "Recording and Assigning Assets", + restricted = True, + module_type = 5, + )), + # Vehicle depends on Assets + #("vehicle", Storage( + # name_nice = "Vehicles", + # #description = "Manage Vehicles", + # restricted = True, + # module_type = 10, + #)), + ("req", Storage( + name_nice = "Requests", + #description = "Manage requests for supplies, assets, staff or other resources. Matches against Inventories where supplies are requested.", + restricted = True, + module_type = 10, + )), + # Used just for Statuses + ("project", Storage( + name_nice = "Tasks", + #description = "Tracking of Projects, Activities and Tasks", + restricted = True, + module_type = 2 + )), + #("cr", Storage( + # name_nice = T("Shelters"), + # #description = "Tracks the location, capacity and breakdown of victims in Shelters", + # restricted = True, + # module_type = 10 + #)), + #("hms", Storage( + # name_nice = T("Hospitals"), + # #description = "Helps to monitor status of hospitals", + # restricted = True, + # module_type = 10 + #)), + #("dvr", Storage( + # name_nice = T("Disaster Victim Registry"), + # #description = "Allow affected individuals & households to register to receive compensation and distributions", + # restricted = True, + # module_type = 10, + #)), + ("event", Storage( + name_nice = "Events", + #description = "Activate Events (e.g. from Scenario templates) for allocation of appropriate Resources (Human, Assets & Facilities).", + restricted = True, + module_type = 10, + )), + #("transport", Storage( + # name_nice = T("Transport"), + # restricted = True, + # module_type = 10, + #)), + ("stats", Storage( + name_nice = T("Statistics"), + #description = "Manages statistics", + restricted = True, + module_type = None, + )), + ]) + + # ------------------------------------------------------------------------- + def customise_cms_post_resource(r, tablename): + + import json + + from s3 import S3SQLCustomForm, S3SQLInlineComponent, \ + S3DateFilter, S3OptionsFilter, S3TextFilter, \ + s3_fieldmethod + + s3db = current.s3db + + # Virtual Field for Comments + # - otherwise need to do per-record DB calls inside cms_post_list_layout + # as direct list_fields come in unsorted, so can't match up to records + ctable = s3db.cms_comment + + def comment_as_json(row): + body = row["cms_comment.body"] + if not body: + return None + return json.dumps({"body": body, + "created_by": row["cms_comment.created_by"], + "created_on": row["cms_comment.created_on"].isoformat(), + }) + + ctable.json_dump = s3_fieldmethod("json_dump", + comment_as_json, + # over-ride the default represent of s3_unicode to prevent HTML being rendered too early + #represent = lambda v: v, + ) + + s3db.configure("cms_comment", + extra_fields = ["body", + "created_by", + "created_on", + ], + # Doesn't seem to have any impact + #orderby = "cms_comment.created_on asc", + ) + + + table = s3db.cms_post + table.priority.readable = table.priority.writable = True + #table.series_id.readable = table.series_id.writable = True + #table.status_id.readable = table.status_id.writable = True + + crud_form = S3SQLCustomForm(#(T("Type"), "series_id"), + (T("Priority"), "priority"), + #(T("Status"), "status_id"), + (T("Title"), "title"), + (T("Text"), "body"), + #(T("Location"), "location_id"), + # Tags are added client-side + S3SQLInlineComponent("document", + name = "file", + label = T("Files"), + fields = [("", "file"), + #"comments", + ], + ), + ) + + date_filter = S3DateFilter("date", + # If we introduce an end_date on Posts: + #["date", "end_date"], + label = "", + #hide_time = True, + #slider = True, + clear_text = "X", + ) + date_filter.input_labels = {"ge": "Start Time/Date", "le": "End Time/Date"} + + filter_widgets = [S3TextFilter(["body", + ], + #formstyle = text_filter_formstyle, + label = T("Search"), + _placeholder = T("Enter search term…"), + ), + #S3OptionsFilter("series_id", + # label = "", + # noneSelectedText = "Type", # T() added in widget + # no_opts = "", + # ), + S3OptionsFilter("priority", + label = "", + noneSelectedText = "Priority", # T() added in widget + no_opts = "", + ), + #S3OptionsFilter("status_id", + # label = "", + # noneSelectedText = "Status", # T() added in widget + # no_opts = "", + # ), + S3OptionsFilter("created_by$organisation_id", + label = "", + noneSelectedText = "Source", # T() added in widget + no_opts = "", + ), + S3OptionsFilter("tag_post.tag_id", + label = "", + noneSelectedText = "Tag", # T() added in widget + no_opts = "", + ), + date_filter, + ] + + from templates.SHARE.controllers import cms_post_list_layout + + s3db.configure("cms_post", + create_next = URL(args = [1, "post", "datalist"]), + crud_form = crud_form, + filter_widgets = filter_widgets, + list_fields = [#"series_id", + "priority", + #"status_id", + "date", + "title", + "body", + "created_by", + "tag.name", + "document.file", + "comment.json_dump", + ], + list_layout = cms_post_list_layout, + ) + + settings.customise_cms_post_resource = customise_cms_post_resource + + # ------------------------------------------------------------------------- + def customise_event_sitrep_resource(r, tablename): + + from s3 import s3_comments_widget + + table = current.s3db.event_sitrep + + table.name.widget = lambda f, v: \ + s3_comments_widget(f, v, _placeholder = "Please provide a brief summary of the Situational Update you are submitting.") + + table.comments.comment = None + table.comments.widget = lambda f, v: \ + s3_comments_widget(f, v, _placeholder = "e.g. Any additional relevant information.") + + current.response.s3.crud_strings[tablename] = Storage( + label_create = T("Add Situational Update"), + title_display = T("HCT Activity and Response Report"), + title_list = T("Situational Updates"), + title_update = T("Edit Situational Update"), + title_upload = T("Import Situational Updates"), + label_list_button = T("List Situational Updates"), + label_delete_button = T("Delete Situational Update"), + msg_record_created = T("Situational Update added"), + msg_record_modified = T("Situational Update updated"), + msg_record_deleted = T("Situational Update deleted"), + msg_list_empty = T("No Situational Updates currently registered")) + + settings.customise_event_sitrep_resource = customise_event_sitrep_resource + # ----------------------------------------------------------------------------- + def customise_event_sitrep_controller(**attr): + + s3 = current.response.s3 + + # Custom postp + standard_postp = s3.postp + def postp(r, output): + # Call standard postp + if callable(standard_postp): + output = standard_postp(r, output) + + if r.interactive: + # Mark this page to have differential CSS + s3.jquery_ready.append('''$('main').attr('id', 'sitrep')''') + + return output + s3.postp = postp + + # Extend the width of the Summary column + dt_col_widths = {0: 110, + 1: 95, + 2: 100, + 3: 100, + 4: 100, + 5: 100, + 6: 110, + 7: 80, + 8: 90, + 9: 300, + 10: 110, + } + if "dtargs" in attr: + attr["dtargs"]["dt_col_widths"] = dt_col_widths + else: + attr["dtargs"] = {"dt_col_widths": dt_col_widths, + } + + return attr + + settings.customise_event_sitrep_controller = customise_event_sitrep_controller + # ----------------------------------------------------------------------------- + def customise_gis_location_controller(**attr): + + s3 = current.response.s3 + + # Custom prep + standard_prep = s3.prep + def custom_prep(r): + + # Call standard prep + if callable(standard_prep): + result = standard_prep(r) + else: + result = True + + if r.representation == "json": + + # Special filter vars to find child locations while + # including the parent location in the JSON result: + # adm => the parent location ID + # l => the target Lx level for child locations + get_vars = r.get_vars + adm = get_vars.get("adm") + if adm: + from s3 import FS + resource = r.resource + + # Filter for children of adm + query = FS("parent") == adm + + # Restrict children to a certain Lx level + level = get_vars.get("l") + if level: + q = FS("level") == level + query = (query & q) if query else q + + # Always include adm + query = (FS("id") == adm) | query + resource.add_filter(query) + + # Push the parent to top of the list + alpha-sort + table = resource.table + resource.configure(orderby = (table.level, table.name)) + + return result + s3.prep = custom_prep + + return attr + + settings.customise_gis_location_controller = customise_gis_location_controller + # ------------------------------------------------------------------------- + def customise_msg_twitter_channel_resource(r, tablename): + + s3db = current.s3db + def onaccept(form): + # Normal onaccept + s3db.msg_channel_onaccept(form) + _id = form.vars.id + db = current.db + table = db.msg_twitter_channel + channel_id = db(table.id == _id).select(table.channel_id, + limitby=(0, 1)).first().channel_id + # Link to Parser + table = s3db.msg_parser + _id = table.insert(channel_id=channel_id, function_name="parse_tweet", enabled=True) + s3db.msg_parser_enable(_id) + + run_async = current.s3task.run_async + # Poll + run_async("msg_poll", args=["msg_twitter_channel", channel_id]) + + # Parse + run_async("msg_parse", args=[channel_id, "parse_tweet"]) + + s3db.configure(tablename, + create_onaccept = onaccept, + ) + + settings.customise_msg_twitter_channel_resource = customise_msg_twitter_channel_resource + + # ------------------------------------------------------------------------- + def customise_org_organisation_resource(r, tablename): + + s3db = current.s3db + + # Custom Components + s3db.add_components(tablename, + org_organisation_tag = (# Request Number + {"name": "req_number", + "joinby": "organisation_id", + "filterby": {"tag": "req_number", + }, + "multiple": False, + }, + # Vision + {"name": "vision", + "joinby": "organisation_id", + "filterby": {"tag": "vision", + }, + "multiple": False, + }, + ), + ) + + from s3 import S3SQLCustomForm, S3SQLInlineComponent, S3SQLInlineLink, s3_comments_widget + + # Individual settings for specific tag components + components_get = s3db.resource(tablename).components.get + + vision = components_get("vision") + vision.table.value.widget = s3_comments_widget + + crud_form = S3SQLCustomForm("name", + "acronym", + S3SQLInlineLink("organisation_type", + field = "organisation_type_id", + # Default 10 options just triggers which adds unnecessary complexity to a commonly-used form & commonly an early one (create Org when registering) + search = False, + label = T("Type"), + multiple = False, + widget = "multiselect", + ), + S3SQLInlineLink("sector", + columns = 4, + field = "sector_id", + label = T("Sectors"), + ), + #S3SQLInlineLink("service", + # columns = 4, + # field = "service_id", + # label = T("Services"), + # ), + "country", + "phone", + "website", + "logo", + (T("About"), "comments"), + S3SQLInlineComponent("vision", + label = T("Vision"), + fields = [("", "value")], + multiple = False, + ), + S3SQLInlineComponent("req_number", + label = T("Request Number"), + fields = [("", "value")], + multiple = False, + ), + ) + + s3db.configure(tablename, + crud_form = crud_form, + ) + + settings.customise_org_organisation_resource = customise_org_organisation_resource + + # ------------------------------------------------------------------------- + def customise_org_sector_controller(**attr): + + s3db = current.s3db + tablename = "org_sector" + + # Just 1 set of sectors / sector leads nationally + # @ToDo: Deployment Setting + #f = s3db.org_sector.location_id + #f.readable = f.writable = False + + # Custom Component for Sector Leads + s3db.add_components(tablename, + org_sector_organisation = {"name": "sector_lead", + "joinby": "sector_id", + "filterby": {"lead": True, + }, + }, + ) + + from s3 import S3SQLCustomForm, S3SQLInlineComponent + crud_form = S3SQLCustomForm("name", + "abrv", + "comments", + S3SQLInlineComponent("sector_lead", + label = T("Lead Organization(s)"), + fields = [("", "organisation_id"),], + ), + ) + + s3db.configure(tablename, + crud_form = crud_form, + list_fields = ["name", + "abrv", + (T("Lead Organization(s)"), "sector_lead.organisation_id"), + ], + ) + + return attr + + settings.customise_org_sector_controller = customise_org_sector_controller + + # ------------------------------------------------------------------------- + def customise_pr_forum_controller(**attr): + + s3db = current.s3db + s3 = current.response.s3 + + s3db.pr_forum + s3.crud_strings["pr_forum"].title_display = T("HCT Coordination Folders") + s3.dl_no_header = True + + # Comments + appname = current.request.application + s3.scripts.append("/%s/static/themes/WACOP/js/update_comments.js" % appname) + script = '''S3.wacop_comments() +S3.redraw_fns.push('wacop_comments')''' + s3.jquery_ready.append(script) + + # Tags for Updates + if s3.debug: + s3.scripts.append("/%s/static/scripts/tag-it.js" % appname) + else: + s3.scripts.append("/%s/static/scripts/tag-it.min.js" % appname) + if current.auth.s3_has_permission("update", s3db.cms_tag_post): + # @ToDo: Move the ajaxUpdateOptions into callback of getS3? + readonly = '''afterTagAdded:function(event,ui){ +if(ui.duringInitialization){return} +var post_id=$(this).attr('data-post_id') +var url=S3.Ap.concat('/cms/post/',post_id,'/add_tag/',ui.tagLabel) +$.getS3(url) +S3.search.ajaxUpdateOptions('#datalist-filter-form') +},afterTagRemoved:function(event,ui){ +var post_id=$(this).attr('data-post_id') +var url=S3.Ap.concat('/cms/post/',post_id,'/remove_tag/',ui.tagLabel) +$.getS3(url) +S3.search.ajaxUpdateOptions('#datalist-filter-form') +},''' + else: + readonly = '''readOnly:true''' + script = \ +'''S3.tagit=function(){$('.s3-tags').tagit({placeholderText:'%s',autocomplete:{source:'%s'},%s})} +S3.tagit() +S3.redraw_fns.push('tagit')''' % (T("Add tags here…"), + URL(c="cms", f="tag", + args="tag_list.json"), + readonly) + s3.jquery_ready.append(script) + + attr["rheader"] = None + attr["hide_filter"] = False + + return attr + + settings.customise_pr_forum_controller = customise_pr_forum_controller + + # ------------------------------------------------------------------------- + def req_need_commit(r, **attr): + """ + Custom method to Commit to a Need by creating an Activity Group + """ + + # Create Activity Group (Response) with values from Need + need_id = r.id + + db = current.db + s3db = current.s3db + + ntable = s3db.req_need + ntable_id = ntable.id + netable = s3db.event_event_need + left = [netable.on(netable.need_id == ntable_id), + ] + need = db(ntable_id == need_id).select(ntable.name, + ntable.location_id, + netable.event_id, + left = left, + limitby = (0, 1) + ).first() + + nttable = s3db.req_need_tag + query = (nttable.need_id == need_id) & \ + (nttable.tag.belongs(("address", "contact"))) & \ + (nttable.deleted == False) + tags = db(query).select(nttable.tag, + nttable.value, + ) + contact = address = None + for tag in tags: + if tag.tag == "address": + address = tag.value + elif tag.tag == "contact": + contact = tag.value + + nrtable = s3db.req_need_response + need_response_id = nrtable.insert(need_id = need_id, + name = need["req_need.name"], + location_id = need["req_need.location_id"], + contact = contact, + address = address, + ) + organisation_id = current.auth.user.organisation_id + if organisation_id: + s3db.req_need_response_organisation.insert(need_response_id = need_response_id, + organisation_id = organisation_id, + role = 1, + ) + + event_id = need["event_event_need.event_id"] + if event_id: + aetable = s3db.event_event_need_response + aetable.insert(need_response_id = need_response_id, + event_id = event_id, + ) + + nltable = s3db.req_need_line + query = (nltable.need_id == need_id) & \ + (nltable.deleted == False) + lines = db(query).select(nltable.id, + nltable.coarse_location_id, + nltable.location_id, + nltable.sector_id, + nltable.parameter_id, + nltable.value, + nltable.value_uncommitted, + nltable.item_category_id, + nltable.item_id, + nltable.item_pack_id, + nltable.quantity, + nltable.quantity_uncommitted, + nltable.status, + ) + if lines: + linsert = s3db.req_need_response_line.insert + for line in lines: + value_uncommitted = line.value_uncommitted + if value_uncommitted is None: + # No commitments yet so commit to all + value = line.value + else: + # Only commit to the remainder + value = value_uncommitted + quantity_uncommitted = line.quantity_uncommitted + if quantity_uncommitted is None: + # No commitments yet so commit to all + quantity = line.quantity + else: + # Only commit to the remainder + quantity = quantity_uncommitted + need_line_id = line.id + linsert(need_response_id = need_response_id, + need_line_id = need_line_id, + coarse_location_id = line.coarse_location_id, + location_id = line.location_id, + sector_id = line.sector_id, + parameter_id = line.parameter_id, + value = value, + item_category_id = line.item_category_id, + item_id = line.item_id, + item_pack_id = line.item_pack_id, + quantity = quantity, + ) + # Update Need Line status + req_need_line_status_update(need_line_id) + + # Redirect to Update + from gluon import redirect + redirect(URL(c= "req", f="need_response", + args = [need_response_id, "update"], + )) + + # ------------------------------------------------------------------------- + def req_need_line_commit(r, **attr): + """ + Custom method to Commit to a Need Line by creating an Activity + """ + + # Create Activity with values from Need Line + need_line_id = r.id + + db = current.db + s3db = current.s3db + + nltable = s3db.req_need_line + query = (nltable.id == need_line_id) + line = db(query).select(nltable.id, + nltable.need_id, + nltable.coarse_location_id, + nltable.location_id, + nltable.sector_id, + nltable.parameter_id, + nltable.value, + nltable.value_uncommitted, + nltable.item_category_id, + nltable.item_id, + nltable.item_pack_id, + nltable.quantity, + nltable.quantity_uncommitted, + nltable.status, + limitby = (0, 1) + ).first() + + need_id = line.need_id + + ntable = s3db.req_need + ntable_id = ntable.id + netable = s3db.event_event_need + left = [netable.on(netable.need_id == ntable_id), + ] + need = db(ntable_id == need_id).select(ntable.name, + ntable.location_id, + netable.event_id, + left = left, + limitby = (0, 1) + ).first() + + nttable = s3db.req_need_tag + query = (nttable.need_id == need_id) & \ + (nttable.tag.belongs(("address", "contact"))) & \ + (nttable.deleted == False) + tags = db(query).select(nttable.tag, + nttable.value, + ) + contact = address = None + for tag in tags: + if tag.tag == "address": + address = tag.value + elif tag.tag == "contact": + contact = tag.value + + nrtable = s3db.req_need_response + need_response_id = nrtable.insert(need_id = need_id, + name = need["req_need.name"], + location_id = need["req_need.location_id"], + contact = contact, + address = address, + ) + organisation_id = current.auth.user.organisation_id + if organisation_id: + s3db.req_need_response_organisation.insert(need_response_id = need_response_id, + organisation_id = organisation_id, + role = 1, + ) + + event_id = need["event_event_need.event_id"] + if event_id: + aetable = s3db.event_event_need_response + aetable.insert(need_response_id = need_response_id, + event_id = event_id, + ) + + value_uncommitted = line.value_uncommitted + if value_uncommitted is None: + # No commitments yet so commit to all + value = line.value + else: + # Only commit to the remainder + value = value_uncommitted + quantity_uncommitted = line.quantity_uncommitted + if quantity_uncommitted is None: + # No commitments yet so commit to all + quantity = line.quantity + else: + # Only commit to the remainder + quantity = quantity_uncommitted + + s3db.req_need_response_line.insert(need_response_id = need_response_id, + need_line_id = need_line_id, + coarse_location_id = line.coarse_location_id, + location_id = line.location_id, + sector_id = line.sector_id, + parameter_id = line.parameter_id, + value = value, + item_category_id = line.item_category_id, + item_id = line.item_id, + item_pack_id = line.item_pack_id, + quantity = quantity, + ) + + # Update Need Line status + req_need_line_status_update(need_line_id) + + # Redirect to Update + from gluon import redirect + redirect(URL(c= "req", f="need_response", + args = [need_response_id, "update"], + )) + + # ------------------------------------------------------------------------- + def req_need_line_status_update(need_line_id): + """ + Update the Need Line's fulfilment Status + """ + + db = current.db + s3db = current.s3db + + # Read the Line details + nltable = s3db.req_need_line + iptable = s3db.supply_item_pack + query = (nltable.id == need_line_id) + left = iptable.on(nltable.item_pack_id == iptable.id) + need_line = db(query).select(nltable.parameter_id, + nltable.value, + nltable.item_id, + nltable.quantity, + iptable.quantity, + left = left, + limitby = (0, 1) + ).first() + need_pack_qty = need_line["supply_item_pack.quantity"] + need_line = need_line["req_need_line"] + need_parameter_id = need_line.parameter_id + need_value = need_line.value or 0 + need_value_committed = 0 + need_value_reached = 0 + need_quantity = need_line.quantity + if need_quantity: + need_quantity = need_quantity * need_pack_qty + else: + need_quantity = 0 + need_item_id = need_line.item_id + need_quantity_committed = 0 + need_quantity_delivered = 0 + + # Lookup which Status means 'Cancelled' + stable = s3db.project_status + status = db(stable.name == "Cancelled").select(stable.id, + limitby = (0, 1) + ).first() + try: + CANCELLED = status.id + except AttributeError: + # Prepop not done? Name changed? + current.log.debug("'Cancelled' Status not found") + CANCELLED = 999999 + + # Read the details of all Response Lines linked to this Need Line + rltable = s3db.req_need_response_line + iptable = s3db.supply_item_pack + query = (rltable.need_line_id == need_line_id) & \ + (rltable.deleted == False) + left = iptable.on(rltable.item_pack_id == iptable.id) + response_lines = db(query).select(rltable.id, + rltable.parameter_id, + rltable.value, + rltable.value_reached, + rltable.item_id, + iptable.quantity, + rltable.quantity, + rltable.quantity_delivered, + rltable.status_id, + left = left, + ) + for line in response_lines: + pack_qty = line["supply_item_pack.quantity"] + line = line["req_need_response_line"] + if line.status_id == CANCELLED: + continue + if line.parameter_id == need_parameter_id: + value = line.value + if value: + need_value_committed += value + value_reached = line.value_reached + if value_reached: + need_value_reached += value_reached + if line.item_id == need_item_id: + quantity = line.quantity + if quantity: + need_quantity_committed += quantity * pack_qty + quantity_delivered = line.quantity_delivered + if quantity_delivered: + need_quantity_delivered += quantity_delivered * pack_qty + + # Calculate Need values & Update + value_uncommitted = max(need_value - need_value_committed, 0) + quantity_uncommitted = max(need_quantity - need_quantity_committed, 0) + if (need_quantity_delivered >= need_quantity) and (need_value_reached >= need_value): + status = 3 + elif (quantity_uncommitted <= 0) and (value_uncommitted <= 0): + status = 2 + elif (need_quantity_committed > 0) or (need_value_committed > 0): + status = 1 + else: + status = 0 + + db(nltable.id == need_line_id).update(value_committed = need_value_committed, + value_uncommitted = value_uncommitted, + value_reached = need_value_reached, + quantity_committed = need_quantity_committed, + quantity_uncommitted = quantity_uncommitted, + quantity_delivered = need_quantity_delivered, + status = status, + ) + + # ------------------------------------------------------------------------- + def req_need_postprocess(form): + """ + Set the Realm + Set the Request Number + """ + + need_id = form.vars.id + + db = current.db + s3db = current.s3db + + # Lookup Organisation + notable = s3db.req_need_organisation + org_link = db(notable.need_id == need_id).select(notable.organisation_id, + limitby = (0, 1), + ).first() + if org_link: + organisation_id = org_link.organisation_id + else: + # Create the link (form isn't doing so when readonly!) + user = current.auth.user + if user and user.organisation_id: + organisation_id = user.organisation_id + if organisation_id: + notable.insert(need_id = need_id, + organisation_id = organisation_id) + else: + # Nothing we can do! + return + else: + # Nothing we can do! + return + + # Lookup Realm + otable = s3db.org_organisation + org = db(otable.id == organisation_id).select(otable.pe_id, + limitby = (0, 1), + ).first() + realm_entity = org.pe_id + + # Set Realm + ntable = s3db.req_need + db(ntable.id == need_id).update(realm_entity = realm_entity) + nltable = s3db.req_need_line + db(nltable.need_id == need_id).update(realm_entity = realm_entity) + + if form.record: + # Update form + return + + # Lookup Request Number format + ottable = s3db.org_organisation_tag + query = (ottable.organisation_id == organisation_id) & \ + (ottable.tag == "req_number") + tag = db(query).select(ottable.value, + limitby = (0, 1), + ).first() + if not tag: + return + + # Lookup most recently-used value + nttable = s3db.req_need_tag + query = (nttable.tag == "req_number") & \ + (nttable.need_id != need_id) & \ + (nttable.need_id == notable.need_id) & \ + (notable.organisation_id == organisation_id) + + need = db(query).select(nttable.value, + limitby = (0, 1), + orderby = ~nttable.created_on, + ).first() + + # Set Request Number + if need: + new_number = int(need.value.split("-", 1)[1]) + 1 + req_number = "%s-%s" % (tag.value, str(new_number).zfill(6)) + else: + req_number = "%s-000001" % tag.value + + nttable.insert(need_id = need_id, + tag = "req_number", + value = req_number, + ) + + # ------------------------------------------------------------------------- + def customise_req_need_resource(r, tablename): + + from gluon import IS_EMPTY_OR, IS_IN_SET + + from s3 import s3_comments_widget, \ + S3LocationSelector, S3LocationDropdownWidget, \ + S3Represent, \ + S3SQLCustomForm, S3SQLInlineComponent, S3SQLInlineLink + + db = current.db + s3db = current.s3db + + table = s3db.req_need + table.name.widget = lambda f, v: \ + s3_comments_widget(f, v, _placeholder = "e.g. 400 families require drinking water in Kegalle DS Division in 1-2 days.") + + table.comments.comment = None + table.comments.widget = lambda f, v: \ + s3_comments_widget(f, v, _placeholder = "e.g. Accessibility issues, additional contacts on the ground (if any), any other relevant information.") + + # These levels/labels are for SHARE/LK + table.location_id.widget = S3LocationSelector(hide_lx = False, + levels = ("L1", "L2"), + required_levels = ("L1", "L2"), + show_map = False) + + ltable = s3db.req_need_line + f = ltable.coarse_location_id + f.label = T("Division") + # @ToDo: Option for gis_LocationRepresent which doesn't show level/parent, but supports translation + # NB cannot have the JS in link to avoid being blocked by Chrome XSS_AUDITOR + location_represent = S3Represent(lookup = "gis_location") + f.represent = location_represent + f.widget = S3LocationDropdownWidget(level="L3", blank=True) + f = ltable.location_id + f.label = T("GN") + f.represent = location_represent + f.widget = S3LocationDropdownWidget(level="L4", blank=True) + + # Custom Filtered Components + s3db.add_components(tablename, + req_need_tag = (# Address + {"name": "address", + "joinby": "need_id", + "filterby": {"tag": "address", + }, + "multiple": False, + }, + # Contact + {"name": "contact", + "joinby": "need_id", + "filterby": {"tag": "contact", + }, + "multiple": False, + }, + # Issue + {"name": "issue", + "joinby": "need_id", + "filterby": {"tag": "issue", + }, + "multiple": False, + }, + # Req Number + {"name": "req_number", + "joinby": "need_id", + "filterby": {"tag": "req_number", + }, + "multiple": False, + }, + # Original Request From + {"name": "request_from", + "joinby": "need_id", + "filterby": {"tag": "request_from", + }, + "multiple": False, + }, + # Verified + {"name": "verified", + "joinby": "need_id", + "filterby": {"tag": "verified", + }, + "multiple": False, + }, + ) + ) + + # Individual settings for specific tag components + components_get = s3db.resource(tablename).components.get + + address = components_get("address") + f = address.table.value + f.widget = s3_comments_widget + + contact = components_get("contact") + f = contact.table.value + f.widget = lambda f, v: \ + s3_comments_widget(f, v, _placeholder = "of person on the ground e.g. GA, DS") + + issue = components_get("issue") + f = issue.table.value + f.widget = lambda f, v: \ + s3_comments_widget(f, v, _placeholder = "e.g. Lack of accessibility and contaminated wells due to heavy rainfall.") + + request_from = components_get("request_from") + f = request_from.table.value + f.widget = lambda f, v: \ + s3_comments_widget(f, v, _placeholder = "Please indicate the requesting organisation/ministry.") + + verified = components_get("verified") + f = verified.table.value + f.requires = IS_EMPTY_OR(IS_IN_SET(("Y", "N"))) + f.represent = lambda v: T("yes") if v == "Y" else T("no") + from s3 import S3TagCheckboxWidget + f.widget = S3TagCheckboxWidget(on="Y", off="N") + f.default = "N" + + auth = current.auth + user = auth.user + if user and user.organisation_id: + organisation_id = user.organisation_id + else: + organisation_id = None + if auth.s3_has_role("ADMIN") or organisation_id: + f.default = "Y" + else: + f.writable = False + + if r.id and r.resource.tablename == tablename: + # Read or Update + create = False + else: + # Create + create = True + + if not create: + # Read or Update + if organisation_id: + org_readonly = True + else: + rotable = s3db.req_need_organisation + org_link = db(rotable.need_id == r.id).select(rotable.organisation_id, + limitby = (0, 1) + ).first() + if org_link: + org_readonly = True + else: + org_readonly = False + #table = s3db.req_need_item + #table.quantity.label = T("Quantity Requested") + #table.quantity_committed.readable = True + #table.quantity_uncommitted.readable = True + #table.quantity_delivered.readable = True + #need_item = S3SQLInlineComponent("need_item", + # label = T("Items Needed"), + # fields = ["item_category_id", + # "item_id", + # (T("Unit"), "item_pack_id"), + # (T("Needed within Timeframe"), "timeframe"), + # "quantity", + # "quantity_committed", + # "quantity_uncommitted", + # "quantity_delivered", + # #(T("Urgency"), "priority"), + # "comments", + # ], + # ) + #table = s3db.req_need_demographic + #table.value.label = T("Number in Need") + #table.value_committed.readable = True + #table.value_uncommitted.readable = True + #table.value_reached.readable = True + #demographic = S3SQLInlineComponent("need_demographic", + # label = T("People Affected"), + # fields = [(T("Type"), "parameter_id"), + # #(T("Needed within Timeframe"), "timeframe"), + # "value", + # "value_committed", + # "value_uncommitted", + # "value_reached", + # "comments", + # ], + # ) + #ltable.value.label = T("Number in Need") + ltable.value_committed.readable = True + ltable.value_uncommitted.readable = True + ltable.value_reached.readable = True + #ltable.quantity.label = T("Quantity Requested") + ltable.quantity_committed.readable = True + ltable.quantity_uncommitted.readable = True + ltable.quantity_delivered.readable = True + line = S3SQLInlineComponent("need_line", + label = "", + fields = ["coarse_location_id", + "location_id", + "sector_id", + (T("People affected"), "parameter_id"), + "value", + "value_committed", + (T("Number Outstanding"), "value_uncommitted"), + "value_reached", + (T("Item Category"), "item_category_id"), + "item_id", + (T("Unit"), "item_pack_id"), + (T("Item Quantity"), "quantity"), + (T("Needed within Timeframe"), "timeframe"), + "quantity_committed", + (T("Quantity Outstanding"), "quantity_uncommitted"), + "quantity_delivered", + #"comments", + ], + ) + else: + # Create + org_readonly = organisation_id is not None + #need_item = S3SQLInlineComponent("need_item", + # label = T("Items Needed"), + # fields = ["item_category_id", + # "item_id", + # (T("Unit"), "item_pack_id"), + # (T("Needed within Timeframe"), "timeframe"), + # "quantity", + # #(T("Urgency"), "priority"), + # "comments", + # ], + # ) + #demographic = S3SQLInlineComponent("need_demographic", + # label = T("People Affected"), + # fields = [(T("Type"), "parameter_id"), + # #(T("Needed within Timeframe"), "timeframe"), + # "value", + # "comments", + # ], + # ) + line = S3SQLInlineComponent("need_line", + label = "", + fields = ["coarse_location_id", + "location_id", + "sector_id", + (T("People affected"), "parameter_id"), + "value", + (T("Item Category"), "item_category_id"), + "item_id", + (T("Unit"), "item_pack_id"), + "quantity", + (T("Needed within Timeframe"), "timeframe"), + #"comments", + ], + ) + + crud_fields = [S3SQLInlineLink("event", + field = "event_id", + label = T("Disaster"), + multiple = False, + required = True, + ), + S3SQLInlineLink("organisation", + field = "organisation_id", + search = False, + label = T("Organization"), + multiple = False, + readonly = org_readonly, + required = not org_readonly, + ), + "location_id", + (T("Date entered"), "date"), + #(T("Urgency"), "priority"), + # Moved into Lines + #S3SQLInlineLink("sector", + # field = "sector_id", + # search = False, + # label = T("Sector"), + # multiple = False, + # ), + "name", + (T("Original Request From"), "request_from.value"), + (T("Issue/cause"), "issue.value"), + #demographic, + #need_item, + line, + S3SQLInlineComponent("document", + label = T("Attachment"), + fields = [("", "file")], + # multiple = True has reliability issues in at least Chrome + multiple = False, + ), + (T("Verified by government official"), "verified.value"), + (T("Contact details"), "contact.value"), + (T("Address for delivery/affected people"), "address.value"), + "comments", + ] + + from .controllers import project_ActivityRepresent + natable = s3db.req_need_activity + #f = natable.activity_id + #f.represent = project_ActivityRepresent() + natable.activity_id.represent = project_ActivityRepresent() + + if not create: + # Read or Update + req_number = components_get("req_number") + req_number.table.value.writable = False + crud_fields.insert(2, (T("Request Number"), "req_number.value")) + crud_fields.insert(-2, "status") + need_links = db(natable.need_id == r.id).select(natable.activity_id) + if need_links: + # This hides the widget from Update forms instead of just rendering read-only! + #f.writable = False + crud_fields.append(S3SQLInlineLink("activity", + field = "activity_id", + label = T("Commits"), + readonly = True, + )) + + crud_form = S3SQLCustomForm(*crud_fields, + postprocess = req_need_postprocess) + + need_line_summary = URL(c="req", f="need_line", args="summary") + + s3db.configure(tablename, + create_next = need_line_summary, + delete_next = need_line_summary, + update_next = need_line_summary, + crud_form = crud_form, + ) + + settings.customise_req_need_resource = customise_req_need_resource + + # ------------------------------------------------------------------------- + def req_need_rheader(r): + """ + Resource Header for Needs + """ + + if r.representation != "html": + # RHeaders only used in interactive views + return None + + record = r.record + if not record: + # RHeaders only used in single-record views + return None + + if r.name == "need": + # No Tabs (all done Inline) + tabs = [(T("Basic Details"), None), + #(T("Demographics"), "demographic"), + #(T("Items"), "need_item"), + #(T("Skills"), "need_skill"), + #(T("Tags"), "tag"), + ] + + from s3 import s3_rheader_tabs + rheader_tabs = s3_rheader_tabs(r, tabs) + + location_id = r.table.location_id + from gluon import DIV, TABLE, TR, TH + rheader = DIV(TABLE(TR(TH("%s: " % location_id.label), + location_id.represent(record.location_id), + )), + rheader_tabs) + + else: + # Not defined, probably using wrong rheader + rheader = None + + return rheader + + # ------------------------------------------------------------------------- + def customise_req_need_controller(**attr): + + line_id = current.request.get_vars.get("line") + if line_id: + from gluon import redirect + nltable = current.s3db.req_need_line + line = current.db(nltable.id == line_id).select(nltable.need_id, + limitby = (0, 1) + ).first() + if line: + redirect(URL(args = [line.need_id], + vars = {})) + + # Custom commit method to create an Activity Group from a Need + current.s3db.set_method("req", "need", + method = "commit", + action = req_need_commit) + + s3 = current.response.s3 + + # Custom postp + standard_postp = s3.postp + def postp(r, output): + # Call standard postp + if callable(standard_postp): + output = standard_postp(r, output) + + if r.interactive: + # Inject the javascript to handle dropdown filtering + # - normally injected through AddResourceLink, but this isn't there in Inline widget + # - we also need to turn the trigger & target into dicts + s3.scripts.append("/%s/static/themes/SHARE/js/need.js" % r.application) + + if r.id and isinstance(output, dict) and \ + current.auth.s3_has_permission("create", "project_activity"): + # Custom Button + from gluon import A + output["commit"] = A(T("Commit"), + _href = URL(args=[r.id, "commit"]), + _class = "action-btn", + #_id = "commit-btn", + ) + #s3.jquery_ready.append( +#'''S3.confirmClick('#commit-btn','%s')''' % T("Do you want to commit to this need?")) + + return output + s3.postp = postp + + attr["rheader"] = req_need_rheader + + return attr + + settings.customise_req_need_controller = customise_req_need_controller + + # ------------------------------------------------------------------------- + def homepage_stats_update(): + """ + Scheduler task to update the data files for the charts + on the homepage + """ + + from .controllers import HomepageStatistics + HomepageStatistics.update_data() + + settings.tasks.homepage_stats_update = homepage_stats_update + + def req_need_line_update_stats(r, **attr): + """ + Method to manually update the data files for the charts + on the homepage; can be run by POSTing an empty request + to req/need_line/update_stats, e.g. via: + +
+ +
+ + (this could e.g. be added to the page footer for ADMINs) + """ + + if r.http == "POST": + + if not current.auth.s3_has_role("ADMIN"): + # No, this is not open for everybody + r.unauthorized() + else: + current.s3task.run_async("settings_task", + args = ["homepage_stats_update"]) + current.session.confirmation = T("Statistics data update started") + + from gluon import redirect + redirect(URL(c="default", f="index")) + else: + r.error("405", current.ERROR.BAD_METHOD) + + # ------------------------------------------------------------------------- + def customise_req_need_line_resource(r, tablename): + + from gluon import IS_EMPTY_OR, IS_IN_SET, SPAN + + from s3 import S3Represent + + s3db = current.s3db + + current.response.s3.crud_strings["req_need_line"]["title_map"] = T("Map of Needs") + + req_status_opts = {0: SPAN(T("Uncommitted"), + _class = "req_status_none", + ), + 1: SPAN(T("Partially Committed"), + _class = "req_status_partial", + ), + 2: SPAN(T("Fully Committed"), + _class = "req_status_committed", + ), + 3: SPAN(T("Complete"), + _class = "req_status_complete", + ), + } + + table = s3db.req_need_line + + f = table.status + f.requires = IS_EMPTY_OR(IS_IN_SET(req_status_opts, zero = None)) + f.represent = S3Represent(options = req_status_opts) + + f = table.coarse_location_id + f.label = T("Division") + # @ToDo: Option for gis_LocationRepresent which doesn't show level/parent, but supports translation + # NB cannot have the JS in link to avoid being blocked by Chrome XSS_AUDITOR + location_represent = S3Represent(lookup = "gis_location") + f.represent = location_represent + f = table.location_id + # @ToDo: Option for gis_LocationRepresent which doesn't show level/parent, but supports translation + f.represent = location_represent + + if r.representation == "plain": + # Settings for Map Popups + f.label = T("GN") + + # Custom method to (manually) update homepage statistics + s3db.set_method("req", "need_line", + method = "update_stats", + action = req_need_line_update_stats, + ) + + settings.customise_req_need_line_resource = customise_req_need_line_resource + + # ------------------------------------------------------------------------- + def customise_req_need_line_controller(**attr): + + from s3 import S3OptionsFilter, S3TextFilter #, S3DateFilter, S3LocationFilter + + s3db = current.s3db + + settings.base.pdf_orientation = "Landscape" + + settings.ui.summary = (# Gets replaced in postp + # @ToDo: better performance by not including here & placing directly into the view instead + {"common": True, + "name": "add", + "widgets": [{"method": "create"}], + }, + #{"common": True, + # "name": "cms", + # "widgets": [{"method": "cms"}], + # }, + {"name": "table", + "label": "Table", + "widgets": [{"method": "datatable"}], + }, + {"name": "charts", + "label": "Report", + "widgets": [{"method": "report", + "ajax_init": True}], + }, + #{"name": "map", + # "label": "Map", + # "widgets": [{"method": "map", + # "ajax_init": True}], + # }, + ) + + # Custom Filtered Components + s3db.add_components("req_need", + req_need_tag = (# Req Number + {"name": "req_number", + "joinby": "need_id", + "filterby": {"tag": "req_number", + }, + "multiple": False, + }, + # Original Request From + {"name": "request_from", + "joinby": "need_id", + "filterby": {"tag": "request_from", + }, + "multiple": False, + }, + # Verified + {"name": "verified", + "joinby": "need_id", + "filterby": {"tag": "verified", + }, + "multiple": False, + }, + ), + ) + + s3db.add_components("req_need_response", + req_need_response_organisation = (# Agency + {"name": "agency", + "joinby": "need_response_id", + "filterby": {"role": 1, + }, + #"multiple": False, + }, + ), + ) + + filter_widgets = [S3TextFilter(["need_id$req_number.value", + "item_id$name", + # These levels are for SHARE/LK + #"location_id$L1", + "location_id$L2", + #"location_id$L3", + #"location_id$L4", + "need_id$name", + "need_id$comments", + ], + label = T("Search"), + comment = T("Search for a Need by Request Number, Item, Location, Summary or Comments"), + ), + #S3OptionsFilter("need_id$event.event_type_id", + # #hidden = True, + # ), + # @ToDo: Filter this list dynamically based on Event Type (if-used): + S3OptionsFilter("need_id$event__link.event_id"), + #S3LocationFilter("location_id", + # # These levels are for SHARE/LK + # levels = ("L2", "L3", "L4"), + # ), + S3OptionsFilter("need_id$location_id", + label = T("District"), + ), + S3OptionsFilter("need_id$organisation__link.organisation_id", + #hidden = True, + ), + S3OptionsFilter("sector_id", + #hidden = True, + ), + S3OptionsFilter("parameter_id"), + S3OptionsFilter("timeframe"), + S3OptionsFilter("item_id"), + S3OptionsFilter("status", + cols = 3, + table = False, + label = T("Status"), + ), + #S3DateFilter("date", + # ), + #S3OptionsFilter("need_id$verified.value", + # cols = 2, + # label = T("Verified"), + # #hidden = True, + # ), + ] + + s3db.configure("req_need_line", + filter_widgets = filter_widgets, + # We create a custom Create Button to create a Need not a Need Line + listadd = False, + list_fields = [(T("Status"), "status"), + (T("Orgs responding"), "need_response_line.need_response_id$agency.organisation_id"), + "need_id$date", + (T("Need entered by"), "need_id$organisation__link.organisation_id"), + (T("Original Request From"), "need_id$request_from.value"), + # These levels/Labels are for SHARE/LK + #(T("Province"), "need_id$location_id$L1"), + (T("District"), "need_id$location_id$L2"), + #(T("DS"), "location_id$L3"), + #(T("GN"), "location_id$L4"), + "sector_id", + "parameter_id", + "item_id", + "quantity", + (T("Quantity Outstanding"),"quantity_uncommitted"), + "timeframe", + (T("Request Number"), "need_id$req_number.value"), + ], + popup_url = URL(c="req", f="need", + vars = {"line": "[id]"} + ), + ) + + # Custom commit method to create an Activity from a Need Line + s3db.set_method("req", "need_line", + method = "commit", + action = req_need_line_commit) + + s3 = current.response.s3 + + s3.crud_strings["req_need_line"] = Storage( + #label_create = T("Add Needs"), + title_list = T("Needs"), + #title_display=T("Needs"), + #title_update=T("Edit Needs"), + #title_upload = T("Import Needs"), + #label_list_button = T("List Needs"), + #label_delete_button=T("Delete Needs"), + msg_record_created=T("Needs added"), + msg_record_modified=T("Needs updated"), + msg_record_deleted=T("Needs deleted"), + msg_list_empty = T("No Needs currently registered"), + ) + + # Custom postp + standard_postp = s3.postp + def postp(r, output): + # Call standard postp + if callable(standard_postp): + output = standard_postp(r, output) + + if r.interactive and r.method == "summary": + + from gluon import A, DIV + from s3 import s3_str#, S3CRUD + + auth = current.auth + + # Normal Action Buttons + #S3CRUD.action_buttons(r) + # Custom Action Buttons + deletable = current.db(auth.s3_accessible_query("delete", "req_need_line")).select(s3db.req_need_line.id) + restrict_d = [str(row.id) for row in deletable] + s3.actions = [{"label": s3_str(T("Open")), + "_class": "action-btn", + "url": URL(f="need", vars={"line": "[id]"}), + }, + {"label": s3_str(T("Delete")), + "_class": "delete-btn", + "url": URL(args=["[id]", "delete"]), + "restrict": restrict_d, + }, + ] + if auth.s3_has_permission("create", "req_need_response"): + s3.actions.append({"label": s3_str(T("Commit")), + "_class": "action-btn", + "url": URL(args=["[id]", "commit"]), + }) + + # Custom Create Button + add_btn = DIV(DIV(DIV(A(T("Add Needs"), + _class = "action-btn", + _href = URL(f="need", args="create"), + ), + _id = "list-btn-add", + ), + _class = "widget-container with-tabs", + ), + _class = "section-container", + ) + output["common"][0] = add_btn + + return output + s3.postp = postp + + return attr + + settings.customise_req_need_line_controller = customise_req_need_line_controller + + # ------------------------------------------------------------------------- + def req_need_response_postprocess(form): + """ + Set the Realm + Ensure that the Need Lines (if-any) have the correct Status + """ + + db = current.db + s3db = current.s3db + + need_response_id = form.vars.id + + # Lookup Organisation + nrotable = s3db.req_need_response_organisation + query = (nrotable.need_response_id == need_response_id) & \ + (nrotable.role == 1) + org_link = db(query).select(nrotable.organisation_id, + limitby = (0, 1), + ).first() + if not org_link: + return + + organisation_id = org_link.organisation_id + + # Lookup Realm + otable = s3db.org_organisation + org = db(otable.id == organisation_id).select(otable.pe_id, + limitby = (0, 1), + ).first() + realm_entity = org.pe_id + + # Set Realm + nrtable = s3db.req_need_response + db(nrtable.id == need_response_id).update(realm_entity = realm_entity) + rltable = s3db.req_need_response_line + db(rltable.need_response_id == need_response_id).update(realm_entity = realm_entity) + + # Lookup the Need Lines + query = (rltable.need_response_id == need_response_id) & \ + (rltable.deleted == False) + response_lines = db(query).select(rltable.need_line_id) + + for line in response_lines: + need_line_id = line.need_line_id + if need_line_id: + req_need_line_status_update(need_line_id) + + # ------------------------------------------------------------------------- + def customise_req_need_response_resource(r, tablename): + + from s3 import s3_comments_widget, \ + S3LocationDropdownWidget, S3LocationSelector, \ + S3Represent, \ + S3SQLCustomForm, S3SQLInlineComponent, S3SQLInlineLink + + #db = current.db + s3db = current.s3db + + table = s3db.req_need_response + + current.response.s3.crud_strings[tablename] = Storage( + label_create = T("Add Activities"), + title_list = T("Activities"), + title_display = T("Activities"), + title_update = T("Edit Activities"), + title_upload = T("Import Activities"), + label_list_button = T("List Activities"), + label_delete_button = T("Delete Activities"), + msg_record_created = T("Activities added"), + msg_record_modified = T("Activities updated"), + msg_record_deleted = T("Activities deleted"), + msg_list_empty = T("No Activities currently registered"), + ) + + # These levels/labels are for SHARE/LK + table.location_id.widget = S3LocationSelector(hide_lx = False, + levels = ("L1", "L2"), + required_levels = ("L1", "L2"), + show_map = False) + + ltable = s3db.req_need_response_line + f = ltable.coarse_location_id + f.label = T("Division") + # @ToDo: Option for gis_LocationRepresent which doesn't show level/parent, but supports translation + f.represent = S3Represent(lookup = "gis_location") + f.widget = S3LocationDropdownWidget(level="L3", blank=True) + f = ltable.location_id + f.label = T("GN") + # @ToDo: Option for gis_LocationRepresent which doesn't show level/parent, but supports translation + f.represent = S3Represent(lookup = "gis_location") + f.widget = S3LocationDropdownWidget(level="L4", blank=True) + + table.comments.comment = None + table.comments.widget = lambda f, v: \ + s3_comments_widget(f, v, _placeholder = "e.g. Items changed/replaced within kits, details on partial committments to a need, any other relevant information.") + + # Custom Filtered Components + s3db.add_components(tablename, + req_need_response_organisation = (# Agency + {"name": "agency", + "joinby": "need_response_id", + "filterby": {"role": 1, + }, + "multiple": False, + }, + # Partners + {"name": "partner", + "joinby": "need_response_id", + "filterby": {"role": 2, + }, + #"multiple": False, + }, + # Donors + {"name": "donor", + "joinby": "need_response_id", + "filterby": {"role": 3, + }, + #"multiple": False, + }, + ), + ) + + # Individual settings for specific tag components + components_get = s3db.resource(tablename).components.get + + donor = components_get("donor") + donor.table.organisation_id.default = None + + partner = components_get("partner") + partner.table.organisation_id.default = None + + crud_fields = [S3SQLInlineLink("event", + field = "event_id", + label = T("Disaster"), + multiple = False, + #required = True, + ), + S3SQLInlineComponent("agency", + name = "agency", + label = T("Organization"), + fields = [("", "organisation_id"),], + multiple = False, + required = True, + ), + # @ToDo: MultiSelectWidget is nicer UI but S3SQLInlineLink + # requires the link*ed* table as component (not the + # link table as applied here) and linked components + # cannot currently be filtered by link table fields + # (=> should solve the latter rather than the former) + # @ToDo: Fix Create Popups + S3SQLInlineComponent("partner", + name = "partner", + label = T("Implementing Partner"), + fields = [("", "organisation_id"),], + ), + S3SQLInlineComponent("donor", + name = "donor", + label = T("Donor"), + fields = [("", "organisation_id"),], + ), + "location_id", + (T("Date entered"), "date"), + (T("Summary of Needs/Activities"), "name"), + S3SQLInlineComponent("need_response_line", + label = "", + fields = ["coarse_location_id", + "location_id", + "sector_id", + "modality", + (T("Activity Date Planned"), "date"), + (T("Activity Date Completed"), "end_date"), + (T("Beneficiaries (Type)"), "parameter_id"), + (T("Beneficiaries Planned"), "value"), + (T("Beneficiaries Reached"), "value_reached"), + (T("Item Category"), "item_category_id"), + "item_id", + (T("Unit"), "item_pack_id"), + (T("Quantity Planned"), "quantity"), + (T("Quantity Delivered"), "quantity_delivered"), + (T("Activity Status"), "status_id"), + #"comments", + ], + #multiple = False, + ), + S3SQLInlineComponent("document", + label = T("Attachment"), + fields = [("", "file")], + # multiple = True has reliability issues in at least Chrome + multiple = False, + ), + "contact", + "address", + "comments", + ] + + if r.id and r.resource.tablename == tablename and r.record.need_id: + from .controllers import req_NeedRepresent + f = table.need_id + f.represent = req_NeedRepresent() + f.writable = False + crud_fields.insert(7, "need_id") + + # Post-process to update need status for response line changes + crud_form = S3SQLCustomForm(*crud_fields, + postprocess = req_need_response_postprocess) + # Make sure need status gets also updated when response lines are deleted + s3db.configure("req_need_response_line", + ondelete = req_need_response_line_ondelete, + ) + + need_response_line_summary = URL(c="req", f="need_response_line", args="summary") + + s3db.configure(tablename, + crud_form = crud_form, + create_next = need_response_line_summary, + delete_next = need_response_line_summary, + update_next = need_response_line_summary, + ) + + settings.customise_req_need_response_resource = customise_req_need_response_resource + + # ------------------------------------------------------------------------- + def customise_req_need_response_controller(**attr): + + line_id = current.request.get_vars.get("line") + if line_id: + from gluon import redirect + nltable = current.s3db.req_need_response_line + line = current.db(nltable.id == line_id).select(nltable.need_response_id, + limitby = (0, 1) + ).first() + if line: + redirect(URL(args = [line.need_response_id], + vars = {})) + + s3 = current.response.s3 + + # Custom postp + standard_postp = s3.postp + def postp(r, output): + # Call standard postp + if callable(standard_postp): + output = standard_postp(r, output) + + if r.interactive: + # Inject the javascript to handle dropdown filtering + # - normally injected through AddResourceLink, but this isn't there in Inline widget + # - we also need to turn the trigger & target into dicts + s3.scripts.append("/%s/static/themes/SHARE/js/need_response.js" % r.application) + + return output + s3.postp = postp + + return attr + + settings.customise_req_need_response_controller = customise_req_need_response_controller + + # ------------------------------------------------------------------------- + def req_need_response_line_ondelete(row): + """ + Ensure that the Need Line (if-any) has the correct Status + """ + + import json + + db = current.db + s3db = current.s3db + + response_line_id = row.get("id") + + # Lookup the Need Line + rltable = s3db.req_need_response_line + record = db(rltable.id == response_line_id).select(rltable.deleted_fk, + limitby = (0, 1) + ).first() + if not record: + return + + deleted_fk = json.loads(record.deleted_fk) + need_line_id = deleted_fk.get("need_line_id") + + if not need_line_id: + return + + # Check that the Need Line hasn't been deleted + nltable = s3db.req_need_line + need_line = db(nltable.id == need_line_id).select(nltable.deleted, + limitby = (0, 1) + ).first() + + if need_line and not need_line.deleted: + req_need_line_status_update(need_line_id) + + # ------------------------------------------------------------------------- + def customise_req_need_response_line_resource(r, tablename): + + from s3 import S3Represent + + s3db = current.s3db + table = s3db.req_need_response_line + + #current.response.s3.crud_strings["req_need_response_line"] = Storage(title_map = T("Map of Activities"),) + + # Settings for Map Popups + f = table.coarse_location_id + f.label = T("Division") + # @ToDo: Option for gis_LocationRepresent which doesn't show level/parent, but supports translation + f.represent = S3Represent(lookup = "gis_location") + f = table.location_id + f.label = T("GN") + # @ToDo: Option for gis_LocationRepresent which doesn't show level/parent, but supports translation + f.represent = S3Represent(lookup = "gis_location") + + s3db.configure(tablename, + ondelete = req_need_response_line_ondelete, + popup_url = URL(c="req", f="need_response", + vars = {"line": "[id]"} + ), + report_represent = NeedResponseLineReportRepresent, + ) + + settings.customise_req_need_response_line_resource = customise_req_need_response_line_resource + + # ------------------------------------------------------------------------- + def customise_req_need_response_line_controller(**attr): + + from s3 import S3OptionsFilter #, S3DateFilter, S3LocationFilter, S3TextFilter + + s3db = current.s3db + table = s3db.req_need_response_line + + settings.base.pdf_orientation = "Landscape" + + settings.ui.summary = (# Gets replaced in postp + # @ToDo: better performance by not including here & placing directly into the view instead + {"common": True, + "name": "add", + "widgets": [{"method": "create"}], + }, + #{"common": True, + # "name": "cms", + # "widgets": [{"method": "cms"}], + # }, + {"name": "table", + "label": "Table", + "widgets": [{"method": "datatable"}], + }, + {"name": "charts", + "label": "Report", + "widgets": [{"method": "report", + "ajax_init": True}], + }, + #{"name": "map", + # "label": "Map", + # "widgets": [{"method": "map", + # "ajax_init": True}], + # }, + ) + + # Custom Filtered Components + s3db.add_components("req_need_response", + req_need_response_organisation = (# Agency + {"name": "agency", + "joinby": "need_response_id", + "filterby": {"role": 1, + }, + #"multiple": False, + }, + # Partners + {"name": "partner", + "joinby": "need_response_id", + "filterby": {"role": 2, + }, + #"multiple": False, + }, + # Donors + {"name": "donor", + "joinby": "need_response_id", + "filterby": {"role": 3, + }, + #"multiple": False, + }, + ), + ) + + s3 = current.response.s3 + + # Custom prep + standard_prep = s3.prep + def prep(r): + # Call standard prep + if callable(standard_prep): + result = standard_postp(r) + else: + result = True + + filter_widgets = [S3OptionsFilter("need_response_id$agency.organisation_id", + label = T("Organization"), + ), + #S3OptionsFilter("need_response_id$event.event_type_id", + # #hidden = True, + # ), + # @ToDo: Filter this list dynamically based on Event Type (if-used): + S3OptionsFilter("need_response_id$event__link.event_id", + #hidden = True, + ), + S3OptionsFilter("sector_id"), + #S3LocationFilter("location_id", + # label = T("Location"), + # # These levels are for SHARE/LK + # levels = ("L2", "L3", "L4"), + # ), + S3OptionsFilter("need_response_id$location_id", + label = T("District"), + ), + S3OptionsFilter("need_response_id$donor.organisation_id", + label = T("Donor"), + ), + S3OptionsFilter("need_response_id$partner.organisation_id", + label = T("Partner"), + ), + S3OptionsFilter("parameter_id"), + S3OptionsFilter("item_id"), + #S3OptionsFilter("modality"), + #S3DateFilter("date"), + S3OptionsFilter("status_id", + cols = 4, + label = T("Status"), + #hidden = True, + ), + ] + + list_fields = [(T("Organization"), "need_response_id$agency.organisation_id"), + (T("Implementing Partner"), "need_response_id$partner.organisation_id"), + (T("Donor"), "need_response_id$donor.organisation_id"), + # These levels/labels are for SHARE/LK + #(T("Province"), "need_response_id$location_id$L1"), + (T("District"), "need_response_id$location_id$L2"), + "coarse_location_id", + "location_id", + (T("Sector"), "sector_id"), + (T("Item"), "item_id"), + (T("Items Planned"), "quantity"), + #(T("Items Delivered"), "quantity_delivered"), + (T("Modality"), "modality"), + (T("Beneficiaries Planned"), "value"), + (T("Beneficiaries Reached"), "value_reached"), + (T("Activity Date (Planned"), "date"), + (T("Activity Status"), "status_id"), + ] + + if r.interactive: + s3.crud_strings["req_need_response_line"] = Storage( + #label_create = T("Add Activity"), + title_list = T("Activities"), + #title_display = T("Activity"), + #title_update = T("Edit Activity"), + #title_upload = T("Import Activities"), + #label_list_button = T("List Activities"), + #label_delete_button = T("Delete Activity"), + #msg_record_created = T("Activity added"), + #msg_record_modified = T("Activity updated"), + msg_record_deleted = T("Activity deleted"), + msg_list_empty = T("No Activities currently registered"), + ) + + #if r.method == "report": + # # In report drilldown, include the (Location) after quantity_delivered + # # => Needs to be a VF as we can't read the record from within represents + # #table.quantity_delivered.represent = + # + # from s3 import S3Represent, s3_fieldmethod + # + # # @ToDo: Option for gis_LocationRepresent which doesn't show level/parent, but supports translation + # gis_represent = S3Represent(lookup = "gis_location") + # + # def quantity_delivered_w_location(row): + # quantity_delivered = row["req_need_response_line.quantity_delivered"] + # location_id = row["req_need_response_line.location_id"] + # if not location_id: + # location_id = row["req_need_response_line.coarse_location_id"] + # if not location_id: + # location_id = row["req_need_response.location_id"] + # location = gis_represent(location_id) + # return "%s (%s)" % (quantity_delivered, location) + # + # table.quantity_delivered_w_location = s3_fieldmethod("quantity_delivered_w_location", + # quantity_delivered_w_location, + # # over-ride the default represent of s3_unicode to prevent HTML being rendered too early + # #represent = lambda v: v, + # ) + # list_fields.insert(9, (T("Items Delivered"), "quantity_delivered_w_location")) + #else: + list_fields.insert(9, (T("Items Delivered"), "quantity_delivered")) + + # Exclude the Disaster column from PDF exports + if r.representation != "pdf": + list_fields.insert(0, (T("Disaster"), "need_response_id$event__link.event_id")) + + s3db.configure("req_need_response_line", + filter_widgets = filter_widgets, + # We create a custom Create Button to create a Need Response not a Need Response Line + listadd = False, + list_fields = list_fields, + ) + + return result + s3.prep = prep + + # Custom postp + standard_postp = s3.postp + def postp(r, output): + # Call standard postp + if callable(standard_postp): + output = standard_postp(r, output) + + if r.interactive and r.method == "summary": + from gluon import A, DIV + from s3 import s3_str + #from s3 import S3CRUD, s3_str + # Normal Action Buttons + #S3CRUD.action_buttons(r) + # Custom Action Buttons + auth = current.auth + deletable = current.db(auth.s3_accessible_query("delete", "req_need_response_line")).select(table.id) + restrict_d = [str(row.id) for row in deletable] + s3.actions = [{"label": s3_str(T("Open")), + "_class": "action-btn", + "url": URL(f="need_response", vars={"line": "[id]"}), + }, + {"label": s3_str(T("Delete")), + "_class": "delete-btn", + "url": URL(args=["[id]", "delete"]), + "restrict": restrict_d, + }, + ] + + # Custom Create Button + add_btn = DIV(DIV(DIV(A(T("Add Activity"), + _class = "action-btn", + _href = URL(f="need_response", args="create"), + ), + _id = "list-btn-add", + ), + _class = "widget-container with-tabs", + ), + _class = "section-container", + ) + output["common"][0] = add_btn + + return output + s3.postp = postp + + return attr + + settings.customise_req_need_response_line_controller = customise_req_need_response_line_controller + +# ============================================================================= +class NeedResponseLineReportRepresent(S3ReportRepresent): + """ + Custom representation of need response line records in + pivot table reports: + - show as location name + """ + + def __call__(self, record_ids): + """ + Represent record_ids (custom) + + @param record_ids: req_need_response_line record IDs + + @returns: a JSON-serializable dict {recordID: representation} + """ + + # Represent the location IDs + resource = current.s3db.resource("req_need_response_line", + id = record_ids, + ) + + rows = resource.select(["id", "coarse_location_id", "location_id"], + represent = True, + raw_data = True, + limit = None, + ).rows + + output = {} + for row in rows: + raw = row["_row"] + if raw["req_need_response_line.location_id"]: + repr_str = row["req_need_response_line.location_id"] + else: + # Fall back to coarse_location_id if no GN available + repr_str = row["req_need_response_line.coarse_location_id"] + output[raw["req_need_response_line.id"]] = repr_str + + return output + +# END ========================================================================= diff --git a/lxc-apps/share/install/sahana_data/masterUsers.csv b/lxc-apps/share/install/sahana_data/masterUsers.csv new file mode 100644 index 0000000..599520b --- /dev/null +++ b/lxc-apps/share/install/sahana_data/masterUsers.csv @@ -0,0 +1,2 @@ +First Name,Last Name,Email,Password,Role,Organisation +Admin,User,${SHARE_ADMIN_USER},${SHARE_ADMIN_PWD},ADMIN, diff --git a/lxc-apps/share/install/update-conf.sh b/lxc-apps/share/install/update-conf.sh new file mode 100755 index 0000000..4e20a3b --- /dev/null +++ b/lxc-apps/share/install/update-conf.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +# Volumes +SAHANA_CONF="${VOLUMES_DIR}/share/sahana_conf" + +# Variables +HTTP_HOST="${HOST}" +[ "${PORT}" != "443" ] && HTTP_HOST="${HTTP_HOST}:${PORT}" + +# Replacements +sed -i "s|\(^settings\.base\.public_url = \).*|\1\"https://${HTTP_HOST}\"|" ${SAHANA_CONF}/000_config.py +sed -i "s|\(^settings\.mail\.sender = \).*|\1\"${EMAIL}\"|" ${SAHANA_CONF}/000_config.py +sed -i "s|\(^settings\.mail\.approver = \).*|\1\"${EMAIL}\"|" ${SAHANA_CONF}/000_config.py +sed -i "s|\(^settings\.gis\.api_google = \).*|\1\"${GMAPS_API_KEY}\"|" ${SAHANA_CONF}/000_config.py diff --git a/lxc-apps/share/uninstall.sh b/lxc-apps/share/uninstall.sh new file mode 100755 index 0000000..9efbd84 --- /dev/null +++ b/lxc-apps/share/uninstall.sh @@ -0,0 +1,8 @@ +#!/bin/sh +set -ev + +# Remove persistent data +rm -rf "${VOLUMES_DIR}/share" + +# Unregister application +vmmgr unregister-app share