From 72bc2d34a4a9294435c00679b941050cf696b4b3 Mon Sep 17 00:00:00 2001 From: alice Date: Sat, 24 Jul 2021 04:34:20 +0200 Subject: [PATCH] switch to a new version of payments module up to date with payment processors, pull-focused, and as a reusable app --- ccvpn/settings.py | 56 +- lambdainst/admin.py | 63 +-- lambdainst/migrations/0005_migrate_coupons.py | 76 +++ lambdainst/models.py | 126 +---- lambdainst/tasks.py | 10 +- .../stripe.py => lambdainst/tests/online.py | 36 +- lambdainst/{tests.py => tests/units.py} | 162 +----- lambdainst/urls.py | 2 - lambdainst/views.py | 47 +- payments/__init__.py | 0 payments/admin.py | 137 ----- payments/backends/__init__.py | 8 - payments/backends/base.py | 56 -- payments/backends/bitcoin.py | 108 ---- payments/backends/coinpayments.py | 301 ----------- payments/backends/paypal.py | 252 --------- payments/backends/stripe.py | 304 ----------- payments/forms.py | 16 - payments/management/__init__.py | 0 payments/management/commands/__init__.py | 0 payments/management/commands/bitcoin_info.py | 15 - .../management/commands/check_btc_payments.py | 28 - .../management/commands/confirm_payment.py | 52 -- .../management/commands/expire_payments.py | 31 -- .../commands/update_stripe_plans.py | 70 --- payments/migrations/0001_initial.py | 60 -- .../migrations/0002_auto_20151204_0341.py | 19 - .../migrations/0003_auto_20151209_0440.py | 20 - .../migrations/0004_auto_20160904_0048.py | 49 -- .../migrations/0005_auto_20160907_0018.py | 20 - .../migrations/0006_auto_20190907_2029.py | 26 - .../migrations/0007_auto_20201114_1730.py | 26 - .../migrations/0008_auto_20210721_1931.py | 99 ---- payments/migrations/__init__.py | 0 payments/models.py | 272 ---------- payments/tasks.py | 33 -- payments/tests/__init__.py | 10 - payments/tests/bitcoin.py | 118 ---- payments/tests/coingate.py | 75 --- payments/tests/paypal.py | 325 ----------- payments/urls.py | 23 - payments/views.py | 189 ------- poetry.lock | 511 +++++++++++------- pyproject.toml | 5 +- static/css/style.css | 6 +- templates/ccvpn/signup.html | 2 +- templates/lambdainst/account.html | 32 +- templates/payments/cancel_subscr.html | 2 +- .../payments/{form.html => order-pay.html} | 2 + .../payments/{list.html => payments.html} | 0 .../payments/{view.html => success.html} | 0 51 files changed, 537 insertions(+), 3343 deletions(-) create mode 100644 lambdainst/migrations/0005_migrate_coupons.py rename payments/tests/online/stripe.py => lambdainst/tests/online.py (85%) rename lambdainst/{tests.py => tests/units.py} (62%) delete mode 100644 payments/__init__.py delete mode 100644 payments/admin.py delete mode 100644 payments/backends/__init__.py delete mode 100644 payments/backends/base.py delete mode 100644 payments/backends/bitcoin.py delete mode 100644 payments/backends/coinpayments.py delete mode 100644 payments/backends/paypal.py delete mode 100644 payments/backends/stripe.py delete mode 100644 payments/forms.py delete mode 100644 payments/management/__init__.py delete mode 100644 payments/management/commands/__init__.py delete mode 100644 payments/management/commands/bitcoin_info.py delete mode 100644 payments/management/commands/check_btc_payments.py delete mode 100644 payments/management/commands/confirm_payment.py delete mode 100644 payments/management/commands/expire_payments.py delete mode 100644 payments/management/commands/update_stripe_plans.py delete mode 100644 payments/migrations/0001_initial.py delete mode 100644 payments/migrations/0002_auto_20151204_0341.py delete mode 100644 payments/migrations/0003_auto_20151209_0440.py delete mode 100644 payments/migrations/0004_auto_20160904_0048.py delete mode 100644 payments/migrations/0005_auto_20160907_0018.py delete mode 100644 payments/migrations/0006_auto_20190907_2029.py delete mode 100644 payments/migrations/0007_auto_20201114_1730.py delete mode 100644 payments/migrations/0008_auto_20210721_1931.py delete mode 100644 payments/migrations/__init__.py delete mode 100644 payments/models.py delete mode 100644 payments/tasks.py delete mode 100644 payments/tests/__init__.py delete mode 100644 payments/tests/bitcoin.py delete mode 100644 payments/tests/coingate.py delete mode 100644 payments/tests/paypal.py delete mode 100644 payments/urls.py delete mode 100644 payments/views.py rename templates/payments/{form.html => order-pay.html} (65%) rename templates/payments/{list.html => payments.html} (100%) rename templates/payments/{view.html => success.html} (100%) diff --git a/ccvpn/settings.py b/ccvpn/settings.py index a6c939f..fa8504e 100644 --- a/ccvpn/settings.py +++ b/ccvpn/settings.py @@ -43,7 +43,6 @@ INSTALLED_APPS = ( 'django.contrib.humanize', 'django_countries', 'django_lcore', - 'django_celery_results', 'django_celery_beat', 'lambdainst', 'payments', @@ -117,6 +116,51 @@ DATABASES = { } } +LOGGING = { + 'version': 1, + 'disable_existing_loggers': True, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + 'formatter': 'console', + }, + }, + 'loggers': { + 'root': { + 'handlers': ['console'], + 'level': 'DEBUG' if DEBUG else 'INFO', + }, + 'urllib3.connectionpool': { + 'handlers': ['console'], + #'level': 'DEBUG' if DEBUG else 'INFO', + 'level': 'INFO', + 'propagate': False, + }, + 'stripe': { + 'handlers': ['console'], + #'level': 'DEBUG' if DEBUG else 'INFO', + 'level': 'INFO', + 'propagate': False, + }, + 'payments': { + 'handlers': ['console'], + 'level': 'DEBUG' if DEBUG else 'INFO', + 'propagate': False, + }, + }, + 'formatters': { + 'console': { + 'format': '%(asctime)s %(levelname).4s %(name)s: %(message)s', + }, + }, +} + +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', + 'LOCATION': os.path.join(BASE_DIR, ".cache"), + } +} # Internationalization # https://docs.djangoproject.com/en/1.8/topics/i18n/ @@ -196,8 +240,6 @@ ROOT_URL = '' REAL_IP_HEADER_NAME = None # Celery defaults -#CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler' -CELERY_RESULT_BACKEND = 'django-db' CELERY_CACHE_BACKEND = 'django-cache' CELERY_BROKER_URL = 'redis://localhost/1' CELERY_TIMEZONE = 'UTC' @@ -283,10 +325,10 @@ PAYMENTS_BACKENDS = { PAYMENTS_CURRENCY = ('eur', '€') PLANS = { - '1m': {'monthly': 3.00, 'months': 1, }, - '3m': {'monthly': 9.00, 'months': 3, }, - '6m': {'monthly': 18.00, 'months': 6, }, - '12m': {'monthly': 36.00, 'months': 12, 'default': 1}, + '1m': {'monthly': 3.00, 'months': 1, }, + '3m': {'monthly': 3.00, 'months': 3, }, + '6m': {'monthly': 3.00, 'months': 6, }, + '12m': {'monthly': 3.00, 'months': 12, 'default': 1}, } diff --git a/lambdainst/admin.py b/lambdainst/admin.py index 98c02d6..01f6d2c 100644 --- a/lambdainst/admin.py +++ b/lambdainst/admin.py @@ -10,25 +10,14 @@ from django.utils.html import format_html import django_lcore import lcoreapi -from lambdainst.models import VPNUser, GiftCode, GiftCodeUser +from lambdainst.models import VPNUser +from payments.admin import UserCouponInline, UserLedgerInline def make_user_link(user): change_url = resolve_url('admin:auth_user_change', user.id) return format_html('{}', change_url, user.username) - -class GiftCodeAdminForm(forms.ModelForm): - def clean(self): - input_code = self.cleaned_data.get('code', '') - code_charset = string.ascii_letters + string.digits - if any(c not in code_charset for c in input_code): - raise forms.ValidationError(_("Code must be [a-zA-Z0-9]")) - if not 1 <= len(input_code) <= 32: - raise forms.ValidationError(_("Code must be between 1 and 32 characters")) - return self.cleaned_data - - class VPNUserInline(admin.StackedInline): model = VPNUser can_delete = False @@ -58,31 +47,8 @@ class VPNUserInline(admin.StackedInline): is_paid.short_description = _("Is paid?") -class GiftCodeUserAdmin(admin.TabularInline): - model = GiftCodeUser - fields = ('user_link', 'code_link', 'date') - readonly_fields = ('user_link', 'code_link', 'date') - list_display = ('user', ) - original = False - - def user_link(self, object): - return make_user_link(object.user) - user_link.short_description = 'User' - - def code_link(self, object): - change_url = resolve_url('admin:lambdainst_giftcode_change', object.code.id) - return format_html('{}', change_url, object.code.code) - code_link.short_description = 'Code' - - def has_add_permission(self, request, obj): - return False - - def has_delete_permission(self, request, obj=None): - return False - - class UserAdmin(UserAdmin): - inlines = (VPNUserInline, GiftCodeUserAdmin) + inlines = (VPNUserInline, UserLedgerInline, UserCouponInline) list_display = ('username', 'email', 'is_staff', 'date_joined', 'is_paid') ordering = ('-date_joined', ) fieldsets = ( @@ -124,28 +90,5 @@ class UserAdmin(UserAdmin): super().delete_model(request, obj) -class GiftCodeAdmin(admin.ModelAdmin): - fields = ('code', 'time', 'created', 'created_by', 'single_use', 'free_only', - 'available', 'comment') - readonly_fields = ('created', 'created_by') - list_display = ('code', 'time', 'comment_head', 'available') - search_fields = ('code', 'comment', 'users__username') - inlines = (GiftCodeUserAdmin,) - list_filter = ('available', 'time') - form = GiftCodeAdminForm - - def comment_head(self, object): - return object.comment_head - comment_head.short_description = _("Comment") - - def save_model(self, request, obj, form, change): - if not change: - obj.created_by = request.user - obj.save() - - admin.site.unregister(User) admin.site.register(User, UserAdmin) - -admin.site.register(GiftCode, GiftCodeAdmin) - diff --git a/lambdainst/migrations/0005_migrate_coupons.py b/lambdainst/migrations/0005_migrate_coupons.py new file mode 100644 index 0000000..4c163b5 --- /dev/null +++ b/lambdainst/migrations/0005_migrate_coupons.py @@ -0,0 +1,76 @@ +# Generated by Django 3.2.5 on 2021-07-22 00:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('lambdainst', '0004_auto_20200829_2054'), + ] + + operations = [ + migrations.RenameField( + model_name='giftcode', + old_name='free_only', + new_name='new_clients_only', + ), + migrations.RemoveField( + model_name='giftcode', + name='created_by', + ), + migrations.AddField( + model_name='giftcode', + name='for_plans', + field=models.CharField(blank=True, help_text='Valid only for the following plans, separated by commas. ("1m,6m", "12m", ...) (empty: any plan allowed)', max_length=100), + ), + migrations.AddField( + model_name='giftcode', + name='valid_after', + field=models.DateTimeField(blank=True, help_text='Valid only after the date. (empty: no limit)', null=True), + ), + migrations.AddField( + model_name='giftcode', + name='valid_before', + field=models.DateTimeField(blank=True, help_text='Valid only before the date. (empty: no limit)', null=True), + ), + migrations.AddField( + model_name='giftcode', + name='value', + field=models.IntegerField(blank=True, help_text='Integer between 0 and 100. If the total is below 0.50$, it will be free and skip the payment', null=True, verbose_name='Discount %'), + ), + migrations.AlterField( + model_name='giftcode', + name='available', + field=models.BooleanField(default=True, help_text='Coupon invalid when unchecked.'), + ), + + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.RemoveField( + model_name='giftcodeuser', + name='code', + ), + migrations.RemoveField( + model_name='giftcodeuser', + name='user', + ), + migrations.DeleteModel( + name='GiftCode', + ), + migrations.DeleteModel( + name='GiftCodeUser', + ), + ], + database_operations=[ + migrations.AlterModelTable( + name='GiftCode', + table='payments_coupon', + ), + migrations.AlterModelTable( + name='GiftCodeUser', + table='payments_couponuser', + ), + ], + ), + ] diff --git a/lambdainst/models.py b/lambdainst/models.py index 15dc720..0a993fd 100644 --- a/lambdainst/models.py +++ b/lambdainst/models.py @@ -1,16 +1,10 @@ import random -from datetime import timedelta -from django.db import models +from django.db import models, IntegrityError from django.contrib.auth.models import User from django.utils.translation import ugettext as _ -from django.db.models.signals import post_save -from django.db import IntegrityError -from django.dispatch import receiver -from constance import config as site_config from django_lcore.core import LcoreUserProfileMethods, setup_sync_hooks, VPN_AUTH_STORAGE -from ccvpn.common import get_trial_period_duration -from payments.models import Subscription +from payments.models import BaseSubUser prng = random.SystemRandom() @@ -20,7 +14,7 @@ def random_gift_code(): return ''.join([prng.choice(charset) for n in range(10)]) -class VPNUser(models.Model, LcoreUserProfileMethods): +class VPNUser(models.Model, BaseSubUser, LcoreUserProfileMethods): class Meta: verbose_name = _("VPN User") verbose_name_plural = _("VPN Users") @@ -53,39 +47,12 @@ class VPNUser(models.Model, LcoreUserProfileMethods): self.referrer_used = False self.campaign = None - def give_trial_period(self): - self.add_paid_time(get_trial_period_duration()) - self.trial_periods_given += 1 - - @property - def can_have_trial(self): - if self.trial_periods_given >= site_config.TRIAL_PERIOD_MAX: - return False - if self.user.payment_set.filter(status='confirmed').count() > 0: - return False - return True - - @property - def remaining_trial_periods(self): - return site_config.TRIAL_PERIOD_MAX - self.trial_periods_given - def on_payment_confirmed(self, payment): if self.referrer and not self.referrer_used: - self.referrer.vpnuser.add_paid_time(timedelta(days=30)) + self.referrer.vpnuser.add_paid_months(1, 'referrer', f"rewarded for {self.user.username} (payment #{payment.id})") self.referrer.vpnuser.save() self.referrer_used = True - - def get_subscription(self, include_unconfirmed=False): - """ Tries to get the active (and most recent) Subscription """ - s = Subscription.objects.filter(user=self.user, status='active') \ - .order_by('-id') \ - .first() - if s is not None or include_unconfirmed is False: - return s - s = Subscription.objects.filter(user=self.user, status='unconfirmed') \ - .order_by('-id') \ - .first() - return s + self.save() def lcore_sync(self): if VPN_AUTH_STORAGE == 'inst': @@ -93,75 +60,26 @@ class VPNUser(models.Model, LcoreUserProfileMethods): from lambdainst.tasks import push_user push_user.delay(user_id=self.user.id) - def __str__(self): - return self.user.username - -setup_sync_hooks(User, VPNUser) + def notify_payment(self, payment): + return -@receiver(post_save, sender=User) -def create_vpnuser(sender, instance, created, **kwargs): - if created: - try: - VPNUser.objects.create(user=instance) - except IntegrityError: - pass + def notify_subscription(self, sub): + return - -class GiftCode(models.Model): - class Meta: - verbose_name = _("Gift Code") - verbose_name_plural = _("Gift Codes") - - code = models.CharField(max_length=32, default=random_gift_code) - time = models.DurationField(default=timedelta(days=30)) - created = models.DateTimeField(auto_now_add=True, null=True, blank=True) - created_by = models.ForeignKey(User, related_name='created_giftcode_set', - on_delete=models.CASCADE, null=True, blank=True) - single_use = models.BooleanField(default=True) - free_only = models.BooleanField(default=True) - available = models.BooleanField(default=True) - comment = models.TextField(blank=True) - users = models.ManyToManyField(User, through='GiftCodeUser') - - def use_on(self, user): - if not self.available: - return False - if self.free_only and user.vpnuser.is_paid: - return False - - link = GiftCodeUser(user=user, code=self) - link.save() - - user.vpnuser.add_paid_time(self.time) - user.vpnuser.save() - - if self.single_use: - self.available = False - - self.save() - - return True - - @property - def comment_head(self): - head = self.comment.split('\n', 1)[0] - if len(head) > 80: - head = head[:80] + "..." - return head + def notify_refund(self, payment): + return def __str__(self): - return self.code - - -class GiftCodeUser(models.Model): - class Meta: - verbose_name = _("Gift Code User") - verbose_name_plural = _("Gift Code Users") - - user = models.ForeignKey(User, on_delete=models.CASCADE) - code = models.ForeignKey(GiftCode, on_delete=models.CASCADE) - date = models.DateTimeField(auto_now_add=True, null=True, blank=True) + return self.user.username - def __str__(self): - return "%s (%s)" % (self.user.username, self.code.code) +setup_sync_hooks(User, VPNUser) +#from django.db.models.signals import post_save +#from django.dispatch import receiver +#@receiver(post_save, sender=User) +#def create_vpnuser(sender, instance, created, **kwargs): +# if created: +# try: +# VPNUser.objects.create(user=instance) +# except IntegrityError: +# pass diff --git a/lambdainst/tasks.py b/lambdainst/tasks.py index 16a5715..f8aa3ec 100644 --- a/lambdainst/tasks.py +++ b/lambdainst/tasks.py @@ -2,7 +2,7 @@ import logging import itertools import time from datetime import timedelta, datetime -from celery import task +from celery import shared_task from django.db.models import Q, F from django.db import transaction from django.conf import settings @@ -22,7 +22,7 @@ SITE_NAME = settings.TICKETS_SITE_NAME logger = logging.getLogger(__name__) -@task(autoretry_for=(Exception,), default_retry_delay=60 * 60) +@shared_task(autoretry_for=(Exception,), default_retry_delay=60 * 60) def push_all_users(): count = 0 for u in User.objects.all(): @@ -37,14 +37,14 @@ def push_all_users(): logger.info("pushed %d users", count) -@task(autoretry_for=(Exception,), max_retries=10, retry_backoff=True) +@shared_task(autoretry_for=(Exception,), max_retries=10, retry_backoff=True) def push_user(user_id): user = User.objects.get(id=user_id) logger.info("pushing user #%d %r", user.id, user) django_lcore.sync_user(user.vpnuser, fail_silently=False) -@task +@shared_task def notify_vpn_auth_fails(): """ due to the high number of regular authentication fails, we'll start sending @@ -179,7 +179,7 @@ def notify_vpn_auth_fails(): examine_users() -@task +@shared_task def notify_account_expiration(): """ Notify users near the end of their subscription """ from_email = settings.DEFAULT_FROM_EMAIL diff --git a/payments/tests/online/stripe.py b/lambdainst/tests/online.py similarity index 85% rename from payments/tests/online/stripe.py rename to lambdainst/tests/online.py index c05b57b..a2ed413 100644 --- a/payments/tests/online/stripe.py +++ b/lambdainst/tests/online.py @@ -28,10 +28,10 @@ class BaseOnlineTest(StaticLiveServerTestCase): def setUpClass(cls): super().setUpClass() cls.selenium = WebDriver(firefox_binary="/usr/bin/firefox") - cls.selenium.implicitly_wait(6) + cls.selenium.implicitly_wait(12) cls.root_url = settings.ROOT_URL - cls.wait = WebDriverWait(cls.selenium, 6) + cls.wait = WebDriverWait(cls.selenium, 20) @classmethod def tearDownClass(cls): @@ -49,7 +49,7 @@ class BaseOnlineTest(StaticLiveServerTestCase): self.selenium.find_element_by_xpath('//input[@value="Sign up"]').click() -def wait(f, initial=5, between=3): +def wait(f, initial=3, between=2): time.sleep(initial) print("waiting for confirmation...") while True: @@ -71,15 +71,15 @@ class OnlineStripeTests(BaseOnlineTest): self.wait.until( EC.visibility_of( self.selenium.find_element_by_xpath( - '//label[@for="tab_onetime"]/..//select[@name="method"]' + '//label[@for="tab_onetime"]/..//select[@name="payment-method"]' ) ) ) self.selenium.find_element_by_xpath( - '//label[@for="tab_onetime"]/..//select[@name="time"]/option[@value="3"]' + '//label[@for="tab_onetime"]/..//select[@name="plan"]/option[@value="3m"]' ).click() self.selenium.find_element_by_xpath( - '//label[@for="tab_onetime"]/..//select[@name="method"]/option[@value="stripe"]' + '//label[@for="tab_onetime"]/..//select[@name="payment-method"]/option[@value="stripe"]' ).click() self.selenium.find_element_by_xpath( '//label[@for="tab_onetime"]/..//input[@value="Buy Now"]' @@ -102,7 +102,15 @@ class OnlineStripeTests(BaseOnlineTest): ) self.selenium.find_element_by_xpath('//button[@type="submit"]').click() + self.wait.until( + EC.presence_of_element_located((By.XPATH, '//*[contains(text(), "Waiting for payment")]')) + ) + def check_active(): + # refresh payment as we dont have a worker + p = Payment.objects.order_by('id').first() + p.refresh() + self.selenium.refresh() self.selenium.find_element_by_xpath('//h2[contains(text(),"Confirmed")]') @@ -119,15 +127,15 @@ class OnlineStripeTests(BaseOnlineTest): self.wait.until( EC.visibility_of( self.selenium.find_element_by_xpath( - '//label[@for="tab_subscr"]/..//select[@name="method"]' + '//label[@for="tab_subscr"]/..//select[@name="payment-method"]' ) ) ) self.selenium.find_element_by_xpath( - '//label[@for="tab_subscr"]/..//select[@name="time"]/option[@value="12"]' + '//label[@for="tab_subscr"]/..//select[@name="plan"]/option[@value="12m"]' ).click() self.selenium.find_element_by_xpath( - '//label[@for="tab_subscr"]/..//select[@name="method"]/option[@value="stripe"]' + '//label[@for="tab_subscr"]/..//select[@name="payment-method"]/option[@value="stripe"]' ).click() self.selenium.find_element_by_xpath( '//label[@for="tab_subscr"]/..//input[@value="Subscribe"]' @@ -150,7 +158,15 @@ class OnlineStripeTests(BaseOnlineTest): ) self.selenium.find_element_by_xpath('//button[@type="submit"]').click() + self.wait.until( + EC.presence_of_element_located((By.XPATH, '//*[contains(text(), "Your subscription is processing.")]')) + ) + def check_active(): + # refresh sub as we dont have a worker + p = Subscription.objects.order_by('id').first() + p.refresh() + self.selenium.refresh() sub_status = self.selenium.find_element_by_xpath( '//td[text()="Subscription"]//following-sibling::td' @@ -166,7 +182,6 @@ class OnlineStripeTests(BaseOnlineTest): user = User.objects.get(username="test-user") assert user.vpnuser.is_paid - assert user.vpnuser.expiration >= (timezone.now() + timedelta(days=359)) sub_status.find_element_by_xpath('a[text()="cancel"]').click() self.selenium.find_element_by_xpath( @@ -180,5 +195,4 @@ class OnlineStripeTests(BaseOnlineTest): user = User.objects.get(username="test-user") assert user.vpnuser.is_paid - assert user.vpnuser.expiration >= (timezone.now() + timedelta(days=359)) assert not user.vpnuser.get_subscription() diff --git a/lambdainst/tests.py b/lambdainst/tests/units.py similarity index 62% rename from lambdainst/tests.py rename to lambdainst/tests/units.py index 6e62ab2..e392288 100644 --- a/lambdainst/tests.py +++ b/lambdainst/tests/units.py @@ -7,108 +7,19 @@ from io import StringIO from constance import config as site_config from constance.test import override_config -from .forms import SignupForm -from .models import VPNUser, User, random_gift_code, GiftCode, GiftCodeUser +from lambdainst.forms import SignupForm +from lambdainst.models import VPNUser, User from payments.models import Payment, Subscription class UserTestMixin: - def assertRemaining(self, vpnuser, time): + def assertRemaining(self, vpnuser, time, delta=5): """ Check that the vpnuser will expire in time (+/- 5 seconds) """ exp = vpnuser.expiration or timezone.now() seconds = (exp - timezone.now() - time).total_seconds() - self.assertAlmostEqual(seconds, 0, delta=5) + self.assertAlmostEqual(seconds, 0, delta=delta) -class UserModelTest(TestCase, UserTestMixin): - def setUp(self): - User.objects.create_user('aaa') - - def test_add_time(self): - u = User.objects.get(username='aaa') - vu = u.vpnuser - p = timedelta(days=1) - - self.assertFalse(vu.is_paid) - - vu.expiration = timezone.now() - vu.add_paid_time(p) - final = vu.expiration - - self.assertRemaining(vu, p) - self.assertGreater(final, timezone.now()) - self.assertTrue(vu.is_paid) - - def test_add_time_past(self): - u = User.objects.get(username='aaa') - vu = u.vpnuser - p = timedelta(days=1) - - self.assertFalse(vu.is_paid) - - vu.expiration = timezone.now() - timedelta(days=135) - vu.add_paid_time(p) - final = vu.expiration - - self.assertRemaining(vu, p) - self.assertGreater(final, timezone.now()) - self.assertTrue(vu.is_paid) - - def test_add_time_initial(self): - u = User.objects.get(username='aaa') - vu = u.vpnuser - p = timedelta(days=1) - - self.assertFalse(vu.is_paid) - - vu.add_paid_time(p) - self.assertTrue(vu.is_paid) - - def test_paid_between_subscr_payments(self): - u = User.objects.get(username='aaa') - vu = u.vpnuser - s = Subscription(user=u, backend_id='paypal', status='new') - s.save() - - self.assertFalse(vu.is_paid) - - s.status = 'active' - s.save() - - self.assertTrue(vu.is_paid) - - def test_grant_trial(self): - p = timedelta(days=1) - u = User.objects.get(username='aaa') - vu = u.vpnuser - - with override_config(TRIAL_PERIOD_HOURS=24, TRIAL_PERIOD_MAX=2): - self.assertEqual(vu.remaining_trial_periods, 2) - self.assertTrue(vu.can_have_trial) - vu.give_trial_period() - self.assertRemaining(vu, p) - - self.assertEqual(vu.remaining_trial_periods, 1) - self.assertTrue(vu.can_have_trial) - vu.give_trial_period() - self.assertRemaining(vu, p * 2) - - self.assertEqual(vu.remaining_trial_periods, 0) - self.assertFalse(vu.can_have_trial) - - def test_trial_refused(self): - p = timedelta(days=1) - u = User.objects.get(username='aaa') - payment = Payment.objects.create(user=u, status='confirmed', amount=300, - time=timedelta(days=30)) - payment.save() - - vu = u.vpnuser - - with override_config(TRIAL_PERIOD_HOURS=24, TRIAL_PERIOD_MAX=2): - self.assertEqual(vu.remaining_trial_periods, 2) - self.assertFalse(vu.can_have_trial) - class UserModelReferrerTest(TestCase, UserTestMixin): def setUp(self): @@ -129,14 +40,7 @@ class UserModelReferrerTest(TestCase, UserTestMixin): self.with_ref.vpnuser.on_payment_confirmed(self.payment) self.assertTrue(self.with_ref.vpnuser.referrer_used) self.assertEqual(self.with_ref.vpnuser.referrer, self.referrer) - self.assertRemaining(self.referrer.vpnuser, timedelta(days=14)) - - -class GCModelTest(TestCase): - def test_generator(self): - c = random_gift_code() - self.assertEqual(len(c), 10) - self.assertNotEqual(c, random_gift_code()) + self.assertRemaining(self.referrer.vpnuser, timedelta(days=30), delta=24*3600*3) class SignupViewTest(TestCase): @@ -242,54 +146,6 @@ class AccountViewsTest(TestCase, UserTestMixin): self.assertFalse(user.check_password('new_test_pw2')) self.assertTrue(user.check_password('test_pw')) - def test_giftcode_use_single(self): - gc = GiftCode.objects.create(time=timedelta(days=42), single_use=True) - - response = self.client.post('/account/gift_code', {'code': gc.code}) - self.assertRedirects(response, '/account/') - - user = User.objects.get(username='test') - self.assertRemaining(user.vpnuser, timedelta(days=42)) - - response = self.client.post('/account/gift_code', {'code': gc.code}) - self.assertRedirects(response, '/account/') - - user = User.objects.get(username='test') - self.assertRemaining(user.vpnuser, timedelta(days=42)) # same expiration - - def test_giftcode_use_free_only(self): - gc = GiftCode.objects.create(time=timedelta(days=42), free_only=True) - - response = self.client.post('/account/gift_code', {'code': gc.code}) - self.assertRedirects(response, '/account/') - - user = User.objects.get(username='test') - self.assertRemaining(user.vpnuser, timedelta(days=42)) - - def test_giftcode_use_free_only_fail(self): - gc = GiftCode.objects.create(time=timedelta(days=42), free_only=True) - user = User.objects.get(username='test') - user.vpnuser.add_paid_time(timedelta(days=1)) - user.vpnuser.save() - - response = self.client.post('/account/gift_code', {'code': gc.code}) - self.assertRedirects(response, '/account/') - - user = User.objects.get(username='test') - self.assertRemaining(user.vpnuser, timedelta(days=1)) - - def test_giftcode_create_gcu(self): - gc = GiftCode.objects.create(time=timedelta(days=42)) - - response = self.client.post('/account/gift_code', {'code': gc.code}) - self.assertRedirects(response, '/account/') - - user = User.objects.get(username='test') - gcu = GiftCodeUser.objects.get(user=user, code=gc) - - self.assertRemaining(user.vpnuser, timedelta(days=42)) - self.assertIn(gcu, user.giftcodeuser_set.all()) - class CACrtViewTest(TestCase): def test_ca_crt(self): @@ -312,7 +168,7 @@ class ExpireNotifyTest(TestCase): def test_notify_first(self): out = StringIO() u = User.objects.create_user('test_username', 'test@example.com', 'testpw') - u.vpnuser.add_paid_time(timedelta(days=2)) + u.vpnuser.add_paid_time(timedelta(days=2), 'initial') u.vpnuser.save() call_command('expire_notify', stdout=out) @@ -328,7 +184,7 @@ class ExpireNotifyTest(TestCase): out = StringIO() u = User.objects.create_user('test_username', 'test@example.com', 'testpw') u.vpnuser.last_expiry_notice = timezone.now() - timedelta(days=2) - u.vpnuser.add_paid_time(timedelta(days=1)) + u.vpnuser.add_paid_time(timedelta(days=1), 'initial') u.vpnuser.save() call_command('expire_notify', stdout=out) @@ -343,7 +199,7 @@ class ExpireNotifyTest(TestCase): def test_notify_subscription(self): out = StringIO() u = User.objects.create_user('test_username', 'test@example.com', 'testpw') - u.vpnuser.add_paid_time(timedelta(days=2)) + u.vpnuser.add_paid_time(timedelta(days=2), 'initial') u.vpnuser.save() s = Subscription(user=u, backend_id='paypal', status='active') @@ -360,7 +216,7 @@ class ExpireNotifyTest(TestCase): def test_notify_subscription_new(self): out = StringIO() u = User.objects.create_user('test_username', 'test@example.com', 'testpw') - u.vpnuser.add_paid_time(timedelta(days=2)) + u.vpnuser.add_paid_time(timedelta(days=2), 'initial') u.vpnuser.last_expiry_notice = timezone.now() - timedelta(days=5) u.vpnuser.save() diff --git a/lambdainst/urls.py b/lambdainst/urls.py index 4daebfc..7edaa29 100644 --- a/lambdainst/urls.py +++ b/lambdainst/urls.py @@ -17,7 +17,5 @@ urlpatterns = [ path('config_dl', django_lcore.views.openvpn_dl), path('wireguard', views.wireguard), path('wireguard/new', views.wireguard_new, name='wireguard_new'), - path('gift_code', views.gift_code), - path('trial', views.trial), path('', views.index, name='index'), ] diff --git a/lambdainst/views.py b/lambdainst/views.py index ba732dd..c6ede44 100644 --- a/lambdainst/views.py +++ b/lambdainst/views.py @@ -28,7 +28,7 @@ import lcoreapi from ccvpn.common import get_client_ip, get_price_float from payments.models import ACTIVE_BACKENDS from .forms import SignupForm, ReqEmailForm, WgPeerForm -from .models import GiftCode, VPNUser +from .models import VPNUser from . import graphs @@ -96,11 +96,7 @@ def signup(request): pass user.vpnuser.campaign = request.session.get('campaign') - user.vpnuser.add_paid_time(timedelta(days=7)) - - if site_config.TRIAL_ON_SIGNUP: - trial_time = timedelta(hours=site_config.TRIAL_ON_SIGNUP) - user.vpnuser.add_paid_time(trial_time) + user.vpnuser.add_paid_time(timedelta(days=7), 'trial') user.vpnuser.save() user.vpnuser.lcore_sync() @@ -191,7 +187,7 @@ def index(request): title=_("Account"), ref_url=ref_url, twitter_link=twitter_url + urlencode(twitter_args), - subscription=request.user.vpnuser.get_subscription(include_unconfirmed=True), + subscription=request.user.vpnuser.get_subscription(), backends=sorted(ACTIVE_BACKENDS.values(), key=lambda x: x.backend_display_name), subscr_backends=sorted((b for b in ACTIVE_BACKENDS.values() if b.backend_has_recurring), @@ -207,6 +203,9 @@ def index(request): def captcha_test(grr, request): api_url = project_settings.HCAPTCHA_API + if not project_settings.HCAPTCHA_SITE_KEY: + return True + if api_url == 'TEST' and grr == 'TEST-TOKEN': # FIXME: i'm sorry. return True @@ -223,22 +222,6 @@ def captcha_test(grr, request): return False -@login_required -def trial(request): - if request.method != 'POST' or not request.user.vpnuser.can_have_trial: - return redirect('account:index') - - grr = request.POST.get('g-recaptcha-response', '') - if captcha_test(grr, request): - request.user.vpnuser.give_trial_period() - request.user.vpnuser.save() - messages.success(request, _("OK!")) - else: - messages.error(request, _("Invalid captcha")) - - return redirect('account:index') - - def make_export_zip(user, name): import io import zipfile @@ -337,7 +320,6 @@ def deactivate_user(user): user.password = "" user.save() - user.giftcodeuser_set.all().delete() user.payment_set.update(backend_data="null") user.subscription_set.update(backend_data="null") @@ -405,23 +387,6 @@ def settings(request): )) -@login_required -def gift_code(request): - try: - code = GiftCode.objects.get(code=request.POST.get('code', '').strip(), available=True) - except GiftCode.DoesNotExist: - code = None - - if code is None: - messages.error(request, _("Gift code not found or already used.")) - elif not code.use_on(request.user): - messages.error(request, _("Gift code only available to free accounts.")) - else: - messages.success(request, _("OK!")) - - return redirect('account:index') - - @login_required def config(request): return render(request, 'lambdainst/config.html', dict( diff --git a/payments/__init__.py b/payments/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/payments/admin.py b/payments/admin.py deleted file mode 100644 index c0ff01e..0000000 --- a/payments/admin.py +++ /dev/null @@ -1,137 +0,0 @@ -import json -from django.shortcuts import resolve_url -from django.contrib import admin -from django.utils.html import format_html -from django.utils.translation import ugettext_lazy as _ -from django.utils.text import Truncator -from .models import Payment, Subscription, Feedback - - -def subscr_mark_as_cancelled(modeladmin, request, queryset): - queryset.update(status='cancelled') -subscr_mark_as_cancelled.short_description = _("Mark as cancelled (do not actually cancel)") - - -def link(text, url): - if not url: - return text - if not text: - text = url - return format_html('{}', url, text) - -def json_format(code): - j = json.dumps(code, indent=2) - return format_html("
{}
", j) - - -class PaymentAdmin(admin.ModelAdmin): - model = Payment - list_display = ('user', 'backend', 'status', 'amount', 'paid_amount', 'created') - list_filter = ('backend_id', 'status') - - fieldsets = ( - (None, { - 'fields': ('backend', 'user_link', 'subscription_link', 'time', 'status', - 'status_message'), - }), - (_("Payment Data"), { - 'fields': ('amount_fmt', 'paid_amount_fmt', - 'backend_extid_link', 'backend_data_fmt'), - }), - ) - - readonly_fields = ('backend', 'user_link', 'time', 'status', 'status_message', - 'amount_fmt', 'paid_amount_fmt', 'subscription_link', - 'backend_extid_link', 'backend_data_fmt') - search_fields = ('user__username', 'user__email', 'backend_extid', 'backend_data') - - def backend(self, object): - try: - return object.backend.backend_verbose_name - except KeyError: - return "#" + object.backend_id - - def backend_data_fmt(self, object): - return json_format(object.backend_data) - - def backend_extid_link(self, object): - try: - ext_url = object.backend.get_ext_url(object) - return link(object.backend_extid, ext_url) - except KeyError: - return "#" + object.backend_id - - def amount_fmt(self, object): - return '%.2f %s' % (object.amount / 100, object.currency_name) - amount_fmt.short_description = _("Amount") - - def paid_amount_fmt(self, object): - return '%.2f %s' % (object.paid_amount / 100, object.currency_name) - paid_amount_fmt.short_description = _("Paid amount") - - def user_link(self, object): - change_url = resolve_url('admin:auth_user_change', object.user.id) - return link(object.user.username, change_url) - user_link.short_description = 'User' - - def subscription_link(self, object): - change_url = resolve_url('admin:payments_subscription_change', - object.subscription.id) - return link(object.subscription.id, change_url) - subscription_link.short_description = 'Subscription' - - -class SubscriptionAdmin(admin.ModelAdmin): - model = Subscription - list_display = ('user', 'created', 'status', 'backend', 'backend_extid') - list_filter = ('backend_id', 'status') - readonly_fields = ('user_link', 'backend', 'created', 'status', - 'last_confirmed_payment', 'payments_links', - 'backend_extid_link', 'backend_data_fmt') - search_fields = ('user__username', 'user__email', 'backend_extid', 'backend_data') - actions = (subscr_mark_as_cancelled,) - fieldsets = ( - (None, { - 'fields': ('backend', 'user_link', 'payments_links', 'status', - 'last_confirmed_payment'), - }), - (_("Payment Data"), { - 'fields': ('backend_extid_link', 'backend_data_fmt'), - }), - ) - - def backend(self, object): - return object.backend.backend_verbose_name - - def backend_data_fmt(self, object): - return json_format(object.backend_data) - - def user_link(self, object): - change_url = resolve_url('admin:auth_user_change', object.user.id) - return link(object.user.id, change_url) - user_link.short_description = 'User' - - def payments_links(self, object): - count = Payment.objects.filter(subscription=object).count() - payments_url = resolve_url('admin:payments_payment_changelist') - url = "%s?subscription__id__exact=%s" % (payments_url, object.id) - return link("%d payment(s)" % count, url) - payments_links.short_description = 'Payments' - - def backend_extid_link(self, object): - ext_url = object.backend.get_subscr_ext_url(object) - return link(object.backend_extid, ext_url) - backend_extid_link.allow_tags = True - -class FeedbackAdmin(admin.ModelAdmin): - model = Feedback - list_display = ('user', 'created', 'short_message') - readonly_fields = ('user', 'created', 'message', 'subscription') - - def short_message(self, obj): - return Truncator(obj.message).chars(80) - -admin.site.register(Payment, PaymentAdmin) -admin.site.register(Subscription, SubscriptionAdmin) -admin.site.register(Feedback, FeedbackAdmin) - diff --git a/payments/backends/__init__.py b/payments/backends/__init__.py deleted file mode 100644 index b0f5ef2..0000000 --- a/payments/backends/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# flake8: noqa - -from .base import BackendBase, ManualBackend -from .paypal import PaypalBackend -from .bitcoin import BitcoinBackend -from .stripe import StripeBackend -from .coinpayments import CoinPaymentsBackend - diff --git a/payments/backends/base.py b/payments/backends/base.py deleted file mode 100644 index 82c7170..0000000 --- a/payments/backends/base.py +++ /dev/null @@ -1,56 +0,0 @@ -from django.utils.translation import ugettext_lazy as _ - - -class BackendBase: - backend_id = None - backend_verbose_name = "" - backend_display_name = "" - backend_enabled = False - backend_has_recurring = False - - def __init__(self, settings): - pass - - def new_payment(self, payment): - """ Initialize a payment and returns an URL to redirect the user. - Can return a HTML string that will be sent back to the user in a - default template (like a form) or a HTTP response (like a redirect). - """ - raise NotImplementedError() - - def callback(self, payment, request): - """ Handle a callback """ - raise NotImplementedError() - - def callback_subscr(self, payment, request): - """ Handle a callback (recurring payments) """ - raise NotImplementedError() - - def cancel_subscription(self, subscr): - """ Cancel a subscription """ - raise NotImplementedError() - - def get_info(self): - """ Returns some status (key, value) list """ - return () - - def get_ext_url(self, payment): - """ Returns URL to external payment view, or None """ - return None - - def get_subscr_ext_url(self, subscr): - """ Returns URL to external payment view, or None """ - return None - - -class ManualBackend(BackendBase): - """ Manual backend used to store and display informations about a - payment processed manually. - More a placeholder than an actual payment beckend, everything raises - NotImplementedError(). - """ - - backend_id = 'manual' - backend_verbose_name = _("Manual") - - diff --git a/payments/backends/bitcoin.py b/payments/backends/bitcoin.py deleted file mode 100644 index 26a075b..0000000 --- a/payments/backends/bitcoin.py +++ /dev/null @@ -1,108 +0,0 @@ -from decimal import Decimal - -from django.shortcuts import redirect -from django.utils.translation import ugettext_lazy as _ -from django.urls import reverse -from constance import config as site_config - -from .base import BackendBase - - -class BitcoinBackend(BackendBase): - """ Bitcoin backend. - Connects to a bitcoind. - """ - backend_id = 'bitcoin' - backend_verbose_name = _("Bitcoin") - backend_display_name = _("Bitcoin") - - COIN = 100000000 - - def __init__(self, settings): - from bitcoin import SelectParams - from bitcoin.rpc import Proxy - - self.account = settings.get('account', 'ccvpn3') - - chain = settings.get('chain') - if chain: - SelectParams(chain) - - self.url = settings.get('url') - if not self.url: - return - - self.make_rpc = lambda: Proxy(self.url) - self.rpc = self.make_rpc() - self.backend_enabled = True - - @property - def btc_value(self): - return site_config.BTC_EUR_VALUE - - def new_payment(self, payment): - rpc = self.make_rpc() - - # bitcoins amount = (amount in cents) / (cents per bitcoin) - btc_price = round(Decimal(payment.amount) / self.btc_value, 5) - - address = str(rpc.getnewaddress(self.account)) - - msg = _("Please send %(amount)s BTC to %(address)s") - payment.status_message = msg % dict(amount=str(btc_price), address=address) - payment.backend_extid = address - payment.backend_data = dict(btc_price=str(btc_price), btc_address=address) - payment.save() - return redirect(reverse('payments:view', args=(payment.id,))) - - def check(self, payment): - rpc = self.make_rpc() - - if payment.status != 'new': - return - - btc_price = payment.backend_data.get('btc_price') - address = payment.backend_data.get('btc_address') - if not btc_price or not address: - return - - btc_price = Decimal(btc_price) - - received = Decimal(rpc.getreceivedbyaddress(address)) / self.COIN - payment.paid_amount = int(received * self.btc_value) - payment.backend_data['btc_paid_price'] = str(received) - - if received >= btc_price: - payment.user.vpnuser.add_paid_time(payment.time) - payment.user.vpnuser.on_payment_confirmed(payment) - payment.user.vpnuser.save() - - payment.status = 'confirmed' - - payment.save() - - def get_info(self): - rpc = self.make_rpc() - - try: - info = rpc.getinfo() - if not info: - return [(_("Status"), "Error: got None")] - except Exception as e: - return [(_("Status"), "Error: " + repr(e))] - v = info.get('version', 0) - return ( - (_("Bitcoin value"), "%.2f €" % (self.btc_value / 100)), - (_("Testnet"), info['testnet']), - (_("Balance"), '{:f}'.format(info['balance'] / self.COIN)), - (_("Blocks"), info['blocks']), - (_("Bitcoind version"), '.'.join(str(v // 10 ** (2 * i) % 10 ** (2 * i)) - for i in range(3, -1, -1))), - ) - - def get_ext_url(self, payment): - if not payment.backend_extid: - return None - return 'https://blockstream.info/address/%s' % payment.backend_extid - - diff --git a/payments/backends/coinpayments.py b/payments/backends/coinpayments.py deleted file mode 100644 index e86c5e4..0000000 --- a/payments/backends/coinpayments.py +++ /dev/null @@ -1,301 +0,0 @@ -import math -from decimal import Decimal - -from django.shortcuts import redirect -from django.utils.translation import ugettext_lazy as _ -from django.urls import reverse -from constance import config as site_config - -from django.conf import settings as project_settings -from .base import BackendBase - - -import hmac -import hashlib -import requests -import logging -import json -from urllib.parse import urlencode -logger = logging.getLogger(__name__) - - -class CoinPaymentsError(Exception): - pass - - -class CoinPayments: - def __init__(self, pkey, skey, api_url=None): - self.public_key = pkey - self.secret_key = skey.encode('utf-8') - self.api_url = api_url or 'https://www.coinpayments.net/api.php' - - def _sign(self, params): - body = urlencode(params).encode('utf-8') - mac = hmac.new(self.secret_key, body, hashlib.sha512) - return body, mac.hexdigest() - - def _request(self, cmd, params): - params.update({ - 'cmd': cmd, - 'key': self.public_key, - 'format': 'json', - 'version': 1, - }) - print(params) - post_body, mac = self._sign(params) - - headers = { - 'HMAC': mac, - 'Content-Type': 'application/x-www-form-urlencoded', - } - - r = requests.post(self.api_url, data=post_body, - headers=headers) - try: - r.raise_for_status() - j = r.json() - except Exception as e: - raise CoinPaymentsError(str(e)) from e - - if j.get('error') == 'ok': - return j.get('result') - else: - raise CoinPaymentsError(j.get('error')) - - def create_transaction(self, **params): - assert 'amount' in params - assert 'currency1' in params - assert 'currency2' in params - return self._request('create_transaction', params) - - def get_account_info(self, **params): - return self._request('get_basic_info', params) - - def get_rates(self, **params): - return self._request('rates', params) - - def get_balances(self, **params): - return self._request('balances', params) - - def get_deposit_address(self, **params): - assert 'currency' in params - return self._request('get_deposit_address', params) - - def get_callback_address(self, **params): - assert 'currency' in params - return self._request('get_callback_address', params) - - def get_tx_info(self, **params): - assert 'txid' in params - return self._request('get_tx_info', params) - - def get_tx_info_multi(self, ids=None, **params): - if ids is not None: - params['txid'] = '|'.join(str(i) for i in ids) - assert 'txid' in params - return self._request('get_tx_info_multi', params) - - def get_tx_ids(self, **params): - return self._request('get_tx_ids', params) - - def create_transfer(self, **params): - assert 'amount' in params - assert 'currency' in params - assert 'merchant' in params or 'pbntag' in params - return self._request('create_transfer', params) - - def create_withdrawal(self, **params): - assert 'amount' in params - assert 'currency' in params - assert 'address' in params or 'pbntag' in params - return self._request('create_withdrawal', params) - - def create_mass_withdrawal(self, **params): - assert 'wd' in params - return self._request('create_mass_withdrawal', params) - - def convert(self, **params): - assert 'amount' in params - assert 'from' in params - assert 'to' in params - return self._request('convert', params) - - def get_withdrawal_history(self, **params): - return self._request('get_withdrawal_history', params) - - def get_withdrawal_info(self, **params): - assert 'id' in params - return self._request('get_withdrawal_info', params) - - def get_conversion_info(self, **params): - assert 'id' in params - return self._request('get_conversion_info', params) - - def get_pbn_info(self, **params): - assert 'pbntag' in params - return self._request('get_pbn_info', params) - - def get_pbn_list(self, **params): - return self._request('get_pbn_list', params) - - def update_pbn_tag(self, **params): - assert 'tagid' in params - return self._request('update_pbn_tag', params) - - def claim_pbn_tag(self, **params): - assert 'tagid' in params - assert 'name' in params - return self._request('claim_pbn_tag', params) - - -class IpnError(Exception): - pass - - -def ipn_assert(request, remote, local, key=None, delta=None): - if (delta is None and remote != local) or (delta is not None and not math.isclose(remote, local, abs_tol=delta)): - logger.debug("Invalid IPN %r: local=%r remote=%r", - key, local, remote) - raise IpnError("Unexpected value: %s" % key) - - -def ipn_assert_post(request, key, local): - remote = request.POST.get(key) - ipn_assert(request, remote, local, key=key) - - -class CoinPaymentsBackend(BackendBase): - backend_id = 'coinpayments' - backend_verbose_name = _("CoinPayments") - backend_display_name = _("Cryptocurrencies") - backend_has_recurring = False - - def __init__(self, settings): - self.merchant_id = settings.get('merchant_id') - self.currency = settings.get('currency', 'EUR') - self.api_base = settings.get('api_base', None) - self.title = settings.get('title', 'VPN Payment') - self.secret = settings.get('secret', '').encode('utf-8') - - if self.merchant_id and self.secret: - self.backend_enabled = True - - def new_payment(self, payment): - ROOT_URL = project_settings.ROOT_URL - params = { - 'cmd': '_pay', - 'reset': '1', - 'want_shipping': '0', - 'merchant': self.merchant_id, - 'currency': self.currency, - 'amountf': '%.2f' % (payment.amount / 100), - 'item_name': self.title, - 'ipn_url': ROOT_URL + reverse('payments:cb_coinpayments', args=(payment.id,)), - 'success_url': ROOT_URL + reverse('payments:view', args=(payment.id,)), - 'cancel_url': ROOT_URL + reverse('payments:cancel', args=(payment.id,)), - } - - payment.status_message = _("Waiting for CoinPayments to confirm the transaction... " + - "It can take up to a few minutes...") - payment.save() - - form = '
' - for k, v in params.items(): - form += '' % (k, v) - form += ''' - redirecting... - -
- - ''' - return form - - def handle_ipn(self, payment, request): - sig = request.META.get('HTTP_HMAC') - if not sig: - raise IpnError("Missing HMAC") - - mac = hmac.new(self.secret, request.body, hashlib.sha512).hexdigest() - - # Sanity checks, if it fails the IPN is to be ignored - ipn_assert(request, sig, mac, 'HMAC') - ipn_assert_post(request, 'ipn_mode', 'hmac') - ipn_assert_post(request, 'merchant', self.merchant_id) - - try: - status = int(request.POST.get('status')) - except ValueError: - raise IpnError("Invalid status (%r)" % status) - - # Some states are final (can't cancel a timeout or refund) - if payment.status not in ('new', 'confirmed', 'error'): - m = "Unexpected state change for %s: is %s, received status=%r" % ( - payment.id, payment.status, status - ) - raise IpnError(m) - - # whatever the status, we can safely update the text and save the tx id - payment.status_text = request.POST.get('status_text') or payment.status_text - payment.backend_extid = request.POST.get('txn_id') - - received_amount = request.POST.get('amount1') - if received_amount: - payment.paid_amount = float(received_amount) * 100 - - # And now the actual processing - if status == 1: # A payment is confirmed paid - if payment.status != 'confirmed': - if payment.paid_amount != payment.amount: - ipn_assert(request, payment.paid_amount, payment.amount, 'paid', - delta=10) - vpnuser = payment.user.vpnuser - vpnuser.add_paid_time(payment.time) - vpnuser.on_payment_confirmed(payment) - vpnuser.save() - - # We save the new state *at the end* - # (it will be retried if there's an error) - payment.status = 'confirmed' - payment.status_message = None - payment.save() - - elif status > 1: # Waiting (that's further confirmation about funds getting moved) - # we have nothing to do, except updating status_text - payment.save() - return - - elif status == -1: # Cancel / Time out - payment.status = 'cancelled' - payment.save() - - elif status == -2: # A refund - if payment.status == 'confirmed': # (paid -> refunded) - payment.status = 'refunded' - # TODO - - elif status <= -3: # Unknown error - payment.status = 'error' - payment.save() - - def callback(self, payment, request): - try: - self.handle_ipn(payment, request) - return True - except IpnError as e: - payment.status = 'error' - payment.status_message = ("Error processing the payment. " - "Please contact support.") - payment.backend_data['ipn_exception'] = repr(e) - payment.backend_data['ipn_last_data'] = repr(request.POST) - payment.save() - logger.warn("IPN error: %s", e) - raise - - diff --git a/payments/backends/paypal.py b/payments/backends/paypal.py deleted file mode 100644 index 91dc0fb..0000000 --- a/payments/backends/paypal.py +++ /dev/null @@ -1,252 +0,0 @@ -from django.shortcuts import redirect -from django.utils.translation import ugettext_lazy as _ -from urllib.parse import urlencode -from urllib.request import urlopen -from django.urls import reverse -from django.conf import settings as project_settings -import requests - -from .base import BackendBase - - -def urljoin(a, b): - if b.startswith('/') and a.endswith('/'): - return a + b[1:] - if b.startswith('/') or a.endswith('/'): - return a + b - return a + "/" + b - - -class PaypalBackend(BackendBase): - backend_id = 'paypal' - backend_verbose_name = _("PayPal") - backend_display_name = _("PayPal") - backend_has_recurring = True - - def __init__(self, settings): - self.test = settings.get('test', False) - self.header_image = settings.get('header_image', None) - self.title = settings.get('title', 'VPN Payment') - self.currency = settings.get('currency', 'EUR') - self.account_address = settings.get('address') - self.receiver_address = settings.get('receiver', self.account_address) - - self.api_username = settings.get('api_username') - self.api_password = settings.get('api_password') - self.api_sig = settings.get('api_sig') - - if self.test: - default_nvp = 'https://api-3t.sandbox.paypal.com/nvp' - default_api = 'https://www.sandbox.paypal.com/' - else: - default_nvp = 'https://api-3t.paypal.com/nvp' - default_api = 'https://www.paypal.com/' - self.api_base = settings.get('api_base', default_api) - self.nvp_api_base = settings.get('nvp_api_base', default_nvp) - - if self.account_address and self.api_username and self.api_password and self.api_sig: - self.backend_enabled = True - - def new_payment(self, payment): - ROOT_URL = project_settings.ROOT_URL - params = { - 'cmd': '_xclick', - 'notify_url': ROOT_URL + reverse('payments:cb_paypal', args=(payment.id,)), - 'item_name': self.title, - 'amount': '%.2f' % (payment.amount / 100), - 'currency_code': self.currency, - 'business': self.account_address, - 'no_shipping': '1', - 'return': ROOT_URL + reverse('payments:view', args=(payment.id,)), - 'cancel_return': ROOT_URL + reverse('payments:cancel', args=(payment.id,)), - } - - if self.header_image: - params['cpp_header_image'] = self.header_image - - payment.status_message = _("Waiting for PayPal to confirm the transaction... " + - "It can take up to a few minutes...") - payment.save() - - return redirect(urljoin(self.api_base, '/cgi-bin/webscr?' + urlencode(params))) - - def new_subscription(self, rps): - months = { - '3m': 3, - '6m': 6, - '12m': 12, - }[rps.period] - - ROOT_URL = project_settings.ROOT_URL - params = { - 'cmd': '_xclick-subscriptions', - 'notify_url': ROOT_URL + reverse('payments:cb_paypal_subscr', args=(rps.id,)), - 'item_name': self.title, - 'currency_code': self.currency, - 'business': self.account_address, - 'no_shipping': '1', - 'return': ROOT_URL + reverse('payments:return_subscr', args=(rps.id,)), - 'cancel_return': ROOT_URL + reverse('account:index'), - - 'a3': '%.2f' % (rps.period_amount / 100), - 'p3': str(months), - 't3': 'M', - 'src': '1', - } - - if self.header_image: - params['cpp_header_image'] = self.header_image - - rps.save() - - return redirect(urljoin(self.api_base, '/cgi-bin/webscr?' + urlencode(params))) - - def handle_verified_callback(self, payment, params): - if self.test and params['test_ipn'] != '1': - raise ValueError('Test IPN') - - txn_type = params.get('txn_type') - if txn_type not in (None, 'web_accept', 'express_checkout'): - # Not handled here and can be ignored - return - - if params['payment_status'] == 'Refunded': - payment.status = 'refunded' - payment.status_message = None - - elif params['payment_status'] == 'Completed': - self.handle_completed_payment(payment, params) - - def handle_verified_callback_subscr(self, subscr, params): - if self.test and params['test_ipn'] != '1': - raise ValueError('Test IPN') - - txn_type = params.get('txn_type') - if not txn_type.startswith('subscr_'): - # Not handled here and can be ignored - return - - if txn_type == 'subscr_payment': - if params['payment_status'] == 'Refunded': - # FIXME: Find the payment and do something - pass - - elif params['payment_status'] == 'Completed': - payment = subscr.create_payment() - if not self.handle_completed_payment(payment, params): - return - - subscr.last_confirmed_payment = payment.created - subscr.backend_extid = params.get('subscr_id', '') - if subscr.status == 'new' or subscr.status == 'unconfirmed': - subscr.status = 'active' - subscr.save() - elif txn_type == 'subscr_cancel' or txn_type == 'subscr_eot': - subscr.status = 'cancelled' - subscr.save() - - def handle_completed_payment(self, payment, params): - from payments.models import Payment - - # Prevent making duplicate Payments if IPN is received twice - pc = Payment.objects.filter(backend_extid=params['txn_id']).count() - if pc > 0: - return False - - if self.receiver_address != params['receiver_email']: - raise ValueError('Wrong receiver: ' + params['receiver_email']) - if self.currency.lower() != params['mc_currency'].lower(): - raise ValueError('Wrong currency: ' + params['mc_currency']) - - payment.paid_amount = int(float(params['mc_gross']) * 100) - if payment.paid_amount < payment.amount: - raise ValueError('Not fully paid.') - - payment.user.vpnuser.add_paid_time(payment.time) - payment.user.vpnuser.on_payment_confirmed(payment) - payment.user.vpnuser.save() - - payment.backend_extid = params['txn_id'] - payment.status = 'confirmed' - payment.status_message = None - payment.save() - - payment.user.vpnuser.lcore_sync() - return True - - def verify_ipn(self, request): - v_url = urljoin(self.api_base, '/cgi-bin/webscr?cmd=_notify-validate') - v_req = urlopen(v_url, data=request.body, timeout=5) - v_res = v_req.read() - return v_res == b'VERIFIED' - - def callback(self, payment, request): - if not self.verify_ipn(request): - return False - - params = request.POST - - try: - self.handle_verified_callback(payment, params) - return True - except (KeyError, ValueError) as e: - payment.status = 'error' - payment.status_message = None - payment.backend_data['ipn_exception'] = repr(e) - payment.backend_data['ipn_last_data'] = repr(request.POST) - payment.save() - raise - - def callback_subscr(self, subscr, request): - if not self.verify_ipn(request): - return False - - params = request.POST - - try: - self.handle_verified_callback_subscr(subscr, params) - return True - except (KeyError, ValueError) as e: - subscr.status = 'error' - subscr.status_message = None - subscr.backend_data['ipn_exception'] = repr(e) - subscr.backend_data['ipn_last_data'] = repr(request.POST) - subscr.save() - raise - - def cancel_subscription(self, subscr): - if not subscr.backend_extid: - return False - - try: - r = requests.post(self.nvp_api_base, data={ - "METHOD": "ManageRecurringPaymentsProfileStatus", - "PROFILEID": subscr.backend_extid, - "ACTION": "cancel", - "USER": self.api_username, - "PWD": self.api_password, - "SIGNATURE": self.api_sig, - "VERSION": "204.0", - }) - r.raise_for_status() - print(r.text) - - subscr.status = 'cancelled' - subscr.save() - return True - except Exception as e: - print(e) - return False - - def get_ext_url(self, payment): - if not payment.backend_extid: - return None - url = 'https://history.paypal.com/webscr?cmd=_history-details-from-hub&id=%s' - return url % payment.backend_extid - - def get_subscr_ext_url(self, subscr): - if not subscr.backend_extid: - return None - return ('https://www.paypal.com/fr/cgi-bin/webscr?cmd=_profile-recurring-payments' - '&encrypted_profile_id=%s' % subscr.backend_extid) - diff --git a/payments/backends/stripe.py b/payments/backends/stripe.py deleted file mode 100644 index 7311c51..0000000 --- a/payments/backends/stripe.py +++ /dev/null @@ -1,304 +0,0 @@ -from django.utils.translation import ugettext_lazy as _ -from django.urls import reverse -from django.conf import settings as project_settings - -from .base import BackendBase - - -class StripeBackend(BackendBase): - backend_id = 'stripe' - backend_verbose_name = _("Stripe") - backend_display_name = _("Credit Card") - backend_has_recurring = True - - def get_plan_id(self, period): - return 'ccvpn_' + period - - def __init__(self, settings): - self.public_key = settings.get('public_key') - self.secret_key = settings.get('secret_key') - self.wh_key = settings.get('wh_key') - - if not self.public_key or not self.secret_key or not self.wh_key: - raise Exception("Missing keys for stripe backend") - - import stripe - self.stripe = stripe - stripe.api_key = self.secret_key - - self.header_image = settings.get('header_image', '') - self.currency = settings.get('currency', 'EUR') - self.title = settings.get('title', 'VPN Payment') - self.backend_enabled = True - - def make_redirect(self, session): - return ''' - - - - '''.format(pk=self.public_key, sess=session['id']) - - def new_payment(self, payment): - root_url = project_settings.ROOT_URL - assert root_url - - months = payment.time.days // 30 - if months > 1: - desc = '{} months for {}'.format(months, payment.user.username) - else: - desc = 'One month for {}'.format(payment.user.username) - - session = self.stripe.checkout.Session.create( - success_url=root_url + reverse('payments:view', args=(payment.id,)), - cancel_url=root_url + reverse('payments:cancel', args=(payment.id,)), - payment_method_types=['card'], - line_items=[ - { - 'amount': payment.amount, - 'currency': self.currency.lower(), - 'name': self.title, - 'description': desc, - 'quantity': 1, - } - ], - ) - payment.backend_extid = session['id'] - payment.backend_data = {'session_id': session['id']} - payment.save() - return self.make_redirect(session) - - def new_subscription(self, subscr): - root_url = project_settings.ROOT_URL - assert root_url - - session = self.stripe.checkout.Session.create( - success_url=root_url + reverse('payments:return_subscr', args=(subscr.id,)), - cancel_url=root_url + reverse('payments:cancel_subscr', args=(subscr.id,)), - client_reference_id='sub_%d'%subscr.id, - payment_method_types=['card'], - subscription_data={ - 'items': [{ - 'plan': self.get_plan_id(subscr.period), - 'quantity': 1, - }], - }, - ) - subscr.backend_data = {'session_id': session['id']} - subscr.save() - return self.make_redirect(session) - - def cancel_subscription(self, subscr): - if subscr.status not in ('new', 'unconfirmed', 'active'): - return - - if subscr.backend_extid.startswith('pi'): - # a session that didn't create a subscription yet (intent) - pass - elif subscr.backend_extid.startswith('sub_'): - # Subscription object - try: - self.stripe.Subscription.delete(subscr.backend_extid) - except self.stripe.error.InvalidRequestError: - pass - elif subscr.backend_extid.startswith('cus_'): - # Legacy Customer object - try: - cust = self.stripe.Customer.retrieve(subscr.backend_extid) - except self.stripe.error.InvalidRequestError: - return - - try: - # Delete customer and cancel any active subscription - cust.delete() - except self.stripe.error.InvalidRequestError: - pass - else: - raise Exception("Failed to cancel subscription %r" % subscr.backend_extid) - - subscr.status = 'cancelled' - subscr.save() - return True - - def refresh_subscription(self, subscr): - if subscr.backend_extid.startswith('cus_'): - customer = self.stripe.Customer.retrieve(subscr.backend_extid) - for s in customer['subscriptions']['data']: - if s['status'] == 'active': - sub = s - break - else: - return - elif subscr.backend_extid.startswith('sub_'): - sub = self.stripe.Subscription.retrieve(subscr.backend_extid) - else: - print("unhandled subscription backend extid: {}".format(subscr.backend_extid)) - return - - if sub['status'] == 'canceled': - subscr.status = 'cancelled' - if sub['status'] == 'past_due': - subscr.status = 'error' - - def webhook_session_completed(self, event): - session = event['data']['object'] - - if session['subscription']: - # Subscription creation - from payments.models import Payment, Subscription - - sub_id = session['subscription'] - assert sub_id - - parts = session['client_reference_id'].split('_') - if len(parts) != 2 or parts[0] != 'sub': - raise Exception("invalid reference id") - sub_internal_id = int(parts[1]) - - # Fetch sub by ID and confirm it - subscr = Subscription.objects.get(id=sub_internal_id) - subscr.status = 'active' - subscr.backend_extid = sub_id - subscr.set_data('subscription_id', sub_id) - subscr.save() - - else: - from payments.models import Payment - payment = Payment.objects.filter(backend_extid=session['id']).get() - - # the amount is provided server-side, we do not have to check - payment.paid_amount = payment.amount - - payment.status = 'confirmed' - payment.status_message = None - payment.save() - payment.user.vpnuser.add_paid_time(payment.time) - payment.user.vpnuser.on_payment_confirmed(payment) - payment.user.vpnuser.save() - - payment.user.vpnuser.lcore_sync() - - def get_subscription_from_invoice(self, invoice): - from payments.models import Subscription - - subscription_id = invoice['subscription'] - customer_id = invoice['customer'] - - # once it's confirmed, the id to the subscription is stored as extid - subscr = Subscription.objects.filter(backend_extid=subscription_id).first() - if subscr: - return subscr - - # older subscriptions will have a customer id instead - subscr = Subscription.objects.filter(backend_extid=customer_id).first() - if subscr: - return subscr - - return None - - def webhook_payment_succeeded(self, event): - """ webhook event for a subscription's succeeded payment """ - from payments.models import Payment - - invoice = event['data']['object'] - subscr = self.get_subscription_from_invoice(invoice) - if not subscr: - # the subscription does not exist - # checkout.confirmed event will create it and handle the initial payment - # return True - raise Exception("Unknown subscription for invoice %r" % invoice['id']) - - # Prevent making duplicate Payments if event is received twice - pc = Payment.objects.filter(backend_extid=invoice['id']).count() - if pc > 0: - return - - payment = subscr.create_payment() - payment.status = 'confirmed' - payment.paid_amount = payment.amount - payment.backend_extid = invoice['id'] - if invoice['subscription']: - if isinstance(invoice['subscription'], str): - payment.backend_sub_id = invoice['subscription'] - else: - payment.backend_sub_id = invoice['subscription']['id'] - payment.set_data('event_id', event['id']) - payment.set_data('sub_id', payment.backend_sub_id) - payment.save() - - payment.user.vpnuser.add_paid_time(payment.time) - payment.user.vpnuser.on_payment_confirmed(payment) - payment.user.vpnuser.save() - payment.save() - - payment.user.vpnuser.lcore_sync() - - def webhook_subscr_update(self, event): - from payments.models import Subscription - stripe_sub = event['data']['object'] - sub = Subscription.objects.get(backend_id='stripe', backend_extid=stripe_sub['id']) - - if not sub: - return - - if stripe_sub['status'] == 'canceled': - sub.status = 'cancelled' - if stripe_sub['status'] == 'past_due': - sub.status = 'error' - sub.save() - - def webhook(self, request): - payload = request.body - sig_header = request.META['HTTP_STRIPE_SIGNATURE'] - - try: - event = self.stripe.Webhook.construct_event( - payload, sig_header, self.wh_key, - ) - except (ValueError, self.stripe.error.InvalidRequestError, self.stripe.error.SignatureVerificationError): - return False - - if event['type'] == 'invoice.payment_succeeded': - self.webhook_payment_succeeded(event) - if event['type'] == 'checkout.session.completed': - self.webhook_session_completed(event) - if event['type'] == 'customer.subscription.deleted': - self.webhook_subscr_update(event) - return True - - def get_ext_url(self, payment): - extid = payment.backend_extid - - if not extid: - return None - - if extid.startswith('in_'): - return 'https://dashboard.stripe.com/invoices/%s' % extid - if extid.startswith('ch_'): - return 'https://dashboard.stripe.com/payments/%s' % extid - - def get_subscr_ext_url(self, subscr): - extid = subscr.backend_extid - - if not extid: - return None - - if extid.startswith('sub_') and self.stripe: - livemode = False - try: - sub = self.stripe.Subscription.retrieve(extid) - livemode = sub['livemode'] - except Exception: - pass - if livemode: - return 'https://dashboard.stripe.com/subscriptions/' + extid - else: - return 'https://dashboard.stripe.com/test/subscriptions/' + extid - - if extid.startswith('cus_'): - return 'https://dashboard.stripe.com/customers/%s' % subscr.backend_extid diff --git a/payments/forms.py b/payments/forms.py deleted file mode 100644 index b2245a5..0000000 --- a/payments/forms.py +++ /dev/null @@ -1,16 +0,0 @@ -from django import forms -from .models import BACKEND_CHOICES - - -class NewPaymentForm(forms.Form): - TIME_CHOICES = ( - ('1', '1'), - ('3', '3'), - ('6', '6'), - ('12', '12'), - ) - - subscr = forms.ChoiceField(choices=(('0', 'no'), ('1', 'yes'))) - time = forms.ChoiceField(choices=TIME_CHOICES) - method = forms.ChoiceField(choices=BACKEND_CHOICES) - diff --git a/payments/management/__init__.py b/payments/management/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/payments/management/commands/__init__.py b/payments/management/commands/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/payments/management/commands/bitcoin_info.py b/payments/management/commands/bitcoin_info.py deleted file mode 100644 index 66265fd..0000000 --- a/payments/management/commands/bitcoin_info.py +++ /dev/null @@ -1,15 +0,0 @@ -from django.core.management.base import BaseCommand, CommandError - -from payments.models import ACTIVE_BACKENDS - - -class Command(BaseCommand): - help = "Get bitcoind info" - - def handle(self, *args, **options): - if 'bitcoin' not in ACTIVE_BACKENDS: - raise CommandError("bitcoin backend not active.") - - backend = ACTIVE_BACKENDS['bitcoin'] - for key, value in backend.get_info(): - self.stdout.write("%s: %s" % (key, value)) diff --git a/payments/management/commands/check_btc_payments.py b/payments/management/commands/check_btc_payments.py deleted file mode 100644 index 0a0e68b..0000000 --- a/payments/management/commands/check_btc_payments.py +++ /dev/null @@ -1,28 +0,0 @@ -from django.core.management.base import BaseCommand, CommandError - -from payments.models import Payment, ACTIVE_BACKENDS - - -class Command(BaseCommand): - help = "Check bitcoin payments status" - - def handle(self, *args, **options): - if 'bitcoin' not in ACTIVE_BACKENDS: - raise CommandError("bitcoin backend not active.") - - backend = ACTIVE_BACKENDS['bitcoin'] - - payments = Payment.objects.filter(backend_id='bitcoin', status='new') - - self.stdout.write("Found %d active unconfirmed payments." % len(payments)) - - for p in payments: - self.stdout.write("Checking payment #%d... " % p.id, ending="") - backend.check(p) - - if p.status == 'confirmed': - self.stdout.write("OK.") - else: - self.stdout.write("Waiting") - - diff --git a/payments/management/commands/confirm_payment.py b/payments/management/commands/confirm_payment.py deleted file mode 100644 index 9cbbe4a..0000000 --- a/payments/management/commands/confirm_payment.py +++ /dev/null @@ -1,52 +0,0 @@ -from django.core.management.base import BaseCommand -from django.utils import timezone -from django.utils.dateparse import parse_duration - -from payments.models import Payment - - -class Command(BaseCommand): - help = "Manually confirm a Payment" - - def add_arguments(self, parser): - parser.add_argument('id', action='store', type=int, help="Payment ID") - parser.add_argument('--paid-amount', dest='amount', action='store', type=int, help="Paid amount") - parser.add_argument('--extid', dest='extid', action='store', type=str) - parser.add_argument('-n', dest='sim', action='store_true', help="Simulate") - - def handle(self, *args, **options): - try: - p = Payment.objects.get(id=options['id']) - except Payment.DoesNotExist: - self.stderr.write("Cannot find payment #%d" % options['id']) - return - - print("Payment #%d by %s (amount=%d; paid_amount=%d)" % (p.id, p.user.username, p.amount, p.paid_amount)) - - if options['amount']: - pa = options['amount'] - else: - pa = p.amount - - extid = options['extid'] - - print("Status -> confirmed") - print("Paid amount -> %d" % pa) - if extid: - print("Ext ID -> %s" % extid) - - print("Confirm? [y/n] ") - i = input() - if i.lower().strip() == 'y': - p.user.vpnuser.add_paid_time(p.time) - p.user.vpnuser.on_payment_confirmed(p) - p.user.vpnuser.save() - - p.paid_amount = pa - p.status = 'confirmed' - if extid: - p.backend_extid = extid - p.save() - else: - print("aborted.") - diff --git a/payments/management/commands/expire_payments.py b/payments/management/commands/expire_payments.py deleted file mode 100644 index deaf1e0..0000000 --- a/payments/management/commands/expire_payments.py +++ /dev/null @@ -1,31 +0,0 @@ -from django.core.management.base import BaseCommand -from django.utils import timezone -from django.utils.dateparse import parse_duration - -from payments.models import Payment - - -class Command(BaseCommand): - help = "Cancels expired Payments" - - def add_arguments(self, parser): - parser.add_argument('-n', dest='sim', action='store_true', help="Simulate") - parser.add_argument('-e', '--exp-time', action='store', - help="Expiration time.", default='3 00:00:00') - - def handle(self, *args, **options): - now = timezone.now() - expdate = now - parse_duration(options['exp_time']) - - self.stdout.write("Now: " + now.isoformat()) - self.stdout.write("Exp: " + expdate.isoformat()) - - expired = Payment.objects.filter(created__lte=expdate, status='new', - paid_amount=0) - - for p in expired: - self.stdout.write("Payment #%d (%s): %s" % (p.id, p.user.username, p.created)) - if not options['sim']: - p.status = 'cancelled' - p.save() - diff --git a/payments/management/commands/update_stripe_plans.py b/payments/management/commands/update_stripe_plans.py deleted file mode 100644 index 444dc69..0000000 --- a/payments/management/commands/update_stripe_plans.py +++ /dev/null @@ -1,70 +0,0 @@ -from django.core.management.base import BaseCommand, CommandError -from django.conf import settings - -from ccvpn.common import get_price -from payments.models import ACTIVE_BACKENDS, SUBSCR_PERIOD_CHOICES, period_months - -CURRENCY_CODE, CURRENCY_NAME = settings.PAYMENTS_CURRENCY - - -class Command(BaseCommand): - help = "Update Stripe plans" - - def add_arguments(self, parser): - parser.add_argument('--force-run', action='store_true', - help="Run even when Stripe backend is disabled") - parser.add_argument('--force-update', action='store_true', - help="Replace plans, including matching ones") - - def handle(self, *args, **options): - if 'stripe' not in ACTIVE_BACKENDS and options['force-run'] is False: - raise CommandError("stripe backend not active.") - - backend = ACTIVE_BACKENDS['stripe'] - stripe = backend.stripe - - for period_id, period_name in SUBSCR_PERIOD_CHOICES: - plan_id = backend.get_plan_id(period_id) - months = period_months(period_id) - amount = months * get_price() - - kwargs = dict( - id=plan_id, - amount=amount, - interval='month', - interval_count=months, - name="VPN Subscription (%s)" % period_id, - currency=CURRENCY_CODE, - ) - - self.stdout.write('Plan %s: %d months for %.2f %s (%s)... ' % ( - plan_id, months, amount / 100, CURRENCY_NAME, CURRENCY_CODE), ending='') - self.stdout.flush() - - try: - plan = stripe.Plan.retrieve(plan_id) - except stripe.error.InvalidRequestError: - plan = None - - def is_valid_plan(): - if not plan: - return False - for k, v in kwargs.items(): - if getattr(plan, k) != v: - return False - return True - - if plan: - if is_valid_plan() and not options['force_update']: - self.stdout.write(self.style.SUCCESS('[ok]')) - continue - plan.delete() - update = True - else: - update = False - - stripe.Plan.create(**kwargs) - if update: - self.stdout.write(self.style.WARNING('[updated]')) - else: - self.stdout.write(self.style.WARNING('[created]')) diff --git a/payments/migrations/0001_initial.py b/payments/migrations/0001_initial.py deleted file mode 100644 index 3425c63..0000000 --- a/payments/migrations/0001_initial.py +++ /dev/null @@ -1,60 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models -import jsonfield.fields -from django.conf import settings - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Payment', - fields=[ - ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)), - ('backend_id', models.CharField(choices=[('bitcoin', 'Bitcoin'), ('coinbase', 'Coinbase'), ('manual', 'Manual'), ('paypal', 'PayPal'), ('stripe', 'Stripe')], max_length=16)), - ('status', models.CharField(choices=[('new', 'Waiting for payment'), ('confirmed', 'Confirmed'), ('cancelled', 'Cancelled'), ('rejected', 'Rejected by processor'), ('error', 'Payment processing failed')], max_length=16)), - ('created', models.DateTimeField(auto_now_add=True)), - ('modified', models.DateTimeField(auto_now=True)), - ('confirmed_on', models.DateTimeField(null=True, blank=True)), - ('amount', models.IntegerField()), - ('paid_amount', models.IntegerField(default=0)), - ('time', models.DurationField()), - ('status_message', models.TextField(null=True, blank=True)), - ('backend_extid', models.CharField(null=True, max_length=64, blank=True)), - ('backend_data', jsonfield.fields.JSONField(blank=True, default=dict)), - ], - options={ - 'ordering': ('-created',), - }, - ), - migrations.CreateModel( - name='RecurringPaymentSource', - fields=[ - ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)), - ('backend', models.CharField(choices=[('bitcoin', 'Bitcoin'), ('coinbase', 'Coinbase'), ('manual', 'Manual'), ('paypal', 'PayPal'), ('stripe', 'Stripe')], max_length=16)), - ('created', models.DateTimeField(auto_now_add=True)), - ('modified', models.DateTimeField(auto_now=True)), - ('period', models.CharField(choices=[('monthly', 'Monthly'), ('biannually', 'Bianually'), ('yearly', 'Yearly')], max_length=16)), - ('last_confirmed_payment', models.DateTimeField(null=True, blank=True)), - ('backend_id', models.CharField(null=True, max_length=64, blank=True)), - ('backend_data', jsonfield.fields.JSONField(blank=True, default=dict)), - ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), - ], - ), - migrations.AddField( - model_name='payment', - name='recurring_source', - field=models.ForeignKey(null=True, to='payments.RecurringPaymentSource', blank=True, on_delete=models.CASCADE), - ), - migrations.AddField( - model_name='payment', - name='user', - field=models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.SET_NULL), - ), - ] diff --git a/payments/migrations/0002_auto_20151204_0341.py b/payments/migrations/0002_auto_20151204_0341.py deleted file mode 100644 index 8755270..0000000 --- a/payments/migrations/0002_auto_20151204_0341.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('payments', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='recurringpaymentsource', - name='period', - field=models.CharField(max_length=16, choices=[('6m', 'Every 6 months'), ('1year', 'Yearly')]), - ), - ] diff --git a/payments/migrations/0003_auto_20151209_0440.py b/payments/migrations/0003_auto_20151209_0440.py deleted file mode 100644 index 312fb3f..0000000 --- a/payments/migrations/0003_auto_20151209_0440.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9 on 2015-12-09 04:40 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('payments', '0002_auto_20151204_0341'), - ] - - operations = [ - migrations.AlterField( - model_name='payment', - name='status', - field=models.CharField(choices=[('new', 'Waiting for payment'), ('confirmed', 'Confirmed'), ('cancelled', 'Cancelled'), ('rejected', 'Rejected by processor'), ('error', 'Payment processing failed')], default='new', max_length=16), - ), - ] diff --git a/payments/migrations/0004_auto_20160904_0048.py b/payments/migrations/0004_auto_20160904_0048.py deleted file mode 100644 index c7db118..0000000 --- a/payments/migrations/0004_auto_20160904_0048.py +++ /dev/null @@ -1,49 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.5 on 2016-09-04 00:48 -from __future__ import unicode_literals - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import jsonfield.fields - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('payments', '0003_auto_20151209_0440'), - ] - - operations = [ - migrations.CreateModel( - name='Subscription', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('backend_id', models.CharField(choices=[('bitcoin', 'Bitcoin'), ('coinbase', 'Coinbase'), ('manual', 'Manual'), ('paypal', 'PayPal'), ('stripe', 'Stripe')], max_length=16)), - ('created', models.DateTimeField(auto_now_add=True)), - ('period', models.CharField(choices=[('3m', 'Every 3 months'), ('6m', 'Every 6 months'), ('12m', 'Every year')], max_length=16)), - ('last_confirmed_payment', models.DateTimeField(blank=True, null=True)), - ('status', models.CharField(choices=[('new', 'Waiting for payment'), ('active', 'Active'), ('cancelled', 'Cancelled')], default='new', max_length=16)), - ('backend_extid', models.CharField(blank=True, max_length=64, null=True)), - ('backend_data', jsonfield.fields.JSONField(blank=True, default=dict)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - ), - migrations.RemoveField( - model_name='recurringpaymentsource', - name='user', - ), - migrations.RemoveField( - model_name='payment', - name='recurring_source', - ), - migrations.DeleteModel( - name='RecurringPaymentSource', - ), - migrations.AddField( - model_name='payment', - name='subscription', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='payments.Subscription'), - ), - ] diff --git a/payments/migrations/0005_auto_20160907_0018.py b/payments/migrations/0005_auto_20160907_0018.py deleted file mode 100644 index 93d608c..0000000 --- a/payments/migrations/0005_auto_20160907_0018.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.5 on 2016-09-07 00:18 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('payments', '0004_auto_20160904_0048'), - ] - - operations = [ - migrations.AlterField( - model_name='subscription', - name='status', - field=models.CharField(choices=[('new', 'Created'), ('unconfirmed', 'Waiting for payment'), ('active', 'Active'), ('cancelled', 'Cancelled'), ('error', 'Error')], default='new', max_length=16), - ), - ] diff --git a/payments/migrations/0006_auto_20190907_2029.py b/payments/migrations/0006_auto_20190907_2029.py deleted file mode 100644 index d19c545..0000000 --- a/payments/migrations/0006_auto_20190907_2029.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 2.2.1 on 2019-09-07 20:29 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('payments', '0005_auto_20160907_0018'), - ] - - operations = [ - migrations.CreateModel( - name='Feedback', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateTimeField(auto_now_add=True)), - ('message', models.TextField()), - ('subscription', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='payments.Subscription')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - ), - ] diff --git a/payments/migrations/0007_auto_20201114_1730.py b/payments/migrations/0007_auto_20201114_1730.py deleted file mode 100644 index a275da1..0000000 --- a/payments/migrations/0007_auto_20201114_1730.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 3.1.2 on 2020-11-14 17:30 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('payments', '0006_auto_20190907_2029'), - ] - - operations = [ - migrations.AlterField( - model_name='payment', - name='backend_extid', - field=models.CharField(blank=True, max_length=256, null=True), - ), - migrations.AlterField( - model_name='subscription', - name='backend_extid', - field=models.CharField(blank=True, max_length=256, null=True), - ), - ] diff --git a/payments/migrations/0008_auto_20210721_1931.py b/payments/migrations/0008_auto_20210721_1931.py deleted file mode 100644 index c00c579..0000000 --- a/payments/migrations/0008_auto_20210721_1931.py +++ /dev/null @@ -1,99 +0,0 @@ -# Generated by Django 3.2.4 on 2021-07-21 19:31 - -from datetime import timedelta -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import jsonfield.fields - -def field_to_plan_id(model_name, field_name): - def fun(apps, schema_editor): - keys = settings.PLANS.keys() - model = apps.get_model('payments', model_name) - for s in model.objects.all(): - d = getattr(s, field_name) - if s.plan_id is not None: - continue - s.plan_id = str(round(d / timedelta(days=30))) + 'm' - if s.plan_id not in keys: - raise Exception(f"unknown plan: {s.plan_id}") - s.save() - return fun - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('payments', '0007_auto_20201114_1730'), - ] - - operations = [ - migrations.RenameField("Subscription", 'period', 'plan_id'), - - # Add plan_id to payments and convert from the time field - migrations.AddField( - model_name='payment', - name='plan_id', - field=models.CharField(choices=[('1m', 'Every 1 month'), ('3m', 'Every 3 months'), ('6m', 'Every 6 months'), ('12m', 'Every 12 months')], max_length=16, null=True), - ), - migrations.RunPython(field_to_plan_id('Payment', 'time'), lambda x, y: ()), - - # Make those two columns non-null once converted - migrations.AlterField( - model_name='payment', - name='plan_id', - field=models.CharField(choices=[('1m', 'Every 1 month'), ('3m', 'Every 3 months'), ('6m', 'Every 6 months'), ('12m', 'Every 12 months')], max_length=16), - ), - migrations.AlterField( - model_name='subscription', - name='plan_id', - field=models.CharField(choices=[('1m', 'Every 1 month'), ('3m', 'Every 3 months'), ('6m', 'Every 6 months'), ('12m', 'Every 12 months')], max_length=16), - ), - - migrations.AddField( - model_name='payment', - name='ip_address', - field=models.GenericIPAddressField(blank=True, null=True), - ), - migrations.AddField( - model_name='payment', - name='refund_date', - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name='payment', - name='refund_text', - field=models.CharField(blank=True, max_length=200), - ), - migrations.AddField( - model_name='subscription', - name='next_payment_date', - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AlterField( - model_name='payment', - name='backend_data', - field=jsonfield.fields.JSONField(blank=True), - ), - migrations.AlterField( - model_name='payment', - name='backend_id', - field=models.CharField(choices=[('bitcoin', 'Bitcoin'), ('paypal', 'PayPal'), ('stripe', 'Stripe')], max_length=16), - ), - migrations.AlterField( - model_name='payment', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), - ), - migrations.AlterField( - model_name='subscription', - name='backend_data', - field=jsonfield.fields.JSONField(blank=True), - ), - migrations.AlterField( - model_name='subscription', - name='backend_id', - field=models.CharField(choices=[('bitcoin', 'Bitcoin'), ('paypal', 'PayPal'), ('stripe', 'Stripe')], max_length=16), - ), - ] diff --git a/payments/migrations/__init__.py b/payments/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/payments/models.py b/payments/models.py deleted file mode 100644 index 5606389..0000000 --- a/payments/models.py +++ /dev/null @@ -1,272 +0,0 @@ -import logging -from django.db import models -from django.conf import settings -from django.utils.translation import ugettext_lazy as _ -from jsonfield import JSONField -from datetime import timedelta -import json - -from ccvpn.common import get_price -from .backends import BackendBase - -logger = logging.getLogger(__name__) - -backends_settings = settings.PAYMENTS_BACKENDS -assert isinstance(backends_settings, dict) - -CURRENCY_CODE, CURRENCY_NAME = settings.PAYMENTS_CURRENCY - -STATUS_CHOICES = ( - ('new', _("Waiting for payment")), - ('confirmed', _("Confirmed")), - ('cancelled', _("Cancelled")), - ('rejected', _("Rejected by processor")), - ('error', _("Payment processing failed")), -) - -# A Subscription is created with status='new'. When getting back from PayPal, -# it may get upgraded to 'unconfirmed'. It will be set 'active' with the first -# confirmed payment. -# 'unconfirmed' exists to prevent creation of a second Subscription while -# waiting for the first one to be confirmed. -SUBSCR_STATUS_CHOICES = ( - ('new', _("Created")), - ('unconfirmed', _("Waiting for payment")), - ('active', _("Active")), - ('cancelled', _("Cancelled")), - ('error', _("Error")), -) - -SUBSCR_PERIOD_CHOICES = ( - ('3m', _("Every 3 months")), - ('6m', _("Every 6 months")), - ('12m', _("Every year")), -) - -BACKEND_CLASSES = BackendBase.__subclasses__() - -# All known backends (classes) -BACKENDS = {} -BACKEND_CHOICES = [] - -# All enabled backends (configured instances) -ACTIVE_BACKENDS = {} -ACTIVE_BACKEND_CHOICES = [] - -logger.info("loading payment backends...") -for cls in BACKEND_CLASSES: - name = cls.backend_id - assert isinstance(name, str) - - if name not in backends_settings: - logger.info("payments: ☐ %s disabled (no settings)", name) - continue - - backend_settings = backends_settings.get(name, {}) - for k, v in backend_settings.items(): - if hasattr(v, '__call__'): - backend_settings[k] = v() - - if not backend_settings.get('enabled'): - logger.info("payments: ☐ %s disabled (by settings)", name) - continue - - obj = cls(backend_settings) - - BACKENDS[name] = obj - BACKEND_CHOICES.append((name, cls.backend_verbose_name)) - - if obj.backend_enabled: - ACTIVE_BACKENDS[name] = obj - ACTIVE_BACKEND_CHOICES.append((name, cls.backend_verbose_name)) - logger.info("payments: ☑ %s initialized", name) - else: - logger.info("payments: ☒ %s disabled (initialization failed)", name) - -BACKEND_CHOICES = sorted(BACKEND_CHOICES, key=lambda x: x[0]) -ACTIVE_BACKEND_CHOICES = sorted(ACTIVE_BACKEND_CHOICES, key=lambda x: x[0]) - -logger.info("payments: finished. %d/%d backends active", len(ACTIVE_BACKENDS), len(BACKEND_CLASSES)) - - -class PlanBase(object): - def __init__(self, name, months, monthly, saves="", default=False): - self.name = name - self.months = months - self.monthly = monthly - self.saves = saves - self.default = default - @property - def due_amount(self): - return round(self.months * self.monthly, 2) - @property - def time_display(self): - return "%d month%s" % (self.months, 's' if self.months > 1 else '') - def json(self): - return {'total': self.due_amount} - - -PLANS = {k: PlanBase(name=k, **settings.PLANS[k]) for k, v in settings.PLANS.items()} -PLAN_CHOICES = [(k, "Every " + p.time_display) for k, p in PLANS.items()] -SUBSCR_PLAN_CHOICES = PLAN_CHOICES - -def period_months(p): - return { - '3m': 3, - '6m': 6, - '12m': 12, - }[p] - - -class BackendData: - backend_data = None - - def set_data(self, key, value): - """ adds a backend data key to this instance's dict """ - if not self.backend_data: - self.backend_data = {} - - if isinstance(self.backend_data, str): - self.backend_data = json.loads(self.backend_data) or {} - - if not isinstance(self.backend_data, dict): - raise Exception("self.backend_data is not a dict (%r)" % self.backend_data) - self.backend_data[key] = value - - -class Payment(models.Model, BackendData): - """ Just a payment. - If subscription is not null, it has been automatically issued. - backend_extid is the external transaction ID, backend_data is other - things that should only be used by the associated backend. - """ - user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) - backend_id = models.CharField(max_length=16, choices=BACKEND_CHOICES) - status = models.CharField(max_length=16, choices=STATUS_CHOICES, default='new') - created = models.DateTimeField(auto_now_add=True) - modified = models.DateTimeField(auto_now=True) - confirmed_on = models.DateTimeField(null=True, blank=True) - amount = models.IntegerField() - paid_amount = models.IntegerField(default=0) - time = models.DurationField() - plan_id = models.CharField(max_length=16, choices=SUBSCR_PLAN_CHOICES) - subscription = models.ForeignKey('Subscription', null=True, blank=True, on_delete=models.CASCADE) - status_message = models.TextField(blank=True, null=True) - ip_address = models.GenericIPAddressField(blank=True, null=True) - - backend_extid = models.CharField(max_length=256, null=True, blank=True) - backend_data = JSONField(blank=True) - - refund_date = models.DateTimeField(blank=True, null=True) - refund_text = models.CharField(max_length=200, blank=True) - - @property - def currency_code(self): - return CURRENCY_CODE - - @property - def currency_name(self): - return CURRENCY_NAME - - @property - def backend(self): - """ Returns a global instance of the backend - :rtype: BackendBase - """ - return BACKENDS[self.backend_id] - - def get_amount_display(self): - return '%.2f %s' % (self.amount / 100, CURRENCY_NAME) - - @property - def is_confirmed(self): - return self.status == 'confirmed' - - def confirm(self): - self.user.vpnuser.add_paid_time(self.time) - self.user.vpnuser.on_payment_confirmed(self) - self.user.vpnuser.save() - self.update_status('confirmed') - - def refund(self): - self.user.vpnuser.remove_paid_time(self.time) - self.user.vpnuser.save() - self.update_status('refunded') - - def update_status(self, status, message=None): - assert any(c[0] == status for c in STATUS_CHOICES) - self.status = status - self.status_message = message - - class Meta: - ordering = ('-created', ) - - @classmethod - def create_payment(self, backend_id, user, months): - payment = Payment( - user=user, - backend_id=backend_id, - status='new', - time=timedelta(days=30 * months), - amount=get_price() * months - ) - return payment - - -class Subscription(models.Model, BackendData): - """ Recurring payment subscription. """ - user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) - backend_id = models.CharField(max_length=16, choices=BACKEND_CHOICES) - created = models.DateTimeField(auto_now_add=True) - plan_id = models.CharField(max_length=16, choices=SUBSCR_PLAN_CHOICES) - next_payment_date = models.DateTimeField(blank=True, null=True) - last_confirmed_payment = models.DateTimeField(blank=True, null=True) - status = models.CharField(max_length=16, choices=SUBSCR_STATUS_CHOICES, default='new') - - backend_extid = models.CharField(max_length=256, null=True, blank=True) - backend_data = JSONField(blank=True) - - @property - def backend(self): - """ Returns a global instance of the backend - :rtype: BackendBase - """ - return BACKENDS[self.backend_id] - - @property - def months(self): - return period_months(self.period) - - @property - def period_amount(self): - return self.months * get_price() - - @property - def next_renew(self): - """ Approximate date of the next payment """ - if self.last_confirmed_payment: - return self.last_confirmed_payment + timedelta(days=self.months * 30) - return self.created + timedelta(days=self.months * 30) - - @property - def monthly_amount(self): - return get_price() - - def create_payment(self): - payment = Payment( - user=self.user, - backend_id=self.backend_id, - status='new', - time=timedelta(days=30 * self.months), - amount=get_price() * self.months, - subscription=self, - ) - return payment - - -class Feedback(models.Model): - user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) - subscription = models.ForeignKey('Subscription', null=True, blank=True, on_delete=models.SET_NULL) - created = models.DateTimeField(auto_now_add=True) - message = models.TextField() - diff --git a/payments/tasks.py b/payments/tasks.py deleted file mode 100644 index 2430f95..0000000 --- a/payments/tasks.py +++ /dev/null @@ -1,33 +0,0 @@ -import logging -from datetime import timedelta -from django.utils import timezone -from celery import task - -from payments.models import Payment -from .models import Payment, Subscription, ACTIVE_BACKENDS - -logger = logging.getLogger(__name__) - -@task -def check_subscriptions(): - logger.debug("checking subscriptions") - subs = Subscription.objects.filter(status='active', backend_id='stripe').all() - for sub in subs: - logger.debug("checking subscription #%s on %s", sub.id, sub.backend_id) - sub.refresh_from_db() - ACTIVE_BACKENDS['stripe'].refresh_subscription(sub) - sub.save() - -@task -def cancel_old_payments(): - expdate = timezone.now() - timedelta(days=3) - - expired = Payment.objects.filter(created__lte=expdate, status='new', - paid_amount=0) - - logger.info("cancelling %d pending payments older than 3 days (%s)", len(expired), expdate.isoformat()) - - for p in expired: - logger.debug("cancelling payment #%d (%s): created on %s", p.id, p.user.username, p.created) - p.status = 'cancelled' - p.save() diff --git a/payments/tests/__init__.py b/payments/tests/__init__.py deleted file mode 100644 index 118c9ab..0000000 --- a/payments/tests/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# flake8: noqa - -from .bitcoin import * -from .paypal import * -from .coingate import * - -from django.conf import settings -if settings.RUN_ONLINE_TESTS: - from .online.stripe import * - diff --git a/payments/tests/bitcoin.py b/payments/tests/bitcoin.py deleted file mode 100644 index 44ec080..0000000 --- a/payments/tests/bitcoin.py +++ /dev/null @@ -1,118 +0,0 @@ -from datetime import timedelta - -from django.test import TestCase -from django.http import HttpResponseRedirect -from django.contrib.auth.models import User -from constance.test import override_config - -from payments.models import Payment -from payments.backends import BitcoinBackend - -from decimal import Decimal - - -class FakeBTCRPCNew: - def getnewaddress(self, account): - return 'TEST_ADDRESS' - - -class FakeBTCRPCUnpaid: - def getreceivedbyaddress(self, address): - assert address == 'TEST_ADDRESS' - return Decimal('0') - - -class FakeBTCRPCPartial: - def getreceivedbyaddress(self, address): - assert address == 'TEST_ADDRESS' - return Decimal('0.5') * 100000000 - - -class FakeBTCRPCPaid: - def getreceivedbyaddress(self, address): - assert address == 'TEST_ADDRESS' - return Decimal('1') * 100000000 - - -class BitcoinBackendTest(TestCase): - def setUp(self): - self.user = User.objects.create_user('test', 'test_user@example.com', None) - - self.p = Payment.objects.create( - user=self.user, time=timedelta(days=30), backend_id='bitcoin', - amount=300) - - @override_config(BTC_EUR_VALUE=300) - def test_new(self): - backend = BitcoinBackend(dict(URL='')) - backend.make_rpc = FakeBTCRPCNew - - backend.new_payment(self.p) - redirect = backend.new_payment(self.p) - self.assertEqual(self.p.backend_extid, 'TEST_ADDRESS') - self.assertEqual(self.p.status, 'new') - self.assertIn('btc_price', self.p.backend_data) - self.assertIn('btc_address', self.p.backend_data) - self.assertEqual(self.p.backend_data['btc_address'], 'TEST_ADDRESS') - self.assertIsInstance(redirect, HttpResponseRedirect) - self.assertEqual(redirect.url, '/payments/view/%d' % self.p.id) - self.assertEqual(self.p.status_message, "Please send 1.00000 BTC to TEST_ADDRESS") - - def test_rounding(self): - """ Rounding test - 300 / 300 = 1 => 1.00000 BTC - 300 / 260 = Decimal('1.153846153846153846153846154') => 1.15385 BTC - """ - with override_config(BTC_EUR_VALUE=300): - backend = BitcoinBackend(dict(URL='')) - backend.make_rpc = FakeBTCRPCNew - backend.new_payment(self.p) - self.assertEqual(self.p.status_message, "Please send 1.00000 BTC to TEST_ADDRESS") - - with override_config(BTC_EUR_VALUE=260): - backend = BitcoinBackend(dict(URL='')) - backend.make_rpc = FakeBTCRPCNew - backend.new_payment(self.p) - self.assertEqual(self.p.status_message, "Please send 1.15385 BTC to TEST_ADDRESS") - - -class BitcoinBackendConfirmTest(TestCase): - @override_config(BTC_EUR_VALUE=300) - def setUp(self): - self.user = User.objects.create_user('test', 'test_user@example.com', None) - - self.p = Payment.objects.create( - user=self.user, time=timedelta(days=30), backend_id='bitcoin', - amount=300) - - # call new_payment - backend = BitcoinBackend(dict(URL='')) - backend.make_rpc = FakeBTCRPCNew - backend.new_payment(self.p) - - @override_config(BTC_EUR_VALUE=300) - def test_check_unpaid(self): - backend = BitcoinBackend(dict(URL='')) - backend.make_rpc = FakeBTCRPCUnpaid - - backend.check(self.p) - self.assertEqual(self.p.status, 'new') - self.assertEqual(self.p.paid_amount, 0) - - @override_config(BTC_EUR_VALUE=300) - def test_check_partially_paid(self): - backend = BitcoinBackend(dict(URL='')) - backend.make_rpc = FakeBTCRPCPartial - backend.check(self.p) - self.assertEqual(self.p.status, 'new') - self.assertEqual(self.p.paid_amount, 150) - - @override_config(BTC_EUR_VALUE=300) - def test_check_paid(self): - backend = BitcoinBackend(dict(URL='')) - backend.make_rpc = FakeBTCRPCPaid - backend.check(self.p) - self.assertEqual(self.p.paid_amount, 300) - self.assertEqual(self.p.status, 'confirmed') - - diff --git a/payments/tests/coingate.py b/payments/tests/coingate.py deleted file mode 100644 index 3c9150d..0000000 --- a/payments/tests/coingate.py +++ /dev/null @@ -1,75 +0,0 @@ -from datetime import timedelta - -from django.test import TestCase, RequestFactory -from django.http import HttpResponseRedirect -from django.contrib.auth.models import User - -from payments.models import Payment -from payments.backends import CoinGateBackend - - -class CoinGateBackendTest(TestCase): - def setUp(self): - self.user = User.objects.create_user('test', 'test_user@example.com', None) - - self.backend_settings = dict( - api_token='test', - title='Test Title', - currency='EUR', - ) - - def test_payment(self): - payment = Payment.objects.create( - user=self.user, - time=timedelta(days=30), - backend_id='coingate', - amount=300 - ) - - def fake_post(_backend, *, data={}): - self.assertEqual(data['order_id'], '1') - self.assertEqual(data['price_amount'], 3.0) - self.assertEqual(data['price_currency'], 'EUR') - self.assertEqual(data['receive_currency'], 'EUR') - return {'id': 42, 'payment_url': 'http://testtoken/'} - - with self.settings(ROOT_URL='root'): - backend = CoinGateBackend(self.backend_settings) - backend._post = fake_post - redirect = backend.new_payment(payment) - - self.assertIsInstance(redirect, HttpResponseRedirect) - self.assertEqual(redirect.url, 'http://testtoken/') - self.assertEqual(payment.backend_data.get('coingate_id'), 42) - - - # Test a standard successful payment callback flow - - def post_callback(status): - callback_data = { - 'token': payment.backend_data['coingate_token'], - 'order_id': str(payment.id), - 'status': status, - } - ipn_url = '/payments/callback/coingate/%d' % payment.id - ipn_request = RequestFactory().post( - ipn_url, - data=callback_data) - return backend.callback(payment, ipn_request) - - r = post_callback('pending') - self.assertTrue(r) - self.assertEqual(payment.status, 'new') - - r = post_callback('confirming') - self.assertTrue(r) - self.assertEqual(payment.status, 'new') - - r = post_callback('paid') - self.assertTrue(r) - self.assertEqual(payment.status, 'confirmed') - self.assertEqual(payment.paid_amount, 300) - - time_left_s = self.user.vpnuser.time_left.total_seconds() - self.assertAlmostEqual(time_left_s, payment.time.total_seconds(), delta=60) - diff --git a/payments/tests/paypal.py b/payments/tests/paypal.py deleted file mode 100644 index 96dcda4..0000000 --- a/payments/tests/paypal.py +++ /dev/null @@ -1,325 +0,0 @@ -from datetime import timedelta -from urllib.parse import parse_qs - -from django.test import TestCase, RequestFactory -from django.http import HttpResponseRedirect -from django.contrib.auth.models import User - -from payments.models import Payment, Subscription -from payments.backends import PaypalBackend - - -PAYPAL_IPN_TEST = '''\ -mc_gross=3.00&\ -protection_eligibility=Eligible&\ -address_status=confirmed&\ -payer_id=LPLWNMTBWMFAY&\ -tax=0.00&\ -address_street=1+Main+St&\ -payment_date=20%3A12%3A59+Jan+13%2C+2009+PST&\ -payment_status=Completed&\ -charset=windows-1252&\ -address_zip=95131&\ -first_name=Test&\ -mc_fee=0.88&\ -address_country_code=US&\ -address_name=Test+User&\ -notify_version=2.6&\ -custom=&\ -payer_status=verified&\ -address_country=United+States&\ -address_city=San+Jose&\ -quantity=1&\ -payer_email=test_user@example.com&\ -txn_id=61E67681CH3238416&\ -payment_type=instant&\ -last_name=User&\ -address_state=CA&\ -receiver_email=test_business@example.com&\ -payment_fee=0.88&\ -receiver_id=S8XGHLYDW9T3S&\ -txn_type=express_checkout&\ -item_name=&\ -mc_currency=EUR&\ -item_number=&\ -residence_country=US&\ -test_ipn=1&\ -handling_amount=0.00&\ -transaction_subject=&\ -payment_gross=3.00&\ -shipping=0.00''' - -PAYPAL_IPN_SUBSCR_PAYMENT = '''\ -transaction_subject=VPN+Payment&\ -payment_date=11%3A19%3A00+Sep+04%2C+2016+PDT&\ -txn_type=subscr_payment&\ -subscr_id=I-1S262863X133&\ -last_name=buyer&\ -residence_country=FR&\ -item_name=VPN+Payment&\ -payment_gross=&\ -mc_currency=EUR&\ -business=test_business@example.com&\ -payment_type=instant&\ -protection_eligibility=Ineligible&\ -payer_status=verified&\ -test_ipn=1&\ -payer_email=test_user@example.com&\ -txn_id=097872679P963871Y&\ -receiver_email=test_business@example.com&\ -first_name=test&\ -payer_id=APYYVSFLNPWUU&\ -receiver_id=MGT8TQ8GC4944&\ -payment_status=Completed&\ -payment_fee=&\ -mc_fee=0.56&\ -mc_gross=9.00&\ -charset=windows-1252&\ -notify_version=3.8&\ -ipn_track_id=546a4aa4300a0''' - - -PAYPAL_IPN_SUBSCR_CANCEL = '''\ -txn_type=subscr_cancel&\ -subscr_id=I-E5SCT6936H40&\ -last_name=buyer&\ -residence_country=FR&\ -mc_currency=EUR&\ -item_name=VPN+Payment&\ -business=test_business@example.com&\ -recurring=1&\ -payer_status=verified&\ -test_ipn=1&\ -payer_email=test_user@example.com&\ -first_name=test&\ -receiver_email=test_business@example.com&\ -payer_id=APYYVSFLNPWUU&\ -reattempt=1&\ -subscr_date=17%3A35%3A14+Sep+04%2C+2016+PDT&\ -charset=windows-1252&\ -notify_version=3.8&\ -period3=3+M&\ -mc_amount3=9.00&\ -ipn_track_id=474870d13b375''' - - -PAYPAL_IPN_SUBSCR_SIGNUP = '''\ -txn_type=subscr_signup&\ -subscr_id=I-1S262863X133&\ -last_name=buyer&\ -residence_country=FR&\ -mc_currency=EUR&\ -item_name=VPN+Payment&\ -business=test_business@example.com&\ -recurring=1&\ -payer_status=verified&\ -test_ipn=1&\ -payer_email=test_user@example.com&\ -first_name=test&\ -receiver_email=test_business@example.com&\ -payer_id=APYYVSFLNPWUU&\ -reattempt=1&\ -subscr_date=11%3A18%3A57+Sep+04%2C+2016+PDT&\ -charset=windows-1252&\ -notify_version=3.8&\ -period3=3+M&\ -mc_amount3=9.00&\ -ipn_track_id=546a4aa4300a0''' - - -class PaypalBackendTest(TestCase): - def setUp(self): - self.user = User.objects.create_user('test', 'test_user@example.com', None) - - def test_paypal(self): - # TODO: This checks the most simple and perfect payment that could - # happen, but not errors or other/invalid IPN - - payment = Payment.objects.create( - user=self.user, - time=timedelta(days=30), - backend_id='paypal', - amount=300 - ) - - settings = dict( - test=True, - title='Test Title', - currency='EUR', - address='test_business@example.com', - ) - - with self.settings(ROOT_URL='root'): - backend = PaypalBackend(settings) - redirect = backend.new_payment(payment) - - self.assertIsInstance(redirect, HttpResponseRedirect) - - host, params = redirect.url.split('?', 1) - params = parse_qs(params) - - expected_notify_url = 'root/payments/callback/paypal/%d' % payment.id - expected_return_url = 'root/payments/view/%d' % payment.id - expected_cancel_url = 'root/payments/cancel/%d' % payment.id - - self.assertEqual(params['cmd'][0], '_xclick') - self.assertEqual(params['notify_url'][0], expected_notify_url) - self.assertEqual(params['return'][0], expected_return_url) - self.assertEqual(params['cancel_return'][0], expected_cancel_url) - self.assertEqual(params['business'][0], 'test_business@example.com') - self.assertEqual(params['currency_code'][0], 'EUR') - self.assertEqual(params['amount'][0], '3.00') - self.assertEqual(params['item_name'][0], 'Test Title') - - # Replace PaypalBackend.verify_ipn to not call the PayPal API - # we will assume the IPN is authentic - backend.verify_ipn = lambda request: True - - ipn_url = '/payments/callback/paypal/%d' % payment.id - ipn_request = RequestFactory().post( - ipn_url, - content_type='application/x-www-form-urlencoded', - data=PAYPAL_IPN_TEST) - r = backend.callback(payment, ipn_request) - - self.assertTrue(r) - self.assertEqual(payment.status, 'confirmed') - self.assertEqual(payment.paid_amount, 300) - self.assertEqual(payment.backend_extid, '61E67681CH3238416') - - def test_paypal_ipn_error(self): - payment = Payment.objects.create( - user=self.user, - time=timedelta(days=30), - backend_id='paypal', - amount=300 - ) - - settings = dict( - test=True, - title='Test Title', - currency='EUR', - address='test_business@example.com', - ) - - with self.settings(ROOT_URL='root'): - backend = PaypalBackend(settings) - redirect = backend.new_payment(payment) - - self.assertIsInstance(redirect, HttpResponseRedirect) - - host, params = redirect.url.split('?', 1) - params = parse_qs(params) - - # Replace PaypalBackend.verify_ipn to not call the PayPal API - # we will assume the IPN is authentic - backend.verify_ipn = lambda request: True - - ipn_url = '/payments/callback/paypal/%d' % payment.id - ipn_request = RequestFactory().post( - ipn_url, - content_type='application/x-www-form-urlencoded', - data=PAYPAL_IPN_TEST) - r = backend.callback(payment, ipn_request) - - self.assertTrue(r) - self.assertEqual(payment.status, 'confirmed') - self.assertEqual(payment.paid_amount, 300) - self.assertEqual(payment.backend_extid, '61E67681CH3238416') - - def test_paypal_subscr(self): - subscription = Subscription.objects.create( - user=self.user, - backend_id='paypal', - period='3m' - ) - - settings = dict( - test=True, - title='Test Title', - currency='EUR', - address='test_business@example.com', - ) - - with self.settings(ROOT_URL='root'): - backend = PaypalBackend(settings) - redirect = backend.new_subscription(subscription) - - self.assertIsInstance(redirect, HttpResponseRedirect) - - host, params = redirect.url.split('?', 1) - params = parse_qs(params) - - expected_notify_url = 'root/payments/callback/paypal_subscr/%d' % subscription.id - expected_return_url = 'root/payments/return_subscr/%d' % subscription.id - expected_cancel_url = 'root/account/' - - self.assertEqual(params['cmd'][0], '_xclick-subscriptions') - self.assertEqual(params['notify_url'][0], expected_notify_url) - self.assertEqual(params['return'][0], expected_return_url) - self.assertEqual(params['cancel_return'][0], expected_cancel_url) - self.assertEqual(params['business'][0], 'test_business@example.com') - self.assertEqual(params['currency_code'][0], 'EUR') - self.assertEqual(params['a3'][0], '9.00') - self.assertEqual(params['p3'][0], '3') - self.assertEqual(params['t3'][0], 'M') - self.assertEqual(params['item_name'][0], 'Test Title') - - # Replace PaypalBackend.verify_ipn to not call the PayPal API - # we will assume the IPN is authentic - backend.verify_ipn = lambda request: True - - self.assertEqual(subscription.status, 'new') - - # 1. the subscr_payment IPN - ipn_url = '/payments/callback/paypal_subscr/%d' % subscription.id - ipn_request = RequestFactory().post( - ipn_url, - content_type='application/x-www-form-urlencoded', - data=PAYPAL_IPN_SUBSCR_PAYMENT) - r = backend.callback_subscr(subscription, ipn_request) - - self.assertTrue(r) - self.assertEqual(subscription.status, 'active') - self.assertEqual(subscription.backend_extid, 'I-1S262863X133') - - payments = Payment.objects.filter(subscription=subscription).all() - self.assertEqual(len(payments), 1) - self.assertEqual(payments[0].amount, 900) - self.assertEqual(payments[0].paid_amount, 900) - self.assertEqual(payments[0].backend_extid, '097872679P963871Y') - - # 2. the subscr_signup IPN - # We don't expect anything to happen here - ipn_url = '/payments/callback/paypal_subscr/%d' % subscription.id - ipn_request = RequestFactory().post( - ipn_url, - content_type='application/x-www-form-urlencoded', - data=PAYPAL_IPN_SUBSCR_SIGNUP) - r = backend.callback_subscr(subscription, ipn_request) - - self.assertTrue(r) - self.assertEqual(subscription.status, 'active') - self.assertEqual(subscription.backend_extid, 'I-1S262863X133') - - payments = Payment.objects.filter(subscription=subscription).all() - self.assertEqual(len(payments), 1) - self.assertEqual(payments[0].amount, 900) - self.assertEqual(payments[0].paid_amount, 900) - self.assertEqual(payments[0].backend_extid, '097872679P963871Y') - - # 3. the subscr_cancel IPN - ipn_url = '/payments/callback/paypal_subscr/%d' % subscription.id - ipn_request = RequestFactory().post( - ipn_url, - content_type='application/x-www-form-urlencoded', - data=PAYPAL_IPN_SUBSCR_CANCEL) - r = backend.callback_subscr(subscription, ipn_request) - - self.assertTrue(r) - self.assertEqual(subscription.status, 'cancelled') - self.assertEqual(subscription.backend_extid, 'I-1S262863X133') - - payments = Payment.objects.filter(subscription=subscription).all() - self.assertEqual(len(payments), 1) - diff --git a/payments/urls.py b/payments/urls.py deleted file mode 100644 index 2567584..0000000 --- a/payments/urls.py +++ /dev/null @@ -1,23 +0,0 @@ -from django.conf.urls import url -from . import views - -app_name = 'payments' - -urlpatterns = [ - url(r'^new$', views.new), - url(r'^view/(?P[0-9]+)$', views.view, name='view'), - url(r'^cancel/(?P[0-9]+)$', views.cancel, name='cancel'), - url(r'^cancel_subscr/(?P[0-9]+)?$', views.cancel_subscr, name='cancel_subscr'), - url(r'^return_subscr/(?P[0-9]+)$', views.return_subscr, name='return_subscr'), - - url(r'^callback/paypal/(?P[0-9]+)$', views.payment_callback('paypal'), name='cb_paypal'), - url(r'^callback/coingate/(?P[0-9]+)$', views.payment_callback('coingate'), name='cb_coingate'), - url(r'^callback/stripe/(?P[0-9]+)$', views.payment_callback('stripe'), name='cb_stripe'), - url(r'^callback/coinbase/$', views.plain_callback('coinbase'), name='cb_coinbase'), - url(r'^callback/coinpayments/(?P[0-9]+)$', views.payment_callback('coinpayments'), name='cb_coinpayments'), - url(r'^callback/paypal_subscr/(?P[0-9]+)$', views.sub_callback('paypal'), name='cb_paypal_subscr'), - - url(r'^callback/stripe_hook$', views.stripe_hook, name='stripe_hook'), - - url(r'^$', views.list_payments), -] diff --git a/payments/views.py b/payments/views.py deleted file mode 100644 index 07aff5f..0000000 --- a/payments/views.py +++ /dev/null @@ -1,189 +0,0 @@ -from datetime import timedelta -from django.shortcuts import render, redirect -from django.urls import reverse -from django.conf import settings -from django.contrib.auth.decorators import login_required -from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotFound, Http404 -from django.views.decorators.csrf import csrf_exempt -from django.utils import timezone -from django.contrib import messages -from django.utils.translation import ugettext_lazy as _ - -from .forms import NewPaymentForm -from .models import Payment, Subscription, BACKENDS, ACTIVE_BACKENDS, Feedback - - -def require_backend(name): - backend = BACKENDS.get(name) - if not backend: - raise Http404() - if not backend.backend_enabled: - raise Http404() - return backend - - -@login_required -def new(request): - if request.method != 'POST': - return redirect('account:index') - - if Payment.objects.filter(user=request.user, status='new').count() > 10: - messages.error(request, "Too many open payments.") - return redirect('account:index') - - form = NewPaymentForm(request.POST) - - if not form.is_valid(): - return redirect('account:index') - - if request.user.vpnuser.get_subscription() is not None: - return redirect('account:index') - - subscr = form.cleaned_data['subscr'] == '1' - backend_id = form.cleaned_data['method'] - months = int(form.cleaned_data['time']) - - if backend_id not in ACTIVE_BACKENDS: - return HttpResponseNotFound() - - if subscr: - if months not in (3, 6, 12): - return redirect('account:index') - - rps = Subscription( - user=request.user, - backend_id=backend_id, - period=str(months) + 'm', - ) - rps.save() - - r = rps.backend.new_subscription(rps) - - else: - payment = Payment.create_payment(backend_id, request.user, months) - payment.save() - - r = payment.backend.new_payment(payment) - - if not r: - payment.status = 'error' - payment.save() - raise Exception("Failed to initialize payment #%d" % payment.id) - - if isinstance(r, str): - return render(request, 'payments/form.html', dict(html=r)) - elif r is None: - return redirect('payments:view', payment.id) - - return r - -def plain_callback(backend_name, method='callback'): - @csrf_exempt - def callback(request): - backend = require_backend(backend_name) - - m = getattr(backend, method) - if m and m(Payment, request): - return HttpResponse() - else: - return HttpResponseBadRequest() - - return callback - -def payment_callback(backend_name): - @csrf_exempt - def callback(request, id): - backend = require_backend(backend_name) - p = Payment.objects.get(id=id) - - if backend.callback(p, request): - return HttpResponse() - else: - return HttpResponseBadRequest() - - return callback - -def sub_callback(backend_name): - @csrf_exempt - def callback(request, id): - backend = require_backend(backend_name) - - p = Subscription.objects.get(id=id) - - if backend.callback_subscr(p, request): - return HttpResponse() - else: - return HttpResponseBadRequest() - - return callback - -@csrf_exempt -def stripe_hook(request): - backend = require_backend('stripe') - - if backend.webhook(request): - return HttpResponse() - else: - return HttpResponseBadRequest() - - -@login_required -@csrf_exempt -def view(request, id): - p = Payment.objects.get(id=id, user=request.user) - return render(request, 'payments/view.html', dict(payment=p)) - - -@login_required -def cancel(request, id): - p = Payment.objects.get(id=id, user=request.user) - if p.status == 'new': - p.status = 'cancelled' - p.save() - return render(request, 'payments/view.html', dict(payment=p)) - - -@login_required -def cancel_subscr(request, id=None): - if request.method == 'POST' and id: - p = Subscription.objects.get(id=id, user=request.user) - - # Saving any feedback note - feedback = request.POST.get('feedback') - if feedback: - feedback = feedback[:10000] - f = Feedback(user=request.user, subscription=p, message=feedback) - f.save() - - try: - if p.backend.cancel_subscription(p): - messages.add_message(request, messages.INFO, _("Subscription cancelled!")) - else: - messages.add_message(request, messages.ERROR, _("Could not cancel the subscription. It may have already been cancelled or caused an error.")) - except NotImplementedError: - pass - return redirect('account:index') - - subscription = request.user.vpnuser.get_subscription(include_unconfirmed=True) - return render(request, 'payments/cancel_subscr.html', { - 'subscription': subscription, - }) - - -@login_required -def return_subscr(request, id): - p = Subscription.objects.get(id=id, user=request.user) - if p.status == 'new': - p.status = 'unconfirmed' - p.save() - return redirect('account:index') - - -@login_required -def list_payments(request): - # Only show recent cancelled payments - cancelled_limit = timezone.now() - timedelta(days=3) - - objects = request.user.payment_set.exclude(status='cancelled', - created__lte=cancelled_limit) - return render(request, 'payments/list.html', dict(payments=objects)) diff --git a/poetry.lock b/poetry.lock index 311bdbe..e5d9024 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,13 +1,13 @@ [[package]] name = "amqp" -version = "2.6.1" +version = "5.0.6" description = "Low-level AMQP client for Python (fork of amqplib)." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.6" [package.dependencies] -vine = ">=1.1.3,<5.0.0a1" +vine = "5.0.0" [[package]] name = "appdirs" @@ -19,7 +19,7 @@ python-versions = "*" [[package]] name = "asgiref" -version = "3.3.4" +version = "3.4.1" description = "ASGI specs, helper code, and adapters" category = "main" optional = false @@ -33,7 +33,7 @@ tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] [[package]] name = "astroid" -version = "2.5.6" +version = "2.6.5" description = "An abstract syntax tree for Python with inference support." category = "dev" optional = false @@ -42,6 +42,7 @@ python-versions = "~=3.6" [package.dependencies] lazy-object-proxy = ">=1.4.0" typed-ast = {version = ">=1.4.0,<1.5", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""} +typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} wrapt = ">=1.11,<1.13" [[package]] @@ -75,44 +76,55 @@ typing-extensions = ">=3.7.4" colorama = ["colorama (>=0.4.3)"] d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] +[[package]] +name = "cached-property" +version = "1.5.2" +description = "A decorator for caching properties in classes." +category = "main" +optional = false +python-versions = "*" + [[package]] name = "celery" -version = "4.4.7" +version = "5.1.2" description = "Distributed Task Queue." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.6," [package.dependencies] -billiard = ">=3.6.3.0,<4.0" -kombu = ">=4.6.10,<4.7" +billiard = ">=3.6.4.0,<4.0" +click = ">=7.0,<8.0" +click-didyoumean = ">=0.0.3" +click-plugins = ">=1.1.1" +click-repl = ">=0.1.6" +kombu = ">=5.1.0,<6.0" pytz = ">0.0-dev" -vine = "1.3.0" +vine = ">=5.0.0,<6.0" [package.extras] arangodb = ["pyArango (>=1.3.2)"] auth = ["cryptography"] -azureblockblob = ["azure-storage (==0.36.0)", "azure-common (==1.1.5)", "azure-storage-common (==1.1.0)"] +azureblockblob = ["azure-storage-blob (==12.6.0)"] brotli = ["brotli (>=1.0.0)", "brotlipy (>=0.7.0)"] cassandra = ["cassandra-driver (<3.21.0)"] -consul = ["python-consul"] +consul = ["python-consul2"] cosmosdbsql = ["pydocumentdb (==2.3.2)"] -couchbase = ["couchbase-cffi (<3.0.0)", "couchbase (<3.0.0)"] +couchbase = ["couchbase (>=3.0.0)"] couchdb = ["pycouchdb"] django = ["Django (>=1.11)"] dynamodb = ["boto3 (>=1.9.178)"] elasticsearch = ["elasticsearch"] -eventlet = ["eventlet (>=0.24.1)"] -gevent = ["gevent"] +eventlet = ["eventlet (>=0.26.1)"] +gevent = ["gevent (>=1.0.0)"] librabbitmq = ["librabbitmq (>=1.5.0)"] -lzma = ["backports.lzma"] memcache = ["pylibmc"] mongodb = ["pymongo[srv] (>=3.3.0)"] msgpack = ["msgpack"] pymemcache = ["python-memcached"] pyro = ["pyro4"] +pytest = ["pytest-celery"] redis = ["redis (>=3.2.0)"] -riak = ["riak (>=2.0)"] s3 = ["boto3 (>=1.9.125)"] slmq = ["softlayer-messaging (>=1.0.3)"] solar = ["ephem"] @@ -132,24 +144,61 @@ optional = false python-versions = "*" [[package]] -name = "chardet" -version = "4.0.0" -description = "Universal encoding detector for Python 2 and 3" +name = "charset-normalizer" +version = "2.0.3" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] [[package]] name = "click" -version = "8.0.1" +version = "7.1.2" description = "Composable command line interface toolkit" -category = "dev" +category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "click-didyoumean" +version = "0.0.3" +description = "Enable git-like did-you-mean feature in click." +category = "main" +optional = false +python-versions = "*" [package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +click = "*" + +[[package]] +name = "click-plugins" +version = "1.1.1" +description = "An extension module for click to enable registering CLI commands via setuptools entry-points." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +click = ">=4.0" + +[package.extras] +dev = ["pytest (>=3.6)", "pytest-cov", "wheel", "coveralls"] + +[[package]] +name = "click-repl" +version = "0.2.0" +description = "REPL plugin for Click" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +click = "*" +prompt-toolkit = "*" +six = "*" [[package]] name = "colorama" @@ -169,7 +218,7 @@ python-versions = ">=3.6, <3.7" [[package]] name = "django" -version = "3.2.4" +version = "3.2.5" description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." category = "main" optional = false @@ -194,29 +243,18 @@ python-versions = "*" [[package]] name = "django-celery-beat" -version = "2.2.0" +version = "2.2.1" description = "Database-backed Periodic Tasks." category = "main" optional = false python-versions = "*" [package.dependencies] -celery = ">=4.4,<6.0" +celery = ">=5.0,<6.0" Django = ">=2.2,<4.0" django-timezone-field = ">=4.1.0,<5.0" python-crontab = ">=2.3.4" -[[package]] -name = "django-celery-results" -version = "1.2.1" -description = "Celery result backends for Django." -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -celery = ">=4.4,<5.0" - [[package]] name = "django-constance" version = "2.8.0" @@ -246,6 +284,18 @@ maintainer = ["transifex-client", "zest.releaser", "django"] pyuca = ["pyuca"] test = ["pytest", "pytest-django", "pytest-cov", "graphene-django"] +[[package]] +name = "django-jsonfield" +version = "1.4.1" +description = "JSONField for django models" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +Django = ">=1.11" +six = "*" + [[package]] name = "django-lcore" version = "1.7.1" @@ -279,9 +329,33 @@ Django = ">=2.2" [package.extras] tests = ["tox"] +[[package]] +name = "django-pipayments" +version = "0.2.0" +description = "" +category = "main" +optional = false +python-versions = "^3.6" +develop = false + +[package.dependencies] +celery = "^5" +Django = "^3.1" +django-constance = "^2.4" +django-jsonfield = "^1.3" +python-dateutil = "^2.8.1" +requests = "^2.22" +stripe = "^2.40" + +[package.source] +type = "git" +url = "git@git.packetimpact.net:lvpn/django-pipayments.git" +reference = "main" +resolved_reference = "31315e7f3e6e0ea84b00e1cdfa6a7cd6550b4fed" + [[package]] name = "django-timezone-field" -version = "4.1.2" +version = "4.2.1" description = "A Django app providing database and form fields for pytz timezone objects." category = "main" optional = false @@ -308,23 +382,25 @@ jsmin = "*" [[package]] name = "flower" -version = "0.9.7" +version = "0.9.5" description = "Celery Flower" category = "main" optional = false python-versions = "*" [package.dependencies] -celery = {version = ">=4.3.0,<5.0.0", markers = "python_version >= \"3.7\""} +celery = [ + {version = ">=3.1.0", markers = "python_version < \"3.7\""}, + {version = ">=4.3.0", markers = "python_version >= \"3.7\""}, +] humanize = "*" prometheus-client = "0.8.0" pytz = "*" tornado = {version = ">=5.0.0,<7.0.0", markers = "python_version >= \"3.5.2\""} -vine = "1.3.0" [[package]] name = "humanize" -version = "3.7.0" +version = "3.10.0" description = "Python humanize utilities" category = "main" optional = false @@ -335,15 +411,15 @@ tests = ["freezegun", "pytest", "pytest-cov"] [[package]] name = "idna" -version = "2.10" +version = "3.2" description = "Internationalized Domain Names in Applications (IDNA)" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.5" [[package]] name = "importlib-metadata" -version = "4.4.0" +version = "4.6.1" description = "Read metadata from Python packages" category = "main" optional = false @@ -355,7 +431,8 @@ zipp = ">=0.5" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +perf = ["ipython"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] [[package]] name = "isort" @@ -391,18 +468,20 @@ Django = ">=2.2" [[package]] name = "kombu" -version = "4.6.11" +version = "5.1.0" description = "Messaging library for Python." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.6" [package.dependencies] -amqp = ">=2.6.0,<2.7" +amqp = ">=5.0.6,<6.0.0" +cached-property = {version = "*", markers = "python_version < \"3.8\""} importlib-metadata = {version = ">=0.18", markers = "python_version < \"3.8\""} +vine = "*" [package.extras] -azureservicebus = ["azure-servicebus (>=0.21.1)"] +azureservicebus = ["azure-servicebus (>=7.0.0)"] azurestoragequeues = ["azure-storage-queue"] consul = ["python-consul (>=0.6.0)"] librabbitmq = ["librabbitmq (>=1.5.2)"] @@ -413,7 +492,7 @@ qpid = ["qpid-python (>=0.26)", "qpid-tools (>=0.26)"] redis = ["redis (>=3.3.11)"] slmq = ["softlayer-messaging (>=1.0.3)"] sqlalchemy = ["sqlalchemy"] -sqs = ["boto3 (>=1.4.4)", "pycurl (==7.43.0.2)"] +sqs = ["boto3 (>=1.4.4)", "pycurl (==7.43.0.2)", "urllib3 (<1.26)"] yaml = ["PyYAML (>=3.10)"] zookeeper = ["kazoo (>=1.3.1)"] @@ -475,11 +554,11 @@ python-versions = "*" [[package]] name = "pathspec" -version = "0.8.1" +version = "0.9.0" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [[package]] name = "prometheus-client" @@ -492,13 +571,24 @@ python-versions = "*" [package.extras] twisted = ["twisted"] +[[package]] +name = "prompt-toolkit" +version = "3.0.3" +description = "Library for building powerful interactive command lines in Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +wcwidth = "*" + [[package]] name = "psycopg2-binary" -version = "2.8.6" +version = "2.9.1" description = "psycopg2 - Python-PostgreSQL Database Adapter" category = "main" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +python-versions = ">=3.6" [[package]] name = "pygal" @@ -524,14 +614,14 @@ python-versions = ">=3.5" [[package]] name = "pylint" -version = "2.8.3" +version = "2.9.5" description = "python code static checker" category = "dev" optional = false python-versions = "~=3.6" [package.dependencies] -astroid = "2.5.6" +astroid = ">=2.6.5,<2.7" colorama = {version = "*", markers = "sys_platform == \"win32\""} isort = ">=4.2.5,<6" mccabe = ">=0.6,<0.7" @@ -589,7 +679,7 @@ cron-schedule = ["croniter"] [[package]] name = "python-dateutil" -version = "2.8.1" +version = "2.8.2" description = "Extensions to the standard Python datetime module" category = "main" optional = false @@ -642,7 +732,7 @@ hiredis = ["hiredis (>=0.1.3)"] [[package]] name = "regex" -version = "2021.4.4" +version = "2021.7.6" description = "Alternative regular expression module, to replace re." category = "dev" optional = false @@ -650,21 +740,21 @@ python-versions = "*" [[package]] name = "requests" -version = "2.25.1" +version = "2.26.0" description = "Python HTTP for Humans." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [package.dependencies] certifi = ">=2017.4.17" -chardet = ">=3.0.2,<5" -idna = ">=2.5,<3" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} urllib3 = ">=1.21.1,<1.27" [package.extras] -security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] [[package]] name = "selenium" @@ -695,7 +785,7 @@ python-versions = ">=3.5" [[package]] name = "stripe" -version = "2.57.0" +version = "2.60.0" description = "Python bindings for the Stripe API" category = "main" optional = false @@ -738,7 +828,7 @@ python-versions = "*" [[package]] name = "urllib3" -version = "1.26.5" +version = "1.26.6" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false @@ -751,11 +841,19 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "vine" -version = "1.3.0" +version = "5.0.0" description = "Promises, promises, promises." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6" + +[[package]] +name = "wcwidth" +version = "0.2.5" +description = "Measures the displayed width of unicode strings in a terminal" +category = "main" +optional = false +python-versions = "*" [[package]] name = "wrapt" @@ -767,7 +865,7 @@ python-versions = "*" [[package]] name = "zipp" -version = "3.4.1" +version = "3.5.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false @@ -775,29 +873,29 @@ python-versions = ">=3.6" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "1eb80ae7794ce4aa0c8fb6f938102718032bfe55f28677213e3c04adf78b7ed5" +content-hash = "3684444cfa9713769dfe63acf8eed5bfe8a25066e7ca312461498c82c444de9f" [metadata.files] amqp = [ - {file = "amqp-2.6.1-py2.py3-none-any.whl", hash = "sha256:aa7f313fb887c91f15474c1229907a04dac0b8135822d6603437803424c0aa59"}, - {file = "amqp-2.6.1.tar.gz", hash = "sha256:70cdb10628468ff14e57ec2f751c7aa9e48e7e3651cfd62d431213c0c4e58f21"}, + {file = "amqp-5.0.6-py3-none-any.whl", hash = "sha256:493a2ac6788ce270a2f6a765b017299f60c1998f5a8617908ee9be082f7300fb"}, + {file = "amqp-5.0.6.tar.gz", hash = "sha256:03e16e94f2b34c31f8bf1206d8ddd3ccaa4c315f7f6a1879b7b1210d229568c2"}, ] appdirs = [ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, ] asgiref = [ - {file = "asgiref-3.3.4-py3-none-any.whl", hash = "sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee"}, - {file = "asgiref-3.3.4.tar.gz", hash = "sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78"}, + {file = "asgiref-3.4.1-py3-none-any.whl", hash = "sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214"}, + {file = "asgiref-3.4.1.tar.gz", hash = "sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9"}, ] astroid = [ - {file = "astroid-2.5.6-py3-none-any.whl", hash = "sha256:4db03ab5fc3340cf619dbc25e42c2cc3755154ce6009469766d7143d1fc2ee4e"}, - {file = "astroid-2.5.6.tar.gz", hash = "sha256:8a398dfce302c13f14bab13e2b14fe385d32b73f4e4853b9bdfb64598baa1975"}, + {file = "astroid-2.6.5-py3-none-any.whl", hash = "sha256:7b963d1c590d490f60d2973e57437115978d3a2529843f160b5003b721e1e925"}, + {file = "astroid-2.6.5.tar.gz", hash = "sha256:83e494b02d75d07d4e347b27c066fd791c0c74fc96c613d1ea3de0c82c48168f"}, ] billiard = [ {file = "billiard-3.6.4.0-py3-none-any.whl", hash = "sha256:87103ea78fa6ab4d5c751c4909bcff74617d985de7fa8b672cf8618afd5a875b"}, @@ -807,21 +905,36 @@ black = [ {file = "black-20.8b1-py3-none-any.whl", hash = "sha256:70b62ef1527c950db59062cda342ea224d772abdf6adc58b86a45421bab20a6b"}, {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, ] +cached-property = [ + {file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"}, + {file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"}, +] celery = [ - {file = "celery-4.4.7-py2.py3-none-any.whl", hash = "sha256:a92e1d56e650781fb747032a3997d16236d037c8199eacd5217d1a72893bca45"}, - {file = "celery-4.4.7.tar.gz", hash = "sha256:d220b13a8ed57c78149acf82c006785356071844afe0b27012a4991d44026f9f"}, + {file = "celery-5.1.2-py3-none-any.whl", hash = "sha256:9dab2170b4038f7bf10ef2861dbf486ddf1d20592290a1040f7b7a1259705d42"}, + {file = "celery-5.1.2.tar.gz", hash = "sha256:8d9a3de9162965e97f8e8cc584c67aad83b3f7a267584fa47701ed11c3e0d4b0"}, ] certifi = [ {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, ] -chardet = [ - {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, - {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, +charset-normalizer = [ + {file = "charset-normalizer-2.0.3.tar.gz", hash = "sha256:c46c3ace2d744cfbdebceaa3c19ae691f53ae621b39fd7570f59d14fb7f2fd12"}, + {file = "charset_normalizer-2.0.3-py3-none-any.whl", hash = "sha256:88fce3fa5b1a84fdcb3f603d889f723d1dd89b26059d0123ca435570e848d5e1"}, ] click = [ - {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, - {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, + {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, + {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, +] +click-didyoumean = [ + {file = "click-didyoumean-0.0.3.tar.gz", hash = "sha256:112229485c9704ff51362fe34b2d4f0b12fc71cc20f6d2b3afabed4b8bfa6aeb"}, +] +click-plugins = [ + {file = "click-plugins-1.1.1.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b"}, + {file = "click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8"}, +] +click-repl = [ + {file = "click-repl-0.2.0.tar.gz", hash = "sha256:cd12f68d745bf6151210790540b4cb064c7b13e571bc64b6957d98d120dacfd8"}, + {file = "click_repl-0.2.0-py3-none-any.whl", hash = "sha256:94b3fbbc9406a236f176e0506524b2937e4b23b6f4c0c0b2a0a83f8a64e9194b"}, ] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, @@ -832,20 +945,16 @@ dataclasses = [ {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"}, ] django = [ - {file = "Django-3.2.4-py3-none-any.whl", hash = "sha256:ea735cbbbb3b2fba6d4da4784a0043d84c67c92f1fdf15ad6db69900e792c10f"}, - {file = "Django-3.2.4.tar.gz", hash = "sha256:66c9d8db8cc6fe938a28b7887c1596e42d522e27618562517cc8929eb7e7f296"}, + {file = "Django-3.2.5-py3-none-any.whl", hash = "sha256:c58b5f19c5ae0afe6d75cbdd7df561e6eb929339985dbbda2565e1cabb19a62e"}, + {file = "Django-3.2.5.tar.gz", hash = "sha256:3da05fea54fdec2315b54a563d5b59f3b4e2b1e69c3a5841dda35019c01855cd"}, ] django-admin-list-filter-dropdown = [ {file = "django-admin-list-filter-dropdown-1.0.3.tar.gz", hash = "sha256:07cd37b6a9be1b08f11d4a92957c69b67bc70b1f87a2a7d4ae886c93ea51eb53"}, {file = "django_admin_list_filter_dropdown-1.0.3-py3-none-any.whl", hash = "sha256:bf1b48bab9772dad79db71efef17e78782d4f2421444d5e49bb10e0da71cd6bb"}, ] django-celery-beat = [ - {file = "django-celery-beat-2.2.0.tar.gz", hash = "sha256:b8a13afb15e7c53fc04f4f847ac71a6d32088959aba701eb7c4a59f0c28ba543"}, - {file = "django_celery_beat-2.2.0-py2.py3-none-any.whl", hash = "sha256:c4c72a9579f20eff4c4ccf1b58ebdca5ef940f4210065057db1754ea5f8dffdc"}, -] -django-celery-results = [ - {file = "django_celery_results-1.2.1-py2.py3-none-any.whl", hash = "sha256:a29ab580f0e38c66c39f51cc426bbdbb2a391b8cc0867df9dea748db2c961db2"}, - {file = "django_celery_results-1.2.1.tar.gz", hash = "sha256:e390f70cc43bbc2cd7c8e4607dc29ab6211a2ab968f93677583f0160921f670c"}, + {file = "django-celery-beat-2.2.1.tar.gz", hash = "sha256:97ae5eb309541551bdb07bf60cc57cadacf42a74287560ced2d2c06298620234"}, + {file = "django_celery_beat-2.2.1-py2.py3-none-any.whl", hash = "sha256:ab43049634fd18dc037927d7c2c7d5f67f95283a20ebbda55f42f8606412e66c"}, ] django-constance = [ {file = "django-constance-2.8.0.tar.gz", hash = "sha256:0a492454acc78799ce7b9f7a28a00c53427d513f34f8bf6fdc90a46d8864b2af"}, @@ -855,34 +964,39 @@ django-countries = [ {file = "django-countries-7.2.1.tar.gz", hash = "sha256:26878b54d36bedff30b4535ceefcb8af6784741a8b30b1b8a662fb14a936a4ab"}, {file = "django_countries-7.2.1-py3-none-any.whl", hash = "sha256:adc965f1d348124274b7d918fc1aad5e29609758af999e1822baa9f2cc06d1b8"}, ] +django-jsonfield = [ + {file = "django-jsonfield-1.4.1.tar.gz", hash = "sha256:f789a0ea1f80b48aff7d6c36dd356ce125dbf1b7cd97a82d315607ac758f50ff"}, + {file = "django_jsonfield-1.4.1-py2.py3-none-any.whl", hash = "sha256:ccb2fe623e1bf7799e49c593b0a89a85084ef8d3debbf26d92a54e27b5305d72"}, +] django-lcore = [] django-picklefield = [ {file = "django-picklefield-3.0.1.tar.gz", hash = "sha256:15ccba592ca953b9edf9532e64640329cd47b136b7f8f10f2939caa5f9ce4287"}, {file = "django_picklefield-3.0.1-py3-none-any.whl", hash = "sha256:3c702a54fde2d322fe5b2f39b8f78d9f655b8f77944ab26f703be6c0ed335a35"}, ] +django-pipayments = [] django-timezone-field = [ - {file = "django-timezone-field-4.1.2.tar.gz", hash = "sha256:cffac62452d060e365938aa9c9f7b72d70d8b26b9c60243bce227b35abd1b9df"}, - {file = "django_timezone_field-4.1.2-py3-none-any.whl", hash = "sha256:897c06e40b619cf5731a30d6c156886a7c64cba3a90364832148da7ef32ccf36"}, + {file = "django-timezone-field-4.2.1.tar.gz", hash = "sha256:97780cde658daa5094ae515bb55ca97c1352928ab554041207ad515dee3fe971"}, + {file = "django_timezone_field-4.2.1-py3-none-any.whl", hash = "sha256:6dc782e31036a58da35b553bd00c70f112d794700025270d8a6a4c1d2e5b26c6"}, ] django-tinymce4-lite = [ {file = "django-tinymce4-lite-1.8.0.tar.gz", hash = "sha256:eb0ee7eda19970d06484f9e121871de01287b5345c4007ea2582d6f80ec3edb3"}, {file = "django_tinymce4_lite-1.8.0-py3-none-any.whl", hash = "sha256:2d53510ddb5fe20f25e525d4eaf7c8f8a567b85c9cc29f8ab2964419d9352d88"}, ] flower = [ - {file = "flower-0.9.7-py2.py3-none-any.whl", hash = "sha256:8d6d6ac03e60b3a4227d156da489eb435e2442d82e89922d413df9054b9221eb"}, - {file = "flower-0.9.7.tar.gz", hash = "sha256:cf27a254268bb06fd4972408d0518237fcd847f7da4b4cd8055e228150ace8f3"}, + {file = "flower-0.9.5-py2.py3-none-any.whl", hash = "sha256:71be02bff7b2f56b0a07bd947fb3c748acba7f44f80ae88125d8954ce1a89697"}, + {file = "flower-0.9.5.tar.gz", hash = "sha256:56916d1d2892e25453d6023437427fc04706a1308e0bd4822321da34e1643f9c"}, ] humanize = [ - {file = "humanize-3.7.0-py3-none-any.whl", hash = "sha256:5eaafcd584fd01ab9a59f040e20b3daa35a24e8125d4c84788392d799be2012a"}, - {file = "humanize-3.7.0.tar.gz", hash = "sha256:8b1463a17bf722c06712ac9d31f7e46efd048dd4e76fafeac9f3b8f972b0b8e3"}, + {file = "humanize-3.10.0-py3-none-any.whl", hash = "sha256:aab7625d62dd5e0a054c8413a47d1fa257f3bdd8e9a2442c2fe36061bdd1d9bf"}, + {file = "humanize-3.10.0.tar.gz", hash = "sha256:b2413730ce6684f85e0439a5b80b8f402e09f03e16ab8023d1da758c6ff41148"}, ] idna = [ - {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, - {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, + {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, + {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.4.0-py3-none-any.whl", hash = "sha256:960d52ba7c21377c990412aca380bf3642d734c2eaab78a2c39319f67c6a5786"}, - {file = "importlib_metadata-4.4.0.tar.gz", hash = "sha256:e592faad8de1bda9fe920cf41e15261e7131bcf266c30306eec00e8e225c1dd5"}, + {file = "importlib_metadata-4.6.1-py3-none-any.whl", hash = "sha256:9f55f560e116f8643ecf2922d9cd3e1c7e8d52e683178fecd9d08f6aa357e11e"}, + {file = "importlib_metadata-4.6.1.tar.gz", hash = "sha256:079ada16b7fc30dfbb5d13399a5113110dab1aa7c2bc62f66af75f0b717c8cac"}, ] isort = [ {file = "isort-5.8.0-py3-none-any.whl", hash = "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"}, @@ -896,8 +1010,8 @@ jsonfield = [ {file = "jsonfield-3.1.0.tar.gz", hash = "sha256:7e4e84597de21eeaeeaaa7cc5da08c61c48a9b64d0c446b2d71255d01812887a"}, ] kombu = [ - {file = "kombu-4.6.11-py2.py3-none-any.whl", hash = "sha256:be48cdffb54a2194d93ad6533d73f69408486483d189fe9f5990ee24255b0e0a"}, - {file = "kombu-4.6.11.tar.gz", hash = "sha256:ca1b45faac8c0b18493d02a8571792f3c40291cf2bcf1f55afed3d8f3aa7ba74"}, + {file = "kombu-5.1.0-py3-none-any.whl", hash = "sha256:e2dedd8a86c9077c350555153825a31e456a0dc20c15d5751f00137ec9c75f0a"}, + {file = "kombu-5.1.0.tar.gz", hash = "sha256:01481d99f4606f6939cdc9b637264ed353ee9e3e4f62cfb582324142c41a572d"}, ] lazy-object-proxy = [ {file = "lazy-object-proxy-1.6.0.tar.gz", hash = "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726"}, @@ -937,46 +1051,47 @@ mypy-extensions = [ {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] pathspec = [ - {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, - {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, + {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, + {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, ] prometheus-client = [ {file = "prometheus_client-0.8.0-py2.py3-none-any.whl", hash = "sha256:983c7ac4b47478720db338f1491ef67a100b474e3bc7dafcbaefb7d0b8f9b01c"}, {file = "prometheus_client-0.8.0.tar.gz", hash = "sha256:c6e6b706833a6bd1fd51711299edee907857be10ece535126a158f911ee80915"}, ] +prompt-toolkit = [ + {file = "prompt_toolkit-3.0.3-py3-none-any.whl", hash = "sha256:c93e53af97f630f12f5f62a3274e79527936ed466f038953dfa379d4941f651a"}, + {file = "prompt_toolkit-3.0.3.tar.gz", hash = "sha256:a402e9bf468b63314e37460b68ba68243d55b2f8c4d0192f85a019af3945050e"}, +] psycopg2-binary = [ - {file = "psycopg2-binary-2.8.6.tar.gz", hash = "sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0"}, - {file = "psycopg2_binary-2.8.6-cp27-cp27m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4"}, - {file = "psycopg2_binary-2.8.6-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db"}, - {file = "psycopg2_binary-2.8.6-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5"}, - {file = "psycopg2_binary-2.8.6-cp27-cp27m-win32.whl", hash = "sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25"}, - {file = "psycopg2_binary-2.8.6-cp27-cp27m-win_amd64.whl", hash = "sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c"}, - {file = "psycopg2_binary-2.8.6-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c"}, - {file = "psycopg2_binary-2.8.6-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1"}, - {file = "psycopg2_binary-2.8.6-cp34-cp34m-win32.whl", hash = "sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2"}, - {file = "psycopg2_binary-2.8.6-cp34-cp34m-win_amd64.whl", hash = "sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152"}, - {file = "psycopg2_binary-2.8.6-cp35-cp35m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:d1b4ab59e02d9008efe10ceabd0b31e79519da6fb67f7d8e8977118832d0f449"}, - {file = "psycopg2_binary-2.8.6-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:ac0c682111fbf404525dfc0f18a8b5f11be52657d4f96e9fcb75daf4f3984859"}, - {file = "psycopg2_binary-2.8.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550"}, - {file = "psycopg2_binary-2.8.6-cp35-cp35m-win32.whl", hash = "sha256:aaa4213c862f0ef00022751161df35804127b78adf4a2755b9f991a507e425fd"}, - {file = "psycopg2_binary-2.8.6-cp35-cp35m-win_amd64.whl", hash = "sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71"}, - {file = "psycopg2_binary-2.8.6-cp36-cp36m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:ee69dad2c7155756ad114c02db06002f4cded41132cc51378e57aad79cc8e4f4"}, - {file = "psycopg2_binary-2.8.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb"}, - {file = "psycopg2_binary-2.8.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da"}, - {file = "psycopg2_binary-2.8.6-cp36-cp36m-win32.whl", hash = "sha256:a0eb43a07386c3f1f1ebb4dc7aafb13f67188eab896e7397aa1ee95a9c884eb2"}, - {file = "psycopg2_binary-2.8.6-cp36-cp36m-win_amd64.whl", hash = "sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a"}, - {file = "psycopg2_binary-2.8.6-cp37-cp37m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679"}, - {file = "psycopg2_binary-2.8.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf"}, - {file = "psycopg2_binary-2.8.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b"}, - {file = "psycopg2_binary-2.8.6-cp37-cp37m-win32.whl", hash = "sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67"}, - {file = "psycopg2_binary-2.8.6-cp37-cp37m-win_amd64.whl", hash = "sha256:cec7e622ebc545dbb4564e483dd20e4e404da17ae07e06f3e780b2dacd5cee66"}, - {file = "psycopg2_binary-2.8.6-cp38-cp38-macosx_10_9_x86_64.macosx_10_9_intel.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f"}, - {file = "psycopg2_binary-2.8.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77"}, - {file = "psycopg2_binary-2.8.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94"}, - {file = "psycopg2_binary-2.8.6-cp38-cp38-win32.whl", hash = "sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729"}, - {file = "psycopg2_binary-2.8.6-cp38-cp38-win_amd64.whl", hash = "sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77"}, - {file = "psycopg2_binary-2.8.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52"}, - {file = "psycopg2_binary-2.8.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd"}, + {file = "psycopg2-binary-2.9.1.tar.gz", hash = "sha256:b0221ca5a9837e040ebf61f48899926b5783668b7807419e4adae8175a31f773"}, + {file = "psycopg2_binary-2.9.1-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:c250a7ec489b652c892e4f0a5d122cc14c3780f9f643e1a326754aedf82d9a76"}, + {file = "psycopg2_binary-2.9.1-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aef9aee84ec78af51107181d02fe8773b100b01c5dfde351184ad9223eab3698"}, + {file = "psycopg2_binary-2.9.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:123c3fb684e9abfc47218d3784c7b4c47c8587951ea4dd5bc38b6636ac57f616"}, + {file = "psycopg2_binary-2.9.1-cp36-cp36m-manylinux_2_24_aarch64.whl", hash = "sha256:995fc41ebda5a7a663a254a1dcac52638c3e847f48307b5416ee373da15075d7"}, + {file = "psycopg2_binary-2.9.1-cp36-cp36m-manylinux_2_24_ppc64le.whl", hash = "sha256:fbb42a541b1093385a2d8c7eec94d26d30437d0e77c1d25dae1dcc46741a385e"}, + {file = "psycopg2_binary-2.9.1-cp36-cp36m-win32.whl", hash = "sha256:20f1ab44d8c352074e2d7ca67dc00843067788791be373e67a0911998787ce7d"}, + {file = "psycopg2_binary-2.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f6fac64a38f6768e7bc7b035b9e10d8a538a9fadce06b983fb3e6fa55ac5f5ce"}, + {file = "psycopg2_binary-2.9.1-cp37-cp37m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:1e3a362790edc0a365385b1ac4cc0acc429a0c0d662d829a50b6ce743ae61b5a"}, + {file = "psycopg2_binary-2.9.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f8559617b1fcf59a9aedba2c9838b5b6aa211ffedecabca412b92a1ff75aac1a"}, + {file = "psycopg2_binary-2.9.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a36c7eb6152ba5467fb264d73844877be8b0847874d4822b7cf2d3c0cb8cdcb0"}, + {file = "psycopg2_binary-2.9.1-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:2f62c207d1740b0bde5c4e949f857b044818f734a3d57f1d0d0edc65050532ed"}, + {file = "psycopg2_binary-2.9.1-cp37-cp37m-manylinux_2_24_ppc64le.whl", hash = "sha256:cfc523edecddaef56f6740d7de1ce24a2fdf94fd5e704091856a201872e37f9f"}, + {file = "psycopg2_binary-2.9.1-cp37-cp37m-win32.whl", hash = "sha256:1e85b74cbbb3056e3656f1cc4781294df03383127a8114cbc6531e8b8367bf1e"}, + {file = "psycopg2_binary-2.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:1473c0215b0613dd938db54a653f68251a45a78b05f6fc21af4326f40e8360a2"}, + {file = "psycopg2_binary-2.9.1-cp38-cp38-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:35c4310f8febe41f442d3c65066ca93cccefd75013df3d8c736c5b93ec288140"}, + {file = "psycopg2_binary-2.9.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c13d72ed6af7fd2c8acbd95661cf9477f94e381fce0792c04981a8283b52917"}, + {file = "psycopg2_binary-2.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14db1752acdd2187d99cb2ca0a1a6dfe57fc65c3281e0f20e597aac8d2a5bd90"}, + {file = "psycopg2_binary-2.9.1-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:aed4a9a7e3221b3e252c39d0bf794c438dc5453bc2963e8befe9d4cd324dff72"}, + {file = "psycopg2_binary-2.9.1-cp38-cp38-manylinux_2_24_ppc64le.whl", hash = "sha256:da113b70f6ec40e7d81b43d1b139b9db6a05727ab8be1ee559f3a69854a69d34"}, + {file = "psycopg2_binary-2.9.1-cp38-cp38-win32.whl", hash = "sha256:4235f9d5ddcab0b8dbd723dca56ea2922b485ea00e1dafacf33b0c7e840b3d32"}, + {file = "psycopg2_binary-2.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:988b47ac70d204aed01589ed342303da7c4d84b56c2f4c4b8b00deda123372bf"}, + {file = "psycopg2_binary-2.9.1-cp39-cp39-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:7360647ea04db2e7dff1648d1da825c8cf68dc5fbd80b8fb5b3ee9f068dcd21a"}, + {file = "psycopg2_binary-2.9.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca86db5b561b894f9e5f115d6a159fff2a2570a652e07889d8a383b5fae66eb4"}, + {file = "psycopg2_binary-2.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ced67f1e34e1a450cdb48eb53ca73b60aa0af21c46b9b35ac3e581cf9f00e31"}, + {file = "psycopg2_binary-2.9.1-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:0f2e04bd2a2ab54fa44ee67fe2d002bb90cee1c0f1cc0ebc3148af7b02034cbd"}, + {file = "psycopg2_binary-2.9.1-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:3242b9619de955ab44581a03a64bdd7d5e470cc4183e8fcadd85ab9d3756ce7a"}, + {file = "psycopg2_binary-2.9.1-cp39-cp39-win32.whl", hash = "sha256:0b7dae87f0b729922e06f85f667de7bf16455d411971b2043bbd9577af9d1975"}, + {file = "psycopg2_binary-2.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:b4d7679a08fea64573c969f6994a2631908bb2c0e69a7235648642f3d2e39a68"}, ] pygal = [ {file = "pygal-2.4.0-py2.py3-none-any.whl", hash = "sha256:27abab93cbc31e21f3c6bdecc05bda6cd3570cbdbd8297b7caa6904051b50d72"}, @@ -987,8 +1102,8 @@ pygments = [ {file = "Pygments-2.9.0.tar.gz", hash = "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f"}, ] pylint = [ - {file = "pylint-2.8.3-py3-none-any.whl", hash = "sha256:792b38ff30903884e4a9eab814ee3523731abd3c463f3ba48d7b627e87013484"}, - {file = "pylint-2.8.3.tar.gz", hash = "sha256:0a049c5d47b629d9070c3932d13bff482b12119b6a241a93bc460b0be16953c8"}, + {file = "pylint-2.9.5-py3-none-any.whl", hash = "sha256:748f81e5776d6273a6619506e08f1b48ff9bcb8198366a56821cf11aac14fc87"}, + {file = "pylint-2.9.5.tar.gz", hash = "sha256:1f333dc72ef7f5ea166b3230936ebcfb1f3b722e76c980cb9fe6b9f95e8d3172"}, ] pylint-django = [ {file = "pylint-django-2.4.4.tar.gz", hash = "sha256:f63f717169b0c2e4e19c28f1c32c28290647330184fcb7427805ae9b6994f3fc"}, @@ -1006,8 +1121,8 @@ python-crontab = [ {file = "python-crontab-2.5.1.tar.gz", hash = "sha256:4bbe7e720753a132ca4ca9d4094915f40e9d9dc8a807a4564007651018ce8c31"}, ] python-dateutil = [ - {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, - {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] python-frontmatter = [ {file = "python-frontmatter-1.0.0.tar.gz", hash = "sha256:e98152e977225ddafea6f01f40b4b0f1de175766322004c826ca99842d19a7cd"}, @@ -1053,51 +1168,51 @@ redis = [ {file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"}, ] regex = [ - {file = "regex-2021.4.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:619d71c59a78b84d7f18891fe914446d07edd48dc8328c8e149cbe0929b4e000"}, - {file = "regex-2021.4.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:47bf5bf60cf04d72bf6055ae5927a0bd9016096bf3d742fa50d9bf9f45aa0711"}, - {file = "regex-2021.4.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:281d2fd05555079448537fe108d79eb031b403dac622621c78944c235f3fcf11"}, - {file = "regex-2021.4.4-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:bd28bc2e3a772acbb07787c6308e00d9626ff89e3bfcdebe87fa5afbfdedf968"}, - {file = "regex-2021.4.4-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7c2a1af393fcc09e898beba5dd59196edaa3116191cc7257f9224beaed3e1aa0"}, - {file = "regex-2021.4.4-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c38c71df845e2aabb7fb0b920d11a1b5ac8526005e533a8920aea97efb8ec6a4"}, - {file = "regex-2021.4.4-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:96fcd1888ab4d03adfc9303a7b3c0bd78c5412b2bfbe76db5b56d9eae004907a"}, - {file = "regex-2021.4.4-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:ade17eb5d643b7fead300a1641e9f45401c98eee23763e9ed66a43f92f20b4a7"}, - {file = "regex-2021.4.4-cp36-cp36m-win32.whl", hash = "sha256:e8e5b509d5c2ff12f8418006d5a90e9436766133b564db0abaec92fd27fcee29"}, - {file = "regex-2021.4.4-cp36-cp36m-win_amd64.whl", hash = "sha256:11d773d75fa650cd36f68d7ca936e3c7afaae41b863b8c387a22aaa78d3c5c79"}, - {file = "regex-2021.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d3029c340cfbb3ac0a71798100ccc13b97dddf373a4ae56b6a72cf70dfd53bc8"}, - {file = "regex-2021.4.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:18c071c3eb09c30a264879f0d310d37fe5d3a3111662438889ae2eb6fc570c31"}, - {file = "regex-2021.4.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:4c557a7b470908b1712fe27fb1ef20772b78079808c87d20a90d051660b1d69a"}, - {file = "regex-2021.4.4-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:01afaf2ec48e196ba91b37451aa353cb7eda77efe518e481707e0515025f0cd5"}, - {file = "regex-2021.4.4-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:3a9cd17e6e5c7eb328517969e0cb0c3d31fd329298dd0c04af99ebf42e904f82"}, - {file = "regex-2021.4.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:90f11ff637fe8798933fb29f5ae1148c978cccb0452005bf4c69e13db951e765"}, - {file = "regex-2021.4.4-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:919859aa909429fb5aa9cf8807f6045592c85ef56fdd30a9a3747e513db2536e"}, - {file = "regex-2021.4.4-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:339456e7d8c06dd36a22e451d58ef72cef293112b559010db3d054d5560ef439"}, - {file = "regex-2021.4.4-cp37-cp37m-win32.whl", hash = "sha256:67bdb9702427ceddc6ef3dc382455e90f785af4c13d495f9626861763ee13f9d"}, - {file = "regex-2021.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:32e65442138b7b76dd8173ffa2cf67356b7bc1768851dded39a7a13bf9223da3"}, - {file = "regex-2021.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1e1c20e29358165242928c2de1482fb2cf4ea54a6a6dea2bd7a0e0d8ee321500"}, - {file = "regex-2021.4.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:314d66636c494ed9c148a42731b3834496cc9a2c4251b1661e40936814542b14"}, - {file = "regex-2021.4.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6d1b01031dedf2503631d0903cb563743f397ccaf6607a5e3b19a3d76fc10480"}, - {file = "regex-2021.4.4-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:741a9647fcf2e45f3a1cf0e24f5e17febf3efe8d4ba1281dcc3aa0459ef424dc"}, - {file = "regex-2021.4.4-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4c46e22a0933dd783467cf32b3516299fb98cfebd895817d685130cc50cd1093"}, - {file = "regex-2021.4.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:e512d8ef5ad7b898cdb2d8ee1cb09a8339e4f8be706d27eaa180c2f177248a10"}, - {file = "regex-2021.4.4-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:980d7be47c84979d9136328d882f67ec5e50008681d94ecc8afa8a65ed1f4a6f"}, - {file = "regex-2021.4.4-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:ce15b6d103daff8e9fee13cf7f0add05245a05d866e73926c358e871221eae87"}, - {file = "regex-2021.4.4-cp38-cp38-win32.whl", hash = "sha256:a91aa8619b23b79bcbeb37abe286f2f408d2f2d6f29a17237afda55bb54e7aac"}, - {file = "regex-2021.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:c0502c0fadef0d23b128605d69b58edb2c681c25d44574fc673b0e52dce71ee2"}, - {file = "regex-2021.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:598585c9f0af8374c28edd609eb291b5726d7cbce16be6a8b95aa074d252ee17"}, - {file = "regex-2021.4.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:ee54ff27bf0afaf4c3b3a62bcd016c12c3fdb4ec4f413391a90bd38bc3624605"}, - {file = "regex-2021.4.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7d9884d86dd4dd489e981d94a65cd30d6f07203d90e98f6f657f05170f6324c9"}, - {file = "regex-2021.4.4-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:bf5824bfac591ddb2c1f0a5f4ab72da28994548c708d2191e3b87dd207eb3ad7"}, - {file = "regex-2021.4.4-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:563085e55b0d4fb8f746f6a335893bda5c2cef43b2f0258fe1020ab1dd874df8"}, - {file = "regex-2021.4.4-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9c3db21af35e3b3c05764461b262d6f05bbca08a71a7849fd79d47ba7bc33ed"}, - {file = "regex-2021.4.4-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:3916d08be28a1149fb97f7728fca1f7c15d309a9f9682d89d79db75d5e52091c"}, - {file = "regex-2021.4.4-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:fd45ff9293d9274c5008a2054ecef86a9bfe819a67c7be1afb65e69b405b3042"}, - {file = "regex-2021.4.4-cp39-cp39-win32.whl", hash = "sha256:fa4537fb4a98fe8fde99626e4681cc644bdcf2a795038533f9f711513a862ae6"}, - {file = "regex-2021.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:97f29f57d5b84e73fbaf99ab3e26134e6687348e95ef6b48cfd2c06807005a07"}, - {file = "regex-2021.4.4.tar.gz", hash = "sha256:52ba3d3f9b942c49d7e4bc105bb28551c44065f139a65062ab7912bef10c9afb"}, + {file = "regex-2021.7.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e6a1e5ca97d411a461041d057348e578dc344ecd2add3555aedba3b408c9f874"}, + {file = "regex-2021.7.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:6afe6a627888c9a6cfbb603d1d017ce204cebd589d66e0703309b8048c3b0854"}, + {file = "regex-2021.7.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ccb3d2190476d00414aab36cca453e4596e8f70a206e2aa8db3d495a109153d2"}, + {file = "regex-2021.7.6-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:ed693137a9187052fc46eedfafdcb74e09917166362af4cc4fddc3b31560e93d"}, + {file = "regex-2021.7.6-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:99d8ab206a5270c1002bfcf25c51bf329ca951e5a169f3b43214fdda1f0b5f0d"}, + {file = "regex-2021.7.6-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:b85ac458354165405c8a84725de7bbd07b00d9f72c31a60ffbf96bb38d3e25fa"}, + {file = "regex-2021.7.6-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:3f5716923d3d0bfb27048242a6e0f14eecdb2e2a7fac47eda1d055288595f222"}, + {file = "regex-2021.7.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5983c19d0beb6af88cb4d47afb92d96751fb3fa1784d8785b1cdf14c6519407"}, + {file = "regex-2021.7.6-cp36-cp36m-win32.whl", hash = "sha256:c92831dac113a6e0ab28bc98f33781383fe294df1a2c3dfd1e850114da35fd5b"}, + {file = "regex-2021.7.6-cp36-cp36m-win_amd64.whl", hash = "sha256:791aa1b300e5b6e5d597c37c346fb4d66422178566bbb426dd87eaae475053fb"}, + {file = "regex-2021.7.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:59506c6e8bd9306cd8a41511e32d16d5d1194110b8cfe5a11d102d8b63cf945d"}, + {file = "regex-2021.7.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:564a4c8a29435d1f2256ba247a0315325ea63335508ad8ed938a4f14c4116a5d"}, + {file = "regex-2021.7.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:59c00bb8dd8775473cbfb967925ad2c3ecc8886b3b2d0c90a8e2707e06c743f0"}, + {file = "regex-2021.7.6-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:9a854b916806c7e3b40e6616ac9e85d3cdb7649d9e6590653deb5b341a736cec"}, + {file = "regex-2021.7.6-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:db2b7df831c3187a37f3bb80ec095f249fa276dbe09abd3d35297fc250385694"}, + {file = "regex-2021.7.6-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:173bc44ff95bc1e96398c38f3629d86fa72e539c79900283afa895694229fe6a"}, + {file = "regex-2021.7.6-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:15dddb19823f5147e7517bb12635b3c82e6f2a3a6b696cc3e321522e8b9308ad"}, + {file = "regex-2021.7.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ddeabc7652024803666ea09f32dd1ed40a0579b6fbb2a213eba590683025895"}, + {file = "regex-2021.7.6-cp37-cp37m-win32.whl", hash = "sha256:f080248b3e029d052bf74a897b9d74cfb7643537fbde97fe8225a6467fb559b5"}, + {file = "regex-2021.7.6-cp37-cp37m-win_amd64.whl", hash = "sha256:d8bbce0c96462dbceaa7ac4a7dfbbee92745b801b24bce10a98d2f2b1ea9432f"}, + {file = "regex-2021.7.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:edd1a68f79b89b0c57339bce297ad5d5ffcc6ae7e1afdb10f1947706ed066c9c"}, + {file = "regex-2021.7.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:422dec1e7cbb2efbbe50e3f1de36b82906def93ed48da12d1714cabcd993d7f0"}, + {file = "regex-2021.7.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cbe23b323988a04c3e5b0c387fe3f8f363bf06c0680daf775875d979e376bd26"}, + {file = "regex-2021.7.6-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:0eb2c6e0fcec5e0f1d3bcc1133556563222a2ffd2211945d7b1480c1b1a42a6f"}, + {file = "regex-2021.7.6-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:1c78780bf46d620ff4fff40728f98b8afd8b8e35c3efd638c7df67be2d5cddbf"}, + {file = "regex-2021.7.6-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bc84fb254a875a9f66616ed4538542fb7965db6356f3df571d783f7c8d256edd"}, + {file = "regex-2021.7.6-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:598c0a79b4b851b922f504f9f39a863d83ebdfff787261a5ed061c21e67dd761"}, + {file = "regex-2021.7.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875c355360d0f8d3d827e462b29ea7682bf52327d500a4f837e934e9e4656068"}, + {file = "regex-2021.7.6-cp38-cp38-win32.whl", hash = "sha256:e586f448df2bbc37dfadccdb7ccd125c62b4348cb90c10840d695592aa1b29e0"}, + {file = "regex-2021.7.6-cp38-cp38-win_amd64.whl", hash = "sha256:2fe5e71e11a54e3355fa272137d521a40aace5d937d08b494bed4529964c19c4"}, + {file = "regex-2021.7.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6110bab7eab6566492618540c70edd4d2a18f40ca1d51d704f1d81c52d245026"}, + {file = "regex-2021.7.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4f64fc59fd5b10557f6cd0937e1597af022ad9b27d454e182485f1db3008f417"}, + {file = "regex-2021.7.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:89e5528803566af4df368df2d6f503c84fbfb8249e6631c7b025fe23e6bd0cde"}, + {file = "regex-2021.7.6-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2366fe0479ca0e9afa534174faa2beae87847d208d457d200183f28c74eaea59"}, + {file = "regex-2021.7.6-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f9392a4555f3e4cb45310a65b403d86b589adc773898c25a39184b1ba4db8985"}, + {file = "regex-2021.7.6-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:2bceeb491b38225b1fee4517107b8491ba54fba77cf22a12e996d96a3c55613d"}, + {file = "regex-2021.7.6-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:f98dc35ab9a749276f1a4a38ab3e0e2ba1662ce710f6530f5b0a6656f1c32b58"}, + {file = "regex-2021.7.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:319eb2a8d0888fa6f1d9177705f341bc9455a2c8aca130016e52c7fe8d6c37a3"}, + {file = "regex-2021.7.6-cp39-cp39-win32.whl", hash = "sha256:eaf58b9e30e0e546cdc3ac06cf9165a1ca5b3de8221e9df679416ca667972035"}, + {file = "regex-2021.7.6-cp39-cp39-win_amd64.whl", hash = "sha256:4c9c3155fe74269f61e27617529b7f09552fbb12e44b1189cebbdb24294e6e1c"}, + {file = "regex-2021.7.6.tar.gz", hash = "sha256:8394e266005f2d8c6f0bc6780001f7afa3ef81a7a2111fa35058ded6fce79e4d"}, ] requests = [ - {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, - {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, + {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, + {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, ] selenium = [ {file = "selenium-3.141.0-py2.py3-none-any.whl", hash = "sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c"}, @@ -1112,8 +1227,8 @@ sqlparse = [ {file = "sqlparse-0.4.1.tar.gz", hash = "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"}, ] stripe = [ - {file = "stripe-2.57.0-py2.py3-none-any.whl", hash = "sha256:178d15444d1364bca1a91f3d167409aedeb21a01835a5f9968a88bba3f79b606"}, - {file = "stripe-2.57.0.tar.gz", hash = "sha256:965a7531c1e64253f0efbd4b31d028a66bb6e5b9352504fbcfdc72a7ba745aff"}, + {file = "stripe-2.60.0-py2.py3-none-any.whl", hash = "sha256:0050763cb67df6745973bd9757f7a765bed1b82b5d5261fb8908cfc6ec9e5200"}, + {file = "stripe-2.60.0.tar.gz", hash = "sha256:8966b7793014380f60c6f121ba333d6f333a55818edaf79c8d70464ce0a7a808"}, ] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, @@ -1200,17 +1315,21 @@ typing-extensions = [ {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, ] urllib3 = [ - {file = "urllib3-1.26.5-py2.py3-none-any.whl", hash = "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c"}, - {file = "urllib3-1.26.5.tar.gz", hash = "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098"}, + {file = "urllib3-1.26.6-py2.py3-none-any.whl", hash = "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4"}, + {file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"}, ] vine = [ - {file = "vine-1.3.0-py2.py3-none-any.whl", hash = "sha256:ea4947cc56d1fd6f2095c8d543ee25dad966f78692528e68b4fada11ba3f98af"}, - {file = "vine-1.3.0.tar.gz", hash = "sha256:133ee6d7a9016f177ddeaf191c1f58421a1dcc6ee9a42c58b34bed40e1d2cd87"}, + {file = "vine-5.0.0-py2.py3-none-any.whl", hash = "sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30"}, + {file = "vine-5.0.0.tar.gz", hash = "sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e"}, +] +wcwidth = [ + {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, + {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, ] wrapt = [ {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, ] zipp = [ - {file = "zipp-3.4.1-py3-none-any.whl", hash = "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"}, - {file = "zipp-3.4.1.tar.gz", hash = "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76"}, + {file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"}, + {file = "zipp-3.5.0.tar.gz", hash = "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"}, ] diff --git a/pyproject.toml b/pyproject.toml index f56312b..bde813d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,9 +23,10 @@ python-frontmatter = "^1" django-tinymce4-lite = "^1.7" django-admin-list-filter-dropdown = "^1.0" django-lcore = {git = "https://git.packetimpact.net/lvpn/django-lcore.git", tag = "v1.7.1"} -celery = "^4.4.7" +django-pipayments = {git = "git@git.packetimpact.net:lvpn/django-pipayments.git", branch = "main"} +#django-pipayments = {path = "../lvpn/django-pipayments", develop=true} +celery = "^5" django-celery-beat = "^2.0.0" -django-celery-results = "^1.2.1" redis = "^3.5.3" flower = "^0.9.5" diff --git a/static/css/style.css b/static/css/style.css index 0323671..2faed59 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -67,8 +67,8 @@ div#captcha > div { } .button.thin { border-width: 1px; - padding: 0.20em 0.50em; - border-radius: 5px; + padding: 0.15em 0.50em; + border-radius: 7px; font-size: 0.85em; } @@ -446,7 +446,7 @@ footer .language-selector ul { color: black; } .message p.info, .message p.success { - background-color: #062D4D; + background-color: #2EA658; } .message p.error, .message p.critical, .message .warning { background-color: #A7332F; diff --git a/templates/ccvpn/signup.html b/templates/ccvpn/signup.html index 1b6ffee..0052d6c 100644 --- a/templates/ccvpn/signup.html +++ b/templates/ccvpn/signup.html @@ -34,7 +34,7 @@ {% trans 'Used to recover your password and confirm stuff.' %}

- {% if not request.session.signup_captcha_pass %} + {% if HCAPTCHA_SITE_KEY and not request.session.signup_captcha_pass %}
{% endif %} diff --git a/templates/lambdainst/account.html b/templates/lambdainst/account.html index 9350626..d750f42 100644 --- a/templates/lambdainst/account.html +++ b/templates/lambdainst/account.html @@ -34,7 +34,7 @@ {% trans "Subscription" %} {% if subscription.status == 'active' %} - {% blocktrans trimmed with until=subscription.next_renew|date:'Y-m-d' backend=subscription.backend.backend_verbose_name %} + {% blocktrans trimmed with until=subscription.next_renew|default:user.vpnuser.expiration|date:'Y-m-d' backend=subscription.backend.backend_verbose_name %} ACTIVE. Renews on {{until}} via {{backend}}. {% endblocktrans %} ({% trans "cancel" %}) @@ -43,6 +43,7 @@ {% endif %} + {% if not subscription or subscription.status != 'active' %} {% trans "Expiration" %} @@ -54,6 +55,7 @@ {% endif %} + {% endif %} @@ -67,21 +69,21 @@
{% csrf_token %} - +
- + + +
- {% for backend in subscr_backends %}
@@ -142,7 +144,7 @@
-
+ {% csrf_token %}
diff --git a/templates/payments/cancel_subscr.html b/templates/payments/cancel_subscr.html index 5f35006..c211f47 100644 --- a/templates/payments/cancel_subscr.html +++ b/templates/payments/cancel_subscr.html @@ -7,7 +7,7 @@
{% if subscription %} - + {% csrf_token %}

diff --git a/templates/payments/form.html b/templates/payments/order-pay.html similarity index 65% rename from templates/payments/form.html rename to templates/payments/order-pay.html index 3d370bc..8c41242 100644 --- a/templates/payments/form.html +++ b/templates/payments/order-pay.html @@ -5,6 +5,8 @@ {% block pagetitle %}{% trans 'Payment' %}{% endblock %} {% block account_content %}

+

Please continue with our trusted payment processor:

+ {{ html | safe }}
diff --git a/templates/payments/list.html b/templates/payments/payments.html similarity index 100% rename from templates/payments/list.html rename to templates/payments/payments.html diff --git a/templates/payments/view.html b/templates/payments/success.html similarity index 100% rename from templates/payments/view.html rename to templates/payments/success.html