Initial commit
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
ASGI config for ExamOnline project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ExamOnline.settings')
|
||||
|
||||
application = get_asgi_application()
|
||||
@@ -0,0 +1,152 @@
|
||||
"""
|
||||
Django settings for ExamOnline project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 3.0.3.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.0/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/3.0/ref/settings/
|
||||
"""
|
||||
import datetime
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0,os.path.join(BASE_DIR,'extra_apps'))
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = '%!t78vti6g=ejpbev3$45qjh)2)##eer9c=q#*71*+k0ynul!j'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
# 解决拓展内置auth_user表出现的认证问题
|
||||
# AUTH_USER_MODEL = 'user.Student'
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'xadmin',
|
||||
'crispy_forms',
|
||||
'rest_framework',
|
||||
'django_filters',
|
||||
'import_export',
|
||||
'user',
|
||||
'exam',
|
||||
'question',
|
||||
'record'
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'ExamOnline.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [os.path.join(BASE_DIR, 'templates')]
|
||||
,
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'ExamOnline.wsgi.application'
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||
}
|
||||
}
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/3.0/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'zh-hans'
|
||||
|
||||
TIME_ZONE = 'Asia/Shanghai'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/3.0/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
|
||||
|
||||
# restframework配置
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||
'rest_framework.authentication.BasicAuthentication',
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
# Json Web Token
|
||||
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
|
||||
),
|
||||
# restframework新版3.10.1需要指定默认schema
|
||||
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema'
|
||||
}
|
||||
|
||||
# JWT设置
|
||||
JWT_AUTH = {
|
||||
# token的有效期限
|
||||
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7),
|
||||
# JWT跟前端保持一致,比如“token”这里设置成JWT
|
||||
'JWT_AUTH_HEADER_PREFIX': 'JWT',
|
||||
# 自定义方法返回用户信息
|
||||
'JWT_RESPONSE_PAYLOAD_HANDLER': 'user.views.jwt_response_payload_handler'
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
"""ExamOnline URL Configuration
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/3.0/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
import xadmin
|
||||
from django.urls import path, include, re_path
|
||||
from rest_framework.documentation import include_docs_urls
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from rest_framework_jwt.views import obtain_jwt_token
|
||||
|
||||
from exam.views import GradeListViewSet, ExamListViewSet, PracticeListViewSet
|
||||
from question.views import ChoiceListViewSet, FillListViewSet, JudgeListViewSet, ProgramListViewSet, CheckProgramApi
|
||||
from record.views import ChoiceRecordListViewSet, FillRecordListViewSet, JudgeRecordListViewSet, \
|
||||
ProgramRecordListViewSet
|
||||
from user.views import RegisterViewSet, StudentViewSet, UpdatePwdApi, ClazzListViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
|
||||
# 配置exams的url
|
||||
router.register(r'exams', ExamListViewSet)
|
||||
router.register(r'grades', GradeListViewSet)
|
||||
router.register(r'choices', ChoiceListViewSet)
|
||||
router.register(r'fills', FillListViewSet)
|
||||
router.register(r'judges', JudgeListViewSet)
|
||||
router.register(r'programs', ProgramListViewSet)
|
||||
router.register(r'register', RegisterViewSet)
|
||||
router.register(r'clazzs', ClazzListViewSet)
|
||||
router.register(r'students', StudentViewSet)
|
||||
router.register(r'practices', PracticeListViewSet)
|
||||
router.register(r'records/choices', ChoiceRecordListViewSet)
|
||||
router.register(r'records/fills', FillRecordListViewSet)
|
||||
router.register(r'records/judges', JudgeRecordListViewSet)
|
||||
router.register(r'records/programs', ProgramRecordListViewSet)
|
||||
|
||||
urlpatterns = [
|
||||
path('xadmin/', xadmin.site.urls),
|
||||
path('docs/', include_docs_urls('Python在线考试系统')),
|
||||
path('api-auth/', include('rest_framework.urls')),
|
||||
path('jwt-auth/', obtain_jwt_token),
|
||||
path('check-program/', CheckProgramApi.as_view()),
|
||||
path('update-pwd/', UpdatePwdApi.as_view()),
|
||||
re_path('^', include(router.urls))
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for ExamOnline project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ExamOnline.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
@@ -0,0 +1,22 @@
|
||||
# ExamOnline
|
||||
Python在线考试系统-大学毕业设计
|
||||
前端代码:https://github.com/520118202/exam-online
|
||||
后端安装依赖
|
||||
pip install -r requirements.txt
|
||||
前端安装依赖
|
||||
npm run install
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
default_app_config = 'exam.apps.ExamConfig'
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
@@ -0,0 +1,84 @@
|
||||
import xadmin
|
||||
from django.contrib.auth.models import User
|
||||
from xadmin.plugins.auth import UserAdmin
|
||||
|
||||
from exam.models import Exam, Grade, Paper
|
||||
from xadmin.views import CommAdminView, BaseAdminView
|
||||
|
||||
|
||||
# Register your models here.
|
||||
|
||||
class GlobalSetting(object):
|
||||
# 全局设置
|
||||
site_title = 'Python在线考试后台管理系统'
|
||||
site_footer = 'Design by Pengshengfu'
|
||||
# 菜单默认收缩
|
||||
# menu_style = 'accordion'
|
||||
|
||||
|
||||
class BaseSetting(object):
|
||||
# 启动主题管理器
|
||||
enable_themes = True
|
||||
# 使用主题
|
||||
use_bootswatch = True
|
||||
|
||||
|
||||
class ExamAdmin(object):
|
||||
list_display = ['id', 'name', 'exam_date', 'total_time', 'paper', 'major', 'tips', 'clazzs']
|
||||
list_filter = ['major', 'exam_date']
|
||||
search_fields = ['id', 'name']
|
||||
list_display_links = ['name']
|
||||
list_per_page = 10
|
||||
# list_editable = ['name']
|
||||
model_icon = 'fa fa-book'
|
||||
relfield_style = 'fk-ajax'
|
||||
# 多对多样式字段支持过滤
|
||||
filter_horizontal = ('clazzs',)
|
||||
# 修改多对多穿梭框样式
|
||||
style_fields = {'clazzs': 'm2m_transfer'}
|
||||
|
||||
|
||||
class PaperAdmin(object):
|
||||
list_display = ['id', 'name', 'score', 'choice_number', 'fill_number', 'judge_number', 'program_number', 'level']
|
||||
list_filter = ['level']
|
||||
search_fields = ['id', 'name']
|
||||
list_display_links = ['name']
|
||||
list_per_page = 10
|
||||
# list_editable = ['name']
|
||||
model_icon = 'fa fa-file-text'
|
||||
|
||||
|
||||
class GradeAdmin(object):
|
||||
list_display = ['id', 'exam', 'student', 'score', 'create_time', 'update_time']
|
||||
list_filter = ['exam', 'student', 'create_time', 'update_time']
|
||||
search_fields = ['exam', 'student']
|
||||
list_display_links = ['score']
|
||||
list_per_page = 10
|
||||
# list_editable = ['id', 'score']
|
||||
model_icon = 'fa fa-bar-chart'
|
||||
|
||||
data_charts = {
|
||||
'grade_charts1': {
|
||||
'title': '考试成绩曲线图',
|
||||
'x-field': 'create_time',
|
||||
'y-field': ('score',),
|
||||
'order': ('id',)
|
||||
},
|
||||
'grade_charts2': {
|
||||
'title': '考试成绩柱状图',
|
||||
'x-field': 'score',
|
||||
'y-field': ('score',),
|
||||
'order': ('id',),
|
||||
'option': {
|
||||
"series": {"bars": {"align": "center", "barWidth": 0.5, "show": True}},
|
||||
"xaxis": {"aggregate": "count", "mode": "score"}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
xadmin.site.register(CommAdminView, GlobalSetting)
|
||||
xadmin.site.register(BaseAdminView, BaseSetting)
|
||||
xadmin.site.register(Exam, ExamAdmin)
|
||||
xadmin.site.register(Paper, PaperAdmin)
|
||||
xadmin.site.register(Grade, GradeAdmin)
|
||||
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ExamConfig(AppConfig):
|
||||
name = 'exam'
|
||||
verbose_name = '考试管理'
|
||||
@@ -0,0 +1,14 @@
|
||||
import django_filters
|
||||
|
||||
from exam.models import Exam
|
||||
|
||||
|
||||
class ExamFilter(django_filters.rest_framework.FilterSet):
|
||||
"""考试过滤的类"""
|
||||
# 两个参数,field_name是要过滤的字段,lookup是执行的行为
|
||||
exam_date_min = django_filters.DateFilter(field_name='exam_date', lookup_expr='gte')
|
||||
exam_date_max = django_filters.DateFilter(field_name="exam_date", lookup_expr='lte')
|
||||
|
||||
class Meta:
|
||||
model = Exam
|
||||
fields = ['exam_date_min', 'exam_date_max']
|
||||
@@ -0,0 +1,72 @@
|
||||
# Generated by Django 3.0.3 on 2020-03-03 07:48
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('user', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Exam',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(default='', max_length=20, verbose_name='考试名称')),
|
||||
('exam_date', models.DateField(default='', verbose_name='考试日期')),
|
||||
('total_time', models.PositiveSmallIntegerField(default=120, help_text='时长按照分钟填写', verbose_name='时长')),
|
||||
('major', models.CharField(default='', max_length=20, verbose_name='专业')),
|
||||
('tips', models.TextField(default='', verbose_name='考生须知')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '考试',
|
||||
'verbose_name_plural': '考试',
|
||||
'db_table': 'exam_info',
|
||||
'ordering': ['id'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Paper',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(default='', max_length=20, verbose_name='试卷名称')),
|
||||
('score', models.PositiveSmallIntegerField(default=100, verbose_name='总分')),
|
||||
('choice_number', models.PositiveSmallIntegerField(default=10, verbose_name='选择题数')),
|
||||
('fill_number', models.PositiveSmallIntegerField(default=10, verbose_name='填空题数')),
|
||||
('judge_number', models.PositiveSmallIntegerField(default=10, verbose_name='判断题数')),
|
||||
('program_number', models.PositiveSmallIntegerField(default=10, verbose_name='编程题数')),
|
||||
('level', models.CharField(choices=[('1', '入门'), ('2', '简单'), ('3', '普通'), ('4', '较难'), ('5', '困难')], default='1', max_length=1, verbose_name='难度等级')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '试卷',
|
||||
'verbose_name_plural': '试卷',
|
||||
'ordering': ['id'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Grade',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('score', models.PositiveSmallIntegerField(default='', verbose_name='分数')),
|
||||
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建日期')),
|
||||
('update_time', models.DateTimeField(auto_now=True, verbose_name='修改日期')),
|
||||
('exam', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='exam.Exam', verbose_name='考试')),
|
||||
('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='user.Student', verbose_name='学生')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '成绩',
|
||||
'verbose_name_plural': '成绩',
|
||||
'ordering': ['id'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='exam',
|
||||
name='paper',
|
||||
field=models.OneToOneField(default='', on_delete=django.db.models.deletion.CASCADE, to='exam.Paper', verbose_name='试卷'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.0.3 on 2020-04-01 14:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('user', '0001_initial'),
|
||||
('exam', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='exam',
|
||||
name='students',
|
||||
field=models.ManyToManyField(to='user.Student', verbose_name='可以参加考试的学生'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='paper',
|
||||
name='program_number',
|
||||
field=models.PositiveSmallIntegerField(default=5, verbose_name='编程题数'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,36 @@
|
||||
# Generated by Django 3.0.3 on 2020-04-20 15:05
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('user', '0001_initial'),
|
||||
('question', '0001_initial'),
|
||||
('exam', '0002_auto_20200401_2255'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Exercise',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=20, verbose_name='练习名称')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Recode',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('your_answer', models.CharField(blank=True, max_length=200, null=True, verbose_name='你的作答')),
|
||||
('choice', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='question.Choice', verbose_name='选择题')),
|
||||
('exercise', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='exam.Exercise', verbose_name='练习')),
|
||||
('fill', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='question.Fill', verbose_name='填空题')),
|
||||
('judge', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='question.Judge', verbose_name='判断题')),
|
||||
('program', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='question.Program', verbose_name='编程题')),
|
||||
('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='user.Student', verbose_name='学生')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 3.0.3 on 2020-04-21 15:22
|
||||
|
||||
import datetime
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('exam', '0003_exercise_recode'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='exercise',
|
||||
options={'ordering': ['id'], 'verbose_name': '练习', 'verbose_name_plural': '练习'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='recode',
|
||||
options={'ordering': ['id'], 'verbose_name': '练习记录', 'verbose_name_plural': '练习记录'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='exercise',
|
||||
name='create_time',
|
||||
field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2020, 4, 21, 23, 22, 51, 717049), verbose_name='练习时间'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 3.0.3 on 2020-04-21 16:05
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('exam', '0004_auto_20200421_2322'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name='Exercise',
|
||||
new_name='Practice',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.0.3 on 2020-04-23 15:33
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('exam', '0005_auto_20200422_0005'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='recode',
|
||||
old_name='exercise',
|
||||
new_name='practice',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
# Generated by Django 3.0.3 on 2020-04-24 15:22
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('exam', '0006_auto_20200423_2333'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name='Recode',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 3.0.3 on 2020-04-25 08:24
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('user', '0001_initial'),
|
||||
('exam', '0007_delete_recode'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='practice',
|
||||
name='student',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='user.Student', verbose_name='学生'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,36 @@
|
||||
# Generated by Django 3.0.3 on 2020-04-25 11:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('exam', '0008_practice_student'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Clazz',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('year', models.CharField(max_length=20, verbose_name='年级')),
|
||||
('major', models.CharField(max_length=20, verbose_name='专业')),
|
||||
('clazz', models.CharField(max_length=20, verbose_name='班级')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '班级',
|
||||
'verbose_name_plural': '班级',
|
||||
'ordering': ['id'],
|
||||
},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='exam',
|
||||
name='students',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='exam',
|
||||
name='clazzs',
|
||||
field=models.ManyToManyField(to='exam.Clazz', verbose_name='参加考试的班级'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 3.0.3 on 2020-04-25 13:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('user', '0003_auto_20200425_2103'),
|
||||
('exam', '0009_auto_20200425_1959'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name='Clazz',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='exam',
|
||||
name='clazzs',
|
||||
field=models.ManyToManyField(to='user.Clazz', verbose_name='参加考试的班级'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,92 @@
|
||||
from django.db import models
|
||||
from question.models import Choice, Fill, Judge, Program
|
||||
from user.models import Student, Clazz
|
||||
from datetime import datetime
|
||||
import random
|
||||
|
||||
|
||||
# Create your models here.
|
||||
class Paper(models.Model):
|
||||
"""试卷模型类"""
|
||||
LEVEL_CHOICES = (
|
||||
('1', '入门'),
|
||||
('2', '简单'),
|
||||
('3', '普通'),
|
||||
('4', '较难'),
|
||||
('5', '困难')
|
||||
)
|
||||
name = models.CharField("试卷名称", max_length=20, default="")
|
||||
score = models.PositiveSmallIntegerField("总分", default=100)
|
||||
choice_number = models.PositiveSmallIntegerField("选择题数", default=10)
|
||||
fill_number = models.PositiveSmallIntegerField("填空题数", default=10)
|
||||
judge_number = models.PositiveSmallIntegerField("判断题数", default=10)
|
||||
program_number = models.PositiveSmallIntegerField("编程题数", default=5)
|
||||
level = models.CharField("难度等级", max_length=1, choices=LEVEL_CHOICES, default="1")
|
||||
|
||||
class Meta:
|
||||
ordering = ["id"]
|
||||
verbose_name = "试卷"
|
||||
verbose_name_plural = verbose_name
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.score = (self.choice_number + self.fill_number + self.judge_number) * 2 + self.program_number * 8
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class Exam(models.Model):
|
||||
"""考试模型类"""
|
||||
name = models.CharField("考试名称", max_length=20, default="")
|
||||
exam_date = models.DateField("考试日期", default="")
|
||||
total_time = models.PositiveSmallIntegerField("时长", default=120, help_text="时长按照分钟填写")
|
||||
paper = models.OneToOneField(Paper, on_delete=models.CASCADE, verbose_name="试卷", default="")
|
||||
major = models.CharField("专业", max_length=20, default="")
|
||||
tips = models.TextField("考生须知", default="")
|
||||
clazzs = models.ManyToManyField(Clazz, verbose_name="参加考试的班级")
|
||||
|
||||
class Meta:
|
||||
ordering = ["id"]
|
||||
db_table = 'exam_info'
|
||||
verbose_name = "考试"
|
||||
verbose_name_plural = verbose_name
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Grade(models.Model):
|
||||
"""成绩模型类"""
|
||||
exam = models.ForeignKey(Exam, verbose_name="考试", on_delete=models.CASCADE)
|
||||
student = models.ForeignKey(Student, verbose_name="学生", on_delete=models.CASCADE)
|
||||
score = models.PositiveSmallIntegerField("分数", default="")
|
||||
create_time = models.DateTimeField("创建日期", auto_now_add=True)
|
||||
update_time = models.DateTimeField("修改日期", auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['id']
|
||||
verbose_name = '成绩'
|
||||
verbose_name_plural = verbose_name
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.id}的{self.student}为{self.score}分'
|
||||
|
||||
|
||||
class Practice(models.Model):
|
||||
"""模拟练习"""
|
||||
name = models.CharField("练习名称", max_length=20)
|
||||
student = models.ForeignKey(Student, verbose_name="学生", on_delete=models.CASCADE)
|
||||
create_time = models.DateTimeField("练习时间", auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['id']
|
||||
verbose_name = '练习'
|
||||
verbose_name_plural = verbose_name
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.name = f'模拟练习{datetime.now().strftime("%Y%m%d")}{random.randint(1000, 9999)}'
|
||||
super().save(*args, **kwargs)
|
||||
@@ -0,0 +1,46 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from exam.models import Exam, Paper, Grade, Practice
|
||||
from user.models import Student
|
||||
from user.serializers import StudentSerializer
|
||||
|
||||
|
||||
class PaperSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Paper
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class ExamSerializer(serializers.ModelSerializer):
|
||||
# 覆盖外键字段
|
||||
paper = PaperSerializer()
|
||||
|
||||
class Meta:
|
||||
model = Exam
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class GradeSerializer(serializers.ModelSerializer):
|
||||
# 覆盖外键字段 只读
|
||||
exam = ExamSerializer(read_only=True)
|
||||
student = StudentSerializer(read_only=True)
|
||||
|
||||
# 用于创建的只写字段
|
||||
exam_id = serializers.PrimaryKeyRelatedField(queryset=Exam.objects.all(), source='exam', write_only=True)
|
||||
student_id = serializers.PrimaryKeyRelatedField(queryset=Student.objects.all(), source='student', write_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Grade
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class PracticeSerializer(serializers.ModelSerializer):
|
||||
# 覆盖外键字段 只读
|
||||
student = StudentSerializer(read_only=True)
|
||||
|
||||
# 用于创建的只写字段
|
||||
student_id = serializers.PrimaryKeyRelatedField(queryset=Student.objects.all(), source='student', write_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Practice
|
||||
fields = '__all__'
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -0,0 +1,85 @@
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework import mixins, viewsets, filters
|
||||
from rest_framework.pagination import PageNumberPagination
|
||||
|
||||
from exam.filter import ExamFilter
|
||||
from exam.models import Exam, Grade, Practice
|
||||
from exam.serializers import ExamSerializer, GradeSerializer, PracticeSerializer
|
||||
# Create your views here.
|
||||
from user.models import Student
|
||||
|
||||
|
||||
class CommonPagination(PageNumberPagination):
|
||||
"""考试列表自定义分页"""
|
||||
# 默认每页显示的个数
|
||||
page_size = 10
|
||||
# 可以动态改变每页显示的个数
|
||||
page_size_query_param = 'page_size'
|
||||
# 页码参数
|
||||
page_query_param = 'page'
|
||||
# 最多能显示多少页
|
||||
max_page_size = 10
|
||||
|
||||
|
||||
class ExamListViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
|
||||
"""考试列表页"""
|
||||
# 这里必须要定义一个默认的排序,否则会报错
|
||||
queryset = Exam.objects.all().order_by('id')
|
||||
# 序列化
|
||||
serializer_class = ExamSerializer
|
||||
# 分页
|
||||
pagination_class = CommonPagination
|
||||
# 开启过滤
|
||||
filter_backends = (DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter)
|
||||
# 设置filter的类为我们自定义的类
|
||||
filter_class = ExamFilter
|
||||
# 搜索,=name表示精确搜索,也可以使用各种正则表达式
|
||||
search_fields = ('name', 'major')
|
||||
# 排序
|
||||
ordering_fields = ('id', 'exam_date')
|
||||
|
||||
# 重写queryset
|
||||
def get_queryset(self):
|
||||
# 学生ID
|
||||
student_id = self.request.query_params.get("student_id")
|
||||
student = Student.objects.get(id=student_id)
|
||||
|
||||
if student:
|
||||
self.queryset = Exam.objects.filter(clazzs__student=student)
|
||||
return self.queryset
|
||||
|
||||
|
||||
class GradeListViewSet(mixins.ListModelMixin, mixins.CreateModelMixin, viewsets.GenericViewSet):
|
||||
"""成绩列表"""
|
||||
# 这里必须要定义一个默认的排序,否则会报错
|
||||
queryset = Grade.objects.all().order_by('-create_time')
|
||||
# 序列化
|
||||
serializer_class = GradeSerializer
|
||||
# 分页
|
||||
pagination_class = CommonPagination
|
||||
|
||||
# 重写queryset
|
||||
def get_queryset(self):
|
||||
# 学生ID
|
||||
student_id = self.request.query_params.get("student_id")
|
||||
|
||||
if student_id:
|
||||
self.queryset = Grade.objects.filter(student_id=student_id)
|
||||
return self.queryset
|
||||
|
||||
|
||||
class PracticeListViewSet(mixins.ListModelMixin, mixins.CreateModelMixin, viewsets.GenericViewSet):
|
||||
"""练习列表"""
|
||||
# 数据集
|
||||
queryset = Practice.objects.all()
|
||||
# 序列化
|
||||
serializer_class = PracticeSerializer
|
||||
# 分页
|
||||
pagination_class = CommonPagination
|
||||
|
||||
def get_queryset(self):
|
||||
# 学生ID
|
||||
student_id = self.request.query_params.get('student_id')
|
||||
if student_id:
|
||||
self.queryset = Practice.objects.filter(student_id=student_id)
|
||||
return self.queryset
|
||||
@@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Time :2022/12/19 21:19
|
||||
# @Author :lzh
|
||||
# @File : __init__.py
|
||||
# @Software: PyCharm
|
||||
Binary file not shown.
@@ -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
|
||||
@@ -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'
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
@@ -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.
@@ -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.
@@ -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.
@@ -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.
@@ -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.
@@ -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.
@@ -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.
@@ -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.
@@ -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.
@@ -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.
@@ -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.
@@ -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.
@@ -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"
|
||||
@@ -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',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.7 on 2016-07-15 06:00
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('xadmin', '0002_log'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='log',
|
||||
name='action_flag',
|
||||
field=models.CharField(max_length=32, verbose_name='action flag'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,191 @@
|
||||
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 smart_text
|
||||
from six import python_2_unicode_compatible
|
||||
|
||||
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)
|
||||
@@ -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]
|
||||
@@ -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)
|
||||
@@ -0,0 +1,69 @@
|
||||
from django.db.models import Avg, Max, Min, Count, Sum
|
||||
from django.core.exceptions import FieldDoesNotExist
|
||||
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)
|
||||
@@ -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)
|
||||
@@ -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')
|
||||
@@ -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
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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')
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user