mirror of
https://github.com/Labelbase/Labelbase.git
synced 2026-08-13 12:33:23 +02:00
v2.1.0 w/ attachments support
This commit is contained in:
parent
3547f4b4b3
commit
113cb6e929
92 changed files with 2268 additions and 82 deletions
|
|
@ -7,6 +7,7 @@ db/
|
|||
*.pyc
|
||||
__pycache__
|
||||
django/importer/uploadeddata/*
|
||||
django/attachments/attachment/*
|
||||
_scratches
|
||||
exports.sh
|
||||
.last_git_commit
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -8,6 +8,7 @@ db/
|
|||
*.pyc
|
||||
__pycache__
|
||||
django/importer/uploadeddata/*
|
||||
django/attachments/attachment/*
|
||||
#_scratches
|
||||
exports.sh
|
||||
.last_git_commit
|
||||
|
|
|
|||
3
django/attachments/README.md
Normal file
3
django/attachments/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
Friendly-forked at commit https://github.com/bartTC/django-attachments/commit/d0c3e7b366691f2a57329c804b0dd002064396a6 (version 1.11) and extended for Labelbase.
|
||||
|
||||
BSD 3-Clause "New" or "Revised" License https://github.com/bartTC/django-attachments/blob/master/LICENSE
|
||||
1
django/attachments/__init__.py
Normal file
1
django/attachments/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
default_app_config = "attachments.apps.AttachmentsConfig"
|
||||
21
django/attachments/admin.py
Normal file
21
django/attachments/admin.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib.contenttypes.admin import GenericStackedInline
|
||||
|
||||
|
||||
|
||||
from django.contrib import admin
|
||||
from .models import LabelAttachment
|
||||
from .models import Attachment
|
||||
|
||||
class AttachmentInlines(GenericStackedInline):
|
||||
model = Attachment
|
||||
exclude = ()
|
||||
extra = 1
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
admin.site.register(LabelAttachment)
|
||||
9
django/attachments/apps.py
Normal file
9
django/attachments/apps.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
from __future__ import unicode_literals
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class AttachmentsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.AutoField'
|
||||
name = "attachments"
|
||||
verbose_name = _("Attachments")
|
||||
BIN
django/attachments/django-attachments-1.11.zip
Normal file
BIN
django/attachments/django-attachments-1.11.zip
Normal file
Binary file not shown.
46
django/attachments/forms.py
Normal file
46
django/attachments/forms.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django import forms
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.template.defaultfilters import filesizeformat
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .models import Attachment
|
||||
|
||||
config = apps.get_app_config("attachments")
|
||||
|
||||
|
||||
def validate_max_size(data):
|
||||
if (
|
||||
hasattr(settings, "FILE_UPLOAD_MAX_SIZE")
|
||||
and data.size > settings.FILE_UPLOAD_MAX_SIZE
|
||||
):
|
||||
raise forms.ValidationError(
|
||||
_("File exceeds maximum size of {size}").format(
|
||||
size=filesizeformat(settings.FILE_UPLOAD_MAX_SIZE)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def custom_attachment_validators(uploaded_file):
|
||||
for validator in getattr(config, "attachment_validators", ()):
|
||||
validator(uploaded_file)
|
||||
|
||||
|
||||
class AttachmentForm(forms.ModelForm):
|
||||
attachment_file = forms.FileField(
|
||||
label=_("Upload attachment"),
|
||||
validators=[validate_max_size, custom_attachment_validators]
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Attachment
|
||||
fields = ("attachment_file",)
|
||||
|
||||
def save(self, request, obj, *args, **kwargs):
|
||||
self.instance.creator = request.user
|
||||
self.instance.content_type = ContentType.objects.get_for_model(obj)
|
||||
self.instance.object_id = obj.pk
|
||||
super(AttachmentForm, self).save(*args, **kwargs)
|
||||
11
django/attachments/locale/README.transifex
Normal file
11
django/attachments/locale/README.transifex
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
Transifex.net Token Verification
|
||||
=================================
|
||||
|
||||
The list of tokens bellow guarantee the respective users to be able to enable
|
||||
submission on components using the following repository url:
|
||||
|
||||
git://github.com/bartTC/django-attachments.git
|
||||
|
||||
Tokens:
|
||||
|
||||
24u9WhbEZQbCGDn3ruWJ6c7YpDpSxCCf / bartTC
|
||||
BIN
django/attachments/locale/da/LC_MESSAGES/django.mo
Normal file
BIN
django/attachments/locale/da/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
54
django/attachments/locale/da/LC_MESSAGES/django.po
Normal file
54
django/attachments/locale/da/LC_MESSAGES/django.po
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# django-attachments in Danish
|
||||
# django-attachments på Dansk
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# Michael Lind Mortensen <illio@cs.au.dk>, 2009.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: django-attachments\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2009-08-27 23:52+0200\n"
|
||||
"PO-Revision-Date: 2010-04-04 00:24+0100\n"
|
||||
"Last-Translator: Martin Mahner <martin@mahner.org>\n"
|
||||
"Language-Team: da <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: forms.py:7
|
||||
msgid "Upload attachment"
|
||||
msgstr "Upload vedhæftning"
|
||||
|
||||
#: models.py:29
|
||||
msgid "creator"
|
||||
msgstr "skaber"
|
||||
|
||||
#: models.py:30
|
||||
msgid "attachment"
|
||||
msgstr "vedhæftning"
|
||||
|
||||
#: models.py:31
|
||||
msgid "created"
|
||||
msgstr "skabt"
|
||||
|
||||
#: models.py:32
|
||||
msgid "modified"
|
||||
msgstr "ændret"
|
||||
|
||||
#: views.py:33
|
||||
msgid "Your attachment was uploaded."
|
||||
msgstr "Din vedhæftning blev uploadet."
|
||||
|
||||
#: views.py:51
|
||||
msgid "Your attachment was deleted."
|
||||
msgstr "Din vedhæftning blev slettet."
|
||||
|
||||
#: templates/attachments/add_form.html:7
|
||||
msgid "Add attachment"
|
||||
msgstr "Tilføj vedhæftning"
|
||||
|
||||
#: templates/attachments/delete_link.html:2
|
||||
msgid "Delete attachment"
|
||||
msgstr "Slet vedhæftning"
|
||||
|
||||
BIN
django/attachments/locale/de/LC_MESSAGES/django.mo
Normal file
BIN
django/attachments/locale/de/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
53
django/attachments/locale/de/LC_MESSAGES/django.po
Normal file
53
django/attachments/locale/de/LC_MESSAGES/django.po
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: django-attachments\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2010-04-04 00:19+0200\n"
|
||||
"PO-Revision-Date: 2010-04-04 00:29+0100\n"
|
||||
"Last-Translator: Martin Mahner <martin@mahner.org>\n"
|
||||
"Language-Team: de <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: forms.py:7
|
||||
msgid "Upload attachment"
|
||||
msgstr "Anhang hochladen"
|
||||
|
||||
#: models.py:29
|
||||
msgid "creator"
|
||||
msgstr "Autor"
|
||||
|
||||
#: models.py:30
|
||||
msgid "attachment"
|
||||
msgstr "Anhang"
|
||||
|
||||
#: models.py:31
|
||||
msgid "created"
|
||||
msgstr "Erstellt"
|
||||
|
||||
#: models.py:32
|
||||
msgid "modified"
|
||||
msgstr "Geändert"
|
||||
|
||||
#: views.py:33
|
||||
msgid "Your attachment was uploaded."
|
||||
msgstr "Dein Anhang wurde hochgeladen."
|
||||
|
||||
#: views.py:51
|
||||
msgid "Your attachment was deleted."
|
||||
msgstr "Dein Anhang wurde gelöscht."
|
||||
|
||||
#: templates/attachments/add_form.html:7
|
||||
msgid "Add attachment"
|
||||
msgstr "Anhang hinzufügen"
|
||||
|
||||
#: templates/attachments/delete_link.html:2
|
||||
msgid "Delete attachment"
|
||||
msgstr "Anhang löschen"
|
||||
|
||||
BIN
django/attachments/locale/el/LC_MESSAGES/django.mo
Normal file
BIN
django/attachments/locale/el/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
53
django/attachments/locale/el/LC_MESSAGES/django.po
Normal file
53
django/attachments/locale/el/LC_MESSAGES/django.po
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# Panos Laganakos <panos.laganakos@gmail.com>, 2010.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: django-attachments\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2010-04-04 02:52+0300\n"
|
||||
"PO-Revision-Date: 2010-04-04 15:07+0100\n"
|
||||
"Last-Translator: Martin Mahner <martin@mahner.org>\n"
|
||||
"Language-Team: gr <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: forms.py:7
|
||||
msgid "Upload attachment"
|
||||
msgstr "Φώρτωση συνημμένου"
|
||||
|
||||
#: models.py:29
|
||||
msgid "creator"
|
||||
msgstr "δημιουργός"
|
||||
|
||||
#: models.py:30
|
||||
msgid "attachment"
|
||||
msgstr "συνημμένο"
|
||||
|
||||
#: models.py:31
|
||||
msgid "created"
|
||||
msgstr "δημιουργήθηκε"
|
||||
|
||||
#: models.py:32
|
||||
msgid "modified"
|
||||
msgstr "τροποποιήθηκε"
|
||||
|
||||
#: views.py:33
|
||||
msgid "Your attachment was uploaded."
|
||||
msgstr "Η επισύναψη σας ολοκληρώθηκε."
|
||||
|
||||
#: views.py:51
|
||||
msgid "Your attachment was deleted."
|
||||
msgstr "Το συνήμμενο σας διεγράφει."
|
||||
|
||||
#: templates/attachments/add_form.html:7
|
||||
msgid "Add attachment"
|
||||
msgstr "Προσθέστε ένα συνημμένο"
|
||||
|
||||
#: templates/attachments/delete_link.html:2
|
||||
msgid "Delete attachment"
|
||||
msgstr "Διαγράψτε το συνημμένο"
|
||||
|
||||
BIN
django/attachments/locale/en/LC_MESSAGES/django.mo
Normal file
BIN
django/attachments/locale/en/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
53
django/attachments/locale/en/LC_MESSAGES/django.po
Normal file
53
django/attachments/locale/en/LC_MESSAGES/django.po
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: django-attachments\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2010-04-04 00:18+0200\n"
|
||||
"PO-Revision-Date: 2010-04-04 00:28+0100\n"
|
||||
"Last-Translator: Martin Mahner <martin@mahner.org>\n"
|
||||
"Language-Team: en <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: forms.py:7
|
||||
msgid "Upload attachment"
|
||||
msgstr "Upload attachment"
|
||||
|
||||
#: models.py:29
|
||||
msgid "creator"
|
||||
msgstr "creator"
|
||||
|
||||
#: models.py:30
|
||||
msgid "attachment"
|
||||
msgstr "attachment"
|
||||
|
||||
#: models.py:31
|
||||
msgid "created"
|
||||
msgstr "created"
|
||||
|
||||
#: models.py:32
|
||||
msgid "modified"
|
||||
msgstr "modified"
|
||||
|
||||
#: views.py:33
|
||||
msgid "Your attachment was uploaded."
|
||||
msgstr "Your attachment was uploaded."
|
||||
|
||||
#: views.py:51
|
||||
msgid "Your attachment was deleted."
|
||||
msgstr "Your attachment was deleted."
|
||||
|
||||
#: templates/attachments/add_form.html:7
|
||||
msgid "Add attachment"
|
||||
msgstr "Add attachment"
|
||||
|
||||
#: templates/attachments/delete_link.html:2
|
||||
msgid "Delete attachment"
|
||||
msgstr "Delete attachment"
|
||||
|
||||
BIN
django/attachments/locale/es_AR/LC_MESSAGES/django.mo
Normal file
BIN
django/attachments/locale/es_AR/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
54
django/attachments/locale/es_AR/LC_MESSAGES/django.po
Normal file
54
django/attachments/locale/es_AR/LC_MESSAGES/django.po
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# Gonzalo Bustos, 2015.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2014-05-31 17:20-0300\n"
|
||||
"PO-Revision-Date: 2015-10-11 22:04-0300\n"
|
||||
"Last-Translator: Gonzalo Bustos\n"
|
||||
"Language-Team: Spanish (Argentina)\n"
|
||||
"Language: es_AR\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 1.6.10\n"
|
||||
|
||||
#: forms.py:7
|
||||
msgid "Upload attachment"
|
||||
msgstr "Subir adjunto"
|
||||
|
||||
#: models.py:32
|
||||
msgid "creator"
|
||||
msgstr "creador"
|
||||
|
||||
#: models.py:33 models.py:38
|
||||
msgid "attachment"
|
||||
msgstr "adjunto"
|
||||
|
||||
#: models.py:34
|
||||
msgid "created"
|
||||
msgstr "creado"
|
||||
|
||||
#: models.py:35
|
||||
msgid "modified"
|
||||
msgstr "modificado"
|
||||
|
||||
#: views.py:34
|
||||
msgid "Your attachment was uploaded."
|
||||
msgstr "Su adjunto fue subido."
|
||||
|
||||
#: views.py:52
|
||||
msgid "Your attachment was deleted."
|
||||
msgstr "Su adjunto fue eliminado."
|
||||
|
||||
#: templates/attachments/add_form.html:7
|
||||
msgid "Add attachment"
|
||||
msgstr "Agregar adjunto"
|
||||
|
||||
#: templates/attachments/delete_link.html:2
|
||||
msgid "Delete attachment"
|
||||
msgstr "Eliminar adjunto"
|
||||
BIN
django/attachments/locale/fi/LC_MESSAGES/django.mo
Normal file
BIN
django/attachments/locale/fi/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
54
django/attachments/locale/fi/LC_MESSAGES/django.po
Normal file
54
django/attachments/locale/fi/LC_MESSAGES/django.po
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: django-attachments\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2016-06-11 15:55+0300\n"
|
||||
"PO-Revision-Date: 2016-06-11 16:03+0300\n"
|
||||
"Last-Translator: Aleksi Häkli <aleksi.hakli@iki.fi>\n"
|
||||
"Language-Team: fi <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: fi\n"
|
||||
"X-Generator: Poedit 1.8.8\n"
|
||||
|
||||
#: forms.py:7
|
||||
msgid "Upload attachment"
|
||||
msgstr "Lähetä liite"
|
||||
|
||||
#: models.py:29
|
||||
msgid "creator"
|
||||
msgstr "luoja"
|
||||
|
||||
#: models.py:30
|
||||
msgid "attachment"
|
||||
msgstr "liite"
|
||||
|
||||
#: models.py:31
|
||||
msgid "created"
|
||||
msgstr "luotu"
|
||||
|
||||
#: models.py:32
|
||||
msgid "modified"
|
||||
msgstr "muokattu"
|
||||
|
||||
#: views.py:33
|
||||
msgid "Your attachment was uploaded."
|
||||
msgstr "Liitteesi ladattiin palvelimelle."
|
||||
|
||||
#: views.py:51
|
||||
msgid "Your attachment was deleted."
|
||||
msgstr "Liitteesi poistettiin palvelimelta."
|
||||
|
||||
#: templates/attachments/add_form.html:7
|
||||
msgid "Add attachment"
|
||||
msgstr "Lisää liite"
|
||||
|
||||
#: templates/attachments/delete_link.html:2
|
||||
msgid "Delete attachment"
|
||||
msgstr "Poista liite"
|
||||
BIN
django/attachments/locale/fr/LC_MESSAGES/django.mo
Normal file
BIN
django/attachments/locale/fr/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
53
django/attachments/locale/fr/LC_MESSAGES/django.po
Normal file
53
django/attachments/locale/fr/LC_MESSAGES/django.po
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: django-attachments\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2010-04-04 00:18+0200\n"
|
||||
"PO-Revision-Date: 2010-04-04 00:28+0100\n"
|
||||
"Last-Translator: AERT <dev.aert@gmail.com>\n"
|
||||
"Language-Team: en <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: forms.py:7
|
||||
msgid "Upload attachment"
|
||||
msgstr "Ajouter une pièce jointe"
|
||||
|
||||
#: models.py:29
|
||||
msgid "creator"
|
||||
msgstr "créateur"
|
||||
|
||||
#: models.py:30
|
||||
msgid "attachment"
|
||||
msgstr "pièce jointe"
|
||||
|
||||
#: models.py:31
|
||||
msgid "created"
|
||||
msgstr "créé"
|
||||
|
||||
#: models.py:32
|
||||
msgid "modified"
|
||||
msgstr "modifié"
|
||||
|
||||
#: views.py:33
|
||||
msgid "Your attachment was uploaded."
|
||||
msgstr "Votre pièce jointe a été ajoutée."
|
||||
|
||||
#: views.py:51
|
||||
msgid "Your attachment was deleted."
|
||||
msgstr "Votre pièce jointe a été supprimée."
|
||||
|
||||
#: templates/attachments/add_form.html:7
|
||||
msgid "Add attachment"
|
||||
msgstr "Ajouter une pièce jointe"
|
||||
|
||||
#: templates/attachments/delete_link.html:2
|
||||
msgid "Delete attachment"
|
||||
msgstr "Supprimer la pièce jointe"
|
||||
|
||||
BIN
django/attachments/locale/it/LC_MESSAGES/django.mo
Normal file
BIN
django/attachments/locale/it/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
77
django/attachments/locale/it/LC_MESSAGES/django.po
Normal file
77
django/attachments/locale/it/LC_MESSAGES/django.po
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-05-29 16:35+0200\n"
|
||||
"PO-Revision-Date: 2018-05-29 16:39+0200\n"
|
||||
"Language: it\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"Last-Translator: Mario Orlandi <morlandi@brainstorm.it>\n"
|
||||
"Language-Team: \n"
|
||||
"X-Generator: Poedit 2.0.7\n"
|
||||
|
||||
#: apps.py:7
|
||||
msgid "Attachments"
|
||||
msgstr "Allegati"
|
||||
|
||||
#: forms.py:13
|
||||
#, python-brace-format
|
||||
msgid "File exceeds maximum size of {size}"
|
||||
msgstr "Il file supera le dimensioni massime di {size}"
|
||||
|
||||
#: forms.py:18
|
||||
msgid "Upload attachment"
|
||||
msgstr "Invia allegato"
|
||||
|
||||
#: models.py:35
|
||||
msgid "creator"
|
||||
msgstr "autore"
|
||||
|
||||
#: models.py:36 models.py:41
|
||||
msgid "attachment"
|
||||
msgstr "allegato"
|
||||
|
||||
#: models.py:37
|
||||
msgid "created"
|
||||
msgstr "creato il"
|
||||
|
||||
#: models.py:38
|
||||
msgid "modified"
|
||||
msgstr "modificato il"
|
||||
|
||||
#: models.py:42
|
||||
msgid "attachments"
|
||||
msgstr "allegati"
|
||||
|
||||
#: models.py:45
|
||||
msgid "Can delete foreign attachments"
|
||||
msgstr "Può eliminare gli allegati esterni"
|
||||
|
||||
#: models.py:49
|
||||
#, python-brace-format
|
||||
msgid "{username} attached {filename}"
|
||||
msgstr "{username} ha allegato {filename}"
|
||||
|
||||
#: templates/attachments/add_form.html:8
|
||||
msgid "Add attachment"
|
||||
msgstr "Aggiungi allegato"
|
||||
|
||||
#: templates/attachments/delete_link.html:2
|
||||
msgid "Delete attachment"
|
||||
msgstr "Elimina allegato"
|
||||
|
||||
#: views.py:51
|
||||
msgid "Your attachment was uploaded."
|
||||
msgstr "L'allegato è stato inviato."
|
||||
|
||||
#: views.py:75
|
||||
msgid "Your attachment was deleted."
|
||||
msgstr "L'allegato è stato eliminato."
|
||||
BIN
django/attachments/locale/pt_BR/LC_MESSAGES/django.mo
Normal file
BIN
django/attachments/locale/pt_BR/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
54
django/attachments/locale/pt_BR/LC_MESSAGES/django.po
Normal file
54
django/attachments/locale/pt_BR/LC_MESSAGES/django.po
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2014-05-31 17:20-0300\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: forms.py:7
|
||||
msgid "Upload attachment"
|
||||
msgstr "Subir anexo"
|
||||
|
||||
#: models.py:32
|
||||
msgid "creator"
|
||||
msgstr "criador"
|
||||
|
||||
#: models.py:33 models.py:38
|
||||
msgid "attachment"
|
||||
msgstr "anexo"
|
||||
|
||||
#: models.py:34
|
||||
msgid "created"
|
||||
msgstr "criado"
|
||||
|
||||
#: models.py:35
|
||||
msgid "modified"
|
||||
msgstr "modificado"
|
||||
|
||||
#: views.py:34
|
||||
msgid "Your attachment was uploaded."
|
||||
msgstr "Seu anexo foi enviado."
|
||||
|
||||
#: views.py:52
|
||||
msgid "Your attachment was deleted."
|
||||
msgstr "Seu anexo foi excluido."
|
||||
|
||||
#: templates/attachments/add_form.html:7
|
||||
msgid "Add attachment"
|
||||
msgstr "Adicionar anexo"
|
||||
|
||||
#: templates/attachments/delete_link.html:2
|
||||
msgid "Delete attachment"
|
||||
msgstr "Excluir anexo"
|
||||
61
django/attachments/locale/ru/LC_MESSAGES/django.po
Normal file
61
django/attachments/locale/ru/LC_MESSAGES/django.po
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: django-attachments\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2017-04-07 23:35+0300\n"
|
||||
"PO-Revision-Date: 2017-04-07 23:26+0300:MI+ZONE\n"
|
||||
"Last-Translator: Maxim Barabanov <maxim.barabanov@gmail.com>\n"
|
||||
"Language-Team: RU <LL@li.org>\n"
|
||||
"Language: Russian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
|
||||
"%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n"
|
||||
"%100>=11 && n%100<=14)? 2 : 3);\n"
|
||||
|
||||
#: .\attachments\forms.py:9
|
||||
msgid "Upload attachment"
|
||||
msgstr "Загрузить вложение"
|
||||
|
||||
#: .\attachments\models.py:35
|
||||
msgid "creator"
|
||||
msgstr "создатель"
|
||||
|
||||
#: .\attachments\models.py:36
|
||||
msgid "attachment"
|
||||
msgstr "вложение"
|
||||
|
||||
#: .\attachments\models.py:37
|
||||
msgid "created"
|
||||
msgstr "создано"
|
||||
|
||||
#: .\attachments\models.py:38
|
||||
msgid "modified"
|
||||
msgstr "изменено"
|
||||
|
||||
#: .\attachments\models.py:43
|
||||
msgid "Can delete foreign attachments"
|
||||
msgstr "Может удалять чужие вложения"
|
||||
|
||||
#: .\attachments\templates\attachments\add_form.html:8
|
||||
msgid "Add attachment"
|
||||
msgstr "Добавить вложение"
|
||||
|
||||
#: .\attachments\templates\attachments\delete_link.html:2
|
||||
msgid "Delete attachment"
|
||||
msgstr "Удалить вложение"
|
||||
|
||||
#: .\attachments\views.py:40
|
||||
msgid "Your attachment was uploaded."
|
||||
msgstr "Ваше вложение было загружено."
|
||||
|
||||
#: .\attachments\views.py:62
|
||||
msgid "Your attachment was deleted."
|
||||
msgstr "Ваше вложение было удалено"
|
||||
0
django/attachments/management/__init__.py
Normal file
0
django/attachments/management/__init__.py
Normal file
0
django/attachments/management/commands/__init__.py
Normal file
0
django/attachments/management/commands/__init__.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
|
||||
from attachments.models import Attachment
|
||||
from attachments.views import remove_file_from_disk
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = ("Remove attachments for which the related objects "
|
||||
"don't exist anymore!")
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'-y', '--yes', default='x', action='store_const', const='y',
|
||||
dest='answer', help='Automatically confirm deletion',
|
||||
)
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
verbose = kwargs['verbosity'] >= 1
|
||||
answer = kwargs['answer']
|
||||
|
||||
# -v0 sets --yes
|
||||
if not verbose:
|
||||
answer = 'y'
|
||||
|
||||
for att in Attachment.objects.all():
|
||||
if att.content_object is None:
|
||||
if verbose:
|
||||
self.stdout.write(
|
||||
"Attachment `%s' to non-existing `%s' with PK `%s'" %
|
||||
(att, att.content_type.model, att.object_id))
|
||||
|
||||
while answer not in 'yn':
|
||||
answer = input("Do you wish to delete? [yN] ")
|
||||
if not answer:
|
||||
answer = 'x'
|
||||
continue
|
||||
answer = answer[0].lower()
|
||||
|
||||
if answer == 'y' :
|
||||
remove_file_from_disk(att.attachment_file)
|
||||
att.delete()
|
||||
|
||||
if verbose:
|
||||
self.stdout.write("Deleted attachment `%s'" % att)
|
||||
34
django/attachments/migrations/0001_initial.py
Normal file
34
django/attachments/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.conf import settings
|
||||
import attachments.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('contenttypes', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Attachment',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('object_id', models.PositiveIntegerField()),
|
||||
('attachment_file', models.FileField(upload_to=attachments.models.attachment_upload, verbose_name='attachment')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='created')),
|
||||
('modified', models.DateTimeField(auto_now=True, verbose_name='modified')),
|
||||
('content_type', models.ForeignKey(to='contenttypes.ContentType', on_delete=models.CASCADE)),
|
||||
('creator', models.ForeignKey(related_name='created_attachments', verbose_name='creator', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-created'],
|
||||
'permissions': (('delete_foreign_attachments', 'Can delete foreign attachments'),),
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
]
|
||||
19
django/attachments/migrations/0002_auto_20180104_1247.py
Normal file
19
django/attachments/migrations/0002_auto_20180104_1247.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2018-01-04 12:47
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('attachments', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='attachment',
|
||||
options={'ordering': ['-created'], 'permissions': (('delete_foreign_attachments', 'Can delete foreign attachments'),), 'verbose_name': 'attachment', 'verbose_name_plural': 'attachments'},
|
||||
),
|
||||
]
|
||||
18
django/attachments/migrations/0003_auto_20190722_1216.py
Normal file
18
django/attachments/migrations/0003_auto_20190722_1216.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 2.2.3 on 2019-07-22 12:16
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('attachments', '0002_auto_20180104_1247'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='attachment',
|
||||
name='object_id',
|
||||
field=models.TextField(),
|
||||
),
|
||||
]
|
||||
25
django/attachments/migrations/0004_db_index.py
Normal file
25
django/attachments/migrations/0004_db_index.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 3.0.9 on 2020-08-17 13:29
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('attachments', '0003_auto_20190722_1216'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='attachment',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True, db_index=True,
|
||||
verbose_name='created'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='attachment',
|
||||
name='modified',
|
||||
field=models.DateTimeField(auto_now=True, db_index=True,
|
||||
verbose_name='modified'),
|
||||
),
|
||||
]
|
||||
16
django/attachments/migrations/0005_object_id_charfield.py
Normal file
16
django/attachments/migrations/0005_object_id_charfield.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('attachments', '0004_db_index'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='attachment',
|
||||
name='object_id',
|
||||
field=models.CharField(db_index=True, max_length=64),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 3.2.25 on 2024-04-11 14:35
|
||||
|
||||
from django.db import migrations, models
|
||||
import uuid_upload_path.storage
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('attachments', '0005_object_id_charfield'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='attachment',
|
||||
name='attachment_file',
|
||||
field=models.FileField(upload_to=uuid_upload_path.storage.upload_to, verbose_name='attachment'),
|
||||
),
|
||||
]
|
||||
28
django/attachments/migrations/0007_labelattachment.py
Normal file
28
django/attachments/migrations/0007_labelattachment.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# Generated by Django 3.2.25 on 2024-04-11 15:06
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('attachments', '0006_alter_attachment_attachment_file'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='LabelAttachment',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('type_ref_hash', models.CharField(blank=True, help_text='Reflects type + ref, where type could be any bip-329 defined type', max_length=64)),
|
||||
('network', models.CharField(choices=[('mainnet', 'Mainnet'), ('testnet', 'Testnet')], default='mainnet', help_text="Choose the network for this labelbase's label attachement.", max_length=10)),
|
||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('user', 'network', 'type_ref_hash')},
|
||||
},
|
||||
),
|
||||
]
|
||||
0
django/attachments/migrations/__init__.py
Normal file
0
django/attachments/migrations/__init__.py
Normal file
117
django/attachments/models.py
Normal file
117
django/attachments/models.py
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from six import python_2_unicode_compatible
|
||||
from uuid_upload_path import upload_to
|
||||
from django.contrib.auth.models import User
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger('labelbase')
|
||||
|
||||
def attachment_upload(instance, filename):
|
||||
"""Stores the attachment in a "per module/appname/primary key" folder"""
|
||||
return "attachments/{app}_{model}/{pk}/{filename}".format(
|
||||
app=instance.content_object._meta.app_label,
|
||||
model=instance.content_object._meta.object_name.lower(),
|
||||
pk=instance.content_object.pk,
|
||||
filename=filename,
|
||||
)
|
||||
|
||||
|
||||
class AttachmentManager(models.Manager):
|
||||
def attachments_for_object(self, obj):
|
||||
object_type = ContentType.objects.get_for_model(obj)
|
||||
print ("x attachments_for_object id {} , {} {}".format( obj.pk, object_type, object_type.id ))
|
||||
return self.filter(content_type__pk=object_type.id, object_id=obj.pk)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Attachment(models.Model):
|
||||
objects = AttachmentManager()
|
||||
|
||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||
object_id = models.CharField(db_index=True, max_length=64)
|
||||
content_object = GenericForeignKey("content_type", "object_id")
|
||||
creator = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
related_name="created_attachments",
|
||||
verbose_name=_("creator"),
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
attachment_file = models.FileField(
|
||||
_("attachment"), upload_to=upload_to # attachment_upload
|
||||
)
|
||||
created = models.DateTimeField(_("created"), auto_now_add=True, db_index=True)
|
||||
modified = models.DateTimeField(_("modified"), auto_now=True, db_index=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("attachment")
|
||||
verbose_name_plural = _("attachments")
|
||||
ordering = ["-created"]
|
||||
permissions = (
|
||||
("delete_foreign_attachments", _("Can delete foreign attachments")),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return _("{username} attached {filename}").format(
|
||||
username=self.creator.get_username(),
|
||||
filename=self.attachment_file.name,
|
||||
)
|
||||
|
||||
@property
|
||||
def filename(self):
|
||||
return os.path.split(self.attachment_file.name)[1]
|
||||
|
||||
def attach_to(self, new_object, update_path=False):
|
||||
"""
|
||||
Attach to a new object and possibly move the actual file on disk!
|
||||
|
||||
.. important::
|
||||
|
||||
As long as path names are valid you can continue serving
|
||||
the files from their original path and not change it!
|
||||
"""
|
||||
self.object_id = new_object.pk
|
||||
self.content_type = ContentType.objects.get_for_model(new_object)
|
||||
self.save()
|
||||
|
||||
if update_path:
|
||||
old_path = self.attachment_file.path
|
||||
self.attachment_file.name = upload_to # attachment_upload(self, self.filename)
|
||||
self.attachment_file.save()
|
||||
|
||||
os.makedirs(
|
||||
os.path.dirname(self.attachment_file.path), exist_ok=True)
|
||||
os.rename(old_path, self.attachment_file.path)
|
||||
|
||||
|
||||
class LabelAttachment(models.Model):
|
||||
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True)
|
||||
type_ref_hash = models.CharField(max_length=64,
|
||||
blank=True,
|
||||
help_text="Reflects type + ref, where type could be any bip-329 defined type")
|
||||
|
||||
MAINNET = 'mainnet'
|
||||
TESTNET = 'testnet'
|
||||
|
||||
NETWORK_CHOICES = [
|
||||
(MAINNET, 'Mainnet'),
|
||||
(TESTNET, 'Testnet'),
|
||||
]
|
||||
|
||||
network = models.CharField(
|
||||
max_length=10,
|
||||
choices=NETWORK_CHOICES,
|
||||
default='mainnet',
|
||||
help_text="Choose the network for this labelbase's label attachement."
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = (("user", "network", "type_ref_hash"),)
|
||||
0
django/attachments/templatetags/__init__.py
Normal file
0
django/attachments/templatetags/__init__.py
Normal file
77
django/attachments/templatetags/attachments_tags.py
Normal file
77
django/attachments/templatetags/attachments_tags.py
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
from django.template import Library
|
||||
from django.urls import reverse
|
||||
|
||||
from ..forms import AttachmentForm
|
||||
from ..models import Attachment
|
||||
from ..views import add_url_for_obj
|
||||
|
||||
register = Library()
|
||||
|
||||
|
||||
@register.inclusion_tag("attachments/add_form.html", takes_context=True)
|
||||
def attachment_form(context, obj, **kwargs):
|
||||
"""
|
||||
Renders a "upload attachment" form.
|
||||
|
||||
The user must own ``attachments.add_attachment permission`` to add
|
||||
attachments.
|
||||
"""
|
||||
if context["user"].has_perm("attachments.add_attachment"):
|
||||
return {
|
||||
"form": AttachmentForm(),
|
||||
"form_url": add_url_for_obj(obj),
|
||||
"next": kwargs.get("next", context.request.build_absolute_uri()),
|
||||
}
|
||||
else:
|
||||
return {"form": None}
|
||||
|
||||
|
||||
@register.inclusion_tag("attachments/delete_link.html", takes_context=True)
|
||||
def attachment_delete_link(context, attachment, **kwargs):
|
||||
"""
|
||||
Renders a html link to the delete view of the given attachment. Returns
|
||||
no content if the request-user has no permission to delete attachments.
|
||||
|
||||
The user must own either the ``attachments.delete_attachment`` permission
|
||||
and is the creator of the attachment, that he can delete it or he has
|
||||
``attachments.delete_foreign_attachments`` which allows him to delete all
|
||||
attachments.
|
||||
"""
|
||||
if context["user"].has_perm("attachments.delete_foreign_attachments") or (
|
||||
context["user"] == attachment.creator
|
||||
and context["user"].has_perm("attachments.delete_attachment")
|
||||
):
|
||||
return {
|
||||
"next": kwargs.get("next", context.request.build_absolute_uri()),
|
||||
"delete_url": reverse(
|
||||
"attachments:delete", kwargs={"attachment_pk": attachment.pk}
|
||||
),
|
||||
}
|
||||
return {"delete_url": None}
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def attachments_count(obj):
|
||||
"""
|
||||
Counts attachments that are attached to a given object::
|
||||
|
||||
{% attachments_count obj %}
|
||||
"""
|
||||
attachments_count = Attachment.objects.attachments_for_object(obj).count()
|
||||
print("obj: {}, attachments_count: {}".format(obj.id, attachments_count))
|
||||
|
||||
return attachments_count
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def get_attachments_for(obj, *args, **kwargs):
|
||||
"""
|
||||
Resolves attachments that are attached to a given object. You can specify
|
||||
the variable name in the context the attachments are stored using the `as`
|
||||
argument.
|
||||
|
||||
Syntax::
|
||||
|
||||
{% get_attachments_for obj as "my_attachments" %}
|
||||
"""
|
||||
return Attachment.objects.attachments_for_object(obj)
|
||||
0
django/attachments/tests/__init__.py
Normal file
0
django/attachments/tests/__init__.py
Normal file
68
django/attachments/tests/base.py
Normal file
68
django/attachments/tests/base.py
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib.auth.models import Permission, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from ..models import Attachment
|
||||
from .testapp.models import TestModel
|
||||
|
||||
|
||||
class BaseTestCase(TestCase):
|
||||
target_model_class = TestModel
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create two users with `attachments.add_attachment` permission
|
||||
and one object to attach files to.
|
||||
"""
|
||||
content_type = ContentType.objects.get_for_model(Attachment)
|
||||
self.add_permission = Permission.objects.get(
|
||||
content_type=content_type, codename="add_attachment"
|
||||
)
|
||||
self.del_permission = Permission.objects.get(
|
||||
content_type=content_type, codename="delete_attachment"
|
||||
)
|
||||
|
||||
self.del_foreign_permission = Permission.objects.get(
|
||||
content_type=content_type, codename="delete_foreign_attachments"
|
||||
)
|
||||
|
||||
self.cred_jon = {"username": "jon", "password": "foobar"}
|
||||
self.cred_jane = {"username": "jane", "password": "foobar"}
|
||||
self.jon = User.objects.create_user(**self.cred_jon)
|
||||
self.jon.user_permissions.add(self.add_permission)
|
||||
self.jon.user_permissions.add(self.del_permission)
|
||||
|
||||
self.jane = User.objects.create_user(**self.cred_jane)
|
||||
self.jane.user_permissions.add(self.add_permission)
|
||||
self.jane.user_permissions.add(self.del_permission)
|
||||
|
||||
self.obj = self.target_model_class.objects.create(title="My first test item")
|
||||
|
||||
def _upload_testfile(self, file_obj=None, file_content=b"file content", **extra):
|
||||
"""
|
||||
Uploads a sample file for the given user.
|
||||
"""
|
||||
add_url = reverse(
|
||||
"attachments:add",
|
||||
kwargs={
|
||||
"app_label": "testapp",
|
||||
"model_name": self.target_model_class.__name__.lower(),
|
||||
"pk": self.obj.pk,
|
||||
},
|
||||
)
|
||||
|
||||
if not file_obj:
|
||||
file_obj = SimpleUploadedFile(
|
||||
"Ünicode Filename 🙂.jpg",
|
||||
file_content,
|
||||
content_type="image/jpeg",
|
||||
)
|
||||
return self.client.post(
|
||||
add_url, {"attachment_file": file_obj}, follow=True,
|
||||
**extra
|
||||
)
|
||||
19
django/attachments/tests/test_integrity.py
Normal file
19
django/attachments/tests/test_integrity.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
from django.core.management import call_command
|
||||
from django.test import TestCase
|
||||
from six import StringIO
|
||||
|
||||
|
||||
class IntegrityTestCase(TestCase):
|
||||
"""
|
||||
Very basic tests around the app itself, not the code.
|
||||
"""
|
||||
|
||||
def test_no_pending_migrations(self):
|
||||
"""
|
||||
Make sure all model changes are reflected with Django migrations.
|
||||
"""
|
||||
output = StringIO()
|
||||
call_command(
|
||||
"makemigrations", "--dry-run", interactive=False, stdout=output
|
||||
)
|
||||
self.assertTrue("No changes detected" in output.getvalue())
|
||||
69
django/attachments/tests/test_template.py
Normal file
69
django/attachments/tests/test_template.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
from django.urls import reverse
|
||||
|
||||
from ..models import Attachment
|
||||
from .base import BaseTestCase
|
||||
|
||||
|
||||
class ViewTestCase(BaseTestCase):
|
||||
def setUp(self):
|
||||
super(ViewTestCase, self).setUp()
|
||||
self.item_url = reverse("testapp-detail", kwargs={"pk": self.obj.pk})
|
||||
|
||||
def test_uploaded_attachment_urls_are_listed(self):
|
||||
self.client.login(**self.cred_jon)
|
||||
self._upload_testfile()
|
||||
response = self.client.get(self.item_url)
|
||||
attachment = Attachment.objects.attachments_for_object(self.obj)[0]
|
||||
self.assertTrue(attachment.attachment_file.url in str(response.content))
|
||||
|
||||
def test_attachment_count_is_listed(self):
|
||||
self.client.login(**self.cred_jon)
|
||||
self._upload_testfile()
|
||||
self._upload_testfile()
|
||||
response = self.client.get(self.item_url)
|
||||
attachment_count = Attachment.objects.attachments_for_object(
|
||||
self.obj
|
||||
).count()
|
||||
self.assertTrue(
|
||||
"Object has %d attachments" % attachment_count
|
||||
in str(response.content)
|
||||
)
|
||||
|
||||
def test_upload_form_is_listed_with_add_permission(self):
|
||||
self.client.login(**self.cred_jon)
|
||||
response = self.client.get(self.item_url)
|
||||
self.assertTrue("<form" in str(response.content))
|
||||
|
||||
def test_upload_form_is_not_listed_without_add_permission(self):
|
||||
self.jon.user_permissions.remove(self.add_permission)
|
||||
self.client.login(**self.cred_jon)
|
||||
response = self.client.get(self.item_url)
|
||||
self.assertFalse("<form" in str(response.content))
|
||||
|
||||
def test_delete_link_is_listed_with_delete_permission(self):
|
||||
self.client.login(**self.cred_jon)
|
||||
self._upload_testfile()
|
||||
response = self.client.get(self.item_url)
|
||||
self.assertTrue("delete-attachment" in str(response.content))
|
||||
|
||||
def test_delete_link_is_not_listed_without_delete_permission(self):
|
||||
self.jon.user_permissions.remove(self.del_permission)
|
||||
self.client.login(**self.cred_jon)
|
||||
self._upload_testfile()
|
||||
response = self.client.get(self.item_url)
|
||||
self.assertFalse("delete-attachment" in str(response.content))
|
||||
|
||||
def test_delete_link_is_listed_with_foreign_delete_permission(self):
|
||||
self.jon.user_permissions.add(self.del_foreign_permission)
|
||||
self.client.login(**self.cred_jane)
|
||||
self._upload_testfile()
|
||||
self.client.login(**self.cred_jon)
|
||||
response = self.client.get(self.item_url)
|
||||
self.assertTrue("delete-attachment" in str(response.content))
|
||||
|
||||
def test_delete_link_is_not_listed_for_others_attachments(self):
|
||||
self.client.login(**self.cred_jane)
|
||||
self._upload_testfile()
|
||||
self.client.login(**self.cred_jon)
|
||||
response = self.client.get(self.item_url)
|
||||
self.assertFalse("delete-attachment" in str(response.content))
|
||||
273
django/attachments/tests/test_views.py
Normal file
273
django/attachments/tests/test_views.py
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
import os
|
||||
import json
|
||||
import mock
|
||||
|
||||
from http import HTTPStatus
|
||||
from django.urls import reverse
|
||||
|
||||
from ..models import Attachment
|
||||
from .base import BaseTestCase
|
||||
from .testapp.models import ModelWithUuidPk
|
||||
|
||||
|
||||
class ViewTestCase(BaseTestCase):
|
||||
def test_empty_post_to_form_wont_create_attachment(self):
|
||||
add_url = reverse(
|
||||
"attachments:add",
|
||||
kwargs={
|
||||
"app_label": "testapp",
|
||||
"model_name": "testmodel",
|
||||
"pk": self.obj.pk,
|
||||
},
|
||||
)
|
||||
response = self.client.post(add_url)
|
||||
self.assertEqual(302, response.status_code)
|
||||
self.assertEqual(Attachment.objects.count(), 0)
|
||||
self.assertEqual(
|
||||
Attachment.objects.attachments_for_object(self.obj).count(), 0
|
||||
)
|
||||
|
||||
def test_invalid_model_wont_fail(self):
|
||||
add_url = reverse(
|
||||
"attachments:add",
|
||||
kwargs={
|
||||
"app_label": "thisdoes",
|
||||
"model_name": "notexist",
|
||||
"pk": self.obj.pk,
|
||||
},
|
||||
)
|
||||
response = self.client.post(add_url)
|
||||
self.assertEqual(302, response.status_code)
|
||||
self.assertEqual(Attachment.objects.count(), 0)
|
||||
self.assertEqual(
|
||||
Attachment.objects.attachments_for_object(self.obj).count(), 0
|
||||
)
|
||||
|
||||
def test_invalid_attachment_wont_fail(self):
|
||||
self.client.login(**self.cred_jon)
|
||||
self._upload_testfile(file_obj="Not a UploadedFile object")
|
||||
self.assertEqual(Attachment.objects.count(), 0)
|
||||
self.assertEqual(
|
||||
Attachment.objects.attachments_for_object(self.obj).count(), 0
|
||||
)
|
||||
|
||||
def test_upload_size_less_than_limit(self):
|
||||
# NOTE: in all of the other tests there's no limit specified
|
||||
# so they will cover the branch where the setting is missing
|
||||
with self.settings(FILE_UPLOAD_MAX_SIZE=1024):
|
||||
self.client.login(**self.cred_jon)
|
||||
self._upload_testfile()
|
||||
self.assertEqual(Attachment.objects.count(), 1)
|
||||
self.assertEqual(
|
||||
Attachment.objects.attachments_for_object(self.obj).count(), 1
|
||||
)
|
||||
|
||||
def test_upload_size_more_than_limit(self):
|
||||
# we set a limit of 1 byte b/c the file used for testing
|
||||
# is very small
|
||||
with self.settings(FILE_UPLOAD_MAX_SIZE=1):
|
||||
self.client.login(**self.cred_jon)
|
||||
response = self._upload_testfile()
|
||||
self.assertContains(response, "File exceeds maximum size of 1")
|
||||
self.assertEqual(Attachment.objects.count(), 0)
|
||||
self.assertEqual(
|
||||
Attachment.objects.attachments_for_object(self.obj).count(), 0
|
||||
)
|
||||
|
||||
def test_upload_without_permission(self):
|
||||
"""
|
||||
Remove the 'add permission' and try to upload a file.
|
||||
"""
|
||||
self.jon.user_permissions.remove(self.add_permission)
|
||||
|
||||
self.client.login(**self.cred_jon)
|
||||
self._upload_testfile()
|
||||
self.assertEqual(Attachment.objects.count(), 0)
|
||||
self.assertEqual(
|
||||
Attachment.objects.attachments_for_object(self.obj).count(), 0
|
||||
)
|
||||
|
||||
def test_upload_with_permission(self):
|
||||
self.client.login(**self.cred_jon)
|
||||
self._upload_testfile()
|
||||
self.assertEqual(Attachment.objects.count(), 1)
|
||||
self.assertEqual(
|
||||
Attachment.objects.attachments_for_object(self.obj).count(), 1
|
||||
)
|
||||
|
||||
def test_unauthed_user_cant_delete_attachment(self):
|
||||
self.client.login(**self.cred_jon)
|
||||
self._upload_testfile()
|
||||
att = Attachment.objects.first()
|
||||
del_url = reverse(
|
||||
"attachments:delete", kwargs={"attachment_pk": att.pk}
|
||||
)
|
||||
self.client.logout()
|
||||
self.client.get(del_url, follow=True)
|
||||
self.assertEqual(Attachment.objects.count(), 1)
|
||||
self.assertEqual(
|
||||
Attachment.objects.attachments_for_object(self.obj).count(), 1
|
||||
)
|
||||
|
||||
def test_author_can_delete_attachment(self):
|
||||
self.client.login(**self.cred_jon)
|
||||
self._upload_testfile()
|
||||
att = Attachment.objects.first()
|
||||
file_path = att.attachment_file.path
|
||||
del_url = reverse(
|
||||
"attachments:delete", kwargs={"attachment_pk": att.pk}
|
||||
)
|
||||
self.client.get(del_url, follow=True)
|
||||
self.assertEqual(Attachment.objects.count(), 0)
|
||||
# file on disk is still present b/c setting not specified
|
||||
self.assertTrue(os.path.exists(file_path))
|
||||
|
||||
def test_author_cant_delete_attachment_if_no_delete_permission(self):
|
||||
self.jon.user_permissions.remove(self.del_permission)
|
||||
|
||||
self.client.login(**self.cred_jon)
|
||||
self._upload_testfile()
|
||||
att = Attachment.objects.first()
|
||||
del_url = reverse(
|
||||
"attachments:delete", kwargs={"attachment_pk": att.pk}
|
||||
)
|
||||
self.client.get(del_url, follow=True)
|
||||
self.assertEqual(Attachment.objects.count(), 1)
|
||||
|
||||
def test_author_cant_delete_others_attachment(self):
|
||||
self.client.login(**self.cred_jon)
|
||||
self._upload_testfile()
|
||||
obj1 = Attachment.objects.order_by("-created")[0]
|
||||
|
||||
self.client.login(**self.cred_jane)
|
||||
self._upload_testfile()
|
||||
obj2 = Attachment.objects.order_by("-created")[0]
|
||||
|
||||
self.assertNotEqual(obj1, obj2)
|
||||
|
||||
# Jon can't delete Janes attachment
|
||||
self.client.login(**self.cred_jon)
|
||||
del_url = reverse(
|
||||
"attachments:delete", kwargs={"attachment_pk": obj2.pk}
|
||||
)
|
||||
self.client.get(del_url, follow=True)
|
||||
|
||||
self.assertEqual(Attachment.objects.count(), 2)
|
||||
|
||||
def test_author_can_delete_others_attachment_with_permission(self):
|
||||
self.client.login(**self.cred_jon)
|
||||
self._upload_testfile()
|
||||
obj1 = Attachment.objects.order_by("-created")[0]
|
||||
path1 = obj1.attachment_file.path
|
||||
|
||||
self.client.login(**self.cred_jane)
|
||||
self._upload_testfile()
|
||||
obj2 = Attachment.objects.order_by("-created")[0]
|
||||
path2 = obj2.attachment_file.path
|
||||
|
||||
self.assertNotEqual(obj1, obj2)
|
||||
|
||||
# Jon has the `delete_foreign_attachments` permission so he can
|
||||
# delete Janes attachment
|
||||
self.jon.user_permissions.add(self.del_foreign_permission)
|
||||
self.client.login(**self.cred_jon)
|
||||
|
||||
# explicitly set the delete setting to False to
|
||||
# cover that branch as well
|
||||
with self.settings(DELETE_ATTACHMENT_FILE=False):
|
||||
del_url = reverse(
|
||||
"attachments:delete", kwargs={"attachment_pk": obj2.pk}
|
||||
)
|
||||
self.client.get(del_url, follow=True)
|
||||
|
||||
self.assertEqual(Attachment.objects.count(), 1)
|
||||
self.assertTrue(os.path.exists(path1))
|
||||
self.assertTrue(os.path.exists(path2))
|
||||
|
||||
def test_delete_removes_files_from_disk_if_settings(self):
|
||||
self.client.login(**self.cred_jon)
|
||||
self._upload_testfile()
|
||||
att = Attachment.objects.first()
|
||||
file_path = att.attachment_file.path
|
||||
with self.settings(DELETE_ATTACHMENTS_FROM_DISK=True):
|
||||
del_url = reverse(
|
||||
"attachments:delete", kwargs={"attachment_pk": att.pk}
|
||||
)
|
||||
self.client.get(del_url, follow=True)
|
||||
self.assertEqual(Attachment.objects.count(), 0)
|
||||
self.assertFalse(os.path.exists(file_path))
|
||||
|
||||
def test_delete_does_not_raise_if_settings_and_file_missing(self):
|
||||
self.client.login(**self.cred_jon)
|
||||
self._upload_testfile()
|
||||
att = Attachment.objects.first()
|
||||
file_path = att.attachment_file.path
|
||||
# remove the file before hand
|
||||
os.remove(file_path)
|
||||
with self.settings(DELETE_ATTACHMENTS_FROM_DISK=True):
|
||||
del_url = reverse(
|
||||
"attachments:delete", kwargs={"attachment_pk": att.pk}
|
||||
)
|
||||
self.client.get(del_url, follow=True)
|
||||
self.assertEqual(Attachment.objects.count(), 0)
|
||||
self.assertFalse(os.path.exists(file_path))
|
||||
|
||||
def test_delete_does_not_raise_if_os_remove_raises(self):
|
||||
self.client.login(**self.cred_jon)
|
||||
self._upload_testfile()
|
||||
att = Attachment.objects.first()
|
||||
|
||||
with mock.patch("attachments.views.os.remove") as _mock:
|
||||
_mock.side_effect = OSError("Test file does not exist")
|
||||
with self.settings(DELETE_ATTACHMENTS_FROM_DISK=True):
|
||||
del_url = reverse(
|
||||
"attachments:delete", kwargs={"attachment_pk": att.pk}
|
||||
)
|
||||
self.client.get(del_url, follow=True)
|
||||
self.assertEqual(Attachment.objects.count(), 0)
|
||||
# NOTE: we don't assert the file path here because
|
||||
# the mock which raises will not actually delete it
|
||||
|
||||
|
||||
class UUIDTestCase(BaseTestCase):
|
||||
target_model_class = ModelWithUuidPk
|
||||
|
||||
def test_upload_with_permission(self):
|
||||
self.client.login(**self.cred_jon)
|
||||
self._upload_testfile()
|
||||
self.assertEqual(Attachment.objects.count(), 1)
|
||||
self.assertEqual(
|
||||
Attachment.objects.attachments_for_object(self.obj).count(), 1
|
||||
)
|
||||
|
||||
|
||||
class CustomValidatorsTestCase(BaseTestCase):
|
||||
def test_deny_specific_content(self):
|
||||
self.client.login(**self.cred_jon)
|
||||
response = self._upload_testfile(file_content=b"<xml>this is not allowed</xml>")
|
||||
|
||||
self.assertContains(response, "XML is forbidden")
|
||||
self.assertEqual(Attachment.objects.count(), 0)
|
||||
self.assertEqual(
|
||||
Attachment.objects.attachments_for_object(self.obj).count(), 0
|
||||
)
|
||||
|
||||
def test_form_errors_are_returned_as_json(self):
|
||||
self.client.login(**self.cred_jon)
|
||||
response = self._upload_testfile(
|
||||
file_content=b"<xml>this is not allowed</xml>",
|
||||
HTTP_X_RETURN_FORM_ERRORS=True,
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST)
|
||||
self.assertEqual(response.headers.get("Content-Type"), "application/json")
|
||||
|
||||
# this should be a dict
|
||||
errors = json.loads(response.content)
|
||||
# note: field errors are a list of string messages
|
||||
self.assertEqual(errors["attachment_file"], ["XML is forbidden"])
|
||||
|
||||
self.assertEqual(Attachment.objects.count(), 0)
|
||||
self.assertEqual(
|
||||
Attachment.objects.attachments_for_object(self.obj).count(), 0
|
||||
)
|
||||
0
django/attachments/tests/testapp/__init__.py
Normal file
0
django/attachments/tests/testapp/__init__.py
Normal file
11
django/attachments/tests/testapp/admin.py
Normal file
11
django/attachments/tests/testapp/admin.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from ...admin import AttachmentInlines
|
||||
from .models import TestModel
|
||||
|
||||
|
||||
class TestModelAdmin(admin.ModelAdmin):
|
||||
inlines = [AttachmentInlines]
|
||||
|
||||
|
||||
admin.site.register(TestModel, TestModelAdmin)
|
||||
19
django/attachments/tests/testapp/apps.py
Normal file
19
django/attachments/tests/testapp/apps.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
"""
|
||||
Custom app config to demonstrate and test the 'custom attachment validators'
|
||||
functionality.
|
||||
"""
|
||||
|
||||
from attachments.apps import AttachmentsConfig
|
||||
from django.forms import ValidationError
|
||||
|
||||
|
||||
def deny_xml_uploads(uploaded_file):
|
||||
if uploaded_file.read().find(b"<xml>") > -1:
|
||||
raise ValidationError("XML is forbidden")
|
||||
|
||||
|
||||
class CustomizedAttachmentsApp(AttachmentsConfig):
|
||||
"""
|
||||
Adds a custom form validator function.
|
||||
"""
|
||||
attachment_validators = (deny_xml_uploads,)
|
||||
16
django/attachments/tests/testapp/migrations/0001_initial.py
Normal file
16
django/attachments/tests/testapp/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='TestModel',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('title', models.CharField(max_length=100)),
|
||||
],
|
||||
bases=(models.Model,),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2018-01-04 12:47
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('testapp', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelTable(
|
||||
name='testmodel',
|
||||
table='testapp_testmodel',
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 3.2.7 on 2023-03-11 11:15
|
||||
|
||||
from django.db import migrations, models
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('testapp', '0002_auto_20180104_1247'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ModelWithUuidPk',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('title', models.CharField(max_length=100)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'testapp_uuid4_model',
|
||||
},
|
||||
),
|
||||
]
|
||||
0
django/attachments/tests/testapp/migrations/__init__.py
Normal file
0
django/attachments/tests/testapp/migrations/__init__.py
Normal file
23
django/attachments/tests/testapp/models.py
Normal file
23
django/attachments/tests/testapp/models.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import uuid
|
||||
from django.db import models
|
||||
|
||||
|
||||
class TestModel(models.Model):
|
||||
title = models.CharField(max_length=100)
|
||||
|
||||
class Meta:
|
||||
db_table = "testapp_testmodel"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return "/"
|
||||
|
||||
|
||||
class ModelWithUuidPk(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
title = models.CharField(max_length=100)
|
||||
|
||||
class Meta:
|
||||
db_table = "testapp_uuid4_model"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return "/"
|
||||
80
django/attachments/tests/testapp/settings.py
Normal file
80
django/attachments/tests/testapp/settings.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import os
|
||||
|
||||
DEBUG = True
|
||||
|
||||
TESTAPP_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
SECRET_KEY = "testsecretkey"
|
||||
|
||||
if os.environ.get("DJANGO_DATABASE_ENGINE") == "postgresql":
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"USER": "postgres",
|
||||
"NAME": "attachments",
|
||||
"HOST": "localhost",
|
||||
"PORT": 5432,
|
||||
}
|
||||
}
|
||||
elif os.environ.get("DJANGO_DATABASE_ENGINE") == "mysql":
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.mysql",
|
||||
"USER": "root",
|
||||
"NAME": "attachments",
|
||||
"HOST": "127.0.0.1",
|
||||
"PORT": 3306,
|
||||
}
|
||||
}
|
||||
else:
|
||||
DATABASES = {
|
||||
"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": "tests.db"}
|
||||
}
|
||||
|
||||
DATABASES["default"].update(
|
||||
{
|
||||
"PASSWORD": os.environ.get("DATABASE_PASSWORD", "testing"),
|
||||
}
|
||||
)
|
||||
|
||||
MEDIA_ROOT = os.path.join(TESTAPP_DIR, "uploads")
|
||||
ROOT_URLCONF = "attachments.tests.testapp.urls"
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"attachments.tests.testapp",
|
||||
"attachments.tests.testapp.apps.CustomizedAttachmentsApp",
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
]
|
||||
|
||||
MIDDLEWARE = (
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
)
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
)
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [os.path.join(TESTAPP_DIR, "templates")],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.request",
|
||||
"django.template.context_processors.i18n",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
]
|
||||
},
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
{% load attachments_tags %}
|
||||
|
||||
<h1>{{ object.title }}</h1>
|
||||
<h2>Object has {% attachments_count object %} attachments</h2>
|
||||
|
||||
{% get_attachments_for object as attachments_list %}
|
||||
{% for att in attachments_list %}
|
||||
<p>
|
||||
{{ att }}
|
||||
{{ att.attachment_file.url }}
|
||||
{{ att.filename }}
|
||||
{% attachment_delete_link att %}
|
||||
</p>
|
||||
{% endfor %}
|
||||
|
||||
{% attachment_form object %}
|
||||
25
django/attachments/tests/testapp/urls.py
Normal file
25
django/attachments/tests/testapp/urls.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
try:
|
||||
from django.urls import re_path as url
|
||||
except ImportError:
|
||||
from django.conf.urls import url
|
||||
|
||||
from django.conf.urls import include
|
||||
from django.contrib import admin
|
||||
from django.views.generic import DetailView
|
||||
|
||||
from .models import TestModel
|
||||
|
||||
admin.autodiscover()
|
||||
|
||||
urlpatterns = [
|
||||
url(r"^attachments/", include("attachments.urls", namespace="attachments")),
|
||||
url(r"^admin/", admin.site.urls),
|
||||
url(
|
||||
r"^testapp/(?P<pk>\d+)/$",
|
||||
DetailView.as_view(
|
||||
template_name="testmodel_detail.html",
|
||||
queryset=TestModel.objects.all(),
|
||||
),
|
||||
name="testapp-detail",
|
||||
),
|
||||
]
|
||||
24
django/attachments/urls.py
Normal file
24
django/attachments/urls.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
try:
|
||||
from django.urls import re_path as url
|
||||
except ImportError:
|
||||
from django.conf.urls import url
|
||||
|
||||
from .views import add_attachment, delete_attachment
|
||||
|
||||
app_name = "attachments"
|
||||
|
||||
urlpatterns = [
|
||||
url(
|
||||
r"^add-for/(?P<app_label>[\w\-]+)/(?P<model_name>[\w\-]+)/(?P<pk>\d+)/$",
|
||||
add_attachment,
|
||||
name="add",
|
||||
),
|
||||
url(
|
||||
r"^add-for/(?P<app_label>[\w\-]+)/(?P<model_name>[\w\-]+)/(?P<pk>[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12})/$",
|
||||
add_attachment,
|
||||
name="add",
|
||||
),
|
||||
url(r"^delete/(?P<attachment_pk>\d+)/$", delete_attachment, name="delete"),
|
||||
]
|
||||
89
django/attachments/views.py
Normal file
89
django/attachments/views.py
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
|
||||
from http import HTTPStatus
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpResponseRedirect, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext
|
||||
from django.views.decorators.http import require_POST
|
||||
|
||||
from .forms import AttachmentForm
|
||||
from .models import Attachment
|
||||
|
||||
|
||||
def add_url_for_obj(obj):
|
||||
return reverse(
|
||||
"attachments:add",
|
||||
kwargs={
|
||||
"app_label": obj._meta.app_label,
|
||||
"model_name": obj._meta.model_name,
|
||||
"pk": obj.pk,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def remove_file_from_disk(f):
|
||||
if getattr(
|
||||
settings, "DELETE_ATTACHMENTS_FROM_DISK", False
|
||||
) and os.path.exists(f.path):
|
||||
try:
|
||||
os.remove(f.path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
def add_attachment(
|
||||
request,
|
||||
app_label,
|
||||
model_name,
|
||||
pk,
|
||||
template_name="attachments/add.html",
|
||||
extra_context=None,
|
||||
):
|
||||
next_ = request.POST.get("next", "/")
|
||||
|
||||
if not request.user.has_perm("attachments.add_attachment"):
|
||||
return HttpResponseRedirect(next_)
|
||||
|
||||
model = apps.get_model(app_label, model_name)
|
||||
obj = get_object_or_404(model, pk=pk)
|
||||
obj = obj.get_label_attachment() # our label to attachment proxy
|
||||
form = AttachmentForm(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
form.save(request, obj)
|
||||
messages.success(request, gettext("Your attachment was uploaded."))
|
||||
return HttpResponseRedirect(next_)
|
||||
|
||||
if request.headers.get("X-Return-Form-Errors", None):
|
||||
return JsonResponse(form.errors, status=HTTPStatus.BAD_REQUEST)
|
||||
|
||||
template_context = {
|
||||
"form": form,
|
||||
"form_url": add_url_for_obj(obj),
|
||||
"next": next_,
|
||||
}
|
||||
template_context.update(extra_context or {})
|
||||
|
||||
return render(request, template_name, template_context)
|
||||
|
||||
|
||||
@login_required
|
||||
def delete_attachment(request, attachment_pk):
|
||||
g = get_object_or_404(Attachment, pk=attachment_pk)
|
||||
if (
|
||||
request.user.has_perm("attachments.delete_attachment")
|
||||
and request.user == g.creator
|
||||
) or request.user.has_perm("attachments.delete_foreign_attachments"):
|
||||
remove_file_from_disk(g.attachment_file)
|
||||
g.delete()
|
||||
messages.success(request, gettext("Your attachment was deleted."))
|
||||
next_ = request.GET.get("next") or "/"
|
||||
return HttpResponseRedirect(next_)
|
||||
|
|
@ -10,8 +10,7 @@ except Exception:
|
|||
|
||||
|
||||
class AppSettings(object):
|
||||
"""
|
||||
"""
|
||||
""" """
|
||||
@property
|
||||
def MAX_ATTEMPTS(self):
|
||||
"""Control how many times a task will be attempted."""
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
Copy-and-pasted at commit https://github.com/coinkite/connectrum/commit/c893bc2100de6acebbdf0bf67b62e6cb9ce1c7be and extended for Labelbase.
|
||||
Friendly-forked at commit https://github.com/coinkite/connectrum/commit/c893bc2100de6acebbdf0bf67b62e6cb9ce1c7be and extended for Labelbase.
|
||||
|
||||
MIT Licence https://github.com/coinkite/connectrum/blob/master/LICENSE
|
||||
|
|
|
|||
|
|
@ -24,11 +24,13 @@ async def interact(conn, server_info, method, utxo):
|
|||
except Exception as ex:
|
||||
blocktime = 0
|
||||
logger.error("Can't get blocktime: {}".format(ex))
|
||||
utxo = txn.get('vout')[int(index)]
|
||||
address = txn.get('vout')[int(index)].get('scriptPubKey', {}).get('address')
|
||||
value = txn.get('vout')[int(index)].get('value')*100000000
|
||||
return (txid, index, address, value, blocktime)
|
||||
return (txid, index, address, value, blocktime, utxo)
|
||||
except ElectrumErrorResponse as ex:
|
||||
logger.error("ERROR: {} {}".format(ex, conn.last_error))
|
||||
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
|
@ -70,20 +72,33 @@ def checkup_label(label_id, loop):
|
|||
|
||||
if elem.type == "output" and is_valid_output_ref(elem.ref) and \
|
||||
(output.spent is not True or output.confirmed_at_block_time == 0):
|
||||
electrum_hostname = elem.labelbase.user.profile.electrum_hostname or "electrum.emzy.de"
|
||||
electrum_ports = elem.labelbase.user.profile.electrum_ports or "s50002"
|
||||
if elem.labelbase.is_mainnet:
|
||||
electrum_hostname = elem.labelbase.user.profile.electrum_hostname or "electrum.emzy.de"
|
||||
electrum_ports = elem.labelbase.user.profile.electrum_ports or "s50002"
|
||||
elif elem.labelbase.is_testnet:
|
||||
electrum_hostname = elem.labelbase.user.profile.electrum_hostname_test or "testnet.qtornado.com"
|
||||
electrum_ports = elem.labelbase.user.profile.electrum_ports_test or "s51002"
|
||||
server_info = ServerInfo(electrum_hostname, electrum_hostname, ports=(electrum_ports))
|
||||
|
||||
conn = StratumClient()
|
||||
utxo = elem.ref
|
||||
utxo_data = {}
|
||||
utxo_resp = loop.run_until_complete(interact(conn, server_info, "blockchain.transaction.get", utxo))
|
||||
|
||||
if utxo_resp:
|
||||
txid, index, address, value, blocktime = utxo_resp
|
||||
txid, index, address, value, blocktime, utxo_data = utxo_resp
|
||||
if utxo_data:
|
||||
output.next_input_attributes = utxo_data
|
||||
if blocktime:
|
||||
HistoricalPrice.get_or_create_from_api(timestamp=blocktime)
|
||||
|
||||
unspents = loop.run_until_complete(interact_addr(conn, server_info, "blockchain.address.listunspent", address))
|
||||
try:
|
||||
unspents = loop.run_until_complete(interact_addr(conn, server_info, "blockchain.address.listunspent", address))
|
||||
except:
|
||||
conn.last_error = None # reset error if needed
|
||||
unspents = loop.run_until_complete(interact_addr(conn, server_info, "blockchain.scripthash.listunspent", address))
|
||||
|
||||
|
||||
|
||||
|
||||
utxo_value = 0
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 3.2.24 on 2024-03-29 17:38
|
||||
|
||||
from django.db import migrations
|
||||
import jsonfield.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('finances', '0009_alter_outputstat_last_error'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='outputstat',
|
||||
name='next_input_attributes',
|
||||
field=jsonfield.fields.JSONField(default={}),
|
||||
),
|
||||
]
|
||||
18
django/finances/migrations/0011_alter_outputstat_network.py
Normal file
18
django/finances/migrations/0011_alter_outputstat_network.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.2.25 on 2024-04-11 15:06
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('finances', '0010_outputstat_next_input_attributes'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='outputstat',
|
||||
name='network',
|
||||
field=models.CharField(choices=[('mainnet', 'Mainnet'), ('testnet', 'Testnet')], default='mainnet', help_text="Choose the network for this labelbase's label output.", max_length=10),
|
||||
),
|
||||
]
|
||||
|
|
@ -43,7 +43,7 @@ class OutputStat(models.Model):
|
|||
confirmed_at_block_time = models.IntegerField(default=0)
|
||||
|
||||
last_error = JSONField(default={})
|
||||
|
||||
next_input_attributes = JSONField(default={}) # will be used for fee estimation
|
||||
MAINNET = 'mainnet'
|
||||
TESTNET = 'testnet'
|
||||
|
||||
|
|
@ -56,7 +56,7 @@ class OutputStat(models.Model):
|
|||
max_length=10,
|
||||
choices=NETWORK_CHOICES,
|
||||
default='mainnet',
|
||||
help_text="Choose the network for this labelbase."
|
||||
help_text="Choose the network for this labelbase's label output."
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from pymempool import MempoolAPI
|
|||
from labellabor.utils import extract_fiat_value
|
||||
from labelbase.utils import compute_type_ref_hash
|
||||
from finances.models import OutputStat
|
||||
from attachments.models import LabelAttachment
|
||||
|
||||
class Labelbase(models.Model):
|
||||
"""
|
||||
|
|
@ -189,6 +190,14 @@ class Label(models.Model):
|
|||
val, cur = extract_fiat_value(self.label)
|
||||
return output.output_metrics_dict(tracked_fiat_value=val, fiat_currency=cur)
|
||||
|
||||
def get_label_attachment(self):
|
||||
type_ref_hash = compute_type_ref_hash(self.type, self.ref)
|
||||
label_attachment, _ = LabelAttachment.objects.get_or_create(
|
||||
user=self.labelbase.user,
|
||||
type_ref_hash=type_ref_hash,
|
||||
network=self.labelbase.network)
|
||||
return label_attachment
|
||||
|
||||
def get_absolute_url(self):
|
||||
"""
|
||||
Is used by "edit label" functionality.
|
||||
|
|
|
|||
BIN
django/labelbase/static/bitcoin.pdf
Normal file
BIN
django/labelbase/static/bitcoin.pdf
Normal file
Binary file not shown.
BIN
django/labelbase/static/ogimg.png
Normal file
BIN
django/labelbase/static/ogimg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
|
|
@ -1,11 +1,17 @@
|
|||
from django import template
|
||||
from django.template import Library, Node, VariableDoesNotExist
|
||||
from django.conf import settings
|
||||
|
||||
from labelbase.forms import LabelbaseForm
|
||||
from labelbase.forms import ExportLabelsForm
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def is_self_hosted():
|
||||
return settings.SELF_HOSTED
|
||||
|
||||
@register.simple_tag
|
||||
def labelbaseform():
|
||||
return LabelbaseForm()
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ CRYPTOGRAPHY_SALT = proj_config.get("internal", "crypto_salt")
|
|||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True # proj_config.getboolean("internal", "debug")
|
||||
|
||||
SELF_HOSTED = proj_config.getboolean("internal", "self_hosted", fallback=True)
|
||||
#if DEBUG:
|
||||
ALLOWED_HOSTS = ["*"] # we don't know your host config, keep like that at the moment.
|
||||
#else:
|
||||
|
|
@ -54,7 +55,7 @@ sentry_sdk.init(
|
|||
traces_sample_rate=1.0,
|
||||
send_default_pii=True, # must be "True" here, will skip or omit in `before_send` callback
|
||||
)
|
||||
sentry_sdk.set_tag("version", "2.0.0")
|
||||
sentry_sdk.set_tag("version", "2.1.0")
|
||||
|
||||
|
||||
LOGGING = {
|
||||
|
|
@ -117,6 +118,7 @@ INSTALLED_APPS = [
|
|||
"knowledge_base",
|
||||
"hashtags",
|
||||
"statusapp",
|
||||
"attachments",
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -272,6 +272,10 @@ urlpatterns = [
|
|||
"",
|
||||
include("user_sessions.urls", "user_sessions")
|
||||
),
|
||||
path(
|
||||
"attachments/",
|
||||
include('attachments.urls', namespace='attachments')
|
||||
),
|
||||
path(
|
||||
"has_seen_welcome_popup/",
|
||||
has_seen_welcome_popup,
|
||||
|
|
|
|||
|
|
@ -572,6 +572,9 @@ class LabelUpdateView(UpdateView):
|
|||
action = self.kwargs['action']
|
||||
if action == 'labeling':
|
||||
return "label_edit_labeling.html"
|
||||
if action == 'attachments' and settings.SELF_HOSTED and \
|
||||
self.object.labelbase.user.profile.use_attachments:
|
||||
return "label_edit_attachments.html"
|
||||
elif action == 'output-details':
|
||||
return "label_edit_output_details.html"
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,21 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://labelbase.space/" />
|
||||
<meta property="og:title" content="Labelbase" />
|
||||
<meta property="og:description" content="All-in-one coin control/label management service for Bitcoin. Add, import, export labels. REST API for integration. Supports BIP-329 format. Stay organized!" />
|
||||
<meta property="og:image" content="https://labelbase.space{% static 'ogimg.png' %}" />
|
||||
|
||||
<!-- twitter card tags additive with the og: tags -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:domain" value="labelbase.space" />
|
||||
<meta name="twitter:title" value="Labelbase" />
|
||||
<meta name="twitter:description" value="All-in-one coin control/label management service for Bitcoin. Add, import, export labels. REST API for integration. Supports BIP-329 format. Stay organized!" />
|
||||
<meta name="twitter:image" content="https://labelbase.space{% static 'ogimg.png' %}" />
|
||||
<meta name="twitter:url" value="https://labelbase.space/" />
|
||||
|
||||
<title>{% block title %}{% endblock %} | Labelbase</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<link href="{% static 'css/bootstrap.min.css' %}" rel="stylesheet"
|
||||
|
|
@ -116,17 +130,19 @@
|
|||
{% if labelbase.id == active_labelbase_id %}
|
||||
<ul style="list-style-type: none;">
|
||||
<li><a class="nav-link" href="{{ labelbase.get_absolute_url }}" style="font-size: .875rem;"><span data-feather="tag" class="align-text-bottom"></span> Labels</a></li>
|
||||
<li><a class="nav-link" style="font-size: .875rem;" href="{% url "labelbase_fix_and_manage" labelbase_id=labelbase.id %}"><span data-feather="git-merge" class="align-text-bottom"></span> Fix & manage</a></li>
|
||||
<li><a class="nav-link" style="font-size: .875rem;" data-bs-toggle="modal" data-bs-target="#importLabelbaseModal"><span data-feather="upload-cloud" class="align-text-bottom"></span> Import</a></li>
|
||||
<li><a class="nav-link" style="font-size: .875rem;" data-bs-toggle="modal" data-bs-target="#exportLabelbaseModal"><span data-feather="download-cloud" class="align-text-bottom"></span> Export</a></li>
|
||||
<li><a class="nav-link" style="font-size: .875rem;" href="{% url "labelbase_fix_and_manage" labelbase_id=labelbase.id %}"><span data-feather="git-merge" class="align-text-bottom"></span> Fix & Manage</a></li>
|
||||
<li><a class="nav-link" style="font-size: .875rem;" href="{% url "labelbase_fix_and_manage" labelbase_id=labelbase.id %}"><span data-feather="activity" class="align-text-bottom"></span>UTXOs Health</a></li>
|
||||
<li><a class="nav-link" style="font-size: .875rem;" href="{% url "labelbase_tree_maps" pk=labelbase.id %}unspent-outputs/"><span data-feather="grid" class="align-text-bottom"></span> Tree Map</a></li>
|
||||
{% comment %}<li><a class="nav-link" style="font-size: .875rem;" href="{% url "labelbase_stats_and_kpi" labelbase_id=labelbase.id %}"><span data-feather="pie-chart" class="align-text-bottom"></span> Stats & KPIs</a></li>{% endcomment %}
|
||||
{% if request.user.profile.use_fiatfinances %}
|
||||
<li><a class="nav-link" style="font-size: .875rem;"><span data-feather="dollar-sign" class="align-text-bottom"></span> Fiat Finances</a></li>
|
||||
{% endif %}
|
||||
<li><a class="nav-link" style="font-size: .875rem;" data-bs-toggle="modal" data-bs-target="#importLabelbaseModal"><span data-feather="upload-cloud" class="align-text-bottom"></span> Import</a></li>
|
||||
<li><a class="nav-link" style="font-size: .875rem;" data-bs-toggle="modal" data-bs-target="#exportLabelbaseModal"><span data-feather="download-cloud" class="align-text-bottom"></span> Export</a></li>
|
||||
{% if labelbase.user.profile.use_hashtags %}
|
||||
<li><a href="{{ labelbase.get_hashtags_url }}" class="nav-link" style="font-size: .875rem;"><span data-feather="hash" class="align-text-bottom"></span> Hashtags</a></li>
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@
|
|||
</ul>
|
||||
</div>
|
||||
<div class="my-2 text-center"><small>
|
||||
<a class="text-600 me-1 termslink" href="JavaScript:void(0);" onclick="window.location='{% url 'privacy_policy' %}';">Privacy Policy</a>•<a class="text-600 mx-1 termslink" href="JavaScript:void(0);" onclick="window.location='{% url 'terms' %}';">Terms</a>•<a class="text-600 mx-1 termslink">v2.0.0</a></small>
|
||||
<a class="text-600 me-1 termslink" href="JavaScript:void(0);" onclick="window.location='{% url 'privacy_policy' %}';">Privacy Policy</a>•<a class="text-600 mx-1 termslink" href="JavaScript:void(0);" onclick="window.location='{% url 'terms' %}';">Terms</a>•<a class="text-600 mx-1 termslink">v2.1.0</a></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
5
django/templates/attachments/add.html
Normal file
5
django/templates/attachments/add.html
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{% extends "attachments/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
{% include "attachments/add_form.html" %}
|
||||
{% endblock %}
|
||||
14
django/templates/attachments/add_form.html
Normal file
14
django/templates/attachments/add_form.html
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{% load i18n %}
|
||||
{% load bootstrap %}
|
||||
|
||||
{% if form %}
|
||||
<form method="post" enctype="multipart/form-data" action="{{ form_url }}" class="add-attachment">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="next" value="{{ next }}"/>
|
||||
{{ form|bootstrap }}
|
||||
<br>
|
||||
<input type="submit" class="btn btn-primary" value="{% trans "Add attachment" %}"/>
|
||||
|
||||
</form>
|
||||
|
||||
{% endif %}
|
||||
1
django/templates/attachments/base.html
Normal file
1
django/templates/attachments/base.html
Normal file
|
|
@ -0,0 +1 @@
|
|||
{% block content %}{% endblock %}
|
||||
2
django/templates/attachments/delete_link.html
Normal file
2
django/templates/attachments/delete_link.html
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
{% load i18n %}
|
||||
{% if delete_url %}<a class="delete-attachment" href="{{ delete_url }}?next={{ next }}">{% trans "Delete" %}</a>{% endif %}
|
||||
|
|
@ -10,35 +10,59 @@
|
|||
</div>
|
||||
|
||||
<p class="fs-6 text-muted">
|
||||
Update your Electrum server information.
|
||||
<br><br>
|
||||
<!-- Trusting Craig on this one. https://github.com/sparrowwallet/sparrow/blob/f003b6d732bd73d93fe904659e61e0d56f6cb280/src/main/java/com/sparrowwallet/sparrow/net/PublicElectrumServer.java -->
|
||||
If you don't run your own Electrum server, here are some community trusted mainnet servers: <br>
|
||||
<ul>
|
||||
<li> electrum.emzy.de / s50002</li>
|
||||
<li> electrum.blockstream.info / s50002</li>
|
||||
<li> <s>bitcoin.lu.ke / s50002</s></li>
|
||||
<!--li> umbrel.local / t50001</s> <sup>1</sup></li-->
|
||||
|
||||
</ul>
|
||||
<form action="" method="POST">
|
||||
{% csrf_token %}
|
||||
<table>
|
||||
{{ form|bootstrap }}
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" style="text-align: center;">
|
||||
<button class="btn btn-primary" type="submit">Update Electrum ServerInfo</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
|
||||
<!--hr>
|
||||
<small><sup>1</sup> Access your local Umbrel node, within your local network. </small-->
|
||||
|
||||
If you don't operate your own Electrum server, you have the option to connect to a community-trusted server.<br>
|
||||
However, keep in mind that using a public server allows it to view your outputs and transactions.
|
||||
</p>
|
||||
|
||||
|
||||
<div class="row" style="padding-top: 1.1rem;">
|
||||
<h5 class="text-muted">Community-trusted Electrum servers</h5>
|
||||
<div class="col-md-6">
|
||||
<p class="fs-6 text-muted">
|
||||
<strong>Mainnet: </strong>
|
||||
<ul>
|
||||
<li>electrum.emzy.de / s50002</li>
|
||||
<li>electrum.blockstream.info / s50002</li>
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<p class="fs-6 text-muted">
|
||||
<strong>Testnet:</strong>
|
||||
<ul>
|
||||
<li>testnet.qtornado.com / s51002</li>
|
||||
<li>testnet.aranguren.org / s51002</li>
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<form action="" method="POST">
|
||||
{% csrf_token %}
|
||||
<p class="fs-6 text-muted">
|
||||
<table>
|
||||
{{ form|bootstrap }}
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" style="text-align: center;">
|
||||
<button class="btn btn-primary" type="submit">Update Electrum Server</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="row" style="padding-top: 2.1rem;">
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,38 @@
|
|||
{% load i18n %}
|
||||
{% load sekizai_tags %}
|
||||
{% load labelbase_tags %}
|
||||
{% load backgroundtask_tags %}
|
||||
{% load attachments_tags %}
|
||||
|
||||
{% block title %}{{ object.type }} {{ object.ref }}{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% get_attachments_for object.get_label_attachment as my_attachments %}
|
||||
<ul class="nav nav-tabs" style="padding-top: 2rem; ">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if action == "update" %}active" aria-current="page" {% else %}"{% endif %} href="{% url 'edit_label' object.id %}">Label Detail</a>
|
||||
</li>
|
||||
|
||||
{% is_self_hosted as my_labelbase_is_self_hosted %}
|
||||
{% if object.labelbase.user.profile.use_attachments %}
|
||||
<li class="nav-item">
|
||||
{% if my_labelbase_is_self_hosted %}
|
||||
<a class="nav-link {% if action == "attachments" %}active " aria-current="page" {% else %}"{% endif %}
|
||||
href="{% url 'edit_label' object.id %}attachments/">
|
||||
Attachments
|
||||
{% if my_attachments.count %}
|
||||
<span class="badge text-bg-secondary top-0 rounded-pill " >{% attachments_count object.get_label_attachment %}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% else %}
|
||||
|
||||
<a class="nav-link disabled">Attachments</a>
|
||||
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% comment %}<!--
|
||||
|
||||
<li class="nav-item">
|
||||
|
|
@ -69,8 +93,17 @@
|
|||
|
||||
|
||||
|
||||
|
||||
{% endswitch %}
|
||||
|
||||
|
||||
|
||||
{% if object.ref == "54e48e5f5c656b26c3bca14a8c95aa583d07ebe84dde3b7dd4a78f4e4186e713" %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/static/bitcoin.pdf">Bitcoin White Paper</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
|
||||
<div style="padding-top: 0rem; padding-bottom: 2.1rem; ">
|
||||
|
|
@ -90,33 +123,58 @@
|
|||
<strong>Ouch!</strong> Got "{{ output.last_error.message }}{% if output.last_error.code %} (code {{ output.last_error.code }}){% endif %}" from Electrum.
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if object.type == "output" %}
|
||||
{% switch output.get_spent_status %}
|
||||
{% case "spent" %}
|
||||
<div class="bd-callout bd-callout-warning">
|
||||
<strong>Output spent!</strong> Blockchain records indicate that this output has been spent in another transaction.
|
||||
</div>
|
||||
{% case "unspent" %}
|
||||
<div class="bd-callout bd-callout-good">
|
||||
<strong>Output unspent!</strong> Blockchain records indicate that this output has not been spent yet.
|
||||
</div>
|
||||
{% case "unconfirmed" %}
|
||||
<div class="alert bd-callout bd-callout-info">
|
||||
<div class="btn-group" role="group" style="position: absolute; top: -8px; right: -4px; padding: 1.25rem 1rem;">
|
||||
<button type="button" class="btn btn-sm btn-outline-info dropdown-toggle " data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Actions
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="{% url 'outputstat_update_redirect' output_stats_id=output.id label_id=object.id %}?force-spent=none">
|
||||
Verify output status</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<strong>Output unconfirmed!</strong> Blockchain records indicate that this output has not been confirmed yet.
|
||||
</div>
|
||||
{% endswitch %}
|
||||
|
||||
{% is_label_id_in_queue object.id as is_in_queue %}
|
||||
{% if is_in_queue %}
|
||||
<div class="alert bd-callout bd-callout-info">
|
||||
<!--div class="btn-group" role="group" style="position: absolute; top: -8px; right: -4px; padding: 1.25rem 1rem;">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger dropdown-toggle " data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Actions
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="{% url 'outputstat_update_redirect' output_stats_id=output.id label_id=object.id %}?force-spent=none">Verify output status</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'outputstat_update_redirect' output_stats_id=output.id label_id=object.id %}?force-spent=true">Mark output as spent</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'outputstat_update_redirect' output_stats_id=output.id label_id=object.id %}?force-spent=false">Mark output as unspent</a></li>
|
||||
</ul>
|
||||
</div-->
|
||||
<strong>Output in queue!</strong> This output is currently in the processing queue. It will be checked shortly.
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
|
||||
{% if object.type == "output" %}
|
||||
{% switch output.get_spent_status %}
|
||||
{% case "spent" %}
|
||||
<div class="bd-callout bd-callout-warning">
|
||||
<strong>Output spent!</strong> Blockchain records indicate that this output has been spent in another transaction.
|
||||
</div>
|
||||
{% case "unspent" %}
|
||||
<div class="bd-callout bd-callout-good">
|
||||
<strong>Output unspent!</strong> Blockchain records indicate that this output has not been spent yet.
|
||||
</div>
|
||||
{% case "unconfirmed" %}
|
||||
<div class="alert bd-callout bd-callout-info">
|
||||
<div class="btn-group" role="group" style="position: absolute; top: -8px; right: -4px; padding: 1.25rem 1rem;">
|
||||
<button type="button" class="btn btn-sm btn-outline-info dropdown-toggle " data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Actions
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="{% url 'outputstat_update_redirect' output_stats_id=output.id label_id=object.id %}?force-spent=none">
|
||||
Verify output status</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<strong>Output unconfirmed!</strong> Blockchain records indicate that this output has not been confirmed yet.
|
||||
</div>
|
||||
{% endswitch %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
|
||||
<!--div class="bd-callout bd-callout-warning">
|
||||
<strong>Heads up!</strong> There are multiple records for this transaction output. <a href="">Review & merge</a>
|
||||
</div-->
|
||||
|
|
@ -128,6 +186,7 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
{% endcomment %}
|
||||
|
||||
{% block label_edit_content %}
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
|||
45
django/templates/label_edit_attachments.html
Normal file
45
django/templates/label_edit_attachments.html
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
{% extends "label_edit.html" %}
|
||||
{% load bootstrap %}
|
||||
{% load i18n %}
|
||||
{% load attachments_tags %}
|
||||
|
||||
|
||||
{% block label_edit_content %}
|
||||
|
||||
|
||||
|
||||
|
||||
{% if my_attachments %}
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col">Attachment</th>
|
||||
<th scope="col">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for attachment in my_attachments %}
|
||||
<tr>
|
||||
<td>{{ attachment.id }}</td>
|
||||
<td><a href="{{ attachment.attachment_file.url }}">{{ attachment.filename }}</a></td>
|
||||
<td>{% attachment_delete_link attachment %}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% else %}
|
||||
|
||||
<p><br>
|
||||
Label has {% attachments_count object.get_label_attachment %} attachments.
|
||||
</p>
|
||||
|
||||
{% endif %}
|
||||
<br>
|
||||
|
||||
{% attachment_form form.instance %}
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
<!--
|
||||
{{ res_tx|safe }}
|
||||
-->
|
||||
{% if action == "output-details" %}
|
||||
{% if action == "label-attachments" %}
|
||||
{% if form.instance.type == "output" %}
|
||||
<br>
|
||||
<pre>
|
||||
|
|
@ -16,8 +16,21 @@
|
|||
Confirmed at block height: {{ output.confirmed_at_block_height }}
|
||||
Confirmed at block time: {{ output.confirmed_at_block_time }}
|
||||
Network: {{ output.get_network_display }}
|
||||
{% if output.next_input_attributes %}
|
||||
Fee estimation will be made based on:
|
||||
|
||||
{{ output.next_input_attributes }}
|
||||
|
||||
{% if output.next_input_attributes.input_n %}
|
||||
* Assuming {{ output.next_input_attributes.input_m }}-of-{{ output.next_input_attributes.input_n }} multisig
|
||||
{% else %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
</pre>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "label_edit.html" %}
|
||||
{% load bootstrap %}
|
||||
{% load i18n %}
|
||||
|
||||
{% load attachments_tags %}
|
||||
{% block label_edit_content %}
|
||||
|
||||
<div class="lb-header">
|
||||
|
|
@ -33,4 +33,6 @@
|
|||
|
||||
</div><!-- lb-header container -->
|
||||
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
{% extends "_base.html" %}
|
||||
{% load i18n %}
|
||||
{% load sekizai_tags %}
|
||||
{% load labelbase_tags %}
|
||||
|
||||
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div >
|
||||
<div>
|
||||
{% include "profile_header_menu.html" %}
|
||||
|
||||
<div style="padding-top: 2rem; ">
|
||||
|
|
@ -11,7 +16,7 @@
|
|||
</div>
|
||||
|
||||
|
||||
|
||||
{% is_self_hosted as my_labelbase_is_self_hosted %}
|
||||
<div class="">
|
||||
|
||||
<h4>Extensions</h4>
|
||||
|
|
@ -35,12 +40,26 @@
|
|||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr >
|
||||
<tr>
|
||||
<td style="width:21%;"><b>Tree Map</b></td>
|
||||
<td style="width:75%;"><small>Enable or disable Tree Map.</small></td>
|
||||
<td>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input user-setting" type="checkbox" {% if request.user.profile.use_treemap %}checked="checked"{% endif %} id="use_treemap">
|
||||
<input class="form-check-input user-setting" type="checkbox" {% if request.user.profile.use_treemap %}checked="checked"{% endif %} id="use_treemap">
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr >
|
||||
<td style="width:21%;"><b>Attachments</b></td>
|
||||
<td style="width:75%;"><small>Enable or disable attachments. This extension is available only for self-hosted Labelbase instances.</small></td>
|
||||
<td>
|
||||
<div class="form-check form-switch">
|
||||
{% if my_labelbase_is_self_hosted %}
|
||||
<input class="form-check-input user-setting" type="checkbox" {% if request.user.profile.use_attachments %}checked="checked"{% endif %} id="use_attachments">
|
||||
{% else %}
|
||||
<input class="form-check-input " type="checkbox" id="use_attachments" disabled>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -81,17 +100,16 @@
|
|||
<td style="width:75%;">
|
||||
<small>Check and update all unspent outputs using <a href="/account/userprofile-electrum/">Electrum</a>.</small></td>
|
||||
<td>
|
||||
<td>
|
||||
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input user-setting" type="checkbox" {% if request.user.profile.update_utxo_on_login %}checked="checked"{% endif %} id="update_utxo_on_login">
|
||||
</div>
|
||||
</td>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -23,13 +23,18 @@ class MempoolForm(forms.ModelForm):
|
|||
class ElectrumServerInfoForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Profile
|
||||
fields = ['electrum_hostname', 'electrum_ports']
|
||||
fields = ['electrum_hostname', 'electrum_ports',
|
||||
'electrum_hostname_test', 'electrum_ports_test']
|
||||
labels = {
|
||||
'electrum_hostname': 'Electrum Hostname',
|
||||
'electrum_ports': 'Electrum Protocol + Port',
|
||||
'electrum_hostname': 'Electrum Hostname (Mainnet)',
|
||||
'electrum_ports': 'Electrum Protocol + Port (Mainnet)',
|
||||
'electrum_hostname_test': 'Electrum Hostname (Testnet)',
|
||||
'electrum_ports_test': 'Electrum Protocol + Port (Testnet)',
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
class ProfileCurrencyForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Profile
|
||||
|
|
|
|||
23
django/userprofile/migrations/0016_auto_20240329_2233.py
Normal file
23
django/userprofile/migrations/0016_auto_20240329_2233.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 3.2.24 on 2024-03-29 22:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('userprofile', '0015_alter_profile_use_sentry'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='electrum_hostname_test',
|
||||
field=models.CharField(blank=True, default='testnet.qtornado.com', max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='electrum_ports_test',
|
||||
field=models.CharField(blank=True, default='t51001', max_length=6),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.2.25 on 2024-03-29 23:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('userprofile', '0016_auto_20240329_2233'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='electrum_ports_test',
|
||||
field=models.CharField(blank=True, default='s51002', max_length=6),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.2.25 on 2024-04-13 22:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('userprofile', '0017_alter_profile_electrum_ports_test'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='use_attachments',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
||||
|
|
@ -26,6 +26,10 @@ class Profile(models.Model):
|
|||
electrum_hostname = models.CharField(max_length=255, default="electrum.emzy.de", blank=True)
|
||||
electrum_ports = models.CharField(max_length=6, default="s50002", blank=True)
|
||||
|
||||
# electrum testnet
|
||||
electrum_hostname_test = models.CharField(max_length=255, default="testnet.qtornado.com", blank=True)
|
||||
electrum_ports_test = models.CharField(max_length=6, default="s51002", blank=True)
|
||||
|
||||
avatar_url = models.CharField(max_length=160,
|
||||
default="/static/avatar.jpg",
|
||||
blank=True,
|
||||
|
|
@ -35,6 +39,7 @@ class Profile(models.Model):
|
|||
auto_cleanup = models.BooleanField(default=False)
|
||||
use_hashtags = models.BooleanField(default=True)
|
||||
use_treemap = models.BooleanField(default=True)
|
||||
use_attachments = models.BooleanField(default=True)
|
||||
use_sentry = models.BooleanField(default=True)
|
||||
use_chatwoot = models.BooleanField(default=False)
|
||||
use_fiatfinances = models.BooleanField(default=True)
|
||||
|
|
|
|||
|
|
@ -92,6 +92,10 @@ class ElectrumInfoUpdateView(UpdateView):
|
|||
profile.electrum_hostname = profile._meta.get_field('electrum_hostname').get_default()
|
||||
if not profile.electrum_ports:
|
||||
profile.electrum_ports = profile._meta.get_field('electrum_ports').get_default()
|
||||
if not profile.electrum_hostname_test:
|
||||
profile.electrum_hostname_test = profile._meta.get_field('electrum_hostname_test').get_default()
|
||||
if not profile.electrum_ports_test:
|
||||
profile.electrum_ports_test = profile._meta.get_field('electrum_ports_test').get_default()
|
||||
profile.save()
|
||||
messages.success(self.request, "<strong>Success!</strong> Electrum ServerInfo updated successfully.")
|
||||
return super().form_valid(form)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ http {
|
|||
upstream labelbase_django {
|
||||
server labelbase_django:8000;
|
||||
}
|
||||
|
||||
|
||||
server {
|
||||
listen 8080;
|
||||
client_max_body_size 69M;
|
||||
|
|
@ -46,6 +46,14 @@ http {
|
|||
break;
|
||||
}
|
||||
|
||||
location /attachments/attachment {
|
||||
autoindex off;
|
||||
index index.html;
|
||||
root /app;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue