diff --git a/main/static/main/css/styles.css b/main/static/main/css/styles.css index 5dc5146ed83be3e041627147452a4601dc03cde0..54e5929ddff1711384fc693feee685a8534bc89d 100644 --- a/main/static/main/css/styles.css +++ b/main/static/main/css/styles.css @@ -3608,6 +3608,14 @@ div.twitter-carousel .slick-arrow.slick-disabled:before, div.twitter-carousel .s height: 15rem; } +.h-10{ + height: 2.5rem; +} + +.h-80{ + height: 20rem; +} + .h-fit{ height: -webkit-fit-content; height: -moz-fit-content; @@ -3674,6 +3682,10 @@ div.twitter-carousel .slick-arrow.slick-disabled:before, div.twitter-carousel .s max-width: 42rem; } +.flex-shrink-0{ + flex-shrink: 0; +} + .shrink-0{ flex-shrink: 0; } @@ -3718,6 +3730,10 @@ div.twitter-carousel .slick-arrow.slick-disabled:before, div.twitter-carousel .s justify-content: flex-start; } +.justify-end{ + justify-content: flex-end; +} + .justify-center{ justify-content: center; } @@ -4567,10 +4583,6 @@ a.icon-link:hover span{ width: 41.666667%; } - .sm\:max-w-xs{ - max-width: 20rem; - } - .sm\:flex-col{ flex-direction: column; } diff --git a/main/styleguide/source/_patterns/molecules/twitter-box.mustache b/main/styleguide/source/_patterns/molecules/twitter-box.mustache index 05fd073231a1423dd648bc63cd146906074ed681..e895c73b58b63d61d7e701538149dab362cdcefc 100644 --- a/main/styleguide/source/_patterns/molecules/twitter-box.mustache +++ b/main/styleguide/source/_patterns/molecules/twitter-box.mustache @@ -1,6 +1,6 @@ -<a href="#" class="mb-5 p-4 w-full flex flex-col items-center justify-between text-center border border-grey-100 sm:mb-0 hover:no-underline"> - <div> - <div class="flex flex-row sm:flex-col items-start sm:items-center"> +<a href="#" class="mb-5 w-full flex flex-col items-center justify-end overflow-hidden text-center border border-grey-100 sm:mb-0 hover:no-underline"> + <div class="h-full p-4"> + <div class="flex flex-row sm:flex-col items-start sm:items-center justify-center"> <img class="rounded-full shadow-sm w-12 mr-2 sm:mr-0 mb-0 sm:mb-2" src="https://randomuser.me/api/portraits/women/56.jpg" @@ -14,12 +14,22 @@ @pirat.tomas.marny </small> </div> + <p class="text-small sm:text-base leading-6 mb-2"> + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam + nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et + justo duo dolores et ea rebum. + </p> </div> - <p class="text-small sm:text-base leading-6 mb-2"> - Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam - nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et - justo duo dolores et ea rebum. - </p> </div> - <i class="ico--twitter text-turquoise-400 text-3xl sm:text-xl"></i> + + <div class="flex-shrink-0 h-10"> + <i class="ico--twitter text-turquoise-400 text-3xl sm:text-xl"></i> + </div> + + <div class="flex-shrink-0 h-80"> + <img + src="https://randomuser.me/api/portraits/women/56.jpg" + alt="Obrázek Tweetu" + /> + </div> </a> diff --git a/main/styleguide/source/_patterns/organisms/twitter-section.mustache b/main/styleguide/source/_patterns/organisms/twitter-section.mustache index dc645e909ab855182be58426ecab37b543a35edc..5cedfbcd3d74a1f9099530089924ebebb2924715 100644 --- a/main/styleguide/source/_patterns/organisms/twitter-section.mustache +++ b/main/styleguide/source/_patterns/organisms/twitter-section.mustache @@ -5,16 +5,16 @@ </h2> </div> <div class="flex flex-wrap justify-center"> - <div class="w-full flex max-w-sm sm:max-w-xs"> + <div class="flex max-w-sm max-w-xs w-full"> {{> molecules-twitter-box }} </div> - <div class="w-full flex max-w-sm sm:max-w-xs"> + <div class="flex max-w-sm max-w-xs w-full"> {{> molecules-twitter-box }} </div> - <div class="w-full flex max-w-sm sm:max-w-xs"> + <div class="flex max-w-sm max-w-xs w-full"> {{> molecules-twitter-box }} </div> - <div class="w-full flex max-w-sm sm:max-w-xs"> + <div class="flex max-w-sm max-w-xs w-full"> {{> molecules-twitter-box }} </div> </div> diff --git a/main/templates/main/includes/twitter_widget.html b/main/templates/main/includes/twitter_widget.html index befe8e701492d6b8d2c25abcb7233b152d700bac..4b3da50580621682122f63abf3fd3f990c2bab1d 100644 --- a/main/templates/main/includes/twitter_widget.html +++ b/main/templates/main/includes/twitter_widget.html @@ -1,15 +1,15 @@ <div class="flex flex-wrap justify-center"> {% for tweet in tweet_list %} - <div class="w-full flex max-w-sm sm:max-w-xs"> + <div class="flex max-w-sm max-w-xs w-full"> <a href="https://twitter.com/{{ tweet.author_username }}" - class="mb-5 p-4 w-full flex flex-col items-center justify-between text-center border border-grey-100 sm:mb-0 hover:no-underline" + class="mb-5 w-full flex flex-col items-center justify-end overflow-hidden text-center border border-grey-100 sm:mb-0 hover:no-underline" > - <div> - <div class="flex flex-row sm:flex-col items-start sm:items-center"> + <div class="h-full p-4"> + <div class="flex flex-row sm:flex-col items-start sm:items-center justify-center"> <img class="rounded-full shadow-sm w-12 mr-2 sm:mr-0 mb-0 sm:mb-2" - src="{{ tweet.author_img_url }}" + src="{{ tweet.author_img.url }}" alt="user image" /> <div class="flex flex-col sm:flex-col"> @@ -21,11 +21,20 @@ </small> </div> </div> - <p class="text-small sm:text-base leading-6 mb-2"> + <p class="text-small sm:text-base leading-6"> {{ tweet.text }} </p> </div> - <i class="ico--twitter text-turquoise-400 text-3xl sm:text-xl"></i> + + <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="flex-shrink-0 h-80" style="height: 320px;"> + <img src="{{ tweet.image.url }}" alt="Obrázek Tweetu" /> + </div> + {% endif %} </a> </div> {% endfor %} diff --git a/main/templates/main/main_person_page.html b/main/templates/main/main_person_page.html index 2e260edbfffa7ad8b985114a8d07b2ac749c7efa..175d75e46333b4e70b05f48e271137c8d17de72f 100644 --- a/main/templates/main/main_person_page.html +++ b/main/templates/main/main_person_page.html @@ -68,7 +68,7 @@ <div class="mb-5 p-4 flex flex-col h-full items-center text-center border border-grey-100 sm:mb-0"> <div class="flex flex-row sm:flex-col items-center"> <img class="rounded-full shadow-sm w-12 h-12 mb-4 sm:mb-2" - src="{{ tweet.author_img_url }}" + src="{{ tweet.author_img.url }}" alt="user image"/> <div class="flex flex-col sm:flex-col"> <h5 class="font-alt text-xl mb-2 sm:text-base">{{ tweet.author_name }}</h5> diff --git a/twitter_utils/migrations/0002_remove_tweet_author_img_url_tweet_author_img_and_more.py b/twitter_utils/migrations/0002_remove_tweet_author_img_url_tweet_author_img_and_more.py new file mode 100644 index 0000000000000000000000000000000000000000..6a4e74c3ae0f9b3b3375096cf8bd7ba8d88e9a79 --- /dev/null +++ b/twitter_utils/migrations/0002_remove_tweet_author_img_url_tweet_author_img_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 4.0.7 on 2022-10-21 09:45 + +from django.db import migrations, models + +import twitter_utils.storages + + +class Migration(migrations.Migration): + + dependencies = [ + ("twitter_utils", "0001_initial"), + ] + + operations = [ + migrations.RemoveField( + model_name="tweet", + name="author_img_url", + ), + migrations.AddField( + model_name="tweet", + name="author_img", + field=models.ImageField( + null=True, + storage=twitter_utils.storages.OverwriteStorage, + upload_to="twitter_accounts", + ), + ), + migrations.AddField( + model_name="tweet", + name="image", + field=models.ImageField(null=True, upload_to="twitter"), + ), + ] diff --git a/twitter_utils/migrations/0003_alter_tweet_author_img.py b/twitter_utils/migrations/0003_alter_tweet_author_img.py new file mode 100644 index 0000000000000000000000000000000000000000..ce4ae896c6cb6c4566931e47e581de039b549ea3 --- /dev/null +++ b/twitter_utils/migrations/0003_alter_tweet_author_img.py @@ -0,0 +1,23 @@ +# Generated by Django 4.0.7 on 2022-10-21 09:45 + +from django.db import migrations, models + +import twitter_utils.storages + + +class Migration(migrations.Migration): + + dependencies = [ + ("twitter_utils", "0002_remove_tweet_author_img_url_tweet_author_img_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="tweet", + name="author_img", + field=models.ImageField( + storage=twitter_utils.storages.OverwriteStorage, + upload_to="twitter_accounts", + ), + ), + ] diff --git a/twitter_utils/models.py b/twitter_utils/models.py index 0f4c7115dc95b96d20c8d733ec5199aa8febc9ee..b15e9eb8f4622e7110c49a4cc949c46fd668123d 100644 --- a/twitter_utils/models.py +++ b/twitter_utils/models.py @@ -1,5 +1,7 @@ from django.db import models +from twitter_utils.storages import OverwriteStorage + class TweetQueryset(models.QuerySet): def username(self, username): @@ -16,11 +18,13 @@ class Tweet(models.Model): nejnovějších Tweetů (2022). """ - author_img_url = models.URLField( - default="https://pbs.twimg.com/profile_images/1556544269443387394/jSO2A2Fr_200x200.jpg" - ) # TODO consider another default, maybe from static + author_img = models.ImageField( + storage=OverwriteStorage, upload_to="twitter_accounts" + ) author_name = models.CharField(max_length=128, default="Piráti") author_username = models.CharField(max_length=128, default="PiratskaStrana") + + image = models.ImageField(null=True, upload_to="twitter") text = models.TextField() twitter_id = models.CharField(max_length=32, unique=True) diff --git a/twitter_utils/services.py b/twitter_utils/services.py index 9057c65b71d24eab13618299795c09e49322b27d..38eec1eb6050714d23f657409aab68e3e941608e 100644 --- a/twitter_utils/services.py +++ b/twitter_utils/services.py @@ -1,7 +1,10 @@ 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 @@ -11,6 +14,7 @@ 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 @@ -35,14 +39,27 @@ class TweetDownloadService: self.days_back = days_back @staticmethod - def get_latest_saved_tweet_id() -> list[int]: + 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) - def get_tweets_response(self, user_id) -> list["TweetResponse"]: + @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. """ @@ -61,7 +78,7 @@ class TweetDownloadService: user_fields=["name", "username"], ) - return tweets_response.data or [] + 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 @@ -105,22 +122,48 @@ class TweetDownloadService: """ 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() - existing_tweet_id_list = self.get_latest_saved_tweet_id() tweets_to_save = [] for user_data in user_data_list: - for tweet in self.get_tweets_response(user_id=user_data.id): - if str(tweet.id) not in existing_tweet_id_list: - tweets_to_save.append( - Tweet( - author_img_url=user_data.profile_image_url, - author_name=user_data.name, - author_username=user_data.username, - text=tweet.text.split("https://t.co")[0], - twitter_id=str(tweet.id), - ) - ) + tweet_resp_list, media_list = self.get_tweets_response(user_id=user_data.id) + for tweet_response in tweet_resp_list: + if str(tweet_response.id) in existing_tweet_id_list: + 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 + ) diff --git a/twitter_utils/storages.py b/twitter_utils/storages.py new file mode 100644 index 0000000000000000000000000000000000000000..04ffc57c65063e95d4a2cc24074df87088d19fa2 --- /dev/null +++ b/twitter_utils/storages.py @@ -0,0 +1,19 @@ +import os + +from django.conf import settings +from django.core.files.storage import get_storage_class + + +class OverwriteStorage(get_storage_class()): + def get_available_name(self, name, max_length): + """ + Returns a filename that's free on the target storage system, and + available for new content to be written to. This file storage solves overwrite + on upload problem. + + 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