Task #3515: Add logo field to project model

remotes/origin/logger
David Barragán Merino 2015-12-01 19:04:15 +01:00
parent 50e00b6d45
commit 0b7fe02a6f
27 changed files with 496 additions and 200 deletions

View File

@ -446,24 +446,38 @@ SOUTH_MIGRATION_MODULES = {
'easy_thumbnails': 'easy_thumbnails.south_migrations', 'easy_thumbnails': 'easy_thumbnails.south_migrations',
} }
DEFAULT_AVATAR_SIZE = 80 # 80x80 pixels
DEFAULT_BIG_AVATAR_SIZE = 300 # 300x300 pixels
DEFAULT_TIMELINE_IMAGE_SIZE = 640 # 640x??? pixels
DEFAUL_CARD_IMAGE_WIDTH = 300 # 300 pixels THN_AVATAR_SIZE = 80 # 80x80 pixels
DEFAUL_CARD_IMAGE_HEIGHT = 200 # 200 pixels THN_AVATAR_BIG_SIZE = 300 # 300x300 pixels
THN_LOGO_SMALL_SIZE = 80 # 80x80 pixels
THN_LOGO_BIG_SIZE = 300 # 300x300 pixels
THN_TIMELINE_IMAGE_SIZE = 640 # 640x??? pixels
THN_CARD_IMAGE_WIDTH = 300 # 300 pixels
THN_CARD_IMAGE_HEIGHT = 200 # 200 pixels
THN_AVATAR_SMALL = "avatar"
THN_AVATAR_BIG = "big-avatar"
THN_LOGO_SMALL = "logo-small"
THN_LOGO_BIG = "logo-big"
THN_ATTACHMENT_TIMELINE = "timeline-image"
THN_ATTACHMENT_CARD = "card-image"
THUMBNAIL_ALIASES = { THUMBNAIL_ALIASES = {
'': { "": {
'avatar': {'size': (DEFAULT_AVATAR_SIZE, DEFAULT_AVATAR_SIZE), 'crop': True}, THN_AVATAR_SMALL: {"size": (THN_AVATAR_SIZE, THN_AVATAR_SIZE), "crop": True},
'big-avatar': {'size': (DEFAULT_BIG_AVATAR_SIZE, DEFAULT_BIG_AVATAR_SIZE), 'crop': True}, THN_AVATAR_BIG: {"size": (THN_AVATAR_BIG_SIZE, THN_AVATAR_BIG_SIZE), "crop": True},
'timeline-image': {'size': (DEFAULT_TIMELINE_IMAGE_SIZE, 0), 'crop': True}, THN_LOGO_SMALL: {"size": (THN_LOGO_SMALL_SIZE, THN_LOGO_SMALL_SIZE), "crop": True},
'card-image': {'size': (DEFAUL_CARD_IMAGE_WIDTH, DEFAUL_CARD_IMAGE_HEIGHT), 'crop': True}, THN_LOGO_BIG: {"size": (THN_LOGO_BIG_SIZE, THN_LOGO_BIG_SIZE), "crop": True},
THN_ATTACHMENT_TIMELINE: {"size": (THN_TIMELINE_IMAGE_SIZE, 0), "crop": True},
THN_ATTACHMENT_CARD: {"size": (THN_CARD_IMAGE_WIDTH, THN_CARD_IMAGE_HEIGHT), "crop": True},
}, },
} }
# GRAVATAR_DEFAULT_AVATAR = "img/user-noimage.png" # GRAVATAR_DEFAULT_AVATAR = "img/user-noimage.png"
GRAVATAR_DEFAULT_AVATAR = "" GRAVATAR_DEFAULT_AVATAR = ""
GRAVATAR_AVATAR_SIZE = DEFAULT_AVATAR_SIZE GRAVATAR_AVATAR_SIZE = THN_AVATAR_SIZE
TAGS_PREDEFINED_COLORS = ["#fce94f", "#edd400", "#c4a000", "#8ae234", TAGS_PREDEFINED_COLORS = ["#fce94f", "#edd400", "#c4a000", "#8ae234",
"#73d216", "#4e9a06", "#d3d7cf", "#fcaf3e", "#73d216", "#4e9a06", "#d3d7cf", "#fcaf3e",

View File

@ -17,7 +17,12 @@
from django.apps import AppConfig from django.apps import AppConfig
from .signals.thumbnails import connect_thumbnail_signals
class BaseAppConfig(AppConfig): class BaseAppConfig(AppConfig):
name = "taiga.base" name = "taiga.base"
verbose_name = "Base App Config" verbose_name = "Base App Config"
def ready(self):
connect_thumbnail_signals()

View File

View File

@ -0,0 +1,31 @@
# Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014-2015 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2015 David Barragán <bameda@dbarragan.com>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django_transactional_cleanup.signals import cleanup_post_delete
from easy_thumbnails.files import get_thumbnailer
def _delete_thumbnail_files(**kwargs):
thumbnailer = get_thumbnailer(kwargs["file"])
thumbnailer.delete_thumbnails()
def connect_thumbnail_signals():
cleanup_post_delete.connect(_delete_thumbnail_files)
def disconnect_thumbnail_signals():
cleanup_post_delete.disconnect(_delete_thumbnail_files)

View File

@ -0,0 +1,30 @@
# Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014-2015 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2015 David Barragán <bameda@dbarragan.com>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from taiga.base.utils.urls import get_absolute_url
from easy_thumbnails.files import get_thumbnailer
from easy_thumbnails.exceptions import InvalidImageFormatError
def get_thumbnail_url(file_obj, thumbnailer_size):
try:
path_url = get_thumbnailer(file_obj)[thumbnailer_size].url
thumb_url = get_absolute_url(path_url)
except InvalidImageFormatError:
thumb_url = None
return thumb_url

View File

@ -50,7 +50,7 @@ from taiga.projects.votes import services as votes_service
from taiga.projects.history import services as history_service from taiga.projects.history import services as history_service
class AttachedFileField(serializers.WritableField): class FileField(serializers.WritableField):
read_only = False read_only = False
def to_native(self, obj): def to_native(self, obj):
@ -308,7 +308,7 @@ class HistoryExportSerializerMixin(serializers.ModelSerializer):
class AttachmentExportSerializer(serializers.ModelSerializer): class AttachmentExportSerializer(serializers.ModelSerializer):
owner = UserRelatedField(required=False) owner = UserRelatedField(required=False)
attached_file = AttachedFileField() attached_file = FileField()
modified_date = serializers.DateTimeField(required=False) modified_date = serializers.DateTimeField(required=False)
class Meta: class Meta:
@ -643,7 +643,21 @@ class TimelineExportSerializer(serializers.ModelSerializer):
class ProjectExportSerializer(WatcheableObjectModelSerializer): class ProjectExportSerializer(WatcheableObjectModelSerializer):
logo = FileField(required=False)
anon_permissions = PgArrayField(required=False)
public_permissions = PgArrayField(required=False)
modified_date = serializers.DateTimeField(required=False)
roles = RoleExportSerializer(many=True, required=False)
owner = UserRelatedField(required=False) owner = UserRelatedField(required=False)
memberships = MembershipExportSerializer(many=True, required=False)
points = PointsExportSerializer(many=True, required=False)
us_statuses = UserStoryStatusExportSerializer(many=True, required=False)
task_statuses = TaskStatusExportSerializer(many=True, required=False)
issue_types = IssueTypeExportSerializer(many=True, required=False)
issue_statuses = IssueStatusExportSerializer(many=True, required=False)
priorities = PriorityExportSerializer(many=True, required=False)
severities = SeverityExportSerializer(many=True, required=False)
tags_colors = JsonField(required=False)
default_points = serializers.SlugRelatedField(slug_field="name", required=False) default_points = serializers.SlugRelatedField(slug_field="name", required=False)
default_us_status = serializers.SlugRelatedField(slug_field="name", required=False) default_us_status = serializers.SlugRelatedField(slug_field="name", required=False)
default_task_status = serializers.SlugRelatedField(slug_field="name", required=False) default_task_status = serializers.SlugRelatedField(slug_field="name", required=False)
@ -651,28 +665,15 @@ class ProjectExportSerializer(WatcheableObjectModelSerializer):
default_severity = serializers.SlugRelatedField(slug_field="name", required=False) default_severity = serializers.SlugRelatedField(slug_field="name", required=False)
default_issue_status = serializers.SlugRelatedField(slug_field="name", required=False) default_issue_status = serializers.SlugRelatedField(slug_field="name", required=False)
default_issue_type = serializers.SlugRelatedField(slug_field="name", required=False) default_issue_type = serializers.SlugRelatedField(slug_field="name", required=False)
memberships = MembershipExportSerializer(many=True, required=False)
points = PointsExportSerializer(many=True, required=False)
us_statuses = UserStoryStatusExportSerializer(many=True, required=False)
task_statuses = TaskStatusExportSerializer(many=True, required=False)
issue_statuses = IssueStatusExportSerializer(many=True, required=False)
priorities = PriorityExportSerializer(many=True, required=False)
severities = SeverityExportSerializer(many=True, required=False)
issue_types = IssueTypeExportSerializer(many=True, required=False)
userstorycustomattributes = UserStoryCustomAttributeExportSerializer(many=True, required=False) userstorycustomattributes = UserStoryCustomAttributeExportSerializer(many=True, required=False)
taskcustomattributes = TaskCustomAttributeExportSerializer(many=True, required=False) taskcustomattributes = TaskCustomAttributeExportSerializer(many=True, required=False)
issuecustomattributes = IssueCustomAttributeExportSerializer(many=True, required=False) issuecustomattributes = IssueCustomAttributeExportSerializer(many=True, required=False)
roles = RoleExportSerializer(many=True, required=False)
milestones = MilestoneExportSerializer(many=True, required=False)
wiki_pages = WikiPageExportSerializer(many=True, required=False)
wiki_links = WikiLinkExportSerializer(many=True, required=False)
user_stories = UserStoryExportSerializer(many=True, required=False) user_stories = UserStoryExportSerializer(many=True, required=False)
tasks = TaskExportSerializer(many=True, required=False) tasks = TaskExportSerializer(many=True, required=False)
milestones = MilestoneExportSerializer(many=True, required=False)
issues = IssueExportSerializer(many=True, required=False) issues = IssueExportSerializer(many=True, required=False)
tags_colors = JsonField(required=False) wiki_links = WikiLinkExportSerializer(many=True, required=False)
anon_permissions = PgArrayField(required=False) wiki_pages = WikiPageExportSerializer(many=True, required=False)
public_permissions = PgArrayField(required=False)
modified_date = serializers.DateTimeField(required=False)
timeline = serializers.SerializerMethodField("get_timeline") timeline = serializers.SerializerMethodField("get_timeline")
class Meta: class Meta:

View File

@ -120,7 +120,8 @@ def render_project(project, outfile, chunk_size = 8190):
b64_data = base64.b64encode(bin_data).decode('utf-8') b64_data = base64.b64encode(bin_data).decode('utf-8')
outfile.write(b64_data) outfile.write(b64_data)
outfile.write('", \n "name":"{}"}}\n}}'.format(os.path.basename(attachment_file.name))) outfile.write('", \n "name":"{}"}}\n}}'.format(
os.path.basename(attachment_file.name)))
outfile.write(']}') outfile.write(']}')
outfile.flush() outfile.flush()
@ -324,8 +325,8 @@ def store_task(project, data):
custom_attributes_values = data.get("custom_attributes_values", None) custom_attributes_values = data.get("custom_attributes_values", None)
if custom_attributes_values: if custom_attributes_values:
custom_attributes = serialized.object.project.taskcustomattributes.all().values('id', 'name') custom_attributes = serialized.object.project.taskcustomattributes.all().values('id', 'name')
custom_attributes_values = _use_id_instead_name_as_key_in_custom_attributes_values(custom_attributes, custom_attributes_values = _use_id_instead_name_as_key_in_custom_attributes_values(
custom_attributes_values) custom_attributes, custom_attributes_values)
store_custom_attributes_values(serialized.object, custom_attributes_values, store_custom_attributes_values(serialized.object, custom_attributes_values,
"task", serializers.TaskCustomAttributesValuesExportSerializer) "task", serializers.TaskCustomAttributesValuesExportSerializer)
@ -457,7 +458,8 @@ def store_user_story(project, data):
if "status" not in data and project.default_us_status: if "status" not in data and project.default_us_status:
data["status"] = project.default_us_status.name data["status"] = project.default_us_status.name
us_data = {key: value for key, value in data.items() if key not in ["role_points", "custom_attributes_values"]} us_data = {key: value for key, value in data.items() if key not in
["role_points", "custom_attributes_values"]}
serialized = serializers.UserStoryExportSerializer(data=us_data, context={"project": project}) serialized = serializers.UserStoryExportSerializer(data=us_data, context={"project": project})
if serialized.is_valid(): if serialized.is_valid():
@ -495,8 +497,8 @@ def store_user_story(project, data):
custom_attributes_values = data.get("custom_attributes_values", None) custom_attributes_values = data.get("custom_attributes_values", None)
if custom_attributes_values: if custom_attributes_values:
custom_attributes = serialized.object.project.userstorycustomattributes.all().values('id', 'name') custom_attributes = serialized.object.project.userstorycustomattributes.all().values('id', 'name')
custom_attributes_values = _use_id_instead_name_as_key_in_custom_attributes_values(custom_attributes, custom_attributes_values = _use_id_instead_name_as_key_in_custom_attributes_values(
custom_attributes_values) custom_attributes, custom_attributes_values)
store_custom_attributes_values(serialized.object, custom_attributes_values, store_custom_attributes_values(serialized.object, custom_attributes_values,
"user_story", serializers.UserStoryCustomAttributesValuesExportSerializer) "user_story", serializers.UserStoryCustomAttributesValuesExportSerializer)
@ -553,8 +555,8 @@ def store_issue(project, data):
custom_attributes_values = data.get("custom_attributes_values", None) custom_attributes_values = data.get("custom_attributes_values", None)
if custom_attributes_values: if custom_attributes_values:
custom_attributes = serialized.object.project.issuecustomattributes.all().values('id', 'name') custom_attributes = serialized.object.project.issuecustomattributes.all().values('id', 'name')
custom_attributes_values = _use_id_instead_name_as_key_in_custom_attributes_values(custom_attributes, custom_attributes_values = _use_id_instead_name_as_key_in_custom_attributes_values(
custom_attributes_values) custom_attributes, custom_attributes_values)
store_custom_attributes_values(serialized.object, custom_attributes_values, store_custom_attributes_values(serialized.object, custom_attributes_values,
"issue", serializers.IssueCustomAttributesValuesExportSerializer) "issue", serializers.IssueCustomAttributesValuesExportSerializer)

View File

@ -17,6 +17,8 @@
import uuid import uuid
from easy_thumbnails.source_generators import pil_image
from django.apps import apps from django.apps import apps
from django.db.models import signals, Prefetch from django.db.models import signals, Prefetch
from django.db.models import Value as V from django.db.models import Value as V
@ -137,6 +139,42 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
return serializer_class return serializer_class
@detail_route(methods=["POST"])
def change_logo(self, request, *args, **kwargs):
"""
Change logo to this project.
"""
self.object = get_object_or_404(self.get_queryset(), **kwargs)
self.check_permissions(request, "change_logo", self.object)
logo = request.FILES.get('logo', None)
if not logo:
raise exc.WrongArguments(_("Incomplete arguments"))
try:
pil_image(logo)
except Exception:
raise exc.WrongArguments(_("Invalid image format"))
self.object.logo = logo
self.object.save(update_fields=["logo"])
serializer = self.get_serializer(self.object)
return response.Ok(serializer.data)
@detail_route(methods=["POST"])
def remove_logo(self, request, *args, **kwargs):
"""
Remove the logo of a project.
"""
self.object = get_object_or_404(self.get_queryset(), **kwargs)
self.check_permissions(request, "remove_logo", self.object)
self.object.logo = None
self.object.save(update_fields=["logo"])
serializer = self.get_serializer(self.object)
return response.Ok(serializer.data)
@detail_route(methods=["POST"]) @detail_route(methods=["POST"])
def watch(self, request, pk=None): def watch(self, request, pk=None):
project = self.get_object() project = self.get_object()
@ -288,9 +326,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, ModelCrudViewSet)
def pre_save(self, obj): def pre_save(self, obj):
if not obj.id: if not obj.id:
obj.owner = self.request.user obj.owner = self.request.user
# TODO REFACTOR THIS
# TODO REFACTOR THIS
if not obj.id:
obj.template = self.request.QUERY_PARAMS.get('template', None) obj.template = self.request.QUERY_PARAMS.get('template', None)
self._set_base_permissions(obj) self._set_base_permissions(obj)

View File

@ -22,60 +22,78 @@ from django.db.models import signals
from . import signals as handlers from . import signals as handlers
def connect_memberships_signals(): ## Project Signals
# On membership object is deleted, update role-points relation.
signals.pre_delete.connect(handlers.membership_post_delete,
sender=apps.get_model("projects", "Membership"),
dispatch_uid='membership_pre_delete')
# On membership object is deleted, update notify policies of all objects relation.
signals.post_save.connect(handlers.create_notify_policy,
sender=apps.get_model("projects", "Membership"),
dispatch_uid='create-notify-policy')
def connect_projects_signals(): def connect_projects_signals():
# On project object is created apply template. # On project object is created apply template.
signals.post_save.connect(handlers.project_post_save, signals.post_save.connect(handlers.project_post_save,
sender=apps.get_model("projects", "Project"), sender=apps.get_model("projects", "Project"),
dispatch_uid='project_post_save') dispatch_uid='project_post_save')
# Tags # Tags normalization after save a project
signals.pre_save.connect(handlers.tags_normalization, signals.pre_save.connect(handlers.tags_normalization,
sender=apps.get_model("projects", "Project"), sender=apps.get_model("projects", "Project"),
dispatch_uid="tags_normalization_projects") dispatch_uid="tags_normalization_projects")
signals.pre_save.connect(handlers.update_project_tags_when_create_or_edit_taggable_item, signals.pre_save.connect(handlers.update_project_tags_when_create_or_edit_taggable_item,
sender=apps.get_model("projects", "Project"), sender=apps.get_model("projects", "Project"),
dispatch_uid="update_project_tags_when_create_or_edit_taggable_item_projects") dispatch_uid="update_project_tags_when_create_or_edit_taggable_item_projects")
def connect_us_status_signals():
signals.post_save.connect(handlers.try_to_close_or_open_user_stories_when_edit_us_status,
sender=apps.get_model("projects", "UserStoryStatus"),
dispatch_uid="try_to_close_or_open_user_stories_when_edit_us_status")
def connect_task_status_signals():
signals.post_save.connect(handlers.try_to_close_or_open_user_stories_when_edit_task_status,
sender=apps.get_model("projects", "TaskStatus"),
dispatch_uid="try_to_close_or_open_user_stories_when_edit_task_status")
def disconnect_memberships_signals():
signals.pre_delete.disconnect(sender=apps.get_model("projects", "Membership"), dispatch_uid='membership_pre_delete')
signals.post_save.disconnect(sender=apps.get_model("projects", "Membership"), dispatch_uid='create-notify-policy')
def disconnect_projects_signals(): def disconnect_projects_signals():
signals.post_save.disconnect(sender=apps.get_model("projects", "Project"), dispatch_uid='project_post_save') signals.post_save.disconnect(sender=apps.get_model("projects", "Project"),
signals.pre_save.disconnect(sender=apps.get_model("projects", "Project"), dispatch_uid="tags_normalization_projects") dispatch_uid='project_post_save')
signals.pre_save.disconnect(sender=apps.get_model("projects", "Project"), dispatch_uid="update_project_tags_when_create_or_edit_taggable_item_projects") signals.pre_save.disconnect(sender=apps.get_model("projects", "Project"),
dispatch_uid="tags_normalization_projects")
signals.pre_save.disconnect(sender=apps.get_model("projects", "Project"),
dispatch_uid="update_project_tags_when_create_or_edit_taggable_item_projects")
## Memberships Signals
def connect_memberships_signals():
# On membership object is deleted, update role-points relation.
signals.pre_delete.connect(handlers.membership_post_delete,
sender=apps.get_model("projects", "Membership"),
dispatch_uid='membership_pre_delete')
# On membership object is deleted, update notify policies of all objects relation.
signals.post_save.connect(handlers.create_notify_policy,
sender=apps.get_model("projects", "Membership"),
dispatch_uid='create-notify-policy')
def disconnect_memberships_signals():
signals.pre_delete.disconnect(sender=apps.get_model("projects", "Membership"),
dispatch_uid='membership_pre_delete')
signals.post_save.disconnect(sender=apps.get_model("projects", "Membership"),
dispatch_uid='create-notify-policy')
## US Statuses Signals
def connect_us_status_signals():
signals.post_save.connect(handlers.try_to_close_or_open_user_stories_when_edit_us_status,
sender=apps.get_model("projects", "UserStoryStatus"),
dispatch_uid="try_to_close_or_open_user_stories_when_edit_us_status")
def disconnect_us_status_signals(): def disconnect_us_status_signals():
signals.post_save.disconnect(sender=apps.get_model("projects", "UserStoryStatus"), dispatch_uid="try_to_close_or_open_user_stories_when_edit_us_status") signals.post_save.disconnect(sender=apps.get_model("projects", "UserStoryStatus"),
dispatch_uid="try_to_close_or_open_user_stories_when_edit_us_status")
## Tasks Statuses Signals
def connect_task_status_signals():
signals.post_save.connect(handlers.try_to_close_or_open_user_stories_when_edit_task_status,
sender=apps.get_model("projects", "TaskStatus"),
dispatch_uid="try_to_close_or_open_user_stories_when_edit_task_status")
def disconnect_task_status_signals(): def disconnect_task_status_signals():
signals.post_save.disconnect(sender=apps.get_model("projects", "TaskStatus"), dispatch_uid="try_to_close_or_open_user_stories_when_edit_task_status") signals.post_save.disconnect(sender=apps.get_model("projects", "TaskStatus"),
dispatch_uid="try_to_close_or_open_user_stories_when_edit_task_status")
class ProjectsAppConfig(AppConfig): class ProjectsAppConfig(AppConfig):
@ -83,7 +101,7 @@ class ProjectsAppConfig(AppConfig):
verbose_name = "Projects" verbose_name = "Projects"
def ready(self): def ready(self):
connect_memberships_signals()
connect_projects_signals() connect_projects_signals()
connect_memberships_signals()
connect_us_status_signals() connect_us_status_signals()
connect_task_status_signals() connect_task_status_signals()

View File

@ -15,25 +15,10 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from easy_thumbnails.files import get_thumbnailer
from django.apps import AppConfig from django.apps import AppConfig
from django.apps import apps from django.apps import apps
from django_transactional_cleanup.signals import cleanup_post_delete
def thumbnail_delete(**kwargs):
thumbnailer = get_thumbnailer(kwargs["file"])
thumbnailer.delete_thumbnails()
def connect_attachment_signals():
cleanup_post_delete.connect(thumbnail_delete)
class AttachmentsAppConfig(AppConfig): class AttachmentsAppConfig(AppConfig):
name = "taiga.projects.attachments" name = "taiga.projects.attachments"
verbose_name = "Attachments" verbose_name = "Attachments"
def ready(self):
connect_attachment_signals()

View File

@ -37,4 +37,4 @@ class AttachmentSerializer(serializers.ModelSerializer):
return obj.attached_file.url return obj.attached_file.url
def get_thumbnail_card_url(self, obj): def get_thumbnail_card_url(self, obj):
return services.get_card_image_thumbnailer_url(obj) return services.get_card_image_thumbnail_url(obj)

View File

@ -12,25 +12,18 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from taiga.base.utils.urls import get_absolute_url from django.conf import settings
from easy_thumbnails.files import get_thumbnailer from taiga.base.utils.thumbnails import get_thumbnail_url
from easy_thumbnails.exceptions import InvalidImageFormatError
def _get_attachment_thumbnailer_url(attachment, thumbnailer_size): def get_timeline_image_thumbnail_url(attachment):
try: if attachment.attached_file:
thumb_url = get_thumbnailer(attachment.attached_file)[thumbnailer_size].url return get_thumbnail_url(attachment.attached_file, settings.THN_ATTACHMENT_TIMELINE)
thumb_url = get_absolute_url(thumb_url) return None
except InvalidImageFormatError:
thumb_url = None
return thumb_url
def get_timeline_image_thumbnailer_url(attachment): def get_card_image_thumbnail_url(attachment):
return _get_attachment_thumbnailer_url(attachment, "timeline-image") if attachment.attached_file:
return get_thumbnail_url(attachment.attached_file, settings.THN_ATTACHMENT_CARD)
return None
def get_card_image_thumbnailer_url(attachment):
return _get_attachment_thumbnailer_url(attachment, "card-image")

View File

@ -27,7 +27,7 @@ from taiga.base.utils.iterators import as_tuple
from taiga.base.utils.iterators import as_dict from taiga.base.utils.iterators import as_dict
from taiga.mdrender.service import render as mdrender from taiga.mdrender.service import render as mdrender
from taiga.projects.attachments.services import get_timeline_image_thumbnailer_url from taiga.projects.attachments.services import get_timeline_image_thumbnail_url
import os import os
@ -178,7 +178,7 @@ def _generic_extract(obj:object, fields:list, default=None) -> dict:
@as_tuple @as_tuple
def extract_attachments(obj) -> list: def extract_attachments(obj) -> list:
for attach in obj.attachments.all(): for attach in obj.attachments.all():
thumb_url = get_timeline_image_thumbnailer_url(attach) thumb_url = get_timeline_image_thumbnail_url(attach)
yield {"id": attach.id, yield {"id": attach.id,
"filename": os.path.basename(attach.attached_file.name), "filename": os.path.basename(attach.attached_file.name),

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import taiga.projects.models
class Migration(migrations.Migration):
dependencies = [
('projects', '0030_auto_20151128_0757'),
]
operations = [
migrations.AddField(
model_name='project',
name='logo',
field=models.FileField(null=True, blank=True, upload_to=taiga.projects.models.get_user_file_path, verbose_name='logo', max_length=500),
),
]

View File

@ -15,9 +15,13 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import hashlib
import os
import os.path as path
import itertools import itertools
import uuid import uuid
from unidecode import unidecode
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
@ -27,18 +31,22 @@ from django.conf import settings
from django.dispatch import receiver from django.dispatch import receiver
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.template.defaultfilters import slugify
from django.utils.encoding import force_bytes
from django.utils import timezone from django.utils import timezone
from django_pgjson.fields import JsonField from django_pgjson.fields import JsonField
from djorm_pgarray.fields import TextArrayField from djorm_pgarray.fields import TextArrayField
from taiga.permissions.permissions import ANON_PERMISSIONS, MEMBERS_PERMISSIONS
from taiga.base.tags import TaggedMixin from taiga.base.tags import TaggedMixin
from taiga.base.utils.slug import slugify_uniquely
from taiga.base.utils.dicts import dict_sum from taiga.base.utils.dicts import dict_sum
from taiga.base.utils.iterators import split_by_n
from taiga.base.utils.sequence import arithmetic_progression from taiga.base.utils.sequence import arithmetic_progression
from taiga.base.utils.slug import slugify_uniquely
from taiga.base.utils.slug import slugify_uniquely_for_queryset from taiga.base.utils.slug import slugify_uniquely_for_queryset
from taiga.permissions.permissions import ANON_PERMISSIONS, MEMBERS_PERMISSIONS
from taiga.projects.notifications.choices import NotifyLevel from taiga.projects.notifications.choices import NotifyLevel
from taiga.projects.notifications.services import ( from taiga.projects.notifications.services import (
get_notify_policy, get_notify_policy,
@ -53,6 +61,22 @@ from . import choices
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
def get_user_file_path(instance, filename):
basename = path.basename(filename).lower()
base, ext = path.splitext(basename)
base = slugify(unidecode(base))
basename = "".join([base, ext])
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("project", hash_part, basename)
class Membership(models.Model): class Membership(models.Model):
# This model stores all project memberships. Also # This model stores all project memberships. Also
# stores invitations to memberships that does not have # stores invitations to memberships that does not have
@ -141,6 +165,11 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
verbose_name=_("slug")) verbose_name=_("slug"))
description = models.TextField(null=False, blank=False, description = models.TextField(null=False, blank=False,
verbose_name=_("description")) verbose_name=_("description"))
logo = models.FileField(upload_to=get_user_file_path,
max_length=500, null=True, blank=True,
verbose_name=_("logo"))
created_date = models.DateTimeField(null=False, blank=False, created_date = models.DateTimeField(null=False, blank=False,
verbose_name=_("created date"), verbose_name=_("created date"),
default=timezone.now) default=timezone.now)

View File

@ -53,6 +53,8 @@ class ProjectPermission(TaigaResourcePermission):
destroy_perms = IsProjectOwner() destroy_perms = IsProjectOwner()
modules_perms = IsProjectOwner() modules_perms = IsProjectOwner()
list_perms = AllowAny() list_perms = AllowAny()
change_logo_perms = IsProjectOwner()
remove_logo_perms = IsProjectOwner()
stats_perms = HasProjectPerm('view_project') stats_perms = HasProjectPerm('view_project')
member_stats_perms = HasProjectPerm('view_project') member_stats_perms = HasProjectPerm('view_project')
issues_stats_perms = HasProjectPerm('view_project') issues_stats_perms = HasProjectPerm('view_project')

View File

@ -20,7 +20,6 @@ from django.utils.translation import ugettext as _
from django.db.models import Q from django.db.models import Q
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.base.fields import JsonField from taiga.base.fields import JsonField
from taiga.base.fields import PgArrayField from taiga.base.fields import PgArrayField
from taiga.base.fields import TagsField from taiga.base.fields import TagsField
@ -36,8 +35,6 @@ from taiga.users.validators import RoleExistsValidator
from taiga.permissions.service import get_user_project_permissions from taiga.permissions.service import get_user_project_permissions
from taiga.permissions.service import is_project_owner from taiga.permissions.service import is_project_owner
from taiga.projects.notifications import models as notify_models
from . import models from . import models
from . import services from . import services
from .notifications.mixins import WatchedResourceModelSerializer from .notifications.mixins import WatchedResourceModelSerializer
@ -47,6 +44,7 @@ from .custom_attributes.serializers import TaskCustomAttributeSerializer
from .custom_attributes.serializers import IssueCustomAttributeSerializer from .custom_attributes.serializers import IssueCustomAttributeSerializer
from .likes.mixins.serializers import FanResourceSerializerMixin from .likes.mixins.serializers import FanResourceSerializerMixin
###################################################### ######################################################
## Custom values for selectors ## Custom values for selectors
###################################################### ######################################################
@ -75,7 +73,6 @@ class PointsSerializer(serializers.ModelSerializer):
class UserStoryStatusSerializer(serializers.ModelSerializer): class UserStoryStatusSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = models.UserStoryStatus model = models.UserStoryStatus
i18n_fields = ("name",) i18n_fields = ("name",)
@ -101,7 +98,6 @@ class UserStoryStatusSerializer(serializers.ModelSerializer):
class BasicUserStoryStatusSerializer(serializers.ModelSerializer): class BasicUserStoryStatusSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = models.UserStoryStatus model = models.UserStoryStatus
i18n_fields = ("name",) i18n_fields = ("name",)
@ -175,7 +171,6 @@ class IssueStatusSerializer(serializers.ModelSerializer):
class BasicIssueStatusSerializer(serializers.ModelSerializer): class BasicIssueStatusSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = models.IssueStatus model = models.IssueStatus
i18n_fields = ("name",) i18n_fields = ("name",)
@ -300,7 +295,8 @@ class ProjectMemberSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = models.Membership model = models.Membership
exclude = ("project", "email", "created_at", "token", "invited_by", "invitation_extra_text", "user_order") exclude = ("project", "email", "created_at", "token", "invited_by", "invitation_extra_text",
"user_order")
def get_photo(self, membership): def get_photo(self, membership):
return get_photo_or_gravatar_url(membership.user) return get_photo_or_gravatar_url(membership.user)
@ -310,21 +306,27 @@ class ProjectMemberSerializer(serializers.ModelSerializer):
## Projects ## Projects
###################################################### ######################################################
class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializer, serializers.ModelSerializer): class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializer,
tags = TagsField(default=[], required=False) serializers.ModelSerializer):
anon_permissions = PgArrayField(required=False) anon_permissions = PgArrayField(required=False)
public_permissions = PgArrayField(required=False) public_permissions = PgArrayField(required=False)
my_permissions = serializers.SerializerMethodField("get_my_permissions") my_permissions = serializers.SerializerMethodField("get_my_permissions")
i_am_owner = serializers.SerializerMethodField("get_i_am_owner") i_am_owner = serializers.SerializerMethodField("get_i_am_owner")
tags = TagsField(default=[], required=False)
tags_colors = TagsColorsField(required=False) tags_colors = TagsColorsField(required=False)
notify_level = serializers.SerializerMethodField("get_notify_level")
total_closed_milestones = serializers.SerializerMethodField("get_total_closed_milestones") total_closed_milestones = serializers.SerializerMethodField("get_total_closed_milestones")
notify_level = serializers.SerializerMethodField("get_notify_level")
total_watchers = serializers.SerializerMethodField("get_total_watchers") total_watchers = serializers.SerializerMethodField("get_total_watchers")
logo_small_url = serializers.SerializerMethodField("get_logo_small_url")
logo_big_url = serializers.SerializerMethodField("get_logo_big_url")
class Meta: class Meta:
model = models.Project model = models.Project
read_only_fields = ("created_date", "modified_date", "owner", "slug") read_only_fields = ("created_date", "modified_date", "owner", "slug")
exclude = ("last_us_ref", "last_task_ref", "last_issue_ref", exclude = ("logo", "last_us_ref", "last_task_ref", "last_issue_ref",
"issues_csv_uuid", "tasks_csv_uuid", "userstories_csv_uuid") "issues_csv_uuid", "tasks_csv_uuid", "userstories_csv_uuid")
def get_my_permissions(self, obj): def get_my_permissions(self, obj):
@ -360,6 +362,12 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ
return obj.notify_policies.exclude(notify_level=NotifyLevel.none).count() return obj.notify_policies.exclude(notify_level=NotifyLevel.none).count()
def get_logo_small_url(self, obj):
return services.get_logo_small_thumbnail_url(obj)
def get_logo_big_url(self, obj):
return services.get_logo_big_thumbnail_url(obj)
class ProjectDetailSerializer(ProjectSerializer): class ProjectDetailSerializer(ProjectSerializer):
us_statuses = UserStoryStatusSerializer(many=True, required=False) # User Stories us_statuses = UserStoryStatusSerializer(many=True, required=False) # User Stories
@ -395,7 +403,7 @@ class ProjectDetailAdminSerializer(ProjectDetailSerializer):
class Meta: class Meta:
model = models.Project model = models.Project
read_only_fields = ("created_date", "modified_date", "owner", "slug") read_only_fields = ("created_date", "modified_date", "owner", "slug")
exclude = ("last_us_ref", "last_task_ref", "last_issue_ref") exclude = ("logo", "last_us_ref", "last_task_ref", "last_issue_ref")
###################################################### ######################################################

View File

@ -29,17 +29,20 @@ from .bulk_update_order import bulk_update_userstory_status_order
from .filters import get_all_tags from .filters import get_all_tags
from .stats import get_stats_for_project_issues from .invitations import send_invitation
from .stats import get_stats_for_project from .invitations import find_invited_user
from .stats import get_member_stats_for_project
from .logo import get_logo_small_thumbnail_url
from .logo import get_logo_big_thumbnail_url
from .members import create_members_in_bulk from .members import create_members_in_bulk
from .members import get_members_from_bulk from .members import get_members_from_bulk
from .members import remove_user_from_project, project_has_valid_owners, can_user_leave_project from .members import remove_user_from_project, project_has_valid_owners, can_user_leave_project
from .invitations import send_invitation from .modules_config import get_modules_config
from .invitations import find_invited_user
from .stats import get_stats_for_project_issues
from .stats import get_stats_for_project
from .stats import get_member_stats_for_project
from .tags_colors import update_project_tags_colors_handler from .tags_colors import update_project_tags_colors_handler
from .modules_config import get_modules_config

View File

@ -0,0 +1,29 @@
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.conf import settings
from taiga.base.utils.thumbnails import get_thumbnail_url
def get_logo_small_thumbnail_url(project):
if project.logo:
return get_thumbnail_url(project.logo, settings.THN_LOGO_SMALL)
return None
def get_logo_big_thumbnail_url(project):
if project.logo:
return get_thumbnail_url(project.logo, settings.THN_LOGO_BIG)
return None

View File

@ -22,6 +22,8 @@ from taiga.projects.services.tags_colors import update_project_tags_colors_handl
from taiga.projects.notifications.services import create_notify_policy_if_not_exists from taiga.projects.notifications.services import create_notify_policy_if_not_exists
from taiga.base.utils.db import get_typename_for_model_class from taiga.base.utils.db import get_typename_for_model_class
from easy_thumbnails.files import get_thumbnailer
#################################### ####################################
# Signals over project items # Signals over project items
@ -46,11 +48,15 @@ def membership_post_delete(sender, instance, using, **kwargs):
instance.project.update_role_points() instance.project.update_role_points()
## Notify policy
def create_notify_policy(sender, instance, using, **kwargs): def create_notify_policy(sender, instance, using, **kwargs):
if instance.user: if instance.user:
create_notify_policy_if_not_exists(instance.project, instance.user) create_notify_policy_if_not_exists(instance.project, instance.user)
## Project attributes
def project_post_save(sender, instance, created, **kwargs): def project_post_save(sender, instance, created, **kwargs):
""" """
Populate new project dependen default data Populate new project dependen default data
@ -82,6 +88,8 @@ def project_post_save(sender, instance, created, **kwargs):
is_owner=True, email=instance.owner.email) is_owner=True, email=instance.owner.email)
## US statuses
def try_to_close_or_open_user_stories_when_edit_us_status(sender, instance, created, **kwargs): def try_to_close_or_open_user_stories_when_edit_us_status(sender, instance, created, **kwargs):
from taiga.projects.userstories import services from taiga.projects.userstories import services
@ -92,6 +100,8 @@ def try_to_close_or_open_user_stories_when_edit_us_status(sender, instance, crea
services.open_userstory(user_story) services.open_userstory(user_story)
## Task statuses
def try_to_close_or_open_user_stories_when_edit_task_status(sender, instance, created, **kwargs): def try_to_close_or_open_user_stories_when_edit_task_status(sender, instance, created, **kwargs):
from taiga.projects.userstories import services from taiga.projects.userstories import services

View File

@ -223,7 +223,6 @@ class UsersViewSet(ModelCrudViewSet):
except Exception: except Exception:
raise exc.WrongArguments(_("Invalid image format")) raise exc.WrongArguments(_("Invalid image format"))
request.user.delete_photo()
request.user.photo = avatar request.user.photo = avatar
request.user.save(update_fields=["photo"]) request.user.save(update_fields=["photo"])
user_data = self.admin_serializer_class(request.user).data user_data = self.admin_serializer_class(request.user).data
@ -236,7 +235,7 @@ class UsersViewSet(ModelCrudViewSet):
Remove the avatar of current logged user. Remove the avatar of current logged user.
""" """
self.check_permissions(request, "remove_avatar", None) self.check_permissions(request, "remove_avatar", None)
request.user.delete_photo() request.user.photo = None
request.user.save(update_fields=["photo"]) request.user.save(update_fields=["photo"])
user_data = self.admin_serializer_class(request.user).data user_data = self.admin_serializer_class(request.user).data
return response.Ok(user_data) return response.Ok(user_data)

View File

@ -241,22 +241,10 @@ class User(AbstractBaseUser, PermissionsMixin):
self.colorize_tags = True self.colorize_tags = True
self.token = None self.token = None
self.set_unusable_password() self.set_unusable_password()
self.delete_photo() self.photo = None
self.save() self.save()
self.auth_data.all().delete() self.auth_data.all().delete()
def delete_photo(self):
# Removing thumbnails
thumbnailer = get_thumbnailer(self.photo)
thumbnailer.delete_thumbnails()
# Removing original photo
if self.photo:
storage, path = self.photo.storage, self.photo.path
storage.delete(path)
self.photo = None
class Role(models.Model): class Role(models.Model):
name = models.CharField(max_length=200, null=False, blank=False, name = models.CharField(max_length=200, null=False, blank=False,

View File

@ -34,9 +34,9 @@ from taiga.base.utils.db import to_tsquery
from taiga.base.utils.urls import get_absolute_url from taiga.base.utils.urls import get_absolute_url
from taiga.projects.notifications.choices import NotifyLevel from taiga.projects.notifications.choices import NotifyLevel
from taiga.projects.notifications.services import get_projects_watched from taiga.projects.notifications.services import get_projects_watched
from .gravatar import get_gravatar_url from .gravatar import get_gravatar_url
from django.conf import settings
def get_user_by_username_or_email(username_or_email): def get_user_by_username_or_email(username_or_email):
@ -55,7 +55,6 @@ def get_user_by_username_or_email(username_or_email):
return user return user
def get_and_validate_user(*, username:str, password:str) -> bool: def get_and_validate_user(*, username:str, password:str) -> bool:
""" """
Check if user with username/email exists and specified Check if user with username/email exists and specified
@ -75,7 +74,7 @@ def get_and_validate_user(*, username:str, password:str) -> bool:
def get_photo_url(photo): def get_photo_url(photo):
"""Get a photo absolute url and the photo automatically cropped.""" """Get a photo absolute url and the photo automatically cropped."""
try: try:
url = get_thumbnailer(photo)['avatar'].url url = get_thumbnailer(photo)[settings.THN_AVATAR_SMALL].url
return get_absolute_url(url) return get_absolute_url(url)
except InvalidImageFormatError as e: except InvalidImageFormatError as e:
return None return None
@ -91,7 +90,7 @@ def get_photo_or_gravatar_url(user):
def get_big_photo_url(photo): def get_big_photo_url(photo):
"""Get a big photo absolute url and the photo automatically cropped.""" """Get a big photo absolute url and the photo automatically cropped."""
try: try:
url = get_thumbnailer(photo)['big-avatar'].url url = get_thumbnailer(photo)[settings.THN_AVATAR_BIG].url
return get_absolute_url(url) return get_absolute_url(url)
except InvalidImageFormatError as e: except InvalidImageFormatError as e:
return None return None
@ -105,13 +104,14 @@ def get_big_photo_or_gravatar_url(user):
if user.photo: if user.photo:
return get_big_photo_url(user.photo) return get_big_photo_url(user.photo)
else: else:
return get_gravatar_url(user.email, size=settings.DEFAULT_BIG_AVATAR_SIZE) return get_gravatar_url(user.email, size=settings.THN_AVATAR_BIG_SIZE)
def get_visible_project_ids(from_user, by_user): def get_visible_project_ids(from_user, by_user):
"""Calculate the project_ids from one user visible by another""" """Calculate the project_ids from one user visible by another"""
required_permissions = ["view_project"] required_permissions = ["view_project"]
#Or condition for membership filtering, the basic one is the access to projects allowing anonymous visualization # Or condition for membership filtering, the basic one is the access to projects
# allowing anonymous visualization
member_perm_conditions = Q(project__anon_permissions__contains=required_permissions) member_perm_conditions = Q(project__anon_permissions__contains=required_permissions)
# Authenticated # Authenticated
@ -138,7 +138,8 @@ def get_stats_for_user(from_user, by_user):
total_num_projects = len(project_ids) total_num_projects = len(project_ids)
roles = [_(r) for r in from_user.memberships.filter(project__id__in=project_ids).values_list("role__name", flat=True)] roles = [_(r) for r in from_user.memberships.filter(project__id__in=project_ids).values_list(
"role__name", flat=True)]
roles = list(set(roles)) roles = list(set(roles))
User = apps.get_model('users', 'User') User = apps.get_model('users', 'User')

View File

@ -18,13 +18,10 @@
import pytest import pytest
import base64 import base64
from django.apps import apps
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from .. import factories as f
from django.apps import apps
from taiga.base.utils import json from taiga.base.utils import json
from taiga.projects.models import Project, Membership from taiga.projects.models import Project, Membership
from taiga.projects.issues.models import Issue from taiga.projects.issues.models import Issue
@ -32,6 +29,9 @@ from taiga.projects.userstories.models import UserStory
from taiga.projects.tasks.models import Task from taiga.projects.tasks.models import Task
from taiga.projects.wiki.models import WikiPage from taiga.projects.wiki.models import WikiPage
from .. import factories as f
from ..utils import DUMMY_BMP_DATA
pytestmark = pytest.mark.django_db pytestmark = pytest.mark.django_db
@ -945,6 +945,36 @@ def test_invalid_dump_import(client):
assert response_data["_error_message"] == "Invalid dump format" assert response_data["_error_message"] == "Invalid dump format"
def test_valid_dump_import_with_logo(client, settings):
settings.CELERY_ENABLED = False
user = f.UserFactory.create()
client.login(user)
url = reverse("importer-load-dump")
data = ContentFile(bytes(json.dumps({
"slug": "valid-project",
"name": "Valid project",
"description": "Valid project desc",
"logo": {
"name": "logo.bmp",
"data": base64.b64encode(DUMMY_BMP_DATA).decode("utf-8")
}
}), "utf-8"))
data.name = "test"
response = client.post(url, {'dump': data})
assert response.status_code == 201
response_data = response.data
assert "id" in response_data
assert response_data["name"] == "Valid project"
assert "logo_small_url" in response_data
assert response_data["logo_small_url"] != None
assert "logo_big_url" in response_data
assert response_data["logo_big_url"] != None
def test_valid_dump_import_with_celery_disabled(client, settings): def test_valid_dump_import_with_celery_disabled(client, settings):
settings.CELERY_ENABLED = False settings.CELERY_ENABLED = False

View File

@ -1,4 +1,7 @@
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.conf import settings
from django.core.files import File
from taiga.base.utils import json from taiga.base.utils import json
from taiga.projects.services import stats as stats_services from taiga.projects.services import stats as stats_services
from taiga.projects.history.services import take_snapshot from taiga.projects.history.services import take_snapshot
@ -6,8 +9,14 @@ from taiga.permissions.permissions import ANON_PERMISSIONS
from taiga.projects.models import Project from taiga.projects.models import Project
from .. import factories as f from .. import factories as f
from ..utils import DUMMY_BMP_DATA
from tempfile import NamedTemporaryFile
from easy_thumbnails.files import generate_all_aliases, get_thumbnailer
import os.path
import pytest import pytest
pytestmark = pytest.mark.django_db pytestmark = pytest.mark.django_db
@ -406,3 +415,80 @@ def test_projects_user_order(client):
response_content = response.data response_content = response.data
assert response.status_code == 200 assert response.status_code == 200
assert(response_content[0]["id"] == project_2.id) assert(response_content[0]["id"] == project_2.id)
@pytest.mark.django_db(transaction=True)
def test_update_project_logo(client):
user = f.UserFactory.create(is_superuser=True)
project = f.create_project()
url = reverse("projects-change-logo", args=(project.id,))
with NamedTemporaryFile(delete=False) as logo:
logo.write(DUMMY_BMP_DATA)
logo.seek(0)
project.logo = File(logo)
project.save()
generate_all_aliases(project.logo, include_global=True)
thumbnailer = get_thumbnailer(project.logo)
original_photo_paths = [project.logo.path]
original_photo_paths += [th.path for th in thumbnailer.get_thumbnails()]
assert all(list(map(os.path.exists, original_photo_paths)))
with NamedTemporaryFile(delete=False) as logo:
logo.write(DUMMY_BMP_DATA)
logo.seek(0)
client.login(user)
post_data = {'logo': logo}
response = client.post(url, post_data)
assert response.status_code == 200
assert not any(list(map(os.path.exists, original_photo_paths)))
@pytest.mark.django_db(transaction=True)
def test_remove_project_logo(client):
user = f.UserFactory.create(is_superuser=True)
project = f.create_project()
url = reverse("projects-remove-logo", args=(project.id,))
with NamedTemporaryFile(delete=False) as logo:
logo.write(DUMMY_BMP_DATA)
logo.seek(0)
project.logo = File(logo)
project.save()
generate_all_aliases(project.logo, include_global=True)
thumbnailer = get_thumbnailer(project.logo)
original_photo_paths = [project.logo.path]
original_photo_paths += [th.path for th in thumbnailer.get_thumbnails()]
assert all(list(map(os.path.exists, original_photo_paths)))
client.login(user)
response = client.post(url)
assert response.status_code == 200
assert not any(list(map(os.path.exists, original_photo_paths)))
@pytest.mark.django_db(transaction=True)
def test_remove_project_with_logo(client):
user = f.UserFactory.create(is_superuser=True)
project = f.create_project()
url = reverse("projects-detail", args=(project.id,))
with NamedTemporaryFile(delete=False) as logo:
logo.write(DUMMY_BMP_DATA)
logo.seek(0)
project.logo = File(logo)
project.save()
generate_all_aliases(project.logo, include_global=True)
thumbnailer = get_thumbnailer(project.logo)
original_photo_paths = [project.logo.path]
original_photo_paths += [th.path for th in thumbnailer.get_thumbnails()]
assert all(list(map(os.path.exists, original_photo_paths)))
client.login(user)
response = client.delete(url)
assert response.status_code == 204
assert not any(list(map(os.path.exists, original_photo_paths)))

View File

@ -6,6 +6,7 @@ from django.core.urlresolvers import reverse
from django.core.files import File from django.core.files import File
from .. import factories as f from .. import factories as f
from ..utils import DUMMY_BMP_DATA
from taiga.base.utils import json from taiga.base.utils import json
from taiga.users import models from taiga.users import models
@ -172,35 +173,7 @@ def test_cancel_self_user_with_invalid_token(client):
assert response.status_code == 400 assert response.status_code == 400
DUMMY_BMP_DATA = b'BM:\x00\x00\x00\x00\x00\x00\x006\x00\x00\x00(\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x18\x00\x00\x00\x00\x00\x04\x00\x00\x00\x13\x0b\x00\x00\x13\x0b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' @pytest.mark.django_db(transaction=True)
def test_change_avatar(client):
url = reverse('users-change-avatar')
user = f.UserFactory()
client.login(user)
with NamedTemporaryFile() as avatar:
# Test no avatar send
post_data = {}
response = client.post(url, post_data)
assert response.status_code == 400
# Test invalid file send
post_data = {
'avatar': avatar
}
response = client.post(url, post_data)
assert response.status_code == 400
# Test empty valid avatar send
avatar.write(DUMMY_BMP_DATA)
avatar.seek(0)
response = client.post(url, post_data)
assert response.status_code == 200
def test_change_avatar_removes_the_old_one(client): def test_change_avatar_removes_the_old_one(client):
url = reverse('users-change-avatar') url = reverse('users-change-avatar')
user = f.UserFactory() user = f.UserFactory()
@ -216,7 +189,7 @@ def test_change_avatar_removes_the_old_one(client):
thumbnailer = get_thumbnailer(user.photo) thumbnailer = get_thumbnailer(user.photo)
original_photo_paths = [user.photo.path] original_photo_paths = [user.photo.path]
original_photo_paths += [th.path for th in thumbnailer.get_thumbnails()] original_photo_paths += [th.path for th in thumbnailer.get_thumbnails()]
assert list(map(os.path.exists, original_photo_paths)) == [True, True, True, True, True] assert all(list(map(os.path.exists, original_photo_paths)))
client.login(user) client.login(user)
avatar.write(DUMMY_BMP_DATA) avatar.write(DUMMY_BMP_DATA)
@ -225,9 +198,10 @@ def test_change_avatar_removes_the_old_one(client):
response = client.post(url, post_data) response = client.post(url, post_data)
assert response.status_code == 200 assert response.status_code == 200
assert list(map(os.path.exists, original_photo_paths)) == [False, False, False, False, False] assert not any(list(map(os.path.exists, original_photo_paths)))
@pytest.mark.django_db(transaction=True)
def test_remove_avatar(client): def test_remove_avatar(client):
url = reverse('users-remove-avatar') url = reverse('users-remove-avatar')
user = f.UserFactory() user = f.UserFactory()
@ -242,13 +216,13 @@ def test_remove_avatar(client):
thumbnailer = get_thumbnailer(user.photo) thumbnailer = get_thumbnailer(user.photo)
original_photo_paths = [user.photo.path] original_photo_paths = [user.photo.path]
original_photo_paths += [th.path for th in thumbnailer.get_thumbnails()] original_photo_paths += [th.path for th in thumbnailer.get_thumbnails()]
assert list(map(os.path.exists, original_photo_paths)) == [True, True, True, True, True] assert all(list(map(os.path.exists, original_photo_paths)))
client.login(user) client.login(user)
response = client.post(url) response = client.post(url)
assert response.status_code == 200 assert response.status_code == 200
assert list(map(os.path.exists, original_photo_paths)) == [False, False, False, False, False] assert not any(list(map(os.path.exists, original_photo_paths)))
def test_list_contacts_private_projects(client): def test_list_contacts_private_projects(client):

View File

@ -18,6 +18,8 @@
from django.db.models import signals from django.db.models import signals
DUMMY_BMP_DATA = b'BM:\x00\x00\x00\x00\x00\x00\x006\x00\x00\x00(\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x18\x00\x00\x00\x00\x00\x04\x00\x00\x00\x13\x0b\x00\x00\x13\x0b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
def signals_switch(): def signals_switch():
pre_save = signals.pre_save.receivers pre_save = signals.pre_save.receivers