feature:合并艺术家和专辑

This commit is contained in:
charlesxie
2023-05-19 13:27:26 +08:00
parent 383ff7bc64
commit 3529ec78fb
581 changed files with 137290 additions and 51 deletions

View 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)

View 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),
),
]

View 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),
),
]

View File

@@ -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

View File

@@ -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)

View File

@@ -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()

View File

@@ -27,6 +27,9 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
"rest_framework",
'xadmin',
'crispy_forms',
'reversion',
"applications.task",
"applications.user",
"applications.music",

View File

@@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View 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"

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View 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 ""

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View 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"

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View 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"

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View 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 ""

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View 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 ""

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View 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 ""

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View 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 ""

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View 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"

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View 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"

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View 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 ""

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View 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"

View 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',
},
),
]

View 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',
},
),
]

View 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'),
),
]

View File

190
lib/xadmin/models.py Normal file
View 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)

View 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]

View 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)

View 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)

View 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
View 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
View 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

View 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
View 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')

View 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)

View 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)

View 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')

View 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)

View 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)

View 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">&times;</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)

View 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)

View 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)

View 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')

View 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)

View 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)

View 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)

View 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')

View 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)

View 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)

View 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)

View 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)

View 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 = '&nbsp;'
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)

View 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)

View 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)

View 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')

View 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)

View 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)

View 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