From 8962dc7a554eeb7684ff46b119129a946ed10b7a Mon Sep 17 00:00:00 2001 From: esir Date: Mon, 1 Nov 2021 13:00:48 +0300 Subject: [PATCH 01/15] Create a list model --- twoopstracker/twoops/models.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/twoopstracker/twoops/models.py b/twoopstracker/twoops/models.py index 1a703f73..521ae33c 100644 --- a/twoopstracker/twoops/models.py +++ b/twoopstracker/twoops/models.py @@ -55,3 +55,22 @@ class TwitterAccount(TimestampedModelMixin): def __str__(self): return self.screen_name + + +class List(TimestampedModelMixin): + """ + A model holding lists of twitter accounts + """ + + list_id = models.BigIntegerField(primary_key=True) + name = models.CharField(max_length=255, help_text=_("List Name")) + owner = models.ForeignKey("TwitterAccount", on_delete=models.CASCADE) + members_count = models.IntegerField() + subscribers_count = models.IntegerField() + deleted = models.BooleanField( + default=False, + help_text=_("When deleted is true, we aren't tracking this list anymore."), + ) + + def __str__(self): + return self.name From 2623db5e0d9108e0dc3dcdc3362b439a2315990b Mon Sep 17 00:00:00 2001 From: esir Date: Mon, 1 Nov 2021 13:33:42 +0300 Subject: [PATCH 02/15] Creates a user profile model for linking with a twitter list --- .../twoops/migrations/0003_userprofile.py | 35 +++++++++++++++++++ twoopstracker/twoops/models.py | 17 ++++----- 2 files changed, 41 insertions(+), 11 deletions(-) create mode 100644 twoopstracker/twoops/migrations/0003_userprofile.py diff --git a/twoopstracker/twoops/migrations/0003_userprofile.py b/twoopstracker/twoops/migrations/0003_userprofile.py new file mode 100644 index 00000000..5d295bab --- /dev/null +++ b/twoopstracker/twoops/migrations/0003_userprofile.py @@ -0,0 +1,35 @@ +# Generated by Django 3.2.8 on 2021-11-01 10:28 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentication", "0001_user_model"), + ("twoops", "0002_twitter_account"), + ] + + operations = [ + migrations.CreateModel( + name="UserProfile", + fields=[ + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + primary_key=True, + serialize=False, + to="authentication.user", + ), + ), + ], + options={ + "get_latest_by": "updated_at", + "abstract": False, + }, + ), + ] diff --git a/twoopstracker/twoops/models.py b/twoopstracker/twoops/models.py index 521ae33c..1b35d7d2 100644 --- a/twoopstracker/twoops/models.py +++ b/twoopstracker/twoops/models.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.db import models from django.utils.translation import ugettext_lazy as _ @@ -57,20 +58,14 @@ def __str__(self): return self.screen_name -class List(TimestampedModelMixin): +class UserProfile(TimestampedModelMixin): """ - A model holding lists of twitter accounts + User Profile model """ - list_id = models.BigIntegerField(primary_key=True) - name = models.CharField(max_length=255, help_text=_("List Name")) - owner = models.ForeignKey("TwitterAccount", on_delete=models.CASCADE) - members_count = models.IntegerField() - subscribers_count = models.IntegerField() - deleted = models.BooleanField( - default=False, - help_text=_("When deleted is true, we aren't tracking this list anymore."), + user = models.OneToOneField( + settings.AUTH_USER_MODEL, on_delete=models.CASCADE, primary_key=True ) def __str__(self): - return self.name + return self.user.username From 86419da4a092c0b2950d7b8c4c00a8102a6f858f Mon Sep 17 00:00:00 2001 From: esir Date: Mon, 1 Nov 2021 13:35:38 +0300 Subject: [PATCH 03/15] Associate a list with a user profile --- twoopstracker/twoops/migrations/0004_list.py | 55 ++++++++++++++++++++ twoopstracker/twoops/models.py | 14 +++++ 2 files changed, 69 insertions(+) create mode 100644 twoopstracker/twoops/migrations/0004_list.py diff --git a/twoopstracker/twoops/migrations/0004_list.py b/twoopstracker/twoops/migrations/0004_list.py new file mode 100644 index 00000000..300c3ae5 --- /dev/null +++ b/twoopstracker/twoops/migrations/0004_list.py @@ -0,0 +1,55 @@ +# Generated by Django 3.2.8 on 2021-11-01 10:34 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("twoops", "0003_userprofile"), + ] + + operations = [ + migrations.CreateModel( + name="List", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "name", + models.CharField(help_text="Name of Twitter List", max_length=255), + ), + ( + "slug", + models.CharField(help_text="Twitter List Slug", max_length=255), + ), + ( + "accounts", + models.ManyToManyField( + related_name="lists", to="twoops.TwitterAccount" + ), + ), + ( + "owner", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="twoops.userprofile", + ), + ), + ], + options={ + "get_latest_by": "updated_at", + "abstract": False, + }, + ), + ] diff --git a/twoopstracker/twoops/models.py b/twoopstracker/twoops/models.py index 1b35d7d2..f2ebebf1 100644 --- a/twoopstracker/twoops/models.py +++ b/twoopstracker/twoops/models.py @@ -69,3 +69,17 @@ class UserProfile(TimestampedModelMixin): def __str__(self): return self.user.username + + +class List(TimestampedModelMixin): + """ + List model + """ + + name = models.CharField(max_length=255, help_text=_("Name of Twitter List")) + slug = models.CharField(max_length=255, help_text=_("Twitter List Slug")) + owner = models.ForeignKey("UserProfile", on_delete=models.CASCADE) + accounts = models.ManyToManyField("TwitterAccount", related_name="lists") + + def __str__(self): + return self.name From c676ccc3bdbbd5a60bfb80c7110053cbdb292f1b Mon Sep 17 00:00:00 2001 From: esir Date: Mon, 1 Nov 2021 16:44:33 +0300 Subject: [PATCH 04/15] Adds API endpoints to display account lists --- twoopstracker/twoops/admin.py | 9 ++++++++- twoopstracker/twoops/migrations/0004_list.py | 2 +- twoopstracker/twoops/models.py | 2 +- twoopstracker/twoops/serializers.py | 8 +++++++- twoopstracker/twoops/urls.py | 4 +++- twoopstracker/twoops/views.py | 17 +++++++++++++++-- 6 files changed, 35 insertions(+), 7 deletions(-) diff --git a/twoopstracker/twoops/admin.py b/twoopstracker/twoops/admin.py index 13a0b56d..0dc14e7a 100644 --- a/twoopstracker/twoops/admin.py +++ b/twoopstracker/twoops/admin.py @@ -1,10 +1,17 @@ from django.contrib import admin -from twoopstracker.twoops.models import Tweet, TwitterAccount +from twoopstracker.twoops.models import ( + Tweet, + TwitterAccount, + TwitterAccountsList, + UserProfile, +) admin.site.register( [ Tweet, TwitterAccount, + UserProfile, + TwitterAccountsList, ] ) diff --git a/twoopstracker/twoops/migrations/0004_list.py b/twoopstracker/twoops/migrations/0004_list.py index 300c3ae5..c525e764 100644 --- a/twoopstracker/twoops/migrations/0004_list.py +++ b/twoopstracker/twoops/migrations/0004_list.py @@ -12,7 +12,7 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name="List", + name="TwitterAccountsList", fields=[ ( "id", diff --git a/twoopstracker/twoops/models.py b/twoopstracker/twoops/models.py index f2ebebf1..975ad01a 100644 --- a/twoopstracker/twoops/models.py +++ b/twoopstracker/twoops/models.py @@ -71,7 +71,7 @@ def __str__(self): return self.user.username -class List(TimestampedModelMixin): +class TwitterAccountsList(TimestampedModelMixin): """ List model """ diff --git a/twoopstracker/twoops/serializers.py b/twoopstracker/twoops/serializers.py index 9a08c1b2..45747f81 100644 --- a/twoopstracker/twoops/serializers.py +++ b/twoopstracker/twoops/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from twoopstracker.twoops.models import Tweet, TwitterAccount +from twoopstracker.twoops.models import Tweet, TwitterAccount, TwitterAccountsList class TwitterAccountSerializer(serializers.ModelSerializer): @@ -14,3 +14,9 @@ class Meta: model = Tweet fields = "__all__" depth = 1 + + +class TwitterAccountListSerializer(serializers.ModelSerializer): + class Meta: + model = TwitterAccountsList + fields = "__all__" diff --git a/twoopstracker/twoops/urls.py b/twoopstracker/twoops/urls.py index 3fde2dbc..9bcb7ec4 100644 --- a/twoopstracker/twoops/urls.py +++ b/twoopstracker/twoops/urls.py @@ -1,7 +1,9 @@ from django.urls import path -from .views import TweetsView +from .views import AccountsList, SingleTwitterList, TweetsView urlpatterns = [ path("tweets/", TweetsView.as_view(), name="tweets"), + path("lists/", AccountsList.as_view(), name="accounts_list"), + path("lists/", SingleTwitterList.as_view(), name="single_account_list"), ] diff --git a/twoopstracker/twoops/views.py b/twoopstracker/twoops/views.py index 30d8acb4..cdd4bd90 100644 --- a/twoopstracker/twoops/views.py +++ b/twoopstracker/twoops/views.py @@ -3,8 +3,11 @@ from django.contrib.postgres.search import SearchQuery, SearchVector from rest_framework import generics -from twoopstracker.twoops.models import Tweet -from twoopstracker.twoops.serializers import TweetSerializer +from twoopstracker.twoops.models import Tweet, TwitterAccountsList +from twoopstracker.twoops.serializers import ( + TweetSerializer, + TwitterAccountListSerializer, +) def get_search_type(search_string): @@ -58,3 +61,13 @@ def get_queryset(self): if location: tweets = tweets.filter(owner__location=location) return tweets + + +class AccountsList(generics.ListCreateAPIView): + queryset = TwitterAccountsList.objects.all() + serializer_class = TwitterAccountListSerializer + + +class SingleTwitterList(generics.RetrieveUpdateDestroyAPIView): + queryset = TwitterAccountsList.objects.all() + serializer_class = TwitterAccountListSerializer From 426e924f06684600f437cf3d1d2c10b834367319 Mon Sep 17 00:00:00 2001 From: esir Date: Mon, 1 Nov 2021 16:56:36 +0300 Subject: [PATCH 05/15] Allow accounts to be private and public --- twoopstracker/twoops/migrations/0004_list.py | 5 +++++ twoopstracker/twoops/models.py | 1 + 2 files changed, 6 insertions(+) diff --git a/twoopstracker/twoops/migrations/0004_list.py b/twoopstracker/twoops/migrations/0004_list.py index c525e764..649e5684 100644 --- a/twoopstracker/twoops/migrations/0004_list.py +++ b/twoopstracker/twoops/migrations/0004_list.py @@ -46,6 +46,11 @@ class Migration(migrations.Migration): to="twoops.userprofile", ), ), + migrations.AddField( + model_name="twitteraccountslist", + name="is_private", + field=models.BooleanField(default=False), + ), ], options={ "get_latest_by": "updated_at", diff --git a/twoopstracker/twoops/models.py b/twoopstracker/twoops/models.py index 975ad01a..553e5479 100644 --- a/twoopstracker/twoops/models.py +++ b/twoopstracker/twoops/models.py @@ -80,6 +80,7 @@ class TwitterAccountsList(TimestampedModelMixin): slug = models.CharField(max_length=255, help_text=_("Twitter List Slug")) owner = models.ForeignKey("UserProfile", on_delete=models.CASCADE) accounts = models.ManyToManyField("TwitterAccount", related_name="lists") + is_private = models.BooleanField(default=False) def __str__(self): return self.name From 575117a9a1f67c6c5239600c2731fca08fa7f327 Mon Sep 17 00:00:00 2001 From: esir Date: Wed, 3 Nov 2021 12:39:36 +0300 Subject: [PATCH 06/15] Create a twitterclient app which will be responsible for twitter interactions e.g getting user details --- twoopstracker/twitterclient/__init__.py | 0 twoopstracker/twitterclient/admin.py | 0 twoopstracker/twitterclient/apps.py | 6 ++++++ twoopstracker/twitterclient/migrations/__init__.py | 0 twoopstracker/twitterclient/models.py | 0 twoopstracker/twitterclient/views.py | 0 6 files changed, 6 insertions(+) create mode 100644 twoopstracker/twitterclient/__init__.py create mode 100644 twoopstracker/twitterclient/admin.py create mode 100644 twoopstracker/twitterclient/apps.py create mode 100644 twoopstracker/twitterclient/migrations/__init__.py create mode 100644 twoopstracker/twitterclient/models.py create mode 100644 twoopstracker/twitterclient/views.py diff --git a/twoopstracker/twitterclient/__init__.py b/twoopstracker/twitterclient/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/twoopstracker/twitterclient/admin.py b/twoopstracker/twitterclient/admin.py new file mode 100644 index 00000000..e69de29b diff --git a/twoopstracker/twitterclient/apps.py b/twoopstracker/twitterclient/apps.py new file mode 100644 index 00000000..c4ea065f --- /dev/null +++ b/twoopstracker/twitterclient/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class TwitterclientConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "twoopstracker.twitterclient" diff --git a/twoopstracker/twitterclient/migrations/__init__.py b/twoopstracker/twitterclient/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/twoopstracker/twitterclient/models.py b/twoopstracker/twitterclient/models.py new file mode 100644 index 00000000..e69de29b diff --git a/twoopstracker/twitterclient/views.py b/twoopstracker/twitterclient/views.py new file mode 100644 index 00000000..e69de29b From 5b4108a2a6a2407b2bf9ef8d4c211f8346fa244d Mon Sep 17 00:00:00 2001 From: esir Date: Wed, 3 Nov 2021 13:42:44 +0300 Subject: [PATCH 07/15] Adds a twitter client that is responsible for getting user details from just a username --- .env.template | 4 +++ requirements-all.txt | 1 + twoopstracker/settings.py | 7 ++++ twoopstracker/twitterclient/__init__.py | 1 + twoopstracker/twitterclient/twitter_client.py | 36 +++++++++++++++++++ 5 files changed, 49 insertions(+) create mode 100644 twoopstracker/twitterclient/twitter_client.py diff --git a/.env.template b/.env.template index f1901092..f511fe7b 100644 --- a/.env.template +++ b/.env.template @@ -1,6 +1,10 @@ # Required TWOOPSTRACKER_DATABASE_URL= TWOOPSTRACKER_SECRET_KEY= +TWOOPSTRACKER_CONSUMER_KEY= +TWOOPSTRACKER_CONSUMER_SECRET= +TWOOPSTRACKER_ACCESS_TOKEN= +TWOOPSTRACKER_ACCESS_TOKEN_SECRET= # End of Required diff --git a/requirements-all.txt b/requirements-all.txt index 4e983217..83b602bd 100644 --- a/requirements-all.txt +++ b/requirements-all.txt @@ -6,3 +6,4 @@ djangorestframework==3.12.4 environs[django]==9.3.4 greenlet==1.1.2 gunicorn[gevent, setproctitle]==20.1.0 +tweepy==4.2.0 diff --git a/twoopstracker/settings.py b/twoopstracker/settings.py index a0c8edb7..606737b4 100644 --- a/twoopstracker/settings.py +++ b/twoopstracker/settings.py @@ -137,6 +137,13 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +# Twitter API credentials +TWOOPSTRACKER_CONSUMER_KEY = env.str("TWOOPSTRACKER_CONSUMER_KEY") +TWOOPSTRACKER_CONSUMER_SECRET = env.str("TWOOPSTRACKER_CONSUMER_SECRET") +TWOOPSTRACKER_ACCESS_TOKEN = env.str("TWOOPSTRACKER_ACCESS_TOKEN") +TWOOPSTRACKER_ACCESS_TOKEN_SECRET = env.str("TWOOPSTRACKER_ACCESS_TOKEN_SECRET") + # CORS CORS_ALLOW_ALL_ORIGINS = True diff --git a/twoopstracker/twitterclient/__init__.py b/twoopstracker/twitterclient/__init__.py index e69de29b..25f211a8 100644 --- a/twoopstracker/twitterclient/__init__.py +++ b/twoopstracker/twitterclient/__init__.py @@ -0,0 +1 @@ +from .twitter_client import TwitterClient # noqa diff --git a/twoopstracker/twitterclient/twitter_client.py b/twoopstracker/twitterclient/twitter_client.py new file mode 100644 index 00000000..e38379a3 --- /dev/null +++ b/twoopstracker/twitterclient/twitter_client.py @@ -0,0 +1,36 @@ +import logging + +import tweepy +from django.conf import settings + +logger = logging.getLogger(__name__) + + +class TwitterClient: + def __init__(self): + self.auth = tweepy.OAuthHandler( + settings.TWOOPSTRACKER_CONSUMER_KEY, settings.TWOOPSTRACKER_CONSUMER_SECRET + ) + self.auth.set_access_token( + settings.TWOOPSTRACKER_ACCESS_TOKEN, + settings.TWOOPSTRACKER_ACCESS_TOKEN_SECRET, + ) + self.api = None + + try: + username = self.get_api().verify_credentials().screen_name + logger.info("@" + username + " is authenticated") + except tweepy.errors.TweepyException: + logger.error("Invalid credentials") + + def get_api(self): + if not self.api: + self.api = tweepy.API(self.auth) + return self.api + + def get_user(self, user): + try: + return self.api.get_user(screen_name=user) + except tweepy.errors.TweepyException as e: + logger.error(e) + return None From 48c056f62b89bbbba6ee47fbbcff71936c547b8c Mon Sep 17 00:00:00 2001 From: esir Date: Wed, 3 Nov 2021 13:45:21 +0300 Subject: [PATCH 08/15] Update post method to automatically create TwitterAccounts into our database everytime a request to add an account to a list is sent. --- twoopstracker/twoops/views.py | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/twoopstracker/twoops/views.py b/twoopstracker/twoops/views.py index cdd4bd90..bfe67233 100644 --- a/twoopstracker/twoops/views.py +++ b/twoopstracker/twoops/views.py @@ -3,7 +3,8 @@ from django.contrib.postgres.search import SearchQuery, SearchVector from rest_framework import generics -from twoopstracker.twoops.models import Tweet, TwitterAccountsList +from twoopstracker.twitterclient import TwitterClient +from twoopstracker.twoops.models import Tweet, TwitterAccount, TwitterAccountsList from twoopstracker.twoops.serializers import ( TweetSerializer, TwitterAccountListSerializer, @@ -67,6 +68,38 @@ class AccountsList(generics.ListCreateAPIView): queryset = TwitterAccountsList.objects.all() serializer_class = TwitterAccountListSerializer + def post(self, request, *args, **kwargs): + twitterclient = TwitterClient() + + accounts = [] + + # Since accounts come in as a list of usernames, + # we need to get the account details and create a TwittwerAccount in our database + for username in request.data.get("accounts"): + # Get account details from twitter + user = twitterclient.get_user(username) + + account_obj, _ = TwitterAccount.objects.get_or_create( + account_id=user.id, + name=user.name, + screen_name=user.screen_name, + description=user.description, + verified=user.verified, + protected=user.protected, + location=user.location, + followers_count=user.followers_count, + friends_count=user.friends_count, + favourites_count=user.favourites_count, + statuses_count=user.statuses_count, + profile_image_url=user.profile_image_url, + ) + account_obj.save() + accounts.append(account_obj.account_id) + + del request.data["accounts"] + request.data["accounts"] = accounts + return self.create(request, *args, **kwargs) + class SingleTwitterList(generics.RetrieveUpdateDestroyAPIView): queryset = TwitterAccountsList.objects.all() From b45a55e81cada18bd8e6cba21365463ea80ae8d2 Mon Sep 17 00:00:00 2001 From: esir Date: Wed, 3 Nov 2021 21:01:55 +0300 Subject: [PATCH 09/15] Add default value for integer fields. --- twoopstracker/twoops/migrations/0004_list.py | 6 +--- .../0005_add_default_values_for_fields.py | 33 +++++++++++++++++++ twoopstracker/twoops/models.py | 8 ++--- 3 files changed, 38 insertions(+), 9 deletions(-) create mode 100644 twoopstracker/twoops/migrations/0005_add_default_values_for_fields.py diff --git a/twoopstracker/twoops/migrations/0004_list.py b/twoopstracker/twoops/migrations/0004_list.py index 649e5684..bddd8d56 100644 --- a/twoopstracker/twoops/migrations/0004_list.py +++ b/twoopstracker/twoops/migrations/0004_list.py @@ -46,11 +46,7 @@ class Migration(migrations.Migration): to="twoops.userprofile", ), ), - migrations.AddField( - model_name="twitteraccountslist", - name="is_private", - field=models.BooleanField(default=False), - ), + ("is_private", models.BooleanField(default=False)), ], options={ "get_latest_by": "updated_at", diff --git a/twoopstracker/twoops/migrations/0005_add_default_values_for_fields.py b/twoopstracker/twoops/migrations/0005_add_default_values_for_fields.py new file mode 100644 index 00000000..79a56846 --- /dev/null +++ b/twoopstracker/twoops/migrations/0005_add_default_values_for_fields.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.8 on 2021-11-03 16:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("twoops", "0004_list"), + ] + + operations = [ + migrations.AlterField( + model_name="twitteraccount", + name="favourites_count", + field=models.IntegerField(default=0), + ), + migrations.AlterField( + model_name="twitteraccount", + name="followers_count", + field=models.IntegerField(default=0), + ), + migrations.AlterField( + model_name="twitteraccount", + name="friends_count", + field=models.IntegerField(default=0), + ), + migrations.AlterField( + model_name="twitteraccount", + name="statuses_count", + field=models.IntegerField(default=0), + ), + ] diff --git a/twoopstracker/twoops/models.py b/twoopstracker/twoops/models.py index 553e5479..9bac6db9 100644 --- a/twoopstracker/twoops/models.py +++ b/twoopstracker/twoops/models.py @@ -44,10 +44,10 @@ class TwitterAccount(TimestampedModelMixin): protected = models.BooleanField(default=False) location = models.CharField(max_length=255) description = models.TextField() - followers_count = models.IntegerField() - friends_count = models.IntegerField() - favourites_count = models.IntegerField() - statuses_count = models.IntegerField() + followers_count = models.IntegerField(default=0) + friends_count = models.IntegerField(default=0) + favourites_count = models.IntegerField(default=0) + statuses_count = models.IntegerField(default=0) profile_image_url = models.URLField(max_length=255) deleted = models.BooleanField( default=False, From 034a36fa1f85f42bf0ace9c7175bb3bb2789d92b Mon Sep 17 00:00:00 2001 From: esir Date: Wed, 3 Nov 2021 21:05:18 +0300 Subject: [PATCH 10/15] Allow accounts list update --- twoopstracker/twitterclient/twitter_client.py | 11 ++-- twoopstracker/twoops/views.py | 59 ++++++++++++++----- 2 files changed, 51 insertions(+), 19 deletions(-) diff --git a/twoopstracker/twitterclient/twitter_client.py b/twoopstracker/twitterclient/twitter_client.py index e38379a3..0e6e7b44 100644 --- a/twoopstracker/twitterclient/twitter_client.py +++ b/twoopstracker/twitterclient/twitter_client.py @@ -20,17 +20,20 @@ def __init__(self): try: username = self.get_api().verify_credentials().screen_name logger.info("@" + username + " is authenticated") - except tweepy.errors.TweepyException: - logger.error("Invalid credentials") + except tweepy.errors.TweepyException as e: + logger.error(e) def get_api(self): if not self.api: self.api = tweepy.API(self.auth) return self.api - def get_user(self, user): + def get_user(self, user, key="screen_name"): try: - return self.api.get_user(screen_name=user) + if key == "screen_name": + return self.api.get_user(screen_name=user) + elif key == "id": + return self.api.get_user(user_id=user) except tweepy.errors.TweepyException as e: logger.error(e) return None diff --git a/twoopstracker/twoops/views.py b/twoopstracker/twoops/views.py index bfe67233..3e9603b0 100644 --- a/twoopstracker/twoops/views.py +++ b/twoopstracker/twoops/views.py @@ -28,6 +28,22 @@ def refromat_search_string(search_string): return " | ".join(search_string.split(",")) +def save_user(account_obj, user): + account_obj.name = user.name + account_obj.screen_name = user.screen_name + account_obj.description = user.description + account_obj.verified = user.verified + account_obj.protected = user.protected + account_obj.location = user.location + account_obj.followers_count = user.followers_count + account_obj.friends_count = user.friends_count + account_obj.favourites_count = user.favourites_count + account_obj.statuses_count = user.statuses_count + account_obj.profile_image_url = user.profile_image_url + + account_obj.save() + + class TweetsView(generics.ListAPIView): serializer_class = TweetSerializer @@ -79,21 +95,11 @@ def post(self, request, *args, **kwargs): # Get account details from twitter user = twitterclient.get_user(username) - account_obj, _ = TwitterAccount.objects.get_or_create( - account_id=user.id, - name=user.name, - screen_name=user.screen_name, - description=user.description, - verified=user.verified, - protected=user.protected, - location=user.location, - followers_count=user.followers_count, - friends_count=user.friends_count, - favourites_count=user.favourites_count, - statuses_count=user.statuses_count, - profile_image_url=user.profile_image_url, - ) - account_obj.save() + account_obj, _ = TwitterAccount.objects.get_or_create(account_id=user.id) + + # Move this to a que + save_user(account_obj, user) + accounts.append(account_obj.account_id) del request.data["accounts"] @@ -104,3 +110,26 @@ def post(self, request, *args, **kwargs): class SingleTwitterList(generics.RetrieveUpdateDestroyAPIView): queryset = TwitterAccountsList.objects.all() serializer_class = TwitterAccountListSerializer + + def put(self, request, *args, **kwargs): + twitterclient = TwitterClient() + accounts = [] + + for username in request.data.get("accounts"): + # Get account details from twitter + try: + int(username) + user = twitterclient.get_user(username, key="id") + except Exception: + # the username is not an integer, so it's a screen_name + user = twitterclient.get_user(username, key="screen_name") + + account_obj, _ = TwitterAccount.objects.get_or_create(account_id=user.id) + + # Move this to a que + save_user(account_obj, user) + accounts.append(account_obj.account_id) + + del request.data["accounts"] + request.data["accounts"] = accounts + return self.update(request, *args, **kwargs) From a4296319973b0fcebf11a9500526de8dadcabb62 Mon Sep 17 00:00:00 2001 From: esir Date: Wed, 3 Nov 2021 21:19:18 +0300 Subject: [PATCH 11/15] Provide defaults for twitter api credentials --- twoopstracker/settings.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/twoopstracker/settings.py b/twoopstracker/settings.py index 606737b4..a13affd4 100644 --- a/twoopstracker/settings.py +++ b/twoopstracker/settings.py @@ -139,10 +139,10 @@ # Twitter API credentials -TWOOPSTRACKER_CONSUMER_KEY = env.str("TWOOPSTRACKER_CONSUMER_KEY") -TWOOPSTRACKER_CONSUMER_SECRET = env.str("TWOOPSTRACKER_CONSUMER_SECRET") -TWOOPSTRACKER_ACCESS_TOKEN = env.str("TWOOPSTRACKER_ACCESS_TOKEN") -TWOOPSTRACKER_ACCESS_TOKEN_SECRET = env.str("TWOOPSTRACKER_ACCESS_TOKEN_SECRET") +TWOOPSTRACKER_CONSUMER_KEY = env.str("TWOOPSTRACKER_CONSUMER_KEY", "") +TWOOPSTRACKER_CONSUMER_SECRET = env.str("TWOOPSTRACKER_CONSUMER_SECRET", "") +TWOOPSTRACKER_ACCESS_TOKEN = env.str("TWOOPSTRACKER_ACCESS_TOKEN", "") +TWOOPSTRACKER_ACCESS_TOKEN_SECRET = env.str("TWOOPSTRACKER_ACCESS_TOKEN_SECRET", "") # CORS CORS_ALLOW_ALL_ORIGINS = True From 0b9a5d004b186f00650e3efe00cbbb1a5acb4e30 Mon Sep 17 00:00:00 2001 From: esir Date: Wed, 3 Nov 2021 21:29:34 +0300 Subject: [PATCH 12/15] Remove unnecessary function --- twoopstracker/twitterclient/twitter_client.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/twoopstracker/twitterclient/twitter_client.py b/twoopstracker/twitterclient/twitter_client.py index 0e6e7b44..47fc7d68 100644 --- a/twoopstracker/twitterclient/twitter_client.py +++ b/twoopstracker/twitterclient/twitter_client.py @@ -15,19 +15,14 @@ def __init__(self): settings.TWOOPSTRACKER_ACCESS_TOKEN, settings.TWOOPSTRACKER_ACCESS_TOKEN_SECRET, ) - self.api = None + self.api = tweepy.API(self.auth) try: - username = self.get_api().verify_credentials().screen_name + username = self.api.verify_credentials().screen_name logger.info("@" + username + " is authenticated") except tweepy.errors.TweepyException as e: logger.error(e) - def get_api(self): - if not self.api: - self.api = tweepy.API(self.auth) - return self.api - def get_user(self, user, key="screen_name"): try: if key == "screen_name": From 7406847a422a8b9e7db0245cdd68d6a48c562405 Mon Sep 17 00:00:00 2001 From: esir Date: Thu, 4 Nov 2021 04:16:46 +0300 Subject: [PATCH 13/15] Display more twitter account details on returned response of the LIST --- twoopstracker/twoops/serializers.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/twoopstracker/twoops/serializers.py b/twoopstracker/twoops/serializers.py index 45747f81..a7906c3c 100644 --- a/twoopstracker/twoops/serializers.py +++ b/twoopstracker/twoops/serializers.py @@ -17,6 +17,25 @@ class Meta: class TwitterAccountListSerializer(serializers.ModelSerializer): + def get_accounts(self, obj): + accounts = obj.accounts.all() + data = [] + for account in accounts: + data.append( + { + "name": account.name, + "account_id": account.account_id, + "screen_name": account.screen_name, + } + ) + + return data + class Meta: model = TwitterAccountsList - fields = "__all__" + fields = ["id", "name", "owner", "is_private", "accounts"] + + def to_representation(self, instance): + data = super().to_representation(instance) + data["accounts"] = self.get_accounts(instance) + return data From b3a3d1bebca57c9c2a08f5db8466405640bf46c0 Mon Sep 17 00:00:00 2001 From: esir Date: Thu, 4 Nov 2021 04:18:15 +0300 Subject: [PATCH 14/15] Get screen_name from request and use it to get user details --- twoopstracker/twoops/views.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/twoopstracker/twoops/views.py b/twoopstracker/twoops/views.py index 3e9603b0..adca37ee 100644 --- a/twoopstracker/twoops/views.py +++ b/twoopstracker/twoops/views.py @@ -91,7 +91,8 @@ def post(self, request, *args, **kwargs): # Since accounts come in as a list of usernames, # we need to get the account details and create a TwittwerAccount in our database - for username in request.data.get("accounts"): + for account in request.data.get("accounts"): + username = account.get("screen_name") # Get account details from twitter user = twitterclient.get_user(username) @@ -115,7 +116,8 @@ def put(self, request, *args, **kwargs): twitterclient = TwitterClient() accounts = [] - for username in request.data.get("accounts"): + for account in request.data.get("accounts"): + username = account.get("screen_name") # Get account details from twitter try: int(username) From a79ec8729567c62be70b2ef483d2e118efcc215e Mon Sep 17 00:00:00 2001 From: esir Date: Thu, 4 Nov 2021 04:44:36 +0300 Subject: [PATCH 15/15] Handle cases where we can't find a twitter account --- twoopstracker/twoops/views.py | 39 +++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/twoopstracker/twoops/views.py b/twoopstracker/twoops/views.py index adca37ee..9906a407 100644 --- a/twoopstracker/twoops/views.py +++ b/twoopstracker/twoops/views.py @@ -88,6 +88,7 @@ def post(self, request, *args, **kwargs): twitterclient = TwitterClient() accounts = [] + failed_accounts = [] # Since accounts come in as a list of usernames, # we need to get the account details and create a TwittwerAccount in our database @@ -95,7 +96,9 @@ def post(self, request, *args, **kwargs): username = account.get("screen_name") # Get account details from twitter user = twitterclient.get_user(username) - + if not user: + failed_accounts.append(username) + continue account_obj, _ = TwitterAccount.objects.get_or_create(account_id=user.id) # Move this to a que @@ -105,7 +108,15 @@ def post(self, request, *args, **kwargs): del request.data["accounts"] request.data["accounts"] = accounts - return self.create(request, *args, **kwargs) + response = self.create(request, *args, **kwargs) + + if failed_accounts: + response.data["errors"] = { + "message": "The following accounts couldn't be processed", + "failed_accounts": failed_accounts, + } + + return response class SingleTwitterList(generics.RetrieveUpdateDestroyAPIView): @@ -115,17 +126,20 @@ class SingleTwitterList(generics.RetrieveUpdateDestroyAPIView): def put(self, request, *args, **kwargs): twitterclient = TwitterClient() accounts = [] + failed_accounts = [] for account in request.data.get("accounts"): username = account.get("screen_name") + user_id = account.get("account_id") # Get account details from twitter - try: - int(username) - user = twitterclient.get_user(username, key="id") - except Exception: - # the username is not an integer, so it's a screen_name - user = twitterclient.get_user(username, key="screen_name") + if user_id: + user = twitterclient.get_user(user_id, key="id") + else: + user = twitterclient.get_user(username) + if not user: + failed_accounts.append(account) + continue account_obj, _ = TwitterAccount.objects.get_or_create(account_id=user.id) # Move this to a que @@ -134,4 +148,11 @@ def put(self, request, *args, **kwargs): del request.data["accounts"] request.data["accounts"] = accounts - return self.update(request, *args, **kwargs) + response = self.update(request, *args, **kwargs) + + if failed_accounts: + response.data["errors"] = { + "message": "The following accounts couldn't be processed", + "failed_accounts": failed_accounts, + } + return response