예제 #1
0
파일: event.py 프로젝트: jeffhurv/eden
    def model(self):

        T = current.T
        db = current.db
        settings = current.deployment_settings

        s3_datetime_format = settings.get_L10n_datetime_format()
        s3_utc_represent = lambda dt: S3DateTime.datetime_represent(dt, utc=True)

        add_component = self.add_component
        configure = self.configure
        crud_strings = current.response.s3.crud_strings
        define_table = self.define_table
        NONE = current.messages["NONE"]

        # ---------------------------------------------------------------------
        # Events
        #
        #   Events are a way of grouping related Incidents
        #
        # ---------------------------------------------------------------------
        tablename = "event_event"
        table = define_table(tablename,
                             Field("name", notnull=True, # Name could be a code
                                   length=64,    # Mayon compatiblity
                                   label=T("Name")),
                             Field("exercise", "boolean",
                                   represent = lambda opt: "√" if opt else NONE,
                                   #comment = DIV(_class="tooltip",
                                   #              _title="%s|%s" % (T("Exercise"),
                                                                   # Should!
                                   #                                T("Exercises mean all screens have a watermark & all notifications have a prefix."))),
                                   label=T("Exercise?")),
                             Field("zero_hour", "datetime",
                                   default = current.request.utcnow,
                                   requires = IS_DATETIME(format=s3_datetime_format),
                                   represent = s3_utc_represent,
                                   comment = DIV(_class="tooltip",
                                                 _title="%s|%s" % (T("Zero Hour"),
                                                                   T("The time at which the Event started."))),
                                   label=T("Zero Hour")),
                             Field("closed", "boolean",
                                   default = False,
                                   represent = s3_yes_no_represent,
                                   label=T("Closed")),
                             s3_comments(),
                             *s3_meta_fields())

        # CRUD strings
        ADD_EVENT = T("New Event")
        crud_strings[tablename] = Storage(
            title_create = ADD_EVENT,
            title_display = T("Event Details"),
            title_list = T("Events"),
            title_update = T("Edit Event"),
            title_search = T("Search Events"),
            subtitle_create = T("Add New Event"),
            label_list_button = T("List Events"),
            label_create_button = ADD_EVENT,
            label_delete_button = T("Delete Event"),
            msg_record_created = T("Event added"),
            msg_record_modified = T("Event updated"),
            msg_record_deleted = T("Event deleted"),
            msg_list_empty = T("No Events currently registered"))

        represent = S3Represent(lookup=tablename)
        event_id = S3ReusableField("event_id", table,
                                   sortby="name",
                                   requires = IS_NULL_OR(
                                                IS_ONE_OF(db, "event_event.id",
                                                          represent,
                                                          filterby="closed",
                                                          filter_opts=[False],
                                                          orderby="event_event.name",
                                                          sort=True)),
                                   represent = represent,
                                   label = T("Event"),
                                   ondelete = "CASCADE",
                                   # Uncomment these to use an Autocomplete & not a Dropdown
                                   #widget = S3AutocompleteWidget()
                                   #comment = DIV(_class="tooltip",
                                   #              _title="%s|%s" % (T("Event"),
                                   #                                T("Enter some characters to bring up a list of possible matches")))
                                   )

        configure(tablename,
                  orderby=~table.zero_hour,
                  list_orderby=~table.zero_hour,
                  update_onaccept=self.event_update_onaccept,
                  deduplicate=self.event_duplicate,
                  list_fields = ["id",
                                 "name",
                                 (T("Location"), "location.name"),
                                 "zero_hour",
                                 "exercise",
                                 "closed",
                                 "comments",
                                 ])

        # Components
        # Incidents
        add_component("event_incident", event_event="event_id")

        # Locations
        add_component("gis_location",
                      event_event=Storage(link="event_event_location",
                                          joinby="event_id",
                                          key="location_id",
                                          actuate="hide"))

        # Requests
        add_component("req_req", event_event="event_id")

        # Tags
        add_component("event_event_tag",
                      event_event=dict(joinby="event_id",
                                       name="tag"))

        # ---------------------------------------------------------------------
        # Event Locations (link table)
        #
        tablename = "event_event_location"
        table = define_table(tablename,
                             event_id(),
                             self.gis_location_id(
                                widget = S3LocationAutocompleteWidget(),
                                requires = IS_LOCATION(),
                                represent = self.gis_location_lx_represent,
                                comment = S3AddResourceLink(c="gis",
                                                            f="location",
                                                            label = T("Add Location"),
                                                            title=T("Location"),
                                                            tooltip=T("Enter some characters to bring up a list of possible matches")),
                                ),
                             *s3_meta_fields())

        # ---------------------------------------------------------------------
        # Event Tags
        # - Key-Value extensions
        # - can be used to identify a Source
        # - can be used to add extra attributes (e.g. Area, Population)
        # - can link Events to other Systems, such as:
        #   * GLIDE (http://glidenumber.net/glide/public/about.jsp)
        #   * Mayon
        #   * WebEOC
        # - can be a Triple Store for Semantic Web support
        #
        tablename = "event_event_tag"
        table = define_table(tablename,
                             event_id(),
                             # key is a reserved word in MySQL
                             Field("tag", label=T("Key")),
                             Field("value", label=T("Value")),
                             s3_comments(),
                             *s3_meta_fields())

        configure(tablename,
                  deduplicate=self.event_event_tag_deduplicate)

        # ---------------------------------------------------------------------
        # Incidents
        #
        #  Incidents are the unit at which responses are managed.
        #  They can be Exercises or real Incidents.
        #  They can be instantiated from Scenario Templates.
        #
        tablename = "event_incident"
        table = define_table(tablename,
                             event_id(),
                             self.event_incident_type_id(),
                             self.scenario_scenario_id(),
                             Field("name", notnull=True, # Name could be a code
                                   length=64,
                                   label=T("Name")),
                             Field("exercise", "boolean",
                                   represent = lambda opt: "√" if opt else NONE,
                                   #comment = DIV(_class="tooltip",
                                   #              _title="%s|%s" % (T("Exercise"),
                                                                   # Should!
                                   #                                T("Exercises mean all screens have a watermark & all notifications have a prefix."))),
                                   label=T("Exercise?")),
                             Field("zero_hour", "datetime",
                                   default = current.request.utcnow,
                                   requires = IS_DATETIME(format=s3_datetime_format),
                                   represent = s3_utc_represent,
                                   comment = DIV(_class="tooltip",
                                                 _title="%s|%s" % (T("Zero Hour"),
                                                                   T("The time at which the Event started."))),
                                   label=T("Zero Hour")),
                             Field("closed", "boolean",
                                   default = False,
                                   represent = s3_yes_no_represent,
                                   label=T("Closed")),
                             s3_comments(),
                             *s3_meta_fields())

        crud_strings[tablename] = Storage(
            title_create = T("Add Incident"),
            title_display = T("Incident Details"),
            title_list = T("Incidents"),
            title_update = T("Edit Incident"),
            title_search = T("Search Incidents"),
            subtitle_create = T("Add New Incident"),
            label_list_button = T("List Incidents"),
            label_create_button = T("Add Incident"),
            label_delete_button = T("Remove Incident from this event"),
            msg_record_created = T("Incident added"),
            msg_record_modified = T("Incident updated"),
            msg_record_deleted = T("Incident removed"),
            msg_list_empty = T("No Incidents currently registered in this event"))

        represent = S3Represent(lookup=tablename)
        incident_id = S3ReusableField("incident_id", table,
                                      sortby="name",
                                      requires = IS_NULL_OR(
                                                    IS_ONE_OF(db, "event_incident.id",
                                                              represent,
                                                              filterby="closed",
                                                              filter_opts=[False],
                                                              orderby="event_incident.name",
                                                              sort=True)),
                                      represent = represent,
                                      label = T("Incident"),
                                      ondelete = "RESTRICT",
                                      # Uncomment these to use an Autocomplete & not a Dropdown
                                      #widget = S3AutocompleteWidget()
                                      #comment = DIV(_class="tooltip",
                                      #              _title="%s|%s" % (T("Incident"),
                                      #                                T("Enter some characters to bring up a list of possible matches")))
                                      )

        if settings.has_module("project"):
            create_next_url = URL(args=["[id]", "task"])
        elif settings.has_module("hrm"):
            create_next_url = URL(args=["[id]", "human_resource"])
        elif settings.has_module("asset"):
            create_next_url = URL(args=["[id]", "asset"])
        else:
            create_next_url = URL(args=["[id]", "site"])

        configure(tablename,
                  create_next = create_next_url,
                  create_onaccept=self.incident_create_onaccept,
                  deduplicate=self.incident_duplicate,
                  list_fields = ["id",
                                 "name",
                                 "incident_type_id",
                                 "exercise",
                                 "closed",
                                 "comments",
                                 ])

        # Components
        # Tasks
        add_component("project_task",
                      event_incident=Storage(link="event_task",
                                             joinby="incident_id",
                                             key="task_id",
                                             # @ToDo: Widget to handle embedded LocationSelector
                                             #actuate="embed",
                                             actuate="link",
                                             autocomplete="name",
                                             autodelete=False))

        # Human Resources
        add_component("event_human_resource", event_event="event_id")
        add_component("hrm_human_resource",
                      event_incident=Storage(link="event_human_resource",
                                             joinby="incident_id",
                                             key="human_resource_id",
                                             # @ToDo: Widget to handle embedded AddPersonWidget
                                             #actuate="embed",
                                             actuate="link",
                                             autocomplete="name",
                                             autodelete=False))

        # Assets
        add_component("asset_asset",
                      event_incident=Storage(link="event_asset",
                                             joinby="incident_id",
                                             key="asset_id",
                                             actuate="embed",
                                             autocomplete="name",
                                             autodelete=False))

        # Facilities
        add_component("event_site", event_incident="incident_id")

        # Map Config
        add_component("gis_config",
                      event_incident=Storage(link="event_config",
                                             joinby="incident_id",
                                             multiple=False,
                                             key="config_id",
                                             actuate="replace",
                                             autocomplete="name",
                                             autodelete=True))

        # ---------------------------------------------------------------------
        # Pass names back to global scope (s3.*)
        #
        return Storage(
                event_event_id = event_id,
                event_incident_id = incident_id,
            )
예제 #2
0
    def model(self):

        T = current.T
        db = current.db
        auth = current.auth

        person_id = self.pr_person_id
        location_id = self.gis_location_id
        organisation_id = self.org_organisation_id

        UNKNOWN_OPT = current.messages.UNKNOWN_OPT

        vehicle = current.deployment_settings.has_module("vehicle")

        # Shortcuts
        add_component = self.add_component
        configure = self.configure
        crud_strings = current.response.s3.crud_strings
        define_table = self.define_table
        super_link = self.super_link

        #--------------------------------------------------------------------------
        # Assets
        #

        asset_type_opts = {
            ASSET_TYPE_VEHICLE: T("Vehicle"),
            #ASSET_TYPE_RADIO      : T("Radio"),
            #ASSET_TYPE_TELEPHONE  : T("Telephone"),
            ASSET_TYPE_OTHER: T("Other")
        }

        asset_item_represent = lambda id: self.supply_item_represent(
            id, show_um=False)

        ctable = self.supply_item_category
        itable = self.supply_item

        tablename = "asset_asset"
        table = define_table(tablename,
                             super_link("track_id", "sit_trackable"),
                             super_link("doc_id", "doc_entity"),
                             Field("number",
                                   label = T("Asset Number")),
                             self.supply_item_entity_id,
                             # @ToDo: Can we set this automatically based on Item Category?
                             Field("type", "integer",
                                   readable = vehicle,
                                   writable = vehicle,
                                   requires = IS_IN_SET(asset_type_opts),
                                   default = ASSET_TYPE_OTHER,
                                   represent = lambda opt: \
                                       asset_type_opts.get(opt, UNKNOWN_OPT),
                                   label = T("Type")),
                             self.supply_item_id(represent = asset_item_represent,
                                                 requires = IS_ONE_OF(db((ctable.can_be_asset == True) & \
                                                                         (itable.item_category_id == ctable.id)),
                                                                      "supply_item.id",
                                                                      asset_item_represent,
                                                                      sort=True,
                                                                      ),
                                                 script = None, # No Item Pack Filter
                                                 ),
                             # This is a component, so needs to be a super_link
                             # - can't override field name, ondelete or requires
                             super_link("site_id", "org_site",
                                        label = T("Office/Warehouse/Facility"),
                                        default = auth.user.site_id if auth.is_logged_in() else None,
                                        readable = True,
                                        writable = True,
                                        empty = False,
                                        ondelete = "RESTRICT",
                                        # Comment these to use a Dropdown & not an Autocomplete
                                        #widget = S3SiteAutocompleteWidget(),
                                        #comment = DIV(_class="tooltip",
                                        #              _title="%s|%s" % (T("Warehouse"),
                                        #                                T("Enter some characters to bring up a list of possible matches"))),
                                        represent = self.org_site_represent
                                        ),
                             organisation_id(),
                             Field("sn",
                                   label = T("Serial Number")),
                             # @ToDo: Switch to using org_organisation filtered to suppliers
                             #supplier_id(),
                             Field("supplier",
                                   label = T("Supplier")),
                             s3_date("purchase_date",
                                     label = T("Purchase Date")
                                     ),
                             Field("purchase_price", "double",
                                   #default=0.00,
                                   represent=lambda v, row=None: IS_FLOAT_AMOUNT.represent(v, precision=2)),
                             s3_currency("purchase_currency"),
                             # Base Location, which should always be a Site & set via Log
                             location_id(readable=False,
                                         writable=False),
                             # Populated onaccept of the log to make a component tab
                             person_id("assigned_to_id",
                                       readable=False,
                                       writable=False,
                                       comment=self.pr_person_comment(child="assigned_to_id")),
                             s3_comments(),
                             *(s3_address_fields() + s3_meta_fields()))

        # CRUD strings
        ADD_ASSET = T("Add Asset")
        crud_strings[tablename] = Storage(
            title_create=ADD_ASSET,
            title_display=T("Asset Details"),
            title_list=T("Assets"),
            title_update=T("Edit Asset"),
            title_search=T("Search Assets"),
            title_upload=T("Import Assets"),
            subtitle_create=T("Add New Asset"),
            label_list_button=T("List Assets"),
            label_create_button=ADD_ASSET,
            label_delete_button=T("Delete Asset"),
            msg_record_created=T("Asset added"),
            msg_record_modified=T("Asset updated"),
            msg_record_deleted=T("Asset deleted"),
            msg_list_empty=T("No Assets currently registered"))

        # Reusable Field
        asset_id = S3ReusableField(
            "asset_id",
            table,
            sortby="number",
            requires=IS_NULL_OR(
                IS_ONE_OF(db,
                          "asset_asset.id",
                          self.asset_represent,
                          sort=True)),
            represent=self.asset_represent,
            label=T("Asset"),
            comment=S3AddResourceLink(
                c="asset",
                f="asset",
                tooltip=
                T("If you don't see the asset in the list, you can add a new one by clicking link 'Add Asset'."
                  )),
            ondelete="CASCADE")

        table.virtualfields.append(AssetVirtualFields())

        # Search Method
        asset_search = S3Search(
            # Advanced Search only
            advanced=(
                S3SearchSimpleWidget(
                    name="asset_search_text",
                    label=T("Search"),
                    comment=
                    T("You can search by asset number, item description or comments. You may use % as wildcard. Press 'Search' without input to list all assets."
                      ),
                    field=[
                        "number",
                        "item_id$name",
                        #"item_id$category_id$name",
                        "comments",
                    ]),
                S3SearchOptionsWidget(name="asset_search_L1",
                                      field="L1",
                                      location_level="L1",
                                      cols=3),
                S3SearchOptionsWidget(name="asset_search_L2",
                                      field="L2",
                                      location_level="L2",
                                      cols=3),
                S3SearchLocationWidget(
                    name="asset_search_map",
                    label=T("Map"),
                ),
                S3SearchOptionsWidget(name="asset_search_item_category",
                                      field="item_id$item_category_id",
                                      label=T("Category"),
                                      cols=3),
            ))

        report_fields = [
            "number",
            (T("Category"), "item_id$item_category_id"),
            (T("Item"), "item_id"),
            (T("Office/Warehouse/Facility"), "site"),
            "L1",
            "L2",
        ]

        # Resource Configuration
        configure(
            tablename,
            super_entity=("supply_item_entity", "sit_trackable"),
            create_next=URL(c="asset", f="asset", args=["[id]"]),
            onaccept=self.asset_onaccept,
            search_method=asset_search,
            report_options=Storage(search=[
                S3SearchOptionsWidget(name="asset_search_L1",
                                      field="L1",
                                      location_level="L1",
                                      cols=3),
                S3SearchOptionsWidget(name="asset_search_L2",
                                      field="L2",
                                      location_level="L2",
                                      cols=3),
                S3SearchOptionsWidget(name="asset_search_item_category",
                                      field="item_id$item_category_id",
                                      label=T("Category"),
                                      cols=3),
            ],
                                   rows=report_fields,
                                   cols=report_fields,
                                   facts=report_fields,
                                   methods=["count", "list"],
                                   defaults=Storage(
                                       aggregate="count",
                                       cols="L1",
                                       fact="number",
                                       rows="item_id$item_category_id")),
            list_fields=[
                "id",
                "number",
                "item_id$item_category_id",
                "item_id",
                "type",
                "purchase_date",
                "organisation_id",
                "site_id",
                #"location_id",
                "L0",
                "L1",
                #"L2",
                #"L3",
                "comments",
            ])

        # Log as component of Assets
        add_component("asset_log", asset_asset="asset_id")

        # Vehicles as component of Assets
        add_component("vehicle_vehicle",
                      asset_asset=dict(joinby="asset_id", multiple=False))

        # GPS as a component of Assets
        add_component("vehicle_gps", asset_asset="asset_id")

        # =====================================================================
        # Asset Log
        #

        asset_log_status_opts = {
            ASSET_LOG_SET_BASE: T("Base Office/Warehouse/Facility Set"),
            ASSET_LOG_ASSIGN: T("Assigned"),
            ASSET_LOG_RETURN: T("Returned"),
            ASSET_LOG_CHECK: T("Checked"),
            ASSET_LOG_REPAIR: T("Repaired"),
            ASSET_LOG_DONATED: T("Donated"),
            ASSET_LOG_LOST: T("Lost"),
            ASSET_LOG_STOLEN: T("Stolen"),
            ASSET_LOG_DESTROY: T("Destroyed"),
        }
        #site_or_location_opts = {SITE     :T("Site"),
        #                         LOCATION :T("Location")}

        asset_condition_opts = {
            1: T("Good Condition"),
            2: T("Minor Damage"),
            3: T("Major Damage"),
            4: T("Un-Repairable"),
            5: T("Needs Maintenance"),
        }

        tablename = "asset_log"
        table = define_table(tablename,
                             asset_id(),
                             Field("status", "integer",
                                   label = T("Status"),
                                   requires = IS_IN_SET(asset_log_status_opts),
                                   represent = lambda opt: \
                                       asset_log_status_opts.get(opt, UNKNOWN_OPT)
                                   ),
                             s3_datetime("datetime",
                                         default="now",
                                         empty=False,
                                         represent="date",
                                         ),
                             s3_datetime("datetime_until",
                                         label = T("Date Until"),
                                         represent="date",
                                         ),
                             person_id(label = T("Assigned To")),
                             Field("check_in_to_person", "boolean",
                                   #label = T("Mobile"),      # Relabel?
                                   label = T("Track with this Person?"),
                                   comment = DIV(_class="tooltip",
                                                 #_title="%s|%s" % (T("Mobile"),
                                                 _title="%s|%s" % (T("Track with this Person?"),
                                                                   T("If selected, then this Asset's Location will be updated whenever the Person's Location is updated."))),
                                   readable = False,
                                   writable = False),
                             # The Organisation to whom the loan is made
                             organisation_id(
                                    readable = False,
                                    writable = False,
                                    widget = None
                                    ),
                             #Field("site_or_location",
                             #      "integer",
                             #      requires = IS_NULL_OR(IS_IN_SET(site_or_location_opts)),
                             #      represent = lambda opt: \
                             #          site_or_location_opts.get(opt, UNKNOWN_OPT),
                             #      widget = RadioWidget.widget,
                             #      label = T("Facility or Location")),
                             # This is a component, so needs to be a super_link
                             # - can't override field name, ondelete or requires
                             super_link("site_id", "org_site",
                                        label = T("Warehouse/Facility/Office"),
                                        #filterby = "site_id",
                                        #filter_opts = auth.permitted_facilities(redirect_on_error=False),
                                        instance_types = auth.org_site_types,
                                        updateable = True,
                                        not_filterby = "obsolete",
                                        not_filter_opts = [True],
                                        #default = user.site_id if is_logged_in() else None,
                                        readable = True,
                                        writable = True,
                                        empty = False,
                                        represent = self.org_site_represent,
                                        #widget = S3SiteAutocompleteWidget(),
                                        comment = SCRIPT(
                                            '''$(document).ready(function(){
 S3FilterFieldChange({
  'FilterField':'organisation_id',
  'Field':'site_id',
  'FieldPrefix':'org',
  'FieldResource':'site',
  'FieldID':'site_id',
  'fncRepresent': function(record, PrepResult) {
                      var InstanceTypeNice = %(instance_type_nice)s;
                      return record.name + " (" + InstanceTypeNice[record.instance_type] + ")";
                  }
 })
})''' % dict(instance_type_nice = auth.org_site_types)),
                                              ),
                             self.org_room_id(),
                             #location_id(),
                             Field("cancel", "boolean",
                                   default = False,
                                   label = T("Cancel Log Entry"),
                                   comment = DIV(_class="tooltip",
                                                 _title="%s|%s" % (T("Cancel Log Entry"),
                                                                   T("'Cancel' will indicate an asset log entry did not occur")))
                                   ),
                             Field("cond", "integer",  # condition is a MySQL reserved word
                                   requires = IS_IN_SET(asset_condition_opts,
                                                        zero = "%s..." % T("Please select")),
                                   represent = lambda opt: \
                                       asset_condition_opts.get(opt, UNKNOWN_OPT),
                                   label = T("Condition")),
                             person_id("by_person_id",
                                       label = T("Assigned By"),               # This can either be the Asset controller if signed-out from the store
                                       default = auth.s3_logged_in_person(),   # or the previous owner if passed on directly (e.g. to successor in their post)
                                       comment = self.pr_person_comment(child="by_person_id"),
                                       ),
                             s3_comments(),
                             *s3_meta_fields())

        # CRUD strings
        ADD_ASSIGN = T("New Entry in Asset Log")
        crud_strings[tablename] = Storage(
            title_create=ADD_ASSIGN,
            title_display=T("Asset Log Details"),
            title_list=T("Asset Log"),
            title_update=T("Edit Asset Log Entry"),
            title_search=T("Search Asset Log"),
            subtitle_create=ADD_ASSIGN,
            label_list_button=T("Asset Log"),
            label_create_button=ADD_ASSIGN,
            label_delete_button=T("Delete Asset Log Entry"),
            msg_record_created=T("Entry added to Asset Log"),
            msg_record_modified=T("Asset Log Entry updated"),
            msg_record_deleted=T("Asset Log Entry deleted"),
            msg_list_empty=T("Asset Log Empty"))

        # Resource configuration
        configure(
            tablename,
            onvalidation=self.asset_log_onvalidation,
            onaccept=self.asset_log_onaccept,
            listadd=False,
            list_fields=[
                "id",
                "status",
                "datetime",
                "datetime_until",
                "organisation_id",
                "site_id",
                "room_id",
                #"location_id",
                "cancel",
                "cond",
                "comments"
            ])

        # ---------------------------------------------------------------------
        # Pass variables back to global scope (s3db.*)
        #
        return Storage(
            asset_asset_id=asset_id,
            asset_rheader=asset_rheader,
            asset_represent=self.asset_represent,
            asset_log_prep=self.asset_log_prep,
        )
예제 #3
0
파일: cms.py 프로젝트: wyw876/eden
    def model(self):

        T = current.T
        db = current.db
        add_component = self.add_component
        configure = self.configure
        crud_strings = current.response.s3.crud_strings
        define_table = self.define_table

        # ---------------------------------------------------------------------
        # Series
        # - lists of Posts displaying in recent-first mode
        #

        tablename = "cms_series"
        table = define_table(
            tablename,
            Field("name", notnull=True, label=T("Name")),
            Field("avatar",
                  "boolean",
                  default=False,
                  represent=s3_yes_no_represent,
                  label=T("Show author picture?")),
            Field("replies",
                  "boolean",
                  default=False,
                  represent=s3_yes_no_represent,
                  label=T("Comments permitted?")),
            s3_comments(),
            # Multiple Roles (@ToDo: Implement the restriction)
            s3_roles_permitted(readable=False, writable=False),
            *s3_meta_fields())

        # CRUD Strings
        ADD_SERIES = T("Add Series")
        crud_strings[tablename] = Storage(
            title_create=ADD_SERIES,
            title_display=T("Series Details"),
            title_list=T("Series"),
            title_update=T("Edit Series"),
            title_search=T("Search Series"),
            title_upload=T("Import Series"),
            subtitle_create=T("Add New Series"),
            label_list_button=T("List Series"),
            label_create_button=ADD_SERIES,
            msg_record_created=T("Series added"),
            msg_record_modified=T("Series updated"),
            msg_record_deleted=T("Series deleted"),
            msg_list_empty=T("No series currently defined"))

        # Reusable field
        series_id = S3ReusableField("series_id",
                                    table,
                                    readable=False,
                                    writable=False,
                                    requires=IS_NULL_OR(
                                        IS_ONE_OF(db, "cms_series.id",
                                                  "%(name)s")),
                                    ondelete="CASCADE")

        # Resource Configuration
        configure(tablename,
                  onaccept=self.series_onaccept,
                  create_next=URL(f="series", args=["[id]", "post"]))

        # Components
        add_component("cms_post", cms_series="series_id")

        # ---------------------------------------------------------------------
        # Posts
        # - single blocks of rich text which can be embedded into a page,
        #   be viewed as full pages or as part of a Series
        #

        modules = {}
        _modules = current.deployment_settings.modules
        for module in _modules:
            if module in ["appadmin", "errors", "sync"]:
                continue
            modules[module] = _modules[module].name_nice

        tablename = "cms_post"
        table = define_table(tablename,
                             series_id(),
                             Field("module",
                                   requires=IS_NULL_OR(
                                                IS_IN_SET_LAZY(lambda: \
                                            sort_dict_by_values(modules))),
                                   comment=T("If you specify a module then this will be used as the text in that module's index page"),
                                   label=T("Module")),
                             Field("name", notnull=True,
                                   comment=T("This isn't visible to the published site, but is used to allow menu items to point to the page"),
                                   label=T("Name")),
                             Field("title",
                                   comment=T("The title of the page, as seen in the browser (optional)"),
                                   label=T("Title")),
                             Field("body", "text", notnull=True,
                                   widget = s3_richtext_widget,
                                   label=T("Body")),
                             Field("avatar", "boolean",
                                   default=False,
                                   represent = s3_yes_no_represent,
                                   label=T("Show author picture?")),
                             Field("replies", "boolean",
                                   default=False,
                                   represent = s3_yes_no_represent,
                                   label=T("Comments permitted?")),
                             #Field("published", "boolean",
                             #      default=True,
                             #      label=T("Published")),
                             s3_comments(),
                             # Multiple Roles (@ToDo: Implement the restriction)
                             s3_roles_permitted(
                                                readable = False,
                                                writable = False
                                                ),
                             *s3_meta_fields())

        # CRUD Strings
        ADD_POST = T("Add Post")
        crud_strings[tablename] = Storage(
            title_create=ADD_POST,
            title_display=T("Post Details"),
            title_list=T("Posts"),
            title_update=T("Edit Post"),
            title_search=T("Search Posts"),
            title_upload=T("Import Posts"),
            subtitle_create=T("Add New Post"),
            label_list_button=T("List Posts"),
            label_create_button=ADD_POST,
            msg_record_created=T("Post added"),
            msg_record_modified=T("Post updated"),
            msg_record_deleted=T("Post deleted"),
            msg_list_empty=T("No posts currently defined"))

        # Reusable field
        post_id = S3ReusableField("post_id", table,
                                  label = T("Post"),
                                  sortby="name",
                                  requires = IS_NULL_OR(
                                                IS_ONE_OF(db, "cms_post.id",
                                                          "%(name)s")),
                                  represent = lambda id, row=None: \
                                                (id and [db.cms_post[id].name] or [NONE])[0],
                                  comment = S3AddResourceLink(c="cms",
                                                              f="post",
                                                              title=ADD_POST,
                                                              tooltip=T("A block of rich text which could be embedded into a page, viewed as a complete page or viewed as a list of news items.")),
                                  ondelete = "CASCADE")

        # Resource Configuration
        configure(tablename, onaccept=self.post_onaccept)

        # Components
        add_component("cms_comment", cms_post="post_id")

        # ---------------------------------------------------------------------
        # Tags
        #
        tablename = "cms_tag"
        table = define_table(
            tablename,
            Field("name", label=T("Tag")),
            s3_comments(),
            # Multiple Roles (@ToDo: Implement the restriction)
            #s3_roles_permitted(
            #                   readable = False,
            #                   writable = False
            #                   ),
            *s3_meta_fields())

        # CRUD Strings
        ADD_TAG = T("Add Tag")
        crud_strings[tablename] = Storage(
            title_create=ADD_TAG,
            title_display=T("Tag Details"),
            title_list=T("Tags"),
            title_update=T("Edit Tag"),
            title_search=T("Search Tags"),
            title_upload=T("Import Tags"),
            subtitle_create=T("Add New Tag"),
            label_list_button=T("List Tags"),
            label_create_button=ADD_TAG,
            msg_record_created=T("Tag added"),
            msg_record_modified=T("Tag updated"),
            msg_record_deleted=T("Tag deleted"),
            msg_list_empty=T("No tags currently defined"))

        # ---------------------------------------------------------------------
        # Tags <> Posts link table
        #
        tablename = "cms_tag"
        table = define_table(tablename, post_id(),
                             Field("tag_id", "reference cms_tag"),
                             *s3_meta_fields())

        # CRUD Strings
        ADD_TAG = T("Tag Post")
        crud_strings[tablename] = Storage(
            title_create=ADD_TAG,
            title_display=T("Tag Details"),
            title_list=T("Tags"),
            title_update=T("Edit Tag"),
            title_search=T("Search Tags"),
            title_upload=T("Import Tags"),
            subtitle_create=T("Add New Tag"),
            label_list_button=T("List Tagged Posts"),
            label_create_button=ADD_TAG,
            msg_record_created=T("Post Tagged"),
            msg_record_modified=T("Tag updated"),
            msg_record_deleted=T("Tag removed"),
            msg_list_empty=T("No posts currently tagged"))

        # ---------------------------------------------------------------------
        # Comments
        # - threaded comments on Posts
        #
        # @ToDo: Attachments?
        #
        # Parent field allows us to:
        #  * easily filter for top-level threads
        #  * easily filter for next level of threading
        #  * hook a new reply into the correct location in the hierarchy
        #
        tablename = "cms_comment"
        table = define_table(
            tablename,
            Field("parent",
                  "reference cms_comment",
                  requires=IS_NULL_OR(IS_ONE_OF(db, "cms_comment.id")),
                  readable=False), post_id(),
            Field("body", "text", notnull=True, label=T("Comment")),
            *s3_meta_fields())

        # Resource Configuration
        configure(tablename,
                  list_fields=["id", "post_id", "created_by", "modified_on"])

        # ---------------------------------------------------------------------
        # Pass variables back to global scope (s3db.*)
        #
        return Storage()