# Mastodon, Teil 2: Toots filtern

Quelle: https://dev.inf.zone/exercises/11/mastodon-2/

## Aufgabe

**Auf einen Blick**

- **Was:** Sie schreiben Trigger-Klassen, die prüfen, ob ein Toot ein bestimmtes Kriterium erfüllt, und filtern damit die Toots aus Teil 1.
- **Datei:** weiter in `mastodon_oop.py` aus [Teil 1](https://dev.inf.zone/exercises/11/mastodon/). `Toot` und `load` müssen dort schon funktionieren.
- **Ihre Arbeit:** die zehn Trigger-Klassen im Gerüst (`MediaTrigger`, `ImageMediaTrigger`, `VideoMediaTrigger`, `PhraseTrigger`, `TimeTrigger`, `BeforeTrigger`, `AfterTrigger`, `NotTrigger`, `AndTrigger`, `OrTrigger`), die Funktion `filter_toots` und das Hauptprogramm unter `if __name__ == '__main__':`. Die Superklasse `Trigger` ist fertig.
- **Am Ende:** `python mastodon_oop.py` lädt Toots und gibt nur die aus, auf die alle Ihre Trigger zutreffen.

### So funktioniert ein Trigger

1. **Prüfen:** Ein Trigger ist ein Objekt mit der Methode `evaluate(self, toot)`. Sie bekommt ein `Toot`-Objekt und gibt `True` zurück, wenn der Toot das Kriterium erfüllt – man sagt: Der Trigger löst aus. Sonst gibt sie `False` zurück.
2. **Erben:** Alle Trigger erben von der Superklasse `Trigger`. Deren `evaluate` löst nur einen Fehler (`NotImplementedError`) aus. Jede Unterklasse, die Sie auf Toots anwenden, muss `evaluate` also selbst überschreiben.
3. **Einfache Trigger** prüfen eine Eigenschaft des Toots: Medienanhänge, eine Phrase im Text oder die Veröffentlichungszeit. Was genau sie prüfen, etwa welche Phrase, bekommen sie beim Erzeugen übergeben.
4. **Kombinierte Trigger** (`NotTrigger`, `AndTrigger`, `OrTrigger`) bekommen beim Erzeugen andere Trigger übergeben. Sie rufen deren `evaluate` auf und verknüpfen die Ergebnisse mit `not`, `and` bzw. `or`.
5. **Filtern:** `filter_toots` behält von einer Liste von Toots nur die, bei denen jeder Trigger aus einer Liste von Triggern auslöst.

**Beispiel** mit zwei Toots und dem kombinierten Trigger `AndTrigger(PhraseTrigger("python"), NotTrigger(ImageMediaTrigger()))` – „erwähnt Python und hat kein Bild“:

|                                    | Toot A                    | Toot B                           |
| ---------------------------------- | ------------------------- | -------------------------------- |
| `content`                          | `Heute lerne ich Python!` | `Mein Python-Setup, siehe Foto.` |
| `media`                            | keine                     | ein Bild (`"type"` ist `"image"`) |
| `pubdate`                          | 10.1.2026, 12:00 Uhr UTC  | 10.1.2026, 14:00 Uhr UTC         |
| `PhraseTrigger("python")`          | `True`                    | `True`                           |
| `ImageMediaTrigger()`              | `False`                   | `True`                           |
| `NotTrigger(ImageMediaTrigger())`  | `True`                    | `False`                          |
| `AndTrigger(…)`                    | `True`                    | `False`                          |

`filter_toots` mit beiden Toots und diesem Trigger gibt also nur Toot A zurück.

Zeit-Trigger rechnen mit Zeitzonen: `BeforeTrigger("2026-01-10 08:00:00")` meint 8 Uhr in der voreingestellten Zeitzone EST (Ostküste der USA, UTC−5), also 13:00 Uhr UTC. Er löst bei Toot A aus, bei Toot B nicht.

## Spezifikation

Im Gerüst steht über jeder Klasse, von welcher Klasse sie erbt (etwa `# MediaTrigger is a subclass of Trigger`). Tragen Sie diese Superklasse in die Klassenzeile ein und ergänzen Sie die Parameter der Methoden.

### Schritt 1: Medien-Trigger

- `MediaTrigger` erbt von `Trigger`. `evaluate` gibt `True` zurück, wenn der Toot Medienanhänge hat, also `toot.media` nicht leer ist.
- `ImageMediaTrigger` erbt von `MediaTrigger`. `evaluate` gibt `True` zurück, wenn der Toot Medienanhänge hat und mindestens einer davon ein Bild ist (`"type"` ist `"image"`). Nutzen Sie für die erste Bedingung das `evaluate` der Superklasse.
- `VideoMediaTrigger` erbt von `MediaTrigger` und funktioniert genauso für Videos (`"type"` ist `"video"`).

Mastodon kennt noch weitere Medientypen wie GIFs oder Audio; dafür brauchen Sie keine eigenen Trigger.

### Schritt 2: `PhraseTrigger`

`PhraseTrigger` erbt von `Trigger` und löst aus, wenn der Text eines Toots eine bestimmte Phrase (ein Wort oder mehrere) enthält.

- `__init__(self, phrase)` speichert die Phrase als Attribut.
- `evaluate` wandelt den Text (`toot.content`) und die Phrase in Kleinbuchstaben um, entfernt aus dem Text die Satzzeichen `.`, `,`, `!` und `?` (ersetzt sie durch den leeren String) und gibt `True` zurück, wenn die Phrase dann im Text enthalten ist.

So löst `PhraseTrigger("Python")` bei `Heute lerne ich Python!` aus, `PhraseTrigger("Java")` nicht.

### Schritt 3: Zeit-Trigger

- `TimeTrigger` erbt von `Trigger`. `__init__(self, ptime, timezone="EST")` bekommt die Auslösezeit `ptime` als String im Format `"YYYY-MM-DD HH:MM:SS"` und optional eine Zeitzone; ohne Angabe gilt `"EST"`. Wandeln Sie `ptime` in ein `datetime`-Objekt um, versehen Sie es mit der Bibliothek `pytz` mit dieser Zeitzone und speichern Sie das Ergebnis als Attribut. Die Uhrzeit bleibt dabei unverändert: `"2026-01-10 08:00:00"` bedeutet 8 Uhr EST. `TimeTrigger` ist eine abstrakte Klasse – sie dient nur als Superklasse und braucht kein `evaluate`.
- `BeforeTrigger` erbt von `TimeTrigger` und damit auch dessen `__init__`. `evaluate` gibt `True` zurück, wenn der Toot (`toot.pubdate`) strikt vor der Auslösezeit veröffentlicht wurde.
- `AfterTrigger` erbt ebenso von `TimeTrigger` und gibt `True` zurück, wenn der Toot strikt nach der Auslösezeit veröffentlicht wurde.

### Schritt 4: Trigger kombinieren

Alle drei erben von `Trigger`, speichern die übergebenen Trigger als Attribute und werten sie in `evaluate` für den jeweiligen Toot aus:

- `NotTrigger(trigger)` kehrt das Ergebnis um: `True`, wenn `trigger` nicht auslöst, sonst `False`.
- `AndTrigger(trigger1, trigger2)` gibt nur dann `True` zurück, wenn beide Trigger auslösen.
- `OrTrigger(trigger1, trigger2)` gibt `True` zurück, wenn mindestens einer der beiden auslöst.

### Schritt 5: `filter_toots`

`filter_toots(toots, trigger_list)` bekommt eine Liste von Toots und eine Liste von Triggern. Sie gibt eine Liste der Toots zurück, bei denen jeder Trigger aus `trigger_list` auslöst (Aufruf von `evaluate`). Bilden Sie diese Liste mit einer List Comprehension.

### Schritt 6: Toots filtern und ausgeben

Im Abschnitt `if __name__ == '__main__':`

1. laden Sie Toots mit `load`,
2. erzeugen Sie einige Trigger und Kombinationen davon,
3. fassen Sie die Trigger, die gelten sollen, in einer Liste zusammen,
4. rufen Sie `filter_toots` mit den Toots und dieser Liste auf,
5. geben Sie die gefilterten Toots mit `print` aus (dabei wird die `__str__`-Methode Ihrer Klasse `Toot` verwendet).

Achten Sie darauf, dass sich Ihre Trigger nicht gegenseitig ausschließen. Ein `AndTrigger` aus `BeforeTrigger("2024-01-01 00:00:00")` und `AfterTrigger("2024-01-01 00:00:00")` etwa lässt keinen Toot durch.

## Hilfestellung

Klicken Sie auf die folgenden Tipps, um einige Ratschläge zu erhalten. Versuchen Sie aber zunächst, selbst so weit wie möglich zu kommen.

**`MediaTrigger` und `ImageMediaTrigger`**

Prüfen Sie zuerst selbst:

- Erbt `MediaTrigger` von `Trigger` und `ImageMediaTrigger` von `MediaTrigger`?
- Hat jede der beiden Klassen eine Methode `evaluate(self, toot)`?
- Prüft `evaluate`, ob `toot.media` eine leere Liste ist?

Wenn das nicht weiterhilft: `MediaTrigger` könnte so aussehen.

```python
class MediaTrigger(Trigger):
    def evaluate(self, toot):
        return bool(toot.media)
```

Bei `ImageMediaTrigger` ruft `super().evaluate(toot)` das `evaluate` von `MediaTrigger` auf. `any(…)` ist `True`, wenn mindestens ein Anhang in `toot.media` ein Bild ist:

```python
class ImageMediaTrigger(MediaTrigger):
    def evaluate(self, toot):
        if not super().evaluate(toot):
            return False
        return any(media['type'] == 'image' for media in toot.media)
```

**Auf den Typ eines Medienanhangs zugreifen**

Lassen Sie sich `toot.media` eines Toots mit Bild einmal mit `print` ausgeben. Es ist eine Liste; jeder Anhang darin ist ein Dictionary. Den Typ eines einzelnen Anhangs `anhang` erhalten Sie mit `anhang['type']`.

**Standardwert für einen Parameter**

Ein Parameter mit Standardwert wird beim Aufruf nur angegeben, wenn man einen anderen Wert will:

```python
def my_function(parameter="bar"):
    print(f"foo, {parameter}")

my_function("my value")  # gibt foo, my value aus
my_function()            # gibt foo, bar aus
```

**Zeit und Zeitzone mit `datetime` und `pytz`**

- `datetime.strptime(ptime, "%Y-%m-%d %H:%M:%S")` macht aus dem String ein `datetime`-Objekt, zunächst ohne Zeitzone.
- `pytz.timezone(timezone)` liefert ein Zeitzonen-Objekt. Dessen Methode `localize` versieht ein `datetime` ohne Zeitzone mit dieser Zeitzone und lässt die Uhrzeit, wie sie ist.
- Verwenden Sie nicht `astimezone`: Diese Methode rechnet die Uhrzeit in eine andere Zeitzone um, statt ihr eine Zeitzone zuzuordnen – aus 8 Uhr würde eine andere Uhrzeit.
- `toot.pubdate` hat bereits eine Zeitzone (UTC). Zwei `datetime`-Objekte mit Zeitzone können Sie direkt mit `<` und `>` vergleichen.

**Mein Filter lässt keinen Toot durch**

- `load` holt die neuesten Toots. Liegt Ihr Zeitfenster in der Vergangenheit, passt keiner. Lassen Sie sich `toot.pubdate` der geladenen Toots ausgeben und wählen Sie die Zeiten passend.
- Testen Sie jeden Trigger zunächst einzeln, bevor Sie Trigger kombinieren.

## Testen

Führen Sie Ihr Programm aus:

```bash
python mastodon_oop.py
```

Ändern Sie dann die Trigger und ihre Kombinationen, bis Sie jeden Trigger mindestens einmal verwendet haben, und prüfen Sie jeweils, ob genau die passenden Toots ausgegeben werden.

Welche Toots Mastodon liefert, ändert sich ständig. Sie können die Toots aus dem Beispiel oben deshalb auch selbst bauen und prüfen, ob Ihre Trigger die Werte aus der Tabelle liefern:

```python
a = Toot("Heute lerne ich Python!", "alice", ["python"],
         datetime(2026, 1, 10, 12, 0, tzinfo=pytz.utc), [])
b = Toot("Mein Python-Setup, siehe Foto.", "bob", ["python"],
         datetime(2026, 1, 10, 14, 0, tzinfo=pytz.utc), [{"type": "image"}])
trigger = AndTrigger(PhraseTrigger("python"), NotTrigger(ImageMediaTrigger()))
print(trigger.evaluate(a), trigger.evaluate(b))  # True False
print(BeforeTrigger("2026-01-10 08:00:00").evaluate(a))  # True
```

### Korrektheit

Führen Sie in Ihrem Terminal den folgenden Befehl aus, um die Korrektheit Ihrer Arbeit zu überprüfen:

```bash
check50 -l inf-zone/exercises/2026/mastodon/filter
```

### Style

Führen Sie den folgenden Befehl aus, um den Stil Ihres Codes mit `style50` zu analysieren:

```bash
style50 mastodon_oop.py
```

Wo sind Sie gut vorangekommen, wo hätten Sie sich mehr Hilfe gewünscht? Schreiben Sie es uns im Forum oder sagen Sie es in den Tutorien – das hilft uns, die Aufgabe zu verbessern.

Wie Teil 1 stammt diese Aufgabe von der FAU Erlangen-Nürnberg und steht unter der Lizenz CC BY-NC-SA 4.0 (Nachweis in [Teil 1](https://dev.inf.zone/exercises/11/mastodon/)).

## Abgeben

Geben Sie im Ordner `mastodon_oop` ab:

```bash
inf upload mastodon/filter
```

Danach sehen Sie, welche Tests Ihr Programm besteht und ob die Übung für die Bonuspunkte zählt. Wie Sie `inf` installieren und sich anmelden, steht unter [Abgeben mit `inf`](https://dev.inf.zone/faq/uebung-solutions/#inf-installieren).

## Zum Weiterlesen: Vererbung und Polymorphie

Für die Lösung der Aufgabe nicht nötig.

Beim Filtern haben Sie Polymorphie genutzt: `filter_toots` ruft für jeden Trigger `evaluate` auf, ohne zu wissen, um welche Art von Trigger es sich handelt. Welches `evaluate` ausgeführt wird, hängt von der Klasse des Objekts ab. Deshalb können Sie neue Trigger ergänzen, etwa einen für Hashtags, ohne `filter_toots` zu ändern.

`Trigger` und `TimeTrigger` sind abstrakte Klassen: Sie legen fest, was alle Unterklassen gemeinsam haben, werden aber selbst nicht auf Toots angewendet. Python bietet dafür auch ein eigenes Modul, [`abc`](https://docs.python.org/3/library/abc.html) (Abstract Base Classes). Damit meldet Python schon beim Erzeugen eines Objekts einen Fehler, wenn eine Unterklasse eine vorgeschriebene Methode nicht implementiert – nicht erst beim Aufruf.
