Skip to content
Snippets Groups Projects
Commit 244a99b3 authored by Tomáš Valenta's avatar Tomáš Valenta
Browse files

sync with instagram branch

parents d767a649 8a06e6ab
No related branches found
No related tags found
2 merge requests!787Release,!743Add Redmine datasets to charts, Instagram feed to homepage
Pipeline #12247 passed
Showing
with 465 additions and 15 deletions
......@@ -143,6 +143,9 @@ redmine_cache.sqlite
#####################################################
# CUSTOM
# requests cache
instagram_cache.sqlite
# direnv
.envrc
......
......@@ -176,6 +176,7 @@ Přes CRON je třeba na pozadí spouštět Django `manage.py` commandy:
* `update_main_timeline_articles` - aktualizuje články na `pirati.cz` z `https://piratipracuji.cz/api/`
* `update_redmine_issues` - aktualizuje programované body MS a KS stránek napojených na Redmine (několikrát denně)
* `update_tweets` - aktualizuje tweety podle nastavení na Homepage pirati.cz - vyžaduje mít v .env TWITTER_BEARER_TOKEN, parametr --days určuje stáří tweetů (default 1)
* `update_instagram` - aktualizuje Instagramové posty na Homepage pirati.cz - vyžaduje mít v .env `INSTAGRAM_APP_ID` a `INSTAGRAM_APP_SECRET`.
### Fulltextové vyhledávání v češtině
......
File moved
from django.apps import AppConfig
class TwitterUtilsConfig(AppConfig):
name = "twitter_utils"
class InstagramUtilsConfig(AppConfig):
name = "instagram_utils"
from django.conf import settings
from django.core.management.base import BaseCommand
from ...services import InstagramDownloadService
class Command(BaseCommand):
def handle(self, *args, **options):
service = InstagramDownloadService(
app_id=settings.INSTAGRAM_APP_ID,
app_secret=settings.INSTAGRAM_APP_SECRET,
)
service.perform_update()
self.stdout.write("\nInstagram post update finished.")
# Generated by Django 4.1.6 on 2023-04-05 17:16
from django.db import migrations, models
import instagram_utils.models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='InstagramPost',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('author_name', models.CharField(max_length=64, verbose_name='Jméno autora')),
('author_username', models.CharField(max_length=64, verbose_name='Username autora')),
('timestamp', models.DateTimeField(default=instagram_utils.models.get_current_datetime, verbose_name='Datum a čas vytvoření')),
('caption', models.TextField(blank=True, null=True, verbose_name='Popis')),
('image', models.ImageField(upload_to='instagram', verbose_name='Obrázek')),
('url', models.URLField(blank=True, null=True, verbose_name='Odkaz')),
],
options={
'ordering': ('timestamp',),
},
),
]
# Generated by Django 4.1.6 on 2023-04-05 17:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('instagram_utils', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='instagrampost',
name='remote_id',
field=models.CharField(default='', max_length=64, verbose_name='ID Postu'),
preserve_default=False,
),
]
# Generated by Django 4.1.6 on 2023-04-05 17:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('instagram_utils', '0002_instagrampost_remote_id'),
]
operations = [
migrations.AlterField(
model_name='instagrampost',
name='remote_id',
field=models.CharField(max_length=64, unique=True, verbose_name='ID Postu'),
),
]
import datetime
from django.db import models
def get_current_datetime() -> datetime.datetime:
return datetime.datetime.now(tz=datetime.timezone.utc)
class InstagramPost(models.Model):
"""
Model representing an Instgram post obtained from its API through the
update_instagram management command.
"""
remote_id = models.CharField(
verbose_name="ID Postu",
max_length=64,
unique=True,
)
timestamp = models.DateTimeField(
verbose_name="Datum a čas vytvoření",
default=get_current_datetime
)
author_name = models.CharField(
verbose_name="Jméno autora",
max_length=64,
)
author_username = models.CharField(
verbose_name="Username autora",
max_length=64,
)
caption = models.TextField(
verbose_name="Popis",
blank=True,
null=True,
)
image = models.ImageField(
verbose_name="Obrázek",
upload_to="instagram",
)
url = models.URLField(
verbose_name="Odkaz",
blank=True,
null=True,
)
def __str__(self) -> str:
return f"@{self.author_username} - {self.caption}"
class Meta:
ordering = ("timestamp",)
import datetime
import logging
import io
import os
import requests_cache
from django.core.files import File
from main.models import MainHomePage, MainPersonPage
from .models import InstagramPost
logger = logging.getLogger()
class InstagramDownloadService:
"""
TODO
"""
def __init__(self, app_id: int, app_secret: str):
self.session = requests_cache.CachedSession("instagram_cache")
self.app_id = app_id
self.app_secret = app_secret
def get_user_info_list(self) -> list[str]:
access_block = MainHomePage.objects.first().instagram_access
homepage_access_list = [
(
block["value"]["name"],
block["value"]["access_token"]
)
for block in access_block.raw_data
]
people_access_list = []
for people_page in MainPersonPage.objects.all():
people_access_list += [
(
block["value"]["name"],
block["value"]["access_token"]
)
for block in people_page.instagram_access.raw_data
]
# Remove duplicates
return list({*people_access_list, *homepage_access_list})
def download_remote_image(self, image_url) -> (str, File):
try:
response = self.session.get(image_url)
response.raise_for_status()
except Exception as exc:
logger.warning(
"Error getting Instagram image at %s: %s",
image_url, exc
)
return "", None
return os.path.basename(image_url), File(io.BytesIO(response.content))
def get_user_data(self, access_token: str) -> dict:
user_data = self.session.get(
f"https://graph.instagram.com/v16.0/me?access_token={access_token}"
"&fields=id,username"
)
user_data.raise_for_status()
return user_data.json()
def get_recent_media(self, user_data: dict, access_token: str) -> list[dict]:
with self.session.cache_disabled():
recent_media = self.session.get(
f"https://graph.instagram.com/v16.0/{user_data['id']}/media?access_token="
f"{access_token}&fields=id,timestamp,caption,media_type,permalink,"
"media_url,thumbnail_url"
)
if not recent_media.ok:
logger.warning(
"Error getting media for user %s: %s",
user_data["id"],
recent_media.status_code
)
return []
logger.debug("Parsing Instagram feed: %s", recent_media)
return recent_media.json()["data"]
def parse_media_for_user(self, name: str, access_token: str) -> None:
user_data = self.get_user_data(access_token)
recent_media_json = self.get_recent_media(user_data, access_token)
if len(recent_media_json) == 0:
return
posts = []
for media_data in recent_media_json:
# Don't recreate existing posts'
if InstagramPost.objects.filter(remote_id=media_data["id"]).exists():
logging.info(
"Skipping Instagram post ID %s, already exists",
media_data["id"]
)
continue
post = InstagramPost(
remote_id=media_data["id"],
author_name=name,
author_username=user_data["username"],
timestamp=datetime.datetime.strptime(
media_data["timestamp"],
"%Y-%m-%dT%H:%M:%S%z",
),
caption=media_data["caption"],
url=media_data["permalink"],
)
post.image.save(
*self.download_remote_image(media_data["media_url"]),
False, # Don't save yet
)
post.save()
logger.info(
"Saved Instagram post ID %s",
post.remote_id,
)
def perform_update(self) -> None:
user_info_list = self.get_user_info_list()
media_list = []
for user_info in user_info_list:
self.parse_media_for_user(*user_info)
......@@ -254,18 +254,6 @@ class RegionsBlock(StructBlock):
label = "Články pro regiony"
class TweetsBlock(StructBlock):
title = CharBlock(
label="Titulek",
help_text="Tweety budou načteny pro všechny profily uvedené v nastavení webu automaticky",
)
class Meta:
template = "main/blocks/twitter_block.html"
icon = "openquote"
label = "Tweety"
class PersonContactBlock(StructBlock):
position = CharBlock(label="Název pozice", required=False)
# email, phone?
......@@ -372,6 +360,32 @@ class CardLinkWithHeadlineBlock(CardLinkWithHeadlineBlockMixin):
label = "Karty odkazů s nadpisem"
class InstagramAccessBlock(StructBlock):
name = CharBlock(label="Zobrazované jméno")
username = CharBlock(label="Username", help_text="Např. pirati.cz, bez @ na začátku!")
access_token = CharBlock(label="Přístupový token")
class Meta:
label = "Synchronizace s Instagramem"
help_text = (
"Informace lze získat přihlášením požadovaným Instagramovým "
"účtem na tools.pirati.cz/instagram . Token je třeba kvůli "
"podmínkám Instagramu každých 60 dní obnovit."
)
class InstagramPostsBlock(StructBlock):
title = CharBlock(
label="Titulek",
help_text="Instagramové posty budou načteny pro všechny profily uvedené v nastavení webu automaticky",
)
class Meta:
template = "main/blocks/instagram_block.html"
icon = "openquote"
label = "Instagramové posty"
class HoaxBlock(StructBlock):
title = CharBlock(label="Titulek")
hoax = RichTextBlock(label="Hoax")
......@@ -405,6 +419,3 @@ class TeamBlock(StructBlock):
value = super().get_prep_value(value)
value["slug"] = slugify(value["title"])
return value
# TwitterCarouselBlock
# Generated by Django 4.1.7 on 2023-04-04 21:34
from django.db import migrations, models
import wagtail.blocks
import wagtail.fields
class Migration(migrations.Migration):
dependencies = [
('main', '0045_alter_mainprogrampage_program'),
]
operations = [
migrations.AddField(
model_name='mainhomepage',
name='instagram_usernames',
field=wagtail.fields.StreamField([('username', wagtail.blocks.CharBlock(label='Instagram uživatelské jméno'))], blank=True, use_json_field=True, verbose_name='Uživatelská jména pro synchronizované Instagram účty'),
),
migrations.AddField(
model_name='mainpersonpage',
name='instagram_username',
field=models.CharField(blank=True, help_text='Uživatelské jméno zadejte bez @ na začátku', max_length=32, null=True, verbose_name='Uživatelské jméno na Instagramu pro získání příspěvků'),
),
migrations.AlterField(
model_name='mainhomepage',
name='twitter_usernames',
field=wagtail.fields.StreamField([('username', wagtail.blocks.CharBlock(label='Twitter uživatelské jméno'))], blank=True, use_json_field=True, verbose_name='Uživatelská jména pro synchronizované Twitter účty'),
),
migrations.AlterField(
model_name='mainpersonpage',
name='twitter_username',
field=models.CharField(blank=True, help_text='Uživatelské jméno zadejte bez @ na začátku', max_length=32, null=True, verbose_name='Uživatelské jméno na Twitteru pro získání příspěvků'),
),
]
# Generated by Django 4.1.6 on 2023-04-05 11:17
from django.db import migrations
import wagtail.blocks
import wagtail.fields
class Migration(migrations.Migration):
dependencies = [
('main', '0046_mainhomepage_instagram_usernames_and_more'),
]
operations = [
migrations.RemoveField(
model_name='mainhomepage',
name='instagram_usernames',
),
migrations.AddField(
model_name='mainhomepage',
name='instagram_access',
field=wagtail.fields.StreamField([('instagram_access', wagtail.blocks.StructBlock([('username', wagtail.blocks.CharBlock(label='Uživatelské jméno')), ('access_token', wagtail.blocks.CharBlock(help_text='TODO', label='Přístupový token'))]))], blank=True, use_json_field=True, verbose_name='Uživatelská jména a přístupové tokeny pro synchronizované Instagram účty'),
),
]
# Generated by Django 4.1.6 on 2023-04-05 11:34
from django.db import migrations
import wagtail.blocks
import wagtail.fields
class Migration(migrations.Migration):
dependencies = [
('main', '0047_remove_mainhomepage_instagram_usernames_and_more'),
]
operations = [
migrations.AlterField(
model_name='mainhomepage',
name='instagram_access',
field=wagtail.fields.StreamField([('instagram_access', wagtail.blocks.StructBlock([('name', wagtail.blocks.CharBlock(label='Zobrazované jméno')), ('user_id', wagtail.blocks.CharBlock(help_text='TODO', label='Uživatelské ID')), ('access_token', wagtail.blocks.CharBlock(help_text='TODO', label='Přístupový token'))]))], blank=True, use_json_field=True, verbose_name='Uživatelská jména a přístupové tokeny pro synchronizované Instagram účty'),
),
]
# Generated by Django 4.1.6 on 2023-04-05 11:56
from django.db import migrations
import wagtail.blocks
import wagtail.fields
class Migration(migrations.Migration):
dependencies = [
('main', '0048_alter_mainhomepage_instagram_access'),
]
operations = [
migrations.AlterField(
model_name='mainhomepage',
name='instagram_access',
field=wagtail.fields.StreamField([('instagram_access', wagtail.blocks.StructBlock([('name', wagtail.blocks.CharBlock(label='Zobrazované jméno')), ('user_id', wagtail.blocks.CharBlock(label='Uživatelské ID')), ('access_token', wagtail.blocks.CharBlock(label='Přístupový token'))]))], blank=True, use_json_field=True, verbose_name='Uživatelská jména a přístupové tokeny pro synchronizované Instagram účty'),
),
]
# Generated by Django 4.1.6 on 2023-04-05 16:45
from django.db import migrations
import wagtail.blocks
import wagtail.fields
class Migration(migrations.Migration):
dependencies = [
('main', '0049_alter_mainhomepage_instagram_access'),
]
operations = [
migrations.AlterField(
model_name='mainhomepage',
name='instagram_access',
field=wagtail.fields.StreamField([('instagram_access', wagtail.blocks.StructBlock([('name', wagtail.blocks.CharBlock(label='Zobrazované jméno')), ('access_token', wagtail.blocks.CharBlock(label='Přístupový token'))]))], blank=True, use_json_field=True, verbose_name='Uživatelská jména a přístupové tokeny pro synchronizované Instagram účty'),
),
]
# Generated by Django 4.1.6 on 2023-04-06 11:09
from django.db import migrations
import main.blocks
import wagtail.blocks
import wagtail.fields
import wagtail.images.blocks
class Migration(migrations.Migration):
dependencies = [
('main', '0050_alter_mainhomepage_instagram_access'),
]
operations = [
migrations.RemoveField(
model_name='mainhomepage',
name='twitter_usernames',
),
migrations.AlterField(
model_name='mainhomepage',
name='content',
field=wagtail.fields.StreamField([('carousel', wagtail.blocks.StructBlock([('slides', wagtail.blocks.ListBlock(main.blocks.HomePageCarouseSlideBlock, label='Obrázky s nadpisy - carouselu'))])), ('news', wagtail.blocks.StructBlock([('title', wagtail.blocks.CharBlock(help_text='Nejnovější články se načtou automaticky', label='Titulek')), ('image', wagtail.images.blocks.ImageChooserBlock(label='Obrázek pozadí', required=False))])), ('people', wagtail.blocks.StructBlock([('title', wagtail.blocks.CharBlock(label='Hlavní titulek')), ('list', wagtail.blocks.ListBlock(main.blocks.BoxBlock, label='Boxíky'))])), ('regions', wagtail.blocks.StructBlock([('title', wagtail.blocks.CharBlock(help_text='Články pro regiony se načtou automaticky', label='Titulek')), ('image', wagtail.images.blocks.ImageChooserBlock(label='Obrázek pozadí', required=False))])), ('instagram_posts', wagtail.blocks.StructBlock([('title', wagtail.blocks.CharBlock(help_text='Instagramové posty budou načteny pro všechny profily uvedené v nastavení webu automaticky', label='Titulek'))])), ('boxes', wagtail.blocks.StructBlock([('title', wagtail.blocks.CharBlock(label='Nadpis')), ('list', wagtail.blocks.ListBlock(main.blocks.BoxBlock, label='Boxíky')), ('image', wagtail.images.blocks.ImageChooserBlock(label='Obrázek pozadí', required=False))]))], blank=True, use_json_field=True, verbose_name='Hlavní obsah'),
),
migrations.AlterField(
model_name='mainhomepage',
name='instagram_access',
field=wagtail.fields.StreamField([('instagram_access', wagtail.blocks.StructBlock([('name', wagtail.blocks.CharBlock(label='Zobrazované jméno')), ('username', wagtail.blocks.CharBlock(help_text='Např. pirati.cz, bez @ na začátku!', label='Username')), ('access_token', wagtail.blocks.CharBlock(label='Přístupový token'))]))], blank=True, use_json_field=True, verbose_name='Uživatelská jména a přístupové tokeny pro synchronizované Instagram účty'),
),
]
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment