├── apps ├── __init__.py ├── blog │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ ├── __pycache__ │ │ │ ├── __init__.cpython-39.pyc │ │ │ ├── 0001_initial.cpython-39.pyc │ │ │ ├── 0004_article_uuid.cpython-39.pyc │ │ │ ├── 0002_auto_20220412_2227.cpython-39.pyc │ │ │ ├── 0005_auto_20220415_0117.cpython-39.pyc │ │ │ ├── 0007_auto_20220415_1512.cpython-39.pyc │ │ │ ├── 0008_auto_20220422_2237.cpython-39.pyc │ │ │ ├── 0003_remove_article_image.cpython-39.pyc │ │ │ └── 0006_alter_article_content.cpython-39.pyc │ │ ├── 0003_remove_article_image.py │ │ ├── 0004_article_uuid.py │ │ ├── 0006_alter_article_content.py │ │ ├── 0005_auto_20220415_0117.py │ │ ├── 0007_auto_20220415_1512.py │ │ ├── 0008_auto_20220422_2237.py │ │ ├── 0002_auto_20220412_2227.py │ │ └── 0001_initial.py │ ├── tests.py │ ├── __pycache__ │ │ ├── apps.cpython-39.pyc │ │ ├── urls.cpython-39.pyc │ │ ├── admin.cpython-39.pyc │ │ ├── forms.cpython-39.pyc │ │ ├── models.cpython-39.pyc │ │ ├── views.cpython-39.pyc │ │ └── __init__.cpython-39.pyc │ ├── apps.py │ ├── admin.py │ ├── urls.py │ ├── forms.py │ ├── models.py │ └── views.py ├── core │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ └── __pycache__ │ │ │ └── __init__.cpython-39.pyc │ ├── models.py │ ├── admin.py │ ├── tests.py │ ├── __pycache__ │ │ ├── apps.cpython-39.pyc │ │ ├── admin.cpython-39.pyc │ │ ├── models.cpython-39.pyc │ │ ├── views.cpython-39.pyc │ │ ├── __init__.cpython-39.pyc │ │ └── sitemaps.cpython-39.pyc │ ├── apps.py │ ├── views.py │ └── templates │ │ └── core │ │ └── home.html ├── accounts │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ └── __pycache__ │ │ │ └── __init__.cpython-39.pyc │ ├── models.py │ ├── admin.py │ ├── tests.py │ ├── __pycache__ │ │ ├── admin.cpython-39.pyc │ │ ├── apps.cpython-39.pyc │ │ ├── forms.cpython-39.pyc │ │ ├── urls.cpython-39.pyc │ │ ├── views.cpython-39.pyc │ │ ├── models.cpython-39.pyc │ │ └── __init__.cpython-39.pyc │ ├── apps.py │ ├── urls.py │ ├── views.py │ └── forms.py ├── payments │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ ├── __pycache__ │ │ │ ├── __init__.cpython-39.pyc │ │ │ ├── 0001_initial.cpython-39.pyc │ │ │ ├── 0002_payment_session.cpython-39.pyc │ │ │ └── 0003_auto_20220424_1546.cpython-39.pyc │ │ ├── 0002_payment_session.py │ │ ├── 0003_auto_20220424_1546.py │ │ └── 0001_initial.py │ ├── tests.py │ ├── __pycache__ │ │ ├── admin.cpython-39.pyc │ │ ├── apps.cpython-39.pyc │ │ ├── urls.cpython-39.pyc │ │ ├── views.cpython-39.pyc │ │ ├── models.cpython-39.pyc │ │ └── __init__.cpython-39.pyc │ ├── admin.py │ ├── urls.py │ ├── apps.py │ ├── views.py │ └── models.py ├── authentication │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ ├── __pycache__ │ │ │ ├── __init__.cpython-39.pyc │ │ │ ├── 0001_initial.cpython-39.pyc │ │ │ └── 0002_auto_20220414_1042.cpython-39.pyc │ │ ├── 0002_auto_20220414_1042.py │ │ └── 0001_initial.py │ ├── admin.py │ ├── tests.py │ ├── __pycache__ │ │ ├── apps.cpython-39.pyc │ │ ├── admin.cpython-39.pyc │ │ ├── forms.cpython-39.pyc │ │ ├── models.cpython-39.pyc │ │ ├── views.cpython-39.pyc │ │ ├── __init__.cpython-39.pyc │ │ ├── helpers.cpython-39.pyc │ │ └── validators.cpython-39.pyc │ ├── apps.py │ ├── helpers.py │ ├── backends.py │ ├── models.py │ ├── forms.py │ ├── views.py │ └── validators.py └── __pycache__ │ └── __init__.cpython-39.pyc ├── paywalled ├── __init__.py ├── .DS_Store ├── db.sqlite3 ├── static │ ├── .DS_Store │ ├── favicon.ico │ ├── css │ │ └── .DS_Store │ ├── images │ │ ├── user.png │ │ ├── loading.gif │ │ ├── favicons │ │ │ ├── favicon.ico │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon-96x96.png │ │ │ ├── mstile-144x144.png │ │ │ ├── mstile-150x150.png │ │ │ ├── mstile-310x150.png │ │ │ ├── mstile-310x310.png │ │ │ ├── mstile-70x70.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── android-chrome-36x36.png │ │ │ ├── android-chrome-48x48.png │ │ │ ├── android-chrome-72x72.png │ │ │ ├── android-chrome-96x96.png │ │ │ ├── android-chrome-144x144.png │ │ │ ├── android-chrome-192x192.png │ │ │ ├── apple-touch-icon-114x114.png │ │ │ ├── apple-touch-icon-120x120.png │ │ │ ├── apple-touch-icon-144x144.png │ │ │ ├── apple-touch-icon-152x152.png │ │ │ ├── apple-touch-icon-180x180.png │ │ │ ├── apple-touch-icon-57x57.png │ │ │ ├── apple-touch-icon-60x60.png │ │ │ ├── apple-touch-icon-72x72.png │ │ │ ├── apple-touch-icon-76x76.png │ │ │ └── apple-touch-icon-precomposed.png │ │ └── logo.svg │ ├── apple-touch-icon.png │ └── js │ │ ├── articles.js │ │ └── copy.js ├── templates │ ├── registration │ │ ├── password_reset_subject.txt │ │ ├── password_reset_complete.html │ │ ├── password_reset_email.html │ │ ├── password_reset_done.html │ │ ├── signup.html │ │ ├── password_reset_form.html │ │ ├── password_change_done.html │ │ ├── login.html │ │ ├── password_change_form.html │ │ └── password_reset_confirm.html │ ├── bamby_forms │ │ ├── layout │ │ │ ├── button.html │ │ │ ├── attrs.html │ │ │ ├── row.html │ │ │ ├── div.html │ │ │ ├── tab.html │ │ │ ├── tab-link.html │ │ │ ├── buttonholder.html │ │ │ ├── field_errors.html │ │ │ ├── field_errors_block.html │ │ │ ├── alert.html │ │ │ ├── column.html │ │ │ ├── fieldset.html │ │ │ ├── formactions.html │ │ │ ├── help_text.html │ │ │ ├── help_text_and_errors.html │ │ │ ├── baseinput.html │ │ │ ├── multifield.html │ │ │ ├── uneditable_input.html │ │ │ ├── radioselect_inline.html │ │ │ ├── checkboxselectmultiple_inline.html │ │ │ ├── inline_field.html │ │ │ ├── field_with_buttons.html │ │ │ ├── radioselect.html │ │ │ ├── checkboxselectmultiple.html │ │ │ ├── prepended_appended_text.html │ │ │ └── field_file.html │ │ ├── accordion.html │ │ ├── uni_formset.html │ │ ├── display_form.html │ │ ├── errors.html │ │ ├── uni_form.html │ │ ├── errors_formset.html │ │ ├── inputs.html │ │ ├── whole_uni_form.html │ │ ├── betterform.html │ │ ├── accordion-group.html │ │ ├── whole_uni_formset.html │ │ ├── table_inline_formset.html │ │ └── field.html │ ├── admin │ │ └── base_site.html │ ├── robots.txt │ ├── form_vertical.html │ ├── includes │ │ ├── messages.html │ │ ├── footer.html │ │ └── header.html │ ├── blog │ │ ├── update_article.html │ │ ├── publish_article.html │ │ ├── create_article.html │ │ ├── draft_list.html │ │ └── article_detail.html │ ├── partials │ │ ├── article_payment.html │ │ ├── article_list.html │ │ └── partial_invoice.html │ ├── 403.html │ ├── 404.html │ ├── 500.html │ └── base.html ├── __pycache__ │ ├── urls.cpython-39.pyc │ ├── wsgi.cpython-39.pyc │ ├── __init__.cpython-39.pyc │ └── settings.cpython-39.pyc ├── asgi.py ├── wsgi.py ├── urls.py └── settings.py ├── .DS_Store ├── paywalled.png ├── env-sample ├── requirements.txt ├── manage.py ├── .gitignore └── README.md /apps/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/blog/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /paywalled/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/accounts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/payments/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/authentication/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/blog/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/core/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/accounts/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/payments/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/authentication/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/.DS_Store -------------------------------------------------------------------------------- /paywalled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled.png -------------------------------------------------------------------------------- /apps/core/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /apps/accounts/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /apps/blog/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /apps/core/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /apps/core/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /apps/accounts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /apps/accounts/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /apps/payments/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /paywalled/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/.DS_Store -------------------------------------------------------------------------------- /paywalled/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/db.sqlite3 -------------------------------------------------------------------------------- /apps/authentication/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /apps/authentication/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /paywalled/static/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/static/.DS_Store -------------------------------------------------------------------------------- /paywalled/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/static/favicon.ico -------------------------------------------------------------------------------- /paywalled/static/css/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/static/css/.DS_Store -------------------------------------------------------------------------------- /paywalled/templates/registration/password_reset_subject.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %}{% trans "Parsifal Password Reset" %} 2 | -------------------------------------------------------------------------------- /paywalled/static/images/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/static/images/user.png -------------------------------------------------------------------------------- /paywalled/static/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/static/images/loading.gif -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/layout/button.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /paywalled/static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/static/apple-touch-icon.png -------------------------------------------------------------------------------- /apps/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /apps/blog/__pycache__/apps.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/blog/__pycache__/apps.cpython-39.pyc -------------------------------------------------------------------------------- /apps/blog/__pycache__/urls.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/blog/__pycache__/urls.cpython-39.pyc -------------------------------------------------------------------------------- /apps/core/__pycache__/apps.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/core/__pycache__/apps.cpython-39.pyc -------------------------------------------------------------------------------- /paywalled/__pycache__/urls.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/__pycache__/urls.cpython-39.pyc -------------------------------------------------------------------------------- /paywalled/__pycache__/wsgi.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/__pycache__/wsgi.cpython-39.pyc -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/accordion.html: -------------------------------------------------------------------------------- 1 |
2 | {{ content|safe }} 3 |
4 | -------------------------------------------------------------------------------- /apps/blog/__pycache__/admin.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/blog/__pycache__/admin.cpython-39.pyc -------------------------------------------------------------------------------- /apps/blog/__pycache__/forms.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/blog/__pycache__/forms.cpython-39.pyc -------------------------------------------------------------------------------- /apps/blog/__pycache__/models.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/blog/__pycache__/models.cpython-39.pyc -------------------------------------------------------------------------------- /apps/blog/__pycache__/views.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/blog/__pycache__/views.cpython-39.pyc -------------------------------------------------------------------------------- /apps/core/__pycache__/admin.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/core/__pycache__/admin.cpython-39.pyc -------------------------------------------------------------------------------- /apps/core/__pycache__/models.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/core/__pycache__/models.cpython-39.pyc -------------------------------------------------------------------------------- /apps/core/__pycache__/views.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/core/__pycache__/views.cpython-39.pyc -------------------------------------------------------------------------------- /apps/accounts/__pycache__/admin.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/accounts/__pycache__/admin.cpython-39.pyc -------------------------------------------------------------------------------- /apps/accounts/__pycache__/apps.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/accounts/__pycache__/apps.cpython-39.pyc -------------------------------------------------------------------------------- /apps/accounts/__pycache__/forms.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/accounts/__pycache__/forms.cpython-39.pyc -------------------------------------------------------------------------------- /apps/accounts/__pycache__/urls.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/accounts/__pycache__/urls.cpython-39.pyc -------------------------------------------------------------------------------- /apps/accounts/__pycache__/views.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/accounts/__pycache__/views.cpython-39.pyc -------------------------------------------------------------------------------- /apps/blog/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/blog/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /apps/core/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/core/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /apps/core/__pycache__/sitemaps.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/core/__pycache__/sitemaps.cpython-39.pyc -------------------------------------------------------------------------------- /apps/payments/__pycache__/admin.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/payments/__pycache__/admin.cpython-39.pyc -------------------------------------------------------------------------------- /apps/payments/__pycache__/apps.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/payments/__pycache__/apps.cpython-39.pyc -------------------------------------------------------------------------------- /apps/payments/__pycache__/urls.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/payments/__pycache__/urls.cpython-39.pyc -------------------------------------------------------------------------------- /apps/payments/__pycache__/views.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/payments/__pycache__/views.cpython-39.pyc -------------------------------------------------------------------------------- /paywalled/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /paywalled/__pycache__/settings.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/__pycache__/settings.cpython-39.pyc -------------------------------------------------------------------------------- /paywalled/static/images/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/static/images/favicons/favicon.ico -------------------------------------------------------------------------------- /apps/accounts/__pycache__/models.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/accounts/__pycache__/models.cpython-39.pyc -------------------------------------------------------------------------------- /apps/payments/__pycache__/models.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/payments/__pycache__/models.cpython-39.pyc -------------------------------------------------------------------------------- /apps/accounts/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/accounts/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /apps/authentication/__pycache__/apps.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/authentication/__pycache__/apps.cpython-39.pyc -------------------------------------------------------------------------------- /apps/payments/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/payments/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /paywalled/static/images/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/static/images/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /paywalled/static/images/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/static/images/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /paywalled/static/images/favicons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/static/images/favicons/favicon-96x96.png -------------------------------------------------------------------------------- /paywalled/static/images/favicons/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/static/images/favicons/mstile-144x144.png -------------------------------------------------------------------------------- /paywalled/static/images/favicons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/static/images/favicons/mstile-150x150.png -------------------------------------------------------------------------------- /paywalled/static/images/favicons/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/static/images/favicons/mstile-310x150.png -------------------------------------------------------------------------------- /paywalled/static/images/favicons/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/static/images/favicons/mstile-310x310.png -------------------------------------------------------------------------------- /paywalled/static/images/favicons/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/static/images/favicons/mstile-70x70.png -------------------------------------------------------------------------------- /apps/authentication/__pycache__/admin.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/authentication/__pycache__/admin.cpython-39.pyc -------------------------------------------------------------------------------- /apps/authentication/__pycache__/forms.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/authentication/__pycache__/forms.cpython-39.pyc -------------------------------------------------------------------------------- /apps/authentication/__pycache__/models.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/authentication/__pycache__/models.cpython-39.pyc -------------------------------------------------------------------------------- /apps/authentication/__pycache__/views.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/authentication/__pycache__/views.cpython-39.pyc -------------------------------------------------------------------------------- /paywalled/static/images/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/static/images/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /paywalled/templates/admin/base_site.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base.html" %} 2 | 3 | {% block branding %} 4 |

Bamby

5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /apps/authentication/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/authentication/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /apps/authentication/__pycache__/helpers.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/authentication/__pycache__/helpers.cpython-39.pyc -------------------------------------------------------------------------------- /apps/blog/migrations/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/blog/migrations/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /apps/core/migrations/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/core/migrations/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /apps/authentication/__pycache__/validators.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/authentication/__pycache__/validators.cpython-39.pyc -------------------------------------------------------------------------------- /apps/payments/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from apps.payments.models import Payment 3 | # Register your models here. 4 | 5 | admin.site.register(Payment) -------------------------------------------------------------------------------- /paywalled/static/images/favicons/android-chrome-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/static/images/favicons/android-chrome-36x36.png -------------------------------------------------------------------------------- /paywalled/static/images/favicons/android-chrome-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/static/images/favicons/android-chrome-48x48.png -------------------------------------------------------------------------------- /paywalled/static/images/favicons/android-chrome-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/static/images/favicons/android-chrome-72x72.png -------------------------------------------------------------------------------- /paywalled/static/images/favicons/android-chrome-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/static/images/favicons/android-chrome-96x96.png -------------------------------------------------------------------------------- /apps/accounts/migrations/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/accounts/migrations/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /apps/blog/migrations/__pycache__/0001_initial.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/blog/migrations/__pycache__/0001_initial.cpython-39.pyc -------------------------------------------------------------------------------- /apps/payments/migrations/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/payments/migrations/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /paywalled/static/images/favicons/android-chrome-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/static/images/favicons/android-chrome-144x144.png -------------------------------------------------------------------------------- /paywalled/static/images/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/static/images/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /paywalled/static/images/favicons/apple-touch-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/static/images/favicons/apple-touch-icon-114x114.png -------------------------------------------------------------------------------- /paywalled/static/images/favicons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/static/images/favicons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /paywalled/static/images/favicons/apple-touch-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/static/images/favicons/apple-touch-icon-144x144.png -------------------------------------------------------------------------------- /paywalled/static/images/favicons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/static/images/favicons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /paywalled/static/images/favicons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/static/images/favicons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /paywalled/static/images/favicons/apple-touch-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/static/images/favicons/apple-touch-icon-57x57.png -------------------------------------------------------------------------------- /paywalled/static/images/favicons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/static/images/favicons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /paywalled/static/images/favicons/apple-touch-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/static/images/favicons/apple-touch-icon-72x72.png -------------------------------------------------------------------------------- /paywalled/static/images/favicons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/static/images/favicons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /paywalled/templates/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | 3 | Disallow: /admin/ 4 | Disallow: /signin/ 5 | Disallow: /signout/ 6 | Disallow: /reset/ 7 | Allow: / 8 | 9 | Sitemap: /sitemap.xml -------------------------------------------------------------------------------- /apps/authentication/migrations/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/authentication/migrations/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /apps/blog/migrations/__pycache__/0004_article_uuid.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/blog/migrations/__pycache__/0004_article_uuid.cpython-39.pyc -------------------------------------------------------------------------------- /apps/payments/migrations/__pycache__/0001_initial.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/payments/migrations/__pycache__/0001_initial.cpython-39.pyc -------------------------------------------------------------------------------- /paywalled/static/images/favicons/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/paywalled/static/images/favicons/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /apps/authentication/migrations/__pycache__/0001_initial.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/authentication/migrations/__pycache__/0001_initial.cpython-39.pyc -------------------------------------------------------------------------------- /apps/blog/migrations/__pycache__/0002_auto_20220412_2227.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/blog/migrations/__pycache__/0002_auto_20220412_2227.cpython-39.pyc -------------------------------------------------------------------------------- /apps/blog/migrations/__pycache__/0005_auto_20220415_0117.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/blog/migrations/__pycache__/0005_auto_20220415_0117.cpython-39.pyc -------------------------------------------------------------------------------- /apps/blog/migrations/__pycache__/0007_auto_20220415_1512.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/blog/migrations/__pycache__/0007_auto_20220415_1512.cpython-39.pyc -------------------------------------------------------------------------------- /apps/blog/migrations/__pycache__/0008_auto_20220422_2237.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/blog/migrations/__pycache__/0008_auto_20220422_2237.cpython-39.pyc -------------------------------------------------------------------------------- /apps/blog/migrations/__pycache__/0003_remove_article_image.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/blog/migrations/__pycache__/0003_remove_article_image.cpython-39.pyc -------------------------------------------------------------------------------- /apps/payments/migrations/__pycache__/0002_payment_session.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/payments/migrations/__pycache__/0002_payment_session.cpython-39.pyc -------------------------------------------------------------------------------- /apps/blog/migrations/__pycache__/0006_alter_article_content.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/blog/migrations/__pycache__/0006_alter_article_content.cpython-39.pyc -------------------------------------------------------------------------------- /apps/payments/migrations/__pycache__/0003_auto_20220424_1546.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/payments/migrations/__pycache__/0003_auto_20220424_1546.cpython-39.pyc -------------------------------------------------------------------------------- /apps/authentication/migrations/__pycache__/0002_auto_20220414_1042.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crukundo/lnd-paywall/HEAD/apps/authentication/migrations/__pycache__/0002_auto_20220414_1042.cpython-39.pyc -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/layout/attrs.html: -------------------------------------------------------------------------------- 1 | {% for name, value in widget.attrs.items %}{% if value is not False %} {{ name }}{% if value is not True %}="{{ value|stringformat:'s' }}"{% endif %}{% endif %}{% endfor %} 2 | -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/layout/row.html: -------------------------------------------------------------------------------- 1 |
2 | {{ fields|safe }} 3 |
4 | -------------------------------------------------------------------------------- /apps/payments/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from apps.payments import views 3 | 4 | app_name = "payments" 5 | 6 | urlpatterns = [ 7 | path('check_payment//', views.check_payment, name="check-payment") 8 | ] -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/layout/div.html: -------------------------------------------------------------------------------- 1 |
3 | {{ fields|safe }} 4 |
5 | -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/layout/tab.html: -------------------------------------------------------------------------------- 1 | 2 | {{ links|safe }} 3 | 4 |
5 | {{ content|safe }} 6 |
7 | -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/layout/tab-link.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/blog/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | class BlogConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'apps.blog' 7 | verbose_name = _("Blog") 8 | -------------------------------------------------------------------------------- /apps/core/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | class CoreConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'apps.core' 7 | verbose_name = _("Core") 8 | -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/layout/buttonholder.html: -------------------------------------------------------------------------------- 1 |
3 | {{ fields_output|safe }} 4 |
5 | -------------------------------------------------------------------------------- /apps/payments/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class PaymentsConfig(AppConfig): 6 | default_auto_field = 'django.db.models.BigAutoField' 7 | name = 'apps.payments' 8 | verbose_name = _("Payments") -------------------------------------------------------------------------------- /apps/accounts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class AccountsConfig(AppConfig): 6 | default_auto_field = 'django.db.models.BigAutoField' 7 | name = 'apps.accounts' 8 | verbose_name = _("Accounts") 9 | -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/uni_formset.html: -------------------------------------------------------------------------------- 1 | {% with formset.management_form as form %} 2 | {% include 'bamby_forms/uni_form.html' %} 3 | {% endwith %} 4 | {% for form in formset %} 5 |
6 | {% include 'bamby_forms/uni_form.html' %} 7 |
8 | {% endfor %} 9 | -------------------------------------------------------------------------------- /apps/authentication/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | class AuthenticationConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'apps.authentication' 7 | verbose_name = _("Authentication") -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/layout/field_errors.html: -------------------------------------------------------------------------------- 1 | {% if form_show_errors and field.errors %} 2 | {% for error in field.errors %} 3 | {{ error }} 4 | {% endfor %} 5 | {% endif %} 6 | -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/layout/field_errors_block.html: -------------------------------------------------------------------------------- 1 | {% if form_show_errors and field.errors %} 2 | {% for error in field.errors %} 3 |

{{ error }}

4 | {% endfor %} 5 | {% endif %} 6 | -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/layout/alert.html: -------------------------------------------------------------------------------- 1 | 2 | {% if dismiss %}{% endif %} 3 | {{ content|safe }} 4 | -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/layout/column.html: -------------------------------------------------------------------------------- 1 |
3 | {{ fields|safe }} 4 |
5 | 6 | 7 | -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/display_form.html: -------------------------------------------------------------------------------- 1 | {% if form.form_html %} 2 | {% if include_media %}{{ form.media }}{% endif %} 3 | {% if form_show_errors %} 4 | {% include "bamby_forms/errors.html" %} 5 | {% endif %} 6 | {{ form.form_html }} 7 | {% else %} 8 | {% include "bamby_forms/uni_form.html" %} 9 | {% endif %} 10 | -------------------------------------------------------------------------------- /apps/blog/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from apps.blog.models import Article 3 | 4 | 5 | # @admin.register(Article) 6 | # class ArticleAdmin(admin.ModelAdmin): 7 | # list_display = ("title", "user", "status") 8 | # list_filter = ("user", "status", "timestamp") 9 | 10 | 11 | admin.site.register(Article) -------------------------------------------------------------------------------- /env-sample: -------------------------------------------------------------------------------- 1 | DEBUG=True 2 | SECRET_KEY=s3cr3t 3 | DATABASE_URL= 4 | ALLOWED_HOSTS=* 5 | TIME_ZONE= 6 | LND_FOLDER=PATH_TO_LND_NODE 7 | LND_MACAROON_FILE=PATH_TO_admin.macaroon 8 | LND_TLS_CERT_FILE=PATH_TO_tls.cert 9 | LND_NETWORK="regtest" 10 | MIN_VIEW_AMOUNT=1500 11 | MIN_PUBLISH_AMOUNT=2100 12 | PUBLISH_INVOICE_EXPIRY=604800 13 | VIEW_INVOICE_EXPIRY=10800 -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/errors.html: -------------------------------------------------------------------------------- 1 | {% if form.non_field_errors %} 2 |
3 | {% if form_error_title %}

{{ form_error_title }}

{% endif %} 4 |
    5 | {{ form.non_field_errors|unordered_list }} 6 |
7 |
8 | {% endif %} 9 | -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/uni_form.html: -------------------------------------------------------------------------------- 1 | {% load crispy_forms_utils %} 2 | 3 | {% specialspaceless %} 4 | {% if include_media %}{{ form.media }}{% endif %} 5 | {% if form_show_errors %} 6 | {% include "bamby_forms/errors.html" %} 7 | {% endif %} 8 | {% for field in form %} 9 | {% include field_template %} 10 | {% endfor %} 11 | {% endspecialspaceless %} 12 | -------------------------------------------------------------------------------- /apps/accounts/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from apps.accounts import views 4 | 5 | app_name = "accounts" 6 | 7 | urlpatterns = [ 8 | path("", views.SettingsRedirectView.as_view(), name="settings"), 9 | path("profile/", views.UpdateProfileView.as_view(), name="profile"), 10 | path("emails/", views.UpdateEmailsView.as_view(), name="emails"), 11 | ] -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/layout/fieldset.html: -------------------------------------------------------------------------------- 1 |
4 | {% if legend %}{{ legend|safe }}{% endif %} 5 | {{ fields|safe }} 6 |
7 | -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/errors_formset.html: -------------------------------------------------------------------------------- 1 | {% if formset.non_form_errors %} 2 |
3 | {% if formset_error_title %}

{{ formset_error_title }}

{% endif %} 4 |
    5 | {{ formset.non_form_errors|unordered_list }} 6 |
7 |
8 | {% endif %} 9 | 10 | -------------------------------------------------------------------------------- /paywalled/templates/registration/password_reset_complete.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block content %} 6 | 7 |

{% trans "Your password has been set. You may go ahead and log in now." %}

8 |

{% trans 'Log in' %}

9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/layout/formactions.html: -------------------------------------------------------------------------------- 1 | 2 | {% if label_class %} 3 |
4 | {% endif %} 5 | 6 |
7 | {{ fields_output|safe }} 8 |
9 | 10 | -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/inputs.html: -------------------------------------------------------------------------------- 1 | {% if inputs %} 2 |
3 | {% if label_class %} 4 |
5 | {% endif %} 6 | 7 |
8 | {% for input in inputs %} 9 | {% include "bamby_forms/layout/baseinput.html" %} 10 | {% endfor %} 11 |
12 |
13 | {% endif %} 14 | -------------------------------------------------------------------------------- /apps/blog/migrations/0003_remove_article_image.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.7 on 2022-04-12 20:05 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('blog', '0002_auto_20220412_2227'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='article', 15 | name='image', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/layout/help_text.html: -------------------------------------------------------------------------------- 1 | {% if field.help_text %} 2 | {% if help_text_inline %} 3 | 4 | {{ field.help_text|safe }} 5 | {% else %} 6 | 7 | {{ field.help_text|safe }} 8 | {% endif %} 9 | {% endif %} 10 | -------------------------------------------------------------------------------- /paywalled/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for paywalled project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'paywalled.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /paywalled/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for paywalled project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'paywalled.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/layout/help_text_and_errors.html: -------------------------------------------------------------------------------- 1 | {% if help_text_inline and not error_text_inline %} 2 | {% include 'bamby_forms/layout/help_text.html' %} 3 | {% endif %} 4 | 5 | {% if error_text_inline %} 6 | {% include 'bamby_forms/layout/field_errors.html' %} 7 | {% else %} 8 | {% include 'bamby_forms/layout/field_errors_block.html' %} 9 | {% endif %} 10 | 11 | {% if not help_text_inline %} 12 | {% include 'bamby_forms/layout/help_text.html' %} 13 | {% endif %} 14 | -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/layout/baseinput.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /apps/authentication/helpers.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import PermissionDenied 2 | from django.views.generic import View 3 | 4 | class AuthorRequiredMixin(View): 5 | """Mixin to validate than the loggedin user is the creator of the object 6 | to be edited or updated.""" 7 | 8 | def dispatch(self, request, *args, **kwargs): 9 | obj = self.get_object() 10 | if obj.user != self.request.user: 11 | raise PermissionDenied 12 | 13 | return super().dispatch(request, *args, **kwargs) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django==3.2.7 2 | django-compressor==2.4.1 3 | django-crispy-forms==1.13.0 4 | dj-database-url==0.5.0 5 | requests==2.26.0 6 | psycopg2-binary==2.9.1 7 | python-decouple==3.4 8 | pytz==2021.1 9 | django-debug-toolbar==3.2.4 # https://github.com/jazzband/django-debug-toolbar 10 | django-extensions==3.1.5 # https://github.com/django-extensions/django-extensions 11 | googleapis-common-protos>=1.5.6 12 | grpcio>=1.19.0 13 | grpcio-tools>=1.19.0 14 | protobuf>=3.7.0 15 | lnd-grpc 16 | django-htmx==1.9.0 17 | django-tinymce 18 | lnurl -------------------------------------------------------------------------------- /apps/blog/migrations/0004_article_uuid.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.7 on 2022-04-12 20:30 2 | 3 | from django.db import migrations, models 4 | import uuid 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('blog', '0003_remove_article_image'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='article', 16 | name='uuid', 17 | field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /apps/blog/migrations/0006_alter_article_content.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.7 on 2022-04-15 08:20 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('blog', '0005_auto_20220415_0117'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='article', 15 | name='content', 16 | field=models.CharField(blank=True, max_length=10000, verbose_name='Content'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/whole_uni_form.html: -------------------------------------------------------------------------------- 1 | {% load crispy_forms_utils %} 2 | 3 | {% specialspaceless %} 4 | {% if form_tag %}
{% endif %} 6 | {% if form_method|lower == 'post' and not disable_csrf %} 7 | {% csrf_token %} 8 | {% endif %} 9 | 10 | {% include "bamby_forms/display_form.html" %} 11 | 12 | {% include "bamby_forms/inputs.html" %} 13 | 14 | {% if form_tag %}
{% endif %} 15 | {% endspecialspaceless %} 16 | -------------------------------------------------------------------------------- /paywalled/static/js/articles.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | $(".publish").click(function () { 3 | $("input[name='status']").val("P"); 4 | $("#article-form").submit(); 5 | }); 6 | 7 | $(".update").click(function () { 8 | $("input[name='status']").val("P"); 9 | //$("input[name='edited']").prop("checked"); 10 | $("input[name='edited']").val("True"); 11 | $("#article-form").submit(); 12 | }); 13 | 14 | $(".draft").click(function () { 15 | $("input[name='status']").val("D"); 16 | $("#article-form").submit(); 17 | }); 18 | 19 | }); -------------------------------------------------------------------------------- /paywalled/templates/form_vertical.html: -------------------------------------------------------------------------------- 1 | {% for hidden in form.hidden_fields %} 2 | {{ hidden }} 3 | {% endfor %} 4 | 5 | {% for field in form.visible_fields %} 6 |
7 | 8 | {{ field }} 9 | {% for error in field.errors %} 10 | {{ error }} 11 | {% endfor %} 12 | {% if field.help_text %} 13 | {{ field.help_text }} 14 | {% endif %} 15 |
16 | {% endfor %} 17 | -------------------------------------------------------------------------------- /apps/blog/migrations/0005_auto_20220415_0117.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.7 on 2022-04-14 22:17 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('blog', '0004_article_uuid'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='article', 15 | name='slug', 16 | ), 17 | migrations.AlterField( 18 | model_name='article', 19 | name='title', 20 | field=models.CharField(max_length=255, null=True, unique=True), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /apps/payments/migrations/0002_payment_session.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.7 on 2022-04-24 12:01 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('sessions', '0001_initial'), 11 | ('payments', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='payment', 17 | name='session', 18 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='sessions.session'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /apps/payments/migrations/0003_auto_20220424_1546.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.7 on 2022-04-24 12:46 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('payments', '0002_payment_session'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='payment', 15 | name='session', 16 | ), 17 | migrations.AddField( 18 | model_name='payment', 19 | name='session_key', 20 | field=models.CharField(blank=True, max_length=40, null=True), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /paywalled/templates/registration/password_reset_email.html: -------------------------------------------------------------------------------- 1 | {% load i18n %}{% autoescape off %} 2 | 3 | {% blocktrans %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %} 4 | 5 | {% trans "Please go to the following page and choose a new password:" %} 6 | 7 | {{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %} 8 | 9 | {% trans "Your username, in case you've forgotten:" %} {{ user.get_username }} 10 | 11 | {% trans "Thanks for using our site!" %} 12 | 13 | {% blocktrans %}The {{ site_name }} team{% endblocktrans %} 14 | 15 | {% endautoescape %} 16 | -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/betterform.html: -------------------------------------------------------------------------------- 1 | {% for fieldset in form.fieldsets %} 2 |
3 | {% if fieldset.legend %} 4 | {{ fieldset.legend }} 5 | {% endif %} 6 | 7 | {% if fieldset.description %} 8 |

{{ fieldset.description }}

9 | {% endif %} 10 | 11 | {% for field in fieldset %} 12 | {% if field.is_hidden %} 13 | {{ field }} 14 | {% else %} 15 | {% include "bamby_forms/field.html" %} 16 | {% endif %} 17 | {% endfor %} 18 | {% if not forloop.last or not fieldset_open %} 19 |
20 | {% endif %} 21 | {% endfor %} 22 | -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/accordion-group.html: -------------------------------------------------------------------------------- 1 |
2 | 10 | 11 |
13 |
14 | {{ fields|safe }} 15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /paywalled/templates/registration/password_reset_done.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Parsifal{% endblock %} 4 | 5 | {% load i18n %} 6 | 7 | {% block content %} 8 | 9 |

{% trans "An email has been sent to the given address. To continue the password reset process please follow the instructions in this email." %}

10 |

11 | {% blocktrans trimmed %} 12 | If you have any problem with the password reset, please contact us at support@parsif.al. 13 | {% endblocktrans %} 14 |

15 | {% endblock content %} 16 | -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/layout/multifield.html: -------------------------------------------------------------------------------- 1 | {% load crispy_forms_field %} 2 | 3 | {% if field.is_hidden %} 4 | {{ field }} 5 | {% else %} 6 | 7 | {% if field.label %} 8 | 25 | {% endif %} 26 | 27 | {% endif %} 28 | -------------------------------------------------------------------------------- /paywalled/templates/includes/messages.html: -------------------------------------------------------------------------------- 1 | {% if messages %} {% for message in messages %} 2 |
3 | 4 |
5 | {% if message.tags == 'success' %} 6 | 7 | {% elif message.tags == 'warning' %} 8 | 9 | {% elif message.tags == 'error' %} 10 | 11 | {% else %} 12 | 13 | {% endif %} 14 |
15 |

{{ message }}

16 |
17 | {% endfor %} {% endif %} 18 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'paywalled.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /paywalled/templates/registration/signup.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load crispy_forms_filters i18n %} 4 | 5 | {% block title %}{% trans "Sign up" %} · Paywalled{% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 | 11 |
12 |
13 |

{% trans "Sign up for Paywalled" %}

14 |
15 |
16 |
17 | {% csrf_token %} 18 | {{ form|crispy }} 19 | 20 |
21 |
22 |
23 |
24 |
25 | {% endblock content %} -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/layout/uneditable_input.html: -------------------------------------------------------------------------------- 1 | {% load crispy_forms_field %} 2 | 3 | 4 |
6 | 9 |
10 | {% crispy_field field 'disabled' 'disabled' %} 11 | {% include 'bamby_forms/layout/help_text.html' %} 12 |
13 |
14 | -------------------------------------------------------------------------------- /paywalled/templates/registration/password_reset_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load crispy_forms_filters i18n %} 4 | 5 | {% block title %}{% trans "Password reset" %} · Parsifal{% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 |
11 |
12 |

{% trans "Password reset" %}

13 |
14 |
15 |
16 | {% csrf_token %} 17 | {{ form|crispy }} 18 | 19 |
20 |
21 |
22 |
23 |
24 | {% endblock content %} 25 | -------------------------------------------------------------------------------- /paywalled/templates/registration/password_change_done.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load crispy_forms_filters i18n %} 4 | 5 | {% block title %}{% trans "Change Password" %} · Parsifal{% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 | {% include "accounts/menu.html" %} 11 |
12 |
13 |
14 | {% csrf_token %} 15 |
16 |
17 |

{% trans "Change password" %}

18 |
19 |
20 | {% trans "Password changed with success!" %} 21 |
22 |
23 |
24 |
25 |
26 | {% endblock content %} 27 | -------------------------------------------------------------------------------- /apps/blog/migrations/0007_auto_20220415_1512.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.7 on 2022-04-15 12:12 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('blog', '0006_alter_article_content'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='article', 15 | options={'ordering': ('-date_published',), 'verbose_name': 'Article', 'verbose_name_plural': 'Articles'}, 16 | ), 17 | migrations.RenameField( 18 | model_name='article', 19 | old_name='timestamp', 20 | new_name='date_created', 21 | ), 22 | migrations.AddField( 23 | model_name='article', 24 | name='date_published', 25 | field=models.DateTimeField(auto_now=True), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /apps/core/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.urls import reverse 3 | from django.views.generic import RedirectView 4 | from apps.blog.models import Article 5 | from apps.payments.models import Payment 6 | from django.contrib.auth.mixins import LoginRequiredMixin 7 | from django.http import HttpRequest, HttpResponse 8 | 9 | # Create your views here. 10 | 11 | def home(request): 12 | published_articles = Article.objects.get_published() 13 | payments = Payment.objects.filter(status='complete').order_by('-modified_at') 14 | return render( 15 | request, "core/home.html", { 16 | "articles": published_articles, 17 | "payments": payments 18 | }, 19 | ) 20 | 21 | 22 | class LoginRedirectView(LoginRequiredMixin, RedirectView): 23 | def get_redirect_url(self, *args, **kwargs): 24 | return reverse("articles:list") -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/layout/radioselect_inline.html: -------------------------------------------------------------------------------- 1 | {% if field.is_hidden %} 2 | {{ field }} 3 | {% else %} 4 |
6 | 7 | {% if field.label %} 8 | 13 | {% endif %} 14 | 15 | {% include 'bamby_forms/layout/radioselect.html' %} 16 |
17 | {% endif %} 18 | -------------------------------------------------------------------------------- /apps/authentication/migrations/0002_auto_20220414_1042.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.7 on 2022-04-14 07:42 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('authentication', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='profile', 15 | name='location', 16 | ), 17 | migrations.RemoveField( 18 | model_name='profile', 19 | name='public_email', 20 | ), 21 | migrations.RemoveField( 22 | model_name='profile', 23 | name='url', 24 | ), 25 | migrations.AddField( 26 | model_name='profile', 27 | name='node_pubkey', 28 | field=models.CharField(blank=True, max_length=80, null=True, unique=True), 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/layout/checkboxselectmultiple_inline.html: -------------------------------------------------------------------------------- 1 | {% if field.is_hidden %} 2 | {{ field }} 3 | {% else %} 4 |
6 | 7 | {% if field.label %} 8 | 13 | {% endif %} 14 | 15 | {% include 'bamby_forms/layout/checkboxselectmultiple.html' %} 16 |
17 | {% endif %} 18 | -------------------------------------------------------------------------------- /paywalled/templates/blog/update_article.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static i18n %} 3 | {% load crispy_forms_tags %} 4 | 5 | {% block head %} 6 | {% endblock head %} 7 | 8 | {% block content %} 9 | 10 |
12 | {% csrf_token %} 13 | {{ form|crispy }} 14 |
15 | 16 | 17 | {% trans 'Cancel' %} 18 |
19 |
20 | {{ form.media }} 21 | 22 | {% endblock content %} 23 | 24 | {% block modal %} 25 | 26 | {% endblock modal %} -------------------------------------------------------------------------------- /paywalled/templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load crispy_forms_filters i18n %} 4 | 5 | {% block title %}{% trans "Log in" %} · Parsifal{% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 |
11 |
12 |

{% trans "Log in" %}

13 |
14 |
15 |
16 | {% csrf_token %} 17 | {{ form|crispy }} 18 | 19 | {% trans "Forgot your password?" %} 20 |
21 |
22 |
23 |
24 |
25 | {% endblock content %} 26 | -------------------------------------------------------------------------------- /apps/blog/migrations/0008_auto_20220422_2237.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.7 on 2022-04-22 19:37 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('blog', '0007_auto_20220415_1512'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='article', 18 | name='content', 19 | field=models.CharField(blank=True, max_length=40000, verbose_name='Content'), 20 | ), 21 | migrations.AlterField( 22 | model_name='article', 23 | name='user', 24 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='articles', to=settings.AUTH_USER_MODEL), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/whole_uni_formset.html: -------------------------------------------------------------------------------- 1 | {% load crispy_forms_tags %} 2 | {% load crispy_forms_utils %} 3 | 4 | {% specialspaceless %} 5 | {% if formset_tag %} 6 |
8 | {% endif %} 9 | {% if formset_method|lower == 'post' and not disable_csrf %} 10 | {% csrf_token %} 11 | {% endif %} 12 | 13 |
14 | {{ formset.management_form|crispy }} 15 |
16 | 17 | {% include "bamby_forms/errors_formset.html" %} 18 | 19 | {% for form in formset %} 20 | {% include "bamby_forms/display_form.html" %} 21 | {% endfor %} 22 | 23 | {% if inputs %} 24 |
25 | {% for input in inputs %} 26 | {% include "bamby_forms/layout/baseinput.html" %} 27 | {% endfor %} 28 |
29 | {% endif %} 30 | {% if formset_tag %}
{% endif %} 31 | {% endspecialspaceless %} 32 | -------------------------------------------------------------------------------- /paywalled/templates/partials/article_payment.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | 4 |
    5 |
    6 |
    7 |
    8 |

    9 | {% if payment.user %}{{ payment.user.profile.get_screen_name }}{% else %} {% endif %} paid 11 | {{ payment.satoshi_amount }} sats to 12 | {{ payment.purpose }} {% if mode == 'general' %}"{{ payment.article.title }}"{% endif %} 14 |

    {{ payment.created_at }} 15 |
    16 |
    17 |
    18 |
  • -------------------------------------------------------------------------------- /paywalled/templates/registration/password_change_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load crispy_forms_filters i18n %} 4 | 5 | {% block title %}{% trans "Change Password" %} · Parsifal{% endblock %} 6 | 7 | {% block content %} 8 |
    9 |
    10 | {% include "accounts/menu.html" %} 11 |
    12 |
    13 |
    14 | {% csrf_token %} 15 |
    16 |
    17 |

    {% trans "Change password" %}

    18 |
    19 |
    20 | {{ form|crispy }} 21 |
    22 | 25 |
    26 |
    27 |
    28 |
    29 | {% endblock content %} 30 | -------------------------------------------------------------------------------- /apps/blog/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from django.urls import path 3 | from requests import delete 4 | 5 | from apps.blog.views import ( 6 | EditArticleView, 7 | list_drafts, 8 | list_articles, 9 | create_new_article, 10 | publish_new_article, 11 | article_detail, 12 | delete_article, 13 | delete_draft_article 14 | ) 15 | 16 | app_name = "articles" 17 | urlpatterns = [ 18 | path("", view=list_articles, name="list"), 19 | path("new/", view=create_new_article, name="write_new"), 20 | path("publish//", view=publish_new_article, name="publish_article"), 21 | path("drafts/", view=list_drafts, name="drafts"), 22 | path("edit//", EditArticleView.as_view(), name="edit_article"), 23 | path("delete//", view=delete_article, name="delete_article"), 24 | path("delete_draft//", view=delete_draft_article, name="delete_draft"), 25 | path("/", view=article_detail, name="article"), 26 | ] -------------------------------------------------------------------------------- /paywalled/templates/registration/password_reset_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load crispy_forms_filters i18n %} 4 | 5 | {% block title %}{% trans "Password reset" %} · Parsifal{% endblock %} 6 | 7 | {% block content %} 8 | {% if validlink %} 9 |
    10 |
    11 |
    12 |
    13 |

    {% trans "Confirm password reset" %}

    14 |
    15 |
    16 |
    17 | {% csrf_token %} 18 | {{ form|crispy }} 19 | 20 |
    21 |
    22 |
    23 |
    24 |
    25 | {% else %} 26 |

    {% trans "Invalid password reset link." %}

    27 | {% endif %} 28 | {% endblock content %} 29 | -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/layout/inline_field.html: -------------------------------------------------------------------------------- 1 | {% load crispy_forms_field %} 2 | 3 | {% if field.is_hidden %} 4 | {{ field }} 5 | {% else %} 6 | {% if field|is_checkbox %} 7 |
    8 | 12 |
    13 | {% else %} 14 |
    15 | 18 | {% crispy_field field 'placeholder' field.label %} 19 |
    20 | {% endif %} 21 | {% endif %} 22 | -------------------------------------------------------------------------------- /paywalled/templates/includes/footer.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | 7 |
    8 | 9 | v{{ parsifal_version }} 10 | 11 |
    12 | 17 |
    18 |
    19 |
    20 | -------------------------------------------------------------------------------- /paywalled/templates/blog/publish_article.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static i18n %} 3 | {% load crispy_forms_tags %} 4 | 5 | {% block head %} 6 | {% endblock head %} 7 | 8 | {% block content %} 9 | 10 |
    11 | {% csrf_token %} 12 | {% crispy form %} 13 |
    14 | 15 | {% endblock content %} 16 | 17 | {% block javascript %} 18 | 19 | 20 | 36 | {% endblock javascript %} -------------------------------------------------------------------------------- /paywalled/static/js/copy.js: -------------------------------------------------------------------------------- 1 | //the helper function 2 | let createCopy = function (textToCopy, triggerElementId, callback = null) { 3 | //add event listner to elementtrigger 4 | let trigger = document.getElementById(triggerElementId); 5 | trigger.addEventListener("click", function () { 6 | //create the readonly textarea with the text in it and hide it 7 | let tarea = document.createElement("textarea"); 8 | tarea.setAttribute("id", triggerElementId + "-copyarea"); 9 | tarea.setAttribute("readonly", "readonly"); 10 | tarea.setAttribute( 11 | "style", 12 | "opacity: 0; position: absolute; z-index: -1; top: 0; left: -9999px;" 13 | ); 14 | tarea.appendChild(document.createTextNode(textToCopy)); 15 | document.body.appendChild(tarea); 16 | 17 | //select and copy the text in the readonly text area 18 | tarea.select(); 19 | document.execCommand("copy"); 20 | 21 | //remove the element from the DOM 22 | document.body.removeChild(tarea); 23 | 24 | //fire callback function if provided 25 | if (typeof callback === "function" && callback()) { 26 | callback(); 27 | } 28 | }); 29 | }; -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/layout/field_with_buttons.html: -------------------------------------------------------------------------------- 1 | {% load crispy_forms_field %} 2 | 3 | 6 | {% if field.label and form_show_labels %} 7 | 12 | {% endif %} 13 | 14 |
    15 |
    16 | {% crispy_field field %} 17 | {{ buttons|safe }} 19 |
    20 | {% include 'bamby_forms/layout/help_text_and_errors.html' %} 21 |
    22 | 23 | -------------------------------------------------------------------------------- /apps/authentication/backends.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.contrib.auth.backends import ModelBackend 3 | 4 | 5 | class CaseInsensitiveUsernameOrEmailModelBackend(ModelBackend): 6 | def authenticate(self, request, username=None, password=None, **kwargs): 7 | UserModel = get_user_model() 8 | if username is None: 9 | username = kwargs.get(UserModel.USERNAME_FIELD) 10 | try: 11 | try: 12 | case_insensitive_username_field = "{}__iexact".format(UserModel.USERNAME_FIELD) 13 | user = UserModel._default_manager.get(**{case_insensitive_username_field: username}) 14 | except UserModel.DoesNotExist: 15 | case_insensitive_email_field = "{}__iexact".format(UserModel.EMAIL_FIELD) 16 | user = UserModel._default_manager.get(**{case_insensitive_email_field: username}) 17 | except UserModel.DoesNotExist: 18 | # Run the default password hasher once to reduce the timing 19 | # difference between an existing and a non-existing user (#20760). 20 | UserModel().set_password(password) 21 | else: 22 | if user.check_password(password) and self.user_can_authenticate(user): 23 | return user -------------------------------------------------------------------------------- /paywalled/templates/blog/create_article.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static i18n %} 3 | {% load crispy_forms_tags %} 4 | 5 | {% block head %} 6 | {% endblock head %} 7 | 8 | {% block content %} 9 | 10 |
    11 | {% csrf_token %} 12 | {{ form|crispy }} 13 |
    14 | 17 | 18 | {% trans 'Cancel' %} 19 | 20 |
    21 | {% for payment in article.payments.all %} 22 |

    {{ payment.payment_request }}

    23 | {% endfor %} 24 |
    25 |
    26 |
    27 | {{ form.media }} 28 | 29 | {% endblock content %} 30 | 31 | {% block modal %} 32 | 33 | {% endblock modal %} -------------------------------------------------------------------------------- /apps/authentication/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.7 on 2022-04-12 17:45 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Profile', 19 | fields=[ 20 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('public_email', models.EmailField(blank=True, max_length=254, verbose_name='public email')), 22 | ('location', models.CharField(blank=True, max_length=50, verbose_name='location')), 23 | ('url', models.CharField(blank=True, max_length=50, verbose_name='url')), 24 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user')), 25 | ], 26 | options={ 27 | 'verbose_name': 'profile', 28 | 'verbose_name_plural': 'profiles', 29 | 'db_table': 'auth_profile', 30 | }, 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /apps/authentication/models.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | 3 | from django.conf import settings as django_settings 4 | from django.contrib.auth.models import User 5 | from django.db import models 6 | from django.db.models.signals import post_save 7 | from django.utils.translation import gettext_lazy as _ 8 | 9 | 10 | class Profile(models.Model): 11 | user = models.OneToOneField(User, on_delete=models.CASCADE, verbose_name=_("user")) 12 | node_pubkey = models.CharField(max_length=80, unique=True, blank=True, null=True) 13 | 14 | class Meta: 15 | verbose_name = _("profile") 16 | verbose_name_plural = _("profiles") 17 | db_table = "auth_profile" 18 | 19 | def __str__(self): 20 | return self.get_screen_name() 21 | 22 | def get_screen_name(self): 23 | try: 24 | if self.user.get_full_name(): 25 | return self.user.get_full_name() 26 | else: 27 | return self.user.username 28 | except Exception: 29 | return self.user.username 30 | 31 | 32 | def create_user_profile(sender, instance, created, **kwargs): 33 | if created: 34 | Profile.objects.create(user=instance) 35 | 36 | 37 | def save_user_profile(sender, instance, **kwargs): 38 | instance.profile.save() 39 | 40 | 41 | post_save.connect(create_user_profile, sender=User) 42 | post_save.connect(save_user_profile, sender=User) -------------------------------------------------------------------------------- /apps/accounts/views.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from django.contrib.auth.decorators import login_required 5 | from django.contrib.auth.mixins import LoginRequiredMixin 6 | from django.contrib.messages.views import SuccessMessageMixin 7 | from django.urls import reverse_lazy 8 | from django.utils.translation import gettext, gettext_lazy as _ 9 | from django.views.generic import RedirectView, UpdateView 10 | 11 | from apps.accounts.forms import ProfileForm, UserEmailForm 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | class SettingsRedirectView(LoginRequiredMixin, RedirectView): 17 | pattern_name = "settings:profile" 18 | 19 | 20 | class UpdateProfileView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): 21 | form_class = ProfileForm 22 | success_url = reverse_lazy("settings:profile") 23 | success_message = _("Your profile was updated with success!") 24 | template_name = "accounts/profile.html" 25 | 26 | def get_object(self, queryset=None): 27 | return self.request.user.profile 28 | 29 | 30 | class UpdateEmailsView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): 31 | form_class = UserEmailForm 32 | success_url = reverse_lazy("settings:emails") 33 | success_message = _("Account email was updated with success!") 34 | template_name = "accounts/emails.html" 35 | 36 | def get_object(self, queryset=None): 37 | return self.request.user 38 | -------------------------------------------------------------------------------- /paywalled/templates/partials/article_list.html: -------------------------------------------------------------------------------- 1 | {% load humanize %} 2 |
    3 |
    4 |
    5 |
    6 | 7 | 8 | 9 |
    10 |
    11 |

    12 | {{ article.title }} 13 |

    14 |

    Updated 15 | {{ article.date_published|naturaltime }} 16 | . Total paid views: {{ article.get_view_count }}. Total reward: 17 | {{ article.get_total_reward.total_reward }} sats

    18 |
    19 | {% if request.user == article.user %} 20 |
    21 | 22 | 24 |
    25 | {% endif %} 26 |
    27 |
    28 |
    -------------------------------------------------------------------------------- /apps/authentication/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.auth.forms import UserCreationForm, UsernameField 3 | from django.utils.translation import gettext 4 | 5 | from apps.authentication.validators import ( 6 | ASCIIUsernameValidator, 7 | validate_case_insensitive_email, 8 | validate_case_insensitive_username, 9 | validate_forbidden_usernames, 10 | ) 11 | 12 | 13 | class ASCIIUsernameField(UsernameField): 14 | def __init__(self, *args, **kwargs): 15 | super().__init__(*args, **kwargs) 16 | self.validators.append(ASCIIUsernameValidator()) 17 | self.validators.append(validate_forbidden_usernames) 18 | self.validators.append(validate_case_insensitive_username) 19 | 20 | 21 | class SignUpForm(UserCreationForm): 22 | 23 | class Meta(UserCreationForm.Meta): 24 | fields = ("username", "email") 25 | field_classes = {"username": ASCIIUsernameField} 26 | 27 | def __init__(self, *args, **kwargs): 28 | self.request = kwargs.pop("request", None) 29 | super().__init__(*args, **kwargs) 30 | self.fields["username"].help_text = gettext("Required. 150 characters or fewer. Letters, digits and . _ only.") 31 | self.fields["email"].validators.append(validate_case_insensitive_email) 32 | self.fields["email"].required = True 33 | 34 | def clean(self): 35 | cleaned_data = super().clean() 36 | return cleaned_data 37 | 38 | def save(self, commit=True): 39 | user = super().save(commit=commit) 40 | return user -------------------------------------------------------------------------------- /apps/authentication/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.contrib.auth import login 3 | from django.core.exceptions import ValidationError 4 | from django.shortcuts import redirect 5 | from django.utils.decorators import method_decorator 6 | from django.utils.functional import cached_property 7 | from django.utils.translation import gettext as _ 8 | from django.views.decorators.cache import never_cache 9 | from django.views.decorators.csrf import csrf_protect 10 | from django.views.decorators.debug import sensitive_post_parameters 11 | from django.views.generic import FormView 12 | 13 | from apps.authentication.forms import SignUpForm 14 | 15 | 16 | @method_decorator([sensitive_post_parameters(), csrf_protect, never_cache], name="dispatch") 17 | class SignUpView(FormView): 18 | form_class = SignUpForm 19 | template_name = "registration/signup.html" 20 | 21 | def get_form_kwargs(self): 22 | kwargs = super().get_form_kwargs() 23 | kwargs.update(request=self.request) 24 | return kwargs 25 | 26 | def form_valid(self, form): 27 | user = form.save() 28 | login(self.request, user) 29 | messages.success(self.request, _("Your account was successfully created.")) 30 | return redirect(user) 31 | 32 | def form_invalid(self, form): 33 | messages.error( 34 | self.request, 35 | _( 36 | "There were some problems while creating your account. " 37 | "Please review the form below before submitting it again." 38 | ), 39 | ) 40 | return super().form_invalid(form) 41 | 42 | def get_context_data(self, **kwargs): 43 | return super().get_context_data(**kwargs) -------------------------------------------------------------------------------- /apps/payments/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.7 on 2022-04-13 23:04 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ('blog', '0004_article_uuid'), 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='Payment', 20 | fields=[ 21 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('purpose', models.CharField(choices=[('publish', 'Publish'), ('view', 'View'), ('edit', 'Edit'), ('comment', 'Comment')], max_length=10)), 23 | ('satoshi_amount', models.IntegerField()), 24 | ('r_hash', models.CharField(max_length=64)), 25 | ('payment_request', models.CharField(max_length=1000)), 26 | ('status', models.CharField(choices=[('pending_invoice', 'Pending Invoice'), ('pending_payment', 'Pending Payment'), ('complete', 'Complete'), ('error', 'Error')], default='pending_invoice', max_length=50)), 27 | ('created_at', models.DateTimeField(auto_now_add=True)), 28 | ('modified_at', models.DateTimeField(auto_now=True)), 29 | ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='blog.article')), 30 | ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='payments', to=settings.AUTH_USER_MODEL)), 31 | ], 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /apps/blog/migrations/0002_auto_20220412_2227.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.7 on 2022-04-12 19:27 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | import markdownx.models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 13 | ('blog', '0001_initial'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Article', 19 | fields=[ 20 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('image', models.ImageField(upload_to='articles_pictures/%Y/%m/%d/', verbose_name='Featured image')), 22 | ('timestamp', models.DateTimeField(auto_now_add=True)), 23 | ('title', models.CharField(max_length=255, unique=True)), 24 | ('slug', models.SlugField(blank=True, max_length=80, null=True)), 25 | ('status', models.CharField(choices=[('D', 'Draft'), ('P', 'Published')], default='D', max_length=1)), 26 | ('content', markdownx.models.MarkdownxField()), 27 | ('edited', models.BooleanField(default=False)), 28 | ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='author', to=settings.AUTH_USER_MODEL)), 29 | ], 30 | options={ 31 | 'verbose_name': 'Article', 32 | 'verbose_name_plural': 'Articles', 33 | 'ordering': ('-timestamp',), 34 | }, 35 | ), 36 | migrations.DeleteModel( 37 | name='Entry', 38 | ), 39 | ] 40 | -------------------------------------------------------------------------------- /apps/blog/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.7 on 2022-04-12 17:45 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Entry', 19 | fields=[ 20 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('title', models.CharField(max_length=255)), 22 | ('slug', models.SlugField(blank=True, max_length=255, null=True)), 23 | ('content', models.TextField(blank=True, max_length=4000, null=True)), 24 | ('summary', models.TextField(blank=True, max_length=255, null=True)), 25 | ('status', models.CharField(choices=[('D', 'Draft'), ('H', 'Hidden'), ('P', 'Published')], max_length=10)), 26 | ('start_publication', models.DateTimeField()), 27 | ('creation_date', models.DateTimeField(auto_now_add=True)), 28 | ('last_update', models.DateTimeField(auto_now=True)), 29 | ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 30 | ('edited_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL)), 31 | ], 32 | options={ 33 | 'verbose_name': 'entry', 34 | 'verbose_name_plural': 'entries', 35 | }, 36 | ), 37 | ] 38 | -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/layout/radioselect.html: -------------------------------------------------------------------------------- 1 | {% load crispy_forms_filters %} 2 | {% load l10n %} 3 | 4 |
    5 | 6 | {% for group, options, index in field|optgroups %} 7 | {% if group %}{{ group }}{% endif %} 8 | {% for option in options %} 9 |
    11 | 15 | 19 | {% if field.errors and forloop.last and not inline_class and forloop.parentloop.last %} 20 | {% include 'bamby_forms/layout/field_errors_block.html' %} 21 | {% endif %} 22 |
    23 | {% endfor %} 24 | {% endfor %} 25 | {% if field.errors and inline_class %} 26 |
    28 | {# the following input is only meant to allow boostrap to render the error message as it has to be after an invalid input. As the input has no name, no data will be sent. #} 29 | 30 | {% include 'bamby_forms/layout/field_errors_block.html' %} 31 |
    32 | {% endif %} 33 | 34 | {% include 'bamby_forms/layout/help_text.html' %} 35 |
    36 | -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/table_inline_formset.html: -------------------------------------------------------------------------------- 1 | {% load crispy_forms_tags %} 2 | {% load crispy_forms_utils %} 3 | {% load crispy_forms_field %} 4 | 5 | {% specialspaceless %} 6 | {% if formset_tag %} 7 |
    9 | {% endif %} 10 | {% if formset_method|lower == 'post' and not disable_csrf %} 11 | {% csrf_token %} 12 | {% endif %} 13 | 14 |
    15 | {{ formset.management_form|crispy }} 16 |
    17 | 18 | 19 | 20 | {% if formset.readonly and not formset.queryset.exists %} 21 | {% else %} 22 | 23 | {% for field in formset.forms.0 %} 24 | {% if field.label and not field.is_hidden %} 25 | 26 | {{ field.label|safe }}{% if field.field.required and not field|is_checkbox %}*{% endif %} 28 | 29 | {% endif %} 30 | {% endfor %} 31 | 32 | {% endif %} 33 | 34 | 35 | 36 | 37 | {% for field in formset.empty_form %} 38 | {% include 'bamby_forms/field.html' with tag="td" form_show_labels=False %} 39 | {% endfor %} 40 | 41 | 42 | {% for form in formset %} 43 | {% if form_show_errors and not form.is_extra %} 44 | {% include "bamby_forms/errors.html" %} 45 | {% endif %} 46 | 47 | 48 | {% for field in form %} 49 | {% include 'bamby_forms/field.html' with tag="td" form_show_labels=False %} 50 | {% endfor %} 51 | 52 | {% endfor %} 53 | 54 | 55 | 56 | {% include "bamby_forms/inputs.html" %} 57 | 58 | {% if formset_tag %} 59 | {% endif %} 60 | {% endspecialspaceless %} 61 | -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/layout/checkboxselectmultiple.html: -------------------------------------------------------------------------------- 1 | {% load crispy_forms_filters %} 2 | {% load l10n %} 3 | 4 |
    5 | 6 | {% for group, options, index in field|optgroups %} 7 | {% if group %}{{ group }}{% endif %} 8 | {% for option in options %} 9 |
    11 | 15 | 19 | {% if field.errors and forloop.last and not inline_class and forloop.parentloop.last %} 20 | {% include 'bamby_forms/layout/field_errors_block.html' %} 21 | {% endif %} 22 |
    23 | {% endfor %} 24 | {% endfor %} 25 | {% if field.errors and inline_class %} 26 |
    28 | {# the following input is only meant to allow boostrap to render the error message as it has to be after an invalid input. As the input has no name, no data will be sent. #} 29 | 30 | {% include 'bamby_forms/layout/field_errors_block.html' %} 31 |
    32 | {% endif %} 33 | 34 | {% include 'bamby_forms/layout/help_text.html' %} 35 |
    36 | -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/layout/prepended_appended_text.html: -------------------------------------------------------------------------------- 1 | {% load crispy_forms_field %} 2 | 3 | {% if field.is_hidden %} 4 | {{ field }} 5 | {% else %} 6 |
    8 | 9 | {% if field.label and form_show_labels %} 10 | 15 | {% endif %} 16 | 17 |
    18 |
    19 | {% if crispy_prepended_text %} 20 |
    21 | {{ crispy_prepended_text|safe }} 22 |
    23 | {% endif %} 24 | {% if field|is_select and use_custom_control %} 25 | {% crispy_field field 'class' 'custom-select' %} 26 | {% else %} 27 | {% crispy_field field %} 28 | {% endif %} 29 | {% if crispy_appended_text %} 30 |
    31 | {{ crispy_appended_text|safe }} 32 |
    33 | {% endif %} 34 | {% if error_text_inline %} 35 | {% include 'bamby_forms/layout/field_errors.html' %} 36 | {% else %} 37 | {% include 'bamby_forms/layout/field_errors_block.html' %} 38 | {% endif %} 39 |
    40 | {% if not help_text_inline %} 41 | {% include 'bamby_forms/layout/help_text.html' %} 42 | {% endif %} 43 |
    44 | 45 |
    46 | {% endif %} 47 | -------------------------------------------------------------------------------- /apps/accounts/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.auth.models import User 3 | from django.core.exceptions import ValidationError 4 | from django.db import transaction 5 | from django.utils.translation import gettext, gettext_lazy as _ 6 | 7 | from apps.authentication.models import Profile 8 | 9 | 10 | class UserEmailForm(forms.ModelForm): 11 | email = forms.CharField( 12 | label=_("Email"), 13 | widget=forms.EmailInput(attrs={"class": "form-control"}), 14 | max_length=254, 15 | help_text=_( 16 | "This email account will not be publicly available. " 17 | "It is used for your Parsifal account management, " 18 | "such as internal notifications and password reset." 19 | ), 20 | ) 21 | 22 | class Meta: 23 | model = User 24 | fields = ("email",) 25 | 26 | def clean_email(self): 27 | email = self.cleaned_data.get("email") 28 | email = User.objects.normalize_email(email) 29 | if User.objects.exclude(pk=self.instance.pk).filter(email__iexact=email).exists(): 30 | raise ValidationError(gettext("User with this Email already exists.")) 31 | return email 32 | 33 | 34 | class ProfileForm(forms.ModelForm): 35 | first_name = forms.CharField(label=_("First name"), max_length=150, required=False) 36 | last_name = forms.CharField(label=_("Last name"), max_length=150, required=False) 37 | 38 | def __init__(self, *args, **kwargs): 39 | super().__init__(*args, **kwargs) 40 | self.fields["first_name"].initial = self.instance.user.first_name 41 | self.fields["last_name"].initial = self.instance.user.last_name 42 | 43 | class Meta: 44 | model = Profile 45 | fields = ("first_name", "last_name", "node_pubkey") 46 | 47 | @transaction.atomic() 48 | def save(self, commit=True): 49 | self.instance.user.first_name = self.cleaned_data["first_name"] 50 | self.instance.user.last_name = self.cleaned_data["last_name"] 51 | if commit: 52 | self.instance.user.save() 53 | return super().save(commit) -------------------------------------------------------------------------------- /apps/payments/views.py: -------------------------------------------------------------------------------- 1 | import re 2 | from django.shortcuts import render, get_object_or_404 3 | from apps.blog.models import Article 4 | from django.views.decorators.http import require_http_methods, require_GET 5 | from django.http import HttpRequest, HttpResponse 6 | from django.conf import settings 7 | from django_htmx.http import HttpResponseStopPolling 8 | 9 | from apps.payments.models import Payment 10 | 11 | import codecs 12 | from lnd_grpc import lnd_grpc 13 | 14 | import lnd_grpc.protos.rpc_pb2 as ln 15 | 16 | lnrpc = lnd_grpc.Client( 17 | lnd_dir = settings.LND_FOLDER, 18 | macaroon_path = settings.LND_MACAROON_FILE, 19 | tls_cert_path = settings.LND_TLS_CERT_FILE, 20 | network = settings.LND_NETWORK, 21 | ) 22 | 23 | 24 | # Create your views here. 25 | 26 | def check_payment(request, pk): 27 | """ 28 | Checks if the Lightning payment has been received for this invoice 29 | """ 30 | # get the payment in question 31 | payment = Payment.objects.get(pk=pk) 32 | 33 | r_hash_base64 = payment.r_hash.encode('utf-8') 34 | r_hash_bytes = codecs.decode(r_hash_base64, 'base64') 35 | invoice_resp = lnrpc.lookup_invoice(r_hash=r_hash_bytes) 36 | 37 | 38 | if request.htmx: 39 | if invoice_resp.settled: 40 | # create session key 41 | if not request.session.session_key: 42 | request.session.create() 43 | # Payment complete 44 | payment.status = 'complete' 45 | if request.user.is_authenticated: 46 | payment.user = request.user 47 | payment.session_key = request.session.session_key 48 | else: 49 | # if user is anon, save in session 50 | payment.session_key = request.session.session_key 51 | payment.save() 52 | return HttpResponseStopPolling("") 53 | else: 54 | # Payment not received 55 | return HttpResponse("") 56 | 57 | -------------------------------------------------------------------------------- /apps/blog/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from crispy_forms.helper import FormHelper 3 | from crispy_forms.layout import Layout, Row, Column, ButtonHolder, Submit, HTML, Field, Button 4 | from tinymce.widgets import TinyMCE 5 | from apps.blog.models import Article 6 | 7 | 8 | class ArticleForm(forms.ModelForm): 9 | status = forms.CharField(widget=forms.HiddenInput()) 10 | edited = forms.BooleanField( 11 | widget=forms.HiddenInput(), required=False, initial=False 12 | ) 13 | title = forms.CharField( 14 | widget=forms.TextInput(attrs={"class": "form-control form-control-lg", "placeholder": "Article title"}), 15 | max_length=400, 16 | required=True, 17 | label="Title of your masterpiece", 18 | ) 19 | content = forms.CharField( 20 | widget=TinyMCE(attrs={'cols': 80, 'rows': 30}), 21 | required=False, 22 | ) 23 | 24 | def __init__(self, *args, **kwargs): 25 | super().__init__(*args, **kwargs) 26 | self.helper = FormHelper(self) 27 | self.helper.layout = Layout( 28 | "title", 29 | "content", 30 | "status", 31 | "edited", 32 | HTML( 33 | """ 34 | {% if payment_made %} 35 | 36 | {% else %} 37 |
    38 | {% if invoice %} 39 | {% include 'partials/partial_invoice.html' with purpose='publish' %} 40 | {% endif %} 41 |
    42 | {% endif %} 43 | """ 44 | ), 45 | ButtonHolder( 46 | Submit("publish", "Publish article", css_class="publish btn btn-lg btn-success mr-2"), 47 | Submit("save", "Save as draft", css_class="draft btn btn-lg btn-subtle-primary mr-2"), 48 | HTML( 49 | """ 50 | Cancel 51 | """ 52 | ) 53 | ), 54 | ) 55 | 56 | class Meta: 57 | model = Article 58 | fields = ["title", "content", "status", "edited"] -------------------------------------------------------------------------------- /paywalled/urls.py: -------------------------------------------------------------------------------- 1 | """paywalled URL Configuration 2 | """ 3 | from django.contrib import admin 4 | from django.urls import path, include 5 | from django.conf.urls import url 6 | from django.views.generic import RedirectView, TemplateView 7 | from django.conf.urls.static import static 8 | from django.conf import settings 9 | from django.apps import apps 10 | from apps.authentication import views as auth_views 11 | from apps.blog import views as blog_views 12 | from apps.core import views as core_views 13 | from django.contrib.sitemaps.views import sitemap 14 | from django.views import defaults as default_views 15 | 16 | urlpatterns = [ 17 | path("", core_views.home, name="home"), 18 | path("", include("django.contrib.auth.urls")), 19 | path("login/success/", core_views.LoginRedirectView.as_view(), name="login_redirect"), 20 | path("signup/", auth_views.SignUpView.as_view(), name="signup"), 21 | path("signin/", RedirectView.as_view(pattern_name="login"), name="signin"), 22 | path("articles/", include("apps.blog.urls", namespace="articles")), 23 | path("payments/", include("apps.payments.urls", namespace="payments")), 24 | path('admin/', admin.site.urls), 25 | path("sitemap.xml", sitemap, name="sitemap"), 26 | path("robots.txt", TemplateView.as_view(template_name="robots.txt", content_type="text/plain")), 27 | path("settings/", include("apps.accounts.urls", namespace="settings")), 28 | path('tinymce/', include('tinymce.urls')), 29 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 30 | 31 | if settings.DEBUG: 32 | # This allows the error pages to be debugged during development 33 | urlpatterns += [ 34 | url( 35 | r"^400/$", 36 | default_views.bad_request, 37 | kwargs={"exception": Exception("Bad Request!")}, 38 | ), 39 | url( 40 | r"^403/$", 41 | default_views.permission_denied, 42 | kwargs={"exception": Exception("Permission Denied")}, 43 | ), 44 | url( 45 | r"^404/$", 46 | default_views.page_not_found, 47 | kwargs={"exception": Exception("Page not Found")}, 48 | ), 49 | url(r"^500/$", default_views.server_error), 50 | ] 51 | if "debug_toolbar" in settings.INSTALLED_APPS: 52 | import debug_toolbar 53 | 54 | urlpatterns = [url(r"^__debug__/", include(debug_toolbar.urls))] + urlpatterns -------------------------------------------------------------------------------- /paywalled/templates/includes/header.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 |
    3 |
    4 |
    5 | Paywalled 6 |
    7 |
    8 | 9 | {% block header-far-right %} 10 |
    11 | 21 | {% if user.is_authenticated %} 22 | 40 | {% else %} 41 | 42 | 43 | {% endif %} 44 |
    45 | {% endblock %} 46 |
    47 |
    48 |
    -------------------------------------------------------------------------------- /paywalled/templates/403.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Parsifal · 403 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 17 | 18 | 19 |
    20 |
    21 |
    22 |
    23 |

    Parsifal

    24 |

    403
    Permission denied

    25 |

    We think you should not be here.

    26 | Home 27 | · 28 | Blog 29 | · 30 | Help 31 |
    32 |
    33 |
    34 |
    35 |
    36 |
    37 |
    38 |
    39 | 42 | 47 |
    48 |
    49 |
    50 | 51 | 52 | 53 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /paywalled/templates/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Parsifal · 404 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 17 | 18 | 19 |
    20 |
    21 |
    22 |
    23 |

    Parsifal

    24 |

    404
    Page not found

    25 |

    The requested url was not found on this server.

    26 | Home 27 | · 28 | Blog 29 | · 30 | Help 31 |
    32 |
    33 |
    34 |
    35 |
    36 |
    37 |
    38 |
    39 | 42 | 47 |
    48 |
    49 |
    50 | 51 | 52 | 53 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /paywalled/templates/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Parsifal · 500 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 17 | 18 | 19 |
    20 |
    21 |
    22 |
    23 |

    Parsifal

    24 |

    500
    Internal server error

    25 |

    Something went wrong. That's all we know.

    26 | Home 27 | · 28 | Blog 29 | · 30 | Help 31 |
    32 |
    33 |
    34 |
    35 |
    36 |
    37 |
    38 |
    39 | 42 | 47 |
    48 |
    49 |
    50 | 51 | 52 | 53 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /apps/payments/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.conf import settings 3 | from apps.blog.models import Article 4 | 5 | import codecs 6 | from lnd_grpc import lnd_grpc 7 | 8 | from django.http import HttpRequest, HttpResponse 9 | from django.conf import settings 10 | from django.contrib.sessions.models import Session 11 | 12 | import lnd_grpc.protos.rpc_pb2 as ln 13 | 14 | lnrpc = lnd_grpc.Client( 15 | lnd_dir = settings.LND_FOLDER, 16 | macaroon_path = settings.LND_MACAROON_FILE, 17 | tls_cert_path = settings.LND_TLS_CERT_FILE, 18 | network = settings.LND_NETWORK, 19 | ) 20 | 21 | # Create your models here. 22 | 23 | class Payment(models.Model): 24 | 25 | PAYMENT_STATUS_CHOICES = ( 26 | ('pending_invoice', 'Pending Invoice'), # Should be atomic 27 | ('pending_payment', 'Pending Payment'), 28 | ('complete', 'Complete'), 29 | ('error', 'Error'), 30 | ) 31 | 32 | PUBLISH = "publish" 33 | VIEW = "view" 34 | EDIT = "edit" 35 | COMMENT = "comment" 36 | PAYMENT_PURPOSE_CHOICES = ( 37 | (PUBLISH, 'Publish'), 38 | (VIEW, 'View'), 39 | (EDIT, 'Edit'), 40 | (COMMENT, 'Comment'), 41 | ) 42 | 43 | user = models.ForeignKey( 44 | settings.AUTH_USER_MODEL, 45 | null=True, 46 | related_name="payments", 47 | on_delete=models.SET_NULL, 48 | ) 49 | session_key = models.CharField(max_length=40, blank=True, null=True) 50 | article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='payments') 51 | purpose = models.CharField(max_length=10, choices=PAYMENT_PURPOSE_CHOICES) 52 | 53 | satoshi_amount = models.IntegerField() 54 | r_hash = models.CharField(max_length=64) 55 | payment_request = models.CharField(max_length=1000) 56 | 57 | status = models.CharField(max_length=50, default='pending_invoice', choices=PAYMENT_STATUS_CHOICES) 58 | created_at = models.DateTimeField(auto_now_add=True) 59 | modified_at = models.DateTimeField(auto_now=True) 60 | 61 | def check_payment(self): 62 | """ 63 | Checks if the Lightning payment has been received for this invoice 64 | """ 65 | # if self.status == 'pending_payment': 66 | # return False 67 | 68 | r_hash_base64 = self.r_hash.encode('utf-8') 69 | r_hash_bytes = str(codecs.decode(r_hash_base64, 'base64')) 70 | invoice_resp = lnrpc.lookup_invoice(ln.PaymentHash(r_hash=r_hash_bytes)) 71 | 72 | if invoice_resp.settled: 73 | # Payment complete 74 | self.status = 'complete' 75 | self.save() 76 | return HttpResponse("Invoice paid successfully") 77 | else: 78 | # Payment not received 79 | return HttpResponse("Invoice pending payment") -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Python template 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Django stuff: 50 | staticfiles/ 51 | 52 | # pyenv 53 | .python-version 54 | 55 | 56 | 57 | # Environments 58 | .venv 59 | venv/ 60 | ENV/ 61 | 62 | # Rope project settings 63 | .ropeproject 64 | 65 | # mkdocs documentation 66 | /site 67 | 68 | # mypy 69 | .mypy_cache/ 70 | 71 | # Coverage directory used by tools like istanbul 72 | coverage 73 | 74 | # Dependency directories 75 | node_modules/ 76 | 77 | # Typescript v1 declaration files 78 | typings/ 79 | 80 | # Optional npm cache directory 81 | .npm 82 | 83 | # Optional eslint cache 84 | .eslintcache 85 | 86 | # Optional REPL history 87 | .node_repl_history 88 | 89 | # Output of 'npm pack' 90 | *.tgz 91 | 92 | # Yarn Integrity file 93 | .yarn-integrity 94 | 95 | 96 | ### Linux template 97 | *~ 98 | 99 | # temporary files which can be created if a process still has a handle open of a deleted file 100 | .fuse_hidden* 101 | 102 | # KDE directory preferences 103 | .directory 104 | 105 | # Linux trash folder which might appear on any partition or disk 106 | .Trash-* 107 | 108 | # .nfs files are created when an open file is removed but is still being accessed 109 | .nfs* 110 | 111 | 112 | ### VisualStudioCode template 113 | .vscode/* 114 | .vscode/settings.json 115 | !.vscode/tasks.json 116 | !.vscode/launch.json 117 | !.vscode/extensions.json 118 | 119 | ### macOS template 120 | # General 121 | *.DS_Store 122 | .AppleDouble 123 | .LSOverride 124 | 125 | # Icon must end with two \r 126 | Icon 127 | 128 | # Thumbnails 129 | ._* 130 | 131 | # Files that might appear in the root of a volume 132 | .DocumentRevisions-V100 133 | .fseventsd 134 | .Spotlight-V100 135 | .TemporaryItems 136 | .Trashes 137 | .VolumeIcon.icns 138 | .com.apple.timemachine.donotpresent 139 | 140 | ### Project template 141 | 142 | paywalled/media/ 143 | paywalled/static/css/theme.css 144 | theme.css 145 | 146 | .pytest_cache/ 147 | 148 | 149 | .ipython/ 150 | .env 151 | .envs/* 152 | !.envs/.local/ 153 | package-lock.json -------------------------------------------------------------------------------- /apps/authentication/validators.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from django.contrib.auth.models import User 4 | from django.core import validators 5 | from django.core.exceptions import ValidationError 6 | from django.utils.deconstruct import deconstructible 7 | from django.utils.translation import gettext, gettext_lazy as _ 8 | 9 | 10 | def validate_forbidden_usernames(value): 11 | forbidden_usernames = { 12 | "admin", 13 | "settings", 14 | "news", 15 | "about", 16 | "help", 17 | "signin", 18 | "signup", 19 | "signout", 20 | "terms", 21 | "privacy", 22 | "cookie", 23 | "new", 24 | "login", 25 | "logout", 26 | "administrator", 27 | "join", 28 | "account", 29 | "username", 30 | "root", 31 | "blog", 32 | "user", 33 | "users", 34 | "billing", 35 | "subscribe", 36 | "review", 37 | "blog", 38 | "blogs", 39 | "edit", 40 | "mail", 41 | "email", 42 | "home", 43 | "job", 44 | "jobs", 45 | "contribute", 46 | "newsletter", 47 | "shop", 48 | "profile", 49 | "register", 50 | "auth", 51 | "authentication", 52 | "campaign", 53 | "config", 54 | "delete", 55 | "remove", 56 | "forum", 57 | "forums", 58 | "download", 59 | "downloads", 60 | "contact", 61 | "blogs", 62 | "feed", 63 | "faq", 64 | "intranet", 65 | "log", 66 | "registration", 67 | "search", 68 | "explore", 69 | "rss", 70 | "support", 71 | "status", 72 | "static", 73 | "media", 74 | "setting", 75 | "css", 76 | "js", 77 | "follow", 78 | "activity", 79 | "library", 80 | "reset", 81 | "sitemap.xml", 82 | "robots.txt", 83 | "password_change", 84 | "password_reset", 85 | } 86 | if value.lower() in forbidden_usernames: 87 | raise ValidationError(gettext("This is a reserved word.")) 88 | 89 | 90 | def validate_case_insensitive_email(value): 91 | if User.objects.filter(email__iexact=value).exists(): 92 | raise ValidationError(gettext("A user with that email already exists.")) 93 | 94 | 95 | def validate_case_insensitive_username(value): 96 | if User.objects.filter(username__iexact=value).exists(): 97 | raise ValidationError(gettext("A user with that username already exists.")) 98 | 99 | 100 | @deconstructible 101 | class ASCIIUsernameValidator(validators.RegexValidator): 102 | regex = r"^[\w.]+\Z" 103 | message = _("Enter a valid username. This value may contain only English letters, numbers, and . _ characters.") 104 | flags = re.ASCII -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/layout/field_file.html: -------------------------------------------------------------------------------- 1 | {% load crispy_forms_field %} 2 | 3 |
    4 | {% for widget in field.subwidgets %} 5 | {% if widget.data.is_initial %} 6 |
    7 |
    8 | {{ widget.data.initial_text }} 9 |
    10 |
    11 | 12 | {{ field.value }} 13 | 14 | {% if not widget.data.required %} 15 | 16 | 17 | 19 | 21 | 22 | 23 | {% endif %} 24 |
    25 |
    26 |
    27 |
    28 | {{ widget.data.input_text }} 29 |
    30 | {% endif %} 31 |
    32 | 37 | 38 | 49 |
    50 | {% if not widget.data.is_initial %} 51 | {% include 'bamby_forms/layout/help_text_and_errors.html' %} 52 | {% endif %} 53 | {% if widget.data.is_initial %} 54 |
    55 |
    56 | {% include 'bamby_forms/layout/help_text_and_errors.html' %} 57 |
    58 | {% endif %} 59 | {% endfor %} 60 |
    61 | -------------------------------------------------------------------------------- /paywalled/templates/partials/partial_invoice.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 8 | 9 |
    10 |
    11 |
    12 | {% if purpose == 'publish' %} 13 | 14 | {% elif purpose == 'view' %} 15 | 16 | {% endif %} 17 |
    18 | 21 | 23 |
    24 | Open 26 | wallet 27 | Copy 28 | Show QR 29 |
    30 |
    31 |
    Checking for payment
    34 | 35 |
    36 |
    37 |
    38 |
    39 |
    40 | 41 |
    42 |
    Scan QR Code
    43 |
    44 |
    45 | 46 | 47 | {% block javascript %} 48 | 49 | 50 | 69 | {% endblock %} -------------------------------------------------------------------------------- /paywalled/templates/bamby_forms/field.html: -------------------------------------------------------------------------------- 1 | {% load crispy_forms_field %} 2 | 3 | {% if field.is_hidden %} 4 | {{ field }} 5 | {% else %} 6 | {% if field|is_checkbox %} 7 |
    8 | {% if label_class %} 9 |
    10 | {% endif %} 11 | {% endif %} 12 | <{% if tag %}{{ tag }}{% else %}div{% endif %} id="div_{{ field.auto_id }}" 13 | class="{% if not field|is_checkbox %}form-group{% if 'form-horizontal' in form_class %} row{% endif %}{% else %}{%if use_custom_control%}custom-control custom-checkbox{% else %}form-check{% endif %}{% endif %}{% if wrapper_class %} {{ wrapper_class }}{% endif %}{% if field.css_classes %} {{ field.css_classes }}{% endif %}"> 14 | {% if field.label and not field|is_checkbox and form_show_labels %} 15 | {# not field|is_radioselect in row below can be removed once Django 3.2 is no longer supported #} 16 | 21 | {% endif %} 22 | 23 | {% if field|is_checkboxselectmultiple %} 24 | {% include 'bamby_forms/layout/checkboxselectmultiple.html' %} 25 | {% endif %} 26 | 27 | {% if field|is_radioselect %} 28 | {% include 'bamby_forms/layout/radioselect.html' %} 29 | {% endif %} 30 | 31 | {% if not field|is_checkboxselectmultiple and not field|is_radioselect %} 32 | {% if field|is_checkbox and form_show_labels %} 33 | {%if use_custom_control%} 34 | {% crispy_field field 'class' 'custom-control-input' %} 35 | {% else %} 36 | {% crispy_field field 'class' 'form-check-input' %} 37 | {% endif %} 38 | 43 | {% include 'bamby_forms/layout/help_text_and_errors.html' %} 44 | {% elif field|is_file and use_custom_control %} 45 | {% include 'bamby_forms/layout/field_file.html' %} 46 | {% else %} 47 |
    48 | {% if field|is_select and use_custom_control %} 49 | {% crispy_field field 'class' 'custom-select' %} 50 | {% else %} 51 | {% crispy_field field %} 52 | {% endif %} 53 | {% include 'bamby_forms/layout/help_text_and_errors.html' %} 54 |
    55 | {% endif %} 56 | {% endif %} 57 | 58 | {% if field|is_checkbox %} 59 | {% if label_class %} 60 |
    61 | {% endif %} 62 |
    63 | {% endif %} 64 | {% endif %} 65 | -------------------------------------------------------------------------------- /paywalled/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load compress static django_htmx %} 2 | 3 | 4 | 5 | 6 | 7 | {% block title %}Paywalled{% endblock %} 8 | 9 | {% block meta %} 10 | 11 | 12 | 13 | 14 | {% endblock %} 15 | 16 | 17 | 18 | 19 | 20 | {% compress css %} 21 | 22 | 23 | {% block stylesheet %}{% endblock %} 24 | {% endcompress %} 25 | 26 | 28 | 29 | 30 | 31 | 32 | {% block body %} 33 |
    34 | {% block header %} 35 | {% include 'includes/header.html' %} 36 | {% endblock header %} 37 |
    38 |
    39 |
    40 | {% block inner %}{% endblock inner %} 41 |
    42 | {% include 'includes/messages.html' %} 43 | {% block content %}{% endblock %} 44 |
    45 |
    46 |
    47 |
    48 |
    49 | {% endblock body %} 50 | 62 |
    63 |
    64 |
    65 |
    66 |
    67 |
    68 |
    69 | {% compress js %} 70 | 73 | 76 | {% block javascript %}{% endblock %} 77 | 78 | {% endcompress %} 79 | 80 | 81 | {% django_htmx_script %} 82 | 83 | 84 | {% block modal %}{% endblock modal %} 85 | 86 | -------------------------------------------------------------------------------- /apps/core/templates/core/home.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static i18n humanize %} 3 | 4 | {% block content %} 5 | 6 |
    7 |
    8 | 9 |
    10 |
    11 |

    Paywalled

    12 |

    Exploring the lightning network through 13 | micropayments. Pay a tiny fee to publish, read, edit and comment on articles.

    14 |
    15 |
    16 | 17 |
    18 |
    19 |
    20 | {% for article in articles %} 21 | {% include 'partials/article_list.html' %} 22 | {% empty %} 23 |

    {% trans 'There is no published article yet' %}. {% trans 'Be the first one to publish one' %}! 25 |

    26 | {% endfor %} 27 |
    28 | 29 | 30 | {% if is_paginated %} 31 | 53 | {% endif %} 54 | 55 |
    56 | 57 |
    58 |
    59 |
    Lightning Rewards 60 | Stream
    61 |
    62 | {% if payments %} 63 |
      64 | {% for payment in payments %} 65 | {% include 'partials/article_payment.html' with mode="general" %} 66 | {% endfor %} 67 |
    68 | {% else %} 69 |

    No payments for this article at this time.

    70 | {% endif %} 71 |
    72 |
    73 |
    74 |
    75 |
    76 |
    77 | 78 | {% endblock content %} -------------------------------------------------------------------------------- /apps/blog/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.db import models 3 | from django.urls import reverse 4 | from django.utils.translation import gettext_lazy as _ 5 | from django.conf import settings 6 | from slugify import slugify 7 | from markdownx.models import MarkdownxField 8 | from markdownx.utils import markdownify 9 | import uuid 10 | from django.db.models import Sum 11 | 12 | import codecs 13 | from lnd_grpc import lnd_grpc 14 | 15 | lnrpc = lnd_grpc.Client( 16 | lnd_dir = settings.LND_FOLDER, 17 | macaroon_path = settings.LND_MACAROON_FILE, 18 | tls_cert_path = settings.LND_TLS_CERT_FILE, 19 | network = settings.LND_NETWORK, 20 | ) 21 | 22 | class ArticleQuerySet(models.query.QuerySet): 23 | """Personalized queryset created to improve model usability""" 24 | 25 | def get_published(self): 26 | """Returns only the published items in the current queryset.""" 27 | return self.filter(status="P").order_by("-date_published") 28 | 29 | def get_drafts(self): 30 | """Returns only the items marked as DRAFT in the current queryset.""" 31 | return self.filter(status="D").order_by("-date_published") 32 | 33 | 34 | class Article(models.Model): 35 | DRAFT = "D" 36 | PUBLISHED = "P" 37 | STATUS = ((DRAFT, _("Draft")), (PUBLISHED, _("Published"))) 38 | 39 | uuid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False) 40 | user = models.ForeignKey( 41 | settings.AUTH_USER_MODEL, 42 | null=True, 43 | related_name="articles", 44 | on_delete=models.SET_NULL, 45 | ) 46 | date_created = models.DateTimeField(auto_now_add=True) 47 | date_published = models.DateTimeField(auto_now=True) 48 | title = models.CharField(max_length=255, null=True, unique=True) 49 | status = models.CharField(max_length=1, choices=STATUS, default=DRAFT) 50 | content = models.CharField(_("Content"), max_length=40000, blank=True) 51 | edited = models.BooleanField(default=False) 52 | objects = ArticleQuerySet.as_manager() 53 | 54 | class Meta: 55 | verbose_name = _("Article") 56 | verbose_name_plural = _("Articles") 57 | ordering = ("-date_published",) 58 | 59 | def __str__(self): 60 | return self.title 61 | 62 | def get_absolute_url(self): 63 | return reverse('articles:article', kwargs={ "article_uuid": self.uuid }) 64 | 65 | def generate_pub_invoice(self): 66 | 67 | add_invoice_resp = lnrpc.add_invoice(value=settings.MIN_PUBLISH_AMOUNT, memo="Payment to Paywalled to publish article", expiry=settings.PUBLISH_INVOICE_EXPIRY) 68 | r_hash_base64 = codecs.encode(add_invoice_resp.r_hash, 'base64') 69 | r_hash = r_hash_base64.decode('utf-8') 70 | payment_request = add_invoice_resp.payment_request 71 | 72 | from apps.payments.models import Payment 73 | payment = Payment.objects.create(user=self.user, article=self, purpose=Payment.PUBLISH, satoshi_amount=settings.MIN_PUBLISH_AMOUNT, r_hash=r_hash, payment_request=payment_request, status='pending_payment') 74 | payment.save() 75 | 76 | def generate_view_invoice(self): 77 | 78 | add_invoice_resp = lnrpc.add_invoice(value=settings.MIN_VIEW_AMOUNT, memo=f"Payment to Paywalled to view article: {self.title}.", expiry=settings.VIEW_INVOICE_EXPIRY) 79 | r_hash_base64 = codecs.encode(add_invoice_resp.r_hash, 'base64') 80 | r_hash = r_hash_base64.decode('utf-8') 81 | payment_request = add_invoice_resp.payment_request 82 | 83 | from apps.payments.models import Payment 84 | payment = Payment.objects.create(article=self, purpose=Payment.VIEW, satoshi_amount=settings.MIN_VIEW_AMOUNT, r_hash=r_hash, payment_request=payment_request, status='pending_payment') 85 | payment.save() 86 | 87 | def get_view_count(self): 88 | return self.payments.filter(status="complete", purpose="view").count() 89 | 90 | def get_total_reward(self): 91 | return self.payments.filter(status="complete").aggregate(total_reward=Sum('satoshi_amount')) -------------------------------------------------------------------------------- /paywalled/templates/blog/draft_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static i18n %} 3 | {% load humanize %} 4 | 5 | {% block title %} {% trans 'Your Drafts' %} {% endblock %} 6 | 7 | {% block head %} 8 | 9 | {% endblock head %} 10 | 11 | {% block content %} 12 | 13 | 14 |
    15 | 16 |
    17 | 18 |
    19 |
    20 | {% for article in drafts %} 21 | 22 |
    23 |
    24 |
    25 |
    26 | 28 | 29 | 30 |
    31 |
    32 |

    33 | {{ article.title|title }} 34 |

    35 |

    {% trans 'Posted' %} 36 | {{ article.date_published|naturaltime }} 37 | by 38 | {{ article.user.profile.get_screen_name|title }}

    39 |
    40 |
    41 | Edit & Publish 43 | Delete 45 |
    46 |
    47 |
    48 |
    49 | {% empty %} 50 |

    You have no drafts yet.

    51 | {% endfor %} 52 |
    53 | 54 | {% if is_paginated %} 55 | 77 | {% endif %} 78 | 79 |
    80 |
    81 | 82 |
    83 | 84 | 85 | 86 | {% endblock content %} -------------------------------------------------------------------------------- /paywalled/templates/blog/article_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static i18n %} 3 | {% load crispy_forms_tags humanize %} 4 | 5 | {% block title %}{{ article.title|title }}{% endblock %} 6 | 7 | {% block content %} 8 | 9 | 26 | 27 |
    28 |
    29 |
    30 |

    {{ article.title }}

    31 |

    Published {{ article.date_published|naturaltime }} by 32 | {{ article.user.profile.get_screen_name }}{% if article.user == request.user %} Edit this 35 | article{% endif %} 36 |

    37 |
    38 |
    39 | {{ article.content|safe }} 40 | {% if not payment_made %} 41 |
    42 | 43 |
    44 | {% endif %} 45 |
    46 |
    47 | {% if payment_made %} 48 | 50 | {% else %} 51 |
    52 | {% if invoice %} 53 | {% include 'partials/partial_invoice.html' with purpose="view" %} 54 | {% else %} 55 |

    missing invoice

    56 | {% endif %} 57 |
    58 | {% endif %} 59 |
    60 | 61 |
    62 |
    63 |
    Total reward: {{ article.get_total_reward.total_reward }} sats
    64 |
    65 | {% if received_payments %} 66 |
      67 | {% for payment in received_payments %} 68 | {% include 'partials/article_payment.html' %} 69 | {% endfor %} 70 |
    71 | {% else %} 72 |

    No payments for this article at this time.

    73 | {% endif %} 74 |
    75 |
    76 |
    77 |
    78 |
    79 | {% endblock content %} 80 | 81 | {% block javascript %} 82 | 117 | {% endblock javascript %} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Paywalled 2 | Paywalled is an open blogging platform that enables writers to monetize their content with micropayments enabled by the lightning network. Spend a few sats to publish, view, edit and comment on content. Have fun with lightning. Inspired by Alex Bosworth's [Y'alls](https://yalls.org) 3 | 4 | ![Paywalled](https://github.com/crukundo/lnd-paywall/blob/main/paywalled.png?raw=true) 5 | 6 | ## A few things 7 | This application uses ordinary user accounts for anyone who wants to publish their content. User accounts allow the platform keep track of all due earnings and make it easy to claim them when required. It also makes edits to content much easier for the user. 8 | 9 | You should have [Lnd](https://github.com/lightningnetwork/lnd/) and [bitcoind](https://github.com/bitcoin/bitcoin) setup already. Plus, for testing, I used regtest. It was quicker to setup and focus on building that way but you can use whatever chain you want. More on this in the Configuration section. 10 | 11 | I also wrote a blog about this, you can read [here](https://rukundo.mataroa.blog/blog/i-built-a-blogging-platform-powered-by-the-lightning-network/) 12 | 13 | ## Setup 14 | Create a virtual environment, clone this repo and install dependencies 15 | ``` 16 | % virtualenv venv 17 | % source venv/bin/activate 18 | % git clone https://github.com/crukundo/lnd-paywall.git 19 | % cd lnd-paywall 20 | % pip install -r requirements.txt 21 | ``` 22 | 23 | ## Database 24 | This project uses sqlite3. Feel free to use whatever engine you want and reference it in your `.env` and settings.py 25 | 26 | ## Configuration 27 | 28 | Once all the dependencies have been installed, you can then create a `.env` file in the root of the project that contains all the configuration parameters for your instance. I added a sample file you can update. Remember to rename as `.env` and add to `.gitignore` 29 | 30 | The following are a list of currently available configuration options and a 31 | short explanation of what each does. 32 | 33 | `LND_FOLDER` (required) 34 | This is the path to your lnd folder. There should be read access to this path 35 | 36 | `LND_MACAROON_FILE` (required) 37 | This is the path to your admin.macaroon file. This will vary depending on the network chain you decide on. There should be read access to this path 38 | 39 | `LND_TLS_CERT_FILE` (required) 40 | This is the path to your tls.cert file. This is usually located inside your $LND_FOLDER. There should be read access to this path 41 | 42 | Other configs you can change if you want (but remember to update the values from settings when you call your rpc client): 43 | 44 | `LND_NETWORK` (optional; defaults to *regtest*) 45 | This selects the network that your node is configured for. regtest is the default and will have you on your way in no time 46 | 47 | `LND_GRPC_HOST` (optional; defaults to *localhost*) 48 | If your node is not on your local machine (say on a different server), you'll 49 | need to change this value to the appropriate value. 50 | 51 | `LND_GRPC_PORT` (optional; defaults to *10009*) 52 | If the GRPC port for your node was changed to anything other than the default 53 | you'll need to update this as well. 54 | 55 | `MIN_VIEW_AMOUNT` = 1500 (number of satoshis to pay to view an article) 56 | 57 | `MIN_PUBLISH_AMOUNT` = 2100 (number of satoshis to pay to publish an article) 58 | 59 | `PUBLISH_INVOICE_EXPIRY` = 604800 (time until a created lightning invoice to publish an article expires) 60 | 61 | `VIEW_INVOICE_EXPIRY` = 10800 (time until a created lightning invoice to view an article expires) 62 | 63 | 64 | ## Initializing the database 65 | 66 | To initialize the database which would create the database file and all the 67 | necessary tables, run the command: 68 | 69 | ``` 70 | % ./manage.py migrate 71 | ``` 72 | 73 | ## Running the application server 74 | 75 | Start the application backend by running the command: 76 | 77 | ``` 78 | % ./manage.py runserver 79 | ``` 80 | 81 | ## Pending matters 82 | 83 | - Add comment section 84 | - Make writers pay to edit their posts 😈 85 | - Add a section for writers with content to claim their rewards and facilitating channel opening to their lnd node through their shared public key 86 | - Possibly add [LNURL-auth](https://github.com/fiatjaf/lnurl-rfc/blob/legacy/lnurl-auth.md) and replace the ordinary user accounts authentication system 87 | 88 | ## Special Credit 89 | - Will Clark's [lnd-grpc](https://github.com/willcl-ark/lnd_grpc) - a python3 gRPC client for LND that did some heavy lifting. 90 | - The incredible supportive team at [Qala](https://qala.dev) -------------------------------------------------------------------------------- /apps/blog/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.contrib.auth.mixins import LoginRequiredMixin 3 | from django.contrib.auth.decorators import login_required 4 | from django.http import HttpResponse 5 | from django.views.decorators.http import require_http_methods 6 | from django.views.generic import CreateView, ListView, UpdateView, DetailView, View 7 | from django.urls import reverse 8 | from django.shortcuts import get_object_or_404, redirect, render 9 | from django.utils.translation import ugettext_lazy as _ 10 | 11 | from apps.authentication.helpers import AuthorRequiredMixin 12 | from apps.blog.models import Article 13 | from apps.blog.forms import ArticleForm 14 | 15 | @login_required() 16 | def list_drafts(request): 17 | drafts = request.user.articles.filter(status="D") 18 | context = { 19 | "drafts": drafts 20 | } 21 | return render(request, "blog/draft_list.html", context) 22 | 23 | @login_required() 24 | def list_articles(request): 25 | articles = request.user.articles.filter(status="P") 26 | context = { 27 | "articles": articles 28 | } 29 | return render(request, "blog/article_list.html", context) 30 | 31 | @login_required() 32 | def create_new_article(request): 33 | article = Article.objects.create(user=request.user) 34 | article.save() 35 | 36 | try: 37 | article.generate_pub_invoice() 38 | except: 39 | raise NotImplementedError() 40 | 41 | return redirect(reverse("articles:publish_article", kwargs={ 42 | 'article_uuid': article.uuid 43 | })) 44 | 45 | @login_required() 46 | def publish_new_article(request, article_uuid): 47 | 48 | article = request.user.articles.get(uuid=article_uuid) 49 | invoice = None 50 | payment_made = False 51 | 52 | try: 53 | # check existence of 'to publish' payments for this article and this user 54 | invoices = article.payments.filter(purpose='publish', user=request.user) 55 | if invoices: 56 | # we just need THE one 57 | invoice = invoices.first() 58 | if invoice.status == 'complete': 59 | payment_made = True 60 | else: 61 | # if no existing payment objects to view this article by this user 62 | article.generate_pub_invoice() 63 | except: 64 | raise NotImplementedError() 65 | 66 | if request.method == "POST": 67 | form = ArticleForm(request.POST, instance=article) 68 | # Publish article after payment 69 | if form.is_valid(): 70 | article = form.save(commit=False) 71 | if "publish" in request.POST: 72 | article.status = Article.PUBLISHED 73 | messages.success(request, "Your article: '{}' has been published successfully".format(article.title)) 74 | 75 | elif "save" in request.POST: 76 | article.status = Article.DRAFT 77 | messages.success(request, "Draft: '{}' has been saved successfully".format(article.title)) 78 | article.save() 79 | 80 | return redirect(reverse("home")) 81 | 82 | else: 83 | form = ArticleForm(instance=article) 84 | 85 | return render(request, "blog/publish_article.html", { 86 | 'article': article, 87 | 'form': form, 88 | 'invoice': invoice, 89 | 'payment_made': payment_made 90 | }) 91 | 92 | @login_required() 93 | def delete_draft_article(request, pk): 94 | article = get_object_or_404(Article, pk=pk) 95 | article.delete() 96 | return redirect(reverse("articles:drafts")) 97 | 98 | @login_required() 99 | def delete_article(request, pk): 100 | article = get_object_or_404(Article, pk=pk) 101 | article.delete() 102 | return redirect(reverse("home")) 103 | 104 | 105 | class EditArticleView(LoginRequiredMixin, AuthorRequiredMixin, UpdateView): 106 | """Basic EditView implementation to edit existing articles.""" 107 | 108 | model = Article 109 | message = _("Your article has been updated.") 110 | form_class = ArticleForm 111 | template_name = "blog/update_article.html" 112 | 113 | def form_valid(self, form): 114 | form.instance.user = self.request.user 115 | return super().form_valid(form) 116 | 117 | def get_success_url(self): 118 | messages.success(self.request, self.message) 119 | return reverse("home") 120 | 121 | def article_detail(request, article_uuid): 122 | article = Article.objects.get(uuid=article_uuid) 123 | # assume the worst first, lol 124 | payment_made = False 125 | received_payments = None 126 | invoice = None 127 | 128 | try: 129 | # create session key if non-existent 130 | if not request.session.session_key: 131 | request.session.create() 132 | # check if logged in user has a "to view" invoice and whether paid? 133 | if request.user.is_authenticated: 134 | invoices = article.payments.filter(purpose='view', user=request.user) 135 | if invoices: 136 | # we just need THE one 137 | invoice = invoices.last() 138 | if invoice.status == 'complete': 139 | payment_made = True 140 | # add session key to invoice 141 | invoice.session_key = request.session.session_key 142 | else: 143 | article.generate_view_invoice() 144 | invoice = article.payments.filter(purpose='view').latest("created_at") 145 | else: 146 | # get the most recent "to view" invoice for this particular session 147 | invoices = article.payments.filter(purpose='view', session_key=request.session.session_key) 148 | if invoices: 149 | # we just need THE one 150 | invoice = invoices.last() 151 | if invoice.status == 'complete': 152 | payment_made = True 153 | # add session key to invoice 154 | invoice.session_key = request.session.session_key 155 | else: 156 | article.generate_view_invoice() 157 | invoice = article.payments.latest("modified_at") 158 | except: 159 | raise NotImplementedError() 160 | 161 | received_payments = article.payments.filter(status='complete').order_by('-modified_at') 162 | 163 | return render(request, "blog/article_detail.html", { 164 | 'article': article, 165 | 'invoice': invoice, 166 | 'payment_made': payment_made, 167 | 'received_payments': received_payments 168 | }) 169 | 170 | # @todo: on edit, check if lightning publish invoice has expired and create a new one. Also mark payment as expired 171 | -------------------------------------------------------------------------------- /paywalled/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for paywalled project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.2.7. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.2/ref/settings/ 11 | """ 12 | import os 13 | 14 | from pathlib import Path 15 | from decouple import config, Csv 16 | from django import conf 17 | 18 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 19 | BASE_DIR = Path(__file__).resolve().parent 20 | 21 | LND_DIR = os.path.join(os.path.join(os.environ['HOME']), 'app-container', '.lnd') 22 | 23 | 24 | # Quick-start development settings - unsuitable for production 25 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 26 | 27 | SECRET_KEY = config("SECRET_KEY", default="") 28 | 29 | 30 | # SECURITY WARNING: don't run with debug turned on in production! 31 | DEBUG = config("DEBUG", default=True, cast=bool) 32 | 33 | ALLOWED_HOSTS = config("ALLOWED_HOSTS", default="127.0.0.1,localhost", cast=Csv()) 34 | 35 | 36 | # Application definition 37 | 38 | INSTALLED_APPS = [ 39 | 'django.contrib.admin', 40 | 'django.contrib.auth', 41 | 'django.contrib.contenttypes', 42 | 'django.contrib.sessions', 43 | 'django.contrib.messages', 44 | 'django.contrib.staticfiles', 45 | 'django.contrib.humanize', 46 | 47 | 'compressor', 48 | 'crispy_forms', 49 | 'django_htmx', 50 | 'tinymce', 51 | 'debug_toolbar', 52 | 'django_extensions', 53 | 'apps.authentication', 54 | 'apps.accounts', 55 | 'apps.core', 56 | 'apps.blog', 57 | 'apps.payments', 58 | ] 59 | 60 | MIDDLEWARE = [ 61 | 'django.middleware.security.SecurityMiddleware', 62 | 'django.contrib.sessions.middleware.SessionMiddleware', 63 | 'django.middleware.common.CommonMiddleware', 64 | 'django.middleware.csrf.CsrfViewMiddleware', 65 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 66 | 'django.contrib.messages.middleware.MessageMiddleware', 67 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 68 | 'debug_toolbar.middleware.DebugToolbarMiddleware', 69 | 'django_htmx.middleware.HtmxMiddleware' 70 | ] 71 | 72 | ROOT_URLCONF = 'paywalled.urls' 73 | 74 | TEMPLATES = [ 75 | { 76 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 77 | "DIRS": [str(BASE_DIR / "templates")], 78 | 'APP_DIRS': True, 79 | 'OPTIONS': { 80 | 'context_processors': [ 81 | 'django.template.context_processors.debug', 82 | 'django.template.context_processors.request', 83 | 'django.contrib.auth.context_processors.auth', 84 | 'django.contrib.messages.context_processors.messages', 85 | ], 86 | }, 87 | }, 88 | ] 89 | 90 | WSGI_APPLICATION = 'paywalled.wsgi.application' 91 | 92 | 93 | # Database 94 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 95 | 96 | DATABASES = { 97 | 'default': { 98 | 'ENGINE': 'django.db.backends.sqlite3', 99 | 'NAME': BASE_DIR / 'db.sqlite3', 100 | } 101 | } 102 | 103 | 104 | # Password validation 105 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 106 | 107 | AUTH_PASSWORD_VALIDATORS = [ 108 | { 109 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 110 | }, 111 | { 112 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 113 | }, 114 | { 115 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 116 | }, 117 | { 118 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 119 | }, 120 | ] 121 | 122 | 123 | ABSOLUTE_URL_OVERRIDES = { 124 | "auth.user": lambda u: "/%s/" % u.username, 125 | } 126 | 127 | LOGIN_REDIRECT_URL = "home" 128 | LOGOUT_REDIRECT_URL = "home" 129 | LOGIN_URL = "login" 130 | LOGOUT_URL = "logout" 131 | 132 | # Internationalization 133 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 134 | 135 | LANGUAGE_CODE = 'en-us' 136 | 137 | TIME_ZONE = config("TIME_ZONE", default="UTC") 138 | 139 | USE_I18N = True 140 | 141 | USE_L10N = True 142 | 143 | USE_TZ = True 144 | 145 | 146 | # ============================================================================== 147 | # STATIC FILES SETTINGS 148 | # ============================================================================== 149 | 150 | STATIC_URL = "/static/" 151 | STATIC_ROOT = BASE_DIR.parent / "static" 152 | STATICFILES_DIRS = [str(BASE_DIR / "static")] 153 | STATICFILES_FINDERS = ( 154 | "django.contrib.staticfiles.finders.FileSystemFinder", 155 | "django.contrib.staticfiles.finders.AppDirectoriesFinder", 156 | "compressor.finders.CompressorFinder", 157 | ) 158 | 159 | 160 | # ============================================================================== 161 | # MEDIA FILES SETTINGS 162 | # ============================================================================== 163 | 164 | MEDIA_URL = "/media/" 165 | MEDIA_ROOT = BASE_DIR.parent / "media/" 166 | DEFAULT_FILE_STORAGE = "django.core.files.storage.FileSystemStorage" 167 | 168 | # Default primary key field type 169 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 170 | 171 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 172 | 173 | CRISPY_TEMPLATE_PACK = "bootstrap4" 174 | 175 | DEBUG_TOOLBAR_CONFIG = { 176 | "DISABLE_PANELS": ["debug_toolbar.panels.redirects.RedirectsPanel"], 177 | "SHOW_TEMPLATE_CONTEXT": True, 178 | "SHOW_TOOLBAR_CALLBACK" : lambda request: True, 179 | } 180 | 181 | # ============================================================================== 182 | # TINYMCE 183 | # ============================================================================== 184 | 185 | TINYMCE_DEFAULT_CONFIG = { 186 | "theme": "silver", 187 | "height": 400, 188 | "menubar": False, 189 | "plugins": "advlist,autolink,lists,link,image,charmap,print,preview,anchor," 190 | "searchreplace,visualblocks,code,fullscreen,insertdatetime,media,table,paste," 191 | "code,help,wordcount", 192 | "toolbar": "undo redo | formatselect | " 193 | "bold italic backcolor | alignleft aligncenter " 194 | "alignright alignjustify | bullist numlist outdent indent | " 195 | "removeformat | help", 196 | } 197 | 198 | 199 | # PAYWALL SETTINGS 200 | MIN_VIEW_AMOUNT = config("MIN_VIEW_AMOUNT", default="1500", cast=int) 201 | MIN_PUBLISH_AMOUNT = config("MIN_PUBLISH_AMOUNT", default="2100", cast=int) 202 | PUBLISH_INVOICE_EXPIRY = config("PUBLISH_INVOICE_EXPIRY", default="604800", cast=int) 203 | VIEW_INVOICE_EXPIRY = config("VIEW_INVOICE_EXPIRY", default="10800", cast=int) 204 | 205 | LND_FOLDER = LND_DIR 206 | LND_MACAROON_FILE = config("LND_MACAROON_FILE") 207 | LND_TLS_CERT_FILE = config("LND_TLS_CERT_FILE") 208 | LND_NETWORK = config("LND_NETWORK", default="regtest") -------------------------------------------------------------------------------- /paywalled/static/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | --------------------------------------------------------------------------------