diff --git a/.dockerignore b/.dockerignore index d7d4b6b..cd49c72 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,6 +7,7 @@ db/ *.pyc __pycache__ django/importer/uploadeddata/* +django/attachments/attachment/* _scratches exports.sh .last_git_commit diff --git a/.gitignore b/.gitignore index 0c41d79..b2c78f4 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ db/ *.pyc __pycache__ django/importer/uploadeddata/* +django/attachments/attachment/* #_scratches exports.sh .last_git_commit diff --git a/django/attachments/README.md b/django/attachments/README.md new file mode 100644 index 0000000..e9cd013 --- /dev/null +++ b/django/attachments/README.md @@ -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 diff --git a/django/attachments/__init__.py b/django/attachments/__init__.py new file mode 100644 index 0000000..0bb3635 --- /dev/null +++ b/django/attachments/__init__.py @@ -0,0 +1 @@ +default_app_config = "attachments.apps.AttachmentsConfig" diff --git a/django/attachments/admin.py b/django/attachments/admin.py new file mode 100644 index 0000000..9e9672d --- /dev/null +++ b/django/attachments/admin.py @@ -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) diff --git a/django/attachments/apps.py b/django/attachments/apps.py new file mode 100644 index 0000000..1c56402 --- /dev/null +++ b/django/attachments/apps.py @@ -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") diff --git a/django/attachments/django-attachments-1.11.zip b/django/attachments/django-attachments-1.11.zip new file mode 100644 index 0000000..ea2e0b9 Binary files /dev/null and b/django/attachments/django-attachments-1.11.zip differ diff --git a/django/attachments/forms.py b/django/attachments/forms.py new file mode 100644 index 0000000..a2d84e7 --- /dev/null +++ b/django/attachments/forms.py @@ -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) diff --git a/django/attachments/locale/README.transifex b/django/attachments/locale/README.transifex new file mode 100644 index 0000000..90789a0 --- /dev/null +++ b/django/attachments/locale/README.transifex @@ -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 diff --git a/django/attachments/locale/da/LC_MESSAGES/django.mo b/django/attachments/locale/da/LC_MESSAGES/django.mo new file mode 100644 index 0000000..cae96e1 Binary files /dev/null and b/django/attachments/locale/da/LC_MESSAGES/django.mo differ diff --git a/django/attachments/locale/da/LC_MESSAGES/django.po b/django/attachments/locale/da/LC_MESSAGES/django.po new file mode 100644 index 0000000..8aeb328 --- /dev/null +++ b/django/attachments/locale/da/LC_MESSAGES/django.po @@ -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 , 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 \n" +"Language-Team: da \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" + diff --git a/django/attachments/locale/de/LC_MESSAGES/django.mo b/django/attachments/locale/de/LC_MESSAGES/django.mo new file mode 100644 index 0000000..979dd37 Binary files /dev/null and b/django/attachments/locale/de/LC_MESSAGES/django.mo differ diff --git a/django/attachments/locale/de/LC_MESSAGES/django.po b/django/attachments/locale/de/LC_MESSAGES/django.po new file mode 100644 index 0000000..22a4f6d --- /dev/null +++ b/django/attachments/locale/de/LC_MESSAGES/django.po @@ -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 , 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 \n" +"Language-Team: de \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" + diff --git a/django/attachments/locale/el/LC_MESSAGES/django.mo b/django/attachments/locale/el/LC_MESSAGES/django.mo new file mode 100644 index 0000000..ce0354e Binary files /dev/null and b/django/attachments/locale/el/LC_MESSAGES/django.mo differ diff --git a/django/attachments/locale/el/LC_MESSAGES/django.po b/django/attachments/locale/el/LC_MESSAGES/django.po new file mode 100644 index 0000000..e2452ee --- /dev/null +++ b/django/attachments/locale/el/LC_MESSAGES/django.po @@ -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 , 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 \n" +"Language-Team: gr \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 "Διαγράψτε το συνημμένο" + diff --git a/django/attachments/locale/en/LC_MESSAGES/django.mo b/django/attachments/locale/en/LC_MESSAGES/django.mo new file mode 100644 index 0000000..d50a24a Binary files /dev/null and b/django/attachments/locale/en/LC_MESSAGES/django.mo differ diff --git a/django/attachments/locale/en/LC_MESSAGES/django.po b/django/attachments/locale/en/LC_MESSAGES/django.po new file mode 100644 index 0000000..0b3ccbc --- /dev/null +++ b/django/attachments/locale/en/LC_MESSAGES/django.po @@ -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 , 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 \n" +"Language-Team: en \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" + diff --git a/django/attachments/locale/es_AR/LC_MESSAGES/django.mo b/django/attachments/locale/es_AR/LC_MESSAGES/django.mo new file mode 100644 index 0000000..a8b2ce5 Binary files /dev/null and b/django/attachments/locale/es_AR/LC_MESSAGES/django.mo differ diff --git a/django/attachments/locale/es_AR/LC_MESSAGES/django.po b/django/attachments/locale/es_AR/LC_MESSAGES/django.po new file mode 100644 index 0000000..12ec32e --- /dev/null +++ b/django/attachments/locale/es_AR/LC_MESSAGES/django.po @@ -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" diff --git a/django/attachments/locale/fi/LC_MESSAGES/django.mo b/django/attachments/locale/fi/LC_MESSAGES/django.mo new file mode 100644 index 0000000..599e3dd Binary files /dev/null and b/django/attachments/locale/fi/LC_MESSAGES/django.mo differ diff --git a/django/attachments/locale/fi/LC_MESSAGES/django.po b/django/attachments/locale/fi/LC_MESSAGES/django.po new file mode 100644 index 0000000..a57f0de --- /dev/null +++ b/django/attachments/locale/fi/LC_MESSAGES/django.po @@ -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 , 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 \n" +"Language-Team: fi \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" diff --git a/django/attachments/locale/fr/LC_MESSAGES/django.mo b/django/attachments/locale/fr/LC_MESSAGES/django.mo new file mode 100644 index 0000000..22abd79 Binary files /dev/null and b/django/attachments/locale/fr/LC_MESSAGES/django.mo differ diff --git a/django/attachments/locale/fr/LC_MESSAGES/django.po b/django/attachments/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 0000000..4f71bc1 --- /dev/null +++ b/django/attachments/locale/fr/LC_MESSAGES/django.po @@ -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 , 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 \n" +"Language-Team: en \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" + diff --git a/django/attachments/locale/it/LC_MESSAGES/django.mo b/django/attachments/locale/it/LC_MESSAGES/django.mo new file mode 100644 index 0000000..a8b50f4 Binary files /dev/null and b/django/attachments/locale/it/LC_MESSAGES/django.mo differ diff --git a/django/attachments/locale/it/LC_MESSAGES/django.po b/django/attachments/locale/it/LC_MESSAGES/django.po new file mode 100644 index 0000000..b782b75 --- /dev/null +++ b/django/attachments/locale/it/LC_MESSAGES/django.po @@ -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 , 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 \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." diff --git a/django/attachments/locale/pt_BR/LC_MESSAGES/django.mo b/django/attachments/locale/pt_BR/LC_MESSAGES/django.mo new file mode 100644 index 0000000..f1b6105 Binary files /dev/null and b/django/attachments/locale/pt_BR/LC_MESSAGES/django.mo differ diff --git a/django/attachments/locale/pt_BR/LC_MESSAGES/django.po b/django/attachments/locale/pt_BR/LC_MESSAGES/django.po new file mode 100644 index 0000000..8b7af3a --- /dev/null +++ b/django/attachments/locale/pt_BR/LC_MESSAGES/django.po @@ -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 , 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 \n" +"Language-Team: LANGUAGE \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" diff --git a/django/attachments/locale/ru/LC_MESSAGES/django.po b/django/attachments/locale/ru/LC_MESSAGES/django.po new file mode 100644 index 0000000..5c74ab9 --- /dev/null +++ b/django/attachments/locale/ru/LC_MESSAGES/django.po @@ -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 , 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 \n" +"Language-Team: RU \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 "Ваше вложение было удалено" diff --git a/django/attachments/management/__init__.py b/django/attachments/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django/attachments/management/commands/__init__.py b/django/attachments/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django/attachments/management/commands/delete_stale_attachments.py b/django/attachments/management/commands/delete_stale_attachments.py new file mode 100644 index 0000000..7f77ad3 --- /dev/null +++ b/django/attachments/management/commands/delete_stale_attachments.py @@ -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) diff --git a/django/attachments/migrations/0001_initial.py b/django/attachments/migrations/0001_initial.py new file mode 100644 index 0000000..3057bbd --- /dev/null +++ b/django/attachments/migrations/0001_initial.py @@ -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,), + ), + ] diff --git a/django/attachments/migrations/0002_auto_20180104_1247.py b/django/attachments/migrations/0002_auto_20180104_1247.py new file mode 100644 index 0000000..707fa43 --- /dev/null +++ b/django/attachments/migrations/0002_auto_20180104_1247.py @@ -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'}, + ), + ] diff --git a/django/attachments/migrations/0003_auto_20190722_1216.py b/django/attachments/migrations/0003_auto_20190722_1216.py new file mode 100644 index 0000000..a3820bd --- /dev/null +++ b/django/attachments/migrations/0003_auto_20190722_1216.py @@ -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(), + ), + ] diff --git a/django/attachments/migrations/0004_db_index.py b/django/attachments/migrations/0004_db_index.py new file mode 100644 index 0000000..852a5b5 --- /dev/null +++ b/django/attachments/migrations/0004_db_index.py @@ -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'), + ), + ] diff --git a/django/attachments/migrations/0005_object_id_charfield.py b/django/attachments/migrations/0005_object_id_charfield.py new file mode 100644 index 0000000..303e51f --- /dev/null +++ b/django/attachments/migrations/0005_object_id_charfield.py @@ -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), + ), + ] diff --git a/django/attachments/migrations/0006_alter_attachment_attachment_file.py b/django/attachments/migrations/0006_alter_attachment_attachment_file.py new file mode 100644 index 0000000..7a398af --- /dev/null +++ b/django/attachments/migrations/0006_alter_attachment_attachment_file.py @@ -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'), + ), + ] diff --git a/django/attachments/migrations/0007_labelattachment.py b/django/attachments/migrations/0007_labelattachment.py new file mode 100644 index 0000000..326e73f --- /dev/null +++ b/django/attachments/migrations/0007_labelattachment.py @@ -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')}, + }, + ), + ] diff --git a/django/attachments/migrations/__init__.py b/django/attachments/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django/attachments/models.py b/django/attachments/models.py new file mode 100644 index 0000000..54c1d06 --- /dev/null +++ b/django/attachments/models.py @@ -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"),) diff --git a/django/attachments/templatetags/__init__.py b/django/attachments/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django/attachments/templatetags/attachments_tags.py b/django/attachments/templatetags/attachments_tags.py new file mode 100644 index 0000000..b4baaa7 --- /dev/null +++ b/django/attachments/templatetags/attachments_tags.py @@ -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) diff --git a/django/attachments/tests/__init__.py b/django/attachments/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django/attachments/tests/base.py b/django/attachments/tests/base.py new file mode 100644 index 0000000..32d6945 --- /dev/null +++ b/django/attachments/tests/base.py @@ -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 + ) diff --git a/django/attachments/tests/test_integrity.py b/django/attachments/tests/test_integrity.py new file mode 100644 index 0000000..68ac80c --- /dev/null +++ b/django/attachments/tests/test_integrity.py @@ -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()) diff --git a/django/attachments/tests/test_template.py b/django/attachments/tests/test_template.py new file mode 100644 index 0000000..1cac854 --- /dev/null +++ b/django/attachments/tests/test_template.py @@ -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("this is not allowed") + + 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"this is not allowed", + 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 + ) diff --git a/django/attachments/tests/testapp/__init__.py b/django/attachments/tests/testapp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django/attachments/tests/testapp/admin.py b/django/attachments/tests/testapp/admin.py new file mode 100644 index 0000000..c0ec653 --- /dev/null +++ b/django/attachments/tests/testapp/admin.py @@ -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) diff --git a/django/attachments/tests/testapp/apps.py b/django/attachments/tests/testapp/apps.py new file mode 100644 index 0000000..8fc6e65 --- /dev/null +++ b/django/attachments/tests/testapp/apps.py @@ -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"") > -1: + raise ValidationError("XML is forbidden") + + +class CustomizedAttachmentsApp(AttachmentsConfig): + """ + Adds a custom form validator function. + """ + attachment_validators = (deny_xml_uploads,) diff --git a/django/attachments/tests/testapp/migrations/0001_initial.py b/django/attachments/tests/testapp/migrations/0001_initial.py new file mode 100644 index 0000000..31858ce --- /dev/null +++ b/django/attachments/tests/testapp/migrations/0001_initial.py @@ -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,), + ), + ] diff --git a/django/attachments/tests/testapp/migrations/0002_auto_20180104_1247.py b/django/attachments/tests/testapp/migrations/0002_auto_20180104_1247.py new file mode 100644 index 0000000..efd259a --- /dev/null +++ b/django/attachments/tests/testapp/migrations/0002_auto_20180104_1247.py @@ -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', + ), + ] diff --git a/django/attachments/tests/testapp/migrations/0003_model_with_uuid_pk.py b/django/attachments/tests/testapp/migrations/0003_model_with_uuid_pk.py new file mode 100644 index 0000000..51e0c7e --- /dev/null +++ b/django/attachments/tests/testapp/migrations/0003_model_with_uuid_pk.py @@ -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', + }, + ), + ] diff --git a/django/attachments/tests/testapp/migrations/__init__.py b/django/attachments/tests/testapp/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django/attachments/tests/testapp/models.py b/django/attachments/tests/testapp/models.py new file mode 100644 index 0000000..4d15eb6 --- /dev/null +++ b/django/attachments/tests/testapp/models.py @@ -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 "/" diff --git a/django/attachments/tests/testapp/settings.py b/django/attachments/tests/testapp/settings.py new file mode 100644 index 0000000..503daac --- /dev/null +++ b/django/attachments/tests/testapp/settings.py @@ -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", + ] + }, + } +] diff --git a/django/attachments/tests/testapp/templates/testmodel_detail.html b/django/attachments/tests/testapp/templates/testmodel_detail.html new file mode 100644 index 0000000..a2959d4 --- /dev/null +++ b/django/attachments/tests/testapp/templates/testmodel_detail.html @@ -0,0 +1,16 @@ +{% load attachments_tags %} + +

{{ object.title }}

+

Object has {% attachments_count object %} attachments

+ +{% get_attachments_for object as attachments_list %} +{% for att in attachments_list %} +

+ {{ att }} + {{ att.attachment_file.url }} + {{ att.filename }} + {% attachment_delete_link att %} +

+{% endfor %} + +{% attachment_form object %} diff --git a/django/attachments/tests/testapp/urls.py b/django/attachments/tests/testapp/urls.py new file mode 100644 index 0000000..3712b35 --- /dev/null +++ b/django/attachments/tests/testapp/urls.py @@ -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\d+)/$", + DetailView.as_view( + template_name="testmodel_detail.html", + queryset=TestModel.objects.all(), + ), + name="testapp-detail", + ), +] diff --git a/django/attachments/urls.py b/django/attachments/urls.py new file mode 100644 index 0000000..e5f2c94 --- /dev/null +++ b/django/attachments/urls.py @@ -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[\w\-]+)/(?P[\w\-]+)/(?P\d+)/$", + add_attachment, + name="add", + ), + url( + r"^add-for/(?P[\w\-]+)/(?P[\w\-]+)/(?P[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\d+)/$", delete_attachment, name="delete"), +] diff --git a/django/attachments/views.py b/django/attachments/views.py new file mode 100644 index 0000000..77f7ddb --- /dev/null +++ b/django/attachments/views.py @@ -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_) diff --git a/django/background_task/settings.py b/django/background_task/settings.py index 3619f84..8504202 100644 --- a/django/background_task/settings.py +++ b/django/background_task/settings.py @@ -10,8 +10,7 @@ except Exception: class AppSettings(object): - """ - """ + """ """ @property def MAX_ATTEMPTS(self): """Control how many times a task will be attempted.""" diff --git a/django/connectrum/README.md b/django/connectrum/README.md index 4f918e8..6d585b0 100644 --- a/django/connectrum/README.md +++ b/django/connectrum/README.md @@ -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 diff --git a/django/finances/electrum.py b/django/finances/electrum.py index f177e26..bb8459b 100644 --- a/django/finances/electrum.py +++ b/django/finances/electrum.py @@ -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 diff --git a/django/finances/migrations/0010_outputstat_next_input_attributes.py b/django/finances/migrations/0010_outputstat_next_input_attributes.py new file mode 100644 index 0000000..5878e5c --- /dev/null +++ b/django/finances/migrations/0010_outputstat_next_input_attributes.py @@ -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={}), + ), + ] diff --git a/django/finances/migrations/0011_alter_outputstat_network.py b/django/finances/migrations/0011_alter_outputstat_network.py new file mode 100644 index 0000000..adb8b4d --- /dev/null +++ b/django/finances/migrations/0011_alter_outputstat_network.py @@ -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), + ), + ] diff --git a/django/finances/models.py b/django/finances/models.py index bc36104..9928aea 100644 --- a/django/finances/models.py +++ b/django/finances/models.py @@ -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: diff --git a/django/labelbase/models.py b/django/labelbase/models.py index 4b2a079..4020547 100644 --- a/django/labelbase/models.py +++ b/django/labelbase/models.py @@ -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. diff --git a/django/labelbase/static/bitcoin.pdf b/django/labelbase/static/bitcoin.pdf new file mode 100644 index 0000000..1e19b73 Binary files /dev/null and b/django/labelbase/static/bitcoin.pdf differ diff --git a/django/labelbase/static/ogimg.png b/django/labelbase/static/ogimg.png new file mode 100644 index 0000000..2951a4c Binary files /dev/null and b/django/labelbase/static/ogimg.png differ diff --git a/django/labelbase/templatetags/labelbase_tags.py b/django/labelbase/templatetags/labelbase_tags.py index 178ef6a..df786cb 100644 --- a/django/labelbase/templatetags/labelbase_tags.py +++ b/django/labelbase/templatetags/labelbase_tags.py @@ -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() diff --git a/django/labellabor/settings.py b/django/labellabor/settings.py index 24e6f67..905b3e3 100644 --- a/django/labellabor/settings.py +++ b/django/labellabor/settings.py @@ -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", ] diff --git a/django/labellabor/urls.py b/django/labellabor/urls.py index d415426..6c51b55 100644 --- a/django/labellabor/urls.py +++ b/django/labellabor/urls.py @@ -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, diff --git a/django/labellabor/views.py b/django/labellabor/views.py index cd89fc3..285195a 100644 --- a/django/labellabor/views.py +++ b/django/labellabor/views.py @@ -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" diff --git a/django/templates/_base.html b/django/templates/_base.html index 75563ac..e315936 100644 --- a/django/templates/_base.html +++ b/django/templates/_base.html @@ -6,7 +6,21 @@ - {% block title %}{% endblock %} + + + + + + + + + + + + + + + {% block title %}{% endblock %} | Labelbase
  • Labels
  • -
  • Fix & manage
  • -
  • Import
  • -
  • Export
  • +
  • Fix & Manage
  • +
  • UTXOs Health
  • Tree Map
  • {% comment %}
  • Stats & KPIs
  • {% endcomment %} {% if request.user.profile.use_fiatfinances %}
  • Fiat Finances
  • {% endif %} +
  • Import
  • +
  • Export
  • {% if labelbase.user.profile.use_hashtags %}
  • Hashtags
  • {% endif %} + {% endif %} diff --git a/django/templates/_dropdown_profile.html b/django/templates/_dropdown_profile.html index 1d64023..c8b93e4 100644 --- a/django/templates/_dropdown_profile.html +++ b/django/templates/_dropdown_profile.html @@ -55,7 +55,7 @@ diff --git a/django/templates/attachments/add.html b/django/templates/attachments/add.html new file mode 100644 index 0000000..79667f3 --- /dev/null +++ b/django/templates/attachments/add.html @@ -0,0 +1,5 @@ +{% extends "attachments/base.html" %} + +{% block content %} + {% include "attachments/add_form.html" %} +{% endblock %} \ No newline at end of file diff --git a/django/templates/attachments/add_form.html b/django/templates/attachments/add_form.html new file mode 100644 index 0000000..96a43a9 --- /dev/null +++ b/django/templates/attachments/add_form.html @@ -0,0 +1,14 @@ +{% load i18n %} +{% load bootstrap %} + +{% if form %} +
    + {% csrf_token %} + + {{ form|bootstrap }} +
    + + +
    + +{% endif %} diff --git a/django/templates/attachments/base.html b/django/templates/attachments/base.html new file mode 100644 index 0000000..372c1bb --- /dev/null +++ b/django/templates/attachments/base.html @@ -0,0 +1 @@ +{% block content %}{% endblock %} \ No newline at end of file diff --git a/django/templates/attachments/delete_link.html b/django/templates/attachments/delete_link.html new file mode 100644 index 0000000..77c2174 --- /dev/null +++ b/django/templates/attachments/delete_link.html @@ -0,0 +1,2 @@ +{% load i18n %} +{% if delete_url %}{% trans "Delete" %}{% endif %} diff --git a/django/templates/electrum_server_info_update.html b/django/templates/electrum_server_info_update.html index 1bd55b2..1388885 100644 --- a/django/templates/electrum_server_info_update.html +++ b/django/templates/electrum_server_info_update.html @@ -10,35 +10,59 @@

    - Update your Electrum server information. -

    - If you don't run your own Electrum server, here are some community trusted mainnet servers:
    -

      -
    • electrum.emzy.de / s50002
    • -
    • electrum.blockstream.info / s50002
    • -
    • bitcoin.lu.ke / s50002
    • - - -
    -
    - {% csrf_token %} - - {{ form|bootstrap }} - - - - - - - -
     
    - -
    -
    - - - + If you don't operate your own Electrum server, you have the option to connect to a community-trusted server.
    + However, keep in mind that using a public server allows it to view your outputs and transactions.

    + + +
    +
    Community-trusted Electrum servers
    +
    +

    + Mainnet: +

      +
    • electrum.emzy.de / s50002
    • +
    • electrum.blockstream.info / s50002
    • +
    +

    +
    +
    +

    + Testnet: +

      +
    • testnet.qtornado.com / s51002
    • +
    • testnet.aranguren.org / s51002
    • +
    +

    +
    +
    + + +
    +
    + {% csrf_token %} +

    + + {{ form|bootstrap }} + + + + + + + +
     
    + +
    + +

    +
    +
    + + + +
    +
    + {% endblock %} diff --git a/django/templates/label_edit.html b/django/templates/label_edit.html index aa2f9fd..ae6f434 100644 --- a/django/templates/label_edit.html +++ b/django/templates/label_edit.html @@ -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 %}