From 1c7ff310418b89b17b7dede8e6e2a91abbabf43f Mon Sep 17 00:00:00 2001 From: Andrew Redden Date: Fri, 16 Aug 2019 16:25:19 -0300 Subject: [PATCH 01/29] New Model: Organization - name - groups (Many to Many relationship to auth.Group) Model Updates: Profile - organizations (Many to Many relationship to dashboard.Organization) sync_profile updates: - updated to account for change to organizations referenced above New Management Command: - app/management/sync_orgs_repo.py - no arguments presently - designed to be run as a long running cron job --- .../management/commands/sync_orgs_repos.py | 109 +++++++++++ app/app/settings.py | 2 +- app/app/utils.py | 10 +- .../migrations/0045_auto_20190816_1645.py | 38 ++++ app/dashboard/models.py | 178 ++++++++++-------- app/git/utils.py | 36 +++- 6 files changed, 290 insertions(+), 83 deletions(-) create mode 100644 app/app/management/commands/sync_orgs_repos.py create mode 100644 app/dashboard/migrations/0045_auto_20190816_1645.py diff --git a/app/app/management/commands/sync_orgs_repos.py b/app/app/management/commands/sync_orgs_repos.py new file mode 100644 index 00000000000..42861eb7337 --- /dev/null +++ b/app/app/management/commands/sync_orgs_repos.py @@ -0,0 +1,109 @@ +from django.core.management.base import BaseCommand +from django.contrib.auth.models import User, Group +from dashboard.models import Profile, Organization +from git.utils import get_organization, get_user, get_repo +from app.utils import sync_profile + + +class Command(BaseCommand): + help = 'Synchronizes Organizations and Repo roles to members' + + # def add_arguments(self, parser): + + def handle(self, *args, **options): + + try: + print("Loading Users....") + all_users = User.objects.all() + print(all_users) + print("Looking up Organization of user") + # memoize the process so we only ever sync once per user + + synced = [] + + def recursive_sync(lsynced, handle): + try: + if handle not in lsynced: + print(f'Syncing User Handle: {handle}') + profile = sync_profile(handle) + lsynced.append(handle) + else: + return lsynced + + access_token = profile.user.social_auth.filter(provider='github').latest('pk').access_token + for org in profile.organizations.all(): + print(f'Syncing Org: {org.name}') + org_members = get_organization( + org.name, + '/members', + (profile.handle, access_token) + ) + + # need a query that cleans out data that isn't in the current set we're processing + # drop users from organizations and the underlying groups when they aren't apart of membership + # or are not apart of the collaborators + + for member in org_members: + + member_user_obj = User.objects.get(profile__handle=member['login']) + + if member_user_obj is None: + continue + + membership = get_organization( + org.name, + f'/memberships/{handle}', + (profile.handle, access_token) + ) + role = membership['role'] if not None else "member" + group = Group.objects.get_or_create(name=f'{org.name}-role-{role}') + org.groups.add(group[0]) + member_user_obj.groups.add(group[0]) + lsynced = recursive_sync(lsynced, member['login']) if not None else [] + + org_repos = get_organization( + org.name, + '/repos', + (profile.handle, access_token) + ) + for repo in org_repos: + repo_collabs = get_repo( + repo['full_name'], + '/collaborators', + (profile.handle, access_token) + ) + + for collaborator in repo_collabs: + member_user_obj = User.objects.get(profile__handle=collaborator['login']) + + if member_user_obj is None: + continue + + if collaborator['permission']['admin']: + permission = "admin" + elif collaborator['permission']['push']: + permission = "write" + elif collaborator['permission']['pull']: + permission = "pull" + else: + permission = "none" + + group = Group.objects.get_or_create(name=f'{org.name}-repo-{repo["name"]}-{permission}') + org.groups.add(group[0]) + member_user_obj.groups.add(group[0]) + lsynced = recursive_sync(lsynced, collaborator['login']) if not None else [] + + return lsynced + except Exception as exc: + print("were over here") + print(exc) + + for user in all_users: + # get profile data now creates or gets the new organization data for each user + synced = recursive_sync(synced, user.profile.handle) + print("Synced profiles") + print(synced) + + except ValueError as e: + print("were here") + print(e) diff --git a/app/app/settings.py b/app/app/settings.py index 14b12c30812..7cf10221c3d 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -527,7 +527,7 @@ SOCIAL_AUTH_GITHUB_SECRET = GITHUB_CLIENT_SECRET SOCIAL_AUTH_POSTGRES_JSONFIELD = True SOCIAL_AUTH_ADMIN_USER_SEARCH_FIELDS = ['username', 'first_name', 'last_name', 'email'] -SOCIAL_AUTH_GITHUB_SCOPE = ['read:public_repo', 'read:user', 'user:email', ] +SOCIAL_AUTH_GITHUB_SCOPE = ['read:public_repo', 'read:user', 'user:email', 'read:org'] SOCIAL_AUTH_SANITIZE_REDIRECTS = True SOCIAL_AUTH_PIPELINE = ( diff --git a/app/app/utils.py b/app/app/utils.py index 6d136c1dccc..54d0bfbdbfe 100644 --- a/app/app/utils.py +++ b/app/app/utils.py @@ -183,7 +183,7 @@ def get_upload_filename(instance, filename): def sync_profile(handle, user=None, hide_profile=True): - from dashboard.models import Profile + from dashboard.models import Profile, Organization handle = handle.strip().replace('@', '').lower() data = get_user(handle) email = '' @@ -207,9 +207,8 @@ def sync_profile(handle, user=None, hide_profile=True): # store the org info in postgres try: profile, created = Profile.objects.update_or_create(handle=handle, defaults=defaults) - print("Profile:", profile, "- created" if created else "- updated") orgs = get_user(handle, '/orgs') - profile.organizations = [ele['login'] for ele in orgs] + print("Profile:", profile, "- created" if created else "- updated") keywords = [] for repo in profile.repos_data_lite: language = repo.get('language') if repo.get('language') else '' @@ -219,6 +218,11 @@ def sync_profile(handle, user=None, hide_profile=True): keywords.append(key) profile.keywords = keywords + profile.organizations.all().delete() + + for ele in orgs: + org = Organization.objects.get_or_create(name=ele['login']) + profile.organizations.add(org[0]) profile.save() except Exception as e: diff --git a/app/dashboard/migrations/0045_auto_20190816_1645.py b/app/dashboard/migrations/0045_auto_20190816_1645.py new file mode 100644 index 00000000000..a277adbf872 --- /dev/null +++ b/app/dashboard/migrations/0045_auto_20190816_1645.py @@ -0,0 +1,38 @@ +# Generated by Django 2.2.3 on 2019-08-16 16:45 + +from django.db import migrations, models +import economy.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0011_update_proxy_permissions'), + ('dashboard', '0044_auto_20190729_1817'), + ] + + operations = [ + migrations.RemoveField( + model_name='profile', + name='organizations', + ), + migrations.CreateModel( + name='Organization', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_on', models.DateTimeField(db_index=True, default=economy.models.get_time)), + ('modified_on', models.DateTimeField(default=economy.models.get_time)), + ('name', models.CharField(max_length=255)), + ('groups', models.ManyToManyField(blank=True, to='auth.Group')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='profile', + name='organizations', + field=models.ManyToManyField(blank=True, to='dashboard.Organization'), + ), + + ] diff --git a/app/dashboard/models.py b/app/dashboard/models.py index c9744322fde..aae00579702 100644 --- a/app/dashboard/models.py +++ b/app/dashboard/models.py @@ -121,33 +121,33 @@ def needs_review(self): """Filter results by bounties that need reviewed.""" return self.prefetch_related('activities') \ .filter( - activities__activity_type__in=['bounty_abandonment_escalation_to_mods', 'bounty_abandonment_warning'], - activities__needs_review=True, - ) + activities__activity_type__in=['bounty_abandonment_escalation_to_mods', 'bounty_abandonment_warning'], + activities__needs_review=True, + ) def reviewed(self): """Filter results by bounties that have been reviewed.""" return self.prefetch_related('activities') \ .filter( - activities__activity_type__in=['bounty_abandonment_escalation_to_mods', 'bounty_abandonment_warning'], - activities__needs_review=False, - ) + activities__activity_type__in=['bounty_abandonment_escalation_to_mods', 'bounty_abandonment_warning'], + activities__needs_review=False, + ) def warned(self): """Filter results by bounties that have been warned for inactivity.""" return self.prefetch_related('activities') \ .filter( - activities__activity_type='bounty_abandonment_warning', - activities__needs_review=True, - ) + activities__activity_type='bounty_abandonment_warning', + activities__needs_review=True, + ) def escalated(self): """Filter results by bounties that have been escalated for review.""" return self.prefetch_related('activities') \ .filter( - activities__activity_type='bounty_abandonment_escalation_to_mods', - activities__needs_review=True, - ) + activities__activity_type='bounty_abandonment_escalation_to_mods', + activities__needs_review=True, + ) def closed(self): """Filter results by bounties that have been closed on Github.""" @@ -164,16 +164,19 @@ def has_funds(self): """Fields that bonties table should index together.""" + + def get_bounty_index_together(): import copy index_together = [ - ["network", "idx_status"], - ["current_bounty", "network"], - ["current_bounty", "network", "idx_status"], - ["current_bounty", "network", "web3_created"], - ["current_bounty", "network", "idx_status", "web3_created"], - ] - additions = ['admin_override_and_hide', 'experience_level', 'is_featured', 'project_length', 'bounty_owner_github_username', 'event'] + ["network", "idx_status"], + ["current_bounty", "network"], + ["current_bounty", "network", "idx_status"], + ["current_bounty", "network", "web3_created"], + ["current_bounty", "network", "idx_status", "web3_created"], + ] + additions = ['admin_override_and_hide', 'experience_level', 'is_featured', 'project_length', + 'bounty_owner_github_username', 'event'] for addition in additions: for ele in copy.copy(index_together): index_together.append([addition] + ele) @@ -312,8 +315,10 @@ class Bounty(SuperModel): featuring_date = models.DateTimeField(blank=True, null=True, db_index=True) fee_amount = models.DecimalField(default=0, decimal_places=18, max_digits=50) fee_tx_id = models.CharField(default="0x0", max_length=255, blank=True) - coupon_code = models.ForeignKey('dashboard.Coupon', blank=True, null=True, related_name='coupon', on_delete=models.SET_NULL) - unsigned_nda = models.ForeignKey('dashboard.BountyDocuments', blank=True, null=True, related_name='bounty', on_delete=models.SET_NULL) + coupon_code = models.ForeignKey('dashboard.Coupon', blank=True, null=True, related_name='coupon', + on_delete=models.SET_NULL) + unsigned_nda = models.ForeignKey('dashboard.BountyDocuments', blank=True, null=True, related_name='bounty', + on_delete=models.SET_NULL) token_value_time_peg = models.DateTimeField(blank=True, null=True) token_value_in_usdt = models.DecimalField(default=0, decimal_places=2, max_digits=50, blank=True, null=True) @@ -331,15 +336,16 @@ class Bounty(SuperModel): admin_mark_as_remarket_ready = models.BooleanField( default=False, help_text=_('Admin override to mark as remarketing ready') ) - admin_override_org_name = models.CharField(max_length=255, blank=True) # TODO: Remove POST ORGS + admin_override_org_name = models.CharField(max_length=255, blank=True) # TODO: Remove POST ORGS admin_override_org_logo = models.ImageField( upload_to=get_upload_filename, null=True, blank=True, help_text=_('Organization Logo - Override'), - ) # TODO: Remove POST ORGS + ) # TODO: Remove POST ORGS attached_job_description = models.URLField(blank=True, null=True, db_index=True) - event = models.ForeignKey('dashboard.HackathonEvent', related_name='bounties', null=True, on_delete=models.SET_NULL, blank=True) + event = models.ForeignKey('dashboard.HackathonEvent', related_name='bounties', null=True, on_delete=models.SET_NULL, + blank=True) # Bounty QuerySet Manager objects = BountyQuerySet.as_manager() @@ -349,13 +355,13 @@ class Meta: verbose_name_plural = 'Bounties' index_together = [ - ["network", "idx_status"], - ] + get_bounty_index_together() + ["network", "idx_status"], + ] + get_bounty_index_together() def __str__(self): """Return the string representation of a Bounty.""" return f"{'(C) ' if self.current_bounty else ''}{self.pk}: {self.title}, {self.value_true} " \ - f"{self.token_name} @ {naturaltime(self.web3_created)}" + f"{self.token_name} @ {naturaltime(self.web3_created)}" def save(self, *args, **kwargs): """Define custom handling for saving bounties.""" @@ -420,14 +426,16 @@ def get_canonical_url(self): _org_name = org_name(self.github_url) _repo_name = repo_name(self.github_url) _issue_num = int(issue_number(self.github_url)) - return settings.BASE_URL.rstrip('/') + reverse('issue_details_new2', kwargs={'ghuser': _org_name, 'ghrepo': _repo_name, 'ghissue': _issue_num}) + return settings.BASE_URL.rstrip('/') + reverse('issue_details_new2', + kwargs={'ghuser': _org_name, 'ghrepo': _repo_name, + 'ghissue': _issue_num}) def get_natural_value(self): token = addr_to_token(self.token_address) if not token: return 0 decimals = token.get('decimals', 0) - return float(self.value_in_token) / 10**decimals + return float(self.value_in_token) / 10 ** decimals @property def url(self): @@ -515,7 +523,7 @@ def org_name(self): return self.github_org_name @property - def org_display_name(self): # TODO: Remove POST ORGS + def org_display_name(self): # TODO: Remove POST ORGS if self.admin_override_org_name: return self.admin_override_org_name return org_name(self.github_url) @@ -556,7 +564,8 @@ def is_fulfiller(self, handle): bool: Whether or not the user is the bounty is_fulfiller. """ - return any(profile.fulfiller_github_username == handle for profile in self.fulfillments.filter(accepted=True).all()) + return any( + profile.fulfiller_github_username == handle for profile in self.fulfillments.filter(accepted=True).all()) def is_funder(self, handle): """Determine whether or not the profile is the bounty funder. @@ -883,7 +892,8 @@ def fetch_issue_comments(self, save=True): return [] comment_count = 0 for comment in comments: - if (isinstance(comment, dict) and comment.get('user', {}).get('login', '') not in settings.IGNORE_COMMENTS_FROM): + if (isinstance(comment, dict) and comment.get('user', {}).get('login', + '') not in settings.IGNORE_COMMENTS_FROM): comment_count += 1 self.github_comments = comment_count if comment_count: @@ -900,14 +910,16 @@ def next_bounty(self): if self.current_bounty: return None try: - return Bounty.objects.filter(standard_bounties_id=self.standard_bounties_id, created_on__gt=self.created_on).order_by('created_on').first() + return Bounty.objects.filter(standard_bounties_id=self.standard_bounties_id, + created_on__gt=self.created_on).order_by('created_on').first() except Exception: return None @property def prev_bounty(self): try: - return Bounty.objects.filter(standard_bounties_id=self.standard_bounties_id, created_on__lt=self.created_on).order_by('-created_on').first() + return Bounty.objects.filter(standard_bounties_id=self.standard_bounties_id, + created_on__lt=self.created_on).order_by('-created_on').first() except Exception: return None @@ -946,7 +958,7 @@ def is_notification_eligible(self, var_to_check=True): """ if not var_to_check or self.get_natural_value() < 0.0001 or ( - self.network != settings.ENABLE_NOTIFICATIONS_ON_NETWORK): + self.network != settings.ENABLE_NOTIFICATIONS_ON_NETWORK): return False if self.network == 'mainnet' and (settings.DEBUG or settings.ENV != 'prod'): return False @@ -1114,6 +1126,7 @@ def submitted(self): """Exclude results that have not been submitted.""" return self.exclude(fulfiller_address='0x0000000000000000000000000000000000000000') + class BountyFulfillment(SuperModel): """The structure of a fulfillment on a Bounty.""" @@ -1147,7 +1160,6 @@ def save(self, *args, **kwargs): self.fulfiller_github_username = self.fulfiller_github_username.lstrip('@') super().save(*args, **kwargs) - @property def should_hide(self): return self.fulfiller_github_username in settings.BLOCKED_USERS @@ -1203,7 +1215,6 @@ def __str__(self): class Subscription(SuperModel): - email = models.EmailField(max_length=255) raw_data = models.TextField() ip = models.CharField(max_length=50) @@ -1213,7 +1224,6 @@ def __str__(self): class BountyDocuments(SuperModel): - doc = models.FileField(upload_to=get_upload_filename, null=True, blank=True, help_text=_('Bounty documents.')) doc_type = models.CharField(max_length=50) @@ -1308,8 +1318,8 @@ def __str__(self): """Return the string representation for a tip.""" if self.web3_type == 'yge': return f"({self.network}) - {self.status}{' ORPHAN' if not self.emails else ''} " \ - f"{self.amount} {self.tokenName} to {self.username} from {self.from_name or 'NA'}, " \ - f"created: {naturalday(self.created_on)}, expires: {naturalday(self.expires_date)}" + f"{self.amount} {self.tokenName} to {self.username} from {self.from_name or 'NA'}, " \ + f"created: {naturalday(self.created_on)}, expires: {naturalday(self.expires_date)}" status = 'funded' if self.txid else 'not funded' status = status if not self.receive_txid else 'received' return f"({self.web3_type}) {status} {self.amount} {self.tokenName} to {self.username} from {self.from_name or 'NA'}" @@ -1318,7 +1328,7 @@ def __str__(self): def get_natural_value(self): token = addr_to_token(self.tokenAddress) decimals = token['decimals'] - return float(self.amount) / 10**decimals + return float(self.amount) / 10 ** decimals @property def value_true(self): @@ -1328,7 +1338,7 @@ def value_true(self): def amount_in_wei(self): token = addr_to_token(self.tokenAddress) decimals = token['decimals'] if token else 18 - return float(self.amount) * 10**decimals + return float(self.amount) * 10 ** decimals @property def amount_in_whole_units(self): @@ -1483,6 +1493,7 @@ def receive_url_for_recipient(self): class TipPayoutException(Exception): pass + @receiver(pre_save, sender=Tip, dispatch_uid="psave_tip") def psave_tip(sender, instance, **kwargs): # when a new tip is saved, make sure it doesnt have whitespace in it @@ -1573,7 +1584,8 @@ class Interest(SuperModel): max_length=7, help_text=_('Whether or not the interest requires review'), verbose_name=_('Needs Review')) - signed_nda = models.ForeignKey('dashboard.BountyDocuments', blank=True, null=True, related_name='interest', on_delete=models.SET_NULL) + signed_nda = models.ForeignKey('dashboard.BountyDocuments', blank=True, null=True, related_name='interest', + on_delete=models.SET_NULL) # Interest QuerySet Manager objects = InterestQuerySet.as_manager() @@ -1599,6 +1611,7 @@ def mark_for_review(self): self.save() return self + def auto_user_approve(interest, bounty): interest.pending = False interest.acceptance_date = timezone.now() @@ -1737,8 +1750,7 @@ class Activity(SuperModel): def __str__(self): """Define the string representation of an interested profile.""" return f"{self.profile.handle} type: {self.activity_type} created: {naturalday(self.created)} " \ - f"needs review: {self.needs_review}" - + f"needs review: {self.needs_review}" @property def humanized_activity_type(self): @@ -1752,7 +1764,6 @@ def humanized_activity_type(self): return activity_type[1] return ' '.join([x.capitalize() for x in self.activity_type.split('_')]) - def i18n_name(self): return _(next((x[1] for x in self.ACTIVITY_TYPES if x[0] == self.activity_type), 'Unknown type')) @@ -1801,7 +1812,8 @@ def view_props(self): obj = self.metadata['new_bounty'] activity['title'] = obj.get('title', '') if 'id' in obj: - if 'category' not in obj or obj['category'] == 'bounty': # backwards-compatible for category-lacking metadata + if 'category' not in obj or obj[ + 'category'] == 'bounty': # backwards-compatible for category-lacking metadata activity['bounty_url'] = Bounty.objects.get(pk=obj['id']).get_relative_url() if activity.get('title'): activity['urled_title'] = f'{activity["title"]}' @@ -1814,7 +1826,7 @@ def view_props(self): activity['token'] = token_by_name(obj['token_name']) if 'value_in_token' in obj and activity['token']: activity['value_in_token_disp'] = round((float(obj['value_in_token']) / - 10 ** activity['token']['decimals']) * 1000) / 1000 + 10 ** activity['token']['decimals']) * 1000) / 1000 # finally done! @@ -1929,6 +1941,17 @@ def hidden(self): return self.filter(hide_profile=True) +class Organization(SuperModel): + name = models.CharField(max_length=255) + groups = models.ManyToManyField('auth.group', blank=True) + + class Meta: + ordering = ('name',) + + def __str__(self): + return self.name + + class Profile(SuperModel): """Define the structure of the user profile. @@ -1969,11 +1992,13 @@ class Profile(SuperModel): help_text='If this option is chosen, the user is able to submit a faucet/ens domain registration even if they are new to github', ) keywords = ArrayField(models.CharField(max_length=200), blank=True, default=list) - organizations = ArrayField(models.CharField(max_length=200), blank=True, default=list) + # organizations = ArrayField(models.CharField(max_length=200), blank=True, default=list) + organizations = models.ManyToManyField(Organization, blank=True) form_submission_records = JSONField(default=list, blank=True) max_num_issues_start_work = models.IntegerField(default=3) preferred_payout_address = models.CharField(max_length=255, default='', blank=True) - preferred_kudos_wallet = models.OneToOneField('kudos.Wallet', related_name='preferred_kudos_wallet', on_delete=models.SET_NULL, null=True, blank=True) + preferred_kudos_wallet = models.OneToOneField('kudos.Wallet', related_name='preferred_kudos_wallet', + on_delete=models.SET_NULL, null=True, blank=True) max_tip_amount_usdt_per_tx = models.DecimalField(default=2500, decimal_places=2, max_digits=50) max_tip_amount_usdt_per_week = models.DecimalField(default=20000, decimal_places=2, max_digits=50) last_visit = models.DateTimeField(null=True, blank=True) @@ -2025,7 +2050,8 @@ def get_sent_bounties(self): @property def get_my_grants(self): from grants.models import Grant - return Grant.objects.filter(Q(admin_profile=self) | Q(team_members__in=[self]) | Q(subscriptions__contributor_profile=self)) + return Grant.objects.filter( + Q(admin_profile=self) | Q(team_members__in=[self]) | Q(subscriptions__contributor_profile=self)) @property def get_my_kudos(self): @@ -2086,21 +2112,20 @@ def get_average_star_rating(self): feedbacks = FeedbackEntry.objects.filter(receiver_profile=self).all() average_rating = {} average_rating['overall'] = sum([feedback.rating for feedback in feedbacks]) \ - / feedbacks.count() if feedbacks.count() != 0 else 0 + / feedbacks.count() if feedbacks.count() != 0 else 0 average_rating['code_quality_rating'] = sum([feedback.code_quality_rating for feedback in feedbacks]) \ - / feedbacks.count() if feedbacks.count() != 0 else 0 + / feedbacks.count() if feedbacks.count() != 0 else 0 average_rating['communication_rating'] = sum([feedback.communication_rating for feedback in feedbacks]) \ - / feedbacks.count() if feedbacks.count() != 0 else 0 + / feedbacks.count() if feedbacks.count() != 0 else 0 average_rating['recommendation_rating'] = sum([feedback.recommendation_rating for feedback in feedbacks]) \ - / feedbacks.count() if feedbacks.count() != 0 else 0 + / feedbacks.count() if feedbacks.count() != 0 else 0 average_rating['satisfaction_rating'] = sum([feedback.satisfaction_rating for feedback in feedbacks]) \ - / feedbacks.count() if feedbacks.count() != 0 else 0 + / feedbacks.count() if feedbacks.count() != 0 else 0 average_rating['speed_rating'] = sum([feedback.speed_rating for feedback in feedbacks]) \ - / feedbacks.count() if feedbacks.count() != 0 else 0 + / feedbacks.count() if feedbacks.count() != 0 else 0 average_rating['total_rating'] = feedbacks.count() return average_rating - @property def get_my_verified_check(self): verification = UserVerificationModel.objects.filter(user=self.user).first() @@ -2133,8 +2158,11 @@ def bounties(self): for interested in self.interested.all(): bounties = bounties | Bounty.objects.filter(interested=interested, current_bounty=True) bounties = bounties | Bounty.objects.filter(pk__in=fulfilled_bounty_ids, current_bounty=True) - bounties = bounties | Bounty.objects.filter(bounty_owner_github_username__iexact=self.handle, current_bounty=True) | Bounty.objects.filter(bounty_owner_github_username__iexact="@" + self.handle, current_bounty=True) - bounties = bounties | Bounty.objects.filter(github_url__in=[url for url in self.tips.values_list('github_url', flat=True)], current_bounty=True) + bounties = bounties | Bounty.objects.filter(bounty_owner_github_username__iexact=self.handle, + current_bounty=True) | Bounty.objects.filter( + bounty_owner_github_username__iexact="@" + self.handle, current_bounty=True) + bounties = bounties | Bounty.objects.filter( + github_url__in=[url for url in self.tips.values_list('github_url', flat=True)], current_bounty=True) bounties = bounties.distinct() return bounties.order_by('-web3_created') @@ -2188,21 +2216,21 @@ def no_times_slashed_by_staff(self): user_actions = UserAction.objects.filter( profile=self, action='bounty_removed_slashed_by_staff', - ) + ) return user_actions.count() def no_times_been_removed_by_funder(self): user_actions = UserAction.objects.filter( profile=self, action='bounty_removed_by_funder', - ) + ) return user_actions.count() def no_times_been_removed_by_staff(self): user_actions = UserAction.objects.filter( profile=self, action='bounty_removed_by_staff', - ) + ) return user_actions.count() def get_desc(self, funded_bounties, fulfilled_bounties): @@ -2219,7 +2247,7 @@ def get_desc(self, funded_bounties, fulfilled_bounties): plural = 's' if total_funded_participated != 1 else '' return f"@{self.handle} is a {role} who has participated in {total_funded_participated} " \ - f"funded issue{plural} on Gitcoin" + f"funded issue{plural} on Gitcoin" @property def desc(self): @@ -2371,7 +2399,7 @@ def get_quarterly_stats(self): bounty.value_in_eth if bounty.value_in_eth else 0 for bounty in fulfilled_bounties ]) - total_earned_eth /= 10**18 + total_earned_eth /= 10 ** 18 total_earned_usd = sum([ bounty.value_in_usdt if bounty.value_in_usdt else 0 for bounty in fulfilled_bounties @@ -2408,11 +2436,11 @@ def get_quarterly_stats(self): relevant_bounties = Bounty.objects.none() for keyword in user_coding_languages: relevant_bounties = relevant_bounties.union(potential_bounties.current().filter( - network=Profile.get_network(), - metadata__icontains=keyword, - idx_status__in=['open'], - ).order_by('?') - ) + network=Profile.get_network(), + metadata__icontains=keyword, + idx_status__in=['open'], + ).order_by('?') + ) relevant_bounties = relevant_bounties[:3] relevant_bounties = list(relevant_bounties) # Round to 2 places of decimals to be diplayed in templates @@ -2499,7 +2527,6 @@ def name(self): return self.username - def is_github_token_valid(self): """Check whether or not a Github OAuth token is valid. @@ -2731,8 +2758,9 @@ def get_all_tokens_sum(self, sum_type='collected', network='mainnet', bounties=N tokens_and_values = bounties.values_list('token_name', 'value_in_token') all_tokens_sum_tmp = {token: 0 for token in set([ele[0] for ele in tokens_and_values])} for ele in tokens_and_values: - all_tokens_sum_tmp[ele[0]] += ele[1] / 10**18 - all_tokens_sum = [{'token_name': token_name, 'value_in_token': value_in_token} for token_name, value_in_token in all_tokens_sum_tmp.items()] + all_tokens_sum_tmp[ele[0]] += ele[1] / 10 ** 18 + all_tokens_sum = [{'token_name': token_name, 'value_in_token': value_in_token} for + token_name, value_in_token in all_tokens_sum_tmp.items()] except Exception: pass @@ -3040,7 +3068,8 @@ class UserAction(SuperModel): ] action = models.CharField(max_length=50, choices=ACTION_TYPES, db_index=True) user = models.ForeignKey(User, related_name='actions', on_delete=models.SET_NULL, null=True, db_index=True) - profile = models.ForeignKey('dashboard.Profile', related_name='actions', on_delete=models.CASCADE, null=True, db_index=True) + profile = models.ForeignKey('dashboard.Profile', related_name='actions', on_delete=models.CASCADE, null=True, + db_index=True) ip_address = models.GenericIPAddressField(null=True) location_data = JSONField(default=dict) metadata = JSONField(default=dict) @@ -3301,6 +3330,7 @@ class HackathonSponsor(SuperModel): default='G', ) + class FeedbackEntry(SuperModel): bounty = models.ForeignKey( 'dashboard.Bounty', diff --git a/app/git/utils.py b/app/git/utils.py index 48e6620e917..6b59b1c574b 100644 --- a/app/git/utils.py +++ b/app/git/utils.py @@ -134,7 +134,7 @@ def check_github(profile): def search_github(q): - params = (('q', q), ('sort', 'updated'), ) + params = (('q', q), ('sort', 'updated'),) response = requests.get('https://api.github.com/search/users', headers=HEADERS, params=params) return response.json() @@ -229,7 +229,7 @@ def get_auth_url(redirect_uri='/'): str: The Github authentication URL. """ - github_callback = reverse('social:begin', args=('github', )) + github_callback = reverse('social:begin', args=('github',)) redirect_params = {'next': BASE_URI + redirect_uri} redirect_uri = urlencode(redirect_params, quote_via=quote_plus) @@ -326,7 +326,7 @@ def get_github_event_emails(oauth_token, username): name = author.get('name', {}) if name and username and user_name: append_email = name.lower() == username.lower() or name.lower() == user_name.lower() \ - and email and 'noreply.github.com' not in email + and email and 'noreply.github.com' not in email if append_email: emails.append(email) @@ -395,7 +395,7 @@ def search(query): request.Response: The github search response. """ - params = (('q', query), ('sort', 'updated'), ) + params = (('q', query), ('sort', 'updated'),) try: response = requests.get('https://api.github.com/search/users', auth=_AUTH, headers=V3HEADERS, params=params) @@ -467,7 +467,8 @@ def get_issue_comments(owner, repo, issue=None, comment_id=None): params = { 'sort': 'created', 'direction': 'desc', - 'per_page': 100, # TODO traverse/concat pages: https://developer.github.com/v3/guides/traversing-with-pagination/ + 'per_page': 100, + # TODO traverse/concat pages: https://developer.github.com/v3/guides/traversing-with-pagination/ } if issue: if comment_id: @@ -603,6 +604,31 @@ def get_user(user, sub_path=''): return response_dict +def get_organization(org, sub_path='', auth=_AUTH): + """Get the github user details.""" + org = org.replace('@', '') + url = f'https://api.github.com/orgs/{org}{sub_path}' + response = requests.get(url, auth=auth, headers=HEADERS) + try: + response_dict = response.json() + except JSONDecodeError: + response_dict = {} + return response_dict + + +def get_repo(repo_full_name, sub_path='', auth=_AUTH): + """Get the github user details.""" + repo_full_name = repo_full_name.replace('@', '') + url = f'https://api.github.com/repos/{repo_full_name}{sub_path}' + response = requests.get(url, auth=auth, headers=HEADERS) + + try: + response_dict = response.json() + except JSONDecodeError: + response_dict = {} + return response_dict + + def get_notifications(): """Get the github notifications.""" url = f'https://api.github.com/notifications?all=1' From e5fc6179c7ba55e2d106166c577c3b8ecb93e98b Mon Sep 17 00:00:00 2001 From: Andrew Redden Date: Fri, 23 Aug 2019 15:05:39 -0300 Subject: [PATCH 02/29] sync_profile helper - Reverted the change around profile.organizations due to its usage in user hover cards Profile Model - organizations restored - orgs key added for the sync work --- .../management/commands/sync_orgs_repos.py | 26 +++++------ app/app/utils.py | 6 +-- app/dashboard/models.py | 43 ++++++++++--------- 3 files changed, 36 insertions(+), 39 deletions(-) diff --git a/app/app/management/commands/sync_orgs_repos.py b/app/app/management/commands/sync_orgs_repos.py index 42861eb7337..242f0eac6f6 100644 --- a/app/app/management/commands/sync_orgs_repos.py +++ b/app/app/management/commands/sync_orgs_repos.py @@ -31,10 +31,11 @@ def recursive_sync(lsynced, handle): return lsynced access_token = profile.user.social_auth.filter(provider='github').latest('pk').access_token - for org in profile.organizations.all(): - print(f'Syncing Org: {org.name}') + for org in profile.organizations: + db_org = Organization.objects.get_or_create(name=org)[0] + print(f'Syncing Org: {db_org.name}') org_members = get_organization( - org.name, + db_org.name, '/members', (profile.handle, access_token) ) @@ -51,18 +52,19 @@ def recursive_sync(lsynced, handle): continue membership = get_organization( - org.name, + db_org.name, f'/memberships/{handle}', (profile.handle, access_token) ) role = membership['role'] if not None else "member" - group = Group.objects.get_or_create(name=f'{org.name}-role-{role}') - org.groups.add(group[0]) + group = Group.objects.get_or_create(name=f'{db_org.name}-role-{role}') + + db_org.groups.add(group[0]) member_user_obj.groups.add(group[0]) lsynced = recursive_sync(lsynced, member['login']) if not None else [] org_repos = get_organization( - org.name, + db_org.name, '/repos', (profile.handle, access_token) ) @@ -88,22 +90,20 @@ def recursive_sync(lsynced, handle): else: permission = "none" - group = Group.objects.get_or_create(name=f'{org.name}-repo-{repo["name"]}-{permission}') - org.groups.add(group[0]) + group = Group.objects.get_or_create( + name=f'{db_org.name}-repo-{repo["name"]}-{permission}') + db_org.groups.add(group[0]) member_user_obj.groups.add(group[0]) lsynced = recursive_sync(lsynced, collaborator['login']) if not None else [] return lsynced except Exception as exc: - print("were over here") print(exc) for user in all_users: # get profile data now creates or gets the new organization data for each user synced = recursive_sync(synced, user.profile.handle) - print("Synced profiles") - print(synced) + print("Profile Sync Completed") except ValueError as e: - print("were here") print(e) diff --git a/app/app/utils.py b/app/app/utils.py index 54d0bfbdbfe..5a62a8e59e0 100644 --- a/app/app/utils.py +++ b/app/app/utils.py @@ -208,6 +208,7 @@ def sync_profile(handle, user=None, hide_profile=True): try: profile, created = Profile.objects.update_or_create(handle=handle, defaults=defaults) orgs = get_user(handle, '/orgs') + profile.organizations = [ele['login'] for ele in orgs] print("Profile:", profile, "- created" if created else "- updated") keywords = [] for repo in profile.repos_data_lite: @@ -218,11 +219,6 @@ def sync_profile(handle, user=None, hide_profile=True): keywords.append(key) profile.keywords = keywords - profile.organizations.all().delete() - - for ele in orgs: - org = Organization.objects.get_or_create(name=ele['login']) - profile.organizations.add(org[0]) profile.save() except Exception as e: diff --git a/app/dashboard/models.py b/app/dashboard/models.py index 5abdca077b3..2a6b37cd739 100644 --- a/app/dashboard/models.py +++ b/app/dashboard/models.py @@ -137,10 +137,10 @@ def has_applicant(self): """Filter results by bounties that have applicants.""" return self.prefetch_related('activities') \ .filter( - activities__activity_type='worker_applied', - activities__needs_review=False, - ) - + activities__activity_type='worker_applied', + activities__needs_review=False, + ) + def warned(self): """Filter results by bounties that have been warned for inactivity.""" return self.prefetch_related('activities') \ @@ -450,33 +450,33 @@ def get_natural_value(self): @property def no_of_applicants(self): return self.interested.count() - + @property def has_applicant(self): """Filter results by bounties that have applicants.""" return self.prefetch_related('activities') \ .filter( - activities__activity_type='worker_applied', - activities__needs_review=False, - ) - + activities__activity_type='worker_applied', + activities__needs_review=False, + ) + @property def warned(self): """Filter results by bounties that have been warned for inactivity.""" return self.prefetch_related('activities') \ .filter( - activities__activity_type='bounty_abandonment_warning', - activities__needs_review=True, - ) - + activities__activity_type='bounty_abandonment_warning', + activities__needs_review=True, + ) + @property def escalated(self): """Filter results by bounties that have been escalated for review.""" return self.prefetch_related('activities') \ .filter( - activities__activity_type='bounty_abandonment_escalation_to_mods', - activities__needs_review=True, - ) + activities__activity_type='bounty_abandonment_escalation_to_mods', + activities__needs_review=True, + ) @property def url(self): @@ -1956,7 +1956,6 @@ def post_add_activity(sender, instance, created, **kwargs): dupe.delete() - class LabsResearch(SuperModel): """Define the structure of Labs Research object.""" @@ -2087,8 +2086,8 @@ class Profile(SuperModel): help_text='If this option is chosen, the user is able to submit a faucet/ens domain registration even if they are new to github', ) keywords = ArrayField(models.CharField(max_length=200), blank=True, default=list) - # organizations = ArrayField(models.CharField(max_length=200), blank=True, default=list) - organizations = models.ManyToManyField(Organization, blank=True) + organizations = ArrayField(models.CharField(max_length=200), blank=True, default=list) + orgs = models.ManyToManyField(Organization, blank=True) form_submission_records = JSONField(default=list, blank=True) max_num_issues_start_work = models.IntegerField(default=3) preferred_payout_address = models.CharField(max_length=255, default='', blank=True) @@ -3392,8 +3391,10 @@ class HackathonEvent(SuperModel): logo_svg = models.FileField(blank=True) start_date = models.DateTimeField() end_date = models.DateTimeField() - background_color = models.CharField(max_length=255, null=True, blank=True, help_text='hexcode for the banner, default to white') - text_color = models.CharField(max_length=255, null=True, blank=True, help_text='hexcode for the text, default to black') + background_color = models.CharField(max_length=255, null=True, blank=True, + help_text='hexcode for the banner, default to white') + text_color = models.CharField(max_length=255, null=True, blank=True, + help_text='hexcode for the text, default to black') identifier = models.CharField(max_length=255, default='', help_text='used for custom styling for the banner') sponsors = models.ManyToManyField(Sponsor, through='HackathonSponsor') From ddc28c20a93236405754d9ed075523ba6322e403 Mon Sep 17 00:00:00 2001 From: Andrew Redden Date: Fri, 23 Aug 2019 21:18:14 -0300 Subject: [PATCH 03/29] Fix for edge case where users wont have their orgs cleared correctly, and not lose permissions --- app/app/management/commands/sync_orgs_repos.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/app/management/commands/sync_orgs_repos.py b/app/app/management/commands/sync_orgs_repos.py index 242f0eac6f6..4d33affda1b 100644 --- a/app/app/management/commands/sync_orgs_repos.py +++ b/app/app/management/commands/sync_orgs_repos.py @@ -14,7 +14,7 @@ def handle(self, *args, **options): try: print("Loading Users....") - all_users = User.objects.all() + all_users = User.objects.all() # potentially want to add some filters here around what users are synced print(all_users) print("Looking up Organization of user") # memoize the process so we only ever sync once per user @@ -26,6 +26,8 @@ def recursive_sync(lsynced, handle): if handle not in lsynced: print(f'Syncing User Handle: {handle}') profile = sync_profile(handle) + profile.orgs.clear() + profile.save() lsynced.append(handle) else: return lsynced From 8bde2464c0035216a766306791c6a753486121d8 Mon Sep 17 00:00:00 2001 From: Andrew Redden Date: Mon, 26 Aug 2019 11:32:23 +0200 Subject: [PATCH 04/29] Reconiliation Updated - stale orgs and groups are now purged from users --- app/app/management/commands/sync_orgs_repos.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/app/app/management/commands/sync_orgs_repos.py b/app/app/management/commands/sync_orgs_repos.py index 4d33affda1b..e3453de6b07 100644 --- a/app/app/management/commands/sync_orgs_repos.py +++ b/app/app/management/commands/sync_orgs_repos.py @@ -14,7 +14,7 @@ def handle(self, *args, **options): try: print("Loading Users....") - all_users = User.objects.all() # potentially want to add some filters here around what users are synced + all_users = User.objects.all() # potentially want to add some filters here around what users are synced print(all_users) print("Looking up Organization of user") # memoize the process so we only ever sync once per user @@ -26,8 +26,13 @@ def recursive_sync(lsynced, handle): if handle not in lsynced: print(f'Syncing User Handle: {handle}') profile = sync_profile(handle) - profile.orgs.clear() - profile.save() + remove = [x for x in profile.orgs.all() if x.name not in []] + + print('Removing Stale Organizations and Groups') + for y in remove: + profile.orgs.remove(y) + profile.user.groups.filter(name__contains=y.name).delete() + lsynced.append(handle) else: return lsynced @@ -36,16 +41,13 @@ def recursive_sync(lsynced, handle): for org in profile.organizations: db_org = Organization.objects.get_or_create(name=org)[0] print(f'Syncing Org: {db_org.name}') + profile.orgs.add(db_org) org_members = get_organization( db_org.name, '/members', (profile.handle, access_token) ) - # need a query that cleans out data that isn't in the current set we're processing - # drop users from organizations and the underlying groups when they aren't apart of membership - # or are not apart of the collaborators - for member in org_members: member_user_obj = User.objects.get(profile__handle=member['login']) From 445071372acd751a95a81f6d0e805dcef6875377 Mon Sep 17 00:00:00 2001 From: Andrew Redden Date: Mon, 26 Aug 2019 16:38:44 +0200 Subject: [PATCH 05/29] migrations added to solve build issues with 4969 --- .../migrations/0049_merge_20190823_0222.py | 14 ++++++++++++++ .../migrations/0050_auto_20190823_1706.py | 19 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 app/dashboard/migrations/0049_merge_20190823_0222.py create mode 100644 app/dashboard/migrations/0050_auto_20190823_1706.py diff --git a/app/dashboard/migrations/0049_merge_20190823_0222.py b/app/dashboard/migrations/0049_merge_20190823_0222.py new file mode 100644 index 00000000000..cf7f7ebe170 --- /dev/null +++ b/app/dashboard/migrations/0049_merge_20190823_0222.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.3 on 2019-08-23 02:22 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('dashboard', '0045_auto_20190816_1645'), + ('dashboard', '0048_merge_20190808_1934'), + ] + + operations = [ + ] diff --git a/app/dashboard/migrations/0050_auto_20190823_1706.py b/app/dashboard/migrations/0050_auto_20190823_1706.py new file mode 100644 index 00000000000..f96ee5c87ac --- /dev/null +++ b/app/dashboard/migrations/0050_auto_20190823_1706.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.3 on 2019-08-23 17:06 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dashboard', '0049_merge_20190823_0222'), + ] + + operations = [ + migrations.AlterField( + model_name='activity', + name='activity_type', + field=models.CharField(blank=True, choices=[('new_bounty', 'New Bounty'), ('start_work', 'Work Started'), ('stop_work', 'Work Stopped'), ('work_submitted', 'Work Submitted'), ('work_done', 'Work Done'), ('worker_approved', 'Worker Approved'), ('worker_rejected', 'Worker Rejected'), ('worker_applied', 'Worker Applied'), ('increased_bounty', 'Increased Funding'), ('killed_bounty', 'Canceled Bounty'), ('new_tip', 'New Tip'), ('receive_tip', 'Tip Received'), ('bounty_abandonment_escalation_to_mods', 'Escalated checkin from @gitcoinbot about bounty status'), ('bounty_abandonment_warning', 'Checkin from @gitcoinbot about bounty status'), ('bounty_removed_slashed_by_staff', 'Dinged and Removed from Bounty by Staff'), ('bounty_removed_by_staff', 'Removed from Bounty by Staff'), ('bounty_removed_by_funder', 'Removed from Bounty by Funder'), ('new_crowdfund', 'New Crowdfund Contribution'), ('new_grant', 'New Grant'), ('update_grant', 'Updated Grant'), ('killed_grant', 'Cancelled Grant'), ('new_grant_contribution', 'Contributed to Grant'), ('new_grant_subscription', 'Subscribed to Grant'), ('killed_grant_contribution', 'Cancelled Grant Contribution'), ('new_milestone', 'New Milestone'), ('update_milestone', 'Updated Milestone'), ('new_kudos', 'New Kudos'), ('joined', 'Joined Gitcoin'), ('updated_avatar', 'Updated Avatar')], db_index=True, max_length=50), + ) + ] From e8e571049c57691bd4e358889e92d47597c038d9 Mon Sep 17 00:00:00 2001 From: Andrew Redden Date: Mon, 26 Aug 2019 16:39:12 +0200 Subject: [PATCH 06/29] modified previously commited migration to ensure accuracy --- app/dashboard/migrations/0045_auto_20190816_1645.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/dashboard/migrations/0045_auto_20190816_1645.py b/app/dashboard/migrations/0045_auto_20190816_1645.py index a277adbf872..42aebaccd91 100644 --- a/app/dashboard/migrations/0045_auto_20190816_1645.py +++ b/app/dashboard/migrations/0045_auto_20190816_1645.py @@ -12,10 +12,6 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RemoveField( - model_name='profile', - name='organizations', - ), migrations.CreateModel( name='Organization', fields=[ @@ -29,10 +25,13 @@ class Migration(migrations.Migration): 'abstract': False, }, ), + migrations.AlterModelOptions( + name='organization', + options={'ordering': ('name',)}, + ), migrations.AddField( model_name='profile', - name='organizations', + name='orgs', field=models.ManyToManyField(blank=True, to='dashboard.Organization'), ), - ] From 7f43a7933bf31d31b57b0f85b5f096e5329afde8 Mon Sep 17 00:00:00 2001 From: Andrew Redden Date: Tue, 3 Sep 2019 15:56:26 +0200 Subject: [PATCH 07/29] reverting formatting changes --- app/dashboard/models.py | 203 ++++++++++++++++++---------------------- 1 file changed, 92 insertions(+), 111 deletions(-) diff --git a/app/dashboard/models.py b/app/dashboard/models.py index 2a6b37cd739..c4577a7c703 100644 --- a/app/dashboard/models.py +++ b/app/dashboard/models.py @@ -121,41 +121,41 @@ def needs_review(self): """Filter results by bounties that need reviewed.""" return self.prefetch_related('activities') \ .filter( - activities__activity_type__in=['bounty_abandonment_escalation_to_mods', 'bounty_abandonment_warning'], - activities__needs_review=True, - ) + activities__activity_type__in=['bounty_abandonment_escalation_to_mods', 'bounty_abandonment_warning'], + activities__needs_review=True, + ) def reviewed(self): """Filter results by bounties that have been reviewed.""" return self.prefetch_related('activities') \ .filter( - activities__activity_type__in=['bounty_abandonment_escalation_to_mods', 'bounty_abandonment_warning'], - activities__needs_review=False, - ) + activities__activity_type__in=['bounty_abandonment_escalation_to_mods', 'bounty_abandonment_warning'], + activities__needs_review=False, + ) def has_applicant(self): """Filter results by bounties that have applicants.""" return self.prefetch_related('activities') \ .filter( - activities__activity_type='worker_applied', - activities__needs_review=False, - ) - + activities__activity_type='worker_applied', + activities__needs_review=False, + ) + def warned(self): """Filter results by bounties that have been warned for inactivity.""" return self.prefetch_related('activities') \ .filter( - activities__activity_type='bounty_abandonment_warning', - activities__needs_review=True, - ) + activities__activity_type='bounty_abandonment_warning', + activities__needs_review=True, + ) def escalated(self): """Filter results by bounties that have been escalated for review.""" return self.prefetch_related('activities') \ .filter( - activities__activity_type='bounty_abandonment_escalation_to_mods', - activities__needs_review=True, - ) + activities__activity_type='bounty_abandonment_escalation_to_mods', + activities__needs_review=True, + ) def closed(self): """Filter results by bounties that have been closed on Github.""" @@ -172,19 +172,16 @@ def has_funds(self): """Fields that bonties table should index together.""" - - def get_bounty_index_together(): import copy index_together = [ - ["network", "idx_status"], - ["current_bounty", "network"], - ["current_bounty", "network", "idx_status"], - ["current_bounty", "network", "web3_created"], - ["current_bounty", "network", "idx_status", "web3_created"], - ] - additions = ['admin_override_and_hide', 'experience_level', 'is_featured', 'project_length', - 'bounty_owner_github_username', 'event'] + ["network", "idx_status"], + ["current_bounty", "network"], + ["current_bounty", "network", "idx_status"], + ["current_bounty", "network", "web3_created"], + ["current_bounty", "network", "idx_status", "web3_created"], + ] + additions = ['admin_override_and_hide', 'experience_level', 'is_featured', 'project_length', 'bounty_owner_github_username', 'event'] for addition in additions: for ele in copy.copy(index_together): index_together.append([addition] + ele) @@ -325,10 +322,8 @@ class Bounty(SuperModel): remarketed_count = models.PositiveSmallIntegerField(default=0, blank=True, null=True) fee_amount = models.DecimalField(default=0, decimal_places=18, max_digits=50) fee_tx_id = models.CharField(default="0x0", max_length=255, blank=True) - coupon_code = models.ForeignKey('dashboard.Coupon', blank=True, null=True, related_name='coupon', - on_delete=models.SET_NULL) - unsigned_nda = models.ForeignKey('dashboard.BountyDocuments', blank=True, null=True, related_name='bounty', - on_delete=models.SET_NULL) + coupon_code = models.ForeignKey('dashboard.Coupon', blank=True, null=True, related_name='coupon', on_delete=models.SET_NULL) + unsigned_nda = models.ForeignKey('dashboard.BountyDocuments', blank=True, null=True, related_name='bounty', on_delete=models.SET_NULL) token_value_time_peg = models.DateTimeField(blank=True, null=True) token_value_in_usdt = models.DecimalField(default=0, decimal_places=2, max_digits=50, blank=True, null=True) @@ -346,16 +341,15 @@ class Bounty(SuperModel): admin_mark_as_remarket_ready = models.BooleanField( default=False, help_text=_('Admin override to mark as remarketing ready') ) - admin_override_org_name = models.CharField(max_length=255, blank=True) # TODO: Remove POST ORGS + admin_override_org_name = models.CharField(max_length=255, blank=True) # TODO: Remove POST ORGS admin_override_org_logo = models.ImageField( upload_to=get_upload_filename, null=True, blank=True, help_text=_('Organization Logo - Override'), - ) # TODO: Remove POST ORGS + ) # TODO: Remove POST ORGS attached_job_description = models.URLField(blank=True, null=True, db_index=True) - event = models.ForeignKey('dashboard.HackathonEvent', related_name='bounties', null=True, on_delete=models.SET_NULL, - blank=True) + event = models.ForeignKey('dashboard.HackathonEvent', related_name='bounties', null=True, on_delete=models.SET_NULL, blank=True) # Bounty QuerySet Manager objects = BountyQuerySet.as_manager() @@ -365,13 +359,13 @@ class Meta: verbose_name_plural = 'Bounties' index_together = [ - ["network", "idx_status"], - ] + get_bounty_index_together() + ["network", "idx_status"], + ] + get_bounty_index_together() def __str__(self): """Return the string representation of a Bounty.""" return f"{'(C) ' if self.current_bounty else ''}{self.pk}: {self.title}, {self.value_true} " \ - f"{self.token_name} @ {naturaltime(self.web3_created)}" + f"{self.token_name} @ {naturaltime(self.web3_created)}" def save(self, *args, **kwargs): """Define custom handling for saving bounties.""" @@ -436,47 +430,45 @@ def get_canonical_url(self): _org_name = org_name(self.github_url) _repo_name = repo_name(self.github_url) _issue_num = int(issue_number(self.github_url)) - return settings.BASE_URL.rstrip('/') + reverse('issue_details_new2', - kwargs={'ghuser': _org_name, 'ghrepo': _repo_name, - 'ghissue': _issue_num}) + return settings.BASE_URL.rstrip('/') + reverse('issue_details_new2', kwargs={'ghuser': _org_name, 'ghrepo': _repo_name, 'ghissue': _issue_num}) def get_natural_value(self): token = addr_to_token(self.token_address) if not token: return 0 decimals = token.get('decimals', 0) - return float(self.value_in_token) / 10 ** decimals + return float(self.value_in_token) / 10**decimals @property def no_of_applicants(self): return self.interested.count() - + @property def has_applicant(self): """Filter results by bounties that have applicants.""" return self.prefetch_related('activities') \ .filter( - activities__activity_type='worker_applied', - activities__needs_review=False, - ) - + activities__activity_type='worker_applied', + activities__needs_review=False, + ) + @property def warned(self): """Filter results by bounties that have been warned for inactivity.""" return self.prefetch_related('activities') \ .filter( - activities__activity_type='bounty_abandonment_warning', - activities__needs_review=True, - ) - + activities__activity_type='bounty_abandonment_warning', + activities__needs_review=True, + ) + @property def escalated(self): """Filter results by bounties that have been escalated for review.""" return self.prefetch_related('activities') \ .filter( - activities__activity_type='bounty_abandonment_escalation_to_mods', - activities__needs_review=True, - ) + activities__activity_type='bounty_abandonment_escalation_to_mods', + activities__needs_review=True, + ) @property def url(self): @@ -564,7 +556,7 @@ def org_name(self): return self.github_org_name @property - def org_display_name(self): # TODO: Remove POST ORGS + def org_display_name(self): # TODO: Remove POST ORGS if self.admin_override_org_name: return self.admin_override_org_name return org_name(self.github_url) @@ -605,8 +597,7 @@ def is_fulfiller(self, handle): bool: Whether or not the user is the bounty is_fulfiller. """ - return any( - profile.fulfiller_github_username == handle for profile in self.fulfillments.filter(accepted=True).all()) + return any(profile.fulfiller_github_username == handle for profile in self.fulfillments.filter(accepted=True).all()) def is_funder(self, handle): """Determine whether or not the profile is the bounty funder. @@ -933,8 +924,7 @@ def fetch_issue_comments(self, save=True): return [] comment_count = 0 for comment in comments: - if (isinstance(comment, dict) and comment.get('user', {}).get('login', - '') not in settings.IGNORE_COMMENTS_FROM): + if (isinstance(comment, dict) and comment.get('user', {}).get('login', '') not in settings.IGNORE_COMMENTS_FROM): comment_count += 1 self.github_comments = comment_count if comment_count: @@ -951,16 +941,14 @@ def next_bounty(self): if self.current_bounty: return None try: - return Bounty.objects.filter(standard_bounties_id=self.standard_bounties_id, - created_on__gt=self.created_on).order_by('created_on').first() + return Bounty.objects.filter(standard_bounties_id=self.standard_bounties_id, created_on__gt=self.created_on).order_by('created_on').first() except Exception: return None @property def prev_bounty(self): try: - return Bounty.objects.filter(standard_bounties_id=self.standard_bounties_id, - created_on__lt=self.created_on).order_by('-created_on').first() + return Bounty.objects.filter(standard_bounties_id=self.standard_bounties_id, created_on__lt=self.created_on).order_by('-created_on').first() except Exception: return None @@ -999,7 +987,7 @@ def is_notification_eligible(self, var_to_check=True): """ if not var_to_check or self.get_natural_value() < 0.0001 or ( - self.network != settings.ENABLE_NOTIFICATIONS_ON_NETWORK): + self.network != settings.ENABLE_NOTIFICATIONS_ON_NETWORK): return False if self.network == 'mainnet' and (settings.DEBUG or settings.ENV != 'prod'): return False @@ -1184,7 +1172,6 @@ def submitted(self): """Exclude results that have not been submitted.""" return self.exclude(fulfiller_address='0x0000000000000000000000000000000000000000') - class BountyFulfillment(SuperModel): """The structure of a fulfillment on a Bounty.""" @@ -1218,6 +1205,7 @@ def save(self, *args, **kwargs): self.fulfiller_github_username = self.fulfiller_github_username.lstrip('@') super().save(*args, **kwargs) + @property def should_hide(self): return self.fulfiller_github_username in settings.BLOCKED_USERS @@ -1273,6 +1261,7 @@ def __str__(self): class Subscription(SuperModel): + email = models.EmailField(max_length=255) raw_data = models.TextField() ip = models.CharField(max_length=50) @@ -1282,6 +1271,7 @@ def __str__(self): class BountyDocuments(SuperModel): + doc = models.FileField(upload_to=get_upload_filename, null=True, blank=True, help_text=_('Bounty documents.')) doc_type = models.CharField(max_length=50) @@ -1376,8 +1366,8 @@ def __str__(self): """Return the string representation for a tip.""" if self.web3_type == 'yge': return f"({self.network}) - {self.status}{' ORPHAN' if not self.emails else ''} " \ - f"{self.amount} {self.tokenName} to {self.username} from {self.from_name or 'NA'}, " \ - f"created: {naturalday(self.created_on)}, expires: {naturalday(self.expires_date)}" + f"{self.amount} {self.tokenName} to {self.username} from {self.from_name or 'NA'}, " \ + f"created: {naturalday(self.created_on)}, expires: {naturalday(self.expires_date)}" status = 'funded' if self.txid else 'not funded' status = status if not self.receive_txid else 'received' return f"({self.web3_type}) {status} {self.amount} {self.tokenName} to {self.username} from {self.from_name or 'NA'}" @@ -1386,7 +1376,7 @@ def __str__(self): def get_natural_value(self): token = addr_to_token(self.tokenAddress) decimals = token['decimals'] - return float(self.amount) / 10 ** decimals + return float(self.amount) / 10**decimals @property def value_true(self): @@ -1396,7 +1386,7 @@ def value_true(self): def amount_in_wei(self): token = addr_to_token(self.tokenAddress) decimals = token['decimals'] if token else 18 - return float(self.amount) * 10 ** decimals + return float(self.amount) * 10**decimals @property def amount_in_whole_units(self): @@ -1551,7 +1541,6 @@ def receive_url_for_recipient(self): class TipPayoutException(Exception): pass - @receiver(pre_save, sender=Tip, dispatch_uid="psave_tip") def psave_tip(sender, instance, **kwargs): # when a new tip is saved, make sure it doesnt have whitespace in it @@ -1642,8 +1631,7 @@ class Interest(SuperModel): max_length=7, help_text=_('Whether or not the interest requires review'), verbose_name=_('Needs Review')) - signed_nda = models.ForeignKey('dashboard.BountyDocuments', blank=True, null=True, related_name='interest', - on_delete=models.SET_NULL) + signed_nda = models.ForeignKey('dashboard.BountyDocuments', blank=True, null=True, related_name='interest', on_delete=models.SET_NULL) # Interest QuerySet Manager objects = InterestQuerySet.as_manager() @@ -1669,7 +1657,6 @@ def mark_for_review(self): self.save() return self - def auto_user_approve(interest, bounty): interest.pending = False interest.acceptance_date = timezone.now() @@ -1808,7 +1795,8 @@ class Activity(SuperModel): def __str__(self): """Define the string representation of an interested profile.""" return f"{self.profile.handle} type: {self.activity_type} created: {naturalday(self.created)} " \ - f"needs review: {self.needs_review}" + f"needs review: {self.needs_review}" + @property def humanized_activity_type(self): @@ -1822,6 +1810,7 @@ def humanized_activity_type(self): return activity_type[1] return ' '.join([x.capitalize() for x in self.activity_type.split('_')]) + def i18n_name(self): return _(next((x[1] for x in self.ACTIVITY_TYPES if x[0] == self.activity_type), 'Unknown type')) @@ -1871,8 +1860,7 @@ def view_props(self): obj = self.metadata['new_bounty'] activity['title'] = obj.get('title', '') if 'id' in obj: - if 'category' not in obj or obj[ - 'category'] == 'bounty': # backwards-compatible for category-lacking metadata + if 'category' not in obj or obj['category'] == 'bounty': # backwards-compatible for category-lacking metadata activity['bounty_url'] = Bounty.objects.get(pk=obj['id']).get_relative_url() if activity.get('title'): activity['urled_title'] = f'{activity["title"]}' @@ -1885,7 +1873,7 @@ def view_props(self): activity['token'] = token_by_name(obj['token_name']) if 'value_in_token' in obj and activity['token']: activity['value_in_token_disp'] = round((float(obj['value_in_token']) / - 10 ** activity['token']['decimals']) * 1000) / 1000 + 10 ** activity['token']['decimals']) * 1000) / 1000 # finally done! @@ -1956,6 +1944,7 @@ def post_add_activity(sender, instance, created, **kwargs): dupe.delete() + class LabsResearch(SuperModel): """Define the structure of Labs Research object.""" @@ -2091,8 +2080,7 @@ class Profile(SuperModel): form_submission_records = JSONField(default=list, blank=True) max_num_issues_start_work = models.IntegerField(default=3) preferred_payout_address = models.CharField(max_length=255, default='', blank=True) - preferred_kudos_wallet = models.OneToOneField('kudos.Wallet', related_name='preferred_kudos_wallet', - on_delete=models.SET_NULL, null=True, blank=True) + preferred_kudos_wallet = models.OneToOneField('kudos.Wallet', related_name='preferred_kudos_wallet', on_delete=models.SET_NULL, null=True, blank=True) max_tip_amount_usdt_per_tx = models.DecimalField(default=2500, decimal_places=2, max_digits=50) max_tip_amount_usdt_per_week = models.DecimalField(default=20000, decimal_places=2, max_digits=50) last_visit = models.DateTimeField(null=True, blank=True) @@ -2144,8 +2132,7 @@ def get_sent_bounties(self): @property def get_my_grants(self): from grants.models import Grant - return Grant.objects.filter( - Q(admin_profile=self) | Q(team_members__in=[self]) | Q(subscriptions__contributor_profile=self)) + return Grant.objects.filter(Q(admin_profile=self) | Q(team_members__in=[self]) | Q(subscriptions__contributor_profile=self)) @property def get_my_kudos(self): @@ -2206,20 +2193,21 @@ def get_average_star_rating(self): feedbacks = FeedbackEntry.objects.filter(receiver_profile=self).all() average_rating = {} average_rating['overall'] = sum([feedback.rating for feedback in feedbacks]) \ - / feedbacks.count() if feedbacks.count() != 0 else 0 + / feedbacks.count() if feedbacks.count() != 0 else 0 average_rating['code_quality_rating'] = sum([feedback.code_quality_rating for feedback in feedbacks]) \ - / feedbacks.count() if feedbacks.count() != 0 else 0 + / feedbacks.count() if feedbacks.count() != 0 else 0 average_rating['communication_rating'] = sum([feedback.communication_rating for feedback in feedbacks]) \ - / feedbacks.count() if feedbacks.count() != 0 else 0 + / feedbacks.count() if feedbacks.count() != 0 else 0 average_rating['recommendation_rating'] = sum([feedback.recommendation_rating for feedback in feedbacks]) \ - / feedbacks.count() if feedbacks.count() != 0 else 0 + / feedbacks.count() if feedbacks.count() != 0 else 0 average_rating['satisfaction_rating'] = sum([feedback.satisfaction_rating for feedback in feedbacks]) \ - / feedbacks.count() if feedbacks.count() != 0 else 0 + / feedbacks.count() if feedbacks.count() != 0 else 0 average_rating['speed_rating'] = sum([feedback.speed_rating for feedback in feedbacks]) \ - / feedbacks.count() if feedbacks.count() != 0 else 0 + / feedbacks.count() if feedbacks.count() != 0 else 0 average_rating['total_rating'] = feedbacks.count() return average_rating + @property def get_my_verified_check(self): verification = UserVerificationModel.objects.filter(user=self.user).first() @@ -2252,11 +2240,8 @@ def bounties(self): for interested in self.interested.all(): bounties = bounties | Bounty.objects.filter(interested=interested, current_bounty=True) bounties = bounties | Bounty.objects.filter(pk__in=fulfilled_bounty_ids, current_bounty=True) - bounties = bounties | Bounty.objects.filter(bounty_owner_github_username__iexact=self.handle, - current_bounty=True) | Bounty.objects.filter( - bounty_owner_github_username__iexact="@" + self.handle, current_bounty=True) - bounties = bounties | Bounty.objects.filter( - github_url__in=[url for url in self.tips.values_list('github_url', flat=True)], current_bounty=True) + bounties = bounties | Bounty.objects.filter(bounty_owner_github_username__iexact=self.handle, current_bounty=True) | Bounty.objects.filter(bounty_owner_github_username__iexact="@" + self.handle, current_bounty=True) + bounties = bounties | Bounty.objects.filter(github_url__in=[url for url in self.tips.values_list('github_url', flat=True)], current_bounty=True) bounties = bounties.distinct() return bounties.order_by('-web3_created') @@ -2310,21 +2295,21 @@ def no_times_slashed_by_staff(self): user_actions = UserAction.objects.filter( profile=self, action='bounty_removed_slashed_by_staff', - ) + ) return user_actions.count() def no_times_been_removed_by_funder(self): user_actions = UserAction.objects.filter( profile=self, action='bounty_removed_by_funder', - ) + ) return user_actions.count() def no_times_been_removed_by_staff(self): user_actions = UserAction.objects.filter( profile=self, action='bounty_removed_by_staff', - ) + ) return user_actions.count() def get_desc(self, funded_bounties, fulfilled_bounties): @@ -2341,7 +2326,7 @@ def get_desc(self, funded_bounties, fulfilled_bounties): plural = 's' if total_funded_participated != 1 else '' return f"@{self.handle} is a {role} who has participated in {total_funded_participated} " \ - f"funded issue{plural} on Gitcoin" + f"funded issue{plural} on Gitcoin" @property def desc(self): @@ -2493,7 +2478,7 @@ def get_quarterly_stats(self): bounty.value_in_eth if bounty.value_in_eth else 0 for bounty in fulfilled_bounties ]) - total_earned_eth /= 10 ** 18 + total_earned_eth /= 10**18 total_earned_usd = sum([ bounty.value_in_usdt if bounty.value_in_usdt else 0 for bounty in fulfilled_bounties @@ -2530,11 +2515,11 @@ def get_quarterly_stats(self): relevant_bounties = Bounty.objects.none() for keyword in user_coding_languages: relevant_bounties = relevant_bounties.union(potential_bounties.current().filter( - network=Profile.get_network(), - metadata__icontains=keyword, - idx_status__in=['open'], - ).order_by('?') - ) + network=Profile.get_network(), + metadata__icontains=keyword, + idx_status__in=['open'], + ).order_by('?') + ) relevant_bounties = relevant_bounties[:3] relevant_bounties = list(relevant_bounties) # Round to 2 places of decimals to be diplayed in templates @@ -2621,6 +2606,7 @@ def name(self): return self.username + def is_github_token_valid(self): """Check whether or not a Github OAuth token is valid. @@ -2852,9 +2838,8 @@ def get_all_tokens_sum(self, sum_type='collected', network='mainnet', bounties=N tokens_and_values = bounties.values_list('token_name', 'value_in_token') all_tokens_sum_tmp = {token: 0 for token in set([ele[0] for ele in tokens_and_values])} for ele in tokens_and_values: - all_tokens_sum_tmp[ele[0]] += ele[1] / 10 ** 18 - all_tokens_sum = [{'token_name': token_name, 'value_in_token': value_in_token} for - token_name, value_in_token in all_tokens_sum_tmp.items()] + all_tokens_sum_tmp[ele[0]] += ele[1] / 10**18 + all_tokens_sum = [{'token_name': token_name, 'value_in_token': value_in_token} for token_name, value_in_token in all_tokens_sum_tmp.items()] except Exception: pass @@ -3162,8 +3147,7 @@ class UserAction(SuperModel): ] action = models.CharField(max_length=50, choices=ACTION_TYPES, db_index=True) user = models.ForeignKey(User, related_name='actions', on_delete=models.SET_NULL, null=True, db_index=True) - profile = models.ForeignKey('dashboard.Profile', related_name='actions', on_delete=models.CASCADE, null=True, - db_index=True) + profile = models.ForeignKey('dashboard.Profile', related_name='actions', on_delete=models.CASCADE, null=True, db_index=True) ip_address = models.GenericIPAddressField(null=True) location_data = JSONField(default=dict) metadata = JSONField(default=dict) @@ -3391,10 +3375,8 @@ class HackathonEvent(SuperModel): logo_svg = models.FileField(blank=True) start_date = models.DateTimeField() end_date = models.DateTimeField() - background_color = models.CharField(max_length=255, null=True, blank=True, - help_text='hexcode for the banner, default to white') - text_color = models.CharField(max_length=255, null=True, blank=True, - help_text='hexcode for the text, default to black') + background_color = models.CharField(max_length=255, null=True, blank=True, help_text='hexcode for the banner, default to white') + text_color = models.CharField(max_length=255, null=True, blank=True, help_text='hexcode for the text, default to black') identifier = models.CharField(max_length=255, default='', help_text='used for custom styling for the banner') sponsors = models.ManyToManyField(Sponsor, through='HackathonSponsor') @@ -3427,7 +3409,6 @@ class HackathonSponsor(SuperModel): default='G', ) - class FeedbackEntry(SuperModel): bounty = models.ForeignKey( 'dashboard.Bounty', From d78dbe63cda5373921c73a10c282ab8b9d9e0f68 Mon Sep 17 00:00:00 2001 From: Andrew Redden Date: Tue, 3 Sep 2019 16:01:42 +0200 Subject: [PATCH 08/29] Fixed an issue with reconciliation, correct the conditional to check in profile.organizations against profile.orgs --- app/app/management/commands/sync_orgs_repos.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/app/management/commands/sync_orgs_repos.py b/app/app/management/commands/sync_orgs_repos.py index e3453de6b07..838dd2d53b4 100644 --- a/app/app/management/commands/sync_orgs_repos.py +++ b/app/app/management/commands/sync_orgs_repos.py @@ -26,13 +26,11 @@ def recursive_sync(lsynced, handle): if handle not in lsynced: print(f'Syncing User Handle: {handle}') profile = sync_profile(handle) - remove = [x for x in profile.orgs.all() if x.name not in []] - print('Removing Stale Organizations and Groups') + remove = [x for x in profile.orgs.all() if x.name not in profile.organizations] for y in remove: profile.orgs.remove(y) profile.user.groups.filter(name__contains=y.name).delete() - lsynced.append(handle) else: return lsynced From 526d8bba66d3c590d97fcf9795061e2c203211ce Mon Sep 17 00:00:00 2001 From: Andrew Redden Date: Tue, 3 Sep 2019 16:10:05 +0200 Subject: [PATCH 09/29] removed stub --- app/app/management/commands/sync_orgs_repos.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/app/management/commands/sync_orgs_repos.py b/app/app/management/commands/sync_orgs_repos.py index 838dd2d53b4..3b587f1e247 100644 --- a/app/app/management/commands/sync_orgs_repos.py +++ b/app/app/management/commands/sync_orgs_repos.py @@ -8,8 +8,6 @@ class Command(BaseCommand): help = 'Synchronizes Organizations and Repo roles to members' - # def add_arguments(self, parser): - def handle(self, *args, **options): try: From 3b53af33b96a04cfabe939da40ad611cc568629e Mon Sep 17 00:00:00 2001 From: Andrew Redden Date: Tue, 3 Sep 2019 16:13:43 +0200 Subject: [PATCH 10/29] filtering user queries based around the is_active flag --- app/app/management/commands/sync_orgs_repos.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/app/management/commands/sync_orgs_repos.py b/app/app/management/commands/sync_orgs_repos.py index 3b587f1e247..6435730f574 100644 --- a/app/app/management/commands/sync_orgs_repos.py +++ b/app/app/management/commands/sync_orgs_repos.py @@ -12,7 +12,8 @@ def handle(self, *args, **options): try: print("Loading Users....") - all_users = User.objects.all() # potentially want to add some filters here around what users are synced + all_users = User.objects.all( + is_active=True) print(all_users) print("Looking up Organization of user") # memoize the process so we only ever sync once per user @@ -46,7 +47,10 @@ def recursive_sync(lsynced, handle): for member in org_members: - member_user_obj = User.objects.get(profile__handle=member['login']) + member_user_obj = User.objects.get( + profile__handle=member['login'], + is_active=True + ) if member_user_obj is None: continue @@ -76,7 +80,7 @@ def recursive_sync(lsynced, handle): ) for collaborator in repo_collabs: - member_user_obj = User.objects.get(profile__handle=collaborator['login']) + member_user_obj = User.objects.get(profile__handle=collaborator['login'], is_active=True) if member_user_obj is None: continue From 8f47727a5aebbfbba3815a9a98296436fe0d9f1b Mon Sep 17 00:00:00 2001 From: Andrew Redden Date: Tue, 3 Sep 2019 18:47:21 +0200 Subject: [PATCH 11/29] adding GithubRepo --- app/dashboard/models.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/dashboard/models.py b/app/dashboard/models.py index c4577a7c703..1752c8637a1 100644 --- a/app/dashboard/models.py +++ b/app/dashboard/models.py @@ -2035,6 +2035,16 @@ def __str__(self): return self.name +class GithubRepo(SuperModel): + name = models.CharField(max_length=255) + + class Meta: + ordering = ('name',) + + def __str__(self): + return self.name + + class Profile(SuperModel): """Define the structure of the user profile. From f141e7fc766aa5f33126e2be6a4485dec06956bc Mon Sep 17 00:00:00 2001 From: Andrew Redden Date: Thu, 5 Sep 2019 08:53:24 +0200 Subject: [PATCH 12/29] Repo Model Class Added to dashboard/models Profile Model Class - repos added - ManyToMany on Repo Organization Class - repos added - ManyToMany on Repo Repos now synced through the sync command --- .../management/commands/sync_orgs_repos.py | 116 +++++++++++------- app/app/settings.py | 2 +- .../migrations/0051_auto_20190903_2102.py | 36 ++++++ app/dashboard/models.py | 9 +- app/git/utils.py | 12 +- 5 files changed, 120 insertions(+), 55 deletions(-) create mode 100644 app/dashboard/migrations/0051_auto_20190903_2102.py diff --git a/app/app/management/commands/sync_orgs_repos.py b/app/app/management/commands/sync_orgs_repos.py index 6435730f574..72833cf72a2 100644 --- a/app/app/management/commands/sync_orgs_repos.py +++ b/app/app/management/commands/sync_orgs_repos.py @@ -1,6 +1,6 @@ from django.core.management.base import BaseCommand from django.contrib.auth.models import User, Group -from dashboard.models import Profile, Organization +from dashboard.models import Profile, Organization, Repo from git.utils import get_organization, get_user, get_repo from app.utils import sync_profile @@ -12,41 +12,56 @@ def handle(self, *args, **options): try: print("Loading Users....") - all_users = User.objects.all( + all_users = User.objects.filter( is_active=True) - print(all_users) - print("Looking up Organization of user") - # memoize the process so we only ever sync once per user synced = [] def recursive_sync(lsynced, handle): try: + if handle not in lsynced: + print(f'Syncing User Handle: {handle}') profile = sync_profile(handle) + access_token = profile.user.social_auth.filter(provider='github').latest('pk').access_token print('Removing Stale Organizations and Groups') - remove = [x for x in profile.orgs.all() if x.name not in profile.organizations] - for y in remove: + remove_org_groups = [x for x in profile.orgs.all() if x.name not in profile.organizations] + for y in remove_org_groups: profile.orgs.remove(y) profile.user.groups.filter(name__contains=y.name).delete() + + user_access_repos = get_repo(handle, '/repos', (handle, access_token), is_user=True) + # Question around user repo acccess if we can't get user repos, should we assume all repos are no longer available in the platform? + + if 'message' not in user_access_repos: + current_user_repos = [] + for y in user_access_repos: + current_user_repos.append(y['name']) + remove_user_repos_names = [x for x in profile.repos.all() if + x.name not in current_user_repos] + + remove_user_repos = Repo.objects.filter(name__in=remove_user_repos_names, + profile__handle=handle) + for y in remove_user_repos: + profile.repos.remove(y) + lsynced.append(handle) else: return lsynced - access_token = profile.user.social_auth.filter(provider='github').latest('pk').access_token + members_to_sync = [] + for org in profile.organizations: db_org = Organization.objects.get_or_create(name=org)[0] - print(f'Syncing Org: {db_org.name}') + print(f'Syncing Organization: {db_org.name}') profile.orgs.add(db_org) + org_members = get_organization( db_org.name, - '/members', - (profile.handle, access_token) + '/members' ) - for member in org_members: - member_user_obj = User.objects.get( profile__handle=member['login'], is_active=True @@ -57,57 +72,66 @@ def recursive_sync(lsynced, handle): membership = get_organization( db_org.name, - f'/memberships/{handle}', - (profile.handle, access_token) + f'/memberships/{member["login"]}' ) + role = membership['role'] if not None else "member" - group = Group.objects.get_or_create(name=f'{db_org.name}-role-{role}') + db_group = Group.objects.get_or_create(name=f'{db_org.name}-role-{role}') - db_org.groups.add(group[0]) - member_user_obj.groups.add(group[0]) - lsynced = recursive_sync(lsynced, member['login']) if not None else [] + db_org.groups.add(db_group[0]) + member_user_obj.groups.add(db_group[0]) + members_to_sync.append(member['login']) + # lsynced = recursive_sync(lsynced, member['login']) if not None else [] org_repos = get_organization( db_org.name, - '/repos', - (profile.handle, access_token) + '/repos' ) + for repo in org_repos: + db_repo = Repo.objects.get_or_create(name=repo['name'])[0] + db_org.repos.add(db_repo) + print(f'Syncing Repo: {db_repo.name}') repo_collabs = get_repo( repo['full_name'], '/collaborators', - (profile.handle, access_token) + (handle, access_token) ) + if 'message' not in repo_collabs: + for collaborator in repo_collabs: + member_user_obj = User.objects.get(profile__handle=collaborator['login'], + is_active=True) + if member_user_obj is None: + continue + member_user_profile = Profile.objects.get(handle=collaborator['login']) + + if collaborator['permissions']['admin']: + permission = "admin" + elif collaborator['permissions']['push']: + permission = "write" + elif collaborator['permissions']['pull']: + permission = "pull" + else: + permission = "none" + + db_group = Group.objects.get_or_create( + name=f'{db_org.name}-repo-{repo["name"]}-{permission}')[0] + db_org.groups.add(db_group) + member_user_obj.groups.add(db_group) + member_user_profile.repos.add(db_repo) + if collaborator['login'] not in members_to_sync or collaborator[ + 'login'] not in lsynced: + members_to_sync.append(collaborator['login']) + for x in members_to_sync: + lsynced = lsynced + recursive_sync(lsynced, x) - for collaborator in repo_collabs: - member_user_obj = User.objects.get(profile__handle=collaborator['login'], is_active=True) - - if member_user_obj is None: - continue - - if collaborator['permission']['admin']: - permission = "admin" - elif collaborator['permission']['push']: - permission = "write" - elif collaborator['permission']['pull']: - permission = "pull" - else: - permission = "none" - - group = Group.objects.get_or_create( - name=f'{db_org.name}-repo-{repo["name"]}-{permission}') - db_org.groups.add(group[0]) - member_user_obj.groups.add(group[0]) - lsynced = recursive_sync(lsynced, collaborator['login']) if not None else [] - - return lsynced + return lsynced except Exception as exc: print(exc) for user in all_users: # get profile data now creates or gets the new organization data for each user synced = recursive_sync(synced, user.profile.handle) - print("Profile Sync Completed") - + print("Sync Completed") except ValueError as e: print(e) diff --git a/app/app/settings.py b/app/app/settings.py index 8ff1be2006f..d256df052c9 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -527,7 +527,7 @@ SOCIAL_AUTH_GITHUB_SECRET = GITHUB_CLIENT_SECRET SOCIAL_AUTH_POSTGRES_JSONFIELD = True SOCIAL_AUTH_ADMIN_USER_SEARCH_FIELDS = ['username', 'first_name', 'last_name', 'email'] -SOCIAL_AUTH_GITHUB_SCOPE = ['read:public_repo', 'read:user', 'user:email', 'read:org'] +SOCIAL_AUTH_GITHUB_SCOPE = ['read:public_repo', 'read:user', 'user:email', 'read:org', 'repo'] SOCIAL_AUTH_SANITIZE_REDIRECTS = True SOCIAL_AUTH_PIPELINE = ( diff --git a/app/dashboard/migrations/0051_auto_20190903_2102.py b/app/dashboard/migrations/0051_auto_20190903_2102.py new file mode 100644 index 00000000000..d9d639c6b7a --- /dev/null +++ b/app/dashboard/migrations/0051_auto_20190903_2102.py @@ -0,0 +1,36 @@ +# Generated by Django 2.2.3 on 2019-09-03 21:02 + +from django.db import migrations, models +import economy.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dashboard', '0050_auto_20190823_1706'), + ] + + operations = [ + migrations.CreateModel( + name='Repo', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_on', models.DateTimeField(db_index=True, default=economy.models.get_time)), + ('modified_on', models.DateTimeField(default=economy.models.get_time)), + ('name', models.CharField(max_length=255)), + ], + options={ + 'ordering': ('name',), + }, + ), + migrations.AddField( + model_name='organization', + name='repos', + field=models.ManyToManyField(blank=True, to='dashboard.Repo'), + ), + migrations.AddField( + model_name='profile', + name='repos', + field=models.ManyToManyField(blank=True, to='dashboard.Repo'), + ), + ] diff --git a/app/dashboard/models.py b/app/dashboard/models.py index 1752c8637a1..6c0d0a37467 100644 --- a/app/dashboard/models.py +++ b/app/dashboard/models.py @@ -2024,9 +2024,8 @@ def hidden(self): return self.filter(hide_profile=True) -class Organization(SuperModel): +class Repo(SuperModel): name = models.CharField(max_length=255) - groups = models.ManyToManyField('auth.group', blank=True) class Meta: ordering = ('name',) @@ -2035,9 +2034,10 @@ def __str__(self): return self.name -class GithubRepo(SuperModel): +class Organization(SuperModel): name = models.CharField(max_length=255) - + groups = models.ManyToManyField('auth.group', blank=True) + repos = models.ManyToManyField(Repo, blank=True) class Meta: ordering = ('name',) @@ -2087,6 +2087,7 @@ class Profile(SuperModel): keywords = ArrayField(models.CharField(max_length=200), blank=True, default=list) organizations = ArrayField(models.CharField(max_length=200), blank=True, default=list) orgs = models.ManyToManyField(Organization, blank=True) + repos = models.ManyToManyField(Repo, blank=True) form_submission_records = JSONField(default=list, blank=True) max_num_issues_start_work = models.IntegerField(default=3) preferred_payout_address = models.CharField(max_length=255, default='', blank=True) diff --git a/app/git/utils.py b/app/git/utils.py index 6b59b1c574b..5fbd445c8b8 100644 --- a/app/git/utils.py +++ b/app/git/utils.py @@ -591,11 +591,11 @@ def get_interested_actions(github_url, username, email=''): return actions_by_interested_party -def get_user(user, sub_path=''): +def get_user(user, sub_path='', auth=_AUTH): """Get the github user details.""" user = user.replace('@', '') url = f'https://api.github.com/users/{user}{sub_path}' - response = requests.get(url, auth=_AUTH, headers=HEADERS) + response = requests.get(url, auth=auth, headers=HEADERS) try: response_dict = response.json() @@ -616,10 +616,14 @@ def get_organization(org, sub_path='', auth=_AUTH): return response_dict -def get_repo(repo_full_name, sub_path='', auth=_AUTH): +def get_repo(repo_full_name, sub_path='', auth=_AUTH, is_user=False): """Get the github user details.""" repo_full_name = repo_full_name.replace('@', '') - url = f'https://api.github.com/repos/{repo_full_name}{sub_path}' + if is_user: + url = f'https://api.github.com/user/repos' + else: + url = f'https://api.github.com/repos/{repo_full_name}{sub_path}' + response = requests.get(url, auth=auth, headers=HEADERS) try: From 508a81fb2a2746faf64d6c0b7879ef564eaaad74 Mon Sep 17 00:00:00 2001 From: Dan Lipert Date: Mon, 16 Sep 2019 22:09:55 +0900 Subject: [PATCH 13/29] add merge migration --- .../migrations/0052_merge_20190916_1302.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 app/dashboard/migrations/0052_merge_20190916_1302.py diff --git a/app/dashboard/migrations/0052_merge_20190916_1302.py b/app/dashboard/migrations/0052_merge_20190916_1302.py new file mode 100644 index 00000000000..f82d5756187 --- /dev/null +++ b/app/dashboard/migrations/0052_merge_20190916_1302.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.4 on 2019-09-16 13:02 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('dashboard', '0051_auto_20190903_2102'), + ('dashboard', '0050_searchhistory_search_type'), + ] + + operations = [ + ] From 50b95168bc50f1ff7c1b86be697145154e2481f1 Mon Sep 17 00:00:00 2001 From: Dan Lipert Date: Mon, 16 Sep 2019 22:12:18 +0900 Subject: [PATCH 14/29] fix isort --- app/app/management/commands/sync_orgs_repos.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/app/management/commands/sync_orgs_repos.py b/app/app/management/commands/sync_orgs_repos.py index 72833cf72a2..f9244f61dbc 100644 --- a/app/app/management/commands/sync_orgs_repos.py +++ b/app/app/management/commands/sync_orgs_repos.py @@ -1,8 +1,9 @@ +from django.contrib.auth.models import Group, User from django.core.management.base import BaseCommand -from django.contrib.auth.models import User, Group -from dashboard.models import Profile, Organization, Repo -from git.utils import get_organization, get_user, get_repo + from app.utils import sync_profile +from dashboard.models import Organization, Profile, Repo +from git.utils import get_organization, get_repo, get_user class Command(BaseCommand): From bd3ce81ce6a84d5ef347fa80d0ddb1953a18d165 Mon Sep 17 00:00:00 2001 From: Andrew Redden Date: Mon, 16 Sep 2019 18:19:10 -0300 Subject: [PATCH 15/29] wip debugging to get around more edge cases with null data/bad token auths --- .../management/commands/sync_orgs_repos.py | 112 ++++++++++++------ 1 file changed, 73 insertions(+), 39 deletions(-) diff --git a/app/app/management/commands/sync_orgs_repos.py b/app/app/management/commands/sync_orgs_repos.py index 72833cf72a2..9e252892f31 100644 --- a/app/app/management/commands/sync_orgs_repos.py +++ b/app/app/management/commands/sync_orgs_repos.py @@ -16,6 +16,7 @@ def handle(self, *args, **options): is_active=True) synced = [] + orgs_synced = [] def recursive_sync(lsynced, handle): try: @@ -34,25 +35,35 @@ def recursive_sync(lsynced, handle): user_access_repos = get_repo(handle, '/repos', (handle, access_token), is_user=True) # Question around user repo acccess if we can't get user repos, should we assume all repos are no longer available in the platform? - if 'message' not in user_access_repos: - current_user_repos = [] - for y in user_access_repos: - current_user_repos.append(y['name']) - remove_user_repos_names = [x for x in profile.repos.all() if - x.name not in current_user_repos] + if 'message' in user_access_repos: + print(user_access_repos['message']) + return lsynced - remove_user_repos = Repo.objects.filter(name__in=remove_user_repos_names, - profile__handle=handle) - for y in remove_user_repos: - profile.repos.remove(y) + current_user_repos = [] + for y in user_access_repos: + current_user_repos.append(y['name']) + remove_user_repos_names = [x for x in profile.repos.all() if + x.name not in current_user_repos] + + remove_user_repos = Repo.objects.filter(name__in=remove_user_repos_names, + profile__handle=handle) + for y in remove_user_repos: + profile.repos.remove(y) lsynced.append(handle) else: return lsynced members_to_sync = [] - + print(profile.organizations) + if profile.organizations is None: + print("no profile") + return [] for org in profile.organizations: + if org in orgs_synced: + continue + + orgs_synced.append(org) db_org = Organization.objects.get_or_create(name=org)[0] print(f'Syncing Organization: {db_org.name}') profile.orgs.add(db_org) @@ -61,6 +72,11 @@ def recursive_sync(lsynced, handle): db_org.name, '/members' ) + + if 'message' in org_members: + print(org_members['message']) + continue + for member in org_members: member_user_obj = User.objects.get( profile__handle=member['login'], @@ -81,13 +97,17 @@ def recursive_sync(lsynced, handle): db_org.groups.add(db_group[0]) member_user_obj.groups.add(db_group[0]) members_to_sync.append(member['login']) - # lsynced = recursive_sync(lsynced, member['login']) if not None else [] + lsynced = recursive_sync(lsynced, member['login']) org_repos = get_organization( db_org.name, '/repos' ) + if 'message' in org_repos: + print(org_repos['message']) + continue + for repo in org_repos: db_repo = Repo.objects.get_or_create(name=repo['name'])[0] db_org.repos.add(db_repo) @@ -97,41 +117,55 @@ def recursive_sync(lsynced, handle): '/collaborators', (handle, access_token) ) - if 'message' not in repo_collabs: - for collaborator in repo_collabs: - member_user_obj = User.objects.get(profile__handle=collaborator['login'], - is_active=True) - if member_user_obj is None: - continue - member_user_profile = Profile.objects.get(handle=collaborator['login']) - - if collaborator['permissions']['admin']: - permission = "admin" - elif collaborator['permissions']['push']: - permission = "write" - elif collaborator['permissions']['pull']: - permission = "pull" - else: - permission = "none" - - db_group = Group.objects.get_or_create( - name=f'{db_org.name}-repo-{repo["name"]}-{permission}')[0] - db_org.groups.add(db_group) - member_user_obj.groups.add(db_group) - member_user_profile.repos.add(db_repo) - if collaborator['login'] not in members_to_sync or collaborator[ - 'login'] not in lsynced: - members_to_sync.append(collaborator['login']) - for x in members_to_sync: + if 'message' in repo_collabs: + print(repo_collabs['message']) + continue + for collaborator in repo_collabs: + print(collaborator) + member_user_obj = User.objects.get(profile__handle=collaborator['login'], + is_active=True) + if member_user_obj is None: + continue + member_user_profile = Profile.objects.get(handle=collaborator['login']) + + if collaborator['permissions']['admin']: + permission = "admin" + elif collaborator['permissions']['push']: + permission = "write" + elif collaborator['permissions']['pull']: + permission = "pull" + else: + permission = "none" + + db_group = Group.objects.get_or_create( + name=f'{db_org.name}-repo-{repo["name"]}-{permission}')[0] + db_org.groups.add(db_group) + member_user_obj.groups.add(db_group) + member_user_profile.repos.add(db_repo) + print(members_to_sync) + + if collaborator['login'] not in members_to_sync or collaborator[ + 'login'] not in lsynced: + members_to_sync.append(collaborator['login']) + for x in members_to_sync: + print(x) + try: lsynced = lsynced + recursive_sync(lsynced, x) + except ValueError as members_loop_exec: + print("here 2") + print(members_loop_exec) return lsynced except Exception as exc: + print("here") print(exc) for user in all_users: # get profile data now creates or gets the new organization data for each user - synced = recursive_sync(synced, user.profile.handle) + try: + synced = recursive_sync(synced, user.profile.handle) + except ValueError as loop_exc: + print(loop_exc) print("Sync Completed") except ValueError as e: print(e) From ef8d506d53b710fa4e95903138024c7ff7cda365 Mon Sep 17 00:00:00 2001 From: Andrew Redden Date: Fri, 20 Sep 2019 17:20:24 -0300 Subject: [PATCH 16/29] streamlined migrations --- .../migrations/0049_merge_20190823_0222.py | 14 -------- .../migrations/0050_auto_20190823_1706.py | 19 ---------- .../migrations/0051_auto_20190903_2102.py | 36 ------------------- .../migrations/0052_merge_20190916_1302.py | 14 -------- ...816_1645.py => 0053_auto_20190920_2018.py} | 28 +++++++++++---- 5 files changed, 21 insertions(+), 90 deletions(-) delete mode 100644 app/dashboard/migrations/0049_merge_20190823_0222.py delete mode 100644 app/dashboard/migrations/0050_auto_20190823_1706.py delete mode 100644 app/dashboard/migrations/0051_auto_20190903_2102.py delete mode 100644 app/dashboard/migrations/0052_merge_20190916_1302.py rename app/dashboard/migrations/{0045_auto_20190816_1645.py => 0053_auto_20190920_2018.py} (51%) diff --git a/app/dashboard/migrations/0049_merge_20190823_0222.py b/app/dashboard/migrations/0049_merge_20190823_0222.py deleted file mode 100644 index cf7f7ebe170..00000000000 --- a/app/dashboard/migrations/0049_merge_20190823_0222.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 2.2.3 on 2019-08-23 02:22 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('dashboard', '0045_auto_20190816_1645'), - ('dashboard', '0048_merge_20190808_1934'), - ] - - operations = [ - ] diff --git a/app/dashboard/migrations/0050_auto_20190823_1706.py b/app/dashboard/migrations/0050_auto_20190823_1706.py deleted file mode 100644 index f96ee5c87ac..00000000000 --- a/app/dashboard/migrations/0050_auto_20190823_1706.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.2.3 on 2019-08-23 17:06 - -import django.contrib.postgres.fields -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('dashboard', '0049_merge_20190823_0222'), - ] - - operations = [ - migrations.AlterField( - model_name='activity', - name='activity_type', - field=models.CharField(blank=True, choices=[('new_bounty', 'New Bounty'), ('start_work', 'Work Started'), ('stop_work', 'Work Stopped'), ('work_submitted', 'Work Submitted'), ('work_done', 'Work Done'), ('worker_approved', 'Worker Approved'), ('worker_rejected', 'Worker Rejected'), ('worker_applied', 'Worker Applied'), ('increased_bounty', 'Increased Funding'), ('killed_bounty', 'Canceled Bounty'), ('new_tip', 'New Tip'), ('receive_tip', 'Tip Received'), ('bounty_abandonment_escalation_to_mods', 'Escalated checkin from @gitcoinbot about bounty status'), ('bounty_abandonment_warning', 'Checkin from @gitcoinbot about bounty status'), ('bounty_removed_slashed_by_staff', 'Dinged and Removed from Bounty by Staff'), ('bounty_removed_by_staff', 'Removed from Bounty by Staff'), ('bounty_removed_by_funder', 'Removed from Bounty by Funder'), ('new_crowdfund', 'New Crowdfund Contribution'), ('new_grant', 'New Grant'), ('update_grant', 'Updated Grant'), ('killed_grant', 'Cancelled Grant'), ('new_grant_contribution', 'Contributed to Grant'), ('new_grant_subscription', 'Subscribed to Grant'), ('killed_grant_contribution', 'Cancelled Grant Contribution'), ('new_milestone', 'New Milestone'), ('update_milestone', 'Updated Milestone'), ('new_kudos', 'New Kudos'), ('joined', 'Joined Gitcoin'), ('updated_avatar', 'Updated Avatar')], db_index=True, max_length=50), - ) - ] diff --git a/app/dashboard/migrations/0051_auto_20190903_2102.py b/app/dashboard/migrations/0051_auto_20190903_2102.py deleted file mode 100644 index d9d639c6b7a..00000000000 --- a/app/dashboard/migrations/0051_auto_20190903_2102.py +++ /dev/null @@ -1,36 +0,0 @@ -# Generated by Django 2.2.3 on 2019-09-03 21:02 - -from django.db import migrations, models -import economy.models - - -class Migration(migrations.Migration): - - dependencies = [ - ('dashboard', '0050_auto_20190823_1706'), - ] - - operations = [ - migrations.CreateModel( - name='Repo', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_on', models.DateTimeField(db_index=True, default=economy.models.get_time)), - ('modified_on', models.DateTimeField(default=economy.models.get_time)), - ('name', models.CharField(max_length=255)), - ], - options={ - 'ordering': ('name',), - }, - ), - migrations.AddField( - model_name='organization', - name='repos', - field=models.ManyToManyField(blank=True, to='dashboard.Repo'), - ), - migrations.AddField( - model_name='profile', - name='repos', - field=models.ManyToManyField(blank=True, to='dashboard.Repo'), - ), - ] diff --git a/app/dashboard/migrations/0052_merge_20190916_1302.py b/app/dashboard/migrations/0052_merge_20190916_1302.py deleted file mode 100644 index f82d5756187..00000000000 --- a/app/dashboard/migrations/0052_merge_20190916_1302.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 2.2.4 on 2019-09-16 13:02 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('dashboard', '0051_auto_20190903_2102'), - ('dashboard', '0050_searchhistory_search_type'), - ] - - operations = [ - ] diff --git a/app/dashboard/migrations/0045_auto_20190816_1645.py b/app/dashboard/migrations/0053_auto_20190920_2018.py similarity index 51% rename from app/dashboard/migrations/0045_auto_20190816_1645.py rename to app/dashboard/migrations/0053_auto_20190920_2018.py index 42aebaccd91..713479a559b 100644 --- a/app/dashboard/migrations/0045_auto_20190816_1645.py +++ b/app/dashboard/migrations/0053_auto_20190920_2018.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.3 on 2019-08-16 16:45 +# Generated by Django 2.2.4 on 2019-09-20 20:18 from django.db import migrations, models import economy.models @@ -8,10 +8,22 @@ class Migration(migrations.Migration): dependencies = [ ('auth', '0011_update_proxy_permissions'), - ('dashboard', '0044_auto_20190729_1817'), + ('dashboard', '0052_auto_20190919_1445'), ] operations = [ + migrations.CreateModel( + name='Repo', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_on', models.DateTimeField(db_index=True, default=economy.models.get_time)), + ('modified_on', models.DateTimeField(default=economy.models.get_time)), + ('name', models.CharField(max_length=255)), + ], + options={ + 'ordering': ('name',), + }, + ), migrations.CreateModel( name='Organization', fields=[ @@ -20,18 +32,20 @@ class Migration(migrations.Migration): ('modified_on', models.DateTimeField(default=economy.models.get_time)), ('name', models.CharField(max_length=255)), ('groups', models.ManyToManyField(blank=True, to='auth.Group')), + ('repos', models.ManyToManyField(blank=True, to='dashboard.Repo')), ], options={ - 'abstract': False, + 'ordering': ('name',), }, ), - migrations.AlterModelOptions( - name='organization', - options={'ordering': ('name',)}, - ), migrations.AddField( model_name='profile', name='orgs', field=models.ManyToManyField(blank=True, to='dashboard.Organization'), ), + migrations.AddField( + model_name='profile', + name='repos', + field=models.ManyToManyField(blank=True, to='dashboard.Repo'), + ), ] From b2af37cf28c522fe8f2480dd8d63d901e2b2c60d Mon Sep 17 00:00:00 2001 From: Andrew Redden Date: Sat, 28 Sep 2019 16:42:18 -0300 Subject: [PATCH 17/29] get_user method updated - added a scope variable that if set lets the scope path be queried for the user scope --- app/app/management/commands/sync_orgs_repos.py | 8 +++++--- app/app/utils.py | 2 +- app/git/utils.py | 11 ++++++++--- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/app/app/management/commands/sync_orgs_repos.py b/app/app/management/commands/sync_orgs_repos.py index c2c9f170f67..f41e9bf4c1f 100644 --- a/app/app/management/commands/sync_orgs_repos.py +++ b/app/app/management/commands/sync_orgs_repos.py @@ -56,7 +56,6 @@ def recursive_sync(lsynced, handle): return lsynced members_to_sync = [] - print(profile.organizations) if profile.organizations is None: print("no profile") return [] @@ -158,15 +157,18 @@ def recursive_sync(lsynced, handle): return lsynced except Exception as exc: - print("here") + print('unexpected error occured:') print(exc) for user in all_users: # get profile data now creates or gets the new organization data for each user try: - synced = recursive_sync(synced, user.profile.handle) + if user and user.profile and user.profile.handle: + synced = recursive_sync(synced, user.profile.handle) except ValueError as loop_exc: + print(f'Error syncing user id:{user.id}') print(loop_exc) + print("Sync Completed") except ValueError as e: print(e) diff --git a/app/app/utils.py b/app/app/utils.py index 5a62a8e59e0..6014dfbdceb 100644 --- a/app/app/utils.py +++ b/app/app/utils.py @@ -207,7 +207,7 @@ def sync_profile(handle, user=None, hide_profile=True): # store the org info in postgres try: profile, created = Profile.objects.update_or_create(handle=handle, defaults=defaults) - orgs = get_user(handle, '/orgs') + orgs = get_user(handle, '', scope='orgs') profile.organizations = [ele['login'] for ele in orgs] print("Profile:", profile, "- created" if created else "- updated") keywords = [] diff --git a/app/git/utils.py b/app/git/utils.py index 5fbd445c8b8..3f892cabbbb 100644 --- a/app/git/utils.py +++ b/app/git/utils.py @@ -591,10 +591,15 @@ def get_interested_actions(github_url, username, email=''): return actions_by_interested_party -def get_user(user, sub_path='', auth=_AUTH): +def get_user(user, sub_path='', scope='', auth=_AUTH): """Get the github user details.""" - user = user.replace('@', '') - url = f'https://api.github.com/users/{user}{sub_path}' + if scope is not '': + print(scope) + url = f'https://api.github.com/user/{scope}' + else: + user = user.replace('@', '') + url = f'https://api.github.com/users/{user}{sub_path}' + response = requests.get(url, auth=auth, headers=HEADERS) try: From 64c01a4c0d394feb23169d5717377bbf21a97452 Mon Sep 17 00:00:00 2001 From: Andrew Redden Date: Mon, 30 Sep 2019 12:07:18 -0300 Subject: [PATCH 18/29] sync_orgs_repos mgmt command updated - switch to querying profiles and querying users where they exist - additional exception handling to database calls made inside the `recursive_sync` function --- .../management/commands/sync_orgs_repos.py | 52 +++++++++++-------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/app/app/management/commands/sync_orgs_repos.py b/app/app/management/commands/sync_orgs_repos.py index f41e9bf4c1f..2a5f133ea27 100644 --- a/app/app/management/commands/sync_orgs_repos.py +++ b/app/app/management/commands/sync_orgs_repos.py @@ -13,8 +13,9 @@ def handle(self, *args, **options): try: print("Loading Users....") - all_users = User.objects.filter( - is_active=True) + all_users = Profile.objects.filter( + user__is_active=True + ).prefetch_related('user') synced = [] orgs_synced = [] @@ -43,6 +44,7 @@ def recursive_sync(lsynced, handle): current_user_repos = [] for y in user_access_repos: current_user_repos.append(y['name']) + remove_user_repos_names = [x for x in profile.repos.all() if x.name not in current_user_repos] @@ -57,7 +59,7 @@ def recursive_sync(lsynced, handle): members_to_sync = [] if profile.organizations is None: - print("no profile") + print("no organizations to sync") return [] for org in profile.organizations: if org in orgs_synced: @@ -78,12 +80,14 @@ def recursive_sync(lsynced, handle): continue for member in org_members: - member_user_obj = User.objects.get( - profile__handle=member['login'], - is_active=True - ) - if member_user_obj is None: + try: + member_profile_obj = Profile.objects.get( + handle=member['login'], + user__is_active=True + ).prefetch_related('user') + + except Exception as e: continue membership = get_organization( @@ -95,7 +99,7 @@ def recursive_sync(lsynced, handle): db_group = Group.objects.get_or_create(name=f'{db_org.name}-role-{role}') db_org.groups.add(db_group[0]) - member_user_obj.groups.add(db_group[0]) + member_profile_obj.user.groups.add(db_group[0]) members_to_sync.append(member['login']) lsynced = recursive_sync(lsynced, member['login']) @@ -121,12 +125,14 @@ def recursive_sync(lsynced, handle): print(repo_collabs['message']) continue for collaborator in repo_collabs: - print(collaborator) - member_user_obj = User.objects.get(profile__handle=collaborator['login'], - is_active=True) - if member_user_obj is None: + try: + member_user_profile = Profile.objects.get(handle=collaborator['login'], + user__is_active=True) + except Exception as e: + continue + + if member_user_profile is None: continue - member_user_profile = Profile.objects.get(handle=collaborator['login']) if collaborator['permissions']['admin']: permission = "admin" @@ -140,9 +146,8 @@ def recursive_sync(lsynced, handle): db_group = Group.objects.get_or_create( name=f'{db_org.name}-repo-{repo["name"]}-{permission}')[0] db_org.groups.add(db_group) - member_user_obj.groups.add(db_group) + member_user_profile.user.groups.add(db_group) member_user_profile.repos.add(db_repo) - print(members_to_sync) if collaborator['login'] not in members_to_sync or collaborator[ 'login'] not in lsynced: @@ -152,22 +157,23 @@ def recursive_sync(lsynced, handle): try: lsynced = lsynced + recursive_sync(lsynced, x) except ValueError as members_loop_exec: - print("here 2") + print(members_loop_exec) return lsynced except Exception as exc: - print('unexpected error occured:') - print(exc) + print(f'Unhandled Exception occurred: {exc}') - for user in all_users: + for profile in all_users: # get profile data now creates or gets the new organization data for each user try: - if user and user.profile and user.profile.handle: - synced = recursive_sync(synced, user.profile.handle) + if profile.handle is not None: + synced = recursive_sync(synced, profile.handle) except ValueError as loop_exc: - print(f'Error syncing user id:{user.id}') + print(f'Error syncing user id:{profile.user.id}') print(loop_exc) + except Exception as e: + print(e) print("Sync Completed") except ValueError as e: From 77c6bb616ff7cc84278a6963ecbb650a569d92bf Mon Sep 17 00:00:00 2001 From: Andrew Redden Date: Mon, 30 Sep 2019 23:02:14 -0300 Subject: [PATCH 19/29] sync_orgs_repo management command - added additional edge case handling for querying the database with profile data and the user doesn't exist - changed profile.orgs -> profile.profile_organizations - recreated migration --- .../management/commands/sync_orgs_repos.py | 97 +++++++++---------- ...920_2018.py => 0054_auto_20190930_1525.py} | 6 +- app/dashboard/models.py | 5 +- 3 files changed, 54 insertions(+), 54 deletions(-) rename app/dashboard/migrations/{0053_auto_20190920_2018.py => 0054_auto_20190930_1525.py} (92%) diff --git a/app/app/management/commands/sync_orgs_repos.py b/app/app/management/commands/sync_orgs_repos.py index 2a5f133ea27..94c4887fdd8 100644 --- a/app/app/management/commands/sync_orgs_repos.py +++ b/app/app/management/commands/sync_orgs_repos.py @@ -29,10 +29,12 @@ def recursive_sync(lsynced, handle): profile = sync_profile(handle) access_token = profile.user.social_auth.filter(provider='github').latest('pk').access_token print('Removing Stale Organizations and Groups') - remove_org_groups = [x for x in profile.orgs.all() if x.name not in profile.organizations] + remove_org_groups = [x for x in profile.profile_organizations.all() if + x.name not in profile.organizations] for y in remove_org_groups: - profile.orgs.remove(y) + profile.profile_organizations.remove(y) profile.user.groups.filter(name__contains=y.name).delete() + print(f'Removing: {profile.handle} from Organization: {y.name} ') user_access_repos = get_repo(handle, '/repos', (handle, access_token), is_user=True) # Question around user repo acccess if we can't get user repos, should we assume all repos are no longer available in the platform? @@ -67,9 +69,9 @@ def recursive_sync(lsynced, handle): orgs_synced.append(org) db_org = Organization.objects.get_or_create(name=org)[0] - print(f'Syncing Organization: {db_org.name}') - profile.orgs.add(db_org) + print(f'Syncing Organization: {db_org.name}') + profile.profile_organizations.add(db_org) org_members = get_organization( db_org.name, '/members' @@ -82,27 +84,25 @@ def recursive_sync(lsynced, handle): for member in org_members: try: + member_profile_obj = Profile.objects.get( handle=member['login'], user__is_active=True - ).prefetch_related('user') - + ) + membership = get_organization( + db_org.name, + f'/memberships/{member["login"]}' + ) + role = membership['role'] if not None else "member" + db_group = Group.objects.get_or_create(name=f'{db_org.name}-role-{role}')[0] + db_org.groups.add(db_group) + member_profile_obj.user.groups.add(db_group) + members_to_sync.append(member['login']) + lsynced = recursive_sync(lsynced, member['login']) except Exception as e: + print(f'Profile for Github Handle does not exist: {member["login"]}') continue - membership = get_organization( - db_org.name, - f'/memberships/{member["login"]}' - ) - - role = membership['role'] if not None else "member" - db_group = Group.objects.get_or_create(name=f'{db_org.name}-role-{role}') - - db_org.groups.add(db_group[0]) - member_profile_obj.user.groups.add(db_group[0]) - members_to_sync.append(member['login']) - lsynced = recursive_sync(lsynced, member['login']) - org_repos = get_organization( db_org.name, '/repos' @@ -115,6 +115,7 @@ def recursive_sync(lsynced, handle): for repo in org_repos: db_repo = Repo.objects.get_or_create(name=repo['name'])[0] db_org.repos.add(db_repo) + print(db_org) print(f'Syncing Repo: {db_repo.name}') repo_collabs = get_repo( repo['full_name'], @@ -124,48 +125,46 @@ def recursive_sync(lsynced, handle): if 'message' in repo_collabs: print(repo_collabs['message']) continue + for collaborator in repo_collabs: try: member_user_profile = Profile.objects.get(handle=collaborator['login'], user__is_active=True) - except Exception as e: - continue - if member_user_profile is None: + if collaborator['permissions']['admin']: + permission = "admin" + elif collaborator['permissions']['push']: + permission = "write" + elif collaborator['permissions']['pull']: + permission = "pull" + else: + permission = "none" + + db_group = Group.objects.get_or_create( + name=f'{db_org.name}-repo-{repo["name"]}-{permission}')[0] + db_org.groups.add(db_group) + member_user_profile.user.groups.add(db_group) + member_user_profile.repos.add(db_repo) + + if collaborator['login'] not in members_to_sync or collaborator[ + 'login'] not in lsynced: + members_to_sync.append(collaborator['login']) + for x in members_to_sync: + print(x) + try: + lsynced = lsynced + recursive_sync(lsynced, x) + except ValueError as members_loop_exec: + + print(members_loop_exec) + except Exception as e: + print(e) continue - if collaborator['permissions']['admin']: - permission = "admin" - elif collaborator['permissions']['push']: - permission = "write" - elif collaborator['permissions']['pull']: - permission = "pull" - else: - permission = "none" - - db_group = Group.objects.get_or_create( - name=f'{db_org.name}-repo-{repo["name"]}-{permission}')[0] - db_org.groups.add(db_group) - member_user_profile.user.groups.add(db_group) - member_user_profile.repos.add(db_repo) - - if collaborator['login'] not in members_to_sync or collaborator[ - 'login'] not in lsynced: - members_to_sync.append(collaborator['login']) - for x in members_to_sync: - print(x) - try: - lsynced = lsynced + recursive_sync(lsynced, x) - except ValueError as members_loop_exec: - - print(members_loop_exec) - return lsynced except Exception as exc: print(f'Unhandled Exception occurred: {exc}') for profile in all_users: - # get profile data now creates or gets the new organization data for each user try: if profile.handle is not None: synced = recursive_sync(synced, profile.handle) diff --git a/app/dashboard/migrations/0053_auto_20190920_2018.py b/app/dashboard/migrations/0054_auto_20190930_1525.py similarity index 92% rename from app/dashboard/migrations/0053_auto_20190920_2018.py rename to app/dashboard/migrations/0054_auto_20190930_1525.py index 713479a559b..b6e0728d509 100644 --- a/app/dashboard/migrations/0053_auto_20190920_2018.py +++ b/app/dashboard/migrations/0054_auto_20190930_1525.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.4 on 2019-09-20 20:18 +# Generated by Django 2.2.4 on 2019-09-30 15:25 from django.db import migrations, models import economy.models @@ -8,7 +8,7 @@ class Migration(migrations.Migration): dependencies = [ ('auth', '0011_update_proxy_permissions'), - ('dashboard', '0052_auto_20190919_1445'), + ('dashboard', '0053_auto_20190920_1816'), ] operations = [ @@ -40,7 +40,7 @@ class Migration(migrations.Migration): ), migrations.AddField( model_name='profile', - name='orgs', + name='profile_organizations', field=models.ManyToManyField(blank=True, to='dashboard.Organization'), ), migrations.AddField( diff --git a/app/dashboard/models.py b/app/dashboard/models.py index c33ff342a0b..0689adfb8e6 100644 --- a/app/dashboard/models.py +++ b/app/dashboard/models.py @@ -2152,8 +2152,9 @@ def __str__(self): class Organization(SuperModel): name = models.CharField(max_length=255) - groups = models.ManyToManyField('auth.group', blank=True) + groups = models.ManyToManyField('auth.Group', blank=True) repos = models.ManyToManyField(Repo, blank=True) + class Meta: ordering = ('name',) @@ -2209,7 +2210,7 @@ class Profile(SuperModel): ) keywords = ArrayField(models.CharField(max_length=200), blank=True, default=list) organizations = ArrayField(models.CharField(max_length=200), blank=True, default=list) - orgs = models.ManyToManyField(Organization, blank=True) + profile_organizations = models.ManyToManyField(Organization, blank=True) repos = models.ManyToManyField(Repo, blank=True) form_submission_records = JSONField(default=list, blank=True) max_num_issues_start_work = models.IntegerField(default=3) From e8ee0b8c60589c3d84d8c0507d25192ef707c495 Mon Sep 17 00:00:00 2001 From: Andrew Redden Date: Tue, 1 Oct 2019 11:16:41 -0300 Subject: [PATCH 20/29] sync_orgs_repos management cmd - additional exception handling and error messages to help debugging any issues with user profiles - private organizations should now correctly be capturable when they are granted access during login --- .../management/commands/sync_orgs_repos.py | 155 +++++++++--------- app/app/utils.py | 3 +- app/git/utils.py | 1 - 3 files changed, 83 insertions(+), 76 deletions(-) diff --git a/app/app/management/commands/sync_orgs_repos.py b/app/app/management/commands/sync_orgs_repos.py index 94c4887fdd8..55bcab92c66 100644 --- a/app/app/management/commands/sync_orgs_repos.py +++ b/app/app/management/commands/sync_orgs_repos.py @@ -63,73 +63,77 @@ def recursive_sync(lsynced, handle): if profile.organizations is None: print("no organizations to sync") return [] - for org in profile.organizations: - if org in orgs_synced: - continue - orgs_synced.append(org) - db_org = Organization.objects.get_or_create(name=org)[0] + print(profile.organizations) + for org in profile.organizations: + try: + print(org) + if org in orgs_synced: + print(f'{org} has been synced already') + continue - print(f'Syncing Organization: {db_org.name}') - profile.profile_organizations.add(db_org) - org_members = get_organization( - db_org.name, - '/members' - ) + orgs_synced.append(org) + db_org = Organization.objects.get_or_create(name=org)[0] - if 'message' in org_members: - print(org_members['message']) - continue + print(f'Syncing Organization: {db_org.name}') + profile.profile_organizations.add(db_org) + org_members = get_organization( + db_org.name, + '/members' + ) - for member in org_members: + if 'message' in org_members: + print(org_members['message']) + continue - try: + for member in org_members: - member_profile_obj = Profile.objects.get( - handle=member['login'], - user__is_active=True - ) - membership = get_organization( - db_org.name, - f'/memberships/{member["login"]}' - ) - role = membership['role'] if not None else "member" - db_group = Group.objects.get_or_create(name=f'{db_org.name}-role-{role}')[0] - db_org.groups.add(db_group) - member_profile_obj.user.groups.add(db_group) - members_to_sync.append(member['login']) - lsynced = recursive_sync(lsynced, member['login']) - except Exception as e: - print(f'Profile for Github Handle does not exist: {member["login"]}') - continue + try: + membership = get_organization( + db_org.name, + f'/memberships/{member["login"]}', + (handle, access_token) + ) + if 'message' in membership: + print(membership['message']) + continue + role = membership['role'] if not None else "member" + db_group = Group.objects.get_or_create(name=f'{db_org.name}-role-{role}')[0] + db_org.groups.add(db_group) + member_profile_obj = Profile.objects.get( + handle=member['login'], + user__is_active=True + ) + member_profile_obj.user.groups.add(db_group) + members_to_sync.append(member['login']) + lsynced = recursive_sync(lsynced, member['login']) + except Exception as e: + print(f'An exception happened in the Organization Loop: handle {member["login"]} {e}') + continue - org_repos = get_organization( - db_org.name, - '/repos' - ) - - if 'message' in org_repos: - print(org_repos['message']) - continue - - for repo in org_repos: - db_repo = Repo.objects.get_or_create(name=repo['name'])[0] - db_org.repos.add(db_repo) - print(db_org) - print(f'Syncing Repo: {db_repo.name}') - repo_collabs = get_repo( - repo['full_name'], - '/collaborators', - (handle, access_token) + org_repos = get_organization( + db_org.name, + '/repos' ) - if 'message' in repo_collabs: - print(repo_collabs['message']) + + if 'message' in org_repos: + print(org_repos['message']) continue - for collaborator in repo_collabs: - try: - member_user_profile = Profile.objects.get(handle=collaborator['login'], - user__is_active=True) + for repo in org_repos: + db_repo = Repo.objects.get_or_create(name=repo['name'])[0] + db_org.repos.add(db_repo) + print(f'Syncing Repo: {db_repo.name}') + repo_collabs = get_repo( + repo['full_name'], + '/collaborators', + (handle, access_token) + ) + if 'message' in repo_collabs: + print(repo_collabs['message']) + continue + + for collaborator in repo_collabs: if collaborator['permissions']['admin']: permission = "admin" @@ -143,27 +147,30 @@ def recursive_sync(lsynced, handle): db_group = Group.objects.get_or_create( name=f'{db_org.name}-repo-{repo["name"]}-{permission}')[0] db_org.groups.add(db_group) - member_user_profile.user.groups.add(db_group) - member_user_profile.repos.add(db_repo) - - if collaborator['login'] not in members_to_sync or collaborator[ - 'login'] not in lsynced: - members_to_sync.append(collaborator['login']) - for x in members_to_sync: - print(x) - try: - lsynced = lsynced + recursive_sync(lsynced, x) - except ValueError as members_loop_exec: - - print(members_loop_exec) - except Exception as e: - print(e) - continue - return lsynced + try: + member_user_profile = Profile.objects.get(handle=collaborator['login'], + user__is_active=True) + member_user_profile.user.groups.add(db_group) + member_user_profile.repos.add(db_repo) + if collaborator['login'] not in members_to_sync or \ + collaborator['login'] not in lsynced: + members_to_sync.append(collaborator['login']) + except Exception as e: + print(f'An exception happened in the Collaborators sync Loop: handle: {collaborator["login"]} {e}') + + for x in members_to_sync: + try: + lsynced = lsynced + recursive_sync(lsynced, x) + except Exception as e: + print(f'An exception happened in the Members sync Loop: handle: {handle} {e}') + except Exception as e: + print(f'An exception happened in the Organization Loop: handle {handle} {e}') except Exception as exc: print(f'Unhandled Exception occurred: {exc}') + return lsynced + for profile in all_users: try: if profile.handle is not None: diff --git a/app/app/utils.py b/app/app/utils.py index 6014dfbdceb..eecf8ff4fe9 100644 --- a/app/app/utils.py +++ b/app/app/utils.py @@ -207,7 +207,8 @@ def sync_profile(handle, user=None, hide_profile=True): # store the org info in postgres try: profile, created = Profile.objects.update_or_create(handle=handle, defaults=defaults) - orgs = get_user(handle, '', scope='orgs') + access_token = profile.user.social_auth.filter(provider='github').latest('pk').access_token + orgs = get_user(handle, '', scope='orgs', auth=(profile.handle, access_token)) profile.organizations = [ele['login'] for ele in orgs] print("Profile:", profile, "- created" if created else "- updated") keywords = [] diff --git a/app/git/utils.py b/app/git/utils.py index 3f892cabbbb..b56f2aff699 100644 --- a/app/git/utils.py +++ b/app/git/utils.py @@ -594,7 +594,6 @@ def get_interested_actions(github_url, username, email=''): def get_user(user, sub_path='', scope='', auth=_AUTH): """Get the github user details.""" if scope is not '': - print(scope) url = f'https://api.github.com/user/{scope}' else: user = user.replace('@', '') From 1831ab99f13b73d0d9baeaa035c9c1ca955ceb69 Mon Sep 17 00:00:00 2001 From: Andrew Redden Date: Tue, 1 Oct 2019 21:27:43 -0300 Subject: [PATCH 21/29] sync_orgs_repo management command - adding a per_page limit of 100 to the calls as a stop gap to proper pagination handling --- .../management/commands/sync_orgs_repos.py | 22 +++++++++---------- app/git/utils.py | 15 +++++++------ 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/app/app/management/commands/sync_orgs_repos.py b/app/app/management/commands/sync_orgs_repos.py index 55bcab92c66..c4cff9bc169 100644 --- a/app/app/management/commands/sync_orgs_repos.py +++ b/app/app/management/commands/sync_orgs_repos.py @@ -64,10 +64,8 @@ def recursive_sync(lsynced, handle): print("no organizations to sync") return [] - print(profile.organizations) for org in profile.organizations: try: - print(org) if org in orgs_synced: print(f'{org} has been synced already') continue @@ -97,7 +95,7 @@ def recursive_sync(lsynced, handle): if 'message' in membership: print(membership['message']) continue - role = membership['role'] if not None else "member" + role = membership['role'] if 'role' in membership else "member" db_group = Group.objects.get_or_create(name=f'{db_org.name}-role-{role}')[0] db_org.groups.add(db_group) member_profile_obj = Profile.objects.get( @@ -106,16 +104,17 @@ def recursive_sync(lsynced, handle): ) member_profile_obj.user.groups.add(db_group) members_to_sync.append(member['login']) - lsynced = recursive_sync(lsynced, member['login']) except Exception as e: print(f'An exception happened in the Organization Loop: handle {member["login"]} {e}') continue org_repos = get_organization( db_org.name, - '/repos' + '/repos', + (handle, access_token) ) + if 'message' in org_repos: print(org_repos['message']) continue @@ -157,13 +156,14 @@ def recursive_sync(lsynced, handle): collaborator['login'] not in lsynced: members_to_sync.append(collaborator['login']) except Exception as e: - print(f'An exception happened in the Collaborators sync Loop: handle: {collaborator["login"]} {e}') + print(f'An exception happened in the Collaborators sync Loop: handle: ' + f'{collaborator["login"]} {e}') - for x in members_to_sync: - try: - lsynced = lsynced + recursive_sync(lsynced, x) - except Exception as e: - print(f'An exception happened in the Members sync Loop: handle: {handle} {e}') + for x in members_to_sync: + try: + lsynced = lsynced + recursive_sync(lsynced, x) + except Exception as e: + print(f'An exception happened in the Members sync Loop: handle: {handle} {e}') except Exception as e: print(f'An exception happened in the Organization Loop: handle {handle} {e}') except Exception as exc: diff --git a/app/git/utils.py b/app/git/utils.py index b56f2aff699..6730e99e2d5 100644 --- a/app/git/utils.py +++ b/app/git/utils.py @@ -42,6 +42,7 @@ JSON_HEADER = {'Accept': 'application/json', 'User-Agent': settings.GITHUB_APP_NAME, 'Origin': settings.BASE_URL} TIMELINE_HEADERS = {'Accept': 'application/vnd.github.mockingbird-preview'} TOKEN_URL = '{api_url}/applications/{client_id}/tokens/{oauth_token}' +PER_PAGE_LIMIT = 100 def github_connect(token=None): @@ -594,10 +595,10 @@ def get_interested_actions(github_url, username, email=''): def get_user(user, sub_path='', scope='', auth=_AUTH): """Get the github user details.""" if scope is not '': - url = f'https://api.github.com/user/{scope}' + url = f'https://api.github.com/user/{scope}?per_page={PER_PAGE_LIMIT}' else: user = user.replace('@', '') - url = f'https://api.github.com/users/{user}{sub_path}' + url = f'https://api.github.com/users/{user}{sub_path}?per_page={PER_PAGE_LIMIT}' response = requests.get(url, auth=auth, headers=HEADERS) @@ -609,9 +610,9 @@ def get_user(user, sub_path='', scope='', auth=_AUTH): def get_organization(org, sub_path='', auth=_AUTH): - """Get the github user details.""" + """Get the github organization details.""" org = org.replace('@', '') - url = f'https://api.github.com/orgs/{org}{sub_path}' + url = f'https://api.github.com/orgs/{org}{sub_path}?per_page={PER_PAGE_LIMIT * 2}' response = requests.get(url, auth=auth, headers=HEADERS) try: response_dict = response.json() @@ -621,12 +622,12 @@ def get_organization(org, sub_path='', auth=_AUTH): def get_repo(repo_full_name, sub_path='', auth=_AUTH, is_user=False): - """Get the github user details.""" + """Get the github repo details.""" repo_full_name = repo_full_name.replace('@', '') if is_user: - url = f'https://api.github.com/user/repos' + url = f'https://api.github.com/user/repos?per_page={PER_PAGE_LIMIT}' else: - url = f'https://api.github.com/repos/{repo_full_name}{sub_path}' + url = f'https://api.github.com/repos/{repo_full_name}{sub_path}?per_page={PER_PAGE_LIMIT}' response = requests.get(url, auth=auth, headers=HEADERS) From 6dd7685a9c24aaf7f95facdca9a85c77fd931e89 Mon Sep 17 00:00:00 2001 From: Andrew Redden Date: Tue, 1 Oct 2019 21:34:57 -0300 Subject: [PATCH 22/29] updated the error message to correct the text --- app/app/management/commands/sync_orgs_repos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/app/management/commands/sync_orgs_repos.py b/app/app/management/commands/sync_orgs_repos.py index c4cff9bc169..e9d249e0787 100644 --- a/app/app/management/commands/sync_orgs_repos.py +++ b/app/app/management/commands/sync_orgs_repos.py @@ -167,7 +167,7 @@ def recursive_sync(lsynced, handle): except Exception as e: print(f'An exception happened in the Organization Loop: handle {handle} {e}') except Exception as exc: - print(f'Unhandled Exception occurred: {exc}') + print(f'Exception occurred inside recursive_sync: {exc}') return lsynced From c8d192dee4d552c1859c0372bdf3ac74fb1fb18b Mon Sep 17 00:00:00 2001 From: octavioamu Date: Wed, 2 Oct 2019 01:16:57 -0300 Subject: [PATCH 23/29] add credentials and fix tests --- app/app/management/commands/sync_orgs_repos.py | 1 + app/git/tests/test_utils.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/app/management/commands/sync_orgs_repos.py b/app/app/management/commands/sync_orgs_repos.py index e9d249e0787..e3d393a446a 100644 --- a/app/app/management/commands/sync_orgs_repos.py +++ b/app/app/management/commands/sync_orgs_repos.py @@ -78,6 +78,7 @@ def recursive_sync(lsynced, handle): org_members = get_organization( db_org.name, '/members' + (handle, access_token) ) if 'message' in org_members: diff --git a/app/git/tests/test_utils.py b/app/git/tests/test_utils.py index b332a946b62..9d0779bfc73 100644 --- a/app/git/tests/test_utils.py +++ b/app/git/tests/test_utils.py @@ -240,7 +240,7 @@ def test_get_issue_timeline_events(self): @responses.activate def test_get_user(self): """Test the github utility get_user method.""" - url = 'https://api.github.com/users/gitcoin' + url = 'https://api.github.com/users/gitcoin?per_page=100'' responses.add(responses.GET, url, headers=HEADERS, json={}, status=200) get_user('@gitcoin') @@ -249,7 +249,7 @@ def test_get_user(self): @responses.activate def test_get_user_subpath(self): """Test the github utility get_user method with a subpath.""" - url = 'https://api.github.com/users/gitcoin/test' + url = 'https://api.github.com/users/gitcoin/test?per_page=100'' responses.add(responses.GET, url, headers=HEADERS, json={}, status=200) get_user('@gitcoin', '/test') From a3e1b1add661be9c5cbb9ae8bd5966ee8d95c328 Mon Sep 17 00:00:00 2001 From: octavioamu Date: Wed, 2 Oct 2019 01:24:13 -0300 Subject: [PATCH 24/29] remove typo --- app/git/tests/test_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/git/tests/test_utils.py b/app/git/tests/test_utils.py index 9d0779bfc73..3da4d31edca 100644 --- a/app/git/tests/test_utils.py +++ b/app/git/tests/test_utils.py @@ -240,7 +240,7 @@ def test_get_issue_timeline_events(self): @responses.activate def test_get_user(self): """Test the github utility get_user method.""" - url = 'https://api.github.com/users/gitcoin?per_page=100'' + url = 'https://api.github.com/users/gitcoin?per_page=100' responses.add(responses.GET, url, headers=HEADERS, json={}, status=200) get_user('@gitcoin') @@ -249,7 +249,7 @@ def test_get_user(self): @responses.activate def test_get_user_subpath(self): """Test the github utility get_user method with a subpath.""" - url = 'https://api.github.com/users/gitcoin/test?per_page=100'' + url = 'https://api.github.com/users/gitcoin/test?per_page=100' responses.add(responses.GET, url, headers=HEADERS, json={}, status=200) get_user('@gitcoin', '/test') From 4ff4d69c3a16a07bb89193182da62269ef8f6b50 Mon Sep 17 00:00:00 2001 From: Andrew Redden Date: Tue, 22 Oct 2019 16:30:03 -0300 Subject: [PATCH 25/29] hide_profile=False is defined for when syncing a profile inside the command(only fetches active user/profiles) --- .../management/commands/sync_orgs_repos.py | 288 +++++++++--------- app/app/utils.py | 2 +- 2 files changed, 146 insertions(+), 144 deletions(-) diff --git a/app/app/management/commands/sync_orgs_repos.py b/app/app/management/commands/sync_orgs_repos.py index 2c725755182..2582598f731 100644 --- a/app/app/management/commands/sync_orgs_repos.py +++ b/app/app/management/commands/sync_orgs_repos.py @@ -11,167 +11,169 @@ class Command(BaseCommand): def handle(self, *args, **options): - try: - print("Loading Users....") - all_users = Profile.objects.filter( - user__is_active=True - ).prefetch_related('user') - - synced = [] - orgs_synced = [] - - def recursive_sync(lsynced, handle): - try: - - if handle not in lsynced: - - print(f'Syncing User Handle: {handle}') - profile = sync_profile(handle) - access_token = profile.user.social_auth.filter(provider='github').latest('pk').access_token - print('Removing Stale Organizations and Groups') - remove_org_groups = [x for x in profile.profile_organizations.all() if - x.name not in profile.organizations] - for y in remove_org_groups: - profile.profile_organizations.remove(y) - profile.user.groups.filter(name__contains=y.name).delete() - print(f'Removing: {profile.handle} from Organization: {y.name} ') - - user_access_repos = get_repo(handle, '/repos', (handle, access_token), is_user=True) - # Question around user repo acccess if we can't get user repos, should we assume all repos are no longer available in the platform? - - if 'message' in user_access_repos: - print(user_access_repos['message']) - return lsynced - - current_user_repos = [] - for y in user_access_repos: - current_user_repos.append(y['name']) - - remove_user_repos_names = [x for x in profile.repos.all() if - x.name not in current_user_repos] - - remove_user_repos = Repo.objects.filter(name__in=remove_user_repos_names, - profile__handle=handle) - for y in remove_user_repos: - profile.repos.remove(y) - - lsynced.append(handle) - else: + def recursive_sync(lsynced, handle): + try: + + if handle not in lsynced: + + print(f'Syncing User Handle: {handle}') + profile = sync_profile(handle) + print('Profile from sync') + print(profile) + access_token = profile.user.social_auth.filter(provider='github').latest('pk').access_token + print('Removing Stale Organizations and Groups') + remove_org_groups = [x for x in profile.profile_organizations.all() if + x.name not in profile.organizations] + for y in remove_org_groups: + profile.profile_organizations.remove(y) + profile.user.groups.filter(name__contains=y.name).delete() + print(f'Removing: {profile.handle} from Organization: {y.name} ') + + user_access_repos = get_repo(handle, '/repos', (handle, access_token), is_user=True) + # Question around user repo acccess if we can't get user repos, should we assume all repos are no longer available in the platform? + + if 'message' in user_access_repos: + print(user_access_repos['message']) return lsynced - members_to_sync = [] - if profile.organizations is None: - print("no organizations to sync") - return [] - - for org in profile.organizations: - try: - if org in orgs_synced: - print(f'{org} has been synced already') + current_user_repos = [] + for y in user_access_repos: + current_user_repos.append(y['name']) + + remove_user_repos_names = [x for x in profile.repos.all() if + x.name not in current_user_repos] + + remove_user_repos = Repo.objects.filter(name__in=remove_user_repos_names, + profile__handle=handle) + for y in remove_user_repos: + profile.repos.remove(y) + + lsynced.append(handle) + else: + return lsynced + + members_to_sync = [] + if profile.organizations is None: + print("no organizations to sync") + return [] + + for org in profile.organizations: + try: + if org in orgs_synced: + print(f'{org} has been synced already') + continue + + orgs_synced.append(org) + db_org = Organization.objects.get_or_create(name=org)[0] + + print(f'Syncing Organization: {db_org.name}') + profile.profile_organizations.add(db_org) + org_members = get_organization( + db_org.name, + '/members', + (handle, access_token) + ) + + if 'message' in org_members: + print(org_members['message']) + continue + + for member in org_members: + + try: + membership = get_organization( + db_org.name, + f'/memberships/{member["login"]}', + (handle, access_token) + ) + if 'message' in membership: + print(membership['message']) + continue + role = membership['role'] if 'role' in membership else "member" + db_group = Group.objects.get_or_create(name=f'{db_org.name}-role-{role}')[0] + db_org.groups.add(db_group) + member_profile_obj = Profile.objects.get( + handle=member['login'], + user__is_active=True + ) + member_profile_obj.user.groups.add(db_group) + members_to_sync.append(member['login']) + except Exception as e: + # print(f'An exception happened in the Organization Loop: handle {member["login"]} {e}') continue - orgs_synced.append(org) - db_org = Organization.objects.get_or_create(name=org)[0] - - print(f'Syncing Organization: {db_org.name}') - profile.profile_organizations.add(db_org) - org_members = get_organization( - db_org.name, - '/members', + org_repos = get_organization( + db_org.name, + '/repos', + (handle, access_token) + ) + + if 'message' in org_repos: + print(org_repos['message']) + continue + + for repo in org_repos: + db_repo = Repo.objects.get_or_create(name=repo['name'])[0] + db_org.repos.add(db_repo) + print(f'Syncing Repo: {db_repo.name}') + repo_collabs = get_repo( + repo['full_name'], + '/collaborators', (handle, access_token) ) - - if 'message' in org_members: - print(org_members['message']) + if 'message' in repo_collabs: + print(repo_collabs['message']) continue - for member in org_members: + for collaborator in repo_collabs: + + if collaborator['permissions']['admin']: + permission = "admin" + elif collaborator['permissions']['push']: + permission = "write" + elif collaborator['permissions']['pull']: + permission = "pull" + else: + permission = "none" + + db_group = Group.objects.get_or_create( + name=f'{db_org.name}-repo-{repo["name"]}-{permission}')[0] + db_org.groups.add(db_group) try: - membership = get_organization( - db_org.name, - f'/memberships/{member["login"]}', - (handle, access_token) - ) - if 'message' in membership: - print(membership['message']) - continue - role = membership['role'] if 'role' in membership else "member" - db_group = Group.objects.get_or_create(name=f'{db_org.name}-role-{role}')[0] - db_org.groups.add(db_group) - member_profile_obj = Profile.objects.get( - handle=member['login'], - user__is_active=True - ) - member_profile_obj.user.groups.add(db_group) - members_to_sync.append(member['login']) + member_user_profile = Profile.objects.get(handle=collaborator['login'], + user__is_active=True) + member_user_profile.user.groups.add(db_group) + member_user_profile.repos.add(db_repo) + if collaborator['login'] not in members_to_sync or \ + collaborator['login'] not in lsynced: + members_to_sync.append(collaborator['login']) except Exception as e: - print(f'An exception happened in the Organization Loop: handle {member["login"]} {e}') continue - org_repos = get_organization( - db_org.name, - '/repos', - (handle, access_token) - ) - - - if 'message' in org_repos: - print(org_repos['message']) + for x in members_to_sync: + try: + lsynced = lsynced + recursive_sync(lsynced, x) + except Exception as e: + # print(f'An exception happened in the Members sync Loop: handle: {handle} {e}') continue + except Exception as e: + # print(f'An exception happened in the Organization Loop: handle {handle} {e}') + continue + except Exception as exc: + print(f'Exception occurred inside recursive_sync: {exc}') - for repo in org_repos: - db_repo = Repo.objects.get_or_create(name=repo['name'])[0] - db_org.repos.add(db_repo) - print(f'Syncing Repo: {db_repo.name}') - repo_collabs = get_repo( - repo['full_name'], - '/collaborators', - (handle, access_token) - ) - if 'message' in repo_collabs: - print(repo_collabs['message']) - continue + return lsynced - for collaborator in repo_collabs: - - if collaborator['permissions']['admin']: - permission = "admin" - elif collaborator['permissions']['push']: - permission = "write" - elif collaborator['permissions']['pull']: - permission = "pull" - else: - permission = "none" - - db_group = Group.objects.get_or_create( - name=f'{db_org.name}-repo-{repo["name"]}-{permission}')[0] - db_org.groups.add(db_group) - - try: - member_user_profile = Profile.objects.get(handle=collaborator['login'], - user__is_active=True) - member_user_profile.user.groups.add(db_group) - member_user_profile.repos.add(db_repo) - if collaborator['login'] not in members_to_sync or \ - collaborator['login'] not in lsynced: - members_to_sync.append(collaborator['login']) - except Exception as e: - print(f'An exception happened in the Collaborators sync Loop: handle: ' - f'{collaborator["login"]} {e}') - - for x in members_to_sync: - try: - lsynced = lsynced + recursive_sync(lsynced, x) - except Exception as e: - print(f'An exception happened in the Members sync Loop: handle: {handle} {e}') - except Exception as e: - print(f'An exception happened in the Organization Loop: handle {handle} {e}') - except Exception as exc: - print(f'Exception occurred inside recursive_sync: {exc}') + try: - return lsynced + print("Loading Users....") + all_users = Profile.objects.filter( + user__is_active=True + ).prefetch_related('user') + synced = [] + orgs_synced = [] for profile in all_users: try: if profile.handle is not None: diff --git a/app/app/utils.py b/app/app/utils.py index eecf8ff4fe9..d1b2a9a54ec 100644 --- a/app/app/utils.py +++ b/app/app/utils.py @@ -183,7 +183,7 @@ def get_upload_filename(instance, filename): def sync_profile(handle, user=None, hide_profile=True): - from dashboard.models import Profile, Organization + from dashboard.models import Profile handle = handle.strip().replace('@', '').lower() data = get_user(handle) email = '' From 7166b6d4859f95fc806d81d383d784cdcbbbc873 Mon Sep 17 00:00:00 2001 From: Andrew Redden Date: Tue, 22 Oct 2019 16:33:21 -0300 Subject: [PATCH 26/29] mend --- app/app/management/commands/sync_orgs_repos.py | 2 +- app/app/utils.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/app/management/commands/sync_orgs_repos.py b/app/app/management/commands/sync_orgs_repos.py index 2582598f731..bd9b2d14d0b 100644 --- a/app/app/management/commands/sync_orgs_repos.py +++ b/app/app/management/commands/sync_orgs_repos.py @@ -17,7 +17,7 @@ def recursive_sync(lsynced, handle): if handle not in lsynced: print(f'Syncing User Handle: {handle}') - profile = sync_profile(handle) + profile = sync_profile(handle, hide_profile=False) print('Profile from sync') print(profile) access_token = profile.user.social_auth.filter(provider='github').latest('pk').access_token diff --git a/app/app/utils.py b/app/app/utils.py index d1b2a9a54ec..f79e360fcba 100644 --- a/app/app/utils.py +++ b/app/app/utils.py @@ -224,6 +224,7 @@ def sync_profile(handle, user=None, hide_profile=True): except Exception as e: logger.error(e) + print("we're crashing out here") return None if user and user.email: From b5f05cdee8d2ba7e7770e9fd7ccd4f2bde895ee2 Mon Sep 17 00:00:00 2001 From: Andrew Redden Date: Tue, 22 Oct 2019 16:59:18 -0300 Subject: [PATCH 27/29] sync_profile updated to check for profile existing before syncing to ensure we aren't changing their profile display status --- app/app/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/app/utils.py b/app/app/utils.py index f79e360fcba..bb02303da85 100644 --- a/app/app/utils.py +++ b/app/app/utils.py @@ -193,7 +193,7 @@ def sync_profile(handle, user=None, hide_profile=True): logger.warning('Failed to fetch github username', exc_info=True, extra={'handle': handle}) return None - defaults = {'last_sync_date': timezone.now(), 'data': data, 'hide_profile': hide_profile, } + defaults = {'last_sync_date': timezone.now(), 'data': data} if user and isinstance(user, User): defaults['user'] = user @@ -206,6 +206,9 @@ def sync_profile(handle, user=None, hide_profile=True): # store the org info in postgres try: + profile_exists = Profile.objects.filter(handle=handle).count() + if not profile_exists: + defaults['hide_profile'] = hide_profile profile, created = Profile.objects.update_or_create(handle=handle, defaults=defaults) access_token = profile.user.social_auth.filter(provider='github').latest('pk').access_token orgs = get_user(handle, '', scope='orgs', auth=(profile.handle, access_token)) From 01c2e6acc15fba1137202c29a113eecf00ba22d5 Mon Sep 17 00:00:00 2001 From: Andrew Redden Date: Tue, 22 Oct 2019 17:06:46 -0300 Subject: [PATCH 28/29] mend --- app/app/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app/app/utils.py b/app/app/utils.py index bb02303da85..dbd0162c4b5 100644 --- a/app/app/utils.py +++ b/app/app/utils.py @@ -227,7 +227,6 @@ def sync_profile(handle, user=None, hide_profile=True): except Exception as e: logger.error(e) - print("we're crashing out here") return None if user and user.email: From 3fd2ce1ff9afa7844cbc1446ff621700367325bf Mon Sep 17 00:00:00 2001 From: Andrew Redden Date: Tue, 22 Oct 2019 17:12:24 -0300 Subject: [PATCH 29/29] mend --- app/app/management/commands/sync_orgs_repos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/app/management/commands/sync_orgs_repos.py b/app/app/management/commands/sync_orgs_repos.py index bd9b2d14d0b..2582598f731 100644 --- a/app/app/management/commands/sync_orgs_repos.py +++ b/app/app/management/commands/sync_orgs_repos.py @@ -17,7 +17,7 @@ def recursive_sync(lsynced, handle): if handle not in lsynced: print(f'Syncing User Handle: {handle}') - profile = sync_profile(handle, hide_profile=False) + profile = sync_profile(handle) print('Profile from sync') print(profile) access_token = profile.user.social_auth.filter(provider='github').latest('pk').access_token