Black formatting

This commit is contained in:
Scot Hacker 2018-12-21 00:38:44 -08:00
parent f526ed5166
commit 21ec87cee4
10 changed files with 276 additions and 227 deletions

View file

@ -5,7 +5,7 @@ from setuptools import setup, find_packages
import todo as package import todo as package
setup( setup(
name='django-todo', name="django-todo",
version=package.__version__, version=package.__version__,
description=package.__doc__.strip(), description=package.__doc__.strip(),
author=package.__author__, author=package.__author__,
@ -14,18 +14,18 @@ setup(
license=package.__license__, license=package.__license__,
packages=find_packages(), packages=find_packages(),
classifiers=[ classifiers=[
'Development Status :: 5 - Production/Stable', "Development Status :: 5 - Production/Stable",
'Environment :: Web Environment', "Environment :: Web Environment",
'Framework :: Django', "Framework :: Django",
'Intended Audience :: Developers', "Intended Audience :: Developers",
'License :: OSI Approved :: BSD License', "License :: OSI Approved :: BSD License",
'Operating System :: OS Independent', "Operating System :: OS Independent",
'Programming Language :: Python', "Programming Language :: Python",
'Programming Language :: Python :: 3', "Programming Language :: Python :: 3",
'Topic :: Office/Business :: Groupware', "Topic :: Office/Business :: Groupware",
'Topic :: Software Development :: Bug Tracking', "Topic :: Software Development :: Bug Tracking",
], ],
include_package_data=True, include_package_data=True,
zip_safe=False, zip_safe=False,
install_requires=['unidecode', ], install_requires=["unidecode"],
) )

View file

@ -1,64 +1,63 @@
import os import os
DEBUG = True, DEBUG = (True,)
BASE_DIR = os.path.dirname(os.path.dirname(__file__)) BASE_DIR = os.path.dirname(os.path.dirname(__file__))
print("bd ", BASE_DIR)
DATABASES = { DATABASES = {
'default': { "default": {
'ENGINE': 'django.db.backends.sqlite3', "ENGINE": "django.db.backends.sqlite3"
} }
} }
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
# Document # Document
TODO_STAFF_ONLY = False TODO_STAFF_ONLY = False
TODO_DEFAULT_LIST_SLUG = 'tickets' TODO_DEFAULT_LIST_SLUG = "tickets"
TODO_DEFAULT_ASSIGNEE = None TODO_DEFAULT_ASSIGNEE = None
TODO_PUBLIC_SUBMIT_REDIRECT = '/' TODO_PUBLIC_SUBMIT_REDIRECT = "/"
SECRET_KEY = "LKFSD8sdl.,8&sdf--" SECRET_KEY = "LKFSD8sdl.,8&sdf--"
SITE_ID = 1 SITE_ID = 1
INSTALLED_APPS = ( INSTALLED_APPS = (
'django.contrib.admin', "django.contrib.admin",
'django.contrib.auth', "django.contrib.auth",
'django.contrib.contenttypes', "django.contrib.contenttypes",
'django.contrib.messages', "django.contrib.messages",
'django.contrib.sessions', "django.contrib.sessions",
'django.contrib.sites', "django.contrib.sites",
'django.contrib.staticfiles', "django.contrib.staticfiles",
'todo', "todo",
) )
ROOT_URLCONF = 'base_urls' ROOT_URLCONF = "base_urls"
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', "django.middleware.security.SecurityMiddleware",
'django.contrib.sessions.middleware.SessionMiddleware', "django.contrib.sessions.middleware.SessionMiddleware",
'django.middleware.common.CommonMiddleware', "django.middleware.common.CommonMiddleware",
'django.middleware.csrf.CsrfViewMiddleware', "django.middleware.csrf.CsrfViewMiddleware",
'django.contrib.auth.middleware.AuthenticationMiddleware', "django.contrib.auth.middleware.AuthenticationMiddleware",
'django.contrib.messages.middleware.MessageMiddleware', "django.contrib.messages.middleware.MessageMiddleware",
'django.middleware.clickjacking.XFrameOptionsMiddleware', "django.middleware.clickjacking.XFrameOptionsMiddleware",
] ]
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', "BACKEND": "django.template.backends.django.DjangoTemplates",
'DIRS': [os.path.join(BASE_DIR, 'todo', 'templates'), ], "DIRS": [os.path.join(BASE_DIR, "todo", "templates")],
'APP_DIRS': True, "APP_DIRS": True,
'OPTIONS': { "OPTIONS": {
'context_processors': [ "context_processors": [
'django.template.context_processors.debug', "django.template.context_processors.debug",
'django.template.context_processors.request', "django.template.context_processors.request",
'django.contrib.auth.context_processors.auth', "django.contrib.auth.context_processors.auth",
'django.template.context_processors.media', "django.template.context_processors.media",
'django.template.context_processors.static', "django.template.context_processors.static",
'django.contrib.messages.context_processors.messages', "django.contrib.messages.context_processors.messages",
# Your stuff: custom template context processors go here # Your stuff: custom template context processors go here
], ]
},
}, },
}
] ]

View file

@ -3,14 +3,14 @@ from todo.models import Task, TaskList, Comment
class TaskAdmin(admin.ModelAdmin): class TaskAdmin(admin.ModelAdmin):
list_display = ('title', 'task_list', 'completed', 'priority', 'due_date') list_display = ("title", "task_list", "completed", "priority", "due_date")
list_filter = ('task_list',) list_filter = ("task_list",)
ordering = ('priority',) ordering = ("priority",)
search_fields = ('name',) search_fields = ("name",)
class CommentAdmin(admin.ModelAdmin): class CommentAdmin(admin.ModelAdmin):
list_display = ('author', 'date', 'snippet') list_display = ("author", "date", "snippet")
admin.site.register(TaskList) admin.site.register(TaskList)

View file

@ -11,13 +11,16 @@ class AddTaskListForm(ModelForm):
def __init__(self, user, *args, **kwargs): def __init__(self, user, *args, **kwargs):
super(AddTaskListForm, self).__init__(*args, **kwargs) super(AddTaskListForm, self).__init__(*args, **kwargs)
self.fields['group'].queryset = Group.objects.filter(user=user) self.fields["group"].queryset = Group.objects.filter(user=user)
self.fields['group'].widget.attrs = { self.fields["group"].widget.attrs = {
'id': 'id_group', 'class': "custom-select mb-3", 'name': 'group'} "id": "id_group",
"class": "custom-select mb-3",
"name": "group",
}
class Meta: class Meta:
model = TaskList model = TaskList
exclude = ['created_date', 'slug', ] exclude = ["created_date", "slug"]
class AddEditTaskForm(ModelForm): class AddEditTaskForm(ModelForm):
@ -26,22 +29,25 @@ class AddEditTaskForm(ModelForm):
def __init__(self, user, *args, **kwargs): def __init__(self, user, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
task_list = kwargs.get('initial').get('task_list') task_list = kwargs.get("initial").get("task_list")
members = task_list.group.user_set.all() members = task_list.group.user_set.all()
self.fields['assigned_to'].queryset = members self.fields["assigned_to"].queryset = members
self.fields['assigned_to'].label_from_instance = lambda obj: "%s (%s)" % (obj.get_full_name(), obj.username) self.fields["assigned_to"].label_from_instance = lambda obj: "%s (%s)" % (
self.fields['assigned_to'].widget.attrs = { obj.get_full_name(),
'id': 'id_assigned_to', 'class': "custom-select mb-3", 'name': 'assigned_to'} obj.username,
self.fields['task_list'].value = kwargs['initial']['task_list'].id )
self.fields["assigned_to"].widget.attrs = {
"id": "id_assigned_to",
"class": "custom-select mb-3",
"name": "assigned_to",
}
self.fields["task_list"].value = kwargs["initial"]["task_list"].id
due_date = forms.DateField( due_date = forms.DateField(widget=forms.DateInput(attrs={"type": "date"}), required=False)
widget=forms.DateInput(attrs={'type': 'date'}), required=False)
title = forms.CharField( title = forms.CharField(widget=forms.widgets.TextInput())
widget=forms.widgets.TextInput())
note = forms.CharField( note = forms.CharField(widget=forms.Textarea(), required=False)
widget=forms.Textarea(), required=False)
class Meta: class Meta:
model = Task model = Task
@ -51,27 +57,24 @@ class AddEditTaskForm(ModelForm):
class AddExternalTaskForm(ModelForm): class AddExternalTaskForm(ModelForm):
"""Form to allow users who are not part of the GTD system to file a ticket.""" """Form to allow users who are not part of the GTD system to file a ticket."""
title = forms.CharField( title = forms.CharField(widget=forms.widgets.TextInput(attrs={"size": 35}), label="Summary")
widget=forms.widgets.TextInput(attrs={'size': 35}), note = forms.CharField(widget=forms.widgets.Textarea(), label="Problem Description")
label="Summary" priority = forms.IntegerField(widget=forms.HiddenInput())
)
note = forms.CharField(
widget=forms.widgets.Textarea(),
label='Problem Description',
)
priority = forms.IntegerField(
widget=forms.HiddenInput(),
)
class Meta: class Meta:
model = Task model = Task
exclude = ( exclude = (
'task_list', 'created_date', 'due_date', 'created_by', 'assigned_to', 'completed', 'completed_date', ) "task_list",
"created_date",
"due_date",
"created_by",
"assigned_to",
"completed",
"completed_date",
)
class SearchForm(forms.Form): class SearchForm(forms.Form):
"""Search.""" """Search."""
q = forms.CharField( q = forms.CharField(widget=forms.widgets.TextInput(attrs={"size": 35}))
widget=forms.widgets.TextInput(attrs={'size': 35})
)

View file

@ -10,7 +10,7 @@ from django.utils import timezone
class TaskList(models.Model): class TaskList(models.Model):
name = models.CharField(max_length=60) name = models.CharField(max_length=60)
slug = models.SlugField(default='',) slug = models.SlugField(default="")
group = models.ForeignKey(Group, on_delete=models.CASCADE) group = models.ForeignKey(Group, on_delete=models.CASCADE)
def __str__(self): def __str__(self):
@ -28,12 +28,19 @@ class Task(models.Model):
title = models.CharField(max_length=140) title = models.CharField(max_length=140)
task_list = models.ForeignKey(TaskList, on_delete=models.CASCADE, null=True) task_list = models.ForeignKey(TaskList, on_delete=models.CASCADE, null=True)
created_date = models.DateField(default=timezone.now, blank=True, null=True) created_date = models.DateField(default=timezone.now, blank=True, null=True)
due_date = models.DateField(blank=True, null=True, ) due_date = models.DateField(blank=True, null=True)
completed = models.BooleanField(default=False) completed = models.BooleanField(default=False)
completed_date = models.DateField(blank=True, null=True) completed_date = models.DateField(blank=True, null=True)
created_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='todo_created_by', on_delete=models.CASCADE) created_by = models.ForeignKey(
settings.AUTH_USER_MODEL, related_name="todo_created_by", on_delete=models.CASCADE
)
assigned_to = models.ForeignKey( assigned_to = models.ForeignKey(
settings.AUTH_USER_MODEL, blank=True, null=True, related_name='todo_assigned_to', on_delete=models.CASCADE) settings.AUTH_USER_MODEL,
blank=True,
null=True,
related_name="todo_assigned_to",
on_delete=models.CASCADE,
)
note = models.TextField(blank=True, null=True) note = models.TextField(blank=True, null=True)
priority = models.PositiveIntegerField() priority = models.PositiveIntegerField()
@ -47,7 +54,7 @@ class Task(models.Model):
return self.title return self.title
def get_absolute_url(self): def get_absolute_url(self):
return reverse('todo:task_detail', kwargs={'task_id': self.id, }) return reverse("todo:task_detail", kwargs={"task_id": self.id})
# Auto-set the Task creation / completed date # Auto-set the Task creation / completed date
def save(self, **kwargs): def save(self, **kwargs):
@ -65,6 +72,7 @@ class Comment(models.Model):
Not using Django's built-in comments because we want to be able to save Not using Django's built-in comments because we want to be able to save
a comment and change task details at the same time. Rolling our own since it's easy. a comment and change task details at the same time. Rolling our own since it's easy.
""" """
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
task = models.ForeignKey(Task, on_delete=models.CASCADE) task = models.ForeignKey(Task, on_delete=models.CASCADE)
date = models.DateTimeField(default=datetime.datetime.now) date = models.DateTimeField(default=datetime.datetime.now)

View file

@ -5,13 +5,14 @@ from django.contrib.auth.models import Group
from todo.models import Task, TaskList from todo.models import Task, TaskList
@pytest.fixture @pytest.fixture
def todo_setup(django_user_model): def todo_setup(django_user_model):
# Two groups with different users, two sets of tasks. # Two groups with different users, two sets of tasks.
g1 = Group.objects.create(name="Workgroup One") g1 = Group.objects.create(name="Workgroup One")
u1 = django_user_model.objects.create_user(username="u1", password="password", email="u1@example.com") u1 = django_user_model.objects.create_user(
username="u1", password="password", email="u1@example.com"
)
u1.groups.add(g1) u1.groups.add(g1)
tlist1 = TaskList.objects.create(group=g1, name="Zip", slug="zip") tlist1 = TaskList.objects.create(group=g1, name="Zip", slug="zip")
Task.objects.create(created_by=u1, title="Task 1", task_list=tlist1, priority=1) Task.objects.create(created_by=u1, title="Task 1", task_list=tlist1, priority=1)
@ -19,7 +20,9 @@ def todo_setup(django_user_model):
Task.objects.create(created_by=u1, title="Task 3", task_list=tlist1, priority=3) Task.objects.create(created_by=u1, title="Task 3", task_list=tlist1, priority=3)
g2 = Group.objects.create(name="Workgroup Two") g2 = Group.objects.create(name="Workgroup Two")
u2 = django_user_model.objects.create_user(username="u2", password="password", email="u2@example.com") u2 = django_user_model.objects.create_user(
username="u2", password="password", email="u2@example.com"
)
u2.groups.add(g2) u2.groups.add(g2)
tlist2 = TaskList.objects.create(group=g2, name="Zap", slug="zap") tlist2 = TaskList.objects.create(group=g2, name="Zap", slug="zap")
Task.objects.create(created_by=u2, title="Task 1", task_list=tlist2, priority=1) Task.objects.create(created_by=u2, title="Task 1", task_list=tlist2, priority=1)

View file

@ -9,7 +9,7 @@ from todo.utils import send_notify_mail, send_email_to_thread_participants
@pytest.fixture() @pytest.fixture()
# Set up an in-memory mail server to receive test emails # Set up an in-memory mail server to receive test emails
def email_backend_setup(settings): def email_backend_setup(settings):
settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
def test_send_notify_mail_not_me(todo_setup, django_user_model, email_backend_setup): def test_send_notify_mail_not_me(todo_setup, django_user_model, email_backend_setup):
@ -46,13 +46,17 @@ def test_send_email_to_thread_participants(todo_setup, django_user_model, email_
u1 = django_user_model.objects.get(username="u1") u1 = django_user_model.objects.get(username="u1")
task = Task.objects.filter(created_by=u1).first() task = Task.objects.filter(created_by=u1).first()
u3 = django_user_model.objects.create_user(username="u3", password="zzz", email="u3@example.com") u3 = django_user_model.objects.create_user(
u4 = django_user_model.objects.create_user(username="u4", password="zzz", email="u4@example.com") username="u3", password="zzz", email="u3@example.com"
Comment.objects.create(author=u3, task=task, body="Hello", ) )
Comment.objects.create(author=u4, task=task, body="Hello", ) u4 = django_user_model.objects.create_user(
username="u4", password="zzz", email="u4@example.com"
)
Comment.objects.create(author=u3, task=task, body="Hello")
Comment.objects.create(author=u4, task=task, body="Hello")
send_email_to_thread_participants(task, "test body", u1) send_email_to_thread_participants(task, "test body", u1)
assert len(mail.outbox) == 1 # One message to multiple recipients assert len(mail.outbox) == 1 # One message to multiple recipients
assert 'u1@example.com' in mail.outbox[0].recipients() assert "u1@example.com" in mail.outbox[0].recipients()
assert 'u3@example.com' in mail.outbox[0].recipients() assert "u3@example.com" in mail.outbox[0].recipients()
assert 'u4@example.com' in mail.outbox[0].recipients() assert "u4@example.com" in mail.outbox[0].recipients()

View file

@ -16,19 +16,20 @@ After that, view contents and behaviors.
# ### SMOKETESTS ### # ### SMOKETESTS ###
@pytest.mark.django_db @pytest.mark.django_db
def test_todo_setup(todo_setup): def test_todo_setup(todo_setup):
assert Task.objects.all().count() == 6 assert Task.objects.all().count() == 6
def test_view_list_lists(todo_setup, admin_client): def test_view_list_lists(todo_setup, admin_client):
url = reverse('todo:lists') url = reverse("todo:lists")
response = admin_client.get(url) response = admin_client.get(url)
assert response.status_code == 200 assert response.status_code == 200
def test_view_reorder(todo_setup, admin_client): def test_view_reorder(todo_setup, admin_client):
url = reverse('todo:reorder_tasks') url = reverse("todo:reorder_tasks")
response = admin_client.get(url) response = admin_client.get(url)
assert response.status_code == 201 # Special case return value expected assert response.status_code == 201 # Special case return value expected
@ -37,53 +38,55 @@ def test_view_external_add(todo_setup, admin_client, settings):
default_list = TaskList.objects.first() default_list = TaskList.objects.first()
settings.TODO_DEFAULT_LIST_SLUG = default_list.slug settings.TODO_DEFAULT_LIST_SLUG = default_list.slug
assert settings.TODO_DEFAULT_LIST_SLUG == default_list.slug assert settings.TODO_DEFAULT_LIST_SLUG == default_list.slug
url = reverse('todo:external_add') url = reverse("todo:external_add")
response = admin_client.get(url) response = admin_client.get(url)
assert response.status_code == 200 assert response.status_code == 200
def test_view_mine(todo_setup, admin_client): def test_view_mine(todo_setup, admin_client):
url = reverse('todo:mine') url = reverse("todo:mine")
response = admin_client.get(url) response = admin_client.get(url)
assert response.status_code == 200 assert response.status_code == 200
def test_view_list_completed(todo_setup, admin_client): def test_view_list_completed(todo_setup, admin_client):
tlist = TaskList.objects.get(slug="zip") tlist = TaskList.objects.get(slug="zip")
url = reverse('todo:list_detail_completed', kwargs={'list_id': tlist.id, 'list_slug': tlist.slug}) url = reverse(
"todo:list_detail_completed", kwargs={"list_id": tlist.id, "list_slug": tlist.slug}
)
response = admin_client.get(url) response = admin_client.get(url)
assert response.status_code == 200 assert response.status_code == 200
def test_view_list(todo_setup, admin_client): def test_view_list(todo_setup, admin_client):
tlist = TaskList.objects.get(slug="zip") tlist = TaskList.objects.get(slug="zip")
url = reverse('todo:list_detail', kwargs={'list_id': tlist.id, 'list_slug': tlist.slug}) url = reverse("todo:list_detail", kwargs={"list_id": tlist.id, "list_slug": tlist.slug})
response = admin_client.get(url) response = admin_client.get(url)
assert response.status_code == 200 assert response.status_code == 200
def test_del_list(todo_setup, admin_client): def test_del_list(todo_setup, admin_client):
tlist = TaskList.objects.get(slug="zip") tlist = TaskList.objects.get(slug="zip")
url = reverse('todo:del_list', kwargs={'list_id': tlist.id, 'list_slug': tlist.slug}) url = reverse("todo:del_list", kwargs={"list_id": tlist.id, "list_slug": tlist.slug})
response = admin_client.get(url) response = admin_client.get(url)
assert response.status_code == 200 assert response.status_code == 200
def test_view_add_list(todo_setup, admin_client): def test_view_add_list(todo_setup, admin_client):
url = reverse('todo:add_list') url = reverse("todo:add_list")
response = admin_client.get(url) response = admin_client.get(url)
assert response.status_code == 200 assert response.status_code == 200
def test_view_task_detail(todo_setup, admin_client): def test_view_task_detail(todo_setup, admin_client):
task = Task.objects.first() task = Task.objects.first()
url = reverse('todo:task_detail', kwargs={'task_id': task.id}) url = reverse("todo:task_detail", kwargs={"task_id": task.id})
response = admin_client.get(url) response = admin_client.get(url)
assert response.status_code == 200 assert response.status_code == 200
def test_view_search(todo_setup, admin_client): def test_view_search(todo_setup, admin_client):
url = reverse('todo:search') url = reverse("todo:search")
response = admin_client.get(url) response = admin_client.get(url)
assert response.status_code == 200 assert response.status_code == 200
@ -100,11 +103,11 @@ def test_no_javascript_in_task_note(todo_setup, client):
"priority": 10, "priority": 10,
"title": title, "title": title,
"note": note, "note": note,
'add_edit_task': 'Submit' "add_edit_task": "Submit",
} }
client.login(username='u2', password="password") client.login(username="u2", password="password")
url = reverse('todo:list_detail', kwargs={"list_id": task_list.id, "list_slug": task_list.slug}) url = reverse("todo:list_detail", kwargs={"list_id": task_list.id, "list_slug": task_list.slug})
response = client.post(url, data) response = client.post(url, data)
assert response.status_code == 302 assert response.status_code == 302
@ -118,7 +121,7 @@ def test_no_javascript_in_task_note(todo_setup, client):
@pytest.mark.django_db @pytest.mark.django_db
def test_no_javascript_in_comments(todo_setup, client): def test_no_javascript_in_comments(todo_setup, client):
user = get_user_model().objects.get(username="u2") user = get_user_model().objects.get(username="u2")
client.login(username='u2', password="password") client.login(username="u2", password="password")
task = Task.objects.first() task = Task.objects.first()
task.created_by = user task.created_by = user
@ -127,11 +130,8 @@ def test_no_javascript_in_comments(todo_setup, client):
user.groups.add(task.task_list.group) user.groups.add(task.task_list.group)
comment = "foo <script>alert('oh noez');</script> bar" comment = "foo <script>alert('oh noez');</script> bar"
data = { data = {"comment-body": comment, "add_comment": "Submit"}
"comment-body": comment, url = reverse("todo:task_detail", kwargs={"task_id": task.id})
"add_comment": 'Submit'
}
url = reverse('todo:task_detail', kwargs={"task_id": task.id})
response = client.post(url, data) response = client.post(url, data)
assert response.status_code == 200 assert response.status_code == 200
@ -152,7 +152,7 @@ These exercise our custom @staff_only decorator without calling that function ex
def test_view_add_list_nonadmin(todo_setup, client): def test_view_add_list_nonadmin(todo_setup, client):
url = reverse('todo:add_list') url = reverse("todo:add_list")
client.login(username="you", password="password") client.login(username="you", password="password")
response = client.get(url) response = client.get(url)
assert response.status_code == 403 assert response.status_code == 403
@ -160,7 +160,7 @@ def test_view_add_list_nonadmin(todo_setup, client):
def test_view_del_list_nonadmin(todo_setup, client): def test_view_del_list_nonadmin(todo_setup, client):
tlist = TaskList.objects.get(slug="zip") tlist = TaskList.objects.get(slug="zip")
url = reverse('todo:del_list', kwargs={'list_id': tlist.id, 'list_slug': tlist.slug}) url = reverse("todo:del_list", kwargs={"list_id": tlist.id, "list_slug": tlist.slug})
client.login(username="you", password="password") client.login(username="you", password="password")
response = client.get(url) response = client.get(url)
assert response.status_code == 403 assert response.status_code == 403
@ -170,7 +170,7 @@ def test_view_list_mine(todo_setup, client):
"""View a list in a group I belong to. """View a list in a group I belong to.
""" """
tlist = TaskList.objects.get(slug="zip") # User u1 is in this group's list tlist = TaskList.objects.get(slug="zip") # User u1 is in this group's list
url = reverse('todo:list_detail', kwargs={'list_id': tlist.id, 'list_slug': tlist.slug}) url = reverse("todo:list_detail", kwargs={"list_id": tlist.id, "list_slug": tlist.slug})
client.login(username="u1", password="password") client.login(username="u1", password="password")
response = client.get(url) response = client.get(url)
assert response.status_code == 200 assert response.status_code == 200
@ -180,7 +180,7 @@ def test_view_list_not_mine(todo_setup, client):
"""View a list in a group I don't belong to. """View a list in a group I don't belong to.
""" """
tlist = TaskList.objects.get(slug="zip") # User u1 is in this group, user u2 is not. tlist = TaskList.objects.get(slug="zip") # User u1 is in this group, user u2 is not.
url = reverse('todo:list_detail', kwargs={'list_id': tlist.id, 'list_slug': tlist.slug}) url = reverse("todo:list_detail", kwargs={"list_id": tlist.id, "list_slug": tlist.slug})
client.login(username="u2", password="password") client.login(username="u2", password="password")
response = client.get(url) response = client.get(url)
assert response.status_code == 403 assert response.status_code == 403
@ -190,7 +190,7 @@ def test_view_task_mine(todo_setup, client):
# Users can always view their own tasks # Users can always view their own tasks
task = Task.objects.filter(created_by__username="u1").first() task = Task.objects.filter(created_by__username="u1").first()
client.login(username="u1", password="password") client.login(username="u1", password="password")
url = reverse('todo:task_detail', kwargs={'task_id': task.id}) url = reverse("todo:task_detail", kwargs={"task_id": task.id})
response = client.get(url) response = client.get(url)
assert response.status_code == 200 assert response.status_code == 200
@ -205,7 +205,7 @@ def test_view_task_my_group(todo_setup, client, django_user_model):
# Now u2 should be able to view one of u1's tasks. # Now u2 should be able to view one of u1's tasks.
task = Task.objects.filter(created_by__username="u1").first() task = Task.objects.filter(created_by__username="u1").first()
url = reverse('todo:task_detail', kwargs={'task_id': task.id}) url = reverse("todo:task_detail", kwargs={"task_id": task.id})
client.login(username="u2", password="password") client.login(username="u2", password="password")
response = client.get(url) response = client.get(url)
assert response.status_code == 200 assert response.status_code == 200
@ -215,7 +215,8 @@ def test_view_task_not_in_my_group(todo_setup, client):
# User canNOT view a task that isn't theirs if the two users are not in a shared group. # User canNOT view a task that isn't theirs if the two users are not in a shared group.
# For this we can use the fixture data as-is. # For this we can use the fixture data as-is.
task = Task.objects.filter(created_by__username="u1").first() task = Task.objects.filter(created_by__username="u1").first()
url = reverse('todo:task_detail', kwargs={'task_id': task.id}) url = reverse("todo:task_detail", kwargs={"task_id": task.id})
client.login(username="u2", password="password") client.login(username="u2", password="password")
response = client.get(url) response = client.get(url)
assert response.status_code == 403 assert response.status_code == 403

View file

@ -11,24 +11,30 @@ def send_notify_mail(new_task):
if not new_task.assigned_to == new_task.created_by: if not new_task.assigned_to == new_task.created_by:
current_site = Site.objects.get_current() current_site = Site.objects.get_current()
email_subject = render_to_string("todo/email/assigned_subject.txt", {'task': new_task}) email_subject = render_to_string("todo/email/assigned_subject.txt", {"task": new_task})
email_body = render_to_string( email_body = render_to_string(
"todo/email/assigned_body.txt", "todo/email/assigned_body.txt", {"task": new_task, "site": current_site}
{'task': new_task, 'site': current_site, }) )
send_mail( send_mail(
email_subject, email_body, new_task.created_by.email, email_subject,
[new_task.assigned_to.email], fail_silently=False) email_body,
new_task.created_by.email,
[new_task.assigned_to.email],
fail_silently=False,
)
def send_email_to_thread_participants(task, msg_body, user, subject=None): def send_email_to_thread_participants(task, msg_body, user, subject=None):
# Notify all previous commentors on a Task about a new comment. # Notify all previous commentors on a Task about a new comment.
current_site = Site.objects.get_current() current_site = Site.objects.get_current()
email_subject = subject if subject else render_to_string("todo/email/assigned_subject.txt", {'task': task}) email_subject = (
subject if subject else render_to_string("todo/email/assigned_subject.txt", {"task": task})
)
email_body = render_to_string( email_body = render_to_string(
"todo/email/newcomment_body.txt", "todo/email/newcomment_body.txt",
{'task': task, 'body': msg_body, 'site': current_site, 'user': user} {"task": task, "body": msg_body, "site": current_site, "user": user},
) )
# Get list of all thread participants - everyone who has commented, plus task creator. # Get list of all thread participants - everyone who has commented, plus task creator.

View file

@ -20,10 +20,7 @@ from django.views.decorators.csrf import csrf_exempt
from todo.forms import AddTaskListForm, AddEditTaskForm, AddExternalTaskForm, SearchForm from todo.forms import AddTaskListForm, AddEditTaskForm, AddExternalTaskForm, SearchForm
from todo.models import Task, TaskList, Comment from todo.models import Task, TaskList, Comment
from todo.utils import ( from todo.utils import send_notify_mail, send_email_to_thread_participants
send_notify_mail,
send_email_to_thread_participants,
)
def staff_only(function): def staff_only(function):
@ -31,6 +28,7 @@ def staff_only(function):
Custom view decorator allows us to raise 403 on insufficient permissions, Custom view decorator allows us to raise 403 on insufficient permissions,
rather than redirect user to login view. rather than redirect user to login view.
""" """
def wrap(request, *args, **kwargs): def wrap(request, *args, **kwargs):
if request.user.is_staff: if request.user.is_staff:
return function(request, *args, **kwargs) return function(request, *args, **kwargs)
@ -52,13 +50,18 @@ def list_lists(request) -> HttpResponse:
# Make sure user belongs to at least one group. # Make sure user belongs to at least one group.
if request.user.groups.all().count() == 0: if request.user.groups.all().count() == 0:
messages.warning(request, "You do not yet belong to any groups. Ask your administrator to add you to one.") messages.warning(
request,
"You do not yet belong to any groups. Ask your administrator to add you to one.",
)
# Superusers see all lists # Superusers see all lists
if request.user.is_superuser: if request.user.is_superuser:
lists = TaskList.objects.all().order_by('group', 'name') lists = TaskList.objects.all().order_by("group", "name")
else: else:
lists = TaskList.objects.filter(group__in=request.user.groups.all()).order_by('group', 'name') lists = TaskList.objects.filter(group__in=request.user.groups.all()).order_by(
"group", "name"
)
list_count = lists.count() list_count = lists.count()
@ -66,7 +69,11 @@ def list_lists(request) -> HttpResponse:
if request.user.is_superuser: if request.user.is_superuser:
task_count = Task.objects.filter(completed=0).count() task_count = Task.objects.filter(completed=0).count()
else: else:
task_count = Task.objects.filter(completed=0).filter(task_list__group__in=request.user.groups.all()).count() task_count = (
Task.objects.filter(completed=0)
.filter(task_list__group__in=request.user.groups.all())
.count()
)
context = { context = {
"lists": lists, "lists": lists,
@ -76,7 +83,7 @@ def list_lists(request) -> HttpResponse:
"task_count": task_count, "task_count": task_count,
} }
return render(request, 'todo/list_lists.html', context) return render(request, "todo/list_lists.html", context)
@staff_only @staff_only
@ -92,10 +99,10 @@ def del_list(request, list_id: int, list_slug: str) -> HttpResponse:
if task_list.group not in request.user.groups.all() and not request.user.is_staff: if task_list.group not in request.user.groups.all() and not request.user.is_staff:
raise PermissionDenied raise PermissionDenied
if request.method == 'POST': if request.method == "POST":
TaskList.objects.get(id=task_list.id).delete() TaskList.objects.get(id=task_list.id).delete()
messages.success(request, "{list_name} is gone.".format(list_name=task_list.name)) messages.success(request, "{list_name} is gone.".format(list_name=task_list.name))
return redirect('todo:lists') return redirect("todo:lists")
else: else:
task_count_done = Task.objects.filter(task_list=task_list.id, completed=True).count() task_count_done = Task.objects.filter(task_list=task_list.id, completed=True).count()
task_count_undone = Task.objects.filter(task_list=task_list.id, completed=False).count() task_count_undone = Task.objects.filter(task_list=task_list.id, completed=False).count()
@ -108,7 +115,7 @@ def del_list(request, list_id: int, list_slug: str) -> HttpResponse:
"task_count_total": task_count_total, "task_count_total": task_count_total,
} }
return render(request, 'todo/del_list.html', context) return render(request, "todo/del_list.html", context)
@login_required @login_required
@ -141,33 +148,36 @@ def list_detail(request, list_id=None, list_slug=None, view_completed=False):
# Add New Task Form # Add New Task Form
# ###################### # ######################
if request.POST.getlist('add_edit_task'): if request.POST.getlist("add_edit_task"):
form = AddEditTaskForm(request.user, request.POST, initial={ form = AddEditTaskForm(
'assigned_to': request.user.id, request.user,
'priority': 999, request.POST,
'task_list': task_list initial={"assigned_to": request.user.id, "priority": 999, "task_list": task_list},
}) )
if form.is_valid(): if form.is_valid():
new_task = form.save(commit=False) new_task = form.save(commit=False)
new_task.created_date = timezone.now() new_task.created_date = timezone.now()
new_task.note = bleach.clean(form.cleaned_data['note'], strip=True) new_task.note = bleach.clean(form.cleaned_data["note"], strip=True)
form.save() form.save()
# Send email alert only if Notify checkbox is checked AND assignee is not same as the submitter # Send email alert only if Notify checkbox is checked AND assignee is not same as the submitter
if "notify" in request.POST and new_task.assigned_to and new_task.assigned_to != request.user: if (
"notify" in request.POST
and new_task.assigned_to
and new_task.assigned_to != request.user
):
send_notify_mail(new_task) send_notify_mail(new_task)
messages.success(request, "New task \"{t}\" has been added.".format(t=new_task.title)) messages.success(request, 'New task "{t}" has been added.'.format(t=new_task.title))
return redirect(request.path) return redirect(request.path)
else: else:
# Don't allow adding new tasks on some views # Don't allow adding new tasks on some views
if list_slug not in ["mine", "recent-add", "recent-complete", ]: if list_slug not in ["mine", "recent-add", "recent-complete"]:
form = AddEditTaskForm(request.user, initial={ form = AddEditTaskForm(
'assigned_to': request.user.id, request.user,
'priority': 999, initial={"assigned_to": request.user.id, "priority": 999, "task_list": task_list},
'task_list': task_list, )
})
context = { context = {
"list_id": list_id, "list_id": list_id,
@ -178,7 +188,7 @@ def list_detail(request, list_id=None, list_slug=None, view_completed=False):
"view_completed": view_completed, "view_completed": view_completed,
} }
return render(request, 'todo/list_detail.html', context) return render(request, "todo/list_detail.html", context)
@login_required @login_required
@ -195,52 +205,54 @@ def task_detail(request, task_id: int) -> HttpResponse:
raise PermissionDenied raise PermissionDenied
# Save submitted comments # Save submitted comments
if request.POST.get('add_comment'): if request.POST.get("add_comment"):
Comment.objects.create( Comment.objects.create(
author=request.user, author=request.user,
task=task, task=task,
body=bleach.clean(request.POST['comment-body'], strip=True), body=bleach.clean(request.POST["comment-body"], strip=True),
) )
send_email_to_thread_participants( send_email_to_thread_participants(
task, request.POST['comment-body'], request.user, task,
subject='New comment posted on task "{}"'.format(task.title)) request.POST["comment-body"],
request.user,
subject='New comment posted on task "{}"'.format(task.title),
)
messages.success(request, "Comment posted. Notification email sent to thread participants.") messages.success(request, "Comment posted. Notification email sent to thread participants.")
# Save task edits # Save task edits
if request.POST.get('add_edit_task'): if request.POST.get("add_edit_task"):
form = AddEditTaskForm(request.user, request.POST, instance=task, initial={'task_list': task.task_list}) form = AddEditTaskForm(
request.user, request.POST, instance=task, initial={"task_list": task.task_list}
)
if form.is_valid(): if form.is_valid():
item = form.save(commit=False) item = form.save(commit=False)
item.note = bleach.clean(form.cleaned_data['note'], strip=True) item.note = bleach.clean(form.cleaned_data["note"], strip=True)
item.save() item.save()
messages.success(request, "The task has been edited.") messages.success(request, "The task has been edited.")
return redirect('todo:list_detail', list_id=task.task_list.id, list_slug=task.task_list.slug) return redirect(
"todo:list_detail", list_id=task.task_list.id, list_slug=task.task_list.slug
)
else: else:
form = AddEditTaskForm(request.user, instance=task, initial={'task_list': task.task_list}) form = AddEditTaskForm(request.user, instance=task, initial={"task_list": task.task_list})
# Mark complete # Mark complete
if request.POST.get('toggle_done'): if request.POST.get("toggle_done"):
results_changed = toggle_done([task.id, ]) results_changed = toggle_done([task.id])
for res in results_changed: for res in results_changed:
messages.success(request, res) messages.success(request, res)
return redirect('todo:task_detail', task_id=task.id,) return redirect("todo:task_detail", task_id=task.id)
if task.due_date: if task.due_date:
thedate = task.due_date thedate = task.due_date
else: else:
thedate = datetime.datetime.now() thedate = datetime.datetime.now()
context = { context = {"task": task, "comment_list": comment_list, "form": form, "thedate": thedate}
"task": task,
"comment_list": comment_list,
"form": form,
"thedate": thedate,
}
return render(request, 'todo/task_detail.html', context) return render(request, "todo/task_detail.html", context)
@csrf_exempt @csrf_exempt
@ -248,7 +260,7 @@ def task_detail(request, task_id: int) -> HttpResponse:
def reorder_tasks(request) -> HttpResponse: def reorder_tasks(request) -> HttpResponse:
"""Handle task re-ordering (priorities) from JQuery drag/drop in list_detail.html """Handle task re-ordering (priorities) from JQuery drag/drop in list_detail.html
""" """
newtasklist = request.POST.getlist('tasktable[]') newtasklist = request.POST.getlist("tasktable[]")
if newtasklist: if newtasklist:
# First task in received list is always empty - remove it # First task in received list is always empty - remove it
del newtasklist[0] del newtasklist[0]
@ -280,24 +292,23 @@ def add_list(request) -> HttpResponse:
newlist.slug = slugify(newlist.name) newlist.slug = slugify(newlist.name)
newlist.save() newlist.save()
messages.success(request, "A new list has been added.") messages.success(request, "A new list has been added.")
return redirect('todo:lists') return redirect("todo:lists")
except IntegrityError: except IntegrityError:
messages.warning( messages.warning(
request, request,
"There was a problem saving the new list. " "There was a problem saving the new list. "
"Most likely a list with the same name in the same group already exists.") "Most likely a list with the same name in the same group already exists.",
)
else: else:
if request.user.groups.all().count() == 1: if request.user.groups.all().count() == 1:
form = AddTaskListForm(request.user, initial={"group": request.user.groups.all()[0]}) form = AddTaskListForm(request.user, initial={"group": request.user.groups.all()[0]})
else: else:
form = AddTaskListForm(request.user) form = AddTaskListForm(request.user)
context = { context = {"form": form}
"form": form,
}
return render(request, 'todo/add_list.html', context) return render(request, "todo/add_list.html", context)
@login_required @login_required
@ -306,36 +317,32 @@ def search(request) -> HttpResponse:
""" """
if request.GET: if request.GET:
query_string = '' query_string = ""
found_tasks = None found_tasks = None
if ('q' in request.GET) and request.GET['q'].strip(): if ("q" in request.GET) and request.GET["q"].strip():
query_string = request.GET['q'] query_string = request.GET["q"]
found_tasks = Task.objects.filter( found_tasks = Task.objects.filter(
Q(title__icontains=query_string) | Q(title__icontains=query_string) | Q(note__icontains=query_string)
Q(note__icontains=query_string)
) )
else: else:
# What if they selected the "completed" toggle but didn't enter a query string? # What if they selected the "completed" toggle but didn't enter a query string?
# We still need found_tasks in a queryset so it can be "excluded" below. # We still need found_tasks in a queryset so it can be "excluded" below.
found_tasks = Task.objects.all() found_tasks = Task.objects.all()
if 'inc_complete' in request.GET: if "inc_complete" in request.GET:
found_tasks = found_tasks.exclude(completed=True) found_tasks = found_tasks.exclude(completed=True)
else: else:
query_string = None query_string = None
found_tasks =None found_tasks = None
# Only include tasks that are in groups of which this user is a member: # Only include tasks that are in groups of which this user is a member:
if not request.user.is_superuser: if not request.user.is_superuser:
found_tasks = found_tasks.filter(task_list__group__in=request.user.groups.all()) found_tasks = found_tasks.filter(task_list__group__in=request.user.groups.all())
context = { context = {"query_string": query_string, "found_tasks": found_tasks}
'query_string': query_string, return render(request, "todo/search_results.html", context)
'found_tasks': found_tasks
}
return render(request, 'todo/search_results.html', context)
@login_required @login_required
@ -348,10 +355,14 @@ def external_add(request) -> HttpResponse:
""" """
if not settings.TODO_DEFAULT_LIST_SLUG: if not settings.TODO_DEFAULT_LIST_SLUG:
raise RuntimeError("This feature requires TODO_DEFAULT_LIST_SLUG: in settings. See documentation.") raise RuntimeError(
"This feature requires TODO_DEFAULT_LIST_SLUG: in settings. See documentation."
)
if not TaskList.objects.filter(slug=settings.TODO_DEFAULT_LIST_SLUG).exists(): if not TaskList.objects.filter(slug=settings.TODO_DEFAULT_LIST_SLUG).exists():
raise RuntimeError("There is no TaskList with slug specified for TODO_DEFAULT_LIST_SLUG in settings.") raise RuntimeError(
"There is no TaskList with slug specified for TODO_DEFAULT_LIST_SLUG in settings."
)
if request.POST: if request.POST:
form = AddExternalTaskForm(request.POST) form = AddExternalTaskForm(request.POST)
@ -367,26 +378,36 @@ def external_add(request) -> HttpResponse:
# Send email to assignee if we have one # Send email to assignee if we have one
if task.assigned_to: if task.assigned_to:
email_subject = render_to_string("todo/email/assigned_subject.txt", {'task': task.title}) email_subject = render_to_string(
email_body = render_to_string("todo/email/assigned_body.txt", {'task': task, 'site': current_site, }) "todo/email/assigned_subject.txt", {"task": task.title}
)
email_body = render_to_string(
"todo/email/assigned_body.txt", {"task": task, "site": current_site}
)
try: try:
send_mail( send_mail(
email_subject, email_body, task.created_by.email, email_subject,
[task.assigned_to.email, ], fail_silently=False) email_body,
task.created_by.email,
[task.assigned_to.email],
fail_silently=False,
)
except ConnectionRefusedError: except ConnectionRefusedError:
messages.warning(request, "Task saved but mail not sent. Contact your administrator.") messages.warning(
request, "Task saved but mail not sent. Contact your administrator."
)
messages.success(request, "Your trouble ticket has been submitted. We'll get back to you soon.") messages.success(
request, "Your trouble ticket has been submitted. We'll get back to you soon."
)
return redirect(settings.TODO_PUBLIC_SUBMIT_REDIRECT) return redirect(settings.TODO_PUBLIC_SUBMIT_REDIRECT)
else: else:
form = AddExternalTaskForm(initial={'priority': 999}) form = AddExternalTaskForm(initial={"priority": 999})
context = { context = {"form": form}
"form": form,
}
return render(request, 'todo/add_task_external.html', context) return render(request, "todo/add_task_external.html", context)
@login_required @login_required
@ -399,9 +420,9 @@ def toggle_done(request, task_id: int) -> HttpResponse:
# Permissions # Permissions
if not ( if not (
(task.created_by == request.user) or (task.created_by == request.user)
(task.assigned_to == request.user) or or (task.assigned_to == request.user)
(task.task_list.group in request.user.groups.all()) or (task.task_list.group in request.user.groups.all())
): ):
raise PermissionDenied raise PermissionDenied
@ -410,8 +431,9 @@ def toggle_done(request, task_id: int) -> HttpResponse:
task.save() task.save()
messages.success(request, "Task status changed for '{}'".format(task.title)) messages.success(request, "Task status changed for '{}'".format(task.title))
return redirect(reverse('todo:list_detail', kwargs={"list_id": tlist.id, "list_slug": tlist.slug})) return redirect(
reverse("todo:list_detail", kwargs={"list_id": tlist.id, "list_slug": tlist.slug})
)
@login_required @login_required
@ -424,9 +446,9 @@ def delete_task(request, task_id: int) -> HttpResponse:
# Permissions # Permissions
if not ( if not (
(task.created_by == request.user) or (task.created_by == request.user)
(task.assigned_to == request.user) or or (task.assigned_to == request.user)
(task.task_list.group in request.user.groups.all()) or (task.task_list.group in request.user.groups.all())
): ):
raise PermissionDenied raise PermissionDenied
@ -434,4 +456,7 @@ def delete_task(request, task_id: int) -> HttpResponse:
task.delete() task.delete()
messages.success(request, "Task '{}' has been deleted".format(task.title)) messages.success(request, "Task '{}' has been deleted".format(task.title))
return redirect(reverse('todo:list_detail', kwargs={"list_id": tlist.id, "list_slug": tlist.slug})) return redirect(
reverse("todo:list_detail", kwargs={"list_id": tlist.id, "list_slug": tlist.slug})
)