Merge branch 'features/mercs_5_6' into 3_way_merge

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
This commit is contained in:
Roberto Rosario
2019-03-21 19:48:13 -04:00
516 changed files with 19888 additions and 14690 deletions

View File

@@ -2,42 +2,42 @@ from __future__ import unicode_literals
from django.contrib.contenttypes.models import ContentType
from rest_framework import generics
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from .classes import Template
from .serializers import ContentTypeSerializer, TemplateSerializer
class APIContentTypeList(generics.ListAPIView):
class ContentTypeAPIViewSet(viewsets.ReadOnlyModelViewSet):
"""
Returns a list of all the available content types.
list:
Return a list of all the available content types.
retrieve:
Return the given content type details.
"""
serializer_class = ContentTypeSerializer
lookup_url_kwarg = 'content_type_id'
queryset = ContentType.objects.order_by('app_label', 'model')
serializer_class = ContentTypeSerializer
class APITemplateListView(generics.ListAPIView):
class TemplateAPIViewSet(viewsets.ReadOnlyModelViewSet):
"""
Returns a list of partial templates.
get: Returns a list of partial templates.
list:
Return a list of partial templates.
retrieve:
Return the given partial template details.
"""
serializer_class = TemplateSerializer
lookup_url_kwarg = 'template_name'
permission_classes = (IsAuthenticated,)
serializer_class = TemplateSerializer
def get_object(self):
return Template.get(name=self.kwargs['template_name']).render(
request=self.request
)
def get_queryset(self):
return Template.all(rendered=True, request=self.request)
class APITemplateView(generics.RetrieveAPIView):
"""
Returns the selected partial template details.
get: Retrieve the details of the partial template.
"""
serializer_class = TemplateSerializer
permission_classes = (IsAuthenticated,)
def get_object(self):
return Template.get(name=self.kwargs['name']).render(
request=self.request
)

View File

@@ -4,6 +4,8 @@ import logging
import os
import warnings
from datetime import timedelta
import sys
import traceback
from kombu import Exchange, Queue
@@ -41,6 +43,7 @@ from .settings import (
from .signals import pre_initial_setup, pre_upgrade
from .tasks import task_delete_stale_uploads # NOQA - Force task registration
from .utils import check_for_sqlite
from .warnings import DatabaseWarning
logger = logging.getLogger(__name__)
@@ -74,6 +77,8 @@ class MayanAppConfig(apps.AppConfig):
'Import time error when running AppConfig.ready() of app '
'"%s".', self.name
)
exc_info = sys.exc_info()
traceback.print_exception(*exc_info)
raise exception
@@ -88,7 +93,9 @@ class CommonApp(MayanAppConfig):
def ready(self):
super(CommonApp, self).ready()
if check_for_sqlite():
warnings.warn(force_text(MESSAGE_SQLITE_WARNING))
warnings.warn(
category=DatabaseWarning, message=force_text(MESSAGE_SQLITE_WARNING)
)
Template(
name='menu_main', template_name='appearance/menu_main.html'

View File

@@ -72,16 +72,6 @@ class ErrorLogNamespace(object):
return ErrorLogEntry.objects.filter(namespace=self.name)
class FakeStorageSubclass(object):
"""
Placeholder class to allow serializing the real storage subclass to
support migrations.
"""
def __eq__(self, other):
return True
class MissingItem(object):
_registry = []
@@ -302,7 +292,7 @@ class Template(object):
def get_absolute_url(self):
return reverse(
viewname='rest_api:template-detail', kwargs={'template_pk': self.name}
viewname='rest_api:template-detail', kwargs={'template_name': self.name}
)
def render(self, request):

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,12 @@ from __future__ import absolute_import, unicode_literals
from mayan.apps.appearance.classes import Icon
icon_about = Icon(driver_name='fontawesome', symbol='info')
icon_add_all = Icon(
driver_name='fontawesome-layers', data=[
{'class': 'far fa-circle'},
{'class': 'fas fa-plus', 'transform': 'shrink-6'}
]
)
icon_assign_remove_add = Icon(driver_name='fontawesome', symbol='plus')
icon_assign_remove_remove = Icon(driver_name='fontawesome', symbol='minus')
icon_check_version = Icon(driver_name='fontawesome', symbol='sync')
@@ -43,6 +49,12 @@ icon_ok = Icon(
icon_packages_licenses = Icon(
driver_name='fontawesome', symbol='certificate'
)
icon_remove_all = Icon(
driver_name='fontawesome-layers', data=[
{'class': 'far fa-circle'},
{'class': 'fas fa-minus', 'transform': 'shrink-6'}
]
)
icon_setup = Icon(
driver_name='fontawesome', symbol='cog'
)

View File

@@ -57,12 +57,12 @@ link_documentation = Link(
link_object_error_list = Link(
icon_class=icon_object_error_list,
kwargs=get_kwargs_factory('resolved_object'),
permissions=(permission_error_log_view,), text=_('Errors'),
permission=permission_error_log_view, text=_('Errors'),
view='common:object_error_list',
)
link_object_error_list_clear = Link(
kwargs=get_kwargs_factory('resolved_object'),
permissions=(permission_error_log_view,), text=_('Clear all'),
permission=permission_error_log_view, text=_('Clear all'),
view='common:object_error_list_clear',
)
link_forum = Link(

View File

@@ -11,7 +11,7 @@ MESSAGE_SQLITE_WARNING = _(
'for development and testing, not for production.'
)
PYPI_URL = 'https://pypi.python.org/pypi'
PK_LIST_SEPARATOR = ','
TEXT_LIST_AS_ITEMS_PARAMETER = '_list_mode'
TEXT_LIST_AS_ITEMS_VARIABLE_NAME = 'list_as_items'
TEXT_CHOICE_ITEMS = 'items'

View File

@@ -11,8 +11,8 @@ from django.core.management.base import CommandError
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.utils import fs_cleanup
from mayan.apps.documents.models import DocumentType
from mayan.apps.storage.utils import fs_cleanup
CONVERTDB_FOLDER = 'convertdb'
CONVERTDB_OUTPUT_FILENAME = 'migrate.json'

View File

@@ -8,21 +8,20 @@ from .icons import icon_menu_about, icon_menu_user
__all__ = (
'menu_about', 'menu_facet', 'menu_list_facet', 'menu_main', 'menu_object',
'menu_multi_item', 'menu_secondary', 'menu_setup', 'menu_sidebar',
'menu_multi_item', 'menu_secondary', 'menu_setup', 'menu_secondary',
'menu_tools', 'menu_topbar', 'menu_user'
)
menu_about = Menu(
icon_class=icon_menu_about, label=_('System'), name='about'
)
menu_facet = Menu(name='facet')
menu_list_facet = Menu(name='list facet')
menu_facet = Menu(label=_('Facet'), name='facet')
menu_list_facet = Menu(label=_('Facet'), name='list facet')
menu_main = Menu(name='main')
menu_multi_item = Menu(name='multi item')
menu_object = Menu(name='object')
menu_secondary = Menu(name='secondary')
menu_object = Menu(label=_('Actions'), name='object')
menu_secondary = Menu(label=_('Secondary'), name='secondary')
menu_setup = Menu(name='setup')
menu_sidebar = Menu(name='sidebar')
menu_tools = Menu(name='tools')
menu_topbar = Menu(name='topbar')
menu_user = Menu(

View File

@@ -1,14 +1,13 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2018-12-29 07:38
from __future__ import unicode_literals
from django.db import migrations, models
import mayan.apps.common.classes
import mayan.apps.common.models
import mayan.apps.storage.classes
class Migration(migrations.Migration):
dependencies = [
('common', '0010_auto_20180403_0702_squashed_0011_auto_20180429_0758'),
]
@@ -17,6 +16,10 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='shareduploadedfile',
name='file',
field=models.FileField(storage=mayan.apps.common.classes.FakeStorageSubclass(), upload_to=mayan.apps.common.models.upload_to, verbose_name='File'),
field=models.FileField(
storage=mayan.apps.storage.classes.FakeStorageSubclass(),
upload_to=mayan.apps.common.models.upload_to,
verbose_name='File'
),
),
]

File diff suppressed because it is too large Load Diff

View File

@@ -18,6 +18,7 @@ from .storages import storage_sharedupload
logger = logging.getLogger(__name__)
# TODO: move outside of models.py or as a static method of SharedUploadedFile
def upload_to(instance, filename):
return 'shared-file-{}'.format(uuid.uuid4().hex)

View File

@@ -48,8 +48,8 @@ class PurePaginator(Paginator):
self.allow_empty_first_page = allow_empty_first_page
self.object_list = object_list
self.orphans = orphans
self.per_page = per_page
self.page_kwarg = page_kwarg
self.per_page = per_page
self.request = request
def page(self, number):

View File

@@ -7,5 +7,5 @@ from mayan.apps.permissions import PermissionNamespace
namespace = PermissionNamespace(label=_('Common'), name='common')
permission_error_log_view = namespace.add_permission(
name='error_log_view', label=_('View error log')
label=_('View error log'), name='error_log_view'
)

View File

@@ -5,13 +5,13 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.task_manager.classes import CeleryQueue
queue_default = CeleryQueue(
name='default', label=_('Default'), is_default_queue=True
is_default_queue=True, label=_('Default'), name='default'
)
queue_tools = CeleryQueue(name='tools', label=_('Tools'))
queue_tools = CeleryQueue(label=_('Tools'), name='tools')
queue_common_periodic = CeleryQueue(
name='common_periodic', label=_('Common periodic'), transient=True
label=_('Common periodic'), name='common_periodic', transient=True
)
queue_common_periodic.add_task_type(
name='mayan.apps.common.tasks.task_delete_stale_uploads',
label=_('Delete stale uploads')
label=_('Delete stale uploads'),
name='mayan.apps.common.tasks.task_delete_stale_uploads'
)

View File

@@ -5,9 +5,15 @@ from django.contrib.contenttypes.models import ContentType
from rest_framework import serializers
class ContentTypeSerializer(serializers.ModelSerializer):
class ContentTypeSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
fields = ('app_label', 'id', 'model')
extra_kwargs = {
'url': {
'lookup_url_kwarg': 'content_type_id',
'view_name': 'rest_api:content_type-detail'
}
}
fields = ('app_label', 'id', 'model', 'url')
model = ContentType
@@ -15,3 +21,7 @@ class TemplateSerializer(serializers.Serializer):
hex_hash = serializers.CharField(read_only=True)
name = serializers.CharField(read_only=True)
html = serializers.CharField(read_only=True)
url = serializers.HyperlinkedIdentityField(
lookup_field='name', lookup_url_kwarg='template_name',
view_name='rest_api:template-detail'
)

View File

@@ -1,7 +1,6 @@
from __future__ import unicode_literals
import os
import tempfile
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
@@ -11,7 +10,7 @@ from mayan.apps.smart_settings import Namespace
from .literals import DEFAULT_COMMON_HOME_VIEW
namespace = Namespace(name='common', label=_('Common'))
namespace = Namespace(label=_('Common'), name='common')
setting_auto_logging = namespace.add_setting(
global_name='COMMON_AUTO_LOGGING',
@@ -53,8 +52,7 @@ setting_production_error_log_path = namespace.add_setting(
global_name='COMMON_PRODUCTION_ERROR_LOG_PATH',
default=os.path.join(settings.MEDIA_ROOT, 'error.log'), help_text=_(
'Path to the logfile that will track errors during production.'
),
is_path=True
)
)
setting_project_title = namespace.add_setting(
global_name='COMMON_PROJECT_TITLE',
@@ -77,16 +75,8 @@ setting_shared_storage_arguments = namespace.add_setting(
global_name='COMMON_SHARED_STORAGE_ARGUMENTS',
default={'location': os.path.join(settings.MEDIA_ROOT, 'shared_files')}
)
setting_temporary_directory = namespace.add_setting(
global_name='COMMON_TEMPORARY_DIRECTORY', default=tempfile.gettempdir(),
help_text=_(
'Temporary directory used site wide to store thumbnails, previews '
'and temporary files.'
),
is_path=True
)
namespace = Namespace(name='django', label=_('Django'))
namespace = Namespace(label=_('Django'), name='django')
setting_django_allowed_hosts = namespace.add_setting(
global_name='ALLOWED_HOSTS', default=settings.ALLOWED_HOSTS,
@@ -357,7 +347,20 @@ setting_django_login_redirect_url = namespace.add_setting(
'for example. This setting also accepts named URL patterns which '
'can be used to reduce configuration duplication since you don\'t '
'have to define the URL in two places (settings and URLconf).'
),
)
)
setting_django_logout_redirect_url = namespace.add_setting(
global_name='LOGOUT_REDIRECT_URL',
default=settings.LOGOUT_REDIRECT_URL,
help_text=_(
'Default: None. The URL where requests are redirected after a user '
'logs out using LogoutView (if the view doesn\'t get a next_page '
'argument). If None, no redirect will be performed and the logout '
'view will be rendered. This setting also accepts named URL '
'patterns which can be used to reduce configuration duplication '
'since you don\'t have to define the URL in two places (settings '
'and URLconf).'
)
)
setting_django_static_url = namespace.add_setting(
global_name='STATIC_URL',
@@ -402,7 +405,7 @@ setting_django_wsgi_application = namespace.add_setting(
),
)
namespace = Namespace(name='celery', label=_('Celery'))
namespace = Namespace(label=_('Celery'), name='celery')
setting_celery_always_eager = namespace.add_setting(
global_name='CELERY_TASK_ALWAYS_EAGER',

View File

@@ -1,7 +1,8 @@
from __future__ import unicode_literals
from mayan.apps.storage.utils import get_storage_subclass
from .settings import setting_shared_storage, setting_shared_storage_arguments
from .utils import get_storage_subclass
storage_sharedupload = get_storage_subclass(
dotted_path=setting_shared_storage.value

View File

@@ -1,5 +1,7 @@
from __future__ import unicode_literals
import logging
from django.template import Context, Library, VariableDoesNotExist, Variable
from django.template.defaultfilters import truncatechars
from django.template.loader import get_template
@@ -14,6 +16,7 @@ from ..icons import icon_list_mode_items, icon_list_mode_list
from ..literals import MESSAGE_SQLITE_WARNING
from ..utils import check_for_sqlite, resolve_attribute
logger = logging.getLogger(__name__)
register = Library()
@@ -48,6 +51,14 @@ def common_calculate_title(context):
return _('Create')
@register.simple_tag
def common_get_object_verbose_name(obj):
try:
return obj._meta.verbose_name
except AttributeError:
return type(obj)
@register.simple_tag
def get_collections():
return Collection.get_all()

View File

@@ -1,30 +1,20 @@
from __future__ import absolute_import, unicode_literals
from django.conf.urls import url
from django.contrib.auth import get_user_model
from django.http import HttpResponse
from django.template import Context, Template
from django.test import TestCase
from django.test.utils import ContextList
from django.urls import clear_url_caches, reverse
from django_downloadview import assert_download_response
from mayan.apps.acls.tests.mixins import ACLBaseTestMixin
from mayan.apps.acls.tests.mixins import ACLTestCaseMixin
from mayan.apps.permissions.classes import Permission
from mayan.apps.smart_settings.classes import Namespace
from mayan.apps.user_management.tests import (
TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME, TEST_USER_PASSWORD,
TEST_USER_USERNAME
)
from .literals import TEST_VIEW_NAME, TEST_VIEW_URL
from .mixins import (
ContentTypeCheckMixin, DatabaseConversionMixin, OpenFileCheckMixin,
TempfileCheckMixin
ClientMethodsTestCaseMixin, ContentTypeCheckMixin, DatabaseConversionMixin,
OpenFileCheckTestCaseMixin, RandomPrimaryKeyModelMonkeyPatchMixin,
TempfileCheckTestCaseMixin, TestViewTestCaseMixin
)
class BaseTestCase(DatabaseConversionMixin, ACLBaseTestMixin, ContentTypeCheckMixin, OpenFileCheckMixin, TempfileCheckMixin, TestCase):
class BaseTestCase(RandomPrimaryKeyModelMonkeyPatchMixin, DatabaseConversionMixin, ACLTestCaseMixin, OpenFileCheckTestCaseMixin, TempfileCheckTestCaseMixin, TestCase):
"""
This is the most basic test case class any test in the project should use.
"""
@@ -36,81 +26,9 @@ class BaseTestCase(DatabaseConversionMixin, ACLBaseTestMixin, ContentTypeCheckMi
Permission.invalidate_cache()
class GenericViewTestCase(BaseTestCase):
def setUp(self):
super(GenericViewTestCase, self).setUp()
self.has_test_view = False
def tearDown(self):
from mayan.urls import urlpatterns
self.client.logout()
if self.has_test_view:
urlpatterns.pop(0)
super(GenericViewTestCase, self).tearDown()
def add_test_view(self, test_object):
from mayan.urls import urlpatterns
def test_view(request):
template = Template('{{ object }}')
context = Context(
{'object': test_object, 'resolved_object': test_object}
)
return HttpResponse(template.render(context=context))
urlpatterns.insert(0, url(TEST_VIEW_URL, test_view, name=TEST_VIEW_NAME))
clear_url_caches()
self.has_test_view = True
def get_test_view(self):
response = self.get(TEST_VIEW_NAME)
if isinstance(response.context, ContextList):
# template widget rendering causes test client response to be
# ContextList rather than RequestContext. Typecast to dictionary
# before updating.
result = dict(response.context).copy()
result.update({'request': response.wsgi_request})
return Context(result)
else:
response.context.update({'request': response.wsgi_request})
return Context(response.context)
def get(self, viewname=None, path=None, *args, **kwargs):
data = kwargs.pop('data', {})
follow = kwargs.pop('follow', False)
if viewname:
path = reverse(viewname=viewname, *args, **kwargs)
return self.client.get(
path=path, data=data, follow=follow
)
def login(self, username, password):
logged_in = self.client.login(username=username, password=password)
user = get_user_model().objects.get(username=username)
self.assertTrue(logged_in)
self.assertTrue(user.is_authenticated)
def login_user(self):
self.login(username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD)
def login_admin_user(self):
self.login(username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD)
def logout(self):
self.client.logout()
def post(self, viewname=None, path=None, *args, **kwargs):
data = kwargs.pop('data', {})
follow = kwargs.pop('follow', False)
if viewname:
path = reverse(viewname=viewname, *args, **kwargs)
return self.client.post(
path=path, data=data, follow=follow
)
class GenericViewTestCase(ClientMethodsTestCaseMixin, ContentTypeCheckMixin, TestViewTestCaseMixin, BaseTestCase):
"""
A generic view test case built on top of the base test case providing
a single, user customizable view to test object resolution and shorthand
HTTP method functions.
"""

View File

@@ -2,20 +2,26 @@ from __future__ import unicode_literals
import glob
import os
import random
from furl import furl
from django.apps import apps
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.conf.urls import url
from django.contrib.contenttypes.models import ContentType
from django.core import management
from django.db import connection
from django.db import models
from django.http import HttpResponse
from django.template import Context, Template
from django.test.utils import ContextList
from django.urls import clear_url_caches, reverse
from django.utils.encoding import force_bytes
from mayan.apps.user_management.tests import (
TEST_ADMIN_EMAIL, TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME,
TEST_GROUP_NAME, TEST_USER_EMAIL, TEST_USER_PASSWORD,
TEST_USER_USERNAME
)
from ..settings import setting_temporary_directory
from mayan.apps.storage.settings import setting_temporary_directory
from .literals import TEST_VIEW_NAME, TEST_VIEW_URL
from .utils import mute_stdout
@@ -23,6 +29,56 @@ if getattr(settings, 'COMMON_TEST_FILE_HANDLES', False):
import psutil
class ClientMethodsTestCaseMixin(object):
def _build_verb_kwargs(self, viewname=None, path=None, *args, **kwargs):
data = kwargs.pop('data', {})
follow = kwargs.pop('follow', False)
query = kwargs.pop('query', {})
if viewname:
path = reverse(viewname=viewname, *args, **kwargs)
path = furl(url=path)
path.args.update(query)
return {'follow': follow, 'data': data, 'path': path.tostr()}
def delete(self, viewname=None, path=None, *args, **kwargs):
return self.client.delete(
**self._build_verb_kwargs(
path=path, viewname=viewname, *args, **kwargs
)
)
def get(self, viewname=None, path=None, *args, **kwargs):
return self.client.get(
**self._build_verb_kwargs(
path=path, viewname=viewname, *args, **kwargs
)
)
def patch(self, viewname=None, path=None, *args, **kwargs):
return self.client.patch(
**self._build_verb_kwargs(
path=path, viewname=viewname, *args, **kwargs
)
)
def post(self, viewname=None, path=None, *args, **kwargs):
return self.client.post(
**self._build_verb_kwargs(
path=path, viewname=viewname, *args, **kwargs
)
)
def put(self, viewname=None, path=None, *args, **kwargs):
return self.client.put(
**self._build_verb_kwargs(
path=path, viewname=viewname, *args, **kwargs
)
)
class ContentTypeCheckMixin(object):
expected_content_type = 'text/html; charset=utf-8'
@@ -34,13 +90,14 @@ class ContentTypeCheckMixin(object):
def request(self, *args, **kwargs):
response = super(CustomClient, self).request(*args, **kwargs)
content_type = response._headers['content-type'][1]
test_instance.assertEqual(
content_type, test_instance.expected_content_type,
msg='Unexpected response content type: {}, expected: {}.'.format(
content_type, test_instance.expected_content_type
content_type = response._headers.get('content-type', [None, ''])[1]
if test_instance.expected_content_type:
test_instance.assertEqual(
content_type, test_instance.expected_content_type,
msg='Unexpected response content type: {}, expected: {}.'.format(
content_type, test_instance.expected_content_type
)
)
)
return response
@@ -55,7 +112,7 @@ class DatabaseConversionMixin(object):
)
class OpenFileCheckMixin(object):
class OpenFileCheckTestCaseMixin(object):
def _get_descriptor_count(self):
process = psutil.Process()
return process.num_fds()
@@ -65,7 +122,7 @@ class OpenFileCheckMixin(object):
return process.open_files()
def setUp(self):
super(OpenFileCheckMixin, self).setUp()
super(OpenFileCheckTestCaseMixin, self).setUp()
if getattr(settings, 'COMMON_TEST_FILE_HANDLES', False):
self._open_files = self._get_open_files()
@@ -80,10 +137,61 @@ class OpenFileCheckMixin(object):
self._skip_file_descriptor_test = False
super(OpenFileCheckMixin, self).tearDown()
super(OpenFileCheckTestCaseMixin, self).tearDown()
class TempfileCheckMixin(object):
class RandomPrimaryKeyModelMonkeyPatchMixin(object):
random_primary_key_random_floor = 100
random_primary_key_random_ceiling = 10000
random_primary_key_maximum_attempts = 100
@staticmethod
def get_unique_primary_key(model):
pk_list = model._meta.default_manager.values_list('pk', flat=True)
attempts = 0
while True:
primary_key = random.randint(
RandomPrimaryKeyModelMonkeyPatchMixin.random_primary_key_random_floor,
RandomPrimaryKeyModelMonkeyPatchMixin.random_primary_key_random_ceiling
)
if primary_key not in pk_list:
break
attempts = attempts + 1
if attempts > RandomPrimaryKeyModelMonkeyPatchMixin.random_primary_key_maximum_attempts:
raise Exception(
'Maximum number of retries for an unique random primary '
'key reached.'
)
return primary_key
def setUp(self):
self.method_save_original = models.Model.save
def method_save_new(instance, *args, **kwargs):
if instance.pk:
return self.method_save_original(instance, *args, **kwargs)
else:
instance.pk = RandomPrimaryKeyModelMonkeyPatchMixin.get_unique_primary_key(
model=instance._meta.model
)
instance.id = instance.pk
return instance.save_base(force_insert=True)
setattr(models.Model, 'save', method_save_new)
super(RandomPrimaryKeyModelMonkeyPatchMixin, self).setUp()
def tearDown(self):
models.Model.save = self.method_save_original
super(RandomPrimaryKeyModelMonkeyPatchMixin, self).tearDown()
class TempfileCheckTestCaseMixin(object):
# Ignore the jvmstat instrumentation and GitLab's CI .config files
# Ignore LibreOffice fontconfig cache dir
ignore_globs = ('hsperfdata_*', '.config', '.cache')
@@ -108,7 +216,7 @@ class TempfileCheckMixin(object):
) - set(ignored_result)
def setUp(self):
super(TempfileCheckMixin, self).setUp()
super(TempfileCheckTestCaseMixin, self).setUp()
if getattr(settings, 'COMMON_TEST_TEMP_FILES', False):
self._temporary_items = self._get_temporary_entries()
@@ -123,4 +231,101 @@ class TempfileCheckMixin(object):
','.join(final_temporary_items - self._temporary_items)
)
)
super(TempfileCheckMixin, self).tearDown()
super(TempfileCheckTestCaseMixin, self).tearDown()
class TestModelTestMixin(object):
def _create_test_model(self, fields=None, model_name='TestModel', options=None):
# Obtain the app_config and app_label from the test's module path
app_config = apps.get_containing_app_config(
object_name=self.__class__.__module__
)
app_label = app_config.label
class Meta:
pass
setattr(Meta, 'app_label', app_label)
if options is not None:
for key, value in options.items():
setattr(Meta, key, value)
def save(instance, *args, **kwargs):
# Custom .save() method to use random primary key values.
if instance.pk:
return models.Model.self(instance, *args, **kwargs)
else:
instance.pk = RandomPrimaryKeyModelMonkeyPatchMixin.get_unique_primary_key(
model=instance._meta.model
)
instance.id = instance.pk
return instance.save_base(force_insert=True)
attrs = {
'__module__': self.__class__.__module__, 'save': save, 'Meta': Meta
}
if fields:
attrs.update(fields)
# Clear previous model registration before re-registering it again to
# avoid conflict with test models with the same name, in the same app
# but from another test module.
apps.all_models[app_label].pop(model_name.lower(), None)
TestModel = type(
force_bytes(model_name), (models.Model,), attrs
)
setattr(self, model_name, TestModel)
with connection.schema_editor() as schema_editor:
schema_editor.create_model(model=TestModel)
ContentType.objects.clear_cache()
def _create_test_object(self, model_name='TestModel', **kwargs):
TestModel = getattr(self, model_name)
self.test_object = TestModel.objects.create(**kwargs)
class TestViewTestCaseMixin(object):
has_test_view = False
def tearDown(self):
from mayan.urls import urlpatterns
self.client.logout()
if self.has_test_view:
urlpatterns.pop(0)
super(TestViewTestCaseMixin, self).tearDown()
def add_test_view(self, test_object):
from mayan.urls import urlpatterns
def test_view(request):
template = Template('{{ object }}')
context = Context(
{'object': test_object, 'resolved_object': test_object}
)
return HttpResponse(template.render(context=context))
urlpatterns.insert(0, url(TEST_VIEW_URL, test_view, name=TEST_VIEW_NAME))
clear_url_caches()
self.has_test_view = True
def get_test_view(self):
response = self.get(TEST_VIEW_NAME)
if isinstance(response.context, ContextList):
# template widget rendering causes test client response to be
# ContextList rather than RequestContext. Typecast to dictionary
# before updating.
result = dict(response.context).copy()
result.update({'request': response.wsgi_request})
return Context(result)
else:
response.context.update({'request': response.wsgi_request})
return Context(response.context)

View File

@@ -1,7 +1,6 @@
from __future__ import unicode_literals
from django.test import override_settings
from django.urls import reverse
from mayan.apps.rest_api.tests import BaseAPITestCase
@@ -11,23 +10,40 @@ TEST_TEMPLATE_RESULT = '<div'
class CommonAPITestCase(BaseAPITestCase):
auto_login_user = False
def test_content_type_list_view(self):
response = self.client.get(reverse('rest_api:content-type-list'))
response = self.get(viewname='rest_api:content_type-list')
self.assertEqual(response.status_code, 200)
@override_settings(LANGUAGE_CODE='de')
def _request_template_detail_view(self):
return self.get(path=self.test_template.get_absolute_url())
def test_template_detail_view(self):
self.login_user()
template_main_menu = Template.get(name='menu_main')
response = self.client.get(template_main_menu.get_absolute_url())
self.test_template = Template.get(name='menu_main')
response = self._request_template_detail_view()
self.assertContains(
response=response, text=TEST_TEMPLATE_RESULT, status_code=200
)
@override_settings(LANGUAGE_CODE='de')
def test_template_detail_german_view(self):
self.login_user()
self.test_template = Template.get(name='menu_main')
response = self._request_template_detail_view()
self.assertContains(
response=response, text=TEST_TEMPLATE_RESULT, status_code=200
)
def test_template_detail_anonymous_view(self):
template_main_menu = Template.get(name='menu_main')
response = self.client.get(template_main_menu.get_absolute_url())
self.test_template = Template.get(name='menu_main')
response = self._request_template_detail_view()
self.assertNotContains(
response=response, text=TEST_TEMPLATE_RESULT, status_code=403
)

View File

@@ -6,5 +6,5 @@ from mayan.apps.user_management.tests.mixins import UserTestMixin
class UserLocaleProfileTestCase(UserTestMixin, BaseTestCase):
def test_natural_keys(self):
self._create_user()
self._create_test_user()
self._test_database_conversion('auth', 'common')

View File

@@ -14,8 +14,6 @@ from .literals import TEST_ERROR_LOG_ENTRY_RESULT
class CommonViewTestCase(GenericViewTestCase):
def test_about_view(self):
self.login_user()
response = self.get('common:about_view')
self.assertContains(response, text='About', status_code=200)
@@ -25,27 +23,36 @@ class CommonViewTestCase(GenericViewTestCase):
)
ErrorLogEntry.objects.register(model=get_user_model())
self.error_log_entry = self.user.error_logs.create(
self.error_log_entry = self._test_case_user.error_logs.create(
result=TEST_ERROR_LOG_ENTRY_RESULT
)
def _request_object_error_log_list(self):
content_type = ContentType.objects.get_for_model(model=self.user)
content_type = ContentType.objects.get_for_model(model=self._test_case_user)
return self.get(
'common:object_error_list', kwargs={
'app_label': content_type.app_label,
'model': content_type.model,
'object_id': self.user.pk
'object_id': self._test_case_user.pk
}, follow=True
)
def test_object_error_list_view_with_permissions(self):
def test_object_error_list_view_no_permissions(self):
self._create_error_log_entry()
response = self._request_object_error_log_list()
self.assertNotContains(
response=response, text=TEST_ERROR_LOG_ENTRY_RESULT,
status_code=403
)
def test_object_error_list_view_with_access(self):
self._create_error_log_entry()
self.login_user()
self.grant_access(
obj=self.user, permission=permission_error_log_view
obj=self._test_case_user, permission=permission_error_log_view
)
response = self._request_object_error_log_list()
@@ -54,15 +61,3 @@ class CommonViewTestCase(GenericViewTestCase):
response=response, text=TEST_ERROR_LOG_ENTRY_RESULT,
status_code=200
)
def test_object_error_list_view_no_permissions(self):
self._create_error_log_entry()
self.login_user()
response = self._request_object_error_log_list()
self.assertNotContains(
response=response, text=TEST_ERROR_LOG_ENTRY_RESULT,
status_code=403
)

View File

@@ -3,9 +3,7 @@ from __future__ import unicode_literals
from django.conf.urls import url
from django.views.i18n import javascript_catalog, set_language
from .api_views import (
APIContentTypeList, APITemplateListView, APITemplateView
)
from .api_views import ContentTypeAPIViewSet, TemplateAPIViewSet
from .views import (
AboutView, CheckVersionView, CurrentUserLocaleProfileDetailsView,
CurrentUserLocaleProfileEditView, FaviconRedirectView, HomeView,
@@ -15,67 +13,65 @@ from .views import (
)
urlpatterns = [
url(r'^$', RootView.as_view(), name='root'),
url(r'^home/$', HomeView.as_view(), name='home'),
url(r'^about/$', AboutView.as_view(), name='about_view'),
url(regex=r'^$', name='root', view=RootView.as_view()),
url(regex=r'^home/$', name='home', view=HomeView.as_view()),
url(regex=r'^about/$', name='about_view', view=AboutView.as_view()),
url(
r'^check_version/$', CheckVersionView.as_view(),
name='check_version_view'
regex=r'^check_version/$', name='check_version_view',
view=CheckVersionView.as_view()
),
url(r'^license/$', LicenseView.as_view(), name='license_view'),
url(regex=r'^license/$', name='license_view', view=LicenseView.as_view()),
url(
r'^packages/licenses/$', PackagesLicensesView.as_view(),
name='packages_licenses_view'
regex=r'^packages/licenses/$', name='packages_licenses_view',
view=PackagesLicensesView.as_view()
),
url(
r'^object/multiple/action/$', multi_object_action_view,
name='multi_object_action_view'
regex=r'^objects/multiple/action/$', name='multi_object_action_view',
view=multi_object_action_view
),
url(r'^setup/$', SetupListView.as_view(), name='setup_list'),
url(r'^tools/$', ToolsListView.as_view(), name='tools_list'),
url(regex=r'^setup/$', name='setup_list', view=SetupListView.as_view()),
url(regex=r'^tools/$', name='tools_list', view=ToolsListView.as_view()),
url(
r'^user/locale/$', CurrentUserLocaleProfileDetailsView.as_view(),
name='current_user_locale_profile_details'
regex=r'^users/current/locale/$',
name='current_user_locale_profile_details',
view=CurrentUserLocaleProfileDetailsView.as_view()
),
url(
r'^user/locale/edit/$', CurrentUserLocaleProfileEditView.as_view(),
name='current_user_locale_profile_edit'
regex=r'^users/current/locale/edit/$',
name='current_user_locale_profile_edit',
view=CurrentUserLocaleProfileEditView.as_view()
),
url(
r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/errors/$',
ObjectErrorLogEntryListView.as_view(), name='object_error_list'
regex=r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/errors/$',
name='object_error_list', view=ObjectErrorLogEntryListView.as_view()
),
url(
r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/errors/clear/$',
ObjectErrorLogEntryListClearView.as_view(),
name='object_error_list_clear'
regex=r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/errors/clear/$',
name='object_error_list_clear',
view=ObjectErrorLogEntryListClearView.as_view()
),
]
urlpatterns += [
url(
r'^favicon\.ico$', FaviconRedirectView.as_view()
regex=r'^favicon\.ico$', view=FaviconRedirectView.as_view()
),
url(
r'^jsi18n/(?P<packages>\S+?)/$', javascript_catalog,
name='javascript_catalog'
regex=r'^jsi18n/(?P<packages>\S+?)/$', name='javascript_catalog',
view=javascript_catalog
),
url(
r'^set_language/$', set_language, name='set_language'
regex=r'^set_language/$', name='set_language', view=set_language
),
]
api_urls = [
url(
r'^content_types/$', APIContentTypeList.as_view(),
name='content-type-list'
),
url(
r'^templates/$', APITemplateListView.as_view(),
name='template-list'
),
url(
r'^templates/(?P<name>[-\w]+)/$', APITemplateView.as_view(),
name='template-detail'
),
]
api_router_entries = (
{
'prefix': r'content_types', 'viewset': ContentTypeAPIViewSet,
'basename': 'content_type'
},
{
'prefix': r'templates', 'viewset': TemplateAPIViewSet,
'basename': 'template'
},
)

View File

@@ -1,18 +1,15 @@
from __future__ import unicode_literals
import logging
import os
import shutil
import tempfile
from django.conf import settings
from django.core.exceptions import FieldDoesNotExist
from django.db.models.constants import LOOKUP_SEP
from django.urls import resolve as django_resolve
from django.urls.base import get_script_prefix
from django.utils.datastructures import MultiValueDict
from django.utils.http import urlencode as django_urlencode
from django.utils.http import urlquote as django_urlquote
from django.utils.module_loading import import_string
from django.utils.six.moves import reduce as reduce_function
from django.utils.six.moves import xmlrpc_client
@@ -20,7 +17,6 @@ import mayan
from .exceptions import NotLatestVersion, UnknownLatestVersion
from .literals import DJANGO_SQLITE_BACKEND, MAYAN_PYPI_NAME, PYPI_URL
from .settings import setting_temporary_directory
logger = logging.getLogger(__name__)
@@ -39,27 +35,6 @@ def check_version():
raise NotLatestVersion(upstream_version=versions[0])
# http://stackoverflow.com/questions/123198/how-do-i-copy-a-file-in-python
def copyfile(source, destination, buffer_size=1024 * 1024):
"""
Copy a file from source to dest. source and dest
can either be strings or any object with a read or
write method, like StringIO for example.
"""
source_descriptor = get_descriptor(source)
destination_descriptor = get_descriptor(destination, read=False)
while True:
copy_buffer = source_descriptor.read(buffer_size)
if copy_buffer:
destination_descriptor.write(copy_buffer)
else:
break
source_descriptor.close()
destination_descriptor.close()
def encapsulate(function):
# Workaround Django ticket 15791
# Changeset 16045
@@ -68,74 +43,49 @@ def encapsulate(function):
return lambda: function
def fs_cleanup(filename, file_descriptor=None, suppress_exceptions=True):
"""
Tries to remove the given filename. Ignores non-existent files
"""
if file_descriptor:
os.close(file_descriptor)
def get_related_field(model, related_field_name):
try:
os.remove(filename)
except OSError:
local_field_name, remaining_field_path = related_field_name.split(
LOOKUP_SEP, 1
)
except ValueError:
local_field_name = related_field_name
remaining_field_path = None
related_field = model._meta.get_field(local_field_name)
if remaining_field_path:
return get_related_field(
model=related_field.related_model,
related_field_name=remaining_field_path
)
return related_field
def introspect_attribute(attribute_name, obj):
try:
# Try as a related field
obj._meta.get_field(field_name=attribute_name)
except (AttributeError, FieldDoesNotExist):
attribute_name = attribute_name.replace('__', '.')
try:
shutil.rmtree(filename)
except OSError:
if suppress_exceptions:
pass
else:
raise
def get_descriptor(file_input, read=True):
try:
# Is it a file like object?
file_input.seek(0)
except AttributeError:
# If not, try open it.
if read:
return open(file_input, mode='rb')
# If there are separators in the attribute name, traverse them
# to the final attribute
attribute_part, attribute_remaining = attribute_name.split(
'.', 1
)
except ValueError:
return attribute_name, obj
else:
return open(file_input, mode='wb')
related_field = obj._meta.get_field(field_name=attribute_part)
return introspect_attribute(
attribute_name=attribute_part,
obj=related_field.related_model,
)
else:
return file_input
def get_storage_subclass(dotted_path):
"""
Import a storage class and return a subclass that will always return eq
True to avoid creating a new migration when for runtime storage class
changes.
"""
imported_storage_class = import_string(dotted_path=dotted_path)
class StorageSubclass(imported_storage_class):
def __init__(self, *args, **kwargs):
return super(StorageSubclass, self).__init__(*args, **kwargs)
def __eq__(self, other):
return True
def deconstruct(self):
return ('mayan.apps.common.classes.FakeStorageSubclass', (), {})
return StorageSubclass
def TemporaryFile(*args, **kwargs):
kwargs.update({'dir': setting_temporary_directory.value})
return tempfile.TemporaryFile(*args, **kwargs)
def mkdtemp(*args, **kwargs):
kwargs.update({'dir': setting_temporary_directory.value})
return tempfile.mkdtemp(*args, **kwargs)
def mkstemp(*args, **kwargs):
kwargs.update({'dir': setting_temporary_directory.value})
return tempfile.mkstemp(*args, **kwargs)
return attribute_name, obj
def resolve(path, urlconf=None):
@@ -230,24 +180,3 @@ def urlquote(link=None, get=None):
return '%s%s' % (link, django_urlencode(get, doseq=True))
else:
return django_urlquote(link)
def validate_path(path):
if not os.path.exists(path):
# If doesn't exist try to create it
try:
os.mkdir(path)
except Exception as exception:
logger.debug('unhandled exception: %s', exception)
return False
# Check if it is writable
try:
fd, test_filepath = tempfile.mkstemp(dir=path)
os.close(fd)
os.unlink(test_filepath)
except Exception as exception:
logger.debug('unhandled exception: %s', exception)
return False
return True

View File

@@ -6,27 +6,25 @@ from django.conf import settings
from django.contrib import messages
from django.contrib.contenttypes.models import ContentType
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, resolve_url
from django.shortcuts import get_object_or_404
from django.template import RequestContext
from django.urls import reverse_lazy
from django.urls import reverse, reverse_lazy
from django.utils import timezone, translation
from django.utils.http import urlencode
from django.utils.translation import ugettext_lazy as _
from django.views.generic import RedirectView, TemplateView
from mayan.apps.acls.models import AccessControlList
from mayan.apps.common.mixins import (
ContentTypeViewMixin, ExternalObjectMixin
)
from .exceptions import NotLatestVersion, UnknownLatestVersion
from .forms import (
LicenseForm, LocaleProfileForm, LocaleProfileForm_view,
PackagesLicensesForm
)
from .generics import ( # NOQA
AssignRemoveView, ConfirmView, FormView, MultiFormView,
MultipleObjectConfirmActionView, MultipleObjectFormActionView, SimpleView,
SingleObjectCreateView, SingleObjectDeleteView, SingleObjectDetailView,
SingleObjectDownloadView, SingleObjectDynamicFormCreateView,
SingleObjectDynamicFormEditView, SingleObjectEditView, SingleObjectListView
from .generics import (
ConfirmView, SimpleView, SingleObjectEditView, SingleObjectListView
)
from .icons import icon_object_error_list, icon_setup
from .menus import menu_setup, menu_tools
@@ -171,7 +169,9 @@ class ObjectErrorLogEntryListClearView(ConfirmView):
)
class ObjectErrorLogEntryListView(SingleObjectListView):
class ObjectErrorLogEntryListView(ContentTypeViewMixin, ExternalObjectMixin, SingleObjectListView):
#TODO: Update for MERC 6. Return 404.
"""
def dispatch(self, request, *args, **kwargs):
AccessControlList.objects.check_access(
obj=self.get_object(), permissions=permission_error_log_view,
@@ -181,6 +181,7 @@ class ObjectErrorLogEntryListView(SingleObjectListView):
return super(ObjectErrorLogEntryListView, self).dispatch(
request, *args, **kwargs
)
"""
def get_extra_context(self):
return {
@@ -202,6 +203,7 @@ class ObjectErrorLogEntryListView(SingleObjectListView):
'title': _('Error log entries for: %s' % self.get_object()),
}
"""
def get_object(self):
content_type = get_object_or_404(
klass=ContentType, app_label=self.kwargs['app_label'],
@@ -211,9 +213,9 @@ class ObjectErrorLogEntryListView(SingleObjectListView):
return get_object_or_404(
klass=content_type.model_class(), pk=self.kwargs['object_id']
)
"""
def get_object_list(self):
return self.get_object().error_logs.all()
return self.get_external_object().error_logs.all()
class PackagesLicensesView(SimpleView):
@@ -234,41 +236,41 @@ class RootView(SimpleView):
template_name = 'appearance/root.html'
class SetupListView(TemplateView):
class SetupListView(SimpleView):
template_name = 'appearance/generic_list_horizontal.html'
def get_context_data(self, **kwargs):
data = super(SetupListView, self).get_context_data(**kwargs)
def get_extra_context(self):
context = RequestContext(self.request)
context['request'] = self.request
data.update(
{
'no_results_icon': icon_setup,
'no_results_label': _('No setup options available.'),
'no_results_text': _(
'No results here means that don\'t have the required '
'permissions to perform administrative task.'
),
'resolved_links': menu_setup.resolve(context=context),
'title': _('Setup items'),
}
)
return data
return {
'no_results_icon': icon_setup,
'no_results_label': _('No setup options available.'),
'no_results_text': _(
'No results here means that don\'t have the required '
'permissions to perform administrative task.'
),
'resolved_links': menu_setup.resolve(context=context),
'title': _('Setup'),
'subtitle': _(
'Here you can configure all aspects of the system.'
),
}
class ToolsListView(SimpleView):
template_name = 'appearance/generic_list_horizontal.html'
def get_menu_links(self):
def get_extra_context(self):
context = RequestContext(self.request)
context['request'] = self.request
return menu_tools.resolve(context=context)
def get_extra_context(self):
return {
'resolved_links': self.get_menu_links(),
'resolved_links': menu_tools.resolve(context=context),
'title': _('Tools'),
'subtitle': _(
'These are programs are modules used to do maintenance in '
'the system.'
),
}
@@ -280,7 +282,7 @@ def multi_object_action_view(request):
next = request.POST.get(
'next', request.GET.get(
'next', request.META.get(
'HTTP_REFERER', resolve_url(settings.LOGIN_REDIRECT_URL)
'HTTP_REFERER', reverse(setting_home_view.value)
)
)
)
@@ -297,7 +299,7 @@ def multi_object_action_view(request):
messages.error(request, _('No action selected.'))
return HttpResponseRedirect(
request.META.get(
'HTTP_REFERER', resolve_url(settings.LOGIN_REDIRECT_URL)
'HTTP_REFERER', reverse(setting_home_view.value)
)
)
@@ -305,7 +307,7 @@ def multi_object_action_view(request):
messages.error(request, _('Must select at least one item.'))
return HttpResponseRedirect(
request.META.get(
'HTTP_REFERER', resolve_url(settings.LOGIN_REDIRECT_URL)
'HTTP_REFERER', reverse(setting_home_view.value)
)
)

View File

@@ -0,0 +1,13 @@
from __future__ import absolute_import
class DatabaseWarning(UserWarning):
"""
Warning when using unsupported database backends
"""
class InterfaceWarning(UserWarning):
"""
Warning when using obsolete internal interfaces
"""

View File

@@ -3,7 +3,6 @@ from __future__ import unicode_literals
from django import forms
from django.template import Context, Template
from django.utils.encoding import force_text
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from .icons import icon_fail as default_icon_fail