diff --git a/app/app/settings.py b/app/app/settings.py index acf01cce86d..e61eccab697 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -204,7 +204,7 @@ # https://docs.djangoproject.com/en/1.11/ref/settings/#databases DATABASES = { 'default': env.db() - } +} if ENV in ['prod']: DATABASES = { 'default': env.db(), diff --git a/app/dashboard/migrations/0189_auto_20211001_1905.py b/app/dashboard/migrations/0189_auto_20211001_1905.py new file mode 100644 index 00000000000..5ed30deb62b --- /dev/null +++ b/app/dashboard/migrations/0189_auto_20211001_1905.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.24 on 2021-10-01 19:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dashboard', '0188_remove_profile_tribe_priority'), + ] + + operations = [ + migrations.AddConstraint( + model_name='portfolioitem', + constraint=models.UniqueConstraint(fields=('profile', 'title', 'link'), name='unique_title_link_per_profile'), + ), + ] diff --git a/app/dashboard/models.py b/app/dashboard/models.py index 06f9a8273d3..57b41f5eb4a 100644 --- a/app/dashboard/models.py +++ b/app/dashboard/models.py @@ -38,7 +38,7 @@ from django.contrib.postgres.fields import ArrayField, JSONField from django.core.validators import MaxValueValidator, MinValueValidator from django.db import connection, models -from django.db.models import Count, F, Q, Subquery, Sum +from django.db.models import Count, F, Q, Subquery, Sum, UniqueConstraint from django.db.models.signals import m2m_changed, post_delete, post_save, pre_save from django.dispatch import receiver from django.forms.models import model_to_dict @@ -5523,6 +5523,11 @@ class PortfolioItem(SuperModel): def __str__(self): return f"{self.title} by {self.profile.handle}" + class Meta: + constraints = [ + UniqueConstraint(fields=['profile', 'title', 'link'], name='unique_title_link_per_profile') + ] + class ProfileStatHistory(SuperModel): """ProfileStatHistory - generalizable model for tracking history of a profiles info""" diff --git a/app/dashboard/tests/views/__init__.py b/app/dashboard/tests/views/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/app/dashboard/tests/views/test_profile_projects.py b/app/dashboard/tests/views/test_profile_projects.py new file mode 100644 index 00000000000..c49b572466e --- /dev/null +++ b/app/dashboard/tests/views/test_profile_projects.py @@ -0,0 +1,67 @@ +from django.test import Client + +import pytest +from dashboard.models import PortfolioItem +from dashboard.tests.factories.profile_factory import ProfileFactory + + +class TestProfileTabProjectCreation: + def test_project_creation_fails_when_url_does_not_start_with_http(self, django_user_model): + user = django_user_model.objects.create(username="gitcoin", password="password123") + project_data = dict(project_title="My New Project", URL="gitcoin.co", tags="") + ProfileFactory(user=user, handle="gitcoin", hide_profile=False) + + client = Client(HTTP_USER_AGENT='chrome') + client.force_login(user) + response = client.post('/gitcoin/portfolio', project_data) + messages = [m.message for m in response.context['messages']] + + assert response.status_code == 200 + assert "Invalid link." in messages + assert "Portfolio Item added." not in messages + + def test_project_creation_fails_when_not_logged_in(self, django_user_model): + user = django_user_model.objects.create(username="gitcoin", password="password123") + project_data = dict(project_title="My New Project", URL="https://gitcoin.co", tags="") + ProfileFactory(user=user, handle="gitcoin", hide_profile=False) + + client = Client(HTTP_USER_AGENT='chrome') + response = client.post('/gitcoin/portfolio', project_data) + messages = [m.message for m in response.context['messages']] + + assert response.status_code == 200 + assert "Not Authorized" in messages + assert "Portfolio Item added." not in messages + + def test_project_created(self, django_user_model): + user = django_user_model.objects.create(username="gitcoin", password="password123") + project_data = dict(project_title="My New Project", URL="http://gitcoin.co", tags="") + ProfileFactory(user=user, handle="gitcoin", hide_profile=False) + + client = Client(HTTP_USER_AGENT='chrome') + client.force_login(user) + response = client.post('/gitcoin/portfolio', project_data) + messages = [m.message for m in response.context['messages']] + + assert response.status_code == 200 + assert "Portfolio Item added." in messages + + @pytest.mark.django_db(transaction=True) + def test_project_creation_fails_when_project_already_exists(self, django_user_model): + user = django_user_model.objects.create(username="gitcoin", password="password123") + project_data = dict(project_title="My New Project", URL="http://gitcoin.co", tags="") + profile = ProfileFactory(user=user, handle="gitcoin", hide_profile=False) + PortfolioItem.objects.create( + profile=profile, + title=project_data['project_title'], + link=project_data['URL'], + tags=[] + ) + + client = Client(HTTP_USER_AGENT='chrome') + client.force_login(user) + response = client.post('/gitcoin/portfolio', project_data) + messages = [m.message for m in response.context['messages']] + + assert response.status_code == 200 + assert "Portfolio Already Exists." in messages diff --git a/app/dashboard/views.py b/app/dashboard/views.py index 05b9fe7c01a..aed07da8d4d 100644 --- a/app/dashboard/views.py +++ b/app/dashboard/views.py @@ -41,6 +41,7 @@ from django.contrib.staticfiles.storage import staticfiles_storage from django.core.exceptions import ObjectDoesNotExist, PermissionDenied, ValidationError from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator +from django.db.utils import IntegrityError from django.db.models import Count, Q, Sum from django.forms import URLField from django.http import Http404, HttpResponse, JsonResponse @@ -2758,18 +2759,19 @@ def get_profile_tab(request, profile, tab, prev_context): if title: if request.POST.get('URL')[0:4] != "http": messages.error(request, 'Invalid link.') - elif not request.POST.get('URL')[0:4]: - messages.error(request, 'Please enter some tags.') elif not request.user.is_authenticated or request.user.profile.pk != profile.pk: messages.error(request, 'Not Authorized') else: - PortfolioItem.objects.create( - profile=request.user.profile, - title=title, - link=request.POST.get('URL'), - tags=request.POST.get('tags').split(','), - ) - messages.info(request, 'Portfolio Item added.') + try: + PortfolioItem.objects.create( + profile=request.user.profile, + title=title, + link=request.POST.get('URL'), + tags=request.POST.get('tags').split(','), + ) + messages.info(request, 'Portfolio Item added.') + except IntegrityError: + messages.error(request, 'Portfolio Already Exists.') if pk: # delete portfolio item if not request.user.is_authenticated or request.user.profile.pk != profile.pk: @@ -3721,7 +3723,7 @@ def request_verify_facebook(request, handle): facebook = connect_facebook() authorization_url, state = facebook.authorization_url(settings.FACEBOOK_AUTH_BASE_URL) - + return redirect(authorization_url) diff --git a/package.json b/package.json index 01a2e25d478..05bab0b1964 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "dependencies": { "@highlightjs/cdn-assets": "^10.0.0", "@joeattardi/emoji-button": "^3.1.1", + "@popperjs/core": "^2.10.2", "bootstrap": "4.6.0", "bootstrap-vue": "2.18.0", "daterangepicker": "3.0.5", diff --git a/yarn.lock b/yarn.lock index 83b8ff65120..3f1bccd228a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1659,6 +1659,11 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.10.1.tgz#728ecd95ab207aab8a9a4e421f0422db329232be" integrity sha512-HnUhk1Sy9IuKrxEMdIRCxpIqPw6BFsbYSEUO9p/hNw5sMld/+3OLMWQP80F8/db9qsv3qUjs7ZR5bS/R+iinXw== +"@popperjs/core@^2.10.2": + version "2.10.2" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.10.2.tgz#0798c03351f0dea1a5a4cabddf26a55a7cbee590" + integrity sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ== + "@semantic-release/commit-analyzer@^8.0.0": version "8.0.1" resolved "https://registry.yarnpkg.com/@semantic-release/commit-analyzer/-/commit-analyzer-8.0.1.tgz#5d2a37cd5a3312da0e3ac05b1ca348bf60b90bca"