diff --git a/controllers/cap.py b/controllers/cap.py index 9b9e605cc2..e5c05e6cc6 100644 --- a/controllers/cap.py +++ b/controllers/cap.py @@ -216,13 +216,6 @@ def prep(r): (table.approved_on != None) resource.add_filter(filter) - #elif r.representation == "cap": - # # This is either importing from or exporting to cap format. Set both - # # postprocessing hooks so we don't have to enumerate methods. - # s3db.configure("gis_location", - # xml_post_parse = s3db.cap_gis_location_xml_post_parse, - # xml_post_render = s3db.cap_gis_location_xml_post_render, - # ) record = r.record if r.id: @@ -923,8 +916,8 @@ def postp(r, output): row_clone["template_info_id"] = row.id row_clone["is_template"] = False row_clone["effective"] = request.utcnow - row_clone["expires"] = s3db.cap_expiry_date() - row_clone["sender_name"] = s3db.cap_sender_name() + row_clone["expires"] = s3db.cap_expirydate() + row_clone["sender_name"] = s3db.cap_sendername() row_clone["web"] = settings.get_base_public_url() + \ URL(c="cap", f=fn, args=lastid) row_clone["audience"] = audience diff --git a/controllers/default.py b/controllers/default.py index b53088e783..88e9ea0e61 100755 --- a/controllers/default.py +++ b/controllers/default.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- """ Default Controllers @@ -394,7 +395,6 @@ def index(): "registered": registered, "r": None, # Required for dataTable to work "datatable_ajax_source": datatable_ajax_source, - } if get_vars.tour: @@ -464,6 +464,7 @@ def organisation(): else: from gluon.http import HTTP raise HTTP(415, ERROR.BAD_FORMAT) + return items # ----------------------------------------------------------------------------- @@ -501,10 +502,11 @@ def message(): % {"system_name": settings.get_system_name(), "email": request.vars.email} image = "email_icon.png" - return dict(title = title, - message = message, - image_src = "/%s/static/img/%s" % (appname, image) - ) + + return {"title": title, + "message": message, + "image_src": "/%s/static/img/%s" % (appname, image), + } # ----------------------------------------------------------------------------- def rapid(): @@ -518,7 +520,7 @@ def rapid(): session.s3.rapid_data_entry = val response.view = "xml.html" - return dict(item=str(session.s3.rapid_data_entry)) + return {"item": str(session.s3.rapid_data_entry)} # ----------------------------------------------------------------------------- def user(): @@ -544,6 +546,11 @@ def user(): auth_settings.profile_onaccept = auth.s3_user_profile_onaccept auth_settings.register_onvalidation = register_validation + # Check for template-specific customisations + customise = settings.customise_auth_user_controller + if customise: + customise(arg=arg) + self_registration = settings.get_security_self_registration() login_form = register_form = None @@ -559,10 +566,7 @@ def user(): auth_settings.actions_disabled = ("retrieve_password", ) - # Check for template-specific customisations - customise = settings.customise_auth_user_controller - if customise: - customise(arg=arg) + header = response.s3_user_header or "" if arg == "login": title = response.title = T("Login") @@ -573,11 +577,16 @@ def user(): login_form = form elif arg == "register": - title = response.title = T("Register") # @ToDo: move this code to /modules/s3/s3aaa.py:def register()? if not self_registration: session.error = T("Registration not permitted") redirect(URL(f="index")) + if response.title: + # Customised + title = response.title + else: + # Default + title = response.title = T("Register") form = register_form = auth.register() elif arg == "change_password": @@ -651,6 +660,7 @@ def user(): break return {"title": title, + "header": header, "form": form, "login_form": login_form, "register_form": register_form, @@ -1332,7 +1342,7 @@ def help(): response.title = T("Help") - return dict(item=item) + return {"item": item} # ----------------------------------------------------------------------------- def privacy(): @@ -1341,7 +1351,7 @@ def privacy(): _custom_view("privacy") response.title = T("Privacy") - return dict() + return {} # ----------------------------------------------------------------------------- def tos(): @@ -1350,7 +1360,7 @@ def tos(): _custom_view("tos") response.title = T("Terms of Service") - return dict() + return {} # ----------------------------------------------------------------------------- def video(): @@ -1359,7 +1369,7 @@ def video(): _custom_view("video") response.title = T("Video Tutorials") - return dict() + return {} # ----------------------------------------------------------------------------- def contact(): @@ -1433,14 +1443,14 @@ def prep(r): raise HTTP("404", "Unable to open Custom View: %s" % view) response.title = T("Contact us") - return dict() + return {} if settings.has_module("cms"): # Use CMS return s3db.cms_index("default", "contact", page_name=T("Contact Us")) # Just use default HTML View - return dict() + return {} # ----------------------------------------------------------------------------- def load_all_models(): diff --git a/controllers/pr.py b/controllers/pr.py index 51a8b440da..a9777dcee9 100755 --- a/controllers/pr.py +++ b/controllers/pr.py @@ -521,13 +521,31 @@ def prep(r): return True s3.prep = prep - return s3_rest_controller("pr", "education") + return s3_rest_controller() # ----------------------------------------------------------------------------- def education_level(): """ RESTful CRUD controller """ - return s3_rest_controller("pr", "education_level") + return s3_rest_controller() + +# ----------------------------------------------------------------------------- +def language(): + """ RESTful CRUD controller """ + + def prep(r): + if r.method in ("create", "create.popup", "update", "update.popup"): + # Coming from Profile page? + person_id = get_vars.get("~.person_id", None) + if person_id: + field = s3db.pr_language.person_id + field.default = person_id + field.readable = field.writable = False + + return True + s3.prep = prep + + return s3_rest_controller() # ----------------------------------------------------------------------------- def occupation_type(): @@ -609,7 +627,7 @@ def tooltip(): if "formfield" in request.vars: response.view = "pr/ajaxtips/%s.html" % request.vars.formfield - return dict() + return {} # ============================================================================= def filter(): diff --git a/languages/de.py b/languages/de.py index 940d345e51..2d51551a56 100644 --- a/languages/de.py +++ b/languages/de.py @@ -1827,7 +1827,9 @@ 'Enter a valid date before': 'Geben Sie zuvor eine gültiges Datum ein', 'Enter a valid email': 'Geben Sie eine gültige E-Mail-Adresse ein', 'Enter a valid future date': 'Geben Sie ein gültiges, zukünftiges Datum ein', +'Enter a value': 'Eingabe erforderlich', 'Enter an integer greater or equal to 0': 'Geben Sie eine ganze Zahl grösser oder gleich 0 ein', +'Enter from %(min)g to %(max)g characters': 'Geben Sie zwischen %(min)g und %(max)g Zeichen ein', 'Enter or scan ID': 'ID eingeben/scannen', 'Enter some characters of the ID or name to start the search, then select from the drop-down': 'Geben Sie die ersten Zeichen der ID oder des Namens ein um die Suche zu starten, und wählen Sie dann aus der Liste aus', 'Enter some characters to bring up a list of possible matches': 'Geben Sie einige Zeichen ein um eine Liste möglicher Übereinstimmungen anzuzeigen', diff --git a/languages/en-gb.py b/languages/en-gb.py index df8d7e99e6..8debbe8ce6 100644 --- a/languages/en-gb.py +++ b/languages/en-gb.py @@ -193,6 +193,7 @@ 'Sent Shipment canceled': 'Sent Shipment cancelled', 'Sent Shipment canceled and items returned to Warehouse': 'Sent Shipment cancelled and items returned to Warehouse', 'Shipping Organization': 'Shipping Organisation', +'Skills Utilized': 'Skills Utilised', 'Social Mobilization': 'Social Mobilisation', 'Specialized Hospital': 'Specialised Hospital', 'Synchronization': 'Synchronisation', diff --git a/modules/s3/s3aaa.py b/modules/s3/s3aaa.py index c7698c9ab4..0914e12545 100644 --- a/modules/s3/s3aaa.py +++ b/modules/s3/s3aaa.py @@ -341,6 +341,7 @@ def define_tables(self, migrate=True, fake_migrate=False): Field("image", "upload", length = current.MAX_FILENAME_LENGTH, ), + Field("consent"), S3MetaFields.uuid(), S3MetaFields.created_on(), S3MetaFields.modified_on(), @@ -1155,13 +1156,13 @@ def register(self, @return: a registration form """ + T = current.T db = current.db settings = self.settings messages = self.messages request = current.request session = current.session deployment_settings = current.deployment_settings - T = current.T # Customise the resource customise = deployment_settings.customise_resource("auth_user") @@ -1245,6 +1246,7 @@ def register(self, ) # Add an opt in clause to receive emails depending on the deployment settings + # @ToDo: Replace with Consent Tracking if deployment_settings.get_auth_opt_in_to_email(): field_id = "%s_opt_in" % utablename comment = DIV(DIV(_class="tooltip", @@ -1329,6 +1331,7 @@ def register(self, field_id + SQLFORM.ID_ROW_SUFFIX, ) + # @ToDo: Replace with Consent Tracking if deployment_settings.get_auth_terms_of_service(): field_id = "%s_tos" % utablename label = T("I agree to the %(terms_of_service)s") % \ @@ -1948,7 +1951,7 @@ def s3_import_prep(self, data): Lookups Pseudo-reference Integer fields from Names e.g.: - auth_membership.pe_id from organisation.name= + auth_membership.pe_id from org_organisation.name= """ db = current.db @@ -2102,9 +2105,9 @@ def org_lookup(org_full): # Replace string with pe_id element.text = new_value # Store in case we get called again with same value - looked_up[pe_tablename][pe_value] = dict(pe_id=new_value, - id=str(record_id), - ) + looked_up[pe_tablename][pe_value] = {"pe_id": new_value, + "id": str(record_id), + } # No longer required since we can use references in the import CSV # Organisations @@ -2317,6 +2320,11 @@ def s3_user_register_onaccept(self, form): if mobile: record["mobile"] = mobile + # Store Consent Question Response + consent = form_vars.consent + if consent: + record["consent"] = consent + # Insert the profile picture image = form_vars.image if image != None and hasattr(image, "file"): @@ -2561,6 +2569,10 @@ def s3_approve_user(self, user, password=None): self.s3_link_user(user) + # Track consent + if deployment_settings.get_auth_consent_tracking(): + s3db.auth_Consent.register_consent(user_id) + if current.response.s3.bulk is True: # Non-interactive imports should stop here user_email = db(utable.id == user_id).select(utable.email, @@ -2684,8 +2696,6 @@ def s3_link_to_person(self, ttable = s3db.auth_user_temp ptable = s3db.pr_person ctable = s3db.pr_contact - atable = s3db.pr_address - gctable = s3db.gis_config ltable = s3db.pr_person_user # Organisation becomes the realm entity of the person record @@ -2698,7 +2708,8 @@ def s3_link_to_person(self, left = [ltable.on(ltable.user_id == utable.id), ptable.on(ptable.pe_id == ltable.pe_id), - ttable.on(utable.id == ttable.user_id)] + ttable.on(utable.id == ttable.user_id), + ] if user is not None: if not isinstance(user, (list, tuple)): @@ -2824,7 +2835,7 @@ def s3_link_to_person(self, person = None # Users own their person records - owner = Storage(owned_by_user=user.id) + owner = Storage(owned_by_user = user.id) if person: other = db(ltable.pe_id == person.pe_id).select(ltable.id, @@ -2834,6 +2845,7 @@ def s3_link_to_person(self, # Match found, and it isn't linked to another user account # => link to this person record (+update it) pe_id = person.pe_id + person_id = person.id # Get the realm entity realm_entity = self.get_realm_entity(ptable, person) @@ -2852,16 +2864,22 @@ def s3_link_to_person(self, db(ctable.pe_id == pe_id).update(**owner) # Assign ownership of the Address record(s) + atable = s3db.pr_address db(atable.pe_id == pe_id).update(**owner) + # Assign ownership of the Details record + dtable = s3db.pr_person_details + db(dtable.person_id == person_id).update(**owner) + # Assign ownership of the GIS Config record(s) + gctable = s3db.gis_config db(gctable.pe_id == pe_id).update(**owner) # Set pe_id if this is the current user if self.user and self.user.id == user.id: self.user.pe_id = pe_id - person_ids.append(person.id) + person_ids.append(person_id) else: # There is no match or it is linked to another user account @@ -2883,7 +2901,7 @@ def s3_link_to_person(self, if person_id: # Update the super-entities - person = Storage(id=person_id) + person = Storage(id = person_id) s3db.update_super(ptable, person) pe_id = person.pe_id @@ -2960,11 +2978,12 @@ def s3_link_to_person(self, if image: # and hasattr(image, "file"): itable = s3db.pr_image url = URL(c="default", f="download", args=image) - itable.insert(pe_id=pe_id, - profile=True, - image=image, + itable.insert(pe_id = pe_id, + profile = True, + image = image, url = url, - description=current.T("Profile Picture")) + description = current.T("Profile Picture"), + ) # Set pe_id if this is the current user if self.user and self.user.id == user.id: @@ -5083,6 +5102,7 @@ def s3_set_record_owner(self, "pr_contact", "pr_address", "pr_contact_emergency", + "pr_person_details", "pr_physical_description", "pr_group_membership", "pr_image", diff --git a/modules/s3/s3organizer.py b/modules/s3/s3organizer.py index 5911ec454d..d313b87824 100644 --- a/modules/s3/s3organizer.py +++ b/modules/s3/s3organizer.py @@ -892,6 +892,12 @@ def inject_script(widget_id, options): options["locale"] = language inject.insert(-1, "fullcalendar/locale/%s" % l10n_file) + # Choose icon set (other than default FA4) + icon_set = current.deployment_settings.get_ui_icons() + if icon_set == "font-awesome3": + options["refreshIconClass"] = "icon-refresh" + options["calendarIconClass"] = "icon-calendar" + # Inject scripts for path in inject: script = "/%s/static/scripts/%s" % (appname, path) diff --git a/modules/s3/s3profile.py b/modules/s3/s3profile.py index 0b90cfc827..2fc35f7df1 100644 --- a/modules/s3/s3profile.py +++ b/modules/s3/s3profile.py @@ -232,6 +232,8 @@ def profile(self, r, **attr): w = self._map(r, widget, widgets, **attr) elif w_type == "report": w = self._report(r, widget, **attr) + elif w_type == "organizer": + w = self._organizer(r, widget, **attr) elif w_type == "custom": w = self._custom(r, widget, **attr) else: @@ -618,7 +620,7 @@ def default_orderby(): if s3.dataTable_pageLength: display_length = s3.dataTable_pageLength else: - display_length = widget.get("pagesize", 10) + display_length = widget_get("pagesize", 10) dtargs["dt_lengthMenu"] = [[10, 25, 50, -1], [10, 25, 50, s3_str(current.T("All"))] ] @@ -656,6 +658,8 @@ def default_orderby(): "msg_no_match") empty = DIV(empty_str, _class="empty") + dtargs["dt_searching"] = widget_get("dt_searching", "true") + dtargs["dt_pagination"] = dt_pagination dtargs["dt_pageLength"] = display_length # @todo: fix base URL (make configurable?) to fix export options @@ -672,13 +676,13 @@ def default_orderby(): datatable = dt.html(totalrows, displayrows, - id=list_id, + id = list_id, **dtargs) if dt.data: - empty.update(_style="display:none") + empty.update(_style = "display:none") else: - datatable.update(_style="display:none") + datatable.update(_style = "display:none") contents = DIV(datatable, empty, _class="dt-contents") # Link for create-popup @@ -705,11 +709,14 @@ def default_orderby(): # Render the widget output = DIV(create_popup, - H4(icon, label, - _class="profile-sub-header"), + H4(icon, + label, + _class = "profile-sub-header", + ), DIV(contents, - _class="card-holder"), - _class=_class, + _class = "card-holder", + ), + _class = _class, ) return output @@ -1071,6 +1078,135 @@ def _report(self, r, widget, **attr): return output + # ------------------------------------------------------------------------- + def _organizer(self, r, widget, **attr): + """ + Generate an Organizer widget + + @param r: the S3Request instance + @param widget: the widget configuration (a dict) + @param attr: controller attributes for the request + """ + + from .s3organizer import S3Organizer, S3OrganizerWidget + + widget_get = widget.get + + # Card holder label and icon + profile_label = widget_get("label", "") + if profile_label and isinstance(profile_label, basestring): + profile_label = current.T(profile_label) + icon = widget_get("icon", "") + if icon: + icon = ICON(icon) + _class = self._lookup_class(r, widget) + + # Get base URL + # - we use an explicit URL here (resource native or component) because + # the create-popup requires it anyway, so we can pass the organizer + # Ajax lookups through it as well + # - when accessing the target table as component, remember to also + # specify "master" and "component" (see further down) + base_url = widget_get("url") + if not base_url: + return DIV(H4(icon, profile_label, _class="profile-sub-header"), + DIV(DIV("Error: missing widget URL", _class="error"), + _class="card-holder", + ), + _class = _class, + ) + + # Construct Ajax URL from base URL + parsed = base_url.split("?") + parsed[0] += "/organize.json" + ajax_url = "?".join(parsed) + + # Get the target resource (customised+filtered) + tablename = widget_get("tablename", None) + resource = current.s3db.resource(tablename) + r.customise_resource(tablename) + + # Parse the resource organizer config + config = S3Organizer.parse_config(resource) + + # Generate organizer config for this resource + table = resource.table + permitted = current.auth.s3_has_permission + + start = config["start"] + end = config["end"] + + resource_config = { + "ajaxURL": ajax_url, + "useTime": config.get("use_time"), + "baseURL": base_url, + "labelCreate": s3_str(self.crud_string(tablename, "label_create")), + "insertable": resource.get_config("insertable", True) and \ + permitted("create", table), + "editable": resource.get_config("editable", True) and \ + permitted("update", table), + "startEditable": start.field and start.field.writable, + "durationEditable": end and end.field and end.field.writable, + "deletable": resource.get_config("deletable", True) and \ + permitted("delete", table), + "start": start.selector if start else None, + "end": end.selector if end else None, + } + + # Description Labels + labels = [] + for rfield in config["description"]: + label = rfield.label + if label is not None: + label = s3_str(label) + labels.append((rfield.colname, label)) + resource_config["columns"] = labels + + # Colors + color = config.get("color") + if color: + resource_config["color"] = color.colname + resource_config["colors"] = config.get("colors") + + # Use the widget-index to create a unique ID + widget_id = "profile-organizer-%s-%s" % (tablename, widget["index"]) + + # Generate form key + import uuid + formkey = uuid.uuid4().get_hex() + + # Determine the formname (see also S3Organizer.formname) + master = widget_get("master") + component = widget_get("component") + if master and component: + # Override default formname when accessing the target + # table as a component of another resource: + # - master: "master_tablename/master_record_id" + # - component: "component_alias" + formname = "%s/%s/organizer" % (master, component) + else: + # Use default formname + formname = "%s/organizer" % tablename + + # Store form key in session + session = current.session + keyname = "_formkey[%s]" % formname + session[keyname] = session.get(keyname, [])[-9:] + [formkey] + + # Instantiate Organizer Widget + organizer = S3OrganizerWidget([resource_config]) + contents = organizer.html(widget_id = widget_id, + formkey = formkey, + ) + + # Render the widget + output = DIV(H4(icon, profile_label, _class="profile-sub-header"), + DIV(contents, _class="card-holder profile-organizer"), + _class = _class, + ) + + return output + # ------------------------------------------------------------------------- @staticmethod def _lookup_class(r, widget): @@ -1087,7 +1223,7 @@ def _lookup_class(r, widget): widget_cols = widget.get("colspan", 1) span = int(12 / page_cols) * widget_cols - formstyle = current.deployment_settings.ui.get("formstyle", "default") + #formstyle = current.deployment_settings.ui.get("formstyle", "default") if current.deployment_settings.ui.get("formstyle") == "bootstrap": # Bootstrap return "profile-widget span%s" % span @@ -1165,6 +1301,11 @@ def _create_popup(r, widget, list_id, resource, context, numrows): # Indicate that popup comes from profile (and which) url_vars.profile = r.tablename + # Add a var to allow special cutomise rules + create_var = widget_get("create_var") + if create_var: + url_vars[create_var] = 1 + # CRUD string label_create = widget_get("label_create", None) # Activate if-required @@ -1189,15 +1330,15 @@ def _create_popup(r, widget, list_id, resource, context, numrows): elif current.deployment_settings.ui.formstyle == "bootstrap": # Bootstrap-style action icon create = A(ICON("plus-sign", _class="small-add"), - _href=add_url, - _class="s3_modal", - _title=label_create, + _href = add_url, + _class = "s3_modal", + _title = label_create, ) else: # Standard action button create = A(label_create, - _href=add_url, - _class="action-btn profile-add-btn s3_modal", + _href = add_url, + _class = "action-btn profile-add-btn s3_modal", ) if widget_get("type") == "datalist": @@ -1207,19 +1348,21 @@ def _create_popup(r, widget, list_id, resource, context, numrows): multiple = widget_get("multiple", True) if not multiple and hasattr(create, "update"): if numrows: - create.update(_style="display:none") + create.update(_style = "display:none") else: - create.update(_style="display:block") + create.update(_style = "display:block") # Script to hide/unhide the create-button on Ajax # list updates - createid = create["_id"] - if not createid: - createid = "%s-add-button" % list_id - create.update(_id=createid) + create_id = create["_id"] + if not create_id: + create_id = "%s-add-button" % list_id + create.update(_id = create_id) script = \ '''$('#%(list_id)s').on('listUpdate',function(){ -$('#%(createid)s').css({display:$(this).datalist('getTotalItems')?'none':'block'}) -})''' % dict(list_id=list_id, createid=createid) +$('#%(create_id)s').css({display:$(this).datalist('getTotalItems')?'none':'block'}) +})''' % {"list_id": list_id, + "create_id": create_id, + } current.response.s3.jquery_ready.append(script) return create diff --git a/modules/s3/s3validators.py b/modules/s3/s3validators.py index 5f0370d563..624aa5a35e 100644 --- a/modules/s3/s3validators.py +++ b/modules/s3/s3validators.py @@ -2824,7 +2824,7 @@ def language_codes(): ("so", "Somali"), #("son", "Songhai languages"), #("sot", "Sotho, Southern"), - ("st", "Sotho, Southern"), + ("st", "Sotho, Southern"), # Sesotho #("spa", "Spanish; Castilian"), ("es", "Spanish; Castilian"), #("srd", "Sardinian"), diff --git a/modules/s3/s3widgets.py b/modules/s3/s3widgets.py index d8f9ebd1b0..db1da28105 100644 --- a/modules/s3/s3widgets.py +++ b/modules/s3/s3widgets.py @@ -5009,7 +5009,8 @@ def __call__(self, field, value, **attributes): ) # ------------------------------------------------------------------------- - def _labels(self, levels, country=None): + @staticmethod + def _labels(levels, country=None): """ Extract the hierarchy labels @@ -5060,24 +5061,27 @@ def _labels(self, levels, country=None): for level in levels: if level == "L0": continue - labels[level] = d[int(level[1:])] = s3_unicode(T(row[level])) + label = row[level] + label = s3_str(T(label)) if label else level + labels[level] = d[int(level[1:])] = label else: row = rows.first() d = compact["d"] = {} for level in levels: if level == "L0": continue - d[int(level[1:])] = s3_unicode(T(row[level])) + d[int(level[1:])] = s3_str(T(row[level])) return labels, compact # ------------------------------------------------------------------------- - def _locations(self, - levels, + @staticmethod + def _locations(levels, values, default_bounds = None, lowest_lx = None, - config = None): + config = None, + ): """ Build initial location dict (to populate Lx dropdowns) @@ -5315,11 +5319,8 @@ def _locations(self, return location_dict # ------------------------------------------------------------------------- - def _layout(self, - components, - map_icon=None, - formstyle=None, - inline=False): + @staticmethod + def _layout(components, map_icon=None, formstyle=None, inline=False): """ Overall layout for visible components diff --git a/modules/s3cfg.py b/modules/s3cfg.py index 4e0c36e8e9..345f7a7ffb 100644 --- a/modules/s3cfg.py +++ b/modules/s3cfg.py @@ -3065,11 +3065,11 @@ def get_cap_identifier_oid(self): # Else fallback to the default OID return self.cap.get("identifier_oid", "") - def get_cap_expire_offset(self): + def get_cap_info_effective_period(self): """ - Offset period for expiration + The period (in days) after which alert info segments expire """ - return self.cap.get("expire_offset", 2) + return self.cap.get("info_effective_period", 2) def get_cap_codes(self): """ diff --git a/modules/s3db/auth.py b/modules/s3db/auth.py index 8bb985fae5..8213a055d4 100644 --- a/modules/s3db/auth.py +++ b/modules/s3db/auth.py @@ -805,6 +805,43 @@ def track(cls, person_id, value): return record_ids + # ------------------------------------------------------------------------- + @classmethod + def register_consent(cls, user_id): + """ + Track consent responses given during user self-registration + + @param user_id: the auth_user ID + """ + + db = current.db + s3db = current.s3db + + ltable = s3db.pr_person_user + ptable = s3db.pr_person + + # Look up the person ID + join = ptable.on(ptable.pe_id == ltable.pe_id) + person = db(ltable.user_id == user_id).select(ptable.id, + join = join, + limitby = (0, 1), + ).first() + if person: + person_id = person.id + + # Look up the consent response from temp user record + ttable = s3db.auth_user_temp + row = db(ttable.user_id == user_id).select(ttable.id, + ttable.consent, + limitby = (0, 1), + ).first() + if row and row.consent: + # Track consent + cls.track(person_id, row.consent) + + # Reset consent response in temp user record + row.update_record(consent=None) + # ------------------------------------------------------------------------- @classmethod def verify(cls, record_id): @@ -951,6 +988,8 @@ def consent_query(cls, table, code, field=None): @param code: the processing type code to check @param field: the field in the table referencing pr_person.id + @returns: Query + @example: consent = s3db.auth_Consent() query = consent.consent_query(table, "PIDSHARE") & (table.deleted == False) @@ -980,16 +1019,51 @@ def consent_query(cls, table, code, field=None): # ------------------------------------------------------------------------- @classmethod - def consent_filter(cls, selector, code): + def consent_filter(cls, code, selector=None): """ Filter resource for records where the person identified by selector has consented to a certain type of data processing. - useful to limit REST methods that require consent + + @param code: the processing type code to check + @param selector: a field selector (string) that references + pr_person.id; if not specified pr_person is + assumed to be the master resource + + @returns: S3ResourceQuery + + @example: + consent = s3db.auth_Consent + resource.add_filter(consent.consent_filter("PIDSHARE", "~.person_id")) + + NB only one consent filter can be used for the same resource; + if multiple consent options must be checked and/or multiple + person_id references apply independently, then either aliased + auth_consent components can be used to construct a filter, or + the query must be split (the latter typically performs better). + Ideally, however, the consent decision for a single operation + should not be complex or second-guessing. """ - # TODO implement this - pass + option_ids = cls.get_consent_options(code) + today = current.request.utcnow.date() + + # Construct sub-selectors + if selector and selector not in ("id", "~.id"): + consent = "%s$person_id:auth_consent" % selector + else: + # Assume pr_person is master + consent = "person_id:auth_consent" + option_id = FS("%s.option_id" % consent) + expires_on = FS("%s.expires_on" % consent) + consenting = FS("%s.consenting" % consent) + + query = (option_id.belongs(option_ids)) & \ + ((expires_on == None) | (expires_on > today)) & \ + (consenting == True) + + return query # ============================================================================= def auth_user_options_get_osm(pe_id): diff --git a/modules/s3db/cap.py b/modules/s3db/cap.py index c5a26390f8..36cfeacfc2 100644 --- a/modules/s3db/cap.py +++ b/modules/s3db/cap.py @@ -27,25 +27,25 @@ OTHER DEALINGS IN THE SOFTWARE. """ -__all__ = ("get_cap_options", - "S3CAPModel", - "S3CAPHistoryModel", - "S3CAPAlertingAuthorityModel", - "S3CAPAreaNameModel", - "S3CAPMessageModel", +__all__ = ("CAPAlertModel", + "CAPInfoModel", + "CAPAreaModel", + "CAPResourceModel", + "CAPWarningPriorityModel", + "CAPHistoryModel", + "CAPAlertingAuthorityModel", + "CAPMessageModel", "cap_alert_is_template", "cap_rheader", "cap_history_rheader", "cap_alert_list_layout", - "add_area_from_template", "cap_expirydate", + "cap_sendername", "cap_ImportAlert", "cap_AssignArea", "cap_AreaRepresent", "cap_CloneAlert", "cap_AlertProfileWidget", - #"cap_gis_location_xml_post_parse", - #"cap_gis_location_xml_post_render", ) import os @@ -64,20 +64,29 @@ # ============================================================================= def get_cap_options(): + """ + Common categories of the CAP model + + @returns: dict of dicts {type: {key: label, ...}, ...} + """ + + T = current.T + + cap_options = {} # List of Incident Categories -- copied from irs module <-- # @ToDo: Switch to using event_incident_type # - # The keys are based on the Canadian ems.incident hierarchy, with a - # few extra general versions added to 'other' - # The values are meant for end-users, so can be customised as-required + # - Keys are based on the Canadian ems.incident hierarchy, with a + # few extra general versions added to 'other' + # - Values are meant for end-users, so can be customised as-required + # - Entries can be hidden from user view in the controller. + # - Additional sets of 'translations' can be added to the tuples. + # # NB It is important that the meaning of these entries is not changed - # as otherwise this hurts our ability to do synchronisation - # Entries can be hidden from user view in the controller. - # Additional sets of 'translations' can be added to the tuples. - T = current.T - cap_options = {} - cap_options["cap_incident_type_opts"] = { + # as otherwise this hurts our ability to do synchronisation + # + cap_options["incident_types"] = { "accident": T("Accident"), "animalHealth.animalDieOff": T("Animal Die Off"), "animalHealth.animalFeed": T("Animal Feed"), @@ -212,7 +221,7 @@ def get_cap_options(): } # CAP alert Status Code (status) - cap_options["cap_alert_status_code_opts"] = OrderedDict([ + cap_options["alert_status"] = OrderedDict([ ("Actual", T("Actual - actionable by all targeted recipients")), ("Exercise", T("Exercise - only for designated participants (decribed in note)")), ("System", T("System - for internal functions")), @@ -221,9 +230,9 @@ def get_cap_options(): ]) # CAP alert message type (msgType) - # NB AllClear is not in msgType as of CAP 1.2, but they target to move it to - # msgType instead of responseType in CAP 2.0 - cap_options["cap_alert_msg_type_code_opts"] = OrderedDict([ + # NB AllClear is not in msgType as of CAP 1.2, but they target to move it + # to msgType instead of responseType in CAP 2.0 + cap_options["msg_types"] = OrderedDict([ ("Alert", T("Alert: Initial information requiring attention by targeted recipients")), ("Update", T("Update: Update and supercede earlier message(s)")), ("Cancel", T("Cancel: Cancel earlier message(s)")), @@ -233,14 +242,14 @@ def get_cap_options(): ]) # CAP alert scope - cap_options["cap_alert_scope_code_opts"] = OrderedDict([ + cap_options["scopes"] = OrderedDict([ ("Public", T("Public - unrestricted audiences")), ("Restricted", T("Restricted - to users with a known operational requirement (described in restriction)")), ("Private", T("Private - only to specified addresses (mentioned as recipients)")) ]) # CAP info categories - cap_options["cap_info_category_opts"] = OrderedDict([ + cap_options["categories"] = OrderedDict([ ("Geo", T("Geo - Geophysical (inc. landslide)")), ("Met", T("Met - Meteorological (inc. flood)")), ("Safety", T("Safety - General emergency and public safety")), @@ -256,7 +265,7 @@ def get_cap_options(): ]) # CAP info response type - cap_options["cap_info_response_type_opts"] = OrderedDict([ + cap_options["response_types"] = OrderedDict([ ("Shelter", T("Shelter - Take shelter in place or per instruction")), ("Evacuate", T("Evacuate - Relocate as instructed in the instruction")), ("Prepare", T("Prepare - Make preparations per the instruction")), @@ -269,7 +278,7 @@ def get_cap_options(): ]) # CAP info urgency - cap_options["cap_info_urgency_opts"] = OrderedDict([ + cap_options["urgency"] = OrderedDict([ ("Immediate", T("Immediate - Response action should be taken immediately")), ("Expected", T("Expected - Response action should be taken soon (within next hour)")), ("Future", T("Future - Responsive action should be taken in the near future")), @@ -278,7 +287,7 @@ def get_cap_options(): ]) # CAP info severity - cap_options["cap_info_severity_opts"] = OrderedDict([ + cap_options["severity"] = OrderedDict([ ("Extreme", T("Extreme - Extraordinary threat to life or property")), ("Severe", T("Severe - Significant threat to life or property")), ("Moderate", T("Moderate - Possible threat to life or property")), @@ -287,7 +296,7 @@ def get_cap_options(): ]) # CAP info certainty - cap_options["cap_info_certainty_opts"] = OrderedDict([ + cap_options["certainty"] = OrderedDict([ ("Observed", T("Observed: determined to have occurred or to be ongoing")), ("Likely", T("Likely (p > ~50%)")), ("Possible", T("Possible but not likely (p <= ~50%)")), @@ -298,62 +307,53 @@ def get_cap_options(): return cap_options # ============================================================================= -class S3CAPModel(S3Model): - """ - CAP: Common Alerting Protocol - - this module is a non-functional stub - - http://eden.sahanafoundation.org/wiki/BluePrint/Messaging#CAP - """ +class CAPAlertModel(S3Model): + """ Model for the cap:alert container object """ names = ("cap_alert", + "cap_alert_ack", "cap_alert_id", "cap_alert_represent", - "cap_alert_approve", - "cap_warning_priority", - "cap_info", - "cap_info_represent", - "cap_info_parameter", - "cap_resource", - "cap_area", - "cap_area_id", - "cap_area_represent", - "cap_area_location", - "cap_area_tag", - "cap_info_category_opts", - "cap_expiry_date", - "cap_sender_name", - "cap_template_represent", - "cap_alert_ack", + "cap_alert_onapprove", ) def model(self): T = current.T + db = current.db settings = current.deployment_settings - - cap_options = get_cap_options() - add_components = self.add_components - configure = self.configure crud_strings = current.response.s3.crud_strings + define_table = self.define_table set_method = self.set_method - #UNKNOWN_OPT = current.messages.UNKNOWN_OPT + cap_options = get_cap_options() + + incident_types = cap_options["incident_types"] + status_opts = cap_options["alert_status"] + msg_types = cap_options["msg_types"] + scopes = cap_options["scopes"] + + # --------------------------------------------------------------------- + # Alerts + # tablename = "cap_alert" define_table(tablename, Field("is_template", "boolean", + default = False, readable = False, writable = True, ), - self.event_type_id(#empty = False, - ondelete = "SET NULL", - label = T("Event Type"), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Event Type of the alert message"), - T("Event Type is classification of event."))), - script = ''' + self.event_type_id( + ondelete = "SET NULL", + label = T("Event Type"), + comment = DIV(_class="tooltip", + _title="%s|%s" % (T("Event Type of the alert message"), + T("Event Type is classification of event."), + ), + ), + script = ''' $.filterOptionsS3({ 'trigger':'event_type_id', 'target':'template_id', @@ -362,17 +362,17 @@ def model(self): 'fncRepresent': function(record,PrepResult){return record.template_title}, 'optional': true, 'lookupURL': S3.Ap.concat('/cap/template.json?~.event_type_id=') -})''' - ), +})''', + ), Field("template_id", "reference cap_alert", label = T("Template"), ondelete = "SET NULL", - represent = self.cap_template_represent, + represent = self.template_represent, requires = IS_EMPTY_OR( IS_ONE_OF(db, "cap_alert.id", - self.cap_template_represent, - filterby="is_template", - filter_opts=(True,) + self.template_represent, + filterby = "is_template", + filter_opts = (True, ), )), comment = T("Apply a template"), ), @@ -380,7 +380,9 @@ def model(self): label = T("Template Title"), comment = DIV(_class="tooltip", _title="%s|%s" % (T("Template Title"), - T("Title for the template, to indicate to which event this template is related to"))), + T("Title for the template, to indicate to which event this template is related to"), + ), + ), ), Field("template_settings", "text", default = "{}", @@ -391,41 +393,49 @@ def model(self): label = T("Identifier"), requires = [IS_LENGTH(255), IS_MATCH(OIDPATTERN, - error_message=T("Cannot be empty and Must not include spaces, commas, or restricted characters (< and &)."), + error_message = T("Cannot be empty and Must not include spaces, commas, or restricted characters (< and &)."), ), ], # Dont Allow to change the identifier - writable = False, readable = False, - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("A unique identifier of the alert message"), - T("A number or string uniquely identifying this message, assigned by the sender. Must not include spaces, commas or restricted characters (< and &)."))), + writable = False, + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("A unique identifier of the alert message"), + T("A number or string uniquely identifying this message, assigned by the sender. Must not include spaces, commas or restricted characters (< and &)."), + ), + ), ), # @ToDo: Switch to using event_incident_type_id Field("incidents", "list:string", label = T("Incidents"), - represent = S3Represent(options = cap_options["cap_incident_type_opts"], - multiple = True), + represent = S3Represent(options = incident_types, + multiple = True, + ), requires = IS_EMPTY_OR( - IS_IN_SET(cap_options["cap_incident_type_opts"], + IS_IN_SET(incident_types, multiple = True, sort = True, )), widget = S3MultiSelectWidget(selectedList = 10), comment = DIV(_class="tooltip", _title="%s|%s" % (T("A list of incident(s) referenced by the alert message"), - T("Used to collate multiple messages referring to different aspects of the same incident. If multiple incident identifiers are referenced, they SHALL be separated by whitespace. Incident names including whitespace SHALL be surrounded by double-quotes."))), + T("Used to collate multiple messages referring to different aspects of the same incident. If multiple incident identifiers are referenced, they SHALL be separated by whitespace. Incident names including whitespace SHALL be surrounded by double-quotes."), + ), + ), ), Field("sender", label = T("Sender"), default = self.generate_sender, + requires = IS_MATCH(OIDPATTERN, + error_message=T("Cannot be empty and Must not include spaces, commas, or restricted characters (< and &)."), + ), readable = False, writable = False, - requires = IS_MATCH(OIDPATTERN, - error_message=T("Cannot be empty and Must not include spaces, commas, or restricted characters (< and &).")), comment = DIV(_class="tooltip", _title="%s|%s" % (T("The identifier of the sender of the alert message"), - T("This is guaranteed by assigner to be unique globally; e.g., may be based on an Internet domain name. Must not include spaces, commas or restricted characters (< and &)."))), + T("This is guaranteed by assigner to be unique globally; e.g., may be based on an Internet domain name. Must not include spaces, commas or restricted characters (< and &)."), + ), + ), ), s3_datetime("sent", default = "now", @@ -434,22 +444,24 @@ def model(self): Field("status", default = "Draft", label = T("Status"), - represent = S3Represent(options = cap_options["cap_alert_status_code_opts"], - ), - requires = IS_IN_SET(cap_options["cap_alert_status_code_opts"]), + represent = S3Represent(options=status_opts), + requires = IS_IN_SET(status_opts), comment = DIV(_class="tooltip", _title="%s|%s" % (T("Denotes the appropriate handling of the alert message"), - T("See options."))), + T("See options."), + ), + ), ), Field("msg_type", label = T("Message Type"), default = "Alert", - #represent = S3Represent(options = cap_options["cap_alert_msg_type_code_opts"], - # ), - requires = IS_IN_SET(cap_options["cap_alert_msg_type_code_opts"]), + #represent = S3Represent(options=msg_types), + requires = IS_IN_SET(msg_types), comment = DIV(_class="tooltip", _title="%s|%s" % (T("The nature of the alert message"), - T("See options."))), + T("See options."), + ), + ), ), Field("source", label = T("Source"), @@ -458,21 +470,27 @@ def model(self): writable = False, comment = DIV(_class="tooltip", _title="%s|%s" % (T("The text identifying the source of the alert message"), - T("The particular source of this alert; e.g., an operator or a specific device."))), + T("The particular source of this alert; e.g., an operator or a specific device."), + ), + ), ), Field("scope", label = T("Scope"), - requires = IS_EMPTY_OR(IS_IN_SET(cap_options["cap_alert_scope_code_opts"])), + requires = IS_EMPTY_OR(IS_IN_SET(scopes)), comment = DIV(_class="tooltip", _title="%s|%s" % (T("Denotes the intended distribution of the alert message"), - T("Who is this alert for?"))), + T("Who is this alert for?"), + ), + ), ), # Text describing the restriction for scope=restricted Field("restriction", "text", label = T("Restriction"), comment = DIV(_class="tooltip", _title="%s|%s" % (T("The text describing the rule for limiting distribution of the restricted alert message"), - T("Used when scope is 'Restricted'."))), + T("Used when scope is 'Restricted'."), + ), + ), ), Field("addresses", "list:string", label = T("Recipients"), @@ -489,56 +507,98 @@ def model(self): widget = S3MultiSelectWidget(), comment = DIV(_class="tooltip", _title="%s|%s" % (T("The group listing of intended recipients of the alert message"), - T("Required when scope is 'Private', optional when scope is 'Public' or 'Restricted'. Each recipient shall be identified by an identifier or an address."))), + T("Required when scope is 'Private', optional when scope is 'Public' or 'Restricted'. Each recipient shall be identified by an identifier or an address."), + ), + ), ), Field("codes", "list:string", default = settings.get_cap_codes(), label = T("Codes"), - represent = self.list_string_represent, + represent = list_string_represent, comment = DIV(_class="tooltip", _title="%s|%s" % (T("Codes for special handling of the message"), - T("Any user-defined flags or special codes used to flag the alert message for special handling."))), + T("Any user-defined flags or special codes used to flag the alert message for special handling."), + ), + ), ), Field("note", "text", label = T("Note"), comment = DIV(_class="tooltip", _title="%s|%s" % (T("The text describing the purpose or significance of the alert message"), - T("The message note is primarily intended for use with status 'Exercise' and message type 'Error'"))), + T("The message note is primarily intended for use with status 'Exercise' and message type 'Error'"), + ), + ), ), - # text data type because as the number of referenced alert - # goes on increasing, it could very easily be more than 512 chars + # Alert references: + # - using "text" field type because alert references could be + # many and thus easily exceed the limit for string fields + # - @todo: consider to use a list:reference instead Field("reference", "text", label = T("Reference"), writable = False, readable = False, + # @todo: implement a suitable representation (if using list:reference) #represent = S3Represent(lookup = tablename, # fields = ["msg_type", "sent", "sender"], # field_sep = " - ", # multiple = True, # ), + # @todo: implement a widget to select from existing alerts + #widget = S3ReferenceWidget(table, + # one_to_many = True, + # allow_create = False, + # ), comment = DIV(_class="tooltip", _title="%s|%s" % (T("The group listing identifying earlier message(s) referenced by the alert message"), - T("The extended message identifier(s) (in the form sender,identifier,sent) of an earlier CAP message or messages referenced by this one."))), - # @ToDo: This should not be manually entered, - # needs a widget - #widget = S3ReferenceWidget(table, - # one_to_many=True, - # allow_create=False), + T("The extended message identifier(s) (in the form sender,identifier,sent) of an earlier CAP message or messages referenced by this one."), + ), + ), ), - # approved_on field for recording when the alert was approved + # To record when the alert was approved s3_datetime("approved_on", readable = False, writable = False, ), - # To separate the external CAP Alert + # To distinguish internally vs. externally generated alerts: Field("external", "boolean", - # This is set to true if interactive in prep + # Set to False in controller for interactive input: default = True, readable = False, writable = False, ), *s3_meta_fields()) + # Components + self.add_components(tablename, + cap_area = "alert_id", + cap_area_location = {"name": "location", + "joinby": "alert_id", + }, + cap_area_tag = {"name": "tag", + "joinby": "alert_id", + }, + cap_info = "alert_id", + cap_info_parameter = "alert_id", + cap_resource = "alert_id", + ) + + # Resource-specific REST Methods + set_method("cap", "alert", + method = "import_feed", + action = cap_ImportAlert, + ) + + set_method("cap", "alert", + method = "assign", + action = cap_AssignArea, + ) + + set_method("cap", "alert", + method = "clone", + action = cap_CloneAlert, + ) + + # List Fields list_fields = ["event_type_id", "msg_type", (T("Sent"), "sent"), @@ -546,31 +606,7 @@ def model(self): "info.sender_name", ] - notify_fields = [(T("Identifier"), "identifier"), - (T("Date"), "sent"), - (T("Status"), "status"), - (T("Message Type"), "msg_type"), - (T("Source"), "source"), - (T("Scope"), "scope"), - (T("Restriction"), "restriction"), - (T("Category"), "info.category"), - (T("Event"), "info.event_type_id"), - (T("Response type"), "info.response_type"), - (T("Priority"), "info.priority"), - (T("Urgency"), "info.urgency"), - (T("Severity"), "info.severity"), - (T("Certainty"), "info.certainty"), - (T("Effective"), "info.effective"), - (T("Expires at"), "info.expires"), - (T("Sender's name"), "info.sender_name"), - (T("Headline"), "info.headline"), - (T("Description"), "info.description"), - (T("Instruction"), "info.instruction"), - (T("Contact information"), "info.contact"), - (T("URL"), "info.web"), - (T("Area Description"), "area.name"), - ] - + # Filter Widgets filter_widgets = [ S3TextFilter(["identifier", "sender", @@ -583,7 +619,7 @@ def model(self): ), S3OptionsFilter("info.category", label = T("Category"), - options = cap_options["cap_info_category_opts"], + options = cap_options["categories"], hidden = True, ), S3OptionsFilter("info.event_type_id", @@ -606,53 +642,56 @@ def model(self): ), ] - configure(tablename, - context = {"location": "location.location_id", - }, - create_onaccept = self.cap_alert_create_onaccept, - deduplicate = S3Duplicate(primary = ("identifier", ), - secondary = ("sender", ), - ), - filter_widgets = filter_widgets, - list_fields = list_fields, - list_layout = cap_alert_list_layout, - #list_orderby = "cap_alert.sent desc", - # Required Fields - mark_required = ("scope",), - notify_fields = notify_fields, - onapprove = self.cap_alert_approve, - onvalidation = self.cap_alert_onvalidation, - # Order with most recent Alert first - orderby = "cap_alert.sent desc", - ) + # Fields to include in Notifications + notify_fields = [(T("Identifier"), "identifier"), + (T("Date"), "sent"), + (T("Status"), "status"), + (T("Message Type"), "msg_type"), + (T("Source"), "source"), + (T("Scope"), "scope"), + (T("Restriction"), "restriction"), + (T("Category"), "info.category"), + (T("Event"), "info.event_type_id"), + (T("Response type"), "info.response_type"), + (T("Priority"), "info.priority"), + (T("Urgency"), "info.urgency"), + (T("Severity"), "info.severity"), + (T("Certainty"), "info.certainty"), + (T("Effective"), "info.effective"), + (T("Expires at"), "info.expires"), + (T("Sender's name"), "info.sender_name"), + (T("Headline"), "info.headline"), + (T("Description"), "info.description"), + (T("Instruction"), "info.instruction"), + (T("Contact information"), "info.contact"), + (T("URL"), "info.web"), + (T("Area Description"), "area.name"), + ] - # Components - add_components(tablename, - cap_area = "alert_id", - cap_area_location = {"name": "location", - "joinby": "alert_id", - }, - cap_area_tag = {"name": "tag", - "joinby": "alert_id", - }, - cap_info = "alert_id", - cap_info_parameter = "alert_id", - cap_resource = "alert_id", + # Table Configuration + # TODO: if an alert is already-published, edit should create + # a new "Update" alert instead of modifying the original + # => implement special CRUD method handler for that + self.configure(tablename, + context = {"location": "location.location_id", + }, + create_onaccept = self.alert_create_onaccept, + deduplicate = S3Duplicate(primary = ("identifier", ), + secondary = ("sender", ), + ), + filter_widgets = filter_widgets, + list_fields = list_fields, + list_layout = cap_alert_list_layout, + # Required Fields + mark_required = ("scope",), + notify_fields = notify_fields, + onapprove = self.alert_onapprove, + onvalidation = self.alert_onvalidation, + # Order with most recent Alert first + orderby = "cap_alert.sent desc", ) - # Custom Methods - set_method("cap", "alert", - method = "import_feed", - action = cap_ImportAlert()) - - set_method("cap", "alert", - method = "assign", - action = self.cap_AssignArea()) - - set_method("cap", "alert", - method = "clone", - action = self.cap_CloneAlert()) - + # CRUD Strings if crud_strings["cap_template"]: crud_strings[tablename] = crud_strings["cap_template"] else: @@ -660,8 +699,6 @@ def model(self): label_create = T("Create Alert"), title_display = T("Alert Details"), title_list = T("Alerts"), - # If already-published, this should create a new "Update" - # alert instead of modifying the original title_update = T("Edit Alert"), title_upload = T("Import Alerts"), label_list_button = T("List Alerts"), @@ -669,12 +706,16 @@ def model(self): msg_record_created = T("Alert created"), msg_record_modified = T("Alert modified"), msg_record_deleted = T("Alert deleted"), - msg_list_empty = T("No alerts to show")) + msg_list_empty = T("No alerts to show"), + ) + # Reference representation alert_represent = S3Represent(lookup = tablename, fields = ["msg_type", "sent", "sender"], - field_sep = " - ") + field_sep = " - ", + ) + # Reusable Field alert_id = S3ReusableField("alert_id", "reference %s" % tablename, comment = T("The alert message containing this information"), label = T("Alert"), @@ -682,153 +723,339 @@ def model(self): represent = alert_represent, requires = IS_EMPTY_OR( IS_ONE_OF(db, "cap_alert.id", - alert_represent)), + alert_represent, + )), ) # --------------------------------------------------------------------- - # Warning Priorities for CAP - - tablename = "cap_warning_priority" + # Acknowledgement Table for CAP Alert + # + tablename = "cap_alert_ack" define_table(tablename, - Field("priority_rank", "integer", - label = T("Priority Sequence"), - length = 2, - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Priority Rank"), - T("The Priority Rank is basically to give it a ranking 1, 2, ..., n. That way we know 1 is the most important of the chain and n is lowest element. For eg. (1, Signal 1), (2, Signal 2)..., (5, Signal 5) to enumerate the priority for cyclone."))), - ), - Field("event_code", - label = T("Event Code"), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Event Code"), - T("Code (key) for the event like for eg. (2001, Typhoon), (2002, Flood)"))), - ), - Field("name", notnull=True, length=64, unique=True, - label = T("Name"), - requires = [IS_LENGTH(64), - IS_NOT_ONE_OF(db, "%s.name" % tablename), - IS_MATCH('^[^"\']+$', - error_message=T('Name cannot be empty and Must not include " or (\')'), - ), - ], - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Name"), - T("The actual name for the warning priority, for eg. Typhoons in Philippines have five priority names (PSWS# 1, PSWS# 2, PSWS# 3, PSWS# 4 and PSWS# 5)"))), - ), - self.event_type_id(empty=False, - ondelete = "SET NULL", - label = T("Event Type"), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Event Type"), - T("The Event to which this priority is targeted for."))), - ), - Field("urgency", - label = T("Urgency"), - requires = IS_IN_SET(cap_options["cap_info_urgency_opts"]), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Denotes the urgency of the subject event of the alert message"), - T("The urgency, severity, and certainty of the information collectively distinguish less emphatic from more emphatic messages." + - "'Immediate' - Responsive action should be taken immediately" + - "'Expected' - Responsive action should be taken soon (within next hour)" + - "'Future' - Responsive action should be taken in the near future" + - "'Past' - Responsive action is no longer required" + - "'Unknown' - Urgency not known"))), - ), - Field("severity", - label = T("Severity"), - requires = IS_IN_SET(cap_options["cap_info_severity_opts"]), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Denotes the severity of the subject event of the alert message"), - T("The urgency, severity, and certainty elements collectively distinguish less emphatic from more emphatic messages." + - "'Extreme' - Extraordinary threat to life or property" + - "'Severe' - Significant threat to life or property" + - "'Moderate' - Possible threat to life or property" + - "'Minor' - Minimal to no known threat to life or property" + - "'Unknown' - Severity unknown"))), - ), - Field("certainty", - label = T("Certainty"), - requires = IS_IN_SET(cap_options["cap_info_certainty_opts"]), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Denotes the certainty of the subject event of the alert message"), - T("The urgency, severity, and certainty elements collectively distinguish less emphatic from more emphatic messages." + - "'Observed' - Determined to have occurred or to be ongoing" + - "'Likely' - Likely (p > ~50%)" + - "'Possible' - Possible but not likely (p <= ~50%)" + - "'Unlikely' - Not expected to occur (p ~ 0)" + - "'Unknown' - Certainty unknown"))), - ), - Field("color_code", - label = T("Color Code"), - represent = warning_priority_color, - widget = S3ColorPickerWidget(options = { - "showPaletteOnly": False, - }), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The color code for this priority"), - T("Pick from the color widget the color that is associated to this priority of the event. The color code is in hex format"))), - ), - # to record the last checked time for the table. - # used to sent notifications to subscribers about available options - s3_datetime("last_checked", - readable = False, - writable = False, + alert_id(readable = False, + writable = False, + ), + # Use this when location intersect filter is supported: + self.gis_location_id( + readable = False, + writable = False, + ), + s3_datetime("acknowlegded_on", + label = T("Acknowledged On"), + default = "now", + requires = IS_NOT_EMPTY(), ), + Field("acknowledged_by", + label = T("Acknowledged By"), + requires = IS_NOT_EMPTY(), + ), + s3_comments(), *s3_meta_fields()) - priority_represent = S3Represent(lookup=tablename, translate=True) - + # CRUD Strings crud_strings[tablename] = Storage( - label_create = T("Create Warning Classification"), - title_display = T("Warning Classification Details"), - title_list = T("Warning Classifications"), - title_update = T("Edit Warning Classification"), - title_upload = T("Import Warning Classifications"), - label_list_button = T("List Warning Classifications"), - label_delete_button = T("Delete Warning Classification"), - msg_record_created = T("Warning Classification added"), - msg_record_modified = T("Warning Classification updated"), - msg_record_deleted = T("Warning Classification removed"), - msg_list_empty = T("No Warning Classifications currently registered") + label_create = T("Add Acknowledgement"), + title_display = T("Alert Acknowledgement"), + title_list = T("Alert Acknowledgements"), + title_update = T("Edit Acknowledgement"), + subtitle_list = T("List Alert Acknowledgements"), + label_list_button = T("List Alert Acknowledgements"), + label_delete_button = T("Delete Acknowledgement"), + msg_record_created = T("Acknowledgement added"), + msg_record_modified = T("Acknowledgement updated"), + msg_record_deleted = T("Acknowledgement deleted"), + msg_list_empty = T("No Acknowledgements currently received for this alert"), ) - list_fields = ["event_type_id", - (T("Color"), "color_code"), - "name", - (T("Rank"), "priority_rank"), - ] + # --------------------------------------------------------------------- + # Pass names back to global scope (s3.*) + # + return {"cap_alert_id": alert_id, + "cap_alert_represent": alert_represent, + "cap_alert_onapprove": self.alert_onapprove, + } - configure(tablename, - create_onaccept = self.cap_warning_priority_onaccept, - # Not needed since unique=True - #deduplicate = S3Duplicate(primary=("event_type_id", "name")), - list_fields = list_fields, - #onvalidation = self.cap_warning_priority_onvalidation, - orderby = "cap_warning_priority.event_type_id,cap_warning_priority.priority_rank desc", - ) + # ------------------------------------------------------------------------- + @staticmethod + def defaults(): + """ + Return safe defaults in case the model has been deactivated. + """ - # --------------------------------------------------------------------- - # CAP info segment - settings.L10n.extra_codes = [("en-US", "English"), - ("en-CA", "Canadian English"), - ("fr-CA", "Canadian French"), - ] - tablename = "cap_info" - define_table(tablename, - alert_id(), - Field("is_template", "boolean", - default = False, - readable = False, - writable = False, - ), - Field("template_info_id", "reference cap_info", - ondelete = "SET NULL", - readable = False, + return {"cap_alert_id": S3ReusableField("alert_id", "integer", + readable = False, + writable = False, + ), + } + + # ------------------------------------------------------------------------- + @staticmethod + def generate_identifier(): + """ + Generate an alert identifier for a new (internal) alert + + @returns: a string of the format "urn:oid:.." + """ + + db = current.db + table = db.cap_alert + + # The OID + # - OID is normally of the form 2.49.0.1.104.xx.(yy) + # - xx=0 identifies this as OID of the organisation itself + # - should be changed to xx=1 when it is used in an alert + oid = current.deployment_settings.get_cap_identifier_oid() + parts = oid.split(".") + if len(parts) > 5 and parts[5] == "0": + parts[5] = "1" + oid = ".".join(parts) + + # Current date, formatted + now = datetime.datetime.utcnow().strftime("%Y.%m.%d") + + # Determine the next record ID in the alert table + row = db().select(table.id, + limitby = (0, 1), + orderby = ~table.id, + ).first() + next_id = (row.id + 1) if row else 1 + + # Format: urn:oid:.. + return "urn:oid:%s.%s.%03d" % (oid, now, next_id) + + # ------------------------------------------------------------------------- + @staticmethod + def generate_sender(): + """ + Generate a sender name for a new (internal) alert + - use email address of current user + + @returns: the sender name as string + """ + + try: + user_email = current.auth.user.email + except AttributeError: + # Not logged-in + sender = "" + else: + sender = s3_str(user_email) + + return sender + + # ------------------------------------------------------------------------- + @staticmethod + def generate_source(): + """ + Generate a source designation for a new (internal) alert + + @returns: the message source designation as string + """ + return "%s@%s" % (current.xml.domain, + current.deployment_settings.get_base_public_url(), + ) + + # ------------------------------------------------------------------------- + @staticmethod + def alert_onvalidation(form): + """ + Validate an alert form + + @param form: the FORM + """ + + T = current.T + + form_vars_get = form.vars.get + if not form_vars_get("is_template"): + # Actual Alert (not a template) + + scope = form_vars_get("scope") + + # Scope is mandatory + if not scope: + form.errors["scope"] = T("'Scope' field is mandatory for actual alerts!") + + ## Scope is mandatory if specifying Recipients + #if form_vars_get("addresses") and not scope: + # form.errors["scope"] = T("'Scope' field mandatory in case using 'Recipients' field") + + # Scope "Private" requires recipients (interactive input only) + request = current.request + if scope == "Private" and \ + request.controller == "cap" and request.function == "alert": + if not form_vars_get("addresses"): + form.errors["addresses"] = T("'Recipients' field mandatory in case of 'Private' scope") + + # Scope "Restricted" requires specification of restriction + elif scope == "Restricted" and not form_vars_get("restriction"): + form.errors["restriction"] = T("'Restriction' field mandatory in case of 'Restricted' scope") + + # Actual alerts cannot have "Draft" status + if form_vars_get("status") == "Draft": + form.errors["status"] = T("Cannot issue 'Draft' alerts!") + + # ------------------------------------------------------------------------- + @staticmethod + def alert_create_onaccept(form): + """ + Onaccept-routine for alerts: + - automatically approve if template + + @param form: the FORM + """ + + form_vars = form.vars + if form_vars.get("is_template"): + + user = current.auth.user + if user: + table = current.s3db.cap_alert + current.db(table.id == form_vars.id).update(approved_by=user.id) + + # ------------------------------------------------------------------------- + @staticmethod + def alert_onapprove(record=None): + """ + On approval of alerts: + - update the approved_on field + - notify the record owner of the approval + - create history entry for the approved record + + @param record: the alert record + + TODO set approved_on even when approval is not required + (we want to allow auto-approval for editors) + """ + + if not record: + return + + alert_id = record["id"] + + # Update approved_on at the time the alert is approved + if alert_id: + + db = current.db + table = db.cap_alert + query = (table.id == alert_id) & \ + (table.deleted == False) + + # Update approved_on + utcnow = current.request.utcnow + db(query).update(approved_on=utcnow, sent=utcnow) + + # Get the record owner + row = db(query).select(table.id, + table.owned_by_user, + limitby = (0, 1), + ).first() + owner = row.owned_by_user if row else None + + # Notify the owner of the record about approval + if owner: + pe_id = current.auth.s3_user_pe_id(owner) + + settings = current.deployment_settings + subject = "%s: Alert Approved" % settings.get_system_name_short() + url = "%s%s" % (settings.get_base_public_url(), + URL(c="cap", f="alert", args=[alert_id]), + ) + message = current.T("This alert that you requested to review has been approved:\n\n%s") % url + current.msg.send_by_pe_id(pe_id, + subject, + message, + alert_id = alert_id, + ) + + # Record the approved alert in alert history + clone(current.request, record) + + # ------------------------------------------------------------------------- + @staticmethod + def template_represent(alert_id, row=None): + """ + Represent an alert template concisely + + @param alert_id: the alert record ID + @param row: the alert record (if already loaded) + + @returns: a string representation for the alert ID + + TODO make S3Represent at module level + """ + + if row: + alert_id = row.id + elif alert_id: + db = current.db + table = db.cap_alert + row = db(table.id == alert_id).select(table.is_template, + table.template_title, + limitby = (0, 1), + ).first() + + if row: + if row.is_template: + # @ToDo: get headline from locale-specific "info" instead? + repr_str = row.template_title + else: + repr_str = current.s3db.cap_alert_represent(alert_id) + else: + repr_str = current.messages["NONE"] + + return repr_str + +# ============================================================================= +class CAPInfoModel(S3Model): + + names = ("cap_info", + "cap_info_id", + "cap_info_represent", + "cap_info_parameter", + ) + + def model(self): + + T = current.T + + db = current.db + settings = current.deployment_settings + crud_strings = current.response.s3.crud_strings + + define_table = self.define_table + configure = self.configure + + cap_options = get_cap_options() + + alert_id = self.cap_alert_id + + # TODO do not modify a deployment setting in the model, + # pass as mandatory options s3_language instead + settings.L10n.extra_codes = [("en-US", "English"), + ("en-CA", "Canadian English"), + ("fr-CA", "Canadian French"), + ] + + # --------------------------------------------------------------------- + # Info segment of Alert + # + tablename = "cap_info" + define_table(tablename, + alert_id(), + Field("is_template", "boolean", + default = False, + readable = False, + writable = False, + ), + Field("template_info_id", "reference cap_info", + ondelete = "SET NULL", + readable = False, requires = IS_EMPTY_OR( IS_ONE_OF(db, "cap_info.id", - self.cap_template_represent, - filterby="is_template", - filter_opts=(True,) + CAPAlertModel.template_represent, + filterby = "is_template", + filter_opts = (True,), )), widget = S3HiddenWidget(), ), @@ -841,230 +1068,224 @@ def model(self): Field("category", "list:string", # 1 or more allowed label = T("Category"), required = IS_NOT_EMPTY(), - represent = S3Represent(options = cap_options["cap_info_category_opts"], + represent = S3Represent(options = cap_options["categories"], multiple = True, ), - requires = IS_IN_SET(cap_options["cap_info_category_opts"], + requires = IS_IN_SET(cap_options["categories"], multiple = True, ), - widget = S3MultiSelectWidget(selectedList = 10), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Denotes the category of the subject event of the alert message"), - T("You may select multiple categories by holding down control and then selecting the items."))), + widget = S3MultiSelectWidget(selectedList=10), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("Denotes the category of the subject event of the alert message"), + T("You may select multiple categories by holding down control and then selecting the items."), + ), + ), ), Field("event", label = T("Event"), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The text denoting the type of the subject event of the alert message"), - T("If not specified, will the same as the Event Type."))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("The text denoting the type of the subject event of the alert message"), + T("If not specified, will the same as the Event Type."), + ), + ), ), - self.event_type_id(#empty = False, - readable = False, - ondelete = "SET NULL", - label = T("Event Type"), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Event Type of the alert message"), - T("Event field above is more general. And this Event Type is classification of event. For eg. Event can be 'Terrorist Attack' and Event Type can be either 'Terrorist Bomb Explosion' or 'Terrorist Chemical Waefare Attack'. If not specified, will the same as the Event Type."))), - script = ''' - $.filterOptionsS3({ - 'trigger':'event_type_id', - 'target':'priority', - 'lookupPrefix': 'cap', - 'lookupResource':'warning_priority', - 'lookupKey': 'event_type_id', - 'optional': true - })''' - ), + self.event_type_id( + #empty = False, + readable = False, + ondelete = "SET NULL", + label = T("Event Type"), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("Event Type of the alert message"), + T("Event field above is more general. And this Event Type is classification of event. For eg. Event can be 'Terrorist Attack' and Event Type can be either 'Terrorist Bomb Explosion' or 'Terrorist Chemical Waefare Attack'. If not specified, will the same as the Event Type."), + ), + ), + script = ''' +$.filterOptionsS3({ + 'trigger':'event_type_id', + 'target':'priority', + 'lookupPrefix': 'cap', + 'lookupResource':'warning_priority', + 'lookupKey': 'event_type_id', + 'optional': true +})''', + ), Field("response_type", "list:string", # 0 or more allowed label = T("Response Type"), - represent = S3Represent(options = cap_options["cap_info_response_type_opts"], + represent = S3Represent(options = cap_options["response_types"], multiple = True, ), - requires = IS_IN_SET(cap_options["cap_info_response_type_opts"], + requires = IS_IN_SET(cap_options["response_types"], multiple = True), - widget = S3MultiSelectWidget(selectedList = 10), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Denotes the type of action recommended for the target audience"), - T("Multiple response types can be selected by holding down control and then selecting the items"))), - ), - Field("priority", "reference cap_warning_priority", - label = T("Priority"), - represent = priority_represent, - requires = IS_EMPTY_OR( - IS_ONE_OF(db, "cap_warning_priority.id", - priority_represent - ), - ), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Priority of the alert message"), - T("Defines the priority of the alert message. Selection of the priority automatically sets the value for 'Urgency', 'Severity' and 'Certainty'"))), + widget = S3MultiSelectWidget(selectedList=10), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("Denotes the type of action recommended for the target audience"), + T("Multiple response types can be selected by holding down control and then selecting the items"), + ), + ), ), + self.cap_warning_priority_id( + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("Priority of the alert message"), + T("Defines the priority of the alert message. Selection of the priority automatically sets the value for 'Urgency', 'Severity' and 'Certainty'") + ), + ), + ), Field("urgency", label = T("Urgency"), - represent = S3Represent(options = cap_options["cap_info_urgency_opts"], + represent = S3Represent(options = cap_options["urgency"], ), # Empty For Template, checked onvalidation hook requires = IS_EMPTY_OR( - IS_IN_SET(cap_options["cap_info_urgency_opts"])), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Denotes the urgency of the subject event of the alert message"), - T("The urgency, severity, and certainty of the information collectively distinguish less emphatic from more emphatic messages." + - "'Immediate' - Responsive action should be taken immediately" + - "'Expected' - Responsive action should be taken soon (within next hour)" + - "'Future' - Responsive action should be taken in the near future" + - "'Past' - Responsive action is no longer required" + - "'Unknown' - Urgency not known"))), + IS_IN_SET(cap_options["urgency"])), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("Urgency"), + T("Denotes the urgency of the subject event of the alert message"), + ), + ), ), Field("severity", label = T("Severity"), - represent = S3Represent(options = cap_options["cap_info_severity_opts"], + represent = S3Represent(options = cap_options["severity"], ), # Empty For Template, checked onvalidation hook requires = IS_EMPTY_OR( - IS_IN_SET(cap_options["cap_info_severity_opts"])), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Denotes the severity of the subject event of the alert message"), - T("The urgency, severity, and certainty elements collectively distinguish less emphatic from more emphatic messages." + - "'Extreme' - Extraordinary threat to life or property" + - "'Severe' - Significant threat to life or property" + - "'Moderate' - Possible threat to life or property" + - "'Minor' - Minimal to no known threat to life or property" + - "'Unknown' - Severity unknown"))), + IS_IN_SET(cap_options["severity"])), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("Severity"), + T("Denotes the severity of the subject event of the alert message"), + ), + ), ), Field("certainty", label = T("Certainty"), - represent = S3Represent(options = cap_options["cap_info_certainty_opts"], + represent = S3Represent(options = cap_options["certainty"], ), # Empty For Template, checked onvalidation hook requires = IS_EMPTY_OR( - IS_IN_SET(cap_options["cap_info_certainty_opts"])), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Denotes the certainty of the subject event of the alert message"), - T("The urgency, severity, and certainty elements collectively distinguish less emphatic from more emphatic messages." + - "'Observed' - Determined to have occurred or to be ongoing" + - "'Likely' - Likely (p > ~50%)" + - "'Possible' - Possible but not likely (p <= ~50%)" + - "'Unlikely' - Not expected to occur (p ~ 0)" + - "'Unknown' - Certainty unknown"))), + IS_IN_SET(cap_options["certainty"])), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("Certainty"), + T("Denotes the certainty of the subject event of the alert message"), + ), + ), ), Field("audience", "text", label = T("Audience"), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Audience"), - T("The intended audience of the alert message"))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("Audience"), + T("The intended audience of the alert message"), + ), + ), ), Field("event_code", "text", label = T("Event Code"), default = settings.get_cap_event_codes(), represent = S3KeyValueWidget.represent, widget = S3KeyValueWidget(), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("A system-specific code identifying the event type of the alert message"), - T("Any system-specific code for events, in the form of key-value pairs. (e.g., SAME, FIPS, ZIP)."))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("A system-specific code identifying the event type of the alert message"), + T("Any system-specific code for events, in the form of key-value pairs. (e.g., SAME, FIPS, ZIP)."), + ), + ), ), s3_datetime("effective", label = T("Effective"), default = "now", - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The effective time of the information of the alert message"), - T("If not specified, the effective time shall be assumed to be the same the time the alert was sent."))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("The effective time of the information of the alert message"), + T("If not specified, the effective time shall be assumed to be the same the time the alert was sent."), + ), + ), ), s3_datetime("onset", label = T("Onset"), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Onset"), - T("The expected time of the beginning of the subject event of the alert message"))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("Onset"), + T("The expected time of the beginning of the subject event of the alert message"), + ), + ), ), s3_datetime("expires", label = T("Expires at"), #past = 0, default = cap_expirydate(), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The expiry time of the information of the alert message"), - T("If this item is not provided, each recipient is free to enforce its own policy as to when the message is no longer in effect."))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("The expiry time of the information of the alert message"), + T("If this item is not provided, each recipient is free to enforce its own policy as to when the message is no longer in effect."), + ), + ), ), Field("sender_name", label = T("Sender's name"), - default = self.cap_sendername, - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The text naming the originator of the alert message"), - T("The human-readable name of the agency or authority issuing this alert."))), + default = cap_sendername, + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("The text naming the originator of the alert message"), + T("The human-readable name of the agency or authority issuing this alert."), + ), + ), ), Field("headline", label = T("Headline"), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The text headline of the alert message"), - T("A brief human-readable headline. Note that some displays (for example, short messaging service devices) may only present this headline; it should be made as direct and actionable as possible while remaining short. 160 characters may be a useful target limit for headline length."))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("The text headline of the alert message"), + T("A brief human-readable headline. Note that some displays (for example, short messaging service devices) may only present this headline; it should be made as direct and actionable as possible while remaining short. 160 characters may be a useful target limit for headline length."), + ), + ), ), Field("description", "text", label = T("Description"), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The subject event of the alert message"), - T("An extended human readable description of the hazard or event that occasioned this message."))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("The subject event of the alert message"), + T("An extended human readable description of the hazard or event that occasioned this message."), + ), + ), ), Field("instruction", "text", label = T("Instruction"), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The recommended action to be taken by recipients of the alert message"), - T("An extended human readable instruction to targeted recipients. If different instructions are intended for different recipients, they should be represented by use of multiple information blocks. You can use a different information block also to specify this information in a different language."))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("The recommended action to be taken by recipients of the alert message"), + T("An extended human readable instruction to targeted recipients. If different instructions are intended for different recipients, they should be represented by use of multiple information blocks. You can use a different information block also to specify this information in a different language."), + ), + ), ), Field("contact", "text", label = T("Contact information"), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Contact"), - T("The contact for follow-up and confirmation of the alert message"))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("Contact"), + T("The contact for follow-up and confirmation of the alert message"), + ), + ), ), Field("web", label = T("URL"), requires = IS_EMPTY_OR(IS_URL()), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("A URL associating additional information with the alert message"), - T("A full, absolute URI for an HTML page or other text resource with additional or reference information regarding this alert."))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("A URL associating additional information with the alert message"), + T("A full, absolute URI for an HTML page or other text resource with additional or reference information regarding this alert."), + ), + ), ), #Field("parameter", "text", # default = settings.get_cap_parameters(), # label = T("Parameters"), # represent = S3KeyValueWidget.represent, # widget = S3KeyValueWidget(), - # comment = DIV(_class="tooltip", - # _title="%s|%s" % (T("A system-specific additional parameter associated with the alert message"), - # T("Any system-specific datum, in the form of key-value pairs."))), + # comment = DIV(_class = "tooltip", + # _title = "%s|%s" % (T("A system-specific additional parameter associated with the alert message"), + # T("Any system-specific datum, in the form of key-value pairs."), + # ), + # ), # ), *s3_meta_fields()) - if crud_strings["cap_template_info"]: - crud_strings[tablename] = crud_strings["cap_template_info"] - else: - ADD_INFO = T("Add alert information") - crud_strings[tablename] = Storage( - label_create = ADD_INFO, - title_display = T("Alert information"), - title_list = T("Information entries"), - title_update = T("Update alert information"), # this will create a new "Update" alert? - title_upload = T("Import alert information"), - subtitle_list = T("Listing of alert information items"), - label_list_button = T("List information entries"), - label_delete_button = T("Delete Information"), - msg_record_created = T("Alert information created"), - msg_record_modified = T("Alert information modified"), - msg_record_deleted = T("Alert information deleted"), - msg_list_empty = T("No alert information to show")) - - info_represent = S3Represent(lookup = tablename, - fields = ["language", "headline"], - field_sep = " - ") - - info_id = S3ReusableField("info_id", "reference %s" % tablename, - label = T("Information Segment"), - ondelete = "CASCADE", - represent = info_represent, - requires = IS_EMPTY_OR( - IS_ONE_OF(db, "cap_info.id", - info_represent) - ), - #sortby = "identifier", - ) + # Components + self.add_components(tablename, + cap_info_parameter = "info_id", + cap_resource = "info_id", + cap_area = "info_id", + ) + # CRUD Form crud_form = S3SQLCustomForm("alert_id", "is_template", "template_info_id", @@ -1090,42 +1311,79 @@ def model(self): "instruction", "contact", "web", - S3SQLInlineComponent("info_parameter", - name = "info_parameter", - label = T("Parameter"), - fields = ["name", - "value", - "mobile", - ], - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("A system-specific additional parameter associated with the alert message"), - T("Any system-specific datum, in the form of key-value pairs."))), - ), + S3SQLInlineComponent( + "info_parameter", + name = "info_parameter", + label = T("Parameter"), + fields = ["name", + "value", + "mobile", + ], + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("A system-specific additional parameter associated with the alert message"), + T("Any system-specific datum, in the form of key-value pairs."), + ), + ), + ), ) + # Table Configuration configure(tablename, + # TODO: if an alert is already-published, edit should create + # a new "Update" alert instead of modifying the original + # => implement special CRUD method handler for that crud_form = crud_form, - deduplicate = self.cap_info_duplicate, + deduplicate = self.info_duplicate, # Required Fields mark_required = ("urgency", "severity", "certainty",), - onaccept = self.cap_info_onaccept, - onvalidation = self.cap_info_onvalidation, + onaccept = self.info_onaccept, + onvalidation = self.info_onvalidation, ) - # Components - add_components(tablename, - cap_info_parameter = "info_id", - cap_resource = "info_id", - cap_area = "info_id", - ) + # CRUD Strings + if crud_strings["cap_template_info"]: + crud_strings[tablename] = crud_strings["cap_template_info"] + else: + crud_strings[tablename] = Storage( + label_create = T("Add alert information"), + title_display = T("Alert information"), + title_list = T("Information entries"), + title_update = T("Update alert information"), + title_upload = T("Import alert information"), + subtitle_list = T("Listing of alert information items"), + label_list_button = T("List information entries"), + label_delete_button = T("Delete Information"), + msg_record_created = T("Alert information created"), + msg_record_modified = T("Alert information modified"), + msg_record_deleted = T("Alert information deleted"), + msg_list_empty = T("No alert information to show"), + ) - # --------------------------------------------------------------------- - # CAP Info Parameters - tablename = "cap_info_parameter" - define_table(tablename, - alert_id(), - info_id(), - Field("name", + # Reference Representation + info_represent = S3Represent(lookup = tablename, + fields = ["language", "headline"], + field_sep = " - ") + + # Reusable Field + info_id = S3ReusableField("info_id", "reference %s" % tablename, + label = T("Information Segment"), + ondelete = "CASCADE", + represent = info_represent, + requires = IS_EMPTY_OR( + IS_ONE_OF(db, "cap_info.id", + info_represent, + )), + #sortby = "identifier", + ) + + # --------------------------------------------------------------------- + # CAP Info Parameters (Name-Value-Pairs) + # + tablename = "cap_info_parameter" + define_table(tablename, + alert_id(), + info_id(), + Field("name", label = T("Name"), ), Field("value", @@ -1138,207 +1396,399 @@ def model(self): ), *s3_meta_fields()) + # Table Configuration configure(tablename, - onaccept = self.cap_info_parameter_onaccept, - #onvalidation = self.cap_info_parameter_onvalidation, + onaccept = self.info_parameter_onaccept, + #onvalidation = self.info_parameter_onvalidation, ) # --------------------------------------------------------------------- - # CAP Resource segments - # - # Resource elements sit inside the Info segment of the export XML - # - however in most cases these would be common across all Infos, so in - # our internal UI we link these primarily to the Alert but still - # allow the option to differentiate by Info - # - tablename = "cap_resource" - define_table(tablename, - alert_id(writable = False, - ), - # Only used for imports - # in CAP, resource are linked to alert - info_id(readable = False, - writable = False, - ), - Field("is_template", "boolean", - default = False, - readable = False, - writable = False, - ), - self.super_link("doc_id", "doc_entity"), - Field("resource_desc", - requires = IS_NOT_EMPTY(), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The type and content of the resource file"), - T("The human-readable text describing the type and content, such as 'map' or 'photo', of the resource file."))), - ), - # Using image field instead of doc_id because the CropWidget doesn't work in inline forms - Field("image", "upload", - label = T("Image"), - length = current.MAX_FILENAME_LENGTH, - represent = self.doc_image_represent, - requires = IS_EMPTY_OR(IS_IMAGE(maxsize=(800, 800), - error_message=\ -T("Upload an image file(bmp, gif, jpeg or png), max. 800x800 pixels!"))), - uploadfolder = os.path.join(current.request.folder, - "uploads", "images"), - widget = S3ImageCropWidget((800, 800)), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Image"), - T("Attach an image that provides extra information about the event"))), - ), - Field("mime_type", - requires = IS_NOT_EMPTY(), - readable = False, - writable = False, - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The identifier of the MIME content type and sub-type describing the resource file"), - T("MIME content type and sub-type as described in [RFC 2046]. (As of this document, the current IANA registered MIME types are listed at http://www.iana.org/assignments/media-types/)"))), - ), - Field("size", "integer", - label = T("Size in Bytes"), - readable = False, - writable = False, - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The integer indicating the size of the resource file"), - T("Approximate size of the resource file in bytes."))), - ), - Field("uri", - label = T("Link to any resources"), - requires = IS_EMPTY_OR(IS_URL()), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The identifier of the hyperlink for the resource file"), - T("A full absolute URI, typically a Uniform Resource Locator that can be used to retrieve the resource over the Internet."))), - ), - #Field("file", "upload"), - Field("deref_uri", "text", - readable = False, - writable = False, - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Deref URI"), - T("The base-64 encoded data content of the resource file"))), - ), - Field("digest", - writable = False, - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The code representing the digital digest ('hash') computed from the resource file"), - T("Calculated using the Secure Hash Algorithm (SHA-1)."))), - ), - *s3_meta_fields()) + # Pass names back to global scope (s3.*) + return {"cap_info_id": info_id, + "cap_info_represent": info_represent, + } - # CRUD Strings - crud_strings[tablename] = Storage( - label_create = T("Add Resource"), - title_display = T("Alert Resource"), - title_list = T("Resources"), - title_update = T("Edit Resource"), - subtitle_list = T("List Resources"), - label_list_button = T("List Resources"), - label_delete_button = T("Delete Resource"), - msg_record_created = T("Resource added"), - msg_record_modified = T("Resource updated"), - msg_record_deleted = T("Resource deleted"), - msg_list_empty = T("No resources currently defined for this alert")) - - # @todo: complete custom form - crud_form = S3SQLCustomForm("alert_id", - "info_id", - "is_template", - "resource_desc", - "uri", - "image", - "mime_type", - "size", - # The CropWidget doesn't work in inline forms - #S3SQLInlineComponent("image", - # label = T("Image"), - # fields = [("", "file")], - # comment = DIV(_class="tooltip", - # _title="%s|%s" % (T("Image"), - # T("Attach an image that provides extra information about the event."))), - # ), - S3SQLInlineComponent("document", - label = T("Document"), - fields = [("", "file")], - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Document"), - T("Attach document that provides extra information about the event."))), - ), - ) + # ------------------------------------------------------------------------- + @staticmethod + def defaults(): + """ + Return safe defaults in case the model has been deactivated. + """ - list_fields = ["resource_desc", - "uri", - "image", - "document.file", - ] + return {} - configure(tablename, - crud_form = crud_form, - list_fields = list_fields, - onaccept = self.cap_resource_onaccept, - onvalidation = self.cap_resource_onvalidation, - super_entity = "doc_entity", - ) + # ------------------------------------------------------------------------- + @staticmethod + def info_onvalidation(form): + """ + Form validation for info-segments: + - check for language duplicates + - for actual alerts: make sure urgency, severity, certainty + and category have been specified + + @param form: the FORM + """ + + T = current.T + + itable = current.db.cap_info + + # Check for existing info with the same language + query = (itable.alert_id == form.request_vars.alert_id) & \ + (itable.language == form.vars.language) & \ + (itable.id != form.request_vars.id) + other_info = current.db(query).select(itable.id, + limitby=(0, 1), + ).first() + + if other_info: + form.errors["language"] = T("Please edit already created info segment with same language!") + + form_record = form.record + if form_record and form_record.is_template == False: + + form_vars = form.vars + + #parameters = json.loads(form_vars.get("parameter")) + #if parameters: + # for parameter in parameters: + # if (parameter["key"] and not parameter["value"]) or \ + # (parameter["value"] and not parameter["key"]): + # form.errors["parameter"] = \ + # current.T("Name-Value Pair is incomplete.") + + if not form_vars.get("urgency"): + form.errors["urgency"] = T("'Urgency' field is mandatory") + if not form_vars.get("severity"): + form.errors["severity"] = T("'Severity' field is mandatory") + if not form_vars.get("certainty"): + form.errors["certainty"] = T("'Certainty' field is mandatory") + + if not form_vars.get("category"): + form.errors["category"] = T("At least one category is required.") + + # ------------------------------------------------------------------------- + @classmethod + def info_onaccept(cls, form): + """ + Onaccept-routine for info-segments + - inherit is_template from alert + - sanitize and complete input data + - sync all info-segments of the same alert + + @param form: the FORM + """ + + if "vars" in form: + form_vars = form.vars + elif "id" in form: + form_vars = form + elif hasattr(form, "vars"): + form_vars = form.vars + else: + form_vars = form + + info_id = form_vars.id + if not info_id: + return + + db = current.db + itable = db.cap_info + + info = db(itable.id == info_id).select(itable.id, + itable.alert_id, + itable.language, + itable.web, + itable.event, + itable.event_type_id, + itable.event_code, + itable.audience, + limitby = (0, 1), + ).first() + if info: + alert_id = info.alert_id + + # Details to update in this info-record: + update = {} + + # Set is_template if alert is template: + if alert_id and cap_alert_is_template(alert_id): + update["is_template"] = True + + # If event description is missing, use representation of event_type_id: + if not info.event: + update["event"] = itable.event_type_id.represent(info.event_type_id) + + # Event codes may contain invalid JSON => sanitize: + event_code = info.event_code + if event_code and ("|{" in event_code or "||" in event_code): + update["event_code"] = cls.sanitize_json(event_code) + + # Unused, using info_parameter component instead: + #parameter = info.parameter + #if parameter and ("|{" in parameter or "||" in parameter): + # update["parameter"] = cls.sanitize_json(parameter) + + # Clean up empty audience field + audience = info.audience + if not audience or audience == current.messages["NONE"]: + update["audience"] = None + + # Always US English: + if info.language == "en": + update["language"] = "en-US" + + if not info.web: + # Use the local alert URL for "web" + atable = current.s3db.cap_alert + row = db(atable.id == alert_id).select(atable.scope, + limitby = (0, 1), + ).first() + if row and row.scope == "Public": + fn = "public" + else: + fn = "alert" + web = "%s%s" % (current.deployment_settings.get_base_public_url(), + URL(c="cap", f=fn, args=[alert_id]), + ) + update["web"] = web + else: + web = None + + form_vars_get = form_vars.get + + # Defaults for relevance date fields + form_effective = form_vars_get("effective", current.request.utcnow) + form_expires = form_vars_get("expires", cap_expirydate()) + form_onset = form_vars_get("onset", form_effective) + + update.update({"effective": form_effective, + "expires": form_expires, + "onset": form_onset, + }) + + if update: + info.update_record(**update) + + # Sync data in all other info-records of the same alert + idata = {"category" : form_vars_get("category"), + "certainty" : form_vars_get("certainty"), + "effective" : form_effective, + "event_type_id" : form_vars_get("event_type_id"), + "expires" : form_expires, + "onset" : form_onset, + "priority" : form_vars_get("priority"), + "response_type" : form_vars_get("response_type"), + "severity" : form_vars_get("severity"), + "urgency" : form_vars_get("urgency"), + } + if web: + idata["web"] = web + query = (itable.alert_id == alert_id) & \ + (itable.id != info_id) & \ + (itable.deleted == False) + db(query).update(**idata) + + # ------------------------------------------------------------------------- + @staticmethod + def info_duplicate(item): + """ + Duplicate detection for info-segments + + @param item: the S3ImportItem + """ + + data = item.data + + alert_id = data.get("alert_id") + language = data.get("language", "en-US") # assume en-US if not specified + + if alert_id and language: + table = item.table + query = (table.alert_id == alert_id) & \ + (table.language == language) + duplicate = current.db(query).select(table.id, + limitby = (0, 1), + ).first() + if duplicate: + item.id = duplicate.id + item.method = item.METHOD.UPDATE + + # ------------------------------------------------------------------------- + @staticmethod + def info_parameter_onvalidation(form): + """ + Form validation for info_parameter + - currently unused (TODO: document why) + + @param form: the FORM + """ + + form_vars = form.vars + try: + name = form_vars.get("name") + value = form_vars.get("value") + except AttributeError: + return + + if not all((name, value)): + form.errors["name"] = current.T("Name-Value Pair is incomplete.") + + # ------------------------------------------------------------------------- + @staticmethod + def info_parameter_onaccept(form): + """ + Onaccept-routine of info parameters + - inherit alert_id from info segment + + @param form: the FORM + """ + + form_vars = form.vars + + try: + record_id = form_vars.id + except AttributeError: + record_id = None + if not record_id: + return + + db = current.db + itable = db.cap_info + ptable = db.cap_info_parameter + + query = (ptable.id == record_id) + + # Find the info record ID + try: + info_id = form_vars.info_id + except AttributeError: + info_id = None + if not info_id: + parameter = db(query).select(ptable.info_id, limitby=(0, 1)).first() + if parameter: + info_id = parameter.info_id + + if info_id and record_id: + # Get the alert ID from info record + info = db(itable.id == info_id).select(itable.alert_id, + limitby = (0, 1), + ).first() + alert_id = info.alert_id + + # Set the alert_id in parameter record + if alert_id: + db(query).update(alert_id=alert_id) + + # ============================================================================= + @staticmethod + def sanitize_json(string): + """ + Sanitize malformed JSON for key-value fields + + @param string: the JSON string + @returns: the sanitized JSON string + + TODO unclear why this is needed? => document, or remove? + """ + + if string == "||": + sanitized = "" + else: + sanitized = string.replace(" ", "") \ + .replace("|", "") \ + .replace("}{", "},{") \ + .replace("{u'", "{'") \ + .replace(":u'", ":'") \ + .replace(",u'", ",'") \ + .replace("'", '"') + + return "[%s]" % sanitized + +# ============================================================================= +class CAPAreaModel(S3Model): + + names = ("cap_area", + "cap_area_represent", + "cap_area_location", + "cap_area_name", + "cap_area_tag", + ) + + def model(self): + + T = current.T + + db = current.db + crud_strings = current.response.s3.crud_strings + + define_table = self.define_table + configure = self.configure + + alert_id = self.cap_alert_id + info_id = self.cap_info_id # --------------------------------------------------------------------- # CAP Area segments # - # Area elements sit inside the Info segment of the export XML - # - however in most cases these would be common across all Infos, so in - # our internal UI we link these primarily to the Alert but still - # allow the option to differentiate by Info + # @ToDo: move the following to developer wiki/blueprint? # - # Each can have multiple elements which are one of , - # , or . - # and are explicit geometry elements. - # is a key-value pair in which the key is a standard - # geocoding system like SAME, FIPS, ZIP, and the value is a defined - # value in that system. The region described by the is the - # union of the areas described by the individual elements, but the - # CAP spec advises that, if geocodes are included, the concrete - # geometry elements should outline the area specified by the geocodes, - # as not all recipients will have access to the meanings of the - # geocodes. However, since geocodes are a compact way to describe an - # area, it may be that they will be used without accompanying geometry, - # so we should not count on having or . + # - Area elements sit inside the Info segment of CAP XML + # - However, in most cases these would be common across all Info + # segments of the same alert, so in the UI we link these + # primarily to the Alert but still allow to differentiate by Info # - # Geometry elements are each represented by a gis_location record, and - # linked to the cap_area record via the cap_area_location link table. - # For the moment, objects are stored with the center in the - # gis_location's lat, lon, and radius (in km) as a tag "radius" and - # value. ToDo: Later, we will add CIRCLESTRING WKT. + # - Each can have multiple elements which are one of , + # , or . and are explicit geometry + # elements. is a key-value pair in which the key is a standard + # geocoding system like SAME, FIPS, ZIP, and the value is a defined + # value in that system. + # - The region described by the is the union of the areas + # described by the individual elements, but the CAP spec advises that, + # if geocodes are included, the concrete geometry elements should + # outline the area specified by the geocodes, as not all recipients + # will have access to the meanings of the geocodes. However, since + # geocodes are a compact way to describe an area, it may be that they + # will be used without accompanying geometry, so we should not count + # on having or . # - # Geocode elements are currently stored as key value pairs in the - # cap_area record. + # - Geometry elements are each represented by a gis_location record, + # and linked to the cap_area record via the cap_area_location link + # table. + # - For the moment, objects are stored with the center in the + # gis_location's lat, lon, and radius (in km) as a tag "radius" and + # value. + # - @ToDo: add support for CIRCLESTRING WKT # - # can also specify a minimum altitude and maximum altitude - # ("ceiling"). These are stored in explicit fields for now, but could - # be replaced by key value pairs, if it is found that they are rarely - # used. + # - Geocode elements are currently stored as key value pairs in the + # cap_area record. + # - can also specify a minimum altitude and maximum altitude + # ("ceiling"). These are stored in explicit fields for now, but + # could be replaced by key value pairs, if it is found that they are + # rarely used. + # + # - An alternative would be to have cap_area link to a gis_location_group + # record. In that case, the geocode tags could be stored in the + # gis_location_group's overall gis_location element's tags. The altitude + # could be stored in the overall gis_location's elevation, with ceiling + # stored in a tag. We could consider adding a maximum elevation field. # - # (An alternative would be to have cap_area link to a gis_location_group - # record. In that case, the geocode tags could be stored in the - # gis_location_group's overall gis_location element's tags. The altitude - # could be stored in the overall gis_location's elevation, with ceiling - # stored in a tag. We could consider adding a maximum elevation field.) - tablename = "cap_area" define_table(tablename, alert_id(), - # Only used for imports - # in CAP, area are linked to alert + # Only used for imports (in UI, area are linked to alert): info_id(readable = False, writable = False, ), - # From which template area is the area assigned from - # Used for internationalisation + # From which template area is the area assigned from: + # - used for internationalisation Field("template_area_id", "reference cap_area", ondelete = "SET NULL", readable = False, requires = IS_EMPTY_OR( IS_ONE_OF(db, "cap_area.id", - filterby="is_template", - filter_opts=(True,) + filterby = "is_template", + filter_opts = (True,), )), widget = S3HiddenWidget(), ), @@ -1352,68 +1802,71 @@ def model(self): requires = [IS_NOT_EMPTY(), IS_LENGTH(1024), ], - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The affected area of the alert message"), - T("A text description of the affected area."))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("The affected area of the alert message"), + T("A text description of the affected area."), + ), + ), ), - Field("altitude", "integer", # Feet above Sea-level in WGS84 (Specific or Minimum is using a range) + Field("altitude", "integer", + # Feet above Sea-level in WGS84 (Specific or Minimum is using a range) label = T("Altitude"), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The specific or minimum altitude of the affected area"), - T("If used with the ceiling element this value is the lower limit of a range. Otherwise, this value specifies a specific altitude. The altitude measure is in feet above mean sea level."))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("The specific or minimum altitude of the affected area"), + T("If used with the ceiling element this value is the lower limit of a range. Otherwise, this value specifies a specific altitude. The altitude measure is in feet above mean sea level."), + ), + ), ), - Field("ceiling", "integer", # Feet above Sea-level in WGS84 (Maximum) + Field("ceiling", "integer", + # Feet above Sea-level in WGS84 (Maximum) label = T("Ceiling"), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The maximum altitude of the affected area"), - T("must not be used except in combination with the 'altitude' element. The ceiling measure is in feet above mean sea level."))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("The maximum altitude of the affected area"), + T("must not be used except in combination with the 'altitude' element. The ceiling measure is in feet above mean sea level."), + ), + ), ), - # Only used for Templates + # Only used for template areas: self.event_type_id(ondelete = "SET NULL", - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Event Type of this predefined alert area"), - T("Event Type relating to this predefined area."))), - # script = ''' - #$.filterOptionsS3({ - # 'trigger':'event_type_id', - # 'target':'priority', - # 'lookupPrefix': 'cap', - # 'lookupResource': 'warning_priority', - # 'lookupKey': 'event_type_id' - # })''' - ), - # Only used for Templates - # Enable this when required - #Field("priority", "reference cap_warning_priority", - # label = T("Priority"), - # represent = priority_represent, - # requires = IS_EMPTY_OR( - # IS_ONE_OF( - # db, "cap_warning_priority.id", - # priority_represent - # ), - # ), - # comment = DIV(_class="tooltip", - # _title="%s|%s" % (T("Priority of the Event Type"), - # T("Defines the priority of the Event Type for this predefined area."))), - # ), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("Event Type of this predefined alert area"), + T("Event Type relating to this predefined area."), + ), + ), + #script = ''' + # $.filterOptionsS3({ + # 'trigger':'event_type_id', + # 'target':'priority', + # 'lookupPrefix': 'cap', + # 'lookupResource': 'warning_priority', + # 'lookupKey': 'event_type_id' + # })''', + ), + # Only relevant for template areas: + # - currently unused, re-enable if required + #self.cap_warning_priority_id( + # comment = DIV(_class = "tooltip", + # _title = "%s|%s" % (T("Priority of the Event Type"), + # T("Defines the priority of the Event Type for this predefined area."), + # ), + # ), + # ), *s3_meta_fields()) - # CRUD Strings - crud_strings[tablename] = Storage( - label_create = T("Add Area"), - title_display = T("Alert Area"), - title_list = T("Areas"), - title_update = T("Edit Area"), - subtitle_list = T("List Areas"), - label_list_button = T("List Areas"), - label_delete_button = T("Delete Area"), - msg_record_created = T("Area added"), - msg_record_modified = T("Area updated"), - msg_record_deleted = T("Area deleted"), - msg_list_empty = T("No areas currently defined for this alert")) - + # Components + self.add_components(tablename, + cap_area_location = {"name": "location", + "joinby": "area_id", + }, + cap_area_tag = {"name": "tag", + "joinby": "area_id", + }, + cap_area_name = {"name": "name", + "joinby": "area_id", + }, + ) + # CRUD Form crud_form = S3SQLCustomForm("alert_id", "info_id", "is_template", @@ -1426,7 +1879,9 @@ def model(self): fields = [("", "location_id")], comment = DIV(_class="tooltip", _title="%s|%s" % (T("Geolocation"), - T("The paired values of points defining a polygon that delineates the affected area of the alert message"))), + T("The paired values of points defining a polygon that delineates the affected area of the alert message"), + ), + ), ), S3SQLInlineComponent("tag", name = "tag", @@ -1435,52 +1890,62 @@ def model(self): ], comment = DIV(_class="tooltip", _title="%s|%s" % (T("The geographic code delineating the affected area"), - T("Any geographically-based code to describe a message target area, in the form. The key is a user-assigned string designating the domain of the code, and the content of value is a string (which may represent a number) denoting the value itself (e.g., name='ZIP' and value='54321'). This should be used in concert with an equivalent description in the more universally understood polygon and circle forms whenever possible."))), + T("Any geographically-based code to describe a message target area, in the form. The key is a user-assigned string designating the domain of the code, and the content of value is a string (which may represent a number) denoting the value itself (e.g., name='ZIP' and value='54321'). This should be used in concert with an equivalent description in the more universally understood polygon and circle forms whenever possible."), + ), + ), ), "altitude", "ceiling", ) - area_represent = cap_AreaRepresent(show_link=True) - + # Table Configuration configure(tablename, context = {"location": "location.location_id", }, #create_next = URL(f="area", args=["[id]", "location"]), crud_form = crud_form, - deduplicate = self.cap_area_duplicate, + deduplicate = self.area_duplicate, mark_required = ("event_type_id",), - onaccept = self.cap_area_onaccept, - onvalidation = self.cap_area_onvalidation, + onaccept = self.area_onaccept, + onvalidation = self.area_onvalidation, ) - # Components - add_components(tablename, - cap_area_location = {"name": "location", - "joinby": "area_id", - }, - cap_area_tag = {"name": "tag", - "joinby": "area_id", - }, - # Names - cap_area_name = {"name": "name", - "joinby": "area_id", - }, - ) + # CRUD Strings + crud_strings[tablename] = Storage( + label_create = T("Add Area"), + title_display = T("Alert Area"), + title_list = T("Areas"), + title_update = T("Edit Area"), + subtitle_list = T("List Areas"), + label_list_button = T("List Areas"), + label_delete_button = T("Delete Area"), + msg_record_created = T("Area added"), + msg_record_modified = T("Area updated"), + msg_record_deleted = T("Area deleted"), + msg_list_empty = T("No areas currently defined for this alert"), + ) + + # Reference Representation + area_represent = cap_AreaRepresent(show_link=True) + # Reusable Field area_id = S3ReusableField("area_id", "reference %s" % tablename, label = T("Area"), ondelete = "CASCADE", represent = area_represent, requires = IS_ONE_OF(db, "cap_area.id", - area_represent), + area_represent, + ), ) - # ToDo: Use a widget tailored to entering and . - # Want to be able to enter them by drawing on the map. - # Also want to allow selecting existing locations that have - # geometry, maybe with some filtering so the list isn't cluttered - # with irrelevant locations. + # --------------------------------------------------------------------- + # Area <> Location Link Table + # + # @ToDo: Use a widget tailored to entering and : + # - we want to be able to enter them by drawing on the map + # - we also want to allow selecting existing locations that + # have geometry, maybe with some filtering so the list + # isn't cluttered with irrelevant locations tablename = "cap_area_location" define_table(tablename, alert_id(readable = False, @@ -1499,6 +1964,12 @@ def model(self): ), *s3_meta_fields()) + # Table Configuration + configure(tablename, + deduplicate = S3Duplicate(primary=("area_id", "location_id")), + onaccept = self.area_location_onaccept, + ) + # CRUD Strings crud_strings[tablename] = Storage( label_create = T("Add Location"), @@ -1511,861 +1982,856 @@ def model(self): msg_record_created = T("Location added"), msg_record_modified = T("Location updated"), msg_record_deleted = T("Location deleted"), - msg_list_empty = T("No locations currently defined for this alert")) + msg_list_empty = T("No locations currently defined for this alert"), + ) + + # --------------------------------------------------------------------- + # Local Area Names + # + tablename = "cap_area_name" + define_table(tablename, + area_id(empty = False, + ondelete = "CASCADE", + ), + s3_language(empty = False, + select = current.deployment_settings.get_cap_languages(), + ), + Field("name_l10n", + label = T("Local Name"), + ), + s3_comments(), + *s3_meta_fields()) + # Table Configuration configure(tablename, - deduplicate = S3Duplicate(primary=("area_id", "location_id")), - onaccept = self.cap_area_location_onaccept + deduplicate = S3Duplicate(primary=("area_id", "language")), ) # --------------------------------------------------------------------- # Area Tags - # - Key-Value extensions - # - Used to hold for geocodes: key is the geocode system name, and - # value is the specific value for this area. - # - Could store other values here as well, to avoid dedicated fields + # - key-value pairs + # - used to hold geocodes: key is the geocode system name, and + # value is the specific value for this area + # - could store other values here as well, to avoid dedicated fields # in cap_area for rarely-used items like altitude and ceiling, but - # would have to distinguish those from geocodes. + # would have to distinguish those from geocodes + # + # @ToDo: Provide a mechanism for pre-loading geocodes that are not + # tied to individual areas + # @ToDo: Allow sharing the key-value pairs. Cf. Ruby on Rails tagging + # systems such as acts-as-taggable-on, which has a single table + # of tags used by all classes. Each tag record has the class + # and field that the tag belongs to, as well as the tag string. + # We'd want tag and value, but the idea is the same: There would + # be a table with tag / value pairs, and individual cap_area, + # event_event, org_whatever records would link to records in + # the tag table. So we actually would not have duplicate tag + # value records as we do now. # - # ToDo: Provide a mechanism for pre-loading geocodes that are not tied - # to individual areas. - # ToDo: Allow sharing the key-value pairs. Cf. Ruby on Rails tagging - # systems such as acts-as-taggable-on, which has a single table of tags - # used by all classes. Each tag record has the class and field that the - # tag belongs to, as well as the tag string. We'd want tag and value, - # but the idea is the same: There would be a table with tag / value - # pairs, and individual cap_area, event_event, org_whatever records - # would link to records in the tag table. So we actually would not have - # duplicate tag value records as we do now. - tablename = "cap_area_tag" define_table(tablename, alert_id(readable = False, writable = False, ), area_id(), - # ToDo: Allow selecting from a dropdown list of pre-defined - # geocode system names. + # @ToDo: Allow selecting from a dropdown list of pre-defined + # geocode system names. Field("tag", label = T("Geocode Name"), ), - # ToDo: Once the geocode system is selected, fetch a list - # of current values for that geocode system. Allow adding - # new values, e.g. with combo box menu. + # @ToDo: Once the geocode system is selected, fetch a list + # of current values for that geocode system. Allow + # adding new values, e.g. with combo box menu. Field("value", label = T("Value"), ), s3_comments(), *s3_meta_fields()) + # Table Configuration configure(tablename, deduplicate = S3Duplicate(primary=("area_id", "tag", "value")), - onaccept = self.cap_area_tag_onaccept, + onaccept = self.area_tag_onaccept, ) - # --------------------------------------------------------------------- - # Acknowledgement Table for CAP Alert - - tablename = "cap_alert_ack" - define_table(tablename, - alert_id(readable = False, - writable = False, - ), - # use-this when location intersect filter is supported - self.gis_location_id(readable = False, - writable = False, - ), - s3_datetime("acknowlegded_on", - label = T("Acknowledged On"), - default = "now", - requires = IS_NOT_EMPTY(), - ), - Field("acknowledged_by", - label = T("Acknowledged By"), - requires = IS_NOT_EMPTY(), - ), - s3_comments(), - *s3_meta_fields()) - - # CRUD Strings - crud_strings[tablename] = Storage( - label_create = T("Add Acknowledgement"), - title_display = T("Alert Acknowledgement"), - title_list = T("Alert Acknowledgements"), - title_update = T("Edit Acknowledgement"), - subtitle_list = T("List Alert Acknowledgements"), - label_list_button = T("List Alert Acknowledgements"), - label_delete_button = T("Delete Acknowledgement"), - msg_record_created = T("Acknowledgement added"), - msg_record_modified = T("Acknowledgement updated"), - msg_record_deleted = T("Acknowledgement deleted"), - msg_list_empty = T("No Acknowledgements currently received for this alert")) - # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) - return dict(cap_alert_id = alert_id, - cap_alert_represent = alert_represent, - cap_alert_approve = self.cap_alert_approve, - cap_area_id = area_id, - cap_area_represent = area_represent, - cap_info_represent = info_represent, - cap_info_category_opts = cap_options["cap_info_category_opts"], - cap_expiry_date = self.cap_expirydate, - cap_sender_name = self.cap_sendername, - cap_template_represent = self.cap_template_represent, - ) + # + return {"cap_area_represent": area_represent, + } # ------------------------------------------------------------------------- @staticmethod def defaults(): """ - Return safe defaults in case the model has been deactivated. - """ - - alert_id = S3ReusableField("alert_id", "integer", - readable = False, - writable = False) - - return dict(cap_alert_id = alert_id) - - # ------------------------------------------------------------------------- - @staticmethod - def generate_identifier(): - """ - Generate an identifier for a new form - """ - - db = current.db - table = db.cap_alert - r = db().select(table.id, - limitby=(0, 1), - orderby=~table.id).first() - - _time = datetime.datetime.strftime(datetime.datetime.utcnow(), "%Y.%m.%d") - if r: - next_id = int(r.id) + 1 - else: - next_id = 1 - - # Format: prefix:oid.time.alert_id - settings = current.deployment_settings - prefix = "urn:oid" - oid = settings.get_cap_identifier_oid() - # In Organization ID, the organization is identified with a 0 - # but the 0 should be changed to 1 when it is used in an alert message - # OID is normally of the form 2.49.0.1.104.xx.(yy) - oid_split = oid.split(".") - if len(oid_split) >= 6 and oid_split[5] == str(0): - oid_split[5] = str(1) - oid = ".".join(oid_split) - - return "%s:%s.%s.%03d" % (prefix, oid, _time, next_id) - - # ------------------------------------------------------------------------- - @staticmethod - def generate_sender(): - """ - Generate a sender for a new form - """ - try: - user_email = current.auth.user.email - except AttributeError: - return "" - - return "%s" % user_email - - # ------------------------------------------------------------------------- - @staticmethod - def generate_source(): - """ - Generate a source for CAP alert - """ - return "%s@%s" % (current.xml.domain, - current.deployment_settings.get_base_public_url()) - - # ------------------------------------------------------------------------- - @staticmethod - def cap_sendername(): - """ - Default Sender name for the alert - Sendername is the name of the organisation if user is associated - else None + Return safe defaults in case the model has been deactivated. """ - db = current.db - utable = db.auth_user - otable = current.s3db.org_organisation - query = (utable.id == current.auth.user.id) & \ - (utable.organisation_id == otable.id) & \ - (otable.deleted != True) - row = db(query).select(otable.name, - limitby=(0, 1)).first() - if row: - return row.name - return current.auth.user.username + return {"cap_area_represent": lambda v, row=None: s3_str(v), + } # ------------------------------------------------------------------------- @staticmethod - def cap_template_represent(alert_id, row=None): + def area_onvalidation(form): """ - Represent an alert template concisely + Validate an area form + + @param form: the Form """ - if row: - alert_id = row.id - elif not alert_id: - return current.messages["NONE"] - else: - db = current.db - table = db.cap_alert - row = db(table.id == alert_id).select(table.is_template, - table.template_title, - limitby=(0, 1), - ).first() + T = current.T - # @ToDo: Should get headline from "info"? - if row.is_template: - return row.template_title - else: - return current.s3db.cap_alert_represent(alert_id) + form_vars = form.vars - # ------------------------------------------------------------------------- - @staticmethod - def list_string_represent(string, fmt=lambda v: v): - try: - if isinstance(string, list): - return ", ".join([fmt(i) for i in string]) - elif isinstance(string, basestring): - return ", ".join([fmt(i) for i in string[1:-1].split("|")]) - except IndexError: - return current.messages.UNKNOWN_OPT - return "" + if form_vars.get("ceiling") and not form_vars.get("altitude"): + form.errors["altitude"] = \ + T("'Altitude' field is mandatory if using 'Ceiling' field.") + + if "event_type_id" in form_vars and form_vars.get("event_type_id") is None: + form.errors["event_type_id"] = \ + T("'Event Type' field is mandatory for Predefined Area.") # ------------------------------------------------------------------------- @staticmethod - def cap_alert_create_onaccept(form): + def area_onaccept(form): """ - Auto-approve Templates + Onaccept-routine for area segments + - inherit alert_id from info segment if not set in form + + @param form: the FORM """ - db = current.db form_vars = form.vars - table = current.s3db.cap_alert - if form_vars.get("is_template"): - user = current.auth.user - if user: - db(table.id == form_vars.id).update(approved_by = user.id) + + if form_vars.get("event_type_id"): + # Predefined Area + return + + db = current.db + + alert_id = form_vars.get("alert_id") + if not alert_id: + info_id = form_vars.get("info_id") + if info_id: + # Get the info-segment's alert ID + itable = db.cap_info + info = db(itable.id == info_id).select(itable.alert_id, + limitby = (0, 1), + ).first() + alert_id = info.alert_id if info else None + + if alert_id: + # Set alert_id in cap_area record + db(db.cap_area.id == form_vars.id).update(alert_id=alert_id) # ------------------------------------------------------------------------- @staticmethod - def cap_alert_onvalidation(form): + def area_duplicate(item): """ - Custom Form Validation: - multi-field level + Detect an area duplicate + + @param item: the S3ImportItem """ - form_vars_get = form.vars.get - if not form_vars_get("is_template"): - # For non templates - if not form_vars_get("scope"): - form.errors["scope"] = \ - current.T("'Scope' field is mandatory for actual alerts!") - - if form_vars_get("scope") == "Private": - # Check if this comes from cap_alert - # Can also come from rss import - request = current.request - if request.controller == "cap" and request.function == "alert": - # Internal alerts - if not form_vars_get("addresses"): - form.errors["addresses"] = \ - current.T("'Recipients' field mandatory in case of 'Private' scope") - - if form_vars_get("scope") == "Restricted" and not form_vars_get("restriction"): - form.errors["restriction"] = \ - current.T("'Restriction' field mandatory in case of 'Restricted' scope") - - if form_vars_get("addresses") and not form_vars_get("scope"): - form.errors["scope"] = \ - current.T("'Scope' field mandatory in case using 'Recipients' field") + data = item.data + name = data.get("name") - if form_vars_get("status") == "Draft": - form.errors["status"] = \ - current.T("Cannot issue 'Draft' alerts!") + if name is not None: + table = item.table + + event_type_id = data.get("event_type_id") + if event_type_id is not None: + # This is a template + query = (table.name == name) & \ + (table.event_type_id == event_type_id) + else: + alert_id = data.get("alert_id") + info_id = data.get("info_id") + + query = (table.name == name) + if alert_id is not None: + # Real Alert, not template + query &= (table.alert_id == alert_id) + elif info_id is not None: + # CAP XML Import + query &= (table.info_id == info_id) + else: + # Nothing we can use + return + + duplicate = current.db(query).select(table.id, + limitby = (0, 1), + ).first() + if duplicate: + item.id = duplicate.id + item.method = item.METHOD.UPDATE # ------------------------------------------------------------------------- @staticmethod - def cap_warning_priority_onvalidation(form): + def area_location_onaccept(form): """ - Custom Form Validation + Onaccept-routine for area-location links + - inherit alert_id from area-segment (non-template areas only) + - remove the location link for imported alerts if polygon import + is not configured (settings.cap.area_default), e.g. SAMBRO/PH + wants alerts only linked to standard locations by geocode, which + happens via a tag match (cap_area_tag <=> gis_location_tag) """ form_vars = form.vars - table = current.s3db.cap_warning_priority - query = (table.event_type_id == form_vars.event_type_id) & \ - (table.urgency == form_vars.urgency) & \ - (table.severity == form_vars.severity) & \ - (table.certainty == form_vars.certainty) & \ - (table.deleted != True) - row = current.db(query).select(table.id, limitby=(0, 1)).first() - if row: - form.errors["event_type_id"] = \ -current.T("This combination of the 'Event Type', 'Urgency', 'Certainty' and 'Severity' is already available in database.") + + area_id = form_vars.get("area_id") + if not area_id: + # Coming from assign method => no action + return + + db = current.db + + # Look up the alert_id + alert_id = form_vars.get("alert_id") + if not alert_id: + atable = db.cap_area + row = db(atable.id == area_id).select(atable.alert_id, + limitby = (0, 1), + ).first() + if row: + alert_id = row.alert_id + + if alert_id: + # Not a template area (template areas have no alert_id) + + # Remove the record if the alert is imported and polygon import + # is not configured (=>a link to a geocode location is created in + # area_tag_onaccept instead) + location_default = current.deployment_settings.get_cap_area_default() + if "polygon" not in location_default: + table = current.s3db.cap_alert + row = db(table.id == alert_id).select(table.external, + limitby = (0, 1), + ).first() + if row.external: + db(db.cap_area_location.id == form_vars["id"]).delete() + return + + # Inherit the alert_id from area + db(db.cap_area_location.id == form_vars["id"]).update(alert_id=alert_id) # ------------------------------------------------------------------------- @staticmethod - def cap_info_onaccept(form): + def area_tag_onaccept(form): """ - After DB I/O + Onaccept-routine for area tags + - inherit alert_id from area (non-template areas only) + - if the tag is a SAME code, create a cap_area_location link + for the area to the corresponding gis_location """ - if "vars" in form: - form_vars = form.vars - elif "id" in form: - form_vars = form - elif hasattr(form, "vars"): - form_vars = form.vars - else: - form_vars = form + form_vars = form.vars - info_id = form_vars.id - if not info_id: + area_id = form_vars.get("area_id") + if not area_id: + # Coming from assign method => no action return db = current.db - itable = db.cap_info - - info = db(itable.id == info_id).select(itable.id, - itable.alert_id, - itable.language, - itable.web, - itable.event, - itable.event_type_id, - itable.event_code, - itable.audience, + atable = db.cap_area + arow = db(atable.id == area_id).select(atable.alert_id, limitby = (0, 1), ).first() - if info: - alert_id = info.alert_id + alert_id = arow.alert_id - # Details to update in this info-record: - update = {} + if alert_id: + # Not a template area (template areas have no alert_id) - if alert_id and cap_alert_is_template(alert_id): - update["is_template"] = True + # Inherit the alert_id + db(db.cap_area_tag.id == form_vars.id).update(alert_id=alert_id) - if not info.event: - update["event"] = itable.event_type_id.represent(info.event_type_id) + s3db = current.s3db + settings = current.deployment_settings - # For prepopulating - event_code = info.event_code - if event_code and ("|{" in event_code or "||" in event_code): - update["event_code"] = json_formatter(event_code) + same_code = settings.get_cap_same_code() - # Unused: - #parameter = info.parameter - #if parameter and ("|{" in parameter or "||" in parameter): - # update["parameter"] = json_formatter(parameter) + tag = form_vars.get("tag") + link_location_by_code = False + + if same_code and tag == "SAME": + # Use this tag to link to an existing gis_location with this + # code if geocode-import is configured or the alert is generated + # locally + if "geocode" in settings.get_cap_area_default(): + link_location_by_code = True + else: + table = s3db.cap_alert + row = db(table.id == alert_id).select(table.external, + limitby = (0, 1), + ).first() + link_location_by_code = not row.external if row else False + + if link_location_by_code: + # Look up the gis_location with a matching SAME code + # - find a matching gis_location_tag where the tag name is the + # configured SAME code, and the value matches the value of this + # tag + # - it is possible for there to be two polygons for the same SAME + # code since polygon change over time, even if the code remains + # the same. Historic polygons are excluded as (gtable.end_date == None) + ttable = s3db.gis_location_tag + gtable = s3db.gis_location + tquery = (ttable.tag == same_code) & \ + (ttable.value == form_vars.get("value")) & \ + (ttable.deleted == False) & \ + (ttable.location_id == gtable.id) & \ + (gtable.end_date == None) & \ + (gtable.deleted == False) + trow = db(tquery).select(ttable.location_id, + limitby = (0, 1), + ).first() + if trow and trow.location_id: + # Match => link area to this location + ltable = db.cap_area_location + link = {"area_id": area_id, + "alert_id": alert_id or None, + "location_id": trow.location_id, + } + link_id = ltable.insert(**link) + current.auth.s3_set_record_owner(ltable, link_id) + # Currently not required: + #link["id"] = link_id + #s3db.onaccept(ltable, link) - audience = info.audience - if not audience or audience == current.messages["NONE"]: - update["audience"] = None +# ============================================================================= +class CAPResourceModel(S3Model): - if info.language == "en": - update["language"] = "en-US" + names = ("cap_resource", + ) - if not info.web: - # Use the local alert URL for "web" - atable = current.s3db.cap_alert - row = db(atable.id == alert_id).select(atable.scope, - limitby = (0, 1), - ).first() - if row and row.scope == "Public": - fn = "public" - else: - fn = "alert" - web = "%s%s" % (current.deployment_settings.get_base_public_url(), - URL(c="cap", f=fn, args=[alert_id]), - ) - update["web"] = web - else: - web = None + def model(self): - form_vars_get = form_vars.get + T = current.T - # Defaults for relevance date fields - form_effective = form_vars_get("effective", current.request.utcnow) - form_expires = form_vars_get("expires", cap_expirydate()) - form_onset = form_vars_get("onset", form_effective) + crud_strings = current.response.s3.crud_strings - update.update({"effective": form_effective, - "expires": form_expires, - "onset": form_onset, - }) + define_table = self.define_table + configure = self.configure - if update: - info.update_record(**update) + alert_id = self.cap_alert_id + info_id = self.cap_info_id - # Sync data in all other info-records of the same alert - idata = {"category" : form_vars_get("category"), - "certainty" : form_vars_get("certainty"), - "effective" : form_effective, - "event_type_id" : form_vars_get("event_type_id"), - "expires" : form_expires, - "onset" : form_onset, - "priority" : form_vars_get("priority"), - "response_type" : form_vars_get("response_type"), - "severity" : form_vars_get("severity"), - "urgency" : form_vars_get("urgency"), - } - if web: - idata["web"] = web - query = (itable.alert_id == alert_id) & \ - (itable.id != info_id) & \ - (itable.deleted == False) - db(query).update(**idata) + # --------------------------------------------------------------------- + # CAP Resource segments + # + # - Resource elements sit inside the Info segment of CAP XML + # - However, in most cases these would be common across all Info + # segments of the same alert, so in the UI we link these + # primarily to the Alert but still allow to differentiate by Info + # + tablename = "cap_resource" + define_table(tablename, + alert_id(writable = False, + ), + # Only used for imports (in UI, resource are linked to alert): + info_id(readable = False, + writable = False, + ), + Field("is_template", "boolean", + default = False, + readable = False, + writable = False, + ), + self.super_link("doc_id", "doc_entity"), + Field("resource_desc", + label = T("Description"), + requires = IS_NOT_EMPTY(), + comment = DIV(_class="tooltip", + _title="%s|%s" % (T("The type and content of the resource file"), + T("The human-readable text describing the type and content, such as 'map' or 'photo', of the resource file."), + ), + ), + ), + # Using image field instead of doc_id because the + # CropWidget doesn't work in inline forms + Field("image", "upload", + label = T("Image"), + length = current.MAX_FILENAME_LENGTH, + represent = self.doc_image_represent, + requires = IS_EMPTY_OR(IS_IMAGE(maxsize=(800, 800), + error_message=\ +T("Upload an image file(bmp, gif, jpeg or png), max. 800x800 pixels!"))), + uploadfolder = os.path.join(current.request.folder, + "uploads", "images"), + widget = S3ImageCropWidget((800, 800)), + comment = DIV(_class="tooltip", + _title="%s|%s" % (T("Image"), + T("Attach an image that provides extra information about the event"), + ), + ), + ), + Field("mime_type", + requires = IS_NOT_EMPTY(), + readable = False, + writable = False, + comment = DIV(_class="tooltip", + _title="%s|%s" % (T("The identifier of the MIME content type and sub-type describing the resource file"), + T("MIME content type and sub-type as described in [RFC 2046]. (As of this document, the current IANA registered MIME types are listed at http://www.iana.org/assignments/media-types/)"), + ), + ), + ), + Field("size", "integer", + label = T("Size in Bytes"), + readable = False, + writable = False, + comment = DIV(_class="tooltip", + _title="%s|%s" % (T("The integer indicating the size of the resource file"), + T("Approximate size of the resource file in bytes."), + ), + ), + ), + Field("uri", + label = T("Link to any resources"), + requires = IS_EMPTY_OR(IS_URL()), + comment = DIV(_class="tooltip", + _title="%s|%s" % (T("The identifier of the hyperlink for the resource file"), + T("A full absolute URI, typically a Uniform Resource Locator that can be used to retrieve the resource over the Internet."), + ), + ), + ), + #Field("file", "upload"), + Field("deref_uri", "text", + readable = False, + writable = False, + comment = DIV(_class="tooltip", + _title="%s|%s" % (T("Deref URI"), + T("The base-64 encoded data content of the resource file"), + ), + ), + ), + Field("digest", + writable = False, + comment = DIV(_class="tooltip", + _title="%s|%s" % (T("The code representing the digital digest ('hash') computed from the resource file"), + T("Calculated using the Secure Hash Algorithm (SHA-1)."), + ), + ), + ), + *s3_meta_fields()) - # ------------------------------------------------------------------------- - @staticmethod - def cap_info_onvalidation(form): - """ - Custom Form Validation: - used for import from CSV - """ + # CRUD Form + crud_form = S3SQLCustomForm("alert_id", + "info_id", + "is_template", + "resource_desc", + "uri", + "image", + "mime_type", + "size", + # The CropWidget doesn't work in inline forms + #S3SQLInlineComponent( + # "image", + # label = T("Image"), + # fields = [("", "file")], + # comment = DIV(_class = "tooltip", + # _title = "%s|%s" % (T("Image"), + # T("Attach an image that provides extra information about the event."), + # ), + # ), + # ), + S3SQLInlineComponent( + "document", + label = T("Document"), + fields = [("", "file")], + comment = DIV(_class="tooltip", + _title="%s|%s" % (T("Document"), + T("Attach document that provides extra information about the event."), + ), + ), + ), + ) - T = current.T - itable = current.db.cap_info - irow = current.db((itable.alert_id == form.request_vars.alert_id) & \ - (itable.id != form.request_vars.id)).\ - select(itable.language, - limitby=(0, 1)).first() - # Check if both info segments are for same language - if irow and irow.language == form.vars.language: - form.errors["language"] = \ - T("Please edit already created info segment with same language!") + # List Fields + list_fields = ["resource_desc", + "uri", + "image", + "document.file", + ] - form_record = form.record - if form_record and form_record.is_template == False: - form_vars = form.vars - #parameters = json.loads(form_vars.get("parameter")) - #if parameters: - # for parameter in parameters: - # if (parameter["key"] and not parameter["value"]) or \ - # (parameter["value"] and not parameter["key"]): - # form.errors["parameter"] = \ - # current.T("Name-Value Pair is incomplete.") + # Table Configuration + configure(tablename, + crud_form = crud_form, + list_fields = list_fields, + onaccept = self.resource_onaccept, + onvalidation = self.resource_onvalidation, + super_entity = "doc_entity", + ) - if not form_vars.get("urgency"): - form.errors["urgency"] = T("'Urgency' field is mandatory") - if not form_vars.get("severity"): - form.errors["severity"] = T("'Severity' field is mandatory") - if not form_vars.get("certainty"): - form.errors["certainty"] = T("'Certainty' field is mandatory") + # CRUD Strings + crud_strings[tablename] = Storage( + label_create = T("Add Resource"), + title_display = T("Alert Resource"), + title_list = T("Resources"), + title_update = T("Edit Resource"), + subtitle_list = T("List Resources"), + label_list_button = T("List Resources"), + label_delete_button = T("Delete Resource"), + msg_record_created = T("Resource added"), + msg_record_modified = T("Resource updated"), + msg_record_deleted = T("Resource deleted"), + msg_list_empty = T("No resources currently defined for this alert"), + ) - if not form_vars.get("category"): - form.errors["category"] = T("At least one category is required.") + # --------------------------------------------------------------------- + # Pass names back to global scope (s3.*) + return {} # ------------------------------------------------------------------------- @staticmethod - def cap_alert_approve(record=None): + def defaults(): """ - Update the approved_on field when alert gets approved + Return safe defaults in case the model has been deactivated. """ - if not record: - return - - alert_id = record["id"] - - # Update approved_on at the time the alert is approved - # @ToDo: update approved_on when approval is not required - # i.e. we allow editors to be self approver - if alert_id: - db = current.db - table = db.cap_alert - query = table.id == alert_id - utcnow = current.request.utcnow - db(query).update(approved_on=utcnow, sent=utcnow) - - # Notify the owner of the record about approval - row = db(query).select(table.owned_by_user, - limitby=(0, 1)).first() - if row.owned_by_user: - settings = current.deployment_settings - pe_id = current.auth.s3_user_pe_id(int(row.owned_by_user)) - subject = "%s: Alert Approved" % settings.get_system_name_short() - url = "%s%s" % (settings.get_base_public_url(), - URL(c="cap", f="alert", args=[alert_id])) - message = current.T("This alert that you requested to review has been approved:\n\n%s") % url - current.msg.send_by_pe_id(pe_id, - subject, - message, - alert_id=alert_id, - ) - - # Record the approved alert in history table without external references - clone(current.request, record) - - # ------------------------------------------------------------------------- - @staticmethod - def cap_info_duplicate(item): - - data = item.data - alert_id = data.get("alert_id") - # Default Info Language - language = data.get("language", "en-US") - - if alert_id and language: - table = item.table - query = (table.alert_id == alert_id) & (table.language == language) - duplicate = current.db(query).select(table.id, - limitby=(0, 1)).first() - if duplicate: - item.id = duplicate.id - item.method = item.METHOD.UPDATE + return {} # ------------------------------------------------------------------------- @staticmethod - def cap_info_parameter_onaccept(form): + def resource_onvalidation(form): """ - Link alert_id to cap_info_parameter table + Resource form validation: + - post-process S3ImageCropWidget + - Determine MIME type and file size of uploaded image + + @param form: the FORM """ form_vars = form.vars - info_id = form_vars.get("info_id", None) - if not info_id: - return - - db = current.db - itable = db.cap_info - irow = db(itable.id == info_id).select(itable.alert_id, - limitby=(0, 1)).first() - alert_id = irow.alert_id - if alert_id: - db(db.cap_info_parameter.id == form_vars["id"]).update(alert_id = alert_id) + image = form_vars.image + if image is None: - # ------------------------------------------------------------------------- - @staticmethod - def cap_info_parameter_onvalidation(form): - """ - Custom Form Validation - """ + encoded_file = form_vars.get("imagecrop-data") + if encoded_file: + # Post-process S3ImageCropWidget (see document_onvalidation) + # - differs from document_onvalidation in that it also + # determines file size and MIME type + metadata, encoded_file = encoded_file.split(",") + filename, datatype = metadata.split(";")[:2] - form_vars = form.vars - parameter_name = form_vars.get("name") - parameter_value = form_vars.get("value") - if (parameter_name and not parameter_value) or \ - (parameter_value and not parameter_name): - form.errors["name"] = current.T("Name-Value Pair is incomplete.") + import uuid + import base64 + from cStringIO import StringIO - # ------------------------------------------------------------------------- - @staticmethod - def cap_area_duplicate(item): + image = Storage(filename = uuid.uuid4().hex + filename) + image.file = stream = StringIO(base64.decodestring(encoded_file)) - data = item.data - name = data.get("name") + form_vars.image = image - if name is not None: - table = item.table - event_type_id = data.get("event_type_id", None) - if event_type_id is not None: - # This is a template - query = (table.name == name) & \ - (table.event_type_id == event_type_id) - else: - alert_id = data.get("alert_id", None) - info_id = data.get("info_id", None) - query_ = (table.name == name) - if alert_id is not None: - # Real Alert, not template - query = query_ & (table.alert_id == alert_id) - elif info_id is not None: - # CAP XML Import - query = query_ & (table.info_id == info_id) - else: - # Nothing we can use - return + # Determine file size + stream.seek(0, os.SEEK_END) + form_vars.size = stream.tell() + stream.seek(0) - duplicate = current.db(query).select(table.id, - limitby=(0, 1)).first() - if duplicate: - item.id = duplicate.id - item.method = item.METHOD.UPDATE + # Determine MIME type + form_vars.mime_type = datatype.split(":")[1] # ------------------------------------------------------------------------- @staticmethod - def cap_area_onaccept(form): + def resource_onaccept(form): """ - Link alert_id for CAP XML import + Onaccept-routine for resources + - inherit alert_id from info segment if not set in form + + @param form: the FORM """ form_vars = form.vars - if form_vars.get("event_type_id"): - # Predefined Area - return - db = current.db - alert_id = form_vars.get("alert_id", None) + + alert_id = form_vars.get("alert_id") if not alert_id: - info_id = form_vars.get("info_id", None) + info_id = form_vars.get("info_id") if info_id: - # CAP XML - # Add the alert_id to this component of component - # to make it a direct component for UI purposes + # Get the info-segment's alert ID itable = db.cap_info - item = db(itable.id == info_id).select(itable.alert_id, - limitby=(0, 1)).first() - alert_id = item.alert_id or None - - if alert_id: - db(db.cap_area.id == form_vars.id).update(alert_id = alert_id) - - # ------------------------------------------------------------------------- - @staticmethod - def cap_area_onvalidation(form): + info = db(itable.id == info_id).select(itable.alert_id, + limitby = (0, 1), + ).first() + alert_id = info.alert_id if info else None - form_vars = form.vars - if form_vars.get("ceiling") and not form_vars.get("altitude"): - form.errors["altitude"] = \ - current.T("'Altitude' field is mandatory if using 'Ceiling' field.") + if alert_id: + # Set alert_id in cap_area record + db(db.cap_resource.id == form_vars.id).update(alert_id=alert_id) - if "event_type_id" in form_vars and form_vars.get("event_type_id") is None: - form.errors["event_type_id"] = \ - current.T("'Event Type' field is mandatory for Predefined Area.") +# ============================================================================= +class CAPWarningPriorityModel(S3Model): - # ------------------------------------------------------------------------- - @staticmethod - def cap_area_location_onaccept(form): - """ - - Link alert_id for non-template area - - for external alerts (from import feed or rss), make sure the - locations are only imported if we set polygons to be imported - """ + names = ("cap_warning_priority", + "cap_warning_priority_id", + ) - form_vars = form.vars + def model(self): - area_id = form_vars.get("area_id", None) - if not area_id: - # comes from assign method - return + T = current.T db = current.db - alert_id = form_vars.get("alert_id", None) - if not alert_id: - atable = db.cap_area - row = db(atable.id == area_id).select(atable.alert_id, - limitby=(0, 1)).first() - alert_id = row.alert_id or None - if alert_id: - # This is not template area - # NB Template area are not linked with alert_id - location_default = current.deployment_settings.get_cap_area_default() - if "polygon" not in location_default: - # Use-case for PH - table = current.s3db.cap_alert - row = db(table.id == alert_id).select(table.external, - limitby=(0, 1)).first() - if row.external: - # If "polygon" is not in deployment_settings (eg. in PH) and - # if it is a external alert (coming from RSS and import_feed), - # don't use this for displaying map in area - db(db.cap_area_location.id == form_vars["id"]).delete() - return - # For alerts generated from SAMBRO, link area to the alert_id - db(db.cap_area_location.id == form_vars["id"]).update(alert_id = alert_id) - - # ------------------------------------------------------------------------- - @staticmethod - def cap_area_tag_onaccept(form): - """ - Link location if area_tag has SAME code - Link alert_id for non-template area - """ + crud_strings = current.response.s3.crud_strings - form_vars = form.vars + cap_options = get_cap_options() - area_id = form_vars.get("area_id", None) - if not area_id: - # comes from assign method - return + # --------------------------------------------------------------------- + # Warning Priorities + # - map event-type-specific custom priority classes to combinations + # of CAP urgency, severity and certainty + # + tablename = "cap_warning_priority" + self.define_table(tablename, + Field("priority_rank", "integer", + label = T("Priority Sequence"), + length = 2, + comment = DIV(_class="tooltip", + _title="%s|%s" % (T("Priority Rank"), + T("The Priority Rank is basically to give it a ranking 1, 2, ..., n. That way we know 1 is the most important of the chain and n is lowest element. For eg. (1, Signal 1), (2, Signal 2)..., (5, Signal 5) to enumerate the priority for cyclone."), + ), + ), + ), + Field("event_code", + label = T("Event Code"), + comment = DIV(_class="tooltip", + _title="%s|%s" % (T("Event Code"), + T("Code (key) for the event like for eg. (2001, Typhoon), (2002, Flood)"), + ), + ), + ), + Field("name", notnull=True, length=64, unique=True, + label = T("Name"), + requires = [IS_LENGTH(64), + IS_NOT_ONE_OF(db, "%s.name" % tablename), + IS_MATCH('^[^"\']+$', + error_message=T('Name cannot be empty and Must not include " or (\')'), + ), + ], + comment = DIV(_class="tooltip", + _title="%s|%s" % (T("Name"), + T("The actual name for the warning priority, for eg. Typhoons in Philippines have five priority names (PSWS# 1, PSWS# 2, PSWS# 3, PSWS# 4 and PSWS# 5)"), + ), + ), + ), + self.event_type_id(empty=False, + ondelete = "SET NULL", + label = T("Event Type"), + comment = DIV(_class="tooltip", + _title="%s|%s" % (T("Event Type"), + T("The Event to which this priority is targeted for."), + ), + ), + ), + Field("urgency", + label = T("Urgency"), + requires = IS_IN_SET(cap_options["urgency"]), + comment = DIV(_class="tooltip", + _title="%s|%s" % (T("Urgency"), + T("Denotes the urgency of the subject event of the alert message"), + ), + ), + ), + Field("severity", + label = T("Severity"), + requires = IS_IN_SET(cap_options["severity"]), + comment = DIV(_class="tooltip", + _title="%s|%s" % (T("Severity"), + T("Denotes the severity of the subject event of the alert message"), + ), + ), + ), + Field("certainty", + label = T("Certainty"), + requires = IS_IN_SET(cap_options["certainty"]), + comment = DIV(_class="tooltip", + _title="%s|%s" % (T("Certainty"), + T("Denotes the certainty of the subject event of the alert message"), + ), + ), + ), + Field("color_code", + label = T("Color Code"), + represent = self.color_code_represent, + widget = S3ColorPickerWidget(options = {"showPaletteOnly": False, + }, + ), + comment = DIV(_class="tooltip", + _title="%s|%s" % (T("The color code for this priority"), + T("Pick from the color widget the color that is associated to this priority of the event. The color code is in hex format"), + ), + ), + ), + # Field to record the last checked time for the table + # - used to sent notifications to subscribers about available options + s3_datetime("last_checked", + readable = False, + writable = False, + ), + *s3_meta_fields()) - db = current.db - atable = db.cap_area + # List Fields + list_fields = ["event_type_id", + (T("Color"), "color_code"), + "name", + (T("Rank"), "priority_rank"), + ] - arow = db(atable.id == area_id).select(atable.alert_id, - limitby=(0, 1)).first() - alert_id = arow.alert_id + # Table Configuration + self.configure(tablename, + onaccept = self.warning_priority_onaccept, + # Not needed since unique=True + #deduplicate = S3Duplicate(primary=("event_type_id", "name")), + list_fields = list_fields, + #onvalidation = self.warning_priority_onvalidation, + orderby = "cap_warning_priority.event_type_id,cap_warning_priority.priority_rank desc", + ) - if alert_id: - # This is not template area - db(db.cap_area_tag.id == form_vars.id).update(alert_id = alert_id) + # CRUD Strings + crud_strings[tablename] = Storage( + label_create = T("Create Warning Classification"), + title_display = T("Warning Classification Details"), + title_list = T("Warning Classifications"), + title_update = T("Edit Warning Classification"), + title_upload = T("Import Warning Classifications"), + label_list_button = T("List Warning Classifications"), + label_delete_button = T("Delete Warning Classification"), + msg_record_created = T("Warning Classification added"), + msg_record_modified = T("Warning Classification updated"), + msg_record_deleted = T("Warning Classification removed"), + msg_list_empty = T("No Warning Classifications currently registered"), + ) - s3db = current.s3db - tag = form_vars.get("tag") - same_code = current.deployment_settings.get_cap_same_code() - import_location = False - if tag and same_code: - # Check if tag and same_code exist. If neither one or none - # exists, do not import location - if tag == "SAME": - # If tag is "SAME" then check other conditions, otherwise - # do not import location - if "geocode" in current.deployment_settings.get_cap_area_default(): - import_location = True - else: - # Check internal alert - table = s3db.cap_alert - row = db(table.id == alert_id).select(table.external, - limitby=(0, 1)).first() - if not row.external: - import_location = True - - if import_location: - # SAME tag refers to some location_id in CAP - ttable = s3db.gis_location_tag - gtable = s3db.gis_location + # Reusable Field + represent = S3Represent(lookup=tablename, translate=True) + priority_id = S3ReusableField("priority", "reference %s" % tablename, + label = T("Priority"), + represent = represent, + requires = IS_EMPTY_OR( + IS_ONE_OF(db, "%s.id" % tablename, + represent + )), + ) - # It is possible for there to be two polygons for the same SAME - # code since polygon change over time, even if the code remains - # the same. Hence the historic polygon is excluded as (gtable.end_date == None) - tquery = (ttable.tag == same_code) & \ - (ttable.value == form_vars.get("value")) & \ - (ttable.deleted != True) & \ - (ttable.location_id == gtable.id) & \ - (gtable.end_date == None) & \ - (gtable.deleted != True) - trow = db(tquery).select(ttable.location_id, limitby=(0, 1)).first() - if trow and trow.location_id: - # Match - ltable = db.cap_area_location - ldata = {"area_id": area_id, - "alert_id": alert_id or None, - "location_id": trow.location_id, - } - lid = ltable.insert(**ldata) - current.auth.s3_set_record_owner(ltable, lid) - # Uncomment this when required - #ldata["id"] = lid - #s3db.onaccept(ltable, ldata) + # --------------------------------------------------------------------- + # Pass names back to global scope (s3.*) + return {"cap_warning_priority_id": priority_id, + } # ------------------------------------------------------------------------- @staticmethod - def cap_resource_onaccept(form): + def defaults(): """ - Link alert_id for CAP XML import + Return safe defaults in case the model has been deactivated. """ - form_vars = form.vars - - info_id = form_vars.get("info_id", None) - if info_id: - # CAP XML - # Add the alert_id to this component of component - # to make it a direct component for UI purposes - db = current.db - itable = db.cap_info - item = db(itable.id == info_id).select(itable.alert_id, - limitby=(0, 1)).first() - alert_id = item.alert_id - if alert_id: - db(db.cap_resource.id == form_vars.id).update(alert_id = alert_id) + return {"cap_warning_priority_id": S3ReusableField("priority", "integer", + readable = False, + writable = False, + ), + } # ------------------------------------------------------------------------- @staticmethod - def cap_resource_onvalidation(form): + def warning_priority_onvalidation(form): """ - For Image Upload - NB not using document_onvalidation here because we are extracting - other values from the file like size and mime type + Form validation for warning priorities: + - check for duplicates + - currently unused (TODO: document why) + + @param form: the FORM """ form_vars = form.vars - image = form_vars.image - if image is None: - encoded_file = form_vars.get("imagecrop-data", None) - if encoded_file: - import base64 - import uuid - import cStringIO - metadata, encoded_file = encoded_file.split(",") - #filename, datatype, enctype = metadata.split(";") - filename, datatype = metadata.split(";")[:2] - f = Storage() - f.filename = uuid.uuid4().hex + filename - f.file = cStringIO.StringIO(base64.decodestring(encoded_file)) - form_vars.image = image = f - stream = image.file - stream.seek(0, os.SEEK_END) - file_size = stream.tell() - stream.seek(0) - - # extract mime_type - if image is not None: - #data, mime_type = datatype.split(":") - form_vars.size = file_size - form_vars.mime_type = datatype.split(":")[1] - - elif isinstance(image, str): - # Image = String => Update not a create, so file not in form - return + table = current.s3db.cap_warning_priority + query = (table.event_type_id == form_vars.event_type_id) & \ + (table.urgency == form_vars.urgency) & \ + (table.severity == form_vars.severity) & \ + (table.certainty == form_vars.certainty) & \ + (table.deleted == False) + row = current.db(query).select(table.id, limitby=(0, 1)).first() + if row: + form.errors["event_type_id"] = current.T("This combination of the 'Event Type', 'Urgency', 'Certainty' and 'Severity' is already available in database.") # ------------------------------------------------------------------------- @staticmethod - def cap_warning_priority_onaccept(form): + def warning_priority_onaccept(form): + """ + Onaccept-routine for warning priorities + - add or update the fill rule (color) to all known CAP map styles + """ form_vars = form.vars + + name = form_vars.name color_code = form_vars.color_code - if color_code: + + if name and color_code: + db = current.db s3db = current.s3db + + # The style ryle for this priority + pstyle = {"prop": "priority", + "cat": name, + "fill": color_code, + "fillOpacity": 0.4, + } + + # Get map styles for all layers with controller=="cap" stable = s3db.gis_style ftable = s3db.gis_layer_feature query = (ftable.controller == "cap") & \ (ftable.layer_id == stable.layer_id) rows = db(query).select(stable.id, stable.style) - if rows: - name = form_vars.name - for row in rows: - style = row.style - if style: - sdata = dict(prop = "priority", - fill = color_code, - fillOpacity = 0.4, - cat = name, - ) - if sdata not in style: - style.append(sdata) - db(stable.id == row.id).update(style = style) -# ============================================================================= -def cap_expirydate(): - """ - Default Expiry date based on the expire offset - """ + for row in rows: - return current.request.utcnow + \ - datetime.timedelta(days = current.deployment_settings.\ - get_cap_expire_offset()) + found = False -# ============================================================================= -def warning_priority_color(color_code): - """ - Shows actual color for hex color code + style = row.style # Usually a list (JSON array), but could be empty - @param color_code: hex color code - """ + if type(style) is list: + # If there is already a style rule for this priority, + # update it, otherwise append it to the list + for rule in style: + if rule.get("prop") == "priority" and \ + rule.get("cat") == name: + rule.update(pstyle) + found = True + if not found: + style.append(pstyle) + else: + # No rules in map style yet => start a new list + style = [pstyle] + + # Update the map style + row.update_record(style=style) + + # ------------------------------------------------------------------------- + @staticmethod + def color_code_represent(color_code): + """ + Represent a hex color code as a colored DIV + + @param color_code: the color code - if not color_code: - return current.messages["NONE"] + @returns: the representation XML + """ + + if not color_code: + output = current.messages["NONE"] + else: + style = "width:%(size)s;height:%(size)s;background-color:#%(color)s;" + output = DIV(_style = style % {"size": "2em", "color": color_code}) - return DIV(_style="width:%(size)s;height:%(size)s;background-color:#%(color)s;" % \ - dict(size="2em", color=color_code)) + return output # ============================================================================= -class S3CAPHistoryModel(S3Model): +class CAPHistoryModel(S3Model): + """ TODO docstring (what is this used for?) """ names = ("cap_alert_history", "cap_info_history", @@ -2379,20 +2845,32 @@ class S3CAPHistoryModel(S3Model): def model(self): T = current.T + db = current.db settings = current.deployment_settings - - cap_options = get_cap_options() - add_components = self.add_components - configure = self.configure crud_strings = current.response.s3.crud_strings + define_table = self.define_table - #UNKNOWN_OPT = current.messages.UNKNOWN_OPT + configure = self.configure + add_components = self.add_components + + cap_options = get_cap_options() + + incident_types = cap_options["incident_types"] + status_opts = cap_options["alert_status"] + msg_types = cap_options["msg_types"] + scopes = cap_options["scopes"] + + categories = cap_options["categories"] + response_types = cap_options["response_types"] + urgency_opts = cap_options["urgency"] + severity_opts = cap_options["severity"] + certainty_opts = cap_options["certainty"] # --------------------------------------------------------------------- # Alert History Table - # Stores Copy of approved alerts without any external references - + # - stores copies of approved alerts without any external references + # tablename = "cap_alert_history" define_table(tablename, Field("event_type", @@ -2402,78 +2880,89 @@ def model(self): label = T("Identifier"), requires = [IS_LENGTH(255), IS_MATCH(OIDPATTERN, - error_message=T("Cannot be empty and Must not include spaces, commas, or restricted characters (< and &)."), + error_message = T("Cannot be empty and Must not include spaces, commas, or restricted characters (< and &)."), ), ], - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("A unique identifier of the alert message"), - T("A number or string uniquely identifying this message, assigned by the sender. Must not include spaces, commas or restricted characters (< and &)."))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("A unique identifier of the alert message"), + T("A number or string uniquely identifying this message, assigned by the sender. Must not include spaces, commas or restricted characters (< and &)."), + ), + ), ), Field("incidents", "list:string", label = T("Incidents"), - represent = S3Represent(options = cap_options["cap_incident_type_opts"], - multiple = True), + represent = S3Represent(options = incident_types, + multiple = True, + ), requires = IS_EMPTY_OR( - IS_IN_SET(cap_options["cap_incident_type_opts"], + IS_IN_SET(incident_types, multiple = True, sort = True, )), - widget = S3MultiSelectWidget(selectedList = 10), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("A list of incident(s) referenced by the alert message"), - T("Used to collate multiple messages referring to different aspects of the same incident. If multiple incident identifiers are referenced, they SHALL be separated by whitespace. Incident names including whitespace SHALL be surrounded by double-quotes."))), + widget = S3MultiSelectWidget(selectedList=10), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("A list of incident(s) referenced by the alert message"), + T("Used to collate multiple messages referring to different aspects of the same incident. If multiple incident identifiers are referenced, they SHALL be separated by whitespace. Incident names including whitespace SHALL be surrounded by double-quotes."), + ), + ), ), Field("sender", label = T("Sender"), requires = IS_MATCH(OIDPATTERN, - error_message=T("Cannot be empty and Must not include spaces, commas, or restricted characters (< and &).")), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The identifier of the sender of the alert message"), - T("This is guaranteed by assigner to be unique globally; e.g., may be based on an Internet domain name. Must not include spaces, commas or restricted characters (< and &)."))), + error_message = T("Cannot be empty and Must not include spaces, commas, or restricted characters (< and &)."), + ), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("The identifier of the sender of the alert message"), + T("This is guaranteed by assigner to be unique globally; e.g., may be based on an Internet domain name. Must not include spaces, commas or restricted characters (< and &)."), + ), + ), ), s3_datetime("sent"), Field("status", label = T("Status"), - represent = S3Represent(options = cap_options["cap_alert_status_code_opts"], - ), - requires = IS_IN_SET(cap_options["cap_alert_status_code_opts"]), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Denotes the appropriate handling of the alert message"), - T("See options."))), + represent = S3Represent(options=status_opts), + requires = IS_IN_SET(status_opts), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("Denotes the appropriate handling of the alert message"), + T("See options."), + ), + ), ), Field("msg_type", label = T("Message Type"), - represent = S3Represent(options = cap_options["cap_alert_msg_type_code_opts"], - ), - requires = IS_EMPTY_OR( - IS_IN_SET(cap_options["cap_alert_msg_type_code_opts"]) - ), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The nature of the alert message"), - T("See options."))), + represent = S3Represent(options=msg_types), + requires = IS_EMPTY_OR(IS_IN_SET(msg_types)), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("The nature of the alert message"), + T("See options."), + ), + ), ), Field("source", label = T("Source"), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The text identifying the source of the alert message"), - T("The particular source of this alert; e.g., an operator or a specific device."))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("The text identifying the source of the alert message"), + T("The particular source of this alert; e.g., an operator or a specific device."), + ), + ), ), Field("scope", label = T("Scope"), - represent = S3Represent(options = cap_options["cap_alert_scope_code_opts"], - ), - requires = IS_EMPTY_OR( - IS_IN_SET(cap_options["cap_alert_scope_code_opts"]) - ), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Denotes the intended distribution of the alert message"), - T("Who is this alert for?"))), + represent = S3Represent(options=scopes), + requires = IS_EMPTY_OR(IS_IN_SET(scopes)), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("Denotes the intended distribution of the alert message"), + T("Who is this alert for?"), + ), + ), ), Field("restriction", "text", label = T("Restriction"), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The text describing the rule for limiting distribution of the restricted alert message"), - T("Used when scope is 'Restricted'."))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("The text describing the rule for limiting distribution of the restricted alert message"), + T("Used when scope is 'Restricted'."), + ), + ), ), Field("addresses", "list:string", label = T("Recipients"), @@ -2488,35 +2977,58 @@ def model(self): multiple = True, ), widget = S3MultiSelectWidget(), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The group listing of intended recipients of the alert message"), - T("Required when scope is 'Private', optional when scope is 'Public' or 'Restricted'. Each recipient shall be identified by an identifier or an address."))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("The group listing of intended recipients of the alert message"), + T("Required when scope is 'Private', optional when scope is 'Public' or 'Restricted'. Each recipient shall be identified by an identifier or an address."), + ), + ), ), Field("codes", "list:string", label = T("Codes"), - represent = self.list_string_represent, - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Codes for special handling of the message"), - T("Any user-defined flags or special codes used to flag the alert message for special handling."))), + represent = list_string_represent, + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("Codes for special handling of the message"), + T("Any user-defined flags or special codes used to flag the alert message for special handling."), + ), + ), ), Field("note", "text", label = T("Note"), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The text describing the purpose or significance of the alert message"), - T("The message note is primarily intended for use with status 'Exercise' and message type 'Error'"))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("The text describing the purpose or significance of the alert message"), + T("The message note is primarily intended for use with status 'Exercise' and message type 'Error'"), + ), + ), ), Field("reference", "text", label = T("Reference"), readable = False, - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The group listing identifying earlier message(s) referenced by the alert message"), - T("The extended message identifier(s) (in the form sender,identifier,sent) of an earlier CAP message or messages referenced by this one."))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("The group listing identifying earlier message(s) referenced by the alert message"), + T("The extended message identifier(s) (in the form sender,identifier,sent) of an earlier CAP message or messages referenced by this one."), + ), + ), ), s3_datetime("approved_on", readable = False, ), *s3_meta_fields()) + # Components + add_components(tablename, + cap_area_history = "alert_history_id", + cap_area_location_history = {"name": "location_history", + "joinby": "alert_history_id", + }, + cap_area_tag_history = {"name": "tag_history", + "joinby": "alert_history_id", + }, + cap_info_history = "alert_history_id", + cap_info_parameter_history = "alert_history_id", + cap_resource_history = "alert_history_id", + ) + + # List Fields list_fields = [(T("Event Type"), "info_history.event"), "msg_type", (T("Sent"), "sent"), @@ -2527,6 +3039,7 @@ def model(self): (T("Approved On"), "approved_on"), ] + # Filter Widgets filter_widgets = [ S3TextFilter(["identifier", "sender", @@ -2539,7 +3052,7 @@ def model(self): ), S3OptionsFilter("info_history.category", label = T("Category"), - options = cap_options["cap_info_category_opts"], + options = categories, hidden = True, ), S3OptionsFilter("info_history.event", @@ -2557,6 +3070,7 @@ def model(self): ), ] + # Table Configuration configure(tablename, deletable = False, editable = False, @@ -2566,220 +3080,279 @@ def model(self): orderby = "cap_info_history.expires desc", ) - # Components - add_components(tablename, - cap_area_history = "alert_history_id", - cap_area_location_history = {"name": "location_history", - "joinby": "alert_history_id", - }, - cap_area_tag_history = {"name": "tag_history", - "joinby": "alert_history_id", - }, - cap_info_history = "alert_history_id", - cap_info_parameter_history = "alert_history_id", - cap_resource_history = "alert_history_id", - ) - + # CRUD Strings crud_strings[tablename] = Storage( title_display = T("Alert History Details"), title_list = T("Alerts History"), label_list_button = T("List Alerts History"), - msg_list_empty = T("No alerts to show")) + msg_list_empty = T("No alerts to show"), + ) + # Reference Representation alert_history_represent = S3Represent(lookup = tablename, - fields = ["msg_type", "sent", "sender"], - field_sep = " - ") - + fields = ["msg_type", + "sent", + "sender", + ], + field_sep = " - ", + ) + + # Reusable Field alert_history_id = S3ReusableField("alert_history_id", "reference %s" % tablename, - comment = T("The alert message containing this information"), - label = T("Alert History"), - ondelete = "CASCADE", - represent = alert_history_represent, - requires = IS_EMPTY_OR( - IS_ONE_OF(db, "cap_alert_history.id", - alert_history_represent)), - ) + comment = T("The alert message containing this information"), + label = T("Alert History"), + ondelete = "CASCADE", + represent = alert_history_represent, + requires = IS_EMPTY_OR( + IS_ONE_OF(db, "cap_alert_history.id", + alert_history_represent, + )), + ) # --------------------------------------------------------------------- # CAP Info History Table + # + + # TODO: avoid changing settings in model, pass directly to + # validator via s3_language settings.L10n.extra_codes = [("en-US", "English"), ] + tablename = "cap_info_history" define_table(tablename, alert_history_id(readable = False, writable = False, ), s3_language(empty = False, - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Denotes the language of the information"), - T("Code Values: Natural language identifier per [RFC 3066]. If not present, an implicit default value of 'en-US' will be assumed. Edit settings.cap.languages in 000_config.py to add more languages. See here for a full list.") % "http://www.i18nguy.com/unicode/language-identifiers.html")), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("Denotes the language of the information"), + T("Code Values: Natural language identifier per [RFC 3066]. If not present, an implicit default value of 'en-US' will be assumed. Edit settings.cap.languages in 000_config.py to add more languages. See here for a full list.") % "http://www.i18nguy.com/unicode/language-identifiers.html", + ), + ), ), Field("category", "list:string", label = T("Category"), - represent = S3Represent(options = cap_options["cap_info_category_opts"], + represent = S3Represent(options = categories, multiple = True, ), - requires = IS_IN_SET(cap_options["cap_info_category_opts"], + requires = IS_IN_SET(categories, multiple = True, ), widget = S3MultiSelectWidget(selectedList = 10), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Denotes the category of the subject event of the alert message"), - T("You may select multiple categories by holding down control and then selecting the items."))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("Denotes the category of the subject event of the alert message"), + T("You may select multiple categories by holding down control and then selecting the items."), + ), + ), ), Field("event", label = T("Event"), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The text denoting the type of the subject event of the alert message"), - T("If not specified, will the same as the Event Type."))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("The text denoting the type of the subject event of the alert message"), + T("If not specified, will the same as the Event Type."), + ), + ), ), Field("response_type", "list:string", label = T("Response Type"), - represent = S3Represent(options = cap_options["cap_info_response_type_opts"], + represent = S3Represent(options = response_types, multiple = True, ), - requires = IS_IN_SET(cap_options["cap_info_response_type_opts"], - multiple = True), + requires = IS_IN_SET(response_types, + multiple = True, + ), widget = S3MultiSelectWidget(selectedList = 10), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Denotes the type of action recommended for the target audience"), - T("Multiple response types can be selected by holding down control and then selecting the items"))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("Denotes the type of action recommended for the target audience"), + T("Multiple response types can be selected by holding down control and then selecting the items"), + ), + ), ), Field("priority", label = T("Priority"), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Priority of the alert message"), - T("Defines the priority of the alert message. Selection of the priority automatically sets the value for 'Urgency', 'Severity' and 'Certainty'"))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("Priority of the alert message"), + T("Defines the priority of the alert message. Selection of the priority automatically sets the value for 'Urgency', 'Severity' and 'Certainty'"), + ), + ), ), Field("urgency", label = T("Urgency"), - represent = S3Represent(options = cap_options["cap_info_urgency_opts"], - ), - requires = IS_IN_SET(cap_options["cap_info_urgency_opts"]), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Denotes the urgency of the subject event of the alert message"), - T("The urgency, severity, and certainty of the information collectively distinguish less emphatic from more emphatic messages." + - "'Immediate' - Responsive action should be taken immediately" + - "'Expected' - Responsive action should be taken soon (within next hour)" + - "'Future' - Responsive action should be taken in the near future" + - "'Past' - Responsive action is no longer required" + - "'Unknown' - Urgency not known"))), + represent = S3Represent(options=urgency_opts), + requires = IS_IN_SET(urgency_opts), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("Urgency"), + T("Denotes the urgency of the subject event of the alert message"), + ), + ), ), Field("severity", label = T("Severity"), - represent = S3Represent(options = cap_options["cap_info_severity_opts"], - ), - requires = IS_IN_SET(cap_options["cap_info_severity_opts"]), + represent = S3Represent(options=severity_opts), + requires = IS_IN_SET(severity_opts), comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Denotes the severity of the subject event of the alert message"), - T("The urgency, severity, and certainty elements collectively distinguish less emphatic from more emphatic messages." + - "'Extreme' - Extraordinary threat to life or property" + - "'Severe' - Significant threat to life or property" + - "'Moderate' - Possible threat to life or property" + - "'Minor' - Minimal to no known threat to life or property" + - "'Unknown' - Severity unknown"))), + _title="%s|%s" % (T("Severity"), + T("Denotes the severity of the subject event of the alert message"), + ), + ), ), Field("certainty", label = T("Certainty"), - represent = S3Represent(options = cap_options["cap_info_certainty_opts"], - ), - requires = IS_IN_SET(cap_options["cap_info_certainty_opts"]), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Denotes the certainty of the subject event of the alert message"), - T("The urgency, severity, and certainty elements collectively distinguish less emphatic from more emphatic messages." + - "'Observed' - Determined to have occurred or to be ongoing" + - "'Likely' - Likely (p > ~50%)" + - "'Possible' - Possible but not likely (p <= ~50%)" + - "'Unlikely' - Not expected to occur (p ~ 0)" + - "'Unknown' - Certainty unknown"))), + represent = S3Represent(options=certainty_opts), + requires = IS_IN_SET(certainty_opts), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("Certainty"), + T("Denotes the certainty of the subject event of the alert message"), + ), + ), ), Field("audience", "text", label = T("Audience"), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Audience"), - T("The intended audience of the alert message"))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("Audience"), + T("The intended audience of the alert message"), + ), + ), ), Field("event_code", "text", label = T("Event Code"), represent = S3KeyValueWidget.represent, widget = S3KeyValueWidget(), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("A system-specific code identifying the event type of the alert message"), - T("Any system-specific code for events, in the form of key-value pairs. (e.g., SAME, FIPS, ZIP)."))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("A system-specific code identifying the event type of the alert message"), + T("Any system-specific code for events, in the form of key-value pairs. (e.g., SAME, FIPS, ZIP)."), + ), + ), ), s3_datetime("effective", label = T("Effective"), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The effective time of the information of the alert message"), - T("If not specified, the effective time shall be assumed to be the same the time the alert was sent."))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("The effective time of the information of the alert message"), + T("If not specified, the effective time shall be assumed to be the same the time the alert was sent."), + ), + ), ), s3_datetime("onset", label = T("Onset"), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Onset"), - T("The expected time of the beginning of the subject event of the alert message"))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("Onset"), + T("The expected time of the beginning of the subject event of the alert message"), + ), + ), ), s3_datetime("expires", label = T("Expires at"), #past = 0, - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The expiry time of the information of the alert message"), - T("If this item is not provided, each recipient is free to enforce its own policy as to when the message is no longer in effect."))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("The expiry time of the information of the alert message"), + T("If this item is not provided, each recipient is free to enforce its own policy as to when the message is no longer in effect."), + ), + ), ), Field("sender_name", label = T("Sender's name"), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The text naming the originator of the alert message"), - T("The human-readable name of the agency or authority issuing this alert."))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("The text naming the originator of the alert message"), + T("The human-readable name of the agency or authority issuing this alert."), + ), + ), ), Field("headline", label = T("Headline"), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The text headline of the alert message"), - T("A brief human-readable headline. Note that some displays (for example, short messaging service devices) may only present this headline; it should be made as direct and actionable as possible while remaining short. 160 characters may be a useful target limit for headline length."))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("The text headline of the alert message"), + T("A brief human-readable headline. Note that some displays (for example, short messaging service devices) may only present this headline; it should be made as direct and actionable as possible while remaining short. 160 characters may be a useful target limit for headline length."), + ), + ), ), Field("description", "text", label = T("Description"), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The subject event of the alert message"), - T("An extended human readable description of the hazard or event that occasioned this message."))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("The subject event of the alert message"), + T("An extended human readable description of the hazard or event that occasioned this message."), + ), + ), ), Field("instruction", "text", label = T("Instruction"), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The recommended action to be taken by recipients of the alert message"), - T("An extended human readable instruction to targeted recipients. If different instructions are intended for different recipients, they should be represented by use of multiple information blocks. You can use a different information block also to specify this information in a different language."))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("The recommended action to be taken by recipients of the alert message"), + T("An extended human readable instruction to targeted recipients. If different instructions are intended for different recipients, they should be represented by use of multiple information blocks. You can use a different information block also to specify this information in a different language."), + ), + ), ), Field("contact", "text", label = T("Contact information"), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Contact"), - T("The contact for follow-up and confirmation of the alert message"))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("Contact"), + T("The contact for follow-up and confirmation of the alert message"), + ), + ), ), Field("web", label = T("URL"), requires = IS_EMPTY_OR(IS_URL()), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("A URL associating additional information with the alert message"), - T("A full, absolute URI for an HTML page or other text resource with additional or reference information regarding this alert."))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("A URL associating additional information with the alert message"), + T("A full, absolute URI for an HTML page or other text resource with additional or reference information regarding this alert."), + ), + ), ), #Field("parameter", "text", # label = T("Parameters"), # represent = S3KeyValueWidget.represent, # widget = S3KeyValueWidget(key_label = T("Name")), - # comment = DIV(_class="tooltip", - # _title="%s|%s" % (T("A system-specific additional parameter associated with the alert message"), - # T("Any system-specific datum, in the form of key-value pairs."))), + # comment = DIV(_class = "tooltip", + # _title = "%s|%s" % (T("A system-specific additional parameter associated with the alert message"), + # T("Any system-specific datum, in the form of key-value pairs."), + # ), + # ), # ), *s3_meta_fields()) - crud_strings[tablename] = Storage( - title_display = T("Alert information History"), - title_list = T("Information History entries"), - subtitle_list = T("Listing of alert information History items"), - label_list_button = T("List information History entries"), - msg_list_empty = T("No alert information to show")) + # Components + add_components(tablename, + cap_info_parameter_history = "info_history_id", + ) + + # CRUD Form + crud_form = S3SQLCustomForm("alert_history_id", + "language", + "category", + "event", + "response_type", + "priority", + "urgency", + "severity", + "certainty", + "audience", + "event_code", + "effective", + "onset", + "expires", + "sender_name", + "headline", + "description", + "instruction", + "contact", + "web", + S3SQLInlineComponent( + "info_parameter_history", + name = "info_parameter_history", + label = T("Parameter"), + fields = ["name", + "value", + "mobile", + ], + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("A system-specific additional parameter associated with the alert message"), + T("Any system-specific datum, in the form of key-value pairs."), + ), + ), + ), + ) + # List Fields list_fields = ["language", "category", (T("Event Type"), "event"), @@ -2791,68 +3364,44 @@ def model(self): "sender_name", ] + # Reference Representation info_history_represent = S3Represent(lookup = tablename, fields = ["language", "headline"], - field_sep = " - ") + field_sep = " - ", + ) + # Reusable Field info_history_id = S3ReusableField("info_history_id", "reference %s" % tablename, label = T("Information History Segment"), ondelete = "CASCADE", represent = info_history_represent, requires = IS_EMPTY_OR( IS_ONE_OF(db, "cap_info_history.id", - info_history_represent) - ), + info_history_represent, + )), ) - crud_form = S3SQLCustomForm("alert_history_id", - "language", - "category", - "event", - "response_type", - "priority", - "urgency", - "severity", - "certainty", - "audience", - "event_code", - "effective", - "onset", - "expires", - "sender_name", - "headline", - "description", - "instruction", - "contact", - "web", - S3SQLInlineComponent("info_parameter_history", - name = "info_parameter_history", - label = T("Parameter"), - fields = ["name", - "value", - "mobile", - ], - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("A system-specific additional parameter associated with the alert message"), - T("Any system-specific datum, in the form of key-value pairs."))), - ), - ) + # CRUD Strings + crud_strings[tablename] = Storage( + title_display = T("Alert information History"), + title_list = T("Information History entries"), + subtitle_list = T("Listing of alert information History items"), + label_list_button = T("List information History entries"), + msg_list_empty = T("No alert information to show"), + ) + # Table Configuration configure(tablename, crud_form = crud_form, deletable = False, editable = False, insertable = False, - list_fields = list_fields, - ) - - # Components - add_components(tablename, - cap_info_parameter_history = "info_history_id", - ) + list_fields = list_fields, + ) # --------------------------------------------------------------------- # CAP Info Parameters History + # tablename = "cap_info_parameter_history" define_table(tablename, alert_history_id(), @@ -2870,6 +3419,7 @@ def model(self): ), *s3_meta_fields()) + # Table Configuration configure(tablename, deletable = False, editable = False, @@ -2878,6 +3428,7 @@ def model(self): # --------------------------------------------------------------------- # CAP Area segments history table + # tablename = "cap_area_history" define_table(tablename, alert_history_id(readable = False, @@ -2886,62 +3437,77 @@ def model(self): Field("name", label = T("Area Description"), required = True, - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The affected area of the alert message"), - T("A text description of the affected area."))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("The affected area of the alert message"), + T("A text description of the affected area."), + ), + ), ), Field("altitude", "integer", label = T("Altitude"), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The specific or minimum altitude of the affected area"), - T("If used with the ceiling element this value is the lower limit of a range. Otherwise, this value specifies a specific altitude. The altitude measure is in feet above mean sea level."))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("The specific or minimum altitude of the affected area"), + T("If used with the ceiling element this value is the lower limit of a range. Otherwise, this value specifies a specific altitude. The altitude measure is in feet above mean sea level."), + ), + ), ), Field("ceiling", "integer", label = T("Ceiling"), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The maximum altitude of the affected area"), - T("must not be used except in combination with the 'altitude' element. The ceiling measure is in feet above mean sea level."))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("The maximum altitude of the affected area"), + T("must not be used except in combination with the 'altitude' element. The ceiling measure is in feet above mean sea level."), + ), + ), ), *s3_meta_fields()) - # CRUD Strings - crud_strings[tablename] = Storage( - title_display = T("Alert Area History"), - title_list = T("Areas History"), - subtitle_list = T("List Areas History"), - label_list_button = T("List Areas History"), - msg_list_empty = T("No areas currently defined for this alert")) - + # Components + add_components(tablename, + cap_area_location_history = {"name": "location_history", + "joinby": "area_history_id", + }, + cap_area_tag_history = {"name": "tag_history", + "joinby": "area_history_id", + }, + ) + # CRUD Form crud_form = S3SQLCustomForm("alert_history_id", "name", - S3SQLInlineComponent("location_history", - name = "location", - multiple = False, - fields = [("", "location")], - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Geolocation"), - T("The paired values of points defining a polygon that delineates the affected area of the alert message"))), - ), - S3SQLInlineComponent("tag_history", - name = "tag", - fields = ["tag", - "value", - ], - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The geographic code delineating the affected area"), - T("Any geographically-based code to describe a message target area, in the form. The key is a user-assigned string designating the domain of the code, and the content of value is a string (which may represent a number) denoting the value itself (e.g., name='ZIP' and value='54321'). This should be used in concert with an equivalent description in the more universally understood polygon and circle forms whenever possible."))), - ), + S3SQLInlineComponent( + "location_history", + name = "location", + multiple = False, + fields = [("", "location")], + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("Geolocation"), + T("The paired values of points defining a polygon that delineates the affected area of the alert message"), + ), + ), + ), + S3SQLInlineComponent( + "tag_history", + name = "tag", + fields = ["tag", + "value", + ], + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("The geographic code delineating the affected area"), + T("Any geographically-based code to describe a message target area, in the form. The key is a user-assigned string designating the domain of the code, and the content of value is a string (which may represent a number) denoting the value itself (e.g., name='ZIP' and value='54321'). This should be used in concert with an equivalent description in the more universally understood polygon and circle forms whenever possible."), + ), + ), + ), "altitude", "ceiling", ) - area_history_represent = cap_AreaRepresent(show_link=True) - + # List Fields list_fields = ["name", "altitude", "ceiling", ] + + # Table Configuration configure(tablename, crud_form = crud_form, deletable = False, @@ -2950,26 +3516,29 @@ def model(self): list_fields = list_fields, ) - # Components - add_components(tablename, - cap_area_location_history = {"name": "location_history", - "joinby": "area_history_id", - }, - cap_area_tag_history = {"name": "tag_history", - "joinby": "area_history_id", - }, - ) + # CRUD Strings + crud_strings[tablename] = Storage( + title_display = T("Alert Area History"), + title_list = T("Areas History"), + subtitle_list = T("List Areas History"), + label_list_button = T("List Areas History"), + msg_list_empty = T("No areas currently defined for this alert"), + ) + # Reusable Field + represent = cap_AreaRepresent(show_link=True) area_history_id = S3ReusableField("area_history_id", "reference %s" % tablename, label = T("Area"), ondelete = "CASCADE", - represent = area_history_represent, + represent = represent, requires = IS_ONE_OF(db, "cap_area_history.id", - area_history_represent), + represent, + ), ) # --------------------------------------------------------------------- # CAP Area Locations history table + # tablename = "cap_area_location_history" define_table(tablename, alert_history_id(readable = False, @@ -2979,22 +3548,25 @@ def model(self): Field("location_wkt", "text"), *s3_meta_fields()) + # Table Configuration + configure(tablename, + deletable = False, + editable = False, + insertable = False, + ) + # CRUD Strings crud_strings[tablename] = Storage( title_display = T("Alert Location History"), title_list = T("Locations History"), subtitle_list = T("List Locations History"), label_list_button = T("List Locations History"), - msg_list_empty = T("No locations currently defined for this alert")) - - configure(tablename, - deletable = False, - editable = False, - insertable = False, - ) + msg_list_empty = T("No locations currently defined for this alert"), + ) # --------------------------------------------------------------------- # CAP Area Tags history table + # tablename = "cap_area_tag_history" define_table(tablename, alert_history_id(readable = False, @@ -3010,6 +3582,7 @@ def model(self): s3_comments(), *s3_meta_fields()) + # Table Configuration configure(tablename, deletable = False, editable = False, @@ -3018,6 +3591,7 @@ def model(self): # --------------------------------------------------------------------- # CAP Resource History segments + # tablename = "cap_resource_history" define_table(tablename, alert_history_id(readable = False, @@ -3025,72 +3599,82 @@ def model(self): ), Field("resource_desc", requires = IS_NOT_EMPTY(), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The type and content of the resource file"), - T("The human-readable text describing the type and content, such as 'map' or 'photo', of the resource file."))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("The type and content of the resource file"), + T("The human-readable text describing the type and content, such as 'map' or 'photo', of the resource file."), + ), + ), ), Field("image", "upload", label = T("Image"), length = current.MAX_FILENAME_LENGTH, represent = self.doc_image_represent, - requires = IS_EMPTY_OR(IS_IMAGE(maxsize=(800, 800), - error_message=\ -T("Upload an image file(bmp, gif, jpeg or png), max. 800x800 pixels!"))), + requires = IS_EMPTY_OR( + IS_IMAGE(maxsize = (800, 800), + error_message = T("Upload an image file(bmp, gif, jpeg or png), max. 800x800 pixels!"), + )), uploadfolder = os.path.join(current.request.folder, - "uploads", "images"), + "uploads", + "images", + ), widget = S3ImageCropWidget((800, 800)), - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Image"), - T("Attach an image that provides extra information about the event"))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("Image"), + T("Attach an image that provides extra information about the event"), + ), + ), ), Field("mime_type", requires = IS_NOT_EMPTY(), readable = False, writable = False, - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The identifier of the MIME content type and sub-type describing the resource file"), - T("MIME content type and sub-type as described in [RFC 2046]. (As of this document, the current IANA registered MIME types are listed at http://www.iana.org/assignments/media-types/)"))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("The identifier of the MIME content type and sub-type describing the resource file"), + T("MIME content type and sub-type as described in [RFC 2046]. (As of this document, the current IANA registered MIME types are listed at http://www.iana.org/assignments/media-types/)"), + ), + ), ), Field("size", "integer", label = T("Size in Bytes"), readable = False, writable = False, - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The integer indicating the size of the resource file"), - T("Approximate size of the resource file in bytes."))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("The integer indicating the size of the resource file"), + T("Approximate size of the resource file in bytes."), + ), + ), ), Field("uri", label = T("Link to any resources"), requires = IS_EMPTY_OR(IS_URL()), readable = False, writable = False, - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The identifier of the hyperlink for the resource file"), - T("A full absolute URI, typically a Uniform Resource Locator that can be used to retrieve the resource over the Internet."))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("The identifier of the hyperlink for the resource file"), + T("A full absolute URI, typically a Uniform Resource Locator that can be used to retrieve the resource over the Internet."), + ), + ), ), Field("deref_uri", "text", readable = False, writable = False, - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("Deref URI"), - T("The base-64 encoded data content of the resource file"))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("Deref URI"), + T("The base-64 encoded data content of the resource file"), + ), + ), ), Field("digest", writable = False, - comment = DIV(_class="tooltip", - _title="%s|%s" % (T("The code representing the digital digest ('hash') computed from the resource file"), - T("Calculated using the Secure Hash Algorithm (SHA-1)."))), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("The code representing the digital digest ('hash') computed from the resource file"), + T("Calculated using the Secure Hash Algorithm (SHA-1)."), + ), + ), ), *s3_meta_fields()) - # CRUD Strings - crud_strings[tablename] = Storage( - title_display = T("Alert Resource History"), - title_list = T("Resources History"), - subtitle_list = T("List Resources History"), - label_list_button = T("List Resources History"), - msg_list_empty = T("No resources currently defined for this alert")) - + # CRUD Form crud_form = S3SQLCustomForm("alert_history_id", "resource_desc", "uri", @@ -3099,10 +3683,12 @@ def model(self): "size", ) + # List Fields list_fields = ["resource_desc", "image", ] + # Table Configuration configure(tablename, crud_form = crud_form, deletable = False, @@ -3111,20 +3697,34 @@ def model(self): list_fields = list_fields, ) + # CRUD Strings + crud_strings[tablename] = Storage( + title_display = T("Alert Resource History"), + title_list = T("Resources History"), + subtitle_list = T("List Resources History"), + label_list_button = T("List Resources History"), + msg_list_empty = T("No resources currently defined for this alert"), + ) + + # --------------------------------------------------------------------- + # Pass names back to global scope (s3.*) + return {} + # ------------------------------------------------------------------------- @staticmethod - def list_string_represent(string, fmt=lambda v: v): - try: - if isinstance(string, list): - return ", ".join([fmt(i) for i in string]) - elif isinstance(string, basestring): - return ", ".join([fmt(i) for i in string[1:-1].split("|")]) - except IndexError: - return current.messages.UNKNOWN_OPT - return "" + def defaults(): + """ + Return safe defaults in case the model has been deactivated. + """ + + return {} # ============================================================================= -class S3CAPAlertingAuthorityModel(S3Model): +class CAPAlertingAuthorityModel(S3Model): + """ + Model for known Alerting Authorities + - see http://alerting.worldweather.org + """ names = ("cap_alerting_authority", "cap_authority_feed_url", @@ -3134,11 +3734,16 @@ class S3CAPAlertingAuthorityModel(S3Model): def model(self): T = current.T - define_table = self.define_table + + db = current.db + crud_strings = current.response.s3.crud_strings + messages = current.messages + define_table = self.define_table + # --------------------------------------------------------------------- - # The data model resembles the data extracted from here http://alerting.worldweather.org/ + # Alerting authority # tablename = "cap_alerting_authority" define_table(tablename, @@ -3146,26 +3751,26 @@ def model(self): label = T("CAP OID"), requires = [IS_LENGTH(255), IS_MATCH(OIDPATTERN, - error_message=T("Cannot be empty and Must not include spaces, commas, or restricted characters (< and &).")), + error_message=T("Cannot be empty and Must not include spaces, commas, or restricted characters (< and &)."), + ), ], ), self.org_organisation_id( - requires = self.org_organisation_requires( - required=True, - ), - widget = None, - comment = S3PopupLink(c = "org", - f = "organisation", - label = T("Create Organization"), - title = messages.ORGANISATION, - ), - ), + empty = False, + widget = None, # Use simple drop-down + comment = S3PopupLink(c = "org", + f = "organisation", + label = T("Create Organization"), + title = messages.ORGANISATION, + ), + ), Field("country", length=2, label = T("Country"), represent = self.gis_country_code_represent, - requires = IS_EMPTY_OR(IS_IN_SET_LAZY( - lambda: current.gis.get_countries(key_type="code"), - zero=messages.SELECT_LOCATION)), + requires = IS_EMPTY_OR( + IS_IN_SET_LAZY(lambda: current.gis.get_countries(key_type="code"), + zero = messages.SELECT_LOCATION, + )), ), Field("authorisation", "text", label = T("Authorization Basis"), @@ -3175,77 +3780,84 @@ def model(self): s3_comments(), *s3_meta_fields()) - # CRUD Strings - self.crud_strings[tablename] = Storage( - label_create = T("Add Alerting Authority"), - title_display = T("Alerting Authority"), - title_list = T("Alerting Authorities"), - title_update = T("Edit Alerting Authority"), - subtitle_list = T("List Alerting Authorities"), - label_list_button = T("List Alerting Authorities"), - label_delete_button = T("Delete Alerting Authority"), - msg_record_created = T("Alerting Authority added"), - msg_record_modified = T("Alerting Authority updated"), - msg_record_deleted = T("Alerting Authority deleted"), - msg_list_empty = T("No Alerting Authority available")) - - list_fields = ["oid", - "organisation_id", - "country", - "feed_url.url", - "forecast_url.url", - ] - - alerting_authority_represent = S3Represent(lookup = tablename, - fields = ["organisation_id", "oid"], - field_sep = " - ") - - alerting_authority_id = S3ReusableField("alerting_authority_id", "reference %s" % tablename, - label = T("CAP Alerting Authority"), - ondelete = "CASCADE", - represent = alerting_authority_represent, - requires = IS_EMPTY_OR( - IS_ONE_OF(current.db, - "cap_alerting_authority.id", - alerting_authority_represent) - )) + # Components + self.add_components(tablename, + cap_authority_feed_url = {"name": "feed_url", + "joinby": "alerting_authority_id", + }, + cap_authority_forecast_url = {"name": "forecast_url", + "joinby": "alerting_authority_id", + }, + ) + # CRUD Form crud_form = S3SQLCustomForm("oid", "organisation_id", "country", "authorisation", S3SQLInlineComponent("feed_url", - name = "feed_url", - label = T("CAP Feed URL"), fields = [("", "url")], + label = T("CAP Feed URL"), + name = "feed_url", ), S3SQLInlineComponent("forecast_url", - name = "forecast_url", - label = T("Forecast URL"), fields = [("", "url")], - ) + label = T("Forecast URL"), + name = "forecast_url", + ), ) + # List Fields + list_fields = ["oid", + "organisation_id", + "country", + "feed_url.url", + "forecast_url.url", + ] + + # Table Configuration self.configure(tablename, crud_form = crud_form, list_fields = list_fields, ) - # Components - self.add_components(tablename, - cap_authority_feed_url = {"name": "feed_url", - "joinby": "alerting_authority_id", - }, - cap_authority_forecast_url = {"name": "forecast_url", - "joinby": "alerting_authority_id", - }, - ) + # CRUD Strings + crud_strings[tablename] = Storage( + label_create = T("Add Alerting Authority"), + title_display = T("Alerting Authority"), + title_list = T("Alerting Authorities"), + title_update = T("Edit Alerting Authority"), + subtitle_list = T("List Alerting Authorities"), + label_list_button = T("List Alerting Authorities"), + label_delete_button = T("Delete Alerting Authority"), + msg_record_created = T("Alerting Authority added"), + msg_record_modified = T("Alerting Authority updated"), + msg_record_deleted = T("Alerting Authority deleted"), + msg_list_empty = T("No Alerting Authority available"), + ) + + # Reusable Field + represent = S3Represent(lookup = tablename, + fields = ["organisation_id", "oid"], + field_sep = " - ", + ) + + authority_id = S3ReusableField("alerting_authority_id", "reference %s" % tablename, + label = T("CAP Alerting Authority"), + ondelete = "CASCADE", + represent = represent, + requires = IS_EMPTY_OR( + IS_ONE_OF(db, "%s.id" % tablename, + represent, + )), + ) # --------------------------------------------------------------------- # Feed URL for CAP Alerting Authority + # tablename = "cap_authority_feed_url" define_table(tablename, - alerting_authority_id(), + authority_id(), # @ToDo: Add username and pwd for url Field("url", label = T("CAP Feed URL"), @@ -3255,9 +3867,10 @@ def model(self): # --------------------------------------------------------------------- # Forecasts URL for CAP Alerting Authority + # tablename = "cap_authority_forecast_url" define_table(tablename, - alerting_authority_id(), + authority_id(), Field("url", label = T("Forecast URL"), requires = IS_EMPTY_OR(IS_URL()), @@ -3268,49 +3881,18 @@ def model(self): # Pass names back to global scope (s3.*) return {} -# ============================================================================= -class S3CAPAreaNameModel(S3Model): - """ - CAP Name Model: - -local names for CAP Area - """ - - names = ("cap_area_name", - ) - - def model(self): - - T = current.T - - # --------------------------------------------------------------------- - # Local Names - # - tablename = "cap_area_name" - self.define_table(tablename, - self.cap_area_id(empty = False, - ondelete = "CASCADE", - ), - s3_language(empty = False, - select = current.deployment_settings.get_cap_languages(), - ), - Field("name_l10n", - label = T("Local Name"), - ), - s3_comments(), - *s3_meta_fields()) - - self.configure(tablename, - deduplicate = S3Duplicate(primary=("area_id", "language")), - ) + # ------------------------------------------------------------------------- + @staticmethod + def defaults(): + """ + Return safe defaults in case the model has been deactivated. + """ - # Pass names back to global scope (s3.*) return {} # ============================================================================= -class S3CAPMessageModel(S3Model): - """ - Link Alert to Message - """ +class CAPMessageModel(S3Model): + """ Link Alerts to Messages """ names = ("cap_alert_message", ) @@ -3335,629 +3917,473 @@ def model(self): # return {} + # ------------------------------------------------------------------------- + @staticmethod + def defaults(): + """ + Return safe defaults in case the model has been deactivated. + """ + + return {} + # ============================================================================= -def json_formatter(fstring): +def list_string_represent(value, fmt=lambda v: v): + """ + Represent a list:string field value as comma-separated list + + @param value: the field value + @param fmt: callable to format each list element (optional) + + @returns: the comma-separated list as string + """ + + if isinstance(value, list): + output = ", ".join([fmt(i) for i in value]) + elif isinstance(value, basestring): + try: + output = ", ".join([fmt(i) for i in value[1:-1].split("|")]) + except IndexError: + output = current.messages.UNKNOWN_OPT + else: + output = "" + + return output + +# ------------------------------------------------------------------------- +def cap_expirydate(): """ - Properly format the Key-Value import string to json + Default expiry date for info segments based on setting + + @returns: datetime.datetime, or None if setting is falsy """ - if fstring == "||": - fstring = "[]" + interval = current.deployment_settings.get_cap_info_effective_period() + if interval: + expiry_date = current.request.utcnow + datetime.timedelta(days=interval) else: - fstring = fstring.replace(" ", "") # Remove space - fstring = fstring.replace("|", "") - fstring = fstring.replace("}{", "},{") - fstring = fstring.replace("{u'", "{'") - fstring = fstring.replace(":u'", ":'") - fstring = fstring.replace(",u'", ",'") - fstring = fstring.replace("'", '"') - fstring = "[%s]" % fstring + # Automatic expiry disabled + expiry_date = None + return expiry_date + +# ------------------------------------------------------------------------- +def cap_sendername(): + """ + Default sender name for alerts + - sender name is the name of the organisation if user is + associated with one, otherwise the user email + + @returns: the sender name + """ + + db = current.db + + utable = db.auth_user + otable = current.s3db.org_organisation + + sender = None + + user = current.auth.user + if user: + query = (utable.id == user.id) & \ + (utable.organisation_id == otable.id) & \ + (otable.deleted == False) + + org = db(query).select(otable.name, limitby=(0, 1)).first() + if org: + sender = org.name + else: + #sender = user.username # not globally unique + sender = user.email - return fstring + return sender # ============================================================================= def get_cap_alert_addresses_opts(): - """ Get the pr_group.id required for cap_alert.addresses field""" + """ + Get the pr_group.id required for cap_alert.addresses field + + TODO improve this docstring: what is this really used for? + TODO document return value + """ T = current.T gtable = current.s3db.pr_group - rows = current.db(gtable.deleted != True).select(gtable.id, - gtable.name) + rows = current.db(gtable.deleted == False).select(gtable.id, + gtable.name, + ) return [(row.id, s3_str(T(row.name))) for row in rows] # ============================================================================= def cap_alert_is_template(alert_id): """ - Tell whether an alert entry is a template + Check whether a cap_alert is a template + + @param alert_id: the alert record ID + + @returns: True|False whether the alert is a template """ if not alert_id: return False table = current.s3db.cap_alert - query = (table.id == alert_id) - r = current.db(query).select(table.is_template, - limitby=(0, 1)).first() - - return r and r.is_template + alert = current.db(table.id == alert_id).select(table.is_template, + limitby = (0, 1), + ).first() + return bool(alert.is_template) if alert else False # ============================================================================= -def cap_rheader(r): - """ Resource Header for CAP module """ +def cap_rheader(r, tabs=None): + """ CAP Module Resource Headers """ + + if r.representation != "html": + # Resource headers only used in interactive views + return None + + tablename, record = s3_rheader_resource(r) + #if tablename != r.tablename: + # resource = current.s3db.resource(tablename, id=record.id) + #else: + # resource = r.resource rheader = None - if r.representation == "html": - record = r.record - if record: - T = current.T - db = current.db - s3db = current.s3db - tablename = r.tablename - if tablename == "cap_alert": - alert_id = record.id - itable = s3db.cap_info - row = db(itable.alert_id == alert_id).select(itable.id, - limitby=(0, 1)).first() - if record.is_template: - if not row: - error = DIV(T("An alert needs to contain at least one info item."), - _class="error") - else: - error = "" - - # Copy button is not working as create?from_record only works - # with default form and not implemented for custom forms. - # cap_info uses a custom form - #if r.component_name == "info" and r.component_id: - # Display "Copy" Button to copy record from the opened info - # copy_btn = A(T("Copy Info Segment"), - # _href = URL(f = "template", - # args = [r.id, "info", "create",], - # vars = {"from_record": r.component_id, - # }, - # ), - # _class = "action-btn" - # ) - #else: - # copy_btn = None + if record: + + record_id = record.id + + T = current.T + db = current.db + s3db = current.s3db + settings = current.deployment_settings + + tablename = r.tablename + if tablename == "cap_alert": + + # Check for info segment + itable = s3db.cap_info + row = db(itable.alert_id == record_id).select(itable.id, + limitby = (0, 1), + ).first() + if record.is_template: + # Default tabs + if not tabs: tabs = [(T("Alert Details"), None), (T("Information"), "info"), #(T("Area"), "area"), (T("Resource Files"), "resource"), ] + rheader_tabs = s3_rheader_tabs(r, tabs) - rheader_tabs = s3_rheader_tabs(r, tabs) + # Hint for missing info segment + if not row: + error = DIV(T("An alert needs to contain at least one info item."), + _class = "error", + ) + else: + error = "" + + # The rheader + rheader = DIV(TABLE(TR(TH("%s: " % T("Template Title")), + TD(A(CAPAlertModel.template_represent(record_id, record), + _href = URL(c="cap", f="template", + args = [record_id, "update"], + ), + ), + ), + ), + TR(TH("%s: " % T("Event Type")), + TD(r.table.event_type_id.represent(record.event_type_id)), + ), + ), + rheader_tabs, + error, + ) + else: + has_permission = current.auth.s3_has_permission - rheader = DIV(TABLE(TR(TH("%s: " % T("Template Title")), - TD(A(s3db.cap_template_represent(alert_id, record), - _href=URL(c="cap", f="template", - args=[alert_id, "update"]))), - ), - TR(TH("%s: " % T("Event Type")), - TD(r.table.event_type_id.represent(record.event_type_id)) - ), - ), - rheader_tabs, - error - ) - #if copy_btn is not None: - # rheader.insert(1, TR(TD(copy_btn))) + action_btn = None + msg_type_buttons = None + + if not row: + # Hint for missing INFO-segment + error = DIV(T("You need to create at least one alert information item in order to be able to broadcast this alert!"), + _class="error") else: - action_btn = None - msg_type_buttons = None - if not row: - error = DIV(T("You need to create at least one alert information item in order to be able to broadcast this alert!"), - _class="error") - #export_btn = "" - else: - error = "" - #export_btn = A(DIV(_class="export_cap_large"), - # _href=URL(c="cap", f="alert", args=["%s.cap" % alert_id]), - # _target="_blank", - # ) - - has_permission = current.auth.s3_has_permission - # Display 'Submit for Approval', 'Publish Alert' or - # 'Review Alert' based on permission and deployment settings - if not current.request.get_vars.get("_next") and \ - current.deployment_settings.get_cap_authorisation() and \ - record.approved_by is None: - # Show these buttons only if there is atleast one area segment - area_table = s3db.cap_area - area_row = db(area_table.alert_id == alert_id).\ - select(area_table.id, - limitby=(0, 1)).first() - if area_row and has_permission("update", "cap_alert", - record_id=alert_id): - action_btn = A(T("Submit for Approval"), - _href = URL(f = "notify_approver", - vars = {"cap_alert.id": alert_id, - }, + error = "" + + # Action buttons for Alerts + # TODO clean up and move into separate function(s) + + # Display 'Submit for Approval', 'Publish Alert' or 'Review Alert' + # based on permission and deployment settings + if not current.request.get_vars.get("_next") and \ + settings.get_cap_authorisation() and \ + record.approved_by is None: + + # Show these buttons only if there is at least one area segment + area_table = s3db.cap_area + area_row = db(area_table.alert_id == record_id).select(area_table.id, limitby=(0, 1)).first() + if area_row and has_permission("update", "cap_alert", record_id=record_id): + action_btn = A(T("Submit for Approval"), + _href = URL(f = "notify_approver", + vars = {"cap_alert.id": record_id, + }, + ), + _class = "action-btn confirm-btn button tiny" + ) + current.response.s3.jquery_ready.append('''S3.confirmClick('.confirm-btn','%s')''' % T("Do you want to submit the alert for approval?")) + + # For Alert Approver + if has_permission("approve", "cap_alert"): + action_btn = A(T("Review Alert"), + _href = URL(args = [record_id, + "review" + ], ), - _class = "action-btn confirm-btn button tiny" + _class = "action-btn button tiny", ) - current.response.s3.jquery_ready.append( -'''S3.confirmClick('.confirm-btn','%s')''' % T("Do you want to submit the alert for approval?")) - - # For Alert Approver - if has_permission("approve", "cap_alert"): - action_btn = A(T("Review Alert"), - _href = URL(args = [alert_id, - "review" - ], - ), - _class = "action-btn button tiny", - ) - - if record.approved_by is not None: - if has_permission("create", "cap_alert"): - relay_alert = A(T("Relay Alert"), - _data = "%s/%s" % (record.msg_type, r.id), - _class = "action-btn cap-clone-update button tiny", - ) - if record.external: - msg_type_buttons = relay_alert - else: - if record.created_by: - utable = db.auth_user - row = db(utable.id == record.created_by).select(\ - utable.organisation_id, - limitby=(0, 1)).first() - row_ = db(utable.id == current.auth.user.id).select(\ - utable.organisation_id, - limitby=(0, 1)).first() - if row.organisation_id == row_.organisation_id: - # Same organisation - msg_type = record.msg_type - if msg_type == "Alert" or msg_type == "Update": - msg_type_buttons = TAG[""]( - TR(TD(A(T("Update Alert"), - _data = "Update/%s" % r.id, - _class = "action-btn cap-clone-update button tiny", - ))), - TR(TD(A(T("Cancel Alert"), - _data = "Cancel/%s" % r.id, - _class = "action-btn cap-clone-update button tiny", - ))), - TR(TD(A(T("Error Alert"), - _data = "Error/%s" % r.id, - _class = "action-btn cap-clone-update button tiny", - ))), - #TR(TD(A(T("All Clear"), - # _data = "AllClear/%s" % r.id, - # _class = "action-btn cap-clone-update button tiny", - # ))), - ) - elif msg_type == "Error": - msg_type_buttons = TAG[""]( - TR(TD(A(T("Update Alert"), - _data = "Update/%s" % r.id, - _class = "action-btn cap-clone-update button tiny", - ))), - #TR(TD(A(T("All Clear"), - # _data = "AllClear/%s" % r.id, - # _class = "action-btn cap-clone-update button tiny", - # ))), - ) - else: - msg_type_buttons = None + + # 'Relay Alert' action button + if record.approved_by is not None: + if has_permission("create", "cap_alert"): + relay_alert = A(T("Relay Alert"), + _data = "%s/%s" % (record.msg_type, r.id), + _class = "action-btn cap-clone-update button tiny", + ) + if record.external: + msg_type_buttons = relay_alert + else: + if record.created_by: + utable = db.auth_user + row = db(utable.id == record.created_by).select( + utable.organisation_id, + limitby = (0, 1), + ).first() + row_ = db(utable.id == current.auth.user.id).select( + utable.organisation_id, + limitby = (0, 1), + ).first() + if row.organisation_id == row_.organisation_id: + # Same organisation + msg_type = record.msg_type + if msg_type == "Alert" or msg_type == "Update": + msg_type_buttons = TAG[""]( + TR(TD(A(T("Update Alert"), + _data = "Update/%s" % r.id, + _class = "action-btn cap-clone-update button tiny", + ))), + TR(TD(A(T("Cancel Alert"), + _data = "Cancel/%s" % r.id, + _class = "action-btn cap-clone-update button tiny", + ))), + TR(TD(A(T("Error Alert"), + _data = "Error/%s" % r.id, + _class = "action-btn cap-clone-update button tiny", + ))), + #TR(TD(A(T("All Clear"), + # _data = "AllClear/%s" % r.id, + # _class = "action-btn cap-clone-update button tiny", + # ))), + ) + elif msg_type == "Error": + msg_type_buttons = TAG[""]( + TR(TD(A(T("Update Alert"), + _data = "Update/%s" % r.id, + _class = "action-btn cap-clone-update button tiny", + ))), + #TR(TD(A(T("All Clear"), + # _data = "AllClear/%s" % r.id, + # _class = "action-btn cap-clone-update button tiny", + # ))), + ) else: - # Different organisation - msg_type_buttons = relay_alert + msg_type_buttons = None else: + # Different organisation msg_type_buttons = relay_alert + else: + msg_type_buttons = relay_alert + # Default tabs + if not tabs: tabs = [(T("Alert Details"), None), (T("Information"), "info"), (T("Area"), "area"), (T("Resource Files"), "resource"), ] + # Additional tabs if r.representation == "html" and \ - current.auth.s3_has_permission("update", "cap_alert", - record_id=alert_id) and \ + has_permission("update", "cap_alert", record_id=record_id) and \ r.record.approved_by is None: + # Show predefined areas tab if we have some defined for this event_type - row_ = db(itable.alert_id == alert_id).select(itable.event_type_id, - limitby=(0, 1)).first() + row_ = db(itable.alert_id == record_id).select(itable.event_type_id, + limitby = (0, 1), + ).first() if row_ is not None: artable = s3db.cap_area - query = (artable.deleted != True) & \ + query = (artable.deleted == False) & \ (artable.is_template == True) & \ (artable.event_type_id == row_.event_type_id) template_area_row = db(query).select(artable.id, - limitby=(0, 1)).first() + limitby = (0, 1), + ).first() if template_area_row: tabs.insert(2, (T("Predefined Areas"), "assign")) - rheader_tabs = s3_rheader_tabs(r, tabs) + rheader_tabs = s3_rheader_tabs(r, tabs) + + # The rheader + rheader = DIV(TABLE(TR(TH("%s: " % T("Alert")), + TD(A("%s - %s" % (record.identifier, record.sender), + _href = URL(c="cap", f="alert", + args = [record_id, "update"], + ), + ), + ), + ), + #TR(export_btn) + ), + rheader_tabs, + error, + ) - rheader = DIV(TABLE(TR(TH("%s: " % T("Alert")), - TD(A("%s - %s" % (record.identifier, record.sender), - _href=URL(c="cap", f="alert", - args=[alert_id, "update"]))), - ), - #TR(export_btn) - ), - rheader_tabs, - error - ) + # Insert buttons + if action_btn: + rheader.insert(1, TR(TD(action_btn))) - if action_btn: - rheader.insert(1, TR(TD(action_btn))) + if msg_type_buttons is not None: + rheader.insert(1, msg_type_buttons) - if msg_type_buttons is not None: - rheader.insert(1, msg_type_buttons) + elif tablename == "cap_area": + # Used only for Area Templates - elif tablename == "cap_area": - # Used only for Area Templates + # Default tabs + if not tabs: tabs = [(T("Area"), None), ] - - if current.deployment_settings.get_L10n_translate_cap_area(): + if settings.get_L10n_translate_cap_area(): tabs.insert(1, (T("Local Names"), "name")) + rheader_tabs = s3_rheader_tabs(r, tabs) - rheader_tabs = s3_rheader_tabs(r, tabs) - rheader = DIV(TABLE(TR(TH("%s: " % T("Area")), - TD(A(s3db.cap_area_represent(record.id, record), - _href=URL(c="cap", f="area", - args=[record.id, "update"]))), - ), - ), - rheader_tabs - ) + # The rheader + rheader = DIV(TABLE(TR(TH("%s: " % T("Area")), + TD(A(s3db.cap_area_represent(record.id, record), + _href = URL(c="cap", f="area", + args = [record.id, "update"], + ), + ), + ), + ), + ), + rheader_tabs, + ) + + elif tablename == "cap_info": - elif tablename == "cap_info": - # Shouldn't ever be called + is_template = cap_alert_is_template(record.alert_id) + + # Default tabs + if not tabs: tabs = [(T("Information"), None), (T("Resource Files"), "resource"), ] - - if cap_alert_is_template(record.alert_id): - rheader_tabs = s3_rheader_tabs(r, tabs) - rheader = DIV(TABLE(TR(TH("%s: " % T("Template")), - TD(A(s3db.cap_template_represent(record.alert_id), - _href=URL(c="cap", f="template", - args=[record.alert_id, "update"]))), + if not is_template: + tabs.insert(1, (T("Areas"), "area")) + rheader_tabs = s3_rheader_tabs(r, tabs) + + # The rheader + if is_template: + rheader = DIV(TABLE(TR(TH("%s: " % T("Template")), + TD(A(CAPAlertModel.template_represent(record.alert_id), + _href = URL(c="cap", f="template", + args = [record.alert_id, "update"], + ), ), - TR(TH("%s: " % T("Info template")), - TD(A(s3db.cap_info_represent(record.id, record), - _href=URL(c="cap", f="info", - args=[record.id, "update"]))), - ) - ), - rheader_tabs, - _class="cap_info_template_form" - ) - current.response.s3.js_global.append('''i18n.cap_locked="%s"''' % T("Locked")) - else: - tabs.insert(1, (T("Areas"), "area")) - rheader_tabs = s3_rheader_tabs(r, tabs) - - rheader = DIV(TABLE(TR(TH("%s: " % T("Alert")), - TD(A(s3db.cap_alert_represent(record.alert_id), - _href=URL(c="cap", f="alert", - args=[record.alert_id, "update"]))), - ), - TR(TH("%s: " % T("Information")), - TD(A(s3db.cap_info_represent(record.id, record), - _href=URL(c="cap", f="info", - args=[record.id, "update"]))), - ) + ), ), - rheader_tabs - ) + TR(TH("%s: " % T("Info template")), + TD(A(s3db.cap_info_represent(record.id, record), + _href = URL(c="cap", f="info", + args = [record.id, "update"], + ), + ), + ), + ), + ), + rheader_tabs, + _class="cap_info_template_form" + ) + current.response.s3.js_global.append('''i18n.cap_locked="%s"''' % T("Locked")) + else: + rheader = DIV(TABLE(TR(TH("%s: " % T("Alert")), + TD(A(s3db.cap_alert_represent(record.alert_id), + _href = URL(c="cap", f="alert", + args = [record.alert_id, "update"], + ), + ), + ), + ), + TR(TH("%s: " % T("Information")), + TD(A(s3db.cap_info_represent(record.id, record), + _href = URL(c="cap", f="info", + args = [record.id, "update"], + ), + ), + ), + ), + ), + rheader_tabs, + ) return rheader # ============================================================================= -def cap_history_rheader(r): +def cap_history_rheader(r, tabs=None): """ Resource Header for CAP history tables """ + if r.representation != "html": + # Resource headers only used in interactive views + return None + + tablename, record = s3_rheader_resource(r) + rheader = None - if r.representation == "html": - record = r.record - if record: - T = current.T - #db = current.db - s3db = current.s3db - if r.tablename == "cap_alert_history": - alert_id = record.id - #itable = s3db.cap_info_history - #row = db(itable.alert_history_id == alert_id).select(itable.id, - # limitby=(0, 1)).first() + if record: + + T = current.T + s3db = current.s3db + + if tablename == "cap_alert_history": + alert_id = record.id + # Default tabs + if not tabs: tabs = [(T("Alert Details"), None), (T("Information"), "info_history"), (T("Area"), "area_history"), (T("Resource Files"), "resource_history"), ] - rheader_tabs = s3_rheader_tabs(r, tabs) + rheader_tabs = s3_rheader_tabs(r, tabs) - rheader = DIV(TABLE(TR(TH("%s: " % T("Alert")), - TD(A(s3db.cap_alert_represent(alert_id, record), - _href=URL(c="cap", f="alert", - args=[alert_id, "update"]))), - )), - rheader_tabs - ) + # The rheader + rheader = DIV(TABLE(TR(TH("%s: " % T("Alert")), + TD(A(s3db.cap_alert_represent(alert_id, record), + _href = URL(c="cap", f="alert", + args=[alert_id, "update"], + ), + ), + ), + ), + ), + rheader_tabs, + ) return rheader -# ============================================================================= -def cap_gis_location_xml_post_parse(element, record): - """ - UNUSED - done in XSLT - - Convert CAP polygon representation to WKT; extract circle lat lon. - Latitude and longitude in CAP are expressed as signed decimal values in - coordinate pairs: - latitude,longitude - The circle text consists of: - latitude,longitude radius - where the radius is in km. - Polygon text consists of a space separated sequence of at least 4 - coordinate pairs where the first and last are the same. - lat1,lon1 lat2,lon2 lat3,lon3 ... lat1,lon1 - """ - - # @ToDo: Extract altitude and ceiling from the enclosing , and - # compute an elevation value to apply to all enclosed gis_locations. - - cap_polygons = element.xpath("cap_polygon") - if cap_polygons: - cap_polygon_text = cap_polygons[0].text - # CAP polygons and WKT have opposite separator conventions: - # CAP has spaces between coordinate pairs and within pairs the - # coordinates are separated by comma, and vice versa for WKT. - # Unfortunately, CAP and WKT (as we use it) also have opposite - # orders of lat and lon. CAP has lat lon, WKT has lon lat. - # Both close the polygon by repeating the first point. - cap_points_text = cap_polygon_text.split() - cap_points = [cpoint.split(",") for cpoint in cap_points_text] - # @ToDo: Should we try interpreting all the points as decimal numbers, - # and failing validation if they're wrong? - wkt_points = ["%s %s" % (cpoint[1], cpoint[0]) for cpoint in cap_points] - wkt_polygon_text = "POLYGON ((%s))" % ", ".join(wkt_points) - record.wkt = wkt_polygon_text - return - - cap_circle_values = element.xpath("resource[@name='gis_location_tag']/data[@field='tag' and text()='cap_circle']/../data[@field='value']") - if cap_circle_values: - cap_circle_text = cap_circle_values[0].text - coords, radius = cap_circle_text.split() - lat, lon = coords.split(",") - try: - # If any of these fail to interpret as numbers, the circle was - # badly formatted. For now, we don't try to fail validation, - # but just don't set the lat, lon. - lat = float(lat) - lon = float(lon) - radius = float(radius) - except ValueError: - return - record.lat = lat - record.lon = lon - # Add a bounding box for the given radius, if it is not zero. - if radius > 0.0: - bbox = current.gis.get_bounds_from_radius(lat, lon, radius) - record.lat_min = bbox["lat_min"] - record.lon_min = bbox["lon_min"] - record.lat_max = bbox["lat_max"] - record.lon_max = bbox["lon_max"] - -# ============================================================================= -def cap_gis_location_xml_post_render(element, record): - """ - UNUSED - done in XSLT - - Convert Eden WKT polygon (and eventually circle) representation to - CAP format and provide them in the rendered s3xml. - - Not all internal formats have a parallel in CAP, but an effort is made - to provide a resonable substitute: - Polygons are supported. - Circles that were read in from CAP (and thus carry the original CAP - circle data) are supported. - Multipolygons are currently rendered as their bounding box. - Points are rendered as zero radius circles. - - Latitude and longitude in CAP are expressed as signed decimal values in - coordinate pairs: - latitude,longitude - The circle text consists of: - latitude,longitude radius - where the radius is in km. - Polygon text consists of a space separated sequence of at least 4 - coordinate pairs where the first and last are the same. - lat1,lon1 lat2,lon2 lat3,lon3 ... lat1,lon1 - """ - - # @ToDo: Can we rely on gis_feature_type == 3 to tell if the location is a - # polygon, or is it better to look for POLYGON in the wkt? For now, check - # both. - # @ToDo: CAP does not support multipolygons. Do we want to extract their - # outer polygon if passed MULTIPOLYGON wkt? For now, these are exported - # with their bounding box as the polygon. - # @ToDo: What if a point (gis_feature_type == 1) that is not a CAP circle - # has a non-point bounding box? Should it be rendered as a polygon for - # the bounding box? - - try: - from lxml import etree - except ImportError: - # This won't fail, since we're in the middle of processing xml. - return - - SubElement = etree.SubElement - - s3xml = current.xml - TAG = s3xml.TAG - RESOURCE = TAG["resource"] - DATA = TAG["data"] - ATTRIBUTE = s3xml.ATTRIBUTE - NAME = ATTRIBUTE["name"] - FIELD = ATTRIBUTE["field"] - #VALUE = ATTRIBUTE["value"] - - #loc_tablename = "gis_location" - tag_tablename = "gis_location_tag" - tag_fieldname = "tag" - val_fieldname = "value" - polygon_tag = "cap_polygon" - circle_tag = "cap_circle" - fallback_polygon_tag = "cap_polygon_fallback" - fallback_circle_tag = "cap_circle_fallback" - - def __cap_gis_location_add_polygon(element, cap_polygon_text, fallback=False): - """ - Helper for cap_gis_location_xml_post_render that adds the CAP polygon - data to the current element in a gis_location_tag element. - """ - # Make a gis_location_tag. - tag_resource = SubElement(element, RESOURCE) - tag_resource.set(NAME, tag_tablename) - tag_field = SubElement(tag_resource, DATA) - # Add tag and value children. - tag_field.set(FIELD, tag_fieldname) - if fallback: - tag_field.text = fallback_polygon_tag - else: - tag_field.text = polygon_tag - val_field = SubElement(tag_resource, DATA) - val_field.set(FIELD, val_fieldname) - val_field.text = cap_polygon_text - - def __cap_gis_location_add_circle(element, lat, lon, radius, fallback=False): - """ - Helper for cap_gis_location_xml_post_render that adds CAP circle - data to the current element in a gis_location_tag element. - """ - # Make a gis_location_tag. - tag_resource = SubElement(element, RESOURCE) - tag_resource.set(NAME, tag_tablename) - tag_field = SubElement(tag_resource, DATA) - # Add tag and value children. - tag_field.set(FIELD, tag_fieldname) - if fallback: - tag_field.text = fallback_circle_tag - else: - tag_field.text = circle_tag - val_field = SubElement(tag_resource, DATA) - val_field.set(FIELD, val_fieldname) - # Construct a CAP circle string: latitude,longitude radius - cap_circle_text = "%s,%s %s" % (lat, lon, radius) - val_field.text = cap_circle_text - - # Sort out the geometry case by wkt, CAP tags, gis_feature_type, bounds,... - # Check the two cases for CAP-specific locations first, as those will have - # definite export values. For others, we'll attempt to produce either a - # circle or polygon: Locations with a bounding box will get a box polygon, - # points will get a zero-radius circle. - - # Currently wkt is stripped out of gis_location records right here: - # https://github.com/flavour/eden/blob/master/modules/s3/s3resource.py#L1332 - # https://github.com/flavour/eden/blob/master/modules/s3/s3resource.py#L1426 - # https://github.com/flavour/eden/blob/master/modules/s3/s3resource.py#L3152 - # Until we provide a way to configure that choice, this will not work for - # polygons. - wkt = record.get("wkt", None) - - # WKT POLYGON: Although there is no WKT spec, according to every reference - # that deals with nested polygons, the outer, enclosing, polygon must be - # listed first. Hence, we extract only the first polygon, as CAP has no - # provision for nesting. - if wkt and wkt.startswith("POLYGON"): - # ToDo: Is it sufficient to test for adjacent (( to find the start of - # the polygon, or might there be whitespace between them? - start = wkt.find("((") - end = wkt.find(")") - if start >=0 and end >=0: - polygon_text = wkt[start + 2 : end] - points_text = polygon_text.split(",") - points = [p.split() for p in points_text] - cap_points_text = ["%s,%s" % (point[1], point[0]) for point in points] - cap_polygon_text = " ".join(cap_points_text) - __cap_gis_location_add_polygon(element, cap_polygon_text) - return - # Fall through if the wkt string was mal-formed. - - # CAP circle stored in a gis_location_tag with tag = cap_circle. - # If there is a cap_circle tag, we don't need to do anything further, as - # export.xsl will use it. However, we don't know if there is a cap_circle - # tag... - # - # @ToDo: The export calls xml_post_render after processing a resource's - # fields, but before its components are added as children in the xml tree. - # If this were delayed til after the components were added, we could look - # there for the cap_circle gis_location_tag record. Since xml_post_parse - # isn't in use yet (except for this), maybe we could look at moving it til - # after the components? - # - # For now, with the xml_post_render before components: We could do a db - # query to check for a real cap_circle tag record, and not bother with - # creating fallbacks from bounding box or point...but we don't have to. - # Instead, just go ahead and add the fallbacks under different tag names, - # and let the export.xsl sort them out. This only wastes a little time - # compared to a db query. - - # ToDo: MULTIPOLYGON -- Can stitch together the outer polygons in the - # multipolygon, but would need to assure all were the same handedness. - - # The remaining cases are for locations that don't have either polygon wkt - # or a cap_circle tag. - - # Bounding box: Make a four-vertex polygon from the bounding box. - # This is a fallback, as if there is a circle tag, we'll use that. - lon_min = record.get("lon_min", None) - lon_max = record.get("lon_max", None) - lat_min = record.get("lat_min", None) - lat_max = record.get("lat_max", None) - if lon_min and lon_max and lat_min and lat_max and \ - (lon_min != lon_max) and (lat_min != lat_max): - # Although there is no WKT requirement, arrange the points in - # counterclockwise order. Recall format is: - # lat1,lon1 lat2,lon2 ... latN,lonN, lat1,lon1 - cap_polygon_text = \ - "%(lat_min)s,%(lon_min)s %(lat_min)s,%(lon_max)s %(lat_max)s,%(lon_max)s %(lat_max)s,%(lon_min)s %(lat_min)s,%(lon_min)s" \ - % {"lon_min": lon_min, - "lon_max": lon_max, - "lat_min": lat_min, - "lat_max": lat_max} - __cap_gis_location_add_polygon(element, cap_polygon_text, fallback=True) - return - - # WKT POINT or location with lat, lon: This can be rendered as a - # zero-radius circle. - # Q: Do we put bounding boxes around POINT locations, and are they - # meaningful? - lat = record.get("lat", None) - lon = record.get("lon", None) - if not lat or not lon: - # Look for POINT. - if wkt and wkt.startswith("POINT"): - start = wkt.find("(") - end = wkt.find(")") - if start >=0 and end >=0: - point_text = wkt[start + 2 : end] - point = point_text.split() - try: - lon = float(point[0]) - lat = float(point[1]) - except ValueError: - pass - if lat and lon: - # Add a (fallback) circle with zero radius. - __cap_gis_location_add_circle(element, lat, lon, 0, True) - return - - # ToDo: Other WKT. - - # Did not find anything to use. Presumably the area has a text description. - return - # ============================================================================= def cap_alert_list_layout(list_id, item_id, resource, rfields, record): """ @@ -3995,7 +4421,8 @@ def cap_alert_list_layout(list_id, item_id, resource, rfields, record): db = current.db wptable = db.cap_warning_priority priority_row = db(wptable.name == priority).select(wptable.color_code, - limitby=(0, 1)).first() + limitby = (0, 1), + ).first() more = A(T("Full Alert"), _href = _href, @@ -4062,8 +4489,8 @@ def cap_alert_list_layout(list_id, item_id, resource, rfields, record): if priority_row: sub_headline["_style"] = "color: #%s" % (priority_row.color_code) - para = T("%(status)s alert for %(area_description)s.") \ - % dict(status=status, area_description=location) + para = T("%(status)s alert for %(area_description)s.") % \ + {"status": status, "area_description": location} issuer = "%s: %s" % (T("Issued by"), sender_name) issue_date = "%s: %s" % (T("Issued on"), sent) @@ -4086,71 +4513,73 @@ def cap_alert_list_layout(list_id, item_id, resource, rfields, record): return item # ============================================================================= -def add_area_from_template(area_id, alert_id): - """ - Add an area from a Template along with its components Location and Tag - """ +class cap_AreaRepresent(S3Represent): + """ Representation of CAP Area """ - afieldnames = ("name", - "altitude", - "ceiling", - ) - lfieldnames = ("location_id", - ) - tfieldnames = ("tag", - "value", - "comments", - ) + def __init__(self, show_link=False, multiple=False): - db = current.db - s3db = current.s3db - set_record_owner = current.auth.s3_set_record_owner - onaccept = s3db.onaccept - atable = s3db.cap_area - #itable = s3db.cap_info - ltable = s3db.cap_area_location - ttable = s3db.cap_area_tag - - # Create Area Record from Template - atemplate = db(atable.id == area_id).select(*afieldnames, - limitby=(0, 1)).first() - - # @ToDo set_record_owner, update_super and/or onaccept - # Currently not required by SAMBRO template - adata = {"is_template": False, - "alert_id": alert_id, - "template_area_id": area_id, - } - for field in afieldnames: - adata[field] = atemplate[field] - - aid = atable.insert(**adata) - set_record_owner(atable, aid) - onaccept(atable, dict(id=aid)) - - # Add Area Location Components of Template - ltemplate = db(ltable.area_id == area_id).select(*lfieldnames) - for lrow in ltemplate: - ldata = {"area_id": aid, - "alert_id": alert_id, - } - for field in lfieldnames: - ldata[field] = lrow[field] - lid = ltable.insert(**ldata) - set_record_owner(ltable, lid) - onaccept(ltable, dict(id=lid)) - - # Add Area Tag Components of Template - ttemplate = db(ttable.area_id == area_id).select(*tfieldnames) - for trow in ttemplate: - tdata = {"area_id": aid, - "alert_id": alert_id, - } - for field in tfieldnames: - tdata[field] = trow[field] - ttable.insert(**tdata) - - return aid + settings = current.deployment_settings + + # Translation using cap_area_name & not T() + translate = settings.get_L10n_translate_cap_area() + if translate: + language = current.session.s3.language + if language == settings.get_L10n_default_language(): + translate = False + + super(cap_AreaRepresent, self).__init__(lookup = "cap_area", + show_link = show_link, + translate = translate, + multiple = multiple, + ) + + # ------------------------------------------------------------------------- + def lookup_rows(self, key, values, fields=None): + """ + Custom lookup method for area Rows; to look up local name if + required. + + @param key: unused (retained for API compatibility) + @param values: the cap_area IDs + @param fields: unused (retained for API compatibility) + """ + + table = self.table + + count = len(values) + if count == 1: + query = (table.id == values[0]) + else: + query = (table.id.belongs(values)) + + fields = [table.id, table.name] + if self.translate: + ltable = current.s3db.cap_area_name + fields.append(ltable.name_l10n) + left = ltable.on((ltable.area_id == table.id) & \ + (ltable.language == current.session.s3.language)) + else: + left = None + + rows = current.db(query).select(left=left, limitby=(0, count), *fields) + return rows + + # ------------------------------------------------------------------------- + def represent_row(self, row): + """ + Represent a single Row + + @param row: the cap_area Row + """ + + name = row["cap_area.name"] + + if self.translate: + local_name = row["cap_area_name.name_l10n"] + if local_name: + name = local_name + + return s3_str(name) if name else self.default # ============================================================================= class cap_ImportAlert(S3Method): @@ -4173,6 +4602,8 @@ def apply_method(self, r, **attr): if not authorised: r.unauthorised() + output = None + if r.representation == "html": T = current.T @@ -4257,11 +4688,11 @@ def apply_method(self, r, **attr): else: response.warning = T("No CAP alerts found at URL") - return output - else: r.error(405, current.ERROR.BAD_METHOD) + return output + # ------------------------------------------------------------------------- def accept(self, resource, form): """ @@ -4567,7 +4998,7 @@ def apply_method(self, r, **attr): for area_id in selected: area_id = int(area_id.strip()) - add_area_from_template(area_id, alert_id) + self.add_area_from_template(area_id, alert_id) added += 1 if added == 1: confirm_text = T("1 area assigned") @@ -4728,6 +5159,102 @@ def apply_method(self, r, **attr): else: r.error(405, current.ERROR.BAD_METHOD) + # ------------------------------------------------------------------------- + @staticmethod + def add_area_from_template(template_area_id, alert_id): + """ + Add a new area to an alert from a template area including + location links and tags + + @param template_area_id: the template cap_area record ID + @param alert_id: the record ID of the alert + + @returns: the new area record ID + """ + + db = current.db + s3db = current.s3db + + set_record_owner = current.auth.s3_set_record_owner + audit = current.audit + #update_super = s3db.update_super + onaccept = s3db.onaccept + + # Get the template + atable = s3db.cap_area + query = (atable.id == template_area_id) & \ + (atable.deleted == False) + template_area = db(query).select(atable.name, + atable.altitude, + atable.ceiling, + limitby = (0, 1), + ).first() + if not template_area: + return None + + # Collect data for new area + area = {"name": template_area.name, + "is_template": False, + "alert_id": alert_id, + "template_area_id": template_area_id, + "altitude": template_area.altitude, + "ceiling": template_area.ceiling, + } + + # Create new area + area_id = atable.insert(**area) + area["id"] = area_id + + # Post-process create + audit("create", "cap", "area", record=area_id) + #update_super(atable, area) # currently not required + set_record_owner(atable, area_id) + onaccept(atable, area) + + # Copy all location links from the template + ltable = s3db.cap_area_location + query = (ltable.area_id == template_area_id) & \ + (ltable.deleted == False) + rows = db(query).select(ltable.location_id, + ) + for row in rows: + link = {"area_id": area_id, + "alert_id": alert_id, + "location_id": row.location_id, + } + link_id = ltable.insert(**link) + link["id"] = link_id + + audit("create", "cap", "area_location", record=link_id) + #update_super(ltable, link) # currently not required + set_record_owner(ltable, link_id) + onaccept(ltable, link) + + # Copy all tags from template + ttable = s3db.cap_area_tag + query = (ttable.area_id == template_area_id) & \ + (ttable.deleted == False) + rows = db(query).select(ttable.tag, + ttable.value, + ttable.comments, + ) + for row in rows: + tag = {"area_id": area_id, + "alert_id": alert_id, + "tag": row.tag, + "value": row.value, + "comments": row.comments, + } + tag_id = ttable.insert(**tag) + tag["id"] = tag_id + + audit("create", "cap", "area_tag", record=tag_id) + #update_super(ttable, tag) # currently not required + set_record_owner(ttable, tag_id) + #onaccept(ttable, tag) # currently not required (redundant?) + + return area_id + # ----------------------------------------------------------------------------- class cap_CloneAlert(S3Method): """ @@ -4738,6 +5265,7 @@ class cap_CloneAlert(S3Method): def apply_method(self, r, **attr): """ Apply method + @param r: the S3Request @param attr: controller options for this request """ @@ -4761,6 +5289,7 @@ def clone(r, record=None, **attr): @param record: the record row @param attr: controller attributes """ + # TODO make a class method of cap_CloneAlert if record and not r.env.request_method == "POST": current.log.error(current.ERROR.BAD_METHOD) @@ -4838,14 +5367,15 @@ def clone(r, record=None, **attr): references = [] if referenced_reference: reference_splits = referenced_reference.split(",") - reference_query_ = (alert_table.deleted != True) & \ + reference_query_ = (alert_table.deleted == False) & \ (alert_table.is_template != True) & \ (alert_table.external != True) for split in reference_splits: reference_query = reference_query_ & (alert_table.identifier == split) reference_row = db(reference_query).select(alert_table.sender, alert_table.sent, - limitby=(0, 1)).first() + limitby = (0, 1), + ).first() if reference_row: references.append(("%s,%s,%s") % (reference_row.sender, split, @@ -5106,91 +5636,14 @@ def clone(r, record=None, **attr): return output return -# ----------------------------------------------------------------------------- -class cap_AreaRepresent(S3Represent): - """ Representation of CAP Area """ - - def __init__(self, - show_link=False, - multiple=False): - - settings = current.deployment_settings - # Translation using cap_area_name & not T() - translate = settings.get_L10n_translate_cap_area() - if translate: - language = current.session.s3.language - if language == settings.get_L10n_default_language(): - translate = False - - super(cap_AreaRepresent, - self).__init__(lookup="cap_area", - show_link=show_link, - translate=translate, - multiple=multiple - ) - - # ------------------------------------------------------------------------- - def lookup_rows(self, key, values, fields=None): - """ - Custom lookup method for Area(CAP) rows.Parameters - key and fields are not used, but are kept for API - compatibility reasons. - - @param values: the cap_area IDs - """ - - s3db = current.s3db - artable = s3db.cap_area - - count = len(values) - if count == 1: - query = (artable.id == values[0]) - else: - query = (artable.id.belongs(values)) - - fields = [artable.id, - artable.name, - ] - - if self.translate: - ltable = s3db.cap_area_name - fields += [ltable.name_l10n, - ] - left = [ltable.on((ltable.area_id == artable.id) & \ - (ltable.language == current.session.s3.language)), - ] - - else: - left = None - - rows = current.db(query).select(left = left, - limitby = (0, count), - *fields) - return rows - - # ------------------------------------------------------------------------- - def represent_row(self, row): - """ - Represent a single Row - - @param row: the cap_area Row - """ - - if self.translate: - name = row["cap_area_name.name_l10n"] or row["cap_area.name"] - else: - name = row["cap_area.name"] - - if not name: - return self.default - - return s3_str(name) - # ----------------------------------------------------------------------------- class cap_AlertProfileWidget(object): """ Custom profile widget builder """ def __init__(self, title, label=None, value=None): + """ + TODO docstring + """ self.title = title self.label = label @@ -5200,7 +5653,10 @@ def __init__(self, title, label=None, value=None): def __call__(self, f): """ Widget builder + @param f: the calling function + + TODO: explain return value and use as decorator """ def widget(r, **attr): diff --git a/modules/s3db/deploy.py b/modules/s3db/deploy.py index 4ecef9cc9d..4afa00202b 100644 --- a/modules/s3db/deploy.py +++ b/modules/s3db/deploy.py @@ -35,6 +35,7 @@ "deploy_alert_select_recipients", "deploy_Inbox", "deploy_response_select_mission", + "deploy_availability_filter", ) from gluon import * @@ -80,6 +81,7 @@ class S3DeploymentModel(S3Model): "deploy_assignment", "deploy_assignment_appraisal", "deploy_assignment_experience", + "deploy_unavailability", ) def model(self): @@ -150,51 +152,51 @@ def model(self): # Profile list_layout = deploy_MissionProfileLayout() - alert_widget = dict(label = "Alerts", - insert = lambda r, list_id, title, url: \ - A(title, - _href = r.url(component = "alert", - method = "create"), - _class = "action-btn profile-add-btn", - ), - label_create = "Create Alert", - type = "datalist", - list_fields = ["modified_on", - "mission_id", - "message_id", - "subject", - "body", - ], - tablename = "deploy_alert", - context = "mission", - list_layout = list_layout, - pagesize = 10, - ) + alert_widget = {"label": "Alerts", + "insert": lambda r, list_id, title, url: \ + A(title, + _href = r.url(component = "alert", + method = "create"), + _class = "action-btn profile-add-btn", + ), + "label_create": "Create Alert", + "type": "datalist", + "list_fields": ["modified_on", + "mission_id", + "message_id", + "subject", + "body", + ], + "tablename": "deploy_alert", + "context": "mission", + "list_layout": list_layout, + "pagesize": 10, + } - response_widget = dict(label = "Responses", - insert = False, - type = "datalist", - tablename = "deploy_response", - # Can't be 'response' as this clobbers web2py global - function = "response_message", - list_fields = [ - "created_on", - "mission_id", - "comments", - "human_resource_id$id", - "human_resource_id$person_id", - "human_resource_id$organisation_id", - "message_id$body", - "message_id$from_address", - "message_id$attachment.document_id$file", - ], - context = "mission", - list_layout = list_layout, - # The popup datalist isn't currently functional - # (needs card layout applying) and not ideal UX anyway - #pagesize = 10, - pagesize = None, - ) + response_widget = {"label": "Responses", + "insert": False, + "type": "datalist", + "tablename": "deploy_response", + # Can't be 'response' as this clobbers web2py global + "function": "response_message", + "list_fields": [ + "created_on", + "mission_id", + "comments", + "human_resource_id$id", + "human_resource_id$person_id", + "human_resource_id$organisation_id", + "message_id$body", + "message_id$from_address", + "message_id$attachment.document_id$file", + ], + "context": "mission", + "list_layout": list_layout, + # The popup datalist isn't currently functional + # (needs card layout applying) and not ideal UX anyway + #"pagesize": 10, + "pagesize": None, + } hr_label = settings.get_deploy_hr_label() if hr_label == "Member": @@ -207,44 +209,44 @@ def model(self): label = "Volunteers Deployed" label_create = "Deploy New Volunteer" - assignment_widget = dict(label = label, - insert = lambda r, list_id, title, url: \ - A(title, - _href=r.url(component = "assignment", - method = "create", - ), - _class="action-btn profile-add-btn", - ), - label_create = label_create, - tablename = "deploy_assignment", - type = "datalist", - #type = "datatable", - #actions = dt_row_actions, - list_fields = [ - "human_resource_id$id", - "human_resource_id$person_id", - "human_resource_id$organisation_id", - "start_date", - "end_date", - "job_title_id", - "job_title", - "appraisal.rating", - "mission_id", - ], - context = "mission", - list_layout = list_layout, - pagesize = None, # all records - ) - - docs_widget = dict(label = "Documents & Links", - label_create = "Add New Document / Link", - type = "datalist", - tablename = "doc_document", - context = ("~.doc_id", "doc_id"), - icon = "attachment", - # Default renderer: - #list_layout = s3db.doc_document_list_layouts, - ) + assignment_widget = {"label": label, + "insert": lambda r, list_id, title, url: \ + A(title, + _href=r.url(component = "assignment", + method = "create", + ), + _class="action-btn profile-add-btn", + ), + "label_create": label_create, + "tablename": "deploy_assignment", + "type": "datalist", + #"type": "datatable", + #"actions": dt_row_actions, + "list_fields": [ + "human_resource_id$id", + "human_resource_id$person_id", + "human_resource_id$organisation_id", + "start_date", + "end_date", + "job_title_id", + "job_title", + "appraisal.rating", + "mission_id", + ], + "context": "mission", + "list_layout": list_layout, + "pagesize": None, # all records + } + + docs_widget = {"label": "Documents & Links", + "label_create": "Add New Document / Link", + "type": "datalist", + "tablename": "doc_document", + "context": ("~.doc_id", "doc_id"), + "icon": "attachment", + # Default renderer: + #"list_layout": s3db.doc_document_list_layouts, + } # Table configuration profile_url = URL(c="deploy", f="mission", args=["[id]", "profile"]) @@ -388,6 +390,48 @@ def model(self): delete_next = URL(c="deploy", f="human_resource", args="summary"), ) + # --------------------------------------------------------------------- + # Unavailability + # - periods where an HR is not available for deployments + # + tablename = "deploy_unavailability" + define_table(tablename, + self.pr_person_id(ondelete="CASCADE"), + s3_date("start_date", + label = T("Start Date"), + set_min = "#deploy_unavailability_end_date", + ), + s3_date("end_date", + label = T("End Date"), + set_max = "#deploy_unavailability_start_date", + ), + s3_comments(), + *s3_meta_fields()) + + # Table Configuration + configure(tablename, + organize = {"start": "start_date", + "end": "end_date", + "title": "comments", + "description": ["start_date", + "end_date", + ], + }, + ) + + # CRUD Strings + crud_strings[tablename] = Storage( + label_create = T("Add Period of Unavailability"), + title_display = T("Unavailability"), + title_list = T("Periods of Unavailability"), + title_update = T("Edit Unavailability"), + label_list_button = T("List Periods of Unavailability"), + label_delete_button = T("Delete Unavailability"), + msg_record_created = T("Unavailability added"), + msg_record_modified = T("Unavailability updated"), + msg_record_deleted = T("Unavailability deleted"), + msg_list_empty = T("No Unavailability currently registered")) + # --------------------------------------------------------------------- # Assignment of human resources # - actual assignment of an HR to a mission @@ -514,9 +558,9 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return dict(deploy_mission_id = mission_id, - deploy_mission_status_opts = mission_status_opts, - ) + return {"deploy_mission_id": mission_id, + "deploy_mission_status_opts": mission_status_opts, + } # ------------------------------------------------------------------------- def defaults(self): @@ -528,8 +572,8 @@ def defaults(self): readable = False, writable = False) - return dict(deploy_mission_id = lambda **attr: dummy("mission_id"), - ) + return {"deploy_mission_id": lambda **attr: dummy("mission_id"), + } # ------------------------------------------------------------------------- @staticmethod @@ -1147,7 +1191,7 @@ def deploy_alert_send(r, **attr): created_by = record.created_by, created_on = record.created_on, ) - new_alert = dict(id=new_alert_id) + new_alert = {"id": new_alert_id} s3db.update_super(table, new_alert) # Add Recipients @@ -1231,7 +1275,7 @@ def deploy_alert_send(r, **attr): current.log.debug("Sending tweets failed: %s" % e) # Update the Alert to show it's been Sent - data = dict(message_id=message_id) + data = {"message_id": message_id} if contact_method == 2: # Clear the Subject data["subject"] = None @@ -1298,6 +1342,57 @@ def deploy_response_update_onaccept(form): doc_id = hr.doc_id db(dtable.id.belongs(attachments)).update(doc_id=doc_id) +# ============================================================================= +def deploy_availability_filter(r): + """ + Filter requested resource (hrm_human_resource or pr_person) for + availability for deployment during a selected interval + - uses special filter selector "available" from GET vars + - called from prep of the respective controller + - adds resource filter for r.resource + + @param r: the S3Request + """ + + get_vars = r.get_vars + + # Parse start/end date + calendar = current.calendar + start = get_vars.pop("available__ge", None) + if start: + start = calendar.parse_date(start) + end = get_vars.pop("available__le", None) + if end: + end = calendar.parse_date(end) + + utable = current.s3db.deploy_unavailability + + # Construct query for unavailability + query = (utable.deleted == False) + if start and end: + query &= ((utable.start_date >= start) & (utable.start_date <= end)) | \ + ((utable.end_date >= start) & (utable.end_date <= end)) | \ + ((utable.start_date < start) & (utable.end_date > end)) + elif start: + query &= (utable.end_date >= start) | (utable.start_date >= start) + elif end: + query &= (utable.start_date <= end) | (utable.end_date <= end) + else: + return + + # Get person_ids of unavailability-entries + rows = current.db(query).select(utable.person_id, + groupby = utable.person_id, + ) + if rows: + person_ids = set(row.person_id for row in rows) + + # Filter r.resource for non-match + if r.tablename == "hrm_human_resource": + r.resource.add_filter(~(FS("person_id").belongs(person_ids))) + elif r.tablename == "pr_person": + r.resource.add_filter(~(FS("id").belongs(person_ids))) + # ============================================================================= def deploy_rheader(r, tabs=None, profile=False): """ Deployment Resource Headers """ @@ -2074,7 +2169,7 @@ def deploy_alert_select_recipients(r, **attr): response.warning = T("No Recipients Selected!") else: response.confirmation = T("%(number)s Recipients added to Alert") % \ - dict(number=added) + {"number": added} get_vars = r.get_vars or {} representation = r.representation @@ -2322,7 +2417,7 @@ def deploy_response_select_mission(r, **attr): # _href=URL(c="deploy", f="mission", # args=[mission_id, "profile"]))) #current.session.confirmation = T("Response linked to %(mission)s") % \ - # dict(mission=mission) + # {"mission": mission} current.session.confirmation = T("Response linked to Mission") redirect(URL(c="deploy", f="email_inbox")) diff --git a/modules/s3db/dvr.py b/modules/s3db/dvr.py index 9c509d1c00..ee84882e8d 100644 --- a/modules/s3db/dvr.py +++ b/modules/s3db/dvr.py @@ -117,7 +117,7 @@ def model(self): define_table(tablename, Field("name", label = T("Type"), - requires = IS_NOT_EMPTY(), + requires = [IS_NOT_EMPTY(), IS_LENGTH(512, minsize=1)], ), # Enable in template if/when org-specific # case types are required: @@ -165,7 +165,7 @@ def model(self): Field("code", length=64, notnull=True, unique=True, label = T("Status Code"), requires = [IS_NOT_EMPTY(), - IS_LENGTH(64), + IS_LENGTH(64, minsize=1), IS_NOT_ONE_OF(db, "%s.code" % tablename, ), @@ -890,7 +890,7 @@ def model(self): define_table(tablename, Field("name", label = T("Name"), - requires = IS_NOT_EMPTY(), + requires = [IS_NOT_EMPTY(), IS_LENGTH(512, minsize=1)], ), Field("advise_at_check_in", "boolean", default = False, @@ -1106,7 +1106,7 @@ def model(self): define_table(tablename, Field("name", label = T("Name"), - requires = IS_NOT_EMPTY(), + requires = [IS_NOT_EMPTY(), IS_LENGTH(512, minsize=1)], ), # This form of hierarchy may not work on all Databases: Field("parent", "reference dvr_need", @@ -1247,7 +1247,7 @@ def model(self): Field("name", length=128, unique=True, label = T("Name"), requires = [IS_NOT_EMPTY(), - IS_LENGTH(128), + IS_LENGTH(128, minsize=1), IS_NOT_ONE_OF(db, "dvr_note_type.name", ), @@ -1351,7 +1351,7 @@ def model(self): self.define_table(tablename, Field("name", label = T("Name"), - requires = IS_NOT_EMPTY(), + requires = [IS_NOT_EMPTY(), IS_LENGTH(512, minsize=1)], ), s3_comments(), *s3_meta_fields()) @@ -1450,7 +1450,7 @@ def model(self): self.org_organisation_id(), Field("name", label = T("Theme"), - requires = IS_NOT_EMPTY(), + requires = [IS_NOT_EMPTY(), IS_LENGTH(512, minsize=1)], ), self.dvr_need_id(readable = themes_needs, writable = themes_needs, @@ -1515,7 +1515,7 @@ def model(self): tablename = "dvr_response_type" define_table(tablename, Field("name", - requires = IS_NOT_EMPTY(), + requires = [IS_NOT_EMPTY(), IS_LENGTH(512, minsize=1)], ), # This form of hierarchy may not work on all databases: Field("parent", "reference dvr_response_type", @@ -1584,7 +1584,7 @@ def model(self): tablename = "dvr_response_status" define_table(tablename, Field("name", - requires = IS_NOT_EMPTY(), + requires = [IS_NOT_EMPTY(), IS_LENGTH(512, minsize=1)], ), Field("workflow_position", "integer", label = T("Workflow Position"), @@ -2322,7 +2322,7 @@ def model(self): define_table(tablename, Field("name", notnull=True, label = T("Type"), - requires = IS_NOT_EMPTY(), + requires = [IS_NOT_EMPTY(), IS_LENGTH(512, minsize=1)], ), s3_comments(), *s3_meta_fields()) @@ -2368,7 +2368,7 @@ def model(self): Field("name", length=128, notnull=True, unique=True, label = T("Type"), requires = [IS_NOT_EMPTY(), - IS_LENGTH(128), + IS_LENGTH(128, minsize=1), IS_NOT_ONE_OF(db, "%s.name" % tablename, ), @@ -2418,7 +2418,7 @@ def model(self): Field("name", length=128, notnull=True, unique=True, label = T("Age Group"), requires = [IS_NOT_EMPTY(), - IS_LENGTH(128), + IS_LENGTH(128, minsize=1), IS_NOT_ONE_OF(db, "%s.name" % tablename, ), @@ -2467,7 +2467,7 @@ def model(self): define_table(tablename, Field("name", notnull=True, label = T("Name"), - requires = IS_NOT_EMPTY(), + requires = [IS_NOT_EMPTY(), IS_LENGTH(512, minsize=1)], ), s3_comments(), *s3_meta_fields()) @@ -2679,7 +2679,7 @@ def model(self): ), Field("name", notnull=True, label = T("Name"), - requires = IS_NOT_EMPTY(), + requires = [IS_NOT_EMPTY(), IS_LENGTH(512, minsize=1)], ), s3_comments(), *s3_meta_fields()) @@ -2725,7 +2725,7 @@ def model(self): tablename = "dvr_case_activity_status" define_table(tablename, Field("name", - requires = IS_NOT_EMPTY(), + requires = [IS_NOT_EMPTY(), IS_LENGTH(512, minsize=1)], ), Field("workflow_position", "integer", label = T("Workflow Position"), @@ -3173,7 +3173,7 @@ def model(self): define_table(tablename, Field("name", label = T("Name"), - requires = IS_NOT_EMPTY(), + requires = [IS_NOT_EMPTY(), IS_LENGTH(512, minsize=1)], ), s3_comments(), *s3_meta_fields()) @@ -3587,7 +3587,7 @@ def model(self): define_table(tablename, Field("name", length=64, notnull=True, unique=True, requires = [IS_NOT_EMPTY(), - IS_LENGTH(64), + IS_LENGTH(64, minsize=1), IS_NOT_ONE_OF(db, "%s.name" % tablename, ), @@ -4025,7 +4025,7 @@ def model(self): define_table(tablename, Field("name", label = T("Type"), - requires = IS_NOT_EMPTY(), + requires = [IS_NOT_EMPTY(), IS_LENGTH(512, minsize=1)], ), s3_comments(), *s3_meta_fields()) @@ -4244,7 +4244,7 @@ def model(self): define_table(tablename, Field("name", label = T("Type"), - requires = IS_NOT_EMPTY(), + requires = [IS_NOT_EMPTY(), IS_LENGTH(512, minsize=1)], ), s3_comments(), *s3_meta_fields()) @@ -4275,7 +4275,7 @@ def model(self): tablename = "dvr_income_source" define_table(tablename, Field("name", - requires = IS_NOT_EMPTY(), + requires = [IS_NOT_EMPTY(), IS_LENGTH(512, minsize=1)], ), s3_comments(), *s3_meta_fields()) @@ -4464,7 +4464,7 @@ def model(self): tablename = "dvr_residence_status_type" define_table(tablename, Field("name", - requires = IS_NOT_EMPTY(), + requires = [IS_NOT_EMPTY(), IS_LENGTH(512, minsize=1)], ), s3_comments(), *s3_meta_fields()) @@ -4512,7 +4512,7 @@ def model(self): tablename = "dvr_residence_permit_type" define_table(tablename, Field("name", - requires = IS_NOT_EMPTY(), + requires = [IS_NOT_EMPTY(), IS_LENGTH(512, minsize=1)], ), s3_comments(), *s3_meta_fields()) @@ -4859,7 +4859,7 @@ def model(self): Field("code", notnull=True, length=64, unique=True, label = T("Code"), requires = [IS_NOT_EMPTY(), - IS_LENGTH(64), + IS_LENGTH(64, minsize=1), IS_NOT_ONE_OF(db, "dvr_case_event_type.code", ), @@ -4867,7 +4867,7 @@ def model(self): ), Field("name", label = T("Name"), - requires = IS_NOT_EMPTY(), + requires = [IS_NOT_EMPTY(), IS_LENGTH(512, minsize=1)], ), Field("is_inactive", "boolean", default = False, @@ -5499,7 +5499,7 @@ def model(self): define_table(tablename, Field("name", label = T("Type of Vulnerability"), - requires = IS_NOT_EMPTY(), + requires = [IS_NOT_EMPTY(), IS_LENGTH(512, minsize=1)], ), # This form of hierarchy may not work on all Databases: Field("parent", "reference dvr_vulnerability_type", @@ -5681,7 +5681,7 @@ def model(self): define_table(tablename, Field("name", label = T("Name"), - requires = IS_NOT_EMPTY(), + requires = [IS_NOT_EMPTY(), IS_LENGTH(512, minsize=1)], ), s3_comments(), *s3_meta_fields()) @@ -7562,7 +7562,9 @@ def registration_form(self, r, **attr): # Standard form fields and data formfields = [Field("label", label = T("ID"), - requires = IS_NOT_EMPTY(error_message=T("Enter or scan an ID")), + requires = [IS_NOT_EMPTY(error_message=T("Enter or scan an ID")), + IS_LENGTH(512, minsize=1), + ], ), Field("person", label = "", diff --git a/modules/s3db/hrm.py b/modules/s3db/hrm.py index 4745067dec..e0861225ac 100644 --- a/modules/s3db/hrm.py +++ b/modules/s3db/hrm.py @@ -1162,8 +1162,9 @@ def hrm_search_ac(r, **attr): MAX_SEARCH_RESULTS = settings.get_search_max_results() if (not limit or limit > MAX_SEARCH_RESULTS) and resource.count() > MAX_SEARCH_RESULTS: output = [ - dict(label=str(current.T("There are more than %(max)s results, please input more characters.") % \ - dict(max=MAX_SEARCH_RESULTS))) + {"label": str(current.T("There are more than %(max)s results, please input more characters.") % \ + {"max": MAX_SEARCH_RESULTS}), + }, ] else: fields = ["id", @@ -1177,10 +1178,10 @@ def hrm_search_ac(r, **attr): fields.append("organisation_id$name") name_format = settings.get_pr_name_format() - test = name_format % dict(first_name=1, - middle_name=2, - last_name=3, - ) + test = name_format % {"first_name": 1, + "middle_name": 2, + "last_name": 3, + } test = "".join(ch for ch in test if ch in ("1", "2", "3")) if test[:1] == "1": orderby = "pr_person.first_name" @@ -1899,7 +1900,8 @@ def model(self): # _class="s3_add_resource_link", # _href=URL(f="position", # args="create", - # vars=dict(format="popup")), + # vars={"format": "popup"} + # ), # _target="top", # _title=label_create), # DIV(_class="tooltip", @@ -2402,7 +2404,8 @@ def model(self): # _class="s3_add_resource_link", # _href=URL(f="skill_provision", # args="create", - # vars=dict(format="popup")), + # vars={"format": "popup"}, + # ), # _target="top", # _title=label_create), # DIV(_class="tooltip", @@ -2513,8 +2516,8 @@ def model(self): requires = IS_EMPTY_OR( IS_ONE_OF(db, "hrm_course.id", course_represent, - filterby="organisation_id", - filter_opts=filter_opts, + filterby = "organisation_id", + filter_opts = filter_opts, )), sortby = "name", comment = course_help, @@ -5165,11 +5168,11 @@ def apply_method(self, r, **attr): if r.representation == "popup": # Don't redirect, so we retain popup extension & so close popup response.confirmation = T("%(number)s assigned") % \ - dict(number=added) + {"number": added} output = {} else: current.session.confirmation = T("%(number)s assigned") % \ - dict(number=added) + {"number": added} if added > 0: redirect(URL(args=[r.id, self.next_tab], vars={})) else: @@ -5319,7 +5322,7 @@ def apply_method(self, r, **attr): response.view = "list_filter.html" output = {"items": items, - "title": T("Assign %(staff)s") % dict(staff=STAFF), + "title": T("Assign %(staff)s") % {"staff": STAFF}, "list_filter_form": ff, } @@ -6982,10 +6985,11 @@ def postp(r,output): S3CRUD.action_buttons(r) args = ["[id]", "group_membership"] - s3.actions.append(dict(label=str(T("Add to a Team")), - _class="action-btn", - url = URL(f = "person", - args = args)) + s3.actions.append({"label": str(T("Add to a Team")), + "_class": "action-btn", + "url": URL(f = "person", + args = args), + } ) return output s3.postp = postp @@ -7276,6 +7280,10 @@ def prep(r): deploy = c == "deploy" vol = c == "vol" + if deploy: + # Apply availability filter + s3db.deploy_availability_filter(r) + if s3.rtl: # Ensure that + appears at the beginning of the number # - using table alias to only apply to filtered component @@ -7468,6 +7476,18 @@ def prep(r): if settings.get_hrm_use_education(): profile_widgets.insert(-1, education_widget) + # Organizer-widget to record periods of unavailability: + #profile_widgets.append({"label": "Unavailability", + # "type": "organizer", + # "tablename": "deploy_unavailability", + # "master": "pr_person/%s" % person_id, + # "component": "unavailability", + # "icon": "calendar", + # "url": URL(c="deploy", f="person", + # args = [person_id, "unavailability"], + # ), + # }) + # Configure resource s3db.configure("hrm_human_resource", profile_cols = 1, @@ -7494,7 +7514,7 @@ def prep(r): if deploy: deploy_team = settings.get_deploy_team_label() s3.crud_strings["hrm_human_resource"]["title_list"] = \ - T("%(team)s Members") % dict(team=T(deploy_team)) + T("%(team)s Members") % {"team": T(deploy_team)} else: s3.crud_strings["hrm_human_resource"]["title_list"] = \ T("Staff & Volunteers") @@ -7848,7 +7868,7 @@ def hrm_person_controller(**attr): title_update = T("Staff Member Details") ) # Upload for configuration (add replace option) - s3.importerPrep = lambda: dict(ReplaceOption=T("Remove existing data before import")) + s3.importerPrep = lambda: {"ReplaceOption": T("Remove existing data before import")} # Import pre-process def import_prep(data, group=group): @@ -8069,17 +8089,18 @@ def postp(r, output): # REST Interface #orgname = session.s3.hrm.orgname - _attr = dict(csv_stylesheet = ("hrm", "person.xsl"), - csv_template = "staff", - csv_extra_fields = [dict(label="Type", - field=s3db.hrm_human_resource.type), - ], - # Better in the native person controller (but this isn't always accessible): - #deduplicate = "", - #orgname = orgname, - replace_option = T("Remove existing data before import"), - rheader = hrm_rheader, - ) + _attr = {"csv_stylesheet": ("hrm", "person.xsl"), + "csv_template": "staff", + "csv_extra_fields": [{"label": "Type", + "field": s3db.hrm_human_resource.type, + }, + ], + # Better in the native person controller (but this isn't always accessible): + #"deduplicate": "", + #"orgname": orgname, + "replace_option": T("Remove existing data before import"), + "rheader": hrm_rheader, + } _attr.update(attr) return current.rest_controller("pr", "person", **_attr) @@ -8147,9 +8168,10 @@ def prep(r): return current.rest_controller("hrm", "training", csv_stylesheet = ("hrm", "training.xsl"), csv_template = ("hrm", "training"), - csv_extra_fields=[dict(label="Training Event", - field=s3db.hrm_training.training_event_id), - ], + csv_extra_fields = [{"label": "Training Event", + "field": s3db.hrm_training.training_event_id, + }, + ], ) # ============================================================================= @@ -8550,139 +8572,139 @@ def row_actions(r, list_id): if experience: tablename = "hrm_experience" r.customise_resource(tablename) - widget = dict(# Use CRUD Strings (easier to customise) - #label = "Experience", - #label_create = "Add Experience", - type = "datatable", - actions = dt_row_actions("experience", tablename), - tablename = tablename, - context = "person", - filter = FS("assignment__link.assignment_id") == None, - create_controller = controller, - create_function = "person", - create_component = "experience", - pagesize = None, # all records - # Settings suitable for RMSAmericas - list_fields = ["start_date", - "end_date", - "employment_type", - "organisation", - "job_title", - ], - ) + widget = {# Use CRUD Strings (easier to customise) + #"label": "Experience", + #"label_create": "Add Experience", + "type": "datatable", + "actions": dt_row_actions("experience", tablename), + "tablename": tablename, + "context": "person", + "filter": FS("assignment__link.assignment_id") == None, + "create_controller": controller, + "create_function": "person", + "create_component": "experience", + "pagesize": None, # all records + # Settings suitable for RMSAmericas + "list_fields": ["start_date", + "end_date", + "employment_type", + "organisation", + "job_title", + ], + } profile_widgets.append(widget) if missions: tablename = "hrm_experience" - widget = dict(label = "Missions", - type = "datatable", - actions = dt_row_actions("experience", tablename), - tablename = tablename, - context = "person", - filter = FS("assignment__link.assignment_id") != None, - insert = False, - pagesize = None, # all records - # Settings suitable for RMSAmericas - list_fields = ["start_date", - "end_date", - "location_id", - #"organisation_id", - "job_title_id", - "job_title", - ], - ) + widget = {"label": "Missions", + "type": "datatable", + "actions": dt_row_actions("experience", tablename), + "tablename": tablename, + "context": "person", + "filter": FS("assignment__link.assignment_id") != None, + "insert": False, + "pagesize": None, # all records + # Settings suitable for RMSAmericas + "list_fields": ["start_date", + "end_date", + "location_id", + #"organisation_id", + "job_title_id", + "job_title", + ], + } profile_widgets.append(widget) if settings.get_hrm_use_trainings(): tablename = "hrm_training" if settings.get_hrm_trainings_external(): - widget = dict(label = "Internal Training", - label_create = "Add Internal Training", - type = "datatable", - actions = dt_row_actions("training", tablename), - tablename = tablename, - context = "person", - filter = FS("course_id$external") == False, - create_controller = controller, - create_function = "person", - create_component = "training", - pagesize = None, # all records - ) + widget = {"label": "Internal Training", + "label_create": "Add Internal Training", + "type": "datatable", + "actions": dt_row_actions("training", tablename), + "tablename": tablename, + "context": "person", + "filter": FS("course_id$external") == False, + "create_controller": controller, + "create_function": "person", + "create_component": "training", + "pagesize": None, # all records + } profile_widgets.append(widget) - widget = dict(label = "External Training", - label_create = "Add External Training", - type = "datatable", - actions = dt_row_actions("training", tablename), - tablename = tablename, - context = "person", - filter = FS("course_id$external") == True, - create_controller = controller, - create_function = "person", - create_component = "training", - pagesize = None, # all records - ) + widget = {"label": "External Training", + "label_create": "Add External Training", + "type": "datatable", + "actions": dt_row_actions("training", tablename), + "tablename": tablename, + "context": "person", + "filter": FS("course_id$external") == True, + "create_controller": controller, + "create_function": "person", + "create_component": "training", + "pagesize": None, # all records + } profile_widgets.append(widget) else: - widget = dict(label = "Training", - label_create = "Add Training", - type = "datatable", - actions = dt_row_actions("training", tablename), - tablename = tablename, - context = "person", - create_controller = controller, - create_function = "person", - create_component = "training", - pagesize = None, # all records - ) + widget = {"label": "Training", + "label_create": "Add Training", + "type": "datatable", + "actions": dt_row_actions("training", tablename), + "tablename": tablename, + "context": "person", + "create_controller": controller, + "create_function": "person", + "create_component": "training", + "pagesize": None, # all records + } profile_widgets.append(widget) if settings.get_hrm_use_skills(): tablename = "hrm_competency" r.customise_resource(tablename) - widget = dict(# Use CRUD Strings (easier to customise) - #label = label, - #label_create = "Add Skill", - type = "datatable", - actions = dt_row_actions("competency", tablename), - tablename = tablename, - context = "person", - create_controller = controller, - create_function = "person", - create_component = "competency", - pagesize = None, # all records - ) + widget = {# Use CRUD Strings (easier to customise) + #"label": label, + #"label_create": "Add Skill", + "type": "datatable", + "actions": dt_row_actions("competency", tablename), + "tablename": tablename, + "context": "person", + "create_controller": controller, + "create_function": "person", + "create_component": "competency", + "pagesize": None, # all records + } profile_widgets.append(widget) if settings.get_hrm_use_certificates(): tablename = "hrm_certification" - widget = dict(label = "Certificates", - label_create = "Add Certificate", - type = "datatable", - actions = dt_row_actions("certification", tablename), - tablename = tablename, - context = "person", - create_controller = controller, - create_function = "person", - create_component = "certification", - pagesize = None, # all records - ) + widget = {"label": "Certificates", + "label_create": "Add Certificate", + "type": "datatable", + "actions": dt_row_actions("certification", tablename), + "tablename": tablename, + "context": "person", + "create_controller": controller, + "create_function": "person", + "create_component": "certification", + "pagesize": None, # all records + } profile_widgets.append(widget) # Person isn't a doc_id #if settings.has_module("doc"): # tablename = "doc_document" - # widget = dict(label = "Documents", - # label_create = "Add Document", - # type = "datatable", - # actions = dt_row_actions("document", tablename), - # tablename = tablename, - # filter = FS("doc_id") == record.doc_id, - # icon = "attachment", - # create_controller = controller, - # create_function = "person", - # create_component = "document", - # pagesize = None, # all records - # ) + # widget = {"label": "Documents", + # "label_create": "Add Document", + # "type": "datatable", + # "actions": dt_row_actions("document", tablename), + # "tablename": tablename, + # "filter": FS("doc_id") == record.doc_id, + # "icon": "attachment", + # "create_controller": controller, + # "create_function": "person", + # "create_component": "document", + # "pagesize": None, # all records + # } # profile_widgets.append(widget) if r.representation == "html": @@ -8812,12 +8834,12 @@ def dt_row_actions(component): code.readable = code.writable = True profile_widgets = [ - dict(label = label, - type = "form", - tablename = "hrm_human_resource", - context = "person", - filter = widget_filter, - ) + {"label": label, + "type": "form", + "tablename": "hrm_human_resource", + "context": "person", + "filter": widget_filter, + }, ] if VOL: @@ -8844,19 +8866,19 @@ def dt_row_actions(component): list_fields.append("job_title_id") list_fields.append("hours") crud_strings_ = crud_strings[tablename] - hours_widget = dict(label = crud_strings_["title_list"], - label_create = crud_strings_["label_create"], - type = "datatable", - actions = dt_row_actions("hours"), - tablename = tablename, - context = "person", - filter = filter_, - list_fields = list_fields, - create_controller = controller, - create_function = "person", - create_component = "hours", - pagesize = None, # all records - ) + hours_widget = {"label": crud_strings_["title_list"], + "label_create": crud_strings_["label_create"], + "type": "datatable", + "actions": dt_row_actions("hours"), + "tablename": tablename, + "context": "person", + "filter": filter_, + "list_fields": list_fields, + "create_controller": controller, + "create_function": "person", + "create_component": "hours", + "pagesize": None, # all records + } profile_widgets.append(hours_widget) elif vol_experience == "activity": # Exclude records which are just to link to Activity & also Training Hours @@ -8871,21 +8893,21 @@ def dt_row_actions(component): #if s3db.vol_activity_hours.job_title_id.readable: # list_fields.append("job_title_id") #list_fields.append("hours") - hours_widget = dict(label = "Activity Hours", - # Don't Add Hours here since the Activity List will be very hard to find the right one in - insert = False, - #label_create = "Add Activity Hours", - type = "datatable", - actions = dt_row_actions("hours"), - tablename = "vol_activity_hours", - context = "person", - #filter = filter_, - list_fields = list_fields, - #create_controller = controller, - #create_function = "person", - #create_component = "activity_hours", - pagesize = None, # all records - ) + hours_widget = {"label": "Activity Hours", + # Don't Add Hours here since the Activity List will be very hard to find the right one in + "insert": False, + #"label_create": "Add Activity Hours", + "type": "datatable", + "actions": dt_row_actions("hours"), + "tablename": "vol_activity_hours", + "context": "person", + #"filter": filter_, + "list_fields": list_fields, + #"create_controller": controller, + #"create_function": "person", + #"create_component": "activity_hours", + "pagesize": None, # all records + } profile_widgets.append(hours_widget) teams = settings.get_hrm_teams() @@ -8895,17 +8917,17 @@ def dt_row_actions(component): label_create = "Add Team" elif teams == "Groups": label_create = "Add Group" - teams_widget = dict(label = teams, - label_create = label_create, - type = "datatable", - actions = dt_row_actions("group_membership"), - tablename = "pr_group_membership", - context = "person", - create_controller = controller, - create_function = "person", - create_component = "group_membership", - pagesize = None, # all records - ) + teams_widget = {"label": teams, + "label_create": label_create, + "type": "datatable", + "actions": dt_row_actions("group_membership"), + "tablename": "pr_group_membership", + "context": "person", + "create_controller": controller, + "create_function": "person", + "create_component": "group_membership", + "pagesize": None, # all records + } profile_widgets.append(teams_widget) if controller == "hrm": @@ -8934,13 +8956,13 @@ def experience_row_actions(component): ] # Configure widget, apply overrides - widget = dict(label = T("Experience"), - label_create = T("Add Experience"), - type = "datatable", - actions = experience_row_actions("experience"), - tablename = "hrm_experience", - pagesize = None, # all records - ) + widget = {"label": T("Experience"), + "label_create": T("Add Experience"), + "type": "datatable", + "actions": experience_row_actions("experience"), + "tablename": "hrm_experience", + "pagesize": None, # all records + } if isinstance(org_experience, dict): widget.update(org_experience) @@ -8960,62 +8982,62 @@ def experience_row_actions(component): # (=> defaults to vol-style experience form) # Configure widget and apply overrides - widget = dict(label = "Experience", - label_create = "Add Experience", - type = "datatable", - actions = dt_row_actions("experience"), - tablename = "hrm_experience", - context = "person", - create_controller = controller, - create_function = "person", - create_component = "experience", - pagesize = None, # all records - ) + widget = {"label": "Experience", + "label_create": "Add Experience", + "type": "datatable", + "actions": dt_row_actions("experience"), + "tablename": "hrm_experience", + "context": "person", + "create_controller": controller, + "create_function": "person", + "create_component": "experience", + "pagesize": None, # all records + } if isinstance(other_experience, dict): widget.update(other_experience) profile_widgets.append(widget) if self.awards: - widget = dict(label = T("Awards"), - label_create = T("Add Award"), - type = "datatable", - actions = dt_row_actions("staff_award"), - tablename = "hrm_award", - context = "person", - create_controller = controller, - create_function = "person", - create_component = "staff_award", - pagesize = None, # all records - ) + widget = {"label": T("Awards"), + "label_create": T("Add Award"), + "type": "datatable", + "actions": dt_row_actions("staff_award"), + "tablename": "hrm_award", + "context": "person", + "create_controller": controller, + "create_function": "person", + "create_component": "staff_award", + "pagesize": None, # all records + } profile_widgets.append(widget) if self.disciplinary_record: - widget = dict(label = T("Disciplinary Record"), - label_create = T("Add Disciplinary Action"), - type = "datatable", - actions = dt_row_actions("disciplinary_action"), - tablename = "hrm_disciplinary_action", - context = "person", - create_controller = controller, - create_function = "person", - create_component = "disciplinary_action", - pagesize = None, # all records - ) + widget = {"label": T("Disciplinary Record"), + "label_create": T("Add Disciplinary Action"), + "type": "datatable", + "actions": dt_row_actions("disciplinary_action"), + "tablename": "hrm_disciplinary_action", + "context": "person", + "create_controller": controller, + "create_function": "person", + "create_component": "disciplinary_action", + "pagesize": None, # all records + } profile_widgets.append(widget) if self.salary: - widget = dict(label = T("Salary"), - label_create = T("Add Salary"), - type = "datatable", - actions = dt_row_actions("salary"), - tablename = "hrm_salary", - context = "person", - create_controller = controller, - create_function = "person", - create_component = "salary", - pagesize = None, # all records - ) + widget = {"label": T("Salary"), + "label_create": T("Add Salary"), + "type": "datatable", + "actions": dt_row_actions("salary"), + "tablename": "hrm_salary", + "context": "person", + "create_controller": controller, + "create_function": "person", + "create_component": "salary", + "pagesize": None, # all records + } profile_widgets.append(widget) if representation == "html": @@ -9173,10 +9195,10 @@ def hrm_configure_pr_group_membership(): (site_label, "person_id$human_resource.site_id"), ] name_format = settings.get_pr_name_format() - test = name_format % dict(first_name=1, - middle_name=2, - last_name=3, - ) + test = name_format % {"first_name": 1, + "middle_name": 2, + "last_name": 3, + } test = "".join(ch for ch in test if ch in ("1", "2", "3")) if test[:1] == "1": orderby = "pr_person.first_name" @@ -9820,6 +9842,16 @@ def hrm_human_resource_filters(resource_type = None, if module == "deploy": # Deployment-specific filters + # Availability Filter + append_filter(S3DateFilter("available", + label = T("Available for Deployment"), + # Use custom selector to prevent automatic + # parsing (which would result in an error) + selector = "available", + hide_time = True, + hidden = True, + )) + # Job title filter append_filter(S3OptionsFilter("credential.job_title_id", # @ToDo: deployment_setting for label (this is RDRT-specific) diff --git a/modules/s3db/pr.py b/modules/s3db/pr.py index 87e633ec81..0d34939835 100644 --- a/modules/s3db/pr.py +++ b/modules/s3db/pr.py @@ -44,6 +44,7 @@ "PRDescriptionModel", "PREducationModel", "PRIdentityModel", + "PRLanguageModel", "PROccupationModel", "PRPersonDetailsModel", "PRPersonTagModel", @@ -1045,6 +1046,7 @@ def model(self): "multiple": False, }, cr_shelter_registration_history = "person_id", + deploy_unavailability = "person_id", project_activity_person = "person_id", supply_distribution_person = "person_id", event_incident = {"link": "event_human_resource", @@ -5386,6 +5388,73 @@ def model(self): # return {} +# ============================================================================= +class PRLanguageModel(S3Model): + """ + Languages for Persons + - alternate model to Skills for alternate UX + - used by IFRC RDRP AP + """ + + names = ("pr_language",) + + def model(self): + + T = current.T + + # --------------------------------------------------------------------- + # Language + # + fluency_opts = {1: T("Beginner"), + 2: T("Intermediate"), + 3: T("Advanced"), + 4: T("Native language"), + } + + tablename = "pr_language" + self.define_table(tablename, + self.pr_person_id(label = T("Person"), + ondelete = "CASCADE", + ), + s3_language(), + Field("writing", "integer", + #default = None, + label = T("Writing"), + represent = S3Represent(options=fluency_opts), + requires = IS_IN_SET(fluency_opts), + ), + Field("speaking", "integer", + #default = None, + label = T("Speaking"), + represent = S3Represent(options=fluency_opts), + requires = IS_IN_SET(fluency_opts), + ), + Field("understanding", "integer", + #default = None, + label = T("Understanding"), + represent = S3Represent(options=fluency_opts), + requires = IS_IN_SET(fluency_opts), + ), + #s3_comments(), + *s3_meta_fields()) + + # CRUD Strings + current.response.s3.crud_strings[tablename] = Storage( + label_create = T("Add Language"), + title_display = T("Language Details"), + title_list = T("Languages"), + title_update = T("Edit Language"), + label_list_button = T("List Languages"), + msg_record_created = T("Language added"), + msg_record_modified = T("Language updated"), + msg_record_deleted = T("Language deleted"), + msg_list_empty = T("No Languages currently registered for this person")) + + # --------------------------------------------------------------------- + # Pass names back to global scope (s3.*) + # + return {} + # ============================================================================= class PROccupationModel(S3Model): """ diff --git a/modules/s3db/setup.py b/modules/s3db/setup.py index 25b3bff91a..c90dbab75e 100644 --- a/modules/s3db/setup.py +++ b/modules/s3db/setup.py @@ -2037,6 +2037,8 @@ def setup_run_playbook(playbook, hosts, tags=None, private_key=None): inventory = InventoryManager(loader=loader, sources=sources) variable_manager = VariableManager(loader=loader, inventory=inventory) + + # Broken with Ansible 2.8 # https://github.com/ansible/ansible/issues/21562 tmp_path = os.path.join("/", "tmp") variable_manager.extra_vars = {"ansible_local_tmp": tmp_path, @@ -2048,6 +2050,7 @@ def setup_run_playbook(playbook, hosts, tags=None, private_key=None): inventory = inventory, variable_manager = variable_manager, loader = loader, + # Not supported in Ansible 2.8 options = options, passwords = {}, ) diff --git a/modules/s3db/vehicle.py b/modules/s3db/vehicle.py index e51544412e..2ea94bb82b 100644 --- a/modules/s3db/vehicle.py +++ b/modules/s3db/vehicle.py @@ -212,7 +212,8 @@ def model(self): msg_record_deleted = T("Vehicle Details deleted"), msg_list_empty = T("No Vehicle Details currently defined")) - represent = S3Represent(lookup=tablename) + represent = S3Represent(lookup = tablename) + vehicle_id = S3ReusableField("vehicle_id", "reference %s" % tablename, label = T("Vehicle"), ondelete = "RESTRICT", @@ -232,8 +233,8 @@ def model(self): # --------------------------------------------------------------------- # Pass names back to global scope (s3.*) # - return dict(vehicle_vehicle_id = vehicle_id, - ) + return {"vehicle_vehicle_id": vehicle_id, + } # ------------------------------------------------------------------------- @staticmethod @@ -244,7 +245,7 @@ def defaults(): readable = False, writable = False) - return dict(vehicle_vehicle_id = lambda **attr: dummy("vehicle_id"), - ) + return {"vehicle_vehicle_id": lambda **attr: dummy("vehicle_id"), + } # END ========================================================================= diff --git a/modules/templates/000_config.py b/modules/templates/000_config.py index bceb2f9354..201dd72e83 100644 --- a/modules/templates/000_config.py +++ b/modules/templates/000_config.py @@ -213,10 +213,8 @@ # 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" -# Change this for the offset period in days that the alert will be effective for -# Expire Date = Effective Date + expire_offset -# Default is 2 days -#settings.cap.expire_offset = 2 +# 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 diff --git a/modules/templates/DRKCM/config.py b/modules/templates/DRKCM/config.py index be0c7980b0..f5b26f9c08 100644 --- a/modules/templates/DRKCM/config.py +++ b/modules/templates/DRKCM/config.py @@ -4,7 +4,7 @@ from collections import OrderedDict -from gluon import current, A, DIV,IS_EMPTY_OR, IS_IN_SET, IS_NOT_EMPTY, SPAN, TAG, URL +from gluon import current, A, DIV,IS_EMPTY_OR, IS_IN_SET, IS_LENGTH, IS_NOT_EMPTY, SPAN, TAG, URL from gluon.storage import Storage from s3 import FS, IS_ONE_OF @@ -1006,7 +1006,7 @@ def custom_prep(r): # Last name is required field = table.last_name - field.requires = IS_NOT_EMPTY() + field.requires = [IS_NOT_EMPTY(), IS_LENGTH(512, minsize=1)] # Optional: site dates if ui_options.get("case_lodging_dates"): @@ -1942,7 +1942,7 @@ def customise_dvr_case_activity_resource(r, tablename): # Expose simple free-text subject field = table.subject field.readable = field.writable = True - field.requires = IS_NOT_EMPTY() + field.requires = [IS_NOT_EMPTY(), IS_LENGTH(512, minsize=1)] # Show need details (optional) field = table.need_details @@ -3284,7 +3284,7 @@ def custom_import_prep(data): # otherwise imports will fail before reaching de-duplicate ttable = s3db.cr_shelter_type field = ttable.name - field.requires = IS_NOT_EMPTY() + field.requires = [IS_NOT_EMPTY(), IS_LENGTH(512, minsize=1)] # Reset to standard (no need to repeat it) s3.import_prep = import_prep diff --git a/modules/templates/IFRC/Train/users.csv b/modules/templates/IFRC/Train/users.csv index 1312daa8a0..f1d815dc62 100644 --- a/modules/templates/IFRC/Train/users.csv +++ b/modules/templates/IFRC/Train/users.csv @@ -102,4 +102,4 @@ Kaikoura,Outreach Worker,kaikoura@example.com,eden,"PO_AGENCIES/org_organisation RDRT,Admin,rdrtadmin@example.com,eden,"RDRT_ADMIN/0",Africa Zone,,, RDRT,Regional,rdrtregional@example.com,eden,"RDRT_REGIONAL/0",Africa Zone,,, AP,RDRT Admin,aprdrtadmin@example.com,eden,"RDRT_ADMIN/0,AP_RDRT_ADMIN/0,message_editor",Asia-Pacific Region,,, -Lillian,Aquino,lillian.acquino@redcross.org.ph,eden,RDRT_MEMBER,Philippine Red Cross,,, \ No newline at end of file +Lillian,Aquino,lillian.acquino@redcross.org.ph,eden,RDRT_MEMBER/0,Philippine Red Cross,,, \ No newline at end of file diff --git a/modules/templates/IFRC/auth_roles.csv b/modules/templates/IFRC/auth_roles.csv index 2dc90a09cf..a490d117f9 100644 --- a/modules/templates/IFRC/auth_roles.csv +++ b/modules/templates/IFRC/auth_roles.csv @@ -3,6 +3,7 @@ ANONYMOUS,Anonymous,,,,org_organisation,,READ,,,,,Required for self-registration ANONYMOUS,Anonymous,,,,org_organisation_organisation_type,,READ,,,,,Required for self-registration ANONYMOUS,Anonymous,,org,sites_for_org,,,READ,,,,,Required for self-registration AUTHENTICATED,Authenticated,,default,person,,,READ|CREATE|UPDATE,,,,, +AUTHENTICATED,Authenticated,,deploy,experience,,,READ,,,,,Need to be able to populate Org/Location dropdowns AUTHENTICATED,Authenticated,,dc,respnse,,,,CREATE|READ|UPDATE,,,, AUTHENTICATED,Authenticated,,,,gis_location,,READ|CREATE|UPDATE,,,,, AUTHENTICATED,Authenticated,,gis,index,,,READ,,,,, @@ -808,12 +809,14 @@ PO_AGENCIES,Outreach Referral Agencies,,,,org_organisation,,READ|CREATE|UPDATE|D PO_AGENCIES,Outreach Referral Agencies,,,,org_organisation_organisation_type,,READ|CREATE|UPDATE|DELETE,,,,, PO_AGENCIES,Outreach Referral Agencies,,,,po_organisation_area,,READ|CREATE|UPDATE|DELETE,,,,, RDRT_ADMIN,RDRT Admin,,deploy,,,,ALL,,,,, +RDRT_ADMIN,RDRT Admin,,deploy,experience,,,ALL,,,,, RDRT_ADMIN,RDRT Admin,,hrm,hr_search,,,READ,,,,, RDRT_ADMIN,RDRT Admin,,hrm,skill_competencies,,,READ,,,,, RDRT_ADMIN,RDRT Admin,,pr,address,,,READ|CREATE|UPDATE|DELETE,,,,, RDRT_ADMIN,RDRT Admin,,pr,contact,,,READ|CREATE|UPDATE|DELETE,,,,, RDRT_ADMIN,RDRT Admin,,pr,contact_emergency,,,READ|CREATE|UPDATE|DELETE,,,,, RDRT_ADMIN,RDRT Admin,,pr,education,,,READ|CREATE|UPDATE|DELETE,,,,, +RDRT_ADMIN,RDRT Admin,,pr,language,,,READ|CREATE|UPDATE|DELETE,,,,, RDRT_ADMIN,RDRT Admin,,pr,person,,,READ|CREATE|UPDATE,,,,, RDRT_ADMIN,RDRT Admin,,,,doc_document,,READ|CREATE|UPDATE|DELETE,,,,, RDRT_ADMIN,RDRT Admin,,,,hrm_human_resource,,READ|CREATE|UPDATE,,,,, @@ -829,13 +832,24 @@ RDRT_ADMIN,RDRT Admin,,,,hrm_training,,READ|CREATE|UPDATE|DELETE,,,,, AP_RDRT_ADMIN,AP RDRT Admin,,doc,,,,ALL,,,,, AP_RDRT_ADMIN,AP RDRT Admin,,pr,group,,,READ|CREATE|UPDATE|DELETE,,,,, AP_RDRT_ADMIN,AP RDRT Admin,,,,pr_group_membership,,READ|CREATE|UPDATE|DELETE,,,,, -RDRT_MEMBER,AP RDRT Member,,deploy,,,,READ,,,,, -RDRT_MEMBER,AP RDRT Member,,,,deploy_mission,Asia-Pacific Region,READ,,,,, -RDRT_MEMBER,AP RDRT Member,,,,doc_document,Asia-Pacific Region,READ,,,,, -RDRT_MEMBER,AP RDRT Member,,,,doc_document,,,READ,,,, -RDRT_MEMBER,AP RDRT Member,,,,hrm_competency,,,READ,,,, -RDRT_MEMBER,AP RDRT Member,,,,hrm_credential,,,READ,,,, -RDRT_MEMBER,AP RDRT Member,,,,hrm_experience,,,READ,,,, +RDRT_MEMBER,AP RDRT Member,,deploy,,,,,READ|CREATE|UPDATE|DELETE,,,, +RDRT_MEMBER,AP RDRT Member,,deploy,experience,,,,READ|CREATE|UPDATE|DELETE,,,, +RDRT_MEMBER,AP RDRT Member,,pr,address,,,CREATE,READ|CREATE|UPDATE|DELETE,,,, +RDRT_MEMBER,AP RDRT Member,,pr,contact,,,CREATE,READ|CREATE|UPDATE|DELETE,,,, +RDRT_MEMBER,AP RDRT Member,,pr,contact_emergency,,,CREATE,READ|CREATE|UPDATE|DELETE,,,, +RDRT_MEMBER,AP RDRT Member,,pr,education,,,CREATE,READ|CREATE|UPDATE|DELETE,,,, +RDRT_MEMBER,AP RDRT Member,,pr,language,,,CREATE,READ|CREATE|UPDATE|DELETE,,,, +RDRT_MEMBER,AP RDRT Member,,doc,document,,,CREATE,READ|CREATE|UPDATE|DELETE,,,, +RDRT_MEMBER,AP RDRT Member,,,,doc_document,,CREATE,READ|CREATE|UPDATE|DELETE,,,, +RDRT_MEMBER,AP RDRT Member,,,,hrm_competency,,CREATE,READ|CREATE|UPDATE|DELETE,,,, +RDRT_MEMBER,AP RDRT Member,,,,hrm_human_resource,,,READ|UPDATE,,,, +RDRT_MEMBER,AP RDRT Member,,,,hrm_experience,,CREATE,READ|CREATE|UPDATE|DELETE,,,, +RDRT_MEMBER,AP RDRT Member,,,,hrm_skill,,READ,,,,, +RDRT_MEMBER,AP RDRT Member,,,,pr_address,,CREATE,READ|CREATE|UPDATE|DELETE,,,, +RDRT_MEMBER,AP RDRT Member,,,,pr_contact,,CREATE,READ|CREATE|UPDATE|DELETE,,,, +RDRT_MEMBER,AP RDRT Member,,,,pr_contact_emergency,,CREATE,READ|CREATE|UPDATE|DELETE,,,, +RDRT_MEMBER,AP RDRT Member,,,,pr_education,,CREATE,READ|CREATE|UPDATE|DELETE,,,, +RDRT_MEMBER,AP RDRT Member,,,,pr_person,,,READ|UPDATE,,,, RDRT_REGIONAL,RDRT Regional Manager,,deploy,,,,ALL,,,,, RDRT_REGIONAL,RDRT Regional Manager,,deploy,email_channel,,,NONE,,,,, RDRT_REGIONAL,RDRT Regional Manager,,,,deploy_mission,,READ|UPDATE,,,,, diff --git a/modules/templates/IFRC/config.py b/modules/templates/IFRC/config.py index 40dabd0c44..a1c9e862c9 100644 --- a/modules/templates/IFRC/config.py +++ b/modules/templates/IFRC/config.py @@ -3620,37 +3620,114 @@ def customise_hrm_department_controller(**attr): # ------------------------------------------------------------------------- def customise_hrm_experience_controller(**attr): - s3 = current.response.s3 + get_vars_get = current.request.get_vars.get + if get_vars_get("rdrt_ap_current"): + from s3 import S3SQLCustomForm + s3db = current.s3db + table = s3db.hrm_experience + f = table.organisation + f.readable = f.writable = True + f = table.comments + f.comment = None + f = table.job_title + f.readable = f.writable = True + crud_form = S3SQLCustomForm("organisation", + (T("From"), "start_date"), + (T("To"), "end_date"), + (T("Total experience"), "comments"), + (T("Designation(s)"), "job_title"), + ) + s3db.configure("hrm_experience", + crud_form = crud_form, + ) + elif get_vars_get("rdrt_ap_deployment"): + from gluon import IS_EMPTY_OR + from s3 import s3_comments_widget, IS_ONE_OF, S3Represent, S3SQLCustomForm#, S3MultiSelectWidget + db = current.db + s3db = current.s3db + + # Limit Orgs to RC roots + ttable = s3db.org_organisation_type + try: + type_id = db(ttable.name == "Red Cross / Red Crescent").select(ttable.id, + limitby=(0, 1), + cache = s3db.cache, + ).first().id + except: + # No IFRC prepop done - skip (e.g. testing impacts of CSS changes in this theme) + return + ltable = s3db.org_organisation_organisation_type + rows = db(ltable.organisation_type_id == type_id).select(ltable.organisation_id) + filter_opts = [row.organisation_id for row in rows] + btable = s3db.org_organisation_branch + rows = db((btable.deleted != True) & + (btable.branch_id.belongs(filter_opts))).select(btable.branch_id) + filter_opts = list(set(filter_opts) - set(row.branch_id for row in rows)) - root_org = current.auth.root_org_name() - vnrc = False - if root_org == VNRC: - vnrc = True + table = s3db.hrm_experience + table.organisation_id.requires = IS_ONE_OF(db, "org_organisation.id", + s3db.org_OrganisationRepresent(acronym = False, + parent = False), + filterby = "id", + filter_opts = filter_opts, + orderby = "org_organisation.name", + sort = True) + f = table.comments + f.comment = None + country_represent = S3Represent(lookup="gis_location", translate=True) + f = table.location_id + f.default = None + f.represent = country_represent + f.requires = IS_EMPTY_OR(IS_ONE_OF(db, "gis_location.id", + country_represent, + filterby = "level", + filter_opts = ["L0"], + sort=True)) + f.widget = None + #f.widget = S3MultiSelectWidget(multiple=False) + table.responsibilities.widget = s3_comments_widget + crud_form = S3SQLCustomForm("organisation_id", + (T("From"), "start_date"), + (T("To"), "end_date"), + (T("Disaster situation (flood / earthquake, etc.)"), "comments"), + (T("Country of Deployment"), "location_id"), + (T("Skills Utilized"), "responsibilities"), + ) + s3db.configure("hrm_experience", + crud_form = crud_form, + ) + else: + s3 = current.response.s3 - standard_prep = s3.prep - def custom_prep(r): - # Call standard prep - if callable(standard_prep): - if not standard_prep(r): - return False + root_org = current.auth.root_org_name() + vnrc = False + if root_org == VNRC: + vnrc = True - if vnrc: - department_id = r.table.department_id - department_id.readable = department_id.writable = True + standard_prep = s3.prep + def custom_prep(r): + # Call standard prep + if callable(standard_prep): + if not standard_prep(r): + return False - if r.controller == "deploy": - # Popups in RDRT Member Profile + if vnrc: + department_id = r.table.department_id + department_id.readable = department_id.writable = True - table = r.table + if r.controller == "deploy": + # Popups in RDRT Africa Member Profile - job_title_id = table.job_title_id - _customise_job_title_field(job_title_id, r) - job_title_id.label = T("Sector / Area of Expertise") + table = r.table - job_title = table.job_title - job_title.readable = job_title.writable = True - return True - s3.prep = custom_prep + job_title_id = table.job_title_id + _customise_job_title_field(job_title_id, r) + job_title_id.label = T("Sector / Area of Expertise") + + job_title = table.job_title + job_title.readable = job_title.writable = True + return True + s3.prep = custom_prep return attr @@ -4564,181 +4641,388 @@ def roles(row): s3db.hrm_human_resource.organisation_id.requires = s3db.org_organisation_requires(required=True) AP = _is_asia_pacific() - if AP: - otable = s3db.org_organisation - org = db(otable.name == AP_ZONE).select(otable.id, - limitby=(0, 1), - cache = s3db.cache, - ).first() - try: - organisation_id = org.id - except: - current.log.error("Cannot find org %s - prepop not done?" % AP_ZONE) - organisation_id = None + + if not AP: + # Africa + # Exclude None-values for training course pivot axis + s3db.configure(tablename, + report_exclude_empty = ("training.course_id", + ), + ) + + if r.record: + widgets = [{"label": "Personal Details", + "tablename": "pr_person", + "type": "datalist", + "insert": False, + "list_fields": ["first_name", + # Not in form!? + "middle_name", + "last_name", + "date_of_birth", + "gender", + # Not in form!? + "person_details.nationality", + # Not in form!? + "physical_description.blood_type", + ], + "filter": FS("id") == r.record.person_id, + "icon": "user", + }, + ] else: - # Filter trainings to courses which belong to - # the AP_ZONE organisation: - ctable = s3db.hrm_course - query = (ctable.organisation_id == organisation_id) & \ - (ctable.deleted != True) - courses = db(query).select(ctable.id) - course_ids = [c.id for c in courses] - s3db.add_components(tablename, - hrm_training = {"link": "pr_person", - "joinby": "id", - "key": "id", - "fkey": "person_id", - "pkey": "person_id", - "filterby": {"course_id": course_ids, - }, - }, - ) - # Reset the component (we're past resource initialization) - r.resource.components.reset(("training",)) + widgets = [] - # Exclude None-values for training course pivot axis - s3db.configure(tablename, - report_exclude_empty = ("training.course_id", - ), - ) + append_widget = widgets.append - # Custom profile widgets for hrm_competency ("skills"): - subsets = (("Computer", "Computer Skills", "Add Computer Skills"), - ("Language", "Language Skills", "Add Language Skills"), - ) - widgets = [] - append_widget = widgets.append - profile_widgets = get_config("profile_widgets") - contacts_filter = None - while profile_widgets: - widget = profile_widgets.pop(0) - w_tablename = widget["tablename"] - if w_tablename == "hrm_competency": - for skill_type, label, label_create in subsets: - query = widget["filter"] & \ - (FS("skill_id$skill_type_id$name") == skill_type) + # Custom profile widgets for hrm_competency ("skills"): + subsets = (("Computer", "Computer Skills", "Add Computer Skills"), + ("Language", "Language Skills", "Add Language Skills"), + ) + profile_widgets = get_config("profile_widgets") + contacts_filter = None + while profile_widgets: + widget = profile_widgets.pop(0) + w_tablename = widget["tablename"] + if w_tablename == "hrm_competency": + for skill_type, label, label_create in subsets: + query = widget["filter"] & \ + (FS("skill_id$skill_type_id$name") == skill_type) + new_widget = dict(widget) + new_widget["label"] = label + new_widget["label_create"] = label_create + new_widget["filter"] = query + append_widget(new_widget) + elif w_tablename == "hrm_experience": new_widget = dict(widget) - new_widget["label"] = label - new_widget["label_create"] = label_create - new_widget["filter"] = query + new_widget["create_controller"] = "deploy" append_widget(new_widget) - elif w_tablename == "hrm_experience": - new_widget = dict(widget) - new_widget["create_controller"] = "deploy" - append_widget(new_widget) - elif w_tablename == "hrm_training" and AP and organisation_id: - new_widget = dict(widget) - new_widget["filter"] = widget["filter"] & \ - (FS("~.course_id$organisation_id") == organisation_id) - append_widget(new_widget) - else: - append_widget(widget) - if widget["tablename"] == "pr_contact": - contacts_filter = widget["filter"] - - # Emergency contacts - if contacts_filter is not None: - emergency_widget = {"label": "Emergency Contacts", - "label_create": "Add Emergency Contact", - "tablename": "pr_contact_emergency", - "type": "datalist", - "filter": contacts_filter, - "icon": "phone", - } - append_widget(emergency_widget) + #elif w_tablename == "hrm_training" and AP and organisation_id: + # new_widget = dict(widget) + # new_widget["filter"] = widget["filter"] & \ + # (FS("~.course_id$organisation_id") == organisation_id) + # append_widget(new_widget) + else: + append_widget(widget) + if widget["tablename"] == "pr_contact": + contacts_filter = widget["filter"] + + # Emergency contacts + if contacts_filter is not None: + emergency_widget = {"label": "Emergency Contacts", + "label_create": "Add Emergency Contact", + "tablename": "pr_contact_emergency", + "type": "datalist", + "filter": contacts_filter, + "icon": "phone", + } + append_widget(emergency_widget) - if r.record: - widgets.insert(0, {"label": "Personal Details", - "tablename": "pr_person", - "type": "datalist", - "insert": False, - "list_fields": ["first_name", - "middle_name", - "last_name", - "date_of_birth", - "gender", - "person_details.nationality", - "physical_description.blood_type", - ], - "filter": FS("id") == r.record.person_id, - "icon": "user", - }) - - # Remove unnecessary filter widgets - filters = [] - append_widget = filters.append - filter_widgets = get_config("filter_widgets") - while filter_widgets: - widget = filter_widgets.pop(0) - if widget.field not in ("location_id", - "site_id", - #"group_membership.group_id", - ): - append_widget(widget) + # Remove unnecessary filter widgets + filters = [] + append_filter = filters.append + filter_widgets = get_config("filter_widgets") + while filter_widgets: + widget = filter_widgets.pop(0) + if widget.field not in ("location_id", + "site_id", + #"group_membership.group_id", + ): + append_filter(widget) + + from s3 import S3OptionsFilter + + # Add gender filter + gender_opts = dict(s3db.pr_gender_opts) + del gender_opts[1] + append_filter(S3OptionsFilter("person_id$gender", + options = gender_opts, + cols = 3, + hidden = True, + )) + # Add Roster status filter + append_filter(S3OptionsFilter("application.active", + cols = 2, + default = True, + # Don't hide otherwise default + # doesn't apply: + #hidden = False, + label = T("Status"), + options = {"True": T("active"), + "False": T("inactive"), + }, + )) - from s3 import S3OptionsFilter + if r.method != "profile": + # Representation of emergency contacts (breaks the update_url construction in render_toolbox) + field = s3db.pr_contact_emergency.id + field.represent = S3Represent(lookup = "pr_contact_emergency", + fields = ("name", "relationship", "phone"), + labels = emergency_contact_represent, + ) + + # Custom list fields for Africa RDRT + phone_label = settings.get_ui_label_mobile_phone() + s3db.org_organisation.root_organisation.label = T("National Society") + list_fields = ["person_id", + (T("Sectors"), "credential.job_title_id"), + # @todo: Languages? + # @todo: Skills? + (T("Trainings"), "training.course_id"), + "organisation_id$root_organisation", + "type", + "job_title_id", + # @todo: Education? + (T("Status"), "application.active"), + (T("Email"), "email.value"), + (phone_label, "phone.value"), + (T("Address"), "person_id$address.location_id"), + "person_id$date_of_birth", + "person_id$gender", + "person_id$person_details.nationality", + #(T("Passport Number"), "person_id$passport.value"), + #(T("Passport Issuer"), "person_id$passport.ia_name"), + #(T("Passport Date"), "person_id$passport.valid_from"), + #(T("Passport Expires"), "person_id$passport.valid_until"), + (T("Emergency Contacts"), "person_id$contact_emergency.id"), + "person_id$physical_description.blood_type", + ] - # Add gender filter - gender_opts = dict(s3db.pr_gender_opts) - del gender_opts[1] - append_widget(S3OptionsFilter("person_id$gender", - options = gender_opts, - cols = 3, - hidden = True, - )) - # Add Roster status filter - append_widget(S3OptionsFilter("application.active", - cols = 2, - default = True, - # Don't hide otherwise default - # doesn't apply: - #hidden = False, - label = T("Status"), - options = {"True": T("active"), - "False": T("inactive"), - }, - )) + resource.configure(filter_widgets = filters, + list_fields = list_fields, + profile_widgets = widgets, + profile_header = rdrt_member_profile_header, + ) + else: + #otable = s3db.org_organisation + #org = db(otable.name == AP_ZONE).select(otable.id, + # limitby=(0, 1), + # cache = s3db.cache, + # ).first() + #try: + # organisation_id = org.id + #except: + # current.log.error("Cannot find org %s - prepop not done?" % AP_ZONE) + # organisation_id = None + #else: + # # Filter trainings to courses which belong to + # # the AP_ZONE organisation: + # ctable = s3db.hrm_course + # query = (ctable.organisation_id == organisation_id) & \ + # (ctable.deleted != True) + # courses = db(query).select(ctable.id) + # course_ids = [c.id for c in courses] + # s3db.add_components(tablename, + # hrm_training = {"link": "pr_person", + # "joinby": "id", + # "key": "id", + # "fkey": "person_id", + # "pkey": "person_id", + # "filterby": {"course_id": course_ids, + # }, + # }, + # ) + # # Reset the component (we're past resource initialization) + # r.resource.components.reset(("training",)) + + record = r.record + if record: + person_id = record.person_id + ptable = db.pr_person + person = db(ptable.id == person_id).select(#ptable.first_name, + #ptable.middle_name, + #ptable.last_name, + ptable.pe_id, + limitby=(0, 1) + ).first() + #name = s3_fullname(person) + pe_id = person.pe_id + + details_widget = {"label": "Personal Details", + "tablename": "pr_person", + "type": "datalist", + "insert": False, + "list_fields": ["first_name", + "middle_name", + "last_name", + "date_of_birth", + "gender", + "person_details.nationality", + "person_details.nationality2", + "physical_description.blood_type", + ], + "filter": FS("id") == person_id, + "icon": "user", + } - if r.method != "profile": - # Representation of emergency contacts (breaks the update_url construction in render_toolbox) - field = s3db.pr_contact_emergency.id - field.represent = S3Represent(lookup = "pr_contact_emergency", - fields = ("name", "relationship", "phone"), - labels = emergency_contact_represent, - ) - - # Custom list fields for RDRT - phone_label = settings.get_ui_label_mobile_phone() - s3db.org_organisation.root_organisation.label = T("National Society") - list_fields = ["person_id", - (T("Sectors"), "credential.job_title_id"), - # @todo: Languages? - # @todo: Skills? - (T("Trainings"), "training.course_id"), - "organisation_id$root_organisation", - "type", - "job_title_id", - # @todo: Education? - (T("Status"), "application.active"), - (T("Email"), "email.value"), - (phone_label, "phone.value"), - (T("Address"), "person_id$address.location_id"), - "person_id$date_of_birth", - "person_id$gender", - "person_id$person_details.nationality", - #(T("Passport Number"), "person_id$passport.value"), - #(T("Passport Issuer"), "person_id$passport.ia_name"), - #(T("Passport Date"), "person_id$passport.valid_from"), - #(T("Passport Expires"), "person_id$passport.valid_until"), - (T("Emergency Contacts"), "person_id$contact_emergency.id"), - "person_id$physical_description.blood_type", - ] + contacts_widget = {"label": "Contacts", + "label_create": "Add Contact", + "tablename": "pr_contact", + "type": "datalist", + "filter": FS("pe_id") == pe_id, + "icon": "phone", + # Default renderer: + #"list_layout": s3db.pr_render_contact, + "orderby": "priority asc", + # Can't do this as this is the HR perspective, not Person perspective + #"create_controller": c, + #"create_function": "person", + #"create_component": "contact", + } - resource.configure(filter_widgets = filters, - list_fields = list_fields, - profile_widgets = widgets, - profile_header = rdrt_member_profile_header, - ) + emergency_widget = {"label": "Emergency Contacts", + "label_create": "Add Emergency Contact", + "tablename": "pr_contact_emergency", + "type": "datalist", + "filter": FS("pe_id") == pe_id, + "icon": "phone", + } + + address_widget = {"label": "Address", + "label_create": "Add Address", + "type": "datalist", + "tablename": "pr_address", + "filter": FS("pe_id") == pe_id, + "icon": "home", + # Default renderer: + #"list_layout": s3db.pr_render_address, + # Can't do this as this is the HR perspective, not Person perspective + #"create_controller": c, + #"create_function": "person", + #"create_component": "address", + } + + education_widget = {"label": "Education", + "label_create": "Add Education", + "type": "datalist", + "tablename": "pr_education", + "filter": FS("person_id") == person_id, + "icon": "book", + "list_fields": [(T("Qualification"), "level"), + (T("Field of Expertise"), "major"), + ], + # Can't do this as this is the HR perspective, not Person perspective + #"create_controller": c, + #"create_function": "person", + #"create_component": "education", + "create_var": "rdrt_ap", + } + + job_widget = {"label": "Current Job Role", + "label_create": "Add Job Role", + "type": "datatable", + "dt_searching": False, + "tablename": "hrm_experience", + "filter": (FS("person_id") == person_id) & \ + (FS("activity_type") == None), + "icon": "wrench", + "list_fields": ["organisation", + (T("From"), "start_date"), + (T("To"), "end_date"), + (T("Total experience"), "comments"), + (T("Designation(s)"), "job_title"), + ], + "create_controller": "deploy", + # Can't do this as this is the HR perspective, not Person perspective + #"create_function": "person", + #"create_component": "experience", + "create_var": "rdrt_ap_current", + "pagesize": 1, + } + + experience_widget = {"label": "NDRT / other Deployments", + "label_create": "Add Deployment", + "type": "datatable", + "dt_searching": False, + "tablename": "hrm_experience", + "filter": (FS("person_id") == person_id) & \ + (FS("activity_type") == "rdrt"), + "icon": "truck", + "list_fields": ["organisation_id", + (T("From"), "start_date"), + (T("To"), "end_date"), + (T("Disaster situation (flood / earthquake, etc.)"), "comments"), + (T("Country of Deployment"), "location_id"), + (T("Skills Utilized"), "responsibilities"), + ], + "create_controller": "deploy", + # Can't do this as this is the HR perspective, not Person perspective + #"create_function": "person", + #"create_component": "experience", + "create_var": "rdrt_ap_deployment", + "pagesize": 2, + } + + language_widget = {"label": "Languages", + "label_create": "Add Language", + "type": "datatable", + "dt_searching": False, + "tablename": "pr_language", + "filter": (FS("person_id") == person_id), + "icon": "comment-alt", + #"create_controller": "deploy", + # Can't do this as this is the HR perspective, not Person perspective + #"create_function": "person", + #"create_component": "language", + } + + skills_widget = {"label": "Areas of Expertise", + "label_create": "Add Skill", + "type": "datatable", + "tablename": "hrm_competency", + "filter": FS("person_id") == person_id, + "icon": "wrench", + "list_fields": ["skill_id", + ], + "create_controller": "deploy", + # Can't do this as this is the HR perspective, not Person perspective + #"create_function": "person", + #"create_component": "competency", + "create_var": "rdrt_ap", + } + + docs_widget = {"label": "Documents", + "label_create": "Add Document", + "type": "datalist", + "tablename": "doc_document", + "filter": FS("doc_id") == record.doc_id, + "icon": "attachment", + # Default renderer: + #"list_layout": s3db.doc_document_list_layout, + } + + from gluon import URL + availability_widget = {"label": "Unavailability", + "type": "organizer", + "tablename": "deploy_unavailability", + "master": "pr_person/%s" % person_id, + "component": "unavailability", + "icon": "calendar", + "url": URL(c="deploy", f="person", + args = [person_id, "unavailability"], + ), + } + + profile_widgets = [details_widget, + contacts_widget, + emergency_widget, + address_widget, + education_widget, + job_widget, + experience_widget, + language_widget, + skills_widget, + docs_widget, + availability_widget, + ] + else: + profile_widgets = [] + + resource.configure(#filter_widgets = filters, + #list_fields = list_fields, + profile_widgets = profile_widgets, + profile_header = rdrt_member_profile_header, + ) return True s3.prep = custom_prep @@ -5210,6 +5494,31 @@ def customise_hrm_appraisal_resource(r, tablename): settings.customise_hrm_appraisal_resource = customise_hrm_appraisal_resource + # ------------------------------------------------------------------------- + def customise_hrm_competency_resource(r, tablename): + + if r.get_vars.get("rdrt_ap"): + # Simplify for RDRT AP + from s3 import IS_ONE_OF, S3Represent, S3SQLCustomForm + db = current.db + s3db = current.s3db + + filter_opts = + + s3db.hrm_competency.skill_id.requires = IS_ONE_OF(db, "hrm_skill.id", + S3Represent(lookup = "hrm_skill", + translate = True), + filterby = "skill_type_id", + filter_opts = filter_opts, + sort = True, + ) + s3db.configure("hrm_competency", + crud_form = S3SQLCustomForm("skill_id", + ), + ) + + settings.customise_hrm_competency_resource = customise_hrm_competency_resource + # ------------------------------------------------------------------------- def hrm_training_onaccept(form): """ @@ -6337,7 +6646,7 @@ def custom_prep(r): r.table.region_id.requires = r.table.region_id.requires.other else: r.table.region_id.readable = r.table.region_id.writable = False - resource.configure(list_fields=list_fields) + resource.configure(list_fields = list_fields) if r.interactive: r.table.country.label = T("Country") @@ -6401,6 +6710,29 @@ def customise_pr_contact_emergency_resource(r, tablename): settings.customise_pr_contact_emergency_resource = customise_pr_contact_emergency_resource + # ------------------------------------------------------------------------- + def customise_pr_education_resource(r, tablename): + + if r.get_vars.get("rdrt_ap"): + # Simplify for RDRT AP + from s3 import S3SQLCustomForm + s3db = current.s3db + # Use legacy field for cleaner options + f = s3db.pr_education.level + f.readable = f.writable = True + #from gluon import IS_IN_SET + #f.requires = IS_IN_SET(("High School", + # "University / College", + # "Post Graduate", + # )) + s3db.configure("pr_education", + crud_form = S3SQLCustomForm((T("Qualification"), "level"), + (T("Field of Expertise"), "major"), + ), + ) + + settings.customise_pr_education_resource = customise_pr_education_resource + # ------------------------------------------------------------------------- def customise_pr_person_availability_resource(r, tablename): @@ -6512,6 +6844,120 @@ def custom_prep(r): settings.customise_pr_group_controller = customise_pr_group_controller + # ------------------------------------------------------------------------- + def customise_pr_language_controller(**attr): + + # Languages for RDRT AP + settings.L10n.languages = OrderedDict([ + ("af", "Afrikaans"), + ("sq", "Albanian"), + ("am", "Amharic"), + ("ar", "Arabic"), + ("hy", "Armenian"), + ("az", "Azerbaijani"), + ("eu", "Basque"), + ("be", "Belarusian"), + ("bn", "Bengali"), + ("bs", "Bosnian"), + ("bg", "Bulgarian"), + ("ca", "Catalan"), + ("ceb", "Cebuano"), + ("ny", "Chichewa"), + ("zh", "Chinese"), + ("co", "Corsican"), + ("hr", "Croatian"), + ("cs", "Czech"), + ("da", "Danish"), + ("nl", "Dutch"), + ("en", "English"), + ("eo", "Esperanto"), + ("et", "Estonian"), + ("fil", "Filipino"), + ("fi", "Finnish"), + ("fr", "French"), + ("gl", "Galician"), + ("ka", "Georgian"), + ("de", "German"), + ("el", "Greek"), # "Greek, Modern (1453-)" + ("gu", "Gujarati"), + ("ht", "Haitian Creole"), + ("ha", "Hausa"), + ("haw", "Hawaiian"), + ("he", "Hebrew"), + ("hi", "Hindi"), + ("hmn", "Hmong"), + ("hu", "Hungarian"), + ("is", "Icelandic"), + ("ig", "Igbo"), + ("id", "Indonesian"), + ("ga", "Irish"), + ("it", "Italian"), + ("ja", "Japanese"), + ("jv", "Javanese"), + ("kn", "Kannada"), + ("kk", "Kazakh"), + ("km", "Khmer"), + ("ko", "Korean"), + ("kos", "Kosraean"), + ("ku", "Kurdish (Kurmanji)"), + ("ky", "Kyrgyz"), + ("lo", "Lao"), + ("la", "Latin"), + ("lv", "Latvian"), + ("lt", "Lithuanian"), + ("lb", "Luxembourgish"), + ("mk", "Macedonian"), + ("mg", "Malagasy"), # Madagascar + ("ms", "Malay"), + ("ml", "Malayalam"), + ("mt", "Maltese"), + ("mi", "Maori"), + ("mr", "Marathi"), + ("mn", "Mongolian"), + ("my", "Myanmar (Burmese)"), + ("ne", "Nepali"), + ("nn", "Norwegian"), + ("ps", "Pashto"), + ("fa", "Persian"), + ("pl", "Polish"), + ("pt", "Portuguese"), + ("pa", "Punjabi"), + ("ro", "Romanian"), + ("ru", "Russian"), + ("sm", "Samoan"), + ("sco", "Scots Gaelic"), + ("sr", "Serbian"), + ("st", "Sesotho"), + ("sn", "Shona"), + ("sd", "Sindhi"), + ("si", "Sinhala"), + ("sk", "Slovak"), + ("sl", "Slovenian"), + ("so", "Somali"), + ("es", "Spanish"), + ("su", "Sundanese"), + ("sw", "Swahili"), + ("sv", "Swedish"), + ("tg", "Tajik"), + ("ta", "Tamil"), + ("te", "Telugu"), + ("th", "Thai"), + ("tr", "Turkish"), + ("uk", "Ukrainian"), + ("ur", "Urdu"), + ("uz", "Uzbek"), + ("vi", "Vietnamese"), + ("cy", "Welsh"), + ("xh", "Xhosa"), + ("yi", "Yiddish"), + ("yo", "Yoruba"), + ("zu", "Zulu"), + ]) + + return attr + + settings.customise_pr_language_controller = customise_pr_language_controller + # ========================================================================= def vol_programme_active(person_id): """ @@ -6781,6 +7227,7 @@ def custom_prep(r): if not result: return False + controller = r.controller component_name = r.component_name method = r.method if component_name == "address": @@ -6823,7 +7270,6 @@ def custom_prep(r): elif component_name == "identity": if crmada: - controller = r.controller table = r.component.table # Set default to National ID Card table.type.default = 2 @@ -6886,7 +7332,7 @@ def custom_prep(r): branches = True, ) if method == "record": - if r.controller == "vol" and root_org == NRCS: + if controller == "vol" and root_org == NRCS: from s3 import S3SQLCustomForm crud_form = S3SQLCustomForm("organisation_id", "details.volunteer_type", @@ -6903,646 +7349,671 @@ def custom_prep(r): # Use default form (legacy) s3db.clear_config("hrm_human_resource", "crud_form") - if arcs: - # Changes common to both Members & Volunteers - from gluon import IS_EMPTY_OR - from s3 import IS_ONE_OF, S3SQLCustomForm, S3SQLInlineComponent, S3LocationSelector - # Ensure that + appears at the beginning of the number - # Done in Model's controller prep - #f = s3db.get_aliased(s3db.pr_contact, "pr_phone_contact").value - #f.represent = s3_phone_represent - #f.widget = S3PhoneWidget() - s3db.pr_address.location_id.widget = S3LocationSelector(show_address = T("Village"), - show_map = False) - etable = s3db.pr_education - etable.level_id.comment = None # Don't Add Education Levels inline - organisation_id = current.auth.root_org() - f = etable.level_id - f.requires = IS_ONE_OF(db, "pr_education_level.id", - f.represent, - filterby = "organisation_id", - filter_opts = (organisation_id,), - ) - s3db.pr_image.image.widget = None # ImageCropWidget doesn't work inline - s3db.add_components("pr_person", - pr_address = {"name": "perm_address", - "joinby": "pe_id", - "pkey": "pe_id", - "filterby": {"type": 2, - }, - "multiple": False, - }, - pr_education = {"name": "previous_education", - "joinby": "person_id", - "filterby": {"current": False, - }, - "multiple": False, - }, - ) - crud_strings = s3.crud_strings - controller = r.controller - if controller == "vol": - crud_strings["pr_person"] = crud_strings["hrm_human_resource"] - #ctable = s3db.hrm_course - #ctable.type.default = 2 - #query = (ctable.organisation_id == current.auth.root_org()) & \ - # (ctable.type == 2) & \ - # (ctable.deleted != True) - #courses = db(query).select(ctable.id) - #course_ids = [c.id for c in courses] - f = s3db.hrm_training.course_id - f.requires = IS_ONE_OF(db, "hrm_course.id", - f.represent, - #filterby = "id", - #filter_opts = course_ids, - filterby = "organisation_id", - filter_opts = (organisation_id,), - sort = True, - ) - s3db.add_components("pr_person", - hrm_human_resource = {"name": "volunteer", - "joinby": "person_id", - "filterby": { - "type": 2, - }, - "multiple": False, - }, - #hrm_training = {"name": "vol_training", - # "joinby": "person_id", - # "filterby": {"course_id": course_ids, - # }, - # }, - pr_education = {"name": "current_education", - "joinby": "person_id", - "filterby": { - "current": True, - }, - "multiple": False, - }, - vol_details = {"name": "volunteer_details", - "link": "hrm_human_resource", - "joinby": "person_id", - "key": "id", - "fkey": "human_resource_id", - "pkey": "id", - "multiple": False, - }, - ) - crud_form = S3SQLCustomForm("volunteer.organisation_id", - "volunteer.code", - (T("Name"), "first_name"), + + if controller == "deploy": + + AP = _is_asia_pacific() + if AP: + from s3 import S3SQLCustomForm, S3SQLInlineComponent + + f = s3db.pr_person.middle_name + f.readable = f.writable = True + + f = s3db.pr_person_details.nationality2 + f.readable = f.writable = True + + crud_form = S3SQLCustomForm("first_name", + "middle_name", "last_name", - (T("Father Name"), "person_details.father_name"), - (T("Grand Father Name"), "person_details.grandfather_name"), "date_of_birth", - (T("Gender"), "gender"), - (T("Job"), "person_details.occupation"), - S3SQLInlineComponent("perm_address", - label = T("Address"), - fields = [("", "location_id")], - filterby = {"field": "type", - "options": 2, - }, - multiple = False, - ), - S3SQLInlineComponent("current_education", - label = T("School / University"), - fields = [("", "institute")], - filterby = {"field": "current", - "options": True, - }, - multiple = False, - ), - S3SQLInlineComponent("phone", - label = T("Phone Number"), - fields = [("", "value")], - filterby = {"field": "contact_method", - "options": "SMS", - }, - multiple = False, - ), - S3SQLInlineComponent("contact_emergency", - label = T("Emergency Contact Number"), - fields = [("", "phone")], - multiple = False, - ), - S3SQLInlineComponent("previous_education", - label = T("Education Level"), - fields = [("", "level_id")], - filterby = {"field": "current", - "options": False, - }, - multiple = False, - ), - (T("Volunteer Start Date"), "volunteer.start_date"), - S3SQLInlineComponent("training", - #"vol_training", - label = T("Trainings"), - fields = (("", "course_id"), - ("", "date"), - ), - ), - S3SQLInlineComponent("volunteer_details", - label = T("Active"), - fields = [("", "active")], - link = False, - update_link = False, - multiple = False, - ), - (T("Remarks"), "comments"), - S3SQLInlineComponent("image", - label = T("Photo"), - fields = [("", "image")], - filterby = {"field": "profile", - "options": True, - }, - multiple = False, - ), + "gender", + "person_details.nationality", + "person_details.nationality2", + "physical_description.blood_type", + "comments", ) + s3db.configure("pr_person", crud_form = crud_form, ) - - elif controller == "member": - mtable = s3db.member_membership - crud_strings["pr_person"] = crud_strings["member_membership"] - f = mtable.leaving_reason - f.readable = f.writable = True - f = mtable.restart_date - f.readable = f.writable = True - f = mtable.election - f.readable = f.writable = True - f = mtable.trainings - f.readable = f.writable = True - #ctable = s3db.hrm_course - #ctable.type.default = 4 - #query = (ctable.organisation_id == current.auth.root_org()) & \ - # (ctable.type == 4) & \ - # (ctable.deleted != True) - #courses = db(query).select(ctable.id) - #course_ids = [c.id for c in courses] - #f = s3db.hrm_training.course_id - #f.requires = IS_ONE_OF(db, "hrm_course.id", - # f.represent, - # filterby = "id", - # filter_opts = course_ids, - # sort = True, - # ) - f = s3db.pr_person_details.grandfather_name - f.readable = f.writable = True - + else: + if arcs: + # Changes common to both Members & Volunteers + from gluon import IS_EMPTY_OR + from s3 import IS_ONE_OF, S3SQLCustomForm, S3SQLInlineComponent, S3LocationSelector + # Ensure that + appears at the beginning of the number + # Done in Model's controller prep + #f = s3db.get_aliased(s3db.pr_contact, "pr_phone_contact").value + #f.represent = s3_phone_represent + #f.widget = S3PhoneWidget() + s3db.pr_address.location_id.widget = S3LocationSelector(show_address = T("Village"), + show_map = False) + etable = s3db.pr_education + etable.level_id.comment = None # Don't Add Education Levels inline + organisation_id = current.auth.root_org() + f = etable.level_id + f.requires = IS_ONE_OF(db, "pr_education_level.id", + f.represent, + filterby = "organisation_id", + filter_opts = (organisation_id,), + ) + s3db.pr_image.image.widget = None # ImageCropWidget doesn't work inline s3db.add_components("pr_person", - #hrm_training = {"name": "member_training", - # "joinby": "person_id", - # "filterby": {"course_id": course_ids, - # }, - # }, - pr_address = {"name": "temp_address", + pr_address = {"name": "perm_address", "joinby": "pe_id", "pkey": "pe_id", - "filterby": { - "type": 1, - }, + "filterby": {"type": 2, + }, "multiple": False, }, - # Make single, so fields can be embedded: - member_membership = {"joinby": "person_id", - "multiple": False, - }, + pr_education = {"name": "previous_education", + "joinby": "person_id", + "filterby": {"current": False, + }, + "multiple": False, + }, ) - # Reset the component (we're past resource initialization) - r.resource.components.reset(("membership",)) - - crud_form = S3SQLCustomForm("membership.organisation_id", - "membership.code", - (T("Name"), "first_name"), - "last_name", - (T("Father Name"), "person_details.father_name"), - (T("Grand Father Name"), "person_details.grandfather_name"), - "date_of_birth", - (T("Gender"), "gender"), - S3SQLInlineComponent("perm_address", - label = T("Permanent Address"), - fields = [("", "location_id")], - filterby = {"field": "type", - "options": 2, - }, - multiple = False, - ), - S3SQLInlineComponent("temp_address", - label = T("Temporary Address"), - fields = [("", "location_id")], - filterby = {"field": "type", - "options": 1, - }, - multiple = False, - ), - (T("Place of Work"), "person_details.company"), - S3SQLInlineComponent("previous_education", - label = T("Education Level"), - fields = [("", "level_id")], - filterby = {"field": "current", - "options": False, - }, - multiple = False, - ), - S3SQLInlineComponent("identity", - label = T("ID Number"), - fields = [("", "value")], - filterby = {"field": "type", - "options": 2, - }, - multiple = False, - ), - S3SQLInlineComponent("physical_description", - label = T("Blood Group"), - fields = [("", "blood_type")], - multiple = False, - ), - S3SQLInlineComponent("phone", - label = T("Phone Number"), - fields = [("", "value")], - filterby = {"field": "contact_method", - "options": "SMS", - }, - multiple = False, - ), - S3SQLInlineComponent("contact_emergency", - label = T("Relatives Contact #"), - fields = [("", "phone")], - multiple = False, - ), - (T("Date of Recruitment"), "membership.start_date"), - (T("Date of Dismissal"), "membership.end_date"), - (T("Reason for Dismissal"), "membership.leaving_reason"), - (T("Date of Re-recruitment"), "membership.restart_date"), - (T("Monthly Membership Fee"), "membership.membership_fee"), - (T("Membership Fee Last Paid"), "membership.membership_paid"), - "membership.membership_due", - "membership.election", - "membership.trainings", - #S3SQLInlineComponent("member_training", - # label = T("Trainings"), - # fields = (("", "course_id"), - # ("", "date"), - # ), - # ), - "membership.comments", - S3SQLInlineComponent("image", - label = T("Photo"), - fields = [("", "image")], - filterby = {"field": "profile", - "options": True, - }, - multiple = False, - ), - ) - s3db.configure("pr_person", - crud_form = crud_form, - ) - - elif vnrc: - controller = r.controller - if not r.component: - crud_fields = ["first_name", - "middle_name", - "last_name", - "date_of_birth", - "gender", - "person_details.marital_status", - "person_details.nationality", - ] - - from gluon import IS_EMPTY_OR, IS_IN_SET - from s3 import IS_ONE_OF - - dtable = s3db.pr_person_details + crud_strings = s3.crud_strings + if controller == "vol": + crud_strings["pr_person"] = crud_strings["hrm_human_resource"] + #ctable = s3db.hrm_course + #ctable.type.default = 2 + #query = (ctable.organisation_id == current.auth.root_org()) & \ + # (ctable.type == 2) & \ + # (ctable.deleted != True) + #courses = db(query).select(ctable.id) + #course_ids = [c.id for c in courses] + f = s3db.hrm_training.course_id + f.requires = IS_ONE_OF(db, "hrm_course.id", + f.represent, + #filterby = "id", + #filter_opts = course_ids, + filterby = "organisation_id", + filter_opts = (organisation_id,), + sort = True, + ) + s3db.add_components("pr_person", + hrm_human_resource = {"name": "volunteer", + "joinby": "person_id", + "filterby": { + "type": 2, + }, + "multiple": False, + }, + #hrm_training = {"name": "vol_training", + # "joinby": "person_id", + # "filterby": {"course_id": course_ids, + # }, + # }, + pr_education = {"name": "current_education", + "joinby": "person_id", + "filterby": { + "current": True, + }, + "multiple": False, + }, + vol_details = {"name": "volunteer_details", + "link": "hrm_human_resource", + "joinby": "person_id", + "key": "id", + "fkey": "human_resource_id", + "pkey": "id", + "multiple": False, + }, + ) + crud_form = S3SQLCustomForm("volunteer.organisation_id", + "volunteer.code", + (T("Name"), "first_name"), + "last_name", + (T("Father Name"), "person_details.father_name"), + (T("Grand Father Name"), "person_details.grandfather_name"), + "date_of_birth", + (T("Gender"), "gender"), + (T("Job"), "person_details.occupation"), + S3SQLInlineComponent("perm_address", + label = T("Address"), + fields = [("", "location_id")], + filterby = {"field": "type", + "options": 2, + }, + multiple = False, + ), + S3SQLInlineComponent("current_education", + label = T("School / University"), + fields = [("", "institute")], + filterby = {"field": "current", + "options": True, + }, + multiple = False, + ), + S3SQLInlineComponent("phone", + label = T("Phone Number"), + fields = [("", "value")], + filterby = {"field": "contact_method", + "options": "SMS", + }, + multiple = False, + ), + S3SQLInlineComponent("contact_emergency", + label = T("Emergency Contact Number"), + fields = [("", "phone")], + multiple = False, + ), + S3SQLInlineComponent("previous_education", + label = T("Education Level"), + fields = [("", "level_id")], + filterby = {"field": "current", + "options": False, + }, + multiple = False, + ), + (T("Volunteer Start Date"), "volunteer.start_date"), + S3SQLInlineComponent("training", + #"vol_training", + label = T("Trainings"), + fields = (("", "course_id"), + ("", "date"), + ), + ), + S3SQLInlineComponent("volunteer_details", + label = T("Active"), + fields = [("", "active")], + link = False, + update_link = False, + multiple = False, + ), + (T("Remarks"), "comments"), + S3SQLInlineComponent("image", + label = T("Photo"), + fields = [("", "image")], + filterby = {"field": "profile", + "options": True, + }, + multiple = False, + ), + ) + s3db.configure("pr_person", + crud_form = crud_form, + ) - # Context-dependent form fields - if controller in ("pr", "hrm", "vol"): - # Provinces of Viet Nam - ltable = s3db.gis_location - ptable = ltable.with_alias("gis_parent_location") - dbset = db((ltable.level == "L1") & \ - (ptable.name == "Viet Nam")) - left = ptable.on(ltable.parent == ptable.id) - vn_provinces = IS_EMPTY_OR(IS_ONE_OF(dbset, "gis_location.name", - "%(name)s", - left=left, - )) - # Place Of Birth - field = dtable.place_of_birth - field.readable = field.writable = True - field.requires = vn_provinces + elif controller == "member": + mtable = s3db.member_membership + crud_strings["pr_person"] = crud_strings["member_membership"] + f = mtable.leaving_reason + f.readable = f.writable = True + f = mtable.restart_date + f.readable = f.writable = True + f = mtable.election + f.readable = f.writable = True + f = mtable.trainings + f.readable = f.writable = True + #ctable = s3db.hrm_course + #ctable.type.default = 4 + #query = (ctable.organisation_id == current.auth.root_org()) & \ + # (ctable.type == 4) & \ + # (ctable.deleted != True) + #courses = db(query).select(ctable.id) + #course_ids = [c.id for c in courses] + #f = s3db.hrm_training.course_id + #f.requires = IS_ONE_OF(db, "hrm_course.id", + # f.represent, + # filterby = "id", + # filter_opts = course_ids, + # sort = True, + # ) + f = s3db.pr_person_details.grandfather_name + f.readable = f.writable = True - # Home Town - field = dtable.hometown - field.readable = field.writable = True - field.requires = vn_provinces + s3db.add_components("pr_person", + #hrm_training = {"name": "member_training", + # "joinby": "person_id", + # "filterby": {"course_id": course_ids, + # }, + # }, + pr_address = {"name": "temp_address", + "joinby": "pe_id", + "pkey": "pe_id", + "filterby": { + "type": 1, + }, + "multiple": False, + }, + # Make single, so fields can be embedded: + member_membership = {"joinby": "person_id", + "multiple": False, + }, + ) + # Reset the component (we're past resource initialization) + r.resource.components.reset(("membership",)) + + crud_form = S3SQLCustomForm("membership.organisation_id", + "membership.code", + (T("Name"), "first_name"), + "last_name", + (T("Father Name"), "person_details.father_name"), + (T("Grand Father Name"), "person_details.grandfather_name"), + "date_of_birth", + (T("Gender"), "gender"), + S3SQLInlineComponent("perm_address", + label = T("Permanent Address"), + fields = [("", "location_id")], + filterby = {"field": "type", + "options": 2, + }, + multiple = False, + ), + S3SQLInlineComponent("temp_address", + label = T("Temporary Address"), + fields = [("", "location_id")], + filterby = {"field": "type", + "options": 1, + }, + multiple = False, + ), + (T("Place of Work"), "person_details.company"), + S3SQLInlineComponent("previous_education", + label = T("Education Level"), + fields = [("", "level_id")], + filterby = {"field": "current", + "options": False, + }, + multiple = False, + ), + S3SQLInlineComponent("identity", + label = T("ID Number"), + fields = [("", "value")], + filterby = {"field": "type", + "options": 2, + }, + multiple = False, + ), + S3SQLInlineComponent("physical_description", + label = T("Blood Group"), + fields = [("", "blood_type")], + multiple = False, + ), + S3SQLInlineComponent("phone", + label = T("Phone Number"), + fields = [("", "value")], + filterby = {"field": "contact_method", + "options": "SMS", + }, + multiple = False, + ), + S3SQLInlineComponent("contact_emergency", + label = T("Relatives Contact #"), + fields = [("", "phone")], + multiple = False, + ), + (T("Date of Recruitment"), "membership.start_date"), + (T("Date of Dismissal"), "membership.end_date"), + (T("Reason for Dismissal"), "membership.leaving_reason"), + (T("Date of Re-recruitment"), "membership.restart_date"), + (T("Monthly Membership Fee"), "membership.membership_fee"), + (T("Membership Fee Last Paid"), "membership.membership_paid"), + "membership.membership_due", + "membership.election", + "membership.trainings", + #S3SQLInlineComponent("member_training", + # label = T("Trainings"), + # fields = (("", "course_id"), + # ("", "date"), + # ), + # ), + "membership.comments", + S3SQLInlineComponent("image", + label = T("Photo"), + fields = [("", "image")], + filterby = {"field": "profile", + "options": True, + }, + multiple = False, + ), + ) + s3db.configure("pr_person", + crud_form = crud_form, + ) - # Use a free-text version of religion field - # @todo: make religion a drop-down list of options - field = dtable.religion_other - field.label = T("Religion") - field.readable = field.writable = True + elif vnrc: + if not r.component: + crud_fields = ["first_name", + "middle_name", + "last_name", + "date_of_birth", + "gender", + "person_details.marital_status", + "person_details.nationality", + ] - crud_fields.extend(["person_details.place_of_birth", - "person_details.hometown", - "person_details.religion_other", - "person_details.mother_name", - "person_details.father_name", - "person_details.affiliations", - ]) - else: - # ID Card Number inline - from s3 import S3SQLInlineComponent - idcard_number = S3SQLInlineComponent("idcard", - label = T("ID Card Number"), - fields = [("", "value")], - default = {"type": 2, - }, - multiple = False, - ) - # @todo: make ethnicity a drop-down list of options - crud_fields.extend(["physical_description.ethnicity", - idcard_number, - ]) - - # Standard option for nationality - field = dtable.nationality - VN = "VN" - field.default = VN - vnrc_only = False - try: - options = dict(field.requires.options()) - except AttributeError: - pass - else: - opts = [VN] - if r.record: - # Get the nationality from the current record - query = (r.table.id == r.id) - left = dtable.on(dtable.person_id == r.id) - row = db(query).select(dtable.nationality, - left = left, - limitby = (0, 1)).first() - if row and row.nationality: - opts.append(row.nationality) - # Check wether this person is only VNRC-associated - htable = s3db.hrm_human_resource - otable = s3db.org_organisation - query = (htable.person_id == r.id) & \ - (htable.deleted != True) & \ - (otable.id == htable.organisation_id) & \ - (otable.name != VNRC) - row = db(query).select(htable.id, limitby=(0, 1)).first() - if not row: - vnrc_only = True - opts = dict((k, options[k]) for k in opts if k in options) - if vnrc_only: - # Person is only associated with VNRC => enforce update, - # and limit options to either current value or VN - field.requires = IS_IN_SET(opts, zero=None) + from gluon import IS_EMPTY_OR, IS_IN_SET + from s3 import IS_ONE_OF + + dtable = s3db.pr_person_details + + # Context-dependent form fields + if controller in ("pr", "hrm", "vol"): + # Provinces of Viet Nam + ltable = s3db.gis_location + ptable = ltable.with_alias("gis_parent_location") + dbset = db((ltable.level == "L1") & \ + (ptable.name == "Viet Nam")) + left = ptable.on(ltable.parent == ptable.id) + vn_provinces = IS_EMPTY_OR(IS_ONE_OF(dbset, "gis_location.name", + "%(name)s", + left=left, + )) + # Place Of Birth + field = dtable.place_of_birth + field.readable = field.writable = True + field.requires = vn_provinces + + # Home Town + field = dtable.hometown + field.readable = field.writable = True + field.requires = vn_provinces + + # Use a free-text version of religion field + # @todo: make religion a drop-down list of options + field = dtable.religion_other + field.label = T("Religion") + field.readable = field.writable = True + + crud_fields.extend(["person_details.place_of_birth", + "person_details.hometown", + "person_details.religion_other", + "person_details.mother_name", + "person_details.father_name", + "person_details.affiliations", + ]) else: - # Person is (also) associated with another org - # => can't enforce update, so just limit options - field.requires = IS_EMPTY_OR(IS_IN_SET(opts)) - - # Also hide some other fields - crud_fields.append("comments") - from s3 import S3SQLCustomForm - s3db.configure("pr_person", - crud_form = S3SQLCustomForm(*crud_fields), - ) + # ID Card Number inline + from s3 import S3SQLInlineComponent + idcard_number = S3SQLInlineComponent("idcard", + label = T("ID Card Number"), + fields = [("", "value")], + default = {"type": 2, + }, + multiple = False, + ) + # @todo: make ethnicity a drop-down list of options + crud_fields.extend(["physical_description.ethnicity", + idcard_number, + ]) - if method == "record" or component_name == "human_resource": - # Hide unwanted fields in human_resource - htable = s3db.hrm_human_resource - for fname in ["job_title_id", - "code", - "essential", - "site_contact", - "start_date", - "end_date", - ]: - field = htable[fname] - field.readable = field.writable = False + # Standard option for nationality + field = dtable.nationality + VN = "VN" + field.default = VN + vnrc_only = False + try: + options = dict(field.requires.options()) + except AttributeError: + pass + else: + opts = [VN] + if r.record: + # Get the nationality from the current record + query = (r.table.id == r.id) + left = dtable.on(dtable.person_id == r.id) + row = db(query).select(dtable.nationality, + left = left, + limitby = (0, 1)).first() + if row and row.nationality: + opts.append(row.nationality) + # Check wether this person is only VNRC-associated + htable = s3db.hrm_human_resource + otable = s3db.org_organisation + query = (htable.person_id == r.id) & \ + (htable.deleted != True) & \ + (otable.id == htable.organisation_id) & \ + (otable.name != VNRC) + row = db(query).select(htable.id, limitby=(0, 1)).first() + if not row: + vnrc_only = True + opts = dict((k, options[k]) for k in opts if k in options) + if vnrc_only: + # Person is only associated with VNRC => enforce update, + # and limit options to either current value or VN + field.requires = IS_IN_SET(opts, zero=None) + else: + # Person is (also) associated with another org + # => can't enforce update, so just limit options + field.requires = IS_EMPTY_OR(IS_IN_SET(opts)) - if method == "record" and controller == "hrm": - # Custom config for method handler + # Also hide some other fields + crud_fields.append("comments") + from s3 import S3SQLCustomForm + s3db.configure("pr_person", + crud_form = S3SQLCustomForm(*crud_fields), + ) - # RC employment history - org_type_name = "organisation_id$organisation_organisation_type.organisation_type_id$name" - widget_filter = (FS(org_type_name) == "Red Cross / Red Crescent") & \ - (FS("organisation") == None) - org_experience = {"label": T("Red Cross Employment History"), - "label_create": T("Add Employment"), - "list_fields": ["start_date", + if method == "record" or component_name == "human_resource": + # Hide unwanted fields in human_resource + htable = s3db.hrm_human_resource + for fname in ["job_title_id", + "code", + "essential", + "site_contact", + "start_date", + "end_date", + ]: + field = htable[fname] + field.readable = field.writable = False + + if method == "record" and controller == "hrm": + # Custom config for method handler + + # RC employment history + org_type_name = "organisation_id$organisation_organisation_type.organisation_type_id$name" + widget_filter = (FS(org_type_name) == "Red Cross / Red Crescent") & \ + (FS("organisation") == None) + org_experience = {"label": T("Red Cross Employment History"), + "label_create": T("Add Employment"), + "list_fields": ["start_date", + "end_date", + "organisation", + "department_id", + "job_title", + "employment_type", + ], + "filter": widget_filter, + } + + # Non-RC employment history + widget_filter = FS("organisation") != None + other_experience = {"label": T("Other Employments"), + "label_create": T("Add Employment"), + "list_fields": ["start_date", + "end_date", + "organisation", + "job_title", + ], + "filter": widget_filter, + } + + s3db.set_method("pr", "person", + method = "record", + action = s3db.hrm_Record(salary=True, + awards=True, + disciplinary_record=True, + org_experience=org_experience, + other_experience=other_experience, + )) + + # Custom list_fields for hrm_salary (exclude monthly amount) + stable = s3db.hrm_salary + stable.salary_grade_id.label = T("Grade Code") + s3db.configure("hrm_salary", + list_fields = ["staff_level_id", + "salary_grade_id", + "start_date", "end_date", - "organisation", - "department_id", - "job_title", - "employment_type", ], - "filter": widget_filter, - } + ) + # Custom list_fields for hrm_award + s3db.configure("hrm_award", + list_fields = ["date", + "awarding_body", + "award_type_id", + ], + orderby = "hrm_award.date desc" + ) + # Custom list_fields for hrm_disciplinary_action + s3db.configure("hrm_disciplinary_action", + list_fields = ["date", + "disciplinary_body", + "disciplinary_type_id", + ], + orderby = "hrm_disciplinary_action.date desc" + ) + # Custom form for hrm_human_resource + from s3 import S3SQLCustomForm, S3SQLInlineComponent + crud_fields = ["organisation_id", + "site_id", + "department_id", + "status", + S3SQLInlineComponent("contract", + label = T("Contract Details"), + fields = ("term", + (T("Hours Model"), "hours"), + ), + multiple = False, + ), + S3SQLInlineComponent("social_insurance", + label = T("Social Insurance"), + name = "social", + fields = ("insurance_number", + "insurer", + ), + default = {"type": "SOCIAL"}, + multiple = False, + ), + S3SQLInlineComponent("health_insurance", + label = T("Health Insurance"), + name = "health", + fields = ("insurance_number", + "provider", + ), + default = {"type": "HEALTH"}, + multiple = False, + ), + "comments", + ] + s3db.configure("hrm_human_resource", + crud_form = S3SQLCustomForm(*crud_fields), + ) - # Non-RC employment history - widget_filter = FS("organisation") != None - other_experience = {"label": T("Other Employments"), - "label_create": T("Add Employment"), - "list_fields": ["start_date", - "end_date", - "organisation", - "job_title", - ], - "filter": widget_filter, - } + elif component_name == "address": + settings.gis.building_name = False + settings.gis.latlon_selector = False + settings.gis.map_selector = False - s3db.set_method("pr", "person", - method = "record", - action = s3db.hrm_Record(salary=True, - awards=True, - disciplinary_record=True, - org_experience=org_experience, - other_experience=other_experience, - )) + elif method == "contacts": + table = s3db.pr_contact_emergency + table.address.readable = table.address.writable = True - # Custom list_fields for hrm_salary (exclude monthly amount) - stable = s3db.hrm_salary - stable.salary_grade_id.label = T("Grade Code") - s3db.configure("hrm_salary", - list_fields = ["staff_level_id", - "salary_grade_id", + elif component_name == "identity": + table = r.component.table + + # Limit options for identity document type + pr_id_type_opts = {1: T("Passport"), + 2: T("National ID Card"), + } + from gluon.validators import IS_IN_SET + table.type.requires = IS_IN_SET(pr_id_type_opts, zero=None) + + if controller == "hrm": + # For staff, set default for ID document type and do not + # allow selection of other options + table.type.default = 2 + table.type.writable = False + hide_fields = ("description", "valid_until", "country_code", "ia_name") + else: + hide_fields = ("description",) + + # Hide unneeded fields + for fname in hide_fields: + field = table[fname] + field.readable = field.writable = False + list_fields = s3db.get_config("pr_identity", "list_fields") + hide_fields = set(hide_fields) + list_fields = (fs for fs in list_fields if fs not in hide_fields) + s3db.configure("pr_identity", list_fields = list_fields) + + elif method == "cv" or component_name == "experience": + table = s3db.hrm_experience + # Use simple free-text variants + table.organisation_id.default = None # should not default in this case + table.organisation.readable = True + table.organisation.writable = True + table.job_title.readable = True + table.job_title.writable = True + table.comments.label = T("Main Duties") + from s3 import S3SQLCustomForm + crud_form = S3SQLCustomForm("organisation", + "job_title", + "comments", + "start_date", + "end_date", + ) + s3db.configure("hrm_experience", + crud_form = crud_form, + list_fields = ["organisation", + "job_title", + "comments", "start_date", "end_date", ], ) - # Custom list_fields for hrm_award - s3db.configure("hrm_award", - list_fields = ["date", - "awarding_body", - "award_type_id", - ], - orderby = "hrm_award.date desc" - ) - # Custom list_fields for hrm_disciplinary_action - s3db.configure("hrm_disciplinary_action", - list_fields = ["date", - "disciplinary_body", - "disciplinary_type_id", - ], - orderby = "hrm_disciplinary_action.date desc" - ) - # Custom form for hrm_human_resource - from s3 import S3SQLCustomForm, S3SQLInlineComponent - crud_fields = ["organisation_id", - "site_id", - "department_id", - "status", - S3SQLInlineComponent("contract", - label = T("Contract Details"), - fields = ("term", - (T("Hours Model"), "hours"), - ), - multiple = False, - ), - S3SQLInlineComponent("social_insurance", - label = T("Social Insurance"), - name = "social", - fields = ("insurance_number", - "insurer", - ), - default = {"type": "SOCIAL"}, - multiple = False, - ), - S3SQLInlineComponent("health_insurance", - label = T("Health Insurance"), - name = "health", - fields = ("insurance_number", - "provider", - ), - default = {"type": "HEALTH"}, - multiple = False, - ), - "comments", - ] - s3db.configure("hrm_human_resource", - crud_form = S3SQLCustomForm(*crud_fields), - ) - - elif component_name == "address": - settings.gis.building_name = False - settings.gis.latlon_selector = False - settings.gis.map_selector = False - - elif method == "contacts": - table = s3db.pr_contact_emergency - table.address.readable = table.address.writable = True - - elif component_name == "identity": - controller = r.controller - table = r.component.table + if method == "cv": + # Customize CV + s3db.set_method("pr", "person", + method = "cv", + action = s3db.hrm_CV(form=vnrc_cv_form)) - # Limit options for identity document type - pr_id_type_opts = {1: T("Passport"), - 2: T("National ID Card"), - } - from gluon.validators import IS_IN_SET - table.type.requires = IS_IN_SET(pr_id_type_opts, zero=None) - - if controller == "hrm": - # For staff, set default for ID document type and do not - # allow selection of other options - table.type.default = 2 - table.type.writable = False - hide_fields = ("description", "valid_until", "country_code", "ia_name") - else: - hide_fields = ("description",) - - # Hide unneeded fields - for fname in hide_fields: - field = table[fname] + elif component_name == "salary": + stable = s3db.hrm_salary + stable.salary_grade_id.label = T("Grade Code") + field = stable.monthly_amount field.readable = field.writable = False - list_fields = s3db.get_config("pr_identity", "list_fields") - hide_fields = set(hide_fields) - list_fields = (fs for fs in list_fields if fs not in hide_fields) - s3db.configure("pr_identity", list_fields = list_fields) - elif method == "cv" or component_name == "experience": - table = s3db.hrm_experience - # Use simple free-text variants - table.organisation_id.default = None # should not default in this case - table.organisation.readable = True - table.organisation.writable = True - table.job_title.readable = True - table.job_title.writable = True - table.comments.label = T("Main Duties") - from s3 import S3SQLCustomForm - crud_form = S3SQLCustomForm("organisation", - "job_title", - "comments", - "start_date", - "end_date", - ) - s3db.configure("hrm_experience", - crud_form = crud_form, - list_fields = ["organisation", - "job_title", - "comments", - "start_date", - "end_date", - ], - ) - if method == "cv": - # Customize CV - s3db.set_method("pr", "person", - method = "cv", - action = s3db.hrm_CV(form=vnrc_cv_form)) - - elif component_name == "salary": - stable = s3db.hrm_salary - stable.salary_grade_id.label = T("Grade Code") - field = stable.monthly_amount - field.readable = field.writable = False + elif component_name == "competency": + ctable = s3db.hrm_competency + # Hide confirming organisation (defaults to VNRC) + ctable.organisation_id.readable = False - elif component_name == "competency": - ctable = s3db.hrm_competency - # Hide confirming organisation (defaults to VNRC) - ctable.organisation_id.readable = False - - elif component_name == "membership": - field = s3db.member_membership.fee_exemption - field.readable = field.writable = True - PROGRAMMES = T("Programs") - from s3 import S3SQLCustomForm, S3SQLInlineLink - crud_form = S3SQLCustomForm("organisation_id", - "code", - "membership_type_id", - "start_date", - "end_date", - "membership_fee", - "membership_paid", - "fee_exemption", - S3SQLInlineLink("programme", - field = "programme_id", - label = PROGRAMMES, - ), - ) + elif component_name == "membership": + field = s3db.member_membership.fee_exemption + field.readable = field.writable = True + PROGRAMMES = T("Programs") + from s3 import S3SQLCustomForm, S3SQLInlineLink + crud_form = S3SQLCustomForm("organisation_id", + "code", + "membership_type_id", + "start_date", + "end_date", + "membership_fee", + "membership_paid", + "fee_exemption", + S3SQLInlineLink("programme", + field = "programme_id", + label = PROGRAMMES, + ), + ) - list_fields = ["organisation_id", - "membership_type_id", - "start_date", - (T("Paid"), "paid"), - (T("Email"), "email.value"), - (T("Phone"), "phone.value"), - (PROGRAMMES, "membership_programme.programme_id"), - ] + list_fields = ["organisation_id", + "membership_type_id", + "start_date", + (T("Paid"), "paid"), + (T("Email"), "email.value"), + (T("Phone"), "phone.value"), + (PROGRAMMES, "membership_programme.programme_id"), + ] - s3db.configure("member_membership", - crud_form = crud_form, - list_fields = list_fields, - ) + s3db.configure("member_membership", + crud_form = crud_form, + list_fields = list_fields, + ) return True s3.prep = custom_prep diff --git a/modules/templates/IFRC/hrm_skill.csv b/modules/templates/IFRC/hrm_skill.csv index 0aa3dd7226..072c30245a 100644 --- a/modules/templates/IFRC/hrm_skill.csv +++ b/modules/templates/IFRC/hrm_skill.csv @@ -16,4 +16,30 @@ Language,Arabic - Reading, Language,Arabic - Writing, Computer,Word, Computer,Excel, -Computer,PowerPoint, \ No newline at end of file +Computer,PowerPoint, +RDRT_AP,Administration/General Support, +RDRT_AP,Climate Change, +RDRT_AP,Communication, +RDRT_AP,Disaster Management, +RDRT_AP,Disaster Preparedness & Risk Reduction, +RDRT_AP,Finance, +RDRT_AP,"Food security, Nutrition and Livelihoods", +RDRT_AP,Resource Mobilization, +RDRT_AP,Health, +RDRT_AP,Human Resources, +RDRT_AP,Humanitarian Diplomacy, +RDRT_AP,Advocacy Information / Knowledge Management, +RDRT_AP,Risk Management and Audit, +RDRT_AP,Information Systems, +RDRT_AP,Education and Learning, +RDRT_AP,Legal/Disaster Law Logistics Management, +RDRT_AP,Movement Cooperation, +RDRT_AP,Organizational Development, +RDRT_AP,Planning / Monitoring / Evaluation / Reporting, +RDRT_AP,Security, +RDRT_AP,Shelter and Settlements, +RDRT_AP,Urban Risk and Community Resilience, +RDRT_AP,Youth and Volunteering, +RDRT_AP,Water Sanitation and Hygiene Promotion, +RDRT_AP,EDUCATION, +RDRT_AP,"Other, please specify", diff --git a/modules/templates/IFRC/menus.py b/modules/templates/IFRC/menus.py index 47b75793bd..d58c8f1c35 100644 --- a/modules/templates/IFRC/menus.py +++ b/modules/templates/IFRC/menus.py @@ -49,24 +49,47 @@ def menu_modules(cls): T = current.T auth = current.auth has_role = auth.s3_has_role + s3db = current.s3db - if not has_role(ADMIN) and auth.s3_has_roles(("EVENT_MONITOR", "EVENT_ORGANISER", "EVENT_OFFICE_MANAGER")): - # Simplified menu for Bangkok CCST - return [homepage("hrm", "org", name=T("Training Events"), f="training_event", - #vars=dict(group="staff"), check=hrm)( - vars=dict(group="staff"))( - #MM("Training Events", c="hrm", f="training_event"), - #MM("Trainings", c="hrm", f="training"), - #MM("Training Courses", c="hrm", f="course"), - ), - ] + if not has_role(ADMIN): + if auth.s3_has_roles(("EVENT_MONITOR", "EVENT_ORGANISER", "EVENT_OFFICE_MANAGER")): + # Simplified menu for Bangkok CCST + return [homepage("hrm", "org", name=T("Training Events"), f="training_event", + #vars=dict(group="staff"), check=hrm)( + vars=dict(group="staff"))( + #MM("Training Events", c="hrm", f="training_event"), + #MM("Trainings", c="hrm", f="training"), + #MM("Training Courses", c="hrm", f="course"), + ), + ] + elif not has_role("RDRT_ADMIN") and auth.s3_has_role("RDRT_MEMBER"): + # Simplified menu for AP RDRT + db = current.db + person_id = auth.s3_logged_in_person() + atable = s3db.deploy_application + htable = s3db.hrm_human_resource + query = (atable.human_resource_id == htable.id) & \ + (htable.person_id == person_id) + member = db(query).select(htable.id, + cache = s3db.cache, + limitby = (0, 1), + ).first() + if member: + profile = MM("My RDRT Profile", + c="deploy", f="human_resource", args=[member.id, "profile"], + ) + else: + profile = None + return [homepage("deploy", name="RDRT")( + profile, + ), + ] settings = current.deployment_settings root_org = auth.root_org_name() ORG_ADMIN = system_roles.ORG_ADMIN - s3db = current.s3db s3db.inv_recv_crud_strings() inv_recv_list = current.response.s3.crud_strings.inv_recv.title_list @@ -108,8 +131,8 @@ def outreach(item): return root_org == NZRC or \ root_org is None and has_role(ADMIN) - def rdrt_admin(item): - return has_role("RDRT_ADMIN") + #def rdrt_admin(item): + # return has_role("RDRT_ADMIN") #def vol(item): # return root_org != HNRC or \ @@ -195,7 +218,7 @@ def vol_teams(item): ), homepage("deploy", name="RDRT", f="mission", m="summary", vars={"~.status__belongs": "2"})( - MM("InBox", c="deploy", f="email_inbox", check=rdrt_admin), + MM("InBox", c="deploy", f="email_inbox"), MM("Missions", c="deploy", f="mission", m="summary"), MM("Members", c="deploy", f="human_resource", m="summary"), ), @@ -453,6 +476,7 @@ def deploy(): query = (atable.human_resource_id == htable.id) & \ (htable.person_id == person_id) member = db(query).select(htable.id, + cache = s3db.cache, limitby = (0, 1), ).first() if member: @@ -506,13 +530,7 @@ def deploy(): ), ), ) - else: - focal_points = None - inbox = None - training = None - members = None - - return M()(M("Alerts", + return M()(M("Alerts", c="deploy", f="alert")( M("Create", m="create"), inbox, @@ -541,6 +559,8 @@ def deploy(): ), #M("Online Manual", c="deploy", f="index"), ) + else: + return M()(profile) # Africa (Default) return M()(M("Missions", diff --git a/modules/templates/SAMBRO/config.py b/modules/templates/SAMBRO/config.py index f06145c80b..fec571e33f 100644 --- a/modules/templates/SAMBRO/config.py +++ b/modules/templates/SAMBRO/config.py @@ -392,7 +392,7 @@ def customise_cap_alert_resource(r, tablename): s3db = current.s3db def onapprove(record): # Normal onapprove - s3db.cap_alert_approve(record) + s3db.cap_alert_onapprove(record) async_task = current.s3task.async @@ -1195,7 +1195,7 @@ def get_html_email_content(row, ack_id=None, system=True): if description else "", BR() if not isinstance(description, list) else "", BR() if response_type else "", - XML(T("%(label)s: %(response_type)s") % + XML(T("%(label)s: %(response_type)s") % {"label": B(T("Expected Response")), "response_type": s3_str(get_formatted_value(response_type, represent = itable.response_type.represent, @@ -1505,7 +1505,7 @@ def get_formatted_value(value, # ------------------------------------------------------------------------- def _get_or_create_attachment(alert_id): - """ + """ Retrieve the CAP attachment for the alert_id if present else creates CAP file as attachment to be sent with the email returns the document_id for the CAP file diff --git a/modules/templates/SAMBRO/controllers.py b/modules/templates/SAMBRO/controllers.py index df34dd0292..0d8c08241c 100644 --- a/modules/templates/SAMBRO/controllers.py +++ b/modules/templates/SAMBRO/controllers.py @@ -1568,7 +1568,7 @@ def __call__(self): response = {"r": roles, "uid": user.id, "pid": auth.s3_logged_in_person(), - "o": current.deployment_settings.get_cap_expire_offset(), + "o": current.deployment_settings.get_cap_info_effective_period(), } current.response.headers["Content-Type"] = "application/json" return json.dumps(response) diff --git a/modules/templates/VM/CCC/Cumbria_L2.csv b/modules/templates/VM/CCC/Cumbria_L2.csv new file mode 100644 index 0000000000..9b964923e7 --- /dev/null +++ b/modules/templates/VM/CCC/Cumbria_L2.csv @@ -0,0 +1,7 @@ +Country,L1,L2 +GB,Cumbria,Allerdale +GB,Cumbria,Barrow-in-Furness +GB,Cumbria,Carlisle +GB,Cumbria,Copeland +GB,Cumbria,Eden +GB,Cumbria,South Lakeland \ No newline at end of file diff --git a/modules/templates/VM/CCC/Demo/organisation.csv b/modules/templates/VM/CCC/Demo/organisation.csv new file mode 100644 index 0000000000..d9857c4957 --- /dev/null +++ b/modules/templates/VM/CCC/Demo/organisation.csv @@ -0,0 +1,2 @@ +Organisation,Acronym,Type,Country,Website,Domain,Sectors +Community Organisation,,Volunteer,GB, diff --git a/modules/templates/VM/CCC/Demo/tasks.cfg b/modules/templates/VM/CCC/Demo/tasks.cfg new file mode 100644 index 0000000000..c708681e09 --- /dev/null +++ b/modules/templates/VM/CCC/Demo/tasks.cfg @@ -0,0 +1,19 @@ +# ============================================================================= +# Add a list of CSV files to import into the system +# +# The list of import files is a comma separated list as follows: +# +# prefix,tablename,csv file name,stylesheet +# +# The CSV file is assumed to be in the same directory as this file +# The style sheet is assumed to be in either of the following directories: +# static/formats/s3csv/prefix/ +# static/formats/s3csv/ +# +# For details on how to import data into the system see the following: +# zzz_1st_run +# s3import::S3BulkImporter +# ============================================================================= +org,organisation,organisation.csv,organisation.xsl +*,import_user,users.csv +# ============================================================================= diff --git a/modules/templates/VM/CCC/Demo/users.csv b/modules/templates/VM/CCC/Demo/users.csv new file mode 100644 index 0000000000..99d8be4ae2 --- /dev/null +++ b/modules/templates/VM/CCC/Demo/users.csv @@ -0,0 +1,5 @@ +First Name,Last Name,Email,Password,Role,Organisation,Facility Type,Office +Reserve,Volunteer,reserve@example.com,eden,"RESERVE/pr_group.name=Reserves",,, +Community,Volunteer,community@example.com,eden,VOLUNTEER,Community Organisation,, +Org,Admin,orgadmin@example.com,eden,ORG_ADMIN,Community Organisation,, +Admin,User,admin@example.com,testing,ADMIN, \ No newline at end of file diff --git a/modules/templates/VM/CCC/auth_consent_option.csv b/modules/templates/VM/CCC/auth_consent_option.csv new file mode 100644 index 0000000000..9116440e46 --- /dev/null +++ b/modules/templates/VM/CCC/auth_consent_option.csv @@ -0,0 +1,2 @@ +Code,Type,Title,Explanation,Valid From,OptOut,Mandatory,Comments +STOREPID,Storing Personal Data,Store my Personal Data,We can store your personal data and use it for some purpose.,2019-06-01,false,true, diff --git a/modules/templates/VM/CCC/auth_roles.csv b/modules/templates/VM/CCC/auth_roles.csv index 211d85ac12..ed2272bfc9 100644 --- a/modules/templates/VM/CCC/auth_roles.csv +++ b/modules/templates/VM/CCC/auth_roles.csv @@ -36,6 +36,7 @@ AUTHENTICATED,Authenticated,,,,pr_contact,CREATE,READ|UPDATE|DELETE,, AUTHENTICATED,Authenticated,,,,pr_contact_emergency,CREATE,READ|UPDATE|DELETE,, AUTHENTICATED,Authenticated,,,,pr_person,,READ|UPDATE,, ORG_ADMIN,"Organisation Administrator",,admin,user,,ALL,, +ORG_ADMIN,"Organisation Administrator",,cms,post,,CREATE|READ|UPDATE|DELETE,,, ORG_ADMIN,"Organisation Administrator",,,,doc_document,ALL,,, ORG_ADMIN,"Organisation Administrator",,hrm,person,,CREATE|READ|UPDATE|DELETE,,, ORG_ADMIN,"Organisation Administrator",,hrm,volunteer,,ALL,,, @@ -52,5 +53,7 @@ ORG_ADMIN,"Organisation Administrator",,,,pr_address,CREATE|READ|UPDATE|DELETE,, ORG_ADMIN,"Organisation Administrator",,,,pr_contact,CREATE|READ|UPDATE|DELETE,,, ORG_ADMIN,"Organisation Administrator",,,,pr_contact_emergency,CREATE|READ|UPDATE|DELETE,,, ORG_ADMIN,"Organisation Administrator",,,,pr_person,CREATE|READ|UPDATE|DELETE,,, -ORG_ADMIN,"Organisation Administrator",,req,,,CREATE|READ|UPDATE|DELETE,,, -ORG_ADMIN,"Organisation Administrator",,,req,,CREATE|READ|UPDATE|DELETE,,, +ORG_ADMIN,"Organisation Administrator",,req,req,,CREATE|READ|UPDATE|DELETE,,, +VOLUNTEER,"Community Volunteer",,cms,post,,READ,,, +VOLUNTEER,"Community Volunteer",,req,req,,READ,,, +RESERVE,"Reserve Volunteer",,cms,post,,READ,,, diff --git a/modules/templates/VM/CCC/cms_post.csv b/modules/templates/VM/CCC/cms_post.csv index fdb955423e..c29a4ac829 100644 --- a/modules/templates/VM/CCC/cms_post.csv +++ b/modules/templates/VM/CCC/cms_post.csv @@ -1,4 +1,4 @@ Name,Module,Resource,Body -Homepage,default,index,"

Help for Cumbria

Information about donating money and general support advice

" -Donate,req,index,"

Donate Items

Information on where items can be donated locally or nationally.

Perhaps a quote from a child?

" -Volunteer,vol,index,"

Volunteer Your Time

Information on BRC and CVS.

What it means to volunteer.

Perhaps a quote?

" +Homepage,default,index,"

Support Cumbria

Support Cumbria is a collaboration of agencies and communities within the County of Cumbria to offer support during a major incident at a community level.

Cumbria appreciates and is thankful to all those wishing to volunteer their time, offer accommodation, offer donated goods or wish to raise money to support those affected.

The greatest benefit is felt through a coordinated response with preparation.

Impacts and consequences of a Major Incident on people’s lives can be devastating and wide ranging.

Disaster appeals are a way in which the public and businesses can support those impacted through allowing the individual to decide what is needed. Financial support allows people to make decisions and some control over the impacts.

In Cumbria there is a partnership with Cumbria Community Foundation who will assist with processing disaster funds.

Cumbria Community Foundation is part of a national and international network of community foundations. Together we seek to support people and organisations wishing to invest in the local community.

If a disaster fund is established you will be able to find details on how to donate on their Website: https://www.cumbriafoundation.org/

Other ways to support communities during a major incident is to donate items or volunteer your time.

If you are already part of an organisation which supports Cumbria in an Emergency but need to register please click here.

" +Donate,req,index,"

Donating items

In the first instance it is paramount that people's safety is the first consideration, and therefore it is difficult to also accommodate storage of items, however assisting people to rebuild their lives is important and you might want to consider the following:

Have a think about what items may be needed once a community starts to move towards recovery from an incident?

Please remember that some of these items won’t be needed for some time and people may be living away from home and be unable to store them.

Donations of the following items, equipment or services may be useful for those affected:

  • Furniture
  • Domestic Supplies (such as cleaning)
  • Bedding
  • Catering
  • White Goods
  • Transportation Assistance
  • Personal Care Items (mobility and hygiene)

What condition should items be in?

The condition of items often depends on the type of items being offered but below are a few key points you may wish to consider:

  • Clothing - Consider raising money so those affected can choose clothing that is appropriate to their needs and fits. This can be important dealing with the well being of those affected.
  • Furniture - Think about 'saleable' condition as a bench mark.
  • Soft Furnishing - It is important that these meet the Furniture and Furnishings (Fire Safety) Regulations 1988.
  • Soft Toys and Games - Please ensure these are in excellent condition and complete, and meet the British Standards Kitemark.
  • Cleaning Products/Equipment - These need to be sealed and in original packaging to ensure any irritants and usage instructions are clearly available.

Where can I take items or how can I tell people about them?

Support Cumbria works with a number of organisations and charities. Those that take large items are listed below:

Existing charitable organisations who have furniture departments;

Check out where is local to you, or who offer free collection on the Reuse Network - https://reuse-network.org.uk/

For more information on Charity Shops in your area see the Charity Retail Association - https://www.charityretail.org.uk/find-a-charity-shop/

Also think about existing platforms where items are offered free of charge to those that need them such as;

Freegle - https://www.ilovefreegle.org/

Freeloved (Preloved) - https://www.preloved.co.uk/freeloved

Freecycle - https://www.freecycle.org/

You can also register Your Donation below. Your details will be held on a secure database and you will be contacted directly by Agencies, Community Groups or other third Sector Organisations working on the incident who need extra support.

" +Volunteer,vol,index,"

Volunteering Your Time

If you have been interested in volunteering in your local area, your Local Volunteer Council can show you opportunities. In Cumbria you can find information on how you can make a difference from the Cumbria Council for Voluntary Service.

If you are particularly interested in Major Incident Response there are many local Community Resilience Groups in Cumbria to volunteer with or the British Red Cross have a Community Reserve Volunteer Scheme. The community reserve volunteer program is a new volunteering opportunity allowing you to make a difference during a flood, fire or any other major emergency near you. Sign up today and take the first step towards being prepared in a crisis.

https://reserves.redcross.org.uk/

Although we understand you might be keen travel to the affected area as soon as possible offer assistance, routes need to be kept clear for emergency services vehicles and personnel.

You can also register to join the Cumbria Reserve Volunteers below. Your details will be held on a secure database and you will be contacted directly by Agencies, Community Groups or other third Sector Organisations working on the incident who need extra support.

It is also important to remember that a sustained volunteering effort will be required to help people recover from a major incident and often there is more need for volunteers in the weeks following an incident.

If you are an established group from outside of Cumbria but wish to offer support as a group please use the link below to register:

We ask you not to self-deploy and any self-deployment in the affected area is done so at your own risk.

Self Deployment

Many people who are local to an incident may wish to consider attending the scene to see if they can help, or offer to assist neighbours of local residents. This is called self-deployment and falls outside of the co-ordinated response. You need to consider your own and others' safety as you do not fall under the insurance of responding organisations.

" diff --git a/modules/templates/VM/CCC/config.py b/modules/templates/VM/CCC/config.py index e63a52257c..dfc1294fd2 100644 --- a/modules/templates/VM/CCC/config.py +++ b/modules/templates/VM/CCC/config.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -#from collections import OrderedDict +from collections import OrderedDict from gluon import current from gluon.storage import Storage @@ -15,8 +15,8 @@ def config(settings): T = current.T - settings.base.system_name = T("Help for Cumbria") - settings.base.system_name_short = T("Help Cumbria") + settings.base.system_name = T("Support Cumbria") + settings.base.system_name_short = T("Support Cumbria") # Theme settings.base.theme = "CCC" @@ -25,15 +25,23 @@ def config(settings): # PrePopulate data settings.base.prepopulate += ("VM/CCC",) - #settings.base.prepopulate_demo += ("VM/CCC/Demo",) + settings.base.prepopulate_demo = ("VM/CCC/Demo",) # Authentication settings # 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? + # - varies by path (see customise_auth_user_controller) #settings.auth.registration_requires_approval = True settings.auth.registration_requests_organisation = True + # ------------------------------------------------------------------------- + # L10n (Localization) settings + settings.L10n.languages = OrderedDict([ + ("en-gb", "English"), + ]) + # Default Language + settings.L10n.default_language = "en-gb" # Uncomment to Hide the language toolbar settings.L10n.display_toolbar = False @@ -114,4 +122,18 @@ def customise_vol_home(): settings.customise_vol_home = customise_vol_home + # ------------------------------------------------------------------------- + def customise_auth_user_controller(**attr): + + if current.request.args(0) == "register": + # Not easy to tweak the URL in the login form's buttons + from gluon import redirect, URL + redirect(URL(c="default", f="index", + args="register", + vars=current.request.get_vars)) + + return attr + + settings.customise_auth_user_controller = customise_auth_user_controller + # END ========================================================================= diff --git a/modules/templates/VM/CCC/controllers.py b/modules/templates/VM/CCC/controllers.py index fd3721d56b..ab98b4a740 100644 --- a/modules/templates/VM/CCC/controllers.py +++ b/modules/templates/VM/CCC/controllers.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- +import uuid + from gluon import * -from s3 import S3CustomController +from s3 import S3CustomController, s3_mark_required THEME = "VM/CCC" @@ -35,9 +37,15 @@ def __call__(self): item = DIV(XML(item.body), BR(), A(current.T("Edit"), - _href=URL(c="cms", f="post", - args=[item.id, "update"]), - _class="action-btn")) + _href = URL(c="cms", f="post", + args = [item.id, "update"], + vars = {"module": module, + "resource": resource, + }, + ), + _class="action-btn", + ), + ) else: item = DIV(XML(item.body)) elif ADMIN: @@ -46,11 +54,13 @@ def __call__(self): else: _class = "action-btn" item = A(current.T("Edit"), - _href=URL(c="cms", f="post", args="create", - vars={"module": module, - "resource": resource - }), - _class="%s cms-edit" % _class) + _href = URL(c="cms", f="post", args="create", + vars = {"module": module, + "resource": resource, + }, + ), + _class="%s cms-edit" % _class, + ) else: item = "" else: @@ -60,4 +70,278 @@ def __call__(self): self._view(THEME, "index.html") return output +# ============================================================================= +class register(S3CustomController): + """ Custom Registration Page """ + + def __call__(self): + + auth = current.auth + auth_settings = auth.settings + + # Redirect if already logged-in + if auth.is_logged_in(): + redirect(auth_settings.logged_url) + + T = current.T + db = current.db + s3db = current.s3db + + request = current.request + response = current.response + session = current.session + settings = current.deployment_settings + + auth_messages = auth.messages + + utable = auth_settings.table_user + passfield = auth_settings.password_field + + # Check Type of Registration + individual = group = existing = False + + get_vars_get = current.request.get_vars.get + if get_vars_get("individual"): + # Individual Volunteer + individual = True + title = T("Register as a Volunteer") + header = "" + f = utable.organisation_id + f.readable = f.writable = False + + elif get_vars_get("group"): + # Volunteer Group + group = True + title = T("Register as a Volunteer Group") + header = "" + f = utable.organisation_id + f.readable = f.writable = False + + elif get_vars_get("existing"): + # Volunteer for Existing Organisation + existing = True + title = T("Register as a Volunteer for an existing Organisation") + header = "" + # Cannot create a new Org here + f = utable.organisation_id + f.comment = None + # @ToDo: Filter dropdown to just those who are accepting volunteers + + else: + # Organisation or Agency + title = T("Register as an Organisation or Agency") + header = P("This is for known CEP/Flood Action Group etc based within Cumbria. Please use ", + A("Volunteer Group", _href=URL(args="register", vars={"group": 1})), + " if you do not fall into these", + ) + # @ToDo: Filter dropdown to just those who are accepting volunteers + #f = utable.organisation_id + #f.comment = None + # @ToDo: Filter out all existing Orgs, ut allow creation of new one + #f.requires = IS_ONE_OF() + + + # Instantiate Consent Tracker + # TODO: limit to relevant data processing types + consent = s3db.auth_Consent() + + # Form Fields + + #mobile_label = settings.get_ui_label_mobile_phone() + + gtable = s3db.gis_location + districts = db((gtable.level == "L2") & (gtable.L1 == "Cumbria")).select(gtable.id, + gtable.name) + districts = {d.id:d.name for d in districts} + + formfields = [utable.first_name, + utable.last_name, + utable.organisation_id, + Field("addr_L2", + label = T("Where Based (District)"), + requires = IS_IN_SET(districts), + ), + Field("addr_street", + label = T("Street Address"), + ), + Field("addr_postcode", + label = T("Postcode"), + ), + Field("mobile", + label = T("Contact Number (Preferred)"), + comment = DIV(_class = "tooltip", + _title = "%s|%s" % (T("Contact Number (Preferred)"), + T("Ideally a Mobile Number, so that we can send you Text Messages.")), + ), + ), + Field("home_phone", + label = T("Contact Number (Secondary)"), + ), + utable.email, + utable[passfield], + # Password Verification Field + Field("password_two", "password", + label = auth_messages.verify_password, + requires = IS_EXPR("value==%s" % \ + repr(request.vars.get(passfield)), + error_message = auth_messages.mismatched_password, + ), + ), + # Consent Question + Field("consent", + label = T("Consent"), + widget = consent.widget, + ), + ] + + # Generate labels (and mark required fields in the process) + required_fields = ["first_name", + "last_name", + "addr_street", + "addr_postcode", + "mobile", + ] + labels = s3_mark_required(formfields, mark_required=required_fields)[0] + + # Form buttons + REGISTER = T("Register") + buttons = [INPUT(_type="submit", _value=REGISTER), + A(T("Login"), + _href=URL(f="user", args="login"), + _id="login-btn", + _class="action-lnk", + ), + ] + + # Construct the form + response.form_label_separator = "" + form = SQLFORM.factory(table_name = utable._tablename, + record = None, + hidden = {"_next": request.vars._next}, + labels = labels, + separator = "", + showid = False, + submit_button = REGISTER, + delete_label = auth_messages.delete_label, + formstyle = settings.get_ui_formstyle(), + buttons = buttons, + *formfields) + + # Identify form for CSS & JS Validation + form.add_class("auth_register") + + # Inject client-side Validation + auth.s3_register_validation() + + # Captcha, if configured + #if auth_settings.captcha != None: + # form[0].insert(-1, DIV("", auth_settings.captcha, "")) + + # Set default registration key, so new users are prevented + # from logging in until approved + utable.registration_key.default = key = str(uuid.uuid4()) + + if form.accepts(request.vars, + session, + formname = "register", + onvalidation = auth_settings.register_onvalidation, + ): + + # Create the user record + user_id = utable.insert(**utable._filter_fields(form.vars, id=False)) + form.vars.id = user_id + + # Save temporary user fields + auth.s3_user_register_onaccept(form) + + # Where to go next? + register_next = request.vars._next or auth_settings.register_next + + # Post-process the new user record + users = db(utable.id > 0).select(utable.id, limitby=(0, 2)) + if len(users) == 1: + # 1st user to register doesn't need verification/approval + auth.s3_approve_user(form.vars) + current.session.confirmation = auth_messages.registration_successful + + # 1st user gets Admin rights + admin_group_id = 1 + auth.add_membership(admin_group_id, users.first().id) + + # Log them in + if "language" not in form.vars: + # Was missing from login form + form.vars.language = T.accepted_language + user = Storage(utable._filter_fields(form.vars, id=True)) + auth.login_user(user) + + # Send welcome email + auth.s3_send_welcome_email(form.vars) + + elif auth_settings.registration_requires_verification: + # System Details for Verification Email + system = {"system_name": settings.get_system_name(), + "url": "%s/default/user/verify_email/%s" % (response.s3.base_url, key), + } + + # Try to send the Verification Email + if not auth_settings.mailer or \ + not auth_settings.mailer.settings.server or \ + not auth_settings.mailer.send(to = form.vars.email, + subject = auth_messages.verify_email_subject % system, + message = auth_messages.verify_email % system, + ): + response.error = auth_messages.email_verification_failed + return form + + # Redirect to Verification Info page + register_next = URL(c = "default", + f = "message", + args = ["verify_email_sent"], + vars = {"email": form.vars.email}, + ) + + else: + # Does the user need to be approved? + approved = auth.s3_verify_user(form.vars) + + if approved: + # Log them in + if "language" not in form.vars: + # Was missing from login form + form.vars.language = T.accepted_language + user = Storage(utable._filter_fields(form.vars, id=True)) + auth.login_user(user) + + # Set a Cookie to present user with login box by default + #auth.set_cookie() + + # Log action + log = auth_messages.register_log + if log: + auth.log_event(log, form.vars) + + # Run onaccept for registration form + onaccept = auth_settings.register_onaccept + if onaccept: + onaccept(form) + + # Redirect + if not register_next: + register_next = auth.url(args = request.args) + elif isinstance(register_next, (list, tuple)): + # fix issue with 2.6 + register_next = register_next[0] + elif register_next and not register_next[0] == "/" and register_next[:4] != "http": + register_next = auth.url(register_next.replace("[id]", str(form.vars.id))) + redirect(register_next) + + # Custom View + self._view(THEME, "register.html") + + return {"title": title, + "header": header, + "form": form, + } + # END ========================================================================= diff --git a/modules/templates/VM/CCC/menus.py b/modules/templates/VM/CCC/menus.py index 727a638b46..1cd9ef9df4 100644 --- a/modules/templates/VM/CCC/menus.py +++ b/modules/templates/VM/CCC/menus.py @@ -36,12 +36,33 @@ def menu(cls): def menu_modules(cls): """ Custom Modules Menu """ - return [MM("Volunteer", c="vol", f="index", + menu = [MM("Volunteer Your Time", c="vol", f="index", ), - MM("Donate", c="req", f="index", + MM("Donate Items", c="req", f="index", ), ] + auth = current.auth + if auth.is_logged_in(): + menu.append(MM("General Information and Advice", c="cms", f="post")) + + if auth.s3_has_role("ADMIN"): + menu += [MM("Events", c="req", f="req", + ), + MM("All Documents", c="cms", f="post", + ), + ] + elif auth.s3_has_role("VOLUNTEER"): + menu += [MM("Events", c="req", f="req", + ), + MM("Organisation Documents", c="cms", f="post", + ), + MM("Contact Organisation Admins", c="cms", f="post", + ), + ] + + return menu + # ------------------------------------------------------------------------- @classmethod def menu_org(cls): @@ -79,9 +100,7 @@ def menu_personal(cls): auth = current.auth #s3 = current.response.s3 - settings = current.deployment_settings - - ADMIN = current.auth.get_system_roles().ADMIN + #settings = current.deployment_settings if not auth.is_logged_in(): request = current.request @@ -91,23 +110,24 @@ def menu_personal(cls): "_next" in request.get_vars: login_next = request.get_vars["_next"] - self_registration = settings.get_security_self_registration() + #self_registration = settings.get_security_self_registration() menu_personal = MP()( - MP("Register", c="default", f="user", - m = "register", - check = self_registration, - ), + #MP("Register", c="default", f="user", + # m = "register", + # check = self_registration, + # ), MP("Login", c="default", f="user", m = "login", vars = {"_next": login_next}, ), ) - if settings.get_auth_password_retrieval(): - menu_personal(MP("Lost Password", c="default", f="user", - m = "retrieve_password", - ), - ) + #if settings.get_auth_password_retrieval(): + # menu_personal(MP("Lost Password", c="default", f="user", + # m = "retrieve_password", + # ), + # ) else: + ADMIN = current.auth.get_system_roles().ADMIN s3_has_role = auth.s3_has_role is_org_admin = lambda i: not s3_has_role(ADMIN) and \ s3_has_role("ORG_ADMIN") @@ -132,12 +152,12 @@ def menu_personal(cls): @classmethod def menu_about(cls): - ADMIN = current.auth.get_system_roles().ADMIN + #ADMIN = current.auth.get_system_roles().ADMIN menu_about = MA(c="default")( MA("Help", f="help"), - MA("Contact", f="contact"), - MA("Version", f="about", restrict = ADMIN), + MA("Contact Us", f="contact"), + #MA("Version", f="about", restrict = ADMIN), ) return menu_about diff --git a/modules/templates/VM/CCC/pr_group.csv b/modules/templates/VM/CCC/pr_group.csv new file mode 100644 index 0000000000..bea8c4d4e0 --- /dev/null +++ b/modules/templates/VM/CCC/pr_group.csv @@ -0,0 +1,2 @@ +uuid,Name,Type,Description +,Reserves,,Just for realm-restricted Role \ No newline at end of file diff --git a/modules/templates/VM/CCC/tasks.cfg b/modules/templates/VM/CCC/tasks.cfg index 9c7dfc31d7..eeff4aa296 100644 --- a/modules/templates/VM/CCC/tasks.cfg +++ b/modules/templates/VM/CCC/tasks.cfg @@ -16,9 +16,15 @@ # ============================================================================= # Roles *,import_role,auth_roles.csv +pr,group,pr_group.csv,group.xsl +# ----------------------------------------------------------------------------- +# Consent Tracking +auth,consent_option,auth_consent_option.csv,consent_option.xsl # ----------------------------------------------------------------------------- # GIS Config +gis,location,Cumbria_L2.csv,location.xsl gis,config,gis_config.csv,config.xsl +# ----------------------------------------------------------------------------- # CMS cms,post,cms_post.csv,post.xsl # END ------------------------------------------------------------------------- diff --git a/modules/templates/VM/CCC/views/donate.html b/modules/templates/VM/CCC/views/donate.html index 65409b7a85..713db73bfe 100644 --- a/modules/templates/VM/CCC/views/donate.html +++ b/modules/templates/VM/CCC/views/donate.html @@ -1,7 +1,16 @@ {{extend "layout.html"}} -{{=item}}
-
- Register a Donation +
+ {{=item}} +
+
+ diff --git a/modules/templates/VM/CCC/views/footer.html b/modules/templates/VM/CCC/views/footer.html index 97e73dd7a1..574b00aacf 100644 --- a/modules/templates/VM/CCC/views/footer.html +++ b/modules/templates/VM/CCC/views/footer.html @@ -1,8 +1,13 @@
-
+
{{=current.menu.about}}
-
+
+
+
+
+
+

Serving the people of Cumbria

diff --git a/modules/templates/VM/CCC/views/index.html b/modules/templates/VM/CCC/views/index.html index 4aeab4c4dc..32c8bdee34 100644 --- a/modules/templates/VM/CCC/views/index.html +++ b/modules/templates/VM/CCC/views/index.html @@ -1,19 +1,23 @@ {{extend "layout.html"}} -{{=item}}
-
+
+ {{=item}} +
+
+
+ -
+ diff --git a/modules/templates/VM/CCC/views/layout.html b/modules/templates/VM/CCC/views/layout.html index 0e4aeb1bc2..2673bbc1b7 100644 --- a/modules/templates/VM/CCC/views/layout.html +++ b/modules/templates/VM/CCC/views/layout.html @@ -71,7 +71,7 @@
{{=org_menu[0]}}
{{else:}}
{{pass}} -
{{if auth.is_logged_in():}}{{=auth.user.email}}{{else:}}{{=T("anonymous user")}}{{pass}}
+
{{if auth.is_logged_in():}}{{=auth.user.email}}{{else:}}{{#=T("anonymous user")}}{{pass}}
{{=current.menu.personal}}
diff --git a/modules/templates/VM/CCC/views/register.html b/modules/templates/VM/CCC/views/register.html new file mode 100644 index 0000000000..649ec2f9e1 --- /dev/null +++ b/modules/templates/VM/CCC/views/register.html @@ -0,0 +1,12 @@ +{{extend "layout.html"}} +
+
+ {{try:}}{{=H2(title)}}{{except:}}{{pass}} + {{try:}}{{=header}}{{except:}}{{pass}} +
+
+
+
+ {{=form}} +
+
diff --git a/modules/templates/VM/CCC/views/volunteer.html b/modules/templates/VM/CCC/views/volunteer.html index d629395485..86b09ea26e 100644 --- a/modules/templates/VM/CCC/views/volunteer.html +++ b/modules/templates/VM/CCC/views/volunteer.html @@ -1,8 +1,6 @@ {{extend "layout.html"}} -{{=item}} \ No newline at end of file diff --git a/modules/templates/locations/GB/config.py b/modules/templates/locations/GB/config.py index c9b43526ae..8bf8351d8b 100644 --- a/modules/templates/locations/GB/config.py +++ b/modules/templates/locations/GB/config.py @@ -16,7 +16,11 @@ def config(settings): # Uncomment to restrict to specific country/countries settings.gis.countries.append("GB") + # ------------------------------------------------------------------------- # L10n (Localization) settings + settings.L10n.languages["en-gb"] = "English (United Kingdom)" + # Default Language (put this in custom template if-required) + #settings.L10n.default_language = "en-gb" # Default timezone for users settings.L10n.timezone = "Europe/London" # Default Country Code for telephone numbers diff --git a/private/eden_deploy b/private/eden_deploy index 836cacc494..c18e62a5e1 160000 --- a/private/eden_deploy +++ b/private/eden_deploy @@ -1 +1 @@ -Subproject commit 836cacc4945574b0c53753dc84b8ef3395fc30fc +Subproject commit c18e62a5e100519227795db457969bd00f70c15c diff --git a/static/scripts/S3/s3.ui.organizer.js b/static/scripts/S3/s3.ui.organizer.js index c2d9d2ce17..7231226725 100644 --- a/static/scripts/S3/s3.ui.organizer.js +++ b/static/scripts/S3/s3.ui.organizer.js @@ -226,6 +226,8 @@ * @prop {string} labelEdit: label for Edit-button * @prop {string} labelDelete: label for the Delete-button * @prop {string} deleteConfirmation: the question for the delete-confirmation + * @prop {string} refreshIconClass: the CSS class for the refresh button icon + * @prop {string} calendarIconClass: the CSS class for the calendar button icon * */ options: { @@ -245,7 +247,10 @@ labelEdit: 'Edit', labelDelete: 'Delete', deleteConfirmation: 'Do you want to delete this entry?', - firstDay: 1 + firstDay: 1, + + refreshIconClass: 'fa fa-refresh', + calendarIconClass: 'fa fa-calendar' }, /** @@ -344,13 +349,13 @@ // View options customButtons: { reload: { - text: '', + text: 'Reload', click: function() { self.reload(); } }, calendar: { - text: '', + text: 'Calendar', click: function() { datePicker.datepicker('show'); } @@ -382,11 +387,15 @@ timezone: 'local' }); - // Remember reloadButton, use icon - this.reloadButton = $('.fc-reload-button').html(''); + // Button icons + var refreshIcon = $('').addClass(opts.refreshIconClass), + calendarIcon = $('').addClass(opts.calendarIconClass); + + // Store reloadButton, use icon + this.reloadButton = $('.fc-reload-button').empty().append(refreshIcon); // Move datepicker into header, use icon for calendar button - var calendarButton = $('.fc-calendar-button').html(''); + var calendarButton = $('.fc-calendar-button').empty().append(calendarIcon); datePicker.datepicker('option', {showOn: 'focus', showButtonPanel: true, firstDay: opts.firstDay}) .insertBefore(calendarButton) .on('change', function() { @@ -396,6 +405,9 @@ } }); + // Hide the datepicker dialog (sometimes showing after init) + datePicker.datepicker('widget').hide(); + // Add throbber var throbber = $('
').css({visibility: 'hidden'}); $('.fc-header-toolbar .fc-left', this.element).append(throbber); @@ -678,8 +690,13 @@ url = resource.baseURL; if (url && label) { - url += '/create.popup'; - var query = []; + + var link = createButton.get(0), + query = []; + + // Set path to create-dialog + link.href = url; + link.pathname += '/create.popup'; // Add refresh-target if (widgetID) { @@ -690,9 +707,15 @@ var dates = start.toISOString() + '--' + moment(end).subtract(1, 'seconds').toISOString(); query.push('organizer=' + encodeURIComponent(dates)); - url += '?' + query.join('&'); - createButton.attr('href', url) - .text(label) + // Update query part of link URL + if (link.search) { + link.search += '&' + query.join('&'); + } else { + link.search = '?' + query.join('&'); + } + + // Complete the button and append it to popup + createButton.text(label) .appendTo(contents) .on('click' + ns, function() { api.hide(); diff --git a/static/scripts/S3/s3.ui.organizer.min.js b/static/scripts/S3/s3.ui.organizer.min.js index 3f9715328f..d0c2533a80 100644 --- a/static/scripts/S3/s3.ui.organizer.min.js +++ b/static/scripts/S3/s3.ui.organizer.min.js @@ -9,21 +9,22 @@ requires qTip2 requires jQuery UI datepicker */ -(function(d,r){function q(){this.items={};this.slices=[]}var t=0;q.prototype.store=function(a,c,b){var e={};b.forEach(function(a){this.items[a.id]=e[a.id]=a},this);b=this.slices;a=[moment(a),moment(c),e];b.push(a);b.sort(function(a,b){return a[0].isBefore(b[0])?-1:b[0].isBefore(a[0])?1:a[1].isBefore(b[1])?-1:b[1].isBefore(a[1])?1:0});if(1');e=d(".fc-calendar-button").html(''); -n.datepicker("option",{showOn:"focus",showButtonPanel:!0,firstDay:a.firstDay}).insertBefore(e).on("change",function(){var a=n.datepicker("getDate");a&&b.fullCalendar("gotoDate",a)});a=d('
').css({visibility:"hidden"});d(".fc-header-toolbar .fc-left",this.element).append(a);this.throbber=a;this.resources=[];c&&c.forEach(function(a,b){this._addResource(a,b)},this);this._bindEvents()},_addResource:function(a,c){var b=d.extend({},a,{_cache:new q});this.resources.push(b);a= -b.timeout;a===r&&(a=this.options.timeout);var e=this;d(this.element).fullCalendar("addEventSource",{id:""+c,allDayDefault:!b.useTime,editable:!!b.editable,startEditable:!!b.startEditable,durationEditable:!!b.end&&!!b.durationEditable,events:function(a,c,d,g){e._fetchItems(b,a,c,d,g)}})},_eventRender:function(a,c){var b=this;d(c).qtip({content:{title:function(c,d){return b._itemTitle(a,d)},text:function(c,d){return b._itemDisplay(a,d)},button:!0},position:{at:"center right",my:"left center",effect:!1, -viewport:d(window),adjust:{method:"flip shift"}},show:{event:"click",solo:!0},hide:{event:"click mouseleave",delay:800,fixed:!0},events:{visible:function(){S3.addModals()}}})},_eventDestroy:function(a,c){d(c).qtip("destroy",!0)},_itemTitle:function(a){var c=[a.start.format(a.allDay&&"L"||"L LT")];a.end&&(a=moment(a.end).endOf("minute"),c.push(a.format("LT")));return c.join(" - ")},_itemDisplay:function(a,c){var b=d('
'),e=this.options,f=e.resources[a.source.id];d("
").html(a.popupTitle).appendTo(b); -var h=f.columns,k=a.description;h&&k&&h.forEach(function(a){var c=a[0];a=a[1];k[c]!==r&&(a&&d("