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")
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()