Initial commit

This commit is contained in:
lzh
2022-12-21 23:08:04 +08:00
commit 101db715f8
7375 changed files with 1142836 additions and 0 deletions
+16
View File
@@ -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()
+152
View File
@@ -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'
}
+54
View File
@@ -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))
]
+16
View File
@@ -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()
+22
View File
@@ -0,0 +1,22 @@
# ExamOnline
Python在线考试系统-大学毕业设计
前端代码:https://github.com/520118202/exam-online
后端安装依赖
pip install -r requirements.txt
前端安装依赖
npm run install
![image](https://github.com/520118202/ExamOnline/blob/master/templates/img/1.png)
![image](https://github.com/520118202/ExamOnline/blob/master/templates/img/2.png)
![image](https://github.com/520118202/ExamOnline/blob/master/templates/img/3.png)
![image](https://github.com/520118202/ExamOnline/blob/master/templates/img/4.png)
![image](https://github.com/520118202/ExamOnline/blob/master/templates/img/5.png)
![image](https://github.com/520118202/ExamOnline/blob/master/templates/img/6.png)
![image](https://github.com/520118202/ExamOnline/blob/master/templates/img/7.png)
![image](https://github.com/520118202/ExamOnline/blob/master/templates/img/8.png)
![image](https://github.com/520118202/ExamOnline/blob/master/templates/img/9.png)
![image](https://github.com/520118202/ExamOnline/blob/master/templates/img/10.png)
![image](https://github.com/520118202/ExamOnline/blob/master/templates/img/11.png)
![image](https://github.com/520118202/ExamOnline/blob/master/templates/img/12.png)
![image](https://github.com/520118202/ExamOnline/blob/master/templates/img/13.png)
![image](https://github.com/520118202/ExamOnline/blob/master/templates/img/14.png)
![image](https://github.com/520118202/ExamOnline/blob/master/templates/img/15.png)
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
default_app_config = 'exam.apps.ExamConfig'
+3
View File
@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.
+84
View File
@@ -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)
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class ExamConfig(AppConfig):
name = 'exam'
verbose_name = '考试管理'
+14
View File
@@ -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='参加考试的班级'),
),
]
+92
View File
@@ -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)
+46
View File
@@ -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__'
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+85
View File
@@ -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
+5
View File
@@ -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)
+15
View File
@@ -0,0 +1,15 @@
from django.apps import AppConfig
from django.core import checks
from django.utils.translation import ugettext_lazy as _
import xadmin
class XAdminConfig(AppConfig):
"""Simple AppConfig which does not do automatic discovery."""
name = 'xadmin'
verbose_name = _("Administration")
def ready(self):
self.module.autodiscover()
setattr(xadmin,'site',xadmin.site)
+573
View File
@@ -0,0 +1,573 @@
from __future__ import absolute_import
from django.db import models
from django.core.exceptions import ImproperlyConfigured
from django.utils.encoding import smart_text
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from django.template.loader import get_template
from django.template.context import Context
from django.utils import six
from django.utils.safestring import mark_safe
from django.utils.html import escape, format_html
from django.utils.text import Truncator
from django.core.cache import cache, caches
from xadmin.views.list import EMPTY_CHANGELIST_VALUE
from xadmin.util import is_related_field, is_related_field2
import datetime
FILTER_PREFIX = '_p_'
SEARCH_VAR = '_q_'
from .util import (get_model_from_relation,
reverse_field_path, get_limit_choices_to_from_path, prepare_lookup_value)
class BaseFilter(object):
title = None
template = 'xadmin/filters/list.html'
@classmethod
def test(cls, field, request, params, model, admin_view, field_path):
pass
def __init__(self, request, params, model, admin_view):
self.used_params = {}
self.request = request
self.params = params
self.model = model
self.admin_view = admin_view
if self.title is None:
raise ImproperlyConfigured(
"The filter '%s' does not specify "
"a 'title'." % self.__class__.__name__)
def query_string(self, new_params=None, remove=None):
return self.admin_view.get_query_string(new_params, remove)
def form_params(self):
arr = map(lambda k: FILTER_PREFIX + k, self.used_params.keys())
if six.PY3:
arr = list(arr)
return self.admin_view.get_form_params(remove=arr)
def has_output(self):
"""
Returns True if some choices would be output for this filter.
"""
raise NotImplementedError
@property
def is_used(self):
return len(self.used_params) > 0
def do_filte(self, queryset):
"""
Returns the filtered queryset.
"""
raise NotImplementedError
def get_context(self):
return {'title': self.title, 'spec': self, 'form_params': self.form_params()}
def __str__(self):
tpl = get_template(self.template)
return mark_safe(tpl.render(context=self.get_context()))
class FieldFilterManager(object):
_field_list_filters = []
_take_priority_index = 0
def register(self, list_filter_class, take_priority=False):
if take_priority:
# This is to allow overriding the default filters for certain types
# of fields with some custom filters. The first found in the list
# is used in priority.
self._field_list_filters.insert(
self._take_priority_index, list_filter_class)
self._take_priority_index += 1
else:
self._field_list_filters.append(list_filter_class)
return list_filter_class
def create(self, field, request, params, model, admin_view, field_path):
for list_filter_class in self._field_list_filters:
if not list_filter_class.test(field, request, params, model, admin_view, field_path):
continue
return list_filter_class(field, request, params,
model, admin_view, field_path=field_path)
manager = FieldFilterManager()
class FieldFilter(BaseFilter):
lookup_formats = {}
def __init__(self, field, request, params, model, admin_view, field_path):
self.field = field
self.field_path = field_path
self.title = getattr(field, 'verbose_name', field_path)
self.context_params = {}
super(FieldFilter, self).__init__(request, params, model, admin_view)
for name, format in self.lookup_formats.items():
p = format % field_path
self.context_params["%s_name" % name] = FILTER_PREFIX + p
if p in params:
value = prepare_lookup_value(p, params.pop(p))
self.used_params[p] = value
self.context_params["%s_val" % name] = value
else:
self.context_params["%s_val" % name] = ''
arr = map(
lambda kv: setattr(self, 'lookup_' + kv[0], kv[1]),
self.context_params.items()
)
if six.PY3:
list(arr)
def get_context(self):
context = super(FieldFilter, self).get_context()
context.update(self.context_params)
obj = map(lambda k: FILTER_PREFIX + k, self.used_params.keys())
if six.PY3:
obj = list(obj)
context['remove_url'] = self.query_string({}, obj)
return context
def has_output(self):
return True
def do_filte(self, queryset):
return queryset.filter(**self.used_params)
class ListFieldFilter(FieldFilter):
template = 'xadmin/filters/list.html'
def get_context(self):
context = super(ListFieldFilter, self).get_context()
context['choices'] = list(self.choices())
return context
@manager.register
class BooleanFieldListFilter(ListFieldFilter):
lookup_formats = {'exact': '%s__exact', 'isnull': '%s__isnull'}
@classmethod
def test(cls, field, request, params, model, admin_view, field_path):
return isinstance(field, (models.BooleanField, models.NullBooleanField))
def choices(self):
for lookup, title in (
('', _('All')),
('1', _('Yes')),
('0', _('No')),
):
yield {
'selected': (
self.lookup_exact_val == lookup
and not self.lookup_isnull_val
),
'query_string': self.query_string(
{self.lookup_exact_name: lookup},
[self.lookup_isnull_name],
),
'display': title,
}
if isinstance(self.field, models.NullBooleanField):
yield {
'selected': self.lookup_isnull_val == 'True',
'query_string': self.query_string(
{self.lookup_isnull_name: 'True'},
[self.lookup_exact_name],
),
'display': _('Unknown'),
}
@manager.register
class ChoicesFieldListFilter(ListFieldFilter):
lookup_formats = {'exact': '%s__exact'}
@classmethod
def test(cls, field, request, params, model, admin_view, field_path):
return bool(field.choices)
def choices(self):
yield {
'selected': self.lookup_exact_val is '',
'query_string': self.query_string({}, [self.lookup_exact_name]),
'display': _('All')
}
for lookup, title in self.field.flatchoices:
yield {
'selected': smart_text(lookup) == self.lookup_exact_val,
'query_string': self.query_string({self.lookup_exact_name: lookup}),
'display': title,
}
@manager.register
class TextFieldListFilter(FieldFilter):
template = 'xadmin/filters/char.html'
lookup_formats = {'in': '%s__in', 'search': '%s__contains'}
@classmethod
def test(cls, field, request, params, model, admin_view, field_path):
return (
isinstance(field, models.CharField)
and field.max_length > 20
or isinstance(field, models.TextField)
)
@manager.register
class NumberFieldListFilter(FieldFilter):
template = 'xadmin/filters/number.html'
lookup_formats = {'equal': '%s__exact', 'lt': '%s__lt', 'gt': '%s__gt',
'ne': '%s__ne', 'lte': '%s__lte', 'gte': '%s__gte',
}
@classmethod
def test(cls, field, request, params, model, admin_view, field_path):
return isinstance(field, (models.DecimalField, models.FloatField, models.IntegerField))
def do_filte(self, queryset):
params = self.used_params.copy()
ne_key = '%s__ne' % self.field_path
if ne_key in params:
queryset = queryset.exclude(
**{self.field_path: params.pop(ne_key)})
return queryset.filter(**params)
@manager.register
class DateFieldListFilter(ListFieldFilter):
template = 'xadmin/filters/date.html'
lookup_formats = {'since': '%s__gte', 'until': '%s__lt',
'year': '%s__year', 'month': '%s__month', 'day': '%s__day',
'isnull': '%s__isnull'}
@classmethod
def test(cls, field, request, params, model, admin_view, field_path):
return isinstance(field, models.DateField)
def __init__(self, field, request, params, model, admin_view, field_path):
self.field_generic = '%s__' % field_path
self.date_params = dict([(FILTER_PREFIX + k, v) for k, v in params.items()
if k.startswith(self.field_generic)])
super(DateFieldListFilter, self).__init__(
field, request, params, model, admin_view, field_path)
now = timezone.now()
# When time zone support is enabled, convert "now" to the user's time
# zone so Django's definition of "Today" matches what the user expects.
if now.tzinfo is not None:
current_tz = timezone.get_current_timezone()
now = now.astimezone(current_tz)
if hasattr(current_tz, 'normalize'):
# available for pytz time zones
now = current_tz.normalize(now)
if isinstance(field, models.DateTimeField):
today = now.replace(hour=0, minute=0, second=0, microsecond=0)
else: # field is a models.DateField
today = now.date()
tomorrow = today + datetime.timedelta(days=1)
self.links = (
(_('Any date'), {}),
(_('Has date'), {
self.lookup_isnull_name: False
}),
(_('Has no date'), {
self.lookup_isnull_name: 'True'
}),
(_('Today'), {
self.lookup_since_name: str(today),
self.lookup_until_name: str(tomorrow),
}),
(_('Past 7 days'), {
self.lookup_since_name: str(today - datetime.timedelta(days=7)),
self.lookup_until_name: str(tomorrow),
}),
(_('This month'), {
self.lookup_since_name: str(today.replace(day=1)),
self.lookup_until_name: str(tomorrow),
}),
(_('This year'), {
self.lookup_since_name: str(today.replace(month=1, day=1)),
self.lookup_until_name: str(tomorrow),
}),
)
def get_context(self):
context = super(DateFieldListFilter, self).get_context()
context['choice_selected'] = bool(self.lookup_year_val) or bool(self.lookup_month_val) \
or bool(self.lookup_day_val)
return context
def choices(self):
for title, param_dict in self.links:
yield {
'selected': self.date_params == param_dict,
'query_string': self.query_string(
param_dict, [FILTER_PREFIX + self.field_generic]),
'display': title,
}
@manager.register
class RelatedFieldSearchFilter(FieldFilter):
template = 'xadmin/filters/fk_search.html'
@classmethod
def test(cls, field, request, params, model, admin_view, field_path):
if not is_related_field2(field):
return False
related_modeladmin = admin_view.admin_site._registry.get(
get_model_from_relation(field))
return related_modeladmin and getattr(related_modeladmin, 'relfield_style', None) in ('fk-ajax', 'fk-select')
def __init__(self, field, request, params, model, model_admin, field_path):
other_model = get_model_from_relation(field)
if hasattr(field, 'remote_field'):
rel_name = field.remote_field.get_related_field().name
else:
rel_name = other_model._meta.pk.name
self.lookup_formats = {'in': '%%s__%s__in' % rel_name, 'exact': '%%s__%s__exact' % rel_name}
super(RelatedFieldSearchFilter, self).__init__(
field, request, params, model, model_admin, field_path)
related_modeladmin = self.admin_view.admin_site._registry.get(other_model)
self.relfield_style = related_modeladmin.relfield_style
if hasattr(field, 'verbose_name'):
self.lookup_title = field.verbose_name
else:
self.lookup_title = other_model._meta.verbose_name
self.title = self.lookup_title
self.search_url = model_admin.get_admin_url('%s_%s_changelist' % (
other_model._meta.app_label, other_model._meta.model_name))
self.label = self.label_for_value(other_model, rel_name, self.lookup_exact_val) if self.lookup_exact_val else ""
self.choices = '?'
if field.remote_field.limit_choices_to:
for i in list(field.remote_field.limit_choices_to):
self.choices += "&_p_%s=%s" % (i, field.remote_field.limit_choices_to[i])
self.choices = format_html(self.choices)
def label_for_value(self, other_model, rel_name, value):
try:
obj = other_model._default_manager.get(**{rel_name: value})
return '%s' % escape(Truncator(obj).words(14, truncate='...'))
except (ValueError, other_model.DoesNotExist):
return ""
def get_context(self):
context = super(RelatedFieldSearchFilter, self).get_context()
context['search_url'] = self.search_url
context['label'] = self.label
context['choices'] = self.choices
context['relfield_style'] = self.relfield_style
return context
@manager.register
class RelatedFieldListFilter(ListFieldFilter):
@classmethod
def test(cls, field, request, params, model, admin_view, field_path):
return is_related_field2(field)
def __init__(self, field, request, params, model, model_admin, field_path):
other_model = get_model_from_relation(field)
if hasattr(field, 'remote_field'):
rel_name = field.remote_field.get_related_field().name
else:
rel_name = other_model._meta.pk.name
self.lookup_formats = {'in': '%%s__%s__in' % rel_name, 'exact': '%%s__%s__exact' %
rel_name, 'isnull': '%s__isnull'}
self.lookup_choices = field.get_choices(include_blank=False)
super(RelatedFieldListFilter, self).__init__(
field, request, params, model, model_admin, field_path)
if hasattr(field, 'verbose_name'):
self.lookup_title = field.verbose_name
else:
self.lookup_title = other_model._meta.verbose_name
self.title = self.lookup_title
def has_output(self):
if (is_related_field(self.field)
and self.field.field.null or hasattr(self.field, 'remote_field')
and self.field.null):
extra = 1
else:
extra = 0
return len(self.lookup_choices) + extra > 1
def expected_parameters(self):
return [self.lookup_kwarg, self.lookup_kwarg_isnull]
def choices(self):
yield {
'selected': self.lookup_exact_val == '' and not self.lookup_isnull_val,
'query_string': self.query_string({},
[self.lookup_exact_name, self.lookup_isnull_name]),
'display': _('All'),
}
for pk_val, val in self.lookup_choices:
yield {
'selected': self.lookup_exact_val == smart_text(pk_val),
'query_string': self.query_string({
self.lookup_exact_name: pk_val,
}, [self.lookup_isnull_name]),
'display': val,
}
if (is_related_field(self.field)
and self.field.field.null or hasattr(self.field, 'remote_field')
and self.field.null):
yield {
'selected': bool(self.lookup_isnull_val),
'query_string': self.query_string({
self.lookup_isnull_name: 'True',
}, [self.lookup_exact_name]),
'display': EMPTY_CHANGELIST_VALUE,
}
@manager.register
class MultiSelectFieldListFilter(ListFieldFilter):
""" Delegates the filter to the default filter and ors the results of each
Lists the distinct values of each field as a checkbox
Uses the default spec for each
"""
template = 'xadmin/filters/checklist.html'
lookup_formats = {'in': '%s__in'}
cache_config = {'enabled': False, 'key': 'quickfilter_%s', 'timeout': 3600, 'cache': 'default'}
@classmethod
def test(cls, field, request, params, model, admin_view, field_path):
return True
def get_cached_choices(self):
if not self.cache_config['enabled']:
return None
c = caches(self.cache_config['cache'])
return c.get(self.cache_config['key'] % self.field_path)
def set_cached_choices(self, choices):
if not self.cache_config['enabled']:
return
c = caches(self.cache_config['cache'])
return c.set(self.cache_config['key'] % self.field_path, choices)
def __init__(self, field, request, params, model, model_admin, field_path, field_order_by=None, field_limit=None, sort_key=None, cache_config=None):
super(MultiSelectFieldListFilter, self).__init__(field, request, params, model, model_admin, field_path)
# Check for it in the cachce
if cache_config is not None and type(cache_config) == dict:
self.cache_config.update(cache_config)
if self.cache_config['enabled']:
self.field_path = field_path
choices = self.get_cached_choices()
if choices:
self.lookup_choices = choices
return
# Else rebuild it
queryset = self.admin_view.queryset().exclude(**{"%s__isnull" % field_path: True}).values_list(field_path, flat=True).distinct()
#queryset = self.admin_view.queryset().distinct(field_path).exclude(**{"%s__isnull"%field_path:True})
if field_order_by is not None:
# Do a subquery to order the distinct set
queryset = self.admin_view.queryset().filter(id__in=queryset).order_by(field_order_by)
if field_limit is not None and type(field_limit) == int and queryset.count() > field_limit:
queryset = queryset[:field_limit]
self.lookup_choices = [str(it) for it in queryset.values_list(field_path, flat=True) if str(it).strip() != ""]
if sort_key is not None:
self.lookup_choices = sorted(self.lookup_choices, key=sort_key)
if self.cache_config['enabled']:
self.set_cached_choices(self.lookup_choices)
def choices(self):
self.lookup_in_val = (type(self.lookup_in_val) in (tuple, list)) and self.lookup_in_val or list(self.lookup_in_val)
yield {
'selected': len(self.lookup_in_val) == 0,
'query_string': self.query_string({}, [self.lookup_in_name]),
'display': _('All'),
}
for val in self.lookup_choices:
yield {
'selected': smart_text(val) in self.lookup_in_val,
'query_string': self.query_string({self.lookup_in_name: ",".join([val] + self.lookup_in_val), }),
'remove_query_string': self.query_string({self.lookup_in_name: ",".join([v for v in self.lookup_in_val if v != val]), }),
'display': val,
}
@manager.register
class AllValuesFieldListFilter(ListFieldFilter):
lookup_formats = {'exact': '%s__exact', 'isnull': '%s__isnull'}
@classmethod
def test(cls, field, request, params, model, admin_view, field_path):
return True
def __init__(self, field, request, params, model, admin_view, field_path):
parent_model, reverse_path = reverse_field_path(model, field_path)
queryset = parent_model._default_manager.all()
# optional feature: limit choices base on existing relationships
# queryset = queryset.complex_filter(
# {'%s__isnull' % reverse_path: False})
limit_choices_to = get_limit_choices_to_from_path(model, field_path)
queryset = queryset.filter(limit_choices_to)
self.lookup_choices = (queryset
.distinct()
.order_by(field.name)
.values_list(field.name, flat=True))
super(AllValuesFieldListFilter, self).__init__(
field, request, params, model, admin_view, field_path)
def choices(self):
yield {
'selected': (self.lookup_exact_val is '' and self.lookup_isnull_val is ''),
'query_string': self.query_string({}, [self.lookup_exact_name, self.lookup_isnull_name]),
'display': _('All'),
}
include_none = False
for val in self.lookup_choices:
if val is None:
include_none = True
continue
val = smart_text(val)
yield {
'selected': self.lookup_exact_val == val,
'query_string': self.query_string({self.lookup_exact_name: val},
[self.lookup_isnull_name]),
'display': val,
}
if include_none:
yield {
'selected': bool(self.lookup_isnull_val),
'query_string': self.query_string({self.lookup_isnull_name: 'True'},
[self.lookup_exact_name]),
'display': EMPTY_CHANGELIST_VALUE,
}
+47
View File
@@ -0,0 +1,47 @@
from django import forms
from django.contrib.auth import authenticate
from django.contrib.auth.forms import AuthenticationForm
from django.utils.translation import ugettext_lazy, ugettext as _
from django.contrib.auth import get_user_model
ERROR_MESSAGE = ugettext_lazy("Please enter the correct username and password "
"for a staff account. Note that both fields are case-sensitive.")
class AdminAuthenticationForm(AuthenticationForm):
"""
A custom authentication form used in the admin app.
"""
this_is_the_login_form = forms.BooleanField(
widget=forms.HiddenInput, initial=1,
error_messages={'required': ugettext_lazy("Please log in again, because your session has expired.")})
def clean(self):
username = self.cleaned_data.get('username')
password = self.cleaned_data.get('password')
message = ERROR_MESSAGE
if username and password:
self.user_cache = authenticate(
username=username, password=password)
if self.user_cache is None:
if u'@' in username:
User = get_user_model()
# Mistakenly entered e-mail address instead of username? Look it up.
try:
user = User.objects.get(email=username)
except (User.DoesNotExist, User.MultipleObjectsReturned):
# Nothing to do here, moving along.
pass
else:
if user.check_password(password):
message = _("Your e-mail address is not your username."
" Try '%s' instead.") % user.username
raise forms.ValidationError(message)
elif not self.user_cache.is_active or not self.user_cache.is_staff:
raise forms.ValidationError(message)
return self.cleaned_data
+113
View File
@@ -0,0 +1,113 @@
from crispy_forms.helper import FormHelper
from crispy_forms.layout import *
from crispy_forms.bootstrap import *
from crispy_forms.utils import render_field, flatatt, TEMPLATE_PACK
from crispy_forms import layout
from crispy_forms import bootstrap
import math
class Fieldset(layout.Fieldset):
template = "xadmin/layout/fieldset.html"
def __init__(self, legend, *fields, **kwargs):
self.description = kwargs.pop('description', None)
self.collapsed = kwargs.pop('collapsed', None)
super(Fieldset, self).__init__(legend, *fields, **kwargs)
class Row(layout.Div):
def __init__(self, *fields, **kwargs):
css_class = 'form-inline form-group'
new_fields = [self.convert_field(f, len(fields)) for f in fields]
super(Row, self).__init__(css_class=css_class, *new_fields, **kwargs)
def convert_field(self, f, counts):
col_class = "col-sm-%d" % int(math.ceil(12 / counts))
if not (isinstance(f, Field) or issubclass(f.__class__, Field)):
f = layout.Field(f)
if f.wrapper_class:
f.wrapper_class += " %s" % col_class
else:
f.wrapper_class = col_class
return f
class Col(layout.Column):
def __init__(self, id, *fields, **kwargs):
css_class = ['column', 'form-column', id, 'col col-sm-%d' %
kwargs.get('span', 6)]
if kwargs.get('horizontal'):
css_class.append('form-horizontal')
super(Col, self).__init__(css_class=' '.join(css_class), *
fields, **kwargs)
class Main(layout.Column):
css_class = "column form-column main col col-sm-9 form-horizontal"
class Side(layout.Column):
css_class = "column form-column sidebar col col-sm-3"
class Container(layout.Div):
css_class = "form-container row clearfix"
# Override bootstrap3
class InputGroup(layout.Field):
template = "xadmin/layout/input_group.html"
def __init__(self, field, *args, **kwargs):
self.field = field
self.inputs = list(args)
if '@@' not in args:
self.inputs.append('@@')
self.input_size = None
css_class = kwargs.get('css_class', '')
if 'input-lg' in css_class:
self.input_size = 'input-lg'
if 'input-sm' in css_class:
self.input_size = 'input-sm'
super(InputGroup, self).__init__(field, **kwargs)
def render(self, form, form_style, context, template_pack=TEMPLATE_PACK, **kwargs):
classes = form.fields[self.field].widget.attrs.get('class', '')
extra_context = {
'inputs': self.inputs,
'input_size': self.input_size,
'classes': classes.replace('form-control', '')
}
if hasattr(self, 'wrapper_class'):
extra_context['wrapper_class'] = self.wrapper_class
return render_field(
self.field, form, form_style, context, template=self.template,
attrs=self.attrs, template_pack=template_pack, extra_context=extra_context, **kwargs)
class PrependedText(InputGroup):
def __init__(self, field, text, **kwargs):
super(PrependedText, self).__init__(field, text, '@@', **kwargs)
class AppendedText(InputGroup):
def __init__(self, field, text, **kwargs):
super(AppendedText, self).__init__(field, '@@', text, **kwargs)
class PrependedAppendedText(InputGroup):
def __init__(self, field, prepended_text=None, appended_text=None, *args, **kwargs):
super(PrependedAppendedText, self).__init__(
field, prepended_text, '@@', appended_text, **kwargs)
File diff suppressed because it is too large Load Diff
@@ -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"
File diff suppressed because it is too large Load Diff
@@ -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 ""
File diff suppressed because it is too large Load Diff
@@ -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"
File diff suppressed because it is too large Load Diff
@@ -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"
File diff suppressed because it is too large Load Diff
@@ -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 ""
File diff suppressed because it is too large Load Diff
@@ -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 ""
File diff suppressed because it is too large Load Diff
@@ -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 ""
File diff suppressed because it is too large Load Diff
@@ -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 ""
File diff suppressed because it is too large Load Diff
@@ -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"
File diff suppressed because it is too large Load Diff
@@ -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"
File diff suppressed because it is too large Load Diff
@@ -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 ""
File diff suppressed because it is too large Load Diff
@@ -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'),
),
]
+191
View File
@@ -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