├── .gitignore ├── INSTALL.rst ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── docs └── overview.rst ├── mobileadmin ├── __init__.py ├── conf │ ├── __init__.py │ └── settings.py ├── context_processors.py ├── decorators.py ├── media │ ├── css │ │ └── mobile_safari.css │ ├── img │ │ ├── arrow.png │ │ ├── default-bg-reverse.gif │ │ ├── default-bg.gif │ │ ├── nav-bg-reverse.gif │ │ ├── nav-bg.gif │ │ └── search.png │ └── js │ │ ├── mobile_safari.js │ │ └── urlify.js ├── models.py ├── options.py ├── sites.py ├── templates │ └── mobileadmin │ │ └── mobile_safari │ │ ├── 404.html │ │ ├── 505.html │ │ ├── app_index.html │ │ ├── auth │ │ └── user │ │ │ └── add_form.html │ │ ├── base.html │ │ ├── change_form.html │ │ ├── change_list.html │ │ ├── delete_confirmation.html │ │ ├── edit_inline │ │ ├── stacked.html │ │ └── tabular.html │ │ ├── filter.html │ │ ├── includes │ │ └── fieldset.html │ │ ├── index.html │ │ ├── invalid_setup.html │ │ ├── login.html │ │ ├── object_history.html │ │ ├── pagination.html │ │ ├── registration │ │ ├── logged_out.html │ │ ├── password_change_done.html │ │ └── password_change_form.html │ │ └── search_form.html ├── templatetags │ ├── __init__.py │ ├── mobile_admin_list.py │ ├── mobile_admin_media.py │ └── mobile_admin_modify.py ├── urls.py ├── utils.py └── views.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .DS_Store 3 | dist/* 4 | MANIFEST -------------------------------------------------------------------------------- /INSTALL.rst: -------------------------------------------------------------------------------- 1 | To install django-mobileadmin, run the following command inside this directory: 2 | 3 | python setup.py install 4 | 5 | Or if you'd prefer you can simply place the included ``mobileadmin`` 6 | directory somewhere on your Python path, or symlink to it from 7 | somewhere on your Python path; this is useful if you're working from a 8 | Subversion checkout. 9 | 10 | Please see the `Installation` paragraph in docs/overview.txt for usage details. 11 | 12 | Note that this application requires Python 2.3 or later, and a recent 13 | Subversion checkout of Django. You can obtain Python from 14 | http://www.python.org/ and Django from http://www.djangoproject.com/. 15 | 16 | This install notice was bluntly stolen from James Bennett's registration 17 | package, http://code.google.com/p/django-registration/ -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | django-mobileadmin 2 | ================== 3 | 4 | Copyright (c) 2007, Jannis Leidel 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are 9 | met: 10 | 11 | * Redistributions of source code must retain the above copyright 12 | notice, this list of conditions and the following disclaimer. 13 | * Redistributions in binary form must reproduce the above 14 | copyright notice, this list of conditions and the following 15 | disclaimer in the documentation and/or other materials provided 16 | with the distribution. 17 | * Neither the name of the author nor the names of other 18 | contributors may be used to endorse or promote products derived 19 | from this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 24 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 25 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 27 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 29 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | 33 | iUI 34 | === 35 | 36 | Copyright (c) 2007, iUI Project Members 37 | 38 | All rights reserved. 39 | 40 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are 41 | met: 42 | 43 | * Redistributions of source code must retain the above copyright 44 | notice, this list of conditions and the following disclaimer. 45 | * Redistributions in binary form must reproduce the above 46 | copyright notice, this list of conditions and the following 47 | disclaimer in the documentation and/or other materials provided 48 | with the distribution. 49 | * Neither the name of the iUI Project nor the names of its 50 | contributors may be used to endorse or promote products derived 51 | from this software without specific prior written permission. 52 | 53 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 54 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 55 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 56 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 57 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 58 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 59 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 60 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 61 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 62 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 63 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 64 | 65 | Django 66 | ====== 67 | 68 | Copyright (c) 2005, the Lawrence Journal-World 69 | All rights reserved. 70 | 71 | Redistribution and use in source and binary forms, with or without modification, 72 | are permitted provided that the following conditions are met: 73 | 74 | 1. Redistributions of source code must retain the above copyright notice, 75 | this list of conditions and the following disclaimer. 76 | 77 | 2. Redistributions in binary form must reproduce the above copyright 78 | notice, this list of conditions and the following disclaimer in the 79 | documentation and/or other materials provided with the distribution. 80 | 81 | 3. Neither the name of Django nor the names of its contributors may be used 82 | to endorse or promote products derived from this software without 83 | specific prior written permission. 84 | 85 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 86 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 87 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 88 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 89 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 90 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 91 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 92 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 93 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 94 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include INSTALL.rst 2 | include LICENSE.txt 3 | include MANIFEST.in 4 | include README.rst 5 | recursive-include docs * 6 | recursive-include mobileadmin/templates * 7 | recursive-include mobileadmin/media * 8 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =================== 2 | Django Mobile Admin 3 | =================== 4 | 5 | This is a drop-in adminstration application for use with Django and a 6 | series of mobile devices and platforms. It requires a recent Django SVN 7 | checkout or Django 1.0. 8 | 9 | For more installation instructions please have a look at INSTALL.rst. 10 | 11 | Cheers, 12 | Jannis/jezdez 13 | -------------------------------------------------------------------------------- /docs/overview.rst: -------------------------------------------------------------------------------- 1 | ============================================= 2 | The Django admin interface for mobile devices 3 | ============================================= 4 | 5 | .. contents:: 6 | :backlinks: none 7 | 8 | This is an alternative admin interface for Django for use with mobile devices 9 | such as the iPhone/iPod touch or Blackberry. Some would call it a theme or a 10 | skin, but actually it's more than that. 11 | 12 | It resembles almost all features of the regular Django admin interface and 13 | brings everything you need to add support for arbitrary devices. 14 | 15 | I hope you like it. 16 | 17 | Download & Installation 18 | ======================= 19 | 20 | Get the source from the application site at:: 21 | 22 | http://code.google.com/p/django-mobileadmin/ 23 | 24 | To install the *mobileadmin*, follow these steps: 25 | 26 | 1. Follow the instructions in the INSTALL.txt file 27 | 2. Add ``'mobileadmin'`` to the INSTALLED_APPS_ setting of your Django site. 28 | That might look like this:: 29 | 30 | INSTALLED_APPS = ( 31 | # ... 32 | 'mobileadmin', 33 | ) 34 | 35 | 3. Make sure you've installed_ the admin contrib app. 36 | 4. Add ``'mobileadmin.context_processors.user_agent'`` to your 37 | TEMPLATE_CONTEXT_PROCESSORS_ setting. It should look like this:: 38 | 39 | TEMPLATE_CONTEXT_PROCESSORS = ( 40 | 'django.core.context_processors.auth', 41 | 'django.core.context_processors.debug', 42 | 'django.core.context_processors.i18n', 43 | 'django.core.context_processors.media', 44 | 'django.core.context_processors.request', 45 | 'mobileadmin.context_processors.user_agent', 46 | ) 47 | 48 | Usage 49 | ===== 50 | 51 | Since *mobileadmin* follows the ideas of Django's admin interface you can 52 | easily reuse your existing admin setup: 53 | 54 | You use the default or custom ModelAdmin classes for each model you wanted 55 | to be editable in the admin interface and hooked up Django's default 56 | AdminSite instance with your URLconf. 57 | 58 | If that's the case you are able to re-use those ModelAdmin (sub-)classes 59 | by using *mobileadmin*'s separate admin site instance and its autoregister() 60 | function. 61 | 62 | 1. In your root URLconf -- just **below** the line where Django's 63 | admin.autodiscover() gets called -- import ``mobileadmin`` and call the 64 | function ``mobileadmin.autoregister()``:: 65 | 66 | # urls.py 67 | from django.conf.urls.defaults import * 68 | from django.contrib import admin 69 | 70 | admin.autodiscover() 71 | 72 | import mobileadmin 73 | mobileadmin.autoregister() 74 | 75 | urlpatterns = patterns('', 76 | ('^admin/(.*)', admin.site.root), 77 | ) 78 | 79 | This registers your existing admin configuration with *mobileadmin*. 80 | 81 | 2. Extend the urlpatterns to hook the default ``MobileAdminSite`` instance` 82 | with your favorite URL, e.g. ``/ma/``. Add:: 83 | 84 | urlpatterns += patterns('', 85 | (r'^ma/(.*)', mobileadmin.sites.site.root), 86 | ) 87 | 88 | *mobileadmin* is now replicating all of the regular admin features and 89 | uses templates adapted to the mobile device you are using. 90 | 91 | 3. Set the ``handler404`` and ``handler500`` variables in your URLConf to the 92 | views that are provided by *mobileadmin*:: 93 | 94 | handler404 = 'mobileadmin.views.page_not_found' 95 | handler500 = 'mobileadmin.views.server_error' 96 | 97 | That is needed to load the ``404.html`` and ``500.html`` templates 98 | according to the user agent of the browser on your mobile device. It 99 | has an automatic fallback to `Django's default error handlers`_. 100 | 101 | .. _INSTALLED_APPS: http://docs.djangoproject.com/en/dev/ref/settings/#installed-apps 102 | .. _ADMIN_MEDIA_PREFIX: http://docs.djangoproject.com/en/dev/ref/settings/#admin-media-prefix 103 | .. _TEMPLATE_CONTEXT_PROCESSORS: http://docs.djangoproject.com/en/dev/ref/settings/#template-context-processors 104 | .. _installed: http://docs.djangoproject.com/en/dev/ref/contrib/admin/#overview 105 | .. _Django's default error handlers: http://docs.djangoproject.com/en/dev/topics/http/views/#customizing-error-views 106 | 107 | Extending ``mobileadmin`` 108 | ========================= 109 | 110 | *mobileadmin* comes with a default set of templates and patterns to 111 | recognize different devices and platforms: 112 | 113 | - Mobile Safari (iPhone/iPod touch) 114 | 115 | But it's made for being extended. 116 | 117 | Since the template loading depends on the user agent of the requesting client, 118 | *mobileadmin* tells Django to look for three templates, when trying to render 119 | one of the default admin views: 120 | 121 | 1. ``mobileadmin/USER_AGENT_NAME/VIEW_TEMPLATE.html`` 122 | 2. ``mobileadmin/VIEW_TEMPLATE.html`` 123 | 3. ``admin/index.html`` 124 | 125 | ..where: 126 | 127 | - ``USER_AGENT`` is the short name of the user agent 128 | - ``VIEW_TEMPLATE`` is the name of the rendered template 129 | 130 | If you would try to access the login view with the iPhone for example, the 131 | following three templates would be tried to load: 132 | 133 | 1. ``mobileadmin/mobile_safari/login.html`` 134 | 2. ``mobileadmin/login.html`` 135 | 3. ``admin/index.html`` 136 | 137 | ..where ``mobile_safari`` is the name of one of the default device patterns 138 | and ``login.html`` the name of the to needed template. 139 | 140 | Creating a custom ``mobileadmin`` setup 141 | --------------------------------------- 142 | 143 | You can add support for more user agents by adding ``MOBILEADMIN_USER_AGENTS`` 144 | to your settings.py file -- consisting of a short name and a regualar 145 | expression, matching that user agent string:: 146 | 147 | MOBILEADMIN_USER_AGENTS = { 148 | 'my_user_agent': r'.*MyUserAgent.*', 149 | } 150 | 151 | With that it would automatically check if the regular expression matches with 152 | the user agent of the current request and -- if yes -- try to load the 153 | templates ``mobileadmin/my_user_agent/login.html``, when accessing the the 154 | login page -- falling back to ``my_user_agent/login.html`` and later to 155 | ``admin/login.html``, if not found. 156 | 157 | Have a look at ``TEMPLATE_MAPPING`` in ``mobileadmin/conf/settings.py`` 158 | if you want to know the default regular expressions. 159 | 160 | *mobileadmin* comes with a ``MobileAdminSite`` and a ``MobileModelAdmin`` 161 | class that uses the default ``TEMPLATE_MAPPING`` and ``USER_AGENTS`` 162 | settings out of the box:: 163 | 164 | from mobileadmin import sites 165 | 166 | class MyMobileAdminSite(sites.MobileAdminSite): 167 | # define here whatever function you want 168 | pass 169 | 170 | But if you want to use the ability of *mobileadmin* to change the template 171 | depending on the user agent, you need to modify a bit of your admin classes. 172 | 173 | Luckily *mobileadmin* comes with a decorator to be used on ``AdminSite`` or 174 | ``ModelAdmin`` methods that changes the template of that method according to 175 | the current user agent by using a template mapping, which can be found in 176 | ``mobileadmin/conf/settings.py`` in the ``TEMPLATE_MAPPING`` variable. 177 | 178 | Those mappings are used by the decorator ``mobile_templates`` that applies 179 | them on the corresponding methods of your own ``AdminSite`` or 180 | ``ModelAdmin``, e.g.:: 181 | 182 | from django.contrib.admin import sites 183 | from mobileadmin.decorators import mobile_templates 184 | 185 | class MyAdminSite(sites.AdminSite): 186 | 187 | def index(self, request, extra_context=None): 188 | 189 | # self.index_template is already automatically set here 190 | # do something cool here 191 | 192 | return super(MyAdminSite, self).index(request, extra_context) 193 | index = mobile_templates(index) 194 | 195 | Furthermore the default mappings can be extended in your site settings.py:: 196 | 197 | MOBILEADMIN_TEMPLATE_MAPPING = { 198 | 'index': ('index_template', 'index.html'), 199 | } 200 | 201 | ..where: 202 | 203 | - ``index`` is the name of the function, whose class attribute and 204 | - ``index_template`` (an attribute of the method's class) would be set to the 205 | the file ``index.html``. 206 | 207 | Using custom mobile admin interfaces 208 | ------------------------------------ 209 | 210 | In case you created your own mobile admin interface, you can use 211 | *mobileadmin*'s subclasses of Django's `ModelAdmin`_, `InlineModelAdmin`_ 212 | and `AdminSite`_ classes, that include the neccesary bits to make it work. 213 | 214 | Just use it as you would use the base classes, e.g.:: 215 | 216 | from mobileadmin import options 217 | from myproject.myapp.models import Author 218 | 219 | class MobileAuthorAdmin(options.MobileModelAdmin): 220 | pass 221 | mobileadmin.sites.site.register(Author, MobileAuthorAdmin) 222 | 223 | Then import ``mobileadmin`` in your URLconf to instantiate a 224 | ``MobileAdminSite`` object, use Django's ``autodiscover()`` to load 225 | ``INSTALLED_APPS`` admin.py modules and add an URL for the *mobileadmin* to 226 | the URLConf:: 227 | 228 | # urls.py 229 | from django.conf.urls.defaults import * 230 | from django.contrib import admin 231 | import mobileadmin 232 | 233 | admin.autodiscover() 234 | 235 | urlpatterns = patterns('', 236 | ('^admin/(.*)', admin.site.root), 237 | (r'^ma/(.*)', mobileadmin.sites.site.root), 238 | ) 239 | 240 | .. _InlineModelAdmin: http://docs.djangoproject.com/en/dev/ref/contrib/admin/#inlinemodeladmin-objects 241 | .. _AdminSite: http://docs.djangoproject.com/en/dev/ref/contrib/admin/#adminsite-objects 242 | .. _ModelAdmin: http://docs.djangoproject.com/en/dev/ref/contrib/admin/#modeladmin-objects 243 | 244 | Media path 245 | ========== 246 | 247 | Please feel free to use some nice little helpers to find the path to 248 | *mobileadmin*'s media directory. If you are using Django (or any other Python 249 | software) to serve static files (which you shouldn't in production) just use 250 | for example:: 251 | 252 | from mobileadmin.conf import settings 253 | 254 | mobileadmin_media_path = settings.MEDIA_PATH 255 | mobileadmin_media_prefix = settings.MEDIA_PREFIX 256 | 257 | You now have the full (platform-independent) path to the media directory of 258 | *mobileadmin* in the variable ``mobileadmin_media_path`` and the default URL 259 | prefix (``'/mobileadmin_media/'``) for the *mobileadmin* media -- CSS, Javascript 260 | and images -- in ``mobileadmin_media_prefix``. Just like the 261 | ADMIN_MEDIA_PREFIX_ but for the ``media`` directory of the *mobileadmin* app. 262 | 263 | You can of course optionally override the default URL prefix by setting 264 | a ``MOBILEADMIN_MEDIA_PREFIX`` in the settings.py file of your Django site. 265 | Please use a trailing slash. This makes updating *mobileadmin* much easier for 266 | you, since you now don't have to bother about different locations of the media 267 | directory. 268 | 269 | Serving *mobileadmin*'s static media 270 | ------------------------------------ 271 | 272 | Even though using Django's ability to serve static files is strongly **NOT 273 | RECOMMENDED** for live production servers, it might be helpful to bring up 274 | *mobileadmin* for a test drive or an intranet website. Just add the following 275 | code to the URLConf (``urls.py``) of your Django site:: 276 | 277 | from mobileadmin.conf import settings 278 | 279 | urlpatterns += patterns('django.views.static', 280 | (settings.MEDIA_REGEX, 'serve', {'document_root': settings.MEDIA_PATH}), 281 | ) 282 | 283 | See how *mobileadmin*'s own settings module is loaded at the top of the snippet 284 | that enables you to obtain a ready-made regex for the static files 285 | (``MEDIA_REGEX``) and the platform-independent filesystem path to the media 286 | files (``MEDIA_PATH``). 287 | 288 | Support 289 | ======= 290 | 291 | Please leave your `questions and problems`_ on the `designated Google Code site`_. 292 | 293 | .. _designated Google Code site: http://code.google.com/p/django-mobileadmin/ 294 | .. _questions and problems: http://code.google.com/p/django-mobileadmin/issues/ 295 | -------------------------------------------------------------------------------- /mobileadmin/__init__.py: -------------------------------------------------------------------------------- 1 | from django.contrib.admin import site as main_site 2 | from django.core.exceptions import ImproperlyConfigured 3 | 4 | from mobileadmin import decorators, views 5 | from mobileadmin.conf import settings 6 | 7 | ### From http://www2.lib.uchicago.edu/keith/courses/python/class/5/#attrref 8 | def classlookup(C, name): 9 | if C.__dict__.has_key(name): 10 | return (1, C.__dict__[name]) 11 | else: 12 | for b in C.__bases__: 13 | success, value = classlookup(b, name) 14 | if success: 15 | return (1, value) 16 | else: 17 | pass 18 | else: 19 | return (0, None) 20 | 21 | 22 | def autoregister(): 23 | """ 24 | Auto-register all ModelAdmin instances of the default AdminSite with the 25 | mobileadmin app and set the templates accordingly. 26 | """ 27 | from django.contrib.auth.admin import UserAdmin 28 | from mobileadmin.sites import site 29 | 30 | for model, modeladmin in main_site._registry.iteritems(): 31 | admin_class = modeladmin.__class__ 32 | for name in settings.TEMPLATE_MAPPING: 33 | (found, value) = classlookup(admin_class, name) 34 | if found: 35 | setattr(admin_class, name, decorators.mobile_templates(value)) 36 | 37 | if admin_class == UserAdmin: 38 | setattr(admin_class, 'add_view', views.auth_add_view) 39 | 40 | site.register(model, admin_class) 41 | 42 | def autodiscover(): 43 | raise ImproperlyConfigured("Please use the autodiscover function of " 44 | "Django's default admin app and then " 45 | "call 'mobileadmin.autoregister' to use " 46 | "mobileadmin.") 47 | -------------------------------------------------------------------------------- /mobileadmin/conf/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jezdez/django-mobileadmin/4d5874916ecca91610b9fbdf3c50b34c2567b83e/mobileadmin/conf/__init__.py -------------------------------------------------------------------------------- /mobileadmin/conf/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | from django.conf import settings 3 | 4 | # PLEASE: Don't change anything here, use your site settings.py 5 | 6 | MEDIA_PATH = os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', 'media') 7 | MEDIA_PREFIX = getattr(settings, 'MOBILEADMIN_MEDIA_PREFIX', '/mobileadmin_media/') 8 | MEDIA_REGEX = r'^%s(?P.*)$' % MEDIA_PREFIX.lstrip('/') 9 | 10 | USER_AGENTS = { 11 | 'mobile_safari': r'AppleWebKit/.*Mobile/', 12 | 'blackberry': r'^BlackBerry', 13 | 'opera_mini': r'[Oo]pera [Mm]ini', 14 | } 15 | USER_AGENTS.update(getattr(settings, 'MOBILEADMIN_USER_AGENTS', {})) 16 | 17 | TEMPLATE_MAPPING = { 18 | 'index': ('index_template', 'index.html'), 19 | 'display_login_form': ('login_template', 'login.html'), 20 | 'app_index': ('app_index_template', 'app_index.html'), 21 | 'render_change_form': ('change_form_template', 'change_form.html'), 22 | 'changelist_view': ('change_list_template', 'change_list.html'), 23 | 'delete_view': ('delete_confirmation_template', 'delete_confirmation.html'), 24 | 'history_view': ('object_history_template', 'object_history.html'), 25 | 'logout': ('logout_template', 'registration/logged_out.html'), 26 | 'password_change': ('password_change_template', 'registration/password_change_form.html'), 27 | 'password_change_done': ('password_change_done_template', 'registration/password_change_done.html'), 28 | } 29 | TEMPLATE_MAPPING.update(getattr(settings, 'MOBILEADMIN_TEMPLATE_MAPPING', {})) 30 | -------------------------------------------------------------------------------- /mobileadmin/context_processors.py: -------------------------------------------------------------------------------- 1 | from mobileadmin.utils import get_user_agent 2 | 3 | def user_agent(request): 4 | return {'user_agent': get_user_agent(request)} 5 | -------------------------------------------------------------------------------- /mobileadmin/decorators.py: -------------------------------------------------------------------------------- 1 | from mobileadmin.conf import settings 2 | from mobileadmin.utils import get_user_agent 3 | 4 | try: 5 | from functools import wraps 6 | except ImportError: 7 | from django.utils.functional import wraps # Python 2.3, 2.4 fallback. 8 | 9 | def mobile_templates(function): 10 | """ 11 | Decorator to be used on ``AdminSite`` or ``ModelAdmin`` methods that 12 | changes the template of that method according to the current user agent 13 | by using a template mapping. 14 | """ 15 | func_name = function.__name__ 16 | 17 | def _change_templates(self, request, *args, **kwargs): 18 | if func_name in settings.TEMPLATE_MAPPING: 19 | path_list = [] 20 | attr_name, template_name = settings.TEMPLATE_MAPPING[func_name] 21 | user_agent = get_user_agent(request) 22 | params = dict(template_name=template_name) 23 | if user_agent: 24 | params.update(user_agent=user_agent) 25 | path_list += [ 26 | 'mobileadmin/%(user_agent)s/%(template_name)s', 27 | 'mobileadmin/%(template_name)s', 28 | ] 29 | # if self is a ModelAdmin instance add more of the default 30 | # templates as fallback 31 | if getattr(self, 'model', False): 32 | opts = self.model._meta 33 | params.update(dict(app_label=opts.app_label, 34 | object_name=opts.object_name.lower())) 35 | path_list = [ 36 | 'mobileadmin/%(user_agent)s/%(app_label)s/%(object_name)s/%(template_name)s', 37 | 'mobileadmin/%(user_agent)s/%(app_label)s/%(template_name)s', 38 | ] + path_list + [ 39 | 'admin/%(app_label)s/%(object_name)s/%(template_name)s', 40 | 'admin/%(app_label)s/%(template_name)s', 41 | ] 42 | path_list += [ 43 | 'admin/%(template_name)s', 44 | '%(template_name)s', 45 | ] 46 | else: 47 | path_list += [ 48 | 'admin/%(template_name)s', 49 | '%(template_name)s', 50 | ] 51 | setattr(self, attr_name, [path % params for path in path_list]) 52 | return function(self, request, *args, **kwargs) 53 | return wraps(function)(_change_templates) 54 | -------------------------------------------------------------------------------- /mobileadmin/media/css/mobile_safari.css: -------------------------------------------------------------------------------- 1 | /* main.css (c) 2007-2008 Jannis Leidel, see LICENSE.txt for license */ 2 | 3 | /* parts from iui.css (c) 2007 by iUI Project Members */ 4 | 5 | body { 6 | margin: 0; 7 | font-family: Helvetica; 8 | background: #fff; 9 | color: #000; 10 | overflow-x: hidden; 11 | -webkit-user-select: none; 12 | -webkit-text-size-adjust: none; 13 | } 14 | 15 | .float-left { 16 | float: left; 17 | } 18 | 19 | .float-right { 20 | float: right; 21 | } 22 | 23 | .clear { 24 | clear: both; 25 | } 26 | 27 | body > *:not(.toolbar) { 28 | display: none; 29 | position: absolute; 30 | margin: 0; 31 | padding: 0; 32 | left: 0; 33 | top: 100px; 34 | width: 100%; 35 | min-height: 372px; 36 | } 37 | 38 | body[orient="landscape"] > *:not(.toolbar|.breadcrumb) { 39 | min-height: 268px; 40 | } 41 | 42 | body > *[selected="true"] { 43 | display: block; 44 | } 45 | 46 | body > .toolbar { 47 | box-sizing: border-box; 48 | -moz-box-sizing: border-box; 49 | -webkit-box-sizing: border-box; 50 | border-bottom: 1px solid #37657b; 51 | padding: 8px 7px 3px 2px; 52 | height: 60px; 53 | background: #417690 repeat-x; 54 | vertical-align: middle; 55 | } 56 | 57 | .toolbar > h1 { 58 | float: left; 59 | overflow: hidden; 60 | padding: 8px 4px 5px 2px; 61 | font-size: 20px; 62 | font-weight: bold; 63 | text-overflow: ellipsis; 64 | white-space: nowrap; 65 | } 66 | 67 | .toolbar > h1 a { 68 | color: #f4f379; 69 | text-decoration: none; 70 | padding: 5px 6px; 71 | } 72 | 73 | .toolbar > h1 a:active { 74 | background: none; 75 | background-color: none; 76 | } 77 | 78 | .toolbar div.greeting { 79 | color: #f4f379; 80 | font-size: 14px; 81 | font-weight: bold; 82 | padding: 1px 4px; 83 | line-height: 20px; 84 | } 85 | 86 | .toolbar > div.user-tools a, 87 | .toolbar > div.user-tools a:active, 88 | .toolbar > div.user-tools a:visited { 89 | line-height: 20px; 90 | padding: 1px 4px; 91 | font-size: 14px; 92 | font-weight: bold; 93 | text-decoration: none; 94 | color: #fff; 95 | text-align: right; 96 | float: right; 97 | } 98 | 99 | body > ul > li { 100 | position: relative; 101 | margin: 0; 102 | font-size: 20px; 103 | font-weight: bold; 104 | list-style: none; 105 | border-bottom: 1px solid #d9d9d9; 106 | padding: 10px 0 10px 10px; 107 | color: #444; 108 | } 109 | 110 | body > ul > li.group, ul#filter h3 { 111 | position: relative; 112 | font-weight: bold; 113 | color: #fff; 114 | background: url(../img/default-bg.gif) repeat-x; 115 | border-top: 1px solid #bdcddf; 116 | border-bottom: 1px solid #86a4c2; 117 | font-size: 16px; 118 | padding: 3px 10px 2px; 119 | margin-top: -1px; 120 | } 121 | 122 | body > ul > li.subgroup { 123 | background: url(../img/nav-bg.gif) -10px repeat-x; 124 | border-bottom: 1px solid #d9d9d9; 125 | color: #666; 126 | } 127 | 128 | body > ul > li.group:first-child { 129 | top: 0; 130 | border-top: none; 131 | margin-top: 0px; 132 | } 133 | 134 | body > ul > li > a { 135 | margin: -10px 0 -10px -10px; 136 | padding: 10px 32px 10px 10px; 137 | text-decoration: none; 138 | color: #5B80B2; 139 | font-size: 20px; 140 | display: block; 141 | background: url(../img/arrow.png) no-repeat right center; 142 | } 143 | 144 | body > ul > li > a.selected { 145 | color: #666; 146 | background: none; 147 | } 148 | 149 | ul#filter li.selected a, ul#filter li.selected a:link { 150 | color: #666; 151 | background: none; 152 | } 153 | 154 | body > ul > li.paginator { 155 | color: #666; 156 | background: #fff url(../img/nav-bg-reverse.gif) repeat-x top; 157 | min-height: 21px; 158 | padding: 9px 0; 159 | font-size: 18px; 160 | font-weight: bold; 161 | margin: 0px; 162 | } 163 | 164 | body > ul > li.paginator a:link, 165 | body > ul > li.paginator a:active, 166 | body > ul > li.paginator a:visited { 167 | text-decoration: none; 168 | color: #5B80B2; 169 | background: none; 170 | margin: -6px 8px 0px -10px; 171 | } 172 | 173 | body > ul > li.paginator a.inactive, 174 | body > ul > li.paginator a.active { 175 | background: none; 176 | } 177 | 178 | body > ul > li.paginator span a.inactive, 179 | body > ul > li.paginator span a.active { 180 | padding: 9px 12px; 181 | margin: -9px -1px 0px 0px; 182 | } 183 | 184 | body > ul > li.paginator span a.active { 185 | color: #666; 186 | margin-left: 0px; 187 | margin-right: 0px; 188 | padding-left: 13px; 189 | } 190 | 191 | body > ul > li.paginator a.inactive { 192 | border: 1px solid #ccc; 193 | border-top: none; 194 | color: #5B80B2; 195 | background: #fff url(../img/nav-bg.gif) repeat-x bottom; 196 | } 197 | 198 | body > ul > li.paginator span.float-right a.inactive { 199 | border-right: none; 200 | } 201 | 202 | body > ul > li.paginator span.active { 203 | padding-right: 12px; 204 | } 205 | 206 | body > ul > li.paginator span a.showall { 207 | margin: 0px; 208 | } 209 | 210 | body > .breadcrumbs { 211 | box-sizing: border-box; 212 | -moz-box-sizing: border-box; 213 | -webkit-box-sizing: border-box; 214 | position: absolute; 215 | background: url(../img/nav-bg-reverse.gif) repeat-x; 216 | color: #666; 217 | top: 60px; 218 | min-height: 40px; 219 | max-height: 40px; 220 | padding: 9px 0px 9px 8px; 221 | font-size: 18px; 222 | border-top: 1px solid #fff; 223 | border-bottom: 1px solid #cbcbcb; 224 | font-weight: bold; 225 | } 226 | 227 | .breadcrumbs a:link, 228 | .breadcrumbs a:active, 229 | .breadcrumbs a:visited { 230 | text-decoration: none; 231 | color: #5B80B2; 232 | background: none; 233 | padding: 9px 16px 9px 10px; 234 | margin: -9px 4px 0px -6px; 235 | } 236 | 237 | .breadcrumbs a:active { 238 | color: #036; 239 | } 240 | 241 | .breadcrumbs a.active, 242 | .breadcrumbs a.inactive { 243 | background: none; 244 | } 245 | 246 | .breadcrumbs span { 247 | padding-right: 12px; 248 | } 249 | 250 | .breadcrumbs a.active, 251 | .breadcrumbs span.active { 252 | color: #666; 253 | } 254 | 255 | .breadcrumbs a.inactive, 256 | .breadcrumbs span.inactive { 257 | padding-right: 12px; 258 | padding-left: 12px; 259 | padding-bottom: 8px; 260 | margin-right: 0px; 261 | margin-left: -1px; 262 | border: 1px solid #ccc; 263 | border-top: none; 264 | color: #5B80B2; 265 | background: #fff url(../img/nav-bg.gif) repeat-x bottom; 266 | } 267 | 268 | .breadcrumbs a.float-left { 269 | margin-left: -11px; 270 | } 271 | 272 | .breadcrumbs a.float-right { 273 | text-align: right; 274 | } 275 | 276 | .breadcrumbs a.active { 277 | margin-left: -8px; 278 | padding-right: 21px; 279 | } 280 | 281 | .breadcrumbs a#_recent.inactive { 282 | padding-left: 12px; 283 | } 284 | 285 | .breadcrumbs a#_recent.active, 286 | .breadcrumbs a#_filter.active { 287 | padding-left: 22px; 288 | margin-left: 0; 289 | padding-right: 9px; 290 | } 291 | 292 | .breadcrumbs a#_filter span.searchimg { 293 | background: url(../img/search.png) no-repeat center center; 294 | } 295 | 296 | .submit-row { 297 | box-sizing: border-box; 298 | -moz-box-sizing: border-box; 299 | -webkit-box-sizing: border-box; 300 | background: #fff url(../img/nav-bg-reverse.gif) repeat-x; 301 | color: #666; 302 | top: 48px; 303 | min-height: 40px; 304 | max-height: 40px; 305 | padding: 9px 0px 9px 8px; 306 | font-size: 18px; 307 | border: 1px solid #d9d9d9; 308 | border-width: 1px 0; 309 | font-weight: bold; 310 | } 311 | 312 | .submit-row a:link, 313 | .submit-row a:active, 314 | .submit-row a:visited { 315 | text-decoration: none; 316 | color: #5B80B2; 317 | padding: 6px; 318 | margin: -6px 6px 0px 6px; 319 | padding: 9px 16px 9px 10px; 320 | margin: -9px 4px 0px -6px; 321 | } 322 | 323 | .submit-row a:active { 324 | color: #036; 325 | } 326 | 327 | .submit-row a.inactive, .submit-row a.active { 328 | background: none; 329 | } 330 | 331 | .submit-row span { 332 | padding-right: 12px; 333 | } 334 | 335 | .submit-row a.active, .submit-row span.active { 336 | color: #666; 337 | } 338 | 339 | .submit-row a.inactive, .submit-row span.inactive { 340 | color: #5B80B2; 341 | padding-right: 12px; 342 | padding-left: 12px; 343 | padding-bottom: 8px; 344 | margin-right: 0px; 345 | margin-left: -1px; 346 | border: 1px solid #ccc; 347 | border-top: none; 348 | background: #fff url(../img/nav-bg.gif) repeat-x bottom; 349 | } 350 | 351 | .submit-row a.float-left { 352 | margin-left: -11px; 353 | } 354 | 355 | .submit-row a.submit { 356 | color: #fff; 357 | background: #BCD1E8 url(../img/default-bg.gif) repeat-x bottom; 358 | 359 | 360 | /* text-shadow: #BCD1E8 1px 1px 0; 361 | */ 362 | } 363 | 364 | .submit-row a.delete { 365 | color: #CC3434; 366 | } 367 | 368 | /***/ 369 | 370 | body > ul > li.searchbar { 371 | color: #666; 372 | background: #fff url(../img/nav-bg-reverse.gif) repeat-x top; 373 | min-height: 21px; 374 | padding: 9px 0; 375 | font-size: 18px; 376 | font-weight: bold; 377 | } 378 | 379 | body > ul > li.searchbar a:link, 380 | body > ul > li.searchbar a:active, 381 | body > ul > li.searchbar a:visited { 382 | text-decoration: none; 383 | color: #5B80B2; 384 | background: none; 385 | margin: -6px 8px 0px -10px; 386 | } 387 | 388 | body > ul > li.searchbar span a.inactive, 389 | body > ul > li.searchbar span a.active { 390 | padding: 9px 12px; 391 | margin: -9px 0px 0px; 392 | } 393 | 394 | body > ul > li.searchbar span.float-left a.inactive { 395 | margin-right: 4px; 396 | } 397 | 398 | body > ul > li.searchbar a.inactive { 399 | border: 1px solid #ccc; 400 | border-top: none; 401 | color: #5B80B2; 402 | background: #fff url(../img/nav-bg.gif) repeat-x bottom; 403 | } 404 | 405 | body > ul > li.searchbar span.float-right a.inactive { 406 | border-right: none; 407 | } 408 | 409 | body > ul > li.searchbar span.active { 410 | padding-right: 12px; 411 | } 412 | 413 | /***/ 414 | 415 | input, textarea, select { 416 | box-sizing: border-box; 417 | -webkit-box-sizing: border-box; 418 | width: 100%; 419 | margin: 8px 0 0 0; 420 | font-size: 16px; 421 | font-weight: normal; 422 | background: none; 423 | } 424 | 425 | body > .panel { 426 | box-sizing: border-box; 427 | -webkit-box-sizing: border-box; 428 | background: #fff; 429 | } 430 | 431 | .panel > fieldset { 432 | position: relative; 433 | margin: 0 0 20px 0; 434 | padding: 0; 435 | background: #FFFFFF; 436 | border: none; 437 | text-align: right; 438 | font-size: 16px; 439 | } 440 | 441 | .form-row { 442 | position: relative; 443 | min-height: 42px; 444 | text-align: left; 445 | padding: 0 8px; 446 | } 447 | 448 | fieldset > .form-row:last-child { 449 | border-bottom: none !important; 450 | } 451 | 452 | fieldset > .form-row:first-child { 453 | border-top: none; 454 | } 455 | 456 | .form-row input, 457 | #search > input[type="text"] { 458 | display: block; 459 | box-sizing: border-box; 460 | -webkit-box-sizing: border-box; 461 | margin: 0; 462 | padding: 12px 10px 0 12px; 463 | height: 40px; 464 | background: none; 465 | -webkit-border-radius: 5px; 466 | border: 1px solid #D9D9D9; 467 | } 468 | 469 | #search > input[type="text"] { 470 | margin-right: 20px; 471 | } 472 | 473 | #search { 474 | margin: 5px 10px 5px 0px; 475 | background: none; 476 | } 477 | 478 | .form-row > input.vDateField, 479 | .form-row > input.vTimeField { 480 | width: 100%; 481 | } 482 | 483 | .form-row > input.vTimeField { 484 | margin-top: 10px; 485 | } 486 | 487 | .form-row > input[type="checkbox"] { 488 | width: 30px; 489 | height: 30px; 490 | margin: 8px 0px 0px; 491 | border: 0; 492 | -webkit-border-radius: 8px; 493 | background: #fff url(../img/default-bg.gif) repeat-x bottom; 494 | float: left; 495 | clear: left; 496 | } 497 | 498 | .radiolist input { 499 | position:relative; 500 | width: 30px; 501 | height: 30px; 502 | margin: -6px 0; 503 | padding: 0; 504 | border: 0; 505 | -webkit-border-radius: 8px; 506 | background: #fff url(../img/default-bg.gif) repeat-x bottom; 507 | float: left; 508 | clear: left; 509 | } 510 | 511 | .form-row span { 512 | float: right; 513 | margin-top: -20px; 514 | } 515 | 516 | .form-row > select { 517 | -webkit-border-radius: 5px; 518 | border: 1px solid #D9D9D9; 519 | margin: 10px 0 0 0; 520 | padding: 12px 10px 10 12px; 521 | height: 40px; 522 | clear: left; 523 | } 524 | 525 | .form-row > textarea { 526 | box-sizing: border-box; 527 | -webkit-box-sizing: border-box; 528 | display: block; 529 | margin: 0; 530 | border: none; 531 | padding: 12px 10px 0 12px; 532 | height: 126px; 533 | background: none; 534 | -webkit-border-radius: 5px; 535 | border: 1px solid #D9D9D9; 536 | } 537 | 538 | .form-row label, 539 | .form-row p, 540 | #search > label { 541 | position: relative; 542 | top: 9px; 543 | padding: 0 3px; 544 | margin-left: 10px; 545 | font-weight: normal; 546 | font-size: 16px; 547 | color: #666; 548 | text-align: left; 549 | background-color: #fff; 550 | } 551 | 552 | .form-row p.datetime { 553 | margin: 5px -13px 0 0px; 554 | background-color: #fff !important; 555 | padding: 5px 0 0 13px; 556 | } 557 | 558 | .form-row p.datetime input { 559 | margin: 5px 0px 10px -13px; 560 | } 561 | 562 | .form-row p.datetime br { 563 | display: none; 564 | } 565 | 566 | .form-row p.datetime span { 567 | display: none; 568 | } 569 | 570 | .form-row label.vCheckboxLabel { 571 | float: left; 572 | padding: 0.4em 0; 573 | } 574 | 575 | .form-row label.required { 576 | font-weight: bold; 577 | } 578 | 579 | .form-row > ul.radiolist li { 580 | list-style: none; 581 | margin: 10px 10px 20px; 582 | padding: 0; 583 | } 584 | .form-row > ul.radiolist label { 585 | top: 13px; 586 | } 587 | 588 | .form-row > ul.inline { 589 | margin-left: 13px; 590 | padding: 0; 591 | } 592 | 593 | .form-row > ul.inline li { 594 | padding: 2px; 595 | } 596 | 597 | .form-row > ul { 598 | color: #333; 599 | list-style: none; 600 | margin: 0px 2px; 601 | padding: 0px; 602 | font-weight: normal; 603 | } 604 | 605 | .form-row > ul > li > ul { 606 | padding: 10px 20px; 607 | } 608 | 609 | .form-row a.add-another { 610 | display: none; 611 | } 612 | 613 | .panel > h2 { 614 | margin: 0; 615 | padding: 2px 10px; 616 | font-size: 14px; 617 | font-weight: bold; 618 | color: #fff; 619 | background: url(../img/default-bg.gif) repeat-x; 620 | border-top: 1px solid #bdcddf; 621 | border-bottom: 1px solid #86a4c2; 622 | } 623 | 624 | .panel > h3 { 625 | margin: 0; 626 | padding: 2px 10px; 627 | font-size: 14px; 628 | font-weight: bold; 629 | color: #fff; 630 | background: url(../img/nav-bg.gif) -10px repeat-x; 631 | border-bottom: 1px solid #d9d9d9; 632 | border-top: 1px solid #d9d9d9; 633 | color: #666; 634 | } 635 | 636 | .panel > h1 { 637 | margin: 0; 638 | padding: 10px; 639 | font-size: 16px; 640 | font-weight: bold; 641 | color: #666; 642 | background: none; 643 | } 644 | 645 | .panel > h1.error { 646 | color: #CC2830; 647 | } 648 | 649 | .panel a.help-link { 650 | float: right; 651 | color: #ccc; 652 | text-decoration: none; 653 | font-weight: bold; 654 | padding: 2px 4px; 655 | font-size: 14px; 656 | } 657 | 658 | ul.errorlist, ul.errorlist li { 659 | list-style: none; 660 | color: #cc2830; 661 | font-size: 12px; 662 | padding: 2px 4px; 663 | float: right; 664 | font-weight: bold; 665 | } 666 | 667 | ul.errorlist > li { 668 | list-style: none; 669 | } 670 | 671 | #preloader { 672 | display: none; 673 | background-image: url(../img/default-bg.gif), url(../img/default-bg-reverse.gif), 674 | url(../img/icon_searchbox.png), url(../img/arrow.png), 675 | url(../img/nav-bg.gif), url(../img/nav-bg-reverse.gif); 676 | } -------------------------------------------------------------------------------- /mobileadmin/media/img/arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jezdez/django-mobileadmin/4d5874916ecca91610b9fbdf3c50b34c2567b83e/mobileadmin/media/img/arrow.png -------------------------------------------------------------------------------- /mobileadmin/media/img/default-bg-reverse.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jezdez/django-mobileadmin/4d5874916ecca91610b9fbdf3c50b34c2567b83e/mobileadmin/media/img/default-bg-reverse.gif -------------------------------------------------------------------------------- /mobileadmin/media/img/default-bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jezdez/django-mobileadmin/4d5874916ecca91610b9fbdf3c50b34c2567b83e/mobileadmin/media/img/default-bg.gif -------------------------------------------------------------------------------- /mobileadmin/media/img/nav-bg-reverse.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jezdez/django-mobileadmin/4d5874916ecca91610b9fbdf3c50b34c2567b83e/mobileadmin/media/img/nav-bg-reverse.gif -------------------------------------------------------------------------------- /mobileadmin/media/img/nav-bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jezdez/django-mobileadmin/4d5874916ecca91610b9fbdf3c50b34c2567b83e/mobileadmin/media/img/nav-bg.gif -------------------------------------------------------------------------------- /mobileadmin/media/img/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jezdez/django-mobileadmin/4d5874916ecca91610b9fbdf3c50b34c2567b83e/mobileadmin/media/img/search.png -------------------------------------------------------------------------------- /mobileadmin/media/js/mobile_safari.js: -------------------------------------------------------------------------------- 1 | addEventListener("load", function() { 2 | setTimeout(updateLayout, 0); 3 | }, false); 4 | 5 | truncateRegistry = {}; 6 | function truncate(elements) { 7 | for (ele in elements) { 8 | if (!(ele in truncateRegistry)) { 9 | truncateRegistry[ele] = { 10 | 'size': elements[ele], 11 | 'content': $(ele).innerHTML, 12 | }; 13 | } 14 | } 15 | } 16 | function truncateDot(mode) { 17 | for (name in truncateRegistry) { 18 | var element = $(name); 19 | var ele = truncateRegistry[name]; 20 | if (mode == 'on') { 21 | if (ele['content'].length > ele['size']) { 22 | if (ele['size'] > 3) { 23 | element.innerHTML = ele['content'].substring(0,(ele['size']-3)) + '...'; 24 | } else { 25 | element.innerHTML = ele['content'].substring(0,ele['size']); 26 | } 27 | } 28 | } else { 29 | element.innerHTML = ele['content']; 30 | } 31 | } 32 | } 33 | 34 | var currentWidth = 0; 35 | function updateLayout() { 36 | switch(window.orientation) { 37 | case 0: 38 | case 180: 39 | var orient = "portrait"; 40 | truncateDot('on'); 41 | break; 42 | 43 | case 90: 44 | case -90: 45 | var orient = "landscape"; 46 | truncateDot('off'); 47 | break; 48 | } 49 | document.body.setAttribute("orient", orient); 50 | setTimeout(function() { 51 | window.scrollTo(0, 1); 52 | }, 100); 53 | } 54 | 55 | function $(ele) { 56 | return document.getElementById(ele); 57 | } 58 | function hasClass(ele,cls) { 59 | return ele.className.match(new RegExp('(\\s|^)'+cls+'(\\s|$)')); 60 | } 61 | function addClass(ele,cls) { 62 | if (!this.hasClass(ele,cls)) ele.className += " "+cls; 63 | } 64 | function removeClass(ele,cls) { 65 | if (hasClass(ele,cls)) { 66 | var reg = new RegExp('(\\s|^)'+cls+'(\\s|$)'); 67 | ele.className=ele.className.replace(reg,' '); 68 | } 69 | } 70 | function addAndRemoveClass(ele,_new,_old) { 71 | addClass(ele,_new); 72 | removeClass(ele,_old); 73 | } 74 | function switch_tabs(tabLabels) { 75 | function toggle() { 76 | var toActivate = this.id.substr(1); 77 | var toDeactivate = tabLabels[toActivate]; 78 | $(toDeactivate).removeAttribute('selected'); 79 | $(toActivate).setAttribute('selected', 'true'); 80 | addAndRemoveClass($(this.id), 'active', 'inactive'); 81 | addAndRemoveClass($("_"+toDeactivate), 'inactive', 'active'); 82 | } 83 | for (label in tabLabels) { 84 | $("_"+label).addEventListener("click", toggle, false); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /mobileadmin/media/js/urlify.js: -------------------------------------------------------------------------------- 1 | var LATIN_MAP = { 2 | 'À': 'A', 'Á': 'A', 'Â': 'A', 'Ã': 'A', 'Ä': 'A', 'Å': 'A', 'Æ': 'AE', 'Ç': 3 | 'C', 'È': 'E', 'É': 'E', 'Ê': 'E', 'Ë': 'E', 'Ì': 'I', 'Í': 'I', 'Î': 'I', 4 | 'Ï': 'I', 'Ð': 'D', 'Ñ': 'N', 'Ò': 'O', 'Ó': 'O', 'Ô': 'O', 'Õ': 'O', 'Ö': 5 | 'O', 'Ő': 'O', 'Ø': 'O', 'Ù': 'U', 'Ú': 'U', 'Û': 'U', 'Ü': 'U', 'Ű': 'U', 6 | 'Ý': 'Y', 'Þ': 'TH', 'ß': 'ss', 'à':'a', 'á':'a', 'â': 'a', 'ã': 'a', 'ä': 7 | 'a', 'å': 'a', 'æ': 'ae', 'ç': 'c', 'è': 'e', 'é': 'e', 'ê': 'e', 'ë': 'e', 8 | 'ì': 'i', 'í': 'i', 'î': 'i', 'ï': 'i', 'ð': 'd', 'ñ': 'n', 'ò': 'o', 'ó': 9 | 'o', 'ô': 'o', 'õ': 'o', 'ö': 'o', 'ő': 'o', 'ø': 'o', 'ù': 'u', 'ú': 'u', 10 | 'û': 'u', 'ü': 'u', 'ű': 'u', 'ý': 'y', 'þ': 'th', 'ÿ': 'y' 11 | } 12 | var LATIN_SYMBOLS_MAP = { 13 | '©':'(c)' 14 | } 15 | var GREEK_MAP = { 16 | 'α':'a', 'β':'b', 'γ':'g', 'δ':'d', 'ε':'e', 'ζ':'z', 'η':'h', 'θ':'8', 17 | 'ι':'i', 'κ':'k', 'λ':'l', 'μ':'m', 'ν':'n', 'ξ':'3', 'ο':'o', 'π':'p', 18 | 'ρ':'r', 'σ':'s', 'τ':'t', 'υ':'y', 'φ':'f', 'χ':'x', 'ψ':'ps', 'ω':'w', 19 | 'ά':'a', 'έ':'e', 'ί':'i', 'ό':'o', 'ύ':'y', 'ή':'h', 'ώ':'w', 'ς':'s', 20 | 'ϊ':'i', 'ΰ':'y', 'ϋ':'y', 'ΐ':'i', 21 | 'Α':'A', 'Β':'B', 'Γ':'G', 'Δ':'D', 'Ε':'E', 'Ζ':'Z', 'Η':'H', 'Θ':'8', 22 | 'Ι':'I', 'Κ':'K', 'Λ':'L', 'Μ':'M', 'Ν':'N', 'Ξ':'3', 'Ο':'O', 'Π':'P', 23 | 'Ρ':'R', 'Σ':'S', 'Τ':'T', 'Υ':'Y', 'Φ':'F', 'Χ':'X', 'Ψ':'PS', 'Ω':'W', 24 | 'Ά':'A', 'Έ':'E', 'Ί':'I', 'Ό':'O', 'Ύ':'Y', 'Ή':'H', 'Ώ':'W', 'Ϊ':'I', 25 | 'Ϋ':'Y' 26 | } 27 | var TURKISH_MAP = { 28 | 'ş':'s', 'Ş':'S', 'ı':'i', 'İ':'I', 'ç':'c', 'Ç':'C', 'ü':'u', 'Ü':'U', 29 | 'ö':'o', 'Ö':'O', 'ğ':'g', 'Ğ':'G' 30 | } 31 | var RUSSIAN_MAP = { 32 | 'а':'a', 'б':'b', 'в':'v', 'г':'g', 'д':'d', 'е':'e', 'ё':'yo', 'ж':'zh', 33 | 'з':'z', 'и':'i', 'й':'j', 'к':'k', 'л':'l', 'м':'m', 'н':'n', 'о':'o', 34 | 'п':'p', 'р':'r', 'с':'s', 'т':'t', 'у':'u', 'ф':'f', 'х':'h', 'ц':'c', 35 | 'ч':'ch', 'ш':'sh', 'щ':'sh', 'ъ':'', 'ы':'y', 'ь':'', 'э':'e', 'ю':'yu', 36 | 'я':'ya', 37 | 'А':'A', 'Б':'B', 'В':'V', 'Г':'G', 'Д':'D', 'Е':'E', 'Ё':'Yo', 'Ж':'Zh', 38 | 'З':'Z', 'И':'I', 'Й':'J', 'К':'K', 'Л':'L', 'М':'M', 'Н':'N', 'О':'O', 39 | 'П':'P', 'Р':'R', 'С':'S', 'Т':'T', 'У':'U', 'Ф':'F', 'Х':'H', 'Ц':'C', 40 | 'Ч':'Ch', 'Ш':'Sh', 'Щ':'Sh', 'Ъ':'', 'Ы':'Y', 'Ь':'', 'Э':'E', 'Ю':'Yu', 41 | 'Я':'Ya' 42 | } 43 | var UKRAINIAN_MAP = { 44 | 'Є':'Ye', 'І':'I', 'Ї':'Yi', 'Ґ':'G', 'є':'ye', 'і':'i', 'ї':'yi', 'ґ':'g' 45 | } 46 | var CZECH_MAP = { 47 | 'č':'c', 'ď':'d', 'ě':'e', 'ň': 'n', 'ř':'r', 'š':'s', 'ť':'t', 'ů':'u', 48 | 'ž':'z', 'Č':'C', 'Ď':'D', 'Ě':'E', 'Ň': 'N', 'Ř':'R', 'Š':'S', 'Ť':'T', 49 | 'Ů':'U', 'Ž':'Z' 50 | } 51 | 52 | var POLISH_MAP = { 53 | 'ą':'a', 'ć':'c', 'ę':'e', 'ł':'l', 'ń':'n', 'ó':'o', 'ś':'s', 'ź':'z', 54 | 'ż':'z', 'Ą':'A', 'Ć':'C', 'Ę':'e', 'Ł':'L', 'Ń':'N', 'Ó':'o', 'Ś':'S', 55 | 'Ź':'Z', 'Ż':'Z' 56 | } 57 | 58 | var LATVIAN_MAP = { 59 | 'ā':'a', 'č':'c', 'ē':'e', 'ģ':'g', 'ī':'i', 'ķ':'k', 'ļ':'l', 'ņ':'n', 60 | 'š':'s', 'ū':'u', 'ž':'z', 'Ā':'A', 'Č':'C', 'Ē':'E', 'Ģ':'G', 'Ī':'i', 61 | 'Ķ':'k', 'Ļ':'L', 'Ņ':'N', 'Š':'S', 'Ū':'u', 'Ž':'Z' 62 | } 63 | 64 | var ALL_DOWNCODE_MAPS=new Array() 65 | ALL_DOWNCODE_MAPS[0]=LATIN_MAP 66 | ALL_DOWNCODE_MAPS[1]=LATIN_SYMBOLS_MAP 67 | ALL_DOWNCODE_MAPS[2]=GREEK_MAP 68 | ALL_DOWNCODE_MAPS[3]=TURKISH_MAP 69 | ALL_DOWNCODE_MAPS[4]=RUSSIAN_MAP 70 | ALL_DOWNCODE_MAPS[5]=UKRAINIAN_MAP 71 | ALL_DOWNCODE_MAPS[6]=CZECH_MAP 72 | ALL_DOWNCODE_MAPS[7]=POLISH_MAP 73 | ALL_DOWNCODE_MAPS[8]=LATVIAN_MAP 74 | 75 | var Downcoder = new Object(); 76 | Downcoder.Initialize = function() 77 | { 78 | if (Downcoder.map) // already made 79 | return ; 80 | Downcoder.map ={} 81 | Downcoder.chars = '' ; 82 | for(var i in ALL_DOWNCODE_MAPS) 83 | { 84 | var lookup = ALL_DOWNCODE_MAPS[i] 85 | for (var c in lookup) 86 | { 87 | Downcoder.map[c] = lookup[c] ; 88 | Downcoder.chars += c ; 89 | } 90 | } 91 | Downcoder.regex = new RegExp('[' + Downcoder.chars + ']|[^' + Downcoder.chars + ']+','g') ; 92 | } 93 | 94 | downcode= function( slug ) 95 | { 96 | Downcoder.Initialize() ; 97 | var downcoded ="" 98 | var pieces = slug.match(Downcoder.regex); 99 | if(pieces) 100 | { 101 | for (var i = 0 ; i < pieces.length ; i++) 102 | { 103 | if (pieces[i].length == 1) 104 | { 105 | var mapped = Downcoder.map[pieces[i]] ; 106 | if (mapped != null) 107 | { 108 | downcoded+=mapped; 109 | continue ; 110 | } 111 | } 112 | downcoded+=pieces[i]; 113 | } 114 | } 115 | else 116 | { 117 | downcoded = slug; 118 | } 119 | return downcoded; 120 | } 121 | 122 | 123 | function URLify(s, num_chars) { 124 | // changes, e.g., "Petty theft" to "petty_theft" 125 | // remove all these words from the string before urlifying 126 | s = downcode(s); 127 | removelist = ["a", "an", "as", "at", "before", "but", "by", "for", "from", 128 | "is", "in", "into", "like", "of", "off", "on", "onto", "per", 129 | "since", "than", "the", "this", "that", "to", "up", "via", 130 | "with"]; 131 | r = new RegExp('\\b(' + removelist.join('|') + ')\\b', 'gi'); 132 | s = s.replace(r, ''); 133 | // if downcode doesn't hit, the char will be stripped here 134 | s = s.replace(/[^-\w\s]/g, ''); // remove unneeded chars 135 | s = s.replace(/^\s+|\s+$/g, ''); // trim leading/trailing spaces 136 | s = s.replace(/[-\s]+/g, '-'); // convert spaces to hyphens 137 | s = s.toLowerCase(); // convert to lowercase 138 | return s.substring(0, num_chars);// trim to first num_chars chars 139 | } 140 | 141 | -------------------------------------------------------------------------------- /mobileadmin/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jezdez/django-mobileadmin/4d5874916ecca91610b9fbdf3c50b34c2567b83e/mobileadmin/models.py -------------------------------------------------------------------------------- /mobileadmin/options.py: -------------------------------------------------------------------------------- 1 | from django.contrib.admin import options 2 | from mobileadmin import decorators 3 | 4 | class MobileModelAdmin(options.ModelAdmin): 5 | """ 6 | A custom model admin class to override the used templates depending on the 7 | user agent of the request. 8 | 9 | Please use it in case you want to create you own mobileadmin. 10 | """ 11 | def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None): 12 | return super(MobileModelAdmin, self).render_change_form(request, context, add, change, form_url, obj) 13 | render_change_form = decorators.mobile_templates(render_change_form) 14 | 15 | def changelist_view(self, request, extra_context=None): 16 | return super(MobileModelAdmin, self).changelist_view(request, extra_context) 17 | changelist_view = decorators.mobile_templates(changelist_view) 18 | 19 | def delete_view(self, request, object_id, extra_context=None): 20 | return super(MobileModelAdmin, self).delete_view(request, object_id, extra_context) 21 | delete_view = decorators.mobile_templates(delete_view) 22 | 23 | def history_view(self, request, object_id, extra_context=None): 24 | return super(MobileModelAdmin, self).history_view(request, object_id, extra_context) 25 | history_view = decorators.mobile_templates(history_view) 26 | 27 | class MobileStackedInline(options.StackedInline): 28 | template = 'edit_inline/stacked.html' 29 | 30 | class MobileTabularInline(options.InlineModelAdmin): 31 | template = 'edit_inline/tabular.html' 32 | -------------------------------------------------------------------------------- /mobileadmin/sites.py: -------------------------------------------------------------------------------- 1 | from django.contrib.admin import sites 2 | from django.views.decorators.cache import never_cache 3 | from django.contrib.auth.views import password_change, password_change_done, logout 4 | 5 | from mobileadmin.decorators import mobile_templates 6 | 7 | class MobileAdminSite(sites.AdminSite): 8 | """ 9 | A custom admin site to override the used templates. 10 | Add that to your urls.py: 11 | 12 | import mobileadmin 13 | urlpatterns += patterns('', 14 | (r'^m/(.*)', mobileadmin.site.root), 15 | ) 16 | """ 17 | logout_template = None 18 | password_change_template = None 19 | password_change_done_template = None 20 | 21 | def index(self, request, extra_context=None): 22 | return super(MobileAdminSite, self).index(request, extra_context) 23 | index = mobile_templates(index) 24 | 25 | def display_login_form(self, request, error_message='', extra_context=None): 26 | return super(MobileAdminSite, self).display_login_form(request, error_message, extra_context) 27 | display_login_form = mobile_templates(display_login_form) 28 | 29 | def app_index(self, request, app_label, extra_context=None): 30 | return super(MobileAdminSite, self).app_index(request, app_label, extra_context) 31 | app_index = mobile_templates(app_index) 32 | 33 | def logout(self, request): 34 | return logout(request, template_name=self.logout_template or 'registration/logged_out.html') 35 | logout = never_cache(mobile_templates(logout)) 36 | 37 | def password_change(self, request): 38 | return password_change(request, 39 | template_name=self.password_change_template or 'registration/password_change_form.html', 40 | post_change_redirect='%spassword_change/done/' % self.root_path) 41 | password_change = mobile_templates(password_change) 42 | 43 | def password_change_done(self, request): 44 | return password_change_done(request, 45 | template_name=self.password_change_done_template or 'registration/password_change_done.html') 46 | password_change_done = mobile_templates(password_change_done) 47 | 48 | site = MobileAdminSite() 49 | -------------------------------------------------------------------------------- /mobileadmin/templates/mobileadmin/mobile_safari/404.html: -------------------------------------------------------------------------------- 1 | {% extends "mobileadmin/mobile_safari/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans 'Page not found' %}{% endblock %} 5 | {% block breadcrumbs %}{% trans 'Page not found' %}{% endblock %} 6 | {% block tools %}{% endblock tools %} 7 | 8 | {% block content %} 9 |
10 |

{% trans "We're sorry, but the requested page could not be found." %}

11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /mobileadmin/templates/mobileadmin/mobile_safari/505.html: -------------------------------------------------------------------------------- 1 | {% extends "mobileadmin/mobile_safari/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans 'Server error (500)' %}{% endblock %} 5 | {% block breadcrumbs %}{% trans 'Server error (500)' %}{% endblock %} 6 | {% block tools %}{% endblock tools %} 7 | 8 | {% block content %} 9 |
10 |

{% trans "There's been an error. It's been reported to the site administrators via e-mail and should be fixed shortly. Thanks for your patience." %}

11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /mobileadmin/templates/mobileadmin/mobile_safari/app_index.html: -------------------------------------------------------------------------------- 1 | {% extends "mobileadmin/mobile_safari/index.html" %} 2 | {% load i18n %} 3 | 4 | {% block breadcrumbs %}{{ title }}{% endblock %} 5 | 6 | {% block content %} 7 | 18 | 19 | {% endblock %} -------------------------------------------------------------------------------- /mobileadmin/templates/mobileadmin/mobile_safari/auth/user/add_form.html: -------------------------------------------------------------------------------- 1 | {% extends "mobileadmin/mobile_safari/change_form.html" %} 2 | {% load i18n %} 3 | 4 | {% block after_field_sets %} 5 |

{% trans "First, enter a username and password. Then, you'll be able to edit more user options." %}

6 |
7 |
8 | {{ form.username.errors }} 9 | {{ form.username }} 10 |
11 | 12 |
13 | {{ form.password1.errors }} 14 | {{ form.password1 }} 15 |
16 | 17 |
18 | {{ form.password2 }} 19 | {{ form.password2.errors }} 20 |
21 | 22 | 23 |
24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /mobileadmin/templates/mobileadmin/mobile_safari/base.html: -------------------------------------------------------------------------------- 1 | {% load i18n mobile_admin_media %}{% spaceless %} 2 | 3 | 4 | {% block title %}{% trans 'Django administration' %}{% endblock %} 5 | 6 | 7 | {% block blockbots %}{% endblock %} 8 | 9 | 10 | {% block extrahead %}{% endblock %} 11 | {% block extrastyle %}{% endblock %} 12 | 13 | 14 | 15 |
16 | {% block header_logo %} 17 |

Django

18 | {% endblock %} 19 | {% block tools %} 20 | {% if user.is_authenticated and user.is_staff %} 21 |
22 | {% trans "Logout" %} 23 | {% trans 'Change password' %} 24 | 25 |
26 |
27 | {% trans 'Welcome,' %} {% if user.first_name %}{{ user.first_name|escape }}{% else %}{{ user.username }}{% endif %} 28 |
29 | {% endif %} 30 | {% endblock tools %} 31 |
32 | 33 | {% block content %}{{ content }}{% endblock %} 34 | 35 | 36 | {% endspaceless %} 37 | -------------------------------------------------------------------------------- /mobileadmin/templates/mobileadmin/mobile_safari/change_form.html: -------------------------------------------------------------------------------- 1 | {% extends "mobileadmin/mobile_safari/base.html" %} 2 | {% load i18n mobile_admin_modify mobile_admin_media %} 3 | 4 | {% block extrahead %}{{ block.super }} 5 | 6 | 7 | {% endblock %} 8 | 9 | 10 | {% block breadcrumbs %}{{ opts.verbose_name_plural|capfirst|escape }} 11 | 12 | {% if change %} 13 | {% if has_absolute_url %}{% trans "Go" %}{% endif%} 14 | {% trans "History" %} 15 | {% endif%} 16 | 17 | {% if add %} 18 | {% trans "Add" %} 19 | {% endif %} 20 | {% endblock %} 21 | 22 | {% block content %} 23 |
{% csrf_token %}{% block form_top %}{% endblock %} 24 | {% if not add %} 25 |

{{ opts.object_name|escape }}: {{ original|escape }}

26 | 31 | {% endif %} 32 | 33 | {% if errors %} 34 |

35 | {% blocktrans count errors.items|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %} 36 |

37 | {% endif %} 38 | 39 | {% for fieldset in adminform %} 40 | {% include "mobileadmin/mobile_safari/includes/fieldset.html" %} 41 | {% endfor %} 42 | 43 | {% block after_field_sets %}{% endblock %} 44 | 45 | {% for inline_admin_formset in inline_admin_formsets %} 46 | {% mobile_inline_admin_formset inline_admin_formset user_agent %} 47 | {% endfor %} 48 | 49 | {% block after_related_objects %}{% endblock %} 50 | 51 |
52 | {% trans 'Save' %} 53 | {% if has_delete_permission %}{% if change or show_delete %}{% trans "Delete" %}{% endif %}{% endif %} 54 |
55 | 56 | {% if add %} 57 | 58 | {% endif %} 59 | 60 | {# JavaScript for prepopulated fields #} 61 | {% prepopulated_fields_js %} 62 |
63 | {% endblock %} 64 | -------------------------------------------------------------------------------- /mobileadmin/templates/mobileadmin/mobile_safari/change_list.html: -------------------------------------------------------------------------------- 1 | {% extends "mobileadmin/mobile_safari/base.html" %} 2 | {% load mobile_admin_list i18n mobile_admin_media %} 3 | 4 | {% block breadcrumbs %} 5 | {{ app_label.title|escape }} 6 | {% trans "Add" %} 7 | {% if cl.search_fields or cl.has_filters %} 8 | 9 |   10 | 11 | {% endif %} 12 | {% endblock %} 13 | 14 | {% block content %} 15 | 23 | 24 | {% if cl.search_fields or cl.has_filters %} 25 | 34 | 44 | {% endif %} 45 | {% endblock %} 46 | -------------------------------------------------------------------------------- /mobileadmin/templates/mobileadmin/mobile_safari/delete_confirmation.html: -------------------------------------------------------------------------------- 1 | {% extends "mobileadmin/mobile_safari/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block breadcrumbs %}{{ object|escape }}{% trans "Delete" %}{% endblock %} 5 | 6 | {% block content %} 7 | {% if perms_lacking %} 8 |
9 |

{% blocktrans with object|escape as escaped_object %}Deleting the {{ object_name }} '{{ escaped_object }}' would result in deleting related objects, but your account doesn't have permission to delete the following types of objects:{% endblocktrans %}

10 |
11 | {% for obj in perms_lacking %} 12 |
13 | 14 |
15 | {% endfor %} 16 |
17 |
18 | {% else %} 19 |
20 |

{% blocktrans with object|escape as escaped_object %}Are you sure you want to delete the {{ object_name }} "{{ escaped_object }}"? All of the following related items will be deleted:{% endblocktrans %}

21 |
22 |
23 |
    {{ deleted_objects|unordered_list|removetags:"a"|safe }}
24 |
25 |
26 | 27 | 30 |
31 | {% endif %} 32 | 33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /mobileadmin/templates/mobileadmin/mobile_safari/edit_inline/stacked.html: -------------------------------------------------------------------------------- 1 | {% load i18n mobile_admin_modify %} 2 | 3 |

{{ inline_admin_formset.opts.verbose_name_plural|title }}

4 | {{ inline_admin_formset.formset.management_form }} 5 | {{ inline_admin_formset.formset.non_form_errors }} 6 | {% for inline_admin_form in inline_admin_formset %} 7 |

{{ inline_admin_formset.opts.verbose_name|title }}: {% if inline_admin_form.original %}{{ inline_admin_form.original }}{% else %} #{{ forloop.counter }}{% endif %}

8 | {% if inline_admin_formset.formset.can_delete and inline_admin_form.original %} 9 |
10 | {{ inline_admin_form.deletion_field.field }} {{ inline_admin_form.deletion_field.label_tag }} 11 |
12 | {% endif %} 13 | {% for fieldset in inline_admin_form %} 14 | {% mobile_inline_admin_fieldset fieldset user_agent %} 15 | {% endfor %} 16 | {{ inline_admin_form.pk_field.field }} 17 | {% endfor %} 18 | -------------------------------------------------------------------------------- /mobileadmin/templates/mobileadmin/mobile_safari/edit_inline/tabular.html: -------------------------------------------------------------------------------- 1 | {% include "mobileadmin/mobile_safari/edit_inline/stacked.html" %} -------------------------------------------------------------------------------- /mobileadmin/templates/mobileadmin/mobile_safari/filter.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 |
  • {% blocktrans with title|escape as filter_title %} By {{ filter_title }} {% endblocktrans %}
  • 3 | {% for choice in choices %} 4 |
  • 5 | 6 | {{ choice.display|escape }} 7 | 8 |
  • 9 | {% endfor %} 10 | -------------------------------------------------------------------------------- /mobileadmin/templates/mobileadmin/mobile_safari/includes/fieldset.html: -------------------------------------------------------------------------------- 1 | {% if fieldset.name %}

    {{ fieldset.name }}

    {% endif %} 2 |
    3 | {% for line in fieldset %} 4 |
    5 | {{ line.errors }} 6 | {% for field in line %} 7 | {% if field.field.field.help_text %} 8 | ? 9 | {% endif %} 10 | {% if field.is_checkbox %} 11 | {{ field.field }} 12 | {% else %} 13 | {{ field.label_tag }}{{ field.field }} 14 | {% endif %} 15 | {% endfor %} 16 |
    17 | {% endfor %} 18 |
    19 | -------------------------------------------------------------------------------- /mobileadmin/templates/mobileadmin/mobile_safari/index.html: -------------------------------------------------------------------------------- 1 | {% extends "mobileadmin/mobile_safari/base.html" %} 2 | {% load i18n mobile_admin_modify %} 3 | 4 | {% block breadcrumbs %} 5 | {% trans "Site administration" %} 6 | {% trans 'Recent Actions' %} 7 | {% endblock %} 8 | 9 | {% block content %} 10 | {% if app_list %} 11 | 17 | {% else %} 18 |
    19 |

    {% trans "You don't have permission to edit anything." %}

    20 |

    21 | {% endif %} 22 | 23 | 40 | 50 | {% endblock %} 51 | -------------------------------------------------------------------------------- /mobileadmin/templates/mobileadmin/mobile_safari/invalid_setup.html: -------------------------------------------------------------------------------- 1 | {% extends "mobileadmin/mobile_safari/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{{ title }}{% endblock %} 5 | 6 | {% block content %} 7 |
    8 |

    {% trans "Something's wrong with your database installation. Make sure the appropriate database tables have been created, and make sure the database is readable by the appropriate user." %}

    9 |
    10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /mobileadmin/templates/mobileadmin/mobile_safari/login.html: -------------------------------------------------------------------------------- 1 | {% extends "mobileadmin/mobile_safari/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block breadcrumbs %}{% trans 'Log in' %}{% endblock %} 5 | 6 | {% block content %} 7 |
    8 | {% csrf_token %} 9 | {% if error_message %}

    {{ error_message }}

    {% endif %} 10 |
    11 |
    12 | 13 | 14 |
    15 |
    16 | 17 | 18 | 19 | 20 |
    21 |
    22 | 25 |
    26 | 27 | 30 | 31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /mobileadmin/templates/mobileadmin/mobile_safari/object_history.html: -------------------------------------------------------------------------------- 1 | {% extends "mobileadmin/mobile_safari/base.html" %} 2 | {% load mobile_admin_modify i18n %} 3 | 4 | {% block breadcrumbs %}{{ object|escape }} {% trans 'History' %} 5 | 6 | {% endblock %} 7 | 8 | {% block content %} 9 | {% if action_list %} 10 | 18 | {% else %} 19 |
    20 |

    {% trans "This object doesn't have a change history. It probably wasn't added via this admin site." %}

    21 |
    22 | {% endif %} 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /mobileadmin/templates/mobileadmin/mobile_safari/pagination.html: -------------------------------------------------------------------------------- 1 | {% load mobile_admin_list i18n %} 2 |
  • 3 | 4 | {% if pagination_required %} 5 | {% for i in page_range %}{% paginator_number cl i %}{% endfor %} 6 | {% endif %} 7 | 8 | {% if show_all_url %} 9 | {% trans 'All' %} 10 | {% endif %} 11 | {{ cl.result_count }} {% ifequal cl.result_count 1 %}{{ cl.opts.verbose_name|capfirst }}{% else %}{{ cl.opts.verbose_name_plural }}{% endifequal %} 12 |
  • 13 | -------------------------------------------------------------------------------- /mobileadmin/templates/mobileadmin/mobile_safari/registration/logged_out.html: -------------------------------------------------------------------------------- 1 | {% extends "mobileadmin/mobile_safari/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block breadcrumbs %}{% trans 'Log in again' %}{% trans "Logged out" %}{% endblock %} 5 | 6 | {% block content %} 7 |
    8 |

    {% trans "Thanks for spending some quality time with the Web site today." %}

    9 |

    10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /mobileadmin/templates/mobileadmin/mobile_safari/registration/password_change_done.html: -------------------------------------------------------------------------------- 1 | {% extends "mobileadmin/mobile_safari/registration/password_change_form.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans 'Password change successful' %}{% endblock %} 5 | 6 | {% block content %} 7 |
    8 |

    {% trans 'Your password was changed.' %}

    9 |
    10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /mobileadmin/templates/mobileadmin/mobile_safari/registration/password_change_form.html: -------------------------------------------------------------------------------- 1 | {% extends "mobileadmin/mobile_safari/base.html" %} 2 | {% load i18n %} 3 | {% block breadcrumbs %}{% trans 'Home' %}{% trans 'Password change' %}{% endblock %} 4 | {% block tools %} 5 | {% if user.is_authenticated and user.is_staff %} 6 |
    7 | {% trans "Logout" %} 8 | {% trans 'Change password' %} 9 |
    10 |
    11 | {% trans 'Welcome,' %} {% if user.first_name %}{{ user.first_name|escape }}{% else %}{{ user.username }}{% endif %} 12 |
    13 | {% endif %} 14 | {% endblock tools %} 15 | 16 | {% block header_logo %} 17 |

    Django

    18 | {% endblock %} 19 | 20 | {% block content %} 21 |
    22 |

    {% trans "Please enter your old password, for security's sake, and then enter your new password twice so we can verify you typed it in correctly." %}

    23 |
    24 |
    25 | 26 | {% if form.old_password.errors %}{{ form.old_password.errors }}{% endif %} 27 | {{ form.old_password }} 28 |
    29 |
    30 | 31 | {% if form.new_password1.errors %}{{ form.new_password1.errors }}{% endif %} 32 | {{ form.new_password1 }} 33 |
    34 |
    35 | 36 | {% if form.new_password2.errors %}{{ form.new_password2.errors }}{% endif %} 37 | {{ form.new_password2 }} 38 |
    39 |
    40 | 43 |
    44 | {% endblock %} 45 | -------------------------------------------------------------------------------- /mobileadmin/templates/mobileadmin/mobile_safari/search_form.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% if cl.search_fields %} 3 |
  • {% trans "Search" %}
  • 4 |
  • 5 | 11 |
  • 12 | 19 | 20 | {% endif %} 21 | -------------------------------------------------------------------------------- /mobileadmin/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jezdez/django-mobileadmin/4d5874916ecca91610b9fbdf3c50b34c2567b83e/mobileadmin/templatetags/__init__.py -------------------------------------------------------------------------------- /mobileadmin/templatetags/mobile_admin_list.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.template.loader import render_to_string 3 | from django.contrib.admin.views.main import ALL_VAR, PAGE_VAR, SEARCH_VAR 4 | 5 | register = template.Library() 6 | 7 | def paginator_number(cl, i): 8 | if i == cl.page_num: 9 | classname = "active" 10 | else: 11 | classname = "inactive" 12 | return u'%d ' % (cl.get_query_string({PAGE_VAR: i}), classname, i+1) 13 | paginator_number = register.simple_tag(paginator_number) 14 | 15 | def pagination(cl, user_agent): 16 | paginator, page_num = cl.paginator, cl.page_num 17 | 18 | pagination_required = (not cl.show_all or not cl.can_show_all) and cl.multi_page 19 | if not pagination_required: 20 | page_range = [] 21 | else: 22 | ON_EACH_SIDE = 1 23 | 24 | # If there are 4 or fewer pages, display links to every page. 25 | # Otherwise, do some fancy 26 | if paginator.num_pages <= 3: 27 | page_range = range(paginator.num_pages) 28 | else: 29 | # Insert "smart" pagination links, so that there are always ON_ENDS 30 | # links at either end of the list of pages, and there are always 31 | # ON_EACH_SIDE links at either end of the "current page" link. 32 | page_range = [] 33 | if page_num > ON_EACH_SIDE: 34 | page_range.extend(range(0, ON_EACH_SIDE - 1)) 35 | page_range.extend(range(page_num - ON_EACH_SIDE, page_num + 1)) 36 | else: 37 | page_range.extend(range(0, page_num + 1)) 38 | if page_num < (paginator.num_pages - ON_EACH_SIDE - 1): 39 | page_range.extend(range(page_num + 1, page_num + ON_EACH_SIDE + 1)) 40 | page_range.extend(range(paginator.num_pages, paginator.num_pages)) 41 | else: 42 | page_range.extend(range(page_num + 1, paginator.num_pages)) 43 | 44 | need_show_all_link = cl.can_show_all and not cl.show_all and cl.multi_page 45 | return render_to_string(( 46 | 'mobileadmin/%s/pagination.html' % user_agent, 47 | 'mobileadmin/pagination.html', 48 | 'admin/pagination.html' 49 | ), { 50 | 'cl': cl, 51 | 'pagination_required': pagination_required, 52 | 'show_all_url': need_show_all_link and cl.get_query_string({ALL_VAR: ''}), 53 | 'page_range': page_range, 54 | 'ALL_VAR': ALL_VAR, 55 | '1': 1, 56 | }) 57 | register.simple_tag(pagination) 58 | 59 | def search_form(cl, user_agent): 60 | return render_to_string(( 61 | 'mobileadmin/%s/search_form.html' % user_agent, 62 | 'mobileadmin/search_form.html', 63 | 'admin/search_form.html' 64 | ), { 65 | 'cl': cl, 66 | 'show_result_count': cl.result_count != cl.full_result_count and not cl.opts.one_to_one_field, 67 | 'search_var': SEARCH_VAR 68 | }) 69 | register.simple_tag(search_form) 70 | 71 | def admin_list_filter(cl, spec, user_agent): 72 | return render_to_string(( 73 | 'mobileadmin/%s/filter.html' % user_agent, 74 | 'mobileadmin/filter.html', 75 | 'admin/filter.html' 76 | ), { 77 | 'title': spec.title(), 78 | 'choices': list(spec.choices(cl)), 79 | }) 80 | register.simple_tag(admin_list_filter) 81 | -------------------------------------------------------------------------------- /mobileadmin/templatetags/mobile_admin_media.py: -------------------------------------------------------------------------------- 1 | from django.template import Library 2 | 3 | register = Library() 4 | 5 | def mobileadmin_media_prefix(): 6 | """ 7 | Returns the string contained in the setting MOBILEADMIN_MEDIA_PREFIX. 8 | """ 9 | try: 10 | from mobileadmin.conf import settings 11 | except ImportError: 12 | return '' 13 | return settings.MEDIA_PREFIX 14 | mobileadmin_media_prefix = register.simple_tag(mobileadmin_media_prefix) 15 | -------------------------------------------------------------------------------- /mobileadmin/templatetags/mobile_admin_modify.py: -------------------------------------------------------------------------------- 1 | import re 2 | from django import template 3 | from django.template.loader import render_to_string 4 | register = template.Library() 5 | 6 | admin_re = re.compile(r'^admin\/') 7 | 8 | def prepopulated_fields_js(context): 9 | """ 10 | Creates a list of prepopulated_fields that should render Javascript for 11 | the prepopulated fields for both the admin form and inlines. 12 | """ 13 | prepopulated_fields = [] 14 | if context['add'] and 'adminform' in context: 15 | prepopulated_fields.extend(context['adminform'].prepopulated_fields) 16 | if 'inline_admin_formsets' in context: 17 | for inline_admin_formset in context['inline_admin_formsets']: 18 | for inline_admin_form in inline_admin_formset: 19 | if inline_admin_form.original is None: 20 | prepopulated_fields.extend(inline_admin_form.prepopulated_fields) 21 | context.update({'prepopulated_fields': prepopulated_fields}) 22 | return context 23 | prepopulated_fields_js = register.inclusion_tag('admin/prepopulated_fields_js.html', takes_context=True)(prepopulated_fields_js) 24 | 25 | def mobile_inline_admin_formset(inline_admin_formset, user_agent): 26 | template_name = inline_admin_formset.opts.template 27 | if admin_re.match(template_name): 28 | # remove admin/ prefix to have a clean template name 29 | template_name = admin_re.sub('', template_name) 30 | return render_to_string(( 31 | 'mobileadmin/%s/%s' % (user_agent, template_name), 32 | 'mobileadmin/%s' % template_name, 33 | 'admin/%s' % template_name, 34 | ), { 35 | 'inline_admin_formset': inline_admin_formset, 36 | 'user_agent': user_agent, 37 | }) 38 | register.simple_tag(mobile_inline_admin_formset) 39 | 40 | def mobile_inline_admin_fieldset(fieldset, user_agent): 41 | return render_to_string(( 42 | 'mobileadmin/%s/includes/fieldset.html' % user_agent, 43 | 'mobileadmin/includes/fieldset.html', 44 | 'admin/includes/fieldset.html', 45 | ), { 46 | 'fieldset': fieldset, 47 | }) 48 | register.simple_tag(mobile_inline_admin_fieldset) 49 | -------------------------------------------------------------------------------- /mobileadmin/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | import mobileadmin 3 | 4 | urlpatterns = patterns('', 5 | (r'^(.*)', mobileadmin.site.root), 6 | ) 7 | -------------------------------------------------------------------------------- /mobileadmin/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | from mobileadmin.conf import settings 3 | 4 | def get_user_agent(request): 5 | """ 6 | Checks if the given user agent string matches one of the valid user 7 | agents. 8 | """ 9 | name = request.META.get('HTTP_USER_AGENT', None) 10 | if not name: 11 | return False 12 | for platform, regex in settings.USER_AGENTS.iteritems(): 13 | if re.compile(regex).search(name) is not None: 14 | return platform.lower() 15 | return False 16 | -------------------------------------------------------------------------------- /mobileadmin/views.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.http import HttpResponseRedirect, HttpResponseNotFound, HttpResponseServerError 3 | from django.views import defaults 4 | from django.shortcuts import render_to_response 5 | from django.core.exceptions import PermissionDenied 6 | from django.template import Context, RequestContext, loader 7 | from django.utils.translation import ugettext, ugettext_lazy as _ 8 | 9 | from mobileadmin import utils 10 | 11 | def auth_add_view(self, request): 12 | if not self.has_change_permission(request): 13 | raise PermissionDenied 14 | template_list = ['admin/auth/user/add_form.html'] 15 | user_agent = utils.get_user_agent(request) 16 | if user_agent: 17 | template_list = [ 18 | 'mobileadmin/%s/auth/user/add_form.html' % user_agent, 19 | ] + template_list 20 | if request.method == 'POST': 21 | form = self.add_form(request.POST) 22 | if form.is_valid(): 23 | new_user = form.save() 24 | msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name': 'user', 'obj': new_user} 25 | self.log_addition(request, new_user) 26 | if "_addanother" in request.POST: 27 | request.user.message_set.create(message=msg) 28 | return HttpResponseRedirect(request.path) 29 | elif '_popup' in request.REQUEST: 30 | return self.response_add(request, new_user) 31 | else: 32 | request.user.message_set.create(message=msg + ' ' + ugettext("You may edit it again below.")) 33 | return HttpResponseRedirect('../%s/' % new_user.id) 34 | else: 35 | form = self.add_form() 36 | return render_to_response(template_list, { 37 | 'title': _('Add user'), 38 | 'form': form, 39 | 'is_popup': '_popup' in request.REQUEST, 40 | 'add': True, 41 | 'change': False, 42 | 'has_add_permission': True, 43 | 'has_delete_permission': False, 44 | 'has_change_permission': True, 45 | 'has_file_field': False, 46 | 'has_absolute_url': False, 47 | 'auto_populated_fields': (), 48 | 'opts': self.model._meta, 49 | 'save_as': False, 50 | 'username_help_text': self.model._meta.get_field('username').help_text, 51 | 'root_path': self.admin_site.root_path, 52 | 'app_label': self.model._meta.app_label, 53 | }, context_instance=template.RequestContext(request)) 54 | 55 | def page_not_found(request, template_name='404.html'): 56 | """ 57 | Mobile 404 handler. 58 | 59 | Templates: `404.html` 60 | Context: 61 | request_path 62 | The path of the requested URL (e.g., '/app/pages/bad_page/') 63 | """ 64 | user_agent = utils.get_user_agent(request) 65 | if user_agent: 66 | template_list = ( 67 | 'mobileadmin/%s/404.html' % user_agent, 68 | template_name, 69 | ) 70 | return HttpResponseNotFound(loader.render_to_string(template_list, { 71 | 'request_path': request.path, 72 | }, context_instance=RequestContext(request))) 73 | return defaults.page_not_found(request, template_name) 74 | 75 | def server_error(request, template_name='500.html'): 76 | """ 77 | Mobile 500 error handler. 78 | 79 | Templates: `500.html` 80 | Context: None 81 | """ 82 | user_agent = utils.get_user_agent(request) 83 | if user_agent: 84 | template_list = ( 85 | 'mobileadmin/%s/500.html' % user_agent, 86 | template_name, 87 | ) 88 | return HttpResponseServerError(loader.render_to_string(template_list)) 89 | return defaults.server_error(request, template_name) 90 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from distutils.core import setup 3 | from distutils.command.install import INSTALL_SCHEMES 4 | 5 | app_name = "mobileadmin" 6 | version = "0.5.2" 7 | 8 | # Tell distutils to put the data_files in platform-specific installation 9 | # locations. See here for an explanation: 10 | # http://groups.google.com/group/comp.lang.python/browse_thread/thread/35ec7b2fed36eaec/2105ee4d9e8042cb 11 | for scheme in INSTALL_SCHEMES.values(): 12 | scheme['data'] = scheme['purelib'] 13 | 14 | # Compile the list of packages available, because distutils doesn't have 15 | # an easy way to do this. 16 | packages, data_files = [], [] 17 | root_dir = os.path.dirname(__file__) 18 | if root_dir: 19 | os.chdir(root_dir) 20 | 21 | for dirpath, dirnames, filenames in os.walk(app_name): 22 | # Ignore dirnames that start with '.' 23 | for i, dirname in enumerate(dirnames): 24 | if dirname.startswith('.'): del dirnames[i] 25 | if '__init__.py' in filenames: 26 | pkg = dirpath.replace(os.path.sep, '.') 27 | if os.path.altsep: 28 | pkg = pkg.replace(os.path.altsep, '.') 29 | packages.append(pkg) 30 | elif filenames: 31 | prefix = dirpath[len(app_name)+1:] # Strip "app_name/" or "app_name\" 32 | for f in filenames: 33 | data_files.append(os.path.join(prefix, f)) 34 | 35 | setup(name='django-'+app_name, 36 | version=version, 37 | description='The Django admin interface for mobile devices.', 38 | long_description=open('docs/overview.rst').read(), 39 | author='Jannis Leidel', 40 | author_email='jannis@leidel.info', 41 | url='http://code.google.com/p/django-%s/' % app_name, 42 | package_dir={app_name: app_name}, 43 | packages=packages, 44 | package_data={app_name: data_files}, 45 | classifiers=['Development Status :: 4 - Beta', 46 | 'Environment :: Web Environment', 47 | 'Intended Audience :: Developers', 48 | 'License :: OSI Approved :: BSD License', 49 | 'Operating System :: OS Independent', 50 | 'Programming Language :: Python', 51 | 'Topic :: Utilities'], 52 | zip_safe=False, 53 | ) 54 | --------------------------------------------------------------------------------