email interface without tests
This commit is contained in:
parent
49329a709e
commit
66bf165053
34 changed files with 1552 additions and 37 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -8,6 +8,13 @@ rc_hip/static/**
|
|||
rc_hip/data
|
||||
rc_hip/data/**
|
||||
|
||||
repapp/static
|
||||
repapp/static/**
|
||||
|
||||
repapp/data
|
||||
repapp/data/**
|
||||
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ The repapp is split into different modules, realized as Django apps, to improve
|
|||
|
||||
The one time login module provides a login for guests using a one time secret as part of an URL.
|
||||
|
||||
### Settings
|
||||
|
||||
- The templates use the `settings.ORGANIZATION` variable to fill the organization name.
|
||||
- The Django mail module is used to send emails. See https://docs.djangoproject.com/en/4.2/topics/email/ for details how to configure the mail host.
|
||||
|
||||
|
|
@ -22,8 +20,8 @@ AUTHENTICATION_BACKENDS = [
|
|||
"one_time_login.authentication_backends.OneTimeLoginBackend",
|
||||
]
|
||||
```
|
||||
The one time login urls must get added using namespace `one_time_login`:
|
||||
|
||||
The one time login urls must get added using namespace `one_time_login`:
|
||||
|
||||
```Python
|
||||
urlpatterns = i18n_patterns(
|
||||
|
|
@ -33,3 +31,46 @@ urlpatterns = i18n_patterns(
|
|||
...
|
||||
)
|
||||
```
|
||||
|
||||
## Email interface
|
||||
|
||||
The email interface modules supports handling of mails. It support sending of mails, including HTML formatting and attachments, and it supports receiving of plain text and HTML formatted mails, including attachments.
|
||||
|
||||
- The Django mail module is used to send emails. See https://docs.djangoproject.com/en/4.2/topics/email/ for details how to configure the mail host.
|
||||
- CKEditor is used for email content editing. See https://django-ckeditor.readthedocs.io/en/latest/ for details.
|
||||
- Crispy forms is used for form rendering. See https://django-crispy-forms.readthedocs.io/en/latest/ for details.
|
||||
- Bootstrap 5 theme for crispy forms is used. See https://github.com/django-crispy-forms/crispy-bootstrap5 for details.
|
||||
|
||||
### Configuration
|
||||
|
||||
The email interface urls must get added using namespace `email_interface`:
|
||||
|
||||
```Python
|
||||
urlpatterns = i18n_patterns(
|
||||
...
|
||||
path(_('emails/'),
|
||||
include('email_interface.urls', namespace='email_interface')),
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
To use CKEditor as authenticated user, the following additional urls must get added:
|
||||
|
||||
```Python
|
||||
urlpatterns = i18n_patterns(
|
||||
...
|
||||
# CKEditor upload views
|
||||
path('upload/', login_required(ckeditor_views.upload), name="ckeditor_upload"),
|
||||
path('browse/', never_cache(login_required(ckeditor_views.browse)),
|
||||
name="ckeditor_browse"),
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
To be abel to use CKEditor also the static an media content must get served. For development this is done with:
|
||||
|
||||
```Python
|
||||
if settings.DEBUG:
|
||||
urlpatterns += static(settings.MEDIA_URL,
|
||||
document_root=settings.MEDIA_ROOT)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -103,3 +103,61 @@ AUTHENTICATION_BACKENDS = [
|
|||
"one_time_login.authentication_backends.OneTimeLoginBackend",
|
||||
]
|
||||
```
|
||||
|
||||
## Email interface
|
||||
|
||||
The email interface uses CKEditor, crispy forms and easy thumbnails. The apps are enabled with:
|
||||
|
||||
```Python
|
||||
INSTALLED_APPS = [
|
||||
...
|
||||
# CKEditor for mail content editing
|
||||
"ckeditor",
|
||||
"ckeditor_uploader",
|
||||
# Crispy forms for bootstrap 5 form design
|
||||
"crispy_forms",
|
||||
"crispy_bootstrap5",
|
||||
# for creating thumbnails, used by email_interface demo views
|
||||
'easy_thumbnails',
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
For CKEditor, we use the following settings:
|
||||
|
||||
```Python
|
||||
# editor upload path, used e.g. for send mail demo
|
||||
CKEDITOR_UPLOAD_PATH = "editor_uploads/"
|
||||
|
||||
CKEDITOR_IMAGE_BACKEND = 'pillow'
|
||||
|
||||
# CKEditor default config
|
||||
CKEDITOR_CONFIGS = {
|
||||
'default': {
|
||||
'toolbar': 'full',
|
||||
'height': 300,
|
||||
'width': '100%',
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
To enable the image upload of CKEditor, also the Django media paths and urls must get configured:
|
||||
|
||||
```Python
|
||||
# Django media and static file config, required for CKEditor
|
||||
STATIC_URL = "static/"
|
||||
MEDIA_URL = "media/"
|
||||
# Use subfolders of project folder
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, "static")
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
|
||||
```
|
||||
|
||||
For crispy forms, we use the bootstrap 5 theme:
|
||||
|
||||
```Python
|
||||
# Crispy forms settings
|
||||
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
|
||||
CRISPY_TEMPLATE_PACK = "bootstrap5"
|
||||
```
|
||||
|
||||
The static files for bootstrap must also be made available.
|
||||
|
|
|
|||
0
repapp/email_interface/__init__.py
Normal file
0
repapp/email_interface/__init__.py
Normal file
20
repapp/email_interface/admin.py
Normal file
20
repapp/email_interface/admin.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
from django.contrib import admin
|
||||
from .models import Attachment, Message
|
||||
|
||||
|
||||
@admin.register(Attachment)
|
||||
class AttachmentAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'owner', 'mime_type']
|
||||
list_filter = ['owner', 'mime_type']
|
||||
search_fields = ['name', 'owner']
|
||||
ordering = ['owner', 'name']
|
||||
|
||||
|
||||
@admin.register(Message)
|
||||
class MessageAdmin(admin.ModelAdmin):
|
||||
list_display = ['receiver', 'summary',
|
||||
'sender', 'created', 'sent', 'answered']
|
||||
list_filter = ['receiver', 'sender', 'created', 'sent', 'answered']
|
||||
search_fields = ['receiver', 'summary', 'sender']
|
||||
date_hierarchy = 'created'
|
||||
ordering = ['-created', 'receiver']
|
||||
27
repapp/email_interface/apps.py
Normal file
27
repapp/email_interface/apps.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import logging
|
||||
from django.conf import settings
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def message_answered_receiver(sender, instance, **kwargs):
|
||||
logger.debug('Message answered signal received: %s %s',
|
||||
instance, kwargs['answer'])
|
||||
|
||||
|
||||
def new_message_receiver(sender, instance, **kwargs):
|
||||
logger.debug('New message signal received: %s', instance)
|
||||
|
||||
|
||||
class EmailInterfaceConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'email_interface'
|
||||
|
||||
def ready(self):
|
||||
if settings.DEBUG:
|
||||
logger.debug('Register dummy signal receivers')
|
||||
from .signals import new_message, message_answered
|
||||
message_answered.connect(message_answered_receiver)
|
||||
new_message.connect(new_message_receiver)
|
||||
47
repapp/email_interface/forms.py
Normal file
47
repapp/email_interface/forms.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import logging
|
||||
from bs4 import BeautifulSoup
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from ckeditor_uploader.widgets import CKEditorUploadingWidget
|
||||
from .models import Message
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MessageForm(forms.ModelForm):
|
||||
"""
|
||||
A form for creating new messages.
|
||||
"""
|
||||
mail_text = forms.CharField(
|
||||
label=_("Text content"), required=False, widget=forms.Textarea)
|
||||
use_mail_text = forms.BooleanField(
|
||||
label=_('Use text content?'), required=False)
|
||||
|
||||
class Meta:
|
||||
model = Message
|
||||
fields = ['receiver', 'summary', 'html_content']
|
||||
widgets = {'html_content': CKEditorUploadingWidget()}
|
||||
|
||||
def clean(self):
|
||||
if self.cleaned_data['use_mail_text'] and self.cleaned_data['mail_text'] == '':
|
||||
raise forms.ValidationError(
|
||||
_("Mail text cannot be empty if it is used!"))
|
||||
|
||||
return super().clean()
|
||||
|
||||
def save(self, commit: bool = True):
|
||||
message = super().save(commit=False)
|
||||
|
||||
if not self.cleaned_data['use_mail_text']:
|
||||
message.text_content = self.cleaned_data['mail_text']
|
||||
else:
|
||||
# convert HTML content to text content
|
||||
soup = BeautifulSoup(
|
||||
self.cleaned_data['html_content'], features='html.parser')
|
||||
message.text_content = soup.get_text('\n')
|
||||
|
||||
if commit:
|
||||
message.save()
|
||||
|
||||
return message
|
||||
47
repapp/email_interface/migrations/0001_initial.py
Normal file
47
repapp/email_interface/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# Generated by Django 4.2 on 2023-08-29 11:40
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import email_interface.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Attachment',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=200, verbose_name='File name')),
|
||||
('mime_type', models.CharField(max_length=200, verbose_name='MIME type')),
|
||||
('file', models.FileField(null=True, upload_to=email_interface.models.attachment_file_path, verbose_name='File')),
|
||||
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Owner')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Message',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('summary', models.CharField(max_length=200, verbose_name='Summary')),
|
||||
('html_content', models.TextField(verbose_name='HTML content')),
|
||||
('text_content', models.TextField(verbose_name='Text content')),
|
||||
('created', models.DateField(default=django.utils.timezone.now, verbose_name='Creation date')),
|
||||
('attachments', models.ManyToManyField(null=True, to='email_interface.attachment', verbose_name='Attachments')),
|
||||
('receiver', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='receiver', to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
||||
('reply_to', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='email_interface.message', verbose_name='Reply to')),
|
||||
('sender', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sender', to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Message',
|
||||
'verbose_name_plural': 'Messages',
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
# Generated by Django 4.2 on 2023-08-29 14:29
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('email_interface', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='message',
|
||||
name='answered',
|
||||
field=models.BooleanField(default=False, verbose_name='answered?'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='message',
|
||||
name='sent',
|
||||
field=models.BooleanField(default=False, verbose_name='sent?'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='message',
|
||||
name='attachments',
|
||||
field=models.ManyToManyField(to='email_interface.attachment', verbose_name='Attachments'),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
# Generated by Django 4.2 on 2023-08-30 11:14
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import email_interface.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('email_interface', '0002_message_answered_message_sent_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='message',
|
||||
name='attachments',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='attachment',
|
||||
name='message',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='email_interface.message', verbose_name='Message'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='attachment',
|
||||
name='file',
|
||||
field=models.FileField(null=True, unique=True, upload_to=email_interface.models.attachment_file_path, verbose_name='File'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='message',
|
||||
name='receiver',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='receiver', to=settings.AUTH_USER_MODEL, verbose_name='Receiver'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='message',
|
||||
name='sender',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sender', to=settings.AUTH_USER_MODEL, verbose_name='Sender'),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 4.2 on 2023-08-30 11:42
|
||||
|
||||
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),
|
||||
('email_interface', '0003_remove_message_attachments_attachment_message_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='attachment',
|
||||
options={'verbose_name': 'Attachment', 'verbose_name_plural': 'Attachments'},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='message',
|
||||
name='receiver',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='receiver', to=settings.AUTH_USER_MODEL, verbose_name='Receiver'),
|
||||
),
|
||||
]
|
||||
0
repapp/email_interface/migrations/__init__.py
Normal file
0
repapp/email_interface/migrations/__init__.py
Normal file
133
repapp/email_interface/models.py
Normal file
133
repapp/email_interface/models.py
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import os
|
||||
import logging
|
||||
import django.utils.timezone
|
||||
import django.dispatch
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Message(models.Model):
|
||||
"""
|
||||
A email message.
|
||||
"""
|
||||
summary = models.CharField(max_length=200, verbose_name=_(
|
||||
"Summary"), null=False)
|
||||
html_content = models.TextField(verbose_name=_("HTML content"), null=False)
|
||||
text_content = models.TextField(verbose_name=_("Text content"), null=False)
|
||||
created = models.DateField(verbose_name=_(
|
||||
"Creation date"), default=django.utils.timezone.now)
|
||||
receiver = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE,
|
||||
null=True, verbose_name=_("Receiver"), related_name='receiver')
|
||||
sender = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE,
|
||||
null=True, verbose_name=_("Sender"), related_name='sender')
|
||||
reply_to = models.ForeignKey(
|
||||
'self', null=True, on_delete=models.SET_NULL, verbose_name=_("Reply to"))
|
||||
sent = models.BooleanField(verbose_name=_("sent?"), default=False)
|
||||
answered = models.BooleanField(verbose_name=_("answered?"), default=False)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Message')
|
||||
verbose_name_plural = _('Messages')
|
||||
|
||||
def __str__(self):
|
||||
return f'Message for user {self.receiver} with summary {self.summary}'
|
||||
|
||||
def attachments(self):
|
||||
return Attachment.objects.filter(message=self).all()
|
||||
|
||||
def thread(self):
|
||||
messages = [self]
|
||||
|
||||
m = self
|
||||
while m.reply_to:
|
||||
messages.append(m.reply_to)
|
||||
m = m.reply_to
|
||||
|
||||
messages.reverse()
|
||||
|
||||
return messages
|
||||
|
||||
def answers(self):
|
||||
return Message.objects.filter(reply_to=self).all()
|
||||
|
||||
def siblings(self):
|
||||
if self.reply_to:
|
||||
return Message.objects.filter(reply_to=self.reply_to).all().exclude(pk=self.pk)
|
||||
else:
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def to_user(user):
|
||||
return Message.objects.filter(receiver=user).all()
|
||||
|
||||
@staticmethod
|
||||
def from_user(user):
|
||||
return Message.objects.filter(sender=user).all()
|
||||
|
||||
|
||||
def attachment_file_path(instance, filename):
|
||||
return f'{instance.owner.username}/{filename}'
|
||||
|
||||
|
||||
class Attachment(models.Model):
|
||||
"""
|
||||
A email attachment.
|
||||
"""
|
||||
owner = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=False, verbose_name=_("Owner"))
|
||||
name = models.CharField(max_length=200, verbose_name=_(
|
||||
"File name"), null=False)
|
||||
mime_type = models.CharField(max_length=200, verbose_name=_(
|
||||
"MIME type"), null=False)
|
||||
file = models.FileField(upload_to=attachment_file_path,
|
||||
null=True, verbose_name=_("File"), unique=True)
|
||||
message = models.ForeignKey(
|
||||
Message, on_delete=models.CASCADE, null=False, verbose_name=_("Message"))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Attachment')
|
||||
verbose_name_plural = _('Attachments')
|
||||
|
||||
def __str__(self):
|
||||
return f'Attachment {self.file} for {self.message}'
|
||||
|
||||
def get_absolute_url(self):
|
||||
return f'{settings.MEDIA_URL}{self.file}'
|
||||
|
||||
@staticmethod
|
||||
def of_user(user):
|
||||
return Attachment.objects.filter(owner=user).all()
|
||||
|
||||
|
||||
@receiver(models.signals.post_delete, sender=Attachment)
|
||||
def auto_delete_file_on_delete(sender, instance, **kwargs):
|
||||
"""
|
||||
Deletes file from filesystem when corresponding Attachment is deleted.
|
||||
"""
|
||||
if instance.file:
|
||||
if os.path.isfile(instance.file.path):
|
||||
os.remove(instance.file.path)
|
||||
|
||||
|
||||
@receiver(models.signals.pre_save, sender=Attachment)
|
||||
def auto_delete_file_on_change(sender, instance, **kwargs):
|
||||
"""
|
||||
Deletes old file from filesystem when corresponding Attachment object is updated with new file.
|
||||
"""
|
||||
if not instance.pk:
|
||||
return False
|
||||
|
||||
try:
|
||||
old_file = Attachment.objects.get(pk=instance.pk).file
|
||||
except Attachment.DoesNotExist:
|
||||
return False
|
||||
|
||||
new_file = instance.file
|
||||
if not old_file == new_file:
|
||||
if os.path.isfile(old_file.path):
|
||||
os.remove(old_file.path)
|
||||
7
repapp/email_interface/signals.py
Normal file
7
repapp/email_interface/signals.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
from django import dispatch
|
||||
|
||||
# Triggered if a message gets answered.
|
||||
message_answered = dispatch.Signal()
|
||||
|
||||
# Triggered if a new message is received.
|
||||
new_message = dispatch.Signal()
|
||||
7
repapp/email_interface/static/email_interface/bootstrap5/css/bootstrap.min.css
vendored
Normal file
7
repapp/email_interface/static/email_interface/bootstrap5/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
7
repapp/email_interface/static/email_interface/bootstrap5/js/bootstrap.min.js
vendored
Normal file
7
repapp/email_interface/static/email_interface/bootstrap5/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
repapp/email_interface/static/email_interface/bootstrap5/js/jquery-3.6.4.min.js
vendored
Normal file
2
repapp/email_interface/static/email_interface/bootstrap5/js/jquery-3.6.4.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,36 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="author" content="Thomas Irgang">
|
||||
{% block head %}
|
||||
{% endblock head %}
|
||||
<link rel="stylesheet"
|
||||
href="{% static 'email_interface/bootstrap5/css/bootstrap.min.css' %}" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
{% if messages %}
|
||||
<div class="col-8">
|
||||
<ul class="messages">
|
||||
{% for message in messages %}
|
||||
<li {% if message.tags %}class="{{ message.tags }}"{% endif %}>{{ message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% block content %}
|
||||
{% endblock content %}
|
||||
</div>
|
||||
</div>
|
||||
<script src="{% static 'email_interface/bootstrap5/js/jquery-3.6.4.min.js' %}"></script>
|
||||
<script src="{% static 'email_interface/bootstrap5/js/bootstrap.min.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static "ckeditor/ckeditor-init.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "ckeditor/ckeditor/ckeditor.js" %}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
{% extends 'email_interface/testing/base.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load thumbnail %}
|
||||
{% load i18n %}
|
||||
{% block head %}
|
||||
<title>"{{ mail.summary }}" {% trans 'from' %} {{ mail.sender }}</title>
|
||||
<meta name="description"
|
||||
content="{{ mail.summary }} {% trans 'from' %} {{ mail.sender }}">
|
||||
<meta property="og:title"
|
||||
content="{{ mail.summary }} {% trans 'from' %} {{ mail.sender }}">
|
||||
<meta property="og:description"
|
||||
content="{{ mail.summary }} {% trans 'from' %} {{ mail.sender }}">
|
||||
{% endblock head %}
|
||||
{% block content %}
|
||||
{% for mail in mails %}
|
||||
<div class="col-8">
|
||||
<div class="card" style="width: 100%; overflow: hidden;">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
{% if mail.sender %}
|
||||
{{ mail.sender }} ({{ mail.sender.email }}):
|
||||
{% else %}
|
||||
{% trans 'No sender' %}
|
||||
{% endif %}
|
||||
<a href="{% url 'email_interface:mail_thread' mail.pk %}">{{ mail.summary }}</a>
|
||||
</h5>
|
||||
<h6 class="card-subtitle mb-2 text-muted">
|
||||
{% trans 'to' %}:
|
||||
{% if mail.receiver %}
|
||||
{{ mail.receiver }}
|
||||
{% else %}
|
||||
{% trans 'No receiver' %}
|
||||
{% endif %}
|
||||
, {{ mail.created|date:"l" }}
|
||||
</h6>
|
||||
<p class="card-text">
|
||||
{% if mail.html_content %}
|
||||
{{ mail.html_content|safe }}
|
||||
{% else %}
|
||||
{{ mail.text_content }}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% for attachment in mail.attachments %}
|
||||
{% if 'image' in attachment.mime_type %}
|
||||
<a href="{{ attachment.file.url }}">
|
||||
<img src="{% thumbnail attachment.file 300x0 %}">
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ attachment.get_absolute_url }}" class="card-link">{{ attachment.name }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if answers %}
|
||||
<div class="col-8">
|
||||
<h2>{% trans 'Answers' %}:</h2>
|
||||
{% for answer in answers %}
|
||||
<a href="{% url 'email_interface:mail_thread' answer.pk %}">{{ answer.summary }} {% trans 'from' %} {{ answer.sender }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if siblings %}
|
||||
<div class="col-8">
|
||||
<h2>{% trans 'Siblings' %}:</h2>
|
||||
{% for sibling in siblings %}
|
||||
<a href="{% url 'email_interface:mail_thread' sibling.pk %}">{{ sibling.summary }} {% trans 'from' %} {{ sibling.sender }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if mail.sender %}
|
||||
<div class="col-8">
|
||||
<h2>{% trans 'Reply to' %} "{{ mail.summary }}" {% trans 'from' %} {{ mail.sender }}:</h2>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.media }}
|
||||
{{ form|crispy }}
|
||||
<button type="submit" class="btn btn-primary">{% trans 'Send' %}</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock content %}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
{% extends 'email_interface/testing/base.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load thumbnail %}
|
||||
{% load i18n %}
|
||||
{% block head %}
|
||||
<title>{% trans 'Attachments' %}</title>
|
||||
<meta name="description" content="{% trans 'Attachments' %}">
|
||||
<meta property="og:title" content="{% trans 'Attachments' %}">
|
||||
<meta property="og:description" content="{% trans 'All my attachments.' %}">
|
||||
{% endblock head %}
|
||||
{% block content %}
|
||||
{% for attachment in attachments %}
|
||||
<div class="col-8">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col">{% trans 'Name' %}</th>
|
||||
<th scope="col">{% trans 'MIME Type' %}</th>
|
||||
<th scope="col">{% trans 'File' %}</th>
|
||||
<th scope="col">{% trans 'Message' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">{{ attachment.pk }}</th>
|
||||
<td>{{ attachment.name }}</td>
|
||||
<td>{{ attachment.mime_type }}</td>
|
||||
<td>
|
||||
{% if 'image' in attachment.mime_type %}
|
||||
<a href="{{ attachment.file.url }}">
|
||||
<img src="{% thumbnail attachment.file 300x0 %}">
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ attachment.get_absolute_url }}" class="card-link">{{ attachment.name }}</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url 'email_interface:mail_thread' attachment.message.pk %}">
|
||||
{{ attachment.message.summary }} {% trans 'from' %} {{ attachment.message.sender }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endblock content %}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
{% extends 'email_interface/testing/base.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load thumbnail %}
|
||||
{% load i18n %}
|
||||
{% block head %}
|
||||
<title>{% trans 'Mails' %}</title>
|
||||
<meta name="description" content="{% trans 'Mails' %}">
|
||||
<meta property="og:title" content="{% trans 'Mails' %}">
|
||||
<meta property="og:description" content="{% trans 'All my mails.' %}">
|
||||
{% endblock head %}
|
||||
{% block content %}
|
||||
{% for mail in mails %}
|
||||
<div class="col-8">
|
||||
<div class="card" style="width: 100%; overflow: hidden;">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
{% if mail.sender %}
|
||||
{{ mail.sender }} ({{ mail.sender.email }}):
|
||||
{% else %}
|
||||
{% trans 'No sender' %}
|
||||
{% endif %}
|
||||
<a href="{% url 'email_interface:mail_thread' mail.pk %}">{{ mail.summary }}</a>
|
||||
</h5>
|
||||
<h6 class="card-subtitle mb-2 text-muted">
|
||||
{% trans 'to' %}:
|
||||
{% if mail.receiver %}
|
||||
{{ mail.receiver }}
|
||||
{% else %}
|
||||
{% trans 'No receiver' %}
|
||||
{% endif %}
|
||||
, {{ mail.created|date:"l" }}
|
||||
</h6>
|
||||
<p class="card-text">
|
||||
{% if mail.html_content %}
|
||||
{{ mail.html_content|safe }}
|
||||
{% else %}
|
||||
{{ mail.text_content }}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% for attachment in mail.attachments %}
|
||||
{% if 'image' in attachment.mime_type %}
|
||||
<a href="{{ attachment.file.url }}">
|
||||
<img src="{% thumbnail attachment.file 300x0 %}">
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ attachment.get_absolute_url }}" class="card-link">{{ attachment.name }}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if mail.answers %}
|
||||
<hr>
|
||||
<p>{% trans 'Answers' %}:</p>
|
||||
{% for answer in mail.answers %}
|
||||
<a href="{% url 'email_interface:mail_thread' answer.pk %}">{{ answer.summary }} {% trans 'from' %} {{ answer.sender }}</a>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if mail.siblings %}
|
||||
<hr>
|
||||
<p>{% trans 'Siblings' %}:</p>
|
||||
{% for sibling in mail.siblings %}
|
||||
<a href="{% url 'email_interface:mail_thread' sibling.pk %}">{{ sibling.summary }} {% trans 'from' %} {{ sibling.sender }}</a>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endblock content %}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
{% extends 'email_interface/testing/base.html' %}
|
||||
{% load i18n %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% block head %}
|
||||
<title>{% trans 'Send mail demo' %}</title>
|
||||
<meta name="description" content="{% trans 'Send mail demo' %}">
|
||||
<meta property="og:title" content="{% trans 'Send mail demo' %}">
|
||||
<meta property="og:description"
|
||||
content="{% trans 'A send mail demo form.' %}">
|
||||
{% endblock head %}
|
||||
{% block content %}
|
||||
<div class="col-8">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.media }}
|
||||
{{ form|crispy }}
|
||||
<button type="submit" class="btn btn-primary">{% trans 'Send' %}</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
BIN
repapp/email_interface/test_data/Nut.jpg
Normal file
BIN
repapp/email_interface/test_data/Nut.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 933 KiB |
3
repapp/email_interface/tests.py
Normal file
3
repapp/email_interface/tests.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
27
repapp/email_interface/urls.py
Normal file
27
repapp/email_interface/urls.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
"""
|
||||
Urls of RepApp.
|
||||
"""
|
||||
import sys
|
||||
import logging
|
||||
from django.urls import path
|
||||
from django.conf import settings
|
||||
from . import views
|
||||
|
||||
|
||||
app_name = 'email_interface'
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("process/", views.process, name="process"),
|
||||
path("sent/", views.my_sent_mails, name="my_sent_mails"),
|
||||
path("received/", views.my_received_mails, name="my_received_mails"),
|
||||
path("attachments/", views.my_attachments, name="my_attachments"),
|
||||
path("<int:id>/view", views.mail_thread, name="mail_thread"),
|
||||
]
|
||||
|
||||
if settings.DEBUG or sys.argv[1:2] == ['test']:
|
||||
logging.info('email_interface: enable test and debug URLs')
|
||||
urlpatterns += [
|
||||
path("test-mail/", views.send_test_mail, name="send_test_mail"),
|
||||
path("send-mail/", views.SendMailView.as_view(), name="send_mail"),
|
||||
]
|
||||
189
repapp/email_interface/utils.py
Normal file
189
repapp/email_interface/utils.py
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
import os
|
||||
import mimetypes
|
||||
import logging
|
||||
from imap_tools import MailBox, AND, MailMessage
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from .models import Message, Attachment
|
||||
from .signals import message_answered, new_message
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_message(request, sender, receiver, summary, html_content, text_content, attachments=[], reply_to=None, send=True) -> bool:
|
||||
"""
|
||||
Create a new email message, and send it if send is True.
|
||||
"""
|
||||
message = Message(
|
||||
sender=sender,
|
||||
receiver=receiver,
|
||||
summary=summary,
|
||||
html_content=html_content,
|
||||
text_content=text_content,
|
||||
reply_to=reply_to
|
||||
)
|
||||
message.save()
|
||||
|
||||
logger.debug('New message %s', message)
|
||||
|
||||
for attachment in attachments:
|
||||
if not os.path.isfile(attachment):
|
||||
logger.debug('Attachment not found: %s', attachment)
|
||||
continue
|
||||
|
||||
_, name = os.path.split(attachment)
|
||||
mime_type = mimetypes.guess_type(attachment)
|
||||
|
||||
if not attachment.startswith(settings.MEDIA_ROOT):
|
||||
with open(attachment, "rb") as file:
|
||||
# copy file
|
||||
attachment = ContentFile(file.read(), name)
|
||||
|
||||
a = Attachment(
|
||||
owner=sender,
|
||||
name=name,
|
||||
mime_type=mime_type,
|
||||
file=attachment,
|
||||
message=message
|
||||
)
|
||||
a.save()
|
||||
|
||||
logger.debug('New attachment %s', a)
|
||||
|
||||
message.save()
|
||||
|
||||
if send:
|
||||
return send_message(message, request)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def send_message(message, request):
|
||||
"""
|
||||
Send a message as email.
|
||||
"""
|
||||
# make media urls absolute
|
||||
absolute_media_url = request.build_absolute_uri(settings.MEDIA_URL)
|
||||
|
||||
logger.debug('Media URL: %s, absolute media url: %s',
|
||||
settings.MEDIA_URL, absolute_media_url)
|
||||
|
||||
html_content = message.html_content
|
||||
html_content = html_content.replace(settings.MEDIA_URL, absolute_media_url)
|
||||
|
||||
# Create email message
|
||||
email = EmailMultiAlternatives(message.summary, message.text_content,
|
||||
settings.DEFAULT_FROM_EMAIL, [message.receiver.email])
|
||||
email.attach_alternative(html_content, "text/html")
|
||||
|
||||
for attachment in Attachment.objects.filter(message=message):
|
||||
with open(attachment.file.path, 'rb') as file:
|
||||
file_content = file.read()
|
||||
email.attach(attachment.name, file_content, attachment.mime_type)
|
||||
|
||||
try:
|
||||
res = email.send()
|
||||
except:
|
||||
# Sending mail cause an exception
|
||||
return False
|
||||
|
||||
if res > 0:
|
||||
message.sent = True
|
||||
message.save()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def process_message(mail_message: MailMessage, guest):
|
||||
"""
|
||||
Process a valid message from a guest.
|
||||
"""
|
||||
receiver = None
|
||||
reply_to = None
|
||||
|
||||
if mail_message.subject.startswith('Re:'):
|
||||
summary = mail_message.subject.replace('Re:', '').strip()
|
||||
reply_to = Message.objects.filter(
|
||||
receiver=guest, summary__icontains=summary).first()
|
||||
|
||||
if reply_to:
|
||||
receiver = reply_to.sender
|
||||
reply_to.answered = True
|
||||
reply_to.save()
|
||||
|
||||
logger.debug('Answer received for %s', reply_to)
|
||||
|
||||
html_content = None
|
||||
if mail_message.html:
|
||||
html_content = mail_message.html
|
||||
|
||||
message = Message(
|
||||
sender=guest,
|
||||
receiver=receiver,
|
||||
summary=mail_message.subject,
|
||||
html_content=html_content,
|
||||
text_content=mail_message.text,
|
||||
reply_to=reply_to,
|
||||
)
|
||||
message.save()
|
||||
|
||||
if reply_to:
|
||||
# trigger message answered signal
|
||||
message_answered.send(
|
||||
sender=Message, instance=reply_to, answer=message)
|
||||
else:
|
||||
# trigger new message signal
|
||||
new_message.send(sender=Message, instance=message)
|
||||
|
||||
logger.debug('Received message %s', message)
|
||||
|
||||
for attachment in mail_message.attachments:
|
||||
name = attachment.filename
|
||||
mime_type = attachment.content_type
|
||||
file = ContentFile(attachment.payload, name)
|
||||
|
||||
logger.debug('Processing attachment %s (%s) of message %s',
|
||||
name, mime_type, message)
|
||||
|
||||
a = Attachment(
|
||||
owner=guest,
|
||||
name=name,
|
||||
mime_type=mime_type,
|
||||
file=file,
|
||||
message=message
|
||||
)
|
||||
a.save()
|
||||
|
||||
logger.debug('New attachment %s', a)
|
||||
|
||||
|
||||
def process_mails():
|
||||
"""
|
||||
Check for new messages and handle valid messages.
|
||||
"""
|
||||
host = os.getenv("DJANGO_EMAIL_HOST", "")
|
||||
user = os.getenv("DJANGO_EMAIL_HOST_USER", None)
|
||||
password = os.getenv("DJANGO_EMAIL_HOST_PASSWORD", None)
|
||||
mailbox = MailBox(host).login(user, password)
|
||||
messages = mailbox.fetch(criteria=AND(seen=False),
|
||||
mark_seen=True, bulk=True)
|
||||
|
||||
message_count = 0
|
||||
for message in messages:
|
||||
message_count += 1
|
||||
sender = message.from_
|
||||
|
||||
guest = get_user_model().objects.filter(email=sender, is_active=True).first()
|
||||
if guest:
|
||||
process_message(message, guest)
|
||||
else:
|
||||
logger.info(
|
||||
'Mail form unknown sender %s with subject %s ignored.', sender, message.subject)
|
||||
|
||||
logger.debug('Processed %d new messages', message_count)
|
||||
|
||||
return message_count
|
||||
150
repapp/email_interface/views.py
Normal file
150
repapp/email_interface/views.py
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
import os
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from django.http import HttpResponse, HttpResponseRedirect, Http404
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.views import generic
|
||||
from django.contrib import messages
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from .forms import MessageForm
|
||||
from .utils import create_message, send_message, process_mails
|
||||
from .models import Message, Attachment
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def process(request):
|
||||
count = process_mails()
|
||||
return HttpResponse(f'{count} new messages were processed.')
|
||||
|
||||
|
||||
@login_required
|
||||
def mail_thread(request, id):
|
||||
message = get_object_or_404(Message, pk=id)
|
||||
|
||||
form = MessageForm()
|
||||
|
||||
form['receiver'].field.widget.attrs['disabled'] = 'disabled'
|
||||
form.initial['receiver'] = message.sender
|
||||
|
||||
form['summary'].field.widget.attrs['disabled'] = 'disabled'
|
||||
summary = f'Re: {message.summary}'
|
||||
form.initial['summary'] = summary
|
||||
|
||||
if request.method == 'POST':
|
||||
# Form was submitted
|
||||
form = MessageForm(request.POST.copy())
|
||||
form.data['receiver'] = message.sender.pk
|
||||
form.data['summary'] = summary
|
||||
|
||||
if form.is_valid():
|
||||
m = form.save(commit=False)
|
||||
m.sender = request.user
|
||||
m.reply_to = message
|
||||
m.save()
|
||||
|
||||
message.answered = True
|
||||
message.save()
|
||||
|
||||
success = send_message(m, request)
|
||||
|
||||
if success:
|
||||
messages.info(request, _('Mail was sent.'))
|
||||
return HttpResponseRedirect(reverse_lazy('email_interface:mail_thread', kwargs={'id': m.pk}))
|
||||
else:
|
||||
messages.error(request, _('Sending of mail failed!'))
|
||||
|
||||
return render(
|
||||
request,
|
||||
"email_interface/testing/mail_thread.html",
|
||||
{
|
||||
'form': form,
|
||||
'mails': message.thread(),
|
||||
'mail': message,
|
||||
'answers': message.answers(),
|
||||
'siblings': message.siblings(),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def send_test_mail(request):
|
||||
"""
|
||||
Debug view to send a test mail.
|
||||
"""
|
||||
# get first user
|
||||
user = get_user_model().objects.get(pk=1)
|
||||
current_folder = Path(__file__).resolve().parent
|
||||
file = os.path.join(current_folder, 'test_data', 'Nut.jpg')
|
||||
res = create_message(
|
||||
request,
|
||||
request.user,
|
||||
user,
|
||||
'A test mail',
|
||||
'<html><body><h1>Test!</h1></body></html>',
|
||||
'Test!',
|
||||
attachments=[file]
|
||||
)
|
||||
|
||||
if res:
|
||||
return HttpResponse('Mail was sent successful!')
|
||||
else:
|
||||
return HttpResponse('Sending of mail failed!')
|
||||
|
||||
|
||||
class SendMailView(LoginRequiredMixin, generic.FormView):
|
||||
form_class = MessageForm
|
||||
template_name = "email_interface/testing/write_mail.html"
|
||||
success_url = reverse_lazy("email_interface:send_mail")
|
||||
|
||||
def form_valid(self, form):
|
||||
message = form.save(commit=False)
|
||||
message.sender = self.request.user
|
||||
message.save()
|
||||
|
||||
success = send_message(message, self.request)
|
||||
|
||||
if success:
|
||||
messages.info(self.request, _('Mail was sent.'))
|
||||
else:
|
||||
messages.error(self.request, _('Sending of mail failed!'))
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
@login_required
|
||||
def my_sent_mails(request):
|
||||
return render(
|
||||
request,
|
||||
"email_interface/testing/my_mails.html",
|
||||
{
|
||||
'mails': Message.from_user(request.user),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def my_received_mails(request):
|
||||
return render(
|
||||
request,
|
||||
"email_interface/testing/my_mails.html",
|
||||
{
|
||||
'mails': Message.to_user(request.user),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def my_attachments(request):
|
||||
return render(
|
||||
request,
|
||||
"email_interface/testing/my_attachments.html",
|
||||
{
|
||||
'attachments': Attachment.of_user(request.user),
|
||||
}
|
||||
)
|
||||
|
|
@ -8,8 +8,8 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-08-29 09:17+0200\n"
|
||||
"PO-Revision-Date: 2023-08-29 09:17+0200\n"
|
||||
"POT-Creation-Date: 2023-08-30 21:12+0200\n"
|
||||
"PO-Revision-Date: 2023-08-30 21:13+0200\n"
|
||||
"Last-Translator: <thomas@irgang.eu>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
|
|
@ -19,6 +19,173 @@ msgstr ""
|
|||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Translated-Using: django-rosetta 0.9.9\n"
|
||||
|
||||
#: email_interface/forms.py:14 email_interface/models.py:18
|
||||
msgid "Text content"
|
||||
msgstr "Text Inhalt"
|
||||
|
||||
#: email_interface/forms.py:16
|
||||
msgid "Use text content?"
|
||||
msgstr "Text Inhalt verwenden?"
|
||||
|
||||
#: email_interface/forms.py:29
|
||||
msgid "Mail text cannot be empty if it is used!"
|
||||
msgstr "Der Text Inhalt kann nicht leer sein wenn er verwendet wird!"
|
||||
|
||||
#: email_interface/models.py:16
|
||||
msgid "Summary"
|
||||
msgstr "Betreff"
|
||||
|
||||
#: email_interface/models.py:17
|
||||
msgid "HTML content"
|
||||
msgstr "HTML Inhalt"
|
||||
|
||||
#: email_interface/models.py:20 one_time_login/models.py:20
|
||||
msgid "Creation date"
|
||||
msgstr "Erstellungsdatum"
|
||||
|
||||
#: email_interface/models.py:22
|
||||
msgid "Receiver"
|
||||
msgstr "Empfänger"
|
||||
|
||||
#: email_interface/models.py:24
|
||||
msgid "Sender"
|
||||
msgstr "Sender"
|
||||
|
||||
#: email_interface/models.py:26
|
||||
#: email_interface/templates/email_interface/testing/mail_thread.html:74
|
||||
msgid "Reply to"
|
||||
msgstr "Antworten an"
|
||||
|
||||
#: email_interface/models.py:27
|
||||
msgid "sent?"
|
||||
msgstr "gesendet?"
|
||||
|
||||
#: email_interface/models.py:28
|
||||
msgid "answered?"
|
||||
msgstr "beantwortet?"
|
||||
|
||||
#: email_interface/models.py:31 email_interface/models.py:84
|
||||
#: email_interface/templates/email_interface/testing/my_attachments.html:21
|
||||
msgid "Message"
|
||||
msgstr "Nachricht"
|
||||
|
||||
#: email_interface/models.py:32
|
||||
msgid "Messages"
|
||||
msgstr "Nachrichten"
|
||||
|
||||
#: email_interface/models.py:76
|
||||
msgid "Owner"
|
||||
msgstr "Besitzer"
|
||||
|
||||
#: email_interface/models.py:78
|
||||
msgid "File name"
|
||||
msgstr "Dateiname"
|
||||
|
||||
#: email_interface/models.py:80
|
||||
msgid "MIME type"
|
||||
msgstr "MIME Typ"
|
||||
|
||||
#: email_interface/models.py:82
|
||||
#: email_interface/templates/email_interface/testing/my_attachments.html:20
|
||||
msgid "File"
|
||||
msgstr "Datei"
|
||||
|
||||
#: email_interface/models.py:87
|
||||
msgid "Attachment"
|
||||
msgstr "Anhang"
|
||||
|
||||
#: email_interface/models.py:88
|
||||
#: email_interface/templates/email_interface/testing/my_attachments.html:6
|
||||
#: email_interface/templates/email_interface/testing/my_attachments.html:7
|
||||
#: email_interface/templates/email_interface/testing/my_attachments.html:8
|
||||
msgid "Attachments"
|
||||
msgstr "Anhänge"
|
||||
|
||||
#: email_interface/templates/email_interface/testing/mail_thread.html:6
|
||||
#: email_interface/templates/email_interface/testing/mail_thread.html:8
|
||||
#: email_interface/templates/email_interface/testing/mail_thread.html:10
|
||||
#: email_interface/templates/email_interface/testing/mail_thread.html:12
|
||||
#: email_interface/templates/email_interface/testing/mail_thread.html:60
|
||||
#: email_interface/templates/email_interface/testing/mail_thread.html:68
|
||||
#: email_interface/templates/email_interface/testing/mail_thread.html:74
|
||||
#: email_interface/templates/email_interface/testing/my_attachments.html:40
|
||||
#: email_interface/templates/email_interface/testing/my_mails.html:53
|
||||
#: email_interface/templates/email_interface/testing/my_mails.html:60
|
||||
msgid "from"
|
||||
msgstr "von"
|
||||
|
||||
#: email_interface/templates/email_interface/testing/mail_thread.html:23
|
||||
#: email_interface/templates/email_interface/testing/my_mails.html:20
|
||||
msgid "No sender"
|
||||
msgstr "Kein Sender"
|
||||
|
||||
#: email_interface/templates/email_interface/testing/mail_thread.html:28
|
||||
#: email_interface/templates/email_interface/testing/my_mails.html:25
|
||||
msgid "to"
|
||||
msgstr "an"
|
||||
|
||||
#: email_interface/templates/email_interface/testing/mail_thread.html:32
|
||||
#: email_interface/templates/email_interface/testing/my_mails.html:29
|
||||
msgid "No receiver"
|
||||
msgstr "Kein Empfänger"
|
||||
|
||||
#: email_interface/templates/email_interface/testing/mail_thread.html:58
|
||||
#: email_interface/templates/email_interface/testing/my_mails.html:51
|
||||
msgid "Answers"
|
||||
msgstr "Antworten"
|
||||
|
||||
#: email_interface/templates/email_interface/testing/mail_thread.html:66
|
||||
#: email_interface/templates/email_interface/testing/my_mails.html:58
|
||||
msgid "Siblings"
|
||||
msgstr "Weitere Antworten"
|
||||
|
||||
#: email_interface/templates/email_interface/testing/mail_thread.html:79
|
||||
#: email_interface/templates/email_interface/testing/write_mail.html:17
|
||||
msgid "Send"
|
||||
msgstr "Senden"
|
||||
|
||||
#: email_interface/templates/email_interface/testing/my_attachments.html:9
|
||||
#| msgid "Attachments"
|
||||
msgid "All my attachments."
|
||||
msgstr "Alle meine Anhänge."
|
||||
|
||||
#: email_interface/templates/email_interface/testing/my_attachments.html:18
|
||||
msgid "Name"
|
||||
msgstr "Name"
|
||||
|
||||
#: email_interface/templates/email_interface/testing/my_attachments.html:19
|
||||
msgid "MIME Type"
|
||||
msgstr "MIME Typ"
|
||||
|
||||
#: email_interface/templates/email_interface/testing/my_mails.html:6
|
||||
#: email_interface/templates/email_interface/testing/my_mails.html:7
|
||||
#: email_interface/templates/email_interface/testing/my_mails.html:8
|
||||
#| msgid "emails/"
|
||||
msgid "Mails"
|
||||
msgstr "emails/"
|
||||
|
||||
#: email_interface/templates/email_interface/testing/my_mails.html:9
|
||||
msgid "All my mails."
|
||||
msgstr "Alle meine eMails."
|
||||
|
||||
#: email_interface/templates/email_interface/testing/write_mail.html:5
|
||||
#: email_interface/templates/email_interface/testing/write_mail.html:6
|
||||
#: email_interface/templates/email_interface/testing/write_mail.html:7
|
||||
msgid "Send mail demo"
|
||||
msgstr "Sende eMail Demo"
|
||||
|
||||
#: email_interface/templates/email_interface/testing/write_mail.html:9
|
||||
msgid "A send mail demo form."
|
||||
msgstr "Ein Beispielformular um eMails zu senden."
|
||||
|
||||
#: email_interface/views.py:57 email_interface/views.py:113
|
||||
msgid "Mail was sent."
|
||||
msgstr "Die eMail wurde gesendet."
|
||||
|
||||
#: email_interface/views.py:60 email_interface/views.py:115
|
||||
msgid "Sending of mail failed!"
|
||||
msgstr "Senden der eMail ist fehlgeschlagen!"
|
||||
|
||||
#: one_time_login/models.py:17
|
||||
msgid "Secret"
|
||||
msgstr "Geheimnis"
|
||||
|
|
@ -27,10 +194,6 @@ msgstr "Geheimnis"
|
|||
msgid "URL"
|
||||
msgstr "URL"
|
||||
|
||||
#: one_time_login/models.py:20
|
||||
msgid "Creation date"
|
||||
msgstr "Erstellungsdatum"
|
||||
|
||||
#: one_time_login/models.py:22
|
||||
msgid "Was the login used?"
|
||||
msgstr "Wurde der Link bereits verwendet?"
|
||||
|
|
@ -84,35 +247,38 @@ msgstr ""
|
|||
msgid "one time login"
|
||||
msgstr "Einmal-Anmeldung"
|
||||
|
||||
#: one_time_login/utils.py:76
|
||||
#: one_time_login/utils.py:81
|
||||
msgid "A new login link was sent by mail."
|
||||
msgstr "Eine neue Einmal-Anmeldung wurde per eMail gesendet."
|
||||
|
||||
#: one_time_login/utils.py:79
|
||||
#: one_time_login/utils.py:84
|
||||
msgid "Sending of one time login link failed!"
|
||||
msgstr "Senden der Einmal-Anmeldung ist fehlgeschlagen!"
|
||||
|
||||
#: one_time_login/utils.py:90
|
||||
#: one_time_login/utils.py:99
|
||||
msgid "Login was used already."
|
||||
msgstr "Die Einmal-Anmeldung wurde bereits verwendet."
|
||||
|
||||
#: one_time_login/views.py:25
|
||||
#: one_time_login/views.py:34
|
||||
msgid "Login successful!"
|
||||
msgstr "Die Anmeldung war erfolgreich."
|
||||
|
||||
#: one_time_login/views.py:28
|
||||
#: one_time_login/views.py:41
|
||||
msgid "Login failed!"
|
||||
msgstr "Die Anmeldung ist fehlgeschlagen!"
|
||||
|
||||
#: repapp/settings.py:179
|
||||
#: repapp/settings.py:191
|
||||
msgid "German"
|
||||
msgstr "Deutsch"
|
||||
|
||||
#: repapp/settings.py:180
|
||||
#: repapp/settings.py:192
|
||||
msgid "English"
|
||||
msgstr "Englisch"
|
||||
|
||||
#: repapp/urls.py:24
|
||||
#| msgid "one time login"
|
||||
#: repapp/urls.py:29
|
||||
msgid "one_time_login/"
|
||||
msgstr "einmal-anmeldung/"
|
||||
|
||||
#: repapp/urls.py:31
|
||||
msgid "emails/"
|
||||
msgstr "emails/"
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-08-29 09:17+0200\n"
|
||||
"PO-Revision-Date: 2023-08-29 09:18+0200\n"
|
||||
"POT-Creation-Date: 2023-08-30 21:12+0200\n"
|
||||
"PO-Revision-Date: 2023-08-30 21:14+0200\n"
|
||||
"Last-Translator: <thomas@irgang.eu>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
|
|
@ -19,6 +19,173 @@ msgstr ""
|
|||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Translated-Using: django-rosetta 0.9.9\n"
|
||||
|
||||
#: email_interface/forms.py:14 email_interface/models.py:18
|
||||
msgid "Text content"
|
||||
msgstr "Text content"
|
||||
|
||||
#: email_interface/forms.py:16
|
||||
msgid "Use text content?"
|
||||
msgstr "Use text content?"
|
||||
|
||||
#: email_interface/forms.py:29
|
||||
msgid "Mail text cannot be empty if it is used!"
|
||||
msgstr "Mail text cannot be empty if it is used!"
|
||||
|
||||
#: email_interface/models.py:16
|
||||
msgid "Summary"
|
||||
msgstr "Summary"
|
||||
|
||||
#: email_interface/models.py:17
|
||||
msgid "HTML content"
|
||||
msgstr "HTML content"
|
||||
|
||||
#: email_interface/models.py:20 one_time_login/models.py:20
|
||||
msgid "Creation date"
|
||||
msgstr "Creation date"
|
||||
|
||||
#: email_interface/models.py:22
|
||||
msgid "Receiver"
|
||||
msgstr "Receiver"
|
||||
|
||||
#: email_interface/models.py:24
|
||||
msgid "Sender"
|
||||
msgstr "Sender"
|
||||
|
||||
#: email_interface/models.py:26
|
||||
#: email_interface/templates/email_interface/testing/mail_thread.html:74
|
||||
msgid "Reply to"
|
||||
msgstr "Reply to"
|
||||
|
||||
#: email_interface/models.py:27
|
||||
msgid "sent?"
|
||||
msgstr "sent?"
|
||||
|
||||
#: email_interface/models.py:28
|
||||
msgid "answered?"
|
||||
msgstr "answered?"
|
||||
|
||||
#: email_interface/models.py:31 email_interface/models.py:84
|
||||
#: email_interface/templates/email_interface/testing/my_attachments.html:21
|
||||
msgid "Message"
|
||||
msgstr "Message"
|
||||
|
||||
#: email_interface/models.py:32
|
||||
msgid "Messages"
|
||||
msgstr "Messages"
|
||||
|
||||
#: email_interface/models.py:76
|
||||
msgid "Owner"
|
||||
msgstr "Owner"
|
||||
|
||||
#: email_interface/models.py:78
|
||||
msgid "File name"
|
||||
msgstr "File name"
|
||||
|
||||
#: email_interface/models.py:80
|
||||
msgid "MIME type"
|
||||
msgstr "MIME type"
|
||||
|
||||
#: email_interface/models.py:82
|
||||
#: email_interface/templates/email_interface/testing/my_attachments.html:20
|
||||
msgid "File"
|
||||
msgstr "File"
|
||||
|
||||
#: email_interface/models.py:87
|
||||
msgid "Attachment"
|
||||
msgstr "Attachment"
|
||||
|
||||
#: email_interface/models.py:88
|
||||
#: email_interface/templates/email_interface/testing/my_attachments.html:6
|
||||
#: email_interface/templates/email_interface/testing/my_attachments.html:7
|
||||
#: email_interface/templates/email_interface/testing/my_attachments.html:8
|
||||
msgid "Attachments"
|
||||
msgstr "Attachments"
|
||||
|
||||
#: email_interface/templates/email_interface/testing/mail_thread.html:6
|
||||
#: email_interface/templates/email_interface/testing/mail_thread.html:8
|
||||
#: email_interface/templates/email_interface/testing/mail_thread.html:10
|
||||
#: email_interface/templates/email_interface/testing/mail_thread.html:12
|
||||
#: email_interface/templates/email_interface/testing/mail_thread.html:60
|
||||
#: email_interface/templates/email_interface/testing/mail_thread.html:68
|
||||
#: email_interface/templates/email_interface/testing/mail_thread.html:74
|
||||
#: email_interface/templates/email_interface/testing/my_attachments.html:40
|
||||
#: email_interface/templates/email_interface/testing/my_mails.html:53
|
||||
#: email_interface/templates/email_interface/testing/my_mails.html:60
|
||||
msgid "from"
|
||||
msgstr "from"
|
||||
|
||||
#: email_interface/templates/email_interface/testing/mail_thread.html:23
|
||||
#: email_interface/templates/email_interface/testing/my_mails.html:20
|
||||
msgid "No sender"
|
||||
msgstr "No sender"
|
||||
|
||||
#: email_interface/templates/email_interface/testing/mail_thread.html:28
|
||||
#: email_interface/templates/email_interface/testing/my_mails.html:25
|
||||
msgid "to"
|
||||
msgstr "to"
|
||||
|
||||
#: email_interface/templates/email_interface/testing/mail_thread.html:32
|
||||
#: email_interface/templates/email_interface/testing/my_mails.html:29
|
||||
msgid "No receiver"
|
||||
msgstr "No receiver"
|
||||
|
||||
#: email_interface/templates/email_interface/testing/mail_thread.html:58
|
||||
#: email_interface/templates/email_interface/testing/my_mails.html:51
|
||||
msgid "Answers"
|
||||
msgstr "Answers"
|
||||
|
||||
#: email_interface/templates/email_interface/testing/mail_thread.html:66
|
||||
#: email_interface/templates/email_interface/testing/my_mails.html:58
|
||||
msgid "Siblings"
|
||||
msgstr "Further answers"
|
||||
|
||||
#: email_interface/templates/email_interface/testing/mail_thread.html:79
|
||||
#: email_interface/templates/email_interface/testing/write_mail.html:17
|
||||
msgid "Send"
|
||||
msgstr "Send"
|
||||
|
||||
#: email_interface/templates/email_interface/testing/my_attachments.html:9
|
||||
#| msgid "Attachments"
|
||||
msgid "All my attachments."
|
||||
msgstr "All my attachments."
|
||||
|
||||
#: email_interface/templates/email_interface/testing/my_attachments.html:18
|
||||
msgid "Name"
|
||||
msgstr "Name"
|
||||
|
||||
#: email_interface/templates/email_interface/testing/my_attachments.html:19
|
||||
msgid "MIME Type"
|
||||
msgstr "MIME type"
|
||||
|
||||
#: email_interface/templates/email_interface/testing/my_mails.html:6
|
||||
#: email_interface/templates/email_interface/testing/my_mails.html:7
|
||||
#: email_interface/templates/email_interface/testing/my_mails.html:8
|
||||
#| msgid "emails/"
|
||||
msgid "Mails"
|
||||
msgstr "emails/"
|
||||
|
||||
#: email_interface/templates/email_interface/testing/my_mails.html:9
|
||||
msgid "All my mails."
|
||||
msgstr "All my mails."
|
||||
|
||||
#: email_interface/templates/email_interface/testing/write_mail.html:5
|
||||
#: email_interface/templates/email_interface/testing/write_mail.html:6
|
||||
#: email_interface/templates/email_interface/testing/write_mail.html:7
|
||||
msgid "Send mail demo"
|
||||
msgstr "Send mail demo"
|
||||
|
||||
#: email_interface/templates/email_interface/testing/write_mail.html:9
|
||||
msgid "A send mail demo form."
|
||||
msgstr "A send mail demo form."
|
||||
|
||||
#: email_interface/views.py:57 email_interface/views.py:113
|
||||
msgid "Mail was sent."
|
||||
msgstr "Mail was sent."
|
||||
|
||||
#: email_interface/views.py:60 email_interface/views.py:115
|
||||
msgid "Sending of mail failed!"
|
||||
msgstr "Sending of mail failed!"
|
||||
|
||||
#: one_time_login/models.py:17
|
||||
msgid "Secret"
|
||||
msgstr "Secret"
|
||||
|
|
@ -27,10 +194,6 @@ msgstr "Secret"
|
|||
msgid "URL"
|
||||
msgstr "URL"
|
||||
|
||||
#: one_time_login/models.py:20
|
||||
msgid "Creation date"
|
||||
msgstr "Creation date"
|
||||
|
||||
#: one_time_login/models.py:22
|
||||
msgid "Was the login used?"
|
||||
msgstr "Was the login used?"
|
||||
|
|
@ -84,35 +247,38 @@ msgstr ""
|
|||
msgid "one time login"
|
||||
msgstr "one time login"
|
||||
|
||||
#: one_time_login/utils.py:76
|
||||
#: one_time_login/utils.py:81
|
||||
msgid "A new login link was sent by mail."
|
||||
msgstr "A new login link was sent by mail."
|
||||
|
||||
#: one_time_login/utils.py:79
|
||||
#: one_time_login/utils.py:84
|
||||
msgid "Sending of one time login link failed!"
|
||||
msgstr "Sending of one time login link failed!"
|
||||
|
||||
#: one_time_login/utils.py:90
|
||||
#: one_time_login/utils.py:99
|
||||
msgid "Login was used already."
|
||||
msgstr "Login was used already."
|
||||
|
||||
#: one_time_login/views.py:25
|
||||
#: one_time_login/views.py:34
|
||||
msgid "Login successful!"
|
||||
msgstr "Login successful!"
|
||||
|
||||
#: one_time_login/views.py:28
|
||||
#: one_time_login/views.py:41
|
||||
msgid "Login failed!"
|
||||
msgstr "Login failed!"
|
||||
|
||||
#: repapp/settings.py:179
|
||||
#: repapp/settings.py:191
|
||||
msgid "German"
|
||||
msgstr "German"
|
||||
|
||||
#: repapp/settings.py:180
|
||||
#: repapp/settings.py:192
|
||||
msgid "English"
|
||||
msgstr "English"
|
||||
|
||||
#: repapp/urls.py:24
|
||||
#| msgid "one time login"
|
||||
#: repapp/urls.py:29
|
||||
msgid "one_time_login/"
|
||||
msgstr "one-time-login/"
|
||||
|
||||
#: repapp/urls.py:31
|
||||
msgid "emails/"
|
||||
msgstr "emails/"
|
||||
|
|
|
|||
|
|
@ -7,6 +7,5 @@ class OneTimeLoginAdmin(admin.ModelAdmin):
|
|||
list_display = ['user', 'url', 'login_used', 'login_date', 'created']
|
||||
list_filter = ['user', 'url', 'login_used', 'login_date', 'created']
|
||||
search_fields = ['user', 'url']
|
||||
raw_id_fields = ['user']
|
||||
date_hierarchy = 'created'
|
||||
ordering = ['created', 'user', 'url']
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
"""
|
||||
Urls of RepApp.
|
||||
"""
|
||||
import sys
|
||||
import logging
|
||||
from django.urls import path
|
||||
from django.conf import settings
|
||||
from . import views
|
||||
|
||||
|
||||
|
|
@ -9,7 +12,11 @@ app_name = 'one_time_login'
|
|||
|
||||
|
||||
urlpatterns = [
|
||||
path("protected/", views.protected_test, name="protected"),
|
||||
path("dummy/", views.dummy_content, name="dummy"),
|
||||
path("<str:secret>/", views.login_view, name="login"),
|
||||
]
|
||||
|
||||
if settings.DEBUG or sys.argv[1:2] == ['test']:
|
||||
logging.info('one_time_login: enable protected test URL')
|
||||
urlpatterns += [
|
||||
path("protected/", views.protected_test, name="protected"),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/4.2/ref/settings/
|
|||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
|
@ -33,6 +34,7 @@ ALLOWED_HOSTS = []
|
|||
|
||||
INSTALLED_APPS = [
|
||||
"one_time_login.apps.OneTimeLoginConfig",
|
||||
"email_interface.apps.EmailInterfaceConfig",
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
|
|
@ -41,6 +43,14 @@ INSTALLED_APPS = [
|
|||
'django.contrib.staticfiles',
|
||||
# enable rosetta online translation interface
|
||||
'rosetta',
|
||||
# CKEditor for mail content editing
|
||||
"ckeditor",
|
||||
"ckeditor_uploader",
|
||||
# Crispy forms for bootstrap 5 form design
|
||||
"crispy_forms",
|
||||
"crispy_bootstrap5",
|
||||
# for creating thumbnails, used by email_interface demo views
|
||||
'easy_thumbnails',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
|
@ -121,7 +131,7 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
# STATIC_URL = 'static/'
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
|
||||
|
|
@ -154,6 +164,8 @@ LOGGING = {
|
|||
},
|
||||
}
|
||||
|
||||
logging.info('DEBUG is %b', DEBUG)
|
||||
|
||||
# Organization name
|
||||
# used by one time login
|
||||
ORGANIZATION = "Repair-Café Hilpoltstein"
|
||||
|
|
@ -195,3 +207,28 @@ if DEBUG:
|
|||
# Default backend for admin login
|
||||
"django.contrib.auth.backends.ModelBackend",
|
||||
]
|
||||
|
||||
# editor upload path, used e.g. for send mail demo
|
||||
CKEDITOR_UPLOAD_PATH = "editor_uploads/"
|
||||
|
||||
CKEDITOR_IMAGE_BACKEND = 'pillow'
|
||||
|
||||
# CKEditor default config
|
||||
CKEDITOR_CONFIGS = {
|
||||
'default': {
|
||||
'toolbar': 'full',
|
||||
'height': 300,
|
||||
'width': '100%',
|
||||
},
|
||||
}
|
||||
|
||||
# Django media and static file config, required for CKEditor
|
||||
STATIC_URL = "static/"
|
||||
MEDIA_URL = "media/"
|
||||
# Use subfolders of project folder
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, "static")
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
|
||||
|
||||
# Crispy forms settings
|
||||
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
|
||||
CRISPY_TEMPLATE_PACK = "bootstrap5"
|
||||
|
|
|
|||
|
|
@ -16,13 +16,28 @@ Including another URLconf
|
|||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.views.decorators.cache import never_cache
|
||||
from django.conf.urls.i18n import i18n_patterns
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from ckeditor_uploader import views as ckeditor_views
|
||||
|
||||
urlpatterns = i18n_patterns(
|
||||
path('admin/', admin.site.urls),
|
||||
path(_('one_time_login/'),
|
||||
include('one_time_login.urls', namespace='one_time_login')),
|
||||
path(_('emails/'),
|
||||
include('email_interface.urls', namespace='email_interface')),
|
||||
# URLs for rosetta translation interface
|
||||
path('rosetta/', include('rosetta.urls')),
|
||||
# CKEditor upload views
|
||||
path('upload/', login_required(ckeditor_views.upload), name="ckeditor_upload"),
|
||||
path('browse/', never_cache(login_required(ckeditor_views.browse)),
|
||||
name="ckeditor_browse"),
|
||||
)
|
||||
|
||||
if settings.DEBUG:
|
||||
urlpatterns += static(settings.MEDIA_URL,
|
||||
document_root=settings.MEDIA_ROOT)
|
||||
|
|
|
|||
|
|
@ -1,2 +1,6 @@
|
|||
Django==4.2
|
||||
django-rosetta==0.9.9
|
||||
easy-thumbnails==2.8.5
|
||||
django-ckeditor==6.5.1
|
||||
crispy-bootstrap5==0.7
|
||||
django-crispy-forms==2.0
|
||||
|
|
|
|||
Reference in a new issue