def topic(topic_uri):
    try:
        topic = page_service.get_page_by_uri_and_type(topic_uri, "topic")
    except PageNotFoundException:
        abort(404)

    # We want to avoid passing measures into the template that the current user should not be able to see listed.
    # Departmental users should not be able to see unpublished measures that have not been explicitly shared with them.
    def user_can_see_measure(measure):
        if measure.published or current_user in measure.shared_with or not current_user.is_departmental_user():
            return True
        else:
            return False

    subtopics = topic.children
    measures = {
        subtopic.guid: list(filter(user_can_see_measure, page_service.get_latest_measures(subtopic)))
        for subtopic in subtopics
    }

    return render_template(
        "static_site/topic.html",
        topic=topic,
        subtopics=subtopics,
        measures=measures,
        static_mode=get_bool(request.args.get("static_mode", False)),
    )
def measure_page(topic_uri, subtopic_uri, measure_uri, version):
    try:
        if version == "latest":
            measure_page = page_service.get_latest_version(topic_uri, subtopic_uri, measure_uri)
        else:
            measure_page = page_service.get_page_by_uri_and_type(measure_uri, "measure", version)
    except PageNotFoundException:
        abort(404)

    versions = page_service.get_previous_major_versions(measure_page)
    edit_history = page_service.get_previous_minor_versions(measure_page)
    if edit_history:
        first_published_date = page_service.get_first_published_date(measure_page)
    else:
        first_published_date = measure_page.publication_date

    dimensions = [dimension.to_dict() for dimension in measure_page.dimensions]

    return render_template(
        "static_site/measure.html",
        topic_uri=topic_uri,
        subtopic_uri=subtopic_uri,
        measure_page=measure_page,
        dimensions=dimensions,
        versions=versions,
        first_published_date=first_published_date,
        edit_history=edit_history,
        static_mode=get_bool(request.args.get("static_mode", False)),
    )
def index():
    topics = sorted(
        Page.query.filter(Page.page_type == "topic", Page.parent_guid == "homepage").all(),
        key=lambda topic: topic.title,
    )

    return render_template(
        "static_site/index.html", topics=topics, static_mode=get_bool(request.args.get("static_mode", False))
    )
def __classification_data_item_from_file_data_row(file_row):
    item_is_required = get_bool(
        file_row[EthnicityClassificationFileColumn.REQUIRED])
    return EthnicityClassificationDataItem(
        display_ethnicity=file_row[
            EthnicityClassificationFileColumn.DISPLAY_VALUE],
        parent=file_row[EthnicityClassificationFileColumn.PARENT],
        order=file_row[EthnicityClassificationFileColumn.ORDER],
        required=item_is_required,
    )
    def inject_globals():
        from application.auth.models import (
            COPY_MEASURE,
            CREATE_MEASURE,
            CREATE_VERSION,
            DELETE_MEASURE,
            MANAGE_SYSTEM,
            MANAGE_USERS,
            MANAGE_DATA_SOURCES,
            ORDER_MEASURES,
            PUBLISH,
            READ,
            UPDATE_MEASURE,
            VIEW_DASHBOARDS,
        )

        return dict(
            COPY_MEASURE=COPY_MEASURE,
            CREATE_MEASURE=CREATE_MEASURE,
            CREATE_VERSION=CREATE_VERSION,
            DELETE_MEASURE=DELETE_MEASURE,
            MANAGE_SYSTEM=MANAGE_SYSTEM,
            MANAGE_USERS=MANAGE_USERS,
            MANAGE_DATA_SOURCES=MANAGE_DATA_SOURCES,
            ORDER_MEASURES=ORDER_MEASURES,
            PUBLISH=PUBLISH,
            READ=READ,
            UPDATE_MEASURE=UPDATE_MEASURE,
            VIEW_DASHBOARDS=VIEW_DASHBOARDS,
            TESTING_SPACE_SLUG=TESTING_SPACE_SLUG,
            get_content_security_policy=get_content_security_policy,
            current_timestamp=datetime.datetime.now().isoformat(),
            get_form_errors=get_form_errors,
            static_mode=get_bool(request.args.get("static_mode", app.config["STATIC_MODE"])),
            raise_exception=jinja_raise_exception,
        )
class Config:
    DEBUG = False
    LOG_LEVEL = logging.INFO
    ENVIRONMENT = os.environ.get("ENVIRONMENT", "PRODUCTION")
    SECRET_KEY = os.environ["SECRET_KEY"]
    PROJECT_NAME = "rd_cms"
    BASE_DIRECTORY = dirname(dirname(os.path.abspath(__file__)))
    WTF_CSRF_ENABLED = True
    SESSION_COOKIE_SECURE = True

    GITHUB_ACCESS_TOKEN = os.environ["GITHUB_ACCESS_TOKEN"]
    HTML_CONTENT_REPO = os.environ.get("HTML_CONTENT_REPO", "rd_html_dev")
    GITHUB_URL = os.environ.get("GITHUB_URL", "github.com/racedisparityaudit")
    STATIC_SITE_REMOTE_REPO = "https://{}:x-oauth-basic@{}.git".format(
        GITHUB_ACCESS_TOKEN, "/".join((GITHUB_URL, HTML_CONTENT_REPO))
    )

    SQLALCHEMY_DATABASE_URI = os.environ["DATABASE_URL"]
    PERMANENT_SESSION_LIFETIME = timedelta(minutes=int(os.environ.get("PERMANENT_SESSION_LIFETIME_MINS", 360)))
    SECURITY_PASSWORD_SALT = SECRET_KEY
    SECURITY_PASSWORD_HASH = "bcrypt"
    SECURITY_URL_PREFIX = "/auth"
    SECURITY_EMAIL_SENDER = "*****@*****.**"

    SQLALCHEMY_TRACK_MODIFICATIONS = False
    RESEARCH = get_bool(os.environ.get("RESEARCH", False))

    SECURITY_FLASH_MESSAGES = False
    STATIC_BUILD_DIR = os.environ["STATIC_BUILD_DIR"]

    FILE_SERVICE = os.environ.get("FILE_SERVICE", "Local")

    S3_UPLOAD_BUCKET_NAME = os.environ["S3_UPLOAD_BUCKET_NAME"]
    S3_STATIC_SITE_BUCKET = os.environ["S3_STATIC_SITE_BUCKET"]
    S3_STATIC_SITE_ERROR_PAGES_BUCKET = os.environ["S3_STATIC_SITE_ERROR_PAGES_BUCKET"]
    S3_REGION = os.environ.get("S3_REGION", "eu-west-2")
    LOCAL_ROOT = os.environ.get("LOCAL_ROOT", None)

    DICTIONARY_LOOKUP_FILE = os.environ.get(
        "DICTIONARY_LOOKUP_FILE", "./application/data/static/standardisers/dictionary_lookup.csv"
    )
    DICTIONARY_LOOKUP_DEFAULTS = ["*", "*", "Unclassified", 960]

    ETHNICITY_CLASSIFICATION_FINDER_LOOKUP = os.environ.get(
        "CLASSIFICATION_FINDER_LOOKUP", "./application/data/static/standardisers/classification_lookup.csv"
    )
    ETHNICITY_CLASSIFICATION_FINDER_CLASSIFICATIONS = os.environ.get(
        "ETHNICITY_CLASSIFICATION_FINDER_CLASSIFICATIONS",
        "./application/data/static/standardisers/classification_definitions.csv",
    )

    SIMPLE_CHART_BUILDER = get_bool(os.environ.get("SIMPLE_CHART_BUILDER", False))
    RDU_SITE = os.environ.get("RDU_SITE", "https://www.ethnicity-facts-figures.service.gov.uk")
    RDU_EMAIL = os.environ.get("RDU_EMAIL", "*****@*****.**")

    LOCAL_BUILD = get_bool(os.environ.get("LOCAL_BUILD", False))

    BUILD_SITE = get_bool(os.environ.get("BUILD_SITE", False))
    PUSH_SITE = get_bool(os.environ.get("PUSH_SITE", False))
    DEPLOY_SITE = get_bool(os.environ.get("DEPLOY_SITE", False))

    ATTACHMENT_SCANNER_ENABLED = get_bool(os.environ.get("ATTACHMENT_SCANNER_ENABLED", False))
    ATTACHMENT_SCANNER_URL = os.environ.get("ATTACHMENT_SCANNER_URL", "")
    ATTACHMENT_SCANNER_API_TOKEN = os.environ.get("ATTACHMENT_SCANNER_API_TOKEN", "")

    JSON_ENABLED = get_bool(os.environ.get("JSON_ENABLED", False))

    GOOGLE_ANALYTICS_ID = os.environ["GOOGLE_ANALYTICS_ID"]

    MAIL_SERVER = os.environ.get("MAILGUN_SMTP_SERVER")
    MAIL_USE_SSL = True
    MAIL_PORT = int(os.environ.get("MAILGUN_SMTP_PORT", 465))
    MAIL_USERNAME = os.environ.get("MAILGUN_SMTP_LOGIN")
    MAIL_PASSWORD = os.environ.get("MAILGUN_SMTP_PASSWORD")
    TOKEN_MAX_AGE_SECONDS = 60 * 60 * 24
    PREVIEW_TOKEN_MAX_AGE_DAYS = int(os.environ.get("PREVIEW_TOKEN_MAX_AGE_DAYS", 14))
    SURVEY_ENABLED = get_bool(os.environ.get("SURVEY_ENABLED", False))
    WTF_CSRF_TIME_LIMIT = None

    TRELLO_API_KEY = os.environ.get("TRELLO_API_KEY", "")
    TRELLO_API_TOKEN = os.environ.get("TRELLO_API_TOKEN", "")

    GOOGLE_CUSTOM_SEARCH_ENDPOINT = "https://cse.google.com/cse/publicurl"
    GOOGLE_CUSTOM_SEARCH_ID = "013520531703188648524:9giiejumedo"

    REDIRECT_HTTP_CODE = os.environ.get("REDIRECT_HTTP_CODE", 301)
    REDIRECT_PROTOCOL = os.environ.get("REDIRECT_PROTOCOL", "http")
    REDIRECT_HOSTNAME = os.environ.get("REDIRECT_HOSTNAME", "localhost")

    NEWSLETTER_SUBSCRIBE_URL = os.environ.get("NEWSLETTER_SUBSCRIBE_URL")
Example #7
0
class MeasureVersionForm(FlaskForm):
    class NotRequiredForMajorVersions:
        def __call__(self, form: "MeasureVersionForm", field):
            if not form.is_minor_update:
                field.errors[:] = []
                raise StopValidation()

    class OnlyIfUpdatingDataMistake:
        def __call__(self, form: "MeasureVersionForm", field):
            if not form.update_corrects_data_mistake.data:
                field.errors[:] = []
                raise StopValidation()

    db_version_id = IntegerField(widget=HiddenInput())
    title = RDUStringField(
        label="Title",
        validators=[DataRequired(message="Enter a page title"), Length(max=255)],
        hint="For example, ‘Self-harm by young people in custody’",
    )
    internal_reference = RDUStringField(
        label="Measure code (optional)", hint="This is for internal use by the Race Disparity Unit"
    )
    published_at = DateField(label="Publication date", format="%Y-%m-%d", validators=[Optional()])
    time_covered = RDUStringField(
        label="Time period covered",
        validators=[RequiredForReviewValidator(message="Enter the time period covered")],
        extended_hint="_time_period_covered.html",
    )

    area_covered = RDUCheckboxField(
        label="Areas covered", enum=UKCountry, validators=[RequiredForReviewValidator(message="Enter the area covered")]
    )

    lowest_level_of_geography_id = RDURadioField(
        label="Geographic breakdown",
        hint="Select the most detailed type of geographic breakdown available in the data",
        validators=[RequiredForReviewValidator(message="Select the geographic breakdown", else_optional=True)],
    )
    suppression_and_disclosure = RDUTextAreaField(
        label="Suppression rules and disclosure control (optional)",
        hint="If any data has been excluded from the analysis, explain why.",
        extended_hint="_suppression_and_disclosure.html",
    )
    estimation = RDUTextAreaField(
        label="Rounding (optional)", hint="For example, ‘Percentages are rounded to one decimal place’"
    )

    summary = RDUTextAreaField(
        label="Main points",
        validators=[RequiredForReviewValidator(message="Enter the main points")],
        hint=Markup(
            "<p class='govuk-body govuk-hint'>Summarise the main findings. Don’t include contextual information.</p>"
            "<details class='govuk-details' data-module='govuk-details'>"
            "<summary class='govuk-details__summary'>"
            "<span class='govuk-details__summary-text'>"
            "What to include"
            "</span>"
            "</summary>"
            "<div class='govuk-details__text'>"
            "<ul class='govuk-list govuk-list--bullet'>"
            "<li>the time period the data covers (in the first bullet point)</li>"
            "<li>definitions of any terms users might not understand</li>"
            "<li>details of any serious issues with data quality</li>"
            "</ul>"
            "</div>"
            "</details>"
        ),
        extended_hint="_summary.html",
    )

    measure_summary = RDUTextAreaField(
        label="What the data measures",
        validators=[RequiredForReviewValidator(message="Explain what the data measures")],
        hint=(
            "Explain what the data is analysing, what’s included in categories labelled as ‘Other’ and define any "
            "terms users might not understand"
        ),
    )

    description = RDUTextAreaField(
        label="Description for search engines",
        validators=[RequiredForReviewValidator(message="Enter a description for search engines")],
        hint=(
            "Choose an up‐to‐date statistic that shows a key disparity or change over time. The figure should work as "
            "a stand-alone statement and end with a full stop."
        ),
        extended_hint="_description.html",
        character_count_limit=160,
    )

    need_to_know = RDUTextAreaField(
        label="Things you need to know",
        validators=[RequiredForReviewValidator(message="Explain what the reader needs to know to understand the data")],
        hint="Outline how the data was collected and explain any limitations",
        extended_hint="_things_you_need_to_know.html",
    )

    ethnicity_definition_summary = RDUTextAreaField(
        label="The ethnic categories used in this data",
        validators=[RequiredForReviewValidator(message="List the ethnic categories used in the data")],
        hint=Markup(
            "Only use this section to explain if:"
            "<ul class='govuk-list govuk-list--bullet govuk-hint'>"
            "<li>the standardised list of 18 ethnic groups isn’t being used (ONS 2011)</li>"
            "<li>there's only data for broad ethnic groups, not specific groups</li>"
            "<li>there are different ethnic classifications used in the same measure page</li>"
            "</ul>"
            "<details class='govuk-details' data-module='govuk-details'>"
            "<summary class='govuk-details__summary'>"
            "<span class='govuk-details__summary-text'>"
            "Example"
            "</span>"
            "</summary>"
            "<div class='govuk-details__text'>"
            "The number of people surveyed was too small to make reliable generalisations about specific ethnic groups."
            "So the data is broken down into [number] aggregated ethnic groups."
            "</div>"
            "</details>"
        ),
    )

    methodology = RDUTextAreaField(
        label="Methodology",
        validators=[RequiredForReviewValidator(message="Enter the data’s methodology")],
        hint="Explain in clear, simple language how the data was collected and processed.",
        extended_hint="_methodology.html",
    )
    related_publications = RDUTextAreaField(
        label="Related publications (optional)", extended_hint="_related_publications.html"
    )
    qmi_url = RDUURLField(label="Link to quality and methodology information")
    further_technical_information = RDUTextAreaField(label="Further technical information (optional)")

    # Edit summaries
    update_corrects_data_mistake = RDURadioField(
        label="Are you correcting something that’s factually incorrect?",
        hint="For example, in the data or commentary",
        choices=((True, "Yes"), (False, "No")),
        coerce=lambda value: None if value is None else get_bool(value),
        validators=[
            NotRequiredForMajorVersions(),
            RequiredForReviewValidator("Confirm whether this is a correction", else_optional=True),
        ],
    )
    update_corrects_measure_version = RDURadioField(
        label="In which version did the mistake first appear?",
        coerce=int,
        validators=[
            NotRequiredForMajorVersions(),
            OnlyIfUpdatingDataMistake(),
            RequiredForReviewValidator("Confirm when the mistake first appeared", else_optional=True),
        ],
    )
    external_edit_summary = RDUTextAreaField(
        label="Changes to previous version",
        validators=[RequiredForReviewValidator(message="Summarise changes to the previous version")],
        hint=(
            "If you’ve corrected the data, explain what’s changed and why. Otherwise, summarise what you’ve updated "
            "(for example, ‘Updated with the latest available data’)."
        ),
    )
    internal_edit_summary = RDUTextAreaField(
        label="Notes (for internal use - optional)",
        hint="Include any additional information someone might need if they’re working on this page in the future",
    )

    def __init__(
        self, is_minor_update: bool, sending_to_review=False, previous_minor_versions=tuple(), *args, **kwargs
    ):
        super(MeasureVersionForm, self).__init__(*args, **kwargs)

        self.is_minor_update = is_minor_update
        self.sending_to_review = sending_to_review

        # Major versions are not considered "corrections to data mistakes", and the question is not shown to end users.
        # So let's provide the default value here.
        if not self.is_minor_update:
            self.update_corrects_data_mistake.data = False

        choices = []
        geographic_choices = LowestLevelOfGeography.query.order_by("position").all()
        for choice in geographic_choices:
            if choice.description is not None:
                description = "%s %s" % (choice.name, choice.description)
                choices.append((choice.name, description))
            else:
                choices.append((choice.name, choice.name))

        self.lowest_level_of_geography_id.choices = choices

        if kwargs.get("obj", None):
            self.internal_reference.data = kwargs["obj"].measure.reference or ""

        if previous_minor_versions or kwargs.get("obj", None):
            self.update_corrects_measure_version.choices = tuple(
                [
                    (measure_version.id, measure_version.version)
                    for measure_version in (previous_minor_versions or kwargs.get("obj", None).previous_minor_versions)
                ]
            )

        else:
            self.update_corrects_measure_version.choices = tuple()

    def populate_obj(self, obj: MeasureVersion):
        super().populate_obj(obj)

        # We only want to record the related measure version if this is actually a correction
        if self.update_corrects_data_mistake.data is not True:
            obj.update_corrects_measure_version = None

    def error_items(self):
        return self.errors.items()