parent_location = bzr+ssh://bazaar.launchpad.net/%2Bbranch/sahana-eden/ submit_branch = bzr+ssh://bazaar.launchpad.net/%2Bbranch/sahana-eden/vita/ push_location = bzr+ssh://bazaar.launchpad.net/~ptressel/sahana-eden/devel/ [commit_data] new_revid = tressel@phobos-20110127212319-sf9o95t0tusc6bu5 message = """=== modified file 'controllers/gis.py' --- controllers/gis.py 2011-01-27 21:23:19 +0000 +++ controllers/gis.py 2011-01-30 15:46:37 +0000 @@ -146,23 +146,16 @@ table.wkt.comment = DIV(_class="stickytip", _title="WKT|" + T("The" + " " + T("Well-Known Text") + " " + "representation of the Polygon/Line.")) - if not deployment_settings.get_security_map() or s3_has_role("MapAdmin"): - table.members.comment = DIV( - _class="tooltip", - _title=T("Members") + "|" + T("A location group is a set of administrative regions -- member locations are added to a location group here. Location groups are used to restrict what is shown on the map and in search results to only entities in one of the regions in the group. A location group can be used to define the extent of an affected area, if it does not fall iwithin one administrative region. Location groups can be used in the Regions menu.")) - if r.method == "update": - # Don't allow converting a location group to non-group and vice - # versa. Don't allow taking away all the members of a group. - # Note: Non-interactive operations are restricted too. - # Note: The "notnull" setting gets the "required" * displayed, - # but does not seem to apply during validation, so the field is - # checked explicitly in onvalidation. - # @ToDo: There seem to be some other settings in the "interactive" - # section that might apply to non-interactive requests. + # We don't allow converting a location group to non-group and + # vice versa. We also don't allow taking away all the members of + # a group -- setting "notnull" gets the "required" * displayed. + # Groups don't have parents. (This is all checked in onvalidation.) location = get_location_info() if location.level == "GR": table.level.writable = False + table.parent.readable = False + table.parent.writable = False table.members.notnull = True # Record that this is a group location. Since we're setting # level to not writable, it won't be in either form.vars or @@ -172,6 +165,7 @@ else: table.members.writable = False table.members.readable = False + response.s3.location_is_group = False # Don't show street address, postcode for hierarchy on read or update. if r.method != "create" and r.id: @@ -262,10 +256,7 @@ try: location except: - location = db(db.gis_location.id == r.id).select( - db.gis_location.lat, - db.gis_location.lon, - db.gis_location.level, limitby=(0, 1)).first() + location = get_location_info() if location and location.lat is not None and location.lon is not None: lat = location.lat lon = location.lon @@ -290,23 +281,6 @@ return True response.s3.prep = lambda r, vars=vars: prep(r, vars) - # For location groups, set the level to "GR" if members are present. - # If it's already a group, don't allow clearing the members. (In general, - # once it's a group, it stays a group, and existing non-group locations - # can't be converted to groups. Latter was prevented by making members - # not readable or writable for non-group location update.) - def onvalidation(form): - - # Set "group" level if the location has members. - if "members" in form.vars and form.vars.members: - form.vars.level = "GR" - return - - # Is this a location group? Make sure no-one takes away all members. - if "location_is_group" in response.s3 and \ - "members" in form.vars and not form.vars.members: - form.errors["members"] = T("A location group must have at least one member.") - # Options _vars = request.vars filters = [] @@ -355,8 +329,6 @@ # We've been called from the Location Selector widget table.addr_street.readable = table.addr_street.writable = False - s3xrc.model.configure(table, onvalidation=onvalidation) - output = s3_rest_controller(module, resourcename) _map = vars.get("_map", None) === modified file 'deployment-templates/models/000_config.py' --- deployment-templates/models/000_config.py 2011-01-27 21:23:19 +0000 +++ deployment-templates/models/000_config.py 2011-01-30 14:37:30 +0000 @@ -16,6 +16,9 @@ if not FINISHED_EDITING_CONFIG_FILE: raise HTTP(501, body="Please edit models/000_config.py first") +from gluon.storage import Storage +from gluon.contrib.simplejson.ordered_dict import OrderedDict + deployment_settings = s3base.S3Config(T) # Database settings @@ -130,20 +133,19 @@ # Currently unused #deployment_settings.gis.display_L1 = True # Allow non-MapAdmins to edit Admin locations? -# (defaults to True, if not set) +# (Defaults to True, if not set. Permission to edit location groups defaults +# to false.) deployment_settings.gis.edit_L0 = False deployment_settings.gis.edit_L1 = True #deployment_settings.gis.edit_L2 = True -deployment_settings.gis.locations_hierarchy = { - "L0":T("Country"), - "L1":T("Province"), - "L2":T("District"), - "L3":T("Town"), - "L4":T("Village"), - #"L5":T("Neighbourhood"), # Currently not supported by testSuite - "XX":T("Imported"), # Filtered from view as no detailed information & many duplicates (e.g. coming from Ushahidi) - "GR":T("Location Group"), -} +deployment_settings.gis.locations_hierarchy = OrderedDict([ + ("L0", T("Country")), + ("L1", T("Province")), + ("L2", T("District")), + ("L3", T("Town")), + ("L4", T("Village")), + #("L5", T("Neighbourhood")), # Currently not supported by testSuite +]) # Should we require locations to follow strict hierarchy? deployment_settings.gis.strict_hierarchy = False # Maximum Marker Size @@ -197,8 +199,6 @@ # Comment/uncomment modules here to disable/enable them # Modules menu is defined in 01_menu.py -from gluon.storage import Storage -from gluon.contrib.simplejson.ordered_dict import OrderedDict deployment_settings.modules = OrderedDict( default = Storage( name_nice = T("Home"), === modified file 'models/00_utils.py' --- models/00_utils.py 2011-01-27 21:23:19 +0000 +++ models/00_utils.py 2011-01-30 16:56:44 +0000 @@ -451,7 +451,7 @@ # ----------------------------------------------------------------------------- def s3_represent_multiref(table, opt, represent=None, separator=", "): - """ @todo: docstring?? """ + """ Produce a representation for a list:reference field. """ if represent is None: if "name" in table.fields: @@ -465,13 +465,27 @@ query = query & (table.deleted == False) records = db(query).select() - try: - options = [represent(r) for r in records] - except TypeError: - options = [represent % r for r in records] - - if options: - return separator.join(map(str, options)) + + if records: + try: + first = represent(records[0]) + rep_function = represent + except TypeError: + first = represent % records[0] + rep_function = lambda r: represent % r + + # NB join only operates on strings, and some callers provide A(). + results = [first] + for record in records[1:len(records)-1]: + results.append(separator) + results.append(rep_function(record)) + + # Wrap in XML to allow showing anchors on read-only pages, else + # Web2py will escape the angle brackets, etc. The single-record + # location represent produces A() (unless told not to), and we + # want to show links if we can. + return XML(DIV(*results)) + else: return UNKNOWN_OPT === modified file 'models/03_gis.py' --- models/03_gis.py 2011-01-27 21:23:19 +0000 +++ models/03_gis.py 2011-01-30 17:20:54 +0000 @@ -24,7 +24,7 @@ # This is needed for onvalidation # @ToDo: & Location Selector (old currently, new in future?) if s3_has_role("MapAdmin"): - _gis.edit_L0 = _gis.edit_L1 = _gis.edit_L2 = _gis.edit_L3 = _gis.edit_L4 = _gis.edit_L5 = True + _gis.edit_L0 = _gis.edit_L1 = _gis.edit_L2 = _gis.edit_L3 = _gis.edit_L4 = _gis.edit_L5 = _gis.edit_GR = True else: _gis.edit_L0 = deployment_settings.get_gis_edit_l0() _gis.edit_L1 = deployment_settings.get_gis_edit_l1() @@ -32,6 +32,7 @@ _gis.edit_L3 = deployment_settings.get_gis_edit_l3() _gis.edit_L4 = deployment_settings.get_gis_edit_l4() _gis.edit_L5 = deployment_settings.get_gis_edit_l5() + _gis.edit_GR = deployment_settings.get_gis_edit_group() # ----------------------------------------------------------------------------- # GIS Markers (Icons) @@ -438,25 +439,28 @@ elif location.level == "L1" and _gis.countries and len(_gis.countries) == 1: # Regions shouldn't show their Parent if the deployment is only for 1 country text = "%s (%s)" % (location.name, level_name) - # @ToDo Why is the parent only shown for the higher levels? - # Isn't it equally useful (or more useful) for small places that - # people are less likely to have heard of, and where the larger - # place they belong to is more likely to be recognized? elif location.level in ["L1", "L2", "L3"]: try: - # Show the Parent location + # Show the Parent location for larger regions. (For smaller, we show + # lon, lat -- parent won't fit.) parent = db(db.gis_location.id == location.parent).select(db.gis_location.name, cache=(cache.ram, 60), limitby=(0, 1)).first() text = "%s (%s, %s)" % (location.name, level_name, parent.name) except: text = "%s (%s)" % (location.name, level_name) - elif location.level: + elif location.level in ["GR", "XX"]: text = "%s (%s)" % (location.name, level_name) else: # Simple #represent = location.name - # Lat/Lon + # Lat/Lon (Need level name for hierarchy locations, else defeats + # the purpose of helping users select hierarchy locations as + # group members. Even when we have a fancy side-by-side multiple + # select widget, we'll still need the level for two reasons: + # First, the widget is going to use it to distinguish hierarchy + # locations. Second, there are different hierarchy locations with + # the same name, e.g. Los Angeles (County) and Los Angeles (City).) lat = location.lat lon = location.lon if lat and lon: @@ -468,9 +472,16 @@ lon_prefix = "E" else: lon_prefix = "W" - text = location.name + " (%s %s %s %s)" % (lat_prefix, lat, lon_prefix, lon) + if location.level: + start = " (%s, " % level_name + else: + start = " (" + text = location.name + "%s%s %s %s %s)" % (start, lat_prefix, lat, lon_prefix, lon) else: - text = location.name + if location.level: + text = "%s (%s)" % (location.name, level_name) + else: + text = location.name # Simple #represent = text # Hyperlink @@ -478,7 +489,7 @@ # Provide either a link (as might be used on a map popup) or plain text # (for a read-only view of a list of locations). if showlink: - represent = A(text, _style="cursor:pointer; cursor:hand", _onclick="s3_viewMap(" + str(id) +");return false") + represent = A(text, _style="cursor:pointer; cursor:hand", _onclick="s3_viewMap(" + str(location.id) +");return false") else: represent = text # ToDo: Convert to popup? (HTML again!) @@ -542,12 +553,13 @@ table.name.label = T("Primary Name") table.name_dummy.label = T("Local Names") table.name_dummy.comment = DIV(_class="tooltip", _title=T("Local Names") + "|" + T("Names can be added in multiple languages")) - table.level.requires = IS_NULL_OR(IS_IN_SET(gis_location_hierarchy)) table.level.represent = lambda level: \ - level and deployment_settings.get_gis_locations_hierarchy(level) or NONE -table.parent.requires = IS_NULL_OR(IS_ONE_OF(db, "gis_location.id", "%(name)s")) -table.parent.represent = lambda id: (id and [db(db.gis_location.id == id).select(db.gis_location.name, limitby=(0, 1)).first().name] or [NONE])[0] + level and deployment_settings.get_gis_all_levels(level) or NONE +table.parent.requires = IS_NULL_OR(IS_ONE_OF(db, "gis_location.id", + shn_gis_location_represent_row, + filterby="level", filter_opts=["L0", "L1", "L2", "L3", "L4", "L5"])) +table.parent.represent = shn_gis_location_represent table.gis_feature_type.requires = IS_IN_SET(gis_feature_type_opts, zero=None) table.gis_feature_type.represent = lambda opt: gis_feature_type_opts.get(opt, UNKNOWN_OPT) # Full WKT validation is done in the onvalidation callback @@ -585,20 +597,22 @@ table.members.requires = IS_NULL_OR(IS_ONE_OF(db, "gis_location.id", shn_gis_location_represent_row, multiple=True)) -# @ToDo: Location represent strings can be long, so would like to show group -# members one per line on read-only views. However, Web2py is treating the -# text as literal, so
is visible. And...it's squeezing out linefeeds, -# so "\n" as a separator gets turned into " ". The element this is in has -# class="w2p_fw" -- perhaps this one element's style can be tweaked. Just -# use a comma-separated list for now. +# Location represent strings can be long, so show group members one per line +# on read-only views. table.members.represent = lambda id: \ id and s3_represent_multiref(db.gis_location, id, - represent=lambda mbr_id: \ - shn_gis_location_represent_row( - mbr_id, showlink=False), - separator=", ") or NONE + represent=lambda mbr_row: \ + shn_gis_location_represent_row(mbr_row), + separator=BR()) or NONE +# FYI, this is how one would show plain text rather than links: +#table.members.represent = lambda id: \ +# id and s3_represent_multiref(db.gis_location, id, +# represent=lambda mbr_row: \ +# shn_gis_location_represent_row( +# mbr_row, showlink=False), +# separator=", ") or NONE table.members.comment = DIV(_class="tooltip", - _title=T("Members") + "|" + T("A location group is a set of administrative regions -- member locations are added to a location group here. Location groups are used to restrict what is shown on the map and in search results to only entities in one of the regions in the group. A location group can be used to define the extent of an affected area, if it does not fall iwithin one administrative region. Location groups can be used in the Regions menu.")) + _title=T("Members") + "|" + T("A location group is a set of locations (often, a set of administrative regions representing a combined area). Member locations are added to a location group here. Location groups may be used to filter what is shown on the map and in search results to only entities covered by locations in the group. A location group can be used to define the extent of an affected area, if it does not fall within one administrative region. Location groups can be used in the Regions menu.")) s3xrc.model.configure(table, listadd=False) #list_fields=["id", "name", "level", "parent", "lat", "lon"]) @@ -686,14 +700,78 @@ On Validation for GIS Locations (before DB I/O) """ + # If you need more info from the old location record, add it here. + # Check if this has already been called and use the existing info. + def get_location_info(): + if "id" in request: + return db(db.gis_location.id == request.id).select( + db.gis_location.level, limitby=(0, 1)).first() + else: + return None + record_error = T("Sorry, only users with the MapAdmin role are allowed to edit these locations") field_error = T("Please select another level") # Shortcuts - level = form.vars.level - parent = form.vars.parent - lat = form.vars.lat - lon = form.vars.lon + level = "level" in form.vars and form.vars.level + parent = "parent" in form.vars and form.vars.parent + lat = "lat" in form.vars and form.vars.lat + lon = "lon" in form.vars and form.vars.lon + members = "members" in form.vars and form.vars.members + + # Allowed changes for location groups: Once it's a group, it stays a + # group, and existing non-group locations can't be converted to groups. + # For a new location, set the level to "GR" if members are present. + # If it's already a group, don't allow clearing the members or altering + # the level. Don't allow adding members to an existing location that's + # not a group. Note: We can't rely on checking form.vars.level to tell + # if an existing location was a group, because it might not be available + # in either form.vars or request.vars -- for an interactive form, that + # field was set to not writable, so it's just plain text in the page. + # Note also that many of the errors "available" here are not accessible + # via the interactive form. Note further that permission to *edit* a + # group does not give one permission to *mess up* a group, so the above + # restrictions apply to MapAdmins too. + if "id" in request.vars: + # Existing location? Check attempts to manipulate group or members. + # Note we cannot rely on merely checking form.vars.level -- it might + # not be present, or this request might be attempting to change the + # level to group. + # Is this a location group? + # Use the breadcrumb set in prep if available to avoid a db read. + if "location_is_group" in response.s3: + location_is_group = response.s3.location_is_group + else: + old_location = get_location_info() + location_is_group = location.level == "GR" + if location_is_group: + if not _gis.edit_GR: + response.error = record_error + return + # Make sure no-one takes away all members. + if "members" in form.vars and not form.vars.members: + form.errors["members"] = T("A location group must have at least one member.") + return + else: + # Don't allow changing non-group to group. + if members: + form.errors["members"] = T("Location cannot be converted into a group.") + return + if level == "GR": + form.errors["level"] = T("Location cannot be converted into a group.") + return + else: + # New location -- if the location has members, and if permitted to + # make a group, set "group" level. Don't allow also setting a parent. + if members: + if _gis.edit_GR: + if "parent" in form.vars and form.vars.parent: + form.errors["parent"] = T("Location group cannot have a parent.") + return + form.vars.level = "GR" + else: + response.error = T("Sorry, only users with the MapAdmin role are allowed to create location groups.") + return # Check Permissions # 'MapAdmin' has all these perms set, no matter what 000_config has @@ -729,9 +807,16 @@ db.gis_location.lon_min, db.gis_location.lat_max, db.gis_location.lon_max, + #db.gis_location.level, limitby=(0, 1), cache=(cache.ram, 3600)).first() + # Don't allow a group as parent (that way lies madness!). + # (Check not needed here -- enforced in requires validator.) + #if _parent and _parent.level == "GR": + # form.errors["parent"] = T("Location group cannot be a parent.") + # return + # Check Parents are in sane order if level and parent and _parent: # Check that parent is of a higher level (http://eden.sahanafoundation.org/ticket/450) === modified file 'modules/s3/s3cfg.py' --- modules/s3/s3cfg.py 2011-01-27 21:23:19 +0000 +++ modules/s3/s3cfg.py 2011-01-30 15:07:38 +0000 @@ -108,19 +108,45 @@ # GIS (Map) Settings - # Location hierarchy, or name of a specific level. Serves as represent - # for level. + # All levels, or name of a specific level. Includes non-hierarchy levels. + # Serves as represent for level. + def get_gis_all_levels(self, level=None): + from gluon.contrib.simplejson.ordered_dict import OrderedDict + all_levels = self.gis.get("all_levels") + if not all_levels: + T = self.T + all_levels = OrderedDict([ + ("L0", T("Country")), + ("L1", T("Province")), + ("L2", T("District")), + ("L3", T("Town")), + ("L4", T("Village")), + ("L5", T("Neighbourhood")), + ("GR", T("Location Group")), + ("XX", T("Imported")), + ]) + if level: + try: + return all_levels[level] + except: + return level + else: + return all_levels + + # Location hierarchy, or name of a specific level. def get_gis_locations_hierarchy(self, level=None): + from gluon.contrib.simplejson.ordered_dict import OrderedDict locations_hierarchy = self.gis.get("locations_hierarchy") if not locations_hierarchy: T = self.T - locations_hierarchy = { - "L0":T("Country"), - "L1":T("Province"), - "L2":T("District"), - "L3":T("Town"), - "L4":T("Village") - } + locations_hierarchy = OrderedDict([ + ("L0", T("Country")), + ("L1", T("Province")), + ("L2", T("District")), + ("L3", T("Town")), + ("L4", T("Village")), + #("L5", T("Neighbourhood")), + ]) if level: try: return locations_hierarchy[level] @@ -168,6 +194,8 @@ return self.gis.get("edit_L4", True) def get_gis_edit_l5(self): return self.gis.get("edit_L5", True) + def get_gis_edit_group(self): + return self.gis.get("edit_GR", False) def get_gis_marker_max_height(self): return self.gis.get("marker_max_height", 35) def get_gis_marker_max_width(self): === modified file 'views/_display.html' --- views/_display.html 2010-10-18 22:27:57 +0000 +++ views/_display.html 2011-01-28 21:29:42 +0000 @@ -22,6 +22,7 @@

{{except:}} {{pass}} +
{{try:}} {{=item}} === modified file 'views/_list.html' --- views/_list.html 2010-10-09 23:51:48 +0000 +++ views/_list.html 2011-01-28 21:29:06 +0000 @@ -13,13 +13,13 @@ {{except:}} {{pass}} {{include "formats.html"}} +
{{try:}} {{=items}} {{except:}} {{pass}}
-

 

{{include "pagenav.html"}} {{include "searchbox.html"}} """ old_revid = tressel@phobos-20110130173109-ripb5xg1ak4gibjl