From 1c7ff310418b89b17b7dede8e6e2a91abbabf43f Mon Sep 17 00:00:00 2001 From: Andrew Redden Date: Fri, 16 Aug 2019 16:25:19 -0300 Subject: [PATCH 01/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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 2e0dedf74f4b680ef8b2722a4e3cbebc073e7755 Mon Sep 17 00:00:00 2001 From: octavioamu Date: Thu, 26 Sep 2019 19:12:39 -0300 Subject: [PATCH 18/35] html structure (cherry picked from commit 0faf13ef48e5f1f3a3a3b9171c36c2bb5aa74a21) --- app/app/urls.py | 1 + app/marketing/views.py | 72 +++++++++++++++++++ .../templates/settings/organizations.html | 19 +++++ app/retail/templates/settings/settings.html | 8 +-- 4 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 app/retail/templates/settings/organizations.html diff --git a/app/app/urls.py b/app/app/urls.py index 970a43edf19..a439d00389f 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -511,6 +511,7 @@ re_path(r'^settings/account/?', marketing.views.account_settings, name='account_settings'), re_path(r'^settings/tokens/?', marketing.views.token_settings, name='token_settings'), re_path(r'^settings/job/?', marketing.views.job_settings, name='job_settings'), + re_path(r'^settings/organizations/?', marketing.views.org_settings, name='org_settings'), re_path(r'^settings/(.*)?', marketing.views.email_settings, name='settings'), # marketing views diff --git a/app/marketing/views.py b/app/marketing/views.py index 47cc1f0640e..9ab6e1b081b 100644 --- a/app/marketing/views.py +++ b/app/marketing/views.py @@ -85,6 +85,9 @@ def get_settings_navs(request): }, { 'body': _('Job Status'), 'href': reverse('job_settings'), + }, { + 'body': _('Organizations'), + 'href': reverse('org_settings'), }] @@ -665,6 +668,75 @@ def job_settings(request): return TemplateResponse(request, 'settings/job.html', context) +def org_settings(request): + """Display and save user's Account settings. + + Returns: + TemplateResponse: The user's Account settings template response. + + """ + msg = '' + profile, es, user, is_logged_in = settings_helper_get_auth(request) + + if not user or not profile or not is_logged_in: + login_redirect = redirect('/login/github?next=' + request.get_full_path()) + return login_redirect + + # if request.POST: + + # if 'preferred_payout_address' in request.POST.keys(): + # profile.preferred_payout_address = request.POST.get('preferred_payout_address', '') + # profile.save() + # msg = _('Updated your Address') + # elif request.POST.get('disconnect', False): + # profile.github_access_token = '' + # profile = record_form_submission(request, profile, 'account-disconnect') + # profile.email = '' + # profile.save() + # create_user_action(profile.user, 'account_disconnected', request) + # messages.success(request, _('Your account has been disconnected from Github')) + # logout_redirect = redirect(reverse('logout') + '?next=/') + # return logout_redirect + # elif request.POST.get('delete', False): + + # # remove profile + # profile.hide_profile = True + # profile = record_form_submission(request, profile, 'account-delete') + # profile.email = '' + # profile.save() + + # # remove email + # delete_user_from_mailchimp(es.email) + + # if es: + # es.delete() + # request.user.delete() + # AccountDeletionRequest.objects.create( + # handle=profile.handle, + # profile={ + # 'ip': get_ip(request), + # } + # ) + # profile.delete() + # messages.success(request, _('Your account has been deleted.')) + # logout_redirect = redirect(reverse('logout') + '?next=/') + # return logout_redirect + # else: + # msg = _('Error: did not understand your request') + + context = { + 'is_logged_in': is_logged_in, + 'nav': 'home', + 'active': '/settings/organizations', + 'title': _('Organizations Settings'), + 'navs': get_settings_navs(request), + 'es': es, + 'profile': profile, + 'msg': msg, + } + return TemplateResponse(request, 'settings/organizations.html', context) + + def _leaderboard(request): """Display the leaderboard for top earning or paying profiles. diff --git a/app/retail/templates/settings/organizations.html b/app/retail/templates/settings/organizations.html new file mode 100644 index 00000000000..e1c9ec6b45c --- /dev/null +++ b/app/retail/templates/settings/organizations.html @@ -0,0 +1,19 @@ +{% extends 'settings/settings.html' %} +{% load i18n static %} +{% block settings_content %} +Organization Permissions +

The users below are able to fund, edit settings, approve contributors, and payout contributors on the bounties of the organization

+ +
+
+
+ + Metamask +
+ Manage on GitHub +
+
+

With supporting text below as a natural lead-in to additional content.

+
+
+{% endblock %} diff --git a/app/retail/templates/settings/settings.html b/app/retail/templates/settings/settings.html index 2a3689a818f..5f8e279aea3 100644 --- a/app/retail/templates/settings/settings.html +++ b/app/retail/templates/settings/settings.html @@ -60,16 +60,16 @@ margin-bottom: 20px; } -
+
-
+ +