diff --git a/django/labellabor/__init__.py b/django/labellabor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django/labellabor/asgi.py b/django/labellabor/asgi.py new file mode 100644 index 0000000..85955c5 --- /dev/null +++ b/django/labellabor/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for labellabor project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "labellabor.settings") + +application = get_asgi_application() diff --git a/django/labellabor/make_config.py b/django/labellabor/make_config.py new file mode 100644 index 0000000..8328e5f --- /dev/null +++ b/django/labellabor/make_config.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +import os +import string +import secrets +import configparser +import time + + + +def generate_random_string(length): + # Exclude curly braces '{}' from the pool of characters + characters = string.ascii_letters + string.digits + return ''.join(secrets.choice(characters) for _ in range(length)) + + +def generate_config_file(config_file_path="config.ini"): + config = configparser.ConfigParser() + + # Generate random values for password and secret_key with specified lengths + dj_secret_key = generate_random_string(50) + + # Set the values in the configuration file + config['internal'] = { + 'secret_key': '{}'.format(dj_secret_key), + 'proj_name': 'labelbase', + 'crypto_salt': 'labelbase_', + 'allowed_host': '*', + 'debug': True, + 'current_timestamp_seconds': int(time.time()) + } + + config['database'] = { + 'name': 'labelbase', + 'user': 'ulabelbase', + 'password': 'vrZvZmX6Kp16B9tTa8JAA4RtAkWEhi', + # 'host': '127.0.0.1' + } + + # Create the configuration file and write the values + with open(config_file_path, 'w') as configfile: + config.write(configfile) + +if __name__ == "__main__": + # Check if the config.ini file exists + if not os.path.isfile(config_file_path): + generate_config_file() + else: + print("{} already exists.".format(config_file_path)) diff --git a/django/labellabor/settings.py b/django/labellabor/settings.py new file mode 100644 index 0000000..7aeb924 --- /dev/null +++ b/django/labellabor/settings.py @@ -0,0 +1,229 @@ +import os +import sentry_sdk +from sentry_sdk.integrations.django import DjangoIntegration +from pathlib import Path +from configparser import RawConfigParser + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +PROJECT_PATH = BASE_DIR = Path(__file__).resolve().parent.parent + +proj_config = RawConfigParser() +config_file_path = "{}/config.ini".format(PROJECT_PATH) +try: + if proj_config.read(config_file_path) == []: + from .make_config import generate_config_file + if not os.path.isfile(config_file_path): + print("creating config file, here: {}".format(config_file_path)) + generate_config_file(config_file_path) + assert proj_config.read(config_file_path) == [] +except AssertionError: + from django.core.exceptions import ImproperlyConfigured + raise ImproperlyConfigured( + "Configuration file {} not found!".format(config_file_path) + ) + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = proj_config.get("internal", "secret_key") +CRYPTOGRAPHY_SALT = proj_config.get("internal", "crypto_salt") + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = proj_config.getboolean("internal", "debug") +if DEBUG: + ALLOWED_HOSTS = ["*"] +else: + ALLOWED_HOSTS = [proj_config.get("internal", "allowed_host")] + +sentry_sdk.init( + dsn="https://3b833ae08ccc4ff68793e961fff4921c@o4504646963232768.ingest.sentry.io/4504646967361536", + integrations=[ + DjangoIntegration(), + ], + # Set traces_sample_rate to 1.0 to capture 100% + # of transactions for performance monitoring. + # We recommend adjusting this value in production. + traces_sample_rate=1.0, + # If you wish to associate users to errors (assuming you are using + # django.contrib.auth) you may enable sending PII data. + send_default_pii=False, +) +sentry_sdk.set_tag("version", "0.23.1") + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': True, + 'formatters': { + 'verbose': { + 'format': '{levelname} {asctime} {module} {message}', + 'style': '{', + }, + }, + 'handlers': { + 'file': { + 'level': 'DEBUG', + 'class': 'logging.handlers.RotatingFileHandler', + 'filename': './labelbase.log', + 'maxBytes': 1024 * 1024 * 5, # 5 MB + 'backupCount': 5, # Keep 5 backup files + 'formatter': 'verbose', + }, + }, + 'loggers': { + 'labelbase': { + 'handlers': ['file'], + 'level': 'DEBUG', + 'propagate': True, + }, + }, +} + +# Application definition +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + # 'django.contrib.sessions', # NOTE: We use "user_sessions" + "user_sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "two_factor", + "two_factor.plugins.phonenumber", + "django_otp", + "django_otp.plugins.otp_static", + "django_otp.plugins.otp_totp", + "labelbase", + "userprofile", + "bootstrapform", + "cryptography", + "rest_framework", + "rest_framework.authtoken", + "sekizai", + "importer", + "finances", + "background_task", + "connectrum", +] + + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + #"django.middleware.cache.UpdateCacheMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django_otp.middleware.OTPMiddleware", + #"django.middleware.cache.FetchFromCacheMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + + +#CACHES = { +# 'default': { +# 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', +# 'LOCATION': 'labelbase-snowflake', +# } +#} + +ROOT_URLCONF = "labellabor.urls" + +LOGIN_URL = "two_factor:login" + +LOGIN_REDIRECT_URL = "two_factor:profile" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [os.path.join(PROJECT_PATH, "templates")], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + "sekizai.context_processors.sekizai", + ], + }, + }, +] + + +CURRENCIES = ['USD', 'EUR', 'GBP', 'CAD', 'CHF', 'AUD', 'JPY'] + + +WSGI_APPLICATION = "labellabor.wsgi.application" + +TWO_FACTOR_REMEMBER_COOKIE_AGE = 60 * 60 * 24 * 14 + + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.mysql", + "NAME": proj_config.get("database", "name"), + "USER": proj_config.get("database", "user"), + "OPTIONS": {"charset": "utf8mb4"}, + "PASSWORD": proj_config.get("database", "password"), + 'HOST': 'localhost', + 'PORT': 3306, + } +} + +TWO_FACTOR_WEBAUTHN_RP_NAME = "labelbase.space" + +SESSION_ENGINE = "user_sessions.backends.db" + +REST_FRAMEWORK = {"DEFAULT_SCHEMA_CLASS": "rest_framework.schemas.coreapi.AutoSchema"} + +# Password validation +# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/3.2/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.2/howto/static-files/ + +STATIC_URL = '/static/' +STATIC_ROOT = os.path.join(BASE_DIR, 'static/') + +# Default primary key field type +# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +# https://docs.djangoproject.com/en/4.1/ref/settings/#std-setting-FILE_UPLOAD_HANDLERS +FILE_UPLOAD_HANDLERS = [ + "django.core.files.uploadhandler.MemoryFileUploadHandler", + "django.core.files.uploadhandler.TemporaryFileUploadHandler", +] diff --git a/django/labellabor/urls.py b/django/labellabor/urls.py new file mode 100644 index 0000000..8eb5313 --- /dev/null +++ b/django/labellabor/urls.py @@ -0,0 +1,250 @@ +from django.contrib import admin +from django.contrib.auth.views import LogoutView +from django.urls import include, path +from two_factor.urls import urlpatterns as tf_urls +from labelbase.api import LabelAPIView, LabelbaseAPIView +from rest_framework.documentation import include_docs_urls +from django.contrib.auth.decorators import login_required + +from userprofile.views import (ProfileView, + ProfileAvatarUpdateView, + ProfileCurrencyUpdateView) +from userprofile.views import APIKeyView + + + + + +from .views import ( + LabelbaseView, + LabelbaseViewActionView, + LabelbaseMergeView, + LabelbaseDeleteView, + LabelDeleteView, + TermsView, + HomeView, + RegistrationCompleteView, + LabelUpdateView, + PrivacyView, + RegistrationView, + LabelbaseFormView, + LabelbaseUpdateView, + FaqView, + AboutView, + EncryptionView, + InteroperationalView, + ExportLabelsView, + #StatsAndKPIView, + TreeMapsView, + FixAndMergeLabelsView, + LabelbaseDatatableView, + LabelbasePortfolioView, +) + +from importer.views import upload_labels +from exporter.views import stream_labels_as_jsonl +from django.contrib.auth import views as auth_views + +from django.conf import settings +from django.conf.urls.static import static + +urlpatterns = [ + path( + "api/v0/labelbase/", + LabelbaseAPIView.as_view() + ), + path( + "api/v0/labelbase//", + LabelbaseAPIView.as_view() + ), + path( + "api/v0/labelbase//label/", + LabelAPIView.as_view() + ), + path( + "api/v0/labelbase//label//", + LabelAPIView.as_view() + ), + path( + "api-reference/", + include_docs_urls(title="Labelbase API") + ), + path( + "account/apikey/", + login_required(APIKeyView.as_view()), + name="apikey" + ), + path( + "account/userprofile-avatar/", + login_required(ProfileAvatarUpdateView.as_view()), + name="userprofile_avatar", + ), + path( + "account/userprofile-currency/", + login_required(ProfileCurrencyUpdateView.as_view()), + name="userprofile_currency", + ), + path( + "account/userprofile/", + login_required(ProfileView.as_view()), + name="userprofile", + ), + path( + "labelbase//delete/", + login_required(LabelbaseDeleteView.as_view()), + name="del_labelbase" + ), + path( + "labelbase//", + login_required(LabelbaseView.as_view()), + name="labelbase" + ), + path( + "labelbase//data/", + login_required(LabelbaseDatatableView.as_view()), + name="labelbase_label_data"), + path( + "labelbase//actions//", + login_required(LabelbaseViewActionView.as_view()), + name="labelbase_actions"), + path( + "labelbase//merge/", + login_required(LabelbaseMergeView.as_view()), + name="labelbase_merge" + ), + path( + "labelbase//portfolio/", + login_required(LabelbasePortfolioView.as_view()), + name="labelbase_portfolio" + ), + path( + "labelbase//edit/", + login_required(LabelbaseUpdateView.as_view()), + name="edit_labelbase" + ), + path( + "labelbase/import/", + upload_labels, + name="import_labels" + ), + path( + "labelbase/export//", + stream_labels_as_jsonl, + name="export_labels" + ), + path( + "labelbase//dyanmic-export/", + login_required(ExportLabelsView.as_view()), + name="labelbase_dynamic_export" + ), + path( + "labelbase//fix-and-manage/", + login_required(FixAndMergeLabelsView.as_view()), + name="labelbase_fix_and_manage" + ), + #path( + # "labelbase//stats-and-kpi/", + # login_required(StatsAndKPIView.as_view()), + # name="labelbase_stats_and_kpi" + #), + path( + "labelbase//tree-maps/", + login_required(TreeMapsView.as_view()), + name="labelbase_tree_maps" + ), + path( + "labelbase//tree-maps//", + login_required(TreeMapsView.as_view()), + name="labelbase_tree_maps" + ), + path( + "labelbase/", + login_required(LabelbaseFormView.as_view()), + name="labelbase_new" + ), + path( + "label//edit/", + login_required(LabelUpdateView.as_view()), + name="edit_label" + ), + path( + "label//edit//", + login_required(LabelUpdateView.as_view()), + name="edit_label_with_action" + ), + path( + "label//delete/", + login_required(LabelDeleteView.as_view()), + name="del_label" + ), + path( + "account/logout/", + LogoutView.as_view(), + name="logout" + ), + path( + "account/register/", + RegistrationView.as_view(), + name="registration" + ), + path( + "account/register/done/", + RegistrationCompleteView.as_view(), + name="registration_complete" + ), + path( + "account/change-password/", + auth_views.PasswordChangeView.as_view( + template_name="change_password.html", + success_url="/" + ), + name="change_password" + ), + path( + "privacy-policy", + PrivacyView.as_view(), + name="privacy_policy" + ), + path( + "terms", + TermsView.as_view(), + name="terms" + ), + path( + "faq", + FaqView.as_view(), + name="faq" + ), + path( + "about", + AboutView.as_view(), + name="about" + ), + path( + "encryption", + EncryptionView.as_view(), + name="encryption" + ), + path( + "interoperational", + InteroperationalView.as_view(), + name="interoperational" + ), + path( + "", + HomeView.as_view(), + name="home" + ), + path( + "", + include(tf_urls) + ), + path( + "", + include("user_sessions.urls", "user_sessions") + ), +] + +#urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) +#if settings.DEBUG: +urlpatterns.append(path("admin/", admin.site.urls)) diff --git a/django/labellabor/utils.py b/django/labellabor/utils.py new file mode 100644 index 0000000..0d75e8d --- /dev/null +++ b/django/labellabor/utils.py @@ -0,0 +1,37 @@ +import re +import decimal + +from django.conf import settings + +def hashtag_to_badge(value): + hashtags = re.findall(r'#\w+', value) + for tag in hashtags: + clean_tag = re.search(r'#(\w+)', tag).group(1) # Extract the word after the '#' symbol + value = value.replace(tag, f'{clean_tag}') + return value + + +def extract_fiat_value(s): + # CHF XXX + pattern = r"({})\s?(\d+(?:\.\d+)?)" + for currency in settings.CURRENCIES: + match = re.search(pattern.format(currency), s) + if match: + print(f"Currency: {match.group(1)}, Value: {match.group(2)}") + return (decimal.Decimal(match.group(2)), match.group(1)) + + # XXX CHF + pattern = r"(\d+(?:\.\d+)?)\s?({})" + for currency in settings.CURRENCIES: + match = re.search(pattern.format(currency), s) + if match: + value = decimal.Decimal(match.group(1)) + currency_symbol = currency + # Find the currency symbol position in the string + symbol_start = match.start(2) + symbol_end = match.end(2) + if symbol_start > 0: + currency_symbol = s[symbol_start:symbol_end] + print(f"Currency: {currency_symbol}, Value: {value}") + return (value, currency_symbol) + return (decimal.Decimal(-1), "") diff --git a/django/labellabor/views.py b/django/labellabor/views.py new file mode 100644 index 0000000..7ddef3b --- /dev/null +++ b/django/labellabor/views.py @@ -0,0 +1,650 @@ +import os +import time +import tempfile +import json +from django.conf import settings +from django.contrib.auth.forms import UserCreationForm +from django.shortcuts import redirect, resolve_url +from django.views.decorators.cache import never_cache +from django.views.generic import FormView, TemplateView +from django.views import View +from django.shortcuts import render +from django.views.generic.list import ListView +from django.views.generic.edit import DeleteView +from django.views.generic.edit import UpdateView +from django.shortcuts import get_object_or_404 +from two_factor.views import OTPRequiredMixin +from two_factor.views.utils import class_view_decorator +from labelbase.models import Label, Labelbase +from labelbase.forms import LabelForm, LabelbaseForm +from labelbase.forms import ExportLabelsForm +from django.http import HttpResponseRedirect +from django.http import FileResponse +from rest_framework.authtoken.models import Token +from bip329.bip329_writer import BIP329JSONLWriter, BIP329JSONLEncryptedWriter +from django.utils.safestring import mark_safe +from django.urls import reverse +from django_datatables_view.base_datatable_view import BaseDatatableView +from django.utils.html import escape +from django.template.loader import render_to_string + +from .utils import hashtag_to_badge, extract_fiat_value +from labelbase.utils import compute_type_ref_hash +from finances.models import OutputStat +from finances.tasks import check_all_outputs + +class AboutView(TemplateView): + template_name = "about.html" + + +class EncryptionView(TemplateView): + template_name = "encryption.html" + + +class InteroperationalView(TemplateView): + template_name = "interoperational.html" + + +class HomeView(TemplateView): + template_name = "home.html" + + +class PrivacyView(TemplateView): + template_name = "privacy.html" + + +class TermsView(TemplateView): + template_name = "terms.html" + + +class FaqView(TemplateView): + template_name = "faq.html" + + +class LabelDeleteView(DeleteView): + model = Label + success_url = "/" + error_url = "/#failed" + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + if self.object.labelbase.user != self.request.user: + return redirect(self.error_url) + return super().post(request, *args, **kwargs) + + +class LabelbaseDeleteView(DeleteView): + model = Labelbase + success_url = "/" + error_url = "/#failed" + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + if self.object.user != self.request.user: + return redirect(self.error_url) + return super().post(request, *args, **kwargs) + + +class LabelbaseDatatableView(BaseDatatableView): + model = Label + + columns = ["id", "type", "ref", "label", "origin", "spendable"] + + order_columns = ["id", "type", "ref", "label", "origin", "spendable"] + + max_display_length = 100 + + def get_initial_queryset(self): + labelbase_id = self.kwargs["labelbase_id"] + + search_tag = self.request.GET.get('tag', None) + + qs = Label.objects.filter(labelbase__user_id=self.request.user.id, + labelbase_id=labelbase_id).order_by("id") + + if search_tag: + # Due to encryption, we need to use a super slow process here... + res_ids = [] + search = f'#{search_tag}' + print(search) + for record in qs: + if record.label and search in record.label: + res_ids.append(record.id) + continue + qs = qs.filter(id__in=res_ids) + return qs + + + def render_column(self, row, column): + if column == 'id': + return f'{row.id}' + elif column == 'type': + return "{}".format(row.get_type_display()) + elif column == 'ref': + return render_to_string('labelbase_dt_ref.html', + context={'row': row, + 'mempool_url': row.get_mempool_url(), + }, request=self.request) + elif column == 'label': + if row.label is None: + return "" + if row.labelbase.user.profile.use_hashtags: + return hashtag_to_badge(f'{row.label}') + else: + return f'{row.label}' + elif column == 'origin': + if row.origin is None: + return "" + return f'{row.origin}' + elif column == 'spendable': + spendable_value = row.spendable + if spendable_value is None: + spendable_formatted = '' + else: + spendable_formatted = 'true' if spendable_value else 'false' + #if spendable_formatted == 'true': + # return f' {spendable_formatted } ' + #elif spendable_formatted == 'false': + # return f'{spendable_formatted }' + return f'{spendable_formatted }' + else: + return super(LabelbaseDatatableView, self).render_column(row, column) + + def filter_queryset(self, qs): + search = self.request.GET.get('search[value]', None) + if search: + # Due to encryption, we need to use a super slow process here... + res_ids = [] + search = search.lower() + for record in qs: + if record.type and search in record.type.lower(): + res_ids.append(record.id) + continue + if record.ref and search in record.ref.lower(): + res_ids.append(record.id) + continue + if record.label and search in record.label.lower(): + res_ids.append(record.id) + continue + if record.origin and search in record.origin.lower(): + res_ids.append(record.id) + continue + + return qs.filter(id__in=res_ids) + return qs + +class LabelbaseViewActionView(View): + def get(self, request, *args, **kwargs): + labelbase = get_object_or_404( + Labelbase, id=self.kwargs["labelbase_id"], user_id=self.request.user.id + ) + if self.kwargs["action"] == "update-spent-outputs": + check_all_outputs(self.request.user.id, labelbase_id=labelbase.id) + return HttpResponseRedirect(labelbase.get_absolute_url()) + + + + +class LabelbaseView(ListView): + template_name = "labelbase.html" + context_object_name = "label_list" + + def get_queryset(self): + qs = Label.objects.filter( + labelbase__user_id=self.request.user.id, + labelbase_id=self.kwargs["labelbase_id"], + ) + return qs.order_by("id") + + def get_context_data(self, **kwargs): + labelbase_id = self.kwargs["labelbase_id"] + context = super(LabelbaseView, self).get_context_data(**kwargs) + context["labelbase"] = get_object_or_404( + Labelbase, id=labelbase_id, user_id=self.request.user.id + ) + context["active_labelbase_id"] = labelbase_id + context["labelform"] = LabelForm( + request=self.request, labelbase_id=labelbase_id + ) + context["api_token"] = Token.objects.get(user_id=self.request.user.id) + return context + + + def post(self, request, *args, **kwargs): + labelbase_id = self.kwargs["labelbase_id"] + labelform = LabelForm(request.POST, request=request, labelbase_id=labelbase_id) + if labelform.is_valid(): + label = labelform.save() + return HttpResponseRedirect(label.labelbase.get_absolute_url()) + + +class LabelbaseMergeView(LabelbaseView): + template_name = "labelbase_merge.html" + + def get_queryset(self): + labels = [] + qs = Label.objects.filter( + labelbase__user_id=self.request.user.id, + labelbase_id=self.kwargs["labelbase_id"], + ) + lbl_type = self.request.GET.get("type", None) + lbl_ref = self.request.GET.get("ref", None) + lbl_label = self.request.GET.get("label", None) + + for l in qs: + if lbl_type and lbl_ref and lbl_label: + if l.type == lbl_type and l.ref == lbl_ref and l.label == lbl_label: + labels.append(l) + elif lbl_type and lbl_ref: + if l.type == lbl_type and l.ref == lbl_ref: + labels.append(l) + elif lbl_type and lbl_label: + if l.type == lbl_type and l.label == lbl_label: + labels.append(l) + elif lbl_ref and lbl_label: + if l.ref == lbl_ref and l.label == lbl_label: + labels.append(l) + elif lbl_type: + if l.type == lbl_type: + labels.append(l) + elif lbl_ref: + if l.ref == lbl_ref: + labels.append(l) + elif lbl_label or lbl_label == '': + if l.label == lbl_label: + labels.append(l) + label_ids = [] + for l in labels: + label_ids.append(l.id) + if label_ids: + qs = Label.objects.filter(id__in=label_ids, + labelbase__user_id=self.request.user.id, + labelbase_id=self.kwargs["labelbase_id"]) + else: + qs = Label.objects.none() + return qs.order_by("id") + +class StatsAndKPIView(View): + template_name = "labelbase_stats_and_kpi.html" + + def get(self, request, *args, **kwargs): + labelbase_id = self.kwargs["labelbase_id"] + labelbase = get_object_or_404( + Labelbase, id=labelbase_id, user_id=self.request.user.id + ) + return render(request, self.template_name, { + "labelbase": labelbase, + "active_labelbase_id": labelbase_id}) + +""" + + +def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["active_labelbase_id"] = self.object.labelbase.id + context["labelbase"] = self.object.labelbase + context["action"] = self.kwargs.get('action', 'unspent-outputs') + if context["action"] == "labeling": + #mempool_api = self.object.labelbase.get_mempool_api() + if self.object.type == "tx": + mempool_api = self.object.labelbase.get_mempool_api() + context["res_tx"] = mempool_api.get_transaction(self.object.ref) + + if self.object.type == "output": + context["output"] = OutputStat.objects.filter(type_ref_hash= \ + self.object.type_ref_hash).last() + + return context + + + + + +""" +class TreeMapsView(ListView): + context_object_name = "label_list" + + def get_template_names(self): + if 'action' in self.kwargs: + action = self.kwargs['action'] + if action == 'unspent-outputs': + return "labelbase_tree_maps_unspent_outputs.html" + elif action == 'unspent-spendable-outputs': + return "labelbase_tree_maps_unspent_outputs.html" + return "labelbase_tree_maps_unspent_outputs.html" + + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + + labelbase_id = self.kwargs["pk"] + labelbase = get_object_or_404( + Labelbase, id=labelbase_id, user_id=self.request.user.id + ) + context['labelbase'] = labelbase + context['active_labelbase_id'] = labelbase.id + context['action'] = self.kwargs.get('action', 'unspent-outputs') + return context + + def get_queryset(self): + #action = self.kwargs.get('action', 'unspent-outputs') + + label_ids = [] + + qs = Label.objects.filter( + labelbase__user_id=self.request.user.id, + labelbase_id=self.kwargs["pk"], + ) + + for l in qs: + if l.type == "output": + output = OutputStat.objects.filter( + type_ref_hash=l.type_ref_hash, + network=l.labelbase.network).last() + if output and output.spent is False: + #if action == 'unspent-spendable-outputs' and l.spendable == True: + # label_ids.append(l.id) + #else: + label_ids.append(l.id) + if label_ids: + qs = qs.filter(id__in=label_ids, + labelbase__user_id=self.request.user.id, + labelbase_id=self.kwargs["pk"]) + else: + qs = Label.objects.none() + return qs.order_by("id") + + + +class LabelbasePortfolioView(LabelbaseView): + template_name = "labelbase_portfolio.html" + + def get_queryset(self): + self.balances = {} + label_ids = [] + qs = Label.objects.filter( + labelbase__user_id=self.request.user.id, + labelbase_id=self.kwargs["labelbase_id"], + ) + for l in qs: + if l.type == "output": + val, cur = extract_fiat_value(l.label) + if val > 0: + if self.balances.get(cur, None) is None: + self.balances[cur] = 0 + self.balances[cur] += val + label_ids.append(l.id) + if label_ids: + qs = Label.objects.filter(id__in=label_ids, + labelbase__user_id=self.request.user.id, + labelbase_id=self.kwargs["labelbase_id"]) + else: + qs = Label.objects.none() + return qs.order_by("id") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['balances'] = self.balances + return context + + + + + +class FixAndMergeLabelsView(View): + template_name = "label_fix_and_merge.html" + + def get(self, request, *args, **kwargs): + labelbase_id = self.kwargs["labelbase_id"] + labelbase = get_object_or_404( + Labelbase, id=labelbase_id, user_id=self.request.user.id + ) + + # Fetch all records for the specified Labelbase + records = Label.objects.filter(labelbase_id=labelbase_id) + + # Create a dictionary to store records grouped by type, ref, and label + record_groups_type_and_ref = {} + record_groups_type_and_ref_and_label = {} + record_groups_all_identical = {} + + resulting_duplicates_type_and_ref = [] + resulting_duplicates_type_and_ref_and_label = [] + resulting_duplicates_all_identical = [] + all_identical_records = [] + + # Iterate through the records and group them + for record in records: + key_type_and_ref = (record.type, record.ref) + key_type_and_ref_label = (record.type, record.ref, record.label) + key_all_identical = (record.type, record.ref, record.label, record.origin, record.spendable) + + if key_type_and_ref in record_groups_type_and_ref: + record_groups_type_and_ref[key_type_and_ref].append(record) + else: + record_groups_type_and_ref[key_type_and_ref] = [record] + + if key_type_and_ref_label in record_groups_type_and_ref_and_label: + record_groups_type_and_ref_and_label[key_type_and_ref_label].append(record) + else: + record_groups_type_and_ref_and_label[key_type_and_ref_label] = [record] + + if key_all_identical in record_groups_all_identical: + record_groups_all_identical[key_all_identical].append(record) + else: + record_groups_all_identical[key_all_identical] = [record] + + # Iterate through the grouped records and identify duplicates + for key_type_and_ref, duplicates in record_groups_type_and_ref.items(): + if len(duplicates) > 1: + type_val, ref_val = key_type_and_ref + print(f"Duplicates for labelbase_id={labelbase_id}, type='{type_val}', ref='{ref_val}', ") + for record in duplicates: + print(f" ID: {record.id}, origin: {record.origin}, spendable: {record.spendable}") + resulting_duplicates_type_and_ref.append({'type': type_val, 'ref': ref_val}) + + for key_type_and_ref_and_label, duplicates in record_groups_type_and_ref_and_label.items(): + if len(duplicates) > 1: + type_val, ref_val, label_val = key_type_and_ref_and_label + print(f"Duplicates for labelbase_id={labelbase_id}, type='{type_val}', ref='{ref_val}', label='{label_val}':") + for record in duplicates: + print(f" ID: {record.id}, origin: {record.origin}, spendable: {record.spendable}") + resulting_duplicates_type_and_ref_and_label.append({'type': type_val, 'ref': ref_val, 'label': label_val}) + + for key_all_identical, duplicates in record_groups_all_identical.items(): + if len(duplicates) > 1: + type_val, ref_val, label_val, origin_val, spendable_cal = key_all_identical + #print(f"Duplicates for labelbase_id={labelbase_id}, type='{type_val}', ref='{ref_val}', label='{label_val}':") + for record in duplicates: + if record not in all_identical_records: + all_identical_records.append(record) + # print(f" ID: {record.id}, origin: {record.origin}, spendable: {record.spendable}") + resulting_duplicates_all_identical.append({'type': type_val, 'ref': ref_val, 'label': label_val, + 'origin': origin_val, 'spendable': spendable_cal}) # nonsense, but counts + + + + fix_suggestions = len(resulting_duplicates_type_and_ref) + \ + len(resulting_duplicates_type_and_ref_and_label) + \ + len(resulting_duplicates_all_identical) + + return render(request, self.template_name, { + "labelbase": labelbase, + "fix_suggestions": fix_suggestions, + "resulting_duplicates_all_identical_final_record_count": len(resulting_duplicates_all_identical), + "resulting_duplicates_all_identical_current_record_count": len(all_identical_records), + + "resulting_duplicates_type_and_ref": resulting_duplicates_type_and_ref, + "resulting_duplicates_type_and_ref_and_label": resulting_duplicates_type_and_ref_and_label, + "active_labelbase_id": labelbase_id}) + + +class ExportLabelsView(View): + def post(self, request, *args, **kwargs): + labelbase_id = self.kwargs["labelbase_id"] + labelbase = get_object_or_404( + Labelbase, id=labelbase_id, user_id=self.request.user.id + ) + form = ExportLabelsForm(request.POST) + if form.is_valid(): + # Retrieve data from the form + tx_checkbox = form.cleaned_data['tx_checkbox'] + addr_checkbox = form.cleaned_data['addr_checkbox'] + pubkey_checkbox = form.cleaned_data['pubkey_checkbox'] + input_checkbox = form.cleaned_data['input_checkbox'] + output_checkbox = form.cleaned_data['output_checkbox'] + xpub_checkbox = form.cleaned_data['xpub_checkbox'] + + encrypt_checkbox = form.cleaned_data['encrypt_checkbox'] + + # Use request.POST, not form.cleaned_data['passphrase'] + passphrase = request.POST.get('passphrase', '') + + # Export labels based on selected checkboxes + selected_type_attributes = [] + if tx_checkbox: + selected_type_attributes.append('tx') + if addr_checkbox: + selected_type_attributes.append('addr') + if pubkey_checkbox: + selected_type_attributes.append('pubkey') + if input_checkbox: + selected_type_attributes.append('input') + if output_checkbox: + selected_type_attributes.append('output') + if xpub_checkbox: + selected_type_attributes.append('xpub') + + timestamp = time.strftime('%Y%m%d%H%M%S') + + # Define the prefix and file extension for the temporary file + prefix = f'labelbase-{labelbase.id}-{timestamp}-' + suffix = '.jsonl' + + # Create a temporary file with the specified prefix and file extension + with tempfile.NamedTemporaryFile(delete=False, prefix=prefix, suffix=suffix) as temp_file: + # Create a BIP329JSONLWriter instance + if encrypt_checkbox and passphrase: + label_writer = BIP329JSONLEncryptedWriter(temp_file.name, passphrase, remove_existing=True) + else: + label_writer = BIP329JSONLWriter(temp_file.name, remove_existing=True) + labels = Label.objects.filter(labelbase_id=labelbase_id, + labelbase__user_id=request.user.id, + type__in=selected_type_attributes + ).order_by("id") + for label in labels: + label_entry = { + "type": label.type, + "ref": label.ref, + "label": label.label, + } + + if label.origin and label.type == "tx": + label_entry["origin"] = label.origin + if label.spendable in [True, False] and label.type == "output": + label_entry["spendable"] = label.spendable + + # Write the label entry to the file + label_writer.write_label(label_entry) + + final_filename = f"labelbase-{labelbase.id}-{timestamp}.jsonl" + if encrypt_checkbox and passphrase: + if label_writer.is_closed is False: + # Close the encrypted writer when finished + label_writer.close() + final_filename = final_filename.replace(".jsonl", ".7z") + + response = FileResponse(open(temp_file.name, 'rb')) + response['Content-Type'] = 'application/json' + + response['Content-Disposition'] = f'attachment; filename="{final_filename}"' + os.remove(temp_file.name) + return response + # TODO: error message + return HttpResponseRedirect(labelbase.get_absolute_url()) + + +class LabelUpdateView(UpdateView): + model = Label + fields = ["type", "ref", "label", "origin", "spendable"] + + def get_template_names(self): + if 'action' in self.kwargs: + action = self.kwargs['action'] + if action == 'labeling': + return "label_edit_labeling.html" + return "label_edit_update.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["active_labelbase_id"] = self.object.labelbase.id + context["labelbase"] = self.object.labelbase + context["action"] = self.kwargs.get('action', 'update') + if context["action"] == "labeling": + #mempool_api = self.object.labelbase.get_mempool_api() + if self.object.type == "tx": + mempool_api = self.object.labelbase.get_mempool_api() + context["res_tx"] = mempool_api.get_transaction(self.object.ref) + + if self.object.type == "output": + context["output"] = OutputStat.objects.filter(type_ref_hash= \ + self.object.type_ref_hash).last() + + return context + + + + + + +class LabelbaseUpdateView(UpdateView): + def post(self, request, *args, **kwargs): + labelbase_id = self.kwargs["labelbase_id"] + labelbase = get_object_or_404( + Labelbase, id=labelbase_id, user_id=self.request.user.id + ) + labelbase.name = request.POST.get("name", "") + labelbase.fingerprint = request.POST.get("fingerprint", "") + labelbase.about = request.POST.get("about", "") + labelbase.operation_mode = request.POST.get("operation_mode", "") + labelbase.network = request.POST.get("network", "") + + labelbase.save() + return HttpResponseRedirect(labelbase.get_absolute_url()) + + +class LabelbaseFormView(FormView): + template_name = "labelbase_new.html" + form_class = LabelbaseForm + + def get(self, request, *args, **kwargs): + return redirect(resolve_url("home")) + + def form_valid(self, form): + form.instance.user = self.request.user + form.save() + return redirect(form.instance.get_absolute_url()) + + +class RegistrationView(FormView): + template_name = "registration.html" + form_class = UserCreationForm + + def form_valid(self, form): + form.save() + return redirect("registration_complete") + + +class RegistrationCompleteView(TemplateView): + template_name = "registration_complete.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["login_url"] = resolve_url(settings.LOGIN_URL) + return context + + +@class_view_decorator(never_cache) +class ExampleSecretView(OTPRequiredMixin, TemplateView): + template_name = "secret.html" diff --git a/django/labellabor/wsgi.py b/django/labellabor/wsgi.py new file mode 100644 index 0000000..56fd2cc --- /dev/null +++ b/django/labellabor/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for labellabor project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "labellabor.settings") + +application = get_wsgi_application()