mirror of
https://github.com/xhongc/music-tag-web.git
synced 2026-05-05 11:54:35 +08:00
feature:合并艺术家和专辑
This commit is contained in:
152
applications/music/adminx.py
Normal file
152
applications/music/adminx.py
Normal file
@@ -0,0 +1,152 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import xadmin
|
||||
from .models import Album, Track, Artist, Genre, Attachment, Playlist, TrackFavorite, Folder
|
||||
|
||||
|
||||
class AlbumAdmin(object):
|
||||
list_display = (
|
||||
'id',
|
||||
'name',
|
||||
'artist',
|
||||
'all_artist_ids',
|
||||
'max_year',
|
||||
'song_count',
|
||||
'plays_count',
|
||||
'duration',
|
||||
'genre',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'accessed_date',
|
||||
'full_text',
|
||||
'size',
|
||||
'comment',
|
||||
'paths',
|
||||
'description',
|
||||
'attachment_cover',
|
||||
)
|
||||
list_filter = (
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'accessed_date',
|
||||
'external_info_updated_at',
|
||||
)
|
||||
search_fields = ('name',)
|
||||
date_hierarchy = 'created_at'
|
||||
list_per_page = 10
|
||||
|
||||
|
||||
class TrackAdmin(object):
|
||||
list_display = (
|
||||
'id',
|
||||
'name',
|
||||
'path',
|
||||
'album',
|
||||
'artist',
|
||||
'has_cover_art',
|
||||
'track_number',
|
||||
'disc_number',
|
||||
'plays_count',
|
||||
'year',
|
||||
'size',
|
||||
'suffix',
|
||||
'mimetype',
|
||||
'duration',
|
||||
'bit_rate',
|
||||
'genre',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'accessed_date',
|
||||
'full_text',
|
||||
)
|
||||
list_filter = (
|
||||
'has_cover_art',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'accessed_date',
|
||||
)
|
||||
search_fields = ('name',)
|
||||
date_hierarchy = 'created_at'
|
||||
list_per_page = 10
|
||||
|
||||
|
||||
class ArtistAdmin(object):
|
||||
list_display = (
|
||||
'id',
|
||||
'name',
|
||||
'album_count',
|
||||
'full_text',
|
||||
'song_count',
|
||||
'size',
|
||||
'mbz_artist_id',
|
||||
'attachment_cover',
|
||||
'similar_artists',
|
||||
'external_url',
|
||||
'external_info_updated_at',
|
||||
)
|
||||
list_filter = ('attachment_cover', 'external_info_updated_at')
|
||||
search_fields = ('name',)
|
||||
|
||||
list_per_page = 10
|
||||
|
||||
|
||||
class GenreAdmin(object):
|
||||
list_display = ('id', 'name')
|
||||
search_fields = ('name',)
|
||||
|
||||
|
||||
class AttachmentAdmin(object):
|
||||
list_display = (
|
||||
'id',
|
||||
'url',
|
||||
'creation_date',
|
||||
'last_fetch_date',
|
||||
'size',
|
||||
'mimetype',
|
||||
'file',
|
||||
)
|
||||
list_filter = ('creation_date', 'last_fetch_date')
|
||||
|
||||
|
||||
class PlaylistAdmin(object):
|
||||
list_display = (
|
||||
'id',
|
||||
'name',
|
||||
'user',
|
||||
'creation_date',
|
||||
'modification_date',
|
||||
'privacy_level',
|
||||
)
|
||||
list_filter = ('user', 'creation_date', 'modification_date')
|
||||
search_fields = ('name',)
|
||||
|
||||
|
||||
class TrackFavoriteAdmin(object):
|
||||
list_display = ('id', 'creation_date', 'user', 'track')
|
||||
list_filter = ('creation_date', 'user', 'track')
|
||||
|
||||
|
||||
class FolderAdmin(object):
|
||||
list_display = (
|
||||
'id',
|
||||
'name',
|
||||
'path',
|
||||
'created_at',
|
||||
'last_scan_time',
|
||||
'file_type',
|
||||
'uid',
|
||||
'parent_id',
|
||||
)
|
||||
list_filter = ('created_at', 'last_scan_time', "file_type", "state")
|
||||
search_fields = ('name',)
|
||||
date_hierarchy = 'created_at'
|
||||
list_per_page = 10
|
||||
|
||||
|
||||
xadmin.site.register(Album, AlbumAdmin)
|
||||
xadmin.site.register(Track, TrackAdmin)
|
||||
xadmin.site.register(Artist, ArtistAdmin)
|
||||
xadmin.site.register(Genre, GenreAdmin)
|
||||
xadmin.site.register(Attachment, AttachmentAdmin)
|
||||
xadmin.site.register(Playlist, PlaylistAdmin)
|
||||
xadmin.site.register(TrackFavorite, TrackFavoriteAdmin)
|
||||
xadmin.site.register(Folder, FolderAdmin)
|
||||
60
applications/music/migrations/0004_auto_20230519_1322.py
Normal file
60
applications/music/migrations/0004_auto_20230519_1322.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# Generated by Django 2.2.6 on 2023-05-19 13:22
|
||||
|
||||
import datetime
|
||||
from django.db import migrations, models
|
||||
import django_mysql.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('music', '0003_folder_state'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='album',
|
||||
name='accessed_date',
|
||||
field=models.DateTimeField(blank=True, null=True, verbose_name='访问时间'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='album',
|
||||
name='all_artist_ids',
|
||||
field=django_mysql.models.ListTextField(models.IntegerField(), blank=True, default=list, null=True, size=None),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='album',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(default=datetime.datetime.now, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='album',
|
||||
name='external_info_updated_at',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='album',
|
||||
name='external_url',
|
||||
field=models.CharField(blank=True, default='', max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='album',
|
||||
name='mbz_album_artist_id',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='album',
|
||||
name='mbz_album_comment',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='album',
|
||||
name='mbz_album_id',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='album',
|
||||
name='mbz_album_type',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
28
applications/music/migrations/0005_auto_20230519_1323.py
Normal file
28
applications/music/migrations/0005_auto_20230519_1323.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 2.2.6 on 2023-05-19 13:23
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('music', '0004_auto_20230519_1322'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='album',
|
||||
name='comment',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='album',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, default='', max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='album',
|
||||
name='paths',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
@@ -13,33 +13,33 @@ class Album(models.Model):
|
||||
name = models.CharField("专辑名称", max_length=255, default='', null=False)
|
||||
artist = models.ForeignKey('Artist', on_delete=models.SET_NULL, null=True, related_name='albums',
|
||||
db_constraint=False)
|
||||
all_artist_ids = ListTextField(base_field=models.IntegerField(), default=list)
|
||||
all_artist_ids = ListTextField(base_field=models.IntegerField(), default=list, null=True, blank=True)
|
||||
|
||||
max_year = models.IntegerField(default=0, null=False)
|
||||
song_count = models.IntegerField("歌曲统计", default=-1, null=False)
|
||||
plays_count = models.IntegerField("播放次数", default=0, null=False)
|
||||
duration = models.FloatField("歌曲时长s", default=0, null=False)
|
||||
genre = models.ForeignKey('Genre', on_delete=models.SET_NULL, null=True, related_name='albums', db_constraint=False)
|
||||
created_at = models.DateTimeField(null=True)
|
||||
created_at = models.DateTimeField(null=True, default=datetime.now)
|
||||
updated_at = models.DateTimeField(null=True, auto_now=True)
|
||||
accessed_date = models.DateTimeField("访问时间", null=True)
|
||||
accessed_date = models.DateTimeField("访问时间", null=True, blank=True)
|
||||
|
||||
full_text = models.CharField(max_length=255, default='', null=True, blank=True)
|
||||
size = models.IntegerField("文件大小", default=0, null=False)
|
||||
comment = models.CharField(max_length=255, null=True)
|
||||
paths = models.CharField(max_length=255, null=True)
|
||||
description = models.CharField(max_length=255, default='', null=True)
|
||||
comment = models.CharField(max_length=255, null=True, blank=True)
|
||||
paths = models.CharField(max_length=255, null=True, blank=True)
|
||||
description = models.CharField(max_length=255, default='', null=True, blank=True)
|
||||
attachment_cover = models.ForeignKey('Attachment', on_delete=models.SET_NULL, null=True, related_name='album_cover',
|
||||
db_constraint=False)
|
||||
|
||||
# musicbrainz fields
|
||||
mbz_album_id = models.CharField(max_length=255, null=True)
|
||||
mbz_album_artist_id = models.CharField(max_length=255, null=True)
|
||||
mbz_album_type = models.CharField(max_length=255, null=True)
|
||||
mbz_album_comment = models.CharField(max_length=255, null=True)
|
||||
mbz_album_id = models.CharField(max_length=255, null=True, blank=True)
|
||||
mbz_album_artist_id = models.CharField(max_length=255, null=True, blank=True)
|
||||
mbz_album_type = models.CharField(max_length=255, null=True, blank=True)
|
||||
mbz_album_comment = models.CharField(max_length=255, null=True, blank=True)
|
||||
|
||||
external_url = models.CharField(max_length=255, default='', null=True)
|
||||
external_info_updated_at = models.DateTimeField(null=True)
|
||||
external_url = models.CharField(max_length=255, default='', null=True, blank=True)
|
||||
external_info_updated_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "专辑"
|
||||
@@ -120,6 +120,9 @@ class Genre(models.Model):
|
||||
verbose_name = "风格"
|
||||
verbose_name_plural = "风格"
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Attachment(models.Model):
|
||||
# Remote URL where the attachment can be fetched
|
||||
|
||||
@@ -42,3 +42,7 @@ class FetchId3ByTitleSerializer(serializers.Serializer):
|
||||
class FetchLlyricSerializer(serializers.Serializer):
|
||||
song_id = serializers.CharField(required=True)
|
||||
resource = serializers.CharField(required=True)
|
||||
|
||||
|
||||
class MergeArtistSerializer(serializers.Serializer):
|
||||
full_text = serializers.CharField(required=True)
|
||||
|
||||
@@ -3,12 +3,14 @@ import copy
|
||||
import os
|
||||
|
||||
import music_tag
|
||||
from django.db import transaction
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
from rest_framework.decorators import action
|
||||
|
||||
from applications.music.models import Artist, Track, Album
|
||||
from applications.task.serialziers import FileListSerializer, Id3Serializer, UpdateId3Serializer, \
|
||||
FetchId3ByTitleSerializer, FetchLlyricSerializer, BatchUpdateId3Serializer
|
||||
FetchId3ByTitleSerializer, FetchLlyricSerializer, BatchUpdateId3Serializer, MergeArtistSerializer
|
||||
from applications.task.services.music_resource import MusicResource
|
||||
from applications.task.services.update_ids import update_music_info
|
||||
from applications.task.tasks import full_scan_folder, scan_music_id3, scan, clear_music
|
||||
@@ -31,6 +33,8 @@ class TaskViewSets(GenericViewSet):
|
||||
return FetchLlyricSerializer
|
||||
elif self.action == "batch_update_id3":
|
||||
return BatchUpdateId3Serializer
|
||||
elif self.action in ["merge_artist", "merge_album"]:
|
||||
return MergeArtistSerializer
|
||||
return FileListSerializer
|
||||
|
||||
@action(methods=['POST'], detail=False)
|
||||
@@ -216,3 +220,32 @@ class TaskViewSets(GenericViewSet):
|
||||
def full_scan_folder(self, request, *args, **kwargs):
|
||||
full_scan_folder.delay()
|
||||
return self.success_response()
|
||||
|
||||
@action(methods=['POST'], detail=False)
|
||||
def merge_artist(self, request, *args, **kwargs):
|
||||
validate_data = self.is_validated_data(request.data)
|
||||
full_text = validate_data["full_text"]
|
||||
artist_list = Artist.objects.filter(full_text=full_text).all()
|
||||
first_artist = artist_list[0]
|
||||
first_artist.name = full_text
|
||||
first_artist.save()
|
||||
with transaction.atomic():
|
||||
for artist in artist_list[1:]:
|
||||
Track.objects.filter(artist=artist).update(artist=first_artist)
|
||||
Album.objects.filter(artist=artist).update(artist=first_artist)
|
||||
Artist.objects.filter(id=artist.id).delete()
|
||||
return self.success_response()
|
||||
|
||||
@action(methods=['POST'], detail=False)
|
||||
def merge_album(self, request, *args, **kwargs):
|
||||
validate_data = self.is_validated_data(request.data)
|
||||
full_text = validate_data["full_text"]
|
||||
album_list = Album.objects.filter(full_text=full_text).all()
|
||||
first_album = album_list[0]
|
||||
first_album.name = full_text
|
||||
first_album.save()
|
||||
with transaction.atomic():
|
||||
for album in album_list[1:]:
|
||||
Track.objects.filter(album=album).update(album=first_album)
|
||||
Album.objects.filter(id=album.id).delete()
|
||||
return self.success_response()
|
||||
|
||||
@@ -27,6 +27,9 @@ INSTALLED_APPS = [
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
"rest_framework",
|
||||
'xadmin',
|
||||
'crispy_forms',
|
||||
'reversion',
|
||||
"applications.task",
|
||||
"applications.user",
|
||||
"applications.music",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import xadmin
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include, re_path
|
||||
@@ -10,6 +12,7 @@ from rest_framework_jwt.views import obtain_jwt_token
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('xadmin/', xadmin.site.urls),
|
||||
path('', index),
|
||||
re_path(r"^api/", include(task_router.urls)),
|
||||
re_path(r"^rest/", include(subsonic_router.urls)),
|
||||
|
||||
14
lib/xadmin/.tx/config
Normal file
14
lib/xadmin/.tx/config
Normal file
@@ -0,0 +1,14 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[xadmin-core.django]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/django.po
|
||||
source_file = locale/en/LC_MESSAGES/django.po
|
||||
source_lang = en
|
||||
type = PO
|
||||
|
||||
[xadmin-core.djangojs]
|
||||
file_filter = locale/<lang>/LC_MESSAGES/djangojs.po
|
||||
source_file = locale/en/LC_MESSAGES/djangojs.po
|
||||
source_lang = en
|
||||
type = PO
|
||||
70
lib/xadmin/__init__.py
Normal file
70
lib/xadmin/__init__.py
Normal file
@@ -0,0 +1,70 @@
|
||||
|
||||
VERSION = (0,6,0)
|
||||
|
||||
from xadmin.sites import AdminSite, site
|
||||
|
||||
class Settings(object):
|
||||
pass
|
||||
|
||||
|
||||
def autodiscover():
|
||||
"""
|
||||
Auto-discover INSTALLED_APPS admin.py modules and fail silently when
|
||||
not present. This forces an import on them to register any admin bits they
|
||||
may want.
|
||||
"""
|
||||
|
||||
from importlib import import_module
|
||||
from django.conf import settings
|
||||
from django.utils.module_loading import module_has_submodule
|
||||
from django.apps import apps
|
||||
|
||||
setattr(settings, 'CRISPY_TEMPLATE_PACK', 'bootstrap3')
|
||||
setattr(settings, 'CRISPY_CLASS_CONVERTERS', {
|
||||
"textinput": "textinput textInput form-control",
|
||||
"fileinput": "fileinput fileUpload form-control",
|
||||
"passwordinput": "textinput textInput form-control",
|
||||
})
|
||||
|
||||
from xadmin.views import register_builtin_views
|
||||
register_builtin_views(site)
|
||||
|
||||
# load xadmin settings from XADMIN_CONF module
|
||||
try:
|
||||
xadmin_conf = getattr(settings, 'XADMIN_CONF', 'xadmin_conf.py')
|
||||
conf_mod = import_module(xadmin_conf)
|
||||
except Exception:
|
||||
conf_mod = None
|
||||
|
||||
if conf_mod:
|
||||
for key in dir(conf_mod):
|
||||
setting = getattr(conf_mod, key)
|
||||
try:
|
||||
if issubclass(setting, Settings):
|
||||
site.register_settings(setting.__name__, setting)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
from xadmin.plugins import register_builtin_plugins
|
||||
register_builtin_plugins(site)
|
||||
|
||||
for app_config in apps.get_app_configs():
|
||||
mod = import_module(app_config.name)
|
||||
# Attempt to import the app's admin module.
|
||||
try:
|
||||
before_import_registry = site.copy_registry()
|
||||
import_module('%s.adminx' % app_config.name)
|
||||
except:
|
||||
# Reset the model registry to the state before the last import as
|
||||
# this import will have to reoccur on the next request and this
|
||||
# could raise NotRegistered and AlreadyRegistered exceptions
|
||||
# (see #8245).
|
||||
site.restore_registry(before_import_registry)
|
||||
|
||||
# Decide whether to bubble up this error. If the app just
|
||||
# doesn't have an admin module, we can ignore the error
|
||||
# attempting to import it, otherwise we want it to bubble up.
|
||||
if module_has_submodule(mod, 'adminx'):
|
||||
raise
|
||||
|
||||
default_app_config = 'xadmin.apps.XAdminConfig'
|
||||
32
lib/xadmin/adminx.py
Normal file
32
lib/xadmin/adminx.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from __future__ import absolute_import
|
||||
import xadmin
|
||||
from .models import UserSettings, Log
|
||||
from xadmin.layout import *
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _, ugettext
|
||||
|
||||
class UserSettingsAdmin(object):
|
||||
model_icon = 'fa fa-cog'
|
||||
hidden_menu = True
|
||||
|
||||
xadmin.site.register(UserSettings, UserSettingsAdmin)
|
||||
|
||||
class LogAdmin(object):
|
||||
|
||||
def link(self, instance):
|
||||
if instance.content_type and instance.object_id and instance.action_flag != 'delete':
|
||||
admin_url = self.get_admin_url('%s_%s_change' % (instance.content_type.app_label, instance.content_type.model),
|
||||
instance.object_id)
|
||||
return "<a href='%s'>%s</a>" % (admin_url, _('Admin Object'))
|
||||
else:
|
||||
return ''
|
||||
link.short_description = ""
|
||||
link.allow_tags = True
|
||||
link.is_column = False
|
||||
|
||||
list_display = ('action_time', 'user', 'ip_addr', '__str__', 'link')
|
||||
list_filter = ['user', 'action_time']
|
||||
search_fields = ['ip_addr', 'message']
|
||||
model_icon = 'fa fa-cog'
|
||||
|
||||
xadmin.site.register(Log, LogAdmin)
|
||||
15
lib/xadmin/apps.py
Normal file
15
lib/xadmin/apps.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from django.apps import AppConfig
|
||||
from django.core import checks
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
import xadmin
|
||||
|
||||
|
||||
class XAdminConfig(AppConfig):
|
||||
"""Simple AppConfig which does not do automatic discovery."""
|
||||
|
||||
name = 'xadmin'
|
||||
verbose_name = _("Administration")
|
||||
|
||||
def ready(self):
|
||||
self.module.autodiscover()
|
||||
setattr(xadmin,'site',xadmin.site)
|
||||
573
lib/xadmin/filters.py
Normal file
573
lib/xadmin/filters.py
Normal file
@@ -0,0 +1,573 @@
|
||||
from __future__ import absolute_import
|
||||
from django.db import models
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.utils.encoding import smart_text
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils import timezone
|
||||
from django.template.loader import get_template
|
||||
from django.template.context import Context
|
||||
from django.utils import six
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.html import escape, format_html
|
||||
from django.utils.text import Truncator
|
||||
from django.core.cache import cache, caches
|
||||
|
||||
from xadmin.views.list import EMPTY_CHANGELIST_VALUE
|
||||
from xadmin.util import is_related_field, is_related_field2
|
||||
import datetime
|
||||
|
||||
FILTER_PREFIX = '_p_'
|
||||
SEARCH_VAR = '_q_'
|
||||
|
||||
from .util import (get_model_from_relation,
|
||||
reverse_field_path, get_limit_choices_to_from_path, prepare_lookup_value)
|
||||
|
||||
|
||||
class BaseFilter(object):
|
||||
title = None
|
||||
template = 'xadmin/filters/list.html'
|
||||
|
||||
@classmethod
|
||||
def test(cls, field, request, params, model, admin_view, field_path):
|
||||
pass
|
||||
|
||||
def __init__(self, request, params, model, admin_view):
|
||||
self.used_params = {}
|
||||
self.request = request
|
||||
self.params = params
|
||||
self.model = model
|
||||
self.admin_view = admin_view
|
||||
|
||||
if self.title is None:
|
||||
raise ImproperlyConfigured(
|
||||
"The filter '%s' does not specify "
|
||||
"a 'title'." % self.__class__.__name__)
|
||||
|
||||
def query_string(self, new_params=None, remove=None):
|
||||
return self.admin_view.get_query_string(new_params, remove)
|
||||
|
||||
def form_params(self):
|
||||
arr = map(lambda k: FILTER_PREFIX + k, self.used_params.keys())
|
||||
if six.PY3:
|
||||
arr = list(arr)
|
||||
return self.admin_view.get_form_params(remove=arr)
|
||||
|
||||
def has_output(self):
|
||||
"""
|
||||
Returns True if some choices would be output for this filter.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def is_used(self):
|
||||
return len(self.used_params) > 0
|
||||
|
||||
def do_filte(self, queryset):
|
||||
"""
|
||||
Returns the filtered queryset.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_context(self):
|
||||
return {'title': self.title, 'spec': self, 'form_params': self.form_params()}
|
||||
|
||||
def __str__(self):
|
||||
tpl = get_template(self.template)
|
||||
return mark_safe(tpl.render(context=self.get_context()))
|
||||
|
||||
|
||||
class FieldFilterManager(object):
|
||||
_field_list_filters = []
|
||||
_take_priority_index = 0
|
||||
|
||||
def register(self, list_filter_class, take_priority=False):
|
||||
if take_priority:
|
||||
# This is to allow overriding the default filters for certain types
|
||||
# of fields with some custom filters. The first found in the list
|
||||
# is used in priority.
|
||||
self._field_list_filters.insert(
|
||||
self._take_priority_index, list_filter_class)
|
||||
self._take_priority_index += 1
|
||||
else:
|
||||
self._field_list_filters.append(list_filter_class)
|
||||
return list_filter_class
|
||||
|
||||
def create(self, field, request, params, model, admin_view, field_path):
|
||||
for list_filter_class in self._field_list_filters:
|
||||
if not list_filter_class.test(field, request, params, model, admin_view, field_path):
|
||||
continue
|
||||
return list_filter_class(field, request, params,
|
||||
model, admin_view, field_path=field_path)
|
||||
|
||||
manager = FieldFilterManager()
|
||||
|
||||
|
||||
class FieldFilter(BaseFilter):
|
||||
|
||||
lookup_formats = {}
|
||||
|
||||
def __init__(self, field, request, params, model, admin_view, field_path):
|
||||
self.field = field
|
||||
self.field_path = field_path
|
||||
self.title = getattr(field, 'verbose_name', field_path)
|
||||
self.context_params = {}
|
||||
|
||||
super(FieldFilter, self).__init__(request, params, model, admin_view)
|
||||
|
||||
for name, format in self.lookup_formats.items():
|
||||
p = format % field_path
|
||||
self.context_params["%s_name" % name] = FILTER_PREFIX + p
|
||||
if p in params:
|
||||
value = prepare_lookup_value(p, params.pop(p))
|
||||
self.used_params[p] = value
|
||||
self.context_params["%s_val" % name] = value
|
||||
else:
|
||||
self.context_params["%s_val" % name] = ''
|
||||
|
||||
arr = map(
|
||||
lambda kv: setattr(self, 'lookup_' + kv[0], kv[1]),
|
||||
self.context_params.items()
|
||||
)
|
||||
if six.PY3:
|
||||
list(arr)
|
||||
|
||||
def get_context(self):
|
||||
context = super(FieldFilter, self).get_context()
|
||||
context.update(self.context_params)
|
||||
obj = map(lambda k: FILTER_PREFIX + k, self.used_params.keys())
|
||||
if six.PY3:
|
||||
obj = list(obj)
|
||||
context['remove_url'] = self.query_string({}, obj)
|
||||
return context
|
||||
|
||||
def has_output(self):
|
||||
return True
|
||||
|
||||
def do_filte(self, queryset):
|
||||
return queryset.filter(**self.used_params)
|
||||
|
||||
|
||||
class ListFieldFilter(FieldFilter):
|
||||
template = 'xadmin/filters/list.html'
|
||||
|
||||
def get_context(self):
|
||||
context = super(ListFieldFilter, self).get_context()
|
||||
context['choices'] = list(self.choices())
|
||||
return context
|
||||
|
||||
|
||||
@manager.register
|
||||
class BooleanFieldListFilter(ListFieldFilter):
|
||||
lookup_formats = {'exact': '%s__exact', 'isnull': '%s__isnull'}
|
||||
|
||||
@classmethod
|
||||
def test(cls, field, request, params, model, admin_view, field_path):
|
||||
return isinstance(field, (models.BooleanField, models.NullBooleanField))
|
||||
|
||||
def choices(self):
|
||||
for lookup, title in (
|
||||
('', _('All')),
|
||||
('1', _('Yes')),
|
||||
('0', _('No')),
|
||||
):
|
||||
yield {
|
||||
'selected': (
|
||||
self.lookup_exact_val == lookup
|
||||
and not self.lookup_isnull_val
|
||||
),
|
||||
'query_string': self.query_string(
|
||||
{self.lookup_exact_name: lookup},
|
||||
[self.lookup_isnull_name],
|
||||
),
|
||||
'display': title,
|
||||
}
|
||||
if isinstance(self.field, models.NullBooleanField):
|
||||
yield {
|
||||
'selected': self.lookup_isnull_val == 'True',
|
||||
'query_string': self.query_string(
|
||||
{self.lookup_isnull_name: 'True'},
|
||||
[self.lookup_exact_name],
|
||||
),
|
||||
'display': _('Unknown'),
|
||||
}
|
||||
|
||||
|
||||
@manager.register
|
||||
class ChoicesFieldListFilter(ListFieldFilter):
|
||||
lookup_formats = {'exact': '%s__exact'}
|
||||
|
||||
@classmethod
|
||||
def test(cls, field, request, params, model, admin_view, field_path):
|
||||
return bool(field.choices)
|
||||
|
||||
def choices(self):
|
||||
yield {
|
||||
'selected': self.lookup_exact_val is '',
|
||||
'query_string': self.query_string({}, [self.lookup_exact_name]),
|
||||
'display': _('All')
|
||||
}
|
||||
for lookup, title in self.field.flatchoices:
|
||||
yield {
|
||||
'selected': smart_text(lookup) == self.lookup_exact_val,
|
||||
'query_string': self.query_string({self.lookup_exact_name: lookup}),
|
||||
'display': title,
|
||||
}
|
||||
|
||||
|
||||
@manager.register
|
||||
class TextFieldListFilter(FieldFilter):
|
||||
template = 'xadmin/filters/char.html'
|
||||
lookup_formats = {'in': '%s__in', 'search': '%s__contains'}
|
||||
|
||||
@classmethod
|
||||
def test(cls, field, request, params, model, admin_view, field_path):
|
||||
return (
|
||||
isinstance(field, models.CharField)
|
||||
and field.max_length > 20
|
||||
or isinstance(field, models.TextField)
|
||||
)
|
||||
|
||||
|
||||
@manager.register
|
||||
class NumberFieldListFilter(FieldFilter):
|
||||
template = 'xadmin/filters/number.html'
|
||||
lookup_formats = {'equal': '%s__exact', 'lt': '%s__lt', 'gt': '%s__gt',
|
||||
'ne': '%s__ne', 'lte': '%s__lte', 'gte': '%s__gte',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def test(cls, field, request, params, model, admin_view, field_path):
|
||||
return isinstance(field, (models.DecimalField, models.FloatField, models.IntegerField))
|
||||
|
||||
def do_filte(self, queryset):
|
||||
params = self.used_params.copy()
|
||||
ne_key = '%s__ne' % self.field_path
|
||||
if ne_key in params:
|
||||
queryset = queryset.exclude(
|
||||
**{self.field_path: params.pop(ne_key)})
|
||||
return queryset.filter(**params)
|
||||
|
||||
|
||||
@manager.register
|
||||
class DateFieldListFilter(ListFieldFilter):
|
||||
template = 'xadmin/filters/date.html'
|
||||
lookup_formats = {'since': '%s__gte', 'until': '%s__lt',
|
||||
'year': '%s__year', 'month': '%s__month', 'day': '%s__day',
|
||||
'isnull': '%s__isnull'}
|
||||
|
||||
@classmethod
|
||||
def test(cls, field, request, params, model, admin_view, field_path):
|
||||
return isinstance(field, models.DateField)
|
||||
|
||||
def __init__(self, field, request, params, model, admin_view, field_path):
|
||||
self.field_generic = '%s__' % field_path
|
||||
self.date_params = dict([(FILTER_PREFIX + k, v) for k, v in params.items()
|
||||
if k.startswith(self.field_generic)])
|
||||
|
||||
super(DateFieldListFilter, self).__init__(
|
||||
field, request, params, model, admin_view, field_path)
|
||||
|
||||
now = timezone.now()
|
||||
# When time zone support is enabled, convert "now" to the user's time
|
||||
# zone so Django's definition of "Today" matches what the user expects.
|
||||
if now.tzinfo is not None:
|
||||
current_tz = timezone.get_current_timezone()
|
||||
now = now.astimezone(current_tz)
|
||||
if hasattr(current_tz, 'normalize'):
|
||||
# available for pytz time zones
|
||||
now = current_tz.normalize(now)
|
||||
|
||||
if isinstance(field, models.DateTimeField):
|
||||
today = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
else: # field is a models.DateField
|
||||
today = now.date()
|
||||
tomorrow = today + datetime.timedelta(days=1)
|
||||
|
||||
self.links = (
|
||||
(_('Any date'), {}),
|
||||
(_('Has date'), {
|
||||
self.lookup_isnull_name: False
|
||||
}),
|
||||
(_('Has no date'), {
|
||||
self.lookup_isnull_name: 'True'
|
||||
}),
|
||||
(_('Today'), {
|
||||
self.lookup_since_name: str(today),
|
||||
self.lookup_until_name: str(tomorrow),
|
||||
}),
|
||||
(_('Past 7 days'), {
|
||||
self.lookup_since_name: str(today - datetime.timedelta(days=7)),
|
||||
self.lookup_until_name: str(tomorrow),
|
||||
}),
|
||||
(_('This month'), {
|
||||
self.lookup_since_name: str(today.replace(day=1)),
|
||||
self.lookup_until_name: str(tomorrow),
|
||||
}),
|
||||
(_('This year'), {
|
||||
self.lookup_since_name: str(today.replace(month=1, day=1)),
|
||||
self.lookup_until_name: str(tomorrow),
|
||||
}),
|
||||
)
|
||||
|
||||
def get_context(self):
|
||||
context = super(DateFieldListFilter, self).get_context()
|
||||
context['choice_selected'] = bool(self.lookup_year_val) or bool(self.lookup_month_val) \
|
||||
or bool(self.lookup_day_val)
|
||||
return context
|
||||
|
||||
def choices(self):
|
||||
for title, param_dict in self.links:
|
||||
yield {
|
||||
'selected': self.date_params == param_dict,
|
||||
'query_string': self.query_string(
|
||||
param_dict, [FILTER_PREFIX + self.field_generic]),
|
||||
'display': title,
|
||||
}
|
||||
|
||||
|
||||
@manager.register
|
||||
class RelatedFieldSearchFilter(FieldFilter):
|
||||
template = 'xadmin/filters/fk_search.html'
|
||||
|
||||
@classmethod
|
||||
def test(cls, field, request, params, model, admin_view, field_path):
|
||||
if not is_related_field2(field):
|
||||
return False
|
||||
related_modeladmin = admin_view.admin_site._registry.get(
|
||||
get_model_from_relation(field))
|
||||
return related_modeladmin and getattr(related_modeladmin, 'relfield_style', None) in ('fk-ajax', 'fk-select')
|
||||
|
||||
def __init__(self, field, request, params, model, model_admin, field_path):
|
||||
other_model = get_model_from_relation(field)
|
||||
if hasattr(field, 'remote_field'):
|
||||
rel_name = field.remote_field.get_related_field().name
|
||||
else:
|
||||
rel_name = other_model._meta.pk.name
|
||||
|
||||
self.lookup_formats = {'in': '%%s__%s__in' % rel_name, 'exact': '%%s__%s__exact' % rel_name}
|
||||
super(RelatedFieldSearchFilter, self).__init__(
|
||||
field, request, params, model, model_admin, field_path)
|
||||
|
||||
related_modeladmin = self.admin_view.admin_site._registry.get(other_model)
|
||||
self.relfield_style = related_modeladmin.relfield_style
|
||||
|
||||
if hasattr(field, 'verbose_name'):
|
||||
self.lookup_title = field.verbose_name
|
||||
else:
|
||||
self.lookup_title = other_model._meta.verbose_name
|
||||
self.title = self.lookup_title
|
||||
self.search_url = model_admin.get_admin_url('%s_%s_changelist' % (
|
||||
other_model._meta.app_label, other_model._meta.model_name))
|
||||
self.label = self.label_for_value(other_model, rel_name, self.lookup_exact_val) if self.lookup_exact_val else ""
|
||||
self.choices = '?'
|
||||
if field.remote_field.limit_choices_to:
|
||||
for i in list(field.remote_field.limit_choices_to):
|
||||
self.choices += "&_p_%s=%s" % (i, field.remote_field.limit_choices_to[i])
|
||||
self.choices = format_html(self.choices)
|
||||
|
||||
def label_for_value(self, other_model, rel_name, value):
|
||||
try:
|
||||
obj = other_model._default_manager.get(**{rel_name: value})
|
||||
return '%s' % escape(Truncator(obj).words(14, truncate='...'))
|
||||
except (ValueError, other_model.DoesNotExist):
|
||||
return ""
|
||||
|
||||
def get_context(self):
|
||||
context = super(RelatedFieldSearchFilter, self).get_context()
|
||||
context['search_url'] = self.search_url
|
||||
context['label'] = self.label
|
||||
context['choices'] = self.choices
|
||||
context['relfield_style'] = self.relfield_style
|
||||
return context
|
||||
|
||||
|
||||
@manager.register
|
||||
class RelatedFieldListFilter(ListFieldFilter):
|
||||
|
||||
@classmethod
|
||||
def test(cls, field, request, params, model, admin_view, field_path):
|
||||
return is_related_field2(field)
|
||||
|
||||
def __init__(self, field, request, params, model, model_admin, field_path):
|
||||
other_model = get_model_from_relation(field)
|
||||
if hasattr(field, 'remote_field'):
|
||||
rel_name = field.remote_field.get_related_field().name
|
||||
else:
|
||||
rel_name = other_model._meta.pk.name
|
||||
|
||||
self.lookup_formats = {'in': '%%s__%s__in' % rel_name, 'exact': '%%s__%s__exact' %
|
||||
rel_name, 'isnull': '%s__isnull'}
|
||||
self.lookup_choices = field.get_choices(include_blank=False)
|
||||
super(RelatedFieldListFilter, self).__init__(
|
||||
field, request, params, model, model_admin, field_path)
|
||||
|
||||
if hasattr(field, 'verbose_name'):
|
||||
self.lookup_title = field.verbose_name
|
||||
else:
|
||||
self.lookup_title = other_model._meta.verbose_name
|
||||
self.title = self.lookup_title
|
||||
|
||||
def has_output(self):
|
||||
if (is_related_field(self.field)
|
||||
and self.field.field.null or hasattr(self.field, 'remote_field')
|
||||
and self.field.null):
|
||||
extra = 1
|
||||
else:
|
||||
extra = 0
|
||||
return len(self.lookup_choices) + extra > 1
|
||||
|
||||
def expected_parameters(self):
|
||||
return [self.lookup_kwarg, self.lookup_kwarg_isnull]
|
||||
|
||||
def choices(self):
|
||||
yield {
|
||||
'selected': self.lookup_exact_val == '' and not self.lookup_isnull_val,
|
||||
'query_string': self.query_string({},
|
||||
[self.lookup_exact_name, self.lookup_isnull_name]),
|
||||
'display': _('All'),
|
||||
}
|
||||
for pk_val, val in self.lookup_choices:
|
||||
yield {
|
||||
'selected': self.lookup_exact_val == smart_text(pk_val),
|
||||
'query_string': self.query_string({
|
||||
self.lookup_exact_name: pk_val,
|
||||
}, [self.lookup_isnull_name]),
|
||||
'display': val,
|
||||
}
|
||||
if (is_related_field(self.field)
|
||||
and self.field.field.null or hasattr(self.field, 'remote_field')
|
||||
and self.field.null):
|
||||
yield {
|
||||
'selected': bool(self.lookup_isnull_val),
|
||||
'query_string': self.query_string({
|
||||
self.lookup_isnull_name: 'True',
|
||||
}, [self.lookup_exact_name]),
|
||||
'display': EMPTY_CHANGELIST_VALUE,
|
||||
}
|
||||
|
||||
|
||||
@manager.register
|
||||
class MultiSelectFieldListFilter(ListFieldFilter):
|
||||
""" Delegates the filter to the default filter and ors the results of each
|
||||
|
||||
Lists the distinct values of each field as a checkbox
|
||||
Uses the default spec for each
|
||||
|
||||
"""
|
||||
template = 'xadmin/filters/checklist.html'
|
||||
lookup_formats = {'in': '%s__in'}
|
||||
cache_config = {'enabled': False, 'key': 'quickfilter_%s', 'timeout': 3600, 'cache': 'default'}
|
||||
|
||||
@classmethod
|
||||
def test(cls, field, request, params, model, admin_view, field_path):
|
||||
return True
|
||||
|
||||
def get_cached_choices(self):
|
||||
if not self.cache_config['enabled']:
|
||||
return None
|
||||
c = caches(self.cache_config['cache'])
|
||||
return c.get(self.cache_config['key'] % self.field_path)
|
||||
|
||||
def set_cached_choices(self, choices):
|
||||
if not self.cache_config['enabled']:
|
||||
return
|
||||
c = caches(self.cache_config['cache'])
|
||||
return c.set(self.cache_config['key'] % self.field_path, choices)
|
||||
|
||||
def __init__(self, field, request, params, model, model_admin, field_path, field_order_by=None, field_limit=None, sort_key=None, cache_config=None):
|
||||
super(MultiSelectFieldListFilter, self).__init__(field, request, params, model, model_admin, field_path)
|
||||
|
||||
# Check for it in the cachce
|
||||
if cache_config is not None and type(cache_config) == dict:
|
||||
self.cache_config.update(cache_config)
|
||||
|
||||
if self.cache_config['enabled']:
|
||||
self.field_path = field_path
|
||||
choices = self.get_cached_choices()
|
||||
if choices:
|
||||
self.lookup_choices = choices
|
||||
return
|
||||
|
||||
# Else rebuild it
|
||||
queryset = self.admin_view.queryset().exclude(**{"%s__isnull" % field_path: True}).values_list(field_path, flat=True).distinct()
|
||||
#queryset = self.admin_view.queryset().distinct(field_path).exclude(**{"%s__isnull"%field_path:True})
|
||||
|
||||
if field_order_by is not None:
|
||||
# Do a subquery to order the distinct set
|
||||
queryset = self.admin_view.queryset().filter(id__in=queryset).order_by(field_order_by)
|
||||
|
||||
if field_limit is not None and type(field_limit) == int and queryset.count() > field_limit:
|
||||
queryset = queryset[:field_limit]
|
||||
|
||||
self.lookup_choices = [str(it) for it in queryset.values_list(field_path, flat=True) if str(it).strip() != ""]
|
||||
if sort_key is not None:
|
||||
self.lookup_choices = sorted(self.lookup_choices, key=sort_key)
|
||||
|
||||
if self.cache_config['enabled']:
|
||||
self.set_cached_choices(self.lookup_choices)
|
||||
|
||||
def choices(self):
|
||||
self.lookup_in_val = (type(self.lookup_in_val) in (tuple, list)) and self.lookup_in_val or list(self.lookup_in_val)
|
||||
yield {
|
||||
'selected': len(self.lookup_in_val) == 0,
|
||||
'query_string': self.query_string({}, [self.lookup_in_name]),
|
||||
'display': _('All'),
|
||||
}
|
||||
for val in self.lookup_choices:
|
||||
yield {
|
||||
'selected': smart_text(val) in self.lookup_in_val,
|
||||
'query_string': self.query_string({self.lookup_in_name: ",".join([val] + self.lookup_in_val), }),
|
||||
'remove_query_string': self.query_string({self.lookup_in_name: ",".join([v for v in self.lookup_in_val if v != val]), }),
|
||||
'display': val,
|
||||
}
|
||||
|
||||
|
||||
@manager.register
|
||||
class AllValuesFieldListFilter(ListFieldFilter):
|
||||
lookup_formats = {'exact': '%s__exact', 'isnull': '%s__isnull'}
|
||||
|
||||
@classmethod
|
||||
def test(cls, field, request, params, model, admin_view, field_path):
|
||||
return True
|
||||
|
||||
def __init__(self, field, request, params, model, admin_view, field_path):
|
||||
parent_model, reverse_path = reverse_field_path(model, field_path)
|
||||
queryset = parent_model._default_manager.all()
|
||||
# optional feature: limit choices base on existing relationships
|
||||
# queryset = queryset.complex_filter(
|
||||
# {'%s__isnull' % reverse_path: False})
|
||||
limit_choices_to = get_limit_choices_to_from_path(model, field_path)
|
||||
queryset = queryset.filter(limit_choices_to)
|
||||
|
||||
self.lookup_choices = (queryset
|
||||
.distinct()
|
||||
.order_by(field.name)
|
||||
.values_list(field.name, flat=True))
|
||||
super(AllValuesFieldListFilter, self).__init__(
|
||||
field, request, params, model, admin_view, field_path)
|
||||
|
||||
def choices(self):
|
||||
yield {
|
||||
'selected': (self.lookup_exact_val is '' and self.lookup_isnull_val is ''),
|
||||
'query_string': self.query_string({}, [self.lookup_exact_name, self.lookup_isnull_name]),
|
||||
'display': _('All'),
|
||||
}
|
||||
include_none = False
|
||||
for val in self.lookup_choices:
|
||||
if val is None:
|
||||
include_none = True
|
||||
continue
|
||||
val = smart_text(val)
|
||||
yield {
|
||||
'selected': self.lookup_exact_val == val,
|
||||
'query_string': self.query_string({self.lookup_exact_name: val},
|
||||
[self.lookup_isnull_name]),
|
||||
'display': val,
|
||||
}
|
||||
if include_none:
|
||||
yield {
|
||||
'selected': bool(self.lookup_isnull_val),
|
||||
'query_string': self.query_string({self.lookup_isnull_name: 'True'},
|
||||
[self.lookup_exact_name]),
|
||||
'display': EMPTY_CHANGELIST_VALUE,
|
||||
}
|
||||
47
lib/xadmin/forms.py
Normal file
47
lib/xadmin/forms.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from django import forms
|
||||
|
||||
from django.contrib.auth import authenticate
|
||||
from django.contrib.auth.forms import AuthenticationForm
|
||||
|
||||
from django.utils.translation import ugettext_lazy, ugettext as _
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
ERROR_MESSAGE = ugettext_lazy("Please enter the correct username and password "
|
||||
"for a staff account. Note that both fields are case-sensitive.")
|
||||
|
||||
|
||||
class AdminAuthenticationForm(AuthenticationForm):
|
||||
"""
|
||||
A custom authentication form used in the admin app.
|
||||
|
||||
"""
|
||||
this_is_the_login_form = forms.BooleanField(
|
||||
widget=forms.HiddenInput, initial=1,
|
||||
error_messages={'required': ugettext_lazy("Please log in again, because your session has expired.")})
|
||||
|
||||
def clean(self):
|
||||
username = self.cleaned_data.get('username')
|
||||
password = self.cleaned_data.get('password')
|
||||
message = ERROR_MESSAGE
|
||||
|
||||
if username and password:
|
||||
self.user_cache = authenticate(
|
||||
username=username, password=password)
|
||||
if self.user_cache is None:
|
||||
if u'@' in username:
|
||||
User = get_user_model()
|
||||
# Mistakenly entered e-mail address instead of username? Look it up.
|
||||
try:
|
||||
user = User.objects.get(email=username)
|
||||
except (User.DoesNotExist, User.MultipleObjectsReturned):
|
||||
# Nothing to do here, moving along.
|
||||
pass
|
||||
else:
|
||||
if user.check_password(password):
|
||||
message = _("Your e-mail address is not your username."
|
||||
" Try '%s' instead.") % user.username
|
||||
raise forms.ValidationError(message)
|
||||
elif not self.user_cache.is_active or not self.user_cache.is_staff:
|
||||
raise forms.ValidationError(message)
|
||||
return self.cleaned_data
|
||||
113
lib/xadmin/layout.py
Normal file
113
lib/xadmin/layout.py
Normal file
@@ -0,0 +1,113 @@
|
||||
from crispy_forms.helper import FormHelper
|
||||
from crispy_forms.layout import *
|
||||
from crispy_forms.bootstrap import *
|
||||
from crispy_forms.utils import render_field, flatatt, TEMPLATE_PACK
|
||||
|
||||
from crispy_forms import layout
|
||||
from crispy_forms import bootstrap
|
||||
|
||||
import math
|
||||
|
||||
|
||||
class Fieldset(layout.Fieldset):
|
||||
template = "xadmin/layout/fieldset.html"
|
||||
|
||||
def __init__(self, legend, *fields, **kwargs):
|
||||
self.description = kwargs.pop('description', None)
|
||||
self.collapsed = kwargs.pop('collapsed', None)
|
||||
super(Fieldset, self).__init__(legend, *fields, **kwargs)
|
||||
|
||||
|
||||
class Row(layout.Div):
|
||||
|
||||
def __init__(self, *fields, **kwargs):
|
||||
css_class = 'form-inline form-group'
|
||||
new_fields = [self.convert_field(f, len(fields)) for f in fields]
|
||||
super(Row, self).__init__(css_class=css_class, *new_fields, **kwargs)
|
||||
|
||||
def convert_field(self, f, counts):
|
||||
col_class = "col-sm-%d" % int(math.ceil(12 / counts))
|
||||
if not (isinstance(f, Field) or issubclass(f.__class__, Field)):
|
||||
f = layout.Field(f)
|
||||
if f.wrapper_class:
|
||||
f.wrapper_class += " %s" % col_class
|
||||
else:
|
||||
f.wrapper_class = col_class
|
||||
return f
|
||||
|
||||
|
||||
class Col(layout.Column):
|
||||
|
||||
def __init__(self, id, *fields, **kwargs):
|
||||
css_class = ['column', 'form-column', id, 'col col-sm-%d' %
|
||||
kwargs.get('span', 6)]
|
||||
if kwargs.get('horizontal'):
|
||||
css_class.append('form-horizontal')
|
||||
super(Col, self).__init__(css_class=' '.join(css_class), *
|
||||
fields, **kwargs)
|
||||
|
||||
|
||||
class Main(layout.Column):
|
||||
css_class = "column form-column main col col-sm-9 form-horizontal"
|
||||
|
||||
|
||||
class Side(layout.Column):
|
||||
css_class = "column form-column sidebar col col-sm-3"
|
||||
|
||||
|
||||
class Container(layout.Div):
|
||||
css_class = "form-container row clearfix"
|
||||
|
||||
|
||||
# Override bootstrap3
|
||||
class InputGroup(layout.Field):
|
||||
|
||||
template = "xadmin/layout/input_group.html"
|
||||
|
||||
def __init__(self, field, *args, **kwargs):
|
||||
self.field = field
|
||||
self.inputs = list(args)
|
||||
if '@@' not in args:
|
||||
self.inputs.append('@@')
|
||||
|
||||
self.input_size = None
|
||||
css_class = kwargs.get('css_class', '')
|
||||
if 'input-lg' in css_class:
|
||||
self.input_size = 'input-lg'
|
||||
if 'input-sm' in css_class:
|
||||
self.input_size = 'input-sm'
|
||||
|
||||
super(InputGroup, self).__init__(field, **kwargs)
|
||||
|
||||
def render(self, form, form_style, context, template_pack=TEMPLATE_PACK, **kwargs):
|
||||
classes = form.fields[self.field].widget.attrs.get('class', '')
|
||||
extra_context = {
|
||||
'inputs': self.inputs,
|
||||
'input_size': self.input_size,
|
||||
'classes': classes.replace('form-control', '')
|
||||
}
|
||||
if hasattr(self, 'wrapper_class'):
|
||||
extra_context['wrapper_class'] = self.wrapper_class
|
||||
|
||||
return render_field(
|
||||
self.field, form, form_style, context, template=self.template,
|
||||
attrs=self.attrs, template_pack=template_pack, extra_context=extra_context, **kwargs)
|
||||
|
||||
|
||||
class PrependedText(InputGroup):
|
||||
|
||||
def __init__(self, field, text, **kwargs):
|
||||
super(PrependedText, self).__init__(field, text, '@@', **kwargs)
|
||||
|
||||
|
||||
class AppendedText(InputGroup):
|
||||
|
||||
def __init__(self, field, text, **kwargs):
|
||||
super(AppendedText, self).__init__(field, '@@', text, **kwargs)
|
||||
|
||||
|
||||
class PrependedAppendedText(InputGroup):
|
||||
|
||||
def __init__(self, field, prepended_text=None, appended_text=None, *args, **kwargs):
|
||||
super(PrependedAppendedText, self).__init__(
|
||||
field, prepended_text, '@@', appended_text, **kwargs)
|
||||
BIN
lib/xadmin/locale/de_DE/LC_MESSAGES/django.mo
Normal file
BIN
lib/xadmin/locale/de_DE/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
1454
lib/xadmin/locale/de_DE/LC_MESSAGES/django.po
Normal file
1454
lib/xadmin/locale/de_DE/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
lib/xadmin/locale/de_DE/LC_MESSAGES/djangojs.mo
Normal file
BIN
lib/xadmin/locale/de_DE/LC_MESSAGES/djangojs.mo
Normal file
Binary file not shown.
72
lib/xadmin/locale/de_DE/LC_MESSAGES/djangojs.po
Normal file
72
lib/xadmin/locale/de_DE/LC_MESSAGES/djangojs.po
Normal file
@@ -0,0 +1,72 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
# Translators:
|
||||
# Azd325 <tim.kleinschmidt@gmail.com>, 2013
|
||||
# Azd325 <tim.kleinschmidt@gmail.com>, 2013
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: xadmin-core\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2013-04-30 23:11+0800\n"
|
||||
"PO-Revision-Date: 2013-11-20 12:41+0000\n"
|
||||
"Last-Translator: Azd325 <tim.kleinschmidt@gmail.com>\n"
|
||||
"Language-Team: German (Germany) (http://www.transifex.com/projects/p/xadmin/language/de_DE/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: de_DE\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: static/xadmin/js/xadmin.plugin.actions.js:20
|
||||
msgid "%(sel)s of %(cnt)s selected"
|
||||
msgid_plural "%(sel)s of %(cnt)s selected"
|
||||
msgstr[0] "%(sel)s von %(cnt)s markiert"
|
||||
msgstr[1] "%(sel)s von %(cnt)s markiert"
|
||||
|
||||
#: static/xadmin/js/xadmin.plugin.revision.js:25
|
||||
msgid "New Item"
|
||||
msgstr "Neues Element"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:32
|
||||
msgid "Sunday Monday Tuesday Wednesday Thursday Friday Saturday Sunday"
|
||||
msgstr "Sonntag Montag Dienstag Mittwoch Donnerstag Freitag Samstag Sonntag"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:33
|
||||
msgid "Sun Mon Tue Wed Thu Fri Sat Sun"
|
||||
msgstr "So Mo Di Mi Do Fr Sa So"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:34
|
||||
msgid "Su Mo Tu We Th Fr Sa Su"
|
||||
msgstr "So Mo Di Mi Do Fr Sa So"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:35
|
||||
msgid ""
|
||||
"January February March April May June July August September October November"
|
||||
" December"
|
||||
msgstr "Januar Februar März April Mai Juni Juli August September Oktober November Dezember"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:36
|
||||
msgid "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec"
|
||||
msgstr "Jan Feb Mär Apr Mai Jun Jul Aug Sep Okt Nov Dez"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:37
|
||||
msgid "Today"
|
||||
msgstr "Heute"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:38
|
||||
msgid "%a %d %b %Y %T %Z"
|
||||
msgstr "%a %d %b %Y %T %Z"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:39
|
||||
msgid "AM PM"
|
||||
msgstr "vorm nachm"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:40
|
||||
msgid "am pm"
|
||||
msgstr "vorm nachm"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:43
|
||||
msgid "%T"
|
||||
msgstr "%T"
|
||||
BIN
lib/xadmin/locale/en/LC_MESSAGES/django.mo
Normal file
BIN
lib/xadmin/locale/en/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
1411
lib/xadmin/locale/en/LC_MESSAGES/django.po
Normal file
1411
lib/xadmin/locale/en/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
lib/xadmin/locale/en/LC_MESSAGES/djangojs.mo
Normal file
BIN
lib/xadmin/locale/en/LC_MESSAGES/djangojs.mo
Normal file
Binary file not shown.
69
lib/xadmin/locale/en/LC_MESSAGES/djangojs.po
Normal file
69
lib/xadmin/locale/en/LC_MESSAGES/djangojs.po
Normal file
@@ -0,0 +1,69 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2013-04-30 23:11+0800\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: static/xadmin/js/xadmin.plugin.actions.js:20
|
||||
msgid "%(sel)s of %(cnt)s selected"
|
||||
msgid_plural "%(sel)s of %(cnt)s selected"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: static/xadmin/js/xadmin.plugin.revision.js:25
|
||||
msgid "New Item"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:32
|
||||
msgid "Sunday Monday Tuesday Wednesday Thursday Friday Saturday Sunday"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:33
|
||||
msgid "Sun Mon Tue Wed Thu Fri Sat Sun"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:34
|
||||
msgid "Su Mo Tu We Th Fr Sa Su"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:35
|
||||
msgid ""
|
||||
"January February March April May June July August September October November "
|
||||
"December"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:36
|
||||
msgid "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:37
|
||||
msgid "Today"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:38
|
||||
msgid "%a %d %b %Y %T %Z"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:39
|
||||
msgid "AM PM"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:40
|
||||
msgid "am pm"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:43
|
||||
msgid "%T"
|
||||
msgstr ""
|
||||
BIN
lib/xadmin/locale/es_MX/LC_MESSAGES/django.mo
Normal file
BIN
lib/xadmin/locale/es_MX/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
1556
lib/xadmin/locale/es_MX/LC_MESSAGES/django.po
Normal file
1556
lib/xadmin/locale/es_MX/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
lib/xadmin/locale/es_MX/LC_MESSAGES/djangojs.mo
Normal file
BIN
lib/xadmin/locale/es_MX/LC_MESSAGES/djangojs.mo
Normal file
Binary file not shown.
76
lib/xadmin/locale/es_MX/LC_MESSAGES/djangojs.po
Normal file
76
lib/xadmin/locale/es_MX/LC_MESSAGES/djangojs.po
Normal file
@@ -0,0 +1,76 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
# Translators:
|
||||
# byroncorrales <byroncorrales@gmail.com>, 2013
|
||||
# byroncorrales <byroncorrales@gmail.com>, 2013
|
||||
# sacrac <crocha09.09@gmail.com>, 2013
|
||||
# netoxico <me@netoxico.com>, 2013
|
||||
# netoxico <me@netoxico.com>, 2013
|
||||
# sacrac <crocha09.09@gmail.com>, 2013
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: xadmin-core\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2013-04-30 23:11+0800\n"
|
||||
"PO-Revision-Date: 2013-11-20 12:41+0000\n"
|
||||
"Last-Translator: sacrac <crocha09.09@gmail.com>\n"
|
||||
"Language-Team: Spanish (Mexico) (http://www.transifex.com/projects/p/xadmin/language/es_MX/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: es_MX\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: static/xadmin/js/xadmin.plugin.actions.js:20
|
||||
msgid "%(sel)s of %(cnt)s selected"
|
||||
msgid_plural "%(sel)s of %(cnt)s selected"
|
||||
msgstr[0] "%(sel)s de %(cnt)s seleccionado."
|
||||
msgstr[1] "%(sel)s de %(cnt)s seleccionado "
|
||||
|
||||
#: static/xadmin/js/xadmin.plugin.revision.js:25
|
||||
msgid "New Item"
|
||||
msgstr "Nuevo elemento"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:32
|
||||
msgid "Sunday Monday Tuesday Wednesday Thursday Friday Saturday Sunday"
|
||||
msgstr "Domingo Lunes Martes Miércoles Jueves Viernes Sábado Domingo"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:33
|
||||
msgid "Sun Mon Tue Wed Thu Fri Sat Sun"
|
||||
msgstr "Dom Lun Mar Mié Jue Vie Sáb Dom"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:34
|
||||
msgid "Su Mo Tu We Th Fr Sa Su"
|
||||
msgstr "Do Lu Ma Mi Ju Vi Sá Do"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:35
|
||||
msgid ""
|
||||
"January February March April May June July August September October November"
|
||||
" December"
|
||||
msgstr "Enero Febrero Marzo Abril Mayo Junio Julio Agosto Septiembre Octubre Noviembre Diciembre"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:36
|
||||
msgid "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec"
|
||||
msgstr "Ene Feb Mar Abr May Jun Jul Ago Sep Oct Nov Dic"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:37
|
||||
msgid "Today"
|
||||
msgstr "Hoy"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:38
|
||||
msgid "%a %d %b %Y %T %Z"
|
||||
msgstr "%a %d %b %Y %T %Z"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:39
|
||||
msgid "AM PM"
|
||||
msgstr "AM PM"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:40
|
||||
msgid "am pm"
|
||||
msgstr "am pm"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:43
|
||||
msgid "%T"
|
||||
msgstr "%T"
|
||||
BIN
lib/xadmin/locale/eu/LC_MESSAGES/django.mo
Normal file
BIN
lib/xadmin/locale/eu/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
1475
lib/xadmin/locale/eu/LC_MESSAGES/django.po
Normal file
1475
lib/xadmin/locale/eu/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
lib/xadmin/locale/eu/LC_MESSAGES/djangojs.mo
Normal file
BIN
lib/xadmin/locale/eu/LC_MESSAGES/djangojs.mo
Normal file
Binary file not shown.
71
lib/xadmin/locale/eu/LC_MESSAGES/djangojs.po
Normal file
71
lib/xadmin/locale/eu/LC_MESSAGES/djangojs.po
Normal file
@@ -0,0 +1,71 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
# Translators:
|
||||
# unaizalakain <unai@gisa-elkartea.org>, 2013
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: xadmin-core\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2013-04-30 23:11+0800\n"
|
||||
"PO-Revision-Date: 2013-11-20 12:41+0000\n"
|
||||
"Last-Translator: unaizalakain <unai@gisa-elkartea.org>\n"
|
||||
"Language-Team: Basque (http://www.transifex.com/projects/p/xadmin/language/eu/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: eu\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: static/xadmin/js/xadmin.plugin.actions.js:20
|
||||
msgid "%(sel)s of %(cnt)s selected"
|
||||
msgid_plural "%(sel)s of %(cnt)s selected"
|
||||
msgstr[0] "%(cnt)stik %(sel)s aukeratua"
|
||||
msgstr[1] "%(cnt)stik %(sel)s aukeratuak"
|
||||
|
||||
#: static/xadmin/js/xadmin.plugin.revision.js:25
|
||||
msgid "New Item"
|
||||
msgstr "Elementu Berria"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:32
|
||||
msgid "Sunday Monday Tuesday Wednesday Thursday Friday Saturday Sunday"
|
||||
msgstr "Igandea Astelehena Asteartea Asteazkena Osteguna Ostirala Larunbata Igandea"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:33
|
||||
msgid "Sun Mon Tue Wed Thu Fri Sat Sun"
|
||||
msgstr "Iga Atl Atr Atz Otg Otr Lar Iga"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:34
|
||||
msgid "Su Mo Tu We Th Fr Sa Su"
|
||||
msgstr "Ig At Ar Az Og Or La Ig"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:35
|
||||
msgid ""
|
||||
"January February March April May June July August September October November"
|
||||
" December"
|
||||
msgstr "Urtarrila Otsaila Martxoa Apirila Maiatza Ekaina Uztaila Abuztua Iraila Urria Azaroa Abendua"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:36
|
||||
msgid "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec"
|
||||
msgstr "Urt Ots Mar Api Mai Eka Uzt Abu Ira Urr Aza Abe"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:37
|
||||
msgid "Today"
|
||||
msgstr "Gaur"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:38
|
||||
msgid "%a %d %b %Y %T %Z"
|
||||
msgstr "%a %d %b %Y %T %Z"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:39
|
||||
msgid "AM PM"
|
||||
msgstr "AM PM"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:40
|
||||
msgid "am pm"
|
||||
msgstr "am pm"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:43
|
||||
msgid "%T"
|
||||
msgstr "%T"
|
||||
BIN
lib/xadmin/locale/id_ID/LC_MESSAGES/django.mo
Normal file
BIN
lib/xadmin/locale/id_ID/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
1403
lib/xadmin/locale/id_ID/LC_MESSAGES/django.po
Normal file
1403
lib/xadmin/locale/id_ID/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
lib/xadmin/locale/id_ID/LC_MESSAGES/djangojs.mo
Normal file
BIN
lib/xadmin/locale/id_ID/LC_MESSAGES/djangojs.mo
Normal file
Binary file not shown.
69
lib/xadmin/locale/id_ID/LC_MESSAGES/djangojs.po
Normal file
69
lib/xadmin/locale/id_ID/LC_MESSAGES/djangojs.po
Normal file
@@ -0,0 +1,69 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
# Translators:
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: xadmin-core\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2013-04-30 23:11+0800\n"
|
||||
"PO-Revision-Date: 2013-11-20 12:41+0000\n"
|
||||
"Last-Translator: sshwsfc <sshwsfc@gmail.com>\n"
|
||||
"Language-Team: Indonesian (Indonesia) (http://www.transifex.com/projects/p/xadmin/language/id_ID/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: id_ID\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
#: static/xadmin/js/xadmin.plugin.actions.js:20
|
||||
msgid "%(sel)s of %(cnt)s selected"
|
||||
msgid_plural "%(sel)s of %(cnt)s selected"
|
||||
msgstr[0] ""
|
||||
|
||||
#: static/xadmin/js/xadmin.plugin.revision.js:25
|
||||
msgid "New Item"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:32
|
||||
msgid "Sunday Monday Tuesday Wednesday Thursday Friday Saturday Sunday"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:33
|
||||
msgid "Sun Mon Tue Wed Thu Fri Sat Sun"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:34
|
||||
msgid "Su Mo Tu We Th Fr Sa Su"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:35
|
||||
msgid ""
|
||||
"January February March April May June July August September October November"
|
||||
" December"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:36
|
||||
msgid "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:37
|
||||
msgid "Today"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:38
|
||||
msgid "%a %d %b %Y %T %Z"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:39
|
||||
msgid "AM PM"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:40
|
||||
msgid "am pm"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:43
|
||||
msgid "%T"
|
||||
msgstr ""
|
||||
BIN
lib/xadmin/locale/ja/LC_MESSAGES/django.mo
Normal file
BIN
lib/xadmin/locale/ja/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
1403
lib/xadmin/locale/ja/LC_MESSAGES/django.po
Normal file
1403
lib/xadmin/locale/ja/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
lib/xadmin/locale/ja/LC_MESSAGES/djangojs.mo
Normal file
BIN
lib/xadmin/locale/ja/LC_MESSAGES/djangojs.mo
Normal file
Binary file not shown.
69
lib/xadmin/locale/ja/LC_MESSAGES/djangojs.po
Normal file
69
lib/xadmin/locale/ja/LC_MESSAGES/djangojs.po
Normal file
@@ -0,0 +1,69 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
# Translators:
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: xadmin-core\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2013-04-30 23:11+0800\n"
|
||||
"PO-Revision-Date: 2013-11-20 12:41+0000\n"
|
||||
"Last-Translator: sshwsfc <sshwsfc@gmail.com>\n"
|
||||
"Language-Team: Japanese (http://www.transifex.com/projects/p/xadmin/language/ja/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: ja\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
#: static/xadmin/js/xadmin.plugin.actions.js:20
|
||||
msgid "%(sel)s of %(cnt)s selected"
|
||||
msgid_plural "%(sel)s of %(cnt)s selected"
|
||||
msgstr[0] ""
|
||||
|
||||
#: static/xadmin/js/xadmin.plugin.revision.js:25
|
||||
msgid "New Item"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:32
|
||||
msgid "Sunday Monday Tuesday Wednesday Thursday Friday Saturday Sunday"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:33
|
||||
msgid "Sun Mon Tue Wed Thu Fri Sat Sun"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:34
|
||||
msgid "Su Mo Tu We Th Fr Sa Su"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:35
|
||||
msgid ""
|
||||
"January February March April May June July August September October November"
|
||||
" December"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:36
|
||||
msgid "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:37
|
||||
msgid "Today"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:38
|
||||
msgid "%a %d %b %Y %T %Z"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:39
|
||||
msgid "AM PM"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:40
|
||||
msgid "am pm"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:43
|
||||
msgid "%T"
|
||||
msgstr ""
|
||||
BIN
lib/xadmin/locale/lt/LC_MESSAGES/django.mo
Normal file
BIN
lib/xadmin/locale/lt/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
1418
lib/xadmin/locale/lt/LC_MESSAGES/django.po
Normal file
1418
lib/xadmin/locale/lt/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
lib/xadmin/locale/lt/LC_MESSAGES/djangojs.mo
Normal file
BIN
lib/xadmin/locale/lt/LC_MESSAGES/djangojs.mo
Normal file
Binary file not shown.
71
lib/xadmin/locale/lt/LC_MESSAGES/djangojs.po
Normal file
71
lib/xadmin/locale/lt/LC_MESSAGES/djangojs.po
Normal file
@@ -0,0 +1,71 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
# Translators:
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: xadmin-core\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2013-04-30 23:11+0800\n"
|
||||
"PO-Revision-Date: 2013-11-20 12:41+0000\n"
|
||||
"Last-Translator: sshwsfc <sshwsfc@gmail.com>\n"
|
||||
"Language-Team: Lithuanian (http://www.transifex.com/projects/p/xadmin/language/lt/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: lt\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||
|
||||
#: static/xadmin/js/xadmin.plugin.actions.js:20
|
||||
msgid "%(sel)s of %(cnt)s selected"
|
||||
msgid_plural "%(sel)s of %(cnt)s selected"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
msgstr[2] ""
|
||||
|
||||
#: static/xadmin/js/xadmin.plugin.revision.js:25
|
||||
msgid "New Item"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:32
|
||||
msgid "Sunday Monday Tuesday Wednesday Thursday Friday Saturday Sunday"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:33
|
||||
msgid "Sun Mon Tue Wed Thu Fri Sat Sun"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:34
|
||||
msgid "Su Mo Tu We Th Fr Sa Su"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:35
|
||||
msgid ""
|
||||
"January February March April May June July August September October November"
|
||||
" December"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:36
|
||||
msgid "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:37
|
||||
msgid "Today"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:38
|
||||
msgid "%a %d %b %Y %T %Z"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:39
|
||||
msgid "AM PM"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:40
|
||||
msgid "am pm"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:43
|
||||
msgid "%T"
|
||||
msgstr ""
|
||||
BIN
lib/xadmin/locale/nl_NL/LC_MESSAGES/django.mo
Normal file
BIN
lib/xadmin/locale/nl_NL/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
1410
lib/xadmin/locale/nl_NL/LC_MESSAGES/django.po
Normal file
1410
lib/xadmin/locale/nl_NL/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
lib/xadmin/locale/nl_NL/LC_MESSAGES/djangojs.mo
Normal file
BIN
lib/xadmin/locale/nl_NL/LC_MESSAGES/djangojs.mo
Normal file
Binary file not shown.
70
lib/xadmin/locale/nl_NL/LC_MESSAGES/djangojs.po
Normal file
70
lib/xadmin/locale/nl_NL/LC_MESSAGES/djangojs.po
Normal file
@@ -0,0 +1,70 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
# Translators:
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: xadmin-core\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2013-04-30 23:11+0800\n"
|
||||
"PO-Revision-Date: 2013-11-20 12:41+0000\n"
|
||||
"Last-Translator: sshwsfc <sshwsfc@gmail.com>\n"
|
||||
"Language-Team: Dutch (Netherlands) (http://www.transifex.com/projects/p/xadmin/language/nl_NL/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: nl_NL\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: static/xadmin/js/xadmin.plugin.actions.js:20
|
||||
msgid "%(sel)s of %(cnt)s selected"
|
||||
msgid_plural "%(sel)s of %(cnt)s selected"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: static/xadmin/js/xadmin.plugin.revision.js:25
|
||||
msgid "New Item"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:32
|
||||
msgid "Sunday Monday Tuesday Wednesday Thursday Friday Saturday Sunday"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:33
|
||||
msgid "Sun Mon Tue Wed Thu Fri Sat Sun"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:34
|
||||
msgid "Su Mo Tu We Th Fr Sa Su"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:35
|
||||
msgid ""
|
||||
"January February March April May June July August September October November"
|
||||
" December"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:36
|
||||
msgid "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:37
|
||||
msgid "Today"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:38
|
||||
msgid "%a %d %b %Y %T %Z"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:39
|
||||
msgid "AM PM"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:40
|
||||
msgid "am pm"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:43
|
||||
msgid "%T"
|
||||
msgstr ""
|
||||
BIN
lib/xadmin/locale/pl/LC_MESSAGES/django.mo
Normal file
BIN
lib/xadmin/locale/pl/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
1481
lib/xadmin/locale/pl/LC_MESSAGES/django.po
Normal file
1481
lib/xadmin/locale/pl/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
lib/xadmin/locale/pl/LC_MESSAGES/djangojs.mo
Normal file
BIN
lib/xadmin/locale/pl/LC_MESSAGES/djangojs.mo
Normal file
Binary file not shown.
83
lib/xadmin/locale/pl/LC_MESSAGES/djangojs.po
Normal file
83
lib/xadmin/locale/pl/LC_MESSAGES/djangojs.po
Normal file
@@ -0,0 +1,83 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: django-xadmin\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2014-08-12 21:07+0200\n"
|
||||
"PO-Revision-Date: 2014-08-12 21:23+0100\n"
|
||||
"Last-Translator: Michał Szpadzik <mszpadzik@gmail.com>\n"
|
||||
"Language-Team: Polish translators <mszpadzik@gmail.com>\n"
|
||||
"Language: pl\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
|
||||
"|| n%100>=20) ? 1 : 2);\n"
|
||||
"X-Generator: Poedit 1.5.4\n"
|
||||
|
||||
#: static/xadmin/js/xadmin.plugin.actions.js:11
|
||||
msgid "%(sel)s of %(cnt)s selected"
|
||||
msgid_plural "%(sel)s of %(cnt)s selected"
|
||||
msgstr[0] "%(sel)s z %(cnt)s wybranych"
|
||||
msgstr[1] "%(sel)s z %(cnt)s wybranych"
|
||||
msgstr[2] "%(sel)s z %(cnt)s wybranych"
|
||||
|
||||
#: static/xadmin/js/xadmin.plugin.quick-form.js:172
|
||||
msgid "Close"
|
||||
msgstr "Zamknij"
|
||||
|
||||
#: static/xadmin/js/xadmin.plugin.quick-form.js:173
|
||||
msgid "Add"
|
||||
msgstr "Dodaj"
|
||||
|
||||
#: static/xadmin/js/xadmin.plugin.revision.js:25
|
||||
msgid "New Item"
|
||||
msgstr "Nowy obiekt"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:32
|
||||
msgid "Sunday Monday Tuesday Wednesday Thursday Friday Saturday Sunday"
|
||||
msgstr "niedziela poniedziałek wtorek środa czwartek piątek sobota niedziela"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:33
|
||||
msgid "Sun Mon Tue Wed Thu Fri Sat Sun"
|
||||
msgstr "niedz. pon. wt. śr. czw. pt. sob. niedz."
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:34
|
||||
msgid "Su Mo Tu We Th Fr Sa Su"
|
||||
msgstr "niedz. pn. wt. śr. czw. pt. sob. niedz."
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:35
|
||||
msgid ""
|
||||
"January February March April May June July August September October November "
|
||||
"December"
|
||||
msgstr ""
|
||||
"styczeń luty marzec kwiecień maj czerwiec lipiec sierpień wrzesień "
|
||||
"październik "
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:36
|
||||
msgid "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec"
|
||||
msgstr "sty. lut. marz. kwie. maj czerw. lip. sier. wrze. paź. list. grudz."
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:37
|
||||
msgid "Today"
|
||||
msgstr "Dzisiaj"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:38
|
||||
msgid "%a %d %b %Y %T %Z"
|
||||
msgstr "%a %d %b %Y %T %Z"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:39
|
||||
msgid "AM PM"
|
||||
msgstr "AM PM"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:40
|
||||
msgid "am pm"
|
||||
msgstr "am pm"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:43
|
||||
msgid "%T"
|
||||
msgstr "%T"
|
||||
BIN
lib/xadmin/locale/pt_BR/LC_MESSAGES/django.mo
Normal file
BIN
lib/xadmin/locale/pt_BR/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
1479
lib/xadmin/locale/pt_BR/LC_MESSAGES/django.po
Normal file
1479
lib/xadmin/locale/pt_BR/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
lib/xadmin/locale/pt_BR/LC_MESSAGES/djangojs.mo
Normal file
BIN
lib/xadmin/locale/pt_BR/LC_MESSAGES/djangojs.mo
Normal file
Binary file not shown.
71
lib/xadmin/locale/pt_BR/LC_MESSAGES/djangojs.po
Normal file
71
lib/xadmin/locale/pt_BR/LC_MESSAGES/djangojs.po
Normal file
@@ -0,0 +1,71 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
# Translators:
|
||||
# korndorfer <codigo.aberto@dorfer.com.br>, 2013
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: xadmin-core\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2013-04-30 23:11+0800\n"
|
||||
"PO-Revision-Date: 2013-11-20 12:41+0000\n"
|
||||
"Last-Translator: korndorfer <codigo.aberto@dorfer.com.br>\n"
|
||||
"Language-Team: Portuguese (Brazil) (http://www.transifex.com/projects/p/xadmin/language/pt_BR/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: pt_BR\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: static/xadmin/js/xadmin.plugin.actions.js:20
|
||||
msgid "%(sel)s of %(cnt)s selected"
|
||||
msgid_plural "%(sel)s of %(cnt)s selected"
|
||||
msgstr[0] "%(sel)s de %(cnt)s selecionado"
|
||||
msgstr[1] "%(sel)s de %(cnt)s selecionados"
|
||||
|
||||
#: static/xadmin/js/xadmin.plugin.revision.js:25
|
||||
msgid "New Item"
|
||||
msgstr "Novo Item"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:32
|
||||
msgid "Sunday Monday Tuesday Wednesday Thursday Friday Saturday Sunday"
|
||||
msgstr "Domingo Segunda Terça Quarta Quinta Sexta Sábado Domingo"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:33
|
||||
msgid "Sun Mon Tue Wed Thu Fri Sat Sun"
|
||||
msgstr "Dom Seg Ter Qua Qui Sex Sáb Dom"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:34
|
||||
msgid "Su Mo Tu We Th Fr Sa Su"
|
||||
msgstr "Do Sg Te Qa Qi Sx Sa Do"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:35
|
||||
msgid ""
|
||||
"January February March April May June July August September October November"
|
||||
" December"
|
||||
msgstr "Janeiro Fevereiro Março Abril Maio Junho Julho Agosto Setembro Outubro Novembro Dezembro"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:36
|
||||
msgid "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec"
|
||||
msgstr "Jan Fev Mar Abr Mai Jun Jul Ago Set Out Nov Dez"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:37
|
||||
msgid "Today"
|
||||
msgstr "Hoje"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:38
|
||||
msgid "%a %d %b %Y %T %Z"
|
||||
msgstr "%a %d %b %Y %T %Z"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:39
|
||||
msgid "AM PM"
|
||||
msgstr "AM PM"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:40
|
||||
msgid "am pm"
|
||||
msgstr "am pm"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:43
|
||||
msgid "%T"
|
||||
msgstr "%T"
|
||||
BIN
lib/xadmin/locale/ru_RU/LC_MESSAGES/django.mo
Normal file
BIN
lib/xadmin/locale/ru_RU/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
1438
lib/xadmin/locale/ru_RU/LC_MESSAGES/django.po
Normal file
1438
lib/xadmin/locale/ru_RU/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
lib/xadmin/locale/ru_RU/LC_MESSAGES/djangojs.mo
Normal file
BIN
lib/xadmin/locale/ru_RU/LC_MESSAGES/djangojs.mo
Normal file
Binary file not shown.
71
lib/xadmin/locale/ru_RU/LC_MESSAGES/djangojs.po
Normal file
71
lib/xadmin/locale/ru_RU/LC_MESSAGES/djangojs.po
Normal file
@@ -0,0 +1,71 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
# Translators:
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: xadmin-core\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2013-04-30 23:11+0800\n"
|
||||
"PO-Revision-Date: 2013-11-20 12:41+0000\n"
|
||||
"Last-Translator: sshwsfc <sshwsfc@gmail.com>\n"
|
||||
"Language-Team: Russian (Russia) (http://www.transifex.com/projects/p/xadmin/language/ru_RU/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: ru_RU\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||
|
||||
#: static/xadmin/js/xadmin.plugin.actions.js:20
|
||||
msgid "%(sel)s of %(cnt)s selected"
|
||||
msgid_plural "%(sel)s of %(cnt)s selected"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
msgstr[2] ""
|
||||
|
||||
#: static/xadmin/js/xadmin.plugin.revision.js:25
|
||||
msgid "New Item"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:32
|
||||
msgid "Sunday Monday Tuesday Wednesday Thursday Friday Saturday Sunday"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:33
|
||||
msgid "Sun Mon Tue Wed Thu Fri Sat Sun"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:34
|
||||
msgid "Su Mo Tu We Th Fr Sa Su"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:35
|
||||
msgid ""
|
||||
"January February March April May June July August September October November"
|
||||
" December"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:36
|
||||
msgid "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:37
|
||||
msgid "Today"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:38
|
||||
msgid "%a %d %b %Y %T %Z"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:39
|
||||
msgid "AM PM"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:40
|
||||
msgid "am pm"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:43
|
||||
msgid "%T"
|
||||
msgstr ""
|
||||
BIN
lib/xadmin/locale/zh_Hans/LC_MESSAGES/django.mo
Normal file
BIN
lib/xadmin/locale/zh_Hans/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
1528
lib/xadmin/locale/zh_Hans/LC_MESSAGES/django.po
Normal file
1528
lib/xadmin/locale/zh_Hans/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
lib/xadmin/locale/zh_Hans/LC_MESSAGES/djangojs.mo
Normal file
BIN
lib/xadmin/locale/zh_Hans/LC_MESSAGES/djangojs.mo
Normal file
Binary file not shown.
87
lib/xadmin/locale/zh_Hans/LC_MESSAGES/djangojs.po
Normal file
87
lib/xadmin/locale/zh_Hans/LC_MESSAGES/djangojs.po
Normal file
@@ -0,0 +1,87 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
# Translators:
|
||||
# sshwsfc <sshwsfc@gmail.com>, 2013
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: xadmin-core\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2015-05-22 16:02+0800\n"
|
||||
"PO-Revision-Date: 2013-11-20 12:41+0000\n"
|
||||
"Last-Translator: sshwsfc <sshwsfc@gmail.com>\n"
|
||||
"Language-Team: Chinese (China) (http://www.transifex.com/projects/p/xadmin/language/zh_CN/)\n"
|
||||
"Language: zh_CN\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
#: static/xadmin/js/xadmin.page.dashboard.js:14
|
||||
#: static/xadmin/js/xadmin.plugin.details.js:24
|
||||
#: static/xadmin/js/xadmin.plugin.quick-form.js:172
|
||||
msgid "Close"
|
||||
msgstr "关闭"
|
||||
|
||||
#: static/xadmin/js/xadmin.page.dashboard.js:15
|
||||
msgid "Save changes"
|
||||
msgstr "保存修改"
|
||||
|
||||
#: static/xadmin/js/xadmin.plugin.actions.js:11
|
||||
msgid "%(sel)s of %(cnt)s selected"
|
||||
msgid_plural "%(sel)s of %(cnt)s selected"
|
||||
msgstr[0] "选中了 %(cnt)s 个中的 %(sel)s 个"
|
||||
msgstr[1] "选中了 %(cnt)s 个中的 %(sel)s 个"
|
||||
|
||||
#: static/xadmin/js/xadmin.plugin.details.js:25
|
||||
msgid "Edit"
|
||||
msgstr "编辑"
|
||||
|
||||
#: static/xadmin/js/xadmin.plugin.quick-form.js:173
|
||||
msgid "Add"
|
||||
msgstr "添加"
|
||||
|
||||
#: static/xadmin/js/xadmin.plugin.revision.js:25
|
||||
msgid "New Item"
|
||||
msgstr "新项目"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:32
|
||||
msgid "Sunday Monday Tuesday Wednesday Thursday Friday Saturday Sunday"
|
||||
msgstr "星期日 星期一 星期二 星期三 星期四 星期五 星期六"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:33
|
||||
msgid "Sun Mon Tue Wed Thu Fri Sat Sun"
|
||||
msgstr "日 一 二 三 四 五 六"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:34
|
||||
msgid "Su Mo Tu We Th Fr Sa Su"
|
||||
msgstr "日 一 二 三 四 五 六"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:35
|
||||
msgid "January February March April May June July August September October November December"
|
||||
msgstr "一月 二月 三月 四月 五月 六月 七月 八月 九月 十月 十一月 十二月"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:36
|
||||
msgid "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec"
|
||||
msgstr "一月 二月 三月 四月 五月 六月 七月 八月 九月 十月 十一 十二"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:37
|
||||
msgid "Today"
|
||||
msgstr "今天"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:38
|
||||
msgid "%a %d %b %Y %T %Z"
|
||||
msgstr ""
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:39
|
||||
msgid "AM PM"
|
||||
msgstr "上午 下午"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:40
|
||||
msgid "am pm"
|
||||
msgstr "上午 下午"
|
||||
|
||||
#: static/xadmin/js/xadmin.widget.datetime.js:43
|
||||
msgid "%T"
|
||||
msgstr "%T"
|
||||
62
lib/xadmin/migrations/0001_initial.py
Normal file
62
lib/xadmin/migrations/0001_initial.py
Normal file
@@ -0,0 +1,62 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.2 on 2016-03-20 13:46
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL)
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Bookmark',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=128, verbose_name='Title')),
|
||||
('url_name', models.CharField(max_length=64, verbose_name='Url Name')),
|
||||
('query', models.CharField(blank=True, max_length=1000, verbose_name='Query String')),
|
||||
('is_share', models.BooleanField(default=False, verbose_name='Is Shared')),
|
||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
|
||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Bookmark',
|
||||
'verbose_name_plural': 'Bookmarks',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserSettings',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('key', models.CharField(max_length=256, verbose_name='Settings Key')),
|
||||
('value', models.TextField(verbose_name='Settings Content')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'User Setting',
|
||||
'verbose_name_plural': 'User Settings',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserWidget',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('page_id', models.CharField(max_length=256, verbose_name='Page')),
|
||||
('widget_type', models.CharField(max_length=50, verbose_name='Widget Type')),
|
||||
('value', models.TextField(verbose_name='Widget Params')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'User Widget',
|
||||
'verbose_name_plural': 'User Widgets',
|
||||
},
|
||||
),
|
||||
]
|
||||
39
lib/xadmin/migrations/0002_log.py
Normal file
39
lib/xadmin/migrations/0002_log.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.7 on 2016-07-15 05:50
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('xadmin', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Log',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('action_time', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='action time')),
|
||||
('ip_addr', models.GenericIPAddressField(blank=True, null=True, verbose_name='action ip')),
|
||||
('object_id', models.TextField(blank=True, null=True, verbose_name='object id')),
|
||||
('object_repr', models.CharField(max_length=200, verbose_name='object repr')),
|
||||
('action_flag', models.PositiveSmallIntegerField(verbose_name='action flag')),
|
||||
('message', models.TextField(blank=True, verbose_name='change message')),
|
||||
('content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='contenttypes.ContentType', verbose_name='content type')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('-action_time',),
|
||||
'verbose_name': 'log entry',
|
||||
'verbose_name_plural': 'log entries',
|
||||
},
|
||||
),
|
||||
]
|
||||
20
lib/xadmin/migrations/0003_auto_20160715_0100.py
Normal file
20
lib/xadmin/migrations/0003_auto_20160715_0100.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.7 on 2016-07-15 06:00
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('xadmin', '0002_log'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='log',
|
||||
name='action_flag',
|
||||
field=models.CharField(max_length=32, verbose_name='action flag'),
|
||||
),
|
||||
]
|
||||
0
lib/xadmin/migrations/__init__.py
Normal file
0
lib/xadmin/migrations/__init__.py
Normal file
190
lib/xadmin/models.py
Normal file
190
lib/xadmin/models.py
Normal file
@@ -0,0 +1,190 @@
|
||||
import json
|
||||
import django
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import ugettext_lazy as _, ugettext
|
||||
from django.urls.base import reverse
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db.models.base import ModelBase
|
||||
from django.utils.encoding import python_2_unicode_compatible, smart_text
|
||||
|
||||
from django.db.models.signals import post_migrate
|
||||
from django.contrib.auth.models import Permission
|
||||
|
||||
import datetime
|
||||
import decimal
|
||||
from xadmin.util import quote
|
||||
|
||||
AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User')
|
||||
|
||||
|
||||
def add_view_permissions(sender, **kwargs):
|
||||
"""
|
||||
This syncdb hooks takes care of adding a view permission too all our
|
||||
content types.
|
||||
"""
|
||||
# for each of our content types
|
||||
for content_type in ContentType.objects.all():
|
||||
# build our permission slug
|
||||
codename = "view_%s" % content_type.model
|
||||
|
||||
# if it doesn't exist..
|
||||
if not Permission.objects.filter(content_type=content_type, codename=codename):
|
||||
# add it
|
||||
Permission.objects.create(content_type=content_type,
|
||||
codename=codename,
|
||||
name="Can view %s" % content_type.name)
|
||||
# print "Added view permission for %s" % content_type.name
|
||||
|
||||
# check for all our view permissions after a syncdb
|
||||
post_migrate.connect(add_view_permissions)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Bookmark(models.Model):
|
||||
title = models.CharField(_(u'Title'), max_length=128)
|
||||
user = models.ForeignKey(AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name=_(u"user"), blank=True, null=True)
|
||||
url_name = models.CharField(_(u'Url Name'), max_length=64)
|
||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||
query = models.CharField(_(u'Query String'), max_length=1000, blank=True)
|
||||
is_share = models.BooleanField(_(u'Is Shared'), default=False)
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
base_url = reverse(self.url_name)
|
||||
if self.query:
|
||||
base_url = base_url + '?' + self.query
|
||||
return base_url
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
class Meta:
|
||||
verbose_name = _(u'Bookmark')
|
||||
verbose_name_plural = _('Bookmarks')
|
||||
|
||||
|
||||
class JSONEncoder(DjangoJSONEncoder):
|
||||
|
||||
def default(self, o):
|
||||
if isinstance(o, datetime.datetime):
|
||||
return o.strftime('%Y-%m-%d %H:%M:%S')
|
||||
elif isinstance(o, datetime.date):
|
||||
return o.strftime('%Y-%m-%d')
|
||||
elif isinstance(o, decimal.Decimal):
|
||||
return str(o)
|
||||
elif isinstance(o, ModelBase):
|
||||
return '%s.%s' % (o._meta.app_label, o._meta.model_name)
|
||||
else:
|
||||
try:
|
||||
return super(JSONEncoder, self).default(o)
|
||||
except Exception:
|
||||
return smart_text(o)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class UserSettings(models.Model):
|
||||
user = models.ForeignKey(AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name=_(u"user"))
|
||||
key = models.CharField(_('Settings Key'), max_length=256)
|
||||
value = models.TextField(_('Settings Content'))
|
||||
|
||||
def json_value(self):
|
||||
return json.loads(self.value)
|
||||
|
||||
def set_json(self, obj):
|
||||
self.value = json.dumps(obj, cls=JSONEncoder, ensure_ascii=False)
|
||||
|
||||
def __str__(self):
|
||||
return "%s %s" % (self.user, self.key)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _(u'User Setting')
|
||||
verbose_name_plural = _('User Settings')
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class UserWidget(models.Model):
|
||||
user = models.ForeignKey(AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name=_(u"user"))
|
||||
page_id = models.CharField(_(u"Page"), max_length=256)
|
||||
widget_type = models.CharField(_(u"Widget Type"), max_length=50)
|
||||
value = models.TextField(_(u"Widget Params"))
|
||||
|
||||
def get_value(self):
|
||||
value = json.loads(self.value)
|
||||
value['id'] = self.id
|
||||
value['type'] = self.widget_type
|
||||
return value
|
||||
|
||||
def set_value(self, obj):
|
||||
self.value = json.dumps(obj, cls=JSONEncoder, ensure_ascii=False)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
created = self.pk is None
|
||||
super(UserWidget, self).save(*args, **kwargs)
|
||||
if created:
|
||||
try:
|
||||
portal_pos = UserSettings.objects.get(
|
||||
user=self.user, key="dashboard:%s:pos" % self.page_id)
|
||||
portal_pos.value = "%s,%s" % (self.pk, portal_pos.value) if portal_pos.value else self.pk
|
||||
portal_pos.save()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def __str__(self):
|
||||
return "%s %s widget" % (self.user, self.widget_type)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _(u'User Widget')
|
||||
verbose_name_plural = _('User Widgets')
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Log(models.Model):
|
||||
action_time = models.DateTimeField(
|
||||
_('action time'),
|
||||
default=timezone.now,
|
||||
editable=False,
|
||||
)
|
||||
user = models.ForeignKey(
|
||||
AUTH_USER_MODEL,
|
||||
models.CASCADE,
|
||||
verbose_name=_('user'),
|
||||
)
|
||||
ip_addr = models.GenericIPAddressField(_('action ip'), blank=True, null=True)
|
||||
content_type = models.ForeignKey(
|
||||
ContentType,
|
||||
models.SET_NULL,
|
||||
verbose_name=_('content type'),
|
||||
blank=True, null=True,
|
||||
)
|
||||
object_id = models.TextField(_('object id'), blank=True, null=True)
|
||||
object_repr = models.CharField(_('object repr'), max_length=200)
|
||||
action_flag = models.CharField(_('action flag'), max_length=32)
|
||||
message = models.TextField(_('change message'), blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('log entry')
|
||||
verbose_name_plural = _('log entries')
|
||||
ordering = ('-action_time',)
|
||||
|
||||
def __repr__(self):
|
||||
return smart_text(self.action_time)
|
||||
|
||||
def __str__(self):
|
||||
if self.action_flag == 'create':
|
||||
return ugettext('Added "%(object)s".') % {'object': self.object_repr}
|
||||
elif self.action_flag == 'change':
|
||||
return ugettext('Changed "%(object)s" - %(changes)s') % {
|
||||
'object': self.object_repr,
|
||||
'changes': self.message,
|
||||
}
|
||||
elif self.action_flag == 'delete' and self.object_repr:
|
||||
return ugettext('Deleted "%(object)s."') % {'object': self.object_repr}
|
||||
|
||||
return self.message
|
||||
|
||||
def get_edited_object(self):
|
||||
"Returns the edited object represented by this log entry"
|
||||
return self.content_type.get_object_for_this_type(pk=self.object_id)
|
||||
41
lib/xadmin/plugins/__init__.py
Normal file
41
lib/xadmin/plugins/__init__.py
Normal file
@@ -0,0 +1,41 @@
|
||||
|
||||
PLUGINS = (
|
||||
'actions',
|
||||
'filters',
|
||||
'bookmark',
|
||||
'export',
|
||||
'layout',
|
||||
'refresh',
|
||||
'details',
|
||||
'editable',
|
||||
'relate',
|
||||
'chart',
|
||||
'ajax',
|
||||
'relfield',
|
||||
'inline',
|
||||
'topnav',
|
||||
'portal',
|
||||
'quickform',
|
||||
'wizard',
|
||||
'images',
|
||||
'auth',
|
||||
'multiselect',
|
||||
'themes',
|
||||
'aggregation',
|
||||
# 'mobile',
|
||||
'passwords',
|
||||
'sitemenu',
|
||||
'language',
|
||||
'quickfilter',
|
||||
'sortablelist',
|
||||
'importexport'
|
||||
)
|
||||
|
||||
|
||||
def register_builtin_plugins(site):
|
||||
from importlib import import_module
|
||||
from django.conf import settings
|
||||
|
||||
exclude_plugins = getattr(settings, 'XADMIN_EXCLUDE_PLUGINS', [])
|
||||
|
||||
[import_module('xadmin.plugins.%s' % plugin) for plugin in PLUGINS if plugin not in exclude_plugins]
|
||||
316
lib/xadmin/plugins/actions.py
Normal file
316
lib/xadmin/plugins/actions.py
Normal file
@@ -0,0 +1,316 @@
|
||||
from collections import OrderedDict
|
||||
from django import forms, VERSION as django_version
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db import router
|
||||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
from django.template import loader
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils import six
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext as _, ungettext
|
||||
from django.utils.text import capfirst
|
||||
|
||||
from django.contrib.admin.utils import get_deleted_objects
|
||||
|
||||
from xadmin.plugins.utils import get_context_dict
|
||||
from xadmin.sites import site
|
||||
from xadmin.util import model_format_dict, model_ngettext
|
||||
from xadmin.views import BaseAdminPlugin, ListAdminView
|
||||
from xadmin.views.base import filter_hook, ModelAdminView
|
||||
|
||||
from xadmin import views
|
||||
|
||||
ACTION_CHECKBOX_NAME = '_selected_action'
|
||||
checkbox = forms.CheckboxInput({'class': 'action-select'}, lambda value: False)
|
||||
|
||||
|
||||
def action_checkbox(obj):
|
||||
return checkbox.render(ACTION_CHECKBOX_NAME, force_text(obj.pk))
|
||||
|
||||
|
||||
action_checkbox.short_description = mark_safe(
|
||||
'<input type="checkbox" id="action-toggle" />')
|
||||
action_checkbox.allow_tags = True
|
||||
action_checkbox.allow_export = False
|
||||
action_checkbox.is_column = False
|
||||
|
||||
|
||||
class BaseActionView(ModelAdminView):
|
||||
action_name = None
|
||||
description = None
|
||||
icon = 'fa fa-tasks'
|
||||
|
||||
model_perm = 'change'
|
||||
|
||||
@classmethod
|
||||
def has_perm(cls, list_view):
|
||||
return list_view.get_model_perms()[cls.model_perm]
|
||||
|
||||
def init_action(self, list_view):
|
||||
self.list_view = list_view
|
||||
self.admin_site = list_view.admin_site
|
||||
|
||||
@filter_hook
|
||||
def do_action(self, queryset):
|
||||
pass
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
super().__init__(request, *args, **kwargs)
|
||||
if django_version > (2, 0):
|
||||
for model in self.admin_site._registry:
|
||||
if not hasattr(self.admin_site._registry[model], 'has_delete_permission'):
|
||||
setattr(self.admin_site._registry[model], 'has_delete_permission', self.has_delete_permission)
|
||||
|
||||
|
||||
class DeleteSelectedAction(BaseActionView):
|
||||
|
||||
action_name = "delete_selected"
|
||||
description = _(u'Delete selected %(verbose_name_plural)s')
|
||||
|
||||
delete_confirmation_template = None
|
||||
delete_selected_confirmation_template = None
|
||||
|
||||
delete_models_batch = True
|
||||
|
||||
model_perm = 'delete'
|
||||
icon = 'fa fa-times'
|
||||
|
||||
@filter_hook
|
||||
def delete_models(self, queryset):
|
||||
n = queryset.count()
|
||||
if n:
|
||||
if self.delete_models_batch:
|
||||
self.log('delete', _('Batch delete %(count)d %(items)s.') % {"count": n, "items": model_ngettext(self.opts, n)})
|
||||
queryset.delete()
|
||||
else:
|
||||
for obj in queryset:
|
||||
self.log('delete', '', obj)
|
||||
obj.delete()
|
||||
self.message_user(_("Successfully deleted %(count)d %(items)s.") % {
|
||||
"count": n, "items": model_ngettext(self.opts, n)
|
||||
}, 'success')
|
||||
|
||||
@filter_hook
|
||||
def do_action(self, queryset):
|
||||
# Check that the user has delete permission for the actual model
|
||||
if not self.has_delete_permission():
|
||||
raise PermissionDenied
|
||||
|
||||
# Populate deletable_objects, a data structure of all related objects that
|
||||
# will also be deleted.
|
||||
|
||||
if django_version > (2, 1):
|
||||
deletable_objects, model_count, perms_needed, protected = get_deleted_objects(
|
||||
queryset, self.opts, self.admin_site)
|
||||
else:
|
||||
using = router.db_for_write(self.model)
|
||||
deletable_objects, model_count, perms_needed, protected = get_deleted_objects(
|
||||
queryset, self.opts, self.user, self.admin_site, using)
|
||||
|
||||
|
||||
# The user has already confirmed the deletion.
|
||||
# Do the deletion and return a None to display the change list view again.
|
||||
if self.request.POST.get('post'):
|
||||
if perms_needed:
|
||||
raise PermissionDenied
|
||||
self.delete_models(queryset)
|
||||
# Return None to display the change list page again.
|
||||
return None
|
||||
|
||||
if len(queryset) == 1:
|
||||
objects_name = force_text(self.opts.verbose_name)
|
||||
else:
|
||||
objects_name = force_text(self.opts.verbose_name_plural)
|
||||
|
||||
if perms_needed or protected:
|
||||
title = _("Cannot delete %(name)s") % {"name": objects_name}
|
||||
else:
|
||||
title = _("Are you sure?")
|
||||
|
||||
context = self.get_context()
|
||||
context.update({
|
||||
"title": title,
|
||||
"objects_name": objects_name,
|
||||
"deletable_objects": [deletable_objects],
|
||||
'queryset': queryset,
|
||||
"perms_lacking": perms_needed,
|
||||
"protected": protected,
|
||||
"opts": self.opts,
|
||||
"app_label": self.app_label,
|
||||
'action_checkbox_name': ACTION_CHECKBOX_NAME,
|
||||
})
|
||||
|
||||
# Display the confirmation page
|
||||
return TemplateResponse(self.request, self.delete_selected_confirmation_template or
|
||||
self.get_template_list('views/model_delete_selected_confirm.html'), context)
|
||||
|
||||
|
||||
class ActionPlugin(BaseAdminPlugin):
|
||||
|
||||
# Actions
|
||||
actions = []
|
||||
actions_selection_counter = True
|
||||
global_actions = [DeleteSelectedAction]
|
||||
|
||||
def init_request(self, *args, **kwargs):
|
||||
self.actions = self.get_actions()
|
||||
return bool(self.actions)
|
||||
|
||||
def get_list_display(self, list_display):
|
||||
if self.actions:
|
||||
list_display.insert(0, 'action_checkbox')
|
||||
self.admin_view.action_checkbox = action_checkbox
|
||||
return list_display
|
||||
|
||||
def get_list_display_links(self, list_display_links):
|
||||
if self.actions:
|
||||
if len(list_display_links) == 1 and list_display_links[0] == 'action_checkbox':
|
||||
return list(self.admin_view.list_display[1:2])
|
||||
return list_display_links
|
||||
|
||||
def get_context(self, context):
|
||||
if self.actions and self.admin_view.result_count:
|
||||
av = self.admin_view
|
||||
selection_note_all = ungettext('%(total_count)s selected',
|
||||
'All %(total_count)s selected', av.result_count)
|
||||
|
||||
new_context = {
|
||||
'selection_note': _('0 of %(cnt)s selected') % {'cnt': len(av.result_list)},
|
||||
'selection_note_all': selection_note_all % {'total_count': av.result_count},
|
||||
'action_choices': self.get_action_choices(),
|
||||
'actions_selection_counter': self.actions_selection_counter,
|
||||
}
|
||||
context.update(new_context)
|
||||
return context
|
||||
|
||||
def post_response(self, response, *args, **kwargs):
|
||||
request = self.admin_view.request
|
||||
av = self.admin_view
|
||||
|
||||
# Actions with no confirmation
|
||||
if self.actions and 'action' in request.POST:
|
||||
action = request.POST['action']
|
||||
|
||||
if action not in self.actions:
|
||||
msg = _("Items must be selected in order to perform "
|
||||
"actions on them. No items have been changed.")
|
||||
av.message_user(msg)
|
||||
else:
|
||||
ac, name, description, icon = self.actions[action]
|
||||
select_across = request.POST.get('select_across', False) == '1'
|
||||
selected = request.POST.getlist(ACTION_CHECKBOX_NAME)
|
||||
|
||||
if not selected and not select_across:
|
||||
# Reminder that something needs to be selected or nothing will happen
|
||||
msg = _("Items must be selected in order to perform "
|
||||
"actions on them. No items have been changed.")
|
||||
av.message_user(msg)
|
||||
else:
|
||||
queryset = av.list_queryset._clone()
|
||||
if not select_across:
|
||||
# Perform the action only on the selected objects
|
||||
queryset = av.list_queryset.filter(pk__in=selected)
|
||||
response = self.response_action(ac, queryset)
|
||||
# Actions may return an HttpResponse, which will be used as the
|
||||
# response from the POST. If not, we'll be a good little HTTP
|
||||
# citizen and redirect back to the changelist page.
|
||||
if isinstance(response, HttpResponse):
|
||||
return response
|
||||
else:
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
return response
|
||||
|
||||
def response_action(self, ac, queryset):
|
||||
if isinstance(ac, type) and issubclass(ac, BaseActionView):
|
||||
action_view = self.get_model_view(ac, self.admin_view.model)
|
||||
action_view.init_action(self.admin_view)
|
||||
return action_view.do_action(queryset)
|
||||
else:
|
||||
return ac(self.admin_view, self.request, queryset)
|
||||
|
||||
def get_actions(self):
|
||||
if self.actions is None:
|
||||
return OrderedDict()
|
||||
|
||||
actions = [self.get_action(action) for action in self.global_actions]
|
||||
|
||||
for klass in self.admin_view.__class__.mro()[::-1]:
|
||||
class_actions = getattr(klass, 'actions', [])
|
||||
if not class_actions:
|
||||
continue
|
||||
actions.extend(
|
||||
[self.get_action(action) for action in class_actions])
|
||||
|
||||
# get_action might have returned None, so filter any of those out.
|
||||
actions = filter(None, actions)
|
||||
if six.PY3:
|
||||
actions = list(actions)
|
||||
|
||||
# Convert the actions into a OrderedDict keyed by name.
|
||||
actions = OrderedDict([
|
||||
(name, (ac, name, desc, icon))
|
||||
for ac, name, desc, icon in actions
|
||||
])
|
||||
|
||||
return actions
|
||||
|
||||
def get_action_choices(self):
|
||||
"""
|
||||
Return a list of choices for use in a form object. Each choice is a
|
||||
tuple (name, description).
|
||||
"""
|
||||
choices = []
|
||||
for ac, name, description, icon in self.actions.values():
|
||||
choice = (name, description % model_format_dict(self.opts), icon)
|
||||
choices.append(choice)
|
||||
return choices
|
||||
|
||||
def get_action(self, action):
|
||||
if isinstance(action, type) and issubclass(action, BaseActionView):
|
||||
if not action.has_perm(self.admin_view):
|
||||
return None
|
||||
return action, getattr(action, 'action_name'), getattr(action, 'description'), getattr(action, 'icon')
|
||||
|
||||
elif callable(action):
|
||||
func = action
|
||||
action = action.__name__
|
||||
|
||||
elif hasattr(self.admin_view.__class__, action):
|
||||
func = getattr(self.admin_view.__class__, action)
|
||||
|
||||
else:
|
||||
return None
|
||||
|
||||
if hasattr(func, 'short_description'):
|
||||
description = func.short_description
|
||||
else:
|
||||
description = capfirst(action.replace('_', ' '))
|
||||
|
||||
return func, action, description, getattr(func, 'icon', 'tasks')
|
||||
|
||||
# View Methods
|
||||
def result_header(self, item, field_name, row):
|
||||
if item.attr and field_name == 'action_checkbox':
|
||||
item.classes.append("action-checkbox-column")
|
||||
return item
|
||||
|
||||
def result_item(self, item, obj, field_name, row):
|
||||
if item.field is None and field_name == u'action_checkbox':
|
||||
item.classes.append("action-checkbox")
|
||||
return item
|
||||
|
||||
# Media
|
||||
def get_media(self, media):
|
||||
if self.actions and self.admin_view.result_count:
|
||||
media = media + self.vendor('xadmin.plugin.actions.js', 'xadmin.plugins.css')
|
||||
return media
|
||||
|
||||
# Block Views
|
||||
def block_results_bottom(self, context, nodes):
|
||||
if self.actions and self.admin_view.result_count:
|
||||
nodes.append(loader.render_to_string('xadmin/blocks/model_list.results_bottom.actions.html',
|
||||
context=get_context_dict(context)))
|
||||
|
||||
|
||||
site.register_plugin(ActionPlugin, ListAdminView)
|
||||
68
lib/xadmin/plugins/aggregation.py
Normal file
68
lib/xadmin/plugins/aggregation.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from django.db.models import FieldDoesNotExist, Avg, Max, Min, Count, Sum
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.forms import Media
|
||||
|
||||
from xadmin.sites import site
|
||||
from xadmin.views import BaseAdminPlugin, ListAdminView
|
||||
|
||||
from xadmin.views.list import ResultRow, ResultItem
|
||||
from xadmin.util import display_for_field
|
||||
|
||||
AGGREGATE_METHODS = {
|
||||
'min': Min, 'max': Max, 'avg': Avg, 'sum': Sum, 'count': Count
|
||||
}
|
||||
AGGREGATE_TITLE = {
|
||||
'min': _('Min'), 'max': _('Max'), 'avg': _('Avg'), 'sum': _('Sum'), 'count': _('Count')
|
||||
}
|
||||
|
||||
|
||||
class AggregationPlugin(BaseAdminPlugin):
|
||||
|
||||
aggregate_fields = {}
|
||||
|
||||
def init_request(self, *args, **kwargs):
|
||||
return bool(self.aggregate_fields)
|
||||
|
||||
def _get_field_aggregate(self, field_name, obj, row):
|
||||
item = ResultItem(field_name, row)
|
||||
item.classes = ['aggregate', ]
|
||||
if field_name not in self.aggregate_fields:
|
||||
item.text = ""
|
||||
else:
|
||||
try:
|
||||
f = self.opts.get_field(field_name)
|
||||
agg_method = self.aggregate_fields[field_name]
|
||||
key = '%s__%s' % (field_name, agg_method)
|
||||
if key not in obj:
|
||||
item.text = ""
|
||||
else:
|
||||
item.text = display_for_field(obj[key], f)
|
||||
item.wraps.append('%%s<span class="aggregate_title label label-info">%s</span>' % AGGREGATE_TITLE[agg_method])
|
||||
item.classes.append(agg_method)
|
||||
except FieldDoesNotExist:
|
||||
item.text = ""
|
||||
|
||||
return item
|
||||
|
||||
def _get_aggregate_row(self):
|
||||
queryset = self.admin_view.list_queryset._clone()
|
||||
obj = queryset.aggregate(*[AGGREGATE_METHODS[method](field_name) for field_name, method in
|
||||
self.aggregate_fields.items() if method in AGGREGATE_METHODS])
|
||||
|
||||
row = ResultRow()
|
||||
row['is_display_first'] = False
|
||||
row.cells = [self._get_field_aggregate(field_name, obj, row) for field_name in self.admin_view.list_display]
|
||||
row.css_class = 'info aggregate'
|
||||
return row
|
||||
|
||||
def results(self, rows):
|
||||
if rows:
|
||||
rows.append(self._get_aggregate_row())
|
||||
return rows
|
||||
|
||||
# Media
|
||||
def get_media(self, media):
|
||||
return media + Media(css={'screen': [self.static('xadmin/css/xadmin.plugin.aggregation.css'), ]})
|
||||
|
||||
|
||||
site.register_plugin(AggregationPlugin, ListAdminView)
|
||||
99
lib/xadmin/plugins/ajax.py
Normal file
99
lib/xadmin/plugins/ajax.py
Normal file
@@ -0,0 +1,99 @@
|
||||
from collections import OrderedDict
|
||||
from django.forms.utils import ErrorDict
|
||||
from django.utils.html import escape
|
||||
from django.utils.encoding import force_text
|
||||
from xadmin.sites import site
|
||||
from xadmin.views import BaseAdminPlugin, ListAdminView, ModelFormAdminView, DetailAdminView
|
||||
|
||||
|
||||
NON_FIELD_ERRORS = '__all__'
|
||||
|
||||
|
||||
class BaseAjaxPlugin(BaseAdminPlugin):
|
||||
|
||||
def init_request(self, *args, **kwargs):
|
||||
return bool(self.request.is_ajax() or self.request.GET.get('_ajax'))
|
||||
|
||||
|
||||
class AjaxListPlugin(BaseAjaxPlugin):
|
||||
|
||||
def get_list_display(self,list_display):
|
||||
list_fields = [field for field in self.request.GET.get('_fields',"").split(",")
|
||||
if field.strip() != ""]
|
||||
if list_fields:
|
||||
return list_fields
|
||||
return list_display
|
||||
|
||||
def get_result_list(self, response):
|
||||
av = self.admin_view
|
||||
base_fields = self.get_list_display(av.base_list_display)
|
||||
headers = dict([(c.field_name, force_text(c.text)) for c in av.result_headers(
|
||||
).cells if c.field_name in base_fields])
|
||||
|
||||
objects = [dict([(o.field_name, escape(str(o.value))) for i, o in
|
||||
enumerate(filter(lambda c:c.field_name in base_fields, r.cells))])
|
||||
for r in av.results()]
|
||||
|
||||
return self.render_response({'headers': headers, 'objects': objects, 'total_count': av.result_count, 'has_more': av.has_more})
|
||||
|
||||
|
||||
class JsonErrorDict(ErrorDict):
|
||||
|
||||
def __init__(self, errors, form):
|
||||
super(JsonErrorDict, self).__init__(errors)
|
||||
self.form = form
|
||||
|
||||
def as_json(self):
|
||||
if not self:
|
||||
return u''
|
||||
return [{'id': self.form[k].auto_id if k != NON_FIELD_ERRORS else NON_FIELD_ERRORS, 'name': k, 'errors': v} for k, v in self.items()]
|
||||
|
||||
|
||||
class AjaxFormPlugin(BaseAjaxPlugin):
|
||||
|
||||
def post_response(self, __):
|
||||
new_obj = self.admin_view.new_obj
|
||||
return self.render_response({
|
||||
'result': 'success',
|
||||
'obj_id': new_obj.pk,
|
||||
'obj_repr': str(new_obj),
|
||||
'change_url': self.admin_view.model_admin_url('change', new_obj.pk),
|
||||
'detail_url': self.admin_view.model_admin_url('detail', new_obj.pk)
|
||||
})
|
||||
|
||||
def get_response(self, __):
|
||||
if self.request.method.lower() != 'post':
|
||||
return __()
|
||||
|
||||
result = {}
|
||||
form = self.admin_view.form_obj
|
||||
if form.is_valid():
|
||||
result['result'] = 'success'
|
||||
else:
|
||||
result['result'] = 'error'
|
||||
result['errors'] = JsonErrorDict(form.errors, form).as_json()
|
||||
|
||||
return self.render_response(result)
|
||||
|
||||
|
||||
class AjaxDetailPlugin(BaseAjaxPlugin):
|
||||
|
||||
def get_response(self, __):
|
||||
if self.request.GET.get('_format') == 'html':
|
||||
self.admin_view.detail_template = 'xadmin/views/quick_detail.html'
|
||||
return __()
|
||||
|
||||
form = self.admin_view.form_obj
|
||||
layout = form.helper.layout
|
||||
|
||||
results = []
|
||||
|
||||
for p, f in layout.get_field_names():
|
||||
result = self.admin_view.get_field_result(f)
|
||||
results.append((result.label, result.val))
|
||||
|
||||
return self.render_response(OrderedDict(results))
|
||||
|
||||
site.register_plugin(AjaxListPlugin, ListAdminView)
|
||||
site.register_plugin(AjaxFormPlugin, ModelFormAdminView)
|
||||
site.register_plugin(AjaxDetailPlugin, DetailAdminView)
|
||||
269
lib/xadmin/plugins/auth.py
Normal file
269
lib/xadmin/plugins/auth.py
Normal file
@@ -0,0 +1,269 @@
|
||||
# coding=utf-8
|
||||
from django import forms
|
||||
from django.contrib.auth.forms import (UserCreationForm, UserChangeForm,
|
||||
AdminPasswordChangeForm, PasswordChangeForm)
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.conf import settings
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.utils.html import escape
|
||||
from django.utils.encoding import smart_text
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.decorators.debug import sensitive_post_parameters
|
||||
from django.forms import ModelMultipleChoiceField
|
||||
from django.contrib.auth import get_user_model
|
||||
from xadmin.layout import Fieldset, Main, Side, Row, FormHelper
|
||||
from xadmin.sites import site
|
||||
from xadmin.util import unquote
|
||||
from xadmin.views import BaseAdminPlugin, ModelFormAdminView, ModelAdminView, CommAdminView, csrf_protect_m
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
ACTION_NAME = {
|
||||
'add': _('Can add %s'),
|
||||
'change': _('Can change %s'),
|
||||
'edit': _('Can edit %s'),
|
||||
'delete': _('Can delete %s'),
|
||||
'view': _('Can view %s'),
|
||||
}
|
||||
|
||||
|
||||
def get_permission_name(p):
|
||||
action = p.codename.split('_')[0]
|
||||
if action in ACTION_NAME:
|
||||
return ACTION_NAME[action] % str(p.content_type)
|
||||
else:
|
||||
return p.name
|
||||
|
||||
|
||||
class PermissionModelMultipleChoiceField(ModelMultipleChoiceField):
|
||||
|
||||
def label_from_instance(self, p):
|
||||
return get_permission_name(p)
|
||||
|
||||
|
||||
class GroupAdmin(object):
|
||||
search_fields = ('name',)
|
||||
ordering = ('name',)
|
||||
style_fields = {'permissions': 'm2m_transfer'}
|
||||
model_icon = 'fa fa-group'
|
||||
|
||||
def get_field_attrs(self, db_field, **kwargs):
|
||||
attrs = super(GroupAdmin, self).get_field_attrs(db_field, **kwargs)
|
||||
if db_field.name == 'permissions':
|
||||
attrs['form_class'] = PermissionModelMultipleChoiceField
|
||||
return attrs
|
||||
|
||||
|
||||
class UserAdmin(object):
|
||||
change_user_password_template = None
|
||||
list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff')
|
||||
list_filter = ('is_staff', 'is_superuser', 'is_active')
|
||||
search_fields = ('username', 'first_name', 'last_name', 'email')
|
||||
ordering = ('username',)
|
||||
style_fields = {'user_permissions': 'm2m_transfer'}
|
||||
model_icon = 'fa fa-user'
|
||||
relfield_style = 'fk-ajax'
|
||||
|
||||
def get_field_attrs(self, db_field, **kwargs):
|
||||
attrs = super(UserAdmin, self).get_field_attrs(db_field, **kwargs)
|
||||
if db_field.name == 'user_permissions':
|
||||
attrs['form_class'] = PermissionModelMultipleChoiceField
|
||||
return attrs
|
||||
|
||||
def get_model_form(self, **kwargs):
|
||||
if self.org_obj is None:
|
||||
self.form = UserCreationForm
|
||||
else:
|
||||
self.form = UserChangeForm
|
||||
return super(UserAdmin, self).get_model_form(**kwargs)
|
||||
|
||||
def get_form_layout(self):
|
||||
if self.org_obj:
|
||||
self.form_layout = (
|
||||
Main(
|
||||
Fieldset('',
|
||||
'username', 'password',
|
||||
css_class='unsort no_title'
|
||||
),
|
||||
Fieldset(_('Personal info'),
|
||||
Row('first_name', 'last_name'),
|
||||
'email'
|
||||
),
|
||||
Fieldset(_('Permissions'),
|
||||
'groups', 'user_permissions'
|
||||
),
|
||||
Fieldset(_('Important dates'),
|
||||
'last_login', 'date_joined'
|
||||
),
|
||||
),
|
||||
Side(
|
||||
Fieldset(_('Status'),
|
||||
'is_active', 'is_staff', 'is_superuser',
|
||||
),
|
||||
)
|
||||
)
|
||||
return super(UserAdmin, self).get_form_layout()
|
||||
|
||||
|
||||
class PermissionAdmin(object):
|
||||
|
||||
def show_name(self, p):
|
||||
return get_permission_name(p)
|
||||
show_name.short_description = _('Permission Name')
|
||||
show_name.is_column = True
|
||||
|
||||
model_icon = 'fa fa-lock'
|
||||
list_display = ('show_name', )
|
||||
|
||||
site.register(Group, GroupAdmin)
|
||||
site.register(User, UserAdmin)
|
||||
site.register(Permission, PermissionAdmin)
|
||||
|
||||
|
||||
class UserFieldPlugin(BaseAdminPlugin):
|
||||
|
||||
user_fields = []
|
||||
|
||||
def get_field_attrs(self, __, db_field, **kwargs):
|
||||
if self.user_fields and db_field.name in self.user_fields:
|
||||
return {'widget': forms.HiddenInput}
|
||||
return __()
|
||||
|
||||
def get_form_datas(self, datas):
|
||||
if self.user_fields and 'data' in datas:
|
||||
if hasattr(datas['data'],'_mutable') and not datas['data']._mutable:
|
||||
datas['data'] = datas['data'].copy()
|
||||
for f in self.user_fields:
|
||||
datas['data'][f] = self.user.id
|
||||
return datas
|
||||
|
||||
site.register_plugin(UserFieldPlugin, ModelFormAdminView)
|
||||
|
||||
|
||||
class ModelPermissionPlugin(BaseAdminPlugin):
|
||||
|
||||
user_can_access_owned_objects_only = False
|
||||
user_owned_objects_field = 'user'
|
||||
|
||||
def queryset(self, qs):
|
||||
if self.user_can_access_owned_objects_only and \
|
||||
not self.user.is_superuser:
|
||||
filters = {self.user_owned_objects_field: self.user}
|
||||
qs = qs.filter(**filters)
|
||||
return qs
|
||||
|
||||
def get_list_display(self, list_display):
|
||||
if self.user_can_access_owned_objects_only and \
|
||||
not self.user.is_superuser and \
|
||||
self.user_owned_objects_field in list_display:
|
||||
list_display.remove(self.user_owned_objects_field)
|
||||
return list_display
|
||||
|
||||
site.register_plugin(ModelPermissionPlugin, ModelAdminView)
|
||||
|
||||
|
||||
class AccountMenuPlugin(BaseAdminPlugin):
|
||||
|
||||
def block_top_account_menu(self, context, nodes):
|
||||
return '<li><a href="%s"><i class="fa fa-key"></i> %s</a></li>' % (self.get_admin_url('account_password'), _('Change Password'))
|
||||
|
||||
site.register_plugin(AccountMenuPlugin, CommAdminView)
|
||||
|
||||
|
||||
class ChangePasswordView(ModelAdminView):
|
||||
model = User
|
||||
change_password_form = AdminPasswordChangeForm
|
||||
change_user_password_template = None
|
||||
|
||||
@csrf_protect_m
|
||||
def get(self, request, object_id):
|
||||
if not self.has_change_permission(request):
|
||||
raise PermissionDenied
|
||||
self.obj = self.get_object(unquote(object_id))
|
||||
self.form = self.change_password_form(self.obj)
|
||||
|
||||
return self.get_response()
|
||||
|
||||
def get_media(self):
|
||||
media = super(ChangePasswordView, self).get_media()
|
||||
media = media + self.vendor('xadmin.form.css', 'xadmin.page.form.js') + self.form.media
|
||||
return media
|
||||
|
||||
def get_context(self):
|
||||
context = super(ChangePasswordView, self).get_context()
|
||||
helper = FormHelper()
|
||||
helper.form_tag = False
|
||||
helper.include_media = False
|
||||
self.form.helper = helper
|
||||
context.update({
|
||||
'title': _('Change password: %s') % escape(smart_text(self.obj)),
|
||||
'form': self.form,
|
||||
'has_delete_permission': False,
|
||||
'has_change_permission': True,
|
||||
'has_view_permission': True,
|
||||
'original': self.obj,
|
||||
})
|
||||
return context
|
||||
|
||||
def get_response(self):
|
||||
return TemplateResponse(self.request, [
|
||||
self.change_user_password_template or
|
||||
'xadmin/auth/user/change_password.html'
|
||||
], self.get_context())
|
||||
|
||||
@method_decorator(sensitive_post_parameters())
|
||||
@csrf_protect_m
|
||||
def post(self, request, object_id):
|
||||
if not self.has_change_permission(request):
|
||||
raise PermissionDenied
|
||||
self.obj = self.get_object(unquote(object_id))
|
||||
self.form = self.change_password_form(self.obj, request.POST)
|
||||
|
||||
if self.form.is_valid():
|
||||
self.form.save()
|
||||
self.message_user(_('Password changed successfully.'), 'success')
|
||||
return HttpResponseRedirect(self.model_admin_url('change', self.obj.pk))
|
||||
else:
|
||||
return self.get_response()
|
||||
|
||||
|
||||
class ChangeAccountPasswordView(ChangePasswordView):
|
||||
change_password_form = PasswordChangeForm
|
||||
|
||||
@csrf_protect_m
|
||||
def get(self, request):
|
||||
self.obj = self.user
|
||||
self.form = self.change_password_form(self.obj)
|
||||
|
||||
return self.get_response()
|
||||
|
||||
def get_context(self):
|
||||
context = super(ChangeAccountPasswordView, self).get_context()
|
||||
context.update({
|
||||
'title': _('Change password'),
|
||||
'account_view': True,
|
||||
})
|
||||
return context
|
||||
|
||||
@method_decorator(sensitive_post_parameters())
|
||||
@csrf_protect_m
|
||||
def post(self, request):
|
||||
self.obj = self.user
|
||||
self.form = self.change_password_form(self.obj, request.POST)
|
||||
|
||||
if self.form.is_valid():
|
||||
self.form.save()
|
||||
self.message_user(_('Password changed successfully.'), 'success')
|
||||
return HttpResponseRedirect(self.get_admin_url('index'))
|
||||
else:
|
||||
return self.get_response()
|
||||
|
||||
|
||||
user_model = settings.AUTH_USER_MODEL.lower().replace('.','/')
|
||||
site.register_view(r'^%s/(.+)/password/$' % user_model,
|
||||
ChangePasswordView, name='user_change_password')
|
||||
site.register_view(r'^account/password/$', ChangeAccountPasswordView,
|
||||
name='account_password')
|
||||
156
lib/xadmin/plugins/batch.py
Normal file
156
lib/xadmin/plugins/batch.py
Normal file
@@ -0,0 +1,156 @@
|
||||
|
||||
import copy
|
||||
from django import forms
|
||||
from django.db import models
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.forms.models import modelform_factory
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
from xadmin.layout import FormHelper, Layout, Fieldset, Container, Col
|
||||
from xadmin.plugins.actions import BaseActionView, ACTION_CHECKBOX_NAME
|
||||
from xadmin.util import model_ngettext, vendor
|
||||
from xadmin.views.base import filter_hook
|
||||
from xadmin.views.edit import ModelFormAdminView
|
||||
|
||||
BATCH_CHECKBOX_NAME = '_batch_change_fields'
|
||||
|
||||
|
||||
class ChangeFieldWidgetWrapper(forms.Widget):
|
||||
|
||||
def __init__(self, widget):
|
||||
self.needs_multipart_form = widget.needs_multipart_form
|
||||
self.attrs = widget.attrs
|
||||
self.widget = widget
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
obj = copy.copy(self)
|
||||
obj.widget = copy.deepcopy(self.widget, memo)
|
||||
obj.attrs = self.widget.attrs
|
||||
memo[id(self)] = obj
|
||||
return obj
|
||||
|
||||
@property
|
||||
def media(self):
|
||||
media = self.widget.media + vendor('xadmin.plugin.batch.js')
|
||||
return media
|
||||
|
||||
def render(self, name, value, attrs=None):
|
||||
output = []
|
||||
is_required = self.widget.is_required
|
||||
output.append(u'<label class="btn btn-info btn-xs">'
|
||||
'<input type="checkbox" class="batch-field-checkbox" name="%s" value="%s"%s/> %s</label>' %
|
||||
(BATCH_CHECKBOX_NAME, name, (is_required and ' checked="checked"' or ''), _('Change this field')))
|
||||
output.extend([('<div class="control-wrap" style="margin-top: 10px;%s" id="id_%s_wrap_container">' %
|
||||
((not is_required and 'display: none;' or ''), name)),
|
||||
self.widget.render(name, value, attrs), '</div>'])
|
||||
return mark_safe(u''.join(output))
|
||||
|
||||
def build_attrs(self, extra_attrs=None, **kwargs):
|
||||
"Helper function for building an attribute dictionary."
|
||||
self.attrs = self.widget.build_attrs(extra_attrs=None, **kwargs)
|
||||
return self.attrs
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
return self.widget.value_from_datadict(data, files, name)
|
||||
|
||||
def id_for_label(self, id_):
|
||||
return self.widget.id_for_label(id_)
|
||||
|
||||
class BatchChangeAction(BaseActionView):
|
||||
|
||||
action_name = "change_selected"
|
||||
description = ugettext_lazy(
|
||||
u'Batch Change selected %(verbose_name_plural)s')
|
||||
|
||||
batch_change_form_template = None
|
||||
|
||||
model_perm = 'change'
|
||||
|
||||
batch_fields = []
|
||||
|
||||
def change_models(self, queryset, cleaned_data):
|
||||
n = queryset.count()
|
||||
|
||||
data = {}
|
||||
fields = self.opts.fields + self.opts.many_to_many
|
||||
for f in fields:
|
||||
if not f.editable or isinstance(f, models.AutoField) \
|
||||
or not f.name in cleaned_data:
|
||||
continue
|
||||
data[f] = cleaned_data[f.name]
|
||||
|
||||
if n:
|
||||
for obj in queryset:
|
||||
for f, v in data.items():
|
||||
f.save_form_data(obj, v)
|
||||
obj.save()
|
||||
self.message_user(_("Successfully change %(count)d %(items)s.") % {
|
||||
"count": n, "items": model_ngettext(self.opts, n)
|
||||
}, 'success')
|
||||
|
||||
def get_change_form(self, is_post, fields):
|
||||
edit_view = self.get_model_view(ModelFormAdminView, self.model)
|
||||
|
||||
def formfield_for_dbfield(db_field, **kwargs):
|
||||
formfield = edit_view.formfield_for_dbfield(db_field, required=is_post, **kwargs)
|
||||
formfield.widget = ChangeFieldWidgetWrapper(formfield.widget)
|
||||
return formfield
|
||||
|
||||
defaults = {
|
||||
"form": edit_view.form,
|
||||
"fields": fields,
|
||||
"formfield_callback": formfield_for_dbfield,
|
||||
}
|
||||
return modelform_factory(self.model, **defaults)
|
||||
|
||||
def do_action(self, queryset):
|
||||
if not self.has_change_permission():
|
||||
raise PermissionDenied
|
||||
|
||||
change_fields = [f for f in self.request.POST.getlist(BATCH_CHECKBOX_NAME) if f in self.batch_fields]
|
||||
|
||||
if change_fields and self.request.POST.get('post'):
|
||||
self.form_obj = self.get_change_form(True, change_fields)(
|
||||
data=self.request.POST, files=self.request.FILES)
|
||||
if self.form_obj.is_valid():
|
||||
self.change_models(queryset, self.form_obj.cleaned_data)
|
||||
return None
|
||||
else:
|
||||
self.form_obj = self.get_change_form(False, self.batch_fields)()
|
||||
|
||||
helper = FormHelper()
|
||||
helper.form_tag = False
|
||||
helper.include_media = False
|
||||
helper.add_layout(Layout(Container(Col('full',
|
||||
Fieldset("", *self.form_obj.fields.keys(), css_class="unsort no_title"), horizontal=True, span=12)
|
||||
)))
|
||||
self.form_obj.helper = helper
|
||||
count = len(queryset)
|
||||
if count == 1:
|
||||
objects_name = force_text(self.opts.verbose_name)
|
||||
else:
|
||||
objects_name = force_text(self.opts.verbose_name_plural)
|
||||
|
||||
context = self.get_context()
|
||||
context.update({
|
||||
"title": _("Batch change %s") % objects_name,
|
||||
'objects_name': objects_name,
|
||||
'form': self.form_obj,
|
||||
'queryset': queryset,
|
||||
'count': count,
|
||||
"opts": self.opts,
|
||||
"app_label": self.app_label,
|
||||
'action_checkbox_name': ACTION_CHECKBOX_NAME,
|
||||
})
|
||||
|
||||
return TemplateResponse(self.request, self.batch_change_form_template or
|
||||
self.get_template_list('views/batch_change_form.html'), context)
|
||||
|
||||
@filter_hook
|
||||
def get_media(self):
|
||||
media = super(BatchChangeAction, self).get_media()
|
||||
media = media + self.form_obj.media + self.vendor(
|
||||
'xadmin.page.form.js', 'xadmin.form.css')
|
||||
return media
|
||||
236
lib/xadmin/plugins/bookmark.py
Normal file
236
lib/xadmin/plugins/bookmark.py
Normal file
@@ -0,0 +1,236 @@
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.urls.base import reverse
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from django.forms import ModelChoiceField
|
||||
from django.http import QueryDict
|
||||
from django.template import loader
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.encoding import smart_text
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.decorators.csrf import csrf_protect
|
||||
|
||||
from xadmin.filters import FILTER_PREFIX, SEARCH_VAR
|
||||
from xadmin.plugins.relate import RELATE_PREFIX
|
||||
from xadmin.plugins.utils import get_context_dict
|
||||
from xadmin.sites import site
|
||||
from xadmin.views import ModelAdminView, BaseAdminPlugin, ListAdminView
|
||||
from xadmin.views.list import COL_LIST_VAR, ORDER_VAR
|
||||
from xadmin.views.dashboard import widget_manager, BaseWidget, PartialBaseWidget
|
||||
|
||||
from xadmin.models import Bookmark
|
||||
|
||||
csrf_protect_m = method_decorator(csrf_protect)
|
||||
|
||||
|
||||
class BookmarkPlugin(BaseAdminPlugin):
|
||||
|
||||
# [{'title': "Female", 'query': {'gender': True}, 'order': ('-age'), 'cols': ('first_name', 'age', 'phones'), 'search': 'Tom'}]
|
||||
list_bookmarks = []
|
||||
show_bookmarks = True
|
||||
|
||||
def has_change_permission(self, obj=None):
|
||||
if not obj or self.user.is_superuser:
|
||||
return True
|
||||
else:
|
||||
return obj.user == self.user
|
||||
|
||||
def get_context(self, context):
|
||||
if not self.show_bookmarks:
|
||||
return context
|
||||
|
||||
bookmarks = []
|
||||
|
||||
current_qs = '&'.join([
|
||||
'%s=%s' % (k, v)
|
||||
for k, v in sorted(filter(
|
||||
lambda i: bool(i[1] and (
|
||||
i[0] in (COL_LIST_VAR, ORDER_VAR, SEARCH_VAR)
|
||||
or i[0].startswith(FILTER_PREFIX)
|
||||
or i[0].startswith(RELATE_PREFIX)
|
||||
)),
|
||||
self.request.GET.items()
|
||||
))
|
||||
])
|
||||
|
||||
model_info = (self.opts.app_label, self.opts.model_name)
|
||||
has_selected = False
|
||||
menu_title = _(u"Bookmark")
|
||||
list_base_url = reverse('xadmin:%s_%s_changelist' %
|
||||
model_info, current_app=self.admin_site.name)
|
||||
|
||||
# local bookmarks
|
||||
for bk in self.list_bookmarks:
|
||||
title = bk['title']
|
||||
params = dict([
|
||||
(FILTER_PREFIX + k, v)
|
||||
for (k, v) in bk['query'].items()
|
||||
])
|
||||
if 'order' in bk:
|
||||
params[ORDER_VAR] = '.'.join(bk['order'])
|
||||
if 'cols' in bk:
|
||||
params[COL_LIST_VAR] = '.'.join(bk['cols'])
|
||||
if 'search' in bk:
|
||||
params[SEARCH_VAR] = bk['search']
|
||||
|
||||
def check_item(i):
|
||||
return bool(i[1]) or i[1] == False
|
||||
bk_qs = '&'.join([
|
||||
'%s=%s' % (k, v)
|
||||
for k, v in sorted(filter(check_item, params.items()))
|
||||
])
|
||||
|
||||
url = list_base_url + '?' + bk_qs
|
||||
selected = (current_qs == bk_qs)
|
||||
|
||||
bookmarks.append(
|
||||
{'title': title, 'selected': selected, 'url': url})
|
||||
if selected:
|
||||
menu_title = title
|
||||
has_selected = True
|
||||
|
||||
content_type = ContentType.objects.get_for_model(self.model)
|
||||
bk_model_info = (Bookmark._meta.app_label, Bookmark._meta.model_name)
|
||||
bookmarks_queryset = Bookmark.objects.filter(
|
||||
content_type=content_type,
|
||||
url_name='xadmin:%s_%s_changelist' % model_info
|
||||
).filter(Q(user=self.user) | Q(is_share=True))
|
||||
|
||||
for bk in bookmarks_queryset:
|
||||
selected = (current_qs == bk.query)
|
||||
|
||||
if self.has_change_permission(bk):
|
||||
change_or_detail = 'change'
|
||||
else:
|
||||
change_or_detail = 'detail'
|
||||
|
||||
bookmarks.append({'title': bk.title, 'selected': selected, 'url': bk.url, 'edit_url':
|
||||
reverse('xadmin:%s_%s_%s' % (bk_model_info[0], bk_model_info[1], change_or_detail),
|
||||
args=(bk.id,))})
|
||||
if selected:
|
||||
menu_title = bk.title
|
||||
has_selected = True
|
||||
|
||||
post_url = reverse('xadmin:%s_%s_bookmark' % model_info,
|
||||
current_app=self.admin_site.name)
|
||||
|
||||
new_context = {
|
||||
'bk_menu_title': menu_title,
|
||||
'bk_bookmarks': bookmarks,
|
||||
'bk_current_qs': current_qs,
|
||||
'bk_has_selected': has_selected,
|
||||
'bk_list_base_url': list_base_url,
|
||||
'bk_post_url': post_url,
|
||||
'has_add_permission_bookmark': self.admin_view.request.user.has_perm('xadmin.add_bookmark'),
|
||||
'has_change_permission_bookmark': self.admin_view.request.user.has_perm('xadmin.change_bookmark')
|
||||
}
|
||||
context.update(new_context)
|
||||
return context
|
||||
|
||||
# Media
|
||||
def get_media(self, media):
|
||||
return media + self.vendor('xadmin.plugin.bookmark.js')
|
||||
|
||||
# Block Views
|
||||
def block_nav_menu(self, context, nodes):
|
||||
if self.show_bookmarks:
|
||||
nodes.insert(0, loader.render_to_string('xadmin/blocks/model_list.nav_menu.bookmarks.html',
|
||||
context=get_context_dict(context)))
|
||||
|
||||
|
||||
class BookmarkView(ModelAdminView):
|
||||
|
||||
@csrf_protect_m
|
||||
@transaction.atomic
|
||||
def post(self, request):
|
||||
model_info = (self.opts.app_label, self.opts.model_name)
|
||||
url_name = 'xadmin:%s_%s_changelist' % model_info
|
||||
bookmark = Bookmark(
|
||||
content_type=ContentType.objects.get_for_model(self.model),
|
||||
title=request.POST[
|
||||
'title'], user=self.user, query=request.POST.get('query', ''),
|
||||
is_share=request.POST.get('is_share', 0), url_name=url_name)
|
||||
bookmark.save()
|
||||
content = {'title': bookmark.title, 'url': bookmark.url}
|
||||
return self.render_response(content)
|
||||
|
||||
|
||||
class BookmarkAdmin(object):
|
||||
|
||||
model_icon = 'fa fa-book'
|
||||
list_display = ('title', 'user', 'url_name', 'query')
|
||||
list_display_links = ('title',)
|
||||
user_fields = ['user']
|
||||
hidden_menu = True
|
||||
|
||||
def queryset(self):
|
||||
if self.user.is_superuser:
|
||||
return Bookmark.objects.all()
|
||||
return Bookmark.objects.filter(Q(user=self.user) | Q(is_share=True))
|
||||
|
||||
def get_list_display(self):
|
||||
list_display = super(BookmarkAdmin, self).get_list_display()
|
||||
if not self.user.is_superuser:
|
||||
list_display.remove('user')
|
||||
return list_display
|
||||
|
||||
def has_change_permission(self, obj=None):
|
||||
if not obj or self.user.is_superuser:
|
||||
return True
|
||||
else:
|
||||
return obj.user == self.user
|
||||
|
||||
|
||||
@widget_manager.register
|
||||
class BookmarkWidget(PartialBaseWidget):
|
||||
widget_type = _('bookmark')
|
||||
widget_icon = 'fa fa-bookmark'
|
||||
description = _(
|
||||
'Bookmark Widget, can show user\'s bookmark list data in widget.')
|
||||
template = "xadmin/widgets/list.html"
|
||||
|
||||
bookmark = ModelChoiceField(
|
||||
label=_('Bookmark'), queryset=Bookmark.objects.all(), required=False)
|
||||
|
||||
def setup(self):
|
||||
BaseWidget.setup(self)
|
||||
|
||||
bookmark = self.cleaned_data['bookmark']
|
||||
model = bookmark.content_type.model_class()
|
||||
data = QueryDict(bookmark.query)
|
||||
self.bookmark = bookmark
|
||||
|
||||
if not self.title:
|
||||
self.title = smart_text(bookmark)
|
||||
|
||||
req = self.make_get_request("", data.items())
|
||||
self.list_view = self.get_view_class(
|
||||
ListAdminView, model, list_per_page=10, list_editable=[])(req)
|
||||
|
||||
def has_perm(self):
|
||||
return True
|
||||
|
||||
def context(self, context):
|
||||
list_view = self.list_view
|
||||
list_view.make_result_list()
|
||||
|
||||
base_fields = list_view.base_list_display
|
||||
if len(base_fields) > 5:
|
||||
base_fields = base_fields[0:5]
|
||||
|
||||
context['result_headers'] = [c for c in list_view.result_headers(
|
||||
).cells if c.field_name in base_fields]
|
||||
context['results'] = [
|
||||
[o for i, o in enumerate(filter(
|
||||
lambda c: c.field_name in base_fields,
|
||||
r.cells
|
||||
))]
|
||||
for r in list_view.results()
|
||||
]
|
||||
context['result_count'] = list_view.result_count
|
||||
context['page_url'] = self.bookmark.url
|
||||
|
||||
site.register(Bookmark, BookmarkAdmin)
|
||||
site.register_plugin(BookmarkPlugin, ListAdminView)
|
||||
site.register_modelview(r'^bookmark/$', BookmarkView, name='%s_%s_bookmark')
|
||||
160
lib/xadmin/plugins/chart.py
Normal file
160
lib/xadmin/plugins/chart.py
Normal file
@@ -0,0 +1,160 @@
|
||||
|
||||
import calendar
|
||||
import datetime
|
||||
import decimal
|
||||
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db import models
|
||||
from django.http import HttpResponse, HttpResponseNotFound
|
||||
from django.template import loader
|
||||
from django.utils.http import urlencode
|
||||
from django.utils.encoding import force_text, smart_text
|
||||
from django.utils.translation import ugettext_lazy as _, ugettext
|
||||
|
||||
from xadmin.plugins.utils import get_context_dict
|
||||
from xadmin.sites import site
|
||||
from xadmin.views import BaseAdminPlugin, ListAdminView
|
||||
from xadmin.views.dashboard import ModelBaseWidget, widget_manager
|
||||
from xadmin.util import lookup_field, label_for_field, json
|
||||
|
||||
|
||||
@widget_manager.register
|
||||
class ChartWidget(ModelBaseWidget):
|
||||
widget_type = 'chart'
|
||||
description = _('Show models simple chart.')
|
||||
template = 'xadmin/widgets/chart.html'
|
||||
widget_icon = 'fa fa-bar-chart-o'
|
||||
|
||||
def convert(self, data):
|
||||
self.list_params = data.pop('params', {})
|
||||
self.chart = data.pop('chart', None)
|
||||
|
||||
def setup(self):
|
||||
super(ChartWidget, self).setup()
|
||||
|
||||
self.charts = {}
|
||||
self.one_chart = False
|
||||
model_admin = self.admin_site._registry[self.model]
|
||||
chart = self.chart
|
||||
|
||||
if hasattr(model_admin, 'data_charts'):
|
||||
if chart and chart in model_admin.data_charts:
|
||||
self.charts = {chart: model_admin.data_charts[chart]}
|
||||
self.one_chart = True
|
||||
if self.title is None:
|
||||
self.title = model_admin.data_charts[chart].get('title')
|
||||
else:
|
||||
self.charts = model_admin.data_charts
|
||||
if self.title is None:
|
||||
self.title = ugettext(
|
||||
"%s Charts") % self.model._meta.verbose_name_plural
|
||||
|
||||
def filte_choices_model(self, model, modeladmin):
|
||||
return bool(getattr(modeladmin, 'data_charts', None)) and \
|
||||
super(ChartWidget, self).filte_choices_model(model, modeladmin)
|
||||
|
||||
def get_chart_url(self, name, v):
|
||||
return self.model_admin_url('chart', name) + "?" + urlencode(self.list_params)
|
||||
|
||||
def context(self, context):
|
||||
context.update({
|
||||
'charts': [{"name": name, "title": v['title'], 'url': self.get_chart_url(name, v)} for name, v in self.charts.items()],
|
||||
})
|
||||
|
||||
# Media
|
||||
def media(self):
|
||||
return self.vendor('flot.js', 'xadmin.plugin.charts.js')
|
||||
|
||||
|
||||
class JSONEncoder(DjangoJSONEncoder):
|
||||
def default(self, o):
|
||||
if isinstance(o, (datetime.date, datetime.datetime)):
|
||||
return calendar.timegm(o.timetuple()) * 1000
|
||||
elif isinstance(o, decimal.Decimal):
|
||||
return str(o)
|
||||
else:
|
||||
try:
|
||||
return super(JSONEncoder, self).default(o)
|
||||
except Exception:
|
||||
return smart_text(o)
|
||||
|
||||
|
||||
class ChartsPlugin(BaseAdminPlugin):
|
||||
|
||||
data_charts = {}
|
||||
|
||||
def init_request(self, *args, **kwargs):
|
||||
return bool(self.data_charts)
|
||||
|
||||
def get_chart_url(self, name, v):
|
||||
return self.admin_view.model_admin_url('chart', name) + self.admin_view.get_query_string()
|
||||
|
||||
# Media
|
||||
def get_media(self, media):
|
||||
return media + self.vendor('flot.js', 'xadmin.plugin.charts.js')
|
||||
|
||||
# Block Views
|
||||
def block_results_top(self, context, nodes):
|
||||
context.update({
|
||||
'charts': [{"name": name, "title": v['title'], 'url': self.get_chart_url(name, v)} for name, v in self.data_charts.items()],
|
||||
})
|
||||
nodes.append(loader.render_to_string('xadmin/blocks/model_list.results_top.charts.html',
|
||||
context=get_context_dict(context)))
|
||||
|
||||
|
||||
class ChartsView(ListAdminView):
|
||||
|
||||
data_charts = {}
|
||||
|
||||
def get_ordering(self):
|
||||
if 'order' in self.chart:
|
||||
return self.chart['order']
|
||||
else:
|
||||
return super(ChartsView, self).get_ordering()
|
||||
|
||||
def get(self, request, name):
|
||||
if name not in self.data_charts:
|
||||
return HttpResponseNotFound()
|
||||
|
||||
self.chart = self.data_charts[name]
|
||||
|
||||
self.x_field = self.chart['x-field']
|
||||
y_fields = self.chart['y-field']
|
||||
self.y_fields = (
|
||||
y_fields,) if type(y_fields) not in (list, tuple) else y_fields
|
||||
|
||||
datas = [{"data":[], "label": force_text(label_for_field(
|
||||
i, self.model, model_admin=self))} for i in self.y_fields]
|
||||
|
||||
self.make_result_list()
|
||||
|
||||
for obj in self.result_list:
|
||||
xf, attrs, value = lookup_field(self.x_field, obj, self)
|
||||
for i, yfname in enumerate(self.y_fields):
|
||||
yf, yattrs, yv = lookup_field(yfname, obj, self)
|
||||
datas[i]["data"].append((value, yv))
|
||||
|
||||
option = {'series': {'lines': {'show': True}, 'points': {'show': False}},
|
||||
'grid': {'hoverable': True, 'clickable': True}}
|
||||
try:
|
||||
xfield = self.opts.get_field(self.x_field)
|
||||
if type(xfield) in (models.DateTimeField, models.DateField, models.TimeField):
|
||||
option['xaxis'] = {'mode': "time", 'tickLength': 5}
|
||||
if type(xfield) is models.DateField:
|
||||
option['xaxis']['timeformat'] = "%y/%m/%d"
|
||||
elif type(xfield) is models.TimeField:
|
||||
option['xaxis']['timeformat'] = "%H:%M:%S"
|
||||
else:
|
||||
option['xaxis']['timeformat'] = "%y/%m/%d %H:%M:%S"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
option.update(self.chart.get('option', {}))
|
||||
|
||||
content = {'data': datas, 'option': option}
|
||||
result = json.dumps(content, cls=JSONEncoder, ensure_ascii=False)
|
||||
|
||||
return HttpResponse(result)
|
||||
|
||||
site.register_plugin(ChartsPlugin, ListAdminView)
|
||||
site.register_modelview(r'^chart/(.+)/$', ChartsView, name='%s_%s_chart')
|
||||
94
lib/xadmin/plugins/comments.py
Normal file
94
lib/xadmin/plugins/comments.py
Normal file
@@ -0,0 +1,94 @@
|
||||
import xadmin
|
||||
|
||||
from xadmin.layout import *
|
||||
from xadmin.util import username_field
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.comments.models import Comment
|
||||
from django.utils.translation import ugettext_lazy as _, ungettext
|
||||
from django.contrib.comments import get_model
|
||||
from django.contrib.comments.views.moderation import perform_flag, perform_approve, perform_delete
|
||||
|
||||
class UsernameSearch(object):
|
||||
"""The User object may not be auth.User, so we need to provide
|
||||
a mechanism for issuing the equivalent of a .filter(user__username=...)
|
||||
search in CommentAdmin.
|
||||
"""
|
||||
def __str__(self):
|
||||
return 'user__%s' % username_field
|
||||
|
||||
|
||||
class CommentsAdmin(object):
|
||||
form_layout = (
|
||||
Main(
|
||||
Fieldset(None,
|
||||
'content_type', 'object_pk', 'site',
|
||||
css_class='unsort no_title'
|
||||
),
|
||||
Fieldset('Content',
|
||||
'user', 'user_name', 'user_email', 'user_url', 'comment'
|
||||
),
|
||||
),
|
||||
Side(
|
||||
Fieldset(_('Metadata'),
|
||||
'submit_date', 'ip_address', 'is_public', 'is_removed'
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
list_display = ('name', 'content_type', 'object_pk', 'ip_address', 'submit_date', 'is_public', 'is_removed')
|
||||
list_filter = ('submit_date', 'site', 'is_public', 'is_removed')
|
||||
ordering = ('-submit_date',)
|
||||
search_fields = ('comment', UsernameSearch(), 'user_name', 'user_email', 'user_url', 'ip_address')
|
||||
actions = ["flag_comments", "approve_comments", "remove_comments"]
|
||||
model_icon = 'fa fa-comment'
|
||||
|
||||
def get_actions(self):
|
||||
actions = super(CommentsAdmin, self).get_actions()
|
||||
# Only superusers should be able to delete the comments from the DB.
|
||||
if not self.user.is_superuser and 'delete_selected' in actions:
|
||||
actions.pop('delete_selected')
|
||||
if not self.user.has_perm('comments.can_moderate'):
|
||||
if 'approve_comments' in actions:
|
||||
actions.pop('approve_comments')
|
||||
if 'remove_comments' in actions:
|
||||
actions.pop('remove_comments')
|
||||
return actions
|
||||
|
||||
def flag_comments(self, request, queryset):
|
||||
self._bulk_flag(queryset, perform_flag,
|
||||
lambda n: ungettext('flagged', 'flagged', n))
|
||||
flag_comments.short_description = _("Flag selected comments")
|
||||
flag_comments.icon = 'flag'
|
||||
|
||||
def approve_comments(self, request, queryset):
|
||||
self._bulk_flag(queryset, perform_approve,
|
||||
lambda n: ungettext('approved', 'approved', n))
|
||||
approve_comments.short_description = _("Approve selected comments")
|
||||
approve_comments.icon = 'ok'
|
||||
|
||||
def remove_comments(self, request, queryset):
|
||||
self._bulk_flag(queryset, perform_delete,
|
||||
lambda n: ungettext('removed', 'removed', n))
|
||||
remove_comments.short_description = _("Remove selected comments")
|
||||
remove_comments.icon = 'remove-circle'
|
||||
|
||||
def _bulk_flag(self, queryset, action, done_message):
|
||||
"""
|
||||
Flag, approve, or remove some comments from an admin action. Actually
|
||||
calls the `action` argument to perform the heavy lifting.
|
||||
"""
|
||||
n_comments = 0
|
||||
for comment in queryset:
|
||||
action(self.request, comment)
|
||||
n_comments += 1
|
||||
|
||||
msg = ungettext('1 comment was successfully %(action)s.',
|
||||
'%(count)s comments were successfully %(action)s.',
|
||||
n_comments)
|
||||
self.message_user(msg % {'count': n_comments, 'action': done_message(n_comments)}, 'success')
|
||||
|
||||
# Only register the default admin if the model is the built-in comment model
|
||||
# (this won't be true if there's a custom comment app).
|
||||
if 'django.contrib.comments' in settings.INSTALLED_APPS and (get_model() is Comment):
|
||||
xadmin.site.register(Comment, CommentsAdmin)
|
||||
63
lib/xadmin/plugins/details.py
Normal file
63
lib/xadmin/plugins/details.py
Normal file
@@ -0,0 +1,63 @@
|
||||
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.urls.base import reverse, NoReverseMatch
|
||||
from django.db import models
|
||||
|
||||
from xadmin.sites import site
|
||||
from xadmin.views import BaseAdminPlugin, ListAdminView
|
||||
|
||||
|
||||
class DetailsPlugin(BaseAdminPlugin):
|
||||
|
||||
show_detail_fields = []
|
||||
show_all_rel_details = True
|
||||
|
||||
def result_item(self, item, obj, field_name, row):
|
||||
if (self.show_all_rel_details or (field_name in self.show_detail_fields)):
|
||||
rel_obj = None
|
||||
if hasattr(item.field, 'remote_field') and isinstance(item.field.remote_field, models.ManyToOneRel):
|
||||
rel_obj = getattr(obj, field_name)
|
||||
elif field_name in self.show_detail_fields:
|
||||
rel_obj = obj
|
||||
|
||||
if rel_obj:
|
||||
if rel_obj.__class__ in site._registry:
|
||||
try:
|
||||
model_admin = site._registry[rel_obj.__class__]
|
||||
has_view_perm = model_admin(self.admin_view.request).has_view_permission(rel_obj)
|
||||
has_change_perm = model_admin(self.admin_view.request).has_change_permission(rel_obj)
|
||||
except:
|
||||
has_view_perm = self.admin_view.has_model_perm(rel_obj.__class__, 'view')
|
||||
has_change_perm = self.has_model_perm(rel_obj.__class__, 'change')
|
||||
else:
|
||||
has_view_perm = self.admin_view.has_model_perm(rel_obj.__class__, 'view')
|
||||
has_change_perm = self.has_model_perm(rel_obj.__class__, 'change')
|
||||
|
||||
if rel_obj and has_view_perm:
|
||||
opts = rel_obj._meta
|
||||
try:
|
||||
item_res_uri = reverse(
|
||||
'%s:%s_%s_detail' % (self.admin_site.app_name,
|
||||
opts.app_label, opts.model_name),
|
||||
args=(getattr(rel_obj, opts.pk.attname),))
|
||||
if item_res_uri:
|
||||
if has_change_perm:
|
||||
edit_url = reverse(
|
||||
'%s:%s_%s_change' % (self.admin_site.app_name, opts.app_label, opts.model_name),
|
||||
args=(getattr(rel_obj, opts.pk.attname),))
|
||||
else:
|
||||
edit_url = ''
|
||||
item.btns.append('<a data-res-uri="%s" data-edit-uri="%s" class="details-handler" rel="tooltip" title="%s"><i class="fa fa-info-circle"></i></a>'
|
||||
% (item_res_uri, edit_url, _(u'Details of %s') % str(rel_obj)))
|
||||
except NoReverseMatch:
|
||||
pass
|
||||
return item
|
||||
|
||||
# Media
|
||||
def get_media(self, media):
|
||||
if self.show_all_rel_details or self.show_detail_fields:
|
||||
media = media + self.vendor('xadmin.plugin.details.js', 'xadmin.form.css')
|
||||
return media
|
||||
|
||||
site.register_plugin(DetailsPlugin, ListAdminView)
|
||||
167
lib/xadmin/plugins/editable.py
Normal file
167
lib/xadmin/plugins/editable.py
Normal file
@@ -0,0 +1,167 @@
|
||||
from django import template
|
||||
from django.core.exceptions import PermissionDenied, ObjectDoesNotExist
|
||||
from django.db import models, transaction
|
||||
from django.forms.models import modelform_factory
|
||||
from django.forms import Media
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.utils.encoding import force_text, smart_text
|
||||
from django.utils.html import escape, conditional_escape
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext as _
|
||||
from xadmin.plugins.ajax import JsonErrorDict
|
||||
from xadmin.sites import site
|
||||
from xadmin.util import lookup_field, display_for_field, label_for_field, unquote, boolean_icon
|
||||
from xadmin.views import BaseAdminPlugin, ModelFormAdminView, ListAdminView
|
||||
from xadmin.views.base import csrf_protect_m, filter_hook
|
||||
from xadmin.views.edit import ModelFormAdminUtil
|
||||
from xadmin.views.list import EMPTY_CHANGELIST_VALUE
|
||||
from xadmin.layout import FormHelper
|
||||
|
||||
|
||||
class EditablePlugin(BaseAdminPlugin):
|
||||
|
||||
list_editable = []
|
||||
|
||||
def __init__(self, admin_view):
|
||||
super(EditablePlugin, self).__init__(admin_view)
|
||||
self.editable_need_fields = {}
|
||||
|
||||
def init_request(self, *args, **kwargs):
|
||||
active = bool(self.request.method == 'GET' and self.admin_view.has_change_permission() and self.list_editable)
|
||||
if active:
|
||||
self.model_form = self.get_model_view(ModelFormAdminUtil, self.model).form_obj
|
||||
return active
|
||||
|
||||
def result_item(self, item, obj, field_name, row):
|
||||
if self.list_editable and item.field and item.field.editable and (field_name in self.list_editable):
|
||||
pk = getattr(obj, obj._meta.pk.attname)
|
||||
field_label = label_for_field(field_name, obj,
|
||||
model_admin=self.admin_view,
|
||||
return_attr=False
|
||||
)
|
||||
|
||||
item.wraps.insert(0, '<span class="editable-field">%s</span>')
|
||||
item.btns.append((
|
||||
'<a class="editable-handler" title="%s" data-editable-field="%s" data-editable-loadurl="%s">' +
|
||||
'<i class="fa fa-edit"></i></a>') %
|
||||
(_(u"Enter %s") % field_label, field_name, self.admin_view.model_admin_url('patch', pk) + '?fields=' + field_name))
|
||||
|
||||
if field_name not in self.editable_need_fields:
|
||||
self.editable_need_fields[field_name] = item.field
|
||||
return item
|
||||
|
||||
# Media
|
||||
def get_media(self, media):
|
||||
if self.editable_need_fields:
|
||||
|
||||
try:
|
||||
m = self.model_form.media
|
||||
except:
|
||||
m = Media()
|
||||
media = media + m +\
|
||||
self.vendor(
|
||||
'xadmin.plugin.editable.js', 'xadmin.widget.editable.css')
|
||||
return media
|
||||
|
||||
|
||||
class EditPatchView(ModelFormAdminView, ListAdminView):
|
||||
|
||||
def init_request(self, object_id, *args, **kwargs):
|
||||
self.org_obj = self.get_object(unquote(object_id))
|
||||
|
||||
# For list view get new field display html
|
||||
self.pk_attname = self.opts.pk.attname
|
||||
|
||||
if not self.has_change_permission(self.org_obj):
|
||||
raise PermissionDenied
|
||||
|
||||
if self.org_obj is None:
|
||||
raise Http404(_('%(name)s object with primary key %(key)r does not exist.') %
|
||||
{'name': force_text(self.opts.verbose_name), 'key': escape(object_id)})
|
||||
|
||||
def get_new_field_html(self, f):
|
||||
result = self.result_item(self.org_obj, f, {'is_display_first':
|
||||
False, 'object': self.org_obj})
|
||||
return mark_safe(result.text) if result.allow_tags else conditional_escape(result.text)
|
||||
|
||||
def _get_new_field_html(self, field_name):
|
||||
try:
|
||||
f, attr, value = lookup_field(field_name, self.org_obj, self)
|
||||
except (AttributeError, ObjectDoesNotExist):
|
||||
return EMPTY_CHANGELIST_VALUE
|
||||
else:
|
||||
allow_tags = False
|
||||
if f is None:
|
||||
allow_tags = getattr(attr, 'allow_tags', False)
|
||||
boolean = getattr(attr, 'boolean', False)
|
||||
if boolean:
|
||||
allow_tags = True
|
||||
text = boolean_icon(value)
|
||||
else:
|
||||
text = smart_text(value)
|
||||
else:
|
||||
if isinstance(f.rel, models.ManyToOneRel):
|
||||
field_val = getattr(self.org_obj, f.name)
|
||||
if field_val is None:
|
||||
text = EMPTY_CHANGELIST_VALUE
|
||||
else:
|
||||
text = field_val
|
||||
else:
|
||||
text = display_for_field(value, f)
|
||||
return mark_safe(text) if allow_tags else conditional_escape(text)
|
||||
|
||||
@filter_hook
|
||||
def get(self, request, object_id):
|
||||
model_fields = [f.name for f in self.opts.fields]
|
||||
fields = [f for f in request.GET['fields'].split(',') if f in model_fields]
|
||||
defaults = {
|
||||
"form": self.form,
|
||||
"fields": fields,
|
||||
"formfield_callback": self.formfield_for_dbfield,
|
||||
}
|
||||
form_class = modelform_factory(self.model, **defaults)
|
||||
form = form_class(instance=self.org_obj)
|
||||
|
||||
helper = FormHelper()
|
||||
helper.form_tag = False
|
||||
helper.include_media = False
|
||||
form.helper = helper
|
||||
|
||||
s = '{% load i18n crispy_forms_tags %}<form method="post" action="{{action_url}}">{% crispy form %}' + \
|
||||
'<button type="submit" class="btn btn-success btn-block btn-sm">{% trans "Apply" %}</button></form>'
|
||||
t = template.Template(s)
|
||||
c = template.Context({'form': form, 'action_url': self.model_admin_url('patch', self.org_obj.pk)})
|
||||
|
||||
return HttpResponse(t.render(c))
|
||||
|
||||
@filter_hook
|
||||
@csrf_protect_m
|
||||
@transaction.atomic
|
||||
def post(self, request, object_id):
|
||||
model_fields = [f.name for f in self.opts.fields]
|
||||
fields = [f for f in request.POST.keys() if f in model_fields]
|
||||
defaults = {
|
||||
"form": self.form,
|
||||
"fields": fields,
|
||||
"formfield_callback": self.formfield_for_dbfield,
|
||||
}
|
||||
form_class = modelform_factory(self.model, **defaults)
|
||||
form = form_class(
|
||||
instance=self.org_obj, data=request.POST, files=request.FILES)
|
||||
|
||||
result = {}
|
||||
if form.is_valid():
|
||||
form.save(commit=True)
|
||||
result['result'] = 'success'
|
||||
result['new_data'] = form.cleaned_data
|
||||
result['new_html'] = dict(
|
||||
[(f, self.get_new_field_html(f)) for f in fields])
|
||||
else:
|
||||
result['result'] = 'error'
|
||||
result['errors'] = JsonErrorDict(form.errors, form).as_json()
|
||||
|
||||
return self.render_response(result)
|
||||
|
||||
|
||||
site.register_plugin(EditablePlugin, ListAdminView)
|
||||
site.register_modelview(r'^(.+)/patch/$', EditPatchView, name='%s_%s_patch')
|
||||
249
lib/xadmin/plugins/export.py
Normal file
249
lib/xadmin/plugins/export.py
Normal file
@@ -0,0 +1,249 @@
|
||||
import io
|
||||
import datetime
|
||||
import sys
|
||||
from future.utils import iteritems
|
||||
|
||||
from django.http import HttpResponse
|
||||
from django.template import loader
|
||||
from django.utils import six
|
||||
from django.utils.encoding import force_text, smart_text
|
||||
from django.utils.html import escape
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.xmlutils import SimplerXMLGenerator
|
||||
from django.db.models import BooleanField, NullBooleanField
|
||||
|
||||
from xadmin.plugins.utils import get_context_dict
|
||||
from xadmin.sites import site
|
||||
from xadmin.views import BaseAdminPlugin, ListAdminView
|
||||
from xadmin.util import json
|
||||
from xadmin.views.list import ALL_VAR
|
||||
|
||||
try:
|
||||
import xlwt
|
||||
has_xlwt = True
|
||||
except:
|
||||
has_xlwt = False
|
||||
|
||||
try:
|
||||
import xlsxwriter
|
||||
has_xlsxwriter = True
|
||||
except:
|
||||
has_xlsxwriter = False
|
||||
|
||||
|
||||
class ExportMenuPlugin(BaseAdminPlugin):
|
||||
|
||||
list_export = ('xlsx', 'xls', 'csv', 'xml', 'json')
|
||||
export_names = {'xlsx': 'Excel 2007', 'xls': 'Excel', 'csv': 'CSV',
|
||||
'xml': 'XML', 'json': 'JSON'}
|
||||
|
||||
def init_request(self, *args, **kwargs):
|
||||
self.list_export = [
|
||||
f for f in self.list_export
|
||||
if (f != 'xlsx' or has_xlsxwriter) and (f != 'xls' or has_xlwt)]
|
||||
|
||||
def block_top_toolbar(self, context, nodes):
|
||||
if self.list_export:
|
||||
context.update({
|
||||
'show_export_all': self.admin_view.paginator.count > self.admin_view.list_per_page and not ALL_VAR in self.admin_view.request.GET,
|
||||
'form_params': self.admin_view.get_form_params({'_do_': 'export'}, ('export_type',)),
|
||||
'export_types': [{'type': et, 'name': self.export_names[et]} for et in self.list_export],
|
||||
})
|
||||
nodes.append(loader.render_to_string('xadmin/blocks/model_list.top_toolbar.exports.html',
|
||||
context=get_context_dict(context)))
|
||||
|
||||
|
||||
class ExportPlugin(BaseAdminPlugin):
|
||||
|
||||
export_mimes = {'xlsx': 'application/vnd.ms-excel',
|
||||
'xls': 'application/vnd.ms-excel', 'csv': 'text/csv',
|
||||
'xml': 'application/xhtml+xml', 'json': 'application/json'}
|
||||
|
||||
def init_request(self, *args, **kwargs):
|
||||
return self.request.GET.get('_do_') == 'export'
|
||||
|
||||
def _format_value(self, o):
|
||||
if (o.field is None and getattr(o.attr, 'boolean', False)) or \
|
||||
(o.field and isinstance(o.field, (BooleanField, NullBooleanField))):
|
||||
value = o.value
|
||||
elif str(o.text).startswith("<span class='text-muted'>"):
|
||||
value = escape(str(o.text)[25:-7])
|
||||
else:
|
||||
value = escape(str(o.text))
|
||||
return value
|
||||
|
||||
def _get_objects(self, context):
|
||||
headers = [c for c in context['result_headers'].cells if c.export]
|
||||
rows = context['results']
|
||||
|
||||
return [dict([
|
||||
(force_text(headers[i].text), self._format_value(o)) for i, o in
|
||||
enumerate(filter(lambda c:getattr(c, 'export', False), r.cells))]) for r in rows]
|
||||
|
||||
def _get_datas(self, context):
|
||||
rows = context['results']
|
||||
|
||||
new_rows = [[self._format_value(o) for o in
|
||||
filter(lambda c:getattr(c, 'export', False), r.cells)] for r in rows]
|
||||
new_rows.insert(0, [force_text(c.text) for c in context['result_headers'].cells if c.export])
|
||||
return new_rows
|
||||
|
||||
def get_xlsx_export(self, context):
|
||||
datas = self._get_datas(context)
|
||||
output = io.BytesIO()
|
||||
export_header = (
|
||||
self.request.GET.get('export_xlsx_header', 'off') == 'on')
|
||||
|
||||
model_name = self.opts.verbose_name
|
||||
book = xlsxwriter.Workbook(output)
|
||||
sheet = book.add_worksheet(
|
||||
u"%s %s" % (_(u'Sheet'), force_text(model_name)))
|
||||
styles = {'datetime': book.add_format({'num_format': 'yyyy-mm-dd hh:mm:ss'}),
|
||||
'date': book.add_format({'num_format': 'yyyy-mm-dd'}),
|
||||
'time': book.add_format({'num_format': 'hh:mm:ss'}),
|
||||
'header': book.add_format({'font': 'name Times New Roman', 'color': 'red', 'bold': 'on', 'num_format': '#,##0.00'}),
|
||||
'default': book.add_format()}
|
||||
|
||||
if not export_header:
|
||||
datas = datas[1:]
|
||||
for rowx, row in enumerate(datas):
|
||||
for colx, value in enumerate(row):
|
||||
if export_header and rowx == 0:
|
||||
cell_style = styles['header']
|
||||
else:
|
||||
if isinstance(value, datetime.datetime):
|
||||
cell_style = styles['datetime']
|
||||
elif isinstance(value, datetime.date):
|
||||
cell_style = styles['date']
|
||||
elif isinstance(value, datetime.time):
|
||||
cell_style = styles['time']
|
||||
else:
|
||||
cell_style = styles['default']
|
||||
sheet.write(rowx, colx, value, cell_style)
|
||||
book.close()
|
||||
|
||||
output.seek(0)
|
||||
return output.getvalue()
|
||||
|
||||
def get_xls_export(self, context):
|
||||
datas = self._get_datas(context)
|
||||
output = io.BytesIO()
|
||||
export_header = (
|
||||
self.request.GET.get('export_xls_header', 'off') == 'on')
|
||||
|
||||
model_name = self.opts.verbose_name
|
||||
book = xlwt.Workbook(encoding='utf8')
|
||||
sheet = book.add_sheet(
|
||||
u"%s %s" % (_(u'Sheet'), force_text(model_name)))
|
||||
styles = {'datetime': xlwt.easyxf(num_format_str='yyyy-mm-dd hh:mm:ss'),
|
||||
'date': xlwt.easyxf(num_format_str='yyyy-mm-dd'),
|
||||
'time': xlwt.easyxf(num_format_str='hh:mm:ss'),
|
||||
'header': xlwt.easyxf('font: name Times New Roman, color-index red, bold on', num_format_str='#,##0.00'),
|
||||
'default': xlwt.Style.default_style}
|
||||
|
||||
if not export_header:
|
||||
datas = datas[1:]
|
||||
for rowx, row in enumerate(datas):
|
||||
for colx, value in enumerate(row):
|
||||
if export_header and rowx == 0:
|
||||
cell_style = styles['header']
|
||||
else:
|
||||
if isinstance(value, datetime.datetime):
|
||||
cell_style = styles['datetime']
|
||||
elif isinstance(value, datetime.date):
|
||||
cell_style = styles['date']
|
||||
elif isinstance(value, datetime.time):
|
||||
cell_style = styles['time']
|
||||
else:
|
||||
cell_style = styles['default']
|
||||
sheet.write(rowx, colx, value, style=cell_style)
|
||||
book.save(output)
|
||||
|
||||
output.seek(0)
|
||||
return output.getvalue()
|
||||
|
||||
def _format_csv_text(self, t):
|
||||
if isinstance(t, bool):
|
||||
return _('Yes') if t else _('No')
|
||||
t = t.replace('"', '""').replace(',', '\,')
|
||||
cls_str = str if six.PY3 else basestring
|
||||
if isinstance(t, cls_str):
|
||||
t = '"%s"' % t
|
||||
return t
|
||||
|
||||
def get_csv_export(self, context):
|
||||
datas = self._get_datas(context)
|
||||
stream = []
|
||||
|
||||
if self.request.GET.get('export_csv_header', 'off') != 'on':
|
||||
datas = datas[1:]
|
||||
|
||||
for row in datas:
|
||||
stream.append(','.join(map(self._format_csv_text, row)))
|
||||
|
||||
return '\r\n'.join(stream)
|
||||
|
||||
def _to_xml(self, xml, data):
|
||||
if isinstance(data, (list, tuple)):
|
||||
for item in data:
|
||||
xml.startElement("row", {})
|
||||
self._to_xml(xml, item)
|
||||
xml.endElement("row")
|
||||
elif isinstance(data, dict):
|
||||
for key, value in iteritems(data):
|
||||
key = key.replace(' ', '_')
|
||||
xml.startElement(key, {})
|
||||
self._to_xml(xml, value)
|
||||
xml.endElement(key)
|
||||
else:
|
||||
xml.characters(smart_text(data))
|
||||
|
||||
def get_xml_export(self, context):
|
||||
results = self._get_objects(context)
|
||||
stream = io.StringIO()
|
||||
|
||||
xml = SimplerXMLGenerator(stream, "utf-8")
|
||||
xml.startDocument()
|
||||
xml.startElement("objects", {})
|
||||
|
||||
self._to_xml(xml, results)
|
||||
|
||||
xml.endElement("objects")
|
||||
xml.endDocument()
|
||||
|
||||
return stream.getvalue().split('\n')[1]
|
||||
|
||||
def get_json_export(self, context):
|
||||
results = self._get_objects(context)
|
||||
return json.dumps({'objects': results}, ensure_ascii=False,
|
||||
indent=(self.request.GET.get('export_json_format', 'off') == 'on') and 4 or None)
|
||||
|
||||
def get_response(self, response, context, *args, **kwargs):
|
||||
file_type = self.request.GET.get('export_type', 'csv')
|
||||
response = HttpResponse(
|
||||
content_type="%s; charset=UTF-8" % self.export_mimes[file_type])
|
||||
|
||||
file_name = self.opts.verbose_name.replace(' ', '_')
|
||||
response['Content-Disposition'] = ('attachment; filename=%s.%s' % (
|
||||
file_name, file_type)).encode('utf-8')
|
||||
|
||||
response.write(getattr(self, 'get_%s_export' % file_type)(context))
|
||||
return response
|
||||
|
||||
# View Methods
|
||||
def get_result_list(self, __):
|
||||
if self.request.GET.get('all', 'off') == 'on':
|
||||
self.admin_view.list_per_page = sys.maxsize
|
||||
return __()
|
||||
|
||||
def result_header(self, item, field_name, row):
|
||||
item.export = not item.attr or field_name == '__str__' or getattr(item.attr, 'allow_export', True)
|
||||
return item
|
||||
|
||||
def result_item(self, item, obj, field_name, row):
|
||||
item.export = item.field or field_name == '__str__' or getattr(item.attr, 'allow_export', True)
|
||||
return item
|
||||
|
||||
|
||||
site.register_plugin(ExportMenuPlugin, ListAdminView)
|
||||
site.register_plugin(ExportPlugin, ListAdminView)
|
||||
246
lib/xadmin/plugins/filters.py
Normal file
246
lib/xadmin/plugins/filters.py
Normal file
@@ -0,0 +1,246 @@
|
||||
import operator
|
||||
from future.utils import iteritems
|
||||
from xadmin import widgets
|
||||
from xadmin.plugins.utils import get_context_dict
|
||||
|
||||
from django.contrib.admin.utils import get_fields_from_path, lookup_needs_distinct
|
||||
from django.core.exceptions import SuspiciousOperation, ImproperlyConfigured, ValidationError
|
||||
from django.db import models
|
||||
from django.db.models.fields import FieldDoesNotExist
|
||||
from django.db.models.constants import LOOKUP_SEP
|
||||
# from django.db.models.sql.constants import QUERY_TERMS
|
||||
from django.template import loader
|
||||
from django.utils import six
|
||||
from django.utils.encoding import smart_str
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from xadmin.filters import manager as filter_manager, FILTER_PREFIX, SEARCH_VAR, DateFieldListFilter, \
|
||||
RelatedFieldSearchFilter
|
||||
from xadmin.sites import site
|
||||
from xadmin.views import BaseAdminPlugin, ListAdminView
|
||||
from xadmin.util import is_related_field
|
||||
from functools import reduce
|
||||
|
||||
|
||||
class IncorrectLookupParameters(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class FilterPlugin(BaseAdminPlugin):
|
||||
list_filter = ()
|
||||
search_fields = ()
|
||||
free_query_filter = True
|
||||
|
||||
def lookup_allowed(self, lookup, value):
|
||||
model = self.model
|
||||
# Check FKey lookups that are allowed, so that popups produced by
|
||||
# ForeignKeyRawIdWidget, on the basis of ForeignKey.limit_choices_to,
|
||||
# are allowed to work.
|
||||
for l in model._meta.related_fkey_lookups:
|
||||
for k, v in widgets.url_params_from_lookup_dict(l).items():
|
||||
if k == lookup and v == value:
|
||||
return True
|
||||
|
||||
parts = lookup.split(LOOKUP_SEP)
|
||||
|
||||
# Last term in lookup is a query term (__exact, __startswith etc)
|
||||
# This term can be ignored.
|
||||
# if len(parts) > 1 and parts[-1] in QUERY_TERMS:
|
||||
# parts.pop()
|
||||
|
||||
# Special case -- foo__id__exact and foo__id queries are implied
|
||||
# if foo has been specificially included in the lookup list; so
|
||||
# drop __id if it is the last part. However, first we need to find
|
||||
# the pk attribute name.
|
||||
rel_name = None
|
||||
for part in parts[:-1]:
|
||||
try:
|
||||
field = model._meta.get_field(part)
|
||||
except FieldDoesNotExist:
|
||||
# Lookups on non-existants fields are ok, since they're ignored
|
||||
# later.
|
||||
return True
|
||||
if hasattr(field, 'remote_field'):
|
||||
model = field.remote_field.to
|
||||
rel_name = field.remote_field.get_related_field().name
|
||||
elif is_related_field(field):
|
||||
model = field.model
|
||||
rel_name = model._meta.pk.name
|
||||
else:
|
||||
rel_name = None
|
||||
if rel_name and len(parts) > 1 and parts[-1] == rel_name:
|
||||
parts.pop()
|
||||
|
||||
if len(parts) == 1:
|
||||
return True
|
||||
clean_lookup = LOOKUP_SEP.join(parts)
|
||||
return clean_lookup in self.list_filter
|
||||
|
||||
def get_list_queryset(self, queryset):
|
||||
lookup_params = dict([(smart_str(k)[len(FILTER_PREFIX):], v) for k, v in self.admin_view.params.items()
|
||||
if smart_str(k).startswith(FILTER_PREFIX) and v != ''])
|
||||
for p_key, p_val in iteritems(lookup_params):
|
||||
if p_val == "False":
|
||||
lookup_params[p_key] = False
|
||||
use_distinct = False
|
||||
|
||||
# for clean filters
|
||||
self.admin_view.has_query_param = bool(lookup_params)
|
||||
self.admin_view.clean_query_url = self.admin_view.get_query_string(remove=[k for k in self.request.GET.keys() if
|
||||
k.startswith(FILTER_PREFIX)])
|
||||
|
||||
# Normalize the types of keys
|
||||
if not self.free_query_filter:
|
||||
for key, value in lookup_params.items():
|
||||
if not self.lookup_allowed(key, value):
|
||||
raise SuspiciousOperation(
|
||||
"Filtering by %s not allowed" % key)
|
||||
|
||||
self.filter_specs = []
|
||||
if self.list_filter:
|
||||
for list_filter in self.list_filter:
|
||||
if callable(list_filter):
|
||||
# This is simply a custom list filter class.
|
||||
spec = list_filter(self.request, lookup_params,
|
||||
self.model, self)
|
||||
else:
|
||||
field_path = None
|
||||
field_parts = []
|
||||
if isinstance(list_filter, (tuple, list)):
|
||||
# This is a custom FieldListFilter class for a given field.
|
||||
field, field_list_filter_class = list_filter
|
||||
else:
|
||||
# This is simply a field name, so use the default
|
||||
# FieldListFilter class that has been registered for
|
||||
# the type of the given field.
|
||||
field, field_list_filter_class = list_filter, filter_manager.create
|
||||
if not isinstance(field, models.Field):
|
||||
field_path = field
|
||||
field_parts = get_fields_from_path(
|
||||
self.model, field_path)
|
||||
field = field_parts[-1]
|
||||
spec = field_list_filter_class(
|
||||
field, self.request, lookup_params,
|
||||
self.model, self.admin_view, field_path=field_path)
|
||||
|
||||
if len(field_parts) > 1:
|
||||
# Add related model name to title
|
||||
spec.title = "%s %s" % (field_parts[-2].name, spec.title)
|
||||
|
||||
# Check if we need to use distinct()
|
||||
use_distinct = (use_distinct or
|
||||
lookup_needs_distinct(self.opts, field_path))
|
||||
if spec and spec.has_output():
|
||||
try:
|
||||
new_qs = spec.do_filte(queryset)
|
||||
except ValidationError as e:
|
||||
new_qs = None
|
||||
self.admin_view.message_user(_("<b>Filtering error:</b> %s") % e.messages[0], 'error')
|
||||
if new_qs is not None:
|
||||
queryset = new_qs
|
||||
|
||||
self.filter_specs.append(spec)
|
||||
|
||||
self.has_filters = bool(self.filter_specs)
|
||||
self.admin_view.filter_specs = self.filter_specs
|
||||
obj = filter(lambda f: f.is_used, self.filter_specs)
|
||||
if six.PY3:
|
||||
obj = list(obj)
|
||||
self.admin_view.used_filter_num = len(obj)
|
||||
|
||||
try:
|
||||
for key, value in lookup_params.items():
|
||||
use_distinct = (
|
||||
use_distinct or lookup_needs_distinct(self.opts, key))
|
||||
except FieldDoesNotExist as e:
|
||||
raise IncorrectLookupParameters(e)
|
||||
|
||||
try:
|
||||
# fix a bug by david: In demo, quick filter by IDC Name() cannot be used.
|
||||
if isinstance(queryset, models.query.QuerySet) and lookup_params:
|
||||
new_lookup_parames = dict()
|
||||
for k, v in lookup_params.items():
|
||||
list_v = v.split(',')
|
||||
if len(list_v) > 0:
|
||||
new_lookup_parames.update({k: list_v})
|
||||
else:
|
||||
new_lookup_parames.update({k: v})
|
||||
queryset = queryset.filter(**new_lookup_parames)
|
||||
except (SuspiciousOperation, ImproperlyConfigured):
|
||||
raise
|
||||
except Exception as e:
|
||||
raise IncorrectLookupParameters(e)
|
||||
else:
|
||||
if not isinstance(queryset, models.query.QuerySet):
|
||||
pass
|
||||
|
||||
query = self.request.GET.get(SEARCH_VAR, '')
|
||||
|
||||
# Apply keyword searches.
|
||||
def construct_search(field_name):
|
||||
if field_name.startswith('^'):
|
||||
return "%s__istartswith" % field_name[1:]
|
||||
elif field_name.startswith('='):
|
||||
return "%s__iexact" % field_name[1:]
|
||||
elif field_name.startswith('@'):
|
||||
return "%s__search" % field_name[1:]
|
||||
else:
|
||||
return "%s__icontains" % field_name
|
||||
|
||||
if self.search_fields and query:
|
||||
orm_lookups = [construct_search(str(search_field))
|
||||
for search_field in self.search_fields]
|
||||
for bit in query.split():
|
||||
or_queries = [models.Q(**{orm_lookup: bit})
|
||||
for orm_lookup in orm_lookups]
|
||||
queryset = queryset.filter(reduce(operator.or_, or_queries))
|
||||
if not use_distinct:
|
||||
for search_spec in orm_lookups:
|
||||
if lookup_needs_distinct(self.opts, search_spec):
|
||||
use_distinct = True
|
||||
break
|
||||
self.admin_view.search_query = query
|
||||
|
||||
if use_distinct:
|
||||
return queryset.distinct()
|
||||
else:
|
||||
return queryset
|
||||
|
||||
# Media
|
||||
def get_media(self, media):
|
||||
arr = filter(lambda s: isinstance(s, DateFieldListFilter), self.filter_specs)
|
||||
if six.PY3:
|
||||
arr = list(arr)
|
||||
if bool(arr):
|
||||
media = media + self.vendor('datepicker.css', 'datepicker.js',
|
||||
'xadmin.widget.datetime.js')
|
||||
arr = filter(lambda s: isinstance(s, RelatedFieldSearchFilter), self.filter_specs)
|
||||
if six.PY3:
|
||||
arr = list(arr)
|
||||
if bool(arr):
|
||||
media = media + self.vendor(
|
||||
'select.js', 'select.css', 'xadmin.widget.select.js')
|
||||
return media + self.vendor('xadmin.plugin.filters.js')
|
||||
|
||||
# Block Views
|
||||
def block_nav_menu(self, context, nodes):
|
||||
if self.has_filters:
|
||||
nodes.append(loader.render_to_string('xadmin/blocks/model_list.nav_menu.filters.html',
|
||||
context=get_context_dict(context)))
|
||||
|
||||
def block_nav_form(self, context, nodes):
|
||||
if self.search_fields:
|
||||
context = get_context_dict(context or {}) # no error!
|
||||
context.update({
|
||||
'search_var': SEARCH_VAR,
|
||||
'remove_search_url': self.admin_view.get_query_string(remove=[SEARCH_VAR]),
|
||||
'search_form_params': self.admin_view.get_form_params(remove=[SEARCH_VAR])
|
||||
})
|
||||
nodes.append(
|
||||
loader.render_to_string(
|
||||
'xadmin/blocks/model_list.nav_form.search_form.html',
|
||||
context=context)
|
||||
)
|
||||
|
||||
|
||||
site.register_plugin(FilterPlugin, ListAdminView)
|
||||
119
lib/xadmin/plugins/images.py
Normal file
119
lib/xadmin/plugins/images.py
Normal file
@@ -0,0 +1,119 @@
|
||||
from django.db import models
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.safestring import mark_safe
|
||||
from xadmin.sites import site
|
||||
from xadmin.views import BaseAdminPlugin, ModelFormAdminView, DetailAdminView, ListAdminView
|
||||
|
||||
|
||||
def get_gallery_modal():
|
||||
return """
|
||||
<!-- modal-gallery is the modal dialog used for the image gallery -->
|
||||
<div id="modal-gallery" class="modal modal-gallery fade" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title"></h4>
|
||||
</div>
|
||||
<div class="modal-body"><div class="modal-image"><h1 class="loader"><i class="fa-spinner fa-spin fa fa-large loader"></i></h1></div></div>
|
||||
<div class="modal-footer">
|
||||
<a class="btn btn-info modal-prev"><i class="fa fa-arrow-left"></i> <span>%s</span></a>
|
||||
<a class="btn btn-primary modal-next"><span>%s</span> <i class="fa fa-arrow-right"></i></a>
|
||||
<a class="btn btn-success modal-play modal-slideshow" data-slideshow="5000"><i class="fa fa-play"></i> <span>%s</span></a>
|
||||
<a class="btn btn-default modal-download" target="_blank"><i class="fa fa-download"></i> <span>%s</span></a>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div>
|
||||
""" % (_('Previous'), _('Next'), _('Slideshow'), _('Download'))
|
||||
|
||||
|
||||
class AdminImageField(forms.ImageField):
|
||||
|
||||
def widget_attrs(self, widget):
|
||||
return {'label': self.label}
|
||||
|
||||
|
||||
class AdminImageWidget(forms.FileInput):
|
||||
"""
|
||||
A ImageField Widget that shows its current value if it has one.
|
||||
"""
|
||||
def __init__(self, attrs={}):
|
||||
super(AdminImageWidget, self).__init__(attrs)
|
||||
|
||||
def render(self, name, value, attrs=None, renderer=None):
|
||||
output = []
|
||||
if value and hasattr(value, "url"):
|
||||
label = self.attrs.get('label', name)
|
||||
output.append('<a href="%s" target="_blank" title="%s" data-gallery="gallery"><img src="%s" class="field_img"/></a><br/>%s ' %
|
||||
(value.url, label, value.url, _('Change:')))
|
||||
output.append(super(AdminImageWidget, self).render(name, value, attrs, renderer))
|
||||
return mark_safe(u''.join(output))
|
||||
|
||||
|
||||
class ModelDetailPlugin(BaseAdminPlugin):
|
||||
|
||||
def __init__(self, admin_view):
|
||||
super(ModelDetailPlugin, self).__init__(admin_view)
|
||||
self.include_image = False
|
||||
|
||||
def get_field_attrs(self, attrs, db_field, **kwargs):
|
||||
if isinstance(db_field, models.ImageField):
|
||||
attrs['widget'] = AdminImageWidget
|
||||
attrs['form_class'] = AdminImageField
|
||||
self.include_image = True
|
||||
return attrs
|
||||
|
||||
def get_field_result(self, result, field_name):
|
||||
if isinstance(result.field, models.ImageField):
|
||||
if result.value:
|
||||
img = getattr(result.obj, field_name)
|
||||
result.text = mark_safe('<a href="%s" target="_blank" title="%s" data-gallery="gallery"><img src="%s" class="field_img"/></a>' % (img.url, result.label, img.url))
|
||||
self.include_image = True
|
||||
return result
|
||||
|
||||
# Media
|
||||
def get_media(self, media):
|
||||
if self.include_image:
|
||||
media = media + self.vendor('image-gallery.js',
|
||||
'image-gallery.css')
|
||||
return media
|
||||
|
||||
def block_before_fieldsets(self, context, node):
|
||||
if self.include_image:
|
||||
return '<div id="gallery" data-toggle="modal-gallery" data-target="#modal-gallery">'
|
||||
|
||||
def block_after_fieldsets(self, context, node):
|
||||
if self.include_image:
|
||||
return "</div>"
|
||||
|
||||
def block_extrabody(self, context, node):
|
||||
if self.include_image:
|
||||
return get_gallery_modal()
|
||||
|
||||
|
||||
class ModelListPlugin(BaseAdminPlugin):
|
||||
|
||||
list_gallery = False
|
||||
|
||||
def init_request(self, *args, **kwargs):
|
||||
return bool(self.list_gallery)
|
||||
|
||||
# Media
|
||||
def get_media(self, media):
|
||||
return media + self.vendor('image-gallery.js', 'image-gallery.css')
|
||||
|
||||
def block_results_top(self, context, node):
|
||||
return '<div id="gallery" data-toggle="modal-gallery" data-target="#modal-gallery">'
|
||||
|
||||
def block_results_bottom(self, context, node):
|
||||
return "</div>"
|
||||
|
||||
def block_extrabody(self, context, node):
|
||||
return get_gallery_modal()
|
||||
|
||||
|
||||
site.register_plugin(ModelDetailPlugin, DetailAdminView)
|
||||
site.register_plugin(ModelDetailPlugin, ModelFormAdminView)
|
||||
site.register_plugin(ModelListPlugin, ListAdminView)
|
||||
462
lib/xadmin/plugins/importexport.py
Executable file
462
lib/xadmin/plugins/importexport.py
Executable file
@@ -0,0 +1,462 @@
|
||||
#!/usr/bin/env python
|
||||
# encoding=utf-8
|
||||
"""
|
||||
Author:zcyuefan
|
||||
Topic:django-import-export plugin for xadmin to help importing and exporting data using .csv/.xls/.../.json files
|
||||
|
||||
Use:
|
||||
+++ settings.py +++
|
||||
INSTALLED_APPS = (
|
||||
...
|
||||
'import_export',
|
||||
)
|
||||
|
||||
+++ model.py +++
|
||||
from django.db import models
|
||||
|
||||
class Foo(models.Model):
|
||||
name = models.CharField(max_length=64)
|
||||
description = models.TextField()
|
||||
|
||||
+++ adminx.py +++
|
||||
import xadmin
|
||||
from import_export import resources
|
||||
from .models import Foo
|
||||
|
||||
class FooResource(resources.ModelResource):
|
||||
|
||||
class Meta:
|
||||
model = Foo
|
||||
# fields = ('name', 'description',)
|
||||
# exclude = ()
|
||||
|
||||
|
||||
@xadmin.sites.register(Foo)
|
||||
class FooAdmin(object):
|
||||
import_export_args = {'import_resource_class': FooResource, 'export_resource_class': FooResource}
|
||||
|
||||
++++++++++++++++
|
||||
More info about django-import-export please refer https://github.com/django-import-export/django-import-export
|
||||
"""
|
||||
from datetime import datetime
|
||||
from django.template import loader
|
||||
from xadmin.plugins.utils import get_context_dict
|
||||
from xadmin.sites import site
|
||||
from xadmin.views import BaseAdminPlugin, ListAdminView, ModelAdminView
|
||||
from xadmin.views.base import csrf_protect_m, filter_hook
|
||||
from django.db import transaction
|
||||
from import_export.admin import DEFAULT_FORMATS, SKIP_ADMIN_LOG, TMP_STORAGE_CLASS
|
||||
from import_export.resources import modelresource_factory
|
||||
from import_export.forms import (
|
||||
ImportForm,
|
||||
ConfirmImportForm,
|
||||
ExportForm,
|
||||
)
|
||||
from import_export.results import RowResult
|
||||
from import_export.signals import post_export, post_import
|
||||
try:
|
||||
from django.utils.encoding import force_text
|
||||
except ImportError:
|
||||
from django.utils.encoding import force_unicode as force_text
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.template.response import TemplateResponse
|
||||
from django.contrib.admin.models import LogEntry, ADDITION, CHANGE, DELETION
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib import messages
|
||||
from django.urls.base import reverse
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import HttpResponseRedirect, HttpResponse
|
||||
|
||||
|
||||
class ImportMenuPlugin(BaseAdminPlugin):
|
||||
import_export_args = {}
|
||||
|
||||
def init_request(self, *args, **kwargs):
|
||||
return bool(self.import_export_args.get('import_resource_class'))
|
||||
|
||||
def block_top_toolbar(self, context, nodes):
|
||||
has_change_perm = self.has_model_perm(self.model, 'change')
|
||||
has_add_perm = self.has_model_perm(self.model, 'add')
|
||||
if has_change_perm and has_add_perm:
|
||||
model_info = (self.opts.app_label, self.opts.model_name)
|
||||
import_url = reverse('xadmin:%s_%s_import' % model_info, current_app=self.admin_site.name)
|
||||
context = get_context_dict(context or {}) # no error!
|
||||
context.update({
|
||||
'import_url': import_url,
|
||||
})
|
||||
nodes.append(loader.render_to_string('xadmin/blocks/model_list.top_toolbar.importexport.import.html',
|
||||
context=context))
|
||||
|
||||
|
||||
class ImportBaseView(ModelAdminView):
|
||||
"""
|
||||
"""
|
||||
resource_class = None
|
||||
import_export_args = {}
|
||||
#: template for import view
|
||||
import_template_name = 'xadmin/import_export/import.html'
|
||||
#: resource class
|
||||
#: available import formats
|
||||
formats = DEFAULT_FORMATS
|
||||
#: import data encoding
|
||||
from_encoding = "utf-8"
|
||||
skip_admin_log = None
|
||||
# storage class for saving temporary files
|
||||
tmp_storage_class = None
|
||||
|
||||
def get_skip_admin_log(self):
|
||||
if self.skip_admin_log is None:
|
||||
return SKIP_ADMIN_LOG
|
||||
else:
|
||||
return self.skip_admin_log
|
||||
|
||||
def get_tmp_storage_class(self):
|
||||
if self.tmp_storage_class is None:
|
||||
return TMP_STORAGE_CLASS
|
||||
else:
|
||||
return self.tmp_storage_class
|
||||
|
||||
def get_resource_kwargs(self, request, *args, **kwargs):
|
||||
return {}
|
||||
|
||||
def get_import_resource_kwargs(self, request, *args, **kwargs):
|
||||
return self.get_resource_kwargs(request, *args, **kwargs)
|
||||
|
||||
def get_resource_class(self, usage):
|
||||
if usage == 'import':
|
||||
return self.import_export_args.get('import_resource_class') if self.import_export_args.get(
|
||||
'import_resource_class') else modelresource_factory(self.model)
|
||||
elif usage == 'export':
|
||||
return self.import_export_args.get('export_resource_class') if self.import_export_args.get(
|
||||
'export_resource_class') else modelresource_factory(self.model)
|
||||
else:
|
||||
return modelresource_factory(self.model)
|
||||
|
||||
def get_import_resource_class(self):
|
||||
"""
|
||||
Returns ResourceClass to use for import.
|
||||
"""
|
||||
return self.process_import_resource(self.get_resource_class(usage='import'))
|
||||
|
||||
def process_import_resource(self, resource):
|
||||
"""
|
||||
Returns processed ResourceClass to use for import.
|
||||
Override to custom your own process
|
||||
"""
|
||||
return resource
|
||||
|
||||
def get_import_formats(self):
|
||||
"""
|
||||
Returns available import formats.
|
||||
"""
|
||||
return [f for f in self.formats if f().can_import()]
|
||||
|
||||
|
||||
class ImportView(ImportBaseView):
|
||||
|
||||
def get_media(self):
|
||||
media = super(ImportView, self).get_media()
|
||||
media = media + self.vendor('xadmin.plugin.importexport.css')
|
||||
return media
|
||||
|
||||
@filter_hook
|
||||
def get(self, request, *args, **kwargs):
|
||||
if not (self.has_change_permission() and self.has_add_permission()):
|
||||
raise PermissionDenied
|
||||
|
||||
resource = self.get_import_resource_class()(**self.get_import_resource_kwargs(request, *args, **kwargs))
|
||||
|
||||
context = super(ImportView, self).get_context()
|
||||
|
||||
import_formats = self.get_import_formats()
|
||||
form = ImportForm(import_formats,
|
||||
request.POST or None,
|
||||
request.FILES or None)
|
||||
|
||||
context['title'] = _("Import") + ' ' + self.opts.verbose_name
|
||||
context['form'] = form
|
||||
context['opts'] = self.model._meta
|
||||
context['fields'] = [f.column_name for f in resource.get_user_visible_fields()]
|
||||
|
||||
request.current_app = self.admin_site.name
|
||||
return TemplateResponse(request, [self.import_template_name],
|
||||
context)
|
||||
|
||||
@filter_hook
|
||||
@csrf_protect_m
|
||||
@transaction.atomic
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
Perform a dry_run of the import to make sure the import will not
|
||||
result in errors. If there where no error, save the user
|
||||
uploaded file to a local temp file that will be used by
|
||||
'process_import' for the actual import.
|
||||
"""
|
||||
if not (self.has_change_permission() and self.has_add_permission()):
|
||||
raise PermissionDenied
|
||||
|
||||
resource = self.get_import_resource_class()(**self.get_import_resource_kwargs(request, *args, **kwargs))
|
||||
|
||||
context = super(ImportView, self).get_context()
|
||||
|
||||
import_formats = self.get_import_formats()
|
||||
form = ImportForm(import_formats,
|
||||
request.POST or None,
|
||||
request.FILES or None)
|
||||
|
||||
if request.POST and form.is_valid():
|
||||
input_format = import_formats[
|
||||
int(form.cleaned_data['input_format'])
|
||||
]()
|
||||
import_file = form.cleaned_data['import_file']
|
||||
# first always write the uploaded file to disk as it may be a
|
||||
# memory file or else based on settings upload handlers
|
||||
tmp_storage = self.get_tmp_storage_class()()
|
||||
data = bytes()
|
||||
for chunk in import_file.chunks():
|
||||
data += chunk
|
||||
|
||||
tmp_storage.save(data, input_format.get_read_mode())
|
||||
|
||||
# then read the file, using the proper format-specific mode
|
||||
# warning, big files may exceed memory
|
||||
try:
|
||||
data = tmp_storage.read(input_format.get_read_mode())
|
||||
if not input_format.is_binary() and self.from_encoding:
|
||||
data = force_text(data, self.from_encoding)
|
||||
dataset = input_format.create_dataset(data)
|
||||
except UnicodeDecodeError as e:
|
||||
return HttpResponse(_(u"<h1>Imported file has a wrong encoding: %s</h1>" % e))
|
||||
except Exception as e:
|
||||
return HttpResponse(_(u"<h1>%s encountered while trying to read file: %s</h1>" % (type(e).__name__,
|
||||
import_file.name)))
|
||||
result = resource.import_data(dataset, dry_run=True,
|
||||
raise_errors=False,
|
||||
file_name=import_file.name,
|
||||
user=request.user)
|
||||
|
||||
context['result'] = result
|
||||
|
||||
if not result.has_errors():
|
||||
context['confirm_form'] = ConfirmImportForm(initial={
|
||||
'import_file_name': tmp_storage.name,
|
||||
'original_file_name': import_file.name,
|
||||
'input_format': form.cleaned_data['input_format'],
|
||||
})
|
||||
|
||||
context['title'] = _("Import") + ' ' + self.opts.verbose_name
|
||||
context['form'] = form
|
||||
context['opts'] = self.model._meta
|
||||
context['fields'] = [f.column_name for f in resource.get_user_visible_fields()]
|
||||
|
||||
request.current_app = self.admin_site.name
|
||||
return TemplateResponse(request, [self.import_template_name],
|
||||
context)
|
||||
|
||||
|
||||
class ImportProcessView(ImportBaseView):
|
||||
|
||||
@filter_hook
|
||||
@csrf_protect_m
|
||||
@transaction.atomic
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
Perform the actual import action (after the user has confirmed he
|
||||
wishes to import)
|
||||
"""
|
||||
resource = self.get_import_resource_class()(**self.get_import_resource_kwargs(request, *args, **kwargs))
|
||||
|
||||
confirm_form = ConfirmImportForm(request.POST)
|
||||
if confirm_form.is_valid():
|
||||
import_formats = self.get_import_formats()
|
||||
input_format = import_formats[
|
||||
int(confirm_form.cleaned_data['input_format'])
|
||||
]()
|
||||
tmp_storage = self.get_tmp_storage_class()(name=confirm_form.cleaned_data['import_file_name'])
|
||||
data = tmp_storage.read(input_format.get_read_mode())
|
||||
if not input_format.is_binary() and self.from_encoding:
|
||||
data = force_text(data, self.from_encoding)
|
||||
dataset = input_format.create_dataset(data)
|
||||
|
||||
result = resource.import_data(dataset, dry_run=False,
|
||||
raise_errors=True,
|
||||
file_name=confirm_form.cleaned_data['original_file_name'],
|
||||
user=request.user)
|
||||
|
||||
if not self.get_skip_admin_log():
|
||||
# Add imported objects to LogEntry
|
||||
logentry_map = {
|
||||
RowResult.IMPORT_TYPE_NEW: ADDITION,
|
||||
RowResult.IMPORT_TYPE_UPDATE: CHANGE,
|
||||
RowResult.IMPORT_TYPE_DELETE: DELETION,
|
||||
}
|
||||
content_type_id = ContentType.objects.get_for_model(self.model).pk
|
||||
for row in result:
|
||||
if row.import_type != row.IMPORT_TYPE_ERROR and row.import_type != row.IMPORT_TYPE_SKIP:
|
||||
LogEntry.objects.log_action(
|
||||
user_id=request.user.pk,
|
||||
content_type_id=content_type_id,
|
||||
object_id=row.object_id,
|
||||
object_repr=row.object_repr,
|
||||
action_flag=logentry_map[row.import_type],
|
||||
change_message="%s through import_export" % row.import_type,
|
||||
)
|
||||
success_message = str(_(u'Import finished')) + ' , ' + str(_(u'Add')) + ' : %d' % result.totals[
|
||||
RowResult.IMPORT_TYPE_NEW] + ' , ' + str(_(u'Update')) + ' : %d' % result.totals[
|
||||
RowResult.IMPORT_TYPE_UPDATE]
|
||||
|
||||
messages.success(request, success_message)
|
||||
tmp_storage.remove()
|
||||
|
||||
post_import.send(sender=None, model=self.model)
|
||||
model_info = (self.opts.app_label, self.opts.model_name)
|
||||
url = reverse('xadmin:%s_%s_changelist' % model_info,
|
||||
current_app=self.admin_site.name)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
class ExportMixin(object):
|
||||
#: resource class
|
||||
resource_class = None
|
||||
#: template for change_list view
|
||||
change_list_template = None
|
||||
import_export_args = {}
|
||||
#: template for export view
|
||||
# export_template_name = 'xadmin/import_export/export.html'
|
||||
#: available export formats
|
||||
formats = DEFAULT_FORMATS
|
||||
#: export data encoding
|
||||
to_encoding = "utf-8"
|
||||
list_select_related = None
|
||||
|
||||
def get_resource_kwargs(self, request, *args, **kwargs):
|
||||
return {}
|
||||
|
||||
def get_export_resource_kwargs(self, request, *args, **kwargs):
|
||||
return self.get_resource_kwargs(request, *args, **kwargs)
|
||||
|
||||
def get_resource_class(self, usage):
|
||||
if usage == 'import':
|
||||
return self.import_export_args.get('import_resource_class') if self.import_export_args.get(
|
||||
'import_resource_class') else modelresource_factory(self.model)
|
||||
elif usage == 'export':
|
||||
return self.import_export_args.get('export_resource_class') if self.import_export_args.get(
|
||||
'export_resource_class') else modelresource_factory(self.model)
|
||||
else:
|
||||
return modelresource_factory(self.model)
|
||||
|
||||
def get_export_resource_class(self):
|
||||
"""
|
||||
Returns ResourceClass to use for export.
|
||||
"""
|
||||
return self.get_resource_class(usage='export')
|
||||
|
||||
def get_export_formats(self):
|
||||
"""
|
||||
Returns available export formats.
|
||||
"""
|
||||
return [f for f in self.formats if f().can_export()]
|
||||
|
||||
def get_export_filename(self, file_format):
|
||||
date_str = datetime.now().strftime('%Y-%m-%d-%H%M%S')
|
||||
filename = "%s-%s.%s" % (self.opts.verbose_name.encode('utf-8'),
|
||||
date_str,
|
||||
file_format.get_extension())
|
||||
return filename
|
||||
|
||||
def get_export_queryset(self, request, context):
|
||||
"""
|
||||
Returns export queryset.
|
||||
|
||||
Default implementation respects applied search and filters.
|
||||
"""
|
||||
# scope = self.request.POST.get('_select_across', False) == '1'
|
||||
scope = request.GET.get('scope')
|
||||
select_across = request.GET.get('_select_across', False) == '1'
|
||||
selected = request.GET.get('_selected_actions', '')
|
||||
if scope == 'all':
|
||||
queryset = self.admin_view.queryset()
|
||||
elif scope == 'header_only':
|
||||
queryset = []
|
||||
elif scope == 'selected':
|
||||
if not select_across:
|
||||
selected_pk = selected.split(',')
|
||||
queryset = self.admin_view.queryset().filter(pk__in=selected_pk)
|
||||
else:
|
||||
queryset = self.admin_view.queryset()
|
||||
else:
|
||||
queryset = [r['object'] for r in context['results']]
|
||||
return queryset
|
||||
|
||||
def get_export_data(self, file_format, queryset, *args, **kwargs):
|
||||
"""
|
||||
Returns file_format representation for given queryset.
|
||||
"""
|
||||
request = kwargs.pop("request")
|
||||
resource_class = self.get_export_resource_class()
|
||||
data = resource_class(**self.get_export_resource_kwargs(request)).export(queryset, *args, **kwargs)
|
||||
export_data = file_format.export_data(data)
|
||||
return export_data
|
||||
|
||||
|
||||
class ExportMenuPlugin(ExportMixin, BaseAdminPlugin):
|
||||
import_export_args = {}
|
||||
|
||||
# Media
|
||||
def get_media(self, media):
|
||||
return media + self.vendor('xadmin.plugin.importexport.css', 'xadmin.plugin.importexport.js')
|
||||
|
||||
def init_request(self, *args, **kwargs):
|
||||
return bool(self.import_export_args.get('export_resource_class'))
|
||||
|
||||
def block_top_toolbar(self, context, nodes):
|
||||
formats = self.get_export_formats()
|
||||
form = ExportForm(formats)
|
||||
|
||||
context = get_context_dict(context or {}) # no error!
|
||||
context.update({
|
||||
'form': form,
|
||||
'opts': self.opts,
|
||||
'form_params': self.admin_view.get_form_params({'_action_': 'export'}),
|
||||
})
|
||||
nodes.append(loader.render_to_string('xadmin/blocks/model_list.top_toolbar.importexport.export.html',
|
||||
context=context))
|
||||
|
||||
|
||||
class ExportPlugin(ExportMixin, BaseAdminPlugin):
|
||||
|
||||
def init_request(self, *args, **kwargs):
|
||||
return self.request.GET.get('_action_') == 'export'
|
||||
|
||||
def get_response(self, response, context, *args, **kwargs):
|
||||
has_view_perm = self.has_model_perm(self.model, 'view')
|
||||
if not has_view_perm:
|
||||
raise PermissionDenied
|
||||
|
||||
export_format = self.request.GET.get('file_format')
|
||||
|
||||
if not export_format:
|
||||
messages.warning(self.request, _('You must select an export format.'))
|
||||
else:
|
||||
formats = self.get_export_formats()
|
||||
file_format = formats[int(export_format)]()
|
||||
queryset = self.get_export_queryset(self.request, context)
|
||||
export_data = self.get_export_data(file_format, queryset, request=self.request)
|
||||
content_type = file_format.get_content_type()
|
||||
# Django 1.7 uses the content_type kwarg instead of mimetype
|
||||
try:
|
||||
response = HttpResponse(export_data, content_type=content_type)
|
||||
except TypeError:
|
||||
response = HttpResponse(export_data, mimetype=content_type)
|
||||
response['Content-Disposition'] = 'attachment; filename=%s' % (
|
||||
self.get_export_filename(file_format),
|
||||
)
|
||||
post_export.send(sender=None, model=self.model)
|
||||
return response
|
||||
|
||||
|
||||
site.register_modelview(r'^import/$', ImportView, name='%s_%s_import')
|
||||
site.register_modelview(r'^process_import/$', ImportProcessView, name='%s_%s_process_import')
|
||||
site.register_plugin(ImportMenuPlugin, ListAdminView)
|
||||
site.register_plugin(ExportMenuPlugin, ListAdminView)
|
||||
site.register_plugin(ExportPlugin, ListAdminView)
|
||||
491
lib/xadmin/plugins/inline.py
Normal file
491
lib/xadmin/plugins/inline.py
Normal file
@@ -0,0 +1,491 @@
|
||||
import copy
|
||||
import inspect
|
||||
from django import forms
|
||||
from django.forms.formsets import all_valid, DELETION_FIELD_NAME
|
||||
from django.forms.models import inlineformset_factory, BaseInlineFormSet, modelform_defines_fields
|
||||
from django.contrib.contenttypes.forms import BaseGenericInlineFormSet, generic_inlineformset_factory
|
||||
from django.template import loader
|
||||
from django.template.loader import render_to_string
|
||||
from django.contrib.auth import get_permission_codename
|
||||
from django.utils import six
|
||||
from django.utils.encoding import smart_text
|
||||
from crispy_forms.utils import TEMPLATE_PACK
|
||||
|
||||
from xadmin.layout import FormHelper, Layout, flatatt, Container, Column, Field, Fieldset
|
||||
from xadmin.plugins.utils import get_context_dict
|
||||
from xadmin.sites import site
|
||||
from xadmin.views import BaseAdminPlugin, ModelFormAdminView, DetailAdminView, filter_hook
|
||||
|
||||
|
||||
class ShowField(Field):
|
||||
template = "xadmin/layout/field_value.html"
|
||||
|
||||
def __init__(self, admin_view, *args, **kwargs):
|
||||
super(ShowField, self).__init__(*args, **kwargs)
|
||||
self.admin_view = admin_view
|
||||
if admin_view.style == 'table':
|
||||
self.template = "xadmin/layout/field_value_td.html"
|
||||
|
||||
def render(self, form, form_style, context, template_pack=TEMPLATE_PACK, **kwargs):
|
||||
html = ''
|
||||
detail = form.detail
|
||||
for field in self.fields:
|
||||
if not isinstance(form.fields[field].widget, forms.HiddenInput):
|
||||
result = detail.get_field_result(field)
|
||||
html += loader.render_to_string(
|
||||
self.template, context={'field': form[field], 'result': result})
|
||||
return html
|
||||
|
||||
|
||||
class DeleteField(Field):
|
||||
|
||||
def render(self, form, form_style, context, template_pack=TEMPLATE_PACK, **kwargs):
|
||||
if form.instance.pk:
|
||||
self.attrs['type'] = 'hidden'
|
||||
return super(DeleteField, self).render(form, form_style, context, template_pack=TEMPLATE_PACK, **kwargs)
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
class TDField(Field):
|
||||
template = "xadmin/layout/td-field.html"
|
||||
|
||||
|
||||
class InlineStyleManager(object):
|
||||
inline_styles = {}
|
||||
|
||||
def register_style(self, name, style):
|
||||
self.inline_styles[name] = style
|
||||
|
||||
def get_style(self, name='stacked'):
|
||||
return self.inline_styles.get(name)
|
||||
|
||||
style_manager = InlineStyleManager()
|
||||
|
||||
|
||||
class InlineStyle(object):
|
||||
template = 'xadmin/edit_inline/stacked.html'
|
||||
|
||||
def __init__(self, view, formset):
|
||||
self.view = view
|
||||
self.formset = formset
|
||||
|
||||
def update_layout(self, helper):
|
||||
pass
|
||||
|
||||
def get_attrs(self):
|
||||
return {}
|
||||
style_manager.register_style('stacked', InlineStyle)
|
||||
|
||||
|
||||
class OneInlineStyle(InlineStyle):
|
||||
template = 'xadmin/edit_inline/one.html'
|
||||
style_manager.register_style("one", OneInlineStyle)
|
||||
|
||||
|
||||
class AccInlineStyle(InlineStyle):
|
||||
template = 'xadmin/edit_inline/accordion.html'
|
||||
style_manager.register_style("accordion", AccInlineStyle)
|
||||
|
||||
|
||||
class TabInlineStyle(InlineStyle):
|
||||
template = 'xadmin/edit_inline/tab.html'
|
||||
style_manager.register_style("tab", TabInlineStyle)
|
||||
|
||||
|
||||
class TableInlineStyle(InlineStyle):
|
||||
template = 'xadmin/edit_inline/tabular.html'
|
||||
|
||||
def update_layout(self, helper):
|
||||
helper.add_layout(
|
||||
Layout(*[TDField(f) for f in self.formset[0].fields.keys()]))
|
||||
|
||||
def get_attrs(self):
|
||||
fields = []
|
||||
readonly_fields = []
|
||||
if len(self.formset):
|
||||
fields = [f for k, f in self.formset[0].fields.items() if k != DELETION_FIELD_NAME]
|
||||
readonly_fields = [f for f in getattr(self.formset[0], 'readonly_fields', [])]
|
||||
return {
|
||||
'fields': fields,
|
||||
'readonly_fields': readonly_fields
|
||||
}
|
||||
style_manager.register_style("table", TableInlineStyle)
|
||||
|
||||
|
||||
def replace_field_to_value(layout, av):
|
||||
if layout:
|
||||
cls_str = str if six.PY3 else basestring
|
||||
for i, lo in enumerate(layout.fields):
|
||||
if isinstance(lo, Field) or issubclass(lo.__class__, Field):
|
||||
layout.fields[i] = ShowField(av, *lo.fields, **lo.attrs)
|
||||
elif isinstance(lo, cls_str):
|
||||
layout.fields[i] = ShowField(av, lo)
|
||||
elif hasattr(lo, 'get_field_names'):
|
||||
replace_field_to_value(lo, av)
|
||||
|
||||
|
||||
class InlineModelAdmin(ModelFormAdminView):
|
||||
|
||||
fk_name = None
|
||||
formset = BaseInlineFormSet
|
||||
extra = 3
|
||||
max_num = None
|
||||
can_delete = True
|
||||
fields = []
|
||||
admin_view = None
|
||||
style = 'stacked'
|
||||
|
||||
def init(self, admin_view):
|
||||
self.admin_view = admin_view
|
||||
self.parent_model = admin_view.model
|
||||
self.org_obj = getattr(admin_view, 'org_obj', None)
|
||||
self.model_instance = self.org_obj or admin_view.model()
|
||||
|
||||
return self
|
||||
|
||||
@filter_hook
|
||||
def get_formset(self, **kwargs):
|
||||
"""Returns a BaseInlineFormSet class for use in admin add/change views."""
|
||||
if self.exclude is None:
|
||||
exclude = []
|
||||
else:
|
||||
exclude = list(self.exclude)
|
||||
exclude.extend(self.get_readonly_fields())
|
||||
if self.exclude is None and hasattr(self.form, '_meta') and self.form._meta.exclude:
|
||||
# Take the custom ModelForm's Meta.exclude into account only if the
|
||||
# InlineModelAdmin doesn't define its own.
|
||||
exclude.extend(self.form._meta.exclude)
|
||||
# if exclude is an empty list we use None, since that's the actual
|
||||
# default
|
||||
exclude = exclude or None
|
||||
can_delete = self.can_delete and self.has_delete_permission()
|
||||
defaults = {
|
||||
"form": self.form,
|
||||
"formset": self.formset,
|
||||
"fk_name": self.fk_name,
|
||||
'fields': forms.ALL_FIELDS,
|
||||
"exclude": exclude,
|
||||
"formfield_callback": self.formfield_for_dbfield,
|
||||
"extra": self.extra,
|
||||
"max_num": self.max_num,
|
||||
"can_delete": can_delete,
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
|
||||
return inlineformset_factory(self.parent_model, self.model, **defaults)
|
||||
|
||||
@filter_hook
|
||||
def instance_form(self, **kwargs):
|
||||
formset = self.get_formset(**kwargs)
|
||||
attrs = {
|
||||
'instance': self.model_instance,
|
||||
'queryset': self.queryset()
|
||||
}
|
||||
if self.request_method == 'post':
|
||||
attrs.update({
|
||||
'data': self.request.POST, 'files': self.request.FILES,
|
||||
'save_as_new': "_saveasnew" in self.request.POST
|
||||
})
|
||||
instance = formset(**attrs)
|
||||
instance.view = self
|
||||
|
||||
helper = FormHelper()
|
||||
helper.form_tag = False
|
||||
helper.include_media = False
|
||||
# override form method to prevent render csrf_token in inline forms, see template 'bootstrap/whole_uni_form.html'
|
||||
helper.form_method = 'get'
|
||||
|
||||
style = style_manager.get_style(
|
||||
'one' if self.max_num == 1 else self.style)(self, instance)
|
||||
style.name = self.style
|
||||
|
||||
if len(instance):
|
||||
layout = copy.deepcopy(self.form_layout)
|
||||
|
||||
if layout is None:
|
||||
layout = Layout(*instance[0].fields.keys())
|
||||
elif type(layout) in (list, tuple) and len(layout) > 0:
|
||||
layout = Layout(*layout)
|
||||
|
||||
rendered_fields = [i[1] for i in layout.get_field_names()]
|
||||
layout.extend([f for f in instance[0]
|
||||
.fields.keys() if f not in rendered_fields])
|
||||
|
||||
helper.add_layout(layout)
|
||||
style.update_layout(helper)
|
||||
|
||||
# replace delete field with Dynamic field, for hidden delete field when instance is NEW.
|
||||
helper[DELETION_FIELD_NAME].wrap(DeleteField)
|
||||
|
||||
instance.helper = helper
|
||||
instance.style = style
|
||||
|
||||
readonly_fields = self.get_readonly_fields()
|
||||
if readonly_fields:
|
||||
for form in instance:
|
||||
form.readonly_fields = []
|
||||
inst = form.save(commit=False)
|
||||
if inst:
|
||||
meta_field_names = [field.name for field in inst._meta.get_fields()]
|
||||
for readonly_field in readonly_fields:
|
||||
value = None
|
||||
label = None
|
||||
if readonly_field in meta_field_names:
|
||||
label = inst._meta.get_field(readonly_field).verbose_name
|
||||
value = smart_text(getattr(inst, readonly_field))
|
||||
elif inspect.ismethod(getattr(inst, readonly_field, None)):
|
||||
value = getattr(inst, readonly_field)()
|
||||
label = getattr(getattr(inst, readonly_field), 'short_description', readonly_field)
|
||||
elif inspect.ismethod(getattr(self, readonly_field, None)):
|
||||
value = getattr(self, readonly_field)(inst)
|
||||
label = getattr(getattr(self, readonly_field), 'short_description', readonly_field)
|
||||
if value:
|
||||
form.readonly_fields.append({'label': label, 'contents': value})
|
||||
return instance
|
||||
|
||||
def has_auto_field(self, form):
|
||||
if form._meta.model._meta.has_auto_field:
|
||||
return True
|
||||
for parent in form._meta.model._meta.get_parent_list():
|
||||
if parent._meta.has_auto_field:
|
||||
return True
|
||||
return False
|
||||
|
||||
def queryset(self):
|
||||
queryset = super(InlineModelAdmin, self).queryset()
|
||||
if not self.has_change_permission() and not self.has_view_permission():
|
||||
queryset = queryset.none()
|
||||
return queryset
|
||||
|
||||
def has_add_permission(self):
|
||||
if self.opts.auto_created:
|
||||
return self.has_change_permission()
|
||||
|
||||
codename = get_permission_codename('add', self.opts)
|
||||
return self.user.has_perm("%s.%s" % (self.opts.app_label, codename))
|
||||
|
||||
def has_change_permission(self):
|
||||
opts = self.opts
|
||||
if opts.auto_created:
|
||||
for field in opts.fields:
|
||||
if field.remote_field and field.remote_field.model != self.parent_model:
|
||||
opts = field.remote_field.model._meta
|
||||
break
|
||||
|
||||
codename = get_permission_codename('change', opts)
|
||||
return self.user.has_perm("%s.%s" % (opts.app_label, codename))
|
||||
|
||||
def has_delete_permission(self):
|
||||
if self.opts.auto_created:
|
||||
return self.has_change_permission()
|
||||
|
||||
codename = get_permission_codename('delete', self.opts)
|
||||
return self.user.has_perm("%s.%s" % (self.opts.app_label, codename))
|
||||
|
||||
|
||||
class GenericInlineModelAdmin(InlineModelAdmin):
|
||||
ct_field = "content_type"
|
||||
ct_fk_field = "object_id"
|
||||
|
||||
formset = BaseGenericInlineFormSet
|
||||
|
||||
def get_formset(self, **kwargs):
|
||||
if self.exclude is None:
|
||||
exclude = []
|
||||
else:
|
||||
exclude = list(self.exclude)
|
||||
exclude.extend(self.get_readonly_fields())
|
||||
if self.exclude is None and hasattr(self.form, '_meta') and self.form._meta.exclude:
|
||||
# Take the custom ModelForm's Meta.exclude into account only if the
|
||||
# GenericInlineModelAdmin doesn't define its own.
|
||||
exclude.extend(self.form._meta.exclude)
|
||||
exclude = exclude or None
|
||||
can_delete = self.can_delete and self.has_delete_permission()
|
||||
defaults = {
|
||||
"ct_field": self.ct_field,
|
||||
"fk_field": self.ct_fk_field,
|
||||
"form": self.form,
|
||||
"formfield_callback": self.formfield_for_dbfield,
|
||||
"formset": self.formset,
|
||||
"extra": self.extra,
|
||||
"can_delete": can_delete,
|
||||
"can_order": False,
|
||||
"max_num": self.max_num,
|
||||
"exclude": exclude,
|
||||
'fields': forms.ALL_FIELDS
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
|
||||
return generic_inlineformset_factory(self.model, **defaults)
|
||||
|
||||
|
||||
class InlineFormset(Fieldset):
|
||||
|
||||
def __init__(self, formset, allow_blank=False, **kwargs):
|
||||
self.fields = []
|
||||
self.css_class = kwargs.pop('css_class', '')
|
||||
self.css_id = "%s-group" % formset.prefix
|
||||
self.template = formset.style.template
|
||||
self.inline_style = formset.style.name
|
||||
if allow_blank and len(formset) == 0:
|
||||
self.template = 'xadmin/edit_inline/blank.html'
|
||||
self.inline_style = 'blank'
|
||||
self.formset = formset
|
||||
self.model = formset.model
|
||||
self.opts = formset.model._meta
|
||||
self.flat_attrs = flatatt(kwargs)
|
||||
self.extra_attrs = formset.style.get_attrs()
|
||||
|
||||
def render(self, form, form_style, context, template_pack=TEMPLATE_PACK, **kwargs):
|
||||
context = get_context_dict(context)
|
||||
context.update(dict(
|
||||
formset=self,
|
||||
prefix=self.formset.prefix,
|
||||
inline_style=self.inline_style,
|
||||
**self.extra_attrs
|
||||
))
|
||||
return render_to_string(self.template, context)
|
||||
|
||||
|
||||
class Inline(Fieldset):
|
||||
|
||||
def __init__(self, rel_model):
|
||||
self.model = rel_model
|
||||
self.fields = []
|
||||
super(Inline, self).__init__(legend="")
|
||||
|
||||
def render(self, form, form_style, context, template_pack=TEMPLATE_PACK, **kwargs):
|
||||
return ""
|
||||
|
||||
|
||||
def get_first_field(layout, clz):
|
||||
for layout_object in layout.fields:
|
||||
if issubclass(layout_object.__class__, clz):
|
||||
return layout_object
|
||||
elif hasattr(layout_object, 'get_field_names'):
|
||||
gf = get_first_field(layout_object, clz)
|
||||
if gf:
|
||||
return gf
|
||||
|
||||
|
||||
def replace_inline_objects(layout, fs):
|
||||
if not fs:
|
||||
return
|
||||
for i, layout_object in enumerate(layout.fields):
|
||||
if isinstance(layout_object, Inline) and layout_object.model in fs:
|
||||
layout.fields[i] = fs.pop(layout_object.model)
|
||||
elif hasattr(layout_object, 'get_field_names'):
|
||||
replace_inline_objects(layout_object, fs)
|
||||
|
||||
|
||||
class InlineFormsetPlugin(BaseAdminPlugin):
|
||||
inlines = []
|
||||
|
||||
@property
|
||||
def inline_instances(self):
|
||||
if not hasattr(self, '_inline_instances'):
|
||||
inline_instances = []
|
||||
for inline_class in self.inlines:
|
||||
inline = self.admin_view.get_view(
|
||||
(getattr(inline_class, 'generic_inline', False) and GenericInlineModelAdmin or InlineModelAdmin),
|
||||
inline_class).init(self.admin_view)
|
||||
if not (inline.has_add_permission() or
|
||||
inline.has_change_permission() or
|
||||
inline.has_delete_permission() or
|
||||
inline.has_view_permission()):
|
||||
continue
|
||||
if not inline.has_add_permission():
|
||||
inline.max_num = 0
|
||||
inline_instances.append(inline)
|
||||
self._inline_instances = inline_instances
|
||||
|
||||
return self._inline_instances
|
||||
|
||||
def instance_forms(self, ret):
|
||||
self.formsets = []
|
||||
for inline in self.inline_instances:
|
||||
if inline.has_change_permission():
|
||||
self.formsets.append(inline.instance_form())
|
||||
else:
|
||||
self.formsets.append(self._get_detail_formset_instance(inline))
|
||||
self.admin_view.formsets = self.formsets
|
||||
|
||||
def valid_forms(self, result):
|
||||
return all_valid(self.formsets) and result
|
||||
|
||||
def save_related(self):
|
||||
for formset in self.formsets:
|
||||
formset.instance = self.admin_view.new_obj
|
||||
formset.save()
|
||||
|
||||
def get_context(self, context):
|
||||
context['inline_formsets'] = self.formsets
|
||||
return context
|
||||
|
||||
def get_error_list(self, errors):
|
||||
for fs in self.formsets:
|
||||
errors.extend(fs.non_form_errors())
|
||||
for errors_in_inline_form in fs.errors:
|
||||
errors.extend(errors_in_inline_form.values())
|
||||
return errors
|
||||
|
||||
def get_form_layout(self, layout):
|
||||
allow_blank = isinstance(self.admin_view, DetailAdminView)
|
||||
# fixed #176 bug, change dict to list
|
||||
fs = [(f.model, InlineFormset(f, allow_blank)) for f in self.formsets]
|
||||
replace_inline_objects(layout, fs)
|
||||
|
||||
if fs:
|
||||
container = get_first_field(layout, Column)
|
||||
if not container:
|
||||
container = get_first_field(layout, Container)
|
||||
if not container:
|
||||
container = layout
|
||||
|
||||
# fixed #176 bug, change dict to list
|
||||
for key, value in fs:
|
||||
container.append(value)
|
||||
|
||||
return layout
|
||||
|
||||
def get_media(self, media):
|
||||
for fs in self.formsets:
|
||||
media = media + fs.media
|
||||
if self.formsets:
|
||||
media = media + self.vendor(
|
||||
'xadmin.plugin.formset.js', 'xadmin.plugin.formset.css')
|
||||
return media
|
||||
|
||||
def _get_detail_formset_instance(self, inline):
|
||||
formset = inline.instance_form(extra=0, max_num=0, can_delete=0)
|
||||
formset.detail_page = True
|
||||
if True:
|
||||
replace_field_to_value(formset.helper.layout, inline)
|
||||
model = inline.model
|
||||
opts = model._meta
|
||||
fake_admin_class = type(str('%s%sFakeAdmin' % (opts.app_label, opts.model_name)), (object, ), {'model': model})
|
||||
for form in formset.forms:
|
||||
instance = form.instance
|
||||
if instance.pk:
|
||||
form.detail = self.get_view(
|
||||
DetailAdminUtil, fake_admin_class, instance)
|
||||
return formset
|
||||
|
||||
|
||||
class DetailAdminUtil(DetailAdminView):
|
||||
|
||||
def init_request(self, obj):
|
||||
self.obj = obj
|
||||
self.org_obj = obj
|
||||
|
||||
|
||||
class DetailInlineFormsetPlugin(InlineFormsetPlugin):
|
||||
|
||||
def get_model_form(self, form, **kwargs):
|
||||
self.formsets = [self._get_detail_formset_instance(
|
||||
inline) for inline in self.inline_instances]
|
||||
return form
|
||||
|
||||
site.register_plugin(InlineFormsetPlugin, ModelFormAdminView)
|
||||
site.register_plugin(DetailInlineFormsetPlugin, DetailAdminView)
|
||||
27
lib/xadmin/plugins/language.py
Normal file
27
lib/xadmin/plugins/language.py
Normal file
@@ -0,0 +1,27 @@
|
||||
|
||||
from django.conf import settings
|
||||
from django.template import loader
|
||||
from django.views.i18n import set_language
|
||||
from xadmin.plugins.utils import get_context_dict
|
||||
from xadmin.sites import site
|
||||
from xadmin.views import BaseAdminPlugin, CommAdminView, BaseAdminView
|
||||
|
||||
|
||||
class SetLangNavPlugin(BaseAdminPlugin):
|
||||
|
||||
def block_top_navmenu(self, context, nodes):
|
||||
context = get_context_dict(context)
|
||||
context['redirect_to'] = self.request.get_full_path()
|
||||
nodes.append(loader.render_to_string('xadmin/blocks/comm.top.setlang.html', context=context))
|
||||
|
||||
|
||||
class SetLangView(BaseAdminView):
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if 'nav_menu' in request.session:
|
||||
del request.session['nav_menu']
|
||||
return set_language(request)
|
||||
|
||||
if settings.LANGUAGES and 'django.middleware.locale.LocaleMiddleware' in settings.MIDDLEWARE:
|
||||
site.register_plugin(SetLangNavPlugin, CommAdminView)
|
||||
site.register_view(r'^i18n/setlang/$', SetLangView, 'set_language')
|
||||
81
lib/xadmin/plugins/layout.py
Normal file
81
lib/xadmin/plugins/layout.py
Normal file
@@ -0,0 +1,81 @@
|
||||
# coding=utf-8
|
||||
from django.template import loader
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from xadmin.plugins.utils import get_context_dict
|
||||
from xadmin.sites import site
|
||||
from xadmin.views import BaseAdminPlugin, ListAdminView
|
||||
from xadmin.util import label_for_field
|
||||
|
||||
LAYOUT_VAR = '_layout'
|
||||
|
||||
DEFAULT_LAYOUTS = {
|
||||
'table': {
|
||||
'key': 'table',
|
||||
'icon': 'fa fa-table',
|
||||
'name': _(u'Table'),
|
||||
'template': 'views/model_list.html',
|
||||
},
|
||||
'thumbnails': {
|
||||
'key': 'thumbnails',
|
||||
'icon': 'fa fa-th-large',
|
||||
'name': _(u'Thumbnails'),
|
||||
'template': 'grids/thumbnails.html',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class GridLayoutPlugin(BaseAdminPlugin):
|
||||
|
||||
grid_layouts = []
|
||||
|
||||
_active_layouts = []
|
||||
_current_layout = None
|
||||
_current_icon = 'table'
|
||||
|
||||
def get_layout(self, l):
|
||||
item = (type(l) is dict) and l or DEFAULT_LAYOUTS[l]
|
||||
return dict({'url': self.admin_view.get_query_string({LAYOUT_VAR: item['key']}), 'selected': False}, **item)
|
||||
|
||||
def init_request(self, *args, **kwargs):
|
||||
active = bool(self.request.method == 'GET' and self.grid_layouts)
|
||||
if active:
|
||||
layouts = (type(self.grid_layouts) in (list, tuple)) and self.grid_layouts or (self.grid_layouts,)
|
||||
self._active_layouts = [self.get_layout(l) for l in layouts]
|
||||
self._current_layout = self.request.GET.get(LAYOUT_VAR, self._active_layouts[0]['key'])
|
||||
for layout in self._active_layouts:
|
||||
if self._current_layout == layout['key']:
|
||||
self._current_icon = layout['icon']
|
||||
layout['selected'] = True
|
||||
self.admin_view.object_list_template = self.admin_view.get_template_list(layout['template'])
|
||||
return active
|
||||
|
||||
def result_item(self, item, obj, field_name, row):
|
||||
if self._current_layout == 'thumbnails':
|
||||
if getattr(item.attr, 'is_column', True):
|
||||
item.field_label = label_for_field(
|
||||
field_name, self.model,
|
||||
model_admin=self.admin_view,
|
||||
return_attr=False
|
||||
)
|
||||
if getattr(item.attr, 'thumbnail_img', False):
|
||||
setattr(item, 'thumbnail_hidden', True)
|
||||
row['thumbnail_img'] = item
|
||||
elif item.is_display_link:
|
||||
setattr(item, 'thumbnail_hidden', True)
|
||||
row['thumbnail_label'] = item
|
||||
|
||||
return item
|
||||
|
||||
# Block Views
|
||||
def block_top_toolbar(self, context, nodes):
|
||||
if len(self._active_layouts) > 1:
|
||||
context.update({
|
||||
'layouts': self._active_layouts,
|
||||
'current_icon': self._current_icon,
|
||||
})
|
||||
nodes.append(loader.render_to_string('xadmin/blocks/model_list.top_toolbar.layouts.html',
|
||||
context=get_context_dict(context)))
|
||||
|
||||
|
||||
site.register_plugin(GridLayoutPlugin, ListAdminView)
|
||||
30
lib/xadmin/plugins/mobile.py
Normal file
30
lib/xadmin/plugins/mobile.py
Normal file
@@ -0,0 +1,30 @@
|
||||
#coding:utf-8
|
||||
from xadmin.sites import site
|
||||
from xadmin.views import BaseAdminPlugin, CommAdminView
|
||||
|
||||
|
||||
class MobilePlugin(BaseAdminPlugin):
|
||||
|
||||
def _test_mobile(self):
|
||||
try:
|
||||
return self.request.META['HTTP_USER_AGENT'].find('Android') >= 0 or \
|
||||
self.request.META['HTTP_USER_AGENT'].find('iPhone') >= 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def init_request(self, *args, **kwargs):
|
||||
return self._test_mobile()
|
||||
|
||||
def get_context(self, context):
|
||||
#context['base_template'] = 'xadmin/base_mobile.html'
|
||||
context['is_mob'] = True
|
||||
return context
|
||||
|
||||
# Media
|
||||
# def get_media(self, media):
|
||||
# return media + self.vendor('xadmin.mobile.css', )
|
||||
|
||||
def block_extrahead(self, context, nodes):
|
||||
nodes.append('<script>window.__admin_ismobile__ = true;</script>')
|
||||
|
||||
site.register_plugin(MobilePlugin, CommAdminView)
|
||||
107
lib/xadmin/plugins/multiselect.py
Normal file
107
lib/xadmin/plugins/multiselect.py
Normal file
@@ -0,0 +1,107 @@
|
||||
# coding:utf-8
|
||||
from itertools import chain
|
||||
|
||||
import xadmin
|
||||
from django import forms
|
||||
from django.db.models import ManyToManyField
|
||||
from django.forms.utils import flatatt
|
||||
from django.template import loader
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.html import escape, conditional_escape
|
||||
from django.utils.safestring import mark_safe
|
||||
from xadmin.util import vendor
|
||||
from xadmin.views import BaseAdminPlugin, ModelFormAdminView
|
||||
|
||||
|
||||
class SelectMultipleTransfer(forms.SelectMultiple):
|
||||
|
||||
@property
|
||||
def media(self):
|
||||
return vendor('xadmin.widget.select-transfer.js', 'xadmin.widget.select-transfer.css')
|
||||
|
||||
def __init__(self, verbose_name, is_stacked, attrs=None, choices=()):
|
||||
self.verbose_name = verbose_name
|
||||
self.is_stacked = is_stacked
|
||||
super(SelectMultipleTransfer, self).__init__(attrs, choices)
|
||||
|
||||
def render_opt(self, selected_choices, option_value, option_label):
|
||||
option_value = force_text(option_value)
|
||||
return u'<option value="%s">%s</option>' % (
|
||||
escape(option_value), conditional_escape(force_text(option_label))), bool(option_value in selected_choices)
|
||||
|
||||
def render(self, name, value, attrs=None, choices=()):
|
||||
if attrs is None:
|
||||
attrs = {}
|
||||
attrs['class'] = ''
|
||||
if self.is_stacked:
|
||||
attrs['class'] += 'stacked'
|
||||
if value is None:
|
||||
value = []
|
||||
final_attrs = self.build_attrs(attrs, extra_attrs={'name': name})
|
||||
|
||||
selected_choices = set(force_text(v) for v in value)
|
||||
available_output = []
|
||||
chosen_output = []
|
||||
|
||||
for option_value, option_label in chain(self.choices, choices):
|
||||
if isinstance(option_label, (list, tuple)):
|
||||
available_output.append(u'<optgroup label="%s">' %
|
||||
escape(force_text(option_value)))
|
||||
for option in option_label:
|
||||
output, selected = self.render_opt(
|
||||
selected_choices, *option)
|
||||
if selected:
|
||||
chosen_output.append(output)
|
||||
else:
|
||||
available_output.append(output)
|
||||
available_output.append(u'</optgroup>')
|
||||
else:
|
||||
output, selected = self.render_opt(
|
||||
selected_choices, option_value, option_label)
|
||||
if selected:
|
||||
chosen_output.append(output)
|
||||
else:
|
||||
available_output.append(output)
|
||||
|
||||
context = {
|
||||
'verbose_name': self.verbose_name,
|
||||
'attrs': attrs,
|
||||
'field_id': attrs['id'],
|
||||
'flatatts': flatatt(final_attrs),
|
||||
'available_options': u'\n'.join(available_output),
|
||||
'chosen_options': u'\n'.join(chosen_output),
|
||||
}
|
||||
return mark_safe(loader.render_to_string('xadmin/forms/transfer.html', context))
|
||||
|
||||
|
||||
class SelectMultipleDropdown(forms.SelectMultiple):
|
||||
|
||||
@property
|
||||
def media(self):
|
||||
return vendor('multiselect.js', 'multiselect.css', 'xadmin.widget.multiselect.js')
|
||||
|
||||
def render(self, name, value, attrs=None, choices=()):
|
||||
if attrs is None:
|
||||
attrs = {}
|
||||
attrs['class'] = 'selectmultiple selectdropdown'
|
||||
return super(SelectMultipleDropdown, self).render(name, value, attrs, choices)
|
||||
|
||||
|
||||
class M2MSelectPlugin(BaseAdminPlugin):
|
||||
|
||||
def init_request(self, *args, **kwargs):
|
||||
return hasattr(self.admin_view, 'style_fields') and \
|
||||
(
|
||||
'm2m_transfer' in self.admin_view.style_fields.values() or
|
||||
'm2m_dropdown' in self.admin_view.style_fields.values()
|
||||
)
|
||||
|
||||
def get_field_style(self, attrs, db_field, style, **kwargs):
|
||||
if style == 'm2m_transfer' and isinstance(db_field, ManyToManyField):
|
||||
return {'widget': SelectMultipleTransfer(db_field.verbose_name, False), 'help_text': ''}
|
||||
if style == 'm2m_dropdown' and isinstance(db_field, ManyToManyField):
|
||||
return {'widget': SelectMultipleDropdown, 'help_text': ''}
|
||||
return attrs
|
||||
|
||||
|
||||
xadmin.site.register_plugin(M2MSelectPlugin, ModelFormAdminView)
|
||||
115
lib/xadmin/plugins/passwords.py
Normal file
115
lib/xadmin/plugins/passwords.py
Normal file
@@ -0,0 +1,115 @@
|
||||
# coding=utf-8
|
||||
from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm
|
||||
from django.contrib.auth.tokens import default_token_generator
|
||||
from django.contrib.auth.views import PasswordResetConfirmView as password_reset_confirm
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from xadmin.sites import site
|
||||
from xadmin.views.base import BaseAdminPlugin, BaseAdminView, csrf_protect_m
|
||||
from xadmin.views.website import LoginView
|
||||
|
||||
|
||||
class ResetPasswordSendView(BaseAdminView):
|
||||
|
||||
need_site_permission = False
|
||||
|
||||
password_reset_form = PasswordResetForm
|
||||
password_reset_template = 'xadmin/auth/password_reset/form.html'
|
||||
password_reset_done_template = 'xadmin/auth/password_reset/done.html'
|
||||
password_reset_token_generator = default_token_generator
|
||||
|
||||
password_reset_from_email = None
|
||||
password_reset_email_template = 'xadmin/auth/password_reset/email.html'
|
||||
password_reset_subject_template = None
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
context = super(ResetPasswordSendView, self).get_context()
|
||||
context['form'] = kwargs.get('form', self.password_reset_form())
|
||||
|
||||
return TemplateResponse(request, self.password_reset_template, context)
|
||||
|
||||
@csrf_protect_m
|
||||
def post(self, request, *args, **kwargs):
|
||||
form = self.password_reset_form(request.POST)
|
||||
|
||||
if form.is_valid():
|
||||
opts = {
|
||||
'use_https': request.is_secure(),
|
||||
'token_generator': self.password_reset_token_generator,
|
||||
'email_template_name': self.password_reset_email_template,
|
||||
'request': request,
|
||||
'domain_override': request.get_host()
|
||||
}
|
||||
|
||||
if self.password_reset_from_email:
|
||||
opts['from_email'] = self.password_reset_from_email
|
||||
if self.password_reset_subject_template:
|
||||
opts['subject_template_name'] = self.password_reset_subject_template
|
||||
|
||||
form.save(**opts)
|
||||
context = super(ResetPasswordSendView, self).get_context()
|
||||
return TemplateResponse(request, self.password_reset_done_template, context)
|
||||
else:
|
||||
return self.get(request, form=form)
|
||||
|
||||
|
||||
site.register_view(r'^xadmin/password_reset/$', ResetPasswordSendView, name='xadmin_password_reset')
|
||||
|
||||
|
||||
class ResetLinkPlugin(BaseAdminPlugin):
|
||||
|
||||
def block_form_bottom(self, context, nodes):
|
||||
reset_link = self.get_admin_url('xadmin_password_reset')
|
||||
return '<div class="text-info" style="margin-top:15px;"><a href="%s"><i class="fa fa-question-sign"></i> %s</a></div>' % (reset_link, _('Forgotten your password or username?'))
|
||||
|
||||
|
||||
site.register_plugin(ResetLinkPlugin, LoginView)
|
||||
|
||||
|
||||
class ResetPasswordComfirmView(BaseAdminView):
|
||||
|
||||
need_site_permission = False
|
||||
|
||||
password_reset_set_form = SetPasswordForm
|
||||
password_reset_confirm_template = 'xadmin/auth/password_reset/confirm.html'
|
||||
password_reset_token_generator = default_token_generator
|
||||
|
||||
def do_view(self, request, uidb36, token, *args, **kwargs):
|
||||
context = super(ResetPasswordComfirmView, self).get_context()
|
||||
return password_reset_confirm(request, uidb36, token,
|
||||
template_name=self.password_reset_confirm_template,
|
||||
token_generator=self.password_reset_token_generator,
|
||||
set_password_form=self.password_reset_set_form,
|
||||
post_reset_redirect=self.get_admin_url('xadmin_password_reset_complete'),
|
||||
current_app=self.admin_site.name, extra_context=context)
|
||||
|
||||
def get(self, request, uidb36, token, *args, **kwargs):
|
||||
return self.do_view(request, uidb36, token)
|
||||
|
||||
def post(self, request, uidb36, token, *args, **kwargs):
|
||||
return self.do_view(request, uidb36, token)
|
||||
|
||||
def get_media(self):
|
||||
return super(ResetPasswordComfirmView, self).get_media() + \
|
||||
self.vendor('xadmin.page.form.js', 'xadmin.form.css')
|
||||
|
||||
|
||||
site.register_view(r'^xadmin/password_reset/(?P<uidb36>[0-9A-Za-z]{1,13})-(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
|
||||
ResetPasswordComfirmView, name='xadmin_password_reset_confirm')
|
||||
|
||||
|
||||
class ResetPasswordCompleteView(BaseAdminView):
|
||||
|
||||
need_site_permission = False
|
||||
|
||||
password_reset_complete_template = 'xadmin/auth/password_reset/complete.html'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
context = super(ResetPasswordCompleteView, self).get_context()
|
||||
context['login_url'] = self.get_admin_url('index')
|
||||
|
||||
return TemplateResponse(request, self.password_reset_complete_template, context)
|
||||
|
||||
|
||||
site.register_view(r'^xadmin/password_reset/complete/$', ResetPasswordCompleteView, name='xadmin_password_reset_complete')
|
||||
74
lib/xadmin/plugins/portal.py
Normal file
74
lib/xadmin/plugins/portal.py
Normal file
@@ -0,0 +1,74 @@
|
||||
#coding:utf-8
|
||||
from xadmin.sites import site
|
||||
from xadmin.models import UserSettings
|
||||
from xadmin.views import BaseAdminPlugin, ModelFormAdminView, DetailAdminView
|
||||
from xadmin.layout import Fieldset, Column
|
||||
|
||||
|
||||
class BasePortalPlugin(BaseAdminPlugin):
|
||||
|
||||
# Media
|
||||
def get_media(self, media):
|
||||
return media + self.vendor('xadmin.plugin.portal.js')
|
||||
|
||||
|
||||
def get_layout_objects(layout, clz, objects):
|
||||
for i, layout_object in enumerate(layout.fields):
|
||||
if layout_object.__class__ is clz or issubclass(layout_object.__class__, clz):
|
||||
objects.append(layout_object)
|
||||
elif hasattr(layout_object, 'get_field_names'):
|
||||
get_layout_objects(layout_object, clz, objects)
|
||||
|
||||
|
||||
class ModelFormPlugin(BasePortalPlugin):
|
||||
|
||||
def _portal_key(self):
|
||||
return '%s_%s_editform_portal' % (self.opts.app_label, self.opts.model_name)
|
||||
|
||||
def get_form_helper(self, helper):
|
||||
cs = []
|
||||
layout = helper.layout
|
||||
get_layout_objects(layout, Column, cs)
|
||||
for i, c in enumerate(cs):
|
||||
if not getattr(c, 'css_id', None):
|
||||
c.css_id = 'column-%d' % i
|
||||
|
||||
# make fieldset index
|
||||
fs = []
|
||||
get_layout_objects(layout, Fieldset, fs)
|
||||
fs_map = {}
|
||||
for i, f in enumerate(fs):
|
||||
if not getattr(f, 'css_id', None):
|
||||
f.css_id = 'box-%d' % i
|
||||
fs_map[f.css_id] = f
|
||||
|
||||
try:
|
||||
layout_pos = UserSettings.objects.get(
|
||||
user=self.user, key=self._portal_key()).value
|
||||
layout_cs = layout_pos.split('|')
|
||||
for i, c in enumerate(cs):
|
||||
c.fields = [fs_map.pop(j) for j in layout_cs[i].split(
|
||||
',') if j in fs_map] if len(layout_cs) > i else []
|
||||
if fs_map and cs:
|
||||
cs[0].fields.extend(fs_map.values())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return helper
|
||||
|
||||
def block_form_top(self, context, node):
|
||||
# put portal key and submit url to page
|
||||
return "<input type='hidden' id='_portal_key' value='%s' />" % self._portal_key()
|
||||
|
||||
|
||||
class ModelDetailPlugin(ModelFormPlugin):
|
||||
|
||||
def _portal_key(self):
|
||||
return '%s_%s_detail_portal' % (self.opts.app_label, self.opts.model_name)
|
||||
|
||||
def block_after_fieldsets(self, context, node):
|
||||
# put portal key and submit url to page
|
||||
return "<input type='hidden' id='_portal_key' value='%s' />" % self._portal_key()
|
||||
|
||||
site.register_plugin(ModelFormPlugin, ModelFormAdminView)
|
||||
site.register_plugin(ModelDetailPlugin, DetailAdminView)
|
||||
168
lib/xadmin/plugins/quickfilter.py
Normal file
168
lib/xadmin/plugins/quickfilter.py
Normal file
@@ -0,0 +1,168 @@
|
||||
'''
|
||||
Created on Mar 26, 2014
|
||||
|
||||
@author: LAB_ADM
|
||||
'''
|
||||
from future.utils import iteritems
|
||||
from django.utils import six
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from xadmin.filters import manager, MultiSelectFieldListFilter
|
||||
from xadmin.plugins.filters import *
|
||||
from xadmin.util import is_related_field
|
||||
|
||||
|
||||
@manager.register
|
||||
class QuickFilterMultiSelectFieldListFilter(MultiSelectFieldListFilter):
|
||||
""" Delegates the filter to the default filter and ors the results of each
|
||||
|
||||
Lists the distinct values of each field as a checkbox
|
||||
Uses the default spec for each
|
||||
|
||||
"""
|
||||
template = 'xadmin/filters/quickfilter.html'
|
||||
|
||||
|
||||
class QuickFilterPlugin(BaseAdminPlugin):
|
||||
""" Add a filter menu to the left column of the page """
|
||||
list_quick_filter = () # these must be a subset of list_filter to work
|
||||
quickfilter = {}
|
||||
search_fields = ()
|
||||
free_query_filter = True
|
||||
|
||||
def init_request(self, *args, **kwargs):
|
||||
menu_style_accordian = hasattr(self.admin_view, 'menu_style') and self.admin_view.menu_style == 'accordion'
|
||||
return bool(self.list_quick_filter) and not menu_style_accordian
|
||||
|
||||
# Media
|
||||
def get_media(self, media):
|
||||
return media + self.vendor('xadmin.plugin.quickfilter.js', 'xadmin.plugin.quickfilter.css')
|
||||
|
||||
def lookup_allowed(self, lookup, value):
|
||||
model = self.model
|
||||
# Check FKey lookups that are allowed, so that popups produced by
|
||||
# ForeignKeyRawIdWidget, on the basis of ForeignKey.limit_choices_to,
|
||||
# are allowed to work.
|
||||
for l in model._meta.related_fkey_lookups:
|
||||
for k, v in widgets.url_params_from_lookup_dict(l).items():
|
||||
if k == lookup and v == value:
|
||||
return True
|
||||
|
||||
parts = lookup.split(LOOKUP_SEP)
|
||||
|
||||
# Last term in lookup is a query term (__exact, __startswith etc)
|
||||
# This term can be ignored.
|
||||
if len(parts) > 1 and parts[-1] in QUERY_TERMS:
|
||||
parts.pop()
|
||||
|
||||
# Special case -- foo__id__exact and foo__id queries are implied
|
||||
# if foo has been specificially included in the lookup list; so
|
||||
# drop __id if it is the last part. However, first we need to find
|
||||
# the pk attribute name.
|
||||
rel_name = None
|
||||
for part in parts[:-1]:
|
||||
try:
|
||||
field = model._meta.get_field(part)
|
||||
except FieldDoesNotExist:
|
||||
# Lookups on non-existants fields are ok, since they're ignored
|
||||
# later.
|
||||
return True
|
||||
if hasattr(field, 'remote_field'):
|
||||
model = field.remote_field.model
|
||||
rel_name = field.remote_field.get_related_field().name
|
||||
elif is_related_field(field):
|
||||
model = field.model
|
||||
rel_name = model._meta.pk.name
|
||||
else:
|
||||
rel_name = None
|
||||
if rel_name and len(parts) > 1 and parts[-1] == rel_name:
|
||||
parts.pop()
|
||||
|
||||
if len(parts) == 1:
|
||||
return True
|
||||
clean_lookup = LOOKUP_SEP.join(parts)
|
||||
return clean_lookup in self.list_quick_filter
|
||||
|
||||
def get_list_queryset(self, queryset):
|
||||
lookup_params = dict([(smart_str(k)[len(FILTER_PREFIX):], v) for k, v in self.admin_view.params.items() if smart_str(k).startswith(FILTER_PREFIX) and v != ''])
|
||||
for p_key, p_val in iteritems(lookup_params):
|
||||
if p_val == "False":
|
||||
lookup_params[p_key] = False
|
||||
use_distinct = False
|
||||
|
||||
if not hasattr(self.admin_view, 'quickfilter'):
|
||||
self.admin_view.quickfilter = {}
|
||||
|
||||
# for clean filters
|
||||
self.admin_view.quickfilter['has_query_param'] = bool(lookup_params)
|
||||
self.admin_view.quickfilter['clean_query_url'] = self.admin_view.get_query_string(remove=[k for k in self.request.GET.keys() if k.startswith(FILTER_PREFIX)])
|
||||
|
||||
# Normalize the types of keys
|
||||
if not self.free_query_filter:
|
||||
for key, value in lookup_params.items():
|
||||
if not self.lookup_allowed(key, value):
|
||||
raise SuspiciousOperation("Filtering by %s not allowed" % key)
|
||||
|
||||
self.filter_specs = []
|
||||
if self.list_quick_filter:
|
||||
for list_quick_filter in self.list_quick_filter:
|
||||
field_path = None
|
||||
field_order_by = None
|
||||
field_limit = None
|
||||
field_parts = []
|
||||
sort_key = None
|
||||
cache_config = None
|
||||
|
||||
if type(list_quick_filter) == dict and 'field' in list_quick_filter:
|
||||
field = list_quick_filter['field']
|
||||
if 'order_by' in list_quick_filter:
|
||||
field_order_by = list_quick_filter['order_by']
|
||||
if 'limit' in list_quick_filter:
|
||||
field_limit = list_quick_filter['limit']
|
||||
if 'sort' in list_quick_filter and callable(list_quick_filter['sort']):
|
||||
sort_key = list_quick_filter['sort']
|
||||
if 'cache' in list_quick_filter and type(list_quick_filter) == dict:
|
||||
cache_config = list_quick_filter['cache']
|
||||
|
||||
else:
|
||||
field = list_quick_filter # This plugin only uses MultiselectFieldListFilter
|
||||
|
||||
if not isinstance(field, models.Field):
|
||||
field_path = field
|
||||
field_parts = get_fields_from_path(self.model, field_path)
|
||||
field = field_parts[-1]
|
||||
spec = QuickFilterMultiSelectFieldListFilter(field, self.request, lookup_params, self.model, self.admin_view, field_path=field_path,
|
||||
field_order_by=field_order_by, field_limit=field_limit, sort_key=sort_key, cache_config=cache_config)
|
||||
|
||||
if len(field_parts) > 1:
|
||||
spec.title = "%s %s" % (field_parts[-2].name, spec.title)
|
||||
|
||||
# Check if we need to use distinct()
|
||||
use_distinct = True # (use_distinct orlookup_needs_distinct(self.opts, field_path))
|
||||
if spec and spec.has_output():
|
||||
try:
|
||||
new_qs = spec.do_filte(queryset)
|
||||
except ValidationError as e:
|
||||
new_qs = None
|
||||
self.admin_view.message_user(_("<b>Filtering error:</b> %s") % e.messages[0], 'error')
|
||||
if new_qs is not None:
|
||||
queryset = new_qs
|
||||
|
||||
self.filter_specs.append(spec)
|
||||
|
||||
self.has_filters = bool(self.filter_specs)
|
||||
self.admin_view.quickfilter['filter_specs'] = self.filter_specs
|
||||
obj = filter(lambda f: f.is_used, self.filter_specs)
|
||||
if six.PY3:
|
||||
obj = list(obj)
|
||||
self.admin_view.quickfilter['used_filter_num'] = len(obj)
|
||||
|
||||
if use_distinct:
|
||||
return queryset.distinct()
|
||||
else:
|
||||
return queryset
|
||||
|
||||
def block_left_navbar(self, context, nodes):
|
||||
nodes.append(loader.render_to_string('xadmin/blocks/modal_list.left_navbar.quickfilter.html',
|
||||
get_context_dict(context)))
|
||||
|
||||
site.register_plugin(QuickFilterPlugin, ListAdminView)
|
||||
110
lib/xadmin/plugins/quickform.py
Normal file
110
lib/xadmin/plugins/quickform.py
Normal file
@@ -0,0 +1,110 @@
|
||||
from django.db import models
|
||||
from django import forms
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.forms.models import modelform_factory
|
||||
import copy
|
||||
from xadmin.sites import site
|
||||
from xadmin.util import get_model_from_relation, vendor
|
||||
from xadmin.views import BaseAdminPlugin, ModelFormAdminView
|
||||
from xadmin.layout import Layout
|
||||
|
||||
|
||||
class QuickFormPlugin(BaseAdminPlugin):
|
||||
|
||||
def init_request(self, *args, **kwargs):
|
||||
if self.request.method == 'GET' and self.request.is_ajax() or self.request.GET.get('_ajax'):
|
||||
self.admin_view.add_form_template = 'xadmin/views/quick_form.html'
|
||||
self.admin_view.change_form_template = 'xadmin/views/quick_form.html'
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_model_form(self, __, **kwargs):
|
||||
if '_field' in self.request.GET:
|
||||
defaults = {
|
||||
"form": self.admin_view.form,
|
||||
"fields": self.request.GET['_field'].split(','),
|
||||
"formfield_callback": self.admin_view.formfield_for_dbfield,
|
||||
}
|
||||
return modelform_factory(self.model, **defaults)
|
||||
return __()
|
||||
|
||||
def get_form_layout(self, __):
|
||||
if '_field' in self.request.GET:
|
||||
return Layout(*self.request.GET['_field'].split(','))
|
||||
return __()
|
||||
|
||||
def get_context(self, context):
|
||||
context['form_url'] = self.request.path
|
||||
return context
|
||||
|
||||
|
||||
class RelatedFieldWidgetWrapper(forms.Widget):
|
||||
"""
|
||||
This class is a wrapper to a given widget to add the add icon for the
|
||||
admin interface.
|
||||
"""
|
||||
|
||||
def __init__(self, widget, rel, add_url, rel_add_url):
|
||||
self.needs_multipart_form = widget.needs_multipart_form
|
||||
self.attrs = widget.attrs
|
||||
self.choices = widget.choices
|
||||
self.is_required = widget.is_required
|
||||
self.widget = widget
|
||||
self.rel = rel
|
||||
|
||||
self.add_url = add_url
|
||||
self.rel_add_url = rel_add_url
|
||||
|
||||
if hasattr(self, 'input_type'):
|
||||
self.input_type = widget.input_type
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
obj = copy.copy(self)
|
||||
obj.widget = copy.deepcopy(self.widget, memo)
|
||||
obj.attrs = self.widget.attrs
|
||||
memo[id(self)] = obj
|
||||
return obj
|
||||
|
||||
@property
|
||||
def media(self):
|
||||
media = self.widget.media + vendor('xadmin.plugin.quick-form.js')
|
||||
return media
|
||||
|
||||
def render(self, name, value, renderer=None, *args, **kwargs):
|
||||
self.widget.choices = self.choices
|
||||
output = []
|
||||
if self.add_url:
|
||||
output.append(u'<a href="%s" title="%s" class="btn btn-primary btn-sm btn-ajax pull-right" data-for-id="id_%s" data-refresh-url="%s"><i class="fa fa-plus"></i></a>'
|
||||
% (
|
||||
self.add_url, (_('Create New %s') % self.rel.model._meta.verbose_name), name,
|
||||
"%s?_field=%s&%s=" % (self.rel_add_url, name, name)))
|
||||
output.extend(['<div class="control-wrap" id="id_%s_wrap_container">' % name,
|
||||
self.widget.render(name, value, *args, **kwargs), '</div>'])
|
||||
return mark_safe(u''.join(output))
|
||||
|
||||
def build_attrs(self, extra_attrs=None, **kwargs):
|
||||
"Helper function for building an attribute dictionary."
|
||||
self.attrs = self.widget.build_attrs(extra_attrs=None, **kwargs)
|
||||
return self.attrs
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
return self.widget.value_from_datadict(data, files, name)
|
||||
|
||||
def id_for_label(self, id_):
|
||||
return self.widget.id_for_label(id_)
|
||||
|
||||
|
||||
class QuickAddBtnPlugin(BaseAdminPlugin):
|
||||
|
||||
def formfield_for_dbfield(self, formfield, db_field, **kwargs):
|
||||
if formfield and self.model in self.admin_site._registry and isinstance(db_field, (models.ForeignKey, models.ManyToManyField)):
|
||||
rel_model = get_model_from_relation(db_field)
|
||||
if rel_model in self.admin_site._registry and self.has_model_perm(rel_model, 'add'):
|
||||
add_url = self.get_model_url(rel_model, 'add')
|
||||
formfield.widget = RelatedFieldWidgetWrapper(
|
||||
formfield.widget, db_field.remote_field, add_url, self.get_model_url(self.model, 'add'))
|
||||
return formfield
|
||||
|
||||
site.register_plugin(QuickFormPlugin, ModelFormAdminView)
|
||||
site.register_plugin(QuickAddBtnPlugin, ModelFormAdminView)
|
||||
39
lib/xadmin/plugins/refresh.py
Normal file
39
lib/xadmin/plugins/refresh.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# coding=utf-8
|
||||
from django.template import loader
|
||||
|
||||
from xadmin.plugins.utils import get_context_dict
|
||||
from xadmin.sites import site
|
||||
from xadmin.views import BaseAdminPlugin, ListAdminView
|
||||
|
||||
REFRESH_VAR = '_refresh'
|
||||
|
||||
|
||||
class RefreshPlugin(BaseAdminPlugin):
|
||||
|
||||
refresh_times = []
|
||||
|
||||
# Media
|
||||
def get_media(self, media):
|
||||
if self.refresh_times and self.request.GET.get(REFRESH_VAR):
|
||||
media = media + self.vendor('xadmin.plugin.refresh.js')
|
||||
return media
|
||||
|
||||
# Block Views
|
||||
def block_top_toolbar(self, context, nodes):
|
||||
if self.refresh_times:
|
||||
current_refresh = self.request.GET.get(REFRESH_VAR)
|
||||
context.update({
|
||||
'has_refresh': bool(current_refresh),
|
||||
'clean_refresh_url': self.admin_view.get_query_string(remove=(REFRESH_VAR,)),
|
||||
'current_refresh': current_refresh,
|
||||
'refresh_times': [{
|
||||
'time': r,
|
||||
'url': self.admin_view.get_query_string({REFRESH_VAR: r}),
|
||||
'selected': str(r) == current_refresh,
|
||||
} for r in self.refresh_times],
|
||||
})
|
||||
nodes.append(loader.render_to_string('xadmin/blocks/model_list.top_toolbar.refresh.html',
|
||||
get_context_dict(context)))
|
||||
|
||||
|
||||
site.register_plugin(RefreshPlugin, ListAdminView)
|
||||
240
lib/xadmin/plugins/relate.py
Normal file
240
lib/xadmin/plugins/relate.py
Normal file
@@ -0,0 +1,240 @@
|
||||
# coding=UTF-8
|
||||
from itertools import chain
|
||||
|
||||
from django.urls.base import reverse
|
||||
from django.db.models.options import PROXY_PARENTS
|
||||
from django.utils import six
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.encoding import smart_str
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.db.models.sql.query import LOOKUP_SEP
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.db import models
|
||||
|
||||
|
||||
from xadmin.sites import site
|
||||
from xadmin.views import BaseAdminPlugin, ListAdminView, CreateAdminView, UpdateAdminView, DeleteAdminView
|
||||
from xadmin.util import is_related_field2
|
||||
|
||||
RELATE_PREFIX = '_rel_'
|
||||
|
||||
|
||||
class RelateMenuPlugin(BaseAdminPlugin):
|
||||
|
||||
related_list = []
|
||||
use_related_menu = True
|
||||
|
||||
def _get_all_related_objects(self, local_only=False, include_hidden=False,
|
||||
include_proxy_eq=False):
|
||||
"""
|
||||
Returns a list of related fields (also many to many)
|
||||
:param local_only:
|
||||
:param include_hidden:
|
||||
:return: list
|
||||
"""
|
||||
include_parents = True if local_only is False else PROXY_PARENTS
|
||||
fields = self.opts._get_fields(
|
||||
forward=False, reverse=True,
|
||||
include_parents=include_parents,
|
||||
include_hidden=include_hidden
|
||||
)
|
||||
if include_proxy_eq:
|
||||
children = chain.from_iterable(c._relation_tree
|
||||
for c in self.opts.concrete_model._meta.proxied_children
|
||||
if c is not self.opts)
|
||||
relations = (f.remote_field for f in children
|
||||
if include_hidden or not f.remote_field.field.remote_field.is_hidden())
|
||||
fields = chain(fields, relations)
|
||||
return list(fields)
|
||||
|
||||
def get_related_list(self):
|
||||
if hasattr(self, '_related_acts'):
|
||||
return self._related_acts
|
||||
|
||||
_related_acts = []
|
||||
for rel in self._get_all_related_objects():
|
||||
if self.related_list and (rel.get_accessor_name() not in self.related_list):
|
||||
continue
|
||||
if rel.related_model not in self.admin_site._registry.keys():
|
||||
continue
|
||||
has_view_perm = self.has_model_perm(rel.related_model, 'view')
|
||||
has_add_perm = self.has_model_perm(rel.related_model, 'add')
|
||||
if not (has_view_perm or has_add_perm):
|
||||
continue
|
||||
|
||||
_related_acts.append((rel, has_view_perm, has_add_perm))
|
||||
|
||||
self._related_acts = _related_acts
|
||||
return self._related_acts
|
||||
|
||||
def related_link(self, instance):
|
||||
links = []
|
||||
for rel, view_perm, add_perm in self.get_related_list():
|
||||
opts = rel.related_model._meta
|
||||
|
||||
label = opts.app_label
|
||||
model_name = opts.model_name
|
||||
|
||||
field = rel.field
|
||||
rel_name = rel.get_related_field().name
|
||||
|
||||
verbose_name = force_text(opts.verbose_name)
|
||||
lookup_name = '%s__%s__exact' % (field.name, rel_name)
|
||||
|
||||
link = ''.join(('<li class="with_menu_btn">',
|
||||
|
||||
'<a href="%s?%s=%s" title="%s"><i class="icon fa fa-th-list"></i> %s</a>' %
|
||||
(
|
||||
reverse('%s:%s_%s_changelist' % (
|
||||
self.admin_site.app_name, label, model_name)),
|
||||
RELATE_PREFIX + lookup_name, str(instance.pk), verbose_name, verbose_name) if view_perm else
|
||||
'<a><span class="text-muted"><i class="icon fa fa-blank"></i> %s</span></a>' % verbose_name,
|
||||
|
||||
'<a class="add_link dropdown-menu-btn" href="%s?%s=%s"><i class="icon fa fa-plus pull-right"></i></a>' %
|
||||
(
|
||||
reverse('%s:%s_%s_add' % (
|
||||
self.admin_site.app_name, label, model_name)),
|
||||
RELATE_PREFIX + lookup_name, str(
|
||||
instance.pk)) if add_perm else "",
|
||||
|
||||
'</li>'))
|
||||
links.append(link)
|
||||
ul_html = '<ul class="dropdown-menu" role="menu">%s</ul>' % ''.join(
|
||||
links)
|
||||
return '<div class="dropdown related_menu pull-right"><a title="%s" class="relate_menu dropdown-toggle" data-toggle="dropdown"><i class="icon fa fa-list"></i></a>%s</div>' % (_('Related Objects'), ul_html)
|
||||
related_link.short_description = ' '
|
||||
related_link.allow_tags = True
|
||||
related_link.allow_export = False
|
||||
related_link.is_column = False
|
||||
|
||||
def get_list_display(self, list_display):
|
||||
if self.use_related_menu and len(self.get_related_list()):
|
||||
list_display.append('related_link')
|
||||
self.admin_view.related_link = self.related_link
|
||||
return list_display
|
||||
|
||||
|
||||
class RelateObject(object):
|
||||
|
||||
def __init__(self, admin_view, lookup, value):
|
||||
self.admin_view = admin_view
|
||||
self.org_model = admin_view.model
|
||||
self.opts = admin_view.opts
|
||||
self.lookup = lookup
|
||||
self.value = value
|
||||
|
||||
parts = lookup.split(LOOKUP_SEP)
|
||||
field = self.opts.get_field(parts[0])
|
||||
|
||||
if not is_related_field2(field):
|
||||
raise Exception(u'Relate Lookup field must a related field')
|
||||
|
||||
self.to_model = field.related_model
|
||||
self.rel_name = '__'.join(parts[1:])
|
||||
self.is_m2m = bool(field.many_to_many)
|
||||
|
||||
to_qs = self.to_model._default_manager.get_queryset()
|
||||
self.to_objs = to_qs.filter(**{self.rel_name: value}).all()
|
||||
|
||||
self.field = field
|
||||
|
||||
def filter(self, queryset):
|
||||
return queryset.filter(**{self.lookup: self.value})
|
||||
|
||||
def get_brand_name(self):
|
||||
if len(self.to_objs) == 1:
|
||||
to_model_name = str(self.to_objs[0])
|
||||
else:
|
||||
to_model_name = force_text(self.to_model._meta.verbose_name)
|
||||
|
||||
return mark_safe(u"<span class='rel-brand'>%s <i class='fa fa-caret-right'></i></span> %s" % (to_model_name, force_text(self.opts.verbose_name_plural)))
|
||||
|
||||
|
||||
class BaseRelateDisplayPlugin(BaseAdminPlugin):
|
||||
|
||||
def init_request(self, *args, **kwargs):
|
||||
self.relate_obj = None
|
||||
for k, v in self.request.GET.items():
|
||||
if smart_str(k).startswith(RELATE_PREFIX):
|
||||
self.relate_obj = RelateObject(
|
||||
self.admin_view, smart_str(k)[len(RELATE_PREFIX):], v)
|
||||
break
|
||||
return bool(self.relate_obj)
|
||||
|
||||
def _get_relate_params(self):
|
||||
return RELATE_PREFIX + self.relate_obj.lookup, self.relate_obj.value
|
||||
|
||||
def _get_input(self):
|
||||
return '<input type="hidden" name="%s" value="%s" />' % self._get_relate_params()
|
||||
|
||||
def _get_url(self, url):
|
||||
return url + ('&' if url.find('?') > 0 else '?') + ('%s=%s' % self._get_relate_params())
|
||||
|
||||
|
||||
class ListRelateDisplayPlugin(BaseRelateDisplayPlugin):
|
||||
|
||||
def get_list_queryset(self, queryset):
|
||||
if self.relate_obj:
|
||||
queryset = self.relate_obj.filter(queryset)
|
||||
return queryset
|
||||
|
||||
def url_for_result(self, url, result):
|
||||
return self._get_url(url)
|
||||
|
||||
def get_context(self, context):
|
||||
context['brand_name'] = self.relate_obj.get_brand_name()
|
||||
context['rel_objs'] = self.relate_obj.to_objs
|
||||
if len(self.relate_obj.to_objs) == 1:
|
||||
context['rel_obj'] = self.relate_obj.to_objs[0]
|
||||
if 'add_url' in context:
|
||||
context['add_url'] = self._get_url(context['add_url'])
|
||||
return context
|
||||
|
||||
def get_list_display(self, list_display):
|
||||
if not self.relate_obj.is_m2m:
|
||||
try:
|
||||
list_display.remove(self.relate_obj.field.name)
|
||||
except Exception:
|
||||
pass
|
||||
return list_display
|
||||
|
||||
|
||||
class EditRelateDisplayPlugin(BaseRelateDisplayPlugin):
|
||||
|
||||
def get_form_datas(self, datas):
|
||||
if self.admin_view.org_obj is None and self.admin_view.request_method == 'get':
|
||||
datas['initial'][
|
||||
self.relate_obj.field.name] = self.relate_obj.value
|
||||
return datas
|
||||
|
||||
def post_response(self, response):
|
||||
cls_str = str if six.PY3 else basestring
|
||||
if isinstance(response, cls_str) and response != self.get_admin_url('index'):
|
||||
return self._get_url(response)
|
||||
return response
|
||||
|
||||
def get_context(self, context):
|
||||
if 'delete_url' in context:
|
||||
context['delete_url'] = self._get_url(context['delete_url'])
|
||||
return context
|
||||
|
||||
def block_after_fieldsets(self, context, nodes):
|
||||
return self._get_input()
|
||||
|
||||
|
||||
class DeleteRelateDisplayPlugin(BaseRelateDisplayPlugin):
|
||||
|
||||
def post_response(self, response):
|
||||
cls_str = str if six.PY3 else basestring
|
||||
if isinstance(response, cls_str) and response != self.get_admin_url('index'):
|
||||
return self._get_url(response)
|
||||
return response
|
||||
|
||||
def block_form_fields(self, context, nodes):
|
||||
return self._get_input()
|
||||
|
||||
site.register_plugin(RelateMenuPlugin, ListAdminView)
|
||||
site.register_plugin(ListRelateDisplayPlugin, ListAdminView)
|
||||
site.register_plugin(EditRelateDisplayPlugin, CreateAdminView)
|
||||
site.register_plugin(EditRelateDisplayPlugin, UpdateAdminView)
|
||||
site.register_plugin(DeleteRelateDisplayPlugin, DeleteAdminView)
|
||||
84
lib/xadmin/plugins/relfield.py
Normal file
84
lib/xadmin/plugins/relfield.py
Normal file
@@ -0,0 +1,84 @@
|
||||
from django.db import models
|
||||
from django.forms.utils import flatatt
|
||||
from django.utils.html import escape, format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.text import Truncator
|
||||
from django.utils.translation import ugettext as _
|
||||
from django import forms
|
||||
from xadmin.sites import site
|
||||
from xadmin.views import BaseAdminPlugin, ModelFormAdminView
|
||||
from xadmin.util import vendor
|
||||
|
||||
|
||||
class ForeignKeySearchWidget(forms.Widget):
|
||||
|
||||
def __init__(self, rel, admin_view, attrs=None, using=None):
|
||||
self.rel = rel
|
||||
self.admin_view = admin_view
|
||||
self.db = using
|
||||
super(ForeignKeySearchWidget, self).__init__(attrs)
|
||||
|
||||
def build_attrs(self, attrs={}, extra_attrs=None, **kwargs):
|
||||
to_opts = self.rel.model._meta
|
||||
if "class" not in attrs:
|
||||
attrs['class'] = 'select-search'
|
||||
else:
|
||||
attrs['class'] = attrs['class'] + ' select-search'
|
||||
attrs['data-search-url'] = self.admin_view.get_admin_url(
|
||||
'%s_%s_changelist' % (to_opts.app_label, to_opts.model_name))
|
||||
attrs['data-placeholder'] = _('Search %s') % to_opts.verbose_name
|
||||
attrs['data-choices'] = '?'
|
||||
if self.rel.limit_choices_to:
|
||||
for i in list(self.rel.limit_choices_to):
|
||||
attrs['data-choices'] += "&_p_%s=%s" % (i, self.rel.limit_choices_to[i])
|
||||
attrs['data-choices'] = format_html(attrs['data-choices'])
|
||||
attrs.update(kwargs)
|
||||
return super(ForeignKeySearchWidget, self).build_attrs(attrs, extra_attrs=extra_attrs)
|
||||
|
||||
def render(self, name, value, attrs=None):
|
||||
final_attrs = self.build_attrs(attrs, extra_attrs={'name': name})
|
||||
output = [format_html('<select{0}>', flatatt(final_attrs))]
|
||||
if value:
|
||||
output.append(format_html('<option selected="selected" value="{0}">{1}</option>', value, self.label_for_value(value)))
|
||||
output.append('</select>')
|
||||
return mark_safe('\n'.join(output))
|
||||
|
||||
def label_for_value(self, value):
|
||||
key = self.rel.get_related_field().name
|
||||
try:
|
||||
obj = self.rel.to._default_manager.using(
|
||||
self.db).get(**{key: value})
|
||||
return '%s' % escape(Truncator(obj).words(14, truncate='...'))
|
||||
except (ValueError, self.rel.to.DoesNotExist):
|
||||
return ""
|
||||
|
||||
@property
|
||||
def media(self):
|
||||
return vendor('select.js', 'select.css', 'xadmin.widget.select.js')
|
||||
|
||||
|
||||
class ForeignKeySelectWidget(ForeignKeySearchWidget):
|
||||
|
||||
def build_attrs(self, attrs={}, **kwargs):
|
||||
attrs = super(ForeignKeySelectWidget, self).build_attrs(attrs, **kwargs)
|
||||
if "class" not in attrs:
|
||||
attrs['class'] = 'select-preload'
|
||||
else:
|
||||
attrs['class'] = attrs['class'] + ' select-preload'
|
||||
attrs['data-placeholder'] = _('Select %s') % self.rel.model._meta.verbose_name
|
||||
return attrs
|
||||
|
||||
|
||||
class RelateFieldPlugin(BaseAdminPlugin):
|
||||
|
||||
def get_field_style(self, attrs, db_field, style, **kwargs):
|
||||
# search able fk field
|
||||
if style in ('fk-ajax', 'fk-select') and isinstance(db_field, models.ForeignKey):
|
||||
if (db_field.remote_field.to in self.admin_view.admin_site._registry) and \
|
||||
self.has_model_perm(db_field.remote_field.to, 'view'):
|
||||
db = kwargs.get('using')
|
||||
return dict(attrs or {},
|
||||
widget=(style == 'fk-ajax' and ForeignKeySearchWidget or ForeignKeySelectWidget)(db_field.remote_field, self.admin_view, using=db))
|
||||
return attrs
|
||||
|
||||
site.register_plugin(RelateFieldPlugin, ModelFormAdminView)
|
||||
22
lib/xadmin/plugins/sitemenu.py
Normal file
22
lib/xadmin/plugins/sitemenu.py
Normal file
@@ -0,0 +1,22 @@
|
||||
|
||||
from xadmin.sites import site
|
||||
from xadmin.views import BaseAdminPlugin, CommAdminView
|
||||
|
||||
BUILDIN_STYLES = {
|
||||
'default': 'xadmin/includes/sitemenu_default.html',
|
||||
'accordion': 'xadmin/includes/sitemenu_accordion.html',
|
||||
}
|
||||
|
||||
|
||||
class SiteMenuStylePlugin(BaseAdminPlugin):
|
||||
|
||||
menu_style = None
|
||||
|
||||
def init_request(self, *args, **kwargs):
|
||||
return bool(self.menu_style) and self.menu_style in BUILDIN_STYLES
|
||||
|
||||
def get_context(self, context):
|
||||
context['menu_template'] = BUILDIN_STYLES[self.menu_style]
|
||||
return context
|
||||
|
||||
site.register_plugin(SiteMenuStylePlugin, CommAdminView)
|
||||
81
lib/xadmin/plugins/sortablelist.py
Normal file
81
lib/xadmin/plugins/sortablelist.py
Normal file
@@ -0,0 +1,81 @@
|
||||
# coding: utf-8
|
||||
"""
|
||||
Make items sortable by drag-drop in list view. Diffierent from
|
||||
builtin plugin sortable, it touches model field indeed intead
|
||||
of only for display.
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls.base import reverse
|
||||
from django.db import transaction
|
||||
|
||||
from xadmin.views import (
|
||||
BaseAdminPlugin, ModelAdminView, ListAdminView
|
||||
)
|
||||
from xadmin.sites import site
|
||||
from xadmin.views.base import csrf_protect_m
|
||||
|
||||
|
||||
class SortableListPlugin(BaseAdminPlugin):
|
||||
|
||||
list_order_field = None
|
||||
|
||||
def init_request(self, *args, **kwargs):
|
||||
return bool(self.list_order_field)
|
||||
|
||||
@property
|
||||
def is_list_sortable(self):
|
||||
return True
|
||||
|
||||
def result_row(self, __, obj):
|
||||
row = __()
|
||||
row.update({
|
||||
"tagattrs": "order-key=order_{}".format(obj.pk)
|
||||
})
|
||||
return row
|
||||
|
||||
def result_item(self, item, obj, field_name, row):
|
||||
if self.is_list_sortable and field_name == self.list_order_field:
|
||||
item.btns.append('<a><i class="fa fa-arrows"></i></a>')
|
||||
return item
|
||||
|
||||
def get_context(self, context):
|
||||
context['save_order_url'] = self.get_model_url(self.admin_view.model, 'save_order')
|
||||
return context
|
||||
|
||||
def block_top_toolbar(self, context, nodes):
|
||||
save_node = render_to_string(
|
||||
'xadmin/blocks/model_list.top_toolbar.saveorder.html', context_instance=context
|
||||
)
|
||||
nodes.append(save_node)
|
||||
|
||||
def get_media(self, media):
|
||||
if self.is_list_sortable:
|
||||
media = media + self.vendor('xadmin.plugin.sortablelist.js')
|
||||
return media
|
||||
|
||||
|
||||
class SaveOrderView(ModelAdminView):
|
||||
|
||||
@csrf_protect_m
|
||||
@transaction.atomic
|
||||
def post(self, request):
|
||||
order_objs = request.POST.getlist("order[]")
|
||||
for order_value, pk in enumerate(order_objs, start=1):
|
||||
self.save_order(pk, order_value)
|
||||
return self.render_response({})
|
||||
|
||||
def save_order(self, pk, order_value):
|
||||
obj = self.model.objects.get(pk=pk)
|
||||
order_field = self.list_order_field
|
||||
is_order_changed = lambda x: getattr(x, order_field) != order_value
|
||||
|
||||
if is_order_changed(obj):
|
||||
setattr(obj, order_field, order_value)
|
||||
obj.save()
|
||||
|
||||
|
||||
site.register_plugin(SortableListPlugin, ListAdminView)
|
||||
site.register_modelview(r'^save-order/$', SaveOrderView, name='%s_%s_save_order')
|
||||
93
lib/xadmin/plugins/themes.py
Normal file
93
lib/xadmin/plugins/themes.py
Normal file
@@ -0,0 +1,93 @@
|
||||
# coding:utf-8
|
||||
from __future__ import print_function
|
||||
import httplib2
|
||||
from django.template import loader
|
||||
from django.core.cache import cache
|
||||
from django.utils import six
|
||||
from django.utils.translation import ugettext as _
|
||||
from xadmin.sites import site
|
||||
from xadmin.models import UserSettings
|
||||
from xadmin.views import BaseAdminPlugin, BaseAdminView
|
||||
from xadmin.util import static, json
|
||||
import six
|
||||
if six.PY2:
|
||||
import urllib
|
||||
else:
|
||||
import urllib.parse
|
||||
|
||||
THEME_CACHE_KEY = 'xadmin_themes'
|
||||
|
||||
|
||||
class ThemePlugin(BaseAdminPlugin):
|
||||
|
||||
enable_themes = False
|
||||
# {'name': 'Blank Theme', 'description': '...', 'css': 'http://...', 'thumbnail': '...'}
|
||||
user_themes = None
|
||||
use_bootswatch = False
|
||||
default_theme = static('xadmin/css/themes/bootstrap-xadmin.css')
|
||||
bootstrap2_theme = static('xadmin/css/themes/bootstrap-theme.css')
|
||||
|
||||
def init_request(self, *args, **kwargs):
|
||||
return self.enable_themes
|
||||
|
||||
def _get_theme(self):
|
||||
if self.user:
|
||||
try:
|
||||
return UserSettings.objects.get(user=self.user, key="site-theme").value
|
||||
except Exception:
|
||||
pass
|
||||
if '_theme' in self.request.COOKIES:
|
||||
if six.PY2:
|
||||
func = urllib.unquote
|
||||
else:
|
||||
func = urllib.parse.unquote
|
||||
return func(self.request.COOKIES['_theme'])
|
||||
return self.default_theme
|
||||
|
||||
def get_context(self, context):
|
||||
context['site_theme'] = self._get_theme()
|
||||
return context
|
||||
|
||||
# Media
|
||||
def get_media(self, media):
|
||||
return media + self.vendor('jquery-ui-effect.js', 'xadmin.plugin.themes.js')
|
||||
|
||||
# Block Views
|
||||
def block_top_navmenu(self, context, nodes):
|
||||
|
||||
themes = [
|
||||
{'name': _(u"Default"), 'description': _(u"Default bootstrap theme"), 'css': self.default_theme},
|
||||
{'name': _(u"Bootstrap2"), 'description': _(u"Bootstrap 2.x theme"), 'css': self.bootstrap2_theme},
|
||||
]
|
||||
select_css = context.get('site_theme', self.default_theme)
|
||||
|
||||
if self.user_themes:
|
||||
themes.extend(self.user_themes)
|
||||
|
||||
if self.use_bootswatch:
|
||||
ex_themes = cache.get(THEME_CACHE_KEY)
|
||||
if ex_themes:
|
||||
themes.extend(json.loads(ex_themes))
|
||||
else:
|
||||
ex_themes = []
|
||||
try:
|
||||
h = httplib2.Http()
|
||||
resp, content = h.request("https://bootswatch.com/api/3.json", 'GET', '',
|
||||
headers={"Accept": "application/json", "User-Agent": self.request.META['HTTP_USER_AGENT']})
|
||||
if six.PY3:
|
||||
content = content.decode()
|
||||
watch_themes = json.loads(content)['themes']
|
||||
ex_themes.extend([
|
||||
{'name': t['name'], 'description': t['description'],
|
||||
'css': t['cssMin'], 'thumbnail': t['thumbnail']}
|
||||
for t in watch_themes])
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
cache.set(THEME_CACHE_KEY, json.dumps(ex_themes), 24 * 3600)
|
||||
themes.extend(ex_themes)
|
||||
|
||||
nodes.append(loader.render_to_string('xadmin/blocks/comm.top.theme.html', {'themes': themes, 'select_css': select_css}))
|
||||
|
||||
|
||||
site.register_plugin(ThemePlugin, BaseAdminView)
|
||||
73
lib/xadmin/plugins/topnav.py
Normal file
73
lib/xadmin/plugins/topnav.py
Normal file
@@ -0,0 +1,73 @@
|
||||
|
||||
from django.template import loader
|
||||
from django.utils.text import capfirst
|
||||
from django.urls.base import reverse, NoReverseMatch
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from xadmin.sites import site
|
||||
from xadmin.filters import SEARCH_VAR
|
||||
from xadmin.views import BaseAdminPlugin, CommAdminView
|
||||
|
||||
|
||||
class TopNavPlugin(BaseAdminPlugin):
|
||||
|
||||
global_search_models = None
|
||||
global_add_models = None
|
||||
|
||||
def get_context(self, context):
|
||||
return context
|
||||
|
||||
# Block Views
|
||||
def block_top_navbar(self, context, nodes):
|
||||
search_models = []
|
||||
|
||||
site_name = self.admin_site.name
|
||||
if self.global_search_models == None:
|
||||
models = self.admin_site._registry.keys()
|
||||
else:
|
||||
models = self.global_search_models
|
||||
|
||||
for model in models:
|
||||
app_label = model._meta.app_label
|
||||
|
||||
if self.has_model_perm(model, "view"):
|
||||
info = (app_label, model._meta.model_name)
|
||||
if getattr(self.admin_site._registry[model], 'search_fields', None):
|
||||
try:
|
||||
search_models.append({
|
||||
'title': _('Search %s') % capfirst(model._meta.verbose_name_plural),
|
||||
'url': reverse('xadmin:%s_%s_changelist' % info, current_app=site_name),
|
||||
'model': model
|
||||
})
|
||||
except NoReverseMatch:
|
||||
pass
|
||||
return nodes.append(loader.render_to_string('xadmin/blocks/comm.top.topnav.html', {'search_models': search_models, 'search_name': SEARCH_VAR}))
|
||||
|
||||
def block_top_navmenu(self, context, nodes):
|
||||
add_models = []
|
||||
|
||||
site_name = self.admin_site.name
|
||||
|
||||
if self.global_add_models == None:
|
||||
models = self.admin_site._registry.keys()
|
||||
else:
|
||||
models = self.global_add_models
|
||||
for model in models:
|
||||
app_label = model._meta.app_label
|
||||
|
||||
if self.has_model_perm(model, "add"):
|
||||
info = (app_label, model._meta.model_name)
|
||||
try:
|
||||
add_models.append({
|
||||
'title': _('Add %s') % capfirst(model._meta.verbose_name),
|
||||
'url': reverse('xadmin:%s_%s_add' % info, current_app=site_name),
|
||||
'model': model
|
||||
})
|
||||
except NoReverseMatch:
|
||||
pass
|
||||
|
||||
nodes.append(
|
||||
loader.render_to_string('xadmin/blocks/comm.top.topnav.html', {'add_models': add_models}))
|
||||
|
||||
|
||||
site.register_plugin(TopNavPlugin, CommAdminView)
|
||||
15
lib/xadmin/plugins/utils.py
Normal file
15
lib/xadmin/plugins/utils.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from django.template.context import RequestContext
|
||||
|
||||
|
||||
def get_context_dict(context):
|
||||
"""
|
||||
Contexts in django version 1.9+ must be dictionaries. As xadmin has a legacy with older versions of django,
|
||||
the function helps the transition by converting the [RequestContext] object to the dictionary when necessary.
|
||||
:param context: RequestContext
|
||||
:return: dict
|
||||
"""
|
||||
if isinstance(context, RequestContext):
|
||||
ctx = context.flatten()
|
||||
else:
|
||||
ctx = context
|
||||
return ctx
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user