email interface without tests

This commit is contained in:
Tom Irgang 2023-08-30 21:19:36 +02:00
parent 49329a709e
commit 66bf165053
34 changed files with 1552 additions and 37 deletions

7
.gitignore vendored
View file

@ -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]

View file

@ -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)
```

View file

@ -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.

View file

View 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']

View 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)

View 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

View 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',
},
),
]

View file

@ -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'),
),
]

View file

@ -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'),
),
]

View file

@ -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'),
),
]

View 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)

View 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()

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -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>

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

Binary file not shown.

After

Width:  |  Height:  |  Size: 933 KiB

View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View 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"),
]

View 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

View 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),
}
)

View file

@ -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/"

View file

@ -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/"

View file

@ -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']

View file

@ -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"),
]

View file

@ -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"

View file

@ -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)

View file

@ -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