Signalen / backend

Backend for Signalen, an application that helps cities manage and prioritize nuisance reports.
https://signalen.org
Mozilla Public License 2.0
5 stars 5 forks source link

Design for configurable questions per category #9

Closed bartjkdp closed 4 years ago

bartjkdp commented 4 years ago

As an administrator I want to be able to configure the questions per category so that I can easily tailor the application to the needs of my organization

Current implementation

The frontend calls the backend on /signals/category/prediction to predict the main category and subcategory as soon as the user entered the text. The backend returns a list of predicted main categories and subcategories together with the probabilities.

The frontend uses this information to decide which questions to show. The questions are currently hardcoded per category in the frontend. This can be found in the folder wizard-step2-vulaan.

The following field types are supported:

A field has a key, metadata (such as the label, the subtitle and values of options), a required flag and conditions that need to be met in order to show the field.

Proposal

We would like to make the questions configurable in the backend. This allows Signals to be reused by other municipalities with a different workflow. This change is also beneficial for the Municipality of Amsterdam as a software developer is no longer required to make changes to the questions.

Backend

We would like to extend the backend with three new models:

Question

Category

CategoryQuestion

CategoryQuestion holds the ordering of the question within a certain category.

We add a new endpoint to the API that allows to fetch questions of a certain main and subcategory:

/signals/v1/public/questions?main_slug={main_slug}&sub_slug={sub_slug}

The response looks as follows:

{
  "_links": {},
  "count": 1,
  "results": [
    {
      "key": "extra_bedrijven_horeca_personen",
      "field_type": "CHECKBOX",
      "meta": {
        "ifOneOf": {
          "extra_bedrijven_horeca_wat": [
            "horecabedrijf",
            "ander_soort_bedrijf",
            "evenement_festival_markt",
            "iets_anders"
          ]
        },
        "label": "Wat is de oorzaak van de overlast?",
        "labelShort": "Oorzaak overlast",
        "values": {
          "dronken_bezoekers": "Dronken bezoekers",
          "schreeuwende_bezoekers": "Schreeuwende bezoekers",
          "rokende_bezoekers": "Rokende bezoekers",
          "teveel_fietsen": "(Teveel) fietsen",
          "wildplassen": "Wildplassen",
          "overgeven": "Overgeven"
        }
      }
    }
  ]
}

With respect to ordering the backend will first return questions assigned to the main category and then questions assigned to the subcategory. Per category the questions are ordered using the order field.

The backend validates the content of meta based on the field type.

Frontend

We implement a new call to fetch the questions from the backend as soon as we received the prediction. We add a new reducer and store the additional questions in Redux.

In the second step we fetch the questions from the Redux store and display them. We can remove the hardcoded steps in wizard-step-2-vulaan.

We can also remove the logic of ifOneOf and ifAllOf with respect to categories. The backend already determines which questions to show.

Migration path

  1. We introduce the new endpoint in the API.
  2. We load the currently hardcoded settings by manually creating a migration file based on the latest frontend code.
  3. We load this data with python manage.py loaddata.
  4. We introduce the change in the frontend.

Admin

In the initial version questions are configurable using the Django admin. We can add jsoneditor to the Django admin to ease editing of the JSONField.

vanbuiten commented 4 years ago

@bartjkdp my first review of the proposal. I also asked Thijs to take a look at it

In general I would like to start with a very basic implementation that will return the questions in a JSON response that can be easily used in the FE. This way we can move the questions to the API. I think that this proposal can be the first step into accomplishing that.

Some remarks/answers on your question:

The endpoint /signals/category/prediction is a proxy for the machine learning tool. The only additional functionality in this endpoint is the abbility to translate old categories to new ones, this can happen if the category is disbaled and the machine learning is not yet trained. Also /signals/category/prediction is a V0 endpoint that will be removed from the api soon (it's replacement /signals/v1/category/prediction is not yet ready for use).

I think this should not live in the machine learning endpoint. I would choose something like /signals/v1/private/categories/151/additional-questions/ and /signals/v1/public/terms/categories/openbaar-groen-en-water/sub_categories/beplanting/additional-questions/. This way we can add it to the "_links" section of the category in the response from the API. The response can be the list of questions, so without the whole prediction part in your example. This way we can easily add more functionality that expose the questions in different ways (for example think of the admin pages in the Front-End).

Do we want to use a JSONField to support the different configuration options per field or do we want to make the options explicit as fields in the Django model?

For speed we can start with using a JSONField, if needed we can always refactor this in a model structure that we find more suitable for the future.

Do we want to make the ordering dynamic based on answers?

This would be nice to have. I can imagine that specific answers to a question can invoke a whole different flow, where questions can be asked in a different order.

tcoenen commented 4 years ago

We talked about this during the Thursday meeting:

PS I see Django 3.0 feature used in this PR (models.TextChoices), we are currently on 2.2 LTS.

bartjkdp commented 4 years ago

Thanks for all the comments @tcoenen @vanbuiten. I just updated the design.

janjaap commented 4 years ago

Do take into account that the questions and the order of those questions doesn't just depend on the category and subcategory, but also on answers given to particular questions.

Take this example where the question is shown where the subcategory equals 'geluidsoverlast-muziek' and where a previously selected option in extra_bedrijven_horeca_wat is either 'horecabedrijf' or 'ander_soort_bedrijf':

extra_bedrijven_horeca_muziek_ramen_dicht: {
  meta: {
    ifAllOf: {
      subcategory: 'geluidsoverlast-muziek',
    },
    ifOneOf: {
      extra_bedrijven_horeca_wat: [
        'horecabedrijf',
        'ander_soort_bedrijf',
      ],
    },
    label: 'Hebt u ook last van het geluid als uw ramen en deuren gesloten zijn?',
    labelShort: 'Overlast met ramen en deuren dicht',
    className: 'col-sm-12 col-md-8',
    pathMerge: 'extra_properties',
    values: {
      ja: 'Ja, ook last met ramen en deuren gesloten',
      nee: 'Nee, geen last met ramen en deuren gesloten',
    },
  },
  render: FormComponents.RadioInput,
},

We're currently also looking into the following:

Uitgangspunt bij de categorie Wonen Overig is dat de melder zelf een keuze maakt wat het onderwerp is van de melding (binnen de hoofdcategorie wonen). Afhankelijk van de keuze worden de vragen van de categorie weergegeven.

Uw melding gaat over:

  • Illegale toeristische verhuur in een woning of woonboot
  • Illegale onderhuur in een woning of woonboot
  • Een woning of woonboot die opvallend lang leegstaat
  • Criminele bewoning of activiteiten in een woning of woonboot
  • Woningdelen (de woning wordt door verschillende mensen gedeeld)
  • Achterstallig onderhoud of een gebrek aan een woning wordt niet verholpen door de eigenaar/beheerder

[Afhankelijk van de keuze worden de vragen van de gekozen categorie geladen]

De antwoorden komen overeen met de volgende categorieën:

  • Vakantieverhuur
  • Onderhuur en adreskwaliteit
  • Leegstand
  • Woningdelen / spookburgers
  • Woningkwaliteit