Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • master
  • test
  • niki-stuff2
  • feat/niki-stuff
  • feat/custom-css
  • feat/redesign-improvements-10
  • feat/redesign-improvements-8
  • feat/redesign-fixes-3
  • feat/pirstan-changes
  • feat/separate-import-thread
  • feat/dary-improvements
  • features/add-pdf-page
  • features/add-typed-table
  • features/fix-broken-calendar-categories
  • features/add-embed-to-articles
  • features/create-mastodon-feed-block
  • features/add-custom-numbering-for-candidates
  • features/add-timeline
  • features/create-wordcloud-from-article-page
  • features/create-collapsible-extra-legal-info
  • features/extend-hero-banner
  • features/add-link-to-images
  • features/add-thumbnail-principle-to-uniweb-and-senate
  • features/donation-panel-should-be-optional
  • features/add-redirects
  • features/add-dynamic-candidate-numbers
  • features/add-feature-enlarging-sub-block
  • feat/instagram-feed
  • feat/hideable-tweets
  • feat/people-octopus-imports
  • feature/crypto-widget
  • feat-more-blocks
  • feat-rework-election-page
  • feat/geo-feature-collections
34 results

Target

Select target project
  • TO / Maják
  • hemp / Maják
2 results
Select Git revision
  • master
  • test
  • niki-stuff2
  • feat/niki-stuff
  • feat/custom-css
  • feat/redesign-improvements-10
  • feat/redesign-improvements-8
  • feat/redesign-fixes-3
  • feat/pirstan-changes
  • feat/separate-import-thread
  • feat/dary-improvements
  • features/add-pdf-page
  • features/add-typed-table
  • features/fix-broken-calendar-categories
  • features/add-embed-to-articles
  • features/create-mastodon-feed-block
  • features/add-custom-numbering-for-candidates
  • features/add-timeline
  • features/create-wordcloud-from-article-page
  • features/create-collapsible-extra-legal-info
  • features/extend-hero-banner
  • features/add-link-to-images
  • features/add-thumbnail-principle-to-uniweb-and-senate
  • features/donation-panel-should-be-optional
  • features/add-redirects
  • features/add-dynamic-candidate-numbers
  • features/add-feature-enlarging-sub-block
  • feat/instagram-feed
  • feat/hideable-tweets
  • feat/people-octopus-imports
  • feature/crypto-widget
  • feat-more-blocks
  • feat-rework-election-page
  • feat/geo-feature-collections
34 results
Show changes
64 files
+ 129105
571
Compare changes
  • Side-by-side
  • Inline

Files

+6 −0
Original line number Diff line number Diff line
@@ -137,9 +137,15 @@ dmypy.json
# Cython debug symbols
cython_debug/

# Requests-cache
redmine_cache.sqlite

#####################################################
# CUSTOM

# requests cache
instagram_cache.sqlite

# direnv
.envrc

+1 −1
Original line number Diff line number Diff line
@@ -3,4 +3,4 @@
line_length = 88
multi_line_output = 3
include_trailing_comma = true
known_third_party = PyPDF2,arrow,bleach,bs4,captcha,celery,dateutil,django,environ,faker,fastjsonschema,icalevnt,markdown,modelcluster,pirates,pytest,pytz,requests,sentry_sdk,taggit,tweepy,wagtail,wagtailmetadata,weasyprint,yaml
known_third_party = PyPDF2,arrow,bleach,bs4,captcha,celery,dateutil,django,environ,faker,fastjsonschema,icalevents,markdown,modelcluster,pirates,pytest,pytz,requests,requests_cache,sentry_sdk,taggit,wagtail,wagtailmetadata,weasyprint,yaml
+1 −0
Original line number Diff line number Diff line
@@ -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ě

+1 −1
Original line number Diff line number Diff line
@@ -3,7 +3,7 @@ from datetime import date, timedelta

import arrow
from django.db import migrations
from icalevnt import icalevents
from icalevents import icalevents

from calendar_utils.parser import process_event_list

+1 −1
Original line number Diff line number Diff line
@@ -4,7 +4,7 @@ from datetime import date, timedelta
import arrow
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models
from icalevnt import icalevents
from icalevents import icalevents

from .parser import process_event_list

+1 −1
Original line number Diff line number Diff line
@@ -7,7 +7,7 @@ import bleach
from django.conf import settings

if TYPE_CHECKING:
    from icalevnt.icalparser import Event
    from icalevents.icalparser import Event

EVENT_KEYS = ("start", "end", "all_day", "summary", "description", "location")

+81 −6
Original line number Diff line number Diff line
# Generated by Django 4.1.6 on 2023-02-28 07:51

from django.db import migrations
import wagtail.blocks
import wagtail.fields
import wagtail.images.blocks
from django.db import migrations


class Migration(migrations.Migration):

    dependencies = [
        ('district', '0105_alter_districtarticlepage_content'),
        ("district", "0105_alter_districtarticlepage_content"),
    ]

    operations = [
        migrations.AlterField(
            model_name='districtcrossroadpage',
            name='cards_content',
            field=wagtail.fields.StreamField([('cards', wagtail.blocks.StructBlock([('headline', wagtail.blocks.CharBlock(label='Titulek bloku', required=False)), ('card_items', wagtail.blocks.ListBlock(wagtail.blocks.StructBlock([('image', wagtail.images.blocks.ImageChooserBlock(label='Obrázek')), ('title', wagtail.blocks.CharBlock(label='Titulek', required=True)), ('text', wagtail.blocks.RichTextBlock(label='Krátký text pod nadpisem', required=False)), ('page', wagtail.blocks.PageChooserBlock(label='Stránka', page_type=['district.DistrictArticlePage', 'district.DistrictArticlesPage', 'district.DistrictCenterPage', 'district.DistrictContactPage', 'district.DistrictCrossroadPage', 'district.DistrictCustomPage', 'district.DistrictElectionCampaignPage', 'district.DistrictElectionProgramPage', 'district.DistrictElectionRootPage', 'district.DistrictPeoplePage', 'district.DistrictPersonPage', 'district.DistrictPostElectionStrategyPage', 'district.DistrictProgramPage'], required=False)), ('link', wagtail.blocks.URLBlock(label='Odkaz', required=False))]), label='Karty s odkazy'))]))], blank=True, use_json_field=True, verbose_name='Karty rozcestníku'),
            model_name="districtcrossroadpage",
            name="cards_content",
            field=wagtail.fields.StreamField(
                [
                    (
                        "cards",
                        wagtail.blocks.StructBlock(
                            [
                                (
                                    "headline",
                                    wagtail.blocks.CharBlock(
                                        label="Titulek bloku", required=False
                                    ),
                                ),
                                (
                                    "card_items",
                                    wagtail.blocks.ListBlock(
                                        wagtail.blocks.StructBlock(
                                            [
                                                (
                                                    "image",
                                                    wagtail.images.blocks.ImageChooserBlock(
                                                        label="Obrázek"
                                                    ),
                                                ),
                                                (
                                                    "title",
                                                    wagtail.blocks.CharBlock(
                                                        label="Titulek", required=True
                                                    ),
                                                ),
                                                (
                                                    "text",
                                                    wagtail.blocks.RichTextBlock(
                                                        label="Krátký text pod nadpisem",
                                                        required=False,
                                                    ),
                                                ),
                                                (
                                                    "page",
                                                    wagtail.blocks.PageChooserBlock(
                                                        label="Stránka",
                                                        page_type=[
                                                            "district.DistrictArticlePage",
                                                            "district.DistrictArticlesPage",
                                                            "district.DistrictCenterPage",
                                                            "district.DistrictContactPage",
                                                            "district.DistrictCrossroadPage",
                                                            "district.DistrictCustomPage",
                                                            "district.DistrictElectionCampaignPage",
                                                            "district.DistrictElectionProgramPage",
                                                            "district.DistrictElectionRootPage",
                                                            "district.DistrictPeoplePage",
                                                            "district.DistrictPersonPage",
                                                            "district.DistrictPostElectionStrategyPage",
                                                            "district.DistrictProgramPage",
                                                        ],
                                                        required=False,
                                                    ),
                                                ),
                                                (
                                                    "link",
                                                    wagtail.blocks.URLBlock(
                                                        label="Odkaz", required=False
                                                    ),
                                                ),
                                            ]
                                        ),
                                        label="Karty s odkazy",
                                    ),
                                ),
                            ]
                        ),
                    )
                ],
                blank=True,
                use_json_field=True,
                verbose_name="Karty rozcestníku",
            ),
        ),
    ]
+60672 −0

File added.

Preview size limit exceeded, changes collapsed.

+60672 −0

File added.

Preview size limit exceeded, changes collapsed.

+5 −0
Original line number Diff line number Diff line
from django.apps import AppConfig


class TwitterUtilsConfig(AppConfig):
    name = "twitter_utils"
class InstagramUtilsConfig(AppConfig):
    name = "instagram_utils"
+15 −0
Original line number Diff line number Diff line
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.")
+30 −0
Original line number Diff line number Diff line
# 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',),
            },
        ),
    ]
+19 −0
Original line number Diff line number Diff line
# 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,
        ),
    ]
+18 −0
Original line number Diff line number Diff line
# 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'),
        ),
    ]
+54 −0
Original line number Diff line number Diff line
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",)
+144 −0
Original line number Diff line number Diff line
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)
+26 −15
Original line number Diff line number Diff line
@@ -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
+288 −21

File changed.

Preview size limit exceeded, changes collapsed.

+35 −0
Original line number Diff line number Diff line
# 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ů'),
        ),
    ]
+36 −6
Original line number Diff line number Diff line
# Generated by Django 4.1.6 on 2023-02-28 10:03

from django.db import migrations
import wagtail.blocks
import wagtail.fields
import wagtail.images.blocks
from django.db import migrations


class Migration(migrations.Migration):

    dependencies = [
        ('main', '0046_alter_mainpeoplepage_options_and_more'),
        ("main", "0046_alter_mainpeoplepage_options_and_more"),
    ]

    operations = [
        migrations.AlterField(
            model_name='mainhoaxpage',
            name='content',
            field=wagtail.fields.StreamField([('hoax', wagtail.blocks.StructBlock([('title', wagtail.blocks.CharBlock(label='Titulek')), ('hoax', wagtail.blocks.RichTextBlock(label='Hoax')), ('image', wagtail.images.blocks.ImageChooserBlock(label='Obrázek')), ('image_explanation', wagtail.blocks.CharBlock(label='Popis obrázku', required=False)), ('reality', wagtail.blocks.RichTextBlock(label='Realita'))]))], blank=True, use_json_field=True, verbose_name='Hoaxy a jejich vysvětlení'),
            model_name="mainhoaxpage",
            name="content",
            field=wagtail.fields.StreamField(
                [
                    (
                        "hoax",
                        wagtail.blocks.StructBlock(
                            [
                                ("title", wagtail.blocks.CharBlock(label="Titulek")),
                                ("hoax", wagtail.blocks.RichTextBlock(label="Hoax")),
                                (
                                    "image",
                                    wagtail.images.blocks.ImageChooserBlock(
                                        label="Obrázek"
                                    ),
                                ),
                                (
                                    "image_explanation",
                                    wagtail.blocks.CharBlock(
                                        label="Popis obrázku", required=False
                                    ),
                                ),
                                (
                                    "reality",
                                    wagtail.blocks.RichTextBlock(label="Realita"),
                                ),
                            ]
                        ),
                    )
                ],
                blank=True,
                use_json_field=True,
                verbose_name="Hoaxy a jejich vysvětlení",
            ),
        ),
    ]
+24 −0
Original line number Diff line number Diff line
# 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'),
        ),
    ]
+20 −0
Original line number Diff line number Diff line
# 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'),
        ),
    ]
+20 −0
Original line number Diff line number Diff line
# 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'),
        ),
    ]
+20 −0
Original line number Diff line number Diff line
# 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'),
        ),
    ]
+31 −0
Original line number Diff line number Diff line
# 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'),
        ),
    ]
+28 −0
Original line number Diff line number Diff line
# Generated by Django 4.1.6 on 2023-04-06 12:36

from django.db import migrations
import wagtail.blocks
import wagtail.fields


class Migration(migrations.Migration):

    dependencies = [
        ('main', '0051_remove_mainhomepage_twitter_usernames_and_more'),
    ]

    operations = [
        migrations.RemoveField(
            model_name='mainpersonpage',
            name='instagram_username',
        ),
        migrations.RemoveField(
            model_name='mainpersonpage',
            name='twitter_username',
        ),
        migrations.AddField(
            model_name='mainpersonpage',
            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='Synchronizace s Instagramem'),
        ),
    ]
+14 −0
Original line number Diff line number Diff line
# Generated by Django 4.1.6 on 2023-04-06 12:46

from django.db import migrations


class Migration(migrations.Migration):

    dependencies = [
        ('main', '0047_alter_mainhoaxpage_content'),
        ('main', '0052_remove_mainpersonpage_instagram_username_and_more'),
    ]

    operations = [
    ]
+67 −56
Original line number Diff line number Diff line
@@ -36,7 +36,7 @@ from shared.models import ( # MenuMixin,
)
from shared.utils import make_promote_panels, subscribe_to_newsletter
from tuning import admin_help
from twitter_utils.models import Tweet
from instagram_utils.models import InstagramPost

from . import blocks
from .constants import MONTH_NAMES
@@ -87,7 +87,7 @@ class MainHomePage(
            ("news", blocks.NewsBlock()),
            ("people", blocks.PeopleOverviewBlock()),
            ("regions", blocks.RegionsBlock()),
            ("tweets", blocks.TweetsBlock()),
            ("instagram_posts", blocks.InstagramPostsBlock()),
            ("boxes", blocks.BoxesBlock()),
        ],
        verbose_name="Hlavní obsah",
@@ -135,9 +135,9 @@ class MainHomePage(
        use_json_field=True,
    )

    twitter_usernames = StreamField(
        [("username", CharBlock(label="Twitter uživatelské jméno"))],
        verbose_name="Uživatelská jména pro synchronizované twitter účty",
    instagram_access = StreamField(
        [("instagram_access", blocks.InstagramAccessBlock())],
        verbose_name="Uživatelská jména a přístupové tokeny pro synchronizované Instagram účty",
        blank=True,
        max_num=64,
        use_json_field=True,
@@ -159,7 +159,7 @@ class MainHomePage(
        FieldPanel("donation_page_text"),
        FieldPanel("social_links"),
        FieldPanel("matomo_id"),
        FieldPanel("twitter_usernames"),
        FieldPanel("instagram_access"),
    ]

    ### EDIT HANDLERS
@@ -204,15 +204,20 @@ class MainHomePage(
    def get_context(self, request, *args, **kwargs):
        context = super().get_context(request, args, kwargs)

        twitter_username_list = [
            username_data["value"] for username_data in self.twitter_usernames.raw_data
        instagram_username_list = [
            access_data["value"]["username"]
            for access_data in self.instagram_access.raw_data
        ]
        tweet_list = Tweet.objects.username_list(twitter_username_list).order_by(
            "-twitter_id"

        instagram_post_list = (
            InstagramPost.
            objects.
            filter(author_username__in=instagram_username_list).
            order_by("-timestamp")
        )

        context["tweet_list"] = tweet_list[:4]
        context["show_next_tweet"] = len(tweet_list) > 4
        context["instagram_post_list"] = instagram_post_list[:4]
        context["show_next_instagram_post"] = len(instagram_post_list) > 4

        context["regions"] = REGION_CHOICES

@@ -251,24 +256,35 @@ class MainHomePage(
        }
        return JsonResponse(data=data, safe=False)

    def get_twitter_response(self, request):
        twitter_username_list = [
            username_data["value"] for username_data in self.twitter_usernames.raw_data
    def get_instagram_response(self, request):
        instagram_username_list = [
            access_data["value"]["username"]
            for access_data in self.instagram_access.raw_data
        ]
        tweet_qs = Tweet.objects.username_list(twitter_username_list).order_by(
            "-twitter_id"

        instagram_post_list_queryset = (
            InstagramPost.
            objects.
            filter(author_username__in=instagram_username_list).
            order_by("-timestamp")
        )
        tweet_paginator = Paginator(tweet_qs, 4)

        tweet_page = tweet_paginator.get_page(request.GET.get("page", 1))
        context = {"tweet_list": tweet_page.object_list}
        instagram_post_paginator = Paginator(instagram_post_list_queryset, 4)

        instagram_post_page = instagram_post_paginator.get_page(request.GET.get("page", 1))
        context = {"instagram_post_list": instagram_post_page.object_list}

        html_content = render(
            request, "main/includes/twitter_widget.html", context
            request,
            "main/includes/instagram_widget.html",
            context
        ).content

        data = {
            "html": html_content.decode("utf-8"),
            "has_next": tweet_page.has_next(),
            "has_next": instagram_post_page.has_next(),
        }

        return JsonResponse(data=data, safe=False)

    def serve(self, request, *args, **kwargs):
@@ -276,7 +292,7 @@ class MainHomePage(
            if "region" in request.GET:
                return self.get_region_response(request)
            else:
                return self.get_twitter_response(request)
                return self.get_instagram_response(request)

        return super().serve(request, *args, **kwargs)

@@ -491,19 +507,13 @@ class MainArticlesPage(

    def get_all_articles_search_response(self, request):
        article_paginator = Paginator(
            MainArticlePage
            .objects
            .order_by("-date")
            .live()
            .search(request.GET["q"]),
            MainArticlePage.objects.live().search(request.GET["q"]).order_by("-date"),
            10,
        )
        article_page = article_paginator.get_page(request.GET.get("page", 1))
        context = {"article_data_list": article_page.object_list}
        html_content = render(
            request,
            "main/includes/person_article_preview.html",
            context
            request, "main/includes/person_article_preview.html", context
        ).content
        data = {
            "html": html_content.decode("utf-8"),
@@ -521,12 +531,7 @@ class MainArticlesPage(
            query = request.GET["q"]

            article_results = (
                MainArticlePage
                .objects
                .order_by("-date")
                .live()
                .search(query)
                [:11]
                MainArticlePage.objects.live().search(query).order_by("-date")[:11]
            )

            return render(
@@ -537,8 +542,8 @@ class MainArticlesPage(
                    "query": query,
                    "article_results": article_results[:10],
                    "show_more_articles": len(article_results) > 10,
                    "sub_heading": f"Vyhledávání „{query}"
                }
                    "sub_heading": f"Vyhledávání „{query}",
                },
            )
        else:
            return HttpResponseRedirect(self.url)
@@ -757,12 +762,13 @@ class MainPersonPage(ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin,
    perex = models.TextField()
    text = RichTextField()

    twitter_username = models.CharField(
        "Uživatelské jméno twitter pro získání příspěvků",
    instagram_access = StreamField(
        [
            ("instagram_access", blocks.InstagramAccessBlock()),
        ],
        verbose_name="Synchronizace s Instagramem",
        blank=True,
        null=True,
        max_length=32,
        help_text="Uživatelské jméno zadejte bez @ na začátku",
        use_json_field=True,
    )

    social_links = StreamField(
@@ -799,7 +805,7 @@ class MainPersonPage(ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin,
        FieldPanel("after_name"),
        FieldPanel("position"),
        FieldPanel("perex"),
        FieldPanel("twitter_username"),
        FieldPanel("instagram_access"),
        FieldPanel("text"),
        FieldPanel("email"),
        FieldPanel("phone"),
@@ -809,12 +815,21 @@ class MainPersonPage(ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin,

    def get_context(self, request):
        context = super().get_context(request)

        context["article_page_list"] = MainArticlePage.objects.filter(
            author_page=self.id
        )
        context["tweet_list"] = Tweet.objects.username(self.twitter_username).order_by(
            "-twitter_id"

        if len(self.instagram_access.raw_data) != 0:
            context["instagram_post_list"] = (
                InstagramPost.
                objects.
                filter(
                    author_username=self.instagram_access.raw_data[0]["value"]["username"]
                ).
                order_by("-timestamp")
            )[:20]

        return context

    ### OTHERS
@@ -962,9 +977,7 @@ class MainCrossroadPage(
        verbose_name = "Rozcestník s kartami"


class MainHoaxPage(
    ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, Page
):
class MainHoaxPage(ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, Page):
    ### FIELDS

    description = RichTextField(
@@ -974,9 +987,7 @@ class MainHoaxPage(
    )

    content = StreamField(
        [
            (("hoax"), blocks.HoaxBlock())
        ],
        [(("hoax"), blocks.HoaxBlock())],
        verbose_name="Hoaxy a jejich vysvětlení",
        blank=True,
        use_json_field=True,
+1 −1

File changed.

Preview size limit exceeded, changes collapsed.

+1 −1

File changed.

Contains only whitespace changes.

+12 −12
Original line number Diff line number Diff line
@@ -4,13 +4,13 @@
      {{ self.title }}
    </h2>
  </div>
  <div id="tweetsList">
    {% include 'main/includes/twitter_widget.html' with tweet_list=tweet_list %}
  </div>
  {% if show_next_tweet %}
  <ul class="flex flex-wrap justify-center gap-3" id="instagram-posts-list">
    {% include 'main/includes/instagram_widget.html' with instagram_post_list=instagram_post_list %}
  </ul>
  {% if show_next_instagram_post %}
    <div class="flex justify-center mt-8 lg:mt-24">
      <a
        onclick="showMoreTweets(event, this)"
        onclick="showMorePosts(event, this)"
        href="#"
        data-url="{{ page_url }}?page="
        data-page="2"
@@ -27,11 +27,11 @@
</div>

<script type="text/javascript">
  async function showMoreTweets(event, btn) {
  async function showMorePosts(event, btn) {
    event.preventDefault()
    const tweetsList = document.getElementById('tweetsList');
    const postList = document.getElementById("instagram-posts-list");

    const url = btn.getAttribute('data-url') + btn.getAttribute('data-page')
    const url = btn.getAttribute("data-url") + btn.getAttribute("data-page")
    const response = await fetch(url, {
      method: "GET",
      headers: {
@@ -40,10 +40,10 @@
    })
    const data = await response.json()

    tweetsList.innerHTML += data.html;
    if (!data.has_next) { btn.style.display = 'none'; }
    postList.innerHTML += data.html;
    if (!data.has_next) { btn.style.display = "none"; }

    const dataPage = parseInt(btn.getAttribute('data-page')) + 1
    btn.setAttribute('data-page', dataPage)
    const dataPage = parseInt(btn.getAttribute("data-page")) + 1
    btn.setAttribute("data-page", dataPage)
  }
</script>
+40 −0
Original line number Diff line number Diff line
{% for post in instagram_post_list %}
  <li class="flex max-w-sm max-w-xs w-full h-[20rem]">
    <a
      href="{{ post.url }}"
      class="group h-full w-full flex flex-col align-center overflow-hidden text-center border border-grey-100 relative hover:no-underline"
    >
      <div class="md:min-h-[20rem] p-4 opacity-0 group-focus:opacity-100 group-hover:opacity-100 duration-150 z-10">
        <div class="flex flex-col items-center">
          <div class="mb-4 flex items-center justify-between xl:flex-col gap-3">
            <div class="flex flex-col">
              <h5 class="font-alt text-xl mt-3 mb-1 text-left sm:text-center">
                {{ post.author_name }}
              </h5>
              <small class="text-brands-instagram text-left sm:text-center">
                @{{ post.author_username }}
              </small>
            </div>
          </div>
          <p class="text-small sm:text-base leading-6 mb-2">
            {{ post.caption }}
          </p>
        </div>
      </div>

      <div class="absolute inset-0 flex-shrink-0 z-0 duration-150 group-focus:blur-lg group-focus:opacity-25 group-hover:blur-lg group-hover:opacity-25">
        <div class="relative">
          <div class="absolute left-4 top-4 bg-white rounded-lg p-1.5 drop-shadow-md">
            <i class="ico--instagram text-brands-instagram text-2xl"></i>
          </div>

          <img
            class="h-[20rem] object-cover"
            src="{{ post.image.url }}"
            alt="Obrázek v Instagramovém postu, popis „{{ post.caption }}“"
          >
        </div>
      </div>
    </a>
  </li>
{% endfor %}
+2 −2

File changed.

Contains only whitespace changes.

+0 −46
Original line number Diff line number Diff line
<div class="flex flex-wrap justify-center">
  {% for tweet in tweet_list %}
    <div class="flex max-w-sm max-w-xs w-full">
      <a
        href="https://twitter.com/{{ tweet.author_username }}"
        class="group mb-5 w-full flex flex-col align-center overflow-hidden text-center border border-grey-100 relative sm:mb-0 hover:no-underline"
      >
        <div class="md:min-h-[21rem] p-4{% if tweet.image %} opacity-0 group-focus:opacity-100 group-hover:opacity-100 duration-150 z-10{% endif %}">
          <div class="flex flex-col items-center">
            <div class="mb-4 flex items-center justify-between xl:flex-col gap-3">
              <img
                class="rounded-full shadow-sm w-12"
                src="{{ tweet.author_img.url }}"
                alt="user image"
              />
              <div class="flex flex-col">
                <h5 class="font-alt text-xl mb-1 text-left sm:text-center">
                  {{ tweet.author_name }}
                </h5>
                <small class="text-turquoise-400 text-left sm:text-center">
                  {{ tweet.author_username }}
                </small>
              </div>
            </div>
            <p class="text-small sm:text-base leading-6 mb-2">
              {{ tweet.text|truncatechars:240 }}
            </p>
          </div>
        </div>

        <div class="flex-shrink-0 h-10">
          <i class="ico--twitter text-turquoise-400 text-3xl sm:text-xl"></i>
        </div>

        {% if tweet.image %}
          <div class="absolute inset-0 flex-shrink-0 z-0 duration-150 group-focus:blur-lg group-focus:opacity-25 group-hover:blur-lg group-hover:opacity-25">
            <img src="{{ tweet.image.url }}"
                 class="tweet-image"
                 alt="Obrázek Tweetu"
            >
          </div>
        {% endif %}
      </a>
    </div>
  {% endfor %}
</div>
+1 −1

File changed.

Contains only whitespace changes.

+25 −29
Original line number Diff line number Diff line
@@ -55,55 +55,51 @@
        </div>
      </section>
    </div>
    {% if tweet_list %}
      <section class="grid-container no-max mr-0 person-twitter-section mb-4 xl:mb-20">
    {% if instagram_post_list %}
      <section class="grid-container no-max mr-0 person-instagram-section mb-4 xl:mb-20">
        <div class="grid-content-with-right-side">
          <h2 class="head-4xl text-left">
            Aktuálně na Twitteru
            Aktuálně na Instagramu
          </h2>
          <div class="__js-root twitter-carousel-root xl:max-w-[1145px]">
            <ui-twitter-carousel>
              {% for tweet in tweet_list %}
                <div class="flex max-w-sm max-w-xs w-full h-full">
          <div class="__js-root instagram-carousel-root xl:max-w-[1145px]">
            <ui-instagram-carousel>
              {% for post in instagram_post_list %}
                <div class="flex max-w-sm max-w-xs w-full h-[20rem]">
                  <a
                    href="https://twitter.com/{{ tweet.author_username }}"
                    class="group mb-5 h-full w-full flex flex-col align-center overflow-hidden text-center border border-grey-100 relative sm:mb-0 hover:no-underline"
                    href="{{ post.url }}"
                    class="group h-full w-full flex flex-col align-center overflow-hidden text-center border border-grey-100 relative hover:no-underline"
                  >
                    <div class="md:min-h-[21rem] p-4{% if tweet.image %} opacity-0 group-focus:opacity-100 group-hover:opacity-100 duration-150 z-10{% endif %}">
                    <div class="md:min-h-[20rem] p-4 opacity-0 group-focus:opacity-100 group-hover:opacity-100 duration-150 z-10">
                      <div class="flex flex-col items-center">
                        <div class="mb-4 flex items-center justify-between xl:flex-col gap-3">
                          <img
                            class="rounded-full shadow-sm w-12"
                            src="{{ tweet.author_img.url }}"
                            alt="Profilový obrázek"
                          >
                          <div class="flex flex-col">
                            <h5 class="font-alt text-xl mb-1 text-left sm:text-center">
                              {{ tweet.author_name }}
                            <h5 class="font-alt text-xl mt-3 mb-1 text-left sm:text-center">
                              {{ post.author_name }}
                            </h5>
                            <small class="text-turquoise-400 text-left sm:text-center">
                              {{ tweet.author_username }}
                            <small class="text-brands-instagram text-left sm:text-center">
                              @{{ post.author_username }}
                            </small>
                          </div>
                        </div>
                        <p class="text-small sm:text-base leading-6 mb-2">
                          {{ tweet.text|truncatechars:240 }}
                          {{ post.caption }}
                        </p>
                      </div>
                    </div>

                    <div class="flex-shrink-0 h-10 mt-auto">
                      <i class="ico--twitter text-turquoise-400 text-3xl sm:text-xl"></i>
                    <div class="absolute inset-0 flex-shrink-0 z-0 duration-150 group-focus:blur-lg group-focus:opacity-25 group-hover:blur-lg group-hover:opacity-25">
                      <div class="relative">
                        <div class="absolute left-4 top-4 bg-white rounded-lg p-1.5 drop-shadow-md">
                          <i class="ico--instagram text-brands-instagram text-2xl"></i>
                        </div>

                    {% if tweet.image %}
                      <div class="absolute inset-0 flex-shrink-0 z-0 duration-150 group-focus:blur-lg group-focus:opacity-25 group-hover:blur-lg group-hover:opacity-25">
                        <img src="{{ tweet.image.url }}"
                            class="tweet-image"
                            alt="Obrázek Tweetu"
                        <img
                          class="h-[20rem] object-cover"
                          src="{{ post.image.url }}"
                          alt="Obrázek v Instagramovém postu, popis „{{ post.caption }}“"
                        >
                      </div>
                    {% endif %}
                    </div>
                  </a>
                </div>
              {% endfor %}
+4 −1
Original line number Diff line number Diff line
@@ -47,7 +47,7 @@ INSTALLED_APPS = [
    "calendar_utils",
    "maps_utils",
    "redmine_utils",
    "twitter_utils",
    "instagram_utils",
    "users",
    "pirates",
    "tuning",
@@ -311,3 +311,6 @@ MAPS_UTILS_MAPPROXY_URL = env.str(
)

TWITTER_BEARER_TOKEN = env.str("TWITTER_BEARER_TOKEN", default="")

INSTAGRAM_APP_ID = env.str("INSTAGRAM_APP_ID", default="")
INSTAGRAM_APP_SECRET = env.str("INSTAGRAM_APP_SECRET", default="")
+4 −2
Original line number Diff line number Diff line
wagtail
wagtail-metadata
wagtail-trash
django-environ
django-environ<0.10.0
django-extensions
django-redis
django-settings-export
@@ -12,7 +12,8 @@ pirates<=0.7
whitenoise
opencv-python
requests
icalevnt
requests-cache
icalevents
ics
arrow
sentry-sdk
@@ -26,3 +27,4 @@ pypdf2
pyyaml
fastjsonschema
tweepy
requests-cache
+55 −39
Original line number Diff line number Diff line
@@ -6,7 +6,7 @@
#
amqp==5.1.1
    # via kombu
anyascii==0.3.1
anyascii==0.3.2
    # via wagtail
appnope==0.1.3
    # via ipython
@@ -21,7 +21,10 @@ asttokens==2.2.1
async-timeout==4.0.2
    # via redis
attrs==22.2.0
    # via ics
    # via
    #   cattrs
    #   ics
    #   requests-cache
backcall==0.2.0
    # via ipython
beautifulsoup4==4.11.2
@@ -34,6 +37,8 @@ bleach==6.0.0
    # via -r base.in
brotli==1.0.9
    # via fonttools
cattrs==22.2.0
    # via requests-cache
celery==5.2.7
    # via -r base.in
certifi==2022.12.7
@@ -44,7 +49,7 @@ cffi==1.15.1
    # via
    #   cryptography
    #   weasyprint
charset-normalizer==3.0.1
charset-normalizer==3.1.0
    # via requests
click==8.1.3
    # via
@@ -58,18 +63,18 @@ click-plugins==1.1.1
    # via celery
click-repl==0.2.0
    # via celery
cryptography==39.0.1
cryptography==40.0.1
    # via
    #   josepy
    #   mozilla-django-oidc
    #   pyopenssl
cssselect2==0.7.0
    # via weasyprint
datetime==4.3
    # via icalevnt
datetime==4.9
    # via icalevents
decorator==5.1.1
    # via ipython
django==4.1.6
django==4.1.8
    # via
    #   django-extensions
    #   django-filter
@@ -114,27 +119,29 @@ draftjs-exporter==2.1.7
    # via wagtail
et-xmlfile==1.1.0
    # via openpyxl
exceptiongroup==1.1.1
    # via cattrs
executing==1.2.0
    # via stack-data
fastjsonschema==2.16.2
fastjsonschema==2.16.3
    # via -r base.in
fonttools[woff]==4.38.0
fonttools[woff]==4.39.3
    # via weasyprint
html5lib==1.1
    # via
    #   wagtail
    #   weasyprint
httplib2==0.20.1
    # via icalevnt
icalendar==4.0.8
    # via icalevnt
icalevnt==0.1.26
httplib2==0.20.4
    # via icalevents
icalendar==4.0.9
    # via icalevents
icalevents==0.1.27
    # via -r base.in
ics==0.7.2
    # via -r base.in
idna==3.4
    # via requests
ipython==8.9.0
ipython==8.12.0
    # via -r base.in
jedi==0.18.2
    # via ipython
@@ -144,7 +151,7 @@ kombu==5.2.4
    # via celery
l18n==2021.3
    # via wagtail
markdown==3.4.1
markdown==3.4.3
    # via -r base.in
matplotlib-inline==0.1.6
    # via ipython
@@ -156,9 +163,9 @@ oauthlib==3.2.2
    # via
    #   requests-oauthlib
    #   tweepy
opencv-python==4.7.0.68
opencv-python==4.7.0.72
    # via -r base.in
openpyxl==3.1.0
openpyxl==3.1.2
    # via wagtail
parso==0.8.3
    # via jedi
@@ -166,18 +173,20 @@ pexpect==4.8.0
    # via ipython
pickleshare==0.7.5
    # via ipython
pillow==9.4.0
pillow==9.5.0
    # via
    #   django-simple-captcha
    #   wagtail
    #   weasyprint
pirates==0.6.0
    # via -r base.in
prompt-toolkit==3.0.36
platformdirs==3.2.0
    # via requests-cache
prompt-toolkit==3.0.38
    # via
    #   click-repl
    #   ipython
psycopg2-binary==2.9.5
psycopg2-binary==2.9.6
    # via -r base.in
ptyprocess==0.7.0
    # via pexpect
@@ -185,23 +194,23 @@ pure-eval==0.2.2
    # via stack-data
pycparser==2.21
    # via cffi
pydyf==0.5.0
pydyf==0.6.0
    # via weasyprint
pygments==2.14.0
pygments==2.15.0
    # via ipython
pyopenssl==23.0.0
pyopenssl==23.1.1
    # via josepy
pyparsing==2.4.7
pyparsing==3.0.9
    # via httplib2
pypdf2==3.0.1
    # via -r base.in
pyphen==0.13.2
pyphen==0.14.0
    # via weasyprint
python-dateutil==2.8.2
    # via
    #   arrow
    #   icalendar
    #   icalevnt
    #   icalevents
    #   ics
pytz==2021.3
    # via
@@ -210,22 +219,25 @@ pytz==2021.3
    #   django-modelcluster
    #   djangorestframework
    #   icalendar
    #   icalevnt
    #   icalevents
    #   l18n
pyyaml==6.0
    # via -r base.in
redis==4.5.1
redis==4.5.4
    # via django-redis
requests==2.28.2
    # via
    #   -r base.in
    #   mozilla-django-oidc
    #   requests-cache
    #   requests-oauthlib
    #   tweepy
    #   wagtail
requests-cache==1.0.1
    # via -r base.in
requests-oauthlib==1.3.1
    # via tweepy
sentry-sdk==1.15.0
sentry-sdk==1.19.1
    # via -r base.in
six==1.16.0
    # via
@@ -236,7 +248,8 @@ six==1.16.0
    #   ics
    #   l18n
    #   python-dateutil
soupsieve==2.3.2.post1
    #   url-normalize
soupsieve==2.4
    # via beautifulsoup4
sqlparse==0.4.3
    # via django
@@ -254,29 +267,32 @@ traitlets==5.9.0
    # via
    #   ipython
    #   matplotlib-inline
tweepy==4.12.1
tweepy==4.13.0
    # via -r base.in
urllib3==1.26.14
url-normalize==1.4.3
    # via requests-cache
urllib3==1.26.15
    # via
    #   requests
    #   requests-cache
    #   sentry-sdk
vine==5.0.0
    # via
    #   amqp
    #   celery
    #   kombu
wagtail==4.2
wagtail==4.2.2
    # via
    #   -r base.in
    #   wagtail-metadata
    #   wagtail-trash
wagtail-metadata==4.0.2
wagtail-metadata==4.0.3
    # via -r base.in
wagtail-trash==0.3.0
wagtail-trash==1.0.0
    # via -r base.in
wcwidth==0.2.6
    # via prompt-toolkit
weasyprint==57.2
weasyprint==58.1
    # via -r base.in
webencodings==0.5.1
    # via
@@ -284,11 +300,11 @@ webencodings==0.5.1
    #   cssselect2
    #   html5lib
    #   tinycss2
whitenoise==6.3.0
whitenoise==6.4.0
    # via -r base.in
willow==1.4.1
    # via wagtail
zope-interface==5.5.2
zope-interface==6.0
    # via datetime
zopfli==0.2.2
    # via fonttools
+1 −1
Original line number Diff line number Diff line
django
django<4.2  # wagtail compatibility
django-debug-toolbar
pytest
pytest-sugar
+9 −11
Original line number Diff line number Diff line
@@ -6,21 +6,19 @@
#
asgiref==3.6.0
    # via django
attrs==22.2.0
    # via pytest
coverage[toml]==7.1.0
coverage[toml]==7.2.3
    # via pytest-cov
django==4.1.6
django==4.1.8
    # via
    #   -r dev.in
    #   django-debug-toolbar
django-debug-toolbar==3.8.1
django-debug-toolbar==4.0.0
    # via -r dev.in
exceptiongroup==1.1.0
exceptiongroup==1.1.1
    # via pytest
factory-boy==3.2.1
    # via pytest-factoryboy
faker==16.7.0
faker==18.4.0
    # via factory-boy
fastdiff==0.3.0
    # via snapshottest
@@ -30,13 +28,13 @@ inflection==0.5.1
    # via pytest-factoryboy
iniconfig==2.0.0
    # via pytest
packaging==23.0
packaging==23.1
    # via
    #   pytest
    #   pytest-sugar
pluggy==1.0.0
    # via pytest
pytest==7.2.1
pytest==7.3.0
    # via
    #   -r dev.in
    #   pytest-cov
@@ -55,7 +53,7 @@ pytest-freezegun==0.4.2
    # via -r dev.in
pytest-mock==3.10.0
    # via -r dev.in
pytest-sugar==0.9.6
pytest-sugar==0.9.7
    # via -r dev.in
python-dateutil==2.8.2
    # via
@@ -79,7 +77,7 @@ tomli==2.0.1
    # via
    #   coverage
    #   pytest
typing-extensions==4.4.0
typing-extensions==4.5.0
    # via pytest-factoryboy
wasmer==1.1.0
    # via fastdiff
+323 −10
Original line number Diff line number Diff line
import datetime
import json
import logging
import re
import typing
import urllib

import requests_cache
from django.core.exceptions import ValidationError
from django.core.files.images import ImageFile
from django.forms.utils import ErrorList
from wagtail import blocks
@@ -674,6 +678,249 @@ class ChartDataset(blocks.StructBlock):
        label = "Zdroj dat"


def get_redmine_projects():
    session = requests_cache.CachedSession(
        "redmine_cache",
        expire_after=datetime.timedelta(hours=1),
    )

    projects = session.get("https://redmine.pirati.cz/projects.json?limit=10000")
    projects.raise_for_status()
    projects = projects.json()["projects"]

    return [(project["id"], project["name"]) for project in projects]


class ChartRedmineIssueDataset(blocks.StructBlock):
    projects = blocks.MultipleChoiceBlock(
        label="Projekty", choices=get_redmine_projects
    )

    is_open = blocks.BooleanBlock(
        label="Jen otevřené",
        required=False,
    )
    is_closed = blocks.BooleanBlock(
        label="Jen uzavřené",
        required=False,
    )

    created_on_min_date = blocks.DateBlock(label="Min. datum vytvoření", required=True)
    created_on_max_date = blocks.DateBlock(label="Max. datum vytvoření", required=True)
    updated_on = blocks.CharBlock(
        label="Filtr pro datum aktualizace",
        max_length=128,
        help_text="Např. <=2023-01-01. Více informací na pi2.cz/redmine-api",
        required=False,
    )

    issue_label = blocks.CharBlock(
        label="Označení úkolů uvnitř grafu",
        max_length=128,
        required=True,
    )

    split_per_project = blocks.BooleanBlock(
        label="Rozdělit podle projektu",
        required=False,
    )

    only_grow = blocks.BooleanBlock(
        label="Pouze růst nahoru",
        required=False,
    )

    def _get_issues_url(self, value, project_id: typing.Union[None, str, list[str]] = None):
        url = "https://redmine.pirati.cz/issues.json"
        params = [
            ("sort", "created_on"),
            ("limit", "100"),
            (
                "created_on",
                f"><{value['created_on_min_date']}|{value['created_on_max_date']}",
            ),
        ]

        if isinstance(project_id, str):
            params.append(("project_id", project_id))
        elif isinstance(project_id, list):
            params.append(("project_id", ",".join(project_id)))

        is_open = value.get("is_open", False)
        is_closed = value.get("is_closed", False)

        if is_open and is_closed:
            params.append(("status_id", "*"))
        elif is_open:
            params.append(("status_id", "open"))
        elif is_closed:
            params.append(("status_id", "closed"))

        if value.get("updated_on", "") != "":
            params.append(("updated_on", value["updated_on"]))

        is_first = True

        for param_set in params:
            param, param_value = param_set

            url += "?" if is_first else "&"
            url += f"{param}={urllib.parse.quote(param_value)}"

            is_first = False

        print(url)

        return url

    def _get_parsed_issues(self, value, labels, issues_url) -> tuple:
        session = requests_cache.CachedSession(
            "redmine_cache",
            expire_after=datetime.timedelta(days=14),
        )

        issues_response = session.get(issues_url)
        issues_response.raise_for_status()
        issues_response = issues_response.json()

        only_grow = value.get("only_grow", False)

        collected_issues = issues_response["issues"]
        offset = 0

        while issues_response["total_count"] - offset > len(issues_response["issues"]):
            offset += 100

            issues_response = session.get(f"{issues_url}&offset={offset}")
            issues_response.raise_for_status()
            issues_response = issues_response.json()

            collected_issues += issues_response["issues"]

        ending_position = len(collected_issues) - 1

        data = None

        current_issue_count = 0
        current_label = datetime.date.fromisoformat(
            collected_issues[0]["created_on"].split("T")[0]
        )

        if not only_grow:
            data = [0] * len(labels)

            for position, issue in enumerate(
                collected_issues
            ):  # Assume correct sorting order
                created_on_date = datetime.date.fromisoformat(
                    issue["created_on"].split("T")[0]
                )

                if current_label != created_on_date or position == ending_position:
                    data[
                        labels.index(current_label)
                    ] = current_issue_count  # Assume labels are unique
                    current_label = created_on_date

                    if position != ending_position:
                        current_issue_count = 0
                    else:
                        data[labels.index(current_label)] = 1
                        break

                current_issue_count += 1
        else:
            data = []
            issue_count_by_date = {}

            for position, issue in enumerate(
                collected_issues
            ):  # Assume correct sorting order
                created_on_date = datetime.date.fromisoformat(
                    issue["created_on"].split("T")[0]
                )

                if current_label not in issue_count_by_date:
                    issue_count_by_date[current_label] = 0

                if current_label != created_on_date or position == ending_position:
                    issue_count_by_date[
                        current_label
                    ] = current_issue_count  # Assume labels are unique
                    current_label = created_on_date

                    if position == ending_position:
                        issue_count_by_date[current_label] = current_issue_count + 1
                        break

                current_issue_count += 1

            previous_date = None

            for date in labels:
                if date not in issue_count_by_date:
                    if previous_date is None:
                        data.append(0)
                        continue

                    data.append(issue_count_by_date[previous_date])
                    continue

                data.append(issue_count_by_date[date])
                previous_date = date

        return data

    def get_context(self, value) -> list:
        context = super().get_context(value)

        labels = []
        datasets = []

        for day_count in range(
            (value["created_on_max_date"] - value["created_on_min_date"]).days + 1
        ):
            day = value["created_on_min_date"] + datetime.timedelta(days=day_count)
            labels.append(day)

        if value.get("split_per_project", False):
            project_choices_lookup = dict(get_redmine_projects())

            for project_id in value["projects"]:
                issues_url = self._get_issues_url(value, project_id)

                datasets.append(
                    {
                        "label": f"{value['issue_label']} - {project_choices_lookup[int(project_id)]}",
                        "data": self._get_parsed_issues(value, labels, issues_url),
                    }
                )
        else:
            issues_url = self._get_issues_url(value, value["projects"])

            datasets.append(
                {
                    "label": value["issue_label"],
                    "data": self._get_parsed_issues(value, labels, issues_url),
                }
            )

        labels = [date.strftime("%d. %m. %Y") for date in labels]

        context["parsed_issue_labels"] = labels
        context["parsed_issues"] = datasets

        return context

    class Meta:
        label = "Zdroj dat z Redmine (úkoly vytvořené za den)"
        help_text = (
            "Po prvním otevření se bude stránka otevírat delší dobu, "
            "zatímco se na pozadí načítají data do grafu. Poté bude "
            "fungovat běžně."
        )


class ChartBlock(blocks.StructBlock):
    title = blocks.CharBlock(
        label="Název",
@@ -692,32 +939,98 @@ class ChartBlock(blocks.StructBlock):
        ],
        default="bar",
    )
    labels = blocks.ListBlock(

    hide_points = blocks.BooleanBlock(
        label="Schovat body",
        required=False,
        help_text="Mění vzhled pouze u linových grafů.",
    )

    local_labels = blocks.ListBlock(
        blocks.CharBlock(
            max_length=40,
            label="Skupina",
        ),
        label="Skupiny",
        default=[],
        blank=True,
        required=False,
        collapsed=True,
        label="Místně definované skupiny",
    )
    datasets = blocks.ListBlock(
    local_datasets = blocks.ListBlock(
        ChartDataset(),
        label="Zdroje dat",
        default=[],
        blank=True,
        required=False,
        collapsed=True,
        label="Místní zdroje dat",
    )

    redmine_issue_datasets = blocks.ListBlock(
        ChartRedmineIssueDataset(label="Redmine úkoly"),
        default=[],
        blank=True,
        required=False,
        label="Zdroje dat z Redmine (úkoly)",
        help_text=(
            "Úkoly, podle doby vytvoření. Pokud definuješ "
            "více zdrojů, datumy v nich musí být stejné."
        ),
    )

    def clean(self, value):
        result = super().clean(value)

        redmine_issues_exist = len(value.get("redmine_issue_datasets", [])) != 0

        if len(value.get("local_datasets", [])) != 0 and redmine_issues_exist:
            raise ValidationError(
                "Definuj pouze jeden typ zdroje dat - místní, nebo z Redmine."
            )

        if redmine_issues_exist:
            min_date = value["redmine_issue_datasets"][0]["created_on_min_date"]
            max_date = value["redmine_issue_datasets"][0]["created_on_max_date"]

            if len(value["redmine_issue_datasets"]) > 1:
                for dataset in value["redmine_issue_datasets"]:
                    if (
                        dataset["created_on_min_date"] != min_date
                        or dataset["created_on_max_date"] != max_date
                    ):
                        raise ValidationError(
                            "Maximální a minimální data všech zdrojů z Redmine musí být stejné"
                        )

        return result

    def get_context(self, value, parent_context=None):
        context = super().get_context(value, parent_context=parent_context)

        datasets = []
        labels = []

        for dataset in value["datasets"]:
            dataset = dict(dataset)
        if len(value["local_datasets"]) != 0:
            labels = value["local_labels"]

            for dataset in value["local_datasets"]:
                datasets.append(
                {"label": dataset["label"], "data": [item for item in dataset["data"]]}
                    {
                        "label": dataset["label"],
                        "data": [item for item in dataset["data"]],
                    }
                )
        elif len(value["redmine_issue_datasets"]) != 0:
            for dataset_wrapper in value["redmine_issue_datasets"]:
                redmine_context = ChartRedmineIssueDataset().get_context(
                    dataset_wrapper
                )

                labels = redmine_context["parsed_issue_labels"]
                datasets += redmine_context["parsed_issues"]

        value["datasets"] = json.dumps(datasets)
        value["labels"] = json.dumps([label for label in value["labels"]])
        value["labels"] = json.dumps([label for label in labels])

        return context

+3 −1
Original line number Diff line number Diff line
@@ -13,7 +13,9 @@ class OverwriteStorage(get_storage_class()):

        Found at https://djangosnippets.org/snippets/976/
        """

        # If the filename already exists, remove it as if it was a true file system
        if self.exists(name):
            os.remove(os.path.join(settings.MEDIA_ROOT, name))

        return name 
+1 −1
Original line number Diff line number Diff line
@@ -69,7 +69,7 @@
        {{ article.title }}
      </h1>
    </a>
    <p class="card-body-text flex-grow{% if article.is_black %} bg-black{% endif %}">
    <p class="card-body-text text-ellipsis overflow-hidden h-64 flex-grow{% if article.is_black %} bg-black{% endif %}">
      {{ article.perex }}
    </p>
    <div class="inline-block-nogap mt-4">
+9 −1
Original line number Diff line number Diff line
@@ -50,6 +50,8 @@

                    tempDataset["borderColor"] = getColor();
                    tempDataset["borderWidth"] = 1;
                    tempDataset["fill"] = true;
                    tempDataset["tension"] = 0.3;

                    finalDatasets.push(tempDataset);
                }
@@ -83,7 +85,13 @@
                                    beginAtZero: true,
                                },
                            },
                        },
                        }{% if value.hide_points %},
                            elements: {
                                point: {
                                    radius: 0
                                }
                            }
                        {% endif %}
                    }
                }
            );
+1 −1
Original line number Diff line number Diff line
@@ -3,7 +3,7 @@ from datetime import datetime
from pathlib import Path

import pytest
from icalevnt.icalparser import Event
from icalevents.icalparser import Event


@pytest.fixture(scope="session")
+2 −4
Original line number Diff line number Diff line
from datetime import datetime
from zoneinfo import ZoneInfo

import arrow
import pytest
from icalevnt.icalparser import Event
from icalevents.icalparser import Event

from calendar_utils.parser import (
    process_event_list,
@@ -20,7 +18,7 @@ def test_split_events(sample_response, sample_future_events, sample_past_events)


@pytest.mark.freeze_time("2022-05-13")
def test_split_events(sample_events, sample_future_events, sample_past_events):
def test_split_dist_list(sample_events, sample_future_events, sample_past_events):
    past_events, future_events = split_event_dict_list(sample_events)
    assert sample_past_events == past_events
    assert sample_future_events == future_events
+0 −23
Original line number Diff line number Diff line
from django.conf import settings
from django.core.management.base import BaseCommand

from ...services import TweetDownloadService


class Command(BaseCommand):
    def add_arguments(self, parser):
        parser.add_argument(
            "--days_back",
            default=1,
            required=False,
            type=int,
            help="Stáří tweetů ve dnech",
        )

    def handle(self, *args, **options):
        tds = TweetDownloadService(
            bearer_token=settings.TWITTER_BEARER_TOKEN, days_back=options["days_back"]
        )
        tds.perform_update()

        self.stdout.write("\nUpdate of tweets finished!")
+0 −39
Original line number Diff line number Diff line
# Generated by Django 4.0.7 on 2022-08-19 08:35

from django.db import migrations, models


class Migration(migrations.Migration):
    initial = True

    dependencies = []

    operations = [
        migrations.CreateModel(
            name="Tweet",
            fields=[
                (
                    "id",
                    models.AutoField(
                        auto_created=True,
                        primary_key=True,
                        serialize=False,
                        verbose_name="ID",
                    ),
                ),
                (
                    "author_img_url",
                    models.URLField(
                        default="https://pbs.twimg.com/profile_images/1556544269443387394/jSO2A2Fr_200x200.jpg"
                    ),
                ),
                ("author_name", models.CharField(default="Piráti", max_length=128)),
                (
                    "author_username",
                    models.CharField(default="PiratskaStrana", max_length=128),
                ),
                ("text", models.TextField()),
                ("twitter_id", models.CharField(max_length=32, unique=True)),
            ],
        ),
    ]
+2 −2
Original line number Diff line number Diff line
@@ -2,7 +2,7 @@

from django.db import migrations, models

import twitter_utils.storages
import shared.storages


class Migration(migrations.Migration):
@@ -20,7 +20,7 @@ class Migration(migrations.Migration):
            name="author_img",
            field=models.ImageField(
                null=True,
                storage=twitter_utils.storages.OverwriteStorage,
                storage=shared.storages.OverwriteStorage,
                upload_to="twitter_accounts",
            ),
        ),
+2 −2
Original line number Diff line number Diff line
@@ -2,7 +2,7 @@

from django.db import migrations, models

import twitter_utils.storages
import shared.storages


class Migration(migrations.Migration):
@@ -15,7 +15,7 @@ class Migration(migrations.Migration):
            model_name="tweet",
            name="author_img",
            field=models.ImageField(
                storage=twitter_utils.storages.OverwriteStorage,
                storage=shared.storages.OverwriteStorage,
                upload_to="twitter_accounts",
            ),
        ),
+1 −1
Original line number Diff line number Diff line
from django.db import models

from twitter_utils.storages import OverwriteStorage
from shared.storages import OverwriteStorage


class TweetQueryset(models.QuerySet):

twitter_utils/services.py

deleted100644 → 0
+0 −173
Original line number Diff line number Diff line
import logging
import os
from datetime import timedelta
from typing import TYPE_CHECKING
from urllib import request

from django.core.files import File
from django.utils import timezone
from tweepy import Client
from tweepy.errors import BadRequest

from main.models import MainHomePage, MainPersonPage

from .models import Tweet

if TYPE_CHECKING:
    from tweepy import Media
    from tweepy import Tweet as TweetResponse
    from tweepy import User


logger = logging.getLogger()


class TweetDownloadService:
    """
    Service class starající se o update tweetů z Twitter API, v současné chvíli
    bere tweety z účtu nastavených v (první) MainHomePage stránce (HP pirati.cz).
    """

    client: Client
    days_back: int

    def __init__(self, bearer_token, days_back=1):
        if not bearer_token:
            raise RuntimeError("Twitter bearer token not set, cannot update tweets")

        self.client = Client(bearer_token=bearer_token)
        self.days_back = days_back

    @staticmethod
    def download_remote_image(image_url) -> (str, File):
        try:
            response = request.urlretrieve(image_url)
        except Exception as exc:
            logger.warning(exc)
            return "", None
        return os.path.basename(image_url), File(open(response[0], "rb"))

    @staticmethod
    def get_existing_tweet_id_list() -> list[int]:
        """
        Vrací IDs už uložených Tweetů - možná by stálo za to brát jen z určitého
        časového období...
        """
        return Tweet.objects.values_list("twitter_id", flat=True)

    @staticmethod
    def get_tweet_media_url(media_key, media_list):
        return next(m.url for m in media_list if m.media_key == media_key)

    def get_tweets_response(self, user_id) -> (list["TweetResponse"], list["Media"]):
        """
        Vrací list tweetů (objektů) pro daného Twitter uživatele.
        """
        tweets_response = self.client.get_users_tweets(
            user_id,
            exclude=["retweets"],
            expansions=[
                "author_id",
                "attachments.media_keys",
                "entities.mentions.username",
            ],
            max_results=100,
            media_fields=["url"],  # TODO use this? download need probably
            start_time=timezone.now() - timedelta(days=self.days_back),
            tweet_fields=["author_id", "created_at", "in_reply_to_user_id"],
            user_fields=["name", "username"],
        )

        return tweets_response.data or [], tweets_response[1].get("media", [])

    def get_user_list_data(self) -> list["User"]:
        twitter_usernames_block = MainHomePage.objects.first().twitter_usernames
        person_username_list = (
            MainPersonPage.objects.filter(twitter_username__isnull=False)
            .values_list("twitter_username", flat=True)
            .distinct()
        )
        homepage_username_list = [
            username_data["value"] for username_data in twitter_usernames_block.raw_data
        ]

        # kvůli duplicitám udělám list/set/list konverzi
        username_list = list({*person_username_list, *homepage_username_list})

        user_data_list = []

        for username in username_list:
            try:
                user_data_list.append(self.get_user_response(username))
            except BadRequest:
                logger.error(
                    "Cannot download tweets for the username",
                    extra={"username": username},
                )

        return user_data_list

    def get_user_response(self, username) -> "User":
        """
        Vrací informace o daném uživateli.
        """
        user_response = self.client.get_user(
            username=username,
            user_fields=["profile_image_url"],  # id, name, username enabled by default
        )

        return user_response.data

    def perform_update(self) -> int:
        """
        Obaluje celý proces downloadu Tweetů z API do DB.
        """
        existing_tweet_id_list = self.get_existing_tweet_id_list()
        user_data_list = self.get_user_list_data()

        tweets_to_save = []

        for user_data in user_data_list:
            tweet_resp_list, media_list = self.get_tweets_response(user_id=user_data.id)
            for tweet_response in tweet_resp_list:
                if (
                    # tweet již načten, nebo je odpověď
                    str(tweet_response.id) in existing_tweet_id_list
                    or tweet_response.in_reply_to_user_id is not None
                ):
                    continue

                # vyzobej data z responses
                tweet = Tweet(
                    author_name=user_data.name,
                    author_username=user_data.username,
                    text=tweet_response.text.split("https://t.co")[0],
                    twitter_id=str(tweet_response.id),
                )

                # ulož obrázek Twitter účtu do media
                tweet.author_img.save(
                    *self.download_remote_image(user_data.profile_image_url),
                    False,  # to prevent model save before bulk create
                )

                # zkus dohledat obrázek pro Tweet
                if tweet_response.attachments:
                    self.try_find_image_for_tweet(tweet, tweet_response, media_list)

                # přidej do seznamu k uložení
                tweets_to_save.append(tweet)

        return Tweet.objects.bulk_create(tweets_to_save)

    def try_find_image_for_tweet(
        self, tweet: Tweet, tweet_response: "TweetResponse", media_list: list["Media"]
    ):
        tweet_media_keys = tweet_response.attachments.get("media_keys", [])
        if tweet_media_keys:
            img_url = self.get_tweet_media_url(tweet_media_keys[0], media_list)
            if img_url:  # ne vždycky je obrázek v media_listu...
                tweet.image.save(
                    *self.download_remote_image(image_url=img_url),
                    False,  # to prevent model save before bulk create
                )
+2 −8
Original line number Diff line number Diff line
from wagtail.blocks import (
    CharBlock,
    ListBlock,
    PageChooserBlock,
    StructBlock,
    URLBlock,
)
from wagtail.blocks import CharBlock, ListBlock, PageChooserBlock, StructBlock, URLBlock


class PersonUrlBlock(StructBlock):
+310 −38
Original line number Diff line number Diff line
# Generated by Django 4.1.6 on 2023-03-09 23:46

from django.db import migrations, models
import django.db.models.deletion
import shared.models
import wagtail.blocks
import wagtail.fields
import wagtailmetadata.models
from django.db import migrations, models

import shared.models

class Migration(migrations.Migration):

class Migration(migrations.Migration):
    dependencies = [
        ('wagtailcore', '0083_workflowcontenttype'),
        ('wagtailimages', '0025_alter_image_file_alter_rendition_file'),
        ('uniweb', '0037_alter_uniwebflexiblepage_content_and_more'),
        ("wagtailcore", "0083_workflowcontenttype"),
        ("wagtailimages", "0025_alter_image_file_alter_rendition_file"),
        ("uniweb", "0037_alter_uniwebflexiblepage_content_and_more"),
    ]

    operations = [
        migrations.CreateModel(
            name='UniwebPersonPage',
            name="UniwebPersonPage",
            fields=[
                ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
                ('job', models.CharField(blank=True, help_text="Např. 'Informatik'", max_length=128, null=True, verbose_name='Povolání')),
                ('job_function', models.CharField(blank=True, help_text="Např. 'Předseda'", max_length=128, null=True, verbose_name='Funkce')),
                ('text', wagtail.fields.RichTextField(blank=True, verbose_name='text')),
                ('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Email')),
                ('show_email', models.BooleanField(default=True, verbose_name='Zobrazovat email na stránce?')),
                ('phone', models.CharField(blank=True, max_length=16, null=True, verbose_name='Telefon')),
                ('city', models.CharField(blank=True, max_length=64, null=True, verbose_name='Město/obec')),
                ('age', models.IntegerField(blank=True, null=True, verbose_name='Věk')),
                ('is_pirate', models.BooleanField(default=True, verbose_name='Je členem Pirátské strany?')),
                ('other_party', models.CharField(blank=True, help_text='Vyplňte pokud osoba není Pirát', max_length=64, null=True, verbose_name='Strana')),
                ('facebook_url', models.URLField(blank=True, null=True, verbose_name='Odkaz na Facebook')),
                ('instagram_url', models.URLField(blank=True, null=True, verbose_name='Odkaz na Instagram')),
                ('twitter_url', models.URLField(blank=True, null=True, verbose_name='Odkaz na Twitter')),
                ('youtube_url', models.URLField(blank=True, null=True, verbose_name='Odkaz na Youtube kanál')),
                ('flickr_url', models.URLField(blank=True, null=True, verbose_name='Odkaz na Flickr')),
                ('custom_web_url', models.URLField(blank=True, null=True, verbose_name='Odkaz na vlastní web')),
                ('other_urls', wagtail.fields.StreamField([('other_url', wagtail.blocks.StructBlock([('title', wagtail.blocks.CharBlock(label='Název', required=True)), ('url', wagtail.blocks.URLBlock(label='URL', required=True)), ('custom_icon', wagtail.blocks.CharBlock(help_text="Pro vlastní ikonku zadejde název ikonky z https://styleguide.pirati.cz/latest/?p=viewall-atoms-icons (bez tečky), např. 'ico--beer'", label='Vlastní ikonka ze styleguide', required=False))]))], blank=True, use_json_field=True, verbose_name='Další odkaz')),
                ('background_photo', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='wagtailimages.image', verbose_name='obrázek do záhlaví')),
                ('other_party_logo', models.ForeignKey(blank=True, help_text='Vyplňte pokud osoba není Pirát', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='wagtailimages.image', verbose_name='Logo strany')),
                ('profile_photo', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='wagtailimages.image', verbose_name='profilová fotka')),
                ('search_image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image', verbose_name='Search image')),
                (
                    "page_ptr",
                    models.OneToOneField(
                        auto_created=True,
                        on_delete=django.db.models.deletion.CASCADE,
                        parent_link=True,
                        primary_key=True,
                        serialize=False,
                        to="wagtailcore.page",
                    ),
                ),
                (
                    "job",
                    models.CharField(
                        blank=True,
                        help_text="Např. 'Informatik'",
                        max_length=128,
                        null=True,
                        verbose_name="Povolání",
                    ),
                ),
                (
                    "job_function",
                    models.CharField(
                        blank=True,
                        help_text="Např. 'Předseda'",
                        max_length=128,
                        null=True,
                        verbose_name="Funkce",
                    ),
                ),
                ("text", wagtail.fields.RichTextField(blank=True, verbose_name="text")),
                (
                    "email",
                    models.EmailField(
                        blank=True, max_length=254, null=True, verbose_name="Email"
                    ),
                ),
                (
                    "show_email",
                    models.BooleanField(
                        default=True, verbose_name="Zobrazovat email na stránce?"
                    ),
                ),
                (
                    "phone",
                    models.CharField(
                        blank=True, max_length=16, null=True, verbose_name="Telefon"
                    ),
                ),
                (
                    "city",
                    models.CharField(
                        blank=True, max_length=64, null=True, verbose_name="Město/obec"
                    ),
                ),
                ("age", models.IntegerField(blank=True, null=True, verbose_name="Věk")),
                (
                    "is_pirate",
                    models.BooleanField(
                        default=True, verbose_name="Je členem Pirátské strany?"
                    ),
                ),
                (
                    "other_party",
                    models.CharField(
                        blank=True,
                        help_text="Vyplňte pokud osoba není Pirát",
                        max_length=64,
                        null=True,
                        verbose_name="Strana",
                    ),
                ),
                (
                    "facebook_url",
                    models.URLField(
                        blank=True, null=True, verbose_name="Odkaz na Facebook"
                    ),
                ),
                (
                    "instagram_url",
                    models.URLField(
                        blank=True, null=True, verbose_name="Odkaz na Instagram"
                    ),
                ),
                (
                    "twitter_url",
                    models.URLField(
                        blank=True, null=True, verbose_name="Odkaz na Twitter"
                    ),
                ),
                (
                    "youtube_url",
                    models.URLField(
                        blank=True, null=True, verbose_name="Odkaz na Youtube kanál"
                    ),
                ),
                (
                    "flickr_url",
                    models.URLField(
                        blank=True, null=True, verbose_name="Odkaz na Flickr"
                    ),
                ),
                (
                    "custom_web_url",
                    models.URLField(
                        blank=True, null=True, verbose_name="Odkaz na vlastní web"
                    ),
                ),
                (
                    "other_urls",
                    wagtail.fields.StreamField(
                        [
                            (
                                "other_url",
                                wagtail.blocks.StructBlock(
                                    [
                                        (
                                            "title",
                                            wagtail.blocks.CharBlock(
                                                label="Název", required=True
                                            ),
                                        ),
                                        (
                                            "url",
                                            wagtail.blocks.URLBlock(
                                                label="URL", required=True
                                            ),
                                        ),
                                        (
                                            "custom_icon",
                                            wagtail.blocks.CharBlock(
                                                help_text="Pro vlastní ikonku zadejde název ikonky z https://styleguide.pirati.cz/latest/?p=viewall-atoms-icons (bez tečky), např. 'ico--beer'",
                                                label="Vlastní ikonka ze styleguide",
                                                required=False,
                                            ),
                                        ),
                                    ]
                                ),
                            )
                        ],
                        blank=True,
                        use_json_field=True,
                        verbose_name="Další odkaz",
                    ),
                ),
                (
                    "background_photo",
                    models.ForeignKey(
                        blank=True,
                        null=True,
                        on_delete=django.db.models.deletion.PROTECT,
                        related_name="+",
                        to="wagtailimages.image",
                        verbose_name="obrázek do záhlaví",
                    ),
                ),
                (
                    "other_party_logo",
                    models.ForeignKey(
                        blank=True,
                        help_text="Vyplňte pokud osoba není Pirát",
                        null=True,
                        on_delete=django.db.models.deletion.PROTECT,
                        related_name="+",
                        to="wagtailimages.image",
                        verbose_name="Logo strany",
                    ),
                ),
                (
                    "profile_photo",
                    models.ForeignKey(
                        blank=True,
                        null=True,
                        on_delete=django.db.models.deletion.PROTECT,
                        related_name="+",
                        to="wagtailimages.image",
                        verbose_name="profilová fotka",
                    ),
                ),
                (
                    "search_image",
                    models.ForeignKey(
                        blank=True,
                        null=True,
                        on_delete=django.db.models.deletion.SET_NULL,
                        related_name="+",
                        to="wagtailimages.image",
                        verbose_name="Search image",
                    ),
                ),
            ],
            options={
                'verbose_name': 'Detail osoby',
                'ordering': ('title',),
                "verbose_name": "Detail osoby",
                "ordering": ("title",),
            },
            bases=(shared.models.SubpageMixin, wagtailmetadata.models.WagtailImageMetadataMixin, 'wagtailcore.page', models.Model),
            bases=(
                shared.models.SubpageMixin,
                wagtailmetadata.models.WagtailImageMetadataMixin,
                "wagtailcore.page",
                models.Model,
            ),
        ),
        migrations.CreateModel(
            name='UniwebPeoplePage',
            name="UniwebPeoplePage",
            fields=[
                ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
                ('content', wagtail.fields.StreamField([('text', wagtail.blocks.RichTextBlock(features=['h2', 'h3', 'h4', 'h5', 'bold', 'italic', 'ol', 'ul', 'hr', 'link', 'document-link', 'image', 'superscript', 'subscript', 'strikethrough', 'blockquote', 'embed'], label='Textový editor')), ('people_group', wagtail.blocks.StructBlock([('group_title', wagtail.blocks.CharBlock(label='Titulek', required=True)), ('person_list', wagtail.blocks.ListBlock(wagtail.blocks.StructBlock([('position', wagtail.blocks.CharBlock(label='Název pozice', required=False)), ('person', wagtail.blocks.PageChooserBlock(label='Osoba', page_type=['uniweb.UniwebPersonPage']))]), label='List osob'))]))], blank=True, use_json_field=True, verbose_name='Obsah stránky')),
                ('search_image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image', verbose_name='Search image')),
                (
                    "page_ptr",
                    models.OneToOneField(
                        auto_created=True,
                        on_delete=django.db.models.deletion.CASCADE,
                        parent_link=True,
                        primary_key=True,
                        serialize=False,
                        to="wagtailcore.page",
                    ),
                ),
                (
                    "content",
                    wagtail.fields.StreamField(
                        [
                            (
                                "text",
                                wagtail.blocks.RichTextBlock(
                                    features=[
                                        "h2",
                                        "h3",
                                        "h4",
                                        "h5",
                                        "bold",
                                        "italic",
                                        "ol",
                                        "ul",
                                        "hr",
                                        "link",
                                        "document-link",
                                        "image",
                                        "superscript",
                                        "subscript",
                                        "strikethrough",
                                        "blockquote",
                                        "embed",
                                    ],
                                    label="Textový editor",
                                ),
                            ),
                            (
                                "people_group",
                                wagtail.blocks.StructBlock(
                                    [
                                        (
                                            "group_title",
                                            wagtail.blocks.CharBlock(
                                                label="Titulek", required=True
                                            ),
                                        ),
                                        (
                                            "person_list",
                                            wagtail.blocks.ListBlock(
                                                wagtail.blocks.StructBlock(
                                                    [
                                                        (
                                                            "position",
                                                            wagtail.blocks.CharBlock(
                                                                label="Název pozice",
                                                                required=False,
                                                            ),
                                                        ),
                                                        (
                                                            "person",
                                                            wagtail.blocks.PageChooserBlock(
                                                                label="Osoba",
                                                                page_type=[
                                                                    "uniweb.UniwebPersonPage"
                                                                ],
                                                            ),
                                                        ),
                                                    ]
                                                ),
                                                label="List osob",
                                            ),
                                        ),
                                    ]
                                ),
                            ),
                        ],
                        blank=True,
                        use_json_field=True,
                        verbose_name="Obsah stránky",
                    ),
                ),
                (
                    "search_image",
                    models.ForeignKey(
                        blank=True,
                        null=True,
                        on_delete=django.db.models.deletion.SET_NULL,
                        related_name="+",
                        to="wagtailimages.image",
                        verbose_name="Search image",
                    ),
                ),
            ],
            options={
                'verbose_name': 'Lidé',
                "verbose_name": "Lidé",
            },
            bases=(shared.models.SubpageMixin, wagtailmetadata.models.WagtailImageMetadataMixin, 'wagtailcore.page', models.Model),
            bases=(
                shared.models.SubpageMixin,
                wagtailmetadata.models.WagtailImageMetadataMixin,
                "wagtailcore.page",
                models.Model,
            ),
        ),
    ]
+33 −6
Original line number Diff line number Diff line
# Generated by Django 4.1.6 on 2023-03-13 09:58

from django.db import migrations
import wagtail.blocks
import wagtail.fields
from django.db import migrations


class Migration(migrations.Migration):

    dependencies = [
        ('uniweb', '0038_uniwebpersonpage_uniwebpeoplepage'),
        ("uniweb", "0038_uniwebpersonpage_uniwebpeoplepage"),
    ]

    operations = [
        migrations.AlterField(
            model_name='uniwebhomepage',
            name='top_menu',
            field=wagtail.fields.StreamField([('item', wagtail.blocks.StructBlock([('name', wagtail.blocks.CharBlock(label='název')), ('page', wagtail.blocks.PageChooserBlock(label='stránka', page_type=['uniweb.UniwebHomePage', 'uniweb.UniwebFlexiblePage', 'uniweb.UniwebArticlesIndexPage', 'uniweb.UniwebFormPage', 'uniweb.UniwebPeoplePage', 'uniweb.UniwebPersonPage']))]))], blank=True, use_json_field=True, verbose_name='horní menu'),
            model_name="uniwebhomepage",
            name="top_menu",
            field=wagtail.fields.StreamField(
                [
                    (
                        "item",
                        wagtail.blocks.StructBlock(
                            [
                                ("name", wagtail.blocks.CharBlock(label="název")),
                                (
                                    "page",
                                    wagtail.blocks.PageChooserBlock(
                                        label="stránka",
                                        page_type=[
                                            "uniweb.UniwebHomePage",
                                            "uniweb.UniwebFlexiblePage",
                                            "uniweb.UniwebArticlesIndexPage",
                                            "uniweb.UniwebFormPage",
                                            "uniweb.UniwebPeoplePage",
                                            "uniweb.UniwebPersonPage",
                                        ],
                                    ),
                                ),
                            ]
                        ),
                    )
                ],
                blank=True,
                use_json_field=True,
                verbose_name="horní menu",
            ),
        ),
    ]
+2964 −0

File added.

Preview size limit exceeded, changes collapsed.

+2964 −0

File added.

Preview size limit exceeded, changes collapsed.

+1 −5
Original line number Diff line number Diff line
@@ -34,11 +34,7 @@ from shared.models import (
from shared.utils import make_promote_panels
from tuning import admin_help

from .blocks import (
    PersonCustomPositionBlock,
    PeopleGroupListBlock,
    PersonUrlBlock,
)
from .blocks import PeopleGroupListBlock, PersonCustomPositionBlock, PersonUrlBlock
from .constants import (
    ALIGN_CHOICES,
    ALIGN_CSS,