├── .gitignore ├── .travis.yml ├── MANIFEST.in ├── README.md ├── dirtyedit ├── __init__.py ├── admin.py ├── apps.py ├── conf.py ├── forms.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── populate_editor.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── msgs.py ├── templates │ ├── admin │ │ └── dirtyedit │ │ │ ├── change_form.html │ │ │ ├── change_list.html │ │ │ └── includes │ │ │ └── fieldset.html │ ├── codemirror2 │ │ └── codemirror_script.html │ └── reversion │ │ └── change_list.html ├── tests.py └── utils.py ├── docs └── img │ ├── screenshot1.png │ └── screenshot2.png ├── requirements.txt ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | settings.py 2 | *.sqlite3 3 | .project 4 | .pydevproject 5 | .settings 6 | 7 | __pycache__/ 8 | *.py[cod] 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | env/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *,cover 50 | 51 | # Django stuff: 52 | *.log 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | 59 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - 2.7 5 | - pypy 6 | 7 | env: 8 | - DJANGO=1.10 9 | 10 | install: 11 | - pip install -r requirements.txt 12 | 13 | script: 14 | python setup.py test 15 | 16 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include dirtyedit/migrations * 2 | recursive-include dirtyedit/templates * -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Django Dirty Edit 2 | ============== 3 | 4 | [![Build Status](https://travis-ci.org/synw/django-dirtyedit.svg?branch=master)](https://travis-ci.org/synw/django-dirtyedit) 5 | 6 | A Django application to edit files from the admin interface. This make it possible for example to let graphic 7 | designers edit some css files in the admin interface. 8 | 9 | Install 10 | -------------- 11 | 12 | pip install django-dirtyedit 13 | 14 | Add these to INSTALLED_APPS: 15 | 16 | 'dirtyedit', 17 | 'ckeditor', 18 | 'codemirror2', 19 | 'reversion', 20 | 21 | Note: `codemirror2` and `reversion` should be loaded after `dirtyedit` 22 | 23 | Settings 24 | -------------- 25 | 26 | Default values are: 27 | 28 | - `DIRTYEDIT_EDIT_MODE = 'code'` : uses codemirror. To use ckeditor set it to `'html'` 29 | - `DIRTYEDIT_CODEMIRROR_KEYMAP = 'default'` : set it to what your like. Ex: `'vim'`, `'emacs'` 30 | - `DIRTYEDIT_AUTHORIZED_PATHS = ('media', 'static', 'templates')` : writing in theses directories and their subdirectories is authorized. 31 | - `DIRTYEDIT_EXCLUDED_PATHS = ()` : to explicitly exclude some paths. Ex: `('media/private')` 32 | - `DIRTYEDIT_CAN_CREATE_FILES = False` : set it to `True` to allow file creation 33 | - `DIRTYEDIT_USE_REVERSION = True` : set it to False to disable reversion 34 | 35 | Management command 36 | ------------------ 37 | 38 | A management command is available to populate the database from a directory: example: 39 | 40 | ``` 41 | python3 manage.py populate_editor templates 42 | ``` 43 | 44 | This will save instances from each of the files that is in the `templates` folder. Note: this is not recursive, only 45 | the files in the directory will be processed 46 | 47 | Warning 48 | -------------- 49 | 50 | Handle with care: its pretty easy to break things with this module! Only give access to it to trusted admin users. 51 | 52 | Screenshots 53 | -------------- 54 | 55 | Select files: 56 | 57 | ![Dirty edit screenshot](https://raw.github.com/synw/django-dirtyedit/master/docs/img/screenshot1.png) 58 | 59 | Edit files (fullscreen edit is available): 60 | 61 | ![Dirty edit screenshot](https://raw.github.com/synw/django-dirtyedit/master/docs/img/screenshot2.png) 62 | 63 | Todo 64 | -------------- 65 | 66 | - Handle file types to auto setup codemirror highlighting mode -------------------------------------------------------------------------------- /dirtyedit/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.3' 2 | default_app_config = 'dirtyedit.apps.DirtyEditConfig' -------------------------------------------------------------------------------- /dirtyedit/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.contrib import admin 4 | from django.contrib import messages 5 | from dirtyedit.models import FileToEdit 6 | from dirtyedit.forms import DirtyEditForm 7 | from dirtyedit.utils import read_file, write_file 8 | from dirtyedit.conf import USE_REVERSION 9 | 10 | 11 | admin_class = admin.ModelAdmin 12 | if USE_REVERSION: 13 | from reversion.admin import VersionAdmin 14 | admin_class = VersionAdmin 15 | 16 | 17 | @admin.register(FileToEdit) 18 | class FileToEditAdmin(admin_class): 19 | form = DirtyEditForm 20 | fieldsets = ( 21 | (None, { 22 | 'fields': ('content',) 23 | }), 24 | (None, { 25 | 'fields': ('relative_path',) 26 | }), 27 | ) 28 | 29 | def save_model(self, request, obj, form, change): 30 | #~ record editor 31 | if getattr(obj, 'editor', None) is None: 32 | obj.editor = request.user 33 | status, msg = write_file(obj.relative_path, obj.content) 34 | if msg != '': 35 | if status == 'warn': 36 | messages.warning(request, msg) 37 | elif status == 'infos': 38 | messages.info(request, msg) 39 | else: 40 | messages.error(request, msg) 41 | return super(FileToEditAdmin, self).save_model( 42 | request, obj, form, change) 43 | 44 | def get_changeform_initial_data(self, request): 45 | if 'fpath' in request.GET.keys(): 46 | filepath = request.GET.get('fpath') 47 | status_msg, msg, filecontent = read_file(filepath) 48 | if status_msg is True: 49 | messages.success(request, msg) 50 | return {'content': filecontent, 'relative_path': filepath} 51 | else: 52 | if status_msg == 'warn': 53 | messages.warning(request, msg) 54 | return 55 | elif status_msg == 'infos': 56 | messages.info(request, msg) 57 | return {'relative_path': filepath} 58 | messages.error(request, msg) 59 | return 60 | return 61 | -------------------------------------------------------------------------------- /dirtyedit/apps.py: -------------------------------------------------------------------------------- 1 | from django.utils.translation import ugettext_lazy as _ 2 | from django.apps import AppConfig 3 | 4 | 5 | class DirtyEditConfig(AppConfig): 6 | name = "dirtyedit" 7 | verbose_name = _(u"Files editor") 8 | 9 | def ready(self): 10 | pass 11 | -------------------------------------------------------------------------------- /dirtyedit/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.conf import settings 4 | 5 | 6 | edit_modes = ('html','code') 7 | 8 | authorized_paths = ('/media', '/static', '/templates') 9 | 10 | EDIT_MODE = getattr(settings, 'DIRTYEDIT_EDIT_MODE', edit_modes[1]) 11 | CODEMIRROR_KEYMAP = getattr(settings, 'DIRTYEDIT_CODEMIRROR_KEYMAP', 'default') 12 | 13 | USE_REVERSION = getattr(settings, 'DIRTYEDIT_USE_REVERSION', True) 14 | 15 | AUTHORIZED_PATHS = getattr(settings, 'DIRTYEDIT_AUTHORIZED_PATHS', authorized_paths) 16 | EXCLUDED_PATHS = getattr(settings, 'DIRTYEDIT_EXCLUDED_PATHS', ()) 17 | 18 | CAN_CREATE_FILES = getattr(settings, 'DIRTYEDIT_CAN_CREATE_FILES', False) -------------------------------------------------------------------------------- /dirtyedit/forms.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django import forms 3 | from codemirror2.widgets import CodeMirrorEditor 4 | from dirtyedit.models import FileToEdit 5 | from dirtyedit.conf import CODEMIRROR_KEYMAP, EDIT_MODE 6 | 7 | if EDIT_MODE == 'html': 8 | from ckeditor_uploader.widgets import CKEditorUploadingWidget 9 | 10 | 11 | class DirtyEditForm(forms.ModelForm): 12 | 13 | def __init__(self, *args, **kwargs): 14 | super(DirtyEditForm, self).__init__(*args, **kwargs) 15 | 16 | if EDIT_MODE == 'html': 17 | content = forms.CharField(widget=CKEditorUploadingWidget()) 18 | elif EDIT_MODE == 'code': 19 | content = forms.CharField( 20 | widget=CodeMirrorEditor(options={ 21 | 'mode': 'htmlmixed', 22 | 'width': '1170px', 23 | 'indentWithTabs': 'true', 24 | 'indentUnit': '4', 25 | 'lineNumbers': 'true', 26 | 'autofocus': 'true', 27 | #'highlightSelectionMatches': '{showToken: /\w/, annotateScrollbar: true}', 28 | 'styleActiveLine': 'true', 29 | 'autoCloseTags': 'true', 30 | 'keyMap': CODEMIRROR_KEYMAP, 31 | 'theme': 'blackboard', 32 | }, 33 | modes=['css', 'xml', 'javascript', 'htmlmixed'], 34 | ) 35 | 36 | ) 37 | else: 38 | content = forms.CharField(widget=forms.Textarea) 39 | content.required = False 40 | 41 | class Meta: 42 | model = FileToEdit 43 | exclude = ('created', 'edited', 'editor') 44 | -------------------------------------------------------------------------------- /dirtyedit/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synw/django-dirtyedit/29b23222471ac07a9c398a9cb4a038189d14624c/dirtyedit/management/__init__.py -------------------------------------------------------------------------------- /dirtyedit/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synw/django-dirtyedit/29b23222471ac07a9c398a9cb4a038189d14624c/dirtyedit/management/commands/__init__.py -------------------------------------------------------------------------------- /dirtyedit/management/commands/populate_editor.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import os 3 | from django.core.management.base import BaseCommand 4 | from ...utils import read_file 5 | from ...msgs import Msgs 6 | from ...models import FileToEdit 7 | 8 | 9 | class Command(BaseCommand, Msgs): 10 | help = 'Populate file instances from a directory for dirtyedit' 11 | 12 | def add_arguments(self, parser): 13 | parser.add_argument('source', type=str) 14 | 15 | def handle(self, *args, **options): 16 | source = options["source"] 17 | # get the files 18 | self.status("Getting files from " + source) 19 | filenames = self.get_filenames(source) 20 | # save instances 21 | for filename in filenames: 22 | path = source + filename 23 | status, msg, filecontent = read_file(path, True) 24 | if status is True: 25 | self.status(msg) 26 | else: 27 | if status == 'warn': 28 | self.error(msg) 29 | elif status == 'infos': 30 | self.info(msg) 31 | self.error(msg) 32 | # save instance 33 | _, created = FileToEdit.objects.get_or_create( 34 | relative_path=path, content=filecontent) 35 | if created is False: 36 | msg = "File " + filename + " already exists in the database" 37 | self.error(msg) 38 | self.ok("Done") 39 | 40 | def get_filenames(self, startpath): 41 | dirfiles = [] 42 | for _, _, files in os.walk(startpath): 43 | for filename in files: 44 | print(filename) 45 | dirfiles.append(filename) 46 | return dirfiles 47 | -------------------------------------------------------------------------------- /dirtyedit/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.6 on 2017-04-03 14:08 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [ 15 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='FileToEdit', 21 | fields=[ 22 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 23 | ('edited', models.DateTimeField(auto_now=True, verbose_name='Edited')), 24 | ('created', models.DateTimeField(auto_now_add=True)), 25 | ('relative_path', models.CharField(max_length=255, null=True, unique=True, verbose_name='File path')), 26 | ('content', models.TextField(blank=True, null=True)), 27 | ('editor', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Edited by')), 28 | ], 29 | options={ 30 | 'verbose_name': 'File to edit', 31 | 'verbose_name_plural': 'Files to edit', 32 | }, 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /dirtyedit/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synw/django-dirtyedit/29b23222471ac07a9c398a9cb4a038189d14624c/dirtyedit/migrations/__init__.py -------------------------------------------------------------------------------- /dirtyedit/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.conf import settings 4 | from django.db import models 5 | from django.utils.translation import ugettext_lazy as _ 6 | 7 | 8 | class FileToEdit(models.Model): 9 | edited = models.DateTimeField(editable=False, auto_now=True, verbose_name=_(u'Edited')) 10 | created = models.DateTimeField(editable=False, auto_now_add=True) 11 | editor = models.ForeignKey(settings.AUTH_USER_MODEL, editable=False, related_name='+', null=True, on_delete=models.SET_NULL, verbose_name=u'Edited by') 12 | relative_path = models.CharField(max_length=255, null=True, unique=True, verbose_name=_(u"File path")) 13 | # to select the mode in codemirror: must figure out how to access this field value in admin.ModelAdmin first 14 | #file_type = models.CharField(max_length=60, blank=True, help_text=_(u'See here for a list: http://codemirror.net/mode/')) 15 | content = models.TextField(null=True, blank=True) 16 | 17 | 18 | class Meta: 19 | verbose_name=_(u'File to edit') 20 | verbose_name_plural = _(u'Files to edit') 21 | 22 | def __str__(self): 23 | return str(self.relative_path) 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /dirtyedit/msgs.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | 3 | 4 | class Msgs(): 5 | """ 6 | A class to handle application messages 7 | """ 8 | 9 | def ok(self, msg): 10 | """ 11 | Prints an error message 12 | """ 13 | err = "[" + self.style.SUCCESS("Ok") + "] " + msg 14 | self.stdout.write(err) 15 | 16 | def error(self, msg): 17 | """ 18 | Prints an error message 19 | """ 20 | err = "[" + self.style.ERROR("Error") + "] " + msg 21 | self.stdout.write(err) 22 | 23 | def status(self, msg): 24 | """ 25 | Prints an info message 26 | """ 27 | err = "[" + self.style.HTTP_NOT_FOUND("Status") + "] " + msg 28 | self.stdout.write(err) 29 | 30 | def info(self, msg): 31 | """ 32 | Prints an info message 33 | """ 34 | err = "[" + self.style.HTTP_NOT_MODIFIED("Info") + "] " + msg 35 | self.stdout.write(err) 36 | -------------------------------------------------------------------------------- /dirtyedit/templates/admin/dirtyedit/change_form.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n admin_urls static admin_modify %} 3 | 4 | {% block extrahead %}{{ block.super }} 5 | 6 | {{ media }} 7 | {% endblock %} 8 | 9 | {% block extrastyle %}{{ block.super }}{% endblock %} 10 | 11 | {% block coltype %}colM{% endblock %} 12 | 13 | {% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} change-form{% endblock %} 14 | 15 | {% block messages %} 16 | {% if messages %} 17 | 20 | {% endif %} 21 | {% endblock messages %} 22 | 23 | {% if not is_popup %} 24 | {% block breadcrumbs %} 25 | 31 | {% endblock %} 32 | {% endif %} 33 | 34 | {% block content %}
35 | {% block object-tools %} 36 | {% if change %}{% if not is_popup %} 37 | 46 | {% endif %}{% endif %} 47 | {% endblock %} 48 |
{% csrf_token %}{% block form_top %}{% endblock %} 49 | 66 | Fullscreen 67 |
68 | {% if is_popup %}{% endif %} 69 | {% if to_field %}{% endif %} 70 | {% if save_on_top %}{% block submit_buttons_top %}{% submit_row %}{% endblock %}{% endif %} 71 | {% if errors %} 72 |

73 | {% if errors|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %} 74 |

75 | {{ adminform.form.non_field_errors }} 76 | {% endif %} 77 | 78 | {% block field_sets %} 79 | {% for fieldset in adminform %} 80 | {% include "admin/dirtyedit/includes/fieldset.html" %} 81 | {% endfor %} 82 | {% endblock %} 83 | 84 | {% block after_field_sets %}{% endblock %} 85 | 86 | {% block inline_field_sets %} 87 | {% for inline_admin_formset in inline_admin_formsets %} 88 | {% include inline_admin_formset.opts.template %} 89 | {% endfor %} 90 | {% endblock %} 91 | 92 | {% block after_related_objects %}{% endblock %} 93 | 94 | {% block submit_buttons_bottom %}{% submit_row %}{% endblock %} 95 | 96 | {% block admin_change_form_document_ready %} 97 | 104 | {% endblock %} 105 | 106 | {# JavaScript for prepopulated fields #} 107 | {% prepopulated_fields_js %} 108 | 109 |
110 |
111 | {% endblock %} 112 | -------------------------------------------------------------------------------- /dirtyedit/templates/admin/dirtyedit/change_list.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n admin_urls static admin_list %} 3 | 4 | {% block extrastyle %} 5 | {{ block.super }} 6 | 7 | {% if cl.formset %} 8 | 9 | {% endif %} 10 | {% if cl.formset or action_form %} 11 | 12 | {% endif %} 13 | {{ media.css }} 14 | {% if not actions_on_top and not actions_on_bottom %} 15 | 18 | {% endif %} 19 | {% endblock %} 20 | 21 | {% block extrahead %} 22 | {{ block.super }} 23 | {{ media.js }} 24 | {% endblock %} 25 | 26 | {% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} change-list{% endblock %} 27 | 28 | {% if not is_popup %} 29 | {% block breadcrumbs %} 30 | 35 | {% endblock %} 36 | {% endif %} 37 | 38 | {% block coltype %}flex{% endblock %} 39 | 40 | {% block content %} 41 |
42 | {% block object-tools %} 43 | 73 | {% endblock %} 74 | {% if cl.formset.errors %} 75 |

76 | {% if cl.formset.total_error_count == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %} 77 |

78 | {{ cl.formset.non_form_errors }} 79 | {% endif %} 80 |
81 | {% block search %}{% search_form cl %}{% endblock %} 82 | {% block date_hierarchy %}{% date_hierarchy cl %}{% endblock %} 83 | 84 | {% block filters %} 85 | {% if cl.has_filters %} 86 |
87 |

{% trans 'Filter' %}

88 | {% for spec in cl.filter_specs %}{% admin_list_filter cl spec %}{% endfor %} 89 |
90 | {% endif %} 91 | {% endblock %} 92 | 93 |
{% csrf_token %} 94 | {% if cl.formset %} 95 |
{{ cl.formset.management_form }}
96 | {% endif %} 97 | 98 | {% block result_list %} 99 | {% if action_form and actions_on_top and cl.show_admin_actions %}{% admin_actions %}{% endif %} 100 | {% result_list cl %} 101 | {% if action_form and actions_on_bottom and cl.show_admin_actions %}{% admin_actions %}{% endif %} 102 | {% endblock %} 103 | {% block pagination %}{% pagination cl %}{% endblock %} 104 |
105 |
106 |
107 | {% endblock %} 108 | -------------------------------------------------------------------------------- /dirtyedit/templates/admin/dirtyedit/includes/fieldset.html: -------------------------------------------------------------------------------- 1 |
2 | {% if fieldset.name %}

{{ fieldset.name }}

{% endif %} 3 | {% if fieldset.description %} 4 |
{{ fieldset.description|safe }}
5 | {% endif %} 6 | {% for line in fieldset %} 7 |
8 | {% if line.fields|length_is:'1' %}{{ line.errors }}{% endif %} 9 | {% for field in line %} 10 | 11 | {% if not line.fields|length_is:'1' and not field.is_readonly %}{{ field.errors }}{% endif %} 12 | {% if field.is_checkbox %} 13 | {{ field.field }}{{ field.label_tag }} 14 | {% else %} 15 | {% if not field.label_tag == '' %} 16 | {{ field.label_tag }} 17 | {% endif %} 18 | {% if field.is_readonly %} 19 |

{{ field.contents }}

20 | {% else %} 21 | {{ field.field }} 22 | {% endif %} 23 | {% endif %} 24 | {% if field.field.help_text %} 25 |

{{ field.field.help_text|safe }}

26 | {% endif %} 27 |
28 | {% endfor %} 29 | 30 | {% endfor %} 31 |
-------------------------------------------------------------------------------- /dirtyedit/templates/codemirror2/codemirror_script.html: -------------------------------------------------------------------------------- 1 | {% load staticfiles %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /dirtyedit/templates/reversion/change_list.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/dirtyedit/change_list.html" %} 2 | {# dirty trick to print out the add file form: check dirtyedit/change_list.html #} 3 | {% load i18n admin_urls %} 4 | 5 | 6 | {% block object-tools-items %} 7 | {% if not is_popup and has_add_permission and has_change_permission %} 8 |
  • {% blocktrans with cl.opts.verbose_name_plural|escape as name %}Recover deleted {{name}}{% endblocktrans %}
  • 9 | {% endif %} 10 | {{block.super}} 11 | {% endblock %} -------------------------------------------------------------------------------- /dirtyedit/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /dirtyedit/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | from django.conf import settings 5 | from django.utils._os import safe_join 6 | from django.utils.translation import ugettext as _ 7 | from dirtyedit.conf import AUTHORIZED_PATHS, EXCLUDED_PATHS, CAN_CREATE_FILES 8 | 9 | filepath_form = """ 10 | 15 |
    16 |
    17 |
    18 | 19 | 20 | 21 | 22 |
    23 |
    24 |
    25 | """ 26 | 27 | 28 | def check_file(relative_path, edit_mode=False, dir_only=False): 29 | """ 30 | Checks the if the file is editable 31 | """ 32 | ok, msg = check_path(relative_path, edit_mode, dir_only) 33 | if ok is False: 34 | return False, msg 35 | return True, '' 36 | 37 | 38 | def check_path(relative_path, edit_mode=False, dir_only=False): 39 | """ 40 | Does some security checks on filepath 41 | """ 42 | # check for empty path 43 | if relative_path == '': 44 | msg = filepath_form + \ 45 | _(u"
    Please provide a file path
    ") 46 | return False, msg 47 | # check for root path 48 | if relative_path == '/': 49 | msg = filepath_form + _(u'
    What?
    ') 50 | return False, msg 51 | # check for filename 52 | if relative_path.endswith('/') and dir_only is False: 53 | print("jjjjjjjjjjj", dir_only) 54 | msg = filepath_form + \ 55 | _(u"
    Path '%s' is invalid: " 56 | "please provide a filename
    ") % (relative_path,) 57 | return False, msg 58 | pathlist = relative_path.split('/') 59 | folderpath = '/'.join(pathlist[:len(pathlist) - 1]) 60 | # check for excluded paths 61 | for fpath in EXCLUDED_PATHS: 62 | if relative_path.startswith(fpath): 63 | msg = filepath_form + \ 64 | _(u"
    You can not edit files in the directory " 65 | "'%s'
    ") % (folderpath,) 66 | return (False, msg) 67 | # check vs authorized paths 68 | is_authorized = False 69 | for authorized_path in AUTHORIZED_PATHS: 70 | #~ check if the path is part of the authorized path 71 | if folderpath.startswith(authorized_path): 72 | # '+folderpath 73 | is_authorized = True 74 | break 75 | if is_authorized is False: 76 | msg = filepath_form + \ 77 | _(u"
    You can not edit files in the directory " 78 | "'%s'
    ") % (folderpath,) 79 | return (False, msg) 80 | # verify that the directory exists and is under project root 81 | absolute_folderpath = safe_join(settings.BASE_DIR, folderpath) 82 | if not os.path.isdir(absolute_folderpath): 83 | msg = filepath_form + \ 84 | _(u"
    The directory %s'" 85 | " does not exist
    ") % (folderpath,) 86 | return (False, msg) 87 | # check if file exists 88 | if dir_only is False: 89 | filepath = safe_join(settings.BASE_DIR, relative_path) 90 | if not os.path.isfile(filepath): 91 | # msgs 92 | if CAN_CREATE_FILES is True: 93 | if not edit_mode is True: 94 | msg = _( 95 | u"
    A new file will be created at " 96 | "'%s'
    ") % (relative_path,) 97 | return ('infos', msg) 98 | else: 99 | if not edit_mode is True: 100 | msg = filepath_form + \ 101 | _(u"
    File '%s' " 102 | "not found
    ") % (relative_path,) 103 | else: 104 | msg = filepath_form + \ 105 | _(u"
    You can not create files
    ") 106 | return (False, msg) 107 | # ok 108 | return True, '' 109 | 110 | 111 | def read_file(relative_path, dir_only=False): 112 | status, msg = check_file(relative_path, False, dir_only) 113 | if status in [False, 'warn', 'infos']: 114 | return (status, msg, None) 115 | # read file 116 | filepath = safe_join(settings.BASE_DIR, relative_path) 117 | filex = open(filepath, "r") 118 | filecontent = filex.read() 119 | msg = _(u"File " + filepath + " found: data populated") 120 | return (True, msg, filecontent) 121 | 122 | 123 | def write_file(relative_path, content): 124 | status, msg = check_file(relative_path, edit_mode=True) 125 | if status in [False, 'warn', 'infos']: 126 | return (status, msg) 127 | else: 128 | filepath = safe_join(settings.BASE_DIR, relative_path) 129 | #~ write the file 130 | filex = open(filepath, "w") 131 | filex.write(content) 132 | filex.close() 133 | return (True, msg) 134 | -------------------------------------------------------------------------------- /docs/img/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synw/django-dirtyedit/29b23222471ac07a9c398a9cb4a038189d14624c/docs/img/screenshot1.png -------------------------------------------------------------------------------- /docs/img/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synw/django-dirtyedit/29b23222471ac07a9c398a9cb4a038189d14624c/docs/img/screenshot2.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django-ckeditor 2 | django-codemirror2 3 | django-reversion -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | 4 | version = __import__('dirtyedit').__version__ 5 | 6 | setup( 7 | name = 'django-dirtyedit', 8 | packages=find_packages(), 9 | include_package_data=True, 10 | version = version, 11 | description = ' Django application to edit files from the admin interface', 12 | author = 'synw', 13 | author_email = 'synwe@yahoo.com', 14 | url = 'https://github.com/synw/django-dirtyedit', 15 | download_url = 'https://github.com/synw/django-dirtyedit/releases/tag/'+version, 16 | keywords = ['django', 'editor'], 17 | classifiers = [ 18 | 'Development Status :: 3 - Alpha', 19 | 'Framework :: Django :: 1.8', 20 | 'Intended Audience :: Developers', 21 | 'License :: OSI Approved :: MIT License', 22 | 'Programming Language :: Python :: 2.7', 23 | ], 24 | install_requires=[ 25 | 'django-ckeditor', 26 | 'django-codemirror2', 27 | 'django-reversion' 28 | ], 29 | zip_safe=False 30 | ) 31 | --------------------------------------------------------------------------------