From 4aef6039466f18537fef392518975b952fea1407 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 16 Sep 2014 12:14:28 +0200 Subject: [PATCH 1/8] Add split_by_n function on utils.iterators module. --- taiga/base/utils/iterators.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/taiga/base/utils/iterators.py b/taiga/base/utils/iterators.py index 0d54cc63..b0359162 100644 --- a/taiga/base/utils/iterators.py +++ b/taiga/base/utils/iterators.py @@ -33,3 +33,12 @@ def as_dict(function): def _decorator(*args, **kwargs): return dict(function(*args, **kwargs)) return _decorator + + +def split_by_n(seq:str, n:int): + """ + A generator to divide a sequence into chunks of n units. + """ + while seq: + yield seq[:n] + seq = seq[n:] From 95845b4eda885ebcf2035b4aa5aad03a7de3bbae Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 16 Sep 2014 13:30:10 +0200 Subject: [PATCH 2/8] Add iter_queryset method to utils.iterators module. --- taiga/base/utils/iterators.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/taiga/base/utils/iterators.py b/taiga/base/utils/iterators.py index b0359162..fff3a9de 100644 --- a/taiga/base/utils/iterators.py +++ b/taiga/base/utils/iterators.py @@ -16,6 +16,8 @@ # along with this program. If not, see . from functools import wraps, partial +from django.core.paginator import Paginator + def as_tuple(function=None, *, remove_nulls=False): if function is None: @@ -42,3 +44,15 @@ def split_by_n(seq:str, n:int): while seq: yield seq[:n] seq = seq[n:] + + +def iter_queryset(queryset, itersize:int=20): + """ + Util function for iterate in more efficient way + all queryset. + """ + paginator = Paginator(queryset, itersize) + for page_num in paginator.page_range: + page = paginator.page(page_num) + for element in page.object_list: + yield element From b20a9d11999f5aa97e11d8b12704a147e588bbaf Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 16 Sep 2014 12:20:24 +0200 Subject: [PATCH 3/8] Add cached name and size fields to attachment model. --- taiga/projects/attachments/api.py | 3 ++ .../0002_add_size_and_name_fields.py | 41 +++++++++++++++++++ taiga/projects/attachments/models.py | 6 ++- taiga/projects/attachments/serializers.py | 15 ------- 4 files changed, 49 insertions(+), 16 deletions(-) create mode 100644 taiga/projects/attachments/migrations/0002_add_size_and_name_fields.py diff --git a/taiga/projects/attachments/api.py b/taiga/projects/attachments/api.py index b1494387..d2f93caa 100644 --- a/taiga/projects/attachments/api.py +++ b/taiga/projects/attachments/api.py @@ -15,6 +15,7 @@ # along with this program. If not, see . import os +import os.path as path import hashlib import mimetypes mimetypes.init() @@ -59,6 +60,8 @@ class BaseAttachmentViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCru if not obj.id: obj.content_type = self.get_content_type() obj.owner = self.request.user + obj.size = obj.attached_file.size + obj.name = path.basename(obj.attached_file.name).lower() if obj.project_id != obj.content_object.project_id: raise exc.WrongArguments("Project ID not matches between object and project") diff --git a/taiga/projects/attachments/migrations/0002_add_size_and_name_fields.py b/taiga/projects/attachments/migrations/0002_add_size_and_name_fields.py new file mode 100644 index 00000000..48468e05 --- /dev/null +++ b/taiga/projects/attachments/migrations/0002_add_size_and_name_fields.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import os.path as path +from django.db import models, migrations + + +def parse_filenames_and_sizes(apps, schema_editor): + Attachment = apps.get_model("attachments", "Attachment") + + for item in Attachment.objects.all(): + try: + item.size = item.attached_file.size + except Exception as e: + item.size = 0 + + item.name = path.basename(item.attached_file.name) + item.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('attachments', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='attachment', + name='name', + field=models.CharField(default='', blank=True, max_length=500), + preserve_default=True, + ), + migrations.AddField( + model_name='attachment', + name='size', + field=models.IntegerField(editable=False, null=True, blank=True, default=None), + preserve_default=True, + ), + migrations.RunPython(parse_filenames_and_sizes) + ] diff --git a/taiga/projects/attachments/models.py b/taiga/projects/attachments/models.py index f780025c..2008c502 100644 --- a/taiga/projects/attachments/models.py +++ b/taiga/projects/attachments/models.py @@ -51,13 +51,17 @@ class Attachment(models.Model): default=timezone.now) modified_date = models.DateTimeField(null=False, blank=False, verbose_name=_("modified date")) - + name = models.CharField(blank=True, default="", max_length=500) + size = models.IntegerField(null=True, blank=True, editable=False, default=None) attached_file = models.FileField(max_length=500, null=True, blank=True, upload_to=get_attachment_file_path, verbose_name=_("attached file")) + + is_deprecated = models.BooleanField(default=False, verbose_name=_("is deprecated")) description = models.TextField(null=False, blank=True, verbose_name=_("description")) order = models.IntegerField(default=0, null=False, blank=False, verbose_name=_("order")) + _importing = None class Meta: diff --git a/taiga/projects/attachments/serializers.py b/taiga/projects/attachments/serializers.py index 46ba0b97..2939f78a 100644 --- a/taiga/projects/attachments/serializers.py +++ b/taiga/projects/attachments/serializers.py @@ -28,10 +28,7 @@ from . import models class AttachmentSerializer(serializers.ModelSerializer): - name = serializers.SerializerMethodField("get_name") url = serializers.SerializerMethodField("get_url") - size = serializers.SerializerMethodField("get_size") - attached_file = serializers.FileField(required=True) class Meta: @@ -41,11 +38,6 @@ class AttachmentSerializer(serializers.ModelSerializer): "object_id", "order") read_only_fields = ("owner", "created_date", "modified_date") - def get_name(self, obj): - if obj.attached_file: - return path.basename(obj.attached_file.path) - return "" - def get_url(self, obj): token = None @@ -59,10 +51,3 @@ class AttachmentSerializer(serializers.ModelSerializer): return url - def get_size(self, obj): - if obj.attached_file: - try: - return obj.attached_file.size - except FileNotFoundError: - pass - return 0 From 91296886a5e5ffe251ae0efb6055080d9f993f05 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 16 Sep 2014 12:21:00 +0200 Subject: [PATCH 4/8] Change the way to generate attachment resource path. --- settings/common.py | 18 ++++++++++------ taiga/projects/attachments/models.py | 25 +++++++++++++++-------- taiga/projects/attachments/serializers.py | 13 +----------- 3 files changed, 29 insertions(+), 27 deletions(-) diff --git a/settings/common.py b/settings/common.py index 587e94e6..b4307383 100644 --- a/settings/common.py +++ b/settings/common.py @@ -58,8 +58,6 @@ SEND_BROKEN_LINK_EMAILS = True IGNORABLE_404_ENDS = (".php", ".cgi") IGNORABLE_404_STARTS = ("/phpmyadmin/",) - -# Default django tz/i18n config ATOMIC_REQUESTS = True TIME_ZONE = "UTC" LANGUAGE_CODE = "en" @@ -94,13 +92,21 @@ EVENTS_PUSH_BACKEND = "taiga.events.backends.postgresql.EventsPushBackend" # Message System MESSAGE_STORAGE = "django.contrib.messages.storage.session.SessionStorage" -# Static configuration. -MEDIA_ROOT = os.path.join(BASE_DIR, "media") -MEDIA_URL = "/media/" -STATIC_ROOT = os.path.join(BASE_DIR, "static") +# The absolute url is mandatory because attachments +# urls depends on it. On production should be set +# something like https://media.taiga.io/ +MEDIA_URL = "http://localhost:8000/media/" + +# Static url is not widelly used by taiga (only +# if admin is activated). STATIC_URL = "/static/" ADMIN_MEDIA_PREFIX = "/static/admin/" +# Static configuration. +MEDIA_ROOT = os.path.join(BASE_DIR, "media") +STATIC_ROOT = os.path.join(BASE_DIR, "static") + + STATICFILES_FINDERS = [ "django.contrib.staticfiles.finders.FileSystemFinder", "django.contrib.staticfiles.finders.AppDirectoriesFinder", diff --git a/taiga/projects/attachments/models.py b/taiga/projects/attachments/models.py index 2008c502..1f84b7c4 100644 --- a/taiga/projects/attachments/models.py +++ b/taiga/projects/attachments/models.py @@ -14,25 +14,32 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import time +import hashlib +import os +import os.path as path from django.db import models from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes import generic -from django.utils.translation import ugettext_lazy as _ from django.utils import timezone +from django.utils.encoding import force_bytes +from django.utils.translation import ugettext_lazy as _ + +from taiga.base.utils.iterators import split_by_n def get_attachment_file_path(instance, filename): - template = "attachment-files/{project}/{model}/{stamp}/{filename}" - current_timestamp = int(time.mktime(timezone.now().timetuple())) + basename = path.basename(filename).lower() - upload_to_path = template.format(stamp=current_timestamp, - project=instance.project.slug, - model=instance.content_type.model, - filename=filename) - return upload_to_path + hs = hashlib.sha256() + hs.update(force_bytes(timezone.now().isoformat())) + hs.update(os.urandom(1024)) + + p1, p2, p3, p4, *p5 = split_by_n(hs.hexdigest(), 1) + hash_part = path.join(p1, p2, p3, p4, "".join(p5)) + + return path.join("attachments", hash_part, basename) class Attachment(models.Model): diff --git a/taiga/projects/attachments/serializers.py b/taiga/projects/attachments/serializers.py index 2939f78a..4b84f4ad 100644 --- a/taiga/projects/attachments/serializers.py +++ b/taiga/projects/attachments/serializers.py @@ -39,15 +39,4 @@ class AttachmentSerializer(serializers.ModelSerializer): read_only_fields = ("owner", "created_date", "modified_date") def get_url(self, obj): - token = None - - url = reverse("attachment-url", kwargs={"pk": obj.pk}) - if "request" in self.context and self.context["request"].user.is_authenticated(): - user_id = self.context["request"].user.id - token_src = "{}-{}-{}".format(settings.ATTACHMENTS_TOKEN_SALT, user_id, obj.id) - token = hashlib.sha1(token_src.encode("utf-8")) - - return "{}?user={}&token={}".format(url, user_id, token.hexdigest()) - - return url - + return obj.attached_file.url From 4a1b0057737e47fa68aac0673aaedad3e3ab1849 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 16 Sep 2014 15:45:46 +0200 Subject: [PATCH 5/8] Apply the same url naming of attachments to user photo. --- .../users/migrations/0005_alter_user_photo.py | 20 ++++++++++++++ taiga/users/models.py | 27 ++++++++++++++++--- 2 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 taiga/users/migrations/0005_alter_user_photo.py diff --git a/taiga/users/migrations/0005_alter_user_photo.py b/taiga/users/migrations/0005_alter_user_photo.py new file mode 100644 index 00000000..f7632621 --- /dev/null +++ b/taiga/users/migrations/0005_alter_user_photo.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import taiga.users.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0004_auto_20140913_1914'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='photo', + field=models.FileField(upload_to=taiga.users.models.get_user_file_path, blank=True, max_length=500, verbose_name='photo', null=True), + ), + ] diff --git a/taiga/users/models.py b/taiga/users/models.py index d1a6b4ba..2a3c35eb 100644 --- a/taiga/users/models.py +++ b/taiga/users/models.py @@ -14,26 +14,44 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import hashlib +import os +import os.path as path +import random +import re + from django.db import models from django.dispatch import receiver from django.utils.translation import ugettext_lazy as _ from django.contrib.auth.models import UserManager, AbstractBaseUser from django.core import validators from django.utils import timezone +from django.utils.encoding import force_bytes from djorm_pgarray.fields import TextArrayField from taiga.base.utils.slug import slugify_uniquely +from taiga.base.utils.iterators import split_by_n from taiga.permissions.permissions import MEMBERS_PERMISSIONS -import random -import re - def generate_random_hex_color(): return "#{:06x}".format(random.randint(0,0xFFFFFF)) +def get_user_file_path(instance, filename): + basename = path.basename(filename).lower() + + hs = hashlib.sha256() + hs.update(force_bytes(timezone.now().isoformat())) + hs.update(os.urandom(1024)) + + p1, p2, p3, p4, *p5 = split_by_n(hs.hexdigest(), 1) + hash_part = path.join(p1, p2, p3, p4, "".join(p5)) + + return path.join("user", hash_part, basename) + + class PermissionsMixin(models.Model): """ A mixin class that adds the fields and methods necessary to support @@ -85,7 +103,8 @@ class User(AbstractBaseUser, PermissionsMixin): color = models.CharField(max_length=9, null=False, blank=True, default=generate_random_hex_color, verbose_name=_("color")) bio = models.TextField(null=False, blank=True, default="", verbose_name=_("biography")) - photo = models.FileField(upload_to="users/photo", max_length=500, null=True, blank=True, + photo = models.FileField(upload_to=get_user_file_path, + max_length=500, null=True, blank=True, verbose_name=_("photo")) date_joined = models.DateTimeField(_('date joined'), default=timezone.now) default_language = models.CharField(max_length=20, null=False, blank=True, default="", From 0a26e3a81c37a57b8ae5deb6e8463419dbadff5b Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 16 Sep 2014 18:12:06 +0200 Subject: [PATCH 6/8] Remove deprecated attachment view responsible of permission checks. --- taiga/projects/attachments/api.py | 36 ------------------------------- taiga/urls.py | 6 ------ 2 files changed, 42 deletions(-) diff --git a/taiga/projects/attachments/api.py b/taiga/projects/attachments/api.py index d2f93caa..d54e9aa4 100644 --- a/taiga/projects/attachments/api.py +++ b/taiga/projects/attachments/api.py @@ -100,39 +100,3 @@ class WikiAttachmentViewSet(BaseAttachmentViewSet): permission_classes = (permissions.WikiAttachmentPermission,) filter_backends = (filters.CanViewWikiAttachmentFilterBackend,) content_type = "wiki.wikipage" - - -class RawAttachmentView(generics.RetrieveAPIView): - queryset = models.Attachment.objects.all() - permission_classes = (permissions.RawAttachmentPermission,) - - def _serve_attachment(self, attachment): - if settings.IN_DEVELOPMENT_SERVER: - return http.HttpResponseRedirect(attachment.url) - - name = attachment.name - response = http.HttpResponse() - response['X-Accel-Redirect'] = "/{filepath}".format(filepath=name) - response['Content-Disposition'] = 'inline;filename={filename}'.format( - filename=os.path.basename(name)) - response['Content-Type'] = mimetypes.guess_type(name)[0] - - return response - - def check_permissions(self, request, action='retrieve', obj=None): - self.object = self.get_object() - user_id = self.request.QUERY_PARAMS.get('user', None) - token = self.request.QUERY_PARAMS.get('token', None) - - if token and user_id: - token_src = "{}-{}-{}".format(settings.ATTACHMENTS_TOKEN_SALT, user_id, self.object.id) - if token == hashlib.sha1(token_src.encode("utf-8")).hexdigest(): - request.user = get_object_or_404(User, pk=user_id) - - return super().check_permissions(request, action, self.object) - - def retrieve(self, request, *args, **kwargs): - self.object = self.get_object() - - self.check_permissions(request, 'retrieve', self.object) - return self._serve_attachment(self.object.attached_file) diff --git a/taiga/urls.py b/taiga/urls.py index 05cd3f3d..5c8c4456 100644 --- a/taiga/urls.py +++ b/taiga/urls.py @@ -20,14 +20,8 @@ from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.contrib import admin from .routers import router -from .projects.attachments.api import RawAttachmentView - - - -admin.autodiscover() urlpatterns = patterns('', - url(r'^attachments/(?P\d+)/$', RawAttachmentView.as_view(), name="attachment-url"), url(r'^api/v1/', include(router.urls)), url(r'^api/v1/api-auth/', include('rest_framework.urls', namespace='rest_framework')), url(r'^admin/', include(admin.site.urls)), From def314e0e26c890f8d0079687ec3fae89d8f4212 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 16 Sep 2014 15:14:40 +0200 Subject: [PATCH 7/8] Add adhoc script for migrate the location and description/content fields to new attachments urls. --- .../attachments/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../commands/migrate_attachments.py | 130 ++++++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 taiga/projects/attachments/management/__init__.py create mode 100644 taiga/projects/attachments/management/commands/__init__.py create mode 100644 taiga/projects/attachments/management/commands/migrate_attachments.py diff --git a/taiga/projects/attachments/management/__init__.py b/taiga/projects/attachments/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/taiga/projects/attachments/management/commands/__init__.py b/taiga/projects/attachments/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/taiga/projects/attachments/management/commands/migrate_attachments.py b/taiga/projects/attachments/management/commands/migrate_attachments.py new file mode 100644 index 00000000..a1c7e283 --- /dev/null +++ b/taiga/projects/attachments/management/commands/migrate_attachments.py @@ -0,0 +1,130 @@ +import re + +from django.apps import apps +from django.core.management.base import BaseCommand, CommandError +from django.core.files import File +from django.conf import settings +from django.db import transaction + +from taiga.base.utils.iterators import iter_queryset + +url = """ +https://api-taiga.kaleidos.net/attachments/446/?user=8&token=9ac0fc593e9c07740975c6282e1e501189578faa +""" + +class Command(BaseCommand): + help = "Parses all objects and try replace old attachments url with one new" + + + trx = r"((?:https?)://api-taiga\.kaleidos\.net/attachments/(\d+)/[^\s]+)" + + @transaction.atomic + def handle(self, *args, **options): + settings.MEDIA_URL="https://media.taiga.io/" + + self.move_user_photo() + self.move_attachments() + self.process_userstories() + self.process_issues() + self.process_wiki() + self.process_tasks() + self.process_history() + + def move_attachments(self): + print("Moving all attachments to new location") + + Attachment = apps.get_model("attachments", "Attachment") + qs = Attachment.objects.all() + + for item in iter_queryset(qs): + try: + with transaction.atomic(): + old_file = item.attached_file + item.attached_file = File(old_file) + item.save() + except FileNotFoundError: + item.delete() + + def move_user_photo(self): + print("Moving all user photos to new location") + + User = apps.get_model("users", "User") + qs = User.objects.all() + + for item in iter_queryset(qs): + try: + with transaction.atomic(): + old_file = item.photo + item.photo = File(old_file) + item.save() + except FileNotFoundError: + pass + + def get_attachment_real_url(self, pk): + if isinstance(pk, str): + pk = int(pk) + + Attachment = apps.get_model("attachments", "Attachment") + return Attachment.objects.get(pk=pk).attached_file.url + + def replace_matches(self, data): + matches = re.findall(self.trx, data) + + original_data = data + + if len(matches) == 0: + return data + + for url, attachment_id in matches: + new_url = self.get_attachment_real_url(attachment_id) + print("Match {} replaced by {}".format(url, new_url)) + + try: + data = data.replace(url, self.get_attachment_real_url(attachment_id)) + except Exception as e: + print("Exception found but ignoring:", e) + + assert data != original_data + + return data + + def process_userstories(self): + UserStory = apps.get_model("userstories", "UserStory") + qs = UserStory.objects.all() + + for item in iter_queryset(qs): + description = self.replace_matches(item.description) + UserStory.objects.filter(pk=item.pk).update(description=description) + + def process_tasks(self): + Task = apps.get_model("tasks", "Task") + qs = Task.objects.all() + + for item in iter_queryset(qs): + description = self.replace_matches(item.description) + Task.objects.filter(pk=item.pk).update(description=description) + + def process_issues(self): + Issue = apps.get_model("issues", "Issue") + qs = Issue.objects.all() + + for item in iter_queryset(qs): + description = self.replace_matches(item.description) + Issue.objects.filter(pk=item.pk).update(description=description) + + def process_wiki(self): + WikiPage = apps.get_model("wiki", "WikiPage") + qs = WikiPage.objects.all() + + for item in iter_queryset(qs): + content = self.replace_matches(item.content) + WikiPage.objects.filter(pk=item.pk).update(content=content) + + def process_history(self): + HistoryEntry = apps.get_model("history", "HistoryEntry") + qs = HistoryEntry.objects.all() + + for item in iter_queryset(qs): + comment = self.replace_matches(item.comment) + comment_html = self.replace_matches(item.comment_html) + HistoryEntry.objects.filter(pk=item.pk).update(comment=comment, comment_html=comment_html) From 6224a9d4ce26edcd99f132a252055ba567d4cd12 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 16 Sep 2014 16:38:38 +0200 Subject: [PATCH 8/8] Improve attachments factories and fix tests related to storage refactor. --- tests/factories.py | 5 ++ tests/integration/test_attachments.py | 69 ++++----------------------- 2 files changed, 14 insertions(+), 60 deletions(-) diff --git a/tests/factories.py b/tests/factories.py index 3a3728eb..12c266ac 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -99,6 +99,7 @@ class UserStoryAttachmentFactory(Factory): project = factory.SubFactory("tests.factories.ProjectFactory") owner = factory.SubFactory("tests.factories.UserFactory") content_object = factory.SubFactory("tests.factories.UserStoryFactory") + attached_file = factory.django.FileField(data=b"File contents") class Meta: model = "attachments.Attachment" @@ -109,6 +110,7 @@ class TaskAttachmentFactory(Factory): project = factory.SubFactory("tests.factories.ProjectFactory") owner = factory.SubFactory("tests.factories.UserFactory") content_object = factory.SubFactory("tests.factories.TaskFactory") + attached_file = factory.django.FileField(data=b"File contents") class Meta: model = "attachments.Attachment" @@ -119,15 +121,18 @@ class IssueAttachmentFactory(Factory): project = factory.SubFactory("tests.factories.ProjectFactory") owner = factory.SubFactory("tests.factories.UserFactory") content_object = factory.SubFactory("tests.factories.IssueFactory") + attached_file = factory.django.FileField(data=b"File contents") class Meta: model = "attachments.Attachment" strategy = factory.CREATE_STRATEGY + class WikiAttachmentFactory(Factory): project = factory.SubFactory("tests.factories.ProjectFactory") owner = factory.SubFactory("tests.factories.UserFactory") content_object = factory.SubFactory("tests.factories.WikiFactory") + attached_file = factory.django.FileField(data=b"File contents") class Meta: model = "attachments.Attachment" diff --git a/tests/integration/test_attachments.py b/tests/integration/test_attachments.py index 3c498101..2d8cc791 100644 --- a/tests/integration/test_attachments.py +++ b/tests/integration/test_attachments.py @@ -4,73 +4,22 @@ from django.core.urlresolvers import reverse from django.core.files.base import File from django.core.files.uploadedfile import SimpleUploadedFile -from .. import factories as f -from ..utils import set_settings - from taiga.projects.attachments.serializers import AttachmentSerializer +from .. import factories as f pytestmark = pytest.mark.django_db -def test_authentication(client): - "User can't access an attachment if not authenticated" - attachment = f.UserStoryAttachmentFactory.create() - url = reverse("attachment-url", kwargs={"pk": attachment.pk}) - - response = client.get(url) - - assert response.status_code == 401 - - -def test_authorization(client): - "User can't access an attachment if not authorized" - attachment = f.UserStoryAttachmentFactory.create() - user = f.UserFactory.create() - - url = reverse("attachment-url", kwargs={"pk": attachment.pk}) - - client.login(user) - response = client.get(url) - - assert response.status_code == 403 - - -@set_settings(IN_DEVELOPMENT_SERVER=True) -def test_attachment_redirect_in_devserver(client): - "When accessing the attachment in devserver redirect to the real attachment url" - attachment = f.UserStoryAttachmentFactory.create(attached_file="test") - - url = reverse("attachment-url", kwargs={"pk": attachment.pk}) - - client.login(attachment.owner) - response = client.get(url) - - assert response.status_code == 302 - - -@set_settings(IN_DEVELOPMENT_SERVER=False) -def test_attachment_redirect(client): - "When accessing the attachment redirect using X-Accel-Redirect header" - attachment = f.UserStoryAttachmentFactory.create() - - url = reverse("attachment-url", kwargs={"pk": attachment.pk}) - - client.login(attachment.owner) - response = client.get(url) - - assert response.status_code == 200 - assert response.has_header('x-accel-redirect') - - -# Bug test "Don't create attachments without attached_file" def test_create_user_story_attachment_without_file(client): + """ + Bug test "Don't create attachments without attached_file" + """ us = f.UserStoryFactory.create() - attachment = f.UserStoryAttachmentFactory(project=us.project, content_object=us) - - attachment_data = AttachmentSerializer(attachment).data - attachment_data["id"] = None - attachment_data["description"] = "test" - attachment_data["attached_file"] = None + attachment_data = { + "description": "test", + "attached_file": None, + "project": us.project_id, + } url = reverse('userstory-attachments-list')