Rewrite aimport_json()

This commit is contained in:
2024-09-21 02:09:21 +02:00
parent e76fc28cfb
commit 2be1191a03
17 changed files with 831 additions and 395 deletions

View File

@ -1,5 +1,6 @@
{ {
"cSpell.words": [ "cSpell.words": [
"adownload",
"aimport", "aimport",
"allauth", "allauth",
"appendonly", "appendonly",

View File

@ -73,11 +73,12 @@ async def add_reward_campaign(reward_campaign: dict | None) -> None:
logger.info("Added reward campaign %s", our_reward_campaign) logger.info("Added reward campaign %s", our_reward_campaign)
async def add_drop_campaign(drop_campaign: dict | None) -> None: async def add_drop_campaign(drop_campaign: dict | None, *, local: bool) -> None:
"""Add a drop campaign to the database. """Add a drop campaign to the database.
Args: Args:
drop_campaign (dict): The drop campaign to add. drop_campaign (dict): The drop campaign to add.
local (bool): Only update status if we are scraping from the Twitch directly.
""" """
if not drop_campaign: if not drop_campaign:
return return
@ -101,7 +102,7 @@ async def add_drop_campaign(drop_campaign: dict | None) -> None:
logger.info("Added game %s", game) logger.info("Added game %s", game)
our_drop_campaign, created = await DropCampaign.objects.aupdate_or_create(twitch_id=drop_campaign["id"]) our_drop_campaign, created = await DropCampaign.objects.aupdate_or_create(twitch_id=drop_campaign["id"])
await our_drop_campaign.aimport_json(drop_campaign, game) await our_drop_campaign.aimport_json(drop_campaign, game, scraping_local_files=local)
if created: if created:
logger.info("Added drop campaign %s", our_drop_campaign.twitch_id) logger.info("Added drop campaign %s", our_drop_campaign.twitch_id)
@ -129,14 +130,14 @@ async def add_time_based_drops(drop_campaign: dict, our_drop_campaign: DropCampa
raise NotImplementedError(msg) raise NotImplementedError(msg)
our_time_based_drop, created = await TimeBasedDrop.objects.aupdate_or_create(twitch_id=time_based_drop["id"]) our_time_based_drop, created = await TimeBasedDrop.objects.aupdate_or_create(twitch_id=time_based_drop["id"])
await our_time_based_drop.aimport_json(time_based_drop, our_drop_campaign) await our_time_based_drop.aimport_json(data=time_based_drop, drop_campaign=our_drop_campaign)
if created: if created:
logger.info("Added time-based drop %s", our_time_based_drop.twitch_id) logger.info("Added time-based drop %s", our_time_based_drop.twitch_id)
if our_time_based_drop and time_based_drop.get("benefitEdges"): if our_time_based_drop and time_based_drop.get("benefitEdges"):
for benefit_edge in time_based_drop["benefitEdges"]: for benefit_edge in time_based_drop["benefitEdges"]:
benefit, created = await Benefit.objects.aupdate_or_create(twitch_id=benefit_edge["benefit"]) benefit, created = await Benefit.objects.aupdate_or_create(twitch_id=benefit_edge["benefit"]["id"])
await benefit.aimport_json(benefit_edge["benefit"], our_time_based_drop) await benefit.aimport_json(benefit_edge["benefit"], our_time_based_drop)
if created: if created:
logger.info("Added benefit %s", benefit.twitch_id) logger.info("Added benefit %s", benefit.twitch_id)
@ -194,7 +195,7 @@ async def process_json_data(num: int, campaign: dict | None, *, local: bool) ->
await add_reward_campaign(reward_campaign=reward_campaign) await add_reward_campaign(reward_campaign=reward_campaign)
if campaign.get("data", {}).get("user", {}).get("dropCampaign"): if campaign.get("data", {}).get("user", {}).get("dropCampaign"):
await add_drop_campaign(drop_campaign=campaign["data"]["user"]["dropCampaign"]) await add_drop_campaign(drop_campaign=campaign["data"]["user"]["dropCampaign"], local=local)
if campaign.get("data", {}).get("currentUser", {}).get("dropCampaigns"): if campaign.get("data", {}).get("currentUser", {}).get("dropCampaigns"):
for drop_campaign in campaign["data"]["currentUser"]["dropCampaigns"]: for drop_campaign in campaign["data"]["currentUser"]["dropCampaigns"]:

View File

@ -1,11 +1,16 @@
# Generated by Django 5.1 on 2024-09-01 22:36 # Generated by Django 5.1 on 2024-09-01 22:36
from __future__ import annotations
from typing import TYPE_CHECKING
import django.contrib.auth.models import django.contrib.auth.models
import django.contrib.auth.validators import django.contrib.auth.validators
import django.db.models.deletion import django.db.models.deletion
import django.utils.timezone import django.utils.timezone
from django.db import migrations, models from django.db import migrations, models
from django.db.migrations.operations.base import Operation
if TYPE_CHECKING:
from django.db.migrations.operations.base import Operation
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -1,7 +1,12 @@
# Generated by Django 5.1 on 2024-09-02 23:28 # Generated by Django 5.1 on 2024-09-02 23:28
from __future__ import annotations
from typing import TYPE_CHECKING
from django.db import migrations from django.db import migrations
from django.db.migrations.operations.base import Operation
if TYPE_CHECKING:
from django.db.migrations.operations.base import Operation
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -1,7 +1,12 @@
# Generated by Django 5.1 on 2024-09-07 19:19 # Generated by Django 5.1 on 2024-09-07 19:19
from __future__ import annotations
from typing import TYPE_CHECKING
from django.db import migrations from django.db import migrations
from django.db.migrations.operations.base import Operation
if TYPE_CHECKING:
from django.db.migrations.operations.base import Operation
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -1,7 +1,12 @@
# Generated by Django 5.1 on 2024-09-09 02:34 # Generated by Django 5.1 on 2024-09-09 02:34
from __future__ import annotations
from typing import TYPE_CHECKING
from django.db import migrations, models from django.db import migrations, models
from django.db.migrations.operations.base import Operation
if TYPE_CHECKING:
from django.db.migrations.operations.base import Operation
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -0,0 +1,38 @@
# Generated by Django 5.1 on 2024-09-15 19:40
from __future__ import annotations
from typing import TYPE_CHECKING
from django.db import migrations
if TYPE_CHECKING:
from django.db.migrations.operations.base import Operation
class Migration(migrations.Migration):
dependencies: list[tuple[str, str]] = [
("core", "0004_alter_dropcampaign_name_alter_game_box_art_url_and_more"),
]
operations: list[Operation] = [
migrations.AlterModelOptions(
name="benefit",
options={"ordering": ["-twitch_created_at"]},
),
migrations.AlterModelOptions(
name="dropcampaign",
options={"ordering": ["ends_at"]},
),
migrations.AlterModelOptions(
name="reward",
options={"ordering": ["-earnable_until"]},
),
migrations.AlterModelOptions(
name="rewardcampaign",
options={"ordering": ["-starts_at"]},
),
migrations.AlterModelOptions(
name="timebaseddrop",
options={"ordering": ["required_minutes_watched"]},
),
]

View File

@ -0,0 +1,50 @@
# Generated by Django 5.1.1 on 2024-09-16 19:32
from __future__ import annotations
from typing import TYPE_CHECKING
from django.db import migrations, models
import core.models
if TYPE_CHECKING:
from django.db.migrations.operations.base import Operation
class Migration(migrations.Migration):
dependencies: list[tuple[str, str]] = [
("core", "0005_alter_benefit_options_alter_dropcampaign_options_and_more"),
]
operations: list[Operation] = [
migrations.AddField(
model_name="benefit",
name="image",
field=models.ImageField(null=True, upload_to=core.models.get_benefit_image_path),
),
migrations.AddField(
model_name="dropcampaign",
name="image",
field=models.ImageField(null=True, upload_to=core.models.get_drop_campaign_image_path),
),
migrations.AddField(
model_name="game",
name="image",
field=models.ImageField(null=True, upload_to=core.models.get_game_image_path),
),
migrations.AddField(
model_name="reward",
name="banner_image",
field=models.ImageField(null=True, upload_to=core.models.get_reward_banner_image_path),
),
migrations.AddField(
model_name="reward",
name="thumbnail_image",
field=models.ImageField(null=True, upload_to=core.models.get_reward_thumbnail_image_path),
),
migrations.AddField(
model_name="rewardcampaign",
name="image",
field=models.ImageField(null=True, upload_to=core.models.get_reward_image_path),
),
]

View File

@ -0,0 +1,31 @@
# Generated by Django 5.1.1 on 2024-09-21 00:08
from __future__ import annotations
from typing import TYPE_CHECKING
from django.db import migrations, models
if TYPE_CHECKING:
from django.db.migrations.operations.base import Operation
class Migration(migrations.Migration):
dependencies: list[tuple[str, str]] = [
("core", "0006_benefit_image_dropcampaign_image_game_image_and_more"),
]
operations: list[Operation] = [
migrations.AlterModelOptions(
name="game",
options={"ordering": ["name"]},
),
migrations.AlterModelOptions(
name="owner",
options={"ordering": ["name"]},
),
migrations.AlterField(
model_name="game",
name="slug",
field=models.TextField(null=True, unique=True),
),
]

File diff suppressed because it is too large Load Diff

View File

@ -40,9 +40,15 @@ THOUSAND_SEPARATOR = " "
ROOT_URLCONF = "core.urls" ROOT_URLCONF = "core.urls"
STATIC_URL = "static/" STATIC_URL = "static/"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
STATICFILES_DIRS: list[Path] = [BASE_DIR / "static"] STATICFILES_DIRS: list[Path] = [BASE_DIR / "static"]
STATIC_ROOT: Path = BASE_DIR / "staticfiles" STATIC_ROOT: Path = BASE_DIR / "staticfiles"
STATIC_ROOT.mkdir(exist_ok=True) STATIC_ROOT.mkdir(exist_ok=True)
MEDIA_URL = "/media/"
MEDIA_ROOT: Path = DATA_DIR / "media"
MEDIA_ROOT.mkdir(exist_ok=True)
AUTH_USER_MODEL = "core.User" AUTH_USER_MODEL = "core.User"
if DEBUG: if DEBUG:
INTERNAL_IPS: list[str] = ["127.0.0.1"] INTERNAL_IPS: list[str] = ["127.0.0.1"]
@ -135,6 +141,9 @@ DATABASES = {
} }
STORAGES: dict[str, dict[str, str]] = { STORAGES: dict[str, dict[str, str]] = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": { "staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
}, },

View File

@ -1,6 +1,7 @@
{% load static %} {% load static %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" data-bs-theme="dark"> <html lang="en" data-bs-theme="dark">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
@ -13,10 +14,8 @@
<link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}"> <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
<link rel="stylesheet" href="{% static 'css/style.css' %}"> <link rel="stylesheet" href="{% static 'css/style.css' %}">
</head> </head>
<body data-bs-spy="scroll"
data-bs-target=".toc" <body data-bs-spy="scroll" data-bs-target=".toc" data-bs-offset="-200" tabindex="0">
data-bs-offset="-200"
tabindex="0">
{% include "partials/alerts.html" %} {% include "partials/alerts.html" %}
<article class="container mt-5"> <article class="container mt-5">
{% include "partials/header.html" %} {% include "partials/header.html" %}
@ -25,4 +24,5 @@
</article> </article>
<script src="{% static 'js/bootstrap.min.js' %}"></script> <script src="{% static 'js/bootstrap.min.js' %}"></script>
</body> </body>
</html> </html>

View File

@ -3,7 +3,6 @@
<div class="container"> <div class="container">
<h2>{{ game.name }}</h2> <h2>{{ game.name }}</h2>
<img src="{{ game.box_art_url }}" alt="{{ game.name }} box art" height="283" width="212"> <img src="{{ game.box_art_url }}" alt="{{ game.name }} box art" height="283" width="212">
<h3>Game Details</h3> <h3>Game Details</h3>
<table class="table table-hover table-sm table-striped" cellspacing="0"> <table class="table table-hover table-sm table-striped" cellspacing="0">
<tr> <tr>
@ -23,18 +22,17 @@
<td><a href="{{ game.box_art_url }}" target="_blank">{{ game.box_art_url }}</a></td> <td><a href="{{ game.box_art_url }}" target="_blank">{{ game.box_art_url }}</a></td>
</tr> </tr>
</table> </table>
<h3>Organization</h3> <h3>Organization</h3>
<table class="table table-hover table-sm table-striped" cellspacing="0"> <table class="table table-hover table-sm table-striped" cellspacing="0">
<tr> <tr>
{% if game.org %} {% if game.org %}
<td><a href="#">{{ game.org.name }} - <span class="text-muted">{{ game.org.pk }}</span></a></td> <td><a href="#">{{ game.org.name }} -
<span class="text-muted">{{ game.org.pk }}</span></a></td>
{% else %} {% else %}
<td>No organization associated with this game.</td> <td>No organization associated with this game.</td>
{% endif %} {% endif %}
</tr> </tr>
</table> </table>
<h3>Drop Campaigns</h3> <h3>Drop Campaigns</h3>
{% if game.drop_campaigns.all %} {% if game.drop_campaigns.all %}
{% for drop_campaign in game.drop_campaigns.all %} {% for drop_campaign in game.drop_campaigns.all %}
@ -48,18 +46,28 @@
<tr> <tr>
<td><img src="{{ drop_campaign.image_url }}" alt="{{ drop_campaign.name }} image"></td> <td><img src="{{ drop_campaign.image_url }}" alt="{{ drop_campaign.name }} image"></td>
<td> <td>
<p><strong>Status:</strong> {{ drop_campaign.status }}</p> <p><strong>Status:</strong>
<p><strong>Description:</strong> {{ drop_campaign.description }}</p> {{ drop_campaign.status }}
<p><strong>Starts at:</strong> {{ drop_campaign.starts_at }}</p> </p>
<p><strong>Ends at:</strong> {{ drop_campaign.ends_at }}</p> <p><strong>Description:</strong>
<p><strong>More details:</strong> <a href="{{ drop_campaign.details_url }}" {{ drop_campaign.description }}
target="_blank">{{ drop_campaign.details_url }}</a></p> </p>
<p><strong>Account Link:</strong> <a href="{{ drop_campaign.account_link_url }}" <p><strong>Starts at:</strong>
target="_blank">{{ drop_campaign.account_link_url }}</a></p> {{ drop_campaign.starts_at }}
</p>
<p><strong>Ends at:</strong>
{{ drop_campaign.ends_at }}
</p>
<p><strong>More details:</strong>
<a href="{{ drop_campaign.details_url }}" target="_blank">{{ drop_campaign.details_url }}</a>
</p>
<p><strong>Account Link:</strong>
<a href="{{ drop_campaign.account_link_url }}"
target="_blank">{{ drop_campaign.account_link_url }}</a>
</p>
</td> </td>
</tr> </tr>
</table> </table>
{% if drop_campaign.drops.all %} {% if drop_campaign.drops.all %}
<table class="table table-hover table-sm table-striped" cellspacing="0"> <table class="table table-hover table-sm table-striped" cellspacing="0">
<tr> <tr>
@ -75,8 +83,7 @@
<td>{{ item.name }}</td> <td>{{ item.name }}</td>
<td>{{ item.required_minutes_watched }}</td> <td>{{ item.required_minutes_watched }}</td>
{% for benefit in item.benefits.all %} {% for benefit in item.benefits.all %}
<td><img src="{{ benefit.image_url }}" alt="{{ benefit.name }} reward image" height="50" width="50"> <td><img src="{{ benefit.image_url }}" alt="{{ benefit.name }} reward image" height="50" width="50"></td>
</td>
<td>{{ benefit.name }}</td> <td>{{ benefit.name }}</td>
{% endfor %} {% endfor %}
</tr> </tr>
@ -89,6 +96,5 @@
{% else %} {% else %}
<p>No drop campaigns associated with this game.</p> <p>No drop campaigns associated with this game.</p>
{% endif %} {% endif %}
</div> </div>
{% endblock content %} {% endblock content %}

View File

@ -1,10 +1,6 @@
{% for message in messages %} {% for message in messages %}
<div class="alert alert-dismissible {{ message.tags }} fade show" <div class="alert alert-dismissible {{ message.tags }} fade show" role="alert">
role="alert"> <div>{{ message | safe }}</div>
<div>{{ message | safe }}</div> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
<button type="button" </div>
class="btn-close"
data-bs-dismiss="alert"
aria-label="Close"></button>
</div>
{% endfor %} {% endfor %}

View File

@ -5,7 +5,8 @@
<h2 class="card-title h2">Information</h2> <h2 class="card-title h2">Information</h2>
<div class="mb-3"> <div class="mb-3">
<p> <p>
This site allows users to subscribe to Twitch drops notifications. You can choose to be alerted when new drops are found on Twitch or when the drops become available for farming. This site allows users to subscribe to Twitch drops notifications. You can choose to be alerted
when new drops are found on Twitch or when the drops become available for farming.
</p> </p>
</div> </div>
</div> </div>

View File

@ -1,48 +1,50 @@
{% if webhooks %} {% if webhooks %}
<div class="card mb-4 shadow-sm" id="info-box"> <div class="card mb-4 shadow-sm" id="info-box">
<div class="row g-0"> <div class="row g-0">
<div class="col-md-10"> <div class="col-md-10">
<div class="card-body"> <div class="card-body">
<h2 class="card-title h2">Site news</h2> <h2 class="card-title h2">Site news</h2>
<div class="mt-auto"> <div class="mt-auto">
{% for webhook in webhooks %} {% for webhook in webhooks %}
<div class="mt-3"> <div class="mt-3">
<img src="{{ webhook.avatar }}?size=32" <img src="{{ webhook.avatar }}?size=32" alt="{{ webhook.name }}" class="rounded-circle"
alt="{{ webhook.name }}" height="32" width="32">
class="rounded-circle" <a href="{{ webhook.url }}" target="_blank">{{ webhook.name }}</a>
height="32" <div class="form-check form-switch">
width="32"> <input class="form-check-input" type="checkbox" id="new-drop-switch-daily">
<a href="{{ webhook.url }}" target="_blank">{{ webhook.name }}</a> <label class="form-check-label" for="new-drop-switch-daily">Daily notification of newly
<div class="form-check form-switch"> added games to TTVdrops</label>
<input class="form-check-input" type="checkbox" id="new-drop-switch-daily"> </div>
<label class="form-check-label" for="new-drop-switch-daily">Daily notification of newly added games to TTVdrops</label> <div class="form-check form-switch">
</div> <input class="form-check-input" type="checkbox" id="new-drop-switch-weekly">
<div class="form-check form-switch"> <label class="form-check-label" for="new-drop-switch-weekly">
<input class="form-check-input" type="checkbox" id="new-drop-switch-weekly"> Weekly notification of newly added games to TTVdrops
<label class="form-check-label" for="new-drop-switch-weekly"> </label>
Weekly notification of newly added games to TTVdrops </div>
</label> <br>
</div> <div class="form-check form-switch">
<br> <input class="form-check-input" type="checkbox" id="new-org-switch-daily">
<div class="form-check form-switch"> <label class="form-check-label" for="new-org-switch-daily">
<input class="form-check-input" type="checkbox" id="new-org-switch-daily"> Daily notification of newly added <abbr
<label class="form-check-label" for="new-org-switch-daily"> title="Organizations are the companies that own the games.">organizations</abbr> to
Daily notification of newly added <abbr title="Organizations are the companies that own the games.">organizations</abbr> to TTVdrops TTVdrops
</label> </label>
</div> </div>
<div class="form-check form-switch"> <div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="new-org-switch-weekly"> <input class="form-check-input" type="checkbox" id="new-org-switch-weekly">
<label class="form-check-label" for="new-org-switch-weekly"> <label class="form-check-label" for="new-org-switch-weekly">
Weekly notification of newly added <abbr title="Organizations are the companies that own the games.">organizations</abbr> to TTVdrops Weekly notification of newly added <abbr
</label> title="Organizations are the companies that own the games.">organizations</abbr> to
</div> TTVdrops
</div> </label>
{% endfor %} </div>
</div> </div>
{% endfor %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
{% else %} {% else %}
<p class="text-muted">No webhooks added yet.</p> <p class="text-muted">No webhooks added yet.</p>
{% endif %} {% endif %}

View File

@ -1,50 +1,42 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<div class="container"> <div class="container">
<h1 class="my-4">Add Discord Webhook</h1> <h1 class="my-4">Add Discord Webhook</h1>
<div class="card card-body mb-3"> <div class="card card-body mb-3">
Webhooks will be saved in a cookie and will be sent to the server when you subscribe to a drop. Webhooks will be saved in a cookie and will be sent to the server when you subscribe to a drop.
</div>
<div>
<form method="post" class="needs-validation" novalidate>
{% csrf_token %}
{{ form.non_field_errors }}
<div class="mb-3">
{{ form.webhook_url.errors }}
<label for="{{ form.webhook_url.id_for_label }}" class="form-label">{{ form.webhook_url.label }}</label>
<input type="url"
name="webhook_url"
required=""
class="form-control"
aria-describedby="id_webhook_url_helptext"
id="id_webhook_url">
<div class="form-text text-muted">{{ form.webhook_url.help_text }}</div>
</div>
<button type="submit" class="btn btn-primary">Add Webhook</button>
</form>
</div>
<h2 class="mt-5">Webhooks</h2>
{% if webhooks %}
<div class="list-group">
{% for webhook in webhooks %}
<div class="list-group-item d-flex justify-content-between align-items-center">
<span>
{% if webhook.avatar %}
<img src="https://cdn.discordapp.com/avatars/{{ webhook.id }}/a_{{ webhook.avatar }}.png"
alt="Avatar of {{ webhook.name }}"
class="rounded-circle"
height="32"
width="32">
{% endif %}
<a href="https://discord.com/api/webhooks/{{ webhook.id }}/{{ webhook.token }}"
target="_blank"
class="text-decoration-none">{{ webhook.name }}</a>
</span>
</div>
{% endfor %}
</div>
{% else %}
<div class="alert alert-info">No webhooks added</div>
{% endif %}
</div> </div>
<div>
<form method="post" class="needs-validation" novalidate>
{% csrf_token %}
{{ form.non_field_errors }}
<div class="mb-3">
{{ form.webhook_url.errors }}
<label for="{{ form.webhook_url.id_for_label }}" class="form-label">{{ form.webhook_url.label }}</label>
<input type="url" name="webhook_url" required="" class="form-control"
aria-describedby="id_webhook_url_helptext" id="id_webhook_url">
<div class="form-text text-muted">{{ form.webhook_url.help_text }}</div>
</div>
<button type="submit" class="btn btn-primary">Add Webhook</button>
</form>
</div>
<h2 class="mt-5">Webhooks</h2>
{% if webhooks %}
<div class="list-group">
{% for webhook in webhooks %}
<div class="list-group-item d-flex justify-content-between align-items-center">
<span>
{% if webhook.avatar %}
<img src="https://cdn.discordapp.com/avatars/{{ webhook.id }}/a_{{ webhook.avatar }}.png"
alt="Avatar of {{ webhook.name }}" class="rounded-circle" height="32" width="32">
{% endif %}
<a href="https://discord.com/api/webhooks/{{ webhook.id }}/{{ webhook.token }}" target="_blank"
class="text-decoration-none">{{ webhook.name }}</a>
</span>
</div>
{% endfor %}
</div>
{% else %}
<div class="alert alert-info">No webhooks added</div>
{% endif %}
</div>
{% endblock content %} {% endblock content %}