added first public views - no check for access rights at the moment
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,5 +1,6 @@
|
|||||||
/filestore
|
/filestore
|
||||||
|
/static
|
||||||
__pycache__
|
__pycache__
|
||||||
.idea
|
.idea
|
||||||
venv
|
venv
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
|
|||||||
35
api/urls.py
Normal file
35
api/urls.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
from django.urls.conf import path
|
||||||
|
from drf_yasg import openapi
|
||||||
|
from drf_yasg.views import get_schema_view
|
||||||
|
from rest_framework import permissions
|
||||||
|
|
||||||
|
# from rest_framework_swagger.views import get_swagger_view
|
||||||
|
# from rest_framework.routers import DefaultRouter
|
||||||
|
# from content.views import QuestionViewSet
|
||||||
|
# schema_view = get_swagger_view(title='TablequizDB API')
|
||||||
|
# router = DefaultRouter()
|
||||||
|
# router.register(r'question', QuestionViewSet)
|
||||||
|
# urlpatterns = [
|
||||||
|
# path('docs/', schema_view, name='api-v1'),
|
||||||
|
# path('v1/', include(router.urls)),
|
||||||
|
# ]
|
||||||
|
|
||||||
|
|
||||||
|
schema_view = get_schema_view(
|
||||||
|
openapi.Info(
|
||||||
|
title="TabelquizDB API",
|
||||||
|
default_version='v1',
|
||||||
|
description="The API",
|
||||||
|
# terms_of_service="https://www.google.com/policies/terms/",
|
||||||
|
contact=openapi.Contact(email="api@tablequizdb.de"),
|
||||||
|
license=openapi.License(name="BSD License"),
|
||||||
|
),
|
||||||
|
public=True,
|
||||||
|
permission_classes=(permissions.AllowAny,),
|
||||||
|
)
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('swagger<format>/', schema_view.without_ui(cache_timeout=0), name='schema-json'),
|
||||||
|
path('', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
|
||||||
|
path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
|
||||||
|
]
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
from content.models import Link, MediaFile, Question, QuestionVersion, Level, Label
|
from content.models import Link, MediaFile, Question, QuestionVersion, Level, Label, SharedQuestion
|
||||||
from lib.utils import color_label
|
|
||||||
from lib.mixins import PermissionsAdminMixin
|
from lib.mixins import PermissionsAdminMixin
|
||||||
|
from lib.utils import color_label
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(SharedQuestion)
|
||||||
|
class SharedQuestionAdmin(admin.ModelAdmin):
|
||||||
|
search_fields = ('user__username',)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Level)
|
@admin.register(Level)
|
||||||
@@ -22,15 +27,19 @@ class LabelAdmin(PermissionsAdminMixin, admin.ModelAdmin):
|
|||||||
|
|
||||||
@admin.register(Question)
|
@admin.register(Question)
|
||||||
class QuestionAdmin(PermissionsAdminMixin, admin.ModelAdmin):
|
class QuestionAdmin(PermissionsAdminMixin, admin.ModelAdmin):
|
||||||
autocomplete_fields = ('medias', 'links', 'labels',)
|
autocomplete_fields = ('medias', 'links', 'labels', 'shares')
|
||||||
list_display = ('name', 'list_labels', 'list_level', 'author')
|
list_display = ('name', 'list_labels', 'list_level', 'author')
|
||||||
search_fields = ('name', 'question', 'awnser', 'description', 'label__name', 'level__value', 'level__name',)
|
search_fields = ('name', 'question', 'awnser', 'description', 'label__name', 'level__value', 'level__name',)
|
||||||
|
|
||||||
def list_labels(self, instance):
|
def list_labels(self, instance):
|
||||||
return mark_safe(', '.join([color_label(l, value=l.name) for l in instance.labels.all()]))
|
if instance.labels:
|
||||||
|
return mark_safe(', '.join([color_label(l, value=l.name) for l in instance.labels.all()]))
|
||||||
|
return ''
|
||||||
|
|
||||||
def list_level(self, instance):
|
def list_level(self, instance):
|
||||||
return mark_safe(color_label(instance.level, value=str(instance.level)))
|
if instance.level:
|
||||||
|
return mark_safe(color_label(instance.level, value=str(instance.level)))
|
||||||
|
return ''
|
||||||
|
|
||||||
list_level.short_description = 'Level'
|
list_level.short_description = 'Level'
|
||||||
list_labels.short_description = 'Labels'
|
list_labels.short_description = 'Labels'
|
||||||
|
|||||||
@@ -10,18 +10,14 @@ from django.forms.models import model_to_dict
|
|||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from filer.fields.file import FilerFileField
|
from filer.fields.file import FilerFileField
|
||||||
|
|
||||||
from lib import get_current_user
|
from lib.core.db.models.base import SharedPermissionBase
|
||||||
|
from lib.core.db.models.mixins import DateAware, AuthorAware, DescriptionAware, NameAware
|
||||||
|
|
||||||
|
|
||||||
class MediaFile(models.Model):
|
class MediaFile(NameAware, DateAware, AuthorAware, DescriptionAware):
|
||||||
name = models.CharField(max_length=25, unique=True, db_index=True)
|
|
||||||
# https://django-filer.readthedocs.io/en/latest/extending_filer.html
|
# https://django-filer.readthedocs.io/en/latest/extending_filer.html
|
||||||
# https://pypi.org/project/django-thumbnails/
|
# https://pypi.org/project/django-thumbnails/
|
||||||
file = FilerFileField(on_delete=models.RESTRICT, related_name='media_file')
|
file = FilerFileField(on_delete=models.RESTRICT, related_name='media_file')
|
||||||
description = models.TextField(null=True, blank=True)
|
|
||||||
author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, default=get_current_user)
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@@ -30,13 +26,8 @@ class MediaFile(models.Model):
|
|||||||
return self.file.name
|
return self.file.name
|
||||||
|
|
||||||
|
|
||||||
class Link(models.Model):
|
class Link(NameAware, DateAware, AuthorAware, DescriptionAware):
|
||||||
name = models.CharField(max_length=25, null=True, blank=True, db_index=True)
|
|
||||||
url = models.URLField(unique=True, db_index=True)
|
url = models.URLField(unique=True, db_index=True)
|
||||||
description = models.TextField(null=True, blank=True)
|
|
||||||
author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, default=get_current_user)
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.url
|
return self.url
|
||||||
@@ -45,14 +36,9 @@ class Link(models.Model):
|
|||||||
return self.url
|
return self.url
|
||||||
|
|
||||||
|
|
||||||
class Level(models.Model):
|
class Level(NameAware, DateAware, AuthorAware, DescriptionAware):
|
||||||
name = models.CharField(max_length=25, unique=True, db_index=True)
|
|
||||||
value = models.IntegerField(unique=True, db_index=True)
|
value = models.IntegerField(unique=True, db_index=True)
|
||||||
description = models.TextField(null=True, blank=True)
|
|
||||||
color = ColorField(default='#F90F90')
|
color = ColorField(default='#F90F90')
|
||||||
author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, default=get_current_user)
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'{self.value} - {self.name}'
|
return f'{self.value} - {self.name}'
|
||||||
@@ -61,13 +47,8 @@ class Level(models.Model):
|
|||||||
return f'{self.value}'
|
return f'{self.value}'
|
||||||
|
|
||||||
|
|
||||||
class Label(models.Model):
|
class Label(NameAware, DateAware, AuthorAware, DescriptionAware):
|
||||||
name = models.CharField(max_length=25, unique=True, db_index=True)
|
|
||||||
description = models.TextField(null=True, blank=True)
|
|
||||||
color = ColorField(default='#666666')
|
color = ColorField(default='#666666')
|
||||||
author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, default=get_current_user)
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@@ -76,19 +57,21 @@ class Label(models.Model):
|
|||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
class Question(models.Model):
|
class SharedQuestion(SharedPermissionBase):
|
||||||
|
class Meta:
|
||||||
|
abstract = False
|
||||||
|
|
||||||
|
|
||||||
|
class Question(DateAware, AuthorAware, DescriptionAware):
|
||||||
name = models.CharField(max_length=500, unique=True, db_index=True)
|
name = models.CharField(max_length=500, unique=True, db_index=True)
|
||||||
question = models.TextField(db_index=True)
|
question = models.TextField(db_index=True)
|
||||||
buzzword = models.CharField(max_length=25, null=True, blank=True)
|
buzzword = models.CharField(max_length=25, null=True, blank=True)
|
||||||
awnser = models.TextField(db_index=True)
|
awnser = models.TextField(db_index=True)
|
||||||
level = models.ForeignKey(Level, on_delete=models.SET_NULL, null=True)
|
level = models.ForeignKey(Level, on_delete=models.SET_NULL, null=True)
|
||||||
description = models.TextField(null=True, blank=True)
|
|
||||||
labels = models.ManyToManyField(Label, blank=True)
|
labels = models.ManyToManyField(Label, blank=True)
|
||||||
medias = models.ManyToManyField(MediaFile, blank=True)
|
medias = models.ManyToManyField(MediaFile, blank=True)
|
||||||
links = models.ManyToManyField(Link, blank=True)
|
links = models.ManyToManyField(Link, blank=True)
|
||||||
author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
|
shares = models.ManyToManyField(SharedQuestion, blank=True)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@@ -96,23 +79,38 @@ class Question(models.Model):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def to_view(self, for_players: bool = False):
|
@staticmethod
|
||||||
ret = {}
|
def searchdomain(term, from_quiz=False):
|
||||||
ret['name'] = '{}{}'.format(self.name, f'({self.buzzword})' if self.buzzword else '')
|
return \
|
||||||
ret['question'] = self.question
|
models.Q(name__icontains=term) \
|
||||||
if self.description:
|
| models.Q(question__icontains=term) \
|
||||||
ret['question'] += ' \n\n------------------------------\n\n' + self.description
|
| models.Q(awnser__icontains=term) \
|
||||||
ret['awnser'] = self.awnser
|
| models.Q(buzzword__icontains=term) \
|
||||||
ret['level'] = self.level.value
|
| models.Q(labels__name__icontains=term)
|
||||||
ret['medias'] = []
|
|
||||||
ret['links'] = []
|
|
||||||
for media in self.medias.all():
|
@staticmethod
|
||||||
ret.setdefault('medias', []).append(os.path.basename(media.file.path))
|
def get_by_tearchterm(term):
|
||||||
for link in self.links.all():
|
return Question.objects.filter(Question.searchdomain(term)).annotate(cnt=models.Count('id'))
|
||||||
ret.setdefault('links', []).append(link.url)
|
|
||||||
ret['question'] = mark_safe(ret['question'].replace('\n', '\n<br />'))
|
|
||||||
logging.error(ret)
|
def to_view(self, for_players: bool = False):
|
||||||
return ret
|
ret = {}
|
||||||
|
ret['name'] = '{}{}'.format(self.name, f'({self.buzzword})' if self.buzzword else '')
|
||||||
|
ret['question'] = self.question
|
||||||
|
if self.description:
|
||||||
|
ret['question'] += ' \n\n------------------------------\n\n' + self.description
|
||||||
|
ret['awnser'] = self.awnser
|
||||||
|
ret['level'] = self.level.value
|
||||||
|
ret['medias'] = []
|
||||||
|
ret['links'] = []
|
||||||
|
for media in self.medias.all():
|
||||||
|
ret.setdefault('medias', []).append(os.path.basename(media.file.path))
|
||||||
|
for link in self.links.all():
|
||||||
|
ret.setdefault('links', []).append(link.url)
|
||||||
|
ret['question'] = mark_safe(ret['question'].replace('\n', '\n<br />'))
|
||||||
|
logging.error(ret)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
def default_json(o):
|
def default_json(o):
|
||||||
@@ -121,12 +119,9 @@ def default_json(o):
|
|||||||
return json.dumps(model_to_dict(o))
|
return json.dumps(model_to_dict(o))
|
||||||
|
|
||||||
|
|
||||||
class QuestionVersion(models.Model):
|
class QuestionVersion(DateAware, AuthorAware):
|
||||||
question = models.ForeignKey(Question, on_delete=models.SET_NULL, null=True)
|
question = models.ForeignKey(Question, on_delete=models.SET_NULL, null=True)
|
||||||
data = models.JSONField()
|
data = models.JSONField()
|
||||||
author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'{self.question} - {self.created_at}' if self.question else f'{self.data.name} - {self.created_at}'
|
return f'{self.question} - {self.created_at}' if self.question else f'{self.data.name} - {self.created_at}'
|
||||||
@@ -140,6 +135,7 @@ def versionize_question(sender, instance: Question, *args, **kwargs):
|
|||||||
data['medias'] = [{'id': m.id, '__str__': str(m)} for m in data['medias']]
|
data['medias'] = [{'id': m.id, '__str__': str(m)} for m in data['medias']]
|
||||||
data['links'] = [{'id': m.id, '__str__': str(m)} for m in data['links']]
|
data['links'] = [{'id': m.id, '__str__': str(m)} for m in data['links']]
|
||||||
data['labels'] = [{'id': m.id, '__str__': str(m)} for m in data['labels']]
|
data['labels'] = [{'id': m.id, '__str__': str(m)} for m in data['labels']]
|
||||||
|
data['shares'] = list(map(str, data['shares']))
|
||||||
|
|
||||||
QuestionVersion.objects.create(
|
QuestionVersion.objects.create(
|
||||||
question=instance,
|
question=instance,
|
||||||
|
|||||||
29
content/serializers.py
Normal file
29
content/serializers.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from content.models import Question, Label, Level
|
||||||
|
|
||||||
|
|
||||||
|
class QuestionSerializer(serializers.Serializer):
|
||||||
|
# question = serializers.CharField()
|
||||||
|
# awnser = serializers.CharField()
|
||||||
|
# buzzword = serializers.CharField(required=False)
|
||||||
|
# level = serializers.IntegerField()
|
||||||
|
# labels = serializers.MultipleChoiceField(choices=[(l.id, l.name) for l in Label.objects.all()], required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Question
|
||||||
|
# fields = ['id', 'name', 'description', 'level', 'labels', 'medias', ]
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
"""
|
||||||
|
def create(self, validated_data):
|
||||||
|
return Question.objects.create(**validated_data)
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
instance.name = validated_data.get('name', instance.name)
|
||||||
|
# ...
|
||||||
|
instance.save()
|
||||||
|
return instance
|
||||||
|
"""
|
||||||
43
content/templates/questions.html
Normal file
43
content/templates/questions.html
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block content %}
|
||||||
|
<form action="/questions" method="get" id="search_q_form">
|
||||||
|
<div id="question-search-field" class="row mb-5 mt-5">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-group">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="text" name="term" class="form-control p-2 bg-light" placeholder="Search for ..." value="{{ request.GET.term }}" />
|
||||||
|
<span class="input-group-text">
|
||||||
|
<i class="fa fa-search" onclick="document.getElementById('search_q_form').submit();"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div id="question-list" class="mt-2">
|
||||||
|
{% if items %}
|
||||||
|
<div class="row text-capitalize text-light bg-secondary border rounded p-2">
|
||||||
|
<div class="col-1">#</div>
|
||||||
|
<div class="col-3">Name</div>
|
||||||
|
<div class="col-3">Question</div>
|
||||||
|
<div class="col-1">Files</div>
|
||||||
|
<div class="col-1">Labels</div>
|
||||||
|
</div>
|
||||||
|
{% for q in items %}
|
||||||
|
<div class="row p-2">
|
||||||
|
<div class="col-1">{{ q.id }}</div>
|
||||||
|
<div class="col-3">{{ q.name }}</div>
|
||||||
|
<div class="col-3">{{ q.question }}</div>
|
||||||
|
<div class="col-1">{{ q.medias.all | length }}</div>
|
||||||
|
<div class="col-3">
|
||||||
|
{% for l in q.labels.all %}
|
||||||
|
{{ l.name }}
|
||||||
|
{% if not forloop.last %},{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
6
content/urls.py
Normal file
6
content/urls.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from django.urls.conf import path
|
||||||
|
from content.views import public
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('questions/', public.search_question, name='search-questions'),
|
||||||
|
]
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from django.shortcuts import render
|
|
||||||
|
|
||||||
# Create your views here.
|
|
||||||
1
content/views/__init__.py
Normal file
1
content/views/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .api import QuestionViewSet
|
||||||
50
content/views/api.py
Normal file
50
content/views/api.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
from drf_yasg import openapi
|
||||||
|
from drf_yasg.utils import swagger_auto_schema, status
|
||||||
|
from rest_framework import viewsets
|
||||||
|
|
||||||
|
from content.models import Question
|
||||||
|
from content.serializers import QuestionSerializer
|
||||||
|
from lib.api.permissions import IsAuthorOrReadOnly
|
||||||
|
|
||||||
|
|
||||||
|
class QuestionViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = Question.objects.all()
|
||||||
|
serializer_class = QuestionSerializer
|
||||||
|
permission_classes = (
|
||||||
|
IsAuthorOrReadOnly,
|
||||||
|
)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
request_body=openapi.Schema(
|
||||||
|
type=openapi.TYPE_OBJECT,
|
||||||
|
properties={
|
||||||
|
"question": openapi.Schema(
|
||||||
|
type=openapi.TYPE_STRING, description="Email Address"
|
||||||
|
),
|
||||||
|
"awnser": openapi.Schema(
|
||||||
|
type=openapi.TYPE_STRING, description="Previous Password"
|
||||||
|
),
|
||||||
|
"label": openapi.Schema(
|
||||||
|
type=openapi.TYPE_STRING, description="New Password"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
responses={
|
||||||
|
status.HTTP_202_ACCEPTED: openapi.Schema(
|
||||||
|
type=openapi.TYPE_OBJECT,
|
||||||
|
properties={
|
||||||
|
"status": openapi.Schema(type=openapi.TYPE_NUMBER),
|
||||||
|
"message": openapi.Schema(type=openapi.TYPE_STRING),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
status.HTTP_401_UNAUTHORIZED: openapi.Schema(
|
||||||
|
type=openapi.TYPE_OBJECT,
|
||||||
|
properties={
|
||||||
|
"status": openapi.Schema(type=openapi.TYPE_NUMBER),
|
||||||
|
"message": openapi.Schema(type=openapi.TYPE_STRING),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def create(self, request):
|
||||||
|
pass
|
||||||
13
content/views/public.py
Normal file
13
content/views/public.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
from content.models import Question
|
||||||
|
|
||||||
|
|
||||||
|
def search_question(request):
|
||||||
|
term = request.GET.get('term')
|
||||||
|
items = []
|
||||||
|
if term:
|
||||||
|
items = Question.get_by_tearchterm(term)
|
||||||
|
else:
|
||||||
|
items = Question.objects.all()[:10]
|
||||||
|
return render(request, 'questions.html', {'items': items})
|
||||||
0
lib/api/__init__.py
Normal file
0
lib/api/__init__.py
Normal file
11
lib/api/permissions.py
Normal file
11
lib/api/permissions.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from rest_framework import permissions
|
||||||
|
|
||||||
|
|
||||||
|
class IsAuthorOrReadOnly(permissions.BasePermission):
|
||||||
|
|
||||||
|
def has_object_permission(self, request, view, obj):
|
||||||
|
# Read permissions are allowed to any request
|
||||||
|
if request.method in permissions.SAFE_METHODS:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return obj.author == request.user or request.user.is_superuser
|
||||||
0
lib/core/db/__init__.py
Normal file
0
lib/core/db/__init__.py
Normal file
0
lib/core/db/models/__init__.py
Normal file
0
lib/core/db/models/__init__.py
Normal file
31
lib/core/db/models/base.py
Normal file
31
lib/core/db/models/base.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
from functools import cached_property
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from lib.core.db.models.mixins import DateAware
|
||||||
|
|
||||||
|
|
||||||
|
class SharedPermissionBase(DateAware, models.Model):
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
view = models.BooleanField(default=True)
|
||||||
|
change = models.BooleanField(default=False)
|
||||||
|
delete = models.BooleanField(default=False)
|
||||||
|
add = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
# item = models.IntegerField(default=NotImplementedError('Must be defined in inheriting class'))
|
||||||
|
@cached_property
|
||||||
|
def perms(self):
|
||||||
|
perms = filter(None, [
|
||||||
|
'read' if self.view else False,
|
||||||
|
'change' if self.change else False,
|
||||||
|
'delete' if self.delete else False,
|
||||||
|
'add' if self.add else False,
|
||||||
|
])
|
||||||
|
return list(perms)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'{self.user.username} ({", ".join(self.perms)})'
|
||||||
35
lib/core/db/models/mixins.py
Normal file
35
lib/core/db/models/mixins.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from lib.middleware.current_user import get_current_user
|
||||||
|
|
||||||
|
|
||||||
|
class DateAware(models.Model):
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorAware(models.Model):
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, default=get_current_user)
|
||||||
|
|
||||||
|
|
||||||
|
class DescriptionAware(models.Model):
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
description = models.TextField(null=True, blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class NameAware(models.Model):
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
name = models.CharField(max_length=100, unique=True, db_index=True)
|
||||||
|
|
||||||
|
|
||||||
@@ -13,13 +13,15 @@ from django.utils.safestring import mark_safe
|
|||||||
from content.models import Question as QuestionContent
|
from content.models import Question as QuestionContent
|
||||||
from lib import get_current_user
|
from lib import get_current_user
|
||||||
from tablequizwiki.settings import BASE_DIR
|
from tablequizwiki.settings import BASE_DIR
|
||||||
|
from lib.core.db.models.mixins import AuthorAware, DateAware
|
||||||
|
|
||||||
|
|
||||||
class Quiz(models.Model):
|
class Quiz(AuthorAware, DateAware):
|
||||||
name = models.CharField(max_length=250)
|
name = models.CharField(max_length=250)
|
||||||
author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, default=get_current_user)
|
# author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, default=get_current_user)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
# created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
# updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@@ -117,14 +119,12 @@ class Quiz(models.Model):
|
|||||||
return str(BASE_DIR) + f'/filestore/csv/quiz.{self.id}.csv'
|
return str(BASE_DIR) + f'/filestore/csv/quiz.{self.id}.csv'
|
||||||
|
|
||||||
|
|
||||||
class Question(models.Model):
|
class Question(DateAware):
|
||||||
question = models.ForeignKey(QuestionContent, on_delete=models.RESTRICT, related_name='content_question')
|
question = models.ForeignKey(QuestionContent, on_delete=models.RESTRICT, related_name='content_question')
|
||||||
quiz = models.ForeignKey(Quiz, on_delete=models.CASCADE)
|
quiz = models.ForeignKey(Quiz, on_delete=models.CASCADE)
|
||||||
points = models.DecimalField(max_digits=10, decimal_places=1, default=1.0)
|
points = models.DecimalField(max_digits=10, decimal_places=1, default=1.0)
|
||||||
order = models.IntegerField(default=1)
|
order = models.IntegerField(default=1)
|
||||||
description = models.TextField(null=True, blank=True)
|
description = models.TextField(null=True, blank=True)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.question)
|
return str(self.question)
|
||||||
|
|||||||
@@ -12,4 +12,7 @@ django-colorfield
|
|||||||
django-thumbnails
|
django-thumbnails
|
||||||
matplotlib
|
matplotlib
|
||||||
django-jazzmin
|
django-jazzmin
|
||||||
requests
|
requests
|
||||||
|
django-rest-swagger
|
||||||
|
drf-yasg
|
||||||
|
django-guardian
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ INSTALLED_APPS = [
|
|||||||
'easy_thumbnails',
|
'easy_thumbnails',
|
||||||
'colorfield',
|
'colorfield',
|
||||||
'filer',
|
'filer',
|
||||||
|
'rest_framework_swagger',
|
||||||
|
# 'guardian',
|
||||||
|
'api',
|
||||||
'content',
|
'content',
|
||||||
'quiz',
|
'quiz',
|
||||||
]
|
]
|
||||||
@@ -103,6 +106,12 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
"""
|
||||||
|
AUTHENTICATION_BACKENDS = (
|
||||||
|
'django.contrib.auth.backends.ModelBackend', # this is default
|
||||||
|
'guardian.backends.ObjectPermissionBackend',
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/4.2/topics/i18n/
|
# https://docs.djangoproject.com/en/4.2/topics/i18n/
|
||||||
@@ -118,6 +127,7 @@ USE_TZ = True
|
|||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
||||||
|
|
||||||
|
STATICFILES_DIRS = [BASE_DIR / 'static']
|
||||||
STATIC_URL = 'static/'
|
STATIC_URL = 'static/'
|
||||||
|
|
||||||
# Default primary key field type
|
# Default primary key field type
|
||||||
@@ -187,3 +197,5 @@ THUMBNAILS = {
|
|||||||
# }
|
# }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
REST_FRAMEWORK = {'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema'}
|
||||||
|
|||||||
@@ -18,8 +18,12 @@ from django.contrib import admin
|
|||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
|
|
||||||
import quiz.urls as quizurls
|
import quiz.urls as quizurls
|
||||||
|
import api.urls as apiurls
|
||||||
|
import content.urls as contenturls
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('quiz/', include(quizurls))
|
path('quiz/', include(quizurls)),
|
||||||
|
path('api/', include(apiurls)),
|
||||||
|
path('', include(contenturls))
|
||||||
]
|
]
|
||||||
|
|||||||
41
templates/base.html
Normal file
41
templates/base.html
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>TablequizDB</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
||||||
|
<style type="text/css">
|
||||||
|
.input-group.input-group-unstyled input.form-control {
|
||||||
|
-webkit-border-radius: 4px;
|
||||||
|
-moz-border-radius: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.input-group-unstyled .input-group-addon {
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 0px;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<link href="https://ka-f.fontawesome.com/releases/v6.5.2/css/free.min.css?token=5f65fb5684" rel="stylesheet" />
|
||||||
|
<link href="https://ka-f.fontawesome.com/releases/v6.5.2/css/free-v4-shims.min.css?token=5f65fb5684" rel="stylesheet" />
|
||||||
|
<link href="https://ka-f.fontawesome.com/releases/v6.5.2/css/free-v5-font-face.min.css?token=5f65fb5684" rel="stylesheet" />
|
||||||
|
<link href="https://ka-f.fontawesome.com/releases/v6.5.2/css/free-v4-font-face.min.css?token=5f65fb5684" rel="stylesheet" />
|
||||||
|
{# <script src="https://kit.fontawesome.com/5f65fb5684.js" crossorigin="anonymous"></script> #}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container mt-2">
|
||||||
|
<header>
|
||||||
|
<h1>Tablequiz DB</h1>
|
||||||
|
<nav>
|
||||||
|
<a class="nav-item" href="/questions/">Questions</a>
|
||||||
|
<a class="nav-item" href="/quizes/">Quizes</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user