diff --git a/apps/bootstrap/links.py b/apps/bootstrap/links.py index 4f15c4d24e..63e51dfa29 100644 --- a/apps/bootstrap/links.py +++ b/apps/bootstrap/links.py @@ -6,7 +6,7 @@ from .permissions import (PERMISSION_BOOTSTRAP_VIEW, PERMISSION_BOOTSTRAP_CREATE PERMISSION_BOOTSTRAP_EDIT, PERMISSION_BOOTSTRAP_DELETE, PERMISSION_BOOTSTRAP_EXECUTE, PERMISSION_BOOTSTRAP_DUMP, PERMISSION_NUKE_DATABASE, PERMISSION_BOOTSTRAP_EXPORT, - PERMISSION_BOOTSTRAP_IMPORT) + PERMISSION_BOOTSTRAP_IMPORT, PERMISSION_BOOTSTRAP_REPOSITORY_SYNC) link_bootstrap_setup_tool = {'text': _(u'bootstrap'), 'view': 'bootstrap_setup_list', 'icon': 'lightning.png', 'permissions': [PERMISSION_BOOTSTRAP_VIEW]} link_bootstrap_setup_list = {'text': _(u'bootstrap setup list'), 'view': 'bootstrap_setup_list', 'famfam': 'lightning', 'permissions': [PERMISSION_BOOTSTRAP_VIEW]} @@ -19,4 +19,5 @@ link_bootstrap_setup_dump = {'text': _(u'dump current setup'), 'view': 'bootstra link_bootstrap_setup_export = {'text': _(u'export'), 'view': 'bootstrap_setup_export', 'args': 'object.pk', 'famfam': 'disk', 'permissions': [PERMISSION_BOOTSTRAP_EXPORT]} link_bootstrap_setup_import_from_file = {'text': _(u'import from file'), 'view': 'bootstrap_setup_import_from_file', 'famfam': 'folder', 'permissions': [PERMISSION_BOOTSTRAP_IMPORT]} link_bootstrap_setup_import_from_url = {'text': _(u'import from URL'), 'view': 'bootstrap_setup_import_from_url', 'famfam': 'world', 'permissions': [PERMISSION_BOOTSTRAP_IMPORT]} +link_bootstrap_setup_repository_sync = {'text': _(u'sync with repository'), 'view': 'bootstrap_setup_repository_sync', 'famfam': 'world', 'permissions': [PERMISSION_BOOTSTRAP_REPOSITORY_SYNC]} link_erase_database = {'text': _(u'erase database'), 'view': 'erase_database_view', 'icon': 'radioactivity.png', 'permissions': [PERMISSION_NUKE_DATABASE]} diff --git a/apps/bootstrap/literals.py b/apps/bootstrap/literals.py index 250a84db63..c85577d1de 100644 --- a/apps/bootstrap/literals.py +++ b/apps/bootstrap/literals.py @@ -76,3 +76,6 @@ FIXTURE_METADATA_DESCRIPTION = 'description' BOOTSTRAP_EXTENSION = 'txt' BOOTSTRAP_SETUP_MAGIC_NUMBER = 'bootstrap setup' + +BOOTSTRAP_REPOSITORY_URL = 'http://bootstrap.mayan-edms.com' +BOOTSTRAP_REPOSITORY_INDEX_FILE = '_repo_index.txt' diff --git a/apps/bootstrap/managers.py b/apps/bootstrap/managers.py index ed94be0dd2..b5e0bea331 100644 --- a/apps/bootstrap/managers.py +++ b/apps/bootstrap/managers.py @@ -6,9 +6,13 @@ import requests from django.db import models from django.core import serializers +from django.utils.simplejson import loads +from django.db import IntegrityError +from django.db.models import Q from .classes import BootstrapModel, FixtureMetadata -from .literals import (FIXTURE_TYPE_FIXTURE_PROCESS, FIXTURE_TYPE_EMPTY_FIXTURE) +from .literals import (FIXTURE_TYPE_FIXTURE_PROCESS, FIXTURE_TYPE_EMPTY_FIXTURE, + BOOTSTRAP_REPOSITORY_URL, BOOTSTRAP_REPOSITORY_INDEX_FILE) logger = logging.getLogger(__name__) @@ -28,19 +32,44 @@ class BootstrapSetupManager(models.Manager): result.append(model_fixture) return FIXTURE_TYPE_FIXTURE_PROCESS[serialization_format]('\n'.join(result)) - def import_setup(self, file_data): + def import_setup(self, file_data, overwrite=False): BootstrapModel.check_magic_number(file_data) metadata = FixtureMetadata.read_all(file_data) instance = self.model(fixture=file_data, **metadata) - instance.save(update_metadata=False) + try: + instance.save(update_metadata=False) + except IntegrityError: + if not overwrite: + raise + else: + # Delete conflicting bootstrap setups + query = Q() + if 'slug' in metadata: + query = query | Q(slug=metadata['slug']) + + if 'name' in metadata: + query = query | Q(name=metadata['name']) + + self.model.objects.filter(query).delete() + self.import_setup(file_data) def import_from_file(self, files): file_data = files.read() self.import_setup(file_data) - def import_from_url(self, url): + def import_from_url(self, url, **kwargs): response = requests.get(url) if response.status_code == requests.codes.ok: - self.import_setup(response.text) + self.import_setup(response.text, **kwargs) else: response.raise_for_status() + + def repository_sync(self): + response = requests.get('%s/%s' % (BOOTSTRAP_REPOSITORY_URL, BOOTSTRAP_REPOSITORY_INDEX_FILE)) + if response.status_code == requests.codes.ok: + for entry in loads(response.text): + bootstrap_setup_url = '%s/%s' % (BOOTSTRAP_REPOSITORY_URL, entry['filename']) + self.import_from_url(bootstrap_setup_url, overwrite=True) + else: + response.raise_for_status() + diff --git a/apps/bootstrap/permissions.py b/apps/bootstrap/permissions.py index 6102374e12..71165fe954 100644 --- a/apps/bootstrap/permissions.py +++ b/apps/bootstrap/permissions.py @@ -14,4 +14,5 @@ PERMISSION_BOOTSTRAP_EXECUTE = Permission.objects.register(namespace, 'bootstrap PERMISSION_BOOTSTRAP_DUMP = Permission.objects.register(namespace, 'bootstrap_dump', _(u'Dump the current project\s setup into a bootstrap setup')) PERMISSION_BOOTSTRAP_EXPORT = Permission.objects.register(namespace, 'bootstrap_export', _(u'Export bootstrap setups as files')) PERMISSION_BOOTSTRAP_IMPORT = Permission.objects.register(namespace, 'bootstrap_import', _(u'Import new bootstrap setups')) +PERMISSION_BOOTSTRAP_REPOSITORY_SYNC = Permission.objects.register(namespace, 'bootstrap_repo_sync', _(u'Sync the local bootstrap setups with a published repository')) PERMISSION_NUKE_DATABASE = Permission.objects.register(namespace, 'nuke_database', _(u'Erase the entire database and document storage')) diff --git a/apps/bootstrap/post_init.py b/apps/bootstrap/post_init.py index 0e72128cc4..3b61c1f0f6 100644 --- a/apps/bootstrap/post_init.py +++ b/apps/bootstrap/post_init.py @@ -8,7 +8,8 @@ from main import __version__ from .links import (link_bootstrap_setup_create, link_bootstrap_setup_execute, link_bootstrap_setup_list, link_bootstrap_setup_edit, link_bootstrap_setup_delete, link_bootstrap_setup_view, link_bootstrap_setup_dump, link_bootstrap_setup_export, - link_bootstrap_setup_import_from_url, link_bootstrap_setup_import_from_file) + link_bootstrap_setup_import_from_url, link_bootstrap_setup_import_from_file, + link_bootstrap_setup_repository_sync) from .models import BootstrapSetup from .classes import FixtureMetadata from .literals import (FIXTURE_METADATA_CREATED, FIXTURE_METADATA_EDITED, @@ -16,8 +17,8 @@ from .literals import (FIXTURE_METADATA_CREATED, FIXTURE_METADATA_EDITED, FIXTURE_METADATA_DESCRIPTION, DATETIME_STRING_FORMAT, FIXTURE_METADATA_SLUG) register_links([BootstrapSetup], [link_bootstrap_setup_view, link_bootstrap_setup_edit, link_bootstrap_setup_delete, link_bootstrap_setup_execute, link_bootstrap_setup_export]) -register_links([BootstrapSetup], [link_bootstrap_setup_list, link_bootstrap_setup_create, link_bootstrap_setup_dump, link_bootstrap_setup_import_from_file, link_bootstrap_setup_import_from_url], menu_name='secondary_menu') -register_links(['bootstrap_setup_list', 'bootstrap_setup_create', 'bootstrap_setup_dump', 'bootstrap_setup_import_from_file', 'bootstrap_setup_import_from_url'], [link_bootstrap_setup_list, link_bootstrap_setup_create, link_bootstrap_setup_dump, link_bootstrap_setup_import_from_file, link_bootstrap_setup_import_from_url], menu_name='secondary_menu') +register_links([BootstrapSetup], [link_bootstrap_setup_list, link_bootstrap_setup_create, link_bootstrap_setup_dump, link_bootstrap_setup_import_from_file, link_bootstrap_setup_import_from_url, link_bootstrap_setup_repository_sync], menu_name='secondary_menu') +register_links(['bootstrap_setup_list', 'bootstrap_setup_create', 'bootstrap_setup_dump', 'bootstrap_setup_import_from_file', 'bootstrap_setup_import_from_url', 'bootstrap_setup_repository_sync'], [link_bootstrap_setup_list, link_bootstrap_setup_create, link_bootstrap_setup_dump, link_bootstrap_setup_import_from_file, link_bootstrap_setup_import_from_url, link_bootstrap_setup_repository_sync], menu_name='secondary_menu') FixtureMetadata(FIXTURE_METADATA_CREATED, generate_function=lambda fixture_instance: fixture_instance.created.strftime(DATETIME_STRING_FORMAT), read_function=lambda x: datetime.datetime.strptime(x, DATETIME_STRING_FORMAT), property_name='created') FixtureMetadata(FIXTURE_METADATA_EDITED, generate_function=lambda fixture_instance: datetime.datetime.now().strftime(DATETIME_STRING_FORMAT)) diff --git a/apps/bootstrap/static/images/icons/world.png b/apps/bootstrap/static/images/icons/world.png new file mode 100755 index 0000000000..644d9d53a7 Binary files /dev/null and b/apps/bootstrap/static/images/icons/world.png differ diff --git a/apps/bootstrap/urls.py b/apps/bootstrap/urls.py index 593ca5baf0..1ad28beebf 100644 --- a/apps/bootstrap/urls.py +++ b/apps/bootstrap/urls.py @@ -11,5 +11,6 @@ urlpatterns = patterns('bootstrap.views', url(r'^setup/dump/$', 'bootstrap_setup_dump', (), 'bootstrap_setup_dump'), url(r'^setup/import/file/$', 'bootstrap_setup_import_from_file', (), 'bootstrap_setup_import_from_file'), url(r'^setup/import/url/$', 'bootstrap_setup_import_from_url', (), 'bootstrap_setup_import_from_url'), + url(r'^setup/repository/sync/$', 'bootstrap_setup_repository_sync', (), 'bootstrap_setup_repository_sync'), url(r'^nuke/$', 'erase_database_view', (), 'erase_database_view'), ) diff --git a/apps/bootstrap/views.py b/apps/bootstrap/views.py index a6fb813fac..1112afb309 100644 --- a/apps/bootstrap/views.py +++ b/apps/bootstrap/views.py @@ -17,7 +17,7 @@ from .classes import Cleanup, BootstrapModel from .permissions import (PERMISSION_BOOTSTRAP_VIEW, PERMISSION_BOOTSTRAP_CREATE, PERMISSION_BOOTSTRAP_EDIT, PERMISSION_BOOTSTRAP_DELETE, PERMISSION_BOOTSTRAP_EXECUTE, PERMISSION_NUKE_DATABASE, PERMISSION_BOOTSTRAP_DUMP, - PERMISSION_BOOTSTRAP_EXPORT, PERMISSION_BOOTSTRAP_IMPORT) + PERMISSION_BOOTSTRAP_EXPORT, PERMISSION_BOOTSTRAP_IMPORT, PERMISSION_BOOTSTRAP_REPOSITORY_SYNC) from .forms import (BootstrapSetupForm, BootstrapSetupForm_view, BootstrapSetupForm_dump, BootstrapSetupForm_edit, BootstrapFileImportForm, BootstrapURLImportForm) from .exceptions import ExistingData, NotABootstrapSetup @@ -242,7 +242,7 @@ def bootstrap_setup_import_from_file(request): except NotABootstrapSetup: messages.error(request, _(u'File is not a bootstrap setup.')) except Exception as exception: - messages.error(request, exception) + messages.error(request, _(u'Error importing bootstrap setup from file; %s.') % exception) return HttpResponseRedirect(previous) else: form = BootstrapFileImportForm() @@ -270,7 +270,7 @@ def bootstrap_setup_import_from_url(request): except NotABootstrapSetup: messages.error(request, _(u'Data from URL is not a bootstrap setup.')) except Exception as exception: - messages.error(request, exception) + messages.error(request, _(u'Error importing bootstrap setup from URL; %s.') % exception) return HttpResponseRedirect(previous) else: form = BootstrapURLImportForm() @@ -312,3 +312,31 @@ def erase_database_view(request): return render_to_response('generic_confirm.html', context, context_instance=RequestContext(request)) + + +def bootstrap_setup_repository_sync(request): + Permission.objects.check_permissions(request.user, [PERMISSION_BOOTSTRAP_REPOSITORY_SYNC]) + + post_action_redirect = reverse('bootstrap_setup_list') + + previous = request.POST.get('previous', request.GET.get('previous', request.META.get('HTTP_REFERER', '/'))) + next = request.POST.get('next', request.GET.get('next', post_action_redirect if post_action_redirect else request.META.get('HTTP_REFERER', '/'))) + + if request.method == 'POST': + try: + BootstrapSetup.objects.repository_sync() + messages.success(request, _(u'Bootstrap repository successfully synchronized.')) + except Exception, e: + messages.error(request, _(u'Bootstrap repository synchronization error: %(error)s') % {'error': e}) + + return HttpResponseRedirect(reverse('bootstrap_setup_list')) + + context = { + 'previous': previous, + 'next': next, + 'title': _(u'Are you sure you wish to synchronize with the bootstrap repository?'), + 'form_icon': 'world.png', + } + + return render_to_response('generic_confirm.html', context, + context_instance=RequestContext(request))