From ede15ec3455e72e3a2bf3c2cf038651f3694d79a Mon Sep 17 00:00:00 2001 From: Miguel Angel G Date: Wed, 16 Sep 2020 10:19:56 -0500 Subject: [PATCH 01/13] Create collections from cart view --- app/assets/v2/js/cart.js | 38 +++++++- app/grants/models.py | 77 ++++++++++++++- app/grants/templates/grants/cart-vue.html | 66 ++++++++++--- app/grants/urls.py | 4 +- app/grants/views.py | 113 ++++++++++++++-------- 5 files changed, 239 insertions(+), 59 deletions(-) diff --git a/app/assets/v2/js/cart.js b/app/assets/v2/js/cart.js index 6facf90aaa0..7852b008d27 100644 --- a/app/assets/v2/js/cart.js +++ b/app/assets/v2/js/cart.js @@ -83,7 +83,11 @@ Vue.component('grants-cart', { display_email_option: false, countDownActive: false, // BrightID - isBrightIDVerified: false + isBrightIDVerified: false, + // Collection + showCreateCollection: false, + collectionTitle: '', + collectionDescription: '' }; }, @@ -2250,13 +2254,43 @@ Vue.component('grants-cart', { // Final processing await this.setInterruptStatus(null, this.userAddress); await this.finalizeCheckout(); - } + }, // ============================================================================================= // ==================================== END ZKSYNC METHODS ===================================== // ============================================================================================= + createCollection: async function() { + const csrfmiddlewaretoken = document.querySelector('[name=csrfmiddlewaretoken]').value; + const cart = CartData.loadCart(); + const grantIds = cart.map(grant => grant.grant_id); + let response; + + const body = { + collectionTitle: this.collectionTitle, + collectionDescription: this.collectionDescription, + grants: grantIds + }; + + try { + + response = await fetchData('/grants/v1/api/collections/new', 'POST', body, {'X-CSRFToken': csrfmiddlewaretoken}); + const redirect = `/grants/collections?collection_id=${response.collection.id}`; + _alert('Congratulations, your new collection was created successfully!', 'success'); + this.cleanCollectionModal(); + this.showCreateCollection = false; + + window.location = redirect; + + } catch (e) { + _alert(e.msg, 'error'); + } + }, + cleanCollectionModal: function() { + this.collectionTitle = ''; + this.collectionDescription = ''; + } }, watch: { diff --git a/app/grants/models.py b/app/grants/models.py index 2204cdcf6f3..77394a504f2 100644 --- a/app/grants/models.py +++ b/app/grants/models.py @@ -22,11 +22,13 @@ from decimal import Decimal from django.conf import settings +from django.contrib.humanize.templatetags.humanize import naturaltime from django.contrib.postgres.fields import ArrayField, JSONField from django.db import models from django.db.models import Q from django.db.models.signals import post_save, pre_save from django.dispatch import receiver +from django.urls import reverse from django.utils import timezone from django.utils.timezone import localtime from django.utils.translation import gettext_lazy as _ @@ -37,7 +39,7 @@ from economy.models import SuperModel, Token from economy.utils import ConversionRateNotFoundError, convert_amount from gas.utils import eth_usd_conv_rate, recommend_min_gas_price_to_confirm_in_time -from grants.utils import get_upload_filename +from grants.utils import get_upload_filename, is_grant_team_member from townsquare.models import Favorite from web3 import Web3 @@ -566,6 +568,44 @@ def contract(self): grant_contract = web3.eth.contract(Web3.toChecksumAddress(self.contract_address), abi=self.abi) return grant_contract + def repr(self, user): + return { + 'id': self.id, + 'logo_url': self.logo.url if self.logo and self.logo.url else f'v2/images/grants/logos/{self.id % 3}.png', + 'details_url': reverse('grants:details', args=(self.id, self.slug)), + 'title': self.title, + 'description': self.description, + 'last_update': self.last_update, + 'last_update_natural': naturaltime(self.last_update), + 'sybil_score': self.sybil_score, + 'weighted_risk_score': self.weighted_risk_score, + 'is_clr_active': self.is_clr_active, + 'clr_round_num': self.clr_round_num, + 'admin_profile': { + 'url': self.admin_profile.url, + 'handle': self.admin_profile.handle, + 'avatar_url': self.admin_profile.avatar_url + }, + 'favorite': self.favorite(user) if user.is_authenticated else False, + 'is_on_team': is_grant_team_member(self, user.profile) if user.is_authenticated else False, + 'clr_prediction_curve': self.clr_prediction_curve, + 'last_clr_calc_date': naturaltime(self.last_clr_calc_date) if self.last_clr_calc_date else None, + 'safe_next_clr_calc_date': naturaltime(self.safe_next_clr_calc_date) if self.safe_next_clr_calc_date else None, + 'amount_received_in_round': self.amount_received_in_round, + 'positive_round_contributor_count': self.positive_round_contributor_count, + 'monthly_amount_subscribed': self.monthly_amount_subscribed, + 'is_clr_eligible': self.is_clr_eligible, + 'slug': self.slug, + 'url': self.url, + 'contract_version': self.contract_version, + 'contract_address': self.contract_address, + 'token_symbol': self.token_symbol, + 'admin_address': self.admin_address, + 'token_address': self.token_address, + 'image_css': self.image_css, + 'verified': self.twitter_verified, + } + def favorite(self, user): return Favorite.objects.filter(user=user, grant=self).exists() @@ -1622,3 +1662,38 @@ class CartActivity(SuperModel): def __str__(self): return f'{self.action} {self.grant.id if self.grant else "bulk"} from the cart {self.profile.handle}' + + +class GrantCollections(SuperModel): + grants = models.ManyToManyField(blank=True, to=Grant, help_text=_('References to grants related to this collection')) + profile = models.ForeignKey('dashboard.Profile', help_text=_('Owner of the collection'), related_name='curator', on_delete=models.CASCADE) + title = models.CharField(max_length=255, help_text=_('Name of the collection')) + description = models.TextField(default='', blank=True, help_text=_('The description of the collection')) + cover = models.ImageField(upload_to=get_upload_filename, null=True,blank=True, max_length=500, help_text=_('Collection image')) + hidden = models.BooleanField(default=False, help_text=_('Hide the collection'), db_index=True) + + def keyword(self, keyword): + if not keyword: + return self + return self.filter( + Q(description__icontains=keyword) | + Q(title__icontains=keyword) | + Q(profile__handle__icontains=keyword) + ) + + + def to_json_dict(self): + return { + 'id': self.id, + 'curator': { + 'url': self.profile.url, + 'handle': self.profile.handle, + 'avatar_url': self.profile.avatar_url + }, + 'title': self.title, + 'description': self.description, + 'cover': self.cover.url if self.cover else '' + } + + def update_cover(self): + pass diff --git a/app/grants/templates/grants/cart-vue.html b/app/grants/templates/grants/cart-vue.html index 922df61428f..a64a4103d37 100644 --- a/app/grants/templates/grants/cart-vue.html +++ b/app/grants/templates/grants/cart-vue.html @@ -441,6 +441,11 @@

+ + @@ -529,12 +534,47 @@

Verify your phone number

+ {% comment %} Create Collection Modal {% endcomment %} + + + + + + {% comment %} ========================================================= {% endcomment %} {% comment %} ================== START ZKSYNC MODAL =================== {% endcomment %} {% comment %} ========================================================= {% endcomment %} - - + {% comment %} ========================================================= {% endcomment %} {% comment %} =================== END ZKSYNC MODAL ==================== {% endcomment %} {% comment %} ========================================================= {% endcomment %} diff --git a/app/grants/urls.py b/app/grants/urls.py index d7ebae245c8..c53ad059ad8 100644 --- a/app/grants/urls.py +++ b/app/grants/urls.py @@ -24,6 +24,7 @@ grant_new, grant_new_whitelabel, grants, grants_addr_as_json, grants_bulk_add, grants_by_grant_type, grants_cart_view, grants_clr, grants_stats_view, invoice, leaderboard, new_matching_partner, profile, quickstart, subscription_cancel, toggle_grant_favorite, verify_grant, zksync_get_interrupt_status, zksync_set_interrupt_status, + save_collection, ) app_name = 'grants' @@ -65,5 +66,6 @@ path('', grants_by_grant_type, name='grants_by_category2'), path('/', grants_by_grant_type, name='grants_by_category'), path('v1/api/clr', grants_clr, name='grants_clr'), - path('v1/api//verify', verify_grant, name='verify_grant') + path('v1/api//verify', verify_grant, name='verify_grant'), + path('v1/api/collections/new', save_collection, name='create_collection') ] diff --git a/app/grants/views.py b/app/grants/views.py index 354b4d1aba5..e7623f2a9de 100644 --- a/app/grants/views.py +++ b/app/grants/views.py @@ -44,7 +44,7 @@ from django.utils.translation import gettext_lazy as _ from django.views.decorators.cache import cache_page from django.views.decorators.csrf import csrf_exempt -from django.views.decorators.http import require_GET +from django.views.decorators.http import require_GET, require_POST import requests import tweepy @@ -65,7 +65,7 @@ from gas.utils import conf_time_spread, eth_usd_conv_rate, gas_advisories, recommend_min_gas_price_to_confirm_in_time from grants.models import ( CartActivity, Contribution, Flag, Grant, GrantCategory, GrantCLR, GrantType, MatchPledge, PhantomFunding, - Subscription, + Subscription, GrantCollections, ) from grants.utils import emoji_codes, get_leaderboard, get_user_code, is_grant_team_member from inbox.utils import send_notification_to_user_from_gitcoinbot @@ -260,8 +260,17 @@ def grants(request): _type = request.GET.get('type', 'all') return grants_by_grant_type(request, _type) +def get_collections(user, sort, keyword): + _collections = GrantCollections.objects.filter(hidden=False) + _collections = _collections.keyword(keyword).order_by(sort, 'pk') + _collections = _collections.prefetch_related('grants') + + return _collections + def get_grants(request): + grants = [] + collections = [] grant_type = request.GET.get('type', 'all') limit = request.GET.get('limit', 6) @@ -275,7 +284,6 @@ def get_grants(request): following = request.GET.get('following', '') != '' only_contributions = request.GET.get('only_contributions', '') == 'true' - filters = { 'request': request, 'grant_type': grant_type, @@ -288,10 +296,15 @@ def get_grants(request): 'idle_grants': idle_grants, 'only_contributions': only_contributions } - _grants = build_grants_by_type(**filters) - paginator = Paginator(_grants, limit) - grants = paginator.get_page(page) + if grant_type == 'collections': + _collections = get_collections(user, sort, keyword) + paginator = Paginator(_collections, limit) + collections = paginator.get_page(page) + else: + _grants = build_grants_by_type(**filters) + paginator = Paginator(_grants, limit) + grants = paginator.get_page(page) contributions = Contribution.objects.none() if request.user.is_authenticated: @@ -315,42 +328,7 @@ def get_grants(request): grants_array = [] for grant in grants: - grant_json = { - 'id': grant.id, - 'logo_url': grant.logo.url if grant.logo and grant.logo.url else f'v2/images/grants/logos/{grant.id % 3}.png', - 'details_url': reverse('grants:details', args=(grant.id, grant.slug)), - 'title': grant.title, - 'description': grant.description, - 'last_update': grant.last_update, - 'last_update_natural': naturaltime(grant.last_update), - 'sybil_score': grant.sybil_score, - 'weighted_risk_score': grant.weighted_risk_score, - 'is_clr_active': grant.is_clr_active, - 'clr_round_num': grant.clr_round_num, - 'admin_profile': { - 'url': grant.admin_profile.url, - 'handle': grant.admin_profile.handle, - 'avatar_url': grant.admin_profile.avatar_url - }, - 'favorite': grant.favorite(request.user) if request.user.is_authenticated else False, - 'is_on_team': is_grant_team_member(grant, request.user.profile) if request.user.is_authenticated else False, - 'clr_prediction_curve': grant.clr_prediction_curve, - 'last_clr_calc_date': naturaltime(grant.last_clr_calc_date) if grant.last_clr_calc_date else None, - 'safe_next_clr_calc_date': naturaltime(grant.safe_next_clr_calc_date) if grant.safe_next_clr_calc_date else None, - 'amount_received_in_round': grant.amount_received_in_round, - 'positive_round_contributor_count': grant.positive_round_contributor_count, - 'monthly_amount_subscribed': grant.monthly_amount_subscribed, - 'is_clr_eligible': grant.is_clr_eligible, - 'slug': grant.slug, - 'url': grant.url, - 'contract_version': grant.contract_version, - 'contract_address': grant.contract_address, - 'token_symbol': grant.token_symbol, - 'admin_address': grant.admin_address, - 'token_address': grant.token_address, - 'image_css': grant.image_css, - 'verified': grant.twitter_verified, - } + grant_json = grant.repr(request.user) grants_array.append(grant_json) @@ -359,6 +337,7 @@ def get_grants(request): 'current_type': grant_type, 'category': category, 'grants': grants_array, + 'collections': [collection.to_json_dict() for collection in collections], 'credentials': { 'is_staff': request.user.is_staff, 'is_authenticated': request.user.is_authenticated @@ -1836,3 +1815,53 @@ def verify_grant(request, grant_id): 'has_text': has_text, 'account': grant.twitter_handle_1 }) + + +@login_required +@require_POST +def save_collection(request): + title = request.POST.get('collectionTitle') + description = request.POST.get('collectionDescription') + grant_ids = request.POST.getlist('grants[]') + profile = request.user.profile + grant_ids = [int(grant_id) for grant_id in grant_ids] + + if len(grant_ids) == 0: + return JsonResponse({ + 'ok': False, + 'msg': 'We can\'t create empty collections' + + }, status=422) + kwargs = { + 'title': title, + 'description': description, + 'profile': profile, + } + + collection = GrantCollections.objects.create(**kwargs) + collection.grants.set(grant_ids) + + return JsonResponse({ + 'ok': True, + 'collection': { + 'id': collection.id, + 'title': title, + } + }) + + +def get_collection(request, collection_id): + collection = GrantCollections.objects.get(pk=collection_id) + + grants = [grant.repr(request.user) for grant in collection.grants] + + return JsonResponse({ + 'id': collection.id, + 'title': collection.title, + 'grants': grants, + 'curator': { + 'url': collection.profile.url, + 'handle': collection.profile.handle, + 'avatar_url': collection.profile.avatar_url + }, + }) From 53f8787d9f350e13fddc0870477af961188e9f80 Mon Sep 17 00:00:00 2001 From: Miguel Angel G Date: Wed, 16 Sep 2020 15:36:18 -0500 Subject: [PATCH 02/13] Create grant collections --- app/assets/v2/css/grants/collection.css | 111 ++++++++++++++++++ app/assets/v2/js/cart-data.js | 15 ++- app/assets/v2/js/grants/components.js | 67 +++++++++++ app/assets/v2/js/grants/index.js | 107 +++++++++++------ app/grants/admin.py | 10 +- app/grants/models.py | 63 ++++++++-- .../grants/components/collection.html | 76 ++++++++++++ app/grants/templates/grants/index.html | 3 + .../grants/shared/landing_grants.html | 5 + .../grants/shared/landing_subnav.html | 5 +- .../grants/shared/sidebar_search.html | 17 ++- app/grants/urls.py | 6 +- app/grants/views.py | 60 ++++++++-- 13 files changed, 479 insertions(+), 66 deletions(-) create mode 100644 app/assets/v2/css/grants/collection.css create mode 100644 app/assets/v2/js/grants/components.js create mode 100644 app/grants/templates/grants/components/collection.html diff --git a/app/assets/v2/css/grants/collection.css b/app/assets/v2/css/grants/collection.css new file mode 100644 index 00000000000..6969f6cabad --- /dev/null +++ b/app/assets/v2/css/grants/collection.css @@ -0,0 +1,111 @@ +.collection-item { + border-radius: 5px; + display: flex; + flex-direction: column; + border: 1px solid #D3D3D3; + background-color: var(--bg-shade-0); +} + +.collection-item__content { + margin-top: 1.0625rem; +} + +.collection-item__title { + line-height: 24px; + overflow-y: hidden; + font-weight: bold; + margin-bottom: 15px; + font-size: var(--fs-header); +} + +.collection-item__title a { + color: #454545; +} + +.collection-item__pitch { + color: #666666; + height: 3.8rem; + overflow: hidden; + line-height: 22px; + text-overflow: ellipsis; +} + +.collection-item__owner { + align-items: center; + display: flex; +} + +.collection-item__img--grid { + display: grid; + grid-template-columns: repeat(2, 1fr); +} + +.collection-item__img { + height: 11rem; + background-color: #0D023B; + overflow: hidden; + padding: 10px 18px; +} + +.collection-item__img__cover { + background: linear-gradient(0deg, #FFFFFF, #FFFFFF), linear-gradient(0deg, #FFFFFF, #FFFFFF), linear-gradient(0deg, #FFFFFF, #FFFFFF), linear-gradient(0deg, #FFFFFF, #FFFFFF), #FFFFFF; + border-radius: 2px; + height: 75px; + width: 154px; + margin: 2px; +} + +.collection-item__img img { + width: 100%; + object-fit: contain; + height: 100%; +} + +.collection-item__logo { + background-color: white; + border-radius: 10px; + display: inline-block; + margin-top: -35px; +} + +.collection-item__logo img { + width: 65px; + height: 65px; + object-fit: cover; + border-radius: 50%; + margin: 13px; +} + +.collection-item__owner-image--cover { + height: 60px; + width: 60px; + position: absolute; + top: calc(50% - 30px); + left: calc(50% - 30px); +} + +.collection-item__owner-image { + height: 20px; + width: 20px; +} + +.collection-item__owner-image img, .collection-item__owner-image--cover img { + width: 100%; + border-radius: 50%; +} + +.grant-item__footer { + margin-top: auto; + background-color: rgba(156, 255, 213, 0.06); +} + +.collection-item__info a { + border-color: white; + background-color: #6144FF; + color: white; +} + +.collection-item__info a:hover { + color: white; + text-decoration: underline; +} diff --git a/app/assets/v2/js/cart-data.js b/app/assets/v2/js/cart-data.js index 450007cacfa..3a9ed1a5203 100644 --- a/app/assets/v2/js/cart-data.js +++ b/app/assets/v2/js/cart-data.js @@ -46,7 +46,7 @@ class CartData { return bulk_add_cart; } - static addToCart(grantData) { + static addToCart(grantData, no_report) { if (this.cartContainsGrantWithId(grantData.grant_id)) { return; } @@ -57,7 +57,8 @@ class CartData { if (!network) { network = 'mainnet'; } - const acceptsAllTokens = (grantData.grant_token_address === '0x0000000000000000000000000000000000000000'); + const acceptsAllTokens = (grantData.grant_token_address === '0x0000000000000000000000000000000000000000' || + grantData.grant_token_address === '0x0'); let accptedTokenName; @@ -100,10 +101,12 @@ class CartData { cartList.push(grantData); this.setCart(cartList); - fetchData(`/grants/${grantData.grant_id}/activity`, 'POST', { - action: 'ADD_ITEM', - metadata: JSON.stringify(cartList) - }, {'X-CSRFToken': $("input[name='csrfmiddlewaretoken']").val()}); + if (!no_report) { + fetchData(`/grants/${grantData.grant_id}/activity`, 'POST', { + action: 'ADD_ITEM', + metadata: JSON.stringify(cartList) + }, {'X-CSRFToken': $("input[name='csrfmiddlewaretoken']").val()}); + } } static removeIdFromCart(grantId) { diff --git a/app/assets/v2/js/grants/components.js b/app/assets/v2/js/grants/components.js new file mode 100644 index 00000000000..b62056a63a2 --- /dev/null +++ b/app/assets/v2/js/grants/components.js @@ -0,0 +1,67 @@ +Vue.component('grant-card', { + delimiters: [ '[[', ']]' ], + props: [ 'grant', 'cred', 'token', 'view', 'short', 'show_contributions', 'contributions', 'toggle_following' ], + methods: { + get_clr_prediction: function(indexA, indexB) { + if (this.grant.clr_prediction_curve && this.grant.clr_prediction_curve.length) { + return this.grant.clr_prediction_curve[indexA][indexB]; + } + }, + getContributions: function(grantId) { + return this.contributions[grantId] || []; + }, + toggleFollowingGrant: async function(grantId, event) { + event.preventDefault(); + + const favorite_url = `/grants/${grantId}/favorite`; + let response = await fetchData(favorite_url, 'POST'); + + if (response.action === 'follow') { + this.grant.favorite = true; + } else { + this.grant.favorite = false; + } + + return true; + } + } +}); + +Vue.component('grant-collection', { + delimiters: [ '[[', ']]' ], + props: ['collection'], + methods: { + shareCollection: function() { + let testingCodeToCopy = document.querySelector(`#collection-${this.collection.id}`); + + testingCodeToCopy.setAttribute('type', 'text'); + testingCodeToCopy.select(); + + try { + const successful = document.execCommand('copy'); + const msg = successful ? 'successful' : 'unsuccessful'; + + alert(`Grant collection was copied ${msg}: ${testingCodeToCopy.value}`); + } catch (err) { + alert('Oops, unable to copy'); + } + + /* unselect the range */ + testingCodeToCopy.setAttribute('type', 'hidden'); + window.getSelection().removeAllRanges(); + }, + addToCart: async function() { + const collectionDetailsURL = `v1/api/collections/${this.collection.id}`; + const collection = await fetchData(collectionDetailsURL, 'GET'); + + (collection.grants || []).forEach((grant) => { + CartData.addToCart(grant); + }); + + showSideCart(); + }, + getGrantLogo(index) { + return this.collection.grants[index].logo; + } + } +}); diff --git a/app/assets/v2/js/grants/index.js b/app/assets/v2/js/grants/index.js index 3889d62ec24..ca62146a90c 100644 --- a/app/assets/v2/js/grants/index.js +++ b/app/assets/v2/js/grants/index.js @@ -2,34 +2,6 @@ let grantsNumPages = ''; let grantsHasNext = false; let numGrants = ''; -Vue.component('grant-card', { - delimiters: [ '[[', ']]' ], - props: [ 'grant', 'cred', 'token', 'view', 'short', 'show_contributions', 'contributions', 'toggle_following' ], - methods: { - get_clr_prediction: function(indexA, indexB) { - if (this.grant.clr_prediction_curve && this.grant.clr_prediction_curve.length) { - return this.grant.clr_prediction_curve[indexA][indexB]; - } - }, - getContributions: function(grantId) { - return this.contributions[grantId] || []; - }, - toggleFollowingGrant: async function(grantId, event) { - event.preventDefault(); - - const favorite_url = `/grants/${grantId}/favorite`; - let response = await fetchData(favorite_url, 'POST'); - - if (response.action === 'follow') { - this.grant.favorite = true; - } else { - this.grant.favorite = false; - } - - return true; - } - } -}); $(document).ready(() => { $('#sort_option').select2({ @@ -88,7 +60,8 @@ Vue.component('grant-sidebar', { data: function() { return { search: this.keyword, - show_filters: false + show_filters: false, + handle: document.contxt.github_handle }; }, methods: { @@ -135,7 +108,6 @@ Vue.component('grant-sidebar', { } else if (this.isMobileDevice() && this.show_filters === null) { this.show_filters = false; } - console.log(this.show_filters); } }, mounted() { @@ -150,6 +122,7 @@ if (document.getElementById('grants-showcase')) { data: { grants: [], page: 1, + collectionsPage: 1, limit: 6, sort: 'weighted_shuffle', network: document.network, @@ -162,11 +135,14 @@ if (document.getElementById('grants-showcase')) { credentials: false, grant_types: [], contributions: {}, + collections: [], show_contributions: document.show_contributions, lock: false, view: localStorage.getItem('grants_view') || 'grid', shortView: true, bottom: false, + cart_lock: false, + collection_id: document.collection_id, grantsNumPages, grantsHasNext, numGrants @@ -225,6 +201,9 @@ if (document.getElementById('grants-showcase')) { if (this.network !== 'mainnet') { query_elements['network'] = this.network; } + if (self.current_type === 'collections' && this.collection_id) { + query_elements['collection_id'] = this.collection_id; + } return $.param(query_elements); }, @@ -235,6 +214,9 @@ if (document.getElementById('grants-showcase')) { if (filters.type !== null && filters.type !== undefined) { this.current_type = filters.type; + if (this.current_type === 'collections') { + this.collection_id = null; + } } if (filters.category !== null && filters.category !== undefined) { this.category = filters.category; @@ -258,6 +240,10 @@ if (document.getElementById('grants-showcase')) { this.network = filters.network; } + if (filters.type === 'collections') { + this.collectionsPage = 1; + } + this.page = 1; const q = this.getQueryParams(); @@ -279,6 +265,7 @@ if (document.getElementById('grants-showcase')) { network: this.network, keyword: this.keyword, state: this.state, + collections_page: this.collectionsPage, category: this.category, type: this.current_type }; @@ -295,6 +282,10 @@ if (document.getElementById('grants-showcase')) { base_params['only_contributions'] = this.show_contributions; } + if (this.current_type === 'collections' && this.collection_id) { + base_params['collection_id'] = this.collection_id; + } + const params = new URLSearchParams(base_params).toString(); const getGrants = await fetchData(`/grants/cards_info?${params}`); @@ -305,18 +296,26 @@ if (document.getElementById('grants-showcase')) { vm.grants.push(item); }); + getGrants.collections.forEach(function(item) { + vm.collections.push(item); + }); + vm.credentials = getGrants.credentials; vm.grant_types = getGrants.grant_types; vm.contributions = getGrants.contributions; vm.grantsNumPages = getGrants.num_pages; vm.grantsHasNext = getGrants.has_next; vm.numGrants = getGrants.count; - vm.lock = false; - if (vm.grantsHasNext) { + if (this.current_type === 'collections') { + if (vm.grantsHasNext) { + vm.collectionsPage = ++vm.collectionsPage; + } else { + vm.collectionsPage = 1; + } + } else if (vm.grantsHasNext) { vm.page = ++vm.page; - } else { vm.page = 1; } @@ -336,7 +335,47 @@ if (document.getElementById('grants-showcase')) { vm.grantsHasNext = false; } } - } + }, + addAllToCart: async function() { + if (this.cart_lock) + return; + + this.cart_lock = true; + + const base_params = { + no_pagination: true, + sort_option: this.sort, + network: this.network, + keyword: this.keyword, + state: this.state, + category: this.category, + type: this.current_type + }; + + if (this.following) { + base_params['following'] = this.following; + } + + if (this.idle_grants) { + base_params['idle'] = this.idle_grants; + } + + if (this.show_contributions) { + base_params['only_contributions'] = this.show_contributions; + } + + const params = new URLSearchParams(base_params).toString(); + const getGrants = await fetchData(`/grants/bulk_cart?${params}`); + + + (getGrants.grants || []).forEach((grant) => { + CartData.addToCart(grant); + }); + + showSideCart(); + _alert(`Congratulations, ${getGrants.grants.length} ${getGrants.grants.length > 1 ? 'grant were' : 'grants was'} added to your cart!`, 'success'); + this.cart_lock = false; + }, }, beforeMount() { window.addEventListener('scroll', () => { diff --git a/app/grants/admin.py b/app/grants/admin.py index 57f8b4ffd65..1d8c07f258a 100644 --- a/app/grants/admin.py +++ b/app/grants/admin.py @@ -28,7 +28,7 @@ import twitter from grants.models import ( CartActivity, CLRMatch, Contribution, Flag, Grant, GrantCategory, GrantCLR, GrantType, MatchPledge, PhantomFunding, - Subscription, + Subscription, GrantCollections, ) @@ -355,11 +355,15 @@ class GrantCategoryAdmin(admin.ModelAdmin): readonly_fields = ['pk'] - class GrantCLRAdmin(admin.ModelAdmin): list_display = ['pk', 'round_num', 'start_date', 'end_date','is_active'] +class GrantCollectionsAdmin(admin.ModelAdmin): + list_display = ['pk', 'title', 'description', 'hidden', 'cache'] + raw_id_fields = ['profile', 'grants'] + + admin.site.register(PhantomFunding, PhantomFundingAdmin) admin.site.register(MatchPledge, MatchPledgeAdmin) admin.site.register(Grant, GrantAdmin) @@ -371,3 +375,5 @@ class GrantCLRAdmin(admin.ModelAdmin): admin.site.register(GrantType, GrantTypeAdmin) admin.site.register(GrantCategory, GrantCategoryAdmin) admin.site.register(GrantCLR, GrantCLRAdmin) +admin.site.register(GrantCollections, GrantCollectionsAdmin) + diff --git a/app/grants/models.py b/app/grants/models.py index 77394a504f2..deb67464843 100644 --- a/app/grants/models.py +++ b/app/grants/models.py @@ -568,6 +568,23 @@ def contract(self): grant_contract = web3.eth.contract(Web3.toChecksumAddress(self.contract_address), abi=self.abi) return grant_contract + def cart_payload(self): + return { + 'grant_id': str(self.id), + 'grant_slug': self.slug, + 'grant_url': self.url, + 'grant_title': self.title, + 'grant_contract_version': self.contract_version, + 'grant_contract_address': self.contract_address, + 'grant_token_symbol': self.token_symbol, + 'grant_admin_address': self.admin_address, + 'grant_token_address': self.token_address, + 'grant_logo': self.logo.url if self.logo and self.logo.url else f'v2/images/grants/logos/{self.id % 3}.png', + 'grant_clr_prediction_curve': self.clr_prediction_curve, + 'grant_image_css': self.image_css, + 'is_clr_eligible': self.is_clr_eligible + } + def repr(self, user): return { 'id': self.id, @@ -1664,13 +1681,12 @@ def __str__(self): return f'{self.action} {self.grant.id if self.grant else "bulk"} from the cart {self.profile.handle}' -class GrantCollections(SuperModel): - grants = models.ManyToManyField(blank=True, to=Grant, help_text=_('References to grants related to this collection')) - profile = models.ForeignKey('dashboard.Profile', help_text=_('Owner of the collection'), related_name='curator', on_delete=models.CASCADE) - title = models.CharField(max_length=255, help_text=_('Name of the collection')) - description = models.TextField(default='', blank=True, help_text=_('The description of the collection')) - cover = models.ImageField(upload_to=get_upload_filename, null=True,blank=True, max_length=500, help_text=_('Collection image')) - hidden = models.BooleanField(default=False, help_text=_('Hide the collection'), db_index=True) +class CollectionsQuerySet(models.QuerySet): + """Handle the manager queryset for Collections.""" + + def visible(self): + """Filter results down to visible collections only.""" + return self.filter(hidden=False) def keyword(self, keyword): if not keyword: @@ -1682,7 +1698,34 @@ def keyword(self, keyword): ) +class GrantCollections(SuperModel): + grants = models.ManyToManyField(blank=True, to=Grant, help_text=_('References to grants related to this collection')) + profile = models.ForeignKey('dashboard.Profile', help_text=_('Owner of the collection'), related_name='curator', on_delete=models.CASCADE) + title = models.CharField(max_length=255, help_text=_('Name of the collection')) + description = models.TextField(default='', blank=True, help_text=_('The description of the collection')) + cover = models.ImageField(upload_to=get_upload_filename, null=True,blank=True, max_length=500, help_text=_('Collection image')) + hidden = models.BooleanField(default=False, help_text=_('Hide the collection'), db_index=True) + cache = JSONField(default=dict, blank=True, help_text=_('Easy access to grant info'),) + + objects = CollectionsQuerySet.as_manager() + + def generate_cache(self): + grants = self.grants.all() + + cache = { + 'count': grants.count(), + 'grants': [{ + 'id': grant.id, + 'logo': grant.logo.url if grant.logo and grant.logo.url else f'v2/images/grants/logos/{self.id % 3}.png', + } for grant in grants] + } + + self.cache = cache + self.save() + def to_json_dict(self): + self.generate_cache() + return { 'id': self.id, 'curator': { @@ -1692,8 +1735,8 @@ def to_json_dict(self): }, 'title': self.title, 'description': self.description, - 'cover': self.cover.url if self.cover else '' + 'cover': self.cover.url if self.cover else '', + 'count': self.cache['count'], + 'grants': self.cache['grants'] } - def update_cover(self): - pass diff --git a/app/grants/templates/grants/components/collection.html b/app/grants/templates/grants/components/collection.html new file mode 100644 index 00000000000..8f72aaa1cc4 --- /dev/null +++ b/app/grants/templates/grants/components/collection.html @@ -0,0 +1,76 @@ + {% comment %} + Copyright (C) 2020 Gitcoin Core + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +{% endcomment %} + + + diff --git a/app/grants/templates/grants/index.html b/app/grants/templates/grants/index.html index e68c2b97162..4e2ea3f2cf5 100644 --- a/app/grants/templates/grants/index.html +++ b/app/grants/templates/grants/index.html @@ -22,6 +22,7 @@ {% include 'shared/cards_pic.html' %} + @@ -121,6 +122,7 @@ document.following = {{ following|yesno:'true,false' }}; document.idle_grants = {{ idle_grants|yesno:'true,false' }}; document.show_contributions = {{ show_contributions|yesno:'true,false' }} + document.collection_id = {{ collection_id|default:'null' }} {% include 'shared/activity_scripts.html' %} @@ -128,6 +130,7 @@ + diff --git a/app/grants/templates/grants/shared/landing_grants.html b/app/grants/templates/grants/shared/landing_grants.html index 6f0868cef4e..4fb2960e1ea 100644 --- a/app/grants/templates/grants/shared/landing_grants.html +++ b/app/grants/templates/grants/shared/landing_grants.html @@ -2,6 +2,11 @@
+
+
+ {% include 'grants/components/collection.html' %} +
+
diff --git a/app/grants/templates/grants/shared/landing_subnav.html b/app/grants/templates/grants/shared/landing_subnav.html index 918f7304cbd..3a16caeec14 100644 --- a/app/grants/templates/grants/shared/landing_subnav.html +++ b/app/grants/templates/grants/shared/landing_subnav.html @@ -1,7 +1,10 @@ {% load humanize static i18n %}
-
+
+ +
+
View List | Grid diff --git a/app/grants/templates/grants/shared/sidebar_search.html b/app/grants/templates/grants/shared/sidebar_search.html index 8b343fb66e3..ec540baae5a 100644 --- a/app/grants/templates/grants/shared/sidebar_search.html +++ b/app/grants/templates/grants/shared/sidebar_search.html @@ -43,25 +43,25 @@
{% if profile %} - + Following {% endif %} - + Show Idle Grants {% if profile %} - + My Contributions - + My Grants @@ -105,6 +105,15 @@ +
+
  • + + View Collections NEW + + + My Collections NEW + +

  • {% trans "Meta" as desc %}{{ desc|upper }} diff --git a/app/grants/urls.py b/app/grants/urls.py index c53ad059ad8..9e6e7b9531c 100644 --- a/app/grants/urls.py +++ b/app/grants/urls.py @@ -24,7 +24,7 @@ grant_new, grant_new_whitelabel, grants, grants_addr_as_json, grants_bulk_add, grants_by_grant_type, grants_cart_view, grants_clr, grants_stats_view, invoice, leaderboard, new_matching_partner, profile, quickstart, subscription_cancel, toggle_grant_favorite, verify_grant, zksync_get_interrupt_status, zksync_set_interrupt_status, - save_collection, + save_collection, bulk_grants_for_cart, get_collection, ) app_name = 'grants' @@ -35,6 +35,7 @@ path('flag/', flag, name='grantflag'), path('cards_info', get_grants, name='grant_cards_info'), path('/activity', grant_activity, name='log_activity'), + path('bulk_cart', bulk_grants_for_cart, name='bulk_grants_for_cart'), path('/favorite', toggle_grant_favorite, name='favorite_grant'), path('activity', grant_activity, name='log_activity'), path('/', grant_details, name='details'), @@ -67,5 +68,6 @@ path('/', grants_by_grant_type, name='grants_by_category'), path('v1/api/clr', grants_clr, name='grants_clr'), path('v1/api//verify', verify_grant, name='verify_grant'), - path('v1/api/collections/new', save_collection, name='create_collection') + path('v1/api/collections/new', save_collection, name='create_collection'), + path('v1/api/collections/', get_collection, name='get_collection') ] diff --git a/app/grants/views.py b/app/grants/views.py index e7623f2a9de..92e34f00dcf 100644 --- a/app/grants/views.py +++ b/app/grants/views.py @@ -260,14 +260,51 @@ def grants(request): _type = request.GET.get('type', 'all') return grants_by_grant_type(request, _type) -def get_collections(user, sort, keyword): + +def get_collections(user, keyword, sort='-modified_on', collection_id=None): _collections = GrantCollections.objects.filter(hidden=False) + if collection_id: + _collections = _collections.filter(pk=int(collection_id)) + _collections = _collections.keyword(keyword).order_by(sort, 'pk') _collections = _collections.prefetch_related('grants') return _collections +def bulk_grants_for_cart(request): + grant_type = request.GET.get('type', 'all') + sort = request.GET.get('sort_option', 'weighted_shuffle') + network = request.GET.get('network', 'mainnet') + keyword = request.GET.get('keyword', '') + state = request.GET.get('state', 'active') + category = request.GET.get('category', '') + idle_grants = request.GET.get('idle', '') == 'true' + following = request.GET.get('following', '') != '' + only_contributions = request.GET.get('only_contributions', '') == 'true' + + filters = { + 'request': request, + 'grant_type': grant_type, + 'sort': sort, + 'network': network, + 'keyword': keyword, + 'state': state, + 'category': category, + 'following': following, + 'idle_grants': idle_grants, + 'only_contributions': only_contributions, + 'omit_my_grants': True + } + _grants = build_grants_by_type(**filters) + grants = [] + + for grant in _grants: + grants.append(grant.cart_payload()) + + return JsonResponse({'grants': grants}) + + def get_grants(request): grants = [] collections = [] @@ -275,6 +312,7 @@ def get_grants(request): limit = request.GET.get('limit', 6) page = request.GET.get('page', 1) + collections_page = request.GET.get('collections_page', 1) sort = request.GET.get('sort_option', 'weighted_shuffle') network = request.GET.get('network', 'mainnet') keyword = request.GET.get('keyword', '') @@ -283,6 +321,7 @@ def get_grants(request): idle_grants = request.GET.get('idle', '') == 'true' following = request.GET.get('following', '') != '' only_contributions = request.GET.get('only_contributions', '') == 'true' + collection_id = request.GET.get('collection_id', '') filters = { 'request': request, @@ -298,9 +337,9 @@ def get_grants(request): } if grant_type == 'collections': - _collections = get_collections(user, sort, keyword) + _collections = get_collections(request.user, keyword, collection_id=collection_id) paginator = Paginator(_collections, limit) - collections = paginator.get_page(page) + collections = paginator.get_page(collections_page) else: _grants = build_grants_by_type(**filters) paginator = Paginator(_grants, limit) @@ -350,7 +389,7 @@ def get_grants(request): def build_grants_by_type(request, grant_type='', sort='weighted_shuffle', network='mainnet', keyword='', state='active', - category='', following=False, idle_grants=False, only_contributions=False): + category='', following=False, idle_grants=False, only_contributions=False, omit_my_grants=False): print(" " + str(round(time.time(), 2))) sort_by_clr_pledge_matching_amount = None @@ -361,7 +400,11 @@ def build_grants_by_type(request, grant_type='', sort='weighted_shuffle', networ if 'match_pledge_amount_' in sort: sort_by_clr_pledge_matching_amount = int(sort.split('amount_')[1]) - if grant_type == 'me' and profile: + if omit_my_grants and profile: + grants_id = list(profile.grant_teams.all().values_list('pk', flat=True)) + \ + list(profile.grant_admin.all().values_list('pk', flat=True)) + _grants = _grants.exclude(id__in=grants_id) + elif grant_type == 'me' and profile: grants_id = list(profile.grant_teams.all().values_list('pk', flat=True)) + \ list(profile.grant_admin.all().values_list('pk', flat=True)) _grants = _grants.filter(id__in=grants_id) @@ -483,6 +526,7 @@ def grants_by_grant_type(request, grant_type): following = request.GET.get('following', '') == 'true' idle_grants = request.GET.get('idle', '') == 'true' only_contributions = request.GET.get('only_contributions', '') == 'true' + collection_id = request.GET.get('collection_id', '') if keyword: category = '' @@ -629,7 +673,8 @@ def grants_by_grant_type(request, grant_type): 'grants_following': grants_following, 'following': following, 'idle_grants': idle_grants, - 'only_contributions': only_contributions + 'only_contributions': only_contributions, + 'collection_id': collection_id } # log this search, it might be useful for matching purposes down the line @@ -1840,6 +1885,7 @@ def save_collection(request): collection = GrantCollections.objects.create(**kwargs) collection.grants.set(grant_ids) + collection.generate_cache() return JsonResponse({ 'ok': True, @@ -1853,7 +1899,7 @@ def save_collection(request): def get_collection(request, collection_id): collection = GrantCollections.objects.get(pk=collection_id) - grants = [grant.repr(request.user) for grant in collection.grants] + grants = [grant.cart_payload() for grant in collection.grants.all()] return JsonResponse({ 'id': collection.id, From 20cd39c7e32a0b36eb34c0616e367f0c97798819 Mon Sep 17 00:00:00 2001 From: Miguel Angel G Date: Wed, 16 Sep 2020 15:42:36 -0500 Subject: [PATCH 03/13] Fix texts --- app/assets/v2/js/grants/index.js | 1 + app/economy/models.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/assets/v2/js/grants/index.js b/app/assets/v2/js/grants/index.js index ca62146a90c..ab992d1087d 100644 --- a/app/assets/v2/js/grants/index.js +++ b/app/assets/v2/js/grants/index.js @@ -203,6 +203,7 @@ if (document.getElementById('grants-showcase')) { } if (self.current_type === 'collections' && this.collection_id) { query_elements['collection_id'] = this.collection_id; + this.collections = []; } return $.param(query_elements); diff --git a/app/economy/models.py b/app/economy/models.py index 9140af6eb08..8c002e3a127 100644 --- a/app/economy/models.py +++ b/app/economy/models.py @@ -66,12 +66,12 @@ def default(self, obj): def get_time(): """Get the local time.""" - return localtime(timezone.now()) + return (timezone.now()) def get_0_time(): """Get the local time.""" - return localtime(timezone.datetime(1970, 1, 1).replace(tzinfo=pytz.utc)) + return (timezone.datetime(1970, 1, 1).replace(tzinfo=pytz.utc)) class SuperModel(models.Model): From 6ecba4c97d063b22916c1a7a08524f622671ab1f Mon Sep 17 00:00:00 2001 From: Miguel Angel G Date: Wed, 16 Sep 2020 16:05:14 -0500 Subject: [PATCH 04/13] FIx integration error --- app/assets/v2/js/grants/index.js | 6 ++-- app/economy/models.py | 4 +-- .../migrations/0084_grantcollections.py | 36 +++++++++++++++++++ app/grants/models.py | 5 +-- app/grants/templates/grants/cart-vue.html | 2 +- .../grants/shared/sidebar_search.html | 2 ++ app/grants/views.py | 4 +-- 7 files changed, 50 insertions(+), 9 deletions(-) create mode 100644 app/grants/migrations/0084_grantcollections.py diff --git a/app/assets/v2/js/grants/index.js b/app/assets/v2/js/grants/index.js index 25ce0c5f688..c5f85859824 100644 --- a/app/assets/v2/js/grants/index.js +++ b/app/assets/v2/js/grants/index.js @@ -220,8 +220,10 @@ if (document.getElementById('grants-showcase')) { if (this.network !== 'mainnet') { query_elements['network'] = this.network; } - if (self.current_type === 'collections' && this.collection_id) { - query_elements['collection_id'] = this.collection_id; + if (this.current_type === 'collections') { + if (this.collection_id) { + query_elements['collection_id'] = this.collection_id; + } this.collections = []; } diff --git a/app/economy/models.py b/app/economy/models.py index 8c002e3a127..9140af6eb08 100644 --- a/app/economy/models.py +++ b/app/economy/models.py @@ -66,12 +66,12 @@ def default(self, obj): def get_time(): """Get the local time.""" - return (timezone.now()) + return localtime(timezone.now()) def get_0_time(): """Get the local time.""" - return (timezone.datetime(1970, 1, 1).replace(tzinfo=pytz.utc)) + return localtime(timezone.datetime(1970, 1, 1).replace(tzinfo=pytz.utc)) class SuperModel(models.Model): diff --git a/app/grants/migrations/0084_grantcollections.py b/app/grants/migrations/0084_grantcollections.py new file mode 100644 index 00000000000..ae0bc45ccbc --- /dev/null +++ b/app/grants/migrations/0084_grantcollections.py @@ -0,0 +1,36 @@ +# Generated by Django 2.2.4 on 2020-09-16 20:59 + +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion +import economy.models +import grants.utils + + +class Migration(migrations.Migration): + + dependencies = [ + ('dashboard', '0148_add_brightid_status'), + ('grants', '0083_matchpledge_clr_round_num'), + ] + + operations = [ + migrations.CreateModel( + name='GrantCollections', + 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)), + ('title', models.CharField(help_text='Name of the collection', max_length=255)), + ('description', models.TextField(blank=True, default='', help_text='The description of the collection')), + ('cover', models.ImageField(blank=True, help_text='Collection image', max_length=500, null=True, upload_to=grants.utils.get_upload_filename)), + ('hidden', models.BooleanField(db_index=True, default=False, help_text='Hide the collection')), + ('cache', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict, help_text='Easy access to grant info')), + ('grants', models.ManyToManyField(blank=True, help_text='References to grants related to this collection', to='grants.Grant')), + ('profile', models.ForeignKey(help_text='Owner of the collection', on_delete=django.db.models.deletion.CASCADE, related_name='curator', to='dashboard.Profile')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/app/grants/models.py b/app/grants/models.py index 54ac6069307..2c4ab281f56 100644 --- a/app/grants/models.py +++ b/app/grants/models.py @@ -28,6 +28,7 @@ from django.db.models import Q from django.db.models.signals import post_save, pre_save from django.dispatch import receiver +from django.templatetags.static import static from django.urls import reverse from django.utils import timezone from django.utils.timezone import localtime @@ -576,10 +577,10 @@ def cart_payload(self): 'is_clr_eligible': self.is_clr_eligible } - def repr(self, user): + def repr(self, user, build_absolute_uri): return { 'id': self.id, - 'logo_url': self.logo.url if self.logo and self.logo.url else f'v2/images/grants/logos/{self.id % 3}.png', + 'logo_url': self.logo.url if self.logo and self.logo.url else build_absolute_uri(static(f'v2/images/grants/logos/{self.id % 3}.png')), 'details_url': reverse('grants:details', args=(self.id, self.slug)), 'title': self.title, 'description': self.description, diff --git a/app/grants/templates/grants/cart-vue.html b/app/grants/templates/grants/cart-vue.html index a64a4103d37..809d60529b7 100644 --- a/app/grants/templates/grants/cart-vue.html +++ b/app/grants/templates/grants/cart-vue.html @@ -443,7 +443,7 @@

    diff --git a/app/grants/templates/grants/shared/sidebar_search.html b/app/grants/templates/grants/shared/sidebar_search.html index 97c1f368c90..de2693daead 100644 --- a/app/grants/templates/grants/shared/sidebar_search.html +++ b/app/grants/templates/grants/shared/sidebar_search.html @@ -126,6 +126,8 @@ View Collections NEW + +
  • My Collections NEW diff --git a/app/grants/views.py b/app/grants/views.py index fe6c759427e..b0820947f42 100644 --- a/app/grants/views.py +++ b/app/grants/views.py @@ -308,7 +308,7 @@ def bulk_grants_for_cart(request): return JsonResponse({'grants': grants}) - + def clr_grants(request, round_num): """CLR grants explorer.""" @@ -392,7 +392,7 @@ def get_grants(request): grants_array = [] for grant in grants: - grant_json = grant.repr(request.user) + grant_json = grant.repr(request.user, request.build_absolute_uri) grants_array.append(grant_json) return JsonResponse({ From 0881e897208b2e310d0e9eddf4326c27d2d6af1c Mon Sep 17 00:00:00 2001 From: Miguel Angel G Date: Thu, 17 Sep 2020 20:09:56 -0500 Subject: [PATCH 05/13] Collection search and filtering --- app/assets/v2/css/grants/collection.css | 10 +- app/assets/v2/js/grants/index.js | 83 +++++++++----- .../grants/components/collection.html | 103 +++++++++--------- .../grants/shared/landing_grants.html | 10 +- .../grants/shared/landing_subnav.html | 48 ++++---- app/grants/views.py | 42 +++++-- 6 files changed, 190 insertions(+), 106 deletions(-) diff --git a/app/assets/v2/css/grants/collection.css b/app/assets/v2/css/grants/collection.css index 6969f6cabad..7fe7555ad8c 100644 --- a/app/assets/v2/css/grants/collection.css +++ b/app/assets/v2/css/grants/collection.css @@ -51,7 +51,7 @@ background: linear-gradient(0deg, #FFFFFF, #FFFFFF), linear-gradient(0deg, #FFFFFF, #FFFFFF), linear-gradient(0deg, #FFFFFF, #FFFFFF), linear-gradient(0deg, #FFFFFF, #FFFFFF), #FFFFFF; border-radius: 2px; height: 75px; - width: 154px; + width: 96%; margin: 2px; } @@ -109,3 +109,11 @@ color: white; text-decoration: underline; } + + +.vertica-scroll { + flex-direction: row; + overflow-x: auto; + flex-wrap: nowrap; + margin-bottom: 25px; +} diff --git a/app/assets/v2/js/grants/index.js b/app/assets/v2/js/grants/index.js index c5f85859824..a811ea0f5aa 100644 --- a/app/assets/v2/js/grants/index.js +++ b/app/assets/v2/js/grants/index.js @@ -147,6 +147,7 @@ if (document.getElementById('grants-showcase')) { cart_lock: false, collection_id: document.collection_id, round_num: document.round_num, + activeCollection: null, grantsNumPages, grantsHasNext, numGrants @@ -157,11 +158,18 @@ if (document.getElementById('grants-showcase')) { localStorage.setItem('grants_view', mode); this.view = mode; }, - setCurrentType: function(currentType, q) { - let vm = this; - + setCurrentType: function(currentType) { this.current_type = currentType; + if (this.current_type === 'collections') { + this.clearSingleCollection(); + } + + this.updateURI(); + }, + updateURI: function() { + let vm = this; + const q = vm.getQueryParams(); if (vm.round_num) { let uri = `/grants/clr/${vm.round_num}/`; @@ -224,7 +232,6 @@ if (document.getElementById('grants-showcase')) { if (this.collection_id) { query_elements['collection_id'] = this.collection_id; } - this.collections = []; } return $.param(query_elements); @@ -267,11 +274,27 @@ if (document.getElementById('grants-showcase')) { } this.page = 1; - const q = this.getQueryParams(); - - this.setCurrentType(this.current_type, q); + this.setCurrentType(this.current_type); this.fetchGrants(this.page); }, + clearSingleCollection: function() { + this.grants = []; + this.collections = []; + this.collection_id = null; + this.activeCollection = null; + this.page = 1; + this.updateURI(); + this.fetchGrants(); + }, + showSingleCollection: function(collectionId) { + this.collection_id = collectionId; + this.collections = []; + this.keyword = ''; + this.grants = []; + this.current_type = 'collections'; + this.updateURI(); + this.fetchGrants(); + }, fetchGrants: async function(page, append_mode) { let vm = this; @@ -322,29 +345,35 @@ if (document.getElementById('grants-showcase')) { vm.grants.push(item); }); - getGrants.collections.forEach(function(item) { - vm.collections.push(item); - }); + if (this.collection_id) { + if (getGrants.collections.length > 0) { + this.activeCollection = getGrants.collections[0]; + } + } else { + if (this.current_type === 'collections') { + getGrants.collections.forEach(function(item) { + vm.collections.push(item); + }); + } else { + vm.collections = getGrants.collections; + } - vm.credentials = getGrants.credentials; - vm.grant_types = getGrants.grant_types; - vm.contributions = getGrants.contributions; - vm.grantsNumPages = getGrants.num_pages; - vm.grantsHasNext = getGrants.has_next; - vm.numGrants = getGrants.count; - vm.lock = false; + vm.credentials = getGrants.credentials; + vm.grant_types = getGrants.grant_types; + vm.contributions = getGrants.contributions; + vm.grantsNumPages = getGrants.num_pages; + vm.grantsHasNext = getGrants.has_next; + vm.numGrants = getGrants.count; - if (this.current_type === 'collections') { if (vm.grantsHasNext) { - vm.collectionsPage = ++vm.collectionsPage; + vm.page = ++vm.page; } else { - vm.collectionsPage = 1; + vm.page = 1; } - } else if (vm.grantsHasNext) { - vm.page = ++vm.page; - } else { - vm.page = 1; } + + vm.lock = false; + return vm.grants; }, scrollEnd: async function(event) { @@ -355,6 +384,10 @@ if (document.getElementById('grants-showcase')) { const pageHeight = document.documentElement.scrollHeight - 500; const bottomOfPage = visible + scrollY >= pageHeight; + if (this.collection_id) { + return; + } + if (bottomOfPage || pageHeight < visible) { if (vm.grantsHasNext) { vm.fetchGrants(vm.page, true); @@ -401,7 +434,7 @@ if (document.getElementById('grants-showcase')) { showSideCart(); _alert(`Congratulations, ${getGrants.grants.length} ${getGrants.grants.length > 1 ? 'grant were' : 'grants was'} added to your cart!`, 'success'); this.cart_lock = false; - }, + } }, beforeMount() { window.addEventListener('scroll', () => { diff --git a/app/grants/templates/grants/components/collection.html b/app/grants/templates/grants/components/collection.html index 8f72aaa1cc4..dd57e140abe 100644 --- a/app/grants/templates/grants/components/collection.html +++ b/app/grants/templates/grants/components/collection.html @@ -14,63 +14,66 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . {% endcomment %} - - diff --git a/app/grants/templates/grants/shared/landing_grants.html b/app/grants/templates/grants/shared/landing_grants.html index 4fb2960e1ea..4a0ed759e93 100644 --- a/app/grants/templates/grants/shared/landing_grants.html +++ b/app/grants/templates/grants/shared/landing_grants.html @@ -2,11 +2,19 @@
    -
    +
    {% include 'grants/components/collection.html' %}
    +
    +
    + {% include 'grants/components/collection.html' %} +
    +
    +
    +

    Grants in this collection:

    +
    diff --git a/app/grants/templates/grants/shared/landing_subnav.html b/app/grants/templates/grants/shared/landing_subnav.html index 35d9ef64cff..8bea97ee151 100644 --- a/app/grants/templates/grants/shared/landing_subnav.html +++ b/app/grants/templates/grants/shared/landing_subnav.html @@ -1,31 +1,37 @@ {% load humanize static i18n %}
    -
    -
    - +
    + -
    + +
    + +
    +
    View List | Grid
    - {% trans "Sort by" %} - +
    + {% trans "Sort by" %} + +
    diff --git a/app/grants/views.py b/app/grants/views.py index b0820947f42..48297be7840 100644 --- a/app/grants/views.py +++ b/app/grants/views.py @@ -265,11 +265,25 @@ def grants(request): return grants_by_grant_type(request, _type) -def get_collections(user, keyword, sort='-modified_on', collection_id=None): +def get_collections(user, keyword, sort='-modified_on', collection_id=None, following=None, + idle_grants=None, only_contributions=None): + three_months_ago = timezone.now() - datetime.timedelta(days=90) + _collections = GrantCollections.objects.filter(hidden=False) if collection_id: _collections = _collections.filter(pk=int(collection_id)) + if idle_grants: + _collections = _collections.filter(grants__last_update__gt=three_months_ago) + + if only_contributions: + contributions = user.profile.grant_contributor.filter(subscription_contribution__success=True).values('grant_id') + _collections = _collections.filter(grants__in=Subquery(contributions)) + + if following and user.is_authenticated: + favorite_grants = Favorite.grants().filter(user=user).values('grant_id') + _collections = _collections.filter(grants__in=Subquery(favorite_grants)) + _collections = _collections.keyword(keyword).order_by(sort, 'pk') _collections = _collections.prefetch_related('grants') @@ -323,7 +337,7 @@ def clr_grants(request, round_num): def get_grants(request): grants = [] - collections = [] + paginator = None grant_type = request.GET.get('type', 'all') limit = request.GET.get('limit', 6) @@ -362,11 +376,23 @@ def get_grants(request): } if grant_type == 'collections': - _collections = get_collections(request.user, keyword, collection_id=collection_id) - paginator = Paginator(_collections, limit) - collections = paginator.get_page(collections_page) + _collections = get_collections(request.user, keyword, collection_id=collection_id, + following=following, idle_grants=idle_grants, + only_contributions=only_contributions) + + if collection_id: + collection = _collections.first() + if collection: + grants = collection.grants.all() + collections = _collections + else: + paginator = Paginator(_collections, limit) + collections = paginator.get_page(collections_page) else: _grants = build_grants_by_type(**filters) + + collections = GrantCollections.objects.filter(grants__in=Subquery(_grants.values('id'))).distinct()[:12] + paginator = Paginator(_grants, limit) grants = paginator.get_page(page) @@ -406,9 +432,9 @@ def get_grants(request): 'is_authenticated': request.user.is_authenticated }, 'contributions': contributions_by_grant, - 'has_next': paginator.page(page).has_next(), - 'count': paginator.count, - 'num_pages': paginator.num_pages, + 'has_next': paginator.page(page).has_next() if paginator else False, + 'count': paginator.count if paginator else 0, + 'num_pages': paginator.num_pages if paginator else 0, }) From 1d1c9c6ebf233e20c38e25f25e6f2c90f60f60f3 Mon Sep 17 00:00:00 2001 From: Miguel Angel G Date: Thu, 17 Sep 2020 20:28:27 -0500 Subject: [PATCH 06/13] fix migration error --- .../{0084_grantcollections.py => 0085_grantcollections.py} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename app/grants/migrations/{0084_grantcollections.py => 0085_grantcollections.py} (94%) diff --git a/app/grants/migrations/0084_grantcollections.py b/app/grants/migrations/0085_grantcollections.py similarity index 94% rename from app/grants/migrations/0084_grantcollections.py rename to app/grants/migrations/0085_grantcollections.py index ae0bc45ccbc..aecc5085711 100644 --- a/app/grants/migrations/0084_grantcollections.py +++ b/app/grants/migrations/0085_grantcollections.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.4 on 2020-09-16 20:59 +# Generated by Django 2.2.4 on 2020-09-18 01:28 import django.contrib.postgres.fields.jsonb from django.db import migrations, models @@ -11,7 +11,7 @@ class Migration(migrations.Migration): dependencies = [ ('dashboard', '0148_add_brightid_status'), - ('grants', '0083_matchpledge_clr_round_num'), + ('grants', '0084_auto_20200917_1631'), ] operations = [ From 334cdfcd59799f56e091e7c4ddd536c096f1a87a Mon Sep 17 00:00:00 2001 From: Miguel Angel G Date: Thu, 17 Sep 2020 21:18:39 -0500 Subject: [PATCH 07/13] Fix pagination --- app/grants/templates/grants/components/collection.html | 2 +- app/grants/views.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/grants/templates/grants/components/collection.html b/app/grants/templates/grants/components/collection.html index dd57e140abe..6ac9a21ab8b 100644 --- a/app/grants/templates/grants/components/collection.html +++ b/app/grants/templates/grants/components/collection.html @@ -39,7 +39,7 @@

    - [[ collection.title.slice(0,60) ]][[ collection.title.length > 60 ? '...' : '']]9 + [[ collection.title.slice(0,60) ]][[ collection.title.length > 60 ? '...' : '']]

    diff --git a/app/grants/views.py b/app/grants/views.py index 4d6705b902e..e5fb4a926ae 100644 --- a/app/grants/views.py +++ b/app/grants/views.py @@ -388,7 +388,7 @@ def get_grants(request): collections = _collections else: paginator = Paginator(_collections, limit) - collections = paginator.get_page(collections_page) + collections = paginator.get_page(page) else: _grants = build_grants_by_type(**filters) From 2470b6a2004abc32d010a19701599bef84210c44 Mon Sep 17 00:00:00 2001 From: Miguel Angel G Date: Tue, 22 Sep 2020 02:03:50 -0500 Subject: [PATCH 08/13] Add add/delete grants and pagination for collection's grants --- app/assets/v2/js/grants/components.js | 25 +++++- app/assets/v2/js/grants/index.js | 46 ++++++---- app/grants/admin.py | 4 +- .../migrations/0086_auto_20200922_0234.py | 24 ++++++ app/grants/models.py | 3 +- app/grants/templates/grants/cart-vue.html | 9 +- .../templates/grants/components/card.html | 40 +++++---- app/grants/templates/grants/index.html | 7 +- .../grants/shared/sidebar_search.html | 13 ++- app/grants/urls.py | 8 +- app/grants/views.py | 86 +++++++++++++++++-- 11 files changed, 212 insertions(+), 53 deletions(-) create mode 100644 app/grants/migrations/0086_auto_20200922_0234.py diff --git a/app/assets/v2/js/grants/components.js b/app/assets/v2/js/grants/components.js index b62056a63a2..957b456fb1a 100644 --- a/app/assets/v2/js/grants/components.js +++ b/app/assets/v2/js/grants/components.js @@ -1,6 +1,13 @@ Vue.component('grant-card', { delimiters: [ '[[', ']]' ], - props: [ 'grant', 'cred', 'token', 'view', 'short', 'show_contributions', 'contributions', 'toggle_following' ], + props: [ 'grant', 'cred', 'token', 'view', 'short', 'show_contributions', + 'contributions', 'toggle_following', 'collection' + ], + data: function() { + return { + collections: document.collections + }; + }, methods: { get_clr_prediction: function(indexA, indexB) { if (this.grant.clr_prediction_curve && this.grant.clr_prediction_curve.length) { @@ -23,6 +30,22 @@ Vue.component('grant-card', { } return true; + }, + addToCart: async function(grant) { + const grantCartPayloadURL = `v1/api/${grant.id}/cart_payload`; + const response = await fetchData(grantCartPayloadURL, 'GET'); + + CartData.addToCart(response.grant); + + showSideCart(); + }, + addToCollection: async function({collection, grant}) { + const collectionAddGrantURL = `v1/api/collections/${collection.id}/grants/add`; + const response = await fetchData(collectionAddGrantURL, 'POST', { + 'grant': grant.id + }); + + _alert('Grant added successfully', 'success', 1000); } } }); diff --git a/app/assets/v2/js/grants/index.js b/app/assets/v2/js/grants/index.js index a811ea0f5aa..ef331d86075 100644 --- a/app/assets/v2/js/grants/index.js +++ b/app/assets/v2/js/grants/index.js @@ -55,7 +55,7 @@ $(document).ready(() => { Vue.component('grant-sidebar', { props: [ 'filter_grants', 'grant_types', 'type', 'selected_category', 'keyword', 'following', 'set_type', - 'idle_grants', 'show_contributions', 'query_params', 'round_num' + 'idle_grants', 'show_contributions', 'query_params', 'round_num', 'featured' ], data: function() { return { @@ -133,6 +133,7 @@ if (document.getElementById('grants-showcase')) { current_type: document.current_type, idle_grants: document.idle_grants, following: document.following, + featured: document.featured, state: 'active', category: document.selected_category, credentials: false, @@ -222,6 +223,9 @@ if (document.getElementById('grants-showcase')) { if (this.show_contributions) { query_elements['only_contributions'] = this.show_contributions; } + if (this.featured) { + query_elements['featured'] = this.featured; + } if (this.sort !== 'weighted_shuffle') { query_elements['sort'] = this.sort; } @@ -265,6 +269,9 @@ if (document.getElementById('grants-showcase')) { if (filters.show_contributions !== null && filters.show_contributions !== undefined) { this.show_contributions = filters.show_contributions; } + if (filters.featured !== null && filters.featured !== undefined) { + this.featured = filters.featured; + } if (filters.network !== null && filters.network !== undefined) { this.network = filters.network; } @@ -291,6 +298,7 @@ if (document.getElementById('grants-showcase')) { this.collections = []; this.keyword = ''; this.grants = []; + this.page = 1; this.current_type = 'collections'; this.updateURI(); this.fetchGrants(); @@ -327,6 +335,10 @@ if (document.getElementById('grants-showcase')) { base_params['only_contributions'] = this.show_contributions; } + if (this.featured) { + base_params['featured'] = this.featured; + } + if (this.current_type === 'collections' && this.collection_id) { base_params['collection_id'] = this.collection_id; } @@ -361,15 +373,16 @@ if (document.getElementById('grants-showcase')) { vm.credentials = getGrants.credentials; vm.grant_types = getGrants.grant_types; vm.contributions = getGrants.contributions; - vm.grantsNumPages = getGrants.num_pages; - vm.grantsHasNext = getGrants.has_next; - vm.numGrants = getGrants.count; + } - if (vm.grantsHasNext) { - vm.page = ++vm.page; - } else { - vm.page = 1; - } + vm.grantsNumPages = getGrants.num_pages; + vm.grantsHasNext = getGrants.has_next; + vm.numGrants = getGrants.count; + + if (vm.grantsHasNext) { + vm.page = ++vm.page; + } else { + vm.page = 1; } vm.lock = false; @@ -384,10 +397,6 @@ if (document.getElementById('grants-showcase')) { const pageHeight = document.documentElement.scrollHeight - 500; const bottomOfPage = visible + scrollY >= pageHeight; - if (this.collection_id) { - return; - } - if (bottomOfPage || pageHeight < visible) { if (vm.grantsHasNext) { vm.fetchGrants(vm.page, true); @@ -428,12 +437,19 @@ if (document.getElementById('grants-showcase')) { (getGrants.grants || []).forEach((grant) => { - CartData.addToCart(grant); + CartData.addToCart(grant, true); }); showSideCart(); - _alert(`Congratulations, ${getGrants.grants.length} ${getGrants.grants.length > 1 ? 'grant were' : 'grants was'} added to your cart!`, 'success'); + _alert(`Congratulations, ${getGrants.grants.length} ${getGrants.grants.length > 1 ? 'grants were' : 'grants was'} added to your cart!`, 'success'); this.cart_lock = false; + }, + removeCollection: async function({collection, grant, event}) { + const getGrants = await fetchData(`v1/api/collections/${collection.id}/grants/remove`, 'POST', { + 'grant': grant.id + }); + + this.grants = getGrants.grants; } }, beforeMount() { diff --git a/app/grants/admin.py b/app/grants/admin.py index 1d8c07f258a..cb173cc2908 100644 --- a/app/grants/admin.py +++ b/app/grants/admin.py @@ -360,8 +360,8 @@ class GrantCLRAdmin(admin.ModelAdmin): class GrantCollectionsAdmin(admin.ModelAdmin): - list_display = ['pk', 'title', 'description', 'hidden', 'cache'] - raw_id_fields = ['profile', 'grants'] + list_display = ['pk', 'title', 'description', 'hidden', 'cache', 'featured'] + raw_id_fields = ['profile', 'grants', 'curators'] admin.site.register(PhantomFunding, PhantomFundingAdmin) diff --git a/app/grants/migrations/0086_auto_20200922_0234.py b/app/grants/migrations/0086_auto_20200922_0234.py new file mode 100644 index 00000000000..f8841275cc7 --- /dev/null +++ b/app/grants/migrations/0086_auto_20200922_0234.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.4 on 2020-09-22 02:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dashboard', '0148_add_brightid_status'), + ('grants', '0085_grantcollections'), + ] + + operations = [ + migrations.AddField( + model_name='grantcollections', + name='curators', + field=models.ManyToManyField(blank=True, help_text='List of allowed curators', to='dashboard.Profile'), + ), + migrations.AddField( + model_name='grantcollections', + name='featured', + field=models.BooleanField(default=False, help_text='Show grant as featured'), + ), + ] diff --git a/app/grants/models.py b/app/grants/models.py index 5973b717284..073b59f6f98 100644 --- a/app/grants/models.py +++ b/app/grants/models.py @@ -1718,8 +1718,9 @@ class GrantCollections(SuperModel): cover = models.ImageField(upload_to=get_upload_filename, null=True,blank=True, max_length=500, help_text=_('Collection image')) hidden = models.BooleanField(default=False, help_text=_('Hide the collection'), db_index=True) cache = JSONField(default=dict, blank=True, help_text=_('Easy access to grant info'),) - + featured = models.BooleanField(default=False, help_text=_('Show grant as featured')) objects = CollectionsQuerySet.as_manager() + curators = models.ManyToManyField(blank=True, to='dashboard.Profile', help_text=_('List of allowed curators')) def generate_cache(self): grants = self.grants.all() diff --git a/app/grants/templates/grants/cart-vue.html b/app/grants/templates/grants/cart-vue.html index b3ee0dec746..2b687c496ca 100644 --- a/app/grants/templates/grants/cart-vue.html +++ b/app/grants/templates/grants/cart-vue.html @@ -76,6 +76,10 @@

    Share cart

    +
    @@ -441,11 +445,6 @@

    - -

    diff --git a/app/grants/templates/grants/components/card.html b/app/grants/templates/grants/components/card.html index 960ecf51a58..0d5924bc245 100644 --- a/app/grants/templates/grants/components/card.html +++ b/app/grants/templates/grants/components/card.html @@ -16,14 +16,15 @@ {% endcomment %} {% load i18n static humanize grants_extra %} + :contributions='contributions' :show_contributions="show_contributions" :collection="activeCollection" + v-on:collection:remove='removeCollection'>
  • -
    -
    - {% include 'grants/components/hidden_inputs.html' %} - -
    - + +
    @@ -210,12 +217,13 @@

    -
    - {% include 'grants/components/hidden_inputs.html' %} - -
    + + +