diff --git a/HISTORY.rst b/HISTORY.rst index 05e6e80..7427e20 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,12 +1,20 @@ History ======= +Version 1.1.0 - 2015/10/17 +-------------------------- + +* Better 'manager_frontend' structure, cleaning some minor things, close #34; +* Added ``asset_tag``; +* Dont show 'available systems' form if all systems allready exist, close #35; +* Moved 'Recalbox manifest' stuff into ``project.recalbox_manifest``, removed manifest loading from settings in profit of loading from ``urls.py``; + Version 1.0.2 - 2015/10/16 -------------------------- * Fixed CRLF caracters when editing recalbox config, close #31; * Fixed not packaged assets with production settings; -* Refactored asset management to use persistent manifest registry (avoid to load JSON manifest file on every request) and move it to its own embedded app to ``project.assets_cartographer``; +* Refactored asset management to use persistent manifest registry (avoid to load JSON manifest file on every request) and move it to its own embedded app to ``project.assets_cartographer``, close #33; * Open Virtual gamepad link into a new window/tab; * Fixed Dropzone assets, close #32; * Moved Python requirements to ``pip-requirements`` directory and refactored them for backward compatible with Recalbox versions without ``psutil`` yet; diff --git a/README.rst b/README.rst index 19b13d6..e78aa5e 100644 --- a/README.rst +++ b/README.rst @@ -68,7 +68,7 @@ Before doing anything, ensure the Raspberry can access to the internet else conf Go where you want to install the manager directory then type the following commands: :: - wget -q -O - https://raw.githubusercontent.com/sveetch/recalbox-manager/master/deployment/install.sh | bash /dev/stdin --release=1.0.2 + wget -q -O - https://raw.githubusercontent.com/sveetch/recalbox-manager/master/deployment/install.sh | bash /dev/stdin --release=1.1.0 This will download an install script and automatically execute it to proceed to install. diff --git a/__init__.py b/__init__.py index f86e5f9..ed47ede 100644 --- a/__init__.py +++ b/__init__.py @@ -1,4 +1,4 @@ """ Django project to manage a Recalbox from a web interface """ -__version__ = '1.0.2' \ No newline at end of file +__version__ = '1.1.0' \ No newline at end of file diff --git a/project/assets_cartographer/parser.py b/project/assets_cartographer/parser.py index 58091d1..e163d46 100644 --- a/project/assets_cartographer/parser.py +++ b/project/assets_cartographer/parser.py @@ -27,7 +27,7 @@ This would eventually not work with static files through S3/etc.. """ -import json, os +import os from django.conf import settings from django.template import Context diff --git a/project/manager_frontend/templates/assets/javascript_tag.html b/project/assets_cartographer/templates/assets/javascript_tag.html similarity index 100% rename from project/manager_frontend/templates/assets/javascript_tag.html rename to project/assets_cartographer/templates/assets/javascript_tag.html diff --git a/project/manager_frontend/templates/assets/stylesheet_tag.html b/project/assets_cartographer/templates/assets/stylesheet_tag.html similarity index 100% rename from project/manager_frontend/templates/assets/stylesheet_tag.html rename to project/assets_cartographer/templates/assets/stylesheet_tag.html diff --git a/project/assets_cartographer/templatetags/assets.py b/project/assets_cartographer/templatetags/assets.py index 1ba2b8e..772f338 100644 --- a/project/assets_cartographer/templatetags/assets.py +++ b/project/assets_cartographer/templatetags/assets.py @@ -3,9 +3,10 @@ TODO: another tag for loading asset kind that are not stylesheet or javascript """ +from django.template.defaulttags import register + from project.assets_cartographer import manifest from project.assets_cartographer.parser import AssetTagsManagerFromManifest -from django.template.defaulttags import register def render_asset_tags(kind, *bundle_names): """ @@ -18,6 +19,25 @@ def render_asset_tags(kind, *bundle_names): return AssetTagsManagerFromManifest(manifest.get_registry()).render_for_kind(bundle_names, kind) +@register.simple_tag +def asset_tag(kind, *bundle_names): + """ + Build tag for any kind of asset + + Usage + ***** + + Give asset kind name as first argument, then package names (one or more) as other arguments: :: + + {% asset_tag "stylesheet" "css/item1.min.css" "css/item2.min.css" .. %} + + Depending on settings.ASSETS_PACKAGED it would build an unique tag (if True) for the packaged asset file or tag (if False) for each package components files. + + Obviously, the given kind have to exist in your asset manifest. + """ + return render_asset_tags(kind, *bundle_names) + + @register.simple_tag def stylesheet_tag(*bundle_names): """ @@ -26,7 +46,7 @@ def stylesheet_tag(*bundle_names): Usage ***** - Just gives package names (one or more) as arguments: :: + Just give package names (one or more) as arguments: :: {% stylesheet_tag "css/item1.min.css" "css/item2.min.css" .. %} @@ -43,7 +63,7 @@ def javascript_tag(*bundle_names): Usage ***** - Just gives package names (one or more) as arguments: :: + Just give package names (one or more) as arguments: :: {% javascript_tag "css/item1.min.js" "css/item2.min.js" .. %} diff --git a/project/manager_frontend/forms/systems.py b/project/manager_frontend/forms/systems.py index e1ca1ff..bcc89f3 100644 --- a/project/manager_frontend/forms/systems.py +++ b/project/manager_frontend/forms/systems.py @@ -9,10 +9,14 @@ from django.utils.translation import ugettext_lazy as _ from django.core.files.storage import FileSystemStorage +from project.recalbox_manifest import manifest as RECALBOX_MANIFEST + from project.manager_frontend.forms import CrispyFormMixin + from project.utils.imports import safe_import_module -class SystemCreateForm(CrispyFormMixin, forms.Form): +#class SystemCreateForm(CrispyFormMixin, forms.Form): +class SystemCreateForm(forms.Form): """ Create a new system directory """ @@ -23,7 +27,7 @@ def __init__(self, *args, **kwargs): self.available_systems = kwargs.pop('available_systems') super(SystemCreateForm, self).__init__(*args, **kwargs) - super(forms.Form, self).__init__(*args, **kwargs) + #super(forms.Form, self).__init__(*args, **kwargs) self.fields['name'] = forms.ChoiceField(label=_('Add a new system'), choices=self.available_systems, required=True) @@ -32,4 +36,4 @@ def save(self): os.mkdir(os.path.join(settings.RECALBOX_ROMS_PATH, name), 0755) - return settings.RECALBOX_MANIFEST[name] + return RECALBOX_MANIFEST[name] diff --git a/project/manager_frontend/templates/manager_frontend/systems_list.html b/project/manager_frontend/templates/manager_frontend/systems_list.html index 1267a39..aa00b92 100644 --- a/project/manager_frontend/templates/manager_frontend/systems_list.html +++ b/project/manager_frontend/templates/manager_frontend/systems_list.html @@ -7,15 +7,7 @@
  • {% trans "Total" %}: {{ systems_list|length }}
  • -{% comment %}
    {% for key,name in systems_list %} -
    - - {{ name }} - -
    -{% endfor %}
    {% endcomment %} - -
    +{% if available_systems %} {% csrf_token %}
    @@ -47,7 +39,7 @@
    -
    +{% endif %}
    {% for key,name in systems_list %} diff --git a/project/utils/manifest.py b/project/manager_frontend/utils/manifest.py similarity index 97% rename from project/utils/manifest.py rename to project/manager_frontend/utils/manifest.py index 81ff255..bedccca 100644 --- a/project/utils/manifest.py +++ b/project/manager_frontend/utils/manifest.py @@ -5,7 +5,7 @@ import json import xml.etree.cElementTree as ET -class ManifestParser(object): +class RecalboxManifestParser(object): """ Recalbox XML manifest parser """ diff --git a/project/utils/views.py b/project/manager_frontend/utils/views.py similarity index 94% rename from project/utils/views.py rename to project/manager_frontend/utils/views.py index 7515f74..9c90f5b 100644 --- a/project/utils/views.py +++ b/project/manager_frontend/utils/views.py @@ -1,15 +1,11 @@ """ Some common class based views - -TODO: Move this into 'manager_frontend' app """ import json from django.views.generic.edit import TemplateResponseMixin, FormMixin, ProcessFormView from django.http import HttpResponse -from project.manager_frontend.forms.roms import RomUploadForm, RomDeleteForm - class MultiFormView(TemplateResponseMixin, FormMixin, ProcessFormView): """ This is a huge rewrite and mixing of some CBV views and mixins to be able @@ -29,9 +25,9 @@ class MultiFormView(TemplateResponseMixin, FormMixin, ProcessFormView): Both must be unique through the forms of the view. - WARNING: This view should implement some warnings about needed stuff like - form attributes and form object names if not defined. So actually - you'll have to remember of this yourself. + TODO: This view should implement some warnings about needed stuff like + form attributes and form object names if not defined. So actually + you'll have to remember of this yourself. """ # The patterns used to find method/names from form 'form_key' attribute getformkwargs_pattern = 'get_{}_form_kwargs' diff --git a/project/manager_frontend/views/bios.py b/project/manager_frontend/views/bios.py index 6958aef..93e5566 100644 --- a/project/manager_frontend/views/bios.py +++ b/project/manager_frontend/views/bios.py @@ -1,19 +1,19 @@ """ Views for Bios views """ -import os, re +import os from operator import itemgetter from django.conf import settings -from django.views.generic import TemplateView -from django.views.generic.edit import FormView from django.core.urlresolvers import reverse from django.contrib import messages from django.http import Http404, HttpResponseBadRequest from django.utils.translation import ugettext as _ +from project.recalbox_manifest import manifest as RECALBOX_MANIFEST + from project.manager_frontend.forms.bios import BiosDeleteForm, BiosUploadForm -from project.utils.views import MultiFormView, JsonMixin +from project.manager_frontend.utils.views import MultiFormView, JsonMixin class BiosListView(MultiFormView): """ @@ -49,7 +49,7 @@ def get_bios_manifest(self): """ bios_dict = {} - for system_key, system_datas in settings.RECALBOX_MANIFEST.items(): + for system_key, system_datas in RECALBOX_MANIFEST.items(): system_name = system_datas.get('name', system_key) if len(system_datas.get('bios', []))>0: for md5hash,filename in system_datas['bios']: diff --git a/project/manager_frontend/views/config.py b/project/manager_frontend/views/config.py index acc632b..c4fe7cd 100644 --- a/project/manager_frontend/views/config.py +++ b/project/manager_frontend/views/config.py @@ -4,8 +4,6 @@ import os from django.conf import settings -from django.shortcuts import render -from django.views.generic import TemplateView from django.views.generic.edit import FormView from django.core.urlresolvers import reverse from django.contrib import messages diff --git a/project/manager_frontend/views/logs.py b/project/manager_frontend/views/logs.py index 384b23b..0fead4b 100644 --- a/project/manager_frontend/views/logs.py +++ b/project/manager_frontend/views/logs.py @@ -2,7 +2,6 @@ Views for logs files """ from django.conf import settings -from django.shortcuts import render from django.views.generic import TemplateView class LogsView(TemplateView): diff --git a/project/manager_frontend/views/monitor.py b/project/manager_frontend/views/monitor.py index e98c351..e3f6ea9 100644 --- a/project/manager_frontend/views/monitor.py +++ b/project/manager_frontend/views/monitor.py @@ -2,7 +2,6 @@ Monitoring views """ from django.conf import settings -from django.shortcuts import render from django.views.generic import TemplateView # Compatibility support for Recalbox versions from 3.2.x to 3.3.x diff --git a/project/manager_frontend/views/roms.py b/project/manager_frontend/views/roms.py index 45e69c5..4b896ac 100644 --- a/project/manager_frontend/views/roms.py +++ b/project/manager_frontend/views/roms.py @@ -1,18 +1,19 @@ """ Views for roms """ -import json, os +import os from operator import itemgetter from django.conf import settings -from django.views.generic import TemplateView from django.core.urlresolvers import reverse from django.contrib import messages from django.http import Http404, HttpResponseBadRequest from django.utils.translation import ugettext as _ +from project.recalbox_manifest import manifest as RECALBOX_MANIFEST + from project.manager_frontend.forms.roms import RomUploadForm, RomDeleteForm -from project.utils.views import MultiFormView, JsonMixin +from project.manager_frontend.utils.views import MultiFormView, JsonMixin class RomListView(MultiFormView): @@ -42,7 +43,7 @@ def init_system(self): 'name': self.system_key }) # Get the system manifest part if any, else a default dict - self.system_manifest = settings.RECALBOX_MANIFEST.get(self.system_key, default_manifest) + self.system_manifest = RECALBOX_MANIFEST.get(self.system_key, default_manifest) def get_rom_choices(self): rom_list = [] diff --git a/project/manager_frontend/views/systems.py b/project/manager_frontend/views/systems.py index 44f0464..55e78ec 100644 --- a/project/manager_frontend/views/systems.py +++ b/project/manager_frontend/views/systems.py @@ -10,6 +10,8 @@ from django.contrib import messages from django.utils.translation import ugettext as _ +from project.recalbox_manifest import manifest as RECALBOX_MANIFEST + from project.manager_frontend.forms.systems import SystemCreateForm class SystemsListView(FormView): @@ -29,14 +31,14 @@ def get_system_list(self): # Only display directories if os.path.isdir(os.path.join(path, item)) and not item.startswith('.'): # Try to find the dirname in the system manifest - if item in settings.RECALBOX_MANIFEST: - existing_sys.append( (item, settings.RECALBOX_MANIFEST[item]['name']) ) + if item in RECALBOX_MANIFEST: + existing_sys.append( (item, RECALBOX_MANIFEST[item]['name']) ) # Unknowed dirname else: existing_sys.append( (item, item) ) # Get system available: the ones that dont allready have a directory in systems dir - for sys_key, sys_values in settings.RECALBOX_MANIFEST.items(): + for sys_key, sys_values in RECALBOX_MANIFEST.items(): if sys_key not in [v[0] for v in existing_sys]: available_sys.append( (sys_key, sys_values['name']) ) @@ -50,6 +52,7 @@ def get_context_data(self, **kwargs): context.update({ 'systems_path': settings.RECALBOX_ROMS_PATH, 'systems_list': self.existing_systems, + 'available_systems': self.available_systems, }) return context @@ -63,11 +66,8 @@ def get_form_kwargs(self): def form_valid(self, form): new_system = form.save() - ## Throw a message to tell about upload success - #if is_backuped: - #messages.success(self.request, _('File has been backuped then edited')) - #else: - #messages.success(self.request, _('File has been edited')) + # Throw a message to tell about upload success + messages.success(self.request, _('System "{}" has been created').format(new_system['name'])) return super(SystemsListView, self).form_valid(form) diff --git a/project/recalbox_manifest/__init__.py b/project/recalbox_manifest/__init__.py new file mode 100644 index 0000000..79f80a7 --- /dev/null +++ b/project/recalbox_manifest/__init__.py @@ -0,0 +1,17 @@ +""" +Recalbox manifest to know everything about supported systems +""" +import os, json +from .registry import manifest + +def autodiscover(): + """ + Dummy loader actually, this should be backward compatible for futur assets + discovery per app + """ + from django.conf import settings + from .parser import RecalboxManifestParser + # Fill the registry with the whole JSON manifest + manifest.update( + RecalboxManifestParser(settings.RECALBOX_MANIFEST_FILEPATH).read() + ) diff --git a/project/recalbox_manifest/parser.py b/project/recalbox_manifest/parser.py new file mode 100644 index 0000000..acd0251 --- /dev/null +++ b/project/recalbox_manifest/parser.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +""" +Manifest parser +""" +import json +import xml.etree.cElementTree as ET + +class RecalboxManifestParser(object): + """ + Recalbox XML manifest parser + """ + def __init__(self, filepath): + self.filepath = filepath + + def get_system_extensions(self, node): + if node.find("extensions") is None: + return [] + return [item.text for item in node.find("extensions").findall("extension")] + + def get_system_download_links(self, node): + if node.find("download_links") is None: + return [] + return [item.text for item in node.find("download_links").findall("link")] + + def get_system_bios(self, node): + if node.find("bios") is None: + return [] + return [(item.get('md5', ''), item.text) for item in node.find("bios").findall("file")] + + def get_system_extra_comments(self, node): + if node.find("extra_comments") is None: + return [] + return [item.text for item in node.find("extra_comments").findall("comment")] + + def read(self): + """ + Return the manifest as a Python dictionnary + """ + manifest = {} + + print "self.filepath:", self.filepath + + tree = ET.parse(self.filepath) + + root = tree.getroot() + + for node in root: + system_key = node.get('key') + manifest[system_key] = { + 'name': node.get('name'), + 'extensions': self.get_system_extensions(node), + 'download_links': self.get_system_download_links(node), + 'bios': self.get_system_bios(node), + 'extra_comments': self.get_system_extra_comments(node), + } + + return manifest + + def json(self): + """ + Return the manifest as a JSON string + """ + return json.dumps(self.read(), indent=4) diff --git a/project/recalbox_manifest/registry.py b/project/recalbox_manifest/registry.py new file mode 100644 index 0000000..b1fb9a3 --- /dev/null +++ b/project/recalbox_manifest/registry.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +""" +Recalbox manifest registry +""" +# Just a dummy dict waiting to be filled +manifest = {} diff --git a/project/settings.py b/project/settings.py index 293f794..97f9b54 100644 --- a/project/settings.py +++ b/project/settings.py @@ -13,7 +13,7 @@ # Build paths inside the project like this: os.path.join(BASE_DIR, ...) import os -from utils.manifest import ManifestParser +from manager_frontend.utils.manifest import RecalboxManifestParser gettext = lambda s: s @@ -55,6 +55,7 @@ 'autobreadcrumbs', 'project.assets_cartographer', + 'project.recalbox_manifest', 'project.manager_frontend', #'ajaxuploader', @@ -151,6 +152,9 @@ os.path.join(PROJECT_DIR, "webapp_statics"), ) +# For Django messages framework to avoid database requests +MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage' + # For assets management ASSETS_PACKAGED = not(DEBUG) ASSETS_STRICT = False @@ -159,8 +163,6 @@ "javascripts": "assets/javascript_tag.html", "stylesheets": "assets/stylesheet_tag.html", } -# For Django messages framework, we could use this settings to avoid database requests -#MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage' # # Recalbox needed paths @@ -187,8 +189,5 @@ 'bios': [], } -# Recalbox manifest about system roms and bios -RECALBOX_MANIFEST = ManifestParser(RECALBOX_MANIFEST_FILEPATH).read() - # Blocking time during psutil watch for cpu charge in second (float number) RECALBOX_PSUTIL_CPU_INTERVAL = 0.5 # 0.1 seems a little too low but 1.0 add 1s on page loading time diff --git a/project/urls.py b/project/urls.py index 017c5cc..7536b29 100644 --- a/project/urls.py +++ b/project/urls.py @@ -18,12 +18,18 @@ from django.contrib import admin from django.views.generic import TemplateView +# Discover crumbs from enabled apps import autobreadcrumbs autobreadcrumbs.autodiscover() +# Load asset manifest in memory from project import assets_cartographer assets_cartographer.autodiscover() +# Load Recalbox manifest in memory +from project import recalbox_manifest +recalbox_manifest.autodiscover() + urlpatterns = [ url(r'^admin/', include(admin.site.urls)), url(r'^', include('project.manager_frontend.urls', namespace='manager')),