+
++ {{ dataset.get_label() }} - Explorer +
+ {{ key }} + +
+
+ -
+
+ {% if warning %}
+
- Annotations +
- + Edit fields + +
- Show annotations +
- Save annotations
+
+
+ Annotations are also saved automatically after editing. Use Download csv on the dataset overview page to download this dataset with annotations as new columns. +
+
+ - Edit annotation fields + {% include "explorer/annotations-editor.html" %} +
- Sort posts by +
- + + + +
- Now showing +
- + {% if not key and has_database %} + {{ post_count }} posts from {{ datasource }}. + {% else %} + Posts {{ offset + 1 }}—{{ post_count if (offset + posts_per_page) > post_count else (offset + posts_per_page) }} ({{ post_count }} total). + {% endif %} + +
{{ warning }}
+ {% endif %}
+ {% if key and post_count > max_posts %}
+ Large dataset - only showing the first {{ max_posts }} posts. Use filter processors to limit the dataset.
+ {% set post_count = max_posts %}
+ {% endif %}
+
+
+
+ -
+
+
+
+
+
+
+
+ {% for poll_answer in post["poll_answers"].split(",") %} +- {{ poll_answer }}
+ {% endfor %}
+
+{{ post.get("body_ask").split("\n")[block_counts.ask] | markdown | social_mediafy(datasource='tumblr') | safe }}
+ {% if end_ask_block %} ++ {% for tag in post["tags"].split(",") %} +- #{{ tag }}
+ {% endfor %}
+
+- {% for post in posts %} - {% include "explorer/post.html" %} - {% endfor %} -
- {% include "explorer/nav-pages.html" %} +{% endif %} + + ++ {% for post in posts %} + {% set post_index = loop.index - 1 %} + {% include "explorer/post.html" %} + {% endfor %} +
+Rendered by 4CAT
\ No newline at end of file diff --git a/webtool/templates/explorer/header.html b/webtool/templates/explorer/header.html deleted file mode 100644 index f700a10a4..000000000 --- a/webtool/templates/explorer/header.html +++ /dev/null @@ -1,54 +0,0 @@ -- - Return to dataset - - - - 4CAT Explorer (beta){% if parameters and parameters.get("label") %} • {{ parameters.get("label") }}{% elif thread %} • {{ thread }}{% endif %} - -
-{{ key }} -Large dataset - only showing the first {{ max_posts }} posts. Use filter processors to limit the dataset.
- {% set post_count = max_posts %} - {% endif %} -Showing posts {{ offset + 1 }} - {{ post_count if (offset + limit) > post_count else (offset + limit) }} ({{ post_count }} in total).
- {% if custom_fields and custom_fields[0] == "invalid" %} -Invalid custom fields JSON - can't show posts properly ({{ custom_fields[1] }}).
- {% endif %} - {% if custom_fields and 'sort_options' in custom_fields %} -Sort posts by: - -
-Showing {{ post_count }} posts from {{ datasource }}/{{ board }} thread {{ thread }}.
-Note that the archived posts may not be complete.
- {% endif %} - -+ {% if annotation.author_original %} + Created by {% if annotation.by_processor %} processor{% endif %} {% if annotation.author_original %}{{ annotation.author_original }}{% endif %} + {% if annotation.timestamp_created %} + on {{ annotation.timestamp_created }} + {% endif %} + {% endif %} + {% if annotation.author != annotation.author_original or annotation.timestamp != annotation.timestamp_created %} + Edited by{% if annotation.by_processor %} processor{% endif %} {% if annotation.author %}{{ annotation.author }}{% endif %}{% if annotation.timestamp %} on {{ annotation.timestamp }}{% endif %} + {% endif %} + {# Extra stuff we may want to display #} + {% if annotation.metadata %} + {% set metadata = annotation.metadata | fromjson %} + {% if metadata.get("processor-parameters") %} + + {% for parameter, input_value in metadata["processor-parameters"].items() %} + {{ parameter }}={{ input_value }} + {% endfor %} + + {% endif %} + {% endif %} +
+ {% endif %} + + {# Store some invisible data here to we can retrieve in with JS #} +{{ dataset.get_label() }}
{% if not dataset.is_finished() or dataset.num_rows == 0 %}{% include "components/result-status.html" %}
{% else %} - {{ dataset.get_results_path()|filesize }}, {{ dataset.result_file.split(".")[-1] }} + {% if dataset.get_results_path().exists() %} + {% if dataset.get_own_processor().map_item or dataset.get_annotation_fields() %} + Download csv + {% else %} + Download {{ dataset.result_file.split(".")[-1] }} + {% endif %} + {% endif %} {% endif %}") - return post - -def convert_markdown(post): - post["body"] = post.get("body", "").replace("\n", "\n\n").replace(">", ">").replace("] (", "](") - post["body"] = markdown2.markdown(post.get("body", ""), extras=["nofollow","target-blank-links"]) - return post diff --git a/webtool/views/views_admin.py b/webtool/views/views_admin.py index 9e09c9f06..78b028be3 100644 --- a/webtool/views/views_admin.py +++ b/webtool/views/views_admin.py @@ -568,10 +568,12 @@ def manipulate_settings(): flash("Invalid settings: %s" % str(e)) all_settings = config.get_all(user=None, tags=[tag]) + options = {} changed_categories = set() - for option in sorted({*all_settings.keys(), *definition.keys()}): + + for option in {*all_settings.keys(), *definition.keys()}: tag_value = all_settings.get(option, definition.get(option, {}).get("default")) global_value = global_settings.get(option, definition.get(option, {}).get("default")) is_changed = tag and global_value != tag_value @@ -613,7 +615,16 @@ def manipulate_settings(): changed_categories.add(option.split(".")[0]) tab = "" if not request.form.get("current-tab") else request.form.get("current-tab") - options = {k: options[k] for k in sorted(options, key=lambda o: options[o]["tabname"])} + + # We are ordering the options based on how they are ordered in their dictionaries, + # and not the database order. To do so, we're adding a simple config order number + # and sort on this. + config_order = 0 + for k, v in definition.items(): + options[k]["config_order"] = config_order + config_order += 1 + + options = {k: options[k] for k in sorted(options, key=lambda o: (options[o]["tabname"], options[o].get("config_order", 0)))} # 'data sources' is one setting but we want to be able to indicate # overrides per sub-item diff --git a/webtool/views/views_dataset.py b/webtool/views/views_dataset.py index 1720b3a84..bdd86a3f0 100644 --- a/webtool/views/views_dataset.py +++ b/webtool/views/views_dataset.py @@ -179,6 +179,8 @@ def get_mapped_result(key): processor of the dataset has a method for mapping its data to CSV, then this route uses that to convert the data to CSV on the fly and serve it as such. + We also use this if there's annotation data saved. + :param str key: Dataset key """ try: @@ -190,22 +192,6 @@ def get_mapped_result(key): config.get("privileges.can_view_private_datasets") or dataset.is_accessible_by(current_user)): return error(403, error="This dataset is private.") - if dataset.get_extension() == ".csv": - # if it's already a csv, just return the existing file - return url_for("get_result", query_file=dataset.get_results_path().name) - - if not hasattr(dataset.get_own_processor(), "map_item"): - # cannot map without a mapping method - return error(404, error="File not found.") - - # Also add possibly added annotation items. - # These cannot be added to the static `map_item` function. - annotation_labels = None - annotation_fields = dataset.get_annotation_fields() - if annotation_fields: - annotation_labels = ["annotation_" + v["label"] for v in annotation_fields.values()] - annotations = dataset.get_annotations() - def map_response(): """ Yield a CSV file line by line @@ -219,10 +205,6 @@ def map_response(): for item in dataset.iterate_items(processor=dataset.get_own_processor(), warn_unmappable=False): if not writer: fieldnames = list(item.keys()) - if annotation_labels: - for label in annotation_labels: - if label not in fieldnames: - fieldnames.append(label) writer = csv.DictWriter(buffer, fieldnames=fieldnames) writer.writeheader() @@ -230,10 +212,6 @@ def map_response(): buffer.truncate(0) buffer.seek(0) - if annotation_fields: - for label in annotation_labels: - item[label] = annotations.get(item.get("id"), {}).get(label, "") - writer.writerow(item) yield buffer.getvalue() buffer.truncate(0) @@ -430,7 +408,7 @@ def show_result(key): datasources = fourcat_modules.datasources datasource_expiration = config.get("datasources.expiration", {}).get(datasource, {}) expires_datasource = False - can_unexpire = ((config.get('expire.allow_optout') and \ + can_unexpire = ((config.get("expire.allow_optout") and \ datasource_expiration.get("allow_optout", True)) or datasource_expiration.get("allow_optout", False)) \ and (current_user.is_admin or dataset.is_accessible_by(current_user, "owner")) @@ -444,6 +422,8 @@ def show_result(key): elif dataset.parameters.get("expires-after"): timestamp_expires = dataset.parameters.get("expires-after") + has_explorer = config.get("explorer.config", {}).get(datasource, {}).get("enabled", False) + # if the dataset has parameters with credentials, give user the option to # erase them has_credentials = [p for p in dataset.parameters if p.startswith("api_") and p not in ("api_type", "api_track")] @@ -456,7 +436,8 @@ def show_result(key): return render_template(template, dataset=dataset, parent_key=dataset.key, processors=fourcat_modules.processors, is_processor_running=is_processor_running, messages=get_flashed_messages(), is_favourite=is_favourite, timestamp_expires=timestamp_expires, has_credentials=has_credentials, - expires_by_datasource=expires_datasource, can_unexpire=can_unexpire, datasources=datasources) + expires_by_datasource=expires_datasource, can_unexpire=can_unexpire, has_explorer=has_explorer, + datasources=datasources) @app.route('/results/