diff --git a/setup.py b/setup.py index ad3dca3..7f0ed19 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup, find_packages import todo as package setup( - name='django-todo', + name="django-todo", version=package.__version__, description=package.__doc__.strip(), author=package.__author__, @@ -14,18 +14,18 @@ setup( license=package.__license__, packages=find_packages(), classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Web Environment', - 'Framework :: Django', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Topic :: Office/Business :: Groupware', - 'Topic :: Software Development :: Bug Tracking', + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Framework :: Django", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Topic :: Office/Business :: Groupware", + "Topic :: Software Development :: Bug Tracking", ], include_package_data=True, zip_safe=False, - install_requires=['unidecode', ], + install_requires=["unidecode"], ) diff --git a/test_settings.py b/test_settings.py index 26bc9c6..157b21b 100644 --- a/test_settings.py +++ b/test_settings.py @@ -1,64 +1,63 @@ import os -DEBUG = True, +DEBUG = (True,) BASE_DIR = os.path.dirname(os.path.dirname(__file__)) -print("bd ", BASE_DIR) DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', + "default": { + "ENGINE": "django.db.backends.sqlite3" } } -EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' +EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" # Document TODO_STAFF_ONLY = False -TODO_DEFAULT_LIST_SLUG = 'tickets' +TODO_DEFAULT_LIST_SLUG = "tickets" TODO_DEFAULT_ASSIGNEE = None -TODO_PUBLIC_SUBMIT_REDIRECT = '/' +TODO_PUBLIC_SUBMIT_REDIRECT = "/" SECRET_KEY = "LKFSD8sdl.,8&sdf--" SITE_ID = 1 INSTALLED_APPS = ( - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.messages', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.staticfiles', - 'todo', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.messages", + "django.contrib.sessions", + "django.contrib.sites", + "django.contrib.staticfiles", + "todo", ) -ROOT_URLCONF = 'base_urls' +ROOT_URLCONF = "base_urls" MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(BASE_DIR, 'todo', 'templates'), ], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.template.context_processors.media', - 'django.template.context_processors.static', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [os.path.join(BASE_DIR, "todo", "templates")], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.template.context_processors.media", + "django.template.context_processors.static", + "django.contrib.messages.context_processors.messages", # Your stuff: custom template context processors go here - ], + ] }, - }, -] \ No newline at end of file + } +] diff --git a/todo/admin.py b/todo/admin.py index 79bb7da..fa5d4ef 100644 --- a/todo/admin.py +++ b/todo/admin.py @@ -3,14 +3,14 @@ from todo.models import Task, TaskList, Comment class TaskAdmin(admin.ModelAdmin): - list_display = ('title', 'task_list', 'completed', 'priority', 'due_date') - list_filter = ('task_list',) - ordering = ('priority',) - search_fields = ('name',) + list_display = ("title", "task_list", "completed", "priority", "due_date") + list_filter = ("task_list",) + ordering = ("priority",) + search_fields = ("name",) class CommentAdmin(admin.ModelAdmin): - list_display = ('author', 'date', 'snippet') + list_display = ("author", "date", "snippet") admin.site.register(TaskList) diff --git a/todo/forms.py b/todo/forms.py index 0986aa7..4793c46 100644 --- a/todo/forms.py +++ b/todo/forms.py @@ -11,13 +11,16 @@ class AddTaskListForm(ModelForm): def __init__(self, user, *args, **kwargs): super(AddTaskListForm, self).__init__(*args, **kwargs) - self.fields['group'].queryset = Group.objects.filter(user=user) - self.fields['group'].widget.attrs = { - 'id': 'id_group', 'class': "custom-select mb-3", 'name': 'group'} + self.fields["group"].queryset = Group.objects.filter(user=user) + self.fields["group"].widget.attrs = { + "id": "id_group", + "class": "custom-select mb-3", + "name": "group", + } class Meta: model = TaskList - exclude = ['created_date', 'slug', ] + exclude = ["created_date", "slug"] class AddEditTaskForm(ModelForm): @@ -26,22 +29,25 @@ class AddEditTaskForm(ModelForm): def __init__(self, user, *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() - 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'].widget.attrs = { - 'id': 'id_assigned_to', 'class': "custom-select mb-3", 'name': 'assigned_to'} - self.fields['task_list'].value = kwargs['initial']['task_list'].id + 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"].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( - widget=forms.DateInput(attrs={'type': 'date'}), required=False) + due_date = forms.DateField(widget=forms.DateInput(attrs={"type": "date"}), required=False) - title = forms.CharField( - widget=forms.widgets.TextInput()) + title = forms.CharField(widget=forms.widgets.TextInput()) - note = forms.CharField( - widget=forms.Textarea(), required=False) + note = forms.CharField(widget=forms.Textarea(), required=False) class Meta: model = Task @@ -51,27 +57,24 @@ class AddEditTaskForm(ModelForm): class AddExternalTaskForm(ModelForm): """Form to allow users who are not part of the GTD system to file a ticket.""" - title = forms.CharField( - widget=forms.widgets.TextInput(attrs={'size': 35}), - label="Summary" - ) - note = forms.CharField( - widget=forms.widgets.Textarea(), - label='Problem Description', - ) - priority = forms.IntegerField( - widget=forms.HiddenInput(), - ) + title = forms.CharField(widget=forms.widgets.TextInput(attrs={"size": 35}), label="Summary") + note = forms.CharField(widget=forms.widgets.Textarea(), label="Problem Description") + priority = forms.IntegerField(widget=forms.HiddenInput()) class Meta: model = Task 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): """Search.""" - q = forms.CharField( - widget=forms.widgets.TextInput(attrs={'size': 35}) - ) + q = forms.CharField(widget=forms.widgets.TextInput(attrs={"size": 35})) diff --git a/todo/models.py b/todo/models.py index ee5955a..89a1837 100644 --- a/todo/models.py +++ b/todo/models.py @@ -10,7 +10,7 @@ from django.utils import timezone class TaskList(models.Model): name = models.CharField(max_length=60) - slug = models.SlugField(default='',) + slug = models.SlugField(default="") group = models.ForeignKey(Group, on_delete=models.CASCADE) def __str__(self): @@ -28,12 +28,19 @@ class Task(models.Model): title = models.CharField(max_length=140) task_list = models.ForeignKey(TaskList, on_delete=models.CASCADE, 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_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( - 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) priority = models.PositiveIntegerField() @@ -47,7 +54,7 @@ class Task(models.Model): return self.title 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 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 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) task = models.ForeignKey(Task, on_delete=models.CASCADE) date = models.DateTimeField(default=datetime.datetime.now) diff --git a/todo/tests/conftest.py b/todo/tests/conftest.py index a2514f4..dc6c6bc 100644 --- a/todo/tests/conftest.py +++ b/todo/tests/conftest.py @@ -5,13 +5,14 @@ from django.contrib.auth.models import Group from todo.models import Task, TaskList - @pytest.fixture def todo_setup(django_user_model): # Two groups with different users, two sets of tasks. 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) tlist1 = TaskList.objects.create(group=g1, name="Zip", slug="zip") 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) 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) tlist2 = TaskList.objects.create(group=g2, name="Zap", slug="zap") Task.objects.create(created_by=u2, title="Task 1", task_list=tlist2, priority=1) diff --git a/todo/tests/test_utils.py b/todo/tests/test_utils.py index aa505eb..0ac4c8a 100644 --- a/todo/tests/test_utils.py +++ b/todo/tests/test_utils.py @@ -9,7 +9,7 @@ from todo.utils import send_notify_mail, send_email_to_thread_participants @pytest.fixture() # Set up an in-memory mail server to receive test emails 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): @@ -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") task = Task.objects.filter(created_by=u1).first() - u3 = django_user_model.objects.create_user(username="u3", password="zzz", email="u3@example.com") - 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", ) + u3 = django_user_model.objects.create_user( + username="u3", password="zzz", email="u3@example.com" + ) + 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) assert len(mail.outbox) == 1 # One message to multiple recipients - assert 'u1@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 "u1@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() diff --git a/todo/tests/test_views.py b/todo/tests/test_views.py index 4f44e8a..dbfa1d0 100644 --- a/todo/tests/test_views.py +++ b/todo/tests/test_views.py @@ -16,19 +16,20 @@ After that, view contents and behaviors. # ### SMOKETESTS ### + @pytest.mark.django_db def test_todo_setup(todo_setup): assert Task.objects.all().count() == 6 def test_view_list_lists(todo_setup, admin_client): - url = reverse('todo:lists') + url = reverse("todo:lists") response = admin_client.get(url) assert response.status_code == 200 def test_view_reorder(todo_setup, admin_client): - url = reverse('todo:reorder_tasks') + url = reverse("todo:reorder_tasks") response = admin_client.get(url) 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() 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) assert response.status_code == 200 def test_view_mine(todo_setup, admin_client): - url = reverse('todo:mine') + url = reverse("todo:mine") response = admin_client.get(url) assert response.status_code == 200 def test_view_list_completed(todo_setup, admin_client): 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) assert response.status_code == 200 def test_view_list(todo_setup, admin_client): 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) assert response.status_code == 200 def test_del_list(todo_setup, admin_client): 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) assert response.status_code == 200 def test_view_add_list(todo_setup, admin_client): - url = reverse('todo:add_list') + url = reverse("todo:add_list") response = admin_client.get(url) assert response.status_code == 200 def test_view_task_detail(todo_setup, admin_client): 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) assert response.status_code == 200 def test_view_search(todo_setup, admin_client): - url = reverse('todo:search') + url = reverse("todo:search") response = admin_client.get(url) assert response.status_code == 200 @@ -100,11 +103,11 @@ def test_no_javascript_in_task_note(todo_setup, client): "priority": 10, "title": title, "note": note, - 'add_edit_task': 'Submit' + "add_edit_task": "Submit", } - client.login(username='u2', password="password") - url = reverse('todo:list_detail', kwargs={"list_id": task_list.id, "list_slug": task_list.slug}) + client.login(username="u2", password="password") + url = reverse("todo:list_detail", kwargs={"list_id": task_list.id, "list_slug": task_list.slug}) response = client.post(url, data) assert response.status_code == 302 @@ -118,7 +121,7 @@ def test_no_javascript_in_task_note(todo_setup, client): @pytest.mark.django_db def test_no_javascript_in_comments(todo_setup, client): 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.created_by = user @@ -127,11 +130,8 @@ def test_no_javascript_in_comments(todo_setup, client): user.groups.add(task.task_list.group) comment = "foo bar" - data = { - "comment-body": comment, - "add_comment": 'Submit' - } - url = reverse('todo:task_detail', kwargs={"task_id": task.id}) + data = {"comment-body": comment, "add_comment": "Submit"} + url = reverse("todo:task_detail", kwargs={"task_id": task.id}) response = client.post(url, data) 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): - url = reverse('todo:add_list') + url = reverse("todo:add_list") client.login(username="you", password="password") response = client.get(url) 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): 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") response = client.get(url) 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. """ 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") response = client.get(url) 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. """ 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") response = client.get(url) assert response.status_code == 403 @@ -190,7 +190,7 @@ def test_view_task_mine(todo_setup, client): # Users can always view their own tasks task = Task.objects.filter(created_by__username="u1").first() 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) 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. 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") response = client.get(url) 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. # For this we can use the fixture data as-is. 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") response = client.get(url) assert response.status_code == 403 + diff --git a/todo/utils.py b/todo/utils.py index 1075481..f2ac6e2 100644 --- a/todo/utils.py +++ b/todo/utils.py @@ -11,24 +11,30 @@ def send_notify_mail(new_task): if not new_task.assigned_to == new_task.created_by: 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( - "todo/email/assigned_body.txt", - {'task': new_task, 'site': current_site, }) + "todo/email/assigned_body.txt", {"task": new_task, "site": current_site} + ) send_mail( - email_subject, email_body, new_task.created_by.email, - [new_task.assigned_to.email], fail_silently=False) + email_subject, + 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): # Notify all previous commentors on a Task about a new comment. 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( "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. diff --git a/todo/views.py b/todo/views.py index ba01504..c4ddbda 100644 --- a/todo/views.py +++ b/todo/views.py @@ -20,10 +20,7 @@ from django.views.decorators.csrf import csrf_exempt from todo.forms import AddTaskListForm, AddEditTaskForm, AddExternalTaskForm, SearchForm from todo.models import Task, TaskList, Comment -from todo.utils import ( - send_notify_mail, - send_email_to_thread_participants, - ) +from todo.utils import send_notify_mail, send_email_to_thread_participants def staff_only(function): @@ -31,6 +28,7 @@ def staff_only(function): Custom view decorator allows us to raise 403 on insufficient permissions, rather than redirect user to login view. """ + def wrap(request, *args, **kwargs): if request.user.is_staff: return function(request, *args, **kwargs) @@ -52,13 +50,18 @@ def list_lists(request) -> HttpResponse: # Make sure user belongs to at least one group. 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 if request.user.is_superuser: - lists = TaskList.objects.all().order_by('group', 'name') + lists = TaskList.objects.all().order_by("group", "name") 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() @@ -66,17 +69,21 @@ def list_lists(request) -> HttpResponse: if request.user.is_superuser: task_count = Task.objects.filter(completed=0).count() 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 = { - "lists": lists, - "thedate": thedate, - "searchform": searchform, - "list_count": list_count, - "task_count": task_count, + "lists": lists, + "thedate": thedate, + "searchform": searchform, + "list_count": list_count, + "task_count": task_count, } - return render(request, 'todo/list_lists.html', context) + return render(request, "todo/list_lists.html", context) @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: raise PermissionDenied - if request.method == 'POST': + if request.method == "POST": TaskList.objects.get(id=task_list.id).delete() messages.success(request, "{list_name} is gone.".format(list_name=task_list.name)) - return redirect('todo:lists') + return redirect("todo:lists") else: 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() @@ -108,7 +115,7 @@ def del_list(request, list_id: int, list_slug: str) -> HttpResponse: "task_count_total": task_count_total, } - return render(request, 'todo/del_list.html', context) + return render(request, "todo/del_list.html", context) @login_required @@ -141,33 +148,36 @@ def list_detail(request, list_id=None, list_slug=None, view_completed=False): # Add New Task Form # ###################### - if request.POST.getlist('add_edit_task'): - form = AddEditTaskForm(request.user, request.POST, initial={ - 'assigned_to': request.user.id, - 'priority': 999, - 'task_list': task_list - }) + if request.POST.getlist("add_edit_task"): + form = AddEditTaskForm( + request.user, + request.POST, + initial={"assigned_to": request.user.id, "priority": 999, "task_list": task_list}, + ) if form.is_valid(): new_task = form.save(commit=False) 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() # 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) - 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) else: # Don't allow adding new tasks on some views - if list_slug not in ["mine", "recent-add", "recent-complete", ]: - form = AddEditTaskForm(request.user, initial={ - 'assigned_to': request.user.id, - 'priority': 999, - 'task_list': task_list, - }) + if list_slug not in ["mine", "recent-add", "recent-complete"]: + form = AddEditTaskForm( + request.user, + initial={"assigned_to": request.user.id, "priority": 999, "task_list": task_list}, + ) context = { "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, } - return render(request, 'todo/list_detail.html', context) + return render(request, "todo/list_detail.html", context) @login_required @@ -195,52 +205,54 @@ def task_detail(request, task_id: int) -> HttpResponse: raise PermissionDenied # Save submitted comments - if request.POST.get('add_comment'): + if request.POST.get("add_comment"): Comment.objects.create( author=request.user, 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( - task, request.POST['comment-body'], request.user, - subject='New comment posted on task "{}"'.format(task.title)) + task, + 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.") # Save task edits - if request.POST.get('add_edit_task'): - form = AddEditTaskForm(request.user, request.POST, instance=task, initial={'task_list': task.task_list}) + if request.POST.get("add_edit_task"): + form = AddEditTaskForm( + request.user, request.POST, instance=task, initial={"task_list": task.task_list} + ) if form.is_valid(): 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() 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: - 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 - if request.POST.get('toggle_done'): - results_changed = toggle_done([task.id, ]) + if request.POST.get("toggle_done"): + results_changed = toggle_done([task.id]) for res in results_changed: 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: thedate = task.due_date else: thedate = datetime.datetime.now() - context = { - "task": task, - "comment_list": comment_list, - "form": form, - "thedate": thedate, - } + context = {"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 @@ -248,7 +260,7 @@ def task_detail(request, task_id: int) -> HttpResponse: def reorder_tasks(request) -> HttpResponse: """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: # First task in received list is always empty - remove it del newtasklist[0] @@ -280,24 +292,23 @@ def add_list(request) -> HttpResponse: newlist.slug = slugify(newlist.name) newlist.save() messages.success(request, "A new list has been added.") - return redirect('todo:lists') + return redirect("todo:lists") except IntegrityError: messages.warning( request, "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: if request.user.groups.all().count() == 1: form = AddTaskListForm(request.user, initial={"group": request.user.groups.all()[0]}) else: form = AddTaskListForm(request.user) - context = { - "form": form, - } + context = {"form": form} - return render(request, 'todo/add_list.html', context) + return render(request, "todo/add_list.html", context) @login_required @@ -306,36 +317,32 @@ def search(request) -> HttpResponse: """ if request.GET: - query_string = '' + query_string = "" found_tasks = None - if ('q' in request.GET) and request.GET['q'].strip(): - query_string = request.GET['q'] + if ("q" in request.GET) and request.GET["q"].strip(): + query_string = request.GET["q"] found_tasks = Task.objects.filter( - Q(title__icontains=query_string) | - Q(note__icontains=query_string) + Q(title__icontains=query_string) | Q(note__icontains=query_string) ) else: # 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. found_tasks = Task.objects.all() - if 'inc_complete' in request.GET: + if "inc_complete" in request.GET: found_tasks = found_tasks.exclude(completed=True) else: query_string = None - found_tasks =None + found_tasks = None # Only include tasks that are in groups of which this user is a member: if not request.user.is_superuser: found_tasks = found_tasks.filter(task_list__group__in=request.user.groups.all()) - context = { - 'query_string': query_string, - 'found_tasks': found_tasks - } - return render(request, 'todo/search_results.html', context) + context = {"query_string": query_string, "found_tasks": found_tasks} + return render(request, "todo/search_results.html", context) @login_required @@ -348,10 +355,14 @@ def external_add(request) -> HttpResponse: """ 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(): - 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: form = AddExternalTaskForm(request.POST) @@ -367,26 +378,36 @@ def external_add(request) -> HttpResponse: # Send email to assignee if we have one if task.assigned_to: - email_subject = render_to_string("todo/email/assigned_subject.txt", {'task': task.title}) - email_body = render_to_string("todo/email/assigned_body.txt", {'task': task, 'site': current_site, }) + email_subject = render_to_string( + "todo/email/assigned_subject.txt", {"task": task.title} + ) + email_body = render_to_string( + "todo/email/assigned_body.txt", {"task": task, "site": current_site} + ) try: send_mail( - email_subject, email_body, task.created_by.email, - [task.assigned_to.email, ], fail_silently=False) + email_subject, + email_body, + task.created_by.email, + [task.assigned_to.email], + fail_silently=False, + ) 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) else: - form = AddExternalTaskForm(initial={'priority': 999}) + form = AddExternalTaskForm(initial={"priority": 999}) - context = { - "form": form, - } + context = {"form": form} - return render(request, 'todo/add_task_external.html', context) + return render(request, "todo/add_task_external.html", context) @login_required @@ -399,9 +420,9 @@ def toggle_done(request, task_id: int) -> HttpResponse: # Permissions if not ( - (task.created_by == request.user) or - (task.assigned_to == request.user) or - (task.task_list.group in request.user.groups.all()) + (task.created_by == request.user) + or (task.assigned_to == request.user) + or (task.task_list.group in request.user.groups.all()) ): raise PermissionDenied @@ -410,8 +431,9 @@ def toggle_done(request, task_id: int) -> HttpResponse: task.save() 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 @@ -424,9 +446,9 @@ def delete_task(request, task_id: int) -> HttpResponse: # Permissions if not ( - (task.created_by == request.user) or - (task.assigned_to == request.user) or - (task.task_list.group in request.user.groups.all()) + (task.created_by == request.user) + or (task.assigned_to == request.user) + or (task.task_list.group in request.user.groups.all()) ): raise PermissionDenied @@ -434,4 +456,7 @@ def delete_task(request, task_id: int) -> HttpResponse: task.delete() 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}) + ) +