Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
114 commits
Select commit Hold shift + click to select a range
57b6454
redesign
stijn-uva Dec 11, 2025
fe0f53e
Merge branch 'master' into redesign
stijn-uva Apr 22, 2026
936299d
Handle visible parameter filtering in function
stijn-uva Apr 23, 2026
90048bc
Tinkering with analysis tree styling
stijn-uva Apr 23, 2026
374e036
update
stijn-uva Apr 30, 2026
655860b
update
stijn-uva Apr 30, 2026
ccce689
Suggestions, implemented
stijn-uva May 1, 2026
da9d351
Merge branch 'master' into redesign
stijn-uva May 1, 2026
362d848
Add icons
stijn-uva May 11, 2026
6ea01eb
Buttons + plain pages
stijn-uva May 11, 2026
653fa9e
Data source icons
stijn-uva Jun 25, 2026
bf87688
Rework for dataset overview page
stijn-uva Jun 25, 2026
25fa33b
Merge branch 'master' into redesign
dale-wahl Jun 30, 2026
c09ad4d
Add icon to 'dataset filtered from' status
stijn-uva Jun 30, 2026
52393f1
feat: import htmx and alpine
sal-uva Jun 30, 2026
71e2b0f
feat: start of processor slideout menu
sal-uva Jun 30, 2026
368e67c
Merge branch 'redesign' of https://github.com/digitalmethodsinitiativ…
sal-uva Jun 30, 2026
8327b03
comment out reference to route that doesn't exist (yet?)
stijn-uva Jul 1, 2026
880e332
Change 'run processor' symbol, add whitespace around curve
stijn-uva Jul 1, 2026
79682e0
"New!" notice in timeline on left
stijn-uva Jul 1, 2026
ad3d895
Add outline to 'Run processor' curve
stijn-uva Jul 1, 2026
634d6c0
Processor catalogue & Output class (#614)
dale-wahl Jul 1, 2026
869358b
Add links to layout
stijn-uva Jul 1, 2026
e82961d
Add icons to some additional processors
stijn-uva Jul 1, 2026
a369fa9
And yet more icons
stijn-uva Jul 1, 2026
e930f1a
feat: fix slideout animation and header
sal-uva Jul 2, 2026
be70aa2
Merge remote-tracking branch 'origin/redesign' into redesign
sal-uva Jul 2, 2026
129df3c
.button-like on a/button instead of wrapper
stijn-uva Jul 2, 2026
228a882
Merge branch 'redesign' of https://github.com/digitalmethodsinitiativ…
stijn-uva Jul 2, 2026
22294a9
Rudimentary 'new!' marker on datasets in timeline
stijn-uva Jul 2, 2026
e4b70c4
Only show 'run processor' button if processors available
stijn-uva Jul 2, 2026
ae627f6
That wasn't supposed to be a button
stijn-uva Jul 2, 2026
22dff83
Status badge always on own line
stijn-uva Jul 2, 2026
1cdac60
Fix asterisk emblem spacing
stijn-uva Jul 2, 2026
f8da03a
Put number of items in status badge
stijn-uva Jul 2, 2026
f75e713
Fix dataset status badge if width exceeds row width
stijn-uva Jul 2, 2026
51ea902
feat: fix slideout animation and header
sal-uva Jul 2, 2026
a49c19c
Merge branch 'redesign' of https://github.com/digitalmethodsinitiativ…
sal-uva Jul 2, 2026
4fab2ba
add route for processor-catalogue/<processor-type>. Fix load order an…
dale-wahl Jul 2, 2026
c19cd02
Tooltips
stijn-uva Jul 2, 2026
96d5a2e
Merge branch 'redesign' of https://github.com/digitalmethodsinitiativ…
stijn-uva Jul 2, 2026
266d67f
Merge branch 'master' into redesign
dale-wahl Jul 2, 2026
c48307f
Use directive value to set tooltip orientation
stijn-uva Jul 2, 2026
327c80f
feat: style the module-cards and processor grid, implement search, an…
sal-uva Jul 2, 2026
e1ac6b8
Merge branch 'redesign' of https://github.com/digitalmethodsinitiativ…
sal-uva Jul 2, 2026
ccdf4db
Fix login link
stijn-uva Jul 3, 2026
44f0a70
feat: tags and headers for processors in the slideout menu
sal-uva Jul 3, 2026
9061db4
Merge branch 'redesign' of https://github.com/digitalmethodsinitiativ…
sal-uva Jul 3, 2026
4b7509b
feat: make htmx and alpine update processor running; category tag sty…
sal-uva Jul 3, 2026
7018712
Button improvements
stijn-uva Jul 3, 2026
c054193
Merge branch 'redesign' of https://github.com/digitalmethodsinitiativ…
stijn-uva Jul 3, 2026
2394d9a
feat: more components
sal-uva Jul 3, 2026
0b7684f
Merge remote-tracking branch 'origin/redesign' into redesign
sal-uva Jul 3, 2026
ff78ef4
Description enhance (#617)
dale-wahl Jul 3, 2026
e316682
Fix in-progress spinner
stijn-uva Jul 3, 2026
3fc2030
Remove 'emphasis' class
stijn-uva Jul 3, 2026
6458880
Merge branch 'redesign' of https://github.com/digitalmethodsinitiativ…
stijn-uva Jul 3, 2026
30c6cec
Update Font Awesome to 7.3.0
stijn-uva Jul 3, 2026
855adc2
Fix 'replace text' icon
stijn-uva Jul 3, 2026
cbab5bd
Update CSS font references for FA7
stijn-uva Jul 3, 2026
5381d92
The future sound of progress
stijn-uva Jul 3, 2026
0ad6fce
add tags, info, and warnings to processor catalogue
dale-wahl Jul 3, 2026
9b4c10d
Fix base font
stijn-uva Jul 6, 2026
5c34f28
feat: .button-secondary
sal-uva Jul 6, 2026
c21b6b8
feat: remove comments in spinner.html
sal-uva Jul 6, 2026
d676476
feat: remove `is_local` and `is_static` datasource attributes and views
sal-uva Jul 6, 2026
6e34abf
feat: processor-catalogue -> module-catalog
sal-uva Jul 6, 2026
a974a85
feat: moving around of module action li styling for button-secondary
sal-uva Jul 6, 2026
3f003d8
feat: homogenize userinputs across forms (dataset creation, processor…
sal-uva Jul 6, 2026
00c57a9
feat: components for all userinputs
sal-uva Jul 6, 2026
8a13ba7
feat: tooltip for dataset action buttons
sal-uva Jul 6, 2026
5f4d9ea
chore: remove processor_map references
sal-uva Jul 6, 2026
3a40cef
featL button-secondary may be disabled
sal-uva Jul 6, 2026
188289c
feat: new datasource-select (should still be modified on results.html)
sal-uva Jul 6, 2026
4a1efa4
feat: helper endpoint for default dataset settings. Are now dynamical…
sal-uva Jul 6, 2026
6d0d2b9
feat: styling for p a tags
sal-uva Jul 6, 2026
0616a44
add info/warning to processors; update yellow-light var
dale-wahl Jul 7, 2026
c227bf1
guard for deprecated datasets (some of us may have all sorts of archa…
dale-wahl Jul 7, 2026
105e9b6
feat: create-dataset css
sal-uva Jul 7, 2026
8a2c9d3
feat: styling of checkboxes, dropdowns, create-dataset form, toggle l…
sal-uva Jul 7, 2026
6b4fe9c
feat: rudimentary table styling
sal-uva Jul 7, 2026
b502a0f
feat: move notices out of processor-card, add colours
sal-uva Jul 7, 2026
492ec59
feat: no blue spinner!!
sal-uva Jul 7, 2026
f548f9e
feat: start of refactoring preview: load in all but gephi lite and ht…
sal-uva Jul 7, 2026
cd4f35e
feat: below is probably better....
sal-uva Jul 7, 2026
2d0d336
fix: finish renaming processor catalogue to them infinitely inferior …
dale-wahl Jul 23, 2026
037c1f9
feat: add some rudimentary metro lines to the processor preview tree.…
sal-uva Jul 29, 2026
e8ed192
feat: clean up the tree for processor preview
sal-uva Jul 30, 2026
e862587
feat: new preview button and slideout features across results, top da…
sal-uva Jul 30, 2026
4b310c1
fix: wrap li nav header buttons in a instead of other way around
sal-uva Jul 31, 2026
e660b84
fix: touch up processor preview tree a bit
sal-uva Jul 31, 2026
83dca62
fix: forgotten preview files
sal-uva Jul 31, 2026
6aa14c6
feat: link to new data source and processor templates to be added as …
sal-uva Jul 31, 2026
81243f6
feat:
sal-uva Jul 31, 2026
32de565
fix: homogenize request string
sal-uva Jul 31, 2026
69d56cd
fix: ruffff
sal-uva Jul 31, 2026
fec4fe2
feat:
sal-uva Aug 3, 2026
b1e12ce
feat: new text toggle button
sal-uva Aug 3, 2026
a0c672f
feat: first steps to transfer explorer to redesign
sal-uva Aug 3, 2026
1bdc25f
fix: better check for map_item in template filers
sal-uva Aug 4, 2026
df96b44
feat: style missing preview data
sal-uva Aug 4, 2026
da1f787
feat: popup
sal-uva Aug 4, 2026
f0f968e
feat: annotation fields editor changes; homogenize and changes inputs…
sal-uva Aug 4, 2026
dd9ff4e
feat:
sal-uva Aug 4, 2026
da0af8e
feat: make input styling more universal, retouch results.html filter nav
sal-uva Aug 5, 2026
e51ef82
feat: add 'variant' to external data sources like webjutter and rende…
sal-uva Aug 5, 2026
54e57a2
feat: misc annotation styling changes
sal-uva Aug 5, 2026
991cf78
feat: remove 'Search' category for data sources and add more specific…
sal-uva Aug 5, 2026
71ee8cb
fix: center fa-spin in warning button on annotation saving
sal-uva Aug 6, 2026
65915b2
feat: remove unneccesary explorer settings
sal-uva Aug 6, 2026
a93cc34
fix: annotation sorting
sal-uva Aug 6, 2026
873ffb7
feat: watch for new annotations made by processors, give max length t…
sal-uva Aug 6, 2026
3825bbc
feat: watch for new annotations made by processors, give max length t…
sal-uva Aug 6, 2026
58e7028
fix: you're not allowed to process/explore empty datasets
sal-uva Aug 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
100 changes: 100 additions & 0 deletions .github/ISSUE_TEMPLATE/datasource_request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
name: Data source request
description: Request a new data source for 4CAT.
title: "[Data source] "
labels: ["module-request", "data source"]
body:
- type: markdown
attributes:
value: |
Before you start:

- Check the [list of available data sources](https://github.com/digitalmethodsinitiative/4cat/wiki) and the open
[data source requests](https://github.com/digitalmethodsinitiative/4cat/issues?q=is%3Aissue+label%3A%22data+source%22)
first.
- Request new [Zeeschuimer data sources on its own GitHub]
(https://github.com/digitalmethodsinitiative/zeeschuimer) instead of here.

- type: input
id: datasource
attributes:
label: Data source name
description: The name of the datasource (platform, upload, etc) you want to collect data from.
placeholder: e.g. 'BlueSky'
validations:
required: true

- type: input
id: datasource_url
attributes:
label: URL of data source
placeholder: https://...
validations:
required: false

- type: dropdown
id: access
attributes:
label: How is the data reachable?
description: Your best guess is fine — say so in the details below if you are unsure.
options:
- It has a documented public API
- It has an API, but it needs approval, an account, or payment
- No API — the data is only visible in the browser
- It offers a bulk export or data archive
- I don't know
validations:
required: true

- type: textarea
id: access_details
attributes:
label: API documentation and access details
description: >
Link to the API documentation if there is any. Does it need credentials, an approved research application, or a
paid plan? Are there rate limits or caps you know of? If there is no API, describe how you currently get at the
data.
validations:
required: false

- type: textarea
id: items
attributes:
label: What should one item in the dataset be?
description: >
4CAT datasets are tables of items. Say what a single item should be for this platform (a post, a comment, a
video, a profile, …) and which fields matter to you — timestamp, author, body text, engagement counts, media
URLs, and so on.
validations:
required: true

- type: textarea
id: use_case
attributes:
label: What would you use this for?
description: >
A sentence or two on the research you have in mind. This helps us judge which fields and query options actually
need to exist.
validations:
required: true

- type: textarea
id: sample
attributes:
label: Example data
description: >
If you can share a small sample of the raw data (an API response, an exported file), paste or attach it here.
This is the single most useful thing you can add. Remove anything personal or sensitive first.
validations:
required: false

- type: checkboxes
id: acknowledgements
attributes:
label: Before submitting
options:
- label: >
I have considered whether collecting this data is compatible with the platform's terms of service and with
the privacy of the people in it, and I am not asking for a way to circumvent access restrictions.
required: true
- label: I searched the existing issues and this data source has not been requested yet.
required: true
92 changes: 92 additions & 0 deletions .github/ISSUE_TEMPLATE/processor_request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
name: Processor request
description: Ask for a new analysis step that can be run on a 4CAT dataset.
title: "[Processor] "
labels: ["module-request", "processor"]
body:
- type: markdown
attributes:
value: |
Before you start:

- Check the [list of available processors](https://github.com/digitalmethodsinitiative/4cat/wiki/Available-processors)
and the open [processor requests](https://github.com/digitalmethodsinitiative/4cat/issues?q=is%3Aissue+label%3Aprocessor)
first — it may already exist.
- Processors take one dataset and produce another. If what you want is a different way of *collecting* data,
open a data source request instead.
- If you want to write it yourself, see
[How to make a processor](https://github.com/digitalmethodsinitiative/4cat/wiki/How-to-make-a-processor). It
does not have to live in 4CAT itself — processors can also be distributed as an extension.
- The clearer the input and output below, the more likely someone can pick this up and build it.

- type: textarea
id: what
attributes:
label: What should it do?
description: >
One or two sentences, starting with a verb — the way a processor describes itself in the interface.
placeholder: e.g. Calculate the lexical diversity of each post and add it as a column.
validations:
required: true

- type: textarea
id: input
attributes:
label: What should it run on?
description: >
Which datasets should this be available for — all csv datasets, any dataset with text, only datasets from a
specific data source, only the output of another processor? Name that processor if so.
validations:
required: true

- type: textarea
id: output
attributes:
label: What should it produce?
description: >
The output format (CSV, NDJSON, an image, a network file, …) and what is in it. For a table, list the columns
and what one row represents.
validations:
required: true

- type: textarea
id: options
attributes:
label: Options
description: >
Anything the user should be able to configure before running it — thresholds, columns to work on, a language, a
model to use. Include sensible defaults if you have them in mind.
validations:
required: false

- type: textarea
id: reference
attributes:
label: Reference implementation
description: >
A paper, method, library, script, or an equivalent feature in another tool. If there is code that already does
this, linking to it is the most useful thing you can add.
validations:
required: false

- type: dropdown
id: requirements
attributes:
label: Does it need anything beyond plain Python?
options:
- "No — it can be computed from the dataset itself"
- "A Python library that 4CAT does not ship yet"
- "A machine learning model (running locally)"
- "A GPU"
- "An external API or paid service"
- "I don't know"
validations:
required: true

- type: textarea
id: use_case
attributes:
label: What would you use this for?
description: >
A sentence or two on the research you have in mind. This helps us get the output shape right the first time.
validations:
required: true
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ Invocation (the plain `docker compose` command uses `docker-compose.yml`):
- Define reusable Jinja2 components when patterns emerge, but avoid over-engineering for future reuse.
- Views are organized by concern in `webtool/views/`. API endpoints are in `api_tool.py` and `api_standalone.py`.
- Static assets go in `webtool/static/`; templates in `webtool/templates/`.
- The project uses htmx `4.0.0-beta*`.

## Testing Expectations
- Run tests with `pytest` from the repo root. Config is in `pytest.ini`.
Expand Down
149 changes: 141 additions & 8 deletions backend/lib/processor.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Basic post-processor worker - should be inherited by workers to post-process results
"""
from dataclasses import dataclass, field
import traceback
import inspect as py_inspect
import zipfile
Expand Down Expand Up @@ -30,6 +31,55 @@
# Shared instance for the legacy default `compatibility`
_DEFAULT_COMPATIBILITY = Compatibility(top_dataset_only=True)

@dataclass
class ProcessorDescription:
"""
A processor's user-facing description: the information shown about it in
the web interface. A processor can declare one of these directly (as its
`description` attribute) or declare the individual attributes; either way
the result is available via `Processor.get_description()`.
"""
title: str
description: str
category: str = "" # for backwards compatability
tags: typing.List[str] = field(default_factory=list)
references: typing.List[str] = field(default_factory=list)
info: typing.List[str] = field(default_factory=list)
warnings: typing.List[str] = field(default_factory=list)
icon: str = ""

def __post_init__(self):
if self.category:
self.category = self.category[0].upper() + self.category[1:]
self.tags = [tag.strip() for tag in self.tags]

# `category` is kept as the first entry of `tags` (as its lower-case
# tag form), so 4CAT can move to tags (which allow several per processor
# and can be filtered on) while `category` keeps working. Derive
# whichever is missing; when both are given, make sure the category
# leads the tag list.
category_tag = self.category.lower()
if self.category and not self.tags:
self.tags = [category_tag]
elif self.tags and not self.category:
self.category = self.tags[0][0].upper() + self.tags[0][1:]
elif self.category and self.tags:
self.tags = [category_tag] + [tag for tag in self.tags if tag != category_tag]


class _DescriptionField:
"""
Exposes one ProcessorDescription field as an attribute on the processor,
e.g. `Processor.title`. A plain `property` only runs on instance access;
this descriptor also runs on class access (`owner` is the class in both
cases), so `Processor.title` and `self.title` both return the value from
the processor's ProcessorDescription.
"""
def __init__(self, name):
self.name = name

def __get__(self, obj, owner):
return getattr(owner._processor_description, self.name)

class BasicProcessor(FourcatModule, BasicWorker, metaclass=abc.ABCMeta):
"""
Expand Down Expand Up @@ -81,11 +131,30 @@ def is_compatible_with(cls, module=None, config=None):
#: The file that is being processed
source_file = None

#: Processor description, which will be displayed in the web interface
description = "No description available"

#: Category identifier, used to group processors in the web interface
category = "Other"
#: The processor's user-facing description (title, category, description
#: text and references) as a single object. A processor may set this
#: directly, or set the individual attributes below; both are normalised
#: into `_processor_description` when the class is defined.
_processor_description = ProcessorDescription(
title="",
description="No description available",
references=[],
info=[],
warnings=[],
icon=""
)

#: Title, category, description text and references, read from the
#: processor's ProcessorDescription. Defined as descriptors so that both
#: `Processor.title` and `self.title` resolve to the stored value.
title = _DescriptionField("title")
category = _DescriptionField("category")
description = _DescriptionField("description")
references = _DescriptionField("references")
info = _DescriptionField("info")
warnings = _DescriptionField("warnings")
tags = _DescriptionField("tags")
icon = _DescriptionField("icon")

#: Extension of the file created by the processor
extension = "csv"
Expand Down Expand Up @@ -991,6 +1060,18 @@ def _validate_map_item_post_run(self):
except Exception:
pass

@classmethod
def get_repo_link(cls, config):
"""
Get a link to the processor's source code repository

:param ConfigManager config: Configuration reader
:return str: URL to the processor's source code repository
"""
repo_url = config.get("4cat.github_url")
path = cls.filepath.replace("\\", "/").lstrip("/")
return f"{repo_url.rstrip('/')}/blob/master/{path}"

@classmethod
def is_compatible_with(cls, module=None, config=None):
"""
Expand Down Expand Up @@ -1026,10 +1107,10 @@ def is_filter(cls):
Filters do not produce their own dataset but replace the source_dataset dataset
instead.

:todo: Make this a bit more robust than sniffing the processor category
:todo: Make this a bit more robust than sniffing the processor tags
:return bool:
"""
return (hasattr(cls, "category") and cls.category and "filter" in cls.category.lower()) or (hasattr(cls, "filter") and cls.filter)
return (hasattr(cls, "tags") and cls.tags and "filtering" in [tag.lower() for tag in cls.tags]) or (hasattr(cls, "filter") and cls.filter)

@classmethod
def get_options(cls, parent_dataset=None, config=None) -> dict:
Expand Down Expand Up @@ -1134,6 +1215,58 @@ def exclude_followup_processors(cls, processor_type=None):
return True
return False

def __init_subclass__(cls, **kwargs):
"""
Normalise a processor's description when its class is defined.

A processor may declare its description either as a ProcessorDescription
object (assigned to `description`) or as the individual attributes
(title, category, description, references, info, warnings, icon, tags).
Either way it is folded into a single `_processor_description` object
here, and the raw attributes are removed so the descriptors on
BasicProcessor provide access to them.
"""
super().__init_subclass__(**kwargs)

# the description inherited from the nearest ancestor (cls has none yet)
inherited = getattr(cls, "_processor_description", None)

# read the raw class-body value, bypassing the descriptor
declared = cls.__dict__.get("description")
if isinstance(declared, ProcessorDescription):
description = declared
else:
# build from the flat attributes, falling back to inherited values
# so a legacy subclass keeps anything an ancestor set
description = ProcessorDescription(
title=cls.__dict__.get("title", inherited.title),
category=cls.__dict__.get("category", inherited.category),
description=cls.__dict__.get("description", inherited.description),
references=list(cls.__dict__.get("references", inherited.references)),
info=list(cls.__dict__.get("info", inherited.info)),
warnings=list(cls.__dict__.get("warnings", inherited.warnings)),
# tags default to this class's category (see __post_init__), so
# don't inherit them — re-derive from the resolved category
tags=list(cls.__dict__.get("tags", [])),
icon=cls.__dict__.get("icon", inherited.icon),
)

# remove raw attributes so the inherited descriptors govern access
for name in ("title", "category", "description", "references", "info", "warnings", "tags", "icon"):
if name in cls.__dict__:
delattr(cls, name)

cls._processor_description = description

@classmethod
def get_description(cls):
"""
Get the processor's user-facing description

:return ProcessorDescription: Description of this processor
"""
return cls._processor_description

@abc.abstractmethod
def process(self):
"""
Expand Down Expand Up @@ -1162,4 +1295,4 @@ def is_preset():

:return: False
"""
return False
return False
Loading
Loading