.. meta:: :description: Reference for the internals of Roundup. Includes background information for cookbook and how-to examples. Reference for the design and internals needed to understand and extend the examples to meet new needs. :tocdepth: 2 ================= Roundup Reference ================= .. admonition:: Welcome This document was part of the `customisation document`_. The customisation document was getting large and unwieldy. The combination of examples and internal information that made finding information difficult. Questions raised on the mailing list were well answered in the customisation document, but finding the info was difficult. The documentation is slowly being reorganized using the `Diataxis framework`_. Help with the reorganization is welcome. This document provides background for the tutorials or how-tos in the customisation document. .. _customisation document: customizing.html .. _diataxis framework: https://diataxis.fr/ .. This document borrows from the ZopeBook section on ZPT. The original was at: http://www.zope.org/Documentation/Books/ZopeBook/current/ZPT.stx .. contents:: :depth: 2 :local: Trackers in a Nutshell ====================== Trackers have the following structure: .. index:: single: tracker; structure db directory single: tracker; structure detectors directory single: tracker; structure extensions directory single: tracker; structure html directory single: tracker; structure html directory single: tracker; structure lib directory .. table:: :class: valign-top =================== ======================================================== Tracker File Description =================== ======================================================== config.ini Holds the basic `tracker configuration`_ schema.py Holds the `tracker schema`_ initial_data.py Loads initial data into the tracker (status, priority ...) when initializing the tracker (optional) interfaces.py Allows `modifying the core of Roundup`_ (optional) db/ Holds the tracker's database db/files/ Holds the tracker's uploaded files and message db/backend_name Names the database back-end for the tracker (obsolete). Use the ``backend`` setting in the ``[rdbms]`` section of ``config.ini`` instead. detectors/ `Auditors and reactors`_ for this tracker extensions/ Additional `actions`_ and `templating utilities`_ html/ Web interface templates, images and style sheets lib/ optional common imports for detectors and extensions =================== ======================================================== .. index:: config.ini .. index:: configuration; see config.ini Tracker Configuration ===================== The ``config.ini`` located in your tracker home contains the basic configuration for the web and e-mail components of Roundup's interfaces. The `tracker schema`_ defines the data captured by your tracker. It also defines the permissions used when accessing the data: see the `security / access controls`_ section. For example, you must grant the "Anonymous" Role the "Email Access" Permission to allow users to automatically register through the email interface,. .. index:: single: config.ini; sections see: configuration; config.ini The following is taken from the `Python Library Reference`__ (July 18, 2018) section "ConfigParser -- Configuration file parser": The configuration file consists of sections, led by a [section] header and followed by name: value entries, with continuations in the style of RFC 822 (see section 3.1.1, “LONG HEADER FIELDS”); name=value is also accepted. Note that leading whitespace is removed from values. The optional values can contain format strings which refer to other values in the same section, or values in a special DEFAULT section. Additional defaults can be provided on initialization and retrieval. Lines beginning with '#' or ';' are ignored and may be used to provide comments. For example:: [My Section] foodir = %(dir)s/whatever dir = frob would resolve the "%(dir)s" to the value of "dir" ("frob" in this case) resulting in "foodir" being "frob/whatever". The reference above discusses using the ``[DEFAULT]`` section and interpolation. For example:: [DEFAULT] local_admin_email = admin@example.com [main] admin_email = %(local_admin_email)s will set the admin_email setting. This works when running the tracker. When upgrading Roundup using ``updateconfig`` to create a new ``config.ini``, the ``DEFAULT`` section is not preserved and interpolation tokens (e.g. ``%(local_admin_email)s`` are replaced with their values (``admin@example.com``). This may be fixed in a future release of Roundup. Note that you can not reference settings in the ``DEFAULT`` section from Roundup. They are only useful when interpolated into a defined setting. __ https://docs.python.org/2/library/configparser.html A default ``config.ini`` file broken into sections is shown below. .. include:: tracker_config.txt Additional notes: The ``[rdbms]`` service defines the Connection Service for your PostgreSQL connection when using a system-wide pg_service.conf or ~/.pg_service.conf as discussed in https://www.postgresql.org/docs/current/libpq-pgservice.html. Setting this to the name of the service allows different trackers to connect to different services when running multiple trackers under one Roundup server. If you are only running one tracker, you can set the PGSERVICE environment variable. Note that other settings specified in this file (rdbms: user, password, port, host, (db)name) will override the corresponding connection service setting. .. index:: single: roundup-admin; config.ini update single: roundup-admin; config.ini create single: config.ini; create single: config.ini; update You may generate a new default config file using the ``roundup-admin genconfig`` command. You can generate a new config file merging in existing settings using the ``roundup-admin updateconfig`` command. Configuration variables may be referred to in lower or upper case. In code, refer to variables not in the "main" section using their section and name, so "domain" in the section "mail" becomes MAIL_DOMAIN. .. index:: pair: configuration; extensions pair: configuration; detectors Extending the configuration file -------------------------------- You can't add new variables to the config.ini file in the tracker home but you can add two new config.ini files: - a config.ini in the ``extensions`` directory is loaded and attached to the config variable as "ext". - a config.ini in the ``detectors`` directory is loaded and attached to the config variable as "detectors". These configuration files support the same operations as the main ``config.ini`` file. This included a ``DEFAULT`` section and interpolation. Note that you can not reference settings in the ``DEFAULT`` section from Roundup. They can only be used for interpolation. For example, the following in ``detectors/config.ini``:: [main] qa_recipients = email@example.com is accessible as:: db.config.detectors['QA_RECIPIENTS'] Note that the name grouping applied to the main configuration file is applied to the extension config files, so if you instead have:: [qa] recipients = email@example.com then the above ``db.config.detectors['QA_RECIPIENTS']`` will still work. Unlike values in the tracker's main ``config.ini``, the values defined in these config files are not validated. For example: a setting that is supposed to be an integer value (e.g. 4) could be the word "foo". If you are writing Python code that uses these settings, you should expect to handle invalid values. Also, incorrect values are discovered when the config setting is used not set. This can be long after the tracker is started and the error may not be seen in the logs. It's possible to validate these settings. Validation involves calling the ``update_options`` method on the configuration option. This can be done by the ``init()`` function in the Python files implementing extensions_ or detectors_. As an example, adding the following to an extension:: from roundup.configuration import SecretMandatoryOption def init(instance): instance.config.ext.update_option('RECAPTCHA_SECRET', SecretMandatoryOption,description="Secret securing reCaptcha.") similarly for a detector:: from roundup.configuration import MailAddressOption def init(db): try: db.config.detectors.update_option('QA_RECIPIENTS', MailAddressOption, description="Email used for QA comment followup.") except KeyError: # COMMENT_EMAIL setting is not found, but it's optional # so continue pass will allow reading the secret from a file or append the tracker domain to an email address if it does not have a domain. Running ``roundup-admin -i tracker_home display user1`` will validate the settings for both config.ini`s. Otherwise detector options are not validated until the first request to the web interface (or email gateway). ``update_option`` takes 4 arguments: 1. config setting name - string (positional, mandatory) 2. option type - Option derived class from configuration.py (positional, mandatory) 3. default value - string (optional, named default) 4. description - string (optional, named description) The first argument is the config setting name as described at the beginning of this section. The second argument is a class in the roundup.configuration module. There are many of these classes: BooleanOption, IntegerNumberOption, RegExpOption.... Please see the configuration module for all Option validators and their descriptions. You can also define your own custom validator in `interfaces.py`_. The third and fourth arguments are optional strings. They are printed when there is an error and may help the user correct the problem. .. index:: ! schema Tracker Schema ============== .. note:: if you modify the schema, you'll most likely need to edit the `web interface`_ HTML template files and `detectors`_ to reflect your changes. A tracker schema defines what data is stored in the tracker's database. Schemas are defined using Python code in the ``schema.py`` module of your tracker. .. index:: schema; allowed changes What you can/can't do to the schema ----------------------------------- Your schema may be changed at any time before or after the tracker has been initialised (or used). You may: **Add new properties to classes, or add whole new classes** This is painless and easy to do - there are generally no repercussions from adding new information to a tracker's schema. **Remove properties** Removing properties is a little more tricky - you need to make sure that the property is no longer used in the `web interface`_ *or* by the detectors_. You must never: **Remove the user class** This class is the only *required* class in Roundup. **Remove the "username", "address", "password", "roles" or "realname" user properties** Various parts of Roundup require these properties. Don't remove them. **Change the type of a property** Property types must *never* [1]_ be changed - the database simply doesn't take this kind of action into account. Note that you can't just remove a property and re-add it as a new type either. If you wanted to make the assignedto property a Multilink, you'd need to create a new property assignedto_list and remove the old assignedto property. .. [1] If you shut down the tracker, `export the database`_, modify the exported csv property data to be compatible with the new type, change the property type in the schema, and finally import the changed exported data, you can change the property type. This is not trivial nor for the faint of heart. But it can be done. .. _export the database: admin_guide.html#using-roundup-admin The ``schema.py`` and ``initial_data.py`` modules ------------------------------------------------- The schema.py module is used to define what your tracker looks like on the inside, the schema of the tracker. It defines the Classes and properties on each class. It also defines the security for those Classes. The next few sections describe how schemas work and what you can do with them. The initial_data.py module sets up the initial state of your tracker. It’s called exactly once - by the ``roundup-admin initialise`` command. See the start of the section on `database content`_ for more info about how this works. .. index:: schema; classic - description of The "classic" schema -------------------- The "classic" schema looks like this (see section `setkey(property)`_ below for the meaning of ``'setkey'`` -- you may also want to look into the sections `setlabelprop(property)`_ and `setorderprop(property)`_ for specifying (default) labelling and ordering of classes.):: pri = Class(db, "priority", name=String(), order=String()) pri.setkey("name") stat = Class(db, "status", name=String(), order=String()) stat.setkey("name") keyword = Class(db, "keyword", name=String()) keyword.setkey("name") user = Class(db, "user", username=String(), organisation=String(), password=String(), address=String(), realname=String(), phone=String(), alternate_addresses=String(), queries=Multilink('query'), roles=String(), timezone=String()) user.setkey("username") msg = FileClass(db, "msg", author=Link("user"), summary=String(), date=Date(), recipients=Multilink("user"), files=Multilink("file"), messageid=String(), inreplyto=String()) file = FileClass(db, "file", name=String()) issue = IssueClass(db, "issue", keyword=Multilink("keyword"), status=Link("status"), assignedto=Link("user"), priority=Link("priority")) issue.setkey('title') .. index:: schema; classes and properties Classes and Properties - creating a new information store --------------------------------------------------------- The tracker above, defines 7 classes of information: priority Defines the possible levels of urgency for issues. status Defines the possible states of processing the issue may be in. keyword Initially empty, will hold keywords useful for searching issues. user Initially holding the "admin" user, will eventually have an entry for all users using Roundup. msg Initially empty, will hold all e-mail messages sent to or generated by Roundup. file Initially empty, will hold all files attached to issues. issue Initially empty, this is where the issue information is stored. It defines the "priority" and "status" classes to allow two things: 1. reduction in the amount of information stored on the issue 2. more powerful, accurate searching of issues by priority and status By requiring a link on the issue (stored as a single number) the chance that someone mis-types a priority or status - or makes a new one up is reduced. Class names access items of that class in the `REST api`_ interface. The classic tracker was created before the REST interface was added. It uses the single form (i.e. issue and user not issues and users) for its classes. Most REST documentation suggests using plural forms. However, to make your API consistent, use singular forms for classes that you add. .. index:: schema; classes schema; items Class and Items ~~~~~~~~~~~~~~~ A Class defines a particular class (or type) of data that will be stored in the database. A class comprises one or more properties, which gives the information about the class items. The actual data entered into the database, using ``class.create()``, are called items. They have a special immutable property called ``'id'``. We sometimes refer to this as the *itemid*. .. index:: schema; property types Properties ~~~~~~~~~~ A Class consists of one or more properties of the following types: Boolean properties store on/off, yes/no, true/false values. Date properties store date-and-time stamps. Their values are Timestamp objects. Integer properties store integer values. (Number can store real/float values.) Interval properties store time periods rather than absolute dates. For example 2 hours. Link properties refers to a single item selected from a specified class. The class is part of the property; the value is an integer, the id of the chosen item. Multilink properties refer to one or more items in a specified class. The value is a list of integers. Number properties store numeric values. There is an option to use double-precision floating point numbers. Password properties store encoded arbitrary-length strings. The default encoding is defined in the ``roundup.password.Password`` class. String properties store arbitrary-length strings. Properties have attributes to change the default behaviour: .. index:: triple: schema; property attributes; required triple: schema; property attributes; default_value triple: schema; property attributes; quiet * All properties support the following attributes: - ``required``: see `design documentation`_. Adds the property to the list returned by calling get_required_props for the class. - ``default_value``: see `design documentation`_ Sets the default value if the property is not set. - ``quiet``: see `design documentation`_. Suppresses reporting user visible changes to this property. The property change is not reported: - in the change feedback/confirmation message in the web interface - the property change section of the nosy email - the web history at the bottom of an item's page This is useful when storing the state of the user interface (e.g. the names of elements that are collapsed or hidden from the user). Properties updates as an indirect result of a user's change (e.g. updating a blockers property, counting number of times an issue is reopened or reassigned etc.) should not be displayed to the user as they can be confusing. .. index:: triple: schema; property attributes; indexme * String properties have an ``indexme`` attribute. The default is 'no'. Setting it to 'yes' includes the property in the full text index. .. index:: triple: schema; property attributes; use_double * Number properties can have a ``use_double`` attribute that, when set to ``True``, will use double precision floating point in the database. * Link and Multilink properties can have several attributes: .. index:: triple: schema; property attributes; do_journal - ``do_journal``: By default, every change of a link property is recorded in the item being linked to (or being unlinked). A typical use-case for setting ``do_journal='no'`` would be to turn off journalling of nosy list, message author and message recipient link and unlink events to prevent the journal from clogged with these events. .. index:: triple: schema; property attributes; try_id_parsing - ``try_id_parsing`` is turned on by default. If a number is entered into a Link or Multilink field, Roundup interprets this number as an ID of the item to link to. Sometimes items can have numeric names (e.g., product codes). For these Roundup needs to match the numeric name and should never match an ID. In this case you can set ``try_id_parsing='no'``. .. index:: triple: schema; property attributes; rev_multilink - The ``rev_multilink`` option takes a property name to be inserted into the linked-to class. This property is a Multilink property that links back to the current class. The new Multilink is read-only (it is automatically modified if the Link or Multilink property defining it is modified). The new property can be used in normal searches using the "filter" method of the Class. This means it can be used like other Multilink properties when searching (in an index template) or via the REST and XMLRPC APIs. As a example, suppose you want to group multiple issues into a super issue. Each issue can be part of one super issue. It is inefficient to find all of the issues linked to the super issue by searching through all issues in the system looking at the part_of link property. To make this more efficient, you can declare an issue's part_of property as:: issue = IssueClass(db, "issue", ... part_of = Link("issue", rev_multilink="components"), ... ) This automatically creates the ``components`` multilink on the issue class. The ``components`` multilink is never explicitly declared in the issue class, but it has the same effect as though you had declared the class as:: issue = IssueClass(db, "issue", ... part_of = Link("issue"), components = Multilink("issue"), ... ) Then wrote a detector to update the components property on the corresponding issue. Writing this detector can be tricky. There is one other difference, you can not explicitly set/modify the ``components`` multilink. The effect of setting ``part_of = 3456`` on issue1234 automatically adds "1234" to the ``components`` property on issue3456. You can search the ``components`` multilink just like a regular multilink, but you can't explicitly assign to it. Another difference of reverse multilinks to normal multilinks is that when a linked node is retired, the node vanishes from the multilink. In the example above, when issue 1234 with ``part_of`` set to issue 5678 is retired, 1234 vanishes from the ``components`` multilink of issue 5678. You can also link between different classes. If the issue definition includes:: issue = IssueClass(db, "issue", ... assigned_to = Link("user", rev_multilink="responsibleFor"), ... ) This makes it easy to list all issues that the user is responsible for (aka assigned_to). .. index:: triple: schema; property attributes; msg_header_property - The ``msg_header_property`` is used by the mail gateway when sending out messages. When a link or multilink property of an issue changes, Roundup creates email headers of the form:: X-Roundup-issue-prop: value where ``value`` is the ``name`` property for the linked item(s). For example, if you have a multilink for attached_files in your issue, you will see a header:: X-Roundup-issue-attached_files: MySpecialFile.doc, HisResume.txt when the class for attached files is defined as:: file = FileClass(db, "file", name=String()) ``MySpecialFile.doc`` is the name for the file object. If you have an ``assigned_to`` property in your issue class that links to the user class and you want to add a header:: X-Roundup-issue-assigned_to: ... so that the mail recipients can filter emails where ``X-Roundup-issue-assigned_to: name`` contains their username. The user class is defined as:: user = Class(db, "user", username=String(), password=Password(), address=String(), realname=String(), phone=String(), organisation=String(), alternate_addresses=String(), queries=Multilink('query'), roles=String(), # comma-separated string of Role names timezone=String()) Because the user class does not have a ``name`` parameter, no header will be written. Setting:: assigned_to=Link("user", msg_header_property="username") will make the mail gateway generate an ``X-Roundup-issue-assigned_to`` using the username property of the linked user. Assume assigned_to for an issue is linked to the user with username=joe_user, setting:: msg_header_property="username" for the assigned_to property will generated message headers of the form:: X-Roundup-issue-assigned_to: joe_user for emails sent on issues where joe_user has been assigned to the issue. If this property is set to the empty string "", no header will be generated on outgoing mail. .. index:: triple: schema; class property; creator triple: schema; class property; creation triple: schema; class property; actor triple: schema; class property; activity All Classes automatically have four properties by default: *creator* Link to the user that created the item. *creation* Date the item was created. *actor* Link to the user that last modified the item. *activity* Date the item was last modified. .. index:: double: schema; class methods Methods ~~~~~~~ All classes have the following methods. .. index:: triple: schema; class method; setkey setkey(property) :::::::::::::::: .. index:: roundup-admin; setting assignedto on an issue Set the key property of a class to a string property. The key property must be unique. References to the items in the class can be done by the content of the key property. For example, you can refer to users by their username. Let's say that there's an issue in Roundup, issue 23. There's also a user, richard, who happens to be user 2. To assign an issue to him, we could do use:: roundup-admin set issue23 assignedto=2 or:: roundup-admin set issue23 assignedto=richard Note, the same thing can be done in the web and e-mail interfaces. .. index:: triple: schema; class method; setlabelprop setlabelprop(property) :::::::::::::::::::::: Select a property of the class to be the label property. The label property is used whenever an item should be uniquely identified, e.g., when displaying a link to an item. If setlabelprop is not specified for a class, the following values are tried for the label: * the key of the class (see the `setkey(property)`_ section above) * the "name" property * the "title" property * the first property from the sorted property name list In most cases you can get away without specifying setlabelprop explicitly. Users should have View access to this property or the id property for a class. If the property can not be viewed by a user, looping over items in the class (e.g. messages attached to an issue) will not work. .. index:: triple: schema; class method; setorderprop setorderprop(property) :::::::::::::::::::::: Select a property of the class to be the order property. The order property is used whenever using a default sort order for the class, e.g., when grouping or sorting class A by a link to class B in the user interface, the order property of class B is used for sorting. If setorderprop is not specified for a class, the following values are tried for the order property: * the property named "order" * the label property (see `setlabelprop(property)`_ above) Usually you can get away without specifying setorderprop explicitly. .. index:: triple: schema; class method; create create(information) ::::::::::::::::::: Create an item in the database. This is used to create items in the :term:`definitional class` like "priority" and "status". .. index:: triple: schema; class property; messages triple: schema; class property; files triple: schema; class property; nosy triple: schema; class property; superseder IssueClass ~~~~~~~~~~ IssueClass automatically includes the "messages", "files", "nosy", and "superseder" properties. The messages and files properties list the links to the messages and files related to the issue. The nosy property is a list of links to users to tell about changes to the issue. They get "CC'ed" e-mails when messages are sent to or generated by the issue. The nosy reactor (in the ``'detectors'`` directory) handles this action. The superseder link indicates an issue which has superseded this one. It is better described in the `original document `_. They also have the default "creation", "activity" and "creator" properties. The value of the "creation" property is the date when an item was created. The value of the "activity" property is the date when any property on the item was last edited (equivalently, these are the dates on the first and last records in the item's journal). The "creator" property holds a link to the user that created the issue. .. index:: triple: schema; class property; content triple: schema; class property; type FileClass ~~~~~~~~~ FileClasses save their "content" attribute off in a separate file from the rest of the database. This reduces the number of large entries in the database, which makes databases more efficient. Also web servers, image processing applications, and command line tools can operate on the files. The content is stored in the ``files`` sub-directory of the ``'db'`` directory in your tracker. FileClasses also have a "type" attribute to store the file's MIME type. Roundup, by default, considers the contents of the file immutable. This assists in maintaining an accurate record of correspondence. The distributed tracker templates do not enforce this. If you have access to the Roundup tracker directory, you can edit the files (make sure to preserve mode, owner and group) to remove information. You may need to do this if somebody includes a password or you need to redact proprietary information. The journal for the message/file won't report that the file has changed. Best practice is to remove offending material and leave a placeholder. E.G. replace a password with the text:: [password has been deleted 2020-12-02 --myname] If you need to delete an entire file, replace the file contents with:: [file contents deleted due to spam 2020-10-21 --myname] rather than deleting the file. If you actually delete the file Roundup will report an error to the user and email the administrator. If you empty the file, a user downloading the file using the direct URL (e.g. ``tracker/msg22``) may be confused and think something is broken when they receive an empty file. Retiring a file/msg does not prevent access to the file using the direct URL. Retiring an item only removes it when requesting a list of all items in the class. If you are replacing the contents, you probably want to change the content type of the file. E.G. from ``image/jpeg`` to ``text/plain``. You can do this easily through the web interface, or using the ``roundup-admin`` command line interface. You can also change the contents of a file or message using the REST interface. Note that this will NOT result in an entry in the journal, so again it allows a silent change. To do this you need to make two rest requests. An example using curl is:: $ curl -u demo:demo -s -H "X-requested-with: rest" \ -H "Referer: https://tracker.example.com/demo/" \ -X GET \ https://tracker.example.com/demo/rest/data/file/30/content { "data": { "id": "30", "type": "", "link": "https://tracker.example.com/demo/rest/data/file/30/content", "data": "hello3", "@etag": "\"3f2f8063dbce5b6bd43567e6f4f3c671\"" } } using the etag, overwrite the content with:: $ curl -u demo:demo -s -H "X-requested-with: rest" \ -H "Referer: https://tracker.example.com/demo/" \ -H 'If-Match: "3f2f8063dbce5b6bd43567e6f4f3c671"' \ -X PUT \ -F "data=@hello" \ https://tracker.example.com/demo/rest/data/file/30/content where ``hello`` is a file on local disk. You can enforce immutability in your tracker by adding an auditor (see detectors_) for the file/msg class that rejects changes to the content property. The auditor could also add a journal entry so that a change via the Roundup mechanism is reported. Using a mixin (see: https://wiki.roundup-tracker.org/MixinClassFileClass) to augment the file class allows for other possibilities including: * signing the file, * recording a checksum in the database and validating the file contents at the time it gets read. This allows detection of changes done on the filesystem outside of the Roundup mechanism. * keeping multiple revisions of the file. .. index:: schema; item ordering A note about ordering ~~~~~~~~~~~~~~~~~~~~~ When we sort items in the hyperdb, we use one of three methods, depending on the property type: 1. String, Integer, Number, Date or Interval property, sort the scalar value of the property. Strings sort case-sensitively. 2. a Link property, sort by either the linked item's "order" property (if it has one) or the linked item's "id". 3. Mulitlinks sort similar to #2, starting with the first Multilink list item, and if they are the same, sort by the second item, and so on. Note that if an "order" property is defined for a class, all items of that Class *must* have a value for the "order" property, or sorting will result in random ordering. Examples of adding to your schema --------------------------------- See :ref:`CustomExamples` for examples. The `Roundup wiki CategorySchema`_ provides a list of additional examples of how to customize schemas to add new functionality. .. _Roundup wiki CategorySchema: https://wiki.roundup-tracker.org/CategorySchema .. index:: !detectors .. _detectors: .. _Auditors and reactors: Schema Integrity ---------------- There is a table in all SQL based schemas called ``schema``. It contains a representation of the current schema and the current Roundup schema version. Roundup will exit if the version is not supported by the release. E.G. Roundup 2.1.0 will not work with a database created by 2.3.0 as db version 8 used by 2.3.0 is not supported by 2.1.0. The current schema representation is automatically updated whenever a change is made to the schema via ``schema.py``. The schema version is upgraded when running ``roundup-admin migrate`` although it can be upgraded automatically in some cases by run a Roundup process (mailgw, web interface). This information is kept in one large blob in the table. To view this in a more understandable format, you can use the commands below (requires the jq command): Postgres .. code:: psql -tq -d 'roundup_db' -U roundup_user -c \ 'select schema from schema;' | \ python3 -c 'import json, sys; d = eval(sys.stdin.read()); \ print(json.dumps(d, indent=2));' | jq . | less replace ``roundup_db``, ``roundup_user`` with the values from ``config.ini`` and use a ``~/.pgpass`` file or type the database password when prompted. SQLite .. code:: sqlite3 demo/db/db 'select schema from schema;' | \ python3 -c 'import json, sys; d = eval(sys.stdin.read()); \ print(json.dumps(d, indent=2));' | jq . | less Something similar for MySQL can be generated as well. Replacing ``jq .`` with ``jq .version`` will display the schema version. Detectors - adding behaviour to your tracker ============================================ Detectors are Python modules that sit in your tracker's ``detectors`` directory. You are free to add and remove them any time, even after the database is initialised via the ``roundup-admin initialise`` command. There are two types of detectors: 1. *auditors* are run before changes are made to the database 2. *reactors* are run after the change has been committed to the database .. index:: auditors; rules for use single: reactors; rules for use Auditor or Reactor? ------------------- Generally speaking, you should observe the following rules: **Auditors** Are used for `vetoing creation of or changes to items`_. They might also make automatic changes to item properties. They can raise the ``Reject`` or ``CheckId`` exceptions to control database changes. **Reactors** Detect changes in the database and react accordingly. They should avoid making changes to the database where possible, as this could create detector loops. Detectors Installed by Default ------------------------------ You will have some detectors installed by default - have a look in the ``detectors`` subdirectory of your tracker home. You can write new detectors or modify the existing ones. The existing detectors installed for you are: .. index:: detectors; installed **nosyreaction.py** This provides the automatic nosy list maintenance and email sending. The nosy reactor (``nosyreaction``) fires when new messages are added to issues. The nosy auditor (``updatenosy``) fires when issues are changed, and figures out what changes need to be made to the nosy list (such as adding new authors, etc.) If you are running a tracker started with ``roundup-demo`` or the ``demo.py`` script, this detector will be missing.This is intentional to prevent email from being sent from a demo tracker. You can find the nosyreaction.py detector in the :term:`template directory (meaning 3)